Refresh the roster and workflow documents only after the member write succeeds, so a failed add does not invent a local session. Co-authored-by: Cursor <cursoragent@cursor.com>
153 lines
5.4 KiB
TypeScript
153 lines
5.4 KiB
TypeScript
import type { Dispatch, SetStateAction } from 'react';
|
|
import {
|
|
addProjectMember,
|
|
removeProjectMember,
|
|
updateProjectMember,
|
|
} from '../api/projectMembers';
|
|
import { buildPrivateSessionId } from '../data/storage';
|
|
import { afterProjectMemberPersisted } from '../lib/afterProjectMemberPersisted';
|
|
import { buildManagedMemberUpsertPayload } from '../lib/createProjectMembersPersist';
|
|
import type {
|
|
ChatMessage,
|
|
ProjectInfo,
|
|
ProjectMember,
|
|
ProjectMemberRole,
|
|
ProjectSession,
|
|
} from '../types/workbench';
|
|
|
|
export interface ProjectMemberActionsInput {
|
|
currentProject: ProjectInfo | undefined;
|
|
syncProjectMembersFromServer: (projectId: number, creatorId: number) => Promise<unknown>;
|
|
setWorkflowDocsEpoch: Dispatch<SetStateAction<number>>;
|
|
setSessions: Dispatch<SetStateAction<ProjectSession[]>>;
|
|
setMessages: Dispatch<SetStateAction<ChatMessage[]>>;
|
|
setProjectMembersError: Dispatch<SetStateAction<string | null>>;
|
|
}
|
|
|
|
export interface ProjectMemberActions {
|
|
handleAddProjectMember: (newMember: Omit<ProjectMember, 'joinedAt' | 'status'>) => Promise<void>;
|
|
handleUpdateProjectMemberRole: (userId: number, newRole: ProjectMemberRole) => Promise<void>;
|
|
handleRemoveProjectMember: (userId: number) => Promise<void>;
|
|
handleRestoreProjectMember: (userId: number) => Promise<void>;
|
|
}
|
|
|
|
export function useProjectMemberActions(input: ProjectMemberActionsInput): ProjectMemberActions {
|
|
const {
|
|
currentProject,
|
|
syncProjectMembersFromServer,
|
|
setWorkflowDocsEpoch,
|
|
setSessions,
|
|
setMessages,
|
|
setProjectMembersError,
|
|
} = input;
|
|
|
|
const handleAddProjectMember = async (newMember: Omit<ProjectMember, 'joinedAt' | 'status'>) => {
|
|
if (!currentProject) return;
|
|
try {
|
|
await addProjectMember(
|
|
currentProject.id,
|
|
buildManagedMemberUpsertPayload(
|
|
{ userId: newMember.userId, role: newMember.role },
|
|
currentProject.creatorId,
|
|
),
|
|
);
|
|
await afterProjectMemberPersisted({
|
|
syncMembers: () => syncProjectMembersFromServer(currentProject.id, currentProject.creatorId),
|
|
refreshWorkflowDocs: () => setWorkflowDocsEpoch((n) => n + 1),
|
|
});
|
|
|
|
const newSessId = buildPrivateSessionId(`p${currentProject.id}`, newMember.userId);
|
|
setSessions((prev) => {
|
|
const exists = prev.some(
|
|
(s) => s.projectId === currentProject.id && s.userId === newMember.userId,
|
|
);
|
|
if (exists) return prev;
|
|
return [
|
|
{
|
|
sessionId: newSessId,
|
|
userId: newMember.userId,
|
|
projectId: currentProject.id,
|
|
title: currentProject.title,
|
|
deviceType: currentProject.deviceType,
|
|
stage: 'V0.1',
|
|
stageLabel: 'V0.1 需求摸底',
|
|
messageCount: 1,
|
|
updatedAt: '刚刚',
|
|
},
|
|
...prev,
|
|
];
|
|
});
|
|
|
|
setMessages((prev) => [
|
|
...prev,
|
|
{
|
|
id: `m-join-${newMember.userId}-${Date.now()}`,
|
|
sessionId: newSessId,
|
|
role: 'system',
|
|
content: `【项目协同通知】您已被项目创建人 ${currentProject.creatorName} 加入项目《${currentProject.title}》,并分配项目角色为【${newMember.role}】。`,
|
|
timestamp: new Date().toTimeString().substring(0, 5),
|
|
},
|
|
]);
|
|
} catch (err) {
|
|
setProjectMembersError(err instanceof Error ? err.message : '添加成员失败');
|
|
throw err;
|
|
}
|
|
};
|
|
|
|
const handleUpdateProjectMemberRole = async (userId: number, newRole: ProjectMemberRole) => {
|
|
if (!currentProject) return;
|
|
try {
|
|
await updateProjectMember(
|
|
currentProject.id,
|
|
buildManagedMemberUpsertPayload({ userId, role: newRole }, currentProject.creatorId),
|
|
);
|
|
await afterProjectMemberPersisted({
|
|
syncMembers: () => syncProjectMembersFromServer(currentProject.id, currentProject.creatorId),
|
|
refreshWorkflowDocs: () => setWorkflowDocsEpoch((n) => n + 1),
|
|
});
|
|
} catch (err) {
|
|
setProjectMembersError(err instanceof Error ? err.message : '更新成员角色失败');
|
|
}
|
|
};
|
|
|
|
const handleRemoveProjectMember = async (userId: number) => {
|
|
if (!currentProject) return;
|
|
try {
|
|
await removeProjectMember(currentProject.id, userId);
|
|
await afterProjectMemberPersisted({
|
|
syncMembers: () => syncProjectMembersFromServer(currentProject.id, currentProject.creatorId),
|
|
refreshWorkflowDocs: () => setWorkflowDocsEpoch((n) => n + 1),
|
|
});
|
|
} catch (err) {
|
|
setProjectMembersError(err instanceof Error ? err.message : '移出成员失败');
|
|
}
|
|
};
|
|
|
|
const handleRestoreProjectMember = async (userId: number) => {
|
|
if (!currentProject) return;
|
|
try {
|
|
const member = currentProject.members.find((m) => m.userId === userId);
|
|
await updateProjectMember(
|
|
currentProject.id,
|
|
buildManagedMemberUpsertPayload(
|
|
{ userId, role: member?.role ?? 'member', status: 'active' },
|
|
currentProject.creatorId,
|
|
),
|
|
);
|
|
await afterProjectMemberPersisted({
|
|
syncMembers: () => syncProjectMembersFromServer(currentProject.id, currentProject.creatorId),
|
|
refreshWorkflowDocs: () => setWorkflowDocsEpoch((n) => n + 1),
|
|
});
|
|
} catch (err) {
|
|
setProjectMembersError(err instanceof Error ? err.message : '恢复成员失败');
|
|
}
|
|
};
|
|
|
|
return {
|
|
handleAddProjectMember,
|
|
handleUpdateProjectMemberRole,
|
|
handleRemoveProjectMember,
|
|
handleRestoreProjectMember,
|
|
};
|
|
}
|