Skip to content
Snippets Groups Projects
Unverified Commit badd7d08 authored by Ryan Laurie's avatar Ryan Laurie Committed by GitHub
Browse files

Combine filter header buttons (#46984)

* combine filter header buttons

* refined styling

* unit tests

* another import conflict

* omg another one

* remove double border for filter button

* use vars and update imports
parent 5edd793d
No related branches found
No related tags found
No related merge requests found
Showing
with 242 additions and 173 deletions
.FilterButton {
transition: background 300ms linear, border 300ms linear;
transition: background 300ms linear;
&:hover:not([data-css-specifity-hack="🤣"]) {
&:hover:not([data-css-specificity-hack="🤣"]) {
color: var(--mb-color-filter);
border-color: color-mix(in srgb, var(--mb-color-filter) 30%, white);
background-color: color-mix(in srgb, var(--mb-color-filter) 10%, white);
}
}
.FilterButtonAttachment {
&:not([data-css-specificity-hack="🤣"]) {
padding: 0.5rem;
}
transition: background 300ms linear;
&:hover:not([data-css-specificity-hack="🤣"]) {
color: var(--mb-color-filter);
background-color: color-mix(in srgb, var(--mb-color-filter) 10%, white);
.FilterCountChip {
background-color: var(--mb-color-filter);
}
}
&[data-expanded="true"] {
background-color: var(--mb-color-filter);
.FilterCountChip {
background-color: var(--mb-color-bg-white);
color: var(--mb-color-text-dark);
}
&:hover {
background-color: color-mix(in srgb, var(--mb-color-filter) 80%, white);
.FilterCountChip {
background-color: var(--mb-color-bg-white);
color: var(--mb-color-text-dark);
}
}
}
}
.FilterCountChip {
transition: background-color 300ms linear;
background-color: var(--mb-color-text-dark);
font-size: 0.6875rem;
border-radius: 10px;
line-height: 1rem;
padding-inline: 0.5rem;
color: var(--mb-color-text-white);
}
.SummarizeButton {
transition: background 300ms linear, border 300ms linear;
......
import cx from "classnames";
import { useMemo } from "react";
import { t } from "ttag";
import type { QueryModalType } from "metabase/query_builder/constants";
import { MODAL_TYPES } from "metabase/query_builder/constants";
import { getFilterItems } from "metabase/querying/filters/components/FilterPanel/utils";
import { Button } from "metabase/ui";
import * as Lib from "metabase-lib";
import type Question from "metabase-lib/v1/Question";
......@@ -13,21 +15,52 @@ import ViewTitleHeaderS from "../ViewTitleHeader.module.css";
interface FilterHeaderButtonProps {
className?: string;
onOpenModal: (modalType: QueryModalType) => void;
query?: Lib.Query;
isExpanded?: boolean;
onExpand?: () => void;
onCollapse?: () => void;
}
export function FilterHeaderButton({
className,
onOpenModal,
query,
isExpanded,
onExpand,
onCollapse,
}: FilterHeaderButtonProps) {
const label = isExpanded ? t`Hide filters` : t`Show filters`;
const items = useMemo(() => query && getFilterItems(query), [query]);
const shouldShowFilterPanelExpander = Boolean(
items?.length && onExpand && onCollapse,
);
return (
<Button
color="filter"
className={cx(className, ViewTitleHeaderS.FilterButton)}
onClick={() => onOpenModal(MODAL_TYPES.FILTERS)}
data-testid="question-filter-header"
>
{t`Filter`}
</Button>
<Button.Group>
<Button
color="filter"
className={cx(className, ViewTitleHeaderS.FilterButton)}
onClick={() => onOpenModal(MODAL_TYPES.FILTERS)}
data-testid="question-filter-header"
>
{t`Filter`}
</Button>
{shouldShowFilterPanelExpander && (
<Button
aria-label={label}
className={ViewTitleHeaderS.FilterButtonAttachment}
onClick={isExpanded ? onCollapse : onExpand}
data-testid="filters-visibility-control"
data-expanded={isExpanded}
style={{ borderLeft: "none" }} // mantine puts a double border between buttons in groups
>
<div className={ViewTitleHeaderS.FilterCountChip}>
{items?.length}
</div>
</Button>
)}
</Button.Group>
);
}
......
import { createMockMetadata } from "__support__/metadata";
import { renderWithProviders, screen } from "__support__/ui";
import Question from "metabase-lib/v1/Question";
import {
ORDERS,
ORDERS_ID,
PRODUCTS,
SAMPLE_DB_ID,
createSampleDatabase,
} from "metabase-types/api/mocks/presets";
import { FilterHeaderButton } from "./FilterHeaderButton";
const metadata = createMockMetadata({
databases: [createSampleDatabase()],
});
const QUERY_WITH_FILTERS = Question.create({
databaseId: SAMPLE_DB_ID,
tableId: ORDERS_ID,
metadata,
})
.legacyQuery({ useStructuredQuery: true })
.aggregate(["count"])
.filter(["time-interval", ["field", ORDERS.CREATED_AT, null], -30, "day"])
.filter(["=", ["field", ORDERS.TOTAL, null], 1234])
.filter([
"contains",
["field", PRODUCTS.TITLE, { "source-field": ORDERS.PRODUCT_ID }],
"asdf",
])
.question()
.query();
const QUERY_WITHOUT_FILTERS = Question.create({
databaseId: SAMPLE_DB_ID,
tableId: ORDERS_ID,
metadata,
})
.legacyQuery({ useStructuredQuery: true })
.aggregate(["count"])
.question()
.query();
describe("FilterHeaderButton", () => {
it("should render filter button", () => {
renderWithProviders(<FilterHeaderButton onOpenModal={jest.fn()} />);
expect(screen.getByRole("button", { name: "Filter" })).toBeInTheDocument();
expect(screen.getByTestId("question-filter-header")).toBeInTheDocument();
});
it("should not render filter count without a query", () => {
renderWithProviders(<FilterHeaderButton onOpenModal={jest.fn()} />);
expect(
screen.queryByTestId("filters-visibility-control"),
).not.toBeInTheDocument();
});
it("should render filter count when a query has filters", () => {
renderWithProviders(
<FilterHeaderButton
onOpenModal={jest.fn()}
query={QUERY_WITH_FILTERS}
isExpanded={false}
onExpand={jest.fn()}
onCollapse={jest.fn()}
/>,
);
expect(screen.getByTestId("filters-visibility-control")).toHaveTextContent(
"3",
);
});
it("should not render filter count when a query has 0 filters", () => {
renderWithProviders(
<FilterHeaderButton
onOpenModal={jest.fn()}
query={QUERY_WITHOUT_FILTERS}
isExpanded={false}
onExpand={jest.fn()}
onCollapse={jest.fn()}
/>,
);
expect(
screen.queryByTestId("filters-visibility-control"),
).not.toBeInTheDocument();
});
it("should not render filter count without onCollapse function", () => {
renderWithProviders(
<FilterHeaderButton
onOpenModal={jest.fn()}
query={QUERY_WITH_FILTERS}
isExpanded={false}
onExpand={jest.fn()}
/>,
);
expect(
screen.queryByTestId("filters-visibility-control"),
).not.toBeInTheDocument();
});
it("should not render filter count without onExpand function", () => {
renderWithProviders(
<FilterHeaderButton
onOpenModal={jest.fn()}
query={QUERY_WITH_FILTERS}
isExpanded={false}
onCollapse={jest.fn()}
/>,
);
expect(
screen.queryByTestId("filters-visibility-control"),
).not.toBeInTheDocument();
});
it("should populate true data-expanded property", () => {
renderWithProviders(
<FilterHeaderButton
onOpenModal={jest.fn()}
query={QUERY_WITH_FILTERS}
isExpanded={true}
onExpand={jest.fn()}
onCollapse={jest.fn()}
/>,
);
expect(screen.getByTestId("filters-visibility-control")).toHaveAttribute(
"data-expanded",
"true",
);
});
it("should populate false data-expanded property", () => {
renderWithProviders(
<FilterHeaderButton
onOpenModal={jest.fn()}
query={QUERY_WITH_FILTERS}
isExpanded={false}
onExpand={jest.fn()}
onCollapse={jest.fn()}
/>,
);
expect(screen.getByTestId("filters-visibility-control")).toHaveAttribute(
"data-expanded",
"false",
);
});
});
import { FilterPanel } from "metabase/querying/filters/components/FilterPanel";
import { FilterPanelButton } from "metabase/querying/filters/components/FilterPanel/FilterPanelButton";
import * as Lib from "metabase-lib";
import type Question from "metabase-lib/v1/Question";
import type { QueryBuilderMode } from "metabase-types/store";
interface FilterHeaderToggleProps {
className?: string;
query: Lib.Query;
isExpanded: boolean;
onExpand: () => void;
onCollapse: () => void;
}
export function QuestionFiltersHeaderToggle({
className,
query,
isExpanded,
onExpand,
onCollapse,
}: FilterHeaderToggleProps) {
return (
<div className={className}>
<FilterPanelButton
query={query}
isExpanded={isExpanded}
onExpand={onExpand}
onCollapse={onCollapse}
/>
</div>
);
}
interface FilterHeaderProps {
question: Question;
expanded: boolean;
......@@ -77,4 +49,3 @@ const shouldRender = ({
};
QuestionFiltersHeader.shouldRender = shouldRender;
QuestionFiltersHeaderToggle.shouldRender = shouldRender;
......@@ -16,7 +16,6 @@ import {
ExploreResultsLink,
FilterHeaderButton,
QuestionActions,
QuestionFiltersHeaderToggle,
QuestionNotebookButton,
QuestionSummarizeWidget,
ToggleNativeQueryPreview,
......@@ -150,19 +149,6 @@ export function ViewTitleHeaderRightSide({
return (
<ViewHeaderActionPanel data-testid="qb-header-action-panel">
{QuestionFiltersHeaderToggle.shouldRender({
question,
queryBuilderMode,
isObjectDetail,
}) && (
<QuestionFiltersHeaderToggle
className={cx(CS.ml2, CS.mr1)}
query={question.query()}
isExpanded={areFiltersExpanded}
onExpand={onExpandFilters}
onCollapse={onCollapseFilters}
/>
)}
{FilterHeaderButton.shouldRender({
question,
queryBuilderMode,
......@@ -172,6 +158,10 @@ export function ViewTitleHeaderRightSide({
<FilterHeaderButton
className={cx(CS.hide, CS.smShow)}
onOpenModal={onOpenModal}
query={question.query()}
isExpanded={areFiltersExpanded}
onExpand={onExpandFilters}
onCollapse={onCollapseFilters}
/>
)}
{QuestionSummarizeWidget.shouldRender({
......
import styled from "@emotion/styled";
import type { ButtonHTMLAttributes } from "react";
import { alpha, color } from "metabase/lib/colors";
import type { ButtonProps } from "metabase/ui";
import { Button } from "metabase/ui";
type FilterButtonProps = ButtonHTMLAttributes<HTMLButtonElement> &
ButtonProps & {
isExpanded: boolean;
};
const shouldForwardProp = (propName: string) => {
return propName !== "isExpanded";
};
export const FilterButton = styled(Button, {
shouldForwardProp,
})<FilterButtonProps>`
color: ${({ isExpanded }) =>
isExpanded ? color("text-white") : color("filter")};
background-color: ${({ isExpanded }) =>
isExpanded ? alpha("filter", 0.8) : alpha("filter", 0.2)};
transition: border 300ms linear, background 300ms linear;
&:hover {
color: var(--mb-color-text-white);
background-color: var(--mb-color-filter);
}
@media (prefers-reduced-motion) {
transition: none;
}
`;
import { useMemo } from "react";
import { t } from "ttag";
import { Icon, Tooltip } from "metabase/ui";
import type * as Lib from "metabase-lib";
import { getFilterItems } from "../utils";
import { FilterButton } from "./FilterPanelButton.styled";
interface FilterPanelButtonProps {
query: Lib.Query;
isExpanded: boolean;
onExpand: () => void;
onCollapse: () => void;
}
export function FilterPanelButton({
query,
isExpanded,
onExpand,
onCollapse,
}: FilterPanelButtonProps) {
const label = isExpanded ? t`Hide filters` : t`Show filters`;
const items = useMemo(() => getFilterItems(query), [query]);
if (items.length === 0) {
return null;
}
return (
<Tooltip label={label}>
<FilterButton
leftIcon={<Icon name="filter" />}
radius="xl"
isExpanded={isExpanded}
aria-label={label}
data-testid="filters-visibility-control"
onClick={isExpanded ? onCollapse : onExpand}
>
{items.length}
</FilterButton>
</Tooltip>
);
}
import { render, screen } from "__support__/ui";
import type * as Lib from "metabase-lib";
import { createQueryWithStringFilter } from "../../FilterPicker/test-utils";
import { FilterPanelButton } from "./FilterPanelButton";
const { query: initialQuery } = createQueryWithStringFilter();
interface SetupOpts {
query: Lib.Query;
isExpanded?: boolean;
onExpand?: () => void;
onCollapse?: () => void;
}
const setup = (props: SetupOpts = { query: initialQuery }) => {
const { isExpanded = true } = props;
const onExpand = jest.fn();
const onCollapse = jest.fn();
render(
<FilterPanelButton
isExpanded={isExpanded}
onExpand={onExpand}
onCollapse={onCollapse}
{...props}
/>,
);
};
describe("FilterPanelButton", () => {
it("should render icon on the left", () => {
setup();
expect(screen.getByLabelText("filter icon")).toBeInTheDocument();
});
});
export * from "./FilterPanelButton";
export * from "./FilterPanel";
export * from "./FilterPanelButton";
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