Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d45001e66c | ||
|
|
fea4dc06c5 | ||
|
|
2e0c729c0c | ||
|
|
3729cbbeef | ||
|
|
2d31c46ebc | ||
|
|
02b6bc8543 | ||
|
|
6ba0a14a1a |
@@ -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
|
||||
|
||||
Generated
+39
-141
@@ -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
@@ -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'
|
||||
|
||||
+5
-3
@@ -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
-2
@@ -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'
|
||||
|
||||
+3
-3
@@ -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
-2
@@ -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
-2
@@ -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
-1
@@ -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
-2
@@ -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
-3
@@ -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() })
|
||||
},
|
||||
}))
|
||||
|
||||
|
||||
+1
-1
@@ -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,
|
||||
|
||||
+1
-1
@@ -49,7 +49,7 @@ vi.mock('@/app/components/workflow/store', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('reactflow', () => ({
|
||||
vi.mock('@xyflow/react', () => ({
|
||||
useStore: () => undefined,
|
||||
}))
|
||||
|
||||
|
||||
+1
-1
@@ -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,
|
||||
}))
|
||||
|
||||
|
||||
+1
-1
@@ -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()
|
||||
|
||||
+1
-1
@@ -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 = {
|
||||
|
||||
+10
-10
@@ -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
-14
@@ -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,
|
||||
}),
|
||||
}
|
||||
|
||||
+1
-1
@@ -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 [
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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(),
|
||||
}))
|
||||
|
||||
|
||||
+1
-1
@@ -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
Reference in New Issue
Block a user