feat: add optimistic role update and IPC handler for member role changes

- Implemented optimistic UI updates in `TeamDetailView` to reflect member role changes immediately.
- Added IPC channel for updating member roles, including validation for team and member names.
- Enhanced tests to cover the new role update functionality, ensuring proper handling of valid and invalid inputs.
This commit is contained in:
iliya 2026-02-24 17:36:12 +02:00
parent 265becae2d
commit ad1ce7fc2c
2 changed files with 47 additions and 0 deletions

View file

@ -1133,6 +1133,12 @@ export const TeamDetailView = ({ teamName }: TeamDetailViewProps): React.JSX.Ele
setUpdatingRoleLoading(true);
try {
await updateMemberRole(teamName, memberName, role);
// Optimistically update local selectedMember to reflect new role
setSelectedMember((prev) => {
if (prev?.name !== memberName) return prev;
const normalized = typeof role === 'string' && role.trim() ? role.trim() : undefined;
return { ...prev, role: normalized };
});
} finally {
setUpdatingRoleLoading(false);
}

View file

@ -37,6 +37,7 @@ vi.mock('@preload/constants/ipcChannels', () => ({
TEAM_ADD_TASK_COMMENT: 'team:addTaskComment',
TEAM_ADD_MEMBER: 'team:addMember',
TEAM_REMOVE_MEMBER: 'team:removeMember',
TEAM_UPDATE_MEMBER_ROLE: 'team:updateMemberRole',
TEAM_GET_PROJECT_BRANCH: 'team:getProjectBranch',
TEAM_GET_ATTACHMENTS: 'team:getAttachments',
}));
@ -72,6 +73,7 @@ import {
TEAM_GET_ATTACHMENTS,
TEAM_GET_PROJECT_BRANCH,
TEAM_REMOVE_MEMBER,
TEAM_UPDATE_MEMBER_ROLE,
} from '../../../src/preload/constants/ipcChannels';
import {
initializeTeamHandlers,
@ -109,6 +111,7 @@ describe('ipc teams handlers', () => {
})),
addMember: vi.fn(async () => undefined),
removeMember: vi.fn(async () => undefined),
updateMemberRole: vi.fn(async () => ({ oldRole: undefined, changed: true })),
};
const provisioningService = {
prepareForProvisioning: vi.fn(async () => ({
@ -170,6 +173,7 @@ describe('ipc teams handlers', () => {
expect(handlers.has(TEAM_ADD_TASK_COMMENT)).toBe(true);
expect(handlers.has(TEAM_ADD_MEMBER)).toBe(true);
expect(handlers.has(TEAM_REMOVE_MEMBER)).toBe(true);
expect(handlers.has(TEAM_UPDATE_MEMBER_ROLE)).toBe(true);
});
it('returns success false on invalid sendMessage args', async () => {
@ -382,6 +386,42 @@ describe('ipc teams handlers', () => {
});
});
describe('updateMemberRole', () => {
it('calls service on valid input', async () => {
const handler = handlers.get(TEAM_UPDATE_MEMBER_ROLE)!;
const result = (await handler({} as never, 'my-team', 'alice', 'developer')) as {
success: boolean;
};
expect(result.success).toBe(true);
expect(service.updateMemberRole).toHaveBeenCalledWith('my-team', 'alice', 'developer');
});
it('normalizes null role to undefined', async () => {
const handler = handlers.get(TEAM_UPDATE_MEMBER_ROLE)!;
const result = (await handler({} as never, 'my-team', 'alice', null)) as {
success: boolean;
};
expect(result.success).toBe(true);
expect(service.updateMemberRole).toHaveBeenCalledWith('my-team', 'alice', undefined);
});
it('rejects invalid team name', async () => {
const handler = handlers.get(TEAM_UPDATE_MEMBER_ROLE)!;
const result = (await handler({} as never, '../bad', 'alice', 'dev')) as {
success: boolean;
};
expect(result.success).toBe(false);
});
it('rejects invalid member name', async () => {
const handler = handlers.get(TEAM_UPDATE_MEMBER_ROLE)!;
const result = (await handler({} as never, 'my-team', '../bad', 'dev')) as {
success: boolean;
};
expect(result.success).toBe(false);
});
});
describe('createTeam prompt validation', () => {
it('accepts valid prompt in team create request', async () => {
const handler = handlers.get(TEAM_CREATE)!;
@ -439,6 +479,7 @@ describe('ipc teams handlers', () => {
expect(handlers.has(TEAM_ADD_TASK_COMMENT)).toBe(false);
expect(handlers.has(TEAM_ADD_MEMBER)).toBe(false);
expect(handlers.has(TEAM_REMOVE_MEMBER)).toBe(false);
expect(handlers.has(TEAM_UPDATE_MEMBER_ROLE)).toBe(false);
expect(handlers.has(TEAM_GET_PROJECT_BRANCH)).toBe(false);
expect(handlers.has(TEAM_GET_ATTACHMENTS)).toBe(false);
});