Merge branch 'master' into feature/754

This commit is contained in:
Claudio Sanches 2019-02-05 15:42:34 -02:00
commit e0004eb461
52 changed files with 887 additions and 354 deletions

View File

@ -103,7 +103,7 @@ describe( 'Leaderboard', () => {
);
const leaderboard = leaderboardWrapper.root.findByType( Leaderboard );
const endpoint = NAMESPACE + 'reports/products';
const endpoint = NAMESPACE + '/reports/products';
const query = { orderby: 'items_sold', per_page: 5, extended_info: 1 };
expect( getReportStatsMock.mock.calls[ 0 ][ 1 ] ).toBe( endpoint );

View File

@ -20,6 +20,7 @@ import { onQueryChange } from '@woocommerce/navigation';
*/
import ReportError from 'analytics/components/report-error';
import { getReportChartData, getReportTableData } from 'wc-api/reports/utils';
import { QUERY_DEFAULTS } from 'wc-api/constants';
import withSelect from 'wc-api/with-select';
import { extendTableData } from './utils';
@ -84,12 +85,12 @@ class ReportTable extends Component {
}
const isRequesting = tableData.isRequesting || primaryData.isRequesting;
const totals = get( primaryData, [ 'data', 'totals' ], null );
const totalResults = items.totalResults || 0;
const totals = get( primaryData, [ 'data', 'totals' ], {} );
const totalResults = items.totalResults;
const { headers, ids, rows, summary } = applyFilters( TABLE_FILTER, {
endpoint: endpoint,
headers: getHeadersContent(),
ids: itemIdField ? items.data.map( item => item[ itemIdField ] ) : null,
ids: itemIdField ? items.data.map( item => item[ itemIdField ] ) : [],
rows: getRowsContent( items.data ),
totals: totals,
summary: getSummary ? getSummary( totals, totalResults ) : null,
@ -107,7 +108,7 @@ class ReportTable extends Component {
onQueryChange={ onQueryChange }
onColumnsChange={ this.onColumnsChange }
rows={ rows }
rowsPerPage={ parseInt( query.per_page ) }
rowsPerPage={ parseInt( query.per_page ) || QUERY_DEFAULTS.pageSize }
summary={ summary }
totalRows={ totalResults }
{ ...tableProps }
@ -159,7 +160,7 @@ ReportTable.propTypes = {
* Primary data of that report. If it's not provided, it will be automatically
* loaded via the provided `endpoint`.
*/
primaryData: PropTypes.object.isRequired,
primaryData: PropTypes.object,
/**
* Table data of that report. If it's not provided, it will be automatically
* loaded via the provided `endpoint`.
@ -176,13 +177,23 @@ ReportTable.propTypes = {
};
ReportTable.defaultProps = {
tableData: {},
primaryData: {},
tableData: {
items: {
data: [],
totalResults: 0,
},
query: {},
},
tableQuery: {},
};
export default compose(
withSelect( ( select, props ) => {
const { endpoint, getSummary, query, tableData, tableQuery, columnPrefsKey } = props;
if ( query.search && ! ( query[ endpoint ] && query[ endpoint ].length ) ) {
return {};
}
const chartEndpoint = 'variations' === endpoint ? 'products' : endpoint;
const primaryData = getSummary
? getReportChartData( chartEndpoint, 'primary', query, select )

View File

@ -111,27 +111,24 @@ class CategoriesReportTable extends Component {
} );
}
getSummary( totals, totalResults ) {
if ( ! totals ) {
return [];
}
getSummary( totals, totalResults = 0 ) {
const { items_sold = 0, net_revenue = 0, orders_count = 0 } = totals;
return [
{
label: _n( 'category', 'categories', totalResults, 'wc-admin' ),
value: numberFormat( totalResults ),
},
{
label: _n( 'item sold', 'items sold', totals.items_sold, 'wc-admin' ),
value: numberFormat( totals.items_sold ),
label: _n( 'item sold', 'items sold', items_sold, 'wc-admin' ),
value: numberFormat( items_sold ),
},
{
label: __( 'net revenue', 'wc-admin' ),
value: formatCurrency( totals.net_revenue ),
value: formatCurrency( net_revenue ),
},
{
label: _n( 'orders', 'orders', totals.orders_count, 'wc-admin' ),
value: numberFormat( totals.orders_count ),
label: _n( 'order', 'orders', orders_count, 'wc-admin' ),
value: numberFormat( orders_count ),
},
];
}
@ -168,7 +165,12 @@ class CategoriesReportTable extends Component {
}
export default compose(
withSelect( select => {
withSelect( ( select, props ) => {
const { query } = props;
if ( query.search && ! ( query.categories && query.categories.length ) ) {
return {};
}
const { getItems, getItemsError, isGetItemsRequesting } = select( 'wc-api' );
const tableQuery = {
per_page: -1,

View File

@ -129,21 +129,19 @@ export default class CouponsReportTable extends Component {
}
getSummary( totals ) {
if ( ! totals ) {
return [];
}
const { coupons_count = 0, orders_count = 0, amount = 0 } = totals;
return [
{
label: _n( 'coupon', 'coupons', totals.coupons_count, 'wc-admin' ),
value: numberFormat( totals.coupons_count ),
label: _n( 'coupon', 'coupons', coupons_count, 'wc-admin' ),
value: numberFormat( coupons_count ),
},
{
label: _n( 'order', 'orders', totals.orders_count, 'wc-admin' ),
value: numberFormat( totals.orders_count ),
label: _n( 'order', 'orders', orders_count, 'wc-admin' ),
value: numberFormat( orders_count ),
},
{
label: __( 'amount discounted', 'wc-admin' ),
value: formatCurrency( totals.amount ),
value: formatCurrency( amount ),
},
];
}

View File

@ -57,7 +57,7 @@ export const advancedFilters = {
input: {
component: 'Search',
type: 'customers',
getLabels: getRequestByIdString( NAMESPACE + 'customers', customer => ( {
getLabels: getRequestByIdString( NAMESPACE + '/customers', customer => ( {
id: customer.id,
label: [ customer.first_name, customer.last_name ].filter( Boolean ).join( ' ' ),
} ) ),
@ -157,7 +157,7 @@ export const advancedFilters = {
input: {
component: 'Search',
type: 'emails',
getLabels: getRequestByIdString( NAMESPACE + 'customers', customer => ( {
getLabels: getRequestByIdString( NAMESPACE + '/customers', customer => ( {
id: customer.id,
label: customer.email,
} ) ),

View File

@ -2,7 +2,7 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { __, _n } from '@wordpress/i18n';
import { Component, Fragment } from '@wordpress/element';
import { Tooltip } from '@wordpress/components';
@ -191,25 +191,28 @@ export default class CustomersReportTable extends Component {
}
getSummary( totals ) {
if ( ! totals ) {
return [];
}
const {
customers_count = 0,
avg_orders_count = 0,
avg_total_spend = 0,
avg_avg_order_value = 0,
} = totals;
return [
{
label: __( 'customers', 'wc-admin' ),
value: numberFormat( totals.customers_count ),
label: _n( 'customer', 'customers', customers_count, 'wc-admin' ),
value: numberFormat( customers_count ),
},
{
label: __( 'average orders', 'wc-admin' ),
value: numberFormat( totals.avg_orders_count ),
label: _n( 'average order', 'average orders', avg_orders_count, 'wc-admin' ),
value: numberFormat( avg_orders_count ),
},
{
label: __( 'average lifetime spend', 'wc-admin' ),
value: formatCurrency( totals.avg_total_spend ),
value: formatCurrency( avg_total_spend ),
},
{
label: __( 'average order value', 'wc-admin' ),
value: formatCurrency( totals.avg_avg_order_value ),
value: formatCurrency( avg_avg_order_value ),
},
];
}

View File

@ -121,9 +121,7 @@ export default class CouponsReportTable extends Component {
}
getSummary( totals ) {
if ( ! totals ) {
return [];
}
const { download_count = 0 } = totals;
const { query } = this.props;
const dates = getCurrentDates( query );
const after = moment( dates.primary.after );
@ -136,8 +134,8 @@ export default class CouponsReportTable extends Component {
value: numberFormat( days ),
},
{
label: _n( 'download', 'downloads', totals.download_count, 'wc-admin' ),
value: numberFormat( totals.download_count ),
label: _n( 'download', 'downloads', download_count, 'wc-admin' ),
value: numberFormat( download_count ),
},
];
}

View File

@ -148,7 +148,7 @@ export default compose(
const items = searchItemsByString( select, report, search );
const ids = Object.keys( items );
if ( ! ids.length ) {
return {}; // @TODO if no results were found, we should avoid making a server request.
return {};
}
return {

View File

@ -182,42 +182,48 @@ export default class OrdersReportTable extends Component {
}
getSummary( totals ) {
if ( ! totals ) {
return [];
}
const {
orders_count = 0,
num_new_customers = 0,
num_returning_customers = 0,
products = 0,
num_items_sold = 0,
coupons = 0,
net_revenue = 0,
} = totals;
return [
{
label: _n( 'order', 'orders', totals.orders_count, 'wc-admin' ),
value: numberFormat( totals.orders_count ),
label: _n( 'order', 'orders', orders_count, 'wc-admin' ),
value: numberFormat( orders_count ),
},
{
label: _n( 'new customer', 'new customers', totals.num_new_customers, 'wc-admin' ),
value: numberFormat( totals.num_new_customers ),
label: _n( 'new customer', 'new customers', num_new_customers, 'wc-admin' ),
value: numberFormat( num_new_customers ),
},
{
label: _n(
'returning customer',
'returning customers',
totals.num_returning_customers,
num_returning_customers,
'wc-admin'
),
value: numberFormat( totals.num_returning_customers ),
value: numberFormat( num_returning_customers ),
},
{
label: _n( 'product', 'products', totals.products, 'wc-admin' ),
value: numberFormat( totals.products ),
label: _n( 'product', 'products', products, 'wc-admin' ),
value: numberFormat( products ),
},
{
label: _n( 'item sold', 'items sold', totals.num_items_sold, 'wc-admin' ),
value: numberFormat( totals.num_items_sold ),
label: _n( 'item sold', 'items sold', num_items_sold, 'wc-admin' ),
value: numberFormat( num_items_sold ),
},
{
label: _n( 'coupon', 'coupons', totals.coupons, 'wc-admin' ),
value: numberFormat( totals.coupons ),
label: _n( 'coupon', 'coupons', coupons, 'wc-admin' ),
value: numberFormat( coupons ),
},
{
label: __( 'net revenue', 'wc-admin' ),
value: formatCurrency( totals.net_revenue ),
value: formatCurrency( net_revenue ),
},
];
}

View File

@ -139,26 +139,24 @@ export default class VariationsReportTable extends Component {
}
getSummary( totals ) {
if ( ! totals ) {
return [];
}
const { products_count = 0, items_sold = 0, net_revenue = 0, orders_count = 0 } = totals;
return [
{
// @TODO: When primaryData is segmented, fix this to reflect variations, not products.
label: _n( 'variation sold', 'variations sold', totals.products_count, 'wc-admin' ),
value: numberFormat( totals.products_count ),
label: _n( 'variation sold', 'variations sold', products_count, 'wc-admin' ),
value: numberFormat( products_count ),
},
{
label: _n( 'item sold', 'items sold', totals.items_sold, 'wc-admin' ),
value: numberFormat( totals.items_sold ),
label: _n( 'item sold', 'items sold', items_sold, 'wc-admin' ),
value: numberFormat( items_sold ),
},
{
label: __( 'net revenue', 'wc-admin' ),
value: formatCurrency( totals.net_revenue ),
value: formatCurrency( net_revenue ),
},
{
label: _n( 'orders', 'orders', totals.orders_count, 'wc-admin' ),
value: numberFormat( totals.orders_count ),
label: _n( 'orders', 'orders', orders_count, 'wc-admin' ),
value: numberFormat( orders_count ),
},
];
}

View File

@ -204,25 +204,23 @@ class ProductsReportTable extends Component {
}
getSummary( totals ) {
if ( ! totals ) {
return [];
}
const { products_count = 0, items_sold = 0, net_revenue = 0, orders_count = 0 } = totals;
return [
{
label: _n( 'product sold', 'products sold', totals.products_count, 'wc-admin' ),
value: numberFormat( totals.products_count ),
label: _n( 'product sold', 'products sold', products_count, 'wc-admin' ),
value: numberFormat( products_count ),
},
{
label: _n( 'item sold', 'items sold', totals.items_sold, 'wc-admin' ),
value: numberFormat( totals.items_sold ),
label: _n( 'item sold', 'items sold', items_sold, 'wc-admin' ),
value: numberFormat( items_sold ),
},
{
label: __( 'net revenue', 'wc-admin' ),
value: formatCurrency( totals.net_revenue ),
value: formatCurrency( net_revenue ),
},
{
label: _n( 'orders', 'orders', totals.orders_count, 'wc-admin' ),
value: numberFormat( totals.orders_count ),
label: _n( 'orders', 'orders', orders_count, 'wc-admin' ),
value: numberFormat( orders_count ),
},
];
}
@ -259,7 +257,12 @@ class ProductsReportTable extends Component {
}
export default compose(
withSelect( select => {
withSelect( ( select, props ) => {
const { query } = props;
if ( query.search && ! ( query.products && query.products.length ) ) {
return {};
}
const { getItems, getItemsError, isGetItemsRequesting } = select( 'wc-api' );
const tableQuery = {
per_page: -1,

View File

@ -153,43 +153,48 @@ class RevenueReportTable extends Component {
} );
}
getSummary( totals, totalResults ) {
if ( ! totals ) {
return [];
}
getSummary( totals, totalResults = 0 ) {
const {
orders_count = 0,
gross_revenue = 0,
refunds = 0,
coupons = 0,
taxes = 0,
shipping = 0,
net_revenue = 0,
} = totals;
return [
{
label: _n( 'day', 'days', totalResults, 'wc-admin' ),
value: numberFormat( totalResults ),
},
{
label: _n( 'order', 'orders', totals.orders_count, 'wc-admin' ),
value: numberFormat( totals.orders_count ),
label: _n( 'order', 'orders', orders_count, 'wc-admin' ),
value: numberFormat( orders_count ),
},
{
label: __( 'gross revenue', 'wc-admin' ),
value: formatCurrency( totals.gross_revenue ),
value: formatCurrency( gross_revenue ),
},
{
label: __( 'refunds', 'wc-admin' ),
value: formatCurrency( totals.refunds ),
value: formatCurrency( refunds ),
},
{
label: __( 'coupons', 'wc-admin' ),
value: formatCurrency( totals.coupons ),
value: formatCurrency( coupons ),
},
{
label: __( 'taxes', 'wc-admin' ),
value: formatCurrency( totals.taxes ),
value: formatCurrency( taxes ),
},
{
label: __( 'shipping', 'wc-admin' ),
value: formatCurrency( totals.shipping ),
value: formatCurrency( shipping ),
},
{
label: __( 'net revenue', 'wc-admin' ),
value: formatCurrency( totals.net_revenue ),
value: formatCurrency( net_revenue ),
},
];
}

View File

@ -103,25 +103,23 @@ export default class StockReportTable extends Component {
}
getSummary( totals ) {
if ( ! totals ) {
return [];
}
const { products = 0, out_of_stock = 0, low_stock = 0, in_stock = 0 } = totals;
return [
{
label: _n( 'product', 'products', totals.products, 'wc-admin' ),
value: numberFormat( totals.products ),
label: _n( 'product', 'products', products, 'wc-admin' ),
value: numberFormat( products ),
},
{
label: __( 'out of stock', totals.out_of_stock, 'wc-admin' ),
value: numberFormat( totals.out_of_stock ),
label: __( 'out of stock', out_of_stock, 'wc-admin' ),
value: numberFormat( out_of_stock ),
},
{
label: __( 'low stock', totals.low_stock, 'wc-admin' ),
value: numberFormat( totals.low_stock ),
label: __( 'low stock', low_stock, 'wc-admin' ),
value: numberFormat( low_stock ),
},
{
label: __( 'in stock', totals.in_stock, 'wc-admin' ),
value: numberFormat( totals.in_stock ),
label: __( 'in stock', in_stock, 'wc-admin' ),
value: numberFormat( in_stock ),
},
];
}

View File

@ -49,7 +49,7 @@ export const filters = [
settings: {
type: 'taxes',
param: 'taxes',
getLabels: getRequestByIdString( NAMESPACE + 'taxes', tax => ( {
getLabels: getRequestByIdString( NAMESPACE + '/taxes', tax => ( {
id: tax.id,
label: getTaxCode( tax ),
} ) ),

View File

@ -111,29 +111,33 @@ export default class TaxesReportTable extends Component {
}
getSummary( totals ) {
if ( ! totals ) {
return [];
}
const {
tax_codes = 0,
total_tax = 0,
order_tax = 0,
shipping_tax = 0,
orders_count = 0,
} = totals;
return [
{
label: _n( 'tax code', 'tax codes', totals.tax_codes, 'wc-admin' ),
value: numberFormat( totals.tax_codes ),
label: _n( 'tax code', 'tax codes', tax_codes, 'wc-admin' ),
value: numberFormat( tax_codes ),
},
{
label: __( 'total tax', 'wc-admin' ),
value: formatCurrency( totals.total_tax ),
value: formatCurrency( total_tax ),
},
{
label: __( 'order tax', 'wc-admin' ),
value: formatCurrency( totals.order_tax ),
value: formatCurrency( order_tax ),
},
{
label: __( 'shipping tax', 'wc-admin' ),
value: formatCurrency( totals.shipping_tax ),
value: formatCurrency( shipping_tax ),
},
{
label: _n( 'order', 'orders', totals.orders, 'wc-admin' ),
value: numberFormat( totals.orders_count ),
label: _n( 'order', 'orders', orders_count, 'wc-admin' ),
value: numberFormat( orders_count ),
},
];
}

View File

@ -4,12 +4,12 @@
*/
import { Component, createElement } from '@wordpress/element';
import { parse } from 'qs';
import { find, last } from 'lodash';
import { find, last, isEqual } from 'lodash';
/**
* WooCommerce dependencies
*/
import { getPersistedQuery, stringifyQuery } from '@woocommerce/navigation';
import { getNewPath, getPersistedQuery, history, stringifyQuery } from '@woocommerce/navigation';
/**
* Internal dependencies
@ -64,11 +64,35 @@ const getPages = () => {
};
class Controller extends Component {
componentDidUpdate( prevProps ) {
const prevQuery = this.getQuery( prevProps.location.search );
const prevBaseQuery = this.getBaseQuery( prevProps.location.search );
const baseQuery = this.getBaseQuery( this.props.location.search );
if ( prevQuery.page > 1 && ! isEqual( prevBaseQuery, baseQuery ) ) {
history.replace( getNewPath( { page: 1 } ) );
}
}
getQuery( searchString ) {
if ( ! searchString ) {
return {};
}
const search = searchString.substring( 1 );
return parse( search );
}
getBaseQuery( searchString ) {
const query = this.getQuery( searchString );
delete query.page;
return query;
}
render() {
// Pass URL parameters (example :report -> params.report) and query string parameters
const { path, url, params } = this.props.match;
const search = this.props.location.search.substring( 1 );
const query = parse( search );
const query = this.getQuery( this.props.location.search );
const page = find( getPages(), { path } );
window.wpNavMenuUrlUpdate( page, query );
window.wpNavMenuClassChange( page );

View File

@ -39,30 +39,30 @@ export function getRequestByIdString( path, handleData = identity ) {
}
export const getCategoryLabels = getRequestByIdString(
NAMESPACE + 'products/categories',
NAMESPACE + '/products/categories',
category => ( {
id: category.id,
label: category.name,
} )
);
export const getCouponLabels = getRequestByIdString( NAMESPACE + 'coupons', coupon => ( {
export const getCouponLabels = getRequestByIdString( NAMESPACE + '/coupons', coupon => ( {
id: coupon.id,
label: coupon.code,
} ) );
export const getCustomerLabels = getRequestByIdString( NAMESPACE + 'customers', customer => ( {
export const getCustomerLabels = getRequestByIdString( NAMESPACE + '/customers', customer => ( {
id: customer.id,
label: customer.username,
} ) );
export const getProductLabels = getRequestByIdString( NAMESPACE + 'products', product => ( {
export const getProductLabels = getRequestByIdString( NAMESPACE + '/products', product => ( {
id: product.id,
label: product.name,
} ) );
export const getVariationLabels = getRequestByIdString(
query => NAMESPACE + `products/${ query.products }/variations`,
query => NAMESPACE + `/products/${ query.products }/variations`,
variation => {
return {
id: variation.id,

View File

@ -239,7 +239,7 @@ export function getReportChartData( endpoint, dataType, query, select ) {
isError: false,
isRequesting: false,
data: {
totals: null,
totals: {},
intervals: [],
},
};
@ -355,6 +355,7 @@ export function getReportTableData( endpoint, urlQuery, select, query = {} ) {
isError: false,
items: {
data: [],
totalResults: 0,
},
};

View File

@ -0,0 +1,25 @@
<?php
/**
* REST API Product Variations Controller
*
* Handles requests to /products/variations.
*
* @package WooCommerce Admin/API
*/
defined( 'ABSPATH' ) || exit;
/**
* Product variations controller.
*
* @package WooCommerce Admin/API
* @extends WC_REST_Product_Variations_Controller
*/
class WC_Admin_REST_Product_Variations_Controller extends WC_REST_Product_Variations_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v4';
}

View File

@ -37,6 +37,11 @@ class WC_Admin_Api_Init {
*/
const ORDERS_LOOKUP_BATCH_INIT = 'wc-admin_orders_lookup_batch_init';
/**
* Action hook for processing a batch of orders.
*/
const SINGLE_ORDER_ACTION = 'wc-admin_process_order';
/**
* Queue instance.
*
@ -59,19 +64,16 @@ class WC_Admin_Api_Init {
add_filter( 'rest_endpoints', array( 'WC_Admin_Api_Init', 'filter_rest_endpoints' ), 10, 1 );
add_filter( 'woocommerce_debug_tools', array( 'WC_Admin_Api_Init', 'add_regenerate_tool' ) );
// Initialize Orders data store class's static vars.
add_action( 'woocommerce_after_register_post_type', array( 'WC_Admin_Api_Init', 'orders_data_store_init' ), 20 );
// Initialize Customers Report data store sync hooks.
// Note: we need to hook in before `wc_current_user_is_active`.
// See: https://github.com/woocommerce/woocommerce/blob/942615101ba00c939c107c3a4820c3d466864872/includes/wc-user-functions.php#L749.
add_action( 'wp_loaded', array( 'WC_Admin_Api_Init', 'customers_report_data_store_init' ) );
// Initialize syncing hooks.
add_action( 'wp_loaded', array( __CLASS__, 'orders_lookup_update_init' ) );
// Initialize scheduled action handlers.
add_action( self::QUEUE_BATCH_ACTION, array( __CLASS__, 'queue_batches' ), 10, 3 );
add_action( self::QUEUE_DEPEDENT_ACTION, array( __CLASS__, 'queue_dependent_action' ), 10, 2 );
add_action( self::QUEUE_DEPEDENT_ACTION, array( __CLASS__, 'queue_dependent_action' ), 10, 3 );
add_action( self::CUSTOMERS_BATCH_ACTION, array( __CLASS__, 'customer_lookup_process_batch' ) );
add_action( self::ORDERS_BATCH_ACTION, array( __CLASS__, 'orders_lookup_process_batch' ) );
add_action( self::ORDERS_LOOKUP_BATCH_INIT, array( __CLASS__, 'orders_lookup_batch_init' ) );
add_action( self::SINGLE_ORDER_ACTION, array( __CLASS__, 'orders_lookup_process_order' ) );
// Add currency symbol to orders endpoint response.
add_filter( 'woocommerce_rest_prepare_shop_order_object', array( __CLASS__, 'add_currency_symbol_to_order_response' ) );
@ -179,6 +181,7 @@ class WC_Admin_Api_Init {
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-products-controller.php';
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-product-categories-controller.php';
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-product-reviews-controller.php';
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-product-variations-controller.php';
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-reports-controller.php';
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-setting-options-controller.php';
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-system-status-tools-controller.php';
@ -215,6 +218,7 @@ class WC_Admin_Api_Init {
'WC_Admin_REST_Products_Controller',
'WC_Admin_REST_Product_Categories_Controller',
'WC_Admin_REST_Product_Reviews_Controller',
'WC_Admin_REST_Product_Variations_Controller',
'WC_Admin_REST_Reports_Controller',
'WC_Admin_REST_Setting_Options_Controller',
'WC_Admin_REST_System_Status_Tools_Controller',
@ -384,6 +388,17 @@ class WC_Admin_Api_Init {
$endpoints['/wc/v4/products/reviews'][1] = $endpoints['/wc/v4/products/reviews'][3];
}
// Override /wc/v4/products/$product_id/variations.
if ( isset( $endpoints['products/(?P<product_id>[\d]+)/variations'] )
&& isset( $endpoints['products/(?P<product_id>[\d]+)/variations'][3] )
&& isset( $endpoints['products/(?P<product_id>[\d]+)/variations'][2] )
&& $endpoints['products/(?P<product_id>[\d]+)/variations'][2]['callback'][0] instanceof WC_Admin_REST_Product_Variations_Controller
&& $endpoints['products/(?P<product_id>[\d]+)/variations'][3]['callback'][0] instanceof WC_Admin_REST_Product_Variations_Controller
) {
$endpoints['products/(?P<product_id>[\d]+)/variations'][0] = $endpoints['products/(?P<product_id>[\d]+)/variations'][2];
$endpoints['products/(?P<product_id>[\d]+)/variations'][1] = $endpoints['products/(?P<product_id>[\d]+)/variations'][3];
}
// Override /wc/v4/taxes.
if ( isset( $endpoints['/wc/v4/taxes'] )
&& isset( $endpoints['/wc/v4/taxes'][3] )
@ -420,7 +435,7 @@ class WC_Admin_Api_Init {
// so that the orders can be associated with the `customer_id` column.
self::customer_lookup_batch_init();
// Queue orders lookup to occur after customers lookup generation is done.
self::queue_dependent_action( self::ORDERS_LOOKUP_BATCH_INIT, self::CUSTOMERS_BATCH_ACTION );
self::queue_dependent_action( self::ORDERS_LOOKUP_BATCH_INIT, array(), self::CUSTOMERS_BATCH_ACTION );
}
/**
@ -444,16 +459,58 @@ class WC_Admin_Api_Init {
}
/**
* Init orders data store.
* Schedule an action to process a single Order.
*
* @param int $order_id Order ID.
* @return void
*/
public static function orders_data_store_init() {
public static function schedule_single_order_process( $order_id ) {
if ( 'shop_order' !== get_post_type( $order_id ) ) {
return;
}
// This can get called multiple times for a single order, so we look
// for existing pending jobs for the same order to avoid duplicating efforts.
$existing_jobs = self::queue()->search(
array(
'status' => 'pending',
'per_page' => 1,
'claimed' => false,
'search' => "[{$order_id}]",
)
);
if ( $existing_jobs ) {
$existing_job = current( $existing_jobs );
// Bail out if there's a pending single order action, or a pending dependent action.
if (
( self::SINGLE_ORDER_ACTION === $existing_job->get_hook() ) ||
(
self::QUEUE_DEPEDENT_ACTION === $existing_job->get_hook() &&
in_array( self::SINGLE_ORDER_ACTION, $existing_job->get_args() )
)
) {
return;
}
}
// We want to ensure that customer lookup updates are scheduled before order updates.
self::queue_dependent_action( self::SINGLE_ORDER_ACTION, array( $order_id ), self::CUSTOMERS_BATCH_ACTION );
}
/**
* Attach order lookup update hooks.
*/
public static function orders_lookup_update_init() {
// Activate WC_Order extension.
WC_Admin_Order::add_filters();
// Initialize data stores.
add_action( 'save_post_shop_order', array( __CLASS__, 'schedule_single_order_process' ) );
add_action( 'woocommerce_order_refunded', array( __CLASS__, 'schedule_single_order_process' ) );
WC_Admin_Reports_Orders_Stats_Data_Store::init();
WC_Admin_Reports_Products_Data_Store::init();
WC_Admin_Reports_Taxes_Data_Store::init();
WC_Admin_Reports_Coupons_Data_Store::init();
WC_Admin_Reports_Customers_Data_Store::init();
}
/**
@ -499,19 +556,35 @@ class WC_Admin_Api_Init {
$order_ids = $order_query->get_orders();
foreach ( $order_ids as $order_id ) {
// @todo: schedule single order update if this fails?
WC_Admin_Reports_Orders_Stats_Data_Store::sync_order( $order_id );
WC_Admin_Reports_Products_Data_Store::sync_order_products( $order_id );
WC_Admin_Reports_Coupons_Data_Store::sync_order_coupons( $order_id );
WC_Admin_Reports_Taxes_Data_Store::sync_order_taxes( $order_id );
self::orders_lookup_process_order( $order_id );
}
}
/**
* Init customers report data store.
* Process a single order to update lookup tables for.
* If an error is encountered in one of the updates, a retry action is scheduled.
*
* @param int $order_id Order ID.
* @return void
*/
public static function customers_report_data_store_init() {
WC_Admin_Reports_Customers_Data_Store::init();
public static function orders_lookup_process_order( $order_id ) {
$result = array_sum(
array(
WC_Admin_Reports_Orders_Stats_Data_Store::sync_order( $order_id ),
WC_Admin_Reports_Products_Data_Store::sync_order_products( $order_id ),
WC_Admin_Reports_Coupons_Data_Store::sync_order_coupons( $order_id ),
WC_Admin_Reports_Taxes_Data_Store::sync_order_taxes( $order_id ),
)
);
// If all updates were either skipped or successful, we're done.
// The update methods return -1 for skip, or a boolean success indicator.
if ( 4 === absint( $result ) ) {
return;
}
// Otherwise assume an error occurred and reschedule.
self::schedule_single_order_process( $order_id );
}
/**
@ -579,9 +652,10 @@ class WC_Admin_Api_Init {
* Queue an action to run after another.
*
* @param string $action Action to run after prerequisite.
* @param array $action_args Action arguments.
* @param string $prerequisite_action Prerequisite action.
*/
public static function queue_dependent_action( $action, $prerequisite_action ) {
public static function queue_dependent_action( $action, $action_args, $prerequisite_action ) {
$blocking_jobs = self::queue()->search(
array(
'status' => 'pending',
@ -600,10 +674,10 @@ class WC_Admin_Api_Init {
self::queue()->schedule_single(
$after_blocking_job,
self::QUEUE_DEPEDENT_ACTION,
array( $action, $prerequisite_action )
array( $action, $action_args, $prerequisite_action )
);
} else {
self::queue()->schedule_single( time() + 5, $action );
self::queue()->schedule_single( time() + 5, $action, $action_args );
}
}

View File

@ -51,15 +51,6 @@ class WC_Admin_Reports_Coupons_Data_Store extends WC_Admin_Reports_Data_Store im
$this->report_columns['orders_count'] = str_replace( 'order_id', $table_name . '.order_id', $this->report_columns['orders_count'] );
}
/**
* Set up all the hooks for maintaining and populating table data.
*/
public static function init() {
add_action( 'save_post', array( __CLASS__, 'sync_order_coupons' ) );
add_action( 'clean_post_cache', array( __CLASS__, 'sync_order_coupons' ) );
add_action( 'woocommerce_order_refunded', array( __CLASS__, 'sync_order_coupons' ) );
}
/**
* Returns comma separated ids of included coupons, based on query arguments from the user.
*
@ -315,21 +306,22 @@ class WC_Admin_Reports_Coupons_Data_Store extends WC_Admin_Reports_Data_Store im
*
* @since 3.5.0
* @param int $order_id Order ID.
* @return void
* @return int|bool Returns -1 if order won't be processed, or a boolean indicating processing success.
*/
public static function sync_order_coupons( $order_id ) {
global $wpdb;
$order = wc_get_order( $order_id );
if ( ! $order ) {
return;
return -1;
}
$coupon_items = $order->get_items( 'coupon' );
$num_updated = 0;
foreach ( $coupon_items as $coupon_item ) {
$coupon_id = wc_get_coupon_id_by_code( $coupon_item->get_code() );
$wpdb->replace(
$result = $wpdb->replace(
$wpdb->prefix . self::TABLE_NAME,
array(
'order_id' => $order_id,
@ -352,7 +344,11 @@ class WC_Admin_Reports_Coupons_Data_Store extends WC_Admin_Reports_Data_Store im
* @param int $order_id Order ID.
*/
do_action( 'woocommerce_reports_update_coupon', $coupon_id, $order_id );
$num_updated += intval( $result );
}
return ( count( $coupon_items ) === $num_updated );
}
}

View File

@ -418,7 +418,7 @@ class WC_Admin_Reports_Data_Store {
*/
protected static function get_excluded_report_order_statuses() {
$excluded_statuses = WC_Admin_Settings::get_option( 'woocommerce_excluded_report_order_statuses', array( 'pending', 'failed', 'cancelled' ) );
$excluded_statuses[] = 'refunded';
$excluded_statuses = array_merge( array( 'refunded', 'trash' ), $excluded_statuses );
return apply_filters( 'woocommerce_reports_excluded_order_statuses', $excluded_statuses );
}

View File

@ -73,10 +73,6 @@ class WC_Admin_Reports_Orders_Stats_Data_Store extends WC_Admin_Reports_Data_Sto
* Set up all the hooks for maintaining and populating table data.
*/
public static function init() {
add_action( 'save_post', array( __CLASS__, 'sync_order' ) );
// @todo: this is required as order update skips save_post.
add_action( 'clean_post_cache', array( __CLASS__, 'sync_order' ) );
add_action( 'woocommerce_order_refunded', array( __CLASS__, 'sync_order' ) );
add_action( 'woocommerce_refund_deleted', array( __CLASS__, 'sync_on_refund_delete' ), 10, 2 );
add_action( 'delete_post', array( __CLASS__, 'delete_order' ) );
}
@ -248,10 +244,8 @@ class WC_Admin_Reports_Orders_Stats_Data_Store extends WC_Admin_Reports_Data_Sto
$unique_products = $this->get_unique_product_count( $totals_query['from_clause'], $totals_query['where_time_clause'], $totals_query['where_clause'] );
$totals[0]['products'] = $unique_products;
$segmenting = new WC_Admin_Reports_Orders_Stats_Segmenting( $query_args, $this->report_columns );
$totals[0]['segments'] = $segmenting->get_totals_segments( $totals_query, $table_name );
$totals = (object) $this->cast_numbers( $totals[0] );
$db_intervals = $wpdb->get_col(
@ -360,18 +354,19 @@ class WC_Admin_Reports_Orders_Stats_Data_Store extends WC_Admin_Reports_Data_Sto
* Add order information to the lookup table when orders are created or modified.
*
* @param int $post_id Post ID.
* @return int|bool Returns -1 if order won't be processed, or a boolean indicating processing success.
*/
public static function sync_order( $post_id ) {
if ( 'shop_order' !== get_post_type( $post_id ) ) {
return;
return -1;
}
$order = wc_get_order( $post_id );
if ( ! $order ) {
return;
return -1;
}
self::update( $order );
return self::update( $order );
}
/**
@ -388,14 +383,14 @@ class WC_Admin_Reports_Orders_Stats_Data_Store extends WC_Admin_Reports_Data_Sto
* Update the database with stats data.
*
* @param WC_Order $order Order to update row for.
* @return int|bool|null Number or rows modified or false on failure.
* @return int|bool Returns -1 if order won't be processed, or a boolean indicating processing success.
*/
public static function update( $order ) {
global $wpdb;
$table_name = $wpdb->prefix . self::TABLE_NAME;
if ( ! $order->get_id() || ! $order->get_date_created() ) {
return false;
return -1;
}
$data = array(
@ -450,7 +445,7 @@ class WC_Admin_Reports_Orders_Stats_Data_Store extends WC_Admin_Reports_Data_Sto
}
// Update or add the information to the DB.
$results = $wpdb->replace( $table_name, $data, $format );
$result = $wpdb->replace( $table_name, $data, $format );
/**
* Fires when order's stats reports are updated.
@ -458,7 +453,8 @@ class WC_Admin_Reports_Orders_Stats_Data_Store extends WC_Admin_Reports_Data_Sto
* @param int $order_id Order ID.
*/
do_action( 'woocommerce_reports_update_order_stats', $order->get_id() );
return $results;
return ( 1 === $result );
}
/**

View File

@ -82,15 +82,6 @@ class WC_Admin_Reports_Products_Data_Store extends WC_Admin_Reports_Data_Store i
$this->report_columns['orders_count'] = str_replace( 'order_id', $table_name . '.order_id', $this->report_columns['orders_count'] );
}
/**
* Set up all the hooks for maintaining and populating table data.
*/
public static function init() {
add_action( 'save_post', array( __CLASS__, 'sync_order_products' ) );
add_action( 'clean_post_cache', array( __CLASS__, 'sync_order_products' ) );
add_action( 'woocommerce_order_refunded', array( __CLASS__, 'sync_order_products' ) );
}
/**
* Fills ORDER BY clause of SQL request based on user supplied parameters.
*
@ -319,7 +310,7 @@ class WC_Admin_Reports_Products_Data_Store extends WC_Admin_Reports_Data_Store i
*
* @since 3.5.0
* @param int $order_id Order ID.
* @return void
* @return int|bool Returns -1 if order won't be processed, or a boolean indicating processing success.
*/
public static function sync_order_products( $order_id ) {
global $wpdb;
@ -328,10 +319,13 @@ class WC_Admin_Reports_Products_Data_Store extends WC_Admin_Reports_Data_Store i
// This hook gets called on refunds as well, so return early to avoid errors.
if ( ! $order || 'shop_order_refund' === $order->get_type() ) {
return;
return -1;
}
foreach ( $order->get_items() as $order_item ) {
$order_items = $order->get_items();
$num_updated = 0;
foreach ( $order_items as $order_item ) {
$order_item_id = $order_item->get_id();
$quantity_refunded = $order->get_item_quantity_refunded( $order_item );
$amount_refunded = $order->get_item_amount_refunded( $order_item );
@ -355,7 +349,7 @@ class WC_Admin_Reports_Products_Data_Store extends WC_Admin_Reports_Data_Store i
$net_revenue = $order_item->get_subtotal( 'edit' ) - $amount_refunded;
if ( $quantity_refunded >= $order_item->get_quantity( 'edit' ) ) {
$wpdb->delete(
$result = $wpdb->delete(
$wpdb->prefix . self::TABLE_NAME,
array( 'order_item_id' => $order_item_id ),
array( '%d' )
@ -369,7 +363,7 @@ class WC_Admin_Reports_Products_Data_Store extends WC_Admin_Reports_Data_Store i
*/
do_action( 'woocommerce_reports_delete_product', $order_item_id, $order->get_id() );
} else {
$wpdb->replace(
$result = $wpdb->replace(
$wpdb->prefix . self::TABLE_NAME,
array(
'order_item_id' => $order_item_id,
@ -414,6 +408,10 @@ class WC_Admin_Reports_Products_Data_Store extends WC_Admin_Reports_Data_Store i
*/
do_action( 'woocommerce_reports_update_product', $order_item_id, $order->get_id() );
}
$num_updated += intval( $result );
}
return ( count( $order_items ) === $num_updated );
}
}

View File

@ -66,15 +66,6 @@ class WC_Admin_Reports_Taxes_Data_Store extends WC_Admin_Reports_Data_Store impl
$this->report_columns['orders_count'] = str_replace( 'order_id', $table_name . '.order_id', $this->report_columns['orders_count'] );
}
/**
* Set up all the hooks for maintaining and populating table data.
*/
public static function init() {
add_action( 'save_post', array( __CLASS__, 'sync_order_taxes' ) );
add_action( 'clean_post_cache', array( __CLASS__, 'sync_order_taxes' ) );
add_action( 'woocommerce_order_refunded', array( __CLASS__, 'sync_order_taxes' ) );
}
/**
* Updates the database query with parameters used for Taxes report: categories and order status.
*
@ -255,17 +246,20 @@ class WC_Admin_Reports_Taxes_Data_Store extends WC_Admin_Reports_Data_Store impl
* Create or update an entry in the wc_order_tax_lookup table for an order.
*
* @param int $order_id Order ID.
* @return void
* @return int|bool Returns -1 if order won't be processed, or a boolean indicating processing success.
*/
public static function sync_order_taxes( $order_id ) {
global $wpdb;
$order = wc_get_order( $order_id );
if ( ! $order ) {
return;
return -1;
}
foreach ( $order->get_items( 'tax' ) as $tax_item ) {
$wpdb->replace(
$tax_items = $order->get_items( 'tax' );
$num_updated = 0;
foreach ( $tax_items as $tax_item ) {
$result = $wpdb->replace(
$wpdb->prefix . self::TABLE_NAME,
array(
'order_id' => $order->get_id(),
@ -292,7 +286,11 @@ class WC_Admin_Reports_Taxes_Data_Store extends WC_Admin_Reports_Data_Store impl
* @param int $order_id Order ID.
*/
do_action( 'woocommerce_reports_update_tax', $tax_item->get_rate_id(), $order->get_id() );
$num_updated += intval( $result );
}
return ( count( $tax_items ) === $num_updated );
}
}

View File

@ -60,11 +60,11 @@
}
},
"@babel/generator": {
"version": "7.3.0",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.3.0.tgz",
"integrity": "sha512-dZTwMvTgWfhmibq4V9X+LMf6Bgl7zAodRn9PvcPdhlzFMbvUutx74dbEv7Atz3ToeEpevYEJtAwfxq/bDCzHWg==",
"version": "7.3.2",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.3.2.tgz",
"integrity": "sha512-f3QCuPppXxtZOEm5GWPra/uYUjmNQlu9pbAD8D/9jze4pTY83rTtB1igTBSwvkeNlC5gR24zFFkz+2WHLFQhqQ==",
"requires": {
"@babel/types": "^7.3.0",
"@babel/types": "^7.3.2",
"jsesc": "^2.5.1",
"lodash": "^4.17.10",
"source-map": "^0.5.0",
@ -274,9 +274,9 @@
}
},
"@babel/parser": {
"version": "7.3.1",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.3.1.tgz",
"integrity": "sha512-ATz6yX/L8LEnC3dtLQnIx4ydcPxhLcoy9Vl6re00zb2w5lG6itY6Vhnr1KFRPq/FHNsgl/gh2mjNN20f9iJTTA=="
"version": "7.3.2",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.3.2.tgz",
"integrity": "sha512-QzNUC2RO1gadg+fs21fi0Uu0OuGNzRKEmgCxoLNzbCdoprLwjfmZwzUrpUNfJPaVRwBpDY47A17yYEGWyRelnQ=="
},
"@babel/plugin-proposal-async-generator-functions": {
"version": "7.2.0",
@ -298,9 +298,9 @@
}
},
"@babel/plugin-proposal-object-rest-spread": {
"version": "7.3.1",
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.3.1.tgz",
"integrity": "sha512-Nmmv1+3LqxJu/V5jU9vJmxR/KIRWFk2qLHmbB56yRRRFhlaSuOVXscX3gUmhaKgUhzA3otOHVubbIEVYsZ0eZg==",
"version": "7.3.2",
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.3.2.tgz",
"integrity": "sha512-DjeMS+J2+lpANkYLLO+m6GjoTMygYglKmRe6cDTbFv3L9i6mmiE8fe6B8MtCSLZpVXscD5kn7s6SgtHrDoBWoA==",
"requires": {
"@babel/helper-plugin-utils": "^7.0.0",
"@babel/plugin-syntax-object-rest-spread": "^7.2.0"
@ -424,9 +424,9 @@
}
},
"@babel/plugin-transform-destructuring": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.2.0.tgz",
"integrity": "sha512-coVO2Ayv7g0qdDbrNiadE4bU7lvCd9H539m2gMknyVjjMdwF/iCOM7R+E8PkntoqLkltO0rk+3axhpp/0v68VQ==",
"version": "7.3.2",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.3.2.tgz",
"integrity": "sha512-Lrj/u53Ufqxl/sGxyjsJ2XNtNuEjDyjpqdhMNh5aZ+XFOdThL46KBj27Uem4ggoezSYBxKWAil6Hu8HtwqesYw==",
"requires": {
"@babel/helper-plugin-utils": "^7.0.0"
}
@ -730,9 +730,9 @@
}
},
"@babel/types": {
"version": "7.3.0",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.3.0.tgz",
"integrity": "sha512-QkFPw68QqWU1/RVPyBe8SO7lXbPfjtqAxRYQKpFpaB8yMq7X2qAqfwK5LKoQufEkSmO5NQ70O6Kc3Afk03RwXw==",
"version": "7.3.2",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.3.2.tgz",
"integrity": "sha512-3Y6H8xlUlpbGR+XvawiH0UXehqydTmNmEpozWcXymqwcrwYAl5KMvKtQ+TF6f6E08V6Jur7v/ykdDSF+WDEIXQ==",
"requires": {
"esutils": "^2.0.2",
"lodash": "^4.17.10",
@ -1765,21 +1765,21 @@
}
},
"inquirer": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.2.1.tgz",
"integrity": "sha512-088kl3DRT2dLU5riVMKKr1DlImd6X7smDhpXUCkJDCKvTEJeRiXh0G132HG9u5a+6Ylw9plFRY7RuTnwohYSpg==",
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.2.2.tgz",
"integrity": "sha512-Z2rREiXA6cHRR9KBOarR3WuLlFzlIfAEIiB45ll5SSadMg7WqOh1MKEjjndfuH5ewXdixWCxqnVfGOQzPeiztA==",
"dev": true,
"requires": {
"ansi-escapes": "^3.0.0",
"chalk": "^2.0.0",
"ansi-escapes": "^3.2.0",
"chalk": "^2.4.2",
"cli-cursor": "^2.1.0",
"cli-width": "^2.0.0",
"external-editor": "^3.0.0",
"external-editor": "^3.0.3",
"figures": "^2.0.0",
"lodash": "^4.17.10",
"lodash": "^4.17.11",
"mute-stream": "0.0.7",
"run-async": "^2.2.0",
"rxjs": "^6.1.0",
"rxjs": "^6.4.0",
"string-width": "^2.1.0",
"strip-ansi": "^5.0.0",
"through": "^2.3.6"
@ -2113,9 +2113,9 @@
"integrity": "sha512-y+h7tNlxDPDrH/TrSw1wCSm6FoEAY8FrbUxYng3BMSYBTUsX1utLooizk9v8J1yy6F9AioXNnPZ1qiu2vsa08Q=="
},
"@types/node": {
"version": "10.12.19",
"resolved": "https://registry.npmjs.org/@types/node/-/node-10.12.19.tgz",
"integrity": "sha512-2NVovndCjJQj6fUUn9jCgpP4WSqr+u1SoUZMZyJkhGeBFsm6dE46l31S7lPUYt9uQ28XI+ibrJA1f5XyH5HNtA=="
"version": "10.12.21",
"resolved": "https://registry.npmjs.org/@types/node/-/node-10.12.21.tgz",
"integrity": "sha512-CBgLNk4o3XMnqMc0rhb6lc77IwShMEglz05deDcn2lQxyXEZivfwgYJu7SMha9V5XcrP6qZuevTHV/QrN2vjKQ=="
},
"@webassemblyjs/ast": {
"version": "1.7.11",
@ -2999,9 +2999,9 @@
},
"dependencies": {
"acorn": {
"version": "6.0.6",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-6.0.6.tgz",
"integrity": "sha512-5M3G/A4uBSMIlfJ+h9W125vJvPFH/zirISsW5qfxF5YzEvXJCtolLoQvM5yZft0DvMcUrPGKPOlgEu55I6iUtA=="
"version": "6.0.7",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-6.0.7.tgz",
"integrity": "sha512-HNJNgE60C9eOTgn974Tlp3dpLZdUr+SoxxDwPaY9J/kDNOLQTkaDgwBUXAF4SSsrAwD9RpdxuHK/EbuF+W9Ahw=="
}
}
},
@ -3060,9 +3060,9 @@
}
},
"ajv": {
"version": "6.7.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.7.0.tgz",
"integrity": "sha512-RZXPviBTtfmtka9n9sy1N5M5b82CbxWIR6HIis4s3WQTXDJamc/0gpCWNGz6EWdWp4DOfjzJfhz/AS9zVPjjWg==",
"version": "6.8.1",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.8.1.tgz",
"integrity": "sha512-eqxCp82P+JfqL683wwsL73XmFs1eG6qjw+RD3YHx+Jll1r0jNd4dh8QG9NYAeNGA/hnZjeEDgtTskgJULbxpWQ==",
"requires": {
"fast-deep-equal": "^2.0.1",
"fast-json-stable-stringify": "^2.0.0",
@ -4262,9 +4262,9 @@
}
},
"binary-extensions": {
"version": "1.12.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.12.0.tgz",
"integrity": "sha512-DYWGk01lDcxeS/K9IHPGWfT8PsJmbXRtRd2Sx72Tnb8pcYZQFF1oSDb8hJtS1vhp212q1Rzi5dUf9+nq0o9UIg==",
"version": "1.13.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.0.tgz",
"integrity": "sha512-EgmjVLMn22z7eGGv3kcnHwSnJXmFHjISTY9E/S5lIcTD3Oxw05QTcBLNkJFzcb3cNueUdF/IN4U+d78V0zO8Hw==",
"dev": true
},
"bindings": {
@ -4632,9 +4632,9 @@
}
},
"caniuse-lite": {
"version": "1.0.30000932",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000932.tgz",
"integrity": "sha512-4bghJFItvzz8m0T3lLZbacmEY9X1Z2AtIzTr7s7byqZIOumASfr4ynDx7rtm0J85nDmx8vsgR6vnaSoeU8Oh0A=="
"version": "1.0.30000934",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000934.tgz",
"integrity": "sha512-o7yfZn0R9N+mWAuksDsdLsb1gu9o//XK0QSU0zSSReKNRsXsFc/n/psxi0YSPNiqlKxImp5h4DHnAPdwYJ8nNA=="
},
"capture-exit": {
"version": "1.2.0",
@ -6357,9 +6357,9 @@
"dev": true
},
"cssom": {
"version": "0.3.4",
"resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.4.tgz",
"integrity": "sha512-+7prCSORpXNeR4/fUP3rL+TzqtiFfhMvTd7uEqMdgPvLPt4+uzFUeufx5RHjGTACCargg/DiEt/moMQmvnfkog=="
"version": "0.3.6",
"resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.6.tgz",
"integrity": "sha512-DtUeseGk9/GBW0hl0vVPpU22iHL6YB5BUX7ml1hB+GMpo0NX5G4voX3kdWiMSEguFtcW3Vh3djqNF4aIe6ne0A=="
},
"cssstyle": {
"version": "1.1.1",
@ -6456,17 +6456,17 @@
"integrity": "sha512-EYVwBxQGEjLCKF2pJ4+yrErskDnz5v403qvAid96cNdCMr8rmCYfY5RGzWz24mdIbxmDf6/4EAH+K9xperD5jg=="
},
"d3-shape": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.3.tgz",
"integrity": "sha512-f7V9wHQCmv4s4N7EmD5i0mwJ5y09L8r1rWVrzH1Av0YfgBKJCnTJGho76rS4HNUIw6tTBbWfFcs4ntP/MKWF4A==",
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.4.tgz",
"integrity": "sha512-izaz4fOpOnY3CD17hkZWNxbaN70sIGagLR/5jb6RS96Y+6VqX+q1BQf1av6QSBRdfULi3Gb8Js4CzG4+KAPjMg==",
"requires": {
"d3-path": "1"
}
},
"d3-time": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/d3-time/-/d3-time-1.0.10.tgz",
"integrity": "sha512-hF+NTLCaJHF/JqHN5hE8HVGAXPStEq6/omumPE/SxyHVrR7/qQxusFDo0t0c/44+sCGHthC7yNGFZIEgju0P8g=="
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/d3-time/-/d3-time-1.0.11.tgz",
"integrity": "sha512-Z3wpvhPLW4vEScGeIMUckDW7+3hWKOQfAWg/U7PlWBnQmeKQ00gCUsTtWSYulrKNA7ta8hJ+xXc6MHrMuITwEw=="
},
"d3-time-format": {
"version": "2.1.3",
@ -7234,9 +7234,9 @@
"dev": true
},
"duplexify": {
"version": "3.6.1",
"resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.6.1.tgz",
"integrity": "sha512-vM58DwdnKmty+FSPzT14K9JXb90H+j5emaR4KYbr2KTIz00WHGbWOe5ghQTx233ZCLZtrGDALzKwcjEtSt35mA==",
"version": "3.7.1",
"resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz",
"integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==",
"dev": true,
"requires": {
"end-of-stream": "^1.0.0",
@ -7317,9 +7317,9 @@
"dev": true
},
"electron-to-chromium": {
"version": "1.3.109",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.109.tgz",
"integrity": "sha512-1qhgVZD9KIULMyeBkbjU/dWmm30zpPUfdWZfVO3nPhbtqMHJqHr4Ua5wBcWtAymVFrUCuAJxjMF1OhG+bR21Ow=="
"version": "1.3.113",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.113.tgz",
"integrity": "sha512-De+lPAxEcpxvqPTyZAXELNpRZXABRxf+uL/rSykstQhzj/B0l1150G/ExIIxKc16lI89Hgz81J0BHAcbTqK49g=="
},
"elliptic": {
"version": "6.4.1",
@ -7422,9 +7422,9 @@
}
},
"enzyme-adapter-react-16": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.8.0.tgz",
"integrity": "sha512-7cVHIKutqnesGeM3CjNFHSvktpypSWBokrBO8wIW+BVx+HGxWCF87W9TpkIIYJqgCtdw9FQGFrAbLg8kSwPRuQ==",
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.9.0.tgz",
"integrity": "sha512-tUqmeLi0Y3PxuiPSykjn8ZMqzCnaRIVywNx0i50+nhd4y/b3JtXRbsvIc8HKxn3heE4t969EI2461Kc9FYxjdw==",
"requires": {
"enzyme-adapter-utils": "^1.10.0",
"function.prototype.name": "^1.1.0",
@ -7607,9 +7607,9 @@
},
"dependencies": {
"acorn": {
"version": "6.0.6",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-6.0.6.tgz",
"integrity": "sha512-5M3G/A4uBSMIlfJ+h9W125vJvPFH/zirISsW5qfxF5YzEvXJCtolLoQvM5yZft0DvMcUrPGKPOlgEu55I6iUtA==",
"version": "6.0.7",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-6.0.7.tgz",
"integrity": "sha512-HNJNgE60C9eOTgn974Tlp3dpLZdUr+SoxxDwPaY9J/kDNOLQTkaDgwBUXAF4SSsrAwD9RpdxuHK/EbuF+W9Ahw==",
"dev": true
},
"acorn-jsx": {
@ -7682,21 +7682,21 @@
"dev": true
},
"inquirer": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.2.1.tgz",
"integrity": "sha512-088kl3DRT2dLU5riVMKKr1DlImd6X7smDhpXUCkJDCKvTEJeRiXh0G132HG9u5a+6Ylw9plFRY7RuTnwohYSpg==",
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.2.2.tgz",
"integrity": "sha512-Z2rREiXA6cHRR9KBOarR3WuLlFzlIfAEIiB45ll5SSadMg7WqOh1MKEjjndfuH5ewXdixWCxqnVfGOQzPeiztA==",
"dev": true,
"requires": {
"ansi-escapes": "^3.0.0",
"chalk": "^2.0.0",
"ansi-escapes": "^3.2.0",
"chalk": "^2.4.2",
"cli-cursor": "^2.1.0",
"cli-width": "^2.0.0",
"external-editor": "^3.0.0",
"external-editor": "^3.0.3",
"figures": "^2.0.0",
"lodash": "^4.17.10",
"lodash": "^4.17.11",
"mute-stream": "0.0.7",
"run-async": "^2.2.0",
"rxjs": "^6.1.0",
"rxjs": "^6.4.0",
"string-width": "^2.1.0",
"strip-ansi": "^5.0.0",
"through": "^2.3.6"
@ -8656,39 +8656,13 @@
"dev": true
},
"flush-write-stream": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.0.3.tgz",
"integrity": "sha512-calZMC10u0FMUqoiunI2AiGIIUtUIvifNwkHhNupZH4cbNnW1Itkoh/Nf5HFYmDrwWPjrUxpkZT0KhuCq0jmGw==",
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.1.0.tgz",
"integrity": "sha512-6MHED/cmsyux1G4/Cek2Z776y9t7WCNd3h2h/HW91vFeU7pzMhA8XvAlDhHcanG5IWuIh/xcC7JASY4WQpG6xg==",
"dev": true,
"requires": {
"inherits": "^2.0.1",
"readable-stream": "^2.0.4"
},
"dependencies": {
"readable-stream": {
"version": "2.3.6",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
"integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
"dev": true,
"requires": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
},
"string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"dev": true,
"requires": {
"safe-buffer": "~5.1.0"
}
}
"inherits": "^2.0.3",
"readable-stream": "^3.1.1"
}
},
"for-in": {
@ -11339,8 +11313,7 @@
"is-wsl": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz",
"integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=",
"dev": true
"integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0="
},
"isarray": {
"version": "1.0.0",
@ -13263,11 +13236,11 @@
}
},
"magic-string": {
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.1.tgz",
"integrity": "sha512-sCuTz6pYom8Rlt4ISPFn6wuFodbKMIHUMv4Qko9P17dpxb7s52KJTmRuZZqHdGmLCK9AOcDare039nRIcfdkEg==",
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.2.tgz",
"integrity": "sha512-iLs9mPjh9IuTtRsqqhNGYcZXGei0Nh/A4xirrsqW7c+QhKVFL2vm7U09ru6cHRD22azaP/wMDgI+HCqbETMTtg==",
"requires": {
"sourcemap-codec": "^1.4.1"
"sourcemap-codec": "^1.4.4"
}
},
"make-dir": {
@ -14163,20 +14136,21 @@
}
},
"node-notifier": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-5.3.0.tgz",
"integrity": "sha512-AhENzCSGZnZJgBARsUjnQ7DnZbzyP+HxlVXuD0xqAnvL8q+OqtSX7lGg9e8nHzwXkMMXNdVeqq4E2M3EUAqX6Q==",
"version": "5.4.0",
"resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-5.4.0.tgz",
"integrity": "sha512-SUDEb+o71XR5lXSTyivXd9J7fCloE3SyP4lSgt3lU2oSANiox+SxlNRGPjDKrwU1YN3ix2KN/VGGCg0t01rttQ==",
"requires": {
"growly": "^1.3.0",
"is-wsl": "^1.1.0",
"semver": "^5.5.0",
"shellwords": "^0.1.1",
"which": "^1.3.0"
}
},
"node-releases": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.6.tgz",
"integrity": "sha512-lODUVHEIZutZx+TDdOk47qLik8FJMXzJ+WnyUGci1MTvTOyzZrz5eVPIIpc5Hb3NfHZGeGHeuwrRYVI1PEITWg==",
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.7.tgz",
"integrity": "sha512-bKdrwaqJUPHqlCzDD7so/R+Nk0jGv9a11ZhLrD9f6i947qGLrGAhU3OxRENa19QQmwzGy/g6zCDEuLGDO8HPvA==",
"requires": {
"semver": "^5.3.0"
}
@ -14457,13 +14431,14 @@
}
},
"npm-package-json-lint": {
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/npm-package-json-lint/-/npm-package-json-lint-3.4.1.tgz",
"integrity": "sha512-W4xlmeFRAY34GQoHUywqoI3PxVZ0hugjbZLiGnVgFjgmvRRcmxKwwmubMe0lAD78vgOHgJZRGubdVXwkp9d3QA==",
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/npm-package-json-lint/-/npm-package-json-lint-3.5.0.tgz",
"integrity": "sha512-MELethOnZW5uVzP65oTQEH2fI6eS/BQEXjvOTyQkUQqGHP9si5pxCWcO+Q4dsahb+4yG7GMxFhpF42AjhCbgRA==",
"requires": {
"ajv": "^6.5.4",
"chalk": "^2.4.1",
"ajv": "^6.7.0",
"chalk": "^2.4.2",
"glob": "^7.1.3",
"ignore": "^5.0.5",
"is-path-inside": "^2.0.0",
"is-plain-obj": "^1.1.0",
"is-resolvable": "^1.1.0",
@ -14472,7 +14447,14 @@
"plur": "^3.0.1",
"semver": "^5.6.0",
"strip-json-comments": "^2.0.1",
"validator": "^10.8.0"
"validator": "^10.11.0"
},
"dependencies": {
"ignore": {
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.0.5.tgz",
"integrity": "sha512-kOC8IUb8HSDMVcYrDVezCxpJkzSQWTAzf3olpKM6o9rM5zpojx23O0Fl8Wr4+qJ6ZbPEHqf1fdwev/DS7v7pmA=="
}
}
},
"npm-packlist": {
@ -14561,9 +14543,9 @@
"integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0="
},
"nwsapi": {
"version": "2.0.9",
"resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.0.9.tgz",
"integrity": "sha512-nlWFSCTYQcHk/6A9FFnfhKc14c3aFhfdNBXgo8Qgi9QTBu/qg3Ww+Uiz9wMzXd1T8GFxPc2QIHB6Qtf2XFryFQ=="
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.1.0.tgz",
"integrity": "sha512-ZG3bLAvdHmhIjaQ/Db1qvBxsGvFMLIRpQszyqbg31VJ53UP++uZX1/gf3Ut96pdwN9AuDwlMqIYLm0UPCdUeHg=="
},
"oauth-sign": {
"version": "0.9.0",
@ -19717,9 +19699,9 @@
}
},
"stylelint-scss": {
"version": "3.5.1",
"resolved": "https://registry.npmjs.org/stylelint-scss/-/stylelint-scss-3.5.1.tgz",
"integrity": "sha512-XNWKTU1a2EUNWdauxHPTJlGNNQzIbg48OTTIdBs5xTXxpbYAGtX/J+jBqMPjxfdySXijc/mexubuZ+ZinUGGgw==",
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/stylelint-scss/-/stylelint-scss-3.5.2.tgz",
"integrity": "sha512-HL95s8Q6wihbJe7c7z6rL9GHVHOF3H3tXkVmGutitwn14LYR52JYMwCkcifqlf4nRsvXrUDaoH6OHOdilifyjw==",
"dev": true,
"requires": {
"lodash": "^4.17.11",
@ -19931,14 +19913,14 @@
}
},
"terser": {
"version": "3.14.1",
"resolved": "https://registry.npmjs.org/terser/-/terser-3.14.1.tgz",
"integrity": "sha512-NSo3E99QDbYSMeJaEk9YW2lTg3qS9V0aKGlb+PlOrei1X02r1wSBHCNX/O+yeTRFSWPKPIGj6MqvvdqV4rnVGw==",
"version": "3.16.1",
"resolved": "https://registry.npmjs.org/terser/-/terser-3.16.1.tgz",
"integrity": "sha512-JDJjgleBROeek2iBcSNzOHLKsB/MdDf+E/BOAJ0Tk9r7p9/fVobfv7LMJ/g/k3v9SXdmjZnIlFd5nfn/Rt0Xow==",
"dev": true,
"requires": {
"commander": "~2.17.1",
"source-map": "~0.6.1",
"source-map-support": "~0.5.6"
"source-map-support": "~0.5.9"
},
"dependencies": {
"commander": {
@ -19966,9 +19948,9 @@
}
},
"terser-webpack-plugin": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.2.1.tgz",
"integrity": "sha512-GGSt+gbT0oKcMDmPx4SRSfJPE1XaN3kQRWG4ghxKQw9cn5G9x6aCKSsgYdvyM0na9NJ4Drv0RG6jbBByZ5CMjw==",
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.2.2.tgz",
"integrity": "sha512-1DMkTk286BzmfylAvLXwpJrI7dWa5BnFmscV/2dCr8+c56egFcbaeFAl7+sujAjdmpLam21XRdhA4oifLyiWWg==",
"dev": true,
"requires": {
"cacache": "^11.0.2",
@ -19976,7 +19958,7 @@
"schema-utils": "^1.0.0",
"serialize-javascript": "^1.4.0",
"source-map": "^0.6.1",
"terser": "^3.8.1",
"terser": "^3.16.1",
"webpack-sources": "^1.1.0",
"worker-farm": "^1.5.2"
},

View File

@ -1,3 +1,8 @@
# 1.5.0 (unreleased)
- Improves display of charts where all values are 0.
- Fix X-axis labels in hourly bar charts.
- New `<Search>` prop named `showClearButton`, that will display a 'Clear' button when the search box contains one or more tags.
# 1.4.2
- Add emoji-flags dependency
@ -5,7 +10,6 @@
- Chart component: format numbers and prices using store currency settings.
- Make `href`/linking optional in SummaryNumber.
- Fix SummaryNumber example code.
- New `<Search>` prop named `showClearButton`, that will display a 'Clear' button when the search box contains one or more tags.
# 1.4.0
- Add download log ip address autocompleter to search component

View File

@ -197,7 +197,7 @@ export const drawAxis = ( node, params, xOffset ) => {
: compareStrings( formatter( prevMonth ), formatter( monthDate ) ).join( ' ' );
};
const yGrids = getYGrids( params.yMax );
const yGrids = getYGrids( params.yMax === 0 ? 1 : params.yMax );
const ticks = params.xTicks.map( d => ( params.type === 'line' ? moment( d ).toDate() : d ) );
@ -210,7 +210,7 @@ export const drawAxis = ( node, params, xOffset ) => {
d3AxisBottom( xScale )
.tickValues( ticks )
.tickFormat( ( d, i ) => params.interval === 'hour'
? params.xFormat( d )
? params.xFormat( d instanceof Date ? d : moment( d ).toDate() )
: removeDuplicateDates( d, i, ticks, params.xFormat ) )
);
@ -257,7 +257,7 @@ export const drawAxis = ( node, params, xOffset ) => {
.attr( 'text-anchor', 'start' )
.call(
d3AxisLeft( params.yTickOffset )
.tickValues( yGrids )
.tickValues( params.yMax === 0 ? [ yGrids[ 0 ] ] : yGrids )
.tickFormat( d => params.yFormat( d !== 0 ? d : 0 ) )
);

View File

@ -72,7 +72,7 @@ export const getYMax = lineData => {
*/
export const getYScale = ( height, yMax ) =>
d3ScaleLinear()
.domain( [ 0, yMax ] )
.domain( [ 0, yMax === 0 ? 1 : yMax ] )
.rangeRound( [ height, 0 ] );
/**
@ -83,5 +83,5 @@ export const getYScale = ( height, yMax ) =>
*/
export const getYTickOffset = ( height, yMax ) =>
d3ScaleLinear()
.domain( [ 0, yMax ] )
.domain( [ 0, yMax === 0 ? 1 : yMax ] )
.rangeRound( [ height + 12, 12 ] );

View File

@ -107,6 +107,14 @@ describe( 'Y scales', () => {
expect( scaleLinear().domain ).toHaveBeenLastCalledWith( [ 0, 15000000 ] );
expect( scaleLinear().rangeRound ).toHaveBeenLastCalledWith( [ 100, 0 ] );
} );
it( 'avoids the domain starting and ending at the same point when yMax is 0', () => {
getYScale( 100, 0 );
const args = scaleLinear().domain.mock.calls;
const lastArgs = args[ args.length - 1 ][ 0 ];
expect( lastArgs[ 0 ] ).toBeLessThan( lastArgs[ 1 ] );
} );
} );
describe( 'getYTickOffset', () => {
@ -116,5 +124,13 @@ describe( 'Y scales', () => {
expect( scaleLinear().domain ).toHaveBeenLastCalledWith( [ 0, 15000000 ] );
expect( scaleLinear().rangeRound ).toHaveBeenLastCalledWith( [ 112, 12 ] );
} );
it( 'avoids the domain starting and ending at the same point when yMax is 0', () => {
getYTickOffset( 100, 0 );
const args = scaleLinear().domain.mock.calls;
const lastArgs = args[ args.length - 1 ][ 0 ];
expect( lastArgs[ 0 ] ).toBeLessThan( lastArgs[ 1 ] );
} );
} );
} );

View File

@ -131,7 +131,7 @@
.woocommerce-search__clear {
position: absolute;
right: 10px;
top: calc( 50% - 10px );
top: calc(50% - 10px);
& > .dashicon {
color: #c9c9c9;

View File

@ -0,0 +1,85 @@
<?php
/**
* REST API Init Class Test
*
* @package WooCommerce\Tests\API
* @since 3.5.0
*/
/**
* Class WC_Tests_API_Init
*/
class WC_Tests_API_Init extends WC_REST_Unit_Test_Case {
/**
* Set up.
*/
public function setUp() {
parent::setUp();
$this->queue = new WC_Admin_Test_Action_Queue();
WC_Admin_Api_Init::set_queue( $this->queue );
}
/**
* Tear down.
*/
public function tearDown() {
parent::tearDown();
WC_Admin_Api_Init::set_queue( null );
$this->queue->actions = array();
}
/**
* Cause a failure when updating order stats for the test order, without deleting it.
*
* @param string $query Query.
* @return string
*/
public function filter_order_query( $query ) {
if (
0 === strpos( $query, 'REPLACE INTO' ) &&
false !== strpos( $query, WC_Admin_Reports_Orders_Stats_Data_Store::TABLE_NAME )
) {
remove_filter( 'query', array( $this, 'filter_order_query' ) );
return 'THIS WONT MATCH';
}
return $query;
}
/**
* Test that a retry job is scheduled for a failed sync.
*
* @return void
*/
public function test_order_sync_retries_on_failure() {
// Create a test Order.
$product = new WC_Product_Simple();
$product->set_name( 'Test Product' );
$product->set_regular_price( 25 );
$product->save();
$order = WC_Helper_Order::create_order( 1, $product );
$order->set_status( 'completed' );
$order->set_total( 100 ); // $25 x 4.
$order->save();
// Clear the existing action queue (the above save adds an action).
$this->queue->actions = array();
// Force a failure by sabotaging the query run after retreiving order coupons.
add_filter( 'query', array( $this, 'filter_order_query' ) );
// Initiate sync.
WC_Admin_Api_Init::orders_lookup_process_order( $order->get_id() );
// Verify that a retry job was scheduled.
$this->assertCount( 1, $this->queue->actions );
$this->assertArraySubset(
array(
'hook' => WC_Admin_Api_Init::SINGLE_ORDER_ACTION,
'args' => array( $order->get_id() ),
),
$this->queue->actions[0]
);
}
}

View File

@ -64,6 +64,8 @@ class WC_Tests_API_Reports_Categories extends WC_REST_Unit_Test_Case {
$order->set_total( 100 ); // $25 x 4.
$order->save();
WC_Helper_Queue::run_all_pending();
$uncategorized_term = get_term_by( 'slug', 'uncategorized', 'product_cat' );
$response = $this->server->dispatch( new WP_REST_Request( 'GET', $this->endpoint ) );

View File

@ -87,6 +87,8 @@ class WC_Tests_API_Reports_Coupons_Stats extends WC_REST_Unit_Test_Case {
$order_2c->set_date_created( $time );
$order_2c->save();
WC_Helper_Queue::run_all_pending();
$request = new WP_REST_Request( 'GET', $this->endpoint );
$request->set_query_params(
array(

View File

@ -82,6 +82,8 @@ class WC_Tests_API_Reports_Coupons extends WC_REST_Unit_Test_Case {
$order_2c->calculate_totals();
$order_2c->save();
WC_Helper_Queue::run_all_pending();
$response = $this->server->dispatch( new WP_REST_Request( 'GET', $this->endpoint ) );
$coupon_reports = $response->get_data();

View File

@ -126,6 +126,8 @@ class WC_Tests_API_Reports_Customers_Stats extends WC_REST_Unit_Test_Case {
$order->set_total( 9.12 );
$order->save();
WC_Helper_Queue::run_all_pending();
$request = new WP_REST_Request( 'GET', $this->endpoint );
$response = $this->server->dispatch( $request );
$reports = $response->get_data();

View File

@ -134,6 +134,8 @@ class WC_Tests_API_Reports_Customers extends WC_REST_Unit_Test_Case {
$order->set_total( 100 );
$order->save();
WC_Helper_Queue::run_all_pending();
$request = new WP_REST_Request( 'GET', $this->endpoint );
$request->set_query_params(
array(

View File

@ -67,6 +67,8 @@ class WC_Tests_API_Reports_Orders extends WC_REST_Unit_Test_Case {
$order->set_total( 100 ); // $25 x 4.
$order->save();
WC_Helper_Queue::run_all_pending();
$expected_customer_id = WC_Admin_Reports_Customers_Data_Store::get_customer_id_by_user_id( 1 );
$response = $this->server->dispatch( new WP_REST_Request( 'GET', $this->endpoint ) );

View File

@ -84,6 +84,8 @@ class WC_Tests_API_Reports_Performance_Indicators extends WC_REST_Unit_Test_Case
$object->set_user_ip_address( '1.2.3.4' );
$object->save();
WC_Helper_Queue::run_all_pending();
$time = time();
$request = new WP_REST_Request( 'GET', $this->endpoint );
$request->set_query_params(

View File

@ -71,6 +71,8 @@ class WC_Tests_API_Reports_Products_Stats extends WC_REST_Unit_Test_Case {
$order->set_total( 97 ); // $25x4 products + $10 shipping - $20 discount + $7 tax.
$order->save();
WC_Helper_Queue::run_all_pending();
$request = new WP_REST_Request( 'GET', $this->endpoint );
$request->set_query_params(
array(

View File

@ -67,6 +67,8 @@ class WC_Tests_API_Reports_Products extends WC_REST_Unit_Test_Case {
$order->set_total( 100 ); // $25 x 4.
$order->save();
WC_Helper_Queue::run_all_pending();
$response = $this->server->dispatch( new WP_REST_Request( 'GET', $this->endpoint ) );
$reports = $response->get_data();

View File

@ -91,6 +91,8 @@ class WC_Tests_API_Reports_Taxes extends WC_REST_Unit_Test_Case {
)
);
WC_Helper_Queue::run_all_pending();
$response = $this->server->dispatch( new WP_REST_Request( 'GET', $this->endpoint ) );
$reports = $response->get_data();

View File

@ -65,6 +65,8 @@ class WC_Tests_API_Reports_Variations extends WC_REST_Unit_Test_Case {
$order->set_total( 100 ); // $25 x 4.
$order->save();
WC_Helper_Queue::run_all_pending();
$response = $this->server->dispatch( new WP_REST_Request( 'GET', $this->endpoint ) );
$reports = $response->get_data();

View File

@ -136,7 +136,7 @@ class WC_Tests_Reports_Regenerate_Batching extends WC_REST_Unit_Test_Case {
// insert a blocking job.
WC_Admin_Api_Init::queue()->schedule_single( time(), 'blocking_job', array( 'stuff' ) );
// queue an action that depends on blocking job.
WC_Admin_Api_Init::queue_dependent_action( 'dependent_action', 'blocking_job' );
WC_Admin_Api_Init::queue_dependent_action( 'dependent_action', array(), 'blocking_job' );
// verify that the action was properly blocked.
$this->assertEmpty(
WC_Admin_Api_Init::queue()->search(
@ -151,13 +151,13 @@ class WC_Tests_Reports_Regenerate_Batching extends WC_REST_Unit_Test_Case {
WC_Admin_Api_Init::queue()->search(
array(
'hook' => WC_Admin_Api_Init::QUEUE_DEPEDENT_ACTION,
'args' => array( 'dependent_action', 'blocking_job' ),
'args' => array( 'dependent_action', array(), 'blocking_job' ),
)
)
);
// queue an action that isn't blocked.
WC_Admin_Api_Init::queue_dependent_action( 'another_dependent_action', 'nonexistant_blocking_job' );
WC_Admin_Api_Init::queue_dependent_action( 'another_dependent_action', array(), 'nonexistant_blocking_job' );
// verify that the dependent action was queued.
$this->assertCount(
1,
@ -172,7 +172,7 @@ class WC_Tests_Reports_Regenerate_Batching extends WC_REST_Unit_Test_Case {
WC_Admin_Api_Init::queue()->search(
array(
'hook' => WC_Admin_Api_Init::QUEUE_DEPEDENT_ACTION,
'args' => array( 'another_dependent_action', 'nonexistant_blocking_job' ),
'args' => array( 'another_dependent_action', array(), 'nonexistant_blocking_job' ),
)
)
);

View File

@ -123,3 +123,4 @@ wc_test_includes();
require_once dirname( __FILE__ ) . '/framework/helpers/class-wc-helper-reports.php';
require_once dirname( __FILE__ ) . '/framework/helpers/class-wc-helper-admin-notes.php';
require_once dirname( __FILE__ ) . '/framework/helpers/class-wc-test-action-queue.php';
require_once dirname( __FILE__ ) . '/framework/helpers/class-wc-helper-queue.php';

View File

@ -0,0 +1,32 @@
<?php
/**
* Helper code for wc-admin unit tests.
*
* @package WooCommerce\Tests\Framework\Helpers
*/
/**
* Class WC_Helper_Queue.
*
* This helper class should ONLY be used for unit tests!.
*/
class WC_Helper_Queue {
/**
* Run all pending queued actions.
*
* @return void
*/
public static function run_all_pending() {
$jobs = WC()->queue()->search(
array(
'per_page' => -1,
'status' => 'pending',
'claimed' => false,
)
);
foreach ( $jobs as $job ) {
$job->execute();
}
}
}

View File

@ -54,6 +54,8 @@ class WC_Tests_Reports_Coupons_Stats extends WC_Unit_Test_Case {
$order_2c->calculate_totals();
$order_2c->save();
WC_Helper_Queue::run_all_pending();
$data_store = new WC_Admin_Reports_Coupons_Stats_Data_Store();
$start_time = date( 'Y-m-d 00:00:00', $order->get_date_created()->getOffsetTimestamp() );
$end_time = date( 'Y-m-d 23:59:59', $order->get_date_created()->getOffsetTimestamp() );

View File

@ -54,6 +54,8 @@ class WC_Tests_Reports_Coupons extends WC_Unit_Test_Case {
$order_2c->calculate_totals();
$order_2c->save();
WC_Helper_Queue::run_all_pending();
$data_store = new WC_Admin_Reports_Coupons_Data_Store();
$start_time = date( 'Y-m-d 00:00:00', $order->get_date_created()->getOffsetTimestamp() );
$end_time = date( 'Y-m-d 23:59:59', $order->get_date_created()->getOffsetTimestamp() );

View File

@ -41,6 +41,8 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
)
);
WC_Helper_Queue::run_all_pending();
$data_store = new WC_Admin_Reports_Orders_Stats_Data_Store();
$start_time = date( 'Y-m-d H:00:00', $order->get_date_created()->getOffsetTimestamp() );
@ -142,6 +144,162 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
$this->assertEquals( $expected_stats, json_decode( json_encode( $query->get_data() ), true ) );
}
/**
* Test that querying statuses includes the default or query-specific statuses.
*/
public function test_populate_and_query_statuses() {
WC_Helper_Reports::reset_stats_dbs();
// Populate all of the data.
$product = new WC_Product_Simple();
$product->set_name( 'Test Product' );
$product->set_regular_price( 25 );
$product->save();
$order_types = array(
array(
'status' => 'refunded',
'total' => 50,
),
array(
'status' => 'completed',
'total' => 100,
),
array(
'status' => 'failed',
'total' => 75,
),
);
foreach ( $order_types as $order_type ) {
$order = WC_Helper_Order::create_order( 1, $product );
$order->set_status( $order_type['status'] );
$order->set_total( $order_type['total'] );
$order->set_shipping_total( 0 );
$order->set_cart_tax( 0 );
$order->save();
// Wait one second to avoid potentially ambiguous new/returning customer.
sleep( 1 );
}
WC_Helper_Queue::run_all_pending();
$data_store = new WC_Admin_Reports_Orders_Stats_Data_Store();
$start_time = date( 'Y-m-d H:00:00', $order->get_date_created()->getOffsetTimestamp() );
$end_time = date( 'Y-m-d H:59:59', $order->get_date_created()->getOffsetTimestamp() );
// Query default statuses that should not include excluded or refunded order statuses.
$args = array(
'interval' => 'hour',
'after' => $start_time,
'before' => $end_time,
);
$expected_stats = array(
'totals' => array(
'orders_count' => 1,
'num_items_sold' => 4,
'avg_items_per_order' => 4,
'avg_order_value' => 100,
'gross_revenue' => 100,
'coupons' => 0,
'refunds' => 0,
'taxes' => 0,
'shipping' => 0,
'net_revenue' => 100,
'num_returning_customers' => 1,
'num_new_customers' => 0,
'products' => 1,
'segments' => array(),
),
'intervals' => array(
array(
'interval' => date( 'Y-m-d H', $order->get_date_created()->getOffsetTimestamp() ),
'date_start' => $start_time,
'date_start_gmt' => $start_time,
'date_end' => $end_time,
'date_end_gmt' => $end_time,
'subtotals' => array(
'gross_revenue' => 100,
'net_revenue' => 100,
'coupons' => 0,
'shipping' => 0,
'taxes' => 0,
'refunds' => 0,
'orders_count' => 1,
'num_items_sold' => 4,
'avg_items_per_order' => 4,
'avg_order_value' => 100,
'num_returning_customers' => 1,
'num_new_customers' => 0,
'segments' => array(),
),
),
),
'total' => 1,
'pages' => 1,
'page_no' => 1,
);
$this->assertEquals( $expected_stats, json_decode( json_encode( $data_store->get_data( $args ) ), true ) );
// Query an excluded status which should still return orders with the queried status.
$args = array(
'interval' => 'hour',
'after' => $start_time,
'before' => $end_time,
'status_is' => array( 'failed' ),
);
$expected_stats = array(
'totals' => array(
'orders_count' => 1,
'num_items_sold' => 4,
'avg_items_per_order' => 4,
'avg_order_value' => 75,
'gross_revenue' => 75,
'coupons' => 0,
'refunds' => 0,
'taxes' => 0,
'shipping' => 0,
'net_revenue' => 75,
'num_returning_customers' => 1,
'num_new_customers' => 0,
'products' => 1,
'segments' => array(),
),
'intervals' => array(
array(
'interval' => date( 'Y-m-d H', $order->get_date_created()->getOffsetTimestamp() ),
'date_start' => $start_time,
'date_start_gmt' => $start_time,
'date_end' => $end_time,
'date_end_gmt' => $end_time,
'subtotals' => array(
'gross_revenue' => 75,
'net_revenue' => 75,
'coupons' => 0,
'shipping' => 0,
'taxes' => 0,
'refunds' => 0,
'orders_count' => 1,
'num_items_sold' => 4,
'avg_items_per_order' => 4,
'avg_order_value' => 75,
'num_returning_customers' => 1,
'num_new_customers' => 0,
'segments' => array(),
),
),
),
'total' => 1,
'pages' => 1,
'page_no' => 1,
);
$this->assertEquals( $expected_stats, json_decode( json_encode( $data_store->get_data( $args ) ), true ) );
}
/**
* Test the calculations and querying works correctly for the case of multiple orders.
*/
@ -330,6 +488,8 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
}
}
WC_Helper_Queue::run_all_pending();
$data_store = new WC_Admin_Reports_Orders_Stats_Data_Store();
// Tests for before & after set to current hour.
@ -1544,6 +1704,8 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
$returning_order->set_date_created( $order_1_time + 1 ); // This is guaranteed to belong to the same hour by the adjustment to $order_1_time.
$returning_order->save();
WC_Helper_Queue::run_all_pending();
$query_args = array(
'after' => $current_hour_start->format( WC_Admin_Reports_Interval::$sql_datetime_format ), // I don't think this makes sense.... date( 'Y-m-d H:i:s', $orders[0]->get_date_created()->getOffsetTimestamp() + 1 ), // Date after initial order to get a returning customer.
'before' => $current_hour_end->format( WC_Admin_Reports_Interval::$sql_datetime_format ),
@ -3171,6 +3333,8 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
$order_2->calculate_totals();
$order_2->save();
WC_Helper_Queue::run_all_pending();
$data_store = new WC_Admin_Reports_Orders_Stats_Data_Store();
// Tests for before & after set to current hour.

View File

@ -38,6 +38,8 @@ class WC_Tests_Reports_Products extends WC_Unit_Test_Case {
$order->set_total( 97 ); // $25x4 products + $10 shipping - $20 discount + $7 tax.
$order->save();
WC_Helper_Queue::run_all_pending();
$data_store = new WC_Admin_Reports_Products_Data_Store();
$start_time = date( 'Y-m-d H:00:00', $order->get_date_created()->getOffsetTimestamp() );
$end_time = date( 'Y-m-d H:00:00', $order->get_date_created()->getOffsetTimestamp() + HOUR_IN_SECONDS );
@ -113,6 +115,8 @@ class WC_Tests_Reports_Products extends WC_Unit_Test_Case {
$order_2->set_date_created( $date_created_2 );
$order_2->save();
WC_Helper_Queue::run_all_pending();
$data_store = new WC_Admin_Reports_Products_Data_Store();
$start_time = date( 'Y-m-d H:00:00', $order->get_date_created()->getOffsetTimestamp() );
$end_time = date( 'Y-m-d H:00:00', $order_2->get_date_created()->getOffsetTimestamp() + HOUR_IN_SECONDS );
@ -211,6 +215,9 @@ class WC_Tests_Reports_Products extends WC_Unit_Test_Case {
$order->set_shipping_tax( 2 );
$order->set_total( 97 ); // $25x4 products + $10 shipping - $20 discount + $7 tax.
$order->save();
WC_Helper_Queue::run_all_pending();
$data_store = new WC_Admin_Reports_Products_Data_Store();
$start_time = date( 'Y-m-d H:00:00', $order->get_date_created()->getOffsetTimestamp() );
$end_time = date( 'Y-m-d H:00:00', $order->get_date_created()->getOffsetTimestamp() + HOUR_IN_SECONDS );
@ -249,4 +256,76 @@ class WC_Tests_Reports_Products extends WC_Unit_Test_Case {
);
$this->assertEquals( $expected_data, $data );
}
/**
* Tests that line item refunds are reflected in product stats.
*/
public function test_populate_and_refund() {
WC_Helper_Reports::reset_stats_dbs();
// Populate all of the data.
$product = new WC_Product_Simple();
$product->set_name( 'Test Product' );
$product->set_regular_price( 25 );
$product->save();
$order = WC_Helper_Order::create_order( 1, $product );
$order->set_status( 'completed' );
$order->set_shipping_total( 10 );
$order->set_discount_total( 20 );
$order->set_discount_tax( 0 );
$order->set_cart_tax( 5 );
$order->set_shipping_tax( 2 );
$order->set_total( 97 ); // $25x4 products + $10 shipping - $20 discount + $7 tax.
$order->save();
foreach ( $order->get_items() as $item_key => $item_values ) {
$item_data = $item_values->get_data();
$refund = wc_create_refund(
array(
'amount' => 12,
'order_id' => $order->get_id(),
'line_items' => array(
$item_data['id'] => array(
'qty' => 1,
'refund_total' => 10,
),
),
)
);
break;
}
WC_Helper_Queue::run_all_pending();
$data_store = new WC_Admin_Reports_Products_Data_Store();
$start_time = date( 'Y-m-d H:00:00', $order->get_date_created()->getOffsetTimestamp() );
$end_time = date( 'Y-m-d H:00:00', $order->get_date_created()->getOffsetTimestamp() + HOUR_IN_SECONDS );
$args = array(
'after' => $start_time,
'before' => $end_time,
);
// Test retrieving the stats through the data store.
$data = $data_store->get_data( $args );
$expected_data = (object) array(
'total' => 1,
'pages' => 1,
'page_no' => 1,
'data' => array(
0 => array(
'product_id' => $product->get_id(),
'items_sold' => 3,
'net_revenue' => 90.0, // $25 * 4 - $10 refund.
'orders_count' => 1,
'extended_info' => new ArrayObject(),
),
),
);
$this->assertEquals( $expected_data, $data );
// Test retrieving the stats through the query class.
$query = new WC_Admin_Reports_Products_Query( $args );
$this->assertEquals( $expected_data, $query->get_data() );
}
}

View File

@ -37,6 +37,8 @@ class WC_Admin_Tests_Reports_Revenue_Stats extends WC_Unit_Test_Case {
$order->set_total( 97 ); // $25x4 products + $10 shipping - $20 discount + $7 tax.
$order->save();
WC_Helper_Queue::run_all_pending();
// /reports/revenue/stats is mapped to Orders_Data_Store.
$data_store = new WC_Admin_Reports_Orders_Stats_Data_Store();

View File

@ -39,6 +39,8 @@ class WC_Tests_Reports_Variations extends WC_Unit_Test_Case {
$order->set_status( 'completed' );
$order->save();
WC_Helper_Queue::run_all_pending();
$data_store = new WC_Admin_Reports_Variations_Data_Store();
$start_time = date( 'Y-m-d H:00:00', $order->get_date_created()->getOffsetTimestamp() );
$end_time = date( 'Y-m-d H:00:00', $order->get_date_created()->getOffsetTimestamp() + HOUR_IN_SECONDS );
@ -106,6 +108,8 @@ class WC_Tests_Reports_Variations extends WC_Unit_Test_Case {
$order->set_status( 'completed' );
$order->save();
WC_Helper_Queue::run_all_pending();
$data_store = new WC_Admin_Reports_Variations_Data_Store();
$start_time = date( 'Y-m-d H:00:00', $order->get_date_created()->getOffsetTimestamp() );
$end_time = date( 'Y-m-d H:00:00', $order->get_date_created()->getOffsetTimestamp() + HOUR_IN_SECONDS );