Files
windmill/frontend/src/lib/components/RunFormAdvancedPopup.svelte
T
GuilhemandClaude Opus 5 b10f83c763 feat(sessions): show an item's deployed page in the preview panel (#11458)
* feat(sessions): show an item's deployed page in the preview panel

The preview panel showed an item's editor and nothing else. It now shows the
deployed page too, as a tab of its own.

**Two sides, two tabs.** The editor and the deployed page are different things
to look at, so `isItemTabFor` keys on the side rather than the item: asking for
a side already open focuses it, asking for the other opens it alongside. Two
editors for one item still cannot coexist — they would race its single
(kind, path) cell — but an editor beside a viewer is safe, since the viewer
reads the deployed version over the API and holds no cell. The tab breadcrumb
keeps re-pointing in place; `Exit & see details` and the detail page's `Edit`
open the other side instead of consuming the one you are on. A tab's label
carries `(edit)` when it is the editor side.

**The detail pages moved out of their routes.** `/scripts/get` and `/flows/get`
are now thin wrappers over `ScriptDetail` and `FlowDetail`, which the panel
renders too. Everything they reach — drawers, triggers, saved inputs — is scoped
to the viewer's workspace through `setOperatingWorkspace`, and every navigation
they attempt is caught by `interceptNav` and turned into a move inside the
panel, so nothing takes the browser out of the session. A plain click is
intercepted; ⌘-click and middle-click still open a real tab, which is why the
pages keep their `href`s.

**The picker opens what exists.** A row opens the deployed page, or the editor
when nothing is deployed there, and carries a Draft / Draft only badge in the
review dock's words. `WorkspaceItem` gained `draftOnly` and `hasDraft` from the
listers, which already returned both; deploys now invalidate the picker cache
through `itemDeployed`, so a just-deployed item stops reading as a draft. Rows
also carry a hover Edit action, reachable from the keyboard with the pick
modifier.

`open_preview` gained a `mode`, narrowed out of the advertised schema for a
session that cannot write drafts and re-checked per path in the handler, where
a refusal reports "couldn't check" apart from "denied". The mode is resolved
before those checks, so a call that omits it is gated as the editor it will
become.

Alongside, the workspace a detail page acts on is now the one it is showing
rather than the one the browser is navigated to: `MoveDrawer`,
`toggleWorkspaceErrorHandler` and the pages' own permission gates read the
operating workspace and its acting user, and `RunForm` mints a password
argument's ephemeral variable there too — in a fork session all of these
previously answered for the parent. `InWorkspaceAppViewer` hands the app's
`ctx` user down as a prop instead of writing the global `userStore`, which from
a session tab was re-pointing every permission check on the page.

Fixes found on the way: `DetailPageLayout` claimed `h-screen` inside a panel
that is not the viewport; Edit on a historical script version dropped the
version from the intercepted click; the Run button stayed spinning after a run
that left the page mounted; and the window-level run shortcut fired from a
collapsed panel and from keys another handler had already claimed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(sessions): historical edits, stale not-found, shared edit rights, tab labels

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(sessions): keep deployed-page links and workspace reads in the session

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(sessions): scope detail hrefs to the session, re-point the viewer's own tab

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(sessions): keep a previewed raw app's route out of the session URL

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(sessions): resolve edit-in-fork against the session workspace

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(sessions): read advanced run tags from the session workspace

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(sessions): fork from the session workspace, star only navigation items

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(sessions): mount only the side a preview tab shows

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(sessions): address CI review round 1 findings

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(sessions): route links in a deployed page's drawers through the panel

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(sessions): tell the chat which deployed page the panel shows

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(sessions): only a 404 reads as an undeployed raw app

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(sessions): scope the unparseable-JSON run gate to the form

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(sessions): a flow deleted from the panel stays in its tab

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(sessions): the picker's edit shortcut works on the current row

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(sessions): load-error toasts say what failed

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(sessions): keep an oversized invalid JSON editor in its form's run gate

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* revert: keep main's draft_only description in openapi.yaml

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-01 17:43:52 +02:00

133 lines
3.5 KiB
Svelte

<script lang="ts">
import { SCRIPT_VIEW_SHOW_SCHEDULE_RUN_LATER } from '$lib/consts'
import Label from './Label.svelte'
import Toggle from './Toggle.svelte'
import Tooltip from './Tooltip.svelte'
import { userStore, workerTags, workspaceStore } from '$lib/stores'
import { Button } from './common'
import { WorkerService } from '$lib/gen'
import DateTimeInput from './DateTimeInput.svelte'
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
interface Props {
runnable:
| {
summary?: string
description?: string
path?: string
is_template?: boolean
hash?: string
kind?: string
can_write?: boolean
created_at?: string
created_by?: string
extra_perms?: Record<string, boolean>
}
| undefined;
scheduledForStr: string | undefined;
invisible_to_owner: boolean | undefined;
overrideTag: string | undefined;
}
let {
runnable,
scheduledForStr = $bindable(),
invisible_to_owner = $bindable(),
overrideTag = $bindable()
}: Props = $props();
// As in WorkerTagPicker: the shared `workerTags` cache is the navigation workspace's, so
// tags of another operating workspace live in a local list rather than overwriting it.
const operatingWorkspace = useOperatingWorkspace()
let usesLocal = $derived($operatingWorkspace !== $workspaceStore)
let localWorkerTags = $state<string[] | undefined>(undefined)
let currentTags = $derived(usesLocal ? localWorkerTags : $workerTags)
loadWorkerGroups()
async function loadWorkerGroups() {
if (usesLocal) {
localWorkerTags ??= await WorkerService.getCustomTagsForWorkspace({
workspace: $operatingWorkspace!
})
} else if (!$workerTags) {
$workerTags = await WorkerService.getCustomTagsForWorkspace({ workspace: $workspaceStore! })
}
}
</script>
<div class="flex flex-col gap-4 p-4">
<div class="flex flex-col gap-2">
{#if SCRIPT_VIEW_SHOW_SCHEDULE_RUN_LATER}
<div class="">
<Label label="Schedule to run later" />
<div class="flex flex-row items-center my-1">
<div>
<label for="run-time"></label>
<DateTimeInput
value={scheduledForStr}
on:change={(e) => {
console.log('e.detail', e.detail)
scheduledForStr = e.detail
}}
/>
</div>
<Button
variant="default"
size="xs"
btnClasses="mx-2 "
on:click={() => {
scheduledForStr = undefined
}}
>
Clear
</Button>
</div>
</div>
{/if}
</div>
{#if !$userStore?.operator}
{#if currentTags && currentTags.length > 0}
<div class="w-full">
<select
placeholder="Worker group"
bind:value={
() => overrideTag ?? '',
(v) => {
if (v == '') {
overrideTag = undefined
} else {
overrideTag = v
}
}
}
>
{#if overrideTag}
<option value="">reset to default</option>
{:else}
<option value="" disabled selected>Override Worker Group Tag</option>
{/if}
{#each currentTags ?? [] as tag (tag)}
<option value={tag}>{tag}</option>
{/each}
</select>
</div>
{/if}
{/if}
{#if runnable?.path?.startsWith(`u/${$userStore?.username}`) != true && (runnable?.path?.split('/')?.length ?? 0) > 2}
<div class="flex items-center gap-1">
<Toggle
options={{
right: `make run invisible to others`
}}
bind:checked={invisible_to_owner}
/>
<Tooltip
>By default, runs are visible to the owner(s) of the script or flow being triggered</Tooltip
>
</div>
{/if}
</div>