From 63f7d3bae29471f04d7b82a5b64e291f30a77faa Mon Sep 17 00:00:00 2001 From: balibabu Date: Fri, 8 Nov 2024 15:50:01 +0800 Subject: [PATCH] feat: Support shortcut keys to copy nodes #3283 (#3293) ### What problem does this PR solve? feat: Support shortcut keys to copy nodes #3283 ### Type of change - [x] New Feature (non-breaking change which adds functionality) --- web/src/pages/flow/canvas/index.tsx | 13 ++- web/src/pages/flow/canvas/node/dropdown.tsx | 10 +-- web/src/pages/flow/hooks.ts | 92 ++++++++++++++------- web/src/pages/flow/index.tsx | 3 +- web/src/pages/flow/store.ts | 4 +- 5 files changed, 79 insertions(+), 43 deletions(-) diff --git a/web/src/pages/flow/canvas/index.tsx b/web/src/pages/flow/canvas/index.tsx index 2794ff526..d92420e58 100644 --- a/web/src/pages/flow/canvas/index.tsx +++ b/web/src/pages/flow/canvas/index.tsx @@ -125,7 +125,6 @@ function FlowCanvas({ chatDrawerVisible, hideChatDrawer }: IProps) { onNodeClick={onNodeClick} onPaneClick={onPaneClick} onInit={setReactFlowInstance} - // onKeyUp={handleKeyUp} onSelectionChange={onSelectionChange} nodeOrigin={[0.5, 0]} isValidConnection={isValidConnection} @@ -141,6 +140,18 @@ function FlowCanvas({ chatDrawerVisible, hideChatDrawer }: IProps) { }, }} deleteKeyCode={['Delete', 'Backspace']} + onPaste={(...params) => { + console.info('onPaste:', ...params); + }} + onPasteCapture={(...params) => { + console.info('onPasteCapture:', ...params); + }} + onCopy={(...params) => { + console.info('onCopy:', ...params); + }} + onCopyCapture={(...params) => { + console.info('onCopyCapture:', ...params); + }} > diff --git a/web/src/pages/flow/canvas/node/dropdown.tsx b/web/src/pages/flow/canvas/node/dropdown.tsx index ea8fcc1fd..7e6fb1e98 100644 --- a/web/src/pages/flow/canvas/node/dropdown.tsx +++ b/web/src/pages/flow/canvas/node/dropdown.tsx @@ -3,7 +3,7 @@ import { CopyOutlined } from '@ant-design/icons'; import { Flex, MenuProps } from 'antd'; import { useCallback } from 'react'; import { useTranslation } from 'react-i18next'; -import { useGetNodeName } from '../../hooks'; +import { useDuplicateNode } from '../../hooks'; import useGraphStore from '../../store'; interface IProps { @@ -15,21 +15,17 @@ interface IProps { const NodeDropdown = ({ id, iconFontColor, label }: IProps) => { const { t } = useTranslation(); const deleteNodeById = useGraphStore((store) => store.deleteNodeById); - const duplicateNodeById = useGraphStore((store) => store.duplicateNode); - const getNodeName = useGetNodeName(); const deleteNode = useCallback(() => { deleteNodeById(id); }, [id, deleteNodeById]); - const duplicateNode = useCallback(() => { - duplicateNodeById(id, getNodeName(label)); - }, [duplicateNodeById, id, getNodeName, label]); + const duplicateNode = useDuplicateNode(); const items: MenuProps['items'] = [ { key: '2', - onClick: duplicateNode, + onClick: () => duplicateNode(id, label), label: ( {t('common.copy')} diff --git a/web/src/pages/flow/hooks.ts b/web/src/pages/flow/hooks.ts index c6937d87b..0fb390db1 100644 --- a/web/src/pages/flow/hooks.ts +++ b/web/src/pages/flow/hooks.ts @@ -4,7 +4,6 @@ import { IGraph } from '@/interfaces/database/flow'; import { useIsFetching } from '@tanstack/react-query'; import React, { ChangeEvent, - KeyboardEventHandler, useCallback, useEffect, useMemo, @@ -20,7 +19,6 @@ import { import { useFetchModelId, useSendMessageWithSse } from '@/hooks/logic-hooks'; import { Variable } from '@/interfaces/database/chat'; import api from '@/utils/api'; -import { useDebounceEffect } from 'ahooks'; import { FormInstance, message } from 'antd'; import { humanId } from 'human-id'; import { lowerFirst } from 'lodash'; @@ -253,20 +251,6 @@ export const useShowDrawer = () => { }; }; -export const useHandleKeyUp = () => { - const deleteEdge = useGraphStore((state) => state.deleteEdge); - const handleKeyUp: KeyboardEventHandler = useCallback( - (e) => { - if (e.code === 'Delete') { - deleteEdge(); - } - }, - [deleteEdge], - ); - - return { handleKeyUp }; -}; - export const useSaveGraph = () => { const { data } = useFetchFlow(); const { setFlow } = useSetFlow(); @@ -284,20 +268,6 @@ export const useSaveGraph = () => { return { saveGraph }; }; -export const useWatchGraphChange = () => { - const nodes = useGraphStore((state) => state.nodes); - const edges = useGraphStore((state) => state.edges); - useDebounceEffect( - () => { - // console.info('useDebounceEffect'); - }, - [nodes, edges], - { - wait: 1000, - }, - ); -}; - export const useHandleFormValuesChange = (id?: string) => { const updateNodeForm = useGraphStore((state) => state.updateNodeForm); const handleValuesChange = useCallback( @@ -348,8 +318,6 @@ export const useFetchDataOnMount = () => { setGraphInfo(data?.dsl?.graph ?? ({} as IGraph)); }, [setGraphInfo, data]); - useWatchGraphChange(); - useEffect(() => { refetch(); }, [refetch]); @@ -640,3 +608,63 @@ export const useGetComponentLabelByValue = (nodeId: string) => { ); return getLabel; }; + +export const useDuplicateNode = () => { + const duplicateNodeById = useGraphStore((store) => store.duplicateNode); + const getNodeName = useGetNodeName(); + + const duplicateNode = useCallback( + (id: string, label: string) => { + duplicateNodeById(id, getNodeName(label)); + }, + [duplicateNodeById, getNodeName], + ); + + return duplicateNode; +}; + +export const useCopyPaste = () => { + const nodes = useGraphStore((state) => state.nodes); + const duplicateNode = useDuplicateNode(); + + const onCopyCapture = useCallback( + (event: ClipboardEvent) => { + event.preventDefault(); + const nodesStr = JSON.stringify( + nodes.filter((n) => n.selected && n.data.label !== Operator.Begin), + ); + + event.clipboardData?.setData('agent:nodes', nodesStr); + }, + [nodes], + ); + + const onPasteCapture = useCallback( + (event: ClipboardEvent) => { + event.preventDefault(); + const nodes = JSON.parse( + event.clipboardData?.getData('agent:nodes') || '[]', + ) as Node[] | undefined; + if (nodes) { + nodes.forEach((n) => { + duplicateNode(n.id, n.data.label); + }); + } + }, + [duplicateNode], + ); + + useEffect(() => { + window.addEventListener('copy', onCopyCapture); + return () => { + window.removeEventListener('copy', onCopyCapture); + }; + }, [onCopyCapture]); + + useEffect(() => { + window.addEventListener('paste', onPasteCapture); + return () => { + window.removeEventListener('paste', onPasteCapture); + }; + }, [onPasteCapture]); +}; diff --git a/web/src/pages/flow/index.tsx b/web/src/pages/flow/index.tsx index 52bc54d5c..980c08b01 100644 --- a/web/src/pages/flow/index.tsx +++ b/web/src/pages/flow/index.tsx @@ -5,7 +5,7 @@ import { ReactFlowProvider } from 'reactflow'; import FlowCanvas from './canvas'; import Sider from './flow-sider'; import FlowHeader from './header'; -import { useFetchDataOnMount } from './hooks'; +import { useCopyPaste, useFetchDataOnMount } from './hooks'; const { Content } = Layout; @@ -18,6 +18,7 @@ function RagFlow() { } = useSetModalState(); useFetchDataOnMount(); + useCopyPaste(); return ( diff --git a/web/src/pages/flow/store.ts b/web/src/pages/flow/store.ts index cd15a4089..ea302ef9a 100644 --- a/web/src/pages/flow/store.ts +++ b/web/src/pages/flow/store.ts @@ -236,8 +236,8 @@ const useGraphStore = create()( const { getNode, addNode, generateNodeName } = get(); const node = getNode(id); const position = { - x: (node?.position?.x || 0) + 30, - y: (node?.position?.y || 0) + 20, + x: (node?.position?.x || 0) + 50, + y: (node?.position?.y || 0) + 50, }; addNode({