Merge pull request woocommerce/woocommerce-admin#349 from woocommerce/add/advanced-filters-url-support
Advanced Filters: Add url support
This commit is contained in:
commit
925140fc78
|
@ -28,7 +28,7 @@ export const advancedFilterConfig = {
|
||||||
addLabel: __( 'Order Status', 'wc-admin' ),
|
addLabel: __( 'Order Status', 'wc-admin' ),
|
||||||
rules: [
|
rules: [
|
||||||
{ value: 'is', label: __( 'Is', '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: {
|
input: {
|
||||||
component: 'SelectControl',
|
component: 'SelectControl',
|
||||||
|
@ -43,28 +43,28 @@ export const advancedFilterConfig = {
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
product: {
|
product_id: {
|
||||||
label: __( 'Product', 'wc-admin' ),
|
label: __( 'Product', 'wc-admin' ),
|
||||||
addLabel: __( 'Products', 'wc-admin' ),
|
addLabel: __( 'Products', 'wc-admin' ),
|
||||||
rules: [
|
rules: [
|
||||||
{ value: 'includes', label: __( 'Includes', 'wc-admin' ) },
|
{ value: 'includes', label: __( 'Includes', 'wc-admin' ) },
|
||||||
{ value: 'excludes', label: __( 'Excludes', 'wc-admin' ) },
|
{ value: 'excludes', label: __( 'Excludes', 'wc-admin' ) },
|
||||||
{ value: 'is', label: __( 'Is', '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: {
|
input: {
|
||||||
component: 'Search',
|
component: 'Search',
|
||||||
type: 'products',
|
type: 'products',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
coupon: {
|
code: {
|
||||||
label: __( 'Coupon Code', 'wc-admin' ),
|
label: __( 'Coupon Code', 'wc-admin' ),
|
||||||
addLabel: __( 'Coupon Codes', 'wc-admin' ),
|
addLabel: __( 'Coupon Codes', 'wc-admin' ),
|
||||||
rules: [
|
rules: [
|
||||||
{ value: 'includes', label: __( 'Includes', 'wc-admin' ) },
|
{ value: 'includes', label: __( 'Includes', 'wc-admin' ) },
|
||||||
{ value: 'excludes', label: __( 'Excludes', 'wc-admin' ) },
|
{ value: 'excludes', label: __( 'Excludes', 'wc-admin' ) },
|
||||||
{ value: 'is', label: __( 'Is', '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: {
|
input: {
|
||||||
component: 'Search',
|
component: 'Search',
|
||||||
|
@ -74,9 +74,12 @@ export const advancedFilterConfig = {
|
||||||
customer: {
|
customer: {
|
||||||
label: __( 'Customer is', 'wc-admin' ),
|
label: __( 'Customer is', 'wc-admin' ),
|
||||||
addLabel: __( 'Customer Type', 'wc-admin' ),
|
addLabel: __( 'Customer Type', 'wc-admin' ),
|
||||||
rules: [
|
input: {
|
||||||
|
component: 'SelectControl',
|
||||||
|
options: [
|
||||||
{ value: 'new', label: __( 'New', 'wc-admin' ) },
|
{ value: 'new', label: __( 'New', 'wc-admin' ) },
|
||||||
{ value: 'returning', label: __( 'Returning', 'wc-admin' ) },
|
{ value: 'returning', label: __( 'Returning', 'wc-admin' ) },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -13,7 +13,11 @@ import Gridicon from 'gridicons';
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import Card from 'components/card';
|
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';
|
import './style.scss';
|
||||||
|
|
||||||
const matches = [
|
const matches = [
|
||||||
|
@ -27,25 +31,22 @@ 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: [
|
activeFilters: activeFiltersFromQuery,
|
||||||
/**
|
previousFilters: activeFiltersFromQuery,
|
||||||
* Example activeFilter
|
|
||||||
* { key: ‘product’, rule: ‘includes’, value: [ ‘one’, ‘two’ ] }
|
|
||||||
*/
|
|
||||||
],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
this.filterListRef = createRef();
|
this.filterListRef = createRef();
|
||||||
|
|
||||||
this.onMatchChange = this.onMatchChange.bind( this );
|
this.onMatchChange = this.onMatchChange.bind( this );
|
||||||
this.onFilterChange = this.onFilterChange.bind( this );
|
this.onFilterChange = this.onFilterChange.bind( this );
|
||||||
this.getSelector = this.getSelector.bind( this );
|
|
||||||
this.getAvailableFilterKeys = this.getAvailableFilterKeys.bind( this );
|
this.getAvailableFilterKeys = this.getAvailableFilterKeys.bind( this );
|
||||||
this.addFilter = this.addFilter.bind( this );
|
this.addFilter = this.addFilter.bind( this );
|
||||||
this.removeFilter = this.removeFilter.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 ) {
|
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 (
|
|
||||||
<SelectControl
|
|
||||||
className="woocommerce-filters-advanced__list-select"
|
|
||||||
options={ input.options }
|
|
||||||
value={ filter.value }
|
|
||||||
onChange={ partial( this.onFilterChange, filter.key, 'value' ) }
|
|
||||||
aria-label={ sprintf( __( 'Select %s', 'wc-admin' ), filterConfig.label ) }
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if ( 'Search' === input.component ) {
|
|
||||||
return (
|
|
||||||
<Search
|
|
||||||
onChange={ partial( this.onFilterChange, filter.key, 'value' ) }
|
|
||||||
type={ input.type }
|
|
||||||
selected={ filter.value }
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
getAvailableFilterKeys() {
|
getAvailableFilterKeys() {
|
||||||
const { config } = this.props;
|
const { config } = this.props;
|
||||||
const activeFilterKeys = this.state.activeFilters.map( f => f.key );
|
const activeFilterKeys = this.state.activeFilters.map( f => f.key );
|
||||||
|
@ -127,9 +99,15 @@ class AdvancedFilters extends Component {
|
||||||
|
|
||||||
addFilter( key, onClose ) {
|
addFilter( key, onClose ) {
|
||||||
const filterConfig = this.props.config[ key ];
|
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 ) {
|
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 => {
|
this.setState( state => {
|
||||||
return {
|
return {
|
||||||
|
@ -144,20 +122,29 @@ class AdvancedFilters extends Component {
|
||||||
} );
|
} );
|
||||||
}
|
}
|
||||||
|
|
||||||
clearAllFilters() {
|
clearFilters() {
|
||||||
this.setState( {
|
this.setState( {
|
||||||
activeFilters: [],
|
activeFilters: [],
|
||||||
} );
|
} );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getUpdateHref( activeFilters ) {
|
||||||
|
const { previousFilters } = this.state;
|
||||||
|
const { path, query } = this.props;
|
||||||
|
const updatedQuery = getQueryFromActiveFilters( activeFilters, previousFilters );
|
||||||
|
return getNewPath( updatedQuery, path, query );
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { config } = this.props;
|
const { config } = this.props;
|
||||||
|
const { activeFilters } = this.state;
|
||||||
const availableFilterKeys = this.getAvailableFilterKeys();
|
const availableFilterKeys = this.getAvailableFilterKeys();
|
||||||
|
const updateHref = this.getUpdateHref( activeFilters );
|
||||||
return (
|
return (
|
||||||
<Card className="woocommerce-filters-advanced" title={ this.getTitle() }>
|
<Card className="woocommerce-filters-advanced" title={ this.getTitle() }>
|
||||||
<ul className="woocommerce-filters-advanced__list" ref={ this.filterListRef }>
|
<ul className="woocommerce-filters-advanced__list" ref={ this.filterListRef }>
|
||||||
{ this.state.activeFilters.map( filter => {
|
{ activeFilters.map( filter => {
|
||||||
const { key, rule } = filter;
|
const { key } = filter;
|
||||||
const filterConfig = config[ key ];
|
const filterConfig = config[ key ];
|
||||||
return (
|
return (
|
||||||
<li className="woocommerce-filters-advanced__list-item" key={ key }>
|
<li className="woocommerce-filters-advanced__list-item" key={ key }>
|
||||||
|
@ -166,22 +153,20 @@ class AdvancedFilters extends Component {
|
||||||
{ /*eslint-enable-next-line jsx-a11y/no-noninteractive-tabindex*/ }
|
{ /*eslint-enable-next-line jsx-a11y/no-noninteractive-tabindex*/ }
|
||||||
<legend className="screen-reader-text">{ filterConfig.label }</legend>
|
<legend className="screen-reader-text">{ filterConfig.label }</legend>
|
||||||
<div className="woocommerce-filters-advanced__fieldset">
|
<div className="woocommerce-filters-advanced__fieldset">
|
||||||
<div className="woocommerce-filters-advanced__fieldset-legend">
|
{ 'SelectControl' === filterConfig.input.component && (
|
||||||
{ filterConfig.label }
|
<SelectFilter
|
||||||
</div>
|
filter={ filter }
|
||||||
<SelectControl
|
config={ filterConfig }
|
||||||
className="woocommerce-filters-advanced__list-specifier"
|
onFilterChange={ this.onFilterChange }
|
||||||
options={ filterConfig.rules }
|
|
||||||
value={ rule }
|
|
||||||
onChange={ partial( this.onFilterChange, key, 'rule' ) }
|
|
||||||
aria-label={ sprintf(
|
|
||||||
__( 'Select a %s filter match', 'wc-admin' ),
|
|
||||||
filterConfig.addLabel
|
|
||||||
) }
|
|
||||||
/>
|
/>
|
||||||
<div className="woocommerce-filters-advanced__list-selector">
|
) }
|
||||||
{ this.getSelector( filter ) }
|
{ 'Search' === filterConfig.input.component && (
|
||||||
</div>
|
<SearchFilter
|
||||||
|
filter={ filter }
|
||||||
|
config={ filterConfig }
|
||||||
|
onFilterChange={ this.onFilterChange }
|
||||||
|
/>
|
||||||
|
) }
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<IconButton
|
<IconButton
|
||||||
|
@ -224,10 +209,17 @@ class AdvancedFilters extends Component {
|
||||||
) }
|
) }
|
||||||
|
|
||||||
<div className="woocommerce-filters-advanced__controls">
|
<div className="woocommerce-filters-advanced__controls">
|
||||||
<Button isPrimary>{ __( 'Filter', 'wc-admin' ) }</Button>
|
<Link
|
||||||
<Button isLink onClick={ this.clearAllFilters }>
|
className="components-button is-primary is-button"
|
||||||
|
type="wc-admin"
|
||||||
|
disabled={ window.location.hash.substr( 1 ) === updateHref }
|
||||||
|
href={ updateHref }
|
||||||
|
>
|
||||||
|
{ __( 'Filter', 'wc-admin' ) }
|
||||||
|
</Link>
|
||||||
|
<Link type="wc-admin" href={ this.getUpdateHref( [] ) } onClick={ this.clearFilters }>
|
||||||
{ __( 'Clear all filters', 'wc-admin' ) }
|
{ __( 'Clear all filters', 'wc-admin' ) }
|
||||||
</Button>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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 (
|
||||||
|
<Fragment>
|
||||||
|
<div className="woocommerce-filters-advanced__fieldset-legend">{ config.label }</div>
|
||||||
|
{ rule && (
|
||||||
|
<SelectControl
|
||||||
|
className="woocommerce-filters-advanced__list-specifier"
|
||||||
|
options={ config.rules }
|
||||||
|
value={ rule }
|
||||||
|
onChange={ partial( onFilterChange, key, 'rule' ) }
|
||||||
|
aria-label={ sprintf( __( 'Select a %s filter match', 'wc-admin' ), config.addLabel ) }
|
||||||
|
/>
|
||||||
|
) }
|
||||||
|
<div className="woocommerce-filters-advanced__list-selector">
|
||||||
|
<Search
|
||||||
|
onChange={ this.onSearchChange }
|
||||||
|
type={ config.input.type }
|
||||||
|
selected={ selected }
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
|
@ -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 (
|
||||||
|
<Fragment>
|
||||||
|
<div className="woocommerce-filters-advanced__fieldset-legend">{ config.label }</div>
|
||||||
|
{ rule && (
|
||||||
|
<SelectControl
|
||||||
|
className="woocommerce-filters-advanced__list-specifier"
|
||||||
|
options={ config.rules }
|
||||||
|
value={ rule }
|
||||||
|
onChange={ partial( onFilterChange, key, 'rule' ) }
|
||||||
|
aria-label={ sprintf( __( 'Select a %s filter match', 'wc-admin' ), config.addLabel ) }
|
||||||
|
/>
|
||||||
|
) }
|
||||||
|
<div className="woocommerce-filters-advanced__list-selector">
|
||||||
|
<SelectControl
|
||||||
|
className="woocommerce-filters-advanced__list-select"
|
||||||
|
options={ config.input.options }
|
||||||
|
value={ value }
|
||||||
|
onChange={ partial( onFilterChange, filter.key, 'value' ) }
|
||||||
|
aria-label={ sprintf( __( 'Select %s', 'wc-admin' ), config.label ) }
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
|
@ -29,13 +29,17 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.woocommerce-filters-advanced__list-item {
|
.woocommerce-filters-advanced__list-item {
|
||||||
padding: $gap-smaller $gap;
|
padding: 0 $gap 0 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: auto 40px;
|
grid-template-columns: auto 40px;
|
||||||
background-color: $core-grey-light-100;
|
background-color: $core-grey-light-100;
|
||||||
border-bottom: 1px solid $core-grey-light-700;
|
border-bottom: 1px solid $core-grey-light-700;
|
||||||
|
|
||||||
|
fieldset {
|
||||||
|
padding: $gap-smaller $gap;
|
||||||
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: $core-grey-light-200;
|
background-color: $core-grey-light-200;
|
||||||
}
|
}
|
||||||
|
@ -116,7 +120,7 @@
|
||||||
.woocommerce-filters-advanced__controls {
|
.woocommerce-filters-advanced__controls {
|
||||||
padding: $gap-smaller $gap;
|
padding: $gap-smaller $gap;
|
||||||
|
|
||||||
& > button {
|
.components-button {
|
||||||
margin-right: $gap;
|
margin-right: $gap;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -144,8 +148,6 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.woocommerce-filters-advanced__list-selector {
|
.woocommerce-filters-advanced__list-selector {
|
||||||
padding: 0 0 0 $gap-smaller;
|
|
||||||
|
|
||||||
@include breakpoint( '<782px' ) {
|
@include breakpoint( '<782px' ) {
|
||||||
padding: $gap-smallest 0;
|
padding: $gap-smallest 0;
|
||||||
}
|
}
|
||||||
|
@ -155,4 +157,12 @@
|
||||||
@include breakpoint( '<782px' ) {
|
@include breakpoint( '<782px' ) {
|
||||||
padding: $gap-smallest 0;
|
padding: $gap-smallest 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
& + .woocommerce-filters-advanced__list-selector {
|
||||||
|
padding: 0 0 0 $gap-smaller;
|
||||||
|
|
||||||
|
@include breakpoint( '<782px' ) {
|
||||||
|
padding: $gap-smallest 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
} );
|
||||||
|
} );
|
|
@ -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 <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.
|
||||||
|
*
|
||||||
|
* @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 };
|
||||||
|
};
|
|
@ -46,6 +46,7 @@ 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 }
|
||||||
|
|
|
@ -123,7 +123,7 @@ Search.propTypes = {
|
||||||
*/
|
*/
|
||||||
type: PropTypes.oneOf( [ 'products', 'product_cats', 'orders', 'customers' ] ).isRequired,
|
type: PropTypes.oneOf( [ 'products', 'product_cats', 'orders', 'customers' ] ).isRequired,
|
||||||
/**
|
/**
|
||||||
* An array of objects describing selected values
|
* An array of objects describing selected values.
|
||||||
*/
|
*/
|
||||||
selected: PropTypes.arrayOf(
|
selected: PropTypes.arrayOf(
|
||||||
PropTypes.shape( {
|
PropTypes.shape( {
|
||||||
|
|
|
@ -38,6 +38,16 @@ export const getAdminLink = path => {
|
||||||
return wcSettings.adminUrl + path;
|
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.
|
* 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.
|
* @return {String} Updated URL merging query params into existing params.
|
||||||
*/
|
*/
|
||||||
export const getNewPath = ( query, path = getPath(), currentQuery = getQuery() ) => {
|
export const getNewPath = ( query, path = getPath(), currentQuery = getQuery() ) => {
|
||||||
const queryString = stringify( { ...currentQuery, ...query } );
|
const queryString = stringifyQuery( { ...currentQuery, ...query } );
|
||||||
return `${ path }?${ queryString }`;
|
return `${ path }${ queryString }`;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -62,13 +72,3 @@ export const updateQueryString = ( query, path = getPath(), currentQuery = getQu
|
||||||
const newPath = getNewPath( query, path, currentQuery );
|
const newPath = getNewPath( query, path, currentQuery );
|
||||||
history.push( newPath );
|
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 ) : '';
|
|
||||||
};
|
|
||||||
|
|
|
@ -37,6 +37,10 @@
|
||||||
color: $woocommerce-500;
|
color: $woocommerce-500;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
a.components-button.is-button {
|
||||||
|
color: $white;
|
||||||
|
}
|
||||||
|
|
||||||
a:hover,
|
a:hover,
|
||||||
a:active,
|
a:active,
|
||||||
a:focus,
|
a:focus,
|
||||||
|
|
Loading…
Reference in New Issue