Files
windmill/frontend/src/lib/components/flows/expandedSubflowStep.ts
T
Ruben Fiszel 0d1cb818ee feat: preview and edit steps inside expanded subflows (#10520)
* feat: preview and edit expanded subflow steps in the flow editor

* fix: hide subflow edit button when no flow editor drawer is available

* fix: address review findings on expanded subflow step panel

* fix: base-prefix subflow links and bound the expanded subflow module cache

* fix: do not let a pre-deploy response repopulate the invalidated subflow cache

* fix: guard expanded subflow reloads against collapse and encode workspace in link

* fix: commit an expanded subflow reload only onto the expansion it fetched for
2026-08-04 20:40:35 +02:00

63 lines
2.4 KiB
TypeScript

import type { FlowModule } from '$lib/gen'
import { findStepPath, parseExpandedSubflowId } from '$lib/components/restartFromStepPath'
export type ResolvedExpandedSubflowStep = {
/** The flow the selected step lives in, i.e. the innermost subflow crossed. */
containingFlowPath: string
/** Path of each subflow crossed from the edited flow down to the containing one. */
pathChain: string[]
/** The selected step, or `undefined` when the id points at something that isn't a
* module of the containing flow (an AI agent tool node, a stale selection...). */
module: FlowModule | undefined
}
/**
* Graph node id of the expansion an inline-expanded subflow node sits in, i.e. the key
* whose modules hold the step it stands for: `subflow:a:b` → `a`, `subflow:a:b:c` →
* `subflow:a:b`. `undefined` for a top-level expansion, whose step belongs to the edited
* flow itself. Note that the last segment of `subflowSteps` is the parent's own step, so
* every segment stays in the parent key.
*/
export function expandedSubflowParentId(nodeId: string): string | undefined {
const steps = parseExpandedSubflowId(nodeId)?.subflowSteps
if (!steps) {
return undefined
}
return steps.length > 1 ? 'subflow:' + steps.join(':') : steps[0]
}
/**
* Resolves a `subflow:A:B:leaf` graph node id (an inline-expanded subflow step) to the
* step it stands for, following each `Flow{path}` boundary through the flows it points
* at. `loadFlowModules` fetches a flow's modules by path.
*
* Returns `undefined` when `id` is not an expanded-subflow node or a boundary can't be
* followed; a resolution whose `module` is undefined still carries the containing flow,
* which is enough to open it in the editor.
*/
export async function resolveExpandedSubflowStep(
id: string,
rootModules: FlowModule[],
loadFlowModules: (path: string) => Promise<FlowModule[]>
): Promise<ResolvedExpandedSubflowStep | undefined> {
const parsed = parseExpandedSubflowId(id)
if (!parsed) {
return undefined
}
let modules = rootModules
const pathChain: string[] = []
for (const stepId of parsed.subflowSteps) {
const value = findStepPath(modules, stepId)?.target.value
if (value?.type !== 'flow') {
return undefined
}
pathChain.push(value.path)
modules = await loadFlowModules(value.path)
}
return {
containingFlowPath: pathChain[pathChain.length - 1],
pathChain,
module: findStepPath(modules, parsed.leaf)?.target
}
}