mirror of
https://github.com/langgenius/dify.git
synced 2026-02-24 18:05:11 +00:00
Compare commits
10 Commits
dependabot
...
refactor/m
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9ebc0cbe32 | ||
|
|
e0554987c9 | ||
|
|
817cd53143 | ||
|
|
cc3ab30728 | ||
|
|
e9462b7504 | ||
|
|
c5bd31b813 | ||
|
|
1a23951ae7 | ||
|
|
4d60a742dc | ||
|
|
8cf99a85cb | ||
|
|
52a874df98 |
@@ -1,9 +1,8 @@
|
|||||||
import type { ListChildComponentProps } from 'react-window'
|
|
||||||
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 { memo, useEffect, useMemo, useState } from 'react'
|
import { useVirtualizer } from '@tanstack/react-virtual'
|
||||||
|
import { memo, useCallback, useMemo, useRef, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { areEqual, FixedSizeList as List } from 'react-window'
|
|
||||||
import { cn } from '@/utils/classnames'
|
import { cn } from '@/utils/classnames'
|
||||||
import Checkbox from '../../checkbox'
|
import Checkbox from '../../checkbox'
|
||||||
import NotionIcon from '../../notion-icon'
|
import NotionIcon from '../../notion-icon'
|
||||||
@@ -32,6 +31,22 @@ type NotionPageItem = {
|
|||||||
depth: number
|
depth: number
|
||||||
} & DataSourceNotionPage
|
} & DataSourceNotionPage
|
||||||
|
|
||||||
|
type ItemProps = {
|
||||||
|
virtualStart: number
|
||||||
|
virtualSize: number
|
||||||
|
current: NotionPageItem
|
||||||
|
onToggle: (pageId: string) => void
|
||||||
|
checkedIds: Set<string>
|
||||||
|
disabledCheckedIds: Set<string>
|
||||||
|
onCheck: (pageId: string) => void
|
||||||
|
canPreview?: boolean
|
||||||
|
onPreview: (pageId: string) => void
|
||||||
|
listMapWithChildrenAndDescendants: NotionPageTreeMap
|
||||||
|
searchValue: string
|
||||||
|
previewPageId: string
|
||||||
|
pagesMap: DataSourceNotionPageMap
|
||||||
|
}
|
||||||
|
|
||||||
const recursivePushInParentDescendants = (
|
const recursivePushInParentDescendants = (
|
||||||
pagesMap: DataSourceNotionPageMap,
|
pagesMap: DataSourceNotionPageMap,
|
||||||
listTreeMap: NotionPageTreeMap,
|
listTreeMap: NotionPageTreeMap,
|
||||||
@@ -69,34 +84,22 @@ const recursivePushInParentDescendants = (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const ItemComponent = ({ index, style, data }: ListChildComponentProps<{
|
const ItemComponent = ({
|
||||||
dataList: NotionPageItem[]
|
virtualStart,
|
||||||
handleToggle: (index: number) => void
|
virtualSize,
|
||||||
checkedIds: Set<string>
|
current,
|
||||||
disabledCheckedIds: Set<string>
|
onToggle,
|
||||||
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,
|
checkedIds,
|
||||||
disabledCheckedIds,
|
disabledCheckedIds,
|
||||||
handleCheck,
|
onCheck,
|
||||||
canPreview,
|
canPreview,
|
||||||
handlePreview,
|
onPreview,
|
||||||
listMapWithChildrenAndDescendants,
|
listMapWithChildrenAndDescendants,
|
||||||
searchValue,
|
searchValue,
|
||||||
previewPageId,
|
previewPageId,
|
||||||
pagesMap,
|
pagesMap,
|
||||||
} = data
|
}: ItemProps) => {
|
||||||
const current = dataList[index]
|
const { t } = useTranslation()
|
||||||
const currentWithChildrenAndDescendants = listMapWithChildrenAndDescendants[current.page_id]
|
const currentWithChildrenAndDescendants = listMapWithChildrenAndDescendants[current.page_id]
|
||||||
const hasChild = currentWithChildrenAndDescendants.descendants.size > 0
|
const hasChild = currentWithChildrenAndDescendants.descendants.size > 0
|
||||||
const ancestors = currentWithChildrenAndDescendants.ancestors
|
const ancestors = currentWithChildrenAndDescendants.ancestors
|
||||||
@@ -109,7 +112,7 @@ const ItemComponent = ({ index, style, data }: ListChildComponentProps<{
|
|||||||
<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
|
||||||
@@ -132,15 +135,21 @@ const ItemComponent = ({ index, style, data }: ListChildComponentProps<{
|
|||||||
return (
|
return (
|
||||||
<div
|
<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')}
|
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
|
<Checkbox
|
||||||
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
|
||||||
@@ -160,7 +169,7 @@ const ItemComponent = ({ index, style, data }: ListChildComponentProps<{
|
|||||||
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>
|
||||||
@@ -179,7 +188,7 @@ const ItemComponent = ({ index, style, data }: ListChildComponentProps<{
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
const Item = memo(ItemComponent, areEqual)
|
const Item = memo(ItemComponent)
|
||||||
|
|
||||||
const PageSelector = ({
|
const PageSelector = ({
|
||||||
value,
|
value,
|
||||||
@@ -193,31 +202,10 @@ const PageSelector = ({
|
|||||||
onPreview,
|
onPreview,
|
||||||
}: PageSelectorProps) => {
|
}: PageSelectorProps) => {
|
||||||
const { t } = useTranslation()
|
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('')
|
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(() => {
|
const listMapWithChildrenAndDescendants = useMemo(() => {
|
||||||
return list.reduce((prev: NotionPageTreeMap, next: DataSourceNotionPage) => {
|
return list.reduce((prev: NotionPageTreeMap, next: DataSourceNotionPage) => {
|
||||||
const pageId = next.page_id
|
const pageId = next.page_id
|
||||||
@@ -229,47 +217,89 @@ const PageSelector = ({
|
|||||||
}, {})
|
}, {})
|
||||||
}, [list, pagesMap])
|
}, [list, pagesMap])
|
||||||
|
|
||||||
const handleToggle = (index: number) => {
|
const childrenByParent = useMemo(() => {
|
||||||
const current = dataList[index]
|
const map = new Map<string | null, DataSourceNotionPage[]>()
|
||||||
const pageId = current.page_id
|
for (const item of list) {
|
||||||
const currentWithChildrenAndDescendants = listMapWithChildrenAndDescendants[pageId]
|
const isRoot = item.parent_id === 'root' || !pagesMap[item.parent_id]
|
||||||
const descendantsIds = Array.from(currentWithChildrenAndDescendants.descendants)
|
const parentKey = isRoot ? null : item.parent_id
|
||||||
const childrenIds = Array.from(currentWithChildrenAndDescendants.children)
|
const children = map.get(parentKey) || []
|
||||||
let newDataList = []
|
children.push(item)
|
||||||
|
map.set(parentKey, children)
|
||||||
|
}
|
||||||
|
return map
|
||||||
|
}, [list, pagesMap])
|
||||||
|
|
||||||
if (current.expand) {
|
const dataList = useMemo(() => {
|
||||||
current.expand = false
|
const result: NotionPageItem[] = []
|
||||||
|
|
||||||
newDataList = dataList.filter(item => !descendantsIds.includes(item.page_id))
|
const buildVisibleList = (parentId: string | null, depth: number) => {
|
||||||
|
const items = childrenByParent.get(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
|
||||||
|
}, [childrenByParent, 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 = useCallback((pageId: string) => {
|
||||||
|
setExpandedIds((prev) => {
|
||||||
|
const next = new Set(prev)
|
||||||
|
if (prev.has(pageId)) {
|
||||||
|
next.delete(pageId)
|
||||||
|
const descendants = listMapWithChildrenAndDescendants[pageId]?.descendants
|
||||||
|
if (descendants) {
|
||||||
|
for (const descendantId of descendants)
|
||||||
|
next.delete(descendantId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
current.expand = true
|
next.add(pageId)
|
||||||
|
|
||||||
newDataList = [
|
|
||||||
...dataList.slice(0, index + 1),
|
|
||||||
...childrenIds.map(item => ({
|
|
||||||
...pagesMap[item],
|
|
||||||
expand: false,
|
|
||||||
depth: listMapWithChildrenAndDescendants[item].depth,
|
|
||||||
})),
|
|
||||||
...dataList.slice(index + 1),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
setDataList(newDataList)
|
|
||||||
}
|
}
|
||||||
|
return next
|
||||||
|
})
|
||||||
|
}, [listMapWithChildrenAndDescendants])
|
||||||
|
|
||||||
const copyValue = new Set(value)
|
const handleCheck = useCallback((pageId: string) => {
|
||||||
const handleCheck = (index: number) => {
|
|
||||||
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 {
|
||||||
@@ -277,22 +307,17 @@ 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(copyValue)
|
||||||
}
|
}, [listMapWithChildrenAndDescendants, onSelect, searchValue, value])
|
||||||
|
|
||||||
const handlePreview = (index: number) => {
|
|
||||||
const current = currentDataList[index]
|
|
||||||
const pageId = current.page_id
|
|
||||||
|
|
||||||
|
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 (
|
||||||
@@ -303,29 +328,41 @@ const PageSelector = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<List
|
<div
|
||||||
|
ref={parentRef}
|
||||||
className="py-2"
|
className="py-2"
|
||||||
height={296}
|
style={{ height: 296, width: '100%', overflow: 'auto' }}
|
||||||
itemCount={currentDataList.length}
|
>
|
||||||
itemSize={28}
|
<div
|
||||||
width="100%"
|
style={{
|
||||||
itemKey={(index, data) => data.dataList[index].page_id}
|
height: virtualizer.getTotalSize(),
|
||||||
itemData={{
|
width: '100%',
|
||||||
dataList: currentDataList,
|
position: 'relative',
|
||||||
handleToggle,
|
|
||||||
checkedIds: value,
|
|
||||||
disabledCheckedIds: disabledValue,
|
|
||||||
handleCheck,
|
|
||||||
canPreview,
|
|
||||||
handlePreview,
|
|
||||||
listMapWithChildrenAndDescendants,
|
|
||||||
searchValue,
|
|
||||||
previewPageId: currentPreviewPageId,
|
|
||||||
pagesMap,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{Item}
|
{virtualizer.getVirtualItems().map((virtualRow) => {
|
||||||
</List>
|
const current = currentDataList[virtualRow.index]
|
||||||
|
return (
|
||||||
|
<Item
|
||||||
|
key={virtualRow.key}
|
||||||
|
virtualStart={virtualRow.start}
|
||||||
|
virtualSize={virtualRow.size}
|
||||||
|
current={current}
|
||||||
|
onToggle={handleToggle}
|
||||||
|
checkedIds={value}
|
||||||
|
disabledCheckedIds={disabledValue}
|
||||||
|
onCheck={handleCheck}
|
||||||
|
canPreview={canPreview}
|
||||||
|
onPreview={handlePreview}
|
||||||
|
listMapWithChildrenAndDescendants={listMapWithChildrenAndDescendants}
|
||||||
|
searchValue={searchValue}
|
||||||
|
previewPageId={currentPreviewPageId}
|
||||||
|
pagesMap={pagesMap}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,21 +11,18 @@ import { recursivePushInParentDescendants } from './utils'
|
|||||||
|
|
||||||
// Note: react-i18next uses global mock from web/vitest.setup.ts
|
// Note: react-i18next uses global mock from web/vitest.setup.ts
|
||||||
|
|
||||||
// Mock react-window FixedSizeList - renders items directly for testing
|
// Mock @tanstack/react-virtual useVirtualizer hook - renders items directly for testing
|
||||||
vi.mock('react-window', () => ({
|
vi.mock('@tanstack/react-virtual', () => ({
|
||||||
FixedSizeList: ({ children: ItemComponent, itemCount, itemData, itemKey }: any) => (
|
useVirtualizer: ({ count, getItemKey }: { count: number, getItemKey?: (index: number) => string }) => ({
|
||||||
<div data-testid="virtual-list">
|
getVirtualItems: () =>
|
||||||
{Array.from({ length: itemCount }).map((_, index) => (
|
Array.from({ length: count }).map((_, index) => ({
|
||||||
<ItemComponent
|
index,
|
||||||
key={itemKey?.(index, itemData) || index}
|
key: getItemKey ? getItemKey(index) : index,
|
||||||
index={index}
|
start: index * 28,
|
||||||
style={{ top: index * 28, left: 0, right: 0, width: '100%', position: 'absolute' }}
|
size: 28,
|
||||||
data={itemData}
|
})),
|
||||||
/>
|
getTotalSize: () => count * 28,
|
||||||
))}
|
}),
|
||||||
</div>
|
|
||||||
),
|
|
||||||
areEqual: (prevProps: any, nextProps: any) => prevProps === nextProps,
|
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// Note: NotionIcon from @/app/components/base/ is NOT mocked - using real component per testing guidelines
|
// Note: NotionIcon from @/app/components/base/ is NOT mocked - using real component per testing guidelines
|
||||||
@@ -119,7 +116,7 @@ describe('PageSelector', () => {
|
|||||||
render(<PageSelector {...props} />)
|
render(<PageSelector {...props} />)
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
expect(screen.getByTestId('virtual-list')).toBeInTheDocument()
|
expect(screen.getByText('Test Page')).toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should render empty state when list is empty', () => {
|
it('should render empty state when list is empty', () => {
|
||||||
@@ -134,7 +131,7 @@ describe('PageSelector', () => {
|
|||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
expect(screen.getByText('common.dataSource.notion.selector.noSearchResult')).toBeInTheDocument()
|
expect(screen.getByText('common.dataSource.notion.selector.noSearchResult')).toBeInTheDocument()
|
||||||
expect(screen.queryByTestId('virtual-list')).not.toBeInTheDocument()
|
expect(screen.queryByText('Test Page')).not.toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should render items using FixedSizeList', () => {
|
it('should render items using FixedSizeList', () => {
|
||||||
@@ -1166,7 +1163,7 @@ describe('PageSelector', () => {
|
|||||||
render(<PageSelector {...props} />)
|
render(<PageSelector {...props} />)
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
expect(screen.getByTestId('virtual-list')).toBeInTheDocument()
|
expect(screen.getByText('Test Page')).toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should handle special characters in page name', () => {
|
it('should handle special characters in page name', () => {
|
||||||
@@ -1340,7 +1337,7 @@ describe('PageSelector', () => {
|
|||||||
render(<PageSelector {...props} />)
|
render(<PageSelector {...props} />)
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
expect(screen.getByTestId('virtual-list')).toBeInTheDocument()
|
expect(screen.getByText('Test Page')).toBeInTheDocument()
|
||||||
if (propVariation.canPreview)
|
if (propVariation.canPreview)
|
||||||
expect(screen.getByText('common.dataSource.notion.selector.preview')).toBeInTheDocument()
|
expect(screen.getByText('common.dataSource.notion.selector.preview')).toBeInTheDocument()
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { DataSourceNotionPage, DataSourceNotionPageMap } from '@/models/common'
|
import type { DataSourceNotionPage, DataSourceNotionPageMap } from '@/models/common'
|
||||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
import { useVirtualizer } from '@tanstack/react-virtual'
|
||||||
|
import { useCallback, useMemo, useRef, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { FixedSizeList as List } from 'react-window'
|
|
||||||
import Item from './item'
|
import Item from './item'
|
||||||
import { recursivePushInParentDescendants } from './utils'
|
import { recursivePushInParentDescendants } from './utils'
|
||||||
|
|
||||||
@@ -45,29 +45,16 @@ const PageSelector = ({
|
|||||||
currentCredentialId,
|
currentCredentialId,
|
||||||
}: PageSelectorProps) => {
|
}: PageSelectorProps) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [dataList, setDataList] = useState<NotionPageItem[]>([])
|
const parentRef = useRef<HTMLDivElement>(null)
|
||||||
|
const [expandedIds, setExpandedIds] = useState<Set<string>>(() => new Set())
|
||||||
const [currentPreviewPageId, setCurrentPreviewPageId] = useState('')
|
const [currentPreviewPageId, setCurrentPreviewPageId] = useState('')
|
||||||
|
const prevCredentialIdRef = useRef(currentCredentialId)
|
||||||
|
|
||||||
useEffect(() => {
|
// Reset expanded state when credential changes (render-time detection)
|
||||||
setDataList(list.filter(item => item.parent_id === 'root' || !pagesMap[item.parent_id]).map((item) => {
|
if (prevCredentialIdRef.current !== currentCredentialId) {
|
||||||
return {
|
prevCredentialIdRef.current = currentCredentialId
|
||||||
...item,
|
setExpandedIds(new Set())
|
||||||
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 currentDataList = searchValue ? searchDataList : dataList
|
|
||||||
|
|
||||||
const listMapWithChildrenAndDescendants = useMemo(() => {
|
const listMapWithChildrenAndDescendants = useMemo(() => {
|
||||||
return list.reduce((prev: NotionPageTreeMap, next: DataSourceNotionPage) => {
|
return list.reduce((prev: NotionPageTreeMap, next: DataSourceNotionPage) => {
|
||||||
@@ -80,39 +67,86 @@ const PageSelector = ({
|
|||||||
}, {})
|
}, {})
|
||||||
}, [list, pagesMap])
|
}, [list, pagesMap])
|
||||||
|
|
||||||
const handleToggle = useCallback((index: number) => {
|
// Pre-build children index for O(1) lookup instead of O(n) filter
|
||||||
const current = dataList[index]
|
const childrenByParent = useMemo(() => {
|
||||||
const pageId = current.page_id
|
const map = new Map<string | null, DataSourceNotionPage[]>()
|
||||||
const currentWithChildrenAndDescendants = listMapWithChildrenAndDescendants[pageId]
|
for (const item of list) {
|
||||||
const descendantsIds = Array.from(currentWithChildrenAndDescendants.descendants)
|
const isRoot = item.parent_id === 'root' || !pagesMap[item.parent_id]
|
||||||
const childrenIds = Array.from(currentWithChildrenAndDescendants.children)
|
const parentKey = isRoot ? null : item.parent_id
|
||||||
let newDataList = []
|
const children = map.get(parentKey) || []
|
||||||
|
children.push(item)
|
||||||
|
map.set(parentKey, children)
|
||||||
|
}
|
||||||
|
return map
|
||||||
|
}, [list, pagesMap])
|
||||||
|
|
||||||
if (current.expand) {
|
// Compute visible data list based on expanded state
|
||||||
current.expand = false
|
const dataList = useMemo(() => {
|
||||||
|
const result: NotionPageItem[] = []
|
||||||
|
|
||||||
newDataList = dataList.filter(item => !descendantsIds.includes(item.page_id))
|
const buildVisibleList = (parentId: string | null, depth: number) => {
|
||||||
|
const items = childrenByParent.get(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
|
||||||
|
}, [childrenByParent, 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 virtualizer = useVirtualizer({
|
||||||
|
count: currentDataList.length,
|
||||||
|
getScrollElement: () => parentRef.current,
|
||||||
|
estimateSize: () => 28,
|
||||||
|
overscan: 5,
|
||||||
|
getItemKey: index => currentDataList[index].page_id,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Stable callback - no dependencies on dataList
|
||||||
|
const handleToggle = useCallback((pageId: string) => {
|
||||||
|
setExpandedIds((prev) => {
|
||||||
|
const next = new Set(prev)
|
||||||
|
if (prev.has(pageId)) {
|
||||||
|
// Collapse: remove current and all descendants
|
||||||
|
next.delete(pageId)
|
||||||
|
const descendants = listMapWithChildrenAndDescendants[pageId]?.descendants
|
||||||
|
if (descendants) {
|
||||||
|
for (const descendantId of descendants)
|
||||||
|
next.delete(descendantId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
current.expand = true
|
next.add(pageId)
|
||||||
|
|
||||||
newDataList = [
|
|
||||||
...dataList.slice(0, index + 1),
|
|
||||||
...childrenIds.map(item => ({
|
|
||||||
...pagesMap[item],
|
|
||||||
expand: false,
|
|
||||||
depth: listMapWithChildrenAndDescendants[item].depth,
|
|
||||||
})),
|
|
||||||
...dataList.slice(index + 1),
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
setDataList(newDataList)
|
return next
|
||||||
}, [dataList, listMapWithChildrenAndDescendants, pagesMap])
|
})
|
||||||
|
}, [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)) {
|
||||||
@@ -120,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 {
|
||||||
@@ -138,18 +171,15 @@ const PageSelector = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onSelect(new Set(copyValue))
|
onSelect(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 (
|
||||||
@@ -160,30 +190,42 @@ const PageSelector = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<List
|
<div
|
||||||
|
ref={parentRef}
|
||||||
className="py-2"
|
className="py-2"
|
||||||
height={296}
|
style={{ height: 296, width: '100%', overflow: 'auto' }}
|
||||||
itemCount={currentDataList.length}
|
>
|
||||||
itemSize={28}
|
<div
|
||||||
width="100%"
|
style={{
|
||||||
itemKey={(index, data) => data.dataList[index].page_id}
|
height: virtualizer.getTotalSize(),
|
||||||
itemData={{
|
width: '100%',
|
||||||
dataList: currentDataList,
|
position: 'relative',
|
||||||
handleToggle,
|
|
||||||
checkedIds,
|
|
||||||
disabledCheckedIds: disabledValue,
|
|
||||||
handleCheck,
|
|
||||||
canPreview,
|
|
||||||
handlePreview,
|
|
||||||
listMapWithChildrenAndDescendants,
|
|
||||||
searchValue,
|
|
||||||
previewPageId: currentPreviewPageId,
|
|
||||||
pagesMap,
|
|
||||||
isMultipleChoice,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{Item}
|
{virtualizer.getVirtualItems().map((virtualRow) => {
|
||||||
</List>
|
const current = currentDataList[virtualRow.index]
|
||||||
|
return (
|
||||||
|
<Item
|
||||||
|
key={virtualRow.key}
|
||||||
|
virtualStart={virtualRow.start}
|
||||||
|
virtualSize={virtualRow.size}
|
||||||
|
current={current}
|
||||||
|
onToggle={handleToggle}
|
||||||
|
checkedIds={checkedIds}
|
||||||
|
disabledCheckedIds={disabledValue}
|
||||||
|
onCheck={handleCheck}
|
||||||
|
canPreview={canPreview}
|
||||||
|
onPreview={handlePreview}
|
||||||
|
listMapWithChildrenAndDescendants={listMapWithChildrenAndDescendants}
|
||||||
|
searchValue={searchValue}
|
||||||
|
previewPageId={currentPreviewPageId}
|
||||||
|
pagesMap={pagesMap}
|
||||||
|
isMultipleChoice={isMultipleChoice}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import type { ListChildComponentProps } from 'react-window'
|
|
||||||
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 * as React from 'react'
|
import { memo } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { areEqual } from 'react-window'
|
|
||||||
import Checkbox from '@/app/components/base/checkbox'
|
import Checkbox from '@/app/components/base/checkbox'
|
||||||
import NotionIcon from '@/app/components/base/notion-icon'
|
import NotionIcon from '@/app/components/base/notion-icon'
|
||||||
import Radio from '@/app/components/base/radio/ui'
|
import Radio from '@/app/components/base/radio/ui'
|
||||||
@@ -23,36 +21,40 @@ type NotionPageItem = {
|
|||||||
depth: number
|
depth: number
|
||||||
} & DataSourceNotionPage
|
} & DataSourceNotionPage
|
||||||
|
|
||||||
const Item = ({ index, style, data }: ListChildComponentProps<{
|
type ItemProps = {
|
||||||
dataList: NotionPageItem[]
|
virtualStart: number
|
||||||
handleToggle: (index: number) => void
|
virtualSize: number
|
||||||
|
current: NotionPageItem
|
||||||
|
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
|
||||||
pagesMap: DataSourceNotionPageMap
|
pagesMap: DataSourceNotionPageMap
|
||||||
isMultipleChoice?: boolean
|
isMultipleChoice?: boolean
|
||||||
}>) => {
|
}
|
||||||
const { t } = useTranslation()
|
|
||||||
const {
|
const Item = ({
|
||||||
dataList,
|
virtualStart,
|
||||||
handleToggle,
|
virtualSize,
|
||||||
|
current,
|
||||||
|
onToggle,
|
||||||
checkedIds,
|
checkedIds,
|
||||||
disabledCheckedIds,
|
disabledCheckedIds,
|
||||||
handleCheck,
|
onCheck,
|
||||||
canPreview,
|
canPreview,
|
||||||
handlePreview,
|
onPreview,
|
||||||
listMapWithChildrenAndDescendants,
|
listMapWithChildrenAndDescendants,
|
||||||
searchValue,
|
searchValue,
|
||||||
previewPageId,
|
previewPageId,
|
||||||
pagesMap,
|
pagesMap,
|
||||||
isMultipleChoice,
|
isMultipleChoice,
|
||||||
} = data
|
}: ItemProps) => {
|
||||||
const current = dataList[index]
|
const { t } = useTranslation()
|
||||||
const currentWithChildrenAndDescendants = listMapWithChildrenAndDescendants[current.page_id]
|
const currentWithChildrenAndDescendants = listMapWithChildrenAndDescendants[current.page_id]
|
||||||
const hasChild = currentWithChildrenAndDescendants.descendants.size > 0
|
const hasChild = currentWithChildrenAndDescendants.descendants.size > 0
|
||||||
const ancestors = currentWithChildrenAndDescendants.ancestors
|
const ancestors = currentWithChildrenAndDescendants.ancestors
|
||||||
@@ -65,7 +67,7 @@ const Item = ({ index, style, data }: ListChildComponentProps<{
|
|||||||
<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
|
||||||
@@ -88,7 +90,15 @@ const Item = ({ index, style, data }: ListChildComponentProps<{
|
|||||||
return (
|
return (
|
||||||
<div
|
<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')}
|
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)`,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{isMultipleChoice
|
{isMultipleChoice
|
||||||
? (
|
? (
|
||||||
@@ -96,9 +106,7 @@ const Item = ({ index, style, data }: ListChildComponentProps<{
|
|||||||
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)
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
: (
|
: (
|
||||||
@@ -106,9 +114,7 @@ const Item = ({ index, style, data }: ListChildComponentProps<{
|
|||||||
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()}
|
||||||
@@ -129,7 +135,7 @@ const Item = ({ index, style, data }: ListChildComponentProps<{
|
|||||||
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>
|
||||||
@@ -149,4 +155,4 @@ const Item = ({ index, style, data }: ListChildComponentProps<{
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default React.memo(Item, areEqual)
|
export default memo(Item)
|
||||||
|
|||||||
@@ -1399,11 +1399,6 @@
|
|||||||
"count": 2
|
"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": {
|
"app/components/base/pagination/index.tsx": {
|
||||||
"unicorn/prefer-number-properties": {
|
"unicorn/prefer-number-properties": {
|
||||||
"count": 1
|
"count": 1
|
||||||
@@ -1848,12 +1843,7 @@
|
|||||||
},
|
},
|
||||||
"app/components/datasets/documents/create-from-pipeline/data-source/online-documents/page-selector/index.spec.tsx": {
|
"app/components/datasets/documents/create-from-pipeline/data-source/online-documents/page-selector/index.spec.tsx": {
|
||||||
"ts/no-explicit-any": {
|
"ts/no-explicit-any": {
|
||||||
"count": 5
|
"count": 2
|
||||||
}
|
|
||||||
},
|
|
||||||
"app/components/datasets/documents/create-from-pipeline/data-source/online-documents/page-selector/index.tsx": {
|
|
||||||
"react-hooks-extra/no-direct-set-state-in-use-effect": {
|
|
||||||
"count": 1
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"app/components/datasets/documents/create-from-pipeline/data-source/online-drive/connect/index.spec.tsx": {
|
"app/components/datasets/documents/create-from-pipeline/data-source/online-drive/connect/index.spec.tsx": {
|
||||||
|
|||||||
@@ -84,6 +84,7 @@
|
|||||||
"@tailwindcss/typography": "0.5.19",
|
"@tailwindcss/typography": "0.5.19",
|
||||||
"@tanstack/react-form": "1.23.7",
|
"@tanstack/react-form": "1.23.7",
|
||||||
"@tanstack/react-query": "5.90.5",
|
"@tanstack/react-query": "5.90.5",
|
||||||
|
"@tanstack/react-virtual": "3.13.18",
|
||||||
"abcjs": "6.5.2",
|
"abcjs": "6.5.2",
|
||||||
"ahooks": "3.9.5",
|
"ahooks": "3.9.5",
|
||||||
"class-variance-authority": "0.7.1",
|
"class-variance-authority": "0.7.1",
|
||||||
@@ -142,7 +143,6 @@
|
|||||||
"react-sortablejs": "6.1.4",
|
"react-sortablejs": "6.1.4",
|
||||||
"react-syntax-highlighter": "15.6.6",
|
"react-syntax-highlighter": "15.6.6",
|
||||||
"react-textarea-autosize": "8.5.9",
|
"react-textarea-autosize": "8.5.9",
|
||||||
"react-window": "1.8.11",
|
|
||||||
"reactflow": "11.11.4",
|
"reactflow": "11.11.4",
|
||||||
"rehype-katex": "7.0.1",
|
"rehype-katex": "7.0.1",
|
||||||
"rehype-raw": "7.0.0",
|
"rehype-raw": "7.0.0",
|
||||||
@@ -199,7 +199,6 @@
|
|||||||
"@types/react-dom": "19.2.3",
|
"@types/react-dom": "19.2.3",
|
||||||
"@types/react-slider": "1.3.6",
|
"@types/react-slider": "1.3.6",
|
||||||
"@types/react-syntax-highlighter": "15.5.13",
|
"@types/react-syntax-highlighter": "15.5.13",
|
||||||
"@types/react-window": "1.8.8",
|
|
||||||
"@types/semver": "7.7.1",
|
"@types/semver": "7.7.1",
|
||||||
"@types/sortablejs": "1.15.8",
|
"@types/sortablejs": "1.15.8",
|
||||||
"@types/uuid": "10.0.0",
|
"@types/uuid": "10.0.0",
|
||||||
|
|||||||
1905
web/pnpm-lock.yaml
generated
1905
web/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user