feat(web): block selector in snippet

This commit is contained in:
JzoNg 2026-03-29 17:01:32 +08:00
parent 0f13aabea8
commit a4ea33167d
7 changed files with 61 additions and 5 deletions

View File

@ -23,6 +23,8 @@ import { useStore as useAppStore } from '@/app/components/app/store'
import { toast } from '@/app/components/base/ui/toast'
import Evaluation from '@/app/components/evaluation'
import { WorkflowWithInnerContext } from '@/app/components/workflow'
import { useAvailableNodesMetaData } from '@/app/components/workflow-app/hooks'
import { BlockEnum } from '@/app/components/workflow/types'
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
import { useConfigsMap } from '../hooks/use-configs-map'
import { useNodesSyncDraft } from '../hooks/use-nodes-sync-draft'
@ -66,6 +68,25 @@ const SnippetMain = ({
} = useNodesSyncDraft(snippetId)
const { handleRefreshWorkflowDraft } = useSnippetRefreshDraft(snippetId)
const configsMap = useConfigsMap(snippetId)
const workflowAvailableNodesMetaData = useAvailableNodesMetaData()
const availableNodesMetaData = useMemo(() => {
const nodes = workflowAvailableNodesMetaData.nodes.filter(node =>
node.metaData.type !== BlockEnum.HumanInput && node.metaData.type !== BlockEnum.End)
if (!workflowAvailableNodesMetaData.nodesMap)
return { nodes }
const {
[BlockEnum.HumanInput]: _humanInput,
[BlockEnum.End]: _end,
...nodesMap
} = workflowAvailableNodesMetaData.nodesMap
return {
nodes,
nodesMap,
}
}, [workflowAvailableNodesMetaData])
const setAppSidebarExpand = useAppStore(state => state.setAppSidebarExpand)
const {
editingField,
@ -150,9 +171,10 @@ const SnippetMain = ({
doSyncWorkflowDraft,
syncWorkflowDraftWhenPageClose,
handleRefreshWorkflowDraft,
availableNodesMetaData,
configsMap,
}
}, [configsMap, doSyncWorkflowDraft, handleRefreshWorkflowDraft, syncWorkflowDraftWhenPageClose])
}, [availableNodesMetaData, configsMap, doSyncWorkflowDraft, handleRefreshWorkflowDraft, syncWorkflowDraftWhenPageClose])
return (
<div className="relative flex h-full overflow-hidden bg-background-body">
@ -190,7 +212,7 @@ const SnippetMain = ({
nodes={nodes}
edges={edges}
viewport={viewport ?? graph.viewport}
hooksStore={hooksStore}
hooksStore={hooksStore as any}
>
<SnippetChildren
snippetId={snippetId}

View File

@ -40,6 +40,7 @@ export const useTabs = ({
noStart = true,
defaultActiveTab,
hasUserInputNode = false,
disableStartTab = false,
forceEnableStartTab = false, // When true, Start tab remains enabled even if trigger/user input nodes already exist.
}: {
noBlocks?: boolean
@ -48,11 +49,15 @@ export const useTabs = ({
noStart?: boolean
defaultActiveTab?: TabsEnum
hasUserInputNode?: boolean
disableStartTab?: boolean
forceEnableStartTab?: boolean
}) => {
const { t } = useTranslation()
const shouldShowStartTab = !noStart
const shouldDisableStartTab = !forceEnableStartTab && hasUserInputNode
const shouldDisableStartTab = disableStartTab || (!forceEnableStartTab && hasUserInputNode)
const startDisabledTip = disableStartTab
? t('tabs.startNotSupportedTip', { ns: 'workflow' })
: t('tabs.startDisabledTip', { ns: 'workflow' })
const tabs = useMemo(() => {
const tabConfigs = [{
key: TabsEnum.Blocks,
@ -71,6 +76,7 @@ export const useTabs = ({
name: t('tabs.start', { ns: 'workflow' }),
show: shouldShowStartTab,
disabled: shouldDisableStartTab,
disabledTip: shouldDisableStartTab ? startDisabledTip : undefined,
}, {
key: TabsEnum.Snippets,
name: t('tabs.snippets', { ns: 'workflow' }),
@ -78,7 +84,7 @@ export const useTabs = ({
}]
return tabConfigs.filter(tab => tab.show)
}, [t, noBlocks, noSources, noTools, shouldShowStartTab, shouldDisableStartTab])
}, [t, noBlocks, noSources, noTools, shouldShowStartTab, shouldDisableStartTab, startDisabledTip])
const getValidTabKey = useCallback((targetKey?: TabsEnum) => {
if (!targetKey)

View File

@ -30,7 +30,9 @@ import {
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
import SearchBox from '@/app/components/plugins/marketplace/search-box'
import { useHooksStore } from '@/app/components/workflow/hooks-store'
import useNodes from '@/app/components/workflow/store/workflow/use-nodes'
import { FlowType } from '@/types/common'
import { BlockEnum, isTriggerNode } from '../types'
import { useTabs } from './hooks'
import Snippets from './snippets'
@ -89,6 +91,7 @@ const NodeSelector: FC<NodeSelectorProps> = ({
}) => {
const { t } = useTranslation()
const nodes = useNodes()
const flowType = useHooksStore(s => s.configsMap?.flowType)
const [searchText, setSearchText] = useState('')
const [snippetsLoading, setSnippetsLoading] = useState(() => Boolean(openFromProps) && defaultActiveTab === TabsEnum.Snippets)
const [tags, setTags] = useState<string[]>([])
@ -122,6 +125,7 @@ const NodeSelector: FC<NodeSelectorProps> = ({
// 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 disableStartTab = flowType === FlowType.snippet
const {
activeTab,
setActiveTab,
@ -133,6 +137,7 @@ const NodeSelector: FC<NodeSelectorProps> = ({
noStart: !showStartTab,
defaultActiveTab,
hasUserInputNode,
disableStartTab,
forceEnableStartTab,
})
const open = openFromProps === undefined ? localOpen : openFromProps

View File

@ -34,6 +34,7 @@ export type TabsProps = {
key: TabsEnum
name: string
disabled?: boolean
disabledTip?: string
}>
filterElem: React.ReactNode
noBlocks?: boolean
@ -225,7 +226,7 @@ const Tabs: FC<TabsProps> = ({
tab={tab}
activeTab={activeTab}
onActiveTabChange={onActiveTabChange}
disabledTip={disabledTip}
disabledTip={tab.disabledTip || disabledTip}
/>
))
}

View File

@ -177,6 +177,11 @@
"count": 2
}
},
"app/(commonLayout)/snippets/[snippetId]/page.tsx": {
"no-restricted-imports": {
"count": 1
}
},
"app/(humanInputLayout)/form/[token]/form.tsx": {
"react/set-state-in-effect": {
"count": 1
@ -4917,6 +4922,11 @@
"count": 2
}
},
"app/components/evaluation/index.tsx": {
"no-restricted-imports": {
"count": 1
}
},
"app/components/explore/banner/banner-item.tsx": {
"react-hooks-extra/no-direct-set-state-in-use-effect": {
"count": 1
@ -6686,6 +6696,11 @@
"count": 2
}
},
"app/components/snippets/components/snippet-main.tsx": {
"ts/no-explicit-any": {
"count": 1
}
},
"app/components/tools/edit-custom-collection-modal/config-credentials.tsx": {
"no-restricted-imports": {
"count": 1
@ -10487,6 +10502,11 @@
"count": 7
}
},
"service/use-evaluation.ts": {
"no-barrel-files/no-barrel-files": {
"count": 2
}
},
"service/use-flow.ts": {
"react/no-unnecessary-use-prefix": {
"count": 1

View File

@ -1148,6 +1148,7 @@
"tabs.sources": "Sources",
"tabs.start": "Start",
"tabs.startDisabledTip": "Trigger node and user input node are mutually exclusive.",
"tabs.startNotSupportedTip": "The Start tab is not supported in snippets.",
"tabs.tools": "Tools",
"tabs.transform": "Transform",
"tabs.usePlugin": "Select tool",

View File

@ -1148,6 +1148,7 @@
"tabs.sources": "数据源",
"tabs.start": "开始",
"tabs.startDisabledTip": "触发节点与用户输入节点互斥。",
"tabs.startNotSupportedTip": "Snippet 暂不支持 Start 标签。",
"tabs.tools": "工具",
"tabs.transform": "转换",
"tabs.usePlugin": "选择工具",