From c90dad566f0aae2997a3aac14223287043b2ff40 Mon Sep 17 00:00:00 2001 From: lyzno1 <92089059+lyzno1@users.noreply.github.com> Date: Thu, 28 Aug 2025 09:41:22 +0800 Subject: [PATCH] feat: enhance workflow error handling and internationalization (#24648) --- .../workflow-onboarding-modal/index.tsx | 16 ++++---- .../workflow/hooks/use-nodes-interactions.ts | 40 ++++++++++++++++--- web/i18n/en-US/workflow.ts | 10 +++++ web/i18n/ja-JP/workflow.ts | 10 +++++ web/i18n/zh-Hans/workflow.ts | 10 +++++ 5 files changed, 73 insertions(+), 13 deletions(-) diff --git a/web/app/components/workflow-app/components/workflow-onboarding-modal/index.tsx b/web/app/components/workflow-app/components/workflow-onboarding-modal/index.tsx index a183b5e9ae..f5d01c13cc 100644 --- a/web/app/components/workflow-app/components/workflow-onboarding-modal/index.tsx +++ b/web/app/components/workflow-app/components/workflow-onboarding-modal/index.tsx @@ -9,6 +9,7 @@ import { BlockEnum } from '@/app/components/workflow/types' import type { ToolDefaultValue } from '@/app/components/workflow/block-selector/types' import Modal from '@/app/components/base/modal' import StartNodeSelectionPanel from './start-node-selection-panel' +import { useDocLink } from '@/context/i18n' type WorkflowOnboardingModalProps = { isShow: boolean @@ -22,6 +23,7 @@ const WorkflowOnboardingModal: FC = ({ onSelectStartNode, }) => { const { t } = useTranslation() + const docLink = useDocLink() const handleSelectUserInput = useCallback(() => { onSelectStartNode(BlockEnum.Start) @@ -60,15 +62,15 @@ const WorkflowOnboardingModal: FC = ({
{t('workflow.onboarding.description')}{' '} - about start node. + {t('workflow.onboarding.learnMore')} + {' '} + {t('workflow.onboarding.aboutStartNode')}
diff --git a/web/app/components/workflow/hooks/use-nodes-interactions.ts b/web/app/components/workflow/hooks/use-nodes-interactions.ts index 1ecdcf2ed9..72b5e434da 100644 --- a/web/app/components/workflow/hooks/use-nodes-interactions.ts +++ b/web/app/components/workflow/hooks/use-nodes-interactions.ts @@ -1,6 +1,7 @@ import type { MouseEvent } from 'react' import { useCallback, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' +import { useToastContext } from '@/app/components/base/toast' import produce from 'immer' import type { NodeDragHandler, @@ -67,6 +68,7 @@ import { getNodeUsedVars } from '../nodes/_base/components/variable/utils' export const useNodesInteractions = () => { const { t } = useTranslation() + const { notify } = useToastContext() const store = useStoreApi() const workflowStore = useWorkflowStore() const reactflow = useReactFlow() @@ -91,6 +93,32 @@ export const useNodesInteractions = () => { const { saveStateToHistory, undo, redo } = useWorkflowHistory() + // Unified error handler for start node missing scenarios + const handleStartNodeMissingError = useCallback((error: Error, operationKey: string): boolean => { + if (error.message === 'Start node not found') { + const operation = t(`workflow.error.operations.${operationKey}`) || operationKey + notify({ + type: 'error', + message: t('workflow.error.startNodeRequired', { operation }) || `Please add a start node first before ${operation}`, + }) + return true // Error handled + } + return false // Error not handled, should re-throw + }, [notify, t]) + + // Safe wrapper for checkNestedParallelLimit with error handling + const safeCheckParallelLimit = useCallback((nodes: Node[], edges: Edge[], parentNodeId?: string, operationKey = 'updatingWorkflow') => { + try { + return checkNestedParallelLimit(nodes, edges, parentNodeId) + } + catch (error: any) { + if (handleStartNodeMissingError(error, operationKey)) + return false // Operation blocked but gracefully handled + + throw error // Re-throw other errors + } + }, [checkNestedParallelLimit, handleStartNodeMissingError]) + const handleNodeDragStart = useCallback((_, node) => { workflowStore.setState({ nodeAnimation: false }) @@ -419,7 +447,7 @@ export const useNodesInteractions = () => { draft.push(newEdge) }) - if (checkNestedParallelLimit(newNodes, newEdges, targetNode?.parentId)) { + if (safeCheckParallelLimit(newNodes, newEdges, targetNode?.parentId, 'connectingNodes')) { setNodes(newNodes) setEdges(newEdges) @@ -434,7 +462,7 @@ export const useNodesInteractions = () => { setConnectingNodePayload(undefined) setEnteringNodePayload(undefined) } - }, [getNodesReadOnly, store, workflowStore, handleSyncWorkflowDraft, saveStateToHistory, checkNestedParallelLimit]) + }, [getNodesReadOnly, store, workflowStore, handleSyncWorkflowDraft, saveStateToHistory, safeCheckParallelLimit]) const handleNodeConnectStart = useCallback((_, { nodeId, handleType, handleId }) => { if (getNodesReadOnly()) @@ -824,7 +852,7 @@ export const useNodesInteractions = () => { draft.push(newEdge) }) - if (checkNestedParallelLimit(newNodes, newEdges, prevNode.parentId)) { + if (safeCheckParallelLimit(newNodes, newEdges, prevNode.parentId, 'addingNodes')) { setNodes(newNodes) setEdges(newEdges) } @@ -944,7 +972,7 @@ export const useNodesInteractions = () => { draft.push(newEdge) }) - if (checkNestedParallelLimit(newNodes, newEdges, nextNode.parentId)) { + if (safeCheckParallelLimit(newNodes, newEdges, nextNode.parentId, 'modifyingWorkflow')) { setNodes(newNodes) setEdges(newEdges) } @@ -953,7 +981,7 @@ export const useNodesInteractions = () => { } } else { - if (checkNestedParallelLimit(newNodes, edges)) + if (safeCheckParallelLimit(newNodes, edges, undefined, 'updatingWorkflow')) setNodes(newNodes) else @@ -1102,7 +1130,7 @@ export const useNodesInteractions = () => { } handleSyncWorkflowDraft() saveStateToHistory(WorkflowHistoryEvent.NodeAdd) - }, [getNodesReadOnly, store, t, handleSyncWorkflowDraft, saveStateToHistory, workflowStore, getAfterNodesInSameBranch, checkNestedParallelLimit]) + }, [getNodesReadOnly, store, t, handleSyncWorkflowDraft, saveStateToHistory, workflowStore, getAfterNodesInSameBranch, safeCheckParallelLimit]) const handleNodeChange = useCallback(( currentNodeId: string, diff --git a/web/i18n/en-US/workflow.ts b/web/i18n/en-US/workflow.ts index f7680a1147..66e531462e 100644 --- a/web/i18n/en-US/workflow.ts +++ b/web/i18n/en-US/workflow.ts @@ -210,6 +210,16 @@ const translation = { invalidVariable: 'Invalid variable', noValidTool: '{{field}} no valid tool selected', toolParameterRequired: '{{field}}: parameter [{{param}}] is required', + startNodeRequired: 'Please add a start node first before {{operation}}', + }, + error: { + startNodeRequired: 'Please add a start node first before {{operation}}', + operations: { + connectingNodes: 'connecting nodes', + addingNodes: 'adding nodes', + modifyingWorkflow: 'modifying workflow', + updatingWorkflow: 'updating workflow', + }, }, singleRun: { testRun: 'Test Run ', diff --git a/web/i18n/ja-JP/workflow.ts b/web/i18n/ja-JP/workflow.ts index 6b7b1045d6..b21cd1f221 100644 --- a/web/i18n/ja-JP/workflow.ts +++ b/web/i18n/ja-JP/workflow.ts @@ -210,6 +210,16 @@ const translation = { invalidVariable: '無効な変数です', noValidTool: '{{field}} に利用可能なツールがありません', toolParameterRequired: '{{field}}: パラメータ [{{param}}] は必須です', + startNodeRequired: '{{operation}}前に開始ノードを追加してください', + }, + error: { + startNodeRequired: '{{operation}}前に開始ノードを追加してください', + operations: { + connectingNodes: 'ノード接続', + addingNodes: 'ノード追加', + modifyingWorkflow: 'ワークフロー変更', + updatingWorkflow: 'ワークフロー更新', + }, }, singleRun: { testRun: 'テスト実行', diff --git a/web/i18n/zh-Hans/workflow.ts b/web/i18n/zh-Hans/workflow.ts index b55ddfa17c..14dab920c0 100644 --- a/web/i18n/zh-Hans/workflow.ts +++ b/web/i18n/zh-Hans/workflow.ts @@ -210,6 +210,16 @@ const translation = { invalidVariable: '无效的变量', noValidTool: '{{field}} 无可用工具', toolParameterRequired: '{{field}}: 参数 [{{param}}] 不能为空', + startNodeRequired: '请先添加开始节点,然后再{{operation}}', + }, + error: { + startNodeRequired: '请先添加开始节点,然后再{{operation}}', + operations: { + connectingNodes: '连接节点', + addingNodes: '添加节点', + modifyingWorkflow: '修改工作流', + updatingWorkflow: '更新工作流', + }, }, singleRun: { testRun: '测试运行 ',