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 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 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 # 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 php wp-cli.phar plugin install gutenberg --activate
fi
fi
# Install WooCommerce # Install WooCommerce
cd "wp-content/plugins/" cd "wp-content/plugins/"

View File

@ -17,7 +17,7 @@ import { Card, EmptyTable, TableCard } from '@woocommerce/components';
* Internal dependencies * Internal dependencies
*/ */
import ReportError from 'analytics/components/report-error'; 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 withSelect from 'wc-api/with-select';
import './style.scss'; import './style.scss';

View File

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

View File

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

View File

@ -19,7 +19,7 @@ import { onQueryChange } from '@woocommerce/navigation';
* Internal dependencies * Internal dependencies
*/ */
import ReportError from 'analytics/components/report-error'; 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 withSelect from 'wc-api/with-select';
import { extendTableData } from './utils'; import { extendTableData } from './utils';

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -5,6 +5,7 @@
import { __ } from '@wordpress/i18n'; import { __ } from '@wordpress/i18n';
import { applyFilters } from '@wordpress/hooks'; import { applyFilters } from '@wordpress/hooks';
import { Component, Fragment } from '@wordpress/element'; import { Component, Fragment } from '@wordpress/element';
import { compose } from '@wordpress/compose';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { find } from 'lodash'; import { find } from 'lodash';
@ -12,6 +13,7 @@ import { find } from 'lodash';
* WooCommerce dependencies * WooCommerce dependencies
*/ */
import { useFilters } from '@woocommerce/components'; import { useFilters } from '@woocommerce/components';
import { getQuery } from '@woocommerce/navigation';
/** /**
* Internal dependencies * Internal dependencies
@ -27,6 +29,8 @@ import TaxesReport from './taxes';
import DownloadsReport from './downloads'; import DownloadsReport from './downloads';
import StockReport from './stock'; import StockReport from './stock';
import CustomersReport from './customers'; import CustomersReport from './customers';
import { searchItemsByString } from 'wc-api/items/utils';
import withSelect from 'wc-api/with-select';
const REPORTS_FILTER = 'woocommerce-reports-list'; const REPORTS_FILTER = 'woocommerce-reports-list';
@ -131,4 +135,27 @@ Report.propTypes = {
params: PropTypes.object.isRequired, 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 { Date, Link, OrderStatus, ViewMoreList } from '@woocommerce/components';
import { defaultTableDateFormat } from '@woocommerce/date'; import { defaultTableDateFormat } from '@woocommerce/date';
import { formatCurrency } from '@woocommerce/currency'; import { formatCurrency } from '@woocommerce/currency';
import { numberFormat } from '@woocommerce/number';
/** /**
* Internal dependencies * Internal dependencies
*/ */
import { numberFormat } from 'lib/number';
import ReportTable from 'analytics/components/report-table'; import ReportTable from 'analytics/components/report-table';
import { getNewPath, getPersistedQuery } from '@woocommerce/navigation'; import { getNewPath, getPersistedQuery } from '@woocommerce/navigation';
import './style.scss'; import './style.scss';

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -12,12 +12,12 @@ import { map } from 'lodash';
import { Link } from '@woocommerce/components'; import { Link } from '@woocommerce/components';
import { formatCurrency, getCurrencyFormatDecimal } from '@woocommerce/currency'; import { formatCurrency, getCurrencyFormatDecimal } from '@woocommerce/currency';
import { getTaxCode } from './utils'; import { getTaxCode } from './utils';
import { numberFormat } from '@woocommerce/number';
/** /**
* Internal dependencies * Internal dependencies
*/ */
import ReportTable from 'analytics/components/report-table'; import ReportTable from 'analytics/components/report-table';
import { numberFormat } from 'lib/number';
export default class TaxesReportTable extends Component { export default class TaxesReportTable extends Component {
constructor() { constructor() {
@ -150,6 +150,7 @@ export default class TaxesReportTable extends Component {
getSummary={ this.getSummary } getSummary={ this.getSummary }
itemIdField="tax_rate_id" itemIdField="tax_rate_id"
query={ query } query={ query }
searchBy="taxes"
tableQuery={ { tableQuery={ {
orderby: query.orderby || 'tax_rate_id', 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 withSelect from 'wc-api/with-select';
import TopSellingCategories from './top-selling-categories'; import TopSellingCategories from './top-selling-categories';
import TopSellingProducts from './top-selling-products'; import TopSellingProducts from './top-selling-products';
import TopCoupons from './top-coupons';
import './style.scss'; import './style.scss';
class Leaderboards extends Component { class Leaderboards extends Component {
@ -134,10 +135,12 @@ class Leaderboards extends Component {
{ ! hiddenLeaderboardKeys.includes( 'top-products' ) && ( { ! hiddenLeaderboardKeys.includes( 'top-products' ) && (
<TopSellingProducts query={ query } totalRows={ rowsPerTable } /> <TopSellingProducts query={ query } totalRows={ rowsPerTable } />
) } ) }
{ ! hiddenLeaderboardKeys.includes( 'top-categories' ) && ( { ! hiddenLeaderboardKeys.includes( 'top-categories' ) && (
<TopSellingCategories query={ query } totalRows={ rowsPerTable } /> <TopSellingCategories query={ query } totalRows={ rowsPerTable } />
) } ) }
{ ! hiddenLeaderboardKeys.includes( 'top-coupons' ) && (
<TopCoupons query={ query } totalRows={ rowsPerTable } />
) }
</div> </div>
</div> </div>
</Fragment> </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 { formatCurrency, getCurrencyFormatDecimal } from '@woocommerce/currency';
import { getNewPath, getPersistedQuery } from '@woocommerce/navigation'; import { getNewPath, getPersistedQuery } from '@woocommerce/navigation';
import { Link } from '@woocommerce/components'; import { Link } from '@woocommerce/components';
import { numberFormat } from '@woocommerce/number';
/** /**
* Internal dependencies * Internal dependencies
*/ */
import { numberFormat } from 'lib/number';
import Leaderboard from 'analytics/components/leaderboard'; import Leaderboard from 'analytics/components/leaderboard';
export class TopSellingCategories extends Component { export class TopSellingCategories extends Component {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -28,7 +28,7 @@ import {
*/ */
import { ActivityCard, ActivityCardPlaceholder } from '../activity-card'; import { ActivityCard, ActivityCardPlaceholder } from '../activity-card';
import ActivityHeader from '../activity-header'; 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 sanitizeHTML from 'lib/sanitize-html';
import withSelect from 'wc-api/with-select'; 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 './stylesheets/_index.scss';
import { PageLayout } from './layout'; import { PageLayout } from './layout';
import 'store';
import 'wc-api/wp-data-store'; import 'wc-api/wp-data-store';
render( render(

View File

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

View File

@ -13,7 +13,7 @@ import { getIdsFromQuery, stringifyQuery } from '@woocommerce/navigation';
/** /**
* Internal dependencies * 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 * 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'; export const NAMESPACE = '/wc/v4';
// TODO: Remove once swagger endpoints are phased out.
export const SWAGGERNAMESPACE = 'https://virtserver.swaggerhub.com/peterfabian/wc-v3-api/1.0.0/';
export const DEFAULT_REQUIREMENT = { export const DEFAULT_REQUIREMENT = {
timeout: 1 * MINUTE, timeout: 1 * MINUTE,
freshness: 5 * MINUTE, freshness: 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 * Internal dependencies
*/ */
import { getResourceIdentifier, getResourcePrefix } from '../../utils'; import { getResourceIdentifier, getResourcePrefix } from 'wc-api/utils';
import { NAMESPACE } from '../../constants'; import { NAMESPACE, SWAGGERNAMESPACE } from 'wc-api/constants';
import { SWAGGERNAMESPACE } from 'store/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. // TODO: Remove once swagger endpoints are phased out.
const swaggerEndpoints = [ 'categories' ]; const swaggerEndpoints = [ 'categories' ];
@ -28,6 +35,7 @@ const typeEndpointMap = {
'report-stats-query-downloads': 'downloads', 'report-stats-query-downloads': 'downloads',
'report-stats-query-coupons': 'coupons', 'report-stats-query-coupons': 'coupons',
'report-stats-query-taxes': 'taxes', 'report-stats-query-taxes': 'taxes',
'report-stats-query-customers': 'customers',
}; };
function read( resourceNames, fetch = apiFetch ) { function read( resourceNames, fetch = apiFetch ) {

View File

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

View File

@ -4,8 +4,10 @@
*/ */
import operations from './operations'; import operations from './operations';
import selectors from './selectors'; import selectors from './selectors';
import mutations from './mutations';
export default { export default {
operations, operations,
selectors, 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 * Internal dependencies
*/ */
import categories from './categories'; import items from './items';
import notes from './notes'; import notes from './notes';
import orders from './orders';
import reportItems from './reports/items'; import reportItems from './reports/items';
import reportStats from './reports/stats'; import reportStats from './reports/stats';
import reviews from './reviews'; import reviews from './reviews';
import settings from './settings';
import user from './user'; import user from './user';
function createWcApiSpec() { function createWcApiSpec() {
return { return {
mutations: { mutations: {
...settings.mutations,
...user.mutations, ...user.mutations,
}, },
selectors: { selectors: {
...categories.selectors, ...items.selectors,
...notes.selectors, ...notes.selectors,
...orders.selectors,
...reportItems.selectors, ...reportItems.selectors,
...reportStats.selectors, ...reportStats.selectors,
...reviews.selectors, ...reviews.selectors,
...settings.selectors,
...user.selectors, ...user.selectors,
}, },
operations: { operations: {
read( resourceNames ) { read( resourceNames ) {
return [ return [
...categories.operations.read( resourceNames ), ...items.operations.read( resourceNames ),
...notes.operations.read( resourceNames ), ...notes.operations.read( resourceNames ),
...orders.operations.read( resourceNames ),
...reportItems.operations.read( resourceNames ), ...reportItems.operations.read( resourceNames ),
...reportStats.operations.read( resourceNames ), ...reportStats.operations.read( resourceNames ),
...reviews.operations.read( resourceNames ), ...reviews.operations.read( resourceNames ),
...settings.operations.read( resourceNames ),
...user.operations.read( resourceNames ), ...user.operations.read( resourceNames ),
]; ];
}, },
update( resourceNames, data ) { 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) `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 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. 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` ### `className`
- Type: String - Type: String
@ -50,3 +29,10 @@ Flag image width.
Additional CSS classes. 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` ### `children`
- **Required** - **Required**
- Type: ReactNode - Type: Function
- Default: null - Default: null
A list of `<SummaryNumber />`s A function returning a list of `<SummaryNumber />`s
### `label` ### `label`
- Type: String - 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) `SummaryNumber` (component)
=========================== ===========================
@ -46,7 +46,7 @@ If omitted, no change value will display.
### `href` ### `href`
- Type: String - Type: String
- Default: `'/analytics'` - Default: `''`
An internal link to the report focused on this number. 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. 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) `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_before'] = $request['last_order_before'];
$args['last_order_after'] = $request['last_order_after']; $args['last_order_after'] = $request['last_order_after'];
$between_params = array( 'orders_count', 'total_spend', 'avg_order_value' ); $between_params_numeric = array( 'orders_count', 'total_spend', 'avg_order_value' );
$normalized = WC_Admin_Reports_Interval::normalize_between_params( $request, $between_params ); $normalized_params_numeric = WC_Admin_Reports_Interval::normalize_between_params( $request, $between_params_numeric, false );
$args = array_merge( $args, $normalized ); $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; return $args;
} }
@ -363,6 +365,11 @@ class WC_Admin_REST_Reports_Customers_Controller extends WC_REST_Reports_Control
'format' => 'date-time', 'format' => 'date-time',
'validate_callback' => 'rest_validate_request_arg', 'validate_callback' => 'rest_validate_request_arg',
); );
$params['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( $params['registered_before'] = array(
'description' => __( 'Limit response to objects registered before (or at) a given ISO8601 compliant datetime.', 'wc-admin' ), 'description' => __( 'Limit response to objects registered before (or at) a given ISO8601 compliant datetime.', 'wc-admin' ),
'type' => 'string', 'type' => 'string',
@ -375,6 +382,11 @@ class WC_Admin_REST_Reports_Customers_Controller extends WC_REST_Reports_Control
'format' => 'date-time', 'format' => 'date-time',
'validate_callback' => 'rest_validate_request_arg', 'validate_callback' => 'rest_validate_request_arg',
); );
$params['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( $params['orders_count_min'] = array(
'description' => __( 'Limit response to objects with an order count greater than or equal to given integer.', 'wc-admin' ), 'description' => __( 'Limit response to objects with an order count greater than or equal to given integer.', 'wc-admin' ),
'type' => 'integer', 'type' => 'integer',
@ -390,7 +402,7 @@ class WC_Admin_REST_Reports_Customers_Controller extends WC_REST_Reports_Control
$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' ), 'description' => __( 'Limit response to objects with an order count between two given integers.', 'wc-admin' ),
'type' => 'array', '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( $params['total_spend_min'] = array(
'description' => __( 'Limit response to objects with a total order spend greater than or equal to given number.', 'wc-admin' ), '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( $params['total_spend_between'] = array(
'description' => __( 'Limit response to objects with a total order spend between two given numbers.', 'wc-admin' ), 'description' => __( 'Limit response to objects with a total order spend between two given numbers.', 'wc-admin' ),
'type' => 'array', '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( $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' ), '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( $params['avg_order_value_between'] = array(
'description' => __( 'Limit response to objects with an average order spend between two given numbers.', 'wc-admin' ), 'description' => __( 'Limit response to objects with an average order spend between two given numbers.', 'wc-admin' ),
'type' => 'array', '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( $params['last_order_before'] = array(
'description' => __( 'Limit response to objects with last order before (or at) a given ISO8601 compliant datetime.', 'wc-admin' ), '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