index.tsx 4.3 KB

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