diff --git a/src/components/Editor/PipelineDetails.tsx b/src/components/Editor/PipelineDetails.tsx index 1b1f8ad49..cf9c08cb5 100644 --- a/src/components/Editor/PipelineDetails.tsx +++ b/src/components/Editor/PipelineDetails.tsx @@ -16,7 +16,7 @@ import { useContextPanel } from "@/providers/ContextPanelProvider"; import { type InputSpec, type OutputSpec, - type TypeSpecType, + typeSpecToString, } from "@/utils/componentSpec"; import { getComponentFileFromList } from "@/utils/componentStore"; import { USER_PIPELINES_LIST_NAME } from "@/utils/constants"; @@ -36,17 +36,6 @@ const PipelineDetails = () => { const [isYamlOpen, setIsYamlOpen] = useState(false); - // Utility function to convert TypeSpecType to string - const typeSpecToString = (typeSpec?: TypeSpecType): string => { - if (typeSpec === undefined) { - return "Any"; - } - if (typeof typeSpec === "string") { - return typeSpec; - } - return JSON.stringify(typeSpec); - }; - // State for file metadata const [fileMeta, setFileMeta] = useState<{ creationTime?: Date; diff --git a/src/components/shared/ComponentEditor/usePreviewTaskNodeData.tsx b/src/components/shared/ComponentEditor/usePreviewTaskNodeData.tsx index 4fd20d0a9..fdaa8ca74 100644 --- a/src/components/shared/ComponentEditor/usePreviewTaskNodeData.tsx +++ b/src/components/shared/ComponentEditor/usePreviewTaskNodeData.tsx @@ -1,9 +1,10 @@ import { useQuery } from "@tanstack/react-query"; import { hydrateComponentReference } from "@/services/componentService"; -import type { TaskNodeData } from "@/types/taskNode"; +import type { TaskNodeData } from "@/types/nodes"; import type { HydratedComponentReference } from "@/utils/componentSpec"; import { generateTaskSpec } from "@/utils/nodes/generateTaskSpec"; +import { createEmptyTaskCallbacks } from "@/utils/nodes/taskCallbackUtils"; export const usePreviewTaskNodeData = (componentText: string) => { const { data: componentRef, isLoading } = useQuery({ @@ -30,5 +31,8 @@ const generatePreviewTaskNodeData = ( taskId: previewTaskId, isGhost: false, readOnly: true, + connectable: false, + highlighted: false, + callbacks: createEmptyTaskCallbacks(), }; }; diff --git a/src/components/shared/ReactFlow/FlowCanvas/FlowCanvas.tsx b/src/components/shared/ReactFlow/FlowCanvas/FlowCanvas.tsx index ceedf1b97..59567b72f 100644 --- a/src/components/shared/ReactFlow/FlowCanvas/FlowCanvas.tsx +++ b/src/components/shared/ReactFlow/FlowCanvas/FlowCanvas.tsx @@ -100,7 +100,7 @@ const useScheduleExecutionOnceWhenConditionMet = ( }; const FlowCanvas = ({ - readOnly, + readOnly = false, nodesConnectable, children, ...rest @@ -312,7 +312,7 @@ const FlowCanvas = ({ () => ({ connectable: !readOnly && !!nodesConnectable, readOnly, - nodeCallbacks, + callbacks: nodeCallbacks, }), [readOnly, nodesConnectable, nodeCallbacks], ); @@ -498,14 +498,14 @@ const FlowCanvas = ({ return; } - const { taskSpec: droppedTask, taskType } = getTaskFromEvent(event); + const { taskSpec: droppedTask, nodeType } = getTaskFromEvent(event); - if (!taskType) { + if (!nodeType) { console.error("Dropped task type not identified."); return; } - if (!droppedTask && taskType === "task") { + if (!droppedTask && nodeType === "task") { console.error("Unable to find dropped task."); return; } @@ -563,7 +563,7 @@ const FlowCanvas = ({ const position = getPositionFromEvent(event, reactFlowInstance); const newSubgraphSpec = addTask( - taskType, + nodeType, droppedTask, position, currentSubgraphSpec, diff --git a/src/components/shared/ReactFlow/FlowCanvas/GhostNode/GhostNode.tsx b/src/components/shared/ReactFlow/FlowCanvas/GhostNode/GhostNode.tsx index 939a6d89f..af256cbf6 100644 --- a/src/components/shared/ReactFlow/FlowCanvas/GhostNode/GhostNode.tsx +++ b/src/components/shared/ReactFlow/FlowCanvas/GhostNode/GhostNode.tsx @@ -3,9 +3,10 @@ import { memo, useMemo } from "react"; import { cn } from "@/lib/utils"; import { TaskNodeProvider } from "@/providers/TaskNodeProvider"; -import type { TaskNodeData } from "@/types/taskNode"; +import type { TaskNodeData } from "@/types/nodes"; import type { ComponentReference } from "@/utils/componentSpec"; import { generateTaskSpec } from "@/utils/nodes/generateTaskSpec"; +import { createEmptyTaskCallbacks } from "@/utils/nodes/taskCallbackUtils"; import { TaskNodeCard } from "../TaskNode/TaskNodeCard"; @@ -74,5 +75,9 @@ const generateGhostTaskNodeData = ( taskSpec, taskId: ghostTaskId, isGhost: true, + readOnly: true, + highlighted: false, + connectable: false, + callbacks: createEmptyTaskCallbacks(), }; }; diff --git a/src/components/shared/ReactFlow/FlowCanvas/GhostNode/HintNode.tsx b/src/components/shared/ReactFlow/FlowCanvas/GhostNode/HintNode.tsx index 3e3808ac6..96777284e 100644 --- a/src/components/shared/ReactFlow/FlowCanvas/GhostNode/HintNode.tsx +++ b/src/components/shared/ReactFlow/FlowCanvas/GhostNode/HintNode.tsx @@ -2,7 +2,7 @@ import { type NodeProps, useReactFlow } from "@xyflow/react"; import { memo, type PropsWithChildren, useMemo } from "react"; import { cn } from "@/lib/utils"; -import type { HintNodeData } from "@/types/hintNode"; +import type { HintNodeData } from "@/types/nodes"; const HintNode = ({ data }: NodeProps) => { const { getZoom } = useReactFlow(); diff --git a/src/components/shared/ReactFlow/FlowCanvas/IONode/IONode.tsx b/src/components/shared/ReactFlow/FlowCanvas/IONode/IONode.tsx index 6e1d39d3c..db14e37ae 100644 --- a/src/components/shared/ReactFlow/FlowCanvas/IONode/IONode.tsx +++ b/src/components/shared/ReactFlow/FlowCanvas/IONode/IONode.tsx @@ -10,16 +10,12 @@ import { Paragraph } from "@/components/ui/typography"; import { cn } from "@/lib/utils"; import { useComponentSpec } from "@/providers/ComponentSpecProvider"; import { useContextPanel } from "@/providers/ContextPanelProvider"; +import type { IONodeData } from "@/types/nodes"; +import { isInputSpec, typeSpecToString } from "@/utils/componentSpec"; interface IONodeProps { type: "input" | "output"; - data: { - label: string; - value?: string; - default?: string; - type?: string; - readOnly?: boolean; - }; + data: IONodeData; selected: boolean; deletable: boolean; } @@ -28,10 +24,9 @@ const IONode = ({ type, data, selected = false }: IONodeProps) => { const { currentGraphSpec, currentSubgraphSpec } = useComponentSpec(); const { setContent, clearContent } = useContextPanel(); - const isInput = type === "input"; - const isOutput = type === "output"; + const { spec, readOnly } = data; - const readOnly = !!data.readOnly; + const isInput = isInputSpec(spec, type); const handleType = isInput ? "source" : "target"; const handlePosition = isInput ? Position.Right : Position.Left; @@ -44,15 +39,14 @@ const IONode = ({ type, data, selected = false }: IONodeProps) => { const borderColor = selected ? selectedColor : defaultColor; const input = useMemo( - () => - currentSubgraphSpec.inputs?.find((input) => input.name === data.label), - [currentSubgraphSpec.inputs, data.label], + () => currentSubgraphSpec.inputs?.find((input) => input.name === spec.name), + [currentSubgraphSpec.inputs, spec.name], ); const output = useMemo( () => - currentSubgraphSpec.outputs?.find((output) => output.name === data.label), - [currentSubgraphSpec.outputs, data.label], + currentSubgraphSpec.outputs?.find((output) => output.name === spec.name), + [currentSubgraphSpec.outputs, spec.name], ); useEffect(() => { @@ -67,7 +61,7 @@ const IONode = ({ type, data, selected = false }: IONodeProps) => { ); } - if (output && isOutput) { + if (output && !isInput) { const outputConnectedDetails = getOutputConnectedDetails( currentGraphSpec, output.name, @@ -92,7 +86,7 @@ const IONode = ({ type, data, selected = false }: IONodeProps) => { const connectedOutput = getOutputConnectedDetails( currentGraphSpec, - data.label, + spec.name, ); const outputConnectedValue = connectedOutput.outputName; const outputConnectedType = connectedOutput.outputType; @@ -103,30 +97,24 @@ const IONode = ({ type, data, selected = false }: IONodeProps) => { const handleClassName = isInput ? "translate-x-1.5" : "-translate-x-1.5"; - const hasDataValue = !!data.value; - const hasDataDefault = !!data.default; - - const inputValue = hasDataValue - ? data.value - : hasDataDefault - ? data.default - : null; - + const inputValue = isInput ? spec.value || spec.default || null : null; const outputValue = outputConnectedValue ?? null; - const value = isInput ? inputValue : outputValue; + const typeValue = isInput + ? typeSpecToString(spec.type) + : typeSpecToString(outputConnectedType); + return ( - {data.label} + {spec.name} {/* type */} - Type:{" "} - {outputConnectedType ?? data.type ?? "Any"} + Type: {typeValue} {!!outputConnectedTaskId && ( diff --git a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/ArgumentsEditor/ArgumentInputDialog.tsx b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/ArgumentsEditor/ArgumentInputDialog.tsx index 90d9ada8f..89d3c43ce 100644 --- a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/ArgumentsEditor/ArgumentInputDialog.tsx +++ b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/ArgumentsEditor/ArgumentInputDialog.tsx @@ -1,7 +1,8 @@ import { MultilineTextInputDialog } from "@/components/shared/Dialogs/MultilineTextInputDialog"; import type { ArgumentInput } from "@/types/arguments"; +import { typeSpecToString } from "@/utils/componentSpec"; -import { getInputValue, typeSpecToString } from "./utils"; +import { getInputValue } from "./utils"; export const ArgumentInputDialog = ({ argument, diff --git a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/ArgumentsEditor/ArgumentInputField.tsx b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/ArgumentsEditor/ArgumentInputField.tsx index a7266591d..3b13dc8a0 100644 --- a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/ArgumentsEditor/ArgumentInputField.tsx +++ b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/ArgumentsEditor/ArgumentInputField.tsx @@ -32,14 +32,10 @@ import { useCallbackOnUnmount } from "@/hooks/useCallbackOnUnmount"; import useToastNotification from "@/hooks/useToastNotification"; import { cn } from "@/lib/utils"; import type { ArgumentInput } from "@/types/arguments"; +import { typeSpecToString } from "@/utils/componentSpec"; import { ArgumentInputDialog } from "./ArgumentInputDialog"; -import { - getDefaultValue, - getInputValue, - getPlaceholder, - typeSpecToString, -} from "./utils"; +import { getDefaultValue, getInputValue, getPlaceholder } from "./utils"; export const ArgumentInputField = ({ argument, diff --git a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/ArgumentsEditor/utils.ts b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/ArgumentsEditor/utils.ts index fa860db2e..3dbf4012f 100644 --- a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/ArgumentsEditor/utils.ts +++ b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/ArgumentsEditor/utils.ts @@ -1,9 +1,5 @@ import type { ArgumentInput } from "@/types/arguments"; -import type { - ArgumentType, - TaskSpec, - TypeSpecType, -} from "@/utils/componentSpec"; +import type { ArgumentType, TaskSpec } from "@/utils/componentSpec"; export const getArgumentInputs = (taskSpec: TaskSpec) => { const componentSpec = taskSpec.componentRef.spec; @@ -40,16 +36,6 @@ export const getArgumentInputs = (taskSpec: TaskSpec) => { return argumentInputs; }; -export const typeSpecToString = (typeSpec?: TypeSpecType): string => { - if (typeSpec === undefined) { - return "Any"; - } - if (typeof typeSpec === "string") { - return typeSpec; - } - return JSON.stringify(typeSpec); -}; - export const getPlaceholder = (argument: ArgumentType) => { if (typeof argument === "string" || !argument) { return null; diff --git a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskNode.tsx b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskNode.tsx index 06c6f8b42..e4ce80330 100644 --- a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskNode.tsx +++ b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskNode.tsx @@ -4,7 +4,7 @@ import { memo, useMemo } from "react"; import type { ContainerExecutionStatus } from "@/api/types.gen"; import { useComponentSpec } from "@/providers/ComponentSpecProvider"; import { TaskNodeProvider } from "@/providers/TaskNodeProvider"; -import type { TaskNodeData } from "@/types/taskNode"; +import type { TaskNodeData } from "@/types/nodes"; import { isCacheDisabled } from "@/utils/cache"; import { StatusIndicator } from "./StatusIndicator"; diff --git a/src/components/shared/ReactFlow/FlowCanvas/utils/addTask.ts b/src/components/shared/ReactFlow/FlowCanvas/utils/addTask.ts index edbbefda1..412b0e39e 100644 --- a/src/components/shared/ReactFlow/FlowCanvas/utils/addTask.ts +++ b/src/components/shared/ReactFlow/FlowCanvas/utils/addTask.ts @@ -1,12 +1,13 @@ import type { XYPosition } from "@xyflow/react"; -import type { TaskType } from "@/types/taskNode"; -import type { - ComponentSpec, - GraphSpec, - InputSpec, - OutputSpec, - TaskSpec, +import type { NodeType } from "@/types/nodes"; +import { + type ComponentSpec, + type GraphSpec, + type InputSpec, + isGraphImplementation, + type OutputSpec, + type TaskSpec, } from "@/utils/componentSpec"; import { deepClone } from "@/utils/deepClone"; import { @@ -16,14 +17,14 @@ import { } from "@/utils/unique"; const addTask = ( - taskType: TaskType, + nodeType: NodeType, taskSpec: TaskSpec | null, position: XYPosition, componentSpec: ComponentSpec, ): ComponentSpec => { const newComponentSpec = deepClone(componentSpec); - if (!("graph" in newComponentSpec.implementation)) { + if (!isGraphImplementation(newComponentSpec.implementation)) { console.error("Implementation does not contain a graph."); return newComponentSpec; } @@ -34,7 +35,7 @@ const addTask = ( "editor.position": JSON.stringify(nodePosition), }; - if (taskType === "task") { + if (nodeType === "task") { if (!taskSpec) { console.error("A taskSpec is required to create a task node."); return newComponentSpec; @@ -77,7 +78,7 @@ const addTask = ( newComponentSpec.implementation.graph = newGraphSpec; } - if (taskType === "input") { + if (nodeType === "input") { const inputId = getUniqueInputName(newComponentSpec); const inputSpec: InputSpec = { name: inputId, @@ -88,7 +89,7 @@ const addTask = ( newComponentSpec.inputs = inputs; } - if (taskType === "output") { + if (nodeType === "output") { const outputId = getUniqueOutputName(newComponentSpec); const outputSpec: OutputSpec = { name: outputId, diff --git a/src/components/shared/ReactFlow/FlowCanvas/utils/duplicateNodes.test.ts b/src/components/shared/ReactFlow/FlowCanvas/utils/duplicateNodes.test.ts index 4221cf3ab..ee7a29026 100644 --- a/src/components/shared/ReactFlow/FlowCanvas/utils/duplicateNodes.test.ts +++ b/src/components/shared/ReactFlow/FlowCanvas/utils/duplicateNodes.test.ts @@ -1,7 +1,7 @@ import type { Node } from "@xyflow/react"; import { describe, expect, it, vi } from "vitest"; -import type { NodeCallbacks, TaskNodeData } from "@/types/taskNode"; +import type { TaskNodeData } from "@/types/nodes"; import type { ComponentSpec, InputSpec, @@ -86,15 +86,6 @@ const createMockTaskNodeCallbacks = () => ({ onUpgrade: vi.fn(), }); -const createMockNodeCallbacks = (): NodeCallbacks => ({ - setArguments: vi.fn(), - setAnnotations: vi.fn(), - setCacheStaleness: vi.fn(), - onDelete: vi.fn(), - onDuplicate: vi.fn(), - onUpgrade: vi.fn(), -}); - const createMockTaskNode = ( taskId: string, taskSpec: TaskSpec, @@ -108,8 +99,10 @@ const createMockTaskNode = ( taskId, label: "Test Task", highlighted: false, + readOnly: false, + isGhost: false, + connectable: true, callbacks: createMockTaskNodeCallbacks(), - nodeCallbacks: createMockNodeCallbacks(), }, selected: false, dragging: false, diff --git a/src/components/shared/ReactFlow/FlowCanvas/utils/duplicateNodes.ts b/src/components/shared/ReactFlow/FlowCanvas/utils/duplicateNodes.ts index 5c76c11b0..ebe8fc082 100644 --- a/src/components/shared/ReactFlow/FlowCanvas/utils/duplicateNodes.ts +++ b/src/components/shared/ReactFlow/FlowCanvas/utils/duplicateNodes.ts @@ -1,6 +1,11 @@ import { type Node, type XYPosition } from "@xyflow/react"; -import type { TaskNodeData } from "@/types/taskNode"; +import { + isInputNode, + isOutputNode, + isTaskNode, + type NodeData, +} from "@/types/nodes"; import { type ComponentSpec, type GraphInputArgument, @@ -23,6 +28,7 @@ import { taskIdToNodeId, } from "@/utils/nodes/nodeIdUtils"; import { setPositionInAnnotations } from "@/utils/nodes/setPositionInAnnotations"; +import { convertTaskCallbacksToNodeCallbacks } from "@/utils/nodes/taskCallbackUtils"; import { getUniqueInputName, getUniqueOutputName, @@ -70,14 +76,14 @@ export const duplicateNodes = ( nodesToDuplicate.forEach((node) => { const oldNodeId = node.id; - if (node.type === "task") { + if (isTaskNode(node)) { const oldTaskId = nodeIdToTaskId(oldNodeId); const newTaskId = getUniqueTaskName(graphSpec, oldTaskId); const newNodeId = taskIdToNodeId(newTaskId); nodeIdMap[oldNodeId] = newNodeId; - const taskSpec = node.data.taskSpec as TaskSpec; + const taskSpec = node.data.taskSpec; const annotations = taskSpec.annotations || {}; const updatedAnnotations = setPositionInAnnotations(annotations, { @@ -90,7 +96,7 @@ export const duplicateNodes = ( annotations: updatedAnnotations, }; newTasks[newTaskId] = newTaskSpec; - } else if (node.type === "input") { + } else if (isInputNode(node)) { const inputSpec = componentSpec.inputs?.find( (input) => input.name === node.data.label, ); @@ -115,7 +121,7 @@ export const duplicateNodes = ( }; newInputs[newInputId] = newInputSpec; - } else if (node.type === "output") { + } else if (isOutputNode(node)) { const outputSpec = componentSpec.outputs?.find( (output) => output.name === node.data.label, ); @@ -271,17 +277,20 @@ export const duplicateNodes = ( return null; } - const originalNodeData = originalNode.data as TaskNodeData; - - if (originalNode.type === "task") { + if (isTaskNode(originalNode)) { const newTaskId = nodeIdToTaskId(newNodeId); const newTaskSpec = updatedGraphSpec.tasks[newTaskId]; - const newNode = createTaskNode( - [newTaskId, newTaskSpec], - originalNodeData, - ); + const nodeData: NodeData = { + readOnly: originalNode.data.readOnly, + connectable: originalNode.data.connectable, + callbacks: convertTaskCallbacksToNodeCallbacks( + originalNode.data.callbacks, + ), + }; + + const newNode = createTaskNode([newTaskId, newTaskSpec], nodeData); newNode.id = newNodeId; newNode.selected = false; @@ -297,7 +306,7 @@ export const duplicateNodes = ( updatedNodes.push(originalNode); return newNode; - } else if (originalNode.type === "input") { + } else if (isInputNode(originalNode)) { const newInputId = nodeIdToInputName(newNodeId); const newInputSpec = updatedInputs.find( (input) => input.name === newInputId, @@ -307,7 +316,11 @@ export const duplicateNodes = ( return null; } - const newNode = createInputNode(newInputSpec, originalNodeData); + const nodeData: NodeData = { + readOnly: originalNode.data.readOnly, + }; + + const newNode = createInputNode(newInputSpec, nodeData); newNode.id = newNodeId; newNode.selected = false; @@ -323,7 +336,7 @@ export const duplicateNodes = ( updatedNodes.push(originalNode); return newNode; - } else if (originalNode.type === "output") { + } else if (isOutputNode(originalNode)) { const newOutputId = nodeIdToOutputName(newNodeId); const newOutputSpec = updatedOutputs.find( (output) => output.name === newOutputId, @@ -333,7 +346,11 @@ export const duplicateNodes = ( return null; } - const newNode = createOutputNode(newOutputSpec, originalNodeData); + const nodeData: NodeData = { + readOnly: originalNode.data.readOnly, + }; + + const newNode = createOutputNode(newOutputSpec, nodeData); newNode.id = newNodeId; @@ -371,7 +388,7 @@ export const duplicateNodes = ( y: node.position.y + offset.y, }; - if (node.type === "task") { + if (isTaskNode(node)) { const taskId = nodeIdToTaskId(node.id); const taskSpec = node.data.taskSpec as TaskSpec; @@ -388,7 +405,7 @@ export const duplicateNodes = ( }; updatedGraphSpec.tasks[taskId] = newTaskSpec; - } else if (node.type === "input") { + } else if (isInputNode(node)) { const inputId = nodeIdToInputName(node.id); const inputSpec = updatedInputs.find((input) => input.name === inputId); @@ -416,7 +433,7 @@ export const duplicateNodes = ( if (updatedInputIndex !== -1) { updatedInputs[updatedInputIndex] = newInputSpec; } - } else if (node.type === "output") { + } else if (isOutputNode(node)) { const outputId = nodeIdToOutputName(node.id); const outputSpec = updatedOutputs.find( diff --git a/src/components/shared/ReactFlow/FlowCanvas/utils/getTaskFromEvent.ts b/src/components/shared/ReactFlow/FlowCanvas/utils/getTaskFromEvent.ts index 4ee670168..3a6eaabb1 100644 --- a/src/components/shared/ReactFlow/FlowCanvas/utils/getTaskFromEvent.ts +++ b/src/components/shared/ReactFlow/FlowCanvas/utils/getTaskFromEvent.ts @@ -1,17 +1,17 @@ import type { DragEvent } from "react"; -import type { TaskType } from "@/types/taskNode"; +import type { NodeType } from "@/types/nodes"; import type { TaskSpec } from "@/utils/componentSpec"; export const getTaskFromEvent = (event: DragEvent) => { const droppedData = event.dataTransfer.getData("application/reactflow"); if (droppedData === "") { - return { taskSpec: null, taskType: null }; + return { taskSpec: null, nodeType: null }; } const droppedDataObject = JSON.parse(droppedData); - const taskType = Object.keys(droppedDataObject)[0] as TaskType; + const nodeType = Object.keys(droppedDataObject)[0] as NodeType; - const taskSpec = droppedDataObject[taskType] as TaskSpec | null; + const taskSpec = droppedDataObject[nodeType] as TaskSpec | null; - return { taskSpec, taskType }; + return { taskSpec, nodeType }; }; diff --git a/src/hooks/useHintNode.ts b/src/hooks/useHintNode.ts index 7dfab100d..bef6abdf0 100644 --- a/src/hooks/useHintNode.ts +++ b/src/hooks/useHintNode.ts @@ -4,7 +4,7 @@ import { useMemo } from "react"; import { useBetaFlagValue } from "@/components/shared/Settings/useBetaFlags"; import { useComponentLibrary } from "@/providers/ComponentLibraryProvider"; -import type { HintNodeData } from "@/types/hintNode"; +import type { HintNodeData } from "@/types/nodes"; const HINT_NODE_ID = "hint-node"; diff --git a/src/hooks/useNodeCallbacks.ts b/src/hooks/useNodeCallbacks.ts index 590c1d9c5..9fc473795 100644 --- a/src/hooks/useNodeCallbacks.ts +++ b/src/hooks/useNodeCallbacks.ts @@ -10,7 +10,7 @@ import replaceTaskArgumentsInGraphSpec from "@/components/shared/ReactFlow/FlowC import { replaceTaskNode } from "@/components/shared/ReactFlow/FlowCanvas/utils/replaceTaskNode"; import { useComponentSpec } from "@/providers/ComponentSpecProvider"; import type { Annotations } from "@/types/annotations"; -import type { NodeAndTaskId } from "@/types/taskNode"; +import type { NodeAndTaskId } from "@/types/nodes"; import type { ComponentReference, TaskSpec } from "@/utils/componentSpec"; import type { ArgumentType } from "@/utils/componentSpec"; import { updateSubgraphSpec } from "@/utils/subgraphUtils"; diff --git a/src/hooks/useTaskNodeDimensions.ts b/src/hooks/useTaskNodeDimensions.ts index 7b4035d1c..b940098e5 100644 --- a/src/hooks/useTaskNodeDimensions.ts +++ b/src/hooks/useTaskNodeDimensions.ts @@ -1,6 +1,6 @@ import { useMemo } from "react"; -import type { TaskNodeDimensions } from "@/types/taskNode"; +import type { TaskNodeDimensions } from "@/types/nodes"; import type { TaskSpec } from "@/utils/componentSpec"; import { DEFAULT_NODE_DIMENSIONS } from "@/utils/constants"; diff --git a/src/providers/TaskNodeProvider.tsx b/src/providers/TaskNodeProvider.tsx index 1d5bf899f..114720082 100644 --- a/src/providers/TaskNodeProvider.tsx +++ b/src/providers/TaskNodeProvider.tsx @@ -6,7 +6,8 @@ import useComponentFromUrl from "@/hooks/useComponentFromUrl"; import { useTaskNodeDimensions } from "@/hooks/useTaskNodeDimensions"; import useToastNotification from "@/hooks/useToastNotification"; import type { Annotations } from "@/types/annotations"; -import type { TaskNodeData, TaskNodeDimensions } from "@/types/taskNode"; +import type { TaskNodeData } from "@/types/nodes"; +import type { TaskNodeDimensions } from "@/types/nodes"; import type { ArgumentType, InputSpec, @@ -94,14 +95,14 @@ export const TaskNodeProvider = ({ const handleSetArguments = useCallback( (args: Record) => { - data.callbacks?.setArguments(args); + data.callbacks.setArguments(args); }, [data.callbacks], ); const handleSetAnnotations = useCallback( (annotations: Annotations) => { - data.callbacks?.setAnnotations(annotations); + data.callbacks.setAnnotations(annotations); }, [data.callbacks], ); @@ -114,11 +115,11 @@ export const TaskNodeProvider = ({ ); const handleDeleteTaskNode = useCallback(() => { - data.callbacks?.onDelete(); + data.callbacks.onDelete(); }, [data.callbacks]); const handleDuplicateTaskNode = useCallback(() => { - data.callbacks?.onDuplicate(); + data.callbacks.onDuplicate(); }, [data.callbacks]); const handleUpgradeTaskNode = useCallback(() => { @@ -127,7 +128,7 @@ export const TaskNodeProvider = ({ return; } - data.callbacks?.onUpgrade(mostRecentComponentRef); + data.callbacks.onUpgrade(mostRecentComponentRef); }, [data.callbacks, isOutdated, mostRecentComponentRef, notify]); const select = useCallback(() => { diff --git a/src/types/hintNode.ts b/src/types/hintNode.ts deleted file mode 100644 index 5f79f60c8..000000000 --- a/src/types/hintNode.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface HintNodeData extends Record { - key: string; - hint: string; - side: "left" | "right"; -} diff --git a/src/types/taskNode.ts b/src/types/nodes.ts similarity index 58% rename from src/types/taskNode.ts rename to src/types/nodes.ts index eea6a2978..f0c23858f 100644 --- a/src/types/taskNode.ts +++ b/src/types/nodes.ts @@ -1,27 +1,54 @@ +import type { Node } from "@xyflow/react"; + import type { ArgumentType, ComponentReference, + InputSpec, + OutputSpec, TaskSpec, } from "@/utils/componentSpec"; import type { Annotations } from "./annotations"; -export type TaskType = "task" | "input" | "output"; +export type NodeType = "task" | "input" | "output"; + +export const isTaskNode = (node: Node): node is Node => { + return node.type === "task"; +}; + +export const isInputNode = (node: Node): node is Node => { + return node.type === "input"; +}; + +export const isOutputNode = (node: Node): node is Node => { + return node.type === "output"; +}; export interface NodeData extends Record { - readOnly?: boolean; + readOnly: boolean; connectable?: boolean; - nodeCallbacks?: NodeCallbacks; + callbacks?: NodeCallbacks; } export interface TaskNodeData extends Record { - taskSpec?: TaskSpec; - taskId?: string; - readOnly?: boolean; - isGhost?: boolean; - connectable?: boolean; - highlighted?: boolean; - callbacks?: TaskCallbacks; + taskSpec: TaskSpec; + taskId: string; + readOnly: boolean; + isGhost: boolean; + connectable: boolean; + highlighted: boolean; + callbacks: TaskCallbacks; +} + +export interface IONodeData extends Record { + spec: InputSpec | OutputSpec; + readOnly: boolean; +} + +export interface HintNodeData extends Record { + key: string; + hint: string; + side: "left" | "right"; } export type NodeAndTaskId = { diff --git a/src/utils/componentSpec.ts b/src/utils/componentSpec.ts index aa126016f..cbffdf551 100644 --- a/src/utils/componentSpec.ts +++ b/src/utils/componentSpec.ts @@ -490,3 +490,36 @@ export const isContainerImplementation = ( export const isGraphImplementation = ( implementation: ImplementationType, ): implementation is GraphImplementation => "graph" in implementation; + +export const isInputSpec = ( + spec: InputSpec | OutputSpec, + type?: "input" | "output", +): spec is InputSpec => { + return ( + "value" in spec || + "default" in spec || + "optional" in spec || + type === "input" + ); +}; + +export const isOutputSpec = ( + spec: InputSpec | OutputSpec, + type?: "input" | "output", +): spec is OutputSpec => { + return ( + !("value" in spec || "default" in spec || "optional" in spec) || + type === "output" + ); +}; + +// Conversions +export const typeSpecToString = (typeSpec?: TypeSpecType): string => { + if (typeSpec === undefined) { + return "Any"; + } + if (typeof typeSpec === "string") { + return typeSpec; + } + return JSON.stringify(typeSpec); +}; diff --git a/src/utils/nodes/createInputNode.ts b/src/utils/nodes/createInputNode.ts index 3e859c1dd..6193a664c 100644 --- a/src/utils/nodes/createInputNode.ts +++ b/src/utils/nodes/createInputNode.ts @@ -1,24 +1,26 @@ import { type Node } from "@xyflow/react"; -import type { NodeData } from "@/types/taskNode"; +import type { IONodeData, NodeData } from "@/types/nodes"; import type { InputSpec } from "../componentSpec"; import { extractPositionFromAnnotations } from "./extractPositionFromAnnotations"; import { inputNameToNodeId } from "./nodeIdUtils"; export const createInputNode = (input: InputSpec, nodeData: NodeData) => { - const { name, annotations, ...rest } = input; + const { name, annotations } = input; + const { readOnly } = nodeData; const position = extractPositionFromAnnotations(annotations); const nodeId = inputNameToNodeId(name); + const inputNodeData: IONodeData = { + spec: input, + readOnly, + }; + return { id: nodeId, - data: { - ...rest, - ...nodeData, - label: name, - }, + data: inputNodeData, position: position, type: "input", } as Node; diff --git a/src/utils/nodes/createNodesFromComponentSpec.test.ts b/src/utils/nodes/createNodesFromComponentSpec.test.ts index 83ac35562..c1f75f2e3 100644 --- a/src/utils/nodes/createNodesFromComponentSpec.test.ts +++ b/src/utils/nodes/createNodesFromComponentSpec.test.ts @@ -93,7 +93,10 @@ describe("createNodesFromComponentSpec", () => { expect(result).toContainEqual({ id: "input_input1", - data: expect.objectContaining({ label: "input1" }), + data: expect.objectContaining({ + spec: expect.objectContaining({ name: "input1" }), + readOnly: false, + }), position: { x: 50, y: 100 }, type: "input", }); @@ -118,7 +121,10 @@ describe("createNodesFromComponentSpec", () => { expect(result).toContainEqual({ id: "output_output1", - data: expect.objectContaining({ label: "output1" }), + data: expect.objectContaining({ + spec: expect.objectContaining({ name: "output1" }), + readOnly: false, + }), position: { x: 300, y: 150 }, type: "output", }); @@ -163,37 +169,4 @@ describe("createNodesFromComponentSpec", () => { }), ); }); - - it("tests the setArguments function in task nodes", () => { - const taskId = "task1"; - const nodeId = `task_${taskId}`; - - const mockSetArguments = mockNodeCallbacks.setArguments; - - const componentSpec = createBasicComponentSpec({ - graph: { - tasks: { - [taskId]: { - componentRef: {}, - arguments: { existingArg: "value" }, - }, - }, - outputValues: {}, - }, - }); - - const result = createNodesFromComponentSpec(componentSpec, mockNodeData); - const taskNode = result.find((node) => node.id === nodeId) as - | { - id: string; - data: { callbacks: { setArguments: (args: any) => void } }; - } - | undefined; - - const newArgs = { newArg: "newValue" }; - taskNode?.data.callbacks.setArguments(newArgs); - - expect(mockSetArguments).toHaveBeenCalledTimes(1); - expect(mockSetArguments).toHaveBeenCalledWith({ taskId, nodeId }, newArgs); - }); }); diff --git a/src/utils/nodes/createNodesFromComponentSpec.ts b/src/utils/nodes/createNodesFromComponentSpec.ts index ee39895a1..57b9f0b1c 100644 --- a/src/utils/nodes/createNodesFromComponentSpec.ts +++ b/src/utils/nodes/createNodesFromComponentSpec.ts @@ -1,6 +1,6 @@ import { type Node } from "@xyflow/react"; -import type { NodeData } from "@/types/taskNode"; +import type { NodeData } from "@/types/nodes"; import { type ComponentSpec, type GraphSpec, diff --git a/src/utils/nodes/createOutputNode.ts b/src/utils/nodes/createOutputNode.ts index 8c8ba67b6..6fee2ee03 100644 --- a/src/utils/nodes/createOutputNode.ts +++ b/src/utils/nodes/createOutputNode.ts @@ -1,24 +1,26 @@ import { type Node } from "@xyflow/react"; -import type { NodeData } from "@/types/taskNode"; +import type { IONodeData, NodeData } from "@/types/nodes"; import type { OutputSpec } from "../componentSpec"; import { extractPositionFromAnnotations } from "./extractPositionFromAnnotations"; import { outputNameToNodeId } from "./nodeIdUtils"; export const createOutputNode = (output: OutputSpec, nodeData: NodeData) => { - const { name, annotations, ...rest } = output; + const { name, annotations } = output; + const { readOnly } = nodeData; const position = extractPositionFromAnnotations(annotations); const nodeId = outputNameToNodeId(name); + const outputNodeData: IONodeData = { + spec: output, + readOnly, + }; + return { id: nodeId, - data: { - ...rest, - ...nodeData, - label: name, - }, + data: outputNodeData, position: position, type: "output", } as Node; diff --git a/src/utils/nodes/createTaskNode.ts b/src/utils/nodes/createTaskNode.ts index 1a1b0166b..69246709f 100644 --- a/src/utils/nodes/createTaskNode.ts +++ b/src/utils/nodes/createTaskNode.ts @@ -1,6 +1,6 @@ import { type Node } from "@xyflow/react"; -import type { NodeData, TaskNodeData } from "@/types/taskNode"; +import type { NodeData, TaskNodeData } from "@/types/nodes"; import type { TaskSpec } from "../componentSpec"; import { extractPositionFromAnnotations } from "./extractPositionFromAnnotations"; @@ -12,7 +12,7 @@ export const createTaskNode = ( nodeData: NodeData, ) => { const [taskId, taskSpec] = task; - const { nodeCallbacks, ...data } = nodeData; + const { callbacks, connectable = true, ...data } = nodeData; const position = extractPositionFromAnnotations(taskSpec.annotations); const nodeId = taskIdToNodeId(taskId); @@ -20,14 +20,16 @@ export const createTaskNode = ( // Inject the taskId and nodeId into the callbacks const taskCallbacks = convertNodeCallbacksToTaskCallbacks( { taskId, nodeId }, - nodeCallbacks, + callbacks, ); const taskNodeData: TaskNodeData = { ...data, taskSpec, taskId, + connectable, highlighted: false, + isGhost: false, callbacks: taskCallbacks, }; diff --git a/src/utils/nodes/taskCallbackUtils.ts b/src/utils/nodes/taskCallbackUtils.ts index 4b564491d..9a6abaac3 100644 --- a/src/utils/nodes/taskCallbackUtils.ts +++ b/src/utils/nodes/taskCallbackUtils.ts @@ -2,7 +2,7 @@ import type { NodeAndTaskId, NodeCallbacks, TaskCallbacks, -} from "@/types/taskNode"; +} from "@/types/nodes"; // Sync TaskCallbacks with NodeCallbacks by injecting nodeId and taskId export const convertNodeCallbacksToTaskCallbacks = ( @@ -25,7 +25,24 @@ export const convertNodeCallbacksToTaskCallbacks = ( }; }; -const createEmptyTaskCallbacks = (): TaskCallbacks => ({ +// Sync NodeCallbacks with TaskCallbacks by removing nodeId and taskId +export const convertTaskCallbacksToNodeCallbacks = ( + taskCallbacks: TaskCallbacks, +): NodeCallbacks => { + return { + setArguments: (_, args) => taskCallbacks.setArguments?.(args), + setAnnotations: (_, annotations) => + taskCallbacks.setAnnotations?.(annotations), + setCacheStaleness: (_, cacheStaleness) => + taskCallbacks.setCacheStaleness?.(cacheStaleness), + onDelete: (_) => taskCallbacks.onDelete?.(), + onDuplicate: (_, selected) => taskCallbacks.onDuplicate?.(selected), + onUpgrade: (_, newComponentRef) => + taskCallbacks.onUpgrade?.(newComponentRef), + }; +}; + +export const createEmptyTaskCallbacks = (): TaskCallbacks => ({ setArguments: () => {}, setAnnotations: () => {}, setCacheStaleness: () => {},