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

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


The following commit(s) were added to refs/heads/master by this push:
     new 11dfda11d31 fix(folders): expand collapsed folders on Select All and 
add selection counter (#38270)
11dfda11d31 is described below

commit 11dfda11d31d461ed1bede7cb42b8ab100a03047
Author: Kamil Gabryjelski <[email protected]>
AuthorDate: Fri Feb 27 07:28:07 2026 +0100

    fix(folders): expand collapsed folders on Select All and add selection 
counter (#38270)
    
    Co-authored-by: Claude Opus 4.6 <[email protected]>
---
 .../FoldersEditor/FoldersEditor.test.tsx           |  64 +++++++++++
 .../components/FoldersToolbarComponent.tsx         | 118 ++++++++++++++++-----
 .../components/Datasource/FoldersEditor/index.tsx  |  36 ++++++-
 .../components/Datasource/FoldersEditor/styles.tsx |  14 +++
 4 files changed, 204 insertions(+), 28 deletions(-)

diff --git 
a/superset-frontend/src/components/Datasource/FoldersEditor/FoldersEditor.test.tsx
 
b/superset-frontend/src/components/Datasource/FoldersEditor/FoldersEditor.test.tsx
index fbf3080cd14..4808f03a6dc 100644
--- 
a/superset-frontend/src/components/Datasource/FoldersEditor/FoldersEditor.test.tsx
+++ 
b/superset-frontend/src/components/Datasource/FoldersEditor/FoldersEditor.test.tsx
@@ -112,6 +112,11 @@ const mockFolders: DatasourceFolder[] = [
     name: 'Metrics',
     children: [
       { type: FoldersEditorItemType.Metric, uuid: 'metric1', name: 'Count' },
+      {
+        type: FoldersEditorItemType.Metric,
+        uuid: 'metric2',
+        name: 'Sum Revenue',
+      },
     ],
   },
   {
@@ -120,6 +125,7 @@ const mockFolders: DatasourceFolder[] = [
     name: 'Columns',
     children: [
       { type: FoldersEditorItemType.Column, uuid: 'col1', name: 'ID' },
+      { type: FoldersEditorItemType.Column, uuid: 'col2', name: 'name' },
     ],
   },
 ];
@@ -200,6 +206,31 @@ test('selects all items when Select all is clicked', async 
() => {
   });
 });
 
+test('shows item count and updates to selection count', async () => {
+  renderEditor(<FoldersEditor {...defaultProps} />);
+
+  // With nothing selected, counter shows total items (2 metrics + 2 columns = 
4)
+  expect(screen.getByText('4 items')).toBeInTheDocument();
+
+  // Click "Select all"
+  const selectAllButton = screen.getByText('Select all');
+  fireEvent.click(selectAllButton);
+
+  // Counter should show total selected out of total items
+  await waitFor(() => {
+    expect(screen.getByText('4 out of 4 selected')).toBeInTheDocument();
+  });
+
+  // Deselect all
+  const deselectAllButton = screen.getByText('Deselect all');
+  fireEvent.click(deselectAllButton);
+
+  // Counter should revert to item count
+  await waitFor(() => {
+    expect(screen.getByText('4 items')).toBeInTheDocument();
+  });
+});
+
 test('expands and collapses folders', async () => {
   renderEditor(<FoldersEditor {...defaultProps} />);
 
@@ -496,6 +527,39 @@ test('drag functionality integrates properly with 
selection state', () => {
   expect(checkboxes.length).toBeGreaterThan(0);
 });
 
+test('select all expands collapsed folders', async () => {
+  renderEditor(<FoldersEditor {...defaultProps} />);
+
+  // Folder should be expanded by default, so Count should be visible
+  expect(screen.getByText('Count')).toBeInTheDocument();
+
+  // Collapse the Metrics folder
+  const downIcons = screen.getAllByRole('img', { name: 'down' });
+  fireEvent.click(downIcons[0]);
+
+  await waitFor(() => {
+    expect(screen.queryByText('Count')).not.toBeInTheDocument();
+  });
+
+  // Click "Select all"
+  const selectAllButton = screen.getByText('Select all');
+  fireEvent.click(selectAllButton);
+
+  // The collapsed folder should be expanded and items should be visible
+  await waitFor(() => {
+    expect(screen.getByText('Count')).toBeInTheDocument();
+  });
+
+  // All checkboxes should be checked
+  const checkboxes = screen.getAllByRole('checkbox');
+  const nonButtonCheckboxes = checkboxes.filter(
+    checkbox => !checkbox.closest('button'),
+  );
+  nonButtonCheckboxes.forEach(checkbox => {
+    expect(checkbox).toBeChecked();
+  });
+});
+
 test('nested folders with items remain visible after drag is cancelled', async 
() => {
   const onChange = jest.fn();
   const nestedFolders: DatasourceFolder[] = [
diff --git 
a/superset-frontend/src/components/Datasource/FoldersEditor/components/FoldersToolbarComponent.tsx
 
b/superset-frontend/src/components/Datasource/FoldersEditor/components/FoldersToolbarComponent.tsx
index 8fe6718c254..e62cff18444 100644
--- 
a/superset-frontend/src/components/Datasource/FoldersEditor/components/FoldersToolbarComponent.tsx
+++ 
b/superset-frontend/src/components/Datasource/FoldersEditor/components/FoldersToolbarComponent.tsx
@@ -17,11 +17,17 @@
  * under the License.
  */
 
-import { memo } from 'react';
-import { t } from '@apache-superset/core';
-import { Button, Input } from '@superset-ui/core/components';
+import { memo, useMemo } from 'react';
+import { t, tn } from '@apache-superset/core';
+import { Button, Input, Tooltip } from '@superset-ui/core/components';
 import { Icons } from '@superset-ui/core/components/Icons';
-import { FoldersToolbar, FoldersSearch, FoldersActions } from '../styles';
+import {
+  FoldersToolbar,
+  FoldersSearch,
+  FoldersActions,
+  FoldersActionsRow,
+  SelectionCount,
+} from '../styles';
 
 interface FoldersToolbarComponentProps {
   onSearch: (e: React.ChangeEvent<HTMLInputElement>) => void;
@@ -29,6 +35,10 @@ interface FoldersToolbarComponentProps {
   onSelectAll: () => void;
   onResetToDefault: () => void;
   allVisibleSelected: boolean;
+  selectedColumnsCount: number;
+  selectedMetricsCount: number;
+  totalColumnsCount: number;
+  totalMetricsCount: number;
 }
 
 function FoldersToolbarComponentInner({
@@ -37,7 +47,56 @@ function FoldersToolbarComponentInner({
   onSelectAll,
   onResetToDefault,
   allVisibleSelected,
+  selectedColumnsCount,
+  selectedMetricsCount,
+  totalColumnsCount,
+  totalMetricsCount,
 }: FoldersToolbarComponentProps) {
+  const selectedCount = selectedColumnsCount + selectedMetricsCount;
+  const totalCount = totalColumnsCount + totalMetricsCount;
+
+  const tooltipTitle = useMemo(() => {
+    if (selectedCount > 0) {
+      return (
+        <>
+          {tn(
+            '%s out of %s column',
+            '%s out of %s columns',
+            totalColumnsCount,
+            selectedColumnsCount,
+            totalColumnsCount,
+          )}
+          <br />
+          {tn(
+            '%s out of %s metric',
+            '%s out of %s metrics',
+            totalMetricsCount,
+            selectedMetricsCount,
+            totalMetricsCount,
+          )}
+        </>
+      );
+    }
+    return (
+      <>
+        {tn('%s column', '%s columns', totalColumnsCount, totalColumnsCount)}
+        <br />
+        {tn('%s metric', '%s metrics', totalMetricsCount, totalMetricsCount)}
+      </>
+    );
+  }, [
+    selectedCount,
+    selectedColumnsCount,
+    selectedMetricsCount,
+    totalColumnsCount,
+    totalMetricsCount,
+  ]);
+
+  const counterText =
+    selectedCount > 0
+      ? t('%s out of %s selected', selectedCount, totalCount)
+      : tn('%s item', '%s items', totalCount, totalCount);
+
   return (
     <FoldersToolbar>
       <FoldersSearch>
@@ -48,29 +107,34 @@ function FoldersToolbarComponentInner({
           prefix={<Icons.SearchOutlined />}
         />
       </FoldersSearch>
-      <FoldersActions>
-        <Button
-          buttonStyle="link"
-          onClick={onAddFolder}
-          icon={<Icons.PlusOutlined />}
-        >
-          {t('Add folder')}
-        </Button>
-        <Button
-          buttonStyle="link"
-          onClick={onSelectAll}
-          icon={<Icons.CheckOutlined />}
-        >
-          {allVisibleSelected ? t('Deselect all') : t('Select all')}
-        </Button>
-        <Button
-          buttonStyle="link"
-          onClick={onResetToDefault}
-          icon={<Icons.HistoryOutlined />}
-        >
-          {t('Reset all folders to default')}
-        </Button>
-      </FoldersActions>
+      <FoldersActionsRow>
+        <FoldersActions>
+          <Button
+            buttonStyle="link"
+            onClick={onAddFolder}
+            icon={<Icons.PlusOutlined />}
+          >
+            {t('Add folder')}
+          </Button>
+          <Button
+            buttonStyle="link"
+            onClick={onSelectAll}
+            icon={<Icons.CheckOutlined />}
+          >
+            {allVisibleSelected ? t('Deselect all') : t('Select all')}
+          </Button>
+          <Button
+            buttonStyle="link"
+            onClick={onResetToDefault}
+            icon={<Icons.HistoryOutlined />}
+          >
+            {t('Reset all folders to default')}
+          </Button>
+        </FoldersActions>
+        <Tooltip title={tooltipTitle}>
+          <SelectionCount>{counterText}</SelectionCount>
+        </Tooltip>
+      </FoldersActionsRow>
     </FoldersToolbar>
   );
 }
diff --git 
a/superset-frontend/src/components/Datasource/FoldersEditor/index.tsx 
b/superset-frontend/src/components/Datasource/FoldersEditor/index.tsx
index 688adb3faee..4733dc91726 100644
--- a/superset-frontend/src/components/Datasource/FoldersEditor/index.tsx
+++ b/superset-frontend/src/components/Datasource/FoldersEditor/index.tsx
@@ -205,8 +205,28 @@ export default function FoldersEditor({
       setSelectedItemIds(new Set());
     } else {
       setSelectedItemIds(itemsToSelect);
+      // Expand ancestor folders of selected items
+      const parentMap = new Map<string, string | null>();
+      for (const item of fullFlattenedItems) {
+        parentMap.set(item.uuid, item.parentId);
+      }
+      const foldersToExpand = new Set<string>();
+      for (const id of itemsToSelect) {
+        let parentId = parentMap.get(id) ?? null;
+        while (parentId) {
+          foldersToExpand.add(parentId);
+          parentId = parentMap.get(parentId) ?? null;
+        }
+      }
+      setCollapsedIds(prev => {
+        const newSet = new Set(prev);
+        for (const folderId of foldersToExpand) {
+          newSet.delete(folderId);
+        }
+        return newSet;
+      });
     }
-  }, [visibleItemIds, fullItemsByUuid, allVisibleSelected]);
+  }, [visibleItemIds, fullItemsByUuid, fullFlattenedItems, 
allVisibleSelected]);
 
   const handleResetToDefault = () => {
     setShowResetConfirm(true);
@@ -380,6 +400,16 @@ export default function FoldersEditor({
     [flattenedItems],
   );
 
+  const selectedMetricsCount = useMemo(() => {
+    let count = 0;
+    for (const id of selectedItemIds) {
+      if (metricsMap.has(id)) {
+        count += 1;
+      }
+    }
+    return count;
+  }, [selectedItemIds, metricsMap]);
+
   const folderChildCounts = useMemo(() => {
     const counts = new Map<string, number>();
     // Initialize all folders with 0
@@ -410,6 +440,10 @@ export default function FoldersEditor({
         onSelectAll={handleSelectAll}
         onResetToDefault={handleResetToDefault}
         allVisibleSelected={allVisibleSelected}
+        selectedMetricsCount={selectedMetricsCount}
+        selectedColumnsCount={selectedItemIds.size - selectedMetricsCount}
+        totalMetricsCount={metrics.length}
+        totalColumnsCount={columns.length}
       />
       <FoldersContent ref={contentRef}>
         <DndContext
diff --git 
a/superset-frontend/src/components/Datasource/FoldersEditor/styles.tsx 
b/superset-frontend/src/components/Datasource/FoldersEditor/styles.tsx
index 1c62cfa2802..7bd04b7a577 100644
--- a/superset-frontend/src/components/Datasource/FoldersEditor/styles.tsx
+++ b/superset-frontend/src/components/Datasource/FoldersEditor/styles.tsx
@@ -54,6 +54,20 @@ export const FoldersActions = styled.div`
   `}
 `;
 
+export const FoldersActionsRow = styled.div`
+  display: flex;
+  flex-direction: column;
+  gap: ${({ theme }) => theme.paddingXS}px;
+`;
+
+export const SelectionCount = styled.div`
+  ${({ theme }) => `
+    align-self: flex-end;
+    font-size: ${theme.fontSizeSM}px;
+    color: ${theme.colorTextSecondary};
+  `}
+`;
+
 export const FoldersContent = styled.div`
   flex: 1;
   min-height: 0;

Reply via email to