index.tsx 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
  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 {
  26. Plus02,
  27. } from '@/app/components/base/icons/src/vender/line/general'
  28. type NodeSelectorProps = {
  29. open?: boolean
  30. onOpenChange?: (open: boolean) => void
  31. onSelect: OnSelectBlock
  32. trigger?: (open: boolean) => React.ReactNode
  33. placement?: Placement
  34. offset?: OffsetOptions
  35. triggerStyle?: React.CSSProperties
  36. triggerClassName?: (open: boolean) => string
  37. triggerInnerClassName?: string
  38. popupClassName?: string
  39. asChild?: boolean
  40. availableBlocksTypes?: BlockEnum[]
  41. disabled?: boolean
  42. noBlocks?: boolean
  43. }
  44. const NodeSelector: FC<NodeSelectorProps> = ({
  45. open: openFromProps,
  46. onOpenChange,
  47. onSelect,
  48. trigger,
  49. placement = 'right',
  50. offset = 6,
  51. triggerClassName,
  52. triggerInnerClassName,
  53. triggerStyle,
  54. popupClassName,
  55. asChild,
  56. availableBlocksTypes,
  57. disabled,
  58. noBlocks = false,
  59. }) => {
  60. const { t } = useTranslation()
  61. const [searchText, setSearchText] = useState('')
  62. const [localOpen, setLocalOpen] = useState(false)
  63. const open = openFromProps === undefined ? localOpen : openFromProps
  64. const handleOpenChange = useCallback((newOpen: boolean) => {
  65. setLocalOpen(newOpen)
  66. if (!newOpen)
  67. setSearchText('')
  68. if (onOpenChange)
  69. onOpenChange(newOpen)
  70. }, [onOpenChange])
  71. const handleTrigger = useCallback<MouseEventHandler<HTMLDivElement>>((e) => {
  72. if (disabled)
  73. return
  74. e.stopPropagation()
  75. handleOpenChange(!open)
  76. }, [handleOpenChange, open, disabled])
  77. const handleSelect = useCallback<OnSelectBlock>((type, toolDefaultValue) => {
  78. handleOpenChange(false)
  79. onSelect(type, toolDefaultValue)
  80. }, [handleOpenChange, onSelect])
  81. const [activeTab, setActiveTab] = useState(noBlocks ? TabsEnum.Tools : TabsEnum.Blocks)
  82. const handleActiveTabChange = useCallback((newActiveTab: TabsEnum) => {
  83. setActiveTab(newActiveTab)
  84. }, [])
  85. const searchPlaceholder = useMemo(() => {
  86. if (activeTab === TabsEnum.Blocks)
  87. return t('workflow.tabs.searchBlock')
  88. if (activeTab === TabsEnum.Tools)
  89. return t('workflow.tabs.searchTool')
  90. return ''
  91. }, [activeTab, t])
  92. return (
  93. <PortalToFollowElem
  94. placement={placement}
  95. offset={offset}
  96. open={open}
  97. onOpenChange={handleOpenChange}
  98. >
  99. <PortalToFollowElemTrigger
  100. asChild={asChild}
  101. onClick={handleTrigger}
  102. className={triggerInnerClassName}
  103. >
  104. {
  105. trigger
  106. ? trigger(open)
  107. : (
  108. <div
  109. className={`
  110. flex items-center justify-center
  111. w-4 h-4 rounded-full bg-primary-600 cursor-pointer z-10
  112. ${triggerClassName?.(open)}
  113. `}
  114. style={triggerStyle}
  115. >
  116. <Plus02 className='w-2.5 h-2.5 text-white' />
  117. </div>
  118. )
  119. }
  120. </PortalToFollowElemTrigger>
  121. <PortalToFollowElemContent className='z-[1000]'>
  122. <div className={`rounded-lg border-[0.5px] border-gray-200 bg-white shadow-lg ${popupClassName}`}>
  123. <div className='px-2 pt-2' onClick={e => e.stopPropagation()}>
  124. <Input
  125. showLeftIcon
  126. showClearIcon
  127. autoFocus
  128. value={searchText}
  129. placeholder={searchPlaceholder}
  130. onChange={e => setSearchText(e.target.value)}
  131. onClear={() => setSearchText('')}
  132. />
  133. </div>
  134. <Tabs
  135. activeTab={activeTab}
  136. onActiveTabChange={handleActiveTabChange}
  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)