index.tsx 4.4 KB

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