This is an automated email from the ASF dual-hosted git repository.

kgabryje pushed a commit to branch folders
in repository https://gitbox.apache.org/repos/asf/superset.git

commit 61b3feb2f0c658b1b9ebb28bb6b0cc02e1b8d7b0
Author: Kamil Gabryjelski <[email protected]>
AuthorDate: Thu Jan 29 16:12:23 2026 +0100

    Range select
---
 .../Datasource/FoldersEditor/TreeItem.styles.ts    |  3 +-
 .../Datasource/FoldersEditor/TreeItem.tsx          |  7 ++-
 .../FoldersEditor/VirtualizedTreeItem.tsx          |  4 +-
 .../FoldersEditor/VirtualizedTreeList.tsx          |  2 +-
 .../components/Datasource/FoldersEditor/index.tsx  | 55 ++++++++++++++++++----
 5 files changed, 53 insertions(+), 18 deletions(-)

diff --git 
a/superset-frontend/src/components/Datasource/FoldersEditor/TreeItem.styles.ts 
b/superset-frontend/src/components/Datasource/FoldersEditor/TreeItem.styles.ts
index ce46739efb9..5e1f4febc41 100644
--- 
a/superset-frontend/src/components/Datasource/FoldersEditor/TreeItem.styles.ts
+++ 
b/superset-frontend/src/components/Datasource/FoldersEditor/TreeItem.styles.ts
@@ -187,7 +187,8 @@ export const EmptyFolderDropZone = styled.div<{
   isForbidden: boolean;
 }>`
   ${({ theme, depth, isOver, isForbidden }) => css`
-    margin: ${theme.marginXS}px ${depth * ITEM_INDENTATION_WIDTH + 
theme.marginMD}px 0;
+    margin: ${theme.marginXS}px
+      ${depth * ITEM_INDENTATION_WIDTH + theme.marginMD}px 0;
     padding: ${theme.paddingLG}px;
     border: 2px dashed
       ${isOver
diff --git 
a/superset-frontend/src/components/Datasource/FoldersEditor/TreeItem.tsx 
b/superset-frontend/src/components/Datasource/FoldersEditor/TreeItem.tsx
index 27479a2c454..4045d921915 100644
--- a/superset-frontend/src/components/Datasource/FoldersEditor/TreeItem.tsx
+++ b/superset-frontend/src/components/Datasource/FoldersEditor/TreeItem.tsx
@@ -34,7 +34,6 @@ import {
   ColumnTypeLabel,
   Metric,
 } from '@superset-ui/chart-controls';
-import type { CheckboxChangeEvent } from 'antd/es/checkbox';
 import {
   OptionControlContainer,
   Label,
@@ -70,7 +69,7 @@ interface TreeItemProps {
   isSelected?: boolean;
   isEditing?: boolean;
   onToggleCollapse?: (id: string) => void;
-  onSelect?: (id: string, selected: boolean) => void;
+  onSelect?: (id: string, selected: boolean, shiftKey?: boolean) => void;
   onStartEdit?: (id: string) => void;
   onFinishEdit?: (id: string, newName: string) => void;
   isDefaultFolder?: boolean;
@@ -274,10 +273,10 @@ function TreeItemComponent({
         <Checkbox
           checked={isSelected}
           disabled={isOverlay}
-          onChange={(e: CheckboxChangeEvent) => {
+          onClick={(e: React.MouseEvent) => {
             if (!isOverlay) {
               e.stopPropagation();
-              onSelect?.(id, e.target.checked);
+              onSelect?.(id, !isSelected, e.shiftKey);
             }
           }}
           css={theme => css`
diff --git 
a/superset-frontend/src/components/Datasource/FoldersEditor/VirtualizedTreeItem.tsx
 
b/superset-frontend/src/components/Datasource/FoldersEditor/VirtualizedTreeItem.tsx
index ddb4ab2f99c..39fa42d19df 100644
--- 
a/superset-frontend/src/components/Datasource/FoldersEditor/VirtualizedTreeItem.tsx
+++ 
b/superset-frontend/src/components/Datasource/FoldersEditor/VirtualizedTreeItem.tsx
@@ -64,7 +64,7 @@ export interface VirtualizedTreeItemData {
   forbiddenDropFolderIds: Set<string>;
   currentDropTargetId: string | null;
   onToggleCollapse: (id: string) => void;
-  onSelect: (id: string, selected: boolean) => void;
+  onSelect: (id: string, selected: boolean, shiftKey?: boolean) => void;
   onStartEdit: (id: string) => void;
   onFinishEdit: (id: string, newName: string) => void;
 }
@@ -84,7 +84,7 @@ interface TreeItemWrapperProps {
   metric?: Metric;
   column?: ColumnMeta;
   onToggleCollapse?: (id: string) => void;
-  onSelect?: (id: string, selected: boolean) => void;
+  onSelect?: (id: string, selected: boolean, shiftKey?: boolean) => void;
   onStartEdit?: (id: string) => void;
   onFinishEdit?: (id: string, newName: string) => void;
 }
diff --git 
a/superset-frontend/src/components/Datasource/FoldersEditor/VirtualizedTreeList.tsx
 
b/superset-frontend/src/components/Datasource/FoldersEditor/VirtualizedTreeList.tsx
index 8bbeaf8c924..a90d4eeb3e3 100644
--- 
a/superset-frontend/src/components/Datasource/FoldersEditor/VirtualizedTreeList.tsx
+++ 
b/superset-frontend/src/components/Datasource/FoldersEditor/VirtualizedTreeList.tsx
@@ -51,7 +51,7 @@ interface VirtualizedTreeListProps {
   forbiddenDropFolderIds: Set<string>;
   currentDropTargetId: string | null;
   onToggleCollapse: (id: string) => void;
-  onSelect: (id: string, selected: boolean) => void;
+  onSelect: (id: string, selected: boolean, shiftKey?: boolean) => void;
   onStartEdit: (id: string) => void;
   onFinishEdit: (id: string, newName: string) => void;
 }
diff --git 
a/superset-frontend/src/components/Datasource/FoldersEditor/index.tsx 
b/superset-frontend/src/components/Datasource/FoldersEditor/index.tsx
index 49ce729b9e3..8fe2f69b687 100644
--- a/superset-frontend/src/components/Datasource/FoldersEditor/index.tsx
+++ b/superset-frontend/src/components/Datasource/FoldersEditor/index.tsx
@@ -17,7 +17,7 @@
  * under the License.
  */
 
-import { useCallback, useMemo, useState } from 'react';
+import { useCallback, useMemo, useRef, useState } from 'react';
 import { debounce } from 'lodash';
 import AutoSizer from 'react-virtualized-auto-sizer';
 import {
@@ -83,6 +83,7 @@ export default function FoldersEditor({
   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);
@@ -238,17 +239,51 @@ export default function FoldersEditor({
     });
   }, []);
 
-  const handleSelect = useCallback((itemId: string, selected: boolean) => {
-    setSelectedItemIds(prev => {
-      const newSet = new Set(prev);
+  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) {
-        newSet.add(itemId);
-      } else {
-        newSet.delete(itemId);
+        lastSelectedItemIdRef.current = itemId;
       }
-      return newSet;
-    });
-  }, []);
+
+      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],
+  );
 
   const handleStartEdit = useCallback((folderId: string) => {
     setEditingFolderId(folderId);

Reply via email to