From f41a6194905f9d5bf62cbcf7cb059424bb4f1810 Mon Sep 17 00:00:00 2001 From: StyleZhang Date: Tue, 19 Mar 2024 17:24:08 +0800 Subject: [PATCH] check before publish --- web/app/components/workflow/constants.ts | 2 +- .../components/workflow/header/publish.tsx | 36 +++++++++------ .../workflow/hooks/use-nodes-sync-draft.ts | 9 +++- .../workflow/hooks/use-workflow-run.ts | 37 +++++++++++++++ .../components/workflow/hooks/use-workflow.ts | 45 +++++++++++++++++++ web/i18n/en-US/workflow.ts | 1 + web/i18n/zh-Hans/workflow.ts | 1 + 7 files changed, 115 insertions(+), 16 deletions(-) diff --git a/web/app/components/workflow/constants.ts b/web/app/components/workflow/constants.ts index 581ce2c8b7..e6e436d04c 100644 --- a/web/app/components/workflow/constants.ts +++ b/web/app/components/workflow/constants.ts @@ -230,7 +230,7 @@ export const NODE_WIDTH = 220 export const X_OFFSET = 64 export const NODE_WIDTH_X_OFFSET = NODE_WIDTH + X_OFFSET export const Y_OFFSET = 39 -export const TREE_DEEPTH = 30 +export const MAX_TREE_DEEPTH = 30 export const START_INITIAL_POSITION = { x: 80, y: 282 } export const AUTO_LAYOUT_OFFSET = { x: -42, diff --git a/web/app/components/workflow/header/publish.tsx b/web/app/components/workflow/header/publish.tsx index 83641323e5..7cc4043cd6 100644 --- a/web/app/components/workflow/header/publish.tsx +++ b/web/app/components/workflow/header/publish.tsx @@ -11,6 +11,7 @@ import { import { useNodesSyncDraft, useWorkflow, + useWorkflowRun, } from '../hooks' import Button from '@/app/components/base/button' import { @@ -20,12 +21,15 @@ import { } from '@/app/components/base/portal-to-follow-elem' import { publishWorkflow } from '@/service/workflow' import { useStore as useAppStore } from '@/app/components/app/store' +import { useToastContext } from '@/app/components/base/toast' const Publish = () => { const { t } = useTranslation() + const { notify } = useToastContext() const [published, setPublished] = useState(false) const workflowStore = useWorkflowStore() const { formatTimeFromNow } = useWorkflow() + const { handleCheckBeforePublish } = useWorkflowRun() const { handleSyncWorkflowDraft } = useNodesSyncDraft() const runningStatus = useStore(s => s.runningStatus) const draftUpdatedAt = useStore(s => s.draftUpdatedAt) @@ -34,16 +38,20 @@ const Publish = () => { const handlePublish = async () => { const appId = useAppStore.getState().appDetail?.id - try { - const res = await publishWorkflow(`/apps/${appId}/workflows/publish`) - if (res) { - setPublished(true) - workflowStore.getState().setPublishedAt(res.created_at) + if (handleCheckBeforePublish()) { + try { + const res = await publishWorkflow(`/apps/${appId}/workflows/publish`) + + if (res) { + notify({ type: 'success', message: t('common.api.actionSuccess') }) + setPublished(true) + workflowStore.getState().setPublishedAt(res.created_at) + } + } + catch (e) { + setPublished(false) } - } - catch (e) { - setPublished(false) } } @@ -84,11 +92,7 @@ const Publish = () => { ${runningStatus && 'cursor-not-allowed opacity-50'} `} > - { - published - ? t('workflow.common.published') - : t('workflow.common.publish') - } + {t('workflow.common.publish')} @@ -109,7 +113,11 @@ const Publish = () => { onClick={handlePublish} disabled={published} > - {t('workflow.common.publish')} + { + published + ? t('workflow.common.published') + : t('workflow.common.publish') + } { diff --git a/web/app/components/workflow/hooks/use-nodes-sync-draft.ts b/web/app/components/workflow/hooks/use-nodes-sync-draft.ts index 32d3f2169d..e21129a2c0 100644 --- a/web/app/components/workflow/hooks/use-nodes-sync-draft.ts +++ b/web/app/components/workflow/hooks/use-nodes-sync-draft.ts @@ -8,6 +8,7 @@ import { useStore, useWorkflowStore, } from '../store' +import { BlockEnum } from '../types' import { syncWorkflowDraft } from '@/service/workflow' import { useFeaturesStore } from '@/app/components/base/features/hooks' import { useStore as useAppStore } from '@/app/components/app/store' @@ -28,8 +29,14 @@ export const useNodesSyncDraft = () => { const appId = useAppStore.getState().appDetail?.id if (appId) { + const nodes = getNodes() + const hasStartNode = nodes.find(node => node.data.type === BlockEnum.Start) + + if (!hasStartNode) + return + const features = featuresStore!.getState().features - const producedNodes = produce(getNodes(), (draft) => { + const producedNodes = produce(nodes, (draft) => { draft.forEach((node) => { Object.keys(node.data).forEach((key) => { if (key.startsWith('_')) diff --git a/web/app/components/workflow/hooks/use-workflow-run.ts b/web/app/components/workflow/hooks/use-workflow-run.ts index 93fe47fe10..a40dbb7334 100644 --- a/web/app/components/workflow/hooks/use-workflow-run.ts +++ b/web/app/components/workflow/hooks/use-workflow-run.ts @@ -3,12 +3,16 @@ import { useReactFlow, useStoreApi, } from 'reactflow' +import { useTranslation } from 'react-i18next' import produce from 'immer' import { useWorkflowStore } from '../store' import { NodeRunningStatus, WorkflowRunningStatus, } from '../types' +import { MAX_TREE_DEEPTH } from '../constants' +import { useNodesExtraData } from './use-nodes-data' +import { useWorkflow } from './use-workflow' import { useStore as useAppStore } from '@/app/components/app/store' import type { IOtherOptions } from '@/service/base' import { ssePost } from '@/service/base' @@ -17,12 +21,17 @@ import { stopWorkflowRun, } from '@/service/workflow' import { useFeaturesStore } from '@/app/components/base/features/hooks' +import { useToastContext } from '@/app/components/base/toast' export const useWorkflowRun = () => { + const { t } = useTranslation() + const { notify } = useToastContext() const store = useStoreApi() const workflowStore = useWorkflowStore() const reactflow = useReactFlow() const featuresStore = useFeaturesStore() + const nodesExtraData = useNodesExtraData() + const { getValidTreeNodes } = useWorkflow() const handleBackupDraft = useCallback(() => { const { @@ -206,11 +215,39 @@ export const useWorkflowRun = () => { } }, [store, reactflow, featuresStore, workflowStore]) + const handleCheckBeforePublish = useCallback(() => { + const { + validNodes, + maxDepth, + } = getValidTreeNodes() + + if (!validNodes.length) + return false + + if (maxDepth > MAX_TREE_DEEPTH) { + notify({ type: 'error', message: t('workflow.common.maxTreeDepth', { depth: MAX_TREE_DEEPTH }) }) + return false + } + + for (let i = 0; i < validNodes.length; i++) { + const node = validNodes[i] + const { errorMessage } = nodesExtraData[node.data.type].checkValid(node.data, t) + + if (errorMessage) { + notify({ type: 'error', message: `[${node.data.title}] ${errorMessage}` }) + return false + } + } + + return true + }, [getValidTreeNodes, nodesExtraData, notify, t]) + return { handleBackupDraft, handleRunSetting, handleRun, handleStopRun, handleRestoreFromPublishedWorkflow, + handleCheckBeforePublish, } } diff --git a/web/app/components/workflow/hooks/use-workflow.ts b/web/app/components/workflow/hooks/use-workflow.ts index 50a44ea18a..72c1b45446 100644 --- a/web/app/components/workflow/hooks/use-workflow.ts +++ b/web/app/components/workflow/hooks/use-workflow.ts @@ -220,6 +220,50 @@ export const useWorkflow = () => { return dayjs(time).locale(locale === 'zh-Hans' ? 'zh-cn' : locale).fromNow() }, [locale]) + const getValidTreeNodes = useCallback(() => { + const { + getNodes, + edges, + } = store.getState() + const nodes = getNodes() + + const startNode = nodes.find(node => node.data.type === BlockEnum.Start) + + if (!startNode) { + return { + validNodes: [], + maxDepth: 0, + } + } + + const list: Node[] = [startNode] + let maxDepth = 1 + + const traverse = (root: Node, depth: number) => { + if (depth > maxDepth) + maxDepth = depth + + const outgoers = getOutgoers(root, nodes, edges) + + if (outgoers.length) { + outgoers.forEach((outgoer) => { + list.push(outgoer) + traverse(outgoer, depth + 1) + }) + } + else { + list.push(root) + } + } + + traverse(startNode, maxDepth) + + return { + validNodes: uniqBy(list, 'id'), + maxDepth, + } + }, [store]) + return { handleLayout, getTreeLeafNodes, @@ -227,6 +271,7 @@ export const useWorkflow = () => { getAfterNodesInSameBranch, isValidConnection, formatTimeFromNow, + getValidTreeNodes, } } diff --git a/web/i18n/en-US/workflow.ts b/web/i18n/en-US/workflow.ts index 22f710d39b..2c4cd1cc59 100644 --- a/web/i18n/en-US/workflow.ts +++ b/web/i18n/en-US/workflow.ts @@ -27,6 +27,7 @@ const translation = { variableNamePlaceholder: 'Variable name', setVarValuePlaceholder: 'Set variable', needConnecttip: 'This step is not connected to anything', + maxTreeDepth: 'Maximum limit of {{depth}} nodes per branch', }, errorMsg: { fieldRequired: '{{field}} is required', diff --git a/web/i18n/zh-Hans/workflow.ts b/web/i18n/zh-Hans/workflow.ts index 4cbb19af7d..ce00caf520 100644 --- a/web/i18n/zh-Hans/workflow.ts +++ b/web/i18n/zh-Hans/workflow.ts @@ -27,6 +27,7 @@ const translation = { variableNamePlaceholder: '变量名', setVarValuePlaceholder: '设置变量值', needConnecttip: '此节点尚未连接到其他节点', + maxTreeDepth: '每个分支最大限制 {{depth}} 个节点', }, errorMsg: { fieldRequired: '{{field}} 不能为空',