diff --git a/.github/workflows/build-check.yml b/.github/workflows/build-check.yml new file mode 100644 index 000000000..cc476da87 --- /dev/null +++ b/.github/workflows/build-check.yml @@ -0,0 +1,26 @@ +name: Build Check + +on: + pull_request: + branches: + - master + - develop + +jobs: + build_check: + runs-on: [ubuntu-latest] + strategy: + matrix: + node: [ '10', '8' ] + steps: + - uses: actions/checkout@v2.0.0 + - name: Setup node + uses: actions/setup-node@v1.1.0 + with: + node-version: ${{ matrix.node }} + - name: installing dependencies + run: npm install + - name: running unit tests + run: npm test + - name: building + run: npm run build:production diff --git a/build-check.yml b/build-check.yml new file mode 100644 index 000000000..cc476da87 --- /dev/null +++ b/build-check.yml @@ -0,0 +1,26 @@ +name: Build Check + +on: + pull_request: + branches: + - master + - develop + +jobs: + build_check: + runs-on: [ubuntu-latest] + strategy: + matrix: + node: [ '10', '8' ] + steps: + - uses: actions/checkout@v2.0.0 + - name: Setup node + uses: actions/setup-node@v1.1.0 + with: + node-version: ${{ matrix.node }} + - name: installing dependencies + run: npm install + - name: running unit tests + run: npm test + - name: building + run: npm run build:production diff --git a/develop/components/pages/TablePage.tsx b/develop/components/pages/TablePage.tsx index 9b478e765..5c08d538f 100644 --- a/develop/components/pages/TablePage.tsx +++ b/develop/components/pages/TablePage.tsx @@ -1,33 +1,31 @@ import * as React from "react"; -import { Table, Column, TableRow, PrimaryActionButton, ActionLinkItem, TableHeader, DataItem } from "../../../src/Table"; +import { Table, Column, TableRow, PrimaryActionButton, ActionLinkItem, TableHeader, DataItem, FilterItem, FilterProps } from "../../../src/Table"; import makeData from "../../__utils/makeData"; import { Pagination } from "../../../src/Pagination/Pagination"; import { Dropdown, DropdownItem } from "../../../src/Dropdown/Dropdown"; import { TextBox } from "../../../src/TextBox/TextBox"; import { Button } from "../../../src/Button/Button"; -const Highlight = (require("react-highlight")).default; +const Highlight = require("react-highlight").default; const docMD = require("../../../src/Table/readme.md"); +interface TableDataProps { + id: string; + firstName: string; + lastName: string; + age: number; + visits: number; + progress: number; + status: string; +} + const TablePage: React.FunctionComponent = () => { const [paginationValue, setPagination] = React.useState(1); const [paginationValue1, setPagination1] = React.useState(1); const [dropDownList1Selected, setDropdownList1Selected] = React.useState>([]); + const [statusDropdownSelected, setStatusDropdownSelected] = React.useState>([]); + const [ageDropdownSelected, setAgeDropdownSelected] = React.useState>([]); const [textBoxValue2, setTextBoxValue2] = React.useState(""); const [searchTriggered, setSearchTriggered] = React.useState(false); - - const pageSize: number = 10; - const listSize: number = 30; - - const primaryButton: PrimaryActionButton = { - label: "Buy", - onClick: (e: React.MouseEvent, selectedRow: TableRow) => { } - } - - const actionLinks: Array = [ - { label: "Add", onClick: (event: React.MouseEvent, selectedRow: TableRow) => {} }, - { label: "Edit", onClick: (event: React.MouseEvent, selectedRow: TableRow) => {} } - ]; - const columns: Array = React.useMemo( () => [ { @@ -37,37 +35,131 @@ const TablePage: React.FunctionComponent = () => { }, { label: "First Name", - accessor: "firstName", + accessor: "firstName" }, { label: "Last Name", - accessor: "lastName", + accessor: "lastName" }, { label: "Age", - accessor: "age", + accessor: "age" }, { label: "Visits", - accessor: "visits", + accessor: "visits" }, { label: "Profile Progress", - accessor: "progress", + accessor: "progress" }, { label: "Status", - accessor: "status", - }, + accessor: "status" + } ], [] ); - const data: Array = React.useMemo(() => makeData>([listSize, 5]), []); - const smallData: Array = React.useMemo(() => makeData>([5, 5]), []); + + const [filters, setFilters] = React.useState>(columns.map((column: Column) => ({ accessor: column.accessor, filters: [] }))); + + React.useEffect(() => { + const updatedFilter: Array = statusDropdownSelected?.map((item: DropdownItem) => item.value); + const updatedFilterItems: Array = filters?.map((filterItem: FilterItem) => { + if (filterItem.accessor === "status") { + return { ...filterItem, filters: updatedFilter }; + } + return filterItem; + }); + setFilters(updatedFilterItems); + }, [statusDropdownSelected]); + + React.useEffect(() => { + const updatedFilter: Array = ageDropdownSelected?.map((item: DropdownItem) => item.value); + const updatedFilterItems: Array = filters?.map((filterItem: FilterItem) => { + if (filterItem.accessor === "age") { + return { ...filterItem, filters: updatedFilter }; + } + return filterItem; + }); + + setFilters(updatedFilterItems); + }, [ageDropdownSelected]); + + const pageSize: number = 10; + const listSize: number = 30; + + const primaryButton: PrimaryActionButton = { + label: "Buy", + onClick: (e: React.MouseEvent, selectedRow: TableRow) => {} + }; + + const actionLinks: Array = [ + { label: "Add", onClick: (event: React.MouseEvent, selectedRow: TableRow) => {} }, + { label: "Edit", onClick: (event: React.MouseEvent, selectedRow: TableRow) => {} } + ]; + + const filterProps: FilterProps = { + onAfterFilter: (rows: Array) => {}, + onRemoveFilter: (item: { accessor: string; value: string }) => { + const updatedFilters: Array = filters.map((filter: FilterItem) => { + if (filter.accessor === item.accessor) { + const indexOfFilterTobeRemoved: number = filter?.filters?.findIndex((filterItem: string) => filterItem === item.value); + return { ...filter, filters: [...filter?.filters?.slice(0, indexOfFilterTobeRemoved), ...filter?.filters?.slice(indexOfFilterTobeRemoved + 1)] }; + } + return filter; + }); + if (item?.accessor === "status") { + const selectedFilter: FilterItem = updatedFilters?.find((filter: FilterItem) => filter.accessor === "status"); + const updatedStatus: Array = selectedFilter?.filters?.map((item: string) => ({ label: item, value: item })); + setStatusDropdownSelected(updatedStatus); + } else if (item?.accessor === "age") { + const selectedFilter: FilterItem = updatedFilters?.find((filter: FilterItem) => filter.accessor === "age"); + const updatedStatus: Array = selectedFilter?.filters?.map((item: string) => ({ label: item, value: item })); + setAgeDropdownSelected(updatedStatus); + } + setFilters(updatedFilters); + }, + filterItems: filters + }; + + const data: Array> = React.useMemo( + () => makeData>>([listSize, 5]), + [] + ); + + const smallData: Array> = React.useMemo( + () => makeData>>([5, 5]), + [] + ); + + const statusDropDownList: Array = React.useMemo( + () => + smallData + .map((data: DataItem) => ({ value: data.status, label: data.status })) + .filter((item: DropdownItem, index: number, self: Array) => { + const selfIndex: number = self.findIndex((filter: DropdownItem) => filter.value === item.value); + return selfIndex === index; + }) + .sort(), + [] + ); + + const ageDropDownList: Array = React.useMemo( + () => + smallData + .map((data: DataItem) => ({ value: data.age, label: String(data.age) })) + .filter((item: DropdownItem, index: number, self: Array) => { + const selfIndex: number = self.findIndex((filter: DropdownItem) => filter.value === item.value); + return selfIndex === index; + }) + .sort(), + [] + ); + return (
-
{docMD} @@ -79,10 +171,7 @@ const TablePage: React.FunctionComponent = () => {

Here are sample outputs of plain table

- +

Here an example with sorting

@@ -91,9 +180,8 @@ const TablePage: React.FunctionComponent = () => { columns={columns} data={smallData} sortProps={{ - onAfterSorting: (rows: Array, sortByColumn: TableHeader) => { } - } - } + onAfterSorting: (rows: Array, sortByColumn: TableHeader) => {} + }} /> @@ -104,65 +192,49 @@ const TablePage: React.FunctionComponent = () => { data={data} offset={pageSize} currentpage={paginationValue} - footer={ - - } + footer={} />

Here is an example with expandable subrows and rowDetails

-
) => { }} - /> +
) => {}} />

Here is an example with row selection

-
) => { }} - /> +
) => {}} />

Here is an example with row selection and subRows

-
) => { }} - onRowExpanded={(rows: Array) => { }} - /> +
) => {}} onRowExpanded={(rows: Array) => {}} />

Here is an example with actions column

-
+
-

Here is an example with search, filter, sorting, pagination, subRows etc.:

+

Here is an example with filter

- ) => setDropdownList1Selected(value)} - multi={true} - /> + ) => setStatusDropdownSelected(value)} multi={true} /> +
+
+ ) => setAgeDropdownSelected(value)} multi={true} /> +
+
+
+
) => {}} onRowExpanded={(rows: Array) => {}} /> + + +

Here is an example with search, sorting, pagination, subRows etc.:

