Jelajahi Sumber

MainMultiFilterList skeleton.

Signed-off-by: Nashwan Azhari <nazhari@cloudbasesolutions.com>
Nashwan Azhari 1 tahun lalu
induk
melakukan
93477c83b6

+ 140 - 0
src/components/ui/Lists/MainMultiFilterList/MainMultiFilterList.spec.tsx

@@ -0,0 +1,140 @@
+/*
+Copyright (C) 2021  Cloudbase Solutions SRL
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Affero General Public License for more details.
+You should have received a copy of the GNU Affero General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+import React, { useState } from "react";
+import { render } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import MainMultiFilterList from "@src/components/ui/Lists/MainMultiFilterList";
+import { DropdownAction } from "@src/components/ui/Dropdowns/ActionDropdown";
+import TestUtils from "@tests/TestUtils";
+import { ThemePalette } from "@src/components/Theme";
+
+const FILTER_ITEMS = [
+  { label: "All", value: "all" },
+  { label: "Items 1", value: "item-1" },
+  { label: "Items 2", value: "item-2" },
+  { label: "Items 3", value: "item-3" },
+];
+
+const ACTIONS: DropdownAction[] = [
+  {
+    label: "Action 1",
+    title: "Action 1 Description",
+    action: jest.fn(),
+  },
+  {
+    label: "Action 2",
+    disabled: true,
+    action: jest.fn(),
+  },
+];
+
+const MainMultiFilterListWrapper = (props?: {
+  onFilterItemClick?: (item: any) => void;
+  onReloadButtonClick?: () => void;
+  onSearchChange?: (value: string) => void;
+  onSelectAllChange?: (checked: boolean) => void;
+}) => {
+  const [selectAllSelected, setSelectAllSelected] = useState(false);
+  return (
+    <MainMultiFilterList
+      onFilterItemClick={props?.onFilterItemClick || (() => {})}
+      onReloadButtonClick={props?.onReloadButtonClick || (() => {})}
+      onSearchChange={props?.onSearchChange || (() => {})}
+      onSelectAllChange={checked => {
+        setSelectAllSelected(checked);
+        if (props?.onSelectAllChange) {
+          props.onSelectAllChange(checked);
+        }
+      }}
+      selectedValue="item-2"
+      selectionInfo={{ total: 3, selected: 1, label: "test item" }}
+      selectAllSelected={selectAllSelected}
+      items={FILTER_ITEMS}
+      dropdownActions={ACTIONS}
+      searchValue="test"
+    />
+  );
+};
+
+describe("MainMultiFilterList", () => {
+  it("renders all basic elements", () => {
+    render(<MainMultiFilterListWrapper />);
+    const items = TestUtils.selectAll("MainMultiFilterList__FilterItem-");
+    expect(items).toHaveLength(FILTER_ITEMS.length);
+    expect(items[2].textContent).toBe(FILTER_ITEMS[2].label);
+    expect(
+      TestUtils.select("SearchInput__Wrapper")?.querySelector("input")?.value
+    ).toBe("test");
+    expect(TestUtils.select("MainMultiFilterList__SelectionText")?.textContent).toBe(
+      "1 of 3\u00a0test item(s) selected"
+    );
+  });
+
+  it("renders actions", () => {
+    render(<MainMultiFilterListWrapper />);
+    TestUtils.select("DropdownButton__Wrapper")?.click();
+    const actions = TestUtils.selectAll("ActionDropdown__ListItem-");
+    expect(actions).toHaveLength(ACTIONS.length);
+    expect(actions[0].textContent).toBe(ACTIONS[0].label);
+    expect(actions[0].hasAttribute("disabled")).toBeFalsy();
+    expect(actions[1].hasAttribute("disabled")).toBeTruthy();
+    actions[0].click();
+    actions[1].click();
+    expect(ACTIONS[0].action).toHaveBeenCalled();
+    expect(ACTIONS[1].action).not.toHaveBeenCalled();
+  });
+
+  it("fires filter item click", () => {
+    const onFilterItemClick = jest.fn();
+    render(<MainMultiFilterListWrapper onFilterItemClick={onFilterItemClick} />);
+    TestUtils.selectAll("MainMultiFilterList__FilterItem-")[1].click();
+    expect(onFilterItemClick).toHaveBeenCalledWith(FILTER_ITEMS[1]);
+  });
+
+  it("has select all change", () => {
+    const onSelectAllChange = jest.fn();
+    render(<MainMultiFilterListWrapper onSelectAllChange={onSelectAllChange} />);
+
+    const checkbox = TestUtils.select("Checkbox__Wrapper")!;
+    const style = () => window.getComputedStyle(checkbox);
+    expect(TestUtils.rgbToHex(style().backgroundColor)).toBe("white");
+    checkbox.click();
+    expect(TestUtils.rgbToHex(style().backgroundColor)).toBe(
+      ThemePalette.primary
+    );
+
+    expect(onSelectAllChange).toHaveBeenCalledWith(true);
+    checkbox.click();
+    expect(onSelectAllChange).toHaveBeenCalledWith(false);
+    expect(TestUtils.rgbToHex(style().backgroundColor)).toBe("white");
+  });
+
+  it("fires reload button click", () => {
+    const onReloadButtonClick = jest.fn();
+    render(<MainMultiFilterListWrapper onReloadButtonClick={onReloadButtonClick} />);
+    TestUtils.select("ReloadButton__Wrapper")!.click();
+    expect(onReloadButtonClick).toHaveBeenCalled();
+  });
+
+  it("fires search change", () => {
+    const onSearchChange = jest.fn();
+    render(<MainMultiFilterListWrapper onSearchChange={onSearchChange} />);
+    userEvent.type(
+      TestUtils.select("SearchInput__Wrapper")?.querySelector("input")!,
+      "test2"
+    );
+    expect(onSearchChange).toHaveBeenCalledWith("test2");
+  });
+});

+ 185 - 0
src/components/ui/Lists/MainMultiFilterList/MainMultiFilterList.tsx

@@ -0,0 +1,185 @@
+/*
+Copyright (C) 2017  Cloudbase Solutions SRL
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Affero General Public License for more details.
+You should have received a copy of the GNU Affero General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+import * as React from "react";
+import { observer } from "mobx-react";
+import styled from "styled-components";
+
+import Checkbox from "@src/components/ui/Checkbox";
+import SearchInput from "@src/components/ui/SearchInput";
+import ActionDropdown from "@src/components/ui/Dropdowns/ActionDropdown";
+import ReloadButton from "@src/components/ui/ReloadButton";
+
+import { ThemePalette, ThemeProps } from "@src/components/Theme";
+
+import type { DropdownAction } from "@src/components/ui/Dropdowns/ActionDropdown";
+
+const Wrapper = styled.div<any>`
+  display: flex;
+  align-items: center;
+  padding-top: 8px;
+  flex-wrap: wrap;
+  flex-shrink: 0;
+`;
+const Main = styled.div<any>`
+  display: flex;
+  margin-right: 16px;
+  flex-grow: 1;
+  margin-bottom: 32px;
+  height: 32px;
+  align-items: center;
+`;
+const FilterGroup = styled.div<any>`
+  display: flex;
+  margin: 0 16px 0 ${props => (props.noMargin ? "0" : "32px")};
+  border-right: 1px solid ${ThemePalette.grayscale[4]};
+`;
+const FilterItem = styled.div<any>`
+  margin-right: 32px;
+  color: ${props =>
+    props.selected ? ThemePalette.primary : ThemePalette.grayscale[4]};
+  ${props => (props.selected ? "text-decoration: underline;" : "")}
+  cursor: pointer;
+  white-space: nowrap;
+
+  &:last-child {
+    margin-right: 16px;
+  }
+`;
+const Selection = styled.div<any>`
+  display: flex;
+  align-items: center;
+  transition: all ${ThemeProps.animations.swift};
+  margin-bottom: 32px;
+  animation: show-animation 0.4s;
+
+  @keyframes show-animation {
+    from {
+      opacity: 0;
+    }
+    to {
+      opacity: 1;
+    }
+  }
+`;
+const SelectionText = styled.div<any>`
+  margin-right: 16px;
+  color: ${ThemePalette.grayscale[4]};
+  white-space: nowrap;
+`;
+
+type DictItem = { value: string; label: string };
+type Props = {
+  onFilterItemClick: (item: DictItem) => void;
+  onReloadButtonClick: () => void;
+  onSearchChange: (value: string) => void;
+  onSelectAllChange: (checked: boolean) => void;
+  selectedValue: string;
+  selectionInfo: { total: number; selected: number; label: string };
+  selectAllSelected: boolean | null;
+  items: DictItem[];
+  customFilterComponent?: React.ReactNode;
+  searchValue?: string;
+  dropdownActions: DropdownAction[] | null;
+  largeDropdownActionItems?: boolean;
+};
+@observer
+class MainMultiFilterList extends React.Component<Props> {
+  renderFilterGroup() {
+    const renderCustomComponent = () => {
+      if (this.props.customFilterComponent) {
+        return this.props.customFilterComponent;
+      }
+      return null;
+    };
+
+    return (
+      <FilterGroup
+        noMargin={
+          !this.props.dropdownActions || this.props.dropdownActions.length === 0
+        }
+      >
+        {renderCustomComponent()}
+        {this.props.items.map(item => (
+          <FilterItem
+            onClick={() => this.props.onFilterItemClick(item)}
+            key={item.value}
+            selected={this.props.selectedValue === item.value}
+          >
+            {item.label}
+          </FilterItem>
+        ))}
+      </FilterGroup>
+    );
+  }
+
+  renderSelectionInfo() {
+    if (!this.props.selectionInfo.selected) {
+      return null;
+    }
+
+    return (
+      <Selection>
+        <SelectionText>
+          {this.props.selectionInfo.selected} of{" "}
+          {this.props.selectionInfo.total}&nbsp;
+          {this.props.selectionInfo.label}(s) selected
+        </SelectionText>
+        {this.props.dropdownActions && this.props.dropdownActions.length ? (
+          <ActionDropdown
+            actions={this.props.dropdownActions}
+            largeItems={this.props.largeDropdownActionItems}
+            style={{ marginLeft: "8px" }}
+          />
+        ) : null}
+      </Selection>
+    );
+  }
+
+  render() {
+    const renderCheckbox = () => {
+      if (this.props.dropdownActions && this.props.dropdownActions.length > 0) {
+        return (
+          <Checkbox
+            onChange={checked => {
+              this.props.onSelectAllChange(checked);
+            }}
+            checked={!!this.props.selectAllSelected}
+          />
+        );
+      }
+      return null;
+    };
+
+    return (
+      <Wrapper>
+        <Main>
+          {renderCheckbox()}
+          {this.renderFilterGroup()}
+          <ReloadButton
+            style={{ marginRight: "16px" }}
+            onClick={this.props.onReloadButtonClick}
+          />
+          <SearchInput
+            onChange={this.props.onSearchChange}
+            value={this.props.searchValue}
+          />
+        </Main>
+        {this.renderSelectionInfo()}
+      </Wrapper>
+    );
+  }
+}
+
+export default MainMultiFilterList;

+ 6 - 0
src/components/ui/Lists/MainMultiFilterList/package.json

@@ -0,0 +1,6 @@
+{
+  "name": "MainMultiFilterList",
+  "version": "0.0.0",
+  "private": true,
+  "main": "./MainMultiFilterList.tsx"
+}

+ 70 - 0
src/components/ui/Lists/MainMultiFilterList/story.tsx

@@ -0,0 +1,70 @@
+/*
+Copyright (C) 2017  Cloudbase Solutions SRL
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Affero General Public License for more details.
+You should have received a copy of the GNU Affero General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+import React from "react";
+import { storiesOf } from "@storybook/react";
+import MainMultiFilterList from ".";
+
+const items = [
+  { label: "Item 1", value: "item-1" },
+  { label: "Item 2", value: "item-2" },
+  { label: "Item 3", value: "item-3" },
+];
+
+const actions = [
+  { label: "Action 1", value: "action-1" },
+  { label: "Action 2", value: "action-2" },
+];
+
+class Wrapper extends React.Component<any> {
+  state = { selectedValue: "item-1", selectAllSelected: false };
+
+  handleChange(selectedValue: string) {
+    this.setState({ selectedValue });
+  }
+
+  handleSelectAllChange(selectAllSelected: boolean) {
+    this.setState({ selectAllSelected });
+  }
+
+  render() {
+    return (
+      <MainMultiFilterList
+        onReloadButtonClick={() => {}}
+        onSearchChange={() => {}}
+        selectionInfo={{ total: 0, selected: 0, label: "" }}
+        items={[]}
+        dropdownActions={[]}
+        // eslint-disable-next-line react/jsx-props-no-spreading
+        {...this.props}
+        selectedValue={this.state.selectedValue}
+        selectAllSelected={this.state.selectAllSelected}
+        onFilterItemClick={item => {
+          this.handleChange(item.value);
+        }}
+        onSelectAllChange={checked => {
+          this.handleSelectAllChange(checked);
+        }}
+      />
+    );
+  }
+}
+
+storiesOf("MainMultiFilterList", module).add("default", () => (
+  <Wrapper
+    items={items}
+    actions={actions}
+    selectionInfo={{ selected: 2, total: 7, label: "items" }}
+  />
+));