From 17efc3ab7933f476627462f2a93aa3bd4850e210 Mon Sep 17 00:00:00 2001 From: Pascal M <11357019+perzeuss@users.noreply.github.com> Date: Fri, 12 Apr 2024 14:40:19 +0200 Subject: [PATCH] feat: add workflow editor shortcuts (#3382) (#3390) --- .../workflow/hooks/use-nodes-interactions.ts | 107 ++++++++++++++++++ web/app/components/workflow/index.tsx | 12 +- web/app/components/workflow/store.ts | 4 + 3 files changed, 122 insertions(+), 1 deletion(-) diff --git a/web/app/components/workflow/hooks/use-nodes-interactions.ts b/web/app/components/workflow/hooks/use-nodes-interactions.ts index 191bd7af0a..803799ac13 100644 --- a/web/app/components/workflow/hooks/use-nodes-interactions.ts +++ b/web/app/components/workflow/hooks/use-nodes-interactions.ts @@ -715,6 +715,108 @@ export const useNodesInteractions = () => { handleSyncWorkflowDraft() }, [store, handleSyncWorkflowDraft, getNodesReadOnly, t]) + const handleNodeCopySelected = useCallback((): undefined | Node[] => { + if (getNodesReadOnly()) + return + + const { + setClipboardElements, + } = workflowStore.getState() + + const { + getNodes, + } = store.getState() + + const nodes = getNodes() + const nodesToCopy = nodes.filter(node => node.data.selected) + + setClipboardElements(nodesToCopy) + + return nodesToCopy + }, [getNodesReadOnly, store, workflowStore]) + + const handleNodePaste = useCallback((): undefined | Node[] => { + if (getNodesReadOnly()) + return + + const { + clipboardElements, + } = workflowStore.getState() + + const { + getNodes, + setNodes, + } = store.getState() + + const nodesToPaste: Node[] = [] + const nodes = getNodes() + + for (const nodeToPaste of clipboardElements) { + const nodeType = nodeToPaste.data.type + const nodesWithSameType = nodes.filter(node => node.data.type === nodeType) + + const newNode = generateNewNode({ + data: { + ...NODES_INITIAL_DATA[nodeType], + ...nodeToPaste.data, + _connectedSourceHandleIds: [], + _connectedTargetHandleIds: [], + title: nodesWithSameType.length > 0 ? `${t(`workflow.blocks.${nodeType}`)} ${nodesWithSameType.length + 1}` : t(`workflow.blocks.${nodeType}`), + selected: true, + }, + position: { + x: nodeToPaste.position.x + 10, + y: nodeToPaste.position.y + 10, + }, + }) + nodesToPaste.push(newNode) + } + + setNodes([...nodes.map((n: Node) => ({ ...n, selected: false, data: { ...n.data, selected: false } })), ...nodesToPaste]) + + handleSyncWorkflowDraft() + + return nodesToPaste + }, [getNodesReadOnly, handleSyncWorkflowDraft, store, t, workflowStore]) + + const handleNodeDuplicateSelected = useCallback(() => { + if (getNodesReadOnly()) + return + + handleNodeCopySelected() + handleNodePaste() + }, [getNodesReadOnly, handleNodeCopySelected, handleNodePaste]) + + const handleNodeCut = useCallback(() => { + if (getNodesReadOnly()) + return + + const nodesToCut = handleNodeCopySelected() + if (!nodesToCut) + return + + for (const node of nodesToCut) + handleNodeDelete(node.id) + }, [getNodesReadOnly, handleNodeCopySelected, handleNodeDelete]) + + const handleNodeDeleteSelected = useCallback(() => { + if (getNodesReadOnly()) + return + + const { + getNodes, + } = store.getState() + + const nodes = getNodes() + const nodesToDelete = nodes.filter(node => node.data.selected) + + if (!nodesToDelete) + return + + for (const node of nodesToDelete) + handleNodeDelete(node.id) + }, [getNodesReadOnly, handleNodeDelete, store]) + return { handleNodeDragStart, handleNodeDrag, @@ -729,5 +831,10 @@ export const useNodesInteractions = () => { handleNodeDelete, handleNodeChange, handleNodeAdd, + handleNodeDuplicateSelected, + handleNodeCopySelected, + handleNodeCut, + handleNodeDeleteSelected, + handleNodePaste, } } diff --git a/web/app/components/workflow/index.tsx b/web/app/components/workflow/index.tsx index 3bcd1cccd5..1bcc05fd93 100644 --- a/web/app/components/workflow/index.tsx +++ b/web/app/components/workflow/index.tsx @@ -113,6 +113,11 @@ const Workflow: FC = memo(({ handleNodeConnect, handleNodeConnectStart, handleNodeConnectEnd, + handleNodeDuplicateSelected, + handleNodeCopySelected, + handleNodeCut, + handleNodeDeleteSelected, + handleNodePaste, } = useNodesInteractions() const { handleEdgeEnter, @@ -128,7 +133,12 @@ const Workflow: FC = memo(({ }, }) - useKeyPress('Backspace', handleEdgeDelete) + useKeyPress(['delete'], handleEdgeDelete) + useKeyPress(['delete'], handleNodeDeleteSelected) + useKeyPress(['ctrl.c', 'meta.c'], handleNodeCopySelected) + useKeyPress(['ctrl.x', 'meta.x'], handleNodeCut) + useKeyPress(['ctrl.v', 'meta.v'], handleNodePaste) + useKeyPress(['ctrl.alt.d', 'meta.shift.d'], handleNodeDuplicateSelected) return (
void customTools: ToolWithProvider[] setCustomTools: (tools: ToolWithProvider[]) => void + clipboardElements: Node[] + setClipboardElements: (clipboardElements: Node[]) => void } export const createWorkflowStore = () => { @@ -107,6 +109,8 @@ export const createWorkflowStore = () => { setBuildInTools: buildInTools => set(() => ({ buildInTools })), customTools: [], setCustomTools: customTools => set(() => ({ customTools })), + clipboardElements: [], + setClipboardElements: clipboardElements => set(() => ({ clipboardElements })), })) }