mirror of
https://github.com/langgenius/dify.git
synced 2026-05-06 02:18:08 +08:00
Allow empty workflows and improve workflow validation (#24627)
This commit is contained in:
@ -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)
|
||||
|
||||
Reference in New Issue
Block a user