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);
