diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5379200f3..bd85c7406 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,7 +1,8 @@ fail_fast: true repos: - repo: https://github.com/prettier/prettier - sha: '1.7.0' + # see https://github.com/prettier/prettier/issues/4637#issuecomment-396475333 + sha: '1.12.1' hooks: - id: prettier diff --git a/.storybook/config.js b/.storybook/config.js index a670bc7cf..119618a81 100644 --- a/.storybook/config.js +++ b/.storybook/config.js @@ -97,6 +97,10 @@ injectGlobal` content: "\\e942"; } + .wg-small-arrow-top:before { + content: "\\e945"; + } + .wg-search:before { content: "\\e941"; } @@ -137,6 +141,7 @@ injectGlobal` .wg-place, .wg-external-link, .wg-small-arrow-bottom, + .wg-small-arrow-top, .wg-search, .wg-close-int, .wg-purveyor, diff --git a/packages/table/README.md b/packages/table/README.md index 272519515..85a0b61d3 100644 --- a/packages/table/README.md +++ b/packages/table/README.md @@ -13,7 +13,7 @@ npm install @crave/farmblocks-table ```javascript import React, { Component } from "react"; import { render } from "react-dom"; -import {Table, Column} from "@crave/farmblocks-table"; +import { Table, Column } from "@crave/farmblocks-table"; const root = document.createElement("div"); document.body.appendChild(root); @@ -27,10 +27,10 @@ const fruits = [ render(
- - row.name} /> - row.price} /> -
+ + row.name} /> + row.price} /> +
, root ); @@ -43,34 +43,41 @@ See the stories source code for more usage examples. This package assumes that the application using it uses a font icon that have a checkmark symbol, and that the class name to include that icon is `.wg-check`. +### Required Polyfills + +This package assumes it will run on an enviroment that has support for Array.includes and Object.keys, if you need to support IE and other older browsers, make sure you have those polyfills in place. + ## Properties The Table component can be used for showing data grids using text cells in like the simple example above, but it also supports -complex rendering inside the cells using the ``customCell`` property. The idea is to describe the columns using the Column component and map the data to columns in any way you choose using functions. The ``data`` property of the Table component is the +complex rendering inside the cells using the `customCell` property. The idea is to describe the columns using the Column component and map the data to columns in any way you choose using functions. The `data` property of the Table component is the core of the whole thing, it should be an array of objects, each item representing a row of the table. ### Table -| property | type | description | -| -------- | ------ | ------------------------------------------------- | -| data | array of objects | the data to be presented in a data table, each item should represent a row | -| selectableRows | boolean | if set, will make the rows selectable by displaying checkboxes on the first column | -| selectionHeader | function ``(selectedRows, clearFunction) => React.node`` | a function that receives an array of selected rows data plus a function to clear selection; and should return a react node to be rendered as an action bar on top of the table, there is a helper component SelectionBar that can be used as the return of this function, or you can create your own... see the stories files for usage examples | -| width | string | use this to manually change the width of the table | -| rowHeight | string | use this to manually change the height of the body rows of the table, the package exports a set of named values as ``rowHeights``, you can import them and use ``rowHeights.SMALL`` to have a more compact table | -| onTitleClick | function ``(columnIndex, data) => any`` | when the option ``clickable`` is used on the Column children, that column title will be a link and will have the ``wg-arrow-down`` icon displayed, upon click such columns will trigger the function you provide in this property, it will be called with 2 arguments, the index of the clicked column from left to right and the whole table data | -| children | React nodes | the table comopnent expect Column children that describes how to interpret and render the table data on each column for all rows | +| property | type | description | +| ------------------ | ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| data | array of objects | the data to be presented in a data table, each item should represent a row | +| selectableRows | boolean | if set, will make the rows selectable by displaying checkboxes on the first column | +| selectionHeader | function `(selectedRows, clearFunction) => React.node` | a function that receives an array of selected rows data plus a function to clear selection; and should return a react node to be rendered as an action bar on top of the table, there is a helper component SelectionBar that can be used as the return of this function, or you can create your own... see the stories files for usage examples | +| width | string | use this to manually change the width of the table | +| rowHeight | string | use this to manually change the height of the body rows of the table, the package exports a set of named values as `rowHeights`, you can import them and use `rowHeights.SMALL` to have a more compact table | +| rowGroupKey | string | if you have rows that contains sub-rows as a list under a key, you can pass this property with the name of the key, to have a table with row groups generated | +| flatGroupCondition | function `(row) => boolean` | if you need to display some row groups as regular rows, use a function to describe in which conditions a rowgroup should be flattened | +| collapsed | boolean | if row groups are used, this flag will add a button column with buttons that works as expand/collapse toggle on the start of row groups. The groups will start collapsed. | +| onTitleClick | function `(columnIndex, data) => any` | when the option `clickable` is used on the Column children, that column title will be a link and will have the `wg-arrow-down` icon displayed, upon click such columns will trigger the function you provide in this property, it will be called with 2 arguments, the index of the clicked column from left to right and the whole table data | +| children | React nodes | the table comopnent expect Column children that describes how to interpret and render the table data on each column for all rows | ### Column -Columns are components that describes what data should be rendered in a column, as well as the name of the column, the two most important properties are ``title``, that is the text name for the column and ``text`` that is a function that receives a full row and should return the text value to print on the cells of the column. If the simple text values and text properties (``fontType``) are not enough, you can use functions that returns React nodes instead in the ``customTitle`` and ``customCell`` properties. +Columns are components that describes what data should be rendered in a column, as well as the name of the column, the two most important properties are `title`, that is the text name for the column and `text` that is a function that receives a full row and should return the text value to print on the cells of the column. If the simple text values and text properties (`fontType`) are not enough, you can use functions that returns React nodes instead in the `customTitle` and `customCell` properties. -Other properties: +Other properties: -- ``clickable``, a flag to make the column title clickeable -- ``width``, to manually set the column width -- ``align``, ``left`` or ``right`` for a particular column -- ``fontType``, one of the available font types in farmblocks-theme, will work only for ``text`` columns, not ``customCell`` +* `clickable`, a flag to make the column title clickeable +* `width`, to manually set the column width +* `align`, `left` or `right` for a particular column +* `fontType`, one of the available font types in farmblocks-theme, will work only for `text` columns, not `customCell` ## License diff --git a/packages/table/package.json b/packages/table/package.json index 0b6f87bd7..bf02d2625 100644 --- a/packages/table/package.json +++ b/packages/table/package.json @@ -5,10 +5,7 @@ "author": "Crave Food Systems and AUTHORS", "license": "MIT", "main": "lib/index.js", - "files": [ - "AUTHORS", - "lib" - ], + "files": ["AUTHORS", "lib"], "publishConfig": { "access": "public" }, @@ -38,11 +35,13 @@ "styled-components": "^3.0.2" }, "dependencies": { + "@crave/farmblocks-button": "^4.0.2", "@crave/farmblocks-input-checkbox": "^1.2.4", "@crave/farmblocks-link": "^2.2.1", "@crave/farmblocks-text": "^1.1.9", "@crave/farmblocks-theme": "^1.5.1", "object.values": "^1.0.4", + "polished": "^1.9.2", "react-addons-css-transition-group": "^15.6.2" }, "devDependencies": { diff --git a/packages/table/src/Table.js b/packages/table/src/Table.js index f0e056e4c..90827cea2 100644 --- a/packages/table/src/Table.js +++ b/packages/table/src/Table.js @@ -4,21 +4,60 @@ import ReactCSSTransitionGroup from "react-addons-css-transition-group"; import Text, { fontSizes, fontTypes } from "@crave/farmblocks-text"; import Link from "@crave/farmblocks-link"; import { Checkbox } from "@crave/farmblocks-input-checkbox"; +import Button from "@crave/farmblocks-button"; + import StyledTable from "./styledComponents/Table"; import { HeaderCell, BodyCell } from "./styledComponents/Cell"; import { rowHeights } from "./constants"; class Table extends React.Component { state = { - selectedRows: [] + rowsMap: {}, + selectedRows: [], + expandedRows: [] }; + updateRowsMap() { + const { rowGroupKey } = this.props; + const hasSubRows = row => + rowGroupKey && row[rowGroupKey] && row[rowGroupKey].length; + + // iterate over all rows and sub-rows to create + // a hash table of rows indexed by keys in the + // format "," + const rowsMap = this.props.data.reduce((entries, row, index) => { + if (hasSubRows(row)) { + const subRows = row[rowGroupKey].reduce( + (subRowEntries, subRow, subIndex) => { + return { ...subRowEntries, [`${index},${subIndex}`]: subRow }; + }, + {} + ); + return { ...entries, ...subRows }; + } + return { ...entries, [`${index},`]: row }; + }, {}); + this.setState({ rowsMap }); + } + + componentDidMount() { + this.updateRowsMap(); + } + + componentDidUpdate(oldProps) { + if (oldProps.data.length !== this.props.data.length) { + this.updateRowsMap(); + } + } + render() { const { data, children, width, rowHeight, + rowGroupKey, + collapsed, selectableRows, selectionHeader, borderless @@ -28,13 +67,12 @@ class Table extends React.Component { const tableProps = { width, rowHeight, - emptySelection, selectionHeaderVisible, borderless }; - const selectedData = this.props.data.filter( - (row, index) => this.state.selectedRows.indexOf(index) !== -1 - ); + const selectedData = Object.keys(this.state.rowsMap) + .filter(key => this.state.selectedRows.includes(key)) + .map(key => this.state.rowsMap[key]); const clearFunction = () => this.selectAllToggle(false, this.state.selectedRows.length); return ( @@ -59,30 +97,99 @@ class Table extends React.Component { column.props && this._renderColumnTitle(index, column.props) )} + {collapsed && this._renderColumnTitle()} - - {data.map((row, index) => { - return ( - - {selectableRows && this._renderSelectRowButton(index)} - {React.Children.map( - children, - column => - column && - column.props && - this._renderColumnCell(row, index, column.props) - )} - - ); - })} - + {data.map((row, index) => { + const isRowGroup = + row[rowGroupKey] && Array.isArray(row[rowGroupKey]); + if (isRowGroup) { + return this._renderRowGroup(row, index); + } + return ( + + {this._renderRow(row, index)} + + ); + })} ); } + _renderRow = ( + row, + index, + subIndex = "", + group = false, + flattened = false + ) => { + const { selectableRows, collapsed, children } = this.props; + const rowKey = `${index},${subIndex}`; + const selected = this.state.selectedRows.includes(rowKey); + const grouped = typeof subIndex === "number" && !flattened; + const rowProps = { selected, grouped }; + return ( + + {selectableRows && this._renderSelectRowButton(rowKey, rowProps, group)} + {React.Children.map( + children, + (column, columnIndex) => + column && + column.props && + this._renderColumnCell(row, index, { + ...column.props, + ...rowProps, + columnIndex + }) + )} + {collapsed && ( + + {group && this._renderExpandToggle(index)} + + )} + + ); + }; + + _renderRowGroup = (row, index) => { + const { rowGroupKey, flatGroupCondition } = this.props; + const { [rowGroupKey]: childRows, ...parentRow } = row; + const shouldUngroup = !!(flatGroupCondition && flatGroupCondition(row)); + const expanded = + shouldUngroup || + !this.props.collapsed || + this.state.expandedRows.includes(index); + return ( + + {!shouldUngroup && this._renderRow(parentRow, index, "", true)} + {childRows.map((row, subindex) => + this._renderRow(row, index, subindex, false, shouldUngroup) + )} + + ); + }; + + _renderExpandToggle = index => { + const icon = this.state.expandedRows.includes(index) + ? "wg-small-arrow-top" + : "wg-small-arrow-bottom"; + return ( + + ( + + count === 1 + ? "1 Order selected" + : `${count} Orders selected` + } + /> + )} + collapsed + rowGroupKey="suborders" + rowHeight={rowHeights.SMALL} + > + row.name} /> + row.totalLabel} /> +
+ + ); + } + } + return ; + }) + ); + storiesOf("Table/SelectionBar", module) .add("Default", withInfo()(() => )) .add( diff --git a/packages/table/src/Table.test.js b/packages/table/src/Table.test.js index a13bef3b4..1ad6803d8 100644 --- a/packages/table/src/Table.test.js +++ b/packages/table/src/Table.test.js @@ -10,114 +10,160 @@ const dataFixture = [{ name: "foo" }, { name: "bar" }]; describe("Table", function() { configure({ adapter: new Adapter() }); - test("Checking the Select All checkbox of a table should select all it's rows", function() { - const component = mount( - - row.name} /> -
- ); - const selectAllButton = component.find("th input"); - selectAllButton.simulate("change", { target: { checked: true } }); - const newState = component.state(); - expect(newState.selectedRows.length).toBe(dataFixture.length); + describe("Selection", () => { + test("Checking the Select All checkbox of a table should select all it's rows", function() { + const component = mount( + + row.name} /> +
+ ); + const selectAllButton = component.find("th input"); + selectAllButton.simulate("change", { target: { checked: true } }); + const newState = component.state(); + expect(newState.selectedRows.length).toBe(dataFixture.length); + }); + + test("Unchecking the Select All checkbox of a table should select all it's rows", function() { + const component = mount( + + row.name} /> +
+ ); + component.setState({ selectedRows: [0, 1] }); + expect(component.state().selectedRows.length).toBe(dataFixture.length); + + const selectAllButton = component.find("th input"); + selectAllButton.simulate("change", { target: { checked: false } }); + expect(component.state().selectedRows.length).toBe(0); + }); + + test("Selecting a row should change the selectedRows state", function() { + const component = mount( + + row.name} /> +
+ ); + const firstRowCheckbox = component.find("td input").first(); + expect(component.state().selectedRows.length).toBe(0); + + firstRowCheckbox.simulate("change", { target: { checked: true } }); + expect(component.state().selectedRows.length).toBe(1); + }); + + test("Deselecting a row should change the selectedRows state", function() { + const component = mount( + + row.name} /> +
+ ); + component.setState({ selectedRows: ["0,"] }); + const firstRowCheckbox = component.find("td input").first(); + expect(component.state().selectedRows.length).toBe(1); + + firstRowCheckbox.simulate("change", { target: { checked: false } }); + expect(component.state().selectedRows.length).toBe(0); + }); + + test("Table with selection header bar should clear all selected row if the passed clearFunction is called", function() { + const selectionHeaderRenderer = (data, clearFunction) => ( + + + + + + +
- $ 9,999.99 + Farm E +
+ + +
+ $ 10
+
- Coconut + Farm F
+
+ $ 5 +
+ + + +
- $ 2.30 + Farm G +
+ + +
+ $ 5
+ diff --git a/packages/table/src/styledComponents/Cell.js b/packages/table/src/styledComponents/Cell.js index 2aead3b96..399b6d5a6 100644 --- a/packages/table/src/styledComponents/Cell.js +++ b/packages/table/src/styledComponents/Cell.js @@ -1,8 +1,18 @@ import styled from "styled-components"; import { colors } from "@crave/farmblocks-theme"; +import { transparentize } from "polished"; const textAlign = props => props.align || "left"; +const selectedBg = transparentize(0.94, colors.INDIGO_MILK_CAP); + +const cellBg = props => { + if (props.selected) { + return `${selectedBg} !important`; + } + return props.grouped ? colors.SUGAR : "white"; +}; + export const HeaderCell = styled.th` text-align: ${textAlign}; background-color: ${colors.SUGAR}; @@ -20,5 +30,5 @@ export const HeaderCell = styled.th` `; export const BodyCell = styled.td` text-align: ${textAlign}; - background-color: ${props => (props.selected ? "white" : colors.SUGAR)}; + background-color: ${cellBg}; `; diff --git a/packages/table/src/styledComponents/Table.js b/packages/table/src/styledComponents/Table.js index 58defd436..6c09b262a 100644 --- a/packages/table/src/styledComponents/Table.js +++ b/packages/table/src/styledComponents/Table.js @@ -8,7 +8,11 @@ const Table = styled.table` border: ${props => !props.borderless && border}; padding: 8px 16px; - ${props => props.selectionHeaderVisible && css`border-top: none;`}; + ${props => + props.selectionHeaderVisible && + css` + border-top: none; + `}; .cell { box-sizing: border-box; height: ${props => props.rowHeight}; @@ -19,6 +23,20 @@ const Table = styled.table` padding-right: 16px; } + /* corner icon for grouped rows */ + &.corner-icon:before { + content: ""; + display: block; + float: left; + box-sizing: border-box; + width: 8px; + height: 8px; + margin-top: 8px; + margin-right: 16px; + border-left: 2px solid ${colors.INDIGO_MILK_CAP}; + border-bottom: 2px solid ${colors.INDIGO_MILK_CAP}; + } + /* @HACK checkbox component already have a left padding, so we use negative margin to keep only the table padding */ & .checkbox { @@ -27,9 +45,14 @@ const Table = styled.table` } } - .body .cell { - ${props => - props.emptySelection && css`background-color: white !important;`}; + tbody.collapsed tr.grouped { + display: none; + } + + tbody tr:hover { + & .cell { + background: ${colors.DEMERARA_SUGAR}; + } } `; diff --git a/packages/theme/src/colors.js b/packages/theme/src/colors.js index 19fe6377b..086fdc7e0 100644 --- a/packages/theme/src/colors.js +++ b/packages/theme/src/colors.js @@ -2,6 +2,7 @@ module.exports = { CARBON: "#2f313a", OYSTER: "#59636f", SUGAR: "#f6f8f9", + DEMERARA_SUGAR: "#e9ebec", RED_ORANGE: "#ff4411", INDIGO_MILK_CAP: "#3498db", BLUE_CORN: "#2980b9", diff --git a/yarn.lock b/yarn.lock index 62d153f47..11162d365 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7388,6 +7388,10 @@ pngjs@^3.0.0, pngjs@^3.3.3: version "3.3.3" resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-3.3.3.tgz#85173703bde3edac8998757b96e5821d0966a21b" +polished@^1.9.2: + version "1.9.2" + resolved "https://registry.yarnpkg.com/polished/-/polished-1.9.2.tgz#d705cac66f3a3ed1bd38aad863e2c1e269baf6b6" + posix-character-classes@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab"