fix(web): send document_duty on member add, update, and restore
Post-create role changes were silently dropping reviewer/approver because only project_role was persisted. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+27
-22
@@ -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<string | null>(null);
|
||||
const [projectMembersModalOpen, setProjectMembersModalOpen] = useState(false);
|
||||
const [projectMembersError, setProjectMembersError] = useState<string | null>(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<ProjectMember, 'joinedAt' | 'status'>) => {
|
||||
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 : '恢复成员失败');
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 = (
|
||||
|
||||
Reference in New Issue
Block a user