mirror of
https://github.com/apache/superset.git
synced 2026-04-18 23:55:00 +00:00
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:
committed by
GitHub
parent
0827ec3811
commit
11dfda11d3
@@ -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[] = [
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user