index.tsx 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158
  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. import classNames from '@/utils/classnames'
  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 (!newOpen)
  68. setSearchText('')
  69. if (onOpenChange)
  70. onOpenChange(newOpen)
  71. }, [onOpenChange])
  72. const handleTrigger = useCallback<MouseEventHandler<HTMLDivElement>>((e) => {
  73. if (disabled)
  74. return
  75. e.stopPropagation()
  76. handleOpenChange(!open)
  77. }, [handleOpenChange, open, disabled])
  78. const handleSelect = useCallback<OnSelectBlock>((type, toolDefaultValue) => {
  79. handleOpenChange(false)
  80. onSelect(type, toolDefaultValue)
  81. }, [handleOpenChange, onSelect])
  82. const [activeTab, setActiveTab] = useState(noBlocks ? TabsEnum.Tools : TabsEnum.Blocks)
  83. const handleActiveTabChange = useCallback((newActiveTab: TabsEnum) => {
  84. setActiveTab(newActiveTab)
  85. }, [])
  86. const searchPlaceholder = useMemo(() => {
  87. if (activeTab === TabsEnum.Blocks)
  88. return t('workflow.tabs.searchBlock')
  89. if (activeTab === TabsEnum.Tools)
  90. return t('workflow.tabs.searchTool')
  91. return ''
  92. }, [activeTab, t])
  93. return (
  94. <PortalToFollowElem
  95. placement={placement}
  96. offset={offset}
  97. open={open}
  98. onOpenChange={handleOpenChange}
  99. >
  100. <PortalToFollowElemTrigger
  101. asChild={asChild}
  102. onClick={handleTrigger}
  103. className={triggerInnerClassName}
  104. >
  105. {
  106. trigger
  107. ? trigger(open)
  108. : (
  109. <div
  110. className={`
  111. flex items-center justify-center
  112. 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
  113. ${triggerClassName?.(open)}
  114. `}
  115. style={triggerStyle}
  116. >
  117. <Plus02 className='w-2.5 h-2.5' />
  118. </div>
  119. )
  120. }
  121. </PortalToFollowElemTrigger>
  122. <PortalToFollowElemContent className='z-[1000]'>
  123. <div className={
  124. classNames(`rounded-lg border-[0.5px] backdrop-blur-[5px]
  125. border-components-panel-border bg-components-panel-bg-blur shadow-lg`, popupClassName)}>
  126. <div className='p-2 pb-1' onClick={e => e.stopPropagation()}>
  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. </div>
  137. <Tabs
  138. activeTab={activeTab}
  139. onActiveTabChange={handleActiveTabChange}
  140. onSelect={handleSelect}
  141. searchText={searchText}
  142. availableBlocksTypes={availableBlocksTypes}
  143. noBlocks={noBlocks}
  144. />
  145. </div>
  146. </PortalToFollowElemContent>
  147. </PortalToFollowElem>
  148. )
  149. }
  150. export default memo(NodeSelector)