Merge pull request woocommerce/woocommerce-admin#192 from woocommerce/add/filter-picker
Components: Add filter picker
This commit is contained in:
commit
e354bc264c
|
@ -9,18 +9,13 @@ import { Component, Fragment } from '@wordpress/element';
|
|||
* Internal dependencies
|
||||
*/
|
||||
import Header from 'layout/header';
|
||||
import DatePicker from 'components/date-picker';
|
||||
import DropdownButton from 'components/dropdown-button';
|
||||
|
||||
export default class extends Component {
|
||||
render() {
|
||||
const { query, path } = this.props;
|
||||
return (
|
||||
<Fragment>
|
||||
<Header sections={ [ __( 'Analytics', 'wc-admin' ) ] } />
|
||||
<DatePicker query={ query } path={ path } />
|
||||
<p>Example single line button - default width 100% of container</p>
|
||||
<DropdownButton labels={ [ 'All Products Sold' ] } />
|
||||
<p>Overview Section</p>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ import PropTypes from 'prop-types';
|
|||
*/
|
||||
import ExampleReport from './example';
|
||||
import RevenueReport from './revenue';
|
||||
import ProductsReport from './products';
|
||||
|
||||
class Report extends Component {
|
||||
render() {
|
||||
|
@ -17,6 +18,8 @@ class Report extends Component {
|
|||
switch ( params.report ) {
|
||||
case 'revenue':
|
||||
return <RevenueReport { ...this.props } />;
|
||||
case 'products':
|
||||
return <ProductsReport { ...this.props } />;
|
||||
default:
|
||||
return <ExampleReport />;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
/** @format */
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
|
||||
export const filters = [
|
||||
{ label: __( 'All Products', 'wc-admin' ), value: 'all' },
|
||||
{
|
||||
label: __( 'Single Product', 'wc-admin' ),
|
||||
value: 'single',
|
||||
subFilters: [
|
||||
{
|
||||
label: __( 'Single Product', 'wc-admin' ),
|
||||
component: 'Search',
|
||||
value: 'single_search',
|
||||
},
|
||||
],
|
||||
},
|
||||
{ label: __( 'Top Products by Items Sold', 'wc-admin' ), value: 'top_items' },
|
||||
{ label: __( 'Top Products by Gross Sales', 'wc-admin' ), value: 'top_sales' },
|
||||
{ label: __( 'Comparison', 'wc-admin' ), value: 'compare' },
|
||||
];
|
||||
|
||||
export const filterPaths = {
|
||||
all: [],
|
||||
single: [],
|
||||
single_search: [ 'single' ],
|
||||
top_items: [],
|
||||
top_sales: [],
|
||||
compare: [],
|
||||
};
|
|
@ -0,0 +1,54 @@
|
|||
/** @format */
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { Component, Fragment } from '@wordpress/element';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import Header from 'layout/header';
|
||||
import DatePicker from 'components/date-picker';
|
||||
import FilterPicker from 'components/filter-picker';
|
||||
import { filters, filterPaths } from './constants';
|
||||
import './style.scss';
|
||||
|
||||
export default class extends Component {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.getQueryParamValue = this.getQueryParamValue.bind( this );
|
||||
}
|
||||
|
||||
getQueryParamValue() {
|
||||
const { query } = this.props;
|
||||
return query.product || 'all';
|
||||
}
|
||||
|
||||
render() {
|
||||
const { query, path } = this.props;
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<Header
|
||||
sections={ [
|
||||
[ '/analytics', __( 'Analytics', 'wc-admin' ) ],
|
||||
__( 'Products', 'wc-admin' ),
|
||||
] }
|
||||
/>
|
||||
<div className="woocommerce-products__pickers">
|
||||
<DatePicker query={ query } path={ path } />
|
||||
<FilterPicker
|
||||
query={ query }
|
||||
path={ path }
|
||||
filters={ filters }
|
||||
filterPaths={ filterPaths }
|
||||
queryParam="product"
|
||||
getQueryParamValue={ this.getQueryParamValue }
|
||||
/>
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
/** @format */
|
||||
|
||||
.woocommerce-products__pickers {
|
||||
display: flex;
|
||||
|
||||
> div {
|
||||
margin-right: 24px;
|
||||
}
|
||||
|
||||
@include breakpoint( '<1100px' ) {
|
||||
flex-direction: column;
|
||||
|
||||
> div {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
Date Picker (Work in Progress)
|
||||
Date Picker
|
||||
===
|
||||
|
||||
Select a range of dates or single dates
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { Component, Fragment } from '@wordpress/element';
|
||||
import { Component } from '@wordpress/element';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { Dropdown } from '@wordpress/components';
|
||||
import { stringify as stringifyQueryObject } from 'qs';
|
||||
|
@ -76,10 +76,9 @@ class DatePicker extends Component {
|
|||
render() {
|
||||
const { period, compare, after, before } = this.state;
|
||||
return (
|
||||
<Fragment>
|
||||
<div className="woocommerce-date-picker">
|
||||
<p>{ __( 'Date Range', 'wc-admin' ) }:</p>
|
||||
<Dropdown
|
||||
className="woocommerce-date-picker"
|
||||
contentClassName="woocommerce-date-picker__content"
|
||||
position="bottom"
|
||||
expandOnMobile
|
||||
|
@ -103,7 +102,7 @@ class DatePicker extends Component {
|
|||
/>
|
||||
) }
|
||||
/>
|
||||
</Fragment>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
Filter Picker
|
||||
===
|
||||
|
||||
Modify a url query parameter via a dropdown selection of configurable options
|
||||
|
||||
## Usage
|
||||
|
||||
```jsx
|
||||
import FilterPicker from 'components/filter-picker';
|
||||
|
||||
const renderFilterPicker = ( { path, query } ) {
|
||||
const filters = [
|
||||
{ label: 'Breakfast', value: 'breakfast' },
|
||||
{ label: 'Lunch', value: 'lunch', subFilters: [
|
||||
{ label: 'Meat', value: 'meat' },
|
||||
{ label: 'Vegan', value: 'vegan' },
|
||||
{ label: 'Pescatarian', value: 'fish', subFilters: [
|
||||
{ label: 'Snapper', value: 'snapper' },
|
||||
{ label: 'Cod', value: 'cod' },
|
||||
] },
|
||||
// Specify a custom component to render (Work in Progress)
|
||||
{ label: 'Other', value: 'other_fish', component: 'OtherFish' },
|
||||
] },
|
||||
{ label: 'Dinner', value: 'dinner' },
|
||||
];
|
||||
|
||||
const filterPaths = {
|
||||
breakfast: [],
|
||||
lunch: [],
|
||||
dinner: [],
|
||||
meat: [ 'lunch' ],
|
||||
vegan: [ 'lunch' ],
|
||||
fish: [ 'lunch' ],
|
||||
snapper: [ 'lunch', 'fish' ],
|
||||
cod: [ 'lunch', 'fish' ],
|
||||
other_fish: [ 'lunch', 'fish' ],
|
||||
};
|
||||
|
||||
const queryParam = 'meal';
|
||||
|
||||
const getQueryParamValue = () => {
|
||||
const { query } = this.props;
|
||||
return return query[ queryParam ] || 'breakfast';
|
||||
}
|
||||
|
||||
return (
|
||||
<FilterPicker
|
||||
query={ query }
|
||||
path={ path }
|
||||
filters={ filters }
|
||||
filterPaths={ filterPaths }
|
||||
queryParam={ queryParam }
|
||||
getQueryParamValue={ getQueryParamValue }
|
||||
/>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Props
|
||||
|
||||
* `query` (required): The query string represented in object form
|
||||
* `path` (required): Parameter supplied by React-Router
|
||||
* `filters` (required): An array of filters and subFilters to construct the menu
|
||||
* `filterPaths` (required): A map of representing the structure of the tree. Required for faster lookups than searches
|
||||
* `queryParam` (required): The query parameter to update
|
||||
* `getQueryParamValue` (required): A function used to obtain the current value represented in the url
|
|
@ -0,0 +1,187 @@
|
|||
/** @format */
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { Component, Fragment, createRef } from '@wordpress/element';
|
||||
import { Dropdown, Button, Dashicon } from '@wordpress/components';
|
||||
import { stringify as stringifyQueryObject } from 'qs';
|
||||
import { omit, find, partial } from 'lodash';
|
||||
import classnames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import DropdownButton from 'components/dropdown-button';
|
||||
import Link from 'components/link';
|
||||
import './style.scss';
|
||||
|
||||
class FilterPicker extends Component {
|
||||
constructor( props ) {
|
||||
super( props );
|
||||
|
||||
const { filterPaths, getQueryParamValue } = props;
|
||||
this.state = {
|
||||
nav: filterPaths[ getQueryParamValue() ],
|
||||
};
|
||||
this.listRef = createRef();
|
||||
|
||||
this.getSelectionPath = this.getSelectionPath.bind( this );
|
||||
this.getOtherQueries = this.getOtherQueries.bind( this );
|
||||
this.getSelectedFilter = this.getSelectedFilter.bind( this );
|
||||
this.selectSubFilters = this.selectSubFilters.bind( this );
|
||||
this.getVisibleFilters = this.getVisibleFilters.bind( this );
|
||||
this.goBack = this.goBack.bind( this );
|
||||
}
|
||||
|
||||
getOtherQueries( query ) {
|
||||
const { queryParam } = this.props;
|
||||
return omit( query, queryParam );
|
||||
}
|
||||
|
||||
getSelectionPath( filter ) {
|
||||
const { path, query, queryParam } = this.props;
|
||||
const otherQueries = this.getOtherQueries( query );
|
||||
const data = {
|
||||
[ queryParam ]: filter.value,
|
||||
};
|
||||
const queryString = stringifyQueryObject( Object.assign( otherQueries, data ) );
|
||||
return `${ path }?${ queryString }`;
|
||||
}
|
||||
|
||||
getSelectedFilter() {
|
||||
const { filters, getQueryParamValue, filterPaths } = this.props;
|
||||
const value = getQueryParamValue();
|
||||
const filterPath = filterPaths[ value ];
|
||||
const visibleFilters = this.getVisibleFilters( filters, [ ...filterPath ] );
|
||||
return find( visibleFilters, filter => filter.value === value );
|
||||
}
|
||||
|
||||
getLabels( selectedFilter ) {
|
||||
// @TODO: handle single product secondary labels
|
||||
return selectedFilter ? [ selectedFilter.label ] : [];
|
||||
}
|
||||
|
||||
selectSubFilters( value ) {
|
||||
const nav = [ ...this.state.nav ];
|
||||
nav.push( value );
|
||||
this.setState( { nav } );
|
||||
this.focusFirstFilter();
|
||||
}
|
||||
|
||||
getVisibleFilters( filters, nav ) {
|
||||
if ( nav.length === 0 ) {
|
||||
return filters;
|
||||
}
|
||||
const value = nav.shift();
|
||||
const nextFilters = find( filters, filter => value === filter.value );
|
||||
return this.getVisibleFilters( nextFilters && nextFilters.subFilters, nav );
|
||||
}
|
||||
|
||||
goBack() {
|
||||
const nav = [ ...this.state.nav ];
|
||||
nav.pop();
|
||||
this.setState( { nav } );
|
||||
this.focusFirstFilter();
|
||||
}
|
||||
|
||||
focusFirstFilter() {
|
||||
setTimeout( () => {
|
||||
const list = this.listRef.current;
|
||||
if ( list.children.length && list.children[ 0 ].children.length ) {
|
||||
list.children[ 0 ].children[ 0 ].focus();
|
||||
}
|
||||
}, 0 );
|
||||
}
|
||||
|
||||
renderButton( filter, onClose ) {
|
||||
if ( filter.subFilters ) {
|
||||
return (
|
||||
<Button
|
||||
className="woocommerce-filter-picker__content-list-item-btn"
|
||||
onClick={ partial( this.selectSubFilters, filter.value ) }
|
||||
>
|
||||
{ filter.label }
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
if ( filter.component ) {
|
||||
return (
|
||||
<Fragment>
|
||||
<Button
|
||||
className="woocommerce-filter-picker__content-list-item-btn has-parent-nav"
|
||||
onClick={ this.goBack }
|
||||
>
|
||||
<Dashicon icon="arrow-left-alt2" />
|
||||
{ filter.label }
|
||||
</Button>
|
||||
<input
|
||||
type="text"
|
||||
style={ { width: '100%', margin: '0' } }
|
||||
placeholder="Search Placeholder"
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Link
|
||||
className="woocommerce-filter-picker__content-list-item-btn components-button"
|
||||
to={ this.getSelectionPath( filter ) }
|
||||
onClick={ onClose }
|
||||
>
|
||||
{ filter.label }
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { filters } = this.props;
|
||||
const visibleFilters = this.getVisibleFilters( filters, [ ...this.state.nav ] );
|
||||
const selectedFilter = this.getSelectedFilter();
|
||||
return (
|
||||
<div className="woocommerce-filter-picker">
|
||||
<p>{ __( 'Show', 'wc-admin' ) }:</p>
|
||||
<Dropdown
|
||||
contentClassName="woocommerce-filter-picker__content"
|
||||
position="bottom"
|
||||
expandOnMobile
|
||||
headerTitle={ __( 'filter report to show:', 'wc-admin' ) }
|
||||
renderToggle={ ( { isOpen, onToggle } ) => (
|
||||
<DropdownButton
|
||||
onClick={ onToggle }
|
||||
isOpen={ isOpen }
|
||||
labels={ this.getLabels( selectedFilter ) }
|
||||
/>
|
||||
) }
|
||||
renderContent={ ( { onClose } ) => (
|
||||
<ul className="woocommerce-filter-picker__content-list" ref={ this.listRef }>
|
||||
{ visibleFilters.map( filter => (
|
||||
<li
|
||||
className={ classnames( 'woocommerce-filter-picker__content-list-item', {
|
||||
'is-selected': selectedFilter.value === filter.value,
|
||||
} ) }
|
||||
>
|
||||
{ this.renderButton( filter, onClose ) }
|
||||
</li>
|
||||
) ) }
|
||||
</ul>
|
||||
) }
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
FilterPicker.propTypes = {
|
||||
path: PropTypes.string.isRequired,
|
||||
query: PropTypes.object.isRequired,
|
||||
filters: PropTypes.array.isRequired,
|
||||
filterPaths: PropTypes.object.isRequired,
|
||||
queryParam: PropTypes.string.isRequired,
|
||||
getQueryParamValue: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default FilterPicker;
|
|
@ -0,0 +1,101 @@
|
|||
/** @format */
|
||||
|
||||
.woocommerce-filter-picker {
|
||||
width: 33.3%;
|
||||
|
||||
@include breakpoint( '<1100px' ) {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
@include breakpoint( '<782px' ) {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.woocommerce-filter-picker__content {
|
||||
.components-popover__content {
|
||||
width: 320px;
|
||||
border: 1px solid $core-grey-light-700;
|
||||
background-color: $white;
|
||||
}
|
||||
|
||||
&.is-mobile {
|
||||
.components-popover__content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.components-popover__header-title {
|
||||
@include font-size( 12 );
|
||||
font-weight: 100;
|
||||
text-transform: uppercase;
|
||||
text-align: center;
|
||||
color: $core-grey-dark-500;
|
||||
}
|
||||
|
||||
.woocommerce-filter-picker__content-list
|
||||
.woocommerce-filter-picker__content-list-item:last-child {
|
||||
border-bottom: 1px solid $core-grey-light-700;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.woocommerce-filter-picker__content-list {
|
||||
margin: 0;
|
||||
|
||||
.woocommerce-filter-picker__content-list-item {
|
||||
border-bottom: 1px solid $core-grey-light-700;
|
||||
margin: 0;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&.is-selected {
|
||||
.woocommerce-filter-picker__content-list-item-btn {
|
||||
background-color: $white;
|
||||
|
||||
&.components-button:not(:disabled):not([aria-disabled='true']):focus {
|
||||
background-color: $white;
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background-color: $woocommerce;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 1em;
|
||||
transform: translate(50%, -50%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.woocommerce-filter-picker__content-list-item-btn {
|
||||
padding: 1em 1em 1em 3em;
|
||||
display: block;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
background-color: $core-grey-light-100;
|
||||
color: $core-grey-dark-500;
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
background-color: $core-grey-light-200;
|
||||
color: $core-grey-dark-500;
|
||||
}
|
||||
|
||||
&.components-button:not(:disabled):not([aria-disabled='true']):focus {
|
||||
background-color: $core-grey-light-100;
|
||||
}
|
||||
|
||||
.dashicon {
|
||||
position: absolute;
|
||||
left: 1em;
|
||||
top: 50%;
|
||||
transform: translate(0, -50%);
|
||||
}
|
||||
}
|
|
@ -74,6 +74,15 @@ function wc_admin_register_pages(){
|
|||
'wc-admin#/analytics/revenue',
|
||||
'wc_admin_page'
|
||||
);
|
||||
|
||||
add_submenu_page(
|
||||
'wc-admin#/analytics',
|
||||
__( 'Products', 'wc-admin' ),
|
||||
__( 'Products', 'wc-admin' ),
|
||||
'manage_options',
|
||||
'wc-admin#/analytics/products',
|
||||
'wc_admin_page'
|
||||
);
|
||||
}
|
||||
add_action( 'admin_menu', 'wc_admin_register_pages' );
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue