diff --git a/plugins/woocommerce-admin/client/analytics/report/customers/config.js b/plugins/woocommerce-admin/client/analytics/report/customers/config.js
index fe8bdc82b42..c201d7d291c 100644
--- a/plugins/woocommerce-admin/client/analytics/report/customers/config.js
+++ b/plugins/woocommerce-admin/client/analytics/report/customers/config.js
@@ -59,7 +59,7 @@ export const advancedFilters = {
type: 'customers',
getLabels: getRequestByIdString( NAMESPACE + '/customers', customer => ( {
id: customer.id,
- label: [ customer.first_name, customer.last_name ].filter( Boolean ).join( ' ' ),
+ label: customer.name,
} ) ),
},
},
diff --git a/plugins/woocommerce-admin/client/analytics/report/revenue/table.js b/plugins/woocommerce-admin/client/analytics/report/revenue/table.js
index e202237ebb0..4721e9d489d 100644
--- a/plugins/woocommerce-admin/client/analytics/report/revenue/table.js
+++ b/plugins/woocommerce-admin/client/analytics/report/revenue/table.js
@@ -240,8 +240,8 @@ export default compose(
return {
tableData: {
items: {
- data: get( revenueData, [ 'data', 'intervals' ] ),
- totalResults: get( revenueData, [ 'totalResults' ] ),
+ data: get( revenueData, [ 'data', 'intervals' ], [] ),
+ totalResults: get( revenueData, [ 'totalResults' ], 0 ),
},
isError,
isRequesting,
diff --git a/plugins/woocommerce-admin/client/analytics/report/stock/config.js b/plugins/woocommerce-admin/client/analytics/report/stock/config.js
index 43d01296c88..9760b9e4d40 100644
--- a/plugins/woocommerce-admin/client/analytics/report/stock/config.js
+++ b/plugins/woocommerce-admin/client/analytics/report/stock/config.js
@@ -17,6 +17,7 @@ export const filters = [
{ label: __( 'Out of Stock', 'wc-admin' ), value: 'outofstock' },
{ label: __( 'Low Stock', 'wc-admin' ), value: 'lowstock' },
{ label: __( 'In Stock', 'wc-admin' ), value: 'instock' },
+ { label: __( 'On Backorder', 'wc-admin' ), value: 'onbackorder' },
],
},
];
diff --git a/plugins/woocommerce-admin/client/analytics/report/stock/table.js b/plugins/woocommerce-admin/client/analytics/report/stock/table.js
index fff176fe4c3..06c4c44d87d 100644
--- a/plugins/woocommerce-admin/client/analytics/report/stock/table.js
+++ b/plugins/woocommerce-admin/client/analytics/report/stock/table.js
@@ -103,23 +103,27 @@ export default class StockReportTable extends Component {
}
getSummary( totals ) {
- const { products = 0, out_of_stock = 0, low_stock = 0, in_stock = 0 } = totals;
+ const { products = 0, outofstock = 0, lowstock = 0, instock = 0, onbackorder = 0 } = totals;
return [
{
label: _n( 'product', 'products', products, 'wc-admin' ),
value: numberFormat( products ),
},
{
- label: __( 'out of stock', out_of_stock, 'wc-admin' ),
- value: numberFormat( out_of_stock ),
+ label: __( 'out of stock', outofstock, 'wc-admin' ),
+ value: numberFormat( outofstock ),
},
{
- label: __( 'low stock', low_stock, 'wc-admin' ),
- value: numberFormat( low_stock ),
+ label: __( 'low stock', lowstock, 'wc-admin' ),
+ value: numberFormat( lowstock ),
},
{
- label: __( 'in stock', in_stock, 'wc-admin' ),
- value: numberFormat( in_stock ),
+ label: __( 'on backorder', onbackorder, 'wc-admin' ),
+ value: numberFormat( onbackorder ),
+ },
+ {
+ label: __( 'in stock', instock, 'wc-admin' ),
+ value: numberFormat( instock ),
},
];
}
@@ -132,7 +136,7 @@ export default class StockReportTable extends Component {
endpoint="stock"
getHeadersContent={ this.getHeadersContent }
getRowsContent={ this.getRowsContent }
- // getSummary={ this.getSummary }
+ getSummary={ this.getSummary }
query={ query }
tableQuery={ {
orderby: query.orderby || 'stock_status',
diff --git a/plugins/woocommerce-admin/client/header/index.js b/plugins/woocommerce-admin/client/header/index.js
index c8a9353f8f2..e0298532bd3 100644
--- a/plugins/woocommerce-admin/client/header/index.js
+++ b/plugins/woocommerce-admin/client/header/index.js
@@ -12,7 +12,7 @@ import PropTypes from 'prop-types';
/**
* WooCommerce dependencies
*/
-import { getNewPath, getPersistedQuery } from '@woocommerce/navigation';
+import { getNewPath } from '@woocommerce/navigation';
import { Link } from '@woocommerce/components';
/**
@@ -89,7 +89,7 @@ class Header extends Component {
{ _sections.map( ( section, i ) => {
const sectionPiece = Array.isArray( section ) ? (
{ section[ 1 ] }
diff --git a/plugins/woocommerce-admin/client/wc-api/constants.js b/plugins/woocommerce-admin/client/wc-api/constants.js
index 91513917478..596c9d3634c 100644
--- a/plugins/woocommerce-admin/client/wc-api/constants.js
+++ b/plugins/woocommerce-admin/client/wc-api/constants.js
@@ -11,7 +11,7 @@ export const SWAGGERNAMESPACE = 'https://virtserver.swaggerhub.com/peterfabian/w
export const DEFAULT_REQUIREMENT = {
timeout: 1 * MINUTE,
- freshness: 5 * MINUTE,
+ freshness: 30 * MINUTE,
};
// WordPress & WooCommerce both set a hard limit of 100 for the per_page parameter
diff --git a/plugins/woocommerce-admin/client/wc-api/reports/stats/operations.js b/plugins/woocommerce-admin/client/wc-api/reports/stats/operations.js
index 3ff20c03312..40b8b9525af 100644
--- a/plugins/woocommerce-admin/client/wc-api/reports/stats/operations.js
+++ b/plugins/woocommerce-admin/client/wc-api/reports/stats/operations.js
@@ -21,6 +21,7 @@ const statEndpoints = [
'orders',
'products',
'revenue',
+ 'stock',
'taxes',
'customers',
];
@@ -34,6 +35,7 @@ const typeEndpointMap = {
'report-stats-query-categories': 'categories',
'report-stats-query-downloads': 'downloads',
'report-stats-query-coupons': 'coupons',
+ 'report-stats-query-stock': 'stock',
'report-stats-query-taxes': 'taxes',
'report-stats-query-customers': 'customers',
};
diff --git a/plugins/woocommerce-admin/client/wc-api/reports/utils.js b/plugins/woocommerce-admin/client/wc-api/reports/utils.js
index da380a51a19..23a010a2169 100644
--- a/plugins/woocommerce-admin/client/wc-api/reports/utils.js
+++ b/plugins/woocommerce-admin/client/wc-api/reports/utils.js
@@ -3,7 +3,7 @@
/**
* External dependencies
*/
-import { find, forEach, isNull, get } from 'lodash';
+import { find, forEach, isNull, get, includes } from 'lodash';
import moment from 'moment';
/**
@@ -52,6 +52,9 @@ export function getFilterQuery( endpoint, query ) {
return {};
}
+// Some stats endpoints don't have interval data, so they can ignore after/before params and omit that part of the response.
+const noIntervalEndpoints = [ 'stock', 'customers' ];
+
/**
* Add timestamp to advanced filter parameters involving date. The api
* expects a timestamp for these values similar to `before` and `after`.
@@ -136,10 +139,11 @@ export function getQueryFromConfig( config, advancedFilters, query ) {
/**
* Returns true if a report object is empty.
*
- * @param {Object} report Report to check
+ * @param {Object} report Report to check
+ * @param {String} endpoint Endpoint slug
* @return {Boolean} True if report is data is empty.
*/
-export function isReportDataEmpty( report ) {
+export function isReportDataEmpty( report, endpoint ) {
if ( ! report ) {
return true;
}
@@ -149,7 +153,9 @@ export function isReportDataEmpty( report ) {
if ( ! report.data.totals || isNull( report.data.totals ) ) {
return true;
}
- if ( ! report.data.intervals || 0 === report.data.intervals.length ) {
+
+ const checkIntervals = ! includes( noIntervalEndpoints, endpoint );
+ if ( checkIntervals && ( ! report.data.intervals || 0 === report.data.intervals.length ) ) {
return true;
}
return false;
@@ -168,15 +174,19 @@ function getRequestQuery( endpoint, dataType, query ) {
const interval = getIntervalForQuery( query );
const filterQuery = getFilterQuery( endpoint, query );
const end = datesFromQuery[ dataType ].before;
- return {
- order: 'asc',
- interval,
- per_page: MAX_PER_PAGE,
- after: appendTimestamp( datesFromQuery[ dataType ].after, 'start' ),
- before: appendTimestamp( end, 'end' ),
- segmentby: query.segmentby,
- ...filterQuery,
- };
+
+ const noIntervals = includes( noIntervalEndpoints, endpoint );
+ return noIntervals
+ ? { ...filterQuery }
+ : {
+ order: 'asc',
+ interval,
+ per_page: MAX_PER_PAGE,
+ after: appendTimestamp( datesFromQuery[ dataType ].after, 'start' ),
+ before: appendTimestamp( end, 'end' ),
+ segmentby: query.segmentby,
+ ...filterQuery,
+ };
}
/**
@@ -250,7 +260,7 @@ export function getReportChartData( endpoint, dataType, query, select ) {
return { ...response, isRequesting: true };
} else if ( getReportStatsError( endpoint, requestQuery ) ) {
return { ...response, isError: true };
- } else if ( isReportDataEmpty( stats ) ) {
+ } else if ( isReportDataEmpty( stats, endpoint ) ) {
return { ...response, isEmpty: true };
}
diff --git a/plugins/woocommerce-admin/docs/components/analytics/report-chart.md b/plugins/woocommerce-admin/docs/components/analytics/report-chart.md
index efd3381b486..5184f564cba 100644
--- a/plugins/woocommerce-admin/docs/components/analytics/report-chart.md
+++ b/plugins/woocommerce-admin/docs/components/analytics/report-chart.md
@@ -38,9 +38,14 @@ Current path
### `primaryData`
-- **Required**
- Type: Object
-- Default: null
+- Default: `{
+ data: {
+ intervals: [],
+ },
+ isError: false,
+ isRequesting: false,
+}`
Primary data to display in the chart.
@@ -54,9 +59,14 @@ The query string represented in object form.
### `secondaryData`
-- **Required**
- Type: Object
-- Default: null
+- Default: `{
+ data: {
+ intervals: [],
+ },
+ isError: false,
+ isRequesting: false,
+}`
Secondary data to display in the chart.
diff --git a/plugins/woocommerce-admin/docs/components/analytics/report-summary.md b/plugins/woocommerce-admin/docs/components/analytics/report-summary.md
index 8dd54053744..98a1e587462 100644
--- a/plugins/woocommerce-admin/docs/components/analytics/report-summary.md
+++ b/plugins/woocommerce-admin/docs/components/analytics/report-summary.md
@@ -42,3 +42,17 @@ The query string represented in object form.
Properties of the selected chart.
+### `summaryData`
+
+- Type: Object
+- Default: `{
+ totals: {
+ primary: {},
+ secondary: {},
+ },
+ isError: false,
+ isRequesting: false,
+}`
+
+Data to display in the SummaryNumbers.
+
diff --git a/plugins/woocommerce-admin/docs/components/analytics/report-table.md b/plugins/woocommerce-admin/docs/components/analytics/report-table.md
index 4800980f346..01da3a6404e 100644
--- a/plugins/woocommerce-admin/docs/components/analytics/report-table.md
+++ b/plugins/woocommerce-admin/docs/components/analytics/report-table.md
@@ -68,9 +68,8 @@ The name of the property in the item object which contains the id.
### `primaryData`
-- **Required**
- Type: Object
-- Default: null
+- Default: `{}`
Primary data of that report. If it's not provided, it will be automatically
loaded via the provided `endpoint`.
@@ -78,7 +77,13 @@ loaded via the provided `endpoint`.
### `tableData`
- Type: Object
-- Default: `{}`
+- Default: `{
+ items: {
+ data: [],
+ totalResults: 0,
+ },
+ query: {},
+}`
Table data of that report. If it's not provided, it will be automatically
loaded via the provided `endpoint`.
diff --git a/plugins/woocommerce-admin/docs/components/packages/chart.md b/plugins/woocommerce-admin/docs/components/packages/chart.md
index 37930f55a2d..49ff3733b3d 100644
--- a/plugins/woocommerce-admin/docs/components/packages/chart.md
+++ b/plugins/woocommerce-admin/docs/components/packages/chart.md
@@ -13,6 +13,14 @@ Props
Allowed intervals to show in a dropdown.
+### `baseValue`
+
+- Type: Number
+- Default: `0`
+
+Base chart value. If no data value is different than the baseValue, the
+`emptyMessage` will be displayed if provided.
+
### `data`
- Type: Array
@@ -27,6 +35,14 @@ An array of data.
Format to parse dates into d3 time format
+### `emptyMessage`
+
+- Type: String
+- Default: null
+
+The message to be displayed if there is no data to render. If no message is provided,
+nothing will be displayed.
+
### `itemsLabel`
- Type: String
diff --git a/plugins/woocommerce-admin/docs/components/packages/search.md b/plugins/woocommerce-admin/docs/components/packages/search.md
index 45c13c3fa4c..bdbfe917791 100644
--- a/plugins/woocommerce-admin/docs/components/packages/search.md
+++ b/plugins/woocommerce-admin/docs/components/packages/search.md
@@ -7,6 +7,13 @@ A search box which autocompletes results while typing, allowing for the user to
Props
-----
+### `allowFreeTextSearch`
+
+- Type: Boolean
+- Default: `false`
+
+Render additional options in the autocompleter to allow free text entering depending on the type.
+
### `className`
- Type: String
@@ -54,6 +61,13 @@ search box.
Render tags inside input, otherwise render below input.
+### `showClearButton`
+
+- Type: Boolean
+- Default: `false`
+
+Render a 'Clear' button next to the input box to remove its contents.
+
### `staticResults`
- Type: Boolean
diff --git a/plugins/woocommerce-admin/docs/components/packages/table.md b/plugins/woocommerce-admin/docs/components/packages/table.md
index 47c5a9bb734..6dec0a0c757 100644
--- a/plugins/woocommerce-admin/docs/components/packages/table.md
+++ b/plugins/woocommerce-admin/docs/components/packages/table.md
@@ -132,13 +132,6 @@ The total number of rows to display per page.
The string to use as a query parameter when searching row items.
-### `searchParam`
-
-- Type: String
-- Default: null
-
-Url query parameter search function operates on
-
### `showMenu`
- Type: Boolean
diff --git a/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-customers-controller.php b/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-customers-controller.php
index 4e606822433..89f8464f46c 100644
--- a/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-customers-controller.php
+++ b/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-customers-controller.php
@@ -13,46 +13,14 @@ defined( 'ABSPATH' ) || exit;
* Customers controller.
*
* @package WooCommerce Admin/API
- * @extends WC_REST_Customers_Controller
+ * @extends WC_Admin_REST_Reports_Customers_Controller
*/
-class WC_Admin_REST_Customers_Controller extends WC_REST_Customers_Controller {
-
- // @todo Add support for guests here. See https://wp.me/p7bje6-1dM.
+class WC_Admin_REST_Customers_Controller extends WC_Admin_REST_Reports_Customers_Controller {
/**
- * Endpoint namespace.
+ * Route base.
*
* @var string
*/
- protected $namespace = 'wc/v4';
-
- /**
- * Searches emails by partial search instead of a strict match.
- * See "search parameters" under https://codex.wordpress.org/Class_Reference/WP_User_Query.
- *
- * @param array $prepared_args Prepared search filter args from the customer endpoint.
- * @param array $request Request/query arguments.
- * @return array
- */
- public static function update_search_filters( $prepared_args, $request ) {
- if ( ! empty( $request['email'] ) ) {
- $prepared_args['search'] = '*' . $prepared_args['search'] . '*';
- }
- return $prepared_args;
- }
-
- /**
- * Get the query params for collections.
- *
- * @return array
- */
- public function get_collection_params() {
- $params = parent::get_collection_params();
- // Allow partial email matches. Previously, this was of format 'email' which required a strict "test@example.com" format.
- // This, in combination with `update_search_filters` allows us to do partial searches.
- $params['email']['format'] = '';
- return $params;
- }
+ protected $rest_base = 'customers';
}
-
-add_filter( 'woocommerce_rest_customer_query', array( 'WC_Admin_REST_Customers_Controller', 'update_search_filters' ), 10, 2 );
diff --git a/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-customers-controller.php b/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-customers-controller.php
index f599cbe5455..0e959a708d7 100644
--- a/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-customers-controller.php
+++ b/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-customers-controller.php
@@ -46,7 +46,7 @@ class WC_Admin_REST_Reports_Customers_Controller extends WC_REST_Reports_Control
$args['order'] = $request['order'];
$args['orderby'] = $request['orderby'];
$args['match'] = $request['match'];
- $args['name'] = $request['name'];
+ $args['search'] = $request['search'];
$args['username'] = $request['username'];
$args['email'] = $request['email'];
$args['country'] = $request['country'];
@@ -60,6 +60,7 @@ class WC_Admin_REST_Reports_Customers_Controller extends WC_REST_Reports_Control
$args['avg_order_value_max'] = $request['avg_order_value_max'];
$args['last_order_before'] = $request['last_order_before'];
$args['last_order_after'] = $request['last_order_after'];
+ $args['customers'] = $request['customers'];
$between_params_numeric = array( 'orders_count', 'total_spend', 'avg_order_value' );
$normalized_params_numeric = WC_Admin_Reports_Interval::normalize_between_params( $request, $between_params_numeric, false );
@@ -172,7 +173,7 @@ class WC_Admin_REST_Reports_Customers_Controller extends WC_REST_Reports_Control
'title' => 'report_customers',
'type' => 'object',
'properties' => array(
- 'customer_id' => array(
+ 'id' => array(
'description' => __( 'Customer ID.', 'wc-admin' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
@@ -333,8 +334,8 @@ class WC_Admin_REST_Reports_Customers_Controller extends WC_REST_Reports_Control
),
'validate_callback' => 'rest_validate_request_arg',
);
- $params['name'] = array(
- 'description' => __( 'Limit response to objects with a specfic customer name.', 'wc-admin' ),
+ $params['search'] = array(
+ 'description' => __( 'Limit response to objects with a customer name containing the search term.', 'wc-admin' ),
'type' => 'string',
'validate_callback' => 'rest_validate_request_arg',
);
@@ -446,6 +447,17 @@ class WC_Admin_REST_Reports_Customers_Controller extends WC_REST_Reports_Control
'format' => 'date-time',
'validate_callback' => 'rest_validate_request_arg',
);
+ $params['customers'] = array(
+ 'description' => __( 'Limit result to items with specified customer ids.', 'wc-admin' ),
+ 'type' => 'array',
+ 'sanitize_callback' => 'wp_parse_id_list',
+ 'validate_callback' => 'rest_validate_request_arg',
+ 'items' => array(
+ 'type' => 'integer',
+ ),
+
+ );
+
return $params;
}
}
diff --git a/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-customers-stats-controller.php b/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-customers-stats-controller.php
index d3d24b01f3f..87cf651cb9e 100644
--- a/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-customers-stats-controller.php
+++ b/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-customers-stats-controller.php
@@ -41,7 +41,7 @@ class WC_Admin_REST_Reports_Customers_Stats_Controller extends WC_REST_Reports_C
$args['registered_before'] = $request['registered_before'];
$args['registered_after'] = $request['registered_after'];
$args['match'] = $request['match'];
- $args['name'] = $request['name'];
+ $args['search'] = $request['search'];
$args['username'] = $request['username'];
$args['email'] = $request['email'];
$args['country'] = $request['country'];
@@ -55,6 +55,7 @@ class WC_Admin_REST_Reports_Customers_Stats_Controller extends WC_REST_Reports_C
$args['avg_order_value_max'] = $request['avg_order_value_max'];
$args['last_order_before'] = $request['last_order_before'];
$args['last_order_after'] = $request['last_order_after'];
+ $args['customers'] = $request['customers'];
$between_params_numeric = array( 'orders_count', 'total_spend', 'avg_order_value' );
$normalized_params_numeric = WC_Admin_Reports_Interval::normalize_between_params( $request, $between_params_numeric, false );
@@ -77,8 +78,6 @@ class WC_Admin_REST_Reports_Customers_Stats_Controller extends WC_REST_Reports_C
$report_data = $customers_query->get_data();
$out_data = array(
'totals' => $report_data,
- // @todo Is this needed? the single element array tricks the isReportDataEmpty() selector.
- 'intervals' => array( (object) array() ),
);
return rest_ensure_response( $out_data );
@@ -161,55 +160,6 @@ class WC_Admin_REST_Reports_Customers_Stats_Controller extends WC_REST_Reports_C
'readonly' => true,
'properties' => $totals,
),
- 'intervals' => array( // @todo Remove this?
- 'description' => __( 'Reports data grouped by intervals.', 'wc-admin' ),
- 'type' => 'array',
- 'context' => array( 'view', 'edit' ),
- 'readonly' => true,
- 'items' => array(
- 'type' => 'object',
- 'properties' => array(
- 'interval' => array(
- 'description' => __( 'Type of interval.', 'wc-admin' ),
- 'type' => 'string',
- 'context' => array( 'view', 'edit' ),
- 'readonly' => true,
- 'enum' => array( 'day', 'week', 'month', 'year' ),
- ),
- 'date_start' => array(
- 'description' => __( "The date the report start, in the site's timezone.", 'wc-admin' ),
- 'type' => 'date-time',
- 'context' => array( 'view', 'edit' ),
- 'readonly' => true,
- ),
- 'date_start_gmt' => array(
- 'description' => __( 'The date the report start, as GMT.', 'wc-admin' ),
- 'type' => 'date-time',
- 'context' => array( 'view', 'edit' ),
- 'readonly' => true,
- ),
- 'date_end' => array(
- 'description' => __( "The date the report end, in the site's timezone.", 'wc-admin' ),
- 'type' => 'date-time',
- 'context' => array( 'view', 'edit' ),
- 'readonly' => true,
- ),
- 'date_end_gmt' => array(
- 'description' => __( 'The date the report end, as GMT.', 'wc-admin' ),
- 'type' => 'date-time',
- 'context' => array( 'view', 'edit' ),
- 'readonly' => true,
- ),
- 'subtotals' => array(
- 'description' => __( 'Interval subtotals.', 'wc-admin' ),
- 'type' => 'object',
- 'context' => array( 'view', 'edit' ),
- 'readonly' => true,
- 'properties' => $totals,
- ),
- ),
- ),
- ),
),
);
@@ -246,7 +196,7 @@ class WC_Admin_REST_Reports_Customers_Stats_Controller extends WC_REST_Reports_C
),
'validate_callback' => 'rest_validate_request_arg',
);
- $params['name'] = array(
+ $params['search'] = array(
'description' => __( 'Limit response to objects with a specfic customer name.', 'wc-admin' ),
'type' => 'string',
'validate_callback' => 'rest_validate_request_arg',
@@ -359,6 +309,16 @@ class WC_Admin_REST_Reports_Customers_Stats_Controller extends WC_REST_Reports_C
'format' => 'date-time',
'validate_callback' => 'rest_validate_request_arg',
);
+ $params['customers'] = array(
+ 'description' => __( 'Limit result to items with specified customer ids.', 'wc-admin' ),
+ 'type' => 'array',
+ 'sanitize_callback' => 'wp_parse_id_list',
+ 'validate_callback' => 'rest_validate_request_arg',
+ 'items' => array(
+ 'type' => 'integer',
+ ),
+
+ );
return $params;
}
diff --git a/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-orders-stats-controller.php b/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-orders-stats-controller.php
index 3fba1631a14..fd440dd9688 100644
--- a/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-orders-stats-controller.php
+++ b/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-orders-stats-controller.php
@@ -170,7 +170,7 @@ class WC_Admin_REST_Reports_Orders_Stats_Controller extends WC_Admin_REST_Report
),
'avg_items_per_order' => array(
'description' => __( 'Average items per order', 'wc-admin' ),
- 'type' => 'integer',
+ 'type' => 'number',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
diff --git a/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-stock-stats-controller.php b/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-stock-stats-controller.php
new file mode 100644
index 00000000000..960e4733766
--- /dev/null
+++ b/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-stock-stats-controller.php
@@ -0,0 +1,138 @@
+get_data();
+ $out_data = array(
+ 'totals' => $report_data,
+ );
+ return rest_ensure_response( $out_data );
+ }
+
+ /**
+ * Prepare a report object for serialization.
+ *
+ * @param WC_Product $report Report data.
+ * @param WP_REST_Request $request Request object.
+ * @return WP_REST_Response
+ */
+ public function prepare_item_for_response( $report, $request ) {
+ $data = $report;
+
+ $context = ! empty( $request['context'] ) ? $request['context'] : 'view';
+ $data = $this->add_additional_fields_to_object( $data, $request );
+ $data = $this->filter_response_by_context( $data, $context );
+
+ // Wrap the data in a response object.
+ $response = rest_ensure_response( $data );
+
+ /**
+ * Filter a report returned from the API.
+ *
+ * Allows modification of the report data right before it is returned.
+ *
+ * @param WP_REST_Response $response The response object.
+ * @param WC_Product $product The original bject.
+ * @param WP_REST_Request $request Request used to generate the response.
+ */
+ return apply_filters( 'woocommerce_rest_prepare_report_stock_stats', $response, $product, $request );
+ }
+
+ /**
+ * Get the Report's schema, conforming to JSON Schema.
+ *
+ * @return array
+ */
+ public function get_item_schema() {
+ $totals = array(
+ 'products' => array(
+ 'description' => __( 'Number of products.', 'wc-admin' ),
+ 'type' => 'integer',
+ 'context' => array( 'view', 'edit' ),
+ 'readonly' => true,
+ ),
+ 'lowstock' => array(
+ 'description' => __( 'Number of low stock products.', 'wc-admin' ),
+ 'type' => 'integer',
+ 'context' => array( 'view', 'edit' ),
+ 'readonly' => true,
+ ),
+ );
+
+ $status_options = wc_get_product_stock_status_options();
+ foreach ( $status_options as $status => $label ) {
+ $totals[ $status ] = array(
+ /* translators: Stock status. Example: "Number of low stock products */
+ 'description' => sprintf( __( 'Number of %s products.', 'wc-admin' ), $label ),
+ 'type' => 'integer',
+ 'context' => array( 'view', 'edit' ),
+ 'readonly' => true,
+ );
+ }
+
+ $schema = array(
+ '$schema' => 'http://json-schema.org/draft-04/schema#',
+ 'title' => 'report_customers_stats',
+ 'type' => 'object',
+ 'properties' => array(
+ 'totals' => array(
+ 'description' => __( 'Totals data.', 'wc-admin' ),
+ 'type' => 'object',
+ 'context' => array( 'view', 'edit' ),
+ 'readonly' => true,
+ 'properties' => $totals,
+ ),
+ ),
+ );
+
+ return $this->add_additional_fields_schema( $schema );
+ }
+
+ /**
+ * Get the query params for collections.
+ *
+ * @return array
+ */
+ public function get_collection_params() {
+ $params = array();
+ $params['context'] = $this->get_context_param( array( 'default' => 'view' ) );
+ return $params;
+ }
+}
diff --git a/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php b/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php
index 4986c133fd1..fa98436ed9a 100644
--- a/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php
+++ b/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php
@@ -68,6 +68,7 @@ class WC_Admin_Api_Init {
require_once dirname( __FILE__ ) . '/class-wc-admin-reports-downloads-stats-query.php';
require_once dirname( __FILE__ ) . '/class-wc-admin-reports-customers-query.php';
require_once dirname( __FILE__ ) . '/class-wc-admin-reports-customers-stats-query.php';
+ require_once dirname( __FILE__ ) . '/class-wc-admin-reports-stock-stats-query.php';
// Data stores.
require_once dirname( __FILE__ ) . '/data-stores/class-wc-admin-reports-data-store.php';
@@ -85,6 +86,7 @@ class WC_Admin_Api_Init {
require_once dirname( __FILE__ ) . '/data-stores/class-wc-admin-reports-downloads-stats-data-store.php';
require_once dirname( __FILE__ ) . '/data-stores/class-wc-admin-reports-customers-data-store.php';
require_once dirname( __FILE__ ) . '/data-stores/class-wc-admin-reports-customers-stats-data-store.php';
+ require_once dirname( __FILE__ ) . '/data-stores/class-wc-admin-reports-stock-stats-data-store.php';
// Data triggers.
require_once dirname( __FILE__ ) . '/data-stores/class-wc-admin-notes-data-store.php';
@@ -100,7 +102,6 @@ class WC_Admin_Api_Init {
public function rest_api_init() {
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-admin-notes-controller.php';
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-coupons-controller.php';
- require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-customers-controller.php';
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-data-controller.php';
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-data-countries-controller.php';
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-data-download-ips-controller.php';
@@ -131,7 +132,9 @@ class WC_Admin_Api_Init {
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-reports-taxes-controller.php';
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-reports-taxes-stats-controller.php';
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-reports-stock-controller.php';
+ require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-reports-stock-stats-controller.php';
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-taxes-controller.php';
+ require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-customers-controller.php';
$controllers = apply_filters(
'woocommerce_admin_rest_controllers',
@@ -163,6 +166,7 @@ class WC_Admin_Api_Init {
'WC_Admin_REST_Reports_Coupons_Controller',
'WC_Admin_REST_Reports_Coupons_Stats_Controller',
'WC_Admin_REST_Reports_Stock_Controller',
+ 'WC_Admin_REST_Reports_Stock_Stats_Controller',
'WC_Admin_REST_Reports_Downloads_Controller',
'WC_Admin_REST_Reports_Downloads_Stats_Controller',
'WC_Admin_REST_Reports_Customers_Controller',
@@ -366,22 +370,23 @@ class WC_Admin_Api_Init {
return array_merge(
$data_stores,
array(
- 'report-revenue-stats' => 'WC_Admin_Reports_Orders_Stats_Data_Store',
- 'report-orders' => 'WC_Admin_Reports_Orders_Data_Store',
- 'report-orders-stats' => 'WC_Admin_Reports_Orders_Stats_Data_Store',
- 'report-products' => 'WC_Admin_Reports_Products_Data_Store',
- 'report-variations' => 'WC_Admin_Reports_Variations_Data_Store',
- 'report-products-stats' => 'WC_Admin_Reports_Products_Stats_Data_Store',
- 'report-categories' => 'WC_Admin_Reports_Categories_Data_Store',
- 'report-taxes' => 'WC_Admin_Reports_Taxes_Data_Store',
- 'report-taxes-stats' => 'WC_Admin_Reports_Taxes_Stats_Data_Store',
- 'report-coupons' => 'WC_Admin_Reports_Coupons_Data_Store',
- 'report-coupons-stats' => 'WC_Admin_Reports_Coupons_Stats_Data_Store',
- 'report-downloads' => 'WC_Admin_Reports_Downloads_Data_Store',
+ 'report-revenue-stats' => 'WC_Admin_Reports_Orders_Stats_Data_Store',
+ 'report-orders' => 'WC_Admin_Reports_Orders_Data_Store',
+ 'report-orders-stats' => 'WC_Admin_Reports_Orders_Stats_Data_Store',
+ 'report-products' => 'WC_Admin_Reports_Products_Data_Store',
+ 'report-variations' => 'WC_Admin_Reports_Variations_Data_Store',
+ 'report-products-stats' => 'WC_Admin_Reports_Products_Stats_Data_Store',
+ 'report-categories' => 'WC_Admin_Reports_Categories_Data_Store',
+ 'report-taxes' => 'WC_Admin_Reports_Taxes_Data_Store',
+ 'report-taxes-stats' => 'WC_Admin_Reports_Taxes_Stats_Data_Store',
+ 'report-coupons' => 'WC_Admin_Reports_Coupons_Data_Store',
+ 'report-coupons-stats' => 'WC_Admin_Reports_Coupons_Stats_Data_Store',
+ 'report-downloads' => 'WC_Admin_Reports_Downloads_Data_Store',
'report-downloads-stats' => 'WC_Admin_Reports_Downloads_Stats_Data_Store',
'admin-note' => 'WC_Admin_Notes_Data_Store',
'report-customers' => 'WC_Admin_Reports_Customers_Data_Store',
'report-customers-stats' => 'WC_Admin_Reports_Customers_Stats_Data_Store',
+ 'report-stock-stats' => 'WC_Admin_Reports_Stock_Stats_Data_Store',
)
);
}
diff --git a/plugins/woocommerce-admin/includes/class-wc-admin-reports-stock-stats-query.php b/plugins/woocommerce-admin/includes/class-wc-admin-reports-stock-stats-query.php
new file mode 100644
index 00000000000..297c19ede46
--- /dev/null
+++ b/plugins/woocommerce-admin/includes/class-wc-admin-reports-stock-stats-query.php
@@ -0,0 +1,29 @@
+get_data();
+ *
+ * @package WooCommerce Admin/Classes
+ */
+
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * WC_Admin_Reports_Stock_Stats_Query
+ */
+class WC_Admin_Reports_Stock_Stats_Query extends WC_Admin_Reports_Query {
+
+ /**
+ * Get product data based on the current query vars.
+ *
+ * @return array
+ */
+ public function get_data() {
+ $data_store = WC_Data_Store::load( 'report-stock-stats' );
+ $results = $data_store->get_data();
+ return apply_filters( 'woocommerce_reports_stock_stats_query', $results );
+ }
+
+}
diff --git a/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-customers-data-store.php b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-customers-data-store.php
index 0e1f4b446f1..62cabec799d 100644
--- a/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-customers-data-store.php
+++ b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-customers-data-store.php
@@ -25,7 +25,7 @@ class WC_Admin_Reports_Customers_Data_Store extends WC_Admin_Reports_Data_Store
* @var array
*/
protected $column_types = array(
- 'customer_id' => 'intval',
+ 'id' => 'intval',
'user_id' => 'intval',
'orders_count' => 'intval',
'total_spend' => 'floatval',
@@ -38,7 +38,7 @@ class WC_Admin_Reports_Customers_Data_Store extends WC_Admin_Reports_Data_Store
* @var array
*/
protected $report_columns = array(
- 'customer_id' => 'customer_id',
+ 'id' => 'customer_id as id',
'user_id' => 'user_id',
'username' => 'username',
'name' => "CONCAT_WS( ' ', first_name, last_name ) as name", // @todo What does this mean for RTL?
@@ -60,7 +60,7 @@ class WC_Admin_Reports_Customers_Data_Store extends WC_Admin_Reports_Data_Store
global $wpdb;
// Initialize some report columns that need disambiguation.
- $this->report_columns['customer_id'] = $wpdb->prefix . self::TABLE_NAME . '.customer_id';
+ $this->report_columns['id'] = $wpdb->prefix . self::TABLE_NAME . '.customer_id as id';
$this->report_columns['date_last_order'] = "MAX( {$wpdb->prefix}wc_order_stats.date_created ) as date_last_order";
}
@@ -230,8 +230,15 @@ class WC_Admin_Reports_Customers_Data_Store extends WC_Admin_Reports_Data_Store
}
}
- if ( ! empty( $query_args['name'] ) ) {
- $where_clauses[] = $wpdb->prepare( "CONCAT_WS( ' ', first_name, last_name ) = %s", $query_args['name'] );
+ if ( ! empty( $query_args['search'] ) ) {
+ $name_like = '%' . $wpdb->esc_like( $query_args['search'] ) . '%';
+ $where_clauses[] = $wpdb->prepare( "CONCAT_WS( ' ', first_name, last_name ) LIKE %s", $name_like );
+ }
+
+ // Allow a list of customer IDs to be specified.
+ if ( ! empty( $query_args['customers'] ) ) {
+ $included_customers = implode( ',', $query_args['customers'] );
+ $where_clauses[] = "{$customer_lookup_table}.customer_id IN ({$included_customers})";
}
$numeric_params = array(
@@ -253,17 +260,18 @@ class WC_Admin_Reports_Customers_Data_Store extends WC_Admin_Reports_Data_Store
$subclauses = array();
$min_param = $numeric_param . '_min';
$max_param = $numeric_param . '_max';
+ $or_equal = isset( $query_args[ $min_param ] ) && isset( $query_args[ $max_param ] ) ? '=' : '';
if ( isset( $query_args[ $min_param ] ) ) {
$subclauses[] = $wpdb->prepare(
- "{$param_info['column']} >= {$param_info['format']}",
+ "{$param_info['column']} >{$or_equal} {$param_info['format']}",
$query_args[ $min_param ]
); // WPCS: unprepared SQL ok.
}
if ( isset( $query_args[ $max_param ] ) ) {
$subclauses[] = $wpdb->prepare(
- "{$param_info['column']} <= {$param_info['format']}",
+ "{$param_info['column']} <{$or_equal} {$param_info['format']}",
$query_args[ $max_param ]
); // WPCS: unprepared SQL ok.
}
@@ -516,6 +524,24 @@ class WC_Admin_Reports_Customers_Data_Store extends WC_Admin_Reports_Data_Store
return $customer_id ? (int) $customer_id : false;
}
+ /**
+ * Retrieve the oldest orders made by a customer.
+ *
+ * @param int $customer_id Customer ID.
+ * @return array Orders.
+ */
+ public static function get_oldest_orders( $customer_id ) {
+ global $wpdb;
+ $orders_table = $wpdb->prefix . 'wc_order_stats';
+
+ return $wpdb->get_results(
+ $wpdb->prepare(
+ "SELECT order_id, date_created FROM {$orders_table} WHERE customer_id = %d ORDER BY date_created, order_id ASC LIMIT 2",
+ $customer_id
+ )
+ ); // WPCS: unprepared SQL ok.
+ }
+
/**
* Update the database with customer data.
*
diff --git a/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-orders-stats-data-store.php b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-orders-stats-data-store.php
index 6ffaf2900d6..89d7d33a53f 100644
--- a/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-orders-stats-data-store.php
+++ b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-orders-stats-data-store.php
@@ -515,24 +515,57 @@ class WC_Admin_Reports_Orders_Stats_Data_Store extends WC_Admin_Reports_Data_Sto
* @return bool
*/
protected static function is_returning_customer( $order ) {
- global $wpdb;
- $customer_id = WC_Admin_Reports_Customers_Data_Store::get_customer_id_by_user_id( $order->get_user_id() );
- $orders_stats_table = $wpdb->prefix . self::TABLE_NAME;
+ $customer_id = WC_Admin_Reports_Customers_Data_Store::get_customer_id_by_user_id( $order->get_user_id() );
if ( ! $customer_id ) {
return false;
}
- $customer_orders = $wpdb->get_var(
+ $oldest_orders = WC_Admin_Reports_Customers_Data_Store::get_oldest_orders( $customer_id );
+
+ if ( empty( $oldest_orders ) ) {
+ return false;
+ }
+
+ $first_order = $oldest_orders[0];
+
+ // Order is older than previous first order.
+ if ( $order->get_date_created() < new WC_DateTime( $first_order->date_created ) ) {
+ self::set_customer_first_order( $customer_id, $order->get_id() );
+ return false;
+ }
+ // First order date has changed and next oldest is now the first order.
+ $second_order = isset( $oldest_orders[1] ) ? $oldest_orders[1] : false;
+ if (
+ (int) $order->get_id() === (int) $first_order->order_id &&
+ $order->get_date_created() > new WC_DateTime( $first_order->date_created ) &&
+ $second_order &&
+ new WC_DateTime( $second_order->date_created ) < $order->get_date_created()
+ ) {
+ self::set_customer_first_order( $customer_id, $second_order->order_id );
+ return true;
+ }
+
+ return (int) $order->get_id() !== (int) $first_order->order_id;
+ }
+
+ /**
+ * Set a customer's first order and all others to returning.
+ *
+ * @param int $customer_id Customer ID.
+ * @param int $order_id Order ID.
+ */
+ protected static function set_customer_first_order( $customer_id, $order_id ) {
+ global $wpdb;
+ $orders_stats_table = $wpdb->prefix . self::TABLE_NAME;
+
+ $wpdb->query(
$wpdb->prepare(
- "SELECT COUNT(*) FROM ${orders_stats_table} WHERE customer_id = %d AND date_created < %s AND order_id != %d",
- $customer_id,
- date( 'Y-m-d H:i:s', $order->get_date_created()->getTimestamp() ),
- $order->get_id()
+ "UPDATE ${orders_stats_table} SET returning_customer = CASE WHEN order_id = %d THEN false ELSE true END WHERE customer_id = %d",
+ $order_id,
+ $customer_id
)
);
-
- return $customer_orders >= 1;
}
/**
diff --git a/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-stock-stats-data-store.php b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-stock-stats-data-store.php
new file mode 100644
index 00000000000..f75c824f427
--- /dev/null
+++ b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-stock-stats-data-store.php
@@ -0,0 +1,138 @@
+get_low_stock_count();
+ set_transient( $low_stock_transient_name, $low_stock_count, $cache_expire );
+ }
+ $report_data['lowstock'] = $low_stock_count;
+
+ $status_options = wc_get_product_stock_status_options();
+ foreach ( $status_options as $status => $label ) {
+ $transient_name = 'wc_admin_stock_count_' . $status;
+ $count = get_transient( $transient_name );
+ if ( false === $count ) {
+ $count = $this->get_count( $status );
+ set_transient( $transient_name, $count, $cache_expire );
+ }
+ $report_data[ $status ] = $count;
+ }
+
+ $product_count_transient_name = 'wc_admin_product_count';
+ $product_count = get_transient( $product_count_transient_name );
+ if ( false === $product_count ) {
+ $product_count = $this->get_product_count();
+ set_transient( $product_count_transient_name, $product_count, $cache_expire );
+ }
+ $report_data['products'] = $product_count;
+ return $report_data;
+ }
+
+ /**
+ * Get low stock count.
+ *
+ * @return int Low stock count.
+ */
+ private function get_low_stock_count() {
+ $query_args = array();
+ $query_args['post_type'] = array( 'product', 'product_variation' );
+ $low_stock = absint( max( get_option( 'woocommerce_notify_low_stock_amount' ), 1 ) );
+ $no_stock = absint( max( get_option( 'woocommerce_notify_no_stock_amount' ), 0 ) );
+ $query_args['meta_query'] = array( // WPCS: slow query ok.
+ array(
+ 'key' => '_manage_stock',
+ 'value' => 'yes',
+ ),
+ array(
+ 'key' => '_stock',
+ 'value' => array( $no_stock, $low_stock ),
+ 'compare' => 'BETWEEN',
+ 'type' => 'NUMERIC',
+ ),
+ array(
+ 'key' => '_stock_status',
+ 'value' => 'instock',
+ ),
+ );
+
+ $query = new WP_Query();
+ $query->query( $query_args );
+ return intval( $query->found_posts );
+ }
+
+ /**
+ * Get count for the passed in stock status.
+ *
+ * @param string $status Status slug.
+ * @return int Count.
+ */
+ private function get_count( $status ) {
+ $query_args = array();
+ $query_args['post_type'] = array( 'product', 'product_variation' );
+ $query_args['meta_query'] = array( // WPCS: slow query ok.
+ array(
+ 'key' => '_stock_status',
+ 'value' => $status,
+ ),
+ );
+
+ $query = new WP_Query();
+ $query->query( $query_args );
+ return intval( $query->found_posts );
+ }
+
+ /**
+ * Get product count for the store.
+ *
+ * @return int Product count.
+ */
+ private function get_product_count() {
+ $query_args = array();
+ $query_args['post_type'] = array( 'product', 'product_variation' );
+ $query = new WP_Query();
+ $query->query( $query_args );
+ return intval( $query->found_posts );
+ }
+}
+
+/**
+ * Clear the count cache when products are added or updated, or when
+ * the no/low stock options are changed.
+ *
+ * @param int $id Post/product ID.
+ */
+function wc_admin_clear_stock_count_cache( $id ) {
+ delete_transient( 'wc_admin_stock_count_lowstock' );
+ delete_transient( 'wc_admin_product_count' );
+ $status_options = wc_get_product_stock_status_options();
+ foreach ( $status_options as $status => $label ) {
+ delete_transient( 'wc_admin_stock_count_' . $status );
+ }
+}
+
+add_action( 'woocommerce_update_product', 'wc_admin_clear_stock_count_cache' );
+add_action( 'woocommerce_new_product', 'wc_admin_clear_stock_count_cache' );
+add_action( 'update_option_woocommerce_notify_low_stock_amount', 'wc_admin_clear_stock_count_cache' );
+add_action( 'update_option_woocommerce_notify_no_stock_amount', 'wc_admin_clear_stock_count_cache' );
diff --git a/plugins/woocommerce-admin/package-lock.json b/plugins/woocommerce-admin/package-lock.json
index b59a99319e2..09bc7e31fb1 100644
--- a/plugins/woocommerce-admin/package-lock.json
+++ b/plugins/woocommerce-admin/package-lock.json
@@ -1,6 +1,6 @@
{
"name": "wc-admin",
- "version": "0.6.0",
+ "version": "0.7.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -8851,8 +8851,7 @@
},
"ansi-regex": {
"version": "2.1.1",
- "bundled": true,
- "optional": true
+ "bundled": true
},
"aproba": {
"version": "1.2.0",
@@ -8870,13 +8869,11 @@
},
"balanced-match": {
"version": "1.0.0",
- "bundled": true,
- "optional": true
+ "bundled": true
},
"brace-expansion": {
"version": "1.1.11",
"bundled": true,
- "optional": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@@ -8889,18 +8886,15 @@
},
"code-point-at": {
"version": "1.1.0",
- "bundled": true,
- "optional": true
+ "bundled": true
},
"concat-map": {
"version": "0.0.1",
- "bundled": true,
- "optional": true
+ "bundled": true
},
"console-control-strings": {
"version": "1.1.0",
- "bundled": true,
- "optional": true
+ "bundled": true
},
"core-util-is": {
"version": "1.0.2",
@@ -9003,8 +8997,7 @@
},
"inherits": {
"version": "2.0.3",
- "bundled": true,
- "optional": true
+ "bundled": true
},
"ini": {
"version": "1.3.5",
@@ -9014,7 +9007,6 @@
"is-fullwidth-code-point": {
"version": "1.0.0",
"bundled": true,
- "optional": true,
"requires": {
"number-is-nan": "^1.0.0"
}
@@ -9027,20 +9019,17 @@
"minimatch": {
"version": "3.0.4",
"bundled": true,
- "optional": true,
"requires": {
"brace-expansion": "^1.1.7"
}
},
"minimist": {
"version": "0.0.8",
- "bundled": true,
- "optional": true
+ "bundled": true
},
"minipass": {
"version": "2.3.5",
"bundled": true,
- "optional": true,
"requires": {
"safe-buffer": "^5.1.2",
"yallist": "^3.0.0"
@@ -9057,7 +9046,6 @@
"mkdirp": {
"version": "0.5.1",
"bundled": true,
- "optional": true,
"requires": {
"minimist": "0.0.8"
}
@@ -9130,8 +9118,7 @@
},
"number-is-nan": {
"version": "1.0.1",
- "bundled": true,
- "optional": true
+ "bundled": true
},
"object-assign": {
"version": "4.1.1",
@@ -9141,7 +9128,6 @@
"once": {
"version": "1.4.0",
"bundled": true,
- "optional": true,
"requires": {
"wrappy": "1"
}
@@ -9217,8 +9203,7 @@
},
"safe-buffer": {
"version": "5.1.2",
- "bundled": true,
- "optional": true
+ "bundled": true
},
"safer-buffer": {
"version": "2.1.2",
@@ -9248,7 +9233,6 @@
"string-width": {
"version": "1.0.2",
"bundled": true,
- "optional": true,
"requires": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",
@@ -9266,7 +9250,6 @@
"strip-ansi": {
"version": "3.0.1",
"bundled": true,
- "optional": true,
"requires": {
"ansi-regex": "^2.0.0"
}
@@ -9305,13 +9288,11 @@
},
"wrappy": {
"version": "1.0.2",
- "bundled": true,
- "optional": true
+ "bundled": true
},
"yallist": {
"version": "3.0.3",
- "bundled": true,
- "optional": true
+ "bundled": true
}
}
},
diff --git a/plugins/woocommerce-admin/package.json b/plugins/woocommerce-admin/package.json
index e4340ee70b1..33f5820113e 100644
--- a/plugins/woocommerce-admin/package.json
+++ b/plugins/woocommerce-admin/package.json
@@ -1,6 +1,6 @@
{
"name": "wc-admin",
- "version": "0.6.0",
+ "version": "0.7.0",
"main": "js/index.js",
"author": "Automattic",
"license": "GPL-2.0-or-later",
@@ -16,7 +16,7 @@
"prebuild": "npm run -s install-if-deps-outdated",
"build:packages": "node ./bin/packages/build.js",
"build:core": "cross-env NODE_ENV=production webpack",
- "build": "npm run build:packages && npm run build:core",
+ "build": "npm run build:feature-config && npm run build:packages && npm run build:core",
"build:release": "cross-env WC_ADMIN_PHASE=plugin ./bin/build-plugin-zip.sh",
"build:feature-config": "php bin/generate-feature-config.php",
"postbuild": "npm run -s i18n:php && npm run -s i18n:pot",
diff --git a/plugins/woocommerce-admin/packages/components/src/chart/d3chart/chart.js b/plugins/woocommerce-admin/packages/components/src/chart/d3chart/chart.js
index 53eb6a14936..73a2486d700 100644
--- a/plugins/woocommerce-admin/packages/components/src/chart/d3chart/chart.js
+++ b/plugins/woocommerce-admin/packages/components/src/chart/d3chart/chart.js
@@ -6,18 +6,14 @@
import { Component, createRef } from '@wordpress/element';
import PropTypes from 'prop-types';
import classNames from 'classnames';
-import { timeFormat as d3TimeFormat, utcParse as d3UTCParse } from 'd3-time-format';
+import { timeFormat as d3TimeFormat } from 'd3-time-format';
/**
* Internal dependencies
*/
import D3Base from './d3base';
import {
- getDateSpaces,
getOrderedKeys,
- getLine,
- getLineData,
- getUniqueKeys,
getUniqueDates,
getFormatter,
isDataEmpty,
@@ -28,11 +24,11 @@ import {
getXLineScale,
getYMax,
getYScale,
- getYTickOffset,
} from './utils/scales';
-import { drawAxis, getXTicks } from './utils/axis';
+import { drawAxis } from './utils/axis';
import { drawBars } from './utils/bar-chart';
import { drawLines } from './utils/line-chart';
+import ChartTooltip from './utils/tooltip';
/**
* A simple D3 line and bar chart component for timeseries data in React.
@@ -45,28 +41,89 @@ class D3Chart extends Component {
this.tooltipRef = createRef();
}
+ getFormatParams() {
+ const { xFormat, x2Format, yFormat } = this.props;
+
+ return {
+ xFormat: getFormatter( xFormat, d3TimeFormat ),
+ x2Format: getFormatter( x2Format, d3TimeFormat ),
+ yFormat: getFormatter( yFormat ),
+ };
+ }
+
+ getScaleParams( uniqueDates ) {
+ const { data, height, margin, orderedKeys, chartType } = this.props;
+
+ const adjHeight = height - margin.top - margin.bottom;
+ const adjWidth = this.getWidth() - margin.left - margin.right;
+ const yMax = getYMax( data );
+ const yScale = getYScale( adjHeight, yMax );
+
+ if ( chartType === 'line' ) {
+ return {
+ xScale: getXLineScale( uniqueDates, adjWidth ),
+ yMax,
+ yScale,
+ };
+ }
+
+ const compact = this.shouldBeCompact();
+ const xScale = getXScale( uniqueDates, adjWidth, compact );
+
+ return {
+ xGroupScale: getXGroupScale( orderedKeys, xScale, compact ),
+ xScale,
+ yMax,
+ yScale,
+ };
+ }
+
+ getParams( uniqueDates ) {
+ const { colorScheme, data, interval, mode, orderedKeys, chartType } = this.props;
+ const newOrderedKeys = orderedKeys || getOrderedKeys( data );
+
+ return {
+ colorScheme,
+ interval,
+ mode,
+ chartType,
+ uniqueDates,
+ visibleKeys: newOrderedKeys.filter( key => key.visible ),
+ };
+ }
+
+ createTooltip( chart, visibleKeys ) {
+ const { colorScheme, tooltipLabelFormat, tooltipPosition, tooltipTitle, tooltipValueFormat } = this.props;
+
+ const tooltip = new ChartTooltip();
+ tooltip.ref = this.tooltipRef.current;
+ tooltip.chart = chart;
+ tooltip.position = tooltipPosition;
+ tooltip.title = tooltipTitle;
+ tooltip.labelFormat = getFormatter( tooltipLabelFormat, d3TimeFormat );
+ tooltip.valueFormat = getFormatter( tooltipValueFormat );
+ tooltip.visibleKeys = visibleKeys;
+ tooltip.colorScheme = colorScheme;
+ this.tooltip = tooltip;
+ }
+
drawChart( node ) {
- const { data, margin, chartType } = this.props;
- const params = this.getParams();
- const adjParams = Object.assign( {}, params, {
- height: params.adjHeight,
- width: params.adjWidth,
- tooltip: this.tooltipRef.current,
- valueType: params.valueType,
- } );
+ const { data, dateParser, margin, chartType } = this.props;
+ const uniqueDates = getUniqueDates( data, dateParser );
+ const formats = this.getFormatParams();
+ const params = this.getParams( uniqueDates );
+ const scales = this.getScaleParams( uniqueDates );
const g = node
.attr( 'id', 'chart' )
.append( 'g' )
- .attr( 'transform', `translate(${ margin.left },${ margin.top })` );
+ .attr( 'transform', `translate(${ margin.left }, ${ margin.top })` );
- const xOffset = chartType === 'line' && adjParams.uniqueDates.length <= 1
- ? adjParams.width / 2
- : 0;
+ this.createTooltip( g.node(), params.visibleKeys );
- drawAxis( g, adjParams, xOffset );
- chartType === 'line' && drawLines( g, data, adjParams, xOffset );
- chartType === 'bar' && drawBars( g, data, adjParams );
+ drawAxis( g, params, scales, formats, margin );
+ chartType === 'line' && drawLines( g, data, params, scales, formats, this.tooltip );
+ chartType === 'bar' && drawBars( g, data, params, scales, formats, this.tooltip );
}
shouldBeCompact() {
@@ -92,75 +149,6 @@ class D3Chart extends Component {
return Math.max( width, minimumWidth + margin.left + margin.right );
}
- getParams() {
- const {
- colorScheme,
- data,
- dateParser,
- height,
- interval,
- margin,
- mode,
- orderedKeys,
- tooltipPosition,
- tooltipLabelFormat,
- tooltipValueFormat,
- tooltipTitle,
- chartType,
- xFormat,
- x2Format,
- yFormat,
- valueType,
- } = this.props;
- const adjHeight = height - margin.top - margin.bottom;
- const adjWidth = this.getWidth() - margin.left - margin.right;
- const compact = this.shouldBeCompact();
- const uniqueKeys = getUniqueKeys( data );
- const newOrderedKeys = orderedKeys || getOrderedKeys( data, uniqueKeys );
- const visibleKeys = newOrderedKeys.filter( key => key.visible );
- const lineData = getLineData( data, newOrderedKeys );
- const yMax = getYMax( lineData );
- const yScale = getYScale( adjHeight, yMax );
- const parseDate = d3UTCParse( dateParser );
- const uniqueDates = getUniqueDates( lineData, parseDate );
- const xLineScale = getXLineScale( uniqueDates, adjWidth );
- const xScale = getXScale( uniqueDates, adjWidth, compact );
- const xTicks = getXTicks( uniqueDates, adjWidth, mode, interval );
-
- return {
- adjHeight,
- adjWidth,
- colorScheme,
- dateSpaces: getDateSpaces( data, uniqueDates, adjWidth, xLineScale ),
- interval,
- line: getLine( xLineScale, yScale ),
- lineData,
- margin,
- mode,
- orderedKeys: newOrderedKeys,
- visibleKeys,
- parseDate,
- tooltipPosition,
- tooltipLabelFormat: getFormatter( tooltipLabelFormat, d3TimeFormat ),
- tooltipValueFormat: getFormatter( tooltipValueFormat ),
- tooltipTitle,
- chartType,
- uniqueDates,
- uniqueKeys,
- valueType,
- xFormat: getFormatter( xFormat, d3TimeFormat ),
- x2Format: getFormatter( x2Format, d3TimeFormat ),
- xGroupScale: getXGroupScale( orderedKeys, xScale, compact ),
- xLineScale,
- xTicks,
- xScale,
- yMax,
- yScale,
- yTickOffset: getYTickOffset( adjHeight, yMax ),
- yFormat: getFormatter( yFormat ),
- };
- }
-
getEmptyMessage() {
const { baseValue, data, emptyMessage } = this.props;
@@ -172,7 +160,7 @@ class D3Chart extends Component {
}
render() {
- const { className, data, height, chartType } = this.props;
+ const { className, data, height, orderedKeys, chartType } = this.props;
const computedWidth = this.getWidth();
return (
diff --git a/plugins/woocommerce-admin/packages/components/src/chart/d3chart/d3base/index.js b/plugins/woocommerce-admin/packages/components/src/chart/d3chart/d3base/index.js
index 78abe142c42..566017f70bc 100644
--- a/plugins/woocommerce-admin/packages/components/src/chart/d3chart/d3base/index.js
+++ b/plugins/woocommerce-admin/packages/components/src/chart/d3chart/d3base/index.js
@@ -9,11 +9,6 @@ import { Component, createRef } from '@wordpress/element';
import { isEqual, throttle } from 'lodash';
import { select as d3Select } from 'd3-selection';
-/**
- * Internal dependencies
- */
-import { hideTooltip } from '../utils/tooltip';
-
/**
* Provides foundation to use D3 within React.
*
@@ -30,10 +25,6 @@ export default class D3Base extends Component {
super( props );
this.chartRef = createRef();
-
- this.delayedScroll = throttle( () => {
- hideTooltip( this.chartRef.current, props.tooltipRef.current );
- }, 300 );
}
componentDidMount() {
@@ -60,6 +51,13 @@ export default class D3Base extends Component {
this.deleteChart();
}
+ delayedScroll() {
+ const { tooltip } = this.props;
+ return throttle( () => {
+ tooltip && tooltip.hide();
+ }, 300 );
+ }
+
deleteChart() {
d3Select( this.chartRef.current )
.selectAll( 'svg' )
@@ -95,8 +93,9 @@ export default class D3Base extends Component {
}
render() {
+ const { className } = this.props;
return (
-
+
);
}
}
@@ -105,5 +104,6 @@ D3Base.propTypes = {
className: PropTypes.string,
data: PropTypes.array,
orderedKeys: PropTypes.array, // required to detect changes in data
+ tooltip: PropTypes.object,
chartType: PropTypes.string,
};
diff --git a/plugins/woocommerce-admin/packages/components/src/chart/d3chart/style.scss b/plugins/woocommerce-admin/packages/components/src/chart/d3chart/style.scss
index fd9090c1801..b295155f7d1 100644
--- a/plugins/woocommerce-admin/packages/components/src/chart/d3chart/style.scss
+++ b/plugins/woocommerce-admin/packages/components/src/chart/d3chart/style.scss
@@ -10,6 +10,7 @@
.d3-chart__container {
position: relative;
+ width: 100%;
svg {
overflow: visible;
@@ -157,8 +158,12 @@
.focus-grid {
line {
- stroke: $core-grey-light-700;
+ stroke: rgba( 0, 0, 0, 0.1 );
stroke-width: 1px;
}
}
+
+ .barfocus {
+ fill: rgba( 0, 0, 0, 0.1 );
+ }
}
diff --git a/plugins/woocommerce-admin/packages/components/src/chart/d3chart/utils/axis.js b/plugins/woocommerce-admin/packages/components/src/chart/d3chart/utils/axis.js
index 13939e97849..6d684b4c40c 100644
--- a/plugins/woocommerce-admin/packages/components/src/chart/d3chart/utils/axis.js
+++ b/plugins/woocommerce-admin/packages/components/src/chart/d3chart/utils/axis.js
@@ -22,7 +22,7 @@ const mostPoints = 31;
*/
const getFactors = inputNum => {
const numFactors = [];
- for ( let i = 1; i <= Math.floor( Math.sqrt( inputNum ) ); i += 1 ) {
+ for ( let i = 1; i <= Math.floor( Math.sqrt( inputNum ) ); i++ ) {
if ( inputNum % i === 0 ) {
numFactors.push( i );
inputNum / i !== i && numFactors.push( inputNum / i );
@@ -176,11 +176,6 @@ export const compareStrings = ( s1, s2, splitChar = new RegExp( [ ' |,' ], 'g' )
export const getYGrids = ( yMax ) => {
const yGrids = [];
- // If all values are 0, yMax can become NaN.
- if ( isNaN( yMax ) ) {
- return null;
- }
-
for ( let i = 0; i < 4; i++ ) {
const value = yMax > 1 ? Math.round( i / 3 * yMax ) : i / 3 * yMax;
if ( yGrids[ yGrids.length - 1 ] !== value ) {
@@ -191,87 +186,90 @@ export const getYGrids = ( yMax ) => {
return yGrids;
};
-export const drawAxis = ( node, params, xOffset ) => {
- const xScale = params.chartType === 'line' ? params.xLineScale : params.xScale;
- const removeDuplicateDates = ( d, i, ticks, formatter ) => {
- const monthDate = moment( d ).toDate();
- let prevMonth = i !== 0 ? ticks[ i - 1 ] : ticks[ i ];
- prevMonth = prevMonth instanceof Date ? prevMonth : moment( prevMonth ).toDate();
- return i === 0
- ? formatter( monthDate )
- : compareStrings( formatter( prevMonth ), formatter( monthDate ) ).join( ' ' );
- };
+const removeDuplicateDates = ( d, i, ticks, formatter ) => {
+ const monthDate = moment( d ).toDate();
+ let prevMonth = i !== 0 ? ticks[ i - 1 ] : ticks[ i ];
+ prevMonth = prevMonth instanceof Date ? prevMonth : moment( prevMonth ).toDate();
+ return i === 0
+ ? formatter( monthDate )
+ : compareStrings( formatter( prevMonth ), formatter( monthDate ) ).join( ' ' );
+};
- const yGrids = getYGrids( params.yMax === 0 ? 1 : params.yMax );
-
- const ticks = params.xTicks.map( d => ( params.chartType === 'line' ? moment( d ).toDate() : d ) );
+const drawXAxis = ( node, params, scales, formats ) => {
+ const height = scales.yScale.range()[ 0 ];
+ let ticks = getXTicks( params.uniqueDates, scales.xScale.range()[ 1 ], params.mode, params.interval );
+ if ( params.type === 'line' ) {
+ ticks = ticks.map( d => moment( d ).toDate() );
+ }
node
.append( 'g' )
.attr( 'class', 'axis' )
.attr( 'aria-hidden', 'true' )
- .attr( 'transform', `translate(${ xOffset }, ${ params.height })` )
+ .attr( 'transform', `translate(0, ${ height })` )
.call(
- d3AxisBottom( xScale )
+ d3AxisBottom( scales.xScale )
.tickValues( ticks )
.tickFormat( ( d, i ) => params.interval === 'hour'
- ? params.xFormat( d instanceof Date ? d : moment( d ).toDate() )
- : removeDuplicateDates( d, i, ticks, params.xFormat ) )
+ ? formats.xFormat( d instanceof Date ? d : moment( d ).toDate() )
+ : removeDuplicateDates( d, i, ticks, formats.xFormat ) )
);
node
.append( 'g' )
.attr( 'class', 'axis axis-month' )
.attr( 'aria-hidden', 'true' )
- .attr( 'transform', `translate(${ xOffset }, ${ params.height + 20 })` )
+ .attr( 'transform', `translate(0, ${ height + 14 })` )
.call(
- d3AxisBottom( xScale )
+ d3AxisBottom( scales.xScale )
.tickValues( ticks )
- .tickFormat( ( d, i ) => removeDuplicateDates( d, i, ticks, params.x2Format ) )
- )
- .call( g => g.select( '.domain' ).remove() );
+ .tickFormat( ( d, i ) => removeDuplicateDates( d, i, ticks, formats.x2Format ) )
+ );
node
.append( 'g' )
.attr( 'class', 'pipes' )
- .attr( 'transform', `translate(${ xOffset }, ${ params.height })` )
+ .attr( 'transform', `translate(0, ${ height })` )
.call(
- d3AxisBottom( xScale )
+ d3AxisBottom( scales.xScale )
.tickValues( ticks )
.tickSize( 5 )
.tickFormat( '' )
);
+};
- if ( yGrids ) {
- node
- .append( 'g' )
- .attr( 'class', 'grid' )
- .attr( 'transform', `translate(-${ params.margin.left }, 0)` )
- .call(
- d3AxisLeft( params.yScale )
- .tickValues( yGrids )
- .tickSize( -params.width - params.margin.left - params.margin.right )
- .tickFormat( '' )
- )
- .call( g => g.select( '.domain' ).remove() );
+const drawYAxis = ( node, scales, formats, margin ) => {
+ const yGrids = getYGrids( scales.yScale.domain()[ 1 ] );
+ const width = scales.xScale.range()[ 1 ];
- node
- .append( 'g' )
- .attr( 'class', 'axis y-axis' )
- .attr( 'aria-hidden', 'true' )
- .attr( 'transform', 'translate(-50, 0)' )
- .attr( 'text-anchor', 'start' )
- .call(
- d3AxisLeft( params.yTickOffset )
- .tickValues( params.yMax === 0 ? [ yGrids[ 0 ] ] : yGrids )
- .tickFormat( d => params.yFormat( d !== 0 ? d : 0 ) )
- );
- }
+ node
+ .append( 'g' )
+ .attr( 'class', 'grid' )
+ .attr( 'transform', `translate(-${ margin.left }, 0)` )
+ .call(
+ d3AxisLeft( scales.yScale )
+ .tickValues( yGrids )
+ .tickSize( -width - margin.left - margin.right )
+ .tickFormat( '' )
+ );
+
+ node
+ .append( 'g' )
+ .attr( 'class', 'axis y-axis' )
+ .attr( 'aria-hidden', 'true' )
+ .attr( 'transform', 'translate(-50, 12)' )
+ .attr( 'text-anchor', 'start' )
+ .call(
+ d3AxisLeft( scales.yScale )
+ .tickValues( scales.yMax === 0 ? [ yGrids[ 0 ] ] : yGrids )
+ .tickFormat( d => formats.yFormat( d !== 0 ? d : 0 ) )
+ );
+};
+
+export const drawAxis = ( node, params, scales, formats, margin ) => {
+ drawXAxis( node, params, scales, formats );
+ drawYAxis( node, scales, formats, margin );
node.selectAll( '.domain' ).remove();
- node
- .selectAll( '.axis' )
- .selectAll( '.tick' )
- .select( 'line' )
- .remove();
+ node.selectAll( '.axis .tick line' ).remove();
};
diff --git a/plugins/woocommerce-admin/packages/components/src/chart/d3chart/utils/bar-chart.js b/plugins/woocommerce-admin/packages/components/src/chart/d3chart/utils/bar-chart.js
index dcfe1e49088..e1c1448640a 100644
--- a/plugins/woocommerce-admin/packages/components/src/chart/d3chart/utils/bar-chart.js
+++ b/plugins/woocommerce-admin/packages/components/src/chart/d3chart/utils/bar-chart.js
@@ -4,23 +4,16 @@
* External dependencies
*/
import { get } from 'lodash';
-import { event as d3Event, select as d3Select } from 'd3-selection';
+import { event as d3Event } from 'd3-selection';
import moment from 'moment';
/**
* Internal dependencies
*/
import { getColor } from './color';
-import { calculateTooltipPosition, hideTooltip, showTooltip } from './tooltip';
-const handleMouseOverBarChart = ( date, parentNode, node, data, params, position ) => {
- d3Select( parentNode )
- .select( '.barfocus' )
- .attr( 'opacity', '0.1' );
- showTooltip( params, data.find( e => e.date === date ), position );
-};
-
-export const drawBars = ( node, data, params ) => {
+export const drawBars = ( node, data, params, scales, formats, tooltip ) => {
+ const height = scales.yScale.range()[ 0 ];
const barGroup = node
.append( 'g' )
.attr( 'class', 'bars' )
@@ -28,14 +21,14 @@ export const drawBars = ( node, data, params ) => {
.data( data )
.enter()
.append( 'g' )
- .attr( 'transform', d => `translate(${ params.xScale( d.date ) },0)` )
+ .attr( 'transform', d => `translate(${ scales.xScale( d.date ) }, 0)` )
.attr( 'class', 'bargroup' )
.attr( 'role', 'region' )
.attr(
'aria-label',
d =>
params.mode === 'item-comparison'
- ? params.tooltipLabelFormat( d.date instanceof Date ? d.date : moment( d.date ).toDate() )
+ ? tooltip.labelFormat( d.date instanceof Date ? d.date : moment( d.date ).toDate() )
: null
);
@@ -44,23 +37,18 @@ export const drawBars = ( node, data, params ) => {
.attr( 'class', 'barfocus' )
.attr( 'x', 0 )
.attr( 'y', 0 )
- .attr( 'width', params.xGroupScale.range()[ 1 ] )
- .attr( 'height', params.height )
+ .attr( 'width', scales.xGroupScale.range()[ 1 ] )
+ .attr( 'height', height )
.attr( 'opacity', '0' )
.on( 'mouseover', ( d, i, nodes ) => {
- const position = calculateTooltipPosition(
- d3Event.target,
- node.node(),
- params.tooltipPosition
- );
- handleMouseOverBarChart( d.date, nodes[ i ].parentNode, node, data, params, position );
+ tooltip.show( data.find( e => e.date === d.date ), d3Event.target, nodes[ i ].parentNode );
} )
- .on( 'mouseout', ( d, i, nodes ) => hideTooltip( nodes[ i ].parentNode, params.tooltip ) );
+ .on( 'mouseout', () => tooltip.hide() );
barGroup
.selectAll( '.bar' )
.data( d =>
- params.orderedKeys.filter( row => row.visible ).map( row => ( {
+ params.visibleKeys.map( row => ( {
key: row.key,
focus: row.focus,
value: get( d, [ row.key, 'value' ], 0 ),
@@ -72,16 +60,16 @@ export const drawBars = ( node, data, params ) => {
.enter()
.append( 'rect' )
.attr( 'class', 'bar' )
- .attr( 'x', d => params.xGroupScale( d.key ) )
- .attr( 'y', d => params.yScale( d.value ) )
- .attr( 'width', params.xGroupScale.bandwidth() )
- .attr( 'height', d => params.height - params.yScale( d.value ) )
+ .attr( 'x', d => scales.xGroupScale( d.key ) )
+ .attr( 'y', d => scales.yScale( d.value ) )
+ .attr( 'width', scales.xGroupScale.bandwidth() )
+ .attr( 'height', d => height - scales.yScale( d.value ) )
.attr( 'fill', d => getColor( d.key, params.visibleKeys, params.colorScheme ) )
.attr( 'pointer-events', 'none' )
.attr( 'tabindex', '0' )
.attr( 'aria-label', d => {
const label = params.mode === 'time-comparison' && d.label ? d.label : d.key;
- return `${ label } ${ params.tooltipValueFormat( d.value ) }`;
+ return `${ label } ${ tooltip.valueFormat( d.value ) }`;
} )
.style( 'opacity', d => {
const opacity = d.focus ? 1 : 0.1;
@@ -89,8 +77,7 @@ export const drawBars = ( node, data, params ) => {
} )
.on( 'focus', ( d, i, nodes ) => {
const targetNode = d.value > 0 ? d3Event.target : d3Event.target.parentNode;
- const position = calculateTooltipPosition( targetNode, node.node(), params.tooltipPosition );
- handleMouseOverBarChart( d.date, nodes[ i ].parentNode, node, data, params, position );
+ tooltip.show( data.find( e => e.date === d.date ), targetNode, nodes[ i ].parentNode );
} )
- .on( 'blur', ( d, i, nodes ) => hideTooltip( nodes[ i ].parentNode, params.tooltip ) );
+ .on( 'blur', () => tooltip.hide() );
};
diff --git a/plugins/woocommerce-admin/packages/components/src/chart/d3chart/utils/index.js b/plugins/woocommerce-admin/packages/components/src/chart/d3chart/utils/index.js
index a0002b1ac05..57dc268c284 100644
--- a/plugins/woocommerce-admin/packages/components/src/chart/d3chart/utils/index.js
+++ b/plugins/woocommerce-admin/packages/components/src/chart/d3chart/utils/index.js
@@ -3,10 +3,9 @@
/**
* External dependencies
*/
-import { find, get, isNil } from 'lodash';
+import { isNil } from 'lodash';
import { format as d3Format } from 'd3-format';
-import { line as d3Line } from 'd3-shape';
-import moment from 'moment';
+import { utcParse as d3UTCParse } from 'd3-time-format';
/**
* Allows an overriding formatter or defaults to d3Format or d3TimeFormat
@@ -17,30 +16,18 @@ import moment from 'moment';
export const getFormatter = ( format, formatter = d3Format ) =>
typeof format === 'function' ? format : formatter( format );
-/**
- * Describes `getUniqueKeys`
- * @param {array} data - The chart component's `data` prop.
- * @returns {array} of unique category keys
- */
-export const getUniqueKeys = data => {
- return [
- ...new Set(
- data.reduce( ( accum, curr ) => {
- Object.keys( curr ).forEach( key => key !== 'date' && accum.push( key ) );
- return accum;
- }, [] )
- ),
- ];
-};
-
/**
* Describes `getOrderedKeys`
* @param {array} data - The chart component's `data` prop.
- * @param {array} uniqueKeys - from `getUniqueKeys`.
* @returns {array} of unique category keys ordered by cumulative total value
*/
-export const getOrderedKeys = ( data, uniqueKeys ) =>
- uniqueKeys
+export const getOrderedKeys = ( data ) => {
+ const keys = new Set(
+ data.reduce( ( acc, curr ) => acc.concat( Object.keys( curr ) ), [] )
+ );
+
+ return [ ...keys ]
+ .filter( key => key !== 'date' )
.map( key => ( {
key,
focus: true,
@@ -48,93 +35,21 @@ export const getOrderedKeys = ( data, uniqueKeys ) =>
visible: true,
} ) )
.sort( ( a, b ) => b.total - a.total );
-
-/**
- * Describes `getLineData`
- * @param {array} data - The chart component's `data` prop.
- * @param {array} orderedKeys - from `getOrderedKeys`.
- * @returns {array} an array objects with a category `key` and an array of `values` with `date` and `value` properties
- */
-export const getLineData = ( data, orderedKeys ) =>
- orderedKeys.map( row => ( {
- key: row.key,
- focus: row.focus,
- visible: row.visible,
- values: data.map( d => ( {
- date: d.date,
- focus: row.focus,
- label: get( d, [ row.key, 'label' ], '' ),
- value: get( d, [ row.key, 'value' ], 0 ),
- visible: row.visible,
- } ) ),
- } ) );
-
-/**
- * Describes `getUniqueDates`
- * @param {array} lineData - from `GetLineData`
- * @param {function} parseDate - D3 time format parser
- * @returns {array} an array of unique date values sorted from earliest to latest
- */
-export const getUniqueDates = ( lineData, parseDate ) => {
- return [
- ...new Set(
- lineData.reduce( ( accum, { values } ) => {
- values.forEach( ( { date } ) => accum.push( date ) );
- return accum;
- }, [] )
- ),
- ].sort( ( a, b ) => parseDate( a ) - parseDate( b ) );
};
/**
- * Describes getLine
- * @param {function} xLineScale - from `getXLineScale`.
- * @param {function} yScale - from `getYScale`.
- * @returns {function} the D3 line function for plotting all category values
+ * Describes `getUniqueDates`
+ * @param {array} data - the chart component's `data` prop.
+ * @param {string} dateParser - D3 time format
+ * @returns {array} an array of unique date values sorted from earliest to latest
*/
-export const getLine = ( xLineScale, yScale ) =>
- d3Line()
- .x( d => xLineScale( moment( d.date ).toDate() ) )
- .y( d => yScale( d.value ) );
-
-/**
- * Describes getDateSpaces
- * @param {array} data - The chart component's `data` prop.
- * @param {array} uniqueDates - from `getUniqueDates`
- * @param {number} width - calculated width of the charting space
- * @param {function} xLineScale - from `getXLineScale`
- * @returns {array} that icnludes the date, start (x position) and width to mode the mouseover rectangles
- */
-export const getDateSpaces = ( data, uniqueDates, width, xLineScale ) =>
- uniqueDates.map( ( d, i ) => {
- const datapoints = find( data, { date: d } );
- const xNow = xLineScale( moment( d ).toDate() );
- const xPrev =
- i >= 1
- ? xLineScale( moment( uniqueDates[ i - 1 ] ).toDate() )
- : xLineScale( moment( uniqueDates[ 0 ] ).toDate() );
- const xNext =
- i < uniqueDates.length - 1
- ? xLineScale( moment( uniqueDates[ i + 1 ] ).toDate() )
- : xLineScale( moment( uniqueDates[ uniqueDates.length - 1 ] ).toDate() );
- let xWidth = i === 0 ? xNext - xNow : xNow - xPrev;
- const xStart = i === 0 ? 0 : xNow - xWidth / 2;
- xWidth = i === 0 || i === uniqueDates.length - 1 ? xWidth / 2 : xWidth;
- return {
- date: d,
- start: uniqueDates.length > 1 ? xStart : 0,
- width: uniqueDates.length > 1 ? xWidth : width,
- values: Object.keys( datapoints )
- .filter( key => key !== 'date' )
- .map( key => {
- return {
- key,
- value: datapoints[ key ].value,
- date: d,
- };
- } ),
- };
- } );
+export const getUniqueDates = ( data, dateParser ) => {
+ const parseDate = d3UTCParse( dateParser );
+ const dates = new Set(
+ data.map( d => d.date )
+ );
+ return [ ...dates ].sort( ( a, b ) => parseDate( a ) - parseDate( b ) );
+};
/**
* Check whether data is empty.
diff --git a/plugins/woocommerce-admin/packages/components/src/chart/d3chart/utils/line-chart.js b/plugins/woocommerce-admin/packages/components/src/chart/d3chart/utils/line-chart.js
index aecff42e6e9..13350c5f478 100644
--- a/plugins/woocommerce-admin/packages/components/src/chart/d3chart/utils/line-chart.js
+++ b/plugins/woocommerce-admin/packages/components/src/chart/d3chart/utils/line-chart.js
@@ -3,38 +3,107 @@
/**
* External dependencies
*/
-import { event as d3Event, select as d3Select } from 'd3-selection';
-import { smallBreak, wideBreak } from './breakpoints';
+import { event as d3Event } from 'd3-selection';
+import { line as d3Line } from 'd3-shape';
import moment from 'moment';
+import { first, get } from 'lodash';
/**
* Internal dependencies
*/
import { getColor } from './color';
-import { calculateTooltipPosition, hideTooltip, showTooltip } from './tooltip';
+import { smallBreak, wideBreak } from './breakpoints';
-const handleMouseOverLineChart = ( date, parentNode, node, data, params, position ) => {
- d3Select( parentNode )
- .select( '.focus-grid' )
- .attr( 'opacity', '1' );
- showTooltip( params, data.find( e => e.date === date ), position );
-};
+/**
+ * Describes getDateSpaces
+ * @param {array} data - The chart component's `data` prop.
+ * @param {array} uniqueDates - from `getUniqueDates`
+ * @param {number} width - calculated width of the charting space
+ * @param {function} xScale - from `getXLineScale`
+ * @returns {array} that includes the date, start (x position) and width to mode the mouseover rectangles
+ */
+export const getDateSpaces = ( data, uniqueDates, width, xScale ) =>
+ uniqueDates.map( ( d, i ) => {
+ const datapoints = first( data.filter( item => item.date === d ) );
+ const xNow = xScale( moment( d ).toDate() );
+ const xPrev =
+ i >= 1
+ ? xScale( moment( uniqueDates[ i - 1 ] ).toDate() )
+ : xScale( moment( uniqueDates[ 0 ] ).toDate() );
+ const xNext =
+ i < uniqueDates.length - 1
+ ? xScale( moment( uniqueDates[ i + 1 ] ).toDate() )
+ : xScale( moment( uniqueDates[ uniqueDates.length - 1 ] ).toDate() );
+ let xWidth = i === 0 ? xNext - xNow : xNow - xPrev;
+ const xStart = i === 0 ? 0 : xNow - xWidth / 2;
+ xWidth = i === 0 || i === uniqueDates.length - 1 ? xWidth / 2 : xWidth;
+ return {
+ date: d,
+ start: uniqueDates.length > 1 ? xStart : 0,
+ width: uniqueDates.length > 1 ? xWidth : width,
+ values: Object.keys( datapoints )
+ .filter( key => key !== 'date' )
+ .map( key => {
+ return {
+ key,
+ value: datapoints[ key ].value,
+ date: d,
+ };
+ } ),
+ };
+ } );
-export const drawLines = ( node, data, params, xOffset ) => {
+/**
+ * Describes getLine
+ * @param {function} xScale - from `getXLineScale`.
+ * @param {function} yScale - from `getYScale`.
+ * @returns {function} the D3 line function for plotting all category values
+ */
+export const getLine = ( xScale, yScale ) =>
+ d3Line()
+ .x( d => xScale( moment( d.date ).toDate() ) )
+ .y( d => yScale( d.value ) );
+
+/**
+ * Describes `getLineData`
+ * @param {array} data - The chart component's `data` prop.
+ * @param {array} orderedKeys - from `getOrderedKeys`.
+ * @returns {array} an array objects with a category `key` and an array of `values` with `date` and `value` properties
+ */
+export const getLineData = ( data, orderedKeys ) =>
+ orderedKeys.map( row => ( {
+ key: row.key,
+ focus: row.focus,
+ visible: row.visible,
+ values: data.map( d => ( {
+ date: d.date,
+ focus: row.focus,
+ label: get( d, [ row.key, 'label' ], '' ),
+ value: get( d, [ row.key, 'value' ], 0 ),
+ visible: row.visible,
+ } ) ),
+ } ) );
+
+export const drawLines = ( node, data, params, scales, formats, tooltip ) => {
+ const height = scales.yScale.range()[ 0 ];
+ const width = scales.xScale.range()[ 1 ];
+ const line = getLine( scales.xScale, scales.yScale );
+ const lineData = getLineData( data, params.visibleKeys );
const series = node
.append( 'g' )
.attr( 'class', 'lines' )
.selectAll( '.line-g' )
- .data( params.lineData.filter( d => d.visible ).reverse() )
+ .data( lineData.filter( d => d.visible ).reverse() )
.enter()
.append( 'g' )
.attr( 'class', 'line-g' )
.attr( 'role', 'region' )
.attr( 'aria-label', d => d.key );
+ const dateSpaces = getDateSpaces( data, params.uniqueDates, width, scales.xScale );
- let lineStroke = params.width <= wideBreak || params.uniqueDates.length > 50 ? 2 : 3;
- lineStroke = params.width <= smallBreak ? 1.25 : lineStroke;
- const dotRadius = params.width <= wideBreak ? 4 : 6;
+ let lineStroke = width <= wideBreak || params.uniqueDates.length > 50 ? 2 : 3;
+ lineStroke = width <= smallBreak ? 1.25 : lineStroke;
+ const dotRadius = width <= wideBreak ? 4 : 6;
params.uniqueDates.length > 1 &&
series
@@ -48,11 +117,11 @@ export const drawLines = ( node, data, params, xOffset ) => {
const opacity = d.focus ? 1 : 0.1;
return d.visible ? opacity : 0;
} )
- .attr( 'd', d => params.line( d.values ) );
+ .attr( 'd', d => line( d.values ) );
const minDataPointSpacing = 36;
- params.width / params.uniqueDates.length > minDataPointSpacing &&
+ width / params.uniqueDates.length > minDataPointSpacing &&
series
.selectAll( 'circle' )
.data( ( d, i ) => d.values.map( row => ( { ...row, i, visible: d.visible, key: d.key } ) ) )
@@ -66,30 +135,25 @@ export const drawLines = ( node, data, params, xOffset ) => {
const opacity = d.focus ? 1 : 0.1;
return d.visible ? opacity : 0;
} )
- .attr( 'cx', d => params.xLineScale( moment( d.date ).toDate() ) + xOffset )
- .attr( 'cy', d => params.yScale( d.value ) )
+ .attr( 'cx', d => scales.xScale( moment( d.date ).toDate() ) )
+ .attr( 'cy', d => scales.yScale( d.value ) )
.attr( 'tabindex', '0' )
.attr( 'aria-label', d => {
const label = d.label
? d.label
- : params.tooltipLabelFormat( d.date instanceof Date ? d.date : moment( d.date ).toDate() );
- return `${ label } ${ params.tooltipValueFormat( d.value ) }`;
+ : tooltip.labelFormat( d.date instanceof Date ? d.date : moment( d.date ).toDate() );
+ return `${ label } ${ tooltip.valueFormat( d.value ) }`;
} )
.on( 'focus', ( d, i, nodes ) => {
- const position = calculateTooltipPosition(
- d3Event.target,
- node.node(),
- params.tooltipPosition
- );
- handleMouseOverLineChart( d.date, nodes[ i ].parentNode, node, data, params, position );
+ tooltip.show( data.find( e => e.date === d.date ), nodes[ i ].parentNode, d3Event.target );
} )
- .on( 'blur', ( d, i, nodes ) => hideTooltip( nodes[ i ].parentNode, params.tooltip ) );
+ .on( 'blur', () => tooltip.hide() );
const focus = node
.append( 'g' )
.attr( 'class', 'focusspaces' )
.selectAll( '.focus' )
- .data( params.dateSpaces )
+ .data( dateSpaces )
.enter()
.append( 'g' )
.attr( 'class', 'focus' );
@@ -101,10 +165,10 @@ export const drawLines = ( node, data, params, xOffset ) => {
focusGrid
.append( 'line' )
- .attr( 'x1', d => params.xLineScale( moment( d.date ).toDate() ) + xOffset )
+ .attr( 'x1', d => scales.xScale( moment( d.date ).toDate() ) )
.attr( 'y1', 0 )
- .attr( 'x2', d => params.xLineScale( moment( d.date ).toDate() ) + xOffset )
- .attr( 'y2', params.height );
+ .attr( 'x2', d => scales.xScale( moment( d.date ).toDate() ) )
+ .attr( 'y2', height );
focusGrid
.selectAll( 'circle' )
@@ -115,8 +179,8 @@ export const drawLines = ( node, data, params, xOffset ) => {
.attr( 'fill', d => getColor( d.key, params.visibleKeys, params.colorScheme ) )
.attr( 'stroke', '#fff' )
.attr( 'stroke-width', lineStroke + 2 )
- .attr( 'cx', d => params.xLineScale( moment( d.date ).toDate() ) + xOffset )
- .attr( 'cy', d => params.yScale( d.value ) );
+ .attr( 'cx', d => scales.xScale( moment( d.date ).toDate() ) )
+ .attr( 'cy', d => scales.yScale( d.value ) );
focus
.append( 'rect' )
@@ -124,18 +188,12 @@ export const drawLines = ( node, data, params, xOffset ) => {
.attr( 'x', d => d.start )
.attr( 'y', 0 )
.attr( 'width', d => d.width )
- .attr( 'height', params.height )
+ .attr( 'height', height )
.attr( 'opacity', 0 )
.on( 'mouseover', ( d, i, nodes ) => {
- const isTooltipLeftAligned = ( i === 0 || i === params.dateSpaces.length - 1 ) && params.uniqueDates.length > 1;
+ const isTooltipLeftAligned = ( i === 0 || i === dateSpaces.length - 1 ) && params.uniqueDates.length > 1;
const elementWidthRatio = isTooltipLeftAligned ? 0 : 0.5;
- const position = calculateTooltipPosition(
- d3Event.target,
- node.node(),
- params.tooltipPosition,
- elementWidthRatio
- );
- handleMouseOverLineChart( d.date, nodes[ i ].parentNode, node, data, params, position );
+ tooltip.show( data.find( e => e.date === d.date ), d3Event.target, nodes[ i ].parentNode, elementWidthRatio );
} )
- .on( 'mouseout', ( d, i, nodes ) => hideTooltip( nodes[ i ].parentNode, params.tooltip ) );
+ .on( 'mouseout', () => tooltip.hide() );
};
diff --git a/plugins/woocommerce-admin/packages/components/src/chart/d3chart/utils/scales.js b/plugins/woocommerce-admin/packages/components/src/chart/d3chart/utils/scales.js
index a50dc342866..26f2adb17a9 100644
--- a/plugins/woocommerce-admin/packages/components/src/chart/d3chart/utils/scales.js
+++ b/plugins/woocommerce-admin/packages/components/src/chart/d3chart/utils/scales.js
@@ -52,13 +52,26 @@ export const getXLineScale = ( uniqueDates, width ) =>
] )
.rangeRound( [ 0, width ] );
+const getMaxYValue = data => {
+ let maxYValue = Number.NEGATIVE_INFINITY;
+ data.map( d => {
+ for ( const [ key, item ] of Object.entries( d ) ) {
+ if ( key !== 'date' && Number.isFinite( item.value ) && item.value > maxYValue ) {
+ maxYValue = item.value;
+ }
+ }
+ } );
+
+ return maxYValue;
+};
+
/**
* Describes and rounds the maximum y value to the nearest thousand, ten-thousand, million etc. In case it is a decimal number, ceils it.
- * @param {array} lineData - from `getLineData`
+ * @param {array} data - The chart component's `data` prop.
* @returns {number} the maximum value in the timeseries multiplied by 4/3
*/
-export const getYMax = lineData => {
- const maxValue = Math.max( ...lineData.map( d => Math.max( ...d.values.map( date => date.value ) ) ) );
+export const getYMax = data => {
+ const maxValue = getMaxYValue( data );
if ( ! Number.isFinite( maxValue ) || maxValue <= 0 ) {
return 0;
}
@@ -70,21 +83,10 @@ export const getYMax = lineData => {
/**
* Describes getYScale
* @param {number} height - calculated height of the charting space
- * @param {number} yMax - from `getYMax`
+ * @param {number} yMax - maximum y value
* @returns {function} the D3 linear scale from 0 to the value from `getYMax`
*/
export const getYScale = ( height, yMax ) =>
d3ScaleLinear()
.domain( [ 0, yMax === 0 ? 1 : yMax ] )
.rangeRound( [ height, 0 ] );
-
-/**
- * Describes getyTickOffset
- * @param {number} height - calculated height of the charting space
- * @param {number} yMax - from `getYMax`
- * @returns {function} the D3 linear scale from 0 to the value from `getYMax`, offset by 12 pixels down
- */
-export const getYTickOffset = ( height, yMax ) =>
- d3ScaleLinear()
- .domain( [ 0, yMax === 0 ? 1 : yMax ] )
- .rangeRound( [ height + 12, 12 ] );
diff --git a/plugins/woocommerce-admin/packages/components/src/chart/d3chart/utils/test/index.js b/plugins/woocommerce-admin/packages/components/src/chart/d3chart/utils/test/index.js
index 9207a90ea9e..a3b518f1454 100644
--- a/plugins/woocommerce-admin/packages/components/src/chart/d3chart/utils/test/index.js
+++ b/plugins/woocommerce-admin/packages/components/src/chart/d3chart/utils/test/index.js
@@ -8,24 +8,14 @@ import { utcParse as d3UTCParse } from 'd3-time-format';
* Internal dependencies
*/
import dummyOrders from './fixtures/dummy-orders';
-import orderedDates from './fixtures/dummy-ordered-dates';
import orderedKeys from './fixtures/dummy-ordered-keys';
import {
- getDateSpaces,
getOrderedKeys,
- getLineData,
- getUniqueKeys,
- getUniqueDates,
isDataEmpty,
} from '../index';
-import { getXLineScale } from '../scales';
const parseDate = d3UTCParse( '%Y-%m-%dT%H:%M:%S' );
-const testUniqueKeys = getUniqueKeys( dummyOrders );
-const testOrderedKeys = getOrderedKeys( dummyOrders, testUniqueKeys );
-const testLineData = getLineData( dummyOrders, testOrderedKeys );
-const testUniqueDates = getUniqueDates( testLineData, parseDate );
-const testXLineScale = getXLineScale( testUniqueDates, 100 );
+const testOrderedKeys = getOrderedKeys( dummyOrders );
describe( 'parseDate', () => {
it( 'correctly parse date in the expected format', () => {
@@ -35,67 +25,12 @@ describe( 'parseDate', () => {
} );
} );
-describe( 'getUniqueKeys', () => {
- it( 'returns an array of keys excluding date', () => {
- // sort is a mutating action so we need a copy
- const testUniqueKeysClone = testUniqueKeys.slice();
- const sortedAZKeys = orderedKeys.map( d => d.key ).slice();
- expect( testUniqueKeysClone.sort() ).toEqual( sortedAZKeys.sort() );
- } );
-} );
-
describe( 'getOrderedKeys', () => {
it( 'returns an array of keys order by value from largest to smallest', () => {
expect( testOrderedKeys ).toEqual( orderedKeys );
} );
} );
-describe( 'getLineData', () => {
- it( 'returns a sorted array of objects with category key', () => {
- expect( testLineData ).toBeInstanceOf( Array );
- expect( testLineData ).toHaveLength( 5 );
- expect( testLineData.map( d => d.key ) ).toEqual( orderedKeys.map( d => d.key ) );
- } );
-
- testLineData.forEach( d => {
- it( 'ensure a key and that the values property is an array', () => {
- expect( d ).toHaveProperty( 'key' );
- expect( d ).toHaveProperty( 'values' );
- expect( d.values ).toBeInstanceOf( Array );
- } );
-
- it( 'ensure all unique dates exist in values array', () => {
- const rowDates = d.values.map( row => row.date );
- expect( rowDates ).toEqual( orderedDates );
- } );
-
- d.values.forEach( row => {
- it( 'ensure a date property and that the values property is an array with date (parseable) and value properties', () => {
- expect( row ).toHaveProperty( 'date' );
- expect( row ).toHaveProperty( 'value' );
- expect( parseDate( row.date ) ).not.toBeNull();
- expect( typeof row.date ).toBe( 'string' );
- expect( typeof row.value ).toBe( 'number' );
- } );
- } );
- } );
-} );
-
-describe( 'getDateSpaces', () => {
- it( 'return an array used to space out the mouseover rectangles, used for tooltips', () => {
- const testDateSpaces = getDateSpaces( dummyOrders, testUniqueDates, 100, testXLineScale );
- expect( testDateSpaces[ 0 ].date ).toEqual( '2018-05-30T00:00:00' );
- expect( testDateSpaces[ 0 ].start ).toEqual( 0 );
- expect( testDateSpaces[ 0 ].width ).toEqual( 10 );
- expect( testDateSpaces[ 3 ].date ).toEqual( '2018-06-02T00:00:00' );
- expect( testDateSpaces[ 3 ].start ).toEqual( 50 );
- expect( testDateSpaces[ 3 ].width ).toEqual( 20 );
- expect( testDateSpaces[ testDateSpaces.length - 1 ].date ).toEqual( '2018-06-04T00:00:00' );
- expect( testDateSpaces[ testDateSpaces.length - 1 ].start ).toEqual( 90 );
- expect( testDateSpaces[ testDateSpaces.length - 1 ].width ).toEqual( 10 );
- } );
-} );
-
describe( 'isDataEmpty', () => {
it( 'should return true when all data values are 0 and no baseValue is provided', () => {
const data = [
diff --git a/plugins/woocommerce-admin/packages/components/src/chart/d3chart/utils/test/line-chart.js b/plugins/woocommerce-admin/packages/components/src/chart/d3chart/utils/test/line-chart.js
new file mode 100644
index 00000000000..439946d6533
--- /dev/null
+++ b/plugins/woocommerce-admin/packages/components/src/chart/d3chart/utils/test/line-chart.js
@@ -0,0 +1,73 @@
+/** @format */
+/**
+ * External dependencies
+ */
+import { utcParse as d3UTCParse } from 'd3-time-format';
+
+/**
+ * Internal dependencies
+ */
+import dummyOrders from './fixtures/dummy-orders';
+import orderedDates from './fixtures/dummy-ordered-dates';
+import orderedKeys from './fixtures/dummy-ordered-keys';
+import {
+ getOrderedKeys,
+ getUniqueDates,
+} from '../index';
+import {
+ getDateSpaces,
+ getLineData,
+} from '../line-chart';
+import { getXLineScale } from '../scales';
+
+const parseDate = d3UTCParse( '%Y-%m-%dT%H:%M:%S' );
+const testOrderedKeys = getOrderedKeys( dummyOrders );
+const testLineData = getLineData( dummyOrders, testOrderedKeys );
+const testUniqueDates = getUniqueDates( dummyOrders, '%Y-%m-%dT%H:%M:%S' );
+const testXLineScale = getXLineScale( testUniqueDates, 100 );
+
+describe( 'getDateSpaces', () => {
+ it( 'return an array used to space out the mouseover rectangles, used for tooltips', () => {
+ const testDateSpaces = getDateSpaces( dummyOrders, testUniqueDates, 100, testXLineScale );
+ expect( testDateSpaces[ 0 ].date ).toEqual( '2018-05-30T00:00:00' );
+ expect( testDateSpaces[ 0 ].start ).toEqual( 0 );
+ expect( testDateSpaces[ 0 ].width ).toEqual( 10 );
+ expect( testDateSpaces[ 3 ].date ).toEqual( '2018-06-02T00:00:00' );
+ expect( testDateSpaces[ 3 ].start ).toEqual( 50 );
+ expect( testDateSpaces[ 3 ].width ).toEqual( 20 );
+ expect( testDateSpaces[ testDateSpaces.length - 1 ].date ).toEqual( '2018-06-04T00:00:00' );
+ expect( testDateSpaces[ testDateSpaces.length - 1 ].start ).toEqual( 90 );
+ expect( testDateSpaces[ testDateSpaces.length - 1 ].width ).toEqual( 10 );
+ } );
+} );
+
+describe( 'getLineData', () => {
+ it( 'returns a sorted array of objects with category key', () => {
+ expect( testLineData ).toBeInstanceOf( Array );
+ expect( testLineData ).toHaveLength( 5 );
+ expect( testLineData.map( d => d.key ) ).toEqual( orderedKeys.map( d => d.key ) );
+ } );
+
+ testLineData.forEach( d => {
+ it( 'ensure a key and that the values property is an array', () => {
+ expect( d ).toHaveProperty( 'key' );
+ expect( d ).toHaveProperty( 'values' );
+ expect( d.values ).toBeInstanceOf( Array );
+ } );
+
+ it( 'ensure all unique dates exist in values array', () => {
+ const rowDates = d.values.map( row => row.date );
+ expect( rowDates ).toEqual( orderedDates );
+ } );
+
+ d.values.forEach( row => {
+ it( 'ensure a date property and that the values property is an array with date (parseable) and value properties', () => {
+ expect( row ).toHaveProperty( 'date' );
+ expect( row ).toHaveProperty( 'value' );
+ expect( parseDate( row.date ) ).not.toBeNull();
+ expect( typeof row.date ).toBe( 'string' );
+ expect( typeof row.value ).toBe( 'number' );
+ } );
+ } );
+ } );
+} );
diff --git a/plugins/woocommerce-admin/packages/components/src/chart/d3chart/utils/test/scales.js b/plugins/woocommerce-admin/packages/components/src/chart/d3chart/utils/test/scales.js
index a93dd2ff89a..bc591fee68b 100644
--- a/plugins/woocommerce-admin/packages/components/src/chart/d3chart/utils/test/scales.js
+++ b/plugins/woocommerce-admin/packages/components/src/chart/d3chart/utils/test/scales.js
@@ -2,7 +2,6 @@
/**
* External dependencies
*/
-import { utcParse as d3UTCParse } from 'd3-time-format';
import { scaleBand, scaleLinear, scaleTime } from 'd3-scale';
/**
@@ -11,11 +10,9 @@ import { scaleBand, scaleLinear, scaleTime } from 'd3-scale';
import dummyOrders from './fixtures/dummy-orders';
import {
getOrderedKeys,
- getLineData,
- getUniqueKeys,
getUniqueDates,
} from '../index';
-import { getXGroupScale, getXScale, getXLineScale, getYMax, getYScale, getYTickOffset } from '../scales';
+import { getXGroupScale, getXScale, getXLineScale, getYMax, getYScale } from '../scales';
jest.mock( 'd3-scale', () => ( {
...require.requireActual( 'd3-scale' ),
@@ -37,13 +34,10 @@ jest.mock( 'd3-scale', () => ( {
} ),
} ) );
-const testUniqueKeys = getUniqueKeys( dummyOrders );
-const testOrderedKeys = getOrderedKeys( dummyOrders, testUniqueKeys );
-const testLineData = getLineData( dummyOrders, testOrderedKeys );
+const testOrderedKeys = getOrderedKeys( dummyOrders );
describe( 'X scales', () => {
- const parseDate = d3UTCParse( '%Y-%m-%dT%H:%M:%S' );
- const testUniqueDates = getUniqueDates( testLineData, parseDate );
+ const testUniqueDates = getUniqueDates( dummyOrders, '%Y-%m-%dT%H:%M:%S' );
describe( 'getXScale', () => {
it( 'creates band scale with correct parameters', () => {
@@ -96,7 +90,7 @@ describe( 'X scales', () => {
describe( 'Y scales', () => {
describe( 'getYMax', () => {
it( 'calculate the correct maximum y value', () => {
- expect( getYMax( testLineData ) ).toEqual( 15000000 );
+ expect( getYMax( dummyOrders ) ).toEqual( 15000000 );
} );
it( 'return 0 if there is no line data', () => {
@@ -120,21 +114,4 @@ describe( 'Y scales', () => {
expect( lastArgs[ 0 ] ).toBeLessThan( lastArgs[ 1 ] );
} );
} );
-
- describe( 'getYTickOffset', () => {
- it( 'creates linear scale with correct parameters', () => {
- getYTickOffset( 100, 15000000 );
-
- 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 ] );
- } );
- } );
} );
diff --git a/plugins/woocommerce-admin/packages/components/src/chart/d3chart/utils/tooltip.js b/plugins/woocommerce-admin/packages/components/src/chart/d3chart/utils/tooltip.js
index c72296c6531..139d7cc60bb 100644
--- a/plugins/woocommerce-admin/packages/components/src/chart/d3chart/utils/tooltip.js
+++ b/plugins/woocommerce-admin/packages/components/src/chart/d3chart/utils/tooltip.js
@@ -11,138 +11,148 @@ import moment from 'moment';
*/
import { getColor } from './color';
-export const hideTooltip = ( parentNode, tooltipNode ) => {
- d3Select( parentNode )
- .selectAll( '.barfocus, .focus-grid' )
- .attr( 'opacity', '0' );
- d3Select( tooltipNode )
- .style( 'visibility', 'hidden' );
-};
+class ChartTooltip {
+ constructor() {
+ this.ref = null;
+ this.chart = null;
+ this.position = '';
+ this.title = '';
+ this.labelFormat = '';
+ this.valueFormat = '';
+ this.visibleKeys = '';
+ this.colorScheme = null;
+ this.margin = 24;
+ }
-const calculateTooltipXPosition = (
- elementCoords,
- chartCoords,
- tooltipSize,
- tooltipMargin,
- elementWidthRatio,
- tooltipPosition
-) => {
- const d3BaseCoords = d3Select( '.d3-base' ).node().getBoundingClientRect();
- const leftMargin = Math.max( d3BaseCoords.left, chartCoords.left );
+ calculateXPosition(
+ elementCoords,
+ chartCoords,
+ elementWidthRatio,
+ ) {
+ const tooltipSize = this.ref.getBoundingClientRect();
+ const d3BaseCoords = d3Select( '.d3-base' ).node().getBoundingClientRect();
+ const leftMargin = Math.max( d3BaseCoords.left, chartCoords.left );
- if ( tooltipPosition === 'below' ) {
- return Math.max(
- tooltipMargin,
- Math.min(
- elementCoords.left + elementCoords.width * 0.5 - tooltipSize.width / 2 - leftMargin,
- d3BaseCoords.width - tooltipSize.width - tooltipMargin
- )
+ if ( this.position === 'below' ) {
+ return Math.max(
+ this.margin,
+ Math.min(
+ elementCoords.left + elementCoords.width * 0.5 - tooltipSize.width / 2 - leftMargin,
+ d3BaseCoords.width - tooltipSize.width - this.margin
+ )
+ );
+ }
+
+ const xPosition =
+ elementCoords.left + elementCoords.width * elementWidthRatio + this.margin - leftMargin;
+
+ if ( xPosition + tooltipSize.width + this.margin > d3BaseCoords.width ) {
+ return Math.max(
+ this.margin,
+ elementCoords.left +
+ elementCoords.width * ( 1 - elementWidthRatio ) -
+ tooltipSize.width -
+ this.margin -
+ leftMargin
+ );
+ }
+
+ return xPosition;
+ }
+
+ calculateYPosition(
+ elementCoords,
+ chartCoords,
+ ) {
+ if ( this.position === 'below' ) {
+ return chartCoords.height;
+ }
+
+ const tooltipSize = this.ref.getBoundingClientRect();
+ const yPosition = elementCoords.top + this.margin - chartCoords.top;
+ if ( yPosition + tooltipSize.height + this.margin > chartCoords.height ) {
+ return Math.max( 0, elementCoords.top - tooltipSize.height - this.margin - chartCoords.top );
+ }
+
+ return yPosition;
+ }
+
+ calculatePosition( element, elementWidthRatio = 1 ) {
+ const elementCoords = element.getBoundingClientRect();
+ const chartCoords = this.chart.getBoundingClientRect();
+
+ if ( this.position === 'below' ) {
+ elementWidthRatio = 0;
+ }
+
+ return {
+ x: this.calculateXPosition(
+ elementCoords,
+ chartCoords,
+ elementWidthRatio,
+ ),
+ y: this.calculateYPosition(
+ elementCoords,
+ chartCoords,
+ ),
+ };
+ }
+
+ hide() {
+ d3Select( this.chart )
+ .selectAll( '.barfocus, .focus-grid' )
+ .attr( 'opacity', '0' );
+ d3Select( this.ref )
+ .style( 'visibility', 'hidden' );
+ }
+
+ getTooltipRowLabel( d, row ) {
+ if ( d[ row.key ].labelDate ) {
+ return this.labelFormat( moment( d[ row.key ].labelDate ).toDate() );
+ }
+ return row.key;
+ }
+
+ show( d, triggerElement, parentNode, elementWidthRatio = 1 ) {
+ if ( ! this.visibleKeys.length ) {
+ return;
+ }
+ d3Select( parentNode )
+ .select( '.focus-grid, .barfocus' )
+ .attr( 'opacity', '1' );
+ const position = this.calculatePosition( triggerElement, elementWidthRatio );
+
+ const keys = this.visibleKeys.map(
+ row => `
+
+
+
+
+ ${ this.getTooltipRowLabel( d, row ) }
+
+ ${ this.valueFormat( d[ row.key ].value ) }
+
+ `
);
+
+ const tooltipTitle = this.title
+ ? this.title
+ : this.labelFormat( moment( d.date ).toDate() );
+
+ d3Select( this.ref )
+ .style( 'left', position.x + 'px' )
+ .style( 'top', position.y + 'px' )
+ .style( 'visibility', 'visible' ).html( `
+
+ ` );
}
+}
- const xPosition =
- elementCoords.left + elementCoords.width * elementWidthRatio + tooltipMargin - leftMargin;
-
- if ( xPosition + tooltipSize.width + tooltipMargin > d3BaseCoords.width ) {
- return Math.max(
- tooltipMargin,
- elementCoords.left +
- elementCoords.width * ( 1 - elementWidthRatio ) -
- tooltipSize.width -
- tooltipMargin -
- leftMargin
- );
- }
-
- return xPosition;
-};
-
-const calculateTooltipYPosition = (
- elementCoords,
- chartCoords,
- tooltipSize,
- tooltipMargin,
- tooltipPosition
-) => {
- if ( tooltipPosition === 'below' ) {
- return chartCoords.height;
- }
-
- const yPosition = elementCoords.top + tooltipMargin - chartCoords.top;
- if ( yPosition + tooltipSize.height + tooltipMargin > chartCoords.height ) {
- return Math.max( 0, elementCoords.top - tooltipSize.height - tooltipMargin - chartCoords.top );
- }
-
- return yPosition;
-};
-
-export const calculateTooltipPosition = ( element, chart, tooltipPosition, elementWidthRatio = 1 ) => {
- const elementCoords = element.getBoundingClientRect();
- const chartCoords = chart.getBoundingClientRect();
- const tooltipSize = d3Select( '.d3-chart__tooltip' )
- .node()
- .getBoundingClientRect();
- const tooltipMargin = 24;
-
- if ( tooltipPosition === 'below' ) {
- elementWidthRatio = 0;
- }
-
- return {
- x: calculateTooltipXPosition(
- elementCoords,
- chartCoords,
- tooltipSize,
- tooltipMargin,
- elementWidthRatio,
- tooltipPosition
- ),
- y: calculateTooltipYPosition(
- elementCoords,
- chartCoords,
- tooltipSize,
- tooltipMargin,
- tooltipPosition
- ),
- };
-};
-
-const getTooltipRowLabel = ( d, row, params ) => {
- if ( d[ row.key ].labelDate ) {
- return params.tooltipLabelFormat( moment( d[ row.key ].labelDate ).toDate() );
- }
- return row.key;
-};
-
-export const showTooltip = ( params, d, position ) => {
- const keys = params.visibleKeys.map(
- row => `
-
-
-
-
- ${ getTooltipRowLabel( d, row, params ) }
-
- ${ params.tooltipValueFormat( d[ row.key ].value ) }
-
- `
- );
-
- const tooltipTitle = params.tooltipTitle
- ? params.tooltipTitle
- : params.tooltipLabelFormat( moment( d.date ).toDate() );
-
- d3Select( params.tooltip )
- .style( 'left', position.x + 'px' )
- .style( 'top', position.y + 'px' )
- .style( 'visibility', 'visible' ).html( `
-
- ` );
-};
+export default ChartTooltip;
diff --git a/plugins/woocommerce-admin/packages/components/src/filters/advanced/search-filter.js b/plugins/woocommerce-admin/packages/components/src/filters/advanced/search-filter.js
index 92189a5b006..dec99989dc7 100644
--- a/plugins/woocommerce-admin/packages/components/src/filters/advanced/search-filter.js
+++ b/plugins/woocommerce-admin/packages/components/src/filters/advanced/search-filter.js
@@ -40,7 +40,12 @@ class SearchFilter extends Component {
}
updateLabels( selected ) {
- this.setState( { selected } );
+ const prevIds = this.state.selected.map( item => item.id );
+ const ids = selected.map( item => item.id );
+
+ if ( ! isEqual( ids.sort(), prevIds.sort() ) ) {
+ this.setState( { selected } );
+ }
}
onSearchChange( values ) {
diff --git a/plugins/woocommerce-admin/packages/components/src/ie.scss b/plugins/woocommerce-admin/packages/components/src/ie.scss
index d14445447f1..42d0131e959 100644
--- a/plugins/woocommerce-admin/packages/components/src/ie.scss
+++ b/plugins/woocommerce-admin/packages/components/src/ie.scss
@@ -1,6 +1,5 @@
/**
* Internal Dependencies
*/
-@import 'segmented-selection/ie.scss';
@import 'summary/ie.scss';
@import 'table/ie.scss';
diff --git a/plugins/woocommerce-admin/packages/components/src/search/autocomplete.js b/plugins/woocommerce-admin/packages/components/src/search/autocomplete.js
index 32793da9919..79e23e73598 100644
--- a/plugins/woocommerce-admin/packages/components/src/search/autocomplete.js
+++ b/plugins/woocommerce-admin/packages/components/src/search/autocomplete.js
@@ -59,7 +59,7 @@ export class Autocomplete extends Component {
this.reset = this.reset.bind( this );
this.search = this.search.bind( this );
this.handleKeyDown = this.handleKeyDown.bind( this );
- this.debouncedLoadOptions = debounce( this.loadOptions, 250 );
+ this.debouncedLoadOptions = debounce( this.loadOptions, 400 );
this.state = this.constructor.getInitialState();
}
diff --git a/plugins/woocommerce-admin/packages/components/src/search/autocompleters/countries.js b/plugins/woocommerce-admin/packages/components/src/search/autocompleters/countries.js
index 874d9ba5d0e..87242f74b14 100644
--- a/plugins/woocommerce-admin/packages/components/src/search/autocompleters/countries.js
+++ b/plugins/woocommerce-admin/packages/components/src/search/autocompleters/countries.js
@@ -20,6 +20,7 @@ import Flag from '../../flag';
export default {
name: 'countries',
className: 'woocommerce-search__country-result',
+ isDebounced: true,
options() {
return wcSettings.dataEndpoints.countries || [];
},
diff --git a/plugins/woocommerce-admin/packages/components/src/search/autocompleters/customers.js b/plugins/woocommerce-admin/packages/components/src/search/autocompleters/customers.js
index a55e9615d1d..c1c838a43ac 100644
--- a/plugins/woocommerce-admin/packages/components/src/search/autocompleters/customers.js
+++ b/plugins/woocommerce-admin/packages/components/src/search/autocompleters/customers.js
@@ -16,8 +16,6 @@ import { stringifyQuery } from '@woocommerce/navigation';
*/
import { computeSuggestionMatch } from './utils';
-const getName = customer => [ customer.first_name, customer.last_name ].filter( Boolean ).join( ' ' );
-
/**
* A customer completer.
* See https://github.com/WordPress/gutenberg/tree/master/packages/components/src/autocomplete#the-completer-interface
@@ -40,7 +38,7 @@ export default {
},
isDebounced: true,
getOptionKeywords( customer ) {
- return [ getName( customer ) ];
+ return [ customer.name ];
},
getFreeTextOptions( query ) {
const label = (
@@ -56,15 +54,15 @@ export default {
const nameOption = {
key: 'name',
label: label,
- value: { id: query, first_name: query },
+ value: { id: query, name: query },
};
return [ nameOption ];
},
getOptionLabel( customer, query ) {
- const match = computeSuggestionMatch( getName( customer ), query ) || {};
+ const match = computeSuggestionMatch( customer.name, query ) || {};
return [
-
+
{ match.suggestionBeforeMatch }
{ match.suggestionMatch }
@@ -78,7 +76,7 @@ export default {
getOptionCompletion( customer ) {
return {
id: customer.id,
- label: getName( customer ),
+ label: customer.name,
};
},
};
diff --git a/plugins/woocommerce-admin/packages/components/src/segmented-selection/ie.scss b/plugins/woocommerce-admin/packages/components/src/segmented-selection/ie.scss
deleted file mode 100644
index 82dba64f037..00000000000
--- a/plugins/woocommerce-admin/packages/components/src/segmented-selection/ie.scss
+++ /dev/null
@@ -1,19 +0,0 @@
-/** @format */
-
-.woocommerce-segmented-selection__item {
- display: block;
- @include set-grid-item-position( 2, 10 );
-
- &:nth-child(2n) {
- border-left: 1px solid $core-grey-light-700;
- border-top: 1px solid $core-grey-light-700;
- }
-
- &:nth-child(2n + 1) {
- border-top: 1px solid $core-grey-light-700;
- }
-
- &:nth-child(-n + 2) {
- border-top: 0;
- }
-}
diff --git a/plugins/woocommerce-admin/packages/components/src/segmented-selection/style.scss b/plugins/woocommerce-admin/packages/components/src/segmented-selection/style.scss
index 152c3980c34..04efce1bf90 100644
--- a/plugins/woocommerce-admin/packages/components/src/segmented-selection/style.scss
+++ b/plugins/woocommerce-admin/packages/components/src/segmented-selection/style.scss
@@ -14,6 +14,24 @@
background-color: $core-grey-light-700;
}
+.woocommerce-segmented-selection__item {
+ display: block;
+ @include set-grid-item-position( 2, 10 );
+
+ &:nth-child(2n) {
+ border-left: 1px solid $core-grey-light-700;
+ border-top: 1px solid $core-grey-light-700;
+ }
+
+ &:nth-child(2n + 1) {
+ border-top: 1px solid $core-grey-light-700;
+ }
+
+ &:nth-child(-n + 2) {
+ border-top: 0;
+ }
+}
+
.woocommerce-segmented-selection__label {
background-color: $core-grey-light-100;
padding: $gap-small $gap-small $gap-small $gap-larger;
diff --git a/plugins/woocommerce-admin/tests/api/reports-customers-stats.php b/plugins/woocommerce-admin/tests/api/reports-customers-stats.php
index 367f5c642e4..e4ab0ae34a9 100644
--- a/plugins/woocommerce-admin/tests/api/reports-customers-stats.php
+++ b/plugins/woocommerce-admin/tests/api/reports-customers-stats.php
@@ -59,9 +59,8 @@ class WC_Tests_API_Reports_Customers_Stats extends WC_REST_Unit_Test_Case {
$data = $response->get_data();
$properties = $data['schema']['properties'];
- $this->assertCount( 2, $properties );
+ $this->assertCount( 1, $properties );
$this->assertArrayHasKey( 'totals', $properties );
- $this->assertArrayHasKey( 'intervals', $properties );
$this->assertCount( 4, $properties['totals']['properties'] );
$this->assertArrayHasKey( 'customers_count', $properties['totals']['properties'] );
$this->assertArrayHasKey( 'avg_orders_count', $properties['totals']['properties'] );
@@ -142,7 +141,7 @@ class WC_Tests_API_Reports_Customers_Stats extends WC_REST_Unit_Test_Case {
// Test name parameter (case with no matches).
$request->set_query_params(
array(
- 'name' => 'Nota Customername',
+ 'search' => 'Nota Customername',
)
);
$response = $this->server->dispatch( $request );
@@ -157,8 +156,8 @@ class WC_Tests_API_Reports_Customers_Stats extends WC_REST_Unit_Test_Case {
// Test name and last_order parameters.
$request->set_query_params(
array(
- 'name' => 'Jeff',
- 'last_order_after' => date( 'Y-m-d' ) . 'T00:00:00Z',
+ 'search' => 'Jeff',
+ 'last_order_after' => date( 'Y-m-d' ) . 'T00:00:00Z',
)
);
$response = $this->server->dispatch( $request );
diff --git a/plugins/woocommerce-admin/tests/api/reports-customers.php b/plugins/woocommerce-admin/tests/api/reports-customers.php
index f2386a45201..6602c4f5d1d 100644
--- a/plugins/woocommerce-admin/tests/api/reports-customers.php
+++ b/plugins/woocommerce-admin/tests/api/reports-customers.php
@@ -52,7 +52,7 @@ class WC_Tests_API_Reports_Customers extends WC_REST_Unit_Test_Case {
* @param array $schema Item to check schema.
*/
public function assert_report_item_schema( $schema ) {
- $this->assertArrayHasKey( 'customer_id', $schema );
+ $this->assertArrayHasKey( 'id', $schema );
$this->assertArrayHasKey( 'user_id', $schema );
$this->assertArrayHasKey( 'name', $schema );
$this->assertArrayHasKey( 'username', $schema );
@@ -163,7 +163,7 @@ class WC_Tests_API_Reports_Customers extends WC_REST_Unit_Test_Case {
// Test name parameter (case with no matches).
$request->set_query_params(
array(
- 'name' => 'Nota Customername',
+ 'search' => 'Nota Customername',
)
);
$response = $this->server->dispatch( $request );
@@ -175,8 +175,8 @@ class WC_Tests_API_Reports_Customers extends WC_REST_Unit_Test_Case {
// Test name and last_order parameters.
$request->set_query_params(
array(
- 'name' => 'Justin',
- 'last_order_after' => date( 'Y-m-d' ) . 'T00:00:00Z',
+ 'search' => 'Justin',
+ 'last_order_after' => date( 'Y-m-d' ) . 'T00:00:00Z',
)
);
$response = $this->server->dispatch( $request );
diff --git a/plugins/woocommerce-admin/tests/api/reports-stock-stats.php b/plugins/woocommerce-admin/tests/api/reports-stock-stats.php
new file mode 100644
index 00000000000..bf238e1563f
--- /dev/null
+++ b/plugins/woocommerce-admin/tests/api/reports-stock-stats.php
@@ -0,0 +1,139 @@
+user = $this->factory->user->create(
+ array(
+ 'role' => 'administrator',
+ )
+ );
+ }
+
+ /**
+ * Test route registration.
+ */
+ public function test_register_routes() {
+ $routes = $this->server->get_routes();
+
+ $this->assertArrayHasKey( $this->endpoint, $routes );
+ }
+
+ /**
+ * Test getting reports.
+ */
+ public function test_get_reports() {
+ wp_set_current_user( $this->user );
+ WC_Helper_Reports::reset_stats_dbs();
+
+ $number_of_low_stock = 3;
+ for ( $i = 1; $i <= $number_of_low_stock; $i++ ) {
+ $low_stock = new WC_Product_Simple();
+ $low_stock->set_name( "Test low stock {$i}" );
+ $low_stock->set_regular_price( 5 );
+ $low_stock->set_manage_stock( true );
+ $low_stock->set_stock_quantity( 1 );
+ $low_stock->set_stock_status( 'instock' );
+ $low_stock->save();
+ }
+
+ $number_of_out_of_stock = 6;
+ for ( $i = 1; $i <= $number_of_out_of_stock; $i++ ) {
+ $out_of_stock = new WC_Product_Simple();
+ $out_of_stock->set_name( "Test out of stock {$i}" );
+ $out_of_stock->set_regular_price( 5 );
+ $out_of_stock->set_stock_status( 'outofstock' );
+ $out_of_stock->save();
+ }
+
+ $number_of_in_stock = 10;
+ for ( $i = 1; $i <= $number_of_in_stock; $i++ ) {
+ $in_stock = new WC_Product_Simple();
+ $in_stock->set_name( "Test in stock {$i}" );
+ $in_stock->set_regular_price( 25 );
+ $in_stock->save();
+ }
+
+ $request = new WP_REST_Request( 'GET', $this->endpoint );
+ $response = $this->server->dispatch( $request );
+ $reports = $response->get_data();
+
+ $this->assertEquals( 200, $response->get_status() );
+
+ $this->assertArrayHasKey( 'totals', $reports );
+ $this->assertEquals( 19, $reports['totals']['products'] );
+ $this->assertEquals( 6, $reports['totals']['outofstock'] );
+ $this->assertEquals( 0, $reports['totals']['onbackorder'] );
+ $this->assertEquals( 3, $reports['totals']['lowstock'] );
+ $this->assertEquals( 13, $reports['totals']['instock'] );
+
+ // Test backorder and cache update.
+ $backorder_stock = new WC_Product_Simple();
+ $backorder_stock->set_name( 'Test backorder' );
+ $backorder_stock->set_regular_price( 5 );
+ $backorder_stock->set_stock_status( 'onbackorder' );
+ $backorder_stock->save();
+
+ $request = new WP_REST_Request( 'GET', $this->endpoint );
+ $response = $this->server->dispatch( $request );
+ $reports = $response->get_data();
+
+ $this->assertEquals( 200, $response->get_status() );
+
+ $this->assertEquals( 20, $reports['totals']['products'] );
+ $this->assertEquals( 6, $reports['totals']['outofstock'] );
+ $this->assertEquals( 1, $reports['totals']['onbackorder'] );
+ $this->assertEquals( 3, $reports['totals']['lowstock'] );
+ $this->assertEquals( 13, $reports['totals']['instock'] );
+ }
+
+ /**
+ * Test getting reports without valid permissions.
+ */
+ public function test_get_reports_without_permission() {
+ wp_set_current_user( 0 );
+ $response = $this->server->dispatch( new WP_REST_Request( 'GET', $this->endpoint ) );
+ $this->assertEquals( 401, $response->get_status() );
+ }
+
+ /**
+ * Test reports schema.
+ */
+ public function test_reports_schema() {
+ wp_set_current_user( $this->user );
+
+ $request = new WP_REST_Request( 'OPTIONS', $this->endpoint );
+ $response = $this->server->dispatch( $request );
+ $data = $response->get_data();
+ $properties = $data['schema']['properties'];
+
+ $this->assertCount( 1, $properties );
+ $this->assertArrayHasKey( 'totals', $properties );
+ $this->assertCount( 5, $properties['totals']['properties'] );
+ $this->assertArrayHasKey( 'products', $properties['totals']['properties'] );
+ $this->assertArrayHasKey( 'outofstock', $properties['totals']['properties'] );
+ $this->assertArrayHasKey( 'onbackorder', $properties['totals']['properties'] );
+ $this->assertArrayHasKey( 'lowstock', $properties['totals']['properties'] );
+ $this->assertArrayHasKey( 'instock', $properties['totals']['properties'] );
+ }
+}
diff --git a/plugins/woocommerce-admin/tests/reports/class-wc-tests-reports-orders-stats.php b/plugins/woocommerce-admin/tests/reports/class-wc-tests-reports-orders-stats.php
index c405dd485cb..f63384ab35a 100644
--- a/plugins/woocommerce-admin/tests/reports/class-wc-tests-reports-orders-stats.php
+++ b/plugins/woocommerce-admin/tests/reports/class-wc-tests-reports-orders-stats.php
@@ -178,9 +178,6 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
$order->set_shipping_total( 0 );
$order->set_cart_tax( 0 );
$order->save();
-
- // Wait one second to avoid potentially ambiguous new/returning customer.
- sleep( 1 ); // @todo Remove this after p90Yrv-XN-p2 is resolved.
}
WC_Helper_Queue::run_all_pending();
@@ -990,8 +987,8 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
'net_revenue' => $net_revenue,
'avg_items_per_order' => $num_items_sold / $orders_count,
'avg_order_value' => $net_revenue / $orders_count,
- 'num_returning_customers' => 0,
- 'num_new_customers' => 2,
+ 'num_returning_customers' => 2,
+ 'num_new_customers' => 0,
'products' => 2,
'segments' => array(),
// product 3 and product 4 (that is sometimes included in the orders with product 3).
@@ -1014,8 +1011,8 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
'net_revenue' => $net_revenue,
'avg_items_per_order' => $num_items_sold / $orders_count,
'avg_order_value' => $net_revenue / $orders_count,
- 'num_returning_customers' => 0,
- 'num_new_customers' => 2,
+ 'num_returning_customers' => 2,
+ 'num_new_customers' => 0,
'segments' => array(),
),
),
@@ -1064,8 +1061,8 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
'net_revenue' => $net_revenue,
'avg_items_per_order' => $num_items_sold / $orders_count,
'avg_order_value' => $net_revenue / $orders_count,
- 'num_returning_customers' => 0,
- 'num_new_customers' => 2,
+ 'num_returning_customers' => 2,
+ 'num_new_customers' => 0,
'products' => 3,
'segments' => array(),
),
@@ -1087,8 +1084,8 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
'net_revenue' => $net_revenue,
'avg_items_per_order' => $num_items_sold / $orders_count,
'avg_order_value' => $net_revenue / $orders_count,
- 'num_returning_customers' => 0,
- 'num_new_customers' => 2,
+ 'num_returning_customers' => 2,
+ 'num_new_customers' => 0,
'segments' => array(),
),
),
@@ -1136,8 +1133,8 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
'net_revenue' => $net_revenue,
'avg_items_per_order' => $num_items_sold / $orders_count,
'avg_order_value' => $net_revenue / $orders_count,
- 'num_returning_customers' => 0,
- 'num_new_customers' => 2,
+ 'num_returning_customers' => 2,
+ 'num_new_customers' => 0,
'products' => 2,
'segments' => array(),
),
@@ -1159,8 +1156,8 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
'net_revenue' => $net_revenue,
'avg_items_per_order' => $num_items_sold / $orders_count,
'avg_order_value' => $net_revenue / $orders_count,
- 'num_returning_customers' => 0,
- 'num_new_customers' => 2,
+ 'num_returning_customers' => 2,
+ 'num_new_customers' => 0,
'segments' => array(),
),
),
@@ -1211,8 +1208,8 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
'net_revenue' => $net_revenue,
'avg_items_per_order' => $num_items_sold / $orders_count,
'avg_order_value' => $net_revenue / $orders_count,
- 'num_returning_customers' => 0,
- 'num_new_customers' => 2,
+ 'num_returning_customers' => 2,
+ 'num_new_customers' => 0,
'products' => 2,
'segments' => array(),
),
@@ -1234,8 +1231,8 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
'net_revenue' => $net_revenue,
'avg_items_per_order' => $num_items_sold / $orders_count,
'avg_order_value' => $net_revenue / $orders_count,
- 'num_returning_customers' => 0,
- 'num_new_customers' => 2,
+ 'num_returning_customers' => 2,
+ 'num_new_customers' => 0,
'segments' => array(),
),
),
@@ -1288,8 +1285,8 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
'net_revenue' => $net_revenue,
'avg_items_per_order' => $num_items_sold / $orders_count,
'avg_order_value' => $net_revenue / $orders_count,
- 'num_returning_customers' => 0,
- 'num_new_customers' => 2,
+ 'num_returning_customers' => 2,
+ 'num_new_customers' => 0,
'products' => 4,
'segments' => array(),
),
@@ -1311,8 +1308,8 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
'net_revenue' => $net_revenue,
'avg_items_per_order' => $num_items_sold / $orders_count,
'avg_order_value' => $net_revenue / $orders_count,
- 'num_returning_customers' => 0,
- 'num_new_customers' => 2,
+ 'num_returning_customers' => 2,
+ 'num_new_customers' => 0,
'segments' => array(),
),
),
@@ -1362,8 +1359,8 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
'net_revenue' => $net_revenue,
'avg_items_per_order' => $num_items_sold / $orders_count,
'avg_order_value' => $net_revenue / $orders_count,
- 'num_returning_customers' => 0,
- 'num_new_customers' => 2,
+ 'num_returning_customers' => 2,
+ 'num_new_customers' => 0,
'products' => 4,
'segments' => array(),
),
@@ -1385,8 +1382,8 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
'net_revenue' => $net_revenue,
'avg_items_per_order' => $num_items_sold / $orders_count,
'avg_order_value' => $net_revenue / $orders_count,
- 'num_returning_customers' => 0,
- 'num_new_customers' => 2,
+ 'num_returning_customers' => 2,
+ 'num_new_customers' => 0,
'segments' => array(),
),
),
@@ -1591,8 +1588,8 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
'net_revenue' => $net_revenue,
'avg_items_per_order' => $num_items_sold / $orders_count,
'avg_order_value' => $net_revenue / $orders_count,
- 'num_returning_customers' => 0,
- 'num_new_customers' => $new_customers,
+ 'num_returning_customers' => 2,
+ 'num_new_customers' => 0,
'products' => 4,
'segments' => array(),
),
@@ -1614,8 +1611,8 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
'net_revenue' => $net_revenue,
'avg_items_per_order' => $num_items_sold / $orders_count,
'avg_order_value' => $net_revenue / $orders_count,
- 'num_returning_customers' => 0,
- 'num_new_customers' => $new_customers,
+ 'num_returning_customers' => 2,
+ 'num_new_customers' => 0,
'segments' => array(),
),
),
@@ -1635,18 +1632,11 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
'customer' => 'new',
);
- $orders_count = 144;
- $num_items_sold = $orders_count / 2 * $qty_per_product
- + $orders_count / 2 * $qty_per_product * 2;
- $coupons = $orders_count;
+ $orders_count = 2;
+ $num_items_sold = $orders_count * $qty_per_product;
+ $coupons = 0;
$shipping = $orders_count * 10;
- $net_revenue = $product_1_price * $qty_per_product * ( $orders_count / 6 )
- + $product_2_price * $qty_per_product * ( $orders_count / 6 )
- + $product_3_price * $qty_per_product * ( $orders_count / 6 )
- + ( $product_1_price + $product_4_price ) * $qty_per_product * ( $orders_count / 6 )
- + ( $product_2_price + $product_4_price ) * $qty_per_product * ( $orders_count / 6 )
- + ( $product_3_price + $product_4_price ) * $qty_per_product * ( $orders_count / 6 )
- - $coupons;
+ $net_revenue = $product_1_price * $qty_per_product * $orders_count;
$gross_revenue = $net_revenue + $shipping;
$expected_stats = array(
@@ -1663,7 +1653,7 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
'avg_order_value' => $net_revenue / $orders_count,
'num_returning_customers' => 0,
'num_new_customers' => $new_customers,
- 'products' => 4,
+ 'products' => 1,
'segments' => array(),
),
'intervals' => array(
@@ -1697,15 +1687,6 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
$this->assertEquals( $expected_stats, json_decode( json_encode( $data_store->get_data( $query_args ) ), true ), 'Query args: ' . print_r( $query_args, true ) . "; query: {$wpdb->last_query}" );
// ** Customer returning
- $returning_order = WC_Helper_Order::create_order( $customer_1->get_id(), $product );
- $returning_order->set_status( 'completed' );
- $returning_order->set_shipping_total( 10 );
- $returning_order->set_total( 110 ); // $25x4 products + $10 shipping.
- $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 ),
@@ -1713,15 +1694,23 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
'customer' => 'returning',
);
- $order_permutations = 72;
+ $total_orders_count = 144;
+ $returning_orders_count = 2;
$order_w_coupon_1_perms = 24;
$order_w_coupon_2_perms = 24;
- $orders_count = 1;
- $num_items_sold = 4;
- $coupons = 0;
+ $orders_count = $total_orders_count - $returning_orders_count;
+ $num_items_sold = $total_orders_count * 6 - ( $returning_orders_count * 4 );
+ $coupons = count( $this_['hour'] ) * ( $order_w_coupon_1_perms * $coupon_1_amount + $order_w_coupon_2_perms * $coupon_2_amount );
$shipping = $orders_count * 10;
- $net_revenue = 100;
+ $net_revenue = $product_1_price * $qty_per_product * ( $total_orders_count / 6 )
+ + $product_2_price * $qty_per_product * ( $total_orders_count / 6 )
+ + $product_3_price * $qty_per_product * ( $total_orders_count / 6 )
+ + ( $product_1_price + $product_4_price ) * $qty_per_product * ( $total_orders_count / 6 )
+ + ( $product_2_price + $product_4_price ) * $qty_per_product * ( $total_orders_count / 6 )
+ + ( $product_3_price + $product_4_price ) * $qty_per_product * ( $total_orders_count / 6 )
+ - $product_1_price * $qty_per_product * $returning_orders_count
+ - $coupons;
$gross_revenue = $net_revenue + $shipping;
$expected_stats = array(
@@ -1734,11 +1723,11 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
'taxes' => 0,
'shipping' => $shipping,
'net_revenue' => $net_revenue,
- 'avg_items_per_order' => $num_items_sold,
+ 'avg_items_per_order' => round( $num_items_sold / $orders_count, 4 ),
'avg_order_value' => $net_revenue / $orders_count,
- 'num_returning_customers' => 1,
+ 'num_returning_customers' => $returning_orders_count,
'num_new_customers' => 0,
- 'products' => 1,
+ 'products' => 4,
'segments' => array(),
),
'intervals' => array(
@@ -1757,9 +1746,9 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
'taxes' => 0,
'shipping' => $shipping,
'net_revenue' => $net_revenue,
- 'avg_items_per_order' => $num_items_sold,
+ 'avg_items_per_order' => round( $num_items_sold / $orders_count, 4 ),
'avg_order_value' => $net_revenue / $orders_count,
- 'num_returning_customers' => 1,
+ 'num_returning_customers' => $returning_orders_count,
'num_new_customers' => 0,
'segments' => array(),
),
@@ -1771,7 +1760,6 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
);
$this->assertEquals( $expected_stats, json_decode( json_encode( $data_store->get_data( $query_args ) ), true ), 'Query args: ' . print_r( $query_args, true ) . "; query: {$wpdb->last_query};" );
- wp_delete_post( $returning_order->get_id(), true );
// Combinations: match all
// status_is + product_includes.
@@ -1891,8 +1879,8 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
'net_revenue' => $net_revenue,
'avg_items_per_order' => $num_items_sold / $orders_count,
'avg_order_value' => $net_revenue / $orders_count,
- 'num_returning_customers' => 0,
- 'num_new_customers' => $new_customers,
+ 'num_returning_customers' => 2,
+ 'num_new_customers' => 0,
'products' => 4,
'segments' => array(),
),
@@ -1914,8 +1902,8 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
'net_revenue' => $net_revenue,
'avg_items_per_order' => $num_items_sold / $orders_count,
'avg_order_value' => $net_revenue / $orders_count,
- 'num_returning_customers' => 0,
- 'num_new_customers' => $new_customers,
+ 'num_returning_customers' => 2,
+ 'num_new_customers' => 0,
'segments' => array(),
),
),
@@ -1965,8 +1953,8 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
'net_revenue' => $net_revenue,
'avg_items_per_order' => $num_items_sold / $orders_count,
'avg_order_value' => $net_revenue / $orders_count,
- 'num_returning_customers' => 0,
- 'num_new_customers' => $new_customers,
+ 'num_returning_customers' => 2,
+ 'num_new_customers' => 0,
'products' => 2,
'segments' => array(),
),
@@ -1988,8 +1976,8 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
'net_revenue' => $net_revenue,
'avg_items_per_order' => $num_items_sold / $orders_count,
'avg_order_value' => $net_revenue / $orders_count,
- 'num_returning_customers' => 0,
- 'num_new_customers' => $new_customers,
+ 'num_returning_customers' => 2,
+ 'num_new_customers' => 0,
'segments' => array(),
),
),
@@ -2042,8 +2030,8 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
'net_revenue' => $net_revenue,
'avg_items_per_order' => $num_items_sold / $orders_count,
'avg_order_value' => $net_revenue / $orders_count,
- 'num_returning_customers' => 0,
- 'num_new_customers' => $new_customers,
+ 'num_returning_customers' => 2,
+ 'num_new_customers' => 0,
'products' => 2,
'segments' => array(),
),
@@ -2065,8 +2053,8 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
'net_revenue' => $net_revenue,
'avg_items_per_order' => $num_items_sold / $orders_count,
'avg_order_value' => $net_revenue / $orders_count,
- 'num_returning_customers' => 0,
- 'num_new_customers' => $new_customers,
+ 'num_returning_customers' => 2,
+ 'num_new_customers' => 0,
'segments' => array(),
),
),
@@ -2123,8 +2111,8 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
'net_revenue' => $net_revenue,
'avg_items_per_order' => $num_items_sold / $orders_count,
'avg_order_value' => $net_revenue / $orders_count,
- 'num_returning_customers' => 0,
- 'num_new_customers' => $new_customers,
+ 'num_returning_customers' => 2,
+ 'num_new_customers' => 0,
'products' => 2,
'segments' => array(),
),
@@ -2146,8 +2134,8 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
'net_revenue' => $net_revenue,
'avg_items_per_order' => $num_items_sold / $orders_count,
'avg_order_value' => $net_revenue / $orders_count,
- 'num_returning_customers' => 0,
- 'num_new_customers' => $new_customers,
+ 'num_returning_customers' => 2,
+ 'num_new_customers' => 0,
'segments' => array(),
),
),
@@ -2289,8 +2277,8 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
'net_revenue' => $net_revenue,
'avg_items_per_order' => $num_items_sold / $orders_count,
'avg_order_value' => $net_revenue / $orders_count,
- 'num_returning_customers' => 0,
- 'num_new_customers' => $new_customers,
+ 'num_returning_customers' => 2,
+ 'num_new_customers' => 0,
'products' => 2,
'segments' => array(),
),
@@ -2312,8 +2300,8 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
'net_revenue' => $net_revenue,
'avg_items_per_order' => $num_items_sold / $orders_count,
'avg_order_value' => $net_revenue / $orders_count,
- 'num_returning_customers' => 0,
- 'num_new_customers' => $new_customers,
+ 'num_returning_customers' => 2,
+ 'num_new_customers' => 0,
'segments' => array(),
),
),
@@ -2377,8 +2365,8 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
'net_revenue' => $net_revenue,
'avg_items_per_order' => $num_items_sold / $orders_count,
'avg_order_value' => $net_revenue / $orders_count,
- 'num_returning_customers' => 0,
- 'num_new_customers' => $new_customers,
+ 'num_returning_customers' => 2,
+ 'num_new_customers' => 0,
'products' => 2,
'segments' => array(),
),
@@ -2400,8 +2388,8 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
'net_revenue' => $net_revenue,
'avg_items_per_order' => $num_items_sold / $orders_count,
'avg_order_value' => $net_revenue / $orders_count,
- 'num_returning_customers' => 0,
- 'num_new_customers' => $new_customers,
+ 'num_returning_customers' => 2,
+ 'num_new_customers' => 0,
'segments' => array(),
),
),
@@ -4732,7 +4720,6 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
) as $order_time
) {
// Order with 1 product.
- sleep( 1 ); // @todo Remove this after p90Yrv-XN-p2 is resolved.
$order = WC_Helper_Order::create_order( $customer->get_id(), $product );
$order->set_date_created( $order_time );
$order->set_status( $order_status );
diff --git a/plugins/woocommerce-admin/wc-admin.php b/plugins/woocommerce-admin/wc-admin.php
index a1e6e5a6adf..02146e2d2d6 100755
--- a/plugins/woocommerce-admin/wc-admin.php
+++ b/plugins/woocommerce-admin/wc-admin.php
@@ -7,7 +7,7 @@
* Author URI: https://woocommerce.com/
* Text Domain: wc-admin
* Domain Path: /languages
- * Version: 0.6.0
+ * Version: 0.7.0
*
* @package WC_Admin
*/