Skip to content

GenericDataViewPage — sort or switch page must collapse details (with dirty-guard) #91

Description

@dynnamitt

Context

Today, when the side editor is open on the /vehicles route, the table's sort column headers are locked — clicks are suppressed and headers dimmed (DataTableHeader.tsx:59-70), driven by sortLocked={!!editingItem} set in GenericDataViewPage.tsx:194. The lock is binary on editingItem and is not dirty-aware. Pagination, page-size, and search/filter inputs are not locked at all today.

This UX is too coarse: it punishes the common case (user wants to keep browsing while a non-modified row is "selected") and gives no path out beyond an explicit close. The replacement flow is:

  • Sort header click or page switch → collapse the open details panel first, then perform the action.
  • Unless the editor is dirty → open a confirm dialog warning the user about unsaved changes and asking them to confirm discard. On confirm: collapse + proceed. On cancel: stay where we are.

A DiscardDialog component already exists (src/components/dialogs/DiscardDialog.tsx) and is used by EditorRail.tsx:193-198 for the collapse/cancel flows — reuse it directly.

Dirty tracking already exists for the vehicle editor (src/data/vehicles/vehicleFormState.ts:44-46 exposes isDirty(), consumed via useDirtyFormBlock(isDirty) at VehicleDetails.tsx:65). It is not currently surfaced to GenericDataViewPage — needs a small lift.

Scope

GenericDataViewPage-using routes only. Today that is just /vehicles (/vehicle-types and /deck-plans have bespoke editors and are not affected). Trigger surface: sort header click and pagination change (next / prev / page-size). Search/filter inputs are out of scope.

Plan

1. Remove the legacy sort lock

  • src/pages/GenericDataViewPage.tsx:194 — drop the sortLocked={!!editingItem} prop.
  • src/components/data/DataPageContent.tsx:43, 66, 121 — drop the sortLocked plumbing.
  • src/components/data/DataTableHeader.tsx:59-70 — remove the sortLocked branch, opacity dimming, and disabled tooltip. Headers click normally again.
  • Translation keys related to the locked tooltip (if any) — clean up.

2. Lift dirty state to EditingContext

Extend src/contexts/EditingContext.tsx so the active editor can report its dirty status up:

```ts
interface EditingContextType {
editingItem: EditingItem | null;
setEditingItem: (item: EditingItem | null) => void;
isEditorDirty: boolean;
setEditorDirty: (dirty: boolean) => void;
}
```

Inside VehicleDetails.tsx, replace the local useDirtyFormBlock(isDirty) wiring with an effect that pushes isDirty() into the context whenever the form state changes. Reset to false on unmount.

3. Add a guarded-close helper

Create a small hook, e.g. src/pages/useGuardedEditorClose.ts, that returns a function closeOrConfirm(onConfirm: () => void):

  • If no editingItem → call onConfirm() immediately.
  • If editingItem && !isEditorDirtysetEditingItem(null), then onConfirm().
  • If dirty → open DiscardDialog. On discard: clear editor, run onConfirm(). On cancel: no-op.

The dialog open state lives inside the hook (returns { closeOrConfirm, dialog } where dialog is the JSX to render).

4. Wire sort + pagination through the guard

In GenericDataViewPage.tsx:

  • Wrap the existing onRequestSort handler in closeOrConfirm.
  • Wrap pagination onPageChange and page-size change in closeOrConfirm.
  • Render the dialog JSX returned by the hook somewhere stable in the page tree.

For the vehicles route specifically, sort and pagination flow through vehicleViewConfig.tsx handlers → useVehicles hook. The wrap lives at the GenericDataViewPage boundary so all consumers of the generic page inherit the behaviour for free.

5. URL-deep-link interaction

The ?selected=<id> URL param drives editor open via useVehicleUrlSelection.tsx. When the guard closes the editor, the URL must be cleared too — reuse the existing clear path at useVehicleUrlSelection.tsx:55. Confirm no race: clearing the URL before sort fires should not retrigger reselection.

Critical files

File Change
src/pages/GenericDataViewPage.tsx Drop sortLocked; wire sort + pagination through guard; render dialog
src/components/data/DataPageContent.tsx Drop sortLocked plumbing
src/components/data/DataTableHeader.tsx Remove lock UI
src/contexts/EditingContext.tsx Add isEditorDirty + setter
src/data/vehicles/VehicleDetails.tsx Report dirty into context
src/data/vehicles/vehicleFormState.ts (reuse existing isDirty)
src/pages/useGuardedEditorClose.ts NEW — guarded close hook
src/components/dialogs/DiscardDialog.tsx (reuse)

Verification

  • /vehicles with editor open + clean form: clicking a sort header collapses the panel and re-sorts the table; pagination next/prev/page-size all collapse the panel and apply.
  • /vehicles with editor open + dirty form: same actions open DiscardDialog. Cancel → editor stays open, sort/page action discarded. Discard → editor closes, action applies.
  • URL ?selected=<id> is cleared whenever the editor collapses via the guard.
  • Deep-link /vehicles?selected=<id> still opens the editor as before.
  • E2E coverage: add a Playwright test under e2e-tests/no-auth/ exercising sort + pagination with editor open, both clean and dirty branches.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions