diff --git a/plugins/woocommerce-admin/client/analytics/report/orders/constants.js b/plugins/woocommerce-admin/client/analytics/report/orders/constants.js
index 7ac6327240a..251ba6ae2ba 100644
--- a/plugins/woocommerce-admin/client/analytics/report/orders/constants.js
+++ b/plugins/woocommerce-admin/client/analytics/report/orders/constants.js
@@ -28,7 +28,7 @@ export const advancedFilterConfig = {
addLabel: __( 'Order Status', 'wc-admin' ),
rules: [
{ value: 'is', label: __( 'Is', 'wc-admin' ) },
- { value: 'is-not', label: __( 'Is Not', 'wc-admin' ) },
+ { value: 'is_not', label: __( 'Is Not', 'wc-admin' ) },
],
input: {
component: 'SelectControl',
@@ -43,28 +43,28 @@ export const advancedFilterConfig = {
],
},
},
- product: {
+ product_id: {
label: __( 'Product', 'wc-admin' ),
addLabel: __( 'Products', 'wc-admin' ),
rules: [
{ value: 'includes', label: __( 'Includes', 'wc-admin' ) },
{ value: 'excludes', label: __( 'Excludes', 'wc-admin' ) },
{ value: 'is', label: __( 'Is', 'wc-admin' ) },
- { value: 'is-not', label: __( 'Is Not', 'wc-admin' ) },
+ { value: 'is_not', label: __( 'Is Not', 'wc-admin' ) },
],
input: {
component: 'Search',
type: 'products',
},
},
- coupon: {
+ code: {
label: __( 'Coupon Code', 'wc-admin' ),
addLabel: __( 'Coupon Codes', 'wc-admin' ),
rules: [
{ value: 'includes', label: __( 'Includes', 'wc-admin' ) },
{ value: 'excludes', label: __( 'Excludes', 'wc-admin' ) },
{ value: 'is', label: __( 'Is', 'wc-admin' ) },
- { value: 'is-not', label: __( 'Is Not', 'wc-admin' ) },
+ { value: 'is_not', label: __( 'Is Not', 'wc-admin' ) },
],
input: {
component: 'Search',
@@ -74,9 +74,12 @@ export const advancedFilterConfig = {
customer: {
label: __( 'Customer is', 'wc-admin' ),
addLabel: __( 'Customer Type', 'wc-admin' ),
- rules: [
- { value: 'new', label: __( 'New', 'wc-admin' ) },
- { value: 'returning', label: __( 'Returning', 'wc-admin' ) },
- ],
+ input: {
+ component: 'SelectControl',
+ options: [
+ { value: 'new', label: __( 'New', 'wc-admin' ) },
+ { value: 'returning', label: __( 'Returning', 'wc-admin' ) },
+ ],
+ },
},
};
diff --git a/plugins/woocommerce-admin/client/components/filters/advanced/index.js b/plugins/woocommerce-admin/client/components/filters/advanced/index.js
index 5b8c1f95974..c74960b9b73 100644
--- a/plugins/woocommerce-admin/client/components/filters/advanced/index.js
+++ b/plugins/woocommerce-admin/client/components/filters/advanced/index.js
@@ -13,7 +13,11 @@ import Gridicon from 'gridicons';
* Internal dependencies
*/
import Card from 'components/card';
-import Search from 'components/search';
+import Link from 'components/link';
+import SelectFilter from './select-filter';
+import SearchFilter from './search-filter';
+import { getActiveFiltersFromQuery, getQueryFromActiveFilters } from './utils';
+import { getNewPath } from 'lib/nav-utils';
import './style.scss';
const matches = [
@@ -27,25 +31,22 @@ const matches = [
class AdvancedFilters extends Component {
constructor( props ) {
super( props );
+ const activeFiltersFromQuery = getActiveFiltersFromQuery( props.query, props.config );
this.state = {
match: matches[ 0 ],
- activeFilters: [
- /**
- * Example activeFilter
- * { key: ‘product’, rule: ‘includes’, value: [ ‘one’, ‘two’ ] }
- */
- ],
+ activeFilters: activeFiltersFromQuery,
+ previousFilters: activeFiltersFromQuery,
};
this.filterListRef = createRef();
this.onMatchChange = this.onMatchChange.bind( this );
this.onFilterChange = this.onFilterChange.bind( this );
- this.getSelector = this.getSelector.bind( this );
this.getAvailableFilterKeys = this.getAvailableFilterKeys.bind( this );
this.addFilter = this.addFilter.bind( this );
this.removeFilter = this.removeFilter.bind( this );
- this.clearAllFilters = this.clearAllFilters.bind( this );
+ this.clearFilters = this.clearFilters.bind( this );
+ this.getUpdateHref = this.getUpdateHref.bind( this );
}
onMatchChange( value ) {
@@ -90,35 +91,6 @@ class AdvancedFilters extends Component {
);
}
- getSelector( filter ) {
- const filterConfig = this.props.config[ filter.key ];
- const { input } = filterConfig;
- if ( ! input ) {
- return null;
- }
- if ( 'SelectControl' === input.component ) {
- return (
-
- );
- }
- if ( 'Search' === input.component ) {
- return (
-
- );
- }
- return null;
- }
-
getAvailableFilterKeys() {
const { config } = this.props;
const activeFilterKeys = this.state.activeFilters.map( f => f.key );
@@ -127,9 +99,15 @@ class AdvancedFilters extends Component {
addFilter( key, onClose ) {
const filterConfig = this.props.config[ key ];
- const newFilter = { key, rule: filterConfig.rules[ 0 ] };
+ const newFilter = { key };
+ if ( Array.isArray( filterConfig.rules ) && filterConfig.rules.length ) {
+ newFilter.rule = filterConfig.rules[ 0 ].value;
+ }
if ( filterConfig.input && filterConfig.input.options ) {
- newFilter.value = filterConfig.input.options[ 0 ];
+ newFilter.value = filterConfig.input.options[ 0 ].value;
+ }
+ if ( filterConfig.input && 'Search' === filterConfig.input.component ) {
+ newFilter.value = [];
}
this.setState( state => {
return {
@@ -144,20 +122,29 @@ class AdvancedFilters extends Component {
} );
}
- clearAllFilters() {
+ clearFilters() {
this.setState( {
activeFilters: [],
} );
}
+ getUpdateHref( activeFilters ) {
+ const { previousFilters } = this.state;
+ const { path, query } = this.props;
+ const updatedQuery = getQueryFromActiveFilters( activeFilters, previousFilters );
+ return getNewPath( updatedQuery, path, query );
+ }
+
render() {
const { config } = this.props;
+ const { activeFilters } = this.state;
const availableFilterKeys = this.getAvailableFilterKeys();
+ const updateHref = this.getUpdateHref( activeFilters );
return (
- { this.state.activeFilters.map( filter => {
- const { key, rule } = filter;
+ { activeFilters.map( filter => {
+ const { key } = filter;
const filterConfig = config[ key ];
return (
-
@@ -166,22 +153,20 @@ class AdvancedFilters extends Component {
{ /*eslint-enable-next-line jsx-a11y/no-noninteractive-tabindex*/ }
-
- { filterConfig.label }
-
-
-
- { this.getSelector( filter ) }
-
+ { 'SelectControl' === filterConfig.input.component && (
+
+ ) }
+ { 'Search' === filterConfig.input.component && (
+
+ ) }
-
-
+
);
diff --git a/plugins/woocommerce-admin/client/components/filters/advanced/search-filter.js b/plugins/woocommerce-admin/client/components/filters/advanced/search-filter.js
new file mode 100644
index 00000000000..5ad345ada80
--- /dev/null
+++ b/plugins/woocommerce-admin/client/components/filters/advanced/search-filter.js
@@ -0,0 +1,86 @@
+/** @format */
+/**
+ * External dependencies
+ */
+import { __, sprintf } from '@wordpress/i18n';
+import { Component, Fragment } from '@wordpress/element';
+import { SelectControl } from '@wordpress/components';
+import { partial } from 'lodash';
+import PropTypes from 'prop-types';
+
+/**
+ * Internal dependencies
+ */
+import Search from 'components/search';
+
+class SearchFilter extends Component {
+ constructor() {
+ super();
+ this.onSearchChange = this.onSearchChange.bind( this );
+ }
+
+ onSearchChange( values ) {
+ const { filter, onFilterChange } = this.props;
+ const nextValues = values.map( value => value.id );
+ onFilterChange( filter.key, 'value', nextValues );
+ }
+
+ render() {
+ const { filter, config, onFilterChange } = this.props;
+ const { key, rule, value } = filter;
+ const selected = value.map( id => {
+ // For now
+ return {
+ id: parseInt( id, 10 ),
+ label: id.toString(),
+ };
+ } );
+ return (
+
+ { config.label }
+ { rule && (
+
+ ) }
+
+
+
+
+ );
+ }
+}
+
+SearchFilter.propTypes = {
+ /**
+ * The configuration object for the single filter to be rendered.
+ */
+ config: PropTypes.shape( {
+ label: PropTypes.string,
+ addLabel: PropTypes.string,
+ rules: PropTypes.arrayOf( PropTypes.object ),
+ input: PropTypes.object,
+ } ).isRequired,
+ /**
+ * The activeFilter handed down by AdvancedFilters.
+ */
+ filter: PropTypes.shape( {
+ key: PropTypes.string,
+ rule: PropTypes.string,
+ value: PropTypes.array,
+ } ).isRequired,
+ /**
+ * Function to be called on update.
+ */
+ onFilterChange: PropTypes.func.isRequired,
+};
+
+export default SearchFilter;
diff --git a/plugins/woocommerce-admin/client/components/filters/advanced/select-filter.js b/plugins/woocommerce-admin/client/components/filters/advanced/select-filter.js
new file mode 100644
index 00000000000..958afbc12aa
--- /dev/null
+++ b/plugins/woocommerce-admin/client/components/filters/advanced/select-filter.js
@@ -0,0 +1,62 @@
+/** @format */
+/**
+ * External dependencies
+ */
+import { __, sprintf } from '@wordpress/i18n';
+import { Fragment } from '@wordpress/element';
+import { SelectControl } from '@wordpress/components';
+import { partial } from 'lodash';
+import PropTypes from 'prop-types';
+
+const SelectFilter = ( { filter, config, onFilterChange } ) => {
+ const { key, rule, value } = filter;
+ return (
+
+ { config.label }
+ { rule && (
+
+ ) }
+
+
+
+
+ );
+};
+
+SelectFilter.propTypes = {
+ /**
+ * The configuration object for the single filter to be rendered.
+ */
+ config: PropTypes.shape( {
+ label: PropTypes.string,
+ addLabel: PropTypes.string,
+ rules: PropTypes.arrayOf( PropTypes.object ),
+ input: PropTypes.object,
+ } ).isRequired,
+ /**
+ * The activeFilter handed down by AdvancedFilters.
+ */
+ filter: PropTypes.shape( {
+ key: PropTypes.string,
+ rule: PropTypes.string,
+ value: PropTypes.string,
+ } ).isRequired,
+ /**
+ * Function to be called on update.
+ */
+ onFilterChange: PropTypes.func.isRequired,
+};
+
+export default SelectFilter;
diff --git a/plugins/woocommerce-admin/client/components/filters/advanced/style.scss b/plugins/woocommerce-admin/client/components/filters/advanced/style.scss
index 18170bca01f..a6b6c7ba78b 100644
--- a/plugins/woocommerce-admin/client/components/filters/advanced/style.scss
+++ b/plugins/woocommerce-admin/client/components/filters/advanced/style.scss
@@ -29,13 +29,17 @@
}
.woocommerce-filters-advanced__list-item {
- padding: $gap-smaller $gap;
+ padding: 0 $gap 0 0;
margin: 0;
display: grid;
grid-template-columns: auto 40px;
background-color: $core-grey-light-100;
border-bottom: 1px solid $core-grey-light-700;
+ fieldset {
+ padding: $gap-smaller $gap;
+ }
+
&:hover {
background-color: $core-grey-light-200;
}
@@ -116,7 +120,7 @@
.woocommerce-filters-advanced__controls {
padding: $gap-smaller $gap;
- & > button {
+ .components-button {
margin-right: $gap;
}
}
@@ -144,8 +148,6 @@
}
.woocommerce-filters-advanced__list-selector {
- padding: 0 0 0 $gap-smaller;
-
@include breakpoint( '<782px' ) {
padding: $gap-smallest 0;
}
@@ -155,4 +157,12 @@
@include breakpoint( '<782px' ) {
padding: $gap-smallest 0;
}
+
+ & + .woocommerce-filters-advanced__list-selector {
+ padding: 0 0 0 $gap-smaller;
+
+ @include breakpoint( '<782px' ) {
+ padding: $gap-smallest 0;
+ }
+ }
}
diff --git a/plugins/woocommerce-admin/client/components/filters/advanced/test/utils.js b/plugins/woocommerce-admin/client/components/filters/advanced/test/utils.js
new file mode 100644
index 00000000000..3e5cd8d28a9
--- /dev/null
+++ b/plugins/woocommerce-admin/client/components/filters/advanced/test/utils.js
@@ -0,0 +1,179 @@
+/**
+ * @format
+ */
+
+/**
+ * External dependencies
+ */
+
+/**
+ * Internal dependencies
+ */
+import {
+ getUrlKey,
+ getSearchFilterValue,
+ getActiveFiltersFromQuery,
+ getUrlValue,
+ getQueryFromActiveFilters,
+} from '../utils';
+
+describe( 'getUrlKey', () => {
+ it( 'should return a correctly formatted string', () => {
+ const key = getUrlKey( 'key', 'rule' );
+ expect( key ).toBe( 'key_rule' );
+ } );
+
+ it( 'should return a correctly formatted string with no rule', () => {
+ const key = getUrlKey( 'key' );
+ expect( key ).toBe( 'key' );
+ } );
+} );
+
+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', () => {
+ 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', () => {
+ const query = {
+ with_select_is: 'pending',
+ with_search_includes: '',
+ with_no_rules: 'pending',
+ };
+
+ const activeFilters = getActiveFiltersFromQuery( query, config );
+ expect( Array.isArray( activeFilters ) ).toBeTruthy();
+ expect( activeFilters.length ).toBe( 3 );
+
+ // with_select
+ const with_select = activeFilters[ 0 ];
+ expect( with_select.key ).toBe( 'with_select' );
+ expect( with_select.rule ).toBe( 'is' );
+ expect( with_select.value ).toBe( 'pending' );
+
+ // with_search
+ const with_search = activeFilters[ 1 ];
+ expect( with_search.key ).toBe( 'with_search' );
+ expect( with_search.rule ).toBe( 'includes' );
+ expect( with_search.value ).toEqual( [] );
+
+ // with_search
+ const with_no_rules = activeFilters[ 2 ];
+ expect( with_no_rules.key ).toBe( 'with_no_rules' );
+ expect( with_no_rules.rule ).toBeUndefined();
+ expect( with_no_rules.value ).toEqual( 'pending' );
+ } );
+
+ it( 'should ignore irrelevant query parameters', () => {
+ const query = {
+ with_select: 'pending', // no rule associated
+ status: 45,
+ };
+
+ const activeFilters = getActiveFiltersFromQuery( query, config );
+ expect( activeFilters.length ).toBe( 0 );
+ } );
+
+ it( 'should return an empty array with no relevant parameters', () => {
+ const query = {};
+
+ const activeFilters = getActiveFiltersFromQuery( query, config );
+ expect( Array.isArray( activeFilters ) ).toBe( true );
+ expect( activeFilters.length ).toBe( 0 );
+ } );
+} );
+
+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', () => {
+ it( 'should return a query object from activeFilters', () => {
+ const activeFilters = [
+ { key: 'status', rule: 'is', value: 'open' },
+ {
+ key: 'things',
+ rule: 'includes',
+ value: [ 1, 2, 3 ],
+ },
+ { key: 'customer', value: 'new' },
+ ];
+
+ const query = getQueryFromActiveFilters( activeFilters );
+ expect( query.status_is ).toBe( 'open' );
+ expect( query.things_includes ).toBe( '1,2,3' );
+ expect( query.customer ).toBe( 'new' );
+ } );
+
+ it( 'should remove parameters from the previous filters', () => {
+ const nextFilters = [];
+ const previousFilters = [
+ { key: 'status', rule: 'is', value: 'open' },
+ {
+ key: 'things',
+ rule: 'includes',
+ value: [ 1, 2, 3 ],
+ },
+ { key: 'customer', value: 'new' },
+ ];
+
+ const query = getQueryFromActiveFilters( nextFilters, previousFilters );
+ expect( query.status_is ).toBeUndefined();
+ expect( query.things_includes ).toBeUndefined();
+ expect( query.customer ).toBeUndefined();
+ } );
+} );
diff --git a/plugins/woocommerce-admin/client/components/filters/advanced/utils.js b/plugins/woocommerce-admin/client/components/filters/advanced/utils.js
new file mode 100644
index 00000000000..53c73d27ebf
--- /dev/null
+++ b/plugins/woocommerce-admin/client/components/filters/advanced/utils.js
@@ -0,0 +1,115 @@
+/** @format */
+/**
+ * External dependencies
+ */
+import { find, compact } from 'lodash';
+
+/**
+ * Get the url query key from the filter key and rule.
+ *
+ * @param {string} key - filter key.
+ * @param {string} rule - filter rule.
+ * @return {string} - url query key.
+ */
+export const getUrlKey = ( key, rule ) => {
+ if ( rule && rule.length ) {
+ return `${ key }_${ rule }`;
+ }
+ return key;
+};
+
+/**
+ * Convert url values to array of objects for 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.
+ *
+ * @typedef {Object} activeFilter
+ * @property {string} key - filter key.
+ * @property {string} [rule] - a modifying rule for a filter, eg 'includes' or 'is_not'.
+ * @property {string|array} value - filter value(s).
+ */
+
+/**
+ * Given a query object, return an array of activeFilters, if any.
+ *
+ * @param {object} query - query oject
+ * @param {object} config - config object
+ * @return {activeFilters[]} - array of activeFilters
+ */
+export const getActiveFiltersFromQuery = ( query, config ) => {
+ return compact(
+ Object.keys( config ).map( configKey => {
+ const filter = config[ configKey ];
+ if ( filter.rules ) {
+ const match = find( filter.rules, rule => {
+ return query.hasOwnProperty( getUrlKey( configKey, rule.value ) );
+ } );
+
+ if ( match ) {
+ const rawValue = query[ getUrlKey( configKey, match.value ) ];
+ const value =
+ 'Search' === filter.input.component ? getSearchFilterValue( rawValue ) : rawValue;
+ return {
+ key: configKey,
+ rule: match.value,
+ value,
+ };
+ }
+ return null;
+ }
+ if ( query[ configKey ] ) {
+ return {
+ key: configKey,
+ value: query[ configKey ],
+ };
+ }
+ return null;
+ } )
+ );
+};
+
+/**
+ * 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
+ * Remove unused params.
+ *
+ * @param {activeFilters[]} nextFilters - activeFilters shown in the UI
+ * @param {activeFilters[]} previousFilters - filters represented by the current url
+ * @return {object} - query object representing the new parameters
+ */
+export const getQueryFromActiveFilters = ( nextFilters, previousFilters = [] ) => {
+ const previousData = previousFilters.reduce( ( query, filter ) => {
+ query[ getUrlKey( filter.key, filter.rule ) ] = undefined;
+ return query;
+ }, {} );
+ const data = nextFilters.reduce( ( query, filter ) => {
+ const urlValue = getUrlValue( filter.value );
+ if ( urlValue ) {
+ query[ getUrlKey( filter.key, filter.rule ) ] = urlValue;
+ }
+ return query;
+ }, {} );
+
+ return { ...previousData, ...data };
+};
diff --git a/plugins/woocommerce-admin/client/components/filters/index.js b/plugins/woocommerce-admin/client/components/filters/index.js
index 2233b1f621f..ff257f64473 100644
--- a/plugins/woocommerce-admin/client/components/filters/index.js
+++ b/plugins/woocommerce-admin/client/components/filters/index.js
@@ -46,6 +46,7 @@ class ReportFilters extends Component {
return (
{
return wcSettings.adminUrl + path;
};
+/**
+ * Converts a query object to a query string.
+ *
+ * @param {Object} query parameters to be converted.
+ * @return {String} Query string.
+ */
+export const stringifyQuery = query => {
+ return query ? '?' + stringify( query ) : '';
+};
+
/**
* Return a URL with set query parameters.
*
@@ -47,8 +57,8 @@ export const getAdminLink = path => {
* @return {String} Updated URL merging query params into existing params.
*/
export const getNewPath = ( query, path = getPath(), currentQuery = getQuery() ) => {
- const queryString = stringify( { ...currentQuery, ...query } );
- return `${ path }?${ queryString }`;
+ const queryString = stringifyQuery( { ...currentQuery, ...query } );
+ return `${ path }${ queryString }`;
};
/**
@@ -62,13 +72,3 @@ export const updateQueryString = ( query, path = getPath(), currentQuery = getQu
const newPath = getNewPath( query, path, currentQuery );
history.push( newPath );
};
-
-/**
- * Converts a query object to a query string.
- *
- * @param {Object} query parameters to be converted.
- * @return {String} Query string.
- */
-export const stringifyQuery = query => {
- return query ? '?' + stringify( query ) : '';
-};
diff --git a/plugins/woocommerce-admin/client/stylesheets/shared/_global.scss b/plugins/woocommerce-admin/client/stylesheets/shared/_global.scss
index 416e7e61f3e..4c77c243bda 100644
--- a/plugins/woocommerce-admin/client/stylesheets/shared/_global.scss
+++ b/plugins/woocommerce-admin/client/stylesheets/shared/_global.scss
@@ -37,6 +37,10 @@
color: $woocommerce-500;
}
+ a.components-button.is-button {
+ color: $white;
+ }
+
a:hover,
a:active,
a:focus,