Skip to content

Commit

Permalink
add BooleanPicker component (metabase#18950)
Browse files Browse the repository at this point in the history
* add BooleanPicker component

* add BooleanPicker unit test

* fix typo

* fix bool filter cy tests

* fix button styling

* remove 'admin' colorScheme and tweak design a lil
  • Loading branch information
daltojohnso authored Nov 16, 2021
1 parent 4afdb6b commit 2d5766a
Show file tree
Hide file tree
Showing 11 changed files with 248 additions and 14 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const propTypes = {
export const PermissionsTabs = ({ tab, onChangeTab }) => (
<div className="px3 mt1">
<Radio
colorScheme="admin"
colorScheme="accent7"
value={tab}
options={[
{ name: t`Data permissions`, value: `data` },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ export const PermissionsSidebarContent = memo(
<Box mb={2}>
<Radio
variant="bubble"
colorScheme="admin"
colorScheme="accent7"
options={entitySwitch.options}
value={entitySwitch.value}
onChange={onEntityChange}
Expand Down
7 changes: 5 additions & 2 deletions frontend/src/metabase/components/Radio.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ const propTypes = {
// Modes
variant: PropTypes.oneOf(["bubble", "normal", "underlined"]),
vertical: PropTypes.bool,
colorScheme: PropTypes.oneOf(["admin", "default"]),
colorScheme: PropTypes.oneOf(["admin", "default", "accent7"]),
};

const defaultNameGetter = option => option.name;
Expand Down Expand Up @@ -120,6 +120,7 @@ function Radio({
>
{option.icon && <Icon name={option.icon} mr={1} />}
<RadioInput
colorScheme={colorScheme}
id={id}
name={name}
value={value}
Expand All @@ -132,7 +133,9 @@ function Radio({
// Workaround for https://github.com/testing-library/dom-testing-library/issues/877
aria-labelledby={labelId}
/>
{showButtons && <RadioButton checked={selected} />}
{showButtons && (
<RadioButton colorScheme={colorScheme} checked={selected} />
)}
<span data-testid={`${id}-name`}>{optionNameFn(option)}</span>
</Item>
</li>
Expand Down
15 changes: 12 additions & 3 deletions frontend/src/metabase/components/Radio.styled.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ import { space } from "styled-system";
import { color, lighten } from "metabase/lib/colors";

const COLOR_SCHEMES = {
admin: {
accent7: {
main: () => color("accent7"),
button: () => color("accent7"),
},
default: {
main: () => color("brand"),
button: () => color("brand"),
},
};

Expand Down Expand Up @@ -37,8 +39,15 @@ export const RadioButton = styled.div`
border: 2px solid white;
box-shadow: 0 0 0 2px ${color("shadow")};
border-radius: 12px;
background-color: ${props =>
props.checked ? color("brand") : "transparent"};
background-color: ${props => {
if (props.checked) {
return props.colorScheme
? COLOR_SCHEMES[props.colorScheme].button()
: color("brand");
} else {
return "transparent";
}
}};
`;

// BASE
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,11 @@ export default function FilterPopoverHeader({
const field = dimension.field();
const operator = filter.operatorName();

const showOperatorSelector = !(field.isTime() || field.isDate());
const showOperatorSelector = !(
field.isTime() ||
field.isDate() ||
field.isBoolean()
);
const showHeader = showFieldPicker || showOperatorSelector;
const showOperatorSelectorOnOwnRow = isSidebar || !showFieldPicker;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import PropTypes from "prop-types";

import DatePicker from "../filters/pickers/DatePicker";
import TimePicker from "../filters/pickers/TimePicker";
import BooleanPicker from "../filters/pickers/BooleanPicker";
import DefaultPicker from "../filters/pickers/DefaultPicker";

export default class FilterPopoverPicker extends React.Component {
Expand Down Expand Up @@ -60,6 +61,12 @@ export default class FilterPopoverPicker extends React.Component {
maxWidth={maxWidth}
isSidebar={isSidebar}
/>
) : field.isBoolean() ? (
<BooleanPicker
className={className}
filter={filter}
onFilterChange={onFilterChange}
/>
) : (
<DefaultPicker
className={className}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import React from "react";
import { PropTypes } from "prop-types";
import _ from "underscore";
import { t } from "ttag";

import { useToggle } from "metabase/hooks/use-toggle";
import Filter from "metabase-lib/lib/queries/structured/Filter";

import { Container, Toggle, FilterRadio } from "./BooleanPicker.styled";

BooleanPicker.propTypes = {
className: PropTypes.string,
filter: PropTypes.instanceOf(Filter),
onFilterChange: PropTypes.func.isRequired,
};

const OPTIONS = [
{ name: t`true`, value: true },
{ name: t`false`, value: false },
];
const EXPANDED_OPTIONS = [
{ name: t`true`, value: true },
{ name: t`false`, value: false },
{ name: t`empty`, value: "is-null" },
{ name: t`not empty`, value: "not-null" },
];

function BooleanPicker({ className, filter, onFilterChange }) {
const value = getValue(filter);
const [isExpanded, { toggle }] = useToggle(!_.isBoolean(value));

const updateFilter = value => {
if (_.isBoolean(value)) {
onFilterChange(filter.setOperator("=").setArguments([value]));
} else {
onFilterChange(filter.setOperator(value));
}
};

return (
<Container className={className}>
<FilterRadio
vertical
colorScheme="accent7"
options={isExpanded ? EXPANDED_OPTIONS : OPTIONS}
value={value}
onChange={updateFilter}
/>
{!isExpanded && <Toggle onClick={toggle} />}
</Container>
);
}

function getValue(filter) {
const operatorName = filter.operatorName();
if (operatorName === "=") {
const [value] = filter.arguments();
return value;
} else {
return operatorName;
}
}

export default BooleanPicker;
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import React from "react";
import PropTypes from "prop-types";
import styled from "styled-components";
import { t } from "ttag";

import { color } from "metabase/lib/colors";
import { space } from "metabase/styled-components/theme";
import Button from "metabase/components/Button";
import Radio from "metabase/components/Radio";

export const FilterRadio = styled(Radio).attrs({
colorScheme: "accent7",
})`
font-weight: 700;
`;

export const Container = styled.div`
margin: 15px 20px 70px 20px;
`;

const ToggleButton = styled(Button).attrs({
iconRight: "chevrondown",
iconSize: 12,
})`
margin-left: ${space(0)};
color: ${color("text-medium")};
border: none;
background-color: transparent;
&:hover {
background-color: transparent;
}
.Icon {
margin-top: 2px;
}
`;

Toggle.propTypes = {
onClick: PropTypes.func.isRequired,
};

export function Toggle({ onClick }) {
return <ToggleButton onClick={onClick}>{t`More options`}</ToggleButton>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import React from "react";
import { render, screen } from "@testing-library/react";

import { metadata } from "__support__/sample_dataset_fixture";

import Question from "metabase-lib/lib/Question";
import Field from "metabase-lib/lib/metadata/Field";
import Filter from "metabase-lib/lib/queries/structured/Filter";

import BooleanPicker from "./BooleanPicker";

const booleanField = new Field({
database_type: "bool",
semantic_type: "type/Category",
table_id: 8,
name: "bool",
has_field_values: "list",
dimensions: {},
dimension_options: [],
effective_type: "type/Boolean",
id: 134,
base_type: "type/Boolean",
metadata,
});

const card = {
dataset_query: {
database: 5,
query: {
"source-table": 8,
filter: ["=", ["field", 134, null], true],
},
type: "query",
},
display: "table",
visualization_settings: {},
};

metadata.fields[booleanField.id] = booleanField;

const question = new Question(card, metadata);

const fieldRef = ["field", 134, null];
const filters = {
true: new Filter(["=", fieldRef, true], null, question.query()),
false: new Filter(["=", fieldRef, false], null, question.query()),
empty: new Filter(["is-null", fieldRef], null, question.query()),
"not empty": new Filter(["not-null", fieldRef], null, question.query()),
};

const mockOnFilterChange = jest.fn();
function setup(filter) {
mockOnFilterChange.mockReset();
return render(
<BooleanPicker filter={filter} onFilterChange={mockOnFilterChange} />,
);
}

describe("BooleanPicker", () => {
it("should hide empty options when empty options are not selected", () => {
setup(filters.true);

expect(screen.queryByLabelText("empty")).toBeNull();
expect(screen.queryByLabelText("not empty")).toBeNull();

screen.getByText("More options").click();

expect(screen.getByLabelText("empty")).toBeInTheDocument();
expect(screen.getByLabelText("not empty")).toBeInTheDocument();
});

it("should show empty options when given an empty filter", () => {
setup(filters.empty);

const option = screen.getByLabelText("empty");
expect(option.checked).toBe(true);

expect(screen.getByLabelText("not empty")).toBeInTheDocument();
});

Object.entries(filters).forEach(([label, filter]) => {
it(`should have the "${label}" option selected when given the associated filter`, () => {
setup(filter);

const option = screen.getByLabelText(label);
expect(option.checked).toBe(true);
});

it(`should correctly update the filter for the "${label}" option when it is selected`, () => {
setup(label === "true" ? filters.false : filters.true);

screen.getByText("More options").click();

screen.getByLabelText(label).click();
expect(mockOnFilterChange).toHaveBeenCalled();
const newFilter = mockOnFilterChange.mock.calls[0][0];

expect(newFilter.raw()).toEqual(filter.raw());
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from "./BooleanPicker";
12 changes: 6 additions & 6 deletions frontend/test/metabase/scenarios/question/filter.cy.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -984,9 +984,9 @@ describe("scenarios > question > filter", () => {
// Not sure exactly what this popover will look like when this issue is fixed.
// In one of the previous versions it said "Update filter" instead of "Add filter".
// If that's the case after the fix, this part of the test might need to be updated accordingly.
cy.button(regexCondition)
.click()
.should("have.class", "bg-purple");
cy.findByLabelText(regexCondition)
.check({ force: true }) // the radio input is hidden
.should("be.checked");
cy.button("Update filter").click();
});

Expand Down Expand Up @@ -1023,9 +1023,9 @@ describe("scenarios > question > filter", () => {

function addBooleanFilter() {
// This is really inconvenient way to ensure that the element is selected, but it's the only one currently
cy.button(regexCondition)
.click()
.should("have.class", "bg-purple");
cy.findByLabelText(regexCondition)
.check({ force: true })
.should("be.checked");
cy.button("Add filter").click();
}

Expand Down

0 comments on commit 2d5766a

Please sign in to comment.