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 {
|
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 =
|
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) +
|
240 |
-
y: (node?.position?.y || 0) +
|
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({
|