| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321 | 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 { RiArrowDownSLine, RiArrowRightSLine } from '@remixicon/react'import Checkbox from '../../checkbox'import NotionIcon from '../../notion-icon'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[]} & DataSourceNotionPagetype NotionPageTreeMap = Record<string, NotionPageTreeItem>type NotionPageItem = {  expand: boolean  depth: number} & DataSourceNotionPageconst 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='mr-1 flex h-5 w-5 shrink-0 items-center justify-center rounded-md hover:bg-components-button-ghost-bg-hover'          style={{ marginLeft: current.depth * 8 }}          onClick={() => handleToggle(index)}        >          {            current.expand              ? <RiArrowDownSLine className='h-4 w-4 text-text-tertiary' />              : <RiArrowRightSLine className='h-4 w-4 text-text-tertiary' />          }        </div>      )    }    if (current.parent_id === 'root' || !pagesMap[current.parent_id]) {      return (        <div></div>      )    }    return (      <div className='mr-1 h-5 w-5 shrink-0' style={{ marginLeft: current.depth * 8 }} />    )  }  return (    <div      className={cn('group flex cursor-pointer items-center rounded-md pl-2 pr-[2px] hover:bg-state-base-hover',        previewPageId === current.page_id && 'bg-state-base-hover')}      style={{ ...style, top: style.top as number + 8, left: 8, right: 8, width: 'calc(100% - 16px)' }}    >      <Checkbox        className='mr-2 shrink-0'        checked={checkedIds.has(current.page_id)}        disabled={disabled}        onCheck={() => {          if (disabled)            return          handleCheck(index)        }}      />      {!searchValue && renderArrow()}      <NotionIcon        className='mr-1 shrink-0'        type='page'        src={current.page_icon}      />      <div        className='grow truncate text-[13px] font-medium leading-4 text-text-secondary'        title={current.page_name}      >        {current.page_name}      </div>      {        canPreview && (          <div            className='ml-1 hidden h-6 shrink-0 cursor-pointer items-center rounded-md border-[0.5px] border-components-button-secondary-border bg-components-button-secondary-bg px-2 text-xs            font-medium leading-4 text-components-button-secondary-text shadow-xs shadow-shadow-shadow-3 backdrop-blur-[10px]            hover:border-components-button-secondary-border-hover hover:bg-components-button-secondary-bg-hover group-hover:flex'            onClick={() => handlePreview(index)}>            {t('common.dataSource.notion.selector.preview')}          </div>        )      }      {        searchValue && (          <div            className='ml-1 max-w-[120px] shrink-0 truncate text-xs text-text-quaternary'            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 h-[296px] items-center justify-center text-[13px] text-text-tertiary'>        {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
 |