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 6ca88f7525f79b0958febf2a36cc6ec860136c4f Author: Kamil Gabryjelski <[email protected]> AuthorDate: Thu Jan 29 15:28:09 2026 +0100 fix folder highlight --- .../Datasource/FoldersEditor/TreeItem.styles.ts | 5 +- .../FoldersEditor/hooks/useDragHandlers.ts | 129 ++++++++++++++++++++- .../FoldersEditor/hooks/useItemHeights.ts | 4 +- 3 files changed, 130 insertions(+), 8 deletions(-) diff --git a/superset-frontend/src/components/Datasource/FoldersEditor/TreeItem.styles.ts b/superset-frontend/src/components/Datasource/FoldersEditor/TreeItem.styles.ts index 04a90687684..ce46739efb9 100644 --- a/superset-frontend/src/components/Datasource/FoldersEditor/TreeItem.styles.ts +++ b/superset-frontend/src/components/Datasource/FoldersEditor/TreeItem.styles.ts @@ -74,7 +74,8 @@ export const TreeFolderContainer = styled(TreeItemContainer)<{ ${({ theme, depth, isForbiddenDropTarget, isOverlay }) => ` margin-top: 0; margin-bottom: 0; - padding-top: ${theme.marginXS}px; + padding-top: ${theme.paddingSM}px; + padding-bottom: ${theme.paddingSM}px; margin-left: ${depth * FOLDER_INDENTATION_WIDTH}px; border-radius: ${theme.borderRadius}px; padding-left: ${theme.paddingSM}px; @@ -186,7 +187,7 @@ export const EmptyFolderDropZone = styled.div<{ isForbidden: boolean; }>` ${({ theme, depth, isOver, isForbidden }) => css` - margin: 0 ${depth * ITEM_INDENTATION_WIDTH + theme.marginMD}px; + margin: ${theme.marginXS}px ${depth * ITEM_INDENTATION_WIDTH + theme.marginMD}px 0; padding: ${theme.paddingLG}px; border: 2px dashed ${isOver diff --git a/superset-frontend/src/components/Datasource/FoldersEditor/hooks/useDragHandlers.ts b/superset-frontend/src/components/Datasource/FoldersEditor/hooks/useDragHandlers.ts index 06a38c8e6dc..8b90c961e13 100644 --- a/superset-frontend/src/components/Datasource/FoldersEditor/hooks/useDragHandlers.ts +++ b/superset-frontend/src/components/Datasource/FoldersEditor/hooks/useDragHandlers.ts @@ -34,6 +34,7 @@ import { TreeItem as TreeItemType, FlattenedTreeItem, DRAG_INDENTATION_WIDTH, + MAX_DEPTH, } from '../constants'; import { buildTree, getProjection, serializeForAPI } from '../treeUtils'; @@ -302,6 +303,65 @@ export function useDragHandlers({ 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; + const maxFolderDescendantDepth = getMaxFolderDescendantDepth( + draggedItem.uuid, + currentDepth, + ); + const descendantDepthOffset = maxFolderDescendantDepth - currentDepth; + const newDepth = projectedPosition.depth; + const newMaxDescendantDepth = newDepth + descendantDepthOffset; + + // MAX_DEPTH is 3, meaning we allow depths 0, 1, 2 (3 levels total) + if (newMaxDescendantDepth >= MAX_DEPTH) { + addWarningToast(t('Maximum folder nesting depth reached')); + return; + } + } + } + } + let newItems = fullFlattenedItems; if (projectedPosition) { @@ -477,12 +537,48 @@ 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 = fullFlattenedItems.find( - (i: FlattenedTreeItem) => i.uuid === id, - ); + const item = itemsByUuid.get(id); if (item) { draggedTypes.add(item.type); if ( @@ -491,9 +587,22 @@ export function useDragHandlers({ ) { hasDraggedDefaultFolder = true; } + // Track the deepest folder descendant offset for dragged folders + if (item.type === FoldersEditorItemType.Folder) { + const descendantOffset = getMaxFolderDescendantDepthOffset( + item.uuid, + item.depth, + ); + maxDraggedFolderDescendantOffset = Math.max( + maxDraggedFolderDescendantOffset, + descendantOffset, + ); + } } }); + const hasDraggedFolder = draggedTypes.has(FoldersEditorItemType.Folder); + fullFlattenedItems.forEach((item: FlattenedTreeItem) => { if (item.type !== FoldersEditorItemType.Folder) { return; @@ -513,7 +622,7 @@ export function useDragHandlers({ if ( (isDefaultMetricsFolder || isDefaultColumnsFolder) && - draggedTypes.has(FoldersEditorItemType.Folder) + hasDraggedFolder ) { forbidden.add(item.uuid); return; @@ -531,6 +640,18 @@ export function useDragHandlers({ draggedTypes.has(FoldersEditorItemType.Metric) ) { forbidden.add(item.uuid); + return; + } + + // Check max depth for folders: dropping into this folder would put the item at depth + 1 + // If that would exceed MAX_DEPTH - 1 (accounting for descendants), forbid it + if (hasDraggedFolder) { + const newFolderDepth = item.depth + 1; + const newMaxDescendantDepth = + newFolderDepth + maxDraggedFolderDescendantOffset; + if (newMaxDescendantDepth >= MAX_DEPTH) { + forbidden.add(item.uuid); + } } }); diff --git a/superset-frontend/src/components/Datasource/FoldersEditor/hooks/useItemHeights.ts b/superset-frontend/src/components/Datasource/FoldersEditor/hooks/useItemHeights.ts index cce59b7aeed..4654637b292 100644 --- a/superset-frontend/src/components/Datasource/FoldersEditor/hooks/useItemHeights.ts +++ b/superset-frontend/src/components/Datasource/FoldersEditor/hooks/useItemHeights.ts @@ -54,8 +54,8 @@ function calculateItemHeights(theme: SupersetTheme): ItemHeights { // The OptionControlContainer sets the actual content height const regularItem = 32; - // Folder header - same base height + small top margin for visual separation - const folderHeader = 32 + theme.marginXS; + // Folder header - base height + vertical padding (for taller highlight) + bottom spacing + const folderHeader = 32 + theme.paddingSM + theme.marginXS; // Separator visible: 1px line + vertical margins (marginSM above and below) const separatorVisible = 1 + theme.marginSM * 2;
