diff --git a/plugins/woocommerce-admin/client/analytics/report/orders/config.js b/plugins/woocommerce-admin/client/analytics/report/orders/config.js index b9cf5e9f0b8..64c2076b0e3 100644 --- a/plugins/woocommerce-admin/client/analytics/report/orders/config.js +++ b/plugins/woocommerce-admin/client/analytics/report/orders/config.js @@ -18,7 +18,7 @@ export const filters = [ ]; /*eslint-disable max-len*/ -export const advancedFilterConfig = { +export const advancedFilters = { title: _x( 'Orders Match {{select /}} Filters', 'A sentence describing filters for Orders. See screen shot for context: https://cloudup.com/cSsUY9VeCVJ', diff --git a/plugins/woocommerce-admin/client/analytics/report/orders/index.js b/plugins/woocommerce-admin/client/analytics/report/orders/index.js index b017de1c388..87730e0cfac 100644 --- a/plugins/woocommerce-admin/client/analytics/report/orders/index.js +++ b/plugins/woocommerce-admin/client/analytics/report/orders/index.js @@ -13,11 +13,11 @@ import { get } from 'lodash'; * Internal dependencies */ import { EmptyContent, ReportFilters } from '@woocommerce/components'; -import { filters, advancedFilterConfig } from './config'; +import { filters, advancedFilters } from './config'; import { getAdminLink } from 'lib/nav-utils'; import { appendTimestamp, getCurrentDates } from 'lib/date'; import { QUERY_DEFAULTS } from 'store/constants'; -import { getReportChartData } from 'store/reports/utils'; +import { getReportChartData, getFilterQuery } from 'store/reports/utils'; import OrdersReportChart from './chart'; import OrdersReportTable from './table'; @@ -71,7 +71,7 @@ class OrdersReport extends Component { query={ query } path={ path } filters={ filters } - advancedConfig={ advancedFilterConfig } + advancedConfig={ advancedFilters } /> ( { id: product.id, label: product.name, @@ -40,7 +40,7 @@ export const filters = [ value: 'compare-product', settings: { type: 'products', - param: 'product', + param: 'products', getLabels: getRequestByIdString( NAMESPACE + 'products', product => ( { id: product.id, label: product.name, @@ -58,7 +58,7 @@ export const filters = [ value: 'compare-product_cat', settings: { type: 'product_cats', - param: 'product_cat', + param: 'categories', getLabels: getRequestByIdString( NAMESPACE + 'products/categories', category => ( { id: category.id, label: category.name, @@ -74,9 +74,11 @@ export const filters = [ { label: __( 'Top Products by Items Sold', 'wc-admin' ), value: 'top_items', + query: { orderby: 'items_sold', order: 'desc' }, }, { - label: __( 'Top Products by Gross Sales', 'wc-admin' ), + label: __( 'Top Products by Gross Revenue', 'wc-admin' ), value: 'top_sales', + query: { orderby: 'gross_revenue', order: 'desc' }, }, ]; diff --git a/plugins/woocommerce-admin/client/analytics/report/products/index.js b/plugins/woocommerce-admin/client/analytics/report/products/index.js index 6e073fdd7c3..3d8b6b7e4c4 100644 --- a/plugins/woocommerce-admin/client/analytics/report/products/index.js +++ b/plugins/woocommerce-admin/client/analytics/report/products/index.js @@ -14,7 +14,7 @@ import { filters } from './config'; import { ReportFilters } from '@woocommerce/components'; import { appendTimestamp, getCurrentDates } from 'lib/date'; import { QUERY_DEFAULTS } from 'store/constants'; -import { getReportChartData } from 'store/reports/utils'; +import { getFilterQuery, getReportChartData } from 'store/reports/utils'; import ProductsReportChart from './chart'; import ProductsReportTable from './table'; @@ -27,6 +27,7 @@ class ProductsReport extends Component { primaryData, products, query, + tableQuery, } = this.props; return ( @@ -38,6 +39,7 @@ class ProductsReport extends Component { isRequesting={ isProductsRequesting || primaryData.isRequesting } products={ products } query={ query } + tableQuery={ tableQuery } totalRows={ get( primaryData, [ 'data', 'totals', 'products_count' ], @@ -56,6 +58,7 @@ export default compose( const primaryData = getReportChartData( 'products', 'primary', query, select ); const { getProducts, isGetProductsError, isGetProductsRequesting } = select( 'wc-admin' ); + const filterQuery = getFilterQuery( 'products', query ); const tableQuery = { orderby: query.orderby || 'items_sold', order: query.order || 'desc', @@ -64,6 +67,7 @@ export default compose( after: appendTimestamp( datesFromQuery.primary.after, 'start' ), before: appendTimestamp( datesFromQuery.primary.before, 'end' ), extended_product_info: true, + ...filterQuery, }; const products = getProducts( tableQuery ); const isProductsError = isGetProductsError( tableQuery ); @@ -74,6 +78,7 @@ export default compose( isProductsRequesting, primaryData, products, + tableQuery, }; } ) )( ProductsReport ); diff --git a/plugins/woocommerce-admin/client/analytics/report/products/table.js b/plugins/woocommerce-admin/client/analytics/report/products/table.js index 7b6df491b7e..87cbcb09348 100644 --- a/plugins/woocommerce-admin/client/analytics/report/products/table.js +++ b/plugins/woocommerce-admin/client/analytics/report/products/table.js @@ -193,18 +193,12 @@ export default class ProductsReportTable extends Component { } render() { - const { isError, isRequesting, query } = this.props; + const { isError, isRequesting, tableQuery } = this.props; if ( isError ) { return ; } - const tableQuery = { - ...query, - orderby: query.orderby || 'items_sold', - order: query.order || 'desc', - }; - if ( isRequesting ) { return this.renderPlaceholderTable( tableQuery ); } diff --git a/plugins/woocommerce-admin/client/components/filters/filter/index.js b/plugins/woocommerce-admin/client/components/filters/filter/index.js index f9b3c7a0e3a..114c5ee25f0 100644 --- a/plugins/woocommerce-admin/client/components/filters/filter/index.js +++ b/plugins/woocommerce-admin/client/components/filters/filter/index.js @@ -6,7 +6,7 @@ import { __ } from '@wordpress/i18n'; import { Button, Dropdown, IconButton } from '@wordpress/components'; import classnames from 'classnames'; import { Component } from '@wordpress/element'; -import { find, omit, partial, last, get, includes } from 'lodash'; +import { find, partial, last, get, includes } from 'lodash'; import PropTypes from 'prop-types'; /** @@ -16,6 +16,7 @@ import AnimationSlider from 'components/animation-slider'; import DropdownButton from 'components/dropdown-button'; import Search from 'components/search'; import { getTimeRelatedQuery, updateQueryString } from 'lib/nav-utils'; +import { flatenFilters } from './utils'; import './style.scss'; export const DEFAULT_FILTER = 'all'; @@ -52,23 +53,9 @@ class FilterPicker extends Component { this.setState( { selectedTag: tags[ 0 ] } ); } - getAllFilters( filters ) { - const allFilters = []; - filters.forEach( f => { - if ( ! f.subFilters ) { - allFilters.push( f ); - } else { - allFilters.push( omit( f, 'subFilters' ) ); - const subFilters = this.getAllFilters( f.subFilters ); - allFilters.push( ...subFilters ); - } - } ); - return allFilters; - } - getFilter( value = false ) { const { filters, query } = this.props; - const allFilters = this.getAllFilters( filters ); + const allFilters = flatenFilters( filters ); value = value || query.filter || DEFAULT_FILTER; return find( allFilters, { value } ) || {}; } @@ -142,7 +129,7 @@ class FilterPicker extends Component { const selectFilter = event => { onClose( event ); - this.update( filter.value ); + this.update( filter.value, filter.query || {} ); this.setState( { selectedTag: null } ); }; diff --git a/plugins/woocommerce-admin/client/components/filters/filter/utils.js b/plugins/woocommerce-admin/client/components/filters/filter/utils.js new file mode 100644 index 00000000000..78c0235a70e --- /dev/null +++ b/plugins/woocommerce-admin/client/components/filters/filter/utils.js @@ -0,0 +1,19 @@ +/** @format */ +/** + * External dependencies + */ +import { omit } from 'lodash'; + +export function flatenFilters( filters ) { + const allFilters = []; + filters.forEach( f => { + if ( ! f.subFilters ) { + allFilters.push( f ); + } else { + allFilters.push( omit( f, 'subFilters' ) ); + const subFilters = flatenFilters( f.subFilters ); + allFilters.push( ...subFilters ); + } + } ); + return allFilters; +} diff --git a/plugins/woocommerce-admin/client/store/reports/test/utils.js b/plugins/woocommerce-admin/client/store/reports/test/utils.js index d81ff76a201..09c91682336 100644 --- a/plugins/woocommerce-admin/client/store/reports/test/utils.js +++ b/plugins/woocommerce-admin/client/store/reports/test/utils.js @@ -5,7 +5,8 @@ /** * Internal dependencies */ -import { isReportDataEmpty, getReportChartData, getSummaryNumbers } from '../utils'; +import { isReportDataEmpty, getReportChartData, getSummaryNumbers, getFilterQuery } from '../utils'; +import * as ordersConfig from 'analytics/report/orders/config'; describe( 'isReportDataEmpty()', () => { it( 'returns false if report is valid', () => { @@ -336,3 +337,90 @@ describe( 'getSummaryNumbers()', () => { expect( result ).toEqual( { ...response, totals } ); } ); } ); + +describe( 'getFilterQuery', () => { + /** + * Mock the orders config + */ + const filters = [ + { value: 'top_meal', query: { lunch: 'burritos' } }, + { value: 'top_dessert', query: { dinner: 'ice_cream' } }, + { value: 'compare-cuisines', settings: { param: 'region' } }, + { + value: 'food_destination', + subFilters: [ { value: 'choose_a_european_city', settings: { param: 'european_cities' } } ], + }, + ]; + + const advancedFilters = { + filters: { + mexican: { + rules: [ { value: 'is' }, { value: 'is_not' } ], + }, + french: { + rules: [ { value: 'includes' }, { value: 'excludes' } ], + }, + }, + }; + + ordersConfig.filters = filters; + ordersConfig.advancedFilters = advancedFilters; + + it( 'should return an empty object if no filter param is given', () => { + const query = {}; + const filterQuery = getFilterQuery( 'orders', query ); + + expect( filterQuery ).toEqual( {} ); + } ); + + it( 'should return an empty object if filter parameter is not in configs', () => { + const query = { filter: 'canned_meat' }; + const filterQuery = getFilterQuery( 'orders', query ); + + expect( filterQuery ).toEqual( {} ); + } ); + + it( 'should return the query for an advanced filter', () => { + const query = { filter: 'advanced', mexican_is: 'delicious' }; + const filterQuery = getFilterQuery( 'orders', query ); + + expect( filterQuery ).toEqual( { mexican_is: 'delicious', match: 'all' } ); + } ); + + it( 'should ignore other queries not defined by filter configs', () => { + const query = { + filter: 'advanced', + mexican_is_not: 'healthy', + orderby: 'calories', + topping: 'salsa-verde', + }; + const filterQuery = getFilterQuery( 'orders', query ); + + expect( filterQuery ).toEqual( { mexican_is_not: 'healthy', match: 'all' } ); + } ); + + it( 'should apply the match parameter advanced filters', () => { + const query = { + filter: 'advanced', + french_includes: 'le-fromage', + match: 'any', + }; + const filterQuery = getFilterQuery( 'orders', query ); + + expect( filterQuery ).toEqual( { french_includes: 'le-fromage', match: 'any' } ); + } ); + + it( 'should return the query for compare filters', () => { + const query = { filter: 'compare-cuisines', region: 'vietnam,malaysia,thailand' }; + const filterQuery = getFilterQuery( 'orders', query ); + + expect( filterQuery ).toEqual( { region: 'vietnam,malaysia,thailand' } ); + } ); + + it( 'should return the query for subFilters', () => { + const query = { filter: 'choose_a_european_city', european_cities: 'paris,rome,barcelona' }; + const filterQuery = getFilterQuery( 'orders', query ); + + expect( filterQuery ).toEqual( { european_cities: 'paris,rome,barcelona' } ); + } ); +} ); diff --git a/plugins/woocommerce-admin/client/store/reports/utils.js b/plugins/woocommerce-admin/client/store/reports/utils.js index b3fd51a2c90..7ce7f98804f 100644 --- a/plugins/woocommerce-admin/client/store/reports/utils.js +++ b/plugins/woocommerce-admin/client/store/reports/utils.js @@ -3,13 +3,71 @@ /** * External dependencies */ -import { forEach, isNull } from 'lodash'; +import { find, forEach, isNull } from 'lodash'; /** * Internal dependencies */ import { MAX_PER_PAGE } from 'store/constants'; import { appendTimestamp, getCurrentDates, getIntervalForQuery } from 'lib/date'; +import { getActiveFiltersFromQuery, getUrlKey } from 'components/filters/advanced/utils'; +import { flatenFilters } from 'components/filters/filter/utils'; +import * as couponsConfig from 'analytics/report/coupons/config'; +import * as ordersConfig from 'analytics/report/orders/config'; +import * as productsConfig from 'analytics/report/products/config'; + +const reportConfigs = { + coupons: couponsConfig, + orders: ordersConfig, + products: productsConfig, +}; + +export function getFilterQuery( endpoint, query ) { + const { filter } = query; + + if ( ! filter ) { + return {}; + } + + const { filters = [], advancedFilters = {} } = reportConfigs[ endpoint ]; + + if ( 'advanced' === filter ) { + const activeFilters = getActiveFiltersFromQuery( query, advancedFilters.filters ); + + if ( activeFilters.length === 0 ) { + return {}; + } + + return activeFilters.reduce( + ( result, activeFilter ) => { + const { key, rule, value } = activeFilter; + result[ getUrlKey( key, rule ) ] = value; + return result; + }, + { match: query.match || 'all' } + ); + } + + const filterConfig = find( flatenFilters( filters ), { value: filter } ); + + if ( ! filterConfig ) { + return {}; + } + + if ( filterConfig.settings && filterConfig.settings.param ) { + const { param } = filterConfig.settings; + + if ( query[ param ] ) { + return { + [ param ]: query[ param ], + }; + } + + return {}; + } + + return {}; +} /** * Returns true if a report object is empty. @@ -36,19 +94,22 @@ export function isReportDataEmpty( report ) { /** * Constructs and returns a query associated with a Report data request. * + * @param {String} endpoint Report API Endpoint * @param {String} dataType 'primary' or 'secondary'. * @param {Object} query query parameters in the url. * @returns {Object} data request query parameters. */ -function getRequestQuery( dataType, query ) { +function getRequestQuery( endpoint, dataType, query ) { const datesFromQuery = getCurrentDates( query ); const interval = getIntervalForQuery( query ); + const filterQuery = getFilterQuery( endpoint, query ); return { order: 'asc', interval, per_page: MAX_PER_PAGE, after: appendTimestamp( datesFromQuery[ dataType ].after, 'start' ), before: appendTimestamp( datesFromQuery[ dataType ].before, 'end' ), + ...filterQuery, }; } @@ -71,7 +132,7 @@ export function getSummaryNumbers( endpoint, query, select ) { }, }; - const primaryQuery = getRequestQuery( 'primary', query ); + const primaryQuery = getRequestQuery( endpoint, 'primary', query ); const primary = getReportStats( endpoint, primaryQuery ); if ( isReportStatsRequesting( endpoint, primaryQuery ) ) { return { ...response, isRequesting: true }; @@ -81,7 +142,7 @@ export function getSummaryNumbers( endpoint, query, select ) { const primaryTotals = ( primary && primary.data && primary.data.totals ) || null; - const secondaryQuery = getRequestQuery( 'secondary', query ); + const secondaryQuery = getRequestQuery( endpoint, 'secondary', query ); const secondary = getReportStats( endpoint, secondaryQuery ); if ( isReportStatsRequesting( endpoint, secondaryQuery ) ) { return { ...response, isRequesting: true }; @@ -116,7 +177,7 @@ export function getReportChartData( endpoint, dataType, query, select ) { }, }; - const requestQuery = getRequestQuery( dataType, query ); + const requestQuery = getRequestQuery( endpoint, dataType, query ); const stats = getReportStats( endpoint, requestQuery ); if ( isReportStatsRequesting( endpoint, requestQuery ) ) { diff --git a/plugins/woocommerce-admin/package-lock.json b/plugins/woocommerce-admin/package-lock.json index d86915d19f1..30e829ad88e 100644 --- a/plugins/woocommerce-admin/package-lock.json +++ b/plugins/woocommerce-admin/package-lock.json @@ -5831,8 +5831,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true, - "optional": true + "dev": true }, "aproba": { "version": "1.2.0", @@ -5856,15 +5855,13 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true, - "optional": true + "dev": true }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -5881,22 +5878,19 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", - "dev": true, - "optional": true + "dev": true }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true, - "optional": true + "dev": true }, "console-control-strings": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", - "dev": true, - "optional": true + "dev": true }, "core-util-is": { "version": "1.0.2", @@ -6027,8 +6021,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true, - "optional": true + "dev": true }, "ini": { "version": "1.3.5", @@ -6042,7 +6035,6 @@ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", "dev": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -6059,7 +6051,6 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", "dev": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -6068,15 +6059,13 @@ "version": "0.0.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", - "dev": true, - "optional": true + "dev": true }, "minipass": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.2.4.tgz", "integrity": "sha512-hzXIWWet/BzWhYs2b+u7dRHlruXhwdgvlTMDKC6Cb1U7ps6Ac6yQlR39xsbjWJE377YTCtKwIXIpJ5oP+j5y8g==", "dev": true, - "optional": true, "requires": { "safe-buffer": "^5.1.1", "yallist": "^3.0.0" @@ -6097,7 +6086,6 @@ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", "dev": true, - "optional": true, "requires": { "minimist": "0.0.8" } @@ -6186,8 +6174,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", - "dev": true, - "optional": true + "dev": true }, "object-assign": { "version": "4.1.1", @@ -6201,7 +6188,6 @@ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", "dev": true, - "optional": true, "requires": { "wrappy": "1" } @@ -6297,8 +6283,7 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==", - "dev": true, - "optional": true + "dev": true }, "safer-buffer": { "version": "2.1.2", @@ -6340,7 +6325,6 @@ "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", "dev": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -6362,7 +6346,6 @@ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "dev": true, - "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -6411,15 +6394,13 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true, - "optional": true + "dev": true }, "yallist": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.2.tgz", "integrity": "sha1-hFK0u36Dx8GI2AQcGoN8dz1ti7k=", - "dev": true, - "optional": true + "dev": true } } }, diff --git a/plugins/woocommerce-admin/tests/js/setup-globals.js b/plugins/woocommerce-admin/tests/js/setup-globals.js index 4da8f2f6ae8..57659768ce8 100644 --- a/plugins/woocommerce-admin/tests/js/setup-globals.js +++ b/plugins/woocommerce-admin/tests/js/setup-globals.js @@ -35,6 +35,15 @@ global.wcSettings = { date: { dow: 0, }, + orderStatuses: { + 'wc-pending': 'Pending payment', + 'wc-processing': 'Processing', + 'wc-on-hold': 'On hold', + 'wc-completed': 'Completed', + 'wc-cancelled': 'Cancelled', + 'wc-refunded': 'Refunded', + 'wc-failed': 'Failed', + }, }; setLocaleData( { '': { domain: 'wc-admin', lang: 'en_US' } }, 'wc-admin' );