You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by kg...@apache.org on 2026/01/29 16:27:09 UTC
(superset) 05/07: Perf improvements
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 <ka...@gmail.com>
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;
}