+
+
+
+ ) => setDropdownList1Selected(value)} multi={true} />
{ />
-
{ searchText: textBoxValue2, triggerSearchOn: "Submit", searchTriggered: searchTriggered, - onSearch: (searchResults: Array) => { } + onSearch: (searchResults: Array) => {} }} primaryActionButton={primaryButton} actionLinks={actionLinks} sortProps={{ - onAfterSorting: (rows: Array, sortByColumn: TableHeader) => { } - } - } - onRowSelected={(rows: Array) => { }} - onRowExpanded={(rows: Array) => { }} - footer={ - - } + onAfterSorting: (rows: Array, sortByColumn: TableHeader) => {} + }} + onRowSelected={(rows: Array) => {}} + onRowExpanded={(rows: Array) => {}} + footer={} /> - - ); }; diff --git a/package-lock.json b/package-lock.json index 5ea5fdbd7..6311ab2de 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8292,22 +8292,22 @@ } }, "coveralls": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/coveralls/-/coveralls-3.0.9.tgz", - "integrity": "sha512-nNBg3B1+4iDox5A5zqHKzUTiwl2ey4k2o0NEcVZYvl+GOSJdKBj4AJGKLv6h3SvWch7tABHePAQOSZWM9E2hMg==", + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/coveralls/-/coveralls-3.0.11.tgz", + "integrity": "sha512-LZPWPR2NyGKyaABnc49dR0fpeP6UqhvGq4B5nUrTQ1UBy55z96+ga7r+/ChMdMJUwBgyJDXBi88UBgz2rs9IiQ==", "dev": true, "requires": { "js-yaml": "^3.13.1", "lcov-parse": "^1.0.0", "log-driver": "^1.2.7", - "minimist": "^1.2.0", + "minimist": "^1.2.5", "request": "^2.88.0" }, "dependencies": { "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", "dev": true } } diff --git a/src/Table/Table.test.tsx b/src/Table/Table.test.tsx index 96d2b3cc7..de7212ca3 100644 --- a/src/Table/Table.test.tsx +++ b/src/Table/Table.test.tsx @@ -1,6 +1,6 @@ import * as React from "react"; import { unmountComponentAtNode, render } from "react-dom"; -import { Column, Table, TableRow, TableHeader, ActionLinkItem, DataItem, sortDirectionTypes } from "./Table"; +import { Column, Table, TableRow, TableHeader, ActionLinkItem, DataItem, sortDirectionTypes, FilterItem, PrimaryActionButton } from "./Table"; import makeData from "../../develop/__utils/makeData"; import { act } from "react-dom/test-utils"; import { Pagination } from "../Pagination/Pagination"; @@ -16,32 +16,32 @@ describe("Component: Table", () => { }, { label: "First Name", - accessor: "firstName", + accessor: "firstName" }, { label: "Last Name", - accessor: "lastName", + accessor: "lastName" }, { label: "Age", - accessor: "age", + accessor: "age" }, { label: "Visits", - accessor: "visits", + accessor: "visits" }, { label: "Profile Progress", - accessor: "progress", + accessor: "progress" }, { label: "Status", - accessor: "status", - }, + accessor: "status" + } ]; - const data: Array = makeData([30, 5]); - const smallData: Array = makeData([5, 5]); + const data: Array> = makeData([30, 5]); + const smallData: Array> = makeData([5, 5]); beforeEach(() => { container = document.createElement("div"); @@ -55,40 +55,70 @@ describe("Component: Table", () => { }); it("Should render simple table", () => { - act(() => { render(
, container); }); + act(() => { + render(
, container); + }); expect(container).toBeDefined(); }); - it('Should render and be able to sort rows ', () => { + it("Should render and be able to sort rows ", () => { const event: jest.Mock = jest.fn((rows: Array, sortByColumn: TableHeader) => console.log("onAfterSorting called")); const onSortEvent: jest.Mock = jest.fn((rows: Array, accessor: string, sortingOrder: sortDirectionTypes) => rows.slice(0, 2)); act(() => { - render(
, container); + render(
, container); + }); + + // sort number type column(age) ascending and descending + act(() => { + container + .querySelectorAll(".icon-holder") + .item(3) + .dispatchEvent(new MouseEvent("click", { bubbles: true })); + }); + act(() => { + container + .querySelectorAll(".icon-holder") + .item(3) + .dispatchEvent(new MouseEvent("click", { bubbles: true })); }); + // sort ascending act(() => { - container.querySelectorAll(".icon-holder").item(1).dispatchEvent(new MouseEvent("click", { bubbles: true })); + container + .querySelectorAll(".icon-holder") + .item(1) + .dispatchEvent(new MouseEvent("click", { bubbles: true })); }); + // sort descending + act(() => { + container + .querySelectorAll(".icon-holder") + .item(1) + .dispatchEvent(new MouseEvent("click", { bubbles: true })); + }); + // note: id column canSort is false expect(container.querySelectorAll(".icon-holder").length).toEqual(columns.length - 1); - expect(event).toHaveBeenCalled(); + expect(event).toHaveBeenCalledTimes(4); // you can also use a custom onSort callback as passed by the user act(() => { - render(
, container); + render(
, container); }); act(() => { - container.querySelectorAll(".icon-holder").item(1).dispatchEvent(new MouseEvent("click", { bubbles: true })); + container + .querySelectorAll(".icon-holder") + .item(1) + .dispatchEvent(new MouseEvent("click", { bubbles: true })); }); // there should just two rows now expect(container.querySelectorAll("tbody > tr.parent-row").length).toEqual(2); + + // it also support a server sorting, just sort and update your data and columns + act(() => { + render(
, container); + }); + + expect(container.querySelectorAll("tbody > tr.parent-row").length).toEqual(2); }); it("Should render and do pagination where necessary ", () => { @@ -96,23 +126,22 @@ describe("Component: Table", () => { const paginationValue: number = 1; const setPage: jest.Mock = jest.fn((n: number) => console.log("setPage called")); act(() => { - render(
- } - />, container); + render( +
} + />, + container + ); }); act(() => { - container.querySelectorAll("tfoot > tr .page-item").item(1).dispatchEvent(new MouseEvent("click", { bubbles: true })) + container + .querySelectorAll("tfoot > tr .page-item") + .item(1) + .dispatchEvent(new MouseEvent("click", { bubbles: true })); }); expect(container.querySelectorAll("tfoot > tr .page-item")).toBeDefined(); expect(setPage).toHaveBeenCalled(); @@ -121,20 +150,20 @@ describe("Component: Table", () => { it("Should render and be able to expand the subRows and row details ", () => { const onRowExpanded: jest.Mock = jest.fn((rows: Array) => console.log("onRowExpanded called")); act(() => { - render( -
, container - ); + render(
, container); }); act(() => { - container.querySelectorAll("tbody > tr.parent-row .icon-holder svg").item(1).dispatchEvent(new MouseEvent("click", { bubbles: true })); + container + .querySelectorAll("tbody > tr.parent-row .icon-holder svg") + .item(1) + .dispatchEvent(new MouseEvent("click", { bubbles: true })); }); act(() => { - container.querySelectorAll("tbody > tr.sub-row .icon-holder svg").item(1).dispatchEvent(new MouseEvent("click", { bubbles: true })); + container + .querySelectorAll("tbody > tr.sub-row .icon-holder svg") + .item(1) + .dispatchEvent(new MouseEvent("click", { bubbles: true })); }); expect(container.querySelectorAll("tbody > tr.parent-row.expanded")).toBeTruthy(); @@ -147,17 +176,14 @@ describe("Component: Table", () => { const newData: Array = smallData.map((row: TableRow) => ({ ...row, subRows: undefined, rowContentDetail:

paragraph

})); act(() => { - render( -
, container - ); + render(
, container); }); act(() => { - container.querySelectorAll("tbody > tr.parent-row .icon-holder svg").item(1).dispatchEvent(new MouseEvent("click", { bubbles: true })); + container + .querySelectorAll("tbody > tr.parent-row .icon-holder svg") + .item(1) + .dispatchEvent(new MouseEvent("click", { bubbles: true })); }); expect(container.querySelectorAll("tbody > tr.parent-row.expanded")).toBeTruthy(); @@ -165,81 +191,69 @@ describe("Component: Table", () => { }); it("should render with and support row selection where necessary", () => { - // all items select + // all items select const onRowSelected: jest.Mock = jest.fn((e: React.ChangeEvent, row: TableRow, type: "row" | "subRow", rowIndex?: number) => console.log("onRowSelected called")); act(() => { - render( -
, container - ); + render(
, container); }); act(() => { - // all items select - container.querySelectorAll("thead .custom-control-input").item(0).dispatchEvent(new MouseEvent("click", { bubbles: true })); + // all items select + container + .querySelectorAll("thead .custom-control-input") + .item(0) + .dispatchEvent(new MouseEvent("click", { bubbles: true })); // one parent item selection - container.querySelectorAll("tbody tr.parent-row .custom-control-input").item(0).dispatchEvent(new MouseEvent("click", { bubbles: true })); + container + .querySelectorAll("tbody tr.parent-row .custom-control-input") + .item(0) + .dispatchEvent(new MouseEvent("click", { bubbles: true })); // one subrow item selected - container.querySelectorAll("tbody tr.sub-row .custom-control-input").item(0).dispatchEvent(new MouseEvent("click", { bubbles: true })); + container + .querySelectorAll("tbody tr.sub-row .custom-control-input") + .item(0) + .dispatchEvent(new MouseEvent("click", { bubbles: true })); }); expect(onRowSelected).toHaveBeenCalledTimes(3); }); it("should render and have optional footer row", () => { act(() => { - render( -
, container - ); + render(
, container); }); expect(container.querySelector("tfoot tr")).toBeFalsy(); act(() => { - render( -
This is a paragraph.

} - />, container - ); + render(
This is a paragraph.

} />, container); }); expect(container.querySelector("tfoot tr")).toBeTruthy(); }); it("should enable and handle custom actions ", () => { - const customButtonCallBack: jest.Mock = jest.fn((event: React.MouseEvent, selectedRow: TableRow) => { }); + const customButtonCallBack: jest.Mock = jest.fn((event: React.MouseEvent, selectedRow: TableRow) => {}); const actionLinks: Array = [ { label: "Add", onClick: customButtonCallBack }, { label: "Edit", onClick: customButtonCallBack } ]; act(() => { - render( -
, container - ); + render(
, container); }); // trigger and open action column - const openedActionColumnString: string = "tbody tr.parent-row > td .action-column .ellipsis-dropdown-holder .dropdown-content.active"; + const openedActionColumnString: string = "tbody tr.parent-row td .action-column .ellipsis-dropdown-holder .dropdown-content.active"; expect(container.querySelector(openedActionColumnString)).toBeNull(); expect(container.querySelector(openedActionColumnString)).toBeFalsy(); act(() => { - container.querySelectorAll("tbody tr.parent-row > td .action-column .ellipsis-dropdown-holder").item(1).dispatchEvent(new MouseEvent("click", { bubbles: true })); + container + .querySelectorAll("tbody tr.parent-row td .action-column .ellipsis-dropdown-holder") + .item(1) + .dispatchEvent(new MouseEvent("click", { bubbles: true })); }); - expect(container.querySelector(openedActionColumnString)).toBeDefined(); expect(container.querySelector(openedActionColumnString)).toBeTruthy(); - // action should be closed when you click outside the div + // action should be closed when you click outside the div act(() => { container.querySelector("tbody").dispatchEvent(new MouseEvent("mousedown", { bubbles: true })); @@ -248,9 +262,8 @@ describe("Component: Table", () => { expect(container.querySelector(openedActionColumnString)).toBeNull(); expect(container.querySelector(openedActionColumnString)).toBeFalsy(); - act(() => { - container.querySelectorAll("tbody tr.parent-row > td .action-column a").forEach((el: Element) => el.dispatchEvent(new MouseEvent("click", { bubbles: true }))); + container.querySelectorAll("tbody tr.parent-row td .action-column a").forEach((el: Element) => el.dispatchEvent(new MouseEvent("click", { bubbles: true }))); }); // it should be called the length of the data twice @@ -260,24 +273,92 @@ describe("Component: Table", () => { expect(container.querySelectorAll("thead tr th").length).toEqual(columns.length + 1); }); - it("should render and support filter and searching ", () => { + it("should render and enable custom button", () => { + const primaryActionButton: PrimaryActionButton = { + label: "Buy", + onClick: jest.fn((e: React.MouseEvent) => {}) + }; + + act(() => { + render(
, container); + }); + + act(() => { + container.querySelector("tbody tr.parent-row td .action-column button").dispatchEvent(new MouseEvent("click", { bubbles: true })); + }); + + expect(primaryActionButton.onClick).toHaveBeenCalled(); + }); + + it("should render and support filtering ", () => { let results: Array = smallData; - const customButtonCallBack: jest.Mock = jest.fn((searchResults: Array) => { results = searchResults; }); - // before search + // before filter + act(() => { + render(
, container); + }); + expect(results.length).toEqual(smallData.length); + + // pass some filters + const onAfterFilterCallBack: jest.Mock = jest.fn((rows: Array) => { + results = rows; + }); + + const onRemoveFilter: jest.Mock = jest.fn((filterItem: FilterItem) => { + console.log(filterItem); + }); + + const filterValues: Array = smallData.map((data: DataItem) => data.status); + act(() => { render(
, container + filterProps={{ + filterItems: [ + { + accessor: "status", + filters: filterValues.slice(0, 1) + } + ], + onAfterFilter: onAfterFilterCallBack, + onRemoveFilter: onRemoveFilter + }} + />, + container ); }); + // after filter, the length of the result should decrease + expect(results.length).not.toEqual(smallData.length); + expect(onAfterFilterCallBack).toBeCalled(); + expect(onRemoveFilter).not.toBeCalled(); + + // mock on + + act(() => { + container.querySelector("tbody .filter-item-holder .filter-item .icon-holder").dispatchEvent(new MouseEvent("click", { bubbles: true })); + }); + + expect(onRemoveFilter).toBeCalled(); + }); + + it("should render and support searching ", () => { + let results: Array = smallData; + const customButtonCallBack: jest.Mock = jest.fn((searchResults: Array) => { + results = searchResults; + }); + + // before search + act(() => { + render(
, container); + }); + expect(results.length).toEqual(smallData.length); expect(customButtonCallBack).not.toHaveBeenCalled(); - // nothing should change if searchInColumns is empty + // nothing should change if searchInColumns is empty or search text is empty act(() => { render(
{ onSearch: customButtonCallBack, searchTriggered: true }} - />, container + />, + container + ); + }); + + expect(results.length).toEqual(smallData.length); + + act(() => { + render( +
, + container ); }); @@ -308,12 +409,47 @@ describe("Component: Table", () => { searchText: smallData[1].firstName, onSearch: customButtonCallBack }} - />, container + />, + container ); }); expect(results.length).toEqual(1); - expect(customButtonCallBack).toHaveBeenCalled(); - }); + // after valid search with number field + act(() => { + render( +
, + container + ); + }); + expect(customButtonCallBack).toHaveBeenCalledTimes(3); + // nothing should be returned when search field is not found or undefined + act(() => { + render( +
, + container + ); + }); + expect(results.length).toEqual(0); + }); }); diff --git a/src/Table/Table.tsx b/src/Table/Table.tsx index 3ac6170e9..e95937313 100644 --- a/src/Table/Table.tsx +++ b/src/Table/Table.tsx @@ -3,18 +3,44 @@ import { randomId } from "../__utils/randomId"; import "./table-style.scss"; -const angleDown: JSX.Element = ; -const angleRightIcon: JSX.Element = ; -const ellipsis: JSX.Element = ; -const sortAsc: JSX.Element = ; -const sortDesc: JSX.Element = ; -const defaultSort: JSX.Element = ; - -export type DataItem = { [name: string]: any } & TableRow; +const angleDown: JSX.Element = ( + + + +); +const angleRightIcon: JSX.Element = ( + + + +); +const ellipsis: JSX.Element = ( + + + +); +const defaultSort: JSX.Element = ( + + + + +); +const timesIcon: JSX.Element = ( + + + +); + +export type DataItem = T & TableRow; type RowTypes = "row" | "subRow"; export interface Column { - label: string; + label: string | React.ReactNode; accessor: string; canSort?: boolean; } @@ -32,6 +58,7 @@ export interface PrimaryActionButton { export interface TableHeader extends Column { isSorted?: boolean; isSortedDesc?: boolean; + filters?: Array; } interface Cell { @@ -95,21 +122,31 @@ function sortArray(items: Array = [], columnName: string, sortDirectio if (isNaN(secondItem[columnName]) && isNaN(firstItem[columnName])) { result = String(firstItem[columnName]).localeCompare(String(secondItem[columnName]), languages as Array, { sensitivity: "base", ignorePunctuation: true }); } else { - result = (firstItem[columnName] - secondItem[columnName]); + result = firstItem[columnName] - secondItem[columnName]; } } else { if (isNaN(secondItem[columnName]) && isNaN(firstItem[columnName])) { result = String(secondItem[columnName]).localeCompare(String(firstItem[columnName]), languages as Array, { sensitivity: "base", ignorePunctuation: true }); } else { - result = (secondItem[columnName] - firstItem[columnName]); + result = secondItem[columnName] - firstItem[columnName]; } } return result; - }); return sortedItems; } +function filterArray(items: Array, columns: Array): Array { + return [...items].filter((row: TableRow) => { + return columns.some((column: TableHeader) => { + return column.filters?.some((filterValue: string) => { + const currentColumn: Cell = row?.cells.find((cell: Cell) => cell?.accessor === column?.accessor); + return currentColumn.value === filterValue; + }); + }); + }); +} + /** * search text in array of table row * @param items the array of table rows @@ -118,24 +155,21 @@ function sortArray(items: Array = [], columnName: string, sortDirectio */ function searchTextInArray(items: Array, keyword: string, searchFields: Array): Array { return [...items].filter((row: TableRow) => { - if (keyword.trim().length === 0 || searchFields.length === 0) { - return true; - } - return ( - searchFields.some((searchColumn: string) => { - let result: boolean = false; - const searchField: string = searchColumn; - const regEx = new RegExp(keyword, "gi"); - if (row[searchField] === null || row[searchField] === undefined) { - result = false; - } else if (typeof row[searchField] === "string") { - result = row[searchField].search(regEx) > -1; - } else if (typeof row[searchField] === "number") { - result = String(row[searchField]).search(regEx) !== -1; - } - return result; - }) - ); + const searchText: string = String(keyword); + + return searchFields.some((searchColumn: string) => { + let result: boolean = false; + const searchField: string = searchColumn; + const regEx: RegExp = new RegExp(searchText, "gi"); + if (row[searchField] === null || row[searchField] === undefined) { + result = false; + } else if (typeof row[searchField] === "string") { + result = row[searchField].search(regEx) > -1; + } else if (typeof row[searchField] === "number") { + result = String(row[searchField]).search(regEx) !== -1; + } + return result; + }); }); } @@ -168,7 +202,7 @@ const ActionColumn: React.FunctionComponent = (props: ActionC return (
- {props.primaryActionButton && + {props.primaryActionButton && ( - } - {props.actionLinks && props.actionLinks.length && + )} + {props.actionLinks && props.actionLinks?.length ? (
) => { @@ -202,14 +236,11 @@ const ActionColumn: React.FunctionComponent = (props: ActionC props.onActionDropped(e); }} > -
+
{ellipsis}
-
- {props.actionLinks.map((link: ActionLinkItem, index: number) => +
+ {props.actionLinks.map((link: ActionLinkItem, index: number) => ( ) => { @@ -219,10 +250,10 @@ const ActionColumn: React.FunctionComponent = (props: ActionC > {link.label} - )} + ))}
- } + ) : null}
); }; @@ -253,9 +284,9 @@ const RowUI: React.FunctionComponent = (props: RowUIProps) => {
- {props.useRowSelection ? + {props.useRowSelection ? ( - : - ((props.row.subRows.length > 0 || props.row.rowContentDetail) && props.rowsAreCollapsable) && - - } + ) : ( + (props.row.subRows.length > 0 || props.row.rowContentDetail) && + props.rowsAreCollapsable && ( + + ) + )} {props.row.cells.map((cell: Cell, cellIndex: number) => { - return ; + return ; })} - {props.useShowActionColumn && + {props.useShowActionColumn && ( - } + )} - {props.type === "subRow" && + {props.type === "subRow" && ( - - } + )} + ); +}; +interface FilterRowProps { + columns: Array; + useRowCollapse: boolean; + useRowSelection: boolean; + showFilterRow?: boolean; + filterProps: FilterProps; +} + +const FilterRowUI: React.FunctionComponent = (props: FilterRowProps) => { + return ( + props.showFilterRow && ( + + {(props.useRowSelection || props.useRowCollapse) && + ))} + + ) ); }; @@ -359,141 +429,133 @@ interface TableUIProps { useRowCollapse: boolean; useRowSelection: boolean; useShowActionColumn: boolean; + showFilterRow?: boolean; + filterProps: FilterProps; } -const TableUI: React.FunctionComponent = React.memo((props: TableUIProps): React.ReactElement => { - const [checkAllRandomIds] = React.useState(randomId("chk-all")); - const tableRef: React.RefObject = React.createRef(); - return ( -
-
= (props: RowUIProps) => { />
- {((props.row.subRows.length > 0 || props.row.rowContentDetail) && props.rowsAreCollapsable) && + {(props.row.subRows.length > 0 || props.row.rowContentDetail) && props.rowsAreCollapsable && (
) => { if (props.type === "row") { props.onRowExpanded(e, props.row); @@ -287,31 +319,32 @@ const RowUI: React.FunctionComponent = (props: RowUIProps) => { > {props.row.expanded ? angleDown : angleRightIcon}
- } -
-
) => { - if (props.type === "row") { - props.onRowExpanded && props.onRowExpanded(e, props.row); - } else { - props.onSubRowExpanded(e, props.row, props.parentRowIndex); - } - }} - > - {props.row.expanded ? angleDown : angleRightIcon} -
+ )}
+
) => { + if (props.type === "row") { + props.onRowExpanded && props.onRowExpanded(e, props.row); + } else { + props.onSubRowExpanded(e, props.row, props.parentRowIndex); + } + }} + > + {props.row.expanded ? angleDown : angleRightIcon} +
+
- {cell.value} - {cell.value} = (props: RowUIProps) => { }} />
-
- {props.row.rowContentDetail} -
+
+
{props.row.rowContentDetail}
} + {props.columns?.map((column: TableHeader, index: number) => ( + +
+ {column.filters?.map((filter: string, filterIndex: number) => { + return ( +
+ {filter} +
) => { + props.filterProps?.onRemoveFilter({ accessor: column?.accessor, value: filter }); + }} + > + {timesIcon} +
+
+ ); + })} +
+
- - - {props.useRowSelection ? - - : - props.rowsAreCollapsable && - )) - } - {props.useShowActionColumn && - - - {props.rows.map((row: TableRow, i: number) => { - return ( - - - {row.subRows.map((subRow: TableRow) => { - return ( - - - - ); - })} - - - - +const TableUI: React.FunctionComponent = React.memo( + (props: TableUIProps): React.ReactElement => { + const [checkAllRandomIds] = React.useState(randomId("chk-all")); + const tableRef: React.RefObject = React.createRef(); - - ); - })} - - - {props.footer && + return ( +
+
-
- -
-
- } - {props.columns.map((header: TableHeader, index: number) => ( - ) => { - if (props.sortable && header.canSort) { - props.onSort(header.accessor, header.isSortedDesc ? sortDirectionTypes.Ascending : sortDirectionTypes.Descending); - } else { - e.preventDefault(); - } - }} - > - {header.label} - {(props.sortable && header.canSort) && -
- {header.isSorted ? - - (header.isSortedDesc ? - sortDesc - : - - sortAsc - ) - : - defaultSort - } -
- } -
} -
-
- {row.rowContentDetail} -
- -
+ - + {props.useRowSelection ? ( + + ) : ( + props.rowsAreCollapsable && + ))} + {props.useShowActionColumn && - } - -
- {props.footer} - +
+ +
+
+ )} + {props.columns?.map((header: TableHeader, index: number) => ( + ) => { + if (props.sortable && header.canSort) { + props.onSort(header?.accessor, header.isSortedDesc ? sortDirectionTypes.Ascending : sortDirectionTypes.Descending); + } else { + e.preventDefault(); + } + }} + > + {header.label} + {props.sortable && header.canSort && ( +
+ {defaultSort} +
+ )} +
}
-
- ); + + + + {props.rows?.map((row: TableRow, i: number) => { + return ( + + + {row.subRows?.map((subRow: TableRow) => { + return ( + + + + ); + })} + + + +
{row.rowContentDetail}
+ + +
+ ); + })} + + + {props.footer && ( + + {props.footer} + + )} + + +
+ ); + } +); -}); export interface SearchProps { onSearch?: (rows: Array) => void; searchInColumns?: Array; @@ -502,8 +564,18 @@ export interface SearchProps { triggerSearchOn?: "Change" | "Submit"; } export interface SortProps { - onSort?: (rows: Array, accessor: string, sortDirection: sortDirectionTypes) => Array; onAfterSorting?: (rows: Array, sortByColumn: TableHeader) => void; + onSort?: (rows: Array, accessor: string, sortDirection: sortDirectionTypes) => Array; + useServerSorting?: boolean; +} +export interface FilterItem { + accessor: string; + filters: Array; +} +export interface FilterProps { + filterItems: Array; + onAfterFilter: (rows: Array) => void; + onRemoveFilter: (item: { accessor: string; value: string }) => void; } interface TableProps { @@ -511,7 +583,8 @@ interface TableProps { className?: string; columns: Array; currentpage?: number; - data: Array; + data: Array>; + filterProps?: FilterProps; footer?: React.ReactNode; offset?: number; onRowExpanded?: (expandedRowList: Array) => void; @@ -521,483 +594,540 @@ interface TableProps { sortProps?: SortProps; } -export const Table: React.FunctionComponent = React.memo((props: TableProps): React.ReactElement => { - const [allItemsChecked, setAllRowsChecked] = React.useState(false); - const [currentTableRows, setCurrentTableRows] = React.useState>([]); - const [hasAnOpenedAction, setAnOpenedAction] = React.useState(false); - const [tableColumns, setTableColumn] = React.useState>([]); - const [tableRows, setTableRows] = React.useState>([]); - const [tableRowsImage, setTableRowsImage] = React.useState>([]); - - // events ------------------------------------------------------------------------------------- - - /** - * Call when item is selected - * @param e change event - * @param selectedRow The selected row - * @param type The row type (i.e either a row or subRow) - * @param rowIndex The index of the parent row incase of subRow - */ - const onItemSelected = React.useCallback((e: React.ChangeEvent, selectedRow: TableRow, type: "subRow" | "row", rowIndex?: number): void => { - const updatedOriginalRows: Array = selectItems(e.target.checked, tableRows, selectedRow, rowIndex, type); - const updatedRows: Array = selectItems(e.target.checked, currentTableRows, selectedRow, rowIndex, type); - - const selectedRowList: Array = updatedOriginalRows.filter((item: TableRow) => { - return item.selected || item.subRows.some((sub: TableRow) => sub.selected); - }).map((newRow: TableRow) => { - return { - ...newRow, - subRows: newRow.subRows.filter((subRowItem: TableRow) => subRowItem.selected) - }; - }); - - setCurrentTableRows(updatedRows); - setTableRows(updatedOriginalRows); - setTableRowsImage(updatedOriginalRows); - props.onRowSelected(selectedRowList); - }, [tableRows, currentTableRows, props.onRowSelected]); - - /** - * - * @param exchange - * Called onAllItemsSelected - */ - const onAllItemsSelected = React.useCallback((e: React.ChangeEvent): void => { - const updatedOriginalRows: Array = tableRows.map((originalRow: TableRow) => { - const updatedSubRows: Array = originalRow.subRows.map((subRow: TableRow) => { - return { ...subRow, selected: e.target.checked }; - }); - return ( - { ...originalRow, selected: e.target.checked, subRows: updatedSubRows } - ); - }); +export const Table: React.FunctionComponent = React.memo( + (props: TableProps): React.ReactElement => { + const [allItemsChecked, setAllRowsChecked] = React.useState(false); + const [currentTableRows, setCurrentTableRows] = React.useState>([]); + const [hasAnOpenedAction, setAnOpenedAction] = React.useState(false); + const [tableColumns, setTableColumn] = React.useState>([]); + const [tableRows, setTableRows] = React.useState>([]); + const [tableRowsImage, setTableRowsImage] = React.useState>([]); + + // events ------------------------------------------------------------------------------------- + + /** + * Call when item is selected + * @param e change event + * @param selectedRow The selected row + * @param type The row type (i.e either a row or subRow) + * @param rowIndex The index of the parent row incase of subRow + */ + const onItemSelected = React.useCallback( + (e: React.ChangeEvent, selectedRow: TableRow, type: "subRow" | "row", rowIndex?: number): void => { + const updatedOriginalRows: Array = selectItems(e.target.checked, tableRows, selectedRow, rowIndex, type); + const updatedRows: Array = selectItems(e.target.checked, currentTableRows, selectedRow, rowIndex, type); + + const selectedRowList: Array = updatedOriginalRows + .filter((item: TableRow) => { + return item.selected || item.subRows.some((sub: TableRow) => sub.selected); + }) + .map((newRow: TableRow) => { + return { + ...newRow, + subRows: newRow.subRows.filter((subRowItem: TableRow) => subRowItem.selected) + }; + }); - const updatedRows: Array = currentTableRows.map((row: TableRow) => { - const updatedSubRows: Array = row.subRows.map((subRow: TableRow) => { - return { ...subRow, selected: e.target.checked }; - }); - return ( - { ...row, selected: e.target.checked, subRows: updatedSubRows } - ); - }); + setCurrentTableRows(updatedRows); + setTableRows(updatedOriginalRows); + setTableRowsImage(updatedOriginalRows); + props.onRowSelected(selectedRowList); + }, + [tableRows, currentTableRows, props.onRowSelected] + ); - setCurrentTableRows(updatedRows); - setTableRows(updatedOriginalRows); - setTableRowsImage(updatedOriginalRows); - props.onRowSelected(updatedOriginalRows); - }, [tableRows, currentTableRows, props.onRowSelected]); - - /** - * Close all opened actions div - */ - const onClickOutside = React.useCallback((e: MouseEvent) => { - const parentElement: Element = (e.target as Element).parentElement; - if (hasAnOpenedAction && (parentElement.id.indexOf("ellipsis") < 0) && (!parentElement.classList.contains("dropdown-content"))) { - const updatedOriginalRows: Array = tableRows.map((originalRow: TableRow) => { - const subRows: Array = originalRow.subRows.map((subRow: TableRow) => { - return { ...subRow, actionsDropdownDropped: false }; + /** + * + * @param exchange + * Called onAllItemsSelected + */ + const onAllItemsSelected = React.useCallback( + (e: React.ChangeEvent): void => { + const updatedOriginalRows: Array = tableRows?.map((originalRow: TableRow) => { + const updatedSubRows: Array = originalRow.subRows.map((subRow: TableRow) => { + return { ...subRow, selected: e.target.checked }; + }); + return { ...originalRow, selected: e.target.checked, subRows: updatedSubRows }; }); - return { ...originalRow, actionsDropdownDropped: false, subRows }; - }); - const updatedRows: Array = currentTableRows.map((currentRow: TableRow) => { - const subRows: Array = currentRow.subRows.map((subRow: TableRow) => { - return { ...subRow, actionsDropdownDropped: false }; + const updatedRows: Array = currentTableRows?.map((row: TableRow) => { + const updatedSubRows: Array = row.subRows.map((subRow: TableRow) => { + return { ...subRow, selected: e.target.checked }; + }); + return { ...row, selected: e.target.checked, subRows: updatedSubRows }; }); - return { ...currentRow, actionsDropdownDropped: false, subRows }; - }); - setCurrentTableRows(updatedRows); - setTableRows(updatedOriginalRows); - setTableRowsImage(updatedOriginalRows); - } - }, [hasAnOpenedAction, tableRows, currentTableRows]); - - /** - * - * @param event click event - * @param row The selected row - * @param rowIndex The index of the parent row - */ - const onActionColumnDropped = React.useCallback((event: React.MouseEvent, row: TableRow, rowIndex?: number) => { - let updatedOriginalRows: Array = []; - let updatedRows: Array = []; - if (rowIndex) { - updatedOriginalRows = tableRows.map((originalRow: TableRow) => { - if (originalRow.rowIndex === rowIndex) { - const subRows: Array = originalRow.subRows.map((subRow: TableRow) => { - if (subRow.rowIndex === row.rowIndex) { - return { ...subRow, actionsDropdownDropped: !subRow.actionsDropdownDropped }; - } + setCurrentTableRows(updatedRows); + setTableRows(updatedOriginalRows); + setTableRowsImage(updatedOriginalRows); + props.onRowSelected(updatedOriginalRows); + }, + [tableRows, currentTableRows, props.onRowSelected] + ); + + /** + * Close all opened actions div + */ + const onClickOutside = React.useCallback( + (e: MouseEvent) => { + const parentElement: Element = (e.target as Element).parentElement; + if (hasAnOpenedAction && parentElement.id.indexOf("ellipsis") < 0 && !parentElement.classList.contains("dropdown-content")) { + const updatedOriginalRows: Array = tableRows?.map((originalRow: TableRow) => { + const subRows: Array = originalRow.subRows.map((subRow: TableRow) => { + return { ...subRow, actionsDropdownDropped: false }; + }); + return { ...originalRow, actionsDropdownDropped: false, subRows }; + }); - return { ...subRow, actionsDropdownDropped: false }; + const updatedRows: Array = currentTableRows?.map((currentRow: TableRow) => { + const subRows: Array = currentRow.subRows.map((subRow: TableRow) => { + return { ...subRow, actionsDropdownDropped: false }; + }); + return { ...currentRow, actionsDropdownDropped: false, subRows }; }); - return { ...originalRow, subRows }; + setCurrentTableRows(updatedRows); + setTableRows(updatedOriginalRows); + setTableRowsImage(updatedOriginalRows); } - return { ...originalRow, actionsDropdownDropped: false }; - }); + }, + [hasAnOpenedAction, tableRows, currentTableRows] + ); + + /** + * + * @param event click event + * @param row The selected row + * @param rowIndex The index of the parent row + */ + const onActionColumnDropped = React.useCallback( + (event: React.MouseEvent, row: TableRow, rowIndex?: number) => { + let updatedOriginalRows: Array = []; + let updatedRows: Array = []; + if (rowIndex) { + updatedOriginalRows = tableRows?.map((originalRow: TableRow) => { + if (originalRow.rowIndex === rowIndex) { + const subRows: Array = originalRow.subRows.map((subRow: TableRow) => { + if (subRow.rowIndex === row.rowIndex) { + return { ...subRow, actionsDropdownDropped: !subRow.actionsDropdownDropped }; + } + + return { ...subRow, actionsDropdownDropped: false }; + }); + + return { ...originalRow, subRows }; + } + return { ...originalRow, actionsDropdownDropped: false }; + }); + + updatedRows = currentTableRows?.map((currentRow: TableRow) => { + if (currentRow.rowIndex === rowIndex) { + const subRows: Array = currentRow.subRows.map((subRow: TableRow) => { + if (subRow.rowIndex === row.rowIndex) { + return { ...subRow, actionsDropdownDropped: !subRow.actionsDropdownDropped }; + } - updatedRows = currentTableRows.map((currentRow: TableRow) => { - if (currentRow.rowIndex === rowIndex) { - const subRows: Array = currentRow.subRows.map((subRow: TableRow) => { - if (subRow.rowIndex === row.rowIndex) { - return { ...subRow, actionsDropdownDropped: !subRow.actionsDropdownDropped }; + return { ...subRow, actionsDropdownDropped: false }; + }); + + return { ...currentRow, subRows }; + } + return { ...currentRow, actionsDropdownDropped: false }; + }); + } else { + updatedOriginalRows = tableRows?.map((originalRow: TableRow) => { + if (originalRow.rowIndex === row.rowIndex) { + return { ...originalRow, actionsDropdownDropped: !originalRow.actionsDropdownDropped }; } - return { ...subRow, actionsDropdownDropped: false }; + return { ...originalRow, actionsDropdownDropped: false }; }); - return { ...currentRow, subRows }; + updatedRows = currentTableRows?.map((currentRow: TableRow, index) => { + if (currentRow.rowIndex === row.rowIndex) { + return { ...currentRow, actionsDropdownDropped: !currentRow.actionsDropdownDropped }; + } + return { ...currentRow, actionsDropdownDropped: false }; + }); } - return { ...currentRow, actionsDropdownDropped: false }; - }); - } else { - updatedOriginalRows = tableRows.map((originalRow: TableRow) => { - if (originalRow.rowIndex === row.rowIndex) { - return { ...originalRow, actionsDropdownDropped: !originalRow.actionsDropdownDropped }; - } + setCurrentTableRows(updatedRows); + setTableRows(updatedOriginalRows); + setTableRowsImage(updatedOriginalRows); + }, + [tableRows, currentTableRows] + ); - return { ...originalRow, actionsDropdownDropped: false }; - }); + /** + * Sort rows in ASC or DESC order + * @param accessor The id of the selected column header + * @param sortDirection The direction of the sort : ASC or DESC + */ + const onSortItems = React.useCallback( + (accessor: string, sortDirection: sortDirectionTypes) => { + let updatedOriginalRows: Array = []; + let updatedCurrentTableRows: Array = []; + let sortByColumn: TableHeader = null; + + if (props.sortProps?.onSort && props.sortProps?.useServerSorting) { + props.sortProps?.onSort(tableRows, accessor, sortDirection); + } else { + if (props.sortProps?.onSort) { + updatedOriginalRows = props.sortProps.onSort(tableRows, accessor, sortDirection); + updatedCurrentTableRows = props.sortProps.onSort(currentTableRows, accessor, sortDirection); + } else { + updatedOriginalRows = sortArray(tableRows, accessor, sortDirection); + updatedCurrentTableRows = sortArray(currentTableRows, accessor, sortDirection); + } + const updatedColumns: Array = tableColumns.map((column: TableHeader) => { + if (column?.accessor === accessor) { + sortByColumn = { + ...column, + isSorted: true, + isSortedDesc: sortDirection === sortDirectionTypes.Descending ? true : false + }; + return sortByColumn; + } + return { ...column, isSorted: false, isSortedDesc: false }; + }); - updatedRows = currentTableRows.map((currentRow: TableRow, index) => { - if (currentRow.rowIndex === row.rowIndex) { - return ( - { ...currentRow, actionsDropdownDropped: !currentRow.actionsDropdownDropped } - ); + setTableRows(updatedOriginalRows); + setCurrentTableRows(updatedCurrentTableRows); + setTableRowsImage(updatedOriginalRows); + setTableColumn(updatedColumns); + props.sortProps?.onAfterSorting(updatedOriginalRows, sortByColumn); } - return { ...currentRow, actionsDropdownDropped: false }; - }); - } + }, + [props.sortProps, tableColumns, tableRows, currentTableRows] + ); - setCurrentTableRows(updatedRows); - setTableRows(updatedOriginalRows); - setTableRowsImage(updatedOriginalRows); - }, [tableRows, currentTableRows]); - - /** - * Sort rows in ASC or DESC order - * @param accessor The id of the selected column header - * @param sortDirection The direction of the sort : ASC or DESC - */ - const onSortItems = React.useCallback(async (accessor: string, sortDirection: sortDirectionTypes) => { - let updatedOriginalRows: Array = []; - let updatedCurrentTableRows: Array = []; - let sortByColumn: TableHeader = null; - - if (props.sortProps?.onSort) { - updatedOriginalRows = props.sortProps.onSort(tableRows, accessor, sortDirection); - updatedCurrentTableRows = props.sortProps.onSort(currentTableRows, accessor, sortDirection); - } else { - updatedOriginalRows = sortArray(tableRows, accessor, sortDirection); - updatedCurrentTableRows = sortArray(currentTableRows, accessor, sortDirection); - } - const updatedColumns: Array = tableColumns.map((column: TableHeader) => { - if (column.accessor === accessor) { - sortByColumn = { - ...column, - isSorted: true, - isSortedDesc: sortDirection === sortDirectionTypes.Descending ? true : false - }; - return sortByColumn; - } - return { ...column, isSorted: false, isSortedDesc: false }; - }); + /** + * Called on sub row is clicked + * @param e change ve + * @param row The subrow Selected row + * @param rowIndex The parent row index + */ + const onSubRowExpanded = React.useCallback( + (e: React.MouseEvent, row: TableRow, rowIndex: number): void => { + const updatedOriginalRows: Array = tableRows?.map((originalRow: TableRow) => { + if (originalRow.rowIndex === rowIndex) { + const subRows: Array = originalRow.subRows.map((subRow: TableRow) => { + if (subRow.rowIndex === row.rowIndex) { + return { ...subRow, expanded: !subRow.expanded }; + } - setTableRows(updatedOriginalRows); - setCurrentTableRows(updatedCurrentTableRows); - setTableRowsImage(updatedOriginalRows); - setTableColumn(updatedColumns); - props.sortProps.onAfterSorting(updatedOriginalRows, sortByColumn); - }, [props.sortProps, tableColumns, tableRows, currentTableRows]); - - /** - * Called on sub row is clicked - * @param e change ve - * @param row The subrow Selected row - * @param rowIndex The parent row index - */ - const onSubRowExpanded = React.useCallback((e: React.MouseEvent, row: TableRow, rowIndex: number): void => { - const updatedOriginalRows: Array = tableRows.map((originalRow: TableRow) => { - if (originalRow.rowIndex === rowIndex) { - const subRows: Array = originalRow.subRows.map((subRow: TableRow) => { - if (subRow.rowIndex === row.rowIndex) { - return { ...subRow, expanded: !subRow.expanded }; - } + return subRow; + }); - return subRow; + return { ...originalRow, subRows }; + } + return originalRow; }); - return { ...originalRow, subRows }; - } - return originalRow; - }); + const updatedRows: Array = currentTableRows?.map((currentRow: TableRow) => { + if (currentRow.rowIndex === rowIndex) { + const subRows: Array = currentRow.subRows.map((subRow: TableRow) => { + if (subRow.rowIndex === row.rowIndex) { + return { ...subRow, expanded: !subRow.expanded }; + } - const updatedRows: Array = currentTableRows.map((currentRow: TableRow) => { - if (currentRow.rowIndex === rowIndex) { - const subRows: Array = currentRow.subRows.map((subRow: TableRow) => { - if (subRow.rowIndex === row.rowIndex) { - return { ...subRow, expanded: !subRow.expanded }; - } + return subRow; + }); - return subRow; + return { ...currentRow, subRows }; + } + return currentRow; }); - return { ...currentRow, subRows }; - } - return currentRow; - }); + const expandedRowList: Array = updatedOriginalRows.filter((item: TableRow) => { + return item.expanded || item.subRows.some((sub: TableRow) => sub.expanded); + }); - const expandedRowList: Array = updatedOriginalRows.filter((item: TableRow) => { - return item.expanded || item.subRows.some((sub: TableRow) => sub.expanded); - }); + setCurrentTableRows(updatedRows); + setTableRows(updatedOriginalRows); + setTableRowsImage(updatedOriginalRows); + props.onRowExpanded(expandedRowList); + }, + [currentTableRows, tableRows, props.onRowExpanded] + ); - setCurrentTableRows(updatedRows); - setTableRows(updatedOriginalRows); - setTableRowsImage(updatedOriginalRows); - props.onRowExpanded(expandedRowList); - }, [currentTableRows, tableRows, props.onRowExpanded]); - - /** - * - * @param e change event - * @param row The selected row - */ - const onRowExpanded = React.useCallback((e: React.MouseEvent, row: TableRow): void => { - const updatedOriginalRows: Array = tableRows.map((originalRow: TableRow) => { - if (originalRow.rowIndex === row.rowIndex) { - return { - ...originalRow, - expanded: !originalRow.expanded, - subRows: originalRow?.subRows.map((subRow: TableRow) => ({ ...subRow, expanded: false })) - }; - } + /** + * + * @param e change event + * @param row The selected row + */ + const onRowExpanded = React.useCallback( + (e: React.MouseEvent, row: TableRow): void => { + const updatedOriginalRows: Array = tableRows?.map((originalRow: TableRow) => { + if (originalRow.rowIndex === row.rowIndex) { + return { + ...originalRow, + expanded: !originalRow.expanded, + subRows: originalRow?.subRows.map((subRow: TableRow) => ({ ...subRow, expanded: false })) + }; + } - return originalRow; - }); + return originalRow; + }); - const updatedRows: Array = currentTableRows.map((currentRow: TableRow, index) => { - if (currentRow.rowIndex === row.rowIndex) { - return ( - { - ...currentRow, expanded: !currentRow.expanded, - subRows: currentRow?.subRows.map((subRow: TableRow) => ({ ...subRow, expanded: false })) + const updatedRows: Array = currentTableRows?.map((currentRow: TableRow, index) => { + if (currentRow.rowIndex === row.rowIndex) { + return { + ...currentRow, + expanded: !currentRow.expanded, + subRows: currentRow?.subRows.map((subRow: TableRow) => ({ ...subRow, expanded: false })) + }; } - ); - } - return currentRow; - }); + return currentRow; + }); - const expandedRowList: Array = updatedOriginalRows.filter((item: TableRow) => { - return item.expanded || item.subRows.some((sub: TableRow) => sub.expanded); - }).map((newRow: TableRow) => { - return { - ...newRow, - subRows: newRow.subRows.filter((subRowItem: TableRow) => subRowItem.expanded) - }; - }); + const expandedRowList: Array = updatedOriginalRows + .filter((item: TableRow) => { + return item.expanded || item.subRows.some((sub: TableRow) => sub.expanded); + }) + .map((newRow: TableRow) => { + return { + ...newRow, + subRows: newRow.subRows.filter((subRowItem: TableRow) => subRowItem.expanded) + }; + }); - setCurrentTableRows(updatedRows); - setTableRows(updatedOriginalRows); - setTableRowsImage(updatedOriginalRows); - props.onRowExpanded(expandedRowList); - }, [tableRows, currentTableRows, props.onRowExpanded]); - // functions ----------------------------------------------------------------------------- - /** - * - * @param rows The table or or data to initialize rows from - */ - const getRows = React.useCallback((rows: Array): Array => { - const updatedRows: Array = rows.map((row: TableRow, index: number) => { - const updatedCells: Array = Object.keys(row).filter((key: string) => { - return key !== "rowContentDetail" && key !== "subRows"; - }).map((accessor: string): Cell => { - return { - id: accessor, - accessor, - value: row[accessor] - }; - }); + setCurrentTableRows(updatedRows); + setTableRows(updatedOriginalRows); + setTableRowsImage(updatedOriginalRows); + props.onRowExpanded(expandedRowList); + }, + [tableRows, currentTableRows, props.onRowExpanded] + ); + // functions ----------------------------------------------------------------------------- + /** + * + * @param rows The table or or data to initialize rows from + */ + const getRows = React.useCallback((rows: Array>): Array => { + const updatedRows: Array = rows?.map((row: TableRow, index: number) => { + const updatedCells: Array = Object.keys(row) + .filter((key: string) => { + return ["rowContentDetail", "subRows", "cells", "expanded", "actionsDropdownDropped", "selected", "rowIndex"].indexOf(key) < 0; + }) + .map( + (accessor: string): Cell => { + return { + id: accessor, + accessor, + value: row[accessor] + }; + } + ); - return ( - { + return { ...row, rowIndex: index, cells: updatedCells, - selected: false, - actionsDropdownDropped: false, - expanded: false, + selected: row.selected || false, + actionsDropdownDropped: row.actionsDropdownDropped || false, + expanded: row.expanded || false, subRows: row.subRows ? getRows(row.subRows) : [] - } - ); - }); + }; + }); - return updatedRows; - }, []); - - /** - * Call when item is selected - * @param checked boolean representing checkbox value - * @param selectedRow The selected row - * @param type The row type (i.e either a row or subRow) - * @param rowIndex The index of the parent row incase of subRow - */ - const selectItems = React.useCallback((checked: boolean, rows: Array, selectedRow: TableRow, rowIndex: number, type: RowTypes): Array => { - const updatedRows: Array = rows.map((originalRow: TableRow) => { - let updatedSubrows: Array = originalRow.subRows; - if (type === "row") { - if (originalRow.rowIndex === selectedRow.rowIndex) { - updatedSubrows = updatedSubrows.map((subRow: TableRow) => ({ ...subRow, selected: checked })); - - return { ...originalRow, selected: checked, subRows: updatedSubrows }; - } - } else if (type === "subRow") { - if (originalRow.rowIndex === rowIndex) { - updatedSubrows = originalRow.subRows.map((originalSubrow: TableRow) => { - if (originalSubrow.rowIndex === selectedRow.rowIndex) { - return { ...originalSubrow, selected: checked }; - } - return originalSubrow; - }); - return { ...originalRow, subRows: updatedSubrows, selected: false }; + return updatedRows || []; + }, []); + + /** + * Call when item is selected + * @param checked boolean representing checkbox value + * @param selectedRow The selected row + * @param type The row type (i.e either a row or subRow) + * @param rowIndex The index of the parent row incase of subRow + */ + const selectItems = React.useCallback((checked: boolean, rows: Array, selectedRow: TableRow, rowIndex: number, type: RowTypes): Array => { + const updatedRows: Array = rows.map((originalRow: TableRow) => { + let updatedSubrows: Array = originalRow.subRows; + if (type === "row") { + if (originalRow.rowIndex === selectedRow.rowIndex) { + updatedSubrows = updatedSubrows.map((subRow: TableRow) => ({ ...subRow, selected: checked })); + + return { ...originalRow, selected: checked, subRows: updatedSubrows }; + } + } else if (type === "subRow") { + if (originalRow.rowIndex === rowIndex) { + updatedSubrows = originalRow.subRows.map((originalSubrow: TableRow) => { + if (originalSubrow.rowIndex === selectedRow.rowIndex) { + return { ...originalSubrow, selected: checked }; + } + return originalSubrow; + }); + return { ...originalRow, subRows: updatedSubrows, selected: false }; + } } - } - return originalRow; - }); + return originalRow; + }); - return updatedRows; - }, []); + return updatedRows; + }, []); - const setDefaultTableRows = React.useCallback(async () => { - const updatedRows: Array = getRows(props.data); - setTableRows(updatedRows); - setTableRowsImage(updatedRows); - }, [props.data]); + const setDefaultTableRows = React.useCallback(() => { + const updatedRows: Array = getRows(props.data); + setTableRows(updatedRows); + setTableRowsImage(updatedRows); + }, [props.data]); - const doPaginate = React.useCallback((): void => { - if (props.currentpage && props.offset && (tableRows.length > 0)) { - // pagination start from 1 hence the need fro deducting 1 - const start: number = (props.currentpage - 1) * props.offset; - const end: number = (props.offset * (props.currentpage)); + const doPaginate = React.useCallback((): void => { + if (props.currentpage && props.offset && tableRows?.length > 0) { + // pagination start from 1 hence the need fro deducting 1 + const start: number = (props.currentpage - 1) * props.offset; + const end: number = props.offset * props.currentpage; - const currentPage: Array = tableRows.slice(start, end); - setCurrentTableRows(currentPage); - } else { - setCurrentTableRows(tableRows); - } - }, [props.currentpage, props.offset, tableRows]); + const currentPage: Array = tableRows?.slice(start, end); + setCurrentTableRows(currentPage); + } else { + setCurrentTableRows(tableRows); + } + }, [props.currentpage, props.offset, tableRows]); - const rowsAreCollapsable = React.useCallback((): boolean => { - return currentTableRows.some((row: TableRow) => { + const rowsAreCollapsable = React.useCallback((): boolean => { return ( - ((row.subRows.length > 0) || row.rowContentDetail) || - (row.subRows.some((subRow: TableRow) => subRow.rowContentDetail || (row.subRows && row.subRows.length > 0))) + currentTableRows?.some((row: TableRow) => { + return row.subRows.length > 0 || row.rowContentDetail || row.subRows.some((subRow: TableRow) => subRow.rowContentDetail || (row.subRows && row.subRows.length > 0)); + }) && !!props.onRowExpanded ); - }) && !!props.onRowExpanded; - }, [currentTableRows]); + }, [currentTableRows]); - const doSearch = React.useCallback((): void => { - let searchResult: Array = []; - if (props.searchProps?.searchText && props.searchProps?.searchInColumns && props.searchProps?.searchInColumns.length > 0) { - searchResult = searchTextInArray(tableRowsImage, props.searchProps.searchText, props.searchProps.searchInColumns); - } else { - searchResult = [...tableRowsImage]; - } + const showFilterRow = React.useCallback((): boolean => { + return tableColumns.some((column: TableHeader) => column.filters?.length); + }, [tableColumns]); - setTableRows(searchResult); - props.searchProps?.onSearch && props.searchProps?.onSearch(searchResult); - }, [props.searchProps, tableRowsImage]); + const doSearch = React.useCallback((): void => { + let searchResult: Array = []; + if (props.searchProps?.searchText && props.searchProps?.searchInColumns && props.searchProps?.searchInColumns?.length) { + searchResult = searchTextInArray(tableRowsImage, props.searchProps.searchText, props.searchProps.searchInColumns); + } else { + searchResult = [...tableRowsImage]; + } - // useEffects ---------------------------------------------------------- + setTableRows(searchResult); + props.searchProps?.onSearch && props.searchProps?.onSearch(searchResult); + }, [props.searchProps, tableRowsImage]); - // Adding event listener to listen to clicks outside the component on mount, removing on unmount - React.useEffect(() => { - document.addEventListener("mousedown", onClickOutside); - return () => { - document.removeEventListener("mousedown", onClickOutside); - }; - }); + // useEffects ---------------------------------------------------------- - React.useEffect(() => { - if (!!props.onRowSelected) { - const notAllsAreRowsSelected: boolean = tableRows.some((row: TableRow) => !row.selected); + // Adding event listener to listen to clicks outside the component on mount, removing on unmount + React.useEffect(() => { + document.addEventListener("mousedown", onClickOutside); + return () => { + document.removeEventListener("mousedown", onClickOutside); + }; + }); - if (notAllsAreRowsSelected) { - setAllRowsChecked(false); - } else { - setAllRowsChecked(true); + React.useEffect(() => { + if (tableColumns?.length && tableRows?.length) { + const shouldFilter: boolean = tableColumns.some((column: TableHeader) => column.filters?.length); + if (shouldFilter) { + const filteredRows: Array = filterArray(tableRowsImage, tableColumns); + props.filterProps?.onAfterFilter && props.filterProps.onAfterFilter(filteredRows); + setTableRows(filteredRows); + } else { + setTableRows(tableRowsImage); + } } - } + }, [tableColumns]); - const actionColumnIsOpened: boolean = tableRows.some((row: TableRow) => { - return row.actionsDropdownDropped || row.subRows?.some((sub: TableRow) => sub.actionsDropdownDropped); - }); - setAnOpenedAction(actionColumnIsOpened); - }, [currentTableRows]); + React.useEffect(() => { + if (!!props.onRowSelected) { + const notAllsAreRowsSelected: boolean = tableRows?.some((row: TableRow) => !row.selected); - React.useEffect(() => { - if (!!props.searchProps?.onSearch) { - if (props.searchProps.triggerSearchOn === "Change") { - doSearch(); + if (notAllsAreRowsSelected) { + setAllRowsChecked(false); + } else { + setAllRowsChecked(true); + } } - } - }, [props.searchProps?.searchInColumns, props.searchProps?.searchText]); - React.useEffect(() => { - if (!!props.searchProps?.onSearch) { - if (props.searchProps?.triggerSearchOn === "Submit") { - doSearch(); + const actionColumnIsOpened: boolean = tableRows?.some((row: TableRow) => { + return row.actionsDropdownDropped || row.subRows?.some((sub: TableRow) => sub.actionsDropdownDropped); + }); + setAnOpenedAction(actionColumnIsOpened); + }, [currentTableRows]); + + React.useEffect(() => { + if (!!props.searchProps?.onSearch) { + if (props.searchProps.triggerSearchOn === "Change") { + doSearch(); + } } - } - }, [props.searchProps?.searchTriggered]); + }, [props.searchProps?.searchInColumns, props.searchProps?.searchText]); - React.useEffect(() => { - const updatedColumns: Array = props.columns.map((column: TableHeader) => { - return { - ...column, - isSorted: false, - canSort: (column.canSort !== undefined) ? column.canSort : (!!props.sortProps ? true : false), - isSortedDesc: false - }; - }); + React.useEffect(() => { + if (!!props.searchProps?.onSearch) { + if (props.searchProps?.triggerSearchOn === "Submit") { + doSearch(); + } + } + }, [props.searchProps?.searchTriggered]); + + React.useEffect(() => { + if (tableColumns && props.filterProps?.filterItems?.length) { + const updatedColumns: Array = tableColumns.map((column: TableHeader) => { + const selectedFilter: FilterItem = props.filterProps?.filterItems?.find((filter: FilterItem) => filter?.accessor === column.accessor); + if (column?.accessor === selectedFilter?.accessor) { + return { ...column, filters: selectedFilter.filters }; + } + return column; + }); - setTableColumn(updatedColumns); - }, [props.columns]); + setTableColumn(updatedColumns); + } + }, [props.filterProps]); - React.useEffect(() => { - setDefaultTableRows(); - }, [props.data]); + React.useEffect(() => { + const updatedColumns: Array = props.columns.map((column: TableHeader) => { + return { + ...column, + isSorted: false, + canSort: column.canSort !== undefined ? column.canSort : !!props.sortProps ? true : false, + isSortedDesc: false, + filters: column.filters || [] + }; + }); - React.useEffect(() => { - doPaginate(); - }, [props.offset, props.currentpage, tableRows]); + setTableColumn(updatedColumns); + }, [props.columns]); - return ( -
- 0) || !!props.primaryActionButton)} - actionLinks={props.actionLinks} - primaryActionButton={props.primaryActionButton} - loading={tableRowsImage.length === 0} - rowsAreCollapsable={rowsAreCollapsable()} - className={props.className} - /> -
+ React.useEffect(() => { + setDefaultTableRows(); + }, [props.data]); - ); -}); + React.useEffect(() => { + doPaginate(); + }, [props.offset, props.currentpage, tableRows]); + + return ( +
+ 0) || !!props.primaryActionButton} + actionLinks={props.actionLinks} + primaryActionButton={props.primaryActionButton} + loading={!props.data} + rowsAreCollapsable={rowsAreCollapsable()} + className={props.className} + showFilterRow={showFilterRow()} + filterProps={props.filterProps} + /> +
+ ); + } +); diff --git a/src/Table/readme.md b/src/Table/readme.md index 8a28af916..a8ea2aa49 100644 --- a/src/Table/readme.md +++ b/src/Table/readme.md @@ -59,8 +59,24 @@ These are the current available properties: |-----------------|---------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------| | onAfterSorting? | `(rows: Array, sortByColumn: TableHeader) => void` | The onsort event, triggered when you click sort. This props enable sorting | | onSort? | `(rows: Array, accessor: string, sortDirection: sortDirectionTypes) => Array` | A custom sorting function that can be alternatively passed by user | +| useServerSorting | `boolean` | when this is enable, the front end sorting is disable, sorting will take place on the backend and the data should be updated | + +### FilterProps properties + +| Property | Type | Description | +|-----------------|-------|----------------------------------------------------------------------------| +| filterItems | `Array` | Array of the filter items, onsists of multiple columns| +| onAfterFilter | `(rows: Array) => void;` |A callback that returns filtered table rows | +| onRemoveFilter | `(item: { accessor: string; value: string }) => void` | A callBack that returns the filter item to be deleted | + +### FilterItem properties +| Property | Type | Description | +|-----------------|-------|----------------------------------------------------------------------------| +| accessor | `string` | The name of the column or accessor | +| filters | `Array` | The array of the filter values | + ## Footnote -1. `Column`, `TableRow`, `PrimaryActionButton`, `ActionLinkItem`, `TableHeader`, `SortProps`, `Data` and `SearchProps` interfaces/types are all importable from the component; +1. `Column`, `TableRow`, `PrimaryActionButton`, `ActionLinkItem`, `TableHeader`, `SortProps`, `Data`, `FilterItem`, `FilterProps` and `SearchProps` interfaces/types are all importable from the component; diff --git a/src/Table/table-style.scss b/src/Table/table-style.scss index 2fdc4cf5f..5d2f59414 100644 --- a/src/Table/table-style.scss +++ b/src/Table/table-style.scss @@ -1,219 +1,261 @@ @import "../colours.scss"; -@import "../../develop/styles/shared/mixins.scss"; + $icon-color: $gray-500; $icon-active-color: $blue-dark; - +$chip-height: 32px; $icon-size: 16px; - $row-height: 40px; +@mixin transition($props) { + -webkit-transition: $props; + -moz-transition: $props; + -o-transition: $props; + transition: $props; +} + table { - &.table { - thead { - > tr { - > th { - vertical-align: middle; - white-space: nowrap; - &.sortable { - cursor: pointer; - &:hover { - color: $icon-active-color; - - svg { - fill: $icon-active-color; - } - } - } - &:first-child { - border-bottom: none; - } - - .custom-control { - position: relative; - display: block; - min-height: 1.5rem; - - &.custom-checkbox { - input[type="checkbox"] { - box-sizing: border-box; - padding: 0; - } - - .custom-control-label { - position: relative; - margin-bottom: 0; - vertical-align: top; - } - - .custom-control-input { - position: absolute; - z-index: -1; - opacity: 0; - } - } - } - - .icon-holder { - height: 100%; - vertical-align: middle; - position: relative; - margin-left: 5px; - height: $icon-size; - - svg { - width: 100%; - height: 100%; - fill: $icon-color; - cursor: pointer; - - &:hover { - fill: $icon-active-color; - } - } - &.active { - svg { - fill: $icon-active-color; - } + &.table { + thead { + > tr { + > th { + vertical-align: middle; + white-space: nowrap; + &.sortable { + cursor: pointer; + &:hover { + color: $icon-active-color; + } + } + &:first-child { + border-bottom: none; + } + + .custom-control { + position: relative; + display: block; + min-height: 1.5rem; + + &.custom-checkbox { + input[type="checkbox"] { + box-sizing: border-box; + padding: 0; + } + + .custom-control-label { + position: relative; + margin-bottom: 0; + vertical-align: top; + } + + .custom-control-input { + position: absolute; + z-index: -1; + opacity: 0; + } + } + } + + .icon-holder { + height: 100%; + vertical-align: middle; + position: relative; + margin-left: 5px; + height: $icon-size; + + > svg { + width: 100%; + height: 100%; + fill: $icon-color; + cursor: pointer; + } + + &.asc { + > svg { + > path:first-child { + fill: $icon-active-color; + } + } + } + + &.desc { + > svg { + > path:last-child { + fill: $icon-active-color; + } + } + } + } + } } - } } - } - } - tbody { - tr { - td { - white-space: nowrap; - position: relative; - vertical-align: middle; - - .icon-holder { - cursor: pointer; - height: $icon-size; - display: flex; - align-items: center; - - &:hover { - svg { - fill: $icon-active-color; - } - } + tbody { + tr { + td { + white-space: nowrap; + position: relative; + vertical-align: middle; + max-width: 300px; - svg { - height: $icon-size; - fill: $icon-color; - } + &.filter-column { + > .filter-item-holder { + display: flex; + flex-wrap: wrap; + > .filter-item { + height: $chip-height; + background-color: $grey-8; + border-radius: 4px; + padding: 4px; + margin: 4px; - &.active { - svg { - fill: $icon-active-color; - } - } - } + display: flex; + align-items: center; - > .action-column { - position: relative; - display: flex; - align-items: center; + &:focus { + outline: 0; + border-color: $blue; + box-shadow: 0 0 0 0, 0 0 0 0.2rem rgba(38, 162, 230, 0.5); + } + .icon-holder { + margin-left: 12px; - button { - min-width: $icon-size; - min-height: $icon-size; - margin: 0px 20px; - } + svg { + fill: $black; + &:hover { + fill: $red; + } + } + } + } + } + } + .icon-holder { + cursor: pointer; + height: $icon-size; + display: flex; + align-items: center; - > .ellipsis-dropdown-holder { - position: relative; - display: inline-block; + &:hover { + svg { + fill: $icon-active-color; + } + } - .icon-holder { - &:hover { - background: $icon-active-color; + svg { + height: $icon-size; + fill: $icon-color; + } - svg { - fill: $white; - } - } - svg { - fill: $icon-active-color; - height: calc(#{$icon-size} * 1.5); - width: 100%; + &.active { + svg { + fill: $icon-active-color; + } + } + } + + > .action-column { + position: relative; + display: flex; + align-items: center; + + button { + min-width: $icon-size; + min-height: $icon-size; + margin: 0px 20px; + } + + > .ellipsis-dropdown-holder { + position: relative; + display: inline-block; + + .icon-holder { + &:hover { + background: $icon-active-color; + + svg { + fill: $white; + } + } + svg { + fill: $icon-active-color; + height: calc(#{$icon-size} * 1.5); + width: 100%; + } + } + + > .dropdown-content { + position: absolute; + background-color: $white; + outline: none; + right: 0; + max-height: 0; + width: 0; + overflow-y: auto; + @include transition(max-height 200ms); + + &.active { + max-height: 10000px; + width: auto; + border: 1px solid $blue-darker; + border-radius: 4px; + z-index: 1; + min-width: 160px; + box-shadow: 0px 3px 3px 0px rgba(0, 0, 0, 0.4); + + &.dropup { + top: auto; + bottom: 100%; + margin-bottom: 0.125rem; + } + } + > a { + color: inherit; + padding: 8px 16px; + text-decoration: none; + display: block; + + &:hover { + background-color: $icon-active-color; + color: $white; + } + } + } + } + } + + &.row-selections-column { + display: flex; + flex-direction: row; + align-items: center; + } } - } - - > .dropdown-content { - position: absolute; - background-color: $white; - outline: none; - right: 0; - max-height: 0; - width: 0; - overflow-y: auto; - @include transition(max-height 200ms); - - &.active { - max-height: 10000px; - width: auto; - border: 1px solid #007ac7; - border-radius: 4px; - z-index: 1; - min-width: 160px; - box-shadow: 0px 3px 3px 0px rgba(0, 0, 0, 0.4); - - &.dropup { - top: auto; - bottom: 100%; - margin-bottom: 0.125rem; - } + &.sub-row { + td { + > div { + &:first-child { + margin-left: 20px; + } + } + } } - > a { - color: inherit; - padding: 8px 16px; - text-decoration: none; - display: block; - - &:hover { - background-color: $icon-active-color; - color: $white; - } + &.sub-description-row { + td { + .description { + margin-left: 40px; + white-space: initial; + } + } } - } - } - } - - &.row-selections-column { - display: flex; - flex-direction: row; - align-items: center; - } - } - &.sub-row { - td { - > div { - &:first-child { - margin-left: 20px; - } - } - } - } - &.sub-description-row { - td { - .description { - margin-left: 40px; - white-space: initial; - } - } - } - &.description-row { - td { - .description { - margin-left: 20px; - white-space: initial; + &.description-row { + td { + .description { + margin-left: 20px; + white-space: initial; + } + } + } } - } } - } } - } }