diff --git a/plugins/woocommerce-admin/client/analytics/components/report-summary/index.js b/plugins/woocommerce-admin/client/analytics/components/report-summary/index.js index 54fc4581a25..11aeb5701c8 100644 --- a/plugins/woocommerce-admin/client/analytics/components/report-summary/index.js +++ b/plugins/woocommerce-admin/client/analytics/components/report-summary/index.js @@ -19,7 +19,7 @@ import { SummaryList, SummaryListPlaceholder, SummaryNumber } from '@woocommerce */ import { getSummaryNumbers } from 'store/reports/utils'; 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'; /** diff --git a/plugins/woocommerce-admin/client/analytics/components/report-summary/utils.js b/plugins/woocommerce-admin/client/analytics/components/report-summary/utils.js deleted file mode 100644 index a4669da29c3..00000000000 --- a/plugins/woocommerce-admin/client/analytics/components/report-summary/utils.js +++ /dev/null @@ -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 ); -} diff --git a/plugins/woocommerce-admin/client/dashboard/index.js b/plugins/woocommerce-admin/client/dashboard/index.js index bd200f5efdc..91457ab5139 100644 --- a/plugins/woocommerce-admin/client/dashboard/index.js +++ b/plugins/woocommerce-admin/client/dashboard/index.js @@ -22,7 +22,7 @@ export default class Dashboard extends Component {
- + diff --git a/plugins/woocommerce-admin/client/dashboard/store-performance/index.js b/plugins/woocommerce-admin/client/dashboard/store-performance/index.js index 849b226e740..a94ed659270 100644 --- a/plugins/woocommerce-admin/client/dashboard/store-performance/index.js +++ b/plugins/woocommerce-admin/client/dashboard/store-performance/index.js @@ -2,9 +2,19 @@ /** * External dependencies */ -import { __ } from '@wordpress/i18n'; +import { __, sprintf } from '@wordpress/i18n'; import { ToggleControl } from '@wordpress/components'; 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 @@ -16,99 +26,202 @@ import { MenuTitle, SectionHeader, SummaryList, + SummaryListPlaceholder, SummaryNumber, } from '@woocommerce/components'; +import withSelect from 'wc-api/with-select'; import './style.scss'; +import { calculateDelta, formatValue } from 'lib/number'; class StorePerformance extends Component { - constructor() { - super( ...arguments ); + constructor( props ) { + super( props ); this.state = { - showCustomers: true, - showProducts: true, - showOrders: true, + userPrefs: props.userPrefs || [], }; - this.toggle = this.toggle.bind( this ); } - toggle( type ) { + toggle( statKey ) { 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() { + const { indicators } = this.props; return ( { __( 'Display Stats:', 'wc-admin' ) } - - - - - - - - - + { indicators.map( ( indicator, i ) => { + const checked = ! this.state.userPrefs.includes( indicator.stat ); + return ( + + + + ); + } ) } ); } - render() { - const totalOrders = 10; - const totalProducts = 1000; - const { showCustomers, showProducts, showOrders } = this.state; + renderList() { + const { + query, + primaryRequesting, + secondaryRequesting, + primaryError, + secondaryError, + primaryData, + secondaryData, + userIndicators, + } = this.props; + if ( primaryRequesting || secondaryRequesting ) { + return ; + } + 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 ( + + { 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 ( + + ); + } ) } + + ); + } + + render() { return ( - - - { showCustomers && ( - - ) } - { showProducts && ( - - ) } - { showOrders && ( - - ) } - - + { this.renderList() } ); } } +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 ); diff --git a/plugins/woocommerce-admin/client/lib/number/index.js b/plugins/woocommerce-admin/client/lib/number/index.js index 1cc17027cbe..c472eee0d5d 100644 --- a/plugins/woocommerce-admin/client/lib/number/index.js +++ b/plugins/woocommerce-admin/client/lib/number/index.js @@ -2,8 +2,9 @@ /** * External dependencies */ -import { get } from 'lodash'; +import { get, isFinite } from 'lodash'; const number_format = require( 'locutus/php/strings/number_format' ); +import { formatCurrency } from '@woocommerce/currency'; /** * 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 * @returns {?String} A formatted string. */ - export function numberFormat( number, precision = null ) { if ( 'number' !== typeof number ) { number = parseFloat( number ); @@ -34,3 +34,30 @@ export function numberFormat( number, precision = null ) { 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 ); +} diff --git a/plugins/woocommerce-admin/client/wc-api/reports/items/operations.js b/plugins/woocommerce-admin/client/wc-api/reports/items/operations.js index a850a119c60..ae7dd63f420 100644 --- a/plugins/woocommerce-admin/client/wc-api/reports/items/operations.js +++ b/plugins/woocommerce-admin/client/wc-api/reports/items/operations.js @@ -26,6 +26,7 @@ const typeEndpointMap = { 'report-items-query-downloads': 'downloads', 'report-items-query-customers': 'customers', 'report-items-query-stock': 'stock', + 'report-items-query-performance-indicators': 'performance-indicators', }; function read( resourceNames, fetch = apiFetch ) { diff --git a/plugins/woocommerce-admin/client/wc-api/user/operations.js b/plugins/woocommerce-admin/client/wc-api/user/operations.js index 2c036b1fadd..0e4bb407ca0 100644 --- a/plugins/woocommerce-admin/client/wc-api/user/operations.js +++ b/plugins/woocommerce-admin/client/wc-api/user/operations.js @@ -40,6 +40,7 @@ function updateCurrentUserData( resourceNames, data, fetch ) { 'revenue_report_columns', 'taxes_report_columns', 'variations_report_columns', + 'dashboard_performance_indicators', 'dashboard_charts', 'dashboard_chart_type', 'dashboard_chart_interval', diff --git a/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-controller.php b/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-controller.php index bbf66396759..9e545e181db 100644 --- a/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-controller.php +++ b/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-controller.php @@ -64,6 +64,7 @@ class WC_Admin_REST_Reports_Controller extends WC_REST_Reports_Controller { return true; } + /** * 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 ) { + 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 ); $data[] = $this->prepare_response_for_collection( $item ); } @@ -154,6 +184,7 @@ class WC_Admin_REST_Reports_Controller extends WC_REST_Reports_Controller { $data = array( 'slug' => $report->slug, 'description' => $report->description, + 'path' => $report->path, ); $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( 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( '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' ), 'readonly' => true, ), + 'path' => array( + 'description' => __( 'API path.', 'wc-admin' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), ), ); diff --git a/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-coupons-stats-controller.php b/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-coupons-stats-controller.php index faf5cea16c9..187beb587da 100644 --- a/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-coupons-stats-controller.php +++ b/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-coupons-stats-controller.php @@ -138,6 +138,8 @@ class WC_Admin_REST_Reports_Coupons_Stats_Controller extends WC_REST_Reports_Con 'type' => 'number', 'context' => array( 'view', 'edit' ), 'readonly' => true, + 'indicator' => true, + 'format' => 'currency', ), 'coupons_count' => array( '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, ), 'orders_count' => array( - 'description' => __( 'Amount of orders.', 'wc-admin' ), + 'description' => __( 'Amount of discounted orders.', 'wc-admin' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, + 'indicator' => true, ), ); diff --git a/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-downloads-stats-controller.php b/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-downloads-stats-controller.php index b7a5ec50ac6..1a622a6c9a5 100644 --- a/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-downloads-stats-controller.php +++ b/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-downloads-stats-controller.php @@ -143,6 +143,7 @@ class WC_Admin_REST_Reports_Downloads_Stats_Controller extends WC_REST_Reports_C 'type' => 'number', 'context' => array( 'view', 'edit' ), 'readonly' => true, + 'indicator' => true, ), ); diff --git a/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-orders-stats-controller.php b/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-orders-stats-controller.php index e743ed90845..f62cf855aad 100644 --- a/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-orders-stats-controller.php +++ b/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-orders-stats-controller.php @@ -146,18 +146,22 @@ class WC_Admin_REST_Reports_Orders_Stats_Controller extends WC_Admin_REST_Report 'type' => 'number', 'context' => array( 'view', 'edit' ), 'readonly' => true, - ), - 'avg_order_value' => array( - 'description' => __( 'Average order value.', 'wc-admin' ), - 'type' => 'number', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, + 'format' => 'currency', ), 'orders_count' => array( 'description' => __( 'Amount of orders', 'wc-admin' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), '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( 'description' => __( 'Average items per order', 'wc-admin' ), diff --git a/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-performance-indicators-controller.php b/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-performance-indicators-controller.php index 3e7bde916f7..289aecc8067 100644 --- a/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-performance-indicators-controller.php +++ b/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-performance-indicators-controller.php @@ -31,6 +31,67 @@ class WC_Admin_REST_Reports_Performance_Indicators_Controller extends WC_REST_Re */ 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. * @@ -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() { - global $wp_rest_server; + private function get_indicator_data() { + // 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' ); $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 ) { 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 ); $data = $response->get_data(); + $prefix = substr( $endpoint['slug'], 0, -6 ); 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 ) { - $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; + } + + /** + * Returns a list of allowed performance indicators. + * + * @param WP_REST_Request $request Request data. + * @return array|WP_Error + */ + 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 ) { /** - * Filter the list of allowed stats that can be returned via the performance indiciator endpoint. + * Custom ordering for store performance indicators. * - * @param array $allowed_stats The list of allowed stats. + * @see https://github.com/woocommerce/wc-admin/issues/1282 + * @param array $indicators A list of ordered indicators. */ - return apply_filters( 'woocommerce_admin_performance_indicators_allowed_stats', $allowed_stats ); + $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 */ public function get_items( $request ) { - $allowed_stats = $this->get_allowed_stats(); - - if ( is_wp_error( $allowed_stats ) ) { - return $allowed_stats; + $indicator_data = $this->get_indicator_data(); + if ( is_wp_error( $indicator_data ) ) { + return $indicator_data; } $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(); - foreach ( $query_args['stats'] as $stat_request ) { + foreach ( $query_args['stats'] as $stat ) { $is_error = false; - $pieces = explode( '/', $stat_request ); - $endpoint = $pieces[0]; - $stat = $pieces[1]; + $pieces = $this->get_stats_parts( $stat ); + $report = $pieces[0]; + $chart = $pieces[1]; - if ( ! in_array( $stat_request, $allowed_stats ) ) { + if ( ! in_array( $stat, $this->allowed_stats ) ) { continue; } - /** - * 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_url = $this->endpoints[ $report ]; + $request = new WP_REST_Request( 'GET', $request_url ); $request->set_param( 'before', $query_args['before'] ); $request->set_param( 'after', $query_args['after'] ); $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( - 'stat' => $stat_request, - 'value' => null, + 'stat' => $stat, + 'chart' => $chart, + 'label' => $label, + 'format' => $format, + 'value' => null, ); continue; } $stats[] = (object) array( - 'stat' => $stat_request, - 'value' => $data['totals'][ $stat ], + 'stat' => $stat, + 'chart' => $chart, + 'label' => $label, + 'format' => $format, + 'value' => $data['totals'][ $chart ], ); } + usort( $stats, array( $this, 'sort' ) ); + $objects = array(); foreach ( $stats as $stat ) { $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 */ protected function prepare_links( $object ) { - $pieces = explode( '/', $object->stat ); + $pieces = $this->get_stats_parts( $object->stat ); $endpoint = $pieces[0]; $stat = $pieces[1]; + $url = $this->urls[ $endpoint ]; $links = array( + 'api' => array( + 'href' => rest_url( $this->endpoints[ $endpoint ] ), + ), 'report' => array( - 'href' => rest_url( sprintf( '/%s/reports/%s/stats', $this->namespace, $endpoint ) ), + 'href' => ! empty( $url ) ? $url : '', ), ); 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. * * @return array */ 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' => 'http://json-schema.org/draft-04/schema#', 'title' => 'report_performance_indicator', @@ -230,6 +416,26 @@ class WC_Admin_REST_Reports_Performance_Indicators_Controller extends WC_REST_Re 'type' => 'string', 'context' => array( 'view', 'edit' ), '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( '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 ); } + /** + * 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. * * @return array */ public function get_collection_params() { - $allowed_stats = $this->get_allowed_stats(); - if ( is_wp_error( $allowed_stats ) ) { + $indicator_data = $this->get_indicator_data(); + if ( is_wp_error( $indicator_data ) ) { $allowed_stats = __( 'There was an issue loading the report endpoints', 'wc-admin' ); } else { - $allowed_stats = implode( ', ', $allowed_stats ); + $allowed_stats = implode( ', ', $this->allowed_stats ); } $params = array(); diff --git a/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-products-stats-controller.php b/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-products-stats-controller.php index 1db8c8a144e..087b90b599a 100644 --- a/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-products-stats-controller.php +++ b/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-products-stats-controller.php @@ -152,12 +152,14 @@ class WC_Admin_REST_Reports_Products_Stats_Controller extends WC_REST_Reports_Co 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, + 'indicator' => true, ), 'net_revenue' => array( 'description' => __( 'Net revenue.', 'wc-admin' ), 'type' => 'number', 'context' => array( 'view', 'edit' ), 'readonly' => true, + 'format' => 'currency', ), 'orders_count' => array( 'description' => __( 'Number of orders.', 'wc-admin' ), diff --git a/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-revenue-stats-controller.php b/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-revenue-stats-controller.php index 697277c5260..324b780c4c5 100644 --- a/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-revenue-stats-controller.php +++ b/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-revenue-stats-controller.php @@ -136,51 +136,61 @@ class WC_Admin_REST_Reports_Revenue_Stats_Controller extends WC_REST_Reports_Con 'type' => 'number', 'context' => array( 'view', 'edit' ), 'readonly' => true, + 'indicator' => true, + 'format' => 'currency', ), 'net_revenue' => array( 'description' => __( 'Net revenue.', 'wc-admin' ), 'type' => 'number', 'context' => array( 'view', 'edit' ), 'readonly' => true, + 'indicator' => true, + 'format' => 'currency', ), 'coupons' => array( 'description' => __( 'Total of coupons.', 'wc-admin' ), 'type' => 'number', 'context' => array( 'view', 'edit' ), 'readonly' => true, + 'format' => 'currency', ), 'shipping' => array( 'description' => __( 'Total of shipping.', 'wc-admin' ), 'type' => 'number', 'context' => array( 'view', 'edit' ), 'readonly' => true, + 'indicator' => true, + 'format' => 'currency', ), 'taxes' => array( 'description' => __( 'Total of taxes.', 'wc-admin' ), 'type' => 'number', 'context' => array( 'view', 'edit' ), 'readonly' => true, + 'format' => 'currency', ), 'refunds' => array( 'description' => __( 'Total of refunds.', 'wc-admin' ), 'type' => 'number', 'context' => array( 'view', 'edit' ), 'readonly' => true, + 'indicator' => true, + 'format' => 'currency', ), 'orders_count' => array( - 'description' => __( 'Amount of orders', 'wc-admin' ), + 'description' => __( 'Amount of orders.', 'wc-admin' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'num_items_sold' => array( - 'description' => __( 'Amount of orders', 'wc-admin' ), + 'description' => __( 'Items sold.', 'wc-admin' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'products' => array( - 'description' => __( 'Amount of orders', 'wc-admin' ), + 'description' => __( 'Products sold.', 'wc-admin' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, diff --git a/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-taxes-stats-controller.php b/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-taxes-stats-controller.php index 62756fcd64a..d545e468492 100644 --- a/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-taxes-stats-controller.php +++ b/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-taxes-stats-controller.php @@ -167,18 +167,24 @@ class WC_Admin_REST_Reports_Taxes_Stats_Controller extends WC_REST_Reports_Contr 'type' => 'number', 'context' => array( 'view', 'edit' ), 'readonly' => true, + 'indicator' => true, + 'format' => 'currency', ), 'order_tax' => array( 'description' => __( 'Order tax.', 'wc-admin' ), 'type' => 'number', 'context' => array( 'view', 'edit' ), 'readonly' => true, + 'indicator' => true, + 'format' => 'currency', ), 'shipping_tax' => array( 'description' => __( 'Shipping tax.', 'wc-admin' ), 'type' => 'number', 'context' => array( 'view', 'edit' ), 'readonly' => true, + 'indicator' => true, + 'format' => 'currency', ), 'orders_count' => array( 'description' => __( 'Amount of orders.', 'wc-admin' ), diff --git a/plugins/woocommerce-admin/lib/admin.php b/plugins/woocommerce-admin/lib/admin.php index 78b3995e82a..bfc132c9295 100644 --- a/plugins/woocommerce-admin/lib/admin.php +++ b/plugins/woocommerce-admin/lib/admin.php @@ -399,6 +399,7 @@ function wc_admin_get_user_data_fields() { 'revenue_report_columns', 'taxes_report_columns', 'variations_report_columns', + 'dashboard_performance_indicators', 'dashboard_charts', 'dashboard_chart_type', 'dashboard_chart_interval', diff --git a/plugins/woocommerce-admin/lib/client-assets.php b/plugins/woocommerce-admin/lib/client-assets.php index 1c117a7560f..ae44494948d 100644 --- a/plugins/woocommerce-admin/lib/client-assets.php +++ b/plugins/woocommerce-admin/lib/client-assets.php @@ -153,7 +153,8 @@ function wc_admin_print_script_settings() { } $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' ) ) { @@ -169,7 +170,7 @@ function wc_admin_print_script_settings() { $current_user_data = array(); 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 ) ); } /** @@ -179,22 +180,22 @@ function wc_admin_print_script_settings() { // Settings and variables can be passed here for access in the app. $settings = array( - 'adminUrl' => admin_url(), - 'wcAssetUrl' => plugins_url( 'assets/', WC_PLUGIN_FILE ), - 'wcAdminAssetUrl' => plugins_url( 'images/', wc_admin_dir_path( 'wc-admin.php' ) ), // Temporary for plugin. See above. - 'embedBreadcrumbs' => wc_admin_get_embed_breadcrumbs(), - 'siteLocale' => esc_attr( get_bloginfo( 'language' ) ), - 'currency' => wc_admin_currency_settings(), - 'orderStatuses' => format_order_statuses( wc_get_order_statuses() ), - 'stockStatuses' => wc_get_product_stock_status_options(), - 'siteTitle' => get_bloginfo( 'name' ), - 'trackingEnabled' => $tracking_enabled, - 'dataEndpoints' => array(), - 'l10n' => array( + 'adminUrl' => admin_url(), + 'wcAssetUrl' => plugins_url( 'assets/', WC_PLUGIN_FILE ), + 'wcAdminAssetUrl' => plugins_url( 'images/', wc_admin_dir_path( 'wc-admin.php' ) ), // Temporary for plugin. See above. + 'embedBreadcrumbs' => wc_admin_get_embed_breadcrumbs(), + 'siteLocale' => esc_attr( get_bloginfo( 'language' ) ), + 'currency' => wc_admin_currency_settings(), + 'orderStatuses' => format_order_statuses( wc_get_order_statuses() ), + 'stockStatuses' => wc_get_product_stock_status_options(), + 'siteTitle' => get_bloginfo( 'name' ), + 'trackingEnabled' => $tracking_enabled, + 'dataEndpoints' => array(), + 'l10n' => array( 'userLocale' => get_user_locale(), 'weekdaysShort' => array_values( $wp_locale->weekday_abbrev ), ), - 'currentUserData' => $current_user_data, + 'currentUserData' => $current_user_data, ); foreach ( $preload_data_endpoints as $key => $endpoint ) { diff --git a/plugins/woocommerce-admin/packages/components/CHANGELOG.md b/plugins/woocommerce-admin/packages/components/CHANGELOG.md index 69071b1cca1..71066dfcf12 100644 --- a/plugins/woocommerce-admin/packages/components/CHANGELOG.md +++ b/plugins/woocommerce-admin/packages/components/CHANGELOG.md @@ -1,5 +1,6 @@ # 1.4.1 (unreleased) - Chart component: format numbers and prices using store currency settings. +- Make `href`/linking optional in SummaryNumber. # 1.4.0 - Add download log ip address autocompleter to search component diff --git a/plugins/woocommerce-admin/packages/components/src/summary/number.js b/plugins/woocommerce-admin/packages/components/src/summary/number.js index 42dfa3020f1..dd4559ed454 100644 --- a/plugins/woocommerce-admin/packages/components/src/summary/number.js +++ b/plugins/woocommerce-admin/packages/components/src/summary/number.js @@ -52,17 +52,23 @@ const SummaryNumber = ( { screenReaderLabel = sprintf( __( 'No change from %s', 'wc-admin' ), prevLabel ); } - const Container = onToggle ? Button : Link; + let Container; const containerProps = { className: classes, 'aria-current': selected ? 'page' : null, }; - if ( ! onToggle ) { - containerProps.href = href; - containerProps.role = 'menuitem'; + + if ( onToggle || href ) { + Container = onToggle ? Button : Link; + if ( ! onToggle ) { + containerProps.href = href; + containerProps.role = 'menuitem'; + } else { + containerProps.onClick = onToggle; + containerProps[ 'aria-expanded' ] = isOpen; + } } else { - containerProps.onClick = onToggle; - containerProps[ 'aria-expanded' ] = isOpen; + Container = 'div'; } return ( @@ -109,7 +115,7 @@ SummaryNumber.propTypes = { /** * 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, * and only applies to the toggle-able item (first in the list). @@ -147,7 +153,7 @@ SummaryNumber.propTypes = { }; SummaryNumber.defaultProps = { - href: '/analytics', + href: '', isOpen: false, prevLabel: __( 'Previous Period:', 'wc-admin' ), reverseTrend: false, diff --git a/plugins/woocommerce-admin/tests/api/reports-performance-indicators.php b/plugins/woocommerce-admin/tests/api/reports-performance-indicators.php index ed4620924a8..e07457fd3c9 100644 --- a/plugins/woocommerce-admin/tests/api/reports-performance-indicators.php +++ b/plugins/woocommerce-admin/tests/api/reports-performance-indicators.php @@ -37,6 +37,7 @@ class WC_Tests_API_Reports_Performance_Indicators extends WC_REST_Unit_Test_Case $routes = $this->server->get_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(); $this->assertEquals( 200, $response->get_status() ); - $this->assertEquals( 2, count( $reports ) ); $this->assertEquals( 'orders/orders_count', $reports[0]['stat'] ); + $this->assertEquals( 'Amount of orders', $reports[0]['label'] ); $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( 'Number of downloads', $reports[1]['label'] ); $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(); $properties = $data['schema']['properties']; - $this->assertEquals( 2, count( $properties ) ); + $this->assertEquals( 5, count( $properties ) ); $this->assertArrayHasKey( 'stat', $properties ); + $this->assertArrayHasKey( 'chart', $properties ); + $this->assertArrayHasKey( 'label', $properties ); + $this->assertArrayHasKey( 'format', $properties ); $this->assertArrayHasKey( 'value', $properties ); } }