Skip to content
Snippets Groups Projects
Unverified Commit 50b88711 authored by Alexander Polyankin's avatar Alexander Polyankin Committed by GitHub
Browse files

Add Select all to dashboard parameters (#47897)

parent 16e31051
No related branches found
No related tags found
No related merge requests found
Showing
with 156 additions and 9 deletions
.loki/reference/chrome_laptop_embed_PublicOrEmbeddedDashboardView_filters_Dark_Theme_Number.png

67.8 KiB | W: | H:

.loki/reference/chrome_laptop_embed_PublicOrEmbeddedDashboardView_filters_Dark_Theme_Number.png

67.8 KiB | W: | H:

.loki/reference/chrome_laptop_embed_PublicOrEmbeddedDashboardView_filters_Dark_Theme_Number.png
.loki/reference/chrome_laptop_embed_PublicOrEmbeddedDashboardView_filters_Dark_Theme_Number.png
.loki/reference/chrome_laptop_embed_PublicOrEmbeddedDashboardView_filters_Dark_Theme_Number.png
.loki/reference/chrome_laptop_embed_PublicOrEmbeddedDashboardView_filters_Dark_Theme_Number.png
  • 2-up
  • Swipe
  • Onion skin
.loki/reference/chrome_laptop_embed_PublicOrEmbeddedDashboardView_filters_Dark_Theme_Parameter_List.png

73.1 KiB | W: | H:

.loki/reference/chrome_laptop_embed_PublicOrEmbeddedDashboardView_filters_Dark_Theme_Parameter_List.png

74 KiB | W: | H:

.loki/reference/chrome_laptop_embed_PublicOrEmbeddedDashboardView_filters_Dark_Theme_Parameter_List.png
.loki/reference/chrome_laptop_embed_PublicOrEmbeddedDashboardView_filters_Dark_Theme_Parameter_List.png
.loki/reference/chrome_laptop_embed_PublicOrEmbeddedDashboardView_filters_Dark_Theme_Parameter_List.png
.loki/reference/chrome_laptop_embed_PublicOrEmbeddedDashboardView_filters_Dark_Theme_Parameter_List.png
  • 2-up
  • Swipe
  • Onion skin
.loki/reference/chrome_laptop_embed_PublicOrEmbeddedDashboardView_filters_Dark_Theme_Parameter_Search.png

70.9 KiB | W: | H:

.loki/reference/chrome_laptop_embed_PublicOrEmbeddedDashboardView_filters_Dark_Theme_Parameter_Search.png

70.7 KiB | W: | H:

.loki/reference/chrome_laptop_embed_PublicOrEmbeddedDashboardView_filters_Dark_Theme_Parameter_Search.png
.loki/reference/chrome_laptop_embed_PublicOrEmbeddedDashboardView_filters_Dark_Theme_Parameter_Search.png
.loki/reference/chrome_laptop_embed_PublicOrEmbeddedDashboardView_filters_Dark_Theme_Parameter_Search.png
.loki/reference/chrome_laptop_embed_PublicOrEmbeddedDashboardView_filters_Dark_Theme_Parameter_Search.png
  • 2-up
  • Swipe
  • Onion skin
.loki/reference/chrome_laptop_embed_PublicOrEmbeddedDashboardView_filters_Light_Theme_Parameter_List.png

72.5 KiB | W: | H:

.loki/reference/chrome_laptop_embed_PublicOrEmbeddedDashboardView_filters_Light_Theme_Parameter_List.png

73.6 KiB | W: | H:

.loki/reference/chrome_laptop_embed_PublicOrEmbeddedDashboardView_filters_Light_Theme_Parameter_List.png
.loki/reference/chrome_laptop_embed_PublicOrEmbeddedDashboardView_filters_Light_Theme_Parameter_List.png
.loki/reference/chrome_laptop_embed_PublicOrEmbeddedDashboardView_filters_Light_Theme_Parameter_List.png
.loki/reference/chrome_laptop_embed_PublicOrEmbeddedDashboardView_filters_Light_Theme_Parameter_List.png
  • 2-up
  • Swipe
  • Onion skin
.loki/reference/chrome_laptop_embed_PublicOrEmbeddedDashboardView_filters_Light_Theme_Parameter_Search.png

71.3 KiB | W: | H:

.loki/reference/chrome_laptop_embed_PublicOrEmbeddedDashboardView_filters_Light_Theme_Parameter_Search.png

71 KiB | W: | H:

.loki/reference/chrome_laptop_embed_PublicOrEmbeddedDashboardView_filters_Light_Theme_Parameter_Search.png
.loki/reference/chrome_laptop_embed_PublicOrEmbeddedDashboardView_filters_Light_Theme_Parameter_Search.png
.loki/reference/chrome_laptop_embed_PublicOrEmbeddedDashboardView_filters_Light_Theme_Parameter_Search.png
.loki/reference/chrome_laptop_embed_PublicOrEmbeddedDashboardView_filters_Light_Theme_Parameter_Search.png
  • 2-up
  • Swipe
  • Onion skin
......@@ -26,7 +26,3 @@ export const OptionsList = styled.ul<OptionListProps>`
export const OptionContainer = styled.li`
padding: 0.5rem 0.125rem;
`;
export const LabelWrapper = styled.div`
padding-left: 0.5rem;
`;
......@@ -14,7 +14,6 @@ import type { RowValue } from "metabase-types/api";
import {
EmptyStateContainer,
FilterInputContainer,
LabelWrapper,
OptionContainer,
OptionsList,
} from "./ListField.styled";
......@@ -62,6 +61,8 @@ export const ListField = ({
const [filter, setFilter] = useState("");
const waitTime = useContext(waitTimeContext);
const debouncedFilter = useDebouncedValue(filter, waitTime);
const isAll = selectedValues.size === sortedOptions.length;
const isNone = selectedValues.size === 0;
const filteredOptions = useMemo(() => {
const formattedFilter = debouncedFilter.trim().toLowerCase();
......@@ -112,6 +113,17 @@ export const ListField = ({
const handleFilterChange: InputProps["onChange"] = e =>
setFilter(e.target.value);
const handleToggleAll = () => {
if (isAll) {
setSelectedValues(new Set());
onChange([]);
} else {
const allValues = sortedOptions.map(([value]) => value);
setSelectedValues(new Set(allValues));
onChange(allValues);
}
};
return (
<>
<FilterInputContainer isDashboardFilter={isDashboardFilter}>
......@@ -133,12 +145,22 @@ export const ListField = ({
)}
<OptionsList isDashboardFilter={isDashboardFilter}>
<OptionContainer>
<Checkbox
variant="stacked"
label={isAll ? `Select none` : t`Select all`}
checked={isAll}
indeterminate={!isAll && !isNone}
fw="bold"
onChange={handleToggleAll}
/>
</OptionContainer>
{filteredOptions.map((option, index) => (
<OptionContainer key={index}>
<Checkbox
data-testid={`${option[0]}-filter-value`}
checked={selectedValues.has(option[0])}
label={<LabelWrapper>{optionRenderer(option)}</LabelWrapper>}
label={optionRenderer(option)}
onChange={() => handleToggleOption(option[0])}
/>
</OptionContainer>
......
import userEvent from "@testing-library/user-event";
import type { JSX } from "react";
import { renderWithProviders, screen } from "__support__/ui";
import { waitTimeContext } from "metabase/context/wait-time";
import type { RowValue } from "metabase-types/api";
import { PRODUCT_CATEGORY_VALUES } from "metabase-types/api/mocks/presets";
import { ListField } from "./ListField";
import type { Option } from "./types";
type SetupOpts = {
value?: RowValue[];
options?: Option[];
optionRenderer?: (option: Option) => JSX.Element;
placeholder?: string;
checkedColor?: string;
isDashboardFilter?: boolean;
};
function setup({
value = [],
options = [],
optionRenderer = ([value]) => value,
placeholder = "Search the list",
checkedColor,
isDashboardFilter,
}: SetupOpts) {
const onChange = jest.fn();
renderWithProviders(
<waitTimeContext.Provider value={0}>
<ListField
value={value}
options={options}
optionRenderer={optionRenderer}
placeholder={placeholder}
checkedColor={checkedColor}
isDashboardFilter={isDashboardFilter}
onChange={onChange}
/>
,
</waitTimeContext.Provider>,
);
return { onChange };
}
describe("ListField", () => {
const allOptions = PRODUCT_CATEGORY_VALUES.values;
const allValues = allOptions.map(([value]) => String(value));
it("should allow to select all options", async () => {
const { onChange } = setup({
value: [],
options: allOptions,
});
const checkbox = screen.getByLabelText("Select all");
expect(checkbox).not.toBeChecked();
await userEvent.click(checkbox);
expect(onChange).toHaveBeenCalledWith(allValues);
});
it("should allow to select all options when some are selected", async () => {
const { onChange } = setup({
value: [allValues[0]],
options: allOptions,
});
const checkbox = screen.getByLabelText("Select all");
expect(checkbox).not.toBeChecked();
await userEvent.click(checkbox);
expect(onChange).toHaveBeenCalledWith(allValues);
});
it("should allow to select all options after search", async () => {
const { onChange } = setup({
value: [],
options: allOptions,
});
await userEvent.type(
screen.getByPlaceholderText("Search the list"),
allValues[0],
);
expect(screen.getByLabelText(allValues[0])).toBeInTheDocument();
expect(screen.queryByLabelText(allValues[1])).not.toBeInTheDocument();
const checkbox = screen.getByLabelText("Select all");
expect(checkbox).not.toBeChecked();
await userEvent.click(checkbox);
expect(onChange).toHaveBeenCalledWith(allValues);
});
it("should allow to deselect all options", async () => {
const { onChange } = setup({
value: allValues,
options: allOptions,
});
const checkbox = screen.getByLabelText("Select none");
expect(checkbox).toBeChecked();
await userEvent.click(checkbox);
expect(onChange).toHaveBeenCalledWith([]);
});
it("should allow to deselect all options after search", async () => {
const { onChange } = setup({
value: allValues,
options: allOptions,
});
await userEvent.type(
screen.getByPlaceholderText("Search the list"),
allValues[0],
);
expect(screen.getByLabelText(allValues[0])).toBeInTheDocument();
expect(screen.queryByLabelText(allValues[1])).not.toBeInTheDocument();
const checkbox = screen.getByLabelText("Select none");
expect(checkbox).toBeChecked();
await userEvent.click(checkbox);
expect(onChange).toHaveBeenCalledWith([]);
});
});
import type { JSX } from "react";
import type { RowValue } from "metabase-types/api";
export type Option = any[];
......
......@@ -3,9 +3,7 @@ import CS from "metabase/css/core/index.css";
import AutoLoadRemapped from "metabase/hoc/Remapped";
import { formatValue } from "metabase/lib/formatting";
const defaultRenderNormal = ({ value }) => (
<span className={CS.textBold}>{value}</span>
);
const defaultRenderNormal = ({ value }) => <span>{value}</span>;
const defaultRenderRemapped = ({ value, displayValue, column }) => (
<span>
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment