|
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) |
|
|