mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-26 08:02:31 +00:00
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
100 lines
4.3 KiB
Svelte
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>
|