add-block.tsx 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111
  1. import {
  2. memo,
  3. useCallback,
  4. useState,
  5. } from 'react'
  6. import cn from 'classnames'
  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. useNodesExtraData,
  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 BlockSelector from '@/app/components/workflow/block-selector'
  22. import { Plus } from '@/app/components/base/icons/src/vender/line/general'
  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 nodesExtraData = useNodesExtraData()
  41. const { nodesReadOnly } = useNodesReadOnly()
  42. const { handlePaneContextmenuCancel } = usePanelInteractions()
  43. const [open, setOpen] = useState(false)
  44. const availableNextNodes = nodesExtraData[BlockEnum.Start].availableNextNodes
  45. const handleOpenChange = useCallback((open: boolean) => {
  46. setOpen(open)
  47. if (!open)
  48. handlePaneContextmenuCancel()
  49. }, [handlePaneContextmenuCancel])
  50. const handleSelect = useCallback<OnSelectBlock>((type, toolDefaultValue) => {
  51. const {
  52. getNodes,
  53. } = store.getState()
  54. const nodes = getNodes()
  55. const nodesWithSameType = nodes.filter(node => node.data.type === type)
  56. const newNode = generateNewNode({
  57. data: {
  58. ...NODES_INITIAL_DATA[type],
  59. title: nodesWithSameType.length > 0 ? `${t(`workflow.blocks.${type}`)} ${nodesWithSameType.length + 1}` : t(`workflow.blocks.${type}`),
  60. ...(toolDefaultValue || {}),
  61. _isCandidate: true,
  62. },
  63. position: {
  64. x: 0,
  65. y: 0,
  66. },
  67. })
  68. workflowStore.setState({
  69. candidateNode: newNode,
  70. })
  71. }, [store, workflowStore, t])
  72. const renderTriggerElement = useCallback((open: boolean) => {
  73. return (
  74. <TipPopup
  75. title={t('workflow.common.addBlock')}
  76. >
  77. <div className={cn(
  78. 'flex items-center justify-center w-8 h-8 rounded-lg hover:bg-black/5 hover:text-gray-700 cursor-pointer',
  79. `${nodesReadOnly && '!cursor-not-allowed opacity-50'}`,
  80. open && '!bg-black/5',
  81. )}>
  82. <Plus className='w-4 h-4' />
  83. </div>
  84. </TipPopup>
  85. )
  86. }, [nodesReadOnly, t])
  87. return (
  88. <BlockSelector
  89. open={open}
  90. onOpenChange={handleOpenChange}
  91. disabled={nodesReadOnly}
  92. onSelect={handleSelect}
  93. placement='top-start'
  94. offset={offset ?? {
  95. mainAxis: 4,
  96. crossAxis: -8,
  97. }}
  98. trigger={renderTrigger || renderTriggerElement}
  99. popupClassName='!min-w-[256px]'
  100. availableBlocksTypes={availableNextNodes}
  101. />
  102. )
  103. }
  104. export default memo(AddBlock)