fix merge conflicts

This commit is contained in:
Ron Rennick 2019-02-14 14:11:15 -04:00
commit f5efd10638
50 changed files with 1341 additions and 907 deletions

View File

@ -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,
} ) ),
},
},

View File

@ -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,

View File

@ -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' },
],
},
];

View File

@ -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',

View File

@ -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 ) ? (
<Link
href={ getNewPath( getPersistedQuery(), section[ 0 ], {} ) }
href={ getNewPath( {}, section[ 0 ], {} ) }
type={ isEmbedded ? 'wp-admin' : 'wc-admin' }
>
{ section[ 1 ] }

View File

@ -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

View File

@ -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',
};

View File

@ -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`.
@ -137,9 +140,10 @@ export function getQueryFromConfig( config, advancedFilters, query ) {
* Returns true if a report object is empty.
*
* @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,7 +174,11 @@ function getRequestQuery( endpoint, dataType, query ) {
const interval = getIntervalForQuery( query );
const filterQuery = getFilterQuery( endpoint, query );
const end = datesFromQuery[ dataType ].before;
return {
const noIntervals = includes( noIntervalEndpoints, endpoint );
return noIntervals
? { ...filterQuery }
: {
order: 'asc',
interval,
per_page: MAX_PER_PAGE,
@ -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 };
}

View File

@ -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.

View File

@ -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.

View File

@ -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`.

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 );

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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,
),

View File

@ -0,0 +1,138 @@
<?php
/**
* REST API Reports stock stats controller
*
* Handles requests to the /reports/stock/stats endpoint.
*
* @package WooCommerce Admin/API
*/
defined( 'ABSPATH' ) || exit;
/**
* REST API Reports stock stats controller class.
*
* @package WooCommerce/API
* @extends WC_REST_Reports_Controller
*/
class WC_Admin_REST_Reports_Stock_Stats_Controller extends WC_REST_Reports_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v4';
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'reports/stock/stats';
/**
* Get Stock Status Totals.
*
* @param WP_REST_Request $request Request data.
* @return array|WP_Error
*/
public function get_items( $request ) {
$stock_query = new WC_Admin_Reports_Stock_Stats_Query();
$report_data = $stock_query->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;
}
}

View File

@ -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',
@ -382,6 +386,7 @@ class WC_Admin_Api_Init {
'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',
)
);
}

View File

@ -0,0 +1,29 @@
<?php
/**
* Class for stock stats report querying
*
* $report = new WC_Admin_Reports_Stock__Stats_Query();
* $mydata = $report->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 );
}
}

View File

@ -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.
*

View File

@ -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;
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;
}
/**

View File

@ -0,0 +1,138 @@
<?php
/**
* WC_Admin_Reports_Stock_Stats_Data_Store class file.
*
* @package WooCommerce Admin/Classes
*/
defined( 'ABSPATH' ) || exit;
/**
* WC_Reports_Stock_Stats_Data_Store.
*/
class WC_Admin_Reports_Stock_Stats_Data_Store extends WC_Admin_Reports_Data_Store implements WC_Admin_Reports_Data_Store_Interface {
/**
* Get stock counts for the whole store.
*
* @param array $query Not used for the stock stats data store, but needed for the interface.
* @return array Array of counts.
*/
public function get_data( $query ) {
$report_data = array();
$cache_expire = DAY_IN_SECONDS * 30;
$low_stock_transient_name = 'wc_admin_stock_count_lowstock';
$low_stock_count = get_transient( $low_stock_transient_name );
if ( false === $low_stock_count ) {
$low_stock_count = $this->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' );

View File

@ -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
}
}
},

View File

@ -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",

View File

@ -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 (
<div
@ -182,12 +170,12 @@ class D3Chart extends Component {
{ this.getEmptyMessage() }
<div className="d3-chart__tooltip" ref={ this.tooltipRef } />
<D3Base
className={ classNames( this.props.className ) }
className={ classNames( className ) }
data={ data }
drawChart={ this.drawChart }
height={ height }
orderedKeys={ this.props.orderedKeys }
tooltipRef={ this.tooltipRef }
orderedKeys={ orderedKeys }
tooltip={ this.tooltip }
chartType={ chartType }
width={ computedWidth }
/>

View File

@ -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 (
<div className={ classNames( 'd3-base', this.props.className ) } ref={ this.chartRef } onScroll={ this.delayedScroll } />
<div className={ classNames( 'd3-base', className ) } ref={ this.chartRef } onScroll={ this.delayedScroll() } />
);
}
}
@ -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,
};

View File

@ -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 );
}
}

View File

@ -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 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( '' )
);
};
const drawYAxis = ( node, scales, formats, margin ) => {
const yGrids = getYGrids( scales.yScale.domain()[ 1 ] );
const width = scales.xScale.range()[ 1 ];
if ( yGrids ) {
node
.append( 'g' )
.attr( 'class', 'grid' )
.attr( 'transform', `translate(-${ params.margin.left }, 0)` )
.attr( 'transform', `translate(-${ margin.left }, 0)` )
.call(
d3AxisLeft( params.yScale )
d3AxisLeft( scales.yScale )
.tickValues( yGrids )
.tickSize( -params.width - params.margin.left - params.margin.right )
.tickSize( -width - margin.left - margin.right )
.tickFormat( '' )
)
.call( g => g.select( '.domain' ).remove() );
);
node
.append( 'g' )
.attr( 'class', 'axis y-axis' )
.attr( 'aria-hidden', 'true' )
.attr( 'transform', 'translate(-50, 0)' )
.attr( 'transform', 'translate(-50, 12)' )
.attr( 'text-anchor', 'start' )
.call(
d3AxisLeft( params.yTickOffset )
.tickValues( params.yMax === 0 ? [ yGrids[ 0 ] ] : yGrids )
.tickFormat( d => params.yFormat( d !== 0 ? d : 0 ) )
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();
};

View File

@ -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() );
};

View File

@ -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.

View File

@ -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() );
};

View File

@ -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 ] );

View File

@ -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 = [

View File

@ -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' );
} );
} );
} );
} );

View File

@ -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 ] );
} );
} );
} );

View File

