add-block.tsx 3.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109
  1. import {
  2. memo,
  3. useCallback,
  4. useState,
  5. } from 'react'
  6. import { RiAddCircleFill } from '@remixicon/react'
  7. import { useStoreApi } from 'reactflow'
  8. import { useTranslation } from 'react-i18next'
  9. import type { OffsetOptions } from '@floating-ui/react'
  10. import {
  11. generateNewNode,
  12. } from '../utils'
  13. import {
  14. useAvailableBlocks,
  15. useNodesReadOnly,
  16. usePanelInteractions,
  17. } from '../hooks'
  18. import { NODES_INITIAL_DATA } from '../constants'
  19. import { useWorkflowStore } from '../store'
  20. import TipPopup from './tip-popup'
  21. import cn from '@/utils/classnames'
  22. import BlockSelector from '@/app/components/workflow/block-selector'
  23. import type {
  24. OnSelectBlock,
  25. } from '@/app/components/workflow/types'
  26. import {
  27. BlockEnum,
  28. } from '@/app/components/workflow/types'
  29. type AddBlockProps = {
  30. renderTrigger?: (open: boolean) => React.ReactNode
  31. offset?: OffsetOptions
  32. }
  33. const AddBlock = ({
  34. renderTrigger,
  35. offset,
  36. }: AddBlockProps) => {
  37. const { t } = useTranslation()
  38. const store = useStoreApi()
  39. const workflowStore = useWorkflowStore()
  40. const { nodesReadOnly } = useNodesReadOnly()
  41. const { handlePaneContextmenuCancel } = usePanelInteractions()
  42. const [open, setOpen] = useState(false)
  43. const { availableNextBlocks } = useAvailableBlocks(BlockEnum.Start, false)
  44. const handleOpenChange = useCallback((open: boolean) => {
  45. setOpen(open)
  46. if (!open)
  47. handlePaneContextmenuCancel()
  48. }, [handlePaneContextmenuCancel])
  49. const handleSelect = useCallback<OnSelectBlock>((type, toolDefaultValue) => {
  50. const {
  51. getNodes,
  52. } = store.getState()
  53. const nodes = getNodes()
  54. const nodesWithSameType = nodes.filter(node => node.data.type === type)
  55. const { newNode } = generateNewNode({
  56. data: {
  57. ...NODES_INITIAL_DATA[type],
  58. title: nodesWithSameType.length > 0 ? `${t(`workflow.blocks.${type}`)} ${nodesWithSameType.length + 1}` : t(`workflow.blocks.${type}`),
  59. ...(toolDefaultValue || {}),
  60. _isCandidate: true,
  61. },
  62. position: {
  63. x: 0,
  64. y: 0,
  65. },
  66. })
  67. workflowStore.setState({
  68. candidateNode: newNode,
  69. })
  70. }, [store, workflowStore, t])
  71. const renderTriggerElement = useCallback((open: boolean) => {
  72. return (
  73. <TipPopup
  74. title={t('workflow.common.addBlock')}
  75. >
  76. <div className={cn(
  77. 'flex items-center justify-center w-8 h-8 rounded-lg hover:bg-black/5 hover:text-gray-700 cursor-pointer',
  78. `${nodesReadOnly && '!cursor-not-allowed opacity-50'}`,
  79. open && '!bg-black/5',
  80. )}>
  81. <RiAddCircleFill className='w-4 h-4' />
  82. </div>
  83. </TipPopup>
  84. )
  85. }, [nodesReadOnly, t])
  86. return (
  87. <BlockSelector
  88. open={open}
  89. onOpenChange={handleOpenChange}
  90. disabled={nodesReadOnly}
  91. onSelect={handleSelect}
  92. placement='top-start'
  93. offset={offset ?? {
  94. mainAxis: 4,
  95. crossAxis: -8,
  96. }}
  97. trigger={renderTrigger || renderTriggerElement}
  98. popupClassName='!min-w-[256px]'
  99. availableBlocksTypes={availableNextBlocks}
  100. />
  101. )
  102. }
  103. export default memo(AddBlock)