This is an automated email from the ASF dual-hosted git repository. kgabryje pushed a commit to branch folders in repository https://gitbox.apache.org/repos/asf/superset.git
commit 66a3d2af9724abfd390c69efa8479cb520dbba62 Author: Kamil Gabryjelski <[email protected]> AuthorDate: Thu Jan 29 17:01:56 2026 +0100 Perf improvements --- .../Datasource/FoldersEditor/TreeItem.tsx | 25 +-- .../FoldersEditor/VirtualizedTreeList.tsx | 1 - .../Datasource/FoldersEditor/folderOperations.ts | 32 ++-- .../FoldersEditor/hooks/useDragHandlers.ts | 211 ++++++++++----------- .../components/Datasource/FoldersEditor/index.tsx | 58 +++--- .../Datasource/FoldersEditor/treeUtils.ts | 50 +++-- 6 files changed, 185 insertions(+), 192 deletions(-) diff --git a/superset-frontend/src/components/Datasource/FoldersEditor/TreeItem.tsx b/superset-frontend/src/components/Datasource/FoldersEditor/TreeItem.tsx index e9d66df487c..a941fd19474 100644 --- a/superset-frontend/src/components/Datasource/FoldersEditor/TreeItem.tsx +++ b/superset-frontend/src/components/Datasource/FoldersEditor/TreeItem.tsx @@ -30,10 +30,12 @@ import { Tooltip, } from '@superset-ui/core/components'; import { + ColumnLabelExtendedType, ColumnMeta, ColumnTypeLabel, Metric, } from '@superset-ui/chart-controls'; +import { GenericDataType } from '@apache-superset/core/api/core'; import { OptionControlContainer, Label, @@ -169,17 +171,18 @@ function TreeItemComponent({ return name; }, [type, metric, column, name]); - const columnType = useMemo(() => { - if (type === FoldersEditorItemType.Metric) { - return 'metric'; - } - if (type === FoldersEditorItemType.Column && column) { - const hasExpression = - column.expression && column.expression !== column.column_name; - return hasExpression ? 'expression' : column.type_generic; - } - return undefined; - }, [type, column]); + const columnType: ColumnLabelExtendedType | GenericDataType | undefined = + useMemo(() => { + if (type === FoldersEditorItemType.Metric) { + return 'metric'; + } + if (type === FoldersEditorItemType.Column && column) { + const hasExpression = + column.expression && column.expression !== column.column_name; + return hasExpression ? 'expression' : column.type_generic; + } + return undefined; + }, [type, column]); const hasEmptyName = !name || name.trim() === ''; diff --git a/superset-frontend/src/components/Datasource/FoldersEditor/VirtualizedTreeList.tsx b/superset-frontend/src/components/Datasource/FoldersEditor/VirtualizedTreeList.tsx index 57ba0e33d29..1920b296198 100644 --- a/superset-frontend/src/components/Datasource/FoldersEditor/VirtualizedTreeList.tsx +++ b/superset-frontend/src/components/Datasource/FoldersEditor/VirtualizedTreeList.tsx @@ -163,7 +163,6 @@ export function VirtualizedTreeList({ itemSeparatorInfo, visibleItemIds, searchTerm, - activeId, ], ); diff --git a/superset-frontend/src/components/Datasource/FoldersEditor/folderOperations.ts b/superset-frontend/src/components/Datasource/FoldersEditor/folderOperations.ts index d84cd9a2414..4543c9cecd6 100644 --- a/superset-frontend/src/components/Datasource/FoldersEditor/folderOperations.ts +++ b/superset-frontend/src/components/Datasource/FoldersEditor/folderOperations.ts @@ -362,27 +362,21 @@ export const ensureDefaultFolders = ( const result = [...enrichedFolders]; - const isItemInFolders = (uuid: string): boolean => { - const checkFolder = (folder: DatasourceFolder): boolean => { - if (!folder.children) return false; - - return folder.children.some(child => { - if (child.uuid === uuid) return true; - if ( - child.type === FoldersEditorItemType.Folder && - 'children' in child - ) { - return checkFolder(child as DatasourceFolder); - } - return false; - }); - }; - - return enrichedFolders.some(checkFolder); + // Build a Set of all assigned UUIDs in a single pass for O(1) lookups + const assignedIds = new Set<string>(); + const collectAssignedIds = (folder: DatasourceFolder) => { + if (!folder.children) return; + for (const child of folder.children) { + assignedIds.add(child.uuid); + if (child.type === FoldersEditorItemType.Folder && 'children' in child) { + collectAssignedIds(child as DatasourceFolder); + } + } }; + enrichedFolders.forEach(collectAssignedIds); if (!hasMetricsFolder) { - const unassignedMetrics = metrics.filter(m => !isItemInFolders(m.uuid)); + const unassignedMetrics = metrics.filter(m => !assignedIds.has(m.uuid)); result.push({ uuid: DEFAULT_METRICS_FOLDER_UUID, @@ -397,7 +391,7 @@ export const ensureDefaultFolders = ( } if (!hasColumnsFolder) { - const unassignedColumns = columns.filter(c => !isItemInFolders(c.uuid)); + const unassignedColumns = columns.filter(c => !assignedIds.has(c.uuid)); result.push({ uuid: DEFAULT_COLUMNS_FOLDER_UUID, diff --git a/superset-frontend/src/components/Datasource/FoldersEditor/hooks/useDragHandlers.ts b/superset-frontend/src/components/Datasource/FoldersEditor/hooks/useDragHandlers.ts index 8b90c961e13..9da07b8a048 100644 --- a/superset-frontend/src/components/Datasource/FoldersEditor/hooks/useDragHandlers.ts +++ b/superset-frontend/src/components/Datasource/FoldersEditor/hooks/useDragHandlers.ts @@ -39,7 +39,6 @@ import { import { buildTree, getProjection, serializeForAPI } from '../treeUtils'; interface UseDragHandlersProps { - items: TreeItemType[]; setItems: React.Dispatch<React.SetStateAction<TreeItemType[]>>; computeFlattenedItems: ( activeId: UniqueIdentifier | null, @@ -51,7 +50,6 @@ interface UseDragHandlersProps { } export function useDragHandlers({ - items, setItems, computeFlattenedItems, fullFlattenedItems, @@ -98,6 +96,58 @@ export function useDragHandlers({ return map; }, [flattenedItems]); + // Shared lookup maps for O(1) access - used by handleDragEnd and forbiddenDropFolderIds + const fullItemsByUuid = useMemo(() => { + const map = new Map<string, FlattenedTreeItem>(); + fullFlattenedItems.forEach(item => { + map.set(item.uuid, item); + }); + return map; + }, [fullFlattenedItems]); + + const fullItemsIndexMap = useMemo(() => { + const map = new Map<string, number>(); + fullFlattenedItems.forEach((item, index) => { + map.set(item.uuid, index); + }); + return map; + }, [fullFlattenedItems]); + + const childrenByParentId = useMemo(() => { + const map = new Map<string, FlattenedTreeItem[]>(); + fullFlattenedItems.forEach(item => { + if (item.parentId) { + const children = map.get(item.parentId) ?? []; + children.push(item); + map.set(item.parentId, children); + } + }); + return map; + }, [fullFlattenedItems]); + + // Shared helper to calculate max folder descendant depth + // Only counts folder depths, not items (columns/metrics) + const getMaxFolderDescendantDepth = useCallback( + (parentId: string, baseDepth: number): number => { + const children = childrenByParentId.get(parentId); + if (!children || children.length === 0) { + return baseDepth; + } + let maxDepth = baseDepth; + for (const child of children) { + if (child.type === FoldersEditorItemType.Folder) { + maxDepth = Math.max(maxDepth, child.depth); + maxDepth = Math.max( + maxDepth, + getMaxFolderDescendantDepth(child.uuid, child.depth), + ); + } + } + return maxDepth; + }, + [childrenByParentId], + ); + const resetDragState = useCallback(() => { setActiveId(null); setOverId(null); @@ -151,13 +201,7 @@ export function useDragHandlers({ setCurrentDropTargetId(newParentId); } }, - [ - activeId, - overId, - flattenedItems, - flattenedItemsIndexMap, - setCurrentDropTargetId, - ], + [activeId, overId, flattenedItems, flattenedItemsIndexMap], ); const handleDragOver = useCallback( @@ -185,7 +229,7 @@ export function useDragHandlers({ setCurrentDropTargetId(null); } }, - [activeId, flattenedItems, flattenedItemsIndexMap, setCurrentDropTargetId], + [activeId, flattenedItems, flattenedItemsIndexMap], ); const handleDragEnd = ({ active, over }: DragEndEvent) => { @@ -208,19 +252,17 @@ export function useDragHandlers({ } } - const activeIndex = fullFlattenedItems.findIndex( - ({ uuid }) => uuid === active.id, - ); - const overIndex = fullFlattenedItems.findIndex( - ({ uuid }) => uuid === targetOverId, - ); + const activeIndex = fullItemsIndexMap.get(active.id as string) ?? -1; + const overIndex = fullItemsIndexMap.get(targetOverId as string) ?? -1; if (activeIndex === -1 || overIndex === -1) { return; } + // Use Set for O(1) lookup instead of Array.includes + const itemsBeingDraggedSet = new Set(itemsBeingDragged); const draggedItems = fullFlattenedItems.filter((item: FlattenedTreeItem) => - itemsBeingDragged.includes(item.uuid), + itemsBeingDraggedSet.has(item.uuid), ); let projectedPosition = getProjection( @@ -229,6 +271,7 @@ export function useDragHandlers({ targetOverId, finalOffsetLeft, DRAG_INDENTATION_WIDTH, + flattenedItemsIndexMap, ); if (isEmptyDrop) { @@ -250,9 +293,21 @@ export function useDragHandlers({ } } - const hasNonFolderItems = draggedItems.some( - item => item.type !== FoldersEditorItemType.Folder, - ); + // Single pass to gather info about dragged items + let hasNonFolderItems = false; + let hasDraggedFolder = false; + let hasDraggedDefaultFolder = false; + for (const item of draggedItems) { + if (item.type === FoldersEditorItemType.Folder) { + hasDraggedFolder = true; + if (isDefaultFolder(item.uuid)) { + hasDraggedDefaultFolder = true; + } + } else { + hasNonFolderItems = true; + } + } + if (hasNonFolderItems) { if (!projectedPosition || !projectedPosition.parentId) { return; @@ -260,9 +315,7 @@ export function useDragHandlers({ } if (projectedPosition && projectedPosition.parentId) { - const targetFolder = fullFlattenedItems.find( - ({ uuid }) => uuid === projectedPosition.parentId, - ); + const targetFolder = fullItemsByUuid.get(projectedPosition.parentId); if (targetFolder && isDefaultFolder(targetFolder.uuid)) { const isDefaultMetricsFolder = @@ -293,55 +346,13 @@ export function useDragHandlers({ } } - const hasDraggedDefaultFolder = draggedItems.some( - item => - item.type === FoldersEditorItemType.Folder && - isDefaultFolder(item.uuid), - ); if (hasDraggedDefaultFolder && projectedPosition?.parentId) { addWarningToast(t('Default folders cannot be nested')); return; } // Check max depth for folders - const hasDraggedFolder = draggedItems.some( - item => item.type === FoldersEditorItemType.Folder, - ); if (hasDraggedFolder && projectedPosition) { - // Build a children map for O(1) lookups - const childrenByParentId = new Map<string, FlattenedTreeItem[]>(); - fullFlattenedItems.forEach((item: FlattenedTreeItem) => { - if (item.parentId) { - const children = childrenByParentId.get(item.parentId) ?? []; - children.push(item); - childrenByParentId.set(item.parentId, children); - } - }); - - // Calculate the maximum depth among FOLDER descendants only - // (items like columns/metrics don't count toward the folder nesting limit) - const getMaxFolderDescendantDepth = ( - parentId: string, - baseDepth: number, - ): number => { - const children = childrenByParentId.get(parentId); - if (!children || children.length === 0) { - return baseDepth; - } - let maxDepth = baseDepth; - for (const child of children) { - // Only count folder depths, not items (columns/metrics) - if (child.type === FoldersEditorItemType.Folder) { - maxDepth = Math.max(maxDepth, child.depth); - maxDepth = Math.max( - maxDepth, - getMaxFolderDescendantDepth(child.uuid, child.depth), - ); - } - } - return maxDepth; - }; - for (const draggedItem of draggedItems) { if (draggedItem.type === FoldersEditorItemType.Folder) { const currentDepth = draggedItem.depth; @@ -390,8 +401,10 @@ export function useDragHandlers({ parentId: string, parentDepthChange: number, ) => { - fullFlattenedItems.forEach((item: FlattenedTreeItem) => { - if (item.parentId === parentId && !itemsToUpdate.has(item.uuid)) { + const children = childrenByParentId.get(parentId); + if (!children) return; + for (const item of children) { + if (!itemsToUpdate.has(item.uuid)) { itemsToUpdate.set(item.uuid, { depth: item.depth + parentDepthChange, parentId: undefined, @@ -400,7 +413,7 @@ export function useDragHandlers({ collectDescendants(item.uuid, parentDepthChange); } } - }); + } }; draggedItems.forEach((item: FlattenedTreeItem) => { @@ -427,14 +440,16 @@ export function useDragHandlers({ const itemsToMoveIds = new Set(itemsBeingDragged); const collectDescendantIds = (parentId: string) => { - fullFlattenedItems.forEach((item: FlattenedTreeItem) => { - if (item.parentId === parentId && !itemsToMoveIds.has(item.uuid)) { + const children = childrenByParentId.get(parentId); + if (!children) return; + for (const item of children) { + if (!itemsToMoveIds.has(item.uuid)) { itemsToMoveIds.add(item.uuid); if (item.type === FoldersEditorItemType.Folder) { collectDescendantIds(item.uuid); } } - }); + } }; draggedItems.forEach((item: FlattenedTreeItem) => { @@ -443,17 +458,18 @@ export function useDragHandlers({ } }); + // Indices are already in ascending order since we iterate fullFlattenedItems sequentially const itemsToMoveIndices: number[] = []; fullFlattenedItems.forEach((item: FlattenedTreeItem, idx: number) => { if (itemsToMoveIds.has(item.uuid)) { itemsToMoveIndices.push(idx); } }); - itemsToMoveIndices.sort((a, b) => a - b); const subtree = itemsToMoveIndices.map(idx => newItems[idx]); + const itemsToMoveIndicesSet = new Set(itemsToMoveIndices); const remaining = newItems.filter( - (_: FlattenedTreeItem, idx: number) => !itemsToMoveIndices.includes(idx), + (_: FlattenedTreeItem, idx: number) => !itemsToMoveIndicesSet.has(idx), ); let insertionIndex = 0; @@ -537,48 +553,12 @@ export function useDragHandlers({ return forbidden; } - // Build a children map for O(1) lookups instead of O(n) scans - const childrenByParentId = new Map<string, FlattenedTreeItem[]>(); - const itemsByUuid = new Map<string, FlattenedTreeItem>(); - fullFlattenedItems.forEach((item: FlattenedTreeItem) => { - itemsByUuid.set(item.uuid, item); - if (item.parentId) { - const children = childrenByParentId.get(item.parentId) ?? []; - children.push(item); - childrenByParentId.set(item.parentId, children); - } - }); - - // Helper to calculate max FOLDER descendant depth offset (items don't count) - const getMaxFolderDescendantDepthOffset = ( - parentId: string, - baseDepth: number, - ): number => { - const children = childrenByParentId.get(parentId); - if (!children || children.length === 0) { - return 0; - } - let maxOffset = 0; - for (const child of children) { - // Only count folder depths, not items (columns/metrics) - if (child.type === FoldersEditorItemType.Folder) { - const offset = child.depth - baseDepth; - maxOffset = Math.max(maxOffset, offset); - maxOffset = Math.max( - maxOffset, - getMaxFolderDescendantDepthOffset(child.uuid, baseDepth), - ); - } - } - return maxOffset; - }; - const draggedTypes = new Set<FoldersEditorItemType>(); let hasDraggedDefaultFolder = false; let maxDraggedFolderDescendantOffset = 0; draggedItemIds.forEach((id: string) => { - const item = itemsByUuid.get(id); + const item = fullItemsByUuid.get(id); if (item) { draggedTypes.add(item.type); if ( @@ -589,10 +569,11 @@ export function useDragHandlers({ } // Track the deepest folder descendant offset for dragged folders if (item.type === FoldersEditorItemType.Folder) { - const descendantOffset = getMaxFolderDescendantDepthOffset( + const maxDescendantDepth = getMaxFolderDescendantDepth( item.uuid, item.depth, ); + const descendantOffset = maxDescendantDepth - item.depth; maxDraggedFolderDescendantOffset = Math.max( maxDraggedFolderDescendantOffset, descendantOffset, @@ -656,7 +637,12 @@ export function useDragHandlers({ }); return forbidden; - }, [draggedItemIds, fullFlattenedItems]); + }, [ + draggedItemIds, + fullFlattenedItems, + fullItemsByUuid, + getMaxFolderDescendantDepth, + ]); return { isDragging: activeId !== null, @@ -667,6 +653,7 @@ export function useDragHandlers({ dragOverlayItems, forbiddenDropFolderIds, currentDropTargetId, + fullItemsByUuid, handleDragStart, handleDragMove, handleDragOver, diff --git a/superset-frontend/src/components/Datasource/FoldersEditor/index.tsx b/superset-frontend/src/components/Datasource/FoldersEditor/index.tsx index 8fe2f69b687..7325569c50c 100644 --- a/superset-frontend/src/components/Datasource/FoldersEditor/index.tsx +++ b/superset-frontend/src/components/Datasource/FoldersEditor/index.tsx @@ -38,7 +38,6 @@ import { flattenTree, buildTree, removeChildrenOf, - getChildCount, serializeForAPI, } from './treeUtils'; import { @@ -93,23 +92,19 @@ export default function FoldersEditor({ const fullFlattenedItems = useMemo(() => flattenTree(items), [items]); - const collapsedFolderIds = useMemo( - () => - fullFlattenedItems.reduce<UniqueIdentifier[]>( - (acc, { uuid, type, children }) => { - if ( - type === FoldersEditorItemType.Folder && - collapsedIds.has(uuid) && - children?.length - ) { - return [...acc, uuid]; - } - return acc; - }, - [], - ), - [fullFlattenedItems, collapsedIds], - ); + const collapsedFolderIds = useMemo(() => { + const result: UniqueIdentifier[] = []; + for (const { uuid, type, children } of fullFlattenedItems) { + if ( + type === FoldersEditorItemType.Folder && + collapsedIds.has(uuid) && + children?.length + ) { + result.push(uuid); + } + } + return result; + }, [fullFlattenedItems, collapsedIds]); const computeFlattenedItems = useCallback( (activeId: UniqueIdentifier | null) => @@ -150,13 +145,13 @@ export default function FoldersEditor({ dragOverlayItems, forbiddenDropFolderIds, currentDropTargetId, + fullItemsByUuid, handleDragStart, handleDragMove, handleDragOver, handleDragEnd, handleDragCancel, } = useDragHandlers({ - items, setItems, computeFlattenedItems, fullFlattenedItems, @@ -186,19 +181,19 @@ export default function FoldersEditor({ const allVisibleSelected = useMemo(() => { const selectableItems = Array.from(visibleItemIds).filter(id => { - const item = fullFlattenedItems.find(i => i.uuid === id); + const item = fullItemsByUuid.get(id); return item && item.type !== FoldersEditorItemType.Folder; }); return ( selectableItems.length > 0 && selectableItems.every(id => selectedItemIds.has(id)) ); - }, [fullFlattenedItems, visibleItemIds, selectedItemIds]); + }, [fullItemsByUuid, visibleItemIds, selectedItemIds]); - const handleSelectAll = () => { + const handleSelectAll = useCallback(() => { const itemsToSelect = new Set( Array.from(visibleItemIds).filter(id => { - const item = fullFlattenedItems.find(i => i.uuid === id); + const item = fullItemsByUuid.get(id); return item && item.type !== FoldersEditorItemType.Folder; }), ); @@ -208,7 +203,7 @@ export default function FoldersEditor({ } else { setSelectedItemIds(itemsToSelect); } - }; + }, [visibleItemIds, fullItemsByUuid, allVisibleSelected]); const handleResetToDefault = () => { setShowResetConfirm(true); @@ -382,13 +377,20 @@ export default function FoldersEditor({ const folderChildCounts = useMemo(() => { const counts = new Map<string, number>(); - flattenedItems.forEach(item => { + // Initialize all folders with 0 + for (const item of flattenedItems) { if (item.type === FoldersEditorItemType.Folder) { - counts.set(item.uuid, getChildCount(items, item.uuid)); + counts.set(item.uuid, 0); } - }); + } + // Single pass: count children by parentId + for (const item of flattenedItems) { + if (item.parentId && counts.has(item.parentId)) { + counts.set(item.parentId, counts.get(item.parentId)! + 1); + } + } return counts; - }, [flattenedItems, items]); + }, [flattenedItems]); return ( <FoldersContainer> diff --git a/superset-frontend/src/components/Datasource/FoldersEditor/treeUtils.ts b/superset-frontend/src/components/Datasource/FoldersEditor/treeUtils.ts index bdea03e4257..4938ea4dcbc 100644 --- a/superset-frontend/src/components/Datasource/FoldersEditor/treeUtils.ts +++ b/superset-frontend/src/components/Datasource/FoldersEditor/treeUtils.ts @@ -94,23 +94,31 @@ export function getProjection( let previousItem: FlattenedTreeItem | undefined; let nextItem: FlattenedTreeItem | undefined; + let previousItemIndex: number; + let nextItemIndex: number; if (activeItemIndex < overItemIndex) { - previousItem = items[overItemIndex]; - nextItem = items[overItemIndex + 1]; + previousItemIndex = overItemIndex; + nextItemIndex = overItemIndex + 1; } else if (activeItemIndex > overItemIndex) { - previousItem = items[overItemIndex - 1]; - nextItem = items[overItemIndex]; + previousItemIndex = overItemIndex - 1; + nextItemIndex = overItemIndex; } else { - previousItem = items[overItemIndex - 1]; - nextItem = items[overItemIndex + 1]; + previousItemIndex = overItemIndex - 1; + nextItemIndex = overItemIndex + 1; } + previousItem = items[previousItemIndex]; + nextItem = items[nextItemIndex]; + + // Skip over the active item if it's adjacent if (previousItem?.uuid === activeId) { - previousItem = items[items.indexOf(previousItem) - 1]; + previousItemIndex -= 1; + previousItem = items[previousItemIndex]; } if (nextItem?.uuid === activeId) { - nextItem = items[items.indexOf(nextItem) + 1]; + nextItemIndex += 1; + nextItem = items[nextItemIndex]; } const dragDepth = getDragDepth(dragOffset, indentationWidth); @@ -128,7 +136,7 @@ export function getProjection( let parentId: string | null = null; if (depth > 0 && previousItem) { if (depth === previousItem.depth) { - parentId = previousItem.parentId; + ({ parentId } = previousItem); } else if (depth > previousItem.depth) { parentId = previousItem.uuid; } else { @@ -136,7 +144,7 @@ export function getProjection( activeItemIndex < overItemIndex ? overItemIndex : overItemIndex - 1; for (let i = searchEnd; i >= 0; i -= 1) { if (items[i].uuid !== activeId && items[i].depth === depth) { - parentId = items[i].parentId; + ({ parentId } = items[i]); break; } } @@ -191,11 +199,10 @@ export function flattenTree(items: TreeItem[]): FlattenedTreeItem[] { export function buildTree(flattenedItems: FlattenedTreeItem[]): TreeItem[] { const root: TreeItem[] = []; - const nodes: Record<string, TreeItem> = {}; - - const sortedItems = [...flattenedItems].sort((a, b) => a.depth - b.depth); + const nodes = new Map<string, TreeItem>(); - for (const item of sortedItems) { + // First pass: create all nodes + for (const item of flattenedItems) { const { uuid, type, name, description } = item; const treeItem: TreeItem = @@ -213,17 +220,18 @@ export function buildTree(flattenedItems: FlattenedTreeItem[]): TreeItem[] { name, } as DatasourceFolderItem); - nodes[uuid] = treeItem; + nodes.set(uuid, treeItem); } - for (const item of sortedItems) { + // Second pass: link children to parents (iteration order preserves structure) + for (const item of flattenedItems) { const { uuid, parentId } = item; - const treeItem = nodes[uuid]; + const treeItem = nodes.get(uuid)!; if (!parentId) { root.push(treeItem); } else { - const parent = nodes[parentId]; + const parent = nodes.get(parentId); if ( parent && parent.type === FoldersEditorItemType.Folder && @@ -267,12 +275,12 @@ export function removeChildrenOf( items: FlattenedTreeItem[], ids: UniqueIdentifier[], ): FlattenedTreeItem[] { - const excludeParentIds = [...ids]; + const excludeParentIds = new Set<UniqueIdentifier>(ids); return items.filter(item => { - if (item.parentId && excludeParentIds.includes(item.parentId)) { + if (item.parentId && excludeParentIds.has(item.parentId)) { if (item.children?.length) { - excludeParentIds.push(item.uuid); + excludeParentIds.add(item.uuid); } return false; }
