index.tsx 4.9 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 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. 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 [tags, setTags] = useState<string[]>([])
  64. const [localOpen, setLocalOpen] = useState(false)
  65. const open = openFromProps === undefined ? localOpen : openFromProps
  66. const handleOpenChange = useCallback((newOpen: boolean) => {
  67. setLocalOpen(newOpen)
  68. if (!newOpen)
  69. setSearchText('')
  70. if (onOpenChange)
  71. onOpenChange(newOpen)
  72. }, [onOpenChange])
  73. const handleTrigger = useCallback<MouseEventHandler<HTMLDivElement>>((e) => {
  74. if (disabled)
  75. return
  76. e.stopPropagation()
  77. handleOpenChange(!open)
  78. }, [handleOpenChange, open, disabled])
  79. const handleSelect = useCallback<OnSelectBlock>((type, toolDefaultValue) => {
  80. handleOpenChange(false)
  81. onSelect(type, toolDefaultValue)
  82. }, [handleOpenChange, onSelect])
  83. const [activeTab, setActiveTab] = useState(noBlocks ? TabsEnum.Tools : TabsEnum.Blocks)
  84. const handleActiveTabChange = useCallback((newActiveTab: TabsEnum) => {
  85. setActiveTab(newActiveTab)
  86. }, [])
  87. const searchPlaceholder = useMemo(() => {
  88. if (activeTab === TabsEnum.Blocks)
  89. return t('workflow.tabs.searchBlock')
  90. if (activeTab === TabsEnum.Tools)
  91. return t('workflow.tabs.searchTool')
  92. return ''
  93. }, [activeTab, t])
  94. return (
  95. <PortalToFollowElem
  96. placement={placement}
  97. offset={offset}
  98. open={open}
  99. onOpenChange={handleOpenChange}
  100. >
  101. <PortalToFollowElemTrigger
  102. asChild={asChild}
  103. onClick={handleTrigger}
  104. className={triggerInnerClassName}
  105. >
  106. {
  107. trigger
  108. ? trigger(open)
  109. : (
  110. <div
  111. className={`
  112. z-10 flex h-4
  113. w-4 cursor-pointer items-center justify-center rounded-full bg-components-button-primary-bg text-text-primary-on-surface hover:bg-components-button-primary-bg-hover
  114. ${triggerClassName?.(open)}
  115. `}
  116. style={triggerStyle}
  117. >
  118. <Plus02 className='h-2.5 w-2.5' />
  119. </div>
  120. )
  121. }
  122. </PortalToFollowElemTrigger>
  123. <PortalToFollowElemContent className='z-[1000]'>
  124. <div className={`rounded-lg border-[0.5px] border-gray-200 bg-white shadow-lg ${popupClassName}`}>
  125. <div className='px-2 pt-2' onClick={e => e.stopPropagation()}>
  126. {activeTab === TabsEnum.Blocks && (
  127. <Input
  128. showLeftIcon
  129. showClearIcon
  130. autoFocus
  131. value={searchText}
  132. placeholder={searchPlaceholder}
  133. onChange={e => setSearchText(e.target.value)}
  134. onClear={() => setSearchText('')}
  135. />
  136. )}
  137. {activeTab === TabsEnum.Tools && (
  138. <SearchBox
  139. search={searchText}
  140. onSearchChange={setSearchText}
  141. tags={tags}
  142. onTagsChange={setTags}
  143. size='small'
  144. placeholder={t('plugin.searchTools')!}
  145. />
  146. )}
  147. </div>
  148. <Tabs
  149. activeTab={activeTab}
  150. onActiveTabChange={handleActiveTabChange}
  151. onSelect={handleSelect}
  152. searchText={searchText}
  153. tags={tags}
  154. availableBlocksTypes={availableBlocksTypes}
  155. noBlocks={noBlocks}
  156. />
  157. </div>
  158. </PortalToFollowElemContent>
  159. </PortalToFollowElem>
  160. )
  161. }
  162. export default memo(NodeSelector)