From a77d5b97fc61f2afd245e8d0bf492fb319af0cd3 Mon Sep 17 00:00:00 2001 From: Gary Thomas Date: Fri, 10 Aug 2018 08:21:41 -0700 Subject: [PATCH 01/10] gt - WIP UncontrolledTable - Name is temp, maybe combine with Table --- package-lock.json | 15 ++ package.json | 3 + src/components/UncontrolledTable.js | 138 +++++++++++++ src/index.js | 2 + stories/SortableTable.js | 294 ---------------------------- stories/Table.js | 169 +++++++++++++--- stories/index.js | 1 - 7 files changed, 301 insertions(+), 321 deletions(-) create mode 100644 src/components/UncontrolledTable.js delete mode 100644 stories/SortableTable.js diff --git a/package-lock.json b/package-lock.json index fc37b7e0b..85c04ee62 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7704,6 +7704,11 @@ "resolved": "https://registry.npmjs.org/lodash.flow/-/lodash.flow-3.5.0.tgz", "integrity": "sha1-h79AKSuM+D5OjOGjrkIJ4gBxZ1o=" }, + "lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8=" + }, "lodash.isarguments": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", @@ -7760,6 +7765,11 @@ "resolved": "https://registry.npmjs.org/lodash.noop/-/lodash.noop-3.0.1.tgz", "integrity": "sha1-OBiPTWUKOkdCWEObluxFsyYXEzw=" }, + "lodash.orderby": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.orderby/-/lodash.orderby-4.6.0.tgz", + "integrity": "sha1-5pfwTOXXhSL1TZM4syuBozk+TrM=" + }, "lodash.pick": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/lodash.pick/-/lodash.pick-4.4.0.tgz", @@ -7801,6 +7811,11 @@ "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=", "dev": true }, + "lodash.without": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.without/-/lodash.without-4.4.0.tgz", + "integrity": "sha1-PNRXSgC2e643OpS3SHcmQFB7eqw=" + }, "lolex": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/lolex/-/lolex-2.7.1.tgz", diff --git a/package.json b/package.json index c9d8d6e84..0344f04f1 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,10 @@ "enzyme-adapter-react-16": "^1.1.1", "fecha": "^2.3.0", "lodash.flow": "^3.5.0", + "lodash.includes": "^4.3.0", "lodash.noop": "^3.0.1", + "lodash.orderby": "^4.6.0", + "lodash.without": "^4.4.0", "prop-types": "^15.5.10", "react-fontawesome": "^1.4.0", "react-onclickoutside": "^6.7.1", diff --git a/src/components/UncontrolledTable.js b/src/components/UncontrolledTable.js new file mode 100644 index 000000000..3966564b7 --- /dev/null +++ b/src/components/UncontrolledTable.js @@ -0,0 +1,138 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import classnames from 'classnames'; +import includes from 'lodash.includes'; +import orderBy from 'lodash.orderby'; +import without from 'lodash.without'; +import SortableTable from './SortableTable'; + +export default class UncontrolledTable extends React.Component { + static propTypes = { + ...SortableTable.propTypes, + sort: PropTypes.shape({ + column: PropTypes.string.isRequired, + ascending: PropTypes.bool.isRequired + }) + } + + static defaultProps = { + ...SortableTable.defaultProps, + sort: { + ascending: true // TODO or: `sort={{ last, 'asc' }}` ? + } + } + + state = { + sort: this.props.sort, + selected: [] + } + + // TODO pagination + + sortedData = (rows, column, ascending) => orderBy( + rows, + [column], + [ascending ? 'asc' : 'desc'] + ); + + sortBy = (column, ascending) => { + if (this.state.sort.column === column) { + this.setState({ + sort: { + column, + ascending + } + }); + } else { + this.setState({ + sort: { + column, + ascending: true + } + }); + } + }; + + get someSelected() { + return this.state.selected.length > 0; + } + + get allSelected() { + return this.props.rows.length && (this.state.selected.length === this.props.rows.length); + } + + selected(value) { + return includes(this.state.selected, value); + } + + toggleSelection = (value) => { + const selected = this.state.selected; + const newSelection = includes(selected, value) ? + without(selected, value) : + [...selected, value]; + + this.setState({ selected: newSelection }, + this.props.onSelect(newSelection) + ); + }; + + toggleAll = () => { + const newSelection = this.allSelected ? [] : this.props.rows; + + this.setState({ selected: newSelection }, + this.props.onSelect(newSelection) + ); + }; + + componentWillReceiveProps(nextProps) { + // Clear selection if rows or selectable change + if (nextProps.rows !== this.props.rows || + nextProps.selectable !== this.props.selectable) { + this.setState({ selected: [] }); + } + } + + render() { + const { sort } = this.state; + const { ascending, column } = sort; + const { columns, rowClassName, rows, selectable, onSelect, ...props } = this.props; + const cols = columns.map((col) => { + return { + active: column === col.key, + ascending, + onSort: asc => this.sortBy(col.key, asc), + ...col + }; + }); + if (selectable) { + cols.unshift({ + header: ( + this.toggleAll()} + /> + ), + key: 'select', + cell: row => ( + this.toggleSelection(row)} + /> + ), + width: '1%' + }); + } + return ( + classnames({ 'table-info': this.selected(row) }, rowClassName(row))} + /> + ); + } +} diff --git a/src/index.js b/src/index.js index 3d3a8d863..87bef8a72 100755 --- a/src/index.js +++ b/src/index.js @@ -125,6 +125,7 @@ import SummaryBoxItem from './components/SummaryBoxItem.js'; import Table from './components/Table.js'; import TimeInput from './components/TimeInput.js'; import Tooltip from './components/Tooltip.js'; +import UncontrolledTable from './components/UncontrolledTable.js'; import ValidatedFormGroup from './components/ValidatedFormGroup.js'; import Waiting from './components/Waiting.js'; @@ -259,6 +260,7 @@ export { SummaryBoxItem, TimeInput, Tooltip, + UncontrolledTable, ValidatedFormGroup, Waiting, }; diff --git a/stories/SortableTable.js b/stories/SortableTable.js deleted file mode 100644 index 1362626c2..000000000 --- a/stories/SortableTable.js +++ /dev/null @@ -1,294 +0,0 @@ -import React from 'react'; -import { storiesOf } from '@storybook/react'; -import { boolean, select } from '@storybook/addon-knobs'; -import { action } from '@storybook/addon-actions'; -import fecha from 'fecha'; -import { orderBy } from 'lodash'; -import { Button, Card, SortableTable } from '../src'; - -const DATA = [ - { key: '111', expanded: false, first: 'Nicole', last: 'Grant', email: 'nicole.grant@example.com', dob: new Date(1968, 6, 15) }, - { key: '222', expanded: false, first: 'Alberto', last: 'Kennedy', email: 'alberto.kennedy@example.com', dob: new Date(1972, 7, 17) }, - { key: '333', expanded: false, first: 'Arron', last: 'Douglas', email: 'arron.douglas@example.com', dob: new Date(1982, 4, 1) }, - { key: '444', expanded: false, first: 'Reginald', last: 'Rhodes', email: 'reginald.rhodes@example.com', dob: new Date(1968, 8, 14) }, - { key: '555', expanded: false, first: 'Jimmy', last: 'Mendoza', email: 'jimmy.mendoza@example.com', dob: new Date(1964, 1, 1) }, - { key: '666', expanded: false, first: 'Georgia', last: 'Montgomery', email: 'georgia.montgomery@example.com', dob: new Date(1960, 6, 4) }, - { key: '777', expanded: true, first: 'Serenity', last: 'Thomas', email: 'serenity.thomas@example.com', dob: new Date(1973, 0, 11) }, - { key: '888', expanded: false, first: 'Tonya', last: 'Elliott', email: 'tonya.elliott@example.com', dob: new Date(1954, 7, 17) }, - { key: '999', expanded: false, first: 'Maxine', last: 'Turner', email: 'maxine.turner@example.com', dob: new Date(1961, 8, 19) }, - { key: '000', expanded: false, first: 'Reginald', last: 'Rice', email: 'reginald.rice@example.com', dob: new Date(1984, 4, 15) } -]; - -class StatefulExample extends React.Component { - static displayName = 'SortableTable'; - state = { - column: 'last', - ascending: true - } - - sortedData = (column, ascending) => orderBy( - DATA, - [column], - [ascending ? 'asc' : 'desc'] - ); - - sortBy = (column, ascending) => { - if (this.state.column === column) { - this.setState({ - ascending - }); - } else { - this.setState({ - column, - ascending: true - }); - } - }; - - rowOnClick = (row) => { - alert(`clicked ${row.key}`); - }; - - render() { - const { ascending, column } = this.state; - const { bordered, hover, size, striped } = this.props; - - return ( - row.first, - onSort: asc => this.sortBy('first', asc), - width: '20%' - }, - { - active: column === 'last', - ascending, - header: 'Last', - key: 'last', - cell: row => row.last, - onSort: asc => this.sortBy('last', asc), - width: '30%' - }, - { - active: column === 'dob', - ascending, - header: 'DOB', - key: 'dob', - cell: row => fecha.format(row.dob, 'MM/DD/YYYY'), - onSort: asc => this.sortBy('dob', asc), - width: '15%' - }, - { - active: column === 'email', - ascending, - header: Email, - key: 'email', - cell: row => {row.email}, - onSort: asc => this.sortBy('email', asc), - width: '35%' - } - ]} - rows={this.sortedData(column, ascending)} - rowExpanded={row => row.expanded &&
} - rowOnClick={this.rowOnClick} - /> - ); - } -} - -storiesOf('SortableTable', module) - .addWithInfo('Live example', () => { - const column = select('active', ['first', 'last', 'dob', 'email'], 'last'); - const ascending = boolean('ascending', true); - return ( -
-

- Note: This is an uncontrolled example, will not sort on click. See 'Stateful Example' story. -

- row.first, - onSort: action('onSort', 'First') - }, - { - active: column === 'last', - ascending, - header: 'Last', - key: 'last', - cell: row => row.last, - onSort: action('onSort', 'Last') - }, - { - active: column === 'dob', - ascending, - header: 'DOB', - key: 'dob', - cell: row => fecha.format(row.dob, 'MM/DD/YYYY'), - onSort: action('onSort', 'DOB') - }, - { - active: column === 'email', - ascending, - header: Email, - key: 'email', - cell: row => {row.email}, - onSort: action('onSort', 'Email') - } - ]} - rows={DATA} - /> -
- ); - }) - .add('Stateful example', () => ( -
- - -

Story Source

-
{`
-class StatefulExample extends React.Component {
-  static displayName = 'SortableTable';
-  state = {
-    column: 'last',
-    ascending: true
-  }
-
-  sortedData = (column, ascending) => orderBy(
-    DATA,
-    [column],
-    [ascending ? 'asc' : 'desc']
-  );
-
-  sortBy = (column, ascending) => {
-    if (this.state.column === column) {
-      this.setState({
-        ascending
-      });
-    } else {
-      this.setState({
-        column,
-        ascending: true
-      });
-    }
-  };
-
-  render() {
-    const { ascending, column } = this.state;
-    const { bordered, hover, size, striped } = this.props;
-
-    return (
-       row.first,
-            onSort: asc => this.sortBy('first', asc),
-            width: '20%'
-          },
-          {
-            active: column === 'last',
-            ascending,
-            header: 'Last',
-            key: 'last',
-            cell: row => row.last,
-            onSort: asc=> this.sortBy('last', asc),
-            width: '30%'
-          },
-          {
-            active: column === 'dob',
-            ascending,
-            header: 'DOB',
-            key: 'dob',
-            cell: row => fecha.format(row.dob, 'MM/DD/YYYY'),
-            onSort: asc => this.sortBy('dob', asc),
-            width: '15%'
-          },
-          {
-            active: column === 'email',
-            ascending,
-            header: Email,
-            key: 'email',
-            cell: row => {row.email},
-            onSort: asc => this.sortBy('email', asc),
-            width: '35%'
-          }
-        ]}
-        rows={this.sortedData(column, ascending)}
-        rowExpanded={row => row.expanded && 
} - /> - ); - } -} - `} -
-
-
- )) - .addWithInfo('Align column', () => ( -
-

- Note: This is an uncontrolled example, will not sort on click. See 'Stateful Example' story. -

- row.first, - }, - { - align: 'left', - header: 'Left Align', - key: 'last', - cell: row => row.last, - }, - { - align: 'center', - header: 'Center Align', - key: 'dob', - cell: row => fecha.format(row.dob, 'MM/DD/YYYY'), - }, - { - align: 'right', - header: 'Right Align', - key: 'email', - cell: row => {row.email}, - } - ]} - rows={DATA} - /> -
- )); - diff --git a/stories/Table.js b/stories/Table.js index 3e18ea0d4..fa92ff4e1 100644 --- a/stories/Table.js +++ b/stories/Table.js @@ -1,26 +1,20 @@ import React from 'react'; +import fecha from 'fecha'; import { storiesOf } from '@storybook/react'; - -import { Table } from '../src'; +import { action } from '@storybook/addon-actions'; import { text, boolean, number, object, select } from '@storybook/addon-knobs'; +import { Table, SortableTable, UncontrolledTable } from '../src'; const DATA = [ - { - name: 'Compson, Ms. Quentin', - company: 'Jefferson County', - phone: '(662) 555-1212', - email: 'qcbaby@faulkner.com' - }, { - name: 'Trump, Paul', - company: 'Appfolio Inc.', - phone: '(805) 555-1213', - email: 'paul.trump@bogus.com' - }, { - name: 'Walker, Jon', - company: 'CTO Appfolio Inc.', - phone: '(805) 555-1212', - email: 'jon@walker.com' - } + { key: '111', expanded: false, first: 'Nicole', last: 'Grant', email: 'nicole.grant@example.com', dob: new Date(1968, 6, 15) }, + { key: '222', expanded: false, first: 'Alberto', last: 'Kennedy', email: 'alberto.kennedy@example.com', dob: new Date(1972, 7, 17) }, + { key: '333', expanded: false, first: 'Arron', last: 'Douglas', email: 'arron.douglas@example.com', dob: new Date(1982, 4, 1) }, + { key: '444', expanded: false, first: 'Reginald', last: 'Rhodes', email: 'reginald.rhodes@example.com', dob: new Date(1968, 8, 14) }, + { key: '555', expanded: false, first: 'Jimmy', last: 'Mendoza', email: 'jimmy.mendoza@example.com', dob: new Date(1964, 1, 1) }, + { key: '666', expanded: false, first: 'Georgia', last: 'Montgomery', email: 'georgia.montgomery@example.com', dob: new Date(1960, 6, 4) }, + { key: '777', expanded: true, first: 'Serenity', last: 'Thomas', email: 'serenity.thomas@example.com', dob: new Date(1973, 0, 11) }, + { key: '888', expanded: false, first: 'Tonya', last: 'Elliott', email: 'tonya.elliott@example.com', dob: new Date(1954, 7, 17) }, + { key: '999', expanded: false, first: 'Maxine', last: 'Turner', email: 'maxine.turner@example.com', dob: new Date(1961, 8, 19) } ]; storiesOf('Table', module) @@ -33,9 +27,9 @@ storiesOf('Table', module) > - Name - Company - Phone + First + Last + DOB Email @@ -43,13 +37,136 @@ storiesOf('Table', module) {DATA.map(row => ( - {row.name} - {row.company} - {row.phone} + {row.first} + {row.last} + {fecha.format(row.dob, 'MM/DD/YYYY')} {row.email} ))} - ) -); + )) + .addWithInfo('SortableTable', () => { + const column = select('active', ['first', 'last', 'dob', 'email'], 'last'); + const ascending = boolean('ascending', true); + return ( +
+

+ Note: This is an uncontrolled example, will not sort on click. See 'UncontrolledTable' story. +

+ row.first, + onSort: action('onSort', 'First') + }, + { + active: column === 'last', + ascending, + header: 'Last', + key: 'last', + cell: row => row.last, + onSort: action('onSort', 'Last') + }, + { + active: column === 'dob', + ascending, + header: 'DOB', + key: 'dob', + cell: row => fecha.format(row.dob, 'MM/DD/YYYY'), + onSort: action('onSort', 'DOB') + }, + { + active: column === 'email', + ascending, + header: Email, + key: 'email', + cell: row => {row.email}, + onSort: action('onSort', 'Email') + } + ]} + rows={DATA} + /> +
+ ); + }) + .addWithInfo('UncontrolledTable', () => ( +
+ row.first, + width: '20%' + }, + { + header: 'Last', + key: 'last', + cell: row => row.last, + width: '30%' + }, + { + header: 'DOB', + key: 'dob', + cell: row => fecha.format(row.dob, 'MM/DD/YYYY'), + width: '15%' + }, + { + header: 'Email', + key: 'email', + cell: row => {row.email}, + width: '35%' + } + ]} + rows={DATA} + sort={{ column: 'last', ascending: true }} + selectable={boolean('selectable', false)} + onSelect={action('onSelect')} + /> +
+ )) + .addWithInfo('Align column', () => ( +
+

+ Note: This is an uncontrolled example, will not sort on click. See 'UncontrolledTable' story. +

+ row.first, + }, + { + align: 'left', + header: 'Left Align', + key: 'last', + cell: row => row.last, + }, + { + align: 'center', + header: 'Center Align', + key: 'dob', + cell: row => fecha.format(row.dob, 'MM/DD/YYYY'), + }, + { + align: 'right', + header: 'Right Align', + key: 'email', + cell: row => {row.email}, + } + ]} + rows={DATA} + /> +
+ )); + diff --git a/stories/index.js b/stories/index.js index 6ca6b15c6..d7dd1663c 100644 --- a/stories/index.js +++ b/stories/index.js @@ -35,7 +35,6 @@ import './Paginator'; import './Popover'; import './Progress'; import './Select'; -import './SortableTable'; import './Spinner'; import './Steps'; import './SummaryBox'; From a4f9bca3cbf3b85081ed5c7ac28d3e3f74091bbe Mon Sep 17 00:00:00 2001 From: Gary Thomas Date: Fri, 10 Aug 2018 18:08:25 -0700 Subject: [PATCH 02/10] gt - Expandable row support --- src/components/UncontrolledTable.js | 43 +++++++++++++++++++++++++++-- stories/Table.js | 2 ++ 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/src/components/UncontrolledTable.js b/src/components/UncontrolledTable.js index 3966564b7..1c6d28c3b 100644 --- a/src/components/UncontrolledTable.js +++ b/src/components/UncontrolledTable.js @@ -4,6 +4,8 @@ import classnames from 'classnames'; import includes from 'lodash.includes'; import orderBy from 'lodash.orderby'; import without from 'lodash.without'; +import Button from './Button'; +import Icon from './Icon'; import SortableTable from './SortableTable'; export default class UncontrolledTable extends React.Component { @@ -24,7 +26,8 @@ export default class UncontrolledTable extends React.Component { state = { sort: this.props.sort, - selected: [] + expanded: [], + selected: [], } // TODO pagination @@ -84,6 +87,19 @@ export default class UncontrolledTable extends React.Component { ); }; + expanded(value) { + return includes(this.state.expanded, value); + } + + toggleExpanded = (value) => { + const expanded = this.state.expanded; + const newExpanded = includes(expanded, value) ? + without(expanded, value) : + [...expanded, value]; + + this.setState({ expanded: newExpanded }); + }; + componentWillReceiveProps(nextProps) { // Clear selection if rows or selectable change if (nextProps.rows !== this.props.rows || @@ -95,7 +111,7 @@ export default class UncontrolledTable extends React.Component { render() { const { sort } = this.state; const { ascending, column } = sort; - const { columns, rowClassName, rows, selectable, onSelect, ...props } = this.props; + const { columns, expandable, rowClassName, rowExpanded, rows, selectable, onSelect, ...props } = this.props; const cols = columns.map((col) => { return { active: column === col.key, @@ -104,8 +120,11 @@ export default class UncontrolledTable extends React.Component { ...col }; }); + if (selectable) { cols.unshift({ + align: 'center', + key: 'select', header: ( this.toggleAll()} /> ), - key: 'select', cell: row => ( ( + + ), + width: '1%' + }); + } + return ( classnames({ 'table-info': this.selected(row) }, rowClassName(row))} + rowExpanded={row => this.expanded(row) && rowExpanded(row)} /> ); } diff --git a/stories/Table.js b/stories/Table.js index fa92ff4e1..f56f34d36 100644 --- a/stories/Table.js +++ b/stories/Table.js @@ -128,7 +128,9 @@ storiesOf('Table', module) } ]} rows={DATA} + rowExpanded={row =>
{row.first} {row.last}
} sort={{ column: 'last', ascending: true }} + expandable={boolean('expandable', false)} selectable={boolean('selectable', false)} onSelect={action('onSelect')} /> From 2cb01975f582f405df85d0bac4f7769aa0c3affb Mon Sep 17 00:00:00 2001 From: Gary Thomas Date: Sat, 11 Aug 2018 20:30:55 -0700 Subject: [PATCH 03/10] gt - Add pagination --- src/components/UncontrolledTable.js | 54 ++++++++++++++++++++++------- stories/Table.js | 7 ++-- 2 files changed, 46 insertions(+), 15 deletions(-) diff --git a/src/components/UncontrolledTable.js b/src/components/UncontrolledTable.js index 1c6d28c3b..86eab90e6 100644 --- a/src/components/UncontrolledTable.js +++ b/src/components/UncontrolledTable.js @@ -6,11 +6,13 @@ import orderBy from 'lodash.orderby'; import without from 'lodash.without'; import Button from './Button'; import Icon from './Icon'; +import Paginator from './Paginator'; import SortableTable from './SortableTable'; export default class UncontrolledTable extends React.Component { static propTypes = { ...SortableTable.propTypes, + pageSize: PropTypes.number, sort: PropTypes.shape({ column: PropTypes.string.isRequired, ascending: PropTypes.bool.isRequired @@ -19,6 +21,7 @@ export default class UncontrolledTable extends React.Component { static defaultProps = { ...SortableTable.defaultProps, + pageSize: 10, sort: { ascending: true // TODO or: `sort={{ last, 'asc' }}` ? } @@ -28,6 +31,7 @@ export default class UncontrolledTable extends React.Component { sort: this.props.sort, expanded: [], selected: [], + page: 0 } // TODO pagination @@ -100,26 +104,34 @@ export default class UncontrolledTable extends React.Component { this.setState({ expanded: newExpanded }); }; + setPage = (page) => { + this.setState({ page }); + } + componentWillReceiveProps(nextProps) { // Clear selection if rows or selectable change if (nextProps.rows !== this.props.rows || nextProps.selectable !== this.props.selectable) { this.setState({ selected: [] }); } + if (nextProps.rows !== this.props.rows || + nextProps.expandable !== this.props.expandable) { + this.setState({ expanded: [] }); + } } render() { - const { sort } = this.state; + const { page, sort } = this.state; const { ascending, column } = sort; - const { columns, expandable, rowClassName, rowExpanded, rows, selectable, onSelect, ...props } = this.props; - const cols = columns.map((col) => { - return { + const { columns, expandable, pageSize, paginated, rowClassName, rowExpanded, rows, selectable, onSelect, ...props } = this.props; + const cols = columns.map(col => (col.sortable !== false) ? + { active: column === col.key, ascending, onSort: asc => this.sortBy(col.key, asc), ...col - }; - }); + } : col + ); if (selectable) { cols.unshift({ @@ -162,14 +174,30 @@ export default class UncontrolledTable extends React.Component { }); } + const start = page * pageSize; + const end = start + pageSize; + const sortedRows = this.sortedData(rows, column, ascending); + const visibleRows = paginated ? sortedRows.slice(start, end) : sortedRows; + return ( - classnames({ 'table-info': this.selected(row) }, rowClassName(row))} - rowExpanded={row => this.expanded(row) && rowExpanded(row)} - /> +
+ classnames({ 'table-info': this.selected(row) }, rowClassName(row))} + rowExpanded={row => expandable && this.expanded(row) && rowExpanded(row)} + /> + {paginated && [ +
, + this.setPage(pg - 1)} + perPage={pageSize} + totalItems={rows.length} + /> + ]} +
); } } diff --git a/stories/Table.js b/stories/Table.js index f56f34d36..2b2c603eb 100644 --- a/stories/Table.js +++ b/stories/Table.js @@ -14,12 +14,13 @@ const DATA = [ { key: '666', expanded: false, first: 'Georgia', last: 'Montgomery', email: 'georgia.montgomery@example.com', dob: new Date(1960, 6, 4) }, { key: '777', expanded: true, first: 'Serenity', last: 'Thomas', email: 'serenity.thomas@example.com', dob: new Date(1973, 0, 11) }, { key: '888', expanded: false, first: 'Tonya', last: 'Elliott', email: 'tonya.elliott@example.com', dob: new Date(1954, 7, 17) }, - { key: '999', expanded: false, first: 'Maxine', last: 'Turner', email: 'maxine.turner@example.com', dob: new Date(1961, 8, 19) } + { key: '999', expanded: false, first: 'Maxine', last: 'Turner', email: 'maxine.turner@example.com', dob: new Date(1961, 8, 19) }, + { key: '000', expanded: false, first: 'Max', last: 'Headroom', email: 'max.headroom@example.com', dob: new Date(1984, 6, 1) } ]; storiesOf('Table', module) .addWithInfo('Live example', () => ( - From 225c2f3da4adcc593a804ce8d32d64fee4b377a7 Mon Sep 17 00:00:00 2001 From: Gary Thomas Date: Thu, 16 Aug 2018 16:30:14 -0700 Subject: [PATCH 04/10] gt - Update props and TODOs --- src/components/UncontrolledTable.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/components/UncontrolledTable.js b/src/components/UncontrolledTable.js index 86eab90e6..2fea95e73 100644 --- a/src/components/UncontrolledTable.js +++ b/src/components/UncontrolledTable.js @@ -14,8 +14,8 @@ export default class UncontrolledTable extends React.Component { ...SortableTable.propTypes, pageSize: PropTypes.number, sort: PropTypes.shape({ - column: PropTypes.string.isRequired, - ascending: PropTypes.bool.isRequired + column: PropTypes.string, + ascending: PropTypes.bool }) } @@ -23,7 +23,7 @@ export default class UncontrolledTable extends React.Component { ...SortableTable.defaultProps, pageSize: 10, sort: { - ascending: true // TODO or: `sort={{ last, 'asc' }}` ? + ascending: true } } @@ -34,8 +34,6 @@ export default class UncontrolledTable extends React.Component { page: 0 } - // TODO pagination - sortedData = (rows, column, ascending) => orderBy( rows, [column], From d8056f7116daa914e3926a2a1fa0a17b6892e7ed Mon Sep 17 00:00:00 2001 From: Gary Thomas Date: Thu, 16 Aug 2018 19:59:44 -0700 Subject: [PATCH 05/10] gt - Add tests --- test/components/UncontrolledTable.spec.js | 263 ++++++++++++++++++++++ 1 file changed, 263 insertions(+) create mode 100644 test/components/UncontrolledTable.spec.js diff --git a/test/components/UncontrolledTable.spec.js b/test/components/UncontrolledTable.spec.js new file mode 100644 index 000000000..aed2b8ce0 --- /dev/null +++ b/test/components/UncontrolledTable.spec.js @@ -0,0 +1,263 @@ +import React from 'react'; +import assert from 'assert'; +import sinon from 'sinon'; +import { mount } from 'enzyme'; + +import { UncontrolledTable } from '../../src'; + +describe('', () => { + it('should render correctly', () => { + const wrapper = mount(); + assert(wrapper); + }); + + it('should accept all normal Table props', () => { + const wrapper = mount(); + const table = wrapper.find('table'); + + assert(wrapper.find('.table-responsive').exists(), 'responsive missing'); + assert(table.hasClass('table-bordered'), 'bordered missing'); + assert(table.hasClass('table-striped'), 'striped missing'); + assert(table.hasClass('table-dark'), 'dark missing'); + assert(table.hasClass('table-hover'), 'hover missing'); + }); + + it('should render all columns', () => { + const columns = [ + { header: 'Alpha' }, + { header: 'Bravo' }, + { header: 'Charlie' }, + { header: 'Delta' } + ]; + const wrapper = mount(); + const headers = wrapper.find('th'); + assert.equal(headers.length, columns.length); + headers.forEach((th, i) => assert.equal(th.text(), columns[i].header)); + }); + + it('should render all rows', () => { + const columns = [{ header: 'Name', cell: row => row }]; + const rows = ['Alpha', 'Bravo', 'Charlie', 'Delta']; + const wrapper = mount(); + const cells = wrapper.find('td'); + assert.equal(cells.length, rows.length); + cells.forEach((td, i) => assert.equal(td.text(), rows[i])); + }); + + it('should render header components', () => { + const classNames = ['alpha', 'bravo', 'charlie', 'delta']; + const columns = classNames.map(name => ({ + header: {name} + })); + const wrapper = mount(); + const headers = wrapper.find('span'); + headers.forEach((th, i) => assert(th.hasClass(classNames[i]))); + }); + + it('should render cell components', () => { + const classNames = ['alpha', 'bravo', 'charlie', 'delta']; + const columns = classNames.map(name => ({ + header: name, + cell: row => {row} + })); + const rows = ['Alpha', 'Bravo', 'Charlie', 'Delta']; + const wrapper = mount(); + const trs = wrapper.find('tr'); + assert.equal(trs.length, rows.length + 1); // +1 includes thead + + classNames.forEach(name => { + const cells = wrapper.find(`.${name}`); + assert.equal(cells.length, rows.length, 'Column cell not rendered for each row'); + }); + }); + + it('should not render tfoot if no footers specified', () => { + const classNames = ['alpha', 'bravo', 'charlie', 'delta']; + const columns = classNames.map(name => ({ + header: name + })); + const wrapper = mount(); + + const footer = wrapper.find('tfoot'); + assert.equal(footer.exists(), false); + }); + + it('should render footer components', () => { + const classNames = ['alpha', 'bravo', 'charlie', 'delta']; + const columns = classNames.map(name => ({ + header: name, + footer: {name} + })); + const wrapper = mount(); + + const footer = wrapper.find('tfoot'); + assert(footer.exists()); + + const footers = wrapper.find('span'); + assert.equal(footers.length, classNames.length); + footers.forEach((th, i) => assert(th.hasClass(classNames[i]))); + }); + + it('should render sorting controls when sortable present', () => { + const columns = [ + { header: 'Alpha' }, + { header: 'Bravo' }, + { header: 'Charlie' }, + { header: 'Delta', sortable: false } + ]; + const wrapper = mount(); + const sortIcons = wrapper.find('Icon'); + assert.equal(sortIcons.length, 3); + }); + + it('should render correct sort icons when specified', () => { + const columns = [ + { key: 'alpha', header: 'Alpha' }, + { key: 'bravo', header: 'Bravo' }, + { key: 'charlie', header: 'Charlie' }, + { key: 'delta', header: 'Delta', sortable: false } + ]; + const wrapper = mount(); + + const activeColumnIcon = wrapper.find('Icon[name="caret-up"]'); + assert.equal(activeColumnIcon.length, 1); + + const activeDescColumnIcon = wrapper.find('Icon[name="caret-down"]'); + assert.equal(activeDescColumnIcon.length, 0); + + const inactiveColumnIcons = wrapper.find('Icon[name="sort"]'); + assert.equal(inactiveColumnIcons.length, 2); + }); + + it('should not render colgroup if no widths specified', () => { + const columns = [ + { header: 'Alpha' }, + { header: 'Bravo' }, + { header: 'Charlie' }, + { header: 'Delta' } + ]; + const wrapper = mount(); + + const colgroup = wrapper.find('colgroup'); + assert.equal(colgroup.exists(), false); + }); + + it('should render column widths if specified', () => { + const columns = [ + { header: 'Alpha', width: '20%' }, + { header: 'Bravo', width: '30%' }, + { header: 'Charlie', width: '15%' }, + { header: 'Delta', width: '35%' } + ]; + const wrapper = mount(); + + const colgroup = wrapper.find('colgroup'); + assert(colgroup.exists()); + + const col = wrapper.find('col'); + columns.forEach((column, i) => assert.equal(col.get(i).props.style.width, column.width)); + }); + + it('should render row className when specified', () => { + const columns = [{ header: 'Name', cell: row => row }]; + const rows = ['Alpha', 'Bravo', 'Charlie', 'Delta']; + const wrapper = mount( + row} + /> + ); + const trs = wrapper.find('tbody tr'); + assert.equal(trs.length, rows.length); + trs.forEach((tr, i) => { + assert(tr.hasClass(rows[i])); + }); + }); + + it('should render expandable column when specified', () => { + const columns = [{ header: 'Name', cell: row => row }]; + const rows = ['Alpha', 'Bravo', 'Charlie', 'Delta']; + const wrapper = mount( + Hey} + /> + ); + + const ths = wrapper.find('th'); + assert.equal(ths.length, columns.length + 1); // For expanded column + }); + + it('should supply onClick row handler when specified', () => { + const columns = [{ header: 'Name', cell: row => row }]; + const rows = ['Alpha', 'Bravo', 'Charlie', 'Delta']; + const onClick = sinon.stub(); + const wrapper = mount( + + ); + wrapper.find('tbody tr').first().simulate('click'); + sinon.assert.calledWith(onClick, 'Alpha'); + }); + + it('should render correct align when present', () => { + const columns = [ + { header: 'Default', cell: () => '-', footer: '-' }, + { header: 'Left', cell: () => '-', footer: '-', align: 'left' }, + { header: 'Center', cell: () => '-', footer: '-', align: 'center' }, + { header: 'Right', cell: () => '-', footer: '-', align: 'right' }, + ]; + const wrapper = mount(); + + assert.equal(wrapper.find('thead th.text-left').length, 1, 'thead th.text-left incorrect'); + assert.equal(wrapper.find('thead th.text-center').length, 1, 'thead th.text-center incorrect'); + assert.equal(wrapper.find('thead th.text-right').length, 1, 'thead th.text-right incorrect'); + + assert.equal(wrapper.find('tbody td.text-left').length, 3, 'tbody td.text-left incorrect'); + assert.equal(wrapper.find('tbody td.text-center').length, 3, 'tbody td.text-center incorrect'); + assert.equal(wrapper.find('tbody td.text-right').length, 3, 'tbody td.text-right incorrect'); + + assert.equal(wrapper.find('tfoot td.text-left').length, 1, 'tfoot td.text-left incorrect'); + assert.equal(wrapper.find('tfoot td.text-center').length, 1, 'tfoot td.text-center incorrect'); + assert.equal(wrapper.find('tfoot td.text-right').length, 1, 'tfoot td.text-right incorrect'); + }); + + it('should render correct column classnames when present', () => { + const columns = [ + { header: 'Default', cell: () => '-', footer: '-' }, + { header: 'Left', cell: () => '-', footer: '-', className: 'whatever' } + ]; + const wrapper = mount(); + + assert.equal(wrapper.find('thead th.whatever').length, 1, 'thead th.whatever incorrect'); + + assert.equal(wrapper.find('tbody td.whatever').length, 3, 'tbody td.whatever incorrect'); + + assert.equal(wrapper.find('tfoot td.whatever').length, 1, 'tfoot td.whatever incorrect'); + }); + + it('should render select column when specified', () => { + const columns = [{ header: 'Name', cell: row => row }]; + const rows = ['Alpha', 'Bravo', 'Charlie', 'Delta']; + const wrapper = mount( + + ); + + const ths = wrapper.find('th'); + assert.equal(ths.length, columns.length + 1); // For selectable column + }); + + it('should call onSelect when selectable row picked'); + it('should show correct rows when paginated specified'); + it('should show correct rows on page change'); +}); From 1d112d670802a1cdba9c03fb5b838105ea82c33b Mon Sep 17 00:00:00 2001 From: Gary Thomas Date: Thu, 16 Aug 2018 20:24:08 -0700 Subject: [PATCH 06/10] gt - Update tests --- test/components/UncontrolledTable.spec.js | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/test/components/UncontrolledTable.spec.js b/test/components/UncontrolledTable.spec.js index aed2b8ce0..35fb1d23d 100644 --- a/test/components/UncontrolledTable.spec.js +++ b/test/components/UncontrolledTable.spec.js @@ -258,6 +258,24 @@ describe('', () => { }); it('should call onSelect when selectable row picked'); - it('should show correct rows when paginated specified'); + + it('should show correct rows when paginated specified', () => { + const columns = [{ header: 'Name', cell: row => row }]; + const rows = ['Alpha', 'Bravo', 'Charlie', 'Delta', 'Echo', 'Foxtrot', 'Golf', 'Hotel']; + const wrapper = mount( + + ); + const trs = wrapper.find('tbody tr'); + assert.equal(trs.length, 4); + // TODO assert rows + }); + it('should show correct rows on page change'); + + it('should show correct rows on sort change'); }); From 3fab31c1a2eac7bfebb3c4c761b1265cdb65b6e7 Mon Sep 17 00:00:00 2001 From: Gary Thomas Date: Thu, 30 Aug 2018 20:56:43 -0700 Subject: [PATCH 07/10] gt - Review feedback --- src/components/UncontrolledTable.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/components/UncontrolledTable.js b/src/components/UncontrolledTable.js index 2fea95e73..7286aa7d6 100644 --- a/src/components/UncontrolledTable.js +++ b/src/components/UncontrolledTable.js @@ -76,9 +76,7 @@ export default class UncontrolledTable extends React.Component { without(selected, value) : [...selected, value]; - this.setState({ selected: newSelection }, - this.props.onSelect(newSelection) - ); + this.setState({ selected: newSelection }, () => this.props.onSelect(newSelection)); }; toggleAll = () => { @@ -140,7 +138,7 @@ export default class UncontrolledTable extends React.Component { type="checkbox" className="mx-1" checked={this.allSelected} - onChange={() => this.toggleAll()} + onChange={this.toggleAll} /> ), cell: row => ( From e210de107101839024d430c2e95d53f4195d3f63 Mon Sep 17 00:00:00 2001 From: Gary Thomas Date: Thu, 30 Aug 2018 21:14:43 -0700 Subject: [PATCH 08/10] gt - Add support for hidden columns --- src/components/UncontrolledTable.js | 18 ++++++++++-------- test/components/UncontrolledTable.spec.js | 2 ++ 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/components/UncontrolledTable.js b/src/components/UncontrolledTable.js index 7286aa7d6..25ed887f5 100644 --- a/src/components/UncontrolledTable.js +++ b/src/components/UncontrolledTable.js @@ -120,14 +120,16 @@ export default class UncontrolledTable extends React.Component { const { page, sort } = this.state; const { ascending, column } = sort; const { columns, expandable, pageSize, paginated, rowClassName, rowExpanded, rows, selectable, onSelect, ...props } = this.props; - const cols = columns.map(col => (col.sortable !== false) ? - { - active: column === col.key, - ascending, - onSort: asc => this.sortBy(col.key, asc), - ...col - } : col - ); + const cols = columns + .filter(col => !col.hidden) + .map(col => (col.sortable !== false) ? + { + active: column === col.key, + ascending, + onSort: asc => this.sortBy(col.key, asc), + ...col + } : col + ); if (selectable) { cols.unshift({ diff --git a/test/components/UncontrolledTable.spec.js b/test/components/UncontrolledTable.spec.js index 35fb1d23d..556eb5b87 100644 --- a/test/components/UncontrolledTable.spec.js +++ b/test/components/UncontrolledTable.spec.js @@ -278,4 +278,6 @@ describe('', () => { it('should show correct rows on page change'); it('should show correct rows on sort change'); + + it('should hide columns when hidden'); }); From df92d1c1e54532b07f5f83e021289098530616cb Mon Sep 17 00:00:00 2001 From: Gary Thomas Date: Thu, 30 Aug 2018 22:08:16 -0700 Subject: [PATCH 09/10] gt - Update test --- test/components/UncontrolledTable.spec.js | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/test/components/UncontrolledTable.spec.js b/test/components/UncontrolledTable.spec.js index 556eb5b87..f9bb259e6 100644 --- a/test/components/UncontrolledTable.spec.js +++ b/test/components/UncontrolledTable.spec.js @@ -279,5 +279,19 @@ describe('', () => { it('should show correct rows on sort change'); - it('should hide columns when hidden'); + it('should hide columns when hidden', () => { + const columns = [ + { header: 'Name', cell: row => row }, + { header: 'Nope', cell: () => 'Nope', hidden: true }, + ]; + const rows = ['Alpha', 'Bravo', 'Charlie', 'Delta', 'Echo', 'Foxtrot', 'Golf', 'Hotel']; + const wrapper = mount( + + ); + assert.equal(wrapper.find('th').length, 1); + assert.equal(wrapper.find('td').length, rows.length); + }); }); From 9c8fa3cdc0d4476ca79366ae7f96a85ed9c75eb0 Mon Sep 17 00:00:00 2001 From: Gary Thomas Date: Thu, 30 Aug 2018 22:18:18 -0700 Subject: [PATCH 10/10] gt - Update tests --- test/components/UncontrolledTable.spec.js | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/test/components/UncontrolledTable.spec.js b/test/components/UncontrolledTable.spec.js index f9bb259e6..ab51c41e1 100644 --- a/test/components/UncontrolledTable.spec.js +++ b/test/components/UncontrolledTable.spec.js @@ -257,7 +257,24 @@ describe('', () => { assert.equal(ths.length, columns.length + 1); // For selectable column }); - it('should call onSelect when selectable row picked'); + it('should call onSelect when selectable row picked', () => { + const columns = [{ header: 'Name', cell: row => row }]; + const rows = ['Alpha', 'Bravo', 'Charlie', 'Delta', 'Echo', 'Foxtrot', 'Golf', 'Hotel']; + const onSelect = sinon.stub(); + const wrapper = mount( + + ); + wrapper + .find({ type: 'checkbox' }) + .first() + .simulate('change', { target: { checked: true } }); + sinon.assert.calledWith(onSelect, rows); + }); it('should show correct rows when paginated specified', () => { const columns = [{ header: 'Name', cell: row => row }];