diff --git a/src/frontend/src/pages/FlowPage/components/PageComponent/index.tsx b/src/frontend/src/pages/FlowPage/components/PageComponent/index.tsx index c902ec2d5..73e02b92b 100644 --- a/src/frontend/src/pages/FlowPage/components/PageComponent/index.tsx +++ b/src/frontend/src/pages/FlowPage/components/PageComponent/index.tsx @@ -110,7 +110,7 @@ export default function Page({ getRandomName(), ); const newGroupNode = generateNodeFromFlow(newFlow, getNodeId); - const newEdges = reconnectEdges(newGroupNode, removedEdges); + // const newEdges = reconnectEdges(newGroupNode, removedEdges); setNodes([ ...clonedNodes.filter( (oldNodes) => @@ -120,17 +120,17 @@ export default function Page({ ), newGroupNode, ]); - setEdges([ - ...clonedEdges.filter( - (oldEdge) => - !clonedSelection!.nodes.some( - (selectionNode) => - selectionNode.id === oldEdge.target || - selectionNode.id === oldEdge.source, - ), - ), - ...newEdges, - ]); + // setEdges([ + // ...clonedEdges.filter( + // (oldEdge) => + // !clonedSelection!.nodes.some( + // (selectionNode) => + // selectionNode.id === oldEdge.target || + // selectionNode.id === oldEdge.source, + // ), + // ), + // ...newEdges, + // ]); } else { setErrorData({ title: INVALID_SELECTION_ERROR_ALERT, diff --git a/src/frontend/src/types/api/index.ts b/src/frontend/src/types/api/index.ts index 273da960d..bb36564f8 100644 --- a/src/frontend/src/types/api/index.ts +++ b/src/frontend/src/types/api/index.ts @@ -13,7 +13,7 @@ export type CustomFieldsType = { }; export type APIClassType = { - base_classes: Array; + base_classes?: Array; description: string; template: APITemplateType; display_name: string; @@ -67,6 +67,8 @@ export type OutputFieldType = { types: Array; selected?: string; name: string; + displayName?: string; + proxy?: { id: string; name: string }; }; export type sendAllProps = { nodes: Node[]; diff --git a/src/frontend/src/utils/reactflowUtils.ts b/src/frontend/src/utils/reactflowUtils.ts index c68374479..baf737a6b 100644 --- a/src/frontend/src/utils/reactflowUtils.ts +++ b/src/frontend/src/utils/reactflowUtils.ts @@ -24,6 +24,7 @@ import { APIObjectType, APITemplateType, InputFieldType, + OutputFieldType, } from "../types/api"; import { FlowType, @@ -78,7 +79,7 @@ export function cleanEdges(nodes: NodeType[], edges: Edge[]) { const parsedSourceHandle = scapeJSONParse(sourceHandle); const name = parsedSourceHandle.name; const output = sourceNode.data.node!.outputs?.find( - (output) => output.name === name + (output) => output.name === name, ); if (output) { const outputTypes = @@ -112,18 +113,18 @@ export function unselectAllNodes({ updateNodes, data }: unselectAllNodesType) { export function isValidConnection( { source, target, sourceHandle, targetHandle }: Connection, nodes: Node[], - edges: Edge[] + edges: Edge[], ) { const targetHandleObject: targetHandleType = scapeJSONParse(targetHandle!); const sourceHandleObject: sourceHandleType = scapeJSONParse(sourceHandle!); if ( targetHandleObject.inputTypes?.some( - (n) => n === sourceHandleObject.dataType + (n) => n === sourceHandleObject.dataType, ) || sourceHandleObject.output_types.some( (t) => targetHandleObject.inputTypes?.some((n) => n === t) || - t === targetHandleObject.type + t === targetHandleObject.type, ) ) { let targetNode = nodes.find((node) => node.id === target!)?.data?.node; @@ -156,7 +157,7 @@ export function removeApiKeys(flow: FlowType): FlowType { export function updateTemplate( reference: APITemplateType, - objectToUpdate: APITemplateType + objectToUpdate: APITemplateType, ): APITemplateType { let clonedObject: APITemplateType = cloneDeep(reference); @@ -216,7 +217,7 @@ export const processDataFromFlow = (flow: FlowType, refreshIds = true) => { export function updateIds( { edges, nodes }: { edges: Edge[]; nodes: Node[] }, - selection?: { edges: Edge[]; nodes: Node[] } + selection?: { edges: Edge[]; nodes: Node[] }, ) { let idsMap = {}; const selectionIds = selection?.nodes.map((n) => n.id); @@ -244,7 +245,7 @@ export function updateIds( edge.source = idsMap[edge.source]; edge.target = idsMap[edge.target]; const sourceHandleObject: sourceHandleType = scapeJSONParse( - edge.sourceHandle! + edge.sourceHandle!, ); edge.sourceHandle = scapedJSONStringfy({ ...sourceHandleObject, @@ -254,7 +255,7 @@ export function updateIds( edge.data.sourceHandle.id = edge.source; } const targetHandleObject: targetHandleType = scapeJSONParse( - edge.targetHandle! + edge.targetHandle!, ); edge.targetHandle = scapedJSONStringfy({ ...targetHandleObject, @@ -300,11 +301,11 @@ export function validateNode(node: NodeType, edges: Edge[]): Array { (scapeJSONParse(edge.targetHandle!) as targetHandleType).fieldName === t && (scapeJSONParse(edge.targetHandle!) as targetHandleType).id === - node.id + node.id, ) ) { errors.push( - `${displayName || type} is missing ${getFieldTitle(template, t)}.` + `${displayName || type} is missing ${getFieldTitle(template, t)}.`, ); } else if ( template[t].type === "dict" && @@ -318,15 +319,15 @@ export function validateNode(node: NodeType, edges: Edge[]): Array { errors.push( `${displayName || type} (${getFieldTitle( template, - t - )}) contains duplicate keys with the same values.` + t, + )}) contains duplicate keys with the same values.`, ); if (hasEmptyKey(template[t].value)) errors.push( `${displayName || type} (${getFieldTitle( template, - t - )}) field must not be empty.` + t, + )}) field must not be empty.`, ); } return errors; @@ -335,7 +336,7 @@ export function validateNode(node: NodeType, edges: Edge[]): Array { export function validateNodes( nodes: Node[], - edges: Edge[] + edges: Edge[], ): // this returns an array of tuples with the node id and the errors Array<{ id: string; errors: Array }> { if (nodes.length === 0) { @@ -356,7 +357,7 @@ export function updateEdges(edges: Edge[]) { if (edges) edges.forEach((edge) => { const targetHandleObject: targetHandleType = scapeJSONParse( - edge.targetHandle! + edge.targetHandle!, ); edge.className = "stroke-gray-900 stroke-connection"; }); @@ -406,7 +407,7 @@ export function updateEdgesHandleIds({ if (source && sourceNode) { const output_types = sourceNode.data.node!.output_types ?? - sourceNode.data.node!.base_classes; + sourceNode.data.node!.base_classes!; newSource = { id: sourceNode.data.id, output_types, @@ -456,11 +457,11 @@ export function updateNewOutput({ nodes, edges }: updateEdgesHandleIdsType) { if (newTargetHandle.inputTypes && newTargetHandle.inputTypes.length > 0) { //conjuction subtraction intersection = newSourceHandle.output_types.filter((type) => - newTargetHandle.inputTypes!.includes(type) + newTargetHandle.inputTypes!.includes(type), ); } else { intersection = newSourceHandle.output_types.filter( - (type) => type === newTargetHandle.type + (type) => type === newTargetHandle.type, ); } const selected = intersection[0]; @@ -472,10 +473,10 @@ export function updateNewOutput({ nodes, edges }: updateEdgesHandleIdsType) { } const types = sourceNode.data.node!.output_types ?? - sourceNode.data.node!.base_classes; + sourceNode.data.node!.base_classes!; if ( !sourceNode.data.node!.outputs.some( - (output) => output.selected === selected + (output) => output.selected === selected, ) ) { sourceNode.data.node!.outputs.push({ @@ -498,7 +499,7 @@ export function handleKeyDown( | React.KeyboardEvent | React.KeyboardEvent, inputValue: string | string[] | null, - block: string + block: string, ) { //condition to fix bug control+backspace on Windows/Linux if ( @@ -523,7 +524,7 @@ export function handleKeyDown( } export function handleOnlyIntegerInput( - event: React.KeyboardEvent + event: React.KeyboardEvent, ) { if ( event.key === "." || @@ -539,7 +540,7 @@ export function handleOnlyIntegerInput( export function getConnectedNodes( edge: Edge, - nodes: Array + nodes: Array, ): Array { const sourceId = edge.source; const targetId = edge.target; @@ -640,7 +641,7 @@ export function checkOldEdgesHandles(edges: Edge[]): boolean { !edge.sourceHandle || !edge.targetHandle || !edge.sourceHandle.includes("{") || - !edge.targetHandle.includes("{") + !edge.targetHandle.includes("{"), ); } @@ -667,7 +668,7 @@ export function customStringify(obj: any): string { const keys = Object.keys(obj).sort(); const keyValuePairs = keys.map( - (key) => `"${key}":${customStringify(obj[key])}` + (key) => `"${key}":${customStringify(obj[key])}`, ); return `{${keyValuePairs.join(",")}}`; } @@ -696,7 +697,7 @@ export function getHandleId( source: string, sourceHandle: string, target: string, - targetHandle: string + targetHandle: string, ) { return ( "reactflow__edge-" + source + sourceHandle + "-" + target + targetHandle @@ -707,16 +708,16 @@ export function generateFlow( selection: OnSelectionChangeParams, nodes: Node[], edges: Edge[], - name: string + name: string, ): generateFlowType { const newFlowData = { nodes, edges, viewport: { zoom: 1, x: 0, y: 0 } }; const uid = new ShortUniqueId({ length: 5 }); /* remove edges that are not connected to selected nodes on both ends */ - newFlowData.edges = selection.edges.filter( + newFlowData.edges = edges.filter( (edge) => selection.nodes.some((node) => node.id === edge.target) && - selection.nodes.some((node) => node.id === edge.source) + selection.nodes.some((node) => node.id === edge.source), ); newFlowData.nodes = selection.nodes; @@ -737,7 +738,7 @@ export function generateFlow( (edge) => (selection.nodes.some((node) => node.id === edge.target) || selection.nodes.some((node) => node.id === edge.source)) && - newFlowData.edges.every((e) => e.id !== edge.id) + newFlowData.edges.every((e) => e.id !== edge.id), ), }; } @@ -748,13 +749,13 @@ export function reconnectEdges(groupNode: NodeType, excludedEdges: Edge[]) { const { nodes, edges } = groupNode.data.node!.flow!.data!; const lastNode = findLastNode(groupNode.data.node!.flow!.data!); newEdges = newEdges.filter( - (e) => !(nodes.some((n) => n.id === e.source) && e.source !== lastNode?.id) + (e) => !(nodes.some((n) => n.id === e.source) && e.source !== lastNode?.id), ); newEdges.forEach((edge) => { if (lastNode && edge.source === lastNode.id) { edge.source = groupNode.id; let newSourceHandle: sourceHandleType = scapeJSONParse( - edge.sourceHandle! + edge.sourceHandle!, ); newSourceHandle.id = groupNode.id; edge.sourceHandle = scapedJSONStringfy(newSourceHandle); @@ -781,7 +782,7 @@ export function reconnectEdges(groupNode: NodeType, excludedEdges: Edge[]) { export function filterFlow( selection: OnSelectionChangeParams, setNodes: (update: Node[] | ((oldState: Node[]) => Node[])) => void, - setEdges: (update: Edge[] | ((oldState: Edge[]) => Edge[])) => void + setEdges: (update: Edge[] | ((oldState: Edge[]) => Edge[])) => void, ) { setNodes((nodes) => nodes.filter((node) => !selection.nodes.includes(node))); setEdges((edges) => edges.filter((edge) => !selection.edges.includes(edge))); @@ -819,7 +820,7 @@ export function updateFlowPosition(NewPosition: XYPosition, flow: FlowType) { export function concatFlows( flow: FlowType, setNodes: (update: Node[] | ((oldState: Node[]) => Node[])) => void, - setEdges: (update: Edge[] | ((oldState: Edge[]) => Edge[])) => void + setEdges: (update: Edge[] | ((oldState: Edge[]) => Edge[])) => void, ) { const { nodes, edges } = flow.data!; setNodes((old) => [...old, ...nodes]); @@ -828,7 +829,7 @@ export function concatFlows( export function validateSelection( selection: OnSelectionChangeParams, - edges: Edge[] + edges: Edge[], ): Array { const clonedSelection = cloneDeep(selection); const clonedEdges = cloneDeep(edges); @@ -842,7 +843,7 @@ export function validateSelection( let nodesSet = new Set(clonedSelection.nodes.map((n) => n.id)); // then filter the edges that are connected to the nodes in the set let connectedEdges = clonedSelection.edges.filter( - (e) => nodesSet.has(e.source) && nodesSet.has(e.target) + (e) => nodesSet.has(e.source) && nodesSet.has(e.target), ); // add the edges to the selection clonedSelection.edges = connectedEdges; @@ -856,17 +857,17 @@ export function validateSelection( clonedSelection.nodes.some( (node) => isInputNode(node.data as NodeDataType) || - isOutputNode(node.data as NodeDataType) + isOutputNode(node.data as NodeDataType), ) ) { errorsArray.push( - "Please select only nodes that are not input or output nodes" + "Please select only nodes that are not input or output nodes", ); } //check if there are two or more nodes with free outputs if ( clonedSelection.nodes.filter( - (n) => !clonedSelection.edges.some((e) => e.source === n.id) + (n) => !clonedSelection.edges.some((e) => e.source === n.id), ).length > 1 ) { errorsArray.push("Please select only one node with free outputs"); @@ -877,7 +878,7 @@ export function validateSelection( clonedSelection.nodes.some( (node) => !clonedSelection.edges.some((edge) => edge.target === node.id) && - !clonedSelection.edges.some((edge) => edge.source === node.id) + !clonedSelection.edges.some((edge) => edge.source === node.id), ) ) { errorsArray.push("Please select only nodes that are connected"); @@ -922,7 +923,7 @@ export function mergeNodeTemplates({ Object.keys(nodeTemplate) .filter((field_name) => field_name.charAt(0) !== "_") .forEach((key) => { - if (!isHandleConnected(edges, key, nodeTemplate[key], node.id)) { + if (!isTargetHandleConnected(edges, key, nodeTemplate[key], node.id)) { template[key + "_" + node.id] = nodeTemplate[key]; template[key + "_" + node.id].proxy = { id: node.id, field: key }; if (node.type === "groupNode") { @@ -934,19 +935,19 @@ export function mergeNodeTemplates({ nodeTemplate[key].display_name ? nodeTemplate[key].display_name : nodeTemplate[key].name - ? toTitleCase(nodeTemplate[key].name) - : toTitleCase(key); + ? toTitleCase(nodeTemplate[key].name) + : toTitleCase(key); } } }); }); return template; } -function isHandleConnected( +function isTargetHandleConnected( edges: Edge[], key: string, field: InputFieldType, - nodeId: string + nodeId: string, ) { /* this function receives a flow and a handleId and check if there is a connection with this handle @@ -962,7 +963,7 @@ function isHandleConnected( id: nodeId, proxy: { id: field.proxy!.id, field: field.proxy!.field }, inputTypes: field.input_types, - } as targetHandleType) + } as targetHandleType), ) ) { return true; @@ -977,7 +978,7 @@ function isHandleConnected( fieldName: key, id: nodeId, inputTypes: field.input_types, - } as targetHandleType) + } as targetHandleType), ) ) { return true; @@ -1000,25 +1001,24 @@ export function generateNodeTemplate(Flow: FlowType) { export function generateNodeFromFlow( flow: FlowType, - getNodeId: (type: string) => string + getNodeId: (type: string) => string, ): NodeType { const { nodes } = flow.data!; const outputNode = cloneDeep(findLastNode(flow.data!)); const position = getMiddlePoint(nodes); let data = cloneDeep(flow); - const id = getNodeId(outputNode?.data.type!); + const id = getNodeId("groupComponent"); const newGroupNode: NodeType = { data: { id, - type: outputNode?.data.type!, + type: "GroupNode", node: { - output_types: outputNode!.data.node!.output_types, display_name: "Group", documentation: "", - base_classes: outputNode!.data.node!.base_classes, description: "", template: generateNodeTemplate(data), flow: data, + outputs: generateNodeOutputs(data), }, }, id, @@ -1028,10 +1028,42 @@ export function generateNodeFromFlow( return newGroupNode; } +function generateNodeOutputs(flow: FlowType) { + const { nodes, edges } = flow.data!; + const outputs: Array = []; + nodes.forEach((node: NodeType) => { + if (node.data.node?.outputs) { + const nodeOutputs = node.data.node.outputs; + nodeOutputs.forEach((output) => { + //filter outputs that are not connected + console.log(output); + console.log(edges); + if ( + !edges.some( + (edge) => + edge.source === node.id && + (edge.data.sourceHandle as sourceHandleType).name === output.name, + ) + ) { + outputs.push( + cloneDeep({ + ...output, + proxy: { id: node.id, name: output.name }, + name: node.id + "_" + output.name, + displayName: output.displayName, + }), + ); + } + }); + } + }); + return outputs; +} + export function connectedInputNodesOnHandle( nodeId: string, handleId: string, - { nodes, edges }: { nodes: NodeType[]; edges: Edge[] } + { nodes, edges }: { nodes: NodeType[]; edges: Edge[] }, ) { const connectedNodes: Array<{ name: string; id: string; isGroup: boolean }> = []; @@ -1068,7 +1100,7 @@ export function connectedInputNodesOnHandle( export function updateProxyIdsOnTemplate( template: APITemplateType, - idsMap: { [key: string]: string } + idsMap: { [key: string]: string }, ) { Object.keys(template).forEach((key) => { if (template[key].proxy && idsMap[template[key].proxy!.id]) { @@ -1079,7 +1111,7 @@ export function updateProxyIdsOnTemplate( export function updateEdgesIds( edges: Edge[], - idsMap: { [key: string]: string } + idsMap: { [key: string]: string }, ) { edges.forEach((edge) => { let targetHandle: targetHandleType = edge.data.targetHandle; @@ -1120,7 +1152,7 @@ export function expandGroupNode( nodes: Node[], edges: Edge[], setNodes: (update: Node[] | ((oldState: Node[]) => Node[])) => void, - setEdges: (update: Edge[] | ((oldState: Edge[]) => Edge[])) => void + setEdges: (update: Edge[] | ((oldState: Edge[]) => Edge[])) => void, ) { const idsMap = updateIds(flow!.data!); updateProxyIdsOnTemplate(template, idsMap); @@ -1163,7 +1195,7 @@ export function expandGroupNode( const lastNode = cloneDeep(findLastNode(flow!.data!)); newEdge.source = lastNode!.id; let newSourceHandle: sourceHandleType = scapeJSONParse( - newEdge.sourceHandle! + newEdge.sourceHandle!, ); newSourceHandle.id = lastNode!.id; newEdge.data.sourceHandle = newSourceHandle; @@ -1220,7 +1252,7 @@ export function expandGroupNode( export function getGroupStatus( flow: FlowType, - ssData: { [key: string]: { valid: boolean; params: string } } + ssData: { [key: string]: { valid: boolean; params: string } }, ) { let status = { valid: true, params: SUCCESS_BUILD }; const { nodes } = flow.data!; @@ -1239,7 +1271,7 @@ export function getGroupStatus( export function createFlowComponent( nodeData: NodeDataType, - version: string + version: string, ): FlowType { const flowNode: FlowType = { data: { @@ -1275,7 +1307,7 @@ export function downloadNode(NodeFLow: FlowType) { export function updateComponentNameAndType( data: any, - component: NodeDataType + component: NodeDataType, ) {} export function removeFileNameFromComponents(flow: FlowType) { @@ -1349,7 +1381,7 @@ export function extractFieldsFromComponenents(data: APIObjectType) { export function downloadFlow( flow: FlowType, flowName: string, - flowDescription?: string + flowDescription?: string, ) { let clonedFlow = cloneDeep(flow); removeFileNameFromComponents(clonedFlow); @@ -1359,7 +1391,7 @@ export function downloadFlow( ...clonedFlow, name: flowName, description: flowDescription, - }) + }), )}`; // create a link element and set its properties @@ -1374,7 +1406,7 @@ export function downloadFlow( export function downloadFlows() { downloadFlowsFromDatabase().then((flows) => { const jsonString = `data:text/json;chatset=utf-8,${encodeURIComponent( - JSON.stringify(flows) + JSON.stringify(flows), )}`; // create a link element and set its properties @@ -1398,7 +1430,7 @@ export function getRandomDescription(): string { export const createNewFlow = ( flowData: ReactFlowJsonObject, flow: FlowType, - folderId: string + folderId: string, ) => { return { description: flow?.description ?? getRandomDescription(),