index.tsx 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302
  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 ItemComponent = ({ 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. }
  146. const Item = memo(ItemComponent, areEqual)
  147. const PageSelector = ({
  148. value,
  149. searchValue,
  150. pagesMap,
  151. list,
  152. onSelect,
  153. canPreview = true,
  154. previewPageId,
  155. onPreview,
  156. }: PageSelectorProps) => {
  157. const { t } = useTranslation()
  158. const [prevDataList, setPrevDataList] = useState(list)
  159. const [dataList, setDataList] = useState<NotionPageItem[]>([])
  160. const [localPreviewPageId, setLocalPreviewPageId] = useState('')
  161. if (prevDataList !== list) {
  162. setPrevDataList(list)
  163. setDataList(list.filter(item => item.parent_id === 'root' || !pagesMap[item.parent_id]).map((item) => {
  164. return {
  165. ...item,
  166. expand: false,
  167. deepth: 0,
  168. }
  169. }))
  170. }
  171. const searchDataList = list.filter((item) => {
  172. return item.page_name.includes(searchValue)
  173. }).map((item) => {
  174. return {
  175. ...item,
  176. expand: false,
  177. deepth: 0,
  178. }
  179. })
  180. const currentDataList = searchValue ? searchDataList : dataList
  181. const currentPreviewPageId = previewPageId === undefined ? localPreviewPageId : previewPageId
  182. const listMapWithChildrenAndDescendants = useMemo(() => {
  183. return list.reduce((prev: NotionPageTreeMap, next: DataSourceNotionPage) => {
  184. const pageId = next.page_id
  185. if (!prev[pageId])
  186. prev[pageId] = { ...next, children: new Set(), descendants: new Set(), deepth: 0, ancestors: [] }
  187. recursivePushInParentDescendants(pagesMap, prev, prev[pageId], prev[pageId])
  188. return prev
  189. }, {})
  190. }, [list, pagesMap])
  191. const handleToggle = (index: number) => {
  192. const current = dataList[index]
  193. const pageId = current.page_id
  194. const currentWithChildrenAndDescendants = listMapWithChildrenAndDescendants[pageId]
  195. const descendantsIds = Array.from(currentWithChildrenAndDescendants.descendants)
  196. const childrenIds = Array.from(currentWithChildrenAndDescendants.children)
  197. let newDataList = []
  198. if (current.expand) {
  199. current.expand = false
  200. newDataList = [...dataList.filter(item => !descendantsIds.includes(item.page_id))]
  201. }
  202. else {
  203. current.expand = true
  204. newDataList = [
  205. ...dataList.slice(0, index + 1),
  206. ...childrenIds.map(item => ({
  207. ...pagesMap[item],
  208. expand: false,
  209. deepth: listMapWithChildrenAndDescendants[item].deepth,
  210. })),
  211. ...dataList.slice(index + 1)]
  212. }
  213. setDataList(newDataList)
  214. }
  215. const copyValue = new Set([...value])
  216. const handleCheck = (index: number) => {
  217. const current = currentDataList[index]
  218. const pageId = current.page_id
  219. const currentWithChildrenAndDescendants = listMapWithChildrenAndDescendants[pageId]
  220. if (copyValue.has(pageId)) {
  221. if (!searchValue) {
  222. for (const item of currentWithChildrenAndDescendants.descendants)
  223. copyValue.delete(item)
  224. }
  225. copyValue.delete(pageId)
  226. }
  227. else {
  228. if (!searchValue) {
  229. for (const item of currentWithChildrenAndDescendants.descendants)
  230. copyValue.add(item)
  231. }
  232. copyValue.add(pageId)
  233. }
  234. onSelect(new Set([...copyValue]))
  235. }
  236. const handlePreview = (index: number) => {
  237. const current = currentDataList[index]
  238. const pageId = current.page_id
  239. setLocalPreviewPageId(pageId)
  240. if (onPreview)
  241. onPreview(pageId)
  242. }
  243. if (!currentDataList.length) {
  244. return (
  245. <div className='flex items-center justify-center h-[296px] text-[13px] text-gray-500'>
  246. {t('common.dataSource.notion.selector.noSearchResult')}
  247. </div>
  248. )
  249. }
  250. return (
  251. <List
  252. className='py-2'
  253. height={296}
  254. itemCount={currentDataList.length}
  255. itemSize={28}
  256. width='100%'
  257. itemKey={(index, data) => data.dataList[index].page_id}
  258. itemData={{
  259. dataList: currentDataList,
  260. handleToggle,
  261. checkedIds: value,
  262. handleCheck,
  263. canPreview,
  264. handlePreview,
  265. listMapWithChildrenAndDescendants,
  266. searchValue,
  267. previewPageId: currentPreviewPageId,
  268. pagesMap,
  269. }}
  270. >
  271. {Item}
  272. </List>
  273. )
  274. }
  275. export default PageSelector