Merge branch 'master' into fix/489-date_date_gmt
# Conflicts: # includes/class-wc-admin-api-init.php
This commit is contained in:
commit
f7d0d2379f
|
@ -26,6 +26,7 @@ wp-cli.local.yml
|
||||||
yarn.lock
|
yarn.lock
|
||||||
tests
|
tests
|
||||||
vendor
|
vendor
|
||||||
|
config
|
||||||
node_modules
|
node_modules
|
||||||
*.sql
|
*.sql
|
||||||
*.tar.gz
|
*.tar.gz
|
||||||
|
|
|
@ -8,6 +8,7 @@ build-style
|
||||||
languages/*
|
languages/*
|
||||||
!languages/README.md
|
!languages/README.md
|
||||||
wc-admin.zip
|
wc-admin.zip
|
||||||
|
includes/feature-config.php
|
||||||
|
|
||||||
# Directories/files that may appear in your environment
|
# Directories/files that may appear in your environment
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
|
@ -63,6 +63,7 @@ fi
|
||||||
|
|
||||||
# Run the build.
|
# Run the build.
|
||||||
status "Generating build... 👷♀️"
|
status "Generating build... 👷♀️"
|
||||||
|
npm run build:feature-config
|
||||||
npm run build
|
npm run build
|
||||||
npm run docs
|
npm run docs
|
||||||
|
|
||||||
|
@ -79,6 +80,6 @@ zip -r wc-admin.zip \
|
||||||
$build_files \
|
$build_files \
|
||||||
languages/wc-admin.pot \
|
languages/wc-admin.pot \
|
||||||
languages/wc-admin.php \
|
languages/wc-admin.php \
|
||||||
README.md
|
readme.txt
|
||||||
|
|
||||||
success "Done. You've built WooCommerce Admin! 🎉 "
|
success "Done. You've built WooCommerce Admin! 🎉 "
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Generates an array of feature flags, based on the config used by the client application.
|
||||||
|
*
|
||||||
|
* @package WooCommerce Admin
|
||||||
|
*/
|
||||||
|
|
||||||
|
$phase = isset( $_SERVER['WC_ADMIN_PHASE'] ) ? $_SERVER['WC_ADMIN_PHASE'] : ''; // WPCS: sanitization ok.
|
||||||
|
if ( ! in_array( $phase, array( 'development', 'plugin', 'core' ) ) ) {
|
||||||
|
$phase = 'core';
|
||||||
|
}
|
||||||
|
$config_json = file_get_contents( 'config/' . $phase . '.json' );
|
||||||
|
$config = json_decode( $config_json );
|
||||||
|
|
||||||
|
$write = "<?php\n";
|
||||||
|
$write .= "// WARNING: Do not directly edit this file.\n";
|
||||||
|
$write .= "// This file is auto-generated as part of the build process and things may break.\n";
|
||||||
|
$write .= "function wc_admin_get_feature_config() {\n";
|
||||||
|
$write .= "\treturn array(\n";
|
||||||
|
foreach ( $config->features as $feature => $bool ) {
|
||||||
|
$write .= "\t\t'{$feature}' => " . ( $bool ? 'true' : 'false' ) . ",\n";
|
||||||
|
}
|
||||||
|
$write .= "\t);\n";
|
||||||
|
$write .= "}\n";
|
||||||
|
|
||||||
|
$config_file = fopen( 'includes/feature-config.php', 'w' );
|
||||||
|
fwrite( $config_file, $write );
|
||||||
|
fclose( $config_file );
|
|
@ -2,6 +2,7 @@
|
||||||
/**
|
/**
|
||||||
* External dependencies
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
|
import { __ } from '@wordpress/i18n';
|
||||||
import { Component } from '@wordpress/element';
|
import { Component } from '@wordpress/element';
|
||||||
import { compose } from '@wordpress/compose';
|
import { compose } from '@wordpress/compose';
|
||||||
import { format as formatDate } from '@wordpress/date';
|
import { format as formatDate } from '@wordpress/date';
|
||||||
|
@ -89,6 +90,7 @@ export class ReportChart extends Component {
|
||||||
|
|
||||||
renderChart( mode, isRequesting, chartData ) {
|
renderChart( mode, isRequesting, chartData ) {
|
||||||
const {
|
const {
|
||||||
|
emptySearchResults,
|
||||||
interactiveLegend,
|
interactiveLegend,
|
||||||
itemsLabel,
|
itemsLabel,
|
||||||
legendPosition,
|
legendPosition,
|
||||||
|
@ -101,11 +103,15 @@ export class ReportChart extends Component {
|
||||||
const currentInterval = getIntervalForQuery( query );
|
const currentInterval = getIntervalForQuery( query );
|
||||||
const allowedIntervals = getAllowedIntervalsForQuery( query );
|
const allowedIntervals = getAllowedIntervalsForQuery( query );
|
||||||
const formats = getDateFormatsForInterval( currentInterval, primaryData.data.intervals.length );
|
const formats = getDateFormatsForInterval( currentInterval, primaryData.data.intervals.length );
|
||||||
|
const emptyMessage = emptySearchResults
|
||||||
|
? __( 'No data for the current search', 'wc-admin' )
|
||||||
|
: __( 'No data for the selected date range', 'wc-admin' );
|
||||||
return (
|
return (
|
||||||
<Chart
|
<Chart
|
||||||
allowedIntervals={ allowedIntervals }
|
allowedIntervals={ allowedIntervals }
|
||||||
data={ chartData }
|
data={ chartData }
|
||||||
dateParser={ '%Y-%m-%dT%H:%M:%S' }
|
dateParser={ '%Y-%m-%dT%H:%M:%S' }
|
||||||
|
emptyMessage={ emptyMessage }
|
||||||
interactiveLegend={ interactiveLegend }
|
interactiveLegend={ interactiveLegend }
|
||||||
interval={ currentInterval }
|
interval={ currentInterval }
|
||||||
isRequesting={ isRequesting }
|
isRequesting={ isRequesting }
|
||||||
|
@ -222,6 +228,7 @@ export default compose(
|
||||||
|
|
||||||
if ( query.search && ! ( query[ endpoint ] && query[ endpoint ].length ) ) {
|
if ( query.search && ! ( query[ endpoint ] && query[ endpoint ].length ) ) {
|
||||||
return {
|
return {
|
||||||
|
emptySearchResults: true,
|
||||||
mode: chartMode,
|
mode: chartMode,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -61,8 +61,15 @@ export class ReportSummary extends Component {
|
||||||
|
|
||||||
const renderSummaryNumbers = ( { onToggle } ) =>
|
const renderSummaryNumbers = ( { onToggle } ) =>
|
||||||
charts.map( chart => {
|
charts.map( chart => {
|
||||||
const { key, label, type } = chart;
|
const { key, order, orderby, label, type } = chart;
|
||||||
const href = getNewPath( { chart: key } );
|
const newPath = { chart: key };
|
||||||
|
if ( orderby ) {
|
||||||
|
newPath.orderby = orderby;
|
||||||
|
}
|
||||||
|
if ( order ) {
|
||||||
|
newPath.order = order;
|
||||||
|
}
|
||||||
|
const href = getNewPath( newPath );
|
||||||
const isSelected = selectedChart.key === key;
|
const isSelected = selectedChart.key === key;
|
||||||
const { delta, prevValue, value } = this.getValues( key, type );
|
const { delta, prevValue, value } = this.getValues( key, type );
|
||||||
|
|
||||||
|
|
|
@ -194,7 +194,9 @@ export default compose(
|
||||||
if ( query.search && ! ( query[ endpoint ] && query[ endpoint ].length ) ) {
|
if ( query.search && ! ( query[ endpoint ] && query[ endpoint ].length ) ) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
const chartEndpoint = 'variations' === endpoint ? 'products' : endpoint;
|
const chartEndpoint = [ 'variations', 'categories' ].includes( endpoint )
|
||||||
|
? 'products'
|
||||||
|
: endpoint;
|
||||||
const primaryData = getSummary
|
const primaryData = getSummary
|
||||||
? getReportChartData( chartEndpoint, 'primary', query, select )
|
? getReportChartData( chartEndpoint, 'primary', query, select )
|
||||||
: {};
|
: {};
|
||||||
|
|
|
@ -13,16 +13,22 @@ export const charts = [
|
||||||
{
|
{
|
||||||
key: 'items_sold',
|
key: 'items_sold',
|
||||||
label: __( 'Items Sold', 'wc-admin' ),
|
label: __( 'Items Sold', 'wc-admin' ),
|
||||||
|
order: 'desc',
|
||||||
|
orderby: 'items_sold',
|
||||||
type: 'number',
|
type: 'number',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'net_revenue',
|
key: 'net_revenue',
|
||||||
label: __( 'Net Revenue', 'wc-admin' ),
|
label: __( 'Net Revenue', 'wc-admin' ),
|
||||||
|
order: 'desc',
|
||||||
|
orderby: 'net_revenue',
|
||||||
type: 'currency',
|
type: 'currency',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'orders_count',
|
key: 'orders_count',
|
||||||
label: __( 'Orders Count', 'wc-admin' ),
|
label: __( 'Orders Count', 'wc-admin' ),
|
||||||
|
order: 'desc',
|
||||||
|
orderby: 'orders_count',
|
||||||
type: 'number',
|
type: 'number',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
*/
|
*/
|
||||||
import { Component, Fragment } from '@wordpress/element';
|
import { Component, Fragment } from '@wordpress/element';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
import { __ } from '@wordpress/i18n';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* WooCommerce dependencies
|
* WooCommerce dependencies
|
||||||
|
@ -20,23 +21,53 @@ import ReportChart from 'analytics/components/report-chart';
|
||||||
import ReportSummary from 'analytics/components/report-summary';
|
import ReportSummary from 'analytics/components/report-summary';
|
||||||
|
|
||||||
export default class CategoriesReport extends Component {
|
export default class CategoriesReport extends Component {
|
||||||
|
getChartMeta() {
|
||||||
|
const { query } = this.props;
|
||||||
|
const isCategoryDetailsView =
|
||||||
|
'top_items' === query.filter ||
|
||||||
|
'top_revenue' === query.filter ||
|
||||||
|
'compare-categories' === query.filter;
|
||||||
|
|
||||||
|
const isSingleCategoryView = query.categories && 1 === query.categories.split( ',' ).length;
|
||||||
|
const mode =
|
||||||
|
isCategoryDetailsView || isSingleCategoryView ? 'item-comparison' : 'time-comparison';
|
||||||
|
const itemsLabel = __( '%d categories', 'wc-admin' );
|
||||||
|
|
||||||
|
return {
|
||||||
|
itemsLabel,
|
||||||
|
mode,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { query, path } = this.props;
|
const { query, path } = this.props;
|
||||||
|
const { mode, itemsLabel } = this.getChartMeta();
|
||||||
|
|
||||||
|
const chartQuery = {
|
||||||
|
...query,
|
||||||
|
};
|
||||||
|
|
||||||
|
if ( 'item-comparison' === mode ) {
|
||||||
|
chartQuery.segmentby = 'category';
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<ReportFilters query={ query } path={ path } filters={ filters } />
|
<ReportFilters query={ query } path={ path } filters={ filters } />
|
||||||
<ReportSummary
|
<ReportSummary
|
||||||
charts={ charts }
|
charts={ charts }
|
||||||
endpoint="categories"
|
endpoint="products"
|
||||||
query={ query }
|
query={ chartQuery }
|
||||||
selectedChart={ getSelectedChart( query.chart, charts ) }
|
selectedChart={ getSelectedChart( query.chart, charts ) }
|
||||||
/>
|
/>
|
||||||
<ReportChart
|
<ReportChart
|
||||||
|
filters={ filters }
|
||||||
charts={ charts }
|
charts={ charts }
|
||||||
endpoint="categories"
|
mode={ mode }
|
||||||
|
endpoint="products"
|
||||||
path={ path }
|
path={ path }
|
||||||
query={ query }
|
query={ chartQuery }
|
||||||
|
itemsLabel={ itemsLabel }
|
||||||
selectedChart={ getSelectedChart( query.chart, charts ) }
|
selectedChart={ getSelectedChart( query.chart, charts ) }
|
||||||
/>
|
/>
|
||||||
<CategoriesReportTable query={ query } />
|
<CategoriesReportTable query={ query } />
|
||||||
|
@ -47,4 +78,5 @@ export default class CategoriesReport extends Component {
|
||||||
|
|
||||||
CategoriesReport.propTypes = {
|
CategoriesReport.propTypes = {
|
||||||
query: PropTypes.object.isRequired,
|
query: PropTypes.object.isRequired,
|
||||||
|
path: PropTypes.string.isRequired,
|
||||||
};
|
};
|
||||||
|
|
|
@ -13,11 +13,15 @@ export const charts = [
|
||||||
{
|
{
|
||||||
key: 'orders_count',
|
key: 'orders_count',
|
||||||
label: __( 'Discounted Orders', 'wc-admin' ),
|
label: __( 'Discounted Orders', 'wc-admin' ),
|
||||||
|
order: 'desc',
|
||||||
|
orderby: 'orders_count',
|
||||||
type: 'number',
|
type: 'number',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'amount',
|
key: 'amount',
|
||||||
label: __( 'Amount', 'wc-admin' ),
|
label: __( 'Amount', 'wc-admin' ),
|
||||||
|
order: 'desc',
|
||||||
|
orderby: 'amount',
|
||||||
type: 'currency',
|
type: 'currency',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
|
@ -39,7 +39,7 @@ export const advancedFilters = {
|
||||||
remove: __( 'Remove customer name filter', 'wc-admin' ),
|
remove: __( 'Remove customer name filter', 'wc-admin' ),
|
||||||
rule: __( 'Select a customer name filter match', 'wc-admin' ),
|
rule: __( 'Select a customer name filter match', 'wc-admin' ),
|
||||||
/* translators: A sentence describing a Product filter. See screen shot for context: https://cloudup.com/cCsm3GeXJbE */
|
/* translators: A sentence describing a Product filter. See screen shot for context: https://cloudup.com/cCsm3GeXJbE */
|
||||||
title: __( 'Name {{rule /}} {{filter /}}', 'wc-admin' ),
|
title: __( '{{title}}Name{{/title}} {{rule /}} {{filter /}}', 'wc-admin' ),
|
||||||
filter: __( 'Select customer name', 'wc-admin' ),
|
filter: __( 'Select customer name', 'wc-admin' ),
|
||||||
},
|
},
|
||||||
rules: [
|
rules: [
|
||||||
|
@ -59,7 +59,7 @@ export const advancedFilters = {
|
||||||
type: 'customers',
|
type: 'customers',
|
||||||
getLabels: getRequestByIdString( NAMESPACE + '/customers', customer => ( {
|
getLabels: getRequestByIdString( NAMESPACE + '/customers', customer => ( {
|
||||||
id: customer.id,
|
id: customer.id,
|
||||||
label: [ customer.first_name, customer.last_name ].filter( Boolean ).join( ' ' ),
|
label: customer.name,
|
||||||
} ) ),
|
} ) ),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -70,7 +70,7 @@ export const advancedFilters = {
|
||||||
remove: __( 'Remove country filter', 'wc-admin' ),
|
remove: __( 'Remove country filter', 'wc-admin' ),
|
||||||
rule: __( 'Select a country filter match', 'wc-admin' ),
|
rule: __( 'Select a country filter match', 'wc-admin' ),
|
||||||
/* translators: A sentence describing a Product filter. See screen shot for context: https://cloudup.com/cCsm3GeXJbE */
|
/* translators: A sentence describing a Product filter. See screen shot for context: https://cloudup.com/cCsm3GeXJbE */
|
||||||
title: __( 'Country {{rule /}} {{filter /}}', 'wc-admin' ),
|
title: __( '{{title}}Country{{/title}} {{rule /}} {{filter /}}', 'wc-admin' ),
|
||||||
filter: __( 'Select country', 'wc-admin' ),
|
filter: __( 'Select country', 'wc-admin' ),
|
||||||
},
|
},
|
||||||
rules: [
|
rules: [
|
||||||
|
@ -111,7 +111,7 @@ export const advancedFilters = {
|
||||||
remove: __( 'Remove customer username filter', 'wc-admin' ),
|
remove: __( 'Remove customer username filter', 'wc-admin' ),
|
||||||
rule: __( 'Select a customer username filter match', 'wc-admin' ),
|
rule: __( 'Select a customer username filter match', 'wc-admin' ),
|
||||||
/* translators: A sentence describing a customer username filter. See screen shot for context: https://cloudup.com/cCsm3GeXJbE */
|
/* translators: A sentence describing a customer username filter. See screen shot for context: https://cloudup.com/cCsm3GeXJbE */
|
||||||
title: __( 'Username {{rule /}} {{filter /}}', 'wc-admin' ),
|
title: __( '{{title}}Username{{/title}} {{rule /}} {{filter /}}', 'wc-admin' ),
|
||||||
filter: __( 'Select customer username', 'wc-admin' ),
|
filter: __( 'Select customer username', 'wc-admin' ),
|
||||||
},
|
},
|
||||||
rules: [
|
rules: [
|
||||||
|
@ -139,7 +139,7 @@ export const advancedFilters = {
|
||||||
remove: __( 'Remove customer email filter', 'wc-admin' ),
|
remove: __( 'Remove customer email filter', 'wc-admin' ),
|
||||||
rule: __( 'Select a customer email filter match', 'wc-admin' ),
|
rule: __( 'Select a customer email filter match', 'wc-admin' ),
|
||||||
/* translators: A sentence describing a customer email filter. See screen shot for context: https://cloudup.com/cCsm3GeXJbE */
|
/* translators: A sentence describing a customer email filter. See screen shot for context: https://cloudup.com/cCsm3GeXJbE */
|
||||||
title: __( 'Email {{rule /}} {{filter /}}', 'wc-admin' ),
|
title: __( '{{title}}Email{{/title}} {{rule /}} {{filter /}}', 'wc-admin' ),
|
||||||
filter: __( 'Select customer email', 'wc-admin' ),
|
filter: __( 'Select customer email', 'wc-admin' ),
|
||||||
},
|
},
|
||||||
rules: [
|
rules: [
|
||||||
|
@ -168,7 +168,7 @@ export const advancedFilters = {
|
||||||
add: __( 'No. of Orders', 'wc-admin' ),
|
add: __( 'No. of Orders', 'wc-admin' ),
|
||||||
remove: __( 'Remove order filter', 'wc-admin' ),
|
remove: __( 'Remove order filter', 'wc-admin' ),
|
||||||
rule: __( 'Select an order count filter match', 'wc-admin' ),
|
rule: __( 'Select an order count filter match', 'wc-admin' ),
|
||||||
title: __( 'No. of Orders {{rule /}} {{filter /}}', 'wc-admin' ),
|
title: __( '{{title}}No. of Orders{{/title}} {{rule /}} {{filter /}}', 'wc-admin' ),
|
||||||
},
|
},
|
||||||
rules: [
|
rules: [
|
||||||
{
|
{
|
||||||
|
@ -196,7 +196,7 @@ export const advancedFilters = {
|
||||||
add: __( 'Total Spend', 'wc-admin' ),
|
add: __( 'Total Spend', 'wc-admin' ),
|
||||||
remove: __( 'Remove total spend filter', 'wc-admin' ),
|
remove: __( 'Remove total spend filter', 'wc-admin' ),
|
||||||
rule: __( 'Select a total spend filter match', 'wc-admin' ),
|
rule: __( 'Select a total spend filter match', 'wc-admin' ),
|
||||||
title: __( 'Total Spend {{rule /}} {{filter /}}', 'wc-admin' ),
|
title: __( '{{title}}Total Spend{{/title}} {{rule /}} {{filter /}}', 'wc-admin' ),
|
||||||
},
|
},
|
||||||
rules: [
|
rules: [
|
||||||
{
|
{
|
||||||
|
@ -224,7 +224,7 @@ export const advancedFilters = {
|
||||||
add: __( 'AOV', 'wc-admin' ),
|
add: __( 'AOV', 'wc-admin' ),
|
||||||
remove: __( 'Remove average older value filter', 'wc-admin' ),
|
remove: __( 'Remove average older value filter', 'wc-admin' ),
|
||||||
rule: __( 'Select an average order value filter match', 'wc-admin' ),
|
rule: __( 'Select an average order value filter match', 'wc-admin' ),
|
||||||
title: __( 'AOV {{rule /}} {{filter /}}', 'wc-admin' ),
|
title: __( '{{title}}AOV{{/title}} {{rule /}} {{filter /}}', 'wc-admin' ),
|
||||||
},
|
},
|
||||||
rules: [
|
rules: [
|
||||||
{
|
{
|
||||||
|
@ -254,7 +254,7 @@ export const advancedFilters = {
|
||||||
remove: __( 'Remove registered filter', 'wc-admin' ),
|
remove: __( 'Remove registered filter', 'wc-admin' ),
|
||||||
rule: __( 'Select a registered filter match', 'wc-admin' ),
|
rule: __( 'Select a registered filter match', 'wc-admin' ),
|
||||||
/* translators: A sentence describing a Product filter. See screen shot for context: https://cloudup.com/cCsm3GeXJbE */
|
/* translators: A sentence describing a Product filter. See screen shot for context: https://cloudup.com/cCsm3GeXJbE */
|
||||||
title: __( 'Registered {{rule /}} {{filter /}}', 'wc-admin' ),
|
title: __( '{{title}}Registered{{/title}} {{rule /}} {{filter /}}', 'wc-admin' ),
|
||||||
filter: __( 'Select registered date', 'wc-admin' ),
|
filter: __( 'Select registered date', 'wc-admin' ),
|
||||||
},
|
},
|
||||||
rules: [
|
rules: [
|
||||||
|
@ -284,7 +284,7 @@ export const advancedFilters = {
|
||||||
remove: __( 'Remove last active filter', 'wc-admin' ),
|
remove: __( 'Remove last active filter', 'wc-admin' ),
|
||||||
rule: __( 'Select a last active filter match', '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 */
|
/* translators: A sentence describing a Product filter. See screen shot for context: https://cloudup.com/cCsm3GeXJbE */
|
||||||
title: __( 'Last active {{rule /}} {{filter /}}', 'wc-admin' ),
|
title: __( '{{title}}Last active{{/title}} {{rule /}} {{filter /}}', 'wc-admin' ),
|
||||||
filter: __( 'Select registered date', 'wc-admin' ),
|
filter: __( 'Select registered date', 'wc-admin' ),
|
||||||
},
|
},
|
||||||
rules: [
|
rules: [
|
||||||
|
|
|
@ -45,7 +45,7 @@ export const advancedFilters = {
|
||||||
remove: __( 'Remove product filter', 'wc-admin' ),
|
remove: __( 'Remove product filter', 'wc-admin' ),
|
||||||
rule: __( 'Select a product filter match', 'wc-admin' ),
|
rule: __( 'Select a product filter match', 'wc-admin' ),
|
||||||
/* translators: A sentence describing a Product filter. See screen shot for context: https://cloudup.com/ccxhyH2mEDg */
|
/* translators: A sentence describing a Product filter. See screen shot for context: https://cloudup.com/ccxhyH2mEDg */
|
||||||
title: __( 'Product {{rule /}} {{filter /}}', 'wc-admin' ),
|
title: __( '{{title}}Product{{/title}} {{rule /}} {{filter /}}', 'wc-admin' ),
|
||||||
filter: __( 'Select product', 'wc-admin' ),
|
filter: __( 'Select product', 'wc-admin' ),
|
||||||
},
|
},
|
||||||
rules: [
|
rules: [
|
||||||
|
@ -73,7 +73,7 @@ export const advancedFilters = {
|
||||||
remove: __( 'Remove customer username filter', 'wc-admin' ),
|
remove: __( 'Remove customer username filter', 'wc-admin' ),
|
||||||
rule: __( 'Select a customer username filter match', 'wc-admin' ),
|
rule: __( 'Select a customer username filter match', 'wc-admin' ),
|
||||||
/* translators: A sentence describing a customer username filter. See screen shot for context: https://cloudup.com/ccxhyH2mEDg */
|
/* translators: A sentence describing a customer username filter. See screen shot for context: https://cloudup.com/ccxhyH2mEDg */
|
||||||
title: __( 'Username {{rule /}} {{filter /}}', 'wc-admin' ),
|
title: __( '{{title}}Username{{/title}} {{rule /}} {{filter /}}', 'wc-admin' ),
|
||||||
filter: __( 'Select customer username', 'wc-admin' ),
|
filter: __( 'Select customer username', 'wc-admin' ),
|
||||||
},
|
},
|
||||||
rules: [
|
rules: [
|
||||||
|
@ -101,7 +101,7 @@ export const advancedFilters = {
|
||||||
remove: __( 'Remove order number filter', 'wc-admin' ),
|
remove: __( 'Remove order number filter', 'wc-admin' ),
|
||||||
rule: __( 'Select a order number filter match', 'wc-admin' ),
|
rule: __( 'Select a order number filter match', 'wc-admin' ),
|
||||||
/* translators: A sentence describing a order number filter. See screen shot for context: https://cloudup.com/ccxhyH2mEDg */
|
/* translators: A sentence describing a order number filter. See screen shot for context: https://cloudup.com/ccxhyH2mEDg */
|
||||||
title: __( 'Order number {{rule /}} {{filter /}}', 'wc-admin' ),
|
title: __( '{{title}}Order number{{/title}} {{rule /}} {{filter /}}', 'wc-admin' ),
|
||||||
filter: __( 'Select order number', 'wc-admin' ),
|
filter: __( 'Select order number', 'wc-admin' ),
|
||||||
},
|
},
|
||||||
rules: [
|
rules: [
|
||||||
|
@ -135,7 +135,7 @@ export const advancedFilters = {
|
||||||
remove: __( 'Remove IP address filter', 'wc-admin' ),
|
remove: __( 'Remove IP address filter', 'wc-admin' ),
|
||||||
rule: __( 'Select an IP address filter match', 'wc-admin' ),
|
rule: __( 'Select an IP address filter match', 'wc-admin' ),
|
||||||
/* translators: A sentence describing a order number filter. See screen shot for context: https://cloudup.com/ccxhyH2mEDg */
|
/* translators: A sentence describing a order number filter. See screen shot for context: https://cloudup.com/ccxhyH2mEDg */
|
||||||
title: __( 'IP Address {{rule /}} {{filter /}}', 'wc-admin' ),
|
title: __( '{{title}}IP Address{{/title}} {{rule /}} {{filter /}}', 'wc-admin' ),
|
||||||
filter: __( 'Select IP address', 'wc-admin' ),
|
filter: __( 'Select IP address', 'wc-admin' ),
|
||||||
},
|
},
|
||||||
rules: [
|
rules: [
|
||||||
|
|
|
@ -35,7 +35,7 @@ import withSelect from 'wc-api/with-select';
|
||||||
const REPORTS_FILTER = 'woocommerce-reports-list';
|
const REPORTS_FILTER = 'woocommerce-reports-list';
|
||||||
|
|
||||||
const getReports = () => {
|
const getReports = () => {
|
||||||
const reports = applyFilters( REPORTS_FILTER, [
|
const reports = [
|
||||||
{
|
{
|
||||||
report: 'revenue',
|
report: 'revenue',
|
||||||
title: __( 'Revenue', 'wc-admin' ),
|
title: __( 'Revenue', 'wc-admin' ),
|
||||||
|
@ -86,9 +86,9 @@ const getReports = () => {
|
||||||
title: __( 'Downloads', 'wc-admin' ),
|
title: __( 'Downloads', 'wc-admin' ),
|
||||||
component: DownloadsReport,
|
component: DownloadsReport,
|
||||||
},
|
},
|
||||||
] );
|
];
|
||||||
|
|
||||||
return reports;
|
return applyFilters( REPORTS_FILTER, reports );
|
||||||
};
|
};
|
||||||
|
|
||||||
class Report extends Component {
|
class Report extends Component {
|
||||||
|
|
|
@ -20,6 +20,8 @@ export const charts = [
|
||||||
{
|
{
|
||||||
key: 'net_revenue',
|
key: 'net_revenue',
|
||||||
label: __( 'Net Revenue', 'wc-admin' ),
|
label: __( 'Net Revenue', 'wc-admin' ),
|
||||||
|
order: 'desc',
|
||||||
|
orderby: 'net_total',
|
||||||
type: 'currency',
|
type: 'currency',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -30,6 +32,8 @@ export const charts = [
|
||||||
{
|
{
|
||||||
key: 'avg_items_per_order',
|
key: 'avg_items_per_order',
|
||||||
label: __( 'Average Items Per Order', 'wc-admin' ),
|
label: __( 'Average Items Per Order', 'wc-admin' ),
|
||||||
|
order: 'desc',
|
||||||
|
orderby: 'num_items_sold',
|
||||||
type: 'average',
|
type: 'average',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
@ -61,7 +65,7 @@ export const advancedFilters = {
|
||||||
remove: __( 'Remove order status filter', 'wc-admin' ),
|
remove: __( 'Remove order status filter', 'wc-admin' ),
|
||||||
rule: __( 'Select an order status filter match', 'wc-admin' ),
|
rule: __( 'Select an order status filter match', 'wc-admin' ),
|
||||||
/* translators: A sentence describing an Order Status filter. See screen shot for context: https://cloudup.com/cSsUY9VeCVJ */
|
/* translators: A sentence describing an Order Status filter. See screen shot for context: https://cloudup.com/cSsUY9VeCVJ */
|
||||||
title: __( 'Order Status {{rule /}} {{filter /}}', 'wc-admin' ),
|
title: __( '{{title}}Order Status{{/title}} {{rule /}} {{filter /}}', 'wc-admin' ),
|
||||||
filter: __( 'Select an order status', 'wc-admin' ),
|
filter: __( 'Select an order status', 'wc-admin' ),
|
||||||
},
|
},
|
||||||
rules: [
|
rules: [
|
||||||
|
@ -91,7 +95,7 @@ export const advancedFilters = {
|
||||||
remove: __( 'Remove products filter', 'wc-admin' ),
|
remove: __( 'Remove products filter', 'wc-admin' ),
|
||||||
rule: __( 'Select a product filter match', 'wc-admin' ),
|
rule: __( 'Select a product filter match', 'wc-admin' ),
|
||||||
/* translators: A sentence describing a Product filter. See screen shot for context: https://cloudup.com/cSsUY9VeCVJ */
|
/* translators: A sentence describing a Product filter. See screen shot for context: https://cloudup.com/cSsUY9VeCVJ */
|
||||||
title: __( 'Product {{rule /}} {{filter /}}', 'wc-admin' ),
|
title: __( '{{title}}Product{{/title}} {{rule /}} {{filter /}}', 'wc-admin' ),
|
||||||
filter: __( 'Select products', 'wc-admin' ),
|
filter: __( 'Select products', 'wc-admin' ),
|
||||||
},
|
},
|
||||||
rules: [
|
rules: [
|
||||||
|
@ -119,7 +123,7 @@ export const advancedFilters = {
|
||||||
remove: __( 'Remove coupon filter', 'wc-admin' ),
|
remove: __( 'Remove coupon filter', 'wc-admin' ),
|
||||||
rule: __( 'Select a coupon filter match', 'wc-admin' ),
|
rule: __( 'Select a coupon filter match', 'wc-admin' ),
|
||||||
/* translators: A sentence describing a Coupon filter. See screen shot for context: https://cloudup.com/cSsUY9VeCVJ */
|
/* translators: A sentence describing a Coupon filter. See screen shot for context: https://cloudup.com/cSsUY9VeCVJ */
|
||||||
title: __( 'Coupon Code {{rule /}} {{filter /}}', 'wc-admin' ),
|
title: __( '{{title}}Coupon Code{{/title}} {{rule /}} {{filter /}}', 'wc-admin' ),
|
||||||
filter: __( 'Select coupon codes', 'wc-admin' ),
|
filter: __( 'Select coupon codes', 'wc-admin' ),
|
||||||
},
|
},
|
||||||
rules: [
|
rules: [
|
||||||
|
@ -146,7 +150,7 @@ export const advancedFilters = {
|
||||||
remove: __( 'Remove customer filter', 'wc-admin' ),
|
remove: __( 'Remove customer filter', 'wc-admin' ),
|
||||||
rule: __( 'Select a customer filter match', 'wc-admin' ),
|
rule: __( 'Select a customer filter match', 'wc-admin' ),
|
||||||
/* translators: A sentence describing a Customer filter. See screen shot for context: https://cloudup.com/cSsUY9VeCVJ */
|
/* translators: A sentence describing a Customer filter. See screen shot for context: https://cloudup.com/cSsUY9VeCVJ */
|
||||||
title: __( 'Customer is {{filter /}}', 'wc-admin' ),
|
title: __( '{{title}}Customer is{{/title}} {{filter /}}', 'wc-admin' ),
|
||||||
filter: __( 'Select a customer type', 'wc-admin' ),
|
filter: __( 'Select a customer type', 'wc-admin' ),
|
||||||
},
|
},
|
||||||
input: {
|
input: {
|
||||||
|
|
|
@ -45,7 +45,6 @@ export default class OrdersReportTable extends Component {
|
||||||
screenReaderLabel: __( 'Order ID', 'wc-admin' ),
|
screenReaderLabel: __( 'Order ID', 'wc-admin' ),
|
||||||
key: 'id',
|
key: 'id',
|
||||||
required: true,
|
required: true,
|
||||||
isSortable: true,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: __( 'Status', 'wc-admin' ),
|
label: __( 'Status', 'wc-admin' ),
|
||||||
|
@ -68,9 +67,9 @@ export default class OrdersReportTable extends Component {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: __( 'Items Sold', 'wc-admin' ),
|
label: __( 'Items Sold', 'wc-admin' ),
|
||||||
key: 'items_sold',
|
key: 'num_items_sold',
|
||||||
required: false,
|
required: false,
|
||||||
isSortable: false,
|
isSortable: true,
|
||||||
isNumeric: true,
|
isNumeric: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -83,9 +82,9 @@ export default class OrdersReportTable extends Component {
|
||||||
{
|
{
|
||||||
label: __( 'N. Revenue', 'wc-admin' ),
|
label: __( 'N. Revenue', 'wc-admin' ),
|
||||||
screenReaderLabel: __( 'Net Revenue', 'wc-admin' ),
|
screenReaderLabel: __( 'Net Revenue', 'wc-admin' ),
|
||||||
key: 'net_revenue',
|
key: 'net_total',
|
||||||
required: true,
|
required: true,
|
||||||
isSortable: false,
|
isSortable: true,
|
||||||
isNumeric: true,
|
isNumeric: true,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
|
@ -13,16 +13,22 @@ export const charts = [
|
||||||
{
|
{
|
||||||
key: 'items_sold',
|
key: 'items_sold',
|
||||||
label: __( 'Items Sold', 'wc-admin' ),
|
label: __( 'Items Sold', 'wc-admin' ),
|
||||||
|
order: 'desc',
|
||||||
|
orderby: 'items_sold',
|
||||||
type: 'number',
|
type: 'number',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'net_revenue',
|
key: 'net_revenue',
|
||||||
label: __( 'Net Revenue', 'wc-admin' ),
|
label: __( 'Net Revenue', 'wc-admin' ),
|
||||||
|
order: 'desc',
|
||||||
|
orderby: 'net_revenue',
|
||||||
type: 'currency',
|
type: 'currency',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'orders_count',
|
key: 'orders_count',
|
||||||
label: __( 'Orders Count', 'wc-admin' ),
|
label: __( 'Orders Count', 'wc-admin' ),
|
||||||
|
order: 'desc',
|
||||||
|
orderby: 'orders_count',
|
||||||
type: 'number',
|
type: 'number',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
|
@ -41,8 +41,8 @@ class ProductsReport extends Component {
|
||||||
isSingleProductView && isSingleProductVariable ? 'variations' : 'products';
|
isSingleProductView && isSingleProductVariable ? 'variations' : 'products';
|
||||||
const label =
|
const label =
|
||||||
isSingleProductView && isSingleProductVariable
|
isSingleProductView && isSingleProductVariable
|
||||||
? __( '%s variations', 'wc-admin' )
|
? __( '%d variations', 'wc-admin' )
|
||||||
: __( '%s products', 'wc-admin' );
|
: __( '%d products', 'wc-admin' );
|
||||||
|
|
||||||
return {
|
return {
|
||||||
compareObject,
|
compareObject,
|
||||||
|
|
|
@ -8,31 +8,41 @@ export const charts = [
|
||||||
{
|
{
|
||||||
key: 'gross_revenue',
|
key: 'gross_revenue',
|
||||||
label: __( 'Gross Revenue', 'wc-admin' ),
|
label: __( 'Gross Revenue', 'wc-admin' ),
|
||||||
|
order: 'desc',
|
||||||
|
orderby: 'gross_revenue',
|
||||||
type: 'currency',
|
type: 'currency',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'refunds',
|
key: 'refunds',
|
||||||
label: __( 'Refunds', 'wc-admin' ),
|
label: __( 'Refunds', 'wc-admin' ),
|
||||||
|
order: 'desc',
|
||||||
|
orderby: 'refunds',
|
||||||
type: 'currency',
|
type: 'currency',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'coupons',
|
key: 'coupons',
|
||||||
label: __( 'Coupons', 'wc-admin' ),
|
label: __( 'Coupons', 'wc-admin' ),
|
||||||
|
order: 'desc',
|
||||||
|
orderby: 'coupons',
|
||||||
type: 'currency',
|
type: 'currency',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'taxes',
|
key: 'taxes',
|
||||||
label: __( 'Taxes', 'wc-admin' ),
|
label: __( 'Taxes', 'wc-admin' ),
|
||||||
|
order: 'desc',
|
||||||
|
orderby: 'taxes',
|
||||||
type: 'currency',
|
type: 'currency',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'shipping',
|
key: 'shipping',
|
||||||
label: __( 'Shipping', 'wc-admin' ),
|
label: __( 'Shipping', 'wc-admin' ),
|
||||||
|
orderby: 'shipping',
|
||||||
type: 'currency',
|
type: 'currency',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'net_revenue',
|
key: 'net_revenue',
|
||||||
label: __( 'Net Revenue', 'wc-admin' ),
|
label: __( 'Net Revenue', 'wc-admin' ),
|
||||||
|
orderby: 'net_revenue',
|
||||||
type: 'currency',
|
type: 'currency',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
|
@ -240,8 +240,8 @@ export default compose(
|
||||||
return {
|
return {
|
||||||
tableData: {
|
tableData: {
|
||||||
items: {
|
items: {
|
||||||
data: get( revenueData, [ 'data', 'intervals' ] ),
|
data: get( revenueData, [ 'data', 'intervals' ], [] ),
|
||||||
totalResults: get( revenueData, [ 'totalResults' ] ),
|
totalResults: get( revenueData, [ 'totalResults' ], 0 ),
|
||||||
},
|
},
|
||||||
isError,
|
isError,
|
||||||
isRequesting,
|
isRequesting,
|
||||||
|
|
|
@ -17,6 +17,7 @@ export const filters = [
|
||||||
{ label: __( 'Out of Stock', 'wc-admin' ), value: 'outofstock' },
|
{ label: __( 'Out of Stock', 'wc-admin' ), value: 'outofstock' },
|
||||||
{ label: __( 'Low Stock', 'wc-admin' ), value: 'lowstock' },
|
{ label: __( 'Low Stock', 'wc-admin' ), value: 'lowstock' },
|
||||||
{ label: __( 'In Stock', 'wc-admin' ), value: 'instock' },
|
{ label: __( 'In Stock', 'wc-admin' ), value: 'instock' },
|
||||||
|
{ label: __( 'On Backorder', 'wc-admin' ), value: 'onbackorder' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
|
@ -76,7 +76,7 @@ export default class StockReportTable extends Component {
|
||||||
);
|
);
|
||||||
|
|
||||||
const stockStatusLink = (
|
const stockStatusLink = (
|
||||||
<Link href={ 'post.php?action=edit&post=' + parent_id || id } type="wp-admin">
|
<Link href={ 'post.php?action=edit&post=' + ( parent_id || id ) } type="wp-admin">
|
||||||
{ stockStatuses[ stock_status ] }
|
{ stockStatuses[ stock_status ] }
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
|
@ -103,23 +103,27 @@ export default class StockReportTable extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
getSummary( totals ) {
|
getSummary( totals ) {
|
||||||
const { products = 0, out_of_stock = 0, low_stock = 0, in_stock = 0 } = totals;
|
const { products = 0, outofstock = 0, lowstock = 0, instock = 0, onbackorder = 0 } = totals;
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
label: _n( 'product', 'products', products, 'wc-admin' ),
|
label: _n( 'product', 'products', products, 'wc-admin' ),
|
||||||
value: numberFormat( products ),
|
value: numberFormat( products ),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: __( 'out of stock', out_of_stock, 'wc-admin' ),
|
label: __( 'out of stock', outofstock, 'wc-admin' ),
|
||||||
value: numberFormat( out_of_stock ),
|
value: numberFormat( outofstock ),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: __( 'low stock', low_stock, 'wc-admin' ),
|
label: __( 'low stock', lowstock, 'wc-admin' ),
|
||||||
value: numberFormat( low_stock ),
|
value: numberFormat( lowstock ),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: __( 'in stock', in_stock, 'wc-admin' ),
|
label: __( 'on backorder', onbackorder, 'wc-admin' ),
|
||||||
value: numberFormat( in_stock ),
|
value: numberFormat( onbackorder ),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: __( 'in stock', instock, 'wc-admin' ),
|
||||||
|
value: numberFormat( instock ),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@ -132,7 +136,7 @@ export default class StockReportTable extends Component {
|
||||||
endpoint="stock"
|
endpoint="stock"
|
||||||
getHeadersContent={ this.getHeadersContent }
|
getHeadersContent={ this.getHeadersContent }
|
||||||
getRowsContent={ this.getRowsContent }
|
getRowsContent={ this.getRowsContent }
|
||||||
// getSummary={ this.getSummary }
|
getSummary={ this.getSummary }
|
||||||
query={ query }
|
query={ query }
|
||||||
tableQuery={ {
|
tableQuery={ {
|
||||||
orderby: query.orderby || 'stock_status',
|
orderby: query.orderby || 'stock_status',
|
||||||
|
|
|
@ -15,21 +15,29 @@ export const charts = [
|
||||||
{
|
{
|
||||||
key: 'total_tax',
|
key: 'total_tax',
|
||||||
label: __( 'Total Tax', 'wc-admin' ),
|
label: __( 'Total Tax', 'wc-admin' ),
|
||||||
|
order: 'desc',
|
||||||
|
orderby: 'total_tax',
|
||||||
type: 'currency',
|
type: 'currency',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'order_tax',
|
key: 'order_tax',
|
||||||
label: __( 'Order Tax', 'wc-admin' ),
|
label: __( 'Order Tax', 'wc-admin' ),
|
||||||
|
order: 'desc',
|
||||||
|
orderby: 'order_tax',
|
||||||
type: 'currency',
|
type: 'currency',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'shipping_tax',
|
key: 'shipping_tax',
|
||||||
label: __( 'Shipping Tax', 'wc-admin' ),
|
label: __( 'Shipping Tax', 'wc-admin' ),
|
||||||
|
order: 'desc',
|
||||||
|
orderby: 'shipping_tax',
|
||||||
type: 'currency',
|
type: 'currency',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'orders_count',
|
key: 'orders_count',
|
||||||
label: __( 'Orders Count', 'wc-admin' ),
|
label: __( 'Orders Count', 'wc-admin' ),
|
||||||
|
order: 'desc',
|
||||||
|
orderby: 'orders_count',
|
||||||
type: 'number',
|
type: 'number',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
|
@ -21,6 +21,7 @@ import './index.scss';
|
||||||
import { analyticsSettings } from './config';
|
import { analyticsSettings } from './config';
|
||||||
import Header from 'header';
|
import Header from 'header';
|
||||||
import Setting from './setting';
|
import Setting from './setting';
|
||||||
|
import withSelect from 'wc-api/with-select';
|
||||||
|
|
||||||
const SETTINGS_FILTER = 'woocommerce_admin_analytics_settings';
|
const SETTINGS_FILTER = 'woocommerce_admin_analytics_settings';
|
||||||
|
|
||||||
|
@ -33,6 +34,7 @@ class Settings extends Component {
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
settings: settings,
|
settings: settings,
|
||||||
|
saving: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.handleInputChange = this.handleInputChange.bind( this );
|
this.handleInputChange = this.handleInputChange.bind( this );
|
||||||
|
@ -59,9 +61,31 @@ class Settings extends Component {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
componentDidUpdate() {
|
||||||
|
const { addNotice, isError, isRequesting } = this.props;
|
||||||
|
const { saving } = this.state;
|
||||||
|
|
||||||
|
if ( saving && ! isRequesting ) {
|
||||||
|
if ( ! isError ) {
|
||||||
|
addNotice( {
|
||||||
|
status: 'success',
|
||||||
|
message: __( 'Your settings have been successfully saved.', 'wc-admin' ),
|
||||||
|
} );
|
||||||
|
} else {
|
||||||
|
addNotice( {
|
||||||
|
status: 'error',
|
||||||
|
message: __( 'There was an error saving your settings. Please try again.', 'wc-admin' ),
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
/* eslint-disable react/no-did-update-set-state */
|
||||||
|
this.setState( { saving: false } );
|
||||||
|
/* eslint-enable react/no-did-update-set-state */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
saveChanges = () => {
|
saveChanges = () => {
|
||||||
this.props.updateSettings( this.state.settings );
|
this.props.updateSettings( this.state.settings );
|
||||||
// @todo Need a confirmation on successful update.
|
this.setState( { saving: true } );
|
||||||
};
|
};
|
||||||
|
|
||||||
handleInputChange( e ) {
|
handleInputChange( e ) {
|
||||||
|
@ -124,10 +148,21 @@ class Settings extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default compose(
|
export default compose(
|
||||||
|
withSelect( select => {
|
||||||
|
const { getSettings, getSettingsError, isGetSettingsRequesting } = select( 'wc-api' );
|
||||||
|
|
||||||
|
const settings = getSettings();
|
||||||
|
const isError = Boolean( getSettingsError() );
|
||||||
|
const isRequesting = isGetSettingsRequesting();
|
||||||
|
|
||||||
|
return { getSettings, isError, isRequesting, settings };
|
||||||
|
} ),
|
||||||
withDispatch( dispatch => {
|
withDispatch( dispatch => {
|
||||||
|
const { addNotice } = dispatch( 'wc-admin' );
|
||||||
const { updateSettings } = dispatch( 'wc-api' );
|
const { updateSettings } = dispatch( 'wc-api' );
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
addNotice,
|
||||||
updateSettings,
|
updateSettings,
|
||||||
};
|
};
|
||||||
} )
|
} )
|
||||||
|
|
|
@ -10,6 +10,7 @@ import { Provider as SlotFillProvider } from 'react-slot-fill';
|
||||||
*/
|
*/
|
||||||
import './stylesheets/_embedded.scss';
|
import './stylesheets/_embedded.scss';
|
||||||
import { EmbedLayout } from './layout';
|
import { EmbedLayout } from './layout';
|
||||||
|
import 'store';
|
||||||
import 'wc-api/wp-data-store';
|
import 'wc-api/wp-data-store';
|
||||||
|
|
||||||
render(
|
render(
|
||||||
|
|
|
@ -12,7 +12,7 @@ import PropTypes from 'prop-types';
|
||||||
/**
|
/**
|
||||||
* WooCommerce dependencies
|
* WooCommerce dependencies
|
||||||
*/
|
*/
|
||||||
import { getNewPath, getPersistedQuery } from '@woocommerce/navigation';
|
import { getNewPath } from '@woocommerce/navigation';
|
||||||
import { Link } from '@woocommerce/components';
|
import { Link } from '@woocommerce/components';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -89,7 +89,7 @@ class Header extends Component {
|
||||||
{ _sections.map( ( section, i ) => {
|
{ _sections.map( ( section, i ) => {
|
||||||
const sectionPiece = Array.isArray( section ) ? (
|
const sectionPiece = Array.isArray( section ) ? (
|
||||||
<Link
|
<Link
|
||||||
href={ getNewPath( getPersistedQuery(), section[ 0 ], {} ) }
|
href={ getNewPath( {}, section[ 0 ], {} ) }
|
||||||
type={ isEmbedded ? 'wp-admin' : 'wc-admin' }
|
type={ isEmbedded ? 'wp-admin' : 'wc-admin' }
|
||||||
>
|
>
|
||||||
{ section[ 1 ] }
|
{ section[ 1 ] }
|
||||||
|
|
|
@ -10,6 +10,7 @@ import { Provider as SlotFillProvider } from 'react-slot-fill';
|
||||||
*/
|
*/
|
||||||
import './stylesheets/_index.scss';
|
import './stylesheets/_index.scss';
|
||||||
import { PageLayout } from './layout';
|
import { PageLayout } from './layout';
|
||||||
|
import 'store';
|
||||||
import 'wc-api/wp-data-store';
|
import 'wc-api/wp-data-store';
|
||||||
|
|
||||||
render(
|
render(
|
||||||
|
|
|
@ -21,44 +21,52 @@ import Dashboard from 'dashboard';
|
||||||
import DevDocs from 'devdocs';
|
import DevDocs from 'devdocs';
|
||||||
|
|
||||||
const getPages = () => {
|
const getPages = () => {
|
||||||
const pages = [
|
const pages = [];
|
||||||
{
|
|
||||||
container: Dashboard,
|
if ( window.wcAdminFeatures.devdocs ) {
|
||||||
path: '/',
|
pages.push( {
|
||||||
wpOpenMenu: 'toplevel_page_woocommerce',
|
|
||||||
wpClosedMenu: 'toplevel_page_wc-admin--analytics-revenue',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
container: Analytics,
|
|
||||||
path: '/analytics',
|
|
||||||
wpOpenMenu: 'toplevel_page_wc-admin--analytics-revenue',
|
|
||||||
wpClosedMenu: 'toplevel_page_woocommerce',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
container: AnalyticsSettings,
|
|
||||||
path: '/analytics/settings',
|
|
||||||
wpOpenMenu: 'toplevel_page_wc-admin--analytics-revenue',
|
|
||||||
wpClosedMenu: 'toplevel_page_woocommerce',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
container: AnalyticsReport,
|
|
||||||
path: '/analytics/:report',
|
|
||||||
wpOpenMenu: 'toplevel_page_wc-admin--analytics-revenue',
|
|
||||||
wpClosedMenu: 'toplevel_page_woocommerce',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
container: DevDocs,
|
container: DevDocs,
|
||||||
path: '/devdocs',
|
path: '/devdocs',
|
||||||
wpOpenMenu: 'toplevel_page_woocommerce',
|
wpOpenMenu: 'toplevel_page_woocommerce',
|
||||||
wpClosedMenu: 'toplevel_page_wc-admin--analytics-revenue',
|
wpClosedMenu: 'toplevel_page_wc-admin--analytics-revenue',
|
||||||
},
|
} );
|
||||||
{
|
pages.push( {
|
||||||
container: DevDocs,
|
container: DevDocs,
|
||||||
path: '/devdocs/:component',
|
path: '/devdocs/:component',
|
||||||
wpOpenMenu: 'toplevel_page_woocommerce',
|
wpOpenMenu: 'toplevel_page_woocommerce',
|
||||||
wpClosedMenu: 'toplevel_page_wc-admin--analytics-revenue',
|
wpClosedMenu: 'toplevel_page_wc-admin--analytics-revenue',
|
||||||
},
|
} );
|
||||||
];
|
}
|
||||||
|
|
||||||
|
if ( window.wcAdminFeatures.dashboard ) {
|
||||||
|
pages.push( {
|
||||||
|
container: Dashboard,
|
||||||
|
path: '/',
|
||||||
|
wpOpenMenu: 'toplevel_page_woocommerce',
|
||||||
|
wpClosedMenu: 'toplevel_page_wc-admin--analytics-revenue',
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( window.wcAdminFeatures.analytics ) {
|
||||||
|
pages.push( {
|
||||||
|
container: Analytics,
|
||||||
|
path: '/analytics',
|
||||||
|
wpOpenMenu: 'toplevel_page_wc-admin--analytics-revenue',
|
||||||
|
wpClosedMenu: 'toplevel_page_woocommerce',
|
||||||
|
} );
|
||||||
|
pages.push( {
|
||||||
|
container: AnalyticsSettings,
|
||||||
|
path: '/analytics/settings',
|
||||||
|
wpOpenMenu: 'toplevel_page_wc-admin--analytics-revenue',
|
||||||
|
wpClosedMenu: 'toplevel_page_woocommerce',
|
||||||
|
} );
|
||||||
|
pages.push( {
|
||||||
|
container: AnalyticsReport,
|
||||||
|
path: '/analytics/:report',
|
||||||
|
wpOpenMenu: 'toplevel_page_wc-admin--analytics-revenue',
|
||||||
|
wpClosedMenu: 'toplevel_page_woocommerce',
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
|
||||||
return pages;
|
return pages;
|
||||||
};
|
};
|
||||||
|
|
|
@ -21,6 +21,7 @@ import { Controller, getPages } from './controller';
|
||||||
import Header from 'header';
|
import Header from 'header';
|
||||||
import Notices from './notices';
|
import Notices from './notices';
|
||||||
import { recordPageView } from 'lib/tracks';
|
import { recordPageView } from 'lib/tracks';
|
||||||
|
import TransientNotices from './transient-notices';
|
||||||
|
|
||||||
class Layout extends Component {
|
class Layout extends Component {
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
|
@ -61,7 +62,8 @@ class Layout extends Component {
|
||||||
const { isEmbeded, ...restProps } = this.props;
|
const { isEmbeded, ...restProps } = this.props;
|
||||||
return (
|
return (
|
||||||
<div className="woocommerce-layout">
|
<div className="woocommerce-layout">
|
||||||
<Slot name="header" />
|
{ window.wcAdminFeatures[ 'activity-panels' ] && <Slot name="header" /> }
|
||||||
|
<TransientNotices />
|
||||||
|
|
||||||
<div className="woocommerce-layout__primary" id="woocommerce-layout__primary">
|
<div className="woocommerce-layout__primary" id="woocommerce-layout__primary">
|
||||||
<Notices />
|
<Notices />
|
||||||
|
|
|
@ -17,6 +17,10 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.woocommerce-feature-disabled-activity-panels .woocommerce-layout__primary {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
.woocommerce-layout .woocommerce-layout__main {
|
.woocommerce-layout .woocommerce-layout__main {
|
||||||
padding-right: $fallback-gutter-large;
|
padding-right: $fallback-gutter-large;
|
||||||
padding-right: $gutter-large;
|
padding-right: $gutter-large;
|
||||||
|
|
|
@ -0,0 +1,48 @@
|
||||||
|
/** @format */
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import classnames from 'classnames';
|
||||||
|
import { Component } from '@wordpress/element';
|
||||||
|
import { compose } from '@wordpress/compose';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import './style.scss';
|
||||||
|
import TransientNotice from './transient-notice';
|
||||||
|
import withSelect from 'wc-api/with-select';
|
||||||
|
|
||||||
|
class TransientNotices extends Component {
|
||||||
|
render() {
|
||||||
|
const { className, notices } = this.props;
|
||||||
|
const classes = classnames( 'woocommerce-transient-notices', className );
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className={ classes }>
|
||||||
|
{ notices && notices.map( ( notice, i ) => <TransientNotice key={ i } { ...notice } /> ) }
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TransientNotices.propTypes = {
|
||||||
|
/**
|
||||||
|
* Additional class name to style the component.
|
||||||
|
*/
|
||||||
|
className: PropTypes.string,
|
||||||
|
/**
|
||||||
|
* Array of notices to be displayed.
|
||||||
|
*/
|
||||||
|
notices: PropTypes.array,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default compose(
|
||||||
|
withSelect( select => {
|
||||||
|
const { getNotices } = select( 'wc-admin' );
|
||||||
|
const notices = getNotices();
|
||||||
|
|
||||||
|
return { notices };
|
||||||
|
} )
|
||||||
|
)( TransientNotices );
|
|
@ -0,0 +1,38 @@
|
||||||
|
/** @format */
|
||||||
|
|
||||||
|
.woocommerce-transient-notices {
|
||||||
|
position: fixed;
|
||||||
|
bottom: $gap-small;
|
||||||
|
left: 0;
|
||||||
|
z-index: 99999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.woocommerce-transient-notice {
|
||||||
|
transform: translateX(calc(-100% - #{$gap}));
|
||||||
|
transition: all 300ms cubic-bezier(0.42, 0, 0.58, 1);
|
||||||
|
max-height: 300px; // Used to animate sliding down when multiple notices exist on exit.
|
||||||
|
|
||||||
|
@media screen and (prefers-reduced-motion: reduce) {
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.slide-enter-active,
|
||||||
|
&.slide-enter-done {
|
||||||
|
transform: translateX(0%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.slide-exit-active {
|
||||||
|
transform: translateX(calc(-100% - #{$gap}));
|
||||||
|
}
|
||||||
|
|
||||||
|
&.slide-exit-done {
|
||||||
|
max-height: 0;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.components-notice {
|
||||||
|
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.16), 0 3px 6px rgba(0, 0, 0, 0.23);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,104 @@
|
||||||
|
/** @format */
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import classnames from 'classnames';
|
||||||
|
import { Component } from '@wordpress/element';
|
||||||
|
import { CSSTransition } from 'react-transition-group';
|
||||||
|
import { noop } from 'lodash';
|
||||||
|
import { Notice } from '@wordpress/components';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { speak } from '@wordpress/a11y';
|
||||||
|
|
||||||
|
class TransientNotice extends Component {
|
||||||
|
constructor( props ) {
|
||||||
|
super( props );
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
visible: false,
|
||||||
|
timeout: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
const exitTime = this.props.exitTime;
|
||||||
|
const timeout = setTimeout(
|
||||||
|
() => {
|
||||||
|
this.setState( { visible: false } );
|
||||||
|
},
|
||||||
|
exitTime,
|
||||||
|
name,
|
||||||
|
exitTime
|
||||||
|
);
|
||||||
|
/* eslint-disable react/no-did-mount-set-state */
|
||||||
|
this.setState( { visible: true, timeout } );
|
||||||
|
/* eslint-enable react/no-did-mount-set-state */
|
||||||
|
speak( this.props.message );
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
clearTimeout( this.state.timeout );
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { actions, className, isDismissible, message, onRemove, status } = this.props;
|
||||||
|
const classes = classnames( 'woocommerce-transient-notice', className );
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CSSTransition in={ this.state.visible } timeout={ 300 } classNames="slide">
|
||||||
|
<div className={ classes }>
|
||||||
|
<Notice
|
||||||
|
status={ status }
|
||||||
|
isDismissible={ isDismissible }
|
||||||
|
onRemove={ onRemove }
|
||||||
|
actions={ actions }
|
||||||
|
>
|
||||||
|
{ message }
|
||||||
|
</Notice>
|
||||||
|
</div>
|
||||||
|
</CSSTransition>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TransientNotice.propTypes = {
|
||||||
|
/**
|
||||||
|
* Array of action objects.
|
||||||
|
* See https://wordpress.org/gutenberg/handbook/designers-developers/developers/components/notice/
|
||||||
|
*/
|
||||||
|
actions: PropTypes.array,
|
||||||
|
/**
|
||||||
|
* Additional class name to style the component.
|
||||||
|
*/
|
||||||
|
className: PropTypes.string,
|
||||||
|
/**
|
||||||
|
* Determines if the notice dimiss button should be shown.
|
||||||
|
* See https://wordpress.org/gutenberg/handbook/designers-developers/developers/components/notice/
|
||||||
|
*/
|
||||||
|
isDismissible: PropTypes.bool,
|
||||||
|
/**
|
||||||
|
* Function called when dismissing the notice.
|
||||||
|
* See https://wordpress.org/gutenberg/handbook/designers-developers/developers/components/notice/
|
||||||
|
*/
|
||||||
|
onRemove: PropTypes.func,
|
||||||
|
/**
|
||||||
|
* Type of notice to display.
|
||||||
|
* See https://wordpress.org/gutenberg/handbook/designers-developers/developers/components/notice/
|
||||||
|
*/
|
||||||
|
status: PropTypes.oneOf( [ 'success', 'error', 'warning' ] ),
|
||||||
|
/**
|
||||||
|
* Time in milliseconds until exit.
|
||||||
|
*/
|
||||||
|
exitTime: PropTypes.number,
|
||||||
|
};
|
||||||
|
|
||||||
|
TransientNotice.defaultProps = {
|
||||||
|
actions: [],
|
||||||
|
className: '',
|
||||||
|
exitTime: 7000,
|
||||||
|
isDismissible: false,
|
||||||
|
onRemove: noop,
|
||||||
|
status: 'warning',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TransientNotice;
|
|
@ -0,0 +1,22 @@
|
||||||
|
/** @format */
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { combineReducers, registerStore } from '@wordpress/data';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import notices from './notices';
|
||||||
|
|
||||||
|
registerStore( 'wc-admin', {
|
||||||
|
reducer: combineReducers( {
|
||||||
|
...notices.reducers,
|
||||||
|
} ),
|
||||||
|
actions: {
|
||||||
|
...notices.actions,
|
||||||
|
},
|
||||||
|
selectors: {
|
||||||
|
...notices.selectors,
|
||||||
|
},
|
||||||
|
} );
|
|
@ -0,0 +1,9 @@
|
||||||
|
/** @format */
|
||||||
|
|
||||||
|
const addNotice = notice => {
|
||||||
|
return { type: 'ADD_NOTICE', notice };
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
addNotice,
|
||||||
|
};
|
|
@ -0,0 +1,13 @@
|
||||||
|
/** @format */
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import reducers from './reducers';
|
||||||
|
import actions from './actions';
|
||||||
|
import selectors from './selectors';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
reducers,
|
||||||
|
actions,
|
||||||
|
selectors,
|
||||||
|
};
|
|
@ -0,0 +1,15 @@
|
||||||
|
/** @format */
|
||||||
|
|
||||||
|
const DEFAULT_STATE = [];
|
||||||
|
|
||||||
|
const notices = ( state = DEFAULT_STATE, action ) => {
|
||||||
|
if ( action.type === 'ADD_NOTICE' ) {
|
||||||
|
return [ ...state, action.notice ];
|
||||||
|
}
|
||||||
|
|
||||||
|
return state;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
notices,
|
||||||
|
};
|
|
@ -0,0 +1,9 @@
|
||||||
|
/** @format */
|
||||||
|
|
||||||
|
const getNotices = state => {
|
||||||
|
return state.notices;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
getNotices,
|
||||||
|
};
|
|
@ -0,0 +1,7 @@
|
||||||
|
/** @format */
|
||||||
|
|
||||||
|
export const DEFAULT_STATE = {
|
||||||
|
notices: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const testNotice = { message: 'Test notice' };
|
|
@ -0,0 +1,55 @@
|
||||||
|
/** @format */
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import actions from '../actions';
|
||||||
|
import { DEFAULT_STATE, testNotice } from './fixtures';
|
||||||
|
import reducers from '../reducers';
|
||||||
|
import selectors from '../selectors';
|
||||||
|
|
||||||
|
describe( 'actions', () => {
|
||||||
|
test( 'should create an add notice action', () => {
|
||||||
|
const expectedAction = {
|
||||||
|
type: 'ADD_NOTICE',
|
||||||
|
notice: testNotice,
|
||||||
|
};
|
||||||
|
|
||||||
|
expect( actions.addNotice( testNotice ) ).toEqual( expectedAction );
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
|
||||||
|
describe( 'selectors', () => {
|
||||||
|
const notices = [ testNotice ];
|
||||||
|
const updatedState = { ...DEFAULT_STATE, ...{ notices } };
|
||||||
|
|
||||||
|
test( 'should return an emtpy initial state', () => {
|
||||||
|
expect( selectors.getNotices( DEFAULT_STATE ) ).toEqual( [] );
|
||||||
|
} );
|
||||||
|
|
||||||
|
test( 'should have an array length matching number of notices', () => {
|
||||||
|
expect( selectors.getNotices( updatedState ).length ).toEqual( 1 );
|
||||||
|
} );
|
||||||
|
|
||||||
|
test( 'should return the message content', () => {
|
||||||
|
expect( selectors.getNotices( updatedState )[ 0 ].message ).toEqual( 'Test notice' );
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
|
||||||
|
describe( 'reducers', () => {
|
||||||
|
test( 'should return an emtpy initial state', () => {
|
||||||
|
expect( reducers.notices( DEFAULT_STATE.notices, {} ) ).toEqual( [] );
|
||||||
|
} );
|
||||||
|
|
||||||
|
test( 'should return the added notice', () => {
|
||||||
|
expect(
|
||||||
|
reducers.notices( DEFAULT_STATE.notices, { type: 'ADD_NOTICE', notice: testNotice } )
|
||||||
|
).toEqual( [ testNotice ] );
|
||||||
|
} );
|
||||||
|
|
||||||
|
const initialNotices = [ { message: 'Initial notice' } ];
|
||||||
|
test( 'should return the initial notice and the added notice', () => {
|
||||||
|
expect(
|
||||||
|
reducers.notices( initialNotices, { type: 'ADD_NOTICE', notice: testNotice } )
|
||||||
|
).toEqual( [ ...initialNotices, testNotice ] );
|
||||||
|
} );
|
||||||
|
} );
|
|
@ -56,11 +56,13 @@
|
||||||
|
|
||||||
.components-button.is-button.is-primary {
|
.components-button.is-button.is-primary {
|
||||||
color: $white;
|
color: $white;
|
||||||
}
|
|
||||||
|
|
||||||
.components-button.is-button.is-primary:hover,
|
&:not(:disabled) {
|
||||||
.components-button.is-button.is-primary:active,
|
&:hover,
|
||||||
.components-button.is-button.is-primary:focus {
|
&:active,
|
||||||
color: $white;
|
&:focus {
|
||||||
|
color: $white;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,12 +6,9 @@ import { MINUTE } from '@fresh-data/framework';
|
||||||
|
|
||||||
export const NAMESPACE = '/wc/v4';
|
export const NAMESPACE = '/wc/v4';
|
||||||
|
|
||||||
// @todo Remove once swagger endpoints are phased out.
|
|
||||||
export const SWAGGERNAMESPACE = 'https://virtserver.swaggerhub.com/peterfabian/wc-v3-api/1.0.0/';
|
|
||||||
|
|
||||||
export const DEFAULT_REQUIREMENT = {
|
export const DEFAULT_REQUIREMENT = {
|
||||||
timeout: 1 * MINUTE,
|
timeout: 1 * MINUTE,
|
||||||
freshness: 5 * MINUTE,
|
freshness: 30 * MINUTE,
|
||||||
};
|
};
|
||||||
|
|
||||||
// WordPress & WooCommerce both set a hard limit of 100 for the per_page parameter
|
// WordPress & WooCommerce both set a hard limit of 100 for the per_page parameter
|
||||||
|
|
|
@ -13,7 +13,7 @@ import { stringifyQuery } from '@woocommerce/navigation';
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import { getResourceIdentifier, getResourcePrefix } from 'wc-api/utils';
|
import { getResourceIdentifier, getResourcePrefix } from 'wc-api/utils';
|
||||||
import { NAMESPACE, SWAGGERNAMESPACE } from 'wc-api/constants';
|
import { NAMESPACE } from 'wc-api/constants';
|
||||||
|
|
||||||
const statEndpoints = [
|
const statEndpoints = [
|
||||||
'coupons',
|
'coupons',
|
||||||
|
@ -21,11 +21,10 @@ const statEndpoints = [
|
||||||
'orders',
|
'orders',
|
||||||
'products',
|
'products',
|
||||||
'revenue',
|
'revenue',
|
||||||
|
'stock',
|
||||||
'taxes',
|
'taxes',
|
||||||
'customers',
|
'customers',
|
||||||
];
|
];
|
||||||
// @todo Remove once swagger endpoints are phased out.
|
|
||||||
const swaggerEndpoints = [ 'categories' ];
|
|
||||||
|
|
||||||
const typeEndpointMap = {
|
const typeEndpointMap = {
|
||||||
'report-stats-query-orders': 'orders',
|
'report-stats-query-orders': 'orders',
|
||||||
|
@ -34,6 +33,7 @@ const typeEndpointMap = {
|
||||||
'report-stats-query-categories': 'categories',
|
'report-stats-query-categories': 'categories',
|
||||||
'report-stats-query-downloads': 'downloads',
|
'report-stats-query-downloads': 'downloads',
|
||||||
'report-stats-query-coupons': 'coupons',
|
'report-stats-query-coupons': 'coupons',
|
||||||
|
'report-stats-query-stock': 'stock',
|
||||||
'report-stats-query-taxes': 'taxes',
|
'report-stats-query-taxes': 'taxes',
|
||||||
'report-stats-query-customers': 'customers',
|
'report-stats-query-customers': 'customers',
|
||||||
};
|
};
|
||||||
|
@ -53,9 +53,7 @@ function read( resourceNames, fetch = apiFetch ) {
|
||||||
parse: false,
|
parse: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
if ( swaggerEndpoints.indexOf( endpoint ) >= 0 ) {
|
if ( statEndpoints.indexOf( endpoint ) >= 0 ) {
|
||||||
fetchArgs.url = SWAGGERNAMESPACE + 'reports/' + endpoint + '/stats' + stringifyQuery( query );
|
|
||||||
} else if ( statEndpoints.indexOf( endpoint ) >= 0 ) {
|
|
||||||
fetchArgs.path = NAMESPACE + '/reports/' + endpoint + '/stats' + stringifyQuery( query );
|
fetchArgs.path = NAMESPACE + '/reports/' + endpoint + '/stats' + stringifyQuery( query );
|
||||||
} else {
|
} else {
|
||||||
fetchArgs.path = endpoint + stringifyQuery( query );
|
fetchArgs.path = endpoint + stringifyQuery( query );
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
/**
|
/**
|
||||||
* External dependencies
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
import { find, forEach, isNull, get } from 'lodash';
|
import { find, forEach, isNull, get, includes } from 'lodash';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -52,6 +52,9 @@ export function getFilterQuery( endpoint, query ) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Some stats endpoints don't have interval data, so they can ignore after/before params and omit that part of the response.
|
||||||
|
const noIntervalEndpoints = [ 'stock', 'customers' ];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add timestamp to advanced filter parameters involving date. The api
|
* Add timestamp to advanced filter parameters involving date. The api
|
||||||
* expects a timestamp for these values similar to `before` and `after`.
|
* expects a timestamp for these values similar to `before` and `after`.
|
||||||
|
@ -136,10 +139,11 @@ export function getQueryFromConfig( config, advancedFilters, query ) {
|
||||||
/**
|
/**
|
||||||
* Returns true if a report object is empty.
|
* Returns true if a report object is empty.
|
||||||
*
|
*
|
||||||
* @param {Object} report Report to check
|
* @param {Object} report Report to check
|
||||||
|
* @param {String} endpoint Endpoint slug
|
||||||
* @return {Boolean} True if report is data is empty.
|
* @return {Boolean} True if report is data is empty.
|
||||||
*/
|
*/
|
||||||
export function isReportDataEmpty( report ) {
|
export function isReportDataEmpty( report, endpoint ) {
|
||||||
if ( ! report ) {
|
if ( ! report ) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -149,7 +153,9 @@ export function isReportDataEmpty( report ) {
|
||||||
if ( ! report.data.totals || isNull( report.data.totals ) ) {
|
if ( ! report.data.totals || isNull( report.data.totals ) ) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if ( ! report.data.intervals || 0 === report.data.intervals.length ) {
|
|
||||||
|
const checkIntervals = ! includes( noIntervalEndpoints, endpoint );
|
||||||
|
if ( checkIntervals && ( ! report.data.intervals || 0 === report.data.intervals.length ) ) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
@ -168,15 +174,19 @@ function getRequestQuery( endpoint, dataType, query ) {
|
||||||
const interval = getIntervalForQuery( query );
|
const interval = getIntervalForQuery( query );
|
||||||
const filterQuery = getFilterQuery( endpoint, query );
|
const filterQuery = getFilterQuery( endpoint, query );
|
||||||
const end = datesFromQuery[ dataType ].before;
|
const end = datesFromQuery[ dataType ].before;
|
||||||
return {
|
|
||||||
order: 'asc',
|
const noIntervals = includes( noIntervalEndpoints, endpoint );
|
||||||
interval,
|
return noIntervals
|
||||||
per_page: MAX_PER_PAGE,
|
? { ...filterQuery }
|
||||||
after: appendTimestamp( datesFromQuery[ dataType ].after, 'start' ),
|
: {
|
||||||
before: appendTimestamp( end, 'end' ),
|
order: 'asc',
|
||||||
segmentby: query.segmentby,
|
interval,
|
||||||
...filterQuery,
|
per_page: MAX_PER_PAGE,
|
||||||
};
|
after: appendTimestamp( datesFromQuery[ dataType ].after, 'start' ),
|
||||||
|
before: appendTimestamp( end, 'end' ),
|
||||||
|
segmentby: query.segmentby,
|
||||||
|
...filterQuery,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -250,7 +260,7 @@ export function getReportChartData( endpoint, dataType, query, select ) {
|
||||||
return { ...response, isRequesting: true };
|
return { ...response, isRequesting: true };
|
||||||
} else if ( getReportStatsError( endpoint, requestQuery ) ) {
|
} else if ( getReportStatsError( endpoint, requestQuery ) ) {
|
||||||
return { ...response, isError: true };
|
return { ...response, isError: true };
|
||||||
} else if ( isReportDataEmpty( stats ) ) {
|
} else if ( isReportDataEmpty( stats, endpoint ) ) {
|
||||||
return { ...response, isEmpty: true };
|
return { ...response, isEmpty: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -48,13 +48,13 @@ function updateSettings( resourceNames, data, fetch ) {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
data: { value: settingsData[ setting ] },
|
data: { value: settingsData[ setting ] },
|
||||||
} )
|
} )
|
||||||
.then( settingsToSettingsResource )
|
.then( settingToSettingsResource.bind( null, data.settings ) )
|
||||||
.catch( error => {
|
.catch( error => {
|
||||||
return { [ resourceName ]: { error } };
|
return { [ resourceName ]: { error } };
|
||||||
} );
|
} );
|
||||||
} );
|
} );
|
||||||
|
|
||||||
return [ promises ];
|
return promises;
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
@ -65,6 +65,11 @@ function settingsToSettingsResource( settings ) {
|
||||||
return { [ 'settings' ]: { data: settingsData } };
|
return { [ 'settings' ]: { data: settingsData } };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function settingToSettingsResource( settings, setting ) {
|
||||||
|
settings[ setting.id ] = setting.value;
|
||||||
|
return { [ 'settings' ]: { data: settings } };
|
||||||
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
read,
|
read,
|
||||||
update,
|
update,
|
||||||
|
|
|
@ -1,14 +1,34 @@
|
||||||
/** @format */
|
/** @format */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { isNil } from 'lodash';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import { DEFAULT_REQUIREMENT } from '../constants';
|
import { DEFAULT_REQUIREMENT } from '../constants';
|
||||||
|
|
||||||
const getSettings = ( getResource, requireResource ) => ( requirement = DEFAULT_REQUIREMENT ) => {
|
const getSettings = ( getResource, requireResource ) => ( requirement = DEFAULT_REQUIREMENT ) => {
|
||||||
return requireResource( requirement, 'settings' ).data;
|
return requireResource( requirement, 'settings' ).data || {};
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSettingsError = getResource => () => {
|
||||||
|
return getResource( 'settings' ).error;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isGetSettingsRequesting = getResource => () => {
|
||||||
|
const { lastRequested, lastReceived } = getResource( 'settings' );
|
||||||
|
if ( isNil( lastRequested ) || isNil( lastReceived ) ) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return lastRequested > lastReceived;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
getSettings,
|
getSettings,
|
||||||
|
getSettingsError,
|
||||||
|
isGetSettingsRequesting,
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"features": {
|
||||||
|
"activity-panels": false,
|
||||||
|
"analytics": false,
|
||||||
|
"dashboard": false,
|
||||||
|
"devdocs": false
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"features": {
|
||||||
|
"activity-panels": true,
|
||||||
|
"analytics": true,
|
||||||
|
"dashboard": true,
|
||||||
|
"devdocs": true
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"features": {
|
||||||
|
"activity-panels": false,
|
||||||
|
"analytics": true,
|
||||||
|
"dashboard": true,
|
||||||
|
"devdocs": false
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
* [Overview](/)
|
* [Overview](/)
|
||||||
* [Components](components/)
|
* [Components](components/)
|
||||||
|
* [Feature Flags](feature-flags)
|
||||||
* [Data](data)
|
* [Data](data)
|
||||||
* [Documentation](documentation)
|
* [Documentation](documentation)
|
||||||
* [Layout](layout)
|
* [Layout](layout)
|
||||||
|
|
|
@ -38,9 +38,14 @@ Current path
|
||||||
|
|
||||||
### `primaryData`
|
### `primaryData`
|
||||||
|
|
||||||
- **Required**
|
|
||||||
- Type: Object
|
- Type: Object
|
||||||
- Default: null
|
- Default: `{
|
||||||
|
data: {
|
||||||
|
intervals: [],
|
||||||
|
},
|
||||||
|
isError: false,
|
||||||
|
isRequesting: false,
|
||||||
|
}`
|
||||||
|
|
||||||
Primary data to display in the chart.
|
Primary data to display in the chart.
|
||||||
|
|
||||||
|
@ -54,9 +59,14 @@ The query string represented in object form.
|
||||||
|
|
||||||
### `secondaryData`
|
### `secondaryData`
|
||||||
|
|
||||||
- **Required**
|
|
||||||
- Type: Object
|
- Type: Object
|
||||||
- Default: null
|
- Default: `{
|
||||||
|
data: {
|
||||||
|
intervals: [],
|
||||||
|
},
|
||||||
|
isError: false,
|
||||||
|
isRequesting: false,
|
||||||
|
}`
|
||||||
|
|
||||||
Secondary data to display in the chart.
|
Secondary data to display in the chart.
|
||||||
|
|
||||||
|
|
|
@ -42,3 +42,17 @@ The query string represented in object form.
|
||||||
|
|
||||||
Properties of the selected chart.
|
Properties of the selected chart.
|
||||||
|
|
||||||
|
### `summaryData`
|
||||||
|
|
||||||
|
- Type: Object
|
||||||
|
- Default: `{
|
||||||
|
totals: {
|
||||||
|
primary: {},
|
||||||
|
secondary: {},
|
||||||
|
},
|
||||||
|
isError: false,
|
||||||
|
isRequesting: false,
|
||||||
|
}`
|
||||||
|
|
||||||
|
Data to display in the SummaryNumbers.
|
||||||
|
|
||||||
|
|
|
@ -68,9 +68,8 @@ The name of the property in the item object which contains the id.
|
||||||
|
|
||||||
### `primaryData`
|
### `primaryData`
|
||||||
|
|
||||||
- **Required**
|
|
||||||
- Type: Object
|
- Type: Object
|
||||||
- Default: null
|
- Default: `{}`
|
||||||
|
|
||||||
Primary data of that report. If it's not provided, it will be automatically
|
Primary data of that report. If it's not provided, it will be automatically
|
||||||
loaded via the provided `endpoint`.
|
loaded via the provided `endpoint`.
|
||||||
|
@ -78,7 +77,13 @@ loaded via the provided `endpoint`.
|
||||||
### `tableData`
|
### `tableData`
|
||||||
|
|
||||||
- Type: Object
|
- Type: Object
|
||||||
- Default: `{}`
|
- Default: `{
|
||||||
|
items: {
|
||||||
|
data: [],
|
||||||
|
totalResults: 0,
|
||||||
|
},
|
||||||
|
query: {},
|
||||||
|
}`
|
||||||
|
|
||||||
Table data of that report. If it's not provided, it will be automatically
|
Table data of that report. If it's not provided, it will be automatically
|
||||||
loaded via the provided `endpoint`.
|
loaded via the provided `endpoint`.
|
||||||
|
|
|
@ -13,6 +13,14 @@ Props
|
||||||
|
|
||||||
Allowed intervals to show in a dropdown.
|
Allowed intervals to show in a dropdown.
|
||||||
|
|
||||||
|
### `baseValue`
|
||||||
|
|
||||||
|
- Type: Number
|
||||||
|
- Default: `0`
|
||||||
|
|
||||||
|
Base chart value. If no data value is different than the baseValue, the
|
||||||
|
`emptyMessage` will be displayed if provided.
|
||||||
|
|
||||||
### `data`
|
### `data`
|
||||||
|
|
||||||
- Type: Array
|
- Type: Array
|
||||||
|
@ -27,6 +35,14 @@ An array of data.
|
||||||
|
|
||||||
Format to parse dates into d3 time format
|
Format to parse dates into d3 time format
|
||||||
|
|
||||||
|
### `emptyMessage`
|
||||||
|
|
||||||
|
- Type: String
|
||||||
|
- Default: null
|
||||||
|
|
||||||
|
The message to be displayed if there is no data to render. If no message is provided,
|
||||||
|
nothing will be displayed.
|
||||||
|
|
||||||
### `itemsLabel`
|
### `itemsLabel`
|
||||||
|
|
||||||
- Type: String
|
- Type: String
|
||||||
|
|
|
@ -7,6 +7,13 @@ A search box which autocompletes results while typing, allowing for the user to
|
||||||
Props
|
Props
|
||||||
-----
|
-----
|
||||||
|
|
||||||
|
### `allowFreeTextSearch`
|
||||||
|
|
||||||
|
- Type: Boolean
|
||||||
|
- Default: `false`
|
||||||
|
|
||||||
|
Render additional options in the autocompleter to allow free text entering depending on the type.
|
||||||
|
|
||||||
### `className`
|
### `className`
|
||||||
|
|
||||||
- Type: String
|
- Type: String
|
||||||
|
@ -54,6 +61,13 @@ search box.
|
||||||
|
|
||||||
Render tags inside input, otherwise render below input.
|
Render tags inside input, otherwise render below input.
|
||||||
|
|
||||||
|
### `showClearButton`
|
||||||
|
|
||||||
|
- Type: Boolean
|
||||||
|
- Default: `false`
|
||||||
|
|
||||||
|
Render a 'Clear' button next to the input box to remove its contents.
|
||||||
|
|
||||||
### `staticResults`
|
### `staticResults`
|
||||||
|
|
||||||
- Type: Boolean
|
- Type: Boolean
|
||||||
|
|
|
@ -132,13 +132,6 @@ The total number of rows to display per page.
|
||||||
|
|
||||||
The string to use as a query parameter when searching row items.
|
The string to use as a query parameter when searching row items.
|
||||||
|
|
||||||
### `searchParam`
|
|
||||||
|
|
||||||
- Type: String
|
|
||||||
- Default: null
|
|
||||||
|
|
||||||
Url query parameter search function operates on
|
|
||||||
|
|
||||||
### `showMenu`
|
### `showMenu`
|
||||||
|
|
||||||
- Type: Boolean
|
- Type: Boolean
|
||||||
|
|
|
@ -0,0 +1,47 @@
|
||||||
|
# Feature Flags
|
||||||
|
|
||||||
|
Features inside the `wc-admin` repository can be in various states of completeness. In addition to the development copy of `wc-admin`, feature plugin versions are bundled, and code is merged to WooCommerce core. To provide a way for improved control over how these features are released in these different environments, `wc-admin` has a system for feature flags.
|
||||||
|
|
||||||
|
We currently support the following environments:
|
||||||
|
|
||||||
|
| Environment | Description |
|
||||||
|
|-------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
|
| development | Development - All features should be enabled in development. These flags are also used in both JS and PHP tests. Ran using `npm start`. |
|
||||||
|
| plugin | Plugin - A packaged release of the featured plugin, for GitHub WordPress.org. Ran using `npm run-script build:release`. | |
|
||||||
|
| core | Core - assets/files ready and stable enough for core merge. @todo No build process exists yet.
|
||||||
|
|
||||||
|
|
||||||
|
## Adding a new flag
|
||||||
|
|
||||||
|
Flags can be added to the files located in the `config/` directory. Make sure to add a flag for each environment and explicitly set the flag to false.
|
||||||
|
Please add new feature flags alphabetically so they are easy to find.
|
||||||
|
|
||||||
|
## Basic Use - Client
|
||||||
|
|
||||||
|
The `window.wcAdminFeatures` constant is a global variable containing the feature flags.
|
||||||
|
|
||||||
|
Instances of `window.wcAdminFeatures` are replaced during the webpack build process by using webpack's [define plugin](https://webpack.js.org/plugins/define-plugin/). Using `webpack` for this allows us to eliminate dead code when making minified/production builds (`plugin`, or `core` environments).
|
||||||
|
|
||||||
|
To check if a feature is enabled, you can simplify check the boolean value of a feature:
|
||||||
|
|
||||||
|
```
|
||||||
|
{ window.wcAdminFeatures[ 'activity-panels' ] && <ActivityPanel /> }
|
||||||
|
```
|
||||||
|
|
||||||
|
We also expose CSS classes on the `body` tag, so that you can target specific feature states:
|
||||||
|
|
||||||
|
```
|
||||||
|
<body class="wp-admin woocommerce-page woocommerce-feature-disabled-analytics woocommerce-feature-enabled-activity-panels ....">
|
||||||
|
```
|
||||||
|
|
||||||
|
## Basic Use - Server
|
||||||
|
|
||||||
|
Feature flags are also available via PHP. To ensure these are consistent with the built client assets, `includes/feature-flags.php` is generated by the plugin build process or `npm start`. Do not edit `includes/feature-flags.php` directly.
|
||||||
|
|
||||||
|
To check if a feature is enabled, you can use the `wc_admin_is_feature_enabled()`:
|
||||||
|
|
||||||
|
```
|
||||||
|
if ( wc_admin_is_feature_enabled( 'activity-panels' ) ) {
|
||||||
|
add_action( 'admin_header', 'wc_admin_activity_panel' );
|
||||||
|
}
|
||||||
|
```
|
|
@ -13,46 +13,14 @@ defined( 'ABSPATH' ) || exit;
|
||||||
* Customers controller.
|
* Customers controller.
|
||||||
*
|
*
|
||||||
* @package WooCommerce Admin/API
|
* @package WooCommerce Admin/API
|
||||||
* @extends WC_REST_Customers_Controller
|
* @extends WC_Admin_REST_Reports_Customers_Controller
|
||||||
*/
|
*/
|
||||||
class WC_Admin_REST_Customers_Controller extends WC_REST_Customers_Controller {
|
class WC_Admin_REST_Customers_Controller extends WC_Admin_REST_Reports_Customers_Controller {
|
||||||
|
|
||||||
// @todo Add support for guests here. See https://wp.me/p7bje6-1dM.
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Endpoint namespace.
|
* Route base.
|
||||||
*
|
*
|
||||||
* @var string
|
* @var string
|
||||||
*/
|
*/
|
||||||
protected $namespace = 'wc/v4';
|
protected $rest_base = 'customers';
|
||||||
|
|
||||||
/**
|
|
||||||
* Searches emails by partial search instead of a strict match.
|
|
||||||
* See "search parameters" under https://codex.wordpress.org/Class_Reference/WP_User_Query.
|
|
||||||
*
|
|
||||||
* @param array $prepared_args Prepared search filter args from the customer endpoint.
|
|
||||||
* @param array $request Request/query arguments.
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
public static function update_search_filters( $prepared_args, $request ) {
|
|
||||||
if ( ! empty( $request['email'] ) ) {
|
|
||||||
$prepared_args['search'] = '*' . $prepared_args['search'] . '*';
|
|
||||||
}
|
|
||||||
return $prepared_args;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the query params for collections.
|
|
||||||
*
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
public function get_collection_params() {
|
|
||||||
$params = parent::get_collection_params();
|
|
||||||
// Allow partial email matches. Previously, this was of format 'email' which required a strict "test@example.com" format.
|
|
||||||
// This, in combination with `update_search_filters` allows us to do partial searches.
|
|
||||||
$params['email']['format'] = '';
|
|
||||||
return $params;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
add_filter( 'woocommerce_rest_customer_query', array( 'WC_Admin_REST_Customers_Controller', 'update_search_filters' ), 10, 2 );
|
|
||||||
|
|
|
@ -46,7 +46,7 @@ class WC_Admin_REST_Reports_Customers_Controller extends WC_REST_Reports_Control
|
||||||
$args['order'] = $request['order'];
|
$args['order'] = $request['order'];
|
||||||
$args['orderby'] = $request['orderby'];
|
$args['orderby'] = $request['orderby'];
|
||||||
$args['match'] = $request['match'];
|
$args['match'] = $request['match'];
|
||||||
$args['name'] = $request['name'];
|
$args['search'] = $request['search'];
|
||||||
$args['username'] = $request['username'];
|
$args['username'] = $request['username'];
|
||||||
$args['email'] = $request['email'];
|
$args['email'] = $request['email'];
|
||||||
$args['country'] = $request['country'];
|
$args['country'] = $request['country'];
|
||||||
|
@ -60,6 +60,7 @@ class WC_Admin_REST_Reports_Customers_Controller extends WC_REST_Reports_Control
|
||||||
$args['avg_order_value_max'] = $request['avg_order_value_max'];
|
$args['avg_order_value_max'] = $request['avg_order_value_max'];
|
||||||
$args['last_order_before'] = $request['last_order_before'];
|
$args['last_order_before'] = $request['last_order_before'];
|
||||||
$args['last_order_after'] = $request['last_order_after'];
|
$args['last_order_after'] = $request['last_order_after'];
|
||||||
|
$args['customers'] = $request['customers'];
|
||||||
|
|
||||||
$between_params_numeric = array( 'orders_count', 'total_spend', 'avg_order_value' );
|
$between_params_numeric = array( 'orders_count', 'total_spend', 'avg_order_value' );
|
||||||
$normalized_params_numeric = WC_Admin_Reports_Interval::normalize_between_params( $request, $between_params_numeric, false );
|
$normalized_params_numeric = WC_Admin_Reports_Interval::normalize_between_params( $request, $between_params_numeric, false );
|
||||||
|
@ -172,7 +173,7 @@ class WC_Admin_REST_Reports_Customers_Controller extends WC_REST_Reports_Control
|
||||||
'title' => 'report_customers',
|
'title' => 'report_customers',
|
||||||
'type' => 'object',
|
'type' => 'object',
|
||||||
'properties' => array(
|
'properties' => array(
|
||||||
'customer_id' => array(
|
'id' => array(
|
||||||
'description' => __( 'Customer ID.', 'wc-admin' ),
|
'description' => __( 'Customer ID.', 'wc-admin' ),
|
||||||
'type' => 'integer',
|
'type' => 'integer',
|
||||||
'context' => array( 'view', 'edit' ),
|
'context' => array( 'view', 'edit' ),
|
||||||
|
@ -333,8 +334,8 @@ class WC_Admin_REST_Reports_Customers_Controller extends WC_REST_Reports_Control
|
||||||
),
|
),
|
||||||
'validate_callback' => 'rest_validate_request_arg',
|
'validate_callback' => 'rest_validate_request_arg',
|
||||||
);
|
);
|
||||||
$params['name'] = array(
|
$params['search'] = array(
|
||||||
'description' => __( 'Limit response to objects with a specfic customer name.', 'wc-admin' ),
|
'description' => __( 'Limit response to objects with a customer name containing the search term.', 'wc-admin' ),
|
||||||
'type' => 'string',
|
'type' => 'string',
|
||||||
'validate_callback' => 'rest_validate_request_arg',
|
'validate_callback' => 'rest_validate_request_arg',
|
||||||
);
|
);
|
||||||
|
@ -446,6 +447,17 @@ class WC_Admin_REST_Reports_Customers_Controller extends WC_REST_Reports_Control
|
||||||
'format' => 'date-time',
|
'format' => 'date-time',
|
||||||
'validate_callback' => 'rest_validate_request_arg',
|
'validate_callback' => 'rest_validate_request_arg',
|
||||||
);
|
);
|
||||||
|
$params['customers'] = array(
|
||||||
|
'description' => __( 'Limit result to items with specified customer ids.', 'wc-admin' ),
|
||||||
|
'type' => 'array',
|
||||||
|
'sanitize_callback' => 'wp_parse_id_list',
|
||||||
|
'validate_callback' => 'rest_validate_request_arg',
|
||||||
|
'items' => array(
|
||||||
|
'type' => 'integer',
|
||||||
|
),
|
||||||
|
|
||||||
|
);
|
||||||
|
|
||||||
return $params;
|
return $params;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,7 +41,7 @@ class WC_Admin_REST_Reports_Customers_Stats_Controller extends WC_REST_Reports_C
|
||||||
$args['registered_before'] = $request['registered_before'];
|
$args['registered_before'] = $request['registered_before'];
|
||||||
$args['registered_after'] = $request['registered_after'];
|
$args['registered_after'] = $request['registered_after'];
|
||||||
$args['match'] = $request['match'];
|
$args['match'] = $request['match'];
|
||||||
$args['name'] = $request['name'];
|
$args['search'] = $request['search'];
|
||||||
$args['username'] = $request['username'];
|
$args['username'] = $request['username'];
|
||||||
$args['email'] = $request['email'];
|
$args['email'] = $request['email'];
|
||||||
$args['country'] = $request['country'];
|
$args['country'] = $request['country'];
|
||||||
|
@ -55,6 +55,7 @@ class WC_Admin_REST_Reports_Customers_Stats_Controller extends WC_REST_Reports_C
|
||||||
$args['avg_order_value_max'] = $request['avg_order_value_max'];
|
$args['avg_order_value_max'] = $request['avg_order_value_max'];
|
||||||
$args['last_order_before'] = $request['last_order_before'];
|
$args['last_order_before'] = $request['last_order_before'];
|
||||||
$args['last_order_after'] = $request['last_order_after'];
|
$args['last_order_after'] = $request['last_order_after'];
|
||||||
|
$args['customers'] = $request['customers'];
|
||||||
|
|
||||||
$between_params_numeric = array( 'orders_count', 'total_spend', 'avg_order_value' );
|
$between_params_numeric = array( 'orders_count', 'total_spend', 'avg_order_value' );
|
||||||
$normalized_params_numeric = WC_Admin_Reports_Interval::normalize_between_params( $request, $between_params_numeric, false );
|
$normalized_params_numeric = WC_Admin_Reports_Interval::normalize_between_params( $request, $between_params_numeric, false );
|
||||||
|
@ -77,8 +78,6 @@ class WC_Admin_REST_Reports_Customers_Stats_Controller extends WC_REST_Reports_C
|
||||||
$report_data = $customers_query->get_data();
|
$report_data = $customers_query->get_data();
|
||||||
$out_data = array(
|
$out_data = array(
|
||||||
'totals' => $report_data,
|
'totals' => $report_data,
|
||||||
// @todo Is this needed? the single element array tricks the isReportDataEmpty() selector.
|
|
||||||
'intervals' => array( (object) array() ),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return rest_ensure_response( $out_data );
|
return rest_ensure_response( $out_data );
|
||||||
|
@ -161,55 +160,6 @@ class WC_Admin_REST_Reports_Customers_Stats_Controller extends WC_REST_Reports_C
|
||||||
'readonly' => true,
|
'readonly' => true,
|
||||||
'properties' => $totals,
|
'properties' => $totals,
|
||||||
),
|
),
|
||||||
'intervals' => array( // @todo Remove this?
|
|
||||||
'description' => __( 'Reports data grouped by intervals.', 'wc-admin' ),
|
|
||||||
'type' => 'array',
|
|
||||||
'context' => array( 'view', 'edit' ),
|
|
||||||
'readonly' => true,
|
|
||||||
'items' => array(
|
|
||||||
'type' => 'object',
|
|
||||||
'properties' => array(
|
|
||||||
'interval' => array(
|
|
||||||
'description' => __( 'Type of interval.', 'wc-admin' ),
|
|
||||||
'type' => 'string',
|
|
||||||
'context' => array( 'view', 'edit' ),
|
|
||||||
'readonly' => true,
|
|
||||||
'enum' => array( 'day', 'week', 'month', 'year' ),
|
|
||||||
),
|
|
||||||
'date_start' => array(
|
|
||||||
'description' => __( "The date the report start, in the site's timezone.", 'wc-admin' ),
|
|
||||||
'type' => 'date-time',
|
|
||||||
'context' => array( 'view', 'edit' ),
|
|
||||||
'readonly' => true,
|
|
||||||
),
|
|
||||||
'date_start_gmt' => array(
|
|
||||||
'description' => __( 'The date the report start, as GMT.', 'wc-admin' ),
|
|
||||||
'type' => 'date-time',
|
|
||||||
'context' => array( 'view', 'edit' ),
|
|
||||||
'readonly' => true,
|
|
||||||
),
|
|
||||||
'date_end' => array(
|
|
||||||
'description' => __( "The date the report end, in the site's timezone.", 'wc-admin' ),
|
|
||||||
'type' => 'date-time',
|
|
||||||
'context' => array( 'view', 'edit' ),
|
|
||||||
'readonly' => true,
|
|
||||||
),
|
|
||||||
'date_end_gmt' => array(
|
|
||||||
'description' => __( 'The date the report end, as GMT.', 'wc-admin' ),
|
|
||||||
'type' => 'date-time',
|
|
||||||
'context' => array( 'view', 'edit' ),
|
|
||||||
'readonly' => true,
|
|
||||||
),
|
|
||||||
'subtotals' => array(
|
|
||||||
'description' => __( 'Interval subtotals.', 'wc-admin' ),
|
|
||||||
'type' => 'object',
|
|
||||||
'context' => array( 'view', 'edit' ),
|
|
||||||
'readonly' => true,
|
|
||||||
'properties' => $totals,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -246,7 +196,7 @@ class WC_Admin_REST_Reports_Customers_Stats_Controller extends WC_REST_Reports_C
|
||||||
),
|
),
|
||||||
'validate_callback' => 'rest_validate_request_arg',
|
'validate_callback' => 'rest_validate_request_arg',
|
||||||
);
|
);
|
||||||
$params['name'] = array(
|
$params['search'] = array(
|
||||||
'description' => __( 'Limit response to objects with a specfic customer name.', 'wc-admin' ),
|
'description' => __( 'Limit response to objects with a specfic customer name.', 'wc-admin' ),
|
||||||
'type' => 'string',
|
'type' => 'string',
|
||||||
'validate_callback' => 'rest_validate_request_arg',
|
'validate_callback' => 'rest_validate_request_arg',
|
||||||
|
@ -359,6 +309,16 @@ class WC_Admin_REST_Reports_Customers_Stats_Controller extends WC_REST_Reports_C
|
||||||
'format' => 'date-time',
|
'format' => 'date-time',
|
||||||
'validate_callback' => 'rest_validate_request_arg',
|
'validate_callback' => 'rest_validate_request_arg',
|
||||||
);
|
);
|
||||||
|
$params['customers'] = array(
|
||||||
|
'description' => __( 'Limit result to items with specified customer ids.', 'wc-admin' ),
|
||||||
|
'type' => 'array',
|
||||||
|
'sanitize_callback' => 'wp_parse_id_list',
|
||||||
|
'validate_callback' => 'rest_validate_request_arg',
|
||||||
|
'items' => array(
|
||||||
|
'type' => 'integer',
|
||||||
|
),
|
||||||
|
|
||||||
|
);
|
||||||
|
|
||||||
return $params;
|
return $params;
|
||||||
}
|
}
|
||||||
|
|
|
@ -170,7 +170,7 @@ class WC_Admin_REST_Reports_Orders_Stats_Controller extends WC_Admin_REST_Report
|
||||||
),
|
),
|
||||||
'avg_items_per_order' => array(
|
'avg_items_per_order' => array(
|
||||||
'description' => __( 'Average items per order', 'wc-admin' ),
|
'description' => __( 'Average items per order', 'wc-admin' ),
|
||||||
'type' => 'integer',
|
'type' => 'number',
|
||||||
'context' => array( 'view', 'edit' ),
|
'context' => array( 'view', 'edit' ),
|
||||||
'readonly' => true,
|
'readonly' => true,
|
||||||
),
|
),
|
||||||
|
|
|
@ -0,0 +1,138 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* REST API Reports stock stats controller
|
||||||
|
*
|
||||||
|
* Handles requests to the /reports/stock/stats endpoint.
|
||||||
|
*
|
||||||
|
* @package WooCommerce Admin/API
|
||||||
|
*/
|
||||||
|
|
||||||
|
defined( 'ABSPATH' ) || exit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* REST API Reports stock stats controller class.
|
||||||
|
*
|
||||||
|
* @package WooCommerce/API
|
||||||
|
* @extends WC_REST_Reports_Controller
|
||||||
|
*/
|
||||||
|
class WC_Admin_REST_Reports_Stock_Stats_Controller extends WC_REST_Reports_Controller {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Endpoint namespace.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $namespace = 'wc/v4';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Route base.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $rest_base = 'reports/stock/stats';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get Stock Status Totals.
|
||||||
|
*
|
||||||
|
* @param WP_REST_Request $request Request data.
|
||||||
|
* @return array|WP_Error
|
||||||
|
*/
|
||||||
|
public function get_items( $request ) {
|
||||||
|
$stock_query = new WC_Admin_Reports_Stock_Stats_Query();
|
||||||
|
$report_data = $stock_query->get_data();
|
||||||
|
$out_data = array(
|
||||||
|
'totals' => $report_data,
|
||||||
|
);
|
||||||
|
return rest_ensure_response( $out_data );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare a report object for serialization.
|
||||||
|
*
|
||||||
|
* @param WC_Product $report Report data.
|
||||||
|
* @param WP_REST_Request $request Request object.
|
||||||
|
* @return WP_REST_Response
|
||||||
|
*/
|
||||||
|
public function prepare_item_for_response( $report, $request ) {
|
||||||
|
$data = $report;
|
||||||
|
|
||||||
|
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
|
||||||
|
$data = $this->add_additional_fields_to_object( $data, $request );
|
||||||
|
$data = $this->filter_response_by_context( $data, $context );
|
||||||
|
|
||||||
|
// Wrap the data in a response object.
|
||||||
|
$response = rest_ensure_response( $data );
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter a report returned from the API.
|
||||||
|
*
|
||||||
|
* Allows modification of the report data right before it is returned.
|
||||||
|
*
|
||||||
|
* @param WP_REST_Response $response The response object.
|
||||||
|
* @param WC_Product $product The original bject.
|
||||||
|
* @param WP_REST_Request $request Request used to generate the response.
|
||||||
|
*/
|
||||||
|
return apply_filters( 'woocommerce_rest_prepare_report_stock_stats', $response, $product, $request );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the Report's schema, conforming to JSON Schema.
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function get_item_schema() {
|
||||||
|
$totals = array(
|
||||||
|
'products' => array(
|
||||||
|
'description' => __( 'Number of products.', 'wc-admin' ),
|
||||||
|
'type' => 'integer',
|
||||||
|
'context' => array( 'view', 'edit' ),
|
||||||
|
'readonly' => true,
|
||||||
|
),
|
||||||
|
'lowstock' => array(
|
||||||
|
'description' => __( 'Number of low stock products.', 'wc-admin' ),
|
||||||
|
'type' => 'integer',
|
||||||
|
'context' => array( 'view', 'edit' ),
|
||||||
|
'readonly' => true,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
$status_options = wc_get_product_stock_status_options();
|
||||||
|
foreach ( $status_options as $status => $label ) {
|
||||||
|
$totals[ $status ] = array(
|
||||||
|
/* translators: Stock status. Example: "Number of low stock products */
|
||||||
|
'description' => sprintf( __( 'Number of %s products.', 'wc-admin' ), $label ),
|
||||||
|
'type' => 'integer',
|
||||||
|
'context' => array( 'view', 'edit' ),
|
||||||
|
'readonly' => true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$schema = array(
|
||||||
|
'$schema' => 'http://json-schema.org/draft-04/schema#',
|
||||||
|
'title' => 'report_customers_stats',
|
||||||
|
'type' => 'object',
|
||||||
|
'properties' => array(
|
||||||
|
'totals' => array(
|
||||||
|
'description' => __( 'Totals data.', 'wc-admin' ),
|
||||||
|
'type' => 'object',
|
||||||
|
'context' => array( 'view', 'edit' ),
|
||||||
|
'readonly' => true,
|
||||||
|
'properties' => $totals,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return $this->add_additional_fields_schema( $schema );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the query params for collections.
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function get_collection_params() {
|
||||||
|
$params = array();
|
||||||
|
$params['context'] = $this->get_context_param( array( 'default' => 'view' ) );
|
||||||
|
return $params;
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,43 +12,6 @@ defined( 'ABSPATH' ) || exit;
|
||||||
*/
|
*/
|
||||||
class WC_Admin_Api_Init {
|
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';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Action hook for processing a batch of orders.
|
|
||||||
*/
|
|
||||||
const SINGLE_ORDER_ACTION = 'wc-admin_process_order';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Queue instance.
|
|
||||||
*
|
|
||||||
* @var WC_Queue_Interface
|
|
||||||
*/
|
|
||||||
protected static $queue = null;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Boostrap REST API.
|
* Boostrap REST API.
|
||||||
*/
|
*/
|
||||||
|
@ -57,50 +20,14 @@ class WC_Admin_Api_Init {
|
||||||
add_action( 'plugins_loaded', array( $this, 'init_classes' ), 19 );
|
add_action( 'plugins_loaded', array( $this, 'init_classes' ), 19 );
|
||||||
// Hook in data stores.
|
// Hook in data stores.
|
||||||
add_filter( 'woocommerce_data_stores', array( 'WC_Admin_Api_Init', 'add_data_stores' ) );
|
add_filter( 'woocommerce_data_stores', array( 'WC_Admin_Api_Init', 'add_data_stores' ) );
|
||||||
// Add wc-admin report tables to list of WooCommerce tables.
|
|
||||||
add_filter( 'woocommerce_install_get_tables', array( 'WC_Admin_Api_Init', 'add_tables' ) );
|
|
||||||
// REST API extensions init.
|
// REST API extensions init.
|
||||||
add_action( 'rest_api_init', array( $this, 'rest_api_init' ) );
|
add_action( 'rest_api_init', array( $this, 'rest_api_init' ) );
|
||||||
add_filter( 'rest_endpoints', array( 'WC_Admin_Api_Init', 'filter_rest_endpoints' ), 10, 1 );
|
add_filter( 'rest_endpoints', array( 'WC_Admin_Api_Init', 'filter_rest_endpoints' ), 10, 1 );
|
||||||
add_filter( 'woocommerce_debug_tools', array( 'WC_Admin_Api_Init', 'add_regenerate_tool' ) );
|
|
||||||
|
|
||||||
// Initialize syncing hooks.
|
|
||||||
add_action( 'wp_loaded', array( __CLASS__, 'orders_lookup_update_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, 3 );
|
|
||||||
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_action( self::SINGLE_ORDER_ACTION, array( __CLASS__, 'orders_lookup_process_order' ) );
|
|
||||||
|
|
||||||
// Add currency symbol to orders endpoint response.
|
// Add currency symbol to orders endpoint response.
|
||||||
add_filter( 'woocommerce_rest_prepare_shop_order_object', array( __CLASS__, 'add_currency_symbol_to_order_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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Init classes.
|
* Init classes.
|
||||||
*/
|
*/
|
||||||
|
@ -141,6 +68,7 @@ class WC_Admin_Api_Init {
|
||||||
require_once dirname( __FILE__ ) . '/class-wc-admin-reports-downloads-stats-query.php';
|
require_once dirname( __FILE__ ) . '/class-wc-admin-reports-downloads-stats-query.php';
|
||||||
require_once dirname( __FILE__ ) . '/class-wc-admin-reports-customers-query.php';
|
require_once dirname( __FILE__ ) . '/class-wc-admin-reports-customers-query.php';
|
||||||
require_once dirname( __FILE__ ) . '/class-wc-admin-reports-customers-stats-query.php';
|
require_once dirname( __FILE__ ) . '/class-wc-admin-reports-customers-stats-query.php';
|
||||||
|
require_once dirname( __FILE__ ) . '/class-wc-admin-reports-stock-stats-query.php';
|
||||||
|
|
||||||
// Data stores.
|
// Data stores.
|
||||||
require_once dirname( __FILE__ ) . '/data-stores/class-wc-admin-reports-data-store.php';
|
require_once dirname( __FILE__ ) . '/data-stores/class-wc-admin-reports-data-store.php';
|
||||||
|
@ -158,6 +86,7 @@ class WC_Admin_Api_Init {
|
||||||
require_once dirname( __FILE__ ) . '/data-stores/class-wc-admin-reports-downloads-stats-data-store.php';
|
require_once dirname( __FILE__ ) . '/data-stores/class-wc-admin-reports-downloads-stats-data-store.php';
|
||||||
require_once dirname( __FILE__ ) . '/data-stores/class-wc-admin-reports-customers-data-store.php';
|
require_once dirname( __FILE__ ) . '/data-stores/class-wc-admin-reports-customers-data-store.php';
|
||||||
require_once dirname( __FILE__ ) . '/data-stores/class-wc-admin-reports-customers-stats-data-store.php';
|
require_once dirname( __FILE__ ) . '/data-stores/class-wc-admin-reports-customers-stats-data-store.php';
|
||||||
|
require_once dirname( __FILE__ ) . '/data-stores/class-wc-admin-reports-stock-stats-data-store.php';
|
||||||
|
|
||||||
// Data triggers.
|
// Data triggers.
|
||||||
require_once dirname( __FILE__ ) . '/data-stores/class-wc-admin-notes-data-store.php';
|
require_once dirname( __FILE__ ) . '/data-stores/class-wc-admin-notes-data-store.php';
|
||||||
|
@ -173,7 +102,6 @@ class WC_Admin_Api_Init {
|
||||||
public function rest_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-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-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-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-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-data-download-ips-controller.php';
|
||||||
|
@ -204,7 +132,9 @@ class WC_Admin_Api_Init {
|
||||||
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-reports-taxes-controller.php';
|
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-reports-taxes-controller.php';
|
||||||
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-reports-taxes-stats-controller.php';
|
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-reports-taxes-stats-controller.php';
|
||||||
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-reports-stock-controller.php';
|
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-reports-stock-controller.php';
|
||||||
|
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-reports-stock-stats-controller.php';
|
||||||
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-taxes-controller.php';
|
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-taxes-controller.php';
|
||||||
|
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-customers-controller.php';
|
||||||
|
|
||||||
$controllers = apply_filters(
|
$controllers = apply_filters(
|
||||||
'woocommerce_admin_rest_controllers',
|
'woocommerce_admin_rest_controllers',
|
||||||
|
@ -236,6 +166,7 @@ class WC_Admin_Api_Init {
|
||||||
'WC_Admin_REST_Reports_Coupons_Controller',
|
'WC_Admin_REST_Reports_Coupons_Controller',
|
||||||
'WC_Admin_REST_Reports_Coupons_Stats_Controller',
|
'WC_Admin_REST_Reports_Coupons_Stats_Controller',
|
||||||
'WC_Admin_REST_Reports_Stock_Controller',
|
'WC_Admin_REST_Reports_Stock_Controller',
|
||||||
|
'WC_Admin_REST_Reports_Stock_Stats_Controller',
|
||||||
'WC_Admin_REST_Reports_Downloads_Controller',
|
'WC_Admin_REST_Reports_Downloads_Controller',
|
||||||
'WC_Admin_REST_Reports_Downloads_Stats_Controller',
|
'WC_Admin_REST_Reports_Downloads_Stats_Controller',
|
||||||
'WC_Admin_REST_Reports_Customers_Controller',
|
'WC_Admin_REST_Reports_Customers_Controller',
|
||||||
|
@ -429,323 +360,6 @@ class WC_Admin_Api_Init {
|
||||||
return $endpoints;
|
return $endpoints;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Regenerate data for reports.
|
|
||||||
*/
|
|
||||||
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_batch_init();
|
|
||||||
// Queue orders lookup to occur after customers lookup generation is done.
|
|
||||||
self::queue_dependent_action( self::ORDERS_LOOKUP_BATCH_INIT, array(), self::CUSTOMERS_BATCH_ACTION );
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds regenerate tool.
|
|
||||||
*
|
|
||||||
* @param array $tools List of tools.
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
public static function add_regenerate_tool( $tools ) {
|
|
||||||
return array_merge(
|
|
||||||
$tools,
|
|
||||||
array(
|
|
||||||
'rebuild_stats' => array(
|
|
||||||
'name' => __( 'Rebuild reports data', 'wc-admin' ),
|
|
||||||
'button' => __( 'Rebuild reports', 'wc-admin' ),
|
|
||||||
'desc' => __( 'This tool will rebuild all of the information used by the reports.', 'wc-admin' ),
|
|
||||||
'callback' => array( 'WC_Admin_Api_Init', 'regenerate_report_data' ),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Schedule an action to process a single Order.
|
|
||||||
*
|
|
||||||
* @param int $order_id Order ID.
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public static function schedule_single_order_process( $order_id ) {
|
|
||||||
if ( 'shop_order' !== get_post_type( $order_id ) ) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// This can get called multiple times for a single order, so we look
|
|
||||||
// for existing pending jobs for the same order to avoid duplicating efforts.
|
|
||||||
$existing_jobs = self::queue()->search(
|
|
||||||
array(
|
|
||||||
'status' => 'pending',
|
|
||||||
'per_page' => 1,
|
|
||||||
'claimed' => false,
|
|
||||||
'search' => "[{$order_id}]",
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
if ( $existing_jobs ) {
|
|
||||||
$existing_job = current( $existing_jobs );
|
|
||||||
|
|
||||||
// Bail out if there's a pending single order action, or a pending dependent action.
|
|
||||||
if (
|
|
||||||
( self::SINGLE_ORDER_ACTION === $existing_job->get_hook() ) ||
|
|
||||||
(
|
|
||||||
self::QUEUE_DEPEDENT_ACTION === $existing_job->get_hook() &&
|
|
||||||
in_array( self::SINGLE_ORDER_ACTION, $existing_job->get_args() )
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// We want to ensure that customer lookup updates are scheduled before order updates.
|
|
||||||
self::queue_dependent_action( self::SINGLE_ORDER_ACTION, array( $order_id ), self::CUSTOMERS_BATCH_ACTION );
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Attach order lookup update hooks.
|
|
||||||
*/
|
|
||||||
public static function orders_lookup_update_init() {
|
|
||||||
// Activate WC_Order extension.
|
|
||||||
WC_Admin_Order::add_filters();
|
|
||||||
|
|
||||||
add_action( 'save_post_shop_order', array( __CLASS__, 'schedule_single_order_process' ) );
|
|
||||||
add_action( 'woocommerce_order_refunded', array( __CLASS__, 'schedule_single_order_process' ) );
|
|
||||||
|
|
||||||
WC_Admin_Reports_Orders_Stats_Data_Store::init();
|
|
||||||
WC_Admin_Reports_Customers_Data_Store::init();
|
|
||||||
WC_Admin_Reports_Coupons_Data_Store::init();
|
|
||||||
WC_Admin_Reports_Products_Data_Store::init();
|
|
||||||
WC_Admin_Reports_Taxes_Data_Store::init();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Init order/product lookup tables update (in batches).
|
|
||||||
*/
|
|
||||||
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();
|
|
||||||
|
|
||||||
if ( 0 === $result->total ) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$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 ) {
|
|
||||||
self::orders_lookup_process_order( $order_id );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Process a single order to update lookup tables for.
|
|
||||||
* If an error is encountered in one of the updates, a retry action is scheduled.
|
|
||||||
*
|
|
||||||
* @param int $order_id Order ID.
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public static function orders_lookup_process_order( $order_id ) {
|
|
||||||
$result = array_sum(
|
|
||||||
array(
|
|
||||||
WC_Admin_Reports_Orders_Stats_Data_Store::sync_order( $order_id ),
|
|
||||||
WC_Admin_Reports_Products_Data_Store::sync_order_products( $order_id ),
|
|
||||||
WC_Admin_Reports_Coupons_Data_Store::sync_order_coupons( $order_id ),
|
|
||||||
WC_Admin_Reports_Taxes_Data_Store::sync_order_taxes( $order_id ),
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
// If all updates were either skipped or successful, we're done.
|
|
||||||
// The update methods return -1 for skip, or a boolean success indicator.
|
|
||||||
if ( 4 === absint( $result ) ) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise assume an error occurred and reschedule.
|
|
||||||
self::schedule_single_order_process( $order_id );
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the batch size for regenerating reports.
|
|
||||||
* Note: can differ per batch action.
|
|
||||||
*
|
|
||||||
* @param string $action Single batch action name.
|
|
||||||
* @return int Batch size.
|
|
||||||
*/
|
|
||||||
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;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 );
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 array $action_args Action arguments.
|
|
||||||
* @param string $prerequisite_action Prerequisite action.
|
|
||||||
*/
|
|
||||||
public static function queue_dependent_action( $action, $action_args, $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.
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
$next_job_schedule = null;
|
|
||||||
$blocking_job_hook = null;
|
|
||||||
|
|
||||||
if ( $blocking_jobs ) {
|
|
||||||
$blocking_job = current( $blocking_jobs );
|
|
||||||
$blocking_job_hook = $blocking_job->get_hook();
|
|
||||||
$next_job_schedule = $blocking_job->get_schedule()->next();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Eliminate the false positive scenario where the blocking job is
|
|
||||||
// actually another queued dependent action awaiting the same prerequisite.
|
|
||||||
// Also, ensure that the next schedule is a DateTime (it can be null).
|
|
||||||
if (
|
|
||||||
is_a( $next_job_schedule, 'DateTime' ) &&
|
|
||||||
( self::QUEUE_DEPEDENT_ACTION !== $blocking_job_hook )
|
|
||||||
) {
|
|
||||||
self::queue()->schedule_single(
|
|
||||||
$next_job_schedule->getTimestamp() + 5,
|
|
||||||
self::QUEUE_DEPEDENT_ACTION,
|
|
||||||
array( $action, $action_args, $prerequisite_action )
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
self::queue()->schedule_single( time() + 5, $action, $action_args );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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();
|
|
||||||
|
|
||||||
if ( 0 === $total_customers ) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$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 ) {
|
|
||||||
// @todo Schedule single customer update if this fails?
|
|
||||||
WC_Admin_Reports_Customers_Data_Store::update_registered_customer( $customer_id );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds data stores.
|
* Adds data stores.
|
||||||
*
|
*
|
||||||
|
@ -756,187 +370,27 @@ class WC_Admin_Api_Init {
|
||||||
return array_merge(
|
return array_merge(
|
||||||
$data_stores,
|
$data_stores,
|
||||||
array(
|
array(
|
||||||
'report-revenue-stats' => 'WC_Admin_Reports_Orders_Stats_Data_Store',
|
'report-revenue-stats' => 'WC_Admin_Reports_Orders_Stats_Data_Store',
|
||||||
'report-orders' => 'WC_Admin_Reports_Orders_Data_Store',
|
'report-orders' => 'WC_Admin_Reports_Orders_Data_Store',
|
||||||
'report-orders-stats' => 'WC_Admin_Reports_Orders_Stats_Data_Store',
|
'report-orders-stats' => 'WC_Admin_Reports_Orders_Stats_Data_Store',
|
||||||
'report-products' => 'WC_Admin_Reports_Products_Data_Store',
|
'report-products' => 'WC_Admin_Reports_Products_Data_Store',
|
||||||
'report-variations' => 'WC_Admin_Reports_Variations_Data_Store',
|
'report-variations' => 'WC_Admin_Reports_Variations_Data_Store',
|
||||||
'report-products-stats' => 'WC_Admin_Reports_Products_Stats_Data_Store',
|
'report-products-stats' => 'WC_Admin_Reports_Products_Stats_Data_Store',
|
||||||
'report-categories' => 'WC_Admin_Reports_Categories_Data_Store',
|
'report-categories' => 'WC_Admin_Reports_Categories_Data_Store',
|
||||||
'report-taxes' => 'WC_Admin_Reports_Taxes_Data_Store',
|
'report-taxes' => 'WC_Admin_Reports_Taxes_Data_Store',
|
||||||
'report-taxes-stats' => 'WC_Admin_Reports_Taxes_Stats_Data_Store',
|
'report-taxes-stats' => 'WC_Admin_Reports_Taxes_Stats_Data_Store',
|
||||||
'report-coupons' => 'WC_Admin_Reports_Coupons_Data_Store',
|
'report-coupons' => 'WC_Admin_Reports_Coupons_Data_Store',
|
||||||
'report-coupons-stats' => 'WC_Admin_Reports_Coupons_Stats_Data_Store',
|
'report-coupons-stats' => 'WC_Admin_Reports_Coupons_Stats_Data_Store',
|
||||||
'report-downloads' => 'WC_Admin_Reports_Downloads_Data_Store',
|
'report-downloads' => 'WC_Admin_Reports_Downloads_Data_Store',
|
||||||
'report-downloads-stats' => 'WC_Admin_Reports_Downloads_Stats_Data_Store',
|
'report-downloads-stats' => 'WC_Admin_Reports_Downloads_Stats_Data_Store',
|
||||||
'admin-note' => 'WC_Admin_Notes_Data_Store',
|
'admin-note' => 'WC_Admin_Notes_Data_Store',
|
||||||
'report-customers' => 'WC_Admin_Reports_Customers_Data_Store',
|
'report-customers' => 'WC_Admin_Reports_Customers_Data_Store',
|
||||||
'report-customers-stats' => 'WC_Admin_Reports_Customers_Stats_Data_Store',
|
'report-customers-stats' => 'WC_Admin_Reports_Customers_Stats_Data_Store',
|
||||||
|
'report-stock-stats' => 'WC_Admin_Reports_Stock_Stats_Data_Store',
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds new tables.
|
|
||||||
*
|
|
||||||
* @param array $wc_tables List of WooCommerce tables.
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
public static function add_tables( $wc_tables ) {
|
|
||||||
global $wpdb;
|
|
||||||
|
|
||||||
return array_merge(
|
|
||||||
$wc_tables,
|
|
||||||
array(
|
|
||||||
// @todo Will this work on multisite?
|
|
||||||
"{$wpdb->prefix}wc_order_stats",
|
|
||||||
"{$wpdb->prefix}wc_order_product_lookup",
|
|
||||||
"{$wpdb->prefix}wc_order_tax_lookup",
|
|
||||||
"{$wpdb->prefix}wc_order_coupon_lookup",
|
|
||||||
"{$wpdb->prefix}wc_admin_notes",
|
|
||||||
"{$wpdb->prefix}wc_admin_note_actions",
|
|
||||||
"{$wpdb->prefix}wc_customer_lookup",
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get database schema.
|
|
||||||
*
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
private static function get_schema() {
|
|
||||||
global $wpdb;
|
|
||||||
|
|
||||||
if ( $wpdb->has_cap( 'collation' ) ) {
|
|
||||||
$collate = $wpdb->get_charset_collate();
|
|
||||||
}
|
|
||||||
|
|
||||||
$tables = "
|
|
||||||
CREATE TABLE {$wpdb->prefix}wc_order_stats (
|
|
||||||
order_id bigint(20) unsigned NOT NULL,
|
|
||||||
date_created datetime DEFAULT '0000-00-00 00:00:00' NOT NULL,
|
|
||||||
num_items_sold int(11) UNSIGNED DEFAULT 0 NOT NULL,
|
|
||||||
gross_total double DEFAULT 0 NOT NULL,
|
|
||||||
coupon_total double DEFAULT 0 NOT NULL,
|
|
||||||
refund_total double DEFAULT 0 NOT NULL,
|
|
||||||
tax_total double DEFAULT 0 NOT NULL,
|
|
||||||
shipping_total double DEFAULT 0 NOT NULL,
|
|
||||||
net_total double DEFAULT 0 NOT NULL,
|
|
||||||
returning_customer boolean DEFAULT 0 NOT NULL,
|
|
||||||
status varchar(200) NOT NULL,
|
|
||||||
customer_id BIGINT UNSIGNED NOT NULL,
|
|
||||||
PRIMARY KEY (order_id),
|
|
||||||
KEY date_created (date_created),
|
|
||||||
KEY customer_id (customer_id),
|
|
||||||
KEY status (status)
|
|
||||||
) $collate;
|
|
||||||
CREATE TABLE {$wpdb->prefix}wc_order_product_lookup (
|
|
||||||
order_item_id BIGINT UNSIGNED NOT NULL,
|
|
||||||
order_id BIGINT UNSIGNED NOT NULL,
|
|
||||||
product_id BIGINT UNSIGNED NOT NULL,
|
|
||||||
variation_id BIGINT UNSIGNED NOT NULL,
|
|
||||||
customer_id BIGINT UNSIGNED NULL,
|
|
||||||
date_created datetime DEFAULT '0000-00-00 00:00:00' NOT NULL,
|
|
||||||
product_qty INT UNSIGNED NOT NULL,
|
|
||||||
product_net_revenue double DEFAULT 0 NOT NULL,
|
|
||||||
product_gross_revenue double DEFAULT 0 NOT NULL,
|
|
||||||
coupon_amount double DEFAULT 0 NOT NULL,
|
|
||||||
tax_amount double DEFAULT 0 NOT NULL,
|
|
||||||
shipping_amount double DEFAULT 0 NOT NULL,
|
|
||||||
shipping_tax_amount double DEFAULT 0 NOT NULL,
|
|
||||||
refund_amount double DEFAULT 0 NOT NULL,
|
|
||||||
PRIMARY KEY (order_item_id),
|
|
||||||
KEY order_id (order_id),
|
|
||||||
KEY product_id (product_id),
|
|
||||||
KEY customer_id (customer_id),
|
|
||||||
KEY date_created (date_created)
|
|
||||||
) $collate;
|
|
||||||
CREATE TABLE {$wpdb->prefix}wc_order_tax_lookup (
|
|
||||||
order_id BIGINT UNSIGNED NOT NULL,
|
|
||||||
tax_rate_id BIGINT UNSIGNED NOT NULL,
|
|
||||||
date_created datetime DEFAULT '0000-00-00 00:00:00' NOT NULL,
|
|
||||||
shipping_tax double DEFAULT 0 NOT NULL,
|
|
||||||
order_tax double DEFAULT 0 NOT NULL,
|
|
||||||
total_tax double DEFAULT 0 NOT NULL,
|
|
||||||
PRIMARY KEY (order_id, tax_rate_id),
|
|
||||||
KEY tax_rate_id (tax_rate_id),
|
|
||||||
KEY date_created (date_created)
|
|
||||||
) $collate;
|
|
||||||
CREATE TABLE {$wpdb->prefix}wc_order_coupon_lookup (
|
|
||||||
order_id BIGINT UNSIGNED NOT NULL,
|
|
||||||
coupon_id BIGINT UNSIGNED NOT NULL,
|
|
||||||
date_created datetime DEFAULT '0000-00-00 00:00:00' NOT NULL,
|
|
||||||
discount_amount double DEFAULT 0 NOT NULL,
|
|
||||||
PRIMARY KEY (order_id, coupon_id),
|
|
||||||
KEY coupon_id (coupon_id),
|
|
||||||
KEY date_created (date_created)
|
|
||||||
) $collate;
|
|
||||||
CREATE TABLE {$wpdb->prefix}wc_admin_notes (
|
|
||||||
note_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
|
||||||
name varchar(255) NOT NULL,
|
|
||||||
type varchar(20) NOT NULL,
|
|
||||||
locale varchar(20) NOT NULL,
|
|
||||||
title longtext NOT NULL,
|
|
||||||
content longtext NOT NULL,
|
|
||||||
icon varchar(200) NOT NULL,
|
|
||||||
content_data longtext NULL default null,
|
|
||||||
status varchar(200) NOT NULL,
|
|
||||||
source varchar(200) NOT NULL,
|
|
||||||
date_created datetime NOT NULL default '0000-00-00 00:00:00',
|
|
||||||
date_reminder datetime NULL default null,
|
|
||||||
PRIMARY KEY (note_id)
|
|
||||||
) $collate;
|
|
||||||
CREATE TABLE {$wpdb->prefix}wc_admin_note_actions (
|
|
||||||
action_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
|
||||||
note_id BIGINT UNSIGNED NOT NULL,
|
|
||||||
name varchar(255) NOT NULL,
|
|
||||||
label varchar(255) NOT NULL,
|
|
||||||
query longtext NOT NULL,
|
|
||||||
PRIMARY KEY (action_id),
|
|
||||||
KEY note_id (note_id)
|
|
||||||
) $collate;
|
|
||||||
CREATE TABLE {$wpdb->prefix}wc_customer_lookup (
|
|
||||||
customer_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
|
||||||
user_id BIGINT UNSIGNED DEFAULT NULL,
|
|
||||||
username varchar(60) DEFAULT '' NOT NULL,
|
|
||||||
first_name varchar(255) NOT NULL,
|
|
||||||
last_name varchar(255) NOT NULL,
|
|
||||||
email varchar(100) NOT NULL,
|
|
||||||
date_last_active timestamp NULL default null,
|
|
||||||
date_registered timestamp NULL default null,
|
|
||||||
country char(2) DEFAULT '' NOT NULL,
|
|
||||||
postcode varchar(20) DEFAULT '' NOT NULL,
|
|
||||||
city varchar(100) DEFAULT '' NOT NULL,
|
|
||||||
PRIMARY KEY (customer_id),
|
|
||||||
UNIQUE KEY user_id (user_id),
|
|
||||||
KEY email (email)
|
|
||||||
) $collate;
|
|
||||||
";
|
|
||||||
|
|
||||||
return $tables;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create database tables.
|
|
||||||
*/
|
|
||||||
public static function create_db_tables() {
|
|
||||||
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
|
|
||||||
|
|
||||||
dbDelta( self::get_schema() );
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Install plugin.
|
|
||||||
*/
|
|
||||||
public static function install() {
|
|
||||||
// Create tables.
|
|
||||||
self::create_db_tables();
|
|
||||||
|
|
||||||
// Initialize report tables.
|
|
||||||
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
|
* Add the currency symbol (in addition to currency code) to each Order
|
||||||
* object in REST API responses. For use in formatCurrency().
|
* object in REST API responses. For use in formatCurrency().
|
||||||
|
|
|
@ -0,0 +1,264 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Installation related functions and actions.
|
||||||
|
*
|
||||||
|
* @package WooCommerce Admin/Classes
|
||||||
|
*/
|
||||||
|
|
||||||
|
defined( 'ABSPATH' ) || exit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WC_Admin_Install Class.
|
||||||
|
*/
|
||||||
|
class WC_Admin_Install {
|
||||||
|
/**
|
||||||
|
* Plugin version.
|
||||||
|
*
|
||||||
|
* @TODO: get this dynamically?
|
||||||
|
*/
|
||||||
|
const VERSION_NUMBER = '0.6.0';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plugin version option name.
|
||||||
|
*/
|
||||||
|
const VERSION_OPTION = 'wc_admin_version';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DB updates and callbacks that need to be run per version.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
private static $db_updates = array();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook in tabs.
|
||||||
|
*/
|
||||||
|
public static function init() {
|
||||||
|
add_action( 'init', array( __CLASS__, 'check_version' ), 5 );
|
||||||
|
add_filter( 'wpmu_drop_tables', array( __CLASS__, 'wpmu_drop_tables' ) );
|
||||||
|
|
||||||
|
// Add wc-admin report tables to list of WooCommerce tables.
|
||||||
|
add_filter( 'woocommerce_install_get_tables', array( __CLASS__, 'add_tables' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check WC Admin version and run the updater is required.
|
||||||
|
*
|
||||||
|
* This check is done on all requests and runs if the versions do not match.
|
||||||
|
*/
|
||||||
|
public static function check_version() {
|
||||||
|
if (
|
||||||
|
! defined( 'IFRAME_REQUEST' ) &&
|
||||||
|
version_compare( get_option( self::VERSION_OPTION ), self::VERSION_NUMBER, '<' )
|
||||||
|
) {
|
||||||
|
self::install();
|
||||||
|
do_action( 'wc_admin_updated' );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Install WC Admin.
|
||||||
|
*/
|
||||||
|
public static function install() {
|
||||||
|
if ( ! is_blog_installed() ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we are not already running this routine.
|
||||||
|
if ( 'yes' === get_transient( 'wc_admin_installing' ) ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we made it till here nothing is running yet, lets set the transient now.
|
||||||
|
set_transient( 'wc_admin_installing', 'yes', MINUTE_IN_SECONDS * 10 );
|
||||||
|
wc_maybe_define_constant( 'WC_ADMIN_INSTALLING', true );
|
||||||
|
|
||||||
|
self::create_tables();
|
||||||
|
WC_Admin_Reports_Sync::regenerate_report_data();
|
||||||
|
self::update_wc_admin_version();
|
||||||
|
|
||||||
|
delete_transient( 'wc_admin_installing' );
|
||||||
|
|
||||||
|
do_action( 'wc_admin_installed' );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get database schema.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
private static function get_schema() {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
if ( $wpdb->has_cap( 'collation' ) ) {
|
||||||
|
$collate = $wpdb->get_charset_collate();
|
||||||
|
}
|
||||||
|
|
||||||
|
$tables = "
|
||||||
|
CREATE TABLE {$wpdb->prefix}wc_order_stats (
|
||||||
|
order_id bigint(20) unsigned NOT NULL,
|
||||||
|
date_created timestamp DEFAULT '0000-00-00 00:00:00' NOT NULL,
|
||||||
|
num_items_sold int(11) UNSIGNED DEFAULT 0 NOT NULL,
|
||||||
|
gross_total double DEFAULT 0 NOT NULL,
|
||||||
|
coupon_total double DEFAULT 0 NOT NULL,
|
||||||
|
refund_total double DEFAULT 0 NOT NULL,
|
||||||
|
tax_total double DEFAULT 0 NOT NULL,
|
||||||
|
shipping_total double DEFAULT 0 NOT NULL,
|
||||||
|
net_total double DEFAULT 0 NOT NULL,
|
||||||
|
returning_customer boolean DEFAULT 0 NOT NULL,
|
||||||
|
status varchar(200) NOT NULL,
|
||||||
|
customer_id BIGINT UNSIGNED NOT NULL,
|
||||||
|
PRIMARY KEY (order_id),
|
||||||
|
KEY date_created (date_created),
|
||||||
|
KEY customer_id (customer_id),
|
||||||
|
KEY status (status)
|
||||||
|
) $collate;
|
||||||
|
CREATE TABLE {$wpdb->prefix}wc_order_product_lookup (
|
||||||
|
order_item_id BIGINT UNSIGNED NOT NULL,
|
||||||
|
order_id BIGINT UNSIGNED NOT NULL,
|
||||||
|
product_id BIGINT UNSIGNED NOT NULL,
|
||||||
|
variation_id BIGINT UNSIGNED NOT NULL,
|
||||||
|
customer_id BIGINT UNSIGNED NULL,
|
||||||
|
date_created timestamp DEFAULT '0000-00-00 00:00:00' NOT NULL,
|
||||||
|
product_qty INT UNSIGNED NOT NULL,
|
||||||
|
product_net_revenue double DEFAULT 0 NOT NULL,
|
||||||
|
product_gross_revenue double DEFAULT 0 NOT NULL,
|
||||||
|
coupon_amount double DEFAULT 0 NOT NULL,
|
||||||
|
tax_amount double DEFAULT 0 NOT NULL,
|
||||||
|
shipping_amount double DEFAULT 0 NOT NULL,
|
||||||
|
shipping_tax_amount double DEFAULT 0 NOT NULL,
|
||||||
|
refund_amount double DEFAULT 0 NOT NULL,
|
||||||
|
PRIMARY KEY (order_item_id),
|
||||||
|
KEY order_id (order_id),
|
||||||
|
KEY product_id (product_id),
|
||||||
|
KEY customer_id (customer_id),
|
||||||
|
KEY date_created (date_created)
|
||||||
|
) $collate;
|
||||||
|
CREATE TABLE {$wpdb->prefix}wc_order_tax_lookup (
|
||||||
|
order_id BIGINT UNSIGNED NOT NULL,
|
||||||
|
tax_rate_id BIGINT UNSIGNED NOT NULL,
|
||||||
|
date_created timestamp DEFAULT '0000-00-00 00:00:00' NOT NULL,
|
||||||
|
shipping_tax double DEFAULT 0 NOT NULL,
|
||||||
|
order_tax double DEFAULT 0 NOT NULL,
|
||||||
|
total_tax double DEFAULT 0 NOT NULL,
|
||||||
|
PRIMARY KEY (order_id, tax_rate_id),
|
||||||
|
KEY tax_rate_id (tax_rate_id),
|
||||||
|
KEY date_created (date_created)
|
||||||
|
) $collate;
|
||||||
|
CREATE TABLE {$wpdb->prefix}wc_order_coupon_lookup (
|
||||||
|
order_id BIGINT UNSIGNED NOT NULL,
|
||||||
|
coupon_id BIGINT UNSIGNED NOT NULL,
|
||||||
|
date_created timestamp DEFAULT '0000-00-00 00:00:00' NOT NULL,
|
||||||
|
discount_amount double DEFAULT 0 NOT NULL,
|
||||||
|
PRIMARY KEY (order_id, coupon_id),
|
||||||
|
KEY coupon_id (coupon_id),
|
||||||
|
KEY date_created (date_created)
|
||||||
|
) $collate;
|
||||||
|
CREATE TABLE {$wpdb->prefix}wc_admin_notes (
|
||||||
|
note_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
|
name varchar(255) NOT NULL,
|
||||||
|
type varchar(20) NOT NULL,
|
||||||
|
locale varchar(20) NOT NULL,
|
||||||
|
title longtext NOT NULL,
|
||||||
|
content longtext NOT NULL,
|
||||||
|
icon varchar(200) NOT NULL,
|
||||||
|
content_data longtext NULL default null,
|
||||||
|
status varchar(200) NOT NULL,
|
||||||
|
source varchar(200) NOT NULL,
|
||||||
|
date_created datetime NOT NULL default '0000-00-00 00:00:00',
|
||||||
|
date_reminder datetime NULL default null,
|
||||||
|
PRIMARY KEY (note_id)
|
||||||
|
) $collate;
|
||||||
|
CREATE TABLE {$wpdb->prefix}wc_admin_note_actions (
|
||||||
|
action_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
|
note_id BIGINT UNSIGNED NOT NULL,
|
||||||
|
name varchar(255) NOT NULL,
|
||||||
|
label varchar(255) NOT NULL,
|
||||||
|
query longtext NOT NULL,
|
||||||
|
PRIMARY KEY (action_id),
|
||||||
|
KEY note_id (note_id)
|
||||||
|
) $collate;
|
||||||
|
CREATE TABLE {$wpdb->prefix}wc_customer_lookup (
|
||||||
|
customer_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
|
user_id BIGINT UNSIGNED DEFAULT NULL,
|
||||||
|
username varchar(60) DEFAULT '' NOT NULL,
|
||||||
|
first_name varchar(255) NOT NULL,
|
||||||
|
last_name varchar(255) NOT NULL,
|
||||||
|
email varchar(100) NOT NULL,
|
||||||
|
date_last_active timestamp NULL default null,
|
||||||
|
date_registered timestamp NULL default null,
|
||||||
|
country char(2) DEFAULT '' NOT NULL,
|
||||||
|
postcode varchar(20) DEFAULT '' NOT NULL,
|
||||||
|
city varchar(100) DEFAULT '' NOT NULL,
|
||||||
|
PRIMARY KEY (customer_id),
|
||||||
|
UNIQUE KEY user_id (user_id),
|
||||||
|
KEY email (email)
|
||||||
|
) $collate;
|
||||||
|
";
|
||||||
|
|
||||||
|
return $tables;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create database tables.
|
||||||
|
*/
|
||||||
|
public static function create_tables() {
|
||||||
|
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
|
||||||
|
|
||||||
|
dbDelta( self::get_schema() );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a list of tables. Used to make sure all WC Admin tables are dropped
|
||||||
|
* when uninstalling the plugin in a single site or multi site environment.
|
||||||
|
*
|
||||||
|
* @return array WC tables.
|
||||||
|
*/
|
||||||
|
public static function get_tables() {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
return array(
|
||||||
|
"{$wpdb->prefix}wc_order_stats",
|
||||||
|
"{$wpdb->prefix}wc_order_product_lookup",
|
||||||
|
"{$wpdb->prefix}wc_order_tax_lookup",
|
||||||
|
"{$wpdb->prefix}wc_order_coupon_lookup",
|
||||||
|
"{$wpdb->prefix}wc_admin_notes",
|
||||||
|
"{$wpdb->prefix}wc_admin_note_actions",
|
||||||
|
"{$wpdb->prefix}wc_customer_lookup",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds new tables.
|
||||||
|
*
|
||||||
|
* @param array $wc_tables List of WooCommerce tables.
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public static function add_tables( $wc_tables ) {
|
||||||
|
return array_merge(
|
||||||
|
$wc_tables,
|
||||||
|
self::get_tables()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uninstall tables when MU blog is deleted.
|
||||||
|
*
|
||||||
|
* @param array $tables List of tables that will be deleted by WP.
|
||||||
|
*
|
||||||
|
* @return string[]
|
||||||
|
*/
|
||||||
|
public static function wpmu_drop_tables( $tables ) {
|
||||||
|
return array_merge( $tables, self::get_tables() );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update WC Admin version to current.
|
||||||
|
*/
|
||||||
|
private static function update_wc_admin_version() {
|
||||||
|
delete_option( self::VERSION_OPTION );
|
||||||
|
add_option( self::VERSION_OPTION, self::VERSION_NUMBER );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
WC_Admin_Install::init();
|
|
@ -94,8 +94,8 @@ class WC_Admin_Reports_Segmenting {
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ( $segments_db_result as $segment_data ) {
|
foreach ( $segments_db_result as $segment_data ) {
|
||||||
$segment_id = $segment_data[ $segment_dimension ];
|
$segment_id = $segment_data[ $segment_dimension ];
|
||||||
$segment_labels = $this->get_segment_labels();
|
$segment_labels = $this->get_segment_labels();
|
||||||
unset( $segment_data[ $segment_dimension ] );
|
unset( $segment_data[ $segment_dimension ] );
|
||||||
$segment_datum = array(
|
$segment_datum = array(
|
||||||
'segment_id' => $segment_id,
|
'segment_id' => $segment_id,
|
||||||
|
@ -208,7 +208,7 @@ class WC_Admin_Reports_Segmenting {
|
||||||
*/
|
*/
|
||||||
protected function merge_segment_intervals_results( $segment_dimension, $result1, $result2 ) {
|
protected function merge_segment_intervals_results( $segment_dimension, $result1, $result2 ) {
|
||||||
$result_segments = array();
|
$result_segments = array();
|
||||||
$segment_labels = $this->get_segment_labels();
|
$segment_labels = $this->get_segment_labels();
|
||||||
|
|
||||||
foreach ( $result1 as $segment_data ) {
|
foreach ( $result1 as $segment_data ) {
|
||||||
$time_interval = $segment_data['time_interval'];
|
$time_interval = $segment_data['time_interval'];
|
||||||
|
@ -317,8 +317,8 @@ class WC_Admin_Reports_Segmenting {
|
||||||
|
|
||||||
$segment_objects = wc_get_products( $args );
|
$segment_objects = wc_get_products( $args );
|
||||||
foreach ( $segment_objects as $segment ) {
|
foreach ( $segment_objects as $segment ) {
|
||||||
$id = $segment->get_id();
|
$id = $segment->get_id();
|
||||||
$segments[] = $id;
|
$segments[] = $id;
|
||||||
$segment_labels[ $id ] = $segment->get_name();
|
$segment_labels[ $id ] = $segment->get_name();
|
||||||
}
|
}
|
||||||
} elseif ( 'variation' === $this->query_args['segmentby'] ) {
|
} elseif ( 'variation' === $this->query_args['segmentby'] ) {
|
||||||
|
@ -342,17 +342,24 @@ class WC_Admin_Reports_Segmenting {
|
||||||
$segment_objects = wc_get_products( $args );
|
$segment_objects = wc_get_products( $args );
|
||||||
|
|
||||||
foreach ( $segment_objects as $segment ) {
|
foreach ( $segment_objects as $segment ) {
|
||||||
$id = $segment->get_id();
|
$id = $segment->get_id();
|
||||||
$segments[] = $id;
|
$segments[] = $id;
|
||||||
$segment_labels[ $id ] = $segment->get_name();
|
$segment_labels[ $id ] = $segment->get_name();
|
||||||
}
|
}
|
||||||
} elseif ( 'category' === $this->query_args['segmentby'] ) {
|
} elseif ( 'category' === $this->query_args['segmentby'] ) {
|
||||||
$categories = get_categories(
|
$args = array(
|
||||||
array(
|
'taxonomy' => 'product_cat',
|
||||||
'taxonomy' => 'product_cat',
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
$segments = wp_list_pluck( $categories, 'cat_ID' );
|
|
||||||
|
if ( isset( $this->query_args['categories'] ) ) {
|
||||||
|
$args['include'] = $this->query_args['categories'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$categories = get_categories( $args );
|
||||||
|
|
||||||
|
$segments = wp_list_pluck( $categories, 'cat_ID' );
|
||||||
|
$segment_labels = wp_list_pluck( $categories, 'name', 'cat_ID' );
|
||||||
|
|
||||||
} elseif ( 'coupon' === $this->query_args['segmentby'] ) {
|
} elseif ( 'coupon' === $this->query_args['segmentby'] ) {
|
||||||
// @todo Switch to a non-direct-SQL way to get all coupons?
|
// @todo Switch to a non-direct-SQL way to get all coupons?
|
||||||
// @todo These are only currently existing coupons, but we should add also deleted ones, if they have been used at least once.
|
// @todo These are only currently existing coupons, but we should add also deleted ones, if they have been used at least once.
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Class for stock stats report querying
|
||||||
|
*
|
||||||
|
* $report = new WC_Admin_Reports_Stock__Stats_Query();
|
||||||
|
* $mydata = $report->get_data();
|
||||||
|
*
|
||||||
|
* @package WooCommerce Admin/Classes
|
||||||
|
*/
|
||||||
|
|
||||||
|
defined( 'ABSPATH' ) || exit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WC_Admin_Reports_Stock_Stats_Query
|
||||||
|
*/
|
||||||
|
class WC_Admin_Reports_Stock_Stats_Query extends WC_Admin_Reports_Query {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get product data based on the current query vars.
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function get_data() {
|
||||||
|
$data_store = WC_Data_Store::load( 'report-stock-stats' );
|
||||||
|
$results = $data_store->get_data();
|
||||||
|
return apply_filters( 'woocommerce_reports_stock_stats_query', $results );
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,411 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Report table sync related functions and actions.
|
||||||
|
*
|
||||||
|
* @package WooCommerce Admin/Classes
|
||||||
|
*/
|
||||||
|
|
||||||
|
defined( 'ABSPATH' ) || exit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WC_Admin_Reports_Sync Class.
|
||||||
|
*/
|
||||||
|
class WC_Admin_Reports_Sync {
|
||||||
|
/**
|
||||||
|
* 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';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Action hook for processing a batch of orders.
|
||||||
|
*/
|
||||||
|
const SINGLE_ORDER_ACTION = 'wc-admin_process_order';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Queue instance.
|
||||||
|
*
|
||||||
|
* @var WC_Queue_Interface
|
||||||
|
*/
|
||||||
|
protected static $queue = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook in sync methods.
|
||||||
|
*/
|
||||||
|
public static function init() {
|
||||||
|
// Add report regeneration to tools menu.
|
||||||
|
add_filter( 'woocommerce_debug_tools', array( __CLASS__, 'add_regenerate_tool' ) );
|
||||||
|
|
||||||
|
// Initialize syncing hooks.
|
||||||
|
add_action( 'wp_loaded', array( __CLASS__, 'orders_lookup_update_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, 3 );
|
||||||
|
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_action( self::SINGLE_ORDER_ACTION, array( __CLASS__, 'orders_lookup_process_order' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Regenerate data for reports.
|
||||||
|
*/
|
||||||
|
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_batch_init();
|
||||||
|
// Queue orders lookup to occur after customers lookup generation is done.
|
||||||
|
self::queue_dependent_action( self::ORDERS_LOOKUP_BATCH_INIT, array(), self::CUSTOMERS_BATCH_ACTION );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds regenerate tool.
|
||||||
|
*
|
||||||
|
* @param array $tools List of tools.
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public static function add_regenerate_tool( $tools ) {
|
||||||
|
return array_merge(
|
||||||
|
$tools,
|
||||||
|
array(
|
||||||
|
'rebuild_stats' => array(
|
||||||
|
'name' => __( 'Rebuild reports data', 'wc-admin' ),
|
||||||
|
'button' => __( 'Rebuild reports', 'wc-admin' ),
|
||||||
|
'desc' => __( 'This tool will rebuild all of the information used by the reports.', 'wc-admin' ),
|
||||||
|
'callback' => array( __CLASS__, 'regenerate_report_data' ),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schedule an action to process a single Order.
|
||||||
|
*
|
||||||
|
* @param int $order_id Order ID.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function schedule_single_order_process( $order_id ) {
|
||||||
|
if ( 'shop_order' !== get_post_type( $order_id ) ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This can get called multiple times for a single order, so we look
|
||||||
|
// for existing pending jobs for the same order to avoid duplicating efforts.
|
||||||
|
$existing_jobs = self::queue()->search(
|
||||||
|
array(
|
||||||
|
'status' => 'pending',
|
||||||
|
'per_page' => 1,
|
||||||
|
'claimed' => false,
|
||||||
|
'search' => "[{$order_id}]",
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if ( $existing_jobs ) {
|
||||||
|
$existing_job = current( $existing_jobs );
|
||||||
|
|
||||||
|
// Bail out if there's a pending single order action, or a pending dependent action.
|
||||||
|
if (
|
||||||
|
( self::SINGLE_ORDER_ACTION === $existing_job->get_hook() ) ||
|
||||||
|
(
|
||||||
|
self::QUEUE_DEPEDENT_ACTION === $existing_job->get_hook() &&
|
||||||
|
in_array( self::SINGLE_ORDER_ACTION, $existing_job->get_args() )
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We want to ensure that customer lookup updates are scheduled before order updates.
|
||||||
|
self::queue_dependent_action( self::SINGLE_ORDER_ACTION, array( $order_id ), self::CUSTOMERS_BATCH_ACTION );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attach order lookup update hooks.
|
||||||
|
*/
|
||||||
|
public static function orders_lookup_update_init() {
|
||||||
|
// Activate WC_Order extension.
|
||||||
|
WC_Admin_Order::add_filters();
|
||||||
|
|
||||||
|
add_action( 'save_post_shop_order', array( __CLASS__, 'schedule_single_order_process' ) );
|
||||||
|
add_action( 'woocommerce_order_refunded', array( __CLASS__, 'schedule_single_order_process' ) );
|
||||||
|
|
||||||
|
WC_Admin_Reports_Orders_Stats_Data_Store::init();
|
||||||
|
WC_Admin_Reports_Customers_Data_Store::init();
|
||||||
|
WC_Admin_Reports_Coupons_Data_Store::init();
|
||||||
|
WC_Admin_Reports_Products_Data_Store::init();
|
||||||
|
WC_Admin_Reports_Taxes_Data_Store::init();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Init order/product lookup tables update (in batches).
|
||||||
|
*/
|
||||||
|
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();
|
||||||
|
|
||||||
|
if ( 0 === $result->total ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$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 ) {
|
||||||
|
self::orders_lookup_process_order( $order_id );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process a single order to update lookup tables for.
|
||||||
|
* If an error is encountered in one of the updates, a retry action is scheduled.
|
||||||
|
*
|
||||||
|
* @param int $order_id Order ID.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function orders_lookup_process_order( $order_id ) {
|
||||||
|
$result = array_sum(
|
||||||
|
array(
|
||||||
|
WC_Admin_Reports_Orders_Stats_Data_Store::sync_order( $order_id ),
|
||||||
|
WC_Admin_Reports_Products_Data_Store::sync_order_products( $order_id ),
|
||||||
|
WC_Admin_Reports_Coupons_Data_Store::sync_order_coupons( $order_id ),
|
||||||
|
WC_Admin_Reports_Taxes_Data_Store::sync_order_taxes( $order_id ),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// If all updates were either skipped or successful, we're done.
|
||||||
|
// The update methods return -1 for skip, or a boolean success indicator.
|
||||||
|
if ( 4 === absint( $result ) ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise assume an error occurred and reschedule.
|
||||||
|
self::schedule_single_order_process( $order_id );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the batch size for regenerating reports.
|
||||||
|
* Note: can differ per batch action.
|
||||||
|
*
|
||||||
|
* @param string $action Single batch action name.
|
||||||
|
* @return int Batch size.
|
||||||
|
*/
|
||||||
|
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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 array $action_args Action arguments.
|
||||||
|
* @param string $prerequisite_action Prerequisite action.
|
||||||
|
*/
|
||||||
|
public static function queue_dependent_action( $action, $action_args, $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.
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
$next_job_schedule = null;
|
||||||
|
$blocking_job_hook = null;
|
||||||
|
|
||||||
|
if ( $blocking_jobs ) {
|
||||||
|
$blocking_job = current( $blocking_jobs );
|
||||||
|
$blocking_job_hook = $blocking_job->get_hook();
|
||||||
|
$next_job_schedule = $blocking_job->get_schedule()->next();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Eliminate the false positive scenario where the blocking job is
|
||||||
|
// actually another queued dependent action awaiting the same prerequisite.
|
||||||
|
// Also, ensure that the next schedule is a DateTime (it can be null).
|
||||||
|
if (
|
||||||
|
is_a( $next_job_schedule, 'DateTime' ) &&
|
||||||
|
( self::QUEUE_DEPEDENT_ACTION !== $blocking_job_hook )
|
||||||
|
) {
|
||||||
|
self::queue()->schedule_single(
|
||||||
|
$next_job_schedule->getTimestamp() + 5,
|
||||||
|
self::QUEUE_DEPEDENT_ACTION,
|
||||||
|
array( $action, $action_args, $prerequisite_action )
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
self::queue()->schedule_single( time() + 5, $action, $action_args );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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();
|
||||||
|
|
||||||
|
if ( 0 === $total_customers ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$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 ) {
|
||||||
|
// @todo Schedule single customer update if this fails?
|
||||||
|
WC_Admin_Reports_Customers_Data_Store::update_registered_customer( $customer_id );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
WC_Admin_Reports_Sync::init();
|
|
@ -25,7 +25,7 @@ class WC_Admin_Reports_Customers_Data_Store extends WC_Admin_Reports_Data_Store
|
||||||
* @var array
|
* @var array
|
||||||
*/
|
*/
|
||||||
protected $column_types = array(
|
protected $column_types = array(
|
||||||
'customer_id' => 'intval',
|
'id' => 'intval',
|
||||||
'user_id' => 'intval',
|
'user_id' => 'intval',
|
||||||
'orders_count' => 'intval',
|
'orders_count' => 'intval',
|
||||||
'total_spend' => 'floatval',
|
'total_spend' => 'floatval',
|
||||||
|
@ -38,7 +38,7 @@ class WC_Admin_Reports_Customers_Data_Store extends WC_Admin_Reports_Data_Store
|
||||||
* @var array
|
* @var array
|
||||||
*/
|
*/
|
||||||
protected $report_columns = array(
|
protected $report_columns = array(
|
||||||
'customer_id' => 'customer_id',
|
'id' => 'customer_id as id',
|
||||||
'user_id' => 'user_id',
|
'user_id' => 'user_id',
|
||||||
'username' => 'username',
|
'username' => 'username',
|
||||||
'name' => "CONCAT_WS( ' ', first_name, last_name ) as name", // @todo What does this mean for RTL?
|
'name' => "CONCAT_WS( ' ', first_name, last_name ) as name", // @todo What does this mean for RTL?
|
||||||
|
@ -60,7 +60,7 @@ class WC_Admin_Reports_Customers_Data_Store extends WC_Admin_Reports_Data_Store
|
||||||
global $wpdb;
|
global $wpdb;
|
||||||
|
|
||||||
// Initialize some report columns that need disambiguation.
|
// Initialize some report columns that need disambiguation.
|
||||||
$this->report_columns['customer_id'] = $wpdb->prefix . self::TABLE_NAME . '.customer_id';
|
$this->report_columns['id'] = $wpdb->prefix . self::TABLE_NAME . '.customer_id as id';
|
||||||
$this->report_columns['date_last_order'] = "MAX( {$wpdb->prefix}wc_order_stats.date_created ) as date_last_order";
|
$this->report_columns['date_last_order'] = "MAX( {$wpdb->prefix}wc_order_stats.date_created ) as date_last_order";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -230,8 +230,15 @@ class WC_Admin_Reports_Customers_Data_Store extends WC_Admin_Reports_Data_Store
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( ! empty( $query_args['name'] ) ) {
|
if ( ! empty( $query_args['search'] ) ) {
|
||||||
$where_clauses[] = $wpdb->prepare( "CONCAT_WS( ' ', first_name, last_name ) = %s", $query_args['name'] );
|
$name_like = '%' . $wpdb->esc_like( $query_args['search'] ) . '%';
|
||||||
|
$where_clauses[] = $wpdb->prepare( "CONCAT_WS( ' ', first_name, last_name ) LIKE %s", $name_like );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow a list of customer IDs to be specified.
|
||||||
|
if ( ! empty( $query_args['customers'] ) ) {
|
||||||
|
$included_customers = implode( ',', $query_args['customers'] );
|
||||||
|
$where_clauses[] = "{$customer_lookup_table}.customer_id IN ({$included_customers})";
|
||||||
}
|
}
|
||||||
|
|
||||||
$numeric_params = array(
|
$numeric_params = array(
|
||||||
|
@ -253,17 +260,18 @@ class WC_Admin_Reports_Customers_Data_Store extends WC_Admin_Reports_Data_Store
|
||||||
$subclauses = array();
|
$subclauses = array();
|
||||||
$min_param = $numeric_param . '_min';
|
$min_param = $numeric_param . '_min';
|
||||||
$max_param = $numeric_param . '_max';
|
$max_param = $numeric_param . '_max';
|
||||||
|
$or_equal = isset( $query_args[ $min_param ] ) && isset( $query_args[ $max_param ] ) ? '=' : '';
|
||||||
|
|
||||||
if ( isset( $query_args[ $min_param ] ) ) {
|
if ( isset( $query_args[ $min_param ] ) ) {
|
||||||
$subclauses[] = $wpdb->prepare(
|
$subclauses[] = $wpdb->prepare(
|
||||||
"{$param_info['column']} >= {$param_info['format']}",
|
"{$param_info['column']} >{$or_equal} {$param_info['format']}",
|
||||||
$query_args[ $min_param ]
|
$query_args[ $min_param ]
|
||||||
); // WPCS: unprepared SQL ok.
|
); // WPCS: unprepared SQL ok.
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( isset( $query_args[ $max_param ] ) ) {
|
if ( isset( $query_args[ $max_param ] ) ) {
|
||||||
$subclauses[] = $wpdb->prepare(
|
$subclauses[] = $wpdb->prepare(
|
||||||
"{$param_info['column']} <= {$param_info['format']}",
|
"{$param_info['column']} <{$or_equal} {$param_info['format']}",
|
||||||
$query_args[ $max_param ]
|
$query_args[ $max_param ]
|
||||||
); // WPCS: unprepared SQL ok.
|
); // WPCS: unprepared SQL ok.
|
||||||
}
|
}
|
||||||
|
@ -517,6 +525,30 @@ class WC_Admin_Reports_Customers_Data_Store extends WC_Admin_Reports_Data_Store
|
||||||
return $customer_id ? (int) $customer_id : false;
|
return $customer_id ? (int) $customer_id : false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the oldest orders made by a customer.
|
||||||
|
*
|
||||||
|
* @param int $customer_id Customer ID.
|
||||||
|
* @return array Orders.
|
||||||
|
*/
|
||||||
|
public static function get_oldest_orders( $customer_id ) {
|
||||||
|
global $wpdb;
|
||||||
|
$orders_table = $wpdb->prefix . 'wc_order_stats';
|
||||||
|
$excluded_statuses = array_map( array( __CLASS__, 'normalize_order_status' ), self::get_excluded_report_order_statuses() );
|
||||||
|
$excluded_statuses_condition = '';
|
||||||
|
if ( ! empty( $excluded_statuses ) ) {
|
||||||
|
$excluded_statuses_str = implode( "','", $excluded_statuses );
|
||||||
|
$excluded_statuses_condition = "AND status NOT IN ('{$excluded_statuses_str}')";
|
||||||
|
}
|
||||||
|
|
||||||
|
return $wpdb->get_results(
|
||||||
|
$wpdb->prepare(
|
||||||
|
"SELECT order_id, date_created FROM {$orders_table} WHERE customer_id = %d {$excluded_statuses_condition} ORDER BY date_created, order_id ASC LIMIT 2",
|
||||||
|
$customer_id
|
||||||
|
)
|
||||||
|
); // WPCS: unprepared SQL ok.
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the database with customer data.
|
* Update the database with customer data.
|
||||||
*
|
*
|
||||||
|
|
|
@ -184,43 +184,17 @@ class WC_Admin_Reports_Downloads_Stats_Data_Store extends WC_Admin_Reports_Downl
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sorts intervals according to user's request.
|
* Normalizes order_by clause to match to SQL query.
|
||||||
*
|
*
|
||||||
* They are pre-sorted in SQL, but after adding gaps, they need to be sorted including the added ones.
|
* @param string $order_by Order by option requeste by user.
|
||||||
*
|
|
||||||
* @param stdClass $data Data object, must contain an array under $data->intervals.
|
|
||||||
* @param string $sort_by Ordering property.
|
|
||||||
* @param string $direction DESC/ASC.
|
|
||||||
*/
|
|
||||||
protected function sort_intervals( &$data, $sort_by, $direction ) {
|
|
||||||
if ( 'date' === $sort_by ) {
|
|
||||||
$this->order_by = 'time_interval';
|
|
||||||
} else {
|
|
||||||
$this->order_by = $sort_by;
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->order = $direction;
|
|
||||||
usort( $data->intervals, array( $this, 'interval_cmp' ) );
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Compares two report data objects by pre-defined object property and ASC/DESC ordering.
|
|
||||||
*
|
|
||||||
* @param stdClass $a Object a.
|
|
||||||
* @param stdClass $b Object b.
|
|
||||||
* @return string
|
* @return string
|
||||||
*/
|
*/
|
||||||
protected function interval_cmp( $a, $b ) {
|
protected function normalize_order_by( $order_by ) {
|
||||||
if ( '' === $this->order_by || '' === $this->order ) {
|
if ( 'date' === $order_by ) {
|
||||||
return 0;
|
return 'time_interval';
|
||||||
}
|
|
||||||
if ( $a[ $this->order_by ] === $b[ $this->order_by ] ) {
|
|
||||||
return 0;
|
|
||||||
} 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 ] ) {
|
|
||||||
return strtolower( $this->order ) === 'desc' ? 1 : -1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return $order_by;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -227,6 +227,8 @@ class WC_Admin_Reports_Orders_Data_Store extends WC_Admin_Reports_Data_Store imp
|
||||||
if ( 'date' === $order_by ) {
|
if ( 'date' === $order_by ) {
|
||||||
return 'date_created';
|
return 'date_created';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return $order_by;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -313,7 +315,7 @@ class WC_Admin_Reports_Orders_Data_Store extends WC_Admin_Reports_Data_Store imp
|
||||||
"SELECT order_id, ID as product_id, post_title as product_name, product_qty as product_quantity
|
"SELECT order_id, ID as product_id, post_title as product_name, product_qty as product_quantity
|
||||||
FROM {$wpdb->prefix}posts
|
FROM {$wpdb->prefix}posts
|
||||||
JOIN {$order_product_lookup_table} ON {$order_product_lookup_table}.product_id = {$wpdb->prefix}posts.ID
|
JOIN {$order_product_lookup_table} ON {$order_product_lookup_table}.product_id = {$wpdb->prefix}posts.ID
|
||||||
WHERE
|
WHERE
|
||||||
order_id IN ({$included_order_ids})
|
order_id IN ({$included_order_ids})
|
||||||
",
|
",
|
||||||
ARRAY_A
|
ARRAY_A
|
||||||
|
@ -337,7 +339,7 @@ class WC_Admin_Reports_Orders_Data_Store extends WC_Admin_Reports_Data_Store imp
|
||||||
"SELECT order_id, coupon_id, post_title as coupon_code
|
"SELECT order_id, coupon_id, post_title as coupon_code
|
||||||
FROM {$wpdb->prefix}posts
|
FROM {$wpdb->prefix}posts
|
||||||
JOIN {$order_coupon_lookup_table} ON {$order_coupon_lookup_table}.coupon_id = {$wpdb->prefix}posts.ID
|
JOIN {$order_coupon_lookup_table} ON {$order_coupon_lookup_table}.coupon_id = {$wpdb->prefix}posts.ID
|
||||||
WHERE
|
WHERE
|
||||||
order_id IN ({$included_order_ids})
|
order_id IN ({$included_order_ids})
|
||||||
",
|
",
|
||||||
ARRAY_A
|
ARRAY_A
|
||||||
|
|
|
@ -514,24 +514,64 @@ class WC_Admin_Reports_Orders_Stats_Data_Store extends WC_Admin_Reports_Data_Sto
|
||||||
* @return bool
|
* @return bool
|
||||||
*/
|
*/
|
||||||
protected static function is_returning_customer( $order ) {
|
protected static function is_returning_customer( $order ) {
|
||||||
global $wpdb;
|
$customer_id = WC_Admin_Reports_Customers_Data_Store::get_customer_id_by_user_id( $order->get_user_id() );
|
||||||
$customer_id = WC_Admin_Reports_Customers_Data_Store::get_customer_id_by_user_id( $order->get_user_id() );
|
|
||||||
$orders_stats_table = $wpdb->prefix . self::TABLE_NAME;
|
|
||||||
|
|
||||||
if ( ! $customer_id ) {
|
if ( ! $customer_id ) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
$customer_orders = $wpdb->get_var(
|
$oldest_orders = WC_Admin_Reports_Customers_Data_Store::get_oldest_orders( $customer_id );
|
||||||
|
|
||||||
|
if ( empty( $oldest_orders ) ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$first_order = $oldest_orders[0];
|
||||||
|
$second_order = isset( $oldest_orders[1] ) ? $oldest_orders[1] : false;
|
||||||
|
$excluded_statuses = self::get_excluded_report_order_statuses();
|
||||||
|
|
||||||
|
// Order is older than previous first order.
|
||||||
|
if ( $order->get_date_created() < new WC_DateTime( $first_order->date_created ) &&
|
||||||
|
! in_array( $order->get_status(), $excluded_statuses, true )
|
||||||
|
) {
|
||||||
|
self::set_customer_first_order( $customer_id, $order->get_id() );
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The current order is the oldest known order.
|
||||||
|
$is_first_order = (int) $order->get_id() === (int) $first_order->order_id;
|
||||||
|
// Order date has changed and next oldest is now the first order.
|
||||||
|
$date_change = $second_order &&
|
||||||
|
$order->get_date_created() > new WC_DateTime( $first_order->date_created ) &&
|
||||||
|
new WC_DateTime( $second_order->date_created ) < $order->get_date_created();
|
||||||
|
// Status has changed to an excluded status and next oldest order is now the first order.
|
||||||
|
$status_change = $second_order &&
|
||||||
|
in_array( $order->get_status(), $excluded_statuses, true );
|
||||||
|
if ( $is_first_order && ( $date_change || $status_change ) ) {
|
||||||
|
self::set_customer_first_order( $customer_id, $second_order->order_id );
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int) $order->get_id() !== (int) $first_order->order_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a customer's first order and all others to returning.
|
||||||
|
*
|
||||||
|
* @param int $customer_id Customer ID.
|
||||||
|
* @param int $order_id Order ID.
|
||||||
|
*/
|
||||||
|
protected static function set_customer_first_order( $customer_id, $order_id ) {
|
||||||
|
global $wpdb;
|
||||||
|
$orders_stats_table = $wpdb->prefix . self::TABLE_NAME;
|
||||||
|
|
||||||
|
$wpdb->query(
|
||||||
$wpdb->prepare(
|
$wpdb->prepare(
|
||||||
"SELECT COUNT(*) FROM ${orders_stats_table} WHERE customer_id = %d AND date_created < %s AND order_id != %d",
|
"UPDATE ${orders_stats_table} SET returning_customer = CASE WHEN order_id = %d THEN false ELSE true END WHERE customer_id = %d",
|
||||||
$customer_id,
|
$order_id,
|
||||||
date( 'Y-m-d H:i:s', $order->get_date_created()->getTimestamp() ),
|
$customer_id
|
||||||
$order->get_id()
|
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
return $customer_orders >= 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -0,0 +1,138 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* WC_Admin_Reports_Stock_Stats_Data_Store class file.
|
||||||
|
*
|
||||||
|
* @package WooCommerce Admin/Classes
|
||||||
|
*/
|
||||||
|
|
||||||
|
defined( 'ABSPATH' ) || exit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WC_Reports_Stock_Stats_Data_Store.
|
||||||
|
*/
|
||||||
|
class WC_Admin_Reports_Stock_Stats_Data_Store extends WC_Admin_Reports_Data_Store implements WC_Admin_Reports_Data_Store_Interface {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get stock counts for the whole store.
|
||||||
|
*
|
||||||
|
* @param array $query Not used for the stock stats data store, but needed for the interface.
|
||||||
|
* @return array Array of counts.
|
||||||
|
*/
|
||||||
|
public function get_data( $query ) {
|
||||||
|
$report_data = array();
|
||||||
|
$cache_expire = DAY_IN_SECONDS * 30;
|
||||||
|
$low_stock_transient_name = 'wc_admin_stock_count_lowstock';
|
||||||
|
$low_stock_count = get_transient( $low_stock_transient_name );
|
||||||
|
if ( false === $low_stock_count ) {
|
||||||
|
$low_stock_count = $this->get_low_stock_count();
|
||||||
|
set_transient( $low_stock_transient_name, $low_stock_count, $cache_expire );
|
||||||
|
}
|
||||||
|
$report_data['lowstock'] = $low_stock_count;
|
||||||
|
|
||||||
|
$status_options = wc_get_product_stock_status_options();
|
||||||
|
foreach ( $status_options as $status => $label ) {
|
||||||
|
$transient_name = 'wc_admin_stock_count_' . $status;
|
||||||
|
$count = get_transient( $transient_name );
|
||||||
|
if ( false === $count ) {
|
||||||
|
$count = $this->get_count( $status );
|
||||||
|
set_transient( $transient_name, $count, $cache_expire );
|
||||||
|
}
|
||||||
|
$report_data[ $status ] = $count;
|
||||||
|
}
|
||||||
|
|
||||||
|
$product_count_transient_name = 'wc_admin_product_count';
|
||||||
|
$product_count = get_transient( $product_count_transient_name );
|
||||||
|
if ( false === $product_count ) {
|
||||||
|
$product_count = $this->get_product_count();
|
||||||
|
set_transient( $product_count_transient_name, $product_count, $cache_expire );
|
||||||
|
}
|
||||||
|
$report_data['products'] = $product_count;
|
||||||
|
return $report_data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get low stock count.
|
||||||
|
*
|
||||||
|
* @return int Low stock count.
|
||||||
|
*/
|
||||||
|
private function get_low_stock_count() {
|
||||||
|
$query_args = array();
|
||||||
|
$query_args['post_type'] = array( 'product', 'product_variation' );
|
||||||
|
$low_stock = absint( max( get_option( 'woocommerce_notify_low_stock_amount' ), 1 ) );
|
||||||
|
$no_stock = absint( max( get_option( 'woocommerce_notify_no_stock_amount' ), 0 ) );
|
||||||
|
$query_args['meta_query'] = array( // WPCS: slow query ok.
|
||||||
|
array(
|
||||||
|
'key' => '_manage_stock',
|
||||||
|
'value' => 'yes',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'key' => '_stock',
|
||||||
|
'value' => array( $no_stock, $low_stock ),
|
||||||
|
'compare' => 'BETWEEN',
|
||||||
|
'type' => 'NUMERIC',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'key' => '_stock_status',
|
||||||
|
'value' => 'instock',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
$query = new WP_Query();
|
||||||
|
$query->query( $query_args );
|
||||||
|
return intval( $query->found_posts );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get count for the passed in stock status.
|
||||||
|
*
|
||||||
|
* @param string $status Status slug.
|
||||||
|
* @return int Count.
|
||||||
|
*/
|
||||||
|
private function get_count( $status ) {
|
||||||
|
$query_args = array();
|
||||||
|
$query_args['post_type'] = array( 'product', 'product_variation' );
|
||||||
|
$query_args['meta_query'] = array( // WPCS: slow query ok.
|
||||||
|
array(
|
||||||
|
'key' => '_stock_status',
|
||||||
|
'value' => $status,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
$query = new WP_Query();
|
||||||
|
$query->query( $query_args );
|
||||||
|
return intval( $query->found_posts );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get product count for the store.
|
||||||
|
*
|
||||||
|
* @return int Product count.
|
||||||
|
*/
|
||||||
|
private function get_product_count() {
|
||||||
|
$query_args = array();
|
||||||
|
$query_args['post_type'] = array( 'product', 'product_variation' );
|
||||||
|
$query = new WP_Query();
|
||||||
|
$query->query( $query_args );
|
||||||
|
return intval( $query->found_posts );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear the count cache when products are added or updated, or when
|
||||||
|
* the no/low stock options are changed.
|
||||||
|
*
|
||||||
|
* @param int $id Post/product ID.
|
||||||
|
*/
|
||||||
|
function wc_admin_clear_stock_count_cache( $id ) {
|
||||||
|
delete_transient( 'wc_admin_stock_count_lowstock' );
|
||||||
|
delete_transient( 'wc_admin_product_count' );
|
||||||
|
$status_options = wc_get_product_stock_status_options();
|
||||||
|
foreach ( $status_options as $status => $label ) {
|
||||||
|
delete_transient( 'wc_admin_stock_count_' . $status );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
add_action( 'woocommerce_update_product', 'wc_admin_clear_stock_count_cache' );
|
||||||
|
add_action( 'woocommerce_new_product', 'wc_admin_clear_stock_count_cache' );
|
||||||
|
add_action( 'update_option_woocommerce_notify_low_stock_amount', 'wc_admin_clear_stock_count_cache' );
|
||||||
|
add_action( 'update_option_woocommerce_notify_no_stock_amount', 'wc_admin_clear_stock_count_cache' );
|
|
@ -21,6 +21,10 @@ function wc_admin_is_admin_page() {
|
||||||
* `wc_get_screen_ids` will also return IDs for extensions that have properly registered themselves.
|
* `wc_get_screen_ids` will also return IDs for extensions that have properly registered themselves.
|
||||||
*/
|
*/
|
||||||
function wc_admin_is_embed_enabled_wc_page() {
|
function wc_admin_is_embed_enabled_wc_page() {
|
||||||
|
if ( ! wc_admin_is_feature_enabled( 'activity-panels' ) ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
$screen_id = wc_admin_get_current_screen_id();
|
$screen_id = wc_admin_get_current_screen_id();
|
||||||
if ( ! $screen_id ) {
|
if ( ! $screen_id ) {
|
||||||
return false;
|
return false;
|
||||||
|
@ -66,106 +70,110 @@ function wc_admin_register_page( $options ) {
|
||||||
function wc_admin_register_pages() {
|
function wc_admin_register_pages() {
|
||||||
global $menu, $submenu;
|
global $menu, $submenu;
|
||||||
|
|
||||||
add_submenu_page(
|
if ( wc_admin_is_feature_enabled( 'dashboard' ) ) {
|
||||||
'woocommerce',
|
add_submenu_page(
|
||||||
__( 'WooCommerce Dashboard', 'wc-admin' ),
|
'woocommerce',
|
||||||
__( 'Dashboard', 'wc-admin' ),
|
__( 'WooCommerce Dashboard', 'wc-admin' ),
|
||||||
'manage_options',
|
__( 'Dashboard', 'wc-admin' ),
|
||||||
'wc-admin',
|
'manage_options',
|
||||||
'wc_admin_page'
|
'wc-admin',
|
||||||
);
|
'wc_admin_page'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
add_menu_page(
|
if ( wc_admin_is_feature_enabled( 'analytics' ) ) {
|
||||||
__( 'WooCommerce Analytics', 'wc-admin' ),
|
add_menu_page(
|
||||||
__( 'Analytics', 'wc-admin' ),
|
__( 'WooCommerce Analytics', 'wc-admin' ),
|
||||||
'manage_options',
|
__( 'Analytics', 'wc-admin' ),
|
||||||
'wc-admin#/analytics/revenue',
|
'manage_options',
|
||||||
'wc_admin_page',
|
'wc-admin#/analytics/revenue',
|
||||||
'dashicons-chart-bar',
|
'wc_admin_page',
|
||||||
56 // After WooCommerce & Product menu items.
|
'dashicons-chart-bar',
|
||||||
);
|
56 // After WooCommerce & Product menu items.
|
||||||
|
);
|
||||||
|
|
||||||
wc_admin_register_page(
|
wc_admin_register_page(
|
||||||
array(
|
array(
|
||||||
'title' => __( 'Revenue', 'wc-admin' ),
|
'title' => __( 'Revenue', 'wc-admin' ),
|
||||||
'parent' => '/analytics/revenue',
|
'parent' => '/analytics/revenue',
|
||||||
'path' => '/analytics/revenue',
|
'path' => '/analytics/revenue',
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
wc_admin_register_page(
|
wc_admin_register_page(
|
||||||
array(
|
array(
|
||||||
'title' => __( 'Orders', 'wc-admin' ),
|
'title' => __( 'Orders', 'wc-admin' ),
|
||||||
'parent' => '/analytics/revenue',
|
'parent' => '/analytics/revenue',
|
||||||
'path' => '/analytics/orders',
|
'path' => '/analytics/orders',
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
wc_admin_register_page(
|
wc_admin_register_page(
|
||||||
array(
|
array(
|
||||||
'title' => __( 'Products', 'wc-admin' ),
|
'title' => __( 'Products', 'wc-admin' ),
|
||||||
'parent' => '/analytics/revenue',
|
'parent' => '/analytics/revenue',
|
||||||
'path' => '/analytics/products',
|
'path' => '/analytics/products',
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
wc_admin_register_page(
|
wc_admin_register_page(
|
||||||
array(
|
array(
|
||||||
'title' => __( 'Categories', 'wc-admin' ),
|
'title' => __( 'Categories', 'wc-admin' ),
|
||||||
'parent' => '/analytics/revenue',
|
'parent' => '/analytics/revenue',
|
||||||
'path' => '/analytics/categories',
|
'path' => '/analytics/categories',
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
wc_admin_register_page(
|
wc_admin_register_page(
|
||||||
array(
|
array(
|
||||||
'title' => __( 'Coupons', 'wc-admin' ),
|
'title' => __( 'Coupons', 'wc-admin' ),
|
||||||
'parent' => '/analytics/revenue',
|
'parent' => '/analytics/revenue',
|
||||||
'path' => '/analytics/coupons',
|
'path' => '/analytics/coupons',
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
wc_admin_register_page(
|
wc_admin_register_page(
|
||||||
array(
|
array(
|
||||||
'title' => __( 'Taxes', 'wc-admin' ),
|
'title' => __( 'Taxes', 'wc-admin' ),
|
||||||
'parent' => '/analytics/revenue',
|
'parent' => '/analytics/revenue',
|
||||||
'path' => '/analytics/taxes',
|
'path' => '/analytics/taxes',
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
wc_admin_register_page(
|
wc_admin_register_page(
|
||||||
array(
|
array(
|
||||||
'title' => __( 'Downloads', 'wc-admin' ),
|
'title' => __( 'Downloads', 'wc-admin' ),
|
||||||
'parent' => '/analytics/revenue',
|
'parent' => '/analytics/revenue',
|
||||||
'path' => '/analytics/downloads',
|
'path' => '/analytics/downloads',
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
wc_admin_register_page(
|
wc_admin_register_page(
|
||||||
array(
|
array(
|
||||||
'title' => __( 'Stock', 'wc-admin' ),
|
'title' => __( 'Stock', 'wc-admin' ),
|
||||||
'parent' => '/analytics/revenue',
|
'parent' => '/analytics/revenue',
|
||||||
'path' => '/analytics/stock',
|
'path' => '/analytics/stock',
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
wc_admin_register_page(
|
wc_admin_register_page(
|
||||||
array(
|
array(
|
||||||
'title' => __( 'Customers', 'wc-admin' ),
|
'title' => __( 'Customers', 'wc-admin' ),
|
||||||
'parent' => '/analytics/revenue',
|
'parent' => '/analytics/revenue',
|
||||||
'path' => '/analytics/customers',
|
'path' => '/analytics/customers',
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
wc_admin_register_page(
|
wc_admin_register_page(
|
||||||
array(
|
array(
|
||||||
'title' => __( 'Settings', 'wc-admin' ),
|
'title' => __( 'Settings', 'wc-admin' ),
|
||||||
'parent' => '/analytics/revenue',
|
'parent' => '/analytics/revenue',
|
||||||
'path' => '/analytics/settings',
|
'path' => '/analytics/settings',
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if ( defined( 'WP_DEBUG' ) && WP_DEBUG && defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) {
|
if ( wc_admin_is_feature_enabled( 'devdocs' ) && defined( 'WP_DEBUG' ) && WP_DEBUG && defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) {
|
||||||
wc_admin_register_page(
|
wc_admin_register_page(
|
||||||
array(
|
array(
|
||||||
'title' => 'DevDocs',
|
'title' => 'DevDocs',
|
||||||
|
@ -236,7 +244,7 @@ function wc_admin_enqueue_script() {
|
||||||
add_action( 'admin_enqueue_scripts', 'wc_admin_enqueue_script' );
|
add_action( 'admin_enqueue_scripts', 'wc_admin_enqueue_script' );
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds an admin body class.
|
* Adds body classes to the main wp-admin wrapper, allowing us to better target elements in specific scenarios.
|
||||||
*
|
*
|
||||||
* @param string $admin_body_class Body class to add.
|
* @param string $admin_body_class Body class to add.
|
||||||
*/
|
*/
|
||||||
|
@ -252,6 +260,18 @@ function wc_admin_admin_body_class( $admin_body_class = '' ) {
|
||||||
if ( wc_admin_is_embed_enabled_wc_page() ) {
|
if ( wc_admin_is_embed_enabled_wc_page() ) {
|
||||||
$classes[] = 'woocommerce-embed-page';
|
$classes[] = 'woocommerce-embed-page';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ( function_exists( 'wc_admin_get_feature_config' ) ) {
|
||||||
|
$features = wc_admin_get_feature_config();
|
||||||
|
foreach ( $features as $feature_key => $bool ) {
|
||||||
|
if ( true === $bool ) {
|
||||||
|
$classes[] = sanitize_html_class( 'woocommerce-feature-enabled-' . $feature_key );
|
||||||
|
} else {
|
||||||
|
$classes[] = sanitize_html_class( 'woocommerce-feature-disabled-' . $feature_key );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$admin_body_class = implode( ' ', array_unique( $classes ) );
|
$admin_body_class = implode( ' ', array_unique( $classes ) );
|
||||||
return " $admin_body_class ";
|
return " $admin_body_class ";
|
||||||
}
|
}
|
||||||
|
@ -261,7 +281,7 @@ add_filter( 'admin_body_class', 'wc_admin_admin_body_class' );
|
||||||
* Runs before admin notices action and hides them.
|
* Runs before admin notices action and hides them.
|
||||||
*/
|
*/
|
||||||
function wc_admin_admin_before_notices() {
|
function wc_admin_admin_before_notices() {
|
||||||
if ( ! wc_admin_is_admin_page() && ! wc_admin_is_embed_enabled_wc_page() ) {
|
if ( ( ! wc_admin_is_admin_page() && ! wc_admin_is_embed_enabled_wc_page() ) || ! wc_admin_is_feature_enabled( 'activity-panels' ) ) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
echo '<div class="woocommerce-layout__notice-list-hide" id="wp__notice-list">';
|
echo '<div class="woocommerce-layout__notice-list-hide" id="wp__notice-list">';
|
||||||
|
@ -273,7 +293,7 @@ add_action( 'admin_notices', 'wc_admin_admin_before_notices', 0 );
|
||||||
* Runs after admin notices and closes div.
|
* Runs after admin notices and closes div.
|
||||||
*/
|
*/
|
||||||
function wc_admin_admin_after_notices() {
|
function wc_admin_admin_after_notices() {
|
||||||
if ( ! wc_admin_is_admin_page() && ! wc_admin_is_embed_enabled_wc_page() ) {
|
if ( ( ! wc_admin_is_admin_page() && ! wc_admin_is_embed_enabled_wc_page() ) || ! wc_admin_is_feature_enabled( 'activity-panels' ) ) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
echo '</div>';
|
echo '</div>';
|
||||||
|
|
|
@ -280,3 +280,18 @@ function wc_admin_currency_settings() {
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns if a specific wc-admin feature is enabled.
|
||||||
|
*
|
||||||
|
* @param string $feature Feature slug.
|
||||||
|
* @return bool Returns true if the feature is enabled.
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
function wc_admin_is_feature_enabled( $feature ) {
|
||||||
|
if ( ! function_exists( 'wc_admin_get_feature_config' ) ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$features = wc_admin_get_feature_config();
|
||||||
|
return isset( $features[ $feature ] ) && true === $features[ $feature ];
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "wc-admin",
|
"name": "wc-admin",
|
||||||
"version": "0.6.0",
|
"version": "0.7.0",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
@ -8851,8 +8851,7 @@
|
||||||
},
|
},
|
||||||
"ansi-regex": {
|
"ansi-regex": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"bundled": true,
|
"bundled": true
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"aproba": {
|
"aproba": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
|
@ -8870,13 +8869,11 @@
|
||||||
},
|
},
|
||||||
"balanced-match": {
|
"balanced-match": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"bundled": true,
|
"bundled": true
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"brace-expansion": {
|
"brace-expansion": {
|
||||||
"version": "1.1.11",
|
"version": "1.1.11",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"optional": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"balanced-match": "^1.0.0",
|
"balanced-match": "^1.0.0",
|
||||||
"concat-map": "0.0.1"
|
"concat-map": "0.0.1"
|
||||||
|
@ -8889,18 +8886,15 @@
|
||||||
},
|
},
|
||||||
"code-point-at": {
|
"code-point-at": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"bundled": true,
|
"bundled": true
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"concat-map": {
|
"concat-map": {
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"bundled": true,
|
"bundled": true
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"console-control-strings": {
|
"console-control-strings": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"bundled": true,
|
"bundled": true
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"core-util-is": {
|
"core-util-is": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
|
@ -9003,8 +8997,7 @@
|
||||||
},
|
},
|
||||||
"inherits": {
|
"inherits": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"bundled": true,
|
"bundled": true
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"ini": {
|
"ini": {
|
||||||
"version": "1.3.5",
|
"version": "1.3.5",
|
||||||
|
@ -9014,7 +9007,6 @@
|
||||||
"is-fullwidth-code-point": {
|
"is-fullwidth-code-point": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"optional": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"number-is-nan": "^1.0.0"
|
"number-is-nan": "^1.0.0"
|
||||||
}
|
}
|
||||||
|
@ -9027,20 +9019,17 @@
|
||||||
"minimatch": {
|
"minimatch": {
|
||||||
"version": "3.0.4",
|
"version": "3.0.4",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"optional": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"brace-expansion": "^1.1.7"
|
"brace-expansion": "^1.1.7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"minimist": {
|
"minimist": {
|
||||||
"version": "0.0.8",
|
"version": "0.0.8",
|
||||||
"bundled": true,
|
"bundled": true
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"minipass": {
|
"minipass": {
|
||||||
"version": "2.3.5",
|
"version": "2.3.5",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"optional": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"safe-buffer": "^5.1.2",
|
"safe-buffer": "^5.1.2",
|
||||||
"yallist": "^3.0.0"
|
"yallist": "^3.0.0"
|
||||||
|
@ -9057,7 +9046,6 @@
|
||||||
"mkdirp": {
|
"mkdirp": {
|
||||||
"version": "0.5.1",
|
"version": "0.5.1",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"optional": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"minimist": "0.0.8"
|
"minimist": "0.0.8"
|
||||||
}
|
}
|
||||||
|
@ -9130,8 +9118,7 @@
|
||||||
},
|
},
|
||||||
"number-is-nan": {
|
"number-is-nan": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"bundled": true,
|
"bundled": true
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"object-assign": {
|
"object-assign": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
|
@ -9141,7 +9128,6 @@
|
||||||
"once": {
|
"once": {
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"optional": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"wrappy": "1"
|
"wrappy": "1"
|
||||||
}
|
}
|
||||||
|
@ -9217,8 +9203,7 @@
|
||||||
},
|
},
|
||||||
"safe-buffer": {
|
"safe-buffer": {
|
||||||
"version": "5.1.2",
|
"version": "5.1.2",
|
||||||
"bundled": true,
|
"bundled": true
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"safer-buffer": {
|
"safer-buffer": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
|
@ -9248,7 +9233,6 @@
|
||||||
"string-width": {
|
"string-width": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"optional": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"code-point-at": "^1.0.0",
|
"code-point-at": "^1.0.0",
|
||||||
"is-fullwidth-code-point": "^1.0.0",
|
"is-fullwidth-code-point": "^1.0.0",
|
||||||
|
@ -9266,7 +9250,6 @@
|
||||||
"strip-ansi": {
|
"strip-ansi": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"optional": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"ansi-regex": "^2.0.0"
|
"ansi-regex": "^2.0.0"
|
||||||
}
|
}
|
||||||
|
@ -9305,13 +9288,11 @@
|
||||||
},
|
},
|
||||||
"wrappy": {
|
"wrappy": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"bundled": true,
|
"bundled": true
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"yallist": {
|
"yallist": {
|
||||||
"version": "3.0.3",
|
"version": "3.0.3",
|
||||||
"bundled": true,
|
"bundled": true
|
||||||
"optional": true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "wc-admin",
|
"name": "wc-admin",
|
||||||
"version": "0.6.0",
|
"version": "0.7.0",
|
||||||
"main": "js/index.js",
|
"main": "js/index.js",
|
||||||
"author": "Automattic",
|
"author": "Automattic",
|
||||||
"license": "GPL-2.0-or-later",
|
"license": "GPL-2.0-or-later",
|
||||||
|
@ -16,13 +16,14 @@
|
||||||
"prebuild": "npm run -s install-if-deps-outdated",
|
"prebuild": "npm run -s install-if-deps-outdated",
|
||||||
"build:packages": "node ./bin/packages/build.js",
|
"build:packages": "node ./bin/packages/build.js",
|
||||||
"build:core": "cross-env NODE_ENV=production webpack",
|
"build:core": "cross-env NODE_ENV=production webpack",
|
||||||
"build": "npm run build:packages && npm run build:core",
|
"build": "npm run build:feature-config && npm run build:packages && npm run build:core",
|
||||||
"build:release": "./bin/build-plugin-zip.sh",
|
"build:release": "cross-env WC_ADMIN_PHASE=plugin ./bin/build-plugin-zip.sh",
|
||||||
|
"build:feature-config": "php bin/generate-feature-config.php",
|
||||||
"postbuild": "npm run -s i18n:php && npm run -s i18n:pot",
|
"postbuild": "npm run -s i18n:php && npm run -s i18n:pot",
|
||||||
"postshrinkwrap": "replace --silent 'http://' 'https://' ./package-lock.json",
|
"postshrinkwrap": "replace --silent 'http://' 'https://' ./package-lock.json",
|
||||||
"prestart": "npm run -s install-if-deps-outdated",
|
"prestart": "npm run -s install-if-deps-outdated",
|
||||||
"dev:packages": "node ./bin/packages/watch.js",
|
"dev:packages": "node ./bin/packages/watch.js",
|
||||||
"start": "npm run build:packages && concurrently \"cross-env webpack --watch\" \"npm run dev:packages\"",
|
"start": "cross-env WC_ADMIN_PHASE=development npm run build:packages && cross-env WC_ADMIN_PHASE=development npm run build:feature-config && concurrently \"cross-env WC_ADMIN_PHASE=development webpack --watch\" \"npm run dev:packages\"",
|
||||||
"i18n:js": "cross-env NODE_ENV=production babel client -o /dev/null",
|
"i18n:js": "cross-env NODE_ENV=production babel client -o /dev/null",
|
||||||
"i18n:php": "pot-to-php ./languages/wc-admin.pot ./languages/wc-admin.php wc-admin",
|
"i18n:php": "pot-to-php ./languages/wc-admin.pot ./languages/wc-admin.php wc-admin",
|
||||||
"i18n:pot": "grunt makepot",
|
"i18n:pot": "grunt makepot",
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
# 1.5.0 (unreleased)
|
# 1.5.0 (unreleased)
|
||||||
|
- Chart component: new props `emptyMessage` and `baseValue`. When an empty message is provided, it will be displayed on top of the chart if there are no values different than `baseValue`.
|
||||||
- Chart component: remove d3-array dependency.
|
- Chart component: remove d3-array dependency.
|
||||||
- Chart component: fix display when there is no data.
|
- Chart component: fix display when there is no data.
|
||||||
- Improves display of charts where all values are 0.
|
- Improves display of charts where all values are 0.
|
||||||
|
|
|
@ -6,20 +6,17 @@
|
||||||
import { Component, createRef } from '@wordpress/element';
|
import { Component, createRef } from '@wordpress/element';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { timeFormat as d3TimeFormat, utcParse as d3UTCParse } from 'd3-time-format';
|
import { timeFormat as d3TimeFormat } from 'd3-time-format';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import D3Base from './d3base';
|
import D3Base from './d3base';
|
||||||
import {
|
import {
|
||||||
getDateSpaces,
|
|
||||||
getOrderedKeys,
|
getOrderedKeys,
|
||||||
getLine,
|
|
||||||
getLineData,
|
|
||||||
getUniqueKeys,
|
|
||||||
getUniqueDates,
|
getUniqueDates,
|
||||||
getFormatter,
|
getFormatter,
|
||||||
|
isDataEmpty,
|
||||||
} from './utils/index';
|
} from './utils/index';
|
||||||
import {
|
import {
|
||||||
getXScale,
|
getXScale,
|
||||||
|
@ -27,11 +24,11 @@ import {
|
||||||
getXLineScale,
|
getXLineScale,
|
||||||
getYMax,
|
getYMax,
|
||||||
getYScale,
|
getYScale,
|
||||||
getYTickOffset,
|
|
||||||
} from './utils/scales';
|
} from './utils/scales';
|
||||||
import { drawAxis, getXTicks } from './utils/axis';
|
import { drawAxis } from './utils/axis';
|
||||||
import { drawBars } from './utils/bar-chart';
|
import { drawBars } from './utils/bar-chart';
|
||||||
import { drawLines } from './utils/line-chart';
|
import { drawLines } from './utils/line-chart';
|
||||||
|
import ChartTooltip from './utils/tooltip';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A simple D3 line and bar chart component for timeseries data in React.
|
* A simple D3 line and bar chart component for timeseries data in React.
|
||||||
|
@ -44,27 +41,89 @@ class D3Chart extends Component {
|
||||||
this.tooltipRef = createRef();
|
this.tooltipRef = createRef();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getFormatParams() {
|
||||||
|
const { xFormat, x2Format, yFormat } = this.props;
|
||||||
|
|
||||||
|
return {
|
||||||
|
xFormat: getFormatter( xFormat, d3TimeFormat ),
|
||||||
|
x2Format: getFormatter( x2Format, d3TimeFormat ),
|
||||||
|
yFormat: getFormatter( yFormat ),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
getScaleParams( uniqueDates ) {
|
||||||
|
const { data, height, margin, orderedKeys, type } = this.props;
|
||||||
|
|
||||||
|
const adjHeight = height - margin.top - margin.bottom;
|
||||||
|
const adjWidth = this.getWidth() - margin.left - margin.right;
|
||||||
|
const yMax = getYMax( data );
|
||||||
|
const yScale = getYScale( adjHeight, yMax );
|
||||||
|
|
||||||
|
if ( type === 'line' ) {
|
||||||
|
return {
|
||||||
|
xScale: getXLineScale( uniqueDates, adjWidth ),
|
||||||
|
yMax,
|
||||||
|
yScale,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const compact = this.shouldBeCompact();
|
||||||
|
const xScale = getXScale( uniqueDates, adjWidth, compact );
|
||||||
|
|
||||||
|
return {
|
||||||
|
xGroupScale: getXGroupScale( orderedKeys, xScale, compact ),
|
||||||
|
xScale,
|
||||||
|
yMax,
|
||||||
|
yScale,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
getParams( uniqueDates ) {
|
||||||
|
const { colorScheme, data, interval, mode, orderedKeys, type } = this.props;
|
||||||
|
const newOrderedKeys = orderedKeys || getOrderedKeys( data );
|
||||||
|
|
||||||
|
return {
|
||||||
|
colorScheme,
|
||||||
|
interval,
|
||||||
|
mode,
|
||||||
|
type,
|
||||||
|
uniqueDates,
|
||||||
|
visibleKeys: newOrderedKeys.filter( key => key.visible ),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
createTooltip( chart, visibleKeys ) {
|
||||||
|
const { colorScheme, tooltipLabelFormat, tooltipPosition, tooltipTitle, tooltipValueFormat } = this.props;
|
||||||
|
|
||||||
|
const tooltip = new ChartTooltip();
|
||||||
|
tooltip.ref = this.tooltipRef.current;
|
||||||
|
tooltip.chart = chart;
|
||||||
|
tooltip.position = tooltipPosition;
|
||||||
|
tooltip.title = tooltipTitle;
|
||||||
|
tooltip.labelFormat = getFormatter( tooltipLabelFormat, d3TimeFormat );
|
||||||
|
tooltip.valueFormat = getFormatter( tooltipValueFormat );
|
||||||
|
tooltip.visibleKeys = visibleKeys;
|
||||||
|
tooltip.colorScheme = colorScheme;
|
||||||
|
this.tooltip = tooltip;
|
||||||
|
}
|
||||||
|
|
||||||
drawChart( node ) {
|
drawChart( node ) {
|
||||||
const { data, margin, type } = this.props;
|
const { data, dateParser, margin, type } = this.props;
|
||||||
const params = this.getParams();
|
const uniqueDates = getUniqueDates( data, dateParser );
|
||||||
const adjParams = Object.assign( {}, params, {
|
const formats = this.getFormatParams();
|
||||||
height: params.adjHeight,
|
const params = this.getParams( uniqueDates );
|
||||||
width: params.adjWidth,
|
const scales = this.getScaleParams( uniqueDates );
|
||||||
tooltip: this.tooltipRef.current,
|
|
||||||
valueType: params.valueType,
|
|
||||||
} );
|
|
||||||
|
|
||||||
const g = node
|
const g = node
|
||||||
.attr( 'id', 'chart' )
|
.attr( 'id', 'chart' )
|
||||||
.append( 'g' )
|
.append( 'g' )
|
||||||
.attr( 'transform', `translate(${ margin.left },${ margin.top })` );
|
.attr( 'transform', `translate(${ margin.left }, ${ margin.top })` );
|
||||||
|
|
||||||
const xOffset = type === 'line' && adjParams.uniqueDates.length <= 1
|
this.createTooltip( g.node(), params.visibleKeys );
|
||||||
? adjParams.width / 2
|
|
||||||
: 0;
|
drawAxis( g, params, scales, formats, margin );
|
||||||
drawAxis( g, adjParams, xOffset );
|
type === 'line' && drawLines( g, data, params, scales, formats, this.tooltip );
|
||||||
type === 'line' && drawLines( g, data, adjParams, xOffset );
|
type === 'bar' && drawBars( g, data, params, scales, formats, this.tooltip );
|
||||||
type === 'bar' && drawBars( g, data, adjParams );
|
|
||||||
}
|
}
|
||||||
|
|
||||||
shouldBeCompact() {
|
shouldBeCompact() {
|
||||||
|
@ -90,90 +149,33 @@ class D3Chart extends Component {
|
||||||
return Math.max( width, minimumWidth + margin.left + margin.right );
|
return Math.max( width, minimumWidth + margin.left + margin.right );
|
||||||
}
|
}
|
||||||
|
|
||||||
getParams() {
|
getEmptyMessage() {
|
||||||
const {
|
const { baseValue, data, emptyMessage } = this.props;
|
||||||
colorScheme,
|
|
||||||
data,
|
if ( emptyMessage && isDataEmpty( data, baseValue ) ) {
|
||||||
dateParser,
|
return (
|
||||||
height,
|
<div className="d3-chart__empty-message">{ emptyMessage }</div>
|
||||||
interval,
|
);
|
||||||
margin,
|
}
|
||||||
mode,
|
|
||||||
orderedKeys,
|
|
||||||
tooltipPosition,
|
|
||||||
tooltipLabelFormat,
|
|
||||||
tooltipValueFormat,
|
|
||||||
tooltipTitle,
|
|
||||||
type,
|
|
||||||
xFormat,
|
|
||||||
x2Format,
|
|
||||||
yFormat,
|
|
||||||
valueType,
|
|
||||||
} = this.props;
|
|
||||||
const adjHeight = height - margin.top - margin.bottom;
|
|
||||||
const adjWidth = this.getWidth() - margin.left - margin.right;
|
|
||||||
const compact = this.shouldBeCompact();
|
|
||||||
const uniqueKeys = getUniqueKeys( data );
|
|
||||||
const newOrderedKeys = orderedKeys || getOrderedKeys( data, uniqueKeys );
|
|
||||||
const visibleKeys = newOrderedKeys.filter( key => key.visible );
|
|
||||||
const lineData = getLineData( data, newOrderedKeys );
|
|
||||||
const yMax = getYMax( lineData );
|
|
||||||
const yScale = getYScale( adjHeight, yMax );
|
|
||||||
const parseDate = d3UTCParse( dateParser );
|
|
||||||
const uniqueDates = getUniqueDates( lineData, parseDate );
|
|
||||||
const xLineScale = getXLineScale( uniqueDates, adjWidth );
|
|
||||||
const xScale = getXScale( uniqueDates, adjWidth, compact );
|
|
||||||
const xTicks = getXTicks( uniqueDates, adjWidth, mode, interval );
|
|
||||||
return {
|
|
||||||
adjHeight,
|
|
||||||
adjWidth,
|
|
||||||
colorScheme,
|
|
||||||
dateSpaces: getDateSpaces( data, uniqueDates, adjWidth, xLineScale ),
|
|
||||||
interval,
|
|
||||||
line: getLine( xLineScale, yScale ),
|
|
||||||
lineData,
|
|
||||||
margin,
|
|
||||||
mode,
|
|
||||||
orderedKeys: newOrderedKeys,
|
|
||||||
visibleKeys,
|
|
||||||
parseDate,
|
|
||||||
tooltipPosition,
|
|
||||||
tooltipLabelFormat: getFormatter( tooltipLabelFormat, d3TimeFormat ),
|
|
||||||
tooltipValueFormat: getFormatter( tooltipValueFormat ),
|
|
||||||
tooltipTitle,
|
|
||||||
type,
|
|
||||||
uniqueDates,
|
|
||||||
uniqueKeys,
|
|
||||||
valueType,
|
|
||||||
xFormat: getFormatter( xFormat, d3TimeFormat ),
|
|
||||||
x2Format: getFormatter( x2Format, d3TimeFormat ),
|
|
||||||
xGroupScale: getXGroupScale( orderedKeys, xScale, compact ),
|
|
||||||
xLineScale,
|
|
||||||
xTicks,
|
|
||||||
xScale,
|
|
||||||
yMax,
|
|
||||||
yScale,
|
|
||||||
yTickOffset: getYTickOffset( adjHeight, yMax ),
|
|
||||||
yFormat: getFormatter( yFormat ),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { className, data, height, type } = this.props;
|
const { className, data, height, orderedKeys, type } = this.props;
|
||||||
const computedWidth = this.getWidth();
|
const computedWidth = this.getWidth();
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={ classNames( 'd3-chart__container', className ) }
|
className={ classNames( 'd3-chart__container', className ) }
|
||||||
style={ { height } }
|
style={ { height } }
|
||||||
>
|
>
|
||||||
|
{ this.getEmptyMessage() }
|
||||||
<div className="d3-chart__tooltip" ref={ this.tooltipRef } />
|
<div className="d3-chart__tooltip" ref={ this.tooltipRef } />
|
||||||
<D3Base
|
<D3Base
|
||||||
className={ classNames( this.props.className ) }
|
className={ classNames( className ) }
|
||||||
data={ data }
|
data={ data }
|
||||||
drawChart={ this.drawChart }
|
drawChart={ this.drawChart }
|
||||||
height={ height }
|
height={ height }
|
||||||
orderedKeys={ this.props.orderedKeys }
|
orderedKeys={ orderedKeys }
|
||||||
tooltipRef={ this.tooltipRef }
|
tooltip={ this.tooltip }
|
||||||
type={ type }
|
type={ type }
|
||||||
width={ computedWidth }
|
width={ computedWidth }
|
||||||
/>
|
/>
|
||||||
|
@ -183,6 +185,11 @@ class D3Chart extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
D3Chart.propTypes = {
|
D3Chart.propTypes = {
|
||||||
|
/**
|
||||||
|
* Base chart value. If no data value is different than the baseValue, the
|
||||||
|
* `emptyMessage` will be displayed if provided.
|
||||||
|
*/
|
||||||
|
baseValue: PropTypes.number,
|
||||||
/**
|
/**
|
||||||
* Additional CSS classes.
|
* Additional CSS classes.
|
||||||
*/
|
*/
|
||||||
|
@ -199,6 +206,11 @@ D3Chart.propTypes = {
|
||||||
* Format to parse dates into d3 time format
|
* Format to parse dates into d3 time format
|
||||||
*/
|
*/
|
||||||
dateParser: PropTypes.string.isRequired,
|
dateParser: PropTypes.string.isRequired,
|
||||||
|
/**
|
||||||
|
* The message to be displayed if there is no data to render. If no message is provided,
|
||||||
|
* nothing will be displayed.
|
||||||
|
*/
|
||||||
|
emptyMessage: PropTypes.string,
|
||||||
/**
|
/**
|
||||||
* Height of the `svg`.
|
* Height of the `svg`.
|
||||||
*/
|
*/
|
||||||
|
@ -264,6 +276,7 @@ D3Chart.propTypes = {
|
||||||
};
|
};
|
||||||
|
|
||||||
D3Chart.defaultProps = {
|
D3Chart.defaultProps = {
|
||||||
|
baseValue: 0,
|
||||||
data: [],
|
data: [],
|
||||||
dateParser: '%Y-%m-%dT%H:%M:%S',
|
dateParser: '%Y-%m-%dT%H:%M:%S',
|
||||||
height: 200,
|
height: 200,
|
||||||
|
|
|
@ -9,11 +9,6 @@ import { Component, createRef } from '@wordpress/element';
|
||||||
import { isEqual, throttle } from 'lodash';
|
import { isEqual, throttle } from 'lodash';
|
||||||
import { select as d3Select } from 'd3-selection';
|
import { select as d3Select } from 'd3-selection';
|
||||||
|
|
||||||
/**
|
|
||||||
* Internal dependencies
|
|
||||||
*/
|
|
||||||
import { hideTooltip } from '../utils/tooltip';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides foundation to use D3 within React.
|
* Provides foundation to use D3 within React.
|
||||||
*
|
*
|
||||||
|
@ -30,10 +25,6 @@ export default class D3Base extends Component {
|
||||||
super( props );
|
super( props );
|
||||||
|
|
||||||
this.chartRef = createRef();
|
this.chartRef = createRef();
|
||||||
|
|
||||||
this.delayedScroll = throttle( () => {
|
|
||||||
hideTooltip( this.chartRef.current, props.tooltipRef.current );
|
|
||||||
}, 300 );
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
|
@ -60,6 +51,13 @@ export default class D3Base extends Component {
|
||||||
this.deleteChart();
|
this.deleteChart();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
delayedScroll() {
|
||||||
|
const { tooltip } = this.props;
|
||||||
|
return throttle( () => {
|
||||||
|
tooltip && tooltip.hide();
|
||||||
|
}, 300 );
|
||||||
|
}
|
||||||
|
|
||||||
deleteChart() {
|
deleteChart() {
|
||||||
d3Select( this.chartRef.current )
|
d3Select( this.chartRef.current )
|
||||||
.selectAll( 'svg' )
|
.selectAll( 'svg' )
|
||||||
|
@ -95,8 +93,9 @@ export default class D3Base extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const { className } = this.props;
|
||||||
return (
|
return (
|
||||||
<div className={ classNames( 'd3-base', this.props.className ) } ref={ this.chartRef } onScroll={ this.delayedScroll } />
|
<div className={ classNames( 'd3-base', className ) } ref={ this.chartRef } onScroll={ this.delayedScroll() } />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -105,5 +104,6 @@ D3Base.propTypes = {
|
||||||
className: PropTypes.string,
|
className: PropTypes.string,
|
||||||
data: PropTypes.array,
|
data: PropTypes.array,
|
||||||
orderedKeys: PropTypes.array, // required to detect changes in data
|
orderedKeys: PropTypes.array, // required to detect changes in data
|
||||||
|
tooltip: PropTypes.object,
|
||||||
type: PropTypes.string,
|
type: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
|
|
@ -61,7 +61,7 @@ class D3Legend extends Component {
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const { isScrollable } = this.state;
|
const { isScrollable } = this.state;
|
||||||
const numberOfRowsVisible = data.filter( row => row.visible ).length;
|
const numberOfRowsVisible = data.filter( row => row.visible ).length;
|
||||||
const showTotalLabel = legendDirection === 'column' && data.length > numberOfRowsVisible && totalLabel;
|
const showTotalLabel = legendDirection === 'column' && data.length > selectionLimit && totalLabel;
|
||||||
|
|
||||||
const visibleKeys = data.filter( key => key.visible );
|
const visibleKeys = data.filter( key => key.visible );
|
||||||
|
|
||||||
|
|
|
@ -57,6 +57,7 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background-color: $white;
|
background-color: $white;
|
||||||
color: $core-grey-dark-500;
|
color: $core-grey-dark-500;
|
||||||
|
cursor: pointer;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
|
@ -69,10 +70,8 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
justify-content: space-between;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
padding: 3px 0 3px 24px;
|
padding: 3px 0 3px 24px;
|
||||||
cursor: pointer;
|
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -118,6 +117,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.woocommerce-legend__item-total {
|
.woocommerce-legend__item-total {
|
||||||
|
margin-left: auto;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -138,11 +138,11 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.woocommerce-legend__direction-column & {
|
.woocommerce-legend__direction-column & {
|
||||||
margin: 2px 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|
||||||
& > button {
|
& > button {
|
||||||
height: 32px;
|
height: 36px;
|
||||||
padding: 0 17px;
|
padding: 0 17px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,11 +10,35 @@
|
||||||
|
|
||||||
.d3-chart__container {
|
.d3-chart__container {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.d3-chart__empty-message {
|
||||||
|
align-items: center;
|
||||||
|
bottom: 0;
|
||||||
|
color: $core-grey-dark-300;
|
||||||
|
display: flex;
|
||||||
|
@include font-size( 18 );
|
||||||
|
font-weight: bold;
|
||||||
|
justify-content: center;
|
||||||
|
left: 0;
|
||||||
|
line-height: 1.5;
|
||||||
|
margin: 0 auto;
|
||||||
|
max-width: 50%;
|
||||||
|
padding-bottom: 48px;
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
@include breakpoint( '<782px' ) {
|
||||||
|
@include font-size( 13 );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.d3-chart__tooltip {
|
.d3-chart__tooltip {
|
||||||
border: 1px solid $core-grey-light-700;
|
border: 1px solid $core-grey-light-700;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
@ -134,8 +158,12 @@
|
||||||
|
|
||||||
.focus-grid {
|
.focus-grid {
|
||||||
line {
|
line {
|
||||||
stroke: $core-grey-light-700;
|
stroke: rgba( 0, 0, 0, 0.1 );
|
||||||
stroke-width: 1px;
|
stroke-width: 1px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.barfocus {
|
||||||
|
fill: rgba( 0, 0, 0, 0.1 );
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,7 +22,7 @@ const mostPoints = 31;
|
||||||
*/
|
*/
|
||||||
const getFactors = inputNum => {
|
const getFactors = inputNum => {
|
||||||
const numFactors = [];
|
const numFactors = [];
|
||||||
for ( let i = 1; i <= Math.floor( Math.sqrt( inputNum ) ); i += 1 ) {
|
for ( let i = 1; i <= Math.floor( Math.sqrt( inputNum ) ); i++ ) {
|
||||||
if ( inputNum % i === 0 ) {
|
if ( inputNum % i === 0 ) {
|
||||||
numFactors.push( i );
|
numFactors.push( i );
|
||||||
inputNum / i !== i && numFactors.push( inputNum / i );
|
inputNum / i !== i && numFactors.push( inputNum / i );
|
||||||
|
@ -176,11 +176,6 @@ export const compareStrings = ( s1, s2, splitChar = new RegExp( [ ' |,' ], 'g' )
|
||||||
export const getYGrids = ( yMax ) => {
|
export const getYGrids = ( yMax ) => {
|
||||||
const yGrids = [];
|
const yGrids = [];
|
||||||
|
|
||||||
// If all values are 0, yMax can become NaN.
|
|
||||||
if ( isNaN( yMax ) ) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
for ( let i = 0; i < 4; i++ ) {
|
for ( let i = 0; i < 4; i++ ) {
|
||||||
const value = yMax > 1 ? Math.round( i / 3 * yMax ) : i / 3 * yMax;
|
const value = yMax > 1 ? Math.round( i / 3 * yMax ) : i / 3 * yMax;
|
||||||
if ( yGrids[ yGrids.length - 1 ] !== value ) {
|
if ( yGrids[ yGrids.length - 1 ] !== value ) {
|
||||||
|
@ -191,87 +186,90 @@ export const getYGrids = ( yMax ) => {
|
||||||
return yGrids;
|
return yGrids;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const drawAxis = ( node, params, xOffset ) => {
|
const removeDuplicateDates = ( d, i, ticks, formatter ) => {
|
||||||
const xScale = params.type === 'line' ? params.xLineScale : params.xScale;
|
const monthDate = moment( d ).toDate();
|
||||||
const removeDuplicateDates = ( d, i, ticks, formatter ) => {
|
let prevMonth = i !== 0 ? ticks[ i - 1 ] : ticks[ i ];
|
||||||
const monthDate = moment( d ).toDate();
|
prevMonth = prevMonth instanceof Date ? prevMonth : moment( prevMonth ).toDate();
|
||||||
let prevMonth = i !== 0 ? ticks[ i - 1 ] : ticks[ i ];
|
return i === 0
|
||||||
prevMonth = prevMonth instanceof Date ? prevMonth : moment( prevMonth ).toDate();
|
? formatter( monthDate )
|
||||||
return i === 0
|
: compareStrings( formatter( prevMonth ), formatter( monthDate ) ).join( ' ' );
|
||||||
? formatter( monthDate )
|
};
|
||||||
: compareStrings( formatter( prevMonth ), formatter( monthDate ) ).join( ' ' );
|
|
||||||
};
|
|
||||||
|
|
||||||
const yGrids = getYGrids( params.yMax === 0 ? 1 : params.yMax );
|
const drawXAxis = ( node, params, scales, formats ) => {
|
||||||
|
const height = scales.yScale.range()[ 0 ];
|
||||||
const ticks = params.xTicks.map( d => ( params.type === 'line' ? moment( d ).toDate() : d ) );
|
let ticks = getXTicks( params.uniqueDates, scales.xScale.range()[ 1 ], params.mode, params.interval );
|
||||||
|
if ( params.type === 'line' ) {
|
||||||
|
ticks = ticks.map( d => moment( d ).toDate() );
|
||||||
|
}
|
||||||
|
|
||||||
node
|
node
|
||||||
.append( 'g' )
|
.append( 'g' )
|
||||||
.attr( 'class', 'axis' )
|
.attr( 'class', 'axis' )
|
||||||
.attr( 'aria-hidden', 'true' )
|
.attr( 'aria-hidden', 'true' )
|
||||||
.attr( 'transform', `translate(${ xOffset }, ${ params.height })` )
|
.attr( 'transform', `translate(0, ${ height })` )
|
||||||
.call(
|
.call(
|
||||||
d3AxisBottom( xScale )
|
d3AxisBottom( scales.xScale )
|
||||||
.tickValues( ticks )
|
.tickValues( ticks )
|
||||||
.tickFormat( ( d, i ) => params.interval === 'hour'
|
.tickFormat( ( d, i ) => params.interval === 'hour'
|
||||||
? params.xFormat( d instanceof Date ? d : moment( d ).toDate() )
|
? formats.xFormat( d instanceof Date ? d : moment( d ).toDate() )
|
||||||
: removeDuplicateDates( d, i, ticks, params.xFormat ) )
|
: removeDuplicateDates( d, i, ticks, formats.xFormat ) )
|
||||||
);
|
);
|
||||||
|
|
||||||
node
|
node
|
||||||
.append( 'g' )
|
.append( 'g' )
|
||||||
.attr( 'class', 'axis axis-month' )
|
.attr( 'class', 'axis axis-month' )
|
||||||
.attr( 'aria-hidden', 'true' )
|
.attr( 'aria-hidden', 'true' )
|
||||||
.attr( 'transform', `translate(${ xOffset }, ${ params.height + 20 })` )
|
.attr( 'transform', `translate(0, ${ height + 14 })` )
|
||||||
.call(
|
.call(
|
||||||
d3AxisBottom( xScale )
|
d3AxisBottom( scales.xScale )
|
||||||
.tickValues( ticks )
|
.tickValues( ticks )
|
||||||
.tickFormat( ( d, i ) => removeDuplicateDates( d, i, ticks, params.x2Format ) )
|
.tickFormat( ( d, i ) => removeDuplicateDates( d, i, ticks, formats.x2Format ) )
|
||||||
)
|
);
|
||||||
.call( g => g.select( '.domain' ).remove() );
|
|
||||||
|
|
||||||
node
|
node
|
||||||
.append( 'g' )
|
.append( 'g' )
|
||||||
.attr( 'class', 'pipes' )
|
.attr( 'class', 'pipes' )
|
||||||
.attr( 'transform', `translate(${ xOffset }, ${ params.height })` )
|
.attr( 'transform', `translate(0, ${ height })` )
|
||||||
.call(
|
.call(
|
||||||
d3AxisBottom( xScale )
|
d3AxisBottom( scales.xScale )
|
||||||
.tickValues( ticks )
|
.tickValues( ticks )
|
||||||
.tickSize( 5 )
|
.tickSize( 5 )
|
||||||
.tickFormat( '' )
|
.tickFormat( '' )
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
if ( yGrids ) {
|
const drawYAxis = ( node, scales, formats, margin ) => {
|
||||||
node
|
const yGrids = getYGrids( scales.yScale.domain()[ 1 ] );
|
||||||
.append( 'g' )
|
const width = scales.xScale.range()[ 1 ];
|
||||||
.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
|
node
|
||||||
.append( 'g' )
|
.append( 'g' )
|
||||||
.attr( 'class', 'axis y-axis' )
|
.attr( 'class', 'grid' )
|
||||||
.attr( 'aria-hidden', 'true' )
|
.attr( 'transform', `translate(-${ margin.left }, 0)` )
|
||||||
.attr( 'transform', 'translate(-50, 0)' )
|
.call(
|
||||||
.attr( 'text-anchor', 'start' )
|
d3AxisLeft( scales.yScale )
|
||||||
.call(
|
.tickValues( yGrids )
|
||||||
d3AxisLeft( params.yTickOffset )
|
.tickSize( -width - margin.left - margin.right )
|
||||||
.tickValues( params.yMax === 0 ? [ yGrids[ 0 ] ] : yGrids )
|
.tickFormat( '' )
|
||||||
.tickFormat( d => params.yFormat( d !== 0 ? d : 0 ) )
|
);
|
||||||
);
|
|
||||||
}
|
node
|
||||||
|
.append( 'g' )
|
||||||
|
.attr( 'class', 'axis y-axis' )
|
||||||
|
.attr( 'aria-hidden', 'true' )
|
||||||
|
.attr( 'transform', 'translate(-50, 12)' )
|
||||||
|
.attr( 'text-anchor', 'start' )
|
||||||
|
.call(
|
||||||
|
d3AxisLeft( scales.yScale )
|
||||||
|
.tickValues( scales.yMax === 0 ? [ yGrids[ 0 ] ] : yGrids )
|
||||||
|
.tickFormat( d => formats.yFormat( d !== 0 ? d : 0 ) )
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const drawAxis = ( node, params, scales, formats, margin ) => {
|
||||||
|
drawXAxis( node, params, scales, formats );
|
||||||
|
drawYAxis( node, scales, formats, margin );
|
||||||
|
|
||||||
node.selectAll( '.domain' ).remove();
|
node.selectAll( '.domain' ).remove();
|
||||||
node
|
node.selectAll( '.axis .tick line' ).remove();
|
||||||
.selectAll( '.axis' )
|
|
||||||
.selectAll( '.tick' )
|
|
||||||
.select( 'line' )
|
|
||||||
.remove();
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -4,23 +4,16 @@
|
||||||
* External dependencies
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
import { get } from 'lodash';
|
import { get } from 'lodash';
|
||||||
import { event as d3Event, select as d3Select } from 'd3-selection';
|
import { event as d3Event } from 'd3-selection';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import { getColor } from './color';
|
import { getColor } from './color';
|
||||||
import { calculateTooltipPosition, hideTooltip, showTooltip } from './tooltip';
|
|
||||||
|
|
||||||
const handleMouseOverBarChart = ( date, parentNode, node, data, params, position ) => {
|
export const drawBars = ( node, data, params, scales, formats, tooltip ) => {
|
||||||
d3Select( parentNode )
|
const height = scales.yScale.range()[ 0 ];
|
||||||
.select( '.barfocus' )
|
|
||||||
.attr( 'opacity', '0.1' );
|
|
||||||
showTooltip( params, data.find( e => e.date === date ), position );
|
|
||||||
};
|
|
||||||
|
|
||||||
export const drawBars = ( node, data, params ) => {
|
|
||||||
const barGroup = node
|
const barGroup = node
|
||||||
.append( 'g' )
|
.append( 'g' )
|
||||||
.attr( 'class', 'bars' )
|
.attr( 'class', 'bars' )
|
||||||
|
@ -28,14 +21,14 @@ export const drawBars = ( node, data, params ) => {
|
||||||
.data( data )
|
.data( data )
|
||||||
.enter()
|
.enter()
|
||||||
.append( 'g' )
|
.append( 'g' )
|
||||||
.attr( 'transform', d => `translate(${ params.xScale( d.date ) },0)` )
|
.attr( 'transform', d => `translate(${ scales.xScale( d.date ) }, 0)` )
|
||||||
.attr( 'class', 'bargroup' )
|
.attr( 'class', 'bargroup' )
|
||||||
.attr( 'role', 'region' )
|
.attr( 'role', 'region' )
|
||||||
.attr(
|
.attr(
|
||||||
'aria-label',
|
'aria-label',
|
||||||
d =>
|
d =>
|
||||||
params.mode === 'item-comparison'
|
params.mode === 'item-comparison'
|
||||||
? params.tooltipLabelFormat( d.date instanceof Date ? d.date : moment( d.date ).toDate() )
|
? tooltip.labelFormat( d.date instanceof Date ? d.date : moment( d.date ).toDate() )
|
||||||
: null
|
: null
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -44,23 +37,18 @@ export const drawBars = ( node, data, params ) => {
|
||||||
.attr( 'class', 'barfocus' )
|
.attr( 'class', 'barfocus' )
|
||||||
.attr( 'x', 0 )
|
.attr( 'x', 0 )
|
||||||
.attr( 'y', 0 )
|
.attr( 'y', 0 )
|
||||||
.attr( 'width', params.xGroupScale.range()[ 1 ] )
|
.attr( 'width', scales.xGroupScale.range()[ 1 ] )
|
||||||
.attr( 'height', params.height )
|
.attr( 'height', height )
|
||||||
.attr( 'opacity', '0' )
|
.attr( 'opacity', '0' )
|
||||||
.on( 'mouseover', ( d, i, nodes ) => {
|
.on( 'mouseover', ( d, i, nodes ) => {
|
||||||
const position = calculateTooltipPosition(
|
tooltip.show( data.find( e => e.date === d.date ), d3Event.target, nodes[ i ].parentNode );
|
||||||
d3Event.target,
|
|
||||||
node.node(),
|
|
||||||
params.tooltipPosition
|
|
||||||
);
|
|
||||||
handleMouseOverBarChart( d.date, nodes[ i ].parentNode, node, data, params, position );
|
|
||||||
} )
|
} )
|
||||||
.on( 'mouseout', ( d, i, nodes ) => hideTooltip( nodes[ i ].parentNode, params.tooltip ) );
|
.on( 'mouseout', () => tooltip.hide() );
|
||||||
|
|
||||||
barGroup
|
barGroup
|
||||||
.selectAll( '.bar' )
|
.selectAll( '.bar' )
|
||||||
.data( d =>
|
.data( d =>
|
||||||
params.orderedKeys.filter( row => row.visible ).map( row => ( {
|
params.visibleKeys.map( row => ( {
|
||||||
key: row.key,
|
key: row.key,
|
||||||
focus: row.focus,
|
focus: row.focus,
|
||||||
value: get( d, [ row.key, 'value' ], 0 ),
|
value: get( d, [ row.key, 'value' ], 0 ),
|
||||||
|
@ -72,16 +60,16 @@ export const drawBars = ( node, data, params ) => {
|
||||||
.enter()
|
.enter()
|
||||||
.append( 'rect' )
|
.append( 'rect' )
|
||||||
.attr( 'class', 'bar' )
|
.attr( 'class', 'bar' )
|
||||||
.attr( 'x', d => params.xGroupScale( d.key ) )
|
.attr( 'x', d => scales.xGroupScale( d.key ) )
|
||||||
.attr( 'y', d => params.yScale( d.value ) )
|
.attr( 'y', d => scales.yScale( d.value ) )
|
||||||
.attr( 'width', params.xGroupScale.bandwidth() )
|
.attr( 'width', scales.xGroupScale.bandwidth() )
|
||||||
.attr( 'height', d => params.height - params.yScale( d.value ) )
|
.attr( 'height', d => height - scales.yScale( d.value ) )
|
||||||
.attr( 'fill', d => getColor( d.key, params.visibleKeys, params.colorScheme ) )
|
.attr( 'fill', d => getColor( d.key, params.visibleKeys, params.colorScheme ) )
|
||||||
.attr( 'pointer-events', 'none' )
|
.attr( 'pointer-events', 'none' )
|
||||||
.attr( 'tabindex', '0' )
|
.attr( 'tabindex', '0' )
|
||||||
.attr( 'aria-label', d => {
|
.attr( 'aria-label', d => {
|
||||||
const label = params.mode === 'time-comparison' && d.label ? d.label : d.key;
|
const label = params.mode === 'time-comparison' && d.label ? d.label : d.key;
|
||||||
return `${ label } ${ params.tooltipValueFormat( d.value ) }`;
|
return `${ label } ${ tooltip.valueFormat( d.value ) }`;
|
||||||
} )
|
} )
|
||||||
.style( 'opacity', d => {
|
.style( 'opacity', d => {
|
||||||
const opacity = d.focus ? 1 : 0.1;
|
const opacity = d.focus ? 1 : 0.1;
|
||||||
|
@ -89,8 +77,7 @@ export const drawBars = ( node, data, params ) => {
|
||||||
} )
|
} )
|
||||||
.on( 'focus', ( d, i, nodes ) => {
|
.on( 'focus', ( d, i, nodes ) => {
|
||||||
const targetNode = d.value > 0 ? d3Event.target : d3Event.target.parentNode;
|
const targetNode = d.value > 0 ? d3Event.target : d3Event.target.parentNode;
|
||||||
const position = calculateTooltipPosition( targetNode, node.node(), params.tooltipPosition );
|
tooltip.show( data.find( e => e.date === d.date ), targetNode, nodes[ i ].parentNode );
|
||||||
handleMouseOverBarChart( d.date, nodes[ i ].parentNode, node, data, params, position );
|
|
||||||
} )
|
} )
|
||||||
.on( 'blur', ( d, i, nodes ) => hideTooltip( nodes[ i ].parentNode, params.tooltip ) );
|
.on( 'blur', () => tooltip.hide() );
|
||||||
};
|
};
|
||||||
|
|
|
@ -3,10 +3,9 @@
|
||||||
/**
|
/**
|
||||||
* External dependencies
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
import { find, get } from 'lodash';
|
import { isNil } from 'lodash';
|
||||||
import { format as d3Format } from 'd3-format';
|
import { format as d3Format } from 'd3-format';
|
||||||
import { line as d3Line } from 'd3-shape';
|
import { utcParse as d3UTCParse } from 'd3-time-format';
|
||||||
import moment from 'moment';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Allows an overriding formatter or defaults to d3Format or d3TimeFormat
|
* Allows an overriding formatter or defaults to d3Format or d3TimeFormat
|
||||||
|
@ -17,30 +16,18 @@ import moment from 'moment';
|
||||||
export const getFormatter = ( format, formatter = d3Format ) =>
|
export const getFormatter = ( format, formatter = d3Format ) =>
|
||||||
typeof format === 'function' ? format : formatter( format );
|
typeof format === 'function' ? format : formatter( format );
|
||||||
|
|
||||||
/**
|
|
||||||
* Describes `getUniqueKeys`
|
|
||||||
* @param {array} data - The chart component's `data` prop.
|
|
||||||
* @returns {array} of unique category keys
|
|
||||||
*/
|
|
||||||
export const getUniqueKeys = data => {
|
|
||||||
return [
|
|
||||||
...new Set(
|
|
||||||
data.reduce( ( accum, curr ) => {
|
|
||||||
Object.keys( curr ).forEach( key => key !== 'date' && accum.push( key ) );
|
|
||||||
return accum;
|
|
||||||
}, [] )
|
|
||||||
),
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Describes `getOrderedKeys`
|
* Describes `getOrderedKeys`
|
||||||
* @param {array} data - The chart component's `data` prop.
|
* @param {array} data - The chart component's `data` prop.
|
||||||
* @param {array} uniqueKeys - from `getUniqueKeys`.
|
|
||||||
* @returns {array} of unique category keys ordered by cumulative total value
|
* @returns {array} of unique category keys ordered by cumulative total value
|
||||||
*/
|
*/
|
||||||
export const getOrderedKeys = ( data, uniqueKeys ) =>
|
export const getOrderedKeys = ( data ) => {
|
||||||
uniqueKeys
|
const keys = new Set(
|
||||||
|
data.reduce( ( acc, curr ) => acc.concat( Object.keys( curr ) ), [] )
|
||||||
|
);
|
||||||
|
|
||||||
|
return [ ...keys ]
|
||||||
|
.filter( key => key !== 'date' )
|
||||||
.map( key => ( {
|
.map( key => ( {
|
||||||
key,
|
key,
|
||||||
focus: true,
|
focus: true,
|
||||||
|
@ -48,90 +35,37 @@ export const getOrderedKeys = ( data, uniqueKeys ) =>
|
||||||
visible: true,
|
visible: true,
|
||||||
} ) )
|
} ) )
|
||||||
.sort( ( a, b ) => b.total - a.total );
|
.sort( ( a, b ) => b.total - a.total );
|
||||||
|
|
||||||
/**
|
|
||||||
* Describes `getLineData`
|
|
||||||
* @param {array} data - The chart component's `data` prop.
|
|
||||||
* @param {array} orderedKeys - from `getOrderedKeys`.
|
|
||||||
* @returns {array} an array objects with a category `key` and an array of `values` with `date` and `value` properties
|
|
||||||
*/
|
|
||||||
export const getLineData = ( data, orderedKeys ) =>
|
|
||||||
orderedKeys.map( row => ( {
|
|
||||||
key: row.key,
|
|
||||||
focus: row.focus,
|
|
||||||
visible: row.visible,
|
|
||||||
values: data.map( d => ( {
|
|
||||||
date: d.date,
|
|
||||||
focus: row.focus,
|
|
||||||
label: get( d, [ row.key, 'label' ], '' ),
|
|
||||||
value: get( d, [ row.key, 'value' ], 0 ),
|
|
||||||
visible: row.visible,
|
|
||||||
} ) ),
|
|
||||||
} ) );
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Describes `getUniqueDates`
|
|
||||||
* @param {array} lineData - from `GetLineData`
|
|
||||||
* @param {function} parseDate - D3 time format parser
|
|
||||||
* @returns {array} an array of unique date values sorted from earliest to latest
|
|
||||||
*/
|
|
||||||
export const getUniqueDates = ( lineData, parseDate ) => {
|
|
||||||
return [
|
|
||||||
...new Set(
|
|
||||||
lineData.reduce( ( accum, { values } ) => {
|
|
||||||
values.forEach( ( { date } ) => accum.push( date ) );
|
|
||||||
return accum;
|
|
||||||
}, [] )
|
|
||||||
),
|
|
||||||
].sort( ( a, b ) => parseDate( a ) - parseDate( b ) );
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Describes getLine
|
* Describes `getUniqueDates`
|
||||||
* @param {function} xLineScale - from `getXLineScale`.
|
* @param {array} data - the chart component's `data` prop.
|
||||||
* @param {function} yScale - from `getYScale`.
|
* @param {string} dateParser - D3 time format
|
||||||
* @returns {function} the D3 line function for plotting all category values
|
* @returns {array} an array of unique date values sorted from earliest to latest
|
||||||
*/
|
*/
|
||||||
export const getLine = ( xLineScale, yScale ) =>
|
export const getUniqueDates = ( data, dateParser ) => {
|
||||||
d3Line()
|
const parseDate = d3UTCParse( dateParser );
|
||||||
.x( d => xLineScale( moment( d.date ).toDate() ) )
|
const dates = new Set(
|
||||||
.y( d => yScale( d.value ) );
|
data.map( d => d.date )
|
||||||
|
);
|
||||||
|
return [ ...dates ].sort( ( a, b ) => parseDate( a ) - parseDate( b ) );
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Describes getDateSpaces
|
* Check whether data is empty.
|
||||||
* @param {array} data - The chart component's `data` prop.
|
* @param {array} data - the chart component's `data` prop.
|
||||||
* @param {array} uniqueDates - from `getUniqueDates`
|
* @param {number} baseValue - base value to test data values against.
|
||||||
* @param {number} width - calculated width of the charting space
|
* @returns {boolean} `false` if there was at least one data value different than
|
||||||
* @param {function} xLineScale - from `getXLineScale`
|
* the baseValue.
|
||||||
* @returns {array} that icnludes the date, start (x position) and width to mode the mouseover rectangles
|
|
||||||
*/
|
*/
|
||||||
export const getDateSpaces = ( data, uniqueDates, width, xLineScale ) =>
|
export const isDataEmpty = ( data, baseValue = 0 ) => {
|
||||||
uniqueDates.map( ( d, i ) => {
|
for ( let i = 0; i < data.length; i++ ) {
|
||||||
const datapoints = find( data, { date: d } );
|
for ( const [ key, item ] of Object.entries( data[ i ] ) ) {
|
||||||
const xNow = xLineScale( moment( d ).toDate() );
|
if ( key !== 'date' && ! isNil( item.value ) && item.value !== baseValue ) {
|
||||||
const xPrev =
|
return false;
|
||||||
i >= 1
|
}
|
||||||
? xLineScale( moment( uniqueDates[ i - 1 ] ).toDate() )
|
}
|
||||||
: xLineScale( moment( uniqueDates[ 0 ] ).toDate() );
|
}
|
||||||
const xNext =
|
|
||||||
i < uniqueDates.length - 1
|
return true;
|
||||||
? xLineScale( moment( uniqueDates[ i + 1 ] ).toDate() )
|
};
|
||||||
: xLineScale( moment( uniqueDates[ uniqueDates.length - 1 ] ).toDate() );
|
|
||||||
let xWidth = i === 0 ? xNext - xNow : xNow - xPrev;
|
|
||||||
const xStart = i === 0 ? 0 : xNow - xWidth / 2;
|
|
||||||
xWidth = i === 0 || i === uniqueDates.length - 1 ? xWidth / 2 : xWidth;
|
|
||||||
return {
|
|
||||||
date: d,
|
|
||||||
start: uniqueDates.length > 1 ? xStart : 0,
|
|
||||||
width: uniqueDates.length > 1 ? xWidth : width,
|
|
||||||
values: Object.keys( datapoints )
|
|
||||||
.filter( key => key !== 'date' )
|
|
||||||
.map( key => {
|
|
||||||
return {
|
|
||||||
key,
|
|
||||||
value: datapoints[ key ].value,
|
|
||||||
date: d,
|
|
||||||
};
|
|
||||||
} ),
|
|
||||||
};
|
|
||||||
} );
|
|
||||||
|
|
|
@ -3,38 +3,107 @@
|
||||||
/**
|
/**
|
||||||
* External dependencies
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
import { event as d3Event, select as d3Select } from 'd3-selection';
|
import { event as d3Event } from 'd3-selection';
|
||||||
import { smallBreak, wideBreak } from './breakpoints';
|
import { line as d3Line } from 'd3-shape';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
|
import { first, get } from 'lodash';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import { getColor } from './color';
|
import { getColor } from './color';
|
||||||
import { calculateTooltipPosition, hideTooltip, showTooltip } from './tooltip';
|
import { smallBreak, wideBreak } from './breakpoints';
|
||||||
|
|
||||||
const handleMouseOverLineChart = ( date, parentNode, node, data, params, position ) => {
|
/**
|
||||||
d3Select( parentNode )
|
* Describes getDateSpaces
|
||||||
.select( '.focus-grid' )
|
* @param {array} data - The chart component's `data` prop.
|
||||||
.attr( 'opacity', '1' );
|
* @param {array} uniqueDates - from `getUniqueDates`
|
||||||
showTooltip( params, data.find( e => e.date === date ), position );
|
* @param {number} width - calculated width of the charting space
|
||||||
};
|
* @param {function} xScale - from `getXLineScale`
|
||||||
|
* @returns {array} that includes the date, start (x position) and width to mode the mouseover rectangles
|
||||||
|
*/
|
||||||
|
export const getDateSpaces = ( data, uniqueDates, width, xScale ) =>
|
||||||
|
uniqueDates.map( ( d, i ) => {
|
||||||
|
const datapoints = first( data.filter( item => item.date === d ) );
|
||||||
|
const xNow = xScale( moment( d ).toDate() );
|
||||||
|
const xPrev =
|
||||||
|
i >= 1
|
||||||
|
? xScale( moment( uniqueDates[ i - 1 ] ).toDate() )
|
||||||
|
: xScale( moment( uniqueDates[ 0 ] ).toDate() );
|
||||||
|
const xNext =
|
||||||
|
i < uniqueDates.length - 1
|
||||||
|
? xScale( moment( uniqueDates[ i + 1 ] ).toDate() )
|
||||||
|
: xScale( moment( uniqueDates[ uniqueDates.length - 1 ] ).toDate() );
|
||||||
|
let xWidth = i === 0 ? xNext - xNow : xNow - xPrev;
|
||||||
|
const xStart = i === 0 ? 0 : xNow - xWidth / 2;
|
||||||
|
xWidth = i === 0 || i === uniqueDates.length - 1 ? xWidth / 2 : xWidth;
|
||||||
|
return {
|
||||||
|
date: d,
|
||||||
|
start: uniqueDates.length > 1 ? xStart : 0,
|
||||||
|
width: uniqueDates.length > 1 ? xWidth : width,
|
||||||
|
values: Object.keys( datapoints )
|
||||||
|
.filter( key => key !== 'date' )
|
||||||
|
.map( key => {
|
||||||
|
return {
|
||||||
|
key,
|
||||||
|
value: datapoints[ key ].value,
|
||||||
|
date: d,
|
||||||
|
};
|
||||||
|
} ),
|
||||||
|
};
|
||||||
|
} );
|
||||||
|
|
||||||
export const drawLines = ( node, data, params, xOffset ) => {
|
/**
|
||||||
|
* Describes getLine
|
||||||
|
* @param {function} xScale - from `getXLineScale`.
|
||||||
|
* @param {function} yScale - from `getYScale`.
|
||||||
|
* @returns {function} the D3 line function for plotting all category values
|
||||||
|
*/
|
||||||
|
export const getLine = ( xScale, yScale ) =>
|
||||||
|
d3Line()
|
||||||
|
.x( d => xScale( moment( d.date ).toDate() ) )
|
||||||
|
.y( d => yScale( d.value ) );
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Describes `getLineData`
|
||||||
|
* @param {array} data - The chart component's `data` prop.
|
||||||
|
* @param {array} orderedKeys - from `getOrderedKeys`.
|
||||||
|
* @returns {array} an array objects with a category `key` and an array of `values` with `date` and `value` properties
|
||||||
|
*/
|
||||||
|
export const getLineData = ( data, orderedKeys ) =>
|
||||||
|
orderedKeys.map( row => ( {
|
||||||
|
key: row.key,
|
||||||
|
focus: row.focus,
|
||||||
|
visible: row.visible,
|
||||||
|
values: data.map( d => ( {
|
||||||
|
date: d.date,
|
||||||
|
focus: row.focus,
|
||||||
|
label: get( d, [ row.key, 'label' ], '' ),
|
||||||
|
value: get( d, [ row.key, 'value' ], 0 ),
|
||||||
|
visible: row.visible,
|
||||||
|
} ) ),
|
||||||
|
} ) );
|
||||||
|
|
||||||
|
export const drawLines = ( node, data, params, scales, formats, tooltip ) => {
|
||||||
|
const height = scales.yScale.range()[ 0 ];
|
||||||
|
const width = scales.xScale.range()[ 1 ];
|
||||||
|
const line = getLine( scales.xScale, scales.yScale );
|
||||||
|
const lineData = getLineData( data, params.visibleKeys );
|
||||||
const series = node
|
const series = node
|
||||||
.append( 'g' )
|
.append( 'g' )
|
||||||
.attr( 'class', 'lines' )
|
.attr( 'class', 'lines' )
|
||||||
.selectAll( '.line-g' )
|
.selectAll( '.line-g' )
|
||||||
.data( params.lineData.filter( d => d.visible ).reverse() )
|
.data( lineData.filter( d => d.visible ).reverse() )
|
||||||
.enter()
|
.enter()
|
||||||
.append( 'g' )
|
.append( 'g' )
|
||||||
.attr( 'class', 'line-g' )
|
.attr( 'class', 'line-g' )
|
||||||
.attr( 'role', 'region' )
|
.attr( 'role', 'region' )
|
||||||
.attr( 'aria-label', d => d.key );
|
.attr( 'aria-label', d => d.key );
|
||||||
|
const dateSpaces = getDateSpaces( data, params.uniqueDates, width, scales.xScale );
|
||||||
|
|
||||||
let lineStroke = params.width <= wideBreak || params.uniqueDates.length > 50 ? 2 : 3;
|
let lineStroke = width <= wideBreak || params.uniqueDates.length > 50 ? 2 : 3;
|
||||||
lineStroke = params.width <= smallBreak ? 1.25 : lineStroke;
|
lineStroke = width <= smallBreak ? 1.25 : lineStroke;
|
||||||
const dotRadius = params.width <= wideBreak ? 4 : 6;
|
const dotRadius = width <= wideBreak ? 4 : 6;
|
||||||
|
|
||||||
params.uniqueDates.length > 1 &&
|
params.uniqueDates.length > 1 &&
|
||||||
series
|
series
|
||||||
|
@ -48,11 +117,11 @@ export const drawLines = ( node, data, params, xOffset ) => {
|
||||||
const opacity = d.focus ? 1 : 0.1;
|
const opacity = d.focus ? 1 : 0.1;
|
||||||
return d.visible ? opacity : 0;
|
return d.visible ? opacity : 0;
|
||||||
} )
|
} )
|
||||||
.attr( 'd', d => params.line( d.values ) );
|
.attr( 'd', d => line( d.values ) );
|
||||||
|
|
||||||
const minDataPointSpacing = 36;
|
const minDataPointSpacing = 36;
|
||||||
|
|
||||||
params.width / params.uniqueDates.length > minDataPointSpacing &&
|
width / params.uniqueDates.length > minDataPointSpacing &&
|
||||||
series
|
series
|
||||||
.selectAll( 'circle' )
|
.selectAll( 'circle' )
|
||||||
.data( ( d, i ) => d.values.map( row => ( { ...row, i, visible: d.visible, key: d.key } ) ) )
|
.data( ( d, i ) => d.values.map( row => ( { ...row, i, visible: d.visible, key: d.key } ) ) )
|
||||||
|
@ -66,30 +135,25 @@ export const drawLines = ( node, data, params, xOffset ) => {
|
||||||
const opacity = d.focus ? 1 : 0.1;
|
const opacity = d.focus ? 1 : 0.1;
|
||||||
return d.visible ? opacity : 0;
|
return d.visible ? opacity : 0;
|
||||||
} )
|
} )
|
||||||
.attr( 'cx', d => params.xLineScale( moment( d.date ).toDate() ) + xOffset )
|
.attr( 'cx', d => scales.xScale( moment( d.date ).toDate() ) )
|
||||||
.attr( 'cy', d => params.yScale( d.value ) )
|
.attr( 'cy', d => scales.yScale( d.value ) )
|
||||||
.attr( 'tabindex', '0' )
|
.attr( 'tabindex', '0' )
|
||||||
.attr( 'aria-label', d => {
|
.attr( 'aria-label', d => {
|
||||||
const label = d.label
|
const label = d.label
|
||||||
? d.label
|
? d.label
|
||||||
: params.tooltipLabelFormat( d.date instanceof Date ? d.date : moment( d.date ).toDate() );
|
: tooltip.labelFormat( d.date instanceof Date ? d.date : moment( d.date ).toDate() );
|
||||||
return `${ label } ${ params.tooltipValueFormat( d.value ) }`;
|
return `${ label } ${ tooltip.valueFormat( d.value ) }`;
|
||||||
} )
|
} )
|
||||||
.on( 'focus', ( d, i, nodes ) => {
|
.on( 'focus', ( d, i, nodes ) => {
|
||||||
const position = calculateTooltipPosition(
|
tooltip.show( data.find( e => e.date === d.date ), nodes[ i ].parentNode, d3Event.target );
|
||||||
d3Event.target,
|
|
||||||
node.node(),
|
|
||||||
params.tooltipPosition
|
|
||||||
);
|
|
||||||
handleMouseOverLineChart( d.date, nodes[ i ].parentNode, node, data, params, position );
|
|
||||||
} )
|
} )
|
||||||
.on( 'blur', ( d, i, nodes ) => hideTooltip( nodes[ i ].parentNode, params.tooltip ) );
|
.on( 'blur', () => tooltip.hide() );
|
||||||
|
|
||||||
const focus = node
|
const focus = node
|
||||||
.append( 'g' )
|
.append( 'g' )
|
||||||
.attr( 'class', 'focusspaces' )
|
.attr( 'class', 'focusspaces' )
|
||||||
.selectAll( '.focus' )
|
.selectAll( '.focus' )
|
||||||
.data( params.dateSpaces )
|
.data( dateSpaces )
|
||||||
.enter()
|
.enter()
|
||||||
.append( 'g' )
|
.append( 'g' )
|
||||||
.attr( 'class', 'focus' );
|
.attr( 'class', 'focus' );
|
||||||
|
@ -101,10 +165,10 @@ export const drawLines = ( node, data, params, xOffset ) => {
|
||||||
|
|
||||||
focusGrid
|
focusGrid
|
||||||
.append( 'line' )
|
.append( 'line' )
|
||||||
.attr( 'x1', d => params.xLineScale( moment( d.date ).toDate() ) + xOffset )
|
.attr( 'x1', d => scales.xScale( moment( d.date ).toDate() ) )
|
||||||
.attr( 'y1', 0 )
|
.attr( 'y1', 0 )
|
||||||
.attr( 'x2', d => params.xLineScale( moment( d.date ).toDate() ) + xOffset )
|
.attr( 'x2', d => scales.xScale( moment( d.date ).toDate() ) )
|
||||||
.attr( 'y2', params.height );
|
.attr( 'y2', height );
|
||||||
|
|
||||||
focusGrid
|
focusGrid
|
||||||
.selectAll( 'circle' )
|
.selectAll( 'circle' )
|
||||||
|
@ -115,8 +179,8 @@ export const drawLines = ( node, data, params, xOffset ) => {
|
||||||
.attr( 'fill', d => getColor( d.key, params.visibleKeys, params.colorScheme ) )
|
.attr( 'fill', d => getColor( d.key, params.visibleKeys, params.colorScheme ) )
|
||||||
.attr( 'stroke', '#fff' )
|
.attr( 'stroke', '#fff' )
|
||||||
.attr( 'stroke-width', lineStroke + 2 )
|
.attr( 'stroke-width', lineStroke + 2 )
|
||||||
.attr( 'cx', d => params.xLineScale( moment( d.date ).toDate() ) + xOffset )
|
.attr( 'cx', d => scales.xScale( moment( d.date ).toDate() ) )
|
||||||
.attr( 'cy', d => params.yScale( d.value ) );
|
.attr( 'cy', d => scales.yScale( d.value ) );
|
||||||
|
|
||||||
focus
|
focus
|
||||||
.append( 'rect' )
|
.append( 'rect' )
|
||||||
|
@ -124,18 +188,12 @@ export const drawLines = ( node, data, params, xOffset ) => {
|
||||||
.attr( 'x', d => d.start )
|
.attr( 'x', d => d.start )
|
||||||
.attr( 'y', 0 )
|
.attr( 'y', 0 )
|
||||||
.attr( 'width', d => d.width )
|
.attr( 'width', d => d.width )
|
||||||
.attr( 'height', params.height )
|
.attr( 'height', height )
|
||||||
.attr( 'opacity', 0 )
|
.attr( 'opacity', 0 )
|
||||||
.on( 'mouseover', ( d, i, nodes ) => {
|
.on( 'mouseover', ( d, i, nodes ) => {
|
||||||
const isTooltipLeftAligned = ( i === 0 || i === params.dateSpaces.length - 1 ) && params.uniqueDates.length > 1;
|
const isTooltipLeftAligned = ( i === 0 || i === dateSpaces.length - 1 ) && params.uniqueDates.length > 1;
|
||||||
const elementWidthRatio = isTooltipLeftAligned ? 0 : 0.5;
|
const elementWidthRatio = isTooltipLeftAligned ? 0 : 0.5;
|
||||||
const position = calculateTooltipPosition(
|
tooltip.show( data.find( e => e.date === d.date ), d3Event.target, nodes[ i ].parentNode, elementWidthRatio );
|
||||||
d3Event.target,
|
|
||||||
node.node(),
|
|
||||||
params.tooltipPosition,
|
|
||||||
elementWidthRatio
|
|
||||||
);
|
|
||||||
handleMouseOverLineChart( d.date, nodes[ i ].parentNode, node, data, params, position );
|
|
||||||
} )
|
} )
|
||||||
.on( 'mouseout', ( d, i, nodes ) => hideTooltip( nodes[ i ].parentNode, params.tooltip ) );
|
.on( 'mouseout', () => tooltip.hide() );
|
||||||
};
|
};
|
||||||
|
|
|
@ -52,13 +52,26 @@ export const getXLineScale = ( uniqueDates, width ) =>
|
||||||
] )
|
] )
|
||||||
.rangeRound( [ 0, width ] );
|
.rangeRound( [ 0, width ] );
|
||||||
|
|
||||||
|
const getMaxYValue = data => {
|
||||||
|
let maxYValue = Number.NEGATIVE_INFINITY;
|
||||||
|
data.map( d => {
|
||||||
|
for ( const [ key, item ] of Object.entries( d ) ) {
|
||||||
|
if ( key !== 'date' && Number.isFinite( item.value ) && item.value > maxYValue ) {
|
||||||
|
maxYValue = item.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
|
||||||
|
return maxYValue;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Describes and rounds the maximum y value to the nearest thousand, ten-thousand, million etc. In case it is a decimal number, ceils it.
|
* Describes and rounds the maximum y value to the nearest thousand, ten-thousand, million etc. In case it is a decimal number, ceils it.
|
||||||
* @param {array} lineData - from `getLineData`
|
* @param {array} data - The chart component's `data` prop.
|
||||||
* @returns {number} the maximum value in the timeseries multiplied by 4/3
|
* @returns {number} the maximum value in the timeseries multiplied by 4/3
|
||||||
*/
|
*/
|
||||||
export const getYMax = lineData => {
|
export const getYMax = data => {
|
||||||
const maxValue = Math.max( ...lineData.map( d => Math.max( ...d.values.map( date => date.value ) ) ) );
|
const maxValue = getMaxYValue( data );
|
||||||
if ( ! Number.isFinite( maxValue ) || maxValue <= 0 ) {
|
if ( ! Number.isFinite( maxValue ) || maxValue <= 0 ) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
@ -70,21 +83,10 @@ export const getYMax = lineData => {
|
||||||
/**
|
/**
|
||||||
* Describes getYScale
|
* Describes getYScale
|
||||||
* @param {number} height - calculated height of the charting space
|
* @param {number} height - calculated height of the charting space
|
||||||
* @param {number} yMax - from `getYMax`
|
* @param {number} yMax - maximum y value
|
||||||
* @returns {function} the D3 linear scale from 0 to the value from `getYMax`
|
* @returns {function} the D3 linear scale from 0 to the value from `getYMax`
|
||||||
*/
|
*/
|
||||||
export const getYScale = ( height, yMax ) =>
|
export const getYScale = ( height, yMax ) =>
|
||||||
d3ScaleLinear()
|
d3ScaleLinear()
|
||||||
.domain( [ 0, yMax === 0 ? 1 : yMax ] )
|
.domain( [ 0, yMax === 0 ? 1 : yMax ] )
|
||||||
.rangeRound( [ height, 0 ] );
|
.rangeRound( [ height, 0 ] );
|
||||||
|
|
||||||
/**
|
|
||||||
* Describes getyTickOffset
|
|
||||||
* @param {number} height - calculated height of the charting space
|
|
||||||
* @param {number} yMax - from `getYMax`
|
|
||||||
* @returns {function} the D3 linear scale from 0 to the value from `getYMax`, offset by 12 pixels down
|
|
||||||
*/
|
|
||||||
export const getYTickOffset = ( height, yMax ) =>
|
|
||||||
d3ScaleLinear()
|
|
||||||
.domain( [ 0, yMax === 0 ? 1 : yMax ] )
|
|
||||||
.rangeRound( [ height + 12, 12 ] );
|
|
||||||
|
|
|
@ -8,23 +8,14 @@ import { utcParse as d3UTCParse } from 'd3-time-format';
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import dummyOrders from './fixtures/dummy-orders';
|
import dummyOrders from './fixtures/dummy-orders';
|
||||||
import orderedDates from './fixtures/dummy-ordered-dates';
|
|
||||||
import orderedKeys from './fixtures/dummy-ordered-keys';
|
import orderedKeys from './fixtures/dummy-ordered-keys';
|
||||||
import {
|
import {
|
||||||
getDateSpaces,
|
|
||||||
getOrderedKeys,
|
getOrderedKeys,
|
||||||
getLineData,
|
isDataEmpty,
|
||||||
getUniqueKeys,
|
|
||||||
getUniqueDates,
|
|
||||||
} from '../index';
|
} from '../index';
|
||||||
import { getXLineScale } from '../scales';
|
|
||||||
|
|
||||||
const parseDate = d3UTCParse( '%Y-%m-%dT%H:%M:%S' );
|
const parseDate = d3UTCParse( '%Y-%m-%dT%H:%M:%S' );
|
||||||
const testUniqueKeys = getUniqueKeys( dummyOrders );
|
const testOrderedKeys = getOrderedKeys( dummyOrders );
|
||||||
const testOrderedKeys = getOrderedKeys( dummyOrders, testUniqueKeys );
|
|
||||||
const testLineData = getLineData( dummyOrders, testOrderedKeys );
|
|
||||||
const testUniqueDates = getUniqueDates( testLineData, parseDate );
|
|
||||||
const testXLineScale = getXLineScale( testUniqueDates, 100 );
|
|
||||||
|
|
||||||
describe( 'parseDate', () => {
|
describe( 'parseDate', () => {
|
||||||
it( 'correctly parse date in the expected format', () => {
|
it( 'correctly parse date in the expected format', () => {
|
||||||
|
@ -34,63 +25,68 @@ describe( 'parseDate', () => {
|
||||||
} );
|
} );
|
||||||
} );
|
} );
|
||||||
|
|
||||||
describe( 'getUniqueKeys', () => {
|
|
||||||
it( 'returns an array of keys excluding date', () => {
|
|
||||||
// sort is a mutating action so we need a copy
|
|
||||||
const testUniqueKeysClone = testUniqueKeys.slice();
|
|
||||||
const sortedAZKeys = orderedKeys.map( d => d.key ).slice();
|
|
||||||
expect( testUniqueKeysClone.sort() ).toEqual( sortedAZKeys.sort() );
|
|
||||||
} );
|
|
||||||
} );
|
|
||||||
|
|
||||||
describe( 'getOrderedKeys', () => {
|
describe( 'getOrderedKeys', () => {
|
||||||
it( 'returns an array of keys order by value from largest to smallest', () => {
|
it( 'returns an array of keys order by value from largest to smallest', () => {
|
||||||
expect( testOrderedKeys ).toEqual( orderedKeys );
|
expect( testOrderedKeys ).toEqual( orderedKeys );
|
||||||
} );
|
} );
|
||||||
} );
|
} );
|
||||||
|
|
||||||
describe( 'getLineData', () => {
|
describe( 'isDataEmpty', () => {
|
||||||
it( 'returns a sorted array of objects with category key', () => {
|
it( 'should return true when all data values are 0 and no baseValue is provided', () => {
|
||||||
expect( testLineData ).toBeInstanceOf( Array );
|
const data = [
|
||||||
expect( testLineData ).toHaveLength( 5 );
|
{
|
||||||
expect( testLineData.map( d => d.key ) ).toEqual( orderedKeys.map( d => d.key ) );
|
lorem: {
|
||||||
|
value: 0,
|
||||||
|
},
|
||||||
|
ipsum: {
|
||||||
|
value: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
expect( isDataEmpty( data ) ).toBeTruthy();
|
||||||
} );
|
} );
|
||||||
|
|
||||||
testLineData.forEach( d => {
|
it( 'should return true when all data values match the base value', () => {
|
||||||
it( 'ensure a key and that the values property is an array', () => {
|
const data = [
|
||||||
expect( d ).toHaveProperty( 'key' );
|
{
|
||||||
expect( d ).toHaveProperty( 'values' );
|
lorem: {
|
||||||
expect( d.values ).toBeInstanceOf( Array );
|
value: 100,
|
||||||
} );
|
},
|
||||||
|
ipsum: {
|
||||||
|
value: 100,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
expect( isDataEmpty( data, 100 ) ).toBeTruthy();
|
||||||
|
} );
|
||||||
|
|
||||||
it( 'ensure all unique dates exist in values array', () => {
|
it( 'should return false if at least one data values doesn\'t match the base value', () => {
|
||||||
const rowDates = d.values.map( row => row.date );
|
const data = [
|
||||||
expect( rowDates ).toEqual( orderedDates );
|
{
|
||||||
} );
|
lorem: {
|
||||||
|
value: 100,
|
||||||
|
},
|
||||||
|
ipsum: {
|
||||||
|
value: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
expect( isDataEmpty( data, 100 ) ).toBeFalsy();
|
||||||
|
} );
|
||||||
|
|
||||||
d.values.forEach( row => {
|
it( 'should return true when all data values match the base value or are null/undefined', () => {
|
||||||
it( 'ensure a date property and that the values property is an array with date (parseable) and value properties', () => {
|
const data = [
|
||||||
expect( row ).toHaveProperty( 'date' );
|
{
|
||||||
expect( row ).toHaveProperty( 'value' );
|
lorem: {
|
||||||
expect( parseDate( row.date ) ).not.toBeNull();
|
value: 100,
|
||||||
expect( typeof row.date ).toBe( 'string' );
|
},
|
||||||
expect( typeof row.value ).toBe( 'number' );
|
ipsum: {
|
||||||
} );
|
value: null,
|
||||||
} );
|
},
|
||||||
} );
|
dolor: {
|
||||||
} );
|
},
|
||||||
|
},
|
||||||
describe( 'getDateSpaces', () => {
|
];
|
||||||
it( 'return an array used to space out the mouseover rectangles, used for tooltips', () => {
|
expect( isDataEmpty( data, 100 ) ).toBeTruthy();
|
||||||
const testDateSpaces = getDateSpaces( dummyOrders, testUniqueDates, 100, testXLineScale );
|
|
||||||
expect( testDateSpaces[ 0 ].date ).toEqual( '2018-05-30T00:00:00' );
|
|
||||||
expect( testDateSpaces[ 0 ].start ).toEqual( 0 );
|
|
||||||
expect( testDateSpaces[ 0 ].width ).toEqual( 10 );
|
|
||||||
expect( testDateSpaces[ 3 ].date ).toEqual( '2018-06-02T00:00:00' );
|
|
||||||
expect( testDateSpaces[ 3 ].start ).toEqual( 50 );
|
|
||||||
expect( testDateSpaces[ 3 ].width ).toEqual( 20 );
|
|
||||||
expect( testDateSpaces[ testDateSpaces.length - 1 ].date ).toEqual( '2018-06-04T00:00:00' );
|
|
||||||
expect( testDateSpaces[ testDateSpaces.length - 1 ].start ).toEqual( 90 );
|
|
||||||
expect( testDateSpaces[ testDateSpaces.length - 1 ].width ).toEqual( 10 );
|
|
||||||
} );
|
} );
|
||||||
} );
|
} );
|
||||||
|
|
73
plugins/woocommerce-admin/packages/components/src/chart/d3chart/utils/test/line-chart.js
vendored
Normal file
73
plugins/woocommerce-admin/packages/components/src/chart/d3chart/utils/test/line-chart.js
vendored
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
/** @format */
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { utcParse as d3UTCParse } from 'd3-time-format';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import dummyOrders from './fixtures/dummy-orders';
|
||||||
|
import orderedDates from './fixtures/dummy-ordered-dates';
|
||||||
|
import orderedKeys from './fixtures/dummy-ordered-keys';
|
||||||
|
import {
|
||||||
|
getOrderedKeys,
|
||||||
|
getUniqueDates,
|
||||||
|
} from '../index';
|
||||||
|
import {
|
||||||
|
getDateSpaces,
|
||||||
|
getLineData,
|
||||||
|
} from '../line-chart';
|
||||||
|
import { getXLineScale } from '../scales';
|
||||||
|
|
||||||
|
const parseDate = d3UTCParse( '%Y-%m-%dT%H:%M:%S' );
|
||||||
|
const testOrderedKeys = getOrderedKeys( dummyOrders );
|
||||||
|
const testLineData = getLineData( dummyOrders, testOrderedKeys );
|
||||||
|
const testUniqueDates = getUniqueDates( dummyOrders, '%Y-%m-%dT%H:%M:%S' );
|
||||||
|
const testXLineScale = getXLineScale( testUniqueDates, 100 );
|
||||||
|
|
||||||
|
describe( 'getDateSpaces', () => {
|
||||||
|
it( 'return an array used to space out the mouseover rectangles, used for tooltips', () => {
|
||||||
|
const testDateSpaces = getDateSpaces( dummyOrders, testUniqueDates, 100, testXLineScale );
|
||||||
|
expect( testDateSpaces[ 0 ].date ).toEqual( '2018-05-30T00:00:00' );
|
||||||
|
expect( testDateSpaces[ 0 ].start ).toEqual( 0 );
|
||||||
|
expect( testDateSpaces[ 0 ].width ).toEqual( 10 );
|
||||||
|
expect( testDateSpaces[ 3 ].date ).toEqual( '2018-06-02T00:00:00' );
|
||||||
|
expect( testDateSpaces[ 3 ].start ).toEqual( 50 );
|
||||||
|
expect( testDateSpaces[ 3 ].width ).toEqual( 20 );
|
||||||
|
expect( testDateSpaces[ testDateSpaces.length - 1 ].date ).toEqual( '2018-06-04T00:00:00' );
|
||||||
|
expect( testDateSpaces[ testDateSpaces.length - 1 ].start ).toEqual( 90 );
|
||||||
|
expect( testDateSpaces[ testDateSpaces.length - 1 ].width ).toEqual( 10 );
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
|
||||||
|
describe( 'getLineData', () => {
|
||||||
|
it( 'returns a sorted array of objects with category key', () => {
|
||||||
|
expect( testLineData ).toBeInstanceOf( Array );
|
||||||
|
expect( testLineData ).toHaveLength( 5 );
|
||||||
|
expect( testLineData.map( d => d.key ) ).toEqual( orderedKeys.map( d => d.key ) );
|
||||||
|
} );
|
||||||
|
|
||||||
|
testLineData.forEach( d => {
|
||||||
|
it( 'ensure a key and that the values property is an array', () => {
|
||||||
|
expect( d ).toHaveProperty( 'key' );
|
||||||
|
expect( d ).toHaveProperty( 'values' );
|
||||||
|
expect( d.values ).toBeInstanceOf( Array );
|
||||||
|
} );
|
||||||
|
|
||||||
|
it( 'ensure all unique dates exist in values array', () => {
|
||||||
|
const rowDates = d.values.map( row => row.date );
|
||||||
|
expect( rowDates ).toEqual( orderedDates );
|
||||||
|
} );
|
||||||
|
|
||||||
|
d.values.forEach( row => {
|
||||||
|
it( 'ensure a date property and that the values property is an array with date (parseable) and value properties', () => {
|
||||||
|
expect( row ).toHaveProperty( 'date' );
|
||||||
|
expect( row ).toHaveProperty( 'value' );
|
||||||
|
expect( parseDate( row.date ) ).not.toBeNull();
|
||||||
|
expect( typeof row.date ).toBe( 'string' );
|
||||||
|
expect( typeof row.value ).toBe( 'number' );
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
} );
|
|
@ -2,7 +2,6 @@
|
||||||
/**
|
/**
|
||||||
* External dependencies
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
import { utcParse as d3UTCParse } from 'd3-time-format';
|
|
||||||
import { scaleBand, scaleLinear, scaleTime } from 'd3-scale';
|
import { scaleBand, scaleLinear, scaleTime } from 'd3-scale';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -11,11 +10,9 @@ import { scaleBand, scaleLinear, scaleTime } from 'd3-scale';
|
||||||
import dummyOrders from './fixtures/dummy-orders';
|
import dummyOrders from './fixtures/dummy-orders';
|
||||||
import {
|
import {
|
||||||
getOrderedKeys,
|
getOrderedKeys,
|
||||||
getLineData,
|
|
||||||
getUniqueKeys,
|
|
||||||
getUniqueDates,
|
getUniqueDates,
|
||||||
} from '../index';
|
} from '../index';
|
||||||
import { getXGroupScale, getXScale, getXLineScale, getYMax, getYScale, getYTickOffset } from '../scales';
|
import { getXGroupScale, getXScale, getXLineScale, getYMax, getYScale } from '../scales';
|
||||||
|
|
||||||
jest.mock( 'd3-scale', () => ( {
|
jest.mock( 'd3-scale', () => ( {
|
||||||
...require.requireActual( 'd3-scale' ),
|
...require.requireActual( 'd3-scale' ),
|
||||||
|
@ -37,13 +34,10 @@ jest.mock( 'd3-scale', () => ( {
|
||||||
} ),
|
} ),
|
||||||
} ) );
|
} ) );
|
||||||
|
|
||||||
const testUniqueKeys = getUniqueKeys( dummyOrders );
|
const testOrderedKeys = getOrderedKeys( dummyOrders );
|
||||||
const testOrderedKeys = getOrderedKeys( dummyOrders, testUniqueKeys );
|
|
||||||
const testLineData = getLineData( dummyOrders, testOrderedKeys );
|
|
||||||
|
|
||||||
describe( 'X scales', () => {
|
describe( 'X scales', () => {
|
||||||
const parseDate = d3UTCParse( '%Y-%m-%dT%H:%M:%S' );
|
const testUniqueDates = getUniqueDates( dummyOrders, '%Y-%m-%dT%H:%M:%S' );
|
||||||
const testUniqueDates = getUniqueDates( testLineData, parseDate );
|
|
||||||
|
|
||||||
describe( 'getXScale', () => {
|
describe( 'getXScale', () => {
|
||||||
it( 'creates band scale with correct parameters', () => {
|
it( 'creates band scale with correct parameters', () => {
|
||||||
|
@ -96,7 +90,7 @@ describe( 'X scales', () => {
|
||||||
describe( 'Y scales', () => {
|
describe( 'Y scales', () => {
|
||||||
describe( 'getYMax', () => {
|
describe( 'getYMax', () => {
|
||||||
it( 'calculate the correct maximum y value', () => {
|
it( 'calculate the correct maximum y value', () => {
|
||||||
expect( getYMax( testLineData ) ).toEqual( 15000000 );
|
expect( getYMax( dummyOrders ) ).toEqual( 15000000 );
|
||||||
} );
|
} );
|
||||||
|
|
||||||
it( 'return 0 if there is no line data', () => {
|
it( 'return 0 if there is no line data', () => {
|
||||||
|
@ -120,21 +114,4 @@ describe( 'Y scales', () => {
|
||||||
expect( lastArgs[ 0 ] ).toBeLessThan( lastArgs[ 1 ] );
|
expect( lastArgs[ 0 ] ).toBeLessThan( lastArgs[ 1 ] );
|
||||||
} );
|
} );
|
||||||
} );
|
} );
|
||||||
|
|
||||||
describe( 'getYTickOffset', () => {
|
|
||||||
it( 'creates linear scale with correct parameters', () => {
|
|
||||||
getYTickOffset( 100, 15000000 );
|
|
||||||
|
|
||||||
expect( scaleLinear().domain ).toHaveBeenLastCalledWith( [ 0, 15000000 ] );
|
|
||||||
expect( scaleLinear().rangeRound ).toHaveBeenLastCalledWith( [ 112, 12 ] );
|
|
||||||
} );
|
|
||||||
|
|
||||||
it( 'avoids the domain starting and ending at the same point when yMax is 0', () => {
|
|
||||||
getYTickOffset( 100, 0 );
|
|
||||||
|
|
||||||
const args = scaleLinear().domain.mock.calls;
|
|
||||||
const lastArgs = args[ args.length - 1 ][ 0 ];
|
|
||||||
expect( lastArgs[ 0 ] ).toBeLessThan( lastArgs[ 1 ] );
|
|
||||||
} );
|
|
||||||
} );
|
|
||||||
} );
|
} );
|
||||||
|
|
|
@ -11,138 +11,148 @@ import moment from 'moment';
|
||||||
*/
|
*/
|
||||||
import { getColor } from './color';
|
import { getColor } from './color';
|
||||||
|
|
||||||
export const hideTooltip = ( parentNode, tooltipNode ) => {
|
class ChartTooltip {
|
||||||
d3Select( parentNode )
|
constructor() {
|
||||||
.selectAll( '.barfocus, .focus-grid' )
|
this.ref = null;
|
||||||
.attr( 'opacity', '0' );
|
this.chart = null;
|
||||||
d3Select( tooltipNode )
|
this.position = '';
|
||||||
.style( 'visibility', 'hidden' );
|
this.title = '';
|
||||||
};
|
this.labelFormat = '';
|
||||||
|
this.valueFormat = '';
|
||||||
|
this.visibleKeys = '';
|
||||||
|
this.colorScheme = null;
|
||||||
|
this.margin = 24;
|
||||||
|
}
|
||||||
|
|
||||||
const calculateTooltipXPosition = (
|
calculateXPosition(
|
||||||
elementCoords,
|
elementCoords,
|
||||||
chartCoords,
|
chartCoords,
|
||||||
tooltipSize,
|
elementWidthRatio,
|
||||||
tooltipMargin,
|
) {
|
||||||
elementWidthRatio,
|
const tooltipSize = this.ref.getBoundingClientRect();
|
||||||
tooltipPosition
|
const d3BaseCoords = d3Select( '.d3-base' ).node().getBoundingClientRect();
|
||||||
) => {
|
const leftMargin = Math.max( d3BaseCoords.left, chartCoords.left );
|
||||||
const d3BaseCoords = d3Select( '.d3-base' ).node().getBoundingClientRect();
|
|
||||||
const leftMargin = Math.max( d3BaseCoords.left, chartCoords.left );
|
|
||||||
|
|
||||||
if ( tooltipPosition === 'below' ) {
|
if ( this.position === 'below' ) {
|
||||||
return Math.max(
|
return Math.max(
|
||||||
tooltipMargin,
|
this.margin,
|
||||||
Math.min(
|
Math.min(
|
||||||
elementCoords.left + elementCoords.width * 0.5 - tooltipSize.width / 2 - leftMargin,
|
elementCoords.left + elementCoords.width * 0.5 - tooltipSize.width / 2 - leftMargin,
|
||||||
d3BaseCoords.width - tooltipSize.width - tooltipMargin
|
d3BaseCoords.width - tooltipSize.width - this.margin
|
||||||
)
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const xPosition =
|
||||||
|
elementCoords.left + elementCoords.width * elementWidthRatio + this.margin - leftMargin;
|
||||||
|
|
||||||
|
if ( xPosition + tooltipSize.width + this.margin > d3BaseCoords.width ) {
|
||||||
|
return Math.max(
|
||||||
|
this.margin,
|
||||||
|
elementCoords.left +
|
||||||
|
elementCoords.width * ( 1 - elementWidthRatio ) -
|
||||||
|
tooltipSize.width -
|
||||||
|
this.margin -
|
||||||
|
leftMargin
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return xPosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
calculateYPosition(
|
||||||
|
elementCoords,
|
||||||
|
chartCoords,
|
||||||
|
) {
|
||||||
|
if ( this.position === 'below' ) {
|
||||||
|
return chartCoords.height;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tooltipSize = this.ref.getBoundingClientRect();
|
||||||
|
const yPosition = elementCoords.top + this.margin - chartCoords.top;
|
||||||
|
if ( yPosition + tooltipSize.height + this.margin > chartCoords.height ) {
|
||||||
|
return Math.max( 0, elementCoords.top - tooltipSize.height - this.margin - chartCoords.top );
|
||||||
|
}
|
||||||
|
|
||||||
|
return yPosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
calculatePosition( element, elementWidthRatio = 1 ) {
|
||||||
|
const elementCoords = element.getBoundingClientRect();
|
||||||
|
const chartCoords = this.chart.getBoundingClientRect();
|
||||||
|
|
||||||
|
if ( this.position === 'below' ) {
|
||||||
|
elementWidthRatio = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
x: this.calculateXPosition(
|
||||||
|
elementCoords,
|
||||||
|
chartCoords,
|
||||||
|
elementWidthRatio,
|
||||||
|
),
|
||||||
|
y: this.calculateYPosition(
|
||||||
|
elementCoords,
|
||||||
|
chartCoords,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
hide() {
|
||||||
|
d3Select( this.chart )
|
||||||
|
.selectAll( '.barfocus, .focus-grid' )
|
||||||
|
.attr( 'opacity', '0' );
|
||||||
|
d3Select( this.ref )
|
||||||
|
.style( 'visibility', 'hidden' );
|
||||||
|
}
|
||||||
|
|
||||||
|
getTooltipRowLabel( d, row ) {
|
||||||
|
if ( d[ row.key ].labelDate ) {
|
||||||
|
return this.labelFormat( moment( d[ row.key ].labelDate ).toDate() );
|
||||||
|
}
|
||||||
|
return row.key;
|
||||||
|
}
|
||||||
|
|
||||||
|
show( d, triggerElement, parentNode, elementWidthRatio = 1 ) {
|
||||||
|
if ( ! this.visibleKeys.length ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
d3Select( parentNode )
|
||||||
|
.select( '.focus-grid, .barfocus' )
|
||||||
|
.attr( 'opacity', '1' );
|
||||||
|
const position = this.calculatePosition( triggerElement, elementWidthRatio );
|
||||||
|
|
||||||
|
const keys = this.visibleKeys.map(
|
||||||
|
row => `
|
||||||
|
<li class="key-row">
|
||||||
|
<div class="key-container">
|
||||||
|
<span
|
||||||
|
class="key-color"
|
||||||
|
style="background-color: ${ getColor( row.key, this.visibleKeys, this.colorScheme ) }">
|
||||||
|
</span>
|
||||||
|
<span class="key-key">${ this.getTooltipRowLabel( d, row ) }</span>
|
||||||
|
</div>
|
||||||
|
<span class="key-value">${ this.valueFormat( d[ row.key ].value ) }</span>
|
||||||
|
</li>
|
||||||
|
`
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const tooltipTitle = this.title
|
||||||
|
? this.title
|
||||||
|
: this.labelFormat( moment( d.date ).toDate() );
|
||||||
|
|
||||||
|
d3Select( this.ref )
|
||||||
|
.style( 'left', position.x + 'px' )
|
||||||
|
.style( 'top', position.y + 'px' )
|
||||||
|
.style( 'visibility', 'visible' ).html( `
|
||||||
|
<div>
|
||||||
|
<h4>${ tooltipTitle }</h4>
|
||||||
|
<ul>
|
||||||
|
${ keys.join( '' ) }
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
` );
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const xPosition =
|
export default ChartTooltip;
|
||||||
elementCoords.left + elementCoords.width * elementWidthRatio + tooltipMargin - leftMargin;
|
|
||||||
|
|
||||||
if ( xPosition + tooltipSize.width + tooltipMargin > d3BaseCoords.width ) {
|
|
||||||
return Math.max(
|
|
||||||
tooltipMargin,
|
|
||||||
elementCoords.left +
|
|
||||||
elementCoords.width * ( 1 - elementWidthRatio ) -
|
|
||||||
tooltipSize.width -
|
|
||||||
tooltipMargin -
|
|
||||||
leftMargin
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return xPosition;
|
|
||||||
};
|
|
||||||
|
|
||||||
const calculateTooltipYPosition = (
|
|
||||||
elementCoords,
|
|
||||||
chartCoords,
|
|
||||||
tooltipSize,
|
|
||||||
tooltipMargin,
|
|
||||||
tooltipPosition
|
|
||||||
) => {
|
|
||||||
if ( tooltipPosition === 'below' ) {
|
|
||||||
return chartCoords.height;
|
|
||||||
}
|
|
||||||
|
|
||||||
const yPosition = elementCoords.top + tooltipMargin - chartCoords.top;
|
|
||||||
if ( yPosition + tooltipSize.height + tooltipMargin > chartCoords.height ) {
|
|
||||||
return Math.max( 0, elementCoords.top - tooltipSize.height - tooltipMargin - chartCoords.top );
|
|
||||||
}
|
|
||||||
|
|
||||||
return yPosition;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const calculateTooltipPosition = ( element, chart, tooltipPosition, elementWidthRatio = 1 ) => {
|
|
||||||
const elementCoords = element.getBoundingClientRect();
|
|
||||||
const chartCoords = chart.getBoundingClientRect();
|
|
||||||
const tooltipSize = d3Select( '.d3-chart__tooltip' )
|
|
||||||
.node()
|
|
||||||
.getBoundingClientRect();
|
|
||||||
const tooltipMargin = 24;
|
|
||||||
|
|
||||||
if ( tooltipPosition === 'below' ) {
|
|
||||||
elementWidthRatio = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
x: calculateTooltipXPosition(
|
|
||||||
elementCoords,
|
|
||||||
chartCoords,
|
|
||||||
tooltipSize,
|
|
||||||
tooltipMargin,
|
|
||||||
elementWidthRatio,
|
|
||||||
tooltipPosition
|
|
||||||
),
|
|
||||||
y: calculateTooltipYPosition(
|
|
||||||
elementCoords,
|
|
||||||
chartCoords,
|
|
||||||
tooltipSize,
|
|
||||||
tooltipMargin,
|
|
||||||
tooltipPosition
|
|
||||||
),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const getTooltipRowLabel = ( d, row, params ) => {
|
|
||||||
if ( d[ row.key ].labelDate ) {
|
|
||||||
return params.tooltipLabelFormat( moment( d[ row.key ].labelDate ).toDate() );
|
|
||||||
}
|
|
||||||
return row.key;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const showTooltip = ( params, d, position ) => {
|
|
||||||
const keys = params.visibleKeys.map(
|
|
||||||
row => `
|
|
||||||
<li class="key-row">
|
|
||||||
<div class="key-container">
|
|
||||||
<span
|
|
||||||
class="key-color"
|
|
||||||
style="background-color:${ getColor( row.key, params.visibleKeys, params.colorScheme ) }">
|
|
||||||
</span>
|
|
||||||
<span class="key-key">${ getTooltipRowLabel( d, row, params ) }</span>
|
|
||||||
</div>
|
|
||||||
<span class="key-value">${ params.tooltipValueFormat( d[ row.key ].value ) }</span>
|
|
||||||
</li>
|
|
||||||
`
|
|
||||||
);
|
|
||||||
|
|
||||||
const tooltipTitle = params.tooltipTitle
|
|
||||||
? params.tooltipTitle
|
|
||||||
: params.tooltipLabelFormat( moment( d.date ).toDate() );
|
|
||||||
|
|
||||||
d3Select( params.tooltip )
|
|
||||||
.style( 'left', position.x + 'px' )
|
|
||||||
.style( 'top', position.y + 'px' )
|
|
||||||
.style( 'visibility', 'visible' ).html( `
|
|
||||||
<div>
|
|
||||||
<h4>${ tooltipTitle }</h4>
|
|
||||||
<ul>
|
|
||||||
${ keys.join( '' ) }
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
` );
|
|
||||||
};
|
|
||||||
|
|
|
@ -266,7 +266,9 @@ class Chart extends Component {
|
||||||
render() {
|
render() {
|
||||||
const { interactiveLegend, orderedKeys, visibleData, width } = this.state;
|
const { interactiveLegend, orderedKeys, visibleData, width } = this.state;
|
||||||
const {
|
const {
|
||||||
|
baseValue,
|
||||||
dateParser,
|
dateParser,
|
||||||
|
emptyMessage,
|
||||||
interval,
|
interval,
|
||||||
isRequesting,
|
isRequesting,
|
||||||
isViewportLarge,
|
isViewportLarge,
|
||||||
|
@ -376,10 +378,12 @@ class Chart extends Component {
|
||||||
{ ! isRequesting &&
|
{ ! isRequesting &&
|
||||||
width > 0 && (
|
width > 0 && (
|
||||||
<D3Chart
|
<D3Chart
|
||||||
|
baseValue={ baseValue }
|
||||||
colorScheme={ d3InterpolateViridis }
|
colorScheme={ d3InterpolateViridis }
|
||||||
data={ visibleData }
|
data={ visibleData }
|
||||||
dateParser={ dateParser }
|
dateParser={ dateParser }
|
||||||
height={ chartHeight }
|
height={ chartHeight }
|
||||||
|
emptyMessage={ emptyMessage }
|
||||||
interval={ interval }
|
interval={ interval }
|
||||||
margin={ margin }
|
margin={ margin }
|
||||||
mode={ mode }
|
mode={ mode }
|
||||||
|
@ -411,6 +415,11 @@ Chart.propTypes = {
|
||||||
* Allowed intervals to show in a dropdown.
|
* Allowed intervals to show in a dropdown.
|
||||||
*/
|
*/
|
||||||
allowedIntervals: PropTypes.array,
|
allowedIntervals: PropTypes.array,
|
||||||
|
/**
|
||||||
|
* Base chart value. If no data value is different than the baseValue, the
|
||||||
|
* `emptyMessage` will be displayed if provided.
|
||||||
|
*/
|
||||||
|
baseValue: PropTypes.number,
|
||||||
/**
|
/**
|
||||||
* An array of data.
|
* An array of data.
|
||||||
*/
|
*/
|
||||||
|
@ -419,6 +428,11 @@ Chart.propTypes = {
|
||||||
* Format to parse dates into d3 time format
|
* Format to parse dates into d3 time format
|
||||||
*/
|
*/
|
||||||
dateParser: PropTypes.string.isRequired,
|
dateParser: PropTypes.string.isRequired,
|
||||||
|
/**
|
||||||
|
* The message to be displayed if there is no data to render. If no message is provided,
|
||||||
|
* nothing will be displayed.
|
||||||
|
*/
|
||||||
|
emptyMessage: PropTypes.string,
|
||||||
/**
|
/**
|
||||||
* Label describing the legend items.
|
* Label describing the legend items.
|
||||||
*/
|
*/
|
||||||
|
@ -500,6 +514,7 @@ Chart.propTypes = {
|
||||||
};
|
};
|
||||||
|
|
||||||
Chart.defaultProps = {
|
Chart.defaultProps = {
|
||||||
|
baseValue: 0,
|
||||||
data: [],
|
data: [],
|
||||||
dateParser: '%Y-%m-%dT%H:%M:%S',
|
dateParser: '%Y-%m-%dT%H:%M:%S',
|
||||||
interactiveLegend: true,
|
interactiveLegend: true,
|
||||||
|
|
|
@ -186,16 +186,17 @@ class DateFilter extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { config, filter, isEnglish } = this.props;
|
const { className, config, filter, isEnglish } = this.props;
|
||||||
const { rule } = filter;
|
const { rule } = filter;
|
||||||
const { labels, rules } = config;
|
const { labels, rules } = config;
|
||||||
const screenReaderText = this.getScreenReaderText( filter, config );
|
const screenReaderText = this.getScreenReaderText( filter, config );
|
||||||
const children = interpolateComponents( {
|
const children = interpolateComponents( {
|
||||||
mixedString: labels.title,
|
mixedString: labels.title,
|
||||||
components: {
|
components: {
|
||||||
|
title: <span className={ className } />,
|
||||||
rule: (
|
rule: (
|
||||||
<SelectControl
|
<SelectControl
|
||||||
className="woocommerce-filters-advanced__rule"
|
className={ classnames( className, 'woocommerce-filters-advanced__rule' ) }
|
||||||
options={ rules }
|
options={ rules }
|
||||||
value={ rule }
|
value={ rule }
|
||||||
onChange={ this.onRuleChange }
|
onChange={ this.onRuleChange }
|
||||||
|
@ -204,7 +205,7 @@ class DateFilter extends Component {
|
||||||
),
|
),
|
||||||
filter: (
|
filter: (
|
||||||
<div
|
<div
|
||||||
className={ classnames( 'woocommerce-filters-advanced__input-range', {
|
className={ classnames( className, 'woocommerce-filters-advanced__input-range', {
|
||||||
'is-between': 'between' === rule,
|
'is-between': 'between' === rule,
|
||||||
} ) }
|
} ) }
|
||||||
>
|
>
|
||||||
|
@ -215,7 +216,7 @@ class DateFilter extends Component {
|
||||||
} );
|
} );
|
||||||
/*eslint-disable jsx-a11y/no-noninteractive-tabindex*/
|
/*eslint-disable jsx-a11y/no-noninteractive-tabindex*/
|
||||||
return (
|
return (
|
||||||
<fieldset tabIndex="0">
|
<fieldset className="woocommerce-filters-advanced__line-item" tabIndex="0">
|
||||||
<legend className="screen-reader-text">{ labels.add || '' }</legend>
|
<legend className="screen-reader-text">{ labels.add || '' }</legend>
|
||||||
<div
|
<div
|
||||||
className={ classnames( 'woocommerce-filters-advanced__fieldset', {
|
className={ classnames( 'woocommerce-filters-advanced__fieldset', {
|
||||||
|
|
|
@ -9,6 +9,7 @@ import { partial, findIndex, difference, isEqual } from 'lodash';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import Gridicon from 'gridicons';
|
import Gridicon from 'gridicons';
|
||||||
import interpolateComponents from 'interpolate-components';
|
import interpolateComponents from 'interpolate-components';
|
||||||
|
import classnames from 'classnames';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* WooCommerce dependencies
|
* WooCommerce dependencies
|
||||||
|
@ -190,6 +191,7 @@ class AdvancedFilters extends Component {
|
||||||
<li className="woocommerce-filters-advanced__list-item" key={ key }>
|
<li className="woocommerce-filters-advanced__list-item" key={ key }>
|
||||||
{ 'SelectControl' === input.component && (
|
{ 'SelectControl' === input.component && (
|
||||||
<SelectFilter
|
<SelectFilter
|
||||||
|
className="woocommerce-filters-advanced__fieldset-item"
|
||||||
filter={ filter }
|
filter={ filter }
|
||||||
config={ config.filters[ key ] }
|
config={ config.filters[ key ] }
|
||||||
onFilterChange={ this.onFilterChange }
|
onFilterChange={ this.onFilterChange }
|
||||||
|
@ -198,6 +200,7 @@ class AdvancedFilters extends Component {
|
||||||
) }
|
) }
|
||||||
{ 'Search' === input.component && (
|
{ 'Search' === input.component && (
|
||||||
<SearchFilter
|
<SearchFilter
|
||||||
|
className="woocommerce-filters-advanced__fieldset-item"
|
||||||
filter={ filter }
|
filter={ filter }
|
||||||
config={ config.filters[ key ] }
|
config={ config.filters[ key ] }
|
||||||
onFilterChange={ this.onFilterChange }
|
onFilterChange={ this.onFilterChange }
|
||||||
|
@ -207,6 +210,7 @@ class AdvancedFilters extends Component {
|
||||||
) }
|
) }
|
||||||
{ 'Number' === input.component && (
|
{ 'Number' === input.component && (
|
||||||
<NumberFilter
|
<NumberFilter
|
||||||
|
className="woocommerce-filters-advanced__fieldset-item"
|
||||||
filter={ filter }
|
filter={ filter }
|
||||||
config={ config.filters[ key ] }
|
config={ config.filters[ key ] }
|
||||||
onFilterChange={ this.onFilterChange }
|
onFilterChange={ this.onFilterChange }
|
||||||
|
@ -216,6 +220,7 @@ class AdvancedFilters extends Component {
|
||||||
) }
|
) }
|
||||||
{ 'Currency' === input.component && (
|
{ 'Currency' === input.component && (
|
||||||
<NumberFilter
|
<NumberFilter
|
||||||
|
className="woocommerce-filters-advanced__fieldset-item"
|
||||||
filter={ filter }
|
filter={ filter }
|
||||||
config={ { ...config.filters[ key ], ...{ input: { type: 'currency', component: 'Currency' } } } }
|
config={ { ...config.filters[ key ], ...{ input: { type: 'currency', component: 'Currency' } } } }
|
||||||
onFilterChange={ this.onFilterChange }
|
onFilterChange={ this.onFilterChange }
|
||||||
|
@ -225,6 +230,7 @@ class AdvancedFilters extends Component {
|
||||||
) }
|
) }
|
||||||
{ 'Date' === input.component && (
|
{ 'Date' === input.component && (
|
||||||
<DateFilter
|
<DateFilter
|
||||||
|
className="woocommerce-filters-advanced__fieldset-item"
|
||||||
filter={ filter }
|
filter={ filter }
|
||||||
config={ config.filters[ key ] }
|
config={ config.filters[ key ] }
|
||||||
onFilterChange={ this.onFilterChange }
|
onFilterChange={ this.onFilterChange }
|
||||||
|
@ -234,7 +240,10 @@ class AdvancedFilters extends Component {
|
||||||
/>
|
/>
|
||||||
) }
|
) }
|
||||||
<IconButton
|
<IconButton
|
||||||
className="woocommerce-filters-advanced__remove"
|
className={ classnames(
|
||||||
|
'woocommerce-filters-advanced__line-item',
|
||||||
|
'woocommerce-filters-advanced__remove'
|
||||||
|
) }
|
||||||
label={ labels.remove }
|
label={ labels.remove }
|
||||||
onClick={ partial( this.removeFilter, key ) }
|
onClick={ partial( this.removeFilter, key ) }
|
||||||
icon={ <Gridicon icon="cross-small" /> }
|
icon={ <Gridicon icon="cross-small" /> }
|
||||||
|
|
|
@ -185,16 +185,17 @@ class NumberFilter extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { config, filter, onFilterChange, isEnglish } = this.props;
|
const { className, config, filter, onFilterChange, isEnglish } = this.props;
|
||||||
const { key, rule } = filter;
|
const { key, rule } = filter;
|
||||||
const { labels, rules } = config;
|
const { labels, rules } = config;
|
||||||
|
|
||||||
const children = interpolateComponents( {
|
const children = interpolateComponents( {
|
||||||
mixedString: labels.title,
|
mixedString: labels.title,
|
||||||
components: {
|
components: {
|
||||||
|
title: <span className={ className } />,
|
||||||
rule: (
|
rule: (
|
||||||
<SelectControl
|
<SelectControl
|
||||||
className="woocommerce-filters-advanced__rule"
|
className={ classnames( className, 'woocommerce-filters-advanced__rule' ) }
|
||||||
options={ rules }
|
options={ rules }
|
||||||
value={ rule }
|
value={ rule }
|
||||||
onChange={ partial( onFilterChange, key, 'rule' ) }
|
onChange={ partial( onFilterChange, key, 'rule' ) }
|
||||||
|
@ -203,7 +204,7 @@ class NumberFilter extends Component {
|
||||||
),
|
),
|
||||||
filter: (
|
filter: (
|
||||||
<div
|
<div
|
||||||
className={ classnames( 'woocommerce-filters-advanced__input-range', {
|
className={ classnames( className, 'woocommerce-filters-advanced__input-range', {
|
||||||
'is-between': 'between' === rule,
|
'is-between': 'between' === rule,
|
||||||
} ) }
|
} ) }
|
||||||
>
|
>
|
||||||
|
@ -217,7 +218,7 @@ class NumberFilter extends Component {
|
||||||
|
|
||||||
/*eslint-disable jsx-a11y/no-noninteractive-tabindex*/
|
/*eslint-disable jsx-a11y/no-noninteractive-tabindex*/
|
||||||
return (
|
return (
|
||||||
<fieldset tabIndex="0">
|
<fieldset className="woocommerce-filters-advanced__line-item" tabIndex="0">
|
||||||
<legend className="screen-reader-text">
|
<legend className="screen-reader-text">
|
||||||
{ labels.add || '' }
|
{ labels.add || '' }
|
||||||
</legend>
|
</legend>
|
||||||
|
|
|
@ -40,7 +40,12 @@ class SearchFilter extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
updateLabels( selected ) {
|
updateLabels( selected ) {
|
||||||
this.setState( { selected } );
|
const prevIds = this.state.selected.map( item => item.id );
|
||||||
|
const ids = selected.map( item => item.id );
|
||||||
|
|
||||||
|
if ( ! isEqual( ids.sort(), prevIds.sort() ) ) {
|
||||||
|
this.setState( { selected } );
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onSearchChange( values ) {
|
onSearchChange( values ) {
|
||||||
|
@ -72,16 +77,17 @@ class SearchFilter extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { config, filter, onFilterChange, isEnglish } = this.props;
|
const { className, config, filter, onFilterChange, isEnglish } = this.props;
|
||||||
const { selected } = this.state;
|
const { selected } = this.state;
|
||||||
const { key, rule } = filter;
|
const { key, rule } = filter;
|
||||||
const { input, labels, rules } = config;
|
const { input, labels, rules } = config;
|
||||||
const children = interpolateComponents( {
|
const children = interpolateComponents( {
|
||||||
mixedString: labels.title,
|
mixedString: labels.title,
|
||||||
components: {
|
components: {
|
||||||
|
title: <span className={ className } />,
|
||||||
rule: (
|
rule: (
|
||||||
<SelectControl
|
<SelectControl
|
||||||
className="woocommerce-filters-advanced__rule"
|
className={ classnames( className, 'woocommerce-filters-advanced__rule' ) }
|
||||||
options={ rules }
|
options={ rules }
|
||||||
value={ rule }
|
value={ rule }
|
||||||
onChange={ partial( onFilterChange, key, 'rule' ) }
|
onChange={ partial( onFilterChange, key, 'rule' ) }
|
||||||
|
@ -90,7 +96,7 @@ class SearchFilter extends Component {
|
||||||
),
|
),
|
||||||
filter: (
|
filter: (
|
||||||
<Search
|
<Search
|
||||||
className="woocommerce-filters-advanced__input"
|
className={ classnames( className, 'woocommerce-filters-advanced__input' ) }
|
||||||
onChange={ this.onSearchChange }
|
onChange={ this.onSearchChange }
|
||||||
type={ input.type }
|
type={ input.type }
|
||||||
placeholder={ labels.placeholder }
|
placeholder={ labels.placeholder }
|
||||||
|
@ -106,7 +112,7 @@ class SearchFilter extends Component {
|
||||||
|
|
||||||
/*eslint-disable jsx-a11y/no-noninteractive-tabindex*/
|
/*eslint-disable jsx-a11y/no-noninteractive-tabindex*/
|
||||||
return (
|
return (
|
||||||
<fieldset tabIndex="0">
|
<fieldset className="woocommerce-filters-advanced__line-item" tabIndex="0">
|
||||||
<legend className="screen-reader-text">
|
<legend className="screen-reader-text">
|
||||||
{ labels.add || '' }
|
{ labels.add || '' }
|
||||||
</legend>
|
</legend>
|
||||||
|
|
|
@ -64,16 +64,17 @@ class SelectFilter extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { config, filter, onFilterChange, isEnglish } = this.props;
|
const { className, config, filter, onFilterChange, isEnglish } = this.props;
|
||||||
const { options } = this.state;
|
const { options } = this.state;
|
||||||
const { key, rule, value } = filter;
|
const { key, rule, value } = filter;
|
||||||
const { labels, rules } = config;
|
const { labels, rules } = config;
|
||||||
const children = interpolateComponents( {
|
const children = interpolateComponents( {
|
||||||
mixedString: labels.title,
|
mixedString: labels.title,
|
||||||
components: {
|
components: {
|
||||||
|
title: <span className={ className } />,
|
||||||
rule: (
|
rule: (
|
||||||
<SelectControl
|
<SelectControl
|
||||||
className="woocommerce-filters-advanced__rule"
|
className={ classnames( className, 'woocommerce-filters-advanced__rule' ) }
|
||||||
options={ rules }
|
options={ rules }
|
||||||
value={ rule }
|
value={ rule }
|
||||||
onChange={ partial( onFilterChange, key, 'rule' ) }
|
onChange={ partial( onFilterChange, key, 'rule' ) }
|
||||||
|
@ -82,7 +83,7 @@ class SelectFilter extends Component {
|
||||||
),
|
),
|
||||||
filter: options ? (
|
filter: options ? (
|
||||||
<SelectControl
|
<SelectControl
|
||||||
className="woocommerce-filters-advanced__input"
|
className={ classnames( className, 'woocommerce-filters-advanced__input' ) }
|
||||||
options={ options }
|
options={ options }
|
||||||
value={ value }
|
value={ value }
|
||||||
onChange={ partial( onFilterChange, filter.key, 'value' ) }
|
onChange={ partial( onFilterChange, filter.key, 'value' ) }
|
||||||
|
@ -98,7 +99,7 @@ class SelectFilter extends Component {
|
||||||
|
|
||||||
/*eslint-disable jsx-a11y/no-noninteractive-tabindex*/
|
/*eslint-disable jsx-a11y/no-noninteractive-tabindex*/
|
||||||
return (
|
return (
|
||||||
<fieldset tabIndex="0">
|
<fieldset className="woocommerce-filters-advanced__line-item" tabIndex="0">
|
||||||
<legend className="screen-reader-text">
|
<legend className="screen-reader-text">
|
||||||
{ labels.add || '' }
|
{ labels.add || '' }
|
||||||
</legend>
|
</legend>
|
||||||
|
|
|
@ -45,18 +45,22 @@
|
||||||
padding: 0 $gap 0 0;
|
padding: 0 $gap 0 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: auto 40px;
|
grid-template-columns: 1fr 40px;
|
||||||
background-color: $core-grey-light-100;
|
background-color: $core-grey-light-100;
|
||||||
border-bottom: 1px solid $core-grey-light-700;
|
border-bottom: 1px solid $core-grey-light-700;
|
||||||
|
|
||||||
fieldset {
|
|
||||||
padding: $gap-smaller $gap-smaller $gap-smaller $gap;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: $core-grey-light-200;
|
background-color: $core-grey-light-200;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.woocommerce-filters-advanced__line-item {
|
||||||
|
@include set-grid-item-position( 2, 2 );
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldset {
|
||||||
|
padding: $gap-smaller $gap-smaller $gap-smaller $gap;
|
||||||
|
}
|
||||||
|
|
||||||
.woocommerce-filters-advanced__remove {
|
.woocommerce-filters-advanced__remove {
|
||||||
width: 40px;
|
width: 40px;
|
||||||
height: 38px;
|
height: 38px;
|
||||||
|
@ -125,7 +129,16 @@
|
||||||
|
|
||||||
&.is-english {
|
&.is-english {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 100px 150px auto;
|
grid-template-columns: 100px 150px 1fr;
|
||||||
|
|
||||||
|
.woocommerce-filters-advanced__fieldset-item {
|
||||||
|
@include set-grid-item-position( 3, 3 );
|
||||||
|
|
||||||
|
&:nth-child(1) {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@include breakpoint( '<782px' ) {
|
@include breakpoint( '<782px' ) {
|
||||||
display: block;
|
display: block;
|
||||||
|
@ -151,6 +164,7 @@
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
fill: currentColor;
|
fill: currentColor;
|
||||||
|
margin: 0 6px 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.components-icon-button:not(:disabled):not([aria-disabled='true']):not(.is-default):hover {
|
&.components-icon-button:not(:disabled):not([aria-disabled='true']):not(.is-default):hover {
|
||||||
|
|
|
@ -59,7 +59,7 @@ export class Autocomplete extends Component {
|
||||||
this.reset = this.reset.bind( this );
|
this.reset = this.reset.bind( this );
|
||||||
this.search = this.search.bind( this );
|
this.search = this.search.bind( this );
|
||||||
this.handleKeyDown = this.handleKeyDown.bind( this );
|
this.handleKeyDown = this.handleKeyDown.bind( this );
|
||||||
this.debouncedLoadOptions = debounce( this.loadOptions, 250 );
|
this.debouncedLoadOptions = debounce( this.loadOptions, 400 );
|
||||||
|
|
||||||
this.state = this.constructor.getInitialState();
|
this.state = this.constructor.getInitialState();
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,7 @@ import Flag from '../../flag';
|
||||||
export default {
|
export default {
|
||||||
name: 'countries',
|
name: 'countries',
|
||||||
className: 'woocommerce-search__country-result',
|
className: 'woocommerce-search__country-result',
|
||||||
|
isDebounced: true,
|
||||||
options() {
|
options() {
|
||||||
return wcSettings.dataEndpoints.countries || [];
|
return wcSettings.dataEndpoints.countries || [];
|
||||||
},
|
},
|
||||||
|
|
|
@ -16,8 +16,6 @@ import { stringifyQuery } from '@woocommerce/navigation';
|
||||||
*/
|
*/
|
||||||
import { computeSuggestionMatch } from './utils';
|
import { computeSuggestionMatch } from './utils';
|
||||||
|
|
||||||
const getName = customer => [ customer.first_name, customer.last_name ].filter( Boolean ).join( ' ' );
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A customer completer.
|
* A customer completer.
|
||||||
* See https://github.com/WordPress/gutenberg/tree/master/packages/components/src/autocomplete#the-completer-interface
|
* See https://github.com/WordPress/gutenberg/tree/master/packages/components/src/autocomplete#the-completer-interface
|
||||||
|
@ -40,7 +38,7 @@ export default {
|
||||||
},
|
},
|
||||||
isDebounced: true,
|
isDebounced: true,
|
||||||
getOptionKeywords( customer ) {
|
getOptionKeywords( customer ) {
|
||||||
return [ getName( customer ) ];
|
return [ customer.name ];
|
||||||
},
|
},
|
||||||
getFreeTextOptions( query ) {
|
getFreeTextOptions( query ) {
|
||||||
const label = (
|
const label = (
|
||||||
|
@ -56,15 +54,15 @@ export default {
|
||||||
const nameOption = {
|
const nameOption = {
|
||||||
key: 'name',
|
key: 'name',
|
||||||
label: label,
|
label: label,
|
||||||
value: { id: query, first_name: query },
|
value: { id: query, name: query },
|
||||||
};
|
};
|
||||||
|
|
||||||
return [ nameOption ];
|
return [ nameOption ];
|
||||||
},
|
},
|
||||||
getOptionLabel( customer, query ) {
|
getOptionLabel( customer, query ) {
|
||||||
const match = computeSuggestionMatch( getName( customer ), query ) || {};
|
const match = computeSuggestionMatch( customer.name, query ) || {};
|
||||||
return [
|
return [
|
||||||
<span key="name" className="woocommerce-search__result-name" aria-label={ getName( customer ) }>
|
<span key="name" className="woocommerce-search__result-name" aria-label={ customer.name }>
|
||||||
{ match.suggestionBeforeMatch }
|
{ match.suggestionBeforeMatch }
|
||||||
<strong className="components-form-token-field__suggestion-match">
|
<strong className="components-form-token-field__suggestion-match">
|
||||||
{ match.suggestionMatch }
|
{ match.suggestionMatch }
|
||||||
|
@ -78,7 +76,7 @@ export default {
|
||||||
getOptionCompletion( customer ) {
|
getOptionCompletion( customer ) {
|
||||||
return {
|
return {
|
||||||
id: customer.id,
|
id: customer.id,
|
||||||
label: getName( customer ),
|
label: customer.name,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue