From 14c50a9f09dd736306b3f42e45785f752a64a01a Mon Sep 17 00:00:00 2001 From: Ponder Date: Fri, 24 Jul 2026 10:16:41 +0800 Subject: [PATCH] feat(web): locate workflow nodes by node_id in the workflow editor (#38187) Co-authored-by: Crazywoola <100913391+crazywoola@users.noreply.github.com> --- .../__tests__/workflow-edge-events.spec.tsx | 5 + .../hooks/__tests__/use-locate-node.spec.tsx | 176 ++++++++++++++++++ .../__tests__/use-workflow-search.spec.tsx | 57 +++++- .../workflow/hooks/use-locate-node.ts | 80 ++++++++ .../workflow/hooks/use-workflow-search.tsx | 10 +- web/app/components/workflow/index.tsx | 4 + web/i18n/ar-TN/workflow.json | 2 + web/i18n/de-DE/workflow.json | 2 + web/i18n/en-US/workflow.json | 2 + web/i18n/es-ES/workflow.json | 2 + web/i18n/fa-IR/workflow.json | 2 + web/i18n/fr-FR/workflow.json | 2 + web/i18n/hi-IN/workflow.json | 2 + web/i18n/id-ID/workflow.json | 2 + web/i18n/it-IT/workflow.json | 2 + web/i18n/ja-JP/workflow.json | 2 + web/i18n/ko-KR/workflow.json | 2 + web/i18n/nl-NL/workflow.json | 2 + web/i18n/pl-PL/workflow.json | 2 + web/i18n/pt-BR/workflow.json | 2 + web/i18n/ro-RO/workflow.json | 2 + web/i18n/ru-RU/workflow.json | 2 + web/i18n/sl-SI/workflow.json | 2 + web/i18n/th-TH/workflow.json | 2 + web/i18n/tr-TR/workflow.json | 2 + web/i18n/uk-UA/workflow.json | 2 + web/i18n/vi-VN/workflow.json | 2 + web/i18n/zh-Hans/workflow.json | 2 + web/i18n/zh-Hant/workflow.json | 2 + 29 files changed, 376 insertions(+), 2 deletions(-) create mode 100644 web/app/components/workflow/hooks/__tests__/use-locate-node.spec.tsx create mode 100644 web/app/components/workflow/hooks/use-locate-node.ts diff --git a/web/app/components/workflow/__tests__/workflow-edge-events.spec.tsx b/web/app/components/workflow/__tests__/workflow-edge-events.spec.tsx index f36e001d5f0..2b572022e88 100644 --- a/web/app/components/workflow/__tests__/workflow-edge-events.spec.tsx +++ b/web/app/components/workflow/__tests__/workflow-edge-events.spec.tsx @@ -165,6 +165,7 @@ vi.mock('@/next/navigation', () => ({ useRouter: () => ({ push: vi.fn(), }), + useSearchParams: () => new URLSearchParams(), })) vi.mock('@/context/event-emitter', () => ({ @@ -445,6 +446,10 @@ vi.mock('../hooks/use-workflow-search', () => ({ useWorkflowSearch: workflowHookMocks.useWorkflowSearch, })) +vi.mock('../hooks/use-locate-node', () => ({ + useLocateNode: vi.fn(), +})) + vi.mock('../nodes/_base/components/variable/use-match-schema-type', () => ({ default: () => ({ schemaTypeDefinitions: undefined, diff --git a/web/app/components/workflow/hooks/__tests__/use-locate-node.spec.tsx b/web/app/components/workflow/hooks/__tests__/use-locate-node.spec.tsx new file mode 100644 index 00000000000..be59307b7a1 --- /dev/null +++ b/web/app/components/workflow/hooks/__tests__/use-locate-node.spec.tsx @@ -0,0 +1,176 @@ +import type { CommonNodeType, Node } from '../../types' +import { renderHook } from '@testing-library/react' +import { useLocateNode } from '../use-locate-node' + +const mockHandleNodeSelect = vi.hoisted(() => vi.fn()) +const mockScrollToWorkflowNode = vi.hoisted(() => vi.fn()) +const mockSearchParams = vi.hoisted(() => new URLSearchParams()) +const mockToastSuccess = vi.hoisted(() => vi.fn()) +const mockToastError = vi.hoisted(() => vi.fn()) + +vi.mock('@/next/navigation', () => ({ + useSearchParams: () => mockSearchParams, +})) + +vi.mock('@langgenius/dify-ui/toast', () => ({ + toast: Object.assign(vi.fn(), { + success: mockToastSuccess, + error: mockToastError, + warning: vi.fn(), + info: vi.fn(), + dismiss: vi.fn(), + update: vi.fn(), + promise: vi.fn(), + }), +})) + +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: ( + selector: (source: Record) => string, + options?: Record, + ) => { + const key = selector( + new Proxy({}, { get: (_, property: string) => property }) as Record, + ) + if (options?.nodeId) return `${key}:${options.nodeId}` + if (options?.title) return `${key}:${options.title}` + return key + }, + }), +})) + +vi.mock('../use-nodes-interactions', () => ({ + useNodesInteractions: () => ({ + handleNodeSelect: mockHandleNodeSelect, + }), +})) + +vi.mock('../../utils/node-navigation', () => ({ + scrollToWorkflowNode: (nodeId: string) => mockScrollToWorkflowNode(nodeId), +})) + +const createNode = (overrides: Partial = {}): Node => ({ + id: 'node-1', + type: 'custom', + position: { x: 0, y: 0 }, + data: { + type: 'llm', + title: 'Writer', + desc: 'Draft content', + } as CommonNodeType, + ...overrides, +}) + +describe('useLocateNode', () => { + beforeEach(() => { + vi.clearAllMocks() + vi.useFakeTimers() + mockSearchParams.delete('node_id') + }) + + afterEach(() => { + vi.useRealTimers() + }) + + it('does nothing when node_id param is absent', () => { + const nodes = [createNode({ id: 'n1' })] + renderHook(() => useLocateNode(nodes)) + + expect(mockHandleNodeSelect).not.toHaveBeenCalled() + expect(mockToastSuccess).not.toHaveBeenCalled() + expect(mockToastError).not.toHaveBeenCalled() + }) + + it('does nothing when nodes are empty', () => { + mockSearchParams.set('node_id', 'n1') + renderHook(() => useLocateNode([])) + + expect(mockHandleNodeSelect).not.toHaveBeenCalled() + expect(mockToastError).not.toHaveBeenCalled() + }) + + it('selects, scrolls to, and shows success toast when node is found', () => { + mockSearchParams.set('node_id', 'target-node') + const nodes = [createNode({ id: 'target-node', data: { title: 'My Node' } as CommonNodeType })] + + renderHook(() => useLocateNode(nodes)) + + expect(mockHandleNodeSelect).toHaveBeenCalledWith('target-node') + expect(mockToastSuccess).toHaveBeenCalledWith('panel.locateNodeSuccess:My Node') + + // Scroll happens after a 200ms delay + expect(mockScrollToWorkflowNode).not.toHaveBeenCalled() + vi.advanceTimersByTime(200) + expect(mockScrollToWorkflowNode).toHaveBeenCalledWith('target-node') + }) + + it('shows error toast after debounce when node is not found', () => { + mockSearchParams.set('node_id', 'missing-node') + const nodes = [createNode({ id: 'other-node' })] + + renderHook(() => useLocateNode(nodes)) + + // Not immediately reported + expect(mockToastError).not.toHaveBeenCalled() + + // After 500ms debounce, reports not found + vi.advanceTimersByTime(500) + expect(mockToastError).toHaveBeenCalledWith('panel.locateNodeNotFound:missing-node') + }) + + it('does not report not-found prematurely when nodes are still loading', () => { + mockSearchParams.set('node_id', 'target-node') + const initialNodes = [createNode({ id: 'other-node' })] + + const { rerender } = renderHook(({ nodes }) => useLocateNode(nodes), { + initialProps: { nodes: initialNodes }, + }) + + // Before debounce fires, more nodes arrive including the target + vi.advanceTimersByTime(300) + rerender({ + nodes: [ + createNode({ id: 'other-node' }), + createNode({ id: 'target-node', data: { title: 'Found!' } as CommonNodeType }), + ], + }) + + // Should have located the node, not reported error + expect(mockToastError).not.toHaveBeenCalled() + expect(mockHandleNodeSelect).toHaveBeenCalledWith('target-node') + expect(mockToastSuccess).toHaveBeenCalledWith('panel.locateNodeSuccess:Found!') + }) + + it('locates only once even if nodes update afterwards', () => { + mockSearchParams.set('node_id', 'target-node') + const nodes1 = [createNode({ id: 'target-node' })] + + const { rerender } = renderHook(({ nodes }) => useLocateNode(nodes), { + initialProps: { nodes: nodes1 }, + }) + + expect(mockHandleNodeSelect).toHaveBeenCalledTimes(1) + + // Simulate a nodes update after locate has already happened + rerender({ + nodes: [createNode({ id: 'target-node' }), createNode({ id: 'extra' })], + }) + + expect(mockHandleNodeSelect).toHaveBeenCalledTimes(1) + expect(mockToastSuccess).toHaveBeenCalledTimes(1) + }) + + it('clears scroll timeout on unmount', () => { + mockSearchParams.set('node_id', 'target-node') + const nodes = [createNode({ id: 'target-node' })] + + const { unmount } = renderHook(() => useLocateNode(nodes)) + + // Unmount before the 200ms scroll timer fires + unmount() + + vi.advanceTimersByTime(500) + expect(mockScrollToWorkflowNode).not.toHaveBeenCalled() + }) +}) diff --git a/web/app/components/workflow/hooks/__tests__/use-workflow-search.spec.tsx b/web/app/components/workflow/hooks/__tests__/use-workflow-search.spec.tsx index ba013d143d2..e0a9a9d5aab 100644 --- a/web/app/components/workflow/hooks/__tests__/use-workflow-search.spec.tsx +++ b/web/app/components/workflow/hooks/__tests__/use-workflow-search.spec.tsx @@ -94,13 +94,68 @@ describe('useWorkflowSearch', () => { const toolResults = findWorkflowNodes('search') expect(toolResults.map((item) => item.id)).toEqual(['tool-1']) - expect(toolResults[0]?.description).toBe('Search the web') + // description now includes nodeId suffix: "desc · nodeId" + expect(toolResults[0]?.description).toBe('Search the web · tool-1') unmount() expect(findWorkflowNodes('gpt')).toEqual([]) }) + it('matches by node_id with highest priority scoring', async () => { + runtimeNodes.push( + createNode({ + id: '1721234567890', + data: { + type: BlockEnum.LLM, + title: 'Writer', + desc: 'Draft content', + } as CommonNodeType, + }), + createNode({ + id: 'other-node', + data: { + type: BlockEnum.LLM, + title: '1721234567890', // title also matches the searchTerm + desc: '', + } as CommonNodeType, + }), + ) + + const { unmount } = renderHook(() => useWorkflowSearch()) + + // Exact nodeId match (120pts) should rank higher than title exact match (100pts) + const results = findWorkflowNodes('1721234567890') + expect(results.map((item) => item.id)).toEqual(['1721234567890', 'other-node']) + + unmount() + }) + + it('matches by partial node_id', async () => { + runtimeNodes.push( + createNode({ + id: '1721234567890', + data: { + type: BlockEnum.LLM, + title: 'Writer', + desc: 'Draft content', + } as CommonNodeType, + }), + ) + + const { unmount } = renderHook(() => useWorkflowSearch()) + + // Prefix match + const prefixResults = findWorkflowNodes('172123') + expect(prefixResults.map((item) => item.id)).toEqual(['1721234567890']) + + // Partial match + const partialResults = findWorkflowNodes('456') + expect(partialResults.map((item) => item.id)).toEqual(['1721234567890']) + + unmount() + }) + it('binds the node selection listener to handleNodeSelect', () => { const { unmount } = renderHook(() => useWorkflowSearch()) diff --git a/web/app/components/workflow/hooks/use-locate-node.ts b/web/app/components/workflow/hooks/use-locate-node.ts new file mode 100644 index 00000000000..28e97bbd2e5 --- /dev/null +++ b/web/app/components/workflow/hooks/use-locate-node.ts @@ -0,0 +1,80 @@ +'use client' + +import type { Node } from '../types' +import { toast } from '@langgenius/dify-ui/toast' +import { useEffect, useRef } from 'react' +import { useTranslation } from 'react-i18next' +import { useSearchParams } from '@/next/navigation' +import { scrollToWorkflowNode } from '../utils/node-navigation' +import { useNodesInteractions } from './use-nodes-interactions' + +/** + * Hook to locate a node by ID from URL query parameter `node_id`. + * + * Usage scenario: operators find a failing node ID from server logs, + * construct a URL like `/workflow?node_id=xxx`, and the editor will + * automatically select and scroll to that node on load. + * + * The hook reads `node_id` from the URL search params, waits for nodes + * to be available, then selects and scrolls to the target node. + * A toast message is shown to indicate success or failure. + */ +export const useLocateNode = (nodes: Node[]) => { + const { t } = useTranslation('workflow') + const searchParams = useSearchParams() + const nodeIdFromUrl = searchParams.get('node_id') + const { handleNodeSelect } = useNodesInteractions() + const hasLocateRef = useRef(false) + + useEffect(() => { + if (!nodeIdFromUrl || hasLocateRef.current) return + + // Wait for nodes to be loaded + if (!nodes.length) return + + const targetNode = nodes.find((n) => n.id === nodeIdFromUrl) + + if (!targetNode) { + // Don't mark as located yet — nodes may still be loading asynchronously. + // Retry on the next nodes update before reporting "not found". + return + } + + // Select the node (opens its panel) and scroll to it + handleNodeSelect(nodeIdFromUrl) + + // Delay scroll to ensure node selection state has been applied + const scrollTimer = setTimeout(() => { + scrollToWorkflowNode(nodeIdFromUrl) + }, 200) + + toast.success( + t(($) => $['panel.locateNodeSuccess'], { + title: targetNode.data?.title || nodeIdFromUrl, + }), + ) + hasLocateRef.current = true + + return () => clearTimeout(scrollTimer) + }, [nodeIdFromUrl, nodes, handleNodeSelect, t]) + + // Report "not found" after nodes have settled (no longer changing) + useEffect(() => { + if (!nodeIdFromUrl || hasLocateRef.current || !nodes.length) return + + const targetNode = nodes.find((n) => n.id === nodeIdFromUrl) + if (targetNode) return + + // Debounce: wait to see if nodes continue loading before reporting not found + const notFoundTimer = setTimeout(() => { + if (hasLocateRef.current) return + const stillMissing = !nodes.find((n) => n.id === nodeIdFromUrl) + if (stillMissing) { + toast.error(t(($) => $['panel.locateNodeNotFound'], { nodeId: nodeIdFromUrl })) + hasLocateRef.current = true + } + }, 500) + + return () => clearTimeout(notFoundTimer) + }, [nodeIdFromUrl, nodes, t]) +} diff --git a/web/app/components/workflow/hooks/use-workflow-search.tsx b/web/app/components/workflow/hooks/use-workflow-search.tsx index 9f9a1d1dfb7..71ef3afc388 100644 --- a/web/app/components/workflow/hooks/use-workflow-search.tsx +++ b/web/app/components/workflow/hooks/use-workflow-search.tsx @@ -80,6 +80,7 @@ export const useWorkflowSearch = () => { return { id: node.id, + nodeId: node.id, title: nodeData?.title || nodeData?.type || 'Untitled', type: nodeData?.type || '', desc: nodeData?.desc || '', @@ -95,6 +96,7 @@ export const useWorkflowSearch = () => { const calculateScore = useCallback( ( node: { + nodeId: string title: string type: string desc: string @@ -107,12 +109,18 @@ export const useWorkflowSearch = () => { const titleMatch = node.title.toLowerCase() const typeMatch = node.type.toLowerCase() const descMatch = node.desc?.toLowerCase() || '' + const nodeIdMatch = node.nodeId?.toLowerCase() || '' const modelProviderMatch = node.modelInfo?.provider?.toLowerCase() || '' const modelNameMatch = node.modelInfo?.name?.toLowerCase() || '' const modelModeMatch = node.modelInfo?.mode?.toLowerCase() || '' let score = 0 + // Node ID matching (exact > partial — useful for locating nodes from server logs) + if (nodeIdMatch === searchTerm) score += 120 + else if (nodeIdMatch.startsWith(searchTerm)) score += 90 + else if (nodeIdMatch.includes(searchTerm)) score += 40 + // Title matching (exact prefix > partial match) if (titleMatch.startsWith(searchTerm)) score += 100 else if (titleMatch.includes(searchTerm)) score += 50 @@ -149,7 +157,7 @@ export const useWorkflowSearch = () => { ? { id: node.id, title: node.title, - description: node.desc || node.type, + description: [node.desc || node.type, node.nodeId].filter(Boolean).join(' · '), type: 'workflow-node' as const, path: `#${node.id}`, icon: ( diff --git a/web/app/components/workflow/index.tsx b/web/app/components/workflow/index.tsx index 4097f3114ca..c7bd4c1e181 100644 --- a/web/app/components/workflow/index.tsx +++ b/web/app/components/workflow/index.tsx @@ -70,6 +70,7 @@ import { useWorkflowRefreshDraft, } from './hooks' import { HooksStoreContextProvider, useHooksStore } from './hooks-store' +import { useLocateNode } from './hooks/use-locate-node' import { useWorkflowComment } from './hooks/use-workflow-comment' import { useWorkflowSearch } from './hooks/use-workflow-search' import { shouldPreventWorkflowBrowserDefault } from './hotkeys' @@ -565,6 +566,9 @@ export const Workflow: FC = memo( // Initialize workflow node search functionality useWorkflowSearch() + // Locate a node by ID from URL query parameter `node_id` + useLocateNode(nodes) + // Set up scroll to node event listener using the utility function useEffect(() => { return setupScrollToNodeListener(nodes, reactflow) diff --git a/web/i18n/ar-TN/workflow.json b/web/i18n/ar-TN/workflow.json index 377561e16e1..cda7d7410da 100644 --- a/web/i18n/ar-TN/workflow.json +++ b/web/i18n/ar-TN/workflow.json @@ -1199,6 +1199,8 @@ "panel.goTo": "الذهاب إلى", "panel.goToFix": "الذهاب إلى الإصلاح", "panel.helpLink": "عرض المستندات", + "panel.locateNodeNotFound": "العقدة غير موجودة: {{nodeId}}", + "panel.locateNodeSuccess": "تم تحديد موقع العقدة: {{title}}", "panel.nextStep": "الخطوة التالية", "panel.openWorkflow": "فتح سير العمل", "panel.optional": "(اختياري)", diff --git a/web/i18n/de-DE/workflow.json b/web/i18n/de-DE/workflow.json index 41f0e688cd7..3cacd33a265 100644 --- a/web/i18n/de-DE/workflow.json +++ b/web/i18n/de-DE/workflow.json @@ -1199,6 +1199,8 @@ "panel.goTo": "Gehe zu", "panel.goToFix": "Zur Behebung", "panel.helpLink": "Hilfe", + "panel.locateNodeNotFound": "Knoten nicht gefunden: {{nodeId}}", + "panel.locateNodeSuccess": "Knoten gefunden: {{title}}", "panel.nextStep": "Nächster Schritt", "panel.openWorkflow": "Workflow öffnen", "panel.optional": "(optional)", diff --git a/web/i18n/en-US/workflow.json b/web/i18n/en-US/workflow.json index e7f5f9c5649..c519c98dd92 100644 --- a/web/i18n/en-US/workflow.json +++ b/web/i18n/en-US/workflow.json @@ -1199,6 +1199,8 @@ "panel.goTo": "Go to", "panel.goToFix": "Go to fix", "panel.helpLink": "View Docs", + "panel.locateNodeNotFound": "Node not found: {{nodeId}}", + "panel.locateNodeSuccess": "Located node: {{title}}", "panel.nextStep": "Next Step", "panel.openWorkflow": "Open Workflow", "panel.optional": "(optional)", diff --git a/web/i18n/es-ES/workflow.json b/web/i18n/es-ES/workflow.json index 7aefb4af4dd..593968a240e 100644 --- a/web/i18n/es-ES/workflow.json +++ b/web/i18n/es-ES/workflow.json @@ -1199,6 +1199,8 @@ "panel.goTo": "Ir a", "panel.goToFix": "Ir a corregir", "panel.helpLink": "Ayuda", + "panel.locateNodeNotFound": "Nodo no encontrado: {{nodeId}}", + "panel.locateNodeSuccess": "Nodo localizado: {{title}}", "panel.nextStep": "Siguiente paso", "panel.openWorkflow": "Abrir flujo de trabajo", "panel.optional": "(opcional)", diff --git a/web/i18n/fa-IR/workflow.json b/web/i18n/fa-IR/workflow.json index ab3c675e894..8e28a179603 100644 --- a/web/i18n/fa-IR/workflow.json +++ b/web/i18n/fa-IR/workflow.json @@ -1199,6 +1199,8 @@ "panel.goTo": "برو به", "panel.goToFix": "برو به رفع", "panel.helpLink": "راهنما", + "panel.locateNodeNotFound": "گره پیدا نشد: {{nodeId}}", + "panel.locateNodeSuccess": "گره پیدا شد: {{title}}", "panel.nextStep": "مرحله بعدی", "panel.openWorkflow": "باز کردن گردش کار", "panel.optional": "(اختیاری)", diff --git a/web/i18n/fr-FR/workflow.json b/web/i18n/fr-FR/workflow.json index e4062e93851..5ff3713e869 100644 --- a/web/i18n/fr-FR/workflow.json +++ b/web/i18n/fr-FR/workflow.json @@ -1199,6 +1199,8 @@ "panel.goTo": "Aller à", "panel.goToFix": "Aller corriger", "panel.helpLink": "Aide", + "panel.locateNodeNotFound": "Nœud introuvable : {{nodeId}}", + "panel.locateNodeSuccess": "Nœud localisé : {{title}}", "panel.nextStep": "Étape suivante", "panel.openWorkflow": "Ouvrir le flux de travail", "panel.optional": "(facultatif)", diff --git a/web/i18n/hi-IN/workflow.json b/web/i18n/hi-IN/workflow.json index bcd97d4834a..eb210f83b19 100644 --- a/web/i18n/hi-IN/workflow.json +++ b/web/i18n/hi-IN/workflow.json @@ -1199,6 +1199,8 @@ "panel.goTo": "जाओ", "panel.goToFix": "ठीक करने जाएँ", "panel.helpLink": "सहायता", + "panel.locateNodeNotFound": "नोड नहीं मिला: {{nodeId}}", + "panel.locateNodeSuccess": "नोड मिला: {{title}}", "panel.nextStep": "अगला कदम", "panel.openWorkflow": "वर्कफ़्लो खोलें", "panel.optional": "(वैकल्पिक)", diff --git a/web/i18n/id-ID/workflow.json b/web/i18n/id-ID/workflow.json index 5414150d9d1..53e2c417ae2 100644 --- a/web/i18n/id-ID/workflow.json +++ b/web/i18n/id-ID/workflow.json @@ -1199,6 +1199,8 @@ "panel.goTo": "Pergi ke", "panel.goToFix": "Pergi ke perbaikan", "panel.helpLink": "Docs", + "panel.locateNodeNotFound": "Node tidak ditemukan: {{nodeId}}", + "panel.locateNodeSuccess": "Node ditemukan: {{title}}", "panel.nextStep": "Langkah Berikutnya", "panel.openWorkflow": "Buka Alur Kerja", "panel.optional": "(opsional)", diff --git a/web/i18n/it-IT/workflow.json b/web/i18n/it-IT/workflow.json index 55984d91547..cf1b83b205f 100644 --- a/web/i18n/it-IT/workflow.json +++ b/web/i18n/it-IT/workflow.json @@ -1199,6 +1199,8 @@ "panel.goTo": "Vai a", "panel.goToFix": "Vai a correggere", "panel.helpLink": "Aiuto", + "panel.locateNodeNotFound": "Nodo non trovato: {{nodeId}}", + "panel.locateNodeSuccess": "Nodo localizzato: {{title}}", "panel.nextStep": "Prossimo Passo", "panel.openWorkflow": "Apri flusso di lavoro", "panel.optional": "(opzionale)", diff --git a/web/i18n/ja-JP/workflow.json b/web/i18n/ja-JP/workflow.json index 96f00b13d4e..498321a3eda 100644 --- a/web/i18n/ja-JP/workflow.json +++ b/web/i18n/ja-JP/workflow.json @@ -1199,6 +1199,8 @@ "panel.goTo": "移動", "panel.goToFix": "修正する", "panel.helpLink": "ドキュメントを見る", + "panel.locateNodeNotFound": "ノードが見つかりません:{{nodeId}}", + "panel.locateNodeSuccess": "ノードを特定しました:{{title}}", "panel.nextStep": "次のステップ", "panel.openWorkflow": "ワークフローを開く", "panel.optional": "(任意)", diff --git a/web/i18n/ko-KR/workflow.json b/web/i18n/ko-KR/workflow.json index 62550cdbed7..7fc45e2a8ba 100644 --- a/web/i18n/ko-KR/workflow.json +++ b/web/i18n/ko-KR/workflow.json @@ -1199,6 +1199,8 @@ "panel.goTo": "로 이동", "panel.goToFix": "수정하러 가기", "panel.helpLink": "도움말 센터", + "panel.locateNodeNotFound": "노드를 찾을 수 없습니다: {{nodeId}}", + "panel.locateNodeSuccess": "노드를 찾았습니다: {{title}}", "panel.nextStep": "다음 단계", "panel.openWorkflow": "워크플로 열기", "panel.optional": "(선택사항)", diff --git a/web/i18n/nl-NL/workflow.json b/web/i18n/nl-NL/workflow.json index b3503ec392a..53641c529c9 100644 --- a/web/i18n/nl-NL/workflow.json +++ b/web/i18n/nl-NL/workflow.json @@ -1199,6 +1199,8 @@ "panel.goTo": "Go to", "panel.goToFix": "Ga naar repareren", "panel.helpLink": "View Docs", + "panel.locateNodeNotFound": "Node niet gevonden: {{nodeId}}", + "panel.locateNodeSuccess": "Node gevonden: {{title}}", "panel.nextStep": "Next Step", "panel.openWorkflow": "Open Workflow", "panel.optional": "(optional)", diff --git a/web/i18n/pl-PL/workflow.json b/web/i18n/pl-PL/workflow.json index bcc5e456f29..9bff93cc309 100644 --- a/web/i18n/pl-PL/workflow.json +++ b/web/i18n/pl-PL/workflow.json @@ -1199,6 +1199,8 @@ "panel.goTo": "Idź do", "panel.goToFix": "Przejdź do naprawy", "panel.helpLink": "Pomoc", + "panel.locateNodeNotFound": "Nie znaleziono węzła: {{nodeId}}", + "panel.locateNodeSuccess": "Zlokalizowano węzeł: {{title}}", "panel.nextStep": "Następny krok", "panel.openWorkflow": "Otwórz przepływ pracy", "panel.optional": "(opcjonalne)", diff --git a/web/i18n/pt-BR/workflow.json b/web/i18n/pt-BR/workflow.json index b0e27beb31f..40ab7e1025f 100644 --- a/web/i18n/pt-BR/workflow.json +++ b/web/i18n/pt-BR/workflow.json @@ -1199,6 +1199,8 @@ "panel.goTo": "Ir para", "panel.goToFix": "Ir para correção", "panel.helpLink": "Ajuda", + "panel.locateNodeNotFound": "Nó não encontrado: {{nodeId}}", + "panel.locateNodeSuccess": "Nó localizado: {{title}}", "panel.nextStep": "Próximo passo", "panel.openWorkflow": "Abrir fluxo de trabalho", "panel.optional": "(opcional)", diff --git a/web/i18n/ro-RO/workflow.json b/web/i18n/ro-RO/workflow.json index 5e4f689fd15..8fa9e3d1100 100644 --- a/web/i18n/ro-RO/workflow.json +++ b/web/i18n/ro-RO/workflow.json @@ -1199,6 +1199,8 @@ "panel.goTo": "Du-te la", "panel.goToFix": "Mergi la remediere", "panel.helpLink": "Ajutor", + "panel.locateNodeNotFound": "Nod negăsit: {{nodeId}}", + "panel.locateNodeSuccess": "Nod localizat: {{title}}", "panel.nextStep": "Pasul următor", "panel.openWorkflow": "Deschide fluxul de lucru", "panel.optional": "(opțional)", diff --git a/web/i18n/ru-RU/workflow.json b/web/i18n/ru-RU/workflow.json index 79b5226000f..58119f32125 100644 --- a/web/i18n/ru-RU/workflow.json +++ b/web/i18n/ru-RU/workflow.json @@ -1199,6 +1199,8 @@ "panel.goTo": "Перейти к", "panel.goToFix": "Перейти к исправлению", "panel.helpLink": "Помощь", + "panel.locateNodeNotFound": "Узел не найден: {{nodeId}}", + "panel.locateNodeSuccess": "Узел найден: {{title}}", "panel.nextStep": "Следующий шаг", "panel.openWorkflow": "Открыть рабочий процесс", "panel.optional": "(необязательно)", diff --git a/web/i18n/sl-SI/workflow.json b/web/i18n/sl-SI/workflow.json index 0141163c9da..6f36cd53f20 100644 --- a/web/i18n/sl-SI/workflow.json +++ b/web/i18n/sl-SI/workflow.json @@ -1199,6 +1199,8 @@ "panel.goTo": "Pojdi na", "panel.goToFix": "Pojdi na popravi", "panel.helpLink": "Pomoč", + "panel.locateNodeNotFound": "Vozlišče ni najdeno: {{nodeId}}", + "panel.locateNodeSuccess": "Vozlišče najdeno: {{title}}", "panel.nextStep": "Naslednji korak", "panel.openWorkflow": "Odpri delovni tok", "panel.optional": "(neobvezno)", diff --git a/web/i18n/th-TH/workflow.json b/web/i18n/th-TH/workflow.json index 6f26185d00a..311a9f28686 100644 --- a/web/i18n/th-TH/workflow.json +++ b/web/i18n/th-TH/workflow.json @@ -1199,6 +1199,8 @@ "panel.goTo": "ไปที่", "panel.goToFix": "ไปแก้ไข", "panel.helpLink": "วิธีใช้", + "panel.locateNodeNotFound": "ไม่พบโหนด: {{nodeId}}", + "panel.locateNodeSuccess": "พบโหนดแล้ว: {{title}}", "panel.nextStep": "ขั้นตอนถัดไป", "panel.openWorkflow": "เปิดเวิร์กโฟลว์", "panel.optional": "(ไม่บังคับ)", diff --git a/web/i18n/tr-TR/workflow.json b/web/i18n/tr-TR/workflow.json index e02c6068804..95fb17c92be 100644 --- a/web/i18n/tr-TR/workflow.json +++ b/web/i18n/tr-TR/workflow.json @@ -1199,6 +1199,8 @@ "panel.goTo": "Git", "panel.goToFix": "Düzeltmeye git", "panel.helpLink": "Yardım", + "panel.locateNodeNotFound": "Düğüm bulunamadı: {{nodeId}}", + "panel.locateNodeSuccess": "Düğüm bulundu: {{title}}", "panel.nextStep": "Sonraki Adım", "panel.openWorkflow": "İş Akışını Aç", "panel.optional": "(isteğe bağlı)", diff --git a/web/i18n/uk-UA/workflow.json b/web/i18n/uk-UA/workflow.json index 720d81292d5..088b681c86d 100644 --- a/web/i18n/uk-UA/workflow.json +++ b/web/i18n/uk-UA/workflow.json @@ -1199,6 +1199,8 @@ "panel.goTo": "Перейти до", "panel.goToFix": "Перейти до виправлення", "panel.helpLink": "Довідковий центр", + "panel.locateNodeNotFound": "Вузол не знайдено: {{nodeId}}", + "panel.locateNodeSuccess": "Вузол знайдено: {{title}}", "panel.nextStep": "Наступний крок", "panel.openWorkflow": "Відкрити робочий процес", "panel.optional": "(необов'язково)", diff --git a/web/i18n/vi-VN/workflow.json b/web/i18n/vi-VN/workflow.json index b6ab2206358..eb83b3488b0 100644 --- a/web/i18n/vi-VN/workflow.json +++ b/web/i18n/vi-VN/workflow.json @@ -1199,6 +1199,8 @@ "panel.goTo": "Đi tới", "panel.goToFix": "Đi đến sửa lỗi", "panel.helpLink": "Trung tâm trợ giúp", + "panel.locateNodeNotFound": "Không tìm thấy nút: {{nodeId}}", + "panel.locateNodeSuccess": "Đã định vị nút: {{title}}", "panel.nextStep": "Bước tiếp theo", "panel.openWorkflow": "Mở quy trình làm việc", "panel.optional": "(tùy chọn)", diff --git a/web/i18n/zh-Hans/workflow.json b/web/i18n/zh-Hans/workflow.json index 9833b253d47..1101f1f7a3e 100644 --- a/web/i18n/zh-Hans/workflow.json +++ b/web/i18n/zh-Hans/workflow.json @@ -1199,6 +1199,8 @@ "panel.goTo": "转到", "panel.goToFix": "前往修复", "panel.helpLink": "查看帮助文档", + "panel.locateNodeNotFound": "未找到节点:{{nodeId}}", + "panel.locateNodeSuccess": "已定位到节点:{{title}}", "panel.nextStep": "下一步", "panel.openWorkflow": "打开工作流", "panel.optional": "(选填)", diff --git a/web/i18n/zh-Hant/workflow.json b/web/i18n/zh-Hant/workflow.json index b2a3f1c05ce..d3cdc5a2588 100644 --- a/web/i18n/zh-Hant/workflow.json +++ b/web/i18n/zh-Hant/workflow.json @@ -1199,6 +1199,8 @@ "panel.goTo": "前往", "panel.goToFix": "前往修復", "panel.helpLink": "查看幫助文件", + "panel.locateNodeNotFound": "未找到節點:{{nodeId}}", + "panel.locateNodeSuccess": "已定位到節點:{{title}}", "panel.nextStep": "下一步", "panel.openWorkflow": "打開工作流程", "panel.optional": "(選擇性)",