Merge pull request woocommerce/woocommerce-admin#419 from woocommerce/add/coupons-autocompleter-plumbing

Add Coupons autocompleter and relevant configs
This commit is contained in:
Paul Sealock 2018-09-21 11:57:56 +12:00 committed by GitHub
commit 679ce51bd1
20 changed files with 218 additions and 66 deletions

View File

@ -0,0 +1,33 @@
/** @format */
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
/**
* Internal dependencies
*/
import { getRequestByIdString } from 'lib/async-requests';
import { NAMESPACE } from 'store/constants';
export const filters = [
{ label: __( 'All Coupons', 'wc-admin' ), value: 'all' },
{
label: __( 'Comparison', 'wc-admin' ),
value: 'compare',
settings: {
type: 'coupons',
param: 'coupon',
getLabels: getRequestByIdString( NAMESPACE + 'coupons', coupon => ( {
id: coupon.id,
label: coupon.code,
} ) ),
labels: {
title: __( 'Compare Coupon Codes', 'wc-admin' ),
update: __( 'Compare', 'wc-admin' ),
},
},
},
{ label: __( 'Top Coupons by Discounted Orders', 'wc-admin' ), value: 'top_orders' },
{ label: __( 'Top Coupons by Gross Discounted', 'wc-admin' ), value: 'top_discount' },
];

View File

@ -0,0 +1,23 @@
/** @format */
/**
* External dependencies
*/
import { Component, Fragment } from '@wordpress/element';
/**
* Internal dependencies
*/
import { filters } from './config';
import { ReportFilters } from '@woocommerce/components';
export default class extends Component {
render() {
const { query, path } = this.props;
return (
<Fragment>
<ReportFilters query={ query } path={ path } filters={ filters } />
</Fragment>
);
}
}

View File

@ -16,6 +16,7 @@ import Header from 'layout/header';
import OrdersReport from './orders';
import ProductsReport from './products';
import RevenueReport from './revenue';
import CouponsReport from './coupons';
import useFilters from 'components/higher-order/use-filters';
const REPORTS_FILTER = 'woocommerce-reports-list';
@ -37,6 +38,11 @@ const getReports = () => {
title: __( 'Orders', 'wc-admin' ),
component: OrdersReport,
},
{
report: 'coupons',
title: __( 'Coupons', 'wc-admin' ),
component: CouponsReport,
},
] );
return reports;

View File

