Skip to content

Pressing Enter in the bulk-assignment or delete-assignment table search reloads the app and signs the user out #1679

Open

Description

@joshunrau

Pressing Enter in the search box of the bulk remote-assignment subject picker, or of the Delete Assignments table, reloads the whole app and signs the user out. libui's DataTable renders its search box as libui's SearchBar, which is a bare <form> with no submit handler. A form with a single text input submits implicitly on Enter, and the default action reloads the current URL. The access token and the current session live only in memory (they are not in PERSISTED_KEYS), so the reload sends the user to the login page and discards everything the wizard was holding: picked subjects, parsed file, timepoints. The repo already has SearchSubmitGuard for exactly this (added in 66d794d for the subject datahub), but neither component wraps its table in it. Typing a search term and pressing Enter is a natural thing to do, so users will hit this.

Where

apps/web/src/components/BulkRemoteAssignmentWizard/SourceStep.tsx:231-300:

<div data-testid="bulk-subject-picker">
  <DataTable
    columns={[ /* ... */ ]}
    data={rows}
    // ...
  />
</div>

apps/web/src/components/DeleteRemoteAssignments/DeleteRemoteAssignments.tsx:136-226:

<DataTable
  columns={[ /* ... */ ]}
  data={rows}
  rootStyle={{ '--color-background': 'var(--color-card)' }}
  onRowClick={(row) => toggle(row.id)}
/>

apps/web/src/components/SearchSubmitGuard/SearchSubmitGuard.tsx:7-15 documents the mechanism. The only call site is routes/_app/datahub/subjects/index.tsx. The searchable tables in routes/_app/admin/users/index.tsx, admin/groups/index.tsx, admin/instruments.tsx and admin/instrument-repos/index.tsx are unguarded in the same way.

Reproduce

  1. As a group manager, open Group → Remote Assignments → Create Assignments.
  2. On "Select Subjects", tick a few subjects, type part of an identifier in the table's search box, and press Enter.

Actual: the page reloads, the user is sent to the login page, and the selection is lost. The same happens on the Delete Assignments table.
Expected: Enter does nothing beyond the as-you-type filtering, and the user stays signed in with the wizard state intact.

Tests

  • apps/web/src/components/BulkRemoteAssignmentWizard/__tests__/SourceStep.test.tsx: it('should cancel the submit when Enter is pressed in the subject search, so the app does not reload and sign the user out') (assert fireEvent.submit on the search form is default-prevented).
  • apps/web/src/components/DeleteRemoteAssignments/__tests__/DeleteRemoteAssignments.test.tsx: the same assertion for its search box.
  • testing/src/specs/remote-assignments-wizard.spec.ts: type in the picker search, press Enter, and assert the wizard is still on the subjects step with the selection kept.

Suggested fix

Wrap both DataTables in <SearchSubmitGuard>, and do the same for the admin route tables listed above. Fixing libui's SearchBar to preventDefault on submit would cover every call site at once.

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)
    Good First IssueWell-scoped, low-risk and self-contained; a good entry point for a new contributor
    on Oct 7, 2026
  2. joshunrau commented on Oct 26, 2026

    @joshunrau
    CollaboratorAuthor

    Still present on main (d86740e). Neither table has been wrapped since this was filed, and no open PR wraps them.

    More unguarded search bars

    • admin/instruments.tsx has two searchable tables: :146 (instruments) and :274 (series).
    • Three bare SearchBars outside any DataTable submit the same way: group/manage.tsx:423 (the Create Series dialog), group/manage.tsx:615 and upload/index.tsx:68. On group/manage.tsx:615 the reload also throws away unsaved instrument ticks and settings edits, because selectedIds (:558) is only sent when the form is submitted (:821). InstrumentShowcase.tsx:68-79 is the only safe one: its own onKeyDown calls preventDefault on Enter.
    • feat: add the instrument hub and record provenance #1807 adds another one. InstrumentTable.tsx renders a searchable DataTable with no guard, and the new /datahub/instruments pages use it.

    That makes 10 sites on main and 11 with #1807. Wrapping each call site has already been missed twice: DeleteRemoteAssignments landed in e2e30f0 two days after the guard (66d794d), and #1807 misses it again.

    Fix

    1. Put one guard on the app shell instead of ten wrappers. Wrap the root of Layout (Layout.tsx:15) in SearchSubmitGuard, and update its doc comment to say where it sits. This is safe because every other form already cancels its own submit. libui Form does it in handleSubmit, and the four native forms call preventDefault first: CreateTemplateForm.tsx:42, EditTemplateDialog.tsx:39, LoginPageEditor/hooks.ts:323 and admin/instrument-repos/index.tsx:47. React propagates onSubmit through portals, so the Create Series dialog is covered too. /auth/* and /setup render outside Layout, but they have no search bar. The change does not touch SourceStep.tsx, DeleteRemoteAssignments.tsx or InstrumentTable.tsx, so it does not conflict with fix(web): align remote assignment table colors with datahub subject table #1811 or feat: add the instrument hub and record provenance #1807. The datahub wrapper (datahub/subjects/index.tsx:493, :652) becomes redundant. feat: add the instrument hub and record provenance #1807 edits that file, so remove the wrapper after feat: add the instrument hub and record provenance #1807 merges.
    2. Fix it upstream as well. libui 6.18.0 (2026-10-07) is the latest release, and its SearchBar.tsx:47 still renders <form className={cn('relative', className)} {...props}> with no onSubmit. No libui issue tracks this. DataTableControls passes fixed props to SearchBar, so a caller cannot add the handler through DataTable. The bump has a blocker: libui 6.18.0 breaks typing in apps/web (libui **6.18.0** publishes type declarations that reference a global React namespace without importing React. libui#126). Its declarations rely on a global React that our vendored react does not declare. With skipLibCheck, the props of Checkbox, Select, Table and others silently become any, and apps/web gets 17 TS7006 errors. The fix, fix(types): import React in files whose emitted declarations use React.* libui#128, is still open. Ship the SearchBar fix in the same patch release or a later one. Once the bump lands, SearchSubmitGuard.test.tsx:39-41 fails. That is the signal to delete the guard.

    Tests

    • apps/web/src/components/Layout/__tests__/Layout.test.tsx: it('should cancel a search bar submit anywhere in the app, so Enter does not reload the page and sign the user out'). Make the Outlet mock render a real libui SearchBar (import @/services/i18n), and add a second case that renders the bar through createPortal. The per-component unit tests in the issue no longer apply, because the components themselves cancel nothing.
    • e2e: add the remote-assignments-wizard.spec.ts case from the issue (fix(web): align remote assignment table colors with datahub subject table #1811 also edits that spec). Add a case to upload.spec.ts for the bare-SearchBar path, using the existing UploadPage.searchInput. Nothing in testing/ opens ?mode=delete yet, so the delete-mode spec that Bulk-deleting remote assignments reports success when the server could not delete them #1685 needs can carry the same Enter check. datahub.spec.ts:88 already presses Enter in the subject table and should keep passing once that wrapper is gone.

    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 lessGood First IssueWell-scoped, low-risk and self-contained; a good entry point for a new contributorPriority: 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