From 6fb48c64df38d1cc79dc39c78905b5ddb715c82e Mon Sep 17 00:00:00 2001 From: Diego Imbert Date: Thu, 17 Sep 2026 17:49:39 +0200 Subject: [PATCH] fix: follow a saved page item in the tab's own workspace and restore in-frame rows Co-Authored-By: Claude Opus 5 (1M context) --- frontend/src/lib/components/ResourceEditor.svelte | 6 ++++++ .../src/lib/components/ResourceEditorDrawer.svelte | 14 ++++++++------ frontend/src/lib/components/VariableEditor.svelte | 5 +++-- .../components/sessions/PageItemEditorView.svelte | 4 +++- .../sessions/sessionPreviewTabs.svelte.ts | 8 ++++---- .../components/sessions/sessionPreviewTabs.test.ts | 4 ++-- 6 files changed, 26 insertions(+), 15 deletions(-) diff --git a/frontend/src/lib/components/ResourceEditor.svelte b/frontend/src/lib/components/ResourceEditor.svelte index f569267a5e..d33b31b332 100644 --- a/frontend/src/lib/components/ResourceEditor.svelte +++ b/frontend/src/lib/components/ResourceEditor.svelte @@ -473,6 +473,12 @@ current.path = npath } + /** The path the resource has in `ws`: after a save, the one it was saved under. Each + * workspace-specific version keeps its own, so the selected one says nothing about `ws`. */ + export function pathIn(ws: string): string | undefined { + return initialStates[ws]?.path + } + /** Whether the write landed. It toasts its own failure, so most callers ignore this; * one that follows the save with bookkeeping of its own has to know not to. */ export async function save(): Promise { diff --git a/frontend/src/lib/components/ResourceEditorDrawer.svelte b/frontend/src/lib/components/ResourceEditorDrawer.svelte index 4f52b7b256..f7d06f8deb 100644 --- a/frontend/src/lib/components/ResourceEditorDrawer.svelte +++ b/frontend/src/lib/components/ResourceEditorDrawer.svelte @@ -41,8 +41,9 @@ inline?: boolean onRestored?: () => void /** Fires after Save has written, for a caller showing state derived from the - * resource — `onRestored` only covers restoring an old version. */ - onSaved?: () => void + * resource — `onRestored` only covers restoring an old version. `path` is where the + * resource now lives in this drawer's workspace, undefined when the save failed. */ + onSaved?: (path: string | undefined) => void } = $props() let drawer: Drawer | undefined = $state() @@ -53,7 +54,8 @@ let resourceEditor: | { - save: () => void + save: () => Promise + pathIn: (ws: string) => string | undefined localDraftDeployed: () => unknown localDraftCurrent: () => unknown discardLocalDraft: () => void @@ -223,10 +225,10 @@ // Closed before the write is awaited, the way it always was: `save()` toasts its // own failures and never rejects, so waiting would only add visible lag to every // caller of this drawer. `onSaved` still fires after the write lands. - const saved = resourceEditor?.save() + const editor = resourceEditor + const saved = editor?.save() drawer?.closeDrawer() - await saved - onSaved?.() + onSaved?.((await saved) ? editor?.pathIn(effectiveWorkspace) : undefined) }} disabled={!canSave} > diff --git a/frontend/src/lib/components/VariableEditor.svelte b/frontend/src/lib/components/VariableEditor.svelte index cea2bbbe07..f6f9adf454 100644 --- a/frontend/src/lib/components/VariableEditor.svelte +++ b/frontend/src/lib/components/VariableEditor.svelte @@ -50,7 +50,8 @@ /** Render in place, filling the parent, with no drawer or close button — for a host * that gives the editor a whole pane. */ inline?: boolean - /** Fires once a save lands, with the path the variable now lives at. */ + /** Fires once a save lands, with the path the variable now lives at in `workspace` — + * not in the workspace-specific version selected, which can be another's. */ onSaved?: (path: string) => void } = $props() // Sole ambient read in this file: the acting workspace is an input, and only its @@ -271,7 +272,7 @@ async function save(): Promise { const dirty = dirtyWorkspaces - const savedPath = current?.path ?? editPath ?? '' + const savedPath = (curWs ? states[curWs]?.draft?.path : undefined) ?? editPath ?? '' try { for (const ws of dirty) { const s = states[ws].draft! diff --git a/frontend/src/lib/components/sessions/PageItemEditorView.svelte b/frontend/src/lib/components/sessions/PageItemEditorView.svelte index db8ec39434..5afeef666f 100644 --- a/frontend/src/lib/components/sessions/PageItemEditorView.svelte +++ b/frontend/src/lib/components/sessions/PageItemEditorView.svelte @@ -144,7 +144,9 @@ bind:this={resourceEditor} inline workspace={workspaceId} - on:refresh={(e) => onSaved(typeof e.detail === 'string' ? e.detail : undefined)} + onSaved={(path) => { + if (path !== undefined) onSaved(path) + }} onRestored={() => savedNonce++} /> {:else if triggerKey} diff --git a/frontend/src/lib/components/sessions/sessionPreviewTabs.svelte.ts b/frontend/src/lib/components/sessions/sessionPreviewTabs.svelte.ts index e1ed37e9a9..2b162f4265 100644 --- a/frontend/src/lib/components/sessions/sessionPreviewTabs.svelte.ts +++ b/frontend/src/lib/components/sessions/sessionPreviewTabs.svelte.ts @@ -210,11 +210,11 @@ export function hydratePreviewTabs(session: { seen.add(t.id) // Rebuilt field-by-field so stray properties on old saved records (e.g. the // retired `pinned` flag) don't survive hydration and get persisted back. - // A list page saved with a row's drawer open comes back as that row's own tab. - const url = pageItemLocation(t.url) + // A list page saved with a row's drawer open comes back as that row's own tab, whether the + // row was asked for (`url`) or opened inside the frame (only its observed `loc` says so). const loc = t.loc || t.url - const stale = parsePageItemRoute(url) || pageItemLocation(loc) !== loc - tabs.push({ id: t.id, url, loc: stale ? url : loc }) + const item = [t.url, loc].map(pageItemLocation).find((u) => parsePageItemRoute(u)) + tabs.push(item ? { id: t.id, url: item, loc: item } : { id: t.id, url: t.url, loc }) } if (tabs.length > 0) { const wantActive = session.activePreviewTabId diff --git a/frontend/src/lib/components/sessions/sessionPreviewTabs.test.ts b/frontend/src/lib/components/sessions/sessionPreviewTabs.test.ts index bdc1024082..323dc4f375 100644 --- a/frontend/src/lib/components/sessions/sessionPreviewTabs.test.ts +++ b/frontend/src/lib/components/sessions/sessionPreviewTabs.test.ts @@ -245,13 +245,13 @@ describe('page item tabs', () => { const snap = hydratePreviewTabs({ previewTabs: [ { id: 'a', url: '/schedules#u/me/daily', loc: '/schedules?path=u#u/me/daily' }, - // A drawer opened inside the frame, with the command still on the list. + // A drawer opened inside the frame: the command is still the bare list. { id: 'b', url: '/variables', loc: '/variables#u/me/token' } ] }) expect(snap.tabs).toEqual([ { id: 'a', url: 'pageitem:schedule/u%2Fme%2Fdaily', loc: 'pageitem:schedule/u%2Fme%2Fdaily' }, - { id: 'b', url: '/variables', loc: '/variables' } + { id: 'b', url: 'pageitem:variable/u%2Fme%2Ftoken', loc: 'pageitem:variable/u%2Fme%2Ftoken' } ]) }) })