fix merge conflicts
This commit is contained in:
commit
8744a8d6c9
|
@ -185,7 +185,7 @@ install_deps() {
|
|||
|
||||
if [ "$TRAVIS_PULL_REQUEST_BRANCH" != "" ]; then
|
||||
BRANCH="$(sed 's/#/%23/' <<<$BRANCH)"
|
||||
# Install wc-admin, the correct branch, if running from Travis CI.
|
||||
# Install woocommerce-admin, the correct branch, if running from Travis CI.
|
||||
php wp-cli.phar plugin install https://github.com/$REPO/archive/$BRANCH.zip --activate
|
||||
fi
|
||||
|
||||
|
|
|
@ -3,8 +3,9 @@
|
|||
* External dependencies
|
||||
*/
|
||||
import { applyFilters } from '@wordpress/hooks';
|
||||
import { Component } from '@wordpress/element';
|
||||
import { Component, createRef, Fragment } from '@wordpress/element';
|
||||
import { compose } from '@wordpress/compose';
|
||||
import { focus } from '@wordpress/dom';
|
||||
import { withDispatch } from '@wordpress/data';
|
||||
import { get } from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
|
@ -24,6 +25,8 @@ import { QUERY_DEFAULTS } from 'wc-api/constants';
|
|||
import withSelect from 'wc-api/with-select';
|
||||
import { extendTableData } from './utils';
|
||||
|
||||
import './style.scss';
|
||||
|
||||
const TABLE_FILTER = 'woocommerce_admin_report_table';
|
||||
|
||||
/**
|
||||
|
@ -34,6 +37,8 @@ class ReportTable extends Component {
|
|||
super( props );
|
||||
|
||||
this.onColumnsChange = this.onColumnsChange.bind( this );
|
||||
this.onPageChange = this.onPageChange.bind( this );
|
||||
this.scrollPointRef = createRef();
|
||||
}
|
||||
|
||||
onColumnsChange( shownColumns ) {
|
||||
|
@ -49,6 +54,18 @@ class ReportTable extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
onPageChange() {
|
||||
this.scrollPointRef.current.scrollIntoView();
|
||||
const tableElement = this.scrollPointRef.current.nextSibling.querySelector(
|
||||
'.woocommerce-table__table'
|
||||
);
|
||||
const focusableElements = focus.focusable.find( tableElement );
|
||||
|
||||
if ( focusableElements.length ) {
|
||||
focusableElements[ 0 ].focus();
|
||||
}
|
||||
}
|
||||
|
||||
filterShownHeaders( headers, hiddenKeys ) {
|
||||
if ( ! hiddenKeys || ! hiddenKeys.length ) {
|
||||
return headers;
|
||||
|
@ -102,19 +119,27 @@ class ReportTable extends Component {
|
|||
const filteredHeaders = this.filterShownHeaders( headers, userPrefColumns );
|
||||
|
||||
return (
|
||||
<TableCard
|
||||
downloadable={ downloadable }
|
||||
headers={ filteredHeaders }
|
||||
ids={ ids }
|
||||
isLoading={ isLoading }
|
||||
onQueryChange={ onQueryChange }
|
||||
onColumnsChange={ this.onColumnsChange }
|
||||
rows={ rows }
|
||||
rowsPerPage={ parseInt( query.per_page ) || QUERY_DEFAULTS.pageSize }
|
||||
summary={ summary }
|
||||
totalRows={ totalResults }
|
||||
{ ...tableProps }
|
||||
/>
|
||||
<Fragment>
|
||||
<div
|
||||
className="woocommerce-report-table__scroll-point"
|
||||
ref={ this.scrollPointRef }
|
||||
aria-hidden
|
||||
/>
|
||||
<TableCard
|
||||
downloadable={ downloadable }
|
||||
headers={ filteredHeaders }
|
||||
ids={ ids }
|
||||
isLoading={ isLoading }
|
||||
onQueryChange={ onQueryChange }
|
||||
onColumnsChange={ this.onColumnsChange }
|
||||
onPageChange={ this.onPageChange }
|
||||
rows={ rows }
|
||||
rowsPerPage={ parseInt( query.per_page ) || QUERY_DEFAULTS.pageSize }
|
||||
summary={ summary }
|
||||
totalRows={ totalResults }
|
||||
{ ...tableProps }
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
/** @format */
|
||||
|
||||
.woocommerce-report-table__scroll-point {
|
||||
position: relative;
|
||||
top: -#{$adminbar-height + $gap};
|
||||
|
||||
@include breakpoint( '<782px' ) {
|
||||
top: -#{$adminbar-height-mobile + $gap};
|
||||
}
|
||||
|
||||
.woocommerce-feature-enabled-activity-panels & {
|
||||
top: -#{$adminbar-height + $large-header-height + $gap};
|
||||
|
||||
@include breakpoint( '<782px' ) {
|
||||
top: -#{$adminbar-height-mobile + $small-header-height + $gap};
|
||||
}
|
||||
|
||||
@include breakpoint( '782px-960px' ) {
|
||||
top: -#{$adminbar-height + $medium-header-height + $gap};
|
||||
}
|
||||
}
|
||||
}
|
|
@ -5,10 +5,10 @@ The core reports offered by WooCommerce live in this folder. The Header is added
|
|||
|
||||
## Extending Reports
|
||||
|
||||
New reports can be added by third-parties without altering `woocommerce-admin`, by hooking into the reports filter, `woocommerce-reports-list`. For example:
|
||||
New reports can be added by third-parties without altering `woocommerce-admin`, by hooking into the reports filter, `woocommerce_admin_reports_list`. For example:
|
||||
|
||||
```js
|
||||
addFilter( 'woocommerce-reports-list', 'wc-example/my-report', pages => {
|
||||
addFilter( 'woocommerce_admin_reports_list', 'analytics/my-report', pages => {
|
||||
return [
|
||||
...pages,
|
||||
{
|
||||
|
@ -33,4 +33,4 @@ The component will get the following props:
|
|||
- `pathMatch` (string): The route matched for this view, should always be `/analytics/:report`.
|
||||
- `params` (object): This will contain the `report` from the path, which should match `report` in the page object.
|
||||
|
||||
**Note:** Adding your page to `woocommerce-reports-list` does not add the item to the admin menu, you'll need to do that in PHP with `wc_admin_register_page`.
|
||||
**Note:** Adding your page to `woocommerce_admin_reports_list` does not add the item to the admin menu, you'll need to do that in PHP with the `woocommerce_admin_report_menu_items` filter.
|
||||
|
|
|
@ -144,7 +144,7 @@ export const advancedFilters = {
|
|||
getLabels: getCouponLabels,
|
||||
},
|
||||
},
|
||||
customer: {
|
||||
customer_type: {
|
||||
labels: {
|
||||
add: __( 'Customer Type', 'woocommerce-admin' ),
|
||||
remove: __( 'Remove customer filter', 'woocommerce-admin' ),
|
||||
|
|
|
@ -58,9 +58,9 @@ export const analyticsSettings = applyFilters( SETTINGS_FILTER, [
|
|||
'woocommerce-admin'
|
||||
);
|
||||
|
||||
apiFetch( { path: '/wc/v3/system_status/tools/rebuild_stats', method: 'PUT' } )
|
||||
apiFetch( { path: '/wc/v4/reports/import', method: 'PUT' } )
|
||||
.then( response => {
|
||||
if ( response.success ) {
|
||||
if ( 'success' === response.status ) {
|
||||
addNotice( { status: 'success', message: response.message } );
|
||||
// @todo This should be changed to detect when the lookup table population is complete.
|
||||
setTimeout( () => resolve(), 300000 );
|
||||
|
|
|
@ -0,0 +1,87 @@
|
|||
/** @format */
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { Button } from '@wordpress/components';
|
||||
import { Fragment } from '@wordpress/element';
|
||||
|
||||
function HistoricalDataActions( {
|
||||
customersProgress,
|
||||
customersTotal,
|
||||
hasImportedData,
|
||||
inProgress,
|
||||
ordersProgress,
|
||||
ordersTotal,
|
||||
} ) {
|
||||
const getActions = () => {
|
||||
// An import is currently in progress
|
||||
if ( inProgress ) {
|
||||
return (
|
||||
<Fragment>
|
||||
<Button
|
||||
className="woocommerce-settings-historical-data__action-button"
|
||||
isPrimary
|
||||
onClick={ () => null }
|
||||
>
|
||||
{ __( 'Stop Import', 'woocommerce-admin' ) }
|
||||
</Button>
|
||||
<div className="woocommerce-setting__help woocommerce-settings-historical-data__action-help">
|
||||
{ __(
|
||||
'Imported data will not be lost if the import is stopped.',
|
||||
'woocommerce-admin'
|
||||
) }
|
||||
<br />
|
||||
{ __(
|
||||
'Navigating away from this page will not affect the import.',
|
||||
'woocommerce-admin'
|
||||
) }
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
// Has no imported data
|
||||
if ( ! hasImportedData ) {
|
||||
return (
|
||||
<Button isPrimary onClick={ () => null }>
|
||||
{ __( 'Start', 'woocommerce-admin' ) }
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
// Has imported all possible data
|
||||
if ( customersProgress === customersTotal && ordersProgress === ordersTotal ) {
|
||||
return (
|
||||
<Fragment>
|
||||
<Button isDefault onClick={ () => null }>
|
||||
{ __( 'Re-import Data', 'woocommerce-admin' ) }
|
||||
</Button>
|
||||
<Button isDefault onClick={ () => null }>
|
||||
{ __( 'Delete Previously Imported Data', 'woocommerce-admin' ) }
|
||||
</Button>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
// It's not in progress and has some imported data
|
||||
return (
|
||||
<Fragment>
|
||||
<Button isPrimary onClick={ () => null }>
|
||||
{ __( 'Start', 'woocommerce-admin' ) }
|
||||
</Button>
|
||||
<Button isDefault onClick={ () => null }>
|
||||
{ __( 'Delete Previously Imported Data', 'woocommerce-admin' ) }
|
||||
</Button>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="woocommerce-settings__actions woocommerce-settings-historical-data__actions">
|
||||
{ getActions() }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default HistoricalDataActions;
|
|
@ -0,0 +1,174 @@
|
|||
/** @format */
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { Component, Fragment } from '@wordpress/element';
|
||||
import moment from 'moment';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import HistoricalDataActions from './actions';
|
||||
import HistoricalDataPeriodSelector from './period-selector';
|
||||
import HistoricalDataProgress from './progress';
|
||||
import HistoricalDataStatus from './status';
|
||||
import HistoricalDataSkipCheckbox from './skip-checkbox';
|
||||
import withSelect from 'wc-api/with-select';
|
||||
import './style.scss';
|
||||
|
||||
class HistoricalData extends Component {
|
||||
constructor() {
|
||||
super( ...arguments );
|
||||
|
||||
this.dateFormat = __( 'MM/DD/YYYY', 'woocommerce-admin' );
|
||||
|
||||
this.state = {
|
||||
period: {
|
||||
date: moment().format( this.dateFormat ),
|
||||
label: 'all',
|
||||
},
|
||||
skipChecked: true,
|
||||
};
|
||||
|
||||
this.onDateChange = this.onDateChange.bind( this );
|
||||
this.onPeriodChange = this.onPeriodChange.bind( this );
|
||||
this.onSkipChange = this.onSkipChange.bind( this );
|
||||
}
|
||||
|
||||
onPeriodChange( val ) {
|
||||
this.setState( {
|
||||
period: {
|
||||
...this.state.period,
|
||||
label: val,
|
||||
},
|
||||
} );
|
||||
}
|
||||
|
||||
onDateChange( val ) {
|
||||
this.setState( {
|
||||
period: {
|
||||
date: val,
|
||||
label: 'custom',
|
||||
},
|
||||
} );
|
||||
}
|
||||
|
||||
onSkipChange( val ) {
|
||||
this.setState( {
|
||||
skipChecked: val,
|
||||
} );
|
||||
}
|
||||
|
||||
getStatus() {
|
||||
const {
|
||||
customersProgress,
|
||||
customersTotal,
|
||||
inProgress,
|
||||
ordersProgress,
|
||||
ordersTotal,
|
||||
} = this.props;
|
||||
|
||||
if ( inProgress ) {
|
||||
if ( customersProgress < customersTotal ) {
|
||||
return 'customers';
|
||||
}
|
||||
if ( ordersProgress < ordersTotal ) {
|
||||
return 'orders';
|
||||
}
|
||||
return 'finalizing';
|
||||
}
|
||||
if (
|
||||
( customersTotal > 0 || ordersTotal > 0 ) &&
|
||||
customersProgress === customersTotal &&
|
||||
ordersProgress === ordersTotal
|
||||
) {
|
||||
return 'finished';
|
||||
}
|
||||
return 'ready';
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
customersProgress,
|
||||
customersTotal,
|
||||
hasImportedData,
|
||||
importDate,
|
||||
inProgress,
|
||||
ordersProgress,
|
||||
ordersTotal,
|
||||
} = this.props;
|
||||
const { period, skipChecked } = this.state;
|
||||
const hasImportedAllData =
|
||||
! inProgress &&
|
||||
hasImportedData &&
|
||||
customersProgress === customersTotal &&
|
||||
ordersProgress === ordersTotal;
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<div className="woocommerce-setting">
|
||||
<div className="woocommerce-setting__label" id="import-historical-data-label">
|
||||
{ __( 'Import Historical Data:', 'woocommerce-admin' ) }
|
||||
</div>
|
||||
<div className="woocommerce-setting__input">
|
||||
<span className="woocommerce-setting__help">
|
||||
{ __(
|
||||
'This tool populates historical analytics data by processing customers ' +
|
||||
'and orders created prior to activating WooCommerce Admin.',
|
||||
'woocommerce-admin'
|
||||
) }
|
||||
</span>
|
||||
{ ! hasImportedAllData && (
|
||||
<Fragment>
|
||||
<HistoricalDataPeriodSelector
|
||||
dateFormat={ this.dateFormat }
|
||||
disabled={ inProgress }
|
||||
onPeriodChange={ this.onPeriodChange }
|
||||
onDateChange={ this.onDateChange }
|
||||
value={ period }
|
||||
/>
|
||||
<HistoricalDataSkipCheckbox
|
||||
disabled={ inProgress }
|
||||
checked={ skipChecked }
|
||||
onChange={ this.onSkipChange }
|
||||
/>
|
||||
<HistoricalDataProgress
|
||||
label={ __( 'Registered Customers', 'woocommerce-admin' ) }
|
||||
progress={ customersProgress }
|
||||
total={ customersTotal }
|
||||
/>
|
||||
<HistoricalDataProgress
|
||||
label={ __( 'Orders', 'woocommerce-admin' ) }
|
||||
progress={ ordersProgress }
|
||||
total={ ordersTotal }
|
||||
/>
|
||||
</Fragment>
|
||||
) }
|
||||
<HistoricalDataStatus importDate={ importDate } status={ this.getStatus() } />
|
||||
</div>
|
||||
</div>
|
||||
<HistoricalDataActions
|
||||
customersProgress={ customersProgress }
|
||||
customersTotal={ customersTotal }
|
||||
hasImportedData={ hasImportedData }
|
||||
inProgress={ inProgress }
|
||||
ordersProgress={ ordersProgress }
|
||||
ordersTotal={ ordersTotal }
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withSelect( () => {
|
||||
return {
|
||||
customersProgress: 0,
|
||||
customersTotal: 0,
|
||||
hasImportedData: false,
|
||||
importDate: '2019-04-01',
|
||||
inProgress: false,
|
||||
ordersProgress: 0,
|
||||
ordersTotal: 0,
|
||||
};
|
||||
} )( HistoricalData );
|
|
@ -0,0 +1,85 @@
|
|||
/** @format */
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import moment from 'moment';
|
||||
import { SelectControl } from '@wordpress/components';
|
||||
|
||||
/**
|
||||
* WooCommerce dependencies
|
||||
*/
|
||||
import { DatePicker } from '@woocommerce/components';
|
||||
import { dateValidationMessages } from '@woocommerce/date';
|
||||
|
||||
function HistoricalDataPeriodSelector( {
|
||||
dateFormat,
|
||||
disabled,
|
||||
onDateChange,
|
||||
onPeriodChange,
|
||||
value,
|
||||
} ) {
|
||||
const onSelectChange = val => {
|
||||
onPeriodChange( val );
|
||||
};
|
||||
const onDatePickerChange = val => {
|
||||
if ( val.date && val.date.isValid ) {
|
||||
onDateChange( val.date.format( dateFormat ) );
|
||||
} else {
|
||||
onDateChange( val.text );
|
||||
}
|
||||
};
|
||||
const getDatePickerError = momentDate => {
|
||||
if ( ! momentDate.isValid() || value.date.length !== dateFormat.length ) {
|
||||
return dateValidationMessages.invalid;
|
||||
}
|
||||
if ( momentDate.isAfter( new Date(), 'day' ) ) {
|
||||
return dateValidationMessages.future;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
const getDatePicker = () => {
|
||||
const momentDate = moment( value.date, dateFormat );
|
||||
return (
|
||||
<div className="woocommerce-settings-historical-data__column">
|
||||
<div className="woocommerce-settings-historical-data__column-label">
|
||||
{ __( 'Beginning on', 'woocommerce-admin' ) }
|
||||
</div>
|
||||
<DatePicker
|
||||
date={ momentDate.isValid() ? momentDate.toDate() : null }
|
||||
dateFormat={ dateFormat }
|
||||
disabled={ disabled }
|
||||
error={ getDatePickerError( momentDate ) }
|
||||
isInvalidDate={ date => moment( date ).isAfter( new Date(), 'day' ) }
|
||||
onUpdate={ onDatePickerChange }
|
||||
text={ value.date }
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="woocommerce-settings-historical-data__columns">
|
||||
<div className="woocommerce-settings-historical-data__column">
|
||||
<SelectControl
|
||||
label={ __( 'Import Historical Data', 'woocommerce-admin' ) }
|
||||
value={ value.label }
|
||||
disabled={ disabled }
|
||||
onChange={ onSelectChange }
|
||||
options={ [
|
||||
{ label: 'All', value: 'all' },
|
||||
{ label: 'Last 365 days', value: '365_days' },
|
||||
{ label: 'Last 90 days', value: '90_days' },
|
||||
{ label: 'Last 30 days', value: '30_days' },
|
||||
{ label: 'Last 7 days', value: '7_days' },
|
||||
{ label: 'Last 24 hours', value: '24_hours' },
|
||||
{ label: 'Custom', value: 'custom' },
|
||||
] }
|
||||
/>
|
||||
</div>
|
||||
{ value.label === 'custom' && getDatePicker() }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default HistoricalDataPeriodSelector;
|
|
@ -0,0 +1,29 @@
|
|||
/** @format */
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __, sprintf } from '@wordpress/i18n';
|
||||
|
||||
function HistoricalDataProgress( { label, progress, total } ) {
|
||||
return (
|
||||
<div className="woocommerce-settings-historical-data__progress">
|
||||
<span className="woocommerce-settings-historical-data__progress-label">
|
||||
{ sprintf( __( 'Imported %(label)s', 'woocommerce-admin' ), {
|
||||
label,
|
||||
} ) +
|
||||
' ' +
|
||||
sprintf( __( '%(progress)s of %(total)s', 'woocommerce-admin' ), {
|
||||
progress,
|
||||
total,
|
||||
} ) }
|
||||
</span>
|
||||
<progress
|
||||
className="woocommerce-settings-historical-data__progress-bar"
|
||||
max={ total }
|
||||
value={ progress }
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default HistoricalDataProgress;
|
|
@ -0,0 +1,20 @@
|
|||
/** @format */
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { CheckboxControl } from '@wordpress/components';
|
||||
|
||||
function HistoricalDataSkipCheckbox( { checked, disabled, onChange } ) {
|
||||
return (
|
||||
<CheckboxControl
|
||||
className="woocommerce-settings-historical-data__skip-checkbox"
|
||||
checked={ checked }
|
||||
disabled={ disabled }
|
||||
label={ __( 'Skip previously imported customers and orders', 'woocommerce-admin' ) }
|
||||
onChange={ onChange }
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default HistoricalDataSkipCheckbox;
|
|
@ -0,0 +1,37 @@
|
|||
/** @format */
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __, sprintf } from '@wordpress/i18n';
|
||||
import { applyFilters } from '@wordpress/hooks';
|
||||
import moment from 'moment';
|
||||
import { Spinner } from '@wordpress/components';
|
||||
|
||||
/**
|
||||
* WooCommerce dependencies
|
||||
*/
|
||||
import { useFilters } from '@woocommerce/components';
|
||||
|
||||
const HISTORICAL_DATA_STATUS_FILTER = 'woocommerce_admin_import_status';
|
||||
|
||||
function HistoricalDataStatus( { importDate, status } ) {
|
||||
const statusLabels = applyFilters( HISTORICAL_DATA_STATUS_FILTER, {
|
||||
ready: __( 'Ready To Import', 'woocommerce-admin' ),
|
||||
customers: [ __( 'Importing Customers', 'woocommerce-admin' ), <Spinner key="spinner" /> ],
|
||||
orders: [ __( 'Importing Orders', 'woocommerce-admin' ), <Spinner key="spinner" /> ],
|
||||
finalizing: [ __( 'Finalizing', 'woocommerce-admin' ), <Spinner key="spinner" /> ],
|
||||
finished: sprintf(
|
||||
__( 'Historical data from %s onward imported', 'woocommerce-admin' ),
|
||||
moment( importDate ).format( 'll' )
|
||||
),
|
||||
} );
|
||||
|
||||
return (
|
||||
<span className="woocommerce-settings-historical-data__status">
|
||||
{ __( 'Status:', 'woocommerce-admin' ) + ' ' }
|
||||
{ statusLabels[ status ] }
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export default useFilters( HISTORICAL_DATA_STATUS_FILTER )( HistoricalDataStatus );
|
|
@ -0,0 +1,111 @@
|
|||
/** @format */
|
||||
|
||||
.woocommerce-settings-historical-data__columns {
|
||||
display: grid;
|
||||
grid-column-gap: $gap-large;
|
||||
grid-template-columns: calc(50% - #{$gap-large/2}) calc(50% - #{$gap-large/2});
|
||||
margin-top: $gap-small;
|
||||
|
||||
.woocommerce-settings-historical-data__column {
|
||||
align-self: end;
|
||||
// Auto-position fix for IE11.
|
||||
@include set-grid-item-position( 2, 2 );
|
||||
}
|
||||
|
||||
@include breakpoint( '<960px' ) {
|
||||
grid-template-columns: 100%;
|
||||
|
||||
.woocommerce-settings-historical-data__column {
|
||||
@include set-grid-item-position( 1, 2 );
|
||||
}
|
||||
}
|
||||
|
||||
.components-base-control__label,
|
||||
.woocommerce-settings-historical-data__column-label {
|
||||
margin-bottom: $gap-small;
|
||||
}
|
||||
|
||||
.components-select-control__input {
|
||||
height: 38px;
|
||||
padding: 8px 2px;
|
||||
}
|
||||
|
||||
.components-base-control__field {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.woocommerce-settings-historical-data__skip-checkbox {
|
||||
margin-top: $gap-large;
|
||||
|
||||
> .components-base-control__field {
|
||||
margin-bottom: 0;
|
||||
|
||||
> .components-checkbox-control__label {
|
||||
display: inline-block;
|
||||
margin-bottom: 0;
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.woocommerce-settings-historical-data__progress-label {
|
||||
display: block;
|
||||
font-weight: bold;
|
||||
margin-bottom: $gap-small;
|
||||
margin-top: $gap-large;
|
||||
}
|
||||
|
||||
.woocommerce-settings-historical-data__progress-bar {
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
border: 0;
|
||||
height: 8px;
|
||||
width: 100%;
|
||||
|
||||
// Firefox
|
||||
& {
|
||||
background-color: #c4c4c4;
|
||||
}
|
||||
|
||||
&::-moz-progress-bar {
|
||||
background-color: #0085ba;
|
||||
}
|
||||
|
||||
// Chrome
|
||||
&::-webkit-progress-bar {
|
||||
background-color: #c4c4c4;
|
||||
}
|
||||
|
||||
&::-webkit-progress-value {
|
||||
background-color: #0085ba;
|
||||
}
|
||||
}
|
||||
|
||||
.woocommerce-settings-historical-data__status {
|
||||
display: block;
|
||||
font-weight: bold;
|
||||
margin-top: $gap-large;
|
||||
|
||||
> .components-spinner {
|
||||
float: none;
|
||||
height: 12px;
|
||||
margin-left: 6px;
|
||||
margin-right: 6px;
|
||||
width: 12px;
|
||||
|
||||
&::before {
|
||||
left: 2px;
|
||||
height: 3px;
|
||||
top: 2px;
|
||||
transform-origin: 4px 4px;
|
||||
width: 3px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.woocommerce-settings-historical-data__actions {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
}
|
|
@ -21,6 +21,7 @@ import './index.scss';
|
|||
import { analyticsSettings } from './config';
|
||||
import Header from 'header';
|
||||
import Setting from './setting';
|
||||
import HistoricalData from './historical-data';
|
||||
import withSelect from 'wc-api/with-select';
|
||||
|
||||
const SETTINGS_FILTER = 'woocommerce_admin_analytics_settings';
|
||||
|
@ -140,6 +141,7 @@ class Settings extends Component {
|
|||
{ __( 'Save Changes', 'woocommerce-admin' ) }
|
||||
</Button>
|
||||
</div>
|
||||
<HistoricalData />
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
Dashboard
|
||||
=======
|
||||
|
||||
This folder contains the components used in the Dashboard page.
|
||||
|
||||
## Extending the Dashboard
|
||||
|
||||
New Dashboard sections can be added by hooking into the filter `woocommerce_dashboard_default_sections`. For example:
|
||||
|
||||
```js
|
||||
addFilter( 'woocommerce_dashboard_default_sections', sections => {
|
||||
return [
|
||||
...sections,
|
||||
{
|
||||
key: 'example',
|
||||
component: ExampleSection,
|
||||
title: 'My Example Dashboard Section',
|
||||
isVisible: true,
|
||||
icon: 'arrow-right-alt',
|
||||
hiddenBlocks: [],
|
||||
},
|
||||
];
|
||||
} );
|
||||
```
|
||||
|
||||
Each section is defined by an object containing the following properties.
|
||||
|
||||
- `key` (string): The key used internally to identify the section.
|
||||
- `title` (string): The title shown in the Dashboard. It can be modified by users.
|
||||
- `icon` (string|function|WPComponent|null): Icon to be used to identify the section.
|
||||
- `component` (react component): The component containing the section content.
|
||||
- `isVisible` (boolean): Whether the section is visible by default. Sections can be added/hidden by users.
|
||||
- `hiddenBlocks` (array of strings): The keys of the blocks that must be hidden by default. Used in Sections that contain several blocks that can be shown or hidden. It can be modified by users.
|
||||
|
||||
The component will get the following props:
|
||||
|
||||
- `hiddenBlocks` (array of strings): Hidden blocks according to the default settings or the user preferences if they had made any modification.
|
||||
- `isFirst` (boolean): Whether the component is the first one shown in the Dashboard.
|
||||
- `isLast` (boolean): Whether the component is the last one shown in the Dashboard.
|
||||
- `onMove` (boolean): Event to trigger when moving the section.
|
||||
- `onRemove` (boolean): Event to trigger when removing the section.
|
||||
- `onTitleBlur` (function): Event to trigger when the edit title input box is unfocused.
|
||||
- `onTitleChange` (function): Event to trigger when the edit title input box receives a change event.
|
||||
- `onToggleHiddenBlock` (function): Event to trigger when the user toggles one of the hidden blocks preferences.
|
||||
- `titleInput` (string): Current string to be displayed in the edit title input box. Title is only updated on blur, so this value will be different than `title` when the user is modifying the input box.
|
||||
- `path` (string): The exact path for this view.
|
||||
- `query` (object): The query string for the current view, can be used to read current preferences for time periods or chart interval/type.
|
||||
- `title` (string): Title of the section according to the default settings or the user preferences if they had made any modification.
|
|
@ -0,0 +1,66 @@
|
|||
/** @format */
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { Icon } from '@wordpress/components';
|
||||
import { Component } from '@wordpress/element';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { MenuItem } from '@woocommerce/components';
|
||||
import './style.scss';
|
||||
|
||||
class SectionControls extends Component {
|
||||
constructor( props ) {
|
||||
super( props );
|
||||
this.onMoveUp = this.onMoveUp.bind( this );
|
||||
this.onMoveDown = this.onMoveDown.bind( this );
|
||||
}
|
||||
|
||||
onMoveUp() {
|
||||
const { onMove, onToggle } = this.props;
|
||||
onMove( -1 );
|
||||
// Close the dropdown
|
||||
onToggle();
|
||||
}
|
||||
|
||||
onMoveDown() {
|
||||
const { onMove, onToggle } = this.props;
|
||||
onMove( 1 );
|
||||
// Close the dropdown
|
||||
onToggle();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { onRemove, isFirst, isLast } = this.props;
|
||||
|
||||
if ( ! window.wcAdminFeatures[ 'dashboard/customizable' ] ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="woocommerce-section-controls">
|
||||
{ ! isFirst && (
|
||||
<MenuItem isClickable onInvoke={ this.onMoveUp }>
|
||||
<Icon icon={ 'arrow-up-alt2' } label={ __( 'Move up' ) } />
|
||||
{ __( 'Move up', 'woocommerce-admin' ) }
|
||||
</MenuItem>
|
||||
) }
|
||||
{ ! isLast && (
|
||||
<MenuItem isClickable onInvoke={ this.onMoveDown }>
|
||||
<Icon icon={ 'arrow-down-alt2' } label={ __( 'Move Down' ) } />
|
||||
{ __( 'Move Down', 'woocommerce-admin' ) }
|
||||
</MenuItem>
|
||||
) }
|
||||
<MenuItem isClickable onInvoke={ onRemove }>
|
||||
<Icon icon={ 'trash' } label={ __( 'Remove block' ) } />
|
||||
{ __( 'Remove section', 'woocommerce-admin' ) }
|
||||
</MenuItem>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default SectionControls;
|
|
@ -0,0 +1,14 @@
|
|||
/** @format */
|
||||
|
||||
.woocommerce-section-controls {
|
||||
border-top: $border-width solid $core-grey-light-500;
|
||||
|
||||
.dashicon {
|
||||
margin: 0 $gap-smaller 0 0;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
.woocommerce-ellipsis-menu__item {
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
/** @format */
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { Component } from '@wordpress/element';
|
||||
import { xor } from 'lodash';
|
||||
|
||||
export default class Section extends Component {
|
||||
constructor( props ) {
|
||||
super( props );
|
||||
const { title } = props;
|
||||
|
||||
this.state = {
|
||||
titleInput: title,
|
||||
};
|
||||
|
||||
this.onToggleHiddenBlock = this.onToggleHiddenBlock.bind( this );
|
||||
this.onTitleChange = this.onTitleChange.bind( this );
|
||||
this.onTitleBlur = this.onTitleBlur.bind( this );
|
||||
}
|
||||
|
||||
onTitleChange( updatedTitle ) {
|
||||
this.setState( { titleInput: updatedTitle } );
|
||||
}
|
||||
|
||||
onTitleBlur() {
|
||||
const { onTitleUpdate, title } = this.props;
|
||||
const { titleInput } = this.state;
|
||||
|
||||
if ( titleInput === '' ) {
|
||||
this.setState( { titleInput: title } );
|
||||
} else if ( onTitleUpdate ) {
|
||||
onTitleUpdate( titleInput );
|
||||
}
|
||||
}
|
||||
|
||||
onToggleHiddenBlock( key ) {
|
||||
return () => {
|
||||
const hiddenBlocks = xor( this.props.hiddenBlocks, [ key ] );
|
||||
this.props.onChangeHiddenBlocks( hiddenBlocks );
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
component: SectionComponent,
|
||||
onChangeHiddenBlocks,
|
||||
onTitleUpdate,
|
||||
...props
|
||||
} = this.props;
|
||||
const { titleInput } = this.state;
|
||||
|
||||
return (
|
||||
<div className="woocommerce-dashboard-section">
|
||||
<SectionComponent
|
||||
onTitleChange={ this.onTitleChange }
|
||||
onTitleBlur={ this.onTitleBlur }
|
||||
onToggleHiddenBlock={ this.onToggleHiddenBlock }
|
||||
titleInput={ titleInput }
|
||||
{ ...props }
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -2,30 +2,211 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { __, sprintf } from '@wordpress/i18n';
|
||||
import { Component, Fragment } from '@wordpress/element';
|
||||
import { compose } from '@wordpress/compose';
|
||||
import { partial } from 'lodash';
|
||||
import { IconButton, Icon, Dropdown, Button } from '@wordpress/components';
|
||||
import { withDispatch } from '@wordpress/data';
|
||||
|
||||
/**
|
||||
* WooCommerce dependencies
|
||||
*/
|
||||
import { H, ReportFilters } from '@woocommerce/components';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import './style.scss';
|
||||
import DashboardCharts from './dashboard-charts';
|
||||
import Leaderboards from './leaderboards';
|
||||
import { ReportFilters, H } from '@woocommerce/components';
|
||||
import StorePerformance from './store-performance';
|
||||
import defaultSections from './default-sections';
|
||||
import Section from './components/section';
|
||||
import withSelect from 'wc-api/with-select';
|
||||
|
||||
class CustomizableDashboard extends Component {
|
||||
constructor( props ) {
|
||||
super( props );
|
||||
|
||||
this.state = {
|
||||
sections: this.mergeSectionsWithDefaults( props.userPrefSections ),
|
||||
};
|
||||
|
||||
this.onMove = this.onMove.bind( this );
|
||||
this.updateSection = this.updateSection.bind( this );
|
||||
}
|
||||
|
||||
mergeSectionsWithDefaults( prefSections ) {
|
||||
if ( ! prefSections || prefSections.length === 0 ) {
|
||||
return defaultSections;
|
||||
}
|
||||
const defaultKeys = defaultSections.map( section => section.key );
|
||||
const prefKeys = prefSections.map( section => section.key );
|
||||
const keys = new Set( [ ...prefKeys, ...defaultKeys ] );
|
||||
const sections = [];
|
||||
|
||||
keys.forEach( key => {
|
||||
const defaultSection = defaultSections.find( section => section.key === key );
|
||||
if ( ! defaultSection ) {
|
||||
return;
|
||||
}
|
||||
const prefSection = prefSections.find( section => section.key === key );
|
||||
|
||||
sections.push( {
|
||||
...defaultSection,
|
||||
...prefSection,
|
||||
} );
|
||||
} );
|
||||
|
||||
return sections;
|
||||
}
|
||||
|
||||
updateSections( newSections ) {
|
||||
this.setState( { sections: newSections } );
|
||||
this.props.updateCurrentUserData( { dashboard_sections: newSections } );
|
||||
}
|
||||
|
||||
updateSection( updatedKey, newSettings ) {
|
||||
const newSections = this.state.sections.map( section => {
|
||||
if ( section.key === updatedKey ) {
|
||||
return {
|
||||
...section,
|
||||
...newSettings,
|
||||
};
|
||||
}
|
||||
return section;
|
||||
} );
|
||||
this.updateSections( newSections );
|
||||
}
|
||||
|
||||
onChangeHiddenBlocks( updatedKey ) {
|
||||
return updatedHiddenBlocks => {
|
||||
this.updateSection( updatedKey, { hiddenBlocks: updatedHiddenBlocks } );
|
||||
};
|
||||
}
|
||||
|
||||
onSectionTitleUpdate( updatedKey ) {
|
||||
return updatedTitle => {
|
||||
this.updateSection( updatedKey, { title: updatedTitle } );
|
||||
};
|
||||
}
|
||||
|
||||
toggleVisibility( key, onToggle ) {
|
||||
return () => {
|
||||
if ( onToggle ) {
|
||||
// Close the dropdown before setting state so an action is not performed on an unmounted component.
|
||||
onToggle();
|
||||
}
|
||||
// When toggling visibility, place section at the end of the array.
|
||||
const sections = [ ...this.state.sections ];
|
||||
const index = sections.findIndex( s => key === s.key );
|
||||
const toggledSection = sections.splice( index, 1 ).shift();
|
||||
toggledSection.isVisible = ! toggledSection.isVisible;
|
||||
sections.push( toggledSection );
|
||||
|
||||
this.updateSections( sections );
|
||||
};
|
||||
}
|
||||
|
||||
onMove( index, change ) {
|
||||
const sections = [ ...this.state.sections ];
|
||||
const movedSection = sections.splice( index, 1 ).shift();
|
||||
sections.splice( index + change, 0, movedSection );
|
||||
|
||||
this.updateSections( sections );
|
||||
}
|
||||
|
||||
renderAddMore() {
|
||||
const { sections } = this.state;
|
||||
const hiddenSections = sections.filter( section => false === section.isVisible );
|
||||
|
||||
if ( 0 === hiddenSections.length ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
position="top center"
|
||||
className="woocommerce-dashboard-section__add-more"
|
||||
renderToggle={ ( { onToggle, isOpen } ) => (
|
||||
<IconButton
|
||||
onClick={ onToggle }
|
||||
icon="plus-alt"
|
||||
title={ __( 'Add more sections', 'woocommerce-admin' ) }
|
||||
aria-expanded={ isOpen }
|
||||
/>
|
||||
) }
|
||||
renderContent={ ( { onToggle } ) => (
|
||||
<Fragment>
|
||||
<H>{ __( 'Dashboard Sections', 'woocommerce-admin' ) }</H>
|
||||
<div className="woocommerce-dashboard-section__add-more-choices">
|
||||
{ hiddenSections.map( section => {
|
||||
return (
|
||||
<Button
|
||||
key={ section.key }
|
||||
onClick={ this.toggleVisibility( section.key, onToggle ) }
|
||||
className="woocommerce-dashboard-section__add-more-btn"
|
||||
title={ sprintf( __( 'Add %s section', 'woocommerce-admin' ), section.title ) }
|
||||
>
|
||||
<Icon icon={ section.icon } size={ 30 } />
|
||||
<span className="woocommerce-dashboard-section__add-more-btn-title">
|
||||
{ section.title }
|
||||
</span>
|
||||
</Button>
|
||||
);
|
||||
} ) }
|
||||
</div>
|
||||
</Fragment>
|
||||
) }
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// @todo Replace dashboard-charts, leaderboards, and store-performance sections as neccessary with customizable equivalents.
|
||||
export default class CustomizableDashboard extends Component {
|
||||
render() {
|
||||
const { query, path } = this.props;
|
||||
const { sections } = this.state;
|
||||
const visibleSections = sections.filter( section => section.isVisible );
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<H>{ __( 'Customizable Dashboard', 'woocommerce-admin' ) }</H>
|
||||
<ReportFilters query={ query } path={ path } />
|
||||
<StorePerformance query={ query } />
|
||||
<DashboardCharts query={ query } path={ path } />
|
||||
<Leaderboards query={ query } />
|
||||
{ visibleSections.map( ( section, index ) => {
|
||||
return (
|
||||
<Section
|
||||
component={ section.component }
|
||||
hiddenBlocks={ section.hiddenBlocks }
|
||||
key={ section.key }
|
||||
onChangeHiddenBlocks={ this.onChangeHiddenBlocks( section.key ) }
|
||||
onTitleUpdate={ this.onSectionTitleUpdate( section.key ) }
|
||||
path={ path }
|
||||
query={ query }
|
||||
title={ section.title }
|
||||
onMove={ partial( this.onMove, index ) }
|
||||
onRemove={ this.toggleVisibility( section.key ) }
|
||||
isFirst={ 0 === index }
|
||||
isLast={ visibleSections.length === index + 1 }
|
||||
/>
|
||||
);
|
||||
} ) }
|
||||
{ this.renderAddMore() }
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default compose(
|
||||
withSelect( select => {
|
||||
const { getCurrentUserData } = select( 'wc-api' );
|
||||
const userData = getCurrentUserData();
|
||||
|
||||
return {
|
||||
userPrefSections: userData.dashboard_sections,
|
||||
};
|
||||
} ),
|
||||
withDispatch( dispatch => {
|
||||
const { updateCurrentUserData } = dispatch( 'wc-api' );
|
||||
|
||||
return {
|
||||
updateCurrentUserData,
|
||||
};
|
||||
} )
|
||||
)( CustomizableDashboard );
|
||||
|
|
|
@ -7,15 +7,14 @@ import classNames from 'classnames';
|
|||
import { Component, Fragment } from '@wordpress/element';
|
||||
import { compose } from '@wordpress/compose';
|
||||
import Gridicon from 'gridicons';
|
||||
import { xor } from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import { IconButton, NavigableMenu, SelectControl } from '@wordpress/components';
|
||||
import { IconButton, NavigableMenu, SelectControl, TextControl } from '@wordpress/components';
|
||||
import { withDispatch } from '@wordpress/data';
|
||||
|
||||
/**
|
||||
* WooCommerce dependencies
|
||||
*/
|
||||
import { EllipsisMenu, MenuItem, SectionHeader } from '@woocommerce/components';
|
||||
import { EllipsisMenu, MenuItem, MenuTitle, SectionHeader } from '@woocommerce/components';
|
||||
import { getAllowedIntervalsForQuery } from '@woocommerce/date';
|
||||
|
||||
/**
|
||||
|
@ -25,40 +24,15 @@ import ChartBlock from './block';
|
|||
import { getChartFromKey, uniqCharts } from './config';
|
||||
import withSelect from 'wc-api/with-select';
|
||||
import './style.scss';
|
||||
import SectionControls from 'dashboard/components/section-controls';
|
||||
|
||||
class DashboardCharts extends Component {
|
||||
constructor( props ) {
|
||||
super( ...arguments );
|
||||
|
||||
this.state = {
|
||||
chartType: props.userPrefChartType || 'line',
|
||||
hiddenChartKeys: props.userPrefCharts || [
|
||||
'avg_order_value',
|
||||
'avg_items_per_order',
|
||||
'items_sold',
|
||||
'gross_revenue',
|
||||
'refunds',
|
||||
'coupons',
|
||||
'taxes',
|
||||
'shipping',
|
||||
'amount',
|
||||
'total_tax',
|
||||
'order_tax',
|
||||
'shipping_tax',
|
||||
],
|
||||
interval: props.userPrefIntervals || 'day',
|
||||
};
|
||||
|
||||
this.toggle = this.toggle.bind( this );
|
||||
}
|
||||
|
||||
toggle( key ) {
|
||||
return () => {
|
||||
const hiddenChartKeys = xor( this.state.hiddenChartKeys, [ key ] );
|
||||
this.setState( { hiddenChartKeys } );
|
||||
const userDataFields = {
|
||||
[ 'dashboard_charts' ]: hiddenChartKeys,
|
||||
};
|
||||
this.props.updateCurrentUserData( userDataFields );
|
||||
interval: props.userPrefChartInterval || 'day',
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -73,24 +47,58 @@ class DashboardCharts extends Component {
|
|||
}
|
||||
|
||||
renderMenu() {
|
||||
const { hiddenChartKeys } = this.state;
|
||||
const {
|
||||
hiddenBlocks,
|
||||
isFirst,
|
||||
isLast,
|
||||
onMove,
|
||||
onRemove,
|
||||
onTitleBlur,
|
||||
onTitleChange,
|
||||
onToggleHiddenBlock,
|
||||
titleInput,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<EllipsisMenu label={ __( 'Choose which charts to display', 'woocommerce-admin' ) }>
|
||||
{ uniqCharts.map( chart => {
|
||||
return (
|
||||
<MenuItem
|
||||
checked={ ! hiddenChartKeys.includes( chart.key ) }
|
||||
isCheckbox
|
||||
isClickable
|
||||
key={ chart.key }
|
||||
onInvoke={ this.toggle( chart.key ) }
|
||||
>
|
||||
{ __( `${ chart.label }`, 'woocommerce-admin' ) }
|
||||
</MenuItem>
|
||||
);
|
||||
} ) }
|
||||
</EllipsisMenu>
|
||||
<EllipsisMenu
|
||||
label={ __( 'Choose which charts to display', 'woocommerce-admin' ) }
|
||||
renderContent={ ( { onToggle } ) => (
|
||||
<Fragment>
|
||||
{ window.wcAdminFeatures[ 'dashboard/customizable' ] && (
|
||||
<div className="woocommerce-ellipsis-menu__item">
|
||||
<TextControl
|
||||
label={ __( 'Section Title', 'woocommerce-admin' ) }
|
||||
onBlur={ onTitleBlur }
|
||||
onChange={ onTitleChange }
|
||||
required
|
||||
value={ titleInput }
|
||||
/>
|
||||
</div>
|
||||
) }
|
||||
<MenuTitle>{ __( 'Charts', 'woocommerce-admin' ) }</MenuTitle>
|
||||
{ uniqCharts.map( chart => {
|
||||
return (
|
||||
<MenuItem
|
||||
checked={ ! hiddenBlocks.includes( chart.key ) }
|
||||
isCheckbox
|
||||
isClickable
|
||||
key={ chart.key }
|
||||
onInvoke={ () => onToggleHiddenBlock( chart.key )() }
|
||||
>
|
||||
{ __( `${ chart.label }`, 'woocommerce-admin' ) }
|
||||
</MenuItem>
|
||||
);
|
||||
} ) }
|
||||
<SectionControls
|
||||
onToggle={ onToggle }
|
||||
onMove={ onMove }
|
||||
onRemove={ onRemove }
|
||||
isFirst={ isFirst }
|
||||
isLast={ isLast }
|
||||
/>
|
||||
</Fragment>
|
||||
) }
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -132,14 +140,14 @@ class DashboardCharts extends Component {
|
|||
};
|
||||
|
||||
render() {
|
||||
const { path } = this.props;
|
||||
const { chartType, hiddenChartKeys, interval } = this.state;
|
||||
const { hiddenBlocks, path, title } = this.props;
|
||||
const { chartType, interval } = this.state;
|
||||
const query = { ...this.props.query, chartType, interval };
|
||||
return (
|
||||
<Fragment>
|
||||
<div className="woocommerce-dashboard__dashboard-charts">
|
||||
<SectionHeader
|
||||
title={ __( 'Charts', 'woocommerce-admin' ) }
|
||||
title={ title || __( 'Charts', 'woocommerce-admin' ) }
|
||||
menu={ this.renderMenu() }
|
||||
className={ 'has-interval-select' }
|
||||
>
|
||||
|
@ -176,7 +184,7 @@ class DashboardCharts extends Component {
|
|||
</SectionHeader>
|
||||
<div className="woocommerce-dashboard__columns">
|
||||
{ uniqCharts.map( chart => {
|
||||
return hiddenChartKeys.includes( chart.key ) ? null : (
|
||||
return hiddenBlocks.includes( chart.key ) ? null : (
|
||||
<ChartBlock
|
||||
charts={ getChartFromKey( chart.key ) }
|
||||
endpoint={ chart.endpoint }
|
||||
|
@ -204,7 +212,6 @@ export default compose(
|
|||
const userData = getCurrentUserData();
|
||||
|
||||
return {
|
||||
userPrefCharts: userData.dashboard_charts,
|
||||
userPrefChartType: userData.dashboard_chart_type,
|
||||
userPrefChartInterval: userData.dashboard_chart_interval,
|
||||
};
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
/** @format */
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { applyFilters } from '@wordpress/hooks';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import DashboardCharts from './dashboard-charts';
|
||||
import Leaderboards from './leaderboards';
|
||||
import StorePerformance from './store-performance';
|
||||
|
||||
const DEFAULT_SECTIONS_FILTER = 'woocommerce_dashboard_default_sections';
|
||||
|
||||
export default applyFilters( DEFAULT_SECTIONS_FILTER, [
|
||||
{
|
||||
key: 'store-performance',
|
||||
component: StorePerformance,
|
||||
title: __( 'Performance', 'woocommerce-admin' ),
|
||||
isVisible: true,
|
||||
icon: 'arrow-right-alt',
|
||||
hiddenBlocks: [
|
||||
'coupons/amount',
|
||||
'coupons/orders_count',
|
||||
'downloads/download_count',
|
||||
'taxes/order_tax',
|
||||
'taxes/total_tax',
|
||||
'taxes/shipping_tax',
|
||||
'revenue/shipping',
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'charts',
|
||||
component: DashboardCharts,
|
||||
title: __( 'Charts', 'woocommerce-admin' ),
|
||||
isVisible: true,
|
||||
icon: 'chart-bar',
|
||||
hiddenBlocks: [
|
||||
'avg_order_value',
|
||||
'avg_items_per_order',
|
||||
'items_sold',
|
||||
'gross_revenue',
|
||||
'refunds',
|
||||
'coupons',
|
||||
'taxes',
|
||||
'shipping',
|
||||
'amount',
|
||||
'total_tax',
|
||||
'order_tax',
|
||||
'shipping_tax',
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'leaderboards',
|
||||
component: Leaderboards,
|
||||
title: __( 'Leaderboards', 'woocommerce-admin' ),
|
||||
isVisible: true,
|
||||
icon: 'editor-ol',
|
||||
hiddenBlocks: [ 'coupons', 'customers' ],
|
||||
},
|
||||
] );
|
|
@ -5,9 +5,8 @@
|
|||
import { __ } from '@wordpress/i18n';
|
||||
import { Component, Fragment } from '@wordpress/element';
|
||||
import { compose } from '@wordpress/compose';
|
||||
import { xor } from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import { SelectControl } from '@wordpress/components';
|
||||
import { SelectControl, TextControl } from '@wordpress/components';
|
||||
import { withDispatch } from '@wordpress/data';
|
||||
|
||||
/**
|
||||
|
@ -20,28 +19,16 @@ import { EllipsisMenu, MenuItem, MenuTitle, SectionHeader } from '@woocommerce/c
|
|||
*/
|
||||
import Leaderboard from 'analytics/components/leaderboard';
|
||||
import withSelect from 'wc-api/with-select';
|
||||
import SectionControls from 'dashboard/components/section-controls';
|
||||
import './style.scss';
|
||||
|
||||
class Leaderboards extends Component {
|
||||
constructor( props ) {
|
||||
super( ...arguments );
|
||||
|
||||
this.state = {
|
||||
hiddenLeaderboardKeys: props.userPrefLeaderboards || [ 'coupons', 'customers' ],
|
||||
rowsPerTable: parseInt( props.userPrefLeaderboardRows ) || 5,
|
||||
};
|
||||
|
||||
this.toggle = this.toggle.bind( this );
|
||||
}
|
||||
|
||||
toggle( key ) {
|
||||
return () => {
|
||||
const hiddenLeaderboardKeys = xor( this.state.hiddenLeaderboardKeys, [ key ] );
|
||||
this.setState( { hiddenLeaderboardKeys } );
|
||||
const userDataFields = {
|
||||
[ 'dashboard_leaderboards' ]: hiddenLeaderboardKeys,
|
||||
};
|
||||
this.props.updateCurrentUserData( userDataFields );
|
||||
};
|
||||
}
|
||||
|
||||
setRowsPerTable = rows => {
|
||||
|
@ -53,52 +40,82 @@ class Leaderboards extends Component {
|
|||
};
|
||||
|
||||
renderMenu() {
|
||||
const { hiddenLeaderboardKeys, rowsPerTable } = this.state;
|
||||
const { allLeaderboards } = this.props;
|
||||
const {
|
||||
allLeaderboards,
|
||||
isFirst,
|
||||
isLast,
|
||||
hiddenBlocks,
|
||||
onMove,
|
||||
onRemove,
|
||||
onTitleBlur,
|
||||
onTitleChange,
|
||||
onToggleHiddenBlock,
|
||||
titleInput,
|
||||
} = this.props;
|
||||
const { rowsPerTable } = this.state;
|
||||
|
||||
return (
|
||||
<EllipsisMenu
|
||||
label={ __(
|
||||
'Choose which leaderboards to display and the number of rows',
|
||||
'Choose which leaderboards to display and other settings',
|
||||
'woocommerce-admin'
|
||||
) }
|
||||
>
|
||||
<Fragment>
|
||||
<MenuTitle>{ __( 'Leaderboards', 'woocommerce-admin' ) }</MenuTitle>
|
||||
{ allLeaderboards.map( leaderboard => {
|
||||
return (
|
||||
<MenuItem
|
||||
checked={ ! hiddenLeaderboardKeys.includes( leaderboard.id ) }
|
||||
isCheckbox
|
||||
isClickable
|
||||
key={ leaderboard.id }
|
||||
onInvoke={ this.toggle( leaderboard.id ) }
|
||||
>
|
||||
{ leaderboard.label }
|
||||
</MenuItem>
|
||||
);
|
||||
} ) }
|
||||
<SelectControl
|
||||
className="woocommerce-dashboard__dashboard-leaderboards__select"
|
||||
label={ <MenuTitle>{ __( 'Rows Per Table', 'woocommerce-admin' ) }</MenuTitle> }
|
||||
value={ rowsPerTable }
|
||||
options={ Array.from( { length: 20 }, ( v, key ) => ( {
|
||||
v: key + 1,
|
||||
label: key + 1,
|
||||
} ) ) }
|
||||
onChange={ this.setRowsPerTable }
|
||||
/>
|
||||
</Fragment>
|
||||
</EllipsisMenu>
|
||||
renderContent={ ( { onToggle } ) => (
|
||||
<Fragment>
|
||||
{ window.wcAdminFeatures[ 'dashboard/customizable' ] && (
|
||||
<div className="woocommerce-ellipsis-menu__item">
|
||||
<TextControl
|
||||
label={ __( 'Section Title', 'woocommerce-admin' ) }
|
||||
onBlur={ onTitleBlur }
|
||||
onChange={ onTitleChange }
|
||||
required
|
||||
value={ titleInput }
|
||||
/>
|
||||
</div>
|
||||
) }
|
||||
<MenuTitle>{ __( 'Leaderboards', 'woocommerce-admin' ) }</MenuTitle>
|
||||
{ allLeaderboards.map( leaderboard => {
|
||||
return (
|
||||
<MenuItem
|
||||
checked={ ! hiddenBlocks.includes( leaderboard.id ) }
|
||||
isCheckbox
|
||||
isClickable
|
||||
key={ leaderboard.id }
|
||||
onInvoke={ () => onToggleHiddenBlock( leaderboard.id )() }
|
||||
>
|
||||
{ leaderboard.label }
|
||||
</MenuItem>
|
||||
);
|
||||
} ) }
|
||||
<SelectControl
|
||||
className="woocommerce-dashboard__dashboard-leaderboards__select"
|
||||
label={ <MenuTitle>{ __( 'Rows Per Table', 'woocommerce-admin' ) }</MenuTitle> }
|
||||
value={ rowsPerTable }
|
||||
options={ Array.from( { length: 20 }, ( v, key ) => ( {
|
||||
v: key + 1,
|
||||
label: key + 1,
|
||||
} ) ) }
|
||||
onChange={ this.setRowsPerTable }
|
||||
/>
|
||||
<SectionControls
|
||||
onToggle={ onToggle }
|
||||
onMove={ onMove }
|
||||
onRemove={ onRemove }
|
||||
isFirst={ isFirst }
|
||||
isLast={ isLast }
|
||||
/>
|
||||
</Fragment>
|
||||
) }
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
renderLeaderboards() {
|
||||
const { hiddenLeaderboardKeys, rowsPerTable } = this.state;
|
||||
const { allLeaderboards, query } = this.props;
|
||||
const { rowsPerTable } = this.state;
|
||||
const { allLeaderboards, hiddenBlocks, query } = this.props;
|
||||
|
||||
return allLeaderboards.map( leaderboard => {
|
||||
if ( hiddenLeaderboardKeys.includes( leaderboard.id ) ) {
|
||||
if ( hiddenBlocks.includes( leaderboard.id ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -116,11 +133,13 @@ class Leaderboards extends Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
const { title } = this.props;
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<div className="woocommerce-dashboard__dashboard-leaderboards">
|
||||
<SectionHeader
|
||||
title={ __( 'Leaderboards', 'woocommerce-admin' ) }
|
||||
title={ title || __( 'Leaderboards', 'woocommerce-admin' ) }
|
||||
menu={ this.renderMenu() }
|
||||
/>
|
||||
<div className="woocommerce-dashboard__columns">{ this.renderLeaderboards() }</div>
|
||||
|
@ -147,7 +166,6 @@ export default compose(
|
|||
getItems,
|
||||
getItemsError,
|
||||
isGetItemsRequesting,
|
||||
userPrefLeaderboards: userData.dashboard_leaderboards,
|
||||
userPrefLeaderboardRows: userData.dashboard_leaderboard_rows,
|
||||
};
|
||||
} ),
|
||||
|
|
|
@ -8,6 +8,7 @@ import { compose } from '@wordpress/compose';
|
|||
import { withDispatch } from '@wordpress/data';
|
||||
import moment from 'moment';
|
||||
import { find } from 'lodash';
|
||||
import { TextControl } from '@wordpress/components';
|
||||
|
||||
/**
|
||||
* WooCommerce dependencies
|
||||
|
@ -30,58 +31,68 @@ import {
|
|||
SummaryNumber,
|
||||
} from '@woocommerce/components';
|
||||
import withSelect from 'wc-api/with-select';
|
||||
import SectionControls from 'dashboard/components/section-controls';
|
||||
import './style.scss';
|
||||
|
||||
class StorePerformance extends Component {
|
||||
constructor( props ) {
|
||||
super( props );
|
||||
this.state = {
|
||||
hiddenIndicators: props.hiddenIndicators || [],
|
||||
};
|
||||
this.toggle = this.toggle.bind( this );
|
||||
}
|
||||
|
||||
toggle( statKey ) {
|
||||
return () => {
|
||||
this.setState( state => {
|
||||
const indicators = [ ...state.hiddenIndicators ];
|
||||
let newHiddenIndicators = [];
|
||||
if ( ! indicators.includes( statKey ) ) {
|
||||
indicators.push( statKey );
|
||||
newHiddenIndicators = indicators;
|
||||
} else {
|
||||
newHiddenIndicators = indicators.filter( indicator => indicator !== statKey );
|
||||
}
|
||||
this.props.updateCurrentUserData( {
|
||||
dashboard_performance_indicators: newHiddenIndicators,
|
||||
} );
|
||||
return {
|
||||
hiddenIndicators: newHiddenIndicators,
|
||||
};
|
||||
} );
|
||||
};
|
||||
}
|
||||
|
||||
renderMenu() {
|
||||
const { indicators } = this.props;
|
||||
const {
|
||||
hiddenBlocks,
|
||||
indicators,
|
||||
isFirst,
|
||||
isLast,
|
||||
onMove,
|
||||
onRemove,
|
||||
onTitleBlur,
|
||||
onTitleChange,
|
||||
onToggleHiddenBlock,
|
||||
titleInput,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<EllipsisMenu label={ __( 'Choose which analytics to display', 'woocommerce-admin' ) }>
|
||||
<MenuTitle>{ __( 'Display Stats:', 'woocommerce-admin' ) }</MenuTitle>
|
||||
{ indicators.map( ( indicator, i ) => {
|
||||
const checked = ! this.state.hiddenIndicators.includes( indicator.stat );
|
||||
return (
|
||||
<MenuItem
|
||||
checked={ checked }
|
||||
isCheckbox
|
||||
isClickable
|
||||
key={ i }
|
||||
onInvoke={ this.toggle( indicator.stat ) }
|
||||
>
|
||||
{ sprintf( __( 'Show %s', 'woocommerce-admin' ), indicator.label ) }
|
||||
</MenuItem>
|
||||
);
|
||||
} ) }
|
||||
</EllipsisMenu>
|
||||
<EllipsisMenu
|
||||
label={ __(
|
||||
'Choose which analytics to display and the section name',
|
||||
'woocommerce-admin'
|
||||
) }
|
||||
renderContent={ ( { onToggle } ) => (
|
||||
<Fragment>
|
||||
{ window.wcAdminFeatures[ 'dashboard/customizable' ] && (
|
||||
<div className="woocommerce-ellipsis-menu__item">
|
||||
<TextControl
|
||||
label={ __( 'Section Title', 'woocommerce-admin' ) }
|
||||
onBlur={ onTitleBlur }
|
||||
onChange={ onTitleChange }
|
||||
required
|
||||
value={ titleInput }
|
||||
/>
|
||||
</div>
|
||||
) }
|
||||
<MenuTitle>{ __( 'Display Stats:', 'woocommerce-admin' ) }</MenuTitle>
|
||||
{ indicators.map( ( indicator, i ) => {
|
||||
const checked = ! hiddenBlocks.includes( indicator.stat );
|
||||
return (
|
||||
<MenuItem
|
||||
checked={ checked }
|
||||
isCheckbox
|
||||
isClickable
|
||||
key={ i }
|
||||
onInvoke={ () => onToggleHiddenBlock( indicator.stat )() }
|
||||
>
|
||||
{ sprintf( __( 'Show %s', 'woocommerce-admin' ), indicator.label ) }
|
||||
</MenuItem>
|
||||
);
|
||||
} ) }
|
||||
<SectionControls
|
||||
onToggle={ onToggle }
|
||||
onMove={ onMove }
|
||||
onRemove={ onRemove }
|
||||
isFirst={ isFirst }
|
||||
isLast={ isLast }
|
||||
/>
|
||||
</Fragment>
|
||||
) }
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -157,42 +168,24 @@ class StorePerformance extends Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
const { userIndicators, title } = this.props;
|
||||
return (
|
||||
<Fragment>
|
||||
<SectionHeader
|
||||
title={ __( 'Store Performance', 'woocommerce-admin' ) }
|
||||
title={ title || __( 'Store Performance', 'woocommerce-admin' ) }
|
||||
menu={ this.renderMenu() }
|
||||
/>
|
||||
<div className="woocommerce-dashboard__store-performance">{ this.renderList() }</div>
|
||||
{ userIndicators.length > 0 && (
|
||||
<div className="woocommerce-dashboard__store-performance">{ this.renderList() }</div>
|
||||
) }
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
export default compose(
|
||||
withSelect( ( select, props ) => {
|
||||
const { query } = props;
|
||||
const {
|
||||
getCurrentUserData,
|
||||
getReportItems,
|
||||
getReportItemsError,
|
||||
isReportItemsRequesting,
|
||||
} = select( 'wc-api' );
|
||||
const userData = getCurrentUserData();
|
||||
let hiddenIndicators = userData.dashboard_performance_indicators;
|
||||
|
||||
// Set default values for user preferences if none is set.
|
||||
// These columns are HIDDEN by default.
|
||||
if ( ! hiddenIndicators ) {
|
||||
hiddenIndicators = [
|
||||
'coupons/amount',
|
||||
'coupons/orders_count',
|
||||
'downloads/download_count',
|
||||
'taxes/order_tax',
|
||||
'taxes/total_tax',
|
||||
'taxes/shipping_tax',
|
||||
'revenue/shipping',
|
||||
];
|
||||
}
|
||||
const { hiddenBlocks, query } = props;
|
||||
const { getReportItems, getReportItemsError, isReportItemsRequesting } = select( 'wc-api' );
|
||||
|
||||
const datesFromQuery = getCurrentDates( query );
|
||||
const endPrimary = datesFromQuery.primary.before;
|
||||
|
@ -200,10 +193,18 @@ export default compose(
|
|||
|
||||
const indicators = wcSettings.dataEndpoints.performanceIndicators;
|
||||
const userIndicators = indicators.filter(
|
||||
indicator => ! hiddenIndicators.includes( indicator.stat )
|
||||
indicator => ! hiddenBlocks.includes( indicator.stat )
|
||||
);
|
||||
const statKeys = userIndicators.map( indicator => indicator.stat ).join( ',' );
|
||||
|
||||
if ( statKeys.length === 0 ) {
|
||||
return {
|
||||
hiddenIndicators,
|
||||
userIndicators,
|
||||
indicators,
|
||||
};
|
||||
}
|
||||
|
||||
const primaryQuery = {
|
||||
after: appendTimestamp( datesFromQuery.primary.after, 'start' ),
|
||||
before: appendTimestamp( endPrimary, endPrimary.isSame( moment(), 'day' ) ? 'now' : 'end' ),
|
||||
|
@ -228,7 +229,7 @@ export default compose(
|
|||
const secondaryRequesting = isReportItemsRequesting( 'performance-indicators', secondaryQuery );
|
||||
|
||||
return {
|
||||
hiddenIndicators,
|
||||
hiddenBlocks,
|
||||
userIndicators,
|
||||
indicators,
|
||||
primaryData,
|
||||
|
|
|
@ -28,3 +28,35 @@
|
|||
.woocommerce-dashboard__widget-item {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.woocommerce-dashboard-section__add-more {
|
||||
margin: 0 auto;
|
||||
width: 84px;
|
||||
padding: 0 $gap-large $gap-large;
|
||||
|
||||
.components-popover__content {
|
||||
padding: 0 $gap $gap-smaller;
|
||||
}
|
||||
}
|
||||
|
||||
.woocommerce-dashboard-section__add-more-choices {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.woocommerce-dashboard-section__add-more-btn {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: $gap;
|
||||
margin: $gap-smaller;
|
||||
|
||||
.dashicons-arrow-right-alt {
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
}
|
||||
|
||||
.woocommerce-dashboard-section__add-more-btn-title {
|
||||
color: $core-grey-dark-300;
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
|
|
@ -101,6 +101,14 @@
|
|||
|
||||
.woocommerce-activity-card__body {
|
||||
grid-area: body;
|
||||
|
||||
& > p:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
& > p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.woocommerce-activity-card__actions {
|
||||
|
@ -332,3 +340,7 @@
|
|||
@include font-size( 12 );
|
||||
}
|
||||
}
|
||||
|
||||
.woocommerce-empty-review-activity-card {
|
||||
grid-template-columns: 72px 1fr;
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@ import { H, Section } from '@woocommerce/components';
|
|||
import {
|
||||
getUnreadNotes,
|
||||
getUnreadOrders,
|
||||
getUnreadReviews,
|
||||
getUnapprovedReviews,
|
||||
getUnreadStock,
|
||||
} from './unread-indicators';
|
||||
import InboxPanel from './panels/inbox';
|
||||
|
@ -98,7 +98,7 @@ class ActivityPanel extends Component {
|
|||
|
||||
// @todo Pull in dynamic unread status/count
|
||||
getTabs() {
|
||||
const { hasUnreadNotes, hasUnreadOrders, hasUnreadReviews, hasUnreadStock } = this.props;
|
||||
const { hasUnreadNotes, hasUnreadOrders, hasUnapprovedReviews, hasUnreadStock } = this.props;
|
||||
return [
|
||||
{
|
||||
name: 'inbox',
|
||||
|
@ -125,7 +125,7 @@ class ActivityPanel extends Component {
|
|||
name: 'reviews',
|
||||
title: __( 'Reviews', 'woocommerce-admin' ),
|
||||
icon: <Gridicon icon="star" />,
|
||||
unread: hasUnreadReviews,
|
||||
unread: hasUnapprovedReviews,
|
||||
}
|
||||
: null,
|
||||
].filter( Boolean );
|
||||
|
@ -141,8 +141,8 @@ class ActivityPanel extends Component {
|
|||
case 'stock':
|
||||
return <StockPanel />;
|
||||
case 'reviews':
|
||||
const { numberOfReviews } = this.props;
|
||||
return <ReviewsPanel numberOfReviews={ numberOfReviews } />;
|
||||
const { hasUnapprovedReviews } = this.props;
|
||||
return <ReviewsPanel hasUnapprovedReviews={ hasUnapprovedReviews } />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
@ -272,7 +272,7 @@ export default withSelect( select => {
|
|||
const hasUnreadNotes = getUnreadNotes( select );
|
||||
const hasUnreadOrders = getUnreadOrders( select );
|
||||
const hasUnreadStock = getUnreadStock( select );
|
||||
const { numberOfReviews, hasUnreadReviews } = getUnreadReviews( select );
|
||||
const hasUnapprovedReviews = getUnapprovedReviews( select );
|
||||
|
||||
return { hasUnreadNotes, hasUnreadOrders, hasUnreadReviews, hasUnreadStock, numberOfReviews };
|
||||
return { hasUnreadNotes, hasUnreadOrders, hasUnreadStock, hasUnapprovedReviews };
|
||||
} )( clickOutside( ActivityPanel ) );
|
||||
|
|
|
@ -235,10 +235,15 @@ class OrdersPanel extends Component {
|
|||
}
|
||||
|
||||
const menu = (
|
||||
<EllipsisMenu label="Demo Menu">
|
||||
<MenuTitle>Test</MenuTitle>
|
||||
<MenuItem onInvoke={ noop }>Test</MenuItem>
|
||||
</EllipsisMenu>
|
||||
<EllipsisMenu
|
||||
label="Demo Menu"
|
||||
renderContent={ () => (
|
||||
<Fragment>
|
||||
<MenuTitle>Test</MenuTitle>
|
||||
<MenuItem onInvoke={ noop }>Test</MenuItem>
|
||||
</Fragment>
|
||||
) }
|
||||
/>
|
||||
);
|
||||
|
||||
const title =
|
||||
|
|
|
@ -5,12 +5,11 @@
|
|||
import { __, sprintf } from '@wordpress/i18n';
|
||||
import classnames from 'classnames';
|
||||
import { Component, Fragment } from '@wordpress/element';
|
||||
import { compose } from '@wordpress/compose';
|
||||
import { Button } from '@wordpress/components';
|
||||
import Gridicon from 'gridicons';
|
||||
import interpolateComponents from 'interpolate-components';
|
||||
import { get, noop, isNull } from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withDispatch } from '@wordpress/data';
|
||||
|
||||
/**
|
||||
* WooCommerce dependencies
|
||||
|
@ -24,13 +23,14 @@ import {
|
|||
Section,
|
||||
SplitButton,
|
||||
} from '@woocommerce/components';
|
||||
import { getAdminLink } from '@woocommerce/navigation';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { ActivityCard, ActivityCardPlaceholder } from '../activity-card';
|
||||
import ActivityHeader from '../activity-header';
|
||||
import { DEFAULT_REVIEW_STATUSES, QUERY_DEFAULTS } from 'wc-api/constants';
|
||||
import { QUERY_DEFAULTS } from 'wc-api/constants';
|
||||
import sanitizeHTML from 'lib/sanitize-html';
|
||||
import withSelect from 'wc-api/with-select';
|
||||
|
||||
|
@ -41,13 +41,6 @@ class ReviewsPanel extends Component {
|
|||
this.mountTime = new Date().getTime();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
const userDataFields = {
|
||||
[ 'activity_panel_reviews_last_read' ]: this.mountTime,
|
||||
};
|
||||
this.props.updateCurrentUserData( userDataFields );
|
||||
}
|
||||
|
||||
renderReview( review, props ) {
|
||||
const { lastRead } = props;
|
||||
const product =
|
||||
|
@ -149,20 +142,85 @@ class ReviewsPanel extends Component {
|
|||
);
|
||||
}
|
||||
|
||||
renderPlaceholders() {
|
||||
const { numberOfReviews } = this.props;
|
||||
const placeholders = new Array( numberOfReviews );
|
||||
return placeholders
|
||||
.fill( 0 )
|
||||
.map( ( p, i ) => (
|
||||
<ActivityCardPlaceholder
|
||||
className="woocommerce-review-activity-card"
|
||||
key={ i }
|
||||
hasAction
|
||||
hasDate
|
||||
lines={ 2 }
|
||||
/>
|
||||
) );
|
||||
renderEmptyMessage() {
|
||||
const { lastApprovedReviewTime } = this.props;
|
||||
|
||||
const title = __( 'You have no reviews to moderate', 'woocommerce-admin' );
|
||||
let buttonUrl = '';
|
||||
let buttonTarget = '';
|
||||
let buttonText = '';
|
||||
let content = '';
|
||||
|
||||
if ( lastApprovedReviewTime ) {
|
||||
const now = new Date();
|
||||
const DAY = 24 * 60 * 60 * 1000;
|
||||
if ( ( now.getTime() - lastApprovedReviewTime ) / DAY > 30 ) {
|
||||
buttonUrl = 'https://woocommerce.com/posts/reviews-woocommerce-best-practices/';
|
||||
buttonTarget = '_blank';
|
||||
buttonText = __( 'Learn more', 'woocommerce-admin' );
|
||||
content = (
|
||||
<Fragment>
|
||||
<p>
|
||||
{ __(
|
||||
"We noticed that it's been a while since your products had any reviews.",
|
||||
'woocommerce-admin'
|
||||
) }
|
||||
</p>
|
||||
<p>
|
||||
{ __(
|
||||
'Take some time to learn about best practices for collecting and using your reviews.',
|
||||
'woocommerce-admin'
|
||||
) }
|
||||
</p>
|
||||
</Fragment>
|
||||
);
|
||||
} else {
|
||||
buttonUrl = getAdminLink( 'edit-comments.php' );
|
||||
buttonText = __( 'View all Reviews', 'woocommerce-admin' );
|
||||
content = (
|
||||
<p>
|
||||
{ __(
|
||||
/* eslint-disable max-len */
|
||||
"Awesome, you've moderated all of your product reviews. How about responding to some of those negative reviews?",
|
||||
'woocommerce-admin'
|
||||
/* eslint-enable */
|
||||
) }
|
||||
</p>
|
||||
);
|
||||
}
|
||||
} else {
|
||||
buttonUrl = 'https://woocommerce.com/posts/reviews-woocommerce-best-practices/';
|
||||
buttonTarget = '_blank';
|
||||
buttonText = __( 'Learn more', 'woocommerce-admin' );
|
||||
content = (
|
||||
<Fragment>
|
||||
<p>
|
||||
{ __( "Your customers haven't started reviewing your products.", 'woocommerce-admin' ) }
|
||||
</p>
|
||||
<p>
|
||||
{ __(
|
||||
'Take some time to learn about best practices for collecting and using your reviews.',
|
||||
'woocommerce-admin'
|
||||
) }
|
||||
</p>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ActivityCard
|
||||
className="woocommerce-empty-review-activity-card"
|
||||
title={ title }
|
||||
icon={ <Gridicon icon="time" size={ 48 } /> }
|
||||
actions={
|
||||
<Button href={ buttonUrl } target={ buttonTarget } isDefault>
|
||||
{ buttonText }
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
{ content }
|
||||
</ActivityCard>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
|
@ -190,15 +248,27 @@ class ReviewsPanel extends Component {
|
|||
);
|
||||
}
|
||||
|
||||
const title =
|
||||
isRequesting || reviews.length
|
||||
? __( 'Reviews', 'woocommerce-admin' )
|
||||
: __( 'No reviews to moderate', 'woocommerce-admin' );
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<ActivityHeader title={ __( 'Reviews', 'woocommerce-admin' ) } />
|
||||
<ActivityHeader title={ title } />
|
||||
<Section>
|
||||
{ isRequesting ? (
|
||||
this.renderPlaceholders()
|
||||
<ActivityCardPlaceholder
|
||||
className="woocommerce-review-activity-card"
|
||||
hasAction
|
||||
hasDate
|
||||
lines={ 2 }
|
||||
/>
|
||||
) : (
|
||||
<Fragment>
|
||||
{ reviews.map( review => this.renderReview( review, this.props ) ) }
|
||||
{ reviews.length
|
||||
? reviews.map( review => this.renderReview( review, this.props ) )
|
||||
: this.renderEmptyMessage() }
|
||||
</Fragment>
|
||||
) }
|
||||
</Section>
|
||||
|
@ -211,44 +281,55 @@ ReviewsPanel.propTypes = {
|
|||
reviews: PropTypes.array.isRequired,
|
||||
isError: PropTypes.bool,
|
||||
isRequesting: PropTypes.bool,
|
||||
numberOfReviews: PropTypes.number,
|
||||
};
|
||||
|
||||
ReviewsPanel.defaultProps = {
|
||||
reviews: [],
|
||||
isError: false,
|
||||
isRequesting: false,
|
||||
numberOfReviews: 0,
|
||||
};
|
||||
|
||||
export default compose(
|
||||
withSelect( ( select, props ) => {
|
||||
const { numberOfReviews } = props;
|
||||
const { getCurrentUserData, getReviews, getReviewsError, isGetReviewsRequesting } = select(
|
||||
'wc-api'
|
||||
);
|
||||
if ( numberOfReviews === 0 ) {
|
||||
return {};
|
||||
}
|
||||
const userData = getCurrentUserData();
|
||||
export default withSelect( ( select, props ) => {
|
||||
const { hasUnapprovedReviews } = props;
|
||||
const { getReviews, getReviewsError, isGetReviewsRequesting } = select( 'wc-api' );
|
||||
let reviews = [];
|
||||
let isError = false;
|
||||
let isRequesting = false;
|
||||
let lastApprovedReviewTime = null;
|
||||
if ( hasUnapprovedReviews ) {
|
||||
const reviewsQuery = {
|
||||
page: 1,
|
||||
per_page: QUERY_DEFAULTS.pageSize,
|
||||
status: DEFAULT_REVIEW_STATUSES,
|
||||
status: 'hold',
|
||||
_embed: 1,
|
||||
};
|
||||
|
||||
const reviews = getReviews( reviewsQuery );
|
||||
const isError = Boolean( getReviewsError( reviewsQuery ) );
|
||||
const isRequesting = isGetReviewsRequesting( reviewsQuery );
|
||||
|
||||
return { reviews, isError, isRequesting, lastRead: userData.activity_panel_reviews_last_read };
|
||||
} ),
|
||||
withDispatch( dispatch => {
|
||||
const { updateCurrentUserData } = dispatch( 'wc-api' );
|
||||
|
||||
return {
|
||||
updateCurrentUserData,
|
||||
reviews = getReviews( reviewsQuery );
|
||||
isError = Boolean( getReviewsError( reviewsQuery ) );
|
||||
isRequesting = isGetReviewsRequesting( reviewsQuery );
|
||||
} else {
|
||||
const approvedReviewsQuery = {
|
||||
page: 1,
|
||||
per_page: 1,
|
||||
status: 'approved',
|
||||
_embed: 1,
|
||||
};
|
||||
} )
|
||||
)( ReviewsPanel );
|
||||
const approvedReviews = getReviews( approvedReviewsQuery );
|
||||
if ( approvedReviews.length ) {
|
||||
const lastApprovedReview = approvedReviews[ 0 ];
|
||||
if ( lastApprovedReview.date_created_gmt ) {
|
||||
const creationDate = new Date( lastApprovedReview.date_created_gmt );
|
||||
lastApprovedReviewTime = creationDate.getTime();
|
||||
}
|
||||
}
|
||||
|
||||
isError = Boolean( getReviewsError( approvedReviewsQuery ) );
|
||||
isRequesting = isGetReviewsRequesting( approvedReviewsQuery );
|
||||
}
|
||||
|
||||
return {
|
||||
reviews,
|
||||
isError,
|
||||
isRequesting,
|
||||
lastApprovedReviewTime,
|
||||
};
|
||||
} )( ReviewsPanel );
|
||||
|
|
|
@ -116,7 +116,10 @@ class ProductStockCard extends Component {
|
|||
render() {
|
||||
const { product } = this.props;
|
||||
const title = (
|
||||
<Link href={ 'post.php?action=edit&post=' + ( product.parent_id || product.id ) } type="wp-admin">
|
||||
<Link
|
||||
href={ 'post.php?action=edit&post=' + ( product.parent_id || product.id ) }
|
||||
type="wp-admin"
|
||||
>
|
||||
{ product.name }
|
||||
</Link>
|
||||
);
|
||||
|
|
|
@ -6,26 +6,24 @@
|
|||
align-items: center;
|
||||
position: fixed;
|
||||
right: 0;
|
||||
height: 80px;
|
||||
height: $medium-header-height;
|
||||
|
||||
@include breakpoint( '<782px' ) {
|
||||
position: relative;
|
||||
background: $white;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
top: -3px;
|
||||
width: 100vw;
|
||||
display: none;
|
||||
height: 60px;
|
||||
flex: 1 100%;
|
||||
}
|
||||
|
||||
@include breakpoint( '782px-960px' ) {
|
||||
max-width: 280px;
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
@include breakpoint( '>960px' ) {
|
||||
height: $large-header-height;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
|
@ -37,11 +35,11 @@
|
|||
.woocommerce-layout__activity-panel-tabs {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
height: 60px;
|
||||
height: $medium-header-height;
|
||||
justify-content: flex-end;
|
||||
|
||||
@include breakpoint( '>960px' ) {
|
||||
height: 80px;
|
||||
height: $large-header-height;
|
||||
}
|
||||
|
||||
.dashicon,
|
||||
|
@ -78,10 +76,10 @@
|
|||
min-width: 80px;
|
||||
width: 100%;
|
||||
font-size: 11px;
|
||||
height: 60px;
|
||||
height: $medium-header-height;
|
||||
@include breakpoint( '>960px' ) {
|
||||
font-size: 13px;
|
||||
height: 80px;
|
||||
height: $large-header-height;
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
|
@ -149,9 +147,9 @@
|
|||
.woocommerce-layout__activity-panel-mobile-toggle {
|
||||
margin-right: 10px;
|
||||
max-width: 48px;
|
||||
height: 46px;
|
||||
height: $small-header-height;
|
||||
position: fixed;
|
||||
top: 50px;
|
||||
top: $adminbar-height-mobile;
|
||||
right: 0;
|
||||
@include breakpoint( '>782px' ) {
|
||||
display: none;
|
||||
|
@ -194,31 +192,26 @@
|
|||
}
|
||||
|
||||
.woocommerce-layout__activity-panel-wrapper {
|
||||
height: calc(100vh - 80px);
|
||||
min-height: calc(100vh - 80px);
|
||||
height: calc(100vh - #{$medium-header-height + $small-header-height + $adminbar-height-mobile});
|
||||
background: $core-grey-light-200;
|
||||
box-shadow: 0 12px 12px 0 rgba(85, 93, 102, 0.3);
|
||||
width: 0;
|
||||
@include activity-panel-slide();
|
||||
position: fixed;
|
||||
right: 0;
|
||||
top: 92px;
|
||||
top: #{$medium-header-height + $small-header-height + $adminbar-height-mobile};
|
||||
z-index: 1000;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
|
||||
// Extra padding is needed at the bottom of the wrapper because of our positioning, height, and overflow rules. Otherwise, some content can get cut off.
|
||||
padding-bottom: $gap-small * 6;
|
||||
@include breakpoint( '782px-960px' ) {
|
||||
padding-bottom: $gap-small;
|
||||
}
|
||||
@include breakpoint( '>960px' ) {
|
||||
top: 112px;
|
||||
padding-bottom: $gap * 2;
|
||||
height: calc(100vh - #{$medium-header-height + $adminbar-height});
|
||||
top: #{$medium-header-height + $adminbar-height};
|
||||
}
|
||||
|
||||
@include breakpoint( '<782px' ) {
|
||||
top: 153px;
|
||||
@include breakpoint( '>960px' ) {
|
||||
height: calc(100vh - #{$large-header-height + $adminbar-height});
|
||||
top: #{$large-header-height + $adminbar-height};
|
||||
}
|
||||
|
||||
&.is-open {
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { DEFAULT_ACTIONABLE_STATUSES, DEFAULT_REVIEW_STATUSES } from 'wc-api/constants';
|
||||
import { DEFAULT_ACTIONABLE_STATUSES } from 'wc-api/constants';
|
||||
|
||||
export function getUnreadNotes( select ) {
|
||||
const { getCurrentUserData, getNotes, getNotesError, isGetNotesRequesting } = select( 'wc-api' );
|
||||
|
@ -57,60 +57,26 @@ export function getUnreadOrders( select ) {
|
|||
return totalOrders > 0;
|
||||
}
|
||||
|
||||
export function getUnreadReviews( select ) {
|
||||
const {
|
||||
getCurrentUserData,
|
||||
getReviews,
|
||||
getReviewsTotalCount,
|
||||
getReviewsError,
|
||||
isGetReviewsRequesting,
|
||||
} = select( 'wc-api' );
|
||||
const userData = getCurrentUserData();
|
||||
let numberOfReviews = null;
|
||||
let hasUnreadReviews = false;
|
||||
|
||||
if ( 'yes' === wcSettings.reviewsEnabled ) {
|
||||
const reviewsQuery = {
|
||||
order: 'desc',
|
||||
orderby: 'date_gmt',
|
||||
export function getUnapprovedReviews( select ) {
|
||||
const { getReviewsTotalCount, getReviewsError, isGetReviewsRequesting } = select( 'wc-api' );
|
||||
if ( 'yes' === wcSettings.reviewsEnabled && '1' === wcSettings.commentModeration ) {
|
||||
const actionableReviewsQuery = {
|
||||
page: 1,
|
||||
// @todo we are not using this review, so when the endpoint supports it,
|
||||
// it could be replaced with `per_page: 0`
|
||||
per_page: 1,
|
||||
status: DEFAULT_REVIEW_STATUSES,
|
||||
status: 'hold',
|
||||
};
|
||||
const reviews = getReviews( reviewsQuery );
|
||||
const totalReviews = getReviewsTotalCount( reviewsQuery );
|
||||
const isReviewsError = Boolean( getReviewsError( reviewsQuery ) );
|
||||
const isReviewsRequesting = isGetReviewsRequesting( reviewsQuery );
|
||||
const totalActionableReviews = getReviewsTotalCount( actionableReviewsQuery );
|
||||
const isActionableReviewsError = Boolean( getReviewsError( actionableReviewsQuery ) );
|
||||
const isActionableReviewsRequesting = isGetReviewsRequesting( actionableReviewsQuery );
|
||||
|
||||
if ( ! isReviewsError && ! isReviewsRequesting ) {
|
||||
numberOfReviews = totalReviews;
|
||||
hasUnreadReviews = Boolean(
|
||||
reviews.length &&
|
||||
reviews[ 0 ].date_created_gmt &&
|
||||
new Date( reviews[ 0 ].date_created_gmt + 'Z' ).getTime() >
|
||||
userData.activity_panel_reviews_last_read
|
||||
);
|
||||
}
|
||||
|
||||
if ( ! hasUnreadReviews && '1' === wcSettings.commentModeration ) {
|
||||
const actionableReviewsQuery = {
|
||||
page: 1,
|
||||
// @todo we are not using this review, so when the endpoint supports it,
|
||||
// it could be replaced with `per_page: 0`
|
||||
per_page: 1,
|
||||
status: 'hold',
|
||||
};
|
||||
const totalActionableReviews = getReviewsTotalCount( actionableReviewsQuery );
|
||||
const isActionableReviewsError = Boolean( getReviewsError( actionableReviewsQuery ) );
|
||||
const isActionableReviewsRequesting = isGetReviewsRequesting( actionableReviewsQuery );
|
||||
|
||||
if ( ! isActionableReviewsError && ! isActionableReviewsRequesting ) {
|
||||
hasUnreadReviews = totalActionableReviews > 0;
|
||||
}
|
||||
if ( ! isActionableReviewsError && ! isActionableReviewsRequesting ) {
|
||||
return totalActionableReviews > 0;
|
||||
}
|
||||
}
|
||||
|
||||
return { numberOfReviews, hasUnreadReviews };
|
||||
return false;
|
||||
}
|
||||
|
||||
export function getUnreadStock( select ) {
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
box-sizing: border-box;
|
||||
border-bottom: 1px solid $white;
|
||||
padding: 0;
|
||||
height: 80px;
|
||||
height: $large-header-height;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
top: $adminbar-height;
|
||||
|
@ -20,12 +20,12 @@
|
|||
|
||||
@include breakpoint( '<782px' ) {
|
||||
top: $adminbar-height-mobile;
|
||||
height: 50px;
|
||||
height: $small-header-height;
|
||||
flex-flow: row wrap;
|
||||
}
|
||||
|
||||
@include breakpoint( '782px-960px' ) {
|
||||
height: 60px;
|
||||
height: $medium-header-height;
|
||||
}
|
||||
|
||||
.woocommerce-layout__header-breadcrumbs {
|
||||
|
@ -34,18 +34,18 @@
|
|||
padding: 0 0 0 $fallback-gutter-large;
|
||||
padding: 0 0 0 $gutter-large;
|
||||
flex: 1 auto;
|
||||
height: 50px;
|
||||
line-height: 50px;
|
||||
height: $small-header-height;
|
||||
line-height: $small-header-height;
|
||||
background: $white;
|
||||
|
||||
@include breakpoint( '782px-960px' ) {
|
||||
height: 60px;
|
||||
line-height: 60px;
|
||||
height: $medium-header-height;
|
||||
line-height: $medium-header-height;
|
||||
}
|
||||
|
||||
@include breakpoint( '>960px' ) {
|
||||
height: 80px;
|
||||
line-height: 80px;
|
||||
height: $large-header-height;
|
||||
line-height: $large-header-height;
|
||||
}
|
||||
|
||||
span + span::before {
|
||||
|
|
|
@ -13,6 +13,11 @@ $gap-small: 12px;
|
|||
$gap-smaller: 8px;
|
||||
$gap-smallest: 4px;
|
||||
|
||||
// Header
|
||||
$small-header-height: 50px;
|
||||
$medium-header-height: 60px;
|
||||
$large-header-height: 80px;
|
||||
|
||||
// @todo Remove this spacing variable
|
||||
$spacing: 16px;
|
||||
|
||||
|
|
|
@ -14,8 +14,6 @@ export const DEFAULT_REQUIREMENT = {
|
|||
// WordPress & WooCommerce both set a hard limit of 100 for the per_page parameter
|
||||
export const MAX_PER_PAGE = 100;
|
||||
|
||||
export const DEFAULT_REVIEW_STATUSES = 'approved,hold';
|
||||
|
||||
export const DEFAULT_ACTIONABLE_STATUSES = [ 'processing', 'on-hold' ];
|
||||
|
||||
export const QUERY_DEFAULTS = {
|
||||
|
|
|
@ -40,14 +40,11 @@ function updateCurrentUserData( resourceNames, data, fetch ) {
|
|||
'revenue_report_columns',
|
||||
'taxes_report_columns',
|
||||
'variations_report_columns',
|
||||
'dashboard_performance_indicators',
|
||||
'dashboard_charts',
|
||||
'dashboard_sections',
|
||||
'dashboard_chart_type',
|
||||
'dashboard_chart_interval',
|
||||
'dashboard_leaderboards',
|
||||
'dashboard_leaderboard_rows',
|
||||
'activity_panel_inbox_last_read',
|
||||
'activity_panel_reviews_last_read',
|
||||
];
|
||||
|
||||
if ( resourceNames.includes( resourceName ) ) {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
|
||||
import { addFilter } from '@wordpress/hooks';
|
||||
import { Fragment } from '@wordpress/element';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
|
@ -8,16 +9,43 @@ import { __ } from '@wordpress/i18n';
|
|||
/**
|
||||
* WooCommerce dependencies
|
||||
*/
|
||||
import { ReportFilters } from '@woocommerce/components';
|
||||
import { ReportFilters, TableCard } from '@woocommerce/components';
|
||||
|
||||
const Report = ( { path, query } ) => {
|
||||
return (
|
||||
<Fragment>
|
||||
<ReportFilters
|
||||
query={ query }
|
||||
path={ path }
|
||||
filters={ [] }
|
||||
advancedFilters={ {} }
|
||||
<ReportFilters query={ query } path={ path } filters={ [] } advancedFilters={ {} } />
|
||||
<TableCard
|
||||
title="Apples"
|
||||
headers={ [
|
||||
{ label: 'Type', key: 'type' },
|
||||
{ label: 'Color', key: 'color' },
|
||||
{ label: 'Taste', key: 'taste' },
|
||||
] }
|
||||
rows={ [
|
||||
[
|
||||
{ display: 'Granny Smith', value: 'type' },
|
||||
{ display: 'Green', value: 'color' },
|
||||
{ display: 'Tangy and sweet', value: 'taste' },
|
||||
],
|
||||
[
|
||||
{ display: 'Golden Delicious', value: 'type' },
|
||||
{ display: 'Gold', value: 'color' },
|
||||
{ display: 'Sweet and cheery', value: 'taste' },
|
||||
],
|
||||
[
|
||||
{ display: 'Gala', value: 'type' },
|
||||
{ display: 'Red', value: 'color' },
|
||||
{ display: 'Mild, sweet and crisp', value: 'taste' },
|
||||
],
|
||||
[
|
||||
{ display: 'Braeburn', value: 'type' },
|
||||
{ display: 'Red', value: 'color' },
|
||||
{ display: 'Firm, crisp and pleasing', value: 'taste' },
|
||||
],
|
||||
] }
|
||||
rowsPerPage={ 100 }
|
||||
totalRows={ 1 }
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
const path = require( 'path' );
|
||||
const CopyWebpackPlugin = require( 'copy-webpack-plugin' );
|
||||
const fs = require( 'fs' );
|
||||
const woocommerceAdminConfig = require( path.resolve( __dirname, '../../../webpack.config.js' ) );
|
||||
|
||||
const extArg = process.argv.find( arg => arg.startsWith( '--ext=' ) );
|
||||
|
||||
|
@ -19,31 +20,6 @@ if ( ! fs.existsSync( extensionPath ) ) {
|
|||
throw new Error( 'Extension example does not exist.' );
|
||||
}
|
||||
|
||||
const externals = {
|
||||
'@wordpress/api-fetch': 'window.wp.apiFetch',
|
||||
'@wordpress/blocks': 'window.wp.blocks',
|
||||
'@wordpress/components': 'window.wp.components',
|
||||
'@wordpress/compose': 'window.wp.compose',
|
||||
'@wordpress/data': 'window.wp.data',
|
||||
'@wordpress/editor': 'window.wp.editor',
|
||||
'@wordpress/element': 'window.wp.element',
|
||||
'@wordpress/hooks': 'window.wp.hooks',
|
||||
'@wordpress/html-entities': 'window.wp.htmlEntities',
|
||||
'@wordpress/i18n': 'window.wp.i18n',
|
||||
'@wordpress/keycodes': 'window.wp.keycodes',
|
||||
tinymce: 'tinymce',
|
||||
moment: 'moment',
|
||||
react: 'React',
|
||||
lodash: 'lodash',
|
||||
'react-dom': 'ReactDOM',
|
||||
'@woocommerce/components': 'window.wc.components',
|
||||
'@woocommerce/csv-export': 'window.wc.csvExport',
|
||||
'@woocommerce/currency': 'window.wc.currency',
|
||||
'@woocommerce/date': 'window.wc.date',
|
||||
'@woocommerce/navigation': 'window.wc.navigation',
|
||||
'@woocommerce/number': 'window.wc.number',
|
||||
};
|
||||
|
||||
const webpackConfig = {
|
||||
mode: 'development',
|
||||
entry: {
|
||||
|
@ -52,8 +28,9 @@ const webpackConfig = {
|
|||
output: {
|
||||
filename: '[name]/dist/index.js',
|
||||
path: path.resolve( __dirname ),
|
||||
libraryTarget: 'this',
|
||||
},
|
||||
externals,
|
||||
externals: woocommerceAdminConfig.externals,
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# 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.
|
||||
Features inside the `woocommerce-admin` repository can be in various states of completeness. In addition to the development copy of `woocommerce-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, `woocommerce-admin` has a system for feature flags.
|
||||
|
||||
We currently support the following environments:
|
||||
|
||||
|
@ -46,4 +46,4 @@ if ( WC_Admin_Loader::is_feature_enabled( 'activity-panels' ) ) {
|
|||
}
|
||||
```
|
||||
|
||||
If you name a directory after the flag in `includes/` with a `class-wc-admin-FEATURE.php` file, this code will also automatically be loaded.
|
||||
If you name a directory after the flag in `includes/` with a `class-wc-admin-FEATURE.php` file, this code will also automatically be loaded.
|
||||
|
|
|
@ -29,11 +29,11 @@ class WC_Admin_REST_Orders_Controller extends WC_REST_Orders_Controller {
|
|||
* @return array
|
||||
*/
|
||||
public function get_collection_params() {
|
||||
$params = parent::get_collection_params();
|
||||
$params = parent::get_collection_params();
|
||||
// This needs to remain a string to support extensions that filter Order Number.
|
||||
$params['number'] = array(
|
||||
'description' => __( 'Limit result set to orders matching part of an order number.', 'woocommerce-admin' ),
|
||||
'type' => 'integer',
|
||||
'sanitize_callback' => 'absint',
|
||||
'type' => 'string',
|
||||
'validate_callback' => 'rest_validate_request_arg',
|
||||
);
|
||||
return $params;
|
||||
|
@ -51,15 +51,22 @@ class WC_Admin_REST_Orders_Controller extends WC_REST_Orders_Controller {
|
|||
|
||||
// Search by partial order number.
|
||||
if ( ! empty( $request['number'] ) ) {
|
||||
$order_ids = $wpdb->get_col(
|
||||
$partial_number = trim( $request['number'] );
|
||||
$limit = intval( $args['posts_per_page'] );
|
||||
$order_ids = $wpdb->get_col(
|
||||
$wpdb->prepare(
|
||||
"SELECT ID FROM {$wpdb->prefix}posts WHERE post_type = 'shop_order' AND ID LIKE %s",
|
||||
intval( $request['number'] ) . '%'
|
||||
"SELECT ID
|
||||
FROM {$wpdb->prefix}posts
|
||||
WHERE post_type = 'shop_order'
|
||||
AND ID LIKE %s
|
||||
LIMIT %d",
|
||||
$wpdb->esc_like( absint( $partial_number ) ) . '%',
|
||||
$limit
|
||||
)
|
||||
);
|
||||
|
||||
// Force WP_Query return empty if don't found any order.
|
||||
$order_ids = ! empty( $order_ids ) ? $order_ids : array( 0 );
|
||||
$order_ids = empty( $order_ids ) ? array( 0 ) : $order_ids;
|
||||
$args['post__in'] = $order_ids;
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,252 @@
|
|||
<?php
|
||||
/**
|
||||
* REST API Reports Import Controller
|
||||
*
|
||||
* Handles requests to /reports/import
|
||||
*
|
||||
* @package WooCommerce Admin/API
|
||||
*/
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* Reports Imports controller.
|
||||
*
|
||||
* @package WooCommerce Admin/API
|
||||
* @extends WC_REST_Data_Controller
|
||||
*/
|
||||
class WC_Admin_REST_Reports_Import_Controller extends WC_Admin_REST_Reports_Controller {
|
||||
/**
|
||||
* Endpoint namespace.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $namespace = 'wc/v4';
|
||||
|
||||
/**
|
||||
* Route base.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $rest_base = 'reports/import';
|
||||
|
||||
/**
|
||||
* Register routes.
|
||||
*/
|
||||
public function register_routes() {
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
'/' . $this->rest_base,
|
||||
array(
|
||||
array(
|
||||
'methods' => WP_REST_Server::EDITABLE,
|
||||
'callback' => array( $this, 'import_items' ),
|
||||
'permission_callback' => array( $this, 'import_permissions_check' ),
|
||||
'args' => $this->get_import_collection_params(),
|
||||
),
|
||||
'schema' => array( $this, 'get_import_public_schema' ),
|
||||
)
|
||||
);
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
'/' . $this->rest_base . '/cancel',
|
||||
array(
|
||||
array(
|
||||
'methods' => WP_REST_Server::EDITABLE,
|
||||
'callback' => array( $this, 'cancel_import' ),
|
||||
'permission_callback' => array( $this, 'import_permissions_check' ),
|
||||
),
|
||||
'schema' => array( $this, 'get_import_public_schema' ),
|
||||
)
|
||||
);
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
'/' . $this->rest_base . '/delete',
|
||||
array(
|
||||
array(
|
||||
'methods' => WP_REST_Server::EDITABLE,
|
||||
'callback' => array( $this, 'delete_imported_items' ),
|
||||
'permission_callback' => array( $this, 'import_permissions_check' ),
|
||||
),
|
||||
'schema' => array( $this, 'get_import_public_schema' ),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes sure the current user has access to WRITE the settings APIs.
|
||||
*
|
||||
* @param WP_REST_Request $request Full data about the request.
|
||||
* @return WP_Error|bool
|
||||
*/
|
||||
public function import_permissions_check( $request ) {
|
||||
if ( ! wc_rest_check_manager_permissions( 'settings', 'edit' ) ) {
|
||||
return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you cannot edit this resource.', 'woocommerce-admin' ), array( 'status' => rest_authorization_required_code() ) );
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Import data based on user request params.
|
||||
*
|
||||
* @param WP_REST_Request $request Request data.
|
||||
* @return WP_Error|WP_REST_Response
|
||||
*/
|
||||
public function import_items( $request ) {
|
||||
$query_args = $this->prepare_objects_query( $request );
|
||||
$import = WC_Admin_Reports_Sync::regenerate_report_data( $query_args['days'], $query_args['skip_existing'] );
|
||||
|
||||
if ( is_wp_error( $import ) ) {
|
||||
$result = array(
|
||||
'status' => 'error',
|
||||
'message' => $import->get_error_message(),
|
||||
);
|
||||
} else {
|
||||
$result = array(
|
||||
'status' => 'success',
|
||||
'message' => $import,
|
||||
);
|
||||
}
|
||||
|
||||
$response = $this->prepare_item_for_response( $result, $request );
|
||||
$data = $this->prepare_response_for_collection( $response );
|
||||
|
||||
return rest_ensure_response( $data );
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare request object as query args.
|
||||
*
|
||||
* @param WP_REST_Request $request Request data.
|
||||
* @return array
|
||||
*/
|
||||
protected function prepare_objects_query( $request ) {
|
||||
$args = array();
|
||||
$args['skip_existing'] = $request['skip_existing'];
|
||||
$args['days'] = $request['days'];
|
||||
|
||||
return $args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the data object for response.
|
||||
*
|
||||
* @param object $item Data object.
|
||||
* @param WP_REST_Request $request Request object.
|
||||
* @return WP_REST_Response $response Response data.
|
||||
*/
|
||||
public function prepare_item_for_response( $item, $request ) {
|
||||
$data = $this->add_additional_fields_to_object( $item, $request );
|
||||
$data = $this->filter_response_by_context( $data, 'view' );
|
||||
$response = rest_ensure_response( $data );
|
||||
|
||||
/**
|
||||
* Filter the list returned from the API.
|
||||
*
|
||||
* @param WP_REST_Response $response The response object.
|
||||
* @param array $item The original item.
|
||||
* @param WP_REST_Request $request Request used to generate the response.
|
||||
*/
|
||||
return apply_filters( 'woocommerce_rest_prepare_reports_import', $response, $item, $request );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the query params for collections.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_import_collection_params() {
|
||||
$params = array();
|
||||
$params['days'] = array(
|
||||
'description' => __( 'Number of days to import.', 'woocommerce-admin' ),
|
||||
'type' => 'integer',
|
||||
'sanitize_callback' => 'absint',
|
||||
'validate_callback' => 'rest_validate_request_arg',
|
||||
'minimum' => 1,
|
||||
);
|
||||
$params['skip_existing'] = array(
|
||||
'description' => __( 'Skip importing existing order data.', 'woocommerce-admin' ),
|
||||
'type' => 'boolean',
|
||||
'default' => false,
|
||||
'sanitize_callback' => 'wc_string_to_bool',
|
||||
'validate_callback' => 'rest_validate_request_arg',
|
||||
);
|
||||
return $params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Report's schema, conforming to JSON Schema.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_import_public_schema() {
|
||||
$schema = array(
|
||||
'$schema' => 'http://json-schema.org/draft-04/schema#',
|
||||
'title' => 'report_import',
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'status' => array(
|
||||
'description' => __( 'Regeneration status.', 'woocommerce-admin' ),
|
||||
'type' => 'string',
|
||||
'context' => array( 'view', 'edit' ),
|
||||
'readonly' => true,
|
||||
),
|
||||
'message' => array(
|
||||
'description' => __( 'Regenerate data message.', 'woocommerce-admin' ),
|
||||
'type' => 'string',
|
||||
'context' => array( 'view', 'edit' ),
|
||||
'readonly' => true,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return $this->add_additional_fields_schema( $schema );
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel all queued import actions.
|
||||
*
|
||||
* @param WP_REST_Request $request Request data.
|
||||
* @return WP_Error|WP_REST_Response
|
||||
*/
|
||||
public function cancel_import( $request ) {
|
||||
WC_Admin_Reports_Sync::clear_queued_actions();
|
||||
|
||||
$result = array(
|
||||
'status' => 'success',
|
||||
'message' => __( 'All pending and in-progress import actions have been cancelled.', 'woocommerce-admin' ),
|
||||
);
|
||||
|
||||
$response = $this->prepare_item_for_response( $result, $request );
|
||||
$data = $this->prepare_response_for_collection( $response );
|
||||
|
||||
return rest_ensure_response( $data );
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all imported items.
|
||||
*
|
||||
* @param WP_REST_Request $request Request data.
|
||||
* @return WP_Error|WP_REST_Response
|
||||
*/
|
||||
public function delete_imported_items( $request ) {
|
||||
$delete = WC_Admin_Reports_Sync::delete_report_data();
|
||||
|
||||
if ( is_wp_error( $delete ) ) {
|
||||
$result = array(
|
||||
'status' => 'error',
|
||||
'message' => $delete->get_error_message(),
|
||||
);
|
||||
} else {
|
||||
$result = array(
|
||||
'status' => 'success',
|
||||
'message' => $delete,
|
||||
);
|
||||
}
|
||||
|
||||
$response = $this->prepare_item_for_response( $result, $request );
|
||||
$data = $this->prepare_response_for_collection( $response );
|
||||
|
||||
return rest_ensure_response( $data );
|
||||
}
|
||||
}
|
|
@ -1,70 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* REST API WC System Status Tools Controller
|
||||
*
|
||||
* Handles requests to the /system_status/tools/* endpoints.
|
||||
*
|
||||
* @package WooCommerce Admin/API
|
||||
*/
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* System status tools controller.
|
||||
*
|
||||
* @package WooCommerce Admin/API
|
||||
* @extends WC_REST_System_Status_Tools_Controller
|
||||
*/
|
||||
class WC_Admin_REST_System_Status_Tools_Controller extends WC_REST_System_Status_Tools_Controller {
|
||||
|
||||
/**
|
||||
* Endpoint namespace.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $namespace = 'wc/v4';
|
||||
|
||||
/**
|
||||
* A list of available tools for use in the system status section.
|
||||
* 'button' becomes 'action' in the API.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_tools() {
|
||||
return array_merge(
|
||||
parent::get_tools(),
|
||||
array(
|
||||
'rebuild_stats' => array(
|
||||
'name' => __( 'Rebuild reports data', 'woocommerce-admin' ),
|
||||
'button' => __( 'Rebuild reports', 'woocommerce-admin' ),
|
||||
'desc' => __( 'This tool will rebuild all of the information used by the reports.', 'woocommerce-admin' ),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Actually executes a tool.
|
||||
*
|
||||
* @param string $tool Tool.
|
||||
* @return array
|
||||
*/
|
||||
public function execute_tool( $tool ) {
|
||||
$ran = true;
|
||||
$message = '';
|
||||
|
||||
switch ( $tool ) {
|
||||
case 'rebuild_stats':
|
||||
WC_Admin_Api_Init::regenerate_report_data();
|
||||
$message = __( 'Rebuilding reports data in the background . . .', 'woocommerce-admin' );
|
||||
break;
|
||||
default:
|
||||
return parent::execute_tool( $tool );
|
||||
}
|
||||
|
||||
return array(
|
||||
'success' => $ran,
|
||||
'message' => $message,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -47,10 +47,10 @@ class WC_Admin_ActionScheduler_WPPostStore extends ActionScheduler_wpPostStore {
|
|||
$action_types = array(
|
||||
WC_Admin_Reports_Sync::QUEUE_BATCH_ACTION,
|
||||
WC_Admin_Reports_Sync::QUEUE_DEPEDENT_ACTION,
|
||||
WC_Admin_Reports_Sync::CUSTOMERS_BATCH_ACTION,
|
||||
WC_Admin_Reports_Sync::ORDERS_BATCH_ACTION,
|
||||
WC_Admin_Reports_Sync::ORDERS_LOOKUP_BATCH_INIT,
|
||||
WC_Admin_Reports_Sync::SINGLE_ORDER_ACTION,
|
||||
WC_Admin_Reports_Sync::CUSTOMERS_IMPORT_BATCH_ACTION,
|
||||
WC_Admin_Reports_Sync::ORDERS_IMPORT_BATCH_ACTION,
|
||||
WC_Admin_Reports_Sync::ORDERS_IMPORT_BATCH_INIT,
|
||||
WC_Admin_Reports_Sync::SINGLE_ORDER_IMPORT_ACTION,
|
||||
);
|
||||
|
||||
foreach ( $action_types as $action_type ) {
|
||||
|
|
|
@ -115,7 +115,6 @@ class WC_Admin_Api_Init {
|
|||
require_once WC_ADMIN_ABSPATH . 'includes/api/class-wc-admin-rest-product-variations-controller.php';
|
||||
require_once WC_ADMIN_ABSPATH . 'includes/api/class-wc-admin-rest-reports-controller.php';
|
||||
require_once WC_ADMIN_ABSPATH . 'includes/api/class-wc-admin-rest-setting-options-controller.php';
|
||||
require_once WC_ADMIN_ABSPATH . 'includes/api/class-wc-admin-rest-system-status-tools-controller.php';
|
||||
require_once WC_ADMIN_ABSPATH . 'includes/api/class-wc-admin-rest-reports-categories-controller.php';
|
||||
require_once WC_ADMIN_ABSPATH . 'includes/api/class-wc-admin-rest-reports-coupons-controller.php';
|
||||
require_once WC_ADMIN_ABSPATH . 'includes/api/class-wc-admin-rest-reports-coupons-stats-controller.php';
|
||||
|
@ -124,6 +123,7 @@ class WC_Admin_Api_Init {
|
|||
require_once WC_ADMIN_ABSPATH . 'includes/api/class-wc-admin-rest-reports-downloads-controller.php';
|
||||
require_once WC_ADMIN_ABSPATH . 'includes/api/class-wc-admin-rest-reports-downloads-files-controller.php';
|
||||
require_once WC_ADMIN_ABSPATH . 'includes/api/class-wc-admin-rest-reports-downloads-stats-controller.php';
|
||||
require_once WC_ADMIN_ABSPATH . 'includes/api/class-wc-admin-rest-reports-import-controller.php';
|
||||
require_once WC_ADMIN_ABSPATH . 'includes/api/class-wc-admin-rest-reports-orders-controller.php';
|
||||
require_once WC_ADMIN_ABSPATH . 'includes/api/class-wc-admin-rest-reports-orders-stats-controller.php';
|
||||
require_once WC_ADMIN_ABSPATH . 'includes/api/class-wc-admin-rest-reports-products-controller.php';
|
||||
|
@ -157,7 +157,7 @@ class WC_Admin_Api_Init {
|
|||
'WC_Admin_REST_Product_Variations_Controller',
|
||||
'WC_Admin_REST_Reports_Controller',
|
||||
'WC_Admin_REST_Setting_Options_Controller',
|
||||
'WC_Admin_REST_System_Status_Tools_Controller',
|
||||
'WC_Admin_REST_Reports_Import_Controller',
|
||||
'WC_Admin_REST_Reports_Products_Controller',
|
||||
'WC_Admin_REST_Reports_Variations_Controller',
|
||||
'WC_Admin_REST_Reports_Products_Stats_Controller',
|
||||
|
|
|
@ -187,7 +187,7 @@ class WC_Admin_Loader {
|
|||
public static function update_link_structure() {
|
||||
global $submenu;
|
||||
// User does not have capabilites to see the submenu.
|
||||
if ( ! current_user_can( 'manage_woocommerce' ) ) {
|
||||
if ( ! current_user_can( 'manage_woocommerce' ) || empty( $submenu['woocommerce'] ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -22,24 +22,44 @@ class WC_Admin_Reports_Sync {
|
|||
const QUEUE_DEPEDENT_ACTION = 'wc-admin_queue_dependent_action';
|
||||
|
||||
/**
|
||||
* Action hook for processing a batch of customers.
|
||||
* Action hook for importing 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';
|
||||
const CUSTOMERS_IMPORT_BATCH_ACTION = 'wc-admin_import_customers_batch';
|
||||
|
||||
/**
|
||||
* Action hook for initializing the orders lookup batch creation.
|
||||
*/
|
||||
const ORDERS_LOOKUP_BATCH_INIT = 'wc-admin_orders_lookup_batch_init';
|
||||
const CUSTOMERS_DELETE_BATCH_INIT = 'wc-admin_delete_customers_batch_init';
|
||||
|
||||
/**
|
||||
* Action hook for processing a batch of orders.
|
||||
* Action hook for deleting a batch of customers.
|
||||
*/
|
||||
const SINGLE_ORDER_ACTION = 'wc-admin_process_order';
|
||||
const CUSTOMERS_DELETE_BATCH_ACTION = 'wc-admin_delete_customers_batch';
|
||||
|
||||
/**
|
||||
* Action hook for importing a batch of orders.
|
||||
*/
|
||||
const ORDERS_IMPORT_BATCH_ACTION = 'wc-admin_import_orders_batch';
|
||||
|
||||
/**
|
||||
* Action hook for initializing the orders lookup batch creation.
|
||||
*/
|
||||
const ORDERS_IMPORT_BATCH_INIT = 'wc-admin_orders_lookup_import_batch_init';
|
||||
|
||||
/**
|
||||
* Action hook for initializing the orders lookup batch deletion.
|
||||
*/
|
||||
const ORDERS_DELETE_BATCH_INIT = 'wc-admin_orders_lookup_delete_batch_init';
|
||||
|
||||
/**
|
||||
* Action hook for deleting a batch of orders.
|
||||
*/
|
||||
const ORDERS_DELETE_BATCH_ACTION = 'wc-admin_delete_orders_batch';
|
||||
|
||||
/**
|
||||
* Action hook for importing a batch of orders.
|
||||
*/
|
||||
const SINGLE_ORDER_IMPORT_ACTION = 'wc-admin_import_order';
|
||||
|
||||
/**
|
||||
* Action scheduler group.
|
||||
|
@ -79,30 +99,32 @@ class WC_Admin_Reports_Sync {
|
|||
* Hook in sync methods.
|
||||
*/
|
||||
public static function init() {
|
||||
// Add report regeneration to tools REST API.
|
||||
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_BATCH_ACTION, array( __CLASS__, 'queue_batches' ), 10, 4 );
|
||||
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_action( self::CUSTOMERS_IMPORT_BATCH_ACTION, array( __CLASS__, 'customer_lookup_import_batch' ), 10, 3 );
|
||||
add_action( self::CUSTOMERS_DELETE_BATCH_INIT, array( __CLASS__, 'customer_lookup_delete_batch_init' ) );
|
||||
add_action( self::CUSTOMERS_DELETE_BATCH_ACTION, array( __CLASS__, 'customer_lookup_delete_batch' ) );
|
||||
add_action( self::ORDERS_IMPORT_BATCH_ACTION, array( __CLASS__, 'orders_lookup_import_batch' ), 10, 4 );
|
||||
add_action( self::ORDERS_IMPORT_BATCH_INIT, array( __CLASS__, 'orders_lookup_import_batch_init' ), 10, 3 );
|
||||
add_action( self::ORDERS_DELETE_BATCH_ACTION, array( __CLASS__, 'orders_lookup_delete_batch' ), 10, 4 );
|
||||
add_action( self::ORDERS_DELETE_BATCH_INIT, array( __CLASS__, 'orders_lookup_delete_batch_init' ), 10, 3 );
|
||||
add_action( self::SINGLE_ORDER_IMPORT_ACTION, array( __CLASS__, 'orders_lookup_import_order' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Regenerate data for reports.
|
||||
*
|
||||
* @param int|bool $days Number of days to import.
|
||||
* @param bool $skip_existing Skip exisiting records.
|
||||
* @return string
|
||||
*/
|
||||
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 );
|
||||
public static function regenerate_report_data( $days, $skip_existing ) {
|
||||
self::customer_lookup_import_batch_init( $days, $skip_existing );
|
||||
self::queue_dependent_action( self::ORDERS_IMPORT_BATCH_INIT, array( $days, $skip_existing ), self::CUSTOMERS_IMPORT_BATCH_ACTION );
|
||||
|
||||
return __( 'Report table data is being rebuilt. Please allow some time for data to fully populate.', 'woocommerce-admin' );
|
||||
}
|
||||
|
@ -122,42 +144,36 @@ class WC_Admin_Reports_Sync {
|
|||
}
|
||||
|
||||
/**
|
||||
* Adds regenerate tool to WC system status tools API.
|
||||
* Delete all data for reports.
|
||||
*
|
||||
* @param array $tools List of tools.
|
||||
* @return array
|
||||
* @return string
|
||||
*/
|
||||
public static function add_regenerate_tool( $tools ) {
|
||||
if ( isset( $_GET['page'] ) && 'wc-status' === $_GET['page'] ) { // phpcs:ignore WordPress.Security.NonceVerification
|
||||
return $tools;
|
||||
}
|
||||
public static function delete_report_data() {
|
||||
// Cancel all pending import jobs.
|
||||
self::clear_queued_actions();
|
||||
|
||||
return array_merge(
|
||||
$tools,
|
||||
array(
|
||||
'rebuild_stats' => array(
|
||||
'name' => __( 'Rebuild reports data', 'woocommerce-admin' ),
|
||||
'button' => __( 'Rebuild reports', 'woocommerce-admin' ),
|
||||
'desc' => __( 'This tool will rebuild all of the information used by the reports.', 'woocommerce-admin' ),
|
||||
'callback' => array( __CLASS__, 'regenerate_report_data' ),
|
||||
),
|
||||
)
|
||||
);
|
||||
// Delete orders in batches.
|
||||
self::queue()->schedule_single( time() + 5, self::ORDERS_DELETE_BATCH_INIT, array(), self::QUEUE_GROUP );
|
||||
|
||||
// Delete customers after order data is deleted.
|
||||
self::queue_dependent_action( self::CUSTOMERS_DELETE_BATCH_INIT, array(), self::ORDERS_DELETE_BATCH_INIT );
|
||||
|
||||
return __( 'Report table data is being deleted.', 'woocommerce-admin' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule an action to process a single Order.
|
||||
* Schedule an action to import a single Order.
|
||||
*
|
||||
* @param int $order_id Order ID.
|
||||
* @return void
|
||||
*/
|
||||
public static function schedule_single_order_process( $order_id ) {
|
||||
public static function schedule_single_order_import( $order_id ) {
|
||||
if ( 'shop_order' !== get_post_type( $order_id ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( apply_filters( 'woocommerce_disable_order_scheduling', false ) ) {
|
||||
self::orders_lookup_process_order( $order_id );
|
||||
self::orders_lookup_import_order( $order_id );
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -178,10 +194,10 @@ class WC_Admin_Reports_Sync {
|
|||
|
||||
// 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::SINGLE_ORDER_IMPORT_ACTION === $existing_job->get_hook() ) ||
|
||||
(
|
||||
self::QUEUE_DEPEDENT_ACTION === $existing_job->get_hook() &&
|
||||
in_array( self::SINGLE_ORDER_ACTION, $existing_job->get_args() )
|
||||
in_array( self::SINGLE_ORDER_IMPORT_ACTION, $existing_job->get_args() )
|
||||
)
|
||||
) {
|
||||
return;
|
||||
|
@ -189,7 +205,7 @@ class WC_Admin_Reports_Sync {
|
|||
}
|
||||
|
||||
// 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 );
|
||||
self::queue_dependent_action( self::SINGLE_ORDER_IMPORT_ACTION, array( $order_id ), self::CUSTOMERS_IMPORT_BATCH_ACTION );
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -199,8 +215,8 @@ class WC_Admin_Reports_Sync {
|
|||
// Activate WC_Order extension.
|
||||
WC_Admin_Order::add_filters();
|
||||
|
||||
add_action( 'save_post', array( __CLASS__, 'schedule_single_order_process' ) );
|
||||
add_action( 'woocommerce_order_refunded', array( __CLASS__, 'schedule_single_order_process' ) );
|
||||
add_action( 'save_post', array( __CLASS__, 'schedule_single_order_import' ) );
|
||||
add_action( 'woocommerce_order_refunded', array( __CLASS__, 'schedule_single_order_import' ) );
|
||||
|
||||
WC_Admin_Reports_Orders_Stats_Data_Store::init();
|
||||
WC_Admin_Reports_Customers_Data_Store::init();
|
||||
|
@ -211,59 +227,98 @@ class WC_Admin_Reports_Sync {
|
|||
|
||||
/**
|
||||
* Init order/product lookup tables update (in batches).
|
||||
*
|
||||
* @param integer|boolean $days Number of days to import.
|
||||
* @param boolean $skip_existing Skip exisiting records.
|
||||
*/
|
||||
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();
|
||||
public static function orders_lookup_import_batch_init( $days, $skip_existing ) {
|
||||
$batch_size = self::get_batch_size( self::ORDERS_IMPORT_BATCH_ACTION );
|
||||
$orders = self::get_orders( 1, 1, $days, $skip_existing );
|
||||
|
||||
if ( 0 === $result->total ) {
|
||||
if ( 0 === $orders->total ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$num_batches = ceil( $result->total / $batch_size );
|
||||
$num_batches = ceil( $orders->total / $batch_size );
|
||||
|
||||
self::queue_batches( 1, $num_batches, self::ORDERS_BATCH_ACTION );
|
||||
self::queue_batches( 1, $num_batches, self::ORDERS_IMPORT_BATCH_ACTION, array( $days, $skip_existing ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a batch of orders to update (stats and products).
|
||||
* Get the order IDs and total count that need to be synced.
|
||||
*
|
||||
* @param int $batch_number Batch number to process (essentially a query page number).
|
||||
* @param int $limit Number of records to retrieve.
|
||||
* @param int $page Page number.
|
||||
* @param int|bool $days Number of days prior to current date to limit search results.
|
||||
* @param bool $skip_existing Skip already imported orders.
|
||||
*/
|
||||
public static function get_orders( $limit = 10, $page = 1, $days = false, $skip_existing = false ) {
|
||||
global $wpdb;
|
||||
$where_clause = '';
|
||||
$offset = $page > 1 ? $page * $limit : 0;
|
||||
|
||||
if ( $days ) {
|
||||
$days_ago = date( 'Y-m-d 00:00:00', time() - ( DAY_IN_SECONDS * $days ) );
|
||||
$where_clause .= " AND post_date >= '{$days_ago}'";
|
||||
}
|
||||
|
||||
if ( $skip_existing ) {
|
||||
$where_clause .= " AND NOT EXISTS (
|
||||
SELECT 1 FROM {$wpdb->prefix}wc_order_stats
|
||||
WHERE {$wpdb->prefix}wc_order_stats.order_id = {$wpdb->posts}.ID
|
||||
)";
|
||||
}
|
||||
|
||||
$count = $wpdb->get_var(
|
||||
"SELECT COUNT(*) FROM {$wpdb->posts}
|
||||
WHERE post_type = 'shop_order'
|
||||
{$where_clause}"
|
||||
); // WPCS: unprepared SQL ok.
|
||||
|
||||
$order_ids = absint( $count ) > 0 ? $wpdb->get_col(
|
||||
$wpdb->prepare(
|
||||
"SELECT ID FROM {$wpdb->posts}
|
||||
WHERE post_type = 'shop_order'
|
||||
{$where_clause}
|
||||
ORDER BY post_date ASC
|
||||
LIMIT %d
|
||||
OFFSET %d",
|
||||
$limit,
|
||||
$offset
|
||||
)
|
||||
) : array(); // WPCS: unprepared SQL ok.
|
||||
|
||||
return (object) array(
|
||||
'total' => absint( $count ),
|
||||
'order_ids' => $order_ids,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports a batch of orders to update (stats and products).
|
||||
*
|
||||
* @param int $batch_number Batch number to import (essentially a query page number).
|
||||
* @param int|bool $days Number of days to import.
|
||||
* @param bool $skip_existing Skip exisiting records.
|
||||
* @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();
|
||||
public static function orders_lookup_import_batch( $batch_number, $days, $skip_existing ) {
|
||||
$batch_size = self::get_batch_size( self::ORDERS_IMPORT_BATCH_ACTION );
|
||||
$orders = self::get_orders( $batch_size, $batch_number, $days, $skip_existing );
|
||||
|
||||
foreach ( $order_ids as $order_id ) {
|
||||
self::orders_lookup_process_order( $order_id );
|
||||
foreach ( $orders->order_ids as $order_id ) {
|
||||
self::orders_lookup_import_order( $order_id );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a single order to update lookup tables for.
|
||||
* Imports 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 ) {
|
||||
public static function orders_lookup_import_order( $order_id ) {
|
||||
$result = array_sum(
|
||||
array(
|
||||
WC_Admin_Reports_Orders_Stats_Data_Store::sync_order( $order_id ),
|
||||
|
@ -280,7 +335,7 @@ class WC_Admin_Reports_Sync {
|
|||
}
|
||||
|
||||
// Otherwise assume an error occurred and reschedule.
|
||||
self::schedule_single_order_process( $order_id );
|
||||
self::schedule_single_order_import( $order_id );
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -292,9 +347,11 @@ class WC_Admin_Reports_Sync {
|
|||
*/
|
||||
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,
|
||||
self::QUEUE_BATCH_ACTION => 100,
|
||||
self::CUSTOMERS_IMPORT_BATCH_ACTION => 25,
|
||||
self::CUSTOMERS_DELETE_BATCH_ACTION => 25,
|
||||
self::ORDERS_IMPORT_BATCH_ACTION => 10,
|
||||
self::ORDERS_DELETE_BATCH_ACTION => 10,
|
||||
);
|
||||
$batch_size = isset( $batch_sizes[ $action ] ) ? $batch_sizes[ $action ] : 25;
|
||||
|
||||
|
@ -314,9 +371,10 @@ class WC_Admin_Reports_Sync {
|
|||
* @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.
|
||||
* @param array $action_args Action arguments.
|
||||
* @return void
|
||||
*/
|
||||
public static function queue_batches( $range_start, $range_end, $single_batch_action ) {
|
||||
public static function queue_batches( $range_start, $range_end, $single_batch_action, $action_args = array() ) {
|
||||
$batch_size = self::get_batch_size( self::QUEUE_BATCH_ACTION );
|
||||
$range_size = 1 + ( $range_end - $range_start );
|
||||
$action_timestamp = time() + 5;
|
||||
|
@ -333,14 +391,15 @@ class WC_Admin_Reports_Sync {
|
|||
self::queue()->schedule_single(
|
||||
$action_timestamp,
|
||||
self::QUEUE_BATCH_ACTION,
|
||||
array( $batch_start, $batch_end, $single_batch_action ),
|
||||
array( $batch_start, $batch_end, $single_batch_action, $action_args ),
|
||||
self::QUEUE_GROUP
|
||||
);
|
||||
}
|
||||
} 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 ), self::QUEUE_GROUP );
|
||||
$batch_action_args = array_merge( array( $i ), $action_args );
|
||||
self::queue()->schedule_single( $action_timestamp, $single_batch_action, $batch_action_args, self::QUEUE_GROUP );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -393,11 +452,64 @@ class WC_Admin_Reports_Sync {
|
|||
}
|
||||
|
||||
/**
|
||||
* Init customer lookup table update (in batches).
|
||||
* Exclude users that already exist in our customer lookup table.
|
||||
*
|
||||
* Meant to be hooked into 'pre_user_query' action.
|
||||
*
|
||||
* @param WP_User_Query $wp_user_query WP_User_Query to modify.
|
||||
*/
|
||||
public static function customer_lookup_batch_init() {
|
||||
$batch_size = self::get_batch_size( self::CUSTOMERS_BATCH_ACTION );
|
||||
$customer_query = new WP_User_Query(
|
||||
public static function exclude_existing_customers_from_query( $wp_user_query ) {
|
||||
global $wpdb;
|
||||
|
||||
$wp_user_query->query_where .= " AND NOT EXISTS (
|
||||
SELECT ID FROM {$wpdb->prefix}wc_customer_lookup
|
||||
WHERE {$wpdb->prefix}wc_customer_lookup.user_id = {$wpdb->users}.ID
|
||||
)";
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve user IDs given import criteria.
|
||||
*
|
||||
* @param int|bool $days Number of days to process.
|
||||
* @param bool $skip_existing Skip exisiting records.
|
||||
* @param array $query_args Optional. WP_User_Query args.
|
||||
* @return WP_User_Query
|
||||
*/
|
||||
public static function get_user_ids_for_batch( $days, $skip_existing, $query_args = array() ) {
|
||||
if ( ! is_array( $query_args ) ) {
|
||||
$query_args = array();
|
||||
}
|
||||
|
||||
if ( $days ) {
|
||||
$query_args['date_query'] = array(
|
||||
'after' => date( 'Y-m-d 00:00:00', time() - ( DAY_IN_SECONDS * $days ) ),
|
||||
);
|
||||
}
|
||||
|
||||
if ( $skip_existing ) {
|
||||
add_action( 'pre_user_query', array( __CLASS__, 'exclude_existing_customers_from_query' ) );
|
||||
}
|
||||
|
||||
$customer_query = new WP_User_Query( $query_args );
|
||||
|
||||
remove_action( 'pre_user_query', array( __CLASS__, 'exclude_existing_customers_from_query' ) );
|
||||
|
||||
return $customer_query;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Init customer lookup table update (in batches).
|
||||
*
|
||||
* @param int|bool $days Number of days to process.
|
||||
* @param bool $skip_existing Skip exisiting records.
|
||||
*/
|
||||
public static function customer_lookup_import_batch_init( $days, $skip_existing ) {
|
||||
$batch_size = self::get_batch_size( self::CUSTOMERS_IMPORT_BATCH_ACTION );
|
||||
$customer_query = self::get_user_ids_for_batch(
|
||||
$days,
|
||||
$skip_existing,
|
||||
array(
|
||||
'fields' => 'ID',
|
||||
'number' => 1,
|
||||
|
@ -411,18 +523,22 @@ class WC_Admin_Reports_Sync {
|
|||
|
||||
$num_batches = ceil( $total_customers / $batch_size );
|
||||
|
||||
self::queue_batches( 1, $num_batches, self::CUSTOMERS_BATCH_ACTION );
|
||||
self::queue_batches( 1, $num_batches, self::CUSTOMERS_IMPORT_BATCH_ACTION, array( $days, $skip_existing ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a batch of customers to update.
|
||||
*
|
||||
* @param int $batch_number Batch number to process (essentially a query page number).
|
||||
* @param int $batch_number Batch number to process (essentially a query page number).
|
||||
* @param int|bool $days Number of days to process.
|
||||
* @param bool $skip_existing Skip exisiting records.
|
||||
* @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(
|
||||
public static function customer_lookup_import_batch( $batch_number, $days, $skip_existing ) {
|
||||
$batch_size = self::get_batch_size( self::CUSTOMERS_IMPORT_BATCH_ACTION );
|
||||
$customer_query = self::get_user_ids_for_batch(
|
||||
$days,
|
||||
$skip_existing,
|
||||
array(
|
||||
'fields' => 'ID',
|
||||
'orderby' => 'ID',
|
||||
|
@ -439,6 +555,79 @@ class WC_Admin_Reports_Sync {
|
|||
WC_Admin_Reports_Customers_Data_Store::update_registered_customer( $customer_id );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete customer lookup table rows (in batches).
|
||||
*/
|
||||
public static function customer_lookup_delete_batch_init() {
|
||||
global $wpdb;
|
||||
$batch_size = self::get_batch_size( self::CUSTOMERS_DELETE_BATCH_ACTION );
|
||||
$count = $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->prefix}wc_customer_lookup" );
|
||||
|
||||
if ( 0 === $count ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$num_batches = ceil( $count / $batch_size );
|
||||
|
||||
self::queue_batches( 1, $num_batches, self::CUSTOMERS_DELETE_BATCH_ACTION );
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a batch of customers.
|
||||
*/
|
||||
public static function customer_lookup_delete_batch() {
|
||||
global $wpdb;
|
||||
$batch_size = self::get_batch_size( self::CUSTOMERS_DELETE_BATCH_ACTION );
|
||||
$customer_ids = $wpdb->get_col(
|
||||
$wpdb->prepare(
|
||||
"SELECT customer_id FROM {$wpdb->prefix}wc_customer_lookup ORDER BY customer_id ASC LIMIT %d",
|
||||
$batch_size
|
||||
)
|
||||
);
|
||||
|
||||
foreach ( $customer_ids as $customer_id ) {
|
||||
WC_Admin_Reports_Customers_Data_Store::delete_customer( $customer_id );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete orders lookup table rows (in batches).
|
||||
*/
|
||||
public static function orders_lookup_delete_batch_init() {
|
||||
global $wpdb;
|
||||
$batch_size = self::get_batch_size( self::ORDERS_DELETE_BATCH_ACTION );
|
||||
$count = $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->prefix}wc_order_stats" );
|
||||
|
||||
if ( 0 === $count ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$num_batches = ceil( $count / $batch_size );
|
||||
|
||||
self::queue_batches( 1, $num_batches, self::ORDERS_DELETE_BATCH_ACTION );
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a batch of orders.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function orders_lookup_delete_batch() {
|
||||
global $wpdb;
|
||||
$batch_size = self::get_batch_size( self::ORDERS_DELETE_BATCH_ACTION );
|
||||
$order_ids = $wpdb->get_col(
|
||||
$wpdb->prepare(
|
||||
"SELECT order_id FROM {$wpdb->prefix}wc_order_stats ORDER BY order_id ASC LIMIT %d",
|
||||
$batch_size
|
||||
)
|
||||
);
|
||||
|
||||
foreach ( $order_ids as $order_id ) {
|
||||
WC_Admin_Reports_Orders_Stats_Data_Store::delete_order( $order_id );
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
WC_Admin_Reports_Sync::init();
|
||||
|
|
|
@ -520,8 +520,8 @@ class WC_Admin_Reports_Customers_Data_Store extends WC_Admin_Reports_Data_Store
|
|||
* @return array
|
||||
*/
|
||||
public static function get_customer_name( $user_id = 0, $order = null ) {
|
||||
$first_name = null;
|
||||
$last_name = null;
|
||||
$first_name = '';
|
||||
$last_name = '';
|
||||
|
||||
if (
|
||||
$user_id &&
|
||||
|
@ -683,7 +683,7 @@ class WC_Admin_Reports_Customers_Data_Store extends WC_Admin_Reports_Data_Store
|
|||
protected static function is_valid_customer( $user_id ) {
|
||||
$customer = new WC_Customer( $user_id );
|
||||
|
||||
if ( $customer->get_id() !== $user_id ) {
|
||||
if ( $customer->get_id() != $user_id ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -694,6 +694,31 @@ class WC_Admin_Reports_Customers_Data_Store extends WC_Admin_Reports_Data_Store
|
|||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a customer lookup row.
|
||||
*
|
||||
* @param int $customer_id Customer ID.
|
||||
*/
|
||||
public static function delete_customer( $customer_id ) {
|
||||
global $wpdb;
|
||||
$customer_id = (int) $customer_id;
|
||||
$table_name = $wpdb->prefix . self::TABLE_NAME;
|
||||
|
||||
$wpdb->query(
|
||||
$wpdb->prepare(
|
||||
"DELETE FROM ${table_name} WHERE customer_id = %d",
|
||||
$customer_id
|
||||
)
|
||||
);
|
||||
|
||||
/**
|
||||
* Fires when a customer is deleted.
|
||||
*
|
||||
* @param int $order_id Order ID.
|
||||
*/
|
||||
do_action( 'woocommerce_reports_delete_customer', $customer_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns string to be used as cache key for the data.
|
||||
*
|
||||
|
|
|
@ -36,7 +36,7 @@ class WC_Admin_Dashboard {
|
|||
}
|
||||
|
||||
/**
|
||||
* Preload data from the performance indiciators endpoint.
|
||||
* Preload data from the performance indicators endpoint.
|
||||
*
|
||||
* @param array $endpoints Array of preloaded endpoints.
|
||||
* @return array
|
||||
|
@ -48,7 +48,7 @@ class WC_Admin_Dashboard {
|
|||
}
|
||||
|
||||
/**
|
||||
* Adds fields so that we can store performance indiciators, row settings, and chart type settings for users.
|
||||
* Adds fields so that we can store performance indicators, row settings, and chart type settings for users.
|
||||
*
|
||||
* @param array $user_data_fields User data fields.
|
||||
* @return array
|
||||
|
@ -57,11 +57,9 @@ class WC_Admin_Dashboard {
|
|||
return array_merge(
|
||||
$user_data_fields,
|
||||
array(
|
||||
'dashboard_performance_indicators',
|
||||
'dashboard_charts',
|
||||
'dashboard_sections',
|
||||
'dashboard_chart_type',
|
||||
'dashboard_chart_interval',
|
||||
'dashboard_leaderboards',
|
||||
'dashboard_leaderboard_rows',
|
||||
)
|
||||
);
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -64,7 +64,7 @@
|
|||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@fresh-data/framework": "0.6.0",
|
||||
"@fresh-data/framework": "0.6.1",
|
||||
"@wordpress/api-fetch": "2.2.6",
|
||||
"@wordpress/components": "7.0.5",
|
||||
"@wordpress/data": "4.2.0",
|
||||
|
@ -107,11 +107,11 @@
|
|||
"redux": "4.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "7.4.3",
|
||||
"@babel/core": "7.4.3",
|
||||
"@babel/plugin-transform-async-to-generator": "7.4.0",
|
||||
"@babel/cli": "7.4.4",
|
||||
"@babel/core": "7.4.4",
|
||||
"@babel/plugin-transform-async-to-generator": "7.4.4",
|
||||
"@babel/plugin-transform-react-jsx": "7.3.0",
|
||||
"@babel/runtime-corejs2": "7.4.3",
|
||||
"@babel/runtime-corejs2": "7.4.4",
|
||||
"@wordpress/babel-plugin-import-jsx-pragma": "1.1.2",
|
||||
"@wordpress/babel-plugin-makepot": "2.1.2",
|
||||
"@wordpress/babel-preset-default": "3.0.1",
|
||||
|
@ -119,7 +119,7 @@
|
|||
"@wordpress/custom-templated-path-webpack-plugin": "1.1.5",
|
||||
"@wordpress/jest-preset-default": "4.0.0",
|
||||
"@wordpress/postcss-themes": "1.0.4",
|
||||
"ast-types": "0.12.3",
|
||||
"ast-types": "0.12.4",
|
||||
"autoprefixer": "9.5.1",
|
||||
"babel-core": "7.0.0-bridge.0",
|
||||
"babel-eslint": "10.0.1",
|
||||
|
@ -137,7 +137,7 @@
|
|||
"eslint": "5.16.0",
|
||||
"eslint-config-wpcalypso": "4.0.1",
|
||||
"eslint-loader": "2.1.2",
|
||||
"eslint-plugin-jest": "22.4.1",
|
||||
"eslint-plugin-jest": "22.5.1",
|
||||
"eslint-plugin-jsx-a11y": "6.2.1",
|
||||
"eslint-plugin-react": "7.12.4",
|
||||
"eslint-plugin-wpcalypso": "4.0.2",
|
||||
|
@ -145,10 +145,10 @@
|
|||
"grunt-checktextdomain": "1.0.1",
|
||||
"grunt-wp-i18n": "1.0.3",
|
||||
"husky": "1.3.1",
|
||||
"lerna": "3.13.3",
|
||||
"lerna": "3.13.4",
|
||||
"locutus": "2.0.10",
|
||||
"mini-css-extract-plugin": "0.6.0",
|
||||
"node-sass": "4.11.0",
|
||||
"node-sass": "4.12.0",
|
||||
"node-watch": "0.6.2",
|
||||
"postcss-color-function": "4.1.0",
|
||||
"postcss-loader": "3.0.0",
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
# unreleased
|
||||
# 3.0.0 (unreleased)
|
||||
- <DateInput> and <DatePicker> got a `disabled` prop.
|
||||
- TableCard component: new `onPageChange` prop.
|
||||
- TableCard now has a `defaultOrder` parameter to specify default sort column sort order.
|
||||
- Pagination no longer considers `0` a valid input and triggers `onPageChange` on the input blur event.
|
||||
- Tweaks to SummaryListPlaceholder height in order to better match SummaryNumber.
|
||||
- EllipsisMenu component (breaking change): Remove `children` prop in favor of a render prop `renderContent` so that function arguments `isOpen`, `onToggle`, and `onClose` can be passed down.
|
||||
|
||||
# 2.0.0
|
||||
- Chart legend component now uses withInstanceId HOC so the ids used in several HTML elements are unique.
|
||||
|
|
|
@ -10,4 +10,4 @@ Install the module
|
|||
npm install @woocommerce/components --save
|
||||
```
|
||||
|
||||
View [the full Component documentation](https://woocommerce.github.io/wc-admin/#/components/) for usage information.
|
||||
View [the full Component documentation](https://woocommerce.github.io/woocommerce-admin/#/components/) for usage information.
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
"module": "build-module/index.js",
|
||||
"react-native": "src/index",
|
||||
"dependencies": {
|
||||
"@babel/runtime-corejs2": "7.4.3",
|
||||
"@babel/runtime-corejs2": "7.4.4",
|
||||
"@woocommerce/csv-export": "1.1.0",
|
||||
"@woocommerce/currency": "1.1.1",
|
||||
"@woocommerce/date": "1.0.7",
|
||||
|
|
|
@ -11,11 +11,15 @@ import { partial } from 'lodash';
|
|||
import { TAB } from '@wordpress/keycodes';
|
||||
import moment from 'moment';
|
||||
|
||||
/**
|
||||
* WooCommerce dependencies
|
||||
*/
|
||||
import { dateValidationMessages, toMoment } from '@woocommerce/date';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import DateInput from './input';
|
||||
import { toMoment } from '@woocommerce/date';
|
||||
import { H, Section } from '../section';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
|
@ -54,7 +58,7 @@ class DatePicker extends Component {
|
|||
const value = event.target.value;
|
||||
const { dateFormat } = this.props;
|
||||
const date = toMoment( dateFormat, value );
|
||||
const error = date ? null : __( 'Invalid date', 'woocommerce-admin' );
|
||||
const error = date ? null : dateValidationMessages.invalid;
|
||||
|
||||
this.props.onUpdate( {
|
||||
date,
|
||||
|
@ -64,7 +68,7 @@ class DatePicker extends Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
const { date, text, dateFormat, error, isInvalidDate } = this.props;
|
||||
const { date, disabled, text, dateFormat, error, isInvalidDate } = this.props;
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
|
@ -72,6 +76,7 @@ class DatePicker extends Component {
|
|||
focusOnMount={ false }
|
||||
renderToggle={ ( { isOpen, onToggle } ) => (
|
||||
<DateInput
|
||||
disabled={ disabled }
|
||||
value={ text }
|
||||
onChange={ this.onInputChange }
|
||||
dateFormat={ dateFormat }
|
||||
|
@ -112,6 +117,10 @@ DatePicker.propTypes = {
|
|||
* A moment date object representing the selected date. `null` for no selection.
|
||||
*/
|
||||
date: PropTypes.object,
|
||||
/**
|
||||
* Whether the input is disabled.
|
||||
*/
|
||||
disabled: PropTypes.bool,
|
||||
/**
|
||||
* The date in human-readable format. Displayed in the text input.
|
||||
*/
|
||||
|
|
|
@ -8,6 +8,7 @@ import { uniqueId, noop } from 'lodash';
|
|||
import PropTypes from 'prop-types';
|
||||
|
||||
const DateInput = ( {
|
||||
disabled,
|
||||
value,
|
||||
onChange,
|
||||
dateFormat,
|
||||
|
@ -36,6 +37,7 @@ const DateInput = ( {
|
|||
placeholder={ dateFormat.toLowerCase() }
|
||||
onFocus={ onFocus }
|
||||
onKeyDown={ onKeyDown }
|
||||
disabled={ disabled }
|
||||
/>
|
||||
{ error && (
|
||||
<Popover
|
||||
|
@ -55,6 +57,7 @@ const DateInput = ( {
|
|||
};
|
||||
|
||||
DateInput.propTypes = {
|
||||
disabled: PropTypes.bool,
|
||||
value: PropTypes.string,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
dateFormat: PropTypes.string.isRequired,
|
||||
|
@ -67,6 +70,7 @@ DateInput.propTypes = {
|
|||
};
|
||||
|
||||
DateInput.defaultProps = {
|
||||
disabled: false,
|
||||
onFocus: () => {},
|
||||
errorPosition: 'bottom center',
|
||||
onKeyDown: noop,
|
||||
|
|
|
@ -97,6 +97,7 @@
|
|||
|
||||
.woocommerce-chart__types {
|
||||
padding: 0 8px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.woocommerce-chart__type-button {
|
||||
|
|
|
@ -1,26 +1,40 @@
|
|||
```jsx
|
||||
import { EllipsisMenu, MenuItem, MenuTitle } from '@woocommerce/components';
|
||||
import { EllipsisMenu, MenuItem, MenuTitle, Button } from '@woocommerce/components';
|
||||
|
||||
const MyEllipsisMenu = withState( {
|
||||
showCustomers: true,
|
||||
showOrders: true,
|
||||
} )( ( { setState, showCustomers, showOrders } ) => (
|
||||
<EllipsisMenu label="Choose which analytics to display">
|
||||
<MenuTitle>Display Stats</MenuTitle>
|
||||
<MenuItem onInvoke={ () => setState( { showCustomers: ! showCustomers } ) }>
|
||||
<ToggleControl
|
||||
label="Show Customers"
|
||||
checked={ showCustomers }
|
||||
onChange={ () => setState( { showCustomers: ! showCustomers } ) }
|
||||
/>
|
||||
</MenuItem>
|
||||
<MenuItem onInvoke={ () => setState( { showOrders: ! showOrders } ) }>
|
||||
<ToggleControl
|
||||
label="Show Orders"
|
||||
checked={ showOrders }
|
||||
onChange={ () => setState( { showOrders: ! showOrders } ) }
|
||||
/>
|
||||
</MenuItem>
|
||||
</EllipsisMenu>
|
||||
<EllipsisMenu label="Choose which analytics to display"
|
||||
renderContent={ ( { onToggle } )=> {
|
||||
return (
|
||||
<div>
|
||||
<MenuTitle>Display Stats</MenuTitle>
|
||||
<MenuItem onInvoke={ () => setState( { showCustomers: ! showCustomers } ) }>
|
||||
<ToggleControl
|
||||
label="Show Customers"
|
||||
checked={ showCustomers }
|
||||
onChange={ () => setState( { showCustomers: ! showCustomers } ) }
|
||||
/>
|
||||
</MenuItem>
|
||||
<MenuItem onInvoke={ () => setState( { showOrders: ! showOrders } ) }>
|
||||
<ToggleControl
|
||||
label="Show Orders"
|
||||
checked={ showOrders }
|
||||
onChange={ () => setState( { showOrders: ! showOrders } ) }
|
||||
/>
|
||||
</MenuItem>
|
||||
<MenuItem onInvoke={ onToggle }>
|
||||
<Button
|
||||
label="Close menu"
|
||||
onClick={ onToggle }
|
||||
>
|
||||
Close Menu
|
||||
</Button>
|
||||
</MenuItem>
|
||||
</div>
|
||||
);
|
||||
} }
|
||||
/>
|
||||
) );
|
||||
```
|
||||
|
|
|
@ -12,12 +12,12 @@ import PropTypes from 'prop-types';
|
|||
*/
|
||||
class EllipsisMenu extends Component {
|
||||
render() {
|
||||
const { children, label } = this.props;
|
||||
if ( ! children ) {
|
||||
const { label, renderContent } = this.props;
|
||||
if ( ! renderContent ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const renderToggle = ( { onToggle, isOpen } ) => {
|
||||
const renderEllipsis = ( { onToggle, isOpen } ) => {
|
||||
const toggleClassname = classnames( 'woocommerce-ellipsis-menu__toggle', {
|
||||
'is-opened': isOpen,
|
||||
} );
|
||||
|
@ -33,8 +33,10 @@ class EllipsisMenu extends Component {
|
|||
);
|
||||
};
|
||||
|
||||
const renderContent = () => (
|
||||
<NavigableMenu className="woocommerce-ellipsis-menu__content">{ children }</NavigableMenu>
|
||||
const renderMenu = renderContentArgs => (
|
||||
<NavigableMenu className="woocommerce-ellipsis-menu__content">
|
||||
{ renderContent( renderContentArgs ) }
|
||||
</NavigableMenu>
|
||||
);
|
||||
|
||||
return (
|
||||
|
@ -42,8 +44,8 @@ class EllipsisMenu extends Component {
|
|||
<Dropdown
|
||||
contentClassName="woocommerce-ellipsis-menu__popover"
|
||||
position="bottom left"
|
||||
renderToggle={ renderToggle }
|
||||
renderContent={ renderContent }
|
||||
renderToggle={ renderEllipsis }
|
||||
renderContent={ renderMenu }
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@ -56,9 +58,9 @@ EllipsisMenu.propTypes = {
|
|||
*/
|
||||
label: PropTypes.string.isRequired,
|
||||
/**
|
||||
* A list of `MenuTitle`/`MenuItem` components
|
||||
* A function returning `MenuTitle`/`MenuItem` components as a render prop. Arguments from Dropdown passed as function arguments.
|
||||
*/
|
||||
children: PropTypes.node,
|
||||
renderContent: PropTypes.func,
|
||||
};
|
||||
|
||||
export default EllipsisMenu;
|
||||
|
|
|
@ -47,6 +47,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
.components-base-control__label,
|
||||
.woocommerce-ellipsis-menu__title {
|
||||
color: #6c7781;
|
||||
padding-bottom: 8px;
|
||||
|
|
|
@ -67,10 +67,10 @@ class Pagination extends Component {
|
|||
}
|
||||
|
||||
onInputBlur( event ) {
|
||||
const { onPageChange } = this.props;
|
||||
const { onPageChange, page } = this.props;
|
||||
const newPage = parseInt( event.target.value, 10 );
|
||||
|
||||
if ( isFinite( newPage ) && newPage > 0 && this.pageCount && this.pageCount >= newPage ) {
|
||||
if ( newPage !== page && isFinite( newPage ) && newPage > 0 && this.pageCount && this.pageCount >= newPage ) {
|
||||
onPageChange( newPage );
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
* External dependencies
|
||||
*/
|
||||
import apiFetch from '@wordpress/api-fetch';
|
||||
import { isNaN } from 'lodash';
|
||||
|
||||
/**
|
||||
* WooCommerce dependencies
|
||||
|
@ -24,18 +23,11 @@ import { computeSuggestionMatch } from './utils';
|
|||
export default {
|
||||
name: 'orders',
|
||||
className: 'woocommerce-search__order-result',
|
||||
inputType: 'number',
|
||||
options( search ) {
|
||||
let payload = '';
|
||||
if ( search ) {
|
||||
const number = parseInt( search );
|
||||
|
||||
if ( isNaN( number ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
const query = {
|
||||
number,
|
||||
number: search,
|
||||
per_page: 10,
|
||||
};
|
||||
payload = stringifyQuery( query );
|
||||
|
|
|
@ -53,6 +53,7 @@ class TableCard extends Component {
|
|||
this.onColumnToggle = this.onColumnToggle.bind( this );
|
||||
this.onClickDownload = this.onClickDownload.bind( this );
|
||||
this.onCompare = this.onCompare.bind( this );
|
||||
this.onPageChange = this.onPageChange.bind( this );
|
||||
this.onSearch = this.onSearch.bind( this );
|
||||
this.selectRow = this.selectRow.bind( this );
|
||||
this.selectAllRows = this.selectAllRows.bind( this );
|
||||
|
@ -168,6 +169,16 @@ class TableCard extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
onPageChange( ...params ) {
|
||||
const { onPageChange, onQueryChange } = this.props;
|
||||
if ( onPageChange ) {
|
||||
onPageChange( ...params );
|
||||
}
|
||||
if ( onQueryChange ) {
|
||||
onQueryChange( 'page' )( ...params );
|
||||
}
|
||||
}
|
||||
|
||||
onSearch( values ) {
|
||||
const { compareParam, searchBy, baseSearchQuery } = this.props;
|
||||
// A comma is used as a separator between search terms, so we want to escape
|
||||
|
@ -329,25 +340,29 @@ class TableCard extends Component {
|
|||
),
|
||||
] }
|
||||
menu={
|
||||
showMenu && <EllipsisMenu label={ __( 'Choose which values to display', 'woocommerce-admin' ) }>
|
||||
<MenuTitle>{ __( 'Columns:', 'woocommerce-admin' ) }</MenuTitle>
|
||||
{ allHeaders.map( ( { key, label, required } ) => {
|
||||
if ( required ) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<MenuItem
|
||||
checked={ showCols.includes( key ) }
|
||||
isCheckbox
|
||||
isClickable
|
||||
key={ key }
|
||||
onInvoke={ this.onColumnToggle( key ) }
|
||||
>
|
||||
{ label }
|
||||
</MenuItem>
|
||||
);
|
||||
} ) }
|
||||
</EllipsisMenu>
|
||||
showMenu && <EllipsisMenu label={ __( 'Choose which values to display', 'woocommerce-admin' ) }
|
||||
renderContent={ () => (
|
||||
<Fragment>
|
||||
<MenuTitle>{ __( 'Columns:', 'woocommerce-admin' ) }</MenuTitle>
|
||||
{ allHeaders.map( ( { key, label, required } ) => {
|
||||
if ( required ) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<MenuItem
|
||||
checked={ showCols.includes( key ) }
|
||||
isCheckbox
|
||||
isClickable
|
||||
key={ key }
|
||||
onInvoke={ this.onColumnToggle( key ) }
|
||||
>
|
||||
{ label }
|
||||
</MenuItem>
|
||||
);
|
||||
} ) }
|
||||
</Fragment>
|
||||
) }
|
||||
/>
|
||||
}
|
||||
>
|
||||
{ isLoading ? (
|
||||
|
@ -380,7 +395,7 @@ class TableCard extends Component {
|
|||
page={ parseInt( query.page ) || 1 }
|
||||
perPage={ rowsPerPage }
|
||||
total={ totalRows }
|
||||
onPageChange={ onQueryChange( 'page' ) }
|
||||
onPageChange={ this.onPageChange }
|
||||
onPerPageChange={ onQueryChange( 'per_page' ) }
|
||||
/>
|
||||
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
"module": "build-module/index.js",
|
||||
"react-native": "src/index",
|
||||
"dependencies": {
|
||||
"@babel/runtime-corejs2": "7.4.3",
|
||||
"@babel/runtime-corejs2": "7.4.4",
|
||||
"browser-filesaver": "1.1.1",
|
||||
"moment": "2.22.2"
|
||||
},
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
"module": "build-module/index.js",
|
||||
"react-native": "src/index",
|
||||
"dependencies": {
|
||||
"@babel/runtime-corejs2": "7.4.3",
|
||||
"@babel/runtime-corejs2": "7.4.4",
|
||||
"@woocommerce/number": "1.0.2",
|
||||
"lodash": "4.17.11"
|
||||
},
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
"module": "build-module/index.js",
|
||||
"react-native": "src/index",
|
||||
"dependencies": {
|
||||
"@babel/runtime-corejs2": "7.4.3",
|
||||
"@babel/runtime-corejs2": "7.4.4",
|
||||
"@wordpress/date": "3.0.1",
|
||||
"@wordpress/i18n": "3.1.0",
|
||||
"lodash": "4.17.11",
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
"module": "build-module/index.js",
|
||||
"react-native": "src/index",
|
||||
"dependencies": {
|
||||
"@babel/runtime-corejs2": "7.4.3",
|
||||
"@babel/runtime-corejs2": "7.4.4",
|
||||
"history": "4.9.0",
|
||||
"lodash": "4.17.11",
|
||||
"qs": "6.7.0"
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
"module": "build-module/index.js",
|
||||
"react-native": "src/index",
|
||||
"dependencies": {
|
||||
"@babel/runtime-corejs2": "7.4.3",
|
||||
"@babel/runtime-corejs2": "7.4.4",
|
||||
"locutus": "2.0.10",
|
||||
"lodash": "4.17.11"
|
||||
},
|
||||
|
|
|
@ -72,13 +72,13 @@ class WC_Tests_API_Init extends WC_REST_Unit_Test_Case {
|
|||
add_filter( 'query', array( $this, 'filter_order_query' ) );
|
||||
|
||||
// Initiate sync.
|
||||
WC_Admin_Reports_Sync::orders_lookup_process_order( $order->get_id() );
|
||||
WC_Admin_Reports_Sync::orders_lookup_import_order( $order->get_id() );
|
||||
|
||||
// Verify that a retry job was scheduled.
|
||||
$this->assertCount( 1, $this->queue->actions );
|
||||
$this->assertArraySubset(
|
||||
array(
|
||||
'hook' => WC_Admin_Reports_Sync::SINGLE_ORDER_ACTION,
|
||||
'hook' => WC_Admin_Reports_Sync::SINGLE_ORDER_IMPORT_ACTION,
|
||||
'args' => array( $order->get_id() ),
|
||||
),
|
||||
$this->queue->actions[0]
|
||||
|
|
|
@ -46,7 +46,7 @@ class WC_Tests_API_Orders extends WC_REST_Unit_Test_Case {
|
|||
$request = new WP_REST_Request( 'GET', $this->endpoint );
|
||||
$request->set_query_params(
|
||||
array(
|
||||
'number' => $order->get_id(),
|
||||
'number' => (string) $order->get_id(),
|
||||
)
|
||||
);
|
||||
|
||||
|
|
|
@ -0,0 +1,288 @@
|
|||
<?php
|
||||
/**
|
||||
* Reports Import REST API Test
|
||||
*
|
||||
* @package WooCommerce\Tests\API
|
||||
*/
|
||||
|
||||
/**
|
||||
* Reports Import REST API Test Class
|
||||
*
|
||||
* @package WooCommerce\Tests\API
|
||||
*/
|
||||
class WC_Tests_API_Reports_Import extends WC_REST_Unit_Test_Case {
|
||||
/**
|
||||
* Endpoint.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $endpoint = '/wc/v4/reports/import';
|
||||
|
||||
/**
|
||||
* Setup test reports products data.
|
||||
*/
|
||||
public function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
$this->user = $this->factory->user->create(
|
||||
array(
|
||||
'role' => 'administrator',
|
||||
)
|
||||
);
|
||||
|
||||
$this->customer = $this->factory->user->create(
|
||||
array(
|
||||
'first_name' => 'Steve',
|
||||
'last_name' => 'User',
|
||||
'role' => 'customer',
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test route registration.
|
||||
*/
|
||||
public function test_register_routes() {
|
||||
$routes = $this->server->get_routes();
|
||||
|
||||
$this->assertArrayHasKey( $this->endpoint, $routes );
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts the report item schema is correct.
|
||||
*
|
||||
* @param array $schema Item to check schema.
|
||||
*/
|
||||
public function assert_report_item_schema( $schema ) {
|
||||
$this->assertArrayHasKey( 'status', $schema );
|
||||
$this->assertArrayHasKey( 'message', $schema );
|
||||
}
|
||||
|
||||
/**
|
||||
* Test reports schema.
|
||||
*/
|
||||
public function test_reports_schema() {
|
||||
wp_set_current_user( $this->user );
|
||||
|
||||
$request = new WP_REST_Request( 'OPTIONS', $this->endpoint );
|
||||
$response = $this->server->dispatch( $request );
|
||||
$data = $response->get_data();
|
||||
$properties = $data['schema']['properties'];
|
||||
|
||||
$this->assertCount( 2, $properties );
|
||||
$this->assert_report_item_schema( $properties );
|
||||
}
|
||||
|
||||
/**
|
||||
* Test getting reports without valid permissions.
|
||||
*/
|
||||
public function test_get_reports_without_permission() {
|
||||
wp_set_current_user( 0 );
|
||||
$response = $this->server->dispatch( new WP_REST_Request( 'POST', $this->endpoint ) );
|
||||
$this->assertEquals( 401, $response->get_status() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the import paramaters.
|
||||
*/
|
||||
public function test_import_params() {
|
||||
global $wpdb;
|
||||
wp_set_current_user( $this->user );
|
||||
|
||||
// Populate all of the data.
|
||||
$product = new WC_Product_Simple();
|
||||
$product->set_name( 'Test Product' );
|
||||
$product->set_regular_price( 25 );
|
||||
$product->save();
|
||||
|
||||
$order_1 = WC_Helper_Order::create_order( 1, $product );
|
||||
$order_1->set_status( 'completed' );
|
||||
$order_1->set_date_created( time() - ( 3 * DAY_IN_SECONDS ) );
|
||||
$order_1->save();
|
||||
$order_2 = WC_Helper_Order::create_order( 1, $product );
|
||||
$order_2->set_total( 100 );
|
||||
$order_2->set_status( 'completed' );
|
||||
$order_2->save();
|
||||
|
||||
// Delete order stats so we can test import API.
|
||||
$wpdb->query( "DELETE FROM {$wpdb->posts} WHERE post_type = 'scheduled-action'" );
|
||||
$wpdb->query( "DELETE FROM {$wpdb->prefix}wc_order_stats" );
|
||||
|
||||
// Use the days param to only process orders in the last day.
|
||||
$request = new WP_REST_Request( 'POST', $this->endpoint );
|
||||
$request->set_query_params( array( 'days' => '1' ) );
|
||||
$response = $this->server->dispatch( $request );
|
||||
$report = $response->get_data();
|
||||
|
||||
$this->assertEquals( 200, $response->get_status() );
|
||||
$this->assertEquals( 'success', $report['status'] );
|
||||
|
||||
// Run pending thrice to process batch and order.
|
||||
WC_Helper_Queue::run_all_pending();
|
||||
WC_Helper_Queue::run_all_pending();
|
||||
WC_Helper_Queue::run_all_pending();
|
||||
|
||||
$request = new WP_REST_Request( 'GET', '/wc/v4/reports/customers' );
|
||||
$response = $this->server->dispatch( $request );
|
||||
$reports = $response->get_data();
|
||||
|
||||
$this->assertEquals( 200, $response->get_status() );
|
||||
$this->assertCount( 2, $reports );
|
||||
$this->assertEquals( $this->customer, $reports[0]['user_id'] );
|
||||
|
||||
$request = new WP_REST_Request( 'GET', '/wc/v4/reports/orders' );
|
||||
$response = $this->server->dispatch( $request );
|
||||
$reports = $response->get_data();
|
||||
|
||||
$this->assertEquals( 200, $response->get_status() );
|
||||
$this->assertCount( 1, $reports );
|
||||
$this->assertEquals( $order_2->get_id(), $reports[0]['order_id'] );
|
||||
|
||||
// Use the skip existing params to skip processing customers/orders.
|
||||
// Compare against order status to make sure previously imported order was skipped.
|
||||
$order_2->set_status( 'processing' );
|
||||
$order_2->save();
|
||||
|
||||
// Compare against name to make sure previously imported customer was skipped.
|
||||
wp_update_user(
|
||||
array(
|
||||
'ID' => $this->customer,
|
||||
'first_name' => 'Changed',
|
||||
)
|
||||
);
|
||||
|
||||
// Delete scheduled actions to avoid default order processing.
|
||||
$wpdb->query( "DELETE FROM {$wpdb->posts} WHERE post_type = 'scheduled-action'" );
|
||||
|
||||
$request = new WP_REST_Request( 'POST', $this->endpoint );
|
||||
$request->set_query_params( array( 'skip_existing' => '1' ) );
|
||||
$response = $this->server->dispatch( $request );
|
||||
$report = $response->get_data();
|
||||
|
||||
$this->assertEquals( 200, $response->get_status() );
|
||||
$this->assertEquals( 'success', $report['status'] );
|
||||
|
||||
// Run pending thrice to process batch and order.
|
||||
WC_Helper_Queue::run_all_pending();
|
||||
WC_Helper_Queue::run_all_pending();
|
||||
WC_Helper_Queue::run_all_pending();
|
||||
|
||||
$request = new WP_REST_Request( 'GET', '/wc/v4/reports/customers' );
|
||||
$response = $this->server->dispatch( $request );
|
||||
$reports = $response->get_data();
|
||||
|
||||
$this->assertEquals( 200, $response->get_status() );
|
||||
$this->assertCount( 2, $reports );
|
||||
$this->assertEquals( 'Steve User', $reports[0]['name'] );
|
||||
|
||||
$request = new WP_REST_Request( 'GET', '/wc/v4/reports/orders' );
|
||||
$request->set_query_params( array( 'per_page' => 5 ) );
|
||||
$response = $this->server->dispatch( $request );
|
||||
$reports = $response->get_data();
|
||||
|
||||
$this->assertEquals( 200, $response->get_status() );
|
||||
$this->assertCount( 2, $reports );
|
||||
$this->assertEquals( 'completed', $reports[0]['status'] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Test cancelling import actions.
|
||||
*/
|
||||
public function test_cancel_import() {
|
||||
wp_set_current_user( $this->user );
|
||||
|
||||
// Populate all of the data.
|
||||
$product = new WC_Product_Simple();
|
||||
$product->set_name( 'Test Product' );
|
||||
$product->set_regular_price( 25 );
|
||||
$product->save();
|
||||
|
||||
$order = WC_Helper_Order::create_order( 1, $product );
|
||||
$order->set_status( 'completed' );
|
||||
$order->set_date_created( time() - ( 3 * DAY_IN_SECONDS ) );
|
||||
$order->save();
|
||||
|
||||
// Verify there are actions to cancel.
|
||||
$pending_actions = WC_Helper_Queue::get_all_pending();
|
||||
$this->assertCount( 1, $pending_actions );
|
||||
|
||||
// Cancel outstanding actions.
|
||||
$request = new WP_REST_Request( 'POST', $this->endpoint . '/cancel' );
|
||||
$response = $this->server->dispatch( $request );
|
||||
$report = $response->get_data();
|
||||
|
||||
$this->assertEquals( 200, $response->get_status() );
|
||||
$this->assertEquals( 'success', $report['status'] );
|
||||
|
||||
// Verify there are no pending actions.
|
||||
$pending_actions = WC_Helper_Queue::get_all_pending();
|
||||
$this->assertCount( 0, $pending_actions );
|
||||
}
|
||||
|
||||
/**
|
||||
* Test import deletion.
|
||||
*/
|
||||
public function test_delete_stats() {
|
||||
global $wpdb;
|
||||
wp_set_current_user( $this->user );
|
||||
|
||||
// Populate all of the data.
|
||||
$product = new WC_Product_Simple();
|
||||
$product->set_name( 'Test Product' );
|
||||
$product->set_regular_price( 25 );
|
||||
$product->save();
|
||||
|
||||
for ( $i = 0; $i < 25; $i++ ) {
|
||||
$order = WC_Helper_Order::create_order( 1, $product );
|
||||
$order->set_status( 'completed' );
|
||||
$order->save();
|
||||
}
|
||||
|
||||
// Check that stats exist before deleting.
|
||||
WC_Helper_Queue::run_all_pending();
|
||||
|
||||
$request = new WP_REST_Request( 'GET', '/wc/v4/reports/orders' );
|
||||
$request->set_query_params( array( 'per_page' => 25 ) );
|
||||
$response = $this->server->dispatch( $request );
|
||||
$reports = $response->get_data();
|
||||
|
||||
$this->assertEquals( 200, $response->get_status() );
|
||||
$this->assertCount( 25, $reports );
|
||||
|
||||
$request = new WP_REST_Request( 'GET', '/wc/v4/reports/customers' );
|
||||
$request->set_query_params( array( 'per_page' => 25 ) );
|
||||
$response = $this->server->dispatch( $request );
|
||||
$reports = $response->get_data();
|
||||
|
||||
$this->assertEquals( 200, $response->get_status() );
|
||||
$this->assertCount( 1, $reports );
|
||||
|
||||
// Delete all stats.
|
||||
$request = new WP_REST_Request( 'POST', $this->endpoint . '/delete' );
|
||||
$response = $this->server->dispatch( $request );
|
||||
$report = $response->get_data();
|
||||
|
||||
$this->assertEquals( 200, $response->get_status() );
|
||||
$this->assertEquals( 'success', $report['status'] );
|
||||
|
||||
// Run pending three times to process batches and dependent actions.
|
||||
WC_Helper_Queue::run_all_pending();
|
||||
WC_Helper_Queue::run_all_pending();
|
||||
WC_Helper_Queue::run_all_pending();
|
||||
|
||||
// Check that stats have been deleted.
|
||||
$request = new WP_REST_Request( 'GET', '/wc/v4/reports/orders' );
|
||||
$response = $this->server->dispatch( $request );
|
||||
$reports = $response->get_data();
|
||||
|
||||
$this->assertEquals( 200, $response->get_status() );
|
||||
$this->assertCount( 0, $reports );
|
||||
|
||||
$request = new WP_REST_Request( 'GET', '/wc/v4/reports/customers' );
|
||||
$response = $this->server->dispatch( $request );
|
||||
$reports = $response->get_data();
|
||||
|
||||
$this->assertEquals( 200, $response->get_status() );
|
||||
$this->assertCount( 0, $reports );
|
||||
}
|
||||
}
|
|
@ -45,7 +45,7 @@ class WC_Tests_Reports_Regenerate_Batching extends WC_REST_Unit_Test_Case {
|
|||
switch ( $action ) {
|
||||
case WC_Admin_Reports_Sync::QUEUE_BATCH_ACTION:
|
||||
return $this->queue_batch_size;
|
||||
case WC_Admin_Reports_Sync::CUSTOMERS_BATCH_ACTION:
|
||||
case WC_Admin_Reports_Sync::CUSTOMERS_IMPORT_BATCH_ACTION:
|
||||
return $this->customers_batch_size;
|
||||
case WC_Admin_Reports_Sync::ORDERS_BATCH_ACTION:
|
||||
return $this->orders_batch_size;
|
||||
|
@ -81,20 +81,20 @@ class WC_Tests_Reports_Regenerate_Batching extends WC_REST_Unit_Test_Case {
|
|||
$num_customers = 1234; // 1234 / 5 = 247 batches
|
||||
$num_batches = ceil( $num_customers / $this->customers_batch_size );
|
||||
|
||||
WC_Admin_Reports_Sync::queue_batches( 1, $num_batches, WC_Admin_Reports_Sync::CUSTOMERS_BATCH_ACTION );
|
||||
WC_Admin_Reports_Sync::queue_batches( 1, $num_batches, WC_Admin_Reports_Sync::CUSTOMERS_IMPORT_BATCH_ACTION );
|
||||
|
||||
$this->assertCount( $this->queue_batch_size, $this->queue->actions );
|
||||
$this->assertArraySubset(
|
||||
array(
|
||||
'hook' => WC_Admin_Reports_Sync::QUEUE_BATCH_ACTION,
|
||||
'args' => array( 1, 25, WC_Admin_Reports_Sync::CUSTOMERS_BATCH_ACTION ),
|
||||
'args' => array( 1, 25, WC_Admin_Reports_Sync::CUSTOMERS_IMPORT_BATCH_ACTION ),
|
||||
),
|
||||
$this->queue->actions[0]
|
||||
);
|
||||
$this->assertArraySubset(
|
||||
array(
|
||||
'hook' => WC_Admin_Reports_Sync::QUEUE_BATCH_ACTION,
|
||||
'args' => array( 226, 247, WC_Admin_Reports_Sync::CUSTOMERS_BATCH_ACTION ),
|
||||
'args' => array( 226, 247, WC_Admin_Reports_Sync::CUSTOMERS_IMPORT_BATCH_ACTION ),
|
||||
),
|
||||
$this->queue->actions[ $this->queue_batch_size - 1 ]
|
||||
);
|
||||
|
@ -107,19 +107,19 @@ class WC_Tests_Reports_Regenerate_Batching extends WC_REST_Unit_Test_Case {
|
|||
$num_customers = 45; // 45 / 5 = 9 batches (which is less than the batch queue size)
|
||||
$num_batches = ceil( $num_customers / $this->customers_batch_size );
|
||||
|
||||
WC_Admin_Reports_Sync::queue_batches( 1, $num_batches, WC_Admin_Reports_Sync::CUSTOMERS_BATCH_ACTION );
|
||||
WC_Admin_Reports_Sync::queue_batches( 1, $num_batches, WC_Admin_Reports_Sync::CUSTOMERS_IMPORT_BATCH_ACTION );
|
||||
|
||||
$this->assertCount( 9, $this->queue->actions );
|
||||
$this->assertArraySubset(
|
||||
array(
|
||||
'hook' => WC_Admin_Reports_Sync::CUSTOMERS_BATCH_ACTION,
|
||||
'hook' => WC_Admin_Reports_Sync::CUSTOMERS_IMPORT_BATCH_ACTION,
|
||||
'args' => array( 1 ),
|
||||
),
|
||||
$this->queue->actions[0]
|
||||
);
|
||||
$this->assertArraySubset(
|
||||
array(
|
||||
'hook' => WC_Admin_Reports_Sync::CUSTOMERS_BATCH_ACTION,
|
||||
'hook' => WC_Admin_Reports_Sync::CUSTOMERS_IMPORT_BATCH_ACTION,
|
||||
'args' => array( 9 ),
|
||||
),
|
||||
$this->queue->actions[8]
|
||||
|
|
|
@ -12,11 +12,11 @@
|
|||
*/
|
||||
class WC_Helper_Queue {
|
||||
/**
|
||||
* Run all pending queued actions.
|
||||
* Get all pending queued actions.
|
||||
*
|
||||
* @return void
|
||||
* @return array Pending jobs.
|
||||
*/
|
||||
public static function run_all_pending() {
|
||||
public static function get_all_pending() {
|
||||
$jobs = WC()->queue()->search(
|
||||
array(
|
||||
'per_page' => -1,
|
||||
|
@ -25,6 +25,17 @@ class WC_Helper_Queue {
|
|||
)
|
||||
);
|
||||
|
||||
return $jobs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run all pending queued actions.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function run_all_pending() {
|
||||
$jobs = self::get_all_pending();
|
||||
|
||||
foreach ( $jobs as $job ) {
|
||||
$job->execute();
|
||||
}
|
||||
|
|
|
@ -37,13 +37,13 @@ class WC_Tests_Reports_Queue_Prioritization extends WC_REST_Unit_Test_Case {
|
|||
* Test that we're setting a priority on our actions.
|
||||
*/
|
||||
public function test_queue_action_sets_priority() {
|
||||
WC_Admin_Reports_Sync::queue()->schedule_single( time(), WC_Admin_Reports_Sync::SINGLE_ORDER_ACTION );
|
||||
WC_Admin_Reports_Sync::queue()->schedule_single( time(), WC_Admin_Reports_Sync::SINGLE_ORDER_IMPORT_ACTION );
|
||||
|
||||
$actions = WC_Admin_Reports_Sync::queue()->search(
|
||||
array(
|
||||
'status' => 'pending',
|
||||
'claimed' => false,
|
||||
'hook' => WC_Admin_Reports_Sync::SINGLE_ORDER_ACTION,
|
||||
'hook' => WC_Admin_Reports_Sync::SINGLE_ORDER_IMPORT_ACTION,
|
||||
)
|
||||
);
|
||||
|
||||
|
@ -55,7 +55,7 @@ class WC_Tests_Reports_Queue_Prioritization extends WC_REST_Unit_Test_Case {
|
|||
|
||||
$this->assertEquals( WC_Admin_ActionScheduler_wpPostStore::JOB_PRIORITY, $action->menu_order );
|
||||
|
||||
WC_Admin_Reports_Sync::queue()->cancel_all( WC_Admin_Reports_Sync::SINGLE_ORDER_ACTION );
|
||||
WC_Admin_Reports_Sync::queue()->cancel_all( WC_Admin_Reports_Sync::SINGLE_ORDER_IMPORT_ACTION );
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue