diff --git a/frontend/src/lib/components/assets/AssetGraph/AssetGraphCanvas.svelte b/frontend/src/lib/components/assets/AssetGraph/AssetGraphCanvas.svelte index 1aa18a4c5f..fb7def7523 100644 --- a/frontend/src/lib/components/assets/AssetGraph/AssetGraphCanvas.svelte +++ b/frontend/src/lib/components/assets/AssetGraph/AssetGraphCanvas.svelte @@ -163,7 +163,8 @@ pathPrefix, defaultPathSuffix, producers: producersByAsset.get(`${a.kind}:${a.path}`) ?? [], - onRunProducer + onRunProducer, + onSelectAsset: () => onselect?.({ kind: 'asset', asset_kind: a.kind, path: a.path }) } }) } diff --git a/frontend/src/lib/components/assets/AssetGraph/AssetGraphDetailsPane.svelte b/frontend/src/lib/components/assets/AssetGraph/AssetGraphDetailsPane.svelte index 67719e94f2..aa9a8dde89 100644 --- a/frontend/src/lib/components/assets/AssetGraph/AssetGraphDetailsPane.svelte +++ b/frontend/src/lib/components/assets/AssetGraph/AssetGraphDetailsPane.svelte @@ -15,8 +15,11 @@ Loader2, Save, Trash2, - X + X, + Pencil } from 'lucide-svelte' + import Popover from '$lib/components/meltComponents/Popover.svelte' + import { tick } from 'svelte' import { inferArgs } from '$lib/infer' import { emptySchema, sendUserToast } from '$lib/utils' import type { AssetGraphSelection } from './types' @@ -73,6 +76,15 @@ // Job id of the most recently dispatched run. Forwarded to the // runs panel so that clicking play auto-selects the new run. runsPendingJobId?: string | undefined + // Folder-scoped non-editable prefix shown next to the suffix + // editor when the user renames a draft (e.g. `f//`). The + // new path = pathPrefix + suffix. + pathPrefix?: string + // Called when the user renames a draft — the parent reseats the + // path key in its drafts map and updates activeDraftPath. Returns + // true on success; false on collision/validation failure so the + // popover can keep itself open and surface the error inline. + onDraftPathChange?: (oldPath: string, newPath: string) => boolean | string } let { selection, @@ -86,7 +98,9 @@ onScriptRemoved, selectionProducers = [], runsRefreshKey, - runsPendingJobId + runsPendingJobId, + pathPrefix = '', + onDraftPathChange }: Props = $props() // Bumped when the runs panel reports a watched job has reached a @@ -227,6 +241,49 @@ let isScriptView = $derived( isDraft || (selection?.kind === 'runnable' && selection.runnable_kind === 'script') ) + + // Suffix editor for the draft-path popover. Seeded from the current + // path each time the popover opens so the user starts with what they + // see, not stale state from an earlier rename. + let draftPathSuffix = $state('') + let draftPathError = $state(undefined) + let draftPathInput: HTMLInputElement | undefined = $state(undefined) + + function suffixOf(fullPath: string): string { + return fullPath.startsWith(pathPrefix) ? fullPath.slice(pathPrefix.length) : fullPath + } + + async function openDraftPathEditor() { + draftPathSuffix = script ? suffixOf(script.path) : '' + draftPathError = undefined + await tick() + draftPathInput?.focus() + draftPathInput?.select() + } + + function confirmDraftPath(close: () => void) { + if (!script) return + const suffix = draftPathSuffix.trim() + if (!suffix) { + draftPathError = 'Path cannot be empty' + return + } + const newPath = pathPrefix + suffix + if (newPath === script.path) { + close() + return + } + const result = onDraftPathChange?.(script.path, newPath) + if (result === true || result === undefined) { + script.path = newPath + draftPathError = undefined + close() + } else if (typeof result === 'string') { + draftPathError = result + } else { + draftPathError = 'Path already in use' + } + }
@@ -236,12 +293,87 @@
{#if isDraft && script} -
- - Draft pipeline script - - {script.path} -
+ {#if onDraftPathChange} + + { + if (e.detail) openDraftPathEditor() + }} + > + {#snippet trigger()} + + {/snippet} + {#snippet content({ close })} +
+ Path +
+ {#if pathPrefix} + + {pathPrefix} + + {/if} + { + if (e.key === 'Enter') { + e.preventDefault() + confirmDraftPath(close) + } else if (e.key === 'Escape') { + e.preventDefault() + close() + } + }} + class="flex-1 min-w-0 px-2 py-1.5 text-sm font-mono bg-transparent focus:outline-none" + placeholder="my_script" + /> +
+ {#if draftPathError} + {draftPathError} + {/if} +
+ +
+
+ {/snippet} +
+ {:else} +
+ + Draft pipeline script + + {script.path} +
+ {/if} {:else if selection?.kind === 'asset'}