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={} /> - - - - - + + + + + + + + {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;