From 985fc0f9315d455e57a7de90638a2ff257a31fac Mon Sep 17 00:00:00 2001 From: weichen Date: Thu, 15 Oct 2026 09:06:24 +0000 Subject: [PATCH] Adopt the server duty draft when a save result is unknown Why: a lost write response was keeping the old duty version, so the next save replayed it, and a busy save could stay locked if another request advanced the sequence. Evidence: SELF vitest duty-draft 6 passed, tsc passed, knip 43, Playwright duty flows 3 passed. Impact: an unknown save reloads server roles, confirmation resets, and busy clears even if a later read is discarded. Refs: docs/proposals/2026-10-15-rdm-duty-matrix-rework.md Co-authored-by: Cursor --- src/components/duties/RdmDutiesSection.tsx | 36 ++++++++++++++-------- src/duties/draft.ts | 5 +-- test/duty-draft.test.ts | 12 ++++++++ 3 files changed, 39 insertions(+), 14 deletions(-) diff --git a/src/components/duties/RdmDutiesSection.tsx b/src/components/duties/RdmDutiesSection.tsx index e90087f..b7e8523 100644 --- a/src/components/duties/RdmDutiesSection.tsx +++ b/src/components/duties/RdmDutiesSection.tsx @@ -69,10 +69,11 @@ export function RdmDutiesSection({ project }: { project: Project }) { const [legacy, setLegacy] = useState([]) const [legacyOpen, setLegacyOpen] = useState(false) const seq = useRef(0) + const op = useRef(0) const docsRef = useRef(docs) docsRef.current = docs - const load = async (cursor: string, request = ++seq.current) => { + const load = async (cursor: string, request = ++seq.current, replaceDirty = false) => { const seenMembers: DutyMember[] = [] const seenCells: MatrixOut['cells'] = [] let pageDocs: MatrixDoc[] = [] @@ -96,7 +97,7 @@ export function RdmDutiesSection({ project }: { project: Project }) { setMembers(seenMembers) setCanAssign(assignable) setNextDoc(nextDocument) - setDocs(current => absorbServerDocs(current, pageDocs, seenCells)) + setDocs(current => absorbServerDocs(current, pageDocs, seenCells, replaceDirty)) } catch (error) { if (request !== seq.current) return setErr(String(error)) @@ -118,6 +119,7 @@ export function RdmDutiesSection({ project }: { project: Project }) { const onCell = (id: string, owner: string, duty: CellDuty) => { setPreview([]) + setConfirm(false) setDocs(current => editCell(current, id, owner, duty)) } @@ -136,7 +138,9 @@ export function RdmDutiesSection({ project }: { project: Project }) { return } const request = ++seq.current + const ticket = ++op.current setBusy(true); setErr(''); setNotice('') + let sent = false try { const looked = await api.rdmProjectCommand(project, 'rdm.duty.preview', dutyPreviewPayload(projectId, payload.docs)) if (request !== seq.current) return @@ -149,18 +153,25 @@ export function RdmDutiesSection({ project }: { project: Project }) { setErr('在途审签将失效。确认后再保存。') return } + sent = true await api.rdmProjectCommand(project, 'rdm.review.assign.doc.batch', dutyPreviewPayload(projectId, savePayload(docsRef.current, confirm).docs)) if (request !== seq.current) return setNotice('已保存文档职责') setPreview([]) + setConfirm(false) setDocs(current => current.map(doc => ({ ...doc, dirty: false }))) await load(docCursor, request) } catch (error) { if (request !== seq.current) return + if (!sent) { + setErr(String(error)) + return + } setErr(`保存结果未确认:${error}`) - await load(docCursor, request) + setConfirm(false) + await load(docCursor, request, true) } finally { - if (request === seq.current) setBusy(false) + if (ticket === op.current) setBusy(false) } } @@ -177,6 +188,7 @@ export function RdmDutiesSection({ project }: { project: Project }) { const applyIdentical = async () => { const request = ++seq.current + const ticket = ++op.current setBusy(true) try { await api.rdmProjectCommand(project, 'rdm.duty.legacy.apply', dutyLegacyPayload(projectId)) @@ -188,7 +200,7 @@ export function RdmDutiesSection({ project }: { project: Project }) { if (request !== seq.current) return setErr(String(error)) } finally { - if (request === seq.current) setBusy(false) + if (ticket === op.current) setBusy(false) } } @@ -205,10 +217,10 @@ export function RdmDutiesSection({ project }: { project: Project }) {
- { setDocCursor(''); setQuery(event.target.value) }} /> - { setDocCursor(''); setTypeKey(event.target.value) }} /> - { setDocCursor(''); setStage(event.target.value) }} /> - { setDocCursor(''); setQuery(event.target.value) }} /> + { setDocCursor(''); setTypeKey(event.target.value) }} /> + { setDocCursor(''); setStage(event.target.value) }} /> +