Skip to content

feat(#463): File menu creates, imports and exports Dashboards workspace-wide - #482

Merged
BorisTyshkevich merged 4 commits into
mainfrom
feat/file-menu-multi-dashboard-463
Jul 27, 2026
Merged

feat(#463): File menu creates, imports and exports Dashboards workspace-wide#482
BorisTyshkevich merged 4 commits into
mainfrom
feat/file-menu-multi-dashboard-463

Conversation

@BorisTyshkevich

@BorisTyshkevich BorisTyshkevich commented Jul 26, 2026

Copy link
Copy Markdown
Collaborator

What & why

Closes #463.

The File menu's three Dashboard commands still assumed one exact Dashboard was
already open — a model that stopped fitting once a workspace can hold many.
There was no New dashboard… at all; Import Dashboard… was disabled on
Query with Open a dashboard and, where it did work, replaced whichever
Dashboard was open; and Export Dashboard… was unavailable unless one was on
screen, even with Dashboards in the workspace to choose from.

All three become workspace operations.

New dashboard… — a new row, immediately after New workspace…. Prompts
for a name, appends an empty Dashboard through app.mutateWorkspace, opens it in
Edit mode. Duplicate names allowed (identity is the id); Cancel/Escape commit
nothing; a rejected commit leaves navigation and local state untouched.

Import dashboard… — additive on every surface, including Query.
planImportDashboard now appends a freshly reminted document, which let its
three-way targeting go entirely: targetDashboardId, the compatibility slot,
requireEmptyCollection, the two stale-target diagnostics that guarded them, and
the destructive Import and replace current Dashboard? confirm. There is no
parameter left that can name an existing entry. The plan reports the local id it
minted, so the import opens what it just added.

Export dashboard… — workspace-aware. Zero Dashboards disables it with No
dashboards
; the Dashboard on screen exports in one click (View as well as Edit);
a sole Dashboard exports directly from Query too; otherwise a chooser lists tile
count plus an id fragment so duplicate names stay distinguishable. Every export
resolves an exact id — no dashboards[0] fallback remains to reach.

FileMenuContext takes dashboardIds instead of dashboardCount: export has to
name a target, and a bare count cannot without the caller reaching back into the
aggregate and re-deciding there. Rows regroup by verb — create / import / export
/ download — behind three separators; the two Dashboard labels drop to sentence
case. Row order, separators, labels, availability and reason text stay the pure
model's alone, and context still changes enabled state only.

Owner decision taken during the run

The issue's "multiple Dashboards with an exact Dashboard currently open:
preselect that Dashboard" reads two ways. Owner chose: export it directly,
no chooser
— exporting the Dashboard you are looking at stays one click. The
chooser appears only when no exact Dashboard resolves.

Review round (second commit)

  • Import did not open the imported Dashboard in Edit mode, which the issue
    requires — an append is otherwise invisible and read as a silent no-op.
  • Export was gated on the id list alone, which the workspace-not-found surface
    can populate from a stale state.dashboard.
  • openDialogShell now returns focus to the File trigger on close (the issue
    lists focus return under Tests).
  • README's File-menu section still described the pre-Refactor File menu for multi-Dashboard creation, import, and export #463 menu.
  • The export-chooser meta test used 6-character ids, so it could not fail if the
    id fragment widened to the whole id.

Owner feedback + review round two (third commit)

  • Creating or importing from the Query surface left the sidebar on Databases.
    The Dashboard opened, but the tree that lists it — and marks it selected —
    stayed behind the other tab. Both commands now switch state.upperRole to
    dashboards as well; a refused commit changes neither.
  • P1 — export could retarget to another workspace. Dashboard ids are unique
    within a workspace, and an imported workspace keeps the ids it was exported
    with. The export awaits the write queue, then re-read app.state.workspaceId
    on the far side of that await — so switching workspace mid-flush let an
    exact-id lookup succeed against the new workspace. The workspace is now
    pinned when the export is chosen (chooser included) and the degraded state
    fallback only applies while it is still current. exportWorkspaceAction got
    the same pin.
  • P2 — the chooser left keyboard users behind the modal. It now focuses its
    first Dashboard row, and openDialogShell traps Tab inside the card (pulling
    focus back in from outside, skipping disabled controls).
  • P2 — duplicate rows were not guaranteed distinguishable. A fixed six-character
    id tail is no tiebreaker (sales-abcdef / ops-abcdef). The new pure
    shortIdFragments widens the tail until every listed fragment differs, falling
    back to the whole id. The import chooser shows one too — reminting after import
    doesn't stop the id identifying the source documents while choosing.

Review round three (fourth commit)

  • P1 — the degraded workspace export could drop a Dashboard. currentWorkspace()
    folded the live projection back through the compatibility slot (entry 0), but
    since Add dashboard selection state and a full-size dashboard work surface #425 that projection is the selected Dashboard: [A, B] with B on
    screen reconstructed as [B, B]. It now folds back by exact id, leaves the
    committed collection alone when the projection names no stored entry, and seats
    it only when nothing is stored. file-menu.ts no longer touches the
    compatibility slot at all.
  • P2 — a pinned export could still take the new workspace's name. The bytes
    were pinned; the file name came from live state after the flush. The request
    now carries workspaceName too, and both exports name themselves from committed
    truth when it loads and the pinned name otherwise (which is also the fallback an
    empty — schema-valid — Dashboard title uses).
  • P2 — a stale chooser target could open an empty modal and throw. Nothing
    closes an open menu when a cross-tab refresh lands, so a choose decision whose
    Dashboards were all removed opened a row-less chooser and threw focusing row
    zero. The Export row re-resolves through liveContext + fileMenuModel on
    activation: gone → toast, narrowed to one → direct export, still many → chooser.

Verification

npm test (5661 tests, per-file gate), tsc --noEmit, npm run check:arch,
npm run build, and Playwright chromium + webkit (269 passed; Firefox cannot
launch in this sandbox and is covered by CI).

Verified live against the served build: the nine rows and their reasons on Query
and on a Dashboard, the name prompt (light + dark, default preselected), create →
Dashboard surface in Edit mode, a second Dashboard under the same name, direct
export from an open Dashboard, re-importing that bundle → appended (2 → 3) and
opened in Edit, the chooser from Query showing three identically-named rows
distinguished by id fragment, and focus returning to the File button on Escape.
No console errors.

Filed #481 (inbox): the empty-Dashboard placeholder's Create dashboard
button is now a second Dashboard-creation path with different UX and a different
write primitive — deferred rather than folded in, since #463's non-goals cover
the adjacent rename/+-button work.

Checklist

  • npm test passes (the per-file coverage gate is non-negotiable)
  • Tests added/updated in the same change as the code
  • npm run build succeeds (single-file dist/sql.html)
  • Layers kept honest: pure logic in src/core/, network in src/net/ (injected fetch), DOM in src/ui/
  • No new runtime dependency (or it's a deliberate, justified addition — see CONTRIBUTING)
  • README / CHANGELOG.md ([Unreleased]) updated if behavior or the deployed surface changed
  • Reconciled affected tracked work (roadmap Roadmap to 1.0.0 #68, the issue body, ADR/CHANGELOG) if this change reshaped it

BorisTyshkevich and others added 2 commits July 26, 2026 20:51
…ce-wide

The File menu's three Dashboard commands assumed one exact Dashboard was
already open. In a workspace that can hold many, that model no longer fits:
there was no way to create a Dashboard from the menu at all, Import was
disabled on Query with "Open a dashboard" and REPLACED whichever Dashboard
was open, and Export was unavailable unless one was on screen.

All three become workspace operations:

- New dashboard... (new row, second position) prompts for a name, appends an
  empty Dashboard through app.mutateWorkspace, and opens it in Edit mode.
  Duplicate names are allowed; identity is the id.
- Import dashboard... is additive on every surface. planImportDashboard now
  APPENDS a freshly reminted document, so its three-way targeting
  (targetDashboardId / the compatibility slot / requireEmptyCollection), the
  two stale-target diagnostics they guarded, and the destructive-replace
  confirm all go away: there is no parameter left that can name an existing
  entry.
- Export dashboard... resolves an exact id from the workspace's own Dashboard
  ids -- preferring the one on screen (owner decision: still one click, in
  View as well as Edit), then a sole Dashboard, and only otherwise asking
  through a chooser that lists tile count plus an id fragment so duplicate
  names stay distinguishable.

FileMenuContext takes dashboardIds instead of dashboardCount: export has to
name a target, and a bare count cannot do that without the caller reaching
back into the aggregate and re-deciding there. Rows regroup by verb --
create / import / export / download -- behind three separators, and the two
Dashboard labels drop to sentence case.

Part of #463

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018mujm1kW7jDGpTEfscndcU
…kspace

Review findings on the branch:

- The issue requires Import dashboard... to open the imported Dashboard in Edit
  mode after a successful commit, and it did not. An append is invisible
  otherwise -- the new Dashboard lands last, in a collection the user may not
  even have open -- so a successful import read exactly like a silent no-op.
  planImportDashboard now reports the LOCAL id it minted (importedDashboardId),
  rather than leaving the caller to re-derive it from dashboards.at(-1) and
  making the navigation depend on the append staying an append.
- Export was gated on the Dashboard-id list alone, which the workspace-not-found
  surface can still populate from a stale state.dashboard projection -- offering
  to download a document from the workspace the user just left. The id list is
  now empty whenever no workspace resolves.
- openDialogShell returns focus to the File trigger on close. Every dialog it
  mounts is opened from a File-menu row, and the row is gone by then, so
  dismissing one dropped focus to <body>; the issue lists focus return under
  Tests.
- README's File-menu section described the pre-#463 menu (six rows, "replaces
  the Dashboard on screen", "disabled on Query", and two reason strings the
  model no longer emits).
- The export-chooser meta test used 6-character ids, so it passed identically if
  the id fragment widened to the whole id. Comments in dashboard.ts,
  dashboard-tree.ts and the legacy-fallback test referenced dashboardCount,
  openConfirm and a latest: null path that no longer exist as described.

Part of #463

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018mujm1kW7jDGpTEfscndcU
BorisTyshkevich and others added 2 commits July 27, 2026 04:55
…fix chooser a11y

Owner feedback + review round two.

**Reveal (owner):** creating or importing a Dashboard from the Query surface
opened it but left the upper sidebar on Databases -- the Dashboard was on screen
while the tree that lists it, and marks it selected, stayed behind the other tab.
Both commands now go through `revealDashboard`, which opens in Edit mode AND
switches `state.upperRole` to `dashboards`. A refused commit changes neither.

**P1 -- export could retarget to another workspace.** Dashboard ids are unique
within a workspace, not across them, and an imported workspace keeps the ids it
was exported with. Every export awaits the write queue before reading, and
`flushAndLoadCommitted` then re-read `app.state.workspaceId` on the far side of
that await: switch workspace while a pending mutation holds the flush, and an
exact-id lookup against the NEW workspace succeeds and downloads its Dashboard
instead. The workspace is now pinned when the export is chosen -- for the chooser
too, so a switch made while it is open fails closed -- and the degraded `state`
fallback only applies while that workspace is still current. `exportWorkspaceAction`
carries the same pin for the same reason.

**P2 -- the chooser left keyboard users behind the modal.** It never moved focus
in, and the shared dialog shell established no initial focus and trapped no Tab,
so the page behind a modal stayed reachable. The picker now focuses its first
Dashboard row (one Enter from done), and `openDialogShell` wraps Tab inside the
card -- pulling focus back in from outside, and skipping disabled controls like
the name dialog's confirm.

**P2 -- duplicate rows were not guaranteed distinguishable.** Tile count is no
tiebreaker for two empty same-named Dashboards, and a fixed six-character id tail
is none either: `sales-abcdef` and `ops-abcdef` share theirs. The new pure
`shortIdFragments` grows the tail until every listed fragment differs, falling
back to the whole id when nothing separates them. The import chooser shows the
fragment too -- reminting the id after import does not stop it identifying the
SOURCE documents while the user is choosing between them.

Part of #463

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018mujm1kW7jDGpTEfscndcU
…mes, re-resolve on click

**P1 -- the degraded workspace export could drop a Dashboard.** `currentWorkspace()`
rebuilds the aggregate from the live projection when no committed read is
available. Since #425 that projection is the SELECTED Dashboard, which can be
any entry, but it was folded back through `withCompatibilityDashboard` -- entry 0,
unconditionally. With `[A, B]` and B on screen the reconstruction produced
`[B, B]`: A silently gone, a duplicate id minted, and either an encode failure or
a backup missing a Dashboard. That is the path a user reaches when IndexedDB is
blocked, over quota, or in private mode -- exactly when export is the recovery
mechanism. It now folds the projection back by EXACT id, leaves the committed
collection alone when the projection names no stored entry (guessing a slot is
what caused the loss), and seats it only when nothing is stored at all -- the
genuine legacy/first-run case. file-menu.ts no longer touches the compatibility
slot anywhere.

**P2 -- a pinned export could still take the new workspace's name.** The contents
were pinned; the file name was read from live `state` after the flush, so
workspace A's bytes could land as `B.json`. A Dashboard with an empty title (which
is schema-valid) falls back to the workspace name and had the same hole. The
request now carries `workspaceName` alongside `workspaceId`, and both exports name
themselves from committed truth when it loads and from the pinned name otherwise.

**P2 -- a stale chooser target could open an empty modal and throw.** The model was
evaluated once when the menu opened and the click handler kept that decision, but
nothing closes an open menu when a cross-tab refresh projects a new workspace --
so a `choose` target whose Dashboards were all removed opened a chooser with no
rows, and then threw focusing row zero. The Export row now re-resolves through
`liveContext` + `fileMenuModel` on activation: gone entirely toasts, narrowed to
one exports directly, still many opens the chooser.

Part of #463

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018mujm1kW7jDGpTEfscndcU
@BorisTyshkevich
BorisTyshkevich merged commit f2cb8cb into main Jul 27, 2026
7 checks passed
@BorisTyshkevich
BorisTyshkevich deleted the feat/file-menu-multi-dashboard-463 branch August 6, 2026 15:28
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.

Refactor File menu for multi-Dashboard creation, import, and export

1 participant