dify/web/app/components/workflow/block-selector/start-blocks.tsx

239 lines
8.4 KiB
TypeScript

import type { BlockEnum, CommonNodeType } from '../types'
import type { TriggerDefaultValue } from './types'
import { cn } from '@langgenius/dify-ui/cn'
import {
createPreviewCardHandle,
PreviewCard,
PreviewCardContent,
PreviewCardTrigger,
} from '@langgenius/dify-ui/preview-card'
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
import {
memo,
useCallback,
useEffect,
useMemo,
} from 'react'
import { useTranslation } from 'react-i18next'
import useNodes from '@/app/components/workflow/store/workflow/use-nodes'
import BlockIcon from '../block-icon'
import { BlockEnum as BlockEnumValues } from '../types'
import { BlockSelectorRow } from './block-selector-row'
// import { useNodeMetaData } from '../hooks'
import { START_BLOCKS } from './constants'
type StartBlocksProps = {
searchText: string
onSelect: (type: BlockEnum, triggerDefaultValue?: TriggerDefaultValue) => void
availableBlocksTypes?: BlockEnum[]
onContentStateChange?: (hasContent: boolean) => void
hideUserInput?: boolean
showMostCommonBadge?: boolean
showUserInputAdded?: boolean
showUserInputDisabled?: boolean
disabled?: boolean
}
type StartBlockPreviewPayload = {
block: typeof START_BLOCKS[number]
}
const StartBlocks = ({
searchText,
onSelect,
availableBlocksTypes = [],
onContentStateChange,
hideUserInput = false, // Allow parent to explicitly hide Start node option (e.g. when one already exists).
showMostCommonBadge = false,
showUserInputAdded = false,
showUserInputDisabled = false,
disabled = false,
}: StartBlocksProps) => {
const { t } = useTranslation()
const nodes = useNodes()
const previewCardHandle = useMemo(() => createPreviewCardHandle<StartBlockPreviewPayload>(), [])
// const nodeMetaData = useNodeMetaData()
const filteredBlocks = useMemo(() => {
// Check if Start node already exists in workflow
const hasStartNode = nodes.some(node => (node.data as CommonNodeType)?.type === BlockEnumValues.Start)
const normalizedSearch = searchText.toLowerCase()
const getDisplayName = (blockType: BlockEnum) => {
if (blockType === BlockEnumValues.TriggerWebhook)
return t('customWebhook', { ns: 'workflow' })
return t(`blocks.${blockType}`, { ns: 'workflow' })
}
return START_BLOCKS.filter((block) => {
// Hide User Input (Start) if it already exists in workflow or if hideUserInput is true.
// In read-only conflict modes, keep it visible so the row can show Added or disabled tooltip state.
if (block.type === BlockEnumValues.Start && (hasStartNode || hideUserInput) && !showUserInputAdded && !showUserInputDisabled)
return false
// Filter by search text
const displayName = getDisplayName(block.type).toLowerCase()
if (!displayName.includes(normalizedSearch) && !block.title.toLowerCase().includes(normalizedSearch))
return false
// availableBlocksTypes now contains properly filtered entry node types from parent
return availableBlocksTypes.includes(block.type)
})
}, [searchText, availableBlocksTypes, nodes, t, hideUserInput, showUserInputAdded, showUserInputDisabled])
const isEmpty = filteredBlocks.length === 0
useEffect(() => {
onContentStateChange?.(!isEmpty)
}, [isEmpty, onContentStateChange])
// Preview is supplementary: the block icon, title and description all become
// reachable from the inspector + canvas once the row is clicked to insert
// the start node, so hover/focus-only activation is a11y-safe. See
// packages/dify-ui/AGENTS.md → Overlay Primitive Selection.
const renderBlock = useCallback((block: typeof START_BLOCKS[number]) => {
const isUserInput = block.type === BlockEnumValues.Start
const isUserInputDisabled = isUserInput && showUserInputDisabled
const isRowDisabled = disabled || (isUserInput && showUserInputAdded) || isUserInputDisabled
const label = t(`blocks.${block.type}`, { ns: 'workflow' })
const disabledReason = t('nodes.startPlaceholder.userInputConflictTip', { ns: 'workflow' })
const row = (
<BlockSelectorRow
aria-disabled={isRowDisabled}
aria-label={isUserInputDisabled ? `${label}. ${disabledReason}` : label}
disabled={isRowDisabled}
onClick={() => {
if (isRowDisabled)
return
onSelect(block.type)
}}
>
<div className={cn('flex min-w-0 flex-1 items-center', isUserInputDisabled && 'opacity-30')}>
<BlockIcon
className="mr-2 shrink-0"
type={block.type}
size="sm"
/>
<div className="flex w-0 grow items-center justify-between text-sm text-text-secondary">
<span className="truncate system-sm-medium">{label}</span>
{isUserInput && showUserInputAdded && (
<span className="ml-2 shrink-0 system-xs-regular text-text-tertiary">
{t('operation.added', { ns: 'common' })}
</span>
)}
{isUserInput && showMostCommonBadge && !showUserInputAdded && (
<span className="ml-2 shrink-0 rounded-[5px] border border-divider-deep px-1 py-0.5 system-2xs-medium-uppercase text-text-tertiary">
{t('blocks.mostCommon', { ns: 'workflow' })}
</span>
)}
{isUserInput && !showMostCommonBadge && !showUserInputAdded && !showUserInputDisabled && (
<span className="ml-2 shrink-0 system-xs-regular text-text-quaternary">{t('blocks.originalStartNode', { ns: 'workflow' })}</span>
)}
</div>
</div>
</BlockSelectorRow>
)
if (isUserInputDisabled) {
return (
<Tooltip key={block.type}>
<TooltipTrigger render={row} />
<TooltipContent placement="right" sideOffset={8} className="max-w-[240px] rounded-xl border-[0.5px] border-components-panel-border bg-components-tooltip-bg px-4 py-3.5 shadow-lg">
<p className="system-xs-regular text-text-secondary">
{disabledReason}
</p>
</TooltipContent>
</Tooltip>
)
}
return (
<PreviewCardTrigger
key={block.type}
delay={150}
closeDelay={150}
handle={previewCardHandle}
payload={{ block }}
render={row}
/>
)
}, [disabled, onSelect, previewCardHandle, showMostCommonBadge, showUserInputAdded, showUserInputDisabled, t])
if (isEmpty)
return null
return (
<div className="p-1">
<div>
{filteredBlocks.map((block, index) => (
<div key={block.type}>
{renderBlock(block)}
{block.type === BlockEnumValues.Start && !showMostCommonBadge && index < filteredBlocks.length - 1 && (
<div className="my-1 px-3">
<div className="border-t border-divider-subtle" />
</div>
)}
</div>
))}
</div>
<PreviewCard handle={previewCardHandle}>
{({ payload }) => (
<StartBlockPreviewCard
payload={payload as StartBlockPreviewPayload | undefined}
t={t}
/>
)}
</PreviewCard>
</div>
)
}
type StartBlockPreviewCardProps = {
payload?: StartBlockPreviewPayload
t: ReturnType<typeof useTranslation>['t']
}
function StartBlockPreviewCard({
payload,
t,
}: StartBlockPreviewCardProps) {
if (!payload)
return null
const { block } = payload
const description = block.type === BlockEnumValues.Start
? t('nodes.start.userInputTipDescription', { ns: 'workflow' })
: t(`blocksAbout.${block.type}`, { ns: 'workflow' })
const showDifyTeamAuthor = [
BlockEnumValues.Start,
BlockEnumValues.TriggerWebhook,
BlockEnumValues.TriggerSchedule,
].includes(block.type)
return (
<PreviewCardContent placement="right" popupClassName="w-[224px] px-3 pt-3 pb-2.5">
<div>
<BlockIcon
size="md"
className="mb-2"
type={block.type}
/>
<div className="mb-1 system-md-medium text-text-primary">
{t(`blocks.${block.type}`, { ns: 'workflow' })}
</div>
<div className="system-xs-regular wrap-break-word text-text-secondary">
{description}
</div>
{showDifyTeamAuthor && (
<div className="mt-1 system-xs-regular text-text-tertiary">
{t('author', { ns: 'tools' })}
{' '}
{t('difyTeam', { ns: 'workflow' })}
</div>
)}
</div>
</PreviewCardContent>
)
}
export default memo(StartBlocks)