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


##########
superset-frontend/packages/superset-ui-core/src/components/Icons/index.tsx:
##########
@@ -42,6 +42,7 @@ const customIcons = [
   'Error',
   'Full',
   'Layers',
+  'Move',

Review Comment:
   **Suggestion:** The newly added `Move` custom icon will cause a runtime 
failure when used because `AsyncIcon` will try to dynamically import 
`src/assets/images/icons/move.svg`, but there is no corresponding SVG file in 
the repository, leading to a rejected dynamic import and missing icon rendering 
(and likely an error in the console); either add the missing `move.svg` asset 
or remove this icon registration until the asset exists. [logic error]
   
   **Severity Level:** Minor ⚠️
   ```suggestion
   
   ```
   <details>
   <summary><b>Why it matters? ⭐ </b></summary>
   
   The suggestion identifies a real runtime risk: AsyncIcon dynamically builds 
a file path from the icon name (filename 'move') and will attempt to import the 
corresponding asset. I searched the repository for both "icons/move" and 
"move.svg" and found no matches, so the asset doesn't exist in the codebase. 
This will result in failed dynamic imports and missing icons (and likely 
console errors) when this icon is used. Adding the missing SVG asset or 
removing the registration are both valid fixes.
   </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-core/src/components/Icons/index.tsx
   **Line:** 45:45
   **Comment:**
        *Logic Error: The newly added `Move` custom icon will cause a runtime 
failure when used because `AsyncIcon` will try to dynamically import 
`src/assets/images/icons/move.svg`, but there is no corresponding SVG file in 
the repository, leading to a rejected dynamic import and missing icon rendering 
(and likely an error in the console); either add the missing `move.svg` asset 
or remove this icon registration until the asset exists.
   
   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/folderOperations.test.ts:
##########
@@ -0,0 +1,751 @@
+/**
+ * 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 { Metric } from '@superset-ui/chart-controls';
+import { ColumnObject } from 'src/features/datasets/types';
+import {
+  DEFAULT_METRICS_FOLDER_UUID,
+  DEFAULT_COLUMNS_FOLDER_UUID,
+  isDefaultFolder,
+} from './constants';
+import {
+  createFolder,
+  deleteFolder,
+  resetToDefault,
+  filterItemsBySearch,
+  renameFolder,
+  nestFolder,
+  reorderFolders,
+  moveItems,
+  cleanupFolders,
+  getAllSelectedItems,
+  areAllVisibleItemsSelected,
+  ensureDefaultFolders,
+} from './folderOperations';
+import {
+  canDropItems,
+  canDropFolder,
+  getFolderDescendants,
+  findFolderById,
+  validateFolders,
+} from './folderValidation';
+import { DatasourceFolder } from 
'src/explore/components/DatasourcePanel/types';
+import { FoldersEditorItemType } from '../types';
+
+describe('folderUtils', () => {
+  const mockMetrics: Metric[] = [
+    {
+      id: 1,
+      uuid: 'metric-1',
+      metric_name: 'Test Metric 1',
+      metric_type: 'count',
+      expression: 'COUNT(*)',
+    } as Metric,
+    {
+      id: 2,
+      uuid: 'metric-2',
+      metric_name: 'Test Metric 2',
+      metric_type: 'sum',
+      expression: 'SUM(value)',
+    } as Metric,

Review Comment:
   **Suggestion:** The test uses a type assertion (`as Metric`) to coerce plain 
objects into `Metric`, which can mask missing runtime properties (like `uuid`) 
and allow malformed mock data to slip through compilation. Make the mock type 
explicitly include `uuid` and remove `as` casts so the compiler enforces the 
object shape and prevents accidental omissions that would break functions 
expecting `uuid` at runtime. [type error]
   
   **Severity Level:** Minor ⚠️
   ```suggestion
     const mockMetrics: (Metric & { uuid: string })[] = [
       {
         id: 1,
         uuid: 'metric-1',
         metric_name: 'Test Metric 1',
         metric_type: 'count',
         expression: 'COUNT(*)',
       },
       {
         id: 2,
         uuid: 'metric-2',
         metric_name: 'Test Metric 2',
         metric_type: 'sum',
         expression: 'SUM(value)',
       },
   ```
   <details>
   <summary><b>Why it matters? ⭐ </b></summary>
   
   The test file currently uses `as Metric` casts for the mock metric objects. 
Replacing the casts with an explicit type that ensures `uuid` (e.g. `(Metric & 
{ uuid: string })[]`) and removing `as` improves compile-time safety and 
prevents malformed mocks from slipping through. This is a real, non-cosmetic 
improvement to test robustness and type correctness.
   </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/folderOperations.test.ts
   **Line:** 52:66
   **Comment:**
        *Type Error: The test uses a type assertion (`as Metric`) to coerce 
plain objects into `Metric`, which can mask missing runtime properties (like 
`uuid`) and allow malformed mock data to slip through compilation. Make the 
mock type explicitly include `uuid` and remove `as` casts so the compiler 
enforces the object shape and prevents accidental omissions that would break 
functions expecting `uuid` at runtime.
   
   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,561 @@
+/**
+ * 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 '@superset-ui/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,
+} from '../constants';
+import { buildTree, getProjection, serializeForAPI } from '../treeUtils';
+
+interface UseDragHandlersProps {
+  items: TreeItemType[];
+  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({
+  items,
+  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);
+  const [projectedParentId, setProjectedParentId] = useState<string | null>(
+    null,
+  );
+  const [draggedItemIds, setDraggedItemIds] = useState<Set<string>>(new Set());
+
+  const pendingParentIdRef = useRef<string | null>(null);
+  const rafIdRef = useRef<number | null>(null);
+
+  const scheduleProjectedParentUpdate = useCallback(
+    (newParentId: string | null) => {
+      pendingParentIdRef.current = newParentId;
+
+      // Only schedule if not already scheduled
+      if (rafIdRef.current === null) {
+        rafIdRef.current = requestAnimationFrame(() => {
+          rafIdRef.current = null;
+          setProjectedParentId(prev =>
+            prev === pendingParentIdRef.current
+              ? prev
+              : pendingParentIdRef.current,
+          );
+        });
+      }
+    },
+    [],
+  );
+
+  const flattenedItems = useMemo(
+    () => computeFlattenedItems(activeId),
+    [computeFlattenedItems, activeId],
+  );
+
+  const flattenedItemsIndexMap = useMemo(() => {
+    const map = new Map<string, number>();
+    flattenedItems.forEach((item, index) => {
+      map.set(item.uuid, index);
+    });
+    return map;
+  }, [flattenedItems]);
+
+  const resetDragState = useCallback(() => {
+    setActiveId(null);
+    setOverId(null);
+    offsetLeftRef.current = 0;
+    setProjectedParentId(null);
+    setDraggedItemIds(new Set());
+    setDragOverlayWidth(null);
+    if (rafIdRef.current !== null) {
+      cancelAnimationFrame(rafIdRef.current);
+      rafIdRef.current = null;
+    }
+    pendingParentIdRef.current = null;
+  }, []);
+
+  const handleDragStart = ({ active }: DragStartEvent) => {
+    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', '');
+          scheduleProjectedParentUpdate(folderId);
+          return;
+        }
+
+        const projection = getProjection(
+          flattenedItems,
+          activeId,
+          overId,
+          delta.x,
+          DRAG_INDENTATION_WIDTH,
+          flattenedItemsIndexMap,
+        );
+        const newParentId = projection?.parentId ?? null;
+        scheduleProjectedParentUpdate(newParentId);
+      }
+    },
+    [
+      activeId,
+      overId,
+      flattenedItems,
+      flattenedItemsIndexMap,
+      scheduleProjectedParentUpdate,
+    ],
+  );
+
+  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', '');
+          scheduleProjectedParentUpdate(folderId);
+          return;

Review Comment:
   **Suggestion:** The drag-over handler uses `if (activeId && over)` which 
again treats numeric IDs of 0 as falsy and can skip projection updates; change 
the check to explicit null/undefined comparison (`activeId != null && over != 
null`) to ensure valid numeric IDs are processed. [logic error]
   
   **Severity Level:** Minor ⚠️
   ```suggestion
             if (activeId != null && over != null) {
   ```
   <details>
   <summary><b>Why it matters? ⭐ </b></summary>
   
   Similar to the drag-move handler, this handler uses a truthy check (if 
(activeId && over)) which will skip projection when activeId is 0. Changing to 
explicit null/undefined checks (activeId != null && over != null) ensures valid 
numeric IDs are handled and prevents subtle incorrect behavior.
   </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:** 174:174
   **Comment:**
        *Logic Error: The drag-over handler uses `if (activeId && over)` which 
again treats numeric IDs of 0 as falsy and can skip projection updates; change 
the check to explicit null/undefined comparison (`activeId != null && over != 
null`) to ensure valid numeric IDs are processed.
   
   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,183 @@
+/**
+ * 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: ${theme.marginXXS}px ${isOverlay ? 0 : theme.marginMD}px;
+    margin-left: ${isOverlay ? 0 : (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;' : ''}
+  `}
+`;
+
+export const ItemSeparator = styled.div<{
+  variant: 'visible' | 'transparent';
+}>`
+  ${({ theme, variant }) => `
+    height: 1px;
+    background-color: ${variant === 'visible' ? theme.colorBorderSecondary : 
'transparent'};
+    margin: ${variant === 'visible' ? theme.marginLG : theme.marginSM}px 
${theme.marginMD}px;
+    margin-left: ${theme.marginSM}px;
+  `}
+`;
+
+export const TreeFolderContainer = styled(TreeItemContainer)<{
+  isDropTarget?: boolean;
+  isForbiddenDropTarget?: boolean;
+}>`
+  ${({ theme, depth, isDropTarget, isForbiddenDropTarget, isOverlay }) => `
+    margin-top: ${isOverlay ? 0 : theme.marginLG}px;
+    margin-bottom: ${isOverlay ? 0 : theme.margin}px;
+    margin-left: ${isOverlay ? 0 : depth * FOLDER_INDENTATION_WIDTH}px;
+    border-radius: ${theme.borderRadius}px;
+    padding: 0 ${theme.paddingSM}px;
+    margin-right: ${isOverlay ? 0 : theme.marginMD}px;
+    transition: background-color 0.15s ease-in-out, box-shadow 0.15s 
ease-in-out;
+    ${
+      isDropTarget && isForbiddenDropTarget
+        ? `
+      background-color: ${theme.colorErrorBg};
+      box-shadow: inset 0 0 0 2px ${theme.colorError};
+      cursor: not-allowed;
+    `
+        : isDropTarget
+          ? `
+      background-color: ${theme.colorPrimaryBg};
+      box-shadow: inset 0 0 0 2px ${theme.colorPrimary};
+    `
+          : ''
+    }
+  `}
+`;
+
+export const DragHandle = styled.span`
+  ${({ theme }) => `
+    color: ${theme.colorTextTertiary};
+    display: inline-flex;
+    align-items: center;
+    cursor: grab;
+
+    &:hover {
+      color: ${theme.colorText};
+    }
+
+    &:active {
+      cursor: grabbing;
+    }
+  `}
+`;
+
+export const CollapseButton = styled.span`
+  ${({ theme }) => `
+    display: inline-flex;
+    align-items: center;
+    justify-content: center;
+    width: 12px;
+    height: 12px;
+    cursor: pointer;
+    color: ${theme.colorTextSecondary};
+    margin-left: auto;
+
+    &:hover {
+      color: ${theme.colorText};
+    }
+  `}
+`;
+
+export const DefaultFolderIconContainer = styled.span`
+  ${({ theme }) => `
+    display: inline-flex;
+    align-items: center;
+    color: ${theme.colorTextSecondary};
+    margin-right: ${theme.marginXS}px;
+  `}
+`;
+
+export const FolderName = styled.span`
+  ${({ theme }) => `
+    margin-right: ${theme.marginMD}px;
+    font-weight: ${theme.fontWeightStrong};
+  `}
+`;
+
+export const DragHandleContainer = styled.div`
+  ${({ theme }) => `
+    height: 100%;
+    display: flex;
+    align-items: center;
+    padding: 0 ${theme.sizeUnit}px;
+    margin-left: auto;
+    cursor: grab;
+    color: ${theme.colorTextTertiary};
+
+    &:hover {
+      color: ${theme.colorText};
+    }
+
+    &:active {
+      cursor: grabbing;
+    }
+  `}
+`;
+
+export const EmptyFolderDropZone = styled.div<{
+  depth: number;
+  isOver: boolean;
+  isForbidden: boolean;
+}>`
+  ${({ theme, depth, isOver, isForbidden }) => css`
+    margin: 0 ${depth * ITEM_INDENTATION_WIDTH + theme.marginMD}px;

Review Comment:
   **Suggestion:** The empty-folder drop zone currently uses the much smaller 
item indentation width to compute its horizontal margin, so its indentation 
grows only a few pixels per depth while folder and item rows shift by the 
larger folder indentation width, causing the empty-state drop area to visually 
drift out of alignment with the tree hierarchy at deeper levels; using the 
folder indentation constant here keeps the empty state aligned with the actual 
nesting. [logic error]
   
   **Severity Level:** Minor ⚠️
   ```suggestion
       margin: 0 ${depth * FOLDER_INDENTATION_WIDTH + theme.marginMD}px;
   ```
   <details>
   <summary><b>Why it matters? ⭐ </b></summary>
   
   Folder rows use FOLDER_INDENTATION_WIDTH for per-level horizontal shifts 
while the drop zone uses ITEM_INDENTATION_WIDTH, which only grows a few pixels 
per level and will visibly drift out of alignment at deeper depths. Switching 
to depth * FOLDER_INDENTATION_WIDTH aligns the empty drop zone with folder/item 
rows and fixes a real visual inconsistency.
   </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:** 160:160
   **Comment:**
        *Logic Error: The empty-folder drop zone currently uses the much 
smaller item indentation width to compute its horizontal margin, so its 
indentation grows only a few pixels per depth while folder and item rows shift 
by the larger folder indentation width, causing the empty-state drop area to 
visually drift out of alignment with the tree hierarchy at deeper levels; using 
the folder indentation constant here keeps the empty state aligned with the 
actual nesting.
   
   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/folderOperations.test.ts:
##########
@@ -0,0 +1,751 @@
+/**
+ * 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 { Metric } from '@superset-ui/chart-controls';
+import { ColumnObject } from 'src/features/datasets/types';
+import {
+  DEFAULT_METRICS_FOLDER_UUID,
+  DEFAULT_COLUMNS_FOLDER_UUID,
+  isDefaultFolder,
+} from './constants';
+import {
+  createFolder,
+  deleteFolder,
+  resetToDefault,
+  filterItemsBySearch,
+  renameFolder,
+  nestFolder,
+  reorderFolders,
+  moveItems,
+  cleanupFolders,
+  getAllSelectedItems,
+  areAllVisibleItemsSelected,
+  ensureDefaultFolders,
+} from './folderOperations';
+import {
+  canDropItems,
+  canDropFolder,
+  getFolderDescendants,
+  findFolderById,
+  validateFolders,
+} from './folderValidation';
+import { DatasourceFolder } from 
'src/explore/components/DatasourcePanel/types';
+import { FoldersEditorItemType } from '../types';
+
+describe('folderUtils', () => {
+  const mockMetrics: Metric[] = [
+    {
+      id: 1,
+      uuid: 'metric-1',
+      metric_name: 'Test Metric 1',
+      metric_type: 'count',
+      expression: 'COUNT(*)',
+    } as Metric,
+    {
+      id: 2,
+      uuid: 'metric-2',
+      metric_name: 'Test Metric 2',
+      metric_type: 'sum',
+      expression: 'SUM(value)',
+    } as Metric,
+  ];
+
+  const mockColumns: (ColumnObject & { uuid: string })[] = [
+    {
+      id: 1,
+      uuid: 'column-1',
+      column_name: 'Test Column 1',
+      type: 'VARCHAR',
+      filterable: true,
+      groupby: true,
+      is_active: true,
+      is_dttm: false,
+    },
+    {
+      id: 2,
+      uuid: 'column-2',
+      column_name: 'Test Column 2',
+      type: 'INTEGER',
+      filterable: true,
+      groupby: true,
+      is_active: true,
+      is_dttm: false,
+    },
+  ];
+
+  describe('createFolder', () => {
+    test('should create a folder with correct properties', () => {
+      const folder = createFolder('Test Folder');
+
+      expect(folder.name).toBe('Test Folder');
+      expect(folder.type).toBe(FoldersEditorItemType.Folder);
+      expect(folder.children).toEqual([]);
+      expect(folder.uuid).toBeDefined();
+    });
+  });
+
+  describe('resetToDefault', () => {
+    test('should create default folders with correct structure', () => {
+      const result = resetToDefault(mockMetrics, mockColumns);
+
+      expect(result).toHaveLength(2);
+
+      const metricsFolder = result.find(
+        f => f.uuid === DEFAULT_METRICS_FOLDER_UUID,
+      );
+      const columnsFolder = result.find(
+        f => f.uuid === DEFAULT_COLUMNS_FOLDER_UUID,
+      );
+
+      expect(metricsFolder).toBeDefined();
+      expect(metricsFolder?.name).toBe('Metrics');
+      expect(metricsFolder?.children).toHaveLength(2);
+
+      expect(columnsFolder).toBeDefined();
+      expect(columnsFolder?.name).toBe('Columns');
+      expect(columnsFolder?.children).toHaveLength(2);
+    });
+  });
+
+  describe('filterItemsBySearch', () => {
+    test('should filter items by search term', () => {
+      const allItems = [...mockMetrics, ...mockColumns];
+      const result = filterItemsBySearch('Test Metric', allItems);
+
+      expect(result.size).toBe(2);
+      expect(result.has('metric-1')).toBe(true);
+      expect(result.has('metric-2')).toBe(true);
+    });
+
+    test('should return all items for empty search', () => {
+      const allItems = [...mockMetrics, ...mockColumns];
+      const result = filterItemsBySearch('', allItems);
+
+      expect(result.size).toBe(4);
+    });
+  });
+
+  describe('renameFolder', () => {
+    test('should rename folder correctly', () => {
+      const folders = resetToDefault(mockMetrics, mockColumns);
+      const result = renameFolder(
+        DEFAULT_METRICS_FOLDER_UUID,
+        'Custom Metrics',
+        folders,
+      );
+
+      const renamedFolder = result.find(
+        f => f.uuid === DEFAULT_METRICS_FOLDER_UUID,
+      );
+      expect(renamedFolder?.name).toBe('Custom Metrics');
+    });
+  });
+
+  describe('canDropItems', () => {
+    test('should allow dropping metrics in Metrics folder', () => {
+      const folders = resetToDefault(mockMetrics, mockColumns);
+      const result = canDropItems(
+        ['metric-1'],
+        DEFAULT_METRICS_FOLDER_UUID,
+        folders,
+        mockMetrics,
+        mockColumns,
+      );
+
+      expect(result).toBe(true);
+    });
+
+    test('should not allow dropping columns in Metrics folder', () => {
+      const folders = resetToDefault(mockMetrics, mockColumns);
+      const result = canDropItems(
+        ['column-1'],
+        DEFAULT_METRICS_FOLDER_UUID,
+        folders,
+        mockMetrics,
+        mockColumns,
+      );
+
+      expect(result).toBe(false);
+    });
+
+    test('should allow dropping any items in custom folders', () => {
+      const customFolder = createFolder('Custom Folder');
+      const folders = [customFolder];
+
+      const metricResult = canDropItems(
+        ['metric-1'],
+        customFolder.uuid,
+        folders,
+        mockMetrics,
+        mockColumns,
+      );
+      const columnResult = canDropItems(
+        ['column-1'],
+        customFolder.uuid,
+        folders,
+        mockMetrics,
+        mockColumns,
+      );
+
+      expect(metricResult).toBe(true);
+      expect(columnResult).toBe(true);
+    });
+  });
+
+  describe('isDefaultFolder', () => {
+    test('should identify default folders by UUID', () => {
+      expect(isDefaultFolder(DEFAULT_METRICS_FOLDER_UUID)).toBe(true);
+      expect(isDefaultFolder(DEFAULT_COLUMNS_FOLDER_UUID)).toBe(true);
+      expect(isDefaultFolder('custom-folder-uuid')).toBe(false);
+    });
+  });
+
+  describe('validateFolders', () => {
+    test('should validate folders successfully', () => {
+      const folders = resetToDefault(mockMetrics, mockColumns);
+      const result = validateFolders(folders);
+
+      expect(result.isValid).toBe(true);
+      expect(result.errors).toHaveLength(0);
+    });
+
+    test('should allow empty folders without names', () => {
+      // Empty folders without names are valid (they get filtered out anyway)
+      const folders = [createFolder('')];
+      const result = validateFolders(folders);
+
+      expect(result.isValid).toBe(true);
+      expect(result.errors).toHaveLength(0);
+    });
+
+    test('should detect folders with content but no name', () => {
+      const folder = createFolder('');
+      folder.children = [
+        { uuid: 'metric-1', type: FoldersEditorItemType.Metric, name: 'Test' },
+      ];
+      const folders = [folder];
+      const result = validateFolders(folders);
+
+      expect(result.isValid).toBe(false);
+      expect(result.errors).toContain('Folder with content must have a name');
+    });
+
+    test('should detect duplicate folder names', () => {
+      const folder1 = createFolder('My Folder');
+      const folder2 = createFolder('My Folder');
+      const folders = [folder1, folder2];
+      const result = validateFolders(folders);
+
+      expect(result.isValid).toBe(false);
+      expect(result.errors.some(e => e.includes('my folder'))).toBe(true);
+    });
+
+    test('should detect duplicate folder names case-insensitively', () => {
+      const folder1 = createFolder('Test Folder');
+      const folder2 = createFolder('test folder');
+      const folders = [folder1, folder2];
+      const result = validateFolders(folders);
+
+      expect(result.isValid).toBe(false);
+      expect(result.errors.some(e => e.includes('test folder'))).toBe(true);
+    });
+  });
+
+  describe('ensureDefaultFolders', () => {
+    test('should create default folders when none exist', () => {
+      const result = ensureDefaultFolders([], mockMetrics, mockColumns);
+
+      expect(result).toHaveLength(2);
+
+      const metricsFolder = result.find(
+        f => f.uuid === DEFAULT_METRICS_FOLDER_UUID,
+      );
+      const columnsFolder = result.find(
+        f => f.uuid === DEFAULT_COLUMNS_FOLDER_UUID,
+      );
+
+      expect(metricsFolder).toBeDefined();
+      expect(columnsFolder).toBeDefined();
+    });
+
+    test('should preserve existing folders', () => {
+      const existingFolders = [createFolder('Custom Folder')];
+      const result = ensureDefaultFolders(
+        existingFolders,
+        mockMetrics,
+        mockColumns,
+      );
+
+      expect(result.length).toBeGreaterThan(2);
+      expect(result.find(f => f.name === 'Custom Folder')).toBeDefined();
+    });
+  });
+
+  describe('deleteFolder', () => {
+    test('should delete a folder by id', () => {
+      const folders = resetToDefault(mockMetrics, mockColumns);
+      const result = deleteFolder(DEFAULT_METRICS_FOLDER_UUID, folders);
+
+      expect(result).toHaveLength(1);
+      expect(
+        result.find(f => f.uuid === DEFAULT_METRICS_FOLDER_UUID),
+      ).toBeUndefined();
+      expect(
+        result.find(f => f.uuid === DEFAULT_COLUMNS_FOLDER_UUID),
+      ).toBeDefined();
+    });
+
+    test('should delete nested folders', () => {
+      const parentFolder: DatasourceFolder = {
+        uuid: 'parent',
+        type: FoldersEditorItemType.Folder,
+        name: 'Parent',
+        children: [
+          {
+            uuid: 'child',
+            type: FoldersEditorItemType.Folder,
+            name: 'Child',
+            children: [],
+          } as DatasourceFolder,
+        ],
+      };
+      const folders = [parentFolder];
+      const result = deleteFolder('child', folders);
+
+      expect(result).toHaveLength(1);
+      expect(
+        (result[0].children as DatasourceFolder[]).find(
+          c => c.uuid === 'child',
+        ),
+      ).toBeUndefined();
+    });
+
+    test('should return unchanged array if folder not found', () => {
+      const folders = resetToDefault(mockMetrics, mockColumns);
+      const result = deleteFolder('nonexistent', folders);
+
+      expect(result).toHaveLength(2);
+    });
+  });
+
+  describe('nestFolder', () => {
+    test('should nest a folder inside another folder', () => {
+      const folder1: DatasourceFolder = {
+        uuid: 'folder1',
+        type: FoldersEditorItemType.Folder,
+        name: 'Folder 1',
+        children: [],
+      };
+      const folder2: DatasourceFolder = {
+        uuid: 'folder2',
+        type: FoldersEditorItemType.Folder,
+        name: 'Folder 2',
+        children: [],
+      };
+      const folders = [folder1, folder2];
+
+      const result = nestFolder('folder2', 'folder1', folders);
+
+      expect(result).toHaveLength(1);
+      expect(result[0].uuid).toBe('folder1');
+      expect(result[0].children).toHaveLength(1);
+      expect((result[0].children as DatasourceFolder[])[0].uuid).toBe(
+        'folder2',
+      );
+    });
+
+    test('should return unchanged if folder to move not found', () => {
+      const folder1: DatasourceFolder = {
+        uuid: 'folder1',
+        type: FoldersEditorItemType.Folder,
+        name: 'Folder 1',
+        children: [],
+      };
+      const folders = [folder1];
+
+      const result = nestFolder('nonexistent', 'folder1', folders);
+
+      expect(result).toEqual(folders);
+    });
+  });
+
+  describe('reorderFolders', () => {
+    test('should reorder folders at root level', () => {
+      const folder1: DatasourceFolder = {
+        uuid: 'folder1',
+        type: FoldersEditorItemType.Folder,
+        name: 'Folder 1',
+        children: [],
+      };
+      const folder2: DatasourceFolder = {
+        uuid: 'folder2',
+        type: FoldersEditorItemType.Folder,
+        name: 'Folder 2',
+        children: [],
+      };
+      const folder3: DatasourceFolder = {
+        uuid: 'folder3',
+        type: FoldersEditorItemType.Folder,
+        name: 'Folder 3',
+        children: [],
+      };
+      const folders = [folder1, folder2, folder3];
+
+      const result = reorderFolders('folder3', 0, folders);
+
+      expect(result[0].uuid).toBe('folder3');

Review Comment:
   **Suggestion:** The reorder test only asserts ordering but doesn't assert 
the resulting list length equals the original, so a buggy implementation that 
drops or duplicates folders could still satisfy the current expectations; add 
an assertion that the length is preserved to catch such regressions. [possible 
bug]
   
   **Severity Level:** Critical 🚨
   ```suggestion
         // Ensure no items were lost or duplicated during reorder
         expect(result).toHaveLength(folders.length);
   ```
   <details>
   <summary><b>Why it matters? ⭐ </b></summary>
   
   Adding an assertion that result length equals the original length guards 
against implementations that accidentally drop or duplicate items during 
reorder. This is a small, valuable test hardening change that can catch 
regressions in reorder logic.
   </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/folderOperations.test.ts
   **Line:** 412:412
   **Comment:**
        *Possible Bug: The reorder test only asserts ordering but doesn't 
assert the resulting list length equals the original, so a buggy implementation 
that drops or duplicates folders could still satisfy the current expectations; 
add an assertion that the length is preserved to catch such regressions.
   
   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,183 @@
+/**
+ * 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: ${theme.marginXXS}px ${isOverlay ? 0 : theme.marginMD}px;
+    margin-left: ${isOverlay ? 0 : (depth - 1) * FOLDER_INDENTATION_WIDTH + 
ITEM_INDENTATION_WIDTH}px;

Review Comment:
   **Suggestion:** The current margin-left calculation for non-folder items 
will become negative when an item has depth 0, which is allowed by the shared 
tree utilities and would cause such items to render shifted off-screen or 
clipped instead of aligned at the left edge; clamping the depth used in the 
calculation to a minimum of 0 prevents this visual break for any root-level 
non-folder items. [logic error]
   
   **Severity Level:** Minor ⚠️
   ```suggestion
       margin-left: ${isOverlay ? 0 : Math.max(depth - 1, 0) * 
FOLDER_INDENTATION_WIDTH + ITEM_INDENTATION_WIDTH}px;
   ```
   <details>
   <summary><b>Why it matters? ⭐ </b></summary>
   
   The existing expression produces a negative margin when depth === 0: (0 - 1) 
* 24 + 4 = -20px, which will shift root-level non-folder items left off the 
intended alignment. Clamping with Math.max(depth - 1, 0) is a minimal, correct 
defensive change that prevents a real visual bug. There's no indication in the 
PR that negative indentation is intended.
   </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:** 33:33
   **Comment:**
        *Logic Error: The current margin-left calculation for non-folder items 
will become negative when an item has depth 0, which is allowed by the shared 
tree utilities and would cause such items to render shifted off-screen or 
clipped instead of aligned at the left edge; clamping the depth used in the 
calculation to a minimum of 0 prevents this visual break for any root-level 
non-folder items.
   
   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,561 @@
+/**
+ * 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 '@superset-ui/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,
+} from '../constants';
+import { buildTree, getProjection, serializeForAPI } from '../treeUtils';
+
+interface UseDragHandlersProps {
+  items: TreeItemType[];
+  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({
+  items,
+  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);
+  const [projectedParentId, setProjectedParentId] = useState<string | null>(
+    null,
+  );
+  const [draggedItemIds, setDraggedItemIds] = useState<Set<string>>(new Set());
+
+  const pendingParentIdRef = useRef<string | null>(null);
+  const rafIdRef = useRef<number | null>(null);
+
+  const scheduleProjectedParentUpdate = useCallback(
+    (newParentId: string | null) => {
+      pendingParentIdRef.current = newParentId;
+
+      // Only schedule if not already scheduled
+      if (rafIdRef.current === null) {
+        rafIdRef.current = requestAnimationFrame(() => {
+          rafIdRef.current = null;
+          setProjectedParentId(prev =>
+            prev === pendingParentIdRef.current
+              ? prev
+              : pendingParentIdRef.current,
+          );
+        });
+      }
+    },
+    [],
+  );
+
+  const flattenedItems = useMemo(
+    () => computeFlattenedItems(activeId),
+    [computeFlattenedItems, activeId],
+  );
+
+  const flattenedItemsIndexMap = useMemo(() => {
+    const map = new Map<string, number>();
+    flattenedItems.forEach((item, index) => {
+      map.set(item.uuid, index);
+    });
+    return map;
+  }, [flattenedItems]);
+
+  const resetDragState = useCallback(() => {
+    setActiveId(null);
+    setOverId(null);
+    offsetLeftRef.current = 0;
+    setProjectedParentId(null);
+    setDraggedItemIds(new Set());
+    setDragOverlayWidth(null);
+    if (rafIdRef.current !== null) {
+      cancelAnimationFrame(rafIdRef.current);
+      rafIdRef.current = null;
+    }
+    pendingParentIdRef.current = null;
+  }, []);
+
+  const handleDragStart = ({ active }: DragStartEvent) => {
+    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', '');
+          scheduleProjectedParentUpdate(folderId);
+          return;
+        }
+
+        const projection = getProjection(
+          flattenedItems,
+          activeId,
+          overId,
+          delta.x,
+          DRAG_INDENTATION_WIDTH,
+          flattenedItemsIndexMap,
+        );
+        const newParentId = projection?.parentId ?? null;
+        scheduleProjectedParentUpdate(newParentId);
+      }
+    },
+    [
+      activeId,
+      overId,
+      flattenedItems,
+      flattenedItemsIndexMap,
+      scheduleProjectedParentUpdate,
+    ],
+  );
+
+  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', '');
+          scheduleProjectedParentUpdate(folderId);
+          return;
+        }
+
+        const projection = getProjection(
+          flattenedItems,
+          activeId,
+          over.id,
+          offsetLeftRef.current,
+          DRAG_INDENTATION_WIDTH,
+          flattenedItemsIndexMap,
+        );
+        const newParentId = projection?.parentId ?? null;
+        scheduleProjectedParentUpdate(newParentId);
+      } else {
+        scheduleProjectedParentUpdate(null);
+      }
+    },
+    [
+      activeId,
+      flattenedItems,
+      flattenedItemsIndexMap,
+      scheduleProjectedParentUpdate,
+    ],
+  );
+
+  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 = fullFlattenedItems.findIndex(
+      ({ uuid }) => uuid === active.id,
+    );
+    const overIndex = fullFlattenedItems.findIndex(
+      ({ uuid }) => uuid === targetOverId,

Review Comment:
   **Suggestion:** `active.id` and `targetOverId` are UniqueIdentifier values 
(may be numbers), while `fullFlattenedItems[].uuid` are strings; comparing them 
directly can fail when IDs are numeric (e.g. 0). Coerce identifiers to strings 
when comparing to `uuid` to avoid failing to find the indexes and 
early-returning. [type error]
   
   **Severity Level:** Minor ⚠️
   ```suggestion
         ({ uuid }) => uuid === String(active.id),
       );
       const overIndex = fullFlattenedItems.findIndex(
         ({ uuid }) => uuid === String(targetOverId),
   ```
   <details>
   <summary><b>Why it matters? ⭐ </b></summary>
   
   active.id and targetOverId are UniqueIdentifier values (string | number). 
fullFlattenedItems[].uuid is a string. If an id ever comes through as a number 
(e.g. 0), strict === will fail to match and cause the function to early-return. 
Coercing to String() ensures reliable comparisons and prevents subtle 
mismatches at runtime.
   </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:** 220:223
   **Comment:**
        *Type Error: `active.id` and `targetOverId` are UniqueIdentifier values 
(may be numbers), while `fullFlattenedItems[].uuid` are strings; comparing them 
directly can fail when IDs are numeric (e.g. 0). Coerce identifiers to strings 
when comparing to `uuid` to avoid failing to find the indexes and 
early-returning.
   
   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,561 @@
+/**
+ * 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 '@superset-ui/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,
+} from '../constants';
+import { buildTree, getProjection, serializeForAPI } from '../treeUtils';
+
+interface UseDragHandlersProps {
+  items: TreeItemType[];
+  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({
+  items,
+  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);
+  const [projectedParentId, setProjectedParentId] = useState<string | null>(
+    null,
+  );
+  const [draggedItemIds, setDraggedItemIds] = useState<Set<string>>(new Set());
+
+  const pendingParentIdRef = useRef<string | null>(null);
+  const rafIdRef = useRef<number | null>(null);
+
+  const scheduleProjectedParentUpdate = useCallback(
+    (newParentId: string | null) => {
+      pendingParentIdRef.current = newParentId;
+
+      // Only schedule if not already scheduled
+      if (rafIdRef.current === null) {
+        rafIdRef.current = requestAnimationFrame(() => {
+          rafIdRef.current = null;
+          setProjectedParentId(prev =>
+            prev === pendingParentIdRef.current
+              ? prev
+              : pendingParentIdRef.current,
+          );
+        });
+      }
+    },
+    [],
+  );
+
+  const flattenedItems = useMemo(
+    () => computeFlattenedItems(activeId),
+    [computeFlattenedItems, activeId],
+  );
+
+  const flattenedItemsIndexMap = useMemo(() => {
+    const map = new Map<string, number>();
+    flattenedItems.forEach((item, index) => {
+      map.set(item.uuid, index);
+    });
+    return map;
+  }, [flattenedItems]);
+
+  const resetDragState = useCallback(() => {
+    setActiveId(null);
+    setOverId(null);
+    offsetLeftRef.current = 0;
+    setProjectedParentId(null);
+    setDraggedItemIds(new Set());
+    setDragOverlayWidth(null);
+    if (rafIdRef.current !== null) {
+      cancelAnimationFrame(rafIdRef.current);
+      rafIdRef.current = null;
+    }
+    pendingParentIdRef.current = null;
+  }, []);
+
+  const handleDragStart = ({ active }: DragStartEvent) => {
+    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', '');
+          scheduleProjectedParentUpdate(folderId);
+          return;

Review Comment:
   **Suggestion:** The handler checks `if (activeId && overId)` which treats 
numeric IDs of 0 as falsy and will skip projection logic; use explicit 
null/undefined checks (`!= null`) so valid numeric IDs (including 0) are 
handled. [logic error]
   
   **Severity Level:** Minor ⚠️
   ```suggestion
             if (activeId != null && overId != null) {
   ```
   <details>
   <summary><b>Why it matters? ⭐ </b></summary>
   
   activeId and overId are UniqueIdentifier | null. Using a truthy check (if 
(activeId && overId)) will treat valid numeric IDs like 0 as falsy and skip the 
projection logic. Replacing with explicit null/undefined checks (activeId != 
null && overId != null) is a small correctness fix that avoids a rare but real 
bug.
   </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:** 142:142
   **Comment:**
        *Logic Error: The handler checks `if (activeId && overId)` which treats 
numeric IDs of 0 as falsy and will skip projection logic; use explicit 
null/undefined checks (`!= null`) so valid numeric IDs (including 0) are 
handled.
   
   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,561 @@
+/**
+ * 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 '@superset-ui/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,
+} from '../constants';
+import { buildTree, getProjection, serializeForAPI } from '../treeUtils';
+
+interface UseDragHandlersProps {
+  items: TreeItemType[];
+  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({
+  items,
+  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);
+  const [projectedParentId, setProjectedParentId] = useState<string | null>(
+    null,
+  );
+  const [draggedItemIds, setDraggedItemIds] = useState<Set<string>>(new Set());
+
+  const pendingParentIdRef = useRef<string | null>(null);
+  const rafIdRef = useRef<number | null>(null);
+
+  const scheduleProjectedParentUpdate = useCallback(
+    (newParentId: string | null) => {
+      pendingParentIdRef.current = newParentId;
+
+      // Only schedule if not already scheduled
+      if (rafIdRef.current === null) {
+        rafIdRef.current = requestAnimationFrame(() => {
+          rafIdRef.current = null;
+          setProjectedParentId(prev =>
+            prev === pendingParentIdRef.current
+              ? prev
+              : pendingParentIdRef.current,
+          );
+        });
+      }
+    },
+    [],
+  );
+
+  const flattenedItems = useMemo(
+    () => computeFlattenedItems(activeId),
+    [computeFlattenedItems, activeId],
+  );
+
+  const flattenedItemsIndexMap = useMemo(() => {
+    const map = new Map<string, number>();
+    flattenedItems.forEach((item, index) => {
+      map.set(item.uuid, index);
+    });
+    return map;
+  }, [flattenedItems]);
+
+  const resetDragState = useCallback(() => {
+    setActiveId(null);
+    setOverId(null);
+    offsetLeftRef.current = 0;
+    setProjectedParentId(null);
+    setDraggedItemIds(new Set());
+    setDragOverlayWidth(null);
+    if (rafIdRef.current !== null) {
+      cancelAnimationFrame(rafIdRef.current);
+      rafIdRef.current = null;
+    }
+    pendingParentIdRef.current = null;
+  }, []);
+
+  const handleDragStart = ({ active }: DragStartEvent) => {
+    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', '');
+          scheduleProjectedParentUpdate(folderId);
+          return;
+        }
+
+        const projection = getProjection(
+          flattenedItems,
+          activeId,
+          overId,
+          delta.x,
+          DRAG_INDENTATION_WIDTH,
+          flattenedItemsIndexMap,
+        );
+        const newParentId = projection?.parentId ?? null;
+        scheduleProjectedParentUpdate(newParentId);
+      }
+    },
+    [
+      activeId,
+      overId,
+      flattenedItems,
+      flattenedItemsIndexMap,
+      scheduleProjectedParentUpdate,
+    ],
+  );
+
+  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', '');
+          scheduleProjectedParentUpdate(folderId);
+          return;
+        }
+
+        const projection = getProjection(
+          flattenedItems,
+          activeId,
+          over.id,
+          offsetLeftRef.current,
+          DRAG_INDENTATION_WIDTH,
+          flattenedItemsIndexMap,
+        );
+        const newParentId = projection?.parentId ?? null;
+        scheduleProjectedParentUpdate(newParentId);
+      } else {
+        scheduleProjectedParentUpdate(null);
+      }
+    },
+    [
+      activeId,
+      flattenedItems,
+      flattenedItemsIndexMap,
+      scheduleProjectedParentUpdate,
+    ],
+  );
+
+  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 = fullFlattenedItems.findIndex(
+      ({ uuid }) => uuid === active.id,
+    );
+    const overIndex = fullFlattenedItems.findIndex(
+      ({ uuid }) => uuid === targetOverId,
+    );
+
+    if (activeIndex === -1 || overIndex === -1) {
+      return;
+    }
+
+    const draggedItems = fullFlattenedItems.filter((item: FlattenedTreeItem) =>
+      itemsBeingDragged.includes(item.uuid),
+    );
+
+    let projectedPosition = getProjection(
+      flattenedItems,
+      active.id,
+      targetOverId,
+      finalOffsetLeft,
+      DRAG_INDENTATION_WIDTH,
+    );

Review Comment:
   **Suggestion:** The call to `getProjection` in the drag-end handler omits 
the `flattenedItemsIndexMap` argument that other calls pass; if `getProjection` 
relies on that map the incorrect call can return a wrong projection or 
undefined, causing incorrect nesting/movement or early returns — add 
`flattenedItemsIndexMap` as the final argument to match other usages. [possible 
bug]
   
   **Severity Level:** Critical 🚨
   ```suggestion
         flattenedItemsIndexMap,
   ```
   <details>
   <summary><b>Why it matters? ⭐ </b></summary>
   
   Other calls to getProjection in this file pass flattenedItemsIndexMap as the 
final argument (see handleDragMove/handleDragOver). The drag-end call omits 
that map, which is likely part of getProjection's inputs (the function is 
consistently called with that map elsewhere). Omitting it can change projection 
results or return undefined, causing incorrect insertion decisions or early 
returns. Adding the missing argument aligns this call with the rest of the file 
and fixes a real bug risk.
   </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:** 240:240
   **Comment:**
        *Possible Bug: The call to `getProjection` in the drag-end handler 
omits the `flattenedItemsIndexMap` argument that other calls pass; if 
`getProjection` relies on that map the incorrect call can return a wrong 
projection or undefined, causing incorrect nesting/movement or early returns — 
add `flattenedItemsIndexMap` as the final argument to match other usages.
   
   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