|
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 Checkbox from '../../checkbox' |
|
import NotionIcon from '../../notion-icon' |
|
import s from './index.module.css' |
|
import cn from '@/utils/classnames' |
|
import type { DataSourceNotionPage, DataSourceNotionPageMap } from '@/models/common' |
|
|
|
type PageSelectorProps = { |
|
value: Set<string> |
|
disabledValue: 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> |
|
depth: number |
|
ancestors: string[] |
|
} & DataSourceNotionPage |
|
type NotionPageTreeMap = Record<string, NotionPageTreeItem> |
|
type NotionPageItem = { |
|
expand: boolean |
|
depth: 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, |
|
depth: 0, |
|
ancestors: [], |
|
} |
|
} |
|
else { |
|
listTreeMap[parentId].children.add(pageId) |
|
listTreeMap[parentId].descendants.add(pageId) |
|
listTreeMap[parentId].descendants.add(leafItem.page_id) |
|
} |
|
leafItem.depth++ |
|
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> |
|
disabledCheckedIds: 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, disabledCheckedIds, 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 disabled = disabledCheckedIds.has(current.page_id) |
|
|
|
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.depth * 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.depth * 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={cn( |
|
'shrink-0 mr-2 group-hover:border-primary-600 group-hover:border-[2px]', |
|
disabled && 'group-hover:border-transparent', |
|
)} |
|
checked={checkedIds.has(current.page_id)} |
|
disabled={disabled} |
|
onCheck={() => { |
|
if (disabled) |
|
return |
|
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, |
|
disabledValue, |
|
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, |
|
depth: 0, |
|
} |
|
})) |
|
} |
|
const searchDataList = list.filter((item) => { |
|
return item.page_name.includes(searchValue) |
|
}).map((item) => { |
|
return { |
|
...item, |
|
expand: false, |
|
depth: 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(), depth: 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, |
|
depth: listMapWithChildrenAndDescendants[item].depth, |
|
})), |
|
...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, |
|
disabledCheckedIds: disabledValue, |
|
handleCheck, |
|
canPreview, |
|
handlePreview, |
|
listMapWithChildrenAndDescendants, |
|
searchValue, |
|
previewPageId: currentPreviewPageId, |
|
pagesMap, |
|
}} |
|
> |
|
{Item} |
|
</List> |
|
) |
|
} |
|
|
|
export default PageSelector |
|
|