@ -11,130 +11,137 @@ 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 = (
calculateXPosition(
elementCoords,
chartCoords,
tooltipSize,
tooltipMargin,
elementWidthRatio,
tooltipPosition
) => {
) {
const tooltipSize = this.ref.getBoundingClientRect();
const d3BaseCoords = d3Select( '.d3-base' ).node().getBoundingClientRect();
const leftMargin = Math.max( d3BaseCoords.left, chartCoords.left );
if ( tooltipPosition === 'below' ) {
if ( this.position === 'below' ) {
return Math.max(
tooltipMargin,
this.margin,
Math.min(
elementCoords.left + elementCoords.width * 0.5 - tooltipSize.width / 2 - leftMargin,
d3BaseCoords.width - tooltipSize.width - tooltipMargin
d3BaseCoords.width - tooltipSize.width - this.margin
)
);
}
const xPosition =
elementCoords.left + elementCoords.width * elementWidthRatio + tooltipMargin - leftMargin;
elementCoords.left + elementCoords.width * elementWidthRatio + this.margin - leftMargin;
if ( xPosition + tooltipSize.width + tooltipMargin > d3BaseCoords.width ) {
if ( xPosition + tooltipSize.width + this.margin > d3BaseCoords.width ) {
return Math.max(
tooltipMargin,
this.margin,
elementCoords.left +
elementCoords.width * ( 1 - elementWidthRatio ) -
tooltipSize.width -
tooltipMargin -
this.margin -
leftMargin
);
}
return xPosition;
};
}
const calculateTooltipYPosition = (
calculateYPosition(
elementCoords,
chartCoords,
tooltipSize,
tooltipMargin,
tooltipPosition
) => {
if ( tooltipPosition === 'below' ) {
) {
if ( this.position === '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 );
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;
};
}
export const calculateTooltipPosition = ( element, chart, tooltipPosition, elementWidthRatio = 1 ) => {
calculatePosition( element, elementWidthRatio = 1 ) {
const elementCoords = element.getBoundingClientRect();
const chartCoords = chart.getBoundingClientRect();
const tooltipSize = d3Select( '.d3-chart__tooltip' )
.node()
.getBoundingClientRect();
const tooltipMargin = 24;
const chartCoords = this.chart.getBoundingClientRect();
if ( tooltipPosition === 'below' ) {
if ( this.position === 'below' ) {
elementWidthRatio = 0;
}
return {
x: calculateTooltipXPosition(
x: this.calculateXPosition(
elementCoords,
chartCoords,
tooltipSize,
tooltipMargin,
elementWidthRatio,
tooltipPosition
),
y: calculateTooltipYPosition(
y: this.calculateYPosition(
elementCoords,
chartCoords,
tooltipSize,
tooltipMargin,
tooltipPosition
),
};
};
}
const getTooltipRowLabel = ( d, row, params ) => {
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 params.tooltipLabelFormat( moment( d[ row.key ].labelDate ).toDate() );
return this.labelFormat( moment( d[ row.key ].labelDate ).toDate() );
}
return row.key;
};
}
export const showTooltip = ( params, d, position ) => {
const keys = params.visibleKeys.map(
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 => `
<li class="key-row">
<div class="key-container">
<span
class="key-color"
style="background-color:${ getColor( row.key, params.visibleKeys, params.colorScheme ) }">
style="background-color: ${ getColor( row.key, this.visibleKeys, this.colorScheme ) }">
</span>
<span class="key-key">${ getTooltipRowLabel( d, row, params ) }</span>
<span class="key-key">${ this.getTooltipRowLabel( d, row ) }</span>
</div>
<span class="key-value">${ params.tooltipValueFormat( d[ row.key ].value ) }</span>
<span class="key-value">${ this.valueFormat( d[ row.key ].value ) }</span>
</li>
`
);
const tooltipTitle = params.tooltipTitle
? params.tooltipTitle
: params.tooltipLabelFormat( moment( d.date ).toDate() );
const tooltipTitle = this.title
? this.title
: this.labelFormat( moment( d.date ).toDate() );
d3Select( params.tooltip )
d3Select( this.ref )
.style( 'left', position.x + 'px' )
.style( 'top', position.y + 'px' )
.style( 'visibility', 'visible' ).html( `
@ -145,4 +152,7 @@ export const showTooltip = ( params, d, position ) => {
</ul>
</div>
` );
};
}
}
export default ChartTooltip;

View File

@ -40,8 +40,13 @@ class SearchFilter extends Component {
}
updateLabels( 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 ) {
this.setState( {

View File

@ -1,6 +1,5 @@
/**
* Internal Dependencies
*/
@import 'segmented-selection/ie.scss';
@import 'summary/ie.scss';
@import 'table/ie.scss';

View File

@ -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();
}

View File

@ -20,6 +20,7 @@ import Flag from '../../flag';
export default {
name: 'countries',
className: 'woocommerce-search__country-result',
isDebounced: true,
options() {
return wcSettings.dataEndpoints.countries || [];
},

View File

@ -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 [
<span key="name" className="woocommerce-search__result-name" aria-label={ getName( customer ) }>
<span key="name" className="woocommerce-search__result-name" aria-label={ customer.name }>
{ match.suggestionBeforeMatch }
<strong className="components-form-token-field__suggestion-match">
{ match.suggestionMatch }
@ -78,7 +76,7 @@ export default {
getOptionCompletion( customer ) {
return {
id: customer.id,
label: getName( customer ),
label: customer.name,
};
},
};

View File

@ -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;
}
}

View File

@ -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;

View File

@ -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,7 +156,7 @@ 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',
'search' => 'Jeff',
'last_order_after' => date( 'Y-m-d' ) . 'T00:00:00Z',
)
);

View File

@ -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,7 +175,7 @@ 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',
'search' => 'Justin',
'last_order_after' => date( 'Y-m-d' ) . 'T00:00:00Z',
)
);

View File

@ -0,0 +1,139 @@
<?php
/**
* Reports Stock Stats REST API Test
*
* @package WooCommerce\Tests\API
*/
/**
* Class WC_Tests_API_Reports_Stock_Stats
*/
class WC_Tests_API_Reports_Stock_Stats extends WC_REST_Unit_Test_Case {
/**
* Endpoints.
*
* @var string
*/
protected $endpoint = '/wc/v4/reports/stock/stats';
/**
* Setup test reports stock data.
*/
public function setUp() {
parent::setUp();
$this->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'] );
}
}

View File

@ -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 );

View File

@ -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
*/