mirror of
https://github.com/langgenius/dify.git
synced 2026-09-08 11:04:27 +08:00
feat(web): snippet graph draft sync
This commit is contained in:
parent
0de1f17e5c
commit
d01428b5bc
@ -10,6 +10,27 @@ vi.mock('../hooks/use-snippet-init', () => ({
|
|||||||
useSnippetInit: (snippetId: string) => mockUseSnippetInit(snippetId),
|
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', () => ({
|
vi.mock('@/next/navigation', () => ({
|
||||||
useRouter: () => ({
|
useRouter: () => ({
|
||||||
replace: vi.fn(),
|
replace: vi.fn(),
|
||||||
|
|||||||
@ -9,7 +9,11 @@ import {
|
|||||||
RiTerminalWindowFill,
|
RiTerminalWindowFill,
|
||||||
RiTerminalWindowLine,
|
RiTerminalWindowLine,
|
||||||
} from '@remixicon/react'
|
} from '@remixicon/react'
|
||||||
import { useEffect, useMemo, useState } from 'react'
|
import {
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
} from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useShallow } from 'zustand/react/shallow'
|
import { useShallow } from 'zustand/react/shallow'
|
||||||
import AppSideBar from '@/app/components/app-sidebar'
|
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 Evaluation from '@/app/components/evaluation'
|
||||||
import { WorkflowWithInnerContext } from '@/app/components/workflow'
|
import { WorkflowWithInnerContext } from '@/app/components/workflow'
|
||||||
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
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 { useSnippetDetailStore } from '../store'
|
||||||
import SnippetChildren from './snippet-children'
|
import SnippetChildren from './snippet-children'
|
||||||
|
|
||||||
@ -52,6 +59,12 @@ const SnippetMain = ({
|
|||||||
const media = useBreakpoints()
|
const media = useBreakpoints()
|
||||||
const isMobile = media === MediaType.mobile
|
const isMobile = media === MediaType.mobile
|
||||||
const [fields, setFields] = useState<SnippetInputField[]>(payload.inputFields)
|
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 setAppSidebarExpand = useAppStore(state => state.setAppSidebarExpand)
|
||||||
const {
|
const {
|
||||||
editingField,
|
editingField,
|
||||||
@ -130,6 +143,15 @@ const SnippetMain = ({
|
|||||||
setInputPanelOpen(false)
|
setInputPanelOpen(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hooksStore = useMemo(() => {
|
||||||
|
return {
|
||||||
|
doSyncWorkflowDraft,
|
||||||
|
syncWorkflowDraftWhenPageClose,
|
||||||
|
handleRefreshWorkflowDraft,
|
||||||
|
configsMap,
|
||||||
|
}
|
||||||
|
}, [configsMap, doSyncWorkflowDraft, handleRefreshWorkflowDraft, syncWorkflowDraftWhenPageClose])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative flex h-full overflow-hidden bg-background-body">
|
<div className="relative flex h-full overflow-hidden bg-background-body">
|
||||||
<AppSideBar
|
<AppSideBar
|
||||||
@ -166,6 +188,7 @@ const SnippetMain = ({
|
|||||||
nodes={nodes}
|
nodes={nodes}
|
||||||
edges={edges}
|
edges={edges}
|
||||||
viewport={viewport ?? graph.viewport}
|
viewport={viewport ?? graph.viewport}
|
||||||
|
hooksStore={hooksStore}
|
||||||
>
|
>
|
||||||
<SnippetChildren
|
<SnippetChildren
|
||||||
fields={fields}
|
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 = {
|
export const flowPrefixMap = {
|
||||||
[FlowType.appFlow]: 'apps',
|
[FlowType.appFlow]: 'apps',
|
||||||
[FlowType.ragPipeline]: 'rag/pipelines',
|
[FlowType.ragPipeline]: 'rag/pipelines',
|
||||||
|
[FlowType.snippet]: 'snippets',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getFlowPrefix = (type?: FlowType) => {
|
export const getFlowPrefix = (type?: FlowType) => {
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
export enum FlowType {
|
export enum FlowType {
|
||||||
appFlow = 'appFlow',
|
appFlow = 'appFlow',
|
||||||
ragPipeline = 'ragPipeline',
|
ragPipeline = 'ragPipeline',
|
||||||
|
snippet = 'snippet',
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user