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();
+
+ // 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();
@@ -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();
+
+ // 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) => 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,
+ )}
+
+ {tn(
+ '%s out of %s metric',
+ '%s out of %s metrics',
+ totalMetricsCount,
+ selectedMetricsCount,
+ totalMetricsCount,
+ )}
+ >
+ );
+ }
+ return (
+ <>
+ {tn('%s column', '%s columns', totalColumnsCount, totalColumnsCount)}
+
+ {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 (
@@ -48,29 +107,34 @@ function FoldersToolbarComponentInner({
prefix={}
/>
-
- }
- >
- {t('Add folder')}
-
- }
- >
- {allVisibleSelected ? t('Deselect all') : t('Select all')}
-
- }
- >
- {t('Reset all folders to default')}
-
-
+
+
+ }
+ >
+ {t('Add folder')}
+
+ }
+ >
+ {allVisibleSelected ? t('Deselect all') : t('Select all')}
+
+ }
+ >
+ {t('Reset all folders to default')}
+
+
+
+ {counterText}
+
+
);
}
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();
+ for (const item of fullFlattenedItems) {
+ parentMap.set(item.uuid, item.parentId);
+ }
+ const foldersToExpand = new Set();
+ 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();
// 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}
/>
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;