balibabu commited on
Commit
50eb137
·
1 Parent(s): 692cc99

feat: Support shortcut keys to copy nodes #3283 (#3293)

Browse files

### 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 CHANGED
@@ -125,7 +125,6 @@ function FlowCanvas({ chatDrawerVisible, hideChatDrawer }: IProps) {
125
  onNodeClick={onNodeClick}
126
  onPaneClick={onPaneClick}
127
  onInit={setReactFlowInstance}
128
- // onKeyUp={handleKeyUp}
129
  onSelectionChange={onSelectionChange}
130
  nodeOrigin={[0.5, 0]}
131
  isValidConnection={isValidConnection}
@@ -141,6 +140,18 @@ function FlowCanvas({ chatDrawerVisible, hideChatDrawer }: IProps) {
141
  },
142
  }}
143
  deleteKeyCode={['Delete', 'Backspace']}
 
 
 
 
 
 
 
 
 
 
 
 
144
  >
145
  <Background />
146
  <Controls />
 
125
  onNodeClick={onNodeClick}
126
  onPaneClick={onPaneClick}
127
  onInit={setReactFlowInstance}
 
128
  onSelectionChange={onSelectionChange}
129
  nodeOrigin={[0.5, 0]}
130
  isValidConnection={isValidConnection}
 
140
  },
141
  }}
142
  deleteKeyCode={['Delete', 'Backspace']}
143
+ onPaste={(...params) => {
144
+ console.info('onPaste:', ...params);
145
+ }}
146
+ onPasteCapture={(...params) => {
147
+ console.info('onPasteCapture:', ...params);
148
+ }}
149
+ onCopy={(...params) => {
150
+ console.info('onCopy:', ...params);
151
+ }}
152
+ onCopyCapture={(...params) => {
153
+ console.info('onCopyCapture:', ...params);
154
+ }}
155
  >
156
  <Background />
157
  <Controls />
web/src/pages/flow/canvas/node/dropdown.tsx CHANGED
@@ -3,7 +3,7 @@ import { CopyOutlined } from '@ant-design/icons';
3
  import { Flex, MenuProps } from 'antd';
4
  import { useCallback } from 'react';
5
  import { useTranslation } from 'react-i18next';
6
- import { useGetNodeName } from '../../hooks';
7
  import useGraphStore from '../../store';
8
 
