Component – Table: Add compare checkboxes (https://github.com/woocommerce/woocommerce-admin/pull/389)
* Switch to withInstanceId higher order component * Add checkboxes to the rows in a TableCard, if a compareBy prop is set * Add Compare button to update query param * Populate the selected rows from the query parameter * Update compare filter display & selected table rows when the query changes * Skip displaying tags if the label is null/undefined * Style table header with compare button, search placeholder * Prevent setting just an ID list as the state, as this will wipe out already-fetched tag labels * Update docs * Shortcut out of fetching tag labels if the query is empty
This commit is contained in:
parent
7ecd6d160b
commit
f2e0165d5f
|
@ -11,8 +11,11 @@ import apiFetch from '@wordpress/api-fetch';
|
|||
import { getIdsFromQuery, stringifyQuery } from 'lib/nav-utils';
|
||||
import { NAMESPACE } from 'store/constants';
|
||||
|
||||
export function getProductLabelsById( queryString ) {
|
||||
export function getProductLabelsById( queryString = '' ) {
|
||||
const idList = getIdsFromQuery( queryString );
|
||||
if ( idList.length < 1 ) {
|
||||
return Promise.resolve( [] );
|
||||
}
|
||||
const payload = stringifyQuery( {
|
||||
include: idList.join( ',' ),
|
||||
per_page: idList.length,
|
||||
|
@ -20,8 +23,11 @@ export function getProductLabelsById( queryString ) {
|
|||
return apiFetch( { path: `${ NAMESPACE }products${ payload }` } );
|
||||
}
|
||||
|
||||
export function getCategoryLabelsById( queryString ) {
|
||||
export function getCategoryLabelsById( queryString = '' ) {
|
||||
const idList = getIdsFromQuery( queryString );
|
||||
if ( idList.length < 1 ) {
|
||||
return Promise.resolve( [] );
|
||||
}
|
||||
const payload = stringifyQuery( {
|
||||
include: idList.join( ',' ),
|
||||
per_page: idList.length,
|
||||
|
|
|
@ -139,6 +139,8 @@ export default class extends Component {
|
|||
totalRows={ 500 }
|
||||
rowsPerPage={ rowsPerPage }
|
||||
headers={ headers }
|
||||
compareBy={ 'product' }
|
||||
ids={ products.map( p => p.product_id ) }
|
||||
onClickDownload={ noop }
|
||||
onQueryChange={ onQueryChange }
|
||||
query={ tableQuery }
|
||||
|
|
|
@ -22,15 +22,15 @@
|
|||
align-items: center;
|
||||
|
||||
.has-action & {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-template-columns: auto 1fr;
|
||||
}
|
||||
|
||||
.has-menu & {
|
||||
grid-template-columns: 1fr 24px;
|
||||
grid-template-columns: auto 24px;
|
||||
}
|
||||
|
||||
.has-menu.has-action & {
|
||||
grid-template-columns: 1fr 1fr 48px;
|
||||
grid-template-columns: auto 1fr 48px;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -52,7 +52,7 @@
|
|||
}
|
||||
|
||||
.woocommerce-card__title {
|
||||
margin: 0;
|
||||
margin: 0 $gap 0 0;
|
||||
// EllipsisMenu is 24px, so to match we add 6px padding around the
|
||||
// heading text, which we know is 18px from line-height.
|
||||
padding: 3px 0;
|
||||
|
|
|
@ -4,14 +4,15 @@
|
|||
*/
|
||||
import { Button } from '@wordpress/components';
|
||||
import { Component } from '@wordpress/element';
|
||||
import { isEqual } from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import Card from 'components/card';
|
||||
import { getIdsFromQuery, updateQueryString } from 'lib/nav-utils';
|
||||
import Search from 'components/search';
|
||||
import { updateQueryString } from 'lib/nav-utils';
|
||||
|
||||
/**
|
||||
* Displays a card + search used to filter results as a comparison between objects.
|
||||
|
@ -31,12 +32,20 @@ class CompareFilter extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
componentDidUpdate( { param } ) {
|
||||
if ( param !== this.props.param ) {
|
||||
componentDidUpdate( { param: prevParam, query: prevQuery } ) {
|
||||
const { getLabels, param, path, query } = this.props;
|
||||
if ( prevParam !== param ) {
|
||||
/* eslint-disable react/no-did-update-set-state */
|
||||
this.setState( { selected: [] } );
|
||||
updateQueryString( { [ param ]: '' }, this.props.path, this.props.query );
|
||||
/* eslint-enable react/no-did-update-set-state */
|
||||
updateQueryString( { [ param ]: '' }, path, query );
|
||||
return;
|
||||
}
|
||||
|
||||
const prevIds = getIdsFromQuery( prevQuery[ param ] );
|
||||
const currentIds = getIdsFromQuery( query[ param ] );
|
||||
if ( ! isEqual( prevIds.sort(), currentIds.sort() ) ) {
|
||||
getLabels( query[ param ] ).then( this.updateLabels );
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -3,9 +3,10 @@
|
|||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { Button, IconButton, ToggleControl } from '@wordpress/components';
|
||||
import classnames from 'classnames';
|
||||
import { Component } from '@wordpress/element';
|
||||
import { IconButton, ToggleControl } from '@wordpress/components';
|
||||
import { fill, find, findIndex, first, noop } from 'lodash';
|
||||
import { fill, find, findIndex, first, isEqual, noop, partial, uniq } from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
/**
|
||||
|
@ -14,6 +15,7 @@ import PropTypes from 'prop-types';
|
|||
import './style.scss';
|
||||
import Card from 'components/card';
|
||||
import EllipsisMenu from 'components/ellipsis-menu';
|
||||
import { getIdsFromQuery } from 'lib/nav-utils';
|
||||
import MenuItem from 'components/ellipsis-menu/menu-item';
|
||||
import MenuTitle from 'components/ellipsis-menu/menu-title';
|
||||
import Pagination from 'components/pagination';
|
||||
|
@ -31,10 +33,26 @@ import TableSummary from './summary';
|
|||
class TableCard extends Component {
|
||||
constructor( props ) {
|
||||
super( props );
|
||||
const { compareBy, query } = props;
|
||||
this.state = {
|
||||
showCols: fill( Array( props.headers.length ), true ),
|
||||
selectedRows: getIdsFromQuery( query[ compareBy ] ),
|
||||
};
|
||||
this.toggleCols = this.toggleCols.bind( this );
|
||||
this.onCompare = this.onCompare.bind( this );
|
||||
this.selectRow = this.selectRow.bind( this );
|
||||
this.selectAllRows = this.selectAllRows.bind( this );
|
||||
}
|
||||
|
||||
componentDidUpdate( { query: prevQuery } ) {
|
||||
const { compareBy, query } = this.props;
|
||||
const prevIds = getIdsFromQuery( prevQuery[ compareBy ] );
|
||||
const currentIds = getIdsFromQuery( query[ compareBy ] );
|
||||
if ( ! isEqual( prevIds.sort(), currentIds.sort() ) ) {
|
||||
/* eslint-disable react/no-did-update-set-state */
|
||||
this.setState( { selectedRows: currentIds } );
|
||||
/* eslint-enable react/no-did-update-set-state */
|
||||
}
|
||||
}
|
||||
|
||||
toggleCols( selected ) {
|
||||
|
@ -57,6 +75,14 @@ class TableCard extends Component {
|
|||
};
|
||||
}
|
||||
|
||||
onCompare() {
|
||||
const { compareBy, onQueryChange } = this.props;
|
||||
const { selectedRows } = this.state;
|
||||
if ( compareBy ) {
|
||||
onQueryChange( 'compare' )( compareBy, selectedRows.join( ',' ) );
|
||||
}
|
||||
}
|
||||
|
||||
filterCols( rows = [] ) {
|
||||
const { showCols } = this.state;
|
||||
// Header is a 1d array
|
||||
|
@ -67,33 +93,119 @@ class TableCard extends Component {
|
|||
return rows.map( row => row.filter( ( col, i ) => showCols[ i ] ) );
|
||||
}
|
||||
|
||||
selectAllRows( event ) {
|
||||
const { ids } = this.props;
|
||||
if ( event.target.checked ) {
|
||||
this.setState( {
|
||||
selectedRows: ids,
|
||||
} );
|
||||
} else {
|
||||
this.setState( {
|
||||
selectedRows: [],
|
||||
} );
|
||||
}
|
||||
}
|
||||
|
||||
selectRow( i, event ) {
|
||||
const { ids } = this.props;
|
||||
if ( event.target.checked ) {
|
||||
this.setState( ( { selectedRows } ) => ( {
|
||||
selectedRows: uniq( [ ids[ i ], ...selectedRows ] ),
|
||||
} ) );
|
||||
} else {
|
||||
this.setState( ( { selectedRows } ) => {
|
||||
const index = selectedRows.indexOf( ids[ i ] );
|
||||
return {
|
||||
selectedRows: [ ...selectedRows.slice( 0, index ), ...selectedRows.slice( index + 1 ) ],
|
||||
};
|
||||
} );
|
||||
}
|
||||
}
|
||||
|
||||
getCheckbox( i ) {
|
||||
const { ids = [] } = this.props;
|
||||
const { selectedRows } = this.state;
|
||||
const isChecked = -1 !== selectedRows.indexOf( ids[ i ] );
|
||||
return {
|
||||
display: (
|
||||
<input type="checkbox" onChange={ partial( this.selectRow, i ) } checked={ isChecked } />
|
||||
),
|
||||
value: false,
|
||||
};
|
||||
}
|
||||
|
||||
getAllCheckbox() {
|
||||
const { ids = [] } = this.props;
|
||||
const { selectedRows } = this.state;
|
||||
const isAllChecked = ids.length === selectedRows.length;
|
||||
return {
|
||||
label: (
|
||||
<input
|
||||
type="checkbox"
|
||||
onChange={ this.selectAllRows }
|
||||
aria-label={ __( 'Select All' ) }
|
||||
checked={ isAllChecked }
|
||||
/>
|
||||
),
|
||||
required: true,
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
compareBy,
|
||||
onClickDownload,
|
||||
onQueryChange,
|
||||
query,
|
||||
rowHeader,
|
||||
rowsPerPage,
|
||||
summary,
|
||||
title,
|
||||
totalRows,
|
||||
rowsPerPage,
|
||||
} = this.props;
|
||||
const { showCols } = this.state;
|
||||
const allHeaders = this.props.headers;
|
||||
const headers = this.filterCols( this.props.headers );
|
||||
const rows = this.filterCols( this.props.rows );
|
||||
let headers = this.filterCols( this.props.headers );
|
||||
let rows = this.filterCols( this.props.rows );
|
||||
if ( compareBy ) {
|
||||
rows = rows.map( ( row, i ) => {
|
||||
return [ this.getCheckbox( i ), ...row ];
|
||||
} );
|
||||
headers = [ this.getAllCheckbox(), ...headers ];
|
||||
}
|
||||
|
||||
const className = classnames( {
|
||||
'woocommerce-table': true,
|
||||
'has-compare': !! compareBy,
|
||||
} );
|
||||
|
||||
return (
|
||||
<Card
|
||||
className="woocommerce-table"
|
||||
className={ className }
|
||||
title={ title }
|
||||
action={
|
||||
action={ [
|
||||
compareBy && (
|
||||
<Button key="compare" onClick={ this.onCompare } isDefault>
|
||||
{ __( 'Compare', 'wc-admin' ) }
|
||||
</Button>
|
||||
),
|
||||
compareBy && (
|
||||
<div key="search" style={ { padding: '4px 12px', color: '#6c7781' } }>
|
||||
Placeholder for search
|
||||
</div>
|
||||
),
|
||||
onClickDownload && (
|
||||
<IconButton onClick={ onClickDownload } icon="arrow-down" size={ 18 } isDefault>
|
||||
<IconButton
|
||||
key="download"
|
||||
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>
|
||||
|
@ -114,7 +226,6 @@ class TableCard extends Component {
|
|||
</EllipsisMenu>
|
||||
}
|
||||
>
|
||||
{ /* @todo Switch a placeholder view if we don't have rows */ }
|
||||
<Table
|
||||
rows={ rows }
|
||||
headers={ headers }
|
||||
|
@ -139,6 +250,10 @@ class TableCard extends Component {
|
|||
}
|
||||
|
||||
TableCard.propTypes = {
|
||||
/**
|
||||
* The string to use as a query parameter when comparing row items.
|
||||
*/
|
||||
compareBy: PropTypes.string,
|
||||
/**
|
||||
* An array of column headers (see `Table` props).
|
||||
*/
|
||||
|
@ -151,6 +266,10 @@ TableCard.propTypes = {
|
|||
required: PropTypes.bool,
|
||||
} )
|
||||
),
|
||||
/**
|
||||
* A list of IDs, matching to the row list so that ids[ 0 ] contains the object ID for the object displayed in row[ 0 ].
|
||||
*/
|
||||
ids: PropTypes.arrayOf( PropTypes.number ),
|
||||
/**
|
||||
* A function which returns a callback function to update the query string for a given `param`.
|
||||
*/
|
||||
|
@ -178,6 +297,10 @@ TableCard.propTypes = {
|
|||
} )
|
||||
)
|
||||
).isRequired,
|
||||
/**
|
||||
* The total number of rows to display per page.
|
||||
*/
|
||||
rowsPerPage: PropTypes.number.isRequired,
|
||||
/**
|
||||
* 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.
|
||||
|
@ -192,8 +315,10 @@ TableCard.propTypes = {
|
|||
* The title used in the card header, also used as the caption for the content in this table.
|
||||
*/
|
||||
title: PropTypes.string.isRequired,
|
||||
/**
|
||||
* The total number of rows (across all pages).
|
||||
*/
|
||||
totalRows: PropTypes.number.isRequired,
|
||||
rowsPerPage: PropTypes.number.isRequired,
|
||||
};
|
||||
|
||||
TableCard.defaultProps = {
|
||||
|
|
|
@ -10,6 +10,15 @@
|
|||
.woocommerce-card__menu {
|
||||
justify-self: flex-end;
|
||||
}
|
||||
|
||||
&.has-compare {
|
||||
.woocommerce-card__action {
|
||||
text-align: left;
|
||||
display: grid;
|
||||
width: 100%;
|
||||
grid-template-columns: auto 1fr auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.woocommerce-table__caption {
|
||||
|
|
|
@ -6,9 +6,10 @@ import { __, sprintf } from '@wordpress/i18n';
|
|||
import { Component, createRef, Fragment } from '@wordpress/element';
|
||||
import classnames from 'classnames';
|
||||
import { IconButton } from '@wordpress/components';
|
||||
import { find, get, noop, uniqueId } from 'lodash';
|
||||
import { find, get, noop } from 'lodash';
|
||||
import Gridicon from 'gridicons';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withInstanceId } from '@wordpress/compose';
|
||||
|
||||
const ASC = 'asc';
|
||||
const DESC = 'desc';
|
||||
|
@ -26,8 +27,6 @@ class Table extends Component {
|
|||
};
|
||||
this.container = createRef();
|
||||
this.sortBy = this.sortBy.bind( this );
|
||||
this.headersID = uniqueId( 'header-' );
|
||||
this.captionID = uniqueId( 'caption-' );
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
|
@ -55,7 +54,16 @@ class Table extends Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
const { ariaHidden, caption, classNames, headers, query, rowHeader, rows } = this.props;
|
||||
const {
|
||||
ariaHidden,
|
||||
caption,
|
||||
classNames,
|
||||
headers,
|
||||
instanceId,
|
||||
query,
|
||||
rowHeader,
|
||||
rows,
|
||||
} = this.props;
|
||||
const { tabIndex } = this.state;
|
||||
const classes = classnames( 'woocommerce-table__table', classNames );
|
||||
const sortedBy = query.orderby || get( find( headers, { defaultSort: true } ), 'key', false );
|
||||
|
@ -67,11 +75,14 @@ class Table extends Component {
|
|||
ref={ this.container }
|
||||
tabIndex={ tabIndex }
|
||||
aria-hidden={ ariaHidden }
|
||||
aria-labelledby={ this.captionID }
|
||||
aria-labelledby={ `caption-${ instanceId }` }
|
||||
role="group"
|
||||
>
|
||||
<table>
|
||||
<caption id={ this.captionID } className="woocommerce-table__caption screen-reader-text">
|
||||
<caption
|
||||
id={ `caption-${ instanceId }` }
|
||||
className="woocommerce-table__caption screen-reader-text"
|
||||
>
|
||||
{ caption }
|
||||
{ tabIndex === '0' && <small>{ __( '(scroll to see more)', 'wc-admin' ) }</small> }
|
||||
</caption>
|
||||
|
@ -79,6 +90,7 @@ class Table extends Component {
|
|||
<tr>
|
||||
{ headers.map( ( header, i ) => {
|
||||
const { isSortable, isNumeric, key, label } = header;
|
||||
const labelId = `header-${ instanceId } -${ i }`;
|
||||
const thProps = {
|
||||
className: classnames( 'woocommerce-table__header', {
|
||||
'is-sortable': isSortable,
|
||||
|
@ -110,13 +122,13 @@ class Table extends Component {
|
|||
<Gridicon size={ 18 } icon="chevron-down" />
|
||||
)
|
||||
}
|
||||
aria-describedby={ `${ this.headersID }-${ i }` }
|
||||
aria-describedby={ labelId }
|
||||
onClick={ this.sortBy( key ) }
|
||||
isDefault
|
||||
>
|
||||
{ label }
|
||||
</IconButton>
|
||||
<span className="screen-reader-text" id={ `${ this.headersID }-${ i }` }>
|
||||
<span className="screen-reader-text" id={ labelId }>
|
||||
{ iconLabel }
|
||||
</span>
|
||||
</Fragment>
|
||||
|
@ -189,7 +201,7 @@ Table.propTypes = {
|
|||
/**
|
||||
* The display label for this column.
|
||||
*/
|
||||
label: PropTypes.string,
|
||||
label: PropTypes.node,
|
||||
/**
|
||||
* Boolean, true if this column should always display in the table (not shown in toggle-able list).
|
||||
*/
|
||||
|
@ -236,4 +248,4 @@ Table.defaultProps = {
|
|||
rowHeader: 0,
|
||||
};
|
||||
|
||||
export default Table;
|
||||
export default withInstanceId( Table );
|
||||
|
|
|
@ -22,6 +22,11 @@ import './style.scss';
|
|||
*/
|
||||
const Tag = ( { id, instanceId, label, remove, removeLabel, screenReaderLabel, className } ) => {
|
||||
screenReaderLabel = screenReaderLabel || label;
|
||||
if ( ! label ) {
|
||||
// A null label probably means something went wrong
|
||||
// @todo Maybe this should be a loading indicator?
|
||||
return null;
|
||||
}
|
||||
const classes = classnames( 'woocommerce-tag', className, {
|
||||
'has-remove': !! remove,
|
||||
} );
|
||||
|
|
Loading…
Reference in New Issue