Merge branch 'master' into fix/1012-2

This commit is contained in:
Peter Fabian 2019-02-01 13:32:35 +01:00
commit c2bcad2e50
150 changed files with 4298 additions and 4695 deletions

View File

@ -168,8 +168,12 @@ install_deps() {
php wp-cli.phar core config --dbname=$DB_NAME --dbuser=$DB_USER --dbpass=$DB_PASS --dbhost=$DB_HOST --dbprefix=wptests_
php wp-cli.phar core install --url="$WP_SITE_URL" --title="Example" --admin_user=admin --admin_password=password --admin_email=info@example.com --path=$WP_CORE_DIR --skip-email
# Install Gutenberg
php wp-cli.phar plugin install gutenberg --activate
# Install Gutenberg if WP < 5
if [[ $WP_VERSION =~ ^([0-9]+)[0-9\.]+\-? ]]; then
if [ "5" -gt "${BASH_REMATCH[1]}" ]; then
php wp-cli.phar plugin install gutenberg --activate
fi
fi
# Install WooCommerce
cd "wp-content/plugins/"

View File

@ -17,7 +17,7 @@ import { Card, EmptyTable, TableCard } from '@woocommerce/components';
* Internal dependencies
*/
import ReportError from 'analytics/components/report-error';
import { getReportTableData } from 'store/reports/utils';
import { getReportTableData } from 'wc-api/reports/utils';
import withSelect from 'wc-api/with-select';
import './style.scss';

View File

@ -12,13 +12,13 @@ import { createRegistry, RegistryProvider } from '@wordpress/data';
* WooCommerce dependencies
*/
import { formatCurrency, getCurrencyFormatDecimal } from '@woocommerce/currency';
import { numberFormat } from '@woocommerce/number';
/**
* Internal dependencies
*/
import LeaderboardWithSelect, { Leaderboard } from '../';
import { NAMESPACE } from 'store/constants';
import { numberFormat } from 'lib/number';
import { NAMESPACE } from 'wc-api/constants';
import mockData from '../__mocks__/top-selling-products-mock-data';
// Mock <Table> to avoid tests failing due to it using DOM properties that

View File

@ -25,7 +25,7 @@ import { Chart } from '@woocommerce/components';
/**
* Internal dependencies
*/
import { getReportChartData, getTooltipValueFormat } from 'store/reports/utils';
import { getReportChartData, getTooltipValueFormat } from 'wc-api/reports/utils';
import ReportError from 'analytics/components/report-error';
import withSelect from 'wc-api/with-select';

View File

@ -13,13 +13,14 @@ import PropTypes from 'prop-types';
import { getDateParamsFromQuery } from '@woocommerce/date';
import { getNewPath } from '@woocommerce/navigation';
import { SummaryList, SummaryListPlaceholder, SummaryNumber } from '@woocommerce/components';
import { calculateDelta, formatValue } from '@woocommerce/number';
import { formatCurrency } from '@woocommerce/currency';
/**
* Internal dependencies
*/
import { getSummaryNumbers } from 'store/reports/utils';
import { getSummaryNumbers } from 'wc-api/reports/utils';
import ReportError from 'analytics/components/report-error';
import { calculateDelta, formatValue } from 'lib/number';
import withSelect from 'wc-api/with-select';
/**
@ -45,11 +46,16 @@ export class ReportSummary extends Component {
const renderSummaryNumbers = ( { onToggle } ) =>
charts.map( chart => {
const { key, label, type } = chart;
const isCurrency = 'currency' === type;
const delta = calculateDelta( primaryTotals[ key ], secondaryTotals[ key ] );
const href = getNewPath( { chart: key } );
const prevValue = formatValue( type, secondaryTotals[ key ] );
const prevValue = isCurrency
? formatCurrency( secondaryTotals[ key ] )
: formatValue( type, secondaryTotals[ key ] );
const isSelected = selectedChart.key === key;
const value = formatValue( type, primaryTotals[ key ] );
const value = isCurrency
? formatCurrency( primaryTotals[ key ] )
: formatValue( type, primaryTotals[ key ] );
return (
<SummaryNumber

View File

@ -19,7 +19,7 @@ import { onQueryChange } from '@woocommerce/navigation';
* Internal dependencies
*/
import ReportError from 'analytics/components/report-error';
import { getReportChartData, getReportTableData } from 'store/reports/utils';
import { getReportChartData, getReportTableData } from 'wc-api/reports/utils';
import withSelect from 'wc-api/with-select';
import { extendTableData } from './utils';

View File

@ -18,7 +18,7 @@ export default class CategoryBreadcrumbs extends Component {
let parent = category.parent;
while ( parent ) {
ancestors.unshift( parent );
parent = categories[ parent ].parent;
parent = categories.get( parent ).parent;
}
return ancestors;
}
@ -30,20 +30,20 @@ export default class CategoryBreadcrumbs extends Component {
return;
}
if ( ancestorIds.length === 1 ) {
return categories[ first( ancestorIds ) ].name + ' ';
return categories.get( first( ancestorIds ) ).name + ' ';
}
if ( ancestorIds.length === 2 ) {
return (
categories[ first( ancestorIds ) ].name +
categories.get( first( ancestorIds ) ).name +
' ' +
categories[ last( ancestorIds ) ].name +
categories.get( last( ancestorIds ) ).name +
' '
);
}
return (
categories[ first( ancestorIds ) ].name +
categories.get( first( ancestorIds ) ).name +
' … ' +
categories[ last( ancestorIds ) ].name +
categories.get( last( ancestorIds ) ).name +
' '
);
}

View File

