Compare commits

...
316 changed files with 2348 additions and 1423 deletions
+5 -5
View File
@@ -2787,7 +2787,7 @@
},
"web/app/components/rag-pipeline/hooks/use-nodes-sync-draft.ts": {
"ts/no-explicit-any": {
"count": 2
"count": 1
}
},
"web/app/components/rag-pipeline/hooks/use-pipeline-config.ts": {
@@ -2797,7 +2797,7 @@
},
"web/app/components/rag-pipeline/hooks/use-pipeline-init.ts": {
"ts/no-explicit-any": {
"count": 2
"count": 1
}
},
"web/app/components/rag-pipeline/hooks/use-pipeline-run.ts": {
@@ -2949,12 +2949,12 @@
},
"web/app/components/workflow-app/hooks/use-nodes-sync-draft.ts": {
"ts/no-explicit-any": {
"count": 2
"count": 1
}
},
"web/app/components/workflow-app/hooks/use-workflow-init.ts": {
"ts/no-explicit-any": {
"count": 3
"count": 2
}
},
"web/app/components/workflow-app/hooks/use-workflow-refresh-draft.ts": {
@@ -4315,7 +4315,7 @@
"count": 16
},
"ts/no-empty-object-type": {
"count": 3
"count": 1
},
"ts/no-explicit-any": {
"count": 8
+39 -141
View File
@@ -243,6 +243,9 @@ catalogs:
'@vitest/coverage-v8':
specifier: 4.1.6
version: 4.1.6
'@xyflow/react':
specifier: 12.10.2
version: 12.10.2
abcjs:
specifier: 6.6.3
version: 6.6.3
@@ -474,9 +477,6 @@ catalogs:
react-textarea-autosize:
specifier: 8.5.9
version: 8.5.9
reactflow:
specifier: 11.11.4
version: 11.11.4
remark-breaks:
specifier: 4.0.0
version: 4.0.0
@@ -971,6 +971,9 @@ importers:
'@tanstack/react-virtual':
specifier: 'catalog:'
version: 3.13.24(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
'@xyflow/react':
specifier: 'catalog:'
version: 12.10.2(@types/react@19.2.14)(immer@11.1.8)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
abcjs:
specifier: 'catalog:'
version: 6.6.3
@@ -1139,9 +1142,6 @@ importers:
react-textarea-autosize:
specifier: 'catalog:'
version: 8.5.9(@types/react@19.2.14)(react@19.2.6)
reactflow:
specifier: 'catalog:'
version: 11.11.4(@types/react@19.2.14)(immer@11.1.8)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
remark-breaks:
specifier: 'catalog:'
version: 4.0.0
@@ -3362,42 +3362,6 @@ packages:
'@types/react':
optional: true
'@reactflow/background@11.3.14':
resolution: {integrity: sha512-Gewd7blEVT5Lh6jqrvOgd4G6Qk17eGKQfsDXgyRSqM+CTwDqRldG2LsWN4sNeno6sbqVIC2fZ+rAUBFA9ZEUDA==}
peerDependencies:
react: '>=17'
react-dom: '>=17'
'@reactflow/controls@11.2.14':
resolution: {integrity: sha512-MiJp5VldFD7FrqaBNIrQ85dxChrG6ivuZ+dcFhPQUwOK3HfYgX2RHdBua+gx+40p5Vw5It3dVNp/my4Z3jF0dw==}
peerDependencies:
react: '>=17'
react-dom: '>=17'
'@reactflow/core@11.11.4':
resolution: {integrity: sha512-H4vODklsjAq3AMq6Np4LE12i1I4Ta9PrDHuBR9GmL8uzTt2l2jh4CiQbEMpvMDcp7xi4be0hgXj+Ysodde/i7Q==}
peerDependencies:
react: '>=17'
react-dom: '>=17'
'@reactflow/minimap@11.7.14':
resolution: {integrity: sha512-mpwLKKrEAofgFJdkhwR5UQ1JYWlcAAL/ZU/bctBkuNTT1yqV+y0buoNVImsRehVYhJwffSWeSHaBR5/GJjlCSQ==}
peerDependencies:
react: '>=17'
react-dom: '>=17'
'@reactflow/node-resizer@2.2.14':
resolution: {integrity: sha512-fwqnks83jUlYr6OHcdFEedumWKChTHRGw/kbCxj0oqBd+ekfs+SIp4ddyNU0pdx96JIm5iNFS0oNrmEiJbbSaA==}
peerDependencies:
react: '>=17'
react-dom: '>=17'
'@reactflow/node-toolbar@1.3.14':
resolution: {integrity: sha512-rbynXQnH/xFNu4P9H+hVqlEUafDCkEoCy0Dg9mG22Sg+rY/0ck6KkrAQrYrTgXusd+cEJOMK0uOOFCK2/5rSGQ==}
peerDependencies:
react: '>=17'
react-dom: '>=17'
'@remixicon/react@4.9.0':
resolution: {integrity: sha512-5/jLDD4DtKxH2B4QVXTobvV1C2uL8ab9D5yAYNtFt+w80O0Ys1xFOrspqROL3fjrZi+7ElFUWE37hBfaAl6U+Q==}
peerDependencies:
@@ -4594,6 +4558,15 @@ packages:
'@xstate/fsm@1.6.5':
resolution: {integrity: sha512-b5o1I6aLNeYlU/3CPlj/Z91ybk1gUsKT+5NAJI+2W4UjvS5KLG28K9v5UvNoFVjHV8PajVZ00RH3vnjyQO7ZAw==}
'@xyflow/react@12.10.2':
resolution: {integrity: sha512-CgIi6HwlcHXwlkTpr0fxLv/0sRVNZ8IdwKLzzeCscaYBwpvfcH1QFOCeaTCuEn1FQEs/B8CjnTSjhs8udgmBgQ==}
peerDependencies:
react: '>=17'
react-dom: '>=17'
'@xyflow/system@0.0.76':
resolution: {integrity: sha512-hvwvnRS1B3REwVDlWexsq7YQaPZeG3/mKo1jv38UmnpWmxihp14bW6VtEOuHEwJX2FvzFw8k77LyKSk/wiZVNA==}
abcjs@6.6.3:
resolution: {integrity: sha512-BerGJCY8+pvJV1+VxZn1Y/VNcuSAk8BysCbBICY0W8fgE5g4W6sA/zB5pKxcgqzY5/gObh8ugl++4ZoaTqUCkw==}
@@ -7313,12 +7286,6 @@ packages:
resolution: {integrity: sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==}
engines: {node: '>=0.10.0'}
reactflow@11.11.4:
resolution: {integrity: sha512-70FOtJkUWH3BAOsN+LU9lCrKoKbtOPnz2uq0CV2PLdNSwxTXOhCbsZr50GmZ+Rtw3jx8Uv7/vBFtCGixLfd4Og==}
peerDependencies:
react: '>=17'
react-dom: '>=17'
read-package-up@12.0.0:
resolution: {integrity: sha512-Q5hMVBYur/eQNWDdbF4/Wqqr9Bjvtrw2kjGxxBbKLbx8bVCL8gcArjTy8zDUuLGQicftpMuU0riQNcAsbtOVsw==}
engines: {node: '>=20'}
@@ -10327,84 +10294,6 @@ snapshots:
optionalDependencies:
'@types/react': 19.2.14
'@reactflow/background@11.3.14(@types/react@19.2.14)(immer@11.1.8)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)':
dependencies:
'@reactflow/core': 11.11.4(@types/react@19.2.14)(immer@11.1.8)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
classcat: 5.0.5
react: 19.2.6
react-dom: 19.2.6(react@19.2.6)
zustand: 4.5.7(@types/react@19.2.14)(immer@11.1.8)(react@19.2.6)
transitivePeerDependencies:
- '@types/react'
- immer
'@reactflow/controls@11.2.14(@types/react@19.2.14)(immer@11.1.8)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)':
dependencies:
'@reactflow/core': 11.11.4(@types/react@19.2.14)(immer@11.1.8)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
classcat: 5.0.5
react: 19.2.6
react-dom: 19.2.6(react@19.2.6)
zustand: 4.5.7(@types/react@19.2.14)(immer@11.1.8)(react@19.2.6)
transitivePeerDependencies:
- '@types/react'
- immer
'@reactflow/core@11.11.4(@types/react@19.2.14)(immer@11.1.8)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)':
dependencies:
'@types/d3': 7.4.3
'@types/d3-drag': 3.0.7
'@types/d3-selection': 3.0.11
'@types/d3-zoom': 3.0.8
classcat: 5.0.5
d3-drag: 3.0.0
d3-selection: 3.0.0
d3-zoom: 3.0.0
react: 19.2.6
react-dom: 19.2.6(react@19.2.6)
zustand: 4.5.7(@types/react@19.2.14)(immer@11.1.8)(react@19.2.6)
transitivePeerDependencies:
- '@types/react'
- immer
'@reactflow/minimap@11.7.14(@types/react@19.2.14)(immer@11.1.8)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)':
dependencies:
'@reactflow/core': 11.11.4(@types/react@19.2.14)(immer@11.1.8)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
'@types/d3-selection': 3.0.11
'@types/d3-zoom': 3.0.8
classcat: 5.0.5
d3-selection: 3.0.0
d3-zoom: 3.0.0
react: 19.2.6
react-dom: 19.2.6(react@19.2.6)
zustand: 4.5.7(@types/react@19.2.14)(immer@11.1.8)(react@19.2.6)
transitivePeerDependencies:
- '@types/react'
- immer
'@reactflow/node-resizer@2.2.14(@types/react@19.2.14)(immer@11.1.8)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)':
dependencies:
'@reactflow/core': 11.11.4(@types/react@19.2.14)(immer@11.1.8)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
classcat: 5.0.5
d3-drag: 3.0.0
d3-selection: 3.0.0
react: 19.2.6
react-dom: 19.2.6(react@19.2.6)
zustand: 4.5.7(@types/react@19.2.14)(immer@11.1.8)(react@19.2.6)
transitivePeerDependencies:
- '@types/react'
- immer
'@reactflow/node-toolbar@1.3.14(@types/react@19.2.14)(immer@11.1.8)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)':
dependencies:
'@reactflow/core': 11.11.4(@types/react@19.2.14)(immer@11.1.8)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
classcat: 5.0.5
react: 19.2.6
react-dom: 19.2.6(react@19.2.6)
zustand: 4.5.7(@types/react@19.2.14)(immer@11.1.8)(react@19.2.6)
transitivePeerDependencies:
- '@types/react'
- immer
'@remixicon/react@4.9.0(react@19.2.6)':
dependencies:
react: 19.2.6
@@ -11677,6 +11566,29 @@ snapshots:
'@xstate/fsm@1.6.5': {}
'@xyflow/react@12.10.2(@types/react@19.2.14)(immer@11.1.8)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)':
dependencies:
'@xyflow/system': 0.0.76
classcat: 5.0.5
react: 19.2.6
react-dom: 19.2.6(react@19.2.6)
zustand: 4.5.7(@types/react@19.2.14)(immer@11.1.8)(react@19.2.6)
transitivePeerDependencies:
- '@types/react'
- immer
'@xyflow/system@0.0.76':
dependencies:
'@types/d3-drag': 3.0.7
'@types/d3-interpolate': 3.0.4
'@types/d3-selection': 3.0.11
'@types/d3-transition': 3.0.9
'@types/d3-zoom': 3.0.8
d3-drag: 3.0.0
d3-interpolate: 3.0.1
d3-selection: 3.0.0
d3-zoom: 3.0.0
abcjs@6.6.3: {}
acorn-jsx@5.3.2(acorn@8.16.0):
@@ -14958,20 +14870,6 @@ snapshots:
react@19.2.6: {}
reactflow@11.11.4(@types/react@19.2.14)(immer@11.1.8)(react-dom@19.2.6(react@19.2.6))(react@19.2.6):
dependencies:
'@reactflow/background': 11.3.14(@types/react@19.2.14)(immer@11.1.8)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
'@reactflow/controls': 11.2.14(@types/react@19.2.14)(immer@11.1.8)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
'@reactflow/core': 11.11.4(@types/react@19.2.14)(immer@11.1.8)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
'@reactflow/minimap': 11.7.14(@types/react@19.2.14)(immer@11.1.8)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
'@reactflow/node-resizer': 2.2.14(@types/react@19.2.14)(immer@11.1.8)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
'@reactflow/node-toolbar': 1.3.14(@types/react@19.2.14)(immer@11.1.8)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
react: 19.2.6
react-dom: 19.2.6(react@19.2.6)
transitivePeerDependencies:
- '@types/react'
- immer
read-package-up@12.0.0:
dependencies:
find-up-simple: 1.0.1
@@ -16261,6 +16159,7 @@ time:
'@vitest/coverage-v8@4.1.6': '2026-05-11T14:38:12.233Z'
'@voidzero-dev/vite-plus-core@0.1.20': '2026-04-29T03:08:39.629Z'
'@voidzero-dev/vite-plus-test@0.1.20': '2026-04-29T03:08:45.501Z'
'@xyflow/react@12.10.2': '2026-03-27T11:52:17.158Z'
abcjs@6.6.3: '2026-04-24T17:38:01.079Z'
agentation@3.0.2: '2026-03-25T16:24:19.682Z'
ahooks@3.9.7: '2026-03-23T15:49:13.605Z'
@@ -16339,7 +16238,6 @@ time:
react-sortablejs@6.1.4: '2022-05-31T07:19:03.552Z'
react-textarea-autosize@8.5.9: '2025-03-30T22:13:11.081Z'
react@19.2.6: '2026-05-06T16:16:47.653Z'
reactflow@11.11.4: '2024-06-20T11:31:29.797Z'
remark-breaks@4.0.0: '2023-09-22T16:45:41.061Z'
remark-directive@4.0.0: '2025-02-27T15:15:20.630Z'
scheduler@0.27.0: '2025-10-01T21:39:15.208Z'
+1 -1
View File
@@ -138,6 +138,7 @@ catalog:
'@vitejs/plugin-react': 6.0.1
'@vitejs/plugin-rsc': 0.5.26
'@vitest/coverage-v8': 4.1.6
'@xyflow/react': 12.10.2
abcjs: 6.6.3
agentation: 3.0.2
ahooks: 3.9.7
@@ -215,7 +216,6 @@ catalog:
react-server-dom-webpack: 19.2.6
react-sortablejs: 6.1.4
react-textarea-autosize: 8.5.9
reactflow: 11.11.4
remark-breaks: 4.0.0
remark-directive: 4.0.0
scheduler: 0.27.0
@@ -17,7 +17,7 @@ vi.mock('react-i18next', () => ({
// Mutable holder so mock data can reference BlockEnum after imports
const mockNodesHolder = vi.hoisted(() => ({ value: [] as Record<string, unknown>[] }))
vi.mock('reactflow', () => ({
vi.mock('@xyflow/react', () => ({
useNodes: () => mockNodesHolder.value,
}))
@@ -25,7 +25,7 @@ vi.mock('@/app/components/workflow/store')
// Mock ReactFlow store
const mockGetNodes = vi.fn()
vi.mock('reactflow', () => ({
vi.mock('@xyflow/react', () => ({
useStoreApi: () => ({
getState: () => ({
getNodes: mockGetNodes,
@@ -22,6 +22,7 @@ import { useSelector as useAppContextWithSelector } from '@/context/app-context'
import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail'
import { useRouter } from '@/next/navigation'
import { checkIsUsedInApp, deleteDataset } from '@/service/datasets'
import { parseResponseError } from '@/service/fetch'
import { datasetDetailQueryKeyPrefix, useInvalidDatasetList } from '@/service/knowledge/use-dataset'
import { useInvalid } from '@/service/use-base'
import { useExportPipelineDSL } from '@/service/use-pipeline'
@@ -34,22 +35,8 @@ type DropDownProps = {
expand: boolean
}
type JsonErrorResponse = {
json: () => Promise<{ message?: string }>
}
const isJsonErrorResponse = (error: unknown): error is JsonErrorResponse => {
return typeof error === 'object'
&& error !== null
&& 'json' in error
&& typeof error.json === 'function'
}
const getErrorMessage = async (error: unknown) => {
if (!isJsonErrorResponse(error))
return 'Unknown error'
const res = await error.json()
const res = await parseResponseError(error)
return res?.message || 'Unknown error'
}
@@ -2,8 +2,8 @@ import type { ReactNode } from 'react'
import type { InputFieldConfiguration } from '../types'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { render, screen } from '@testing-library/react'
import { ReactFlowProvider } from '@xyflow/react'
import { useMemo } from 'react'
import { ReactFlowProvider } from 'reactflow'
import { useAppForm } from '../../..'
import NodePanelField from '../field'
import { InputFieldType } from '../types'
@@ -1,11 +1,13 @@
import type { LexicalEditor } from 'lexical'
import type { UpdateWorkflowNodesMapPayload } from '../index'
import type { ValueSelector, Var } from '@/app/components/workflow/types'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import {
useLexicalComposerContext,
} from '@lexical/react/LexicalComposerContext'
import { mergeRegister } from '@lexical/utils'
import { act, render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { useReactFlow, useStoreApi } from 'reactflow'
import { useReactFlow, useStoreApi } from '@xyflow/react'
import { Type } from '@/app/components/workflow/nodes/llm/types'
import { BlockEnum, VarType } from '@/app/components/workflow/types'
import { useSelectOrDelete } from '../../../hooks'
@@ -21,7 +23,7 @@ const { mockVarLabel, mockIsExceptionVariable, mockForcedVariableKind } = vi.hoi
vi.mock('@lexical/react/LexicalComposerContext')
vi.mock('@lexical/utils')
vi.mock('reactflow')
vi.mock('@xyflow/react')
vi.mock('../../../hooks')
vi.mock('@/app/components/workflow/utils', async (importOriginal) => {
const actual = await importOriginal<typeof import('@/app/components/workflow/utils')>()
@@ -17,7 +17,7 @@ import {
useState,
} from 'react'
import { useTranslation } from 'react-i18next'
import { useReactFlow, useStoreApi } from 'reactflow'
import { useWorkflowReactFlow, useWorkflowStoreApi } from '@/app/components/workflow/hooks/use-workflow-reactflow'
import { isRagVariableVar, isSpecialVar, isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils'
import VarFullPathPanel from '@/app/components/workflow/nodes/_base/components/variable/var-full-path-panel'
import {
@@ -88,8 +88,8 @@ const WorkflowVariableBlockComponent = ({
return sourceNode.vars.some(v => v.variable === variables[1])
}, [localAvailableVariables, variables])
const reactflow = useReactFlow()
const store = useStoreApi()
const reactflow = useWorkflowReactFlow()
const store = useWorkflowStoreApi()
useEffect(() => {
if (!editor.hasNodes([WorkflowVariableBlockNode]))
@@ -1,5 +1,4 @@
import type { ReactNode } from 'react'
import type { Node } from 'reactflow'
import type {
CredentialFormSchema,
CredentialFormSchemaNumberInput,
@@ -9,9 +8,7 @@ import type {
CredentialFormSchemaTextInput,
FormValue,
} from '../declarations'
import type {
NodeOutPutVar,
} from '@/app/components/workflow/types'
import type { Node, NodeOutPutVar } from '@/app/components/workflow/types'
import { cn } from '@langgenius/dify-ui/cn'
import { Select, SelectContent, SelectItem, SelectItemIndicator, SelectItemText, SelectTrigger } from '@langgenius/dify-ui/select'
import { useCallback, useState } from 'react'
@@ -1,4 +1,3 @@
import type { Node } from 'reactflow'
import type {
CredentialFormSchema,
CredentialFormSchemaBase,
@@ -9,7 +8,7 @@ import type {
FormValue,
} from '../../declarations'
import type { AppSelectorValue } from '@/app/components/plugins/plugin-detail-panel/app-selector'
import type { NodeOutPutVar } from '@/app/components/workflow/types'
import type { Node, NodeOutPutVar } from '@/app/components/workflow/types'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { FormTypeEnum } from '../../declarations'
@@ -1,6 +1,5 @@
import type { Node } from 'reactflow'
import type { ToolValue } from '@/app/components/workflow/block-selector/types'
import type { NodeOutPutVar, ToolWithProvider } from '@/app/components/workflow/types'
import type { Node, NodeOutPutVar, ToolWithProvider } from '@/app/components/workflow/types'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
@@ -8,6 +7,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
// ==================== Imports (after mocks) ====================
import { MCPToolAvailabilityProvider } from '@/app/components/workflow/nodes/_base/components/mcp-tool-availability'
import { BlockEnum } from '@/app/components/workflow/types'
import MultipleToolSelector from '../index'
// ==================== Mock Setup ====================
@@ -175,7 +175,7 @@ const createNodeOutputVar = (overrides: Partial<NodeOutPutVar> = {}): NodeOutPut
const createNode = (overrides: Partial<Node> = {}): Node => ({
id: 'node-1',
position: { x: 0, y: 0 },
data: { title: 'Test Node' },
data: { title: 'Test Node', desc: '', type: BlockEnum.Start },
...overrides,
})
@@ -1,6 +1,5 @@
import type { Node } from 'reactflow'
import type { ToolValue } from '@/app/components/workflow/block-selector/types'
import type { NodeOutPutVar } from '@/app/components/workflow/types'
import type { Node, NodeOutPutVar } from '@/app/components/workflow/types'
import { cn } from '@langgenius/dify-ui/cn'
import {
RiAddLine,
@@ -1,9 +1,8 @@
import type { ReactNode } from 'react'
import type { Node } from 'reactflow'
import type { Collection } from '@/app/components/tools/types'
import type { ToolDefaultValue, ToolValue } from '@/app/components/workflow/block-selector/types'
import type { SchemaRoot } from '@/app/components/workflow/nodes/llm/types'
import type { NodeOutPutVar, ToolWithProvider } from '@/app/components/workflow/types'
import type { Node, NodeOutPutVar, ToolWithProvider } from '@/app/components/workflow/types'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { act, fireEvent, render, renderHook, screen, waitFor } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
@@ -1,7 +1,6 @@
import type { Node } from 'reactflow'
import type { CredentialFormSchema } from '@/app/components/header/account-setting/model-provider-page/declarations'
import type { ToolFormSchema } from '@/app/components/tools/utils/to-form-schema'
import type { NodeOutPutVar, ValueSelector, Var } from '@/app/components/workflow/types'
import type { Node, NodeOutPutVar, ValueSelector, Var } from '@/app/components/workflow/types'
import { produce } from 'immer'
import { FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { VarType as VarKindType } from '@/app/components/workflow/nodes/tool/types'
@@ -1,8 +1,8 @@
import type { Node } from 'reactflow'
import type { ReasoningConfigValue as ReasoningConfigValueShape } from './reasoning-config-form.helpers'
import type { ToolFormSchema } from '@/app/components/tools/utils/to-form-schema'
import type { SchemaRoot } from '@/app/components/workflow/nodes/llm/types'
import type {
Node,
NodeOutPutVar,
ValueSelector,
} from '@/app/components/workflow/types'
@@ -1,13 +1,12 @@
'use client'
import type { FC } from 'react'
import type { Node } from 'reactflow'
import type { TabType } from '../hooks/use-tool-selector-state'
import type { ReasoningConfigValue } from './reasoning-config-form'
import type { CredentialFormSchema } from '@/app/components/header/account-setting/model-provider-page/declarations'
import type { ToolFormSchema } from '@/app/components/tools/utils/to-form-schema'
import type { ToolValue } from '@/app/components/workflow/block-selector/types'
import type { ToolVarInputs } from '@/app/components/workflow/nodes/tool/types'
import type { NodeOutPutVar, ToolWithProvider } from '@/app/components/workflow/types'
import type { Node, NodeOutPutVar, ToolWithProvider } from '@/app/components/workflow/types'
import { useTranslation } from 'react-i18next'
import Divider from '@/app/components/base/divider'
import TabSlider from '@/app/components/base/tab-slider-plain'
@@ -2,9 +2,8 @@
import type { OffsetOptions } from '@floating-ui/react'
import type { Placement } from '@langgenius/dify-ui/popover'
import type { FC } from 'react'
import type { Node } from 'reactflow'
import type { ToolValue } from '@/app/components/workflow/block-selector/types'
import type { NodeOutPutVar } from '@/app/components/workflow/types'
import type { Node, NodeOutPutVar } from '@/app/components/workflow/types'
import { cn } from '@langgenius/dify-ui/cn'
import {
Popover,
@@ -1,5 +1,6 @@
import type { Viewport } from '@xyflow/react'
import type { PropsWithChildren } from 'react'
import type { Edge, Node, Viewport } from 'reactflow'
import type { Edge, Node } from '@/app/components/workflow/types'
import { cleanup, render, screen } from '@testing-library/react'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import RagPipelineMain from '../rag-pipeline-main'
@@ -3,19 +3,19 @@ import { render, screen, waitFor } from '@testing-library/react'
import * as React from 'react'
import RagPipelinePanel from '../index'
vi.mock('reactflow', () => ({
vi.mock('@xyflow/react', () => ({
useNodes: () => [],
useStoreApi: () => ({
getState: () => ({
getNodes: () => [],
nodes: [],
}),
}),
useReactFlow: () => ({
getNodes: () => [],
nodes: [],
}),
useStore: (selector: (state: Record<string, unknown>) => unknown) => {
const state = {
getNodes: () => [],
nodes: [],
}
return selector(state)
},
@@ -3,9 +3,9 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { useFloatingRight } from '../hooks'
const mockGetNodes = vi.fn()
vi.mock('reactflow', () => ({
useStore: (selector: (s: { getNodes: () => { id: string, data: { selected: boolean } }[] }) => unknown) => {
return selector({ getNodes: mockGetNodes })
vi.mock('@xyflow/react', () => ({
useStore: (selector: (s: { nodes: { id: string, data: { selected: boolean } }[] }) => unknown) => {
return selector({ nodes: mockGetNodes() })
},
}))
@@ -8,7 +8,7 @@ import { PipelineInputVarType } from '@/models/pipeline'
import InputFieldPanel from '../index'
let mockNodesData: Node<DataSourceNodeType>[] = []
vi.mock('reactflow', () => ({
vi.mock('@xyflow/react', () => ({
useNodes: () => mockNodesData,
}))
@@ -1,5 +1,10 @@
import { useEffect, useMemo, useState } from 'react'
import { useStore as useReactflow } from 'reactflow'
import { useStore as useReactflow,
} from '@xyflow/react'
import {
useEffect,
useMemo,
useState,
} from 'react'
import { useShallow } from 'zustand/react/shallow'
import { useStore } from '@/app/components/workflow/store'
@@ -10,7 +15,7 @@ export const useFloatingRight = (targetElementWidth: number) => {
const otherPanelWidth = useStore(state => state.otherPanelWidth)
const selectedNodeId = useReactflow(useShallow((s) => {
const nodes = s.getNodes()
const { nodes } = s
const currentNode = nodes.find(node => node.data.selected)
if (currentNode)
@@ -1,7 +1,9 @@
import type { DataSourceNodeType } from '@/app/components/workflow/nodes/data-source/types'
import type { Node } from '@/app/components/workflow/types'
import type { InputVar, RAGPipelineVariables } from '@/models/pipeline'
import { Button } from '@langgenius/dify-ui/button'
import {
Button,
} from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import { RiCloseLine, RiEyeLine } from '@remixicon/react'
import {
@@ -11,11 +13,11 @@ import {
useRef,
} from 'react'
import { useTranslation } from 'react-i18next'
import { useNodes } from 'reactflow'
import Divider from '@/app/components/base/divider'
import { Infotip } from '@/app/components/base/infotip'
import { useInputFieldPanel } from '@/app/components/rag-pipeline/hooks'
import { useNodesSyncDraft } from '@/app/components/workflow/hooks'
import { useWorkflowFlowNodes } from '@/app/components/workflow/hooks/use-workflow-reactflow'
import { useStore } from '@/app/components/workflow/store'
import { BlockEnum } from '@/app/components/workflow/types'
import FieldList from './field-list'
@@ -25,7 +27,7 @@ import GlobalInputs from './label-right-content/global-inputs'
const InputFieldPanel = () => {
const { t } = useTranslation()
const nodes = useNodes<DataSourceNodeType>()
const nodes = useWorkflowFlowNodes<DataSourceNodeType>()
const {
closeAllInputFieldPanels,
toggleInputFieldPreviewPanel,
@@ -49,7 +49,7 @@ vi.mock('@/app/components/workflow/store', () => ({
}),
}))
vi.mock('reactflow', () => ({
vi.mock('@xyflow/react', () => ({
useStore: () => undefined,
}))
@@ -6,7 +6,7 @@ import { BlockEnum } from '@/app/components/workflow/types'
import { useDatasourceOptions, useOnlineDocument, useOnlineDrive, useTestRunSteps, useWebsiteCrawl } from '../hooks'
const mockNodes: Array<{ id: string, data: Partial<DataSourceNodeType> & { type: string } }> = []
vi.mock('reactflow', () => ({
vi.mock('@xyflow/react', () => ({
useNodes: () => mockNodes,
}))
@@ -30,7 +30,7 @@ const createNodeData = (overrides?: Partial<DataSourceNodeType>): DataSourceNode
...overrides,
} as unknown as DataSourceNodeType)
vi.mock('reactflow', () => ({
vi.mock('@xyflow/react', () => ({
useNodes: () => mockNodes,
}))
@@ -1,9 +1,13 @@
import type { DataSourceOption } from '../types'
import type { DataSourceNodeType } from '@/app/components/workflow/nodes/data-source/types'
import { useCallback, useMemo, useState } from 'react'
import {
useCallback,
useMemo,
useState,
} from 'react'
import { useTranslation } from 'react-i18next'
import { useNodes } from 'reactflow'
import { useDataSourceStore } from '@/app/components/datasets/documents/create-from-pipeline/data-source/store'
import { useWorkflowFlowNodes } from '@/app/components/workflow/hooks/use-workflow-reactflow'
import { BlockEnum } from '@/app/components/workflow/types'
import { CrawlStep } from '@/models/datasets'
import { TestRunStep } from '../types'
@@ -40,7 +44,7 @@ export const useTestRunSteps = () => {
}
export const useDatasourceOptions = () => {
const nodes = useNodes<DataSourceNodeType>()
const nodes = useWorkflowFlowNodes<DataSourceNodeType>()
const datasourceNodes = nodes.filter(node => node.data.type === BlockEnum.DataSource)
const options = useMemo(() => {
@@ -2,12 +2,14 @@ import { renderHook } from '@testing-library/react'
import { act } from 'react'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { CUSTOM_NOTE_NODE } from '@/app/components/workflow/note-node/constants'
import { BlockEnum } from '@/app/components/workflow/types'
import { useNodesSyncDraft } from '../use-nodes-sync-draft'
const mockGetNodes = vi.fn()
const mockStoreGetState = vi.fn()
vi.mock('reactflow', () => ({
vi.mock('@xyflow/react', () => ({
useStoreApi: () => ({
getState: mockStoreGetState,
}),
@@ -53,8 +55,24 @@ vi.mock('@/config', () => ({
API_PREFIX: '/api',
}))
const mockPostWithKeepalive = vi.fn()
const {
mockPostWithKeepalive,
mockParseResponseError,
} = vi.hoisted(() => ({
mockPostWithKeepalive: vi.fn(),
mockParseResponseError: vi.fn(async (error: { bodyUsed?: boolean, json?: () => Promise<unknown> }) => {
if (error.bodyUsed || !error.json)
return null
try {
return await error.json()
}
catch {
return null
}
}),
}))
vi.mock('@/service/fetch', () => ({
parseResponseError: mockParseResponseError,
postWithKeepalive: (...args: unknown[]) => mockPostWithKeepalive(...args),
}))
@@ -62,16 +80,15 @@ describe('useNodesSyncDraft', () => {
beforeEach(() => {
vi.clearAllMocks()
mockStoreGetState.mockReturnValue({
getNodes: mockGetNodes,
edges: [],
transform: [0, 0, 1],
})
mockGetNodes.mockReturnValue([
{ id: 'node-1', data: { type: 'start', _temp: true }, position: { x: 0, y: 0 } },
{ id: 'node-2', data: { type: 'end' }, position: { x: 100, y: 0 } },
])
mockStoreGetState.mockImplementation(() => ({
nodes: mockGetNodes(),
edges: [],
transform: [0, 0, 1],
}))
mockWorkflowStoreGetState.mockReturnValue({
pipelineId: 'test-pipeline-id',
@@ -201,6 +218,51 @@ describe('useNodesSyncDraft', () => {
const sentParams = mockPostWithKeepalive.mock.calls[0]![1]
expect(sentParams.graph.nodes[0].data._privateData).toBeUndefined()
})
it('should strip fixed dimensions from normal measured nodes before keepalive sync', () => {
mockGetNodes.mockReturnValue([
{
id: 'normal-node',
width: 240,
height: 120,
data: { type: BlockEnum.Code },
position: { x: 0, y: 0 },
},
{
id: 'loop-node',
width: 360,
height: 240,
data: { type: BlockEnum.Loop, width: 360, height: 240 },
position: { x: 100, y: 0 },
},
{
id: 'note-node',
type: CUSTOM_NOTE_NODE,
width: 240,
height: 88,
data: { type: '' as BlockEnum, width: 240, height: 88 },
position: { x: 200, y: 0 },
},
])
const { result } = renderHook(() => useNodesSyncDraft())
act(() => {
result.current.syncWorkflowDraftWhenPageClose()
})
const syncedNodes = mockPostWithKeepalive.mock.calls[0]![1].graph.nodes
expect(syncedNodes.find((node: { id: string }) => node.id === 'normal-node')).not.toHaveProperty('width')
expect(syncedNodes.find((node: { id: string }) => node.id === 'normal-node')).not.toHaveProperty('height')
expect(syncedNodes.find((node: { id: string }) => node.id === 'loop-node')).toMatchObject({
width: 360,
height: 240,
})
expect(syncedNodes.find((node: { id: string }) => node.id === 'note-node')).toMatchObject({
width: 240,
height: 88,
})
})
})
describe('doSyncWorkflowDraft', () => {
@@ -376,7 +438,7 @@ describe('useNodesSyncDraft', () => {
describe('getPostParams', () => {
it('should include viewport coordinates in params', () => {
mockStoreGetState.mockReturnValue({
getNodes: mockGetNodes,
nodes: mockGetNodes(),
edges: [],
transform: [100, 200, 1.5],
})
@@ -457,7 +519,7 @@ describe('useNodesSyncDraft', () => {
it('should remove underscore-prefixed keys from edges', () => {
mockStoreGetState.mockReturnValue({
getNodes: mockGetNodes,
nodes: mockGetNodes(),
edges: [{ id: 'edge-1', source: 'node-1', target: 'node-2', data: { _hidden: true, visible: false } }],
transform: [0, 0, 1],
})
@@ -8,7 +8,7 @@ import { usePipelineRun } from '../use-pipeline-run'
const mockStoreGetState = vi.fn()
const mockGetViewport = vi.fn()
vi.mock('reactflow', () => ({
vi.mock('@xyflow/react', () => ({
useStoreApi: () => ({
getState: mockStoreGetState,
}),
@@ -109,7 +109,7 @@ describe('usePipelineRun', () => {
document.body.appendChild(mockWorkflowContainer)
mockStoreGetState.mockReturnValue({
getNodes: mockGetNodes,
nodes: mockGetNodes(),
setNodes: mockSetNodes,
edges: [],
})
@@ -7,10 +7,10 @@ const mockGetNodes = vi.fn()
const mockSetNodes = vi.fn()
const mockEdges: Array<{ id: string, source: string, target: string }> = []
vi.mock('reactflow', () => ({
vi.mock('@xyflow/react', () => ({
useStoreApi: () => ({
getState: () => ({
getNodes: mockGetNodes,
nodes: mockGetNodes(),
setNodes: mockSetNodes,
edges: mockEdges,
}),
@@ -1,33 +1,33 @@
import type { SyncDraftCallback } from '@/app/components/workflow/hooks-store'
import { produce } from 'immer'
import { useCallback } from 'react'
import { useStoreApi } from 'reactflow'
import { useSerialAsyncCallback } from '@/app/components/workflow/hooks/use-serial-async-callback'
import {
useNodesReadOnly,
} from '@/app/components/workflow/hooks/use-workflow'
import { useWorkflowStoreApi } from '@/app/components/workflow/hooks/use-workflow-reactflow'
import {
useWorkflowStore,
} from '@/app/components/workflow/store'
import { normalizeNodeDimensionsForReactFlowV12 } from '@/app/components/workflow/utils/workflow-init'
import { API_PREFIX } from '@/config'
import { postWithKeepalive } from '@/service/fetch'
import { parseResponseError, postWithKeepalive } from '@/service/fetch'
import { syncWorkflowDraft } from '@/service/workflow'
import { usePipelineRefreshDraft } from '.'
export const useNodesSyncDraft = () => {
const store = useStoreApi()
const store = useWorkflowStoreApi()
const workflowStore = useWorkflowStore()
const { getNodesReadOnly } = useNodesReadOnly()
const { handleRefreshWorkflowDraft } = usePipelineRefreshDraft()
const getPostParams = useCallback(() => {
const {
getNodes,
nodes,
edges,
transform,
} = store.getState()
const nodesOriginal = getNodes()
const nodes = nodesOriginal.filter(node => !node.data._isTempNode)
const validNodes = nodes.filter(node => !node.data._isTempNode)
const [x, y, zoom] = transform
const {
pipelineId,
@@ -36,9 +36,10 @@ export const useNodesSyncDraft = () => {
ragPipelineVariables,
} = workflowStore.getState()
if (pipelineId && !!nodes.length) {
const producedNodes = produce(nodes, (draft) => {
if (pipelineId && !!validNodes.length) {
const producedNodes = produce(validNodes, (draft) => {
draft.forEach((node) => {
normalizeNodeDimensionsForReactFlowV12(node)
Object.keys(node.data).forEach((key) => {
if (key.startsWith('_'))
delete node.data[key]
@@ -47,9 +48,10 @@ export const useNodesSyncDraft = () => {
})
const producedEdges = produce(edges, (draft) => {
draft.forEach((edge) => {
Object.keys(edge.data).forEach((key) => {
const data = edge.data as typeof edge.data & Record<string, unknown>
Object.keys(data).forEach((key) => {
if (key.startsWith('_'))
delete edge.data[key]
delete data[key]
})
})
})
@@ -102,12 +104,9 @@ export const useNodesSyncDraft = () => {
callback?.onSuccess?.()
}
catch (error: any) {
if (error && error.json && !error.bodyUsed) {
error.json().then((err: any) => {
if (err.code === 'draft_workflow_not_sync' && !notRefreshWhenSyncError)
handleRefreshWorkflowDraft()
})
}
const err = await parseResponseError(error)
if (err?.code === 'draft_workflow_not_sync' && !notRefreshWhenSyncError)
handleRefreshWorkflowDraft()
callback?.onError?.()
}
finally {
@@ -8,6 +8,7 @@ import {
useWorkflowStore,
} from '@/app/components/workflow/store'
import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail'
import { parseResponseError } from '@/service/fetch'
import {
fetchWorkflowDraft,
syncWorkflowDraft,
@@ -57,28 +58,25 @@ export const usePipelineInit = () => {
setIsLoading(false)
}
catch (error: any) {
if (error && error.json && !error.bodyUsed && datasetId) {
error.json().then((err: any) => {
if (err.code === 'draft_workflow_not_exist') {
workflowStore.setState({
notInitialWorkflow: true,
shouldAutoOpenStartNodeSelector: true,
})
syncWorkflowDraft({
url: `/rag/pipelines/${datasetId}/workflows/draft`,
params: {
graph: {
nodes: nodesTemplate,
edges: edgesTemplate,
},
environment_variables: [],
},
}).then((res) => {
const { setDraftUpdatedAt } = workflowStore.getState()
setDraftUpdatedAt(res.updated_at)
handleGetInitialWorkflowData()
})
}
const err = await parseResponseError(error)
if (err?.code === 'draft_workflow_not_exist' && datasetId) {
workflowStore.setState({
notInitialWorkflow: true,
shouldAutoOpenStartNodeSelector: true,
})
syncWorkflowDraft({
url: `/rag/pipelines/${datasetId}/workflows/draft`,
params: {
graph: {
nodes: nodesTemplate,
edges: edgesTemplate,
},
environment_variables: [],
},
}).then((res) => {
const { setDraftUpdatedAt } = workflowStore.getState()
setDraftUpdatedAt(res.updated_at)
handleGetInitialWorkflowData()
})
}
}
@@ -2,12 +2,9 @@ import type { IOtherOptions } from '@/service/base'
import type { VersionHistory } from '@/types/workflow'
import { produce } from 'immer'
import { useCallback, useRef } from 'react'
import {
useReactFlow,
useStoreApi,
} from 'reactflow'
import { useSetWorkflowVarsWithValue } from '@/app/components/workflow/hooks/use-fetch-workflow-inspect-vars'
import { useWorkflowUpdate } from '@/app/components/workflow/hooks/use-workflow-interactions'
import { useWorkflowReactFlow, useWorkflowStoreApi } from '@/app/components/workflow/hooks/use-workflow-reactflow'
import { useWorkflowRunEvent } from '@/app/components/workflow/hooks/use-workflow-run-event/use-workflow-run-event'
import { useStore, useWorkflowStore } from '@/app/components/workflow/store'
import { WorkflowRunningStatus } from '@/app/components/workflow/types'
@@ -18,9 +15,9 @@ import { FlowType } from '@/types/common'
import { useNodesSyncDraft } from './use-nodes-sync-draft'
export const usePipelineRun = () => {
const store = useStoreApi()
const store = useWorkflowStoreApi()
const workflowStore = useWorkflowStore()
const reactflow = useReactFlow()
const reactflow = useWorkflowReactFlow()
const { doSyncWorkflowDraft } = useNodesSyncDraft()
const { handleUpdateWorkflowCanvas } = useWorkflowUpdate()
@@ -46,7 +43,7 @@ export const usePipelineRun = () => {
const handleBackupDraft = useCallback(() => {
const {
getNodes,
nodes,
edges,
} = store.getState()
const { getViewport } = reactflow
@@ -58,7 +55,7 @@ export const usePipelineRun = () => {
if (!backupDraft) {
setBackupDraft({
nodes: getNodes(),
nodes,
edges,
viewport: getViewport(),
environmentVariables,
@@ -104,10 +101,10 @@ export const usePipelineRun = () => {
callback?: IOtherOptions,
) => {
const {
getNodes,
nodes,
setNodes,
} = store.getState()
const newNodes = produce(getNodes(), (draft) => {
const newNodes = produce(nodes, (draft) => {
draft.forEach((node) => {
node.data.selected = false
node.data._runningStatus = undefined
@@ -1,30 +1,29 @@
import type { DataSourceNodeType } from '../../workflow/nodes/data-source/types'
import type { Node, ValueSelector } from '../../workflow/types'
import { getOutgoers } from '@xyflow/react'
import { uniqBy } from 'es-toolkit/compat'
import { useCallback } from 'react'
import { getOutgoers, useStoreApi } from 'reactflow'
import { useWorkflowStoreApi } from '@/app/components/workflow/hooks/use-workflow-reactflow'
import { findUsedVarNodes, updateNodeVars } from '../../workflow/nodes/_base/components/variable/utils'
import { BlockEnum } from '../../workflow/types'
export const usePipeline = () => {
const store = useStoreApi()
const store = useWorkflowStoreApi()
const getAllDatasourceNodes = useCallback(() => {
const {
getNodes,
nodes,
} = store.getState()
const nodes = getNodes() as Node<DataSourceNodeType>[]
const datasourceNodes = nodes.filter(node => node.data.type === BlockEnum.DataSource)
const datasourceNodes = (nodes as Node<DataSourceNodeType>[]).filter(node => node.data.type === BlockEnum.DataSource)
return datasourceNodes
}, [store])
const getAllNodesInSameBranch = useCallback((nodeId: string) => {
const {
getNodes,
nodes,
edges,
} = store.getState()
const nodes = getNodes()
const list: Node[] = []
const traverse = (root: Node, callback: (node: Node) => void) => {
@@ -78,11 +77,11 @@ export const usePipeline = () => {
}, [getAllNodesInSameBranch])
const handleInputVarRename = useCallback((nodeId: string, oldValeSelector: ValueSelector, newVarSelector: ValueSelector) => {
const { getNodes, setNodes } = store.getState()
const { nodes, setNodes } = store.getState()
const afterNodes = getAllNodesInSameBranch(nodeId)
const effectNodes = findUsedVarNodes(oldValeSelector, afterNodes)
if (effectNodes.length > 0) {
const newNodes = getNodes().map((node) => {
const newNodes = nodes.map((node) => {
if (effectNodes.find(n => n.id === node.id))
return updateNodeVars(node, oldValeSelector, newVarSelector)
@@ -94,11 +93,11 @@ export const usePipeline = () => {
const removeUsedVarInNodes = useCallback((varSelector: ValueSelector) => {
const nodeId = varSelector[1] // Assuming the first element is always 'VARIABLE_PREFIX'(rag)
const { getNodes, setNodes } = store.getState()
const { nodes, setNodes } = store.getState()
const afterNodes = getAllNodesInSameBranch(nodeId!)
const effectNodes = findUsedVarNodes(varSelector, afterNodes)
if (effectNodes.length > 0) {
const newNodes = getNodes().map((node) => {
const newNodes = nodes.map((node) => {
if (effectNodes.find(n => n.id === node.id))
return updateNodeVars(node, varSelector, [])
@@ -1,4 +1,4 @@
import type { Viewport } from 'reactflow'
import type { Viewport } from '@xyflow/react'
import type { Node } from '@/app/components/workflow/types'
import { describe, expect, it, vi } from 'vitest'
import { BlockEnum } from '@/app/components/workflow/types'
@@ -1,4 +1,4 @@
import type { Viewport } from 'reactflow'
import type { Viewport } from '@xyflow/react'
import type { Node } from '@/app/components/workflow/types'
import { BlockEnum } from '@/app/components/workflow/types'
import { processNodesWithoutDataSource } from '../nodes'
@@ -1,4 +1,4 @@
import type { Viewport } from 'reactflow'
import type { Viewport } from '@xyflow/react'
import type { NoteNodeType } from '@/app/components/workflow/note-node/types'
import type { Node } from '@/app/components/workflow/types'
import {
@@ -48,7 +48,7 @@ let workflowStoreState: WorkflowStoreState
let eventSubscription: ((value: { type: string, payload: { data: Array<Record<string, unknown>> } }) => void) | null = null
let lastGenerateNodeInput: Record<string, unknown> | null = null
vi.mock('reactflow', () => ({
vi.mock('@xyflow/react', () => ({
useStoreApi: () => ({
getState: () => ({
setNodes: mockSetNodes,
@@ -91,11 +91,11 @@ vi.mock('@/app/components/workflow/store', () => ({
}),
}))
vi.mock('reactflow', () => ({
vi.mock('@xyflow/react', () => ({
useReactFlow: () => ({
getNodes: () => [],
nodes: [],
setNodes: vi.fn(),
getEdges: () => [],
edges: [],
setEdges: vi.fn(),
}),
}))
@@ -8,7 +8,6 @@ import {
useCallback,
useState,
} from 'react'
import { useStoreApi } from 'reactflow'
import { DSL_EXPORT_CHECK, START_INITIAL_POSITION } from '@/app/components/workflow/constants'
import {
useAutoGenerateWebhookUrl,
@@ -16,6 +15,7 @@ import {
usePanelInteractions,
} from '@/app/components/workflow/hooks'
import { useNodesSyncDraft } from '@/app/components/workflow/hooks/use-nodes-sync-draft'
import { useWorkflowStoreApi } from '@/app/components/workflow/hooks/use-workflow-reactflow'
import { useStore } from '@/app/components/workflow/store'
import { BlockEnum } from '@/app/components/workflow/types'
import { generateNewNode } from '@/app/components/workflow/utils'
@@ -75,7 +75,7 @@ const WorkflowChildren = () => {
const setShowOnboarding = useStore(s => s.setShowOnboarding)
const setHasSelectedStartNode = useStore(s => s.setHasSelectedStartNode)
const setShouldAutoOpenStartNodeSelector = useStore(s => s.setShouldAutoOpenStartNodeSelector)
const reactFlowStore = useStoreApi()
const reactFlowStore = useWorkflowStoreApi()
const availableNodesMetaData = useAvailableNodesMetaData()
const { handleSyncWorkflowDraft } = useNodesSyncDraft()
const { handleOnboardingClose } = useAutoOnboarding()
@@ -96,7 +96,7 @@ vi.mock('@/app/components/workflow/store/workflow/use-nodes', () => ({
default: () => mockUseNodes(),
}))
vi.mock('reactflow', () => ({
vi.mock('@xyflow/react', () => ({
useEdges: () => mockUseEdges(),
}))
@@ -2,11 +2,12 @@ import type { ModelAndParameter } from '@/app/components/app/configuration/debug
import type { EndNodeType } from '@/app/components/workflow/nodes/end/types'
import type { StartNodeType } from '@/app/components/workflow/nodes/start/types'
import type {
CommonEdgeType,
Node,
} from '@/app/components/workflow/types'
import type { PublishWorkflowParams } from '@/types/workflow'
import { Button } from '@langgenius/dify-ui/button'
import {
Button,
} from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import { toast } from '@langgenius/dify-ui/toast'
import { RiApps2AddLine } from '@remixicon/react'
@@ -16,7 +17,6 @@ import {
useMemo,
} from 'react'
import { useTranslation } from 'react-i18next'
import { useEdges } from 'reactflow'
import AppPublisher from '@/app/components/app/app-publisher'
import { useStore as useAppStore } from '@/app/components/app/store'
import { useFeatures } from '@/app/components/base/features/hooks'
@@ -29,6 +29,7 @@ import {
useNodesSyncDraft,
// useWorkflowRunValidation,
} from '@/app/components/workflow/hooks'
import { useWorkflowFlowEdges } from '@/app/components/workflow/hooks/use-workflow-reactflow'
import {
useStore,
useWorkflowStore,
@@ -65,7 +66,7 @@ const FeaturesTrigger = () => {
const startNode = nodes.find(node => node.data.type === BlockEnum.Start)
const endNode = nodes.find(node => node.data.type === BlockEnum.End)
const startVariables = (startNode as Node<StartNodeType>)?.data?.variables
const edges = useEdges<CommonEdgeType>()
const edges = useWorkflowFlowEdges()
const fileSettings = useFeatures(s => s.features.file)
const variables = useMemo(() => {
@@ -10,13 +10,13 @@ import {
useMemo,
useRef,
} from 'react'
import { useReactFlow } from 'reactflow'
import { useFeaturesStore } from '@/app/components/base/features/hooks'
import { FILE_EXTS } from '@/app/components/base/prompt-editor/constants'
import { WorkflowWithInnerContext } from '@/app/components/workflow'
import { collaborationManager } from '@/app/components/workflow/collaboration/core/collaboration-manager'
import { useCollaboration } from '@/app/components/workflow/collaboration/hooks/use-collaboration'
import { useWorkflowUpdate } from '@/app/components/workflow/hooks/use-workflow-interactions'
import { useWorkflowReactFlow } from '@/app/components/workflow/hooks/use-workflow-reactflow'
import { useStore, useWorkflowStore } from '@/app/components/workflow/store'
import { SupportUploadFileTypes } from '@/app/components/workflow/types'
import { fetchWorkflowDraft } from '@/service/workflow'
@@ -45,13 +45,13 @@ const WorkflowMain = ({
const workflowStore = useWorkflowStore()
const appId = useStore(s => s.appId)
const containerRef = useRef<HTMLDivElement>(null)
const reactFlow = useReactFlow()
const reactFlow = useWorkflowReactFlow()
const reactFlowStore = useMemo(() => ({
getState: () => ({
getNodes: () => reactFlow.getNodes(),
nodes: reactFlow.getNodes(),
setNodes: (nodesToSet: Node[]) => reactFlow.setNodes(nodesToSet),
getEdges: () => reactFlow.getEdges(),
edges: reactFlow.getEdges(),
setEdges: (edgesToSet: Edge[]) => reactFlow.setEdges(edgesToSet),
}),
}), [reactFlow])
@@ -11,10 +11,10 @@ const mockSetHasShownOnboarding = vi.fn()
const mockSetShouldAutoOpenStartNodeSelector = vi.fn()
const mockSetHasSelectedStartNode = vi.fn()
vi.mock('reactflow', () => ({
vi.mock('@xyflow/react', () => ({
useStoreApi: () => ({
getState: () => ({
getNodes: mockGetNodes,
nodes: mockGetNodes(),
}),
}),
}))
@@ -2,6 +2,8 @@ import { act } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { renderHookWithSystemFeatures } from '@/__tests__/utils/mock-system-features'
import { CUSTOM_NOTE_NODE } from '@/app/components/workflow/note-node/constants'
import { BlockEnum } from '@/app/components/workflow/types'
import { useNodesSyncDraft } from '../use-nodes-sync-draft'
const mockGetNodes = vi.fn()
@@ -15,7 +17,7 @@ const mockCollaborationEmitSyncRequest = vi.fn()
let isCollaborationEnabled = false
let reactFlowState: {
getNodes: typeof mockGetNodes
nodes: Array<Record<string, unknown>>
edges: Array<Record<string, unknown>>
transform: [number, number, number]
}
@@ -42,7 +44,7 @@ let featuresState: {
}
}
vi.mock('reactflow', () => ({
vi.mock('@xyflow/react', () => ({
useStoreApi: () => ({ getState: () => reactFlowState }),
}))
@@ -109,7 +111,9 @@ describe('useNodesSyncDraft — handleRefreshWorkflowDraft(true) on 409', () =>
beforeEach(() => {
vi.clearAllMocks()
reactFlowState = {
getNodes: mockGetNodes,
get nodes() {
return mockGetNodes()
},
edges: [],
transform: [0, 0, 1],
}
@@ -197,6 +201,9 @@ describe('useNodesSyncDraft — handleRefreshWorkflowDraft(true) on 409', () =>
it('should strip temp entities and private data, use the latest hash, and invoke success callbacks', async () => {
reactFlowState = {
...reactFlowState,
get nodes() {
return mockGetNodes()
},
edges: [
{ id: 'edge-1', source: 'n1', target: 'n2', data: { _isTemp: false, _private: 'drop', stable: 'keep' } },
{ id: 'temp-edge', source: 'n2', target: 'n3', data: { _isTemp: true } },
@@ -267,9 +274,57 @@ describe('useNodesSyncDraft — handleRefreshWorkflowDraft(true) on 409', () =>
expect(callbacks.onSettled).toHaveBeenCalled()
})
it('should strip fixed dimensions from normal measured nodes before draft sync', async () => {
mockGetNodes.mockReturnValue([
{
id: 'normal-node',
width: 240,
height: 120,
position: { x: 0, y: 0 },
data: { type: BlockEnum.Code, title: 'Code' },
},
{
id: 'iteration-node',
width: 360,
height: 240,
position: { x: 100, y: 0 },
data: { type: BlockEnum.Iteration, title: 'Iteration', width: 360, height: 240 },
},
{
id: 'note-node',
type: CUSTOM_NOTE_NODE,
width: 240,
height: 88,
position: { x: 200, y: 0 },
data: { type: '' as BlockEnum, title: 'Note', width: 240, height: 88 },
},
])
const { result } = renderUseNodesSyncDraft()
await act(async () => {
await result.current.doSyncWorkflowDraft(false)
})
const syncedNodes = mockSyncWorkflowDraft.mock.calls[0]![0].params.graph.nodes
expect(syncedNodes.find((node: { id: string }) => node.id === 'normal-node')).not.toHaveProperty('width')
expect(syncedNodes.find((node: { id: string }) => node.id === 'normal-node')).not.toHaveProperty('height')
expect(syncedNodes.find((node: { id: string }) => node.id === 'iteration-node')).toMatchObject({
width: 360,
height: 240,
})
expect(syncedNodes.find((node: { id: string }) => node.id === 'note-node')).toMatchObject({
width: 240,
height: 88,
})
})
it('should post workflow draft with keepalive when the page closes', () => {
reactFlowState = {
...reactFlowState,
get nodes() {
return mockGetNodes()
},
transform: [1, 2, 3],
}
workflowStoreState = {
@@ -328,4 +383,25 @@ describe('useNodesSyncDraft — handleRefreshWorkflowDraft(true) on 409', () =>
expect(mockPostWithKeepalive).not.toHaveBeenCalled()
})
it('should ignore non-JSON upstream errors without creating an unhandled rejection', async () => {
const callbacks = {
onError: vi.fn(),
onSettled: vi.fn(),
}
const error = new Response('Upstream proxy error', {
status: 502,
headers: { 'Content-Type': 'text/plain' },
})
mockSyncWorkflowDraft.mockRejectedValue(error)
const { result } = renderUseNodesSyncDraft()
await act(async () => {
await expect(result.current.doSyncWorkflowDraft(false, callbacks)).resolves.toBeUndefined()
})
expect(mockHandleRefreshWorkflowDraft).not.toHaveBeenCalled()
expect(callbacks.onError).toHaveBeenCalled()
expect(callbacks.onSettled).toHaveBeenCalled()
})
})
@@ -39,6 +39,9 @@ const mocks = vi.hoisted(() => {
const reactFlowStoreState = {
edges: [{ id: 'edge-1' }],
getNodes: vi.fn(),
get nodes() {
return this.getNodes()
},
setNodes: vi.fn(),
}
const workflowStoreState: WorkflowStoreState = {}
@@ -103,7 +106,7 @@ const mocks = vi.hoisted(() => {
}
})
vi.mock('reactflow', () => ({
vi.mock('@xyflow/react', () => ({
useStoreApi: () => ({
getState: () => mocks.reactFlowStoreState,
}),
@@ -26,10 +26,10 @@ const mockSetHistoryWorkflowData = vi.fn()
let workflowStoreState: Record<string, unknown>
vi.mock('reactflow', () => ({
vi.mock('@xyflow/react', () => ({
useStoreApi: () => ({
getState: () => ({
getNodes: mockGetNodes,
nodes: mockGetNodes(),
}),
}),
}))
@@ -55,6 +55,10 @@ describe('useWorkflowTemplate', () => {
sourceHandle: 'source',
target: 'llm',
targetHandle: 'target',
data: {
sourceType: 'start',
targetType: 'llm',
},
},
{
id: 'llm-answer',
@@ -62,6 +66,10 @@ describe('useWorkflowTemplate', () => {
sourceHandle: 'source',
target: 'answer',
targetHandle: 'target',
data: {
sourceType: 'llm',
targetType: 'answer',
},
},
])
expect(generateNewNodeCalls).toHaveLength(3)
@@ -1,13 +1,13 @@
import { useCallback, useEffect } from 'react'
import { useStoreApi } from 'reactflow'
import { useWorkflowStoreApi } from '@/app/components/workflow/hooks/use-workflow-reactflow'
import { useWorkflowStore } from '@/app/components/workflow/store'
export const useAutoOnboarding = () => {
const store = useStoreApi()
const store = useWorkflowStoreApi()
const workflowStore = useWorkflowStore()
const checkAndShowOnboarding = useCallback(() => {
const { getNodes } = store.getState()
const { nodes } = store.getState()
const {
showOnboarding,
hasShownOnboarding,
@@ -21,8 +21,6 @@ export const useAutoOnboarding = () => {
if (showOnboarding || notInitialWorkflow)
return
const nodes = getNodes()
// Check if canvas is completely empty (no nodes at all)
// Only trigger onboarding when canvas is completely blank to avoid data loss
const isCompletelyEmpty = nodes.length === 0
@@ -3,20 +3,21 @@ import type { WorkflowDraftFeaturesPayload } from '@/service/workflow'
import { useSuspenseQuery } from '@tanstack/react-query'
import { produce } from 'immer'
import { useCallback } from 'react'
import { useStoreApi } from 'reactflow'
import { useFeaturesStore } from '@/app/components/base/features/hooks'
import { collaborationManager } from '@/app/components/workflow/collaboration/core/collaboration-manager'
import { useSerialAsyncCallback } from '@/app/components/workflow/hooks/use-serial-async-callback'
import { useNodesReadOnly } from '@/app/components/workflow/hooks/use-workflow'
import { useWorkflowStoreApi } from '@/app/components/workflow/hooks/use-workflow-reactflow'
import { useWorkflowStore } from '@/app/components/workflow/store'
import { normalizeNodeDimensionsForReactFlowV12 } from '@/app/components/workflow/utils/workflow-init'
import { API_PREFIX } from '@/config'
import { postWithKeepalive } from '@/service/fetch'
import { parseResponseError, postWithKeepalive } from '@/service/fetch'
import { systemFeaturesQueryOptions } from '@/service/system-features'
import { syncWorkflowDraft } from '@/service/workflow'
import { useWorkflowRefreshDraft } from '.'
export const useNodesSyncDraft = () => {
const store = useStoreApi()
const store = useWorkflowStoreApi()
const workflowStore = useWorkflowStore()
const featuresStore = useFeaturesStore()
const { getNodesReadOnly } = useNodesReadOnly()
@@ -28,11 +29,11 @@ export const useNodesSyncDraft = () => {
const getPostParams = useCallback(() => {
const {
getNodes,
nodes,
edges,
transform,
} = store.getState()
const nodes = getNodes().filter(node => !node.data?._isTempNode)
const validNodes = nodes.filter(node => !node.data?._isTempNode)
const [x, y, zoom] = transform
const {
appId,
@@ -46,8 +47,9 @@ export const useNodesSyncDraft = () => {
return null
const features = featuresStore!.getState().features
const producedNodes = produce(nodes, (draft) => {
const producedNodes = produce(validNodes, (draft) => {
draft.forEach((node) => {
normalizeNodeDimensionsForReactFlowV12(node)
Object.keys(node.data).forEach((key) => {
if (key.startsWith('_'))
delete node.data[key]
@@ -56,9 +58,10 @@ export const useNodesSyncDraft = () => {
})
const producedEdges = produce(edges.filter(edge => !edge.data?._isTemp), (draft) => {
draft.forEach((edge) => {
Object.keys(edge.data).forEach((key) => {
const data = edge.data as typeof edge.data & Record<string, unknown>
Object.keys(data).forEach((key) => {
if (key.startsWith('_'))
delete edge.data[key]
delete data[key]
})
})
})
@@ -154,12 +157,9 @@ export const useNodesSyncDraft = () => {
callback?.onSuccess?.()
}
catch (error: any) {
if (error && error.json && !error.bodyUsed) {
error.json().then((err: any) => {
if (err.code === 'draft_workflow_not_sync' && !notRefreshWhenSyncError)
handleRefreshWorkflowDraft(true)
})
}
const err = await parseResponseError(error)
if (err?.code === 'draft_workflow_not_sync' && !notRefreshWhenSyncError)
handleRefreshWorkflowDraft(true)
callback?.onError?.()
}
finally {
@@ -12,6 +12,7 @@ import {
useWorkflowStore,
} from '@/app/components/workflow/store'
import { BlockEnum } from '@/app/components/workflow/types'
import { parseResponseError } from '@/service/fetch'
import { useWorkflowConfig } from '@/service/use-workflow'
import {
fetchNodesDefaultConfigs,
@@ -72,38 +73,35 @@ export const useWorkflowInit = () => {
setIsLoading(false)
}
catch (error: any) {
if (error && error.json && !error.bodyUsed && appDetail) {
error.json().then((err: any) => {
if (err.code === 'draft_workflow_not_exist') {
const isAdvancedChat = appDetail.mode === AppModeEnum.ADVANCED_CHAT
workflowStore.setState({
notInitialWorkflow: true,
showOnboarding: !isAdvancedChat,
shouldAutoOpenStartNodeSelector: !isAdvancedChat,
hasShownOnboarding: false,
})
const nodesData = isAdvancedChat ? nodesTemplate : []
const edgesData = isAdvancedChat ? edgesTemplate : []
const err = await parseResponseError(error)
if (err?.code === 'draft_workflow_not_exist' && appDetail) {
const isAdvancedChat = appDetail.mode === AppModeEnum.ADVANCED_CHAT
workflowStore.setState({
notInitialWorkflow: true,
showOnboarding: !isAdvancedChat,
shouldAutoOpenStartNodeSelector: !isAdvancedChat,
hasShownOnboarding: false,
})
const nodesData = isAdvancedChat ? nodesTemplate : []
const edgesData = isAdvancedChat ? edgesTemplate : []
syncWorkflowDraft({
url: `/apps/${appDetail.id}/workflows/draft`,
params: {
graph: {
nodes: nodesData,
edges: edgesData,
},
features: {
retriever_resource: { enabled: true },
},
environment_variables: [],
conversation_variables: [],
},
}).then((res) => {
workflowStore.getState().setDraftUpdatedAt(res.updated_at)
setSyncWorkflowDraftHash(res.hash)
handleGetInitialWorkflowData()
})
}
syncWorkflowDraft({
url: `/apps/${appDetail.id}/workflows/draft`,
params: {
graph: {
nodes: nodesData,
edges: edgesData,
},
features: {
retriever_resource: { enabled: true },
},
environment_variables: [],
conversation_variables: [],
},
}).then((res) => {
workflowStore.getState().setDraftUpdatedAt(res.updated_at)
setSyncWorkflowDraftHash(res.hash)
handleGetInitialWorkflowData()
})
}
}
@@ -6,10 +6,6 @@ import type { VersionHistory } from '@/types/workflow'
import { noop } from 'es-toolkit/function'
import { produce } from 'immer'
import { useCallback, useRef } from 'react'
import {
useReactFlow,
useStoreApi,
} from 'reactflow'
import { v4 as uuidV4 } from 'uuid'
import { useStore as useAppStore } from '@/app/components/app/store'
import { trackEvent } from '@/app/components/base/amplitude'
@@ -17,6 +13,7 @@ import { AudioPlayerManager } from '@/app/components/base/audio-btn/audio.player
import { useFeaturesStore } from '@/app/components/base/features/hooks'
import { TriggerType } from '@/app/components/workflow/header/test-run-menu'
import { useWorkflowUpdate } from '@/app/components/workflow/hooks/use-workflow-interactions'
import { useWorkflowReactFlow, useWorkflowStoreApi } from '@/app/components/workflow/hooks/use-workflow-reactflow'
import { useWorkflowRunEvent } from '@/app/components/workflow/hooks/use-workflow-run-event/use-workflow-run-event'
import { useWorkflowStore } from '@/app/components/workflow/store'
import { usePathname } from '@/next/navigation'
@@ -49,9 +46,9 @@ import {
} from './use-workflow-run-utils'
export const useWorkflowRun = () => {
const store = useStoreApi()
const store = useWorkflowStoreApi()
const workflowStore = useWorkflowStore()
const reactflow = useReactFlow()
const reactflow = useWorkflowReactFlow()
const featuresStore = useFeaturesStore()
const { doSyncWorkflowDraft } = useNodesSyncDraft()
const { handleUpdateWorkflowCanvas } = useWorkflowUpdate()
@@ -91,7 +88,7 @@ export const useWorkflowRun = () => {
const handleBackupDraft = useCallback(() => {
const {
getNodes,
nodes,
edges,
} = store.getState()
const { getViewport } = reactflow
@@ -104,7 +101,7 @@ export const useWorkflowRun = () => {
if (!backupDraft) {
setBackupDraft({
nodes: getNodes(),
nodes,
edges,
viewport: getViewport(),
features,
@@ -148,10 +145,10 @@ export const useWorkflowRun = () => {
const runMode = options?.mode ?? TriggerType.UserInput
const resolvedParams = params ?? {}
const {
getNodes,
nodes,
setNodes,
} = store.getState()
const newNodes = produce(getNodes(), (draft: Node[]) => {
const newNodes = produce(nodes, (draft: Node[]) => {
draft.forEach((node) => {
node.data.selected = false
node.data._runningStatus = undefined
@@ -1,8 +1,9 @@
import type { StartNodeType } from '@/app/components/workflow/nodes/start/types'
import { useCallback } from 'react'
import { useStoreApi } from 'reactflow'
import { useFeaturesStore } from '@/app/components/base/features/hooks'
import { TriggerType } from '@/app/components/workflow/header/test-run-menu'
import { useWorkflowInteractions } from '@/app/components/workflow/hooks'
import { useWorkflowStoreApi } from '@/app/components/workflow/hooks/use-workflow-reactflow'
import { useWorkflowStore } from '@/app/components/workflow/store'
import {
BlockEnum,
@@ -15,7 +16,7 @@ import {
} from '.'
export const useWorkflowStartRun = () => {
const store = useStoreApi()
const store = useWorkflowStoreApi<StartNodeType>()
const workflowStore = useWorkflowStore()
const featuresStore = useFeaturesStore()
const isChatMode = useIsChatMode()
@@ -31,8 +32,7 @@ export const useWorkflowStartRun = () => {
if (workflowRunningData?.result.status === WorkflowRunningStatus.Running)
return
const { getNodes } = store.getState()
const nodes = getNodes()
const { nodes } = store.getState()
const startNode = nodes.find(node => node.data.type === BlockEnum.Start)
const startVariables = startNode?.data.variables || []
const fileSettings = featuresStore!.getState().features.file
@@ -84,8 +84,7 @@ export const useWorkflowStartRun = () => {
if (workflowRunningData?.result.status === WorkflowRunningStatus.Running)
return
const { getNodes } = store.getState()
const nodes = getNodes()
const { nodes } = store.getState()
const scheduleNode = nodes.find(node => node.id === nodeId && node.data.type === BlockEnum.TriggerSchedule)
if (!scheduleNode) {
@@ -139,8 +138,7 @@ export const useWorkflowStartRun = () => {
if (workflowRunningData?.result.status === WorkflowRunningStatus.Running)
return
const { getNodes } = store.getState()
const nodes = getNodes()
const { nodes } = store.getState()
const webhookNode = nodes.find(node => node.id === nodeId && node.data.type === BlockEnum.TriggerWebhook)
if (!webhookNode) {
@@ -190,8 +188,7 @@ export const useWorkflowStartRun = () => {
if (workflowRunningData?.result.status === WorkflowRunningStatus.Running)
return
const { getNodes } = store.getState()
const nodes = getNodes()
const { nodes } = store.getState()
const pluginNode = nodes.find(node => node.id === nodeId && node.data.type === BlockEnum.TriggerPlugin)
if (!pluginNode) {
@@ -1,4 +1,5 @@
import type { StartNodeType } from '@/app/components/workflow/nodes/start/types'
import type { Edge } from '@/app/components/workflow/types'
import { useTranslation } from 'react-i18next'
import {
NODE_WIDTH_X_OFFSET,
@@ -56,20 +57,28 @@ export const useWorkflowTemplate = () => {
},
} as any)
const startToLlmEdge = {
const startToLlmEdge: Edge = {
id: `${startNode.id}-${llmNode.id}`,
source: startNode.id,
sourceHandle: 'source',
target: llmNode.id,
targetHandle: 'target',
data: {
sourceType: startDefault.metaData.type,
targetType: llmDefault.metaData.type,
},
}
const llmToAnswerEdge = {
const llmToAnswerEdge: Edge = {
id: `${llmNode.id}-${answerNode.id}`,
source: llmNode.id,
sourceHandle: 'source',
target: answerNode.id,
targetHandle: 'target',
data: {
sourceType: llmDefault.metaData.type,
targetType: answerDefault.metaData.type,
},
}
return {
@@ -21,7 +21,7 @@ vi.mock('ahooks', () => ({
useEventListener: (...args: unknown[]) => mockUseEventListener(...args),
}))
vi.mock('reactflow', () => ({
vi.mock('@xyflow/react', () => ({
useStoreApi: () => mockUseStoreApi(),
useReactFlow: () => mockUseReactFlow(),
useViewport: () => mockUseViewport(),
@@ -104,7 +104,7 @@ describe('CandidateNodeMain', () => {
})
mockUseStoreApi.mockReturnValue({
getState: () => ({
getNodes: () => nodes,
nodes,
setNodes: mockSetNodes,
}),
})
@@ -1,6 +1,8 @@
import type { ComponentProps } from 'react'
import { render } from '@testing-library/react'
import { getBezierPath, Position } from 'reactflow'
import {
render,
} from '@testing-library/react'
import { getBezierPath, Position } from '@xyflow/react'
import CustomConnectionLine from '../custom-connection-line'
const createConnectionLineProps = (
@@ -1,6 +1,11 @@
import type { ReactNode } from 'react'
import { fireEvent, render, screen } from '@testing-library/react'
import { Position } from 'reactflow'
import {
fireEvent,
render,
screen,
} from '@testing-library/react'
import { Position,
} from '@xyflow/react'
import { ErrorHandleTypeEnum } from '@/app/components/workflow/nodes/_base/components/error-handle/types'
import CustomEdge from '../custom-edge'
import { BlockEnum, NodeRunningStatus } from '../types'
@@ -10,7 +15,7 @@ const mockUseNodesInteractions = vi.hoisted(() => vi.fn())
const mockBlockSelector = vi.hoisted(() => vi.fn())
const mockGradientRender = vi.hoisted(() => vi.fn())
vi.mock('reactflow', () => ({
vi.mock('@xyflow/react', () => ({
BaseEdge: (props: {
id: string
path: string
@@ -2,7 +2,7 @@ import type { Edge, Node } from '../types'
import { fireEvent, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { useEffect } from 'react'
import { useEdges, useNodes, useStoreApi } from 'reactflow'
import { useWorkflowFlowEdges, useWorkflowFlowNodes, useWorkflowStoreApi } from '@/app/components/workflow/hooks/use-workflow-reactflow'
import { createEdge, createNode } from '../__tests__/fixtures'
import { renderWorkflowFlowComponent } from '../__tests__/workflow-test-env'
import EdgeContextmenu from '../edge-contextmenu'
@@ -91,8 +91,8 @@ let latestNodes: Node[] = []
let latestEdges: Edge[] = []
const RuntimeProbe = () => {
latestNodes = useNodes() as Node[]
latestEdges = useEdges() as Edge[]
latestNodes = useWorkflowFlowNodes() as Node[]
latestEdges = useWorkflowFlowEdges() as Edge[]
return null
}
@@ -103,8 +103,8 @@ const hooksStoreProps = {
const EdgeMenuHarness = () => {
const { handleEdgeContextMenu, handleEdgeDelete } = useEdgesInteractions()
const edges = useEdges() as Edge[]
const reactFlowStore = useStoreApi()
const edges = useWorkflowFlowEdges() as Edge[]
const reactFlowStore = useWorkflowStoreApi()
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
@@ -2,7 +2,7 @@ import type { InputVar } from '../types'
import type { PromptVariable } from '@/models/debug'
import { screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { useNodes } from 'reactflow'
import { useWorkflowFlowNodes } from '@/app/components/workflow/hooks/use-workflow-reactflow'
import Features from '../features'
import { InputVarType } from '../types'
import { createStartNode } from './fixtures'
@@ -126,7 +126,7 @@ const startNode = createStartNode({
})
const DelayedFeatures = () => {
const nodes = useNodes()
const nodes = useWorkflowFlowNodes()
if (!nodes.length)
return null
@@ -1,6 +1,6 @@
import type { CommonEdgeType, CommonNodeType, Edge, Node, WorkflowRunningData } from '../types'
import type { NodeTracing } from '@/types/workflow'
import { Position } from 'reactflow'
import { Position } from '@xyflow/react'
import { CUSTOM_NODE } from '../constants'
import { BlockEnum, NodeRunningStatus } from '../types'
@@ -1,6 +1,6 @@
import type { Edge, Node } from '../types'
import { render, screen } from '@testing-library/react'
import { useStoreApi } from 'reactflow'
import { useWorkflowStoreApi } from '@/app/components/workflow/hooks/use-workflow-reactflow'
import { WorkflowContextProvider } from '../context'
import { useDatasetsDetailStore } from '../datasets-detail-store/store'
import WorkflowWithDefaultContext from '../index'
@@ -38,7 +38,7 @@ const edges: Edge[] = [
const ContextConsumer = () => {
const { store } = useWorkflowHistoryStore()
const datasetCount = useDatasetsDetailStore(state => Object.keys(state.datasetsDetail).length)
const reactFlowStore = useStoreApi()
const reactFlowStore = useWorkflowStoreApi()
return (
<div>
@@ -6,7 +6,7 @@
*
* Usage (one line at top of test file):
* ```ts
* vi.mock('reactflow', async () =>
* vi.mock('@xyflow/react', async () =>
* (await import('../../__tests__/reactflow-mock-state')).createReactFlowModuleMock(),
* )
* ```
@@ -65,14 +65,13 @@ export function createReactFlowModuleMock() {
useStoreApi: vi.fn(() => ({
getState: () => ({
getNodes: () => rfState.nodes,
nodes: rfState.nodes,
setNodes: rfState.setNodes,
edges: rfState.edges,
setEdges: rfState.setEdges,
transform: rfState.transform,
nodeInternals: new Map(),
d3Selection: null,
d3Zoom: null,
nodeLookup: new Map(),
panZoom: null,
}),
setState: vi.fn(),
subscribe: vi.fn().mockReturnValue(vi.fn()),
@@ -85,6 +84,8 @@ export function createReactFlowModuleMock() {
zoomIn: vi.fn(),
zoomOut: vi.fn(),
zoomTo: vi.fn(),
nodes: rfState.nodes,
edges: rfState.edges,
getNodes: () => rfState.nodes,
getEdges: () => rfState.edges,
setNodes: rfState.setNodes,
@@ -136,7 +137,6 @@ export function createReactFlowModuleMock() {
getBezierPath: vi.fn().mockReturnValue(['M 0 0', 0, 0]),
getSmoothStepPath: vi.fn().mockReturnValue(['M 0 0', 0, 0]),
getStraightPath: vi.fn().mockReturnValue(['M 0 0', 0, 0]),
internalsSymbol: Symbol('internals'),
}
}
@@ -1,7 +1,7 @@
import type { Edge, Node } from '../types'
import { act, fireEvent, screen, waitFor } from '@testing-library/react'
import { useEffect } from 'react'
import { useNodes } from 'reactflow'
import { useWorkflowFlowNodes } from '@/app/components/workflow/hooks/use-workflow-reactflow'
import SelectionContextmenu from '../selection-contextmenu'
import { useWorkflowHistoryStore } from '../workflow-history-store'
import { createEdge, createNode } from './fixtures'
@@ -30,7 +30,7 @@ vi.mock('../hooks', async () => {
})
const RuntimeProbe = () => {
latestNodes = useNodes() as Node[]
latestNodes = useWorkflowFlowNodes() as Node[]
const { store } = useWorkflowHistoryStore()
useEffect(() => {
@@ -1,7 +1,8 @@
import type { Edge, Node } from '../types'
import { act, fireEvent, screen, waitFor } from '@testing-library/react'
import { BaseEdge, Position, ReactFlowProvider } from '@xyflow/react'
import * as React from 'react'
import { BaseEdge, internalsSymbol, Position, ReactFlowProvider, useStoreApi } from 'reactflow'
import { useWorkflowStoreApi } from '@/app/components/workflow/hooks/use-workflow-reactflow'
import { FlowType } from '@/types/common'
import { WORKFLOW_DATA_UPDATE } from '../constants'
import { Workflow } from '../index'
@@ -21,7 +22,7 @@ const eventEmitterState = vi.hoisted(() => ({
}))
const reactFlowBridge = vi.hoisted(() => ({
store: null as null | ReturnType<typeof useStoreApi>,
store: null as null | ReturnType<typeof useWorkflowStoreApi>,
}))
const collaborationBridge = vi.hoisted(() => ({
@@ -92,10 +93,11 @@ function createInitializedNode(id: string, x: number, label: string) {
positionAbsolute: { x, y: 0 },
width: 160,
height: 40,
measured: { width: 160, height: 40 },
sourcePosition: Position.Right,
targetPosition: Position.Left,
data: { label },
[internalsSymbol]: {
internals: {
positionAbsolute: { x, y: 0 },
handleBounds: {
source: [{
@@ -120,6 +122,7 @@ function createInitializedNode(id: string, x: number, label: string) {
}],
},
z: 0,
userNode: undefined,
},
}
}
@@ -453,14 +456,15 @@ function renderSubject(options?: {
}
function ReactFlowEdgeBootstrap({ nodes, edges }: { nodes: Node[], edges: Edge[] }) {
const store = useStoreApi()
const store = useWorkflowStoreApi()
React.useEffect(() => {
store.setState({
nodes,
edges,
width: 500,
height: 500,
nodeInternals: new Map(nodes.map(node => [node.id, node])),
nodeLookup: new Map(nodes.map(node => [node.id, node])) as never,
})
reactFlowBridge.store = store
@@ -521,7 +525,7 @@ describe('Workflow edge event wiring', () => {
expect(workflowHookMocks.handleNodeContextMenu).toHaveBeenCalledWith(expect.objectContaining({
clientX: 24,
clientY: 48,
}), expect.objectContaining({ id: 'node-1' }))
}), expect.any(Object))
expect(workflowHookMocks.handlePaneContextMenu).toHaveBeenCalledWith(expect.objectContaining({
clientX: 24,
clientY: 48,
@@ -4,7 +4,7 @@
import type { Shape } from '../store/workflow'
import { act, screen } from '@testing-library/react'
import * as React from 'react'
import { useNodes } from 'reactflow'
import { useWorkflowFlowNodes } from '@/app/components/workflow/hooks/use-workflow-reactflow'
import { FlowType } from '@/types/common'
import { useHooksStore } from '../hooks-store/store'
import { useStore, useWorkflowStore } from '../store/workflow'
@@ -51,7 +51,7 @@ function NodeRenderer(props: { id: string, data: { title: string }, selected?: b
}
function FlowReader() {
const nodes = useNodes()
const nodes = useWorkflowFlowNodes()
const showConfirm = useStore(s => s.showConfirm)
return React.createElement('div', { 'data-testid': 'flow-reader' }, `${nodes.length}:${showConfirm ? 'confirm' : 'clear'}`)
}
@@ -164,7 +164,7 @@ describe('renderWorkflowFlowComponent', () => {
describe('renderWorkflowFlowHook', () => {
it('should render hooks inside a real ReactFlow provider', () => {
const { result } = renderWorkflowFlowHook(() => useNodes(), {
const { result } = renderWorkflowFlowHook(() => useWorkflowFlowNodes(), {
nodes: [
createNode({ id: 'flow-1' }),
],
@@ -4,19 +4,28 @@
* ## Quick start (hook)
*
* ```ts
* import { resetReactFlowMockState, rfState } from '../../__tests__/reactflow-mock-state'
* import {
resetReactFlowMockState,
rfState } from '../../__tests__/reactflow-mock-state'
* import { renderWorkflowHook } from '../../__tests__/workflow-test-env'
*
* // Mock ReactFlow (one line, only needed when the hook imports reactflow)
* vi.mock('reactflow', async () =>
* // Mock ReactFlow (one line,
only needed when the hook imports /react)
* vi.mock('@xyflow/react',
async () =>
* (await import('../../__tests__/reactflow-mock-state')).createReactFlowModuleMock(),
* )
*
* it('example', () => {
* it('example',
() => {
* resetReactFlowMockState()
* rfState.nodes = [{ id: 'n1', position: { x: 0, y: 0 }, data: {} }]
* rfState.nodes = [{ id: 'n1',
position: { x: 0,
y: 0 },
data: {} }]
*
* const { result, store } = renderWorkflowHook(
* const { result,
store } = renderWorkflowHook(
* () => useMyHook(),
* {
* initialStoreState: { workflowRunningData: {...} },
@@ -35,8 +44,10 @@
* ```ts
* import { renderWorkflowComponent } from '../../__tests__/workflow-test-env'
*
* it('renders correctly', () => {
* const { getByText, store } = renderWorkflowComponent(
* it('renders correctly',
() => {
* const { getByText,
store } = renderWorkflowComponent(
* <MyComponent someProp="value" />,
* { initialStoreState: { showConfirm: undefined } },
* )
@@ -50,11 +61,16 @@
* ```ts
* import { renderNodeComponent } from '../../__tests__/workflow-test-env'
*
* it('renders node', () => {
* const { getByText, store } = renderNodeComponent(
* it('renders node',
() => {
* const { getByText,
store } = renderNodeComponent(
* MyNodeComponent,
* { type: BlockEnum.Code, title: 'My Node', desc: '' },
* { nodeId: 'n-1', initialStoreState: { ... } },
* { type: BlockEnum.Code,
title: 'My Node',
desc: '' },
* { nodeId: 'n-1',
initialStoreState: { ... } },
* )
* expect(getByText('My Node')).toBeInTheDocument()
* })
@@ -67,8 +83,8 @@ import type { WorkflowHistoryState } from '../store/workflow/history-slice'
import type { Edge, Node, WorkflowRunningData } from '../types'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { render, renderHook } from '@testing-library/react'
import { ReactFlow, ReactFlowProvider } from '@xyflow/react'
import * as React from 'react'
import ReactFlow, { ReactFlowProvider } from 'reactflow'
import { seedSystemFeatures } from '@/__tests__/utils/mock-system-features'
import { WorkflowContext } from '../context'
import { HooksStoreContext } from '../hooks-store/provider'
@@ -9,10 +9,10 @@ const runtimeState = vi.hoisted(() => ({
nodes: [] as Array<{ data: { type?: BlockEnum } }>,
}))
vi.mock('reactflow', () => ({
vi.mock('@xyflow/react', () => ({
useStoreApi: () => ({
getState: () => ({
getNodes: () => runtimeState.nodes,
nodes: runtimeState.nodes,
}),
}),
}))
@@ -5,7 +5,7 @@ import { BlockEnum } from '../../types'
import NodeSelectorWrapper from '../index'
import { BlockClassificationEnum } from '../types'
vi.mock('reactflow', async () =>
vi.mock('@xyflow/react', async () =>
(await import('../../__tests__/reactflow-mock-state')).createReactFlowModuleMock())
vi.mock('@/service/use-plugins', () => ({
@@ -6,10 +6,10 @@ import { BlockEnum } from '../../types'
import NodeSelector from '../main'
import { BlockClassificationEnum } from '../types'
vi.mock('reactflow', () => ({
vi.mock('@xyflow/react', () => ({
useStoreApi: () => ({
getState: () => ({
getNodes: () => [],
nodes: [],
}),
}),
}))
@@ -13,8 +13,8 @@ import {
useMemo,
} from 'react'
import { useTranslation } from 'react-i18next'
import { useStoreApi } from 'reactflow'
import Badge from '@/app/components/base/badge'
import { useWorkflowStoreApi } from '@/app/components/workflow/hooks/use-workflow-reactflow'
import BlockIcon from '../block-icon'
import { BlockEnum } from '../types'
import { BLOCK_CLASSIFICATIONS } from './constants'
@@ -37,7 +37,7 @@ const Blocks = ({
blocks: blocksFromProps,
}: BlocksProps) => {
const { t } = useTranslation()
const store = useStoreApi()
const store = useWorkflowStoreApi()
const blocksFromHooks = useBlocks()
const previewCardHandle = useMemo(() => createPreviewCardHandle<BlockPreviewPayload>(), [])
@@ -80,8 +80,7 @@ const Blocks = ({
const renderGroup = useCallback((classification: BlockClassificationEnum) => {
const list = groups[classification]!.sort((a, b) => (a.metaData.sort || 0) - (b.metaData.sort || 0))
const { getNodes } = store.getState()
const nodes = getNodes()
const { nodes } = store.getState()
const hasKnowledgeBaseNode = nodes.some(node => node.data.type === BlockEnum.KnowledgeBase)
const filteredList = list.filter((block) => {
if (hasKnowledgeBaseNode)
@@ -4,16 +4,16 @@ import type {
import type {
Node,
} from '@/app/components/workflow/types'
import {
useViewport,
} from '@xyflow/react'
import { useEventListener } from 'ahooks'
import { produce } from 'immer'
import {
memo,
} from 'react'
import {
useReactFlow,
useViewport,
} from 'reactflow'
import { useCollaborativeWorkflow } from '@/app/components/workflow/hooks/use-collaborative-workflow'
import { useWorkflowReactFlow } from '@/app/components/workflow/hooks/use-workflow-reactflow'
import { CUSTOM_NODE } from './constants'
import { useAutoGenerateWebhookUrl, useNodesInteractions, useNodesSyncDraft, useWorkflowHistory, WorkflowHistoryEvent } from './hooks'
import CustomNode from './nodes'
@@ -32,7 +32,7 @@ type Props = {
const CandidateNodeMain: FC<Props> = ({
candidateNode,
}) => {
const reactflow = useReactFlow()
const reactflow = useWorkflowReactFlow()
const workflowStore = useWorkflowStore()
const mousePosition = useStore(s => s.mousePosition)
const { zoom } = useViewport()
@@ -1,6 +1,6 @@
import type { FC } from 'react'
import type { CursorPosition, OnlineUser } from '@/app/components/workflow/collaboration/types/collaboration'
import { useViewport } from 'reactflow'
import { useViewport } from '@xyflow/react'
import { getUserColor } from '../utils/user-color'
type UserCursorsProps = {
@@ -9,9 +9,9 @@ import { webSocketClient } from '../websocket-manager'
type ReactFlowStore = {
getState: () => {
getNodes: () => Node[]
nodes: Node[]
setNodes: (nodes: Node[]) => void
getEdges: () => Edge[]
edges: Edge[]
setEdges: (edges: Edge[]) => void
}
}
@@ -92,7 +92,7 @@ describe('CollaborationManager logs and event helpers', () => {
internals.reactFlowStore = {
getState: () => ({
getNodes: () => [{
nodes: [{
...node,
data: {
...node.data,
@@ -100,7 +100,7 @@ describe('CollaborationManager logs and event helpers', () => {
},
}],
setNodes: vi.fn(),
getEdges: () => [edge],
edges: [edge],
setEdges: vi.fn(),
}),
}
@@ -154,9 +154,9 @@ describe('CollaborationManager logs and event helpers', () => {
internals.reactFlowStore = {
getState: () => ({
getNodes: () => oldNodes,
nodes: oldNodes,
setNodes: vi.fn(),
getEdges: () => [edge],
edges: [edge],
setEdges: vi.fn(),
}),
}
@@ -201,9 +201,9 @@ describe('CollaborationManager logs and event helpers', () => {
internals.graphSyncDiagnostics = [{ timestamp: 3 }]
internals.reactFlowStore = {
getState: () => ({
getNodes: () => [createNode('rf-1'), createNode('rf-2')],
nodes: [createNode('rf-1'), createNode('rf-2')],
setNodes: vi.fn(),
getEdges: () => [createEdge('rf-e', 'rf-1', 'rf-2')],
edges: [createEdge('rf-e', 'rf-1', 'rf-2')],
setEdges: vi.fn(),
}),
}
@@ -319,9 +319,9 @@ describe('CollaborationManager logs and event helpers', () => {
const setEdgesSpy = vi.fn()
internals.reactFlowStore = {
getState: () => ({
getNodes: () => [createNode('old-node')],
nodes: [createNode('old-node')],
setNodes: setNodesSpy,
getEdges: () => [createEdge('old-edge', 'old-node', 'old-target')],
edges: [createEdge('old-edge', 'old-node', 'old-target')],
setEdges: setEdgesSpy,
}),
}
@@ -14,9 +14,9 @@ import { webSocketClient } from '../websocket-manager'
type ReactFlowStore = {
getState: () => {
getNodes: () => Node[]
nodes: Node[]
setNodes: (nodes: Node[]) => void
getEdges: () => Edge[]
edges: Edge[]
setEdges: (edges: Edge[]) => void
}
}
@@ -520,9 +520,9 @@ describe('CollaborationManager socket and subscription behavior', () => {
})
internals.reactFlowStore = {
getState: () => ({
getNodes: () => reactFlowNodes,
nodes: reactFlowNodes,
setNodes: setNodesSpy,
getEdges: () => reactFlowEdges,
edges: reactFlowEdges,
setEdges: setEdgesSpy,
}),
}
@@ -644,9 +644,9 @@ describe('CollaborationManager socket and subscription behavior', () => {
const reactFlowStore = {
getState: () => ({
getNodes: () => [],
nodes: [],
setNodes: vi.fn(),
getEdges: () => [],
edges: [],
setEdges: vi.fn(),
}),
}
@@ -758,9 +758,9 @@ describe('CollaborationManager socket and subscription behavior', () => {
const { manager, internals } = setupManagerWithDoc()
const reactFlowStore = {
getState: () => ({
getNodes: () => [{ ...createNode('local-node'), selected: true }],
nodes: [{ ...createNode('local-node'), selected: true }],
setNodes: vi.fn(),
getEdges: () => [],
edges: [],
setEdges: vi.fn(),
}),
}
@@ -937,9 +937,9 @@ describe('CollaborationManager socket and subscription behavior', () => {
const reactFlowStore = {
getState: () => ({
getNodes: () => [],
nodes: [],
setNodes: vi.fn(),
getEdges: () => [],
edges: [],
setEdges: vi.fn(),
}),
}
@@ -1000,9 +1000,9 @@ describe('CollaborationManager socket and subscription behavior', () => {
const { internals } = setupManagerWithDoc()
const reactFlowStore = {
getState: () => ({
getNodes: () => [],
nodes: [],
setNodes: vi.fn(),
getEdges: () => [],
edges: [],
setEdges: vi.fn(),
}),
}
@@ -1077,9 +1077,9 @@ describe('CollaborationManager socket and subscription behavior', () => {
})
const reactFlowStore = {
getState: () => ({
getNodes: () => nodes,
nodes,
setNodes: setNodesSpy,
getEdges: () => edges,
edges,
setEdges: setEdgesSpy,
}),
}
@@ -4,8 +4,8 @@ import type {
NodePanelPresenceUser,
} from '@/app/components/workflow/collaboration/types/collaboration'
import type { CommonNodeType, Edge, Node } from '@/app/components/workflow/types'
import { Position } from '@xyflow/react'
import { LoroDoc } from 'loro-crdt/base64'
import { Position } from 'reactflow'
import { CollaborationManager } from '@/app/components/workflow/collaboration/core/collaboration-manager'
import { BlockEnum } from '@/app/components/workflow/types'
@@ -34,9 +34,9 @@ type NodePanelPresenceEventData = {
type ReactFlowStore = {
getState: () => {
getNodes: () => Node[]
nodes: Node[]
setNodes: (nodes: Node[]) => void
getEdges: () => Edge[]
edges: Edge[]
setEdges: (edges: Edge[]) => void
}
}
@@ -513,7 +513,7 @@ export class CollaborationManager {
excludeOriginPrefixes: [], // Don't exclude anything - let UndoManager track all local operations
onPush: (_isUndo, _range, _event) => {
// Store current selection state when an operation is pushed
const selectedNode = this.reactFlowStore?.getState().getNodes().find((n: Node) => n.data?.selected)
const selectedNode = this.reactFlowStore?.getState().nodes.find((n: Node) => n.data?.selected)
// Emit event to update UI button states when new operation is pushed
setTimeout(() => {
@@ -538,7 +538,7 @@ export class CollaborationManager {
if (selectedNodeId) {
const state = this.reactFlowStore.getState()
const { setNodes } = state
const nodes = state.getNodes()
const nodes = state.nodes
const newNodes = nodes.map((n: Node) => ({
...n,
data: {
@@ -823,7 +823,7 @@ export class CollaborationManager {
requestAnimationFrame(() => {
// Get ReactFlow's native setters, not the collaborative ones
const state = reactFlowStore.getState()
const previousNodes = state.getNodes()
const previousNodes = state.nodes
const updatedNodes = Array.from(this.nodesMap?.values() || []) as Node[]
const updatedEdges = Array.from(this.edgesMap?.values() || []) as Edge[]
// Call ReactFlow's native setters directly to avoid triggering collaboration
@@ -865,7 +865,7 @@ export class CollaborationManager {
requestAnimationFrame(() => {
// Get ReactFlow's native setters, not the collaborative ones
const state = reactFlowStore.getState()
const previousNodes = state.getNodes()
const previousNodes = state.nodes
const updatedNodes = Array.from(this.nodesMap?.values() || []) as Node[]
const updatedEdges = Array.from(this.edgesMap?.values() || []) as Edge[]
// Call ReactFlow's native setters directly to avoid triggering collaboration
@@ -990,8 +990,8 @@ export class CollaborationManager {
this.recordGraphSyncDiagnostic('nodes_subscribe', 'queued', 'raf_scheduled')
requestAnimationFrame(() => {
const state = reactFlowStore.getState()
const previousNodes: Node[] = state.getNodes()
const previousEdges: Edge[] = state.getEdges()
const previousNodes: Node[] = state.nodes
const previousEdges: Edge[] = state.edges
this.startImportLog('nodes', { nodes: previousNodes, edges: previousEdges })
const previousNodeMap = new Map(previousNodes.map(node => [node.id, node]))
const selectedIds = new Set(
@@ -1084,8 +1084,8 @@ export class CollaborationManager {
requestAnimationFrame(() => {
// Get ReactFlow's native setters, not the collaborative ones
const state = reactFlowStore.getState()
const previousNodes = state.getNodes()
const previousEdges = state.getEdges()
const previousNodes = state.nodes
const previousEdges = state.edges
this.startImportLog('edges', { nodes: previousNodes, edges: previousEdges })
const updatedEdges = Array.from(this.edgesMap?.values() || []) as Edge[]
@@ -1158,7 +1158,7 @@ export class CollaborationManager {
private mergeLocalNodeState(nodes: Node[]): Node[] {
const reactFlowStore = this.reactFlowStore
const state = reactFlowStore?.getState()
const localNodes = state?.getNodes() || []
const localNodes = state?.nodes || []
if (localNodes.length === 0)
return nodes
@@ -1225,8 +1225,8 @@ export class CollaborationManager {
edges: this.getEdges().length,
},
reactFlowCounts: {
nodes: reactFlowState?.getNodes().length ?? 0,
edges: reactFlowState?.getEdges().length ?? 0,
nodes: reactFlowState?.nodes.length ?? 0,
edges: reactFlowState?.edges.length ?? 0,
},
},
}
@@ -1328,8 +1328,8 @@ export class CollaborationManager {
const state = this.reactFlowStore.getState()
return {
nodes: cloneDeep(state.getNodes()),
edges: cloneDeep(state.getEdges()),
nodes: cloneDeep(state.nodes),
edges: cloneDeep(state.edges),
}
}
@@ -1599,8 +1599,8 @@ export class CollaborationManager {
return
const state = this.reactFlowStore.getState()
const nodes = state.getNodes()
const edges = state.getEdges()
const nodes = state.nodes
const edges = state.edges
if (!nodes.length && !edges.length)
return
@@ -1,4 +1,5 @@
import type { ReactFlowInstance } from 'reactflow'
import type { ReactFlowInstance } from '@xyflow/react'
import type { Edge, Node } from '../../types'
import type {
CollaborationState,
CursorPosition,
@@ -20,6 +21,7 @@ type CollaborationViewState = {
}
type ReactFlowStore = NonNullable<Parameters<typeof collaborationManager.connect>[1]>
type WorkflowReactFlowInstance = ReactFlowInstance<Node, Edge>
const initialState: CollaborationViewState = {
isConnected: false,
@@ -123,7 +125,7 @@ export function useCollaboration(appId: string, reactFlowStore?: ReactFlowStore)
prevIsConnected.current = state.isConnected || false
}, [state.isConnected])
const startCursorTracking = (containerRef: React.RefObject<HTMLElement>, reactFlowInstance?: ReactFlowInstance) => {
const startCursorTracking = (containerRef: React.RefObject<HTMLElement>, reactFlowInstance?: WorkflowReactFlowInstance) => {
if (!isCollaborationEnabled || !cursorServiceRef.current)
return
@@ -1,4 +1,4 @@
import type { ReactFlowInstance } from 'reactflow'
import type { ReactFlowInstance } from '@xyflow/react'
import { CursorService } from '../cursor-service'
describe('CursorService', () => {
@@ -37,7 +37,7 @@ describe('CursorService', () => {
getZoom: () => 2,
} as unknown as ReactFlowInstance
service.startTracking({ current: container }, onEmit, reactFlow)
service.startTracking({ current: container }, onEmit, reactFlow as never)
container.dispatchEvent(new MouseEvent('mousemove', { clientX: 30, clientY: 50 }))
expect(onEmit).toHaveBeenCalledTimes(1)
@@ -1,5 +1,6 @@
import type { ReactFlowInstance } from '@xyflow/react'
import type { RefObject } from 'react'
import type { ReactFlowInstance } from 'reactflow'
import type { Edge, Node } from '../../types'
import type { CursorPosition } from '../types/collaboration'
const CURSOR_MIN_MOVE_DISTANCE = 10
@@ -7,7 +8,7 @@ const CURSOR_THROTTLE_MS = 300
export class CursorService {
private containerRef: RefObject<HTMLElement> | null = null
private reactFlowInstance: ReactFlowInstance | null = null
private reactFlowInstance: ReactFlowInstance<Node, Edge> | null = null
private isTracking = false
private onCursorUpdate: ((cursors: Record<string, CursorPosition>) => void) | null = null
private onEmitPosition: ((position: CursorPosition) => void) | null = null
@@ -17,7 +18,7 @@ export class CursorService {
startTracking(
containerRef: RefObject<HTMLElement>,
onEmitPosition: (position: CursorPosition) => void,
reactFlowInstance?: ReactFlowInstance,
reactFlowInstance?: ReactFlowInstance<Node, Edge>,
): void {
if (this.isTracking)
this.stopTracking()
@@ -10,7 +10,7 @@ let mockUserId = 'user-1'
const mockFlowToScreenPosition = vi.fn((position: Position) => position)
const mockScreenToFlowPosition = vi.fn((position: Position) => position)
vi.mock('reactflow', () => ({
vi.mock('@xyflow/react', () => ({
useReactFlow: () => ({
flowToScreenPosition: mockFlowToScreenPosition,
screenToFlowPosition: mockScreenToFlowPosition,
@@ -2,9 +2,12 @@
import type { FC, PointerEvent as ReactPointerEvent } from 'react'
import type { WorkflowCommentList } from '@/contract/console/workflow-comment'
import {
useViewport,
} from '@xyflow/react'
import { memo, useCallback, useMemo, useRef, useState } from 'react'
import { useReactFlow, useViewport } from 'reactflow'
import { UserAvatarList } from '@/app/components/base/user-avatar-list'
import { useWorkflowReactFlow } from '@/app/components/workflow/hooks/use-workflow-reactflow'
import { useAppContext } from '@/context/app-context'
import CommentPreview from './comment-preview'
@@ -16,7 +19,7 @@ type CommentIconProps = {
}
export const CommentIcon: FC<CommentIconProps> = memo(({ comment, onClick, isActive = false, onPositionUpdate }) => {
const { flowToScreenPosition, screenToFlowPosition } = useReactFlow()
const { flowToScreenPosition, screenToFlowPosition } = useWorkflowReactFlow()
const viewport = useViewport()
const { userProfile } = useAppContext()
const isAuthor = comment.created_by_account?.id === userProfile?.id
@@ -41,7 +41,7 @@ vi.mock('@/context/app-context', () => ({
}),
}))
vi.mock('reactflow', () => ({
vi.mock('@xyflow/react', () => ({
useReactFlow: () => ({
flowToScreenPosition: mockFlowToScreenPosition,
}),
@@ -11,12 +11,15 @@ import {
} from '@langgenius/dify-ui/dropdown-menu'
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
import { RiArrowDownSLine, RiArrowUpSLine, RiCheckboxCircleFill, RiCheckboxCircleLine, RiCloseLine, RiDeleteBinLine, RiMoreFill } from '@remixicon/react'
import {
useViewport,
} from '@xyflow/react'
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useReactFlow, useViewport } from 'reactflow'
import Divider from '@/app/components/base/divider'
import InlineDeleteConfirm from '@/app/components/base/inline-delete-confirm'
import { getUserColor } from '@/app/components/workflow/collaboration/utils/user-color'
import { useWorkflowReactFlow } from '@/app/components/workflow/hooks/use-workflow-reactflow'
import { useAppContext } from '@/context/app-context'
import { useFormatTimeFromNow } from '@/hooks/use-format-time-from-now'
import { useParams } from '@/next/navigation'
@@ -173,7 +176,7 @@ export const CommentThread: FC<CommentThreadProps> = memo(({
}) => {
const params = useParams()
const appId = params.appId as string
const { flowToScreenPosition } = useReactFlow()
const { flowToScreenPosition } = useWorkflowReactFlow()
const viewport = useViewport()
const { userProfile } = useAppContext()
const { t } = useTranslation()
@@ -1,9 +1,11 @@
import type { ConnectionLineComponentProps } from 'reactflow'
import { memo } from 'react'
import type { ConnectionLineComponentProps } from '@xyflow/react'
import {
getBezierPath,
Position,
} from 'reactflow'
} from '@xyflow/react'
import {
memo,
} from 'react'
const CustomConnectionLine = ({ fromX, fromY, toX, toY }: ConnectionLineComponentProps) => {
const [
+11 -9
View File
@@ -1,9 +1,17 @@
import type { EdgeProps } from 'reactflow'
import type { EdgeProps } from '@xyflow/react'
import type {
Edge,
OnSelectBlock,
} from './types'
import { cn } from '@langgenius/dify-ui/cn'
import {
cn,
} from '@langgenius/dify-ui/cn'
import {
BaseEdge,
EdgeLabelRenderer,
getBezierPath,
Position,
} from '@xyflow/react'
import { intersection } from 'es-toolkit/array'
import {
memo,
@@ -11,12 +19,6 @@ import {
useMemo,
useState,
} from 'react'
import {
BaseEdge,
EdgeLabelRenderer,
getBezierPath,
Position,
} from 'reactflow'
import { ErrorHandleTypeEnum } from '@/app/components/workflow/nodes/_base/components/error-handle/types'
import BlockSelector from './block-selector'
import { ITERATION_CHILDREN_Z_INDEX, LOOP_CHILDREN_Z_INDEX } from './constants'
@@ -40,7 +42,7 @@ const CustomEdge = ({
targetX,
targetY,
selected,
}: EdgeProps) => {
}: EdgeProps<Edge>) => {
const [
edgePath,
labelX,
@@ -8,7 +8,7 @@ import {
useMemo,
} from 'react'
import { useTranslation } from 'react-i18next'
import { useEdges } from 'reactflow'
import { useWorkflowFlowEdges } from '@/app/components/workflow/hooks/use-workflow-reactflow'
import { useEdgesInteractions, usePanelInteractions } from './hooks'
import { ShortcutKbd } from './shortcuts/shortcut-kbd'
import { useStore } from './store'
@@ -18,7 +18,7 @@ const EdgeContextmenu = () => {
const edgeMenu = useStore(s => s.edgeMenu)
const { handleEdgeDeleteById } = useEdgesInteractions()
const { handleEdgeContextmenuCancel } = usePanelInteractions()
const edges = useEdges()
const edges = useWorkflowFlowEdges()
const currentEdgeExists = !edgeMenu || edges.some(edge => edge.id === edgeMenu.edgeId)
const anchor = useMemo(() => {
+3 -3
View File
@@ -1,15 +1,15 @@
import type { StartNodeType } from './nodes/start/types'
import type { CommonNodeType, InputVar, Node } from './types'
import type { InputVar, Node } from './types'
import type { PromptVariable } from '@/models/debug'
import type { WorkflowDraftFeaturesPayload } from '@/service/workflow'
import {
memo,
useCallback,
} from 'react'
import { useNodes } from 'reactflow'
import { useFeaturesStore } from '@/app/components/base/features/hooks'
import NewFeaturePanel from '@/app/components/base/features/new-feature-panel'
import { webSocketClient } from '@/app/components/workflow/collaboration/core/websocket-manager'
import { useWorkflowFlowNodes } from '@/app/components/workflow/hooks/use-workflow-reactflow'
import { updateFeatures } from '@/service/workflow'
import {
useIsChatMode,
@@ -25,7 +25,7 @@ const Features = () => {
const isChatMode = useIsChatMode()
const { nodesReadOnly } = useNodesReadOnly()
const featuresStore = useFeaturesStore()
const nodes = useNodes<CommonNodeType>()
const nodes = useWorkflowFlowNodes()
const startNode = nodes.find(node => node.data.type === 'start')
const { id, data } = startNode as Node<StartNodeType>
const { handleAddVariable } = useConfig(id, data)
@@ -26,7 +26,7 @@ const mockViewHistory = vi.fn()
let mockNodesReadOnly = false
let mockTheme: 'light' | 'dark' = 'light'
vi.mock('reactflow', () => ({
vi.mock('@xyflow/react', () => ({
useNodes: () => mockUseNodes(),
}))
@@ -5,7 +5,7 @@ import ScrollToSelectedNodeButton from '../scroll-to-selected-node-button'
const mockScrollToWorkflowNode = vi.fn()
vi.mock('reactflow', async () =>
vi.mock('@xyflow/react', async () =>
(await import('../../__tests__/reactflow-mock-state')).createReactFlowModuleMock())
vi.mock('../../utils/node-navigation', () => ({
@@ -32,7 +32,7 @@ type PopoverProps = {
let latestOnOpenChange: PopoverProps['onOpenChange']
vi.mock('reactflow', () => ({
vi.mock('@xyflow/react', () => ({
useEdges: () => [],
}))
@@ -1,8 +1,8 @@
import type { ChecklistItem } from '../../hooks/use-checklist'
import type {
CommonEdgeType,
} from '../../types'
import { cn } from '@langgenius/dify-ui/cn'
import {
cn,
} from '@langgenius/dify-ui/cn'
import {
Popover,
PopoverClose,
@@ -17,9 +17,7 @@ import {
useState,
} from 'react'
import { useTranslation } from 'react-i18next'
import {
useEdges,
} from 'reactflow'
import { useWorkflowFlowEdges } from '@/app/components/workflow/hooks/use-workflow-reactflow'
import useNodes from '@/app/components/workflow/store/workflow/use-nodes'
import {
useChecklist,
@@ -41,7 +39,7 @@ const WorkflowChecklist = ({
}: WorkflowChecklistProps) => {
const { t } = useTranslation()
const [open, setOpen] = useState(false)
const edges = useEdges<CommonEdgeType>()
const edges = useWorkflowFlowEdges()
const nodes = useNodes()
const needWarningNodes = useChecklist(nodes, edges)
const { handleNodeSelect } = useNodesInteractions()
@@ -3,8 +3,8 @@ import type { RunAndHistoryProps } from './run-and-history'
import {
useCallback,
} from 'react'
import { useNodes } from 'reactflow'
import { useInputFieldPanel } from '@/app/components/rag-pipeline/hooks'
import { useWorkflowFlowNodes } from '@/app/components/workflow/hooks/use-workflow-reactflow'
import Divider from '../../base/divider'
import {
useNodesInteractions,
@@ -44,7 +44,7 @@ const HeaderInNormal = ({
const setShowVariableInspectPanel = useStore(s => s.setShowVariableInspectPanel)
const setShowChatVariablePanel = useStore(s => s.setShowChatVariablePanel)
const setShowGlobalVariablePanel = useStore(s => s.setShowGlobalVariablePanel)
const nodes = useNodes<StartNodeType>()
const nodes = useWorkflowFlowNodes<StartNodeType>()
const selectedNode = nodes.find(node => node.data.selected)
const { handleBackupDraft } = useWorkflowRun()
const { closeAllInputFieldPanels } = useInputFieldPanel()
@@ -1,4 +1,5 @@
'use client'
import type { OnlineUser } from '../collaboration/types/collaboration'
import { ChevronDownIcon } from '@heroicons/react/20/solid'
import { AvatarFallback, AvatarImage, AvatarRoot } from '@langgenius/dify-ui/avatar'
@@ -11,7 +12,7 @@ import {
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useReactFlow } from 'reactflow'
import { useWorkflowReactFlow } from '@/app/components/workflow/hooks/use-workflow-reactflow'
import { useAppContext } from '@/context/app-context'
import { getAvatar } from '@/service/common'
import { useCollaboration } from '../collaboration/hooks/use-collaboration'
@@ -55,7 +56,7 @@ const OnlineUsers = () => {
const appId = useStore(s => s.appId)
const { onlineUsers, cursors, isEnabled: isCollaborationEnabled } = useCollaboration(appId as string)
const { userProfile } = useAppContext()
const reactFlow = useReactFlow()
const reactFlow = useWorkflowReactFlow()
const [dropdownOpen, setDropdownOpen] = useState(false)
const avatarUrls = useAvatarUrls(onlineUsers || [])
@@ -1,13 +1,12 @@
import type { FC } from 'react'
import type { CommonNodeType } from '../types'
import { cn } from '@langgenius/dify-ui/cn'
import { useTranslation } from 'react-i18next'
import { useNodes } from 'reactflow'
import { useWorkflowFlowNodes } from '@/app/components/workflow/hooks/use-workflow-reactflow'
import { scrollToWorkflowNode } from '../utils/node-navigation'
const ScrollToSelectedNodeButton: FC = () => {
const { t } = useTranslation()
const nodes = useNodes<CommonNodeType>()
const nodes = useWorkflowFlowNodes()
const selectedNode = nodes.find(node => node.data.selected)
if (!selectedNode)
@@ -4,7 +4,7 @@ import HelpLine from '../index'
const mockUseViewport = vi.hoisted(() => vi.fn())
const mockUseStore = vi.hoisted(() => vi.fn())
vi.mock('reactflow', () => ({
vi.mock('@xyflow/react', () => ({
useViewport: () => mockUseViewport(),
}))
@@ -2,8 +2,8 @@ import type {
HelpLineHorizontalPosition,
HelpLineVerticalPosition,
} from './types'
import { useViewport } from '@xyflow/react'
import { memo } from 'react'
import { useViewport } from 'reactflow'
import { useStore } from '../store'
const HelpLineHorizontal = memo(({
@@ -10,11 +10,10 @@ const mockStore = {
}
let mockReactflowState = {
d3Selection: null as object | null,
d3Zoom: null as object | null,
panZoom: null as object | null,
}
vi.mock('reactflow', () => ({
vi.mock('@xyflow/react', () => ({
useStore: (selector: (state: typeof mockReactflowState) => unknown) => selector(mockReactflowState),
}))
@@ -35,8 +34,7 @@ describe('hooks-store provider', () => {
beforeEach(() => {
vi.clearAllMocks()
mockReactflowState = {
d3Selection: null,
d3Zoom: null,
panZoom: null,
}
})
@@ -51,11 +49,10 @@ describe('hooks-store provider', () => {
expect(mockRefreshAll).not.toHaveBeenCalled()
})
it('should refresh the hooks store when both d3Selection and d3Zoom are available', async () => {
it('should refresh the hooks store when panZoom is available', async () => {
const handleRun = vi.fn()
mockReactflowState = {
d3Selection: {},
d3Zoom: {},
panZoom: {},
}
render(
@@ -1,10 +1,11 @@
import type { Shape } from './store'
import { useStore,
} from '@xyflow/react'
import {
createContext,
useEffect,
useRef,
} from 'react'
import { useStore } from 'reactflow'
import {
createHooksStore,
} from './store'
@@ -16,13 +17,12 @@ type HooksStoreContextProviderProps = Partial<Shape> & {
}
export const HooksStoreContextProvider = ({ children, ...restProps }: HooksStoreContextProviderProps) => {
const storeRef = useRef<HooksStore | undefined>(undefined)
const d3Selection = useStore(s => s.d3Selection)
const d3Zoom = useStore(s => s.d3Zoom)
const panZoom = useStore(s => s.panZoom)
useEffect(() => {
if (storeRef.current && d3Selection && d3Zoom)
if (storeRef.current && panZoom)
storeRef.current.getState().refreshAll(restProps)
}, [d3Selection, d3Zoom])
}, [panZoom])
if (!storeRef.current)
storeRef.current = createHooksStore(restProps)
@@ -1,10 +1,13 @@
import type { Node } from '../../types'
import { act, waitFor } from '@testing-library/react'
import { useNodes } from 'reactflow'
import {
act,
waitFor,
} from '@testing-library/react'
import { createNode } from '../../__tests__/fixtures'
import { renderWorkflowFlowHook } from '../../__tests__/workflow-test-env'
import { BlockEnum } from '../../types'
import { useAutoGenerateWebhookUrl } from '../use-auto-generate-webhook-url'
import { useWorkflowFlowNodes } from '../use-workflow-reactflow'
type WebhookFlowNode = Node & {
data: NonNullable<Node['data']> & {
@@ -37,7 +40,7 @@ describe('useAutoGenerateWebhookUrl', () => {
const renderAutoGenerateWebhookUrlHook = () =>
renderWorkflowFlowHook(() => ({
autoGenerateWebhookUrl: useAutoGenerateWebhookUrl(),
nodes: useNodes<WebhookFlowNode>(),
nodes: useWorkflowFlowNodes<WebhookFlowNode['data']>(),
}), {
nodes: createFlowNodes(),
edges: [],

Some files were not shown because too many files have changed in this diff Show More