Update/table component (https://github.com/woocommerce/woocommerce-admin/pull/251)
* Update mock data to reflect latest API format * Update Table component to create a TableCard, TableSummary, and plain Table Use the TableCard container to display mocked revenue data * Add horizontal scroll only on the table itself * Remove `sortable` prop, base column sortability on whether the `value` is true/false Checking against a strict false should prevent any real 0 or ‘’ values from triggering unsortability * Add a checkbox to the demo table * Update accepted props * Update README for TableCard, Table, and TableSummary * Update README * Fix confused defaults
This commit is contained in:
parent
d346945702
commit
8141a9f66d
|
@ -8,77 +8,120 @@ export default {
|
|||
shipping: 990.0,
|
||||
taxes: '663.32',
|
||||
net_revenue: '7173.68',
|
||||
order_count: 42.0,
|
||||
orders_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,
|
||||
},
|
||||
},
|
||||
],
|
||||
interval: 'day',
|
||||
date_start: '2018-04-29T00:00:00',
|
||||
date_start_gmt: '2018-04-29T00:00:00',
|
||||
date_end: '2018-04-30T00:00:00',
|
||||
date_end_gmt: '2018-04-30T00:00:00',
|
||||
subtotals: {
|
||||
gross_revenue: 200.0,
|
||||
coupons: 19.0,
|
||||
refunds: 19.0,
|
||||
shipping: 19.0,
|
||||
taxes: '100.00',
|
||||
net_revenue: '200.0',
|
||||
orders_count: 30.0,
|
||||
},
|
||||
},
|
||||
{
|
||||
interval: 'day',
|
||||
date_start: '2018-04-30T00:00:00',
|
||||
date_start_gmt: '2018-04-30T00:00:00',
|
||||
date_end: '2018-05-01T00:00:00',
|
||||
date_end_gmt: '2018-05-01T00:00:00',
|
||||
subtotals: {
|
||||
gross_revenue: 150.0,
|
||||
coupons: 10.0,
|
||||
refunds: 10.0,
|
||||
shipping: 10.0,
|
||||
taxes: '150.00',
|
||||
net_revenue: '150.0',
|
||||
orders_count: 42.0,
|
||||
},
|
||||
},
|
||||
{
|
||||
interval: 'day',
|
||||
date_start: '2018-05-01T00:00:00',
|
||||
date_start_gmt: '2018-05-01T00:00:00',
|
||||
date_end: '2018-05-02T00:00:00',
|
||||
date_end_gmt: '2018-05-02T00:00:00',
|
||||
subtotals: {
|
||||
gross_revenue: 100.0,
|
||||
coupons: 11.0,
|
||||
refunds: 11.0,
|
||||
shipping: 11.0,
|
||||
taxes: '990.00',
|
||||
net_revenue: '100.0',
|
||||
orders_count: 36.0,
|
||||
},
|
||||
},
|
||||
{
|
||||
interval: 'day',
|
||||
date_start: '2018-05-02T00:00:00',
|
||||
date_start_gmt: '2018-05-02T00:00:00',
|
||||
date_end: '2018-05-03T00:00:00',
|
||||
date_end_gmt: '2018-05-03T00:00:00',
|
||||
subtotals: {
|
||||
gross_revenue: 300.0,
|
||||
coupons: 22.0,
|
||||
refunds: 22.0,
|
||||
shipping: 22.0,
|
||||
taxes: '1000.00',
|
||||
net_revenue: '300.0',
|
||||
orders_count: 28.0,
|
||||
},
|
||||
},
|
||||
{
|
||||
interval: 'day',
|
||||
date_start: '2018-05-03T00:00:00',
|
||||
date_start_gmt: '2018-05-03T00:00:00',
|
||||
date_end: '2018-05-04T00:00:00',
|
||||
date_end_gmt: '2018-05-04T00:00:00',
|
||||
subtotals: {
|
||||
gross_revenue: 250.0,
|
||||
coupons: 16.0,
|
||||
refunds: 16.0,
|
||||
shipping: 16.0,
|
||||
taxes: '663.32',
|
||||
net_revenue: '250.0',
|
||||
orders_count: 39.0,
|
||||
},
|
||||
},
|
||||
{
|
||||
interval: 'day',
|
||||
date_start: '2018-05-04T00:00:00',
|
||||
date_start_gmt: '2018-05-04T00:00:00',
|
||||
date_end: '2018-05-05T00:00:00',
|
||||
date_end_gmt: '2018-05-05T00:00:00',
|
||||
subtotals: {
|
||||
gross_revenue: 400.0,
|
||||
coupons: 18.0,
|
||||
refunds: 18.0,
|
||||
shipping: 18.0,
|
||||
taxes: '663.32',
|
||||
net_revenue: '400.0',
|
||||
orders_count: 40.0,
|
||||
},
|
||||
},
|
||||
{
|
||||
interval: 'day',
|
||||
date_start: '2018-05-05T00:00:00',
|
||||
date_start_gmt: '2018-05-05T00:00:00',
|
||||
date_end: '2018-05-06T00:00:00',
|
||||
date_end_gmt: '2018-05-06T00:00:00',
|
||||
subtotals: {
|
||||
gross_revenue: 350.0,
|
||||
coupons: 10.0,
|
||||
refunds: 10.0,
|
||||
shipping: 10.0,
|
||||
taxes: '663.32',
|
||||
net_revenue: '350.0',
|
||||
orders_count: 29.0,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
|
@ -4,36 +4,33 @@
|
|||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { Component, Fragment } from '@wordpress/element';
|
||||
import { get, map } from 'lodash';
|
||||
import { format as formatDate } from '@wordpress/date';
|
||||
import { map, noop } from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import Card from 'components/card';
|
||||
import DatePicker from 'components/date-picker';
|
||||
import { formatCurrency } from 'lib/currency';
|
||||
import { formatCurrency, getCurrencyFormatDecimal } from 'lib/currency';
|
||||
import { getAdminLink, updateQueryString } from 'lib/nav-utils';
|
||||
import { getReportData } from 'lib/swagger';
|
||||
import Header from 'layout/header';
|
||||
import { SummaryList, SummaryNumber } from 'components/summary';
|
||||
import Table from 'components/table';
|
||||
import Pagination from 'components/pagination';
|
||||
import { TableCard } from 'components/table';
|
||||
|
||||
// Mock data until we fetch from an API
|
||||
import rawData from './mock-data';
|
||||
|
||||
class RevenueReport extends Component {
|
||||
constructor() {
|
||||
super();
|
||||
this.onPageChange = this.onPageChange.bind( this );
|
||||
this.onPerPageChange = this.onPerPageChange.bind( this );
|
||||
this.onQueryChange = this.onQueryChange.bind( this );
|
||||
|
||||
// TODO remove this when we implement real endpoints
|
||||
this.state = { stats: {} };
|
||||
}
|
||||
|
||||
onPageChange( page ) {
|
||||
updateQueryString( { page } );
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
// Swagger doesn't support returning different data based on query args
|
||||
// this is more or less to show how we will manipulate data calls based on props
|
||||
|
@ -48,17 +45,37 @@ class RevenueReport extends Component {
|
|||
return;
|
||||
}
|
||||
|
||||
response.json().then( data => {
|
||||
this.setState( { stats: data } );
|
||||
response.json().then( () => {
|
||||
// Ignore data, just use our fake data once we have a response
|
||||
this.setState( { stats: rawData } );
|
||||
} );
|
||||
} );
|
||||
}
|
||||
|
||||
onPerPageChange( perPage ) {
|
||||
updateQueryString( { per_page: perPage } );
|
||||
/**
|
||||
* This function returns an event handler for the given `param`
|
||||
* @param {string} param The parameter in the querystring which should be updated (ex `page`, `per_page`)
|
||||
* @return {function} A callback which will update `param` to the passed value when called.
|
||||
*/
|
||||
onQueryChange( param ) {
|
||||
return value => updateQueryString( { [ param ]: value } );
|
||||
}
|
||||
|
||||
getRowsContent( data ) {
|
||||
getHeadersContent() {
|
||||
return [
|
||||
__( 'Select', 'wc-admin' ),
|
||||
__( 'Date', 'wc-admin' ),
|
||||
__( 'Orders', 'wc-admin' ),
|
||||
__( 'Gross Revenue', 'wc-admin' ),
|
||||
__( 'Refunds', 'wc-admin' ),
|
||||
__( 'Coupons', 'wc-admin' ),
|
||||
__( 'Taxes', 'wc-admin' ),
|
||||
__( 'Shipping', 'wc-admin' ),
|
||||
__( 'Net Revenue', 'wc-admin' ),
|
||||
];
|
||||
}
|
||||
|
||||
getRowsContent( data = [] ) {
|
||||
return map( data, row => {
|
||||
const {
|
||||
coupons,
|
||||
|
@ -78,34 +95,81 @@ class RevenueReport extends Component {
|
|||
</a>
|
||||
);
|
||||
return [
|
||||
row.start_date,
|
||||
orderLink,
|
||||
formatCurrency( gross_revenue ),
|
||||
formatCurrency( refunds ),
|
||||
formatCurrency( coupons ),
|
||||
formatCurrency( taxes ),
|
||||
formatCurrency( shipping ),
|
||||
formatCurrency( net_revenue ),
|
||||
{
|
||||
display: <input type="checkbox" />,
|
||||
value: false,
|
||||
},
|
||||
{
|
||||
display: formatDate( 'm/d/Y', row.date_start ),
|
||||
value: row.date_start,
|
||||
},
|
||||
{
|
||||
display: orderLink,
|
||||
value: Number( orders_count ),
|
||||
},
|
||||
{
|
||||
display: formatCurrency( gross_revenue ),
|
||||
value: getCurrencyFormatDecimal( gross_revenue ),
|
||||
},
|
||||
{
|
||||
display: formatCurrency( refunds ),
|
||||
value: getCurrencyFormatDecimal( refunds ),
|
||||
},
|
||||
{
|
||||
display: formatCurrency( coupons ),
|
||||
value: getCurrencyFormatDecimal( coupons ),
|
||||
},
|
||||
{
|
||||
display: formatCurrency( taxes ),
|
||||
value: getCurrencyFormatDecimal( taxes ),
|
||||
},
|
||||
{
|
||||
display: formatCurrency( shipping ),
|
||||
value: getCurrencyFormatDecimal( shipping ),
|
||||
},
|
||||
{
|
||||
display: formatCurrency( net_revenue ),
|
||||
value: getCurrencyFormatDecimal( net_revenue ),
|
||||
},
|
||||
];
|
||||
} );
|
||||
}
|
||||
|
||||
getSummaryContent( data = {} ) {
|
||||
return [
|
||||
{
|
||||
label: __( 'gross revenue', 'wc-admin' ),
|
||||
value: formatCurrency( data.gross_revenue ),
|
||||
},
|
||||
{
|
||||
label: __( 'refunds', 'wc-admin' ),
|
||||
value: formatCurrency( data.refunds ),
|
||||
},
|
||||
{
|
||||
label: __( 'coupons', 'wc-admin' ),
|
||||
value: formatCurrency( data.coupons ),
|
||||
},
|
||||
{
|
||||
label: __( 'taxes', 'wc-admin' ),
|
||||
value: formatCurrency( data.taxes ),
|
||||
},
|
||||
{
|
||||
label: __( 'shipping', 'wc-admin' ),
|
||||
value: formatCurrency( data.shipping ),
|
||||
},
|
||||
{
|
||||
label: __( 'net revenue', 'wc-admin' ),
|
||||
value: formatCurrency( data.net_revenue ),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
render() {
|
||||
const { path, query } = this.props;
|
||||
const summaryStats = get( this.state.stats, 'totals', {} );
|
||||
const intervalStats = get( this.state.stats, 'intervals', [] );
|
||||
const rows = this.getRowsContent( intervalStats ) || [];
|
||||
|
||||
const headers = [
|
||||
__( 'Date', 'wc-admin' ),
|
||||
__( 'Orders', 'wc-admin' ),
|
||||
__( 'Gross Revenue', 'wc-admin' ),
|
||||
__( 'Refunds', 'wc-admin' ),
|
||||
__( 'Coupons', 'wc-admin' ),
|
||||
__( 'Taxes', 'wc-admin' ),
|
||||
__( 'Shipping', 'wc-admin' ),
|
||||
__( 'Net Revenue', 'wc-admin' ),
|
||||
];
|
||||
const { totals = {}, intervals = [] } = this.state.stats;
|
||||
const summary = this.getSummaryContent( totals ) || [];
|
||||
const rows = this.getRowsContent( intervals ) || [];
|
||||
const headers = this.getHeadersContent();
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
|
@ -119,40 +183,36 @@ class RevenueReport extends Component {
|
|||
|
||||
<SummaryList>
|
||||
<SummaryNumber
|
||||
value={ formatCurrency( summaryStats.gross_revenue ) }
|
||||
value={ formatCurrency( totals.gross_revenue ) }
|
||||
label={ __( 'Gross Revenue', 'wc-admin' ) }
|
||||
delta={ 29 }
|
||||
/>
|
||||
<SummaryNumber
|
||||
value={ formatCurrency( summaryStats.refunds ) }
|
||||
value={ formatCurrency( totals.refunds ) }
|
||||
label={ __( 'Refunds', 'wc-admin' ) }
|
||||
delta={ -10 }
|
||||
selected
|
||||
/>
|
||||
<SummaryNumber
|
||||
value={ formatCurrency( summaryStats.coupons ) }
|
||||
value={ formatCurrency( totals.coupons ) }
|
||||
label={ __( 'Coupons', 'wc-admin' ) }
|
||||
delta={ 15 }
|
||||
/>
|
||||
<SummaryNumber
|
||||
value={ formatCurrency( summaryStats.taxes ) }
|
||||
value={ formatCurrency( totals.taxes ) }
|
||||
label={ __( 'Taxes', 'wc-admin' ) }
|
||||
/>
|
||||
</SummaryList>
|
||||
|
||||
<Card title={ __( 'Gross Revenue' ) }>
|
||||
<p>Graph here</p>
|
||||
<hr />
|
||||
{ /* @todo Switch a placeholder view if we don't have rows */ }
|
||||
<Table rows={ rows } headers={ headers } caption={ __( 'Revenue Last Week' ) } />
|
||||
</Card>
|
||||
|
||||
<Pagination
|
||||
page={ parseInt( query.page ) || 1 }
|
||||
perPage={ parseInt( query.per_page ) || 25 }
|
||||
total={ 5000 }
|
||||
onPageChange={ this.onPageChange }
|
||||
onPerPageChange={ this.onPerPageChange }
|
||||
<TableCard
|
||||
title={ __( 'Revenue Last Week', 'wc-admin' ) }
|
||||
rows={ rows }
|
||||
rowHeader={ 1 }
|
||||
headers={ headers }
|
||||
onClickDownload={ noop }
|
||||
onQueryChange={ this.onQueryChange }
|
||||
query={ query }
|
||||
summary={ summary }
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
|
|
|
@ -1,19 +1,28 @@
|
|||
Table
|
||||
=====
|
||||
`Table` Components
|
||||
==================
|
||||
|
||||
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.
|
||||
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).
|
||||
|
||||
`TableCard` serves as Card wrapper & contains a card header, `<Table />`, `<TableSummary />`, and `<Pagination />`. This includes filtering and comparison functionality for report pages.
|
||||
|
||||
`Table` itself can be used outside of the Card + filtering context for displaying any tabular data.
|
||||
|
||||
`TableSummary` can also be used alone, and will display the list of data passed in on a single line.
|
||||
|
||||
## How to use:
|
||||
|
||||
```jsx
|
||||
import Table from 'components/table';
|
||||
import { TableCard } from 'components/table';
|
||||
|
||||
render: function() {
|
||||
return (
|
||||
<Table
|
||||
<TableCard
|
||||
title="Revenue Last Week"
|
||||
rows={ rows }
|
||||
headers={ headers }
|
||||
caption={ __( 'Revenue Last Week' ) }
|
||||
onQueryChange={ this.onQueryChange }
|
||||
query={ query }
|
||||
summary={ summary }
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -21,28 +30,61 @@ render: function() {
|
|||
|
||||
## Props
|
||||
|
||||
### `TableCard` props
|
||||
|
||||
* `headers`: An array of column headers
|
||||
* `onQueryChange`: A function which returns a callback function to update the query string for a given `param`.
|
||||
* `onClickDownload`: A callback function which handles then "download" button press. Optional, if not used, the button won't appear.
|
||||
* `query`: An object of the query parameters passed to the page, ex `{ page: 2, per_page: 5 }`.
|
||||
* `rows` (required): An array of arrays of display/value object pairs. `display` is used for rendering, strings or elements are best here. `value` is used for sorting, and should be a string or number. A column with `false` value will not be sortable.
|
||||
* `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.
|
||||
* `summary`: An array of objects with `label` & `value` properties, which display in a line under the table. Optional, can be left off to show no summary.
|
||||
* `title` (required): The title used in the card header, also used as the caption for the content in this table
|
||||
|
||||
|
||||
`rows`, `headers`, `rowHeader`, and `title` are passed through to `<Table />`. `summary` is passed through as `data` to `<TableSummary />`. `query.page`, `query.per_page`, and `onQueryChange` are passed through to `<Pagination />`.
|
||||
|
||||
### `Table` 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.
|
||||
|
||||
### `TableSummary` props
|
||||
|
||||
* `data`: An array of objects with `label` & `value` properties, which display on a single line.
|
||||
|
||||
## 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.
|
||||
|
||||
Each row-cell should be an object with a `display` and `value` property, to enable consistent sortability.
|
||||
|
||||
```js
|
||||
const headers = [ 'Month', 'Orders', 'Revenue' ];
|
||||
const rows = [
|
||||
[ 'January', 10, 530 ],
|
||||
[ 'February', 13, 675 ],
|
||||
[ 'March', 9, 460 ],
|
||||
[
|
||||
{ display: 'January', value: 1 },
|
||||
{ display: 10, value: 10 },
|
||||
{ display: '$530.00', value: 530 },
|
||||
],
|
||||
[
|
||||
{ display: 'February', value: 2 },
|
||||
{ display: 13, value: 13 },
|
||||
{ display: '$675.00', value: 675 },
|
||||
],
|
||||
[
|
||||
{ display: 'March', value: 3 },
|
||||
{ display: 9, value: 9 },
|
||||
{ display: '$460.00', value: 460 },
|
||||
],
|
||||
]
|
||||
```
|
||||
|
||||
| Month | Orders | Revenue |
|
||||
| ---------|--------|---------|
|
||||
| January | 10 | 530 |
|
||||
| February | 13 | 675 |
|
||||
| March | 9 | 460 |
|
||||
| January | 10 | $530.00 |
|
||||
| February | 13 | $675.00 |
|
||||
| March | 9 | $460.00 |
|
||||
|
|
|
@ -2,164 +2,99 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __, sprintf } from '@wordpress/i18n';
|
||||
import { Component, createRef } from '@wordpress/element';
|
||||
import classnames from 'classnames';
|
||||
import { IconButton } from '@wordpress/components';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { IconButton, ToggleControl } from '@wordpress/components';
|
||||
import { noop } from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import { isEqual, uniqueId } from 'lodash';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import './style.scss';
|
||||
import Card from 'components/card';
|
||||
import { EllipsisMenu, MenuItem, MenuTitle } from 'components/ellipsis-menu';
|
||||
import Pagination from 'components/pagination';
|
||||
import Table from './table';
|
||||
import TableSummary from './summary';
|
||||
|
||||
const ASC = 'ascending';
|
||||
const DESC = 'descending';
|
||||
// @todo Handle toggling columns
|
||||
|
||||
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-' );
|
||||
}
|
||||
const TableCard = ( {
|
||||
headers,
|
||||
onClickDownload,
|
||||
onQueryChange,
|
||||
query,
|
||||
rows,
|
||||
rowHeader,
|
||||
summary,
|
||||
title,
|
||||
} ) => {
|
||||
return (
|
||||
<Card
|
||||
className="woocommerce-table"
|
||||
title={ title }
|
||||
action={
|
||||
onClickDownload && (
|
||||
<IconButton onClick={ onClickDownload } icon="arrow-down" size={ 18 } isDefault>
|
||||
{ __( 'Download', 'wc-admin' ) }
|
||||
</IconButton>
|
||||
)
|
||||
}
|
||||
menu={
|
||||
<EllipsisMenu label={ __( 'Choose which values to display', 'wc-admin' ) }>
|
||||
<MenuTitle>{ __( 'Columns:', 'wc-admin' ) }</MenuTitle>
|
||||
{ headers.map( ( label, i ) => (
|
||||
<MenuItem key={ i } onInvoke={ noop }>
|
||||
<ToggleControl label={ label } checked={ true } onChange={ noop } />
|
||||
</MenuItem>
|
||||
) ) }
|
||||
</EllipsisMenu>
|
||||
}
|
||||
>
|
||||
{ /* @todo Switch a placeholder view if we don't have rows */ }
|
||||
<Table rows={ rows } headers={ headers } rowHeader={ rowHeader } caption={ title } />
|
||||
|
||||
componentDidUpdate( prevProps ) {
|
||||
if ( ! isEqual( this.props.rows, prevProps.rows ) ) {
|
||||
/* eslint-disable react/no-did-update-set-state */
|
||||
this.setState( {
|
||||
rows: this.props.rows,
|
||||
} );
|
||||
/* eslint-enable react/no-did-update-set-state */
|
||||
}
|
||||
}
|
||||
{ summary && <TableSummary data={ summary } /> }
|
||||
|
||||
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 */
|
||||
}
|
||||
<Pagination
|
||||
page={ parseInt( query.page ) || 1 }
|
||||
perPage={ parseInt( query.per_page ) || 25 }
|
||||
total={ 5000 }
|
||||
onPageChange={ onQueryChange( 'page' ) }
|
||||
onPerPageChange={ onQueryChange( 'per_page' ) }
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
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,
|
||||
};
|
||||
} );
|
||||
}
|
||||
|
||||
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)', 'wc-admin' ) }</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', 'wc-admin' ), header )
|
||||
: sprintf( __( 'Sort by %s in descending order', 'wc-admin' ), 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,
|
||||
TableCard.propTypes = {
|
||||
headers: PropTypes.arrayOf( PropTypes.node ),
|
||||
rows: PropTypes.arrayOf( PropTypes.arrayOf( PropTypes.node ) ).isRequired,
|
||||
onQueryChange: PropTypes.func,
|
||||
onClickDownload: PropTypes.func,
|
||||
query: PropTypes.object,
|
||||
rowHeader: PropTypes.oneOfType( [ PropTypes.number, PropTypes.bool ] ),
|
||||
sortable: PropTypes.bool,
|
||||
rows: PropTypes.arrayOf(
|
||||
PropTypes.arrayOf(
|
||||
PropTypes.shape( {
|
||||
display: PropTypes.node,
|
||||
value: PropTypes.oneOfType( [ PropTypes.string, PropTypes.number, PropTypes.bool ] ),
|
||||
} )
|
||||
)
|
||||
).isRequired,
|
||||
summary: PropTypes.arrayOf(
|
||||
PropTypes.shape( {
|
||||
label: PropTypes.node,
|
||||
value: PropTypes.oneOfType( [ PropTypes.string, PropTypes.number ] ),
|
||||
} )
|
||||
),
|
||||
title: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
Table.defaultProps = {
|
||||
headers: [],
|
||||
TableCard.defaultProps = {
|
||||
onQueryChange: noop,
|
||||
query: {},
|
||||
rowHeader: 0,
|
||||
sortable: true,
|
||||
rows: [],
|
||||
};
|
||||
|
||||
export default Table;
|
||||
export { TableCard, Table, TableSummary };
|
||||
|
|
|
@ -1,26 +1,36 @@
|
|||
/** @format */
|
||||
|
||||
.woocommerce-table {
|
||||
overflow-x: auto;
|
||||
background: white;
|
||||
.woocommerce-card__body {
|
||||
padding: 0;
|
||||
padding-bottom: $gap;
|
||||
}
|
||||
|
||||
.woocommerce-card__action,
|
||||
.woocommerce-card__menu {
|
||||
justify-self: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
.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%;
|
||||
overflow-x: auto;
|
||||
margin-bottom: $gap;
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.woocommerce-table__header,
|
||||
.woocommerce-table__item {
|
||||
padding: $spacing/2;
|
||||
border-bottom: 1px solid $gray;
|
||||
padding: $gap $gap-large;
|
||||
border-bottom: 1px solid $core-grey-light-500;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
|
@ -31,15 +41,76 @@ th.woocommerce-table__item {
|
|||
}
|
||||
|
||||
.woocommerce-table__header {
|
||||
padding: $gap-smaller $gap-large;
|
||||
background-color: #f8f9fa;
|
||||
border-bottom: 1px solid $core-grey-light-700;
|
||||
|
||||
& + .woocommerce-table__header {
|
||||
border-left: 1px solid $gray;
|
||||
border-left: 1px solid $core-grey-light-700;
|
||||
}
|
||||
|
||||
.components-icon-button {
|
||||
margin-right: 5px;
|
||||
padding: 0;
|
||||
.components-button.is-button {
|
||||
padding: $gap-smaller $gap $gap-smaller 0;
|
||||
height: auto;
|
||||
width: 100%;
|
||||
vertical-align: middle;
|
||||
line-height: 1;
|
||||
border: none;
|
||||
background: transparent;
|
||||
box-shadow: none !important;
|
||||
|
||||
// @todo Add interactive styles
|
||||
&:hover {
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
&:active {
|
||||
box-shadow: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-sortable {
|
||||
padding: 0;
|
||||
|
||||
.gridicon {
|
||||
visibility: hidden;
|
||||
margin-left: $gap-smallest;
|
||||
}
|
||||
|
||||
&.is-sorted .components-button,
|
||||
.components-button:hover,
|
||||
.components-button:focus {
|
||||
.gridicon {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.woocommerce-table__summary {
|
||||
margin: $gap 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.woocommerce-table__summary-item {
|
||||
display: inline-block;
|
||||
margin-bottom: 0;
|
||||
|
||||
.woocommerce-table__summary-label,
|
||||
.woocommerce-table__summary-value {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.woocommerce-table__summary-label {
|
||||
margin-left: $gap-smallest;
|
||||
}
|
||||
|
||||
.woocommerce-table__summary-value {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
& + .woocommerce-table__summary-item:before {
|
||||
content: '/';
|
||||
margin: 0 $gap-smallest;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
/** @format */
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const TableSummary = ( { data } ) => {
|
||||
return (
|
||||
<ul className="woocommerce-table__summary">
|
||||
{ data.map( ( { label, value }, i ) => (
|
||||
<li className="woocommerce-table__summary-item" key={ i }>
|
||||
<span className="woocommerce-table__summary-value">{ value }</span>
|
||||
<span className="woocommerce-table__summary-label">{ label }</span>
|
||||
</li>
|
||||
) ) }
|
||||
</ul>
|
||||
);
|
||||
};
|
||||
|
||||
TableSummary.propTypes = {
|
||||
data: PropTypes.array,
|
||||
};
|
||||
|
||||
export default TableSummary;
|
|
@ -0,0 +1,183 @@
|
|||
/** @format */
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __, sprintf } from '@wordpress/i18n';
|
||||
import { Component, createRef } from '@wordpress/element';
|
||||
import classnames from 'classnames';
|
||||
import { IconButton } from '@wordpress/components';
|
||||
import Gridicon from 'gridicons';
|
||||
import PropTypes from 'prop-types';
|
||||
import { isEqual, uniqueId } from 'lodash';
|
||||
|
||||
const ASC = 'ascending';
|
||||
const DESC = 'descending';
|
||||
|
||||
const getDisplay = cell => cell.display || null;
|
||||
const getValue = cell => cell.value || 0;
|
||||
|
||||
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-' );
|
||||
}
|
||||
|
||||
componentDidUpdate( prevProps ) {
|
||||
if ( ! isEqual( this.props.rows, prevProps.rows ) ) {
|
||||
/* eslint-disable react/no-did-update-set-state */
|
||||
this.setState( {
|
||||
rows: this.props.rows,
|
||||
} );
|
||||
/* eslint-enable react/no-did-update-set-state */
|
||||
}
|
||||
}
|
||||
|
||||
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 { rows: [ first ] } = this.props;
|
||||
if ( ! first || ! first[ col ] ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return false !== first[ col ].value;
|
||||
}
|
||||
|
||||
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
|
||||
? getValue( a[ col ] ) > getValue( b[ col ] )
|
||||
: getValue( a[ col ] ) < getValue( b[ col ] )
|
||||
),
|
||||
sortedBy: col,
|
||||
sortDir,
|
||||
};
|
||||
} );
|
||||
}
|
||||
|
||||
render() {
|
||||
const { caption, classNames, headers, rowHeader } = this.props;
|
||||
const { rows, sortedBy, sortDir, tabIndex } = this.state;
|
||||
const classes = classnames( 'woocommerce-table__table', classNames );
|
||||
|
||||
return (
|
||||
<div
|
||||
className={ classes }
|
||||
ref={ this.container }
|
||||
tabIndex={ tabIndex }
|
||||
aria-labelledby={ this.captionID }
|
||||
role="group"
|
||||
>
|
||||
<table>
|
||||
<caption id={ this.captionID } className="woocommerce-table__caption screen-reader-text">
|
||||
{ caption }
|
||||
{ tabIndex === '0' && <small>{ __( '(scroll to see more)', 'wc-admin' ) }</small> }
|
||||
</caption>
|
||||
<tbody>
|
||||
<tr>
|
||||
{ headers.map( ( header, i ) => {
|
||||
const isSortable = this.isColSortable( i );
|
||||
return (
|
||||
<th
|
||||
role="columnheader"
|
||||
scope="col"
|
||||
key={ i }
|
||||
aria-sort={ sortedBy === i ? sortDir : 'none' }
|
||||
className={ classnames( 'woocommerce-table__header', {
|
||||
'is-sortable': isSortable,
|
||||
'is-sorted': sortedBy === i,
|
||||
} ) }
|
||||
>
|
||||
{ isSortable ? (
|
||||
<IconButton
|
||||
icon={
|
||||
sortDir === ASC ? (
|
||||
<Gridicon size={ 18 } icon="chevron-up" />
|
||||
) : (
|
||||
<Gridicon size={ 18 } icon="chevron-down" />
|
||||
)
|
||||
}
|
||||
label={
|
||||
sortDir !== ASC
|
||||
? sprintf( __( 'Sort by %s in ascending order', 'wc-admin' ), header )
|
||||
: sprintf( __( 'Sort by %s in descending order', 'wc-admin' ), header )
|
||||
}
|
||||
onClick={ () => this.sortBy( i ) }
|
||||
isDefault
|
||||
>
|
||||
{ header }
|
||||
</IconButton>
|
||||
) : (
|
||||
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">
|
||||
{ getDisplay( cell ) }
|
||||
</th>
|
||||
) : (
|
||||
<td key={ j } className="woocommerce-table__item">
|
||||
{ getDisplay( 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.shape( {
|
||||
display: PropTypes.node,
|
||||
value: PropTypes.oneOfType( [ PropTypes.string, PropTypes.number, PropTypes.bool ] ),
|
||||
} )
|
||||
)
|
||||
).isRequired,
|
||||
rowHeader: PropTypes.oneOfType( [ PropTypes.number, PropTypes.bool ] ),
|
||||
};
|
||||
|
||||
Table.defaultProps = {
|
||||
headers: [],
|
||||
rowHeader: 0,
|
||||
};
|
||||
|
||||
export default Table;
|
|
@ -5,6 +5,7 @@ $gutter: var(--main-gap);
|
|||
$gap-large: 24px;
|
||||
$gap: 16px;
|
||||
$gap-small: 12px;
|
||||
$gap-smaller: 8px;
|
||||
$gap-smallest: 4px;
|
||||
|
||||
// @todo remove this spacing variable
|
||||
|
|
Loading…
Reference in New Issue