codeant-ai-for-open-source[bot] commented on code in PR #36239:
URL: https://github.com/apache/superset/pull/36239#discussion_r2754223528


##########
superset-frontend/packages/superset-ui-chart-controls/src/components/ColumnTypeLabel/ColumnTypeLabel.tsx:
##########
@@ -59,7 +60,9 @@ export function ColumnTypeLabel({ type }: 
ColumnTypeLabelProps) {
     <QuestionOutlined aria-label={t('unknown type icon')} />
   );
 
-  if (type === '' || type === 'expression') {
+  if (type === 'metric') {
+    typeIcon = <Icons.Sigma aria-label={t('metric type icon')} />;
+  } else if (type === '' || type === 'expression') {

Review Comment:
   **Suggestion:** The `ColumnTypeLabel` component is being called with the 
string value `aggregate` (e.g., from `AggregateOption`), but this case is not 
handled in the type switch, so aggregate functions fall back to the generic 
"unknown type" icon instead of a more appropriate function-style icon; treating 
`aggregate` like an expression fixes this logic mismatch and aligns the UI with 
caller intent. [logic error]
   
   <details>
   <summary><b>Severity Level:</b> Major ⚠️</summary>
   
   ```mdx
   - ❌ Aggregate icon shows as unknown in Metric aggregate UI.
   - ⚠️ Metric pickers and aggregate option displays inconsistent.
   - ⚠️ Tests referencing AggregateOption may assert wrong icon.
   ```
   </details>
   
   ```suggestion
     } else if (type === '' || type === 'expression' || type === 'aggregate') {
   ```
   <details>
   <summary><b>Steps of Reproduction ✅ </b></summary>
   
   ```mdx
   1. Open the file
   
superset-frontend/src/explore/components/controls/MetricControl/AggregateOption.tsx
 and
   inspect line 33 where the component renders the type icon: `{showType && 
<ColumnTypeLabel
   type={'aggregate' as any} />}` (file and line discovered via Grep:
   "src/explore/components/controls/MetricControl/AggregateOption.tsx:33: 
{showType &&
   <ColumnTypeLabel type={'aggregate' as any} />}" ).
   
   2. With the current PR code, when AggregateOption renders, it passes the 
literal string
   'aggregate' into ColumnTypeLabel (as above). ColumnTypeLabel implementation 
(file:
   
superset-frontend/packages/superset-ui-chart-controls/src/components/ColumnTypeLabel/ColumnTypeLabel.tsx)
   checks for '' and 'expression' but does not check for 'aggregate' (existing 
code at lines
   shown in PR: "65 } else if (type === '' || type === 'expression') {" and "67 
typeIcon =
   <FunctionOutlined aria-label={t('function type icon')} />;").
   
   3. Render the UI that shows AggregateOption (e.g., open the metric selection 
UI or run the
   unit test that mounts AggregateOption — tests referencing AggregateOption 
exist at
   
"src/explore/components/controls/MetricControl/AggregateOption.test.tsx:25"). 
Observe that
   instead of the function-style icon, the ColumnTypeLabel falls back to the 
default
   QuestionOutlined unknown icon (because 'aggregate' does not match any 
handled branch).
   
   4. Apply the proposed change (treat 'aggregate' like 'expression') and 
re-run the same
   render (UI or test). Confirm the FunctionOutlined (function-style) icon now 
appears for
   AggregateOption ('aggregate'), matching caller intent seen in 
AggregateOption.tsx.
   ```
   </details>
   <details>
   <summary><b>Prompt for AI Agent 🤖 </b></summary>
   
   ```mdx
   This is a comment left during a code review.
   
   **Path:** 
superset-frontend/packages/superset-ui-chart-controls/src/components/ColumnTypeLabel/ColumnTypeLabel.tsx
   **Line:** 65:65
   **Comment:**
        *Logic Error: The `ColumnTypeLabel` component is being called with the 
string value `aggregate` (e.g., from `AggregateOption`), but this case is not 
handled in the type switch, so aggregate functions fall back to the generic 
"unknown type" icon instead of a more appropriate function-style icon; treating 
`aggregate` like an expression fixes this logic mismatch and aligns the UI with 
caller intent.
   
   Validate the correctness of the flagged issue. If correct, How can I resolve 
this? If you propose a fix, implement it and please make it concise.
   ```
   </details>



##########
superset-frontend/src/components/Datasource/FoldersEditor/VirtualizedTreeItem.tsx:
##########
@@ -0,0 +1,220 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { CSSProperties, memo } from 'react';
+import type { ListChildComponentProps } from 'react-window';
+import { useDroppable } from '@dnd-kit/core';
+import type { UniqueIdentifier } from '@dnd-kit/core';
+import type { Metric, ColumnMeta } from '@superset-ui/chart-controls';
+import { FoldersEditorItemType } from '../types';
+import type { FlattenedTreeItem } from './constants';
+import { isDefaultFolder } from './constants';
+import { TreeItem } from './TreeItem';
+
+// Invisible placeholder that keeps the droppable area for horizontal drag 
depth changes
+interface DragPlaceholderProps {
+  id: string;
+  style: CSSProperties;
+  type: FoldersEditorItemType;
+  isFolder: boolean;
+}
+
+const DragPlaceholder = memo(function DragPlaceholder({
+  id,
+  style,
+  type,
+  isFolder,
+}: DragPlaceholderProps) {
+  const { setNodeRef } = useDroppable({
+    id,
+    data: { type, isFolder },
+  });
+
+  return <div ref={setNodeRef} style={{ ...style, visibility: 'hidden' }} />;
+});
+
+export interface VirtualizedTreeItemData {
+  flattenedItems: FlattenedTreeItem[];
+  collapsedIds: Set<string>;
+  selectedItemIds: Set<string>;
+  editingFolderId: string | null;
+  folderChildCounts: Map<string, number>;
+  itemSeparatorInfo: Map<string, 'visible' | 'transparent'>;
+  visibleItemIds: Set<string>;
+  searchTerm: string;
+  metricsMap: Map<string, Metric>;
+  columnsMap: Map<string, ColumnMeta>;
+  activeId: UniqueIdentifier | null;
+  forbiddenDropFolderIds: Set<string>;
+  currentDropTargetId: string | null;
+  onToggleCollapse: (id: string) => void;
+  onSelect: (id: string, selected: boolean, shiftKey?: boolean) => void;
+  onStartEdit: (id: string) => void;
+  onFinishEdit: (id: string, newName: string) => void;
+}
+
+// Inner component that receives state as props for proper memoization
+interface TreeItemWrapperProps {
+  item: FlattenedTreeItem;
+  style: CSSProperties;
+  isFolder: boolean;
+  isCollapsed: boolean;
+  isSelected: boolean;
+  isEditing: boolean;
+  showEmptyState: boolean;
+  separatorType?: 'visible' | 'transparent';
+  isForbiddenDrop: boolean;
+  isDropTarget: boolean;
+  metric?: Metric;
+  column?: ColumnMeta;
+  onToggleCollapse?: (id: string) => void;
+  onSelect?: (id: string, selected: boolean, shiftKey?: boolean) => void;
+  onStartEdit?: (id: string) => void;
+  onFinishEdit?: (id: string, newName: string) => void;
+}
+
+const TreeItemWrapper = memo(function TreeItemWrapper({
+  item,
+  style,
+  isFolder,
+  isCollapsed,
+  isSelected,
+  isEditing,
+  showEmptyState,
+  separatorType,
+  isForbiddenDrop,
+  isDropTarget,
+  metric,
+  column,
+  onToggleCollapse,
+  onSelect,
+  onStartEdit,
+  onFinishEdit,
+}: TreeItemWrapperProps) {
+  return (
+    <div style={style}>
+      <TreeItem
+        id={item.uuid}
+        type={item.type}
+        name={item.name}
+        depth={item.depth}
+        isFolder={isFolder}
+        isCollapsed={isCollapsed}
+        isSelected={isSelected}
+        isEditing={isEditing}
+        isDefaultFolder={isDefaultFolder(item.uuid)}
+        showEmptyState={showEmptyState}
+        separatorType={separatorType}
+        isForbiddenDrop={isForbiddenDrop}
+        isDropTarget={isDropTarget}
+        onToggleCollapse={onToggleCollapse}
+        onSelect={onSelect}
+        onStartEdit={onStartEdit}
+        onFinishEdit={onFinishEdit}
+        metric={metric}
+        column={column}
+      />
+    </div>
+  );
+});
+
+function VirtualizedTreeItemComponent({
+  index,
+  style,
+  data,
+}: ListChildComponentProps<VirtualizedTreeItemData>) {
+  const {
+    flattenedItems,
+    collapsedIds,
+    selectedItemIds,
+    editingFolderId,
+    folderChildCounts,
+    itemSeparatorInfo,
+    visibleItemIds,
+    searchTerm,
+    metricsMap,
+    columnsMap,
+    activeId,
+    forbiddenDropFolderIds,
+    currentDropTargetId,
+    onToggleCollapse,
+    onSelect,
+    onStartEdit,
+    onFinishEdit,
+  } = data;
+
+  const item = flattenedItems[index];
+
+  if (!item) {
+    return null;
+  }
+
+  const isFolder = item.type === FoldersEditorItemType.Folder;
+
+  // Hide items that don't match search (unless they're folders)
+  if (!isFolder && searchTerm && !visibleItemIds.has(item.uuid)) {
+    return null;
+  }
+
+  // Render invisible placeholder for active dragged item - keeps droppable 
area
+  // for horizontal drag depth changes while visual is in DragOverlay
+  if (activeId === item.uuid) {
+    return (
+      <DragPlaceholder
+        id={item.uuid}
+        style={style}
+        type={item.type}
+        isFolder={isFolder}
+      />
+    );

Review Comment:
   **Suggestion:** The search filter check runs before the `activeId` 
placeholder check, so if the user starts dragging a metric/column and then 
changes the search so that the dragged item no longer matches, the active item 
row is short-circuited by the search filter and returns `null`. This removes 
the droppable placeholder for the active item during an ongoing drag, which can 
break horizontal drag-depth adjustments and drop behavior for that item. Move 
the `activeId` check before the search filter so the placeholder is always 
rendered for the active item, even if it no longer matches the current search. 
[logic error]
   
   <details>
   <summary><b>Severity Level:</b> Major ⚠️</summary>
   
   ```mdx
   - ❌ Dataset modal Folders tab drag-and-drop breaks mid-drag.
   - ⚠️ Horizontal drag-depth adjustments behave inconsistently.
   - ⚠️ Moving metrics/columns between folders unreliable.
   - ⚠️ UX regression during search while dragging items.
   ```
   </details>
   
   ```suggestion
     // Render invisible placeholder for active dragged item - keeps droppable 
area
     // for horizontal drag depth changes while visual is in DragOverlay
     if (activeId === item.uuid) {
       return (
         <DragPlaceholder
           id={item.uuid}
           style={style}
           type={item.type}
           isFolder={isFolder}
         />
       );
     }
   
     // Hide items that don't match search (unless they're folders)
     if (!isFolder && searchTerm && !visibleItemIds.has(item.uuid)) {
       return null;
   ```
   <details>
   <summary><b>Steps of Reproduction ✅ </b></summary>
   
   ```mdx
   1. Enable the DATASET_FOLDERS feature flag and open the dataset modal 
Folders tab (UI
   entry described in PR). The UI uses VirtualizedTreeItemComponent at
   
superset-frontend/src/components/Datasource/FoldersEditor/VirtualizedTreeItem.tsx.
   
   2. Start dragging a metric or column item so that 
VirtualizedTreeItemComponent renders the
   dragged item; the activeId prop is set to the dragged item's uuid (see code 
at line
   168-186 in the same file).
   
   3. While the drag is ongoing, type a search that filters out the dragged 
item (so
   visibleItemIds no longer contains item.uuid). With the existing code (lines 
168-186), the
   component runs the search-filter check first and returns null, removing the 
row and its
   droppable placeholder.
   
   4. Observe that the DragPlaceholder (the invisible droppable node created 
when activeId
   === item.uuid) is not rendered and horizontal drag-depth adjustments or drop 
behavior may
   fail or behave inconsistently.
   
   5. The suggested change moves the activeId check before the search filter 
(improved code),
   ensuring the DragPlaceholder remains present during an active drag even if 
the search
   would otherwise hide the item.
   ```
   </details>
   <details>
   <summary><b>Prompt for AI Agent 🤖 </b></summary>
   
   ```mdx
   This is a comment left during a code review.
   
   **Path:** 
superset-frontend/src/components/Datasource/FoldersEditor/VirtualizedTreeItem.tsx
   **Line:** 170:185
   **Comment:**
        *Logic Error: The search filter check runs before the `activeId` 
placeholder check, so if the user starts dragging a metric/column and then 
changes the search so that the dragged item no longer matches, the active item 
row is short-circuited by the search filter and returns `null`. This removes 
the droppable placeholder for the active item during an ongoing drag, which can 
break horizontal drag-depth adjustments and drop behavior for that item. Move 
the `activeId` check before the search filter so the placeholder is always 
rendered for the active item, even if it no longer matches the current search.
   
   Validate the correctness of the flagged issue. If correct, How can I resolve 
this? If you propose a fix, implement it and please make it concise.
   ```
   </details>



##########
superset-frontend/src/components/Datasource/FoldersEditor/hooks/useDragHandlers.ts:
##########
@@ -0,0 +1,663 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { useCallback, useMemo, useRef, useState } from 'react';
+import { t } from '@apache-superset/core';
+import {
+  UniqueIdentifier,
+  DragStartEvent,
+  DragMoveEvent,
+  DragEndEvent,
+  DragOverEvent,
+} from '@dnd-kit/core';
+import { FoldersEditorItemType } from '../../types';
+import {
+  isDefaultFolder,
+  DEFAULT_COLUMNS_FOLDER_UUID,
+  DEFAULT_METRICS_FOLDER_UUID,
+  TreeItem as TreeItemType,
+  FlattenedTreeItem,
+  DRAG_INDENTATION_WIDTH,
+  MAX_DEPTH,
+} from '../constants';
+import { buildTree, getProjection, serializeForAPI } from '../treeUtils';
+
+interface UseDragHandlersProps {
+  setItems: React.Dispatch<React.SetStateAction<TreeItemType[]>>;
+  computeFlattenedItems: (
+    activeId: UniqueIdentifier | null,
+  ) => FlattenedTreeItem[];
+  fullFlattenedItems: FlattenedTreeItem[];
+  selectedItemIds: Set<string>;
+  onChange: (folders: ReturnType<typeof serializeForAPI>) => void;
+  addWarningToast: (message: string) => void;
+}
+
+export function useDragHandlers({
+  setItems,
+  computeFlattenedItems,
+  fullFlattenedItems,
+  selectedItemIds,
+  onChange,
+  addWarningToast,
+}: UseDragHandlersProps) {
+  const [activeId, setActiveId] = useState<UniqueIdentifier | null>(null);
+  const [overId, setOverId] = useState<UniqueIdentifier | null>(null);
+  const [dragOverlayWidth, setDragOverlayWidth] = useState<number | 
null>(null);
+  const offsetLeftRef = useRef(0);
+  // Track current drop target - use state so virtualized items can render 
correctly
+  const [currentDropTargetId, setCurrentDropTargetId] = useState<string | 
null>(
+    null,
+  );
+  const [draggedItemIds, setDraggedItemIds] = useState<Set<string>>(new Set());
+
+  // Store the flattened items at drag start to keep them stable during drag
+  // This prevents react-window from re-rendering due to flattenedItems 
reference changes
+  const dragStartFlattenedItemsRef = useRef<FlattenedTreeItem[] | null>(null);
+
+  // Compute flattened items, but during drag use the stable snapshot from 
drag start
+  // This prevents react-window from re-rendering/re-measuring when 
flattenedItems changes
+  const computedFlattenedItems = useMemo(
+    () => computeFlattenedItems(activeId),
+    [computeFlattenedItems, activeId],
+  );
+
+  // Use stable items during drag to prevent scroll jumping
+  // Memoize to avoid creating new array references on every render
+  const flattenedItems = useMemo(
+    () =>
+      activeId && dragStartFlattenedItemsRef.current
+        ? dragStartFlattenedItemsRef.current
+        : computedFlattenedItems,
+    [activeId, computedFlattenedItems],
+  );
+
+  const flattenedItemsIndexMap = useMemo(() => {
+    const map = new Map<string, number>();
+    flattenedItems.forEach((item, index) => {
+      map.set(item.uuid, index);
+    });
+    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);
+    offsetLeftRef.current = 0;
+    setCurrentDropTargetId(null);
+    setDraggedItemIds(new Set());
+    setDragOverlayWidth(null);
+    // Clear the stable snapshot so next render uses fresh computed items
+    dragStartFlattenedItemsRef.current = null;
+  }, []);
+
+  const handleDragStart = ({ active }: DragStartEvent) => {
+    // Capture the current flattened items BEFORE setting activeId
+    // This ensures the list stays stable during the entire drag operation
+    dragStartFlattenedItemsRef.current = computeFlattenedItems(null);
+
+    setActiveId(active.id);
+
+    const element = active.rect.current.initial;
+    if (element) {
+      setDragOverlayWidth(element.width);
+    }
+
+    if (selectedItemIds.has(active.id as string)) {
+      setDraggedItemIds(new Set(selectedItemIds));
+    } else {
+      setDraggedItemIds(new Set([active.id as string]));
+    }
+  };
+
+  const handleDragMove = useCallback(
+    ({ delta }: DragMoveEvent) => {
+      offsetLeftRef.current = delta.x;
+
+      if (activeId && overId) {
+        if (typeof overId === 'string' && overId.endsWith('-empty')) {
+          const folderId = overId.replace('-empty', '');
+          setCurrentDropTargetId(folderId);
+          return;
+        }
+
+        const projection = getProjection(
+          flattenedItems,
+          activeId,
+          overId,
+          delta.x,
+          DRAG_INDENTATION_WIDTH,
+          flattenedItemsIndexMap,
+        );
+        const newParentId = projection?.parentId ?? null;
+        setCurrentDropTargetId(newParentId);
+      }
+    },
+    [activeId, overId, flattenedItems, flattenedItemsIndexMap],
+  );
+
+  const handleDragOver = useCallback(
+    ({ over }: DragOverEvent) => {
+      setOverId(over?.id ?? null);
+
+      if (activeId && over) {
+        if (typeof over.id === 'string' && over.id.endsWith('-empty')) {
+          const folderId = over.id.replace('-empty', '');
+          setCurrentDropTargetId(folderId);
+          return;
+        }
+
+        const projection = getProjection(
+          flattenedItems,
+          activeId,
+          over.id,
+          offsetLeftRef.current,
+          DRAG_INDENTATION_WIDTH,
+          flattenedItemsIndexMap,
+        );
+        const newParentId = projection?.parentId ?? null;
+        setCurrentDropTargetId(newParentId);
+      } else {
+        setCurrentDropTargetId(null);
+      }
+    },
+    [activeId, flattenedItems, flattenedItemsIndexMap],
+  );
+
+  const handleDragEnd = ({ active, over }: DragEndEvent) => {
+    const itemsBeingDragged = Array.from(draggedItemIds);
+    const finalOffsetLeft = offsetLeftRef.current;
+    resetDragState();
+
+    if (!over || itemsBeingDragged.length === 0) {
+      return;
+    }
+
+    let targetOverId = over.id;
+    let isEmptyDrop = false;
+    if (typeof over.id === 'string' && over.id.endsWith('-empty')) {
+      targetOverId = over.id.replace('-empty', '');
+      isEmptyDrop = true;
+
+      if (itemsBeingDragged.includes(targetOverId as string)) {
+        return;
+      }
+    }
+
+    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) =>
+      itemsBeingDraggedSet.has(item.uuid),
+    );
+
+    let projectedPosition = getProjection(
+      flattenedItems,
+      active.id,
+      targetOverId,
+      finalOffsetLeft,
+      DRAG_INDENTATION_WIDTH,
+      flattenedItemsIndexMap,
+    );
+
+    if (isEmptyDrop) {
+      const targetFolder = fullFlattenedItems[overIndex];
+      projectedPosition = {
+        depth: targetFolder.depth + 1,
+        maxDepth: targetFolder.depth + 1,
+        minDepth: targetFolder.depth + 1,
+        parentId: targetOverId as string,
+      };
+    }
+
+    const activeItem = fullFlattenedItems[activeIndex];
+    if (active.id === targetOverId) {
+      const newParentId = projectedPosition?.parentId ?? null;
+      const currentParentId = activeItem.parentId;
+      if (newParentId === currentParentId) {
+        return;
+      }
+    }
+
+    // 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;
+      }
+    }
+
+    if (projectedPosition && projectedPosition.parentId) {
+      const targetFolder = fullItemsByUuid.get(projectedPosition.parentId);
+
+      if (targetFolder && isDefaultFolder(targetFolder.uuid)) {
+        const isDefaultMetricsFolder =
+          targetFolder.uuid === DEFAULT_METRICS_FOLDER_UUID;
+        const isDefaultColumnsFolder =
+          targetFolder.uuid === DEFAULT_COLUMNS_FOLDER_UUID;
+
+        for (const draggedItem of draggedItems) {
+          if (draggedItem.type === FoldersEditorItemType.Folder) {
+            addWarningToast(t('Cannot nest folders in default folders'));
+            return;
+          }
+          if (
+            isDefaultMetricsFolder &&
+            draggedItem.type === FoldersEditorItemType.Column
+          ) {
+            addWarningToast(t('This folder only accepts metrics'));
+            return;
+          }
+          if (
+            isDefaultColumnsFolder &&
+            draggedItem.type === FoldersEditorItemType.Metric
+          ) {
+            addWarningToast(t('This folder only accepts columns'));
+            return;
+          }
+        }
+      }
+    }
+
+    if (hasDraggedDefaultFolder && projectedPosition?.parentId) {
+      addWarningToast(t('Default folders cannot be nested'));
+      return;
+    }
+
+    // Check max depth for folders
+    if (hasDraggedFolder && projectedPosition) {
+      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) {
+      const depthChange = projectedPosition.depth - activeItem.depth;
+
+      const itemsToUpdate = new Map<
+        string,
+        { depth: number; parentId: string | null | undefined }
+      >();
+
+      draggedItems.forEach((item: FlattenedTreeItem) => {
+        if (item.uuid === active.id) {
+          itemsToUpdate.set(item.uuid, {
+            depth: projectedPosition.depth,
+            parentId: projectedPosition.parentId,
+          });
+        } else {

Review Comment:
   **Suggestion:** When dragging multiple items, including a folder and items 
(or subfolders) inside that folder, the current logic sets the same parentId 
for every dragged item, which flattens the hierarchy by turning children into 
siblings of the folder instead of keeping them nested; this will cause folder 
contents to "fall out" of their folder after a multi-select drag, contradicting 
the expected behavior of moving whole subtrees together. [logic error]
   
   <details>
   <summary><b>Severity Level:</b> Critical 🚨</summary>
   
   ```mdx
   - ❌ Dataset Folders editor breaks subtree moves.
   - ⚠️ Folder nesting lost after multi-select drag.
   - ⚠️ Explore dataset panel shows incorrect folder structure.
   ```
   </details>
   
   ```suggestion
           // Precompute set of dragged IDs to detect items whose parent is 
also being dragged.
           const draggedItemIdSet = new Set(draggedItems.map(item => 
item.uuid));
   
           draggedItems.forEach((item: FlattenedTreeItem) => {
             // Active item always moves to the projected parent.
             if (item.uuid === active.id) {
               itemsToUpdate.set(item.uuid, {
                 depth: projectedPosition.depth,
                 parentId: projectedPosition.parentId,
               });
               return;
             }
   
             const isChildOfDraggedItem =
               item.parentId != null && draggedItemIdSet.has(item.parentId);
   
             // If this item is inside another dragged folder, keep its 
parentId so the
             // folder hierarchy is preserved and only adjust depth.
             if (isChildOfDraggedItem) {
               itemsToUpdate.set(item.uuid, {
                 depth: item.depth + depthChange,
                 parentId: undefined,
               });
             } else {
               // Standalone dragged items (e.g. metrics or top-level folders)
               // should move under the same parent as the active item.
   ```
   <details>
   <summary><b>Steps of Reproduction ✅ </b></summary>
   
   ```mdx
   1. Open the dataset Folders editor UI and select multiple items that include 
a folder and
   one or more items contained inside that folder (e.g. select Folder A and 
Column X where
   Column X is a child of Folder A). This selection triggers the drag flow 
implemented in
   handleDragStart -> handleDragEnd in useDragHandlers.ts (entry: handleDragEnd 
at
   
superset-frontend/src/components/Datasource/FoldersEditor/hooks/useDragHandlers.ts:235).
   
   2. Begin a multi-select drag and drop operation so handleDragEnd executes. 
The function
   computes projectedPosition and depthChange, then reaches the itemsToUpdate 
population code
   at lines
   
superset-frontend/src/components/Datasource/FoldersEditor/hooks/useDragHandlers.ts:381-397
   (the existing_code block).
   
   3. The current logic (existing_code) assigns projectedPosition.parentId to 
every dragged
   item that is not the active item, regardless of whether that item is 
actually a descendant
   of another dragged folder. This converts nested children into siblings under 
the new
   parent (flattening the subtree). Observe resulting newTree created at 
buildTree(...) and
   applied via setItems/onChange (lines ~531-533).
   
   4. In the UI after drop, the previously nested items will appear as siblings 
of the
   dragged folder (their nesting is lost). Reproduce by dragging a folder with 
nested
   subfolders + columns — verify children are no longer nested beneath their 
original folder
   in the dataset panel in Explore view.
   
   Explanation: The change proposed in improved_code preserves parent-child 
relationships
   among dragged items by detecting when a dragged item's parent is also 
dragged and avoiding
   overwriting its parentId, therefore keeping subtrees intact. The current 
implementation
   (existing_code) flattens nested items because it unconditionally sets 
parentId to
   projectedPosition.parentId for all dragged items except the active item.
   ```
   </details>
   <details>
   <summary><b>Prompt for AI Agent 🤖 </b></summary>
   
   ```mdx
   This is a comment left during a code review.
   
   **Path:** 
superset-frontend/src/components/Datasource/FoldersEditor/hooks/useDragHandlers.ts
   **Line:** 386:392
   **Comment:**
        *Logic Error: When dragging multiple items, including a folder and 
items (or subfolders) inside that folder, the current logic sets the same 
parentId for every dragged item, which flattens the hierarchy by turning 
children into siblings of the folder instead of keeping them nested; this will 
cause folder contents to "fall out" of their folder after a multi-select drag, 
contradicting the expected behavior of moving whole subtrees together.
   
   Validate the correctness of the flagged issue. If correct, How can I resolve 
this? If you propose a fix, implement it and please make it concise.
   ```
   </details>



##########
superset-frontend/src/components/Datasource/FoldersEditor/hooks/useAutoScroll.ts:
##########
@@ -0,0 +1,191 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { useEffect, useRef } from 'react';
+import type { VariableSizeList as List } from 'react-window';
+
+// Distance from edge where auto-scroll activates (in pixels)
+const SCROLL_THRESHOLD = 80;
+// Scroll speed (pixels per 16ms frame at ~60fps)
+const BASE_SCROLL_SPEED = 8;
+// Maximum scroll speed multiplier when at the very edge
+const MAX_SPEED_MULTIPLIER = 3;
+
+interface UseAutoScrollOptions {
+  listRef: React.RefObject<List>;
+  containerRef: React.RefObject<HTMLDivElement>;
+  isDragging: boolean;
+  listHeight: number;
+}
+
+/**
+ * Custom auto-scroll hook for virtualized lists during drag operations.
+ * This replaces dnd-kit's built-in auto-scroll which conflicts with 
virtualization.
+ *
+ * When the user drags near the top or bottom edge of the list container,
+ * this hook smoothly scrolls the react-window list.
+ */
+export function useAutoScroll({
+  listRef,
+  containerRef,
+  isDragging,
+  listHeight,
+}: UseAutoScrollOptions) {
+  // Use refs for all mutable state to avoid re-creating callbacks
+  const scrollStateRef = useRef({
+    direction: null as 'up' | 'down' | null,
+    speed: 0,
+    mouseY: 0,
+    rafId: null as number | null,
+    lastTime: 0,
+    isScrolling: false,
+  });
+
+  useEffect(() => {
+    if (!isDragging) {
+      // Clean up when not dragging
+      const state = scrollStateRef.current;
+      if (state.rafId !== null) {
+        cancelAnimationFrame(state.rafId);
+        state.rafId = null;
+      }
+      state.direction = null;
+      state.speed = 0;
+      return;
+    }
+
+    const state = scrollStateRef.current;
+
+    // Calculate scroll parameters based on mouse position
+    const updateScrollParams = () => {
+      const container = containerRef.current;
+      if (!container) return;
+
+      const containerRect = container.getBoundingClientRect();
+      const relativeY = state.mouseY - containerRect.top;
+
+      // Near top edge - scroll up
+      if (relativeY < SCROLL_THRESHOLD && relativeY >= 0) {
+        const proximity = 1 - relativeY / SCROLL_THRESHOLD;
+        state.direction = 'up';
+        state.speed =
+          BASE_SCROLL_SPEED * (1 + proximity * (MAX_SPEED_MULTIPLIER - 1));
+        return;
+      }
+
+      // Near bottom edge - scroll down
+      if (
+        relativeY > listHeight - SCROLL_THRESHOLD &&
+        relativeY <= listHeight
+      ) {
+        const distanceFromBottom = listHeight - relativeY;
+        const proximity = 1 - distanceFromBottom / SCROLL_THRESHOLD;
+        state.direction = 'down';
+        state.speed =
+          BASE_SCROLL_SPEED * (1 + proximity * (MAX_SPEED_MULTIPLIER - 1));
+        return;
+      }
+
+      // Not in scroll zone
+      state.direction = null;
+      state.speed = 0;
+    };
+
+    // Animation frame callback - uses time-based scrolling for consistent 
speed
+    const scrollFrame = (currentTime: number) => {
+      const list = listRef.current;
+      const outerElement = (list as any)?._outerRef;
+
+      if (!list || !outerElement || !state.direction) {
+        // Restore pointer events when scrolling stops
+        const container = containerRef.current;
+        if (container && state.isScrolling) {
+          container.style.pointerEvents = '';
+          state.isScrolling = false;
+        }
+        state.rafId = null;
+        return;
+      }
+
+      // Disable pointer events during scroll to prevent hover recalculations
+      const container = containerRef.current;
+      if (container && !state.isScrolling) {
+        container.style.pointerEvents = 'none';
+        state.isScrolling = true;
+      }
+
+      // Calculate time delta for frame-rate independent scrolling
+      const deltaTime = state.lastTime
+        ? (currentTime - state.lastTime) / 16
+        : 1;
+      state.lastTime = currentTime;
+
+      const currentScroll = outerElement.scrollTop;
+      const maxScroll = outerElement.scrollHeight - outerElement.clientHeight;
+      const scrollAmount = state.speed * deltaTime;
+
+      let newScroll = currentScroll;
+      if (state.direction === 'up') {
+        newScroll = Math.max(0, currentScroll - scrollAmount);
+      } else if (state.direction === 'down') {
+        newScroll = Math.min(maxScroll, currentScroll + scrollAmount);
+      }
+
+      if (Math.abs(newScroll - currentScroll) > 0.5) {
+        // Use direct DOM manipulation for smoother scrolling
+        // react-window's scrollTo triggers re-renders which can cause stutter
+        outerElement.scrollTop = newScroll;
+      }
+
+      // Continue animation loop
+      state.rafId = requestAnimationFrame(scrollFrame);
+    };
+
+    // Handle mouse move - just update position, let animation loop handle 
scrolling
+    const handleMouseMove = (event: MouseEvent) => {
+      state.mouseY = event.clientY;
+      updateScrollParams();

Review Comment:
   **Suggestion:** The auto-scroll logic only considers the vertical mouse 
position, so any drag whose Y coordinate overlaps the list's vertical range 
will trigger scrolling even when the pointer is horizontally far away from the 
list (e.g., over a different panel), causing unrelated lists to scroll while 
dragging; adding a horizontal bounds check around the container prevents this 
unintended behavior. [logic error]
   
   <details>
   <summary><b>Severity Level:</b> Major ⚠️</summary>
   
   ```mdx
   - ⚠️ Unintended list scrolling during drag across panels.
   - ❌ Folders editor drag UX becomes confusing.
   - ⚠️ Adjacent panels may appear to move unexpectedly.
   ```
   </details>
   
   ```suggestion
       const updateScrollParams = (mouseX: number, mouseY: number) => {
         const container = containerRef.current;
         if (!container) return;
   
         const containerRect = container.getBoundingClientRect();
         const { top, bottom, left, right } = containerRect;
   
         // If the cursor is outside the container horizontally or vertically,
         // do not auto-scroll this list.
         if (
           mouseX < left ||
           mouseX > right ||
           mouseY < top ||
           mouseY > bottom
         ) {
           state.direction = null;
           state.speed = 0;
           return;
         }
   
         const relativeY = mouseY - top;
   
         // Near top edge - scroll up
         if (relativeY < SCROLL_THRESHOLD && relativeY >= 0) {
           const proximity = 1 - relativeY / SCROLL_THRESHOLD;
           state.direction = 'up';
           state.speed =
             BASE_SCROLL_SPEED * (1 + proximity * (MAX_SPEED_MULTIPLIER - 1));
           return;
         }
   
         // Near bottom edge - scroll down
         if (
           relativeY > listHeight - SCROLL_THRESHOLD &&
           relativeY <= listHeight
         ) {
           const distanceFromBottom = listHeight - relativeY;
           const proximity = 1 - distanceFromBottom / SCROLL_THRESHOLD;
           state.direction = 'down';
           state.speed =
             BASE_SCROLL_SPEED * (1 + proximity * (MAX_SPEED_MULTIPLIER - 1));
           return;
         }
   
         // Not in scroll zone
         state.direction = null;
         state.speed = 0;
       };
   
       // Animation frame callback - uses time-based scrolling for consistent 
speed
       const scrollFrame = (currentTime: number) => {
         const list = listRef.current;
         const outerElement = (list as any)?._outerRef;
   
         if (!list || !outerElement || !state.direction) {
           // Restore pointer events when scrolling stops
           const container = containerRef.current;
           if (container && state.isScrolling) {
             container.style.pointerEvents = '';
             state.isScrolling = false;
           }
           state.rafId = null;
           return;
         }
   
         // Disable pointer events during scroll to prevent hover recalculations
         const container = containerRef.current;
         if (container && !state.isScrolling) {
           container.style.pointerEvents = 'none';
           state.isScrolling = true;
         }
   
         // Calculate time delta for frame-rate independent scrolling
         const deltaTime = state.lastTime
           ? (currentTime - state.lastTime) / 16
           : 1;
         state.lastTime = currentTime;
   
         const currentScroll = outerElement.scrollTop;
         const maxScroll = outerElement.scrollHeight - 
outerElement.clientHeight;
         const scrollAmount = state.speed * deltaTime;
   
         let newScroll = currentScroll;
         if (state.direction === 'up') {
           newScroll = Math.max(0, currentScroll - scrollAmount);
         } else if (state.direction === 'down') {
           newScroll = Math.min(maxScroll, currentScroll + scrollAmount);
         }
   
         if (Math.abs(newScroll - currentScroll) > 0.5) {
           // Use direct DOM manipulation for smoother scrolling
           // react-window's scrollTo triggers re-renders which can cause 
stutter
           outerElement.scrollTop = newScroll;
         }
   
         // Continue animation loop
         state.rafId = requestAnimationFrame(scrollFrame);
       };
   
       // Handle mouse move - just update position, let animation loop handle 
scrolling
       const handleMouseMove = (event: MouseEvent) => {
         updateScrollParams(event.clientX, event.clientY);
   ```
   <details>
   <summary><b>Steps of Reproduction ✅ </b></summary>
   
   ```mdx
   1. Open the dataset Folders editor which renders VirtualizedTreeList (file:
   
superset-frontend/src/components/Datasource/FoldersEditor/VirtualizedTreeList.tsx).
 The
   containerRef is created at VirtualizedTreeList.tsx:83-85 and applied to the 
wrapper div at
   VirtualizedTreeList.tsx:215.
   
   2. Start dragging an item in the Folders editor so isDragging becomes true 
(drag
   interactions handled by the Folders editor; VirtualizedTreeList passes 
isDragging into
   useAutoScroll at VirtualizedTreeList.tsx:86-92).
   
   3. While dragging, move the mouse vertically so its Y coordinate overlaps 
the list's
   vertical bounds, but keep the pointer horizontally outside the list (for 
example, over an
   adjacent panel). The current implementation only uses mouseY (see 
useAutoScroll.ts lines
   75-82 and handleMouseMove at 160-169) so updateScrollParams will treat the 
pointer as
   "near edge".
   
   4. The hook starts the animation loop (requestAnimationFrame at 
useAutoScroll.ts lines
   ~157-169) and manipulates the list's outerElement.scrollTop 
(useAutoScroll.ts lines
   139-154), causing the VirtualizedTreeList (the element referenced at
   VirtualizedTreeList.tsx:217) to scroll even though the pointer is not above 
it
   horizontally.
   
   5. Observe unintended scrolling of the list while the pointer is over a 
different panel.
   This reproduces the behavior described by the suggestion.
   
   Note: The reproduction is based on direct references in the code: 
useAutoScroll is
   imported and used in VirtualizedTreeList (VirtualizedTreeList.tsx:28 and 
86-92),
   containerRef is set on the wrapper div (VirtualizedTreeList.tsx:215), and 
useAutoScroll
   only reads event.clientY into state.mouseY (useAutoScroll.ts lines 161-163).
   ```
   </details>
   <details>
   <summary><b>Prompt for AI Agent 🤖 </b></summary>
   
   ```mdx
   This is a comment left during a code review.
   
   **Path:** 
superset-frontend/src/components/Datasource/FoldersEditor/hooks/useAutoScroll.ts
   **Line:** 76:163
   **Comment:**
        *Logic Error: The auto-scroll logic only considers the vertical mouse 
position, so any drag whose Y coordinate overlaps the list's vertical range 
will trigger scrolling even when the pointer is horizontally far away from the 
list (e.g., over a different panel), causing unrelated lists to scroll while 
dragging; adding a horizontal bounds check around the container prevents this 
unintended behavior.
   
   Validate the correctness of the flagged issue. If correct, How can I resolve 
this? If you propose a fix, implement it and please make it concise.
   ```
   </details>



##########
superset-frontend/src/components/Datasource/FoldersEditor/index.tsx:
##########
@@ -0,0 +1,467 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { useCallback, useMemo, useRef, useState } from 'react';
+import { debounce } from 'lodash';
+import AutoSizer from 'react-virtualized-auto-sizer';
+import {
+  DndContext,
+  DragOverlay,
+  PointerSensor,
+  useSensor,
+  useSensors,
+  UniqueIdentifier,
+} from '@dnd-kit/core';
+import {
+  SortableContext,
+  verticalListSortingStrategy,
+} from '@dnd-kit/sortable';
+import { FoldersEditorItemType } from '../types';
+import { TreeItem as TreeItemType } from './constants';
+import {
+  flattenTree,
+  buildTree,
+  removeChildrenOf,
+  serializeForAPI,
+} from './treeUtils';
+import {
+  createFolder,
+  resetToDefault,
+  ensureDefaultFolders,
+  filterItemsBySearch,
+} from './folderOperations';
+import {
+  pointerSensorOptions,
+  measuringConfig,
+  autoScrollConfig,
+} from './sensors';
+import { FoldersContainer, FoldersContent } from './styles';
+import { FoldersEditorProps } from './types';
+import { useToasts } from 'src/components/MessageToasts/withToasts';
+import { useDragHandlers } from './hooks/useDragHandlers';
+import { useItemHeights } from './hooks/useItemHeights';
+import { useHeightCache } from './hooks/useHeightCache';
+import {
+  FoldersToolbarComponent,
+  ResetConfirmModal,
+  DragOverlayContent,
+} from './components';
+import { VirtualizedTreeList } from './VirtualizedTreeList';
+
+export default function FoldersEditor({
+  folders: initialFolders,
+  metrics,
+  columns,
+  onChange,
+}: FoldersEditorProps) {
+  const { addWarningToast } = useToasts();
+  const itemHeights = useItemHeights();
+  const heightCache = useHeightCache();
+
+  const [items, setItems] = useState<TreeItemType[]>(() => {
+    const ensured = ensureDefaultFolders(initialFolders, metrics, columns);
+    return ensured;
+  });
+
+  const [selectedItemIds, setSelectedItemIds] = useState<Set<string>>(
+    new Set(),
+  );
+  const lastSelectedItemIdRef = useRef<string | null>(null);
+  const [searchTerm, setSearchTerm] = useState('');
+  const [collapsedIds, setCollapsedIds] = useState<Set<string>>(new Set());
+  const [editingFolderId, setEditingFolderId] = useState<string | null>(null);
+  const [showResetConfirm, setShowResetConfirm] = useState(false);
+
+  const sensors = useSensors(useSensor(PointerSensor, pointerSensorOptions));
+
+  const fullFlattenedItems = useMemo(() => flattenTree(items), [items]);
+
+  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) =>
+      removeChildrenOf(
+        fullFlattenedItems,
+        activeId != null
+          ? [activeId, ...collapsedFolderIds]
+          : collapsedFolderIds,
+      ),
+    [fullFlattenedItems, collapsedFolderIds],
+  );
+
+  const visibleItemIds = useMemo(() => {
+    if (!searchTerm) {
+      const allIds = new Set<string>();
+      metrics.forEach(m => allIds.add(m.uuid));
+      columns.forEach(c => allIds.add(c.uuid));
+      return allIds;
+    }
+    const allItems = [...metrics, ...columns];
+    return filterItemsBySearch(searchTerm, allItems);
+  }, [searchTerm, metrics, columns]);
+
+  const metricsMap = useMemo(
+    () => new Map(metrics.map(m => [m.uuid, m])),
+    [metrics],
+  );
+  const columnsMap = useMemo(
+    () => new Map(columns.map(c => [c.uuid, c])),
+    [columns],
+  );
+
+  const {
+    isDragging,
+    activeId,
+    dragOverlayWidth,
+    flattenedItems,
+    dragOverlayItems,
+    forbiddenDropFolderIds,
+    currentDropTargetId,
+    fullItemsByUuid,
+    handleDragStart,
+    handleDragMove,
+    handleDragOver,
+    handleDragEnd,
+    handleDragCancel,
+  } = useDragHandlers({
+    setItems,
+    computeFlattenedItems,
+    fullFlattenedItems,
+    selectedItemIds,
+    onChange,
+    addWarningToast,
+  });
+
+  const debouncedSearch = useCallback(
+    debounce((term: string) => {
+      setSearchTerm(term);
+    }, 300),
+    [],
+  );
+
+  const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
+    debouncedSearch(e.target.value);
+  };
+
+  const handleAddFolder = () => {
+    const newFolder = createFolder('');
+    const updatedItems = [newFolder, ...items];
+    setItems(updatedItems);
+    setEditingFolderId(newFolder.uuid);
+    onChange(serializeForAPI(updatedItems));
+  };
+
+  const allVisibleSelected = useMemo(() => {
+    const selectableItems = Array.from(visibleItemIds).filter(id => {
+      const item = fullItemsByUuid.get(id);
+      return item && item.type !== FoldersEditorItemType.Folder;
+    });
+    return (
+      selectableItems.length > 0 &&
+      selectableItems.every(id => selectedItemIds.has(id))
+    );
+  }, [fullItemsByUuid, visibleItemIds, selectedItemIds]);
+
+  const handleSelectAll = useCallback(() => {
+    const itemsToSelect = new Set(
+      Array.from(visibleItemIds).filter(id => {
+        const item = fullItemsByUuid.get(id);
+        return item && item.type !== FoldersEditorItemType.Folder;
+      }),
+    );
+
+    if (allVisibleSelected) {
+      setSelectedItemIds(new Set());
+    } else {
+      setSelectedItemIds(itemsToSelect);
+    }
+  }, [visibleItemIds, fullItemsByUuid, allVisibleSelected]);
+
+  const handleResetToDefault = () => {
+    setShowResetConfirm(true);
+  };
+
+  const handleConfirmReset = () => {
+    const resetFolders = resetToDefault(metrics, columns);
+    setItems(resetFolders);
+    setSelectedItemIds(new Set());
+    setEditingFolderId(null);
+    setShowResetConfirm(false);
+    onChange(serializeForAPI(resetFolders));
+  };
+
+  const handleCancelReset = () => {
+    setShowResetConfirm(false);
+  };
+
+  const handleToggleCollapse = useCallback((folderId: string) => {
+    setCollapsedIds(prev => {
+      const newSet = new Set(prev);
+      if (newSet.has(folderId)) {
+        newSet.delete(folderId);
+      } else {
+        newSet.add(folderId);
+      }
+      return newSet;
+    });
+  }, []);
+
+  const handleSelect = useCallback(
+    (itemId: string, selected: boolean, shiftKey?: boolean) => {
+      // Capture ref value before setState to avoid timing issues with React 
18 batching
+      const lastSelectedId = lastSelectedItemIdRef.current;
+
+      // Update ref immediately for next interaction
+      if (selected) {
+        lastSelectedItemIdRef.current = itemId;
+      }
+
+      setSelectedItemIds(prev => {
+        const newSet = new Set(prev);
+
+        // Range selection when shift is held and we have a previous selection
+        if (shiftKey && selected && lastSelectedId) {
+          const selectableItems = flattenedItems.filter(
+            item => item.type !== FoldersEditorItemType.Folder,
+          );
+
+          const currentIndex = selectableItems.findIndex(
+            item => item.uuid === itemId,
+          );
+          const lastIndex = selectableItems.findIndex(
+            item => item.uuid === lastSelectedId,
+          );
+
+          if (currentIndex !== -1 && lastIndex !== -1) {
+            const startIndex = Math.min(currentIndex, lastIndex);
+            const endIndex = Math.max(currentIndex, lastIndex);
+
+            for (let i = startIndex; i <= endIndex; i += 1) {
+              newSet.add(selectableItems[i].uuid);
+            }
+          }
+        } else if (selected) {
+          newSet.add(itemId);
+        } else {
+          newSet.delete(itemId);
+        }
+
+        return newSet;
+      });
+    },
+    [flattenedItems],

Review Comment:
   **Suggestion:** Shift-click range selection currently uses all flattened 
items regardless of the active search filter, so range selection can include 
items that are not visible in the UI, causing hidden metrics/columns to be 
selected and moved unintentionally. [logic error]
   
   <details>
   <summary><b>Severity Level:</b> Major ⚠️</summary>
   
   ```mdx
   - ❌ Folder editor selects hidden items during search.
   - ❌ Dragging moves invisible metrics/columns unexpectedly.
   - ⚠️ Dataset modal Folders tab shows inconsistent selection.
   - ⚠️ Subsequent Explore panel folder layout may change.
   ```
   </details>
   
   ```suggestion
               item =>
                 item.type !== FoldersEditorItemType.Folder &&
                 visibleItemIds.has(item.uuid),
             );
   
             const currentIndex = selectableItems.findIndex(
               item => item.uuid === itemId,
             );
             const lastIndex = selectableItems.findIndex(
               item => item.uuid === lastSelectedId,
             );
   
             if (currentIndex !== -1 && lastIndex !== -1) {
               const startIndex = Math.min(currentIndex, lastIndex);
               const endIndex = Math.max(currentIndex, lastIndex);
   
               for (let i = startIndex; i <= endIndex; i += 1) {
                 newSet.add(selectableItems[i].uuid);
               }
             }
           } else if (selected) {
             newSet.add(itemId);
           } else {
             newSet.delete(itemId);
           }
   
           return newSet;
         });
       },
       [flattenedItems, visibleItemIds],
   ```
   <details>
   <summary><b>Steps of Reproduction ✅ </b></summary>
   
   ```mdx
   1. Enable DATASET_FOLDERS and open Dataset modal → Folders tab (UI entry 
point).
   
   2. In 
superset-frontend/src/components/Datasource/FoldersEditor/index.tsx:120-129, 
note
   visibleItemIds is computed from searchTerm and metrics/columns 
(visibleItemIds referenced
   at lines ~120-129).
   
   3. Type into the search box (handled by handleSearch at lines 170-172) so 
the UI filters
   items — many items become hidden in the rendered list.
   
   4. Click one visible non-folder item, then Shift+click another visible 
non-folder item to
   trigger range selection. The Shift-range logic uses flattenedItems (from 
useDragHandlers)
   at lines 93 and 140-161 and filters only by type (existing code, lines 
247-269 in the
   hunk).
   
   5. Because the existing implementation builds the selectableItems array from
   flattenedItems without checking visibleItemIds (existing code lines 
249-258), the
   selection range includes items that match the indices in flattenedItems but 
are currently
   filtered out by search.
   
   6. Observe more items become selected than those visible in the UI 
(selection state
   includes hidden UUIDs). Subsequent move/drag operations (DndContext handlers 
at lines
   410-419 and useDragHandlers) will move hidden items unexpectedly.
   
   7. The improved code (uses visibleItemIds.has(...) and updates dependency to
   visibleItemIds at lines ... ) restricts the range to visible items only, 
preventing hidden
   selection.
   ```
   </details>
   <details>
   <summary><b>Prompt for AI Agent 🤖 </b></summary>
   
   ```mdx
   This is a comment left during a code review.
   
   **Path:** superset-frontend/src/components/Datasource/FoldersEditor/index.tsx
   **Line:** 253:280
   **Comment:**
        *Logic Error: Shift-click range selection currently uses all flattened 
items regardless of the active search filter, so range selection can include 
items that are not visible in the UI, causing hidden metrics/columns to be 
selected and moved unintentionally.
   
   Validate the correctness of the flagged issue. If correct, How can I resolve 
this? If you propose a fix, implement it and please make it concise.
   ```
   </details>



##########
superset-frontend/src/components/Datasource/FoldersEditor/VirtualizedTreeList.tsx:
##########
@@ -0,0 +1,229 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { useCallback, useEffect, useMemo, useRef } from 'react';
+import { VariableSizeList as List } from 'react-window';
+import type { UniqueIdentifier } from '@dnd-kit/core';
+import type { Metric, ColumnMeta } from '@superset-ui/chart-controls';
+import { FoldersEditorItemType } from '../types';
+import type { FlattenedTreeItem } from './constants';
+import type { ItemHeights } from './hooks/useItemHeights';
+import type { HeightCache } from './hooks/useHeightCache';
+import { useAutoScroll } from './hooks/useAutoScroll';
+import {
+  VirtualizedTreeItem,
+  VirtualizedTreeItemData,
+} from './VirtualizedTreeItem';
+
+interface VirtualizedTreeListProps {
+  width: number;
+  height: number;
+  flattenedItems: FlattenedTreeItem[];
+  itemHeights: ItemHeights;
+  heightCache: HeightCache;
+  collapsedIds: Set<string>;
+  selectedItemIds: Set<string>;
+  editingFolderId: string | null;
+  folderChildCounts: Map<string, number>;
+  itemSeparatorInfo: Map<string, 'visible' | 'transparent'>;
+  visibleItemIds: Set<string>;
+  searchTerm: string;
+  metricsMap: Map<string, Metric>;
+  columnsMap: Map<string, ColumnMeta>;
+  isDragging: boolean;
+  activeId: UniqueIdentifier | null;
+  forbiddenDropFolderIds: Set<string>;
+  currentDropTargetId: string | null;
+  onToggleCollapse: (id: string) => void;
+  onSelect: (id: string, selected: boolean, shiftKey?: boolean) => void;
+  onStartEdit: (id: string) => void;
+  onFinishEdit: (id: string, newName: string) => void;
+}
+
+export function VirtualizedTreeList({
+  width,
+  height,
+  flattenedItems,
+  itemHeights,
+  heightCache,
+  collapsedIds,
+  selectedItemIds,
+  editingFolderId,
+  folderChildCounts,
+  itemSeparatorInfo,
+  visibleItemIds,
+  searchTerm,
+  metricsMap,
+  columnsMap,
+  isDragging,
+  activeId,
+  forbiddenDropFolderIds,
+  currentDropTargetId,
+  onToggleCollapse,
+  onSelect,
+  onStartEdit,
+  onFinishEdit,
+}: VirtualizedTreeListProps) {
+  const listRef = useRef<List>(null);
+  const containerRef = useRef<HTMLDivElement>(null);
+
+  // Custom auto-scroll during drag (replaces dnd-kit's auto-scroll which 
conflicts with virtualization)
+  useAutoScroll({
+    listRef,
+    containerRef,
+    isDragging,
+    listHeight: height,
+  });
+
+  // Reset list cache when items structure changes, but not during drag
+  // Resetting during drag causes jumping/flickering
+  useEffect(() => {
+    if (!isDragging) {
+      listRef.current?.resetAfterIndex(0);
+    }
+  }, [
+    flattenedItems,
+    collapsedIds,
+    folderChildCounts,
+    itemSeparatorInfo,
+    visibleItemIds,
+    isDragging,

Review Comment:
   **Suggestion:** The list height for empty expanded folders uses a cached 
measurement from `heightCache`, but the react-window `VariableSizeList` cache 
is only reset when `flattenedItems`, `collapsedIds`, `folderChildCounts`, 
`itemSeparatorInfo`, or `visibleItemIds` change; if `heightCache` is updated 
(for example when the actual empty-folder content is measured), the list will 
keep using the old fallback height, causing inconsistent item positioning and 
visual glitches because the virtualized row sizes no longer match their 
rendered heights. [possible bug]
   
   <details>
   <summary><b>Severity Level:</b> Major ⚠️</summary>
   
   ```mdx
   - ❌ Visual misalignment in Dataset Folders editor list.
   - ⚠️ Scrolling and drag interactions show layout glitches.
   - ⚠️ Affects empty-folder rendering measured via heightCache.
   - ⚠️ Occurs in folder editor used inside dataset modal.
   ```
   </details>
   
   ```suggestion
       heightCache,
   ```
   <details>
   <summary><b>Steps of Reproduction ✅ </b></summary>
   
   ```mdx
   1. Open the dataset Folders editor (component at
   
superset-frontend/src/components/Datasource/FoldersEditor/VirtualizedTreeList.tsx).
 The
   list reset effect is defined at lines 94-107 and does not depend on 
heightCache.
   
   2. Create or open a folder that is empty and expanded. The empty-folder 
height is read
   from heightCache via heightCache.getHeight(item.uuid) at lines 139-141 of 
the same file
   (see totalHeight calculation).
   
   3. When the empty folder's actual rendered empty-state is measured elsewhere 
(the
   measurement logic updates heightCache with the real height), heightCache 
changes but the
   useEffect that calls listRef.current?.resetAfterIndex(0) will not run 
because heightCache
   is not listed in the dependency array (lines 96-106).
   
   4. As a result, react-window's internal size cache (VariableSizeList) keeps 
the old size
   for that row while the DOM now has a different rendered height (measured 
height), causing
   visual misalignment and possible jump/glitch when scrolling or interacting 
with the list.
   
   5. Reproducing locally: (a) enable DATASET_FOLDERS flag; (b) open dataset 
modal → Folders
   tab; (c) create an empty folder, expand it so measurement code stores a 
height into
   heightCache (measurement happens in the empty-folder render path); (d) 
observe that the
   visual row height differs from the virtualized slot until an action that 
resets the
   react-window cache occurs (e.g., toggle collapse or restructure items). This 
demonstrates
   heightCache updates are not triggering a List reset.
   
   6. Explanation why change is needed: the code reads from heightCache at 
lines ~139-141 but
   the reset effect (lines 94-107) only listens to flattenedItems, collapsedIds,
   folderChildCounts, itemSeparatorInfo, visibleItemIds, and isDragging — so it 
won't react
   when cached measurements change. Adding heightCache to dependencies ensures
   resetAfterIndex runs when measurements update.
   ```
   </details>
   <details>
   <summary><b>Prompt for AI Agent 🤖 </b></summary>
   
   ```mdx
   This is a comment left during a code review.
   
   **Path:** 
superset-frontend/src/components/Datasource/FoldersEditor/VirtualizedTreeList.tsx
   **Line:** 106:106
   **Comment:**
        *Possible Bug: The list height for empty expanded folders uses a cached 
measurement from `heightCache`, but the react-window `VariableSizeList` cache 
is only reset when `flattenedItems`, `collapsedIds`, `folderChildCounts`, 
`itemSeparatorInfo`, or `visibleItemIds` change; if `heightCache` is updated 
(for example when the actual empty-folder content is measured), the list will 
keep using the old fallback height, causing inconsistent item positioning and 
visual glitches because the virtualized row sizes no longer match their 
rendered heights.
   
   Validate the correctness of the flagged issue. If correct, How can I resolve 
this? If you propose a fix, implement it and please make it concise.
   ```
   </details>



##########
superset-frontend/src/explore/components/controls/MetricControl/FilterDefinitionOption.tsx:
##########
@@ -62,7 +62,7 @@ export default function FilterDefinitionOption({
   if (option.label) {
     return (
       <StyledColumnOption
-        column={{ column_name: option.label, type: 'expression' }}
+        column={{ column_name: option.label, type: 'metric' }}

Review Comment:
   **Suggestion:** Similarly, for adhoc metrics rendered via the `label` 
branch, setting `type: 'metric'` on the column-like object conflates 
metric/column classification with the column data type field, which downstream 
components (like `ColumnOption`) typically use for physical type information 
and UI badges. This can result in inconsistent or incorrect UI rendering for 
adhoc metrics, so the synthetic object should omit the `type` override. [logic 
error]
   
   <details>
   <summary><b>Severity Level:</b> Major ⚠️</summary>
   
   ```mdx
   - ❌ Adhoc metric UI shows missing type badge.
   - ⚠️ Tooltip about column type becomes inaccurate.
   - ⚠️ Metric editing popovers may display inconsistent labels.
   ```
   </details>
   
   ```suggestion
           column={{ column_name: option.label }}
   ```
   <details>
   <summary><b>Steps of Reproduction ✅ </b></summary>
   
   ```mdx
   1. Render FilterDefinitionOption with an adhoc metric option: open
   
`superset-frontend/src/explore/components/controls/MetricControl/FilterDefinitionOption.tsx`
   and trigger the `if (option.label)` branch (file lines 62-68). The component 
returns JSX
   at lines 64-66 creating a synthetic column object with `type: 'metric'`.
   
   2. That JSX passes the synthetic object to `StyledColumnOption`, which is 
rendered by
   ColumnOption 
(`packages/superset-ui-chart-controls/src/components/ColumnOption.tsx`).
   ColumnOption uses `type_generic` to derive the displayed type and uses 
`column.type` for
   tooltips (see ColumnOption lines 55-63 and 70-83).
   
   3. Because the synthetic object supplies `type: 'metric'` but not 
`type_generic` or other
   expected column metadata, ColumnOption's logic will not correctly display a 
physical type
   label and may display unexpected tooltip content or no badge for adhoc 
metrics. This
   behavior can be observed by opening the metric picker/popover that renders 
adhoc metrics
   and inspecting the type badge next to labels.
   
   4. Reproduce in UI: open metric creation/edit UI or any control that lists 
adhoc metrics
   (the MetricControl/AdhocMetric edit popover paths reference 
StyledColumnOption usage—see
   grep hits). Add or view an adhoc metric with a `label` value so the `label` 
branch runs;
   observe the ColumnTypeLabel or tooltip behavior next to the adhoc metric 
label. The
   incorrect or missing type label is caused by misusing `type` for 
classification rather
   than providing the column metadata ColumnOption expects.
   
   Note: Steps reference concrete files and lines discovered with Grep/Read:
   FilterDefinitionOption branches (file/lines) and ColumnOption implementation 
(file/lines).
   ```
   </details>
   <details>
   <summary><b>Prompt for AI Agent 🤖 </b></summary>
   
   ```mdx
   This is a comment left during a code review.
   
   **Path:** 
superset-frontend/src/explore/components/controls/MetricControl/FilterDefinitionOption.tsx
   **Line:** 65:65
   **Comment:**
        *Logic Error: Similarly, for adhoc metrics rendered via the `label` 
branch, setting `type: 'metric'` on the column-like object conflates 
metric/column classification with the column data type field, which downstream 
components (like `ColumnOption`) typically use for physical type information 
and UI badges. This can result in inconsistent or incorrect UI rendering for 
adhoc metrics, so the synthetic object should omit the `type` override.
   
   Validate the correctness of the flagged issue. If correct, How can I resolve 
this? If you propose a fix, implement it and please make it concise.
   ```
   </details>



##########
superset-frontend/src/explore/components/controls/MetricControl/FilterDefinitionOption.tsx:
##########
@@ -46,7 +46,7 @@ export default function FilterDefinitionOption({
   if (option.saved_metric_name) {
     return (
       <StyledColumnOption
-        column={{ column_name: option.saved_metric_name, type: 'expression' }}
+        column={{ column_name: option.saved_metric_name, type: 'metric' }}

Review Comment:
   **Suggestion:** The `type` property on the synthetic column object for saved 
metrics is being set to the string `'metric'`, but `type` in column metadata is 
generally used for the underlying data type (e.g., VARCHAR, numeric). 
Overloading it with `'metric'` can confuse consumers like `ColumnOption` that 
rely on this field for type badges or behavior, leading to inconsistent or 
incorrect rendering for saved metrics. Instead, keep the minimal shape needed 
for display and omit the `type` override. [logic error]
   
   <details>
   <summary><b>Severity Level:</b> Major ⚠️</summary>
   
   ```mdx
   - ❌ Metric selection UI shows incorrect type badge.
   - ⚠️ Column type tooltip content may be missing.
   - ⚠️ FilterDefinitionOption affects MetricControl metric list.
   ```
   </details>
   
   ```suggestion
           column={{ column_name: option.saved_metric_name }}
   ```
   <details>
   <summary><b>Steps of Reproduction ✅ </b></summary>
   
   ```mdx
   1. Render FilterDefinitionOption with a saved metric option: open the code at
   
`superset-frontend/src/explore/components/controls/MetricControl/FilterDefinitionOption.tsx`
   and trigger the branch guarded by `if (option.saved_metric_name)` (see file 
lines 46-52).
   The component returns the exact JSX at lines 48-51 which creates a synthetic 
column object
   with `type: 'metric'`.
   
   2. That JSX passes the synthetic object to `StyledColumnOption` (same JSX).
   `StyledColumnOption` delegates to the ColumnOption component implemented at
   
`superset-frontend/packages/superset-ui-chart-controls/src/components/ColumnOption.tsx`
   (see read result). ColumnOption expects column metadata with fields like 
`type_generic`
   and `type` (see ColumnOption lines 55-63 and 72-83).
   
   3. ColumnOption computes the displayed type label via `const type = 
hasExpression ?
   'expression' : type_generic;` and uses `showType` to render a 
ColumnTypeLabel using `type`
   (ColumnOption lines 55 and 72-81). The synthetic object only supplies `type: 
'metric'` and
   omits `type_generic`, so ColumnOption will treat `type_generic` as undefined 
and derive
   `type` incorrectly (no physical type), potentially causing the type label or 
tooltip to be
   missing or inconsistent for saved metrics.
   
   4. Reproduce in UI: open any UI that uses FilterDefinitionOption (metric 
selection/filter
   UI). Select or list saved metrics so the saved-metric branch is rendered; 
inspect the type
   badge shown next to the metric name. Expect missing/incorrect type badge or 
tooltip text
   because ColumnOption relies on `type_generic`/`type` semantics found in the 
code at
   `packages/superset-ui-chart-controls/src/components/ColumnOption.tsx`.
   
   Note: This reproduction is based on concrete code paths: the branch in
   FilterDefinitionOption (file/lines cited) and ColumnOption implementation 
(file/lines
   cited). The current code creates a synthetic object with `type: 'metric'` 
which does not
   match ColumnOption's expected `type_generic` usage.
   ```
   </details>
   <details>
   <summary><b>Prompt for AI Agent 🤖 </b></summary>
   
   ```mdx
   This is a comment left during a code review.
   
   **Path:** 
superset-frontend/src/explore/components/controls/MetricControl/FilterDefinitionOption.tsx
   **Line:** 49:49
   **Comment:**
        *Logic Error: The `type` property on the synthetic column object for 
saved metrics is being set to the string `'metric'`, but `type` in column 
metadata is generally used for the underlying data type (e.g., VARCHAR, 
numeric). Overloading it with `'metric'` can confuse consumers like 
`ColumnOption` that rely on this field for type badges or behavior, leading to 
inconsistent or incorrect rendering for saved metrics. Instead, keep the 
minimal shape needed for display and omit the `type` override.
   
   Validate the correctness of the flagged issue. If correct, How can I resolve 
this? If you propose a fix, implement it and please make it concise.
   ```
   </details>



##########
superset-frontend/src/components/Datasource/FoldersEditor/TreeItem.styles.ts:
##########
@@ -0,0 +1,214 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { styled, css } from '@apache-superset/core/ui';
+
+export const FOLDER_INDENTATION_WIDTH = 24;
+export const ITEM_INDENTATION_WIDTH = 4;
+
+export const TreeItemContainer = styled.div<{
+  depth: number;
+  isDragging: boolean;
+  isOver: boolean;
+  isOverlay?: boolean;
+}>`
+  ${({ theme, depth, isDragging, isOverlay }) => `
+    margin: 0 ${theme.marginMD}px;
+    margin-left: ${isOverlay ? ITEM_INDENTATION_WIDTH : (depth - 1) * 
FOLDER_INDENTATION_WIDTH + ITEM_INDENTATION_WIDTH}px;
+    padding-left: ${theme.paddingSM}px;
+    display: flex;
+    align-items: center;
+    cursor: pointer;
+    opacity: ${isDragging ? 0.4 : 1};
+    user-select: none;
+    ${isDragging || isOverlay ? 'will-change: transform;' : ''}
+  `}

Review Comment:
   **Suggestion:** The `margin-left` calculation in the base tree item 
container assumes `depth` is at least 1, so when a non-folder item has `depth` 
0 (which is allowed by the tree utilities for root-level items) it produces a 
negative margin and shifts the item off-screen, making it effectively unusable; 
clamping the effective depth to a minimum of 1 before computing `margin-left` 
prevents this. [logic error]
   
   <details>
   <summary><b>Severity Level:</b> Major ⚠️</summary>
   
   ```mdx
   - ❌ Root-level item layout shifted off-screen.
   - ⚠️ Dataset Folders editor item selection impaired.
   - ⚠️ Drag preview coordinates may appear incorrect.
   ```
   </details>
   
   ```suggestion
     ${({ theme, depth, isDragging, isOverlay }) => {
         const effectiveDepth = Math.max(depth, 1);
         const marginLeft = isOverlay
           ? ITEM_INDENTATION_WIDTH
           : (effectiveDepth - 1) * FOLDER_INDENTATION_WIDTH + 
ITEM_INDENTATION_WIDTH;
         return `
           margin: 0 ${theme.marginMD}px;
           margin-left: ${marginLeft}px;
           padding-left: ${theme.paddingSM}px;
           display: flex;
           align-items: center;
           cursor: pointer;
           opacity: ${isDragging ? 0.4 : 1};
           user-select: none;
           ${isDragging || isOverlay ? 'will-change: transform;' : ''}
         `;
       }}
   ```
   <details>
   <summary><b>Steps of Reproduction ✅ </b></summary>
   
   ```mdx
   1. Open the dataset Folders editor UI (feature flag DATASET_FOLDERS enabled) 
so the
   component tree rendering includes TreeItem (file:
   superset-frontend/src/components/Datasource/FoldersEditor/TreeItem.tsx). The 
TreeItem
   component receives a numeric depth prop (see TreeItemProps depth at 
TreeItem.tsx:68-69).
   
   2. Render a root-level non-folder item that is passed depth = 0 (TreeItem is 
used for
   non-folder items in the non-folder branch at TreeItem.tsx:357-361 where 
<TreeItemContainer
   {...restContainerProps}> is returned). The container props include depth (see
   containerProps assignment at TreeItem.tsx:252-259).
   
   3. With depth = 0 the current style function in TreeItem.styles.ts builds 
margin-left
   using (depth - 1) * FOLDER_INDENTATION_WIDTH + ITEM_INDENTATION_WIDTH
   (TreeItem.styles.ts:31-34). Substituting depth = 0 yields a negative 
margin-left value ((0
   - 1) * 24 + 4 = -20), shifting the item left off-screen.
   
   4. Observe the rendered non-folder item visually misaligned (partially/fully 
off-screen)
   in the dataset Folders editor; verify by inspecting the element's computed 
style in
   browser devtools—computed margin-left will be negative (file:
   superset-frontend/src/components/Datasource/FoldersEditor/TreeItem.styles.ts 
lines 31-34).
   
   Explanation: TreeItem.props.depth is a plain number and can be 0 for 
root-level items
   (TreeItem.tsx declares depth: number at line 68). The existing code assumes 
depth >= 1 and
   therefore produces negative margins for depth 0.
   ```
   </details>
   <details>
   <summary><b>Prompt for AI Agent 🤖 </b></summary>
   
   ```mdx
   This is a comment left during a code review.
   
   **Path:** 
superset-frontend/src/components/Datasource/FoldersEditor/TreeItem.styles.ts
   **Line:** 31:41
   **Comment:**
        *Logic Error: The `margin-left` calculation in the base tree item 
container assumes `depth` is at least 1, so when a non-folder item has `depth` 
0 (which is allowed by the tree utilities for root-level items) it produces a 
negative margin and shifts the item off-screen, making it effectively unusable; 
clamping the effective depth to a minimum of 1 before computing `margin-left` 
prevents this.
   
   Validate the correctness of the flagged issue. If correct, How can I resolve 
this? If you propose a fix, implement it and please make it concise.
   ```
   </details>



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to