mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-17 16:03:06 +00:00
feat(sessions): switch preview sides through Exit & see details / Edit
The preview tab's side is now driven by the same affordances the full-page editor and detail page already carry, instead of a View|Edit toggle in the session header: `Exit & see details` in the deploy dropdown flips the tab to the deployed item, and the detail page's `Edit` flips it back. Both stay in the same tab, and neither leaves /sessions. `Exit & see details` is gated on `no_deployed !== true` in both builders, so a draft-only item offers no entry at all — that replaces the toggle's disabled state and its tooltip. `deployedExists` in SessionRuntime existed only to feed that tooltip and is gone. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CoC4MCs3BhVV1DyevrVc7N
This commit is contained in:
co-authored by
Claude Opus 5
parent
b94323cfa1
commit
0a079469f4
@@ -1083,10 +1083,11 @@
|
||||
onClick: () => void
|
||||
}> = []
|
||||
|
||||
// In a session pane every one of these leaves the session (details page, new
|
||||
// tab), so the deploy button carries no dropdown there — as in ScriptBuilder.
|
||||
if (untrack(() => customUi).topBar?.extraDeployOptions != false && !inSessionPane) {
|
||||
if (!newFlow) {
|
||||
if (untrack(() => customUi).topBar?.extraDeployOptions != false) {
|
||||
// The one option that stays in a session pane: the session hosts the details page
|
||||
// itself, so this flips the tab to it rather than leaving the session. Everything
|
||||
// below opens a details page or a new tab outside the session, so it stays hidden.
|
||||
if (!newFlow && savedFlow?.no_deployed !== true) {
|
||||
dropdownItems.push({
|
||||
label: 'Exit & see details',
|
||||
// Use the deployed path, not the live `$pathStore` — the latter
|
||||
@@ -1096,7 +1097,7 @@
|
||||
})
|
||||
}
|
||||
|
||||
if (!untrack(() => newFlow)) {
|
||||
if (!untrack(() => newFlow) && !inSessionPane) {
|
||||
dropdownItems.push({
|
||||
label: 'Fork',
|
||||
onClick: () => window.open(`/flows/add?template=${initialPath}`)
|
||||
@@ -1105,6 +1106,7 @@
|
||||
|
||||
if (
|
||||
!untrack(() => newFlow) &&
|
||||
!inSessionPane &&
|
||||
!isCloudHosted() &&
|
||||
editInForkAllowed(opWorkspace, $userWorkspaces)
|
||||
) {
|
||||
|
||||
@@ -806,9 +806,10 @@
|
||||
|
||||
// Inside an AI session pane (which injects an aiChatManager via context) the
|
||||
// extra deploy-dropdown options — Deploy & Stay here, Fork, Edit in workspace
|
||||
// fork, Exit & See details, Export — don't make sense: the session always
|
||||
// stays put and is already scoped to a fork. Diff is exposed as a standalone
|
||||
// top-bar button (rendered independently of the session pane), not here.
|
||||
// fork, Export — don't make sense: the session always stays put and is already
|
||||
// scoped to a fork. `Exit & See details` is the exception: the session hosts the
|
||||
// details page itself, so it switches the tab instead of leaving. Diff is exposed
|
||||
// as a standalone top-bar button (rendered independently of the pane), not here.
|
||||
const inSessionPane = !!getContext('aiChatManager')
|
||||
|
||||
async function openDiffDrawer() {
|
||||
@@ -874,10 +875,7 @@
|
||||
: [])
|
||||
]
|
||||
: []),
|
||||
...(!inSessionPane &&
|
||||
savedScript?.no_deployed !== true &&
|
||||
script.kind === 'script' &&
|
||||
!script.auto_kind
|
||||
...(savedScript?.no_deployed !== true && script.kind === 'script' && !script.auto_kind
|
||||
? [
|
||||
{
|
||||
label: 'Exit & See details',
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import { userStore } from '$lib/stores'
|
||||
import { createEventDispatcher, getContext, tick } from 'svelte'
|
||||
import { MediaQuery } from 'svelte/reactivity'
|
||||
import SummaryPathDisplay from '$lib/components/SummaryPathDisplay.svelte'
|
||||
import type { TriggerContext } from '../triggers'
|
||||
import type { Item } from '$lib/utils'
|
||||
@@ -49,6 +48,10 @@
|
||||
onSaved?: (newPath: string) => void
|
||||
children?: import('svelte').Snippet
|
||||
trigger_badges?: import('svelte').Snippet
|
||||
/** Whether the bar is wide enough to hold every button beside the summary, decided by
|
||||
* the layout from its own width. Not a viewport media query: the same page also renders
|
||||
* inside an AI session's preview panel, where the window is wide and the pane is not. */
|
||||
wide?: boolean
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -64,15 +67,13 @@
|
||||
inheritedLabels = undefined,
|
||||
onSaved,
|
||||
children,
|
||||
trigger_badges
|
||||
trigger_badges,
|
||||
wide = true
|
||||
}: Props = $props()
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
// Tailwind's `lg`, matched in JS so the one ellipsis menu can carry the collapsed buttons.
|
||||
const wide = new MediaQuery('(min-width: 1024px)')
|
||||
|
||||
const barButtons = $derived(wide.current ? mainButtons : mainButtons.filter((b) => !b.narrow))
|
||||
const barButtons = $derived(wide ? mainButtons : mainButtons.filter((b) => !b.narrow))
|
||||
|
||||
function dropdownHost(btn: MainButton): MainButton | undefined {
|
||||
if (typeof btn.narrow !== 'object') return undefined
|
||||
@@ -90,7 +91,7 @@
|
||||
}
|
||||
|
||||
const allMenuItems: Item[] = $derived([
|
||||
...(wide.current ? [] : mainButtons.filter((b) => b.narrow && !dropdownHost(b))).map((b) => ({
|
||||
...(wide ? [] : mainButtons.filter((b) => b.narrow && !dropdownHost(b))).map((b) => ({
|
||||
displayName: b.label,
|
||||
description: b.description,
|
||||
icon: b.buttonProps.startIcon,
|
||||
@@ -99,7 +100,7 @@
|
||||
disabled: b.buttonProps.disabled,
|
||||
type: 'action' as const
|
||||
})),
|
||||
...(wide.current
|
||||
...(wide
|
||||
? []
|
||||
: [
|
||||
{
|
||||
@@ -114,12 +115,12 @@
|
||||
icon: item.Icon,
|
||||
action: item.onclick,
|
||||
type: item.color === 'red' ? ('delete' as const) : ('action' as const),
|
||||
separatorTop: i === 0 && !wide.current
|
||||
separatorTop: i === 0 && !wide
|
||||
}))
|
||||
])
|
||||
|
||||
function dropdownItemsOf(host: MainButton) {
|
||||
if (wide.current) return undefined
|
||||
if (wide) return undefined
|
||||
const items = mainButtons
|
||||
.filter((b) => dropdownHost(b) === host)
|
||||
.map((b) => ({
|
||||
@@ -181,7 +182,7 @@
|
||||
<DropdownV2 items={allMenuItems} placement="bottom-end" size="md" />
|
||||
{/key}
|
||||
{/if}
|
||||
{#if wide.current}
|
||||
{#if wide}
|
||||
<ErrorHandlerToggleButton
|
||||
kind={errorHandlerKind}
|
||||
{scriptOrFlowPath}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
selected: string
|
||||
forceSmallScreen?: boolean
|
||||
isChatMode?: boolean
|
||||
header?: import('svelte').Snippet
|
||||
header?: import('svelte').Snippet<[{ wide: boolean }]>
|
||||
/** `graphInline`: whether the form should carry the flow graph under it. It does in the
|
||||
* split layout; the tabbed layout gives the graph a tab of its own. */
|
||||
form?: import('svelte').Snippet<[{ graphInline: boolean }]>
|
||||
@@ -70,7 +70,7 @@
|
||||
<main class="h-screen w-full" bind:clientWidth>
|
||||
{#if useDesktopLayout}
|
||||
<div class="h-full w-full flex flex-col">
|
||||
{@render header?.()}
|
||||
{@render header?.({ wide: useDesktopLayout })}
|
||||
<div class="grow min-h-0 w-full">
|
||||
<Splitpanes>
|
||||
<Pane size={65} minSize={50}>
|
||||
@@ -101,7 +101,7 @@
|
||||
</div>
|
||||
{:else}
|
||||
<div class="h-full w-full flex flex-col">
|
||||
{@render header?.()}
|
||||
{@render header?.({ wide: useDesktopLayout })}
|
||||
<div class="grow min-h-0 w-full flex flex-col">
|
||||
<!-- no-scrollbar: at phone widths the tabs overflow their strip, and a browser with
|
||||
classic scrollbars would spend a track under them, opening a band between the tabs
|
||||
|
||||
@@ -603,8 +603,9 @@
|
||||
schema: flow?.schema
|
||||
}}
|
||||
>
|
||||
{#snippet header()}
|
||||
{#snippet header({ wide }: { wide: boolean })}
|
||||
<DetailPageHeader
|
||||
{wide}
|
||||
on:seeTriggers={() => {
|
||||
detailLayout?.showTriggers()
|
||||
}}
|
||||
|
||||
@@ -798,8 +798,9 @@
|
||||
</Drawer>
|
||||
{#key script?.hash}
|
||||
<DetailPageLayout bind:selected={rightPaneSelected} isOperator={$userStore?.operator}>
|
||||
{#snippet header()}
|
||||
{#snippet header({ wide }: { wide: boolean })}
|
||||
<DetailPageHeader
|
||||
{wide}
|
||||
{mainButtons}
|
||||
menuItems={getMenuItems(script, deployUiSettings)}
|
||||
bind:errorHandlerMuted={
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
path,
|
||||
workspaceId,
|
||||
onNavigate,
|
||||
onSeeDetails,
|
||||
isActiveSession = true,
|
||||
active = true,
|
||||
initialSelectedId
|
||||
@@ -23,6 +24,9 @@
|
||||
path: string
|
||||
workspaceId: string
|
||||
onNavigate?: (item: WorkspaceItem) => void
|
||||
/** `Exit & see details` — flips this tab to the item's deployed view rather than
|
||||
* navigating out of the session. */
|
||||
onSeeDetails?: (e: { path: string }) => void
|
||||
/** Forwarded to SessionEditorTarget — only the visible session claims the
|
||||
* workspace's single live-editor slot. */
|
||||
isActiveSession?: boolean
|
||||
@@ -117,6 +121,7 @@
|
||||
bind:savedFlow={cell.saved.val}
|
||||
{diffDrawer}
|
||||
{onNavigate}
|
||||
onDetails={onSeeDetails}
|
||||
condensedHeader={true}
|
||||
customUi={{ topBar: { aiBuilder: false } }}
|
||||
onDeploy={() => {
|
||||
|
||||
@@ -70,9 +70,6 @@
|
||||
const notDeployed = $derived(loadState === 'not_found')
|
||||
function setLoadState(state: 'loaded' | 'not_found') {
|
||||
loadState = state
|
||||
// Tell the runtime, so the View|Edit control can withhold the switch from here on
|
||||
// rather than sending the next reader to this same empty panel.
|
||||
runtime.setDeployedExists(kind, path, state === 'loaded')
|
||||
}
|
||||
$effect(() => {
|
||||
reloadKey
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
darkMode,
|
||||
fullscreen = false,
|
||||
onNavigate,
|
||||
onSeeDetails,
|
||||
onLoad
|
||||
}: {
|
||||
tab: SessionPreviewTab
|
||||
@@ -57,6 +58,9 @@
|
||||
darkMode: boolean
|
||||
/** A link click inside a live editor re-points the active preview tab. */
|
||||
onNavigate: (item: WorkspaceItem) => void
|
||||
/** An editor's `Exit & see details` — re-points this tab at the item's deployed
|
||||
* view instead of navigating out of the session. */
|
||||
onSeeDetails: (item: WorkspaceItem) => void
|
||||
/** Iframe finished loading — the page reads back its observed location. */
|
||||
onLoad: (frame: HTMLIFrameElement) => void
|
||||
} = $props()
|
||||
@@ -112,6 +116,16 @@
|
||||
else if (slot.kind === 'viewer') viewVisited = true
|
||||
})
|
||||
|
||||
// The path an editor reports is its deployed one, which a staged rename can make
|
||||
// differ from this tab's — so the view opens on what actually exists.
|
||||
const seeDetails = (e: { path: string }) =>
|
||||
onSeeDetails({
|
||||
path: e.path,
|
||||
summary: '',
|
||||
kind: itemKind === 'raw_app' ? 'app' : (itemKind as 'script' | 'flow'),
|
||||
raw_app: itemKind === 'raw_app'
|
||||
})
|
||||
|
||||
const sideVisibility = (shown: boolean) =>
|
||||
shown ? 'opacity-100 pointer-events-auto' : 'opacity-0 pointer-events-none'
|
||||
// Where inside the editor the tab was opened on ("open this flow step in a
|
||||
@@ -332,6 +346,7 @@
|
||||
path={itemPath}
|
||||
{workspaceId}
|
||||
{onNavigate}
|
||||
onSeeDetails={seeDetails}
|
||||
{isActiveSession}
|
||||
active={active && mode === 'edit'}
|
||||
initialSelectedId={selectedId}
|
||||
@@ -346,6 +361,7 @@
|
||||
path={itemPath}
|
||||
{workspaceId}
|
||||
{onNavigate}
|
||||
onSeeDetails={seeDetails}
|
||||
{isActiveSession}
|
||||
active={active && mode === 'edit'}
|
||||
{fullscreen}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
path,
|
||||
workspaceId,
|
||||
onNavigate,
|
||||
onSeeDetails,
|
||||
fullscreen = false,
|
||||
isActiveSession = true,
|
||||
active = true
|
||||
@@ -23,6 +24,9 @@
|
||||
path: string
|
||||
workspaceId: string
|
||||
onNavigate?: (item: WorkspaceItem) => void
|
||||
/** `Exit & See details` — flips this tab to the item's deployed view rather than
|
||||
* navigating out of the session. */
|
||||
onSeeDetails?: (e: { path: string }) => void
|
||||
/** Preview panel is in full screen: collapse the test pane in the narrow
|
||||
* side-by-side layout, reopen it when there's room in full screen. */
|
||||
fullscreen?: boolean
|
||||
@@ -114,6 +118,7 @@
|
||||
condensedHeader={true}
|
||||
{diffDrawer}
|
||||
{onNavigate}
|
||||
{onSeeDetails}
|
||||
testPanelCollapsed={!fullscreen}
|
||||
onDeploy={(e) => {
|
||||
// Fires on every deploy (primary, "Deploy & Stay here", and lib — we
|
||||
|
||||
@@ -203,12 +203,6 @@ export interface SessionRuntime {
|
||||
* viewer refetches when this changes — it reads the deployed version from the
|
||||
* API, so nothing else tells it the editor beside it just published. */
|
||||
deployedRevision(kind: SessionTargetKind, path: string): number
|
||||
/** Whether a deployed version exists at (kind, path); `undefined` until something has
|
||||
* looked. Read by the View|Edit control to withhold a switch that could only land on
|
||||
* "not deployed yet". */
|
||||
deployedExists(kind: SessionTargetKind, path: string): boolean | undefined
|
||||
/** Record what a load found, from either side of the item. */
|
||||
setDeployedExists(kind: SessionTargetKind, path: string, exists: boolean): void
|
||||
/** Record that a preview tab landed on `url`. Fires for a newly opened tab and
|
||||
* for an existing one switched to the other side of its item. */
|
||||
logTabUsage(url: string): void
|
||||
@@ -485,18 +479,6 @@ function createRuntime(session: Session): SessionRuntime {
|
||||
function deployedRevision(kind: SessionTargetKind, path: string): number {
|
||||
return deployedRevisions[revisionKey(kind, path)] ?? 0
|
||||
}
|
||||
// Whether a deployed version exists at (kind, path), recorded by whichever side found
|
||||
// out: the editor cell's load carries `no_deployed`, and the viewer's own fetch either
|
||||
// renders the deployed item or 404s. Kept on the runtime rather than either component
|
||||
// so the answer outlives them — a tab opened straight onto the View side has no editor
|
||||
// cell to ask, and the View|Edit control has to know before it can withhold the switch.
|
||||
const deployedExistsByItem = $state<Record<string, boolean>>({})
|
||||
function deployedExists(kind: SessionTargetKind, path: string): boolean | undefined {
|
||||
return deployedExistsByItem[revisionKey(kind, path)]
|
||||
}
|
||||
function setDeployedExists(kind: SessionTargetKind, path: string, exists: boolean): void {
|
||||
deployedExistsByItem[revisionKey(kind, path)] = exists
|
||||
}
|
||||
function logTabUsage(url: string): void {
|
||||
logFeatureUsage('ai_session', 'tab', {
|
||||
key: tabUsageKey(resolvePreviewTab(url)),
|
||||
@@ -607,8 +589,6 @@ function createRuntime(session: Session): SessionRuntime {
|
||||
flowCell,
|
||||
loadedEditorPath,
|
||||
deployedRevision,
|
||||
deployedExists,
|
||||
setDeployedExists,
|
||||
logTabUsage,
|
||||
|
||||
async loadFlow(workspace: string, path: string, force = false) {
|
||||
@@ -645,10 +625,8 @@ function createRuntime(session: Session): SessionRuntime {
|
||||
try {
|
||||
const result = await FlowService.getFlowByPath({ workspace, path, getDraft: true })
|
||||
saved.val = result as SavedFlow
|
||||
setDeployedExists('flow', path, !(result as { no_deployed?: boolean }).no_deployed)
|
||||
} catch {
|
||||
saved.val = undefined
|
||||
setDeployedExists('flow', path, false)
|
||||
}
|
||||
await initFlow(aiDraft, store, stateStore, workspace)
|
||||
if (deployedVersionId != null && store.val) store.val.version_id = deployedVersionId
|
||||
@@ -660,7 +638,6 @@ function createRuntime(session: Session): SessionRuntime {
|
||||
// No local draft yet — seed from `result.draft ?? result`.
|
||||
const result = await FlowService.getFlowByPath({ workspace, path, getDraft: true })
|
||||
saved.val = result as SavedFlow
|
||||
setDeployedExists('flow', path, !(result as { no_deployed?: boolean }).no_deployed)
|
||||
const flow: Flow = ((result as SavedFlow).draft ?? (result as Flow)) as Flow
|
||||
// Seed the per-tab last_sync from the server draft's timestamp so the
|
||||
// seeding save below attaches a matching last_sync and the server can
|
||||
@@ -712,10 +689,8 @@ function createRuntime(session: Session): SessionRuntime {
|
||||
try {
|
||||
const result = await ScriptService.getScriptByPath({ workspace, path, getDraft: true })
|
||||
saved.val = result as SavedScript
|
||||
setDeployedExists('script', path, !(result as { no_deployed?: boolean }).no_deployed)
|
||||
} catch {
|
||||
saved.val = undefined
|
||||
setDeployedExists('script', path, false)
|
||||
}
|
||||
// Clone before layering the AI draft on top, else we'd mutate
|
||||
// `saved.val` in place and lose the pristine diff baseline.
|
||||
@@ -756,7 +731,6 @@ function createRuntime(session: Session): SessionRuntime {
|
||||
// No local draft yet — seed from `result.draft ?? result`.
|
||||
const result = await ScriptService.getScriptByPath({ workspace, path, getDraft: true })
|
||||
saved.val = result as SavedScript
|
||||
setDeployedExists('script', path, !(result as { no_deployed?: boolean }).no_deployed)
|
||||
// Clone before mutating, else `baseline` aliases `result` and
|
||||
// `baseline.parent_hash` corrupts the diff baseline.
|
||||
const baseline = structuredClone(
|
||||
@@ -825,10 +799,8 @@ function createRuntime(session: Session): SessionRuntime {
|
||||
custom_path: result.custom_path,
|
||||
no_deployed: result.no_deployed
|
||||
}
|
||||
setDeployedExists('raw_app', path, !result.no_deployed)
|
||||
} catch {
|
||||
saved.val = undefined
|
||||
setDeployedExists('raw_app', path, false)
|
||||
}
|
||||
store.val = applyDraftToRuntimeRawApp(
|
||||
{
|
||||
@@ -863,7 +835,6 @@ function createRuntime(session: Session): SessionRuntime {
|
||||
custom_path: result.custom_path,
|
||||
no_deployed: result.no_deployed
|
||||
}
|
||||
setDeployedExists('raw_app', path, !result.no_deployed)
|
||||
// Prefer the server draft over the deployed value (mirrors the
|
||||
// flow/script `result.draft ?? result`). A raw-app draft is already
|
||||
// editor-shaped, same keys the extraction below reads.
|
||||
|
||||
@@ -67,11 +67,8 @@
|
||||
parsePreviewItemRoute,
|
||||
previewLocationLabel,
|
||||
resolvePreviewTab,
|
||||
type PreviewItemMode,
|
||||
type PreviewTarget
|
||||
} from '$lib/components/sessions/previewRouter'
|
||||
import ToggleButtonGroup from '$lib/components/common/toggleButton-v2/ToggleButtonGroup.svelte'
|
||||
import ToggleButton from '$lib/components/common/toggleButton-v2/ToggleButton.svelte'
|
||||
import {
|
||||
toolReloadEffect,
|
||||
tabsToReload,
|
||||
@@ -650,68 +647,6 @@
|
||||
// to the plain path.
|
||||
const parsedRoute = $derived(parsePreviewItemRoute(displayPath))
|
||||
|
||||
// The View|Edit control acts on the active tab by re-pointing its URL. Only an item
|
||||
// with two sides gets one: pages, artifacts, pipelines and legacy drag-and-drop apps
|
||||
// have a single side. Read from the tab's `url` rather than the observed `loc` — an
|
||||
// in-realm side reports no location of its own, so `loc` can lag a flip.
|
||||
const activeSide = $derived.by(() => {
|
||||
const url = owner?.activeTab?.url
|
||||
const slot = url ? resolvePreviewTab(url) : undefined
|
||||
if (!slot) return undefined
|
||||
if (slot.kind === 'viewer') {
|
||||
return { kind: slot.viewerKind, path: slot.path, mode: 'view' as PreviewItemMode }
|
||||
}
|
||||
if (slot.kind === 'editor' && slot.editorKind !== 'pipeline') {
|
||||
return { kind: slot.editorKind, path: slot.path, mode: 'edit' as PreviewItemMode }
|
||||
}
|
||||
return undefined
|
||||
})
|
||||
|
||||
// Nothing to view until the item has been deployed once. This only ever hides the
|
||||
// switch when the editor cell has actually been told so by the backend; when it knows
|
||||
// nothing the switch stays open and the viewer says what it found, which is the case
|
||||
// that has to work — a tab can be opened, or restored, straight onto the View side
|
||||
// with no editor ever mounted.
|
||||
const viewDisabledReason = $derived.by(() => {
|
||||
const side = activeSide
|
||||
const rt = activeRuntime
|
||||
if (!side || !rt) return undefined
|
||||
// Through the cell accessor, not `loadedEditorPath`: that one peeks into a plain
|
||||
// Map, so this would read "no cell yet" once and never re-run when the load lands.
|
||||
// The accessor creates the cell on a miss, which costs nothing here — a tab is open
|
||||
// on this item, so its Edit side owns a cell either way.
|
||||
const cell =
|
||||
side.kind === 'flow'
|
||||
? rt.flowCell(side.path)
|
||||
: side.kind === 'script'
|
||||
? rt.scriptCell(side.path)
|
||||
: rt.rawAppCell(side.path)
|
||||
// Two ways to know, because either side can be the one that looked: the editor
|
||||
// cell's `no_deployed`, and what the viewer's own fetch found. An absent baseline is
|
||||
// NOT a third: the cell is created empty and `SessionEditorTarget` clears
|
||||
// `loadedPath` on every unmount, so "this cell knows nothing" is a state a deployed
|
||||
// item reaches too — reading that as "not deployed" offered View on drafts and
|
||||
// withheld it from deployed items, by whether the Edit side happened to be loaded.
|
||||
const undeployed =
|
||||
cell.saved.val?.no_deployed === true || rt.deployedExists(side.kind, side.path) === false
|
||||
return undeployed ? 'Not deployed yet' : undefined
|
||||
})
|
||||
|
||||
function switchSide(mode: PreviewItemMode) {
|
||||
const side = activeSide
|
||||
if (!side || side.mode === mode) return
|
||||
const item: WorkspaceItem = {
|
||||
path: side.path,
|
||||
summary: '',
|
||||
kind: side.kind === 'raw_app' ? 'app' : side.kind,
|
||||
raw_app: side.kind === 'raw_app'
|
||||
}
|
||||
owner?.navigate({ type: 'item', item, mode })
|
||||
// Counted per switch onto the View side, under keys disjoint from the tab-open
|
||||
// ones, so `entity_count` answers how many sessions ever used it.
|
||||
if (mode === 'view') activeRuntime?.logTabUsage(owner?.activeTab?.url ?? '')
|
||||
}
|
||||
|
||||
// Split the item path into breadcrumb dirs + leaf, mirroring EditorHeader:
|
||||
// scope (`f/<folder>` | `u/<user>`) → subfolders → item name. Prefers the
|
||||
// tab's friendly path (a draft-only item's typed name): the picker tree
|
||||
@@ -862,6 +797,12 @@
|
||||
owner?.navigate({ type: 'item', item })
|
||||
}
|
||||
|
||||
// An editor's `Exit & see details`: the session hosts the details page itself, so the
|
||||
// tab moves to the item's deployed view rather than the browser leaving the session.
|
||||
function seeDetailsInPreview(item: WorkspaceItem) {
|
||||
owner?.navigate({ type: 'item', item, mode: 'view' })
|
||||
}
|
||||
|
||||
// A preview iframe that navigates to an editor route posts up to us instead of
|
||||
// booting the editor inside the frame (see the logged layout's beforeNavigate).
|
||||
// Retarget the active tab — the navigating frame is the visible one the user
|
||||
@@ -1049,28 +990,6 @@
|
||||
bind:clientWidth={previewActionsWidth}
|
||||
class="absolute top-0 right-1 z-30 flex h-8 items-center gap-0.5"
|
||||
>
|
||||
{#if activeSide}
|
||||
<!-- Both sides of an item are one tab; this re-points it. -->
|
||||
<div class="mr-1">
|
||||
<ToggleButtonGroup
|
||||
selected={activeSide.mode}
|
||||
on:selected={({ detail }) => switchSide(detail as PreviewItemMode)}
|
||||
>
|
||||
{#snippet children({ item })}
|
||||
<ToggleButton small value="edit" label="Edit" icon={Pen} {item} />
|
||||
<ToggleButton
|
||||
small
|
||||
value="view"
|
||||
label="View"
|
||||
icon={Eye}
|
||||
disabled={!!viewDisabledReason}
|
||||
tooltip={viewDisabledReason}
|
||||
{item}
|
||||
/>
|
||||
{/snippet}
|
||||
</ToggleButtonGroup>
|
||||
</div>
|
||||
{/if}
|
||||
{#if !activeTabHasNoWorkspacePage}
|
||||
<a
|
||||
href={withWorkspaceParam(
|
||||
@@ -1227,6 +1146,7 @@
|
||||
darkMode={isDarkMode.val}
|
||||
{fullscreen}
|
||||
onNavigate={navigateEditorTo}
|
||||
onSeeDetails={seeDetailsInPreview}
|
||||
onLoad={(frame) => tabs && onTabLoad(tabs, tab, frame)}
|
||||
/>
|
||||
{/each}
|
||||
|
||||
Reference in New Issue
Block a user