Files
windmill/frontend/src/lib/components/sessions/ScheduleEditorView.svelte
T
Diego ImbertandClaude Opus 5 c7faaf1956 fix(sessions): report a write per workspace, and leave the reporting editor alone
The same-path refresh reached the tab that did the save, remounting it over the
edit its own settle had just preserved — the teardown's flush and the fresh
editor's read race, and the older value can win. The reporting tab is skipped:
it has settled its own state, which is what the report is telling everyone else.

A save can also write several workspaces while only one event went out, labelled
with the acting one, and a discard deletes the draft of whichever workspace the
form is showing while claiming the acting one — so a linked workspace's tabs kept
a replaced baseline and the wrong tabs were closed. Each write now reports its
own workspace and the path it wrote there, which retires the acting-workspace
bookkeeping that stood in for it.

A tab re-pointed mid-write no longer swallows the report either: only the local
remount was ever its to suppress.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KcyBRxE8j3ujCYkum9KrC4
2026-09-06 07:23:13 +02:00

100 lines
4.3 KiB
Svelte

<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,
onBack,
onRemoved,
onSavedTo
}: {
/** 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
/** The item at `fromPath` is gone — a draft-only one whose draft was discarded.
* Distinct from `onBack`, which moves whichever tab is active: the discard
* awaits a reload of the runnable, by which time the user may be looking at
* another tab, or have pointed this one somewhere else. */
onRemoved?: (fromPath: string, fromWorkspace: string) => void
/** The item at `fromPath` was saved, to `newPath`. Every tab on it has to hear:
* a rename moves the ones addressing it by path — their label, the chat's
* ACTIVE PREVIEW and the draft key all do — and a plain save moves none but
* leaves the others holding a baseline the deploy has replaced. */
onSavedTo?: (newPath: string, fromPath: string, fromWorkspace: string) => void
} = $props()
// Captured at init, so it must read the current prop rather than close over it.
setTriggerWorkspace(() => workspaceId)
let editor = $state<ScheduleEditorInner | undefined>()
// A save leaves the mounted editor holding the pre-save config as its deployed
// baseline — the drawer hides that by closing, which inline is a no-op, so the
// banner would go on claiming unsaved changes and Discard would restore the
// value the save just replaced. Remounting re-reads the saved schedule.
let generation = $state(0)
// Loads the schedule this tab holds, on the instance the `{#key}` below just
// mounted for it — `editor` is rebound per instance, so this re-runs per path
// and per generation. `isFlow` is a first guess only — loadScheduleCfg sets it
// from the loaded config — so the tab needs no knowledge beyond the path.
$effect(() => {
const e = editor
if (!path || !e) return
untrack(() => void e.openEdit(path, false))
})
</script>
<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). `customLabel` is that
Section's header, which is where the way back belongs. -->
<!-- Keyed on the path as well: `openEdit` loads asynchronously, and re-pointing a
live editor would leave the previous schedule's load to finish into the new
one's form and deployed baseline. -->
{#key `${path}#${generation}`}
<!-- No `allowDraft`: it switches the toolbar to the trigger-panel branch, whose
deploy button is gated on the `trigger`/`isDeployed` a script or flow editor
supplies alongside a staged trigger. A standalone tab has neither, and would
be left permanently unsavable; this keeps the drawer's plain Save. -->
<ScheduleEditorInner
bind:this={editor}
useDrawer={false}
showDraftBanner
{onRemoved}
onUpdate={(saved: string | undefined, from: string, fromWs: string) => {
// Remount only while this tab is still on the schedule that was saved:
// pointed elsewhere since, it would be taken back through a load it never
// asked for. The report goes out either way — the tabs on that schedule
// elsewhere have to hear, and a draft-only save can land on a new path.
if (from === path) generation++
if (saved) onSavedTo?.(saved, from, fromWs)
}}
>
{#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>
{/key}
</div>