This commit is contained in:
yyh
2026-01-21 16:16:52 +08:00
parent 8cf99a85cb
commit 4d60a742dc
3 changed files with 86 additions and 88 deletions

View File

@ -1,7 +1,7 @@
import type { DataSourceNotionPage, DataSourceNotionPageMap } from '@/models/common' import type { DataSourceNotionPage, DataSourceNotionPageMap } from '@/models/common'
import { RiArrowDownSLine, RiArrowRightSLine } from '@remixicon/react' import { RiArrowDownSLine, RiArrowRightSLine } from '@remixicon/react'
import { useVirtualizer } from '@tanstack/react-virtual' import { useVirtualizer } from '@tanstack/react-virtual'
import { memo, useMemo, useRef, useState } from 'react' import { memo, useCallback, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { cn } from '@/utils/classnames' import { cn } from '@/utils/classnames'
import Checkbox from '../../checkbox' import Checkbox from '../../checkbox'
@ -32,16 +32,15 @@ type NotionPageItem = {
} & DataSourceNotionPage } & DataSourceNotionPage
type ItemProps = { type ItemProps = {
index: number
virtualStart: number virtualStart: number
virtualSize: number virtualSize: number
current: NotionPageItem current: NotionPageItem
handleToggle: (index: number) => void onToggle: (pageId: string) => void
checkedIds: Set<string> checkedIds: Set<string>
disabledCheckedIds: Set<string> disabledCheckedIds: Set<string>
handleCheck: (index: number) => void onCheck: (pageId: string) => void
canPreview?: boolean canPreview?: boolean
handlePreview: (index: number) => void onPreview: (pageId: string) => void
listMapWithChildrenAndDescendants: NotionPageTreeMap listMapWithChildrenAndDescendants: NotionPageTreeMap
searchValue: string searchValue: string
previewPageId: string previewPageId: string
@ -86,16 +85,15 @@ const recursivePushInParentDescendants = (
} }
const ItemComponent = ({ const ItemComponent = ({
index,
virtualStart, virtualStart,
virtualSize, virtualSize,
current, current,
handleToggle, onToggle,
checkedIds, checkedIds,
disabledCheckedIds, disabledCheckedIds,
handleCheck, onCheck,
canPreview, canPreview,
handlePreview, onPreview,
listMapWithChildrenAndDescendants, listMapWithChildrenAndDescendants,
searchValue, searchValue,
previewPageId, previewPageId,
@ -114,7 +112,7 @@ const ItemComponent = ({
<div <div
className="mr-1 flex h-5 w-5 shrink-0 items-center justify-center rounded-md hover:bg-components-button-ghost-bg-hover" 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 }} style={{ marginLeft: current.depth * 8 }}
onClick={() => handleToggle(index)} onClick={() => onToggle(current.page_id)}
> >
{ {
current.expand current.expand
@ -151,9 +149,7 @@ const ItemComponent = ({
className="mr-2 shrink-0" className="mr-2 shrink-0"
checked={checkedIds.has(current.page_id)} checked={checkedIds.has(current.page_id)}
disabled={disabled} disabled={disabled}
onCheck={() => { onCheck={() => onCheck(current.page_id)}
handleCheck(index)
}}
/> />
{!searchValue && renderArrow()} {!searchValue && renderArrow()}
<NotionIcon <NotionIcon
@ -173,7 +169,7 @@ const ItemComponent = ({
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 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] 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" hover:border-components-button-secondary-border-hover hover:bg-components-button-secondary-bg-hover group-hover:flex"
onClick={() => handlePreview(index)} onClick={() => onPreview(current.page_id)}
> >
{t('dataSource.notion.selector.preview', { ns: 'common' })} {t('dataSource.notion.selector.preview', { ns: 'common' })}
</div> </div>
@ -221,14 +217,25 @@ const PageSelector = ({
}, {}) }, {})
}, [list, pagesMap]) }, [list, pagesMap])
// Pre-build children index for O(1) lookup instead of O(n) filter
const childrenByParent = useMemo(() => {
const map = new Map<string | null, DataSourceNotionPage[]>()
for (const item of list) {
const isRoot = item.parent_id === 'root' || !pagesMap[item.parent_id]
const parentKey = isRoot ? null : item.parent_id
const children = map.get(parentKey) || []
children.push(item)
map.set(parentKey, children)
}
return map
}, [list, pagesMap])
// Compute visible data list based on expanded state // Compute visible data list based on expanded state
const dataList = useMemo(() => { const dataList = useMemo(() => {
const result: NotionPageItem[] = [] const result: NotionPageItem[] = []
const buildVisibleList = (parentId: string | null, depth: number) => { const buildVisibleList = (parentId: string | null, depth: number) => {
const items = parentId === null const items = childrenByParent.get(parentId) || []
? list.filter(item => item.parent_id === 'root' || !pagesMap[item.parent_id])
: list.filter(item => item.parent_id === parentId)
for (const item of items) { for (const item of items) {
const isExpanded = expandedIds.has(item.page_id) const isExpanded = expandedIds.has(item.page_id)
@ -245,7 +252,7 @@ const PageSelector = ({
buildVisibleList(null, 0) buildVisibleList(null, 0)
return result return result
}, [list, pagesMap, expandedIds]) }, [childrenByParent, expandedIds])
const searchDataList = useMemo(() => list.filter((item) => { const searchDataList = useMemo(() => list.filter((item) => {
return item.page_name.includes(searchValue) return item.page_name.includes(searchValue)
@ -268,39 +275,37 @@ const PageSelector = ({
getItemKey: index => currentDataList[index].page_id, getItemKey: index => currentDataList[index].page_id,
}) })
const handleToggle = (index: number) => { // Stable callback - no dependencies on dataList
const current = dataList[index] const handleToggle = useCallback((pageId: string) => {
const pageId = current.page_id
const currentWithChildrenAndDescendants = listMapWithChildrenAndDescendants[pageId]
setExpandedIds((prev) => { setExpandedIds((prev) => {
const next = new Set(prev) const next = new Set(prev)
if (prev.has(pageId)) { if (prev.has(pageId)) {
// Collapse: remove current and all descendants // Collapse: remove current and all descendants
next.delete(pageId) next.delete(pageId)
for (const descendantId of currentWithChildrenAndDescendants.descendants) // Note: We access listMapWithChildrenAndDescendants via closure, but it's stable (memoized)
next.delete(descendantId) const descendants = listMapWithChildrenAndDescendants[pageId]?.descendants
if (descendants) {
for (const descendantId of descendants)
next.delete(descendantId)
}
} }
else { else {
// Expand: add current
next.add(pageId) next.add(pageId)
} }
return next return next
}) })
} }, [listMapWithChildrenAndDescendants])
const copyValue = new Set(value) // Stable callback - uses pageId parameter instead of index
const handleCheck = (index: number) => { const handleCheck = useCallback((pageId: string) => {
const current = currentDataList[index]
const pageId = current.page_id
const currentWithChildrenAndDescendants = listMapWithChildrenAndDescendants[pageId] const currentWithChildrenAndDescendants = listMapWithChildrenAndDescendants[pageId]
const copyValue = new Set(value)
if (copyValue.has(pageId)) { if (copyValue.has(pageId)) {
if (!searchValue) { if (!searchValue) {
for (const item of currentWithChildrenAndDescendants.descendants) for (const item of currentWithChildrenAndDescendants.descendants)
copyValue.delete(item) copyValue.delete(item)
} }
copyValue.delete(pageId) copyValue.delete(pageId)
} }
else { else {
@ -308,22 +313,18 @@ const PageSelector = ({
for (const item of currentWithChildrenAndDescendants.descendants) for (const item of currentWithChildrenAndDescendants.descendants)
copyValue.add(item) copyValue.add(item)
} }
copyValue.add(pageId) copyValue.add(pageId)
} }
onSelect(new Set(copyValue)) onSelect(new Set(copyValue))
} }, [listMapWithChildrenAndDescendants, onSelect, searchValue, value])
const handlePreview = (index: number) => {
const current = currentDataList[index]
const pageId = current.page_id
// Stable callback
const handlePreview = useCallback((pageId: string) => {
setLocalPreviewPageId(pageId) setLocalPreviewPageId(pageId)
if (onPreview) if (onPreview)
onPreview(pageId) onPreview(pageId)
} }, [onPreview])
if (!currentDataList.length) { if (!currentDataList.length) {
return ( return (
@ -351,16 +352,15 @@ const PageSelector = ({
return ( return (
<Item <Item
key={virtualRow.key} key={virtualRow.key}
index={virtualRow.index}
virtualStart={virtualRow.start} virtualStart={virtualRow.start}
virtualSize={virtualRow.size} virtualSize={virtualRow.size}
current={current} current={current}
handleToggle={handleToggle} onToggle={handleToggle}
checkedIds={value} checkedIds={value}
disabledCheckedIds={disabledValue} disabledCheckedIds={disabledValue}
handleCheck={handleCheck} onCheck={handleCheck}
canPreview={canPreview} canPreview={canPreview}
handlePreview={handlePreview} onPreview={handlePreview}
listMapWithChildrenAndDescendants={listMapWithChildrenAndDescendants} listMapWithChildrenAndDescendants={listMapWithChildrenAndDescendants}
searchValue={searchValue} searchValue={searchValue}
previewPageId={currentPreviewPageId} previewPageId={currentPreviewPageId}

View File

@ -67,14 +67,25 @@ const PageSelector = ({
}, {}) }, {})
}, [list, pagesMap]) }, [list, pagesMap])
// Pre-build children index for O(1) lookup instead of O(n) filter
const childrenByParent = useMemo(() => {
const map = new Map<string | null, DataSourceNotionPage[]>()
for (const item of list) {
const isRoot = item.parent_id === 'root' || !pagesMap[item.parent_id]
const parentKey = isRoot ? null : item.parent_id
const children = map.get(parentKey) || []
children.push(item)
map.set(parentKey, children)
}
return map
}, [list, pagesMap])
// Compute visible data list based on expanded state // Compute visible data list based on expanded state
const dataList = useMemo(() => { const dataList = useMemo(() => {
const result: NotionPageItem[] = [] const result: NotionPageItem[] = []
const buildVisibleList = (parentId: string | null, depth: number) => { const buildVisibleList = (parentId: string | null, depth: number) => {
const items = parentId === null const items = childrenByParent.get(parentId) || []
? list.filter(item => item.parent_id === 'root' || !pagesMap[item.parent_id])
: list.filter(item => item.parent_id === parentId)
for (const item of items) { for (const item of items) {
const isExpanded = expandedIds.has(item.page_id) const isExpanded = expandedIds.has(item.page_id)
@ -91,7 +102,7 @@ const PageSelector = ({
buildVisibleList(null, 0) buildVisibleList(null, 0)
return result return result
}, [list, pagesMap, expandedIds]) }, [childrenByParent, expandedIds])
const searchDataList = useMemo(() => list.filter((item) => { const searchDataList = useMemo(() => list.filter((item) => {
return item.page_name.includes(searchValue) return item.page_name.includes(searchValue)
@ -113,31 +124,29 @@ const PageSelector = ({
getItemKey: index => currentDataList[index].page_id, getItemKey: index => currentDataList[index].page_id,
}) })
const handleToggle = useCallback((index: number) => { // Stable callback - no dependencies on dataList
const current = dataList[index] const handleToggle = useCallback((pageId: string) => {
const pageId = current.page_id
const currentWithChildrenAndDescendants = listMapWithChildrenAndDescendants[pageId]
setExpandedIds((prev) => { setExpandedIds((prev) => {
const next = new Set(prev) const next = new Set(prev)
if (prev.has(pageId)) { if (prev.has(pageId)) {
// Collapse: remove current and all descendants // Collapse: remove current and all descendants
next.delete(pageId) next.delete(pageId)
for (const descendantId of currentWithChildrenAndDescendants.descendants) const descendants = listMapWithChildrenAndDescendants[pageId]?.descendants
next.delete(descendantId) if (descendants) {
for (const descendantId of descendants)
next.delete(descendantId)
}
} }
else { else {
// Expand: add current
next.add(pageId) next.add(pageId)
} }
return next return next
}) })
}, [dataList, listMapWithChildrenAndDescendants]) }, [listMapWithChildrenAndDescendants])
const handleCheck = useCallback((index: number) => { // Stable callback - uses pageId parameter instead of index
const handleCheck = useCallback((pageId: string) => {
const copyValue = new Set(checkedIds) const copyValue = new Set(checkedIds)
const current = currentDataList[index]
const pageId = current.page_id
const currentWithChildrenAndDescendants = listMapWithChildrenAndDescendants[pageId] const currentWithChildrenAndDescendants = listMapWithChildrenAndDescendants[pageId]
if (copyValue.has(pageId)) { if (copyValue.has(pageId)) {
@ -145,7 +154,6 @@ const PageSelector = ({
for (const item of currentWithChildrenAndDescendants.descendants) for (const item of currentWithChildrenAndDescendants.descendants)
copyValue.delete(item) copyValue.delete(item)
} }
copyValue.delete(pageId) copyValue.delete(pageId)
} }
else { else {
@ -164,17 +172,14 @@ const PageSelector = ({
} }
onSelect(new Set(copyValue)) onSelect(new Set(copyValue))
}, [currentDataList, isMultipleChoice, listMapWithChildrenAndDescendants, onSelect, searchValue, checkedIds]) }, [checkedIds, isMultipleChoice, listMapWithChildrenAndDescendants, onSelect, searchValue])
const handlePreview = useCallback((index: number) => {
const current = currentDataList[index]
const pageId = current.page_id
// Stable callback
const handlePreview = useCallback((pageId: string) => {
setCurrentPreviewPageId(pageId) setCurrentPreviewPageId(pageId)
if (onPreview) if (onPreview)
onPreview(pageId) onPreview(pageId)
}, [currentDataList, onPreview]) }, [onPreview])
if (!currentDataList.length) { if (!currentDataList.length) {
return ( return (
@ -202,16 +207,15 @@ const PageSelector = ({
return ( return (
<Item <Item
key={virtualRow.key} key={virtualRow.key}
index={virtualRow.index}
virtualStart={virtualRow.start} virtualStart={virtualRow.start}
virtualSize={virtualRow.size} virtualSize={virtualRow.size}
current={current} current={current}
handleToggle={handleToggle} onToggle={handleToggle}
checkedIds={checkedIds} checkedIds={checkedIds}
disabledCheckedIds={disabledValue} disabledCheckedIds={disabledValue}
handleCheck={handleCheck} onCheck={handleCheck}
canPreview={canPreview} canPreview={canPreview}
handlePreview={handlePreview} onPreview={handlePreview}
listMapWithChildrenAndDescendants={listMapWithChildrenAndDescendants} listMapWithChildrenAndDescendants={listMapWithChildrenAndDescendants}
searchValue={searchValue} searchValue={searchValue}
previewPageId={currentPreviewPageId} previewPageId={currentPreviewPageId}

View File

@ -22,16 +22,15 @@ type NotionPageItem = {
} & DataSourceNotionPage } & DataSourceNotionPage
type ItemProps = { type ItemProps = {
index: number
virtualStart: number virtualStart: number
virtualSize: number virtualSize: number
current: NotionPageItem current: NotionPageItem
handleToggle: (index: number) => void onToggle: (pageId: string) => void
checkedIds: Set<string> checkedIds: Set<string>
disabledCheckedIds: Set<string> disabledCheckedIds: Set<string>
handleCheck: (index: number) => void onCheck: (pageId: string) => void
canPreview?: boolean canPreview?: boolean
handlePreview: (index: number) => void onPreview: (pageId: string) => void
listMapWithChildrenAndDescendants: NotionPageTreeMap listMapWithChildrenAndDescendants: NotionPageTreeMap
searchValue: string searchValue: string
previewPageId: string previewPageId: string
@ -40,16 +39,15 @@ type ItemProps = {
} }
const Item = ({ const Item = ({
index,
virtualStart, virtualStart,
virtualSize, virtualSize,
current, current,
handleToggle, onToggle,
checkedIds, checkedIds,
disabledCheckedIds, disabledCheckedIds,
handleCheck, onCheck,
canPreview, canPreview,
handlePreview, onPreview,
listMapWithChildrenAndDescendants, listMapWithChildrenAndDescendants,
searchValue, searchValue,
previewPageId, previewPageId,
@ -69,7 +67,7 @@ const Item = ({
<div <div
className="mr-1 flex h-5 w-5 shrink-0 items-center justify-center rounded-md hover:bg-components-button-ghost-bg-hover" 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 }} style={{ marginLeft: current.depth * 8 }}
onClick={() => handleToggle(index)} onClick={() => onToggle(current.page_id)}
> >
{ {
current.expand current.expand
@ -108,9 +106,7 @@ const Item = ({
className="mr-2 shrink-0" className="mr-2 shrink-0"
checked={checkedIds.has(current.page_id)} checked={checkedIds.has(current.page_id)}
disabled={disabled} disabled={disabled}
onCheck={() => { onCheck={() => onCheck(current.page_id)}
handleCheck(index)
}}
/> />
) )
: ( : (
@ -118,9 +114,7 @@ const Item = ({
className="mr-2 shrink-0" className="mr-2 shrink-0"
isChecked={checkedIds.has(current.page_id)} isChecked={checkedIds.has(current.page_id)}
disabled={disabled} disabled={disabled}
onCheck={() => { onCheck={() => onCheck(current.page_id)}
handleCheck(index)
}}
/> />
)} )}
{!searchValue && renderArrow()} {!searchValue && renderArrow()}
@ -141,7 +135,7 @@ const Item = ({
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 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] 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" hover:border-components-button-secondary-border-hover hover:bg-components-button-secondary-bg-hover group-hover:flex"
onClick={() => handlePreview(index)} onClick={() => onPreview(current.page_id)}
> >
{t('dataSource.notion.selector.preview', { ns: 'common' })} {t('dataSource.notion.selector.preview', { ns: 'common' })}
</div> </div>