feat(sessions): offer the way back to the list from an entity editor tab

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KcyBRxE8j3ujCYkum9KrC4
This commit is contained in:
Diego Imbert
2026-09-05 15:26:27 +02:00
co-authored by Claude Opus 5
parent 4d9ec4b816
commit 1b840dbd7f
7 changed files with 104 additions and 20 deletions
@@ -3,7 +3,7 @@
import DrawerContent from './common/drawer/DrawerContent.svelte'
import { History, Loader2, Save } from 'lucide-svelte'
import { ArrowLeft, History, Loader2, Save } from 'lucide-svelte'
import WsSpecificVersions from './WsSpecificVersions.svelte'
import { userStore, workspaceStore } from '$lib/stores'
import { isOwner } from '$lib/utils'
@@ -24,7 +24,8 @@
disableChatOffset = false,
onRestored = undefined,
onSaved = undefined,
useDrawer = true
useDrawer = true,
onBack = undefined
}: {
workspace?: string
disableChatOffset?: boolean
@@ -39,6 +40,9 @@
* drawer to open, so it simply takes effect.
*/
useDrawer?: boolean
/** Inline only: offered in the header when the host replaced something the
* user should be able to get back to (a session tab that took over the list). */
onBack?: () => void
} = $props()
let drawer: Drawer | undefined = $state()
@@ -233,9 +237,21 @@
{:else}
<div class="flex flex-col h-full min-h-0">
<div class="flex flex-row items-center gap-2 justify-between px-4 py-2 border-b">
<span class="text-sm font-semibold truncate"
>{mode == 'edit' ? 'Edit ' + path : addResourceTitle(resource_type)}</span
>
<div class="flex flex-row items-center gap-2 min-w-0">
{#if onBack}
<Button
variant="subtle"
unifiedSize="sm"
startIcon={{ icon: ArrowLeft }}
on:click={onBack}
title="Back to Resources"
iconOnly
/>
{/if}
<span class="text-sm font-semibold truncate"
>{mode == 'edit' ? 'Edit ' + path : addResourceTitle(resource_type)}</span
>
</div>
<div class="flex flex-row items-center gap-2 shrink-0">
{@render editorActions()}
</div>
@@ -15,7 +15,7 @@
import Alert from './common/alert/Alert.svelte'
import { sendUserToast } from '$lib/toast'
import { canWrite } from '$lib/utils'
import { Save } from 'lucide-svelte'
import { ArrowLeft, Save } from 'lucide-svelte'
import VariableForm from './VariableForm.svelte'
import { invalidateWorkspacePaths } from './PathNameAutocomplete.svelte'
import WsSpecificVersions from './WsSpecificVersions.svelte'
@@ -41,7 +41,8 @@
// rather than in the navigation workspace. Defaults to $workspaceStore.
let {
workspace = undefined,
useDrawer = true
useDrawer = true,
onBack = undefined
}: {
workspace?: string
/**
@@ -51,6 +52,9 @@
* no drawer to open, so it simply takes effect.
*/
useDrawer?: boolean
/** Inline only: offered in the header when the host replaced something the
* user should be able to get back to (a session tab that took over the list). */
onBack?: () => void
} = $props()
let curWs = $derived(workspace ?? $workspaceStore)
@@ -409,9 +413,21 @@
{:else}
<div class="flex flex-col h-full min-h-0">
<div class="flex flex-row items-center gap-2 justify-between px-4 py-2 border-b">
<span class="text-sm font-semibold truncate"
>{edit ? `Update variable at ${initialPath}` : 'Add a variable'}</span
>
<div class="flex flex-row items-center gap-2 min-w-0">
{#if onBack}
<Button
variant="subtle"
unifiedSize="sm"
startIcon={{ icon: ArrowLeft }}
on:click={onBack}
title="Back to Variables"
iconOnly
/>
{/if}
<span class="text-sm font-semibold truncate"
>{edit ? `Update variable at ${initialPath}` : 'Add a variable'}</span
>
</div>
<div class="flex flex-row items-center gap-2 shrink-0">
{@render editorActions()}
</div>
@@ -12,6 +12,8 @@
import type { SessionRuntime } from './sessionRuntime.svelte'
import { Loader2 } from 'lucide-svelte'
import {
entityListPage,
pageHref,
resolvePreviewTab,
parsePreviewItemRoute,
parsePreviewSelectedId,
@@ -142,6 +144,20 @@
active ? 'z-10 opacity-100 pointer-events-auto' : 'z-0 opacity-0 pointer-events-none'
)
// An entity editor replaced the list its tab was opened from (the row is the
// tab now, not a drawer over the list), so it has to offer the way back.
// Re-points this tab rather than opening another: the list is where the tab
// came from, not a second destination.
const backToList = $derived(
slot.kind === 'entity' && runtime
? () => {
const page = entityListPage(slot.entityKind)
if (page)
runtime.previewTabs.navigate({ type: 'page', href: pageHref(page.path), label: page.label })
}
: undefined
)
// Overlays a tab opens (drawers, modals, popovers) anchor here rather than to the
// document, so they stay within this tab and hide with it when another tab takes over.
// Every branch that renders content in-realm must bind this — an unbound host makes the
@@ -324,19 +340,19 @@
{#await import('./ScheduleEditorView.svelte')}
{@render editorLoading()}
{:then Module}
<Module.default path={slot.path} {workspaceId} />
<Module.default path={slot.path} {workspaceId} onBack={backToList} />
{/await}
{:else if slot.entityKind === 'resource'}
{#await import('./ResourceEditorView.svelte')}
{@render editorLoading()}
{:then Module}
<Module.default path={slot.path} {workspaceId} />
<Module.default path={slot.path} {workspaceId} onBack={backToList} />
{/await}
{:else}
{#await import('./VariableEditorView.svelte')}
{@render editorLoading()}
{:then Module}
<Module.default path={slot.path} {workspaceId} />
<Module.default path={slot.path} {workspaceId} onBack={backToList} />
{/await}
{/if}
</div>
@@ -4,13 +4,16 @@
let {
path,
workspaceId
workspaceId,
onBack
}: {
/** The resource this tab edits (the row its location deep-links). */
path: string
/** The session's acting workspace, which the editor operates on instead of
* `$workspaceStore` (the nav workspace, which a session leaves put). */
workspaceId: string
/** Back to the list this editor was reached through; the tab replaced it. */
onBack?: () => void
} = $props()
let editor = $state<ResourceEditorDrawer | undefined>()
@@ -25,4 +28,4 @@
})
</script>
<ResourceEditorDrawer bind:this={editor} useDrawer={false} workspace={workspaceId} />
<ResourceEditorDrawer bind:this={editor} useDrawer={false} workspace={workspaceId} {onBack} />
@@ -1,17 +1,22 @@
<script lang="ts">
import ScheduleEditorInner from '$lib/components/triggers/schedules/ScheduleEditorInner.svelte'
import { setTriggerWorkspace } from '$lib/components/triggers/triggerWorkspace'
import { Button } from '$lib/components/common'
import { ArrowLeft } from 'lucide-svelte'
import { untrack } from 'svelte'
let {
path,
workspaceId
workspaceId,
onBack
}: {
/** The schedule this tab edits (the row its location deep-links). */
path: string
/** The session's acting workspace, which the whole trigger subtree reads
* through the `triggerWorkspace` resolver instead of `$workspaceStore`. */
workspaceId: string
/** Back to the list this editor was reached through; the tab replaced it. */
onBack?: () => void
} = $props()
// Captured at init, so it must read the current prop rather than close over it.
@@ -33,6 +38,23 @@
<div class="h-full w-full overflow-auto p-4">
<!-- useDrawer=false renders the editor as a Section, the same inline form the
script/flow trigger panel mounts (see SchedulePanel). -->
<ScheduleEditorInner bind:this={editor} useDrawer={false} allowDraft showDraftBanner />
script/flow trigger panel mounts (see SchedulePanel). `customLabel` is that
Section's header, which is where the way back belongs. -->
<ScheduleEditorInner bind:this={editor} useDrawer={false} allowDraft showDraftBanner>
{#snippet customLabel()}
<div class="flex flex-row items-center gap-2 min-w-0">
{#if onBack}
<Button
variant="subtle"
unifiedSize="sm"
startIcon={{ icon: ArrowLeft }}
on:click={onBack}
title="Back to Schedules"
iconOnly
/>
{/if}
<span class="text-sm font-semibold truncate">Schedule</span>
</div>
{/snippet}
</ScheduleEditorInner>
</div>
@@ -4,13 +4,16 @@
let {
path,
workspaceId
workspaceId,
onBack
}: {
/** The variable this tab edits (the row its location deep-links). */
path: string
/** The session's acting workspace, which the editor operates on instead of
* `$workspaceStore` (the nav workspace, which a session leaves put). */
workspaceId: string
/** Back to the list this editor was reached through; the tab replaced it. */
onBack?: () => void
} = $props()
let editor = $state<VariableEditor | undefined>()
@@ -25,4 +28,4 @@
})
</script>
<VariableEditor bind:this={editor} useDrawer={false} workspace={workspaceId} />
<VariableEditor bind:this={editor} useDrawer={false} workspace={workspaceId} {onBack} />
@@ -133,6 +133,14 @@ const IN_REALM_ENTITY_PAGES: Partial<Record<string, EntityEditorKind>> = {
[VARIABLES_PATH]: 'variable'
}
/** The list page an entity editor was reached through, so its host can offer the
* way back. Undefined only if a kind is registered above without a curated page,
* which `PREVIEW_PAGES` covers for all three today. */
export function entityListPage(kind: EntityEditorKind): PreviewPage | undefined {
const path = Object.keys(IN_REALM_ENTITY_PAGES).find((p) => IN_REALM_ENTITY_PAGES[p] === kind)
return path ? matchPreviewPage(path) : undefined
}
/** The workspace entity a preview location opens the editor of — a list page
* with a row deep-linked in its hash, for the pages whose editor mounts in
* process. Undefined for the bare list page (an iframe of the list) and for