| import { logger } from 'app/logging/logger'; |
| import { getStore } from 'app/store/nanostores/store'; |
| import { deepClone } from 'common/util/deepClone'; |
| import { |
| $copiedEdges, |
| $copiedNodes, |
| $cursorPos, |
| $edgesToCopiedNodes, |
| $templates, |
| edgesChanged, |
| nodesChanged, |
| } from 'features/nodes/store/nodesSlice'; |
| import { selectNodesSlice } from 'features/nodes/store/selectors'; |
| import { findUnoccupiedPosition } from 'features/nodes/store/util/findUnoccupiedPosition'; |
| import { validateConnection } from 'features/nodes/store/util/validateConnection'; |
| import { t } from 'i18next'; |
| import { isEqual, isNil, uniqWith } from 'lodash-es'; |
| import type { EdgeChange, NodeChange } from 'reactflow'; |
| import { assert } from 'tsafe'; |
| import { v4 as uuidv4 } from 'uuid'; |
|
|
| const log = logger('workflows'); |
|
|
| const copySelection = () => { |
| |
| const { getState } = getStore(); |
| const { nodes, edges } = selectNodesSlice(getState()); |
| const selectedNodes = nodes.filter((node) => node.selected); |
| const selectedEdges = edges.filter((edge) => edge.selected); |
| const edgesToSelectedNodes = edges.filter((edge) => selectedNodes.some((node) => node.id === edge.target)); |
| $copiedNodes.set(selectedNodes); |
| $copiedEdges.set(selectedEdges); |
| $edgesToCopiedNodes.set(edgesToSelectedNodes); |
| }; |
|
|
| const _pasteSelection = (withEdgesToCopiedNodes?: boolean) => { |
| const { getState, dispatch } = getStore(); |
| const { nodes, edges } = selectNodesSlice(getState()); |
| const templates = $templates.get(); |
| const cursorPos = $cursorPos.get(); |
|
|
| const copiedNodes = deepClone($copiedNodes.get()); |
| let copiedEdges = deepClone($copiedEdges.get()); |
|
|
| if (withEdgesToCopiedNodes) { |
| const edgesToCopiedNodes = deepClone($edgesToCopiedNodes.get()); |
| copiedEdges = uniqWith([...copiedEdges, ...edgesToCopiedNodes], isEqual); |
| } |
|
|
| |
| const xCoords = copiedNodes.map((node) => node.position.x); |
| const yCoords = copiedNodes.map((node) => node.position.y); |
| const minX = Math.min(...xCoords); |
| const minY = Math.min(...yCoords); |
| const offsetX = cursorPos ? cursorPos.x - minX : 50; |
| const offsetY = cursorPos ? cursorPos.y - minY : 50; |
|
|
| copiedNodes.forEach((node) => { |
| const { x, y } = findUnoccupiedPosition(nodes, node.position.x + offsetX, node.position.y + offsetY); |
| node.position.x = x; |
| node.position.y = y; |
| |
| node.selected = true; |
| |
| const id = uuidv4(); |
| |
| for (const edge of copiedEdges) { |
| if (edge.source === node.id) { |
| edge.source = id; |
| } else if (edge.target === node.id) { |
| edge.target = id; |
| } |
| } |
| node.id = id; |
| node.data.id = id; |
| }); |
|
|
| copiedEdges.forEach((edge) => { |
| |
| edge.id = uuidv4(); |
| }); |
|
|
| const nodeChanges: NodeChange[] = []; |
| const edgeChanges: EdgeChange[] = []; |
| |
| nodes.forEach(({ id, selected }) => { |
| if (selected) { |
| nodeChanges.push({ |
| type: 'select', |
| id, |
| selected: false, |
| }); |
| } |
| }); |
| |
| copiedNodes.forEach((n) => { |
| nodeChanges.push({ |
| type: 'add', |
| item: n, |
| }); |
| }); |
| |
| edges.forEach(({ id, selected }) => { |
| if (selected) { |
| edgeChanges.push({ |
| type: 'select', |
| id, |
| selected: false, |
| }); |
| } |
| }); |
|
|
| |
| |
| const validationNodes = [...nodes, ...copiedNodes]; |
| |
| |
| const validationEdges = [...edges]; |
|
|
| |
| copiedEdges.forEach((e) => { |
| const { source, sourceHandle, target, targetHandle } = e; |
| |
| assert(!isNil(sourceHandle)); |
| assert(!isNil(targetHandle)); |
|
|
| |
| const validationResult = validateConnection( |
| { source, sourceHandle, target, targetHandle }, |
| validationNodes, |
| validationEdges, |
| templates, |
| null, |
| true |
| ); |
| |
| if (!validationResult.isValid) { |
| log.warn( |
| { edge: { source, sourceHandle, target, targetHandle } }, |
| `Invalid edge, cannot paste: ${t(validationResult.messageTKey)}` |
| ); |
| return; |
| } |
| edgeChanges.push({ |
| type: 'add', |
| item: e, |
| }); |
| |
| validationEdges.push(e); |
| }); |
| if (nodeChanges.length > 0) { |
| dispatch(nodesChanged(nodeChanges)); |
| } |
| if (edgeChanges.length > 0) { |
| dispatch(edgesChanged(edgeChanges)); |
| } |
| }; |
|
|
| const pasteSelection = () => { |
| _pasteSelection(false); |
| }; |
|
|
| const pasteSelectionWithEdges = () => { |
| _pasteSelection(true); |
| }; |
|
|
| const api = { copySelection, pasteSelection, pasteSelectionWithEdges }; |
|
|
| export const useCopyPaste = () => { |
| return api; |
| }; |
|
|