index.tsx 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314
  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 Checkbox from '../../checkbox'
  6. import NotionIcon from '../../notion-icon'
  7. import s from './index.module.css'
  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={cn(s.arrow, current.expand && s['arrow-expand'], 'shrink-0 mr-1 w-5 h-5 hover:bg-gray-200 rounded-md')}
  91. style={{ marginLeft: current.depth * 8 }}
  92. onClick={() => handleToggle(index)}
  93. />
  94. )
  95. }
  96. if (current.parent_id === 'root' || !pagesMap[current.parent_id]) {
  97. return (
  98. <div></div>
  99. )
  100. }
  101. return (
  102. <div className='shrink-0 mr-1 w-5 h-5' style={{ marginLeft: current.depth * 8 }} />
  103. )
  104. }
  105. return (
  106. <div
  107. 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'])}
  108. style={{ ...style, top: style.top as number + 8, left: 8, right: 8, width: 'calc(100% - 16px)' }}
  109. >
  110. <Checkbox
  111. className={cn(
  112. 'shrink-0 mr-2 group-hover:border-primary-600 group-hover:border-[2px]',
  113. disabled && 'group-hover:border-transparent',
  114. )}
  115. checked={checkedIds.has(current.page_id)}
  116. disabled={disabled}
  117. onCheck={() => {
  118. if (disabled)
  119. return
  120. handleCheck(index)
  121. }}
  122. />
  123. {!searchValue && renderArrow()}
  124. <NotionIcon
  125. className='shrink-0 mr-1'
  126. type='page'
  127. src={current.page_icon}
  128. />
  129. <div
  130. className='grow text-sm font-medium text-gray-700 truncate'
  131. title={current.page_name}
  132. >
  133. {current.page_name}
  134. </div>
  135. {
  136. canPreview && (
  137. <div
  138. 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'
  139. onClick={() => handlePreview(index)}>
  140. {t('common.dataSource.notion.selector.preview')}
  141. </div>
  142. )
  143. }
  144. {
  145. searchValue && (
  146. <div
  147. className='shrink-0 ml-1 max-w-[120px] text-xs text-gray-400 truncate'
  148. title={breadCrumbs.join(' / ')}
  149. >
  150. {breadCrumbs.join(' / ')}
  151. </div>
  152. )
  153. }
  154. </div>
  155. )
  156. }
  157. const Item = memo(ItemComponent, areEqual)
  158. const PageSelector = ({
  159. value,
  160. disabledValue,
  161. searchValue,
  162. pagesMap,
  163. list,
  164. onSelect,
  165. canPreview = true,
  166. previewPageId,
  167. onPreview,
  168. }: PageSelectorProps) => {
  169. const { t } = useTranslation()
  170. const [prevDataList, setPrevDataList] = useState(list)
  171. const [dataList, setDataList] = useState<NotionPageItem[]>([])
  172. const [localPreviewPageId, setLocalPreviewPageId] = useState('')
  173. if (prevDataList !== list) {
  174. setPrevDataList(list)
  175. setDataList(list.filter(item => item.parent_id === 'root' || !pagesMap[item.parent_id]).map((item) => {
  176. return {
  177. ...item,
  178. expand: false,
  179. depth: 0,
  180. }
  181. }))
  182. }
  183. const searchDataList = list.filter((item) => {
  184. return item.page_name.includes(searchValue)
  185. }).map((item) => {
  186. return {
  187. ...item,
  188. expand: false,
  189. depth: 0,
  190. }
  191. })
  192. const currentDataList = searchValue ? searchDataList : dataList
  193. const currentPreviewPageId = previewPageId === undefined ? localPreviewPageId : previewPageId
  194. const listMapWithChildrenAndDescendants = useMemo(() => {
  195. return list.reduce((prev: NotionPageTreeMap, next: DataSourceNotionPage) => {
  196. const pageId = next.page_id
  197. if (!prev[pageId])
  198. prev[pageId] = { ...next, children: new Set(), descendants: new Set(), depth: 0, ancestors: [] }
  199. recursivePushInParentDescendants(pagesMap, prev, prev[pageId], prev[pageId])
  200. return prev
  201. }, {})
  202. }, [list, pagesMap])
  203. const handleToggle = (index: number) => {
  204. const current = dataList[index]
  205. const pageId = current.page_id
  206. const currentWithChildrenAndDescendants = listMapWithChildrenAndDescendants[pageId]
  207. const descendantsIds = Array.from(currentWithChildrenAndDescendants.descendants)
  208. const childrenIds = Array.from(currentWithChildrenAndDescendants.children)
  209. let newDataList = []
  210. if (current.expand) {
  211. current.expand = false
  212. newDataList = [...dataList.filter(item => !descendantsIds.includes(item.page_id))]
  213. }
  214. else {
  215. current.expand = true
  216. newDataList = [
  217. ...dataList.slice(0, index + 1),
  218. ...childrenIds.map(item => ({
  219. ...pagesMap[item],
  220. expand: false,
  221. depth: listMapWithChildrenAndDescendants[item].depth,
  222. })),
  223. ...dataList.slice(index + 1)]
  224. }
  225. setDataList(newDataList)
  226. }
  227. const copyValue = new Set([...value])
  228. const handleCheck = (index: number) => {
  229. const current = currentDataList[index]
  230. const pageId = current.page_id
  231. const currentWithChildrenAndDescendants = listMapWithChildrenAndDescendants[pageId]
  232. if (copyValue.has(pageId)) {
  233. if (!searchValue) {
  234. for (const item of currentWithChildrenAndDescendants.descendants)
  235. copyValue.delete(item)
  236. }
  237. copyValue.delete(pageId)
  238. }
  239. else {
  240. if (!searchValue) {
  241. for (const item of currentWithChildrenAndDescendants.descendants)
  242. copyValue.add(item)
  243. }
  244. copyValue.add(pageId)
  245. }
  246. onSelect(new Set([...copyValue]))
  247. }
  248. const handlePreview = (index: number) => {
  249. const current = currentDataList[index]
  250. const pageId = current.page_id
  251. setLocalPreviewPageId(pageId)
  252. if (onPreview)
  253. onPreview(pageId)
  254. }
  255. if (!currentDataList.length) {
  256. return (
  257. <div className='flex items-center justify-center h-[296px] text-[13px] text-gray-500'>
  258. {t('common.dataSource.notion.selector.noSearchResult')}
  259. </div>
  260. )
  261. }
  262. return (
  263. <List
  264. className='py-2'
  265. height={296}
  266. itemCount={currentDataList.length}
  267. itemSize={28}
  268. width='100%'
  269. itemKey={(index, data) => data.dataList[index].page_id}
  270. itemData={{
  271. dataList: currentDataList,
  272. handleToggle,
  273. checkedIds: value,
  274. disabledCheckedIds: disabledValue,
  275. handleCheck,
  276. canPreview,
  277. handlePreview,
  278. listMapWithChildrenAndDescendants,
  279. searchValue,
  280. previewPageId: currentPreviewPageId,
  281. pagesMap,
  282. }}
  283. >
  284. {Item}
  285. </List>
  286. )
  287. }
  288. export default PageSelector