index.tsx 8.9 KB

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