diff --git a/web/src/App.tsx b/web/src/App.tsx index bfab2c0d..84c5f408 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -34,17 +34,17 @@ import { removeProjectMember, updateProjectMember, } from './api/projectMembers'; -import { ensureCreatedProjectWithMembers } from './lib/createProjectMembersPersist'; +import { + buildManagedMemberUpsertPayload, + ensureCreatedProjectWithMembers, +} from './lib/createProjectMembersPersist'; import { beginCreateProjectSubmit, endCreateProjectSubmit, resolveCreateProjectSubmitPhase, type CreateProjectSubmitPhase, } from './lib/createProjectSubmitGuard'; -import { - mapApiMembersToProjectMembers, - toApiProjectRole, -} from './lib/projectMemberAdapter'; +import { mapApiMembersToProjectMembers } from './lib/projectMemberAdapter'; import { mergeWorkflowDocumentsFromServer } from './lib/deliverableSync'; import { resolveCurrentDocRole } from './lib/currentDocRole'; import { canAddBlockComment } from './lib/commentPolicy'; @@ -645,6 +645,7 @@ export default function App() { const [sessionSyncNotice, setSessionSyncNotice] = useState(null); const [projectMembersModalOpen, setProjectMembersModalOpen] = useState(false); const [projectMembersError, setProjectMembersError] = useState(null); + const [workflowDocsEpoch, setWorkflowDocsEpoch] = useState(0); const [projectSettingsOpen, setProjectSettingsOpen] = useState(false); const [workflowGovernanceEnabled, setWorkflowGovernanceEnabled] = useState(false); @@ -1736,7 +1737,7 @@ export default function App() { return () => { active = false; }; - }, [session, sessionValidated, currentProjectId, currentUser.id, currentUser.name]); + }, [session, sessionValidated, currentProjectId, currentUser.id, currentUser.name, workflowDocsEpoch]); // 文档占用是多人共享运行态:定期从服务端同步,避免其他标签页/成员开始编辑后本页无感知。 useEffect(() => { @@ -3772,11 +3773,15 @@ export default function App() { const handleAddProjectMember = async (newMember: Omit) => { if (!currentProject) return; try { - await addProjectMember(currentProject.id, { - user_id: newMember.userId, - project_role: toApiProjectRole(newMember.role, newMember.userId, currentProject.creatorId), - }); + await addProjectMember( + currentProject.id, + buildManagedMemberUpsertPayload( + { userId: newMember.userId, role: newMember.role }, + currentProject.creatorId, + ), + ); await syncProjectMembersFromServer(currentProject.id, currentProject.creatorId); + setWorkflowDocsEpoch((n) => n + 1); const newSessId = buildPrivateSessionId(`p${currentProject.id}`, newMember.userId); setSessions((prev) => { @@ -3819,11 +3824,12 @@ export default function App() { const handleUpdateProjectMemberRole = async (userId: number, newRole: ProjectMemberRole) => { if (!currentProject) return; try { - await updateProjectMember(currentProject.id, { - user_id: userId, - project_role: toApiProjectRole(newRole, userId, currentProject.creatorId), - }); + await updateProjectMember( + currentProject.id, + buildManagedMemberUpsertPayload({ userId, role: newRole }, currentProject.creatorId), + ); await syncProjectMembersFromServer(currentProject.id, currentProject.creatorId); + setWorkflowDocsEpoch((n) => n + 1); } catch (err) { setProjectMembersError(err instanceof Error ? err.message : '更新成员角色失败'); } @@ -3843,16 +3849,15 @@ export default function App() { if (!currentProject) return; try { const member = currentProject.members.find((m) => m.userId === userId); - await updateProjectMember(currentProject.id, { - user_id: userId, - project_role: toApiProjectRole( - member?.role ?? 'member', - userId, - currentProject.creatorId + await updateProjectMember( + currentProject.id, + buildManagedMemberUpsertPayload( + { userId, role: member?.role ?? 'member', status: 'active' }, + currentProject.creatorId, ), - status: 'active', - }); + ); await syncProjectMembersFromServer(currentProject.id, currentProject.creatorId); + setWorkflowDocsEpoch((n) => n + 1); } catch (err) { setProjectMembersError(err instanceof Error ? err.message : '恢复成员失败'); } diff --git a/web/src/lib/createProjectMembersPersist.test.ts b/web/src/lib/createProjectMembersPersist.test.ts index 78f1b85d..16aaedb5 100644 --- a/web/src/lib/createProjectMembersPersist.test.ts +++ b/web/src/lib/createProjectMembersPersist.test.ts @@ -2,6 +2,7 @@ import test from 'node:test'; import assert from 'node:assert/strict'; import { buildInitialMemberUpsertPayload, + buildManagedMemberUpsertPayload, ensureCreatedProjectWithMembers, persistInitialProjectMembers, selectNonCreatorInitialMembers, @@ -21,6 +22,33 @@ test('selectNonCreatorInitialMembers skips creator only', () => { ); }); +test('buildManagedMemberUpsertPayload sends document_duty on add/update/restore', () => { + assert.deepEqual(buildManagedMemberUpsertPayload({ userId: 171, role: 'reviewer' }, 170), { + user_id: 171, + project_role: 'member', + document_duty: 'reviewer', + }); + assert.deepEqual(buildManagedMemberUpsertPayload({ userId: 4, role: 'approver' }, 170), { + user_id: 4, + project_role: 'member', + document_duty: 'approver', + }); + assert.deepEqual( + buildManagedMemberUpsertPayload({ userId: 171, role: 'reviewer', status: 'active' }, 170), + { + user_id: 171, + project_role: 'member', + document_duty: 'reviewer', + status: 'active', + }, + ); + assert.deepEqual(buildManagedMemberUpsertPayload({ userId: 170, role: 'owner' }, 170), { + user_id: 170, + project_role: 'owner', + document_duty: 'author', + }); +}); + test('buildInitialMemberUpsertPayload maps reviewer UI role to member API role', () => { assert.deepEqual(buildInitialMemberUpsertPayload({ userId: 99, userName: 'B', role: 'reviewer' }, 86), { user_id: 99, diff --git a/web/src/lib/createProjectMembersPersist.ts b/web/src/lib/createProjectMembersPersist.ts index ff918702..9ff79c7a 100644 --- a/web/src/lib/createProjectMembersPersist.ts +++ b/web/src/lib/createProjectMembersPersist.ts @@ -27,11 +27,25 @@ export function buildInitialMemberUpsertPayload( member: InitialProjectMemberInput, creatorUserId: number, ): UpsertProjectMemberPayload { - return { - user_id: member.userId, - project_role: toApiProjectRole(member.role, member.userId, creatorUserId), - document_duty: documentDutyFromMemberRole(member.role), + return buildManagedMemberUpsertPayload( + { userId: member.userId, role: member.role }, + creatorUserId, + ); +} + +export function buildManagedMemberUpsertPayload( + input: { userId: number; role: ProjectMemberRole; status?: 'active' | 'exited' }, + creatorUserId: number, +): UpsertProjectMemberPayload { + const payload: UpsertProjectMemberPayload = { + user_id: input.userId, + project_role: toApiProjectRole(input.role, input.userId, creatorUserId), + document_duty: documentDutyFromMemberRole(input.role), }; + if (input.status) { + payload.status = input.status; + } + return payload; } export type AddProjectMemberFn = (