check before publish

This commit is contained in:
StyleZhang 2024-03-19 17:24:08 +08:00
parent 16a1562900
commit f41a619490
7 changed files with 115 additions and 16 deletions

View File

@ -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,

View File

@ -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')}
</Button>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-[11]'>
@ -109,7 +113,11 @@ const Publish = () => {
onClick={handlePublish}
disabled={published}
>
{t('workflow.common.publish')}
{
published
? t('workflow.common.published')
: t('workflow.common.publish')
}
</Button>
</div>
{

View File

@ -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('_'))

View File

@ -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,
}
}

View File

@ -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,
}
}

View File

@ -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',

View File

@ -27,6 +27,7 @@ const translation = {
variableNamePlaceholder: '变量名',
setVarValuePlaceholder: '设置变量值',
needConnecttip: '此节点尚未连接到其他节点',
maxTreeDepth: '每个分支最大限制 {{depth}} 个节点',
},
errorMsg: {
fieldRequired: '{{field}} 不能为空',