From c490e774b10b96e94c58d62cdeb94bf9f20ed98b Mon Sep 17 00:00:00 2001 From: "G.Wood-Sun" Date: Tue, 17 Jun 2025 15:42:06 +0800 Subject: [PATCH] feat: copy cross page --- web/app/components/workflow/hooks/index.ts | 2 + .../workflow/hooks/use-nodes-interactions.ts | 2 + .../workflow/hooks/use-panel-interactions.ts | 1 + .../hooks/use-selection-graph-menu.ts | 36 ++++++++++ .../workflow/hooks/use-selection-graph.ts | 25 +++++++ .../workflow/hooks/use-selection-paste.ts | 28 ++++++++ web/app/components/workflow/index.tsx | 24 +++++++ .../components/workflow/panel-contextmenu.tsx | 2 + .../workflow/select-panel-contextmenu.tsx | 68 +++++++++++++++++++ .../workflow/store/workflow/index.ts | 5 +- .../store/workflow/select-node-panel.ts | 25 +++++++ 11 files changed, 217 insertions(+), 1 deletion(-) create mode 100644 web/app/components/workflow/hooks/use-selection-graph-menu.ts create mode 100644 web/app/components/workflow/hooks/use-selection-graph.ts create mode 100644 web/app/components/workflow/hooks/use-selection-paste.ts create mode 100644 web/app/components/workflow/select-panel-contextmenu.tsx create mode 100644 web/app/components/workflow/store/workflow/select-node-panel.ts diff --git a/web/app/components/workflow/hooks/index.ts b/web/app/components/workflow/hooks/index.ts index fda0f50aa6..342689ed07 100644 --- a/web/app/components/workflow/hooks/index.ts +++ b/web/app/components/workflow/hooks/index.ts @@ -17,3 +17,5 @@ export * from './use-workflow-interactions' export * from './use-workflow-mode' export * from './use-format-time-from-now' export * from './use-workflow-refresh-draft' +export * from './use-selection-graph-menu' +export * from './use-selection-paste' diff --git a/web/app/components/workflow/hooks/use-nodes-interactions.ts b/web/app/components/workflow/hooks/use-nodes-interactions.ts index 94b10c9929..32d033c609 100644 --- a/web/app/components/workflow/hooks/use-nodes-interactions.ts +++ b/web/app/components/workflow/hooks/use-nodes-interactions.ts @@ -1356,6 +1356,8 @@ export const useNodesInteractions = () => { } }) + console.log(nodesToPaste, edgesToPaste) + setNodes([...nodes, ...nodesToPaste]) setEdges([...edges, ...edgesToPaste]) saveStateToHistory(WorkflowHistoryEvent.NodePaste) diff --git a/web/app/components/workflow/hooks/use-panel-interactions.ts b/web/app/components/workflow/hooks/use-panel-interactions.ts index 1f02ac7c74..2d12f846c7 100644 --- a/web/app/components/workflow/hooks/use-panel-interactions.ts +++ b/web/app/components/workflow/hooks/use-panel-interactions.ts @@ -26,6 +26,7 @@ export const usePanelInteractions = () => { const handleNodeContextmenuCancel = useCallback(() => { workflowStore.setState({ nodeMenu: undefined, + selectPanelMenu: undefined, }) }, [workflowStore]) diff --git a/web/app/components/workflow/hooks/use-selection-graph-menu.ts b/web/app/components/workflow/hooks/use-selection-graph-menu.ts new file mode 100644 index 0000000000..42bd1e1ee3 --- /dev/null +++ b/web/app/components/workflow/hooks/use-selection-graph-menu.ts @@ -0,0 +1,36 @@ +import type { MouseEvent } from 'react' +import { useCallback } from 'react' +import { useWorkflowStore } from '../store' + +export const useSelectionGraphMenu = () => { + const workflowStore = useWorkflowStore() + + const { setSelectPanelMenu, setNodeMenu, setPanelMenu } = workflowStore.getState() + + const handleSelectPanelContextMenu = useCallback((e: MouseEvent) => { + e.preventDefault() + console.log('handleSelectPanelContextMenu', e.clientX, e.clientY) + const container = document.querySelector('#workflow-container') + const { x, y } = container!.getBoundingClientRect() + + setSelectPanelMenu({ + top: e.clientY - y, + left: e.clientX - x, + }) + }, [setSelectPanelMenu]) + + const handleSelectPanelContextmenuCancel = useCallback(() => { + setSelectPanelMenu(undefined) + }, [setSelectPanelMenu]) + + const handleOtherContextmenuCancel = useCallback(() => { + setNodeMenu(undefined) + setPanelMenu(undefined) + }, [setNodeMenu, setPanelMenu]) + + return { + handleSelectPanelContextMenu, + handleSelectPanelContextmenuCancel, + handleOtherContextmenuCancel, + } +} diff --git a/web/app/components/workflow/hooks/use-selection-graph.ts b/web/app/components/workflow/hooks/use-selection-graph.ts new file mode 100644 index 0000000000..da5113f5bf --- /dev/null +++ b/web/app/components/workflow/hooks/use-selection-graph.ts @@ -0,0 +1,25 @@ +import { + useCallback, +} from 'react' +import type { + OnSelectionChangeParams, +} from 'reactflow' +import { useOnSelectionChange } from 'reactflow' +import { useWorkflowStore } from '../store' + +export const useSelectionGraph = () => { + const workflowStore = useWorkflowStore() + const { setSelectGraph } = workflowStore.getState() + + const onChange = useCallback((params: OnSelectionChangeParams) => { + const { nodes, edges } = params + setSelectGraph({ + nodes, + edges, + }) + }, []) + + useOnSelectionChange({ + onChange, + }) +} diff --git a/web/app/components/workflow/hooks/use-selection-paste.ts b/web/app/components/workflow/hooks/use-selection-paste.ts new file mode 100644 index 0000000000..0130e94baf --- /dev/null +++ b/web/app/components/workflow/hooks/use-selection-paste.ts @@ -0,0 +1,28 @@ +import type { RefObject } from 'react' +import { useEffect } from'react' +import { + useStore, +} from 'reactflow' + +type UseSelectionPasteProps = { + workflowContainerRef: RefObject +} + +export const useSelectionPaste = ({ workflowContainerRef }: UseSelectionPasteProps) => { + const domNode = useStore(s => s.domNode) + const handlePaste = (e: ClipboardEvent) => { + e.preventDefault() + console.log(e.clipboardData?.getData('text')) + } + + useEffect(() => { + if (domNode) { + console.log('workflowContainerRef.current', domNode) + domNode.addEventListener('paste', handlePaste) + } + return () => { + if (domNode) + domNode.removeEventListener('paste', handlePaste) + } + }, [domNode]) +} diff --git a/web/app/components/workflow/index.tsx b/web/app/components/workflow/index.tsx index 549117faf7..2e7df34678 100644 --- a/web/app/components/workflow/index.tsx +++ b/web/app/components/workflow/index.tsx @@ -41,6 +41,7 @@ import { useNodesSyncDraft, usePanelInteractions, useSelectionInteractions, + useSelectionPaste, useShortcuts, useWorkflow, useWorkflowReadOnly, @@ -80,6 +81,9 @@ import Confirm from '@/app/components/base/confirm' import DatasetsDetailProvider from './datasets-detail-store/provider' import { HooksStoreContextProvider } from './hooks-store' import type { Shape as HooksStoreShape } from './hooks-store' +import { useSelectionGraph } from './hooks/use-selection-graph' +import { useSelectionGraphMenu } from './hooks/use-selection-graph-menu' +import SelectPanelContextmenu from './select-panel-contextmenu' const nodeTypes = { [CUSTOM_NODE]: CustomNode, @@ -115,6 +119,14 @@ export const Workflow: FC = memo(({ const nodeAnimation = useStore(s => s.nodeAnimation) const showConfirm = useStore(s => s.showConfirm) + // add selection hook + useSelectionGraph() + // add paste hook + useSelectionPaste({ + workflowContainerRef, + }) + const { handleSelectPanelContextMenu } = useSelectionGraphMenu() + const { setShowConfirm, setControlPromptEditorRerenderKey, @@ -270,6 +282,7 @@ export const Workflow: FC = memo(({ + { !!showConfirm && ( @@ -307,6 +320,7 @@ export const Workflow: FC = memo(({ onSelectionDrag={handleSelectionDrag} onPaneContextMenu={handlePaneContextMenu} connectionLineComponent={CustomConnectionLine} + onSelectionContextMenu={handleSelectPanelContextMenu} // TODO: For LOOP node, how to distinguish between ITERATION and LOOP here? Maybe both are the same? connectionLineContainerStyle={{ zIndex: ITERATION_CHILDREN_Z_INDEX }} defaultViewport={viewport} @@ -345,6 +359,16 @@ export const WorkflowWithInnerContext = memo(({ hooksStore, ...restProps }: WorkflowWithInnerContextProps) => { + const handlePaste = (e: ClipboardEvent) => { + console.log(e, 123) + } + useEffect(() => { + document.body.addEventListener('paste', handlePaste, true) + return () => { + document.body.removeEventListener('paste', handlePaste, true) + } + }, []) + return ( diff --git a/web/app/components/workflow/panel-contextmenu.tsx b/web/app/components/workflow/panel-contextmenu.tsx index 0a09452c67..9154b7df06 100644 --- a/web/app/components/workflow/panel-contextmenu.tsx +++ b/web/app/components/workflow/panel-contextmenu.tsx @@ -30,6 +30,8 @@ const PanelContextmenu = () => { const { handleAddNote } = useOperator() const { exportCheck } = useDSL() + console.log(clipboardElements, 'clipboardElements') + useEffect(() => { if (panelMenu) handleNodeContextmenuCancel() diff --git a/web/app/components/workflow/select-panel-contextmenu.tsx b/web/app/components/workflow/select-panel-contextmenu.tsx new file mode 100644 index 0000000000..9e947dc1e0 --- /dev/null +++ b/web/app/components/workflow/select-panel-contextmenu.tsx @@ -0,0 +1,68 @@ +import { + memo, + useEffect, + useRef, +} from 'react' +import { useTranslation } from 'react-i18next' +import { useClickAway } from 'ahooks' +import ShortcutsName from './shortcuts-name' +import { useStore } from './store' +import { + useNodesInteractions, + useSelectionGraphMenu, + useWorkflowStartRun, +} from './hooks' +import { useStore as useAppStore } from '@/app/components/app/store' + +const PanelContextmenu = () => { + const { t } = useTranslation() + const ref = useRef(null) + const selectPanelMenu = useStore(s => s.selectPanelMenu) + const selectGraph = useStore(s => s.selectGraph) + const clipboardElements = useStore(s => s.clipboardElements) + const { handleNodesPaste } = useNodesInteractions() + const { handleSelectPanelContextmenuCancel, handleOtherContextmenuCancel } = useSelectionGraphMenu() + const { handleStartWorkflowRun } = useWorkflowStartRun() + + const appDetail = useAppStore(state => state.appDetail) + + console.log(appDetail?.mode, selectGraph) + + useEffect(() => { + if (selectPanelMenu) + handleOtherContextmenuCancel() + }, [selectPanelMenu, handleOtherContextmenuCancel]) + + useClickAway(() => { + handleSelectPanelContextmenuCancel() + }, ref) + + if (!selectPanelMenu) + return null + + return ( +
+
+
{ + handleStartWorkflowRun() + handleSelectPanelContextmenuCancel() + }} + > + {t('workflow.common.run')} + +
+
+
+ ) +} + +export default memo(PanelContextmenu) diff --git a/web/app/components/workflow/store/workflow/index.ts b/web/app/components/workflow/store/workflow/index.ts index 0e2f5eb0f7..8fd617d93f 100644 --- a/web/app/components/workflow/store/workflow/index.ts +++ b/web/app/components/workflow/store/workflow/index.ts @@ -30,6 +30,7 @@ import type { WorkflowSliceShape } from './workflow-slice' import { createWorkflowSlice } from './workflow-slice' import { WorkflowContext } from '@/app/components/workflow/context' import type { WorkflowSliceShape as WorkflowAppSliceShape } from '@/app/components/workflow-app/store/workflow/workflow-slice' +import { type SelectPanelSliceShape, createSelectPanelSlice } from './select-node-panel' export type Shape = ChatVariableSliceShape & @@ -43,7 +44,8 @@ export type Shape = VersionSliceShape & WorkflowDraftSliceShape & WorkflowSliceShape & - WorkflowAppSliceShape + WorkflowAppSliceShape & + SelectPanelSliceShape type CreateWorkflowStoreParams = { injectWorkflowStoreSliceFn?: StateCreator @@ -65,6 +67,7 @@ export const createWorkflowStore = (params: CreateWorkflowStoreParams) => { ...createWorkflowDraftSlice(...args), ...createWorkflowSlice(...args), ...(injectWorkflowStoreSliceFn?.(...args) || {} as WorkflowAppSliceShape), + ...createSelectPanelSlice(...args), })) } diff --git a/web/app/components/workflow/store/workflow/select-node-panel.ts b/web/app/components/workflow/store/workflow/select-node-panel.ts new file mode 100644 index 0000000000..23bb5ff203 --- /dev/null +++ b/web/app/components/workflow/store/workflow/select-node-panel.ts @@ -0,0 +1,25 @@ +import type { Edge, Node } from 'reactflow' +import type { StateCreator } from 'zustand' + +export type SelectPanelSliceShape = { + selectGraph: { + nodes: Node[], + edges: Edge[], + }, + selectPanelMenu?: { + top: number + left: number + }, + setSelectPanelMenu: (panelMenu: SelectPanelSliceShape['selectPanelMenu']) => void, + setSelectGraph: (selectGraph: SelectPanelSliceShape['selectGraph']) => void, +} + +export const createSelectPanelSlice: StateCreator = set => ({ + selectPanelMenu: undefined, + setSelectPanelMenu: selectPanelMenu => set(() => ({ selectPanelMenu })), + selectGraph: { + nodes: [], + edges: [], + }, + setSelectGraph: selectGraph => set(() => ({ selectGraph })), +})