Skip to content

fix: repopulate corrupts repeat groups when rows are deleted on the server (#1940)#2008

Open
clinnygee wants to merge 1 commit into
mainfrom
issue/1940
Open

fix: repopulate corrupts repeat groups when rows are deleted on the server (#1940)#2008
clinnygee wants to merge 1 commit into
mainfrom
issue/1940

Conversation

@clinnygee

Copy link
Copy Markdown
Collaborator

Fixes the re-opened part of #1940, reported by Liam: when rows are deleted on the server between repopulations, the repopulate dialog correctly identifies the deletions, but applying them corrupts the form — deleted rows are resurrected/duplicated, and the internal mark-as-deleted extension leaks into the QuestionnaireResponse.

Root cause

The Value Restoration step in getFilteredItemsToRepopulate (itemsToRepopulateSelector.ts) did two things in one forward loop over serverQRItems:

  1. Fill unselected (sparse) slots from current values
  2. splice out items marked with the mark-as-deleted extension

Splicing in-place while iterating forward without adjusting i meant:

  • The item after a splice shifted into an already-visited index, so its deletion marker was never checked — leaking the marker extension into the response.
  • The vacated tail slot became undefined again, so the fill branch re-filled it from currentQRItems — resurrecting the row the user had just accepted as deleted.

Concrete trace — form has rows [A, B, C]; A and B deleted on the server (server returns [C]); user accepts both deletions in the dialog:

  • Detection aligns matched-first: current = [C, A, B], server = [C] — dialog correctly shows "A removed", "B removed" ✓
  • Selection filtering builds sparse server = [_, A+del, B+del]
  • Restoration loop: i=0 fills C → i=1 splices A+del (B+del shifts to index 1, skipped) → i=2 now empty, re-filled with B from current
  • Applied result: [C, B+del, B] instead of the promised [C]

Fix

Split the loop into two passes: fill all unselected slots first, then filter() out every item carrying the mark-as-deleted extension. This also strips the internal marker so it never enters the QuestionnaireResponse, and drops any holes left in the sparse array.

Tests

Added regression tests modelling the scenario above:

  • accepting all deletions yields exactly the rows the dialog promised
  • the mark-as-deleted extension does not leak into the result
  • unaccepted deletions keep their rows in the form

All three fail against the previous code and pass with the fix (verified by stash-reverting the fix and re-running).

🤖 Generated with Claude Code

…erver (issue #1940)

The Value Restoration step in getFilteredItemsToRepopulate spliced accepted
deletions out of serverQRItems inside the same forward loop that fills
unselected slots from current values. Splicing shifted unvisited deletion
markers into already-visited indices (so they leaked into the
QuestionnaireResponse) and re-opened their vacated tail slots to be re-filled
from current values, resurrecting the deleted rows as duplicates.

Split the loop into two passes: fill unselected slots first, then filter out
all items marked with the mark-as-deleted extension. This also strips the
internal marker extension from the final response and drops sparse-array holes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant