= ({
tabs={tabs}
activeTab={activeTab}
blocks={blocks}
+ allowStartNodeSelection={canSelectUserInput}
onActiveTabChange={handleActiveTabChange}
filterElem={
e.stopPropagation()}>
diff --git a/web/app/components/workflow/block-selector/start-blocks.tsx b/web/app/components/workflow/block-selector/start-blocks.tsx
index 4fdfd475d9..31b6abce6c 100644
--- a/web/app/components/workflow/block-selector/start-blocks.tsx
+++ b/web/app/components/workflow/block-selector/start-blocks.tsx
@@ -20,6 +20,7 @@ type StartBlocksProps = {
onSelect: (type: BlockEnum, triggerDefaultValue?: TriggerDefaultValue) => void
availableBlocksTypes?: BlockEnum[]
onContentStateChange?: (hasContent: boolean) => void
+ hideUserInput?: boolean
}
const StartBlocks = ({
@@ -27,6 +28,7 @@ const StartBlocks = ({
onSelect,
availableBlocksTypes = [],
onContentStateChange,
+ hideUserInput = false, // Allow parent to explicitly hide Start node option (e.g. when one already exists).
}: StartBlocksProps) => {
const { t } = useTranslation()
const nodes = useNodes()
@@ -45,8 +47,8 @@ const StartBlocks = ({
}
return START_BLOCKS.filter((block) => {
- // Hide User Input (Start) if it already exists in workflow
- if (block.type === BlockEnumValues.Start && hasStartNode)
+ // Hide User Input (Start) if it already exists in workflow or if hideUserInput is true
+ if (block.type === BlockEnumValues.Start && (hasStartNode || hideUserInput))
return false
// Filter by search text
@@ -57,7 +59,7 @@ const StartBlocks = ({
// availableBlocksTypes now contains properly filtered entry node types from parent
return availableBlocksTypes.includes(block.type)
})
- }, [searchText, availableBlocksTypes, nodes, t])
+ }, [searchText, availableBlocksTypes, nodes, t, hideUserInput])
const isEmpty = filteredBlocks.length === 0
diff --git a/web/app/components/workflow/block-selector/tabs.tsx b/web/app/components/workflow/block-selector/tabs.tsx
index a1b2839b45..ecdb8797c0 100644
--- a/web/app/components/workflow/block-selector/tabs.tsx
+++ b/web/app/components/workflow/block-selector/tabs.tsx
@@ -1,5 +1,6 @@
import type { Dispatch, FC, SetStateAction } from 'react'
import { memo, useEffect, useMemo } from 'react'
+import { useTranslation } from 'react-i18next'
import { useAllBuiltInTools, useAllCustomTools, useAllMCPTools, useAllWorkflowTools, useInvalidateAllBuiltInTools } from '@/service/use-tools'
import type {
BlockEnum,
@@ -17,6 +18,7 @@ import { useFeaturedToolsRecommendations } from '@/service/use-plugins'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { useWorkflowStore } from '../store'
import { basePath } from '@/utils/var'
+import Tooltip from '@/app/components/base/tooltip'
export type TabsProps = {
activeTab: TabsEnum
@@ -31,11 +33,13 @@ export type TabsProps = {
tabs: Array<{
key: TabsEnum
name: string
+ disabled?: boolean
}>
filterElem: React.ReactNode
noBlocks?: boolean
noTools?: boolean
forceShowStartContent?: boolean // Force show Start content even when noBlocks=true
+ allowStartNodeSelection?: boolean // Allow user input option even when trigger node already exists (e.g. change-node flow or when no Start node yet).
}
const Tabs: FC
= ({
activeTab,
@@ -52,7 +56,9 @@ const Tabs: FC = ({
noBlocks,
noTools,
forceShowStartContent = false,
+ allowStartNodeSelection = false,
}) => {
+ const { t } = useTranslation()
const { data: buildInTools } = useAllBuiltInTools()
const { data: customTools } = useAllCustomTools()
const { data: workflowTools } = useAllWorkflowTools()
@@ -125,20 +131,46 @@ const Tabs: FC = ({
!noBlocks && (
{
- tabs.map(tab => (
-
onActiveTabChange(tab.key)}
- >
- {tab.name}
-
- ))
+ tabs.map((tab) => {
+ const commonProps = {
+ 'className': cn(
+ 'system-sm-medium relative mr-0.5 flex h-8 items-center rounded-t-lg px-3',
+ tab.disabled
+ ? 'cursor-not-allowed text-text-disabled opacity-60'
+ : activeTab === tab.key
+ ? 'sm-no-bottom cursor-default bg-components-panel-bg text-text-accent'
+ : 'cursor-pointer text-text-tertiary',
+ ),
+ 'aria-disabled': tab.disabled,
+ 'onClick': () => {
+ if (tab.disabled || activeTab === tab.key)
+ return
+ onActiveTabChange(tab.key)
+ },
+ } as const
+ if (tab.disabled) {
+ return (
+
+
+ {tab.name}
+
+
+ )
+ }
+ return (
+
+ {tab.name}
+
+ )
+ })
}
)
@@ -148,6 +180,7 @@ const Tabs: FC = ({
activeTab === TabsEnum.Start && (!noBlocks || forceShowStartContent) && (
{
const { t } = useTranslation()
const { handleNodeChange } = useNodesInteractions()
+ const nodes = useNodes()
const {
availablePrevBlocks,
availableNextBlocks,
@@ -37,6 +47,30 @@ const ChangeBlock = ({
const isChatMode = useIsChatMode()
const flowType = useHooksStore(s => s.configsMap?.flowType)
const showStartTab = flowType !== FlowType.ragPipeline && !isChatMode
+ // Count total trigger nodes
+ const totalTriggerNodes = useMemo(() => (
+ nodes.filter(node => TRIGGER_NODE_TYPES.includes(node.data.type as BlockEnum)).length
+ ), [nodes])
+ // Check if there is a User Input node
+ const hasUserInputNode = useMemo(() => (
+ nodes.some(node => node.data.type === BlockEnum.Start)
+ ), [nodes])
+ // Check if the current node is a trigger node
+ const isTriggerNode = TRIGGER_NODE_TYPES.includes(nodeData.type as BlockEnum)
+ // Force enabling Start tab regardless of existing trigger/user input nodes (e.g., when changing Start node type).
+ const forceEnableStartTab = isTriggerNode || nodeData.type === BlockEnum.Start
+ // Only allow converting a trigger into User Input when it's the sole trigger and no User Input exists yet.
+ const canChangeTriggerToUserInput = isTriggerNode && !hasUserInputNode && totalTriggerNodes === 1
+ // Ignore current node when it's a trigger so the Start tab logic doesn't treat it as existing trigger.
+ const ignoreNodeIds = useMemo(() => {
+ if (TRIGGER_NODE_TYPES.includes(nodeData.type as BlockEnum))
+ return [nodeId]
+ return undefined
+ }, [nodeData.type, nodeId])
+ // Determine user input selection based on node type and trigger/user input node presence.
+ const allowUserInputSelection = forceEnableStartTab
+ ? (nodeData.type === BlockEnum.Start ? false : canChangeTriggerToUserInput)
+ : undefined
const availableNodes = useMemo(() => {
if (availablePrevBlocks.length && availableNextBlocks.length)
@@ -71,6 +105,10 @@ const ChangeBlock = ({
popupClassName='min-w-[240px]'
availableBlocksTypes={availableNodes}
showStartTab={showStartTab}
+ ignoreNodeIds={ignoreNodeIds}
+ // When changing Start/Trigger nodes, force-enable Start tab to allow switching among entry nodes.
+ forceEnableStartTab={forceEnableStartTab}
+ allowUserInputSelection={allowUserInputSelection}
/>
)
}
diff --git a/web/app/components/workflow/nodes/start/default.ts b/web/app/components/workflow/nodes/start/default.ts
index 9780549c5a..60584b5144 100644
--- a/web/app/components/workflow/nodes/start/default.ts
+++ b/web/app/components/workflow/nodes/start/default.ts
@@ -9,7 +9,7 @@ const metaData = genNodeMetaData({
isStart: true,
isRequired: false,
isSingleton: true,
- isTypeFixed: true,
+ isTypeFixed: false, // support node type change for start node(user input)
helpLinkUri: 'user-input',
})
const nodeDefault: NodeDefault = {
diff --git a/web/i18n/en-US/workflow.ts b/web/i18n/en-US/workflow.ts
index 77e71b0973..92a0b110c7 100644
--- a/web/i18n/en-US/workflow.ts
+++ b/web/i18n/en-US/workflow.ts
@@ -287,6 +287,7 @@ const translation = {
'hideActions': 'Hide tools',
'noFeaturedPlugins': 'Discover more tools in Marketplace',
'noFeaturedTriggers': 'Discover more triggers in Marketplace',
+ 'startDisabledTip': 'Trigger node and user input node are mutually exclusive.',
},
blocks: {
'start': 'User Input',
diff --git a/web/i18n/ja-JP/workflow.ts b/web/i18n/ja-JP/workflow.ts
index d55f12fc8b..07241b8c4f 100644
--- a/web/i18n/ja-JP/workflow.ts
+++ b/web/i18n/ja-JP/workflow.ts
@@ -276,6 +276,7 @@ const translation = {
'searchDataSource': 'データソースを検索',
'sources': 'ソース',
'start': '始める',
+ 'startDisabledTip': 'トリガーノードとユーザー入力ノードは互いに排他です。',
},
blocks: {
'start': 'ユーザー入力',
diff --git a/web/i18n/zh-Hans/workflow.ts b/web/i18n/zh-Hans/workflow.ts
index 53ec625160..18e76caa64 100644
--- a/web/i18n/zh-Hans/workflow.ts
+++ b/web/i18n/zh-Hans/workflow.ts
@@ -286,6 +286,7 @@ const translation = {
'hideActions': '收起工具',
'noFeaturedPlugins': '前往插件市场查看更多工具',
'noFeaturedTriggers': '前往插件市场查看更多触发器',
+ 'startDisabledTip': '触发节点与用户输入节点互斥。',
},
blocks: {
'start': '用户输入',