add tanstack react query and migrate page selector

This commit is contained in:
yyh
2026-01-21 15:42:38 +08:00
parent d4f5a113ed
commit 52a874df98
4 changed files with 154 additions and 116 deletions

View File

@ -1,9 +1,8 @@
import type { ListChildComponentProps } from 'react-window'
import type { DataSourceNotionPage, DataSourceNotionPageMap } from '@/models/common'
import { RiArrowDownSLine, RiArrowRightSLine } from '@remixicon/react'
import { memo, useEffect, useMemo, useState } from 'react'
import { useVirtualizer } from '@tanstack/react-virtual'
import { memo, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { areEqual, FixedSizeList as List } from 'react-window'
import { cn } from '@/utils/classnames'
import Checkbox from '../../checkbox'
import NotionIcon from '../../notion-icon'
@ -32,6 +31,23 @@ type NotionPageItem = {
depth: number
} & DataSourceNotionPage
type ItemProps = {
index: number
virtualStart: number
virtualSize: number
current: 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 recursivePushInParentDescendants = (
pagesMap: DataSourceNotionPageMap,
listTreeMap: NotionPageTreeMap,
@ -69,34 +85,23 @@ const recursivePushInParentDescendants = (
}
}
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 ItemComponent = ({
index,
virtualStart,
virtualSize,
current,
handleToggle,
checkedIds,
disabledCheckedIds,
handleCheck,
canPreview,
handlePreview,
listMapWithChildrenAndDescendants,
searchValue,
previewPageId,
pagesMap,
}: ItemProps) => {
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
@ -132,7 +137,15 @@ const ItemComponent = ({ index, style, data }: ListChildComponentProps<{
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)' }}
style={{
position: 'absolute',
top: 0,
left: 8,
right: 8,
width: 'calc(100% - 16px)',
height: virtualSize,
transform: `translateY(${virtualStart + 8}px)`,
}}
>
<Checkbox
className="mr-2 shrink-0"
@ -179,7 +192,7 @@ const ItemComponent = ({ index, style, data }: ListChildComponentProps<{
</div>
)
}
const Item = memo(ItemComponent, areEqual)
const Item = memo(ItemComponent)
const PageSelector = ({
value,
@ -193,31 +206,10 @@ const PageSelector = ({
onPreview,
}: PageSelectorProps) => {
const { t } = useTranslation()
const [dataList, setDataList] = useState<NotionPageItem[]>([])
const parentRef = useRef<HTMLDivElement>(null)
const [expandedIds, setExpandedIds] = useState<Set<string>>(() => new Set())
const [localPreviewPageId, setLocalPreviewPageId] = useState('')
useEffect(() => {
setDataList(list.filter(item => item.parent_id === 'root' || !pagesMap[item.parent_id]).map((item) => {
return {
...item,
expand: false,
depth: 0,
}
}))
}, [list])
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
@ -229,33 +221,72 @@ const PageSelector = ({
}, {})
}, [list, pagesMap])
// Compute visible data list based on expanded state
const dataList = useMemo(() => {
const result: NotionPageItem[] = []
const buildVisibleList = (parentId: string | null, depth: number) => {
const items = parentId === null
? list.filter(item => item.parent_id === 'root' || !pagesMap[item.parent_id])
: list.filter(item => item.parent_id === parentId)
for (const item of items) {
const isExpanded = expandedIds.has(item.page_id)
result.push({
...item,
expand: isExpanded,
depth,
})
if (isExpanded) {
buildVisibleList(item.page_id, depth + 1)
}
}
}
buildVisibleList(null, 0)
return result
}, [list, pagesMap, expandedIds])
const searchDataList = useMemo(() => list.filter((item) => {
return item.page_name.includes(searchValue)
}).map((item) => {
return {
...item,
expand: false,
depth: 0,
}
}), [list, searchValue])
const currentDataList = searchValue ? searchDataList : dataList
const currentPreviewPageId = previewPageId === undefined ? localPreviewPageId : previewPageId
const virtualizer = useVirtualizer({
count: currentDataList.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 28,
overscan: 5,
getItemKey: index => currentDataList[index].page_id,
})
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)
setExpandedIds((prev) => {
const next = new Set(prev)
if (prev.has(pageId)) {
// Collapse: remove current and all descendants
next.delete(pageId)
for (const descendantId of currentWithChildrenAndDescendants.descendants)
next.delete(descendantId)
}
else {
// Expand: add current
next.add(pageId)
}
return next
})
}
const copyValue = new Set(value)
@ -303,29 +334,42 @@ const PageSelector = ({
}
return (
<List
<div
ref={parentRef}
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,
}}
style={{ height: 296, width: '100%', overflow: 'auto' }}
>
{Item}
</List>
<div
style={{
height: virtualizer.getTotalSize(),
width: '100%',
position: 'relative',
}}
>
{virtualizer.getVirtualItems().map((virtualRow) => {
const current = currentDataList[virtualRow.index]
return (
<Item
key={virtualRow.key}
index={virtualRow.index}
virtualStart={virtualRow.start}
virtualSize={virtualRow.size}
current={current}
handleToggle={handleToggle}
checkedIds={value}
disabledCheckedIds={disabledValue}
handleCheck={handleCheck}
canPreview={canPreview}
handlePreview={handlePreview}
listMapWithChildrenAndDescendants={listMapWithChildrenAndDescendants}
searchValue={searchValue}
previewPageId={currentPreviewPageId}
pagesMap={pagesMap}
/>
)
})}
</div>
</div>
)
}

View File

@ -1309,11 +1309,6 @@
"count": 2
}
},
"app/components/base/notion-page-selector/page-selector/index.tsx": {
"react-hooks-extra/no-direct-set-state-in-use-effect": {
"count": 1
}
},
"app/components/base/pagination/index.tsx": {
"unicorn/prefer-number-properties": {
"count": 1
@ -4252,11 +4247,6 @@
"count": 1
}
},
"middleware.ts": {
"node/prefer-global/buffer": {
"count": 1
}
},
"models/common.ts": {
"ts/no-explicit-any": {
"count": 3

View File

@ -80,6 +80,7 @@
"@tailwindcss/typography": "0.5.19",
"@tanstack/react-form": "1.23.7",
"@tanstack/react-query": "5.90.5",
"@tanstack/react-virtual": "3.13.18",
"abcjs": "6.5.2",
"ahooks": "3.9.5",
"class-variance-authority": "0.7.1",

19
web/pnpm-lock.yaml generated
View File

@ -135,6 +135,9 @@ importers:
'@tanstack/react-query':
specifier: 5.90.5
version: 5.90.5(react@19.2.3)
'@tanstack/react-virtual':
specifier: 3.13.18
version: 3.13.18(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
abcjs:
specifier: 6.5.2
version: 6.5.2
@ -3467,8 +3470,8 @@ packages:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
'@tanstack/react-virtual@3.13.13':
resolution: {integrity: sha512-4o6oPMDvQv+9gMi8rE6gWmsOjtUZUYIJHv7EB+GblyYdi8U6OqLl8rhHWIUZSL1dUU2dPwTdTgybCKf9EjIrQg==}
'@tanstack/react-virtual@3.13.18':
resolution: {integrity: sha512-dZkhyfahpvlaV0rIKnvQiVoWPyURppl6w4m9IwMDpuIjcJ1sD9YGWrt0wISvgU7ewACXx2Ct46WPgI6qAD4v6A==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
@ -3476,8 +3479,8 @@ packages:
'@tanstack/store@0.7.7':
resolution: {integrity: sha512-xa6pTan1bcaqYDS9BDpSiS63qa6EoDkPN9RsRaxHuDdVDNntzq3xNwR5YKTU/V3SkSyC9T4YVOPh2zRQN0nhIQ==}
'@tanstack/virtual-core@3.13.13':
resolution: {integrity: sha512-uQFoSdKKf5S8k51W5t7b2qpfkyIbdHMzAn+AMQvHPxKUPeo1SsGaA4JRISQT87jm28b7z8OEqPcg1IOZagQHcA==}
'@tanstack/virtual-core@3.13.18':
resolution: {integrity: sha512-Mx86Hqu1k39icq2Zusq+Ey2J6dDWTjDvEv43PJtRCoEYTLyfaPnxIQ6iy7YAOK0NV/qOEmZQ/uCufrppZxTgcg==}
'@testing-library/dom@10.4.1':
resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==}
@ -10092,7 +10095,7 @@ snapshots:
'@floating-ui/react': 0.26.28(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
'@react-aria/focus': 3.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
'@react-aria/interactions': 3.25.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
'@tanstack/react-virtual': 3.13.13(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
'@tanstack/react-virtual': 3.13.18(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
@ -11855,15 +11858,15 @@ snapshots:
react-dom: 19.2.3(react@19.2.3)
use-sync-external-store: 1.6.0(react@19.2.3)
'@tanstack/react-virtual@3.13.13(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
'@tanstack/react-virtual@3.13.18(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
dependencies:
'@tanstack/virtual-core': 3.13.13
'@tanstack/virtual-core': 3.13.18
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
'@tanstack/store@0.7.7': {}
'@tanstack/virtual-core@3.13.13': {}
'@tanstack/virtual-core@3.13.18': {}
'@testing-library/dom@10.4.1':
dependencies: