Merge pull request woocommerce/woocommerce-admin#1085 from woocommerce/add/name-filter-autocompleter

Customers report: Name Advanced Filter
This commit is contained in:
Albert Juhé Lluveras 2018-12-19 10:39:22 +01:00 committed by GitHub
commit 776e8d31fe
10 changed files with 148 additions and 43 deletions

View File

@ -55,12 +55,11 @@ export const advancedFilters = {
},
],
input: {
// Use products autocompleter for now, see https://github.com/woocommerce/wc-admin/issues/1029 for progress
component: 'Search',
type: 'products',
getLabels: getRequestByIdString( NAMESPACE + 'products', product => ( {
id: product.id,
label: product.name,
type: 'customers',
getLabels: getRequestByIdString( NAMESPACE + 'customers', customer => ( {
id: customer.id,
label: [ customer.first_name, customer.last_name ].filter( Boolean ).join( ' ' ),
} ) ),
},
},

View File

@ -173,7 +173,6 @@ export default class CustomersReportTable extends Component {
return (
<ReportTable
compareBy="customers"
endpoint="customers"
extendItemsMethodNames={ {
load: 'getCustomers',
@ -184,6 +183,9 @@ export default class CustomersReportTable extends Component {
getRowsContent={ this.getRowsContent }
itemIdField="id"
query={ query }
labels={ { placeholder: __( 'Search by customer name', 'wc-admin' ) } }
searchBy="customers"
searchParam="name_includes"
title={ __( 'Registered Customers', 'wc-admin' ) }
columnPrefsKey="customers_report_columns"
/>

View File

@ -43,7 +43,9 @@ A placeholder for the search input.
- label: String
- Default: `[]`
An array of objects describing selected values.
An array of objects describing selected values. If the label of the selected
value is omitted, the Tag of that value will not be rendered inside the
search box.
### `inlineTags`

View File

@ -18,6 +18,13 @@ Props
The string to use as a query parameter when comparing row items.
### `compareParam`
- Type: String
- Default: `'filter'`
Url query parameter compare function operates on
### `headers`
- Type: Array
@ -118,6 +125,20 @@ Which column should be the row header, defaults to the first item (`0`) (see `Ta
The total number of rows to display per page.
### `searchBy`
- Type: String
- Default: null
The string to use as a query parameter when searching row items.
### `searchParam`
- Type: String
- Default: null
Url query parameter search function operates on
### `summary`
- Type: Array
@ -144,13 +165,6 @@ The title used in the card header, also used as the caption for the content in t
The total number of rows (across all pages).
### `compareParam`
- Type: String
- Default: `'filter'`
Url query parameter compare function operates on
`EmptyTable` (component)
========================

View File

@ -5,7 +5,7 @@
import { __ } from '@wordpress/i18n';
import { Component, createRef } from '@wordpress/element';
import { SelectControl, Button, Dropdown, IconButton } from '@wordpress/components';
import { partial, findIndex, difference } from 'lodash';
import { partial, findIndex, difference, isEqual } from 'lodash';
import PropTypes from 'prop-types';
import Gridicon from 'gridicons';
import interpolateComponents from 'interpolate-components';
@ -56,6 +56,19 @@ class AdvancedFilters extends Component {
this.getUpdateHref = this.getUpdateHref.bind( this );
}
componentDidUpdate( prevProps ) {
const { config, query } = this.props;
const { query: prevQuery } = prevProps;
if ( ! isEqual( prevQuery, query ) ) {
/* eslint-disable react/no-did-update-set-state */
this.setState( {
activeFilters: getActiveFiltersFromQuery( query, config.filters ),
} );
/* eslint-enable react/no-did-update-set-state */
}
}
onMatchChange( match ) {
this.setState( { match } );
}

View File

@ -4,7 +4,7 @@
*/
import { Component } from '@wordpress/element';
import { SelectControl } from '@wordpress/components';
import { find, partial } from 'lodash';
import { find, isEqual, partial } from 'lodash';
import PropTypes from 'prop-types';
import interpolateComponents from 'interpolate-components';
import classnames from 'classnames';
@ -29,6 +29,15 @@ class SearchFilter extends Component {
}
}
componentDidUpdate( prevProps ) {
const { config, filter, query } = this.props;
const { filter: prevFilter } = prevProps;
if ( filter.value.length && ! isEqual( prevFilter, filter ) ) {
config.input.getLabels( filter.value, query ).then( this.updateLabels );
}
}
updateLabels( selected ) {
this.setState( { selected } );
}

View File

@ -14,6 +14,8 @@ import { stringifyQuery } from '@woocommerce/navigation';
*/
import { computeSuggestionMatch } from './utils';
const getName = customer => [ customer.first_name, customer.last_name ].filter( Boolean ).join( ' ' );
/**
* A customer completer.
* See https://github.com/WordPress/gutenberg/tree/master/packages/components/src/autocomplete#the-completer-interface
@ -23,11 +25,11 @@ import { computeSuggestionMatch } from './utils';
export default {
name: 'customers',
className: 'woocommerce-search__customers-result',
options( search ) {
options( name ) {
let payload = '';
if ( search ) {
if ( name ) {
const query = {
search,
name,
per_page: 10,
};
payload = stringifyQuery( query );
@ -36,12 +38,12 @@ export default {
},
isDebounced: true,
getOptionKeywords( customer ) {
return [ customer.name ];
return [ getName( customer ) ];
},
getOptionLabel( customer, query ) {
const match = computeSuggestionMatch( customer.name, query ) || {};
const match = computeSuggestionMatch( getName( customer ), query ) || {};
return [
<span key="name" className="woocommerce-search__result-name" aria-label={ customer.name }>
<span key="name" className="woocommerce-search__result-name" aria-label={ getName( customer ) }>
{ match.suggestionBeforeMatch }
<strong className="components-form-token-field__suggestion-match">
{ match.suggestionMatch }
@ -53,10 +55,9 @@ export default {
// This is slightly different than gutenberg/Autocomplete, we don't support different methods
// of replace/insertion, so we can just return the value.
getOptionCompletion( customer ) {
const value = {
return {
id: customer.id,
label: customer.name,
label: getName( customer ),
};
return value;
},
};

View File

@ -89,11 +89,20 @@ class Search extends Component {
}
}
shouldRenderTags() {
const { selected } = this.props;
return selected.some( item => Boolean( item.label ) );
}
renderTags() {
const { selected } = this.props;
return selected.length ? (
return this.shouldRenderTags() ? (
<div className="woocommerce-search__token-list">
{ selected.map( ( item, i ) => {
if ( ! item.label ) {
return null;
}
const screenReaderLabel = sprintf(
__( '%1$s (%2$s of %3$s)', 'wc-admin' ),
item.label,
@ -130,6 +139,8 @@ class Search extends Component {
'aria-labelledby': this.props[ 'aria-labelledby' ],
'aria-label': this.props[ 'aria-label' ],
};
const shouldRenderTags = this.shouldRenderTags();
return (
<div className={ classnames( 'woocommerce-search', className ) }>
<Gridicon className="woocommerce-search__icon" icon="search" size={ 18 } />
@ -164,7 +175,7 @@ class Search extends Component {
value.length ) + 1
}
value={ value }
placeholder={ ( selected.length === 0 && placeholder ) || '' }
placeholder={ ( ! shouldRenderTags && placeholder ) || '' }
className="woocommerce-search__inline-input"
onChange={ this.updateSearch( onChange ) }
aria-owns={ listBoxId }
@ -172,7 +183,7 @@ class Search extends Component {
onFocus={ this.onFocus }
onBlur={ this.onBlur }
aria-describedby={
selected.length ? `search-inline-input-${ instanceId }` : null
shouldRenderTags ? `search-inline-input-${ instanceId }` : null
}
{ ...aria }
/>
@ -230,7 +241,9 @@ Search.propTypes = {
*/
placeholder: PropTypes.string,
/**
* An array of objects describing selected values.
* An array of objects describing selected values. If the label of the selected
* value is omitted, the Tag of that value will not be rendered inside the
* search box.
*/
selected: PropTypes.arrayOf(
PropTypes.shape( {
@ -238,7 +251,7 @@ Search.propTypes = {
PropTypes.number,
PropTypes.string,
] ).isRequired,
label: PropTypes.string.isRequired,
label: PropTypes.string,
} )
),
/**

View File

@ -17,7 +17,7 @@ import {
generateCSVDataFromTable,
generateCSVFileName,
} from '@woocommerce/csv-export';
import { getIdsFromQuery } from '@woocommerce/navigation';
import { getIdsFromQuery, updateQueryString } from '@woocommerce/navigation';
/**
* Internal dependencies
@ -147,16 +147,21 @@ class TableCard extends Component {
}
}
onSearch( value ) {
const { compareBy, compareParam, onQueryChange } = this.props;
const { selectedRows } = this.state;
onSearch( values ) {
const { compareBy, compareParam, onQueryChange, searchBy, searchParam } = this.props;
const ids = values.map( v => v.id );
if ( compareBy ) {
const ids = value.map( v => v.id );
const { selectedRows } = this.state;
onQueryChange( 'compare' )(
compareBy,
compareParam,
[ ...selectedRows, ...ids ].join( ',' )
);
} else if ( searchBy ) {
updateQueryString( {
filter: 'advanced',
[ searchParam ]: ids.join( ',' ),
} );
}
}
@ -230,6 +235,8 @@ class TableCard extends Component {
query,
rowHeader,
rowsPerPage,
searchBy,
searchParam,
summary,
title,
totalRows,
@ -248,6 +255,7 @@ class TableCard extends Component {
const className = classnames( {
'woocommerce-table': true,
'has-compare': !! compareBy,
'has-search': !! searchBy,
} );
return (
@ -268,12 +276,13 @@ class TableCard extends Component {
{ labels.compareButton || __( 'Compare', 'wc-admin' ) }
</CompareButton>
),
compareBy && (
( compareBy || searchBy ) && (
<Search
key="search"
placeholder={ labels.placeholder || __( 'Search by item name', 'wc-admin' ) }
type={ compareBy }
type={ compareBy || searchBy }
onChange={ this.onSearch }
selected={ searchParam && getIdsFromQuery( query[ searchParam ] ).map( id => ( { id } ) ) }
/>
),
( downloadable || onClickDownload ) && (
@ -350,6 +359,10 @@ TableCard.propTypes = {
* The string to use as a query parameter when comparing row items.
*/
compareBy: PropTypes.string,
/**
* Url query parameter compare function operates on
*/
compareParam: PropTypes.string,
/**
* An array of column headers (see `Table` props).
*/
@ -420,6 +433,14 @@ TableCard.propTypes = {
* The total number of rows to display per page.
*/
rowsPerPage: PropTypes.number.isRequired,
/**
* The string to use as a query parameter when searching row items.
*/
searchBy: PropTypes.string,
/**
* Url query parameter search function operates on
*/
searchParam: PropTypes.string,
/**
* 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.
@ -438,13 +459,10 @@ TableCard.propTypes = {
* The total number of rows (across all pages).
*/
totalRows: PropTypes.number.isRequired,
/**
* Url query parameter compare function operates on
*/
compareParam: PropTypes.string,
};
TableCard.defaultProps = {
compareParam: 'filter',
downloadable: false,
isLoading: false,
onQueryChange: noop,
@ -452,7 +470,6 @@ TableCard.defaultProps = {
query: {},
rowHeader: 0,
rows: [],
compareParam: 'filter',
};
export default TableCard;

View File

@ -19,7 +19,8 @@
justify-self: flex-end;
}
&.has-compare {
&.has-compare,
&.has-search {
.woocommerce-card__action {
align-items: center;
text-align: left;
@ -71,6 +72,40 @@
}
}
&.has-search {
.woocommerce-card__action {
grid-template-columns: 1fr auto;
.woocommerce-search {
align-self: center;
grid-column-start: 1;
grid-column-end: 2;
}
.woocommerce-table__download-button {
align-self: center;
grid-column-start: 2;
grid-column-end: 3;
}
}
@include breakpoint( '<960px' ) {
.woocommerce-card__action {
grid-area: 1 / 1 / 3 / 4;
grid-template-columns: auto 1fr 24px;
.woocommerce-search {
grid-area: 2 / 1 / 3 / 4;
margin-left: 0;
}
.woocommerce-table__download-button {
grid-area: 1 / 2 / 2 / 3;
}
}
}
}
.woocommerce-search {
margin: 0 $gap;
}