Skip to content

Bulk-deleting remote assignments reports success when the server could not delete them #1685

Open

Description

@joshunrau

Deleting remote assignments in bulk shows a success notification and clears the selection even when the API could not delete some or all of them. POST /v1/assignments/bulk/delete deletes each assignment separately. For an outstanding one it first calls the gateway, and it catches each failure and reports that id in failedIds, still answering 200. useDeleteBulkAssignmentsMutation parses that result, and its own test says the ids are returned "so the caller can report them". But the hook's onSuccess shows a generic success notification, and DeleteRemoteAssignments.handleDelete ignores the result entirely. When the gateway is unreachable, the user is told the delete succeeded while every outstanding assignment survives, and its link still works for the subject.

Where

apps/web/src/components/DeleteRemoteAssignments/DeleteRemoteAssignments.tsx:98-109:

const handleDelete = () => {
  deleteMutation.mutate(
    { ids: [...selectedIds] },
    {
      onSuccess() {
        setSelectedIds(new Set());
        setIsConfirmOpen(false);
        onBack?.();
      }
    }
  );
};

apps/web/src/hooks/useDeleteBulkAssignmentsMutation.ts:16-20:

onSuccess(result) {
  addNotification({ type: 'success' });
  void queryClient.invalidateQueries({ queryKey: [ASSIGNMENTS_QUERY_KEY_PREFIX] });
  return result;
}

The server side is apps/api/src/assignments/assignments.service.ts:146-176 (deleteBulk collects failedIds and returns { deletedCount, failedIds }).

Reproduce

  1. With the gateway stopped (or unreachable from the API), open Group → Remote Assignments → Delete Assignments.
  2. Select one or more Outstanding assignments and confirm the delete.

Actual: a success notification appears and the selection clears. After the list refetches, the same assignments are still there, and nothing tells the user why.
Expected: when failedIds is non-empty, the user is told how many assignments could not be deleted (an error notification, not a success one), and the failed ones stay selected so they can retry.

Tests

  • apps/web/src/components/DeleteRemoteAssignments/__tests__/DeleteRemoteAssignments.test.tsx: it('should report the assignments the server could not delete, so a partial failure is not shown as success'), with the mutation resolving to { deletedCount: 0, failedIds: ['a1'] }.
  • testing/src/specs/remote-assignments-wizard.spec.ts: route **/v1/assignments/bulk/delete to a 200 with a non-empty failedIds, and assert an error message is shown instead of the success toast.

Suggested fix

Show the success notification only when failedIds is empty, moving that check out of the hook or making it conditional there. In handleDelete, read the resolved failedIds: when there are any, show an error naming the count and keep those ids selected. Clear the selection only for the ids that were deleted.

Activity

  1. added
    BugType: existing behavior is wrong
    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). No commit since this was filed touches the hook, the component or deleteBulk, and no open PR fixes it.

    The unit tests currently encode the bug, so they change with the fix:

    • apps/web/src/hooks/__tests__/useDeleteBulkAssignmentsMutation.test.ts:30 resolves every test to { deletedCount: 1, failedIds: ['assignment-2'] }, and :53-57 asserts { type: 'success' } against that result. The success test needs a consistent { deletedCount: 1, failedIds: [] } for ids: ['assignment-1'].
    • DeleteRemoteAssignments.test.tsx:13 types the per-call onSuccess as () => void, and the tests at :247-266 call it with no argument. Once handleDelete reads the result, they need to pass a DeleteBulkAssignmentsResult.

    Two gaps in the suggested fix

    1. failedIds does not cover every assignment that was not deleted. deleteBulk only loads rows that are still OUTSTANDING or EXPIRED (apps/api/src/assignments/assignments.service.ts:147-153). If the subject completes an assignment after the list loaded, or someone cancels or deletes it, its id is in neither deletedCount nor failedIds, and the fix as written still shows success. Treat the delete as a success only when deletedCount === ids.length, and report ids.length - deletedCount. Keep failedIds selected. The skipped ids leave the list when it refetches.
    2. Some failed ids can never succeed on a retry. The gateway answers 404 for an id it does not hold (apps/gateway/src/routers/api.router.ts:165-167). The API's axios client rejects that 404 (gateway.http.ts sets no validateStatus), so deleteBulk reports the id as failed. The API is left with an OUTSTANDING row and no gateway row when the gateway delete succeeds and the Mongo write after it fails (assignments.service.ts:161-163, and cancel at :203-206). The same happens when the gateway's SQLite database is recreated. Such a row then fails every delete and every cancel. Counting the gateway's own 404 as deleted fixes this. That is an API change, and it also reaches the synchronizer (gateway.synchronizer.ts:92, :216), so do it with Bulk assignment delete makes one gateway call and one database delete per assignment, serially, with no cap on the batch #1749. That issue's gateway bulk route "reports the ids it did not find", and those ids should count as deleted, not go into failedIds.

    Keep the 200. Mutations default to throwOnError: true (apps/web/AGENTS.md), so an error status would replace the page with the error boundary, as in #1671.

    Plan

    • useDeleteBulkAssignmentsMutation.ts: onSuccess(result, { ids }) always invalidates. It adds { type: 'success' } only when deletedCount === ids.length. Otherwise it adds { type: 'error', message: t({ en, es, fr }) } with the count.
    • DeleteRemoteAssignments.tsx handleDelete: always close the dialog. On full success, clear the selection and call onBack as today. Otherwise, set the selection to new Set(result.failedIds) and skip onBack, so Delete Selected (N) becomes the retry.

    Tests

    • Hook: it('should announce an error with the count when the server could not delete every assignment').
    • Component: the test in the issue, plus it('should keep only the failed assignments selected and stay on the page, so Delete Selected retries them').
    • e2e: nothing in testing/ exercises bulk/delete. The header of remote-assignments-wizard.spec.ts says no assignment is created, so add a new testing/src/specs/remote-assignments-delete.spec.ts. Seed an assignment in a dedicated group with api.createAssignment (testing/src/support/api-client.ts:54), open /group/remote-assignments?mode=delete, and route **/v1/assignments/bulk/delete to { deletedCount: 0, failedIds: [id] }. Assert that the error shows and that delete-select-assignment-<id> stays checked. Cover gap 1 against the real server: tick the assignment, cancel it with PATCH /v1/assignments/:id { status: 'CANCELED' } (api-client.ts needs a helper for this), confirm, and assert the error. Put the delete-mode locators in RemoteAssignmentsPage, because Bulk assignment delete makes one gateway call and one database delete per assignment, serially, with no cap on the batch #1749's planned e2e needs them too.

    Landing

    #1679, #1682 and #1757 also change DeleteRemoteAssignments.tsx, so land them together or in sequence. #1811 drops rootStyle from the same DataTable and adds members to remote-assignments.page.ts. Expect textual conflicts there, not behavioural ones. #1749 keeps the { deletedCount, failedIds } shape, so this can land first.

    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: MediumReal value; schedule normally

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions