-
{
+ if (disabled)
+ return
if (hasAction) {
setIsFold(!isFold)
return
@@ -125,13 +139,14 @@ const TriggerPluginItem: FC
= ({
})
}}
>
-
+
-
+
{notShowProvider ? actions[0]?.label[language] : payload.label[language]}
{groupName}
@@ -142,20 +157,33 @@ const TriggerPluginItem: FC
= ({
)}
-
+
{!notShowProvider && hasAction && !isFold && (
- actions.map(action => (
-
- ))
+
+
+
+ {actions.map(action => (
+
+ ))}
+
+
+
+
+
+
)}
diff --git a/web/app/components/workflow/block-selector/trigger-plugin/list.tsx b/web/app/components/workflow/block-selector/trigger-plugin/list.tsx
index 2d2752c4f67..d73ac3985dd 100644
--- a/web/app/components/workflow/block-selector/trigger-plugin/list.tsx
+++ b/web/app/components/workflow/block-selector/trigger-plugin/list.tsx
@@ -14,12 +14,14 @@ type TriggerPluginListProps = {
searchText: string
onContentStateChange?: (hasContent: boolean) => void
tags?: string[]
+ disabled?: boolean
}
const TriggerPluginList = ({
onSelect,
searchText,
onContentStateChange,
+ disabled = false,
}: TriggerPluginListProps) => {
const { data: triggerPluginsData } = useAllTriggerPlugins()
const language = useGetLanguage()
@@ -99,6 +101,7 @@ const TriggerPluginList = ({
key={plugin.id}
payload={plugin}
onSelect={onSelect}
+ disabled={disabled}
hasSearchText={!!searchText}
previewCardHandle={previewCardHandle}
/>
diff --git a/web/app/components/workflow/hooks/__tests__/use-available-blocks.spec.ts b/web/app/components/workflow/hooks/__tests__/use-available-blocks.spec.ts
index 0989b08cbdb..955c514c81b 100644
--- a/web/app/components/workflow/hooks/__tests__/use-available-blocks.spec.ts
+++ b/web/app/components/workflow/hooks/__tests__/use-available-blocks.spec.ts
@@ -11,6 +11,7 @@ vi.mock('@/context/i18n', () => ({ useGetLanguage: () => 'en' }))
const mockNodeTypes = [
BlockEnum.Start,
+ BlockEnum.StartPlaceholder,
BlockEnum.End,
BlockEnum.LLM,
BlockEnum.Code,
@@ -56,6 +57,11 @@ describe('useAvailableBlocks', () => {
expect(result.current.availablePrevBlocks).toEqual([])
})
+ it('should return empty array for StartPlaceholder node', () => {
+ const { result } = renderWorkflowHook(() => useAvailableBlocks(BlockEnum.StartPlaceholder), { hooksStoreProps })
+ expect(result.current.availablePrevBlocks).toEqual([])
+ })
+
it('should return empty array for trigger nodes', () => {
for (const trigger of [BlockEnum.TriggerPlugin, BlockEnum.TriggerWebhook, BlockEnum.TriggerSchedule]) {
const { result } = renderWorkflowHook(() => useAvailableBlocks(trigger), { hooksStoreProps })
@@ -97,9 +103,15 @@ describe('useAvailableBlocks', () => {
expect(result.current.availableNextBlocks).toEqual([])
})
+ it('should return empty array for StartPlaceholder node', () => {
+ const { result } = renderWorkflowHook(() => useAvailableBlocks(BlockEnum.StartPlaceholder), { hooksStoreProps })
+ expect(result.current.availableNextBlocks).toEqual([])
+ })
+
it('should return all available nodes for regular block types', () => {
const { result } = renderWorkflowHook(() => useAvailableBlocks(BlockEnum.LLM), { hooksStoreProps })
expect(result.current.availableNextBlocks.length).toBeGreaterThan(0)
+ expect(result.current.availableNextBlocks).not.toContain(BlockEnum.StartPlaceholder)
})
})
@@ -144,6 +156,14 @@ describe('useAvailableBlocks', () => {
expect(blocks.availablePrevBlocks).toEqual([])
})
+ it('should return no blocks for StartPlaceholder node', () => {
+ const { result } = renderWorkflowHook(() => useAvailableBlocks(BlockEnum.LLM), { hooksStoreProps })
+ const blocks = result.current.getAvailableBlocks(BlockEnum.StartPlaceholder)
+
+ expect(blocks.availablePrevBlocks).toEqual([])
+ expect(blocks.availableNextBlocks).toEqual([])
+ })
+
it('should return empty nextBlocks for LoopEnd/KnowledgeBase and available nodes for End', () => {
const { result } = renderWorkflowHook(() => useAvailableBlocks(BlockEnum.LLM), { hooksStoreProps })
diff --git a/web/app/components/workflow/hooks/__tests__/use-checklist.spec.ts b/web/app/components/workflow/hooks/__tests__/use-checklist.spec.ts
index ab9eb993d1f..ac7e11e113d 100644
--- a/web/app/components/workflow/hooks/__tests__/use-checklist.spec.ts
+++ b/web/app/components/workflow/hooks/__tests__/use-checklist.spec.ts
@@ -256,6 +256,31 @@ describe('useChecklist', () => {
expect(startRequired!.canNavigate).toBe(false)
})
+ it('should not report the global missing start node item when a start placeholder is present', () => {
+ mockNodesMap[BlockEnum.StartPlaceholder] = {
+ checkValid: () => ({ errorMessage: 'workflow.nodes.startPlaceholder.validationRequired' }),
+ metaData: { isStart: false, isRequired: false },
+ }
+ const placeholderNode = createNode({
+ id: 'start-placeholder',
+ data: { type: BlockEnum.StartPlaceholder, title: 'Workflow start' },
+ })
+
+ const { result } = renderWorkflowHook(
+ () => useChecklist([placeholderNode], []),
+ )
+
+ expect(result.current.find((item: ChecklistItem) => item.id === 'start-node-required')).toBeUndefined()
+ expect(result.current).toEqual([
+ expect.objectContaining({
+ id: 'start-placeholder',
+ type: BlockEnum.StartPlaceholder,
+ unConnected: false,
+ errorMessages: ['workflow.nodes.startPlaceholder.validationRequired'],
+ }),
+ ])
+ })
+
it('should detect plugin not installed', () => {
const startNode = createNode({ id: 'start', data: { type: BlockEnum.Start, title: 'Start' } })
const toolNode = createNode({
diff --git a/web/app/components/workflow/hooks/use-available-blocks.ts b/web/app/components/workflow/hooks/use-available-blocks.ts
index 888e1264744..c42eee5345f 100644
--- a/web/app/components/workflow/hooks/use-available-blocks.ts
+++ b/web/app/components/workflow/hooks/use-available-blocks.ts
@@ -6,6 +6,9 @@ import { BlockEnum } from '../types'
import { useNodesMetaData } from './use-nodes-meta-data'
const availableBlocksFilter = (nodeType: BlockEnum, inContainer?: boolean) => {
+ if (nodeType === BlockEnum.StartPlaceholder)
+ return false
+
if (inContainer && (nodeType === BlockEnum.Iteration || nodeType === BlockEnum.Loop || nodeType === BlockEnum.End || nodeType === BlockEnum.DataSource || nodeType === BlockEnum.KnowledgeBase || nodeType === BlockEnum.HumanInput))
return false
@@ -21,7 +24,7 @@ export const useAvailableBlocks = (nodeType?: BlockEnum, inContainer?: boolean)
} = useNodesMetaData()
const availableNodesType = useMemo(() => availableNodes.map(node => node.metaData.type), [availableNodes])
const availablePrevBlocks = useMemo(() => {
- if (!nodeType || nodeType === BlockEnum.Start || nodeType === BlockEnum.DataSource
+ if (!nodeType || nodeType === BlockEnum.Start || nodeType === BlockEnum.StartPlaceholder || nodeType === BlockEnum.DataSource
|| nodeType === BlockEnum.TriggerPlugin || nodeType === BlockEnum.TriggerWebhook
|| nodeType === BlockEnum.TriggerSchedule) {
return []
@@ -30,7 +33,7 @@ export const useAvailableBlocks = (nodeType?: BlockEnum, inContainer?: boolean)
return availableNodesType
}, [availableNodesType, nodeType])
const availableNextBlocks = useMemo(() => {
- if (!nodeType || nodeType === BlockEnum.LoopEnd || nodeType === BlockEnum.KnowledgeBase)
+ if (!nodeType || nodeType === BlockEnum.StartPlaceholder || nodeType === BlockEnum.LoopEnd || nodeType === BlockEnum.KnowledgeBase)
return []
return availableNodesType
@@ -38,11 +41,11 @@ export const useAvailableBlocks = (nodeType?: BlockEnum, inContainer?: boolean)
const getAvailableBlocks = useCallback((nodeType?: BlockEnum, inContainer?: boolean) => {
let availablePrevBlocks = availableNodesType
- if (!nodeType || nodeType === BlockEnum.Start || nodeType === BlockEnum.DataSource)
+ if (!nodeType || nodeType === BlockEnum.Start || nodeType === BlockEnum.StartPlaceholder || nodeType === BlockEnum.DataSource)
availablePrevBlocks = []
let availableNextBlocks = availableNodesType
- if (!nodeType || nodeType === BlockEnum.LoopEnd || nodeType === BlockEnum.KnowledgeBase)
+ if (!nodeType || nodeType === BlockEnum.StartPlaceholder || nodeType === BlockEnum.LoopEnd || nodeType === BlockEnum.KnowledgeBase)
availableNextBlocks = []
return {
diff --git a/web/app/components/workflow/hooks/use-checklist.ts b/web/app/components/workflow/hooks/use-checklist.ts
index 2f74c66d5f4..2eb73ae2a2c 100644
--- a/web/app/components/workflow/hooks/use-checklist.ts
+++ b/web/app/components/workflow/hooks/use-checklist.ts
@@ -310,7 +310,8 @@ export const useChecklist = (nodes: Node[], edges: Edge[], options?: { flowType?
}
const isStartNodeMeta = nodesExtraData?.[node!.data.type as BlockEnum]?.metaData.isStart ?? false
- const canSkipConnectionCheck = shouldCheckStartNode ? isStartNodeMeta : true
+ const isStartPlaceholderNode = node!.data.type === BlockEnum.StartPlaceholder
+ const canSkipConnectionCheck = shouldCheckStartNode ? isStartNodeMeta || isStartPlaceholderNode : true
const isUnconnected = !validNodes.some(n => n.id === node!.id)
const shouldShowError = errorMessages.length > 0 || (isUnconnected && !canSkipConnectionCheck)
@@ -337,7 +338,8 @@ export const useChecklist = (nodes: Node[], edges: Edge[], options?: { flowType?
// Check for start nodes (including triggers)
if (shouldCheckStartNode) {
const startNodesFiltered = nodes.filter(node => START_NODE_TYPES.includes(node.data.type as BlockEnum))
- if (startNodesFiltered.length === 0) {
+ const hasStartPlaceholderNode = nodes.some(node => node.data.type === BlockEnum.StartPlaceholder)
+ if (startNodesFiltered.length === 0 && !hasStartPlaceholderNode) {
list.push({
id: 'start-node-required',
type: BlockEnum.Start,
diff --git a/web/app/components/workflow/hooks/use-inspect-vars-crud.ts b/web/app/components/workflow/hooks/use-inspect-vars-crud.ts
index 1bca16aea6a..b978455a075 100644
--- a/web/app/components/workflow/hooks/use-inspect-vars-crud.ts
+++ b/web/app/components/workflow/hooks/use-inspect-vars-crud.ts
@@ -12,7 +12,8 @@ const varsAppendStartNodeKeys = ['query', 'files']
const useInspectVarsCrud = () => {
const partOfNodesWithInspectVars = useStore(s => s.nodesWithInspectVars)
const configsMap = useHooksStore(s => s.configsMap)
- const shouldSkipSharedVariableQueries = configsMap?.flowType === FlowType.ragPipeline || configsMap?.flowType === FlowType.snippet
+ const shouldSkipSharedVariableQueries = configsMap?.flowType === FlowType.ragPipeline
+ || configsMap?.flowType === FlowType.snippet
const variableFlowId = shouldSkipSharedVariableQueries ? '' : configsMap?.flowId
const { data: conversationVars } = useConversationVarValues(configsMap?.flowType, variableFlowId)
const { data: allSystemVars } = useSysVarValues(configsMap?.flowType, variableFlowId)
diff --git a/web/app/components/workflow/nodes/__tests__/index.spec.tsx b/web/app/components/workflow/nodes/__tests__/index.spec.tsx
index 41eb853a99e..a65b3278382 100644
--- a/web/app/components/workflow/nodes/__tests__/index.spec.tsx
+++ b/web/app/components/workflow/nodes/__tests__/index.spec.tsx
@@ -8,9 +8,11 @@ import CustomNode, { Panel } from '../index'
vi.mock('../components', () => ({
NodeComponentMap: {
[BlockEnum.Start]: () =>
start-node-component
,
+ [BlockEnum.StartPlaceholder]: () =>
start-placeholder-node-component
,
},
PanelComponentMap: {
[BlockEnum.Start]: () =>
start-panel-component
,
+ [BlockEnum.StartPlaceholder]: () =>
start-placeholder-panel-component
,
},
}))
@@ -32,9 +34,10 @@ vi.mock('../_base/node', () => ({
),
}))
-vi.mock('../_base/components/workflow-panel', () => ({
- __esModule: true,
- default: ({
+vi.mock('../_base/components/workflow-panel', async () => {
+ const React = await vi.importActual
('react')
+
+ const MockWorkflowPanel = ({
id,
data,
children,
@@ -42,13 +45,23 @@ vi.mock('../_base/components/workflow-panel', () => ({
id: string
data: { type: BlockEnum }
children: ReactElement
- }) => (
-
-
{`base-panel:${id}:${data.type}`}
- {children}
-
- ),
-}))
+ }) => {
+ const [initialType] = React.useState(data.type)
+
+ return (
+
+
{`base-panel:${id}:${data.type}`}
+
{`base-panel-initial:${initialType}`}
+ {children}
+
+ )
+ }
+
+ return {
+ __esModule: true,
+ default: MockWorkflowPanel,
+ }
+})
const createNodeData = (): WorkflowNode['data'] => ({
title: 'Start',
@@ -56,6 +69,12 @@ const createNodeData = (): WorkflowNode['data'] => ({
type: BlockEnum.Start,
})
+const createStartPlaceholderData = (): WorkflowNode['data'] => ({
+ title: 'Pick a start node',
+ desc: '',
+ type: BlockEnum.StartPlaceholder,
+})
+
const baseNodeProps = {
type: CUSTOM_NODE,
selected: false,
@@ -93,6 +112,29 @@ describe('workflow nodes index', () => {
expect(screen.getByText('start-panel-component')).toBeInTheDocument()
})
+ it('should remount the base panel when a node keeps its id but changes type', () => {
+ const { rerender } = render(
+ ,
+ )
+
+ expect(screen.getByText('base-panel-initial:start-placeholder')).toBeInTheDocument()
+
+ rerender(
+ ,
+ )
+
+ expect(screen.getByText('base-panel:node-1:start')).toBeInTheDocument()
+ expect(screen.getByText('base-panel-initial:start')).toBeInTheDocument()
+ })
+
it('should return null for non-custom panel types', () => {
const { container } = render(
{
expect(document.querySelector('.i-ri-pause-circle-fill')).toBeInTheDocument()
})
- it('should render success icon when inspect vars exist without running status and hide description for loop nodes', () => {
+ it('should render success icon when inspect vars exist without running status and hide description for non-description nodes', () => {
const t = ((key: string) => key) as unknown as TFunction
const { rerender } = render(
{
rerender()
expect(screen.queryByText('hidden')).not.toBeInTheDocument()
+
+ rerender()
+ expect(screen.queryByText('old placeholder description')).not.toBeInTheDocument()
})
})
diff --git a/web/app/components/workflow/nodes/_base/__tests__/node.helpers.spec.ts b/web/app/components/workflow/nodes/_base/__tests__/node.helpers.spec.ts
index 78e1f938c55..01275cbcd33 100644
--- a/web/app/components/workflow/nodes/_base/__tests__/node.helpers.spec.ts
+++ b/web/app/components/workflow/nodes/_base/__tests__/node.helpers.spec.ts
@@ -24,6 +24,7 @@ describe('node helpers', () => {
it('should identify entry and container nodes', () => {
expect(isEntryWorkflowNode(BlockEnum.Start)).toBe(true)
+ expect(isEntryWorkflowNode(BlockEnum.StartPlaceholder)).toBe(true)
expect(isEntryWorkflowNode(BlockEnum.TriggerWebhook)).toBe(true)
expect(isEntryWorkflowNode(BlockEnum.Tool)).toBe(false)
diff --git a/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx b/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx
index c73361e35c3..c6da272c937 100644
--- a/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx
+++ b/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx
@@ -91,6 +91,11 @@ import {
} from './helpers'
import LastRun from './last-run'
import useLastRun from './last-run/use-last-run'
+import {
+ StartPlaceholderPanelBody,
+ StartPlaceholderPanelDescription,
+ StartPlaceholderPanelTitle,
+} from './start-placeholder-panel'
import { TriggerSubscription } from './trigger-subscription'
import { TabType } from './types'
@@ -480,6 +485,19 @@ const BasePanel: FC = ({
const singleRunActionLabel = isSingleRunning
? t('debug.variableInspect.trigger.stop', { ns: 'workflow' })
: runThisStepLabel
+ const isStartPlaceholderPanel = data.type === BlockEnum.StartPlaceholder
+ const panelChildren = cloneElement(children as any, {
+ id,
+ data,
+ panelProps: {
+ getInputVars,
+ toVarInputs,
+ runInputData,
+ setRunInputData,
+ runResult,
+ runInputDataRef,
+ },
+ })
const panelTabs = (
@@ -519,16 +537,24 @@ const BasePanel: FC = ({
>
-
-
+ {!isStartPlaceholderPanel && (
+
+ )}
+ {isStartPlaceholderPanel
+ ? (
+
+ )
+ : (
+
+ )}
{viewingUsers.length > 0 && (
= ({
-
-
-
- {
- needsToolAuth && (
-
-
- {panelTabs}
-
+ )
+ : (
+
+
+
+ )}
+ {!isStartPlaceholderPanel && (
+ <>
+ {
+ needsToolAuth && (
+
-
-
- )
- }
- {
- !!currentDataSource && (
-
-
- {panelTabs}
-
+
+
+ )
+ }
+ {
+ !!currentDataSource && (
+
-
-
- )
- }
- {
- currentTriggerPlugin && (
-
- {panelTabs}
-
- )
- }
- {
- !needsToolAuth && !currentDataSource && !currentTriggerPlugin && (
-
- {panelTabs}
-
- )
- }
-
+ isAuthorized={currentDataSource.is_authorized}
+ >
+
+
+ )
+ }
+ {
+ currentTriggerPlugin && (
+
+ {panelTabs}
+
+ )
+ }
+ {
+ !needsToolAuth && !currentDataSource && !currentTriggerPlugin && (
+
+ {panelTabs}
+
+ )
+ }
+
+ >
+ )}
-
-
- {cloneElement(children as any, {
- id,
- data,
- panelProps: {
- getInputVars,
- toVarInputs,
- runInputData,
- setRunInputData,
- runResult,
- runInputDataRef,
- },
- })}
-
-
- {
- hasRetryNode(data.type) && (
-
- )
- }
- {
- hasErrorHandleNode(data.type) && (
-
- )
- }
- {
- !!availableNextBlocks.length && (
-
-
- {t('panel.nextStep', { ns: 'workflow' }).toLocaleUpperCase()}
-
-
- {t('panel.addNextStep', { ns: 'workflow' })}
-
-
-
- )
- }
- {readmeEntranceComponent}
-
-
-
-
+ {isStartPlaceholderPanel && (
+
+ {panelChildren}
+
+ )}
+
+ {!isStartPlaceholderPanel && (
+
+
+ {panelChildren}
+
+
+ {
+ hasRetryNode(data.type) && (
+
+ )
+ }
+ {
+ hasErrorHandleNode(data.type) && (
+
+ )
+ }
+ {
+ !!availableNextBlocks.length && (
+
+
+ {t('panel.nextStep', { ns: 'workflow' }).toLocaleUpperCase()}
+
+
+ {t('panel.addNextStep', { ns: 'workflow' })}
+
+
+
+ )
+ }
+ {readmeEntranceComponent}
+
+ )}
+
+ {!isStartPlaceholderPanel && (
+
+
+
+ )}
diff --git a/web/app/components/workflow/nodes/_base/components/workflow-panel/last-run/use-last-run.ts b/web/app/components/workflow/nodes/_base/components/workflow-panel/last-run/use-last-run.ts
index b0bbc98d7e4..a538bcf29e3 100644
--- a/web/app/components/workflow/nodes/_base/components/workflow-panel/last-run/use-last-run.ts
+++ b/web/app/components/workflow/nodes/_base/components/workflow-panel/last-run/use-last-run.ts
@@ -54,6 +54,7 @@ const singleRunFormParamsHooks: Record