fix(cards): one server-authoritative permission read for every card editor write gate - #3032
Conversation
…itor The type selector's server-authoritative permission read (#2952) answered a question three sibling gates in the same editor were also asking of the same payload - and still answering with an omitted optional field read as "no". `canWrite` exposes the resolved answer so one read can serve all of them. Splitting the old `permissionDecides` in two is what makes that safe: whether an unresolved permission is worth a request (`readDecides`, still excluding an archived card and demo mode exactly as #2952 decided) is not the same question as whether the caller may write to the board. Restoring an archived card IS a write offered on an archived card, so folding the card's archive state into the shared answer would have disabled the one control an archived card exists to offer. `canEditType` keeps its exact previous meaning: the shared answer, plus the card not being archived.
…ssion A board payload cached before `canWrite` existed omits it, and the contract in types/board.ts treats only an explicit `false` as read-only. The parent selector, the archive/restore control and the assignment field each read that silence as "no", so after #2952 an authorized writer watched the type selector enable while three controls beside it stayed dead with nothing to do about it. All three now answer from the editor's single resolved read: - `CardParentField` takes `canWrite` as a required prop and no longer derives one from the payload, so no host can silently fall back to the inference that was the defect. - `CardArchiveAction` takes it as an optional override with an `undefined` default, the same idiom as `archived`: `BoardCardArchive`, which has not resolved anything server-side, keeps the payload-derived answer. - The assignment field's `read-only` input is the shared answer plus the card's archive state. Its own 403 lock (#2982) is untouched: the lock still releases only on `readOnly` turning writable, which remains a server-derived permission and never a successful participant read. Demo mode, a card whose board is not the loaded one, a viewer, an archived board and an archived card all keep exactly the behaviour they had. The permission message now names editing the board rather than changing the type, because one control's recovery now speaks for four.
One resolved read now gates four controls, not one. STATUS carries the shipped behaviour with its local evidence and its residual; the work-item type page keeps the mechanism next to the #2952 text it extends.
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
Review round 1 on #3032. The parent-field case set a board payload it no longer reads, and a comment that claimed the omitted-payload journey as its subject; the prop alone is what it proves, and the journey is covered in CardModal.spec.ts. The work-item type page now carries the archived-card exception that STATUS already recorded. No behaviour change.
Review round 1 - independent fresh-context adversarial pass, triagedThe Codex connector posted only a usage-limit notice on this PR, so no connector review will arrive. Fixed here (
|
Closes #3028.
Summary
PR #3026 (#2952) made the work-item type selector ask the server for the caller's board
write permission when the loaded
Boardpayload omits the optionalcanWritefield. Threesibling gates in the same open editor kept reading that silence as "no", so on a payload
cached before the field existed an authorized writer watched the type selector enable while
the parent selector, the archive/restore control and the assignment editor stayed dead.
One resolved read now answers for all four.
useCardTypePermissionexposescanWrite, the board-level answer.canEditTypekeeps itsexact previous meaning (
canWriteplus the card not being archived), and the oldpermissionDecidessplits in two: whether an unresolved permission is worth a request(
readDecides- still excluding an archived card and demo mode exactly as [Cards] Reconcile work-item type editing with older board payloads #2952 decided)is not the same question as whether the caller may write to the board. Restoring an
archived card is a write offered on an archived card, so folding the card's archive
state into the shared answer would have disabled the one control an archived card exists
to offer.
CardParentFieldtakescanWriteas a required prop and no longer derives one fromthe payload, so no host can silently fall back to the inference that was the defect.
CardArchiveActiontakes it as an optional override with anundefineddefault - the sameidiom as its
archivedprop - soBoardCardArchive, which has resolved nothingserver-side, keeps the payload-derived answer it has today.
CardModal's assignmentread-onlyinput is the shared answer plus the card's archivestate.
because one recovery control now speaks for four gates. Key paths and
data-testids areunchanged.
No client-derived ownership inference anywhere (
permissionsStore.canEditreadsBoardAccessrows, which board owners do not have), and every write stays server-authoritative regardless of
what these controls offer.
Preserved on purpose
one all keep exactly the behaviour they had - including that an archived card spends no
request ([Cards] Reconcile work-item type editing with older board payloads #2952's decision) and shows no recovery affordance.
the lock still releases only on
readOnlyturning writable, which remains a server-derivedpermission and never a successful participant read. The only new way for that input to turn
writable is this composable's own server read resolving from unknown to
true- evidence ofthe same kind, from the same endpoint.
#3030(the a11y/comment residuals of the same composable) is deliberately untouched: thepermission region stays in
CardModalForm.vueunder the samev-if, with the same testidsand i18n key paths, and the composable keeps its file name, so that slice does not have to
rebase over a rename. The only overlap is the wording of two strings.
Deviation from the acceptance list, deliberate
The issue asks for a spec where "archived -> all disabled". That holds for an archived
board (all four). For an archived card it holds for the three editable gates, but the
archive control keeps the permission the payload states, because its label there is Restore
card - disabling it would be a regression, not a gate, and the same acceptance list asks for
archived behaviour to be preserved. Both cases are specified.
Residual, pre-existing and unchanged
An archived card whose board payload omits
canWritestill spends no read, so its Restorestays disabled exactly as before this PR. Fixing that means asking for permission on archived
cards too, which reverses #2952's explicit "no request for an archived card" decision and puts
a recovery affordance under a disabled type selector; out of scope here.
Verification
Run in this worktree (
C:/wt/i3028), Node 24, at head5b9c98bbb:npx vitest --run --maxWorkers=2 src/tests/components/CardModal.spec.ts src/tests/composables/useCardTypePermission.spec.ts src/tests/components/board/CardParentField.spec.ts- 85 passed (3 files).8 failed - the six new gate/
canWritecases plus the two that prove the fix. Thepreservation cases (viewer, archived board, archived card + Restore) passed red-side too,
which is what makes them preservation cases.
npx vitest --run --maxWorkers=2over the ten neighbouring specs that mount thesecomponents (
AppShell,BoardCardArchive,CardArchiveAction,CardAssignmentField,CardModalAssignmentSave,ColumnLane,useCardModal,boardMutationCapabilityParity,PaperBoardManagement,PaperBoardView) - 322 passed (10 files).npm run typecheck- clean.npm run build- clean.node scripts/check-docs-governance.mjs,node scripts/check-doc-links.mjs- both pass.NOT run / NOT verified: the full frontend suite (OOMs on this box; hosted Frontend Unit is
the broad evidence), any backend test (no backend surface touched), Playwright/E2E, and real
browser or screen-reader behaviour. The es/it wording is a machine translation of the English
string, not a reviewed one.