Merge pull request woocommerce/woocommerce-admin#1085 from woocommerce/add/name-filter-autocompleter
Customers report: Name Advanced Filter
This commit is contained in:
commit
776e8d31fe
|
@ -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( ' ' ),
|
||||
} ) ),
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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"
|
||||
/>
|
||||
|
|
|
@ -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`
|
||||
|
||||
|
|
|
@ -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)
|
||||
========================
|
||||
|
||||
|
|
|
@ -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 } );
|
||||
}
|
||||
|
|
|
@ -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 } );
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
} )
|
||||
),
|
||||
/**
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue