Add Table component (https://github.com/woocommerce/woocommerce-admin/pull/118)
* Update package-lock * Add data table component * Add fake data & display revenue on the analytics test report * Update styling * Display table inside a card * Update mock data to be easier to scan for testing ascending/descending * Fix sorting function to correctly sort all columns * Check column content before determining “sortability” — objects (links) are not sortable * Update README * Add translation to scroll helper text
This commit is contained in:
parent
060b1dd08f
commit
b1c2b3fdc4
|
@ -0,0 +1,84 @@
|
|||
/** @format */
|
||||
|
||||
export default {
|
||||
totals: {
|
||||
gross_revenue: 9476.0,
|
||||
coupons: 504.0,
|
||||
refunds: 145.0,
|
||||
shipping: 990.0,
|
||||
taxes: '663.32',
|
||||
net_revenue: '7173.68',
|
||||
order_count: 42.0,
|
||||
},
|
||||
intervals: [
|
||||
{
|
||||
week: [
|
||||
{
|
||||
'2018-W22': {
|
||||
gross_revenue: 200.0,
|
||||
coupons: 19.0,
|
||||
refunds: 19.0,
|
||||
shipping: 19.0,
|
||||
taxes: '100.00',
|
||||
net_revenue: '200.0',
|
||||
order_count: 30.0,
|
||||
},
|
||||
'2018-W23': {
|
||||
gross_revenue: 150.0,
|
||||
coupons: 10.0,
|
||||
refunds: 10.0,
|
||||
shipping: 10.0,
|
||||
taxes: '150.00',
|
||||
net_revenue: '150.0',
|
||||
order_count: 42.0,
|
||||
},
|
||||
'2018-W24': {
|
||||
gross_revenue: 100.0,
|
||||
coupons: 11.0,
|
||||
refunds: 11.0,
|
||||
shipping: 11.0,
|
||||
taxes: '990.00',
|
||||
net_revenue: '100.0',
|
||||
order_count: 36.0,
|
||||
},
|
||||
'2018-W25': {
|
||||
gross_revenue: 300.0,
|
||||
coupons: 22.0,
|
||||
refunds: 22.0,
|
||||
shipping: 22.0,
|
||||
taxes: '1000.00',
|
||||
net_revenue: '300.0',
|
||||
order_count: 28.0,
|
||||
},
|
||||
'2018-W26': {
|
||||
gross_revenue: 250.0,
|
||||
coupons: 16.0,
|
||||
refunds: 16.0,
|
||||
shipping: 16.0,
|
||||
taxes: '663.32',
|
||||
net_revenue: '250.0',
|
||||
order_count: 39.0,
|
||||
},
|
||||
'2018-W27': {
|
||||
gross_revenue: 400.0,
|
||||
coupons: 18.0,
|
||||
refunds: 18.0,
|
||||
shipping: 18.0,
|
||||
taxes: '663.32',
|
||||
net_revenue: '400.0',
|
||||
order_count: 40.0,
|
||||
},
|
||||
'2018-W28': {
|
||||
gross_revenue: 350.0,
|
||||
coupons: 10.0,
|
||||
refunds: 10.0,
|
||||
shipping: 10.0,
|
||||
taxes: '663.32',
|
||||
net_revenue: '350.0',
|
||||
order_count: 29.0,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
|
@ -4,18 +4,60 @@
|
|||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { Component, Fragment } from '@wordpress/element';
|
||||
import { map } from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import Card from 'components/card';
|
||||
import DatePicker from 'components/date-picker';
|
||||
import { getAdminLink } from 'lib/nav-utils';
|
||||
import { getCurrencyFormatString } from 'lib/currency';
|
||||
import Header from 'components/header';
|
||||
import { SummaryList, SummaryNumber } from 'components/summary';
|
||||
import Table from 'components/table';
|
||||
|
||||
// Mock data until we fetch from an API
|
||||
import rawData from './mock-data';
|
||||
|
||||
class RevenueReport extends Component {
|
||||
getRowsContent( data ) {
|
||||
return map( data, ( row, label ) => {
|
||||
// @TODO How to create this per-report? Can use `w`, `year`, `m` to build time-specific order links
|
||||
// we need to know which kind of report this is, and parse the `label` to get this row's date
|
||||
const orderLink = (
|
||||
<a href={ getAdminLink( '/edit.php?post_type=shop_order&w=4&year=2018' ) }>
|
||||
{ row.order_count }
|
||||
</a>
|
||||
);
|
||||
return [
|
||||
label,
|
||||
orderLink,
|
||||
getCurrencyFormatString( row.gross_revenue ),
|
||||
getCurrencyFormatString( row.refunds ),
|
||||
getCurrencyFormatString( row.coupons ),
|
||||
getCurrencyFormatString( row.taxes ),
|
||||
getCurrencyFormatString( row.shipping ),
|
||||
getCurrencyFormatString( row.net_revenue ),
|
||||
];
|
||||
} );
|
||||
}
|
||||
|
||||
render() {
|
||||
const { path, query } = this.props;
|
||||
const rows = this.getRowsContent( rawData.intervals[ 0 ].week[ 0 ] );
|
||||
const headers = [
|
||||
__( 'Date', 'woo-dash' ),
|
||||
__( 'Orders', 'woo-dash' ),
|
||||
__( 'Gross Revenue', 'woo-dash' ),
|
||||
__( 'Refunds', 'woo-dash' ),
|
||||
__( 'Coupons', 'woo-dash' ),
|
||||
__( 'Taxes', 'woo-dash' ),
|
||||
__( 'Shipping', 'woo-dash' ),
|
||||
__( 'Net Revenue', 'woo-dash' ),
|
||||
];
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<Header
|
||||
|
@ -41,6 +83,11 @@ class RevenueReport extends Component {
|
|||
<SummaryNumber value={ '$49.90' } label={ __( 'Coupons', 'woo-dash' ) } delta={ 15 } />
|
||||
<SummaryNumber value={ '$66.39' } label={ __( 'Taxes', 'woo-dash' ) } />
|
||||
</SummaryList>
|
||||
<Card title={ __( 'Gross Revenue' ) }>
|
||||
<p>Graph here</p>
|
||||
<hr />
|
||||
<Table rows={ rows } headers={ headers } caption={ __( 'Revenue Last Week' ) } />
|
||||
</Card>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
Table
|
||||
=====
|
||||
|
||||
This is an accessible, sortable, and scrollable table for displaying tabular data (like revenue and other analytics data). It accepts `headers` for column headers, and `rows` for the table content. `rowHeader` can be used to define the index of the row header (or false if no header). Sortability is enabled by default (and can be turned off), however only string and number columns are sortable.
|
||||
|
||||
## How to use:
|
||||
|
||||
```jsx
|
||||
import Table from 'components/table';
|
||||
|
||||
render: function() {
|
||||
return (
|
||||
<Table
|
||||
rows={ rows }
|
||||
headers={ headers }
|
||||
caption={ __( 'Revenue Last Week' ) }
|
||||
/>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Props
|
||||
|
||||
* `caption` (required): A label for the content in this table
|
||||
* `className`: Optional additional classes
|
||||
* `headers`: An array of column headers
|
||||
* `rows` (required): An array of arrays of renderable elements, strings or numbers are best for sorting
|
||||
* `rowHeader`: Which column should be the row header, defaults to the first item (`0`) (but could be set to `1`, if the first col is checkboxes, for example). Set to false to disable row headers.
|
||||
* `sortable`: Boolean, false if column sorting should be disabled. Defaults to true.
|
||||
|
||||
## Rows Format
|
||||
|
||||
Row data should be passed to the component as a list of arrays, where each array is a row in the table. Headers are passed in separately as an array of strings. For example, this data would render the following table.
|
||||
|
||||
```js
|
||||
const headers = [ 'Month', 'Orders', 'Revenue' ];
|
||||
const rows = [
|
||||
[ 'January', 10, 530 ],
|
||||
[ 'February', 13, 675 ],
|
||||
[ 'March', 9, 460 ],
|
||||
]
|
||||
```
|
||||
|
||||
| Month | Orders | Revenue |
|
||||
| ---------|--------|---------|
|
||||
| January | 10 | 530 |
|
||||
| February | 13 | 675 |
|
||||
| March | 9 | 460 |
|
|
@ -0,0 +1,155 @@
|
|||
/** @format */
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __, sprintf } from '@wordpress/i18n';
|
||||
import { Component, createRef } from '@wordpress/element';
|
||||
import classnames from 'classnames';
|
||||
import { IconButton } from '@wordpress/components';
|
||||
import PropTypes from 'prop-types';
|
||||
import { uniqueId } from 'lodash';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import './style.scss';
|
||||
|
||||
const ASC = 'ascending';
|
||||
const DESC = 'descending';
|
||||
|
||||
class Table extends Component {
|
||||
constructor( props ) {
|
||||
super( props );
|
||||
this.state = {
|
||||
tabIndex: null,
|
||||
rows: props.rows || [],
|
||||
sortedBy: null,
|
||||
sortDir: 'none',
|
||||
};
|
||||
this.container = createRef();
|
||||
this.sortBy = this.sortBy.bind( this );
|
||||
this.captionID = uniqueId( 'caption-' );
|
||||
}
|
||||
|
||||
sortBy( col ) {
|
||||
this.setState( prevState => {
|
||||
// Set the sort direction as inverse of current state
|
||||
const sortDir = prevState.sortDir === ASC ? DESC : ASC;
|
||||
return {
|
||||
rows: prevState.rows
|
||||
.slice( 0 )
|
||||
.sort( ( a, b ) => ( sortDir === ASC ? a[ col ] > b[ col ] : a[ col ] < b[ col ] ) ),
|
||||
sortedBy: col,
|
||||
sortDir,
|
||||
};
|
||||
} );
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { scrollWidth, clientWidth } = this.container.current;
|
||||
const scrollable = scrollWidth > clientWidth;
|
||||
/* eslint-disable react/no-did-mount-set-state */
|
||||
this.setState( {
|
||||
tabIndex: scrollable ? '0' : null,
|
||||
} );
|
||||
/* eslint-enable react/no-did-mount-set-state */
|
||||
}
|
||||
|
||||
isColSortable( col ) {
|
||||
const { sortable, rows: [ first ] } = this.props;
|
||||
if ( ! first ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// The table is not set to be sortable, we don't need to check cols.
|
||||
if ( ! sortable ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return 'object' !== typeof first[ col ];
|
||||
}
|
||||
|
||||
render() {
|
||||
const { caption, classNames, headers, rowHeader } = this.props;
|
||||
const { rows, sortedBy, sortDir, tabIndex } = this.state;
|
||||
const classes = classnames( 'woocommerce-table', classNames );
|
||||
|
||||
return (
|
||||
<div
|
||||
className={ classes }
|
||||
ref={ this.container }
|
||||
tabIndex={ tabIndex }
|
||||
aria-labelledby={ this.captionID }
|
||||
role="group"
|
||||
>
|
||||
<table className="woocommerce-table__table">
|
||||
<caption id={ this.captionID } className="woocommerce-table__caption">
|
||||
{ caption }
|
||||
{ tabIndex === '0' && <small>{ __( '(scroll to see more)', 'woo-dash' ) }</small> }
|
||||
</caption>
|
||||
<tbody>
|
||||
<tr>
|
||||
{ headers.map( ( header, i ) => (
|
||||
<th
|
||||
role="columnheader"
|
||||
scope="col"
|
||||
key={ i }
|
||||
aria-sort={ sortedBy === i ? sortDir : 'none' }
|
||||
className={ classnames( 'woocommerce-table__header', {
|
||||
'is-sorted': sortedBy === i,
|
||||
} ) }
|
||||
>
|
||||
{ this.isColSortable( i ) && (
|
||||
<IconButton
|
||||
icon={ sortDir !== ASC ? 'arrow-up' : 'arrow-down' }
|
||||
label={
|
||||
sortDir !== ASC
|
||||
? sprintf( __( 'Sort by %s in ascending order', 'woo-dash' ), header )
|
||||
: sprintf( __( 'Sort by %s in descending order', 'woo-dash' ), header )
|
||||
}
|
||||
onClick={ () => this.sortBy( i ) }
|
||||
/>
|
||||
) }
|
||||
{ header }
|
||||
</th>
|
||||
) ) }
|
||||
</tr>
|
||||
{ rows.map( ( row, i ) => (
|
||||
<tr key={ i }>
|
||||
{ row.map(
|
||||
( cell, j ) =>
|
||||
rowHeader === j ? (
|
||||
<th scope="row" key={ j } className="woocommerce-table__item">
|
||||
{ cell }
|
||||
</th>
|
||||
) : (
|
||||
<td key={ j } className="woocommerce-table__item">
|
||||
{ cell }
|
||||
</td>
|
||||
)
|
||||
) }
|
||||
</tr>
|
||||
) ) }
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Table.propTypes = {
|
||||
caption: PropTypes.string.isRequired,
|
||||
className: PropTypes.string,
|
||||
headers: PropTypes.arrayOf( PropTypes.node ),
|
||||
rows: PropTypes.arrayOf( PropTypes.arrayOf( PropTypes.node ) ).isRequired,
|
||||
rowHeader: PropTypes.oneOfType( [ PropTypes.number, PropTypes.bool ] ),
|
||||
sortable: PropTypes.bool,
|
||||
};
|
||||
|
||||
Table.defaultProps = {
|
||||
headers: [],
|
||||
rowHeader: 0,
|
||||
sortable: true,
|
||||
};
|
||||
|
||||
export default Table;
|
|
@ -0,0 +1,45 @@
|
|||
/** @format */
|
||||
|
||||
.woocommerce-table {
|
||||
overflow-x: auto;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.woocommerce-table__caption {
|
||||
@include font-size( 24 );
|
||||
margin-bottom: $spacing;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.woocommerce-table__table {
|
||||
border-collapse: collapse;
|
||||
border: 1px solid $gray;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.woocommerce-table__header,
|
||||
.woocommerce-table__item {
|
||||
padding: $spacing/2;
|
||||
border-bottom: 1px solid $gray;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.woocommerce-table__header,
|
||||
th.woocommerce-table__item {
|
||||
font-weight: bold;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.woocommerce-table__header {
|
||||
background-color: #f8f9fa;
|
||||
|
||||
& + .woocommerce-table__header {
|
||||
border-left: 1px solid $gray;
|
||||
}
|
||||
|
||||
.components-icon-button {
|
||||
margin-right: 5px;
|
||||
padding: 0;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
|
@ -4975,6 +4975,7 @@
|
|||
},
|
||||
"eslint-plugin-wordpress": {
|
||||
"version": "git://github.com/WordPress-Coding-Standards/eslint-plugin-wordpress.git#1774343f6226052a46b081e01db3fca8793cc9f1",
|
||||
"from": "git://github.com/WordPress-Coding-Standards/eslint-plugin-wordpress.git#1774343f6226052a46b081e01db3fca8793cc9f1",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"eslint-plugin-i18n": "1.2.0",
|
||||
|
@ -7247,6 +7248,7 @@
|
|||
},
|
||||
"gutenberg": {
|
||||
"version": "github:WordPress/gutenberg#9221170ee6604f21818279230457479a0349187e",
|
||||
"from": "gutenberg@github:WordPress/gutenberg#9221170ee6604f21818279230457479a0349187e",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@wordpress/a11y": "1.0.6",
|
||||
|
@ -13071,6 +13073,7 @@
|
|||
},
|
||||
"prettier": {
|
||||
"version": "github:automattic/calypso-prettier#c56b42511ec98ba6d8f72b6c391e0a626e90f531",
|
||||
"from": "prettier@github:automattic/calypso-prettier#c56b42511ec98ba6d8f72b6c391e0a626e90f531",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"babel-code-frame": "7.0.0-beta.3",
|
||||
|
|
Loading…
Reference in New Issue