refactor(web): migrate notion page selectors to tanstack virtual (#34508)

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
yyh
2026-04-03 15:03:12 +08:00
committed by GitHub
parent a9cf8f6c5d
commit c94951b2f8
25 changed files with 639 additions and 900 deletions

View File

@@ -0,0 +1,36 @@
import { vi } from 'vitest'
const mockVirtualizer = ({
count,
estimateSize,
}: {
count: number
estimateSize?: (index: number) => number
}) => {
const getSize = (index: number) => estimateSize?.(index) ?? 0
return {
getTotalSize: () => Array.from({ length: count }).reduce<number>((total, _, index) => total + getSize(index), 0),
getVirtualItems: () => {
let start = 0
return Array.from({ length: count }).map((_, index) => {
const size = getSize(index)
const virtualItem = {
end: start + size,
index,
key: index,
size,
start,
}
start += size
return virtualItem
})
},
measureElement: vi.fn(),
scrollToIndex: vi.fn(),
}
}
export { mockVirtualizer as useVirtualizer }

View File

@@ -15,7 +15,6 @@ import { highlightCode } from './shiki-highlight'
const Flowchart = dynamic(() => import('@/app/components/base/mermaid'), { ssr: false })
// Available language https://github.com/react-syntax-highlighter/react-syntax-highlighter/blob/master/AVAILABLE_LANGUAGES_HLJS.MD
const capitalizationLanguageNameMap: Record<string, string> = {
sql: 'SQL',
javascript: 'JavaScript',

View File

@@ -9,6 +9,8 @@ import { useModalContextSelector } from '@/context/modal-context'
import { useInvalidPreImportNotionPages, usePreImportNotionPages } from '@/service/knowledge/use-import'
import NotionPageSelector from '../base'
vi.mock('@tanstack/react-virtual')
vi.mock('@/service/knowledge/use-import', () => ({
usePreImportNotionPages: vi.fn(),
useInvalidPreImportNotionPages: vi.fn(),
@@ -183,7 +185,7 @@ describe('NotionPageSelector Base', () => {
const user = userEvent.setup()
render(<NotionPageSelector credentialList={mockCredentialList} onSelect={vi.fn()} />)
await user.click(screen.getByRole('button', { name: 'Configure Notion' }))
await user.click(screen.getByRole('button', { name: 'common.dataSource.notion.selector.configure' }))
expect(mockSetShowAccountSettingModal).toHaveBeenCalledWith({ payload: ACCOUNT_SETTING_TAB.DATA_SOURCE })
})

View File

@@ -2,6 +2,7 @@ import type { DataSourceCredential } from '../../header/account-setting/data-sou
import type { NotionCredential } from './credential-selector'
import type { DataSourceNotionPageMap, DataSourceNotionWorkspace, NotionPage } from '@/models/common'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
import { useModalContextSelector } from '@/context/modal-context'
import { useInvalidPreImportNotionPages, usePreImportNotionPages } from '@/service/knowledge/use-import'
@@ -33,6 +34,7 @@ const NotionPageSelector = ({
credentialList,
onSelectCredential,
}: NotionPageSelectorProps) => {
const { t } = useTranslation()
const [searchValue, setSearchValue] = useState('')
const setShowAccountSettingModal = useModalContextSelector(s => s.setShowAccountSettingModal)
@@ -48,27 +50,34 @@ const NotionPageSelector = ({
}
})
}, [credentialList])
const [currentCredential, setCurrentCredential] = useState(notionCredentials[0])
const [selectedCredentialId, setSelectedCredentialId] = useState(() => notionCredentials[0]?.credentialId ?? '')
const currentCredential = useMemo(() => {
return notionCredentials.find(item => item.credentialId === selectedCredentialId) ?? notionCredentials[0] ?? null
}, [notionCredentials, selectedCredentialId])
const currentCredentialId = currentCredential?.credentialId ?? ''
useEffect(() => {
const credential = notionCredentials.find(item => item.credentialId === currentCredential?.credentialId)
if (!credential) {
const firstCredential = notionCredentials[0]
invalidPreImportNotionPages({ datasetId, credentialId: firstCredential.credentialId })
setCurrentCredential(notionCredentials[0])
onSelect([]) // Clear selected pages when changing credential
onSelectCredential?.(firstCredential.credentialId)
onSelectCredential?.(currentCredentialId)
}, [currentCredentialId, onSelectCredential])
useEffect(() => {
if (!notionCredentials.length) {
onSelect([])
return
}
else {
onSelectCredential?.(credential?.credentialId || '')
}
}, [notionCredentials])
if (!selectedCredentialId || selectedCredentialId === currentCredentialId)
return
invalidPreImportNotionPages({ datasetId, credentialId: currentCredentialId })
onSelect([])
}, [currentCredentialId, datasetId, invalidPreImportNotionPages, notionCredentials.length, onSelect, selectedCredentialId])
const {
data: notionsPages,
isFetching: isFetchingNotionPages,
isError: isFetchingNotionPagesError,
} = usePreImportNotionPages({ datasetId, credentialId: currentCredential.credentialId || '' })
} = usePreImportNotionPages({ datasetId, credentialId: currentCredentialId })
const pagesMapAndSelectedPagesId: [DataSourceNotionPageMap, Set<string>, Set<string>] = useMemo(() => {
const selectedPagesId = new Set<string>()
@@ -94,28 +103,24 @@ const NotionPageSelector = ({
const defaultSelectedPagesId = useMemo(() => {
return [...Array.from(pagesMapAndSelectedPagesId[1]), ...(value || [])]
}, [pagesMapAndSelectedPagesId, value])
const [selectedPagesId, setSelectedPagesId] = useState<Set<string>>(() => new Set(defaultSelectedPagesId))
useEffect(() => {
setSelectedPagesId(new Set(defaultSelectedPagesId))
}, [defaultSelectedPagesId])
const selectedPagesId = useMemo(() => new Set(defaultSelectedPagesId), [defaultSelectedPagesId])
const handleSearchValueChange = useCallback((value: string) => {
setSearchValue(value)
}, [])
const handleSelectCredential = useCallback((credentialId: string) => {
const credential = notionCredentials.find(item => item.credentialId === credentialId)!
invalidPreImportNotionPages({ datasetId, credentialId: credential.credentialId })
setCurrentCredential(credential)
if (credentialId === currentCredentialId)
return
invalidPreImportNotionPages({ datasetId, credentialId })
setSelectedCredentialId(credentialId)
onSelect([]) // Clear selected pages when changing credential
onSelectCredential?.(credential.credentialId)
}, [datasetId, invalidPreImportNotionPages, notionCredentials, onSelect, onSelectCredential])
}, [currentCredentialId, datasetId, invalidPreImportNotionPages, onSelect])
const handleSelectPages = useCallback((newSelectedPagesId: Set<string>) => {
const selectedPages = Array.from(newSelectedPagesId).map(pageId => pagesMapAndSelectedPagesId[0][pageId])
setSelectedPagesId(new Set(Array.from(newSelectedPagesId)))
onSelect(selectedPages)
}, [pagesMapAndSelectedPagesId, onSelect])
@@ -140,16 +145,16 @@ const NotionPageSelector = ({
<div className="flex flex-col gap-y-2" data-testid="notion-page-selector-base">
<Header
onClickConfiguration={handleConfigureNotion}
title="Choose notion pages"
buttonText="Configure Notion"
docTitle="Notion docs"
title={t('dataSource.notion.selector.headerTitle', { ns: 'common' })}
buttonText={t('dataSource.notion.selector.configure', { ns: 'common' })}
docTitle={t('dataSource.notion.selector.docs', { ns: 'common' })}
docLink="https://www.notion.so/docs"
/>
<div className="rounded-xl border border-components-panel-border bg-background-default-subtle">
<div className="flex h-12 items-center gap-x-2 rounded-t-xl border-b border-b-divider-regular bg-components-panel-bg p-2">
<div className="flex grow items-center gap-x-1">
<WorkspaceSelector
value={currentCredential.credentialId}
value={currentCredentialId}
items={notionCredentials}
onSelect={handleSelectCredential}
/>
@@ -168,6 +173,7 @@ const NotionPageSelector = ({
)
: (
<PageSelector
key={currentCredentialId || 'default'}
value={selectedPagesId}
disabledValue={pagesMapAndSelectedPagesId[2]}
searchValue={searchValue}

View File

@@ -3,6 +3,8 @@ import { render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import PageSelector from '../index'
vi.mock('@tanstack/react-virtual')
const buildPage = (overrides: Partial<DataSourceNotionPage>): DataSourceNotionPage => ({
page_id: 'page-id',
page_name: 'Page name',

View File

@@ -1,11 +1,7 @@
import type { ListChildComponentProps } from 'react-window'
import type { DataSourceNotionPage, DataSourceNotionPageMap } from '@/models/common'
import { memo, useEffect, useMemo, 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'
import { usePageSelectorModel } from './use-page-selector-model'
import VirtualPageList from './virtual-page-list'
type PageSelectorProps = {
value: Set<string>
@@ -17,173 +13,7 @@ type PageSelectorProps = {
canPreview?: boolean
previewPageId?: string
onPreview?: (selectedPageId: string) => void
isMultipleChoice?: boolean
}
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="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)}
data-testid={`notion-page-toggle-${current.page_id}`}
>
{
current.expand
? <div className="i-ri-arrow-down-s-line h-4 w-4 text-text-tertiary" />
: <div className="i-ri-arrow-right-s-line 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)' }}
data-testid={`notion-page-row-${current.page_id}`}
>
<Checkbox
className="mr-2 shrink-0"
checked={checkedIds.has(current.page_id)}
disabled={disabled}
onCheck={() => {
handleCheck(index)
}}
id={`notion-page-checkbox-${current.page_id}`}
/>
{!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}
data-testid={`notion-page-name-${current.page_id}`}
>
{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)}
data-testid={`notion-page-preview-${current.page_id}`}
>
{t('dataSource.notion.selector.preview', { ns: 'common' })}
</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,
@@ -197,108 +27,25 @@ const PageSelector = ({
onPreview,
}: PageSelectorProps) => {
const { t } = useTranslation()
const [dataList, setDataList] = useState<NotionPageItem[]>([])
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 {
currentPreviewPageId,
effectiveSearchValue,
rows,
handlePreview,
handleSelect,
handleToggle,
} = usePageSelectorModel({
checkedIds: value,
list,
onPreview,
onSelect,
pagesMap,
previewPageId,
searchValue,
selectionMode: 'multiple',
})
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) {
if (!rows.length) {
return (
<div className="flex h-[296px] items-center justify-center text-[13px] text-text-tertiary">
{t('dataSource.notion.selector.noSearchResult', { ns: 'common' })}
@@ -307,29 +54,18 @@ const PageSelector = ({
}
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>
<VirtualPageList
checkedIds={value}
disabledValue={disabledValue}
onPreview={handlePreview}
onSelect={handleSelect}
onToggle={handleToggle}
previewPageId={currentPreviewPageId}
rows={rows}
searchValue={effectiveSearchValue}
selectionMode="multiple"
showPreview={canPreview}
/>
)
}

View File

@@ -0,0 +1,116 @@
import type { CSSProperties } from 'react'
import type { NotionPageRow as NotionPageRowData, NotionPageSelectionMode } from './types'
import { RiArrowDownSLine, RiArrowRightSLine } from '@remixicon/react'
import { memo } from 'react'
import { useTranslation } from 'react-i18next'
import Checkbox from '@/app/components/base/checkbox'
import NotionIcon from '@/app/components/base/notion-icon'
import Radio from '@/app/components/base/radio/ui'
import { cn } from '@/utils/classnames'
type NotionPageRowProps = {
checked: boolean
disabled: boolean
isPreviewed: boolean
onPreview: (pageId: string) => void
onSelect: (pageId: string) => void
onToggle: (pageId: string) => void
row: NotionPageRowData
searchValue: string
selectionMode: NotionPageSelectionMode
showPreview: boolean
style: CSSProperties
}
const NotionPageRow = ({
checked,
disabled,
isPreviewed,
onPreview,
onSelect,
onToggle,
row,
searchValue,
selectionMode,
showPreview,
style,
}: NotionPageRowProps) => {
const { t } = useTranslation()
const pageId = row.page.page_id
const breadcrumbs = row.ancestors.length ? [...row.ancestors, row.page.page_name] : [row.page.page_name]
return (
<div
className={cn('group flex cursor-pointer items-center rounded-md pr-[2px] pl-2 hover:bg-state-base-hover', isPreviewed && 'bg-state-base-hover')}
style={style}
data-testid={`notion-page-row-${pageId}`}
>
{selectionMode === 'multiple'
? (
<Checkbox
className="mr-2 shrink-0"
checked={checked}
disabled={disabled}
onCheck={() => onSelect(pageId)}
id={`notion-page-checkbox-${pageId}`}
/>
)
: (
<Radio
className="mr-2 shrink-0"
isChecked={checked}
disabled={disabled}
onCheck={() => onSelect(pageId)}
/>
)}
{!searchValue && row.hasChild && (
<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: row.depth * 8 }}
onClick={() => onToggle(pageId)}
data-testid={`notion-page-toggle-${pageId}`}
>
{row.expand
? <RiArrowDownSLine className="h-4 w-4 text-text-tertiary" />
: <RiArrowRightSLine className="h-4 w-4 text-text-tertiary" />}
</div>
)}
{!searchValue && !row.hasChild && row.parentExists && (
<div className="mr-1 h-5 w-5 shrink-0" style={{ marginLeft: row.depth * 8 }} />
)}
<NotionIcon
className="mr-1 shrink-0"
type="page"
src={row.page.page_icon}
/>
<div
className="grow truncate text-[13px] leading-4 font-medium text-text-secondary"
title={row.page.page_name}
data-testid={`notion-page-name-${pageId}`}
>
{row.page.page_name}
</div>
{showPreview && (
<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
leading-4 font-medium text-components-button-secondary-text shadow-xs shadow-shadow-shadow-3 backdrop-blur-[10px]
group-hover:flex hover:border-components-button-secondary-border-hover hover:bg-components-button-secondary-bg-hover"
onClick={() => onPreview(pageId)}
data-testid={`notion-page-preview-${pageId}`}
>
{t('dataSource.notion.selector.preview', { ns: 'common' })}
</div>
)}
{searchValue && (
<div
className="ml-1 max-w-[120px] shrink-0 truncate text-xs text-text-quaternary"
title={breadcrumbs.join(' / ')}
>
{breadcrumbs.join(' / ')}
</div>
)}
</div>
)
}
export default memo(NotionPageRow)

View File

@@ -0,0 +1,21 @@
import type { DataSourceNotionPage } from '@/models/common'
export type NotionPageSelectionMode = 'multiple' | 'single'
export type NotionPageTreeItem = {
children: Set<string>
descendants: Set<string>
depth: number
ancestors: string[]
} & DataSourceNotionPage
export type NotionPageTreeMap = Record<string, NotionPageTreeItem>
export type NotionPageRow = {
page: DataSourceNotionPage
parentExists: boolean
depth: number
expand: boolean
hasChild: boolean
ancestors: string[]
}

View File

@@ -0,0 +1,88 @@
import type { NotionPageSelectionMode } from './types'
import type { DataSourceNotionPage, DataSourceNotionPageMap } from '@/models/common'
import { startTransition, useCallback, useDeferredValue, useMemo, useState } from 'react'
import { buildNotionPageTree, getNextSelectedPageIds, getRootPageIds, getVisiblePageRows } from './utils'
type UsePageSelectorModelProps = {
checkedIds: Set<string>
searchValue: string
pagesMap: DataSourceNotionPageMap
list: DataSourceNotionPage[]
onSelect: (selectedPagesId: Set<string>) => void
previewPageId?: string
onPreview?: (selectedPageId: string) => void
selectionMode: NotionPageSelectionMode
}
export const usePageSelectorModel = ({
checkedIds,
searchValue,
pagesMap,
list,
onSelect,
previewPageId,
onPreview,
selectionMode,
}: UsePageSelectorModelProps) => {
const deferredSearchValue = useDeferredValue(searchValue)
const [expandedIds, setExpandedIds] = useState<Set<string>>(() => new Set())
const [localPreviewPageId, setLocalPreviewPageId] = useState('')
const treeMap = useMemo(() => buildNotionPageTree(list, pagesMap), [list, pagesMap])
const rootPageIds = useMemo(() => getRootPageIds(list, pagesMap), [list, pagesMap])
const rows = useMemo(() => {
return getVisiblePageRows({
list,
pagesMap,
searchValue: deferredSearchValue,
treeMap,
rootPageIds,
expandedIds,
})
}, [deferredSearchValue, expandedIds, list, pagesMap, rootPageIds, treeMap])
const currentPreviewPageId = previewPageId ?? localPreviewPageId
const handleToggle = useCallback((pageId: string) => {
startTransition(() => {
setExpandedIds((currentExpandedIds) => {
const nextExpandedIds = new Set(currentExpandedIds)
if (nextExpandedIds.has(pageId)) {
nextExpandedIds.delete(pageId)
treeMap[pageId]?.descendants.forEach(descendantId => nextExpandedIds.delete(descendantId))
}
else {
nextExpandedIds.add(pageId)
}
return nextExpandedIds
})
})
}, [treeMap])
const handleSelect = useCallback((pageId: string) => {
onSelect(getNextSelectedPageIds({
checkedIds,
pageId,
searchValue: deferredSearchValue,
selectionMode,
treeMap,
}))
}, [checkedIds, deferredSearchValue, onSelect, selectionMode, treeMap])
const handlePreview = useCallback((pageId: string) => {
setLocalPreviewPageId(pageId)
onPreview?.(pageId)
}, [onPreview])
return {
currentPreviewPageId,
effectiveSearchValue: deferredSearchValue,
rows,
handlePreview,
handleSelect,
handleToggle,
}
}

View File

@@ -0,0 +1,163 @@
import type { NotionPageRow, NotionPageSelectionMode, NotionPageTreeItem, NotionPageTreeMap } from './types'
import type { DataSourceNotionPage, DataSourceNotionPageMap } from '@/models/common'
export 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)
}
}
export const buildNotionPageTree = (
list: DataSourceNotionPage[],
pagesMap: DataSourceNotionPageMap,
): NotionPageTreeMap => {
return list.reduce((prev: NotionPageTreeMap, next) => {
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
}, {})
}
export const getRootPageIds = (
list: DataSourceNotionPage[],
pagesMap: DataSourceNotionPageMap,
) => {
return list
.filter(item => item.parent_id === 'root' || !pagesMap[item.parent_id])
.map(item => item.page_id)
}
export const getVisiblePageRows = ({
list,
pagesMap,
searchValue,
treeMap,
rootPageIds,
expandedIds,
}: {
list: DataSourceNotionPage[]
pagesMap: DataSourceNotionPageMap
searchValue: string
treeMap: NotionPageTreeMap
rootPageIds: string[]
expandedIds: Set<string>
}): NotionPageRow[] => {
if (searchValue) {
return list
.filter(item => item.page_name.includes(searchValue))
.map(item => ({
page: item,
parentExists: item.parent_id !== 'root' && Boolean(pagesMap[item.parent_id]),
depth: treeMap[item.page_id]?.depth ?? 0,
expand: false,
hasChild: (treeMap[item.page_id]?.children.size ?? 0) > 0,
ancestors: treeMap[item.page_id]?.ancestors ?? [],
}))
}
const rows: NotionPageRow[] = []
const visit = (pageId: string) => {
const current = treeMap[pageId]
if (!current)
return
const expand = expandedIds.has(pageId)
rows.push({
page: current,
parentExists: current.parent_id !== 'root' && Boolean(pagesMap[current.parent_id]),
depth: current.depth,
expand,
hasChild: current.children.size > 0,
ancestors: current.ancestors,
})
if (!expand)
return
current.children.forEach(visit)
}
rootPageIds.forEach(visit)
return rows
}
export const getNextSelectedPageIds = ({
checkedIds,
pageId,
searchValue,
selectionMode,
treeMap,
}: {
checkedIds: Set<string>
pageId: string
searchValue: string
selectionMode: NotionPageSelectionMode
treeMap: NotionPageTreeMap
}) => {
const nextCheckedIds = new Set(checkedIds)
const descendants = treeMap[pageId]?.descendants ?? new Set<string>()
if (selectionMode === 'single') {
if (nextCheckedIds.has(pageId)) {
nextCheckedIds.delete(pageId)
}
else {
nextCheckedIds.clear()
nextCheckedIds.add(pageId)
}
return nextCheckedIds
}
if (nextCheckedIds.has(pageId)) {
if (!searchValue)
descendants.forEach(item => nextCheckedIds.delete(item))
nextCheckedIds.delete(pageId)
return nextCheckedIds
}
if (!searchValue)
descendants.forEach(item => nextCheckedIds.add(item))
nextCheckedIds.add(pageId)
return nextCheckedIds
}

View File

@@ -0,0 +1,93 @@
'use client'
import type { NotionPageRow, NotionPageSelectionMode } from './types'
import { useVirtualizer } from '@tanstack/react-virtual'
import { useRef } from 'react'
import PageRow from './page-row'
type VirtualPageListProps = {
checkedIds: Set<string>
disabledValue: Set<string>
onPreview: (pageId: string) => void
onSelect: (pageId: string) => void
onToggle: (pageId: string) => void
previewPageId: string
rows: NotionPageRow[]
searchValue: string
selectionMode: NotionPageSelectionMode
showPreview: boolean
}
const rowHeight = 28
const VirtualPageList = ({
checkedIds,
disabledValue,
onPreview,
onSelect,
onToggle,
previewPageId,
rows,
searchValue,
selectionMode,
showPreview,
}: VirtualPageListProps) => {
const scrollRef = useRef<HTMLDivElement>(null)
const rowVirtualizer = useVirtualizer({
count: rows.length,
estimateSize: () => rowHeight,
getScrollElement: () => scrollRef.current,
overscan: 6,
paddingEnd: 8,
paddingStart: 8,
})
const virtualRows = rowVirtualizer.getVirtualItems()
return (
<div
ref={scrollRef}
className="h-[296px] overflow-auto"
data-testid="virtual-list"
>
<div
style={{
height: `${rowVirtualizer.getTotalSize()}px`,
position: 'relative',
}}
>
{virtualRows.map((virtualRow) => {
const row = rows[virtualRow.index]
const pageId = row.page.page_id
return (
<PageRow
key={pageId}
checked={checkedIds.has(pageId)}
disabled={disabledValue.has(pageId)}
isPreviewed={previewPageId === pageId}
onPreview={onPreview}
onSelect={onSelect}
onToggle={onToggle}
row={row}
searchValue={searchValue}
selectionMode={selectionMode}
showPreview={showPreview}
style={{
height: `${virtualRow.size}px`,
left: 8,
position: 'absolute',
top: 0,
transform: `translateY(${virtualRow.start}px)`,
width: 'calc(100% - 16px)',
}}
/>
)
})}
</div>
</div>
)
}
export default VirtualPageList

View File

@@ -106,7 +106,7 @@ const OnlineDocuments = ({
if (!currentCredentialId)
return
getOnlineDocuments()
}, [currentCredentialId])
}, [currentCredentialId, getOnlineDocuments])
const handleSearchValueChange = useCallback((value: string) => {
const { setSearchValue } = dataSourceStore.getState()
@@ -156,6 +156,7 @@ const OnlineDocuments = ({
{documentsData?.length
? (
<PageSelector
key={`${currentCredentialId}:${supportBatchUpload ? 'multiple' : 'single'}`}
checkedIds={selectedPagesId}
disabledValue={new Set()}
searchValue={searchValue}
@@ -165,7 +166,6 @@ const OnlineDocuments = ({
canPreview={!isInPipeline}
onPreview={handlePreviewPage}
isMultipleChoice={supportBatchUpload}
currentCredentialId={currentCredentialId}
/>
)
: (

View File

@@ -1,26 +1,11 @@
import type { NotionPageTreeItem, NotionPageTreeMap } from '../index'
import type { NotionPageTreeItem, NotionPageTreeMap } from '@/app/components/base/notion-page-selector/page-selector/types'
import type { DataSourceNotionPage, DataSourceNotionPageMap } from '@/models/common'
import { fireEvent, render, screen } from '@testing-library/react'
import * as React from 'react'
import { recursivePushInParentDescendants } from '@/app/components/base/notion-page-selector/page-selector/utils'
import PageSelector from '../index'
import { recursivePushInParentDescendants } from '../utils'
// Mock react-window FixedSizeList - renders items directly for testing
vi.mock('react-window', () => ({
FixedSizeList: ({ children: ItemComponent, itemCount, itemData, itemKey }: { children: React.ComponentType<{ index: number, style: React.CSSProperties, data: unknown }>, itemCount: number, itemData: unknown, itemKey?: (index: number, data: unknown) => string | number }) => (
<div data-testid="virtual-list">
{Array.from({ length: itemCount }).map((_, index) => (
<ItemComponent
key={itemKey?.(index, itemData) || index}
index={index}
style={{ top: index * 28, left: 0, right: 0, width: '100%', position: 'absolute' as const }}
data={itemData}
/>
))}
</div>
),
areEqual: (prevProps: Record<string, unknown>, nextProps: Record<string, unknown>) => prevProps === nextProps,
}))
vi.mock('@tanstack/react-virtual')
// Note: NotionIcon from @/app/components/base/ is NOT mocked - using real component per testing guidelines
@@ -70,7 +55,6 @@ const createDefaultProps = (overrides?: Partial<PageSelectorProps>): PageSelecto
canPreview: true,
onPreview: vi.fn(),
isMultipleChoice: true,
currentCredentialId: 'cred-1',
...overrides,
}
}
@@ -114,7 +98,7 @@ describe('PageSelector', () => {
expect(screen.queryByTestId('virtual-list')).not.toBeInTheDocument()
})
it('should render items using FixedSizeList', () => {
it('should render items using VirtualList', () => {
const pages = [
createMockPage({ page_id: 'page-1', page_name: 'Page 1' }),
createMockPage({ page_id: 'page-2', page_name: 'Page 2' }),

View File

@@ -1,7 +1,7 @@
import type { NotionPageTreeItem, NotionPageTreeMap } from '../index'
import type { NotionPageTreeItem, NotionPageTreeMap } from '@/app/components/base/notion-page-selector/page-selector/types'
import type { DataSourceNotionPageMap } from '@/models/common'
import { describe, expect, it } from 'vitest'
import { recursivePushInParentDescendants } from '../utils'
import { recursivePushInParentDescendants } from '@/app/components/base/notion-page-selector/page-selector/utils'
const makePageEntry = (overrides: Partial<NotionPageTreeItem>): NotionPageTreeItem => ({
page_icon: null,

View File

@@ -1,9 +1,7 @@
import type { DataSourceNotionPage, DataSourceNotionPageMap } from '@/models/common'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { FixedSizeList as List } from 'react-window'
import Item from './item'
import { recursivePushInParentDescendants } from './utils'
import { usePageSelectorModel } from '@/app/components/base/notion-page-selector/page-selector/use-page-selector-model'
import VirtualPageList from '@/app/components/base/notion-page-selector/page-selector/virtual-page-list'
type PageSelectorProps = {
checkedIds: Set<string>
@@ -15,23 +13,9 @@ type PageSelectorProps = {
canPreview?: boolean
onPreview?: (selectedPageId: string) => void
isMultipleChoice?: boolean
currentCredentialId: string
currentCredentialId?: string
}
export type NotionPageTreeItem = {
children: Set<string>
descendants: Set<string>
depth: number
ancestors: string[]
} & DataSourceNotionPage
export type NotionPageTreeMap = Record<string, NotionPageTreeItem>
type NotionPageItem = {
expand: boolean
depth: number
} & DataSourceNotionPage
const PageSelector = ({
checkedIds,
disabledValue,
@@ -42,116 +26,28 @@ const PageSelector = ({
canPreview = true,
onPreview,
isMultipleChoice = true,
currentCredentialId,
currentCredentialId: _currentCredentialId,
}: PageSelectorProps) => {
const { t } = useTranslation()
const [dataList, setDataList] = useState<NotionPageItem[]>([])
const [currentPreviewPageId, setCurrentPreviewPageId] = useState('')
useEffect(() => {
setDataList(list.filter(item => item.parent_id === 'root' || !pagesMap[item.parent_id]).map((item) => {
return {
...item,
expand: false,
depth: 0,
}
}))
}, [currentCredentialId])
const searchDataList = list.filter((item) => {
return item.page_name.includes(searchValue)
}).map((item) => {
return {
...item,
expand: false,
depth: 0,
}
const selectionMode = isMultipleChoice ? 'multiple' : 'single'
const {
currentPreviewPageId,
effectiveSearchValue,
rows,
handlePreview,
handleSelect,
handleToggle,
} = usePageSelectorModel({
checkedIds,
list,
onPreview,
onSelect,
pagesMap,
searchValue,
selectionMode,
})
const currentDataList = searchValue ? searchDataList : dataList
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 = useCallback((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)
}, [dataList, listMapWithChildrenAndDescendants, pagesMap])
const handleCheck = useCallback((index: number) => {
const copyValue = new Set(checkedIds)
const current = currentDataList[index]
const pageId = current.page_id
const currentWithChildrenAndDescendants = listMapWithChildrenAndDescendants[pageId]
if (copyValue.has(pageId)) {
if (!searchValue && isMultipleChoice) {
for (const item of currentWithChildrenAndDescendants.descendants)
copyValue.delete(item)
}
copyValue.delete(pageId)
}
else {
if (!searchValue && isMultipleChoice) {
for (const item of currentWithChildrenAndDescendants.descendants)
copyValue.add(item)
}
// Single choice mode, clear previous selection
if (!isMultipleChoice && copyValue.size > 0) {
copyValue.clear()
copyValue.add(pageId)
}
else {
copyValue.add(pageId)
}
}
onSelect(new Set(copyValue))
}, [currentDataList, isMultipleChoice, listMapWithChildrenAndDescendants, onSelect, searchValue, checkedIds])
const handlePreview = useCallback((index: number) => {
const current = currentDataList[index]
const pageId = current.page_id
setCurrentPreviewPageId(pageId)
if (onPreview)
onPreview(pageId)
}, [currentDataList, onPreview])
if (!currentDataList.length) {
if (!rows.length) {
return (
<div className="flex h-[296px] items-center justify-center text-[13px] text-text-tertiary">
{t('dataSource.notion.selector.noSearchResult', { ns: 'common' })}
@@ -160,30 +56,18 @@ const PageSelector = ({
}
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,
disabledCheckedIds: disabledValue,
handleCheck,
canPreview,
handlePreview,
listMapWithChildrenAndDescendants,
searchValue,
previewPageId: currentPreviewPageId,
pagesMap,
isMultipleChoice,
}}
>
{Item}
</List>
<VirtualPageList
checkedIds={checkedIds}
disabledValue={disabledValue}
onPreview={handlePreview}
onSelect={handleSelect}
onToggle={handleToggle}
previewPageId={currentPreviewPageId}
rows={rows}
searchValue={effectiveSearchValue}
selectionMode={selectionMode}
showPreview={canPreview}
/>
)
}

View File

@@ -1,152 +0,0 @@
import type { ListChildComponentProps } from 'react-window'
import type { DataSourceNotionPage, DataSourceNotionPageMap } from '@/models/common'
import { RiArrowDownSLine, RiArrowRightSLine } from '@remixicon/react'
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import { areEqual } from 'react-window'
import Checkbox from '@/app/components/base/checkbox'
import NotionIcon from '@/app/components/base/notion-icon'
import Radio from '@/app/components/base/radio/ui'
import { cn } from '@/utils/classnames'
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 Item = ({ 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
isMultipleChoice?: boolean
}>) => {
const { t } = useTranslation()
const {
dataList,
handleToggle,
checkedIds,
disabledCheckedIds,
handleCheck,
canPreview,
handlePreview,
listMapWithChildrenAndDescendants,
searchValue,
previewPageId,
pagesMap,
isMultipleChoice,
} = 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)' }}
>
{isMultipleChoice
? (
<Checkbox
className="mr-2 shrink-0"
checked={checkedIds.has(current.page_id)}
disabled={disabled}
onCheck={() => {
handleCheck(index)
}}
/>
)
: (
<Radio
className="mr-2 shrink-0"
isChecked={checkedIds.has(current.page_id)}
disabled={disabled}
onCheck={() => {
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('dataSource.notion.selector.preview', { ns: 'common' })}
</div>
)
}
{
searchValue && (
<div
className="ml-1 max-w-[120px] shrink-0 truncate text-xs text-text-quaternary"
title={breadCrumbs.join(' / ')}
>
{breadCrumbs.join(' / ')}
</div>
)
}
</div>
)
}
export default React.memo(Item, areEqual)

View File

@@ -1,39 +0,0 @@
import type { NotionPageTreeItem, NotionPageTreeMap } from './index'
import type { DataSourceNotionPageMap } from '@/models/common'
export 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)
}
}

View File

@@ -1070,9 +1070,6 @@
filter: invert(50%);
}
.markdown-body .react-syntax-highlighter-line-number {
color: var(--color-text-quaternary);
}
.markdown-body .abcjs-inline-audio .abcjs-btn {
display: flex !important;
}

View File

@@ -3328,11 +3328,6 @@
"count": 2
}
},
"app/components/base/notion-page-selector/base.tsx": {
"react/set-state-in-effect": {
"count": 2
}
},
"app/components/base/notion-page-selector/credential-selector/index.tsx": {
"tailwindcss/enforce-consistent-class-order": {
"count": 2
@@ -3348,14 +3343,6 @@
"count": 1
}
},
"app/components/base/notion-page-selector/page-selector/index.tsx": {
"react/set-state-in-effect": {
"count": 1
},
"tailwindcss/enforce-consistent-class-order": {
"count": 3
}
},
"app/components/base/notion-page-selector/search-input/index.tsx": {
"tailwindcss/enforce-consistent-class-order": {
"count": 1
@@ -4831,16 +4818,6 @@
"count": 1
}
},
"app/components/datasets/documents/create-from-pipeline/data-source/online-documents/page-selector/index.tsx": {
"react/set-state-in-effect": {
"count": 1
}
},
"app/components/datasets/documents/create-from-pipeline/data-source/online-documents/page-selector/item.tsx": {
"tailwindcss/enforce-consistent-class-order": {
"count": 3
}
},
"app/components/datasets/documents/create-from-pipeline/data-source/online-documents/title.tsx": {
"tailwindcss/enforce-consistent-class-order": {
"count": 1

View File

@@ -135,6 +135,9 @@
"dataSource.notion.pagesAuthorized": "Pages authorized",
"dataSource.notion.remove": "Remove",
"dataSource.notion.selector.addPages": "Add pages",
"dataSource.notion.selector.configure": "Configure Notion",
"dataSource.notion.selector.docs": "Notion docs",
"dataSource.notion.selector.headerTitle": "Choose Notion pages",
"dataSource.notion.selector.noSearchResult": "No search results",
"dataSource.notion.selector.pageSelected": "Pages Selected",
"dataSource.notion.selector.preview": "PREVIEW",

View File

@@ -81,6 +81,7 @@
"@tailwindcss/typography": "catalog:",
"@tanstack/react-form": "catalog:",
"@tanstack/react-query": "catalog:",
"@tanstack/react-virtual": "catalog:",
"abcjs": "catalog:",
"ahooks": "catalog:",
"class-variance-authority": "catalog:",
@@ -136,7 +137,6 @@
"react-pdf-highlighter": "catalog:",
"react-sortablejs": "catalog:",
"react-textarea-autosize": "catalog:",
"react-window": "catalog:",
"reactflow": "catalog:",
"remark-breaks": "catalog:",
"remark-directive": "catalog:",
@@ -197,7 +197,6 @@
"@types/qs": "catalog:",
"@types/react": "catalog:",
"@types/react-dom": "catalog:",
"@types/react-window": "catalog:",
"@types/sortablejs": "catalog:",
"@typescript-eslint/parser": "catalog:",
"@typescript/native-preview": "catalog:",