index.tsx 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321
  1. import { memo, useMemo, useState } from 'react'
  2. import { useTranslation } from 'react-i18next'
  3. import { FixedSizeList as List, areEqual } from 'react-window'
  4. import type { ListChildComponentProps } from 'react-window'
  5. import { RiArrowDownSLine, RiArrowRightSLine } from '@remixicon/react'
  6. import Checkbox from '../../checkbox'
  7. import NotionIcon from '../../notion-icon'
  8. import cn from '@/utils/classnames'
  9. import type { DataSourceNotionPage, DataSourceNotionPageMap } from '@/models/common'
  10. type PageSelectorProps = {
  11. value: Set<string>
  12. disabledValue: Set<string>
  13. searchValue: string
  14. pagesMap: DataSourceNotionPageMap
  15. list: DataSourceNotionPage[]
  16. onSelect: (selectedPagesId: Set<string>) => void
  17. canPreview?: boolean
  18. previewPageId?: string
  19. onPreview?: (selectedPageId: string) => void
  20. }
  21. type NotionPageTreeItem = {
  22. children: Set<string>
  23. descendants: Set<string>
  24. depth: number
  25. ancestors: string[]
  26. } & DataSourceNotionPage
  27. type NotionPageTreeMap = Record<string, NotionPageTreeItem>
  28. type NotionPageItem = {
  29. expand: boolean
  30. depth: number
  31. } & DataSourceNotionPage
  32. const recursivePushInParentDescendants = (
  33. pagesMap: DataSourceNotionPageMap,
  34. listTreeMap: NotionPageTreeMap,
  35. current: NotionPageTreeItem,
  36. leafItem: NotionPageTreeItem,
  37. ) => {
  38. const parentId = current.parent_id
  39. const pageId = current.page_id
  40. if (!parentId || !pageId)
  41. return
  42. if (parentId !== 'root' && pagesMap[parentId]) {
  43. if (!listTreeMap[parentId]) {
  44. const children = new Set([pageId])
  45. const descendants = new Set([pageId, leafItem.page_id])
  46. listTreeMap[parentId] = {
  47. ...pagesMap[parentId],
  48. children,
  49. descendants,
  50. depth: 0,
  51. ancestors: [],
  52. }
  53. }
  54. else {
  55. listTreeMap[parentId].children.add(pageId)
  56. listTreeMap[parentId].descendants.add(pageId)
  57. listTreeMap[parentId].descendants.add(leafItem.page_id)
  58. }
  59. leafItem.depth++
  60. leafItem.ancestors.unshift(listTreeMap[parentId].page_name)
  61. if (listTreeMap[parentId].parent_id !== 'root')
  62. recursivePushInParentDescendants(pagesMap, listTreeMap, listTreeMap[parentId], leafItem)
  63. }
  64. }
  65. const ItemComponent = ({ index, style, data }: ListChildComponentProps<{
  66. dataList: NotionPageItem[]
  67. handleToggle: (index: number) => void
  68. checkedIds: Set<string>
  69. disabledCheckedIds: Set<string>
  70. handleCheck: (index: number) => void
  71. canPreview?: boolean
  72. handlePreview: (index: number) => void
  73. listMapWithChildrenAndDescendants: NotionPageTreeMap
  74. searchValue: string
  75. previewPageId: string
  76. pagesMap: DataSourceNotionPageMap
  77. }>) => {
  78. const { t } = useTranslation()
  79. const { dataList, handleToggle, checkedIds, disabledCheckedIds, handleCheck, canPreview, handlePreview, listMapWithChildrenAndDescendants, searchValue, previewPageId, pagesMap } = data
  80. const current = dataList[index]
  81. const currentWithChildrenAndDescendants = listMapWithChildrenAndDescendants[current.page_id]
  82. const hasChild = currentWithChildrenAndDescendants.descendants.size > 0
  83. const ancestors = currentWithChildrenAndDescendants.ancestors
  84. const breadCrumbs = ancestors.length ? [...ancestors, current.page_name] : [current.page_name]
  85. const disabled = disabledCheckedIds.has(current.page_id)
  86. const renderArrow = () => {
  87. if (hasChild) {
  88. return (
  89. <div
  90. className='mr-1 flex h-5 w-5 shrink-0 items-center justify-center rounded-md hover:bg-components-button-ghost-bg-hover'
  91. style={{ marginLeft: current.depth * 8 }}
  92. onClick={() => handleToggle(index)}
  93. >
  94. {
  95. current.expand
  96. ? <RiArrowDownSLine className='h-4 w-4 text-text-tertiary' />
  97. : <RiArrowRightSLine className='h-4 w-4 text-text-tertiary' />
  98. }
  99. </div>
  100. )
  101. }
  102. if (current.parent_id === 'root' || !pagesMap[current.parent_id]) {
  103. return (
  104. <div></div>
  105. )
  106. }
  107. return (
  108. <div className='mr-1 h-5 w-5 shrink-0' style={{ marginLeft: current.depth * 8 }} />
  109. )
  110. }
  111. return (
  112. <div
  113. className={cn('group flex cursor-pointer items-center rounded-md pl-2 pr-[2px] hover:bg-state-base-hover',
  114. previewPageId === current.page_id && 'bg-state-base-hover')}
  115. style={{ ...style, top: style.top as number + 8, left: 8, right: 8, width: 'calc(100% - 16px)' }}
  116. >
  117. <Checkbox
  118. className='mr-2 shrink-0'
  119. checked={checkedIds.has(current.page_id)}
  120. disabled={disabled}
  121. onCheck={() => {
  122. if (disabled)
  123. return
  124. handleCheck(index)
  125. }}
  126. />
  127. {!searchValue && renderArrow()}
  128. <NotionIcon
  129. className='mr-1 shrink-0'
  130. type='page'
  131. src={current.page_icon}
  132. />
  133. <div
  134. className='grow truncate text-[13px] font-medium leading-4 text-text-secondary'
  135. title={current.page_name}
  136. >
  137. {current.page_name}
  138. </div>
  139. {
  140. canPreview && (
  141. <div
  142. className='ml-1 hidden h-6 shrink-0 cursor-pointer items-center rounded-md border-[0.5px] border-components-button-secondary-border bg-components-button-secondary-bg px-2 text-xs
  143. font-medium leading-4 text-components-button-secondary-text shadow-xs shadow-shadow-shadow-3 backdrop-blur-[10px]
  144. hover:border-components-button-secondary-border-hover hover:bg-components-button-secondary-bg-hover group-hover:flex'
  145. onClick={() => handlePreview(index)}>
  146. {t('common.dataSource.notion.selector.preview')}
  147. </div>
  148. )
  149. }
  150. {
  151. searchValue && (
  152. <div
  153. className='ml-1 max-w-[120px] shrink-0 truncate text-xs text-text-quaternary'
  154. title={breadCrumbs.join(' / ')}
  155. >
  156. {breadCrumbs.join(' / ')}
  157. </div>
  158. )
  159. }
  160. </div>
  161. )
  162. }
  163. const Item = memo(ItemComponent, areEqual)
  164. const PageSelector = ({
  165. value,
  166. disabledValue,
  167. searchValue,
  168. pagesMap,
  169. list,
  170. onSelect,
  171. canPreview = true,
  172. previewPageId,
  173. onPreview,
  174. }: PageSelectorProps) => {
  175. const { t } = useTranslation()
  176. const [prevDataList, setPrevDataList] = useState(list)
  177. const [dataList, setDataList] = useState<NotionPageItem[]>([])
  178. const [localPreviewPageId, setLocalPreviewPageId] = useState('')
  179. if (prevDataList !== list) {
  180. setPrevDataList(list)
  181. setDataList(list.filter(item => item.parent_id === 'root' || !pagesMap[item.parent_id]).map((item) => {
  182. return {
  183. ...item,
  184. expand: false,
  185. depth: 0,
  186. }
  187. }))
  188. }
  189. const searchDataList = list.filter((item) => {
  190. return item.page_name.includes(searchValue)
  191. }).map((item) => {
  192. return {
  193. ...item,
  194. expand: false,
  195. depth: 0,
  196. }
  197. })
  198. const currentDataList = searchValue ? searchDataList : dataList
  199. const currentPreviewPageId = previewPageId === undefined ? localPreviewPageId : previewPageId
  200. const listMapWithChildrenAndDescendants = useMemo(() => {
  201. return list.reduce((prev: NotionPageTreeMap, next: DataSourceNotionPage) => {
  202. const pageId = next.page_id
  203. if (!prev[pageId])
  204. prev[pageId] = { ...next, children: new Set(), descendants: new Set(), depth: 0, ancestors: [] }
  205. recursivePushInParentDescendants(pagesMap, prev, prev[pageId], prev[pageId])
  206. return prev
  207. }, {})
  208. }, [list, pagesMap])
  209. const handleToggle = (index: number) => {
  210. const current = dataList[index]
  211. const pageId = current.page_id
  212. const currentWithChildrenAndDescendants = listMapWithChildrenAndDescendants[pageId]
  213. const descendantsIds = Array.from(currentWithChildrenAndDescendants.descendants)
  214. const childrenIds = Array.from(currentWithChildrenAndDescendants.children)
  215. let newDataList = []
  216. if (current.expand) {
  217. current.expand = false
  218. newDataList = [...dataList.filter(item => !descendantsIds.includes(item.page_id))]
  219. }
  220. else {
  221. current.expand = true
  222. newDataList = [
  223. ...dataList.slice(0, index + 1),
  224. ...childrenIds.map(item => ({
  225. ...pagesMap[item],
  226. expand: false,
  227. depth: listMapWithChildrenAndDescendants[item].depth,
  228. })),
  229. ...dataList.slice(index + 1)]
  230. }
  231. setDataList(newDataList)
  232. }
  233. const copyValue = new Set([...value])
  234. const handleCheck = (index: number) => {
  235. const current = currentDataList[index]
  236. const pageId = current.page_id
  237. const currentWithChildrenAndDescendants = listMapWithChildrenAndDescendants[pageId]
  238. if (copyValue.has(pageId)) {
  239. if (!searchValue) {
  240. for (const item of currentWithChildrenAndDescendants.descendants)
  241. copyValue.delete(item)
  242. }
  243. copyValue.delete(pageId)
  244. }
  245. else {
  246. if (!searchValue) {
  247. for (const item of currentWithChildrenAndDescendants.descendants)
  248. copyValue.add(item)
  249. }
  250. copyValue.add(pageId)
  251. }
  252. onSelect(new Set([...copyValue]))
  253. }
  254. const handlePreview = (index: number) => {
  255. const current = currentDataList[index]
  256. const pageId = current.page_id
  257. setLocalPreviewPageId(pageId)
  258. if (onPreview)
  259. onPreview(pageId)
  260. }
  261. if (!currentDataList.length) {
  262. return (
  263. <div className='flex h-[296px] items-center justify-center text-[13px] text-text-tertiary'>
  264. {t('common.dataSource.notion.selector.noSearchResult')}
  265. </div>
  266. )
  267. }
  268. return (
  269. <List
  270. className='py-2'
  271. height={296}
  272. itemCount={currentDataList.length}
  273. itemSize={28}
  274. width='100%'
  275. itemKey={(index, data) => data.dataList[index].page_id}
  276. itemData={{
  277. dataList: currentDataList,
  278. handleToggle,
  279. checkedIds: value,
  280. disabledCheckedIds: disabledValue,
  281. handleCheck,
  282. canPreview,
  283. handlePreview,
  284. listMapWithChildrenAndDescendants,
  285. searchValue,
  286. previewPageId: currentPreviewPageId,
  287. pagesMap,
  288. }}
  289. >
  290. {Item}
  291. </List>
  292. )
  293. }
  294. export default PageSelector