dify/web/app/components/workflow/operator/add-block.tsx
yyh 2ec34b2cfb
feat(workflow): improve block selector navigation and previews (#39212)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-07-18 09:56:36 +00:00

133 lines
4.2 KiB
TypeScript

import type { NodeSelectorProps } from '@/app/components/workflow/block-selector/main'
import type { Node, OnSelectBlock } from '@/app/components/workflow/types'
import { cn } from '@langgenius/dify-ui/cn'
import { RiAddCircleFill } from '@remixicon/react'
import { memo, useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useStoreApi } from 'reactflow'
import BlockSelector from '@/app/components/workflow/block-selector'
import { BlockEnum } from '@/app/components/workflow/types'
import { FlowType } from '@/types/common'
import {
useAvailableBlocks,
useIsChatMode,
useNodesMetaData,
useNodesReadOnly,
usePanelInteractions,
} from '../hooks'
import { useHooksStore } from '../hooks-store'
import { useWorkflowStore } from '../store'
import {
generateNewNode,
getNodeCustomTypeByNodeDataType,
getNodesWithSameDefaultDataType,
} from '../utils'
import TipPopup from './tip-popup'
type AddBlockProps = {
renderTrigger?: (open: boolean) => React.ReactNode
renderTriggerAsButtonRoot?: boolean
offset?: NodeSelectorProps['offset']
onClose?: () => void
isolateKeyboardEvents?: boolean
}
const AddBlock = ({
renderTrigger,
renderTriggerAsButtonRoot,
offset,
onClose,
isolateKeyboardEvents,
}: AddBlockProps) => {
const { t } = useTranslation()
const store = useStoreApi()
const workflowStore = useWorkflowStore()
const isChatMode = useIsChatMode()
const { nodesReadOnly } = useNodesReadOnly()
const { handlePaneContextmenuCancel } = usePanelInteractions()
const [open, setOpen] = useState(false)
const { availableNextBlocks } = useAvailableBlocks(BlockEnum.Start, false)
const { nodesMap: nodesMetaDataMap } = useNodesMetaData()
const flowType = useHooksStore((s) => s.configsMap?.flowType)
const showStartTab = flowType !== FlowType.ragPipeline && !isChatMode
const handleOpenChange = useCallback(
(open: boolean) => {
setOpen(open)
if (!open) (onClose ?? handlePaneContextmenuCancel)()
},
[handlePaneContextmenuCancel, onClose],
)
const handleSelect = useCallback<OnSelectBlock>(
(type, pluginDefaultValue) => {
const { getNodes } = store.getState()
const { defaultValue } = nodesMetaDataMap![type]
const nodes = getNodes()
const nodesWithSameType = getNodesWithSameDefaultDataType(nodes, type, defaultValue)
const { newNode } = generateNewNode({
type: getNodeCustomTypeByNodeDataType(type),
data: {
...(defaultValue as Node['data']),
title:
nodesWithSameType.length > 0
? `${defaultValue.title} ${nodesWithSameType.length + 1}`
: defaultValue.title,
...pluginDefaultValue,
_isCandidate: true,
},
position: {
x: 0,
y: 0,
},
})
workflowStore.setState({
candidateNode: newNode,
})
},
[store, workflowStore, nodesMetaDataMap],
)
const renderTriggerElement = useCallback(
(open: boolean) => {
return (
<TipPopup title={t(($) => $['common.addBlock'], { ns: 'workflow' })}>
<div
className={cn(
'flex size-8 cursor-pointer items-center justify-center rounded-lg text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary',
`${nodesReadOnly && 'cursor-not-allowed text-text-disabled hover:bg-transparent hover:text-text-disabled'}`,
open && 'bg-state-accent-active text-text-accent',
)}
>
<RiAddCircleFill className="size-4" />
</div>
</TipPopup>
)
},
[nodesReadOnly, t],
)
return (
<BlockSelector
open={open}
onOpenChange={handleOpenChange}
disabled={nodesReadOnly}
onSelect={handleSelect}
placement="right-start"
offset={
offset ?? {
mainAxis: 4,
crossAxis: -8,
}
}
trigger={renderTrigger || renderTriggerElement}
renderTriggerAsButtonRoot={renderTriggerAsButtonRoot}
popupClassName="min-w-[256px]!"
availableBlocksTypes={availableNextBlocks}
showStartTab={showStartTab}
isolateKeyboardEvents={isolateKeyboardEvents}
/>
)
}
export default memo(AddBlock)