feat(#463): File menu creates, imports and exports Dashboards workspace-wide - #482
Merged
Merged
Conversation
…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
This was referenced Jul 26, 2026
…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
This was referenced Jul 27, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 inEdit 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.
planImportDashboardnow appends a freshly reminted document, which let itsthree-way targeting go entirely:
targetDashboardId, the compatibility slot,requireEmptyCollection, the two stale-target diagnostics that guarded them, andthe 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.FileMenuContexttakesdashboardIdsinstead ofdashboardCount: export has toname 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)
requires — an append is otherwise invisible and read as a silent no-op.
can populate from a stale
state.dashboard.openDialogShellnow returns focus to the File trigger on close (the issuelists focus return under Tests).
id fragment widened to the whole id.
Owner feedback + review round two (third commit)
The Dashboard opened, but the tree that lists it — and marks it selected —
stayed behind the other tab. Both commands now switch
state.upperRoletodashboardsas well; a refused commit changes neither.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.workspaceIdon 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
statefallback only applies while it is still current.
exportWorkspaceActiongotthe same pin.
first Dashboard row, and
openDialogShelltraps Tab inside the card (pullingfocus back in from outside, skipping disabled controls).
id tail is no tiebreaker (
sales-abcdef/ops-abcdef). The new pureshortIdFragmentswidens the tail until every listed fragment differs, fallingback 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)
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 onscreen reconstructed as
[B, B]. It now folds back by exact id, leaves thecommitted collection alone when the projection names no stored entry, and seats
it only when nothing is stored.
file-menu.tsno longer touches thecompatibility slot at all.
were pinned; the file name came from live
stateafter the flush. The requestnow carries
workspaceNametoo, and both exports name themselves from committedtruth when it loads and the pinned name otherwise (which is also the fallback an
empty — schema-valid — Dashboard title uses).
closes an open menu when a cross-tab refresh lands, so a
choosedecision whoseDashboards were all removed opened a row-less chooser and threw focusing row
zero. The Export row re-resolves through
liveContext+fileMenuModelonactivation: 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 cannotlaunch 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 dashboardbutton 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 testpasses (the per-file coverage gate is non-negotiable)npm run buildsucceeds (single-filedist/sql.html)src/core/, network insrc/net/(injected fetch), DOM insrc/ui/CHANGELOG.md([Unreleased]) updated if behavior or the deployed surface changed