Files
windmill/frontend/src/lib/components/flows/content/WorkspaceScriptSettingsDrawer.svelte
T
6186a0645d feat: edit variables, resources and triggers in their own session tab (#11206)
* feat: edit variables, resources and triggers in their own session tab

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

* fix: take a trigger's flow kind from its config and name the written trigger

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

* refactor: one operating-workspace context for editors acting on a session's workspace

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

* fix: reload page item tabs only in sessions acting on the tool's workspace

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

* fix: resolve open in workspace for every preview tab kind in one place

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

* fix: guard every component under a session editor against the navigation workspace

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

* fix: show a page item tab whose item is gone instead of mounting its editor

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

* 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) <noreply@anthropic.com>

* feat: close a page item tab from its editor header

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

* fix: judge trigger permissions by the user acting in the session workspace

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

* fix: judge every session-editor permission by the user acting in that workspace

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

* fix: keep session permissions reactive to the acting role and restore tabs as last seen

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

* fix: default Path's owner controls to the user acting in its workspace

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
2026-09-19 23:29:14 +02:00

153 lines
5.6 KiB
Svelte

<script lang="ts">
import { Button, Drawer, DrawerContent } from '$lib/components/common'
import Alert from '$lib/components/common/alert/Alert.svelte'
import ScriptAdvancedSettings from '$lib/components/ScriptAdvancedSettings.svelte'
import ScriptSettingsBadges from '$lib/components/ScriptSettingsBadges.svelte'
import { ScriptService, type Script } from '$lib/gen'
import { emptyString, sendUserToast } from '$lib/utils'
import { Loader2, Save } from 'lucide-svelte'
import { getContext } from 'svelte'
import type { FlowEditorContext } from '../types'
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
const operatingWorkspace = useOperatingWorkspace()
const flowEditorContext = getContext<FlowEditorContext>('FlowEditorContext')
let opWs = $derived(flowEditorContext?.opWorkspace?.() ?? $operatingWorkspace)
let drawer: Drawer | undefined = $state()
// cache_ignore_s3_path lives on NewScript but not the returned Script type;
// ScriptAdvancedSettings still edits it, so widen the local type to keep it.
let script: (Script & { cache_ignore_s3_path?: boolean }) | undefined = $state(undefined)
let loading = $state(false)
let loadError = $state<string | undefined>(undefined)
let saving = $state(false)
let callback: (() => void) | undefined = undefined
let current: { path: string; hash: string | undefined } | undefined = undefined
// Guards against a slow load for a previously-opened script resolving after
// the drawer was reopened for a different one and overwriting its target.
let openSeq = 0
// Open the settings drawer for the workspace script referenced by `path`.
// A specific `hash` may be passed to load that version.
export async function openDrawer(
path: string,
hash: string | undefined,
cb?: () => void
): Promise<void> {
callback = cb
drawer?.openDrawer?.()
await load(path, hash)
}
async function load(path: string, hash: string | undefined): Promise<void> {
const seq = ++openSeq
script = undefined
loadError = undefined
current = { path, hash }
loading = true
// A save still in flight for the previous target no longer clears this flag
// (it is seq-guarded), so reset it here or the new target's Save stays disabled.
saving = false
try {
const loaded = hash
? await ScriptService.getScriptByHash({ workspace: opWs!, hash })
: await ScriptService.getScriptByPath({ workspace: opWs!, path })
if (seq !== openSeq) return
script = loaded
} catch (e) {
if (seq === openSeq) loadError = `${e?.body ?? e}`
} finally {
if (seq === openSeq) loading = false
}
}
async function save(): Promise<void> {
if (!script) return
// The drawer is a singleton: bind this save to the target that started it, so a
// reopen for another script can't receive its callback or get closed under the
// user. The captured callback still fires — it refreshes the script it belongs to.
const seq = openSeq
const cb = callback
saving = true
try {
// Spread the full loaded script so fields we don't edit here (codebase,
// labels, envs, on_behalf_of_email, ...) survive the new version; only
// override lineage and normalize the edited settings.
// parent_hash without auto_parent is an optimistic-concurrency guard: if the
// script was deployed elsewhere while the drawer was open, the save fails
// loudly (non-linear lineage) instead of silently reverting that deploy.
// preserve_on_behalf_of/skip_draft_deletion keep a settings-only save from
// hijacking the execution identity or discarding the author's code draft.
await ScriptService.createScript({
workspace: opWs!,
requestBody: {
...script,
summary: script.summary ?? '',
description: script.description ?? '',
parent_hash: script.hash,
lock: script.lock,
concurrency_key: emptyString(script.concurrency_key) ? undefined : script.concurrency_key,
debounce_key: emptyString(script.debounce_key) ? undefined : script.debounce_key,
preserve_on_behalf_of: true,
skip_draft_deletion: true
}
})
sendUserToast('Script settings saved')
cb?.()
if (seq === openSeq) drawer?.closeDrawer()
} catch (e) {
if (seq === openSeq) sendUserToast(`Could not save script settings: ${e.body ?? e}`, true)
} finally {
if (seq === openSeq) saving = false
}
}
</script>
<Drawer bind:this={drawer} size="600px">
<DrawerContent title="Script settings" on:close={() => drawer?.closeDrawer()}>
{#if loading}
<div class="center-center flex-col h-full text-tertiary">
<Loader2 class="animate-spin" size={16} />
<span class="text-xs mt-1">Loading</span>
</div>
{:else if loadError || !script}
<div class="center-center flex-col h-full gap-3">
<Alert type="error" title="Could not load script settings" size="xs">
{loadError ?? 'Script not found.'}
</Alert>
{#if current}
<Button
size="xs"
variant="border"
on:click={() => current && load(current.path, current.hash)}
>
Retry
</Button>
{/if}
</div>
{:else}
<div class="flex flex-col gap-4">
<div class="flex flex-row items-center gap-2 flex-wrap">
<span class="text-xs text-tertiary">{script.path}</span>
<ScriptSettingsBadges settings={script} />
</div>
<p class="text-xs text-secondary">
Saving creates a new version of the workspace script with these runtime settings. The code
is left unchanged.
</p>
<ScriptAdvancedSettings {script} workspaceId={opWs} />
</div>
{/if}
{#snippet actions()}
<Button
on:click={save}
disabled={!script || loading || saving}
startIcon={{ icon: saving ? Loader2 : Save }}
>
Save
</Button>
{/snippet}
</DrawerContent>
</Drawer>