diff --git a/web/src/store/workflow.ts b/web/src/store/workflow.ts new file mode 100644 index 00000000..0999d35a --- /dev/null +++ b/web/src/store/workflow.ts @@ -0,0 +1,21 @@ +/* + * @Author: ZhaoYing + * @Date: 2026-04-10 18:11:19 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2026-04-10 18:11:19 + */ +import { create } from 'zustand' +import type { NodeCheckResult } from '@/views/Workflow/components/CheckList' + +interface WorkflowState { + checkResults: Record + setCheckResults: (appId: string, results: NodeCheckResult[]) => void + getCheckResults: (appId: string) => NodeCheckResult[] +} + +export const useWorkflowStore = create((set, get) => ({ + checkResults: {}, + setCheckResults: (appId, results) => + set(state => ({ checkResults: { ...state.checkResults, [appId]: results } })), + getCheckResults: (appId) => get().checkResults[appId] ?? [], +})) diff --git a/web/src/views/ApplicationConfig/ReleasePage.tsx b/web/src/views/ApplicationConfig/ReleasePage.tsx index 4de226f1..ba573795 100644 --- a/web/src/views/ApplicationConfig/ReleasePage.tsx +++ b/web/src/views/ApplicationConfig/ReleasePage.tsx @@ -16,9 +16,9 @@ import { getReleaseList, rollbackRelease, appExport } from '@/api/application' import ReleaseModal from './components/ReleaseModal' import ReleaseShareModal from './components/ReleaseShareModal' import AppSharingModal from './components/AppSharingModal' -import type { Release, ReleaseModalRef, ReleaseShareModalRef, AppSharingModalRef, WorkflowRef } from './types' +import type { Release, ReleaseModalRef, ReleaseShareModalRef, AppSharingModalRef } from './types' import type { Application } from '@/views/ApplicationManagement/types' -import { runCheckOnGraph } from '@/views/Workflow/components/CheckList' +import { useWorkflowStore } from '@/store/workflow' import Empty from '@/components/Empty' import { formatDateTime } from '@/utils/format'; import Markdown from '@/components/Markdown' @@ -39,9 +39,10 @@ const heightClass = 'rb:max-h-[calc(100vh-140px)]' * @param data - Application data * @param refresh - Function to refresh application data */ -const ReleasePage: FC<{data: Application; refresh: () => void; workflowRef?: React.RefObject}> = ({data, refresh, workflowRef}) => { +const ReleasePage: FC<{data: Application; refresh: () => void}> = ({data, refresh}) => { const { t } = useTranslation(); const { message } = App.useApp() + const { getCheckResults } = useWorkflowStore() const releaseModalRef = useRef(null) const releaseShareModalRef = useRef(null) const appSharingModalRef = useRef(null) @@ -148,13 +149,10 @@ const ReleasePage: FC<{data: Application; refresh: () => void; workflowRef?: Rea } { if (data?.type === 'workflow') { - const graph = workflowRef?.current?.graphRef?.current - if (graph) { - const errors = await runCheckOnGraph(graph, t) - if (errors.length) { - message.error(t('workflow.checkListHasErrors')) - return - } + const errors = getCheckResults(data.id) + if (errors.length) { + message.error(t('workflow.checkListHasErrors')) + return } } releaseModalRef.current?.handleOpen() diff --git a/web/src/views/ApplicationConfig/components/ConfigHeader.tsx b/web/src/views/ApplicationConfig/components/ConfigHeader.tsx index b2e1e36b..d38a657a 100644 --- a/web/src/views/ApplicationConfig/components/ConfigHeader.tsx +++ b/web/src/views/ApplicationConfig/components/ConfigHeader.tsx @@ -207,7 +207,7 @@ const ConfigHeader: FC = ({ } extra={application?.type === 'workflow' && source !== 'sharing' && activeTab === 'arrangement' ? - +
+ appId: string } -interface CheckError { +export interface CheckError { key: string message: string } -interface NodeCheckResult { +export interface NodeCheckResult { id: string name: string type: string @@ -112,10 +114,67 @@ function validateNode(type: string, config: Record): CheckError[] { return errors } -const CheckList: FC = ({ workflowRef }) => { +export async function runCheckOnGraph( + graph: import('@antv/x6').Graph, + t: (key: string) => string +): Promise { + const nodes = graph.getNodes() + const edges = graph.getEdges() + const targetIds = new Set() + const childTargetIds = new Set() + edges.forEach(e => { + targetIds.add(e.getTargetCellId()) + const srcData = graph.getCellById(e.getSourceCellId())?.getData() + const tgtData = graph.getCellById(e.getTargetCellId())?.getData() + if (srcData?.cycle && tgtData?.cycle && srcData.cycle === tgtData.cycle) { + childTargetIds.add(e.getTargetCellId()) + } + }) + + const checked: NodeCheckResult[] = [] + for (const node of nodes) { + const data = node.getData() + if (!data || ['add-node', 'notes', 'cycle-start', 'break'].includes(data.type)) continue + + const errors: CheckError[] = [] + const isChildNode = !!data.cycle + const hasIncoming = isChildNode ? childTargetIds.has(node.id) : !['start', 'cycle-start'].includes(data.type) ? targetIds.has(node.id) : true + if (!hasIncoming) errors.push({ key: 'notConnected', message: t('workflow.notConnected') }) + + const configErrors = validateNode(data.type, data.config ?? {}) + configErrors.forEach(e => { + errors.push({ key: e.key, message: `${t(`workflow.checkListErrors.${e.key}`)} ${t('workflow.cannotBeEmpty')}`.trim() }) + }) + + if (data.type === 'tool') { + const toolId = data.config?.tool_id?.defaultValue ?? data.config?.tool_id + const toolParameters = data.config?.tool_parameters?.defaultValue ?? data.config?.tool_parameters ?? {} + if (toolId) { + try { + const methods = await getToolMethods(toolId) as Array<{ name: string; parameters: Array<{ name: string; required: boolean }> }> + const operation = toolParameters?.operation + const method = operation ? methods.find(m => m.name === operation) : methods[0] + if (method) { + method.parameters + .filter(p => p.required && (toolParameters[p.name] === undefined || toolParameters[p.name] === null || toolParameters[p.name] === '')) + .forEach(p => errors.push({ key: 'tool.tool_parameters', message: `${p.name} ${t('workflow.cannotBeEmpty')}` })) + } + } catch { /* ignore */ } + } + } + + if (errors.length) { + checked.push({ id: node.id, name: data.name || t(`workflow.${data.type}`), type: data.type, icon: nodeIconMap[data.type] ?? '', errors }) + } + } + return checked +} + +const CheckList: FC = ({ workflowRef, appId }) => { const { t } = useTranslation() const [open, setOpen] = useState(false) - const [results, setResults] = useState([]) + const { setCheckResults, getCheckResults } = useWorkflowStore() + const results = getCheckResults(appId) const timerRef = useRef>() const runCheck = useCallback(async () => { @@ -195,7 +254,7 @@ const CheckList: FC = ({ workflowRef }) => { const scheduleCheck = useCallback(() => { clearTimeout(timerRef.current) timerRef.current = setTimeout(async () => { - setResults(await runCheck()) + setCheckResults(appId, await runCheck()) }, 500) }, [runCheck]) @@ -211,7 +270,7 @@ const CheckList: FC = ({ workflowRef }) => { } }, [workflowRef.current?.graphRef?.current]) - const handleOpen = () => { +const handleOpen = () => { setOpen(true) }