|
import { memo, useMemo, useState } from 'react'
|
|
import { useTranslation } from 'react-i18next'
|
|
import { FixedSizeList as List, areEqual } from 'react-window'
|
|
import type { ListChildComponentProps } from 'react-window'
|
|
import cn from 'classnames'
|
|
import Checkbox from '../../checkbox'
|
|
import NotionIcon from '../../notion-icon'
|
|
import s from './index.module.css'
|
|
import type { DataSourceNotionPage, DataSourceNotionPageMap } from '@/models/common'
|
|
|
|
type PageSelectorProps = {
|
|
value: Set<string>
|
|
searchValue: string
|
|
pagesMap: DataSourceNotionPageMap
|
|
list: DataSourceNotionPage[]
|
|
onSelect: (selectedPagesId: Set<string>) => void
|
|
canPreview?: boolean
|
|
previewPageId?: string
|
|
onPreview?: (selectedPageId: string) => void
|
|
}
|
|
type NotionPageTreeItem = {
|
|
children: Set<string>
|
|
descendants: Set<string>
|
|
deepth: number
|
|
ancestors: string[]
|
|
} & DataSourceNotionPage
|
|
type NotionPageTreeMap = Record<string, NotionPageTreeItem>
|
|
type NotionPageItem = {
|
|
expand: boolean
|
|
deepth: number
|
|
} & DataSourceNotionPage
|
|
|
|
const recursivePushInParentDescendants = (
|
|
pagesMap: DataSourceNotionPageMap,
|
|
listTreeMap: NotionPageTreeMap,
|
|
current: NotionPageTreeItem,
|
|
leafItem: NotionPageTreeItem,
|
|
) => {
|
|
const parentId = current.parent_id
|
|
const pageId = current.page_id
|
|
|
|
if (!parentId || !pageId)
|
|
return
|
|
|
|
if (parentId !== 'root' && pagesMap[parentId]) {
|
|
if (!listTreeMap[parentId]) {
|
|
const children = new Set([pageId])
|
|
const descendants = new Set([pageId, leafItem.page_id])
|
|
listTreeMap[parentId] = {
|
|
...pagesMap[parentId],
|
|
children,
|
|
descendants,
|
|
deepth: 0,
|
|
ancestors: [],
|
|
}
|
|
}
|
|
else {
|
|
listTreeMap[parentId].children.add(pageId)
|
|
listTreeMap[parentId].descendants.add(pageId)
|
|
listTreeMap[parentId].descendants.add(leafItem.page_id)
|
|
}
|
|
leafItem.deepth++
|
|
leafItem.ancestors.unshift(listTreeMap[parentId].page_name)
|
|
|
|
if (listTreeMap[parentId].parent_id !== 'root')
|
|
recursivePushInParentDescendants(pagesMap, listTreeMap, listTreeMap[parentId], leafItem)
|
|
}
|
|
}
|
|
|
|
const ItemComponent = ({ index, style, data }: ListChildComponentProps<{
|
|
dataList: NotionPageItem[]
|
|
handleToggle: (index: number) => void
|
|
checkedIds: Set<string>
|
|
handleCheck: (index: number) => void
|
|
canPreview?: boolean
|
|
handlePreview: (index: number) => void
|
|
listMapWithChildrenAndDescendants: NotionPageTreeMap
|
|
searchValue: string
|
|
previewPageId: string
|
|
pagesMap: DataSourceNotionPageMap
|
|
}>) => {
|
|
const { t } = useTranslation()
|
|
const { dataList, handleToggle, checkedIds, handleCheck, canPreview, handlePreview, listMapWithChildrenAndDescendants, searchValue, previewPageId, pagesMap } = data
|
|
const current = dataList[index]
|
|
const currentWithChildrenAndDescendants = listMapWithChildrenAndDescendants[current.page_id]
|
|
const hasChild = currentWithChildrenAndDescendants.descendants.size > 0
|
|
const ancestors = currentWithChildrenAndDescendants.ancestors
|
|
const breadCrumbs = ancestors.length ? [...ancestors, current.page_name] : [current.page_name]
|
|
|
|
const renderArrow = () => {
|
|
if (hasChild) {
|
|
return (
|
|
<div
|
|
className={cn(s.arrow, current.expand && s['arrow-expand'], 'shrink-0 mr-1 w-5 h-5 hover:bg-gray-200 rounded-md')}
|
|
style={{ marginLeft: current.deepth * 8 }}
|
|
onClick={() => handleToggle(index)}
|
|
/>
|
|
)
|
|
}
|
|
if (current.parent_id === 'root' || !pagesMap[current.parent_id]) {
|
|
return (
|
|
<div></div>
|
|
)
|
|
}
|
|
return (
|
|
<div className='shrink-0 mr-1 w-5 h-5' style={{ marginLeft: current.deepth * 8 }} />
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div
|
|
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'])}
|
|
style={{ ...style, top: style.top as number + 8, left: 8, right: 8, width: 'calc(100% - 16px)' }}
|
|
>
|
|
<Checkbox
|
|
className='shrink-0 mr-2 group-hover:border-primary-600 group-hover:border-[2px]'
|
|
checked={checkedIds.has(current.page_id)}
|
|
onCheck={() => handleCheck(index)}
|
|
/>
|
|
{!searchValue && renderArrow()}
|
|
<NotionIcon
|
|
className='shrink-0 mr-1'
|
|
type='page'
|
|
src={current.page_icon}
|
|
/>
|
|
<div
|
|
className='grow text-sm font-medium text-gray-700 truncate'
|
|
title={current.page_name}
|
|
>
|
|
{current.page_name}
|
|
</div>
|
|
{
|
|
canPreview && (
|
|
<div
|
|
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'
|
|
onClick={() => handlePreview(index)}>
|
|
{t('common.dataSource.notion.selector.preview')}
|
|
</div>
|
|
)
|
|
}
|
|
{
|
|
searchValue && (
|
|
<div
|
|
className='shrink-0 ml-1 max-w-[120px] text-xs text-gray-400 truncate'
|
|
title={breadCrumbs.join(' / ')}
|
|
>
|
|
{breadCrumbs.join(' / ')}
|
|
</div>
|
|
)
|
|
}
|
|
</div>
|
|
)
|
|
}
|
|
const Item = memo(ItemComponent, areEqual)
|
|
|
|
const PageSelector = ({
|
|
value,
|
|
searchValue,
|
|
pagesMap,
|
|
list,
|
|
onSelect,
|
|
canPreview = true,
|
|
previewPageId,
|
|
onPreview,
|
|
}: PageSelectorProps) => {
|
|
const { t } = useTranslation()
|
|
const [prevDataList, setPrevDataList] = useState(list)
|
|
const [dataList, setDataList] = useState<NotionPageItem[]>([])
|
|
const [localPreviewPageId, setLocalPreviewPageId] = useState('')
|
|
if (prevDataList !== list) {
|
|
setPrevDataList(list)
|
|
setDataList(list.filter(item => item.parent_id === 'root' || !pagesMap[item.parent_id]).map((item) => {
|
|
return {
|
|
...item,
|
|
expand: false,
|
|
deepth: 0,
|
|
}
|
|
}))
|
|
}
|
|
const searchDataList = list.filter((item) => {
|
|
return item.page_name.includes(searchValue)
|
|
}).map((item) => {
|
|
return {
|
|
...item,
|
|
expand: false,
|
|
deepth: 0,
|
|
}
|
|
})
|
|
const currentDataList = searchValue ? searchDataList : dataList
|
|
const currentPreviewPageId = previewPageId === undefined ? localPreviewPageId : previewPageId
|
|
|
|
const listMapWithChildrenAndDescendants = useMemo(() => {
|
|
return list.reduce((prev: NotionPageTreeMap, next: DataSourceNotionPage) => {
|
|
const pageId = next.page_id
|
|
if (!prev[pageId])
|
|
prev[pageId] = { ...next, children: new Set(), descendants: new Set(), deepth: 0, ancestors: [] }
|
|
|
|
recursivePushInParentDescendants(pagesMap, prev, prev[pageId], prev[pageId])
|
|
return prev
|
|
}, {})
|
|
}, [list, pagesMap])
|
|
|
|
const handleToggle = (index: number) => {
|
|
const current = dataList[index]
|
|
const pageId = current.page_id
|
|
const currentWithChildrenAndDescendants = listMapWithChildrenAndDescendants[pageId]
|
|
const descendantsIds = Array.from(currentWithChildrenAndDescendants.descendants)
|
|
const childrenIds = Array.from(currentWithChildrenAndDescendants.children)
|
|
let newDataList = []
|
|
|
|
if (current.expand) {
|
|
current.expand = false
|
|
|
|
newDataList = [...dataList.filter(item => !descendantsIds.includes(item.page_id))]
|
|
}
|
|
else {
|
|
current.expand = true
|
|
|
|
newDataList = [
|
|
...dataList.slice(0, index + 1),
|
|
...childrenIds.map(item => ({
|
|
...pagesMap[item],
|
|
expand: false,
|
|
deepth: listMapWithChildrenAndDescendants[item].deepth,
|
|
})),
|
|
...dataList.slice(index + 1)]
|
|
}
|
|
setDataList(newDataList)
|
|
}
|
|
|
|
const copyValue = new Set([...value])
|
|
const handleCheck = (index: number) => {
|
|
const current = currentDataList[index]
|
|
const pageId = current.page_id
|
|
const currentWithChildrenAndDescendants = listMapWithChildrenAndDescendants[pageId]
|
|
|
|
if (copyValue.has(pageId)) {
|
|
if (!searchValue) {
|
|
for (const item of currentWithChildrenAndDescendants.descendants)
|
|
copyValue.delete(item)
|
|
}
|
|
|
|
copyValue.delete(pageId)
|
|
}
|
|
else {
|
|
if (!searchValue) {
|
|
for (const item of currentWithChildrenAndDescendants.descendants)
|
|
copyValue.add(item)
|
|
}
|
|
|
|
copyValue.add(pageId)
|
|
}
|
|
|
|
onSelect(new Set([...copyValue]))
|
|
}
|
|
|
|
const handlePreview = (index: number) => {
|
|
const current = currentDataList[index]
|
|
const pageId = current.page_id
|
|
|
|
setLocalPreviewPageId(pageId)
|
|
|
|
if (onPreview)
|
|
onPreview(pageId)
|
|
}
|
|
|
|
if (!currentDataList.length) {
|
|
return (
|
|
<div className='flex items-center justify-center h-[296px] text-[13px] text-gray-500'>
|
|
{t('common.dataSource.notion.selector.noSearchResult')}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<List
|
|
className='py-2'
|
|
height={296}
|
|
itemCount={currentDataList.length}
|
|
itemSize={28}
|
|
width='100%'
|
|
itemKey={(index, data) => data.dataList[index].page_id}
|
|
itemData={{
|
|
dataList: currentDataList,
|
|
handleToggle,
|
|
checkedIds: value,
|
|
handleCheck,
|
|
canPreview,
|
|
handlePreview,
|
|
listMapWithChildrenAndDescendants,
|
|
searchValue,
|
|
previewPageId: currentPreviewPageId,
|
|
pagesMap,
|
|
}}
|
|
>
|
|
{Item}
|
|
</List>
|
|
)
|
|
}
|
|
|
|
export default PageSelector
|
|
|