index.tsx 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149
  1. import type {
  2. FC,
  3. MouseEventHandler,
  4. } from 'react'
  5. import {
  6. memo,
  7. useCallback,
  8. useState,
  9. } from 'react'
  10. import { useTranslation } from 'react-i18next'
  11. import type {
  12. OffsetOptions,
  13. Placement,
  14. } from '@floating-ui/react'
  15. import type { BlockEnum, OnSelectBlock } from '../types'
  16. import Tabs from './tabs'
  17. import {
  18. PortalToFollowElem,
  19. PortalToFollowElemContent,
  20. PortalToFollowElemTrigger,
  21. } from '@/app/components/base/portal-to-follow-elem'
  22. import {
  23. Plus02,
  24. SearchLg,
  25. } from '@/app/components/base/icons/src/vender/line/general'
  26. import { XCircle } from '@/app/components/base/icons/src/vender/solid/general'
  27. type NodeSelectorProps = {
  28. open?: boolean
  29. onOpenChange?: (open: boolean) => void
  30. onSelect: OnSelectBlock
  31. trigger?: (open: boolean) => React.ReactNode
  32. placement?: Placement
  33. offset?: OffsetOptions
  34. triggerStyle?: React.CSSProperties
  35. triggerClassName?: (open: boolean) => string
  36. triggerInnerClassName?: string
  37. popupClassName?: string
  38. asChild?: boolean
  39. availableBlocksTypes?: BlockEnum[]
  40. disabled?: boolean
  41. noBlocks?: boolean
  42. }
  43. const NodeSelector: FC<NodeSelectorProps> = ({
  44. open: openFromProps,
  45. onOpenChange,
  46. onSelect,
  47. trigger,
  48. placement = 'right',
  49. offset = 6,
  50. triggerClassName,
  51. triggerInnerClassName,
  52. triggerStyle,
  53. popupClassName,
  54. asChild,
  55. availableBlocksTypes,
  56. disabled,
  57. noBlocks = false,
  58. }) => {
  59. const { t } = useTranslation()
  60. const [searchText, setSearchText] = useState('')
  61. const [localOpen, setLocalOpen] = useState(false)
  62. const open = openFromProps === undefined ? localOpen : openFromProps
  63. const handleOpenChange = useCallback((newOpen: boolean) => {
  64. setLocalOpen(newOpen)
  65. if (onOpenChange)
  66. onOpenChange(newOpen)
  67. }, [onOpenChange])
  68. const handleTrigger = useCallback<MouseEventHandler<HTMLDivElement>>((e) => {
  69. if (disabled)
  70. return
  71. e.stopPropagation()
  72. handleOpenChange(!open)
  73. }, [handleOpenChange, open, disabled])
  74. const handleSelect = useCallback<OnSelectBlock>((type, toolDefaultValue) => {
  75. handleOpenChange(false)
  76. onSelect(type, toolDefaultValue)
  77. }, [handleOpenChange, onSelect])
  78. return (
  79. <PortalToFollowElem
  80. placement={placement}
  81. offset={offset}
  82. open={open}
  83. onOpenChange={handleOpenChange}
  84. >
  85. <PortalToFollowElemTrigger
  86. asChild={asChild}
  87. onClick={handleTrigger}
  88. className={triggerInnerClassName}
  89. >
  90. {
  91. trigger
  92. ? trigger(open)
  93. : (
  94. <div
  95. className={`
  96. flex items-center justify-center
  97. w-4 h-4 rounded-full bg-primary-600 cursor-pointer z-10
  98. ${triggerClassName?.(open)}
  99. `}
  100. style={triggerStyle}
  101. >
  102. <Plus02 className='w-2.5 h-2.5 text-white' />
  103. </div>
  104. )
  105. }
  106. </PortalToFollowElemTrigger>
  107. <PortalToFollowElemContent className='z-[1000]'>
  108. <div className={`rounded-lg border-[0.5px] border-gray-200 bg-white shadow-lg ${popupClassName}`}>
  109. <div className='px-2 pt-2'>
  110. <div
  111. className='flex items-center px-2 rounded-lg bg-gray-100'
  112. onClick={e => e.stopPropagation()}
  113. >
  114. <SearchLg className='shrink-0 ml-[1px] mr-[5px] w-3.5 h-3.5 text-gray-400' />
  115. <input
  116. value={searchText}
  117. className='grow px-0.5 py-[7px] text-[13px] text-gray-700 bg-transparent appearance-none outline-none caret-primary-600 placeholder:text-gray-400'
  118. placeholder={t('workflow.tabs.searchBlock') || ''}
  119. onChange={e => setSearchText(e.target.value)}
  120. autoFocus
  121. />
  122. {
  123. searchText && (
  124. <div
  125. className='flex items-center justify-center ml-[5px] w-[18px] h-[18px] cursor-pointer'
  126. onClick={() => setSearchText('')}
  127. >
  128. <XCircle className='w-[14px] h-[14px] text-gray-400' />
  129. </div>
  130. )
  131. }
  132. </div>
  133. </div>
  134. <Tabs
  135. onSelect={handleSelect}
  136. searchText={searchText}
  137. availableBlocksTypes={availableBlocksTypes}
  138. noBlocks={noBlocks}
  139. />
  140. </div>
  141. </PortalToFollowElemContent>
  142. </PortalToFollowElem>
  143. )
  144. }
  145. export default memo(NodeSelector)