feat(filters): Adding empty state for filter modal (#38909)

This commit is contained in:
Alexandru Soare
2026-04-15 15:59:11 +03:00
committed by GitHub
parent ffcc6e8b63
commit 411f769896
6 changed files with 162 additions and 50 deletions

View File

@@ -75,6 +75,7 @@ export const BaseModalWrapper = styled(StyledModal)<BaseModalWrapperProps>`
export const BaseModalBody = styled.div<BaseModalBodyProps>`
display: flex;
height: 100%;
min-height: 500px;
flex-direction: row;
flex: 1;

View File

@@ -17,7 +17,13 @@
* under the License.
*/
import { act, render, screen, userEvent } from 'spec/helpers/testing-library';
import {
act,
fireEvent,
render,
screen,
userEvent,
} from 'spec/helpers/testing-library';
import { stateWithoutNativeFilters } from 'spec/fixtures/mockStore';
import { testWithId } from 'src/utils/testUtils';
import { Preset, makeApi } from '@superset-ui/core';
@@ -387,6 +393,15 @@ test('FilterBar apply button is disabled after creating a filter', async () => {
userEvent.click(screen.getByTestId(getTestId('collapsable')));
userEvent.click(screen.getByLabelText('setting'));
userEvent.click(screen.getByText('Add or edit filters and controls'));
// First add a filter via the dropdown (modal now shows empty state by default)
const dropdownButton = screen.getByTestId('new-item-dropdown-button');
fireEvent.mouseEnter(dropdownButton);
const addFilterMenuItem = await screen.findByRole('menuitem', {
name: /add filter/i,
});
fireEvent.click(addFilterMenuItem);
userEvent.click(screen.getByText('Value'));
userEvent.click(screen.getByText('Time range'));
userEvent.type(

View File

@@ -84,7 +84,7 @@ const FilterBarSettings = () => {
const { openFilterConfigModal, FilterConfigModalComponent } =
useFilterConfigModal({
createNewOnOpen: filterValues.length === 0,
createNewOnOpen: false,
dashboardId,
});

View File

@@ -25,7 +25,8 @@ import {
} from '@superset-ui/core';
import type { FormInstance } from '@superset-ui/core/components';
import { styled } from '@apache-superset/core/theme';
import { Flex } from '@superset-ui/core/components';
import { EmptyState, Flex } from '@superset-ui/core/components';
import { t } from '@apache-superset/core/translation';
import FilterContentRenderer from './FilterContentRenderer';
import CustomizationContentRenderer from './CustomizationContentRenderer';
import { FiltersConfigFormHandle } from '../FiltersConfigForm/FiltersConfigForm';
@@ -105,6 +106,28 @@ function ConfigModalContent({
isChartCustomizationId(currentItemId) &&
chartCustomizationIds.includes(currentItemId);
const hasNoItems =
filterState.orderedIds.length === 0 &&
customizationState.orderedIds.length === 0;
const showEmptyState = hasNoItems || !currentItemId;
if (showEmptyState) {
return (
<StyledContentFlex vertical>
<Flex flex={1}>
<EmptyState
size="small"
title=""
image="empty.svg"
description={t(
'Manage filters and customizations to set scoping, descriptions, and limitations. Create new elements for better dashboard insights.',
)}
/>
</Flex>
</StyledContentFlex>
);
}
return (
<StyledContentFlex vertical>
<div

View File

@@ -20,7 +20,7 @@ import { FC, ReactNode, useCallback, useState } from 'react';
import { t } from '@apache-superset/core/translation';
import { NativeFilterType, ChartCustomizationType } from '@superset-ui/core';
import { styled } from '@apache-superset/core/theme';
import { Collapse, Flex } from '@superset-ui/core/components';
import { Collapse, EmptyState, Flex } from '@superset-ui/core/components';
import type { DragEndEvent } from '@dnd-kit/core';
import {
DndContext,
@@ -49,6 +49,10 @@ const StyledSidebarFlex = styled(Flex)`
const StyledHeaderFlex = styled(Flex)`
padding: ${({ theme }) => theme.sizeUnit * 3}px;
& button {
width: 100%;
}
`;
// min-height: 0 lets the flex item shrink below its content size so that
@@ -264,6 +268,9 @@ const ConfigModalSidebar: FC<ConfigModalSidebarProps> = ({
</div>
);
const hasNoItems =
filterOrderedIds.length === 0 && customizationOrderedIds.length === 0;
return (
<DndContext
sensors={sensors}
@@ -279,54 +286,65 @@ const ConfigModalSidebar: FC<ConfigModalSidebarProps> = ({
onAddCustomization={onAddCustomization}
/>
</StyledHeaderFlex>
<StyledCollapse
key={formValuesVersion}
activeKey={activeCollapseKeys}
onChange={keys => onCollapseChange(keys as string[])}
ghost
isDragging={isDragging}
>
<StyledCollapse.Panel key="filters" header={filtersHeader}>
<ItemSectionContent
currentItemId={currentItemId}
items={filterOrderedIds}
removedItems={filterRemovedItems}
erroredItems={filterErroredItems}
getItemTitle={getTitle}
onChange={onChange}
onRearrange={onRearrange}
onRemove={onRemove}
restoreItem={restoreItem}
dataTestId="filter-title-container"
deleteAltText={t('Remove filter')}
dragType={FILTER_TYPE}
isCurrentSection={isFilterId(currentItemId)}
onCrossListDrop={handleFilterCrossListDrop}
{hasNoItems ? (
<Flex>
<EmptyState
size="small"
title=""
image="empty.svg"
description={t('No filters or customizations created yet')}
/>
</StyledCollapse.Panel>
<StyledCollapse.Panel
key="chartCustomizations"
header={customizationsHeader}
</Flex>
) : (
<StyledCollapse
key={formValuesVersion}
activeKey={activeCollapseKeys}
onChange={keys => onCollapseChange(keys as string[])}
ghost
isDragging={isDragging}
>
<ItemSectionContent
currentItemId={currentItemId}
items={customizationOrderedIds}
removedItems={customizationRemovedItems}
erroredItems={customizationErroredItems}
getItemTitle={getTitle}
onChange={onChange}
onRearrange={onRearrange}
onRemove={onRemove}
restoreItem={restoreItem}
dataTestId="customization-title-container"
deleteAltText={t('Remove customization')}
dragType={CUSTOMIZATION_TYPE}
isCurrentSection={isChartCustomizationId(currentItemId)}
onCrossListDrop={handleCustomizationCrossListDrop}
/>
</StyledCollapse.Panel>
</StyledCollapse>
<StyledCollapse.Panel key="filters" header={filtersHeader}>
<ItemSectionContent
currentItemId={currentItemId}
items={filterOrderedIds}
removedItems={filterRemovedItems}
erroredItems={filterErroredItems}
getItemTitle={getTitle}
onChange={onChange}
onRearrange={onRearrange}
onRemove={onRemove}
restoreItem={restoreItem}
dataTestId="filter-title-container"
deleteAltText={t('Remove filter')}
dragType={FILTER_TYPE}
isCurrentSection={isFilterId(currentItemId)}
onCrossListDrop={handleFilterCrossListDrop}
/>
</StyledCollapse.Panel>
<StyledCollapse.Panel
key="chartCustomizations"
header={customizationsHeader}
>
<ItemSectionContent
currentItemId={currentItemId}
items={customizationOrderedIds}
removedItems={customizationRemovedItems}
erroredItems={customizationErroredItems}
getItemTitle={getTitle}
onChange={onChange}
onRearrange={onRearrange}
onRemove={onRemove}
restoreItem={restoreItem}
dataTestId="customization-title-container"
deleteAltText={t('Remove customization')}
dragType={CUSTOMIZATION_TYPE}
isCurrentSection={isChartCustomizationId(currentItemId)}
onCrossListDrop={handleCustomizationCrossListDrop}
/>
</StyledCollapse.Panel>
</StyledCollapse>
)}
</StyledSidebarFlex>
</DndContext>
);

View File

@@ -772,3 +772,58 @@ test('renders a filter with a chart containing BigInt values', async () => {
expect(screen.getByText(FILTER_TYPE_REGEX)).toBeInTheDocument();
});
test('displays empty state when modal opens with no filters and createNewOnOpen is false', () => {
defaultRender(defaultState(), { ...props, createNewOnOpen: false });
// Check left panel empty state
expect(
screen.getByText('No filters or customizations created yet'),
).toBeInTheDocument();
// Check right panel empty state
expect(
screen.getByText(
/Manage filters and customizations to set scoping, descriptions, and limitations/,
),
).toBeInTheDocument();
// Verify no filter form is rendered (no "Untitled" filter created)
expect(screen.queryByText(FILTER_TYPE_REGEX)).not.toBeInTheDocument();
});
test('does not auto-create a filter when createNewOnOpen is false', () => {
defaultRender(defaultState(), { ...props, createNewOnOpen: false });
// The filter configuration form should not be visible
expect(screen.queryByText(FILTER_NAME_REGEX)).not.toBeInTheDocument();
expect(screen.queryByText(DATASET_REGEX)).not.toBeInTheDocument();
});
test('empty state disappears when a filter is added via dropdown', async () => {
defaultRender(defaultState(), {
...props,
createNewOnOpen: false,
});
// Verify empty state is shown initially
expect(
screen.getByText('No filters or customizations created yet'),
).toBeInTheDocument();
// Add a filter via the dropdown
const dropdownButton = screen.getByTestId('new-item-dropdown-button');
fireEvent.mouseEnter(dropdownButton);
const addFilterMenuItem = await screen.findByRole('menuitem', {
name: /add filter/i,
});
fireEvent.click(addFilterMenuItem);
// Verify empty state is gone and filter form is shown
await waitFor(() => {
expect(
screen.queryByText('No filters or customizations created yet'),
).not.toBeInTheDocument();
});
expect(screen.getByText(FILTER_TYPE_REGEX)).toBeInTheDocument();
});