fix(folders): expand collapsed folders on Select All and add selection counter (#38270)

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Kamil Gabryjelski
2026-02-27 07:28:07 +01:00
committed by GitHub
parent 0827ec3811
commit 11dfda11d3
4 changed files with 204 additions and 28 deletions

View File

@@ -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[] = [

View File

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

View File

@@ -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

View File

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