| @@ -7,6 +7,7 @@ import { | |||
| useWorkflowStore, | |||
| } from '../store' | |||
| import { BlockEnum } from '../types' | |||
| import { useWorkflowUpdate } from '../hooks' | |||
| import { useNodesReadOnly } from './use-workflow' | |||
| import { syncWorkflowDraft } from '@/service/workflow' | |||
| import { useFeaturesStore } from '@/app/components/base/features/hooks' | |||
| @@ -17,19 +18,23 @@ export const useNodesSyncDraft = () => { | |||
| const workflowStore = useWorkflowStore() | |||
| const featuresStore = useFeaturesStore() | |||
| const { getNodesReadOnly } = useNodesReadOnly() | |||
| const { handleRefreshWorkflowDraft } = useWorkflowUpdate() | |||
| const debouncedSyncWorkflowDraft = useStore(s => s.debouncedSyncWorkflowDraft) | |||
| const params = useParams() | |||
| const getPostParams = useCallback((appIdParams?: string) => { | |||
| const getPostParams = useCallback(() => { | |||
| const { | |||
| getNodes, | |||
| edges, | |||
| transform, | |||
| } = store.getState() | |||
| const [x, y, zoom] = transform | |||
| const appId = workflowStore.getState().appId | |||
| const { | |||
| appId, | |||
| syncWorkflowDraftHash, | |||
| } = workflowStore.getState() | |||
| if (appId || appIdParams) { | |||
| if (appId) { | |||
| const nodes = getNodes() | |||
| const hasStartNode = nodes.find(node => node.data.type === BlockEnum.Start) | |||
| @@ -54,7 +59,7 @@ export const useNodesSyncDraft = () => { | |||
| }) | |||
| }) | |||
| return { | |||
| url: `/apps/${appId || appIdParams}/workflows/draft`, | |||
| url: `/apps/${appId}/workflows/draft`, | |||
| params: { | |||
| graph: { | |||
| nodes: producedNodes, | |||
| @@ -75,6 +80,7 @@ export const useNodesSyncDraft = () => { | |||
| sensitive_word_avoidance: features.moderation, | |||
| file_upload: features.file, | |||
| }, | |||
| hash: syncWorkflowDraftHash, | |||
| }, | |||
| } | |||
| } | |||
| @@ -93,23 +99,38 @@ export const useNodesSyncDraft = () => { | |||
| } | |||
| }, [getPostParams, params.appId, getNodesReadOnly]) | |||
| const doSyncWorkflowDraft = useCallback(async (appId?: string) => { | |||
| const doSyncWorkflowDraft = useCallback(async (notRefreshWhenSyncError?: boolean) => { | |||
| if (getNodesReadOnly()) | |||
| return | |||
| const postParams = getPostParams(appId) | |||
| const postParams = getPostParams() | |||
| if (postParams) { | |||
| const res = await syncWorkflowDraft(postParams) | |||
| workflowStore.getState().setDraftUpdatedAt(res.updated_at) | |||
| const { | |||
| setSyncWorkflowDraftHash, | |||
| setDraftUpdatedAt, | |||
| } = workflowStore.getState() | |||
| try { | |||
| const res = await syncWorkflowDraft(postParams) | |||
| setSyncWorkflowDraftHash(res.hash) | |||
| setDraftUpdatedAt(res.updated_at) | |||
| } | |||
| catch (error: any) { | |||
| if (error && error.json && !error.bodyUsed) { | |||
| error.json().then((err: any) => { | |||
| if (err.code === 'draft_workflow_not_sync' && !notRefreshWhenSyncError) | |||
| handleRefreshWorkflowDraft() | |||
| }) | |||
| } | |||
| } | |||
| } | |||
| }, [workflowStore, getPostParams, getNodesReadOnly]) | |||
| }, [workflowStore, getPostParams, getNodesReadOnly, handleRefreshWorkflowDraft]) | |||
| const handleSyncWorkflowDraft = useCallback((sync?: boolean, appId?: string) => { | |||
| const handleSyncWorkflowDraft = useCallback((sync?: boolean, notRefreshWhenSyncError?: boolean) => { | |||
| if (getNodesReadOnly()) | |||
| return | |||
| if (sync) | |||
| doSyncWorkflowDraft(appId) | |||
| doSyncWorkflowDraft(notRefreshWhenSyncError) | |||
| else | |||
| debouncedSyncWorkflowDraft(doSyncWorkflowDraft) | |||
| }, [debouncedSyncWorkflowDraft, doSyncWorkflowDraft, getNodesReadOnly]) | |||
| @@ -10,13 +10,12 @@ import { | |||
| import { useEdgesInteractions } from './use-edges-interactions' | |||
| import { useNodesInteractions } from './use-nodes-interactions' | |||
| import { useEventEmitterContextContext } from '@/context/event-emitter' | |||
| import { fetchWorkflowDraft } from '@/service/workflow' | |||
| export const useWorkflowInteractions = () => { | |||
| const reactflow = useReactFlow() | |||
| const workflowStore = useWorkflowStore() | |||
| const { handleNodeCancelRunningStatus } = useNodesInteractions() | |||
| const { handleEdgeCancelRunningStatus } = useEdgesInteractions() | |||
| const { eventEmitter } = useEventEmitterContextContext() | |||
| const handleCancelDebugAndPreviewPanel = useCallback(() => { | |||
| workflowStore.setState({ | |||
| @@ -27,6 +26,16 @@ export const useWorkflowInteractions = () => { | |||
| handleEdgeCancelRunningStatus() | |||
| }, [workflowStore, handleNodeCancelRunningStatus, handleEdgeCancelRunningStatus]) | |||
| return { | |||
| handleCancelDebugAndPreviewPanel, | |||
| } | |||
| } | |||
| export const useWorkflowUpdate = () => { | |||
| const reactflow = useReactFlow() | |||
| const workflowStore = useWorkflowStore() | |||
| const { eventEmitter } = useEventEmitterContextContext() | |||
| const handleUpdateWorkflowCanvas = useCallback((payload: WorkflowDataUpdator) => { | |||
| const { | |||
| nodes, | |||
| @@ -44,8 +53,19 @@ export const useWorkflowInteractions = () => { | |||
| setViewport(viewport) | |||
| }, [eventEmitter, reactflow]) | |||
| const handleRefreshWorkflowDraft = useCallback(() => { | |||
| const { | |||
| appId, | |||
| setSyncWorkflowDraftHash, | |||
| } = workflowStore.getState() | |||
| fetchWorkflowDraft(`/apps/${appId}/workflows/draft`).then((response) => { | |||
| handleUpdateWorkflowCanvas(response.graph as WorkflowDataUpdator) | |||
| setSyncWorkflowDraftHash(response.hash) | |||
| }) | |||
| }, [handleUpdateWorkflowCanvas, workflowStore]) | |||
| return { | |||
| handleCancelDebugAndPreviewPanel, | |||
| handleUpdateWorkflowCanvas, | |||
| handleRefreshWorkflowDraft, | |||
| } | |||
| } | |||
| @@ -10,7 +10,7 @@ import { | |||
| NodeRunningStatus, | |||
| WorkflowRunningStatus, | |||
| } from '../types' | |||
| import { useWorkflowInteractions } from './use-workflow-interactions' | |||
| import { useWorkflowUpdate } from './use-workflow-interactions' | |||
| import { useStore as useAppStore } from '@/app/components/app/store' | |||
| import type { IOtherOptions } from '@/service/base' | |||
| import { ssePost } from '@/service/base' | |||
| @@ -26,7 +26,7 @@ export const useWorkflowRun = () => { | |||
| const reactflow = useReactFlow() | |||
| const featuresStore = useFeaturesStore() | |||
| const { doSyncWorkflowDraft } = useNodesSyncDraft() | |||
| const { handleUpdateWorkflowCanvas } = useWorkflowInteractions() | |||
| const { handleUpdateWorkflowCanvas } = useWorkflowUpdate() | |||
| const handleBackupDraft = useCallback(() => { | |||
| const { | |||
| @@ -385,6 +385,7 @@ export const useWorkflowInit = () => { | |||
| } = useWorkflowTemplate() | |||
| const { handleFetchAllTools } = useFetchToolsData() | |||
| const appDetail = useAppStore(state => state.appDetail)! | |||
| const setSyncWorkflowDraftHash = useStore(s => s.setSyncWorkflowDraftHash) | |||
| const [data, setData] = useState<FetchWorkflowDraftResponse>() | |||
| const [isLoading, setIsLoading] = useState(true) | |||
| workflowStore.setState({ appId: appDetail.id }) | |||
| @@ -394,6 +395,7 @@ export const useWorkflowInit = () => { | |||
| const res = await fetchWorkflowDraft(`/apps/${appDetail.id}/workflows/draft`) | |||
| setData(res) | |||
| setSyncWorkflowDraftHash(res.hash) | |||
| setIsLoading(false) | |||
| } | |||
| catch (error: any) { | |||
| @@ -418,7 +420,7 @@ export const useWorkflowInit = () => { | |||
| }) | |||
| } | |||
| } | |||
| }, [appDetail, nodesTemplate, edgesTemplate, workflowStore]) | |||
| }, [appDetail, nodesTemplate, edgesTemplate, workflowStore, setSyncWorkflowDraftHash]) | |||
| useEffect(() => { | |||
| handleGetInitialWorkflowData() | |||
| @@ -42,6 +42,7 @@ import { | |||
| useWorkflowInit, | |||
| useWorkflowReadOnly, | |||
| useWorkflowStartRun, | |||
| useWorkflowUpdate, | |||
| } from './hooks' | |||
| import Header from './header' | |||
| import CustomNode from './nodes' | |||
| @@ -119,14 +120,17 @@ const Workflow: FC<WorkflowProps> = memo(({ | |||
| useEffect(() => { | |||
| return () => { | |||
| handleSyncWorkflowDraft(true) | |||
| handleSyncWorkflowDraft(true, true) | |||
| } | |||
| }, []) | |||
| const { handleRefreshWorkflowDraft } = useWorkflowUpdate() | |||
| const handleSyncWorkflowDraftWhenPageClose = useCallback(() => { | |||
| if (document.visibilityState === 'hidden') | |||
| syncWorkflowDraftWhenPageClose() | |||
| }, [syncWorkflowDraftWhenPageClose]) | |||
| else if (document.visibilityState === 'visible') | |||
| handleRefreshWorkflowDraft() | |||
| }, [syncWorkflowDraftWhenPageClose, handleRefreshWorkflowDraft]) | |||
| useEffect(() => { | |||
| document.addEventListener('visibilitychange', handleSyncWorkflowDraftWhenPageClose) | |||
| @@ -96,6 +96,8 @@ type Shape = { | |||
| setNodeMenu: (nodeMenu: Shape['nodeMenu']) => void | |||
| mousePosition: { pageX: number; pageY: number; elementX: number; elementY: number } | |||
| setMousePosition: (mousePosition: Shape['mousePosition']) => void | |||
| syncWorkflowDraftHash: string | |||
| setSyncWorkflowDraftHash: (hash: string) => void | |||
| } | |||
| export const createWorkflowStore = () => { | |||
| @@ -164,6 +166,8 @@ export const createWorkflowStore = () => { | |||
| setNodeMenu: nodeMenu => set(() => ({ nodeMenu })), | |||
| mousePosition: { pageX: 0, pageY: 0, elementX: 0, elementY: 0 }, | |||
| setMousePosition: mousePosition => set(() => ({ mousePosition })), | |||
| syncWorkflowDraftHash: '', | |||
| setSyncWorkflowDraftHash: syncWorkflowDraftHash => set(() => ({ syncWorkflowDraftHash })), | |||
| })) | |||
| } | |||
| @@ -9,12 +9,12 @@ import type { | |||
| } from '@/types/workflow' | |||
| import type { BlockEnum } from '@/app/components/workflow/types' | |||
| export const fetchWorkflowDraft: Fetcher<FetchWorkflowDraftResponse, string> = (url) => { | |||
| return get<FetchWorkflowDraftResponse>(url, {}, { silent: true }) | |||
| export const fetchWorkflowDraft = (url: string) => { | |||
| return get(url, {}, { silent: true }) as Promise<FetchWorkflowDraftResponse> | |||
| } | |||
| export const syncWorkflowDraft = ({ url, params }: { url: string; params: Pick<FetchWorkflowDraftResponse, 'graph' | 'features'> }) => { | |||
| return post<CommonResponse & { updated_at: number }>(url, { body: params }) | |||
| return post<CommonResponse & { updated_at: number; hash: string }>(url, { body: params }, { silent: true }) | |||
| } | |||
| export const fetchNodesDefaultConfigs: Fetcher<NodesDefaultConfigsResponse, string> = (url) => { | |||
| @@ -48,6 +48,7 @@ export type FetchWorkflowDraftResponse = { | |||
| name: string | |||
| email: string | |||
| } | |||
| hash: string | |||
| updated_at: number | |||
| } | |||