From 87abfbf515a296cd653bee9c991951957a1a2423 Mon Sep 17 00:00:00 2001 From: lyzno1 <92089059+lyzno1@users.noreply.github.com> Date: Wed, 27 Aug 2025 17:49:09 +0800 Subject: [PATCH] Allow empty workflows and improve workflow validation (#24627) --- web/app/components/app-sidebar/basic.tsx | 10 +++---- web/app/components/app/overview/app-card.tsx | 2 +- .../assets/vender/workflow/api-aggregate.svg | 3 +++ .../src/vender/workflow/ApiAggregate.json | 26 ++++++++++++++++++ .../src/vender/workflow/ApiAggregate.tsx | 20 ++++++++++++++ .../base/icons/src/vender/workflow/index.ts | 1 + .../components/tools/mcp/mcp-service-card.tsx | 2 +- .../hooks/use-nodes-sync-draft.ts | 12 +-------- .../workflow/hooks/use-checklist.ts | 2 +- .../workflow/hooks/use-nodes-interactions.ts | 27 +++---------------- web/i18n/en-US/app-overview.ts | 1 + web/i18n/en-US/workflow.ts | 3 ++- web/i18n/ja-JP/app-overview.ts | 1 + web/i18n/ja-JP/workflow.ts | 3 ++- web/i18n/zh-Hans/app-overview.ts | 1 + web/i18n/zh-Hans/workflow.ts | 3 ++- 16 files changed, 72 insertions(+), 45 deletions(-) create mode 100644 web/app/components/base/icons/assets/vender/workflow/api-aggregate.svg create mode 100644 web/app/components/base/icons/src/vender/workflow/ApiAggregate.json create mode 100644 web/app/components/base/icons/src/vender/workflow/ApiAggregate.tsx diff --git a/web/app/components/app-sidebar/basic.tsx b/web/app/components/app-sidebar/basic.tsx index 00357d6c27..17298d8e77 100644 --- a/web/app/components/app-sidebar/basic.tsx +++ b/web/app/components/app-sidebar/basic.tsx @@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next' import AppIcon from '../base/app-icon' import Tooltip from '@/app/components/base/tooltip' import { - Code, + ApiAggregate, WindowCursor, } from '@/app/components/base/icons/src/vender/workflow' @@ -40,8 +40,8 @@ const NotionSvg = , - api:
- + api:
+
, dataset: , webapp:
@@ -56,12 +56,12 @@ export default function AppBasic({ icon, icon_background, name, isExternal, type return (
{icon && icon_background && iconType === 'app' && ( -
+
)} {iconType !== 'app' - &&
+ &&
{ICON_MAP[iconType]}
diff --git a/web/app/components/app/overview/app-card.tsx b/web/app/components/app/overview/app-card.tsx index 0dddfed0f2..14c372670f 100644 --- a/web/app/components/app/overview/app-card.tsx +++ b/web/app/components/app/overview/app-card.tsx @@ -101,7 +101,7 @@ function AppCard({ const isApp = cardType === 'webapp' const basicName = isApp - ? appInfo?.site?.title + ? t('appOverview.overview.appInfo.title') : t('appOverview.overview.apiInfo.title') const hasStartNode = currentWorkflow?.graph?.nodes.find(node => node.data.type === BlockEnum.Start) const isWorkflowAndMissingStart = appInfo.mode === 'workflow' && !hasStartNode diff --git a/web/app/components/base/icons/assets/vender/workflow/api-aggregate.svg b/web/app/components/base/icons/assets/vender/workflow/api-aggregate.svg new file mode 100644 index 0000000000..aaf2206d21 --- /dev/null +++ b/web/app/components/base/icons/assets/vender/workflow/api-aggregate.svg @@ -0,0 +1,3 @@ + + + diff --git a/web/app/components/base/icons/src/vender/workflow/ApiAggregate.json b/web/app/components/base/icons/src/vender/workflow/ApiAggregate.json new file mode 100644 index 0000000000..1057842352 --- /dev/null +++ b/web/app/components/base/icons/src/vender/workflow/ApiAggregate.json @@ -0,0 +1,26 @@ +{ + "icon": { + "type": "element", + "isRootNode": true, + "name": "svg", + "attributes": { + "width": "16", + "height": "16", + "viewBox": "0 0 16 16", + "fill": "none", + "xmlns": "http://www.w3.org/2000/svg" + }, + "children": [ + { + "type": "element", + "name": "path", + "attributes": { + "d": "M5.92578 11.0094C5.92578 10.0174 5.12163 9.21256 4.12956 9.21256C3.13752 9.2126 2.33333 10.0174 2.33333 11.0094C2.33349 12.0014 3.13762 12.8056 4.12956 12.8057C5.12153 12.8057 5.92562 12.0014 5.92578 11.0094ZM13.6667 11.0094C13.6667 10.0174 12.8625 9.2126 11.8704 9.21256C10.8784 9.21256 10.0742 10.0174 10.0742 11.0094C10.0744 12.0014 10.8785 12.8057 11.8704 12.8057C12.8624 12.8056 13.6665 12.0014 13.6667 11.0094ZM9.79622 4.32389C9.79619 3.33186 8.99205 2.52767 8 2.52767C7.00796 2.52767 6.20382 3.33186 6.20378 4.32389C6.20378 5.31596 7.00793 6.12012 8 6.12012C8.99207 6.12012 9.79622 5.31596 9.79622 4.32389ZM11.1296 4.32389C11.1296 5.82351 10.0748 7.07628 8.66667 7.38184V7.9196L9.74284 8.71387C10.3012 8.19607 11.0489 7.87923 11.8704 7.87923C13.5989 7.87927 15 9.28101 15 11.0094C14.9998 12.7377 13.5988 14.139 11.8704 14.139C10.1421 14.139 8.74104 12.7378 8.74089 11.0094C8.74089 10.5837 8.82585 10.1776 8.97982 9.80762L8 9.08366L7.01953 9.80762C7.17356 10.1777 7.25911 10.5836 7.25911 11.0094C7.25896 12.7378 5.85791 14.139 4.12956 14.139C2.40124 14.139 1.00016 12.7377 1 11.0094C1 9.28101 2.40114 7.87927 4.12956 7.87923C4.95094 7.87923 5.69819 8.19627 6.25651 8.71387L7.33333 7.9196V7.38184C5.92523 7.07628 4.87044 5.82351 4.87044 4.32389C4.87048 2.59548 6.27158 1.19434 8 1.19434C9.72843 1.19434 11.1295 2.59548 11.1296 4.32389Z", + "fill": "currentColor" + }, + "children": [] + } + ] + }, + "name": "ApiAggregate" +} diff --git a/web/app/components/base/icons/src/vender/workflow/ApiAggregate.tsx b/web/app/components/base/icons/src/vender/workflow/ApiAggregate.tsx new file mode 100644 index 0000000000..d11e122db2 --- /dev/null +++ b/web/app/components/base/icons/src/vender/workflow/ApiAggregate.tsx @@ -0,0 +1,20 @@ +// GENERATE BY script +// DON NOT EDIT IT MANUALLY + +import * as React from 'react' +import data from './ApiAggregate.json' +import IconBase from '@/app/components/base/icons/IconBase' +import type { IconData } from '@/app/components/base/icons/IconBase' + +const Icon = ( + { + ref, + ...props + }: React.SVGProps & { + ref?: React.RefObject>; + }, +) => + +Icon.displayName = 'ApiAggregate' + +export default Icon diff --git a/web/app/components/base/icons/src/vender/workflow/index.ts b/web/app/components/base/icons/src/vender/workflow/index.ts index 4968aaf12e..8f370d5d02 100644 --- a/web/app/components/base/icons/src/vender/workflow/index.ts +++ b/web/app/components/base/icons/src/vender/workflow/index.ts @@ -1,5 +1,6 @@ export { default as Agent } from './Agent' export { default as Answer } from './Answer' +export { default as ApiAggregate } from './ApiAggregate' export { default as Assigner } from './Assigner' export { default as Asterisk } from './Asterisk' export { default as CalendarCheckLine } from './CalendarCheckLine' diff --git a/web/app/components/tools/mcp/mcp-service-card.tsx b/web/app/components/tools/mcp/mcp-service-card.tsx index 2230d3701c..f9ad9f7e48 100644 --- a/web/app/components/tools/mcp/mcp-service-card.tsx +++ b/web/app/components/tools/mcp/mcp-service-card.tsx @@ -142,7 +142,7 @@ function MCPServiceCard({
-
+
diff --git a/web/app/components/workflow-app/hooks/use-nodes-sync-draft.ts b/web/app/components/workflow-app/hooks/use-nodes-sync-draft.ts index cd7701f2e2..5d131fba5b 100644 --- a/web/app/components/workflow-app/hooks/use-nodes-sync-draft.ts +++ b/web/app/components/workflow-app/hooks/use-nodes-sync-draft.ts @@ -5,7 +5,6 @@ import { useParams } from 'next/navigation' import { useWorkflowStore, } from '@/app/components/workflow/store' -import { BlockEnum } from '@/app/components/workflow/types' import { useNodesReadOnly, } from '@/app/components/workflow/hooks/use-workflow' @@ -38,16 +37,7 @@ export const useNodesSyncDraft = () => { if (appId) { const nodes = getNodes() - const startNodeTypes = [ - BlockEnum.Start, - BlockEnum.TriggerSchedule, - BlockEnum.TriggerWebhook, - BlockEnum.TriggerPlugin, - ] - const hasStartNode = nodes.some(node => startNodeTypes.includes(node.data.type)) - - if (!hasStartNode) - return + // Allow empty workflows - sync restrictions removed to support empty workflow editing const features = featuresStore!.getState().features const producedNodes = produce(nodes, (draft) => { diff --git a/web/app/components/workflow/hooks/use-checklist.ts b/web/app/components/workflow/hooks/use-checklist.ts index 335662a322..87d0fa83c9 100644 --- a/web/app/components/workflow/hooks/use-checklist.ts +++ b/web/app/components/workflow/hooks/use-checklist.ts @@ -166,7 +166,7 @@ export const useChecklist = (nodes: Node[], edges: Edge[]) => { list.push({ id: 'start-node-required', type: BlockEnum.Start, - title: t('workflow.blocks.start'), + title: t('workflow.panel.startNode'), errorMessage: t('workflow.common.needStartNode'), }) } diff --git a/web/app/components/workflow/hooks/use-nodes-interactions.ts b/web/app/components/workflow/hooks/use-nodes-interactions.ts index 2bbd675a19..1ecdcf2ed9 100644 --- a/web/app/components/workflow/hooks/use-nodes-interactions.ts +++ b/web/app/components/workflow/hooks/use-nodes-interactions.ts @@ -18,7 +18,6 @@ import { } from 'reactflow' import { unionBy } from 'lodash-es' import type { ToolDefaultValue } from '../block-selector/types' -import { ENTRY_NODE_TYPES } from '../block-selector/constants' import type { Edge, Node, @@ -64,23 +63,7 @@ import { WorkflowHistoryEvent, useWorkflowHistory } from './use-workflow-history import useInspectVarsCrud from './use-inspect-vars-crud' import { getNodeUsedVars } from '../nodes/_base/components/variable/utils' -// Helper function to check if a node is an entry node -const isEntryNode = (nodeType: BlockEnum): boolean => { - return ENTRY_NODE_TYPES.includes(nodeType as any) -} - -// Helper function to check if entry node can be deleted -const canDeleteEntryNode = (nodes: Node[], nodeId: string): boolean => { - const targetNode = nodes.find(node => node.id === nodeId) - if (!targetNode || !isEntryNode(targetNode.data.type)) - return true // Non-entry nodes can always be deleted - - // Count all entry nodes - const entryNodes = nodes.filter(node => isEntryNode(node.data.type)) - - // Can delete if there's more than one entry node - return entryNodes.length > 1 -} +// Entry node deletion restriction has been removed to allow empty workflows export const useNodesInteractions = () => { const { t } = useTranslation() @@ -568,9 +551,7 @@ export const useNodesInteractions = () => { const nodes = getNodes() - // Check if entry node can be deleted (must keep at least one entry node) - if (!canDeleteEntryNode(nodes, nodeId)) - return // Cannot delete the last entry node + // Allow deleting any node including the last entry node const currentNodeIndex = nodes.findIndex(node => node.id === nodeId) const currentNode = nodes[currentNodeIndex] @@ -1410,7 +1391,7 @@ export const useNodesInteractions = () => { const nodes = getNodes() const bundledNodes = nodes.filter(node => - node.data._isBundled && canDeleteEntryNode(nodes, node.id), + node.data._isBundled, ) if (bundledNodes.length) { @@ -1424,7 +1405,7 @@ export const useNodesInteractions = () => { return const selectedNode = nodes.find(node => - node.data.selected && canDeleteEntryNode(nodes, node.id), + node.data.selected, ) if (selectedNode) diff --git a/web/i18n/en-US/app-overview.ts b/web/i18n/en-US/app-overview.ts index feedc32e6b..078f1a1aea 100644 --- a/web/i18n/en-US/app-overview.ts +++ b/web/i18n/en-US/app-overview.ts @@ -30,6 +30,7 @@ const translation = { overview: { title: 'Overview', appInfo: { + title: 'Web App', explanation: 'Ready-to-use AI web app', accessibleAddress: 'Public URL', preview: 'Preview', diff --git a/web/i18n/en-US/workflow.ts b/web/i18n/en-US/workflow.ts index 5c64d4307e..f7680a1147 100644 --- a/web/i18n/en-US/workflow.ts +++ b/web/i18n/en-US/workflow.ts @@ -46,7 +46,7 @@ const translation = { needConnectTip: 'This step is not connected to anything', maxTreeDepth: 'Maximum limit of {{depth}} nodes per branch', needEndNode: 'The End node must be added', - needStartNode: 'An entry node (User Input or Trigger) must be added', + needStartNode: 'At least one start node must be added', needAnswerNode: 'The Answer node must be added', workflowProcess: 'Workflow Process', notRunning: 'Not running yet', @@ -332,6 +332,7 @@ const translation = { checklist: 'Checklist', checklistTip: 'Make sure all issues are resolved before publishing', checklistResolved: 'All issues are resolved', + startNode: 'Start Node', organizeBlocks: 'Organize nodes', change: 'Change', optional: '(optional)', diff --git a/web/i18n/ja-JP/app-overview.ts b/web/i18n/ja-JP/app-overview.ts index d948bc3b28..3de41b2049 100644 --- a/web/i18n/ja-JP/app-overview.ts +++ b/web/i18n/ja-JP/app-overview.ts @@ -30,6 +30,7 @@ const translation = { overview: { title: '概要', appInfo: { + title: 'Web App', explanation: '使いやすい AI Web アプリ', accessibleAddress: '公開 URL', preview: 'プレビュー', diff --git a/web/i18n/ja-JP/workflow.ts b/web/i18n/ja-JP/workflow.ts index f7a2822315..6b7b1045d6 100644 --- a/web/i18n/ja-JP/workflow.ts +++ b/web/i18n/ja-JP/workflow.ts @@ -46,7 +46,7 @@ const translation = { needConnectTip: '接続されていないステップがあります', maxTreeDepth: '1 ブランチあたりの最大ノード数:{{depth}}', needEndNode: '終了ブロックを追加する必要があります', - needStartNode: '開始ノード(スタートまたはトリガー)を追加する必要があります', + needStartNode: '少なくとも1つのスタートノードを追加する必要があります', needAnswerNode: '回答ブロックを追加する必要があります', workflowProcess: 'ワークフロー処理', notRunning: 'まだ実行されていません', @@ -332,6 +332,7 @@ const translation = { checklist: 'チェックリスト', checklistTip: '公開前に全ての項目を確認してください', checklistResolved: '全てのチェックが完了しました', + startNode: '開始ノード', organizeBlocks: 'ノード整理', change: '変更', optional: '(任意)', diff --git a/web/i18n/zh-Hans/app-overview.ts b/web/i18n/zh-Hans/app-overview.ts index a41a86975a..16d455bc27 100644 --- a/web/i18n/zh-Hans/app-overview.ts +++ b/web/i18n/zh-Hans/app-overview.ts @@ -30,6 +30,7 @@ const translation = { overview: { title: '概览', appInfo: { + title: 'Web App', explanation: '开箱即用的 AI web app', accessibleAddress: '公开访问 URL', preview: '预览', diff --git a/web/i18n/zh-Hans/workflow.ts b/web/i18n/zh-Hans/workflow.ts index 2b403dd794..b55ddfa17c 100644 --- a/web/i18n/zh-Hans/workflow.ts +++ b/web/i18n/zh-Hans/workflow.ts @@ -45,7 +45,7 @@ const translation = { needConnectTip: '此节点尚未连接到其他节点', maxTreeDepth: '每个分支最大限制 {{depth}} 个节点', needEndNode: '必须添加结束节点', - needStartNode: '必须添加开始节点(开始或触发器)', + needStartNode: '必须添加至少一个开始节点', needAnswerNode: '必须添加直接回复节点', workflowProcess: '工作流', notRunning: '尚未运行', @@ -332,6 +332,7 @@ const translation = { checklist: '检查清单', checklistTip: '发布前确保所有问题均已解决', checklistResolved: '所有问题均已解决', + startNode: '开始节点', organizeBlocks: '整理节点', change: '更改', optional: '(选填)',