Changelog

0.5.2 — One settings-modal chrome across Create/Edit View + Edit Table, persistent select options, notification-freeze fix

Create View, Edit View, and Edit Table now render on the same shared `ModalShell` — centered, solid `bg-background` panel with tinted config rail, header subtitle, pinned footer, and popover-aware Escape. Creating a select option from a cell now persists to the column, so filter pickers everywhere see it. And the grid livelock that froze the app after clicking a record-created notification is fixed.

What's new

One centered-modal chrome for Create View / Edit View / Edit Table

CreateViewModal's chrome — dimmed blurred backdrop, solid bg-background panel at rounded-2xl + sheet elevation, title / subtitle header with close button, pinned footer bar, and popover-aware Escape — is now a shared ModalShell component. Both sibling settings surfaces move onto it, and the deprecated right-side SideSheet chrome is deleted.

  • Edit View settings (ViewSettingsSheet) is now a centered modal instead of a right-side slide-in, matching Create View's tinted bg-sidebar/50 config-rail styling, SectionHeading rhythm from ModalFormPrimitives, and Cancel + primary Save footer (errors surface in the footer). The gear on any saved-view row — sidebar tree and the grid header's view dropdown — opens it.
  • Edit Table settings (EditTableSheet) also moves onto the shared shell: the table's name and field count in the header subtitle, a Fields and Settings tab bar pinned above the scrolling body, and the same Cancel + Save footer. All tab content is unchanged — the switch is chrome-only.
  • CreateViewModal is refactored onto the shell too; its chrome-contract tests pass unchanged. The rail width is 400 px (up from 320 px in the first drop), so filter-value dropdowns show the picked option's full label instead of collapsing to a ~36 px sliver.
  • Popover-aware Escape. With an open value dropdown or date-picker layer (data-popover-layer), Escape closes only that layer; the modal closes only when no popover is open. A mis-typed refinement never drops the whole draft.

See Data → Create view modal, Data → Saved views, and Data → Per-table settings.

Select options created from a cell now persist to the column

Creating an option via a select cell's Create ⟨value⟩ action used to only write the value onto that row — never appending it to the column's stored options list. That produced "ghost" options that vanished from pickers when the last row holding them was edited, and that never appeared in surfaces reading only stored options — notably the Create View modal's filter-value dropdown (e.g. CABoom on prospects.source was missing there while visible in the cell editor).

  • Persist on create. A new shared addSelectOption helper in src/lib/columnOptions.js appends the created value to the column's stored options; no-ops for duplicates, blanks, non-select columns, and value-set-backed columns (whose options live in the value-set store). Wired into every create surface: the grid's cell editor popup (select + multi-select), CanvasDataCard, and DataPage.
  • Filter pickers see stored options ∪ row values. The enrichment the grid's filter popover already did is extracted into a shared enrichSelectFieldOptions helper, and now also used by the Create View modal and the Edit View settings modal — so any ghost values still living in existing rows remain filterable without a data migration. ViewSettingsSheet fetches the table's rows on open, since it can be opened from the sidebar before the grid has loaded them.
  • Cell editors in CanvasDataCard / DataPage also merge row values into their option lists, matching the grid.

See Data → Inline editing.

Fixes

Grid livelock that froze the app after notification clicks

Clicking a record-created notification opened the record, then the entire page stopped responding to clicks until a hard refresh. Root cause was on the Data page from the moment it mounted: 'Maximum update depth exceeded' fired continuously with zero interaction —

  • LargeWidget rebuilt visibleCols as a fresh array literal on every render, so orderedCols → toolbarFields → viewCounts all changed identity every render.
  • The onViewStateChange effect (keyed on viewCounts) re-fired after every commit, and its setGridViewState in DataWorkspace re-rendered the grid → infinite passive-effect loop.
  • React Router v7 runs navigations as startTransitions; the endless normal-priority updates kept interrupting them, so navigations never committed — the URL changed but the UI didn't, and every later click died silently. The notification click was just the first navigation users hit from that page.

The fix stabilizes identities down the chain: visibleCols / userCols / systemTrailing are memoized in LargeWidget; useViewCounts returns the previous object when counts are value-equal; and DataWorkspace bails out of gridViewState updates when nothing changed — a circuit breaker so future identity churn can't re-arm the loop.

Same fault line, addressed under the hood:

  • RecordDeepLinkHost and CalendarDeepLinkHost no longer call setSearchParams during render (illegal render-phase navigation in Router v7). Dead links are cleared from effects only after the lookup has settled.
  • RecordDeepLinkHost refetches the target table before declaring a row missing, so a notification pointing at a row newer than the local cache — the teammate-notification race — opens the sheet instead of being silently discarded.
  • fetchRows in-flight dedup now returns the shared request promise instead of [], so concurrent callers all wait on the same result.

Under the hood

  • src/components/shared/ModalShell.jsx (new) — the shared centered-modal chrome. CreateViewModal, ViewSettingsSheet, and EditTableSheet all mount on it. The now-orphaned src/components/shared/SideSheet.jsx is deleted; ModalShell is documented under §Modals in DESIGN-PHILOSOPHY.md.
  • src/lib/columnOptions.js (new) — addSelectOption (persist on create) and enrichSelectFieldOptions (stored options ∪ row values for filter pickers), with 13 unit tests.
  • ViewSettingsSheet opens the row cache — fetches rows on mount so the enriched value picker has row values to work with when the sheet is opened from the sidebar before the grid has loaded.
  • useViewCounts identity-stable — returns the previous object when counts are value-equal; combined with memoized visibleCols / userCols / systemTrailing, breaks the render loop at its source.

What's next

  • Bring the medium canvas DataTableViewerWidget's edit-view surface onto the same modal shell as its full-app siblings so every view / table settings entry point renders the same panel.
  • Extend the shared ModalShell to the next batch of settings surfaces (NewTableModal, CreateEventSheet) so the whole in-app settings-modal family reads as one system.

See Data → Saved views, Data → Create view modal, Data → Per-table settings, and Data → Toast notifications on record creation.

On this page