mirror of https://github.com/langgenius/dify.git
Allow empty workflows and improve workflow validation (#24627)
This commit is contained in:
parent
73e65fd838
commit
87abfbf515
|
|
@ -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 = <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xm
|
|||
|
||||
const ICON_MAP = {
|
||||
app: <AppIcon className='border !border-[rgba(0,0,0,0.05)]' />,
|
||||
api: <div className='rounded-lg border-[0.5px] border-divider-subtle bg-util-colors-violet-violet-500 p-1 shadow-md'>
|
||||
<Code className='h-4 w-4 text-text-primary-on-surface' />
|
||||
api: <div className='rounded-lg border-[0.5px] border-divider-subtle bg-util-colors-blue-brand-blue-brand-500 p-1 shadow-md'>
|
||||
<ApiAggregate className='h-4 w-4 text-text-primary-on-surface' />
|
||||
</div>,
|
||||
dataset: <AppIcon innerIcon={DatasetSvg} className='!border-[0.5px] !border-indigo-100 !bg-indigo-25' />,
|
||||
webapp: <div className='rounded-lg border-[0.5px] border-divider-subtle bg-util-colors-blue-brand-blue-brand-500 p-1 shadow-md'>
|
||||
|
|
@ -56,12 +56,12 @@ export default function AppBasic({ icon, icon_background, name, isExternal, type
|
|||
return (
|
||||
<div className="flex grow items-center">
|
||||
{icon && icon_background && iconType === 'app' && (
|
||||
<div className='mr-3 shrink-0'>
|
||||
<div className='mr-2 shrink-0'>
|
||||
<AppIcon icon={icon} background={icon_background} />
|
||||
</div>
|
||||
)}
|
||||
{iconType !== 'app'
|
||||
&& <div className='mr-3 shrink-0'>
|
||||
&& <div className='mr-2 shrink-0'>
|
||||
{ICON_MAP[iconType]}
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path 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="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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<SVGSVGElement> & {
|
||||
ref?: React.RefObject<React.MutableRefObject<HTMLOrSVGElement>>;
|
||||
},
|
||||
) => <IconBase {...props} ref={ref} data={data as IconData} />
|
||||
|
||||
Icon.displayName = 'ApiAggregate'
|
||||
|
||||
export default Icon
|
||||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -142,7 +142,7 @@ function MCPServiceCard({
|
|||
<div className='flex w-full flex-col items-start justify-center gap-3 self-stretch border-b-[0.5px] border-divider-subtle p-3'>
|
||||
<div className='flex w-full items-center gap-3 self-stretch'>
|
||||
<div className='flex grow items-center'>
|
||||
<div className='mr-3 shrink-0 rounded-lg border-[0.5px] border-divider-subtle bg-util-colors-indigo-indigo-500 p-1 shadow-md'>
|
||||
<div className='mr-2 shrink-0 rounded-lg border-[0.5px] border-divider-subtle bg-util-colors-indigo-indigo-500 p-1 shadow-md'>
|
||||
<Mcp className='h-4 w-4 text-text-primary-on-surface' />
|
||||
</div>
|
||||
<div className="group w-full">
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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)',
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ const translation = {
|
|||
overview: {
|
||||
title: '概要',
|
||||
appInfo: {
|
||||
title: 'Web App',
|
||||
explanation: '使いやすい AI Web アプリ',
|
||||
accessibleAddress: '公開 URL',
|
||||
preview: 'プレビュー',
|
||||
|
|
|
|||
|
|
@ -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: '(任意)',
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ const translation = {
|
|||
overview: {
|
||||
title: '概览',
|
||||
appInfo: {
|
||||
title: 'Web App',
|
||||
explanation: '开箱即用的 AI web app',
|
||||
accessibleAddress: '公开访问 URL',
|
||||
preview: '预览',
|
||||
|
|
|
|||
|
|
@ -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: '(选填)',
|
||||
|
|
|
|||
Loading…
Reference in New Issue