index.tsx 5.0 KB

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