index.tsx 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  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 type { BlockEnum, OnSelectBlock } from '../types'
  17. import Tabs from './tabs'
  18. import { TabsEnum } from './types'
  19. import {
  20. PortalToFollowElem,
  21. PortalToFollowElemContent,
  22. PortalToFollowElemTrigger,
  23. } from '@/app/components/base/portal-to-follow-elem'
  24. import Input from '@/app/components/base/input'
  25. import SearchBox from '@/app/components/plugins/marketplace/search-box'
  26. import {
  27. Plus02,
  28. } from '@/app/components/base/icons/src/vender/line/general'
  29. import classNames from '@/utils/classnames'
  30. type NodeSelectorProps = {
  31. open?: boolean
  32. onOpenChange?: (open: boolean) => void
  33. onSelect: OnSelectBlock
  34. trigger?: (open: boolean) => React.ReactNode
  35. placement?: Placement
  36. offset?: OffsetOptions
  37. triggerStyle?: React.CSSProperties
  38. triggerClassName?: (open: boolean) => string
  39. triggerInnerClassName?: string
  40. popupClassName?: string
  41. asChild?: boolean
  42. availableBlocksTypes?: BlockEnum[]
  43. disabled?: boolean
  44. noBlocks?: boolean
  45. }
  46. const NodeSelector: FC<NodeSelectorProps> = ({
  47. open: openFromProps,
  48. onOpenChange,
  49. onSelect,
  50. trigger,
  51. placement = 'right',
  52. offset = 6,
  53. triggerClassName,
  54. triggerInnerClassName,
  55. triggerStyle,
  56. popupClassName,
  57. asChild,
  58. availableBlocksTypes,
  59. disabled,
  60. noBlocks = false,
  61. }) => {
  62. const { t } = useTranslation()
  63. const [searchText, setSearchText] = useState('')
  64. const [tags, setTags] = useState<string[]>([])
  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-components-button-primary-bg text-text-primary-on-surface hover:bg-components-button-primary-bg-hover cursor-pointer z-10
  115. ${triggerClassName?.(open)}
  116. `}
  117. style={triggerStyle}
  118. >
  119. <Plus02 className='w-2.5 h-2.5' />
  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' onClick={e => e.stopPropagation()}>
  127. {activeTab === TabsEnum.Blocks && (
  128. <Input
  129. showLeftIcon
  130. showClearIcon
  131. autoFocus
  132. value={searchText}
  133. placeholder={searchPlaceholder}
  134. onChange={e => setSearchText(e.target.value)}
  135. onClear={() => setSearchText('')}
  136. />
  137. )}
  138. {activeTab === TabsEnum.Tools && (
  139. <SearchBox
  140. search={searchText}
  141. onSearchChange={setSearchText}
  142. tags={tags}
  143. onTagsChange={setTags}
  144. size='small'
  145. placeholder={t('plugin.searchTools')!}
  146. />
  147. )}
  148. </div>
  149. <Tabs
  150. activeTab={activeTab}
  151. onActiveTabChange={handleActiveTabChange}
  152. onSelect={handleSelect}
  153. searchText={searchText}
  154. tags={tags}
  155. availableBlocksTypes={availableBlocksTypes}
  156. noBlocks={noBlocks}
  157. />
  158. </div>
  159. </PortalToFollowElemContent>
  160. </PortalToFollowElem>
  161. )
  162. }
  163. export default memo(NodeSelector)