| import { | |
| memo, | |
| useCallback, | |
| useState, | |
| } from 'react' | |
| import { RiAddCircleFill } from '@remixicon/react' | |
| import { useStoreApi } from 'reactflow' | |
| import { useTranslation } from 'react-i18next' | |
| import type { OffsetOptions } from '@floating-ui/react' | |
| import { | |
| generateNewNode, | |
| } from '../utils' | |
| import { | |
| useAvailableBlocks, | |
| useNodesReadOnly, | |
| usePanelInteractions, | |
| } from '../hooks' | |
| import { NODES_INITIAL_DATA } from '../constants' | |
| import { useWorkflowStore } from '../store' | |
| import TipPopup from './tip-popup' | |
| import cn from '@/utils/classnames' | |
| import BlockSelector from '@/app/components/workflow/block-selector' | |
| import type { | |
| OnSelectBlock, | |
| } from '@/app/components/workflow/types' | |
| import { | |
| BlockEnum, | |
| } from '@/app/components/workflow/types' | |
| type AddBlockProps = { | |
| renderTrigger?: (open: boolean) => React.ReactNode | |
| offset?: OffsetOptions | |
| } | |
| const AddBlock = ({ | |
| renderTrigger, | |
| offset, | |
| }: AddBlockProps) => { | |
| const { t } = useTranslation() | |
| const store = useStoreApi() | |
| const workflowStore = useWorkflowStore() | |
| const { nodesReadOnly } = useNodesReadOnly() | |
| const { handlePaneContextmenuCancel } = usePanelInteractions() | |
| const [open, setOpen] = useState(false) | |
| const { availableNextBlocks } = useAvailableBlocks(BlockEnum.Start, false) | |
| const handleOpenChange = useCallback((open: boolean) => { | |
| setOpen(open) | |
| if (!open) | |
| handlePaneContextmenuCancel() | |
| }, [handlePaneContextmenuCancel]) | |
| const handleSelect = useCallback<OnSelectBlock>((type, toolDefaultValue) => { | |
| const { | |
| getNodes, | |
| } = store.getState() | |
| const nodes = getNodes() | |
| const nodesWithSameType = nodes.filter(node => node.data.type === type) | |
| const { newNode } = generateNewNode({ | |
| data: { | |
| ...NODES_INITIAL_DATA[type], | |
| title: nodesWithSameType.length > 0 ? `${t(`workflow.blocks.${type}`)} ${nodesWithSameType.length + 1}` : t(`workflow.blocks.${type}`), | |
| ...(toolDefaultValue || {}), | |
| _isCandidate: true, | |
| }, | |
| position: { | |
| x: 0, | |
| y: 0, | |
| }, | |
| }) | |
| workflowStore.setState({ | |
| candidateNode: newNode, | |
| }) | |
| }, [store, workflowStore, t]) | |
| const renderTriggerElement = useCallback((open: boolean) => { | |
| return ( | |
| <TipPopup | |
| title={t('workflow.common.addBlock')} | |
| > | |
| <div className={cn( | |
| 'flex items-center justify-center w-8 h-8 rounded-lg hover:bg-black/5 hover:text-gray-700 cursor-pointer', | |
| `${nodesReadOnly && '!cursor-not-allowed opacity-50'}`, | |
| open && '!bg-black/5', | |
| )}> | |
| <RiAddCircleFill className='w-4 h-4' /> | |
| </div> | |
| </TipPopup> | |
| ) | |
| }, [nodesReadOnly, t]) | |
| return ( | |
| <BlockSelector | |
| open={open} | |
| onOpenChange={handleOpenChange} | |
| disabled={nodesReadOnly} | |
| onSelect={handleSelect} | |
| placement='top-start' | |
| offset={offset ?? { | |
| mainAxis: 4, | |
| crossAxis: -8, | |
| }} | |
| trigger={renderTrigger || renderTriggerElement} | |
| popupClassName='!min-w-[256px]' | |
| availableBlocksTypes={availableNextBlocks} | |
| /> | |
| ) | |
| } | |
| export default memo(AddBlock) | |