import type { PopoverPositionerProps, PopoverTriggerProps } from '@langgenius/dify-ui/popover' import type { CSSProperties, KeyboardEvent, MouseEventHandler } from 'react' import type { CommonNodeType, NodeDefault, OnNodeAdd, OnSelectBlock, ToolWithProvider, } from '../types' import type { TabType } from './types' import { cn } from '@langgenius/dify-ui/cn' import { IconButton } from '@langgenius/dify-ui/icon-button' import { Popover, PopoverClose, PopoverPopup, PopoverPortal, PopoverPositioner, PopoverTitle, PopoverTrigger, } from '@langgenius/dify-ui/popover' import { memo, useCallback, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { useHooksStore } from '@/app/components/workflow/hooks-store' import useNodes from '@/app/components/workflow/store/workflow/use-nodes' import { FlowType } from '@/types/common' import TipPopup from '../operator/tip-popup' import { useStore } from '../store' import { BlockEnum, isTriggerNode } from '../types' import { useTabs } from './hooks' import { BlockSelectorPanels } from './tabs' export type BlockSelectorProps = Pick< PopoverPositionerProps, 'alignOffset' | 'placement' | 'sideOffset' > & { open?: boolean onOpenChange?: (open: boolean) => void onSelect: OnSelectBlock trigger?: NonNullable triggerTooltip?: string triggerStyle?: CSSProperties triggerClassName?: string triggerAriaLabel?: string popupClassName?: string availableBlocksTypes?: BlockEnum[] disabled?: boolean blocks?: NodeDefault[] dataSources?: ToolWithProvider[] noBlocks?: boolean noTools?: boolean standalonePanel?: TabType showStartTab?: boolean defaultActiveTab?: TabType 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. snippetInsertPayload?: Parameters[1] isolateKeyboardEvents?: boolean } function BlockSelector({ open: openFromProps, onOpenChange, onSelect, trigger, triggerTooltip, placement = 'right', sideOffset, alignOffset, triggerClassName, triggerAriaLabel, triggerStyle, popupClassName, availableBlocksTypes, disabled, blocks, dataSources, noBlocks = false, noTools = false, standalonePanel, showStartTab = false, defaultActiveTab, ignoreNodeIds = [], forceEnableStartTab = false, allowUserInputSelection, snippetInsertPayload, isolateKeyboardEvents = false, }: BlockSelectorProps) { const { t } = useTranslation() const [localOpen, setLocalOpen] = useState(false) const searchInputRef = useRef(null) const open = openFromProps === undefined ? localOpen : openFromProps const handleOpenChange = useCallback( (newOpen: boolean) => { if (newOpen && disabled) return setLocalOpen(newOpen) if (onOpenChange) onOpenChange(newOpen) }, [disabled, onOpenChange], ) const handleTrigger = useCallback>((e) => { e.stopPropagation() }, []) const handlePopupClick = useCallback>((event) => { event.stopPropagation() }, []) const handleSelect = useCallback( (type, pluginDefaultValue) => { if (disabled) return handleOpenChange(false) onSelect(type, pluginDefaultValue) }, [disabled, handleOpenChange, onSelect], ) const handlePopupKeyDown = useCallback( (event: KeyboardEvent) => { if (isolateKeyboardEvents) event.stopPropagation() }, [isolateKeyboardEvents], ) const triggerControl = trigger ? ( ) : ( $['common.addBlock'], { ns: 'workflow' })} variant="primary" size="xs" className={cn('z-10 rounded-full', triggerClassName)} style={triggerStyle} > } onClick={handleTrigger} /> ) const triggerWithTooltip = triggerTooltip ? ( {triggerControl} ) : ( triggerControl ) return ( {triggerWithTooltip} {t(($) => $['common.addBlock'], { ns: 'workflow' })}
handleOpenChange(false)} availableBlocksTypes={availableBlocksTypes} dataSources={dataSources} noBlocks={noBlocks} noTools={noTools} showStartTab={showStartTab} defaultActiveTab={defaultActiveTab} ignoreNodeIds={ignoreNodeIds} forceEnableStartTab={forceEnableStartTab} allowUserInputSelection={allowUserInputSelection} snippetInsertPayload={snippetInsertPayload} />
{t(($) => $['operation.close'], { ns: 'common' })}
) } type BlockSelectorContentProps = Pick< BlockSelectorProps, | 'allowUserInputSelection' | 'availableBlocksTypes' | 'blocks' | 'dataSources' | 'defaultActiveTab' | 'forceEnableStartTab' | 'ignoreNodeIds' | 'noBlocks' | 'noTools' | 'showStartTab' | 'snippetInsertPayload' | 'standalonePanel' > & { onSelect: OnSelectBlock onRequestClose: () => void searchInputRef: React.RefObject } function BlockSelectorContent({ allowUserInputSelection, availableBlocksTypes, blocks: blocksFromProps, dataSources: dataSourcesFromProps, defaultActiveTab, forceEnableStartTab = false, ignoreNodeIds = [], noBlocks = false, noTools = false, onRequestClose, onSelect, searchInputRef, showStartTab = false, snippetInsertPayload, standalonePanel, }: BlockSelectorContentProps) { const nodes = useNodes() const flowType = useHooksStore((state) => state.configsMap?.flowType) const availableNodesMetaData = useHooksStore((state) => state.availableNodesMetaData) const fallbackDataSources = useStore((state) => state.dataSourceList) const blocks = useMemo(() => { if (blocksFromProps) return blocksFromProps return (availableNodesMetaData?.nodes ?? []).filter((block) => { return ![ BlockEnum.Start, BlockEnum.StartPlaceholder, BlockEnum.DataSource, BlockEnum.Tool, BlockEnum.IterationStart, BlockEnum.LoopStart, BlockEnum.DataSourceEmpty, ].includes(block.metaData.type) }) }, [availableNodesMetaData?.nodes, blocksFromProps]) const dataSources = dataSourcesFromProps ?? fallbackDataSources ?? [] const filteredNodes = useMemo(() => { if (!ignoreNodeIds.length) return nodes const ignoredNodeIds = new Set(ignoreNodeIds) return nodes.filter((node) => !ignoredNodeIds.has(node.id)) }, [ignoreNodeIds, nodes]) const { hasTriggerNode, hasUserInputNode, hasStartPlaceholderNode } = useMemo(() => { const result = { hasTriggerNode: false, hasUserInputNode: false, hasStartPlaceholderNode: 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 (nodeType === BlockEnum.StartPlaceholder) result.hasStartPlaceholderNode = true if (isTriggerNode(nodeType)) result.hasTriggerNode = true if (result.hasTriggerNode && result.hasUserInputNode && result.hasStartPlaceholderNode) break } return result }, [filteredNodes]) const disableStartTab = flowType === FlowType.snippet const { initialTab, tabs } = useTabs({ noBlocks, noSources: !dataSources.length, noTools, noSnippets: flowType === FlowType.snippet, noStart: !showStartTab, defaultActiveTab, hasStartPlaceholderNode, disableStartTab, forceEnableStartTab, }) const allowStartNodeSelection = allowUserInputSelection ?? (!hasUserInputNode && !hasTriggerNode) return ( ) } export default memo(BlockSelector)