mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-06 08:01:35 +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>
312 lines
8.3 KiB
TypeScript
312 lines
8.3 KiB
TypeScript
import type { FlowStatusModule } from '$lib/gen'
|
|
import type { AIModuleAction } from '$lib/components/flows/flowDiff'
|
|
|
|
export const NODE = {
|
|
width: 275,
|
|
height: 34,
|
|
gap: {
|
|
horizontal: 50,
|
|
vertical: 62
|
|
}
|
|
}
|
|
|
|
export type FlowNodeColorClasses = {
|
|
text: string
|
|
bg: string
|
|
outline: string
|
|
badge: string
|
|
}
|
|
|
|
export const AI_OR_ASSET_NODE_TYPES = ['asset', 'assetsOverflowed', 'newAiTool', 'aiTool']
|
|
|
|
export type FlowNodeState =
|
|
| FlowStatusModule['type']
|
|
| '_VirtualItem'
|
|
| '_Skipped'
|
|
| '_AIAdded'
|
|
| '_AIModified'
|
|
| '_AIRemoved'
|
|
| '_AIShadowed'
|
|
| undefined
|
|
|
|
/**
|
|
* Convert AI module action to FlowNodeState
|
|
*/
|
|
export function aiActionToNodeState(action: AIModuleAction): FlowNodeState {
|
|
switch (action) {
|
|
case 'added':
|
|
return '_AIAdded'
|
|
case 'modified':
|
|
return '_AIModified'
|
|
case 'removed':
|
|
return '_AIRemoved'
|
|
case 'shadowed':
|
|
return '_AIShadowed'
|
|
default:
|
|
return undefined
|
|
}
|
|
}
|
|
|
|
export function getNodeColorClasses(state: FlowNodeState, selected: boolean): FlowNodeColorClasses {
|
|
let outlined = ' outline outline-1 active:outline active:outline-1'
|
|
|
|
let defaultStyle = {
|
|
selected: {
|
|
bg: 'bg-surface-accent-selected',
|
|
outline: 'outline-border-selected' + outlined,
|
|
text: 'text-accent',
|
|
badge: 'bg-blue-100 outline-border-selected text-blue-800'
|
|
},
|
|
notSelected: {
|
|
bg: 'bg-surface-tertiary',
|
|
outline: '',
|
|
text: 'text-emphasis',
|
|
badge: 'bg-component-virtual-node text-emphasis'
|
|
}
|
|
} satisfies Record<any, FlowNodeColorClasses>
|
|
let orangeStyle = {
|
|
selected: {
|
|
bg: 'bg-orange-200 dark:bg-orange-700',
|
|
outline: 'outline-orange-500' + outlined,
|
|
text: 'text-orange-800 dark:text-orange-200',
|
|
badge: 'bg-orange-100 text-orange-700'
|
|
},
|
|
notSelected: {
|
|
bg: 'bg-orange-100 dark:bg-orange-800',
|
|
outline: '',
|
|
text: 'text-orange-700 dark:text-orange-300',
|
|
badge: 'bg-orange-200 text-orange-700'
|
|
}
|
|
} satisfies Record<any, FlowNodeColorClasses>
|
|
let map = {
|
|
_VirtualItem: {
|
|
selected: defaultStyle.selected,
|
|
notSelected: {
|
|
bg: 'bg-component-virtual-node',
|
|
outline: '',
|
|
text: 'text-emphasis'
|
|
}
|
|
},
|
|
_Skipped: {
|
|
selected: defaultStyle.selected,
|
|
notSelected: {
|
|
bg: 'bg-blue-100 dark:bg-blue-950',
|
|
outline: '',
|
|
text: 'text-blue-600 dark:text-blue-200',
|
|
badge: 'bg-blue-200 outline-border-selected text-blue-800'
|
|
}
|
|
},
|
|
Success: {
|
|
selected: {
|
|
bg: 'bg-green-200 dark:bg-green-600',
|
|
outline: 'outline-green-500 dark:outline-green-400' + outlined,
|
|
text: 'text-green-800 dark:text-green-100',
|
|
badge: 'bg-green-100 text-green-700'
|
|
},
|
|
notSelected: {
|
|
bg: 'bg-green-100 dark:bg-green-700',
|
|
outline: '',
|
|
text: 'text-green-700 dark:text-green-100',
|
|
badge: 'bg-green-200 text-green-700'
|
|
}
|
|
},
|
|
Failure: {
|
|
selected: {
|
|
bg: 'bg-red-200 dark:bg-red-600',
|
|
outline: 'outline-red-500' + outlined,
|
|
text: 'text-red-800 dark:text-red-100',
|
|
badge: 'bg-red-100 text-red-700'
|
|
},
|
|
notSelected: {
|
|
bg: 'bg-red-100 dark:bg-red-700',
|
|
outline: '',
|
|
text: 'text-red-700 dark:text-red-200',
|
|
badge: 'bg-red-200 text-red-700'
|
|
}
|
|
},
|
|
InProgress: orangeStyle,
|
|
WaitingForExecutor: orangeStyle,
|
|
WaitingForEvents: {
|
|
selected: {
|
|
bg: 'bg-purple-200 dark:bg-purple-600',
|
|
outline: 'outline-purple-500' + outlined,
|
|
text: 'text-purple-800 dark:text-purple-100',
|
|
badge: 'bg-purple-100 text-purple-700'
|
|
},
|
|
notSelected: {
|
|
bg: 'bg-purple-100 dark:bg-purple-700',
|
|
outline: '',
|
|
text: 'text-purple-700 dark:text-purple-200',
|
|
badge: 'bg-purple-200 text-purple-700'
|
|
}
|
|
},
|
|
// AI Module Action states (distinct shades from execution states)
|
|
_AIAdded: {
|
|
selected: {
|
|
bg: 'bg-green-300 dark:bg-green-800',
|
|
outline: 'outline-green-600 dark:outline-green-500' + outlined,
|
|
text: 'text-green-900 dark:text-green-100',
|
|
badge: 'bg-green-200 text-green-800'
|
|
},
|
|
notSelected: {
|
|
bg: 'bg-green-300 dark:bg-green-900',
|
|
outline: '',
|
|
text: 'text-green-800 dark:text-green-200',
|
|
badge: 'bg-green-300 text-green-800'
|
|
}
|
|
},
|
|
_AIModified: {
|
|
selected: {
|
|
bg: 'bg-orange-300 dark:bg-orange-800',
|
|
outline: 'outline-orange-600' + outlined,
|
|
text: 'text-orange-900 dark:text-orange-100',
|
|
badge: 'bg-orange-200 text-orange-800'
|
|
},
|
|
notSelected: {
|
|
bg: 'bg-orange-300 dark:bg-orange-900',
|
|
outline: '',
|
|
text: 'text-orange-800 dark:text-orange-200',
|
|
badge: 'bg-orange-300 text-orange-800'
|
|
}
|
|
},
|
|
_AIRemoved: {
|
|
selected: {
|
|
bg: 'bg-red-300/50 dark:bg-red-800/50',
|
|
outline: 'outline-red-600' + outlined,
|
|
text: 'text-red-900 dark:text-red-100',
|
|
badge: 'bg-red-200 text-red-800'
|
|
},
|
|
notSelected: {
|
|
bg: 'bg-red-300/50 dark:bg-red-900/50',
|
|
outline: '',
|
|
text: 'text-red-800 dark:text-red-200',
|
|
badge: 'bg-red-300 text-red-800'
|
|
}
|
|
},
|
|
_AIShadowed: {
|
|
selected: {
|
|
bg: 'bg-gray-300/30 dark:bg-gray-600/30 opacity-50',
|
|
outline: 'outline-gray-500' + outlined,
|
|
text: 'text-gray-700 dark:text-gray-300',
|
|
badge: 'bg-gray-200 text-gray-700'
|
|
},
|
|
notSelected: {
|
|
bg: 'bg-gray-300/30 dark:bg-gray-700/30 opacity-50',
|
|
outline: '',
|
|
text: 'text-gray-600 dark:text-gray-400',
|
|
badge: 'bg-gray-300 text-gray-700'
|
|
}
|
|
},
|
|
default: defaultStyle
|
|
} as Record<
|
|
NonNullable<FlowNodeState> | 'default',
|
|
Record<'selected' | 'notSelected', FlowNodeColorClasses>
|
|
>
|
|
|
|
let r =
|
|
map[state ?? 'default']?.[selected ? 'selected' : 'notSelected'] ??
|
|
defaultStyle[selected ? 'selected' : 'notSelected']
|
|
r.bg += ' transition-colors'
|
|
r.outline += ' transition-colors'
|
|
r.text += ' transition-colors'
|
|
r.badge = r.badge ?? ''
|
|
|
|
return r
|
|
}
|
|
|
|
/**
|
|
* Calculate the bounding box for a collection of nodes.
|
|
* Also includes expanded subflow nodes when calculating bounds for subflow containers
|
|
* @param containedIds - Array of node IDs to calculate bounds for
|
|
* @param allNodes - Array of all nodes to search for expanded subflow nodes
|
|
* @returns The bounds { minX, minY, maxX, maxY }
|
|
*/
|
|
export function calculateNodesBoundsWithOffset(
|
|
containedIds: string[],
|
|
allNodes: Array<{
|
|
id: string
|
|
position: { x: number; y: number }
|
|
type: string
|
|
measured?: { width?: number; height?: number }
|
|
}>
|
|
): {
|
|
minX: number
|
|
minY: number
|
|
maxX: number
|
|
maxY: number
|
|
} {
|
|
// Find related subflow nodes
|
|
const nodesToCalculate = getAllRelatedSubflowNodes(containedIds, allNodes)
|
|
|
|
return nodesToCalculate.reduce(
|
|
(acc, node) => {
|
|
const w = node.measured?.width ?? NODE.width
|
|
const h = node.measured?.height ?? NODE.height
|
|
return {
|
|
minX: Math.min(acc.minX, node.position.x),
|
|
minY: Math.min(acc.minY, node.position.y),
|
|
maxX: Math.max(acc.maxX, node.position.x + w),
|
|
maxY: Math.max(acc.maxY, node.position.y + h)
|
|
}
|
|
},
|
|
{
|
|
minX: Infinity,
|
|
minY: Infinity,
|
|
maxX: -Infinity,
|
|
maxY: -Infinity
|
|
}
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Find all nodes related to the given node IDs, including expanded subflow nodes
|
|
* @param targetNodeIds - Array of node IDs to find related nodes for
|
|
* @param allNodes - Array of all available nodes
|
|
* @returns Array of nodes including original nodes and any related subflow nodes
|
|
*/
|
|
function getAllRelatedSubflowNodes(
|
|
targetNodeIds: string[],
|
|
allNodes: Array<{
|
|
id: string
|
|
position: { x: number; y: number }
|
|
type: string
|
|
measured?: { width?: number; height?: number }
|
|
}>
|
|
): Array<{
|
|
id: string
|
|
position: { x: number; y: number }
|
|
measured?: { width?: number; height?: number }
|
|
}> {
|
|
const relatedNodeIds = new Set<string>()
|
|
|
|
// Add original target nodes
|
|
targetNodeIds.forEach((id) => relatedNodeIds.add(id))
|
|
|
|
// For each target node, check if it's a subflow or container and find child nodes
|
|
targetNodeIds.forEach((nodeId) => {
|
|
// Find nodes like "subflow:{nodeId}:*"
|
|
const subflowNodes = allNodes.filter(
|
|
(node) =>
|
|
node.id.startsWith(`subflow:${nodeId}:`) && !AI_OR_ASSET_NODE_TYPES.includes(node.type)
|
|
)
|
|
|
|
// Find end node like "{nodeId}-subflow-end"
|
|
const endNode = allNodes.find((node) => node.id === `${nodeId}-subflow-end`)
|
|
|
|
// Add all found nodes
|
|
subflowNodes.forEach((node) => relatedNodeIds.add(node.id))
|
|
if (endNode) relatedNodeIds.add(endNode.id)
|
|
})
|
|
|
|
// Return actual node objects that exist in allNodes
|
|
return allNodes.filter((node) => relatedNodeIds.has(node.id))
|
|
}
|
|
|
|
/**
|
|
* Generate a random unique ID for notes
|
|
* @returns A random string ID
|
|
*/
|
|
export function generateId(prefix: string = 'note-'): string {
|
|
return prefix + Math.random().toString(36).substring(2) + Math.random().toString(36).substring(2)
|
|
}
|