* Add label and generated report URL to the performance indicator response.

* Hook up performance indicators to the REST API

* Handle PR feedback

* Fix setting default values
This commit is contained in:
Justin Shreve 2019-01-18 14:22:11 -05:00 committed by GitHub
commit 2f60837ba5
20 changed files with 593 additions and 191 deletions

View File

@ -19,7 +19,7 @@ import { SummaryList, SummaryListPlaceholder, SummaryNumber } from '@woocommerce
*/ */
import { getSummaryNumbers } from 'store/reports/utils'; import { getSummaryNumbers } from 'store/reports/utils';
import ReportError from 'analytics/components/report-error'; import ReportError from 'analytics/components/report-error';
import { calculateDelta, formatValue } from './utils'; import { calculateDelta, formatValue } from 'lib/number';
import withSelect from 'wc-api/with-select'; import withSelect from 'wc-api/with-select';
/** /**

View File

@ -1,42 +0,0 @@
/** @format */
/**
* External dependencies
*/
import { isFinite } from 'lodash';
/**
* WooCommerce dependencies
*/
import { formatCurrency } from '@woocommerce/currency';
/**
* Internal dependencies
*/
import { numberFormat } from 'lib/number';
export function formatValue( type, value ) {
if ( ! isFinite( value ) ) {
return null;
}
switch ( type ) {
case 'average':
return Math.round( value );
case 'currency':
return formatCurrency( value );
case 'number':
return numberFormat( value );
}
}
export function calculateDelta( primaryValue, secondaryValue ) {
if ( ! isFinite( primaryValue ) || ! isFinite( secondaryValue ) ) {
return null;
}
if ( secondaryValue === 0 ) {
return 0;
}
return Math.round( ( primaryValue - secondaryValue ) / secondaryValue * 100 );
}

View File

@ -22,7 +22,7 @@ export default class Dashboard extends Component {
<Fragment> <Fragment>
<Header sections={ [ __( 'Dashboard', 'wc-admin' ) ] } /> <Header sections={ [ __( 'Dashboard', 'wc-admin' ) ] } />
<ReportFilters query={ query } path={ path } /> <ReportFilters query={ query } path={ path } />
<StorePerformance /> <StorePerformance query={ query } />
<Leaderboards query={ query } /> <Leaderboards query={ query } />
<DashboardCharts query={ query } path={ path } /> <DashboardCharts query={ query } path={ path } />
</Fragment> </Fragment>

View File

@ -2,9 +2,19 @@
/** /**
* External dependencies * External dependencies
*/ */
import { __ } from '@wordpress/i18n'; import { __, sprintf } from '@wordpress/i18n';
import { ToggleControl } from '@wordpress/components'; import { ToggleControl } from '@wordpress/components';
import { Component, Fragment } from '@wordpress/element'; import { Component, Fragment } from '@wordpress/element';
import { compose } from '@wordpress/compose';
import { withDispatch } from '@wordpress/data';
import moment from 'moment';
import { find } from 'lodash';
/**
* WooCommerce dependencies
*/
import { getCurrentDates, appendTimestamp, getDateParamsFromQuery } from '@woocommerce/date';
import { getNewPath, getPersistedQuery } from '@woocommerce/navigation';
/** /**
* Internal dependencies * Internal dependencies
@ -16,99 +26,202 @@ import {
MenuTitle, MenuTitle,
SectionHeader, SectionHeader,
SummaryList, SummaryList,
SummaryListPlaceholder,
SummaryNumber, SummaryNumber,
} from '@woocommerce/components'; } from '@woocommerce/components';
import withSelect from 'wc-api/with-select';
import './style.scss'; import './style.scss';
import { calculateDelta, formatValue } from 'lib/number';
class StorePerformance extends Component { class StorePerformance extends Component {
constructor() { constructor( props ) {
super( ...arguments ); super( props );
this.state = { this.state = {
showCustomers: true, userPrefs: props.userPrefs || [],
showProducts: true,
showOrders: true,
}; };
this.toggle = this.toggle.bind( this ); this.toggle = this.toggle.bind( this );
} }
toggle( type ) { toggle( statKey ) {
return () => { return () => {
this.setState( state => ( { [ type ]: ! state[ type ] } ) ); this.setState( state => {
const prefs = [ ...state.userPrefs ];
let newPrefs = [];
if ( ! prefs.includes( statKey ) ) {
prefs.push( statKey );
newPrefs = prefs;
} else {
newPrefs = prefs.filter( pref => pref !== statKey );
}
this.props.updateCurrentUserData( {
dashboard_performance_indicators: newPrefs,
} );
return {
userPrefs: newPrefs,
};
} );
}; };
} }
renderMenu() { renderMenu() {
const { indicators } = this.props;
return ( return (
<EllipsisMenu label={ __( 'Choose which analytics to display', 'wc-admin' ) }> <EllipsisMenu label={ __( 'Choose which analytics to display', 'wc-admin' ) }>
<MenuTitle>{ __( 'Display Stats:', 'wc-admin' ) }</MenuTitle> <MenuTitle>{ __( 'Display Stats:', 'wc-admin' ) }</MenuTitle>
<MenuItem onInvoke={ this.toggle( 'showCustomers' ) }> { indicators.map( ( indicator, i ) => {
const checked = ! this.state.userPrefs.includes( indicator.stat );
return (
<MenuItem onInvoke={ this.toggle( indicator.stat ) } key={ i }>
<ToggleControl <ToggleControl
label={ __( 'Show Customers', 'wc-admin' ) } label={ sprintf( __( 'Show %s', 'wc-admin' ), indicator.label ) }
checked={ this.state.showCustomers } checked={ checked }
onChange={ this.toggle( 'showCustomers' ) } onChange={ this.toggle( indicator.stat ) }
/>
</MenuItem>
<MenuItem onInvoke={ this.toggle( 'showProducts' ) }>
<ToggleControl
label={ __( 'Show Products', 'wc-admin' ) }
checked={ this.state.showProducts }
onChange={ this.toggle( 'showProducts' ) }
/>
</MenuItem>
<MenuItem onInvoke={ this.toggle( 'showOrders' ) }>
<ToggleControl
label={ __( 'Show Orders', 'wc-admin' ) }
checked={ this.state.showOrders }
onChange={ this.toggle( 'showOrders' ) }
/> />
</MenuItem> </MenuItem>
);
} ) }
</EllipsisMenu> </EllipsisMenu>
); );
} }
render() { renderList() {
const totalOrders = 10; const {
const totalProducts = 1000; query,
const { showCustomers, showProducts, showOrders } = this.state; primaryRequesting,
secondaryRequesting,
primaryError,
secondaryError,
primaryData,
secondaryData,
userIndicators,
} = this.props;
if ( primaryRequesting || secondaryRequesting ) {
return <SummaryListPlaceholder numberOfItems={ userIndicators.length } />;
}
if ( primaryError || secondaryError ) {
return null;
}
const persistedQuery = getPersistedQuery( query );
const { compare } = getDateParamsFromQuery( query );
const prevLabel =
'previous_period' === compare
? __( 'Previous Period:', 'wc-admin' )
: __( 'Previous Year:', 'wc-admin' );
return (
<SummaryList>
{ userIndicators.map( ( indicator, i ) => {
const primaryItem = find( primaryData.data, data => data.stat === indicator.stat );
const secondaryItem = find( secondaryData.data, data => data.stat === indicator.stat );
if ( ! primaryItem || ! secondaryItem ) {
return null;
}
const href =
( primaryItem._links &&
primaryItem._links.report[ 0 ] &&
primaryItem._links.report[ 0 ].href ) ||
'';
const reportUrl =
( href && getNewPath( persistedQuery, href, { chart: primaryItem.chart } ) ) || '';
const delta = calculateDelta( primaryItem.value, secondaryItem.value );
const primaryValue = formatValue( primaryItem.format, primaryItem.value );
const secondaryValue = formatValue( secondaryItem.format, secondaryItem.value );
return (
<SummaryNumber
key={ i }
href={ reportUrl }
label={ indicator.label }
value={ primaryValue }
prevLabel={ prevLabel }
prevValue={ secondaryValue }
delta={ delta }
/>
);
} ) }
</SummaryList>
);
}
render() {
return ( return (
<Fragment> <Fragment>
<SectionHeader title={ __( 'Store Performance', 'wc-admin' ) } menu={ this.renderMenu() } /> <SectionHeader title={ __( 'Store Performance', 'wc-admin' ) } menu={ this.renderMenu() } />
<Card className="woocommerce-dashboard__store-performance"> <Card className="woocommerce-dashboard__store-performance">{ this.renderList() }</Card>
<SummaryList>
{ showCustomers && (
<SummaryNumber
label={ __( 'New Customers', 'wc-admin' ) }
value={ '2' }
prevLabel={ __( 'Previous Week:', 'wc-admin' ) }
prevValue={ 3 }
delta={ -33 }
/>
) }
{ showProducts && (
<SummaryNumber
label={ __( 'Total Products', 'wc-admin' ) }
value={ totalProducts }
prevLabel={ __( 'Previous Week:', 'wc-admin' ) }
prevValue={ totalProducts }
delta={ 0 }
/>
) }
{ showOrders && (
<SummaryNumber
label={ __( 'Total Orders', 'wc-admin' ) }
value={ totalOrders }
prevLabel={ __( 'Previous Week:', 'wc-admin' ) }
prevValue={ totalOrders }
delta={ 0 }
/>
) }
</SummaryList>
</Card>
</Fragment> </Fragment>
); );
} }
} }
export default compose(
withSelect( ( select, props ) => {
const { query } = props;
const {
getCurrentUserData,
getReportItems,
getReportItemsError,
isReportItemsRequesting,
} = select( 'wc-api' );
const userData = getCurrentUserData();
let userPrefs = userData.dashboard_performance_indicators;
export default StorePerformance; // Set default values for user preferences if none is set.
// These columns are HIDDEN by default.
if ( ! userPrefs ) {
userPrefs = [ 'taxes/order_tax', 'taxes/shipping_tax', 'downloads/download_count' ];
}
const datesFromQuery = getCurrentDates( query );
const endPrimary = datesFromQuery.primary.before;
const endSecondary = datesFromQuery.secondary.before;
const indicators = wcSettings.dataEndpoints.performanceIndicators;
const userIndicators = indicators.filter( indicator => ! userPrefs.includes( indicator.stat ) );
const statKeys = userIndicators.map( indicator => indicator.stat ).join( ',' );
const primaryQuery = {
after: appendTimestamp( datesFromQuery.primary.after, 'start' ),
before: appendTimestamp( endPrimary, endPrimary.isSame( moment(), 'day' ) ? 'now' : 'end' ),
stats: statKeys,
};
const secondaryQuery = {
after: appendTimestamp( datesFromQuery.secondary.after, 'start' ),
before: appendTimestamp(
endSecondary,
endSecondary.isSame( moment(), 'day' ) ? 'now' : 'end'
),
stats: statKeys,
};
const primaryData = getReportItems( 'performance-indicators', primaryQuery );
const primaryError = getReportItemsError( 'performance-indicators', primaryQuery ) || null;
const primaryRequesting = isReportItemsRequesting( 'performance-indicators', primaryQuery );
const secondaryData = getReportItems( 'performance-indicators', secondaryQuery );
const secondaryError = getReportItemsError( 'performance-indicators', secondaryQuery ) || null;
const secondaryRequesting = isReportItemsRequesting( 'performance-indicators', secondaryQuery );
return {
userPrefs,
userIndicators,
indicators,
primaryData,
primaryError,
primaryRequesting,
secondaryData,
secondaryError,
secondaryRequesting,
};
} ),
withDispatch( dispatch => {
const { updateCurrentUserData } = dispatch( 'wc-api' );
return {
updateCurrentUserData,
};
} )
)( StorePerformance );

View File

@ -2,8 +2,9 @@
/** /**
* External dependencies * External dependencies
*/ */
import { get } from 'lodash'; import { get, isFinite } from 'lodash';
const number_format = require( 'locutus/php/strings/number_format' ); const number_format = require( 'locutus/php/strings/number_format' );
import { formatCurrency } from '@woocommerce/currency';
/** /**
* Formats a number using site's current locale * Formats a number using site's current locale
@ -13,7 +14,6 @@ const number_format = require( 'locutus/php/strings/number_format' );
* @param {int|null} [precision=null] optional decimal precision * @param {int|null} [precision=null] optional decimal precision
* @returns {?String} A formatted string. * @returns {?String} A formatted string.
*/ */
export function numberFormat( number, precision = null ) { export function numberFormat( number, precision = null ) {
if ( 'number' !== typeof number ) { if ( 'number' !== typeof number ) {
number = parseFloat( number ); number = parseFloat( number );
@ -34,3 +34,30 @@ export function numberFormat( number, precision = null ) {
return number_format( number, precision, decimalSeparator, thousandSeparator ); return number_format( number, precision, decimalSeparator, thousandSeparator );
} }
export function formatValue( type, value ) {
if ( ! isFinite( value ) ) {
return null;
}
switch ( type ) {
case 'average':
return Math.round( value );
case 'currency':
return formatCurrency( value );
case 'number':
return numberFormat( value );
}
}
export function calculateDelta( primaryValue, secondaryValue ) {
if ( ! isFinite( primaryValue ) || ! isFinite( secondaryValue ) ) {
return null;
}
if ( secondaryValue === 0 ) {
return 0;
}
return Math.round( ( primaryValue - secondaryValue ) / secondaryValue * 100 );
}

View File

@ -26,6 +26,7 @@ const typeEndpointMap = {
'report-items-query-downloads': 'downloads', 'report-items-query-downloads': 'downloads',
'report-items-query-customers': 'customers', 'report-items-query-customers': 'customers',
'report-items-query-stock': 'stock', 'report-items-query-stock': 'stock',
'report-items-query-performance-indicators': 'performance-indicators',
}; };
function read( resourceNames, fetch = apiFetch ) { function read( resourceNames, fetch = apiFetch ) {

View File

@ -40,6 +40,7 @@ function updateCurrentUserData( resourceNames, data, fetch ) {
'revenue_report_columns', 'revenue_report_columns',
'taxes_report_columns', 'taxes_report_columns',
'variations_report_columns', 'variations_report_columns',
'dashboard_performance_indicators',
'dashboard_charts', 'dashboard_charts',
'dashboard_chart_type', 'dashboard_chart_type',
'dashboard_chart_interval', 'dashboard_chart_interval',

View File

@ -64,6 +64,7 @@ class WC_Admin_REST_Reports_Controller extends WC_REST_Reports_Controller {
return true; return true;
} }
/** /**
* Get all reports. * Get all reports.
* *
@ -135,7 +136,36 @@ class WC_Admin_REST_Reports_Controller extends WC_REST_Reports_Controller {
), ),
); );
/**
* Filter the list of allowed reports, so that data can be loaded from third party extensions in addition to WooCommerce core.
* Array items should be in format of array( 'slug' => 'downloads/stats', 'description' => '',
* 'url' => '', and 'path' => '/wc-ext/v1/...'.
*
* @param array $endpoints The list of allowed reports..
*/
$reports = apply_filters( 'woocommerce_admin_reports', $reports );
foreach ( $reports as $report ) { foreach ( $reports as $report ) {
if ( empty( $report['slug'] ) ) {
continue;
}
if ( empty( $report['path'] ) ) {
$report['path'] = '/' . $this->namespace . '/reports/' . $report['slug'];
}
// Allows a different admin page to be loaded here,
// or allows an empty url if no report exists for a set of performance indicators.
if ( ! isset( $report['url'] ) ) {
if ( '/stats' === substr( $report['slug'], -6 ) ) {
$url_slug = substr( $report['slug'], 0, -6 );
} else {
$url_slug = $report['slug'];
}
$report['url'] = '/analytics/' . $url_slug;
}
$item = $this->prepare_item_for_response( (object) $report, $request ); $item = $this->prepare_item_for_response( (object) $report, $request );
$data[] = $this->prepare_response_for_collection( $item ); $data[] = $this->prepare_response_for_collection( $item );
} }
@ -154,6 +184,7 @@ class WC_Admin_REST_Reports_Controller extends WC_REST_Reports_Controller {
$data = array( $data = array(
'slug' => $report->slug, 'slug' => $report->slug,
'description' => $report->description, 'description' => $report->description,
'path' => $report->path,
); );
$context = ! empty( $request['context'] ) ? $request['context'] : 'view'; $context = ! empty( $request['context'] ) ? $request['context'] : 'view';
@ -165,7 +196,10 @@ class WC_Admin_REST_Reports_Controller extends WC_REST_Reports_Controller {
$response->add_links( $response->add_links(
array( array(
'self' => array( 'self' => array(
'href' => rest_url( sprintf( '/%s/%s/%s', $this->namespace, $this->rest_base, $report->slug ) ), 'href' => rest_url( $report->path ),
),
'report' => array(
'href' => $report->url,
), ),
'collection' => array( 'collection' => array(
'href' => rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ), 'href' => rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ),
@ -208,6 +242,12 @@ class WC_Admin_REST_Reports_Controller extends WC_REST_Reports_Controller {
'context' => array( 'view' ), 'context' => array( 'view' ),
'readonly' => true, 'readonly' => true,
), ),
'path' => array(
'description' => __( 'API path.', 'wc-admin' ),
'type' => 'string',
'context' => array( 'view' ),
'readonly' => true,
),
), ),
); );

View File

@ -138,6 +138,8 @@ class WC_Admin_REST_Reports_Coupons_Stats_Controller extends WC_REST_Reports_Con
'type' => 'number', 'type' => 'number',
'context' => array( 'view', 'edit' ), 'context' => array( 'view', 'edit' ),
'readonly' => true, 'readonly' => true,
'indicator' => true,
'format' => 'currency',
), ),
'coupons_count' => array( 'coupons_count' => array(
'description' => __( 'Amount of coupons.', 'wc-admin' ), 'description' => __( 'Amount of coupons.', 'wc-admin' ),
@ -146,10 +148,11 @@ class WC_Admin_REST_Reports_Coupons_Stats_Controller extends WC_REST_Reports_Con
'readonly' => true, 'readonly' => true,
), ),
'orders_count' => array( 'orders_count' => array(
'description' => __( 'Amount of orders.', 'wc-admin' ), 'description' => __( 'Amount of discounted orders.', 'wc-admin' ),
'type' => 'integer', 'type' => 'integer',
'context' => array( 'view', 'edit' ), 'context' => array( 'view', 'edit' ),
'readonly' => true, 'readonly' => true,
'indicator' => true,
), ),
); );

View File

@ -143,6 +143,7 @@ class WC_Admin_REST_Reports_Downloads_Stats_Controller extends WC_REST_Reports_C
'type' => 'number', 'type' => 'number',
'context' => array( 'view', 'edit' ), 'context' => array( 'view', 'edit' ),
'readonly' => true, 'readonly' => true,
'indicator' => true,
), ),
); );

View File

@ -146,18 +146,22 @@ class WC_Admin_REST_Reports_Orders_Stats_Controller extends WC_Admin_REST_Report
'type' => 'number', 'type' => 'number',
'context' => array( 'view', 'edit' ), 'context' => array( 'view', 'edit' ),
'readonly' => true, 'readonly' => true,
), 'format' => 'currency',
'avg_order_value' => array(
'description' => __( 'Average order value.', 'wc-admin' ),
'type' => 'number',
'context' => array( 'view', 'edit' ),
'readonly' => true,
), ),
'orders_count' => array( 'orders_count' => array(
'description' => __( 'Amount of orders', 'wc-admin' ), 'description' => __( 'Amount of orders', 'wc-admin' ),
'type' => 'integer', 'type' => 'integer',
'context' => array( 'view', 'edit' ), 'context' => array( 'view', 'edit' ),
'readonly' => true, 'readonly' => true,
'indicator' => true,
),
'avg_order_value' => array(
'description' => __( 'Average order value.', 'wc-admin' ),
'type' => 'number',
'context' => array( 'view', 'edit' ),
'readonly' => true,
'indicator' => true,
'format' => 'currency',
), ),
'avg_items_per_order' => array( 'avg_items_per_order' => array(
'description' => __( 'Average items per order', 'wc-admin' ), 'description' => __( 'Average items per order', 'wc-admin' ),

View File

@ -31,6 +31,67 @@ class WC_Admin_REST_Reports_Performance_Indicators_Controller extends WC_REST_Re
*/ */
protected $rest_base = 'reports/performance-indicators'; protected $rest_base = 'reports/performance-indicators';
/**
* Contains a list of endpoints by report slug.
*
* @var array
*/
protected $endpoints = array();
/**
* Contains a list of allowed stats.
*
* @var array
*/
protected $allowed_stats = array();
/**
* Contains a list of stat labels.
*
* @var array
*/
protected $labels = array();
/**
* Contains a list of endpoints by url.
*
* @var array
*/
protected $urls = array();
/**
* Register the routes for reports.
*/
public function register_routes() {
register_rest_route(
$this->namespace,
'/' . $this->rest_base,
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_items' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
'args' => $this->get_collection_params(),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/allowed',
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_allowed_items' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
'args' => $this->get_collection_params(),
),
'schema' => array( $this, 'get_public_allowed_item_schema' ),
)
);
}
/** /**
* Maps query arguments from the REST request. * Maps query arguments from the REST request.
* *
@ -46,12 +107,15 @@ class WC_Admin_REST_Reports_Performance_Indicators_Controller extends WC_REST_Re
} }
/** /**
* Get all allowed stats that can be returned from this endpoint. * Get information such as allowed stats, stat labels, and endpoint data from stats reports.
* *
* @return array * @return WP_Error|True
*/ */
public function get_allowed_stats() { private function get_indicator_data() {
global $wp_rest_server; // Data already retrieved.
if ( ! empty( $this->endpoints ) && ! empty( $this->labels ) && ! empty( $this->allowed_stats ) ) {
return true;
}
$request = new WP_REST_Request( 'GET', '/wc/v4/reports' ); $request = new WP_REST_Request( 'GET', '/wc/v4/reports' );
$response = rest_do_request( $request ); $response = rest_do_request( $request );
@ -63,9 +127,10 @@ class WC_Admin_REST_Reports_Performance_Indicators_Controller extends WC_REST_Re
foreach ( $endpoints as $endpoint ) { foreach ( $endpoints as $endpoint ) {
if ( '/stats' === substr( $endpoint['slug'], -6 ) ) { if ( '/stats' === substr( $endpoint['slug'], -6 ) ) {
$request = new WP_REST_Request( 'OPTIONS', '/wc/v4/reports/' . $endpoint['slug'] ); $request = new WP_REST_Request( 'OPTIONS', $endpoint['path'] );
$response = rest_do_request( $request ); $response = rest_do_request( $request );
$data = $response->get_data(); $data = $response->get_data();
$prefix = substr( $endpoint['slug'], 0, -6 ); $prefix = substr( $endpoint['slug'], 0, -6 );
if ( empty( $data['schema']['properties']['totals']['properties'] ) ) { if ( empty( $data['schema']['properties']['totals']['properties'] ) ) {
@ -73,17 +138,113 @@ class WC_Admin_REST_Reports_Performance_Indicators_Controller extends WC_REST_Re
} }
foreach ( $data['schema']['properties']['totals']['properties'] as $property_key => $schema_info ) { foreach ( $data['schema']['properties']['totals']['properties'] as $property_key => $schema_info ) {
$allowed_stats[] = $prefix . '/' . $property_key; if ( empty( $schema_info['indicator'] ) || ! $schema_info['indicator'] ) {
continue;
} }
$stat = $prefix . '/' . $property_key;
$allowed_stats[] = $stat;
$this->labels[ $stat ] = trim( preg_replace( '/\W+/', ' ', $schema_info['description'] ) );
$this->formats[ $stat ] = isset( $schema_info['format'] ) ? $schema_info['format'] : 'number';
}
$this->endpoints[ $prefix ] = $endpoint['path'];
$this->urls[ $prefix ] = $endpoint['_links']['report'][0]['href'];
} }
} }
$this->allowed_stats = $allowed_stats;
return true;
}
/** /**
* Filter the list of allowed stats that can be returned via the performance indiciator endpoint. * Returns a list of allowed performance indicators.
* *
* @param array $allowed_stats The list of allowed stats. * @param WP_REST_Request $request Request data.
* @return array|WP_Error
*/ */
return apply_filters( 'woocommerce_admin_performance_indicators_allowed_stats', $allowed_stats ); public function get_allowed_items( $request ) {
$indicator_data = $this->get_indicator_data();
if ( is_wp_error( $indicator_data ) ) {
return $indicator_data;
}
$data = array();
foreach ( $this->allowed_stats as $stat ) {
$pieces = $this->get_stats_parts( $stat );
$report = $pieces[0];
$chart = $pieces[1];
$data[] = (object) array(
'stat' => $stat,
'chart' => $chart,
'label' => $this->labels[ $stat ],
);
}
usort( $data, array( $this, 'sort' ) );
$objects = array();
foreach ( $data as $item ) {
$prepared = $this->prepare_item_for_response( $item, $request );
$objects[] = $this->prepare_response_for_collection( $prepared );
}
$response = rest_ensure_response( $objects );
$response->header( 'X-WP-Total', count( $data ) );
$response->header( 'X-WP-TotalPages', 1 );
$base = add_query_arg( $request->get_query_params(), rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ) );
return $response;
}
/**
* Sorts the list of stats. Sorted by custom arrangement.
*
* @see https://github.com/woocommerce/wc-admin/issues/1282
* @param object $a First item.
* @param object $b Second item.
* @return order
*/
public function sort( $a, $b ) {
/**
* Custom ordering for store performance indicators.
*
* @see https://github.com/woocommerce/wc-admin/issues/1282
* @param array $indicators A list of ordered indicators.
*/
$stat_order = apply_filters(
'woocommerce_rest_report_sort_performance_indicators',
array(
'revenue/gross_revenue',
'revenue/net_revenue',
'orders/orders_count',
'orders/avg_order_value',
'products/items_sold',
'revenue/refunds',
'coupons/orders_count',
'coupons/amount',
'taxes/total_tax',
'taxes/order_tax',
'taxes/shipping_tax',
'revenue/shipping',
'downloads/download_count',
)
);
$a = array_search( $a->stat, $stat_order );
$b = array_search( $b->stat, $stat_order );
if ( false === $a && false === $b ) {
return 0;
} elseif ( false === $a ) {
return 1;
} elseif ( false === $b ) {
return -1;
} else {
return $a - $b;
}
} }
/** /**
@ -93,10 +254,9 @@ class WC_Admin_REST_Reports_Performance_Indicators_Controller extends WC_REST_Re
* @return array|WP_Error * @return array|WP_Error
*/ */
public function get_items( $request ) { public function get_items( $request ) {
$allowed_stats = $this->get_allowed_stats(); $indicator_data = $this->get_indicator_data();
if ( is_wp_error( $indicator_data ) ) {
if ( is_wp_error( $allowed_stats ) ) { return $indicator_data;
return $allowed_stats;
} }
$query_args = $this->prepare_reports_query( $request ); $query_args = $this->prepare_reports_query( $request );
@ -105,51 +265,50 @@ class WC_Admin_REST_Reports_Performance_Indicators_Controller extends WC_REST_Re
} }
$stats = array(); $stats = array();
foreach ( $query_args['stats'] as $stat_request ) { foreach ( $query_args['stats'] as $stat ) {
$is_error = false; $is_error = false;
$pieces = explode( '/', $stat_request ); $pieces = $this->get_stats_parts( $stat );
$endpoint = $pieces[0]; $report = $pieces[0];
$stat = $pieces[1]; $chart = $pieces[1];
if ( ! in_array( $stat_request, $allowed_stats ) ) { if ( ! in_array( $stat, $this->allowed_stats ) ) {
continue; continue;
} }
/** $request_url = $this->endpoints[ $report ];
* Filter the list of allowed endpoints, so that data can be loaded from extensions rather than core.
* These should be in the format of slug => path. Example: `bookings` => `/wc-bookings/v1/reports/bookings/stats`.
*
* @param array $endpoints The list of allowed endpoints.
*/
$stats_endpoints = apply_filters( 'woocommerce_admin_performance_indicators_stats_endpoints', array() );
if ( ! empty( $stats_endpoints [ $endpoint ] ) ) {
$request_url = $stats_endpoints [ $endpoint ];
} else {
$request_url = '/wc/v4/reports/' . $endpoint . '/stats';
}
$request = new WP_REST_Request( 'GET', $request_url ); $request = new WP_REST_Request( 'GET', $request_url );
$request->set_param( 'before', $query_args['before'] ); $request->set_param( 'before', $query_args['before'] );
$request->set_param( 'after', $query_args['after'] ); $request->set_param( 'after', $query_args['after'] );
$response = rest_do_request( $request ); $response = rest_do_request( $request );
$data = $response->get_data();
if ( 200 !== $response->get_status() || empty( $data['totals'][ $stat ] ) ) { $data = $response->get_data();
$format = $this->formats[ $stat ];
$label = $this->labels[ $stat ];
if ( 200 !== $response->get_status() || ! isset( $data['totals'][ $chart ] ) ) {
$stats[] = (object) array( $stats[] = (object) array(
'stat' => $stat_request, 'stat' => $stat,
'chart' => $chart,
'label' => $label,
'format' => $format,
'value' => null, 'value' => null,
); );
continue; continue;
} }
$stats[] = (object) array( $stats[] = (object) array(
'stat' => $stat_request, 'stat' => $stat,
'value' => $data['totals'][ $stat ], 'chart' => $chart,
'label' => $label,
'format' => $format,
'value' => $data['totals'][ $chart ],
); );
} }
usort( $stats, array( $this, 'sort' ) );
$objects = array(); $objects = array();
foreach ( $stats as $stat ) { foreach ( $stats as $stat ) {
$data = $this->prepare_item_for_response( $stat, $request ); $data = $this->prepare_item_for_response( $stat, $request );
@ -201,25 +360,52 @@ class WC_Admin_REST_Reports_Performance_Indicators_Controller extends WC_REST_Re
* @return array * @return array
*/ */
protected function prepare_links( $object ) { protected function prepare_links( $object ) {
$pieces = explode( '/', $object->stat ); $pieces = $this->get_stats_parts( $object->stat );
$endpoint = $pieces[0]; $endpoint = $pieces[0];
$stat = $pieces[1]; $stat = $pieces[1];
$url = $this->urls[ $endpoint ];
$links = array( $links = array(
'api' => array(
'href' => rest_url( $this->endpoints[ $endpoint ] ),
),
'report' => array( 'report' => array(
'href' => rest_url( sprintf( '/%s/reports/%s/stats', $this->namespace, $endpoint ) ), 'href' => ! empty( $url ) ? $url : '',
), ),
); );
return $links; return $links;
} }
/**
* Returns the endpoint part of a stat request (prefix) and the actual stat total we want.
* To allow extensions to namespace (example: fue/emails/sent), we break on the last forward slash.
*
* @param string $full_stat A stat request string like orders/avg_order_value or fue/emails/sent.
* @return array Containing the prefix (endpoint) and suffix (stat).
*/
private function get_stats_parts( $full_stat ) {
$endpoint = substr( $full_stat, 0, strrpos( $full_stat, '/' ) );
$stat = substr( $full_stat, ( strrpos( $full_stat, '/' ) + 1 ) );
return array(
$endpoint,
$stat,
);
}
/** /**
* Get the Report's schema, conforming to JSON Schema. * Get the Report's schema, conforming to JSON Schema.
* *
* @return array * @return array
*/ */
public function get_item_schema() { public function get_item_schema() {
$indicator_data = $this->get_indicator_data();
if ( is_wp_error( $indicator_data ) ) {
$allowed_stats = array();
} else {
$allowed_stats = $this->allowed_stats;
}
$schema = array( $schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#', '$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'report_performance_indicator', 'title' => 'report_performance_indicator',
@ -230,6 +416,26 @@ class WC_Admin_REST_Reports_Performance_Indicators_Controller extends WC_REST_Re
'type' => 'string', 'type' => 'string',
'context' => array( 'view', 'edit' ), 'context' => array( 'view', 'edit' ),
'readonly' => true, 'readonly' => true,
'enum' => $allowed_stats,
),
'chart' => array(
'description' => __( 'The specific chart this stat referrers to.', 'wc-admin' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'label' => array(
'description' => __( 'Human readable label for the stat.', 'wc-admin' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'format' => array(
'description' => __( 'Format of the stat.', 'wc-admin' ),
'type' => 'number',
'context' => array( 'view', 'edit' ),
'readonly' => true,
'enum' => array( 'number', 'currency' ),
), ),
'value' => array( 'value' => array(
'description' => __( 'Value of the stat. Returns null if the stat does not exist or cannot be loaded.', 'wc-admin' ), 'description' => __( 'Value of the stat. Returns null if the stat does not exist or cannot be loaded.', 'wc-admin' ),
@ -243,17 +449,29 @@ class WC_Admin_REST_Reports_Performance_Indicators_Controller extends WC_REST_Re
return $this->add_additional_fields_schema( $schema ); return $this->add_additional_fields_schema( $schema );
} }
/**
* Get schema for the list of allowed performance indicators.
*
* @return array $schema
*/
public function get_public_allowed_item_schema() {
$schema = $this->get_public_item_schema();
unset( $schema['properties']['value'] );
unset( $schema['properties']['format'] );
return $sceham;
}
/** /**
* Get the query params for collections. * Get the query params for collections.
* *
* @return array * @return array
*/ */
public function get_collection_params() { public function get_collection_params() {
$allowed_stats = $this->get_allowed_stats(); $indicator_data = $this->get_indicator_data();
if ( is_wp_error( $allowed_stats ) ) { if ( is_wp_error( $indicator_data ) ) {
$allowed_stats = __( 'There was an issue loading the report endpoints', 'wc-admin' ); $allowed_stats = __( 'There was an issue loading the report endpoints', 'wc-admin' );
} else { } else {
$allowed_stats = implode( ', ', $allowed_stats ); $allowed_stats = implode( ', ', $this->allowed_stats );
} }
$params = array(); $params = array();

View File

@ -152,12 +152,14 @@ class WC_Admin_REST_Reports_Products_Stats_Controller extends WC_REST_Reports_Co
'type' => 'integer', 'type' => 'integer',
'context' => array( 'view', 'edit' ), 'context' => array( 'view', 'edit' ),
'readonly' => true, 'readonly' => true,
'indicator' => true,
), ),
'net_revenue' => array( 'net_revenue' => array(
'description' => __( 'Net revenue.', 'wc-admin' ), 'description' => __( 'Net revenue.', 'wc-admin' ),
'type' => 'number', 'type' => 'number',
'context' => array( 'view', 'edit' ), 'context' => array( 'view', 'edit' ),
'readonly' => true, 'readonly' => true,
'format' => 'currency',
), ),
'orders_count' => array( 'orders_count' => array(
'description' => __( 'Number of orders.', 'wc-admin' ), 'description' => __( 'Number of orders.', 'wc-admin' ),

View File

@ -136,51 +136,61 @@ class WC_Admin_REST_Reports_Revenue_Stats_Controller extends WC_REST_Reports_Con
'type' => 'number', 'type' => 'number',
'context' => array( 'view', 'edit' ), 'context' => array( 'view', 'edit' ),
'readonly' => true, 'readonly' => true,
'indicator' => true,
'format' => 'currency',
), ),
'net_revenue' => array( 'net_revenue' => array(
'description' => __( 'Net revenue.', 'wc-admin' ), 'description' => __( 'Net revenue.', 'wc-admin' ),
'type' => 'number', 'type' => 'number',
'context' => array( 'view', 'edit' ), 'context' => array( 'view', 'edit' ),
'readonly' => true, 'readonly' => true,
'indicator' => true,
'format' => 'currency',
), ),
'coupons' => array( 'coupons' => array(
'description' => __( 'Total of coupons.', 'wc-admin' ), 'description' => __( 'Total of coupons.', 'wc-admin' ),
'type' => 'number', 'type' => 'number',
'context' => array( 'view', 'edit' ), 'context' => array( 'view', 'edit' ),
'readonly' => true, 'readonly' => true,
'format' => 'currency',
), ),
'shipping' => array( 'shipping' => array(
'description' => __( 'Total of shipping.', 'wc-admin' ), 'description' => __( 'Total of shipping.', 'wc-admin' ),
'type' => 'number', 'type' => 'number',
'context' => array( 'view', 'edit' ), 'context' => array( 'view', 'edit' ),
'readonly' => true, 'readonly' => true,
'indicator' => true,
'format' => 'currency',
), ),
'taxes' => array( 'taxes' => array(
'description' => __( 'Total of taxes.', 'wc-admin' ), 'description' => __( 'Total of taxes.', 'wc-admin' ),
'type' => 'number', 'type' => 'number',
'context' => array( 'view', 'edit' ), 'context' => array( 'view', 'edit' ),
'readonly' => true, 'readonly' => true,
'format' => 'currency',
), ),
'refunds' => array( 'refunds' => array(
'description' => __( 'Total of refunds.', 'wc-admin' ), 'description' => __( 'Total of refunds.', 'wc-admin' ),
'type' => 'number', 'type' => 'number',
'context' => array( 'view', 'edit' ), 'context' => array( 'view', 'edit' ),
'readonly' => true, 'readonly' => true,
'indicator' => true,
'format' => 'currency',
), ),
'orders_count' => array( 'orders_count' => array(
'description' => __( 'Amount of orders', 'wc-admin' ), 'description' => __( 'Amount of orders.', 'wc-admin' ),
'type' => 'integer', 'type' => 'integer',
'context' => array( 'view', 'edit' ), 'context' => array( 'view', 'edit' ),
'readonly' => true, 'readonly' => true,
), ),
'num_items_sold' => array( 'num_items_sold' => array(
'description' => __( 'Amount of orders', 'wc-admin' ), 'description' => __( 'Items sold.', 'wc-admin' ),
'type' => 'integer', 'type' => 'integer',
'context' => array( 'view', 'edit' ), 'context' => array( 'view', 'edit' ),
'readonly' => true, 'readonly' => true,
), ),
'products' => array( 'products' => array(
'description' => __( 'Amount of orders', 'wc-admin' ), 'description' => __( 'Products sold.', 'wc-admin' ),
'type' => 'integer', 'type' => 'integer',
'context' => array( 'view', 'edit' ), 'context' => array( 'view', 'edit' ),
'readonly' => true, 'readonly' => true,

View File

@ -167,18 +167,24 @@ class WC_Admin_REST_Reports_Taxes_Stats_Controller extends WC_REST_Reports_Contr
'type' => 'number', 'type' => 'number',
'context' => array( 'view', 'edit' ), 'context' => array( 'view', 'edit' ),
'readonly' => true, 'readonly' => true,
'indicator' => true,
'format' => 'currency',
), ),
'order_tax' => array( 'order_tax' => array(
'description' => __( 'Order tax.', 'wc-admin' ), 'description' => __( 'Order tax.', 'wc-admin' ),
'type' => 'number', 'type' => 'number',
'context' => array( 'view', 'edit' ), 'context' => array( 'view', 'edit' ),
'readonly' => true, 'readonly' => true,
'indicator' => true,
'format' => 'currency',
), ),
'shipping_tax' => array( 'shipping_tax' => array(
'description' => __( 'Shipping tax.', 'wc-admin' ), 'description' => __( 'Shipping tax.', 'wc-admin' ),
'type' => 'number', 'type' => 'number',
'context' => array( 'view', 'edit' ), 'context' => array( 'view', 'edit' ),
'readonly' => true, 'readonly' => true,
'indicator' => true,
'format' => 'currency',
), ),
'orders_count' => array( 'orders_count' => array(
'description' => __( 'Amount of orders.', 'wc-admin' ), 'description' => __( 'Amount of orders.', 'wc-admin' ),

View File

@ -399,6 +399,7 @@ function wc_admin_get_user_data_fields() {
'revenue_report_columns', 'revenue_report_columns',
'taxes_report_columns', 'taxes_report_columns',
'variations_report_columns', 'variations_report_columns',
'dashboard_performance_indicators',
'dashboard_charts', 'dashboard_charts',
'dashboard_chart_type', 'dashboard_chart_type',
'dashboard_chart_interval', 'dashboard_chart_interval',

View File

@ -154,6 +154,7 @@ function wc_admin_print_script_settings() {
$preload_data_endpoints = array( $preload_data_endpoints = array(
'countries' => '/wc/v4/data/countries', 'countries' => '/wc/v4/data/countries',
'performanceIndicators' => '/wc/v4/reports/performance-indicators/allowed',
); );
if ( function_exists( 'gutenberg_preload_api_request' ) ) { if ( function_exists( 'gutenberg_preload_api_request' ) ) {
@ -169,7 +170,7 @@ function wc_admin_print_script_settings() {
$current_user_data = array(); $current_user_data = array();
foreach ( wc_admin_get_user_data_fields() as $user_field ) { foreach ( wc_admin_get_user_data_fields() as $user_field ) {
$current_user_data[ $user_field ] = get_user_meta( get_current_user_id(), 'wc_admin_' . $user_field, true ); $current_user_data[ $user_field ] = json_decode( get_user_meta( get_current_user_id(), 'wc_admin_' . $user_field, true ) );
} }
/** /**

View File

@ -1,5 +1,6 @@
# 1.4.1 (unreleased) # 1.4.1 (unreleased)
- Chart component: format numbers and prices using store currency settings. - Chart component: format numbers and prices using store currency settings.
- Make `href`/linking optional in SummaryNumber.
# 1.4.0 # 1.4.0
- Add download log ip address autocompleter to search component - Add download log ip address autocompleter to search component

View File

@ -52,11 +52,14 @@ const SummaryNumber = ( {
screenReaderLabel = sprintf( __( 'No change from %s', 'wc-admin' ), prevLabel ); screenReaderLabel = sprintf( __( 'No change from %s', 'wc-admin' ), prevLabel );
} }
const Container = onToggle ? Button : Link; let Container;
const containerProps = { const containerProps = {
className: classes, className: classes,
'aria-current': selected ? 'page' : null, 'aria-current': selected ? 'page' : null,
}; };
if ( onToggle || href ) {
Container = onToggle ? Button : Link;
if ( ! onToggle ) { if ( ! onToggle ) {
containerProps.href = href; containerProps.href = href;
containerProps.role = 'menuitem'; containerProps.role = 'menuitem';
@ -64,6 +67,9 @@ const SummaryNumber = ( {
containerProps.onClick = onToggle; containerProps.onClick = onToggle;
containerProps[ 'aria-expanded' ] = isOpen; containerProps[ 'aria-expanded' ] = isOpen;
} }
} else {
Container = 'div';
}
return ( return (
<li className={ liClasses }> <li className={ liClasses }>
@ -109,7 +115,7 @@ SummaryNumber.propTypes = {
/** /**
* An internal link to the report focused on this number. * An internal link to the report focused on this number.
*/ */
href: PropTypes.string.isRequired, href: PropTypes.string,
/** /**
* Boolean describing whether the menu list is open. Only applies in mobile view, * Boolean describing whether the menu list is open. Only applies in mobile view,
* and only applies to the toggle-able item (first in the list). * and only applies to the toggle-able item (first in the list).
@ -147,7 +153,7 @@ SummaryNumber.propTypes = {
}; };
SummaryNumber.defaultProps = { SummaryNumber.defaultProps = {
href: '/analytics', href: '',
isOpen: false, isOpen: false,
prevLabel: __( 'Previous Period:', 'wc-admin' ), prevLabel: __( 'Previous Period:', 'wc-admin' ),
reverseTrend: false, reverseTrend: false,

View File

@ -37,6 +37,7 @@ class WC_Tests_API_Reports_Performance_Indicators extends WC_REST_Unit_Test_Case
$routes = $this->server->get_routes(); $routes = $this->server->get_routes();
$this->assertArrayHasKey( $this->endpoint, $routes ); $this->assertArrayHasKey( $this->endpoint, $routes );
$this->assertArrayHasKey( $this->endpoint . '/allowed', $routes );
} }
/** /**
@ -96,14 +97,19 @@ class WC_Tests_API_Reports_Performance_Indicators extends WC_REST_Unit_Test_Case
$reports = $response->get_data(); $reports = $response->get_data();
$this->assertEquals( 200, $response->get_status() ); $this->assertEquals( 200, $response->get_status() );
$this->assertEquals( 2, count( $reports ) ); $this->assertEquals( 2, count( $reports ) );
$this->assertEquals( 'orders/orders_count', $reports[0]['stat'] ); $this->assertEquals( 'orders/orders_count', $reports[0]['stat'] );
$this->assertEquals( 'Amount of orders', $reports[0]['label'] );
$this->assertEquals( 1, $reports[0]['value'] ); $this->assertEquals( 1, $reports[0]['value'] );
$this->assertEquals( 'orders_count', $reports[0]['chart'] );
$this->assertEquals( '/analytics/orders', $response->data[0]['_links']['report'][0]['href'] );
$this->assertEquals( 'downloads/download_count', $reports[1]['stat'] ); $this->assertEquals( 'downloads/download_count', $reports[1]['stat'] );
$this->assertEquals( 'Number of downloads', $reports[1]['label'] );
$this->assertEquals( 2, $reports[1]['value'] ); $this->assertEquals( 2, $reports[1]['value'] );
$this->assertEquals( 'download_count', $reports[1]['chart'] );
$this->assertEquals( '/analytics/downloads', $response->data[1]['_links']['report'][0]['href'] );
} }
/** /**
@ -148,8 +154,11 @@ class WC_Tests_API_Reports_Performance_Indicators extends WC_REST_Unit_Test_Case
$data = $response->get_data(); $data = $response->get_data();
$properties = $data['schema']['properties']; $properties = $data['schema']['properties'];
$this->assertEquals( 2, count( $properties ) ); $this->assertEquals( 5, count( $properties ) );
$this->assertArrayHasKey( 'stat', $properties ); $this->assertArrayHasKey( 'stat', $properties );
$this->assertArrayHasKey( 'chart', $properties );
$this->assertArrayHasKey( 'label', $properties );
$this->assertArrayHasKey( 'format', $properties );
$this->assertArrayHasKey( 'value', $properties ); $this->assertArrayHasKey( 'value', $properties );
} }
} }