Merge branch 'master' into fix/1307
This commit is contained in:
commit
d26e82b526
|
@ -6,12 +6,10 @@ import { Component } from '@wordpress/element';
|
|||
import { compose } from '@wordpress/compose';
|
||||
import { format as formatDate } from '@wordpress/date';
|
||||
import PropTypes from 'prop-types';
|
||||
import { find, get } from 'lodash';
|
||||
|
||||
/**
|
||||
* WooCommerce dependencies
|
||||
*/
|
||||
import { flattenFilters } from '@woocommerce/navigation';
|
||||
import {
|
||||
getAllowedIntervalsForQuery,
|
||||
getCurrentDates,
|
||||
|
@ -28,66 +26,37 @@ import { Chart } from '@woocommerce/components';
|
|||
import { getReportChartData, getTooltipValueFormat } from 'wc-api/reports/utils';
|
||||
import ReportError from 'analytics/components/report-error';
|
||||
import withSelect from 'wc-api/with-select';
|
||||
|
||||
export const DEFAULT_FILTER = 'all';
|
||||
import { getChartMode } from './utils';
|
||||
|
||||
/**
|
||||
* Component that renders the chart in reports.
|
||||
*/
|
||||
export class ReportChart extends Component {
|
||||
getSelectedFilter( filters, query ) {
|
||||
if ( filters.length === 0 ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const filterConfig = filters.pop();
|
||||
|
||||
if ( filterConfig.showFilters( query ) ) {
|
||||
const allFilters = flattenFilters( filterConfig.filters );
|
||||
const value = query[ filterConfig.param ] || DEFAULT_FILTER;
|
||||
const selectedFilter = find( allFilters, { value } );
|
||||
const selectedFilterParam = get( selectedFilter, [ 'settings', 'param' ] );
|
||||
|
||||
if ( ! selectedFilterParam || Object.keys( query ).includes( selectedFilterParam ) ) {
|
||||
return selectedFilter;
|
||||
}
|
||||
}
|
||||
|
||||
return this.getSelectedFilter( filters, query );
|
||||
getItemChartData() {
|
||||
const { primaryData, selectedChart } = this.props;
|
||||
const chartData = primaryData.data.intervals.map( function( interval ) {
|
||||
const intervalData = {};
|
||||
interval.subtotals.segments.forEach( function( segment ) {
|
||||
if ( segment.segment_label ) {
|
||||
const label = intervalData[ segment.segment_label ]
|
||||
? segment.segment_label + ' (#' + segment.segment_id + ')'
|
||||
: segment.segment_label;
|
||||
intervalData[ label ] = {
|
||||
value: segment.subtotals[ selectedChart.key ] || 0,
|
||||
};
|
||||
}
|
||||
} );
|
||||
return {
|
||||
date: formatDate( 'Y-m-d\\TH:i:s', interval.date_start ),
|
||||
...intervalData,
|
||||
};
|
||||
} );
|
||||
return chartData;
|
||||
}
|
||||
|
||||
getChartMode() {
|
||||
const { filters, query } = this.props;
|
||||
if ( ! filters ) {
|
||||
return;
|
||||
}
|
||||
const clonedFilters = filters.slice( 0 );
|
||||
const selectedFilter = this.getSelectedFilter( clonedFilters, query );
|
||||
|
||||
return get( selectedFilter, [ 'chartMode' ] );
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
interactiveLegend,
|
||||
itemsLabel,
|
||||
legendPosition,
|
||||
mode,
|
||||
path,
|
||||
primaryData,
|
||||
query,
|
||||
secondaryData,
|
||||
selectedChart,
|
||||
showHeaderControls,
|
||||
} = this.props;
|
||||
|
||||
if ( primaryData.isError || secondaryData.isError ) {
|
||||
return <ReportError isError />;
|
||||
}
|
||||
|
||||
getTimeChartData() {
|
||||
const { query, primaryData, secondaryData, selectedChart } = this.props;
|
||||
const currentInterval = getIntervalForQuery( query );
|
||||
const allowedIntervals = getAllowedIntervalsForQuery( query );
|
||||
const formats = getDateFormatsForInterval( currentInterval, primaryData.data.intervals.length );
|
||||
const { primary, secondary } = getCurrentDates( query );
|
||||
const primaryKey = `${ primary.label } (${ primary.range })`;
|
||||
const secondaryKey = `${ secondary.label } (${ secondary.range })`;
|
||||
|
@ -115,6 +84,23 @@ export class ReportChart extends Component {
|
|||
};
|
||||
} );
|
||||
|
||||
return chartData;
|
||||
}
|
||||
|
||||
renderChart( mode, isRequesting, chartData ) {
|
||||
const {
|
||||
interactiveLegend,
|
||||
itemsLabel,
|
||||
legendPosition,
|
||||
path,
|
||||
query,
|
||||
selectedChart,
|
||||
showHeaderControls,
|
||||
primaryData,
|
||||
} = this.props;
|
||||
const currentInterval = getIntervalForQuery( query );
|
||||
const allowedIntervals = getAllowedIntervalsForQuery( query );
|
||||
const formats = getDateFormatsForInterval( currentInterval, primaryData.data.intervals.length );
|
||||
return (
|
||||
<Chart
|
||||
allowedIntervals={ allowedIntervals }
|
||||
|
@ -122,16 +108,16 @@ export class ReportChart extends Component {
|
|||
dateParser={ '%Y-%m-%dT%H:%M:%S' }
|
||||
interactiveLegend={ interactiveLegend }
|
||||
interval={ currentInterval }
|
||||
isRequesting={ primaryData.isRequesting || secondaryData.isRequesting }
|
||||
isRequesting={ isRequesting }
|
||||
itemsLabel={ itemsLabel }
|
||||
legendPosition={ legendPosition }
|
||||
mode={ mode || this.getChartMode() }
|
||||
mode={ mode }
|
||||
path={ path }
|
||||
query={ query }
|
||||
showHeaderControls={ showHeaderControls }
|
||||
title={ selectedChart.label }
|
||||
tooltipLabelFormat={ formats.tooltipLabelFormat }
|
||||
tooltipTitle={ selectedChart.label }
|
||||
tooltipTitle={ ( 'time-comparison' === mode && selectedChart.label ) || null }
|
||||
tooltipValueFormat={ getTooltipValueFormat( selectedChart.type ) }
|
||||
type={ getChartTypeForQuery( query ) }
|
||||
valueType={ selectedChart.type }
|
||||
|
@ -140,6 +126,40 @@ export class ReportChart extends Component {
|
|||
/>
|
||||
);
|
||||
}
|
||||
|
||||
renderItemComparison() {
|
||||
const { primaryData } = this.props;
|
||||
|
||||
if ( primaryData.isError ) {
|
||||
return <ReportError isError />;
|
||||
}
|
||||
|
||||
const isRequesting = primaryData.isRequesting;
|
||||
const chartData = this.getItemChartData();
|
||||
|
||||
return this.renderChart( 'item-comparison', isRequesting, chartData );
|
||||
}
|
||||
|
||||
renderTimeComparison() {
|
||||
const { primaryData, secondaryData } = this.props;
|
||||
|
||||
if ( ! primaryData || primaryData.isError || secondaryData.isError ) {
|
||||
return <ReportError isError />;
|
||||
}
|
||||
|
||||
const isRequesting = primaryData.isRequesting || secondaryData.isRequesting;
|
||||
const chartData = this.getTimeChartData();
|
||||
|
||||
return this.renderChart( 'time-comparison', isRequesting, chartData );
|
||||
}
|
||||
|
||||
render() {
|
||||
const { mode } = this.props;
|
||||
if ( 'item-comparison' === mode ) {
|
||||
return this.renderItemComparison();
|
||||
}
|
||||
return this.renderTimeComparison();
|
||||
}
|
||||
}
|
||||
|
||||
ReportChart.propTypes = {
|
||||
|
@ -171,7 +191,7 @@ ReportChart.propTypes = {
|
|||
/**
|
||||
* Secondary data to display in the chart.
|
||||
*/
|
||||
secondaryData: PropTypes.object.isRequired,
|
||||
secondaryData: PropTypes.object,
|
||||
/**
|
||||
* Properties of the selected chart.
|
||||
*/
|
||||
|
@ -180,10 +200,21 @@ ReportChart.propTypes = {
|
|||
|
||||
export default compose(
|
||||
withSelect( ( select, props ) => {
|
||||
const { query, endpoint } = props;
|
||||
const { query, endpoint, filters } = props;
|
||||
const chartMode = props.mode || getChartMode( filters, query ) || 'time-comparison';
|
||||
|
||||
if ( 'item-comparison' === chartMode ) {
|
||||
const primaryData = getReportChartData( endpoint, 'primary', query, select );
|
||||
return {
|
||||
mode: chartMode,
|
||||
primaryData,
|
||||
};
|
||||
}
|
||||
|
||||
const primaryData = getReportChartData( endpoint, 'primary', query, select );
|
||||
const secondaryData = getReportChartData( endpoint, 'secondary', query, select );
|
||||
return {
|
||||
mode: chartMode,
|
||||
primaryData,
|
||||
secondaryData,
|
||||
};
|
||||
|
|
|
@ -8,6 +8,7 @@ import { shallow } from 'enzyme';
|
|||
* Internal dependencies
|
||||
*/
|
||||
import { ReportChart } from '../';
|
||||
import { getChartMode } from '../utils';
|
||||
|
||||
jest.mock( '@woocommerce/components', () => ( {
|
||||
...require.requireActual( '@woocommerce/components' ),
|
||||
|
@ -30,7 +31,7 @@ const selectedChart = {
|
|||
};
|
||||
|
||||
describe( 'ReportChart', () => {
|
||||
test( 'should not set the mode prop by default', () => {
|
||||
test( 'should set the time-comparison mode prop by default', () => {
|
||||
const reportChart = shallow(
|
||||
<ReportChart
|
||||
path={ path }
|
||||
|
@ -42,7 +43,7 @@ describe( 'ReportChart', () => {
|
|||
);
|
||||
const chart = reportChart.find( 'Chart' );
|
||||
|
||||
expect( chart.props().mode ).toBeUndefined();
|
||||
expect( chart.props().mode ).toEqual( 'time-comparison' );
|
||||
} );
|
||||
|
||||
test( 'should set the mode prop depending on the active filter', () => {
|
||||
|
@ -62,19 +63,7 @@ describe( 'ReportChart', () => {
|
|||
},
|
||||
];
|
||||
const query = { filter: 'lorem-ipsum', filter2: 'ipsum-lorem' };
|
||||
const reportChart = shallow(
|
||||
<ReportChart
|
||||
filters={ filters }
|
||||
path={ path }
|
||||
primaryData={ data }
|
||||
query={ query }
|
||||
secondaryData={ data }
|
||||
selectedChart={ selectedChart }
|
||||
/>
|
||||
);
|
||||
|
||||
const chart = reportChart.find( 'Chart' );
|
||||
|
||||
expect( chart.props().mode ).toEqual( 'item-comparison' );
|
||||
const mode = getChartMode( filters, query );
|
||||
expect( mode ).toEqual( 'item-comparison' );
|
||||
} );
|
||||
} );
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
/** @format */
|
||||
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { find, get } from 'lodash';
|
||||
|
||||
/**
|
||||
* WooCommerce dependencies
|
||||
*/
|
||||
import { flattenFilters } from '@woocommerce/navigation';
|
||||
|
||||
export const DEFAULT_FILTER = 'all';
|
||||
|
||||
export function getSelectedFilter( filters, query, selectedFilterArgs = {} ) {
|
||||
if ( filters.length === 0 ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const filterConfig = filters.pop();
|
||||
|
||||
if ( filterConfig.showFilters( query, selectedFilterArgs ) ) {
|
||||
const allFilters = flattenFilters( filterConfig.filters );
|
||||
const value = query[ filterConfig.param ] || DEFAULT_FILTER;
|
||||
const selectedFilter = find( allFilters, { value } );
|
||||
const selectedFilterParam = get( selectedFilter, [ 'settings', 'param' ] );
|
||||
|
||||
if ( ! selectedFilterParam || Object.keys( query ).includes( selectedFilterParam ) ) {
|
||||
return selectedFilter;
|
||||
}
|
||||
}
|
||||
|
||||
return getSelectedFilter( filters, query, selectedFilterArgs );
|
||||
}
|
||||
|
||||
export function getChartMode( filters, query, selectedFilterArgs ) {
|
||||
if ( ! filters ) {
|
||||
return;
|
||||
}
|
||||
const clonedFilters = filters.slice( 0 );
|
||||
const selectedFilter = getSelectedFilter( clonedFilters, query, selectedFilterArgs );
|
||||
|
||||
return get( selectedFilter, [ 'chartMode' ] );
|
||||
}
|
|
@ -29,7 +29,7 @@ export const charts = [
|
|||
|
||||
const filterConfig = {
|
||||
label: __( 'Show', 'wc-admin' ),
|
||||
staticParams: [ 'chart' ],
|
||||
staticParams: [],
|
||||
param: 'filter',
|
||||
showFilters: () => true,
|
||||
filters: [
|
||||
|
@ -114,19 +114,20 @@ const filterConfig = {
|
|||
label: __( 'Top Products by Items Sold', 'wc-admin' ),
|
||||
value: 'top_items',
|
||||
chartMode: 'item-comparison',
|
||||
query: { orderby: 'items_sold', order: 'desc' },
|
||||
query: { orderby: 'items_sold', order: 'desc', chart: 'items_sold' },
|
||||
},
|
||||
{
|
||||
label: __( 'Top Products by Net Revenue', 'wc-admin' ),
|
||||
value: 'top_sales',
|
||||
chartMode: 'item-comparison',
|
||||
query: { orderby: 'net_revenue', order: 'desc' },
|
||||
query: { orderby: 'net_revenue', order: 'desc', chart: 'net_revenue' },
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const variationsConfig = {
|
||||
showFilters: query => 'single_product' === query.filter && !! query.products,
|
||||
showFilters: query =>
|
||||
'single_product' === query.filter && !! query.products && query[ 'is-variable' ],
|
||||
staticParams: [ 'filter', 'products' ],
|
||||
param: 'filter-variations',
|
||||
filters: [
|
||||
|
@ -151,13 +152,13 @@ const variationsConfig = {
|
|||
label: __( 'Top Variations by Items Sold', 'wc-admin' ),
|
||||
chartMode: 'item-comparison',
|
||||
value: 'top_items',
|
||||
query: { orderby: 'items_sold', order: 'desc' },
|
||||
query: { orderby: 'items_sold', order: 'desc', chart: 'item_sold' },
|
||||
},
|
||||
{
|
||||
label: __( 'Top Variations by Net Revenue', 'wc-admin' ),
|
||||
chartMode: 'item-comparison',
|
||||
value: 'top_sales',
|
||||
query: { orderby: 'net_revenue', order: 'desc' },
|
||||
query: { orderby: 'net_revenue', order: 'desc', chart: 'net_revenue' },
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
|
@ -4,12 +4,13 @@
|
|||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { Component, Fragment } from '@wordpress/element';
|
||||
import { compose } from '@wordpress/compose';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
/**
|
||||
* WooCommerce dependencies
|
||||
*/
|
||||
import { ReportFilters } from '@woocommerce/components';
|
||||
import { ReportFilters, SummaryListPlaceholder, ChartPlaceholder } from '@woocommerce/components';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
|
@ -18,37 +19,99 @@ import { charts, filters } from './config';
|
|||
import getSelectedChart from 'lib/get-selected-chart';
|
||||
import ProductsReportTable from './table';
|
||||
import ReportChart from 'analytics/components/report-chart';
|
||||
import ReportError from 'analytics/components/report-error';
|
||||
import ReportSummary from 'analytics/components/report-summary';
|
||||
import VariationsReportTable from './table-variations';
|
||||
import withSelect from 'wc-api/with-select';
|
||||
|
||||
class ProductsReport extends Component {
|
||||
getChartMeta() {
|
||||
const { query, isSingleProductView, isSingleProductVariable } = this.props;
|
||||
|
||||
const isProductDetailsView =
|
||||
'top_items' === query.filter ||
|
||||
'top_sales' === query.filter ||
|
||||
'single_product' === query.filter;
|
||||
|
||||
const mode =
|
||||
isProductDetailsView || ( isSingleProductView && isSingleProductVariable )
|
||||
? 'item-comparison'
|
||||
: 'time-comparison';
|
||||
const compareObject =
|
||||
isSingleProductView && isSingleProductVariable ? 'variations' : 'products';
|
||||
const label =
|
||||
isSingleProductView && isSingleProductVariable
|
||||
? __( '%s variations', 'wc-admin' )
|
||||
: __( '%s products', 'wc-admin' );
|
||||
|
||||
return {
|
||||
compareObject,
|
||||
itemsLabel: label,
|
||||
mode,
|
||||
};
|
||||
}
|
||||
|
||||
export default class ProductsReport extends Component {
|
||||
render() {
|
||||
const { path, query } = this.props;
|
||||
const isProductDetailsView = query.filter === 'single_product';
|
||||
const { compareObject, itemsLabel, mode } = this.getChartMeta();
|
||||
const {
|
||||
path,
|
||||
query,
|
||||
isProductsError,
|
||||
isProductsRequesting,
|
||||
isSingleProductVariable,
|
||||
} = this.props;
|
||||
|
||||
const itemsLabel = isProductDetailsView
|
||||
? __( '%s variations', 'wc-admin' )
|
||||
: __( '%s products', 'wc-admin' );
|
||||
if ( isProductsError ) {
|
||||
return <ReportError isError />;
|
||||
}
|
||||
|
||||
if ( isProductsRequesting ) {
|
||||
return (
|
||||
<Fragment>
|
||||
<ReportFilters query={ query } path={ path } filters={ filters } />
|
||||
<SummaryListPlaceholder numberOfItems={ charts.length } />
|
||||
<span className="screen-reader-text">
|
||||
{ __( 'Your requested data is loading', 'wc-admin' ) }
|
||||
</span>
|
||||
<div className="woocommerce-chart">
|
||||
<div className="woocommerce-chart__body">
|
||||
<ChartPlaceholder height={ 220 } />
|
||||
</div>
|
||||
</div>
|
||||
<ProductsReportTable query={ query } />
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
const chartQuery = {
|
||||
...query,
|
||||
};
|
||||
|
||||
if ( 'item-comparison' === mode ) {
|
||||
chartQuery.segmentby = 'products' === compareObject ? 'product' : 'variation';
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<ReportFilters query={ query } path={ path } filters={ filters } />
|
||||
<ReportSummary
|
||||
mode={ mode }
|
||||
charts={ charts }
|
||||
endpoint="products"
|
||||
query={ query }
|
||||
query={ chartQuery }
|
||||
selectedChart={ getSelectedChart( query.chart, charts ) }
|
||||
/>
|
||||
<ReportChart
|
||||
mode={ mode }
|
||||
filters={ filters }
|
||||
charts={ charts }
|
||||
endpoint="products"
|
||||
itemsLabel={ itemsLabel }
|
||||
path={ path }
|
||||
query={ query }
|
||||
selectedChart={ getSelectedChart( query.chart, charts ) }
|
||||
query={ chartQuery }
|
||||
selectedChart={ getSelectedChart( chartQuery.chart, charts ) }
|
||||
/>
|
||||
{ isProductDetailsView ? (
|
||||
{ isSingleProductVariable ? (
|
||||
<VariationsReportTable query={ query } />
|
||||
) : (
|
||||
<ProductsReportTable query={ query } />
|
||||
|
@ -62,3 +125,34 @@ ProductsReport.propTypes = {
|
|||
path: PropTypes.string.isRequired,
|
||||
query: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default compose(
|
||||
withSelect( ( select, props ) => {
|
||||
const { query } = props;
|
||||
const { getProducts, isGetProductsRequesting, getProductsError } = select( 'wc-api' );
|
||||
const isSingleProductView = query.products && 1 === query.products.split( ',' ).length;
|
||||
if ( isSingleProductView ) {
|
||||
const includeArgs = { include: query.products };
|
||||
// TODO Look at similar usage to populate tags in the Search component.
|
||||
const products = getProducts( includeArgs );
|
||||
const isVariable = products[ 0 ] && 'variable' === products[ 0 ].type;
|
||||
const isProductsRequesting = isGetProductsRequesting( includeArgs );
|
||||
const isProductsError = getProductsError( includeArgs );
|
||||
return {
|
||||
query: {
|
||||
...query,
|
||||
'is-variable': isVariable,
|
||||
},
|
||||
isSingleProductView,
|
||||
isSingleProductVariable: isVariable,
|
||||
isProductsRequesting,
|
||||
isProductsError,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
query,
|
||||
isSingleProductView,
|
||||
};
|
||||
} )
|
||||
)( ProductsReport );
|
||||
|
|
|
@ -139,12 +139,11 @@ export default class VariationsReportTable extends Component {
|
|||
}
|
||||
|
||||
getSummary( totals ) {
|
||||
const { products_count = 0, items_sold = 0, net_revenue = 0, orders_count = 0 } = totals;
|
||||
const { variations_count = 0, items_sold = 0, net_revenue = 0, orders_count = 0 } = totals;
|
||||
return [
|
||||
{
|
||||
// @TODO: When primaryData is segmented, fix this to reflect variations, not products.
|
||||
label: _n( 'variation sold', 'variations sold', products_count, 'wc-admin' ),
|
||||
value: numberFormat( products_count ),
|
||||
label: _n( 'variation sold', 'variations sold', variations_count, 'wc-admin' ),
|
||||
value: numberFormat( variations_count ),
|
||||
},
|
||||
{
|
||||
label: _n( 'item sold', 'items sold', items_sold, 'wc-admin' ),
|
||||
|
@ -176,7 +175,7 @@ export default class VariationsReportTable extends Component {
|
|||
endpoint="variations"
|
||||
getHeadersContent={ this.getHeadersContent }
|
||||
getRowsContent={ this.getRowsContent }
|
||||
itemIdField="product_id"
|
||||
itemIdField="variation_id"
|
||||
labels={ labels }
|
||||
query={ query }
|
||||
getSummary={ this.getSummary }
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
/** @format */
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import operations from './operations';
|
||||
import selectors from './selectors';
|
||||
|
||||
export default {
|
||||
operations,
|
||||
selectors,
|
||||
};
|
|
@ -0,0 +1,51 @@
|
|||
/** @format */
|
||||
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import apiFetch from '@wordpress/api-fetch';
|
||||
|
||||
/**
|
||||
* WooCommerce dependencies
|
||||
*/
|
||||
import { stringifyQuery } from '@woocommerce/navigation';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { isResourcePrefix, getResourceIdentifier, getResourceName } from '../utils';
|
||||
|
||||
function read( resourceNames, fetch = apiFetch ) {
|
||||
const filteredNames = resourceNames.filter( name => isResourcePrefix( name, 'product-query' ) );
|
||||
|
||||
return filteredNames.map( async resourceName => {
|
||||
const query = getResourceIdentifier( resourceName );
|
||||
const url = `/wc/v3/products${ stringifyQuery( query ) }`;
|
||||
|
||||
try {
|
||||
const products = await fetch( {
|
||||
path: url,
|
||||
} );
|
||||
|
||||
const ids = products.map( product => product.id );
|
||||
const productResources = products.reduce( ( resources, product ) => {
|
||||
resources[ getResourceName( 'product', product.id ) ] = { data: product };
|
||||
return resources;
|
||||
}, {} );
|
||||
|
||||
return {
|
||||
[ resourceName ]: {
|
||||
data: ids,
|
||||
totalCount: ids.length,
|
||||
},
|
||||
...productResources,
|
||||
};
|
||||
} catch ( error ) {
|
||||
return { [ resourceName ]: { error } };
|
||||
}
|
||||
} );
|
||||
}
|
||||
|
||||
export default {
|
||||
read,
|
||||
};
|
|
@ -0,0 +1,44 @@
|
|||
/** @format */
|
||||
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { isNil } from 'lodash';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { getResourceName } from '../utils';
|
||||
import { DEFAULT_REQUIREMENT } from '../constants';
|
||||
|
||||
const getProducts = ( getResource, requireResource ) => (
|
||||
query = {},
|
||||
requirement = DEFAULT_REQUIREMENT
|
||||
) => {
|
||||
const resourceName = getResourceName( 'product-query', query );
|
||||
const ids = requireResource( requirement, resourceName ).data || [];
|
||||
const products = ids.map( id => getResource( getResourceName( 'product', id ) ).data || {} );
|
||||
return products;
|
||||
};
|
||||
|
||||
const getProductsError = getResource => ( query = {} ) => {
|
||||
const resourceName = getResourceName( 'product-query', query );
|
||||
return getResource( resourceName ).error;
|
||||
};
|
||||
|
||||
const isGetProductsRequesting = getResource => ( query = {} ) => {
|
||||
const resourceName = getResourceName( 'product-query', query );
|
||||
const { lastRequested, lastReceived } = getResource( resourceName );
|
||||
|
||||
if ( isNil( lastRequested ) || isNil( lastReceived ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return lastRequested > lastReceived;
|
||||
};
|
||||
|
||||
export default {
|
||||
getProducts,
|
||||
getProductsError,
|
||||
isGetProductsRequesting,
|
||||
};
|
|
@ -168,14 +168,13 @@ function getRequestQuery( endpoint, dataType, query ) {
|
|||
const interval = getIntervalForQuery( query );
|
||||
const filterQuery = getFilterQuery( endpoint, query );
|
||||
const end = datesFromQuery[ dataType ].before;
|
||||
const endingTimeOfDay = end.isSame( moment(), 'day' ) ? 'now' : 'end';
|
||||
|
||||
return {
|
||||
order: 'asc',
|
||||
interval,
|
||||
per_page: MAX_PER_PAGE,
|
||||
after: appendTimestamp( datesFromQuery[ dataType ].after, 'start' ),
|
||||
before: appendTimestamp( end, endingTimeOfDay ),
|
||||
before: appendTimestamp( end, 'end' ),
|
||||
segmentby: query.segmentby,
|
||||
...filterQuery,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
*/
|
||||
import items from './items';
|
||||
import notes from './notes';
|
||||
import products from './products';
|
||||
import reportItems from './reports/items';
|
||||
import reportStats from './reports/stats';
|
||||
import reviews from './reviews';
|
||||
|
@ -20,6 +21,7 @@ function createWcApiSpec() {
|
|||
selectors: {
|
||||
...items.selectors,
|
||||
...notes.selectors,
|
||||
...products.selectors,
|
||||
...reportItems.selectors,
|
||||
...reportStats.selectors,
|
||||
...reviews.selectors,
|
||||
|
@ -31,6 +33,7 @@ function createWcApiSpec() {
|
|||
return [
|
||||
...items.operations.read( resourceNames ),
|
||||
...notes.operations.read( resourceNames ),
|
||||
...products.operations.read( resourceNames ),
|
||||
...reportItems.operations.read( resourceNames ),
|
||||
...reportStats.operations.read( resourceNames ),
|
||||
...reviews.operations.read( resourceNames ),
|
||||
|
|
|
@ -60,6 +60,7 @@ class WC_Admin_REST_Reports_Products_Stats_Controller extends WC_REST_Reports_Co
|
|||
'net_revenue',
|
||||
'orders_count',
|
||||
'products_count',
|
||||
'variations_count',
|
||||
),
|
||||
);
|
||||
|
||||
|
@ -188,6 +189,13 @@ class WC_Admin_REST_Reports_Products_Stats_Controller extends WC_REST_Reports_Co
|
|||
'context' => array( 'view', 'edit' ),
|
||||
'readonly' => true,
|
||||
),
|
||||
'segment_label' => array(
|
||||
'description' => __( 'Human readable segment label, either product or variation name.', 'wc-admin' ),
|
||||
'type' => 'string',
|
||||
'context' => array( 'view', 'edit' ),
|
||||
'readonly' => true,
|
||||
'enum' => array( 'day', 'week', 'month', 'year' ),
|
||||
),
|
||||
'subtotals' => array(
|
||||
'description' => __( 'Interval subtotals.', 'wc-admin' ),
|
||||
'type' => 'object',
|
||||
|
@ -383,6 +391,15 @@ class WC_Admin_REST_Reports_Products_Stats_Controller extends WC_REST_Reports_Co
|
|||
'type' => 'integer',
|
||||
),
|
||||
);
|
||||
$params['variations'] = array(
|
||||
'description' => __( 'Limit result to items with specified variation ids.', 'wc-admin' ),
|
||||
'type' => 'array',
|
||||
'sanitize_callback' => 'wp_parse_id_list',
|
||||
'validate_callback' => 'rest_validate_request_arg',
|
||||
'items' => array(
|
||||
'type' => 'integer',
|
||||
),
|
||||
);
|
||||
$params['segmentby'] = array(
|
||||
'description' => __( 'Segment the response by additional constraint.', 'wc-admin' ),
|
||||
'type' => 'string',
|
||||
|
|
|
@ -290,7 +290,7 @@ class WC_Admin_REST_Reports_Variations_Controller extends WC_REST_Reports_Contro
|
|||
'default' => 'date',
|
||||
'enum' => array(
|
||||
'date',
|
||||
'gross_revenue',
|
||||
'net_revenue',
|
||||
'orders_count',
|
||||
'items_sold',
|
||||
),
|
||||
|
|
|
@ -180,6 +180,7 @@ class WC_Admin_Api_Init {
|
|||
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-variations-controller.php';
|
||||
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-product-reviews-controller.php';
|
||||
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-product-variations-controller.php';
|
||||
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-reports-controller.php';
|
||||
|
@ -217,6 +218,7 @@ class WC_Admin_Api_Init {
|
|||
'WC_Admin_REST_Orders_Controller',
|
||||
'WC_Admin_REST_Products_Controller',
|
||||
'WC_Admin_REST_Product_Categories_Controller',
|
||||
'WC_Admin_REST_Product_Variations_Controller',
|
||||
'WC_Admin_REST_Product_Reviews_Controller',
|
||||
'WC_Admin_REST_Product_Variations_Controller',
|
||||
'WC_Admin_REST_Reports_Controller',
|
||||
|
|
|
@ -21,10 +21,11 @@ class WC_Admin_Reports_Products_Stats_Segmenting extends WC_Admin_Reports_Segmen
|
|||
*/
|
||||
protected function get_segment_selections_product_level( $products_table ) {
|
||||
$columns_mapping = array(
|
||||
'items_sold' => "SUM($products_table.product_qty) as items_sold",
|
||||
'net_revenue' => "SUM($products_table.product_net_revenue ) AS net_revenue",
|
||||
'orders_count' => "COUNT( DISTINCT $products_table.order_id ) AS orders_count",
|
||||
'products_count' => "COUNT( DISTINCT $products_table.product_id ) AS products_count",
|
||||
'items_sold' => "SUM($products_table.product_qty) as items_sold",
|
||||
'net_revenue' => "SUM($products_table.product_net_revenue ) AS net_revenue",
|
||||
'orders_count' => "COUNT( DISTINCT $products_table.order_id ) AS orders_count",
|
||||
'products_count' => "COUNT( DISTINCT $products_table.product_id ) AS products_count",
|
||||
'variations_count' => "COUNT( DISTINCT $products_table.variation_id ) AS variations_count",
|
||||
);
|
||||
|
||||
return $this->prepare_selections( $columns_mapping );
|
||||
|
|
|
@ -19,6 +19,13 @@ class WC_Admin_Reports_Segmenting {
|
|||
*/
|
||||
protected $all_segment_ids = false;
|
||||
|
||||
/**
|
||||
* Array of all segment labels.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $segment_labels = array();
|
||||
|
||||
/**
|
||||
* Query arguments supplied by the user for data store.
|
||||
*
|
||||
|
@ -88,10 +95,12 @@ class WC_Admin_Reports_Segmenting {
|
|||
|
||||
foreach ( $segments_db_result as $segment_data ) {
|
||||
$segment_id = $segment_data[ $segment_dimension ];
|
||||
$segment_labels = $this->get_segment_labels();
|
||||
unset( $segment_data[ $segment_dimension ] );
|
||||
$segment_datum = array(
|
||||
'segment_id' => $segment_id,
|
||||
'subtotals' => $segment_data,
|
||||
'segment_id' => $segment_id,
|
||||
'segment_label' => $segment_labels[ $segment_id ],
|
||||
'subtotals' => $segment_data,
|
||||
);
|
||||
$segment_result[ $segment_id ] = $segment_datum;
|
||||
}
|
||||
|
@ -133,13 +142,15 @@ class WC_Admin_Reports_Segmenting {
|
|||
*/
|
||||
protected function merge_segment_totals_results( $segment_dimension, $result1, $result2 ) {
|
||||
$result_segments = array();
|
||||
$segment_labels = $this->get_segment_labels();
|
||||
|
||||
foreach ( $result1 as $segment_data ) {
|
||||
$segment_id = $segment_data[ $segment_dimension ];
|
||||
unset( $segment_data[ $segment_dimension ] );
|
||||
$result_segments[ $segment_id ] = array(
|
||||
'segment_id' => $segment_id,
|
||||
'subtotals' => $segment_data,
|
||||
'segment_label' => $segment_labels[ $segment_id ],
|
||||
'segment_id' => $segment_id,
|
||||
'subtotals' => $segment_data,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -148,8 +159,9 @@ class WC_Admin_Reports_Segmenting {
|
|||
unset( $segment_data[ $segment_dimension ] );
|
||||
if ( ! isset( $result_segments[ $segment_id ] ) ) {
|
||||
$result_segments[ $segment_id ] = array(
|
||||
'segment_id' => $segment_id,
|
||||
'subtotals' => array(),
|
||||
'segment_label' => $segment_labels[ $segment_id ],
|
||||
'segment_id' => $segment_id,
|
||||
'subtotals' => array(),
|
||||
);
|
||||
}
|
||||
$result_segments[ $segment_id ]['subtotals'] = array_merge( $result_segments[ $segment_id ]['subtotals'], $segment_data );
|
||||
|
@ -196,6 +208,7 @@ class WC_Admin_Reports_Segmenting {
|
|||
*/
|
||||
protected function merge_segment_intervals_results( $segment_dimension, $result1, $result2 ) {
|
||||
$result_segments = array();
|
||||
$segment_labels = $this->get_segment_labels();
|
||||
|
||||
foreach ( $result1 as $segment_data ) {
|
||||
$time_interval = $segment_data['time_interval'];
|
||||
|
@ -208,8 +221,9 @@ class WC_Admin_Reports_Segmenting {
|
|||
$segment_id = $segment_data[ $segment_dimension ];
|
||||
unset( $segment_data[ $segment_dimension ] );
|
||||
$segment_datum = array(
|
||||
'segment_id' => $segment_id,
|
||||
'subtotals' => $segment_data,
|
||||
'segment_label' => $segment_labels[ $segment_id ],
|
||||
'segment_id' => $segment_id,
|
||||
'subtotals' => $segment_data,
|
||||
);
|
||||
$result_segments[ $time_interval ]['segments'][ $segment_id ] = $segment_datum;
|
||||
}
|
||||
|
@ -227,8 +241,9 @@ class WC_Admin_Reports_Segmenting {
|
|||
|
||||
if ( ! isset( $result_segments[ $time_interval ]['segments'][ $segment_id ] ) ) {
|
||||
$result_segments[ $time_interval ]['segments'][ $segment_id ] = array(
|
||||
'segment_id' => $segment_id,
|
||||
'subtotals' => array(),
|
||||
'segment_label' => $segment_labels[ $segment_id ],
|
||||
'segment_id' => $segment_id,
|
||||
'subtotals' => array(),
|
||||
);
|
||||
}
|
||||
$result_segments[ $time_interval ]['segments'][ $segment_id ]['subtotals'] = array_merge( $result_segments[ $time_interval ]['segments'][ $segment_id ]['subtotals'], $segment_data );
|
||||
|
@ -251,6 +266,8 @@ class WC_Admin_Reports_Segmenting {
|
|||
$segment_dimension = substr( strstr( $segment_dimension, '.' ), 1 );
|
||||
}
|
||||
|
||||
$segment_labels = $this->get_segment_labels();
|
||||
|
||||
foreach ( $segments_db_result as $segment_data ) {
|
||||
$time_interval = $segment_data['time_interval'];
|
||||
if ( ! isset( $aggregated_segment_result[ $time_interval ] ) ) {
|
||||
|
@ -262,8 +279,9 @@ class WC_Admin_Reports_Segmenting {
|
|||
$segment_id = $segment_data[ $segment_dimension ];
|
||||
unset( $segment_data[ $segment_dimension ] );
|
||||
$segment_datum = array(
|
||||
'segment_id' => $segment_id,
|
||||
'subtotals' => $segment_data,
|
||||
'segment_label' => $segment_labels[ $segment_id ],
|
||||
'segment_id' => $segment_id,
|
||||
'subtotals' => $segment_data,
|
||||
);
|
||||
$aggregated_segment_result[ $time_interval ]['segments'][ $segment_id ] = $segment_datum;
|
||||
}
|
||||
|
@ -284,13 +302,25 @@ class WC_Admin_Reports_Segmenting {
|
|||
return;
|
||||
}
|
||||
|
||||
$segments = array();
|
||||
$segment_labels = array();
|
||||
|
||||
if ( 'product' === $this->query_args['segmentby'] ) {
|
||||
$segments = wc_get_products(
|
||||
array(
|
||||
'return' => 'ids',
|
||||
'limit' => -1,
|
||||
)
|
||||
$args = array(
|
||||
'return' => 'objects',
|
||||
'limit' => -1,
|
||||
);
|
||||
|
||||
if ( isset( $this->query_args['product_includes'] ) ) {
|
||||
$args['include'] = $this->query_args['product_includes'];
|
||||
}
|
||||
|
||||
$segment_objects = wc_get_products( $args );
|
||||
foreach ( $segment_objects as $segment ) {
|
||||
$id = $segment->get_id();
|
||||
$segments[] = $id;
|
||||
$segment_labels[ $id ] = $segment->get_name();
|
||||
}
|
||||
} elseif ( 'variation' === $this->query_args['segmentby'] ) {
|
||||
// @todo: assuming that this will only be used for one product, check assumption.
|
||||
if ( ! isset( $this->query_args['product_includes'] ) || count( $this->query_args['product_includes'] ) !== 1 ) {
|
||||
|
@ -298,14 +328,24 @@ class WC_Admin_Reports_Segmenting {
|
|||
return;
|
||||
}
|
||||
|
||||
$segments = wc_get_products(
|
||||
array(
|
||||
'return' => 'ids',
|
||||
'limit' => - 1,
|
||||
'type' => 'variation',
|
||||
'parent' => $this->query_args['product_includes'][0],
|
||||
)
|
||||
$args = array(
|
||||
'return' => 'objects',
|
||||
'limit' => -1,
|
||||
'type' => 'variation',
|
||||
'parent' => $this->query_args['product_includes'][0],
|
||||
);
|
||||
|
||||
if ( isset( $this->query_args['variations'] ) ) {
|
||||
$args['include'] = $this->query_args['variations'];
|
||||
}
|
||||
|
||||
$segment_objects = wc_get_products( $args );
|
||||
|
||||
foreach ( $segment_objects as $segment ) {
|
||||
$id = $segment->get_id();
|
||||
$segments[] = $id;
|
||||
$segment_labels[ $id ] = $segment->get_name();
|
||||
}
|
||||
} elseif ( 'category' === $this->query_args['segmentby'] ) {
|
||||
$categories = get_categories(
|
||||
array(
|
||||
|
@ -338,6 +378,7 @@ class WC_Admin_Reports_Segmenting {
|
|||
}
|
||||
|
||||
$this->all_segment_ids = $segments;
|
||||
$this->segment_labels = $segment_labels;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -353,6 +394,19 @@ class WC_Admin_Reports_Segmenting {
|
|||
return $this->all_segment_ids;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all segment labels for given segmentby query parameter.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function get_segment_labels() {
|
||||
if ( ! is_array( $this->all_segment_ids ) ) {
|
||||
$this->set_all_segments();
|
||||
}
|
||||
|
||||
return $this->segment_labels;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares two report data objects by pre-defined object property and ASC/DESC ordering.
|
||||
*
|
||||
|
@ -395,11 +449,13 @@ class WC_Admin_Reports_Segmenting {
|
|||
$segments = array();
|
||||
}
|
||||
$all_segment_ids = $this->get_all_segments();
|
||||
$segment_labels = $this->get_segment_labels();
|
||||
foreach ( $all_segment_ids as $segment_id ) {
|
||||
if ( ! isset( $segments[ $segment_id ] ) ) {
|
||||
$segments[ $segment_id ] = array(
|
||||
'segment_id' => $segment_id,
|
||||
'subtotals' => $segment_subtotals,
|
||||
'segment_id' => $segment_id,
|
||||
'segment_label' => $segment_labels[ $segment_id ],
|
||||
'subtotals' => $segment_subtotals,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -211,7 +211,7 @@ class WC_Admin_Reports_Coupons_Stats_Data_Store extends WC_Admin_Reports_Coupons
|
|||
if ( WC_Admin_Reports_Interval::intervals_missing( $expected_interval_count, $db_interval_count, $intervals_query['per_page'], $query_args['page'], $query_args['order'], $query_args['orderby'], count( $intervals ) ) ) {
|
||||
$this->fill_in_missing_intervals( $db_intervals, $query_args['adj_after'], $query_args['adj_before'], $query_args['interval'], $data );
|
||||
$this->sort_intervals( $data, $query_args['orderby'], $query_args['order'] );
|
||||
$this->remove_extra_records( $data, $query_args['page'], $intervals_query['per_page'], $db_interval_count, $expected_interval_count, $query_args['orderby'] );
|
||||
$this->remove_extra_records( $data, $query_args['page'], $intervals_query['per_page'], $db_interval_count, $expected_interval_count, $query_args['orderby'], $query_args['order'] );
|
||||
} else {
|
||||
$this->update_interval_boundary_dates( $query_args['after'], $query_args['before'], $query_args['interval'], $data->intervals );
|
||||
}
|
||||
|
|
|
@ -76,7 +76,15 @@ class WC_Admin_Reports_Data_Store {
|
|||
// @todo: should return WP_Error here perhaps?
|
||||
}
|
||||
if ( $a[ $this->order_by ] === $b[ $this->order_by ] ) {
|
||||
return 0;
|
||||
// As relative order is undefined in case of equality in usort, second-level sorting by date needs to be enforced
|
||||
// so that paging is stable.
|
||||
if ( $a['time_interval'] === $b['time_interval'] ) {
|
||||
return 0; // This should never happen.
|
||||
} elseif ( $a['time_interval'] > $b['time_interval'] ) {
|
||||
return 1;
|
||||
} elseif ( $a['time_interval'] < $b['time_interval'] ) {
|
||||
return -1;
|
||||
}
|
||||
} elseif ( $a[ $this->order_by ] > $b[ $this->order_by ] ) {
|
||||
return strtolower( $this->order ) === 'desc' ? -1 : 1;
|
||||
} elseif ( $a[ $this->order_by ] < $b[ $this->order_by ] ) {
|
||||
|
@ -174,12 +182,17 @@ class WC_Admin_Reports_Data_Store {
|
|||
* @param int $db_interval_count Database interval count.
|
||||
* @param int $expected_interval_count Expected interval count on the output.
|
||||
* @param string $order_by Order by field.
|
||||
* @param string $order ASC or DESC.
|
||||
*/
|
||||
protected function remove_extra_records( &$data, $page_no, $items_per_page, $db_interval_count, $expected_interval_count, $order_by ) {
|
||||
protected function remove_extra_records( &$data, $page_no, $items_per_page, $db_interval_count, $expected_interval_count, $order_by, $order ) {
|
||||
if ( 'date' === strtolower( $order_by ) ) {
|
||||
$offset = 0;
|
||||
} else {
|
||||
$offset = ( $page_no - 1 ) * $items_per_page - $db_interval_count;
|
||||
if ( 'asc' === strtolower( $order ) ) {
|
||||
$offset = ( $page_no - 1 ) * $items_per_page;
|
||||
} else {
|
||||
$offset = ( $page_no - 1 ) * $items_per_page - $db_interval_count;
|
||||
}
|
||||
$offset = $offset < 0 ? 0 : $offset;
|
||||
}
|
||||
$count = $expected_interval_count - ( $page_no - 1 ) * $items_per_page;
|
||||
|
@ -672,6 +685,24 @@ class WC_Admin_Reports_Data_Store {
|
|||
return $included_products_str;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns comma separated ids of allowed variations, based on query arguments from the user.
|
||||
*
|
||||
* @param array $query_args Parameters supplied by the user.
|
||||
* @return string
|
||||
*/
|
||||
protected function get_included_variations( $query_args ) {
|
||||
$included_variations = array();
|
||||
$operator = $this->get_match_operator( $query_args );
|
||||
|
||||
if ( isset( $query_args['variations'] ) && is_array( $query_args['variations'] ) && count( $query_args['variations'] ) > 0 ) {
|
||||
$included_variations = array_filter( array_map( 'intval', $query_args['variations'] ) );
|
||||
}
|
||||
|
||||
$included_variations_str = implode( ',', $included_variations );
|
||||
return $included_variations_str;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns comma separated ids of excluded products, based on query arguments from the user.
|
||||
*
|
||||
|
|
|
@ -167,7 +167,7 @@ class WC_Admin_Reports_Downloads_Stats_Data_Store extends WC_Admin_Reports_Downl
|
|||
if ( $this->intervals_missing( $expected_interval_count, $db_records_count, $intervals_query['per_page'], $query_args['page'], $query_args['order'], $query_args['orderby'], count( $intervals ) ) ) {
|
||||
$this->fill_in_missing_intervals( $db_intervals, $query_args['adj_after'], $query_args['adj_before'], $query_args['interval'], $data );
|
||||
$this->sort_intervals( $data, $query_args['orderby'], $query_args['order'] );
|
||||
$this->remove_extra_records( $data, $query_args['page'], $intervals_query['per_page'], $db_records_count, $expected_interval_count, $query_args['orderby'] );
|
||||
$this->remove_extra_records( $data, $query_args['page'], $intervals_query['per_page'], $db_records_count, $expected_interval_count, $query_args['orderby'], $query_args['order'] );
|
||||
} else {
|
||||
$this->update_interval_boundary_dates( $query_args['after'], $query_args['before'], $query_args['interval'], $data->intervals );
|
||||
}
|
||||
|
|
|
@ -311,7 +311,7 @@ class WC_Admin_Reports_Orders_Stats_Data_Store extends WC_Admin_Reports_Data_Sto
|
|||
if ( WC_Admin_Reports_Interval::intervals_missing( $expected_interval_count, $db_interval_count, $intervals_query['per_page'], $query_args['page'], $query_args['order'], $query_args['orderby'], count( $intervals ) ) ) {
|
||||
$this->fill_in_missing_intervals( $db_intervals, $query_args['adj_after'], $query_args['adj_before'], $query_args['interval'], $data );
|
||||
$this->sort_intervals( $data, $query_args['orderby'], $query_args['order'] );
|
||||
$this->remove_extra_records( $data, $query_args['page'], $intervals_query['per_page'], $db_interval_count, $expected_interval_count, $query_args['orderby'] );
|
||||
$this->remove_extra_records( $data, $query_args['page'], $intervals_query['per_page'], $db_interval_count, $expected_interval_count, $query_args['orderby'], $query_args['order'] );
|
||||
} else {
|
||||
$this->update_interval_boundary_dates( $query_args['after'], $query_args['before'], $query_args['interval'], $data->intervals );
|
||||
}
|
||||
|
|
|
@ -140,6 +140,11 @@ class WC_Admin_Reports_Products_Data_Store extends WC_Admin_Reports_Data_Store i
|
|||
$sql_query_params['where_clause'] .= " AND {$order_product_lookup_table}.product_id IN ({$included_products})";
|
||||
}
|
||||
|
||||
$included_variations = $this->get_included_variations( $query_args );
|
||||
if ( $included_variations ) {
|
||||
$sql_query_params['where_clause'] .= " AND {$order_product_lookup_table}.variation_id IN ({$included_variations})";
|
||||
}
|
||||
|
||||
$order_status_filter = $this->get_status_subquery( $query_args );
|
||||
if ( $order_status_filter ) {
|
||||
$sql_query_params['from_clause'] .= " JOIN {$wpdb->prefix}wc_order_stats ON {$order_product_lookup_table}.order_id = {$wpdb->prefix}wc_order_stats.order_id";
|
||||
|
|
|
@ -19,13 +19,14 @@ class WC_Admin_Reports_Products_Stats_Data_Store extends WC_Admin_Reports_Produc
|
|||
* @var array
|
||||
*/
|
||||
protected $column_types = array(
|
||||
'date_start' => 'strval',
|
||||
'date_end' => 'strval',
|
||||
'product_id' => 'intval',
|
||||
'items_sold' => 'intval',
|
||||
'net_revenue' => 'floatval',
|
||||
'orders_count' => 'intval',
|
||||
'products_count' => 'intval',
|
||||
'date_start' => 'strval',
|
||||
'date_end' => 'strval',
|
||||
'product_id' => 'intval',
|
||||
'items_sold' => 'intval',
|
||||
'net_revenue' => 'floatval',
|
||||
'orders_count' => 'intval',
|
||||
'products_count' => 'intval',
|
||||
'variations_count' => 'intval',
|
||||
);
|
||||
|
||||
/**
|
||||
|
@ -34,10 +35,11 @@ class WC_Admin_Reports_Products_Stats_Data_Store extends WC_Admin_Reports_Produc
|
|||
* @var array
|
||||
*/
|
||||
protected $report_columns = array(
|
||||
'items_sold' => 'SUM(product_qty) as items_sold',
|
||||
'net_revenue' => 'SUM(product_net_revenue) AS net_revenue',
|
||||
'orders_count' => 'COUNT(DISTINCT order_id) as orders_count',
|
||||
'products_count' => 'COUNT(DISTINCT product_id) as products_count',
|
||||
'items_sold' => 'SUM(product_qty) as items_sold',
|
||||
'net_revenue' => 'SUM(product_net_revenue) AS net_revenue',
|
||||
'orders_count' => 'COUNT(DISTINCT order_id) as orders_count',
|
||||
'products_count' => 'COUNT(DISTINCT product_id) as products_count',
|
||||
'variations_count' => 'COUNT(DISTINCT variation_id) as variations_count',
|
||||
);
|
||||
|
||||
/**
|
||||
|
@ -70,6 +72,11 @@ class WC_Admin_Reports_Products_Stats_Data_Store extends WC_Admin_Reports_Produc
|
|||
$products_where_clause .= " AND {$order_product_lookup_table}.product_id IN ({$included_products})";
|
||||
}
|
||||
|
||||
$included_variations = $this->get_included_variations( $query_args );
|
||||
if ( $included_variations ) {
|
||||
$products_where_clause .= " AND {$order_product_lookup_table}.variation_id IN ({$included_variations})";
|
||||
}
|
||||
|
||||
$order_status_filter = $this->get_status_subquery( $query_args );
|
||||
if ( $order_status_filter ) {
|
||||
$products_from_clause .= " JOIN {$wpdb->prefix}wc_order_stats ON {$order_product_lookup_table}.order_id = {$wpdb->prefix}wc_order_stats.order_id";
|
||||
|
@ -207,7 +214,7 @@ class WC_Admin_Reports_Products_Stats_Data_Store extends WC_Admin_Reports_Produc
|
|||
if ( WC_Admin_Reports_Interval::intervals_missing( $expected_interval_count, $db_interval_count, $intervals_query['per_page'], $query_args['page'], $query_args['order'], $query_args['orderby'], count( $intervals ) ) ) {
|
||||
$this->fill_in_missing_intervals( $db_intervals, $query_args['adj_after'], $query_args['adj_before'], $query_args['interval'], $data );
|
||||
$this->sort_intervals( $data, $query_args['orderby'], $query_args['order'] );
|
||||
$this->remove_extra_records( $data, $query_args['page'], $intervals_query['per_page'], $db_interval_count, $expected_interval_count, $query_args['orderby'] );
|
||||
$this->remove_extra_records( $data, $query_args['page'], $intervals_query['per_page'], $db_interval_count, $expected_interval_count, $query_args['orderby'], $query_args['order'] );
|
||||
} else {
|
||||
$this->update_interval_boundary_dates( $query_args['after'], $query_args['before'], $query_args['interval'], $data->intervals );
|
||||
}
|
||||
|
|
|
@ -229,6 +229,7 @@ class WC_Admin_Reports_Variations_Data_Store extends WC_Admin_Reports_Data_Store
|
|||
{$sql_query_params['from_clause']}
|
||||
WHERE
|
||||
1=1
|
||||
{$sql_query_params['where_time_clause']}
|
||||
{$sql_query_params['where_clause']}
|
||||
GROUP BY
|
||||
variation_id
|
||||
|
@ -248,6 +249,7 @@ class WC_Admin_Reports_Variations_Data_Store extends WC_Admin_Reports_Data_Store
|
|||
{$sql_query_params['from_clause']}
|
||||
WHERE
|
||||
1=1
|
||||
{$sql_query_params['where_time_clause']}
|
||||
{$sql_query_params['where_clause']}
|
||||
GROUP BY
|
||||
variation_id
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import classNames from 'classnames';
|
||||
import { Component, createRef } from '@wordpress/element';
|
||||
import PropTypes from 'prop-types';
|
||||
|
@ -36,7 +37,7 @@ class D3Legend extends Component {
|
|||
}
|
||||
|
||||
updateListScroll() {
|
||||
if ( ! this.listRef ) {
|
||||
if ( ! this || ! this.listRef ) {
|
||||
return;
|
||||
}
|
||||
const list = this.listRef.current;
|
||||
|
@ -93,7 +94,12 @@ class D3Legend extends Component {
|
|||
<button
|
||||
onClick={ handleLegendToggle }
|
||||
id={ row.key }
|
||||
disabled={ ( row.visible && numberOfRowsVisible <= 1 ) || ! interactive }
|
||||
disabled={
|
||||
( row.visible && numberOfRowsVisible <= 1 ) ||
|
||||
( ! row.visible && numberOfRowsVisible >= 5 ) ||
|
||||
! interactive
|
||||
}
|
||||
title={ numberOfRowsVisible >= 5 ? __( 'You may select up to 5 items.', 'wc-admin' ) : '' }
|
||||
>
|
||||
<div className="woocommerce-legend__item-container" id={ row.key }>
|
||||
<span
|
||||
|
|
|
@ -4,9 +4,12 @@
|
|||
*/
|
||||
@import './legend.scss';
|
||||
|
||||
.woocommerce-chart__body-row .d3-chart__container {
|
||||
width: calc( 100% - 320px );
|
||||
}
|
||||
|
||||
.d3-chart__container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
|
||||
svg {
|
||||
overflow: visible;
|
||||
|
|
|
@ -176,6 +176,11 @@ export const compareStrings = ( s1, s2, splitChar = new RegExp( [ ' |,' ], 'g' )
|
|||
export const getYGrids = ( yMax ) => {
|
||||
const yGrids = [];
|
||||
|
||||
// If all values are 0, yMax can become NaN.
|
||||
if ( isNaN( yMax ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for ( let i = 0; i < 4; i++ ) {
|
||||
const value = yMax > 1 ? Math.round( i / 3 * yMax ) : i / 3 * yMax;
|
||||
if ( yGrids[ yGrids.length - 1 ] !== value ) {
|
||||
|
@ -237,29 +242,31 @@ export const drawAxis = ( node, params, xOffset ) => {
|
|||
.tickFormat( '' )
|
||||
);
|
||||
|
||||
node
|
||||
.append( 'g' )
|
||||
.attr( 'class', 'grid' )
|
||||
.attr( 'transform', `translate(-${ params.margin.left }, 0)` )
|
||||
.call(
|
||||
d3AxisLeft( params.yScale )
|
||||
.tickValues( yGrids )
|
||||
.tickSize( -params.width - params.margin.left - params.margin.right )
|
||||
.tickFormat( '' )
|
||||
)
|
||||
.call( g => g.select( '.domain' ).remove() );
|
||||
if ( yGrids ) {
|
||||
node
|
||||
.append( 'g' )
|
||||
.attr( 'class', 'grid' )
|
||||
.attr( 'transform', `translate(-${ params.margin.left }, 0)` )
|
||||
.call(
|
||||
d3AxisLeft( params.yScale )
|
||||
.tickValues( yGrids )
|
||||
.tickSize( -params.width - params.margin.left - params.margin.right )
|
||||
.tickFormat( '' )
|
||||
)
|
||||
.call( g => g.select( '.domain' ).remove() );
|
||||
|
||||
node
|
||||
.append( 'g' )
|
||||
.attr( 'class', 'axis y-axis' )
|
||||
.attr( 'aria-hidden', 'true' )
|
||||
.attr( 'transform', 'translate(-50, 0)' )
|
||||
.attr( 'text-anchor', 'start' )
|
||||
.call(
|
||||
d3AxisLeft( params.yTickOffset )
|
||||
.tickValues( params.yMax === 0 ? [ yGrids[ 0 ] ] : yGrids )
|
||||
.tickFormat( d => params.yFormat( d !== 0 ? d : 0 ) )
|
||||
);
|
||||
node
|
||||
.append( 'g' )
|
||||
.attr( 'class', 'axis y-axis' )
|
||||
.attr( 'aria-hidden', 'true' )
|
||||
.attr( 'transform', 'translate(-50, 0)' )
|
||||
.attr( 'text-anchor', 'start' )
|
||||
.call(
|
||||
d3AxisLeft( params.yTickOffset )
|
||||
.tickValues( params.yMax === 0 ? [ yGrids[ 0 ] ] : yGrids )
|
||||
.tickFormat( d => params.yFormat( d !== 0 ? d : 0 ) )
|
||||
);
|
||||
}
|
||||
|
||||
node.selectAll( '.domain' ).remove();
|
||||
node
|
||||
|
|
|
@ -6,7 +6,7 @@ import { __, sprintf } from '@wordpress/i18n';
|
|||
import classNames from 'classnames';
|
||||
import { Component, createRef, Fragment } from '@wordpress/element';
|
||||
import { formatDefaultLocale as d3FormatDefaultLocale } from 'd3-format';
|
||||
import { get, isEqual, partial } from 'lodash';
|
||||
import { get, isEqual, partial, isEmpty } from 'lodash';
|
||||
import Gridicon from 'gridicons';
|
||||
import { IconButton, NavigableMenu, SelectControl } from '@wordpress/components';
|
||||
import { interpolateViridis as d3InterpolateViridis } from 'd3-scale-chromatic';
|
||||
|
@ -57,18 +57,29 @@ function getOrderedKeys( props, previousOrderedKeys = [] ) {
|
|||
return accum;
|
||||
}, [] )
|
||||
),
|
||||
].map( key => {
|
||||
].map( ( key ) => {
|
||||
const previousKey = previousOrderedKeys.find( item => key === item.key );
|
||||
const defaultVisibleStatus = 'item-comparison' === props.mode ? false : true;
|
||||
return {
|
||||
key,
|
||||
total: props.data.reduce( ( a, c ) => a + c[ key ].value, 0 ),
|
||||
visible: previousKey ? previousKey.visible : true,
|
||||
visible: previousKey ? previousKey.visible : defaultVisibleStatus,
|
||||
focus: true,
|
||||
};
|
||||
} );
|
||||
if ( props.mode === 'item-comparison' ) {
|
||||
|
||||
if ( 'item-comparison' === props.mode ) {
|
||||
updatedKeys.sort( ( a, b ) => b.total - a.total );
|
||||
if ( isEmpty( previousOrderedKeys ) ) {
|
||||
return updatedKeys.filter( key => key.total > 0 ).map( ( key, index ) => {
|
||||
return {
|
||||
...key,
|
||||
visible: index < 5 || key.visible,
|
||||
};
|
||||
} );
|
||||
}
|
||||
}
|
||||
|
||||
return updatedKeys;
|
||||
}
|
||||
|
||||
|
@ -94,7 +105,7 @@ class Chart extends Component {
|
|||
}
|
||||
|
||||
componentDidUpdate( prevProps ) {
|
||||
const { data } = this.props;
|
||||
const { data, query, mode } = this.props;
|
||||
if ( ! isEqual( [ ...data ].sort(), [ ...prevProps.data ].sort() ) ) {
|
||||
/**
|
||||
* Only update the orderedKeys when data is present so that
|
||||
|
@ -110,6 +121,16 @@ class Chart extends Component {
|
|||
} );
|
||||
/* eslint-enable react/no-did-update-set-state */
|
||||
}
|
||||
|
||||
if ( 'item-comparison' === mode && ! isEqual( query, prevProps.query ) ) {
|
||||
const orderedKeys = getOrderedKeys( this.props );
|
||||
/* eslint-disable react/no-did-update-set-state */
|
||||
this.setState( {
|
||||
orderedKeys,
|
||||
visibleData: this.getVisibleData( data, orderedKeys ),
|
||||
} );
|
||||
/* eslint-enable react/no-did-update-set-state */
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
|
@ -267,7 +288,7 @@ class Chart extends Component {
|
|||
const chartDirection = legendPosition === 'side' ? 'row' : 'column';
|
||||
|
||||
const chartHeight = this.getChartHeight();
|
||||
const legend = (
|
||||
const legend = isRequesting ? null : (
|
||||
<D3Legend
|
||||
colorScheme={ d3InterpolateViridis }
|
||||
data={ orderedKeys }
|
||||
|
|
|
@ -45,8 +45,7 @@ import TableSummary from './summary';
|
|||
class TableCard extends Component {
|
||||
constructor( props ) {
|
||||
super( props );
|
||||
const { compareBy, query } = props;
|
||||
|
||||
const { query, compareBy } = this.props;
|
||||
const showCols = props.headers.map( ( { key, hiddenByDefault } ) => ! hiddenByDefault && key ).filter( Boolean );
|
||||
const selectedRows = query.filter ? getIdsFromQuery( query[ compareBy ] ) : [];
|
||||
|
||||
|
@ -143,11 +142,10 @@ class TableCard extends Component {
|
|||
}
|
||||
|
||||
onCompare() {
|
||||
const { compareBy, compareParam, onQueryChange } = this.props;
|
||||
const { selectedRows } = this.state;
|
||||
if ( compareBy ) {
|
||||
onQueryChange( 'compare' )( compareBy, compareParam, selectedRows.join( ',' ) );
|
||||
}
|
||||
// Reset selected rows so the user can start a comparison again.
|
||||
this.setState( {
|
||||
selectedRows: [],
|
||||
} );
|
||||
}
|
||||
|
||||
onSearch( values ) {
|
||||
|
|
|
@ -87,11 +87,12 @@ class WC_Tests_API_Reports_Products_Stats extends WC_REST_Unit_Test_Case {
|
|||
|
||||
$expected_reports = array(
|
||||
'totals' => array(
|
||||
'items_sold' => 4,
|
||||
'net_revenue' => 100.0,
|
||||
'orders_count' => 1,
|
||||
'products_count' => 1,
|
||||
'segments' => array(),
|
||||
'items_sold' => 4,
|
||||
'net_revenue' => 100.0,
|
||||
'orders_count' => 1,
|
||||
'products_count' => 1,
|
||||
'variations_count' => 1,
|
||||
'segments' => array(),
|
||||
),
|
||||
'intervals' => array(
|
||||
array(
|
||||
|
@ -101,11 +102,12 @@ class WC_Tests_API_Reports_Products_Stats extends WC_REST_Unit_Test_Case {
|
|||
'date_end' => date( 'Y-m-d 23:59:59', $time ),
|
||||
'date_end_gmt' => date( 'Y-m-d 23:59:59', $time ),
|
||||
'subtotals' => (object) array(
|
||||
'items_sold' => 4,
|
||||
'net_revenue' => 100.0,
|
||||
'orders_count' => 1,
|
||||
'products_count' => 1,
|
||||
'segments' => array(),
|
||||
'items_sold' => 4,
|
||||
'net_revenue' => 100.0,
|
||||
'orders_count' => 1,
|
||||
'products_count' => 1,
|
||||
'variations_count' => 1,
|
||||
'segments' => array(),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue