mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-07 16:03:21 +00:00
* feat: add flow group nodes core infrastructure Add group data model (start_id/end_id boundary pairs), GroupEditor for CRUD operations, groupDetectionUtils for membership computation and validation, GroupedModulesProxy for reactive sync, and compound layout support. Update openflow.openapi.yaml with group schema. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: add group UI components and rendering Add GroupOverlay with bounding box and z-ordering, GroupHeader with StepCountTab and ellipsis menu, GroupNodeCard, GroupNoteArea for inline markdown notes, CollapsedGroupNode/CollapsedSubflowNode for collapsed rendering, GroupEndNode/GroupHeadNode boundary markers, and group actions in NodeContextMenu and SelectionBoundingBox. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: integrate groups into flow graph, builder, and existing components Wire group support into FlowGraphV2 (overlays, collapsed rendering, group-aware layout), graphBuilder (GroupedModule tree, container collapse/expand, group boundary nodes), BaseEdge (drop targets for group operations), ModuleNode (collapsed container rendering), and flow map components (schema item grouping). Remove SubflowBound in favor of CollapsedSubflowNode. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: remove banned $bindable(default) pattern and dead ternary Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor: decouple collapse state from grouped module tree Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: pass groups prop to FlowGraphV2 and use GroupDisplayState via graphContext Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor: remove group membership system, compute nesting depth from visual bounds Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor: simplify GroupOverlay bounds, remove unused headerY and showNotes prop Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: populate innerNodeIds for expanded subflow overlay Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor: remove expanded subflow overlay feature for separate PR Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: flatten groups in getContainerModules to prevent crash on collapsed containers Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: add drag-to-move support for group nodes Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: derive group boundaries from expanded membership to prevent splitting existing groups Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: catch group validation errors and display as flow graph alert Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * test: add unit tests for group validation in buildGroupedModules Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: reject virtual nodes (Input, Result, Trigger) from groups Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * test: add virtual node rejection tests for buildGroupedModules Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: exclude preprocessor and failure module from groups Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: disable Create group button when preprocessor is selected Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: reject selection entirely when it contains excluded nodes Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor: remove unnecessary excludeIds from buildGroupedModules Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: remove debug console.log from FlowGraphV2 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: use cross-browser CSS grid trick for group summary input auto-sizing Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: hide group boundary edges and reformat GroupNoteArea Hide edges between group header and first node, and between last node and group-end, keeping them in the DOM but visually hidden. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor: stop FlowGraphV2 from reading groups via groupEditorContext Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: show module previews with status, selection, and suspend popover in collapsed groups Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor: extract collapsible implicit containers to separate branch Remove collapse/expand functionality for implicit containers (forloops, while loops, branches) from this branch. Backed up as collapsible-implicit-containers-backup for later rebase. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor: use original reactive modules for graph node data instead of proxy snapshots Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: prevent node loss when moving into forloop inside a group Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor: replace GroupedModule proxy with structure-only FlowStructureNode tree Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: use "group-" prefix for group IDs instead of "note-" Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: update group boundaries when renaming a module ID When a module at a group boundary (start_id or end_id) is renamed, the group definitions now get updated before the reactive rebuild, preventing stale references that would break the flow structure. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: update graph layout when removing a group note Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: add opaque background behind test run button to prevent see-through Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: detect and reject duplicate group IDs Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: simplify group creation validation with early marker normalization Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: use $state.raw in MiniFlowGraph to avoid xyflow performance warning Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor: address code review feedback - Revert backend traverse_modules change (not part of this feature) - Use Map for node lookup in GroupOverlay (O(1) vs O(n) per group) - Extract computeNodeExtraSpace to nodeExtraSpace.ts for testability Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor: address PR review feedback - Compute group depths from structure tree O(n) instead of O(n²) bounds comparison - Remove unnecessary $derived(groups) in GroupOverlay - Remove unused collapsed field from container types in OpenAPI spec - Use NODE.width constant in GroupNodeCard instead of hardcoded 275px - Add comment explaining intentional stale preservation in rebuild() Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: preserve flow groups during dependency job re-serialization Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: resolve Svelte state_referenced_locally warnings in GroupHeader and FlowGraphV2 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: show subflow groups when expanding a subflow in the graph - Store both modules and groups when expanding a subflow - Pass groups to buildStructureTree so group nodes render - Include subflow groups in overlay rendering and collapse tracking - Clone modules for prefix rewriting to avoid state_unsafe_mutation - Register expanded subflow modules in moduleMap before prefix rewriting - Disable group editing in expanded subflows and read-only views Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: restore accidentally removed code from main - Restore subflowBound selection handling in selectionUtils - Restore comments in SelectionBoundingBox - Restore deletable={false} in FirstStepInputs Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor: remove redundant adjacency check from MoveManager The disableMoveIds check already prevents all invalid drop targets, making the adjacencySourceId/adjacencyTargetId fields unnecessary. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore: regenerate auto-generated files after OpenAPI schema change Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore: regenerate cli skills after main merge Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: include groups in view_graph localStorage state Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor: centralize canCreateGroup and replace group note with group creation - Add canCreateGroup StateStore to GroupEditorContext, computed in FlowGraphV2 - Replace "Create group note" with "Create group" in FlowSelectionPanel - Remove "Add note" from selection bounding box dropdown - Remove unused NodeContextMenu component - Wire createGroup through FlowModuleSchemaMap → FlowGraphV2 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: reject groups spanning parallel branches and surface ill-formed group errors Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore: regenerate auto-generated files after main merge Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: ensure modules appears before groups in YAML export Svelte 5's $state proxy registers groups as a tracked property before it's explicitly set, causing it to appear before modules in Object.keys iteration. Reorder the value object at export time for readable YAML. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor: address second round of PR review feedback - Add comment explaining duplicateMultiple bypasses structure tree - Add warning log for inverted ranges in computeGroupModuleIds - Use NODE.width constant in CollapsedGroupNode instead of hardcoded 275px - Simplify redundant condition in getGroupsEmptiedBy Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor: remove stored group ID, derive ephemeral key from start_id:end_id Groups no longer store an `id` field. Instead, a `groupKey(g)` helper derives an ephemeral key from `${start_id}:${end_id}` at read time. This simplifies the schema while preserving all runtime functionality. When boundaries shift (module deletion), runtime state (collapse, note heights) is remapped to the new key via GroupDisplayState.remapGroupKey. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: add note button, save/cancel hints, and rename collapsed_by_default to autocollapse Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: propagate selection from collapsed group badges to external listeners Pass eventHandlers to GroupModuleIcons so clicking a module badge calls both selectionManager.selectId (visual highlight) and eventHandlers.select (side panel propagation via onSelect). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore: regenerate auto-generated files after main merge Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: hide In/Out popovers and actions during click-to-move Replace isDragging with isMoving derived that covers both drag-move and click-move states, disabling popovers, delete button, and test run button during any move operation. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
284 lines
7.6 KiB
TypeScript
284 lines
7.6 KiB
TypeScript
import {
|
|
JobService,
|
|
type Flow,
|
|
type FlowModule,
|
|
type InputTransform,
|
|
type Job,
|
|
type RestartedFrom,
|
|
type OpenFlow,
|
|
type MemoryConfig
|
|
} from '$lib/gen'
|
|
import { workspaceStore } from '$lib/stores'
|
|
import { cleanExpr, emptySchema } from '$lib/utils'
|
|
import { get } from 'svelte/store'
|
|
import type { FlowModuleState } from './flowState'
|
|
import { type PickableProperties, dfs } from './previousResults'
|
|
import { NEVER_TESTED_THIS_FAR } from './models'
|
|
import { sendUserToast } from '$lib/toast'
|
|
import type { ExtendedOpenFlow } from './types'
|
|
import type { GraphModuleState } from '../graph'
|
|
import type { ModulesTestStates } from '../modulesTest.svelte'
|
|
|
|
function create_context_function_template(eval_string: string, context: Record<string, any>) {
|
|
return `
|
|
return function (context) {
|
|
"use strict";
|
|
${
|
|
Object.keys(context).length > 0
|
|
? `let ${Object.keys(context).map((key) => ` ${key} = context['${key}']`)};`
|
|
: ``
|
|
}
|
|
return ${eval_string}
|
|
}`
|
|
}
|
|
|
|
function make_context_evaluator(eval_string, context): (context) => any {
|
|
let template = create_context_function_template(eval_string, context)
|
|
let functor = Function(template)
|
|
return functor()
|
|
}
|
|
|
|
export function evalValue(
|
|
k: string,
|
|
mod: FlowModule,
|
|
pickableProperties: PickableProperties | undefined,
|
|
showError: boolean
|
|
): any {
|
|
let inputTransforms = (mod.value['input_transforms'] ?? {}) as Record<string, InputTransform>
|
|
let v: any
|
|
let t = inputTransforms?.[k]
|
|
|
|
if (t.type == 'static') {
|
|
v = t.value
|
|
} else if (t.type == 'javascript') {
|
|
try {
|
|
let context = {
|
|
flow_input: pickableProperties?.flow_input,
|
|
results: pickableProperties?.priorIds
|
|
}
|
|
v = make_context_evaluator(t.expr, context)(context)
|
|
} catch (e) {
|
|
if (showError) {
|
|
sendUserToast(`Error evaluating ${k}: ${e.message}`, true)
|
|
}
|
|
v = undefined
|
|
}
|
|
} else {
|
|
v = undefined
|
|
}
|
|
if (v === NEVER_TESTED_THIS_FAR) {
|
|
v = undefined
|
|
}
|
|
return v
|
|
}
|
|
|
|
/** Ensure modules comes first and groups last in the value object for readable YAML export. */
|
|
function reorderFlowValue(value: ExtendedOpenFlow['value']): ExtendedOpenFlow['value'] {
|
|
const { modules, groups, ...rest } = value
|
|
return { modules, ...rest, ...(groups != null ? { groups } : {}) }
|
|
}
|
|
|
|
export function filteredContentForExport(flow: ExtendedOpenFlow) {
|
|
let o = {
|
|
summary: flow.summary,
|
|
description: flow.description,
|
|
value: reorderFlowValue(flow.value),
|
|
schema: flow.schema
|
|
}
|
|
if (flow.dedicated_worker) {
|
|
o['dedicated_worker'] = flow.dedicated_worker
|
|
}
|
|
if (flow.visible_to_runner_only) {
|
|
o['visible_to_runner_only'] = flow.visible_to_runner_only
|
|
}
|
|
if (flow.on_behalf_of_email) {
|
|
o['on_behalf_of_email'] = flow.on_behalf_of_email
|
|
}
|
|
if (flow.ws_error_handler_muted) {
|
|
o['ws_error_handler_muted'] = flow.ws_error_handler_muted
|
|
}
|
|
if (flow.tag) {
|
|
o['tag'] = flow.tag
|
|
}
|
|
return o
|
|
}
|
|
|
|
import { dfs as dfsApply } from './dfs'
|
|
import { randomUUID } from './conversations/FlowChatManager.svelte'
|
|
|
|
export function cleanFlow(flow: OpenFlow | any): OpenFlow & {
|
|
tag?: string
|
|
ws_error_handler_muted?: boolean
|
|
dedicated_worker?: boolean
|
|
visible_to_runner_only?: boolean
|
|
on_behalf_of_email?: string
|
|
} {
|
|
const newFlow: Flow = $state.snapshot(flow)
|
|
|
|
dfsApply(newFlow.value.modules, (mod) => {
|
|
if (mod.value.type == 'rawscript' || mod.value.type == 'script') {
|
|
Object.values(mod.value.input_transforms ?? {}).forEach((inp) => {
|
|
// for now we use the value for dynamic expression when done in the static editor so we have to resort to this
|
|
if (inp.type == 'javascript') {
|
|
//@ts-ignore
|
|
inp.value = undefined
|
|
inp.expr = cleanExpr(inp.expr)
|
|
} else {
|
|
//@ts-ignore
|
|
inp.expr = undefined
|
|
}
|
|
})
|
|
}
|
|
if (mod.value.type == 'rawscript' && mod.value.assets?.length == 0) {
|
|
mod.value.assets = undefined
|
|
}
|
|
// Generate memory_id for AI agents with auto memory if not already set
|
|
// Only if chat input is not enabled, as otherwise memory id is based on conversation id
|
|
if (!newFlow.value.chat_input_enabled && mod.value.type === 'aiagent') {
|
|
const memoryTransform = mod.value.input_transforms?.memory
|
|
if (memoryTransform?.type === 'static' && memoryTransform.value) {
|
|
const memoryValue = memoryTransform.value as MemoryConfig
|
|
if (
|
|
memoryValue.kind === 'auto' &&
|
|
memoryValue.context_length &&
|
|
memoryValue.context_length > 0 &&
|
|
!memoryValue.memory_id
|
|
) {
|
|
memoryTransform.value = {
|
|
...memoryValue,
|
|
memory_id: randomUUID()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
})
|
|
if (newFlow.value.concurrency_key == '') {
|
|
newFlow.value.concurrency_key = undefined
|
|
}
|
|
|
|
return newFlow
|
|
}
|
|
|
|
export function getDefaultExpr(
|
|
key: string = 'myfield',
|
|
previousModuleId: string | undefined,
|
|
previousExpr?: string
|
|
) {
|
|
return (
|
|
previousExpr ?? (previousModuleId ? `results.${previousModuleId}.${key}` : `flow_input.${key}`)
|
|
)
|
|
}
|
|
|
|
export function jobsToResults(jobs: Job[]) {
|
|
return jobs.map((job) => {
|
|
if ('result' in job) {
|
|
return job.result
|
|
} else if (Array.isArray(job)) {
|
|
return jobsToResults(job)
|
|
}
|
|
})
|
|
}
|
|
|
|
export async function runFlowPreview(
|
|
args: Record<string, any>,
|
|
flow: OpenFlow & { tag?: string },
|
|
path: string,
|
|
restartedFrom: RestartedFrom | undefined,
|
|
conversationId?: string | undefined
|
|
) {
|
|
const newFlow = flow
|
|
return await JobService.runFlowPreview({
|
|
workspace: get(workspaceStore) ?? '',
|
|
requestBody: {
|
|
args,
|
|
value: newFlow.value,
|
|
path: path,
|
|
tag: newFlow.tag,
|
|
restarted_from: restartedFrom
|
|
},
|
|
memoryId: conversationId
|
|
})
|
|
}
|
|
|
|
export function codeToStaticTemplate(code?: string): string | undefined {
|
|
if (!code || typeof code != 'string') return undefined
|
|
|
|
const lines = code.split('\n')
|
|
if (lines.length == 1) {
|
|
const line = lines[0].trim()
|
|
if (line[0] == '`' && line.charAt(line.length - 1) == '`') {
|
|
return line.slice(1, line.length - 1).replaceAll('\\`', '`')
|
|
} else {
|
|
return `\$\{${line}\}`
|
|
}
|
|
}
|
|
return undefined
|
|
}
|
|
|
|
export function emptyFlowModuleState(): FlowModuleState {
|
|
return {
|
|
schema: emptySchema(),
|
|
previewResult: NEVER_TESTED_THIS_FAR
|
|
}
|
|
}
|
|
|
|
export function checkIfParentLoop(
|
|
flowStore: ExtendedOpenFlow,
|
|
modId: string
|
|
): { id: string; type: 'forloopflow' | 'whileloopflow' } | undefined {
|
|
const flow: ExtendedOpenFlow = JSON.parse(JSON.stringify(flowStore))
|
|
const parents = dfs(modId, flow, true)
|
|
for (const parent of parents.slice(1)) {
|
|
if (parent.value.type === 'forloopflow' || parent.value.type === 'whileloopflow') {
|
|
return { id: parent.id, type: parent.value.type }
|
|
}
|
|
}
|
|
return undefined
|
|
}
|
|
|
|
/**
|
|
* Updates moduleStates based on test job data from modulesTestStates
|
|
* Extracts job information and converts it to GraphModuleState format
|
|
* for graph rendering
|
|
*/
|
|
export function updateDerivedModuleStatesFromTestJobs(
|
|
moduleId: string | undefined,
|
|
moduleTestStates: ModulesTestStates | undefined,
|
|
moduleStates: Record<string, GraphModuleState> | undefined
|
|
) {
|
|
if (!moduleId || !moduleTestStates || !moduleStates) {
|
|
return
|
|
}
|
|
const newStates: Record<string, GraphModuleState> = {}
|
|
|
|
const testState = moduleTestStates?.states[moduleId]
|
|
if (testState) {
|
|
if (testState.testJob) {
|
|
const job = testState.testJob
|
|
|
|
// Create GraphModuleState from job data
|
|
const moduleState: GraphModuleState = {
|
|
args: job.args,
|
|
type: job.type === 'QueuedJob' ? 'InProgress' : job['success'] ? 'Success' : 'Failure',
|
|
job_id: job.id,
|
|
tag: job.tag,
|
|
duration_ms: job['duration_ms'],
|
|
started_at: job.started_at ? new Date(job.started_at).getTime() : undefined
|
|
}
|
|
|
|
newStates[moduleId] = moduleState
|
|
} else if (testState.loading) {
|
|
// If test is loading, show as InProgress
|
|
newStates[moduleId] = {
|
|
type: 'InProgress',
|
|
args: {}
|
|
}
|
|
}
|
|
}
|
|
|
|
return {
|
|
...moduleStates,
|
|
...newStates
|
|
}
|
|
}
|