Files
windmill/frontend/src/lib/components/flows/content/WorkspaceScriptSettingsDrawer.svelte
T
0249bb94ac refactor: harmonize flow and script settings layout (#12013)
* refactor: harmonize flow and script settings layout

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

* fix: keep the priority input open while it is edited

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

* fix: keep settings rows open while edited and name the deploying user for non-admins

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

* fix: write a setting before its row opens and show its real state while edited

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

* fix: slide settings rows open to their full height

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

* fix: remount a settings row's fields when its toggle turns it on

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

* fix: put the AI prompt behind a settings toggle

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat: summarize active runtime and permission settings on the metadata tab

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor: move the active settings chips into the settings header

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: bring back the active setting icons on the settings tabs

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
2026-10-27 01:24:09 +01: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 ScriptRuntimeSettings from '$lib/components/itemSettings/ScriptRuntimeSettings.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;
// ScriptRuntimeSettings 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="Runtime 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
unifiedSize="xs"
variant="default"
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>
<ScriptRuntimeSettings {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>