9
  interface IProps {
@@ -15,21 +15,17 @@ interface IProps {
15
  const NodeDropdown = ({ id, iconFontColor, label }: IProps) => {
16
  const { t } = useTranslation();
17
  const deleteNodeById = useGraphStore((store) => store.deleteNodeById);
18
- const duplicateNodeById = useGraphStore((store) => store.duplicateNode);
19
- const getNodeName = useGetNodeName();
20
 
21
  const deleteNode = useCallback(() => {
22
  deleteNodeById(id);
23
  }, [id, deleteNodeById]);
24
 
25
- const duplicateNode = useCallback(() => {
26
- duplicateNodeById(id, getNodeName(label));
27
- }, [duplicateNodeById, id, getNodeName, label]);
28
 
29
  const items: MenuProps['items'] = [
30
  {
31
  key: '2',
32
- onClick: duplicateNode,
33
  label: (
34
  <Flex justify={'space-between'}>
35
  {t('common.copy')}
 
3
  import { Flex, MenuProps } from 'antd';
4
  import { useCallback } from 'react';
5
  import { useTranslation } from 'react-i18next';
6
+ import { useDuplicateNode } from '../../hooks';
7
  import useGraphStore from '../../store';
8
 
9
  interface IProps {
 
15
  const NodeDropdown = ({ id, iconFontColor, label }: IProps) => {
16
  const { t } = useTranslation();
17
  const deleteNodeById = useGraphStore((store) => store.deleteNodeById);
 
 
18
 
19
  const deleteNode = useCallback(() => {
20
  deleteNodeById(id);
21
  }, [id, deleteNodeById]);
22
 
23
+ const duplicateNode = useDuplicateNode();
 
 
24
 
25
  const items: MenuProps['items'] = [
26
  {
27
  key: '2',
28
+ onClick: () => duplicateNode(id, label),
29
  label: (
30
  <Flex justify={'space-between'}>
31
  {t('common.copy')}
web/src/pages/flow/hooks.ts CHANGED
@@ -4,7 +4,6 @@ import { IGraph } from '@/interfaces/database/flow';
4
  import { useIsFetching } from '@tanstack/react-query';
5
  import React, {
6
  ChangeEvent,
7
- KeyboardEventHandler,
8
  useCallback,
9
  useEffect,
10
  useMemo,
@@ -20,7 +19,6 @@ import {
20
  import { useFetchModelId, useSendMessageWithSse } from '@/hooks/logic-hooks';
21
  import { Variable } from '@/interfaces/database/chat';
22
  import api from '@/utils/api';
23
- import { useDebounceEffect } from 'ahooks';
24
  import { FormInstance, message } from 'antd';
25
  import { humanId } from 'human-id';
26
  import { lowerFirst } from 'lodash';
@@ -253,20 +251,6 @@ export const useShowDrawer = () => {
253
  };
254
  };
255
 
256
- export const useHandleKeyUp = () => {
257
- const deleteEdge = useGraphStore((state) => state.deleteEdge);
258
- const handleKeyUp: KeyboardEventHandler = useCallback(
259
- (e) => {
260
- if (e.code === 'Delete') {
261
- deleteEdge();
262
- }
263
- },
264
- [deleteEdge],
265
- );
266
-
267
- return { handleKeyUp };
268
- };
269
-
270
  export const useSaveGraph = () => {
271
  const { data } = useFetchFlow();
272
  const { setFlow } = useSetFlow();
@@ -284,20 +268,6 @@ export const useSaveGraph = () => {
284
  return { saveGraph };
285
  };
286
 
287
- export const useWatchGraphChange = () => {
288
- const nodes = useGraphStore((state) => state.nodes);
289
- const edges = useGraphStore((state) => state.edges);
290
- useDebounceEffect(
291
- () => {
292
- // console.info('useDebounceEffect');
293
- },
294
- [nodes, edges],
295
- {
296
- wait: 1000,
297
- },
298
- );
299
- };
300
-
301
  export const useHandleFormValuesChange = (id?: string) => {
302
  const updateNodeForm = useGraphStore((state) => state.updateNodeForm);
303
  const handleValuesChange = useCallback(
@@ -348,8 +318,6 @@ export const useFetchDataOnMount = () => {
348
  setGraphInfo(data?.dsl?.graph ?? ({} as IGraph));
349
  }, [setGraphInfo, data]);
350
 
351
- useWatchGraphChange();
352
-
353
  useEffect(() => {
354
  refetch();
355
  }, [refetch]);
@@ -640,3 +608,63 @@ export const useGetComponentLabelByValue = (nodeId: string) => {
640
  );
641
  return getLabel;
642
  };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
  import { useIsFetching } from '@tanstack/react-query';
5
  import React, {
6
  ChangeEvent,
 
7
  useCallback,
8
  useEffect,
9
  useMemo,
 
19
  import { useFetchModelId, useSendMessageWithSse } from '@/hooks/logic-hooks';
20
  import { Variable } from '@/interfaces/database/chat';
21
  import api from '@/utils/api';
 
22
  import { FormInstance, message } from 'antd';
23
  import { humanId } from 'human-id';
24
  import { lowerFirst } from 'lodash';
 
251
  };
252
  };
253
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
254
  export const useSaveGraph = () => {
255
  const { data } = useFetchFlow();
256
  const { setFlow } = useSetFlow();
 
268
  return { saveGraph };
269
  };
270
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
271
  export const useHandleFormValuesChange = (id?: string) => {
272
  const updateNodeForm = useGraphStore((state) => state.updateNodeForm);
273
  const handleValuesChange = useCallback(
 
318
  setGraphInfo(data?.dsl?.graph ?? ({} as IGraph));
319
  }, [setGraphInfo, data]);
320
 
 
 
321
  useEffect(() => {
322
  refetch();
323
  }, [refetch]);
 
608
  );
609
  return getLabel;
610
  };
611
+
612
+ export const useDuplicateNode = () => {
613
+ const duplicateNodeById = useGraphStore((store) => store.duplicateNode);
614
+ const getNodeName = useGetNodeName();
615
+
616
+ const duplicateNode = useCallback(
617
+ (id: string, label: string) => {
618
+ duplicateNodeById(id, getNodeName(label));
619
+ },
620
+ [duplicateNodeById, getNodeName],
621
+ );
622
+
623
+ return duplicateNode;
624
+ };
625
+
626
+ export const useCopyPaste = () => {
627
+ const nodes = useGraphStore((state) => state.nodes);
628
+ const duplicateNode = useDuplicateNode();
629
+
630
+ const onCopyCapture = useCallback(
631
+ (event: ClipboardEvent) => {
632
+ event.preventDefault();
633
+ const nodesStr = JSON.stringify(
634
+ nodes.filter((n) => n.selected && n.data.label !== Operator.Begin),
635
+ );
636
+
637
+ event.clipboardData?.setData('agent:nodes', nodesStr);
638
+ },
639
+ [nodes],
640
+ );
641
+
642
+ const onPasteCapture = useCallback(
643
+ (event: ClipboardEvent) => {
644
+ event.preventDefault();
645
+ const nodes = JSON.parse(
646
+ event.clipboardData?.getData('agent:nodes') || '[]',
647
+ ) as Node[] | undefined;
648
+ if (nodes) {
649
+ nodes.forEach((n) => {
650
+ duplicateNode(n.id, n.data.label);
651
+ });
652
+ }
653
+ },
654
+ [duplicateNode],
655
+ );
656
+
657
+ useEffect(() => {
658
+ window.addEventListener('copy', onCopyCapture);
659
+ return () => {
660
+ window.removeEventListener('copy', onCopyCapture);
661
+ };
662
+ }, [onCopyCapture]);
663
+
664
+ useEffect(() => {
665
+ window.addEventListener('paste', onPasteCapture);
666
+ return () => {
667
+ window.removeEventListener('paste', onPasteCapture);
668
+ };
669
+ }, [onPasteCapture]);
670
+ };
web/src/pages/flow/index.tsx CHANGED
@@ -5,7 +5,7 @@ import { ReactFlowProvider } from 'reactflow';
5
  import FlowCanvas from './canvas';
6
  import Sider from './flow-sider';
7
  import FlowHeader from './header';
8
- import { useFetchDataOnMount } from './hooks';
9
 
10
  const { Content } = Layout;
11
 
@@ -18,6 +18,7 @@ function RagFlow() {
18
  } = useSetModalState();
19
 
20
  useFetchDataOnMount();
 
21
 
22
  return (
23
  <Layout>
 
5
  import FlowCanvas from './canvas';
6
  import Sider from './flow-sider';
7
  import FlowHeader from './header';
8
+ import { useCopyPaste, useFetchDataOnMount } from './hooks';
9
 
10
  const { Content } = Layout;
11
 
 
18
  } = useSetModalState();
19
 
20
  useFetchDataOnMount();
21
+ useCopyPaste();
22
 
23
  return (
24
  <Layout>
web/src/pages/flow/store.ts CHANGED
@@ -236,8 +236,8 @@ const useGraphStore = create<RFState>()(
236
  const { getNode, addNode, generateNodeName } = get();
237
  const node = getNode(id);
238
  const position = {
239
- x: (node?.position?.x || 0) + 30,
240
- y: (node?.position?.y || 0) + 20,
241
  };
242
 
243
  addNode({
 
236
  const { getNode, addNode, generateNodeName } = get();
237
  const node = getNode(id);
238
  const position = {
239
+ x: (node?.position?.x || 0) + 50,
240
+ y: (node?.position?.y || 0) + 50,
241
  };
242
 
243
  addNode({