Repository navigation
Description
Ticking a row on page 2 or later of the bulk remote-assignment subject picker, or of the Delete Assignments table, sends the table back to page 1. Both components rebuild their rows array on every render, and every selection change re-renders them. libui's DataTable passes each new data array to TanStack's table, whose core row model then fires _autoResetPageIndex. Neither table sets autoResetPageIndex or manualPagination, so the page index is reset to 0 after each click. libui only shows 10 rows per page, so in any group with more than 10 subjects or assignments, every tick past the first page throws the user back to the start. They have to navigate back after each click, and can easily tick the wrong row once the page has moved under them.
Where
apps/web/src/components/BulkRemoteAssignmentWizard/SourceStep.tsx:106-111 (rebuilt every render, passed as data={rows} at :295):
const rows: PickerRow[] = subjects.map((subject) => ({
dateOfBirth: subject.dateOfBirth ? toBasicISOString(subject.dateOfBirth) : t({ en: 'NULL', es: 'NULO', fr: 'NUL' }),
id: subject.id,
sex: subject.sex ?? '',
subject: removeSubjectIdScope(subject.id).slice(0, subjectIdDisplayLength)
}));apps/web/src/components/DeleteRemoteAssignments/DeleteRemoteAssignments.tsx:58-73 (same pattern, data={rows} at :223):
const instrumentMap = new Map<string, UnilingualInstrumentInfo>(/* ... */);
const deletableAssignments = (assignmentsQuery.data ?? []).filter(/* ... */);
const rows: AssignmentRow[] = deletableAssignments.map((assignment) => ({ /* ... */ }));Each checkbox's onCheckedChange / onRowClick updates the selection state, which re-renders the component, produces a new rows array, and leads to table.resetPageIndex() in @tanstack/table-core (getCoreRowModel → _autoResetPageIndex).
Reproduce
- In a group with at least 11 subjects, open Group → Remote Assignments → Create Assignments.
- On "Select Subjects", go to page 2 of the table and tick a subject.
Actual: the subject is selected, but the table jumps back to page 1. (Confirmed in a unit test with 25 subjects: after going to page 2 and ticking S012, the visible rows were S000–S009.) The Delete Assignments table behaves the same way with more than 10 deletable assignments.
Expected: the table stays on page 2 and the user can keep ticking rows there.
Tests
apps/web/src/components/BulkRemoteAssignmentWizard/__tests__/SourceStep.test.tsx:it('should stay on the current page when a subject is ticked, so a selection past page one does not bounce back to the start'). Render with a stateful wrapper aroundselectedIdsand more than 10 subjects.apps/web/src/components/DeleteRemoteAssignments/__tests__/DeleteRemoteAssignments.test.tsx: the same for an assignment on page 2.testing/src/specs/remote-assignments-wizard.spec.ts: seed more than 10 subjects, tick one on page 2, and assert page 2 is still shown.
Suggested fix
Memoize rows with useMemo so the array identity changes only when the source data does (subjects, subjectIdDisplayLength, the language in SourceStep; assignmentsQuery.data, instrumentInfoQuery.data, subjectIdDisplayLength in DeleteRemoteAssignments). Selection then never changes data.
Activity
joshunrau commentedon Oct 26, 2026
Still reproduces on
main(d86740e). The cited lines have not moved.Why libui does not catch it
DouglasNeuroInformatics/libui#106 (in the installed 6.16.1) stopped
reset()from rebuilding table state. Its regression test passes adataarray defined outside the component, and its "Known gaps" section names this exact case: an unmemoizeddatastill tripsautoResetPageIndex, and the proper fix isautoResetPageIndex: falseplus an explicit page-reset policy in the store.DataTablePropsdoes not exposeautoResetPageIndex, so we cannot set it from here.src/components/DataTable/is identical in libui 6.18.0, and no libui issue tracks the gap.Notes on the
useMemofixrows.columnshas to stay inline because the checkbox cells close overselected/selectedIds, which libui#106 also points out. That is safe: against@tanstack/table-core8.21.3, a newcolumnsarray with the samedatastays on page 2, and only a rebuiltdatadrops to page 1.SourceStep: the deps aresubjects,subjectIdDisplayLengthandt. libui'suseTranslationmemoizestonresolvedLanguage, so theNULLlabel still follows a language change.DeleteRemoteAssignments: buildinstrumentMapanddeletableAssignmentsinside the memo. Both are rebuilt on every render (:58-64), so as deps they would defeat it.tis not needed. Instrument titles already follow the language, becauseuseInstrumentInfoQuerykeys onresolvedLanguage.Datefields. React Query'sreplaceEqualDeep(@tanstack/query-core5.104.0) shares only plain objects and arrays, so every refetch returns a new array. The app'sQueryClientsets nostaleTime, so tabbing away and back still resets the table to page 1. Only the libui change fixes that, so it is worth a libui issue.Same defect in admin Instruments
apps/web/src/routes/_app/admin/instruments.tsx:116-142(FormInstrumentsView) and:247-270(SeriesInstrumentsView) also rebuildrowson every render, and they holdpreviewItem/rowToArchivestate. Previewing an instrument from page 2 (double-click or the Preview row action) sends the table to page 1 behind the dialog, and the table stays there after the dialog closes.useInstrumentSource(:99-108) returns a new function every render, so wrap it inuseCallbackbefore it becomes a memo dependency. I would fold this into the same fix, with tests inroutes/_app/admin/__tests__/instruments.test.tsxandtesting/src/specs/admin-instruments.spec.ts.The other client-mode tables are fine. Admin groups, users and instrument repos pass query data straight through,
datahub/subjects/index.tsx:474-479memoizesdisplayData, anddatahub/subjects/$subjectId/tablekeepsrecordsin state.Tests
routes/_app/admin/audit/__tests__/logs.test.tsx:158-163does:getAllByRole('button', { hidden: true }), filtered by text.mocks.assignmentsbefore render. A fixture rebuilt on every render reproduces the bug even with the fix in place.POST /v1/assignments/bulk, asbulk-remote-assignments.spec.tsdoes, and open/group/remote-assignments?mode=delete. Add the delete table's locators totesting/src/pages/_app/group/remote-assignments.page.ts.Landing
#1679 touches the same two
DataTables and test files, so the two fit in one PR. #1685 and #1757 also editDeleteRemoteAssignments.tsx. If #1757 moves the assignments query touseSuspenseQuery, the assignments?? []inside the memo goes away. #1811 edits bothDataTablecall sites, the wizard spec and the page object, but notrows, so any conflict is textual. #1825 also adds a test toremote-assignments-wizard.spec.ts.Triage note written with Claude Code. Code references point at
main@d86740e.