feat(trigger): add support for trigger nodes and user input node management in workflow components

This commit is contained in:
zhsama
2025-11-11 15:21:04 +08:00
parent 50619fba0a
commit 9c37f8c1cb
12 changed files with 297 additions and 105 deletions

View File

@@ -105,6 +105,7 @@ export type AppPublisherProps = {
onRefreshData?: () => void
workflowToolAvailable?: boolean
missingStartNode?: boolean
hasTriggerNode?: boolean // Whether workflow currently contains any trigger nodes (used to hide missing-start CTA when triggers exist).
}
const PUBLISH_SHORTCUT = ['ctrl', '⇧', 'P']
@@ -125,6 +126,7 @@ const AppPublisher = ({
onRefreshData,
workflowToolAvailable = true,
missingStartNode = false,
hasTriggerNode = false,
}: AppPublisherProps) => {
const { t } = useTranslation()
@@ -350,84 +352,88 @@ const AppPublisher = ({
</div>
{!isAppAccessSet && <p className='system-xs-regular mt-1 text-text-warning'>{t('app.publishApp.notSetDesc')}</p>}
</div>}
<div className='flex flex-col gap-y-1 border-t-[0.5px] border-t-divider-regular p-4 pt-3'>
<Tooltip triggerClassName='flex' disabled={!disabledFunctionButton} popupContent={disabledFunctionTooltip} asChild={false}>
<SuggestedAction
className='flex-1'
disabled={disabledFunctionButton}
link={appURL}
icon={<RiPlayCircleLine className='h-4 w-4' />}
>
{t('workflow.common.runApp')}
</SuggestedAction>
</Tooltip>
{appDetail?.mode === AppModeEnum.WORKFLOW || appDetail?.mode === AppModeEnum.COMPLETION
? (
{
// Hide run/batch run app buttons when there is a trigger node.
!hasTriggerNode && (
<div className='flex flex-col gap-y-1 border-t-[0.5px] border-t-divider-regular p-4 pt-3'>
<Tooltip triggerClassName='flex' disabled={!disabledFunctionButton} popupContent={disabledFunctionTooltip} asChild={false}>
<SuggestedAction
className='flex-1'
disabled={disabledFunctionButton}
link={`${appURL}${appURL.includes('?') ? '&' : '?'}mode=batch`}
icon={<RiPlayList2Line className='h-4 w-4' />}
link={appURL}
icon={<RiPlayCircleLine className='h-4 w-4' />}
>
{t('workflow.common.batchRunApp')}
{t('workflow.common.runApp')}
</SuggestedAction>
</Tooltip>
)
: (
<SuggestedAction
onClick={() => {
setEmbeddingModalOpen(true)
handleTrigger()
}}
disabled={!publishedAt}
icon={<CodeBrowser className='h-4 w-4' />}
>
{t('workflow.common.embedIntoSite')}
</SuggestedAction>
)}
<Tooltip triggerClassName='flex' disabled={!disabledFunctionButton} popupContent={disabledFunctionTooltip} asChild={false}>
<SuggestedAction
className='flex-1'
onClick={() => {
if (publishedAt)
handleOpenInExplore()
}}
disabled={disabledFunctionButton}
icon={<RiPlanetLine className='h-4 w-4' />}
>
{t('workflow.common.openInExplore')}
</SuggestedAction>
</Tooltip>
<Tooltip triggerClassName='flex' disabled={!!publishedAt && !missingStartNode} popupContent={!publishedAt ? t('app.notPublishedYet') : t('app.noUserInputNode')} asChild={false}>
<SuggestedAction
className='flex-1'
disabled={!publishedAt || missingStartNode}
link='./develop'
icon={<RiTerminalBoxLine className='h-4 w-4' />}
>
{t('workflow.common.accessAPIReference')}
</SuggestedAction>
</Tooltip>
{appDetail?.mode === AppModeEnum.WORKFLOW && (
<WorkflowToolConfigureButton
disabled={workflowToolDisabled}
published={!!toolPublished}
detailNeedUpdate={!!toolPublished && published}
workflowAppId={appDetail?.id}
icon={{
content: (appDetail.icon_type === 'image' ? '🤖' : appDetail?.icon) || '🤖',
background: (appDetail.icon_type === 'image' ? appDefaultIconBackground : appDetail?.icon_background) || appDefaultIconBackground,
}}
name={appDetail?.name}
description={appDetail?.description}
inputs={inputs}
handlePublish={handlePublish}
onRefreshData={onRefreshData}
disabledReason={workflowToolMessage}
/>
{appDetail?.mode === AppModeEnum.WORKFLOW || appDetail?.mode === AppModeEnum.COMPLETION
? (
<Tooltip triggerClassName='flex' disabled={!disabledFunctionButton} popupContent={disabledFunctionTooltip} asChild={false}>
<SuggestedAction
className='flex-1'
disabled={disabledFunctionButton}
link={`${appURL}${appURL.includes('?') ? '&' : '?'}mode=batch`}
icon={<RiPlayList2Line className='h-4 w-4' />}
>
{t('workflow.common.batchRunApp')}
</SuggestedAction>
</Tooltip>
)
: (
<SuggestedAction
onClick={() => {
setEmbeddingModalOpen(true)
handleTrigger()
}}
disabled={!publishedAt}
icon={<CodeBrowser className='h-4 w-4' />}
>
{t('workflow.common.embedIntoSite')}
</SuggestedAction>
)}
<Tooltip triggerClassName='flex' disabled={!disabledFunctionButton} popupContent={disabledFunctionTooltip} asChild={false}>
<SuggestedAction
className='flex-1'
onClick={() => {
if (publishedAt)
handleOpenInExplore()
}}
disabled={disabledFunctionButton}
icon={<RiPlanetLine className='h-4 w-4' />}
>
{t('workflow.common.openInExplore')}
</SuggestedAction>
</Tooltip>
<Tooltip triggerClassName='flex' disabled={!!publishedAt && !missingStartNode} popupContent={!publishedAt ? t('app.notPublishedYet') : t('app.noUserInputNode')} asChild={false}>
<SuggestedAction
className='flex-1'
disabled={!publishedAt || missingStartNode}
link='./develop'
icon={<RiTerminalBoxLine className='h-4 w-4' />}
>
{t('workflow.common.accessAPIReference')}
</SuggestedAction>
</Tooltip>
{appDetail?.mode === AppModeEnum.WORKFLOW && (
<WorkflowToolConfigureButton
disabled={workflowToolDisabled}
published={!!toolPublished}
detailNeedUpdate={!!toolPublished && published}
workflowAppId={appDetail?.id}
icon={{
content: (appDetail.icon_type === 'image' ? '🤖' : appDetail?.icon) || '🤖',
background: (appDetail.icon_type === 'image' ? appDefaultIconBackground : appDetail?.icon_background) || appDefaultIconBackground,
}}
name={appDetail?.name}
description={appDetail?.description}
inputs={inputs}
handlePublish={handlePublish}
onRefreshData={onRefreshData}
disabledReason={workflowToolMessage}
/>
)}
</div>
)}
</div>
</>}
</div>
</PortalToFollowElemContent>

View File

@@ -40,6 +40,12 @@ import { useIsChatMode } from '../../hooks'
import type { StartNodeType } from '@/app/components/workflow/nodes/start/types'
import type { Node } from '@/app/components/workflow/types'
const TRIGGER_NODE_TYPES: BlockEnum[] = [
BlockEnum.TriggerSchedule,
BlockEnum.TriggerWebhook,
BlockEnum.TriggerPlugin,
]
const FeaturesTrigger = () => {
const { t } = useTranslation()
const { theme } = useTheme()
@@ -89,6 +95,10 @@ const FeaturesTrigger = () => {
return false
return edges.some(edge => startNodeIds.includes(edge.source))
}, [edges, startNodeIds])
// Track trigger presence so the publisher can adjust UI (e.g. hide missing start section).
const hasTriggerNode = useMemo(() => (
nodes.some(node => TRIGGER_NODE_TYPES.includes(node.data.type as BlockEnum))
), [nodes])
const resetWorkflowVersionHistory = useResetWorkflowVersionHistory()
const invalidateAppTriggers = useInvalidateAppTriggers()
@@ -189,6 +199,7 @@ const FeaturesTrigger = () => {
workflowToolAvailable: lastPublishedHasUserInput,
crossAxisOffset: 4,
missingStartNode: !startNode,
hasTriggerNode,
}}
/>
</>

View File

@@ -32,6 +32,7 @@ type AllStartBlocksProps = {
onSelect: (type: BlockEnum, trigger?: TriggerDefaultValue) => void
availableBlocksTypes?: BlockEnum[]
tags?: string[]
allowUserInputSelection?: boolean // Allow user input option even when trigger node already exists (e.g. when no Start node yet or changing node type).
}
const AllStartBlocks = ({
@@ -40,6 +41,7 @@ const AllStartBlocks = ({
onSelect,
availableBlocksTypes,
tags = [],
allowUserInputSelection = false,
}: AllStartBlocksProps) => {
const { t } = useTranslation()
const [hasStartBlocksContent, setHasStartBlocksContent] = useState(false)
@@ -122,6 +124,7 @@ const AllStartBlocks = ({
searchText={trimmedSearchText}
onSelect={onSelect as OnSelectBlock}
availableBlocksTypes={entryNodeTypes as unknown as BlockEnum[]}
hideUserInput={!allowUserInputSelection}
onContentStateChange={handleStartBlocksContentChange}
/>

View File

@@ -1,4 +1,6 @@
import {
useCallback,
useEffect,
useMemo,
useState,
} from 'react'
@@ -31,16 +33,28 @@ export const useStartBlocks = () => {
})
}
export const useTabs = ({ noBlocks, noSources, noTools, noStart = true, defaultActiveTab }: {
export const useTabs = ({
noBlocks,
noSources,
noTools,
noStart = true,
defaultActiveTab,
hasUserInputNode = false,
forceEnableStartTab = false, // When true, Start tab remains enabled even if trigger/user input nodes already exist.
}: {
noBlocks?: boolean
noSources?: boolean
noTools?: boolean
noStart?: boolean
defaultActiveTab?: TabsEnum
hasUserInputNode?: boolean
forceEnableStartTab?: boolean
}) => {
const { t } = useTranslation()
const shouldShowStartTab = !noStart
const shouldDisableStartTab = !forceEnableStartTab && hasUserInputNode
const tabs = useMemo(() => {
return [{
const tabConfigs = [{
key: TabsEnum.Blocks,
name: t('workflow.tabs.blocks'),
show: !noBlocks,
@@ -56,25 +70,54 @@ export const useTabs = ({ noBlocks, noSources, noTools, noStart = true, defaultA
{
key: TabsEnum.Start,
name: t('workflow.tabs.start'),
show: !noStart,
}].filter(tab => tab.show)
}, [t, noBlocks, noSources, noTools, noStart])
show: shouldShowStartTab,
disabled: shouldDisableStartTab,
}]
return tabConfigs.filter(tab => tab.show)
}, [t, noBlocks, noSources, noTools, shouldShowStartTab, shouldDisableStartTab])
const getValidTabKey = useCallback((targetKey?: TabsEnum) => {
if (!targetKey)
return undefined
const tab = tabs.find(tabItem => tabItem.key === targetKey)
if (!tab || tab.disabled)
return undefined
return tab.key
}, [tabs])
const initialTab = useMemo(() => {
// If a default tab is specified, use it
if (defaultActiveTab)
return defaultActiveTab
const fallbackTab = tabs.find(tab => !tab.disabled)?.key ?? TabsEnum.Blocks
const preferredDefault = getValidTabKey(defaultActiveTab)
if (preferredDefault)
return preferredDefault
if (noBlocks)
return noTools ? TabsEnum.Sources : TabsEnum.Tools
const preferredOrder: TabsEnum[] = []
if (!noBlocks)
preferredOrder.push(TabsEnum.Blocks)
if (!noTools)
preferredOrder.push(TabsEnum.Tools)
if (!noSources)
preferredOrder.push(TabsEnum.Sources)
if (!noStart)
preferredOrder.push(TabsEnum.Start)
if (noTools)
return noBlocks ? TabsEnum.Sources : TabsEnum.Blocks
for (const tabKey of preferredOrder) {
const validKey = getValidTabKey(tabKey)
if (validKey)
return validKey
}
return TabsEnum.Blocks
}, [noBlocks, noSources, noTools, defaultActiveTab])
return fallbackTab
}, [defaultActiveTab, noBlocks, noSources, noTools, noStart, tabs, getValidTabKey])
const [activeTab, setActiveTab] = useState(initialTab)
useEffect(() => {
const currentTab = tabs.find(tab => tab.key === activeTab)
if (!currentTab || currentTab.disabled)
setActiveTab(initialTab)
}, [tabs, activeTab, initialTab])
return {
tabs,
activeTab,

View File

@@ -9,16 +9,18 @@ import {
useState,
} from 'react'
import { useTranslation } from 'react-i18next'
import { useNodes } from 'reactflow'
import type {
OffsetOptions,
Placement,
} from '@floating-ui/react'
import type {
BlockEnum,
CommonNodeType,
NodeDefault,
OnSelectBlock,
ToolWithProvider,
} from '../types'
import { BlockEnum } from '../types'
import Tabs from './tabs'
import { TabsEnum } from './types'
import { useTabs } from './hooks'
@@ -33,6 +35,12 @@ import {
} from '@/app/components/base/icons/src/vender/line/general'
import SearchBox from '@/app/components/plugins/marketplace/search-box'
const TRIGGER_NODE_TYPES: BlockEnum[] = [
BlockEnum.TriggerSchedule,
BlockEnum.TriggerWebhook,
BlockEnum.TriggerPlugin,
]
export type NodeSelectorProps = {
open?: boolean
onOpenChange?: (open: boolean) => void
@@ -54,6 +62,9 @@ export type NodeSelectorProps = {
showStartTab?: boolean
defaultActiveTab?: TabsEnum
forceShowStartContent?: boolean
ignoreNodeIds?: string[]
forceEnableStartTab?: boolean // Force enabling Start tab regardless of existing trigger/user input nodes (e.g., when changing Start node type).
allowUserInputSelection?: boolean // Override user-input availability; default logic blocks it when triggers exist.
}
const NodeSelector: FC<NodeSelectorProps> = ({
open: openFromProps,
@@ -76,11 +87,44 @@ const NodeSelector: FC<NodeSelectorProps> = ({
showStartTab = false,
defaultActiveTab,
forceShowStartContent = false,
ignoreNodeIds = [],
forceEnableStartTab = false,
allowUserInputSelection,
}) => {
const { t } = useTranslation()
const nodes = useNodes()
const [searchText, setSearchText] = useState('')
const [tags, setTags] = useState<string[]>([])
const [localOpen, setLocalOpen] = useState(false)
// Exclude nodes explicitly ignored (such as the node currently being edited) when checking canvas state.
const filteredNodes = useMemo(() => {
if (!ignoreNodeIds.length)
return nodes
const ignoreSet = new Set(ignoreNodeIds)
return nodes.filter(node => !ignoreSet.has(node.id))
}, [nodes, ignoreNodeIds])
const { hasTriggerNode, hasUserInputNode } = useMemo(() => {
const result = {
hasTriggerNode: false,
hasUserInputNode: false,
}
for (const node of filteredNodes) {
const nodeType = (node.data as CommonNodeType | undefined)?.type
if (!nodeType)
continue
if (nodeType === BlockEnum.Start)
result.hasUserInputNode = true
if (TRIGGER_NODE_TYPES.includes(nodeType))
result.hasTriggerNode = true
if (result.hasTriggerNode && result.hasUserInputNode)
break
}
return result
}, [filteredNodes])
// Default rule: user input option is only available when no Start node nor Trigger node exists on canvas.
const defaultAllowUserInputSelection = !hasUserInputNode && !hasTriggerNode
const canSelectUserInput = allowUserInputSelection ?? defaultAllowUserInputSelection
const open = openFromProps === undefined ? localOpen : openFromProps
const handleOpenChange = useCallback((newOpen: boolean) => {
setLocalOpen(newOpen)
@@ -107,7 +151,15 @@ const NodeSelector: FC<NodeSelectorProps> = ({
activeTab,
setActiveTab,
tabs,
} = useTabs({ noBlocks, noSources: !dataSources.length, noTools, noStart: !showStartTab, defaultActiveTab })
} = useTabs({
noBlocks,
noSources: !dataSources.length,
noTools,
noStart: !showStartTab,
defaultActiveTab,
hasUserInputNode,
forceEnableStartTab,
})
const handleActiveTabChange = useCallback((newActiveTab: TabsEnum) => {
setActiveTab(newActiveTab)
@@ -146,7 +198,7 @@ const NodeSelector: FC<NodeSelectorProps> = ({
: (
<div
className={`
z-10 flex h-4
z-10 flex h-4
w-4 cursor-pointer items-center justify-center rounded-full bg-components-button-primary-bg text-text-primary-on-surface hover:bg-components-button-primary-bg-hover
${triggerClassName?.(open)}
`}
@@ -163,6 +215,7 @@ const NodeSelector: FC<NodeSelectorProps> = ({
tabs={tabs}
activeTab={activeTab}
blocks={blocks}
allowStartNodeSelection={canSelectUserInput}
onActiveTabChange={handleActiveTabChange}
filterElem={
<div className='relative m-2' onClick={e => e.stopPropagation()}>

View File

@@ -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

View File

@@ -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<TabsProps> = ({
activeTab,
@@ -52,7 +56,9 @@ const Tabs: FC<TabsProps> = ({
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<TabsProps> = ({
!noBlocks && (
<div className='relative flex bg-background-section-burn pl-1 pt-1'>
{
tabs.map(tab => (
<div
key={tab.key}
className={cn(
'system-sm-medium relative mr-0.5 flex h-8 cursor-pointer items-center rounded-t-lg px-3 ',
activeTab === tab.key
? 'sm-no-bottom cursor-default bg-components-panel-bg text-text-accent'
: 'text-text-tertiary',
)}
onClick={() => onActiveTabChange(tab.key)}
>
{tab.name}
</div>
))
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 (
<Tooltip
key={tab.key}
position='top'
popupClassName='max-w-[200px]'
popupContent={t('workflow.tabs.startDisabledTip')}
>
<div {...commonProps}>
{tab.name}
</div>
</Tooltip>
)
}
return (
<div
key={tab.key}
{...commonProps}
>
{tab.name}
</div>
)
})
}
</div>
)
@@ -148,6 +180,7 @@ const Tabs: FC<TabsProps> = ({
activeTab === TabsEnum.Start && (!noBlocks || forceShowStartContent) && (
<div className='border-t border-divider-subtle'>
<AllStartBlocks
allowUserInputSelection={allowStartNodeSelection}
searchText={searchText}
onSelect={onSelect}
availableBlocksTypes={availableBlocksTypes}

View File

@@ -5,6 +5,7 @@ import {
} from 'react'
import { useTranslation } from 'react-i18next'
import { intersection } from 'lodash-es'
import { useNodes } from 'reactflow'
import BlockSelector from '@/app/components/workflow/block-selector'
import {
useAvailableBlocks,
@@ -13,9 +14,17 @@ import {
} from '@/app/components/workflow/hooks'
import { useHooksStore } from '@/app/components/workflow/hooks-store'
import type {
CommonNodeType,
Node,
OnSelectBlock,
} from '@/app/components/workflow/types'
import { BlockEnum } from '@/app/components/workflow/types'
const TRIGGER_NODE_TYPES: BlockEnum[] = [
BlockEnum.TriggerSchedule,
BlockEnum.TriggerWebhook,
BlockEnum.TriggerPlugin,
]
import { FlowType } from '@/types/common'
type ChangeBlockProps = {
@@ -30,6 +39,7 @@ const ChangeBlock = ({
}: ChangeBlockProps) => {
const { t } = useTranslation()
const { handleNodeChange } = useNodesInteractions()
const nodes = useNodes<CommonNodeType>()
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}
/>
)
}

View File

@@ -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<StartNodeType> = {

View File

@@ -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',

View File

@@ -276,6 +276,7 @@ const translation = {
'searchDataSource': 'データソースを検索',
'sources': 'ソース',
'start': '始める',
'startDisabledTip': 'トリガーノードとユーザー入力ノードは互いに排他です。',
},
blocks: {
'start': 'ユーザー入力',

View File

@@ -286,6 +286,7 @@ const translation = {
'hideActions': '收起工具',
'noFeaturedPlugins': '前往插件市场查看更多工具',
'noFeaturedTriggers': '前往插件市场查看更多触发器',
'startDisabledTip': '触发节点与用户输入节点互斥。',
},
blocks: {
'start': '用户输入',