diff --git a/e2e/__tests__/__image_snapshots__/table-test-js-components-table-with-selection-1-snap.png b/e2e/__tests__/__image_snapshots__/table-test-js-components-table-with-selection-1-snap.png new file mode 100644 index 00000000..1e1fa960 Binary files /dev/null and b/e2e/__tests__/__image_snapshots__/table-test-js-components-table-with-selection-1-snap.png differ diff --git a/e2e/__tests__/__image_snapshots__/table-test-js-components-table-with-selection-all-selected-1-snap.png b/e2e/__tests__/__image_snapshots__/table-test-js-components-table-with-selection-all-selected-1-snap.png new file mode 100644 index 00000000..46662eb8 Binary files /dev/null and b/e2e/__tests__/__image_snapshots__/table-test-js-components-table-with-selection-all-selected-1-snap.png differ diff --git a/e2e/__tests__/__image_snapshots__/table-test-js-components-table-with-sort-1-snap.png b/e2e/__tests__/__image_snapshots__/table-test-js-components-table-with-sort-1-snap.png new file mode 100644 index 00000000..12988418 Binary files /dev/null and b/e2e/__tests__/__image_snapshots__/table-test-js-components-table-with-sort-1-snap.png differ diff --git a/e2e/__tests__/table.test.js b/e2e/__tests__/table.test.js index e8b1ffc5..325ad0cd 100644 --- a/e2e/__tests__/table.test.js +++ b/e2e/__tests__/table.test.js @@ -5,6 +5,20 @@ const SUITES = [ baisy.suite('Components/Table', 'with data'), baisy.suite('Components/Table', 'with loader'), baisy.suite('Components/Table', 'without data'), + baisy.suite('Components/Table', 'with sort') + .setEnhancer(async (iframe) => { + await (await await iframe.waitForXPath('//*[contains(text(),"Id")]')).click(); + await (await await iframe.waitForXPath('//*[contains(text(),"Id")]')).click(); + await (await await iframe.waitForXPath('//*[contains(text(),"Email")]')).click(); + }), + baisy.suite('Components/Table', 'with selection') + .setEnhancer(async (iframe) => { + await (await await iframe.waitForXPath('(//i)[4]')).click(); + }), + baisy.suite('Components/Table', 'with selection', 'all selected') + .setEnhancer(async (iframe) => { + await (await await iframe.waitForXPath('//i')).click(); + }), ]; diff --git a/src/components/Checkbox/Checkbox.js b/src/components/Checkbox/Checkbox.js index e03528cc..bdbe13ef 100644 --- a/src/components/Checkbox/Checkbox.js +++ b/src/components/Checkbox/Checkbox.js @@ -56,7 +56,9 @@ class Checkbox extends PureComponent { type="checkbox" tagName="input" /> - { label } + + { label } + ); } diff --git a/src/components/Checkbox/Checkbox.theme.js b/src/components/Checkbox/Checkbox.theme.js index 7e458e84..346c2946 100644 --- a/src/components/Checkbox/Checkbox.theme.js +++ b/src/components/Checkbox/Checkbox.theme.js @@ -75,10 +75,10 @@ const CheckboxTag = createStyledTag(`${name}Tag`, { display: 'none', }); -const CheckboxTextTag = createStyledTag(`${name}Text`, props => ({ - paddingLeft: props.label ? '12px' : '0', +const CheckboxTextTag = createStyledTag(`${name}Text`, { + paddingLeft: '12px', cursor: 'pointer', -})); +}); export { theme, CheckboxSquareTag, CheckboxTag, CheckboxWrapperTag, CheckboxTextTag, CheckboxIconTag }; diff --git a/src/components/Table/Table.js b/src/components/Table/Table.js index d45f384f..5c4b7da2 100644 --- a/src/components/Table/Table.js +++ b/src/components/Table/Table.js @@ -17,7 +17,7 @@ import { TableBodyCell, theme as tableBodyCellTheme } from './TableBodyCell'; type TablePlateProps = { children?: React$Node, stretch?: boolean, -}; +} const name = 'tablePlate'; @@ -46,10 +46,18 @@ const theme = { const TableTag = createStyledTag(name); function Table({ + action, children, + columns, + data, + onActionClick, ...rest }: TablePlateProps) { - return { children }; + return ( + + { children } + + ); } Table.defaultProps = { diff --git a/src/components/Table/Table.stories.js b/src/components/Table/Table.stories.js index a0736d27..076c89df 100644 --- a/src/components/Table/Table.stories.js +++ b/src/components/Table/Table.stories.js @@ -2,6 +2,27 @@ import React from 'react'; +const TABLE_COLUMNS = [{ + name: 'id', + title: 'Id', + width: '300px', +}, { + name: 'createdAt', + title: 'Created At', +}, { + name: 'updatedAt', + title: 'Updated At', +}, { + name: 'firstName', + title: 'First Name', +}, { + name: 'lastName', + title: 'Last Name', +}, { + name: 'email', + title: 'Email', +}]; + const TABLE_DATA = [{ id: '1', createdAt: '2018-09-03T09:59:43.000Z', @@ -214,8 +235,26 @@ const TABLE_DATA = [{ email: 'zouxuoz@gmail.com', }]; + +class TableState extends React.Component { + state = { + tableState: {}, + } + + setTableState = (tableState) => { + this.setState({ tableState }); + } + + render() { + const { children } = this.props; + const { tableState } = this.state; + + return children({ tableState, setTableState: this.setTableState }); + } +} + export default (asStory) => { - asStory('Components/Table', module, (story, { Table, Link, Dropdown, Icon, Menu, Button }) => { + asStory('Components/Table', module, (story, { Table, TableBuilder, Link, Dropdown, Icon, Menu, Button }) => { story .add('default', () => (
@@ -373,6 +412,38 @@ export default (asStory) => { />
+ )) + + .add('with sort', () => ( +
+ + { ({ tableState, setTableState }) => ( + alert('Create') } + onChange={ setTableState } + tableState={ tableState } + /> + ) } + +
+ )) + .add('with selection', () => ( +
+ + { ({ tableState, setTableState }) => ( + alert('Create') } + onChange={ setTableState } + tableState={ tableState } + withSelection + /> + ) } + +
)); }); }; diff --git a/src/components/Table/TableBodyCell.js b/src/components/Table/TableBodyCell.js index 91ec4509..2548fcfd 100644 --- a/src/components/Table/TableBodyCell.js +++ b/src/components/Table/TableBodyCell.js @@ -2,6 +2,7 @@ import React from 'react'; import { createStyledTag, createComponentTheme } from '../../utils'; +import { Row } from '../FlexLayout'; type TableBodyCellProps = { children?: React$Node, @@ -29,7 +30,7 @@ function TableBodyCell({ children, ...rest }: TableBodyCellProps) { - return { children }; + return { children }; } export { TableBodyCell, theme }; diff --git a/src/components/Table/TableBuilder.js b/src/components/Table/TableBuilder.js new file mode 100644 index 00000000..57803c07 --- /dev/null +++ b/src/components/Table/TableBuilder.js @@ -0,0 +1,190 @@ +// @flow + +import React, { Component } from 'react'; + +import fp from 'lodash/fp'; +import { TableHeader } from './TableHeader'; +import { TableBody } from './TableBody'; +import { TableBodyRow } from './TableBodyRow'; +import { TableHeaderCell } from './TableHeaderCell'; +import { TableBodyCell } from './TableBodyCell'; +import { Table } from './Table'; +import { Checkbox } from '../Checkbox'; + + +type ColumnType = { + title: string, + name: string, + sortEnable?: boolean, +} + +type TableState = { + sort?: Array<{ name: string, order: 'ASC' | 'DESC' }>, + selection?: Array +} + +type TableBulderProps = { + columns: Array, + data: Array, + onActionClick?: () => void, + action?: React$Node, + + tableState: TableState, + withSelection?: boolean, + onChange: TableState => void, +} + +class TableBuilder extends Component { + + static defaultProps = { + columns: [], + data: [], + tableState: { + sort: [], + selection: [], + }, + } + + getGridColumns = () => { + const { columns, withSelection } = this.props; + + return withSelection + ? `80px repeat(${columns.length}, 1fr)` + : `repeat(${columns.length}, 1fr)`; + } + + onSort = fp.memoize((name: string) => (order: 'ASC' | 'DESC') => { + const { onChange, tableState = {}} = this.props; + const previousSort = tableState.sort || []; + + const sort = previousSort + .filter(({ name: columnName }) => columnName !== name) + .concat([{ name, order }]); + + onChange({ + ...tableState, + sort, + }); + }) + + getColumnOrder = (name: string) => { + const { tableState = {}} = this.props; + const sort = tableState.sort || []; + + const { order } = sort + .find(({ name: columnName }) => columnName === name) + || {}; + + return order; + } + + + selectAllRows = () => { + const { onChange, tableState = {}, data } = this.props; + const isAllRowsSelected = this.hasAllRowsSelection(); + const allIds = fp.map('id', data); + + const selection = isAllRowsSelected + ? [] + : allIds; + + onChange({ + ...tableState, + selection, + }); + } + + selectRow = fp.memoize((id: string) => () => { + const { onChange, tableState = {}} = this.props; + const previousSelection = tableState.selection || []; + const isRowSelected = this.hasRowSelection(id); + + const selection = isRowSelected + ? fp.xor(previousSelection, [id]) + : fp.uniq([...previousSelection, id]); + + onChange({ + ...tableState, + selection, + }); + }) + + hasRowSelection = (id: string) => { + const { tableState: { selection } = {}} = this.props; + + return fp.findIndex( + fp.equals(id), + selection, + ) >= 0; + } + + hasAllRowsSelection = () => { + const { tableState: { selection } = {}, data } = this.props; + const allIds = fp.map('id', data); + + return fp.isEmpty(fp.xor(selection, allIds)); + } + + + renderHeader = () => { + const { columns, withSelection } = this.props; + + return ( + + + + + + + { columns.map(({ title, name }) => ( + + { title } + + )) + } + + ); + } + + renderBody = () => { + const { columns, onActionClick, action, data, withSelection } = this.props; + + return ( + + { + (rowData) => ( + + + + + + + { columns.map(({ name }) => ( + { rowData[name] } + )) } + + ) + } + + ); + } + + render() { + return ( + + { this.renderHeader() } + { this.renderBody() } +
+ ); + } +} + +export { TableBuilder }; + +export type { TableBulderProps, ColumnType }; + diff --git a/src/components/Table/TableHeaderCell.js b/src/components/Table/TableHeaderCell.js index d1372143..bd998f01 100644 --- a/src/components/Table/TableHeaderCell.js +++ b/src/components/Table/TableHeaderCell.js @@ -1,10 +1,18 @@ // @flow -import React from 'react'; +import React, { PureComponent } from 'react'; +import styled from 'react-emotion'; import { createStyledTag, createComponentTheme } from '../../utils'; +import { Row } from '../FlexLayout'; +import { Icon } from '../Icon'; + +const DEFAULT_SORT = 'DESC'; type TableHeaderCellProps = { children?: React$Node, + onSort?: ('ASC' | 'DESC') => void, + order?: 'ASC' | 'DESC', + cursor?: 'pointer' | 'default' | 'inherit', }; const name = 'tableHeaderCell'; @@ -23,16 +31,48 @@ const theme = createComponentTheme(name, ({ SIZES }: *) => ({ }, })); -const TableHeaderCellTag = createStyledTag(name, { +const TableHeaderCellTag = createStyledTag(name, props => ({ display: 'flex', alignItems: 'center', + cursor: props.cursor, +})); + +const IconTransform = styled('div')(props => { + const styles = {}; + + if (props.order === 'ASC') { + styles.transform = 'rotate(180deg)'; + } + + return styles; }); -function TableHeaderCell({ - children, - ...rest - }: TableHeaderCellProps) { - return { children }; +class TableHeaderCell extends PureComponent { + + onSort = () => { + const { onSort, order } = this.props; + + switch (order) { + case 'ASC': return onSort && onSort('DESC'); + case 'DESC': return onSort && onSort('ASC'); + default: return onSort && onSort(DEFAULT_SORT); + } + } + + render() { + const { children, order, cursor, ...rest } = this.props; + + return ( + + { children } + + + + + + + ); + } } export { TableHeaderCell, theme }; diff --git a/src/components/Table/index.js b/src/components/Table/index.js index 498e25ac..3b4415f0 100644 --- a/src/components/Table/index.js +++ b/src/components/Table/index.js @@ -1,3 +1,4 @@ // @flow export { Table, theme } from './Table'; +export { TableBuilder } from './TableBuilder'; diff --git a/src/components/components.js b/src/components/components.js index 1416ced3..f830b2d6 100644 --- a/src/components/components.js +++ b/src/components/components.js @@ -36,7 +36,7 @@ export { SecondaryNavigation } from './SecondaryNavigation'; export { Select } from './Select'; export { SelectField } from './SelectField'; export { Switch } from './Switch'; -export { Table } from './Table'; +export { Table, TableBuilder } from './Table'; export { Tabs } from './Tabs'; export { Tag } from './Tag'; export { Text } from './Text';