Skip to content

Clicking Cancel in a subject's assignment sheet cancels the remote assignment without confirmation #1693

Open

Description

@joshunrau

The sheet that opens from a subject's Assignments tab has a red footer button labelled Cancel (t('core.cancel'), "Cancel" / "Cancelar" / "Annuler"). Everywhere else in the app that word dismisses a dialog, but here it cancels the remote assignment: clicking it immediately sends PATCH /v1/assignments/:id with status: 'CANCELED', and the API then deletes the assignment from the gateway (assignments.service.ts:203-205). There is no confirmation step and no way to undo it. A clinician who clicks "Cancel" to close the sheet kills the link the participant was sent, and the participant can no longer complete the assignment. The button beside it, "Resend Notification", is permanently disabled with no explanation, so the only enabled action in the footer is the destructive one.

Where

apps/web/src/routes/_app/datahub/subjects/$subjectId/assignments.tsx:66-79:

<Sheet.Footer>
  <Button
    className="w-full text-sm"
    disabled={assignment?.status !== 'OUTSTANDING'}
    label={t('core.cancel')}
    variant="danger"
    onClick={() => onCancel(assignment!)}
  />
  <Button
    disabled
    className="w-full text-sm whitespace-nowrap"
    label={t('datahub.assignments.resendNotification')}
    variant="secondary"
  />
</Sheet.Footer>

and assignments.tsx:174-177, which cancels with no confirmation:

onCancel={({ id }) => {
  updateAssignmentMutation.mutate({ data: { status: 'CANCELED' }, params: { id } });
  setIsEditSliderOpen(false);
}}

Reproduce

  1. With the gateway enabled, start a session and create a remote assignment for the subject.
  2. Open View Current Subject → Assignments and click the outstanding assignment's row.
  3. Click Cancel in the sheet footer, intending to close the sheet.

Actual: the sheet closes, the assignment's status becomes Canceled, and the participant's link stops working. Nothing asked for confirmation.
Expected: the button says what it does ("Cancel assignment"), clicking it opens a confirmation that states the link will stop working, and only confirming cancels it. The always-disabled "Resend Notification" button is not shown.

Tests

  • apps/web/src/routes/_app/datahub/subjects/$subjectId/__tests__/assignments.test.tsx: it('should ask for confirmation before cancelling an assignment, so closing the sheet cannot cancel it by accident').
  • testing/src/specs/subject-detail.spec.ts: open an outstanding assignment, click the cancel action, dismiss the confirmation, and assert the row still reads Outstanding.

Suggested fix

Relabel the button with a specific action (e.g. "Cancel assignment" in all three languages), route onCancel through a confirmation Dialog like the archive-user flow in admin/users/index.tsx, and remove the disabled "Resend Notification" button until the feature exists.

Activity

  1. added
    BugType: existing behavior is wrong
    Priority: HighSignificant or frequently hit user impact; target the next release or two
    Difficulty: LowIsolated change, about 2 hours or less
    Area: Webapps/web (clinician-facing React SPA)
    on Oct 7, 2026
  2. joshunrau commented on Oct 26, 2026

    @joshunrau
    CollaboratorAuthor

    Still reproduces on main (d86740e), and no open PR touches assignments.tsx. It is the only variant="danger" button in apps/web/src (tests and stories aside) that mutates on the first click. Every other one opens a Dialog or sits inside one.

    On the suggested fix

    • apps/web/src/routes/_app/admin/groups/index.tsx:156-268 is a closer model than admin/users: a Sheet whose footer Delete is a Dialog.Trigger (:218-222) for a Dialog nested inside Sheet.Content.
    • Keep core.cancel out of the whole flow. The confirmation needs a dismiss button, and "Cancel" next to "Cancel assignment" brings the ambiguity back. Use "Keep assignment" / "Cancel assignment", or a core.yes / core.no pair under a title that names the action. If "Cancel assignment" labels both the trigger and the confirm button, it is reused, so per apps/web/AGENTS.md it goes in datahub.json rather than inline, with the "asignación" / "assignation" wording the sheet's other strings use.
    • Resend Notification has nothing left to wait for. The sheet body already renders AssignmentEmailForm (assignments.tsx:57-63), which emails the link when mail is enabled. Delete the button and datahub.assignments.resendNotification (datahub.json:28-32), which nothing else reads. Put a secondary t('core.close') in its place, as the post-create sheet has (session/remote-assignment.tsx:67-74), so the footer has an unambiguous way out.
    • Disable the confirm button while isPending and close on success, as DeleteRemoteAssignments.tsx:98-109 and :269 do. A second PATCH is not a no-op: updateById (apps/api/src/assignments/assignments.service.ts:198-209) never checks the current status, so it calls deleteRemoteAssignment again, the gateway answers 404 (apps/gateway/src/routers/api.router.ts:165-167), and throwOnError: true (apps/web/src/services/react-query.ts:5-7) sends the failure to the error boundary.

    Tests

    • assignments.test.tsx:209-215 pins the one-click cancel. Replace it with two tests: the trigger opens the confirmation without calling mutate, and confirming calls it. The tests at :206 and :212 query { name: 'Cancel' }, which Testing Library matches exactly, so both break on the relabel. The useUpdateAssignment mock (:29) returns only { mutate }. It needs isPending, and mutate has to call the onSuccess it is passed for a close-on-success test.
    • testing/src/specs/subject-detail.spec.ts:108-112 fails as soon as the confirmation exists. Extend it: dismiss, assert the row still reads Outstanding and that no PATCH /v1/assignments/ was sent (page.on('request'), as instrument-completion.spec.ts:252 does), then confirm and expect Canceled.
    • SubjectAssignmentsPage.cancelButton (testing/src/pages/_app/datahub/subjects/$subjectId/assignments.page.ts:14) is getByRole('button', { name: 'Cancel' }). Playwright matches that as a case-insensitive substring, so it would quietly match the relabelled trigger or a "Cancel" dismiss button. testing/AGENTS.md wants data-testid selectors: add cancel-assignment and confirm-cancel-assignment.

    #1730 (:128-134) and #1757 (:154-169) also edit this route and its unit test, on different lines, so they can land in any order.

    Separate from this issue, and worth its own: an intended cancel can lose a submission. The gateway DELETE (api.router.ts:158-173) deletes the row, encryptedData included, whatever its status. The API learns of a completion only on the next sync pass (GATEWAY_REFRESH_INTERVAL, 10 s in .env.template:56). Until then the row still reads Outstanding and Cancel is enabled, and a cancel deletes the participant's answers before they are imported. deleteBulk (assignments.service.ts:158-163) has the same window. The gateway cannot simply refuse to delete a COMPLETE row, because the sync deletes it the same way after importing (gateway.synchronizer.ts:216).

    Triage note written with Claude Code. Code references point at main @ d86740e.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Area: Webapps/web (clinician-facing React SPA)BugType: existing behavior is wrongDifficulty: LowIsolated change, about 2 hours or lessPriority: HighSignificant or frequently hit user impact; target the next release or two

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions