diff --git a/src/components/shared/ReactFlow/FlowCanvas/Subgraphs/create/NodeListItem.tsx b/src/components/shared/ReactFlow/FlowCanvas/Subgraphs/create/NodeListItem.tsx index 377a5f3c5..fdd6c2092 100644 --- a/src/components/shared/ReactFlow/FlowCanvas/Subgraphs/create/NodeListItem.tsx +++ b/src/components/shared/ReactFlow/FlowCanvas/Subgraphs/create/NodeListItem.tsx @@ -7,6 +7,7 @@ import { InlineStack } from "@/components/ui/layout"; import { Paragraph } from "@/components/ui/typography"; import { cn } from "@/lib/utils"; +import { isFlexNode } from "../../types"; import { getNodeTypeColor } from "./utils"; interface NodeListItemProps { @@ -34,6 +35,12 @@ export function NodeListItem({ } }; + const isFlexType = isFlexNode(node); + + const displayValue = isFlexType + ? `Sticky Note: ${node.data.properties.title.length > 0 ? node.data.properties.title : node.id}` + : node.id; + return (
  • - + - {node.id} + {displayValue} {isOrphaned && ( diff --git a/src/components/shared/ReactFlow/FlowCanvas/Subgraphs/create/checkForOrphanedNodes.ts b/src/components/shared/ReactFlow/FlowCanvas/Subgraphs/create/checkForOrphanedNodes.ts index 50ce6fc97..b0431b19d 100644 --- a/src/components/shared/ReactFlow/FlowCanvas/Subgraphs/create/checkForOrphanedNodes.ts +++ b/src/components/shared/ReactFlow/FlowCanvas/Subgraphs/create/checkForOrphanedNodes.ts @@ -98,7 +98,7 @@ export const checkForOrphanedNodes = ( }); const orphanedNodes = selectedNodes.filter( - (node) => !connectedNodeIds.has(node.id), + (node) => !connectedNodeIds.has(node.id) && node.type !== "flex", ); return orphanedNodes; diff --git a/src/components/shared/ReactFlow/FlowCanvas/Subgraphs/create/utils.ts b/src/components/shared/ReactFlow/FlowCanvas/Subgraphs/create/utils.ts index 88f3bc41f..8773192c4 100644 --- a/src/components/shared/ReactFlow/FlowCanvas/Subgraphs/create/utils.ts +++ b/src/components/shared/ReactFlow/FlowCanvas/Subgraphs/create/utils.ts @@ -27,6 +27,8 @@ export const canGroupNodes = (nodes: Node[]): GroupingValidation => { export const getNodeTypeColor = (nodeType: string | undefined): string => { switch (nodeType) { + case "flex": + return "bg-yellow-300"; case "input": return "bg-blue-500"; case "output": diff --git a/src/utils/nodes/createSubgraphFromNodes.ts b/src/utils/nodes/createSubgraphFromNodes.ts index cc2b12ecb..2ca2b4ede 100644 --- a/src/utils/nodes/createSubgraphFromNodes.ts +++ b/src/utils/nodes/createSubgraphFromNodes.ts @@ -1,5 +1,10 @@ import type { Node, XYPosition } from "@xyflow/react"; +import { + getFlexNodeAnnotations, + serializeFlexNodes, +} from "@/components/shared/ReactFlow/FlowCanvas/FlexNode/interface"; +import type { FlexNodeData } from "@/components/shared/ReactFlow/FlowCanvas/FlexNode/types"; import { type Bounds, calculateNodesCenter, @@ -23,7 +28,10 @@ import { isTaskOutputArgument, } from "@/utils/componentSpec"; -import { EDITOR_POSITION_ANNOTATION } from "../annotations"; +import { + EDITOR_POSITION_ANNOTATION, + FLEX_NODES_ANNOTATION, +} from "../annotations"; import { extractPositionFromAnnotations } from "../annotations"; import { generateDigest } from "../componentStore"; import { getUniqueName, getUniqueTaskName } from "../unique"; @@ -67,12 +75,14 @@ export const createSubgraphFromNodes = async ( const taskNodes = selectedNodes.filter((node) => node.type === "task"); const inputNodes = selectedNodes.filter((node) => node.type === "input"); const outputNodes = selectedNodes.filter((node) => node.type === "output"); + const flexNodes = selectedNodes.filter((node) => node.type === "flex"); const subgraphTasks: Record = {}; const subgraphInputs: InputSpec[] = []; const subgraphArguments: Record = {}; const subgraphOutputs: OutputSpec[] = []; const subgraphOutputValues: Record = {}; + const subgraphAnnotations: Record = {}; const bounds = getNodesBounds(selectedNodes); @@ -145,6 +155,13 @@ export const createSubgraphFromNodes = async ( currentGraphSpec, ); + processSelectedFlexNodes( + flexNodes, + bounds, + subgraphAnnotations, + currentComponentSpec, + ); + // Create the replacement task that represents the subgraph const subgraphPosition = calculateNodesCenter(selectedNodes); @@ -159,6 +176,7 @@ export const createSubgraphFromNodes = async ( subgraphOutputValues, subgraphPosition, subgraphArguments, + subgraphAnnotations, ); const text = await getComponentText(subgraphTask.componentRef); @@ -179,6 +197,7 @@ const createSubgraphTask = async ( outputValues: Record, position: XYPosition, args: Record, + annotations: Record = {}, ) => { let author: string = "Unknown"; try { @@ -205,6 +224,7 @@ const createSubgraphTask = async ( sdk: "https://cloud-pipelines.net/pipeline-editor/", "editor.flow-direction": "left-to-right", author, + ...annotations, }, }, }; @@ -309,6 +329,38 @@ const processSelectedOutputNodes = ( }); }; +const processSelectedFlexNodes = ( + flexNodes: Node[], + bounds: Bounds, + annotations: Record, + currentComponentSpec: ComponentSpec, +): void => { + const existingFlexNodes = getFlexNodeAnnotations(currentComponentSpec); + const newFlexNodes: FlexNodeData[] = []; + + flexNodes.forEach((node) => { + const flexNodeId = node.id; + + const existingFlexNode = existingFlexNodes.find( + (flex) => flex.id === flexNodeId, + ); + + if (existingFlexNode) { + const newFlexNode = { ...existingFlexNode }; + const normalizedPosition = normalizeNodePosition(node, bounds); + newFlexNode.position = normalizedPosition; + + newFlexNodes.push(newFlexNode); + } + }); + + if (newFlexNodes.length === 0) { + return; + } + + annotations[FLEX_NODES_ANNOTATION] = serializeFlexNodes(newFlexNodes); +}; + const processTaskInputConnections = ( taskSpec: TaskSpec, taskPosition: XYPosition, diff --git a/src/utils/nodes/unpacking/helpers.ts b/src/utils/nodes/unpacking/helpers.ts index ae8a6e2d0..b69001174 100644 --- a/src/utils/nodes/unpacking/helpers.ts +++ b/src/utils/nodes/unpacking/helpers.ts @@ -1,9 +1,16 @@ import type { XYPosition } from "@xyflow/react"; +import { + getFlexNodeAnnotations, + serializeFlexNodes, +} from "@/components/shared/ReactFlow/FlowCanvas/FlexNode/interface"; import addTask from "@/components/shared/ReactFlow/FlowCanvas/utils/addTask"; import { setGraphOutputValue } from "@/components/shared/ReactFlow/FlowCanvas/utils/setGraphOutputValue"; import { setTaskArgument } from "@/components/shared/ReactFlow/FlowCanvas/utils/setTaskArgument"; -import { extractPositionFromAnnotations } from "@/utils/annotations"; +import { + extractPositionFromAnnotations, + FLEX_NODES_ANNOTATION, +} from "@/utils/annotations"; import { type ArgumentType, type ComponentSpec, @@ -23,6 +30,51 @@ import { normalizeNodePositionInGroup, } from "@/utils/graphUtils"; +export const unpackFlexNodes = ( + containerSpec: ComponentSpec, + containerPosition: XYPosition, + componentSpec: ComponentSpec, +): ComponentSpec => { + const updatedSpec = componentSpec; + + const flexNodes = getFlexNodeAnnotations(containerSpec); + + const containerCenter = calculateSpecCenter(containerSpec); + + const newFlexNodes = flexNodes.map((flexNode) => { + const position = normalizeNodePositionInGroup( + { + x: flexNode.position.x, + y: flexNode.position.y, + }, + containerPosition, + containerCenter, + ); + + return { + ...flexNode, + position, + }; + }); + + if (!updatedSpec.metadata) { + updatedSpec.metadata = {}; + } + + if (!updatedSpec.metadata.annotations) { + updatedSpec.metadata.annotations = {}; + } + + const existingFlexNodes = getFlexNodeAnnotations(updatedSpec); + + const mergedFlexNodes = [...existingFlexNodes, ...newFlexNodes]; + + updatedSpec.metadata.annotations[FLEX_NODES_ANNOTATION] = + serializeFlexNodes(mergedFlexNodes); + + return updatedSpec; +}; + export const unpackInputs = ( containerSpec: ComponentSpec, containerPosition: XYPosition, diff --git a/src/utils/nodes/unpacking/unpackSubgraph.test.ts b/src/utils/nodes/unpacking/unpackSubgraph.test.ts index ee849443e..b8318b48b 100644 --- a/src/utils/nodes/unpacking/unpackSubgraph.test.ts +++ b/src/utils/nodes/unpacking/unpackSubgraph.test.ts @@ -16,6 +16,7 @@ import { reconnectDownstreamOutputs, reconnectDownstreamTasks, reconnectUpstreamInputsAndTasks, + unpackFlexNodes, unpackInputs, unpackOutputs, unpackTasks, @@ -77,6 +78,7 @@ describe("unpackSubgraph", () => { vi.mocked(extractPositionFromAnnotations).mockReturnValue(mockPosition); vi.mocked(getOutputNodesConnectedToTask).mockReturnValue({}); vi.mocked(getDownstreamTaskNodesConnectedToTask).mockReturnValue({}); + vi.mocked(unpackFlexNodes).mockImplementation((_, __, spec) => spec); }); it("should return unchanged spec if implementation is not a graph", () => { diff --git a/src/utils/nodes/unpacking/unpackSubgraph.ts b/src/utils/nodes/unpacking/unpackSubgraph.ts index b3dc014c2..70c461f97 100644 --- a/src/utils/nodes/unpacking/unpackSubgraph.ts +++ b/src/utils/nodes/unpacking/unpackSubgraph.ts @@ -11,6 +11,7 @@ import { reconnectDownstreamOutputs, reconnectDownstreamTasks, reconnectUpstreamInputsAndTasks, + unpackFlexNodes, unpackInputs, unpackOutputs, unpackTasks, @@ -46,6 +47,14 @@ export const unpackSubgraph = ( let updatedComponentSpec = componentSpec; + // Unpack flex Nodes + const specAfterFlexNodes = unpackFlexNodes( + subgraphSpec, + subgraphPosition, + updatedComponentSpec, + ); + updatedComponentSpec = specAfterFlexNodes; + // Unpack inputs const { spec: specAfterInputs, inputNameMap } = unpackInputs( subgraphSpec,