From c9228aa79991f6c4ef67d977942f5ebb59ab634d Mon Sep 17 00:00:00 2001 From: Paul Sealock Date: Tue, 11 Dec 2018 11:06:21 +1300 Subject: [PATCH] Add calendar to date advanced filter --- client/analytics/report/customers/config.js | 30 +++ .../src/filters/advanced/date-filter.js | 223 ++++++++++++++++++ .../components/src/filters/advanced/index.js | 10 + .../src/filters/advanced/number-filter.js | 2 +- .../src/filters/advanced/style.scss | 2 +- 5 files changed, 265 insertions(+), 2 deletions(-) create mode 100644 packages/components/src/filters/advanced/date-filter.js diff --git a/client/analytics/report/customers/config.js b/client/analytics/report/customers/config.js index 5ee8e7b1bcb..ab1cac2b101 100644 --- a/client/analytics/report/customers/config.js +++ b/client/analytics/report/customers/config.js @@ -251,6 +251,36 @@ export const advancedFilters = { component: 'Currency', }, }, + registered: { + labels: { + add: __( 'Registered', 'wc-admin' ), + remove: __( 'Remove registered filter', 'wc-admin' ), + rule: __( 'Select a registered filter match', 'wc-admin' ), + /* translators: A sentence describing a Product filter. See screen shot for context: https://cloudup.com/cCsm3GeXJbE */ + title: __( 'Registered {{rule /}} {{filter /}}', 'wc-admin' ), + filter: __( 'Select registered date', 'wc-admin' ), + }, + rules: [ + { + value: 'before', + /* translators: Sentence fragment, logical, "Before" refers to customers registered before a given date. Screenshot for context: https://cloudup.com/cCsm3GeXJbE */ + label: _x( 'Before', 'date', 'wc-admin' ), + }, + { + value: 'after', + /* translators: Sentence fragment, logical, "after" refers to customers registered after a given date. Screenshot for context: https://cloudup.com/cCsm3GeXJbE */ + label: _x( 'After', 'date', 'wc-admin' ), + }, + { + value: 'between', + /* translators: Sentence fragment, logical, "Between" refers to average order value of a customer, between two given amounts. Screenshot for context: https://cloudup.com/cCsm3GeXJbE */ + label: _x( 'Between', 'date', 'wc-admin' ), + }, + ], + input: { + component: 'Date', + }, + }, }, }; /*eslint-enable max-len*/ diff --git a/packages/components/src/filters/advanced/date-filter.js b/packages/components/src/filters/advanced/date-filter.js new file mode 100644 index 00000000000..d4523d3a2c5 --- /dev/null +++ b/packages/components/src/filters/advanced/date-filter.js @@ -0,0 +1,223 @@ +/** @format */ +/** + * External dependencies + */ +import { Component, Fragment } from '@wordpress/element'; +import interpolateComponents from 'interpolate-components'; +import { SelectControl } from '@wordpress/components'; +import { find, partial } from 'lodash'; +import classnames from 'classnames'; +import { __, _x } from '@wordpress/i18n'; + +/** + * WooCommerce dependencies + */ +import { isoDateFormat, toMoment } from '@woocommerce/date'; + +/** + * Internal dependencies + */ +import DatePicker from '../../calendar/date-picker'; +import { textContent } from './utils'; + +const dateStringFormat = __( 'MMM D, YYYY', 'wc-admin' ); +const dateFormat = __( 'MM/DD/YYYY', 'wc-admin' ); + +class DateFilter extends Component { + constructor( { filter } ) { + super( ...arguments ); + + const [ isoAfter, isoBefore ] = ( filter.value || '' ).split( ',' ); + const after = isoAfter ? toMoment( isoDateFormat, isoAfter ) : null; + const before = isoBefore ? toMoment( isoDateFormat, isoBefore ) : null; + + this.state = { + before, + beforeText: before ? before.format( dateFormat ) : '', + beforeError: null, + after, + afterText: after ? after.format( dateFormat ) : '', + afterError: null, + }; + + this.onSingleDateChange = this.onSingleDateChange.bind( this ); + this.onRangeDateChange = this.onRangeDateChange.bind( this ); + } + + getBetweenString() { + return _x( + '{{after /}}{{span}} and {{/span}}{{before /}}', + 'Date range inputs arranged on a single line', + 'wc-admin' + ); + } + + getScreenReaderText( filter, config ) { + const rule = find( config.rules, { value: filter.rule } ) || {}; + + const { before, after } = this.state; + + // Return nothing if we're missing input(s) + if ( ! before || 'between' === rule.value && ! after ) { + return ''; + } + + let filterStr = before.format( dateStringFormat ); + + if ( 'between' === rule.value ) { + filterStr = interpolateComponents( { + mixedString: this.getBetweenString(), + components: { + after: { after.format( dateStringFormat ) }, + before: { before.format( dateStringFormat ) }, + span: , + }, + } ); + } + + return textContent( interpolateComponents( { + mixedString: config.labels.title, + components: { + filter: { filterStr }, + rule: { rule.label }, + }, + } ) ); + } + + onSingleDateChange( { date, text, error } ) { + const { filter, onFilterChange } = this.props; + this.setState( { before: date, beforeText: text, beforeError: error } ); + + if ( date ) { + onFilterChange( filter.key, 'value', date.format( isoDateFormat ) ); + } + } + + onRangeDateChange( input, { date, text, error } ) { + const { filter, onFilterChange } = this.props; + + this.setState( { + [ input ]: date, + [ input + 'Text' ]: text, + [ input + 'Error' ]: error, + } ); + + if ( date ) { + const { before, after } = this.state; + let nextAfter = null; + let nextBefore = null; + + if ( 'after' === input ) { + nextAfter = date.format( isoDateFormat ); + nextBefore = before ? before.format( isoDateFormat ) : null; + } + + if ( 'before' === input ) { + nextAfter = after ? after.format( isoDateFormat ) : null; + nextBefore = date.format( isoDateFormat ); + } + + if ( nextAfter && nextBefore ) { + onFilterChange( filter.key, 'value', [ nextAfter, nextBefore ].join( ',' ) ); + } + } + } + + getFilterInputs() { + const { filter } = this.props; + const { before, beforeText, beforeError, after, afterText, afterError } = this.state; + + if ( 'between' === filter.rule ) { + return interpolateComponents( { + mixedString: this.getBetweenString(), + components: { + after: ( + + ), + before: ( + + ), + span: , + }, + } ); + } + + return ( + + ); + } + + render() { + const { config, filter, onFilterChange, isEnglish } = this.props; + const { key, rule } = filter; + const { labels, rules } = config; + const screenReaderText = this.getScreenReaderText( filter, config ); + const children = interpolateComponents( { + mixedString: labels.title, + components: { + rule: ( + + ), + filter: ( +
+ { this.getFilterInputs() } +
+ ), + }, + } ); + /*eslint-disable jsx-a11y/no-noninteractive-tabindex*/ + return ( +
+ + { labels.add || '' } + +
+ { children } +
+ { screenReaderText && ( + + { screenReaderText } + + ) } +
+ ); + /*eslint-enable jsx-a11y/no-noninteractive-tabindex*/ + } +} + +export default DateFilter; diff --git a/packages/components/src/filters/advanced/index.js b/packages/components/src/filters/advanced/index.js index 4b39460a513..8f71516ffb0 100644 --- a/packages/components/src/filters/advanced/index.js +++ b/packages/components/src/filters/advanced/index.js @@ -28,6 +28,7 @@ import Link from '../../link'; import SelectFilter from './select-filter'; import SearchFilter from './search-filter'; import NumberFilter from './number-filter'; +import DateFilter from './date-filter'; const matches = [ { value: 'all', label: __( 'All', 'wc-admin' ) }, @@ -210,6 +211,15 @@ class AdvancedFilters extends Component { query={ query } /> ) } + { 'Date' === input.component && ( + + ) } diff --git a/packages/components/src/filters/advanced/style.scss b/packages/components/src/filters/advanced/style.scss index 3f1194fe673..9ec4f9f72bf 100644 --- a/packages/components/src/filters/advanced/style.scss +++ b/packages/components/src/filters/advanced/style.scss @@ -171,7 +171,7 @@ } } -.woocommerce-filters-advanced__input-numeric-range { +.woocommerce-filters-advanced__input-range { align-items: center; display: grid; grid-template-columns: 1fr;