mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-07 08:02:40 +00:00
fix: address review findings on trigger draft reconciliation
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
20226a15b6
commit
de14e6a1fb
@@ -865,11 +865,11 @@ async fn list_schedule(
|
||||
// The `draft` row's key addresses the schedule; the value's `path` is
|
||||
// only where it will deploy. Reporting the key as `path` is what lets
|
||||
// a renamed draft still be opened, updated and deleted.
|
||||
// The row key addresses the draft, so a value with no `path` yet (a
|
||||
// half-filled editor) is still listable — same as the trigger handler.
|
||||
let intended_path = v.get("path").and_then(|s| s.as_str()).unwrap_or("");
|
||||
if intended_path.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let draft_path = (intended_path != row.path).then(|| intended_path.to_string());
|
||||
let draft_path =
|
||||
(!intended_path.is_empty() && intended_path != row.path).then(|| intended_path.to_string());
|
||||
let path = row.path.clone();
|
||||
let schedule = v
|
||||
.get("schedule")
|
||||
|
||||
@@ -672,7 +672,9 @@ async fn list_triggers<T: TriggerCrud>(
|
||||
// only where it will deploy. Surfacing the key as `path` is what lets
|
||||
// a renamed draft still be opened, updated and deleted.
|
||||
if let Some(intended) = map.insert("path".into(), row.path.clone().into()) {
|
||||
if intended.as_str() != Some(row.path.as_str()) {
|
||||
let intended_str = intended.as_str().unwrap_or("");
|
||||
// A half-filled editor may not have a path yet; that is not a rename.
|
||||
if !intended_str.is_empty() && intended_str != row.path {
|
||||
map.insert("draft_path".into(), intended);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1188,7 +1188,9 @@
|
||||
selectedId && untrack(() => select(selectedId))
|
||||
})
|
||||
$effect.pre(() => {
|
||||
initialPath && initialPath != '' && opWorkspace && untrack(() => loadTriggers())
|
||||
// Not gated on `initialPath`: an undeployed flow has no deployed triggers
|
||||
// but can already have drafted ones (loadTriggers falls back to $pathStore).
|
||||
opWorkspace && untrack(() => loadTriggers())
|
||||
})
|
||||
$effect.pre(() => {
|
||||
const hasAiDiff = aiChatManager.flowAiChatHelpers?.hasPendingChanges() ?? false
|
||||
|
||||
@@ -13,9 +13,14 @@
|
||||
...restProps
|
||||
} = $props()
|
||||
|
||||
function openScheduleEditor(isFlow: boolean) {
|
||||
async function openScheduleEditor(isFlow: boolean) {
|
||||
if (selectedTrigger.isNew) {
|
||||
scheduleEditor?.openNew(isFlow, path, { ...defaultValues, path: selectedTrigger.path })
|
||||
await scheduleEditor?.openNew(isFlow, path, { ...defaultValues, path: selectedTrigger.path })
|
||||
// The autosave inside `openNew` created the draft row, so this trigger is
|
||||
// no longer new: re-selecting it must reload that row, not reset the form
|
||||
// to defaults and overwrite what the user just configured.
|
||||
selectedTrigger.isNew = false
|
||||
selectedTrigger.newTriggerSeed = undefined
|
||||
} else {
|
||||
scheduleEditor?.openEdit(selectedTrigger.path, isFlow, defaultValues)
|
||||
}
|
||||
|
||||
@@ -309,18 +309,19 @@
|
||||
})
|
||||
|
||||
export async function loadTriggers() {
|
||||
if (!initialPath) {
|
||||
return
|
||||
// An undeployed script has no deployed triggers to count, but it can already
|
||||
// have drafted ones — fetch against the path they were drafted at.
|
||||
if (initialPath) {
|
||||
$triggersCount = await ScriptService.getTriggersCountOfScript({
|
||||
workspace: opWorkspace!,
|
||||
path: initialPath
|
||||
})
|
||||
}
|
||||
$triggersCount = await ScriptService.getTriggersCountOfScript({
|
||||
workspace: opWorkspace!,
|
||||
path: initialPath
|
||||
})
|
||||
|
||||
await triggersState.fetchTriggers(
|
||||
triggersCount,
|
||||
opWorkspace,
|
||||
initialPath,
|
||||
initialPath || script.path,
|
||||
false,
|
||||
$primaryScheduleStore,
|
||||
$userStore
|
||||
@@ -1017,7 +1018,9 @@
|
||||
}
|
||||
}
|
||||
$effect(() => {
|
||||
initialPath != '' && untrack(() => loadTriggers())
|
||||
// Not gated on `initialPath`: an undeployed script can already have drafted
|
||||
// triggers (loadTriggers falls back to the live path).
|
||||
untrack(() => loadTriggers())
|
||||
})
|
||||
let langs = $derived(
|
||||
processLangs(script.language, $defaultScripts?.order ?? Object.keys(defaultScriptLanguages))
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { userStore, workspaceStore } from '$lib/stores'
|
||||
import { UserDraft } from '$lib/userDraft.svelte'
|
||||
import FlowCard from '../flows/common/FlowCard.svelte'
|
||||
import { getContext, onDestroy, createEventDispatcher } from 'svelte'
|
||||
import { getContext, onDestroy, createEventDispatcher, tick } from 'svelte'
|
||||
import type { TriggerContext } from '$lib/components/triggers'
|
||||
import { Pane, Splitpanes } from 'svelte-splitpanes'
|
||||
import TriggersTable from './TriggersTable.svelte'
|
||||
@@ -171,9 +171,10 @@
|
||||
triggersState.deleteTrigger(triggersCount, triggerIndex)
|
||||
triggersState.selectedTriggerIndex = triggersState.triggers.length - 1
|
||||
|
||||
// Let the panel unmount before dropping the row: its autosave still holds
|
||||
// the config and would write it straight back as a fresh draft.
|
||||
await tick()
|
||||
// Drop the draft row too, else the trigger reappears on the next fetch.
|
||||
// After the list update, so the editor panel's autosave has unmounted
|
||||
// rather than re-persisting the config it still holds.
|
||||
const draftKind = triggerDraftKind(trigger.type)
|
||||
if (draftKind && trigger.path && $workspaceStore) {
|
||||
UserDraft.remove(draftKind, trigger.path, { workspace: $workspaceStore })
|
||||
|
||||
@@ -55,6 +55,15 @@
|
||||
onEmailDomain,
|
||||
...props
|
||||
}: Props = $props()
|
||||
// `newTriggerSeed` seeds `openNew` and applies ONLY while the trigger is new.
|
||||
// Once its row exists, passing anything here would short-circuit the editors'
|
||||
// backend load (`loadTrigger` returns early on a `defaultConfig`) and show the
|
||||
// seed instead of the saved draft — overwriting it on the next autosave.
|
||||
const draftDefaults = $derived(
|
||||
selectedTrigger.isNew
|
||||
? selectedTrigger.newTriggerSeed
|
||||
: (selectedTrigger.draftConfig ?? selectedTrigger.captureConfig ?? undefined)
|
||||
)
|
||||
</script>
|
||||
|
||||
{#if selectedTrigger.type === 'http'}
|
||||
@@ -62,7 +71,7 @@
|
||||
{selectedTrigger}
|
||||
{isFlow}
|
||||
path={initialPath || fakeInitialPath}
|
||||
defaultValues={selectedTrigger.draftConfig ?? selectedTrigger.captureConfig ?? undefined}
|
||||
defaultValues={draftDefaults}
|
||||
{customLabel}
|
||||
{...props}
|
||||
/>
|
||||
@@ -90,7 +99,7 @@
|
||||
{isFlow}
|
||||
path={initialPath || fakeInitialPath}
|
||||
{selectedTrigger}
|
||||
defaultValues={selectedTrigger.draftConfig ?? selectedTrigger.captureConfig ?? undefined}
|
||||
defaultValues={draftDefaults}
|
||||
{schema}
|
||||
{customLabel}
|
||||
{...props}
|
||||
@@ -100,7 +109,7 @@
|
||||
{isFlow}
|
||||
path={initialPath || fakeInitialPath}
|
||||
{selectedTrigger}
|
||||
defaultValues={selectedTrigger.draftConfig ?? selectedTrigger.captureConfig ?? undefined}
|
||||
defaultValues={draftDefaults}
|
||||
{customLabel}
|
||||
{...props}
|
||||
/>
|
||||
@@ -109,7 +118,7 @@
|
||||
{isFlow}
|
||||
path={initialPath || fakeInitialPath}
|
||||
{selectedTrigger}
|
||||
defaultValues={selectedTrigger.draftConfig ?? selectedTrigger.captureConfig ?? undefined}
|
||||
defaultValues={draftDefaults}
|
||||
{customLabel}
|
||||
{...props}
|
||||
/>
|
||||
@@ -118,7 +127,7 @@
|
||||
{isFlow}
|
||||
path={initialPath || fakeInitialPath}
|
||||
{selectedTrigger}
|
||||
defaultValues={selectedTrigger.draftConfig ?? selectedTrigger.captureConfig ?? undefined}
|
||||
defaultValues={draftDefaults}
|
||||
{customLabel}
|
||||
{...props}
|
||||
/>
|
||||
@@ -127,7 +136,7 @@
|
||||
{isFlow}
|
||||
path={initialPath || fakeInitialPath}
|
||||
{selectedTrigger}
|
||||
defaultValues={selectedTrigger.draftConfig ?? selectedTrigger.captureConfig ?? undefined}
|
||||
defaultValues={draftDefaults}
|
||||
{customLabel}
|
||||
{...props}
|
||||
/>
|
||||
@@ -136,7 +145,7 @@
|
||||
{isFlow}
|
||||
path={initialPath || fakeInitialPath}
|
||||
{selectedTrigger}
|
||||
defaultValues={selectedTrigger.draftConfig ?? selectedTrigger.captureConfig ?? undefined}
|
||||
defaultValues={draftDefaults}
|
||||
{customLabel}
|
||||
{...props}
|
||||
/>
|
||||
@@ -145,7 +154,7 @@
|
||||
{isFlow}
|
||||
path={initialPath || fakeInitialPath}
|
||||
{selectedTrigger}
|
||||
defaultValues={selectedTrigger.draftConfig ?? selectedTrigger.captureConfig ?? undefined}
|
||||
defaultValues={draftDefaults}
|
||||
{customLabel}
|
||||
{...props}
|
||||
/>
|
||||
@@ -154,7 +163,7 @@
|
||||
{isFlow}
|
||||
path={initialPath || fakeInitialPath}
|
||||
{selectedTrigger}
|
||||
defaultValues={selectedTrigger.draftConfig ?? selectedTrigger.captureConfig ?? undefined}
|
||||
defaultValues={draftDefaults}
|
||||
{customLabel}
|
||||
{...props}
|
||||
/>
|
||||
@@ -163,7 +172,7 @@
|
||||
{isFlow}
|
||||
path={initialPath || fakeInitialPath}
|
||||
{selectedTrigger}
|
||||
defaultValues={selectedTrigger.draftConfig ?? selectedTrigger.captureConfig ?? undefined}
|
||||
defaultValues={draftDefaults}
|
||||
{customLabel}
|
||||
{...props}
|
||||
/>
|
||||
@@ -172,7 +181,7 @@
|
||||
{isFlow}
|
||||
path={initialPath || fakeInitialPath}
|
||||
{selectedTrigger}
|
||||
defaultValues={selectedTrigger.draftConfig ?? selectedTrigger.captureConfig ?? undefined}
|
||||
defaultValues={draftDefaults}
|
||||
{customLabel}
|
||||
{...props}
|
||||
/>
|
||||
@@ -181,7 +190,7 @@
|
||||
{isFlow}
|
||||
path={initialPath || fakeInitialPath}
|
||||
{selectedTrigger}
|
||||
defaultValues={selectedTrigger.draftConfig ?? selectedTrigger.captureConfig ?? undefined}
|
||||
defaultValues={draftDefaults}
|
||||
{customLabel}
|
||||
{onEmailDomain}
|
||||
{...props}
|
||||
@@ -194,7 +203,7 @@
|
||||
{isFlow}
|
||||
path={initialPath || fakeInitialPath}
|
||||
{selectedTrigger}
|
||||
defaultValues={selectedTrigger.draftConfig ?? selectedTrigger.captureConfig ?? undefined}
|
||||
defaultValues={draftDefaults}
|
||||
{customLabel}
|
||||
{...props}
|
||||
/>
|
||||
@@ -204,7 +213,7 @@
|
||||
{isFlow}
|
||||
path={initialPath || fakeInitialPath}
|
||||
{selectedTrigger}
|
||||
defaultValues={selectedTrigger.draftConfig ?? selectedTrigger.captureConfig ?? undefined}
|
||||
defaultValues={draftDefaults}
|
||||
{customLabel}
|
||||
{...props}
|
||||
/>
|
||||
@@ -214,7 +223,7 @@
|
||||
{isFlow}
|
||||
path={initialPath || fakeInitialPath}
|
||||
{selectedTrigger}
|
||||
defaultValues={selectedTrigger.draftConfig ?? selectedTrigger.captureConfig ?? undefined}
|
||||
defaultValues={draftDefaults}
|
||||
{customLabel}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
@@ -19,7 +19,12 @@
|
||||
|
||||
async function openAmqpTriggerEditor(isFlow: boolean) {
|
||||
if (selectedTrigger.isNew) {
|
||||
amqpTriggerEditor?.openNew(isFlow, path, { ...defaultValues, path: selectedTrigger.path })
|
||||
await amqpTriggerEditor?.openNew(isFlow, (selectedTrigger.newTriggerSeed?.script_path ?? path), { ...defaultValues, path: selectedTrigger.path })
|
||||
// The autosave inside `openNew` created the draft row, so this trigger is
|
||||
// no longer new: re-selecting it must reload that row, not reset the form
|
||||
// to defaults and overwrite what the user just configured.
|
||||
selectedTrigger.isNew = false
|
||||
selectedTrigger.newTriggerSeed = undefined
|
||||
} else {
|
||||
amqpTriggerEditor?.openEdit(selectedTrigger.path, isFlow, selectedTrigger.draftConfig)
|
||||
}
|
||||
|
||||
@@ -19,7 +19,12 @@
|
||||
|
||||
async function openAzureTriggerEditor(isFlow: boolean) {
|
||||
if (selectedTrigger.isNew) {
|
||||
azureTriggerEditor?.openNew(isFlow, path, { ...defaultValues, path: selectedTrigger.path })
|
||||
await azureTriggerEditor?.openNew(isFlow, (selectedTrigger.newTriggerSeed?.script_path ?? path), { ...defaultValues, path: selectedTrigger.path })
|
||||
// The autosave inside `openNew` created the draft row, so this trigger is
|
||||
// no longer new: re-selecting it must reload that row, not reset the form
|
||||
// to defaults and overwrite what the user just configured.
|
||||
selectedTrigger.isNew = false
|
||||
selectedTrigger.newTriggerSeed = undefined
|
||||
} else {
|
||||
azureTriggerEditor?.openEdit(selectedTrigger.path, isFlow, defaultValues)
|
||||
}
|
||||
|
||||
@@ -31,7 +31,12 @@
|
||||
|
||||
async function openEmailTriggerEditor(isFlow: boolean) {
|
||||
if (selectedTrigger.isNew) {
|
||||
emailTriggerEditor?.openNew(isFlow, path, { ...defaultValues, path: selectedTrigger.path })
|
||||
await emailTriggerEditor?.openNew(isFlow, (selectedTrigger.newTriggerSeed?.script_path ?? path), { ...defaultValues, path: selectedTrigger.path })
|
||||
// The autosave inside `openNew` created the draft row, so this trigger is
|
||||
// no longer new: re-selecting it must reload that row, not reset the form
|
||||
// to defaults and overwrite what the user just configured.
|
||||
selectedTrigger.isNew = false
|
||||
selectedTrigger.newTriggerSeed = undefined
|
||||
} else {
|
||||
emailTriggerEditor?.openEdit(selectedTrigger.path ?? '', isFlow, defaultValues)
|
||||
}
|
||||
|
||||
@@ -19,7 +19,12 @@
|
||||
|
||||
async function openGcpTriggerEditor(isFlow: boolean) {
|
||||
if (selectedTrigger.isNew) {
|
||||
gcpTriggerEditor?.openNew(isFlow, path, { ...defaultValues, path: selectedTrigger.path })
|
||||
await gcpTriggerEditor?.openNew(isFlow, (selectedTrigger.newTriggerSeed?.script_path ?? path), { ...defaultValues, path: selectedTrigger.path })
|
||||
// The autosave inside `openNew` created the draft row, so this trigger is
|
||||
// no longer new: re-selecting it must reload that row, not reset the form
|
||||
// to defaults and overwrite what the user just configured.
|
||||
selectedTrigger.isNew = false
|
||||
selectedTrigger.newTriggerSeed = undefined
|
||||
} else {
|
||||
gcpTriggerEditor?.openEdit(selectedTrigger.path, isFlow, defaultValues)
|
||||
}
|
||||
|
||||
@@ -18,7 +18,12 @@
|
||||
|
||||
async function openRouteEditor(isFlow: boolean) {
|
||||
if (selectedTrigger.isNew) {
|
||||
routeEditor?.openNew(isFlow, path, { ...defaultValues, path: selectedTrigger.path })
|
||||
await routeEditor?.openNew(isFlow, (selectedTrigger.newTriggerSeed?.script_path ?? path), { ...defaultValues, path: selectedTrigger.path })
|
||||
// The autosave inside `openNew` created the draft row, so this trigger is
|
||||
// no longer new: re-selecting it must reload that row, not reset the form
|
||||
// to defaults and overwrite what the user just configured.
|
||||
selectedTrigger.isNew = false
|
||||
selectedTrigger.newTriggerSeed = undefined
|
||||
} else {
|
||||
routeEditor?.openEdit(selectedTrigger.path, isFlow, defaultValues)
|
||||
}
|
||||
|
||||
@@ -20,7 +20,12 @@
|
||||
|
||||
async function openKafkaTriggerEditor(isFlow: boolean) {
|
||||
if (selectedTrigger.isNew) {
|
||||
kafkaTriggerEditor?.openNew(isFlow, path, { ...defaultValues, path: selectedTrigger.path })
|
||||
await kafkaTriggerEditor?.openNew(isFlow, (selectedTrigger.newTriggerSeed?.script_path ?? path), { ...defaultValues, path: selectedTrigger.path })
|
||||
// The autosave inside `openNew` created the draft row, so this trigger is
|
||||
// no longer new: re-selecting it must reload that row, not reset the form
|
||||
// to defaults and overwrite what the user just configured.
|
||||
selectedTrigger.isNew = false
|
||||
selectedTrigger.newTriggerSeed = undefined
|
||||
} else {
|
||||
kafkaTriggerEditor?.openEdit(selectedTrigger.path, isFlow, defaultValues)
|
||||
}
|
||||
|
||||
@@ -19,7 +19,12 @@
|
||||
|
||||
async function openMqttTriggerEditor(isFlow: boolean) {
|
||||
if (selectedTrigger.isNew) {
|
||||
mqttTriggerEditor?.openNew(isFlow, path, { ...defaultValues, path: selectedTrigger.path })
|
||||
await mqttTriggerEditor?.openNew(isFlow, (selectedTrigger.newTriggerSeed?.script_path ?? path), { ...defaultValues, path: selectedTrigger.path })
|
||||
// The autosave inside `openNew` created the draft row, so this trigger is
|
||||
// no longer new: re-selecting it must reload that row, not reset the form
|
||||
// to defaults and overwrite what the user just configured.
|
||||
selectedTrigger.isNew = false
|
||||
selectedTrigger.newTriggerSeed = undefined
|
||||
} else {
|
||||
mqttTriggerEditor?.openEdit(selectedTrigger.path, isFlow, selectedTrigger.draftConfig)
|
||||
}
|
||||
|
||||
@@ -19,7 +19,12 @@
|
||||
|
||||
async function openNatsTriggerEditor(isFlow: boolean) {
|
||||
if (selectedTrigger.isNew) {
|
||||
natsTriggerEditor?.openNew(isFlow, path, { ...defaultValues, path: selectedTrigger.path })
|
||||
await natsTriggerEditor?.openNew(isFlow, (selectedTrigger.newTriggerSeed?.script_path ?? path), { ...defaultValues, path: selectedTrigger.path })
|
||||
// The autosave inside `openNew` created the draft row, so this trigger is
|
||||
// no longer new: re-selecting it must reload that row, not reset the form
|
||||
// to defaults and overwrite what the user just configured.
|
||||
selectedTrigger.isNew = false
|
||||
selectedTrigger.newTriggerSeed = undefined
|
||||
} else {
|
||||
natsTriggerEditor?.openEdit(selectedTrigger.path, isFlow, selectedTrigger.draftConfig)
|
||||
}
|
||||
|
||||
@@ -21,7 +21,12 @@
|
||||
|
||||
async function openPostgresTriggerEditor(isFlow: boolean) {
|
||||
if (selectedTrigger.isNew) {
|
||||
postgresTriggerEditor?.openNew(isFlow, path, { ...defaultValues, path: selectedTrigger.path }, newDraft)
|
||||
await postgresTriggerEditor?.openNew(isFlow, (selectedTrigger.newTriggerSeed?.script_path ?? path), { ...defaultValues, path: selectedTrigger.path }, newDraft)
|
||||
// The autosave inside `openNew` created the draft row, so this trigger is
|
||||
// no longer new: re-selecting it must reload that row, not reset the form
|
||||
// to defaults and overwrite what the user just configured.
|
||||
selectedTrigger.isNew = false
|
||||
selectedTrigger.newTriggerSeed = undefined
|
||||
} else {
|
||||
postgresTriggerEditor?.openEdit(selectedTrigger.path, isFlow, defaultValues)
|
||||
}
|
||||
|
||||
@@ -20,7 +20,12 @@
|
||||
|
||||
async function openSqsTriggerEditor(isFlow: boolean) {
|
||||
if (selectedTrigger.isNew) {
|
||||
sqsTriggerEditor?.openNew(isFlow, path, { ...defaultValues, path: selectedTrigger.path })
|
||||
await sqsTriggerEditor?.openNew(isFlow, (selectedTrigger.newTriggerSeed?.script_path ?? path), { ...defaultValues, path: selectedTrigger.path })
|
||||
// The autosave inside `openNew` created the draft row, so this trigger is
|
||||
// no longer new: re-selecting it must reload that row, not reset the form
|
||||
// to defaults and overwrite what the user just configured.
|
||||
selectedTrigger.isNew = false
|
||||
selectedTrigger.newTriggerSeed = undefined
|
||||
} else {
|
||||
sqsTriggerEditor?.openEdit(selectedTrigger.path, isFlow, selectedTrigger.draftConfig)
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
type Trigger,
|
||||
type TriggerDraftTarget
|
||||
} from './utils'
|
||||
import { UserDraft } from '$lib/userDraft.svelte'
|
||||
import { get, type Writable } from 'svelte/store'
|
||||
import type { TriggerType } from './utils'
|
||||
import type { UserExt } from '$lib/stores'
|
||||
@@ -89,26 +90,29 @@ export class Triggers {
|
||||
return $state.snapshot(this.#selectedTriggerIndex)
|
||||
}
|
||||
|
||||
/** Add an undeployed trigger to the editor's list. For the draft-backed kinds
|
||||
* this writes the `trigger_*` draft row up front — draft rows are path-keyed,
|
||||
* so the trigger needs a path before the user has typed one, and persisting
|
||||
* immediately is what makes it survive a reload. Native kinds have no draft
|
||||
* row yet and keep their config in `draftConfig` until deploy. */
|
||||
addDraftTrigger(
|
||||
/** Add an undeployed trigger to the editor's list.
|
||||
*
|
||||
* Draft rows are path-keyed, so the path is reserved here. Who writes the row
|
||||
* depends on where the config comes from: a caller-supplied `seedCfg` is
|
||||
* already complete (a scheduled poll, a trigger step's primary schedule), so
|
||||
* it is written now and the trigger is deployable without ever opening its
|
||||
* panel. Without one, only the editor's `openNew` can produce a complete
|
||||
* config, so the trigger stays `isNew` until that autosave lands.
|
||||
*
|
||||
* Native kinds have no draft row at all and keep their config in
|
||||
* `draftConfig` until they deploy. */
|
||||
async addDraftTrigger(
|
||||
triggersCountStore: Writable<TriggersCount | undefined>,
|
||||
type: TriggerType,
|
||||
target: TriggerDraftTarget,
|
||||
seedCfg?: Record<string, any>
|
||||
): number {
|
||||
): Promise<number> {
|
||||
const primaryScheduleExists = this.#triggers.some((t) => t.type === 'schedule' && t.isPrimary)
|
||||
const isPrimary = type === 'schedule' && !primaryScheduleExists
|
||||
const draftKind = triggerDraftKind(type)
|
||||
const { runnablePath, isFlow, workspace } = target
|
||||
|
||||
// Reserve the path the draft row will be keyed at. The row itself is
|
||||
// written by the editor panel's autosave, which is what turns the form's
|
||||
// defaults into a complete config — seeding a partial one here would leave
|
||||
// required fields unset and crash the editors that don't default them.
|
||||
// Reserve the path the draft row is keyed at.
|
||||
const path =
|
||||
draftKind && workspace
|
||||
? newDraftTriggerPath(
|
||||
@@ -119,19 +123,30 @@ export class Triggers {
|
||||
)
|
||||
: undefined
|
||||
|
||||
// The runnable is carried explicitly: the editors otherwise fall back to the
|
||||
// fake capture path an undeployed runnable is given.
|
||||
const seed = { ...seedCfg, script_path: runnablePath, is_flow: isFlow }
|
||||
// Only a caller-supplied config is complete enough to store; writing the
|
||||
// bare seed would leave required fields unset for the editors that read a
|
||||
// draft back without defaulting them.
|
||||
const writeNow = !!draftKind && !!workspace && !!path && !!seedCfg
|
||||
|
||||
if (writeNow) {
|
||||
UserDraft.save(draftKind!, path!, { ...seed, path }, { workspace })
|
||||
await UserDraft.forcePersist(draftKind!, path!, { workspace })
|
||||
}
|
||||
|
||||
const newTrigger: Trigger = {
|
||||
id: generateRandomString(),
|
||||
type,
|
||||
path,
|
||||
isPrimary,
|
||||
isDraft: true,
|
||||
isNew: true,
|
||||
hasDraft: !!draftKind,
|
||||
// Until the row exists this is the editor's starting point, whatever the
|
||||
// kind — `TriggersWrapper` hands it to `openNew` as defaults. It carries
|
||||
// the runnable explicitly: the editors otherwise fall back to the fake
|
||||
// capture path an undeployed runnable is given.
|
||||
draftConfig: { ...seedCfg, script_path: runnablePath, is_flow: isFlow }
|
||||
isNew: !!draftKind && !writeNow,
|
||||
hasDraft: writeNow,
|
||||
newTriggerSeed: seed,
|
||||
// Pending state, for the kinds with nowhere else to hold it.
|
||||
draftConfig: draftKind ? undefined : seed
|
||||
}
|
||||
|
||||
this.#triggers.push(newTrigger)
|
||||
@@ -162,17 +177,21 @@ export class Triggers {
|
||||
): number {
|
||||
const currentTriggers = this.#triggers
|
||||
// Preserve the editor-local config of the kinds that have no draft row
|
||||
// (native); every other kind's pending state lives in the backend rows.
|
||||
// (native); every other kind's pending state lives in the backend rows, and
|
||||
// carrying anything local onto them would read as a permanent "Modified".
|
||||
const configMap = new Map<string, Record<string, any>>(
|
||||
currentTriggers
|
||||
.filter((t) => t.type === type && t.draftConfig)
|
||||
.filter((t) => t.type === type && t.draftConfig && !triggerDraftKind(t.type))
|
||||
.map((t) => [t.path ?? '', t.draftConfig!])
|
||||
)
|
||||
|
||||
const backendTriggers = remoteTriggers.map((trigger) => ({
|
||||
type: type as TriggerType,
|
||||
path: trigger.path,
|
||||
isPrimary: type === 'schedule' && trigger.path === trigger.script_path,
|
||||
// `path` is the draft row's key on a draft-only row; the schedule is the
|
||||
// primary when the path it will DEPLOY to is the runnable's own.
|
||||
isPrimary:
|
||||
type === 'schedule' && (trigger.draft_path ?? trigger.path) === trigger.script_path,
|
||||
// `draft_only` rows are synthesized from a draft with no deployed
|
||||
// counterpart; `is_draft` also covers a draft layered on a deployed row.
|
||||
isDraft: !!trigger.draft_only,
|
||||
|
||||
@@ -112,8 +112,11 @@ export type Trigger = {
|
||||
draftPath?: string
|
||||
/** Added in this editor session and not yet written to the `draft` table. Its
|
||||
* editor opens through `openNew`, the path that fills a complete config; the
|
||||
* resulting autosave is what creates the row. */
|
||||
* resulting autosave is what creates the row, after which this clears. */
|
||||
isNew?: boolean
|
||||
/** Starting values for that `openNew`. NOT pending state — it is the defaults
|
||||
* the form opens on, so it must not count as a change or reach a deploy. */
|
||||
newTriggerSeed?: Record<string, any>
|
||||
/** Editor-local unsaved config, for the trigger kinds that have no draft row
|
||||
* yet (native only — see `triggerDraftKind`). */
|
||||
draftConfig?: Record<string, any>
|
||||
@@ -438,11 +441,21 @@ export async function repointTriggerDrafts(
|
||||
if (!kind || !trigger.path || !triggerHasPendingChanges(trigger)) return
|
||||
const draft = await readTriggerDraft(kind, trigger.path, workspace)
|
||||
if (!draft) return
|
||||
if (draft.script_path === runnablePath && draft.is_flow === isFlow) return
|
||||
// The primary schedule is the one deploying to the runnable's own path,
|
||||
// so a rename moves where it deploys too — otherwise it stops reading as
|
||||
// primary on the next reload and lands at the stale path.
|
||||
const intendedPath = trigger.isPrimary ? runnablePath : draft.path
|
||||
if (
|
||||
draft.script_path === runnablePath &&
|
||||
draft.is_flow === isFlow &&
|
||||
draft.path === intendedPath
|
||||
) {
|
||||
return
|
||||
}
|
||||
UserDraft.save(
|
||||
kind,
|
||||
trigger.path,
|
||||
{ ...draft, script_path: runnablePath, is_flow: isFlow },
|
||||
{ ...draft, path: intendedPath, script_path: runnablePath, is_flow: isFlow },
|
||||
{ workspace }
|
||||
)
|
||||
})
|
||||
|
||||
@@ -18,7 +18,12 @@
|
||||
|
||||
async function openWebsocketTriggerEditor(isFlow: boolean) {
|
||||
if (selectedTrigger.isNew) {
|
||||
wsTriggerEditor?.openNew(isFlow, path, { ...defaultValues, path: selectedTrigger.path })
|
||||
await wsTriggerEditor?.openNew(isFlow, (selectedTrigger.newTriggerSeed?.script_path ?? path), { ...defaultValues, path: selectedTrigger.path })
|
||||
// The autosave inside `openNew` created the draft row, so this trigger is
|
||||
// no longer new: re-selecting it must reload that row, not reset the form
|
||||
// to defaults and overwrite what the user just configured.
|
||||
selectedTrigger.isNew = false
|
||||
selectedTrigger.newTriggerSeed = undefined
|
||||
} else {
|
||||
wsTriggerEditor?.openEdit(selectedTrigger.path, isFlow, selectedTrigger.draftConfig)
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
*
|
||||
* Idempotent: the caller strips the field, so a converted draft has none.
|
||||
*/
|
||||
import { UserDraft } from '$lib/userDraft.svelte'
|
||||
import { UserDraft, type UserDraftItemKind } from '$lib/userDraft.svelte'
|
||||
import { newDraftTriggerPath, triggerDraftKind, type Trigger } from '$lib/components/triggers/utils'
|
||||
|
||||
export async function migrateLegacyDraftTriggers(opts: {
|
||||
@@ -24,12 +24,12 @@ export async function migrateLegacyDraftTriggers(opts: {
|
||||
runnablePath: string
|
||||
isFlow: boolean
|
||||
workspace: string
|
||||
}): Promise<boolean> {
|
||||
}): Promise<{ kind: UserDraftItemKind; path: string }[]> {
|
||||
const { legacy, runnablePath, isFlow, workspace } = opts
|
||||
if (!Array.isArray(legacy) || legacy.length === 0) return false
|
||||
if (!Array.isArray(legacy) || legacy.length === 0) return []
|
||||
|
||||
const taken: string[] = []
|
||||
let migrated = false
|
||||
const migrated: { kind: UserDraftItemKind; path: string }[] = []
|
||||
for (const trigger of legacy) {
|
||||
const cfg = trigger?.draftConfig
|
||||
const kind = trigger?.type ? triggerDraftKind(trigger.type) : undefined
|
||||
@@ -49,7 +49,7 @@ export async function migrateLegacyDraftTriggers(opts: {
|
||||
{ workspace }
|
||||
)
|
||||
await UserDraft.forcePersist(kind, path, { workspace })
|
||||
migrated = true
|
||||
migrated.push({ kind, path })
|
||||
}
|
||||
return migrated
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@ import { classicAppDraftParts } from '$lib/appDiffSides'
|
||||
import { invalidateWorkspaceDrafts } from '$lib/workspaceDrafts.svelte'
|
||||
import { invalidateWorkspaceComparison } from '$lib/workspaceComparison'
|
||||
import { setLocalDraftHint } from '$lib/localDraftHints.svelte'
|
||||
import { migrateLegacyDraftTriggers } from '$lib/legacyDraftTriggers'
|
||||
import { userStore } from '$lib/stores'
|
||||
import { saveScheduleFromCfg } from '$lib/components/flows/scheduleUtils'
|
||||
import { saveHttpRouteFromCfg } from '$lib/components/triggers/http/utils'
|
||||
@@ -403,6 +404,24 @@ export async function fetchDraftBaseStale(
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/** Deploy the trigger drafts just promoted out of a legacy `draft_triggers`
|
||||
* array. Before triggers had their own rows they deployed alongside their
|
||||
* runnable, so deploying the runnable must still land them rather than leave
|
||||
* them behind as drafts. Failures are surfaced but don't fail the deploy — the
|
||||
* runnable is already live and the draft survives for a retry. */
|
||||
async function deployMigratedTriggers(
|
||||
migrated: { kind: DraftKind; path: string }[],
|
||||
workspace: string
|
||||
): Promise<void> {
|
||||
for (const { kind, path } of migrated) {
|
||||
const res = await deployDraft(kind, path, workspace, { draftOnly: true })
|
||||
if (!res.success) {
|
||||
console.error(`Could not deploy migrated ${kind} at ${path}: ${res.error}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Promote a draft to deployed by replaying the editor's create/update call with
|
||||
* the stored draft value. The matching draft row is deleted server-side by the
|
||||
@@ -428,10 +447,18 @@ export async function deployDraft(
|
||||
const r = (await ScriptService.getScriptByPath({ workspace, path, getDraft: true })) as any
|
||||
const d = r.draft ?? r
|
||||
// Drop editor-only / server-managed keys; deploy as a real (non-draft) version.
|
||||
const { draft_triggers: _t, draft_only: _o, ...rest } = d
|
||||
const { draft_triggers: legacyTriggers, draft_only: _o, ...rest } = d
|
||||
const scriptPath = d.path ?? path
|
||||
// Deploy at the draft's path so a rename in the draft is honored (same as
|
||||
// the editor: createScript at the new path with parent_hash links lineage).
|
||||
// Drafts written before triggers had their own rows carry them inline;
|
||||
// promote them first — the create below deletes the draft they live in.
|
||||
const migratedTriggers = await migrateLegacyDraftTriggers({
|
||||
legacy: legacyTriggers,
|
||||
runnablePath: scriptPath,
|
||||
isFlow: false,
|
||||
workspace
|
||||
})
|
||||
await ScriptService.createScript({
|
||||
workspace,
|
||||
requestBody: {
|
||||
@@ -441,6 +468,7 @@ export async function deployDraft(
|
||||
deployment_message: deploymentMessage
|
||||
}
|
||||
})
|
||||
await deployMigratedTriggers(migratedTriggers, workspace)
|
||||
} else if (kind === 'flow') {
|
||||
const r = (await FlowService.getFlowByPath({ workspace, path, getDraft: true })) as any
|
||||
const d = r.draft ?? r
|
||||
@@ -462,6 +490,12 @@ export async function deployDraft(
|
||||
labels: d.labels,
|
||||
deployment_message: deploymentMessage
|
||||
}
|
||||
const migratedTriggers = await migrateLegacyDraftTriggers({
|
||||
legacy: d.draft_triggers,
|
||||
runnablePath: requestBody.path,
|
||||
isFlow: true,
|
||||
workspace
|
||||
})
|
||||
// Draft-only flows have NO flow row (they live solely in the
|
||||
// draft table), so they deploy via createFlow; a draft on a
|
||||
// deployed flow updates it.
|
||||
@@ -470,6 +504,7 @@ export async function deployDraft(
|
||||
} else {
|
||||
await FlowService.updateFlow({ workspace, path, requestBody })
|
||||
}
|
||||
await deployMigratedTriggers(migratedTriggers, workspace)
|
||||
} else if (kind === 'app') {
|
||||
// `raw_app` is handled above; only visual apps reach here.
|
||||
const r = (await AppService.getAppByPath({ workspace, path, getDraft: true })) as any
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
discardDraftAfterDeploy,
|
||||
runResetToDeployed
|
||||
} from '$lib/userDraftToast'
|
||||
import { migrateLegacyDraftTriggers } from '$lib/legacyDraftTriggers'
|
||||
|
||||
let version: undefined | number = $state(undefined)
|
||||
|
||||
@@ -424,6 +425,20 @@
|
||||
draftSync.draft = flowToRender
|
||||
}
|
||||
|
||||
// Drafts written before triggers had their own rows carry their trigger
|
||||
// configs inline; promote them and drop the dead field.
|
||||
const legacyTriggers = (flowToRender as any).draft_triggers
|
||||
if (legacyTriggers && $workspaceStore) {
|
||||
await migrateLegacyDraftTriggers({
|
||||
legacy: legacyTriggers,
|
||||
runnablePath: (flowToRender as any).draft_path ?? flowToRender.path,
|
||||
isFlow: true,
|
||||
workspace: $workspaceStore
|
||||
})
|
||||
const { draft_triggers: _legacy, ...rest } = draftSync.draft as any
|
||||
draftSync.draft = rest
|
||||
}
|
||||
|
||||
await initFlow(flow, flowStore, flowStateStore)
|
||||
if (tok !== loadFlowToken) return
|
||||
loading = false
|
||||
|
||||
Reference in New Issue
Block a user