Merge pull request woocommerce/woocommerce-admin#386 from woocommerce/add/advanced-filters-search-getLabels

Advanced Filters: Add api fetch for Search results
This commit is contained in:
Paul Sealock 2018-09-14 11:17:59 +12:00 committed by GitHub
commit b125c75eef
9 changed files with 117 additions and 175 deletions

View File

@ -4,6 +4,11 @@
*/ */
import { __ } from '@wordpress/i18n'; import { __ } from '@wordpress/i18n';
/**
* Internal dependencies
*/
import { getProductLabelsById } from 'analytics/report/products/config';
export const filters = [ export const filters = [
{ label: __( 'All Orders', 'wc-admin' ), value: 'all' }, { label: __( 'All Orders', 'wc-admin' ), value: 'all' },
{ {
@ -55,6 +60,7 @@ export const advancedFilterConfig = {
input: { input: {
component: 'Search', component: 'Search',
type: 'products', type: 'products',
getLabels: getProductLabelsById,
}, },
}, },
code: { code: {

View File

@ -13,7 +13,7 @@ import { partial } from 'lodash';
* Internal dependencies * Internal dependencies
*/ */
import { Card, ReportFilters } from '@woocommerce/components'; import { Card, ReportFilters } from '@woocommerce/components';
import { filters, advancedFilterConfig } from './constants'; import { filters, advancedFilterConfig } from './config';
import './style.scss'; import './style.scss';
class OrdersReport extends Component { class OrdersReport extends Component {

View File

@ -9,6 +9,31 @@ import apiFetch from '@wordpress/api-fetch';
* Internal dependencies * Internal dependencies
*/ */
import { stringifyQuery } from 'lib/nav-utils'; import { stringifyQuery } from 'lib/nav-utils';
import { NAMESPACE } from 'store/constants';
export const getProductLabelsById = queryString => {
const idList = queryString
.split( ',' )
.map( id => parseInt( id, 10 ) )
.filter( Boolean );
const payload = stringifyQuery( {
include: idList.join( ',' ),
per_page: idList.length,
} );
return apiFetch( { path: NAMESPACE + 'products' + payload } );
};
export const getCategoryLabelsById = queryString => {
const idList = queryString
.split( ',' )
.map( id => parseInt( id, 10 ) )
.filter( Boolean );
const payload = stringifyQuery( {
include: idList.join( ',' ),
per_page: idList.length,
} );
return apiFetch( { path: NAMESPACE + 'products/categories' + payload } );
};
export const filters = [ export const filters = [
{ label: __( 'All Products', 'wc-admin' ), value: 'all' }, { label: __( 'All Products', 'wc-admin' ), value: 'all' },
@ -29,17 +54,7 @@ export const filters = [
settings: { settings: {
type: 'products', type: 'products',
param: 'product', param: 'product',
getLabels: function( queryString ) { getLabels: getProductLabelsById,
const idList = queryString
.split( ',' )
.map( id => parseInt( id, 10 ) )
.filter( Boolean );
const payload = stringifyQuery( {
include: idList.join( ',' ),
per_page: idList.length,
} );
return apiFetch( { path: '/wc/v3/products' + payload } );
},
labels: { labels: {
title: __( 'Compare Products', 'wc-admin' ), title: __( 'Compare Products', 'wc-admin' ),
update: __( 'Compare', 'wc-admin' ), update: __( 'Compare', 'wc-admin' ),
@ -52,17 +67,7 @@ export const filters = [
settings: { settings: {
type: 'product_cats', type: 'product_cats',
param: 'product_cat', param: 'product_cat',
getLabels: function( queryString ) { getLabels: getCategoryLabelsById,
const idList = queryString
.split( ',' )
.map( id => parseInt( id, 10 ) )
.filter( Boolean );
const payload = stringifyQuery( {
include: idList.join( ',' ),
per_page: idList.length,
} );
return apiFetch( { path: '/wc/v3/products/categories' + payload } );
},
labels: { labels: {
title: __( 'Compare Product Categories', 'wc-admin' ), title: __( 'Compare Product Categories', 'wc-admin' ),
update: __( 'Compare', 'wc-admin' ), update: __( 'Compare', 'wc-admin' ),

View File

@ -7,7 +7,7 @@ import { Component, Fragment } from '@wordpress/element';
/** /**
* Internal dependencies * Internal dependencies
*/ */
import { filters } from './constants'; import { filters } from './config';
import { ReportFilters } from '@woocommerce/components'; import { ReportFilters } from '@woocommerce/components';
import './style.scss'; import './style.scss';

View File

@ -31,11 +31,9 @@ const matches = [
class AdvancedFilters extends Component { class AdvancedFilters extends Component {
constructor( props ) { constructor( props ) {
super( props ); super( props );
const activeFiltersFromQuery = getActiveFiltersFromQuery( props.query, props.config );
this.state = { this.state = {
match: matches[ 0 ], match: matches[ 0 ],
activeFilters: activeFiltersFromQuery, activeFilters: getActiveFiltersFromQuery( props.query, props.config ),
previousFilters: activeFiltersFromQuery,
}; };
this.filterListRef = createRef(); this.filterListRef = createRef();
@ -107,7 +105,7 @@ class AdvancedFilters extends Component {
newFilter.value = filterConfig.input.options[ 0 ].value; newFilter.value = filterConfig.input.options[ 0 ].value;
} }
if ( filterConfig.input && 'Search' === filterConfig.input.component ) { if ( filterConfig.input && 'Search' === filterConfig.input.component ) {
newFilter.value = []; newFilter.value = '';
} }
this.setState( state => { this.setState( state => {
return { return {
@ -129,9 +127,8 @@ class AdvancedFilters extends Component {
} }
getUpdateHref( activeFilters ) { getUpdateHref( activeFilters ) {
const { previousFilters } = this.state; const { path, query, config } = this.props;
const { path, query } = this.props; const updatedQuery = getQueryFromActiveFilters( activeFilters, query, config );
const updatedQuery = getQueryFromActiveFilters( activeFilters, previousFilters );
return getNewPath( updatedQuery, path, query ); return getNewPath( updatedQuery, path, query );
} }

View File

@ -14,27 +14,38 @@ import PropTypes from 'prop-types';
import Search from 'components/search'; import Search from 'components/search';
class SearchFilter extends Component { class SearchFilter extends Component {
constructor() { constructor( { filter, config } ) {
super(); super( ...arguments );
this.onSearchChange = this.onSearchChange.bind( this ); this.onSearchChange = this.onSearchChange.bind( this );
this.state = {
selected: [],
};
this.updateLabels = this.updateLabels.bind( this );
if ( filter.value.length ) {
config.input.getLabels( filter.value ).then( this.updateLabels );
}
}
updateLabels( data ) {
const selected = data.map( p => ( { id: p.id, label: p.name } ) );
this.setState( { selected } );
} }
onSearchChange( values ) { onSearchChange( values ) {
this.setState( {
selected: values,
} );
const { filter, onFilterChange } = this.props; const { filter, onFilterChange } = this.props;
const nextValues = values.map( value => value.id ); const idList = values.map( value => value.id ).join( ',' );
onFilterChange( filter.key, 'value', nextValues ); onFilterChange( filter.key, 'value', idList );
} }
render() { render() {
const { filter, config, onFilterChange } = this.props; const { filter, config, onFilterChange } = this.props;
const { key, rule, value } = filter; const { selected } = this.state;
const selected = value.map( id => { const { key, rule } = filter;
// For now
return {
id: parseInt( id, 10 ),
label: id.toString(),
};
} );
return ( return (
<Fragment> <Fragment>
<div className="woocommerce-filters-advanced__fieldset-legend">{ config.label }</div> <div className="woocommerce-filters-advanced__fieldset-legend">{ config.label }</div>
@ -75,7 +86,7 @@ SearchFilter.propTypes = {
filter: PropTypes.shape( { filter: PropTypes.shape( {
key: PropTypes.string, key: PropTypes.string,
rule: PropTypes.string, rule: PropTypes.string,
value: PropTypes.array, value: PropTypes.string,
} ).isRequired, } ).isRequired,
/** /**
* Function to be called on update. * Function to be called on update.

View File

@ -9,13 +9,29 @@
/** /**
* Internal dependencies * Internal dependencies
*/ */
import { import { getUrlKey, getActiveFiltersFromQuery, getQueryFromActiveFilters } from '../utils';
getUrlKey,
getSearchFilterValue, const config = {
getActiveFiltersFromQuery, with_select: {
getUrlValue, rules: [ { value: 'is' } ],
getQueryFromActiveFilters, input: {
} from '../utils'; component: 'SelectControl',
options: [ { value: 'pending' } ],
},
},
with_search: {
rules: [ { value: 'includes' } ],
input: {
component: 'Search',
},
},
with_no_rules: {
input: {
component: 'SelectControl',
options: [ { value: 'pending' } ],
},
},
};
describe( 'getUrlKey', () => { describe( 'getUrlKey', () => {
it( 'should return a correctly formatted string', () => { it( 'should return a correctly formatted string', () => {
@ -29,51 +45,11 @@ describe( 'getUrlKey', () => {
} ); } );
} ); } );
describe( 'getSearchFilterValue', () => {
it( 'should convert url query param into value readable by Search component', () => {
const str = '1,2,3';
const values = getSearchFilterValue( str );
expect( Array.isArray( values ) ).toBeTruthy();
expect( values[ 0 ] ).toBe( '1' );
expect( values[ 1 ] ).toBe( '2' );
expect( values[ 2 ] ).toBe( '3' );
} );
it( 'should convert an empty string into an empty array', () => {
const str = '';
const values = getSearchFilterValue( str );
expect( Array.isArray( values ) ).toBeTruthy();
expect( values.length ).toBe( 0 );
} );
} );
describe( 'getActiveFiltersFromQuery', () => { describe( 'getActiveFiltersFromQuery', () => {
const config = {
with_select: {
rules: [ { value: 'is' } ],
input: {
component: 'SelectControl',
options: [ { value: 'pending' } ],
},
},
with_search: {
rules: [ { value: 'includes' } ],
input: {
component: 'Search',
},
},
with_no_rules: {
input: {
component: 'SelectControl',
options: [ { value: 'pending' } ],
},
},
};
it( 'should return activeFilters from a query', () => { it( 'should return activeFilters from a query', () => {
const query = { const query = {
with_select_is: 'pending', with_select_is: 'pending',
with_search_includes: '', with_search_includes: '1,2,3',
with_no_rules: 'pending', with_no_rules: 'pending',
}; };
@ -91,7 +67,7 @@ describe( 'getActiveFiltersFromQuery', () => {
const with_search = activeFilters[ 1 ]; const with_search = activeFilters[ 1 ];
expect( with_search.key ).toBe( 'with_search' ); expect( with_search.key ).toBe( 'with_search' );
expect( with_search.rule ).toBe( 'includes' ); expect( with_search.rule ).toBe( 'includes' );
expect( with_search.value ).toEqual( [] ); expect( with_search.value ).toEqual( '1,2,3' );
// with_search // with_search
const with_no_rules = activeFilters[ 2 ]; const with_no_rules = activeFilters[ 2 ];
@ -119,28 +95,6 @@ describe( 'getActiveFiltersFromQuery', () => {
} ); } );
} ); } );
describe( 'getUrlValue', () => {
it( 'should pass through a string', () => {
const value = getUrlValue( 'my string' );
expect( value ).toBe( 'my string' );
} );
it( 'should return null for a non-string value', () => {
const value = getUrlValue( {} );
expect( value ).toBeNull();
} );
it( 'should return null for an empty array', () => {
const value = getUrlValue( [] );
expect( value ).toBeNull();
} );
it( 'should return comma separated values when given an array', () => {
const value = getUrlValue( [ 1, 2, 3 ] );
expect( value ).toBe( '1,2,3' );
} );
} );
describe( 'getQueryFromActiveFilters', () => { describe( 'getQueryFromActiveFilters', () => {
it( 'should return a query object from activeFilters', () => { it( 'should return a query object from activeFilters', () => {
const activeFilters = [ const activeFilters = [
@ -148,32 +102,27 @@ describe( 'getQueryFromActiveFilters', () => {
{ {
key: 'things', key: 'things',
rule: 'includes', rule: 'includes',
value: [ 1, 2, 3 ], value: '1,2,3',
}, },
{ key: 'customer', value: 'new' }, { key: 'customer', value: 'new' },
]; ];
const query = getQueryFromActiveFilters( activeFilters ); const query = {};
expect( query.status_is ).toBe( 'open' ); const nextQuery = getQueryFromActiveFilters( activeFilters, query, config );
expect( query.things_includes ).toBe( '1,2,3' ); expect( nextQuery.status_is ).toBe( 'open' );
expect( query.customer ).toBe( 'new' ); expect( nextQuery.things_includes ).toBe( '1,2,3' );
expect( nextQuery.customer ).toBe( 'new' );
} ); } );
it( 'should remove parameters from the previous filters', () => { it( 'should remove parameters from the previous filters', () => {
const nextFilters = []; const activeFilters = [];
const previousFilters = [ const query = {
{ key: 'status', rule: 'is', value: 'open' }, with_select_is: 'complete',
{ with_search_includes: '45',
key: 'things', };
rule: 'includes',
value: [ 1, 2, 3 ],
},
{ key: 'customer', value: 'new' },
];
const query = getQueryFromActiveFilters( nextFilters, previousFilters ); const nextQuery = getQueryFromActiveFilters( activeFilters, query, config );
expect( query.status_is ).toBeUndefined(); expect( nextQuery.with_select_is ).toBeUndefined();
expect( query.things_includes ).toBeUndefined(); expect( nextQuery.with_search_includes ).toBeUndefined();
expect( query.customer ).toBeUndefined();
} ); } );
} ); } );

View File

@ -18,23 +18,13 @@ export const getUrlKey = ( key, rule ) => {
return key; return key;
}; };
/**
* Convert url values to array of objects for <Search /> component
*
* @param {string} str - url query parameter value
* @return {array} - array of Search values
*/
export const getSearchFilterValue = str => {
return str.length ? str.trim().split( ',' ) : [];
};
/** /**
* Describe activeFilter object. * Describe activeFilter object.
* *
* @typedef {Object} activeFilter * @typedef {Object} activeFilter
* @property {string} key - filter key. * @property {string} key - filter key.
* @property {string} [rule] - a modifying rule for a filter, eg 'includes' or 'is_not'. * @property {string} [rule] - a modifying rule for a filter, eg 'includes' or 'is_not'.
* @property {string|array} value - filter value(s). * @property {string} value - filter value(s).
*/ */
/** /**
@ -54,9 +44,7 @@ export const getActiveFiltersFromQuery = ( query, config ) => {
} ); } );
if ( match ) { if ( match ) {
const rawValue = query[ getUrlKey( configKey, match.value ) ]; const value = query[ getUrlKey( configKey, match.value ) ];
const value =
'Search' === filter.input.component ? getSearchFilterValue( rawValue ) : rawValue;
return { return {
key: configKey, key: configKey,
rule: match.value, rule: match.value,
@ -76,40 +64,27 @@ export const getActiveFiltersFromQuery = ( query, config ) => {
); );
}; };
/**
* Create a string value for url. Return a string directly or concatenate ids if supplied
* an array of objects.
*
* @param {string|array} value - value of an activeFilter
* @return {string|null} - url query param value
*/
export const getUrlValue = value => {
if ( Array.isArray( value ) ) {
return value.length ? value.join( ',' ) : null;
}
return 'string' === typeof value ? value : null;
};
/** /**
* Given activeFilters, create a new query object to update the url. Use previousFilters to * Given activeFilters, create a new query object to update the url. Use previousFilters to
* Remove unused params. * Remove unused params.
* *
* @param {activeFilters[]} nextFilters - activeFilters shown in the UI * @param {activeFilters[]} activeFilters - activeFilters shown in the UI
* @param {activeFilters[]} previousFilters - filters represented by the current url * @param {object} query - the current url query object
* @param {object} config - config object
* @return {object} - query object representing the new parameters * @return {object} - query object representing the new parameters
*/ */
export const getQueryFromActiveFilters = ( nextFilters, previousFilters = [] ) => { export const getQueryFromActiveFilters = ( activeFilters, query, config ) => {
const previousData = previousFilters.reduce( ( query, filter ) => { const previousFilters = getActiveFiltersFromQuery( query, config );
query[ getUrlKey( filter.key, filter.rule ) ] = undefined; const previousData = previousFilters.reduce( ( data, filter ) => {
return query; data[ getUrlKey( filter.key, filter.rule ) ] = undefined;
return data;
}, {} ); }, {} );
const data = nextFilters.reduce( ( query, filter ) => { const nextData = activeFilters.reduce( ( data, filter ) => {
const urlValue = getUrlValue( filter.value ); if ( filter.value ) {
if ( urlValue ) { data[ getUrlKey( filter.key, filter.rule ) ] = filter.value;
query[ getUrlKey( filter.key, filter.rule ) ] = urlValue;
} }
return query; return data;
}, {} ); }, {} );
return { ...previousData, ...data }; return { ...previousData, ...nextData };
}; };

View File

@ -46,7 +46,6 @@ class ReportFilters extends Component {
return ( return (
<div className="woocommerce-filters__advanced-filters"> <div className="woocommerce-filters__advanced-filters">
<AdvancedFilters <AdvancedFilters
key={ JSON.stringify( query ) }
config={ advancedConfig } config={ advancedConfig }
filterTitle={ __( 'Orders', 'wc-admin' ) } filterTitle={ __( 'Orders', 'wc-admin' ) }
path={ path } path={ path }