Merge branch 'master' into fix/1035

# Conflicts:
#	client/analytics/report/customers/table.js
#	includes/api/class-wc-admin-rest-reports-orders-stats-controller.php
#	tests/reports/class-wc-tests-reports-orders-stats.php
This commit is contained in:
Peter Fabian 2019-01-23 10:13:55 +01:00
commit 507ee13825
151 changed files with 2631 additions and 1284 deletions

View File

@ -15,6 +15,11 @@
"wpApiSettings": true,
"wcSettings": true,
},
"settings": {
"react": {
"version": "detect",
},
},
"rules": {
"camelcase": 0,
"indent": 0,

View File

@ -14,7 +14,7 @@ And as such will require data layer logic for products to fully build the table
"_links": {
"product": [
{
"href": "https://example.com/wp-json/wc/v3/products/20"
"href": "https://example.com/wp-json/wc/v4/products/20"
}
]
}
@ -32,7 +32,7 @@ export default [
_links: {
product: [
{
href: 'https://example.com/wp-json/wc/v3/products/20',
href: 'https://example.com/wp-json/wc/v4/products/20',
},
],
},
@ -46,7 +46,7 @@ export default [
_links: {
product: [
{
href: 'https://example.com/wp-json/wc/v3/products/22',
href: 'https://example.com/wp-json/wc/v4/products/22',
},
],
},
@ -60,7 +60,7 @@ export default [
_links: {
product: [
{
href: 'https://example.com/wp-json/wc/v3/products/23',
href: 'https://example.com/wp-json/wc/v4/products/23',
},
],
},
@ -74,7 +74,7 @@ export default [
_links: {
product: [
{
href: 'https://example.com/wp-json/wc/v3/products/24',
href: 'https://example.com/wp-json/wc/v4/products/24',
},
],
},
@ -88,7 +88,7 @@ export default [
_links: {
product: [
{
href: 'https://example.com/wp-json/wc/v3/products/25',
href: 'https://example.com/wp-json/wc/v4/products/25',
},
],
},

View File

@ -69,7 +69,7 @@ Leaderboard.propTypes = {
/**
* The endpoint to use in API calls to populate the table rows and summary.
* For example, if `taxes` is provided, data will be fetched from the report
* `taxes` endpoint (ie: `/wc/v3/reports/taxes` and `/wc/v3/reports/taxes/stats`).
* `taxes` endpoint (ie: `/wc/v4/reports/taxes` and `/wc/v4/reports/taxes/stats`).
* If the provided endpoint doesn't exist, an error will be shown to the user
* with `ReportError`.
*/

View File

@ -17,6 +17,7 @@ import { formatCurrency, getCurrencyFormatDecimal } from '@woocommerce/currency'
* Internal dependencies
*/
import LeaderboardWithSelect, { Leaderboard } from '../';
import { NAMESPACE } from 'store/constants';
import { numberFormat } from 'lib/number';
import mockData from '../__mocks__/top-selling-products-mock-data';
@ -102,7 +103,7 @@ describe( 'Leaderboard', () => {
);
const leaderboard = leaderboardWrapper.root.findByType( Leaderboard );
const endpoint = '/wc/v3/reports/products';
const endpoint = NAMESPACE + 'reports/products';
const query = { orderby: 'items_sold', per_page: 5, extended_info: 1 };
expect( getReportStatsMock.mock.calls[ 0 ][ 1 ] ).toBe( endpoint );

View File

@ -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';
/**
@ -42,33 +42,35 @@ export class ReportSummary extends Component {
const secondaryTotals = totals.secondary || {};
const { compare } = getDateParamsFromQuery( query );
const summaryNumbers = charts.map( chart => {
const { key, label, type } = chart;
const delta = calculateDelta( primaryTotals[ key ], secondaryTotals[ key ] );
const href = getNewPath( { chart: key } );
const prevValue = formatValue( type, secondaryTotals[ key ] );
const isSelected = selectedChart.key === key;
const value = formatValue( type, primaryTotals[ key ] );
const renderSummaryNumbers = ( { onToggle } ) =>
charts.map( chart => {
const { key, label, type } = chart;
const delta = calculateDelta( primaryTotals[ key ], secondaryTotals[ key ] );
const href = getNewPath( { chart: key } );
const prevValue = formatValue( type, secondaryTotals[ key ] );
const isSelected = selectedChart.key === key;
const value = formatValue( type, primaryTotals[ key ] );
return (
<SummaryNumber
key={ key }
delta={ delta }
href={ href }
label={ label }
prevLabel={
'previous_period' === compare
? __( 'Previous Period:', 'wc-admin' )
: __( 'Previous Year:', 'wc-admin' )
}
prevValue={ prevValue }
selected={ isSelected }
value={ value }
/>
);
} );
return (
<SummaryNumber
key={ key }
delta={ delta }
href={ href }
label={ label }
prevLabel={
'previous_period' === compare
? __( 'Previous Period:', 'wc-admin' )
: __( 'Previous Year:', 'wc-admin' )
}
prevValue={ prevValue }
selected={ isSelected }
value={ value }
onLinkClickCallback={ onToggle }
/>
);
} );
return <SummaryList>{ summaryNumbers }</SummaryList>;
return <SummaryList>{ renderSummaryNumbers }</SummaryList>;
}
}
@ -80,7 +82,7 @@ ReportSummary.propTypes = {
/**
* The endpoint to use in API calls to populate the Summary Numbers.
* For example, if `taxes` is provided, data will be fetched from the report
* `taxes` endpoint (ie: `/wc/v3/reports/taxes/stats`). If the provided endpoint
* `taxes` endpoint (ie: `/wc/v4/reports/taxes/stats`). If the provided endpoint
* doesn't exist, an error will be shown to the user with `ReportError`.
*/
endpoint: PropTypes.string.isRequired,

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

@ -124,7 +124,7 @@ ReportTable.propTypes = {
/**
* The endpoint to use in API calls to populate the table rows and summary.
* For example, if `taxes` is provided, data will be fetched from the report
* `taxes` endpoint (ie: `/wc/v3/reports/taxes` and `/wc/v3/reports/taxes/stats`).
* `taxes` endpoint (ie: `/wc/v4/reports/taxes` and `/wc/v4/reports/taxes/stats`).
* If the provided endpoint doesn't exist, an error will be shown to the user
* with `ReportError`.
*/

View File

@ -10,6 +10,7 @@ import { Spinner } from '@wordpress/components';
* WooCommerce dependencies
*/
import { Link } from '@woocommerce/components';
import { getNewPath, getPersistedQuery } from '@woocommerce/navigation';
export default class CategoryBreadcrumbs extends Component {
getCategoryAncestorIds( category, categories ) {
@ -48,14 +49,18 @@ export default class CategoryBreadcrumbs extends Component {
}
render() {
const { categories, category } = this.props;
const { categories, category, query } = this.props;
const persistedQuery = getPersistedQuery( query );
return category ? (
<div className="woocommerce-table__breadcrumbs">
{ this.getCategoryAncestors( category, categories ) }
<Link
href={ 'term.php?taxonomy=product_cat&post_type=product&tag_ID=' + category.id }
type="wp-admin"
href={ getNewPath( persistedQuery, 'categories', {
filter: 'single_category',
categories: category.id,
} ) }
type="wc-admin"
>
{ category.name }
</Link>

View File

@ -36,6 +36,31 @@ export const filters = [
showFilters: () => true,
filters: [
{ label: __( 'All Categories', 'wc-admin' ), value: 'all' },
{
label: __( 'Single Category', 'wc-admin' ),
value: 'select_category',
chartMode: 'item-comparison',
subFilters: [
{
component: 'Search',
value: 'single_category',
chartMode: 'item-comparison',
path: [ 'select_category' ],
settings: {
type: 'categories',
param: 'categories',
getLabels: getRequestByIdString( NAMESPACE + 'products/categories', category => ( {
id: category.id,
label: category.name,
} ) ),
labels: {
placeholder: __( 'Type to search for a category', 'wc-admin' ),
button: __( 'Single Category', 'wc-admin' ),
},
},
},
],
},
{
label: __( 'Comparison', 'wc-admin' ),
value: 'compare-categories',

View File

@ -11,6 +11,8 @@ import { map } from 'lodash';
* WooCommerce dependencies
*/
import { formatCurrency, getCurrencyFormatDecimal } from '@woocommerce/currency';
import { getNewPath, getPersistedQuery } from '@woocommerce/navigation';
import { Link } from '@woocommerce/components';
/**
* Internal dependencies
@ -66,14 +68,18 @@ class CategoriesReportTable extends Component {
}
getRowsContent( categoryStats ) {
const { query } = this.props;
return map( categoryStats, categoryStat => {
const { category_id, items_sold, net_revenue, products_count, orders_count } = categoryStat;
const categories = this.props.categories;
const { categories, query } = this.props;
const category = categories[ category_id ];
const persistedQuery = getPersistedQuery( query );
return [
{
display: <CategoryBreacrumbs category={ category } categories={ categories } />,
display: (
<CategoryBreacrumbs query={ query } category={ category } categories={ categories } />
),
value: category && category.name,
},
{
@ -85,7 +91,17 @@ class CategoriesReportTable extends Component {
value: getCurrencyFormatDecimal( net_revenue ),
},
{
display: numberFormat( products_count ),
display: category && (
<Link
href={ getNewPath( persistedQuery, 'categories', {
filter: 'single_category',
categories: category.id,
} ) }
type="wc-admin"
>
{ numberFormat( products_count ) }
</Link>
),
value: products_count,
},
{

View File

@ -31,6 +31,31 @@ export const filters = [
showFilters: () => true,
filters: [
{ label: __( 'All Coupons', 'wc-admin' ), value: 'all' },
{
label: __( 'Single Coupon', 'wc-admin' ),
value: 'select_coupon',
chartMode: 'item-comparison',
subFilters: [
{
component: 'Search',
value: 'single_coupon',
chartMode: 'item-comparison',
path: [ 'select_coupon' ],
settings: {
type: 'coupons',
param: 'coupons',
getLabels: getRequestByIdString( NAMESPACE + 'coupons', coupon => ( {
id: coupon.id,
label: coupon.code,
} ) ),
labels: {
placeholder: __( 'Type to search for a coupon', 'wc-admin' ),
button: __( 'Single Coupon', 'wc-admin' ),
},
},
},
],
},
{
label: __( 'Comparison', 'wc-admin' ),
value: 'compare-coupons',
@ -44,6 +69,7 @@ export const filters = [
labels: {
title: __( 'Compare Coupon Codes', 'wc-admin' ),
update: __( 'Compare', 'wc-admin' ),
helpText: __( 'Select at least two coupon codes to compare', 'wc-admin' ),
},
},
},

View File

@ -281,6 +281,36 @@ export const advancedFilters = {
component: 'Date',
},
},
last_active: {
labels: {
add: __( 'Last active', 'wc-admin' ),
remove: __( 'Remove last active filter', 'wc-admin' ),
rule: __( 'Select a last active filter match', 'wc-admin' ),
/* translators: A sentence describing a Product filter. See screen shot for context: https://cloudup.com/cCsm3GeXJbE */
title: __( 'Last active {{rule /}} {{filter /}}', 'wc-admin' ),
filter: __( 'Select registered date', 'wc-admin' ),
},
rules: [
{
value: 'before',
/* translators: Sentence fragment, logical, "Before" refers to customers registered before a given date. Screenshot for context: https://cloudup.com/cCsm3GeXJbE */
label: _x( 'Before', 'date', 'wc-admin' ),
},
{
value: 'after',
/* translators: Sentence fragment, logical, "after" refers to customers registered after a given date. Screenshot for context: https://cloudup.com/cCsm3GeXJbE */
label: _x( 'After', 'date', 'wc-admin' ),
},
{
value: 'between',
/* translators: Sentence fragment, logical, "Between" refers to average order value of a customer, between two given amounts. Screenshot for context: https://cloudup.com/cCsm3GeXJbE */
label: _x( 'Between', 'date', 'wc-admin' ),
},
],
input: {
component: 'Date',
},
},
},
};
/*eslint-enable max-len*/

View File

@ -3,7 +3,8 @@
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { Component } from '@wordpress/element';
import { Component, Fragment } from '@wordpress/element';
import { Tooltip } from '@wordpress/components';
/**
* WooCommerce dependencies
@ -90,6 +91,12 @@ export default class CustomersReportTable extends Component {
];
}
getCountryName( code ) {
const countries = ( wcSettings.dataEndpoints && wcSettings.dataEndpoints.countries ) || [];
const country = countries.find( c => c.code === code );
return country ? country.name : null;
}
getRowsContent( customers ) {
return customers.map( customer => {
const {
@ -106,6 +113,7 @@ export default class CustomersReportTable extends Component {
city,
country,
} = customer;
const countryName = this.getCountryName( country );
const customerNameLink = user_id ? (
<Link href={ 'user-edit.php?user_id=' + user_id } type="wp-admin">
@ -121,6 +129,15 @@ export default class CustomersReportTable extends Component {
'—'
);
const countryDisplay = (
<Fragment>
<Tooltip text={ countryName }>
<span aria-hidden="true">{ country }</span>
</Tooltip>
<span className="screen-reader-text">{ countryName }</span>
</Fragment>
);
return [
{
display: customerNameLink,
@ -155,7 +172,7 @@ export default class CustomersReportTable extends Component {
value: date_last_active,
},
{
display: country,
display: countryDisplay,
value: country,
},
{
@ -176,11 +193,6 @@ export default class CustomersReportTable extends Component {
return (
<ReportTable
endpoint="customers"
extendItemsMethodNames={ {
load: 'getCustomers',
getError: 'getCustomersError',
isRequesting: 'isGetCustomersRequesting',
} }
getHeadersContent={ this.getHeadersContent }
getRowsContent={ this.getRowsContent }
itemIdField="id"

View File

@ -2,30 +2,26 @@
/**
* External dependencies
*/
import { __, _n, sprintf } from '@wordpress/i18n';
import { __, _n, _x, sprintf } from '@wordpress/i18n';
import { Component, Fragment } from '@wordpress/element';
import { compose } from '@wordpress/compose';
import { map } from 'lodash';
/**
* WooCommerce dependencies
*/
import { appendTimestamp, defaultTableDateFormat, getCurrentDates } from '@woocommerce/date';
import { Date, Link, OrderStatus, ViewMoreList } from '@woocommerce/components';
import { defaultTableDateFormat } from '@woocommerce/date';
import { formatCurrency } from '@woocommerce/currency';
/**
* Internal dependencies
*/
import { QUERY_DEFAULTS } from 'store/constants';
import { getFilterQuery } from 'store/reports/utils';
import { numberFormat } from 'lib/number';
import withSelect from 'wc-api/with-select';
import ReportTable from 'analytics/components/report-table';
import { formatTableOrders } from './utils';
import { getNewPath, getPersistedQuery } from '@woocommerce/navigation';
import './style.scss';
class OrdersReportTable extends Component {
export default class OrdersReportTable extends Component {
constructor() {
super();
@ -96,45 +92,52 @@ class OrdersReportTable extends Component {
}
getRowsContent( tableData ) {
const { query } = this.props;
const persistedQuery = getPersistedQuery( query );
return map( tableData, row => {
const {
date,
id,
status,
customer_id,
line_items,
items_sold,
coupon_lines,
currency,
net_revenue,
customer_type,
date_created,
extended_info,
net_total,
num_items_sold,
order_id,
status,
} = row;
const { coupons, products } = extended_info;
const products = line_items
const formattedProducts = products
.sort( ( itemA, itemB ) => itemB.quantity - itemA.quantity )
.map( item => ( {
label: item.name,
href: 'post.php?post=' + item.product_id + '&action=edit',
quantity: item.quantity,
href: getNewPath( persistedQuery, 'products', {
filter: 'single_product',
products: item.id,
} ),
} ) );
const coupons = coupon_lines.map( coupon => ( {
const formattedCoupons = coupons.map( coupon => ( {
label: coupon.code,
// @TODO It should link to the coupons report
href: 'edit.php?s=' + coupon.code + '&post_type=shop_coupon',
href: getNewPath( persistedQuery, 'coupons', {
filter: 'single_coupon',
coupons: coupon.id,
} ),
} ) );
return [
{
display: <Date date={ date } visibleFormat={ defaultTableDateFormat } />,
value: date,
display: <Date date={ date_created } visibleFormat={ defaultTableDateFormat } />,
value: date_created,
},
{
display: (
<Link href={ 'post.php?post=' + id + '&action=edit' } type="wp-admin">
{ id }
<Link href={ 'post.php?post=' + order_id + '&action=edit' } type="wp-admin">
{ order_id }
</Link>
),
value: id,
value: order_id,
},
{
display: (
@ -143,32 +146,36 @@ class OrdersReportTable extends Component {
value: status,
},
{
// @TODO This should display customer type (new/returning) once it's
// implemented in the API.
display: customer_id,
value: customer_id,
display:
customer_type === 'new'
? _x( 'New', 'customer type', 'wc-admin' )
: _x( 'Returning', 'customer type', 'wc-admin' ),
value: customer_type,
},
{
display: this.renderList(
products.length ? [ products[ 0 ] ] : [],
products.map( product => ( {
formattedProducts.length ? [ formattedProducts[ 0 ] ] : [],
formattedProducts.map( product => ( {
label: sprintf( __( '%s× %s', 'wc-admin' ), product.quantity, product.label ),
href: product.href,
} ) )
),
value: products.map( product => product.label ).join( ' ' ),
value: formattedProducts.map( product => product.label ).join( ' ' ),
},
{
display: numberFormat( items_sold ),
value: items_sold,
display: numberFormat( num_items_sold ),
value: num_items_sold,
},
{
display: this.renderList( coupons.length ? [ coupons[ 0 ] ] : [], coupons ),
value: coupons.map( item => item.code ).join( ' ' ),
display: this.renderList(
formattedCoupons.length ? [ formattedCoupons[ 0 ] ] : [],
formattedCoupons
),
value: formattedCoupons.map( item => item.code ).join( ' ' ),
},
{
display: formatCurrency( net_revenue, currency ),
value: net_revenue,
display: formatCurrency( net_total, currency ),
value: net_total,
},
];
} );
@ -217,7 +224,7 @@ class OrdersReportTable extends Component {
renderLinks( items = [] ) {
return items.map( ( item, i ) => (
<Link href={ item.href } key={ i } type="wp-admin">
<Link href={ item.href } key={ i } type="wc-admin">
{ item.label }
</Link>
) );
@ -233,7 +240,7 @@ class OrdersReportTable extends Component {
}
render() {
const { query, tableData } = this.props;
const { query } = this.props;
return (
<ReportTable
@ -242,49 +249,12 @@ class OrdersReportTable extends Component {
getRowsContent={ this.getRowsContent }
getSummary={ this.getSummary }
query={ query }
tableData={ tableData }
tableQuery={ {
extended_info: true,
} }
title={ __( 'Orders', 'wc-admin' ) }
columnPrefsKey="orders_report_columns"
/>
);
}
}
export default compose(
withSelect( ( select, props ) => {
const { query } = props;
const datesFromQuery = getCurrentDates( query );
const filterQuery = getFilterQuery( 'orders', query );
const { getOrders, getOrdersTotalCount, getOrdersError, isGetOrdersRequesting } = select(
'wc-api'
);
const tableQuery = {
orderby: query.orderby || 'date',
order: query.order || 'asc',
page: query.page || 1,
per_page: query.per_page || QUERY_DEFAULTS.pageSize,
after: appendTimestamp( datesFromQuery.primary.after, 'start' ),
before: appendTimestamp( datesFromQuery.primary.before, 'end' ),
status: [ 'processing', 'on-hold', 'completed' ],
...filterQuery,
};
const orders = getOrders( tableQuery );
const ordersTotalCount = getOrdersTotalCount( tableQuery );
const isError = Boolean( getOrdersError( tableQuery ) );
const isRequesting = isGetOrdersRequesting( tableQuery );
return {
tableData: {
items: {
data: formatTableOrders( orders ),
totalResults: ordersTotalCount,
},
isError,
isRequesting,
query: tableQuery,
},
};
} )
)( OrdersReportTable );

View File

@ -60,6 +60,31 @@ const filterConfig = {
},
],
},
{
label: __( 'Single Product Category', 'wc-admin' ),
value: 'select_category',
chartMode: 'item-comparison',
subFilters: [
{
component: 'Search',
value: 'single_category',
chartMode: 'item-comparison',
path: [ 'select_category' ],
settings: {
type: 'categories',
param: 'categories',
getLabels: getRequestByIdString( NAMESPACE + 'products/categories', category => ( {
id: category.id,
label: category.name,
} ) ),
labels: {
placeholder: __( 'Type to search for a product category', 'wc-admin' ),
button: __( 'Single Product Category', 'wc-admin' ),
},
},
},
],
},
{
label: __( 'Product Comparison', 'wc-admin' ),
value: 'compare-products',

View File

@ -172,6 +172,7 @@ class ProductsReportTable extends Component {
category={ category }
categories={ categories }
key={ category.id }
query={ query }
/>
) ) }
/>

View File

@ -222,7 +222,7 @@ export default compose(
const tableQuery = {
interval: 'day',
orderby: query.orderby || 'date',
order: query.order || 'asc',
order: query.order || 'desc',
page: query.page || 1,
per_page: query.per_page || QUERY_DEFAULTS.pageSize,
after: appendTimestamp( datesFromQuery.primary.after, 'start' ),

View File

@ -43,12 +43,13 @@ export default class StockReportTable extends Component {
{
label: __( 'Status', 'wc-admin' ),
key: 'stock_status',
isSortable: true,
defaultSort: true,
},
{
label: __( 'Stock', 'wc-admin' ),
key: 'stock_quantity',
isSortable: true,
defaultSort: true,
},
];
}
@ -136,8 +137,8 @@ export default class StockReportTable extends Component {
// getSummary={ this.getSummary }
query={ query }
tableQuery={ {
orderby: query.orderby || 'stock_quantity',
order: query.order || 'desc',
orderby: query.orderby || 'stock_status',
order: query.order || 'asc',
type: query.type || 'all',
} }
title={ __( 'Stock', 'wc-admin' ) }

View File

@ -13,13 +13,13 @@ import { NAMESPACE } from 'store/constants';
export const charts = [
{
key: 'order_tax',
label: __( 'Order Tax', 'wc-admin' ),
key: 'total_tax',
label: __( 'Total Tax', 'wc-admin' ),
type: 'currency',
},
{
key: 'total_tax',
label: __( 'Total Tax', 'wc-admin' ),
key: 'order_tax',
label: __( 'Order Tax', 'wc-admin' ),
type: 'currency',
},
{

View File

@ -13,7 +13,7 @@
}
.woocommerce-chart__footer {
position: relative;
&:after {
&::after {
content: '';
position: absolute;
width: 100%;

View File

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

View File

@ -59,9 +59,8 @@ export class TopSellingCategories extends Component {
return map( data, row => {
const { category_id, items_sold, net_revenue, extended_info } = row;
const name = get( extended_info, [ 'name' ] );
// TODO Update this to use a single_category filter, once it exists.
const categoryUrl = getNewPath( persistedQuery, 'analytics/categories', {
filter: 'compare-categories',
filter: 'single_category',
categories: category_id,
} );
const categoryLink = (

View File

@ -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,204 @@ 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 (
<EllipsisMenu label={ __( 'Choose which analytics to display', 'wc-admin' ) }>
<MenuTitle>{ __( 'Display Stats:', 'wc-admin' ) }</MenuTitle>
<MenuItem onInvoke={ this.toggle( 'showCustomers' ) }>
<ToggleControl
label={ __( 'Show Customers', 'wc-admin' ) }
checked={ this.state.showCustomers }
onChange={ this.toggle( 'showCustomers' ) }
/>
</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>
{ indicators.map( ( indicator, i ) => {
const checked = ! this.state.userPrefs.includes( indicator.stat );
return (
<MenuItem onInvoke={ this.toggle( indicator.stat ) } key={ i }>
<ToggleControl
label={ sprintf( __( 'Show %s', 'wc-admin' ), indicator.label ) }
checked={ checked }
onChange={ this.toggle( indicator.stat ) }
/>
</MenuItem>
);
} ) }
</EllipsisMenu>
);
}
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 <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 (
<Fragment>
<SectionHeader title={ __( 'Store Performance', 'wc-admin' ) } menu={ this.renderMenu() } />
<Card className="woocommerce-dashboard__store-performance">
<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>
<Card className="woocommerce-dashboard__store-performance">{ this.renderList() }</Card>
</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

@ -133,11 +133,11 @@ function OrdersPanel( { orders, isRequesting, isError } ) {
</span>
{ refundValue ? (
<span>
<s>{ formatCurrency( total, order.currency ) }</s>{' '}
{ formatCurrency( remainingTotal, order.currency ) }
<s>{ formatCurrency( total, order.currency_symbol ) }</s>{' '}
{ formatCurrency( remainingTotal, order.currency_symbol ) }
</span>
) : (
<span>{ formatCurrency( total, order.currency ) }</span>
<span>{ formatCurrency( total, order.currency_symbol ) }</span>
) }
</div>
}

View File

@ -11,6 +11,10 @@
@include breakpoint( '>960px' ) {
margin-top: 100px;
}
@include breakpoint( '<782px' ) {
margin-top: 60px;
}
}
.woocommerce-layout .woocommerce-layout__main {

View File

@ -1,20 +1,63 @@
/** @format */
/**
* External dependencies
*/
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
*
* @format
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/NumberFormat
* @param {Number|String} number number to format
* @returns {?String} A formatted string.
* @see http://locutus.io/php/strings/number_format/
* @param {Number|String} number number to format
* @param {int|null} [precision=null] optional decimal precision
* @returns {?String} A formatted string.
*/
export function numberFormat( number ) {
const locale = wcSettings.siteLocale || 'en-US'; // Default so we don't break.
export function numberFormat( number, precision = null ) {
if ( 'number' !== typeof number ) {
number = parseFloat( number );
}
if ( isNaN( number ) ) {
return '';
}
return new Intl.NumberFormat( locale ).format( number );
const decimalSeparator = get( wcSettings, [ 'currency', 'decimal_separator' ], '.' );
const thousandSeparator = get( wcSettings, [ 'currency', 'thousand_separator' ], ',' );
precision = parseInt( precision );
if ( isNaN( precision ) ) {
const [ , decimals ] = number.toString().split( '.' );
precision = decimals ? decimals.length : 0;
}
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

@ -5,7 +5,7 @@
import { numberFormat } from '../index';
describe( 'numberFormat', () => {
it( 'should default to en-US formatting', () => {
it( 'should default to precision=null decimal=. thousands=,', () => {
expect( numberFormat( 1000 ) ).toBe( '1,000' );
} );
@ -16,4 +16,20 @@ describe( 'numberFormat', () => {
it( 'should accept a string', () => {
expect( numberFormat( '10000' ) ).toBe( '10,000' );
} );
it( 'maintains all decimals if no precision specified', () => {
expect( numberFormat( '10000.123456' ) ).toBe( '10,000.123456' );
} );
it( 'maintains all decimals if invalid precision specified', () => {
expect( numberFormat( '10000.123456', 'not a number' ) ).toBe( '10,000.123456' );
} );
it( 'uses store currency settings, not locale', () => {
global.wcSettings.siteLocale = 'en-US';
global.wcSettings.currency.decimal_separator = ',';
global.wcSettings.currency.thousand_separator = '.';
expect( numberFormat( '12345.6789', 3 ) ).toBe( '12.345,679' );
} );
} );

View File

@ -2,7 +2,7 @@
* @format
*/
export const NAMESPACE = '/wc/v3/';
export const NAMESPACE = '/wc/v4/';
export const SWAGGERNAMESPACE = 'https://virtserver.swaggerhub.com/peterfabian/wc-v3-api/1.0.0/';
export const ERROR = 'ERROR';

View File

@ -11,6 +11,7 @@ import { dispatch } from '@wordpress/data';
/**
* Internal dependencies
*/
import { NAMESPACE } from 'store/constants';
import resolvers from '../resolvers';
const { getNotes } = resolvers;
@ -29,10 +30,10 @@ describe( 'getNotes', () => {
beforeAll( () => {
apiFetch.mockImplementation( options => {
if ( options.path === '/wc/v3/admin/notes' ) {
if ( options.path === NAMESPACE + 'admin/notes' ) {
return Promise.resolve( NOTES_1 );
}
if ( options.path === '/wc/v3/admin/notes?page=2' ) {
if ( options.path === NAMESPACE + 'admin/notes?page=2' ) {
return Promise.resolve( NOTES_2 );
}
} );

View File

@ -11,6 +11,7 @@ import { dispatch } from '@wordpress/data';
/**
* Internal dependencies
*/
import { NAMESPACE } from 'store/constants';
import resolvers from '../resolvers';
const { getOrders } = resolvers;
@ -29,10 +30,10 @@ describe( 'getOrders', () => {
beforeAll( () => {
apiFetch.mockImplementation( options => {
if ( options.path === '/wc/v3/orders' ) {
if ( options.path === NAMESPACE + 'orders' ) {
return Promise.resolve( ORDERS_1 );
}
if ( options.path === '/wc/v3/orders?orderby=id' ) {
if ( options.path === NAMESPACE + 'orders?orderby=id' ) {
return Promise.resolve( ORDERS_2 );
}
} );

View File

@ -31,7 +31,7 @@ describe( 'getReportItems', () => {
beforeAll( () => {
apiFetch.mockImplementation( options => {
if ( options.path === `/wc/v3/reports/${ endpoint }` ) {
if ( options.path === `/wc/v4/reports/${ endpoint }` ) {
return Promise.resolve( {
headers: {
get: () => ITEMS_1_COUNT,
@ -39,7 +39,7 @@ describe( 'getReportItems', () => {
json: () => Promise.resolve( ITEMS_1 ),
} );
}
if ( options.path === `/wc/v3/reports/${ endpoint }?orderby=id` ) {
if ( options.path === `/wc/v4/reports/${ endpoint }?orderby=id` ) {
return Promise.resolve( {
headers: {
get: () => ITEMS_2_COUNT,

View File

@ -55,7 +55,7 @@ describe( 'getReportStats', () => {
beforeAll( () => {
apiFetch.mockImplementation( options => {
if ( options.path === '/wc/v3/reports/revenue/stats' ) {
if ( options.path === '/wc/v4/reports/revenue/stats' ) {
return Promise.resolve( {
headers: {
get: header => REPORT_1_TOTALS[ header ],
@ -63,7 +63,7 @@ describe( 'getReportStats', () => {
json: () => Promise.resolve( REPORT_1 ),
} );
}
if ( options.path === '/wc/v3/reports/products/stats?interval=week' ) {
if ( options.path === '/wc/v4/reports/products/stats?interval=week' ) {
return Promise.resolve( {
headers: {
get: header => REPORT_2_TOTALS[ header ],

View File

@ -11,6 +11,7 @@ import {
getSummaryNumbers,
getFilterQuery,
getReportTableData,
timeStampFilterDates,
} from '../utils';
import * as ordersConfig from 'analytics/report/orders/config';
@ -527,3 +528,71 @@ describe( 'getReportTableData()', () => {
expect( select( 'wc-api' ).getReportItemsError ).toHaveBeenLastCalledWith( 'coupons', query );
} );
} );
describe( 'timeStampFilterDates', () => {
const advancedFilters = {
filters: {
city: {
input: { component: 'Search' },
},
my_date: {
input: { component: 'Date' },
},
},
};
it( 'should not change activeFilters not using the Date component', () => {
const activeFilter = {
key: 'name',
rule: 'is',
value: 'New York',
};
const timeStampedActiveFilter = timeStampFilterDates( advancedFilters, activeFilter );
expect( timeStampedActiveFilter ).toMatchObject( activeFilter );
} );
it( 'should append timestamps to activeFilters using the Date component', () => {
const activeFilter = {
key: 'my_date',
rule: 'after',
value: '2018-04-04',
};
const timeStampedActiveFilter = timeStampFilterDates( advancedFilters, activeFilter );
expect( timeStampedActiveFilter.value ).toBe( '2018-04-04T00:00:00+00:00' );
} );
it( 'should append start of day for "after" rule', () => {
const activeFilter = {
key: 'my_date',
rule: 'after',
value: '2018-04-04',
};
const timeStampedActiveFilter = timeStampFilterDates( advancedFilters, activeFilter );
expect( timeStampedActiveFilter.value ).toBe( '2018-04-04T00:00:00+00:00' );
} );
it( 'should append end of day for "before" rule', () => {
const activeFilter = {
key: 'my_date',
rule: 'before',
value: '2018-04-04',
};
const timeStampedActiveFilter = timeStampFilterDates( advancedFilters, activeFilter );
expect( timeStampedActiveFilter.value ).toBe( '2018-04-04T23:59:59+00:00' );
} );
it( 'should handle "between" values', () => {
const activeFilter = {
key: 'my_date',
rule: 'before',
value: [ '2018-04-04', '2018-04-10' ],
};
const timeStampedActiveFilter = timeStampFilterDates( advancedFilters, activeFilter );
expect( Array.isArray( timeStampedActiveFilter.value ) ).toBe( true );
expect( timeStampedActiveFilter.value ).toHaveLength( 2 );
expect( timeStampedActiveFilter.value[ 0 ] ).toContain( 'T00:00:00+00:00' );
expect( timeStampedActiveFilter.value[ 1 ] ).toContain( 'T23:59:59+00:00' );
} );
} );

View File

@ -3,7 +3,7 @@
/**
* External dependencies
*/
import { find, forEach, isNull } from 'lodash';
import { find, forEach, isNull, get } from 'lodash';
import moment from 'moment';
/**
@ -46,6 +46,42 @@ export function getFilterQuery( endpoint, query ) {
return {};
}
/**
* Add timestamp to advanced filter parameters involving date. The api
* expects a timestamp for these values similar to `before` and `after`.
*
* @param {object} config - advancedFilters config object.
* @param {object} activeFilter - an active filter.
* @returns {object} - an active filter with timestamp added to date values.
*/
export function timeStampFilterDates( config, activeFilter ) {
const advancedFilterConfig = config.filters[ activeFilter.key ];
if ( 'Date' !== get( advancedFilterConfig, [ 'input', 'component' ] ) ) {
return activeFilter;
}
const { rule, value } = activeFilter;
const timeOfDayMap = {
after: 'start',
before: 'end',
};
// If the value is an array, it signifies "between" values which must have a timestamp
// appended to each value.
if ( Array.isArray( value ) ) {
const [ after, before ] = value;
return Object.assign( {}, activeFilter, {
value: [
appendTimestamp( moment( after ), timeOfDayMap.after ),
appendTimestamp( moment( before ), timeOfDayMap.before ),
],
} );
}
return Object.assign( {}, activeFilter, {
value: appendTimestamp( moment( value ), timeOfDayMap[ rule ] ),
} );
}
export function getQueryFromConfig( config, advancedFilters, query ) {
const queryValue = query[ config.param ];
@ -60,7 +96,7 @@ export function getQueryFromConfig( config, advancedFilters, query ) {
return {};
}
return activeFilters.reduce(
return activeFilters.map( filter => timeStampFilterDates( advancedFilters, filter ) ).reduce(
( result, activeFilter ) => {
const { key, rule, value } = activeFilter;
result[ getUrlKey( key, rule ) ] = value;
@ -284,7 +320,7 @@ export function getReportTableQuery( endpoint, urlQuery, query ) {
return {
orderby: urlQuery.orderby || 'date',
order: urlQuery.order || 'asc',
order: urlQuery.order || 'desc',
after: appendTimestamp( datesFromQuery.primary.after, 'start' ),
before: appendTimestamp( datesFromQuery.primary.before, 'end' ),
page: urlQuery.page || 1,

View File

@ -14,13 +14,14 @@ import { stringifyQuery } from '@woocommerce/navigation';
* Internal dependencies
*/
import { isResourcePrefix, getResourceIdentifier, getResourceName } from '../utils';
import { NAMESPACE } from '../constants';
function read( resourceNames, fetch = apiFetch ) {
const filteredNames = resourceNames.filter( name => isResourcePrefix( name, 'category-query' ) );
return filteredNames.map( async resourceName => {
const query = getResourceIdentifier( resourceName );
const url = `/wc/v3/products/categories${ stringifyQuery( query ) }`;
const url = NAMESPACE + `/products/categories${ stringifyQuery( query ) }`;
try {
const categories = await fetch( {

View File

@ -2,12 +2,12 @@
/**
* External dependencies
*/
import { SECOND, MINUTE } from '@fresh-data/framework';
import { MINUTE } from '@fresh-data/framework';
export const NAMESPACE = '/wc/v3';
export const NAMESPACE = '/wc/v4';
export const DEFAULT_REQUIREMENT = {
timeout: 5 * SECOND,
timeout: 1 * MINUTE,
freshness: 5 * MINUTE,
};

View File

@ -1,11 +0,0 @@
/** @format */
/**
* Internal dependencies
*/
import operations from './operations';
import selectors from './selectors';
export default {
operations,
selectors,
};

View File

@ -1,47 +0,0 @@
/** @format */
/**
* External dependencies
*/
import apiFetch from '@wordpress/api-fetch';
/**
* WooCommerce dependencies
*/
import { stringifyQuery } from '@woocommerce/navigation';
/**
* Internal dependencies
*/
import { isResourcePrefix, getResourceIdentifier, getResourceName } from '../utils';
import { NAMESPACE } from '../constants';
function read( resourceNames, fetch = apiFetch ) {
const filteredNames = resourceNames.filter( name => isResourcePrefix( name, 'customers-query' ) );
return filteredNames.map( async resourceName => {
const query = getResourceIdentifier( resourceName );
const url = `${ NAMESPACE }/customers${ stringifyQuery( query ) }`;
try {
const customers = await fetch( { path: url } );
const ids = customers.map( customer => customer.id );
const customerResources = customers.reduce( ( resources, customer ) => {
resources[ getResourceName( 'customer', customer.id ) ] = { data: customer };
return resources;
}, {} );
return {
[ resourceName ]: {
data: ids,
},
...customerResources,
};
} catch ( error ) {
return { [ resourceName ]: { error } };
}
} );
}
export default {
read,
};

View File

@ -1,43 +0,0 @@
/** @format */
/**
* External dependencies
*/
import { isNil } from 'lodash';
/**
* Internal dependencies
*/
import { getResourceName } from '../utils';
import { DEFAULT_REQUIREMENT } from '../constants';
const getCustomers = ( getResource, requireResource ) => (
query = {},
requirement = DEFAULT_REQUIREMENT
) => {
const resourceName = getResourceName( 'customers-query', query );
const ids = requireResource( requirement, resourceName ).data || [];
return ids.map( id => getResource( getResourceName( 'customer', id ) ).data || {} );
};
const getCustomersError = getResource => ( query = {} ) => {
const resourceName = getResourceName( 'customers-query', query );
return getResource( resourceName ).error;
};
const isGetCustomersRequesting = getResource => ( query = {} ) => {
const resourceName = getResourceName( 'customers-query', query );
const { lastRequested, lastReceived } = getResource( resourceName );
if ( isNil( lastRequested ) || isNil( lastReceived ) ) {
return true;
}
return lastRequested > lastReceived;
};
export default {
getCustomers,
getCustomersError,
isGetCustomersRequesting,
};

View File

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

View File

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

View File

@ -4,7 +4,6 @@
* Internal dependencies
*/
import categories from './categories';
import customers from './customers';
import notes from './notes';
import orders from './orders';
import reportItems from './reports/items';
@ -19,7 +18,6 @@ function createWcApiSpec() {
},
selectors: {
...categories.selectors,
...customers.selectors,
...notes.selectors,
...orders.selectors,
...reportItems.selectors,
@ -31,7 +29,6 @@ function createWcApiSpec() {
read( resourceNames ) {
return [
...categories.operations.read( resourceNames ),
...customers.operations.read( resourceNames ),
...notes.operations.read( resourceNames ),
...orders.operations.read( resourceNames ),
...reportItems.operations.read( resourceNames ),

View File

@ -22,7 +22,7 @@ Properties of all the charts available for that report.
The endpoint to use in API calls to populate the Summary Numbers.
For example, if `taxes` is provided, data will be fetched from the report
`taxes` endpoint (ie: `/wc/v3/reports/taxes/stats`). If the provided endpoint
`taxes` endpoint (ie: `/wc/v4/reports/taxes/stats`). If the provided endpoint
doesn't exist, an error will be shown to the user with `ReportError`.
### `query`

View File

@ -20,7 +20,7 @@ The key for user preferences settings for column visibility.
The endpoint to use in API calls to populate the table rows and summary.
For example, if `taxes` is provided, data will be fetched from the report
`taxes` endpoint (ie: `/wc/v3/reports/taxes` and `/wc/v3/reports/taxes/stats`).
`taxes` endpoint (ie: `/wc/v4/reports/taxes` and `/wc/v4/reports/taxes/stats`).
If the provided endpoint doesn't exist, an error will be shown to the user
with `ReportError`.

View File

@ -11,7 +11,7 @@ Props
### `date`
- **Required**
- Type: String
- Type: One of type: string, object
- Default: null
Date to use in the component.

View File

@ -139,6 +139,13 @@ The string to use as a query parameter when searching row items.
Url query parameter search function operates on
### `showMenu`
- Type: Boolean
- Default: `true`
Boolean to determine whether or not ellipsis menu is shown.
### `summary`
- Type: Array

View File

@ -22,7 +22,7 @@ class WC_Admin_REST_Admin_Notes_Controller extends WC_REST_CRUD_Controller {
*
* @var string
*/
protected $namespace = 'wc/v3';
protected $namespace = 'wc/v4';
/**
* Route base.

View File

@ -0,0 +1,27 @@
<?php
/**
* REST API Coupons Controller
*
* Handles requests to /coupons/*
*
* @package WooCommerce Admin/API
*/
defined( 'ABSPATH' ) || exit;
/**
* Coupons controller.
*
* @package WooCommerce Admin/API
* @extends WC_REST_Coupons_Controller
*/
class WC_Admin_REST_Coupons_Controller extends WC_REST_Coupons_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v4';
}

View File

@ -19,6 +19,13 @@ class WC_Admin_REST_Customers_Controller extends WC_REST_Customers_Controller {
// TODO Add support for guests here. See https://wp.me/p7bje6-1dM.
/**
* Endpoint namespace.
*
* @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.

View File

@ -17,6 +17,13 @@ defined( 'ABSPATH' ) || exit;
*/
class WC_Admin_REST_Data_Controller extends WC_REST_Data_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v4';
/**
* Return the list of data resources.
*

View File

@ -0,0 +1,27 @@
<?php
/**
* REST API Data countries controller.
*
* Handles requests to the /data/countries endpoint.
*
* @package WooCommerce Admin/API
*/
defined( 'ABSPATH' ) || exit;
/**
* REST API Data countries controller class.
*
* @package WooCommerce Admin/API
* @extends WC_REST_Data_Countries_Controller
*/
class WC_Admin_REST_Data_Countries_Controller extends WC_REST_Data_Countries_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v4';
}

View File

@ -21,7 +21,7 @@ class WC_Admin_REST_Data_Download_Ips_Controller extends WC_REST_Data_Controller
*
* @var string
*/
protected $namespace = 'wc/v3';
protected $namespace = 'wc/v4';
/**
* Route base.

View File

@ -15,7 +15,14 @@ defined( 'ABSPATH' ) || exit;
* @package WooCommerce Admin/API
* @extends WC_REST_Orders_Controller
*/
class WC_Admin_REST_Orders_Stats_Controller extends WC_REST_Orders_Controller {
class WC_Admin_REST_Orders_Controller extends WC_REST_Orders_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v4';
/**
* Get the query params for collections.
*

View File

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

View File

@ -16,6 +16,12 @@ defined( 'ABSPATH' ) || exit;
* @extends WC_REST_Product_Reviews_Controller
*/
class WC_Admin_REST_Product_Reviews_Controller extends WC_REST_Product_Reviews_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v4';
/**
* Prepare links for the request.

View File

@ -17,6 +17,13 @@ defined( 'ABSPATH' ) || exit;
*/
class WC_Admin_REST_Products_Controller extends WC_REST_Products_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v4';
/**
* Adds properties that can be embed via ?_embed=1.
*

View File

@ -22,7 +22,7 @@ class WC_Admin_REST_Reports_Categories_Controller extends WC_Admin_REST_Reports_
*
* @var string
*/
protected $namespace = 'wc/v3';
protected $namespace = 'wc/v4';
/**
* Route base.

View File

@ -22,7 +22,7 @@ class WC_Admin_REST_Reports_Controller extends WC_REST_Reports_Controller {
*
* @var string
*/
protected $namespace = 'wc/v3';
protected $namespace = 'wc/v4';
/**
* Route base.
@ -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,
),
),
);

View File

@ -22,7 +22,7 @@ class WC_Admin_REST_Reports_Coupons_Controller extends WC_REST_Reports_Controlle
*
* @var string
*/
protected $namespace = 'wc/v3';
protected $namespace = 'wc/v4';
/**
* Route base.

View File

@ -22,7 +22,7 @@ class WC_Admin_REST_Reports_Coupons_Stats_Controller extends WC_REST_Reports_Con
*
* @var string
*/
protected $namespace = 'wc/v3';
protected $namespace = 'wc/v4';
/**
* Route base.
@ -139,6 +139,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' ),
@ -147,10 +149,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,
),
);

View File

@ -22,7 +22,7 @@ class WC_Admin_REST_Reports_Customers_Controller extends WC_REST_Reports_Control
*
* @var string
*/
protected $namespace = 'wc/v3';
protected $namespace = 'wc/v4';
/**
* Route base.

View File

@ -22,7 +22,7 @@ class WC_Admin_REST_Reports_Downloads_Controller extends WC_REST_Reports_Control
*
* @var string
*/
protected $namespace = 'wc/v3';
protected $namespace = 'wc/v4';
/**
* Route base.

View File

@ -22,7 +22,7 @@ class WC_Admin_REST_Reports_Downloads_Files_Controller extends WC_REST_Reports_C
*
* @var string
*/
protected $namespace = 'wc/v3';
protected $namespace = 'wc/v4';
/**
* Route base.

View File

@ -22,7 +22,7 @@ class WC_Admin_REST_Reports_Downloads_Stats_Controller extends WC_REST_Reports_C
*
* @var string
*/
protected $namespace = 'wc/v3';
protected $namespace = 'wc/v4';
/**
* Route base.
@ -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,
),
);

View File

@ -22,7 +22,7 @@ class WC_Admin_REST_Reports_Orders_Stats_Controller extends WC_Admin_REST_Report
*
* @var string
*/
protected $namespace = 'wc/v3';
protected $namespace = 'wc/v4';
/**
* Route base.
@ -147,18 +147,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' ),

View File

@ -22,7 +22,7 @@ class WC_Admin_REST_Reports_Performance_Indicators_Controller extends WC_REST_Re
*
* @var string
*/
protected $namespace = 'wc/v3';
protected $namespace = 'wc/v4';
/**
* Route base.
@ -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,14 +107,17 @@ 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/v3/reports' );
$request = new WP_REST_Request( 'GET', '/wc/v4/reports' );
$response = rest_do_request( $request );
$endpoints = $response->get_data();
$allowed_stats = array();
@ -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/v3/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/v3/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 $schema;
}
/**
* 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();

View File

@ -22,7 +22,7 @@ class WC_Admin_REST_Reports_Products_Controller extends WC_REST_Reports_Controll
*
* @var string
*/
protected $namespace = 'wc/v3';
protected $namespace = 'wc/v4';
/**
* Route base.

View File

@ -22,7 +22,7 @@ class WC_Admin_REST_Reports_Products_Stats_Controller extends WC_REST_Reports_Co
*
* @var string
*/
protected $namespace = 'wc/v3';
protected $namespace = 'wc/v4';
/**
* Route base.
@ -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' ),

View File

@ -22,7 +22,7 @@ class WC_Admin_REST_Reports_Revenue_Stats_Controller extends WC_REST_Reports_Con
*
* @var string
*/
protected $namespace = 'wc/v3';
protected $namespace = 'wc/v4';
/**
* Route base.
@ -137,51 +137,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,

View File

@ -22,7 +22,7 @@ class WC_Admin_REST_Reports_Stock_Controller extends WC_REST_Reports_Controller
*
* @var string
*/
protected $namespace = 'wc/v3';
protected $namespace = 'wc/v4';
/**
* Route base.
@ -51,6 +51,23 @@ class WC_Admin_REST_Reports_Stock_Controller extends WC_REST_Reports_Controller
if ( 'date' === $args['orderby'] ) {
$args['orderby'] = 'date ID';
} elseif ( 'stock_status' === $args['orderby'] ) {
$args['meta_query'] = array( // WPCS: slow query ok.
'relation' => 'AND',
'_stock_status' => array(
'key' => '_stock_status',
'compare' => 'EXISTS',
),
'_stock' => array(
'key' => '_stock',
'compare' => 'EXISTS',
'type' => 'NUMERIC',
),
);
$args['orderby'] = array(
'_stock_status' => $args['order'],
'_stock' => 'desc' === $args['order'] ? 'asc' : 'desc',
);
} elseif ( 'stock_quantity' === $args['orderby'] ) {
$args['meta_key'] = '_stock'; // WPCS: slow query ok.
$args['orderby'] = 'meta_value_num';
@ -353,8 +370,9 @@ class WC_Admin_REST_Reports_Stock_Controller extends WC_REST_Reports_Controller
$params['orderby'] = array(
'description' => __( 'Sort collection by object attribute.', 'wc-admin' ),
'type' => 'string',
'default' => 'stock_quantity',
'default' => 'stock_status',
'enum' => array(
'stock_status',
'stock_quantity',
'date',
'id',

View File

@ -22,7 +22,7 @@ class WC_Admin_REST_Reports_Taxes_Controller extends WC_REST_Reports_Controller
*
* @var string
*/
protected $namespace = 'wc/v3';
protected $namespace = 'wc/v4';
/**
* Route base.

View File

@ -22,7 +22,7 @@ class WC_Admin_REST_Reports_Taxes_Stats_Controller extends WC_REST_Reports_Contr
*
* @var string
*/
protected $namespace = 'wc/v3';
protected $namespace = 'wc/v4';
/**
* Route base.
@ -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' ),

View File

@ -22,7 +22,7 @@ class WC_Admin_REST_Reports_Variations_Controller extends WC_REST_Reports_Contro
*
* @var string
*/
protected $namespace = 'wc/v3';
protected $namespace = 'wc/v4';
/**
* Route base.

View File

@ -22,7 +22,7 @@ class WC_Admin_REST_System_Status_Tools_Controller extends WC_REST_System_Status
*
* @var string
*/
protected $namespace = 'wc/v3';
protected $namespace = 'wc/v4';
/**
* A list of available tools for use in the system status section.
@ -55,7 +55,7 @@ class WC_Admin_REST_System_Status_Tools_Controller extends WC_REST_System_Status
switch ( $tool ) {
case 'rebuild_stats':
WC_Admin_Reports_Orders_Stats_Data_Store::queue_order_stats_repopulate_database();
WC_Admin_Api_Init::regenerate_report_data();
$message = __( 'Rebuilding reports data in the background . . .', 'wc-admin' );
break;
default:

View File

@ -12,6 +12,38 @@ defined( 'ABSPATH' ) || exit;
*/
class WC_Admin_Api_Init {
/**
* Action hook for reducing a range of batches down to single actions.
*/
const QUEUE_BATCH_ACTION = 'wc-admin_queue_batches';
/**
* Action hook for queuing an action after another is complete.
*/
const QUEUE_DEPEDENT_ACTION = 'wc-admin_queue_dependent_action';
/**
* Action hook for processing a batch of customers.
*/
const CUSTOMERS_BATCH_ACTION = 'wc-admin_process_customers_batch';
/**
* Action hook for processing a batch of orders.
*/
const ORDERS_BATCH_ACTION = 'wc-admin_process_orders_batch';
/**
* Action hook for initializing the orders lookup batch creation.
*/
const ORDERS_LOOKUP_BATCH_INIT = 'wc-admin_orders_lookup_batch_init';
/**
* Queue instance.
*
* @var WC_Queue_Interface
*/
protected static $queue = null;
/**
* Boostrap REST API.
*/
@ -30,9 +62,41 @@ class WC_Admin_Api_Init {
// Initialize Orders data store class's static vars.
add_action( 'woocommerce_after_register_post_type', array( 'WC_Admin_Api_Init', 'orders_data_store_init' ), 20 );
// Initialize Customers Report data store sync hooks.
// Note: we need to hook into 'wp' before `wc_current_user_is_active`.
// Note: we need to hook in before `wc_current_user_is_active`.
// See: https://github.com/woocommerce/woocommerce/blob/942615101ba00c939c107c3a4820c3d466864872/includes/wc-user-functions.php#L749.
add_action( 'wp', array( 'WC_Admin_Api_Init', 'customers_report_data_store_init' ), 9 );
add_action( 'wp_loaded', array( 'WC_Admin_Api_Init', 'customers_report_data_store_init' ) );
// Initialize scheduled action handlers.
add_action( self::QUEUE_BATCH_ACTION, array( __CLASS__, 'queue_batches' ), 10, 3 );
add_action( self::QUEUE_DEPEDENT_ACTION, array( __CLASS__, 'queue_dependent_action' ), 10, 2 );
add_action( self::CUSTOMERS_BATCH_ACTION, array( __CLASS__, 'customer_lookup_process_batch' ) );
add_action( self::ORDERS_BATCH_ACTION, array( __CLASS__, 'orders_lookup_process_batch' ) );
add_action( self::ORDERS_LOOKUP_BATCH_INIT, array( __CLASS__, 'orders_lookup_batch_init' ) );
// Add currency symbol to orders endpoint response.
add_filter( 'woocommerce_rest_prepare_shop_order_object', array( __CLASS__, 'add_currency_symbol_to_order_response' ) );
}
/**
* Get queue instance.
*
* @return WC_Queue_Interface
*/
public static function queue() {
if ( is_null( self::$queue ) ) {
self::$queue = WC()->queue();
}
return self::$queue;
}
/**
* Set queue instance.
*
* @param WC_Queue_Interface $queue Queue instance.
*/
public static function set_queue( $queue ) {
self::$queue = $queue;
}
/**
@ -97,11 +161,14 @@ 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';
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-orders-stats-controller.php';
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-orders-controller.php';
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-products-controller.php';
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-product-categories-controller.php';
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-product-reviews-controller.php';
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-reports-controller.php';
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-system-status-tools-controller.php';
@ -127,11 +194,14 @@ class WC_Admin_Api_Init {
'woocommerce_admin_rest_controllers',
array(
'WC_Admin_REST_Admin_Notes_Controller',
'WC_Admin_REST_Coupons_Controller',
'WC_Admin_REST_Customers_Controller',
'WC_Admin_REST_Data_Controller',
'WC_Admin_REST_Data_Countries_Controller',
'WC_Admin_REST_Data_Download_Ips_Controller',
'WC_Admin_REST_Orders_Stats_Controller',
'WC_Admin_REST_Orders_Controller',
'WC_Admin_REST_Products_Controller',
'WC_Admin_REST_Product_Categories_Controller',
'WC_Admin_REST_Product_Reviews_Controller',
'WC_Admin_REST_Reports_Controller',
'WC_Admin_REST_System_Status_Tools_Controller',
@ -169,112 +239,134 @@ class WC_Admin_Api_Init {
* @return array
*/
public static function filter_rest_endpoints( $endpoints ) {
// Override GET /wc/v3/system_status/tools.
if ( isset( $endpoints['/wc/v3/system_status/tools'] )
&& isset( $endpoints['/wc/v3/system_status/tools'][1] )
&& isset( $endpoints['/wc/v3/system_status/tools'][0] )
&& $endpoints['/wc/v3/system_status/tools'][1]['callback'][0] instanceof WC_Admin_REST_System_Status_Tools_Controller
// Override GET /wc/v4/system_status/tools.
if ( isset( $endpoints['/wc/v4/system_status/tools'] )
&& isset( $endpoints['/wc/v4/system_status/tools'][1] )
&& isset( $endpoints['/wc/v4/system_status/tools'][0] )
&& $endpoints['/wc/v4/system_status/tools'][1]['callback'][0] instanceof WC_Admin_REST_System_Status_Tools_Controller
) {
$endpoints['/wc/v3/system_status/tools'][0] = $endpoints['/wc/v3/system_status/tools'][1];
$endpoints['/wc/v4/system_status/tools'][0] = $endpoints['/wc/v4/system_status/tools'][1];
}
// // Override GET & PUT for /wc/v3/system_status/tools.
if ( isset( $endpoints['/wc/v3/system_status/tools/(?P<id>[\w-]+)'] )
&& isset( $endpoints['/wc/v3/system_status/tools/(?P<id>[\w-]+)'][3] )
&& isset( $endpoints['/wc/v3/system_status/tools/(?P<id>[\w-]+)'][2] )
&& $endpoints['/wc/v3/system_status/tools/(?P<id>[\w-]+)'][2]['callback'][0] instanceof WC_Admin_REST_System_Status_Tools_Controller
&& $endpoints['/wc/v3/system_status/tools/(?P<id>[\w-]+)'][3]['callback'][0] instanceof WC_Admin_REST_System_Status_Tools_Controller
// // Override GET & PUT for /wc/v4/system_status/tools.
if ( isset( $endpoints['/wc/v4/system_status/tools/(?P<id>[\w-]+)'] )
&& isset( $endpoints['/wc/v4/system_status/tools/(?P<id>[\w-]+)'][3] )
&& isset( $endpoints['/wc/v4/system_status/tools/(?P<id>[\w-]+)'][2] )
&& $endpoints['/wc/v4/system_status/tools/(?P<id>[\w-]+)'][2]['callback'][0] instanceof WC_Admin_REST_System_Status_Tools_Controller
&& $endpoints['/wc/v4/system_status/tools/(?P<id>[\w-]+)'][3]['callback'][0] instanceof WC_Admin_REST_System_Status_Tools_Controller
) {
$endpoints['/wc/v3/system_status/tools/(?P<id>[\w-]+)'][0] = $endpoints['/wc/v3/system_status/tools/(?P<id>[\w-]+)'][2];
$endpoints['/wc/v3/system_status/tools/(?P<id>[\w-]+)'][1] = $endpoints['/wc/v3/system_status/tools/(?P<id>[\w-]+)'][3];
$endpoints['/wc/v4/system_status/tools/(?P<id>[\w-]+)'][0] = $endpoints['/wc/v4/system_status/tools/(?P<id>[\w-]+)'][2];
$endpoints['/wc/v4/system_status/tools/(?P<id>[\w-]+)'][1] = $endpoints['/wc/v4/system_status/tools/(?P<id>[\w-]+)'][3];
}
// Override GET /wc/v3/reports.
if ( isset( $endpoints['/wc/v3/reports'] )
&& isset( $endpoints['/wc/v3/reports'][1] )
&& isset( $endpoints['/wc/v3/reports'][0] )
&& $endpoints['/wc/v3/reports'][1]['callback'][0] instanceof WC_Admin_REST_Reports_Controller
// Override GET /wc/v4/reports.
if ( isset( $endpoints['/wc/v4/reports'] )
&& isset( $endpoints['/wc/v4/reports'][1] )
&& isset( $endpoints['/wc/v4/reports'][0] )
&& $endpoints['/wc/v4/reports'][1]['callback'][0] instanceof WC_Admin_REST_Reports_Controller
) {
$endpoints['/wc/v3/reports'][0] = $endpoints['/wc/v3/reports'][1];
$endpoints['/wc/v4/reports'][0] = $endpoints['/wc/v4/reports'][1];
}
// Override /wc/v3/customers.
if ( isset( $endpoints['/wc/v3/customers'] )
&& isset( $endpoints['/wc/v3/customers'][3] )
&& isset( $endpoints['/wc/v3/customers'][2] )
&& $endpoints['/wc/v3/customers'][2]['callback'][0] instanceof WC_Admin_REST_Customers_Controller
&& $endpoints['/wc/v3/customers'][3]['callback'][0] instanceof WC_Admin_REST_Customers_Controller
// Override /wc/v4/coupons.
if ( isset( $endpoints['/wc/v4/coupons'] )
&& isset( $endpoints['/wc/v4/coupons'][3] )
&& isset( $endpoints['/wc/v4/coupons'][2] )
&& $endpoints['/wc/v4/coupons'][2]['callback'][0] instanceof WC_Admin_REST_Orders_Controller
&& $endpoints['/wc/v4/coupons'][3]['callback'][0] instanceof WC_Admin_REST_Orders_Controller
) {
$endpoints['/wc/v3/customers'][0] = $endpoints['/wc/v3/customers'][2];
$endpoints['/wc/v3/customers'][1] = $endpoints['/wc/v3/customers'][3];
$endpoints['/wc/v4/coupons'][0] = $endpoints['/wc/v4/coupons'][2];
$endpoints['/wc/v4/coupons'][1] = $endpoints['/wc/v4/coupons'][3];
}
// Override /wc/v3/orders/$id.
if ( isset( $endpoints['/wc/v3/orders/(?P<id>[\d]+)'] )
&& isset( $endpoints['/wc/v3/orders/(?P<id>[\d]+)'][5] )
&& isset( $endpoints['/wc/v3/orders/(?P<id>[\d]+)'][4] )
&& isset( $endpoints['/wc/v3/orders/(?P<id>[\d]+)'][3] )
&& $endpoints['/wc/v3/orders/(?P<id>[\d]+)'][3]['callback'][0] instanceof WC_Admin_REST_Orders_Stats_Controller
&& $endpoints['/wc/v3/orders/(?P<id>[\d]+)'][4]['callback'][0] instanceof WC_Admin_REST_Orders_Stats_Controller
&& $endpoints['/wc/v3/orders/(?P<id>[\d]+)'][5]['callback'][0] instanceof WC_Admin_REST_Orders_Stats_Controller
// Override /wc/v4/customers.
if ( isset( $endpoints['/wc/v4/customers'] )
&& isset( $endpoints['/wc/v4/customers'][3] )
&& isset( $endpoints['/wc/v4/customers'][2] )
&& $endpoints['/wc/v4/customers'][2]['callback'][0] instanceof WC_Admin_REST_Customers_Controller
&& $endpoints['/wc/v4/customers'][3]['callback'][0] instanceof WC_Admin_REST_Customers_Controller
) {
$endpoints['/wc/v3/orders/(?P<id>[\d]+)'][0] = $endpoints['/wc/v3/orders/(?P<id>[\d]+)'][3];
$endpoints['/wc/v3/orders/(?P<id>[\d]+)'][1] = $endpoints['/wc/v3/orders/(?P<id>[\d]+)'][4];
$endpoints['/wc/v3/orders/(?P<id>[\d]+)'][2] = $endpoints['/wc/v3/orders/(?P<id>[\d]+)'][5];
$endpoints['/wc/v4/customers'][0] = $endpoints['/wc/v4/customers'][2];
$endpoints['/wc/v4/customers'][1] = $endpoints['/wc/v4/customers'][3];
}
// Override /wc/v3orders.
if ( isset( $endpoints['/wc/v3/orders'] )
&& isset( $endpoints['/wc/v3/orders'][3] )
&& isset( $endpoints['/wc/v3/orders'][2] )
&& $endpoints['/wc/v3/orders'][2]['callback'][0] instanceof WC_Admin_REST_Orders_Stats_Controller
&& $endpoints['/wc/v3/orders'][3]['callback'][0] instanceof WC_Admin_REST_Orders_Stats_Controller
// Override /wc/v4/orders/$id.
if ( isset( $endpoints['/wc/v4/orders/(?P<id>[\d]+)'] )
&& isset( $endpoints['/wc/v4/orders/(?P<id>[\d]+)'][5] )
&& isset( $endpoints['/wc/v4/orders/(?P<id>[\d]+)'][4] )
&& isset( $endpoints['/wc/v4/orders/(?P<id>[\d]+)'][3] )
&& $endpoints['/wc/v4/orders/(?P<id>[\d]+)'][3]['callback'][0] instanceof WC_Admin_REST_Orders_Controller
&& $endpoints['/wc/v4/orders/(?P<id>[\d]+)'][4]['callback'][0] instanceof WC_Admin_REST_Orders_Controller
&& $endpoints['/wc/v4/orders/(?P<id>[\d]+)'][5]['callback'][0] instanceof WC_Admin_REST_Orders_Controller
) {
$endpoints['/wc/v3/orders'][0] = $endpoints['/wc/v3/orders'][2];
$endpoints['/wc/v3/orders'][1] = $endpoints['/wc/v3/orders'][3];
$endpoints['/wc/v4/orders/(?P<id>[\d]+)'][0] = $endpoints['/wc/v4/orders/(?P<id>[\d]+)'][3];
$endpoints['/wc/v4/orders/(?P<id>[\d]+)'][1] = $endpoints['/wc/v4/orders/(?P<id>[\d]+)'][4];
$endpoints['/wc/v4/orders/(?P<id>[\d]+)'][2] = $endpoints['/wc/v4/orders/(?P<id>[\d]+)'][5];
}
// Override /wc/v3/data.
if ( isset( $endpoints['/wc/v3/data'] )
&& isset( $endpoints['/wc/v3/data'][1] )
&& $endpoints['/wc/v3/data'][1]['callback'][0] instanceof WC_Admin_REST_Data_Controller
// Override /wc/v4/orders.
if ( isset( $endpoints['/wc/v4/orders'] )
&& isset( $endpoints['/wc/v4/orders'][3] )
&& isset( $endpoints['/wc/v4/orders'][2] )
&& $endpoints['/wc/v4/orders'][2]['callback'][0] instanceof WC_Admin_REST_Orders_Controller
&& $endpoints['/wc/v4/orders'][3]['callback'][0] instanceof WC_Admin_REST_Orders_Controller
) {
$endpoints['/wc/v3/data'][0] = $endpoints['/wc/v3/data'][1];
$endpoints['/wc/v4/orders'][0] = $endpoints['/wc/v4/orders'][2];
$endpoints['/wc/v4/orders'][1] = $endpoints['/wc/v4/orders'][3];
}
// Override /wc/v3/products.
if ( isset( $endpoints['/wc/v3/products'] )
&& isset( $endpoints['/wc/v3/products'][3] )
&& isset( $endpoints['/wc/v3/products'][2] )
&& $endpoints['/wc/v3/products'][2]['callback'][0] instanceof WC_Admin_REST_Products_Controller
&& $endpoints['/wc/v3/products'][3]['callback'][0] instanceof WC_Admin_REST_Products_Controller
// Override /wc/v4/data.
if ( isset( $endpoints['/wc/v4/data'] )
&& isset( $endpoints['/wc/v4/data'][1] )
&& $endpoints['/wc/v4/data'][1]['callback'][0] instanceof WC_Admin_REST_Data_Controller
) {
$endpoints['/wc/v3/products'][0] = $endpoints['/wc/v3/products'][2];
$endpoints['/wc/v3/products'][1] = $endpoints['/wc/v3/products'][3];
$endpoints['/wc/v4/data'][0] = $endpoints['/wc/v4/data'][1];
}
// Override /wc/v3/products/$id.
if ( isset( $endpoints['/wc/v3/products/(?P<id>[\d]+)'] )
&& isset( $endpoints['/wc/v3/products/(?P<id>[\d]+)'][5] )
&& isset( $endpoints['/wc/v3/products/(?P<id>[\d]+)'][4] )
&& isset( $endpoints['/wc/v3/products/(?P<id>[\d]+)'][3] )
&& $endpoints['/wc/v3/products/(?P<id>[\d]+)'][3]['callback'][0] instanceof WC_Admin_REST_Products_Controller
&& $endpoints['/wc/v3/products/(?P<id>[\d]+)'][4]['callback'][0] instanceof WC_Admin_REST_Products_Controller
&& $endpoints['/wc/v3/products/(?P<id>[\d]+)'][5]['callback'][0] instanceof WC_Admin_REST_Products_Controller
// Override /wc/v4/products.
if ( isset( $endpoints['/wc/v4/products'] )
&& isset( $endpoints['/wc/v4/products'][3] )
&& isset( $endpoints['/wc/v4/products'][2] )
&& $endpoints['/wc/v4/products'][2]['callback'][0] instanceof WC_Admin_REST_Products_Controller
&& $endpoints['/wc/v4/products'][3]['callback'][0] instanceof WC_Admin_REST_Products_Controller
) {
$endpoints['/wc/v3/products/(?P<id>[\d]+)'][0] = $endpoints['/wc/v3/products/(?P<id>[\d]+)'][3];
$endpoints['/wc/v3/products/(?P<id>[\d]+)'][1] = $endpoints['/wc/v3/products/(?P<id>[\d]+)'][4];
$endpoints['/wc/v3/products/(?P<id>[\d]+)'][2] = $endpoints['/wc/v3/products/(?P<id>[\d]+)'][5];
$endpoints['/wc/v4/products'][0] = $endpoints['/wc/v4/products'][2];
$endpoints['/wc/v4/products'][1] = $endpoints['/wc/v4/products'][3];
}
// Override /wc/v3/products/reviews.
if ( isset( $endpoints['/wc/v3/products/reviews'] )
&& isset( $endpoints['/wc/v3/products/reviews'][3] )
&& isset( $endpoints['/wc/v3/products/reviews'][2] )
&& $endpoints['/wc/v3/products/reviews'][2]['callback'][0] instanceof WC_Admin_REST_Product_Reviews_Controller
&& $endpoints['/wc/v3/products/reviews'][3]['callback'][0] instanceof WC_Admin_REST_Product_Reviews_Controller
// Override /wc/v4/products/$id.
if ( isset( $endpoints['/wc/v4/products/(?P<id>[\d]+)'] )
&& isset( $endpoints['/wc/v4/products/(?P<id>[\d]+)'][5] )
&& isset( $endpoints['/wc/v4/products/(?P<id>[\d]+)'][4] )
&& isset( $endpoints['/wc/v4/products/(?P<id>[\d]+)'][3] )
&& $endpoints['/wc/v4/products/(?P<id>[\d]+)'][3]['callback'][0] instanceof WC_Admin_REST_Products_Controller
&& $endpoints['/wc/v4/products/(?P<id>[\d]+)'][4]['callback'][0] instanceof WC_Admin_REST_Products_Controller
&& $endpoints['/wc/v4/products/(?P<id>[\d]+)'][5]['callback'][0] instanceof WC_Admin_REST_Products_Controller
) {
$endpoints['/wc/v3/products/reviews'][0] = $endpoints['/wc/v3/products/reviews'][2];
$endpoints['/wc/v3/products/reviews'][1] = $endpoints['/wc/v3/products/reviews'][3];
$endpoints['/wc/v4/products/(?P<id>[\d]+)'][0] = $endpoints['/wc/v4/products/(?P<id>[\d]+)'][3];
$endpoints['/wc/v4/products/(?P<id>[\d]+)'][1] = $endpoints['/wc/v4/products/(?P<id>[\d]+)'][4];
$endpoints['/wc/v4/products/(?P<id>[\d]+)'][2] = $endpoints['/wc/v4/products/(?P<id>[\d]+)'][5];
}
// Override /wc/v4/products/categories.
if ( isset( $endpoints['/wc/v4/products/categories'] )
&& isset( $endpoints['/wc/v4/products/categories'][3] )
&& isset( $endpoints['/wc/v4/products/categories'][2] )
&& $endpoints['/wc/v4/products/categories'][2]['callback'][0] instanceof WC_Admin_REST_Product_categories_Controller
&& $endpoints['/wc/v4/products/categories'][3]['callback'][0] instanceof WC_Admin_REST_Product_categories_Controller
) {
$endpoints['/wc/v4/products/categories'][0] = $endpoints['/wc/v4/products/categories'][2];
$endpoints['/wc/v4/products/categories'][1] = $endpoints['/wc/v4/products/categories'][3];
}
// Override /wc/v4/products/reviews.
if ( isset( $endpoints['/wc/v4/products/reviews'] )
&& isset( $endpoints['/wc/v4/products/reviews'][3] )
&& isset( $endpoints['/wc/v4/products/reviews'][2] )
&& $endpoints['/wc/v4/products/reviews'][2]['callback'][0] instanceof WC_Admin_REST_Product_Reviews_Controller
&& $endpoints['/wc/v4/products/reviews'][3]['callback'][0] instanceof WC_Admin_REST_Product_Reviews_Controller
) {
$endpoints['/wc/v4/products/reviews'][0] = $endpoints['/wc/v4/products/reviews'][2];
$endpoints['/wc/v4/products/reviews'][1] = $endpoints['/wc/v4/products/reviews'][3];
}
return $endpoints;
@ -286,9 +378,9 @@ class WC_Admin_Api_Init {
public static function regenerate_report_data() {
// Add registered customers to the lookup table before updating order stats
// so that the orders can be associated with the `customer_id` column.
self::customer_lookup_store_init();
WC_Admin_Reports_Orders_Stats_Data_Store::queue_order_stats_repopulate_database();
self::order_product_lookup_store_init();
self::customer_lookup_batch_init();
// Queue orders lookup to occur after customers lookup generation is done.
self::queue_dependent_action( self::ORDERS_LOOKUP_BATCH_INIT, self::CUSTOMERS_BATCH_ACTION );
}
/**
@ -322,39 +414,54 @@ class WC_Admin_Api_Init {
}
/**
* Init orders product lookup store.
*
* @param WC_Background_Updater|null $updater Updater instance.
* @return bool
* Init order/product lookup tables update (in batches).
*/
public static function order_product_lookup_store_init( $updater = null ) {
// TODO: this needs to be updated a bit, as it no longer runs as a part of WC_Install, there is no bg updater.
global $wpdb;
public static function orders_lookup_batch_init() {
$batch_size = self::get_batch_size( self::ORDERS_BATCH_ACTION );
$order_query = new WC_Order_Query(
array(
'return' => 'ids',
'limit' => 1,
'paginate' => true,
)
);
$result = $order_query->get_orders();
$orders = get_transient( 'wc_update_350_all_orders' );
if ( false === $orders ) {
$orders = wc_get_orders(
array(
'limit' => -1,
'return' => 'ids',
)
);
set_transient( 'wc_update_350_all_orders', $orders, DAY_IN_SECONDS );
if ( 0 === $result->total ) {
return;
}
// Process orders until close to running out of memory timeouts on large sites then requeue.
foreach ( $orders as $order_id ) {
$num_batches = ceil( $result->total / $batch_size );
self::queue_batches( 1, $num_batches, self::ORDERS_BATCH_ACTION );
}
/**
* Process a batch of orders to update (stats and products).
*
* @param int $batch_number Batch number to process (essentially a query page number).
* @return void
*/
public static function orders_lookup_process_batch( $batch_number ) {
$batch_size = self::get_batch_size( self::ORDERS_BATCH_ACTION );
$order_query = new WC_Order_Query(
array(
'return' => 'ids',
'limit' => $batch_size,
'page' => $batch_number,
'orderby' => 'ID',
'order' => 'ASC',
)
);
$order_ids = $order_query->get_orders();
foreach ( $order_ids as $order_id ) {
// TODO: schedule single order update if this fails?
WC_Admin_Reports_Orders_Stats_Data_Store::sync_order( $order_id );
WC_Admin_Reports_Products_Data_Store::sync_order_products( $order_id );
// Pop the order ID from the array for updating the transient later should we near memory exhaustion.
unset( $orders[ $order_id ] );
if ( $updater instanceof WC_Background_Updater && $updater->is_memory_exceeded() ) {
// Update the transient for the next run to avoid processing the same orders again.
set_transient( 'wc_update_350_all_orders', $orders, DAY_IN_SECONDS );
return true;
}
WC_Admin_Reports_Coupons_Data_Store::sync_order_coupons( $order_id );
WC_Admin_Reports_Taxes_Data_Store::sync_order_taxes( $order_id );
}
return true;
}
/**
@ -365,49 +472,144 @@ class WC_Admin_Api_Init {
}
/**
* Init customer lookup store.
* Returns the batch size for regenerating reports.
* Note: can differ per batch action.
*
* @param WC_Background_Updater|null $updater Updater instance.
* @return bool
* @param string $action Single batch action name.
* @return int Batch size.
*/
public static function customer_lookup_store_init( $updater = null ) {
// TODO: this needs to be updated a bit, as it no longer runs as a part of WC_Install, there is no bg updater.
global $wpdb;
public static function get_batch_size( $action ) {
$batch_sizes = array(
self::QUEUE_BATCH_ACTION => 100,
self::CUSTOMERS_BATCH_ACTION => 25,
self::ORDERS_BATCH_ACTION => 10,
);
$batch_size = isset( $batch_sizes[ $action ] ) ? $batch_sizes[ $action ] : 25;
// Backfill customer lookup table with registered customers.
$customer_ids = get_transient( 'wc_update_350_all_customers' );
/**
* Filter the batch size for regenerating a report table.
*
* @param int $batch_size Batch size.
* @param string $action Batch action name.
*/
return apply_filters( 'wc_admin_report_regenerate_batch_size', $batch_size, $action );
}
if ( false === $customer_ids ) {
$customer_query = new WP_User_Query(
array(
'fields' => 'ID',
'role' => 'customer',
'number' => -1,
)
/**
* Queue a large number of batch jobs, respecting the batch size limit.
* Reduces a range of batches down to "single batch" jobs.
*
* @param int $range_start Starting batch number.
* @param int $range_end Ending batch number.
* @param string $single_batch_action Action to schedule for a single batch.
* @return void
*/
public static function queue_batches( $range_start, $range_end, $single_batch_action ) {
$batch_size = self::get_batch_size( self::QUEUE_BATCH_ACTION );
$range_size = 1 + ( $range_end - $range_start );
$action_timestamp = time() + 5;
if ( $range_size > $batch_size ) {
// If the current batch range is larger than a single batch,
// split the range into $queue_batch_size chunks.
$chunk_size = ceil( $range_size / $batch_size );
for ( $i = 0; $i < $batch_size; $i++ ) {
$batch_start = $range_start + ( $i * $chunk_size );
$batch_end = min( $range_end, $range_start + ( $chunk_size * ( $i + 1 ) ) - 1 );
self::queue()->schedule_single(
$action_timestamp,
self::QUEUE_BATCH_ACTION,
array( $batch_start, $batch_end, $single_batch_action )
);
}
} else {
// Otherwise, queue the single batches.
for ( $i = $range_start; $i <= $range_end; $i++ ) {
self::queue()->schedule_single( $action_timestamp, $single_batch_action, array( $i ) );
}
}
}
/**
* Queue an action to run after another.
*
* @param string $action Action to run after prerequisite.
* @param string $prerequisite_action Prerequisite action.
*/
public static function queue_dependent_action( $action, $prerequisite_action ) {
$blocking_jobs = self::queue()->search(
array(
'status' => 'pending',
'orderby' => 'date',
'order' => 'DESC',
'per_page' => 1,
'claimed' => false,
'search' => $prerequisite_action, // search is used instead of hook to find queued batch creation.
)
);
if ( $blocking_jobs ) {
$blocking_job = current( $blocking_jobs );
$after_blocking_job = $blocking_job->get_schedule()->next()->getTimestamp() + 5;
self::queue()->schedule_single(
$after_blocking_job,
self::QUEUE_DEPEDENT_ACTION,
array( $action, $prerequisite_action )
);
} else {
self::queue()->schedule_single( time() + 5, $action );
}
}
$customer_ids = $customer_query->get_results();
/**
* Init customer lookup table update (in batches).
*/
public static function customer_lookup_batch_init() {
$batch_size = self::get_batch_size( self::CUSTOMERS_BATCH_ACTION );
$customer_query = new WP_User_Query(
array(
'fields' => 'ID',
'number' => 1,
)
);
$total_customers = $customer_query->get_total();
set_transient( 'wc_update_350_all_customers', $customer_ids, DAY_IN_SECONDS );
if ( 0 === $total_customers ) {
return;
}
// Process customers until close to running out of memory timeouts on large sites then requeue.
$num_batches = ceil( $total_customers / $batch_size );
self::queue_batches( 1, $num_batches, self::CUSTOMERS_BATCH_ACTION );
}
/**
* Process a batch of customers to update.
*
* @param int $batch_number Batch number to process (essentially a query page number).
* @return void
*/
public static function customer_lookup_process_batch( $batch_number ) {
$batch_size = self::get_batch_size( self::CUSTOMERS_BATCH_ACTION );
$customer_query = new WP_User_Query(
array(
'fields' => 'ID',
'orderby' => 'ID',
'order' => 'ASC',
'number' => $batch_size,
'paged' => $batch_number,
)
);
$customer_ids = $customer_query->get_results();
foreach ( $customer_ids as $customer_id ) {
$result = WC_Admin_Reports_Customers_Data_Store::update_registered_customer( $customer_id );
if ( $result ) {
// Pop the customer ID from the array for updating the transient later should we near memory exhaustion.
unset( $customer_ids[ $customer_id ] );
}
if ( $updater instanceof WC_Background_Updater && $updater->is_memory_exceeded() ) {
// Update the transient for the next run to avoid processing the same orders again.
set_transient( 'wc_update_350_all_customers', $customer_ids, DAY_IN_SECONDS );
return true;
}
// TODO: schedule single customer update if this fails?
WC_Admin_Reports_Customers_Data_Store::update_registered_customer( $customer_id );
}
return true;
}
/**
@ -595,10 +797,25 @@ class WC_Admin_Api_Init {
self::create_db_tables();
// Initialize report tables.
add_action( 'woocommerce_after_register_post_type', array( 'WC_Admin_Api_Init', 'order_product_lookup_store_init' ), 20 );
add_action( 'woocommerce_after_register_post_type', array( 'WC_Admin_Api_Init', 'customer_lookup_store_init' ), 20 );
add_action( 'woocommerce_after_register_post_type', array( __CLASS__, 'regenerate_report_data' ), 20 );
}
/**
* Add the currency symbol (in addition to currency code) to each Order
* object in REST API responses. For use in formatCurrency().
*
* @param {WP_REST_Response} $response REST response object.
* @returns {WP_REST_Response}
*/
public static function add_currency_symbol_to_order_response( $response ) {
$response_data = $response->get_data();
$currency_code = $response_data['currency'];
$currency_symbol = get_woocommerce_currency_symbol( $currency_code );
$response_data['currency_symbol'] = html_entity_decode( $currency_symbol );
$response->set_data( $response_data );
return $response;
}
}
new WC_Admin_Api_Init();

View File

@ -1,77 +0,0 @@
<?php
/**
* Order stats background process.
*
* @package WooCommerce Admin/Classes
*/
defined( 'ABSPATH' ) || exit;
if ( ! class_exists( 'WC_Background_Process', false ) ) {
include_once WC_ABSPATH . '/includes/abstracts/class-wc-background-process.php';
}
/**
* WC_Admin_Order_Stats_Background_Process class.
*
* @todo use Action Scheduler instead of this.
*/
class WC_Admin_Order_Stats_Background_Process extends WC_Background_Process {
/**
* Initiate new background process.
*/
public function __construct() {
// Uses unique prefix per blog so each blog has separate queue.
$this->prefix = 'wp_' . get_current_blog_id();
$this->action = 'wc_order_stats';
parent::__construct();
}
/**
* Push to queue without scheduling duplicate recalculation events.
* Overrides WC_Background_Process::push_to_queue.
*
* @param integer $data Timestamp of hour to generate stats.
*/
public function push_to_queue( $data ) {
$data = absint( $data );
if ( ! in_array( $data, $this->data, true ) ) {
$this->data[] = $data;
}
return $this;
}
/**
* Dispatch but only if there is data to update.
* Overrides WC_Background_Process::dispatch.
*/
public function dispatch() {
if ( ! $this->data ) {
return false;
}
return parent::dispatch();
}
/**
* Code to execute for each item in the queue
*
* @param string $item Queue item to iterate over.
* @return bool
*/
protected function task( $item ) {
if ( ! $item ) {
return false;
}
$order = wc_get_order( $item );
if ( ! $order ) {
return false;
}
WC_Admin_Reports_Orders_Stats_Data_Store::update( $order );
return false;
}
}

View File

@ -42,7 +42,7 @@ class WC_Admin_Reports_Orders_Data_Store extends WC_Admin_Reports_Data_Store imp
protected $report_columns = array(
'order_id' => 'order_id',
'date_created' => 'date_created',
'status' => 'status',
'status' => 'REPLACE(status, "wc-", "") as status',
'customer_id' => 'customer_id',
'net_total' => 'net_total',
'num_items_sold' => 'num_items_sold',
@ -240,6 +240,7 @@ class WC_Admin_Reports_Orders_Data_Store extends WC_Admin_Reports_Data_Store imp
$mapped_orders = $this->map_array_by_key( $orders_data, 'order_id' );
$products = $this->get_products_by_order_ids( array_keys( $mapped_orders ) );
$mapped_products = $this->map_array_by_key( $products, 'product_id' );
$coupons = $this->get_coupons_by_order_ids( array_keys( $mapped_orders ) );
$product_categories = $this->get_product_categories_by_product_ids( array_keys( $mapped_products ) );
$mapped_data = array();
@ -250,8 +251,9 @@ class WC_Admin_Reports_Orders_Data_Store extends WC_Admin_Reports_Data_Store imp
}
$mapped_data[ $product['order_id'] ]['products'][] = array(
'id' => $product['product_id'],
'name' => $product['product_name'],
'id' => $product['product_id'],
'name' => $product['product_name'],
'quantity' => $product['product_quantity'],
);
$mapped_data[ $product['order_id'] ]['categories'] = array_unique(
array_merge(
@ -261,8 +263,24 @@ class WC_Admin_Reports_Orders_Data_Store extends WC_Admin_Reports_Data_Store imp
);
}
foreach ( $coupons as $coupon ) {
if ( ! isset( $mapped_data[ $coupon['order_id'] ] ) ) {
$mapped_data[ $product['order_id'] ]['coupons'] = array();
}
$mapped_data[ $coupon['order_id'] ]['coupons'][] = array(
'id' => $coupon['coupon_id'],
'code' => wc_format_coupon_code( $coupon['coupon_code'] ),
);
}
foreach ( $orders_data as $key => $order_data ) {
$orders_data[ $key ]['extended_info'] = $mapped_data[ $order_data['order_id'] ];
$defaults = array(
'products' => array(),
'categories' => array(),
'coupons' => array(),
);
$orders_data[ $key ]['extended_info'] = isset( $mapped_data[ $order_data['order_id'] ] ) ? array_merge( $defaults, $mapped_data[ $order_data['order_id'] ] ) : $defaults;
}
}
@ -282,7 +300,7 @@ class WC_Admin_Reports_Orders_Data_Store extends WC_Admin_Reports_Data_Store imp
}
/**
* Get product Ids, names, and categories from order IDs.
* Get product IDs, names, and quantity from order IDs.
*
* @param array $order_ids Array of order IDs.
* @return array
@ -293,7 +311,7 @@ class WC_Admin_Reports_Orders_Data_Store extends WC_Admin_Reports_Data_Store imp
$included_order_ids = implode( ',', $order_ids );
$products = $wpdb->get_results(
"SELECT order_id, ID as product_id, post_title as product_name
"SELECT order_id, ID as product_id, post_title as product_name, product_qty as product_quantity
FROM {$wpdb->prefix}posts
JOIN {$order_product_lookup_table} ON {$order_product_lookup_table}.product_id = {$wpdb->prefix}posts.ID
WHERE
@ -305,6 +323,30 @@ class WC_Admin_Reports_Orders_Data_Store extends WC_Admin_Reports_Data_Store imp
return $products;
}
/**
* Get coupon information from order IDs.
*
* @param array $order_ids Array of order IDs.
* @return array
*/
protected function get_coupons_by_order_ids( $order_ids ) {
global $wpdb;
$order_coupon_lookup_table = $wpdb->prefix . 'wc_order_coupon_lookup';
$included_order_ids = implode( ',', $order_ids );
$coupons = $wpdb->get_results(
"SELECT order_id, coupon_id, post_title as coupon_code
FROM {$wpdb->prefix}posts
JOIN {$order_coupon_lookup_table} ON {$order_coupon_lookup_table}.coupon_id = {$wpdb->prefix}posts.ID
WHERE
order_id IN ({$included_order_ids})
",
ARRAY_A
); // WPCS: cache ok, DB call ok, unprepared SQL ok.
return $coupons;
}
/**
* Get product categories by array of product IDs
*
@ -335,7 +377,6 @@ class WC_Admin_Reports_Orders_Data_Store extends WC_Admin_Reports_Data_Store imp
return $mapped_product_categories;
}
/**
* Returns string to be used as cache key for the data.
*

View File

@ -7,10 +7,6 @@
defined( 'ABSPATH' ) || exit;
if ( ! class_exists( 'WC_Admin_Order_Stats_Background_Process', false ) ) {
include_once WC_ADMIN_ABSPATH . '/includes/class-wc-admin-order-stats-background-process.php';
}
/**
* WC_Admin_Reports_Orders_Stats_Data_Store.
*
@ -68,26 +64,11 @@ class WC_Admin_Reports_Orders_Stats_Data_Store extends WC_Admin_Reports_Data_Sto
'net_revenue' => '( SUM(net_total) - SUM(refund_total) ) AS net_revenue',
'avg_items_per_order' => 'AVG(num_items_sold) AS avg_items_per_order',
'avg_order_value' => '( SUM(net_total) - SUM(refund_total) ) / COUNT(*) AS avg_order_value',
'num_returning_customers' => 'SUM(returning_customer = 1) AS num_returning_customers',
'num_new_customers' => 'SUM(returning_customer = 0) AS num_new_customers',
// Count returning customers as ( total_customers - new_customers ) to get an accurate number and count customers in with both new and old statuses as new.
'num_returning_customers' => '( COUNT( DISTINCT( customer_id ) ) - COUNT( DISTINCT( CASE WHEN returning_customer = 0 THEN customer_id END ) ) ) AS num_returning_customers',
'num_new_customers' => 'COUNT( DISTINCT( CASE WHEN returning_customer = 0 THEN customer_id END ) ) AS num_new_customers',
);
/**
* Background process to populate order stats.
*
* @var WC_Admin_Order_Stats_Background_Process
*/
protected static $background_process;
/**
* Constructor.
*/
public function __construct() {
if ( ! self::$background_process ) {
self::$background_process = new WC_Admin_Order_Stats_Background_Process();
}
}
/**
* Set up all the hooks for maintaining and populating table data.
*/
@ -97,10 +78,7 @@ class WC_Admin_Reports_Orders_Stats_Data_Store extends WC_Admin_Reports_Data_Sto
add_action( 'clean_post_cache', array( __CLASS__, 'sync_order' ) );
add_action( 'woocommerce_order_refunded', array( __CLASS__, 'sync_order' ) );
add_action( 'woocommerce_refund_deleted', array( __CLASS__, 'sync_on_refund_delete' ), 10, 2 );
if ( ! self::$background_process ) {
self::$background_process = new WC_Admin_Order_Stats_Background_Process();
}
add_action( 'delete_post', array( __CLASS__, 'delete_order' ) );
}
/**
@ -378,31 +356,6 @@ class WC_Admin_Reports_Orders_Stats_Data_Store extends WC_Admin_Reports_Data_Sto
); // WPCS: cache ok, DB call ok, unprepared SQL ok.
}
/**
* Queue a background process that will repopulate the entire orders stats database.
*
* @todo Make this work on large DBs.
*/
public static function queue_order_stats_repopulate_database() {
// This needs to be updated to work in batches instead of getting all orders, as
// that will not work well on DBs with more than a few hundred orders.
$order_ids = wc_get_orders(
array(
'limit' => -1,
'type' => 'shop_order',
'return' => 'ids',
)
);
foreach ( $order_ids as $id ) {
self::$background_process->push_to_queue( $id );
}
self::$background_process->save();
self::$background_process->dispatch();
}
/**
* Add order information to the lookup table when orders are created or modified.
*
@ -500,6 +453,28 @@ class WC_Admin_Reports_Orders_Stats_Data_Store extends WC_Admin_Reports_Data_Sto
return $wpdb->replace( $table_name, $data, $format );
}
/**
* Deletes the order stats when an order is deleted.
*
* @param int $post_id Post ID.
*/
public static function delete_order( $post_id ) {
global $wpdb;
$table_name = $wpdb->prefix . self::TABLE_NAME;
if ( 'shop_order' !== get_post_type( $post_id ) ) {
return;
}
$wpdb->query(
$wpdb->prepare(
"DELETE FROM ${table_name} WHERE order_id = %d",
$post_id
)
);
}
/**
* Calculation methods.
*/
@ -528,23 +503,23 @@ class WC_Admin_Reports_Orders_Stats_Data_Store extends WC_Admin_Reports_Data_Sto
* @return bool
*/
protected static function is_returning_customer( $order ) {
$customer_id = $order->get_user_id();
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 ( 0 === $customer_id ) {
if ( ! $customer_id ) {
return false;
}
$customer_orders = get_posts(
array(
'meta_key' => '_customer_user', // WPCS: slow query ok.
'meta_value' => $customer_id, // WPCS: slow query ok.
'post_type' => 'shop_order',
'post_status' => array( 'wc-on-hold', 'wc-processing', 'wc-completed' ),
'numberposts' => 2,
$customer_orders = $wpdb->get_var(
$wpdb->prepare(
"SELECT COUNT(*) FROM ${orders_stats_table} WHERE customer_id = %d AND date_created < %s",
$customer_id,
date( 'Y-m-d H:i:s', $order->get_date_created()->getTimestamp() )
)
);
return count( $customer_orders ) > 1;
return $customer_orders >= 1;
}
/**

View File

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

View File

@ -153,7 +153,8 @@ function wc_admin_print_script_settings() {
}
$preload_data_endpoints = array(
'countries' => '/wc/v3/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 ) {

View File

@ -269,10 +269,13 @@ function wc_admin_currency_settings() {
return apply_filters(
'wc_currency_settings',
array(
'code' => $code,
'precision' => wc_get_price_decimals(),
'symbol' => get_woocommerce_currency_symbol( $code ),
'position' => get_option( 'woocommerce_currency_pos' ),
'code' => $code,
'precision' => wc_get_price_decimals(),
'symbol' => html_entity_decode( get_woocommerce_currency_symbol( $code ) ),
'position' => get_option( 'woocommerce_currency_pos' ),
'decimal_separator' => wc_get_price_decimal_separator(),
'thousand_separator' => wc_get_price_thousand_separator(),
'price_format' => html_entity_decode( get_woocommerce_price_format() ),
)
);
}

View File

@ -1,6 +1,6 @@
{
"name": "wc-admin",
"version": "0.4.0",
"version": "0.5.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@ -6102,9 +6102,9 @@
}
},
"core-js": {
"version": "2.6.1",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.1.tgz",
"integrity": "sha512-L72mmmEayPJBejKIWe2pYtGis5r0tQ5NaJekdhyXgeMQTpJoBsH0NL4ElY2LfSoV15xeQWKQ+XTTOZdyero5Xg=="
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.2.tgz",
"integrity": "sha512-NdBPF/RVwPW6jr0NCILuyN9RiqLo2b1mddWHkUL+VnvcB7dzlnBJ1bXYntjpTGOgkZiiLWj2JxmOr7eGE3qK6g=="
},
"core-util-is": {
"version": "1.0.2",
@ -7238,9 +7238,9 @@
}
},
"dompurify": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-1.0.8.tgz",
"integrity": "sha512-vetRFbN1SXSPfP3ClIiYnxTrXquSqakBEOoB5JESn0SVcSYzpu6ougjakpKnskGctYdlNpwf+riUHSkG7d4XUw=="
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-1.0.9.tgz",
"integrity": "sha512-lt9f3A3RO1OCNaWdA+s/k7YVn0Typ5MbAKmX94PLCZbs8wLNccX3Bj4xXA7GLKOoDb/MeVoAoeIJarZD1JUnjg=="
},
"domutils": {
"version": "1.5.1",
@ -7811,9 +7811,9 @@
}
},
"eslint-plugin-jest": {
"version": "22.1.2",
"resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-22.1.2.tgz",
"integrity": "sha512-jSPT4rVmNetkeCIyrvvOM0wJtgoUSbKHIUDoOGzIISsg51eWN/nISPNKVM+jXMMDI9oowbyapOnpKSXlsLiDpQ==",
"version": "22.1.3",
"resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-22.1.3.tgz",
"integrity": "sha512-JTZTI6WQoNruAugNyCO8fXfTONVcDd5i6dMRFA5g3rUFn1UDDLILY1bTL6alvNXbW2U7Sc2OSpi8m08pInnq0A==",
"dev": true
},
"eslint-plugin-jsx-a11y": {
@ -8947,8 +8947,7 @@
},
"ansi-regex": {
"version": "2.1.1",
"bundled": true,
"optional": true
"bundled": true
},
"aproba": {
"version": "1.2.0",
@ -8966,13 +8965,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"
@ -8985,18 +8982,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",
@ -9099,8 +9093,7 @@
},
"inherits": {
"version": "2.0.3",
"bundled": true,
"optional": true
"bundled": true
},
"ini": {
"version": "1.3.5",
@ -9110,7 +9103,6 @@
"is-fullwidth-code-point": {
"version": "1.0.0",
"bundled": true,
"optional": true,
"requires": {
"number-is-nan": "^1.0.0"
}
@ -9123,20 +9115,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.2.4",
"bundled": true,
"optional": true,
"requires": {
"safe-buffer": "^5.1.1",
"yallist": "^3.0.0"
@ -9153,7 +9142,6 @@
"mkdirp": {
"version": "0.5.1",
"bundled": true,
"optional": true,
"requires": {
"minimist": "0.0.8"
}
@ -9226,8 +9214,7 @@
},
"number-is-nan": {
"version": "1.0.1",
"bundled": true,
"optional": true
"bundled": true
},
"object-assign": {
"version": "4.1.1",
@ -9237,7 +9224,6 @@
"once": {
"version": "1.4.0",
"bundled": true,
"optional": true,
"requires": {
"wrappy": "1"
}
@ -9313,8 +9299,7 @@
},
"safe-buffer": {
"version": "5.1.1",
"bundled": true,
"optional": true
"bundled": true
},
"safer-buffer": {
"version": "2.1.2",
@ -9344,7 +9329,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",
@ -9362,7 +9346,6 @@
"strip-ansi": {
"version": "3.0.1",
"bundled": true,
"optional": true,
"requires": {
"ansi-regex": "^2.0.0"
}
@ -9401,13 +9384,11 @@
},
"wrappy": {
"version": "1.0.2",
"bundled": true,
"optional": true
"bundled": true
},
"yallist": {
"version": "3.0.2",
"bundled": true,
"optional": true
"bundled": true
}
}
},
@ -12929,6 +12910,11 @@
"semver": "^5.4.1"
}
},
"locutus": {
"version": "2.0.10",
"resolved": "https://registry.npmjs.org/locutus/-/locutus-2.0.10.tgz",
"integrity": "sha512-AZg2kCqrquMJ5FehDsBidV0qHl98NrsYtseUClzjAQ3HFnsDBJTCwGVplSQ82t9/QfgahqvTjaKcZqZkHmS0wQ=="
},
"lodash": {
"version": "4.17.11",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz",
@ -14131,6 +14117,12 @@
}
}
},
"node-watch": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/node-watch/-/node-watch-0.6.0.tgz",
"integrity": "sha512-XAgTL05z75ptd7JSVejH1a2Dm1zmXYhuDr9l230Qk6Z7/7GPcnAs/UyJJ4ggsXSvWil8iOzwQLW0zuGUvHpG8g==",
"dev": true
},
"node-wp-i18n": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/node-wp-i18n/-/node-wp-i18n-1.2.2.tgz",
@ -17033,23 +17025,17 @@
}
},
"recast": {
"version": "0.16.1",
"resolved": "https://registry.npmjs.org/recast/-/recast-0.16.1.tgz",
"integrity": "sha512-ZUQm94F3AHozRaTo4Vz6yIgkSEZIL7p+BsWeGZ23rx+ZVRoqX+bvBA8br0xmCOU0DSR4qYGtV7Y5HxTsC4V78A==",
"version": "0.16.2",
"resolved": "https://registry.npmjs.org/recast/-/recast-0.16.2.tgz",
"integrity": "sha512-O/7qXi51DPjRVdbrpNzoBQH5dnAPQNbfoOFyRiUwreTMJfIHYOEBzwuH+c0+/BTSJ3CQyKs6ILSWXhESH6Op3A==",
"dev": true,
"requires": {
"ast-types": "0.11.6",
"ast-types": "0.11.7",
"esprima": "~4.0.0",
"private": "~0.1.5",
"source-map": "~0.6.1"
},
"dependencies": {
"ast-types": {
"version": "0.11.6",
"resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.11.6.tgz",
"integrity": "sha512-nHiuV14upVGl7MWwFUYbzJ6YlfwWS084CU9EA8HajfYQjMSli5TQi3UTRygGF58LFWVkXxS1rbgRhROEqlQkXg==",
"dev": true
},
"esprima": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",

View File

@ -1,6 +1,6 @@
{
"name": "wc-admin",
"version": "0.4.0",
"version": "0.5.0",
"main": "js/index.js",
"author": "Automattic",
"license": "GPL-2.0-or-later",
@ -76,7 +76,7 @@
"eslint": "5.12.0",
"eslint-config-wpcalypso": "4.0.1",
"eslint-loader": "2.1.1",
"eslint-plugin-jest": "22.1.2",
"eslint-plugin-jest": "22.1.3",
"eslint-plugin-jsx-a11y": "6.1.2",
"eslint-plugin-react": "7.12.3",
"eslint-plugin-wpcalypso": "4.0.2",
@ -87,7 +87,7 @@
"lerna": "3.10.5",
"mini-css-extract-plugin": "0.5.0",
"node-sass": "4.11.0",
"node-watch": "^0.6.0",
"node-watch": "0.6.0",
"postcss-color-function": "4.0.1",
"postcss-loader": "3.0.0",
"prettier": "github:automattic/calypso-prettier#c56b4251",
@ -95,7 +95,7 @@
"raw-loader": "1.0.0",
"react-docgen": "2.21.0",
"readline-sync": "1.4.9",
"recast": "0.16.1",
"recast": "0.16.2",
"replace": "1.0.1",
"rimraf": "2.6.2",
"rtlcss": "2.4.0",
@ -121,7 +121,7 @@
"@wordpress/viewport": "^2.0.7",
"browser-filesaver": "^1.1.1",
"classnames": "^2.2.5",
"core-js": "2.6.1",
"core-js": "2.6.2",
"d3-array": "^2.0.0",
"d3-axis": "^1.0.12",
"d3-format": "^1.3.2",
@ -130,12 +130,13 @@
"d3-selection": "^1.3.2",
"d3-shape": "^1.2.2",
"d3-time-format": "^2.1.3",
"dompurify": "1.0.8",
"dompurify": "1.0.9",
"gfm-code-blocks": "1.0.0",
"gridicons": "3.1.1",
"history": "4.7.2",
"html-to-react": "1.3.4",
"interpolate-components": "1.1.1",
"locutus": "^2.0.10",
"lodash": "^4.17.11",
"marked": "0.6.0",
"prismjs": "^1.15.0",

View File

@ -1,4 +1,8 @@
# 1.4.0 ( unreleased )
# 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
- Add order number autocompleter to search component
- Add order number, username, and IP address filters to the downloads report.

View File

@ -1,6 +1,6 @@
{
"name": "@woocommerce/components",
"version": "1.3.0",
"version": "1.4.0",
"description": "UI components for WooCommerce.",
"author": "Automattic",
"license": "GPL-2.0-or-later",
@ -22,9 +22,9 @@
"react-native": "src/index",
"dependencies": {
"@babel/runtime-corejs2": "7.2.0",
"@woocommerce/csv-export": "^1.0.1",
"@woocommerce/csv-export": "^1.0.2",
"@woocommerce/currency": "^1.0.0",
"@woocommerce/date": "^1.0.3",
"@woocommerce/date": "^1.0.5",
"@woocommerce/navigation": "^1.1.0",
"@wordpress/components": "7.0.5",
"@wordpress/compose": "3.0.0",
@ -35,7 +35,7 @@
"@wordpress/keycodes": "2.0.5",
"@wordpress/viewport": "^2.0.7",
"classnames": "^2.2.5",
"core-js": "2.6.1",
"core-js": "2.6.2",
"d3-array": "^2.0.0",
"d3-axis": "^1.0.12",
"d3-format": "^1.3.2",

View File

@ -60,8 +60,11 @@ class D3Chart extends Component {
.append( 'g' )
.attr( 'transform', `translate(${ margin.left },${ margin.top })` );
drawAxis( g, adjParams );
type === 'line' && drawLines( g, data, adjParams );
const xOffset = type === 'line' && adjParams.uniqueDates.length <= 1
? adjParams.width / 2
: 0;
drawAxis( g, adjParams, xOffset );
type === 'line' && drawLines( g, data, adjParams, xOffset );
type === 'bar' && drawBars( g, data, adjParams );
}
@ -140,6 +143,7 @@ class D3Chart extends Component {
type,
uniqueDates,
uniqueKeys,
valueType,
xFormat: getFormatter( xFormat, d3TimeFormat ),
x2Format: getFormatter( x2Format, d3TimeFormat ),
xGroupScale: getXGroupScale( orderedKeys, xScale, compact ),
@ -150,7 +154,6 @@ class D3Chart extends Component {
yScale,
yTickOffset: getYTickOffset( adjHeight, yMax ),
yFormat: getFormatter( yFormat ),
valueType,
};
}

View File

@ -186,7 +186,7 @@ export const getYGrids = ( yMax ) => {
return yGrids;
};
export const drawAxis = ( node, params ) => {
export const drawAxis = ( node, params, xOffset ) => {
const xScale = params.type === 'line' ? params.xLineScale : params.xScale;
const removeDuplicateDates = ( d, i, ticks, formatter ) => {
const monthDate = moment( d ).toDate();
@ -205,7 +205,7 @@ export const drawAxis = ( node, params ) => {
.append( 'g' )
.attr( 'class', 'axis' )
.attr( 'aria-hidden', 'true' )
.attr( 'transform', `translate(0, ${ params.height })` )
.attr( 'transform', `translate(${ xOffset }, ${ params.height })` )
.call(
d3AxisBottom( xScale )
.tickValues( ticks )
@ -218,7 +218,7 @@ export const drawAxis = ( node, params ) => {
.append( 'g' )
.attr( 'class', 'axis axis-month' )
.attr( 'aria-hidden', 'true' )
.attr( 'transform', `translate(0, ${ params.height + 20 })` )
.attr( 'transform', `translate(${ xOffset }, ${ params.height + 20 })` )
.call(
d3AxisBottom( xScale )
.tickValues( ticks )
@ -229,7 +229,7 @@ export const drawAxis = ( node, params ) => {
node
.append( 'g' )
.attr( 'class', 'pipes' )
.attr( 'transform', `translate(0, ${ params.height })` )
.attr( 'transform', `translate(${ xOffset }, ${ params.height })` )
.call(
d3AxisBottom( xScale )
.tickValues( ticks )
@ -240,7 +240,7 @@ export const drawAxis = ( node, params ) => {
node
.append( 'g' )
.attr( 'class', 'grid' )
.attr( 'transform', `translate(-${ params.margin.left },0)` )
.attr( 'transform', `translate(-${ params.margin.left }, 0)` )
.call(
d3AxisLeft( params.yScale )
.tickValues( yGrids )

View File

@ -20,7 +20,7 @@ const handleMouseOverLineChart = ( date, parentNode, node, data, params, positio
showTooltip( params, data.find( e => e.date === date ), position );
};
export const drawLines = ( node, data, params ) => {
export const drawLines = ( node, data, params, xOffset ) => {
const series = node
.append( 'g' )
.attr( 'class', 'lines' )
@ -36,18 +36,19 @@ export const drawLines = ( node, data, params ) => {
lineStroke = params.width <= smallBreak ? 1.25 : lineStroke;
const dotRadius = params.width <= wideBreak ? 4 : 6;
series
.append( 'path' )
.attr( 'fill', 'none' )
.attr( 'stroke-width', lineStroke )
.attr( 'stroke-linejoin', 'round' )
.attr( 'stroke-linecap', 'round' )
.attr( 'stroke', d => getColor( d.key, params.orderedKeys, params.colorScheme ) )
.style( 'opacity', d => {
const opacity = d.focus ? 1 : 0.1;
return d.visible ? opacity : 0;
} )
.attr( 'd', d => params.line( d.values ) );
params.uniqueDates.length > 1 &&
series
.append( 'path' )
.attr( 'fill', 'none' )
.attr( 'stroke-width', lineStroke )
.attr( 'stroke-linejoin', 'round' )
.attr( 'stroke-linecap', 'round' )
.attr( 'stroke', d => getColor( d.key, params.orderedKeys, params.colorScheme ) )
.style( 'opacity', d => {
const opacity = d.focus ? 1 : 0.1;
return d.visible ? opacity : 0;
} )
.attr( 'd', d => params.line( d.values ) );
const minDataPointSpacing = 36;
@ -65,7 +66,7 @@ export const drawLines = ( node, data, params ) => {
const opacity = d.focus ? 1 : 0.1;
return d.visible ? opacity : 0;
} )
.attr( 'cx', d => params.xLineScale( moment( d.date ).toDate() ) )
.attr( 'cx', d => params.xLineScale( moment( d.date ).toDate() ) + xOffset )
.attr( 'cy', d => params.yScale( d.value ) )
.attr( 'tabindex', '0' )
.attr( 'aria-label', d => {
@ -100,9 +101,9 @@ export const drawLines = ( node, data, params ) => {
focusGrid
.append( 'line' )
.attr( 'x1', d => params.xLineScale( moment( d.date ).toDate() ) )
.attr( 'x1', d => params.xLineScale( moment( d.date ).toDate() ) + xOffset )
.attr( 'y1', 0 )
.attr( 'x2', d => params.xLineScale( moment( d.date ).toDate() ) )
.attr( 'x2', d => params.xLineScale( moment( d.date ).toDate() ) + xOffset )
.attr( 'y2', params.height );
focusGrid
@ -114,7 +115,7 @@ export const drawLines = ( node, data, params ) => {
.attr( 'fill', d => getColor( d.key, params.orderedKeys, params.colorScheme ) )
.attr( 'stroke', '#fff' )
.attr( 'stroke-width', lineStroke + 2 )
.attr( 'cx', d => params.xLineScale( moment( d.date ).toDate() ) )
.attr( 'cx', d => params.xLineScale( moment( d.date ).toDate() ) + xOffset )
.attr( 'cy', d => params.yScale( d.value ) );
focus
@ -126,7 +127,8 @@ export const drawLines = ( node, data, params ) => {
.attr( 'height', params.height )
.attr( 'opacity', 0 )
.on( 'mouseover', ( d, i, nodes ) => {
const elementWidthRatio = i === 0 || i === params.dateSpaces.length - 1 ? 0 : 0.5;
const isTooltipLeftAligned = ( i === 0 || i === params.dateSpaces.length - 1 ) && params.uniqueDates.length > 1;
const elementWidthRatio = isTooltipLeftAligned ? 0 : 0.5;
const position = calculateTooltipPosition(
d3Event.target,
node.node(),

View File

@ -11,17 +11,6 @@ import {
} from 'd3-scale';
import moment from 'moment';
/**
* 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`
* @returns {number} the maximum value in the timeseries multiplied by 4/3
*/
export const getYMax = lineData => {
const yMax = 4 / 3 * d3Max( lineData, d => d3Max( d.values.map( date => date.value ) ) );
const pow3Y = Math.pow( 10, ( ( Math.log( yMax ) * Math.LOG10E + 1 ) | 0 ) - 2 ) * 3;
return Math.ceil( Math.ceil( yMax / pow3Y ) * pow3Y );
};
/**
* Describes getXScale
* @param {array} uniqueDates - from `getUniqueDates`
@ -33,7 +22,7 @@ export const getYMax = lineData => {
export const getXScale = ( uniqueDates, width, compact = false ) =>
d3ScaleBand()
.domain( uniqueDates )
.rangeRound( [ 0, width ] )
.range( [ 0, width ] )
.paddingInner( compact ? 0 : 0.1 );
/**
@ -64,6 +53,17 @@ export const getXLineScale = ( uniqueDates, width ) =>
] )
.rangeRound( [ 0, width ] );
/**
* 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`
* @returns {number} the maximum value in the timeseries multiplied by 4/3
*/
export const getYMax = lineData => {
const yMax = 4 / 3 * d3Max( lineData, d => d3Max( d.values.map( date => date.value ) ) );
const pow3Y = Math.pow( 10, ( ( Math.log( yMax ) * Math.LOG10E + 1 ) | 0 ) - 2 ) * 3;
return Math.ceil( Math.ceil( yMax / pow3Y ) * pow3Y );
};
/**
* Describes getYScale
* @param {number} height - calculated height of the charting space

View File

@ -3,13 +3,12 @@
* External dependencies
*/
import { utcParse as d3UTCParse } from 'd3-time-format';
import { scaleBand, scaleLinear, scaleTime } from 'd3-scale';
/**
* Internal dependencies
*/
import dummyOrders from './fixtures/dummy-orders';
import orderedDates from './fixtures/dummy-ordered-dates';
import orderedKeys from './fixtures/dummy-ordered-keys';
import {
getOrderedKeys,
getLineData,
@ -18,61 +17,104 @@ import {
} from '../index';
import { getXGroupScale, getXScale, getXLineScale, getYMax, getYScale, getYTickOffset } from '../scales';
const parseDate = d3UTCParse( '%Y-%m-%dT%H:%M:%S' );
jest.mock( 'd3-scale', () => ( {
...require.requireActual( 'd3-scale' ),
scaleBand: jest.fn().mockReturnValue( {
bandwidth: jest.fn().mockReturnThis(),
domain: jest.fn().mockReturnThis(),
padding: jest.fn().mockReturnThis(),
paddingInner: jest.fn().mockReturnThis(),
range: jest.fn().mockReturnThis(),
rangeRound: jest.fn().mockReturnThis(),
} ),
scaleLinear: jest.fn().mockReturnValue( {
domain: jest.fn().mockReturnThis(),
rangeRound: jest.fn().mockReturnThis(),
} ),
scaleTime: jest.fn().mockReturnValue( {
domain: jest.fn().mockReturnThis(),
rangeRound: jest.fn().mockReturnThis(),
} ),
} ) );
const testUniqueKeys = getUniqueKeys( dummyOrders );
const testOrderedKeys = getOrderedKeys( dummyOrders, testUniqueKeys );
const testLineData = getLineData( dummyOrders, testOrderedKeys );
const testUniqueDates = getUniqueDates( testLineData, parseDate );
const testXScale = getXScale( testUniqueDates, 100 );
const testXLineScale = getXLineScale( testUniqueDates, 100 );
const testYMax = getYMax( testLineData );
const testYScale = getYScale( 100, testYMax );
describe( 'getXScale', () => {
it( 'properly scale inputs to the provided domain and range', () => {
expect( testXScale( orderedDates[ 0 ] ) ).toEqual( 3 );
expect( testXScale( orderedDates[ 2 ] ) ).toEqual( 35 );
expect( testXScale( orderedDates[ orderedDates.length - 1 ] ) ).toEqual( 83 );
describe( 'X scales', () => {
const parseDate = d3UTCParse( '%Y-%m-%dT%H:%M:%S' );
const testUniqueDates = getUniqueDates( testLineData, parseDate );
describe( 'getXScale', () => {
it( 'creates band scale with correct parameters', () => {
getXScale( testUniqueDates, 100 );
expect( scaleBand().domain ).toHaveBeenLastCalledWith( testUniqueDates );
expect( scaleBand().range ).toHaveBeenLastCalledWith( [ 0, 100 ] );
expect( scaleBand().paddingInner ).toHaveBeenLastCalledWith( 0.1 );
} );
it( 'creates band scale with correct paddingInner parameter when it\'s in compact mode', () => {
getXScale( testUniqueDates, 100, true );
expect( scaleBand().paddingInner ).toHaveBeenLastCalledWith( 0 );
} );
} );
it( 'properly scale inputs and test the bandwidth', () => {
expect( testXScale.bandwidth() ).toEqual( 14 );
describe( 'getXGroupScale', () => {
const testXScale = getXScale( testUniqueDates, 100 );
it( 'creates band scale with correct parameters', () => {
getXGroupScale( testOrderedKeys, testXScale );
const filteredOrderedKeys = [ 'Cap', 'T-Shirt', 'Sunglasses', 'Polo', 'Hoodie' ];
expect( scaleBand().domain ).toHaveBeenLastCalledWith( filteredOrderedKeys );
expect( scaleBand().range ).toHaveBeenLastCalledWith( [ 0, 100 ] );
expect( scaleBand().padding ).toHaveBeenLastCalledWith( 0.07 );
} );
it( 'creates band scale with correct padding parameter when it\'s in compact mode', () => {
getXGroupScale( testOrderedKeys, testXScale, true );
expect( scaleBand().padding ).toHaveBeenLastCalledWith( 0 );
} );
} );
describe( 'getXLineScale', () => {
it( 'creates time scale with correct parameters', () => {
getXLineScale( testUniqueDates, 100 );
expect( scaleTime().domain ).toHaveBeenLastCalledWith( [
new Date( '2018-05-30T00:00:00' ),
new Date( '2018-06-04T00:00:00' ),
] );
expect( scaleTime().rangeRound ).toHaveBeenLastCalledWith( [ 0, 100 ] );
} );
} );
} );
describe( 'getXGroupScale', () => {
it( 'properly scale inputs based on the getXScale', () => {
const testXGroupScale = getXGroupScale( testOrderedKeys, testXScale );
expect( testXGroupScale( orderedKeys[ 0 ].key ) ).toEqual( 2 );
expect( testXGroupScale( orderedKeys[ 2 ].key ) ).toEqual( 6 );
expect( testXGroupScale( orderedKeys[ orderedKeys.length - 1 ].key ) ).toEqual( 10 );
describe( 'Y scales', () => {
describe( 'getYMax', () => {
it( 'calculate the correct maximum y value', () => {
expect( getYMax( testLineData ) ).toEqual( 15000000 );
} );
} );
} );
describe( 'getXLineScale', () => {
it( 'properly scale inputs for the line', () => {
expect( testXLineScale( new Date( orderedDates[ 0 ] ) ) ).toEqual( 0 );
expect( testXLineScale( new Date( orderedDates[ 2 ] ) ) ).toEqual( 40 );
expect( testXLineScale( new Date( orderedDates[ orderedDates.length - 1 ] ) ) ).toEqual( 100 );
} );
} );
describe( 'getYScale', () => {
it( 'creates linear scale with correct parameters', () => {
getYScale( 100, 15000000 );
describe( 'getYMax', () => {
it( 'calculate the correct maximum y value', () => {
expect( testYMax ).toEqual( 15000000 );
expect( scaleLinear().domain ).toHaveBeenLastCalledWith( [ 0, 15000000 ] );
expect( scaleLinear().rangeRound ).toHaveBeenLastCalledWith( [ 100, 0 ] );
} );
} );
} );
describe( 'getYScale', () => {
it( 'properly scale the y values given the height and maximum y value', () => {
expect( testYScale( 0 ) ).toEqual( 100 );
expect( testYScale( testYMax ) ).toEqual( 0 );
} );
} );
describe( 'getYTickOffset', () => {
it( 'creates linear scale with correct parameters', () => {
getYTickOffset( 100, 15000000 );
describe( 'getYTickOffset', () => {
it( 'properly scale the y values for the y-axis ticks given the height and maximum y value', () => {
const testYTickOffset1 = getYTickOffset( 100, testYMax );
expect( testYTickOffset1( 0 ) ).toEqual( 112 );
expect( testYTickOffset1( testYMax ) ).toEqual( 12 );
expect( scaleLinear().domain ).toHaveBeenLastCalledWith( [ 0, 15000000 ] );
expect( scaleLinear().rangeRound ).toHaveBeenLastCalledWith( [ 112, 12 ] );
} );
} );
} );

View File

@ -5,7 +5,6 @@
import { __, sprintf } from '@wordpress/i18n';
import classNames from 'classnames';
import { Component, createRef, Fragment } from '@wordpress/element';
import { decodeEntities } from '@wordpress/html-entities';
import { formatDefaultLocale as d3FormatDefaultLocale } from 'd3-format';
import { get, isEqual, partial } from 'lodash';
import Gridicon from 'gridicons';
@ -26,11 +25,28 @@ import ChartPlaceholder from './placeholder';
import { H, Section } from '../section';
import { D3Chart, D3Legend } from './d3chart';
function getD3CurrencyFormat( symbol, position ) {
switch ( position ) {
case 'left_space':
return [ symbol + ' ', '' ];
case 'right':
return [ '', symbol ];
case 'right_space':
return [ '', ' ' + symbol ];
case 'left':
default:
return [ symbol, '' ];
}
}
const currencySymbol = get( wcSettings, [ 'currency', 'symbol' ], '' );
const symbolPosition = get( wcSettings, [ 'currency', 'position' ], 'left' );
d3FormatDefaultLocale( {
decimal: '.',
thousands: ',',
decimal: get( wcSettings, [ 'currency', 'decimal_separator' ], '.' ),
thousands: get( wcSettings, [ 'currency', 'thousand_separator' ], ',' ),
grouping: [ 3 ],
currency: [ decodeEntities( get( wcSettings, 'currency.symbol', '' ) ), '' ],
currency: getD3CurrencyFormat( currencySymbol, symbolPosition ),
} );
function getOrderedKeys( props, previousOrderedKeys = [] ) {
@ -272,13 +288,13 @@ class Chart extends Component {
switch ( valueType ) {
case 'average':
yFormat = '.0f';
yFormat = ',.0f';
break;
case 'currency':
yFormat = '$.3~s';
break;
case 'number':
yFormat = '.0f';
yFormat = ',.0f';
break;
}
return (

View File

@ -27,7 +27,7 @@ class DateFilter extends Component {
constructor( { filter } ) {
super( ...arguments );
const [ isoAfter, isoBefore ] = ( filter.value || '' ).split( ',' );
const [ isoAfter, isoBefore ] = Array.isArray( filter.value ) ? filter.value : [ null, filter.value ];
const after = isoAfter ? toMoment( isoDateFormat, isoAfter ) : null;
const before = isoBefore ? toMoment( isoDateFormat, isoBefore ) : null;
@ -42,6 +42,7 @@ class DateFilter extends Component {
this.onSingleDateChange = this.onSingleDateChange.bind( this );
this.onRangeDateChange = this.onRangeDateChange.bind( this );
this.onRuleChange = this.onRuleChange.bind( this );
}
getBetweenString() {
@ -58,7 +59,7 @@ class DateFilter extends Component {
const { before, after } = this.state;
// Return nothing if we're missing input(s)
if ( ! before || 'between' === rule.value && ! after ) {
if ( ! before || ( 'between' === rule.value && ! after ) ) {
return '';
}
@ -75,13 +76,15 @@ class DateFilter extends Component {
} );
}
return textContent( interpolateComponents( {
mixedString: config.labels.title,
components: {
filter: <Fragment>{ filterStr }</Fragment>,
rule: <Fragment>{ rule.label }</Fragment>,
},
} ) );
return textContent(
interpolateComponents( {
mixedString: config.labels.title,
components: {
filter: <Fragment>{ filterStr }</Fragment>,
rule: <Fragment>{ rule.label }</Fragment>,
},
} )
);
}
onSingleDateChange( { date, text, error } ) {
@ -118,7 +121,7 @@ class DateFilter extends Component {
}
if ( nextAfter && nextBefore ) {
onFilterChange( filter.key, 'value', [ nextAfter, nextBefore ].join( ',' ) );
onFilterChange( filter.key, 'value', [ nextAfter, nextBefore ] );
}
}
}
@ -168,9 +171,23 @@ class DateFilter extends Component {
);
}
onRuleChange( value ) {
const { onFilterChange, filter, updateFilter } = this.props;
const { before } = this.state;
if ( 'between' === filter.rule && 'between' !== value ) {
updateFilter( {
key: filter.key,
rule: value,
value: before ? before.format( isoDateFormat ) : undefined,
} );
} else {
onFilterChange( filter.key, 'rule', value );
}
}
render() {
const { config, filter, onFilterChange, isEnglish } = this.props;
const { key, rule } = filter;
const { config, filter, isEnglish } = this.props;
const { rule } = filter;
const { labels, rules } = config;
const screenReaderText = this.getScreenReaderText( filter, config );
const children = interpolateComponents( {
@ -181,7 +198,7 @@ class DateFilter extends Component {
className="woocommerce-filters-advanced__rule"
options={ rules }
value={ rule }
onChange={ partial( onFilterChange, key, 'rule' ) }
onChange={ this.onRuleChange }
aria-label={ labels.rule }
/>
),
@ -199,9 +216,7 @@ class DateFilter extends Component {
/*eslint-disable jsx-a11y/no-noninteractive-tabindex*/
return (
<fieldset tabIndex="0">
<legend className="screen-reader-text">
{ labels.add || '' }
</legend>
<legend className="screen-reader-text">{ labels.add || '' }</legend>
<div
className={ classnames( 'woocommerce-filters-advanced__fieldset', {
'is-english': isEnglish,
@ -209,11 +224,7 @@ class DateFilter extends Component {
>
{ children }
</div>
{ screenReaderText && (
<span className="screen-reader-text">
{ screenReaderText }
</span>
) }
{ screenReaderText && <span className="screen-reader-text">{ screenReaderText }</span> }
</fieldset>
);
/*eslint-enable jsx-a11y/no-noninteractive-tabindex*/

View File

@ -55,6 +55,7 @@ class AdvancedFilters extends Component {
this.removeFilter = this.removeFilter.bind( this );
this.clearFilters = this.clearFilters.bind( this );
this.getUpdateHref = this.getUpdateHref.bind( this );
this.updateFilter = this.updateFilter.bind( this );
}
componentDidUpdate( prevProps ) {
@ -85,6 +86,17 @@ class AdvancedFilters extends Component {
this.setState( { activeFilters } );
}
updateFilter( filter ) {
const activeFilters = this.state.activeFilters.map( activeFilter => {
if ( filter.key === activeFilter.key ) {
return filter;
}
return activeFilter;
} );
this.setState( { activeFilters } );
}
removeFilter( key ) {
const activeFilters = [ ...this.state.activeFilters ];
const index = findIndex( activeFilters, filter => filter.key === key );
@ -218,6 +230,7 @@ class AdvancedFilters extends Component {
onFilterChange={ this.onFilterChange }
isEnglish={ isEnglish }
query={ query }
updateFilter={ this.updateFilter }
/>
) }
<IconButton

View File

@ -20,6 +20,15 @@
.components-base-control__field {
margin-bottom: 0;
}
@include breakpoint( '<782px' ) {
margin: $gap 0;
border: 1px solid $core-grey-light-700;
}
@include breakpoint( '<400px' ) {
margin: $gap-small 0;
}
}
.woocommerce-filters-advanced__title-select {
@ -52,11 +61,23 @@
width: 40px;
height: 38px;
align-self: center;
@include breakpoint( '<400px' ) {
position: absolute;
top: 0;
right: $gap-smallest;
}
}
.components-form-token-field {
border-radius: 4px;
}
@include breakpoint( '<400px' ) {
display: block;
position: relative;
padding: $gap-smaller $gap-smaller 0 0;
}
}
.woocommerce-filters-advanced__add-filter {
@ -85,11 +106,14 @@
padding: 0 $gap-smallest;
@include breakpoint( '<782px' ) {
display: block;
margin: 0;
width: 100%;
padding: $gap-smallest 0;
}
@include breakpoint( '<400px' ) {
//display: block;
//margin: 0;
}
}
display: flex;
@ -186,10 +210,6 @@
}
.separator {
padding: 0 8px;
@include breakpoint( '<782px' ) {
padding: 0;
}
text-align: center;
}
}

View File

@ -134,7 +134,9 @@ class FilterPicker extends Component {
renderButton( filter, onClose ) {
if ( filter.component ) {
const { type, labels } = filter.settings;
const { selectedTag } = this.state;
const persistedFilter = this.getFilter();
const selectedTag = persistedFilter.value === filter.value ? this.state.selectedTag : null;
return (
<Search
className="woocommerce-filters-filter__search"

View File

@ -4,6 +4,11 @@
.components-base-control__field {
margin-bottom: 0;
}
@include breakpoint( '<400px' ) {
margin-left: -8px;
margin-right: -8px;
}
}
.woocommerce-filters__basic-filters {
@ -13,10 +18,14 @@
@include breakpoint( '<1280px' ) {
flex-direction: column;
}
@include breakpoint( '<782px' ) {
margin-bottom: $gap;
}
}
.woocommerce-filters-filter {
width: 33.3%;
width: 25%;
padding: 0 $gap-small;
min-height: 82px;
display: flex;
@ -31,6 +40,10 @@
padding-right: 0;
}
@include breakpoint( '<1440px' ) {
width: 33.3%;
}
@include breakpoint( '<1280px' ) {
width: 50%;
padding: 0;

View File

@ -146,6 +146,9 @@ export class Autocomplete extends Component {
const promise = ( this.activePromise = Promise.resolve(
typeof options === 'function' ? options( query ) : options
).then( optionsData => {
if ( ! optionsData ) {
return;
}
const { selected } = this.props;
if ( promise !== this.activePromise ) {
// Another promise has become active since this one was asked to resolve, so do nothing,

View File

@ -24,7 +24,7 @@ export default {
return wcSettings.dataEndpoints.countries || [];
},
getOptionKeywords( country ) {
return [ decodeEntities( country.name ) ];
return [ country.code, decodeEntities( country.name ) ];
},
getOptionLabel( country, query ) {
const name = decodeEntities( country.name );

View File

@ -32,7 +32,7 @@ export default {
};
payload = stringifyQuery( query );
}
return apiFetch( { path: `/wc/v3/coupons${ payload }` } );
return apiFetch( { path: `/wc/v4/coupons${ payload }` } );
},
isDebounced: true,
getOptionKeywords( coupon ) {

View File

@ -34,7 +34,7 @@ export default {
};
payload = stringifyQuery( query );
}
return apiFetch( { path: `/wc/v3/customers${ payload }` } );
return apiFetch( { path: `/wc/v4/customers${ payload }` } );
},
isDebounced: true,
getOptionKeywords( customer ) {

Some files were not shown because too many files have changed in this diff Show More