| @@ -35,7 +35,9 @@ const RunMode = memo(() => { | |||
| 'hover:bg-state-accent-hover cursor-pointer', | |||
| isRunning && 'bg-state-accent-hover !cursor-not-allowed', | |||
| )} | |||
| onClick={() => handleWorkflowStartRunInWorkflow()} | |||
| onClick={() => { | |||
| handleWorkflowStartRunInWorkflow() | |||
| }} | |||
| > | |||
| { | |||
| isRunning | |||
| @@ -17,7 +17,7 @@ import { | |||
| useWorkflowInteractions, | |||
| useWorkflowRun, | |||
| } from '../hooks' | |||
| import { WorkflowRunningStatus } from '../types' | |||
| import { ControlMode, WorkflowRunningStatus } from '../types' | |||
| import cn from '@/utils/classnames' | |||
| import { | |||
| PortalToFollowElem, | |||
| @@ -58,6 +58,7 @@ const ViewHistory = ({ | |||
| handleCancelDebugAndPreviewPanel, | |||
| } = useWorkflowInteractions() | |||
| const workflowStore = useWorkflowStore() | |||
| const setControlMode = useStore(s => s.setControlMode) | |||
| const { appDetail, setCurrentLogItem, setShowMessageLogModal } = useAppStore(useShallow(state => ({ | |||
| appDetail: state.appDetail, | |||
| setCurrentLogItem: state.setCurrentLogItem, | |||
| @@ -173,6 +174,7 @@ const ViewHistory = ({ | |||
| setOpen(false) | |||
| handleNodesCancelSelected() | |||
| handleCancelDebugAndPreviewPanel() | |||
| setControlMode(ControlMode.Hand) | |||
| }} | |||
| > | |||
| { | |||
| @@ -7,11 +7,12 @@ export * from './use-workflow' | |||
| export * from './use-workflow-run' | |||
| export * from './use-workflow-template' | |||
| export * from './use-checklist' | |||
| export * from './use-workflow-mode' | |||
| export * from './use-workflow-interactions' | |||
| export * from './use-selection-interactions' | |||
| export * from './use-panel-interactions' | |||
| export * from './use-workflow-start-run' | |||
| export * from './use-nodes-layout' | |||
| export * from './use-workflow-history' | |||
| export * from './use-workflow-variables' | |||
| export * from './use-shortcuts' | |||
| export * from './use-workflow-interactions' | |||
| export * from './use-workflow-mode' | |||
| @@ -48,6 +48,7 @@ import { useHelpline } from './use-helpline' | |||
| import { | |||
| useNodesReadOnly, | |||
| useWorkflow, | |||
| useWorkflowReadOnly, | |||
| } from './use-workflow' | |||
| import { WorkflowHistoryEvent, useWorkflowHistory } from './use-workflow-history' | |||
| @@ -62,6 +63,7 @@ export const useNodesInteractions = () => { | |||
| getAfterNodesInSameBranch, | |||
| } = useWorkflow() | |||
| const { getNodesReadOnly } = useNodesReadOnly() | |||
| const { getWorkflowReadOnly } = useWorkflowReadOnly() | |||
| const { handleSetHelpline } = useHelpline() | |||
| const { | |||
| handleNodeIterationChildDrag, | |||
| @@ -1029,14 +1031,7 @@ export const useNodesInteractions = () => { | |||
| if (getNodesReadOnly()) | |||
| return | |||
| const { | |||
| setClipboardElements, | |||
| shortcutsDisabled, | |||
| showFeaturesPanel, | |||
| } = workflowStore.getState() | |||
| if (shortcutsDisabled || showFeaturesPanel) | |||
| return | |||
| const { setClipboardElements } = workflowStore.getState() | |||
| const { | |||
| getNodes, | |||
| @@ -1062,14 +1057,9 @@ export const useNodesInteractions = () => { | |||
| const { | |||
| clipboardElements, | |||
| shortcutsDisabled, | |||
| showFeaturesPanel, | |||
| mousePosition, | |||
| } = workflowStore.getState() | |||
| if (shortcutsDisabled || showFeaturesPanel) | |||
| return | |||
| const { | |||
| getNodes, | |||
| setNodes, | |||
| @@ -1107,6 +1097,11 @@ export const useNodesInteractions = () => { | |||
| }) | |||
| newNode.id = newNode.id + index | |||
| // If only the iteration start node is copied, remove the isIterationStart flag | |||
| // This new node is movable and can be placed anywhere | |||
| if (clipboardElements.length === 1 && newNode.data.isIterationStart) | |||
| newNode.data.isIterationStart = false | |||
| let newChildren: Node[] = [] | |||
| if (nodeToPaste.data.type === BlockEnum.Iteration) { | |||
| newNode.data._children = []; | |||
| @@ -1145,14 +1140,6 @@ export const useNodesInteractions = () => { | |||
| if (getNodesReadOnly()) | |||
| return | |||
| const { | |||
| shortcutsDisabled, | |||
| showFeaturesPanel, | |||
| } = workflowStore.getState() | |||
| if (shortcutsDisabled || showFeaturesPanel) | |||
| return | |||
| const { | |||
| getNodes, | |||
| edges, | |||
| @@ -1175,7 +1162,7 @@ export const useNodesInteractions = () => { | |||
| if (selectedNode) | |||
| handleNodeDelete(selectedNode.id) | |||
| }, [store, workflowStore, getNodesReadOnly, handleNodeDelete]) | |||
| }, [store, getNodesReadOnly, handleNodeDelete]) | |||
| const handleNodeResize = useCallback((nodeId: string, params: ResizeParamsWithDirection) => { | |||
| if (getNodesReadOnly()) | |||
| @@ -1234,14 +1221,7 @@ export const useNodesInteractions = () => { | |||
| }, [getNodesReadOnly, store, handleSyncWorkflowDraft, saveStateToHistory]) | |||
| const handleHistoryBack = useCallback(() => { | |||
| if (getNodesReadOnly()) | |||
| return | |||
| const { | |||
| shortcutsDisabled, | |||
| } = workflowStore.getState() | |||
| if (shortcutsDisabled) | |||
| if (getNodesReadOnly() || getWorkflowReadOnly()) | |||
| return | |||
| const { setEdges, setNodes } = store.getState() | |||
| @@ -1253,17 +1233,10 @@ export const useNodesInteractions = () => { | |||
| setEdges(edges) | |||
| setNodes(nodes) | |||
| }, [store, undo, workflowHistoryStore, workflowStore, getNodesReadOnly]) | |||
| }, [store, undo, workflowHistoryStore, getNodesReadOnly, getWorkflowReadOnly]) | |||
| const handleHistoryForward = useCallback(() => { | |||
| if (getNodesReadOnly()) | |||
| return | |||
| const { | |||
| shortcutsDisabled, | |||
| } = workflowStore.getState() | |||
| if (shortcutsDisabled) | |||
| if (getNodesReadOnly() || getWorkflowReadOnly()) | |||
| return | |||
| const { setEdges, setNodes } = store.getState() | |||
| @@ -1275,7 +1248,7 @@ export const useNodesInteractions = () => { | |||
| setEdges(edges) | |||
| setNodes(nodes) | |||
| }, [redo, store, workflowHistoryStore, workflowStore, getNodesReadOnly]) | |||
| }, [redo, store, workflowHistoryStore, getNodesReadOnly, getWorkflowReadOnly]) | |||
| return { | |||
| handleNodeDragStart, | |||
| @@ -8,7 +8,9 @@ import { | |||
| } from '../store' | |||
| import { BlockEnum } from '../types' | |||
| import { useWorkflowUpdate } from '../hooks' | |||
| import { useNodesReadOnly } from './use-workflow' | |||
| import { | |||
| useNodesReadOnly, | |||
| } from './use-workflow' | |||
| import { syncWorkflowDraft } from '@/service/workflow' | |||
| import { useFeaturesStore } from '@/app/components/base/features/hooks' | |||
| import { API_PREFIX } from '@/config' | |||
| @@ -0,0 +1,186 @@ | |||
| import { useReactFlow } from 'reactflow' | |||
| import { useKeyPress } from 'ahooks' | |||
| import { useCallback } from 'react' | |||
| import { | |||
| getKeyboardKeyCodeBySystem, | |||
| isEventTargetInputArea, | |||
| } from '../utils' | |||
| import { useWorkflowHistoryStore } from '../workflow-history-store' | |||
| import { useWorkflowStore } from '../store' | |||
| import { | |||
| useEdgesInteractions, | |||
| useNodesInteractions, | |||
| useNodesSyncDraft, | |||
| useWorkflowMoveMode, | |||
| useWorkflowOrganize, | |||
| useWorkflowStartRun, | |||
| } from '.' | |||
| export const useShortcuts = (): void => { | |||
| const { | |||
| handleNodesCopy, | |||
| handleNodesPaste, | |||
| handleNodesDuplicate, | |||
| handleNodesDelete, | |||
| handleHistoryBack, | |||
| handleHistoryForward, | |||
| } = useNodesInteractions() | |||
| const { handleStartWorkflowRun } = useWorkflowStartRun() | |||
| const { shortcutsEnabled: workflowHistoryShortcutsEnabled } = useWorkflowHistoryStore() | |||
| const { handleSyncWorkflowDraft } = useNodesSyncDraft() | |||
| const { handleEdgeDelete } = useEdgesInteractions() | |||
| const workflowStore = useWorkflowStore() | |||
| const { | |||
| handleModeHand, | |||
| handleModePointer, | |||
| } = useWorkflowMoveMode() | |||
| const { handleLayout } = useWorkflowOrganize() | |||
| const { | |||
| zoomIn, | |||
| zoomOut, | |||
| zoomTo, | |||
| fitView, | |||
| } = useReactFlow() | |||
| const shouldHandleShortcut = useCallback((e: KeyboardEvent) => { | |||
| const { showFeaturesPanel } = workflowStore.getState() | |||
| return !showFeaturesPanel && !isEventTargetInputArea(e.target as HTMLElement) | |||
| }, [workflowStore]) | |||
| useKeyPress(['delete', 'backspace'], (e) => { | |||
| if (shouldHandleShortcut(e)) { | |||
| e.preventDefault() | |||
| handleNodesDelete() | |||
| handleEdgeDelete() | |||
| } | |||
| }) | |||
| useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.c`, (e) => { | |||
| if (shouldHandleShortcut(e)) { | |||
| e.preventDefault() | |||
| handleNodesCopy() | |||
| } | |||
| }, { exactMatch: true, useCapture: true }) | |||
| useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.v`, (e) => { | |||
| if (shouldHandleShortcut(e)) { | |||
| e.preventDefault() | |||
| handleNodesPaste() | |||
| } | |||
| }, { exactMatch: true, useCapture: true }) | |||
| useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.d`, (e) => { | |||
| if (shouldHandleShortcut(e)) { | |||
| e.preventDefault() | |||
| handleNodesDuplicate() | |||
| } | |||
| }, { exactMatch: true, useCapture: true }) | |||
| useKeyPress(`${getKeyboardKeyCodeBySystem('alt')}.r`, (e) => { | |||
| if (shouldHandleShortcut(e)) { | |||
| e.preventDefault() | |||
| handleStartWorkflowRun() | |||
| } | |||
| }, { exactMatch: true, useCapture: true }) | |||
| useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.z`, (e) => { | |||
| if (shouldHandleShortcut(e)) { | |||
| e.preventDefault() | |||
| workflowHistoryShortcutsEnabled && handleHistoryBack() | |||
| } | |||
| }, { exactMatch: true, useCapture: true }) | |||
| useKeyPress( | |||
| [`${getKeyboardKeyCodeBySystem('ctrl')}.y`, `${getKeyboardKeyCodeBySystem('ctrl')}.shift.z`], | |||
| (e) => { | |||
| if (shouldHandleShortcut(e)) { | |||
| e.preventDefault() | |||
| workflowHistoryShortcutsEnabled && handleHistoryForward() | |||
| } | |||
| }, | |||
| { exactMatch: true, useCapture: true }, | |||
| ) | |||
| useKeyPress('h', (e) => { | |||
| if (shouldHandleShortcut(e)) { | |||
| e.preventDefault() | |||
| handleModeHand() | |||
| } | |||
| }, { | |||
| exactMatch: true, | |||
| useCapture: true, | |||
| }) | |||
| useKeyPress('v', (e) => { | |||
| if (shouldHandleShortcut(e)) { | |||
| e.preventDefault() | |||
| handleModePointer() | |||
| } | |||
| }, { | |||
| exactMatch: true, | |||
| useCapture: true, | |||
| }) | |||
| useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.o`, (e) => { | |||
| if (shouldHandleShortcut(e)) { | |||
| e.preventDefault() | |||
| handleLayout() | |||
| } | |||
| }, { exactMatch: true, useCapture: true }) | |||
| useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.1`, (e) => { | |||
| if (shouldHandleShortcut(e)) { | |||
| e.preventDefault() | |||
| fitView() | |||
| handleSyncWorkflowDraft() | |||
| } | |||
| }, { | |||
| exactMatch: true, | |||
| useCapture: true, | |||
| }) | |||
| useKeyPress('shift.1', (e) => { | |||
| if (shouldHandleShortcut(e)) { | |||
| e.preventDefault() | |||
| zoomTo(1) | |||
| handleSyncWorkflowDraft() | |||
| } | |||
| }, { | |||
| exactMatch: true, | |||
| useCapture: true, | |||
| }) | |||
| useKeyPress('shift.5', (e) => { | |||
| if (shouldHandleShortcut(e)) { | |||
| e.preventDefault() | |||
| zoomTo(0.5) | |||
| handleSyncWorkflowDraft() | |||
| } | |||
| }, { | |||
| exactMatch: true, | |||
| useCapture: true, | |||
| }) | |||
| useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.dash`, (e) => { | |||
| if (shouldHandleShortcut(e)) { | |||
| e.preventDefault() | |||
| zoomOut() | |||
| handleSyncWorkflowDraft() | |||
| } | |||
| }, { | |||
| exactMatch: true, | |||
| useCapture: true, | |||
| }) | |||
| useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.equalsign`, (e) => { | |||
| if (shouldHandleShortcut(e)) { | |||
| e.preventDefault() | |||
| zoomIn() | |||
| handleSyncWorkflowDraft() | |||
| } | |||
| }, { | |||
| exactMatch: true, | |||
| useCapture: true, | |||
| }) | |||
| } | |||
| @@ -3,17 +3,29 @@ import { | |||
| useState, | |||
| } from 'react' | |||
| import { useTranslation } from 'react-i18next' | |||
| import { useReactFlow } from 'reactflow' | |||
| import { useWorkflowStore } from '../store' | |||
| import { DSL_EXPORT_CHECK, WORKFLOW_DATA_UPDATE } from '../constants' | |||
| import type { WorkflowDataUpdator } from '../types' | |||
| import { useReactFlow, useStoreApi } from 'reactflow' | |||
| import produce from 'immer' | |||
| import { useStore, useWorkflowStore } from '../store' | |||
| import { | |||
| CUSTOM_NODE, DSL_EXPORT_CHECK, | |||
| WORKFLOW_DATA_UPDATE, | |||
| } from '../constants' | |||
| import type { Node, WorkflowDataUpdator } from '../types' | |||
| import { ControlMode } from '../types' | |||
| import { | |||
| getLayoutByDagre, | |||
| initialEdges, | |||
| initialNodes, | |||
| } from '../utils' | |||
| import { | |||
| useNodesReadOnly, | |||
| useSelectionInteractions, | |||
| useWorkflowReadOnly, | |||
| } from '../hooks' | |||
| import { useEdgesInteractions } from './use-edges-interactions' | |||
| import { useNodesInteractions } from './use-nodes-interactions' | |||
| import { useNodesSyncDraft } from './use-nodes-sync-draft' | |||
| import { WorkflowHistoryEvent, useWorkflowHistory } from './use-workflow-history' | |||
| import { useEventEmitterContextContext } from '@/context/event-emitter' | |||
| import { fetchWorkflowDraft } from '@/service/workflow' | |||
| import { exportAppConfig } from '@/service/apps' | |||
| @@ -39,6 +51,158 @@ export const useWorkflowInteractions = () => { | |||
| } | |||
| } | |||
| export const useWorkflowMoveMode = () => { | |||
| const setControlMode = useStore(s => s.setControlMode) | |||
| const { | |||
| getNodesReadOnly, | |||
| } = useNodesReadOnly() | |||
| const { handleSelectionCancel } = useSelectionInteractions() | |||
| const handleModePointer = useCallback(() => { | |||
| if (getNodesReadOnly()) | |||
| return | |||
| setControlMode(ControlMode.Pointer) | |||
| }, [getNodesReadOnly, setControlMode]) | |||
| const handleModeHand = useCallback(() => { | |||
| if (getNodesReadOnly()) | |||
| return | |||
| setControlMode(ControlMode.Hand) | |||
| handleSelectionCancel() | |||
| }, [getNodesReadOnly, setControlMode, handleSelectionCancel]) | |||
| return { | |||
| handleModePointer, | |||
| handleModeHand, | |||
| } | |||
| } | |||
| export const useWorkflowOrganize = () => { | |||
| const workflowStore = useWorkflowStore() | |||
| const store = useStoreApi() | |||
| const reactflow = useReactFlow() | |||
| const { getNodesReadOnly } = useNodesReadOnly() | |||
| const { saveStateToHistory } = useWorkflowHistory() | |||
| const { handleSyncWorkflowDraft } = useNodesSyncDraft() | |||
| const handleLayout = useCallback(async () => { | |||
| if (getNodesReadOnly()) | |||
| return | |||
| workflowStore.setState({ nodeAnimation: true }) | |||
| const { | |||
| getNodes, | |||
| edges, | |||
| setNodes, | |||
| } = store.getState() | |||
| const { setViewport } = reactflow | |||
| const nodes = getNodes() | |||
| const layout = getLayoutByDagre(nodes, edges) | |||
| const rankMap = {} as Record<string, Node> | |||
| nodes.forEach((node) => { | |||
| if (!node.parentId && node.type === CUSTOM_NODE) { | |||
| const rank = layout.node(node.id).rank! | |||
| if (!rankMap[rank]) { | |||
| rankMap[rank] = node | |||
| } | |||
| else { | |||
| if (rankMap[rank].position.y > node.position.y) | |||
| rankMap[rank] = node | |||
| } | |||
| } | |||
| }) | |||
| const newNodes = produce(nodes, (draft) => { | |||
| draft.forEach((node) => { | |||
| if (!node.parentId && node.type === CUSTOM_NODE) { | |||
| const nodeWithPosition = layout.node(node.id) | |||
| node.position = { | |||
| x: nodeWithPosition.x - node.width! / 2, | |||
| y: nodeWithPosition.y - node.height! / 2 + rankMap[nodeWithPosition.rank!].height! / 2, | |||
| } | |||
| } | |||
| }) | |||
| }) | |||
| setNodes(newNodes) | |||
| const zoom = 0.7 | |||
| setViewport({ | |||
| x: 0, | |||
| y: 0, | |||
| zoom, | |||
| }) | |||
| saveStateToHistory(WorkflowHistoryEvent.LayoutOrganize) | |||
| setTimeout(() => { | |||
| handleSyncWorkflowDraft() | |||
| }) | |||
| }, [getNodesReadOnly, store, reactflow, workflowStore, handleSyncWorkflowDraft, saveStateToHistory]) | |||
| return { | |||
| handleLayout, | |||
| } | |||
| } | |||
| export const useWorkflowZoom = () => { | |||
| const { handleSyncWorkflowDraft } = useNodesSyncDraft() | |||
| const { getWorkflowReadOnly } = useWorkflowReadOnly() | |||
| const { | |||
| zoomIn, | |||
| zoomOut, | |||
| zoomTo, | |||
| fitView, | |||
| } = useReactFlow() | |||
| const handleFitView = useCallback(() => { | |||
| if (getWorkflowReadOnly()) | |||
| return | |||
| fitView() | |||
| handleSyncWorkflowDraft() | |||
| }, [getWorkflowReadOnly, fitView, handleSyncWorkflowDraft]) | |||
| const handleBackToOriginalSize = useCallback(() => { | |||
| if (getWorkflowReadOnly()) | |||
| return | |||
| zoomTo(1) | |||
| handleSyncWorkflowDraft() | |||
| }, [getWorkflowReadOnly, zoomTo, handleSyncWorkflowDraft]) | |||
| const handleSizeToHalf = useCallback(() => { | |||
| if (getWorkflowReadOnly()) | |||
| return | |||
| zoomTo(0.5) | |||
| handleSyncWorkflowDraft() | |||
| }, [getWorkflowReadOnly, zoomTo, handleSyncWorkflowDraft]) | |||
| const handleZoomOut = useCallback(() => { | |||
| if (getWorkflowReadOnly()) | |||
| return | |||
| zoomOut() | |||
| handleSyncWorkflowDraft() | |||
| }, [getWorkflowReadOnly, zoomOut, handleSyncWorkflowDraft]) | |||
| const handleZoomIn = useCallback(() => { | |||
| if (getWorkflowReadOnly()) | |||
| return | |||
| zoomIn() | |||
| handleSyncWorkflowDraft() | |||
| }, [getWorkflowReadOnly, zoomIn, handleSyncWorkflowDraft]) | |||
| return { | |||
| handleFitView, | |||
| handleBackToOriginalSize, | |||
| handleSizeToHalf, | |||
| handleZoomOut, | |||
| handleZoomIn, | |||
| } | |||
| } | |||
| export const useWorkflowUpdate = () => { | |||
| const reactflow = useReactFlow() | |||
| const workflowStore = useWorkflowStore() | |||
| @@ -7,19 +7,14 @@ import { | |||
| import dayjs from 'dayjs' | |||
| import { uniqBy } from 'lodash-es' | |||
| import { useContext } from 'use-context-selector' | |||
| import produce from 'immer' | |||
| import { | |||
| getIncomers, | |||
| getOutgoers, | |||
| useReactFlow, | |||
| useStoreApi, | |||
| } from 'reactflow' | |||
| import type { | |||
| Connection, | |||
| } from 'reactflow' | |||
| import { | |||
| getLayoutByDagre, | |||
| } from '../utils' | |||
| import type { | |||
| Edge, | |||
| Node, | |||
| @@ -34,15 +29,12 @@ import { | |||
| useWorkflowStore, | |||
| } from '../store' | |||
| import { | |||
| CUSTOM_NODE, | |||
| SUPPORT_OUTPUT_VARS_NODE, | |||
| } from '../constants' | |||
| import { CUSTOM_NOTE_NODE } from '../note-node/constants' | |||
| import { findUsedVarNodes, getNodeOutputVars, updateNodeVars } from '../nodes/_base/components/variable/utils' | |||
| import { useNodesExtraData } from './use-nodes-data' | |||
| import { useWorkflowTemplate } from './use-workflow-template' | |||
| import { useNodesSyncDraft } from './use-nodes-sync-draft' | |||
| import { WorkflowHistoryEvent, useWorkflowHistory } from './use-workflow-history' | |||
| import { useStore as useAppStore } from '@/app/components/app/store' | |||
| import { | |||
| fetchNodesDefaultConfigs, | |||
| @@ -68,68 +60,13 @@ export const useIsChatMode = () => { | |||
| export const useWorkflow = () => { | |||
| const { locale } = useContext(I18n) | |||
| const store = useStoreApi() | |||
| const reactflow = useReactFlow() | |||
| const workflowStore = useWorkflowStore() | |||
| const nodesExtraData = useNodesExtraData() | |||
| const { handleSyncWorkflowDraft } = useNodesSyncDraft() | |||
| const { saveStateToHistory } = useWorkflowHistory() | |||
| const setPanelWidth = useCallback((width: number) => { | |||
| localStorage.setItem('workflow-node-panel-width', `${width}`) | |||
| workflowStore.setState({ panelWidth: width }) | |||
| }, [workflowStore]) | |||
| const handleLayout = useCallback(async () => { | |||
| workflowStore.setState({ nodeAnimation: true }) | |||
| const { | |||
| getNodes, | |||
| edges, | |||
| setNodes, | |||
| } = store.getState() | |||
| const { setViewport } = reactflow | |||
| const nodes = getNodes() | |||
| const layout = getLayoutByDagre(nodes, edges) | |||
| const rankMap = {} as Record<string, Node> | |||
| nodes.forEach((node) => { | |||
| if (!node.parentId && node.type === CUSTOM_NODE) { | |||
| const rank = layout.node(node.id).rank! | |||
| if (!rankMap[rank]) { | |||
| rankMap[rank] = node | |||
| } | |||
| else { | |||
| if (rankMap[rank].position.y > node.position.y) | |||
| rankMap[rank] = node | |||
| } | |||
| } | |||
| }) | |||
| const newNodes = produce(nodes, (draft) => { | |||
| draft.forEach((node) => { | |||
| if (!node.parentId && node.type === CUSTOM_NODE) { | |||
| const nodeWithPosition = layout.node(node.id) | |||
| node.position = { | |||
| x: nodeWithPosition.x - node.width! / 2, | |||
| y: nodeWithPosition.y - node.height! / 2 + rankMap[nodeWithPosition.rank!].height! / 2, | |||
| } | |||
| } | |||
| }) | |||
| }) | |||
| setNodes(newNodes) | |||
| const zoom = 0.7 | |||
| setViewport({ | |||
| x: 0, | |||
| y: 0, | |||
| zoom, | |||
| }) | |||
| saveStateToHistory(WorkflowHistoryEvent.LayoutOrganize) | |||
| setTimeout(() => { | |||
| handleSyncWorkflowDraft() | |||
| }) | |||
| }, [workflowStore, store, reactflow, saveStateToHistory, handleSyncWorkflowDraft]) | |||
| const getTreeLeafNodes = useCallback((nodeId: string) => { | |||
| const { | |||
| getNodes, | |||
| @@ -392,19 +329,8 @@ export const useWorkflow = () => { | |||
| return nodes.find(node => node.id === nodeId) || nodes.find(node => node.data.type === BlockEnum.Start) | |||
| }, [store]) | |||
| const enableShortcuts = useCallback(() => { | |||
| const { setShortcutsDisabled } = workflowStore.getState() | |||
| setShortcutsDisabled(false) | |||
| }, [workflowStore]) | |||
| const disableShortcuts = useCallback(() => { | |||
| const { setShortcutsDisabled } = workflowStore.getState() | |||
| setShortcutsDisabled(true) | |||
| }, [workflowStore]) | |||
| return { | |||
| setPanelWidth, | |||
| handleLayout, | |||
| getTreeLeafNodes, | |||
| getBeforeNodesInSameBranch, | |||
| getBeforeNodesInSameBranchIncludeParent, | |||
| @@ -418,8 +344,6 @@ export const useWorkflow = () => { | |||
| getNode, | |||
| getBeforeNodeById, | |||
| getIterationNodeChildren, | |||
| enableShortcuts, | |||
| disableShortcuts, | |||
| } | |||
| } | |||
| @@ -12,7 +12,6 @@ import { | |||
| import { setAutoFreeze } from 'immer' | |||
| import { | |||
| useEventListener, | |||
| useKeyPress, | |||
| } from 'ahooks' | |||
| import ReactFlow, { | |||
| Background, | |||
| @@ -34,6 +33,9 @@ import type { | |||
| EnvironmentVariable, | |||
| Node, | |||
| } from './types' | |||
| import { | |||
| ControlMode, | |||
| } from './types' | |||
| import { WorkflowContextProvider } from './context' | |||
| import { | |||
| useDSL, | |||
| @@ -43,10 +45,10 @@ import { | |||
| useNodesSyncDraft, | |||
| usePanelInteractions, | |||
| useSelectionInteractions, | |||
| useShortcuts, | |||
| useWorkflow, | |||
| useWorkflowInit, | |||
| useWorkflowReadOnly, | |||
| useWorkflowStartRun, | |||
| useWorkflowUpdate, | |||
| } from './hooks' | |||
| import Header from './header' | |||
| @@ -70,10 +72,8 @@ import { | |||
| useWorkflowStore, | |||
| } from './store' | |||
| import { | |||
| getKeyboardKeyCodeBySystem, | |||
| initialEdges, | |||
| initialNodes, | |||
| isEventTargetInputArea, | |||
| } from './utils' | |||
| import { | |||
| CUSTOM_NODE, | |||
| @@ -81,7 +81,7 @@ import { | |||
| ITERATION_CHILDREN_Z_INDEX, | |||
| WORKFLOW_DATA_UPDATE, | |||
| } from './constants' | |||
| import { WorkflowHistoryProvider, useWorkflowHistoryStore } from './workflow-history-store' | |||
| import { WorkflowHistoryProvider } from './workflow-history-store' | |||
| import Loading from '@/app/components/base/loading' | |||
| import { FeaturesProvider } from '@/app/components/base/features' | |||
| import type { Features as FeaturesData } from '@/app/components/base/features/types' | |||
| @@ -225,17 +225,12 @@ const Workflow: FC<WorkflowProps> = memo(({ | |||
| handleNodeConnectStart, | |||
| handleNodeConnectEnd, | |||
| handleNodeContextMenu, | |||
| handleNodesCopy, | |||
| handleNodesPaste, | |||
| handleNodesDuplicate, | |||
| handleNodesDelete, | |||
| handleHistoryBack, | |||
| handleHistoryForward, | |||
| } = useNodesInteractions() | |||
| const { | |||
| handleEdgeEnter, | |||
| handleEdgeLeave, | |||
| handleEdgeDelete, | |||
| handleEdgesChange, | |||
| } = useEdgesInteractions() | |||
| const { | |||
| @@ -250,7 +245,6 @@ const Workflow: FC<WorkflowProps> = memo(({ | |||
| const { | |||
| isValidConnection, | |||
| } = useWorkflow() | |||
| const { handleStartWorkflowRun } = useWorkflowStartRun() | |||
| const { | |||
| exportCheck, | |||
| handleExportDSL, | |||
| @@ -262,41 +256,7 @@ const Workflow: FC<WorkflowProps> = memo(({ | |||
| }, | |||
| }) | |||
| const { shortcutsEnabled: workflowHistoryShortcutsEnabled } = useWorkflowHistoryStore() | |||
| useKeyPress(['delete', 'backspace'], (e) => { | |||
| if (isEventTargetInputArea(e.target as HTMLElement)) | |||
| return | |||
| handleNodesDelete() | |||
| }) | |||
| useKeyPress(['delete', 'backspace'], handleEdgeDelete) | |||
| useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.c`, (e) => { | |||
| if (isEventTargetInputArea(e.target as HTMLElement)) | |||
| return | |||
| handleNodesCopy() | |||
| }, { exactMatch: true, useCapture: true }) | |||
| useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.v`, (e) => { | |||
| if (isEventTargetInputArea(e.target as HTMLElement)) | |||
| return | |||
| handleNodesPaste() | |||
| }, { exactMatch: true, useCapture: true }) | |||
| useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.d`, handleNodesDuplicate, { exactMatch: true, useCapture: true }) | |||
| useKeyPress(`${getKeyboardKeyCodeBySystem('alt')}.r`, handleStartWorkflowRun, { exactMatch: true, useCapture: true }) | |||
| useKeyPress(`${getKeyboardKeyCodeBySystem('alt')}.r`, handleStartWorkflowRun, { exactMatch: true, useCapture: true }) | |||
| useKeyPress( | |||
| `${getKeyboardKeyCodeBySystem('ctrl')}.z`, | |||
| () => workflowHistoryShortcutsEnabled && handleHistoryBack(), | |||
| { exactMatch: true, useCapture: true }, | |||
| ) | |||
| useKeyPress( | |||
| [`${getKeyboardKeyCodeBySystem('ctrl')}.y`, `${getKeyboardKeyCodeBySystem('ctrl')}.shift.z`], | |||
| () => workflowHistoryShortcutsEnabled && handleHistoryForward(), | |||
| { exactMatch: true, useCapture: true }, | |||
| ) | |||
| useShortcuts() | |||
| const store = useStoreApi() | |||
| if (process.env.NODE_ENV === 'development') { | |||
| @@ -388,14 +348,14 @@ const Workflow: FC<WorkflowProps> = memo(({ | |||
| nodesConnectable={!nodesReadOnly} | |||
| nodesFocusable={!nodesReadOnly} | |||
| edgesFocusable={!nodesReadOnly} | |||
| panOnDrag={controlMode === 'hand' && !workflowReadOnly} | |||
| panOnDrag={controlMode === ControlMode.Hand && !workflowReadOnly} | |||
| zoomOnPinch={!workflowReadOnly} | |||
| zoomOnScroll={!workflowReadOnly} | |||
| zoomOnDoubleClick={!workflowReadOnly} | |||
| isValidConnection={isValidConnection} | |||
| selectionKeyCode={null} | |||
| selectionMode={SelectionMode.Partial} | |||
| selectionOnDrag={controlMode === 'pointer' && !workflowReadOnly} | |||
| selectionOnDrag={controlMode === ControlMode.Pointer && !workflowReadOnly} | |||
| minZoom={0.25} | |||
| > | |||
| <Background | |||
| @@ -1,7 +1,6 @@ | |||
| import type { MouseEvent } from 'react' | |||
| import { | |||
| memo, | |||
| useCallback, | |||
| } from 'react' | |||
| import { useTranslation } from 'react-i18next' | |||
| import { | |||
| @@ -10,13 +9,14 @@ import { | |||
| RiHand, | |||
| RiStickyNoteAddLine, | |||
| } from '@remixicon/react' | |||
| import { useKeyPress } from 'ahooks' | |||
| import { | |||
| useNodesReadOnly, | |||
| useSelectionInteractions, | |||
| useWorkflow, | |||
| useWorkflowMoveMode, | |||
| useWorkflowOrganize, | |||
| } from '../hooks' | |||
| import { getKeyboardKeyCodeBySystem, isEventTargetInputArea } from '../utils' | |||
| import { | |||
| ControlMode, | |||
| } from '../types' | |||
| import { useStore } from '../store' | |||
| import AddBlock from './add-block' | |||
| import TipPopup from './tip-popup' | |||
| @@ -26,62 +26,13 @@ import cn from '@/utils/classnames' | |||
| const Control = () => { | |||
| const { t } = useTranslation() | |||
| const controlMode = useStore(s => s.controlMode) | |||
| const setControlMode = useStore(s => s.setControlMode) | |||
| const { handleLayout } = useWorkflow() | |||
| const { handleModePointer, handleModeHand } = useWorkflowMoveMode() | |||
| const { handleLayout } = useWorkflowOrganize() | |||
| const { handleAddNote } = useOperator() | |||
| const { | |||
| nodesReadOnly, | |||
| getNodesReadOnly, | |||
| } = useNodesReadOnly() | |||
| const { handleSelectionCancel } = useSelectionInteractions() | |||
| const handleModePointer = useCallback(() => { | |||
| if (getNodesReadOnly()) | |||
| return | |||
| setControlMode('pointer') | |||
| }, [getNodesReadOnly, setControlMode]) | |||
| const handleModeHand = useCallback(() => { | |||
| if (getNodesReadOnly()) | |||
| return | |||
| setControlMode('hand') | |||
| handleSelectionCancel() | |||
| }, [getNodesReadOnly, setControlMode, handleSelectionCancel]) | |||
| useKeyPress('h', (e) => { | |||
| if (getNodesReadOnly()) | |||
| return | |||
| if (isEventTargetInputArea(e.target as HTMLElement)) | |||
| return | |||
| e.preventDefault() | |||
| handleModeHand() | |||
| }, { | |||
| exactMatch: true, | |||
| useCapture: true, | |||
| }) | |||
| useKeyPress('v', (e) => { | |||
| if (isEventTargetInputArea(e.target as HTMLElement)) | |||
| return | |||
| e.preventDefault() | |||
| handleModePointer() | |||
| }, { | |||
| exactMatch: true, | |||
| useCapture: true, | |||
| }) | |||
| const goLayout = () => { | |||
| if (getNodesReadOnly()) | |||
| return | |||
| handleLayout() | |||
| } | |||
| useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.o`, (e) => { | |||
| e.preventDefault() | |||
| goLayout() | |||
| }, { exactMatch: true, useCapture: true }) | |||
| const addNote = (e: MouseEvent<HTMLDivElement>) => { | |||
| if (getNodesReadOnly()) | |||
| @@ -110,7 +61,7 @@ const Control = () => { | |||
| <div | |||
| className={cn( | |||
| 'flex items-center justify-center mr-[1px] w-8 h-8 rounded-lg cursor-pointer', | |||
| controlMode === 'pointer' ? 'bg-primary-50 text-primary-600' : 'hover:bg-black/5 hover:text-gray-700', | |||
| controlMode === ControlMode.Pointer ? 'bg-primary-50 text-primary-600' : 'hover:bg-black/5 hover:text-gray-700', | |||
| `${nodesReadOnly && '!cursor-not-allowed opacity-50'}`, | |||
| )} | |||
| onClick={handleModePointer} | |||
| @@ -122,7 +73,7 @@ const Control = () => { | |||
| <div | |||
| className={cn( | |||
| 'flex items-center justify-center w-8 h-8 rounded-lg cursor-pointer', | |||
| controlMode === 'hand' ? 'bg-primary-50 text-primary-600' : 'hover:bg-black/5 hover:text-gray-700', | |||
| controlMode === ControlMode.Hand ? 'bg-primary-50 text-primary-600' : 'hover:bg-black/5 hover:text-gray-700', | |||
| `${nodesReadOnly && '!cursor-not-allowed opacity-50'}`, | |||
| )} | |||
| onClick={handleModeHand} | |||
| @@ -137,7 +88,7 @@ const Control = () => { | |||
| 'flex items-center justify-center w-8 h-8 rounded-lg hover:bg-black/5 hover:text-gray-700 cursor-pointer', | |||
| `${nodesReadOnly && '!cursor-not-allowed opacity-50'}`, | |||
| )} | |||
| onClick={goLayout} | |||
| onClick={handleLayout} | |||
| > | |||
| <RiFunctionAddLine className='w-4 h-4' /> | |||
| </div> | |||
| @@ -9,7 +9,6 @@ import { | |||
| RiZoomInLine, | |||
| RiZoomOutLine, | |||
| } from '@remixicon/react' | |||
| import { useKeyPress } from 'ahooks' | |||
| import { useTranslation } from 'react-i18next' | |||
| import { | |||
| useReactFlow, | |||
| @@ -20,9 +19,7 @@ import { | |||
| useWorkflowReadOnly, | |||
| } from '../hooks' | |||
| import { | |||
| getKeyboardKeyCodeBySystem, | |||
| getKeyboardKeyNameBySystem, | |||
| isEventTargetInputArea, | |||
| } from '../utils' | |||
| import ShortcutsName from '../shortcuts-name' | |||
| import TipPopup from './tip-popup' | |||
| @@ -116,87 +113,6 @@ const ZoomInOut: FC = () => { | |||
| handleSyncWorkflowDraft() | |||
| } | |||
| useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.1`, (e) => { | |||
| e.preventDefault() | |||
| if (workflowReadOnly) | |||
| return | |||
| fitView() | |||
| handleSyncWorkflowDraft() | |||
| }, { | |||
| exactMatch: true, | |||
| useCapture: true, | |||
| }) | |||
| useKeyPress('shift.1', (e) => { | |||
| if (workflowReadOnly) | |||
| return | |||
| if (isEventTargetInputArea(e.target as HTMLElement)) | |||
| return | |||
| e.preventDefault() | |||
| zoomTo(1) | |||
| handleSyncWorkflowDraft() | |||
| }, { | |||
| exactMatch: true, | |||
| useCapture: true, | |||
| }) | |||
| useKeyPress('shift.2', (e) => { | |||
| if (workflowReadOnly) | |||
| return | |||
| if (isEventTargetInputArea(e.target as HTMLElement)) | |||
| return | |||
| e.preventDefault() | |||
| zoomTo(2) | |||
| handleSyncWorkflowDraft() | |||
| }, { | |||
| exactMatch: true, | |||
| useCapture: true, | |||
| }) | |||
| useKeyPress('shift.5', (e) => { | |||
| if (workflowReadOnly) | |||
| return | |||
| if (isEventTargetInputArea(e.target as HTMLElement)) | |||
| return | |||
| e.preventDefault() | |||
| zoomTo(0.5) | |||
| handleSyncWorkflowDraft() | |||
| }, { | |||
| exactMatch: true, | |||
| useCapture: true, | |||
| }) | |||
| useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.dash`, (e) => { | |||
| e.preventDefault() | |||
| if (workflowReadOnly) | |||
| return | |||
| zoomOut() | |||
| handleSyncWorkflowDraft() | |||
| }, { | |||
| exactMatch: true, | |||
| useCapture: true, | |||
| }) | |||
| useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.equalsign`, (e) => { | |||
| e.preventDefault() | |||
| if (workflowReadOnly) | |||
| return | |||
| zoomIn() | |||
| handleSyncWorkflowDraft() | |||
| }, { | |||
| exactMatch: true, | |||
| useCapture: true, | |||
| }) | |||
| const handleTrigger = useCallback(() => { | |||
| if (getWorkflowReadOnly()) | |||
| return | |||
| @@ -289,11 +205,6 @@ const ZoomInOut: FC = () => { | |||
| <ShortcutsName keys={['shift', '1']} /> | |||
| ) | |||
| } | |||
| { | |||
| option.key === ZoomType.zoomTo200 && ( | |||
| <ShortcutsName keys={['shift', '2']} /> | |||
| ) | |||
| } | |||
| </div> | |||
| )) | |||
| } | |||
| @@ -7,7 +7,6 @@ import { Panel as NodePanel } from '../nodes' | |||
| import { useStore } from '../store' | |||
| import { | |||
| useIsChatMode, | |||
| useWorkflow, | |||
| } from '../hooks' | |||
| import DebugAndPreview from './debug-and-preview' | |||
| import Record from './record' | |||
| @@ -28,10 +27,6 @@ const Panel: FC = () => { | |||
| const showEnvPanel = useStore(s => s.showEnvPanel) | |||
| const showChatVariablePanel = useStore(s => s.showChatVariablePanel) | |||
| const isRestoring = useStore(s => s.isRestoring) | |||
| const { | |||
| enableShortcuts, | |||
| disableShortcuts, | |||
| } = useWorkflow() | |||
| const { currentLogItem, setCurrentLogItem, showMessageLogModal, setShowMessageLogModal, currentLogModalActiveTab } = useAppStore(useShallow(state => ({ | |||
| currentLogItem: state.currentLogItem, | |||
| setCurrentLogItem: state.setCurrentLogItem, | |||
| @@ -44,8 +39,6 @@ const Panel: FC = () => { | |||
| <div | |||
| tabIndex={-1} | |||
| className={cn('absolute top-14 right-0 bottom-2 flex z-10 outline-none')} | |||
| onFocus={disableShortcuts} | |||
| onBlur={enableShortcuts} | |||
| key={`${isRestoring}`} | |||
| > | |||
| { | |||
| @@ -99,8 +99,6 @@ type Shape = { | |||
| setWorkflowTools: (tools: ToolWithProvider[]) => void | |||
| clipboardElements: Node[] | |||
| setClipboardElements: (clipboardElements: Node[]) => void | |||
| shortcutsDisabled: boolean | |||
| setShortcutsDisabled: (shortcutsDisabled: boolean) => void | |||
| showDebugAndPreviewPanel: boolean | |||
| setShowDebugAndPreviewPanel: (showDebugAndPreviewPanel: boolean) => void | |||
| showEnvPanel: boolean | |||
| @@ -217,8 +215,6 @@ export const createWorkflowStore = () => { | |||
| setWorkflowTools: workflowTools => set(() => ({ workflowTools })), | |||
| clipboardElements: [], | |||
| setClipboardElements: clipboardElements => set(() => ({ clipboardElements })), | |||
| shortcutsDisabled: false, | |||
| setShortcutsDisabled: shortcutsDisabled => set(() => ({ shortcutsDisabled })), | |||
| showDebugAndPreviewPanel: false, | |||
| setShowDebugAndPreviewPanel: showDebugAndPreviewPanel => set(() => ({ showDebugAndPreviewPanel })), | |||
| showEnvPanel: false, | |||
| @@ -29,6 +29,11 @@ export enum BlockEnum { | |||
| Assigner = 'assigner', // is now named as VariableAssigner | |||
| } | |||
| export enum ControlMode { | |||
| Pointer = 'pointer', | |||
| Hand = 'hand', | |||
| } | |||
| export type Branch = { | |||
| id: string | |||
| name: string | |||