Merge pull request woocommerce/woocommerce-admin#192 from woocommerce/add/filter-picker

Components: Add filter picker
This commit is contained in:
Paul Sealock 2018-07-17 15:26:19 +12:00 committed by GitHub
commit e354bc264c
12 changed files with 2548 additions and 16121 deletions

View File

@ -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>
);
}

View File

@ -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 />;
}

View File

@ -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: [],
};

View File

@ -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>
);
}
}

View File

@ -0,0 +1,17 @@
/** @format */
.woocommerce-products__pickers {
display: flex;
> div {
margin-right: 24px;
}
@include breakpoint( '<1100px' ) {
flex-direction: column;
> div {
margin-right: 0;
}
}
}

View File

@ -1,4 +1,4 @@
Date Picker (Work in Progress)
Date Picker
===
Select a range of dates or single dates

View File

@ -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>
);
}
}

View File

@ -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

View File

@ -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;

View File

@ -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%);
}
}

View File

@ -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