diff --git a/client/src/pages/platform/cluster-element-editor/hooks/useClusterElementsLayout.ts b/client/src/pages/platform/cluster-element-editor/hooks/useClusterElementsLayout.ts index ff067f8f790..ef7922b256f 100644 --- a/client/src/pages/platform/cluster-element-editor/hooks/useClusterElementsLayout.ts +++ b/client/src/pages/platform/cluster-element-editor/hooks/useClusterElementsLayout.ts @@ -14,6 +14,7 @@ import useDataPillPanelStore from '../../workflow-editor/stores/useDataPillPanel import useWorkflowDataStore from '../../workflow-editor/stores/useWorkflowDataStore'; import useWorkflowEditorStore from '../../workflow-editor/stores/useWorkflowEditorStore'; import useWorkflowNodeDetailsPanelStore from '../../workflow-editor/stores/useWorkflowNodeDetailsPanelStore'; +import {getTask} from '../../workflow-editor/utils/getTask'; import {getLayoutedElements} from '../../workflow-editor/utils/layoutUtils'; import useClusterElementsDataStore from '../stores/useClusterElementsDataStore'; import {isPlainObject} from '../utils/clusterElementsUtils'; @@ -106,13 +107,16 @@ const useClusterElementsLayout = () => { return JSON.parse(workflow.definition).tasks; }, [workflow.definition]); - const mainRootClusterElementTask = useMemo( - () => - workflowDefinitionTasks.find( - (task: {name: string}) => task.name === rootClusterElementNodeData?.workflowNodeName - ), - [workflowDefinitionTasks, rootClusterElementNodeData?.workflowNodeName] - ); + const mainRootClusterElementTask = useMemo(() => { + if (!rootClusterElementNodeData?.workflowNodeName || !workflowDefinitionTasks.length) { + return undefined; + } + + return getTask({ + tasks: workflowDefinitionTasks, + workflowNodeName: rootClusterElementNodeData.workflowNodeName, + }); + }, [workflowDefinitionTasks, rootClusterElementNodeData?.workflowNodeName]); const clusterElements = useMemo( () => mainRootClusterElementTask?.clusterElements || {}, diff --git a/client/src/pages/platform/workflow-editor/components/WorkflowNodeDetailsPanel.tsx b/client/src/pages/platform/workflow-editor/components/WorkflowNodeDetailsPanel.tsx index 5922b4e1ea5..a007e322cf8 100644 --- a/client/src/pages/platform/workflow-editor/components/WorkflowNodeDetailsPanel.tsx +++ b/client/src/pages/platform/workflow-editor/components/WorkflowNodeDetailsPanel.tsx @@ -9,6 +9,7 @@ import DescriptionTab from '@/pages/platform/workflow-editor/components/node-det import ConnectionTab from '@/pages/platform/workflow-editor/components/node-details-tabs/connection-tab/ConnectionTab'; import OutputTab from '@/pages/platform/workflow-editor/components/node-details-tabs/output-tab/OutputTab'; import Properties from '@/pages/platform/workflow-editor/components/properties/Properties'; +import {getTask} from '@/pages/platform/workflow-editor/utils/getTask'; import {CONDITION_CASE_FALSE, CONDITION_CASE_TRUE, TASK_DISPATCHER_DATA_KEY_MAP} from '@/shared/constants'; import { ActionDefinition, @@ -83,6 +84,7 @@ import getParametersWithDefaultValues from '../utils/getParametersWithDefaultVal import saveClusterElementFieldChange from '../utils/saveClusterElementFieldChange'; import saveTaskDispatcherSubtaskFieldChange from '../utils/saveTaskDispatcherSubtaskFieldChange'; import saveWorkflowDefinition from '../utils/saveWorkflowDefinition'; +import {getTaskDispatcherTask} from '../utils/taskDispatcherConfig'; import {DescriptionTabSkeleton, FieldsetSkeleton, PropertiesTabSkeleton} from './WorkflowEditorSkeletons'; const TABS: Array<{label: string; name: TabNameType}> = [ @@ -430,41 +432,184 @@ const WorkflowNodeDetailsPanel = ({ ); const currentWorkflowTask = useMemo( - () => workflow.tasks?.find((task) => task.name === currentNode?.workflowNodeName), + () => + currentNode?.workflowNodeName + ? getTask({ + tasks: workflow.tasks || [], + workflowNodeName: currentNode.workflowNodeName, + }) + : undefined, [workflow.tasks, currentNode] ); const currentClusterElementsConnections = useMemo(() => { - const mainClusterRootTask = workflow.tasks?.find( - (task) => task.name === rootClusterElementNodeData?.workflowNodeName - ); + if (!rootClusterElementNodeData?.workflowNodeName) { + return undefined; + } + + const mainClusterRootTask = getTask({ + tasks: workflow.tasks || [], + workflowNodeName: rootClusterElementNodeData.workflowNodeName, + }); + + if (!mainClusterRootTask) { + return undefined; + } - if (mainClusterRootTask?.connections && currentNode?.clusterElementType) { + if (mainClusterRootTask.connections && currentNode?.clusterElementType) { return mainClusterRootTask.connections.filter( (connection) => connection.key === currentNode?.workflowNodeName ); } + + if (currentNode?.clusterRoot && !currentNode?.isNestedClusterRoot) { + const connections: ComponentConnection[] = []; + + if (mainClusterRootTask.type && mainClusterRootTask.name) { + const typeParts = mainClusterRootTask.type.split('/'); + const componentName = typeParts[0]; + const componentVersion = parseInt(typeParts[1]?.replace('v', '') || '1'); + + if (currentComponentDefinition?.connection) { + const mainConnection = { + componentName, + componentVersion, + key: componentName, + required: currentComponentDefinition.connectionRequired || false, + workflowNodeName: mainClusterRootTask.name, + }; + + connections.push(mainConnection); + } + } + + if (mainClusterRootTask.clusterElements) { + const clusterElements = mainClusterRootTask.clusterElements; + + const getConnectionsRecursively = (clusterElement: unknown): void => { + if (!clusterElement || typeof clusterElement !== 'object') { + return; + } + + if (Array.isArray(clusterElement)) { + clusterElement.forEach((subClusterElement) => getConnectionsRecursively(subClusterElement)); + + return; + } + + const clusterElementObj = clusterElement as { + clusterElements?: unknown; + name?: string; + type?: string; + }; + + if (clusterElementObj.type && clusterElementObj.name) { + const typeParts = clusterElementObj.type.split('/'); + const componentName = typeParts[0]; + const componentVersion = parseInt(typeParts[1]?.replace('v', '') || '1'); + + const connection: ComponentConnection = { + componentName, + componentVersion, + key: clusterElementObj.name, + required: false, + workflowNodeName: clusterElementObj.name, + }; + + if (connection) { + connections.push(connection); + } + } + + if (clusterElementObj.clusterElements) { + const nestedClusterElements = clusterElementObj.clusterElements as Record; + Object.values(nestedClusterElements).forEach((nestedClusterElement) => { + getConnectionsRecursively(nestedClusterElement); + }); + } + }; + + Object.values(clusterElements).forEach((clusterElement) => { + getConnectionsRecursively(clusterElement); + }); + } + + return connections.length > 0 ? connections : undefined; + } + + return undefined; }, [ currentNode?.clusterElementType, + currentNode?.clusterRoot, + currentNode?.isNestedClusterRoot, currentNode?.workflowNodeName, rootClusterElementNodeData?.workflowNodeName, workflow.tasks, + currentComponentDefinition?.connection, + currentComponentDefinition?.connectionRequired, ]); - const currentWorkflowNodeConnections: ComponentConnection[] = useMemo( - () => + const currentWorkflowNodeConnections: ComponentConnection[] = useMemo(() => { + if (currentNode?.clusterRoot && !currentNode?.isNestedClusterRoot) { + return ( + currentWorkflowTask?.connections || + currentWorkflowTrigger?.connections || + currentClusterElementsConnections || + [] + ); + } + + const connections = currentWorkflowTask?.connections || currentWorkflowTrigger?.connections || currentClusterElementsConnections || - [], - [currentWorkflowTask?.connections, currentWorkflowTrigger?.connections, currentClusterElementsConnections] - ); + currentNode?.connections || + []; + + if ( + connections.length === 0 && + currentComponentDefinition?.connection && + currentComponentDefinition?.name && + currentComponentDefinition?.version && + currentNode?.workflowNodeName && + !currentNode?.clusterElementType + ) { + return [ + { + componentName: currentComponentDefinition.name, + componentVersion: currentComponentDefinition.version, + key: currentComponentDefinition.name, + required: currentComponentDefinition.connectionRequired || false, + workflowNodeName: currentNode.workflowNodeName, + }, + ]; + } + + return connections; + }, [ + currentClusterElementsConnections, + currentComponentDefinition?.connection, + currentComponentDefinition?.connectionRequired, + currentComponentDefinition?.name, + currentComponentDefinition?.version, + currentNode?.clusterElementType, + currentNode?.clusterRoot, + currentNode?.connections, + currentNode?.isNestedClusterRoot, + currentNode?.workflowNodeName, + currentWorkflowTask?.connections, + currentWorkflowTrigger?.connections, + ]); const nodeTabs = useMemo( () => TABS.filter(({name}) => { if (name === 'connection') { - return currentWorkflowNodeConnections.length > 0; + return ( + (currentWorkflowNodeConnections.length > 0 || + currentComponentDefinition?.connection !== undefined) && + !currentNode?.clusterElementType + ); } if (name === 'output') { @@ -480,11 +625,12 @@ const WorkflowNodeDetailsPanel = ({ return true; }), [ - currentWorkflowNodeConnections, - showOutputTab, + currentComponentDefinition, currentNode, - currentTaskDispatcherDefinition, currentOperationProperties, + currentTaskDispatcherDefinition, + currentWorkflowNodeConnections, + showOutputTab, ] ); @@ -554,70 +700,77 @@ const WorkflowNodeDetailsPanel = ({ currentNode?.clusterElementType, ]); - const calculatedDataPills = useMemo(() => { - if (!previousComponentDefinitions || !workflowNodeOutputs) { - return []; - } - - let filteredNodeNames = workflowNodeOutputs?.map((output) => output.workflowNodeName) || []; - - if (currentNode?.conditionData) { - const parentConditionTask = workflow.tasks?.find( - (task) => task.name === currentNode.conditionData?.conditionId - ); + const filterNodeNamesForCondition = useCallback( + (nodeNames: string[], conditionData: {conditionId: string; conditionCase: string}) => { + const parentConditionTask = getTaskDispatcherTask({ + taskDispatcherId: conditionData.conditionId, + tasks: workflow.tasks || [], + }); if (!parentConditionTask) { - return []; + return null; } - const {conditionCase} = currentNode.conditionData; - const oppositeConditionCase = - conditionCase === CONDITION_CASE_TRUE ? CONDITION_CASE_FALSE : CONDITION_CASE_TRUE; + conditionData.conditionCase === CONDITION_CASE_TRUE ? CONDITION_CASE_FALSE : CONDITION_CASE_TRUE; const oppositeConditionCaseNodeNames = parentConditionTask.parameters?.[oppositeConditionCase].map( (task: WorkflowTask) => task.name ); - filteredNodeNames = filteredNodeNames.filter( - (nodeName) => !oppositeConditionCaseNodeNames?.includes(nodeName) - ); - } else if (currentNode?.branchData) { - const parentBranchTask = workflow.tasks?.find((task) => task.name === currentNode.branchData?.branchId); + return nodeNames.filter((nodeName) => !oppositeConditionCaseNodeNames?.includes(nodeName)); + }, + [workflow.tasks] + ); - if (!parentBranchTask || !parentBranchTask.parameters) { - return []; + const filterNodeNamesForBranch = useCallback( + (nodeNames: string[], branchData: {branchId: string; caseKey: string | number}) => { + const parentBranchTask = getTaskDispatcherTask({ + taskDispatcherId: branchData.branchId, + tasks: workflow.tasks || [], + }); + + if (!parentBranchTask?.parameters) { + return null; } - const {caseKey} = currentNode.branchData; const branchCases: BranchCaseType[] = [ {key: 'default', tasks: parentBranchTask.parameters.default}, ...parentBranchTask.parameters.cases, ]; - let otherCaseKeys; - - if (caseKey === 'default') { - otherCaseKeys = branchCases.map((caseItem) => caseItem.key); - } else { - otherCaseKeys = ['default']; - - branchCases.forEach((caseItem) => { - if (caseItem.key !== caseKey) { - otherCaseKeys.push(caseItem.key); - } - }); - } + const otherCaseKeys = + branchData.caseKey === 'default' + ? branchCases.map((caseItem) => caseItem.key) + : ['default', ...branchCases.filter((c) => c.key !== branchData.caseKey).map((c) => c.key)]; const otherCasesNodeNames = branchCases .filter((caseItem) => otherCaseKeys.includes(caseItem.key)) - .map((caseItem) => caseItem.tasks.map((task: WorkflowTask) => task.name)) - .flat(Infinity); + .flatMap((caseItem) => caseItem.tasks.map((task: WorkflowTask) => task.name)); + + return nodeNames.filter((nodeName) => !otherCasesNodeNames.includes(nodeName)); + }, + [workflow.tasks] + ); + + const calculatedDataPills = useMemo(() => { + if (!previousComponentDefinitions || !workflowNodeOutputs) { + return []; + } + + let filteredNodeNames = workflowNodeOutputs.map((output) => output.workflowNodeName); - filteredNodeNames = filteredNodeNames.filter((nodeName) => !otherCasesNodeNames?.includes(nodeName)); + if (currentNode?.conditionData) { + const filtered = filterNodeNamesForCondition(filteredNodeNames, currentNode.conditionData); + if (filtered === null) return []; + filteredNodeNames = filtered; + } else if (currentNode?.branchData) { + const filtered = filterNodeNamesForBranch(filteredNodeNames, currentNode.branchData); + if (filtered === null) return []; + filteredNodeNames = filtered; } - const componentProperties: Array = previousComponentDefinitions?.map( + const componentProperties: Array = previousComponentDefinitions.map( (componentDefinition, index) => { const outputSchemaDefinition: PropertyAllType | undefined = workflowNodeOutputs[index]?.outputResponse?.outputSchema; @@ -634,15 +787,16 @@ const WorkflowNodeDetailsPanel = ({ } ); - const dataPills = getDataPillsFromProperties(componentProperties!, filteredNodeNames); + const dataPills = getDataPillsFromProperties(componentProperties, filteredNodeNames); return dataPills.flat(Infinity); }, [ + currentNode?.branchData, + currentNode?.conditionData, + filterNodeNamesForBranch, + filterNodeNamesForCondition, previousComponentDefinitions, workflowNodeOutputs, - currentNode?.conditionData, - currentNode?.branchData, - workflow.tasks, ]); const handleOperationSelectChange = useCallback( @@ -860,7 +1014,11 @@ const WorkflowNodeDetailsPanel = ({ // Tab switching logic useEffect(() => { - if (activeTab === 'connection' && currentWorkflowNodeConnections.length === 0) { + if ( + activeTab === 'connection' && + ((currentWorkflowNodeConnections.length === 0 && currentComponentDefinition?.connection === undefined) || + currentNode?.clusterElementType) + ) { setActiveTab('description'); return; @@ -988,9 +1146,12 @@ const WorkflowNodeDetailsPanel = ({ const workflowDefinitionTasks = JSON.parse(workflow.definition).tasks; - const mainClusterRootTask = workflowDefinitionTasks?.find( - (task: {name: string}) => task.name === rootClusterElementNodeData?.workflowNodeName - ); + const mainClusterRootTask = rootClusterElementNodeData?.workflowNodeName + ? getTask({ + tasks: workflowDefinitionTasks, + workflowNodeName: rootClusterElementNodeData.workflowNodeName, + }) + : undefined; if (!mainClusterRootTask) { return; diff --git a/client/src/pages/platform/workflow-editor/components/WorkflowNodesPopoverMenuOperationList.tsx b/client/src/pages/platform/workflow-editor/components/WorkflowNodesPopoverMenuOperationList.tsx index 0be2ad3aa04..cc7e44f44d2 100644 --- a/client/src/pages/platform/workflow-editor/components/WorkflowNodesPopoverMenuOperationList.tsx +++ b/client/src/pages/platform/workflow-editor/components/WorkflowNodesPopoverMenuOperationList.tsx @@ -29,6 +29,7 @@ import useWorkflowNodeDetailsPanelStore from '../stores/useWorkflowNodeDetailsPa import calculateNodeInsertIndex from '../utils/calculateNodeInsertIndex'; import getFormattedName from '../utils/getFormattedName'; import getParametersWithDefaultValues from '../utils/getParametersWithDefaultValues'; +import {getTask} from '../utils/getTask'; import getTaskDispatcherContext from '../utils/getTaskDispatcherContext'; import handleComponentAddedSuccess from '../utils/handleComponentAddedSuccess'; import handleTaskDispatcherSubtaskOperationClick from '../utils/handleTaskDispatcherSubtaskOperationClick'; @@ -187,18 +188,25 @@ const WorkflowNodesPopoverMenuOperationList = ({ return; } + if (!rootClusterElementNodeData?.workflowNodeName || !rootClusterElementNodeData?.componentName) { + console.error('Root cluster element node data is missing required properties'); + + return; + } + const workflowDefinitionTasks = JSON.parse(workflow.definition).tasks; - const mainClusterRootTask = workflowDefinitionTasks?.find( - (task: {name: string}) => task.name === rootClusterElementNodeData?.workflowNodeName - ); + const mainClusterRootTask = getTask({ + tasks: workflowDefinitionTasks, + workflowNodeName: rootClusterElementNodeData.workflowNodeName, + }); if (!mainClusterRootTask) { return; } const clusterElements = initializeClusterElementsObject({ - clusterElementsData: mainClusterRootTask?.clusterElements || {}, + clusterElementsData: mainClusterRootTask.clusterElements || {}, mainClusterRootComponentDefinition, mainClusterRootTask, }); @@ -208,7 +216,7 @@ const WorkflowNodesPopoverMenuOperationList = ({ clusterElements, elementType: clusterElementType, isMultipleElements, - mainRootId: rootClusterElementNodeData?.workflowNodeName, + mainRootId: rootClusterElementNodeData.workflowNodeName, sourceNodeId, }); @@ -230,22 +238,28 @@ const WorkflowNodesPopoverMenuOperationList = ({ } if (workflowNodeDetailsPanelOpen && currentNode?.workflowNodeName === sourceNodeName) { - if (rootClusterElementNodeData) { - setCurrentNode({ - ...rootClusterElementNodeData, - clusterElements: updatedClusterElements.nestedClusterElements, - }); - } + setCurrentNode({ + ...rootClusterElementNodeData, + clusterElements: updatedClusterElements.nestedClusterElements, + }); setWorkflowNodeDetailsPanelOpen(false); } saveWorkflowDefinition({ invalidateWorkflowQueries, - nodeData: updatedNodeData, + nodeData: { + ...updatedNodeData, + componentName: rootClusterElementNodeData.componentName, + workflowNodeName: rootClusterElementNodeData.workflowNodeName, + }, onSuccess: () => { handleComponentAddedSuccess({ - nodeData: updatedNodeData, + nodeData: { + ...updatedNodeData, + componentName: rootClusterElementNodeData.componentName, + workflowNodeName: rootClusterElementNodeData.workflowNodeName, + }, queryClient, workflow, }); diff --git a/client/src/pages/platform/workflow-editor/components/node-details-tabs/DescriptionTab.tsx b/client/src/pages/platform/workflow-editor/components/node-details-tabs/DescriptionTab.tsx index f1fdb34d618..59a6b964a8c 100644 --- a/client/src/pages/platform/workflow-editor/components/node-details-tabs/DescriptionTab.tsx +++ b/client/src/pages/platform/workflow-editor/components/node-details-tabs/DescriptionTab.tsx @@ -3,6 +3,7 @@ import {Label} from '@/components/ui/label'; import {Textarea} from '@/components/ui/textarea'; import useWorkflowDataStore from '@/pages/platform/workflow-editor/stores/useWorkflowDataStore'; import useWorkflowNodeDetailsPanelStore from '@/pages/platform/workflow-editor/stores/useWorkflowNodeDetailsPanelStore'; +import {getTask} from '@/pages/platform/workflow-editor/utils/getTask'; import { ClusterElementDefinition, ComponentDefinition, @@ -179,9 +180,14 @@ const DescriptionTab = ({invalidateWorkflowQueries, nodeDefinition, updateWorkfl }); }, 600); - let workflowTaskOrTrigger = [...(workflow.tasks ?? []), ...(workflow.triggers ?? [])]?.find( - (task) => task.name === currentNode?.workflowNodeName - ); + let workflowTaskOrTrigger = + workflow.triggers?.find((trigger) => trigger.name === currentNode?.workflowNodeName) || + (currentNode?.workflowNodeName + ? getTask({ + tasks: workflow.tasks || [], + workflowNodeName: currentNode.workflowNodeName, + }) + : undefined); if (!workflowTaskOrTrigger && currentNode?.clusterElementType) { // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/client/src/pages/platform/workflow-editor/components/node-details-tabs/connection-tab/ConnectionTabConnectionFieldset.tsx b/client/src/pages/platform/workflow-editor/components/node-details-tabs/connection-tab/ConnectionTabConnectionFieldset.tsx index c050a31fe54..5b696c3d145 100644 --- a/client/src/pages/platform/workflow-editor/components/node-details-tabs/connection-tab/ConnectionTabConnectionFieldset.tsx +++ b/client/src/pages/platform/workflow-editor/components/node-details-tabs/connection-tab/ConnectionTabConnectionFieldset.tsx @@ -22,7 +22,7 @@ const ConnectionTabConnectionFieldset = ({ componentVersion: componentConnection.componentVersion, }); - if (!componentDefinition) { + if (!componentDefinition?.connection) { return <>; } diff --git a/client/src/pages/platform/workflow-editor/components/properties/components/property-code-editor/PropertyCodeEditorSheet.tsx b/client/src/pages/platform/workflow-editor/components/properties/components/property-code-editor/PropertyCodeEditorSheet.tsx index a04870cc3f6..364a51aa10a 100644 --- a/client/src/pages/platform/workflow-editor/components/properties/components/property-code-editor/PropertyCodeEditorSheet.tsx +++ b/client/src/pages/platform/workflow-editor/components/properties/components/property-code-editor/PropertyCodeEditorSheet.tsx @@ -14,6 +14,7 @@ import {ResizableHandle, ResizablePanel, ResizablePanelGroup} from '@/components import {Sheet, SheetCloseButton, SheetContent, SheetHeader, SheetTitle} from '@/components/ui/sheet'; import {Tooltip, TooltipContent, TooltipTrigger} from '@/components/ui/tooltip'; import PropertyCodeEditorSheetRightPanel from '@/pages/platform/workflow-editor/components/properties/components/property-code-editor/PropertyCodeEditorSheetRightPanel'; +import {getTask} from '@/pages/platform/workflow-editor/utils/getTask'; import MonacoEditorLoader from '@/shared/components/MonacoEditorLoader'; import CopilotButton from '@/shared/components/copilot/CopilotButton'; import {Source, useCopilotStore} from '@/shared/components/copilot/stores/useCopilotStore'; @@ -54,7 +55,10 @@ const PropertyCodeEditorSheet = ({ const copilotPanelOpen = useCopilotStore((state) => state.copilotPanelOpen); const currentEnvironmentId = useEnvironmentStore((state) => state.currentEnvironmentId); - const currentWorkflowTask = workflow.tasks?.find((task) => task.name === workflowNodeName); + const currentWorkflowTask = getTask({ + tasks: workflow.tasks || [], + workflowNodeName, + }); const handleRunClick = () => { setScriptIsRunning(true); diff --git a/client/src/pages/platform/workflow-editor/components/properties/components/property-code-editor/PropertyCodeEditorSheetRightPanel.tsx b/client/src/pages/platform/workflow-editor/components/properties/components/property-code-editor/PropertyCodeEditorSheetRightPanel.tsx index 103f145358e..fb7cc4aad6f 100644 --- a/client/src/pages/platform/workflow-editor/components/properties/components/property-code-editor/PropertyCodeEditorSheetRightPanel.tsx +++ b/client/src/pages/platform/workflow-editor/components/properties/components/property-code-editor/PropertyCodeEditorSheetRightPanel.tsx @@ -1,5 +1,6 @@ import PropertyCodeEditorSheetRightPanelConnections from '@/pages/platform/workflow-editor/components/properties/components/property-code-editor/PropertyCodeEditorSheetRightPanelConnections'; import PropertyCodeEditorSheetRightPanelInputs from '@/pages/platform/workflow-editor/components/properties/components/property-code-editor/PropertyCodeEditorSheetRightPanelInputs'; +import {getTask} from '@/pages/platform/workflow-editor/utils/getTask'; import {ComponentConnection, Workflow} from '@/shared/middleware/platform/configuration'; interface PropertyCodeEditorSheetConnectionsSheetRightPanelProps { @@ -13,12 +14,15 @@ const PropertyCodeEditorSheetRightPanel = ({ workflow, workflowNodeName, }: PropertyCodeEditorSheetConnectionsSheetRightPanelProps) => { + const currentTask = getTask({ + tasks: workflow.tasks || [], + workflowNodeName, + }); + return (
- task.name === workflowNodeName)?.parameters?.input ?? {}} - /> +
diff --git a/client/src/pages/platform/workflow-editor/components/properties/components/property-mentions-input/PropertyMentionsInputEditor.tsx b/client/src/pages/platform/workflow-editor/components/properties/components/property-mentions-input/PropertyMentionsInputEditor.tsx index fc5a1378ea4..dd49c64c028 100644 --- a/client/src/pages/platform/workflow-editor/components/properties/components/property-mentions-input/PropertyMentionsInputEditor.tsx +++ b/client/src/pages/platform/workflow-editor/components/properties/components/property-mentions-input/PropertyMentionsInputEditor.tsx @@ -9,6 +9,7 @@ import { encodePath, transformValueForObjectAccess, } from '@/pages/platform/workflow-editor/utils/encodingUtils'; +import {getTask} from '@/pages/platform/workflow-editor/utils/getTask'; import saveProperty from '@/pages/platform/workflow-editor/utils/saveProperty'; import {TASK_DISPATCHER_NAMES} from '@/shared/constants'; import { @@ -140,9 +141,12 @@ const PropertyMentionsInputEditor = forwardRef task.name === rootClusterElementNodeData?.workflowNodeName - ); + const mainClusterRootTask = rootClusterElementNodeData?.workflowNodeName + ? getTask({ + tasks: workflowDefinitionTasks, + workflowNodeName: rootClusterElementNodeData.workflowNodeName, + }) + : undefined; if (mainClusterRootTask?.clusterElements) { return getClusterElementByName(mainClusterRootTask.clusterElements, currentNode.name); diff --git a/client/src/pages/platform/workflow-editor/components/properties/hooks/useProperty.ts b/client/src/pages/platform/workflow-editor/components/properties/hooks/useProperty.ts index 55eecf2c7f3..0b47bcf120b 100644 --- a/client/src/pages/platform/workflow-editor/components/properties/hooks/useProperty.ts +++ b/client/src/pages/platform/workflow-editor/components/properties/hooks/useProperty.ts @@ -38,6 +38,8 @@ import {Control, FieldValues, FormState} from 'react-hook-form'; import {useDebouncedCallback} from 'use-debounce'; import {useShallow} from 'zustand/react/shallow'; +import {getTask} from '../../../utils/getTask'; + const INPUT_PROPERTY_CONTROL_TYPES = [ 'DATE', 'DATE_TIME', @@ -663,9 +665,12 @@ export const useProperty = ({ if (currentNode.clusterElementType) { const workflowDefinitionTasks = JSON.parse(workflow.definition).tasks; - const mainClusterRootTask = workflowDefinitionTasks?.find( - (task: {name: string}) => task.name === rootClusterElementNodeData?.workflowNodeName - ); + const mainClusterRootTask = rootClusterElementNodeData?.workflowNodeName + ? getTask({ + tasks: workflowDefinitionTasks, + workflowNodeName: rootClusterElementNodeData.workflowNodeName, + }) + : undefined; if (mainClusterRootTask?.clusterElements) { return getClusterElementByName(mainClusterRootTask.clusterElements, currentNode.name); diff --git a/client/src/pages/platform/workflow-editor/nodes/AiAgentNode.tsx b/client/src/pages/platform/workflow-editor/nodes/AiAgentNode.tsx index 4d4cd94c0db..83d5723d306 100644 --- a/client/src/pages/platform/workflow-editor/nodes/AiAgentNode.tsx +++ b/client/src/pages/platform/workflow-editor/nodes/AiAgentNode.tsx @@ -2,7 +2,6 @@ import {Button} from '@/components/ui/button'; import {HoverCard, HoverCardContent, HoverCardTrigger} from '@/components/ui/hover-card'; import {Skeleton} from '@/components/ui/skeleton'; import {Tooltip, TooltipContent, TooltipTrigger} from '@/components/ui/tooltip'; -import {WorkflowTask} from '@/shared/middleware/platform/configuration'; import {useGetWorkflowNodeDescriptionQuery} from '@/shared/queries/platform/workflowNodeDescriptions.queries'; import {useEnvironmentStore} from '@/shared/stores/useEnvironmentStore'; import {NodeDataType} from '@/shared/types'; @@ -22,6 +21,7 @@ import {useWorkflowEditor} from '../providers/workflowEditorProvider'; import useWorkflowDataStore from '../stores/useWorkflowDataStore'; import useWorkflowEditorStore from '../stores/useWorkflowEditorStore'; import useWorkflowNodeDetailsPanelStore from '../stores/useWorkflowNodeDetailsPanelStore'; +import {getTask} from '../utils/getTask'; import handleDeleteTask from '../utils/handleDeleteTask'; import styles from './NodeTypes.module.css'; @@ -47,9 +47,12 @@ const AiAgentNode = ({data, id}: {data: NodeDataType; id: string}) => { const workflowDefinitionTasks = JSON.parse(workflow.definition).tasks; - const mainClusterRootTask = workflowDefinitionTasks?.find( - (task: WorkflowTask) => task.name === data.workflowNodeName - ); + const mainClusterRootTask = data?.workflowNodeName + ? getTask({ + tasks: workflowDefinitionTasks, + workflowNodeName: data.workflowNodeName, + }) + : undefined; if (!mainClusterRootTask?.clusterElements) { return {iconsToShow: [], remainingIcons: []}; diff --git a/client/src/pages/platform/workflow-editor/utils/getAllTasksRecursively.ts b/client/src/pages/platform/workflow-editor/utils/getAllTasksRecursively.ts new file mode 100644 index 00000000000..7542e8edc05 --- /dev/null +++ b/client/src/pages/platform/workflow-editor/utils/getAllTasksRecursively.ts @@ -0,0 +1,36 @@ +import {TASK_DISPATCHER_NAMES} from '@/shared/constants'; +import {WorkflowTask} from '@/shared/middleware/platform/configuration'; + +import {TASK_DISPATCHER_CONFIG} from './taskDispatcherConfig'; + +/** + * Recursively gets all tasks from a workflow, including tasks nested inside task dispatchers + */ +export default function getAllTasksRecursively(tasks: Array): Array { + const allTasks: Array = []; + + const extractTasks = (taskList: Array) => { + taskList.forEach((task) => { + allTasks.push(task); + + // Check if this is a task dispatcher + const componentName = task.type?.split('/')[0]; + + if (componentName && TASK_DISPATCHER_NAMES.includes(componentName)) { + const config = TASK_DISPATCHER_CONFIG[componentName as keyof typeof TASK_DISPATCHER_CONFIG]; + + if (config) { + const subtasks = config.getSubtasks({getAllSubtasks: true, task}); + + if (subtasks && subtasks.length > 0) { + extractTasks(subtasks); + } + } + } + }); + }; + + extractTasks(tasks); + + return allTasks; +} diff --git a/client/src/pages/platform/workflow-editor/utils/getFormattedName.ts b/client/src/pages/platform/workflow-editor/utils/getFormattedName.ts index ddacc5c970a..b0cd17ad0e6 100644 --- a/client/src/pages/platform/workflow-editor/utils/getFormattedName.ts +++ b/client/src/pages/platform/workflow-editor/utils/getFormattedName.ts @@ -3,6 +3,7 @@ import {ClusterElementsType, NodeDataType} from '@/shared/types'; import {isPlainObject} from '../../cluster-element-editor/utils/clusterElementsUtils'; import useWorkflowDataStore from '../stores/useWorkflowDataStore'; +import getAllTasksRecursively from './getAllTasksRecursively'; export default function getFormattedName(itemName: string): string { const {nodes, workflow} = useWorkflowDataStore.getState(); @@ -13,7 +14,9 @@ export default function getFormattedName(itemName: string): string { const workflowDefinition = JSON.parse(workflow.definition!); - const clusterElementNames = workflowDefinition.tasks.map((task: WorkflowTask) => { + const allTasks = getAllTasksRecursively(workflowDefinition.tasks); + + const clusterElementNames = allTasks.map((task: WorkflowTask) => { const elementNames: string[] = []; const {clusterElements} = task; diff --git a/client/src/pages/platform/workflow-editor/utils/getTask.ts b/client/src/pages/platform/workflow-editor/utils/getTask.ts new file mode 100644 index 00000000000..cc4278248e5 --- /dev/null +++ b/client/src/pages/platform/workflow-editor/utils/getTask.ts @@ -0,0 +1,46 @@ +import {TASK_DISPATCHER_SUBTASK_COLLECTIONS} from '@/shared/constants'; +import {WorkflowTask} from '@/shared/middleware/platform/configuration'; +import {BranchCaseType} from '@/shared/types'; + +type GetParentTaskType = { + tasks: Array; + workflowNodeName: string; +}; + +const ALL_COLLECTION_NAMES = Object.values(TASK_DISPATCHER_SUBTASK_COLLECTIONS).flat(); + +export function getTask({tasks, workflowNodeName}: GetParentTaskType): WorkflowTask | undefined { + for (const task of tasks) { + if (task?.name === workflowNodeName) { + return task; + } + + if (task.parameters) { + for (const collectionName of ALL_COLLECTION_NAMES) { + let subtasks = task.parameters[collectionName]; + + if (collectionName === 'cases') { + subtasks = subtasks?.flatMap((branchCase: BranchCaseType) => branchCase.tasks); + } else if (collectionName === 'branches') { + subtasks = subtasks?.flat(); + } else if (collectionName === 'iteratee') { + if (subtasks && typeof subtasks === 'object' && !Array.isArray(subtasks)) { + subtasks = [subtasks]; + } else if (!Array.isArray(subtasks)) { + subtasks = []; + } + } + + if (Array.isArray(subtasks) && subtasks.length > 0) { + const foundTask = getTask({tasks: subtasks, workflowNodeName: workflowNodeName}); + + if (foundTask) { + return foundTask; + } + } + } + } + } + + return undefined; +} diff --git a/client/src/pages/platform/workflow-editor/utils/handleDeleteTask.ts b/client/src/pages/platform/workflow-editor/utils/handleDeleteTask.ts index 9d27b2bb94c..989630c0c0a 100644 --- a/client/src/pages/platform/workflow-editor/utils/handleDeleteTask.ts +++ b/client/src/pages/platform/workflow-editor/utils/handleDeleteTask.ts @@ -9,6 +9,8 @@ import {QueryClient, UseMutationResult} from '@tanstack/react-query'; import {WorkflowDataType} from '../stores/useWorkflowDataStore'; import useWorkflowNodeDetailsPanelStore from '../stores/useWorkflowNodeDetailsPanelStore'; import findAndRemoveClusterElement from './findAndRemoveClusterElement'; +import getRecursivelyUpdatedTasks from './getRecursivelyUpdatedTasks'; +import {getTask} from './getTask'; import {TASK_DISPATCHER_CONFIG} from './taskDispatcherConfig'; interface HandleDeleteTaskProps { @@ -193,7 +195,10 @@ export default function handleDeleteTask({ return parentForkJoinTask; }) as Array; } else if (clusterElementsCanvasOpen && rootClusterElementNodeData) { - const mainRootClusterElementTask = workflowTasks.find((task) => task.name === rootClusterElementNodeData?.name); + const mainRootClusterElementTask = getTask({ + tasks: workflowTasks, + workflowNodeName: rootClusterElementNodeData.name, + }); if (!mainRootClusterElementTask || !mainRootClusterElementTask.clusterElements) { return; @@ -237,13 +242,23 @@ export default function handleDeleteTask({ } } - updatedTasks = workflowTasks.map((task) => { - if (task.name !== mainRootClusterElementTask?.name) { - return task; - } + // Check if the task is at top level + const topLevelTaskIndex = workflowTasks.findIndex((task) => task.name === mainRootClusterElementTask.name); - return updatedRootClusterElementTask; - }) as Array; + if (topLevelTaskIndex !== -1) { + updatedTasks = workflowTasks.map((task) => { + if (task.name !== mainRootClusterElementTask?.name) { + return task; + } + + return updatedRootClusterElementTask; + }) as Array; + } else { + updatedTasks = getRecursivelyUpdatedTasks( + workflowTasks as Array, + updatedRootClusterElementTask + ) as Array; + } } else { updatedTasks = workflowTasks.filter((task: WorkflowTask) => task.name !== data.name); } diff --git a/client/src/pages/platform/workflow-editor/utils/saveClusterElementFieldChange.ts b/client/src/pages/platform/workflow-editor/utils/saveClusterElementFieldChange.ts index 105e9d96f88..79a8522d271 100644 --- a/client/src/pages/platform/workflow-editor/utils/saveClusterElementFieldChange.ts +++ b/client/src/pages/platform/workflow-editor/utils/saveClusterElementFieldChange.ts @@ -7,6 +7,7 @@ import useWorkflowEditorStore from '../stores/useWorkflowEditorStore'; import useWorkflowNodeDetailsPanelStore from '../stores/useWorkflowNodeDetailsPanelStore'; import {updateClusterRootElementField, updateNestedClusterElementField} from './clusterElementsFieldChangeUtils'; import getParametersWithDefaultValues from './getParametersWithDefaultValues'; +import {getTask} from './getTask'; import saveWorkflowDefinition from './saveWorkflowDefinition'; type FieldUpdateType = { @@ -40,11 +41,18 @@ export default function saveClusterElementFieldChange({ const {componentName, name, workflowNodeName} = currentNode; + if (!rootClusterElementNodeData?.workflowNodeName || !rootClusterElementNodeData?.componentName) { + console.error('Root cluster element node data is missing required properties'); + + return; + } + const workflowDefinitionTasks = JSON.parse(workflow.definition).tasks; - const mainClusterRootTask = workflowDefinitionTasks?.find( - (task: {name: string}) => task.name === rootClusterElementNodeData?.workflowNodeName - ); + const mainClusterRootTask = getTask({ + tasks: workflowDefinitionTasks, + workflowNodeName: rootClusterElementNodeData.workflowNodeName, + }); if (!mainClusterRootTask) { return; @@ -55,20 +63,24 @@ export default function saveClusterElementFieldChange({ if ( currentNode.clusterRoot && - currentNode.workflowNodeName === rootClusterElementNodeData?.workflowNodeName && + currentNode.workflowNodeName === rootClusterElementNodeData.workflowNodeName && !currentNode.isNestedClusterRoot ) { updatedMainRootData = updateClusterRootElementField({ currentComponentDefinition, currentOperationProperties, fieldUpdate, - mainRootElement: mainClusterRootTask, + mainRootElement: { + ...mainClusterRootTask, + componentName: rootClusterElementNodeData.componentName, + workflowNodeName: rootClusterElementNodeData.workflowNodeName, + }, }); } else if ( currentNode.clusterElementType && - currentNode.workflowNodeName !== rootClusterElementNodeData?.workflowNodeName + currentNode.workflowNodeName !== rootClusterElementNodeData.workflowNodeName ) { - const clusterElements = mainClusterRootTask?.clusterElements; + const clusterElements = mainClusterRootTask.clusterElements; if (!clusterElements || Object.keys(clusterElements).length === 0) { return; @@ -85,6 +97,8 @@ export default function saveClusterElementFieldChange({ updatedMainRootData = { ...mainClusterRootTask, clusterElements: updatedClusterElements, + componentName: rootClusterElementNodeData.componentName, + workflowNodeName: rootClusterElementNodeData.workflowNodeName, }; } else { console.error('Unknown cluster element type or root element mismatch'); diff --git a/client/src/pages/platform/workflow-editor/utils/saveClusterElementNodesPosition.ts b/client/src/pages/platform/workflow-editor/utils/saveClusterElementNodesPosition.ts index 392537722e4..1749b8e4da3 100644 --- a/client/src/pages/platform/workflow-editor/utils/saveClusterElementNodesPosition.ts +++ b/client/src/pages/platform/workflow-editor/utils/saveClusterElementNodesPosition.ts @@ -3,6 +3,7 @@ import {ClusterElementItemType, UpdateWorkflowMutationType} from '@/shared/types import useClusterElementsDataStore from '../../cluster-element-editor/stores/useClusterElementsDataStore'; import useWorkflowEditorStore from '../stores/useWorkflowEditorStore'; +import {getTask} from './getTask'; import {removeClusterElementPosition} from './removeClusterElementPosition'; import saveWorkflowDefinition from './saveWorkflowDefinition'; import updateClusterElementsPositions from './updateClusterElementsPositions'; @@ -33,9 +34,12 @@ export default function saveClusterElementNodesPosition({ const workflowDefinitionTasks = JSON.parse(workflow.definition).tasks; - const mainClusterRootTask = workflowDefinitionTasks.find( - (task: {name: string}) => task.name === rootClusterElementNodeData?.workflowNodeName - ); + const mainClusterRootTask = rootClusterElementNodeData?.workflowNodeName + ? getTask({ + tasks: workflowDefinitionTasks, + workflowNodeName: rootClusterElementNodeData.workflowNodeName, + }) + : undefined; if (!mainClusterRootTask || !mainClusterRootTask.clusterElements) { console.error('Main cluster root task or cluster elements not found'); @@ -80,7 +84,11 @@ export default function saveClusterElementNodesPosition({ // Save updated data but reset the position saving flag even when there are errors saveWorkflowDefinition({ invalidateWorkflowQueries, - nodeData: updatedNodeData, + nodeData: { + ...updatedNodeData, + componentName: rootClusterElementNodeData.componentName, + workflowNodeName: rootClusterElementNodeData.workflowNodeName, + }, updateWorkflowMutation, }) .catch((error) => { @@ -107,7 +115,11 @@ export default function saveClusterElementNodesPosition({ saveWorkflowDefinition({ invalidateWorkflowQueries, - nodeData: updatedNodeData, + nodeData: { + ...updatedNodeData, + componentName: rootClusterElementNodeData.componentName, + workflowNodeName: rootClusterElementNodeData.workflowNodeName, + }, updateWorkflowMutation, }); } diff --git a/client/src/pages/platform/workflow-editor/utils/saveWorkflowDefinition.ts b/client/src/pages/platform/workflow-editor/utils/saveWorkflowDefinition.ts index 62f62eeaca7..7dc6a687e81 100644 --- a/client/src/pages/platform/workflow-editor/utils/saveWorkflowDefinition.ts +++ b/client/src/pages/platform/workflow-editor/utils/saveWorkflowDefinition.ts @@ -3,6 +3,8 @@ import {BranchCaseType, NodeDataType, TaskDispatcherContextType, WorkflowDefinit import {UseMutationResult} from '@tanstack/react-query'; import useWorkflowDataStore from '../stores/useWorkflowDataStore'; +import getRecursivelyUpdatedTasks from './getRecursivelyUpdatedTasks'; +import {getTask} from './getTask'; import insertTaskDispatcherSubtask from './insertTaskDispatcherSubtask'; const SPACE = 4; @@ -153,41 +155,53 @@ export default async function saveWorkflowDefinition({ (task) => task.name === existingWorkflowTask.name ); - if (existingTaskIndex === undefined || existingTaskIndex === -1) { - return; - } - let combinedParameters = { ...existingWorkflowTask.parameters, ...newTask.parameters, }; if (existingWorkflowTask.type !== newTask.type) { - delete updatedWorkflowDefinitionTasks[existingTaskIndex].parameters; - combinedParameters = newTask.parameters ?? {}; } - if (existingWorkflowTask.clusterRoot) { - const rootClusterElementTask: WorkflowTask = { - ...newTask, - clusterElements: { - ...(newTask.clusterElements || {}), - }, - }; - - updatedWorkflowDefinitionTasks[existingTaskIndex] = rootClusterElementTask; - } else { - const combinedTask: WorkflowTask = { - ...newTask, - parameters: combinedParameters, - }; + const taskToUpdate = existingWorkflowTask.clusterRoot + ? { + ...newTask, + clusterElements: { + ...(newTask.clusterElements || {}), + }, + } + : { + ...newTask, + parameters: combinedParameters, + }; + + if (existingTaskIndex !== undefined && existingTaskIndex !== -1) { + if (existingWorkflowTask.type !== newTask.type) { + delete updatedWorkflowDefinitionTasks[existingTaskIndex].parameters; + } updatedWorkflowDefinitionTasks = [ ...updatedWorkflowDefinitionTasks.slice(0, existingTaskIndex), - combinedTask, + taskToUpdate, ...updatedWorkflowDefinitionTasks.slice(existingTaskIndex + 1), ]; + } else { + const nestedTask = getTask({ + tasks: workflowDefinitionTasks, + workflowNodeName: existingWorkflowTask.name, + }); + + if (!nestedTask) { + console.error(`Task ${existingWorkflowTask.name} not found in workflow definition`); + + return; + } + + updatedWorkflowDefinitionTasks = getRecursivelyUpdatedTasks( + updatedWorkflowDefinitionTasks, + taskToUpdate + ); } } else { updatedWorkflowDefinitionTasks = [...(workflowDefinitionTasks || [])];