mirror of
				https://github.com/langgenius/dify.git
				synced 2025-11-03 20:33:00 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			315 lines
		
	
	
		
			9.4 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			315 lines
		
	
	
		
			9.4 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
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>
 | 
						|
  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>
 | 
						|
  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.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={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,
 | 
						|
        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,
 | 
						|
        disabledCheckedIds: disabledValue,
 | 
						|
        handleCheck,
 | 
						|
        canPreview,
 | 
						|
        handlePreview,
 | 
						|
        listMapWithChildrenAndDescendants,
 | 
						|
        searchValue,
 | 
						|
        previewPageId: currentPreviewPageId,
 | 
						|
        pagesMap,
 | 
						|
      }}
 | 
						|
    >
 | 
						|
      {Item}
 | 
						|
    </List>
 | 
						|
  )
 | 
						|
}
 | 
						|
 | 
						|
export default PageSelector
 |