@ -7,7 +7,8 @@ import { __ } from '@wordpress/i18n';
/**
* Internal dependencies
*/
import { getProductLabelsById } from 'analytics/report/products/config';
import { getRequestByIdString } from 'lib/async-requests';
import { NAMESPACE } from 'store/constants';
export const filters = [
{ label: __( 'All Orders', 'wc-admin' ), value: 'all' },
@ -69,7 +70,10 @@ export const advancedFilterConfig = {
input: {
component: 'Search',
type: 'products',
getLabels: getProductLabelsById,
getLabels: getRequestByIdString( NAMESPACE + 'products', product => ( {
id: product.id,
label: product.name,
} ) ),
},
},
code: {
@ -88,7 +92,11 @@ export const advancedFilterConfig = {
],
input: {
component: 'Search',
type: 'products', // For now. "coupons" autocompleter required
type: 'coupons',
getLabels: getRequestByIdString( NAMESPACE + 'coupons', coupon => ( {
id: coupon.id,
label: coupon.code,
} ) ),
},
},
customer: {

View File

@ -14,7 +14,6 @@ import { partial } from 'lodash';
*/
import { Card, ReportFilters } from '@woocommerce/components';
import { filters, advancedFilterConfig } from './config';
import './style.scss';
class OrdersReport extends Component {
constructor( props ) {

View File

@ -3,38 +3,13 @@
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import apiFetch from '@wordpress/api-fetch';
/**
* Internal dependencies
*/
import { getIdsFromQuery, stringifyQuery } from 'lib/nav-utils';
import { getRequestByIdString } from 'lib/async-requests';
import { NAMESPACE } from 'store/constants';
export function getProductLabelsById( queryString = '' ) {
const idList = getIdsFromQuery( queryString );
if ( idList.length < 1 ) {
return Promise.resolve( [] );
}
const payload = stringifyQuery( {
include: idList.join( ',' ),
per_page: idList.length,
} );
return apiFetch( { path: `${ NAMESPACE }products${ payload }` } );
}
export function getCategoryLabelsById( queryString = '' ) {
const idList = getIdsFromQuery( queryString );
if ( idList.length < 1 ) {
return Promise.resolve( [] );
}
const payload = stringifyQuery( {
include: idList.join( ',' ),
per_page: idList.length,
} );
return apiFetch( { path: `${ NAMESPACE }products/categories${ payload }` } );
}
export const filters = [
{ label: __( 'All Products', 'wc-admin' ), value: 'all' },
{
@ -54,7 +29,10 @@ export const filters = [
settings: {
type: 'products',
param: 'product',
getLabels: getProductLabelsById,
getLabels: getRequestByIdString( NAMESPACE + 'products', product => ( {
id: product.id,
label: product.name,
} ) ),
labels: {
helpText: __( 'Select at least two products to compare', 'wc-admin' ),
placeholder: __( 'Search for products to compare', 'wc-admin' ),
@ -69,7 +47,10 @@ export const filters = [
settings: {
type: 'product_cats',
param: 'product_cat',
getLabels: getCategoryLabelsById,
getLabels: getRequestByIdString( NAMESPACE + 'products/categories', category => ( {
id: category.id,
label: category.name,
} ) ),
labels: {
helpText: __( 'Select at least two product categories to compare', 'wc-admin' ),
placeholder: __( 'Search for product categories to compare', 'wc-admin' ),

View File

@ -14,7 +14,6 @@ import { formatCurrency, getCurrencyFormatDecimal } from 'lib/currency';
import { numberFormat } from 'lib/number';
import { getAdminLink, onQueryChange } from 'lib/nav-utils';
import { ReportFilters, TableCard } from '@woocommerce/components';
import './style.scss';
import products from './__mocks__/data';

View File

@ -28,8 +28,7 @@ class SearchFilter extends Component {
}
}
updateLabels( data ) {
const selected = data.map( p => ( { id: p.id, label: p.name } ) );
updateLabels( selected ) {
this.setState( { selected } );
}

View File

@ -57,8 +57,7 @@ class CompareFilter extends Component {
return getNewPath( { [ param ]: undefined }, path, query );
}
updateLabels( data ) {
const selected = data.map( p => ( { id: p.id, label: p.name } ) );
updateLabels( selected ) {
this.setState( { selected } );
}

View File

@ -0,0 +1,59 @@
/** @format */
/**
* External dependencies
*/
import apiFetch from '@wordpress/api-fetch';
/**
* Internal dependencies
*/
import { computeSuggestionMatch } from './utils';
import { stringifyQuery } from 'lib/nav-utils';
import { NAMESPACE } from 'store/constants';
/**
* A coupon completer.
* See https://github.com/WordPress/gutenberg/tree/master/packages/components/src/autocomplete#the-completer-interface
*
* @type {Completer}
*/
export default {
name: 'coupons',
className: 'woocommerce-search__coupon-result',
options( search ) {
let payload = '';
if ( search ) {
const query = {
search: encodeURIComponent( search ),
per_page: 10,
};
payload = stringifyQuery( query );
}
return apiFetch( { path: `${ NAMESPACE }coupons${ payload }` } );
},
isDebounced: true,
getOptionKeywords( coupon ) {
return [ coupon.code ];
},
getOptionLabel( coupon, query ) {
const match = computeSuggestionMatch( coupon.code, query ) || {};
return [
<span key="name" className="woocommerce-search__result-name" aria-label={ coupon.code }>
{ match.suggestionBeforeMatch }
<strong className="components-form-token-field__suggestion-match">
{ match.suggestionMatch }
</strong>
{ match.suggestionAfterMatch }
</span>,
];
},
// This is slightly different than gutenberg/Autocomplete, we don't support different methods
// of replace/insertion, so we can just return the value.
getOptionCompletion( coupon ) {
const value = {
id: coupon.id,
label: coupon.code,
};
return value;
},
};

View File

@ -4,3 +4,4 @@
*/
export { default as product } from './product';
export { default as productCategory } from './product-cat';
export { default as coupons } from './coupons';

View File

@ -9,6 +9,7 @@ import apiFetch from '@wordpress/api-fetch';
*/
import { computeSuggestionMatch } from './utils';
import { stringifyQuery } from 'lib/nav-utils';
import { NAMESPACE } from 'store/constants';
/**
* A product categories completer.
@ -29,7 +30,7 @@ export default {
};
payload = stringifyQuery( query );
}
return apiFetch( { path: '/wc/v3/products/categories' + payload } );
return apiFetch( { path: `${ NAMESPACE }products/categories${ payload }` } );
},
isDebounced: true,
getOptionKeywords( cat ) {

View File

@ -10,6 +10,7 @@ import apiFetch from '@wordpress/api-fetch';
import { computeSuggestionMatch } from './utils';
import ProductImage from 'components/product-image';
import { stringifyQuery } from 'lib/nav-utils';
import { NAMESPACE } from 'store/constants';
/**
* A products completer.
@ -30,7 +31,7 @@ export default {
};
payload = stringifyQuery( query );
}
return apiFetch( { path: '/wc/v3/products' + payload } );
return apiFetch( { path: `${ NAMESPACE }products${ payload }` } );
},
isDebounced: true,
getOptionKeywords( product ) {

View File

@ -12,7 +12,7 @@ import PropTypes from 'prop-types';
* Internal dependencies
*/
import Autocomplete from './autocomplete';
import { product, productCategory } from './autocompleters';
import { product, productCategory, coupons } from './autocompleters';
import Tag from 'components/tag';
import './style.scss';
@ -64,6 +64,8 @@ class Search extends Component {
return product;
case 'product_cats':
return productCategory;
case 'coupons':
return coupons;
default:
return {};
}
@ -125,7 +127,8 @@ Search.propTypes = {
/**
* The object type to be used in searching.
*/
type: PropTypes.oneOf( [ 'products', 'product_cats', 'orders', 'customers' ] ).isRequired,
type: PropTypes.oneOf( [ 'products', 'product_cats', 'orders', 'customers', 'coupons' ] )
.isRequired,
/**
* A placeholder for the search input.
*/

View File

@ -0,0 +1,33 @@
/** @format */
/**
* External dependencies
*/
import apiFetch from '@wordpress/api-fetch';
import { identity } from 'lodash';
/**
* Internal dependencies
*/
import { getIdsFromQuery, stringifyQuery } from 'lib/nav-utils';
/**
* Get a function that accepts ids as they are found in url parameter and
* returns a promise with an optional method applied to results
*
* @param {string} path - api path
* @param {Function} [handleData] - function applied to each iteration of data
* @returns {Function} - a function of ids returning a promise
*/
export function getRequestByIdString( path, handleData = identity ) {
return function( queryString = '' ) {
const idList = getIdsFromQuery( queryString );
if ( idList.length < 1 ) {
return Promise.resolve( [] );
}
const payload = stringifyQuery( {
include: idList.join( ',' ),
per_page: idList.length,
} );
return apiFetch( { path: path + payload } ).then( data => data.map( handleData ) );
};
}

View File

@ -4,6 +4,7 @@
*/
import history from 'lib/history';
import { parse, stringify } from 'qs';
import { uniq } from 'lodash';
/**
* Returns a string with the site's wp-admin URL appended. JS version of `admin_url`.
@ -35,10 +36,12 @@ export const stringifyQuery = query => ( query ? '?' + stringify( query ) : '' )
* @return {Array} List of IDs converted to numbers.
*/
export function getIdsFromQuery( queryString = '' ) {
return queryString
.split( ',' )
.map( id => parseInt( id, 10 ) )
.filter( Boolean );
return uniq(
queryString
.split( ',' )
.map( id => parseInt( id, 10 ) )
.filter( Boolean )
);
}
/**

View File

@ -96,6 +96,12 @@ function wc_admin_register_pages() {
'parent' => '/analytics',
'path' => '/analytics/orders',
) );
wc_admin_register_page( array(
'title' => __( 'Coupons', 'wc-admin' ),
'parent' => '/analytics',
'path' => '/analytics/coupons',
) );
}
add_action( 'admin_menu', 'wc_admin_register_pages' );

View File

@ -2531,7 +2531,7 @@
},
"babel-plugin-syntax-class-properties": {
"version": "6.13.0",
"resolved": "http://registry.npmjs.org/babel-plugin-syntax-class-properties/-/babel-plugin-syntax-class-properties-6.13.0.tgz",
"resolved": "https://registry.npmjs.org/babel-plugin-syntax-class-properties/-/babel-plugin-syntax-class-properties-6.13.0.tgz",
"integrity": "sha1-1+sjt5oxf4VDlixQW4J8fWysJ94=",
"dev": true
},
@ -2943,7 +2943,7 @@
},
"browserify-aes": {
"version": "1.2.0",
"resolved": "http://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz",
"resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz",
"integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==",
"dev": true,
"requires": {
@ -2980,7 +2980,7 @@
},
"browserify-rsa": {
"version": "4.0.1",
"resolved": "http://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz",
"resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz",
"integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=",
"dev": true,
"requires": {
@ -3042,7 +3042,7 @@
},
"buffer": {
"version": "4.9.1",
"resolved": "http://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz",
"integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=",
"dev": true,
"requires": {
@ -3751,7 +3751,7 @@
},
"create-hash": {
"version": "1.2.0",
"resolved": "http://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz",
"resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz",
"integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==",
"dev": true,
"requires": {
@ -3764,7 +3764,7 @@
},
"create-hmac": {
"version": "1.1.7",
"resolved": "http://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz",
"resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz",
"integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==",
"dev": true,
"requires": {
@ -4406,7 +4406,7 @@
},
"diffie-hellman": {
"version": "5.0.3",
"resolved": "http://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz",
"resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz",
"integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==",
"dev": true,
"requires": {
@ -5525,7 +5525,7 @@
},
"external-editor": {
"version": "2.2.0",
"resolved": "http://registry.npmjs.org/external-editor/-/external-editor-2.2.0.tgz",
"resolved": "https://registry.npmjs.org/external-editor/-/external-editor-2.2.0.tgz",
"integrity": "sha512-bSn6gvGxKt+b7+6TKEv1ZycHleA7aHhRHyAqJyp5pbUFuYYNIzpZnQDk7AsYckyWdEnTeAnay0aCy2aV6iTk9A==",
"dev": true,
"requires": {
@ -7170,7 +7170,7 @@
},
"http-errors": {
"version": "1.6.3",
"resolved": "http://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz",
"integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=",
"dev": true,
"requires": {
@ -7735,7 +7735,7 @@
},
"is-obj": {
"version": "1.0.1",
"resolved": "http://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz",
"resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz",
"integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=",
"dev": true
},
@ -9748,7 +9748,7 @@
},
"jsonfile": {
"version": "2.4.0",
"resolved": "http://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz",
"integrity": "sha1-NzaitCi4e72gzIO1P6PWM6NcKug=",
"dev": true,
"requires": {
@ -11358,7 +11358,7 @@
},
"minimist": {
"version": "1.2.0",
"resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
"integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
"dev": true
},
@ -11561,7 +11561,7 @@
},
"parse-asn1": {
"version": "5.1.1",
"resolved": "http://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.1.tgz",
"resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.1.tgz",
"integrity": "sha512-KPx7flKXg775zZpnp9SxJlz00gTd4BmJ2yJufSc44gMCRrRQ7NSzAcSJQfifuOLgW6bEi+ftrALtsgALeB2Adw==",
"dev": true,
"requires": {
@ -12491,7 +12491,7 @@
},
"public-encrypt": {
"version": "4.0.2",
"resolved": "http://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.2.tgz",
"resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.2.tgz",
"integrity": "sha512-4kJ5Esocg8X3h8YgJsKAuoesBgB7mqH3eowiDzMUPKiRDDE7E/BqqZD1hnTByIaAFiwAw246YEltSq7tdrOH0Q==",
"dev": true,
"requires": {
@ -12642,7 +12642,7 @@
"dependencies": {
"minimist": {
"version": "1.2.0",
"resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
"integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
"dev": true
}
@ -13615,7 +13615,7 @@
"dependencies": {
"minimist": {
"version": "1.2.0",
"resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
"integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
"dev": true
}
@ -13650,7 +13650,7 @@
},
"os-locale": {
"version": "1.4.0",
"resolved": "http://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz",
"resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz",
"integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=",
"dev": true,
"requires": {
@ -13931,7 +13931,7 @@
},
"sha.js": {
"version": "2.4.11",
"resolved": "http://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz",
"resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz",
"integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==",
"dev": true,
"requires": {
@ -15354,7 +15354,7 @@
"dependencies": {
"minimist": {
"version": "1.2.0",
"resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
"integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
"dev": true
}