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;

Reply via email to