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({
|