mirror of
https://github.com/langgenius/dify.git
synced 2026-04-05 16:39:26 +08:00
feat(web): snippet graph draft sync
This commit is contained in:
@@ -10,6 +10,27 @@ vi.mock('../hooks/use-snippet-init', () => ({
|
||||
useSnippetInit: (snippetId: string) => mockUseSnippetInit(snippetId),
|
||||
}))
|
||||
|
||||
vi.mock('../hooks/use-configs-map', () => ({
|
||||
useConfigsMap: () => ({
|
||||
flowId: 'snippet-1',
|
||||
flowType: 'snippet',
|
||||
fileSettings: {},
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('../hooks/use-nodes-sync-draft', () => ({
|
||||
useNodesSyncDraft: () => ({
|
||||
doSyncWorkflowDraft: vi.fn(),
|
||||
syncWorkflowDraftWhenPageClose: vi.fn(),
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('../hooks/use-snippet-refresh-draft', () => ({
|
||||
useSnippetRefreshDraft: () => ({
|
||||
handleRefreshWorkflowDraft: vi.fn(),
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/next/navigation', () => ({
|
||||
useRouter: () => ({
|
||||
replace: vi.fn(),
|
||||
|
||||
@@ -9,7 +9,11 @@ import {
|
||||
RiTerminalWindowFill,
|
||||
RiTerminalWindowLine,
|
||||
} from '@remixicon/react'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import {
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useShallow } from 'zustand/react/shallow'
|
||||
import AppSideBar from '@/app/components/app-sidebar'
|
||||
@@ -20,6 +24,9 @@ import { toast } from '@/app/components/base/ui/toast'
|
||||
import Evaluation from '@/app/components/evaluation'
|
||||
import { WorkflowWithInnerContext } from '@/app/components/workflow'
|
||||
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
||||
import { useConfigsMap } from '../hooks/use-configs-map'
|
||||
import { useNodesSyncDraft } from '../hooks/use-nodes-sync-draft'
|
||||
import { useSnippetRefreshDraft } from '../hooks/use-snippet-refresh-draft'
|
||||
import { useSnippetDetailStore } from '../store'
|
||||
import SnippetChildren from './snippet-children'
|
||||
|
||||
@@ -52,6 +59,12 @@ const SnippetMain = ({
|
||||
const media = useBreakpoints()
|
||||
const isMobile = media === MediaType.mobile
|
||||
const [fields, setFields] = useState<SnippetInputField[]>(payload.inputFields)
|
||||
const {
|
||||
doSyncWorkflowDraft,
|
||||
syncWorkflowDraftWhenPageClose,
|
||||
} = useNodesSyncDraft(snippetId)
|
||||
const { handleRefreshWorkflowDraft } = useSnippetRefreshDraft(snippetId)
|
||||
const configsMap = useConfigsMap(snippetId)
|
||||
const setAppSidebarExpand = useAppStore(state => state.setAppSidebarExpand)
|
||||
const {
|
||||
editingField,
|
||||
@@ -130,6 +143,15 @@ const SnippetMain = ({
|
||||
setInputPanelOpen(false)
|
||||
}
|
||||
|
||||
const hooksStore = useMemo(() => {
|
||||
return {
|
||||
doSyncWorkflowDraft,
|
||||
syncWorkflowDraftWhenPageClose,
|
||||
handleRefreshWorkflowDraft,
|
||||
configsMap,
|
||||
}
|
||||
}, [configsMap, doSyncWorkflowDraft, handleRefreshWorkflowDraft, syncWorkflowDraftWhenPageClose])
|
||||
|
||||
return (
|
||||
<div className="relative flex h-full overflow-hidden bg-background-body">
|
||||
<AppSideBar
|
||||
@@ -166,6 +188,7 @@ const SnippetMain = ({
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
viewport={viewport ?? graph.viewport}
|
||||
hooksStore={hooksStore}
|
||||
>
|
||||
<SnippetChildren
|
||||
fields={fields}
|
||||
|
||||
24
web/app/components/snippets/hooks/use-configs-map.ts
Normal file
24
web/app/components/snippets/hooks/use-configs-map.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { useMemo } from 'react'
|
||||
import { useStore } from '@/app/components/workflow/store'
|
||||
import { Resolution, TransferMethod } from '@/types/app'
|
||||
import { FlowType } from '@/types/common'
|
||||
|
||||
export const useConfigsMap = (snippetId: string) => {
|
||||
const fileUploadConfig = useStore(s => s.fileUploadConfig)
|
||||
|
||||
return useMemo(() => {
|
||||
return {
|
||||
flowId: snippetId,
|
||||
flowType: FlowType.snippet,
|
||||
fileSettings: {
|
||||
image: {
|
||||
enabled: false,
|
||||
detail: Resolution.high,
|
||||
number_limits: 3,
|
||||
transfer_methods: [TransferMethod.local_file, TransferMethod.remote_url],
|
||||
},
|
||||
fileUploadConfig,
|
||||
},
|
||||
}
|
||||
}, [fileUploadConfig, snippetId])
|
||||
}
|
||||
125
web/app/components/snippets/hooks/use-nodes-sync-draft.ts
Normal file
125
web/app/components/snippets/hooks/use-nodes-sync-draft.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
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 { useWorkflowStore } from '@/app/components/workflow/store'
|
||||
import { API_PREFIX } from '@/config'
|
||||
import { consoleClient } from '@/service/client'
|
||||
import { postWithKeepalive } from '@/service/fetch'
|
||||
import { useSnippetRefreshDraft } from './use-snippet-refresh-draft'
|
||||
|
||||
const isSyncConflictError = (error: unknown): error is { bodyUsed: boolean, json: () => Promise<{ code?: string }> } => {
|
||||
return !!error
|
||||
&& typeof error === 'object'
|
||||
&& 'bodyUsed' in error
|
||||
&& 'json' in error
|
||||
&& typeof error.json === 'function'
|
||||
}
|
||||
|
||||
export const useNodesSyncDraft = (snippetId: string) => {
|
||||
const store = useStoreApi()
|
||||
const workflowStore = useWorkflowStore()
|
||||
const { getNodesReadOnly } = useNodesReadOnly()
|
||||
const { handleRefreshWorkflowDraft } = useSnippetRefreshDraft(snippetId)
|
||||
|
||||
const getPostParams = useCallback(() => {
|
||||
const {
|
||||
getNodes,
|
||||
edges,
|
||||
transform,
|
||||
} = store.getState()
|
||||
const nodes = getNodes().filter(node => !node.data?._isTempNode)
|
||||
const [x, y, zoom] = transform
|
||||
const { syncWorkflowDraftHash } = workflowStore.getState()
|
||||
|
||||
if (!snippetId)
|
||||
return null
|
||||
|
||||
const producedNodes = produce(nodes, (draft) => {
|
||||
draft.forEach((node) => {
|
||||
Object.keys(node.data).forEach((key) => {
|
||||
if (key.startsWith('_'))
|
||||
delete node.data[key]
|
||||
})
|
||||
})
|
||||
})
|
||||
const producedEdges = produce(edges.filter(edge => !edge.data?._isTemp), (draft) => {
|
||||
draft.forEach((edge) => {
|
||||
Object.keys(edge.data).forEach((key) => {
|
||||
if (key.startsWith('_'))
|
||||
delete edge.data[key]
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
return {
|
||||
url: `/snippets/${snippetId}/workflows/draft`,
|
||||
params: {
|
||||
graph: {
|
||||
nodes: producedNodes,
|
||||
edges: producedEdges,
|
||||
viewport: { x, y, zoom },
|
||||
},
|
||||
hash: syncWorkflowDraftHash,
|
||||
},
|
||||
}
|
||||
}, [snippetId, store, workflowStore])
|
||||
|
||||
const syncWorkflowDraftWhenPageClose = useCallback(() => {
|
||||
if (getNodesReadOnly())
|
||||
return
|
||||
|
||||
const postParams = getPostParams()
|
||||
if (postParams)
|
||||
postWithKeepalive(`${API_PREFIX}${postParams.url}`, postParams.params)
|
||||
}, [getNodesReadOnly, getPostParams])
|
||||
|
||||
const performSync = useCallback(async (
|
||||
notRefreshWhenSyncError?: boolean,
|
||||
callback?: SyncDraftCallback,
|
||||
) => {
|
||||
if (getNodesReadOnly())
|
||||
return
|
||||
|
||||
const postParams = getPostParams()
|
||||
if (!postParams)
|
||||
return
|
||||
|
||||
const {
|
||||
setDraftUpdatedAt,
|
||||
setSyncWorkflowDraftHash,
|
||||
} = workflowStore.getState()
|
||||
|
||||
try {
|
||||
const response = await consoleClient.snippets.syncDraftWorkflow({
|
||||
params: { snippetId },
|
||||
body: postParams.params,
|
||||
})
|
||||
|
||||
setSyncWorkflowDraftHash(response.hash)
|
||||
setDraftUpdatedAt(response.updated_at)
|
||||
callback?.onSuccess?.()
|
||||
}
|
||||
catch (error: unknown) {
|
||||
if (isSyncConflictError(error) && !error.bodyUsed) {
|
||||
error.json().then((err) => {
|
||||
if (err.code === 'draft_workflow_not_sync' && !notRefreshWhenSyncError)
|
||||
handleRefreshWorkflowDraft()
|
||||
})
|
||||
}
|
||||
callback?.onError?.()
|
||||
}
|
||||
finally {
|
||||
callback?.onSettled?.()
|
||||
}
|
||||
}, [getNodesReadOnly, getPostParams, handleRefreshWorkflowDraft, snippetId, workflowStore])
|
||||
|
||||
const doSyncWorkflowDraft = useSerialAsyncCallback(performSync, getNodesReadOnly)
|
||||
|
||||
return {
|
||||
doSyncWorkflowDraft,
|
||||
syncWorkflowDraftWhenPageClose,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import type { WorkflowDataUpdater } from '@/app/components/workflow/types'
|
||||
import { useCallback } from 'react'
|
||||
import { useWorkflowUpdate } from '@/app/components/workflow/hooks'
|
||||
import { useWorkflowStore } from '@/app/components/workflow/store'
|
||||
import { consoleClient } from '@/service/client'
|
||||
|
||||
export const useSnippetRefreshDraft = (snippetId: string) => {
|
||||
const workflowStore = useWorkflowStore()
|
||||
const { handleUpdateWorkflowCanvas } = useWorkflowUpdate()
|
||||
|
||||
const handleRefreshWorkflowDraft = useCallback(() => {
|
||||
const {
|
||||
setDraftUpdatedAt,
|
||||
setIsSyncingWorkflowDraft,
|
||||
setSyncWorkflowDraftHash,
|
||||
} = workflowStore.getState()
|
||||
|
||||
if (!snippetId)
|
||||
return
|
||||
|
||||
setIsSyncingWorkflowDraft(true)
|
||||
consoleClient.snippets.draftWorkflow({
|
||||
params: { snippetId },
|
||||
}).then((response) => {
|
||||
handleUpdateWorkflowCanvas({
|
||||
...response.graph,
|
||||
nodes: response.graph?.nodes || [],
|
||||
edges: response.graph?.edges || [],
|
||||
viewport: response.graph?.viewport || { x: 0, y: 0, zoom: 1 },
|
||||
} as WorkflowDataUpdater)
|
||||
setSyncWorkflowDraftHash(response.hash)
|
||||
setDraftUpdatedAt(response.updated_at)
|
||||
}).finally(() => {
|
||||
setIsSyncingWorkflowDraft(false)
|
||||
})
|
||||
}, [handleUpdateWorkflowCanvas, snippetId, workflowStore])
|
||||
|
||||
return {
|
||||
handleRefreshWorkflowDraft,
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import { FlowType } from '@/types/common'
|
||||
export const flowPrefixMap = {
|
||||
[FlowType.appFlow]: 'apps',
|
||||
[FlowType.ragPipeline]: 'rag/pipelines',
|
||||
[FlowType.snippet]: 'snippets',
|
||||
}
|
||||
|
||||
export const getFlowPrefix = (type?: FlowType) => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export enum FlowType {
|
||||
appFlow = 'appFlow',
|
||||
ragPipeline = 'ragPipeline',
|
||||
snippet = 'snippet',
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user