@ -13,12 +13,12 @@ import { map } from 'lodash';
import { formatCurrency, getCurrencyFormatDecimal } from '@woocommerce/currency';
import { getNewPath, getPersistedQuery } from '@woocommerce/navigation';
import { Link } from '@woocommerce/components';
import { numberFormat } from '@woocommerce/number';
/**
* Internal dependencies
*/
import CategoryBreacrumbs from './breadcrumbs';
import { numberFormat } from 'lib/number';
import ReportTable from 'analytics/components/report-table';
import withSelect from 'wc-api/with-select';
@ -68,11 +68,10 @@ class CategoriesReportTable extends Component {
}
getRowsContent( categoryStats ) {
const { query } = this.props;
return map( categoryStats, categoryStat => {
const { category_id, items_sold, net_revenue, products_count, orders_count } = categoryStat;
const { categories, query } = this.props;
const category = categories[ category_id ];
const category = categories.get( category_id );
const persistedQuery = getPersistedQuery( query );
return [
@ -154,6 +153,7 @@ class CategoriesReportTable extends Component {
getSummary={ this.getSummary }
itemIdField="category_id"
query={ query }
searchBy="categories"
labels={ labels }
tableQuery={ {
orderby: query.orderby || 'items_sold',
@ -169,14 +169,14 @@ class CategoriesReportTable extends Component {
export default compose(
withSelect( select => {
const { getCategories, getCategoriesError, isGetCategoriesRequesting } = select( 'wc-api' );
const { getItems, getItemsError, isGetItemsRequesting } = select( 'wc-api' );
const tableQuery = {
per_page: -1,
};
const categories = getCategories( tableQuery );
const isError = Boolean( getCategoriesError( tableQuery ) );
const isRequesting = isGetCategoriesRequesting( tableQuery );
const categories = getItems( 'categories', tableQuery );
const isError = Boolean( getItemsError( 'categories', tableQuery ) );
const isRequesting = isGetItemsRequesting( 'categories', tableQuery );
return { categories, isError, isRequesting };
} )

View File

@ -12,12 +12,13 @@ import { map } from 'lodash';
import { Date, Link } from '@woocommerce/components';
import { defaultTableDateFormat } from '@woocommerce/date';
import { formatCurrency, getCurrencyFormatDecimal } from '@woocommerce/currency';
import { getNewPath, getPersistedQuery } from '@woocommerce/navigation';
import { numberFormat } from '@woocommerce/number';
/**
* Internal dependencies
*/
import ReportTable from 'analytics/components/report-table';
import { numberFormat } from 'lib/number';
export default class CouponsReportTable extends Component {
constructor() {
@ -67,22 +68,29 @@ export default class CouponsReportTable extends Component {
}
getRowsContent( coupons ) {
const { query } = this.props;
const persistedQuery = getPersistedQuery( query );
return map( coupons, coupon => {
const { amount, coupon_id, extended_info, orders_count } = coupon;
const { code, date_created, date_expires, discount_type } = extended_info;
// @TODO must link to the coupon detail report
const couponUrl = getNewPath( persistedQuery, '/analytics/coupons', {
filter: 'single_coupon',
coupons: coupon_id,
} );
const couponLink = (
<Link href="" type="wc-admin">
<Link href={ couponUrl } type="wc-admin">
{ code }
</Link>
);
const ordersUrl = getNewPath( persistedQuery, '/analytics/orders', {
filter: 'advanced',
coupon_includes: coupon_id,
} );
const ordersLink = (
<Link
href={ '/analytics/orders?filter=advanced&code_includes=' + coupon_id }
type="wc-admin"
>
<Link href={ ordersUrl } type="wc-admin">
{ numberFormat( orders_count ) }
</Link>
);
@ -161,9 +169,10 @@ export default class CouponsReportTable extends Component {
getSummary={ this.getSummary }
itemIdField="coupon_id"
query={ query }
searchBy="coupons"
tableQuery={ {
orderby: query.orderby || 'coupon_id',
order: query.order || 'asc',
orderby: query.orderby || 'orders_count',
order: query.order || 'desc',
extended_info: true,
} }
title={ __( 'Coupons', 'wc-admin' ) }

View File

@ -9,7 +9,7 @@ import { decodeEntities } from '@wordpress/html-entities';
* Internal dependencies
*/
import { getCustomerLabels, getRequestByIdString } from 'lib/async-requests';
import { NAMESPACE } from 'store/constants';
import { NAMESPACE } from 'wc-api/constants';
export const filters = [
{
@ -163,7 +163,7 @@ export const advancedFilters = {
} ) ),
},
},
order_count: {
orders_count: {
labels: {
add: __( 'No. of Orders', 'wc-admin' ),
remove: __( 'Remove order filter', 'wc-admin' ),

View File

@ -20,7 +20,7 @@ export default class CustomersReport extends Component {
render() {
const { query, path } = this.props;
const tableQuery = {
orderby: 'date_registered',
orderby: 'date_last_active',
order: 'desc',
...query,
};

View File

@ -12,12 +12,12 @@ import { Tooltip } from '@wordpress/components';
import { defaultTableDateFormat } from '@woocommerce/date';
import { formatCurrency, getCurrencyFormatDecimal } from '@woocommerce/currency';
import { Date, Link } from '@woocommerce/components';
import { numberFormat } from '@woocommerce/number';
/**
* Internal dependencies
*/
import ReportTable from 'analytics/components/report-table';
import { numberFormat } from 'lib/number';
export default class CustomersReportTable extends Component {
constructor() {
@ -25,6 +25,7 @@ export default class CustomersReportTable extends Component {
this.getHeadersContent = this.getHeadersContent.bind( this );
this.getRowsContent = this.getRowsContent.bind( this );
this.getSummary = this.getSummary.bind( this );
}
getHeadersContent() {
@ -41,10 +42,15 @@ export default class CustomersReportTable extends Component {
key: 'username',
hiddenByDefault: true,
},
{
label: __( 'Last Active', 'wc-admin' ),
key: 'date_last_active',
defaultSort: true,
isSortable: true,
},
{
label: __( 'Sign Up', 'wc-admin' ),
key: 'date_registered',
defaultSort: true,
isSortable: true,
},
{
@ -58,7 +64,7 @@ export default class CustomersReportTable extends Component {
isNumeric: true,
},
{
label: __( 'Lifetime Spend', 'wc-admin' ),
label: __( 'Total Spend', 'wc-admin' ),
key: 'total_spend',
isSortable: true,
isNumeric: true,
@ -69,11 +75,6 @@ export default class CustomersReportTable extends Component {
key: 'avg_order_value',
isNumeric: true,
},
{
label: __( 'Last Active', 'wc-admin' ),
key: 'date_last_active',
isSortable: true,
},
{
label: __( 'Country', 'wc-admin' ),
key: 'country',
@ -147,6 +148,12 @@ export default class CustomersReportTable extends Component {
display: username,
value: username,
},
{
display: date_last_active && (
<Date date={ date_last_active } visibleFormat={ defaultTableDateFormat } />
),
value: date_last_active,
},
{
display: dateRegistered,
value: date_registered,
@ -167,10 +174,6 @@ export default class CustomersReportTable extends Component {
display: formatCurrency( avg_order_value ),
value: getCurrencyFormatDecimal( avg_order_value ),
},
{
display: <Date date={ date_last_active } visibleFormat={ defaultTableDateFormat } />,
value: date_last_active,
},
{
display: countryDisplay,
value: country,
@ -187,6 +190,30 @@ export default class CustomersReportTable extends Component {
} );
}
getSummary( totals ) {
if ( ! totals ) {
return [];
}
return [
{
label: __( 'customers', 'wc-admin' ),
value: numberFormat( totals.customers_count ),
},
{
label: __( 'average orders', 'wc-admin' ),
value: numberFormat( totals.avg_orders_count ),
},
{
label: __( 'average lifetime spend', 'wc-admin' ),
value: formatCurrency( totals.avg_total_spend ),
},
{
label: __( 'average order value', 'wc-admin' ),
value: formatCurrency( totals.avg_avg_order_value ),
},
];
}
render() {
const { query } = this.props;
@ -195,11 +222,11 @@ export default class CustomersReportTable extends Component {
endpoint="customers"
getHeadersContent={ this.getHeadersContent }
getRowsContent={ this.getRowsContent }
getSummary={ this.getSummary }
itemIdField="id"
query={ query }
labels={ { placeholder: __( 'Search by customer name', 'wc-admin' ) } }
searchBy="customers"
searchParam="name_includes"
title={ __( 'Customers', 'wc-admin' ) }
columnPrefsKey="customers_report_columns"
/>

View File

@ -13,12 +13,12 @@ import moment from 'moment';
import { defaultTableDateFormat, getCurrentDates } from '@woocommerce/date';
import { Date, Link } from '@woocommerce/components';
import { getNewPath, getPersistedQuery } from '@woocommerce/navigation';
import { numberFormat } from '@woocommerce/number';
/**
* Internal dependencies
*/
import ReportTable from 'analytics/components/report-table';
import { numberFormat } from 'lib/number';
export default class CouponsReportTable extends Component {
constructor() {

View File

@ -5,6 +5,7 @@
import { __ } from '@wordpress/i18n';
import { applyFilters } from '@wordpress/hooks';
import { Component, Fragment } from '@wordpress/element';
import { compose } from '@wordpress/compose';
import PropTypes from 'prop-types';
import { find } from 'lodash';
@ -12,6 +13,7 @@ import { find } from 'lodash';
* WooCommerce dependencies
*/
import { useFilters } from '@woocommerce/components';
import { getQuery } from '@woocommerce/navigation';
/**
* Internal dependencies
@ -27,6 +29,8 @@ import TaxesReport from './taxes';
import DownloadsReport from './downloads';
import StockReport from './stock';
import CustomersReport from './customers';
import { searchItemsByString } from 'wc-api/items/utils';
import withSelect from 'wc-api/with-select';
const REPORTS_FILTER = 'woocommerce-reports-list';
@ -131,4 +135,27 @@ Report.propTypes = {
params: PropTypes.object.isRequired,
};
export default useFilters( REPORTS_FILTER )( Report );
export default compose(
useFilters( REPORTS_FILTER ),
withSelect( ( select, props ) => {
const { search } = getQuery();
if ( ! search ) {
return {};
}
const { report } = props.params;
const items = searchItemsByString( select, report, search );
const ids = Object.keys( items );
if ( ! ids.length ) {
return {}; // @TODO if no results were found, we should avoid making a server request.
}
return {
query: {
...props.query,
[ report ]: ids.join( ',' ),
},
};
} )
)( Report );

View File

@ -12,11 +12,11 @@ import { map } from 'lodash';
import { Date, Link, OrderStatus, ViewMoreList } from '@woocommerce/components';
import { defaultTableDateFormat } from '@woocommerce/date';
import { formatCurrency } from '@woocommerce/currency';
import { numberFormat } from '@woocommerce/number';
/**
* Internal dependencies
*/
import { numberFormat } from 'lib/number';
import ReportTable from 'analytics/components/report-table';
import { getNewPath, getPersistedQuery } from '@woocommerce/navigation';
import './style.scss';

View File

@ -24,7 +24,7 @@ import VariationsReportTable from './table-variations';
export default class ProductsReport extends Component {
render() {
const { path, query } = this.props;
const isProductDetailsView = query.products && 1 === query.products.split( ',' ).length;
const isProductDetailsView = query.filter === 'single_product';
const itemsLabel = isProductDetailsView
? __( '%s variations', 'wc-admin' )

View File

@ -12,12 +12,12 @@ import { map, get } from 'lodash';
import { Link } from '@woocommerce/components';
import { formatCurrency, getCurrencyFormatDecimal } from '@woocommerce/currency';
import { getNewPath, getPersistedQuery } from '@woocommerce/navigation';
import { numberFormat } from '@woocommerce/number';
/**
* Internal dependencies
*/
import ReportTable from 'analytics/components/report-table';
import { numberFormat } from 'lib/number';
import { isLowStock } from './utils';
export default class VariationsReportTable extends Component {
@ -36,6 +36,12 @@ export default class VariationsReportTable extends Component {
required: true,
isLeftAligned: true,
},
{
label: __( 'SKU', 'wc-admin' ),
key: 'sku',
hiddenByDefault: true,
isSortable: true,
},
{
label: __( 'Items Sold', 'wc-admin' ),
key: 'items_sold',
@ -77,7 +83,7 @@ export default class VariationsReportTable extends Component {
return map( data, row => {
const { items_sold, net_revenue, orders_count, extended_info, product_id } = row;
const { stock_status, stock_quantity, low_stock_amount } = extended_info;
const { stock_status, stock_quantity, low_stock_amount, sku } = extended_info;
const name = get( row, [ 'extended_info', 'name' ], '' ).replace( ' - ', ' / ' );
const ordersLink = getNewPath( persistedQuery, 'orders', {
filter: 'advanced',
@ -94,6 +100,10 @@ export default class VariationsReportTable extends Component {
),
value: name,
},
{
display: sku,
value: sku,
},
{
display: items_sold,
value: items_sold,
@ -172,6 +182,7 @@ export default class VariationsReportTable extends Component {
labels={ labels }
query={ query }
getSummary={ this.getSummary }
searchBy="variations"
tableQuery={ {
orderby: query.orderby || 'items_sold',
order: query.order || 'desc',

View File

@ -13,13 +13,13 @@ import { map } from 'lodash';
import { formatCurrency, getCurrencyFormatDecimal } from '@woocommerce/currency';
import { getNewPath, getPersistedQuery } from '@woocommerce/navigation';
import { Link, Tag } from '@woocommerce/components';
import { numberFormat } from '@woocommerce/number';
/**
* Internal dependencies
*/
import CategoryBreacrumbs from '../categories/breadcrumbs';
import { isLowStock } from './utils';
import { numberFormat } from 'lib/number';
import ReportTable from 'analytics/components/report-table';
import withSelect from 'wc-api/with-select';
import './style.scss';
@ -119,11 +119,11 @@ class ProductsReportTable extends Component {
filter: 'single_product',
products: product_id,
} );
const categories = this.props.categories;
const { categories } = this.props;
const productCategories =
( category_ids &&
category_ids.map( category_id => categories[ category_id ] ).filter( Boolean ) ) ||
category_ids.map( category_id => categories.get( category_id ) ).filter( Boolean ) ) ||
[];
return [
@ -245,6 +245,7 @@ class ProductsReportTable extends Component {
itemIdField="product_id"
labels={ labels }
query={ query }
searchBy="products"
tableQuery={ {
orderby: query.orderby || 'items_sold',
order: query.order || 'desc',
@ -259,14 +260,14 @@ class ProductsReportTable extends Component {
export default compose(
withSelect( select => {
const { getCategories, getCategoriesError, isGetCategoriesRequesting } = select( 'wc-api' );
const { getItems, getItemsError, isGetItemsRequesting } = select( 'wc-api' );
const tableQuery = {
per_page: -1,
};
const categories = getCategories( tableQuery );
const isError = Boolean( getCategoriesError( tableQuery ) );
const isRequesting = isGetCategoriesRequesting( tableQuery );
const categories = getItems( 'categories', tableQuery );
const isError = Boolean( getItemsError( 'categories', tableQuery ) );
const isRequesting = isGetItemsRequesting( 'categories', tableQuery );
return { categories, isError, isRequesting };
} )

View File

@ -14,12 +14,12 @@ import { get } from 'lodash';
import { appendTimestamp, defaultTableDateFormat, getCurrentDates } from '@woocommerce/date';
import { Date, Link } from '@woocommerce/components';
import { formatCurrency, getCurrencyFormatDecimal } from '@woocommerce/currency';
import { numberFormat } from '@woocommerce/number';
/**
* Internal dependencies
*/
import { QUERY_DEFAULTS } from 'store/constants';
import { numberFormat } from 'lib/number';
import { QUERY_DEFAULTS } from 'wc-api/constants';
import ReportTable from 'analytics/components/report-table';
import withSelect from 'wc-api/with-select';

View File

@ -10,12 +10,12 @@ import { Component } from '@wordpress/element';
*/
import { Link } from '@woocommerce/components';
import { getNewPath, getPersistedQuery } from '@woocommerce/navigation';
import { numberFormat } from '@woocommerce/number';
/**
* Internal dependencies
*/
import ReportTable from 'analytics/components/report-table';
import { numberFormat } from 'lib/number';
export default class StockReportTable extends Component {
constructor() {

View File

@ -9,7 +9,7 @@ import { __ } from '@wordpress/i18n';
*/
import { getRequestByIdString } from 'lib/async-requests';
import { getTaxCode } from './utils';
import { NAMESPACE } from 'store/constants';
import { NAMESPACE } from 'wc-api/constants';
export const charts = [
{

View File

@ -12,12 +12,12 @@ import { map } from 'lodash';
import { Link } from '@woocommerce/components';
import { formatCurrency, getCurrencyFormatDecimal } from '@woocommerce/currency';
import { getTaxCode } from './utils';
import { numberFormat } from '@woocommerce/number';
/**
* Internal dependencies
*/
import ReportTable from 'analytics/components/report-table';
import { numberFormat } from 'lib/number';
export default class TaxesReportTable extends Component {
constructor() {
@ -150,6 +150,7 @@ export default class TaxesReportTable extends Component {
getSummary={ this.getSummary }
itemIdField="tax_rate_id"
query={ query }
searchBy="taxes"
tableQuery={ {
orderby: query.orderby || 'tax_rate_id',
} }

View File

@ -0,0 +1,34 @@
Settings
=======
The settings used to modify the way data is retreived or displayed in WooCommerce reports.
## Extending Settings
Settings can be added, removed, or modified outside oc `wc-admin` by hooking into `woocommerce_admin_analytics_settings`. For example:
```js
addFilter( 'woocommerce_admin_analytics_settings', 'wc-example/my-setting', settings => {
return [
...settings,
{
name: 'custom_setting',
label: __( 'Custom setting:', 'wc-admin' ),
inputType: 'text',
helpText: __( 'Help text to describe what the setting does.' ),
initialValue: 'Initial value used',
defaultValue: 'Default value',
},
];
} );
```
Each settings has the following properties:
- `name` (string): The slug of the setting to be updated.
- `label` (string): The label used to describe and displayed next to the setting.
- `inputType` (enum: text|checkbox|checkboxGroup): The type of input to use.
- `helpText` (string): Text displayed beneath the setting.
- `options` (array): Array of options used for inputs with selectable options.
- `initialValue` (string|array): Initial value used when rendering the setting.
- `defaultValue` (string|array): Value used when resetting to default settings.

View File

@ -0,0 +1,68 @@
/** @format */
/**
* External dependencies
*/
import { __, sprintf } from '@wordpress/i18n';
import { applyFilters } from '@wordpress/hooks';
import interpolateComponents from 'interpolate-components';
/**
* WooCommerce dependencies
*/
import { Link } from '@woocommerce/components';
const SETTINGS_FILTER = 'woocommerce_admin_analytics_settings';
const defaultOrderStatuses = [
'completed',
'processing',
'refunded',
'cancelled',
'failed',
'pending',
'on-hold',
];
const orderStatuses = Object.keys( wcSettings.orderStatuses )
.filter( status => status !== 'refunded' )
.map( key => {
return {
value: key,
label: wcSettings.orderStatuses[ key ],
description: sprintf(
__( 'Exclude the %s status from reports', 'wc-admin' ),
wcSettings.orderStatuses[ key ]
),
};
} );
export const analyticsSettings = applyFilters( SETTINGS_FILTER, [
{
name: 'woocommerce_excluded_report_order_statuses',
label: __( 'Excluded Statuses:', 'wc-admin' ),
inputType: 'checkboxGroup',
options: [
{
key: 'defaultStatuses',
options: orderStatuses.filter( status => defaultOrderStatuses.includes( status.value ) ),
},
{
key: 'customStatuses',
label: __( 'Custom Statuses', 'wc-admin' ),
options: orderStatuses.filter( status => ! defaultOrderStatuses.includes( status.value ) ),
},
],
helpText: interpolateComponents( {
mixedString: __(
'Orders with these statuses are excluded from the totals in your reports. ' +
'The {{strong}}Refunded{{/strong}} status can not be excluded. {{moreLink}}Learn more{{/moreLink}}',
'wc-admin'
),
components: {
strong: <strong />,
moreLink: <Link href="#" type="external" />, // @TODO: this needs to be replaced with a real link.
},
} ),
initialValue: wcSettings.wcAdminSettings.woocommerce_excluded_report_order_statuses || [],
defaultValue: [ 'pending', 'cancelled', 'failed' ],
},
] );

View File

@ -0,0 +1,134 @@
/** @format */
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { Button } from '@wordpress/components';
import { Component, Fragment } from '@wordpress/element';
import { compose } from '@wordpress/compose';
import { remove } from 'lodash';
import { withDispatch } from '@wordpress/data';
/**
* WooCommerce dependencies
*/
import { SectionHeader, useFilters } from '@woocommerce/components';
/**
* Internal dependencies
*/
import './index.scss';
import { analyticsSettings } from './config';
import Header from 'header';
import Setting from './setting';
const SETTINGS_FILTER = 'woocommerce_admin_analytics_settings';
class Settings extends Component {
constructor() {
super( ...arguments );
const settings = {};
analyticsSettings.forEach( setting => ( settings[ setting.name ] = setting.initialValue ) );
this.state = {
settings: settings,
};
this.handleInputChange = this.handleInputChange.bind( this );
}
componentDidCatch( error ) {
this.setState( {
hasError: true,
} );
/* eslint-disable no-console */
console.warn( error );
/* eslint-enable no-console */
}
resetDefaults = () => {
if (
window.confirm(
__( 'Are you sure you want to reset all settings to default values?', 'wc-admin' )
)
) {
const settings = {};
analyticsSettings.forEach( setting => ( settings[ setting.name ] = setting.defaultValue ) );
this.setState( { settings }, this.saveChanges );
}
};
saveChanges = () => {
this.props.updateSettings( this.state.settings );
// @TODO: Need a confirmation on successful update.
};
handleInputChange( e ) {
const { checked, name, type, value } = e.target;
const { settings } = this.state;
if ( 'checkbox' === type ) {
if ( checked ) {
settings[ name ].push( value );
} else {
remove( settings[ name ], v => v === value );
}
} else {
settings[ name ] = value;
}
this.setState( { settings } );
}
render() {
const { hasError } = this.state;
if ( hasError ) {
return null;
}
return (
<Fragment>
<Header
sections={ [
[ '/analytics/revenue', __( 'Analytics', 'wc-admin' ) ],
__( 'Settings', 'wc-admin' ),
] }
/>
<SectionHeader title={ __( 'Analytics Settings', 'wc-admin' ) } />
<div className="woocommerce-settings__wrapper">
{ analyticsSettings.map( setting => (
<Setting
handleChange={ this.handleInputChange }
helpText={ setting.helpText }
inputType={ setting.inputType }
key={ setting.name }
label={ setting.label }
name={ setting.name }
options={ setting.options }
value={ this.state.settings[ setting.name ] }
/>
) ) }
<div className="woocommerce-settings__actions">
<Button isDefault onClick={ this.resetDefaults }>
{ __( 'Reset Defaults', 'wc-admin' ) }
</Button>
<Button isPrimary onClick={ this.saveChanges }>
{ __( 'Save Changes', 'wc-admin' ) }
</Button>
</div>
</div>
</Fragment>
);
}
}
export default compose(
withDispatch( dispatch => {
const { updateSettings } = dispatch( 'wc-api' );
return {
updateSettings,
};
} )
)( useFilters( SETTINGS_FILTER )( Settings ) );

View File

@ -0,0 +1,17 @@
/** @format */
.woocommerce-settings__wrapper {
@include breakpoint( '>782px' ) {
padding: 0 ($gap - 3);
}
}
.woocommerce-settings__actions {
@include breakpoint( '>1280px' ) {
margin-left: 15%;
}
button {
margin-right: $gap;
}
}

View File

@ -0,0 +1,141 @@
/** @format */
/**
* External dependencies
*/
import { Component } from '@wordpress/element';
import PropTypes from 'prop-types';
import { uniqueId } from 'lodash';
/**
* Internal dependencies
*/
import './setting.scss';
class Setting extends Component {
renderInput = () => {
const { handleChange, name, inputType, options, value } = this.props;
const id = uniqueId( name );
switch ( inputType ) {
case 'checkboxGroup':
return options.map(
optionGroup =>
optionGroup.options.length > 0 && (
<div
className="woocommerce-setting__options-group"
key={ optionGroup.key }
aria-labelledby={ name + '-label' }
>
{ optionGroup.label && (
<span className="woocommerce-setting__options-group-label">
{ optionGroup.label }
</span>
) }
{ this.renderCheckboxOptions( optionGroup.options ) }
</div>
)
);
case 'checkbox':
return this.renderCheckboxOptions( options );
case 'text':
default:
return (
<input id={ id } type="text" name={ name } onChange={ handleChange } value={ value } />
);
}
};
renderCheckboxOptions( options ) {
const { handleChange, name, value } = this.props;
return options.map( option => {
const id = uniqueId( name + '-' + option.value );
return (
<label htmlFor={ id } key={ option.value }>
<input
id={ id }
type="checkbox"
name={ name }
onChange={ handleChange }
aria-label={ option.description }
checked={ value && value.includes( option.value ) }
value={ option.value }
/>
{ option.label }
</label>
);
} );
}
render() {
const { helpText, label, name } = this.props;
return (
<div className="woocommerce-setting">
<div className="woocommerce-setting__label" id={ name + '-label' }>
{ label }
</div>
<div className="woocommerce-setting__options">
{ this.renderInput() }
{ helpText && <span className="woocommerce-setting__help">{ helpText }</span> }
</div>
</div>
);
}
}
Setting.propTypes = {
/**
* Function assigned to the onChange of all inputs.
*/
handleChange: PropTypes.func.isRequired,
/**
* Optional help text displayed underneath the setting.
*/
helpText: PropTypes.oneOfType( [ PropTypes.string, PropTypes.array ] ),
/**
* Type of input to use; defaults to a text input.
*/
inputType: PropTypes.oneOf( [ 'checkbox', 'checkboxGroup', 'text' ] ),
/**
* Label used for describing the setting.
*/
label: PropTypes.string.isRequired,
/**
* Setting slug applied to input names.
*/
name: PropTypes.string.isRequired,
/**
* Array of options used for when the `inputType` allows multiple selections.
*/
options: PropTypes.arrayOf(
PropTypes.shape( {
/**
* Input value for this option.
*/
value: PropTypes.string,
/**
* Label for this option or above a group for a group `inputType`.
*/
label: PropTypes.string,
/**
* Description used for screen readers.
*/
description: PropTypes.string,
/**
* Key used for a group `inputType`.
*/
key: PropTypes.string,
/**
* Nested options for a group `inputType`.
*/
options: PropTypes.array,
} )
),
/**
* The string value used for the input or array of items if the input allows multiselection.
*/
value: PropTypes.oneOfType( [ PropTypes.string, PropTypes.array ] ),
};
export default Setting;

View File

@ -0,0 +1,49 @@
/** @format */
.woocommerce-setting {
display: flex;
margin-bottom: $gap-large;
@include breakpoint( '<1280px' ) {
flex-direction: column;
}
}
.woocommerce-setting__label {
@include font-size(16);
margin-bottom: $gap;
padding-right: $gap;
font-weight: bold;
@include breakpoint( '>1280px' ) {
width: 15%;
}
}
.woocommerce-setting__options {
display: flex;
flex-direction: column;
@include breakpoint( '>1280px' ) {
width: 35%;
}
label {
width: 100%;
display: block;
margin-bottom: $gap-small;
color: $core-grey-dark-500;
}
input[type='checkbox'] {
margin-right: $gap-small;
}
}
.woocommerce-setting__options-group-label {
display: block;
font-weight: bold;
margin-bottom: $gap-small;
}
.woocommerce-setting__help {
font-style: italic;
color: $core-grey-dark-300;
}

View File

@ -21,6 +21,7 @@ import { EllipsisMenu, MenuItem, MenuTitle, SectionHeader } from '@woocommerce/c
import withSelect from 'wc-api/with-select';
import TopSellingCategories from './top-selling-categories';
import TopSellingProducts from './top-selling-products';
import TopCoupons from './top-coupons';
import './style.scss';
class Leaderboards extends Component {
@ -134,10 +135,12 @@ class Leaderboards extends Component {
{ ! hiddenLeaderboardKeys.includes( 'top-products' ) && (
<TopSellingProducts query={ query } totalRows={ rowsPerTable } />
) }
{ ! hiddenLeaderboardKeys.includes( 'top-categories' ) && (
<TopSellingCategories query={ query } totalRows={ rowsPerTable } />
) }
{ ! hiddenLeaderboardKeys.includes( 'top-coupons' ) && (
<TopCoupons query={ query } totalRows={ rowsPerTable } />
) }
</div>
</div>
</Fragment>

View File

@ -0,0 +1,122 @@
/** @format */
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { Component } from '@wordpress/element';
import { map } from 'lodash';
/**
* WooCommerce dependencies
*/
import { formatCurrency, getCurrencyFormatDecimal } from '@woocommerce/currency';
import { getNewPath, getPersistedQuery } from '@woocommerce/navigation';
import { Link } from '@woocommerce/components';
import { numberFormat } from '@woocommerce/number';
/**
* Internal dependencies
*/
import Leaderboard from 'analytics/components/leaderboard';
export class TopCoupons extends Component {
constructor( props ) {
super( props );
this.getRowsContent = this.getRowsContent.bind( this );
this.getHeadersContent = this.getHeadersContent.bind( this );
}
getHeadersContent() {
return [
{
label: __( 'Coupon Code', 'wc-admin' ),
key: 'code',
required: true,
isLeftAligned: true,
isSortable: false,
},
{
label: __( 'Orders', 'wc-admin' ),
key: 'orders_count',
required: true,
defaultSort: true,
isSortable: false,
isNumeric: true,
},
{
label: __( 'Amount Discounted', 'wc-admin' ),
key: 'amount',
isSortable: false,
isNumeric: true,
},
];
}
getRowsContent( data ) {
const { query } = this.props;
const persistedQuery = getPersistedQuery( query );
return map( data, row => {
const { amount, coupon_id, extended_info, orders_count } = row;
const { code } = extended_info;
const couponUrl = getNewPath( persistedQuery, 'analytics/coupons', {
filter: 'single_coupon',
coupons: coupon_id,
} );
const couponLink = (
<Link href={ couponUrl } type="wc-admin">
{ code }
</Link>
);
const ordersUrl = getNewPath( persistedQuery, 'analytics/orders', {
filter: 'advanced',
coupon_includes: coupon_id,
} );
const ordersLink = (
<Link href={ ordersUrl } type="wc-admin">
{ numberFormat( orders_count ) }
</Link>
);
return [
{
display: couponLink,
value: code,
},
{
display: ordersLink,
value: orders_count,
},
{
display: formatCurrency( amount ),
value: getCurrencyFormatDecimal( amount ),
},
];
} );
}
render() {
const { query, totalRows } = this.props;
const tableQuery = {
orderby: 'orders_count',
order: 'desc',
per_page: totalRows,
extended_info: true,
};
return (
<Leaderboard
endpoint="coupons"
getHeadersContent={ this.getHeadersContent }
getRowsContent={ this.getRowsContent }
query={ query }
tableQuery={ tableQuery }
title={ __( 'Top Coupons', 'wc-admin' ) }
/>
);
}
}
export default TopCoupons;

View File

@ -12,11 +12,11 @@ import { get, map } from 'lodash';
import { formatCurrency, getCurrencyFormatDecimal } from '@woocommerce/currency';
import { getNewPath, getPersistedQuery } from '@woocommerce/navigation';
import { Link } from '@woocommerce/components';
import { numberFormat } from '@woocommerce/number';
/**
* Internal dependencies
*/
import { numberFormat } from 'lib/number';
import Leaderboard from 'analytics/components/leaderboard';
export class TopSellingCategories extends Component {

View File

@ -12,11 +12,11 @@ import { get, map } from 'lodash';
import { formatCurrency, getCurrencyFormatDecimal } from '@woocommerce/currency';
import { getNewPath, getPersistedQuery } from '@woocommerce/navigation';
import { Link } from '@woocommerce/components';
import { numberFormat } from '@woocommerce/number';
/**
* Internal dependencies
*/
import { numberFormat } from 'lib/number';
import Leaderboard from 'analytics/components/leaderboard';
export class TopSellingProducts extends Component {

View File

@ -15,6 +15,8 @@ import { find } from 'lodash';
*/
import { getCurrentDates, appendTimestamp, getDateParamsFromQuery } from '@woocommerce/date';
import { getNewPath, getPersistedQuery } from '@woocommerce/navigation';
import { calculateDelta, formatValue } from '@woocommerce/number';
import { formatCurrency } from '@woocommerce/currency';
/**
* Internal dependencies
@ -31,7 +33,6 @@ import {
} from '@woocommerce/components';
import withSelect from 'wc-api/with-select';
import './style.scss';
import { calculateDelta, formatValue } from 'lib/number';
class StorePerformance extends Component {
constructor( props ) {
@ -128,9 +129,15 @@ class StorePerformance extends Component {
'';
const reportUrl =
( href && getNewPath( persistedQuery, href, { chart: primaryItem.chart } ) ) || '';
const isCurrency = 'currency' === primaryItem.format;
const delta = calculateDelta( primaryItem.value, secondaryItem.value );
const primaryValue = formatValue( primaryItem.format, primaryItem.value );
const secondaryValue = formatValue( secondaryItem.format, secondaryItem.value );
const primaryValue = isCurrency
? formatCurrency( primaryItem.value )
: formatValue( primaryItem.format, primaryItem.value );
const secondaryValue = isCurrency
? formatCurrency( secondaryItem.value )
: formatValue( secondaryItem.format, secondaryItem.value );
return (
<SummaryNumber

View File

@ -10,7 +10,6 @@ import { Provider as SlotFillProvider } from 'react-slot-fill';
*/
import './stylesheets/_embedded.scss';
import { EmbedLayout } from './layout';
import 'store';
import 'wc-api/wp-data-store';
render(

View File

@ -16,7 +16,7 @@ import { ActivityCard, ActivityCardPlaceholder } from '../activity-card';
import ActivityHeader from '../activity-header';
import { EmptyContent, Section } from '@woocommerce/components';
import sanitizeHTML from 'lib/sanitize-html';
import { QUERY_DEFAULTS } from 'store/constants';
import { QUERY_DEFAULTS } from 'wc-api/constants';
class InboxPanel extends Component {
render() {

View File

@ -33,7 +33,7 @@ import { ActivityCard, ActivityCardPlaceholder } from '../activity-card';
import ActivityHeader from '../activity-header';
import ActivityOutboundLink from '../activity-outbound-link';
import { getOrderRefundTotal } from 'lib/order-values';
import { QUERY_DEFAULTS } from 'store/constants';
import { QUERY_DEFAULTS } from 'wc-api/constants';
import withSelect from 'wc-api/with-select';
function OrdersPanel( { orders, isRequesting, isError } ) {
@ -85,13 +85,58 @@ function OrdersPanel( { orders, isRequesting, isError } ) {
orderLink: <Link href={ 'post.php?action=edit&post=' + order.id } type="wp-admin" />,
// @TODO: Hook up customer name link
customerLink: <Link href={ '#' } type="wp-admin" />,
destinationFlag: <Flag order={ order } round={ false } height={ 9 } width={ 12 } />,
destinationFlag: <Flag order={ order } round={ false } />,
},
} ) }
</Fragment>
);
};
const cards = [];
orders.forEach( ( order, id ) => {
// We want the billing address, but shipping can be used as a fallback.
const address = { ...order.shipping, ...order.billing };
const productsCount = order.line_items.reduce( ( total, line ) => total + line.quantity, 0 );
const total = order.total;
const refundValue = getOrderRefundTotal( order );
const remainingTotal = getCurrencyFormatDecimal( order.total ) + refundValue;
cards.push(
<ActivityCard
key={ id }
className="woocommerce-order-activity-card"
title={ orderCardTitle( order, address ) }
date={ order.date_created }
subtitle={
<div>
<span>
{ sprintf(
_n( '%d product', '%d products', productsCount, 'wc-admin' ),
productsCount
) }
</span>
{ refundValue ? (
<span>
<s>{ formatCurrency( total, order.currency_symbol ) }</s>{' '}
{ formatCurrency( remainingTotal, order.currency_symbol ) }
</span>
) : (
<span>{ formatCurrency( total, order.currency_symbol ) }</span>
) }
</div>
}
actions={
<Button isDefault href={ getAdminLink( 'post.php?action=edit&post=' + order.id ) }>
{ __( 'Begin fulfillment' ) }
</Button>
}
>
<OrderStatus order={ order } />
</ActivityCard>
);
} );
return (
<Fragment>
<ActivityHeader title={ __( 'Orders', 'wc-admin' ) } menu={ menu } />
@ -105,55 +150,7 @@ function OrdersPanel( { orders, isRequesting, isError } ) {
/>
) : (
<Fragment>
{ orders.map( ( order, i ) => {
// We want the billing address, but shipping can be used as a fallback.
const address = { ...order.shipping, ...order.billing };
const productsCount = order.line_items.reduce(
( total, line ) => total + line.quantity,
0
);
const total = order.total;
const refundValue = getOrderRefundTotal( order );
const remainingTotal = getCurrencyFormatDecimal( order.total ) + refundValue;
return (
<ActivityCard
key={ i }
className="woocommerce-order-activity-card"
title={ orderCardTitle( order, address ) }
date={ order.date_created }
subtitle={
<div>
<span>
{ sprintf(
_n( '%d product', '%d products', productsCount, 'wc-admin' ),
productsCount
) }
</span>
{ refundValue ? (
<span>
<s>{ formatCurrency( total, order.currency_symbol ) }</s>{' '}
{ formatCurrency( remainingTotal, order.currency_symbol ) }
</span>
) : (
<span>{ formatCurrency( total, order.currency_symbol ) }</span>
) }
</div>
}
actions={
<Button
isDefault
href={ getAdminLink( 'post.php?action=edit&post=' + order.id ) }
>
{ __( 'Begin fulfillment' ) }
</Button>
}
>
<OrderStatus order={ order } />
</ActivityCard>
);
} ) }
{ cards }
<ActivityOutboundLink href={ 'edit.php?post_type=shop_order' }>
{ __( 'Manage all orders' ) }
</ActivityOutboundLink>
@ -165,29 +162,29 @@ function OrdersPanel( { orders, isRequesting, isError } ) {
}
OrdersPanel.propTypes = {
orders: PropTypes.array.isRequired,
orders: PropTypes.instanceOf( Map ).isRequired,
isError: PropTypes.bool,
isRequesting: PropTypes.bool,
};
OrdersPanel.defaultProps = {
orders: [],
orders: new Map(),
isError: false,
isRequesting: false,
};
export default compose(
withSelect( select => {
const { getOrders, getOrdersError, isGetOrdersRequesting } = select( 'wc-api' );
const { getItems, getItemsError, isGetItemsRequesting } = select( 'wc-api' );
const ordersQuery = {
page: 1,
per_page: QUERY_DEFAULTS.pageSize,
status: 'processing',
};
const orders = getOrders( ordersQuery );
const isError = Boolean( getOrdersError( ordersQuery ) );
const isRequesting = isGetOrdersRequesting( ordersQuery );
const orders = getItems( 'orders', ordersQuery );
const isError = Boolean( getItemsError( 'orders', ordersQuery ) );
const isRequesting = isGetItemsRequesting( 'orders', ordersQuery );
return { orders, isError, isRequesting };
} )

View File

@ -28,7 +28,7 @@ import {
*/
import { ActivityCard, ActivityCardPlaceholder } from '../activity-card';
import ActivityHeader from '../activity-header';
import { QUERY_DEFAULTS } from 'store/constants';
import { QUERY_DEFAULTS } from 'wc-api/constants';
import sanitizeHTML from 'lib/sanitize-html';
import withSelect from 'wc-api/with-select';

View File

@ -10,7 +10,6 @@ import { Provider as SlotFillProvider } from 'react-slot-fill';
*/
import './stylesheets/_index.scss';
import { PageLayout } from './layout';
import 'store';
import 'wc-api/wp-data-store';
render(

View File

@ -16,6 +16,7 @@ import { getPersistedQuery, stringifyQuery } from '@woocommerce/navigation';
*/
import Analytics from 'analytics';
import AnalyticsReport from 'analytics/report';
import AnalyticsSettings from 'analytics/settings';
import Dashboard from 'dashboard';
import DevDocs from 'devdocs';
@ -33,6 +34,12 @@ const getPages = () => {
wpOpenMenu: 'toplevel_page_wc-admin--analytics-revenue',
wpClosedMenu: 'toplevel_page_woocommerce',
},
{
container: AnalyticsSettings,
path: '/analytics/settings',
wpOpenMenu: 'toplevel_page_wc-admin--analytics-revenue',
wpClosedMenu: 'toplevel_page_woocommerce',
},
{
container: AnalyticsReport,
path: '/analytics/:report',

View File

@ -13,7 +13,7 @@ import { getIdsFromQuery, stringifyQuery } from '@woocommerce/navigation';
/**
* Internal dependencies
*/
import { NAMESPACE } from 'store/constants';
import { NAMESPACE } from 'wc-api/constants';
/**
* Get a function that accepts ids as they are found in url parameter and

View File

@ -1,16 +0,0 @@
/**
* @format
*/
export const NAMESPACE = '/wc/v4/';
export const SWAGGERNAMESPACE = 'https://virtserver.swaggerhub.com/peterfabian/wc-v3-api/1.0.0/';
export const ERROR = 'ERROR';
// WordPress & WooCommerce both set a hard limit of 100 for the per_page parameter
export const MAX_PER_PAGE = 100;
export const QUERY_DEFAULTS = {
pageSize: 25,
period: 'month',
compare: 'previous_year',
};

View File

@ -1,41 +0,0 @@
/** @format */
/**
* External dependencies
*/
import { combineReducers, registerStore } from '@wordpress/data';
/**
* Internal dependencies
*/
import { applyMiddleware, addThunks } from './middleware';
import orders from 'store/orders';
import reports from 'store/reports';
import notes from 'store/notes';
const store = registerStore( 'wc-admin', {
reducer: combineReducers( {
orders: orders.reducer,
reports: reports.reducer,
notes: notes.reducer,
} ),
actions: {
...orders.actions,
...reports.actions,
...notes.actions,
},
selectors: {
...orders.selectors,
...reports.selectors,
...notes.selectors,
},
resolvers: {
...orders.resolvers,
...reports.resolvers,
...notes.resolvers,
},
} );
applyMiddleware( store, [ addThunks ] );

View File

@ -1,16 +0,0 @@
/** @format */
export function applyMiddleware( store, middlewares ) {
middlewares = middlewares.slice();
middlewares.reverse();
let dispatch = store.dispatch;
middlewares.forEach( middleware => ( dispatch = middleware( store )( dispatch ) ) );
return Object.assign( store, { dispatch } );
}
export const addThunks = ( { getState } ) => next => action => {
if ( 'function' === typeof action ) {
return action( getState );
}
return next( action );
};

View File

@ -1,17 +0,0 @@
/** @format */
export default {
setNotes( notes, query ) {
return {
type: 'SET_NOTES',
notes,
query: query || {},
};
},
setNotesError( query ) {
return {
type: 'SET_NOTES_ERROR',
query: query || {},
};
},
};

View File

@ -1,15 +0,0 @@
/** @format */
/**
* Internal dependencies
*/
import actions from './actions';
import reducer from './reducer';
import resolvers from './resolvers';
import selectors from './selectors';
export default {
actions,
reducer,
resolvers,
selectors,
};

View File

@ -1,31 +0,0 @@
/** @format */
/**
* External dependencies
*/
import { merge } from 'lodash';
/**
* Internal dependencies
*/
import { ERROR } from 'store/constants';
import { getJsonString } from 'store/utils';
const DEFAULT_STATE = {};
export default function notesReducer( state = DEFAULT_STATE, action ) {
const queryKey = getJsonString( action.query );
switch ( action.type ) {
case 'SET_NOTES':
return merge( {}, state, {
[ queryKey ]: action.notes,
} );
case 'SET_NOTES_ERROR':
return merge( {}, state, {
[ queryKey ]: ERROR,
} );
}
return state;
}

View File

@ -1,32 +0,0 @@
/** @format */
/**
* External dependencies
*/
import { dispatch } from '@wordpress/data';
import apiFetch from '@wordpress/api-fetch';
/**
* WooCommerce dependencies
*/
import { stringifyQuery } from '@woocommerce/navigation';
/**
* Internal dependencies
*/
import { NAMESPACE } from 'store/constants';
export default {
// TODO: Use controls data plugin or fresh-data instead of async
async getNotes( ...args ) {
// This is interim code to work with either 2.x or 3.x version of @wordpress/data
// TODO: Change to just `getNotes( query )` after Gutenberg plugin uses @wordpress/data 3+
const query = args.length === 1 ? args[ 0 ] : args[ 1 ];
try {
const notes = await apiFetch( { path: NAMESPACE + 'admin/notes' + stringifyQuery( query ) } );
dispatch( 'wc-admin' ).setNotes( notes, query );
} catch ( error ) {
dispatch( 'wc-admin' ).setNotesError( query );
}
},
};

View File

@ -1,49 +0,0 @@
/** @format */
/**
* External dependencies
*/
import { get } from 'lodash';
import { select } from '@wordpress/data';
/**
* Internal dependencies
*/
import { getJsonString } from 'store/utils';
import { ERROR } from 'store/constants';
/**
* Returns notes for a specific query.
*
* @param {Object} state Current state
* @param {Object} query Note query parameters
* @return {Array} Notes
*/
function getNotes( state, query = {} ) {
return get( state, [ 'notes', getJsonString( query ) ], [] );
}
export default {
getNotes,
/**
* Returns true if a query is pending.
*
* @param {Object} state Current state
* @return {Boolean} True if the `getNotes` request is pending, false otherwise
*/
isGetNotesRequesting( state, ...args ) {
return select( 'core/data' ).isResolving( 'wc-admin', 'getNotes', args );
},
/**
* Returns true if a get notes request has returned an error.
*
* @param {Object} state Current state
* @param {Object} query Query parameters
* @return {Boolean} True if the `getNotes` request has failed, false otherwise
*/
isGetNotesError( state, query ) {
return ERROR === getNotes( state, query );
},
};

View File

@ -1,80 +0,0 @@
/**
* @format
*/
/**
* External dependencies
*/
import deepFreeze from 'deep-freeze';
/**
* Internal dependencies
*/
import { ERROR } from 'store/constants';
import notesReducer from '../reducer';
import { getJsonString } from 'store/utils';
describe( 'notesReducer()', () => {
it( 'returns an empty data object by default', () => {
const state = notesReducer( undefined, {} );
expect( state ).toEqual( {} );
} );
it( 'returns with received notes data', () => {
const originalState = deepFreeze( {} );
const query = {
page: 2,
};
const notes = [ { id: 1214 }, { id: 1215 }, { id: 1216 } ];
const state = notesReducer( originalState, {
type: 'SET_NOTES',
query,
notes,
} );
const queryKey = getJsonString( query );
expect( state[ queryKey ] ).toEqual( notes );
} );
it( 'tracks multiple queries in notes data', () => {
const otherQuery = {
page: 3,
};
const otherQueryKey = getJsonString( otherQuery );
const otherNotes = [ { id: 1 }, { id: 2 }, { id: 3 } ];
const otherQueryState = {
[ otherQueryKey ]: otherNotes,
};
const originalState = deepFreeze( otherQueryState );
const query = {
page: 2,
};
const notes = [ { id: 1214 }, { id: 1215 }, { id: 1216 } ];
const state = notesReducer( originalState, {
type: 'SET_NOTES',
query,
notes,
} );
const queryKey = getJsonString( query );
expect( state[ queryKey ] ).toEqual( notes );
expect( state[ otherQueryKey ] ).toEqual( otherNotes );
} );
it( 'returns with received error data', () => {
const originalState = deepFreeze( {} );
const query = {
page: 2,
};
const state = notesReducer( originalState, {
type: 'SET_NOTES_ERROR',
query,
} );
const queryKey = getJsonString( query );
expect( state[ queryKey ] ).toEqual( ERROR );
} );
} );

View File

@ -1,53 +0,0 @@
/*
* @format
*/
/**
* External dependencies
*/
import apiFetch from '@wordpress/api-fetch';
import { dispatch } from '@wordpress/data';
/**
* Internal dependencies
*/
import { NAMESPACE } from 'store/constants';
import resolvers from '../resolvers';
const { getNotes } = resolvers;
jest.mock( '@wordpress/data', () => ( {
dispatch: jest.fn().mockReturnValue( {
setNotes: jest.fn(),
} ),
} ) );
jest.mock( '@wordpress/api-fetch', () => jest.fn() );
describe( 'getNotes', () => {
const NOTES_1 = [ { id: 1214 }, { id: 1215 }, { id: 1216 } ];
const NOTES_2 = [ { id: 1 }, { id: 2 }, { id: 3 } ];
beforeAll( () => {
apiFetch.mockImplementation( options => {
if ( options.path === NAMESPACE + 'admin/notes' ) {
return Promise.resolve( NOTES_1 );
}
if ( options.path === NAMESPACE + 'admin/notes?page=2' ) {
return Promise.resolve( NOTES_2 );
}
} );
} );
it( 'returns requested data', async () => {
expect.assertions( 1 );
await getNotes();
expect( dispatch().setNotes ).toHaveBeenCalledWith( NOTES_1, undefined );
} );
it( 'returns requested data for a specific query', async () => {
expect.assertions( 1 );
await getNotes( { page: 2 } );
expect( dispatch().setNotes ).toHaveBeenCalledWith( NOTES_2, { page: 2 } );
} );
} );

View File

@ -1,92 +0,0 @@
/*
* @format
*/
/**
* External dependencies
*/
import deepFreeze from 'deep-freeze';
/**
* Internal dependencies
*/
import { ERROR } from 'store/constants';
import selectors from '../selectors';
import { select } from '@wordpress/data';
import { getJsonString } from 'store/utils';
const { getNotes, isGetNotesRequesting, isGetNotesError } = selectors;
jest.mock( '@wordpress/data', () => ( {
...require.requireActual( '@wordpress/data' ),
select: jest.fn().mockReturnValue( {} ),
} ) );
const query = { page: 1 };
const queryKey = getJsonString( query );
describe( 'getNotes()', () => {
it( 'returns an empty array when no notes are available', () => {
const state = deepFreeze( {} );
expect( getNotes( state, query ) ).toEqual( [] );
} );
it( 'returns stored notes for current query', () => {
const notes = [ { id: 1214 }, { id: 1215 }, { id: 1216 } ];
const state = deepFreeze( {
notes: {
[ queryKey ]: notes,
},
} );
expect( getNotes( state, query ) ).toEqual( notes );
} );
} );
describe( 'isGetNotesRequesting()', () => {
beforeAll( () => {
select( 'core/data' ).isResolving = jest.fn().mockReturnValue( false );
} );
afterAll( () => {
select( 'core/data' ).isResolving.mockRestore();
} );
function setIsResolving( isResolving ) {
select( 'core/data' ).isResolving.mockImplementation(
( reducerKey, selectorName ) =>
isResolving && reducerKey === 'wc-admin' && selectorName === 'getNotes'
);
}
it( 'returns false if never requested', () => {
const result = isGetNotesRequesting( query );
expect( result ).toBe( false );
} );
it( 'returns false if request finished', () => {
setIsResolving( false );
const result = isGetNotesRequesting( query );
expect( result ).toBe( false );
} );
it( 'returns true if requesting', () => {
setIsResolving( true );
const result = isGetNotesRequesting( query );
expect( result ).toBe( true );
} );
} );
describe( 'isGetNotesError()', () => {
it( 'returns false by default', () => {
const state = deepFreeze( {} );
expect( isGetNotesError( state, query ) ).toEqual( false );
} );
it( 'returns true if ERROR constant is found', () => {
const state = deepFreeze( {
notes: {
[ queryKey ]: ERROR,
},
} );
expect( isGetNotesError( state, query ) ).toEqual( true );
} );
} );

View File

@ -1,17 +0,0 @@
/** @format */
export default {
setOrders( orders, query ) {
return {
type: 'SET_ORDERS',
orders,
query: query || {},
};
},
setOrdersError( query ) {
return {
type: 'SET_ORDERS_ERROR',
query: query || {},
};
},
};

View File

@ -1,15 +0,0 @@
/** @format */
/**
* Internal dependencies
*/
import actions from './actions';
import reducer from './reducer';
import resolvers from './resolvers';
import selectors from './selectors';
export default {
actions,
reducer,
resolvers,
selectors,
};

View File

@ -1,32 +0,0 @@
/** @format */
/**
* External dependencies
*/
import { merge } from 'lodash';
/**
* Internal dependencies
*/
import { ERROR } from 'store/constants';
import { getJsonString } from 'store/utils';
const DEFAULT_STATE = {};
export default function ordersReducer( state = DEFAULT_STATE, action ) {
const queryKey = getJsonString( action.query );
switch ( action.type ) {
case 'SET_ORDERS':
return merge( {}, state, {
[ queryKey ]: action.orders,
} );
case 'SET_ORDERS_ERROR':
return merge( {}, state, {
[ queryKey ]: ERROR,
} );
}
return state;
}

View File

@ -1,32 +0,0 @@
/** @format */
/**
* External dependencies
*/
import { dispatch } from '@wordpress/data';
import apiFetch from '@wordpress/api-fetch';
/**
* WooCommerce dependencies
*/
import { stringifyQuery } from '@woocommerce/navigation';
/**
* Internal dependencies
*/
import { NAMESPACE } from 'store/constants';
export default {
// TODO: Use controls data plugin or fresh-data instead of async
async getOrders( ...args ) {
// This is interim code to work with either 2.x or 3.x version of @wordpress/data
// TODO: Change to just `getNotes( query )` after Gutenberg plugin uses @wordpress/data 3+
const query = args.length === 1 ? args[ 0 ] : args[ 1 ];
try {
const orders = await apiFetch( { path: NAMESPACE + 'orders' + stringifyQuery( query ) } );
dispatch( 'wc-admin' ).setOrders( orders, query );
} catch ( error ) {
dispatch( 'wc-admin' ).setOrdersError( query );
}
},
};

View File

@ -1,49 +0,0 @@
/** @format */
/**
* External dependencies
*/
import { get } from 'lodash';
import { select } from '@wordpress/data';
/**
* Internal dependencies
*/
import { getJsonString } from 'store/utils';
import { ERROR } from 'store/constants';
/**
* Returns orders for a specific query.
*
* @param {Object} state Current state
* @param {Object} query Report query parameters
* @return {Array} Report details
*/
function getOrders( state, query = {} ) {
return get( state, [ 'orders', getJsonString( query ) ], [] );
}
export default {
getOrders,
/**
* Returns true if a getOrders request is pending.
*
* @param {Object} state Current state
* @return {Boolean} True if the `getOrders` request is pending, false otherwise
*/
isGetOrdersRequesting( state, ...args ) {
return select( 'core/data' ).isResolving( 'wc-admin', 'getOrders', args );
},
/**
* Returns true if a getOrders request has returned an error.
*
* @param {Object} state Current state
* @param {Object} query Query parameters
* @return {Boolean} True if the `getOrders` request has failed, false otherwise
*/
isGetOrdersError( state, query ) {
return ERROR === getOrders( state, query );
},
};

View File

@ -1,80 +0,0 @@
/**
* @format
*/
/**
* External dependencies
*/
import deepFreeze from 'deep-freeze';
/**
* Internal dependencies
*/
import { ERROR } from 'store/constants';
import ordersReducer from '../reducer';
import { getJsonString } from 'store/utils';
describe( 'ordersReducer()', () => {
it( 'returns an empty data object by default', () => {
const state = ordersReducer( undefined, {} );
expect( state ).toEqual( {} );
} );
it( 'returns with received orders data', () => {
const originalState = deepFreeze( {} );
const query = {
orderby: 'date',
};
const orders = [ { id: 1214 }, { id: 1215 }, { id: 1216 } ];
const state = ordersReducer( originalState, {
type: 'SET_ORDERS',
query,
orders,
} );
const queryKey = getJsonString( query );
expect( state[ queryKey ] ).toEqual( orders );
} );
it( 'tracks multiple queries in orders data', () => {
const otherQuery = {
orderby: 'id',
};
const otherQueryKey = getJsonString( otherQuery );
const otherOrders = [ { id: 1 }, { id: 2 }, { id: 3 } ];
const otherQueryState = {
[ otherQueryKey ]: otherOrders,
};
const originalState = deepFreeze( otherQueryState );
const query = {
orderby: 'date',
};
const orders = [ { id: 1214 }, { id: 1215 }, { id: 1216 } ];
const state = ordersReducer( originalState, {
type: 'SET_ORDERS',
query,
orders,
} );
const queryKey = getJsonString( query );
expect( state[ queryKey ] ).toEqual( orders );
expect( state[ otherQueryKey ] ).toEqual( otherOrders );
} );
it( 'returns with received error data', () => {
const originalState = deepFreeze( {} );
const query = {
orderby: 'date',
};
const state = ordersReducer( originalState, {
type: 'SET_ORDERS_ERROR',
query,
} );
const queryKey = getJsonString( query );
expect( state[ queryKey ] ).toEqual( ERROR );
} );
} );

View File

@ -1,53 +0,0 @@
/*
* @format
*/
/**
* External dependencies
*/
import apiFetch from '@wordpress/api-fetch';
import { dispatch } from '@wordpress/data';
/**
* Internal dependencies
*/
import { NAMESPACE } from 'store/constants';
import resolvers from '../resolvers';
const { getOrders } = resolvers;
jest.mock( '@wordpress/data', () => ( {
dispatch: jest.fn().mockReturnValue( {
setOrders: jest.fn(),
} ),
} ) );
jest.mock( '@wordpress/api-fetch', () => jest.fn() );
describe( 'getOrders', () => {
const ORDERS_1 = [ { id: 1214 }, { id: 1215 }, { id: 1216 } ];
const ORDERS_2 = [ { id: 1 }, { id: 2 }, { id: 3 } ];
beforeAll( () => {
apiFetch.mockImplementation( options => {
if ( options.path === NAMESPACE + 'orders' ) {
return Promise.resolve( ORDERS_1 );
}
if ( options.path === NAMESPACE + 'orders?orderby=id' ) {
return Promise.resolve( ORDERS_2 );
}
} );
} );
it( 'returns requested report data', async () => {
expect.assertions( 1 );
await getOrders();
expect( dispatch().setOrders ).toHaveBeenCalledWith( ORDERS_1, undefined );
} );
it( 'returns requested report data for a specific query', async () => {
expect.assertions( 1 );
await getOrders( { orderby: 'id' } );
expect( dispatch().setOrders ).toHaveBeenCalledWith( ORDERS_2, { orderby: 'id' } );
} );
} );

View File

@ -1,92 +0,0 @@
/*
* @format
*/
/**
* External dependencies
*/
import deepFreeze from 'deep-freeze';
import { select } from '@wordpress/data';
/**
* Internal dependencies
*/
import { ERROR } from 'store/constants';
import selectors from '../selectors';
import { getJsonString } from 'store/utils';
const { getOrders, isGetOrdersRequesting, isGetOrdersError } = selectors;
jest.mock( '@wordpress/data', () => ( {
...require.requireActual( '@wordpress/data' ),
select: jest.fn().mockReturnValue( {} ),
} ) );
const query = { orderby: 'date' };
const queryKey = getJsonString( query );
describe( 'getOrders()', () => {
it( 'returns an empty array when no orders are available', () => {
const state = deepFreeze( {} );
expect( getOrders( state, query ) ).toEqual( [] );
} );
it( 'returns stored orders for current query', () => {
const orders = [ { id: 1214 }, { id: 1215 }, { id: 1216 } ];
const state = deepFreeze( {
orders: {
[ queryKey ]: orders,
},
} );
expect( getOrders( state, query ) ).toEqual( orders );
} );
} );
describe( 'isGetOrdersRequesting()', () => {
beforeAll( () => {
select( 'core/data' ).isResolving = jest.fn().mockReturnValue( false );
} );
afterAll( () => {
select( 'core/data' ).isResolving.mockRestore();
} );
function setIsResolving( isResolving ) {
select( 'core/data' ).isResolving.mockImplementation(
( reducerKey, selectorName ) =>
isResolving && reducerKey === 'wc-admin' && selectorName === 'getOrders'
);
}
it( 'returns false if never requested', () => {
const result = isGetOrdersRequesting( query );
expect( result ).toBe( false );
} );
it( 'returns false if request finished', () => {
setIsResolving( false );
const result = isGetOrdersRequesting( query );
expect( result ).toBe( false );
} );
it( 'returns true if requesting', () => {
setIsResolving( true );
const result = isGetOrdersRequesting( query );
expect( result ).toBe( true );
} );
} );
describe( 'isGetOrdersError()', () => {
it( 'returns false by default', () => {
const state = deepFreeze( {} );
expect( isGetOrdersError( state, query ) ).toEqual( false );
} );
it( 'returns true if ERROR constant is found', () => {
const state = deepFreeze( {
orders: {
[ queryKey ]: ERROR,
},
} );
expect( isGetOrdersError( state, query ) ).toEqual( true );
} );
} );

View File

@ -1,31 +0,0 @@
/** @format */
/**
* External dependencies
*/
import { combineReducers } from '@wordpress/data';
/**
* Internal dependencies
*/
import items from './items';
import stats from './stats';
export default {
reducer: combineReducers( {
items: items.reducer,
stats: stats.reducer,
} ),
actions: {
...items.actions,
...stats.actions,
},
selectors: {
...items.selectors,
...stats.selectors,
},
resolvers: {
...items.resolvers,
...stats.resolvers,
},
};

View File

@ -1,20 +0,0 @@
/** @format */
export default {
setReportItems( endpoint, query, data, totalCount ) {
return {
type: 'SET_REPORT_ITEMS',
endpoint,
query: query || {},
data,
totalCount,
};
},
setReportItemsError( endpoint, query ) {
return {
type: 'SET_REPORT_ITEMS_ERROR',
endpoint,
query: query || {},
};
},
};

View File

@ -1,15 +0,0 @@
/** @format */
/**
* Internal dependencies
*/
import actions from './actions';
import reducer from './reducer';
import resolvers from './resolvers';
import selectors from './selectors';
export default {
actions,
reducer,
resolvers,
selectors,
};

View File

@ -1,39 +0,0 @@
/** @format */
/**
* External dependencies
*/
import { merge } from 'lodash';
/**
* Internal dependencies
*/
import { ERROR } from 'store/constants';
import { getJsonString } from 'store/utils';
const DEFAULT_STATE = {};
export default function reportItemsReducer( state = DEFAULT_STATE, action ) {
const queryKey = getJsonString( action.query );
switch ( action.type ) {
case 'SET_REPORT_ITEMS':
return merge( {}, state, {
[ action.endpoint ]: {
[ queryKey ]: {
data: action.data,
totalCount: action.totalCount,
},
},
} );
case 'SET_REPORT_ITEMS_ERROR':
return merge( {}, state, {
[ action.endpoint ]: {
[ queryKey ]: ERROR,
},
} );
}
return state;
}

View File

@ -1,36 +0,0 @@
/** @format */
/**
* External dependencies
*/
import apiFetch from '@wordpress/api-fetch';
import { dispatch } from '@wordpress/data';
/**
* WooCommerce dependencies
*/
import { stringifyQuery } from '@woocommerce/navigation';
/**
* Internal dependencies
*/
import { NAMESPACE } from 'store/constants';
export default {
async getReportItems( ...args ) {
const [ endpoint, query ] = args.slice( -2 );
try {
const response = await apiFetch( {
parse: false,
path: NAMESPACE + 'reports/' + endpoint + stringifyQuery( query ),
} );
const itemsData = await response.json();
const totalCount = parseInt( response.headers.get( 'x-wp-total' ) );
dispatch( 'wc-admin' ).setReportItems( endpoint, query, itemsData, totalCount );
} catch ( error ) {
dispatch( 'wc-admin' ).setReportItemsError( endpoint, query );
}
},
};

View File

@ -1,51 +0,0 @@
/** @format */
/**
* External dependencies
*/
import { get } from 'lodash';
import { select } from '@wordpress/data';
/**
* Internal dependencies
*/
import { ERROR } from 'store/constants';
import { getJsonString } from 'store/utils';
/**
* Returns report items for a specific endpoint query.
*
* @param {Object} state Current state
* @param {String} endpoint Stats endpoint
* @param {Object} query Report query parameters
* @return {Object} Report details
*/
function getReportItems( state, endpoint, query = {} ) {
return get( state, [ 'reports', 'items', endpoint, getJsonString( query ) ], { data: [] } );
}
export default {
getReportItems,
/**
* Returns true if a getReportItems request is pending.
*
* @param {Object} state Current state
* @return {Boolean} True if the `getReportItems` request is pending, false otherwise
*/
isGetReportItemsRequesting( state, ...args ) {
return select( 'core/data' ).isResolving( 'wc-admin', 'getReportItems', args );
},
/**
* Returns true if a getReportItems request has returned an error.
*
* @param {Object} state Current state
* @param {String} endpoint Items endpoint
* @param {Object} query Report query parameters
* @return {Boolean} True if the `getReportItems` request has failed, false otherwise
*/
isGetReportItemsError( state, endpoint, query ) {
return ERROR === getReportItems( state, endpoint, query );
},
};

View File

@ -1,104 +0,0 @@
/**
* @format
*/
/**
* External dependencies
*/
import deepFreeze from 'deep-freeze';
/**
* Internal dependencies
*/
import { ERROR } from 'store/constants';
import reportItemsReducer from '../reducer';
import { getJsonString } from 'store/utils';
describe( 'reportItemsReducer()', () => {
const endpoint = 'coupons';
it( 'returns an empty object by default', () => {
const state = reportItemsReducer( undefined, {} );
expect( state ).toEqual( {} );
} );
it( 'returns with received items data', () => {
const originalState = deepFreeze( {} );
const query = {
orderby: 'orders_count',
};
const itemsData = [ { id: 1214 }, { id: 1215 }, { id: 1216 } ];
const itemsTotalCount = 50;
const state = reportItemsReducer( originalState, {
type: 'SET_REPORT_ITEMS',
endpoint,
query,
data: itemsData,
totalCount: itemsTotalCount,
} );
const queryKey = getJsonString( query );
expect( state[ endpoint ][ queryKey ] ).toEqual( {
data: itemsData,
totalCount: itemsTotalCount,
} );
} );
it( 'tracks multiple queries in items data', () => {
const otherQuery = {
orderby: 'id',
};
const otherQueryKey = getJsonString( otherQuery );
const otherItemsData = [ { id: 1 }, { id: 2 }, { id: 3 } ];
const otherItemsTotalCount = 70;
const otherQueryState = {
[ endpoint ]: {
[ otherQueryKey ]: {
data: otherItemsData,
totalCount: otherItemsTotalCount,
},
},
};
const originalState = deepFreeze( otherQueryState );
const query = {
orderby: 'orders_count',
};
const itemsData = [ { id: 1214 }, { id: 1215 }, { id: 1216 } ];
const itemsTotalCount = 50;
const state = reportItemsReducer( originalState, {
type: 'SET_REPORT_ITEMS',
endpoint,
query,
data: itemsData,
totalCount: itemsTotalCount,
} );
const queryKey = getJsonString( query );
expect( state[ endpoint ][ queryKey ] ).toEqual( {
data: itemsData,
totalCount: itemsTotalCount,
} );
expect( state[ endpoint ][ otherQueryKey ] ).toEqual( {
data: otherItemsData,
totalCount: otherItemsTotalCount,
} );
} );
it( 'returns with received error data', () => {
const originalState = deepFreeze( {} );
const query = {
orderby: 'orders_count',
};
const state = reportItemsReducer( originalState, {
type: 'SET_REPORT_ITEMS_ERROR',
endpoint,
query,
} );
const queryKey = getJsonString( query );
expect( state[ endpoint ][ queryKey ] ).toEqual( ERROR );
} );
} );

View File

@ -1,74 +0,0 @@
/*
* @format
*/
/**
* External dependencies
*/
import apiFetch from '@wordpress/api-fetch';
import { dispatch } from '@wordpress/data';
/**
* Internal dependencies
*/
import resolvers from '../resolvers';
const { getReportItems } = resolvers;
jest.mock( '@wordpress/data', () => ( {
dispatch: jest.fn().mockReturnValue( {
setReportItems: jest.fn(),
} ),
} ) );
jest.mock( '@wordpress/api-fetch', () => jest.fn() );
describe( 'getReportItems', () => {
const ITEMS_1 = [ { id: 1214 }, { id: 1215 }, { id: 1216 } ];
const ITEMS_1_COUNT = 50;
const ITEMS_2 = [ { id: 1 }, { id: 2 }, { id: 3 } ];
const ITEMS_2_COUNT = 75;
const endpoint = 'products';
beforeAll( () => {
apiFetch.mockImplementation( options => {
if ( options.path === `/wc/v4/reports/${ endpoint }` ) {
return Promise.resolve( {
headers: {
get: () => ITEMS_1_COUNT,
},
json: () => Promise.resolve( ITEMS_1 ),
} );
}
if ( options.path === `/wc/v4/reports/${ endpoint }?orderby=id` ) {
return Promise.resolve( {
headers: {
get: () => ITEMS_2_COUNT,
},
json: () => Promise.resolve( ITEMS_2 ),
} );
}
} );
} );
it( 'returns requested report data', async () => {
expect.assertions( 1 );
await getReportItems( endpoint );
expect( dispatch().setReportItems ).toHaveBeenCalledWith(
endpoint,
undefined,
ITEMS_1,
ITEMS_1_COUNT
);
} );
it( 'returns requested report data for a specific query', async () => {
expect.assertions( 1 );
await getReportItems( endpoint, { orderby: 'id' } );
expect( dispatch().setReportItems ).toHaveBeenCalledWith(
endpoint,
{ orderby: 'id' },
ITEMS_2,
ITEMS_2_COUNT
);
} );
} );

View File

@ -1,106 +0,0 @@
/*
* @format
*/
/**
* External dependencies
*/
import deepFreeze from 'deep-freeze';
import { select } from '@wordpress/data';
/**
* Internal dependencies
*/
import { ERROR } from 'store/constants';
import { getJsonString } from 'store/utils';
import selectors from '../selectors';
const { getReportItems, isGetReportItemsRequesting, isGetReportItemsError } = selectors;
jest.mock( '@wordpress/data', () => ( {
...require.requireActual( '@wordpress/data' ),
select: jest.fn().mockReturnValue( {} ),
} ) );
const query = { orderby: 'date' };
const queryKey = getJsonString( query );
const endpoint = 'coupons';
describe( 'getReportItems()', () => {
it( 'returns an empty object when no items are available', () => {
const state = deepFreeze( {} );
expect( getReportItems( state, endpoint, query ) ).toEqual( { data: [] } );
} );
it( 'returns stored items for current query', () => {
const itemsData = [ { id: 1214 }, { id: 1215 }, { id: 1216 } ];
const itemsTotalCount = 50;
const queryState = {
data: itemsData,
totalCount: itemsTotalCount,
};
const state = deepFreeze( {
reports: {
items: {
[ endpoint ]: {
[ queryKey ]: queryState,
},
},
},
} );
expect( getReportItems( state, endpoint, query ) ).toEqual( queryState );
} );
} );
describe( 'isGetReportItemsRequesting()', () => {
beforeAll( () => {
select( 'core/data' ).isResolving = jest.fn().mockReturnValue( false );
} );
afterAll( () => {
select( 'core/data' ).isResolving.mockRestore();
} );
function setIsResolving( isResolving ) {
select( 'core/data' ).isResolving.mockImplementation(
( reducerKey, selectorName ) =>
isResolving && reducerKey === 'wc-admin' && selectorName === 'getReportItems'
);
}
it( 'returns false if never requested', () => {
const result = isGetReportItemsRequesting( endpoint );
expect( result ).toBe( false );
} );
it( 'returns false if request finished', () => {
setIsResolving( false );
const result = isGetReportItemsRequesting( endpoint );
expect( result ).toBe( false );
} );
it( 'returns true if requesting', () => {
setIsResolving( true );
const result = isGetReportItemsRequesting( endpoint );
expect( result ).toBe( true );
} );
} );
describe( 'isGetReportItemsError()', () => {
it( 'returns false by default', () => {
const state = deepFreeze( {} );
expect( isGetReportItemsError( state, endpoint, query ) ).toEqual( false );
} );
it( 'returns true if ERROR constant is found', () => {
const state = deepFreeze( {
reports: {
items: {
[ endpoint ]: {
[ queryKey ]: ERROR,
},
},
},
} );
expect( isGetReportItemsError( state, endpoint, query ) ).toEqual( true );
} );
} );

View File

@ -1,21 +0,0 @@
/** @format */
export default {
setReportStats( endpoint, report, query, totalResults, totalPages ) {
return {
type: 'SET_REPORT_STATS',
endpoint,
report,
totalResults,
totalPages,
query: query || {},
};
},
setReportStatsError( endpoint, query ) {
return {
type: 'SET_REPORT_STATS_ERROR',
endpoint,
query: query || {},
};
},
};

View File

@ -1,16 +0,0 @@
/** @format */
/**
* Internal dependencies
*/
import actions from './actions';
import reducer from './reducer';
import resolvers from './resolvers';
import selectors from './selectors';
export default {
actions,
reducer,
resolvers,
selectors,
};

View File

@ -1,40 +0,0 @@
/** @format */
/**
* External dependencies
*/
import { merge } from 'lodash';
/**
* Internal dependencies
*/
import { ERROR } from 'store/constants';
import { getJsonString } from 'store/utils';
const DEFAULT_STATE = {};
export default function reportStatsReducer( state = DEFAULT_STATE, action ) {
if ( 'SET_REPORT_STATS' === action.type ) {
const queryKey = getJsonString( action.query );
return merge( {}, state, {
[ action.endpoint ]: {
[ queryKey ]: {
data: action.report,
totalResults: action.totalResults,
totalPages: action.totalPages,
},
},
} );
}
if ( 'SET_REPORT_STATS_ERROR' === action.type ) {
const queryKey = getJsonString( action.query );
return merge( {}, state, {
[ action.endpoint ]: {
[ queryKey ]: ERROR,
},
} );
}
return state;
}

View File

@ -1,63 +0,0 @@
/** @format */
/**
* External dependencies
*/
import apiFetch from '@wordpress/api-fetch';
import { dispatch } from '@wordpress/data';
/**
* WooCommerce dependencies
*/
import { stringifyQuery } from '@woocommerce/navigation';
/**
* Internal dependencies
*/
import { NAMESPACE, SWAGGERNAMESPACE } from 'store/constants';
export default {
// TODO: Use controls data plugin or fresh-data instead of async
async getReportStats( ...args ) {
// This is interim code to work with either 2.x or 3.x version of @wordpress/data
// TODO: Change to just `getNotes( endpoint, query )`
// after Gutenberg plugin uses @wordpress/data 3+
const [ endpoint, query ] = args.length === 2 ? args : args.slice( 1, 3 );
const statEndpoints = [ 'orders', 'revenue', 'products', 'taxes' ];
let apiPath = endpoint + stringifyQuery( query );
// TODO: Remove once swagger endpoints are phased out.
const swaggerEndpoints = [ 'categories' ];
if ( swaggerEndpoints.indexOf( endpoint ) >= 0 ) {
apiPath = SWAGGERNAMESPACE + 'reports/' + endpoint + '/stats' + stringifyQuery( query );
try {
const response = await fetch( apiPath );
const report = await response.json();
dispatch( 'wc-admin' ).setReportStats( endpoint, report, query );
} catch ( error ) {
dispatch( 'wc-admin' ).setReportStatsError( endpoint, query );
}
return;
}
if ( statEndpoints.indexOf( endpoint ) >= 0 ) {
apiPath = NAMESPACE + 'reports/' + endpoint + '/stats' + stringifyQuery( query );
}
try {
const response = await apiFetch( {
path: apiPath,
parse: false,
} );
const report = await response.json();
const totalResults = parseInt( response.headers.get( 'x-wp-total' ) );
const totalPages = parseInt( response.headers.get( 'x-wp-totalpages' ) );
dispatch( 'wc-admin' ).setReportStats( endpoint, report, query, totalResults, totalPages );
} catch ( error ) {
dispatch( 'wc-admin' ).setReportStatsError( endpoint, query );
}
},
};

View File

@ -1,52 +0,0 @@
/** @format */
/**
* External dependencies
*/
import { get } from 'lodash';
import { select } from '@wordpress/data';
/**
* Internal dependencies
*/
import { ERROR } from 'store/constants';
import { getJsonString } from 'store/utils';
/**
* Returns report stats details for a specific endpoint query.
*
* @param {Object} state Current state
* @param {String} endpoint Stats endpoint
* @param {Object} query Report query parameters
* @return {Object} Report details
*/
function getReportStats( state, endpoint, query = {} ) {
const queries = get( state, [ 'reports', 'stats', endpoint ], {} );
return queries[ getJsonString( query ) ] || null;
}
export default {
getReportStats,
/**
* Returns true if a stats query is pending.
*
* @param {Object} state Current state
* @return {Boolean} True if the `getReportStats` request is pending, false otherwise
*/
isReportStatsRequesting( state, ...args ) {
return select( 'core/data' ).isResolving( 'wc-admin', 'getReportStats', args );
},
/**
* Returns true if a report stat request has returned an error.
*
* @param {Object} state Current state
* @param {String} endpoint Stats endpoint
* @param {Object} query Report query parameters
* @return {Boolean} True if the `getReportStats` request has failed, false otherwise
*/
isReportStatsError( state, endpoint, query ) {
return ERROR === getReportStats( state, endpoint, query );
},
};

View File

@ -1,164 +0,0 @@
/**
* @format
*/
/**
* External dependencies
*/
import deepFreeze from 'deep-freeze';
/**
* Internal dependencies
*/
import { ERROR } from 'store/constants';
import reportStatsReducer from '../reducer';
import { getJsonString } from 'store/utils';
describe( 'reportStatsReducer()', () => {
it( 'returns an empty data object by default', () => {
const state = reportStatsReducer( undefined, {} );
expect( state ).toEqual( {} );
} );
it( 'returns with received report data', () => {
const originalState = deepFreeze( {} );
const query = {
after: '2018-01-04T00:00:00+00:00',
before: '2018-07-14T00:00:00+00:00',
interval: 'day',
};
const report = {
totals: {
orders_count: 10,
num_items_sold: 9,
},
interval: [ 0, 1, 2 ],
};
const endpoint = 'revenue';
const state = reportStatsReducer( originalState, {
type: 'SET_REPORT_STATS',
endpoint,
query,
report,
totalResults: 3,
totalPages: 1,
} );
const queryKey = getJsonString( query );
expect( state[ endpoint ][ queryKey ] ).toEqual( {
data: { ...report },
totalResults: 3,
totalPages: 1,
} );
} );
it( 'tracks multiple queries per endpoint in report data', () => {
const otherQuery = {
after: '2018-01-04T00:00:00+00:00',
before: '2018-07-14T00:00:00+00:00',
interval: 'week',
};
const otherQueryKey = getJsonString( otherQuery );
const otherQueryState = {
revenue: {
[ otherQueryKey ]: { data: { totals: 1000 } },
},
};
const originalState = deepFreeze( otherQueryState );
const query = {
after: '2018-01-04T00:00:00+00:00',
before: '2018-07-14T00:00:00+00:00',
interval: 'day',
};
const report = {
totals: {
orders_count: 10,
num_items_sold: 9,
},
interval: [ 0, 1, 2 ],
};
const endpoint = 'revenue';
const state = reportStatsReducer( originalState, {
type: 'SET_REPORT_STATS',
endpoint,
query,
report,
totalResults: 3,
totalPages: 1,
} );
const queryKey = getJsonString( query );
expect( state[ endpoint ][ queryKey ] ).toEqual( {
data: { ...report },
totalResults: 3,
totalPages: 1,
} );
expect( state[ endpoint ][ otherQueryKey ].data.totals ).toEqual( 1000 );
} );
it( 'tracks multiple endpoints in report data', () => {
const productsQuery = {
after: '2018-01-04T00:00:00+00:00',
before: '2018-07-14T00:00:00+00:00',
interval: 'week',
};
const productsQueryKey = getJsonString( productsQuery );
const productsQueryState = {
products: {
[ productsQueryKey ]: { data: { totals: 1999 } },
},
};
const originalState = deepFreeze( productsQueryState );
const query = {
after: '2018-01-04T00:00:00+00:00',
before: '2018-07-14T00:00:00+00:00',
interval: 'day',
};
const report = {
totals: {
orders_count: 10,
num_items_sold: 9,
},
interval: [ 0, 1, 2 ],
};
const endpoint = 'revenue';
const state = reportStatsReducer( originalState, {
type: 'SET_REPORT_STATS',
endpoint,
query,
report,
totalResults: 3,
totalPages: 1,
} );
const queryKey = getJsonString( query );
expect( state[ endpoint ][ queryKey ] ).toEqual( {
data: { ...report },
totalResults: 3,
totalPages: 1,
} );
expect( state.products[ productsQueryKey ].data.totals ).toEqual( 1999 );
} );
it( 'returns with received error data', () => {
const originalState = deepFreeze( {} );
const query = {
after: '2018-01-04T00:00:00+00:00',
before: '2018-07-14T00:00:00+00:00',
interval: 'day',
};
const endpoint = 'revenue';
const state = reportStatsReducer( originalState, {
type: 'SET_REPORT_STATS_ERROR',
endpoint,
query,
} );
const queryKey = getJsonString( query );
expect( state[ endpoint ][ queryKey ] ).toEqual( ERROR );
} );
} );

View File

@ -1,107 +0,0 @@
/*
* @format
*/
/**
* External dependencies
*/
import apiFetch from '@wordpress/api-fetch';
import { dispatch } from '@wordpress/data';
/**
* Internal dependencies
*/
import resolvers from '../resolvers';
const { getReportStats } = resolvers;
jest.mock( '@wordpress/data', () => ( {
dispatch: jest.fn().mockReturnValue( {
setReportStats: jest.fn(),
} ),
} ) );
jest.mock( '@wordpress/api-fetch', () => jest.fn() );
describe( 'getReportStats', () => {
const REPORT_1 = {
totals: {
orders_count: 10,
num_items_sold: 9,
},
interval: [ 0, 1, 2 ],
};
const REPORT_1_TOTALS = {
'x-wp-total': 10,
'x-wp-totalpages': 2,
};
const REPORT_2 = {
totals: {
orders_count: 5,
items_sold: 5,
gross_revenue: 999.99,
},
intervals: [
{
interval: 'week',
subtotals: {},
},
],
};
const REPORT_2_TOTALS = {
'x-wp-total': 20,
'x-wp-totalpages': 4,
};
beforeAll( () => {
apiFetch.mockImplementation( options => {
if ( options.path === '/wc/v4/reports/revenue/stats' ) {
return Promise.resolve( {
headers: {
get: header => REPORT_1_TOTALS[ header ],
},
json: () => Promise.resolve( REPORT_1 ),
} );
}
if ( options.path === '/wc/v4/reports/products/stats?interval=week' ) {
return Promise.resolve( {
headers: {
get: header => REPORT_2_TOTALS[ header ],
},
json: () => Promise.resolve( REPORT_2 ),
} );
}
} );
} );
it( 'returns requested report data', async () => {
expect.assertions( 1 );
const endpoint = 'revenue';
await getReportStats( endpoint, undefined );
expect( dispatch().setReportStats ).toHaveBeenCalledWith(
endpoint,
REPORT_1,
undefined,
REPORT_1_TOTALS[ 'x-wp-total' ],
REPORT_1_TOTALS[ 'x-wp-totalpages' ]
);
} );
it( 'returns requested report data for a specific query', async () => {
expect.assertions( 1 );
const endpoint = 'products';
const query = { interval: 'week' };
await getReportStats( endpoint, query );
expect( dispatch().setReportStats ).toHaveBeenCalledWith(
endpoint,
REPORT_2,
query,
REPORT_2_TOTALS[ 'x-wp-total' ],
REPORT_2_TOTALS[ 'x-wp-totalpages' ]
);
} );
} );

View File

@ -1,102 +0,0 @@
/*
* @format
*/
/**
* External dependencies
*/
import deepFreeze from 'deep-freeze';
import { select } from '@wordpress/data';
/**
* Internal dependencies
*/
import { ERROR } from 'store/constants';
import selectors from '../selectors';
const { getReportStats, isReportStatsRequesting, isReportStatsError } = selectors;
jest.mock( '@wordpress/data', () => ( {
...require.requireActual( '@wordpress/data' ),
select: jest.fn().mockReturnValue( {} ),
} ) );
const endpointName = 'revenue';
describe( 'getReportStats()', () => {
it( 'returns null when no report data is available', () => {
const state = deepFreeze( {} );
expect( getReportStats( state, endpointName ) ).toEqual( null );
} );
it( 'returns stored report information by endpoint and query combination', () => {
const report = {
totals: {
orders_count: 10,
num_items_sold: 9,
},
interval: [ 0, 1, 2 ],
};
const state = deepFreeze( {
reports: {
stats: {
revenue: {
'{}': { ...report },
},
},
},
} );
expect( getReportStats( state, endpointName ) ).toEqual( report );
} );
} );
describe( 'isReportStatsRequesting()', () => {
beforeAll( () => {
select( 'core/data' ).isResolving = jest.fn().mockReturnValue( false );
} );
afterAll( () => {
select( 'core/data' ).isResolving.mockRestore();
} );
function setIsResolving( isResolving ) {
select( 'core/data' ).isResolving.mockImplementation(
( reducerKey, selectorName ) =>
isResolving && reducerKey === 'wc-admin' && selectorName === 'getReportStats'
);
}
it( 'returns false if never requested', () => {
const result = isReportStatsRequesting( endpointName );
expect( result ).toBe( false );
} );
it( 'returns false if request finished', () => {
setIsResolving( false );
const result = isReportStatsRequesting( endpointName );
expect( result ).toBe( false );
} );
it( 'returns true if requesting', () => {
setIsResolving( true );
const result = isReportStatsRequesting( endpointName );
expect( result ).toBe( true );
} );
} );
describe( 'isReportStatsError()', () => {
it( 'returns false by default', () => {
const state = deepFreeze( {} );
expect( isReportStatsError( state, endpointName ) ).toEqual( false );
} );
it( 'returns true if ERROR constant is found', () => {
const state = deepFreeze( {
reports: {
stats: {
revenue: {
'{}': ERROR,
},
},
},
} );
expect( isReportStatsError( state, endpointName ) ).toEqual( true );
} );
} );

View File

@ -1,598 +0,0 @@
/*
* @format
*/
/**
* Internal dependencies
*/
import {
isReportDataEmpty,
getReportChartData,
getSummaryNumbers,
getFilterQuery,
getReportTableData,
timeStampFilterDates,
} from '../utils';
import * as ordersConfig from 'analytics/report/orders/config';
jest.mock( '../utils', () => ( {
...require.requireActual( '../utils' ),
getReportTableQuery: () => ( {
after: '2018-10-10',
before: '2018-10-10',
} ),
} ) );
describe( 'isReportDataEmpty()', () => {
it( 'returns false if report is valid', () => {
const report = {
data: {
totals: {
orders_count: 10,
num_items_sold: 9,
},
intervals: [ 0, 1, 2 ],
},
};
expect( isReportDataEmpty( report ) ).toEqual( false );
} );
it( 'returns true if report object is undefined', () => {
expect( isReportDataEmpty( undefined ) ).toEqual( true );
} );
it( 'returns true if data response object is missing', () => {
expect( isReportDataEmpty( {} ) ).toEqual( true );
} );
it( 'returns true if totals response object is missing', () => {
expect( isReportDataEmpty( { data: {} } ) ).toEqual( true );
} );
it( 'returns true if intervals response object is empty', () => {
expect( isReportDataEmpty( { data: { intervals: [], totals: 2 } } ) ).toEqual( true );
} );
} );
describe( 'getReportChartData()', () => {
const select = jest.fn().mockReturnValue( {} );
const response = {
isEmpty: false,
isError: false,
isRequesting: false,
data: {
totals: null,
intervals: [],
},
};
beforeAll( () => {
select( 'wc-api' ).getReportStats = jest.fn().mockReturnValue( {} );
select( 'wc-api' ).isReportStatsRequesting = jest.fn().mockReturnValue( false );
select( 'wc-api' ).getReportStatsError = jest.fn().mockReturnValue( false );
} );
afterAll( () => {
select( 'wc-api' ).getReportStats.mockRestore();
select( 'wc-api' ).isReportStatsRequesting.mockRestore();
select( 'wc-api' ).getReportStatsError.mockRestore();
} );
function setGetReportStats( func ) {
select( 'wc-api' ).getReportStats.mockImplementation( ( ...args ) => func( ...args ) );
}
function setIsReportStatsRequesting( func ) {
select( 'wc-api' ).isReportStatsRequesting.mockImplementation( ( ...args ) => func( ...args ) );
}
function setGetReportStatsError( func ) {
select( 'wc-api' ).getReportStatsError.mockImplementation( ( ...args ) => func( ...args ) );
}
it( 'returns isRequesting if first request is in progress', () => {
setIsReportStatsRequesting( () => {
return true;
} );
const result = getReportChartData( 'revenue', 'primary', {}, select );
expect( result ).toEqual( { ...response, isRequesting: true } );
} );
it( 'returns isError if first request errors', () => {
setIsReportStatsRequesting( () => {
return false;
} );
setGetReportStatsError( () => {
return { error: 'Error' };
} );
const result = getReportChartData( 'revenue', 'primary', {}, select );
expect( result ).toEqual( { ...response, isError: true } );
} );
it( 'returns results after single page of data', () => {
const data = {
totals: {
orders_count: 115,
gross_revenue: 13966.92,
},
intervals: [
{
interval: 'day',
date_start: '2018-07-01 00:00:00',
subtotals: {
orders_count: 115,
gross_revenue: 13966.92,
},
},
],
};
setIsReportStatsRequesting( () => {
return false;
} );
setGetReportStatsError( () => {
return undefined;
} );
setGetReportStats( () => {
return {
totalResults: 1,
data,
};
} );
const result = getReportChartData( 'revenue', 'primary', {}, select );
expect( result ).toEqual( { ...response, data: { ...data } } );
} );
it( 'returns combined results for multiple pages of data', () => {
const totalResults = 110;
const orders_count = 115;
const gross_revenue = 13966.92;
const intervals = [];
for ( let i = 0; i < totalResults; i++ ) {
intervals.push( {
interval: 'day',
date_start: '2018-07-01 00:00:00',
subtotals: { orders_count, gross_revenue },
} );
}
const totals = {
orders_count: orders_count * totalResults,
gross_revenue: gross_revenue * totalResults,
};
setIsReportStatsRequesting( () => {
return false;
} );
setGetReportStatsError( () => {
return undefined;
} );
setGetReportStats( ( endpoint, query ) => {
if ( 2 === query.page ) {
return {
totalResults,
data: {
totals,
intervals: intervals.slice( 100, 110 ),
},
};
}
return {
totalResults,
data: {
totals,
intervals: intervals.slice( 0, 100 ),
},
};
} );
const actualResponse = getReportChartData( 'revenue', 'primary', {}, select );
const expectedResponse = {
...response,
data: {
totals,
intervals,
},
};
expect( actualResponse ).toEqual( expectedResponse );
} );
it( 'returns isRequesting if additional requests are in progress', () => {
setIsReportStatsRequesting( ( endpoint, query ) => {
if ( 2 === query.page ) {
return true;
}
return false;
} );
setGetReportStatsError( () => {
return undefined;
} );
const result = getReportChartData( 'revenue', 'primary', {}, select );
expect( result ).toEqual( { ...response, isRequesting: true } );
} );
it( 'returns isError if additional requests return an error', () => {
setIsReportStatsRequesting( () => {
return false;
} );
setGetReportStatsError( ( endpoint, query ) => {
if ( 2 === query.page ) {
return { error: 'Error' };
}
return undefined;
} );
const result = getReportChartData( 'revenue', 'primary', {}, select );
expect( result ).toEqual( { ...response, isError: true } );
} );
it( 'returns empty state if a query returns no data', () => {
setIsReportStatsRequesting( () => {
return false;
} );
setGetReportStatsError( () => {
return undefined;
} );
setGetReportStats( () => {
return {
totalResults: undefined,
data: {},
};
} );
const result = getReportChartData( 'revenue', 'primary', {}, select );
expect( result ).toEqual( { ...response, isEmpty: true } );
} );
} );
describe( 'getSummaryNumbers()', () => {
const select = jest.fn().mockReturnValue( {} );
const response = {
isError: false,
isRequesting: false,
totals: {
primary: null,
secondary: null,
},
};
const query = {
after: '2018-10-10',
before: '2018-10-10',
period: 'custom',
compare: 'previous_period',
};
beforeAll( () => {
select( 'wc-api' ).getReportStats = jest.fn().mockReturnValue( {} );
select( 'wc-api' ).isReportStatsRequesting = jest.fn().mockReturnValue( false );
select( 'wc-api' ).getReportStatsError = jest.fn().mockReturnValue( false );
} );
afterAll( () => {
select( 'wc-api' ).getReportStats.mockRestore();
select( 'wc-api' ).isReportStatsRequesting.mockRestore();
select( 'wc-api' ).getReportStatsError.mockRestore();
} );
function setGetReportStats( func ) {
select( 'wc-api' ).getReportStats.mockImplementation( ( ...args ) => func( ...args ) );
}
function setIsReportStatsRequesting( func ) {
select( 'wc-api' ).isReportStatsRequesting.mockImplementation( ( ...args ) => func( ...args ) );
}
function setGetReportStatsError( func ) {
select( 'wc-api' ).getReportStatsError.mockImplementation( ( ...args ) => func( ...args ) );
}
it( 'returns isRequesting if a request is in progress', () => {
setIsReportStatsRequesting( () => {
return true;
} );
const result = getSummaryNumbers( 'revenue', query, select );
expect( result ).toEqual( { ...response, isRequesting: true } );
} );
it( 'returns isError if request errors', () => {
setIsReportStatsRequesting( () => {
return false;
} );
setGetReportStatsError( () => {
return { error: 'Error' };
} );
const result = getSummaryNumbers( 'revenue', query, select );
expect( result ).toEqual( { ...response, isError: true } );
} );
it( 'returns results after queries finish', () => {
const totals = {
primary: {
orders_count: 115,
gross_revenue: 13966.92,
},
secondary: {
orders_count: 85,
gross_revenue: 10406.1,
},
};
setIsReportStatsRequesting( () => {
return false;
} );
setGetReportStatsError( () => {
return undefined;
} );
setGetReportStats( () => {
return {
totals,
};
} );
setGetReportStats( ( endpoint, _query ) => {
if ( '2018-10-10T00:00:00+00:00' === _query.after ) {
return {
data: {
totals: totals.primary,
intervals: [],
},
};
}
return {
data: {
totals: totals.secondary,
intervals: [],
},
};
} );
const result = getSummaryNumbers( 'revenue', query, select );
expect( result ).toEqual( { ...response, totals } );
} );
} );
describe( 'getFilterQuery', () => {
/**
* Mock the orders config
*/
const filters = [
{
param: 'filter',
filters: [
{ value: 'top_meal', query: { lunch: 'burritos' } },
{ value: 'top_dessert', query: { dinner: 'ice_cream' } },
{ value: 'compare-cuisines', settings: { param: 'region' } },
{
value: 'food_destination',
subFilters: [
{ value: 'choose_a_european_city', settings: { param: 'european_cities' } },
],
},
],
},
];
const advancedFilters = {
filters: {
mexican: {
rules: [ { value: 'is' }, { value: 'is_not' } ],
},
french: {
rules: [ { value: 'includes' }, { value: 'excludes' } ],
},
},
};
ordersConfig.filters = filters;
ordersConfig.advancedFilters = advancedFilters;
it( 'should return an empty object if no filter param is given', () => {
const query = {};
const filterQuery = getFilterQuery( 'orders', query );
expect( filterQuery ).toEqual( {} );
} );
it( 'should return an empty object if filter parameter is not in configs', () => {
const query = { filter: 'canned_meat' };
const filterQuery = getFilterQuery( 'orders', query );
expect( filterQuery ).toEqual( {} );
} );
it( 'should return the query for an advanced filter', () => {
const query = { filter: 'advanced', mexican_is: 'delicious' };
const filterQuery = getFilterQuery( 'orders', query );
expect( filterQuery ).toEqual( { mexican_is: 'delicious', match: 'all' } );
} );
it( 'should ignore other queries not defined by filter configs', () => {
const query = {
filter: 'advanced',
mexican_is_not: 'healthy',
orderby: 'calories',
topping: 'salsa-verde',
};
const filterQuery = getFilterQuery( 'orders', query );
expect( filterQuery ).toEqual( { mexican_is_not: 'healthy', match: 'all' } );
} );
it( 'should apply the match parameter advanced filters', () => {
const query = {
filter: 'advanced',
french_includes: 'le-fromage',
match: 'any',
};
const filterQuery = getFilterQuery( 'orders', query );
expect( filterQuery ).toEqual( { french_includes: 'le-fromage', match: 'any' } );
} );
it( 'should return the query for compare filters', () => {
const query = { filter: 'compare-cuisines', region: 'vietnam,malaysia,thailand' };
const filterQuery = getFilterQuery( 'orders', query );
expect( filterQuery ).toEqual( { region: 'vietnam,malaysia,thailand' } );
} );
it( 'should return the query for subFilters', () => {
const query = { filter: 'choose_a_european_city', european_cities: 'paris,rome,barcelona' };
const filterQuery = getFilterQuery( 'orders', query );
expect( filterQuery ).toEqual( { european_cities: 'paris,rome,barcelona' } );
} );
} );
describe( 'getReportTableData()', () => {
const select = jest.fn().mockReturnValue( {} );
const response = {
isError: false,
isRequesting: false,
items: {
data: [],
},
};
const query = {
after: '2018-10-10',
before: '2018-10-10',
};
beforeAll( () => {
select( 'wc-api' ).getReportItems = jest.fn().mockReturnValue( {} );
select( 'wc-api' ).isReportItemsRequesting = jest.fn().mockReturnValue( false );
select( 'wc-api' ).getReportItemsError = jest.fn().mockReturnValue( undefined );
} );
afterAll( () => {
select( 'wc-api' ).getReportItems.mockRestore();
select( 'wc-api' ).isReportItemsRequesting.mockRestore();
select( 'wc-api' ).getReportItemsError.mockRestore();
} );
function setGetReportItems( func ) {
select( 'wc-api' ).getReportItems.mockImplementation( ( ...args ) => func( ...args ) );
}
function setIsReportItemsRequesting( func ) {
select( 'wc-api' ).isReportItemsRequesting.mockImplementation( ( ...args ) => func( ...args ) );
}
function setGetReportItemsError( func ) {
select( 'wc-api' ).getReportItemsError.mockImplementation( ( ...args ) => func( ...args ) );
}
it( 'returns isRequesting if a request is in progress', () => {
setIsReportItemsRequesting( () => true );
const result = getReportTableData( 'coupons', query, select );
expect( result ).toEqual( { ...response, query, isRequesting: true } );
expect( select( 'wc-api' ).getReportItems ).toHaveBeenLastCalledWith( 'coupons', query );
expect( select( 'wc-api' ).isReportItemsRequesting ).toHaveBeenLastCalledWith(
'coupons',
query
);
expect( select( 'wc-api' ).getReportItemsError ).toHaveBeenCalledTimes( 0 );
} );
it( 'returns isError if request errors', () => {
setIsReportItemsRequesting( () => false );
setGetReportItemsError( () => ( { error: 'Error' } ) );
const result = getReportTableData( 'coupons', query, select );
expect( result ).toEqual( { ...response, query, isError: true } );
expect( select( 'wc-api' ).getReportItems ).toHaveBeenLastCalledWith( 'coupons', query );
expect( select( 'wc-api' ).isReportItemsRequesting ).toHaveBeenLastCalledWith(
'coupons',
query
);
expect( select( 'wc-api' ).getReportItemsError ).toHaveBeenLastCalledWith( 'coupons', query );
} );
it( 'returns results after queries finish', () => {
const items = [ { id: 1 }, { id: 2 }, { id: 3 } ];
setIsReportItemsRequesting( () => false );
setGetReportItemsError( () => undefined );
setGetReportItems( () => items );
const result = getReportTableData( 'coupons', query, select );
expect( result ).toEqual( { ...response, query, items } );
expect( select( 'wc-api' ).getReportItems ).toHaveBeenLastCalledWith( 'coupons', query );
expect( select( 'wc-api' ).isReportItemsRequesting ).toHaveBeenLastCalledWith(
'coupons',
query
);
expect( select( 'wc-api' ).getReportItemsError ).toHaveBeenLastCalledWith( 'coupons', query );
} );
} );
describe( 'timeStampFilterDates', () => {
const advancedFilters = {
filters: {
city: {
input: { component: 'Search' },
},
my_date: {
input: { component: 'Date' },
},
},
};
it( 'should not change activeFilters not using the Date component', () => {
const activeFilter = {
key: 'name',
rule: 'is',
value: 'New York',
};
const timeStampedActiveFilter = timeStampFilterDates( advancedFilters, activeFilter );
expect( timeStampedActiveFilter ).toMatchObject( activeFilter );
} );
it( 'should append timestamps to activeFilters using the Date component', () => {
const activeFilter = {
key: 'my_date',
rule: 'after',
value: '2018-04-04',
};
const timeStampedActiveFilter = timeStampFilterDates( advancedFilters, activeFilter );
expect( timeStampedActiveFilter.value ).toBe( '2018-04-04T00:00:00+00:00' );
} );
it( 'should append start of day for "after" rule', () => {
const activeFilter = {
key: 'my_date',
rule: 'after',
value: '2018-04-04',
};
const timeStampedActiveFilter = timeStampFilterDates( advancedFilters, activeFilter );
expect( timeStampedActiveFilter.value ).toBe( '2018-04-04T00:00:00+00:00' );
} );
it( 'should append end of day for "before" rule', () => {
const activeFilter = {
key: 'my_date',
rule: 'before',
value: '2018-04-04',
};
const timeStampedActiveFilter = timeStampFilterDates( advancedFilters, activeFilter );
expect( timeStampedActiveFilter.value ).toBe( '2018-04-04T23:59:59+00:00' );
} );
it( 'should handle "between" values', () => {
const activeFilter = {
key: 'my_date',
rule: 'before',
value: [ '2018-04-04', '2018-04-10' ],
};
const timeStampedActiveFilter = timeStampFilterDates( advancedFilters, activeFilter );
expect( Array.isArray( timeStampedActiveFilter.value ) ).toBe( true );
expect( timeStampedActiveFilter.value ).toHaveLength( 2 );
expect( timeStampedActiveFilter.value[ 0 ] ).toContain( 'T00:00:00+00:00' );
expect( timeStampedActiveFilter.value[ 1 ] ).toContain( 'T23:59:59+00:00' );
} );
} );

View File

@ -1,11 +0,0 @@
/** @format */
/**
* Returns a string representation of a sorted query object.
*
* @param {Object} query Current state
* @return {String} Query Key
*/
export function getJsonString( query = {} ) {
return JSON.stringify( query, Object.keys( query ).sort() );
}

View File

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

View File

@ -1,56 +0,0 @@
/** @format */
/**
* External dependencies
*/
import { isNil } from 'lodash';
/**
* Internal dependencies
*/
import { getResourceName } from '../utils';
import { DEFAULT_REQUIREMENT } from '../constants';
const getCategories = ( getResource, requireResource ) => (
query = {},
requirement = DEFAULT_REQUIREMENT
) => {
const resourceName = getResourceName( 'category-query', query );
const ids = requireResource( requirement, resourceName ).data || [];
const categories = ids.reduce(
( acc, id ) => ( {
...acc,
[ id ]: getResource( getResourceName( 'category', id ) ).data || {},
} ),
{}
);
return categories;
};
const getCategoriesTotalCount = getResource => ( query = {} ) => {
const resourceName = getResourceName( 'category-query', query );
return getResource( resourceName ).totalCount || 0;
};
const getCategoriesError = getResource => ( query = {} ) => {
const resourceName = getResourceName( 'category-query', query );
return getResource( resourceName ).error;
};
const isGetCategoriesRequesting = getResource => ( query = {} ) => {
const resourceName = getResourceName( 'category-query', query );
const { lastRequested, lastReceived } = getResource( resourceName );
if ( isNil( lastRequested ) || isNil( lastReceived ) ) {
return true;
}
return lastRequested > lastReceived;
};
export default {
getCategories,
getCategoriesError,
getCategoriesTotalCount,
isGetCategoriesRequesting,
};

View File

@ -6,6 +6,9 @@ import { MINUTE } from '@fresh-data/framework';
export const NAMESPACE = '/wc/v4';
// TODO: Remove once swagger endpoints are phased out.
export const SWAGGERNAMESPACE = 'https://virtserver.swaggerhub.com/peterfabian/wc-v3-api/1.0.0/';
export const DEFAULT_REQUIREMENT = {
timeout: 1 * MINUTE,
freshness: 5 * MINUTE,

View File

@ -0,0 +1,66 @@
/** @format */
/**
* External dependencies
*/
import apiFetch from '@wordpress/api-fetch';
/**
* WooCommerce dependencies
*/
import { stringifyQuery } from '@woocommerce/navigation';
/**
* Internal dependencies
*/
import { getResourceIdentifier, getResourcePrefix, getResourceName } from '../utils';
import { NAMESPACE } from '../constants';
const typeEndpointMap = {
'items-query-categories': 'products/categories',
'items-query-customers': 'customers',
'items-query-coupons': 'coupons',
'items-query-orders': 'orders',
'items-query-products': 'products',
'items-query-taxes': 'taxes',
};
function read( resourceNames, fetch = apiFetch ) {
const filteredNames = resourceNames.filter( name => {
const prefix = getResourcePrefix( name );
return Boolean( typeEndpointMap[ prefix ] );
} );
return filteredNames.map( async resourceName => {
const prefix = getResourcePrefix( resourceName );
const endpoint = typeEndpointMap[ prefix ];
const query = getResourceIdentifier( resourceName );
const url = NAMESPACE + `/${ endpoint }${ stringifyQuery( query ) }`;
try {
const items = await fetch( {
path: url,
} );
const ids = items.map( item => item.id );
const itemResources = items.reduce( ( resources, item ) => {
resources[ getResourceName( `${ prefix }-item`, item.id ) ] = { data: item };
return resources;
}, {} );
return {
[ resourceName ]: {
data: ids,
totalCount: ids.length,
},
...itemResources,
};
} catch ( error ) {
return { [ resourceName ]: { error } };
}
} );
}
export default {
read,
};

View File

@ -0,0 +1,54 @@
/** @format */
/**
* External dependencies
*/
import { isNil } from 'lodash';
/**
* Internal dependencies
*/
import { getResourceName } from '../utils';
import { DEFAULT_REQUIREMENT } from '../constants';
const getItems = ( getResource, requireResource ) => (
type,
query = {},
requirement = DEFAULT_REQUIREMENT
) => {
const resourceName = getResourceName( `items-query-${ type }`, query );
const ids = requireResource( requirement, resourceName ).data || [];
const items = new Map();
ids.forEach( id => {
items.set( id, getResource( getResourceName( `items-query-${ type }-item`, id ) ).data );
} );
return items;
};
const getItemsTotalCount = getResource => ( type, query = {} ) => {
const resourceName = getResourceName( `items-query-${ type }`, query );
return getResource( resourceName ).totalCount || 0;
};
const getItemsError = getResource => ( type, query = {} ) => {
const resourceName = getResourceName( `items-query-${ type }`, query );
return getResource( resourceName ).error;
};
const isGetItemsRequesting = getResource => ( type, query = {} ) => {
const resourceName = getResourceName( `items-query-${ type }`, query );
const { lastRequested, lastReceived } = getResource( resourceName );
if ( isNil( lastRequested ) || isNil( lastReceived ) ) {
return true;
}
return lastRequested > lastReceived;
};
export default {
getItems,
getItemsError,
getItemsTotalCount,
isGetItemsRequesting,
};

View File

@ -0,0 +1,31 @@
/** @format */
/**
* External dependencies
*/
/**
* Returns items based on a search query.
*
* @param {Object} select Instance of @wordpress/select
* @param {String} endpoint Report API Endpoint
* @param {String} search Search strings separated by commas.
* @return {Object} Object Object containing the matching items.
*/
export function searchItemsByString( select, endpoint, search ) {
const { getItems } = select( 'wc-api' );
const searchWords = search.split( ',' );
const items = {};
searchWords.forEach( searchWord => {
const newItems = getItems( endpoint, {
search: searchWord,
per_page: 10,
} );
newItems.forEach( ( item, id ) => {
items[ id ] = item;
} );
} );
return items;
}

View File

@ -1,76 +0,0 @@
/** @format */
/**
* External dependencies
*/
import apiFetch from '@wordpress/api-fetch';
/**
* WooCommerce dependencies
*/
import { stringifyQuery } from '@woocommerce/navigation';
/**
* Internal dependencies
*/
import { isResourcePrefix, getResourceIdentifier, getResourceName } from '../utils';
import { NAMESPACE } from '../constants';
function read( resourceNames, fetch = apiFetch ) {
return [ ...readOrders( resourceNames, fetch ), ...readOrderQueries( resourceNames, fetch ) ];
}
function readOrderQueries( resourceNames, fetch ) {
const filteredNames = resourceNames.filter( name => isResourcePrefix( name, 'order-query' ) );
return filteredNames.map( async resourceName => {
const query = getResourceIdentifier( resourceName );
const url = `${ NAMESPACE }/orders${ stringifyQuery( query ) }`;
try {
const response = await fetch( {
parse: false,
path: url,
} );
const orders = await response.json();
const totalCount = parseInt( response.headers.get( 'x-wp-total' ) );
const ids = orders.map( order => order.id );
const orderResources = orders.reduce( ( resources, order ) => {
resources[ getResourceName( 'order', order.id ) ] = { data: order };
return resources;
}, {} );
return {
[ resourceName ]: {
data: ids,
totalCount,
},
...orderResources,
};
} catch ( error ) {
return { [ resourceName ]: { error } };
}
} );
}
function readOrders( resourceNames, fetch ) {
const filteredNames = resourceNames.filter( name => isResourcePrefix( name, 'order' ) );
return filteredNames.map( resourceName => readOrder( resourceName, fetch ) );
}
function readOrder( resourceName, fetch ) {
const id = getResourceIdentifier( resourceName );
const url = `${ NAMESPACE }/orders/${ id }`;
return fetch( { path: url } )
.then( order => {
return { [ resourceName ]: { data: order } };
} )
.catch( error => {
return { [ resourceName ]: { error } };
} );
}
export default {
read,
};

View File

@ -1,53 +0,0 @@
/** @format */
/**
* External dependencies
*/
import { isNil } from 'lodash';
/**
* Internal dependencies
*/
import { getResourceName } from '../utils';
import { DEFAULT_REQUIREMENT } from '../constants';
const getOrders = ( getResource, requireResource ) => (
query = {},
requirement = DEFAULT_REQUIREMENT
) => {
const resourceName = getResourceName( 'order-query', query );
const ids = requireResource( requirement, resourceName ).data || [];
const orders = ids.map( id => getResource( getResourceName( 'order', id ) ).data || {} );
return orders;
};
const getOrdersError = getResource => ( query = {} ) => {
const resourceName = getResourceName( 'order-query', query );
return getResource( resourceName ).error;
};
const getOrdersTotalCount = ( getResource, requireResource ) => (
query = {},
requirement = DEFAULT_REQUIREMENT
) => {
const resourceName = getResourceName( 'order-query', query );
return requireResource( requirement, resourceName ).totalCount || 0;
};
const isGetOrdersRequesting = getResource => ( query = {} ) => {
const resourceName = getResourceName( 'order-query', query );
const { lastRequested, lastReceived } = getResource( resourceName );
if ( isNil( lastRequested ) || isNil( lastReceived ) ) {
return true;
}
return lastRequested > lastReceived;
};
export default {
getOrders,
getOrdersError,
getOrdersTotalCount,
isGetOrdersRequesting,
};

View File

@ -12,11 +12,18 @@ import { stringifyQuery } from '@woocommerce/navigation';
/**
* Internal dependencies
*/
import { getResourceIdentifier, getResourcePrefix } from '../../utils';
import { NAMESPACE } from '../../constants';
import { SWAGGERNAMESPACE } from 'store/constants';
import { getResourceIdentifier, getResourcePrefix } from 'wc-api/utils';
import { NAMESPACE, SWAGGERNAMESPACE } from 'wc-api/constants';
const statEndpoints = [ 'coupons', 'downloads', 'orders', 'products', 'revenue', 'taxes' ];
const statEndpoints = [
'coupons',
'downloads',
'orders',
'products',
'revenue',
'taxes',
'customers',
];
// TODO: Remove once swagger endpoints are phased out.
const swaggerEndpoints = [ 'categories' ];
@ -28,6 +35,7 @@ const typeEndpointMap = {
'report-stats-query-downloads': 'downloads',
'report-stats-query-coupons': 'coupons',
'report-stats-query-taxes': 'taxes',
'report-stats-query-customers': 'customers',
};
function read( resourceNames, fetch = apiFetch ) {

View File

@ -16,7 +16,7 @@ import { formatCurrency } from '@woocommerce/currency';
/**
* Internal dependencies
*/
import { MAX_PER_PAGE, QUERY_DEFAULTS } from 'store/constants';
import { MAX_PER_PAGE, QUERY_DEFAULTS } from 'wc-api/constants';
import * as categoriesConfig from 'analytics/report/categories/config';
import * as couponsConfig from 'analytics/report/coupons/config';
import * as customersConfig from 'analytics/report/customers/config';
@ -37,6 +37,12 @@ const reportConfigs = {
};
export function getFilterQuery( endpoint, query ) {
if ( query.search ) {
return {
[ endpoint ]: query[ endpoint ],
};
}
if ( reportConfigs[ endpoint ] ) {
const { filters = [], advancedFilters = {} } = reportConfigs[ endpoint ];
return filters
@ -335,7 +341,7 @@ export function getReportTableQuery( endpoint, urlQuery, query ) {
*
* @param {String} endpoint Report API Endpoint
* @param {Object} urlQuery Query parameters in the url
* @param {object} select Instance of @wordpress/select
* @param {Object} select Instance of @wordpress/select
* @param {Object} query Query parameters specific for that endpoint
* @return {Object} Object Table data response
*/

View File

@ -4,8 +4,10 @@
*/
import operations from './operations';
import selectors from './selectors';
import mutations from './mutations';
export default {
operations,
selectors,
mutations,
};

View File

@ -0,0 +1,10 @@
/** @format */
const updateSettings = operations => settingFields => {
const resourceKey = 'settings';
operations.update( [ resourceKey ], { [ resourceKey ]: settingFields } );
};
export default {
updateSettings,
};

View File

@ -0,0 +1,71 @@
/** @format */
/**
* External dependencies
*/
import apiFetch from '@wordpress/api-fetch';
import { pick } from 'lodash';
/**
* Internal dependencies
*/
import { NAMESPACE } from '../constants';
function read( resourceNames, fetch = apiFetch ) {
return [ ...readSettings( resourceNames, fetch ) ];
}
function update( resourceNames, data, fetch = apiFetch ) {
return [ ...updateSettings( resourceNames, data, fetch ) ];
}
function readSettings( resourceNames, fetch ) {
if ( resourceNames.includes( 'settings' ) ) {
const url = NAMESPACE + '/settings/wc_admin';
return [
fetch( { path: url } )
.then( settingsToSettingsResource )
.catch( error => {
return { [ 'settings' ]: { error: String( error.message ) } };
} ),
];
}
return [];
}
function updateSettings( resourceNames, data, fetch ) {
const resourceName = 'settings';
const settingsFields = [ 'woocommerce_excluded_report_order_statuses' ];
if ( resourceNames.includes( resourceName ) ) {
const url = NAMESPACE + '/settings/wc_admin/';
const settingsData = pick( data[ resourceName ], settingsFields );
const promises = Object.keys( settingsData ).map( setting => {
return fetch( {
path: url + setting,
method: 'POST',
data: { value: settingsData[ setting ] },
} )
.then( settingsToSettingsResource )
.catch( error => {
return { [ resourceName ]: { error } };
} );
} );
return [ promises ];
}
return [];
}
function settingsToSettingsResource( settings ) {
const settingsData = {};
settings.forEach( setting => ( settingsData[ setting.id ] = setting.value ) );
return { [ 'settings' ]: { data: settingsData } };
}
export default {
read,
update,
};

View File

@ -0,0 +1,14 @@
/** @format */
/**
* Internal dependencies
*/
import { DEFAULT_REQUIREMENT } from '../constants';
const getSettings = ( getResource, requireResource ) => ( requirement = DEFAULT_REQUIREMENT ) => {
return requireResource( requirement, 'settings' ).data;
};
export default {
getSettings,
};

View File

@ -3,42 +3,46 @@
/**
* Internal dependencies
*/
import categories from './categories';
import items from './items';
import notes from './notes';
import orders from './orders';
import reportItems from './reports/items';
import reportStats from './reports/stats';
import reviews from './reviews';
import settings from './settings';
import user from './user';
function createWcApiSpec() {
return {
mutations: {
...settings.mutations,
...user.mutations,
},
selectors: {
...categories.selectors,
...items.selectors,
...notes.selectors,
...orders.selectors,
...reportItems.selectors,
...reportStats.selectors,
...reviews.selectors,
...settings.selectors,
...user.selectors,
},
operations: {
read( resourceNames ) {
return [
...categories.operations.read( resourceNames ),
...items.operations.read( resourceNames ),
...notes.operations.read( resourceNames ),
...orders.operations.read( resourceNames ),
...reportItems.operations.read( resourceNames ),
...reportStats.operations.read( resourceNames ),
...reviews.operations.read( resourceNames ),
...settings.operations.read( resourceNames ),
...user.operations.read( resourceNames ),
];
},
update( resourceNames, data ) {
return [ ...user.operations.update( resourceNames, data ) ];
return [
...settings.operations.update( resourceNames, data ),
...user.operations.update( resourceNames, data ),
];
},
},
};

View File

@ -1,9 +1,9 @@
`Flag` (component)
==================
Use the `Flag` component to display a country's flag.
Use the `Flag` component to display a country's flag using the operating system's emojis.
React component.
Props
-----
@ -22,27 +22,6 @@ Two letter, three letter or three digit country code.
An order can be passed instead of `code` and the code will automatically be pulled from the billing or shipping data.
### `round`
- Type: Boolean
- Default: `true`
True to display a rounded flag.
### `height`
- Type: Number
- Default: `24`
Flag image height.
### `width`
- Type: Number
- Default: `24`
Flag image width.
### `className`
- Type: String
@ -50,3 +29,10 @@ Flag image width.
Additional CSS classes.
### `size`
- Type: Number
- Default: null
Supply a font size to be applied to the emoji flag.

View File

@ -12,17 +12,17 @@ Props
### `children`
- **Required**
- Type: ReactNode
- Type: Function
- Default: null
A list of `<SummaryNumber />`s
A function returning a list of `<SummaryNumber />`s
### `label`
- Type: String
- Default: null
- Default: `__( 'Performance Indicators', 'wc-admin' )`
An optional label of this group, read to screen reader users. Defaults to "Performance Indicators".
An optional label of this group, read to screen reader users.
`SummaryNumber` (component)
===========================
@ -46,7 +46,7 @@ If omitted, no change value will display.
### `href`
- Type: String
- Default: `'/analytics'`
- Default: `''`
An internal link to the report focused on this number.
@ -109,6 +109,13 @@ A boolean used to show a highlight style on this number.
A string or number value to display - a string is allowed so we can accept currency formatting.
### `onLinkClickCallback`
- Type: Function
- Default: `noop`
A function to be called after a SummaryNumber, rendered as a link, is clicked.
`SummaryListPlaceholder` (component)
====================================

View File

@ -61,9 +61,11 @@ class WC_Admin_REST_Reports_Customers_Controller extends WC_REST_Reports_Control
$args['last_order_before'] = $request['last_order_before'];
$args['last_order_after'] = $request['last_order_after'];
$between_params = array( 'orders_count', 'total_spend', 'avg_order_value' );
$normalized = WC_Admin_Reports_Interval::normalize_between_params( $request, $between_params );
$args = array_merge( $args, $normalized );
$between_params_numeric = array( 'orders_count', 'total_spend', 'avg_order_value' );
$normalized_params_numeric = WC_Admin_Reports_Interval::normalize_between_params( $request, $between_params_numeric, false );
$between_params_date = array( 'last_active', 'registered' );
$normalized_params_date = WC_Admin_Reports_Interval::normalize_between_params( $request, $between_params_date, true );
$args = array_merge( $args, $normalized_params_numeric, $normalized_params_date );
return $args;
}
@ -296,14 +298,14 @@ class WC_Admin_REST_Reports_Customers_Controller extends WC_REST_Reports_Control
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
);
$params['order'] = array(
$params['order'] = array(
'description' => __( 'Order sort attribute ascending or descending.', 'wc-admin' ),
'type' => 'string',
'default' => 'desc',
'enum' => array( 'asc', 'desc' ),
'validate_callback' => 'rest_validate_request_arg',
);
$params['orderby'] = array(
$params['orderby'] = array(
'description' => __( 'Sort collection by object attribute.', 'wc-admin' ),
'type' => 'string',
'default' => 'date_registered',
@ -321,7 +323,7 @@ class WC_Admin_REST_Reports_Customers_Controller extends WC_REST_Reports_Control
),
'validate_callback' => 'rest_validate_request_arg',
);
$params['match'] = array(
$params['match'] = array(
'description' => __( 'Indicates whether all the conditions should be true for the resulting set, or if any one of them is sufficient. Match affects the following parameters: status_is, status_is_not, product_includes, product_excludes, coupon_includes, coupon_excludes, customer, categories', 'wc-admin' ),
'type' => 'string',
'default' => 'all',
@ -331,7 +333,7 @@ class WC_Admin_REST_Reports_Customers_Controller extends WC_REST_Reports_Control
),
'validate_callback' => 'rest_validate_request_arg',
);
$params['name'] = array(
$params['name'] = array(
'description' => __( 'Limit response to objects with a specfic customer name.', 'wc-admin' ),
'type' => 'string',
'validate_callback' => 'rest_validate_request_arg',
@ -363,34 +365,44 @@ class WC_Admin_REST_Reports_Customers_Controller extends WC_REST_Reports_Control
'format' => 'date-time',
'validate_callback' => 'rest_validate_request_arg',
);
$params['registered_before'] = array(
$params['last_active_between'] = array(
'description' => __( 'Limit response to objects last active between two given ISO8601 compliant datetime.', 'wc-admin' ),
'type' => 'array',
'validate_callback' => array( 'WC_Admin_Reports_Interval', 'rest_validate_between_date_arg' ),
);
$params['registered_before'] = array(
'description' => __( 'Limit response to objects registered before (or at) a given ISO8601 compliant datetime.', 'wc-admin' ),
'type' => 'string',
'format' => 'date-time',
'validate_callback' => 'rest_validate_request_arg',
);
$params['registered_after'] = array(
$params['registered_after'] = array(
'description' => __( 'Limit response to objects registered after (or at) a given ISO8601 compliant datetime.', 'wc-admin' ),
'type' => 'string',
'format' => 'date-time',
'validate_callback' => 'rest_validate_request_arg',
);
$params['orders_count_min'] = array(
$params['registered_between'] = array(
'description' => __( 'Limit response to objects last active between two given ISO8601 compliant datetime.', 'wc-admin' ),
'type' => 'array',
'validate_callback' => array( 'WC_Admin_Reports_Interval', 'rest_validate_between_date_arg' ),
);
$params['orders_count_min'] = array(
'description' => __( 'Limit response to objects with an order count greater than or equal to given integer.', 'wc-admin' ),
'type' => 'integer',
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
);
$params['orders_count_max'] = array(
$params['orders_count_max'] = array(
'description' => __( 'Limit response to objects with an order count less than or equal to given integer.', 'wc-admin' ),
'type' => 'integer',
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
);
$params['orders_count_between'] = array(
$params['orders_count_between'] = array(
'description' => __( 'Limit response to objects with an order count between two given integers.', 'wc-admin' ),
'type' => 'array',
'validate_callback' => array( 'WC_Admin_Reports_Interval', 'rest_validate_between_arg' ),
'validate_callback' => array( 'WC_Admin_Reports_Interval', 'rest_validate_between_numeric_arg' ),
);
$params['total_spend_min'] = array(
'description' => __( 'Limit response to objects with a total order spend greater than or equal to given number.', 'wc-admin' ),
@ -405,7 +417,7 @@ class WC_Admin_REST_Reports_Customers_Controller extends WC_REST_Reports_Control
$params['total_spend_between'] = array(
'description' => __( 'Limit response to objects with a total order spend between two given numbers.', 'wc-admin' ),
'type' => 'array',
'validate_callback' => array( 'WC_Admin_Reports_Interval', 'rest_validate_between_arg' ),
'validate_callback' => array( 'WC_Admin_Reports_Interval', 'rest_validate_between_numeric_arg' ),
);
$params['avg_order_value_min'] = array(
'description' => __( 'Limit response to objects with an average order spend greater than or equal to given number.', 'wc-admin' ),
@ -420,7 +432,7 @@ class WC_Admin_REST_Reports_Customers_Controller extends WC_REST_Reports_Control
$params['avg_order_value_between'] = array(
'description' => __( 'Limit response to objects with an average order spend between two given numbers.', 'wc-admin' ),
'type' => 'array',
'validate_callback' => array( 'WC_Admin_Reports_Interval', 'rest_validate_between_arg' ),
'validate_callback' => array( 'WC_Admin_Reports_Interval', 'rest_validate_between_numeric_arg' ),
);
$params['last_order_before'] = array(
'description' => __( 'Limit response to objects with last order before (or at) a given ISO8601 compliant datetime.', 'wc-admin' ),

View File

@ -0,0 +1,365 @@
<?php
/**
* REST API Reports customers stats controller
*
* Handles requests to the /reports/customers/stats endpoint.
*
* @package WooCommerce Admin/API
*/
defined( 'ABSPATH' ) || exit;
/**
* REST API Reports customers stats controller class.
*
* @package WooCommerce/API
* @extends WC_REST_Reports_Controller
*/
class WC_Admin_REST_Reports_Customers_Stats_Controller extends WC_REST_Reports_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v4';
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'reports/customers/stats';
/**
* Maps query arguments from the REST request.
*
* @param array $request Request array.
* @return array
*/
protected function prepare_reports_query( $request ) {
$args = array();
$args['registered_before'] = $request['registered_before'];
$args['registered_after'] = $request['registered_after'];
$args['match'] = $request['match'];
$args['name'] = $request['name'];
$args['username'] = $request['username'];
$args['email'] = $request['email'];
$args['country'] = $request['country'];
$args['last_active_before'] = $request['last_active_before'];
$args['last_active_after'] = $request['last_active_after'];
$args['orders_count_min'] = $request['orders_count_min'];
$args['orders_count_max'] = $request['orders_count_max'];
$args['total_spend_min'] = $request['total_spend_min'];
$args['total_spend_max'] = $request['total_spend_max'];
$args['avg_order_value_min'] = $request['avg_order_value_min'];
$args['avg_order_value_max'] = $request['avg_order_value_max'];
$args['last_order_before'] = $request['last_order_before'];
$args['last_order_after'] = $request['last_order_after'];
$between_params_numeric = array( 'orders_count', 'total_spend', 'avg_order_value' );
$normalized_params_numeric = WC_Admin_Reports_Interval::normalize_between_params( $request, $between_params_numeric, false );
$between_params_date = array( 'last_active', 'registered' );
$normalized_params_date = WC_Admin_Reports_Interval::normalize_between_params( $request, $between_params_date, true );
$args = array_merge( $args, $normalized_params_numeric, $normalized_params_date );
return $args;
}
/**
* Get all reports.
*
* @param WP_REST_Request $request Request data.
* @return array|WP_Error
*/
public function get_items( $request ) {
$query_args = $this->prepare_reports_query( $request );
$customers_query = new WC_Admin_Reports_Customers_Stats_Query( $query_args );
$report_data = $customers_query->get_data();
$out_data = array(
'totals' => $report_data,
// TODO: is this needed? the single element array tricks the isReportDataEmpty() selector.
'intervals' => array( (object) array() ),
);
return rest_ensure_response( $out_data );
}
/**
* Prepare a report object for serialization.
*
* @param Array $report Report data.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response
*/
public function prepare_item_for_response( $report, $request ) {
$data = $report;
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $context );
// Wrap the data in a response object.
$response = rest_ensure_response( $data );
/**
* Filter a report returned from the API.
*
* Allows modification of the report data right before it is returned.
*
* @param WP_REST_Response $response The response object.
* @param object $report The original report object.
* @param WP_REST_Request $request Request used to generate the response.
*/
return apply_filters( 'woocommerce_rest_prepare_report_customers_stats', $response, $report, $request );
}
/**
* Get the Report's schema, conforming to JSON Schema.
*
* @return array
*/
public function get_item_schema() {
// TODO: should any of these be 'indicator's?
$totals = array(
'customers_count' => array(
'description' => __( 'Number of customers.', 'wc-admin' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'avg_orders_count' => array(
'description' => __( 'Average number of orders.', 'wc-admin' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'avg_total_spend' => array(
'description' => __( 'Average total spend per customer.', 'wc-admin' ),
'type' => 'number',
'context' => array( 'view', 'edit' ),
'readonly' => true,
'format' => 'currency',
),
'avg_avg_order_value' => array(
'description' => __( 'Average AOV per customer.', 'wc-admin' ),
'type' => 'number',
'context' => array( 'view', 'edit' ),
'readonly' => true,
'format' => 'currency',
),
);
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'report_customers_stats',
'type' => 'object',
'properties' => array(
'totals' => array(
'description' => __( 'Totals data.', 'wc-admin' ),
'type' => 'object',
'context' => array( 'view', 'edit' ),
'readonly' => true,
'properties' => $totals,
),
'intervals' => array( // TODO: remove this?
'description' => __( 'Reports data grouped by intervals.', 'wc-admin' ),
'type' => 'array',
'context' => array( 'view', 'edit' ),
'readonly' => true,
'items' => array(
'type' => 'object',
'properties' => array(
'interval' => array(
'description' => __( 'Type of interval.', 'wc-admin' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
'enum' => array( 'day', 'week', 'month', 'year' ),
),
'date_start' => array(
'description' => __( "The date the report start, in the site's timezone.", 'wc-admin' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_start_gmt' => array(
'description' => __( 'The date the report start, as GMT.', 'wc-admin' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_end' => array(
'description' => __( "The date the report end, in the site's timezone.", 'wc-admin' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_end_gmt' => array(
'description' => __( 'The date the report end, as GMT.', 'wc-admin' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'subtotals' => array(
'description' => __( 'Interval subtotals.', 'wc-admin' ),
'type' => 'object',
'context' => array( 'view', 'edit' ),
'readonly' => true,
'properties' => $totals,
),
),
),
),
),
);
return $this->add_additional_fields_schema( $schema );
}
/**
* Get the query params for collections.
*
* @return array
*/
public function get_collection_params() {
$params = array();
$params['context'] = $this->get_context_param( array( 'default' => 'view' ) );
$params['registered_before'] = array(
'description' => __( 'Limit response to objects registered before (or at) a given ISO8601 compliant datetime.', 'wc-admin' ),
'type' => 'string',
'format' => 'date-time',
'validate_callback' => 'rest_validate_request_arg',
);
$params['registered_after'] = array(
'description' => __( 'Limit response to objects registered after (or at) a given ISO8601 compliant datetime.', 'wc-admin' ),
'type' => 'string',
'format' => 'date-time',
'validate_callback' => 'rest_validate_request_arg',
);
$params['match'] = array(
'description' => __( 'Indicates whether all the conditions should be true for the resulting set, or if any one of them is sufficient. Match affects the following parameters: status_is, status_is_not, product_includes, product_excludes, coupon_includes, coupon_excludes, customer, categories', 'wc-admin' ),
'type' => 'string',
'default' => 'all',
'enum' => array(
'all',
'any',
),
'validate_callback' => 'rest_validate_request_arg',
);
$params['name'] = array(
'description' => __( 'Limit response to objects with a specfic customer name.', 'wc-admin' ),
'type' => 'string',
'validate_callback' => 'rest_validate_request_arg',
);
$params['username'] = array(
'description' => __( 'Limit response to objects with a specfic username.', 'wc-admin' ),
'type' => 'string',
'validate_callback' => 'rest_validate_request_arg',
);
$params['email'] = array(
'description' => __( 'Limit response to objects equal to an email.', 'wc-admin' ),
'type' => 'string',
'validate_callback' => 'rest_validate_request_arg',
);
$params['country'] = array(
'description' => __( 'Limit response to objects with a specfic country.', 'wc-admin' ),
'type' => 'string',
'validate_callback' => 'rest_validate_request_arg',
);
$params['last_active_before'] = array(
'description' => __( 'Limit response to objects last active before (or at) a given ISO8601 compliant datetime.', 'wc-admin' ),
'type' => 'string',
'format' => 'date-time',
'validate_callback' => 'rest_validate_request_arg',
);
$params['last_active_after'] = array(
'description' => __( 'Limit response to objects last active after (or at) a given ISO8601 compliant datetime.', 'wc-admin' ),
'type' => 'string',
'format' => 'date-time',
'validate_callback' => 'rest_validate_request_arg',
);
$params['last_active_between'] = array(
'description' => __( 'Limit response to objects last active between two given ISO8601 compliant datetime.', 'wc-admin' ),
'type' => 'array',
'validate_callback' => array( 'WC_Admin_Reports_Interval', 'rest_validate_between_date_arg' ),
);
$params['registered_before'] = array(
'description' => __( 'Limit response to objects registered before (or at) a given ISO8601 compliant datetime.', 'wc-admin' ),
'type' => 'string',
'format' => 'date-time',
'validate_callback' => 'rest_validate_request_arg',
);
$params['registered_after'] = array(
'description' => __( 'Limit response to objects registered after (or at) a given ISO8601 compliant datetime.', 'wc-admin' ),
'type' => 'string',
'format' => 'date-time',
'validate_callback' => 'rest_validate_request_arg',
);
$params['registered_between'] = array(
'description' => __( 'Limit response to objects last active between two given ISO8601 compliant datetime.', 'wc-admin' ),
'type' => 'array',
'validate_callback' => array( 'WC_Admin_Reports_Interval', 'rest_validate_between_date_arg' ),
);
$params['orders_count_min'] = array(
'description' => __( 'Limit response to objects with an order count greater than or equal to given integer.', 'wc-admin' ),
'type' => 'integer',
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
);
$params['orders_count_max'] = array(
'description' => __( 'Limit response to objects with an order count less than or equal to given integer.', 'wc-admin' ),
'type' => 'integer',
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
);
$params['orders_count_between'] = array(
'description' => __( 'Limit response to objects with an order count between two given integers.', 'wc-admin' ),
'type' => 'array',
'validate_callback' => array( 'WC_Admin_Reports_Interval', 'rest_validate_between_numeric_arg' ),
);
$params['total_spend_min'] = array(
'description' => __( 'Limit response to objects with a total order spend greater than or equal to given number.', 'wc-admin' ),
'type' => 'number',
'validate_callback' => 'rest_validate_request_arg',
);
$params['total_spend_max'] = array(
'description' => __( 'Limit response to objects with a total order spend less than or equal to given number.', 'wc-admin' ),
'type' => 'number',
'validate_callback' => 'rest_validate_request_arg',
);
$params['total_spend_between'] = array(
'description' => __( 'Limit response to objects with a total order spend between two given numbers.', 'wc-admin' ),
'type' => 'array',
'validate_callback' => array( 'WC_Admin_Reports_Interval', 'rest_validate_between_numeric_arg' ),
);
$params['avg_order_value_min'] = array(
'description' => __( 'Limit response to objects with an average order spend greater than or equal to given number.', 'wc-admin' ),
'type' => 'number',
'validate_callback' => 'rest_validate_request_arg',
);
$params['avg_order_value_max'] = array(
'description' => __( 'Limit response to objects with an average order spend less than or equal to given number.', 'wc-admin' ),
'type' => 'number',
'validate_callback' => 'rest_validate_request_arg',
);
$params['avg_order_value_between'] = array(
'description' => __( 'Limit response to objects with an average order spend between two given numbers.', 'wc-admin' ),
'type' => 'array',
'validate_callback' => array( 'WC_Admin_Reports_Interval', 'rest_validate_between_numeric_arg' ),
);
$params['last_order_before'] = array(
'description' => __( 'Limit response to objects with last order before (or at) a given ISO8601 compliant datetime.', 'wc-admin' ),
'type' => 'string',
'format' => 'date-time',
'validate_callback' => 'rest_validate_request_arg',
);
$params['last_order_after'] = array(
'description' => __( 'Limit response to objects with last order after (or at) a given ISO8601 compliant datetime.', 'wc-admin' ),
'type' => 'string',
'format' => 'date-time',
'validate_callback' => 'rest_validate_request_arg',
);
return $params;
}
}

View File

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

View File

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

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