diff --git a/frontend/src/lib/components/assets/AssetGraph/AssetGraphDetailsPane.svelte b/frontend/src/lib/components/assets/AssetGraph/AssetGraphDetailsPane.svelte index 28bd90a5ff..21965e7c2f 100644 --- a/frontend/src/lib/components/assets/AssetGraph/AssetGraphDetailsPane.svelte +++ b/frontend/src/lib/components/assets/AssetGraph/AssetGraphDetailsPane.svelte @@ -304,12 +304,13 @@ // its editor. `onRunInputs` runs it with them, drafts included, and // `onShowScript` leaves this view for the script. inputsOnly?: boolean - /** `content` is the pane's live buffer, newer than the stored draft when it was - * edited before switching to the inputs view. */ + /** `live` is the pane's buffer when it differs from what is stored: always + * for a draft, and for a deployed script edited before switching to the + * inputs view. A run with it previews that body. */ onRunInputs?: ( path: string, args: Record, - content?: string + live?: { content: string; language: ScriptLang } ) => Promise onShowScript?: () => void } @@ -525,15 +526,12 @@ // alongside `liveBodyAssets` and forwarded so the live graph can show // inferred column lineage on the edited script before it deploys. let liveColumnLineage = $state(undefined) - // The script `liveBodyAssets` was inferred for: they stay bound after its editor - // unmounts, so a script with no editor of its own (the inputs-only view) must - // not report or persist them as its lineage. Not reset on a switch: the - // previous script's save-on-switch cleanup still reads its own. + // The script whose editor last bound `liveBodyAssets` (undefined there means it + // inferred nothing). They stay bound after that editor unmounts, so a script + // that never had one (the inputs-only view) must not report or persist them as + // its lineage. Set by an effect declared after the save-on-switch one, whose + // cleanup still needs the previous owner when the selection moves. let liveAssetsOwner: string | undefined = undefined - $effect.pre(() => { - const assets = liveBodyAssets - untrack(() => (liveAssetsOwner = assets === undefined ? undefined : script?.path)) - }) // Bumped when the runs panel reports a watched job has reached a // terminal state. Drives S3FilePreview's refreshKey so the preview @@ -651,7 +649,7 @@ return () => { // No editor inferred this script (the inputs-only view): keep its stored // lineage rather than persisting it as empty. - const inferred = liveBodyAssets !== undefined && liveAssetsOwner === captured.path + const inferred = liveAssetsOwner === captured.path const writes = !inferred ? (writesAtRegister ?? []) : (liveBodyAssets ?? []) @@ -712,12 +710,23 @@ } }) + $effect(() => { + if (script && !readOnly && !inputsOnly) liveAssetsOwner = script.path + }) + let saving = $state(false) // What this pane last deployed, so the persist-back cleanup can tell "the // buffer equals the new deployed head" from real unsaved edits (plain // variable: only read inside the untracked cleanup). let deployedFromPane: { path: string; content: string } | undefined = undefined let isDraft = $derived(draftScript != undefined) + function liveBody(): { content: string; language: ScriptLang } | undefined { + if (!script) return undefined + const live = { content: script.content ?? '', language: script.language as ScriptLang } + if (isDraft) return live + const orig = scriptRes.current + return orig?.path === script.path && (orig.content ?? '') !== live.content ? live : undefined + } // A draft's stored schema is empty until an editor infers it, and the inputs // view mounts none: infer from the body here so its fields (the file picker) // show up. @@ -1396,7 +1405,7 @@ runsDrafts inputsOnly {onShowScript} - onRun={onRunInputs && ((path, args) => onRunInputs(path, args, script?.content))} + onRun={onRunInputs && ((path, args) => onRunInputs(path, args, liveBody()))} script={inputsScript ?? script} {runsRefreshKey} {runsPendingJobId} diff --git a/frontend/src/lib/components/assets/AssetGraph/PipelineGraphEditor.svelte b/frontend/src/lib/components/assets/AssetGraph/PipelineGraphEditor.svelte index 061a939808..ea0e72f085 100644 --- a/frontend/src/lib/components/assets/AssetGraph/PipelineGraphEditor.svelte +++ b/frontend/src/lib/components/assets/AssetGraph/PipelineGraphEditor.svelte @@ -221,7 +221,7 @@ onRunInputs?: ( path: string, args: Record, - content?: string + live?: { content: string; language: ScriptLang } ) => Promise onShowScript?: () => void // Data-upload entry scripts whose staged upload is ready (green node). diff --git a/frontend/src/lib/components/sessions/PipelineEditorView.svelte b/frontend/src/lib/components/sessions/PipelineEditorView.svelte index 695613b7f3..09ff9c12a5 100644 --- a/frontend/src/lib/components/sessions/PipelineEditorView.svelte +++ b/frontend/src/lib/components/sessions/PipelineEditorView.svelte @@ -12,7 +12,7 @@ AssetGraphSelection, NativeTriggerKind } from '$lib/components/assets/AssetGraph/types' - import { AssetService, JobService } from '$lib/gen' + import { AssetService, JobService, type ScriptLang } from '$lib/gen' import { DATA_ASSET_KINDS } from '$lib/components/assets/AssetGraph/cascadeRun' import { usePipelineAssetPrefetch } from '$lib/components/assets/AssetGraph/pipelineAssetPrefetch.svelte' import { sendUserToast } from '$lib/utils' @@ -263,25 +263,27 @@ let runsRefreshKey = $state(0) let runsPendingJobId = $state(undefined) - // `content` overrides the stored draft body with a newer one (an open editor's). + // `live` is an open editor's body, newer than what is stored: it is previewed, + // a deployed node included. async function runNode( nodePath: string, args: Record = {}, cascade = false, - content?: string + live?: { content: string; language: ScriptLang } ): Promise { const draft = pe.drafts.get(nodePath) + const body = live ?? (draft && { content: draft.script.content, language: draft.script.language }) activeRunnables.arm(`script:${nodePath}`) try { let jobId: string - if (draft) { - // Preview-run the draft content; a preview never dispatches downstream. + if (body) { + // Preview-run the unsaved body; a preview never dispatches downstream. jobId = await JobService.runScriptPreview({ workspace: workspaceId, requestBody: { path: nodePath, - content: content ?? draft.script.content, - language: draft.script.language, + content: body.content, + language: body.language, args } }) @@ -513,7 +515,7 @@ onRunByPath={(path, args) => runNode(path, args)} inputsOnly={uploadInputsPath !== undefined && (pe.openScriptPath === undefined || uploadInputsPath === pe.openScriptPath)} - onRunInputs={(path, args, content) => runNode(path, args, false, content)} + onRunInputs={(path, args, live) => runNode(path, args, false, live)} onShowScript={() => (uploadInputsPath = undefined)} canRunByPath onTestStateChange={(running) => { diff --git a/frontend/src/routes/(root)/(logged)/pipeline/[folder]/+page.svelte b/frontend/src/routes/(root)/(logged)/pipeline/[folder]/+page.svelte index 1203ca3f6e..d97a3d9906 100644 --- a/frontend/src/routes/(root)/(logged)/pipeline/[folder]/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/pipeline/[folder]/+page.svelte @@ -2504,24 +2504,27 @@ let uploadInputsPath = $state(undefined) // Runs the open node with its form inputs: a draft previews its own content, a // deployed script runs for real (in view mode, cascading like any legit run). - // `content` is the pane's live buffer, newer than the stored draft when it was - // edited before switching to the inputs view. + // `live` is the pane's buffer when it differs from what is stored (a draft, or a + // deployed node edited before switching to the inputs view): it is previewed. async function runUploadInputs( path: string, args: Record, - content?: string + live?: { content: string; language: ScriptLang } ): Promise { const draft = mode === 'edit' ? pe.drafts.get(path) : undefined - if (!draft) return mode === 'edit' ? runDeployedWithArgs(path, args) : runByPathLegit(path, args) - const body = content ?? draft.script.content - if (!$workspaceStore || !body || !draft.script.language) return undefined + const body = + mode === 'edit' + ? (live ?? (draft && { content: draft.script.content, language: draft.script.language })) + : undefined + if (!body) return mode === 'edit' ? runDeployedWithArgs(path, args) : runByPathLegit(path, args) + if (!$workspaceStore || !body.content || !body.language) return undefined activeRunnables.arm(`script:${path}`) try { const jobId = await JobService.runScriptPreview({ workspace: $workspaceStore, requestBody: { - content: body, - language: draft.script.language, + content: body.content, + language: body.language, path, args: { ...args, _wmill_skip_asset_dispatch: true } }