Merge branch 'master' into add/name-filter-autocompleter

This commit is contained in:
Albert Juhé Lluveras 2018-12-18 13:52:48 +01:00
commit c4290f757e
81 changed files with 2515 additions and 256 deletions

View File

@ -26,6 +26,7 @@ There are also some helper scripts:
- `npm run lint` : Run eslint over the javascript files - `npm run lint` : Run eslint over the javascript files
- `npm run i18n` : A multi-step process, used to create a pot file from both the JS and PHP gettext calls. First it runs `i18n:js`, which creates a temporary `.pot` file from the JS files. Next it runs `i18n:php`, which converts that `.pot` file to a PHP file. Lastly, it runs `i18n:pot`, which creates the final `.pot` file from all the PHP files in the plugin (including the generated one with the JS strings). - `npm run i18n` : A multi-step process, used to create a pot file from both the JS and PHP gettext calls. First it runs `i18n:js`, which creates a temporary `.pot` file from the JS files. Next it runs `i18n:php`, which converts that `.pot` file to a PHP file. Lastly, it runs `i18n:pot`, which creates the final `.pot` file from all the PHP files in the plugin (including the generated one with the JS strings).
- `npm test` : Run the JS test suite
## Privacy ## Privacy

View File

@ -5,6 +5,7 @@
import { applyFilters } from '@wordpress/hooks'; import { applyFilters } from '@wordpress/hooks';
import { Component } from '@wordpress/element'; import { Component } from '@wordpress/element';
import { compose } from '@wordpress/compose'; import { compose } from '@wordpress/compose';
import { withDispatch } from '@wordpress/data';
import { get, orderBy } from 'lodash'; import { get, orderBy } from 'lodash';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
@ -20,10 +21,33 @@ import { onQueryChange } from '@woocommerce/navigation';
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 'store/reports/utils';
import withSelect from 'wc-api/with-select'; import withSelect from 'wc-api/with-select';
import { extendTableData } from './utils';
const TABLE_FILTER = 'woocommerce_admin_report_table'; const TABLE_FILTER = 'woocommerce_admin_report_table';
class ReportTable extends Component { class ReportTable extends Component {
onColumnsChange = columns => {
const { columnPrefsKey } = this.props;
if ( columnPrefsKey ) {
const userDataFields = {
[ columnPrefsKey ]: columns,
};
this.props.updateCurrentUserData( userDataFields );
}
};
filterShownHeaders = ( headers, shownKeys ) => {
if ( ! shownKeys ) {
return headers;
}
return headers.map( header => {
const hidden = ! shownKeys.includes( header.key );
return { ...header, hiddenByDefault: hidden };
} );
};
render() { render() {
const { const {
getHeadersContent, getHeadersContent,
@ -36,6 +60,7 @@ class ReportTable extends Component {
// so they are not included in the `tableProps` variable. // so they are not included in the `tableProps` variable.
endpoint, endpoint,
tableQuery, tableQuery,
userPrefColumns,
...tableProps ...tableProps
} = this.props; } = this.props;
@ -50,7 +75,7 @@ class ReportTable extends Component {
const isRequesting = tableData.isRequesting || primaryData.isRequesting; const isRequesting = tableData.isRequesting || primaryData.isRequesting;
const orderedItems = orderBy( items.data, query.orderby, query.order ); const orderedItems = orderBy( items.data, query.orderby, query.order );
const totals = get( primaryData, [ 'data', 'totals' ], null ); const totals = get( primaryData, [ 'data', 'totals' ], null );
const totalCount = items.totalCount || 0; const totalResults = items.totalResults || 0;
const { headers, ids, rows, summary } = applyFilters( TABLE_FILTER, { const { headers, ids, rows, summary } = applyFilters( TABLE_FILTER, {
endpoint: endpoint, endpoint: endpoint,
headers: getHeadersContent(), headers: getHeadersContent(),
@ -58,20 +83,24 @@ class ReportTable extends Component {
ids: itemIdField ? orderedItems.map( item => item[ itemIdField ] ) : null, ids: itemIdField ? orderedItems.map( item => item[ itemIdField ] ) : null,
rows: getRowsContent( orderedItems ), rows: getRowsContent( orderedItems ),
totals: totals, totals: totals,
summary: getSummary ? getSummary( totals, totalCount ) : null, summary: getSummary ? getSummary( totals, totalResults ) : null,
} ); } );
// Hide any headers based on user prefs, if loaded.
const filteredHeaders = this.filterShownHeaders( headers, userPrefColumns );
return ( return (
<TableCard <TableCard
downloadable downloadable
headers={ headers } headers={ filteredHeaders }
ids={ ids } ids={ ids }
isLoading={ isRequesting } isLoading={ isRequesting }
onQueryChange={ onQueryChange } onQueryChange={ onQueryChange }
onColumnsChange={ this.onColumnsChange }
rows={ rows } rows={ rows }
rowsPerPage={ parseInt( query.per_page ) } rowsPerPage={ parseInt( query.per_page ) }
summary={ summary } summary={ summary }
totalRows={ totalCount } totalRows={ totalResults }
{ ...tableProps } { ...tableProps }
/> />
); );
@ -79,10 +108,24 @@ class ReportTable extends Component {
} }
ReportTable.propTypes = { ReportTable.propTypes = {
/**
* The key for user preferences settings for column visibility.
*/
columnPrefsKey: PropTypes.string,
/** /**
* The endpoint to use in API calls. * The endpoint to use in API calls.
*/ */
endpoint: PropTypes.string, endpoint: PropTypes.string,
/**
* Name of the methods available via `select( 'wc-api' )` that will be used to
* load more data for table items. If omitted, no call will be made and only
* the data returned by the reports endpoint will be used.
*/
extendItemsMethodNames: PropTypes.shape( {
getError: PropTypes.string,
isRequesting: PropTypes.string,
load: PropTypes.string,
} ),
/** /**
* A function that returns the headers object to build the table. * A function that returns the headers object to build the table.
*/ */
@ -124,16 +167,33 @@ ReportTable.defaultProps = {
export default compose( export default compose(
withSelect( ( select, props ) => { withSelect( ( select, props ) => {
const { endpoint, getSummary, query, tableData, tableQuery } = props; const { endpoint, getSummary, query, tableData, tableQuery, columnPrefsKey } = props;
const chartEndpoint = 'variations' === endpoint ? 'products' : endpoint; const chartEndpoint = 'variations' === endpoint ? 'products' : endpoint;
const primaryData = getSummary const primaryData = getSummary
? getReportChartData( chartEndpoint, 'primary', query, select ) ? getReportChartData( chartEndpoint, 'primary', query, select )
: {}; : {};
const queriedTableData = tableData || getReportTableData( endpoint, query, select, tableQuery ); const queriedTableData = tableData || getReportTableData( endpoint, query, select, tableQuery );
const extendedTableData = extendTableData( select, props, queriedTableData );
const selectProps = {
primaryData,
tableData: extendedTableData,
};
if ( columnPrefsKey ) {
const { getCurrentUserData } = select( 'wc-api' );
const userData = getCurrentUserData();
selectProps.userPrefColumns = userData[ columnPrefsKey ];
}
return selectProps;
} ),
withDispatch( dispatch => {
const { updateCurrentUserData } = dispatch( 'wc-api' );
return { return {
primaryData, updateCurrentUserData,
tableData: queriedTableData,
}; };
} ) } )
)( ReportTable ); )( ReportTable );

View File

@ -0,0 +1,54 @@
/** @format */
/**
* External dependencies
*/
import { first } from 'lodash';
export function extendTableData( select, props, queriedTableData ) {
const { extendItemsMethodNames, itemIdField } = props;
const itemsData = queriedTableData.items.data;
if (
! Array.isArray( itemsData ) ||
! itemsData.length ||
! extendItemsMethodNames ||
! itemIdField
) {
return queriedTableData;
}
const {
[ extendItemsMethodNames.getError ]: getErrorMethod,
[ extendItemsMethodNames.isRequesting ]: isRequestingMethod,
[ extendItemsMethodNames.load ]: loadMethod,
} = select( 'wc-api' );
const extendQuery = {
include: itemsData.map( item => item[ itemIdField ] ).join( ',' ),
per_page: itemsData.length,
};
const extendedItems = loadMethod( extendQuery );
const isExtendedItemsRequesting = isRequestingMethod ? isRequestingMethod( extendQuery ) : false;
const isExtendedItemsError = getErrorMethod ? getErrorMethod( extendQuery ) : false;
const extendedItemsData = itemsData.map( item => {
const extendedItemData = first(
extendedItems.filter( extendedItem => item.id === extendedItem.id )
);
return {
...item,
...extendedItemData,
};
} );
const isRequesting = queriedTableData.isRequesting || isExtendedItemsRequesting;
const isError = queriedTableData.isError || isExtendedItemsError;
return {
...queriedTableData,
isRequesting,
isError,
items: {
...queriedTableData.items,
data: extendedItemsData,
},
};
}

View File

@ -98,15 +98,15 @@ export default class CategoriesReportTable extends Component {
} ); } );
} }
getSummary( totals, totalCount ) { getSummary( totals, totalResults ) {
if ( ! totals ) { if ( ! totals ) {
return []; return [];
} }
return [ return [
{ {
label: _n( 'category', 'categories', totalCount, 'wc-admin' ), label: _n( 'category', 'categories', totalResults, 'wc-admin' ),
value: numberFormat( totalCount ), value: numberFormat( totalResults ),
}, },
{ {
label: _n( 'item sold', 'items sold', totals.items_sold, 'wc-admin' ), label: _n( 'item sold', 'items sold', totals.items_sold, 'wc-admin' ),
@ -136,6 +136,7 @@ export default class CategoriesReportTable extends Component {
itemIdField="category_id" itemIdField="category_id"
query={ query } query={ query }
title={ __( 'Categories', 'wc-admin' ) } title={ __( 'Categories', 'wc-admin' ) }
columnPrefsKey="categories_report_columns"
/> />
); );
} }

View File

@ -162,6 +162,7 @@ export default class CouponsReportTable extends Component {
itemIdField="coupon_id" itemIdField="coupon_id"
query={ query } query={ query }
title={ __( 'Coupons', 'wc-admin' ) } title={ __( 'Coupons', 'wc-admin' ) }
columnPrefsKey="coupons_report_columns"
/> />
); );
} }

View File

@ -3,8 +3,13 @@
* External dependencies * External dependencies
*/ */
import { __, _x } from '@wordpress/i18n'; import { __, _x } from '@wordpress/i18n';
import { getRequestByIdString } from '../../../lib/async-requests'; import { decodeEntities } from '@wordpress/html-entities';
import { NAMESPACE } from '../../../store/constants';
/**
* Internal dependencies
*/
import { getRequestByIdString } from 'lib/async-requests';
import { NAMESPACE } from 'store/constants';
export const filters = [ export const filters = [
{ {
@ -30,7 +35,7 @@ export const advancedFilters = {
name: { name: {
labels: { labels: {
add: __( 'Name', 'wc-admin' ), add: __( 'Name', 'wc-admin' ),
placeholder: __( 'Search customer name', 'wc-admin' ), placeholder: __( 'Search', 'wc-admin' ),
remove: __( 'Remove customer name filter', 'wc-admin' ), remove: __( 'Remove customer name filter', 'wc-admin' ),
rule: __( 'Select a customer name filter match', 'wc-admin' ), rule: __( 'Select a customer name filter match', 'wc-admin' ),
/* translators: A sentence describing a Product filter. See screen shot for context: https://cloudup.com/cCsm3GeXJbE */ /* translators: A sentence describing a Product filter. See screen shot for context: https://cloudup.com/cCsm3GeXJbE */
@ -58,6 +63,78 @@ export const advancedFilters = {
} ) ), } ) ),
}, },
}, },
country: {
labels: {
add: __( 'Country', 'wc-admin' ),
placeholder: __( 'Search', 'wc-admin' ),
remove: __( 'Remove country filter', 'wc-admin' ),
rule: __( 'Select a country filter match', 'wc-admin' ),
/* translators: A sentence describing a Product filter. See screen shot for context: https://cloudup.com/cCsm3GeXJbE */
title: __( 'Country {{rule /}} {{filter /}}', 'wc-admin' ),
filter: __( 'Select country', 'wc-admin' ),
},
rules: [
{
value: 'includes',
/* translators: Sentence fragment, logical, "Includes" refers to countries including a given country or countries. Screenshot for context: https://cloudup.com/cCsm3GeXJbE */
label: _x( 'Includes', 'countries', 'wc-admin' ),
},
{
value: 'excludes',
/* translators: Sentence fragment, logical, "Excludes" refers to countries excluding a given country or countries. Screenshot for context: https://cloudup.com/cCsm3GeXJbE */
label: _x( 'Excludes', 'countries', 'wc-admin' ),
},
],
input: {
component: 'Search',
type: 'countries',
getLabels: async value => {
const countries =
( wcSettings.dataEndpoints && wcSettings.dataEndpoints.countries ) || [];
const allLabels = countries.map( country => ( {
id: country.code,
label: decodeEntities( country.name ),
} ) );
const labels = value.split( ',' );
return await allLabels.filter( label => {
return labels.includes( label.id );
} );
},
},
},
email: {
labels: {
add: __( 'Email', 'wc-admin' ),
placeholder: __( 'Search customer email', 'wc-admin' ),
remove: __( 'Remove customer email filter', 'wc-admin' ),
rule: __( 'Select a customer email filter match', 'wc-admin' ),
/* translators: A sentence describing a customer email filter. See screen shot for context: https://cloudup.com/cCsm3GeXJbE */
title: __( 'Email {{rule /}} {{filter /}}', 'wc-admin' ),
filter: __( 'Select customer email', 'wc-admin' ),
},
rules: [
{
value: 'includes',
/* translators: Sentence fragment, logical, "Includes" refers to customer emails including a given email(s). Screenshot for context: https://cloudup.com/cCsm3GeXJbE */
label: _x( 'Includes', 'customer emails', 'wc-admin' ),
},
{
value: 'excludes',
/* translators: Sentence fragment, logical, "Excludes" refers to customer emails excluding a given email(s). Screenshot for context: https://cloudup.com/cCsm3GeXJbE */
label: _x( 'Excludes', 'customer emails', 'wc-admin' ),
},
],
input: {
component: 'Search',
type: 'emails',
getLabels: getRequestByIdString( NAMESPACE + 'customers', customer => ( {
id: customer.id,
label: customer.email,
} ) ),
},
},
}, },
}; };
/*eslint-enable max-len*/ /*eslint-enable max-len*/

View File

@ -66,7 +66,7 @@ export default class CustomersReportTable extends Component {
{ {
label: __( 'AOV', 'wc-admin' ), label: __( 'AOV', 'wc-admin' ),
screenReaderLabel: __( 'Average Order Value', 'wc-admin' ), screenReaderLabel: __( 'Average Order Value', 'wc-admin' ),
key: 'average_order_value', key: 'avg_order_value',
isNumeric: true, isNumeric: true,
}, },
{ {
@ -98,19 +98,20 @@ export default class CustomersReportTable extends Component {
return customers.map( customer => { return customers.map( customer => {
const { const {
average_order_value, avg_order_value,
id, billing,
city,
country,
date_last_active, date_last_active,
date_sign_up, date_sign_up,
email, email,
name, first_name,
id,
last_name,
orders_count, orders_count,
postal_code,
username, username,
total_spend, total_spend,
} = customer; } = customer;
const { postcode, city, country } = billing || {};
const name = `${ first_name } ${ last_name }`;
const customerNameLink = ( const customerNameLink = (
<Link href={ 'user-edit.php?user_id=' + id } type="wp-admin"> <Link href={ 'user-edit.php?user_id=' + id } type="wp-admin">
@ -144,8 +145,8 @@ export default class CustomersReportTable extends Component {
value: getCurrencyFormatDecimal( total_spend ), value: getCurrencyFormatDecimal( total_spend ),
}, },
{ {
display: average_order_value, display: formatCurrency( avg_order_value ),
value: getCurrencyFormatDecimal( average_order_value ), value: getCurrencyFormatDecimal( avg_order_value ),
}, },
{ {
display: formatDate( formats.tableFormat, date_last_active ), display: formatDate( formats.tableFormat, date_last_active ),
@ -160,8 +161,8 @@ export default class CustomersReportTable extends Component {
value: city, value: city,
}, },
{ {
display: postal_code, display: postcode,
value: postal_code, value: postcode,
}, },
]; ];
} ); } );
@ -173,6 +174,11 @@ export default class CustomersReportTable extends Component {
return ( return (
<ReportTable <ReportTable
endpoint="customers" endpoint="customers"
extendItemsMethodNames={ {
load: 'getCustomers',
getError: 'getCustomersError',
isRequesting: 'isGetCustomersRequesting',
} }
getHeadersContent={ this.getHeadersContent } getHeadersContent={ this.getHeadersContent }
getRowsContent={ this.getRowsContent } getRowsContent={ this.getRowsContent }
itemIdField="id" itemIdField="id"
@ -181,6 +187,7 @@ export default class CustomersReportTable extends Component {
searchBy="customers" searchBy="customers"
searchParam="name_includes" searchParam="name_includes"
title={ __( 'Registered Customers', 'wc-admin' ) } title={ __( 'Registered Customers', 'wc-admin' ) }
columnPrefsKey="customers_report_columns"
/> />
); );
} }

View File

@ -0,0 +1,67 @@
/** @format */
/**
* External dependencies
*/
import { __, _x } from '@wordpress/i18n';
/**
* Internal dependencies
*/
import { getRequestByIdString } from 'lib/async-requests';
import { NAMESPACE } from 'store/constants';
export const filters = [
{
label: __( 'Show', 'wc-admin' ),
staticParams: [],
param: 'filter',
showFilters: () => true,
filters: [
{ label: __( 'All Downloads', 'wc-admin' ), value: 'all' },
{ label: __( 'Advanced Filters', 'wc-admin' ), value: 'advanced' },
],
},
];
/*eslint-disable max-len*/
export const advancedFilters = {
title: _x(
'Downloads Match {{select /}} Filters',
'A sentence describing filters for Downloads. See screen shot for context: https://cloudup.com/ccxhyH2mEDg',
'wc-admin'
),
filters: {
product: {
labels: {
add: __( 'Product', 'wc-admin' ),
placeholder: __( 'Search', 'wc-admin' ),
remove: __( 'Remove product filter', 'wc-admin' ),
rule: __( 'Select a product filter match', 'wc-admin' ),
/* translators: A sentence describing a Product filter. See screen shot for context: https://cloudup.com/ccxhyH2mEDg */
title: __( 'Product {{rule /}} {{filter /}}', 'wc-admin' ),
filter: __( 'Select product', 'wc-admin' ),
},
rules: [
{
value: 'includes',
/* translators: Sentence fragment, logical, "Includes" refers to products including a given product(s). Screenshot for context: https://cloudup.com/ccxhyH2mEDg */
label: _x( 'Includes', 'products', 'wc-admin' ),
},
{
value: 'excludes',
/* translators: Sentence fragment, logical, "Excludes" refers to products excluding a products(s). Screenshot for context: https://cloudup.com/ccxhyH2mEDg */
label: _x( 'Excludes', 'products', 'wc-admin' ),
},
],
input: {
component: 'Search',
type: 'products',
getLabels: getRequestByIdString( NAMESPACE + 'products', product => ( {
id: product.id,
label: product.name,
} ) ),
},
},
},
};
/*eslint-enable max-len*/

View File

@ -0,0 +1,38 @@
/** @format */
/**
* External dependencies
*/
import { Component, Fragment } from '@wordpress/element';
import PropTypes from 'prop-types';
/**
* WooCommerce dependencies
*/
import { ReportFilters } from '@woocommerce/components';
/**
* Internal dependencies
*/
import { filters, advancedFilters } from './config';
export default class DownloadsReport extends Component {
render() {
const { query, path } = this.props;
return (
<Fragment>
<ReportFilters
query={ query }
path={ path }
filters={ filters }
showDatePicker={ false }
advancedFilters={ advancedFilters }
/>
</Fragment>
);
}
}
DownloadsReport.propTypes = {
query: PropTypes.object.isRequired,
};

View File

@ -24,6 +24,8 @@ import RevenueReport from './revenue';
import CategoriesReport from './categories'; import CategoriesReport from './categories';
import CouponsReport from './coupons'; import CouponsReport from './coupons';
import TaxesReport from './taxes'; import TaxesReport from './taxes';
import DownloadsReport from './downloads';
import StockReport from './stock';
import CustomersReport from './customers'; import CustomersReport from './customers';
const REPORTS_FILTER = 'woocommerce-reports-list'; const REPORTS_FILTER = 'woocommerce-reports-list';
@ -60,6 +62,16 @@ const getReports = () => {
title: __( 'Taxes', 'wc-admin' ), title: __( 'Taxes', 'wc-admin' ),
component: TaxesReport, component: TaxesReport,
}, },
{
report: 'downloads',
title: __( 'Downloads', 'wc-admin' ),
component: DownloadsReport,
},
{
report: 'stock',
title: __( 'Stock', 'wc-admin' ),
component: StockReport,
},
{ {
report: 'customers', report: 'customers',
title: __( 'Customers', 'wc-admin' ), title: __( 'Customers', 'wc-admin' ),

View File

@ -250,6 +250,7 @@ class OrdersReportTable extends Component {
query={ query } query={ query }
tableData={ tableData } tableData={ tableData }
title={ __( 'Orders', 'wc-admin' ) } title={ __( 'Orders', 'wc-admin' ) }
columnPrefsKey="orders_report_columns"
/> />
); );
} }
@ -261,7 +262,7 @@ export default compose(
const datesFromQuery = getCurrentDates( query ); const datesFromQuery = getCurrentDates( query );
const filterQuery = getFilterQuery( 'orders', query ); const filterQuery = getFilterQuery( 'orders', query );
const { getOrders, getOrdersTotalCount, isGetOrdersError, isGetOrdersRequesting } = select( const { getOrders, getOrdersTotalCount, getOrdersError, isGetOrdersRequesting } = select(
'wc-api' 'wc-api'
); );
@ -277,14 +278,14 @@ export default compose(
}; };
const orders = getOrders( tableQuery ); const orders = getOrders( tableQuery );
const ordersTotalCount = getOrdersTotalCount( tableQuery ); const ordersTotalCount = getOrdersTotalCount( tableQuery );
const isError = isGetOrdersError( tableQuery ); const isError = Boolean( getOrdersError( tableQuery ) );
const isRequesting = isGetOrdersRequesting( tableQuery ); const isRequesting = isGetOrdersRequesting( tableQuery );
return { return {
tableData: { tableData: {
items: { items: {
data: formatTableOrders( orders ), data: formatTableOrders( orders ),
totalCount: ordersTotalCount, totalResults: ordersTotalCount,
}, },
isError, isError,
isRequesting, isRequesting,

View File

@ -178,6 +178,7 @@ export default class VariationsReportTable extends Component {
extended_info: true, extended_info: true,
} } } }
title={ __( 'Variations', 'wc-admin' ) } title={ __( 'Variations', 'wc-admin' ) }
columnPrefsKey="variations_report_columns"
/> />
); );
} }

View File

@ -214,6 +214,7 @@ export default class ProductsReportTable extends Component {
extended_info: true, extended_info: true,
} } } }
title={ __( 'Products', 'wc-admin' ) } title={ __( 'Products', 'wc-admin' ) }
columnPrefsKey="products_report_columns"
/> />
); );
} }

View File

@ -162,15 +162,15 @@ class RevenueReportTable extends Component {
} ); } );
} }
getSummary( totals, totalCount ) { getSummary( totals, totalResults ) {
if ( ! totals ) { if ( ! totals ) {
return []; return [];
} }
return [ return [
{ {
label: _n( 'day', 'days', totalCount, 'wc-admin' ), label: _n( 'day', 'days', totalResults, 'wc-admin' ),
value: numberFormat( totalCount ), value: numberFormat( totalResults ),
}, },
{ {
label: _n( 'order', 'orders', totals.orders_count, 'wc-admin' ), label: _n( 'order', 'orders', totals.orders_count, 'wc-admin' ),
@ -215,6 +215,7 @@ class RevenueReportTable extends Component {
query={ query } query={ query }
tableData={ tableData } tableData={ tableData }
title={ __( 'Revenue', 'wc-admin' ) } title={ __( 'Revenue', 'wc-admin' ) }
columnPrefsKey="revenue_report_columns"
/> />
); );
} }
@ -224,7 +225,7 @@ export default compose(
withSelect( ( select, props ) => { withSelect( ( select, props ) => {
const { query } = props; const { query } = props;
const datesFromQuery = getCurrentDates( query ); const datesFromQuery = getCurrentDates( query );
const { getReportStats, isReportStatsRequesting, isReportStatsError } = select( 'wc-api' ); const { getReportStats, getReportStatsError, isReportStatsRequesting } = select( 'wc-api' );
// TODO Support hour here when viewing a single day // TODO Support hour here when viewing a single day
const tableQuery = { const tableQuery = {
@ -237,14 +238,14 @@ export default compose(
before: appendTimestamp( datesFromQuery.primary.before, 'end' ), before: appendTimestamp( datesFromQuery.primary.before, 'end' ),
}; };
const revenueData = getReportStats( 'revenue', tableQuery ); const revenueData = getReportStats( 'revenue', tableQuery );
const isError = isReportStatsError( 'revenue', tableQuery ); const isError = Boolean( getReportStatsError( 'revenue', tableQuery ) );
const isRequesting = isReportStatsRequesting( 'revenue', tableQuery ); const isRequesting = isReportStatsRequesting( 'revenue', tableQuery );
return { return {
tableData: { tableData: {
items: { items: {
data: get( revenueData, [ 'data', 'intervals' ] ), data: get( revenueData, [ 'data', 'intervals' ] ),
totalCount: get( revenueData, [ 'totalResults' ] ), totalResults: get( revenueData, [ 'totalResults' ] ),
}, },
isError, isError,
isRequesting, isRequesting,

View File

@ -0,0 +1,22 @@
/** @format */
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
export const showDatePicker = false;
export const filters = [
{
label: __( 'Show', 'wc-admin' ),
staticParams: [],
param: 'type',
showFilters: () => true,
filters: [
{ label: __( 'All Products', 'wc-admin' ), value: 'all' },
{ label: __( 'Out of Stock', 'wc-admin' ), value: 'outofstock' },
{ label: __( 'Low Stock', 'wc-admin' ), value: 'lowstock' },
{ label: __( 'In Stock', 'wc-admin' ), value: 'instock' },
],
},
];

View File

@ -0,0 +1,39 @@
/** @format */
/**
* External dependencies
*/
import { Component, Fragment } from '@wordpress/element';
import PropTypes from 'prop-types';
/**
* WooCommerce dependencies
*/
import { ReportFilters } from '@woocommerce/components';
/**
* Internal dependencies
*/
import { showDatePicker, filters } from './config';
import StockReportTable from './table';
export default class StockReport extends Component {
render() {
const { query, path } = this.props;
return (
<Fragment>
<ReportFilters
query={ query }
path={ path }
showDatePicker={ showDatePicker }
filters={ filters }
/>
<StockReportTable query={ query } />
</Fragment>
);
}
}
StockReport.propTypes = {
query: PropTypes.object.isRequired,
};

View File

@ -0,0 +1,145 @@
/** @format */
/**
* External dependencies
*/
import { __, _n } from '@wordpress/i18n';
import { Component } from '@wordpress/element';
/**
* WooCommerce dependencies
*/
import { Link } from '@woocommerce/components';
import { getNewPath, getPersistedQuery } from '@woocommerce/navigation';
/**
* Internal dependencies
*/
import ReportTable from 'analytics/components/report-table';
import { numberFormat } from 'lib/number';
export default class StockReportTable extends Component {
constructor() {
super();
this.getHeadersContent = this.getHeadersContent.bind( this );
this.getRowsContent = this.getRowsContent.bind( this );
this.getSummary = this.getSummary.bind( this );
}
getHeadersContent() {
return [
{
label: __( 'Product / Variation', 'wc-admin' ),
key: 'product_variation',
required: true,
isLeftAligned: true,
},
{
label: __( 'SKU', 'wc-admin' ),
key: 'sku',
},
{
label: __( 'Status', 'wc-admin' ),
key: 'stock_status',
},
{
label: __( 'Stock', 'wc-admin' ),
key: 'stock_quantity',
isSortable: true,
defaultSort: true,
},
];
}
getRowsContent( products ) {
const { query } = this.props;
const persistedQuery = getPersistedQuery( query );
const { stockStatuses } = wcSettings;
return products.map( product => {
const { id, name, parent_id, sku, stock_quantity, stock_status } = product;
const productDetailLink = getNewPath( persistedQuery, 'products', {
filter: 'single_product',
products: parent_id || id,
} );
const formattedName = name.replace( ' - ', ' / ' );
const nameLink = (
<Link href={ productDetailLink } type="wc-admin">
{ formattedName }
</Link>
);
const stockStatusLink = (
<Link href={ 'post.php?action=edit&post=' + parent_id || id } type="wp-admin">
{ stockStatuses[ stock_status ] }
</Link>
);
return [
{
display: nameLink,
value: formattedName,
},
{
display: sku,
value: sku,
},
{
display: stockStatusLink,
value: stock_status,
},
{
display: numberFormat( stock_quantity ),
value: stock_quantity,
},
];
} );
}
getSummary( totals ) {
if ( ! totals ) {
return [];
}
return [
{
label: _n( 'product', 'products', totals.products, 'wc-admin' ),
value: numberFormat( totals.products ),
},
{
label: __( 'out of stock', totals.out_of_stock, 'wc-admin' ),
value: numberFormat( totals.out_of_stock ),
},
{
label: __( 'low stock', totals.low_stock, 'wc-admin' ),
value: numberFormat( totals.low_stock ),
},
{
label: __( 'in stock', totals.in_stock, 'wc-admin' ),
value: numberFormat( totals.in_stock ),
},
];
}
render() {
const { query } = this.props;
return (
<ReportTable
endpoint="stock"
getHeadersContent={ this.getHeadersContent }
getRowsContent={ this.getRowsContent }
// getSummary={ this.getSummary }
query={ query }
tableQuery={ {
orderby: query.orderby || 'stock_quantity',
order: query.order || 'desc',
type: query.type || 'all',
} }
title={ __( 'Stock', 'wc-admin' ) }
/>
);
}
}

View File

@ -11,6 +11,7 @@ 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';
/** /**
* Internal dependencies * Internal dependencies
@ -71,25 +72,23 @@ export default class TaxesReportTable extends Component {
getRowsContent( taxes ) { getRowsContent( taxes ) {
return map( taxes, tax => { return map( taxes, tax => {
const { order_tax, orders_count, tax_rate_id, total_tax, shipping_tax } = tax; const { order_tax, orders_count, tax_rate, tax_rate_id, total_tax, shipping_tax } = tax;
// @TODO must link to the tax detail report // @TODO must link to the tax detail report
const taxLink = ( const taxLink = (
<Link href="" type="wc-admin"> <Link href="" type="wc-admin">
{ tax_rate_id } { getTaxCode( tax ) }
</Link> </Link>
); );
return [ return [
// @TODO it should be the tax code, not the tax ID
{ {
display: taxLink, display: taxLink,
value: tax_rate_id, value: tax_rate_id,
}, },
{ {
// @TODO add `rate` once it's returned by the API display: tax_rate.toFixed( 2 ) + '%',
display: '', value: tax_rate,
value: '',
}, },
{ {
display: formatCurrency( total_tax ), display: formatCurrency( total_tax ),
@ -115,12 +114,10 @@ export default class TaxesReportTable extends Component {
if ( ! totals ) { if ( ! totals ) {
return []; return [];
} }
// @TODO the number of total rows should come from the API
const totalRows = 0;
return [ return [
{ {
label: _n( 'tax code', 'tax codes', totalRows, 'wc-admin' ), label: _n( 'tax code', 'tax codes', totals.tax_codes, 'wc-admin' ),
value: numberFormat( totalRows ), value: numberFormat( totals.tax_codes ),
}, },
{ {
label: __( 'total tax', 'wc-admin' ), label: __( 'total tax', 'wc-admin' ),
@ -135,7 +132,7 @@ export default class TaxesReportTable extends Component {
value: formatCurrency( totals.shipping_tax ), value: formatCurrency( totals.shipping_tax ),
}, },
{ {
label: _n( 'order', 'orders', totals.orders_count, 'wc-admin' ), label: _n( 'order', 'orders', totals.orders, 'wc-admin' ),
value: numberFormat( totals.orders_count ), value: numberFormat( totals.orders_count ),
}, },
]; ];
@ -153,7 +150,11 @@ export default class TaxesReportTable extends Component {
getSummary={ this.getSummary } getSummary={ this.getSummary }
itemIdField="tax_rate_id" itemIdField="tax_rate_id"
query={ query } query={ query }
tableQuery={ {
orderby: query.orderby || 'tax_rate_id',
} }
title={ __( 'Taxes', 'wc-admin' ) } title={ __( 'Taxes', 'wc-admin' ) }
columnPrefsKey="taxes_report_columns"
/> />
); );
} }

View File

@ -123,7 +123,7 @@ export class TopSellingProducts extends Component {
export default compose( export default compose(
withSelect( select => { withSelect( select => {
const { getReportStats, isReportStatsRequesting, isReportStatsError } = select( 'wc-admin' ); const { getReportStats, getReportStatsError, isReportStatsRequesting } = select( 'wc-admin' );
const endpoint = NAMESPACE + 'reports/products'; const endpoint = NAMESPACE + 'reports/products';
// @TODO We will need to add the date parameters from the Date Picker // @TODO We will need to add the date parameters from the Date Picker
// { after: '2018-04-22', before: '2018-05-06' } // { after: '2018-04-22', before: '2018-05-06' }
@ -131,7 +131,7 @@ export default compose(
const stats = getReportStats( endpoint, query ); const stats = getReportStats( endpoint, query );
const isRequesting = isReportStatsRequesting( endpoint, query ); const isRequesting = isReportStatsRequesting( endpoint, query );
const isError = isReportStatsError( endpoint, query ); const isError = Boolean( getReportStatsError( endpoint, query ) );
return { data: get( stats, 'data', [] ), isRequesting, isError }; return { data: get( stats, 'data', [] ), isRequesting, isError };
} ) } )

View File

@ -50,14 +50,14 @@ describe( 'TopSellingProducts', () => {
test( 'should load report stats from API', () => { test( 'should load report stats from API', () => {
const getReportStatsMock = jest.fn().mockReturnValue( { data: mockData } ); const getReportStatsMock = jest.fn().mockReturnValue( { data: mockData } );
const isReportStatsRequestingMock = jest.fn().mockReturnValue( false ); const isReportStatsRequestingMock = jest.fn().mockReturnValue( false );
const isReportStatsErrorMock = jest.fn().mockReturnValue( false ); const getReportStatsErrorMock = jest.fn().mockReturnValue( undefined );
const registry = createRegistry(); const registry = createRegistry();
registry.registerStore( 'wc-admin', { registry.registerStore( 'wc-admin', {
reducer: () => {}, reducer: () => {},
selectors: { selectors: {
getReportStats: getReportStatsMock, getReportStats: getReportStatsMock,
isReportStatsRequesting: isReportStatsRequestingMock, isReportStatsRequesting: isReportStatsRequestingMock,
isReportStatsError: isReportStatsErrorMock, getReportStatsError: getReportStatsErrorMock,
}, },
} ); } );
const topSellingProductsWrapper = TestRenderer.create( const topSellingProductsWrapper = TestRenderer.create(
@ -74,8 +74,8 @@ describe( 'TopSellingProducts', () => {
expect( getReportStatsMock.mock.calls[ 0 ][ 2 ] ).toEqual( query ); expect( getReportStatsMock.mock.calls[ 0 ][ 2 ] ).toEqual( query );
expect( isReportStatsRequestingMock.mock.calls[ 0 ][ 1 ] ).toBe( endpoint ); expect( isReportStatsRequestingMock.mock.calls[ 0 ][ 1 ] ).toBe( endpoint );
expect( isReportStatsRequestingMock.mock.calls[ 0 ][ 2 ] ).toEqual( query ); expect( isReportStatsRequestingMock.mock.calls[ 0 ][ 2 ] ).toEqual( query );
expect( isReportStatsErrorMock.mock.calls[ 0 ][ 1 ] ).toBe( endpoint ); expect( getReportStatsErrorMock.mock.calls[ 0 ][ 1 ] ).toBe( endpoint );
expect( isReportStatsErrorMock.mock.calls[ 0 ][ 2 ] ).toEqual( query ); expect( getReportStatsErrorMock.mock.calls[ 0 ][ 2 ] ).toEqual( query );
expect( topSellingProducts.props.data ).toBe( mockData ); expect( topSellingProducts.props.data ).toBe( mockData );
} ); } );
} ); } );

View File

@ -23,5 +23,6 @@
{ "component": "Summary", "render": "MySummaryList" }, { "component": "Summary", "render": "MySummaryList" },
{ "component": "Table" }, { "component": "Table" },
{ "component": "Tag" }, { "component": "Tag" },
{ "component": "TextControlWithAffixes" },
{ "component": "ViewMoreList" } { "component": "ViewMoreList" }
] ]

View File

@ -89,7 +89,7 @@ class InboxPanel extends Component {
export default compose( export default compose(
withSelect( select => { withSelect( select => {
const { getNotes, isGetNotesError, isGetNotesRequesting } = select( 'wc-api' ); const { getNotes, getNotesError, isGetNotesRequesting } = select( 'wc-api' );
const inboxQuery = { const inboxQuery = {
page: 1, page: 1,
per_page: QUERY_DEFAULTS.pageSize, per_page: QUERY_DEFAULTS.pageSize,
@ -97,7 +97,7 @@ export default compose(
}; };
const notes = getNotes( inboxQuery ); const notes = getNotes( inboxQuery );
const isError = isGetNotesError( inboxQuery ); const isError = Boolean( getNotesError( inboxQuery ) );
const isRequesting = isGetNotesRequesting( inboxQuery ); const isRequesting = isGetNotesRequesting( inboxQuery );
return { notes, isError, isRequesting }; return { notes, isError, isRequesting };

View File

@ -178,7 +178,7 @@ OrdersPanel.defaultProps = {
export default compose( export default compose(
withSelect( select => { withSelect( select => {
const { getOrders, isGetOrdersError, isGetOrdersRequesting } = select( 'wc-api' ); const { getOrders, getOrdersError, isGetOrdersRequesting } = select( 'wc-api' );
const ordersQuery = { const ordersQuery = {
page: 1, page: 1,
per_page: QUERY_DEFAULTS.pageSize, per_page: QUERY_DEFAULTS.pageSize,
@ -186,7 +186,7 @@ export default compose(
}; };
const orders = getOrders( ordersQuery ); const orders = getOrders( ordersQuery );
const isError = isGetOrdersError( ordersQuery ); const isError = Boolean( getOrdersError( ordersQuery ) );
const isRequesting = isGetOrdersRequesting( ordersQuery ); const isRequesting = isGetOrdersRequesting( ordersQuery );
return { orders, isError, isRequesting }; return { orders, isError, isRequesting };

View File

@ -172,7 +172,7 @@ ReviewsPanel.defaultProps = {
export default compose( export default compose(
withSelect( select => { withSelect( select => {
const { getReviews, isGetReviewsError, isGetReviewsRequesting } = select( 'wc-api' ); const { getReviews, getReviewsError, isGetReviewsRequesting } = select( 'wc-api' );
const reviewsQuery = { const reviewsQuery = {
page: 1, page: 1,
per_page: QUERY_DEFAULTS.pageSize, per_page: QUERY_DEFAULTS.pageSize,
@ -180,7 +180,7 @@ export default compose(
}; };
const reviews = getReviews( reviewsQuery ); const reviews = getReviews( reviewsQuery );
const isError = isGetReviewsError( reviewsQuery ); const isError = Boolean( getReviewsError( reviewsQuery ) );
const isRequesting = isGetReviewsRequesting( reviewsQuery ); const isRequesting = isGetReviewsRequesting( reviewsQuery );
return { reviews, isError, isRequesting }; return { reviews, isError, isRequesting };

View File

@ -20,7 +20,7 @@ export default {
async getReportItems( ...args ) { async getReportItems( ...args ) {
const [ endpoint, query ] = args.slice( -2 ); const [ endpoint, query ] = args.slice( -2 );
const swaggerEndpoints = [ 'categories', 'coupons', 'customers', 'taxes' ]; const swaggerEndpoints = [ 'categories', 'coupons' ];
if ( swaggerEndpoints.indexOf( endpoint ) >= 0 ) { if ( swaggerEndpoints.indexOf( endpoint ) >= 0 ) {
try { try {
const response = await fetch( const response = await fetch(

View File

@ -23,12 +23,12 @@ export default {
// TODO: Change to just `getNotes( endpoint, query )` // TODO: Change to just `getNotes( endpoint, query )`
// after Gutenberg plugin uses @wordpress/data 3+ // after Gutenberg plugin uses @wordpress/data 3+
const [ endpoint, query ] = args.length === 2 ? args : args.slice( 1, 3 ); const [ endpoint, query ] = args.length === 2 ? args : args.slice( 1, 3 );
const statEndpoints = [ 'orders', 'revenue', 'products' ]; const statEndpoints = [ 'orders', 'revenue', 'products', 'taxes' ];
let apiPath = endpoint + stringifyQuery( query ); let apiPath = endpoint + stringifyQuery( query );
// TODO: Remove once swagger endpoints are phased out. // TODO: Remove once swagger endpoints are phased out.
const swaggerEndpoints = [ 'categories', 'coupons', 'taxes' ]; const swaggerEndpoints = [ 'categories', 'coupons' ];
if ( swaggerEndpoints.indexOf( endpoint ) >= 0 ) { if ( swaggerEndpoints.indexOf( endpoint ) >= 0 ) {
apiPath = SWAGGERNAMESPACE + 'reports/' + endpoint + '/stats' + stringifyQuery( query ); apiPath = SWAGGERNAMESPACE + 'reports/' + endpoint + '/stats' + stringifyQuery( query );
try { try {

View File

@ -64,13 +64,13 @@ describe( 'getReportChartData()', () => {
beforeAll( () => { beforeAll( () => {
select( 'wc-api' ).getReportStats = jest.fn().mockReturnValue( {} ); select( 'wc-api' ).getReportStats = jest.fn().mockReturnValue( {} );
select( 'wc-api' ).isReportStatsRequesting = jest.fn().mockReturnValue( false ); select( 'wc-api' ).isReportStatsRequesting = jest.fn().mockReturnValue( false );
select( 'wc-api' ).isReportStatsError = jest.fn().mockReturnValue( false ); select( 'wc-api' ).getReportStatsError = jest.fn().mockReturnValue( false );
} ); } );
afterAll( () => { afterAll( () => {
select( 'wc-api' ).getReportStats.mockRestore(); select( 'wc-api' ).getReportStats.mockRestore();
select( 'wc-api' ).isReportStatsRequesting.mockRestore(); select( 'wc-api' ).isReportStatsRequesting.mockRestore();
select( 'wc-api' ).isReportStatsError.mockRestore(); select( 'wc-api' ).getReportStatsError.mockRestore();
} ); } );
function setGetReportStats( func ) { function setGetReportStats( func ) {
@ -78,13 +78,11 @@ describe( 'getReportChartData()', () => {
} }
function setIsReportStatsRequesting( func ) { function setIsReportStatsRequesting( func ) {
select( 'wc-api' ).isReportStatsRequesting.mockImplementation( ( ...args ) => select( 'wc-api' ).isReportStatsRequesting.mockImplementation( ( ...args ) => func( ...args ) );
func( ...args )
);
} }
function setIsReportStatsError( func ) { function setGetReportStatsError( func ) {
select( 'wc-api' ).isReportStatsError.mockImplementation( ( ...args ) => func( ...args ) ); select( 'wc-api' ).getReportStatsError.mockImplementation( ( ...args ) => func( ...args ) );
} }
it( 'returns isRequesting if first request is in progress', () => { it( 'returns isRequesting if first request is in progress', () => {
@ -99,8 +97,8 @@ describe( 'getReportChartData()', () => {
setIsReportStatsRequesting( () => { setIsReportStatsRequesting( () => {
return false; return false;
} ); } );
setIsReportStatsError( () => { setGetReportStatsError( () => {
return true; return { error: 'Error' };
} ); } );
const result = getReportChartData( 'revenue', 'primary', {}, select ); const result = getReportChartData( 'revenue', 'primary', {}, select );
expect( result ).toEqual( { ...response, isError: true } ); expect( result ).toEqual( { ...response, isError: true } );
@ -127,8 +125,8 @@ describe( 'getReportChartData()', () => {
setIsReportStatsRequesting( () => { setIsReportStatsRequesting( () => {
return false; return false;
} ); } );
setIsReportStatsError( () => { setGetReportStatsError( () => {
return false; return undefined;
} ); } );
setGetReportStats( () => { setGetReportStats( () => {
return { return {
@ -161,8 +159,8 @@ describe( 'getReportChartData()', () => {
setIsReportStatsRequesting( () => { setIsReportStatsRequesting( () => {
return false; return false;
} ); } );
setIsReportStatsError( () => { setGetReportStatsError( () => {
return false; return undefined;
} ); } );
setGetReportStats( ( endpoint, query ) => { setGetReportStats( ( endpoint, query ) => {
if ( 2 === query.page ) { if ( 2 === query.page ) {
@ -202,8 +200,8 @@ describe( 'getReportChartData()', () => {
} }
return false; return false;
} ); } );
setIsReportStatsError( () => { setGetReportStatsError( () => {
return false; return undefined;
} ); } );
const result = getReportChartData( 'revenue', 'primary', {}, select ); const result = getReportChartData( 'revenue', 'primary', {}, select );
@ -214,11 +212,11 @@ describe( 'getReportChartData()', () => {
setIsReportStatsRequesting( () => { setIsReportStatsRequesting( () => {
return false; return false;
} ); } );
setIsReportStatsError( ( endpoint, query ) => { setGetReportStatsError( ( endpoint, query ) => {
if ( 2 === query.page ) { if ( 2 === query.page ) {
return true; return { error: 'Error' };
} }
return false; return undefined;
} ); } );
const result = getReportChartData( 'revenue', 'primary', {}, select ); const result = getReportChartData( 'revenue', 'primary', {}, select );
expect( result ).toEqual( { ...response, isError: true } ); expect( result ).toEqual( { ...response, isError: true } );
@ -228,8 +226,8 @@ describe( 'getReportChartData()', () => {
setIsReportStatsRequesting( () => { setIsReportStatsRequesting( () => {
return false; return false;
} ); } );
setIsReportStatsError( () => { setGetReportStatsError( () => {
return false; return undefined;
} ); } );
setGetReportStats( () => { setGetReportStats( () => {
return { return {
@ -264,13 +262,13 @@ describe( 'getSummaryNumbers()', () => {
beforeAll( () => { beforeAll( () => {
select( 'wc-api' ).getReportStats = jest.fn().mockReturnValue( {} ); select( 'wc-api' ).getReportStats = jest.fn().mockReturnValue( {} );
select( 'wc-api' ).isReportStatsRequesting = jest.fn().mockReturnValue( false ); select( 'wc-api' ).isReportStatsRequesting = jest.fn().mockReturnValue( false );
select( 'wc-api' ).isReportStatsError = jest.fn().mockReturnValue( false ); select( 'wc-api' ).getReportStatsError = jest.fn().mockReturnValue( false );
} ); } );
afterAll( () => { afterAll( () => {
select( 'wc-api' ).getReportStats.mockRestore(); select( 'wc-api' ).getReportStats.mockRestore();
select( 'wc-api' ).isReportStatsRequesting.mockRestore(); select( 'wc-api' ).isReportStatsRequesting.mockRestore();
select( 'wc-api' ).isReportStatsError.mockRestore(); select( 'wc-api' ).getReportStatsError.mockRestore();
} ); } );
function setGetReportStats( func ) { function setGetReportStats( func ) {
@ -278,13 +276,11 @@ describe( 'getSummaryNumbers()', () => {
} }
function setIsReportStatsRequesting( func ) { function setIsReportStatsRequesting( func ) {
select( 'wc-api' ).isReportStatsRequesting.mockImplementation( ( ...args ) => select( 'wc-api' ).isReportStatsRequesting.mockImplementation( ( ...args ) => func( ...args ) );
func( ...args )
);
} }
function setIsReportStatsError( func ) { function setGetReportStatsError( func ) {
select( 'wc-api' ).isReportStatsError.mockImplementation( ( ...args ) => func( ...args ) ); select( 'wc-api' ).getReportStatsError.mockImplementation( ( ...args ) => func( ...args ) );
} }
it( 'returns isRequesting if a request is in progress', () => { it( 'returns isRequesting if a request is in progress', () => {
@ -299,8 +295,8 @@ describe( 'getSummaryNumbers()', () => {
setIsReportStatsRequesting( () => { setIsReportStatsRequesting( () => {
return false; return false;
} ); } );
setIsReportStatsError( () => { setGetReportStatsError( () => {
return true; return { error: 'Error' };
} ); } );
const result = getSummaryNumbers( 'revenue', query, select ); const result = getSummaryNumbers( 'revenue', query, select );
expect( result ).toEqual( { ...response, isError: true } ); expect( result ).toEqual( { ...response, isError: true } );
@ -321,8 +317,8 @@ describe( 'getSummaryNumbers()', () => {
setIsReportStatsRequesting( () => { setIsReportStatsRequesting( () => {
return false; return false;
} ); } );
setIsReportStatsError( () => { setGetReportStatsError( () => {
return false; return undefined;
} ); } );
setGetReportStats( () => { setGetReportStats( () => {
return { return {
@ -464,31 +460,29 @@ describe( 'getReportTableData()', () => {
beforeAll( () => { beforeAll( () => {
select( 'wc-api' ).getReportItems = jest.fn().mockReturnValue( {} ); select( 'wc-api' ).getReportItems = jest.fn().mockReturnValue( {} );
select( 'wc-api' ).isReportItemsRequesting = jest.fn().mockReturnValue( false ); select( 'wc-api' ).isReportItemsRequesting = jest.fn().mockReturnValue( false );
select( 'wc-api' ).isReportItemsError = jest.fn().mockReturnValue( false ); select( 'wc-api' ).getReportItemsError = jest.fn().mockReturnValue( undefined );
} ); } );
afterAll( () => { afterAll( () => {
select( 'wc-api' ).getReportItems.mockRestore(); select( 'wc-api' ).getReportItems.mockRestore();
select( 'wc-api' ).isReportItemsRequesting.mockRestore(); select( 'wc-api' ).isReportItemsRequesting.mockRestore();
select( 'wc-api' ).isReportItemsError.mockRestore(); select( 'wc-api' ).getReportItemsError.mockRestore();
} ); } );
function setGetReportItems( func ) { function setGetReportItems( func ) {
select( 'wc-api' ).getReportItems.mockImplementation( ( ...args ) => func( ...args ) ); select( 'wc-api' ).getReportItems.mockImplementation( ( ...args ) => func( ...args ) );
} }
function setisReportItemsRequesting( func ) { function setIsReportItemsRequesting( func ) {
select( 'wc-api' ).isReportItemsRequesting.mockImplementation( ( ...args ) => select( 'wc-api' ).isReportItemsRequesting.mockImplementation( ( ...args ) => func( ...args ) );
func( ...args )
);
} }
function setisReportItemsError( func ) { function setGetReportItemsError( func ) {
select( 'wc-api' ).isReportItemsError.mockImplementation( ( ...args ) => func( ...args ) ); select( 'wc-api' ).getReportItemsError.mockImplementation( ( ...args ) => func( ...args ) );
} }
it( 'returns isRequesting if a request is in progress', () => { it( 'returns isRequesting if a request is in progress', () => {
setisReportItemsRequesting( () => true ); setIsReportItemsRequesting( () => true );
const result = getReportTableData( 'coupons', query, select ); const result = getReportTableData( 'coupons', query, select );
@ -498,12 +492,12 @@ describe( 'getReportTableData()', () => {
'coupons', 'coupons',
query query
); );
expect( select( 'wc-api' ).isReportItemsError ).toHaveBeenCalledTimes( 0 ); expect( select( 'wc-api' ).getReportItemsError ).toHaveBeenCalledTimes( 0 );
} ); } );
it( 'returns isError if request errors', () => { it( 'returns isError if request errors', () => {
setisReportItemsRequesting( () => false ); setIsReportItemsRequesting( () => false );
setisReportItemsError( () => true ); setGetReportItemsError( () => ( { error: 'Error' } ) );
const result = getReportTableData( 'coupons', query, select ); const result = getReportTableData( 'coupons', query, select );
@ -513,16 +507,13 @@ describe( 'getReportTableData()', () => {
'coupons', 'coupons',
query query
); );
expect( select( 'wc-api' ).isReportItemsError ).toHaveBeenLastCalledWith( expect( select( 'wc-api' ).getReportItemsError ).toHaveBeenLastCalledWith( 'coupons', query );
'coupons',
query
);
} ); } );
it( 'returns results after queries finish', () => { it( 'returns results after queries finish', () => {
const items = [ { id: 1 }, { id: 2 }, { id: 3 } ]; const items = [ { id: 1 }, { id: 2 }, { id: 3 } ];
setisReportItemsRequesting( () => false ); setIsReportItemsRequesting( () => false );
setisReportItemsError( () => false ); setGetReportItemsError( () => undefined );
setGetReportItems( () => items ); setGetReportItems( () => items );
const result = getReportTableData( 'coupons', query, select ); const result = getReportTableData( 'coupons', query, select );
@ -533,9 +524,6 @@ describe( 'getReportTableData()', () => {
'coupons', 'coupons',
query query
); );
expect( select( 'wc-api' ).isReportItemsError ).toHaveBeenLastCalledWith( expect( select( 'wc-api' ).getReportItemsError ).toHaveBeenLastCalledWith( 'coupons', query );
'coupons',
query
);
} ); } );
} ); } );

View File

@ -141,7 +141,7 @@ function getRequestQuery( endpoint, dataType, query ) {
* @return {Object} Object containing summary number responses. * @return {Object} Object containing summary number responses.
*/ */
export function getSummaryNumbers( endpoint, query, select ) { export function getSummaryNumbers( endpoint, query, select ) {
const { getReportStats, isReportStatsRequesting, isReportStatsError } = select( 'wc-api' ); const { getReportStats, getReportStatsError, isReportStatsRequesting } = select( 'wc-api' );
const response = { const response = {
isRequesting: false, isRequesting: false,
isError: false, isError: false,
@ -155,7 +155,7 @@ export function getSummaryNumbers( endpoint, query, select ) {
const primary = getReportStats( endpoint, primaryQuery ); const primary = getReportStats( endpoint, primaryQuery );
if ( isReportStatsRequesting( endpoint, primaryQuery ) ) { if ( isReportStatsRequesting( endpoint, primaryQuery ) ) {
return { ...response, isRequesting: true }; return { ...response, isRequesting: true };
} else if ( isReportStatsError( endpoint, primaryQuery ) ) { } else if ( getReportStatsError( endpoint, primaryQuery ) ) {
return { ...response, isError: true }; return { ...response, isError: true };
} }
@ -165,7 +165,7 @@ export function getSummaryNumbers( endpoint, query, select ) {
const secondary = getReportStats( endpoint, secondaryQuery ); const secondary = getReportStats( endpoint, secondaryQuery );
if ( isReportStatsRequesting( endpoint, secondaryQuery ) ) { if ( isReportStatsRequesting( endpoint, secondaryQuery ) ) {
return { ...response, isRequesting: true }; return { ...response, isRequesting: true };
} else if ( isReportStatsError( endpoint, secondaryQuery ) ) { } else if ( getReportStatsError( endpoint, secondaryQuery ) ) {
return { ...response, isError: true }; return { ...response, isError: true };
} }
@ -184,7 +184,7 @@ export function getSummaryNumbers( endpoint, query, select ) {
* @return {Object} Object containing API request information (response, fetching, and error details) * @return {Object} Object containing API request information (response, fetching, and error details)
*/ */
export function getReportChartData( endpoint, dataType, query, select ) { export function getReportChartData( endpoint, dataType, query, select ) {
const { getReportStats, isReportStatsRequesting, isReportStatsError } = select( 'wc-api' ); const { getReportStats, getReportStatsError, isReportStatsRequesting } = select( 'wc-api' );
const response = { const response = {
isEmpty: false, isEmpty: false,
@ -201,7 +201,7 @@ export function getReportChartData( endpoint, dataType, query, select ) {
if ( isReportStatsRequesting( endpoint, requestQuery ) ) { if ( isReportStatsRequesting( endpoint, requestQuery ) ) {
return { ...response, isRequesting: true }; return { ...response, isRequesting: true };
} else if ( isReportStatsError( endpoint, requestQuery ) ) { } else if ( getReportStatsError( endpoint, requestQuery ) ) {
return { ...response, isError: true }; return { ...response, isError: true };
} else if ( isReportDataEmpty( stats ) ) { } else if ( isReportDataEmpty( stats ) ) {
return { ...response, isEmpty: true }; return { ...response, isEmpty: true };
@ -224,7 +224,7 @@ export function getReportChartData( endpoint, dataType, query, select ) {
if ( isReportStatsRequesting( endpoint, nextQuery ) ) { if ( isReportStatsRequesting( endpoint, nextQuery ) ) {
continue; continue;
} }
if ( isReportStatsError( endpoint, nextQuery ) ) { if ( getReportStatsError( endpoint, nextQuery ) ) {
isError = true; isError = true;
isFetching = false; isFetching = false;
break; break;
@ -298,7 +298,7 @@ export function getReportTableQuery( endpoint, urlQuery, query ) {
* @return {Object} Object Table data response * @return {Object} Object Table data response
*/ */
export function getReportTableData( endpoint, urlQuery, select, query = {} ) { export function getReportTableData( endpoint, urlQuery, select, query = {} ) {
const { getReportItems, isReportItemsRequesting, isReportItemsError } = select( 'wc-api' ); const { getReportItems, getReportItemsError, isReportItemsRequesting } = select( 'wc-api' );
const tableQuery = reportsUtils.getReportTableQuery( endpoint, urlQuery, query ); const tableQuery = reportsUtils.getReportTableQuery( endpoint, urlQuery, query );
const response = { const response = {
@ -313,7 +313,7 @@ export function getReportTableData( endpoint, urlQuery, select, query = {} ) {
const items = getReportItems( endpoint, tableQuery ); const items = getReportItems( endpoint, tableQuery );
if ( isReportItemsRequesting( endpoint, tableQuery ) ) { if ( isReportItemsRequesting( endpoint, tableQuery ) ) {
return { ...response, isRequesting: true }; return { ...response, isRequesting: true };
} else if ( isReportItemsError( endpoint, tableQuery ) ) { } else if ( getReportItemsError( endpoint, tableQuery ) ) {
return { ...response, isError: true }; return { ...response, isError: true };
} }

View File

@ -0,0 +1,11 @@
/** @format */
/**
* Internal dependencies
*/
import operations from './operations';
import selectors from './selectors';
export default {
operations,
selectors,
};

View File

@ -0,0 +1,47 @@
/** @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, 'customers-query' ) );
return filteredNames.map( async resourceName => {
const query = getResourceIdentifier( resourceName );
const url = `${ NAMESPACE }/customers${ stringifyQuery( query ) }`;
try {
const customers = await fetch( { path: url } );
const ids = customers.map( customer => customer.id );
const customerResources = customers.reduce( ( resources, customer ) => {
resources[ getResourceName( 'customer', customer.id ) ] = { data: customer };
return resources;
}, {} );
return {
[ resourceName ]: {
data: ids,
},
...customerResources,
};
} catch ( error ) {
return { [ resourceName ]: { error } };
}
} );
}
export default {
read,
};

View File

@ -0,0 +1,43 @@
/** @format */
/**
* External dependencies
*/
import { isNil } from 'lodash';
/**
* Internal dependencies
*/
import { getResourceName } from '../utils';
import { DEFAULT_REQUIREMENT } from '../constants';
const getCustomers = ( getResource, requireResource ) => (
query = {},
requirement = DEFAULT_REQUIREMENT
) => {
const resourceName = getResourceName( 'customers-query', query );
const ids = requireResource( requirement, resourceName ).data || [];
return ids.map( id => getResource( getResourceName( 'customer', id ) ).data || {} );
};
const getCustomersError = getResource => ( query = {} ) => {
const resourceName = getResourceName( 'customers-query', query );
return getResource( resourceName ).error;
};
const isGetCustomersRequesting = getResource => ( query = {} ) => {
const resourceName = getResourceName( 'customers-query', query );
const { lastRequested, lastReceived } = getResource( resourceName );
if ( isNil( lastRequested ) || isNil( lastReceived ) ) {
return true;
}
return lastRequested > lastReceived;
};
export default {
getCustomers,
getCustomersError,
isGetCustomersRequesting,
};

View File

@ -21,6 +21,11 @@ const getNotes = ( getResource, requireResource ) => (
return notes; return notes;
}; };
const getNotesError = getResource => ( query = {} ) => {
const resourceName = getResourceName( 'note-query', query );
return getResource( resourceName ).error;
};
const isGetNotesRequesting = getResource => ( query = {} ) => { const isGetNotesRequesting = getResource => ( query = {} ) => {
const resourceName = getResourceName( 'note-query', query ); const resourceName = getResourceName( 'note-query', query );
const { lastRequested, lastReceived } = getResource( resourceName ); const { lastRequested, lastReceived } = getResource( resourceName );
@ -36,13 +41,8 @@ const isGetNotesRequesting = getResource => ( query = {} ) => {
return lastRequested > lastReceived; return lastRequested > lastReceived;
}; };
const isGetNotesError = getResource => ( query = {} ) => {
const resourceName = getResourceName( 'note-query', query );
return getResource( resourceName ).error;
};
export default { export default {
getNotes, getNotes,
getNotesError,
isGetNotesRequesting, isGetNotesRequesting,
isGetNotesError,
}; };

View File

@ -21,6 +21,11 @@ const getOrders = ( getResource, requireResource ) => (
return orders; return orders;
}; };
const getOrdersError = getResource => ( query = {} ) => {
const resourceName = getResourceName( 'order-query', query );
return getResource( resourceName ).error;
};
const getOrdersTotalCount = ( getResource, requireResource ) => ( const getOrdersTotalCount = ( getResource, requireResource ) => (
query = {}, query = {},
requirement = DEFAULT_REQUIREMENT requirement = DEFAULT_REQUIREMENT
@ -40,14 +45,9 @@ const isGetOrdersRequesting = getResource => ( query = {} ) => {
return lastRequested > lastReceived; return lastRequested > lastReceived;
}; };
const isGetOrdersError = getResource => ( query = {} ) => {
const resourceName = getResourceName( 'order-query', query );
return getResource( resourceName ).error;
};
export default { export default {
getOrders, getOrders,
getOrdersError,
getOrdersTotalCount, getOrdersTotalCount,
isGetOrdersRequesting, isGetOrdersRequesting,
isGetOrdersError,
}; };

View File

@ -17,7 +17,7 @@ import { NAMESPACE } from '../../constants';
import { SWAGGERNAMESPACE } from 'store/constants'; import { SWAGGERNAMESPACE } from 'store/constants';
// TODO: Remove once swagger endpoints are phased out. // TODO: Remove once swagger endpoints are phased out.
const swaggerEndpoints = [ 'categories', 'coupons', 'customers', 'taxes' ]; const swaggerEndpoints = [ 'categories', 'coupons', 'customers' ];
const typeEndpointMap = { const typeEndpointMap = {
'report-items-query-orders': 'orders', 'report-items-query-orders': 'orders',
@ -28,6 +28,7 @@ const typeEndpointMap = {
'report-items-query-taxes': 'taxes', 'report-items-query-taxes': 'taxes',
'report-items-query-variations': 'variations', 'report-items-query-variations': 'variations',
'report-items-query-customers': 'customers', 'report-items-query-customers': 'customers',
'report-items-query-stock': 'stock',
}; };
function read( resourceNames, fetch = apiFetch ) { function read( resourceNames, fetch = apiFetch ) {

View File

@ -20,6 +20,11 @@ const getReportItems = ( getResource, requireResource ) => (
return requireResource( requirement, resourceName ) || {}; return requireResource( requirement, resourceName ) || {};
}; };
const getReportItemsError = getResource => ( type, query = {} ) => {
const resourceName = getResourceName( `report-items-query-${ type }`, query );
return getResource( resourceName ).error;
};
const isReportItemsRequesting = getResource => ( type, query = {} ) => { const isReportItemsRequesting = getResource => ( type, query = {} ) => {
const resourceName = getResourceName( `report-items-query-${ type }`, query ); const resourceName = getResourceName( `report-items-query-${ type }`, query );
const { lastRequested, lastReceived } = getResource( resourceName ); const { lastRequested, lastReceived } = getResource( resourceName );
@ -31,13 +36,8 @@ const isReportItemsRequesting = getResource => ( type, query = {} ) => {
return lastRequested > lastReceived; return lastRequested > lastReceived;
}; };
const isReportItemsError = getResource => ( type, query = {} ) => {
const resourceName = getResourceName( `report-items-query-${ type }`, query );
return getResource( resourceName ).error;
};
export default { export default {
getReportItems, getReportItems,
getReportItemsError,
isReportItemsRequesting, isReportItemsRequesting,
isReportItemsError,
}; };

View File

@ -16,9 +16,9 @@ import { getResourceIdentifier, getResourcePrefix } from '../../utils';
import { NAMESPACE } from '../../constants'; import { NAMESPACE } from '../../constants';
import { SWAGGERNAMESPACE } from 'store/constants'; import { SWAGGERNAMESPACE } from 'store/constants';
const statEndpoints = [ 'orders', 'revenue', 'products' ]; const statEndpoints = [ 'orders', 'revenue', 'products', 'taxes' ];
// TODO: Remove once swagger endpoints are phased out. // TODO: Remove once swagger endpoints are phased out.
const swaggerEndpoints = [ 'categories', 'coupons', 'taxes' ]; const swaggerEndpoints = [ 'categories', 'coupons' ];
const typeEndpointMap = { const typeEndpointMap = {
'report-stats-query-orders': 'orders', 'report-stats-query-orders': 'orders',

View File

@ -22,6 +22,11 @@ const getReportStats = ( getResource, requireResource ) => (
return data; return data;
}; };
const getReportStatsError = getResource => ( type, query = {} ) => {
const resourceName = getResourceName( `report-stats-query-${ type }`, query );
return getResource( resourceName ).error;
};
const isReportStatsRequesting = getResource => ( type, query = {} ) => { const isReportStatsRequesting = getResource => ( type, query = {} ) => {
const resourceName = getResourceName( `report-stats-query-${ type }`, query ); const resourceName = getResourceName( `report-stats-query-${ type }`, query );
const { lastRequested, lastReceived } = getResource( resourceName ); const { lastRequested, lastReceived } = getResource( resourceName );
@ -33,13 +38,8 @@ const isReportStatsRequesting = getResource => ( type, query = {} ) => {
return lastRequested > lastReceived; return lastRequested > lastReceived;
}; };
const isReportStatsError = getResource => ( type, query = {} ) => {
const resourceName = getResourceName( `report-stats-query-${ type }`, query );
return getResource( resourceName ).error;
};
export default { export default {
getReportStats, getReportStats,
getReportStatsError,
isReportStatsRequesting, isReportStatsRequesting,
isReportStatsError,
}; };

View File

@ -29,6 +29,11 @@ const getReviewsTotalCount = ( getResource, requireResource ) => (
return requireResource( requirement, resourceName ).totalCount || 0; return requireResource( requirement, resourceName ).totalCount || 0;
}; };
const getReviewsError = getResource => ( query = {} ) => {
const resourceName = getResourceName( 'review-query', query );
return getResource( resourceName ).error;
};
const isGetReviewsRequesting = getResource => ( query = {} ) => { const isGetReviewsRequesting = getResource => ( query = {} ) => {
const resourceName = getResourceName( 'review-query', query ); const resourceName = getResourceName( 'review-query', query );
const { lastRequested, lastReceived } = getResource( resourceName ); const { lastRequested, lastReceived } = getResource( resourceName );
@ -40,14 +45,9 @@ const isGetReviewsRequesting = getResource => ( query = {} ) => {
return lastRequested > lastReceived; return lastRequested > lastReceived;
}; };
const isGetReviewsError = getResource => ( query = {} ) => {
const resourceName = getResourceName( 'review-query', query );
return getResource( resourceName ).error;
};
export default { export default {
getReviews, getReviews,
getReviewsError,
getReviewsTotalCount, getReviewsTotalCount,
isGetReviewsRequesting, isGetReviewsRequesting,
isGetReviewsError,
}; };

View File

@ -0,0 +1,13 @@
/** @format */
/**
* Internal dependencies
*/
import operations from './operations';
import selectors from './selectors';
import mutations from './mutations';
export default {
operations,
selectors,
mutations,
};

View File

@ -0,0 +1,10 @@
/** @format */
const updateCurrentUserData = operations => userDataFields => {
const resourceKey = 'current-user-data';
operations.update( [ resourceKey ], { [ resourceKey ]: userDataFields } );
};
export default {
updateCurrentUserData,
};

View File

@ -0,0 +1,75 @@
/** @format */
/**
* External dependencies
*/
import apiFetch from '@wordpress/api-fetch';
import { mapValues, pick } from 'lodash';
function read( resourceNames, fetch = apiFetch ) {
return [ ...readCurrentUserData( resourceNames, fetch ) ];
}
function update( resourceNames, data, fetch = apiFetch ) {
return [ ...updateCurrentUserData( resourceNames, data, fetch ) ];
}
function readCurrentUserData( resourceNames, fetch ) {
if ( resourceNames.includes( 'current-user-data' ) ) {
const url = '/wp/v2/users/me?context=edit';
return [
fetch( { path: url } )
.then( userToUserDataResource )
.catch( error => {
return { [ 'current-user-data' ]: { error: String( error.message ) } };
} ),
];
}
return [];
}
function updateCurrentUserData( resourceNames, data, fetch ) {
const resourceName = 'current-user-data';
const userDataFields = [
'categories_report_columns',
'coupons_report_columns',
'customers_report_columns',
'orders_report_columns',
'products_report_columns',
'revenue_report_columns',
'taxes_report_columns',
'variations_report_columns',
];
if ( resourceNames.includes( resourceName ) ) {
const url = '/wp/v2/users/me';
const userData = pick( data[ resourceName ], userDataFields );
const meta = mapValues( userData, JSON.stringify );
const user = { woocommerce_meta: meta };
return [
fetch( { path: url, method: 'POST', data: user } )
.then( userToUserDataResource )
.catch( error => {
return { [ resourceName ]: { error } };
} ),
];
}
return [];
}
function userToUserDataResource( user ) {
const userData = mapValues( user.woocommerce_meta, data => {
if ( ! data || 0 === data.length ) {
return '';
}
return JSON.parse( data );
} );
return { [ 'current-user-data' ]: { data: userData } };
}
export default {
read,
update,
};

View File

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

View File

@ -3,31 +3,43 @@
/** /**
* Internal dependencies * Internal dependencies
*/ */
import customers from './customers';
import notes from './notes'; import notes from './notes';
import orders from './orders'; 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 user from './user';
function createWcApiSpec() { function createWcApiSpec() {
return { return {
mutations: {
...user.mutations,
},
selectors: { selectors: {
...customers.selectors,
...notes.selectors, ...notes.selectors,
...orders.selectors, ...orders.selectors,
...reportItems.selectors, ...reportItems.selectors,
...reportStats.selectors, ...reportStats.selectors,
...reviews.selectors, ...reviews.selectors,
...user.selectors,
}, },
operations: { operations: {
read( resourceNames ) { read( resourceNames ) {
return [ return [
...customers.operations.read( resourceNames ),
...notes.operations.read( resourceNames ), ...notes.operations.read( resourceNames ),
...orders.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 ),
...user.operations.read( resourceNames ),
]; ];
}, },
update( resourceNames, data ) {
return [ ...user.operations.update( resourceNames, data ) ];
},
}, },
}; };
} }

View File

@ -43,9 +43,9 @@ const withSelect = mapSelectToProps =>
super( props ); super( props );
this.onStoreChange = this.onStoreChange.bind( this ); this.onStoreChange = this.onStoreChange.bind( this );
this.subscribe( props.registry ); this.subscribe( props.registry );
this.onUnmounts = {};
this.mergeProps = this.getNextMergeProps( props ); this.mergeProps = this.getNextMergeProps( props );
} }
@ -59,6 +59,7 @@ const withSelect = mapSelectToProps =>
getNextMergeProps( props ) { getNextMergeProps( props ) {
const storeSelectors = {}; const storeSelectors = {};
const onCompletes = []; const onCompletes = [];
const onUnmounts = {};
const componentContext = { component: this }; const componentContext = { component: this };
const getStoreFromRegistry = ( key, registry, context ) => { const getStoreFromRegistry = ( key, registry, context ) => {
@ -69,8 +70,9 @@ const withSelect = mapSelectToProps =>
if ( isFunction( selectorsForKey ) ) { if ( isFunction( selectorsForKey ) ) {
// This store has special handling for its selectors. // This store has special handling for its selectors.
// We give it a context, and we check for a "resolve" // We give it a context, and we check for a "resolve"
const { selectors, onComplete } = selectorsForKey( context ); const { selectors, onComplete, onUnmount } = selectorsForKey( context );
onComplete && onCompletes.push( onComplete ); onComplete && onCompletes.push( onComplete );
onUnmount && ( onUnmounts[ key ] = onUnmount );
storeSelectors[ key ] = selectors; storeSelectors[ key ] = selectors;
} else { } else {
storeSelectors[ key ] = selectorsForKey; storeSelectors[ key ] = selectorsForKey;
@ -107,6 +109,7 @@ const withSelect = mapSelectToProps =>
componentWillUnmount() { componentWillUnmount() {
this.canRunSelection = false; this.canRunSelection = false;
this.unsubscribe(); this.unsubscribe();
Object.keys( this.onUnmounts ).forEach( key => this.onUnmounts[ key ]() );
} }
shouldComponentUpdate( nextProps, nextState ) { shouldComponentUpdate( nextProps, nextState ) {

View File

@ -39,8 +39,8 @@ function createWcApiStore() {
return getComponentSelectors( component ); return getComponentSelectors( component );
}, },
getActions() { getActions() {
// TODO: Add mutations here. const mutations = apiClient.getMutations();
return {}; return mutations;
}, },
subscribe: apiClient.subscribe, subscribe: apiClient.subscribe,
}; };

View File

@ -1,6 +1,7 @@
* [Overview](/) * [Overview](/)
* [Components](components/) * [Components](components/)
* [Data](data) * [Data](data)
* [Documentation](documentation)
* [Layout](layout) * [Layout](layout)
* [CSS Structure](stylesheets) * [CSS Structure](stylesheets)
* [Examples](examples/) * [Examples](examples/)

View File

@ -27,4 +27,5 @@
* [Summary](components/summary.md) * [Summary](components/summary.md)
* [Table](components/table.md) * [Table](components/table.md)
* [Tag](components/tag.md) * [Tag](components/tag.md)
* [TextControlWithAffixes](components/text-control-with-affixes.md)
* [ViewMoreList](components/view-more-list.md) * [ViewMoreList](components/view-more-list.md)

View File

@ -0,0 +1,69 @@
`TextControlWithAffixes` (component)
====================================
This component is essentially a wrapper (really a reimplementation) around the
TextControl component that adds support for affixes, i.e. the ability to display
a fixed part either at the beginning or at the end of the text input.
Props
-----
### `label`
- Type: String
- Default: null
If this property is added, a label will be generated using label property as the content.
### `help`
- Type: String
- Default: null
If this property is added, a help text will be generated using help property as the content.
### `type`
- Type: String
- Default: `'text'`
Type of the input element to render. Defaults to "text".
### `value`
- **Required**
- Type: String
- Default: null
The current value of the input.
### `className`
- Type: String
- Default: null
The class that will be added with "components-base-control" to the classes of the wrapper div.
If no className is passed only components-base-control is used.
### `onChange`
- **Required**
- Type: Function
- Default: null
A function that receives the value of the input.
### `prefix`
- Type: ReactNode
- Default: null
Markup to be inserted at the beginning of the input.
### `suffix`
- Type: ReactNode
- Default: null
Markup to be appended at the end of the input.

View File

@ -0,0 +1,23 @@
# How To Document Components
We rely on inline documentation and a markdown example file for both [the docs site on github.io,](https://woocommerce.github.io/wc-admin/#/) and the DevDocs section in wp-admin. This allows us to keep the docs site up-to-date if any components change, because the documentation is kept in the same place as the code. Read on for how to build out the docs files and devdocs section.
## 1. Add the inline documentation to generate a markdown file
This consists of a docblock for a component description at the top of the component, correct `PropTypes` definitions for all props used, and docblocks for each prop in PropTypes. See [count/index.js](https://github.com/woocommerce/wc-admin/blob/master/packages/components/src/count/index.js) for a simple example, or [table/table.js](https://github.com/woocommerce/wc-admin/blob/master/packages/components/src/table/table.js) for a very detailed example.
## 2. Generate the docs with `npm run docs`
This creates the file in `/docs/components/` for your new component, or adds it to the existing component doc (if this is a sub-component, like TablePlaceholder).
You can test this by running `npx docsify serve docs`, it will spin up a server at localhost:3000 to preview the docs. This also live-reloads as you're making changes.
## 3. Add an `example.md` file
You can use [`card/example.md`](https://raw.githubusercontent.com/woocommerce/wc-admin/master/packages/components/src/card/example.md) as a simple example, or [`pagination/example.md`](https://raw.githubusercontent.com/woocommerce/wc-admin/master/packages/components/src/pagination/example.md) as an example with state ([using `withState` HoC from gutenberg](https://github.com/WordPress/gutenberg/tree/master/packages/compose/src/with-state)).
## 4. Add your example to `client/devdocs/examples.json`
Keep these alphabetized. Optional properties here are `render` and `filePath`. `render` defaults to `My{ComponentName}`, and `filePath` defaults to `/docs/component/{component-name-as-slug}`.
Now you can visit `/wp-admin/admin.php?page=wc-admin#/devdocs` to see your component in action.

View File

@ -0,0 +1,51 @@
<?php
/**
* REST API Customers Controller
*
* Handles requests to /customers/*
*
* @package WooCommerce Admin/API
*/
defined( 'ABSPATH' ) || exit;
/**
* Customers controller.
*
* @package WooCommerce Admin/API
* @extends WC_REST_Customers_Controller
*/
class WC_Admin_REST_Customers_Controller extends WC_REST_Customers_Controller {
// TODO Add support for guests here. See https://wp.me/p7bje6-1dM.
/**
* Searches emails by partial search instead of a strict match.
* See "search parameters" under https://codex.wordpress.org/Class_Reference/WP_User_Query.
*
* @param array $prepared_args Prepared search filter args from the customer endpoint.
* @param array $request Request/query arguments.
* @return array
*/
public static function update_search_filters( $prepared_args, $request ) {
if ( ! empty( $request['email'] ) ) {
$prepared_args['search'] = '*' . $prepared_args['search'] . '*';
}
return $prepared_args;
}
/**
* Get the query params for collections.
*
* @return array
*/
public function get_collection_params() {
$params = parent::get_collection_params();
// Allow partial email matches. Previously, this was of format 'email' which required a strict "test@example.com" format.
// This, in combination with `update_search_filters` allows us to do partial searches.
$params['email']['format'] = '';
return $params;
}
}
add_filter( 'woocommerce_rest_customer_query', array( 'WC_Admin_REST_Customers_Controller', 'update_search_filters' ), 10, 2 );

View File

@ -0,0 +1,393 @@
<?php
/**
* REST API Reports stock controller
*
* Handles requests to the /reports/stock endpoint.
*
* @package WooCommerce Admin/API
*/
defined( 'ABSPATH' ) || exit;
/**
* REST API Reports stock controller class.
*
* @package WooCommerce/API
* @extends WC_REST_Reports_Controller
*/
class WC_Admin_REST_Reports_Stock_Controller extends WC_REST_Reports_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v3';
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'reports/stock';
/**
* Maps query arguments from the REST request.
*
* @param WP_REST_Request $request Request array.
* @return array
*/
protected function prepare_reports_query( $request ) {
$args = array();
$args['offset'] = $request['offset'];
$args['order'] = $request['order'];
$args['orderby'] = $request['orderby'];
$args['paged'] = $request['page'];
$args['post__in'] = $request['include'];
$args['post__not_in'] = $request['exclude'];
$args['posts_per_page'] = $request['per_page'];
$args['post_parent__in'] = $request['parent'];
$args['post_parent__not_in'] = $request['parent_exclude'];
if ( 'date' === $args['orderby'] ) {
$args['orderby'] = 'date ID';
} elseif ( 'stock_quantity' === $args['orderby'] ) {
$args['meta_key'] = '_stock'; // WPCS: slow query ok.
$args['orderby'] = 'meta_value_num';
} elseif ( 'include' === $args['orderby'] ) {
$args['orderby'] = 'post__in';
} elseif ( 'id' === $args['orderby'] ) {
$args['orderby'] = 'ID'; // ID must be capitalized.
} elseif ( 'slug' === $args['orderby'] ) {
$args['orderby'] = 'name';
}
$args['post_type'] = array( 'product', 'product_variation' );
if ( 'lowstock' === $request['type'] ) {
$low_stock = absint( max( get_option( 'woocommerce_notify_low_stock_amount' ), 1 ) );
$no_stock = absint( max( get_option( 'woocommerce_notify_no_stock_amount' ), 0 ) );
$args['meta_query'] = array( // WPCS: slow query ok.
array(
'key' => '_manage_stock',
'value' => 'yes',
),
array(
'key' => '_stock',
'value' => array( $no_stock, $low_stock ),
'compare' => 'BETWEEN',
'type' => 'NUMERIC',
),
array(
'key' => '_stock_status',
'value' => 'instock',
),
);
} elseif ( in_array( $request['type'], array_keys( wc_get_product_stock_status_options() ), true ) ) {
$args['meta_query'] = array( // WPCS: slow query ok.
array(
'key' => '_stock_status',
'value' => $request['type'],
),
);
}
$query_args['ignore_sticky_posts'] = true;
return $args;
}
/**
* Query products.
*
* @param array $query_args Query args.
* @return array
*/
protected function get_products( $query_args ) {
$query = new WP_Query();
$result = $query->query( $query_args );
$total_posts = $query->found_posts;
if ( $total_posts < 1 ) {
// Out-of-bounds, run the query again without LIMIT for total count.
unset( $query_args['paged'] );
$count_query = new WP_Query();
$count_query->query( $query_args );
$total_posts = $count_query->found_posts;
}
return array(
'objects' => array_map( 'wc_get_product', $result ),
'total' => (int) $total_posts,
'pages' => (int) ceil( $total_posts / (int) $query->query_vars['posts_per_page'] ),
);
}
/**
* 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 );
$query_results = $this->get_products( $query_args );
$objects = array();
foreach ( $query_results['objects'] as $object ) {
$data = $this->prepare_item_for_response( $object, $request );
$objects[] = $this->prepare_response_for_collection( $data );
}
$page = (int) $query_args['paged'];
$max_pages = $query_results['pages'];
$response = rest_ensure_response( $objects );
$response->header( 'X-WP-Total', $query_results['total'] );
$response->header( 'X-WP-TotalPages', (int) $max_pages );
$base = add_query_arg( $request->get_query_params(), rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ) );
if ( $page > 1 ) {
$prev_page = $page - 1;
if ( $prev_page > $max_pages ) {
$prev_page = $max_pages;
}
$prev_link = add_query_arg( 'page', $prev_page, $base );
$response->link_header( 'prev', $prev_link );
}
if ( $max_pages > $page ) {
$next_page = $page + 1;
$next_link = add_query_arg( 'page', $next_page, $base );
$response->link_header( 'next', $next_link );
}
return $response;
}
/**
* Prepare a report object for serialization.
*
* @param WC_Product $product Report data.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response
*/
public function prepare_item_for_response( $product, $request ) {
$data = array(
'id' => $product->get_id(),
'parent_id' => $product->get_parent_id(),
'name' => $product->get_name(),
'sku' => $product->get_sku(),
'stock_status' => $product->get_stock_status(),
'stock_quantity' => (float) $product->get_stock_quantity(),
);
$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 );
$response->add_links( $this->prepare_links( $product ) );
/**
* Filter a report returned from the API.
*
* Allows modification of the report data right before it is returned.
*
* @param WP_REST_Response $response The response object.
* @param WC_Product $product The original product object.
* @param WP_REST_Request $request Request used to generate the response.
*/
return apply_filters( 'woocommerce_rest_prepare_report_stock', $response, $product, $request );
}
/**
* Prepare links for the request.
*
* @param WC_Product $product Object data.
* @return array
*/
protected function prepare_links( $product ) {
if ( $product->is_type( 'variation' ) ) {
$links = array(
'product' => array(
'href' => rest_url( sprintf( '/%s/products/%d/variations/%d', $this->namespace, $product->get_parent_id(), $product->get_id() ) ),
),
'parent' => array(
'href' => rest_url( sprintf( '/%s/products/%d', $this->namespace, $product->get_parent_id() ) ),
),
);
} elseif ( $product->get_parent_id() ) {
$links = array(
'product' => array(
'href' => rest_url( sprintf( '/%s/products/%d', $this->namespace, $product->get_id() ) ),
),
'parent' => array(
'href' => rest_url( sprintf( '/%s/products/%d', $this->namespace, $product->get_parent_id() ) ),
),
);
} else {
$links = array(
'product' => array(
'href' => rest_url( sprintf( '/%s/products/%d', $this->namespace, $product->get_id() ) ),
),
);
}
return $links;
}
/**
* Get the Report's schema, conforming to JSON Schema.
*
* @return array
*/
public function get_item_schema() {
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'report_stock',
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'Unique identifier for the resource.', 'wc-admin' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'parent_id' => array(
'description' => __( 'Product parent ID.', 'wc-admin' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'name' => array(
'description' => __( 'Product name.', 'wc-admin' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'sku' => array(
'description' => __( 'Unique identifier.', 'wc-admin' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'stock_status' => array(
'description' => __( 'Stock status.', 'wc-admin' ),
'type' => 'string',
'enum' => array_keys( wc_get_product_stock_status_options() ),
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'stock_quantity' => array(
'description' => __( 'Stock quantity.', 'wc-admin' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
),
);
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['page'] = array(
'description' => __( 'Current page of the collection.', 'wc-admin' ),
'type' => 'integer',
'default' => 1,
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
'minimum' => 1,
);
$params['per_page'] = array(
'description' => __( 'Maximum number of items to be returned in result set.', 'wc-admin' ),
'type' => 'integer',
'default' => 10,
'minimum' => 1,
'maximum' => 100,
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
);
$params['exclude'] = array(
'description' => __( 'Ensure result set excludes specific IDs.', 'wc-admin' ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
'default' => array(),
'sanitize_callback' => 'wp_parse_id_list',
);
$params['include'] = array(
'description' => __( 'Limit result set to specific ids.', 'wc-admin' ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
'default' => array(),
'sanitize_callback' => 'wp_parse_id_list',
);
$params['offset'] = array(
'description' => __( 'Offset the result set by a specific number of items.', 'wc-admin' ),
'type' => 'integer',
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
);
$params['order'] = array(
'description' => __( 'Order sort attribute ascending or descending.', 'wc-admin' ),
'type' => 'string',
'default' => 'asc',
'enum' => array( 'asc', 'desc' ),
'validate_callback' => 'rest_validate_request_arg',
);
$params['orderby'] = array(
'description' => __( 'Sort collection by object attribute.', 'wc-admin' ),
'type' => 'string',
'default' => 'stock_quantity',
'enum' => array(
'stock_quantity',
'date',
'id',
'include',
'title',
'slug',
),
'validate_callback' => 'rest_validate_request_arg',
);
$params['parent'] = array(
'description' => __( 'Limit result set to those of particular parent IDs.', 'wc-admin' ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
'sanitize_callback' => 'wp_parse_id_list',
'default' => array(),
);
$params['parent_exclude'] = array(
'description' => __( 'Limit result set to all items except those of a particular parent ID.', 'wc-admin' ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
'sanitize_callback' => 'wp_parse_id_list',
'default' => array(),
);
$params['type'] = array(
'description' => __( 'Limit result set to items assigned a stock report type.', 'wc-admin' ),
'type' => 'string',
'default' => 'all',
'enum' => array_merge( array( 'all', 'lowstock' ), array_keys( wc_get_product_stock_status_options() ) ),
);
return $params;
}
}

View File

@ -31,6 +31,36 @@ class WC_Admin_REST_Reports_Taxes_Stats_Controller extends WC_REST_Reports_Contr
*/ */
protected $rest_base = 'reports/taxes/stats'; protected $rest_base = 'reports/taxes/stats';
/**
* Constructor.
*/
public function __construct() {
add_filter( 'woocommerce_reports_taxes_stats_select_query', array( $this, 'set_default_report_data' ) );
}
/**
* Set the default results to 0 if API returns an empty array
*
* @param Mixed $results Report data.
* @return object
*/
public function set_default_report_data( $results ) {
if ( empty( $results ) ) {
$results = new stdClass();
$results->total = 0;
$results->totals = new stdClass();
$results->totals->tax_codes = 0;
$results->totals->total_tax = 0;
$results->totals->order_tax = 0;
$results->totals->shipping_tax = 0;
$results->totals->orders = 0;
$results->intervals = array();
$results->pages = 1;
$results->page_no = 1;
}
return $results;
}
/** /**
* Maps query arguments from the REST request. * Maps query arguments from the REST request.
* *
@ -150,7 +180,7 @@ class WC_Admin_REST_Reports_Taxes_Stats_Controller extends WC_REST_Reports_Contr
'context' => array( 'view', 'edit' ), 'context' => array( 'view', 'edit' ),
'readonly' => true, 'readonly' => true,
), ),
'order_count' => array( 'orders_count' => array(
'description' => __( 'Amount of orders.', 'wc-admin' ), 'description' => __( 'Amount of orders.', 'wc-admin' ),
'type' => 'integer', 'type' => 'integer',
'context' => array( 'view', 'edit' ), 'context' => array( 'view', 'edit' ),

View File

@ -76,6 +76,7 @@ class WC_Admin_Api_Init {
*/ */
public function rest_api_init() { public function rest_api_init() {
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-admin-notes-controller.php'; require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-admin-notes-controller.php';
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-customers-controller.php';
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-products-controller.php'; require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-products-controller.php';
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-product-reviews-controller.php'; require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-product-reviews-controller.php';
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-reports-controller.php'; require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-reports-controller.php';
@ -94,9 +95,11 @@ class WC_Admin_Api_Init {
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-reports-revenue-stats-controller.php'; require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-reports-revenue-stats-controller.php';
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-reports-taxes-controller.php'; require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-reports-taxes-controller.php';
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-reports-taxes-stats-controller.php'; require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-reports-taxes-stats-controller.php';
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-reports-stock-controller.php';
$controllers = array( $controllers = array(
'WC_Admin_REST_Admin_Notes_Controller', 'WC_Admin_REST_Admin_Notes_Controller',
'WC_Admin_REST_Customers_Controller',
'WC_Admin_REST_Products_Controller', 'WC_Admin_REST_Products_Controller',
'WC_Admin_REST_Product_Reviews_Controller', 'WC_Admin_REST_Product_Reviews_Controller',
'WC_Admin_REST_Reports_Controller', 'WC_Admin_REST_Reports_Controller',
@ -111,6 +114,7 @@ class WC_Admin_Api_Init {
'WC_Admin_REST_Reports_Taxes_Stats_Controller', 'WC_Admin_REST_Reports_Taxes_Stats_Controller',
'WC_Admin_REST_Reports_Coupons_Controller', 'WC_Admin_REST_Reports_Coupons_Controller',
'WC_Admin_REST_Reports_Coupons_Stats_Controller', 'WC_Admin_REST_Reports_Coupons_Stats_Controller',
'WC_Admin_REST_Reports_Stock_Controller',
); );
foreach ( $controllers as $controller ) { foreach ( $controllers as $controller ) {
@ -165,6 +169,17 @@ class WC_Admin_Api_Init {
$endpoints['/wc/v3/products'][1] = $endpoints['/wc/v3/products'][3]; $endpoints['/wc/v3/products'][1] = $endpoints['/wc/v3/products'][3];
} }
// Override /wc/v3/customers.
if ( isset( $endpoints['/wc/v3/customers'] )
&& isset( $endpoints['/wc/v3/customers'][3] )
&& isset( $endpoints['/wc/v3/customers'][2] )
&& $endpoints['/wc/v3/customers'][2]['callback'][0] instanceof WC_Admin_REST_Customers_Controller
&& $endpoints['/wc/v3/customers'][3]['callback'][0] instanceof WC_Admin_REST_Customers_Controller
) {
$endpoints['/wc/v3/customers'][0] = $endpoints['/wc/v3/customers'][2];
$endpoints['/wc/v3/customers'][1] = $endpoints['/wc/v3/customers'][3];
}
// Override /wc/v3/products/$id. // Override /wc/v3/products/$id.
if ( isset( $endpoints['/wc/v3/products/(?P<id>[\d]+)'] ) if ( isset( $endpoints['/wc/v3/products/(?P<id>[\d]+)'] )
&& isset( $endpoints['/wc/v3/products/(?P<id>[\d]+)'][5] ) && isset( $endpoints['/wc/v3/products/(?P<id>[\d]+)'][5] )

View File

@ -30,7 +30,7 @@ class WC_Admin_Reports_Taxes_Stats_Data_Store extends WC_Admin_Reports_Data_Stor
'total_tax' => 'floatval', 'total_tax' => 'floatval',
'order_tax' => 'floatval', 'order_tax' => 'floatval',
'shipping_tax' => 'floatval', 'shipping_tax' => 'floatval',
'order_count' => 'intval', 'orders_count' => 'intval',
); );
/** /**
@ -43,7 +43,7 @@ class WC_Admin_Reports_Taxes_Stats_Data_Store extends WC_Admin_Reports_Data_Stor
'total_tax' => 'SUM(total_tax) AS total_tax', 'total_tax' => 'SUM(total_tax) AS total_tax',
'order_tax' => 'SUM(order_tax) as order_tax', 'order_tax' => 'SUM(order_tax) as order_tax',
'shipping_tax' => 'SUM(shipping_tax) as shipping_tax', 'shipping_tax' => 'SUM(shipping_tax) as shipping_tax',
'order_count' => 'COUNT(DISTINCT order_id) as orders', 'orders_count' => 'COUNT(DISTINCT order_id) as orders_count',
); );
/** /**
@ -198,6 +198,10 @@ class WC_Admin_Reports_Taxes_Stats_Data_Store extends WC_Admin_Reports_Data_Stor
} }
$totals = (object) $this->cast_numbers( $totals[0] ); $totals = (object) $this->cast_numbers( $totals[0] );
$this->update_interval_boundary_dates( $query_args['after'], $query_args['before'], $query_args['interval'], $intervals );
$this->create_interval_subtotals( $intervals );
$data = (object) array( $data = (object) array(
'totals' => $totals, 'totals' => $totals,
'intervals' => $intervals, 'intervals' => $intervals,

View File

@ -133,6 +133,22 @@ function wc_admin_register_pages() {
) )
); );
wc_admin_register_page(
array(
'title' => __( 'Downloads', 'wc-admin' ),
'parent' => '/analytics/revenue',
'path' => '/analytics/downloads',
)
);
wc_admin_register_page(
array(
'title' => __( 'Stock', 'wc-admin' ),
'parent' => '/analytics/revenue',
'path' => '/analytics/stock',
)
);
wc_admin_register_page( wc_admin_register_page(
array( array(
'title' => __( 'Customers', 'wc-admin' ), 'title' => __( 'Customers', 'wc-admin' ),
@ -338,5 +354,80 @@ function woocommerce_embed_page_header() {
</div> </div>
<?php <?php
} }
add_action( 'in_admin_header', 'woocommerce_embed_page_header' ); add_action( 'in_admin_header', 'woocommerce_embed_page_header' );
/**
* Registers WooCommerce specific user data to the WordPress user API.
*/
function wc_admin_register_user_data() {
register_rest_field(
'user',
'woocommerce_meta',
array(
'get_callback' => 'wc_admin_get_user_data_values',
'update_callback' => 'wc_admin_update_user_data_values',
'schema' => null,
)
);
}
add_action( 'rest_api_init', 'wc_admin_register_user_data' );
/**
* We store some WooCommerce specific user meta attached to users endpoint,
* so that we can track certain preferences or values such as the inbox activity panel last open time.
* Additional fields can be added in the function below, and then used via wc-admin's currentUser data.
*
* @return array Fields to expose over the WP user endpoint.
*/
function wc_admin_get_user_data_fields() {
$user_data_fields = array(
'categories_report_columns',
'coupons_report_columns',
'customers_report_columns',
'orders_report_columns',
'products_report_columns',
'revenue_report_columns',
'taxes_report_columns',
'variations_report_columns',
);
return apply_filters( 'wc_admin_get_user_data_fields', $user_data_fields );
}
/**
* For all the registered user data fields ( wc_admin_get_user_data_fields ), fetch the data
* for returning via the REST API.
*
* @param WP_User $user Current user.
*/
function wc_admin_get_user_data_values( $user ) {
$values = array();
foreach ( wc_admin_get_user_data_fields() as $field ) {
$values[ $field ] = get_user_meta( $user['id'], 'wc_admin_' . $field, true );
}
return $values;
}
/**
* For all the registered user data fields ( wc_admin_get_user_data_fields ), update the data
* for the REST API.
*
* @param array $values The new values for the meta.
* @param WP_User $user The current user.
* @param string $field_id The field id for the user meta.
*/
function wc_admin_update_user_data_values( $values, $user, $field_id ) {
if ( empty( $values ) || ! is_array( $values ) || 'woocommerce_meta' !== $field_id ) {
return;
}
$fields = wc_admin_get_user_data_fields();
$updates = array();
foreach ( $values as $field => $value ) {
if ( in_array( $field, $fields, true ) ) {
$updates[ $field ] = $value;
update_user_meta( $user->ID, 'wc_admin_' . $field, $value );
}
}
return $updates;
}

View File

@ -142,6 +142,22 @@ function wc_admin_print_script_settings() {
$tracking_script .= "window._tkq = window._tkq || [];\n"; $tracking_script .= "window._tkq = window._tkq || [];\n";
$tracking_script .= "document.head.appendChild( wc_tracking_script );\n"; $tracking_script .= "document.head.appendChild( wc_tracking_script );\n";
} }
$preload_data_endpoints = array(
'countries' => '/wc/v3/data/countries',
);
if ( function_exists( 'gutenberg_preload_api_request' ) ) {
$preload_function = 'gutenberg_preload_api_request';
} else {
$preload_function = 'rest_preload_api_request';
}
$preload_data = array_reduce(
array_values( $preload_data_endpoints ),
$preload_function
);
/** /**
* TODO: On merge, once plugin images are added to core WooCommerce, `wcAdminAssetUrl` can be retired, and * TODO: On merge, once plugin images are added to core WooCommerce, `wcAdminAssetUrl` can be retired, and
* `wcAssetUrl` can be used in its place throughout the codebase. * `wcAssetUrl` can be used in its place throughout the codebase.
@ -163,6 +179,10 @@ function wc_admin_print_script_settings() {
'siteTitle' => get_bloginfo( 'name' ), 'siteTitle' => get_bloginfo( 'name' ),
'trackingEnabled' => $tracking_enabled, 'trackingEnabled' => $tracking_enabled,
); );
foreach ( $preload_data_endpoints as $key => $endpoint ) {
$settings['dataEndpoints'][ $key ] = $preload_data[ $endpoint ]['body'];
}
?> ?>
<script type="text/javascript"> <script type="text/javascript">
<?php <?php

View File

@ -272,6 +272,7 @@ function wc_admin_currency_settings() {
'code' => $code, 'code' => $code,
'precision' => wc_get_price_decimals(), 'precision' => wc_get_price_decimals(),
'symbol' => get_woocommerce_currency_symbol( $code ), 'symbol' => get_woocommerce_currency_symbol( $code ),
'position' => get_option( 'woocommerce_currency_pos' ),
) )
); );
} }

View File

@ -1289,7 +1289,7 @@
}, },
"globby": { "globby": {
"version": "8.0.1", "version": "8.0.1",
"resolved": "http://registry.npmjs.org/globby/-/globby-8.0.1.tgz", "resolved": "https://registry.npmjs.org/globby/-/globby-8.0.1.tgz",
"integrity": "sha512-oMrYrJERnKBLXNLVTqhm3vPEdJ/b2ZE28xN4YARiix1NOIOBPEpOUnm844K1iu/BkphCaf2WNFwMszv8Soi1pw==", "integrity": "sha512-oMrYrJERnKBLXNLVTqhm3vPEdJ/b2ZE28xN4YARiix1NOIOBPEpOUnm844K1iu/BkphCaf2WNFwMszv8Soi1pw==",
"dev": true, "dev": true,
"requires": { "requires": {
@ -1740,7 +1740,7 @@
}, },
"globby": { "globby": {
"version": "8.0.1", "version": "8.0.1",
"resolved": "http://registry.npmjs.org/globby/-/globby-8.0.1.tgz", "resolved": "https://registry.npmjs.org/globby/-/globby-8.0.1.tgz",
"integrity": "sha512-oMrYrJERnKBLXNLVTqhm3vPEdJ/b2ZE28xN4YARiix1NOIOBPEpOUnm844K1iu/BkphCaf2WNFwMszv8Soi1pw==", "integrity": "sha512-oMrYrJERnKBLXNLVTqhm3vPEdJ/b2ZE28xN4YARiix1NOIOBPEpOUnm844K1iu/BkphCaf2WNFwMszv8Soi1pw==",
"dev": true, "dev": true,
"requires": { "requires": {
@ -3546,11 +3546,6 @@
"commander": "^2.11.0" "commander": "^2.11.0"
} }
}, },
"arity-n": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/arity-n/-/arity-n-1.0.4.tgz",
"integrity": "sha1-2edrEXM+CFacCEeuezmyhgswt0U="
},
"arr-diff": { "arr-diff": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz", "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz",
@ -3686,7 +3681,7 @@
}, },
"util": { "util": {
"version": "0.10.3", "version": "0.10.3",
"resolved": "http://registry.npmjs.org/util/-/util-0.10.3.tgz", "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz",
"integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=",
"dev": true, "dev": true,
"requires": { "requires": {
@ -4759,11 +4754,6 @@
"parse5": "^3.0.1" "parse5": "^3.0.1"
} }
}, },
"chickencurry": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/chickencurry/-/chickencurry-1.1.1.tgz",
"integrity": "sha1-AmVfKyazvC7hrh5TFoht4463lzg="
},
"chokidar": { "chokidar": {
"version": "2.0.4", "version": "2.0.4",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.0.4.tgz", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.0.4.tgz",
@ -5198,14 +5188,6 @@
"resolved": "https://registry.npmjs.org/component-xor/-/component-xor-0.0.4.tgz", "resolved": "https://registry.npmjs.org/component-xor/-/component-xor-0.0.4.tgz",
"integrity": "sha1-xV2DzMG5TNUImk6T+niRxyY+Wao=" "integrity": "sha1-xV2DzMG5TNUImk6T+niRxyY+Wao="
}, },
"compose-function": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/compose-function/-/compose-function-2.0.0.tgz",
"integrity": "sha1-5kL6fh2iFSlyADFHZ3b8JGkawLA=",
"requires": {
"arity-n": "^1.0.4"
}
},
"concat-map": { "concat-map": {
"version": "0.0.1", "version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@ -6313,7 +6295,7 @@
}, },
"regexpu-core": { "regexpu-core": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "http://registry.npmjs.org/regexpu-core/-/regexpu-core-1.0.0.tgz", "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-1.0.0.tgz",
"integrity": "sha1-hqdj9Y7k18L2sQLkdkBQ3n7ZDGs=", "integrity": "sha1-hqdj9Y7k18L2sQLkdkBQ3n7ZDGs=",
"dev": true, "dev": true,
"requires": { "requires": {
@ -6951,8 +6933,9 @@
} }
}, },
"docsify-cli": { "docsify-cli": {
"version": "github:docsifyjs/docsify-cli#5df389962a0eb69ac02701b297b77b2fbb85ec28", "version": "4.3.0",
"from": "github:docsifyjs/docsify-cli#5df38996", "resolved": "https://registry.npmjs.org/docsify-cli/-/docsify-cli-4.3.0.tgz",
"integrity": "sha512-88O1sMeoZv4lb5GPSJzDtOAv2KzBjpQaSqVlVqY+6hGJfb2wpz9PvlUhvlgPq54zu4kPDeCCyUYgqa/llhKg3w==",
"dev": true, "dev": true,
"requires": { "requires": {
"chalk": "^1.1.3", "chalk": "^1.1.3",
@ -7831,7 +7814,7 @@
}, },
"espree": { "espree": {
"version": "3.5.4", "version": "3.5.4",
"resolved": "https://registry.npmjs.org/espree/-/espree-3.5.4.tgz", "resolved": "http://registry.npmjs.org/espree/-/espree-3.5.4.tgz",
"integrity": "sha512-yAcIQxtmMiB/jL32dzEp2enBeidsB7xWPLNiw3IIkpVds1P+h7qF9YwJq1yUNzp2OKXgAprs4F61ih66UsoD1A==", "integrity": "sha512-yAcIQxtmMiB/jL32dzEp2enBeidsB7xWPLNiw3IIkpVds1P+h7qF9YwJq1yUNzp2OKXgAprs4F61ih66UsoD1A==",
"requires": { "requires": {
"acorn": "^5.5.0", "acorn": "^5.5.0",
@ -7883,7 +7866,7 @@
}, },
"events": { "events": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "http://registry.npmjs.org/events/-/events-1.1.1.tgz", "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz",
"integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=", "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=",
"dev": true "dev": true
}, },
@ -10458,15 +10441,15 @@
"dev": true "dev": true
}, },
"html-to-react": { "html-to-react": {
"version": "1.3.3", "version": "1.3.4",
"resolved": "https://registry.npmjs.org/html-to-react/-/html-to-react-1.3.3.tgz", "resolved": "https://registry.npmjs.org/html-to-react/-/html-to-react-1.3.4.tgz",
"integrity": "sha512-4Qi5/t8oBr6c1t1kBJKyxEeJu0lb7ctvq29oFZioiUHH0Wz88VWGwoXuH26HDt9v64bDHA4NMPNTH8bVrcaJWA==", "integrity": "sha512-/tWDdb/8Koi/QEP5YUY1653PcDpBnnMblXRhotnTuhFDjI1Fc6Wzox5d4sw73Xk5rM2OdM5np4AYjT/US/Wj7Q==",
"requires": { "requires": {
"domhandler": "^2.3.0", "domhandler": "^2.4.2",
"escape-string-regexp": "^1.0.5", "escape-string-regexp": "^1.0.5",
"htmlparser2": "^3.8.3", "htmlparser2": "^3.10.0",
"ramda": "^0.25.0", "lodash.camelcase": "^4.3.0",
"underscore.string.fp": "^1.0.4" "ramda": "^0.26"
} }
}, },
"htmlparser2": { "htmlparser2": {
@ -13079,6 +13062,11 @@
"integrity": "sha1-DZnzzNem0mHRm9rrkkUAXShYCOc=", "integrity": "sha1-DZnzzNem0mHRm9rrkkUAXShYCOc=",
"dev": true "dev": true
}, },
"lodash.camelcase": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
"integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY="
},
"lodash.clonedeep": { "lodash.clonedeep": {
"version": "4.5.0", "version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
@ -14082,7 +14070,7 @@
}, },
"camelcase-keys": { "camelcase-keys": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "http://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz",
"integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=", "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=",
"dev": true, "dev": true,
"requires": { "requires": {
@ -14130,7 +14118,7 @@
}, },
"meow": { "meow": {
"version": "3.7.0", "version": "3.7.0",
"resolved": "http://registry.npmjs.org/meow/-/meow-3.7.0.tgz", "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz",
"integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=", "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=",
"dev": true, "dev": true,
"requires": { "requires": {
@ -15977,11 +15965,24 @@
} }
}, },
"prismjs": { "prismjs": {
"version": "1.6.0", "version": "1.15.0",
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.6.0.tgz", "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.15.0.tgz",
"integrity": "sha1-EY2V+3pm26InLjQ7NF9SNmWds2U=", "integrity": "sha512-Lf2JrFYx8FanHrjoV5oL8YHCclLQgbJcVZR+gikGGMqz6ub5QVWDTM6YIwm3BuPxM/LOV+rKns3LssXNLIf+DA==",
"requires": { "requires": {
"clipboard": "^1.5.5" "clipboard": "^2.0.0"
},
"dependencies": {
"clipboard": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/clipboard/-/clipboard-2.0.4.tgz",
"integrity": "sha512-Vw26VSLRpJfBofiVaFb/I8PVfdI1OxKcYShe6fm0sP/DtmiWQNCjhM/okTvdCo0G+lMMm1rMYbk4IK4x1X+kgQ==",
"optional": true,
"requires": {
"good-listener": "^1.2.2",
"select": "^1.1.2",
"tiny-emitter": "^2.0.0"
}
}
} }
}, },
"private": { "private": {
@ -16179,9 +16180,9 @@
"integrity": "sha1-635iZ1SN3t+4mcG5Dlc3RVnN234=" "integrity": "sha1-635iZ1SN3t+4mcG5Dlc3RVnN234="
}, },
"ramda": { "ramda": {
"version": "0.25.0", "version": "0.26.1",
"resolved": "https://registry.npmjs.org/ramda/-/ramda-0.25.0.tgz", "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.26.1.tgz",
"integrity": "sha512-GXpfrYVPwx3K7RQ6aYT8KPS8XViSXUVJT1ONhoKPE9VAleW42YE+U+8VEyGWt41EnEQW7gwecYJriTI0pKoecQ==" "integrity": "sha512-hLWjpy7EnsDBb0p+Z3B7rPi3GDeRG5ZtiI33kJhTt+ORCd38AbAIjB/9zRIUoeTbE/AVX5ZkU7m6bznsvrf8eQ=="
}, },
"randexp": { "randexp": {
"version": "0.4.6", "version": "0.4.6",
@ -16240,10 +16241,33 @@
"dev": true "dev": true
}, },
"raw-loader": { "raw-loader": {
"version": "0.5.1", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/raw-loader/-/raw-loader-0.5.1.tgz", "resolved": "https://registry.npmjs.org/raw-loader/-/raw-loader-1.0.0.tgz",
"integrity": "sha1-DD0L6u2KAclm2Xh793goElKpeao=", "integrity": "sha512-Uqy5AqELpytJTRxYT4fhltcKPj0TyaEpzJDcGz7DFJi+pQOOi3GjR/DOdxTkTsF+NzhnldIoG6TORaBlInUuqA==",
"dev": true "dev": true,
"requires": {
"loader-utils": "^1.1.0",
"schema-utils": "^1.0.0"
},
"dependencies": {
"ajv-keywords": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.2.0.tgz",
"integrity": "sha1-6GuBnGAs+IIa1jdBNpjx3sAhhHo=",
"dev": true
},
"schema-utils": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz",
"integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==",
"dev": true,
"requires": {
"ajv": "^6.1.0",
"ajv-errors": "^1.0.0",
"ajv-keywords": "^3.1.0"
}
}
}
}, },
"rc": { "rc": {
"version": "1.2.8", "version": "1.2.8",
@ -16441,6 +16465,16 @@
"prismjs": "1.6", "prismjs": "1.6",
"prop-types": "^15.5.8", "prop-types": "^15.5.8",
"unescape": "^0.2.0" "unescape": "^0.2.0"
},
"dependencies": {
"prismjs": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.6.0.tgz",
"integrity": "sha1-EY2V+3pm26InLjQ7NF9SNmWds2U=",
"requires": {
"clipboard": "^1.5.5"
}
}
} }
}, },
"react-moment-proptypes": { "react-moment-proptypes": {
@ -17501,11 +17535,6 @@
"integrity": "sha1-52OI0heZLCUnUCQdPTlW/tmNj/Q=", "integrity": "sha1-52OI0heZLCUnUCQdPTlW/tmNj/Q=",
"dev": true "dev": true
}, },
"reverse-arguments": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/reverse-arguments/-/reverse-arguments-1.0.0.tgz",
"integrity": "sha1-woCVo6khrHFdYYNN3s6QJ5kmZ80="
},
"rgb": { "rgb": {
"version": "0.1.0", "version": "0.1.0",
"resolved": "https://registry.npmjs.org/rgb/-/rgb-0.1.0.tgz", "resolved": "https://registry.npmjs.org/rgb/-/rgb-0.1.0.tgz",
@ -18077,7 +18106,7 @@
"dependencies": { "dependencies": {
"source-map": { "source-map": {
"version": "0.4.4", "version": "0.4.4",
"resolved": "http://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz",
"integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=",
"dev": true, "dev": true,
"requires": { "requires": {
@ -18643,7 +18672,7 @@
}, },
"stream-browserify": { "stream-browserify": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "http://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.1.tgz", "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.1.tgz",
"integrity": "sha1-ZiZu5fm9uZQKTkUUyvtDu3Hlyds=", "integrity": "sha1-ZiZu5fm9uZQKTkUUyvtDu3Hlyds=",
"dev": true, "dev": true,
"requires": { "requires": {
@ -20141,22 +20170,6 @@
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.4.4.tgz", "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.4.4.tgz",
"integrity": "sha1-YaajIBBiKvoHljvzJSA88SI51gQ=" "integrity": "sha1-YaajIBBiKvoHljvzJSA88SI51gQ="
}, },
"underscore.string": {
"version": "3.0.3",
"resolved": "http://registry.npmjs.org/underscore.string/-/underscore.string-3.0.3.tgz",
"integrity": "sha1-Rhe4waJQz25QZPu7Nj0PqWzxRVI="
},
"underscore.string.fp": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/underscore.string.fp/-/underscore.string.fp-1.0.4.tgz",
"integrity": "sha1-BUs/GEO8rlYShsh95eiHm0/Jg2Q=",
"requires": {
"chickencurry": "1.1.1",
"compose-function": "^2.0.0",
"reverse-arguments": "1.0.0",
"underscore.string": "3.0.3"
}
},
"unescape": { "unescape": {
"version": "0.2.0", "version": "0.2.0",
"resolved": "https://registry.npmjs.org/unescape/-/unescape-0.2.0.tgz", "resolved": "https://registry.npmjs.org/unescape/-/unescape-0.2.0.tgz",
@ -21437,7 +21450,7 @@
}, },
"yargs": { "yargs": {
"version": "11.1.0", "version": "11.1.0",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-11.1.0.tgz", "resolved": "http://registry.npmjs.org/yargs/-/yargs-11.1.0.tgz",
"integrity": "sha512-NwW69J42EsCSanF8kyn5upxvjp5ds+t3+udGBeTbFnERA+lF541DDpMawzo4z6W/QrzNM18D+BPMiOBibnFV5A==", "integrity": "sha512-NwW69J42EsCSanF8kyn5upxvjp5ds+t3+udGBeTbFnERA+lF541DDpMawzo4z6W/QrzNM18D+BPMiOBibnFV5A==",
"requires": { "requires": {
"cliui": "^4.0.0", "cliui": "^4.0.0",

View File

@ -70,7 +70,7 @@
"css-loader": "2.0.0", "css-loader": "2.0.0",
"deasync": "0.1.14", "deasync": "0.1.14",
"deep-freeze": "0.0.1", "deep-freeze": "0.0.1",
"docsify-cli": "github:docsifyjs/docsify-cli#5df38996", "docsify-cli": "4.3.0",
"eslint": "5.10.0", "eslint": "5.10.0",
"eslint-config-wpcalypso": "4.0.1", "eslint-config-wpcalypso": "4.0.1",
"eslint-loader": "2.1.1", "eslint-loader": "2.1.1",
@ -89,7 +89,7 @@
"postcss-loader": "3.0.0", "postcss-loader": "3.0.0",
"prettier": "github:automattic/calypso-prettier#c56b4251", "prettier": "github:automattic/calypso-prettier#c56b4251",
"prop-types": "15.6.2", "prop-types": "15.6.2",
"raw-loader": "0.5.1", "raw-loader": "1.0.0",
"react-docgen": "2.21.0", "react-docgen": "2.21.0",
"readline-sync": "1.4.9", "readline-sync": "1.4.9",
"recast": "0.16.1", "recast": "0.16.1",
@ -130,10 +130,11 @@
"gfm-code-blocks": "1.0.0", "gfm-code-blocks": "1.0.0",
"gridicons": "3.1.1", "gridicons": "3.1.1",
"history": "4.7.2", "history": "4.7.2",
"html-to-react": "1.3.3", "html-to-react": "1.3.4",
"interpolate-components": "1.1.1", "interpolate-components": "1.1.1",
"lodash": "^4.17.11", "lodash": "^4.17.11",
"marked": "0.5.2", "marked": "0.5.2",
"prismjs": "^1.15.0",
"qs": "^6.5.2", "qs": "^6.5.2",
"react-click-outside": "3.0.1", "react-click-outside": "3.0.1",
"react-dates": "^18.0.4", "react-dates": "^18.0.4",

View File

@ -2,6 +2,14 @@
- Update `<Table />` to use header keys to denote which columns are shown - Update `<Table />` to use header keys to denote which columns are shown
- Add `onColumnsChange` property to `<Table />` which is called when columns are shown/hidden - Add `onColumnsChange` property to `<Table />` which is called when columns are shown/hidden
- Add country autocompleter to search component
- Add customer email autocompleter to search component
- Adding new `<Chart />` component.
- Added new `showDatePicker` prop to `<Filters />` component which allows to use the filters component without the date picker.
- Added new taxes and customers autocompleters, and added support for using them within `<Filters />`.
- Bug fix for `<SummaryNumber />` returning N/A instead of zero.
- Bug fix for screen reader label IDs in `<Table />` header.
- Added new component `<TextControlWithAffixes />`.
# 1.2.0 # 1.2.0

View File

@ -27,6 +27,7 @@ import Card from '../../card';
import Link from '../../link'; import Link from '../../link';
import SelectFilter from './select-filter'; import SelectFilter from './select-filter';
import SearchFilter from './search-filter'; import SearchFilter from './search-filter';
import NumberFilter from './number-filter';
const matches = [ const matches = [
{ value: 'all', label: __( 'All', 'wc-admin' ) }, { value: 'all', label: __( 'All', 'wc-admin' ) },
@ -191,6 +192,15 @@ class AdvancedFilters extends Component {
query={ query } query={ query }
/> />
) } ) }
{ 'Number' === input.component && (
<NumberFilter
filter={ filter }
config={ config.filters[ key ] }
onFilterChange={ this.onFilterChange }
isEnglish={ isEnglish }
query={ query }
/>
) }
<IconButton <IconButton
className="woocommerce-filters-advanced__remove" className="woocommerce-filters-advanced__remove"
label={ labels.remove } label={ labels.remove }

View File

@ -0,0 +1,187 @@
/** @format */
/**
* External dependencies
*/
import { Component, Fragment } from '@wordpress/element';
import { SelectControl, TextControl } from '@wordpress/components';
import { get, find, partial } from 'lodash';
import interpolateComponents from 'interpolate-components';
import classnames from 'classnames';
import { _x } from '@wordpress/i18n';
/**
* Internal dependencies
*/
import TextControlWithAffixes from '../../text-control-with-affixes';
import { formatCurrency } from '@woocommerce/currency';
class NumberFilter extends Component {
getBetweenString() {
return _x(
'{{rangeStart /}}{{span}}and{{/span}}{{rangeEnd /}}',
'Numerical range inputs arranged on a single line',
'wc-admin'
);
}
getLegend( filter, config ) {
const inputType = get( config, [ 'input', 'type' ], 'number' );
const rule = find( config.rules, { value: filter.rule } ) || {};
let [ rangeStart, rangeEnd ] = ( filter.value || '' ).split( ',' );
if ( 'currency' === inputType ) {
rangeStart = formatCurrency( rangeStart );
rangeEnd = formatCurrency( rangeEnd );
}
let filterStr = rangeStart;
if ( 'between' === rule.value ) {
filterStr = interpolateComponents( {
mixedString: this.getBetweenString(),
components: {
rangeStart: <Fragment>{ rangeStart }</Fragment>,
rangeEnd: <Fragment>{ rangeEnd }</Fragment>,
span: <Fragment />,
},
} );
}
return interpolateComponents( {
mixedString: config.labels.title,
components: {
filter: <span>{ filterStr }</span>,
rule: <span>{ rule.label }</span>,
},
} );
}
getFormControl( type, value, onChange ) {
if ( 'currency' === type ) {
const currencySymbol = get( wcSettings, [ 'currency', 'symbol' ] );
const symbolPosition = get( wcSettings, [ 'currency', 'position' ] );
return (
0 === symbolPosition.indexOf( 'right' )
? <TextControlWithAffixes
suffix={ <span dangerouslySetInnerHTML={ { __html: currencySymbol } } /> }
className="woocommerce-filters-advanced__input"
type="number"
value={ value }
onChange={ onChange }
/>
: <TextControlWithAffixes
prefix={ <span dangerouslySetInnerHTML={ { __html: currencySymbol } } /> }
className="woocommerce-filters-advanced__input"
type="number"
value={ value }
onChange={ onChange }
/>
);
}
return (
<TextControl
className="woocommerce-filters-advanced__input"
type="number"
value={ value }
onChange={ onChange }
/>
);
}
getFilterInputs() {
const { config, filter, onFilterChange } = this.props;
const inputType = get( config, [ 'input', 'type' ], 'number' );
if ( 'between' === filter.rule ) {
return this.getRangeInput();
}
const [ rangeStart, rangeEnd ] = ( filter.value || '' ).split( ',' );
if ( Boolean( rangeEnd ) ) {
// If there's a value for rangeEnd, we've just changed from "between"
// to "less than" or "more than" and need to transition the value
onFilterChange( filter.key, 'value', rangeStart || rangeEnd );
}
return this.getFormControl(
inputType,
rangeStart || rangeEnd,
partial( onFilterChange, filter.key, 'value' )
);
}
getRangeInput() {
const { config, filter, onFilterChange } = this.props;
const inputType = get( config, [ 'input', 'type' ], 'number' );
const [ rangeStart, rangeEnd ] = ( filter.value || '' ).split( ',' );
const rangeStartOnChange = ( newRangeStart ) => {
const newValue = [ newRangeStart, rangeEnd ].join( ',' );
onFilterChange( filter.key, 'value', newValue );
};
const rangeEndOnChange = ( newRangeEnd ) => {
const newValue = [ rangeStart, newRangeEnd ].join( ',' );
onFilterChange( filter.key, 'value', newValue );
};
return interpolateComponents( {
mixedString: this.getBetweenString(),
components: {
rangeStart: this.getFormControl( inputType, rangeStart, rangeStartOnChange ),
rangeEnd: this.getFormControl( inputType, rangeEnd, rangeEndOnChange ),
span: <span className="separator" />,
},
} );
}
render() {
const { config, filter, onFilterChange, isEnglish } = this.props;
const { key, rule } = filter;
const { labels, rules } = config;
const children = interpolateComponents( {
mixedString: labels.title,
components: {
rule: (
<SelectControl
className="woocommerce-filters-advanced__rule"
options={ rules }
value={ rule }
onChange={ partial( onFilterChange, key, 'rule' ) }
aria-label={ labels.rule }
/>
),
filter: (
<div
className={ classnames( 'woocommerce-filters-advanced__input-numeric-range', {
'is-between': 'between' === rule,
} ) }
>
{ this.getFilterInputs() }
</div>
),
},
} );
/*eslint-disable jsx-a11y/no-noninteractive-tabindex*/
return (
<fieldset tabIndex="0">
<legend className="screen-reader-text">
{ this.getLegend( filter, config ) }
</legend>
<div
className={ classnames( 'woocommerce-filters-advanced__fieldset', {
'is-english': isEnglish,
} ) }
>
{ children }
</div>
</fieldset>
);
/*eslint-enable jsx-a11y/no-noninteractive-tabindex*/
}
}
export default NumberFilter;

View File

@ -171,3 +171,26 @@
} }
} }
} }
.woocommerce-filters-advanced__input-numeric-range {
align-items: center;
display: grid;
grid-template-columns: 1fr;
&.is-between {
grid-template-columns: 1fr 36px 1fr;
}
input {
height: 38px;
margin: 0;
}
.separator {
padding: 0 8px;
@include breakpoint( '<782px' ) {
padding: 0;
}
}
}

View File

@ -88,6 +88,57 @@ const advancedFilters = {
defaultOption: 'new', defaultOption: 'new',
}, },
}, },
quantity: {
labels: {
add: 'Item Quantity',
remove: 'Remove item quantity filter',
rule: 'Select an item quantity filter match',
title: 'Item Quantity is {{rule /}} {{filter /}}',
},
rules: [
{
value: 'lessthan',
label: 'Less Than',
},
{
value: 'morethan',
label: 'More Than',
},
{
value: 'between',
label: 'Between',
},
],
input: {
component: 'Number',
},
},
subtotal: {
labels: {
add: 'Subtotal',
remove: 'Remove subtotal filter',
rule: 'Select a subtotal filter match',
title: 'Subtotal is {{rule /}} {{filter /}}',
},
rules: [
{
value: 'lessthan',
label: 'Less Than',
},
{
value: 'morethan',
label: 'More Than',
},
{
value: 'between',
label: 'Between',
},
],
input: {
component: 'Number',
type: 'currency',
},
},
}, },
}; };

View File

@ -45,5 +45,6 @@ export { default as EmptyTable } from './table/empty';
export { default as TablePlaceholder } from './table/placeholder'; export { default as TablePlaceholder } from './table/placeholder';
export { default as TableSummary } from './table/summary'; export { default as TableSummary } from './table/summary';
export { default as Tag } from './tag'; export { default as Tag } from './tag';
export { default as TextControlWithAffixes } from './text-control-with-affixes';
export { default as useFilters } from './higher-order/use-filters'; export { default as useFilters } from './higher-order/use-filters';
export { default as ViewMoreList } from './view-more-list'; export { default as ViewMoreList } from './view-more-list';

View File

@ -0,0 +1,58 @@
/** @format */
/**
* External dependencies
*/
import { decodeEntities } from '@wordpress/html-entities';
/**
* Internal dependencies
*/
import { computeSuggestionMatch } from './utils';
import Flag from '../../flag';
/**
* A country completer.
* See https://github.com/WordPress/gutenberg/tree/master/packages/components/src/autocomplete#the-completer-interface
*
* @type {Completer}
*/
export default {
name: 'countries',
className: 'woocommerce-search__country-result',
options() {
return wcSettings.dataEndpoints.countries || [];
},
getOptionKeywords( country ) {
return [ decodeEntities( country.name ) ];
},
getOptionLabel( country, query ) {
const name = decodeEntities( country.name );
const match = computeSuggestionMatch( name, query ) || {};
return [
<Flag
key="thumbnail"
className="woocommerce-search__result-thumbnail"
code={ country.code }
width={ 18 }
height={ 18 }
/>,
<span key="name" className="woocommerce-search__result-name" aria-label={ name }>
{ match.suggestionBeforeMatch }
<strong className="components-form-token-field__suggestion-match">
{ match.suggestionMatch }
</strong>
{ match.suggestionAfterMatch }
</span>,
];
},
// This is slightly different than gutenberg/Autocomplete, we don't support different methods
// of replace/insertion, so we can just return the value.
getOptionCompletion( country ) {
const value = {
id: country.code,
label: decodeEntities( country.name ),
};
return value;
},
};

View File

@ -0,0 +1,61 @@
/** @format */
/**
* External dependencies
*/
import apiFetch from '@wordpress/api-fetch';
/**
* WooCommerce dependencies
*/
import { stringifyQuery } from '@woocommerce/navigation';
/**
* Internal dependencies
*/
import { computeSuggestionMatch } from './utils';
/**
* A customer email completer.
* See https://github.com/WordPress/gutenberg/tree/master/packages/components/src/autocomplete#the-completer-interface
*
* @type {Completer}
*/
export default {
name: 'emails',
className: 'woocommerce-search__emails-result',
options( search ) {
let payload = '';
if ( search ) {
const query = {
email: search,
per_page: 10,
};
payload = stringifyQuery( query );
}
return apiFetch( { path: `/wc/v3/customers${ payload }` } );
},
isDebounced: true,
getOptionKeywords( customer ) {
return [ customer.email ];
},
getOptionLabel( customer, query ) {
const match = computeSuggestionMatch( customer.email, query ) || {};
return [
<span key="name" className="woocommerce-search__result-name" aria-label={ customer.email }>
{ match.suggestionBeforeMatch }
<strong className="components-form-token-field__suggestion-match">
{ match.suggestionMatch }
</strong>
{ match.suggestionAfterMatch }
</span>,
];
},
// This is slightly different than gutenberg/Autocomplete, we don't support different methods
// of replace/insertion, so we can just return the value.
getOptionCompletion( customer ) {
return {
id: customer.id,
label: customer.email,
};
},
};

View File

@ -2,8 +2,10 @@
/** /**
* Export all autocompleters * Export all autocompleters
*/ */
export { default as countries } from './countries';
export { default as coupons } from './coupons'; export { default as coupons } from './coupons';
export { default as customers } from './customers'; export { default as customers } from './customers';
export { default as emails } from './emails';
export { default as product } from './product'; export { default as product } from './product';
export { default as productCategory } from './product-cat'; export { default as productCategory } from './product-cat';
export { default as variations } from './variations'; export { default as variations } from './variations';

View File

@ -12,8 +12,7 @@ import { stringifyQuery } from '@woocommerce/navigation';
/** /**
* Internal dependencies * Internal dependencies
*/ */
import { computeSuggestionMatch } from './utils'; import { computeSuggestionMatch, getTaxCode } from './utils';
import { getTaxCode } from 'analytics/report/taxes/utils';
/** /**
* A tax completer. * A tax completer.

View File

@ -1,4 +1,9 @@
/** @format */ /** @format */
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
/** /**
* Parse a string suggestion, split apart by where the first matching query is. * Parse a string suggestion, split apart by where the first matching query is.
* Used to display matched partial in bold. * Used to display matched partial in bold.
@ -19,3 +24,15 @@ export function computeSuggestionMatch( suggestion, query ) {
suggestionAfterMatch: suggestion.substring( indexOfMatch + query.length ), suggestionAfterMatch: suggestion.substring( indexOfMatch + query.length ),
}; };
} }
export function getTaxCode( tax ) {
return [ tax.country, tax.state, tax.name || __( 'TAX', 'wc-admin' ), tax.priority ]
.map( item =>
item
.toString()
.toUpperCase()
.trim()
)
.filter( Boolean )
.join( '-' );
}

View File

@ -14,7 +14,7 @@ import classnames from 'classnames';
* Internal dependencies * Internal dependencies
*/ */
import Autocomplete from './autocomplete'; import Autocomplete from './autocomplete';
import { coupons, customers, product, productCategory, taxes, variations } from './autocompleters'; import { countries, coupons, customers, emails, product, productCategory, taxes, variations } from './autocompleters';
import Tag from '../tag'; import Tag from '../tag';
/** /**
@ -66,18 +66,22 @@ class Search extends Component {
getAutocompleter() { getAutocompleter() {
switch ( this.props.type ) { switch ( this.props.type ) {
case 'countries':
return countries;
case 'coupons':
return coupons;
case 'customers':
return customers;
case 'emails':
return emails;
case 'products': case 'products':
return product; return product;
case 'product_cats': case 'product_cats':
return productCategory; return productCategory;
case 'coupons':
return coupons;
case 'taxes': case 'taxes':
return taxes; return taxes;
case 'variations': case 'variations':
return variations; return variations;
case 'customers':
return customers;
default: default:
return {}; return {};
} }
@ -219,11 +223,13 @@ Search.propTypes = {
* The object type to be used in searching. * The object type to be used in searching.
*/ */
type: PropTypes.oneOf( [ type: PropTypes.oneOf( [
'countries',
'coupons',
'customers',
'emails',
'orders',
'products', 'products',
'product_cats', 'product_cats',
'orders',
'customers',
'coupons',
'taxes', 'taxes',
'variations', 'variations',
] ).isRequired, ] ).isRequired,
@ -238,7 +244,10 @@ Search.propTypes = {
*/ */
selected: PropTypes.arrayOf( selected: PropTypes.arrayOf(
PropTypes.shape( { PropTypes.shape( {
id: PropTypes.number.isRequired, id: PropTypes.oneOfType( [
PropTypes.number,
PropTypes.string,
] ).isRequired,
label: PropTypes.string, label: PropTypes.string,
} ) } )
), ),

View File

@ -28,4 +28,5 @@
@import 'summary/style.scss'; @import 'summary/style.scss';
@import 'table/style.scss'; @import 'table/style.scss';
@import 'tag/style.scss'; @import 'tag/style.scss';
@import 'text-control-with-affixes/style.scss';
@import 'view-more-list/style.scss'; @import 'view-more-list/style.scss';

View File

@ -82,7 +82,11 @@ Tag.propTypes = {
/** /**
* The ID for this item, used in the remove function. * The ID for this item, used in the remove function.
*/ */
id: PropTypes.number, id: PropTypes.oneOfType( [
PropTypes.number,
PropTypes.string,
] ),
/** /**
* The name for this item, displayed as the tag's text. * The name for this item, displayed as the tag's text.
*/ */

View File

@ -0,0 +1,46 @@
```jsx
import { TextControlWithAffixes } from '@woocommerce/components';
const MyTextControlWithAffixes = withState( {
first: '',
second: '',
third: '',
fourth: '',
fifth: '',
} )( ( { first, second, third, fourth, fifth, setState } ) => (
<div>
<TextControlWithAffixes
label="Text field without affixes"
value={ first }
placeholder="Placeholder"
onChange={ value => setState( { first: value } ) }
/>
<TextControlWithAffixes
prefix="$"
label="Text field with a prefix"
value={ second }
onChange={ value => setState( { second: value } ) }
/>
<TextControlWithAffixes
prefix="Prefix"
suffix="Suffix"
label="Text field with both affixes"
value={ third }
onChange={ value => setState( { third: value } ) }
/>
<TextControlWithAffixes
suffix="%"
label="Text field with a suffix"
value={ fourth }
onChange={ value => setState( { fourth: value } ) }
/>
<TextControlWithAffixes
prefix="$"
label="Text field with prefix and help text"
value={ fifth }
onChange={ value => setState( { fifth: value } ) }
help="This is some help text."
/>
</div>
) );
```

View File

@ -0,0 +1,119 @@
/** @format */
/**
* External dependencies
*/
import { Component } from '@wordpress/element';
import PropTypes from 'prop-types';
import { BaseControl } from '@wordpress/components';
import { withInstanceId } from '@wordpress/compose';
/**
* This component is essentially a wrapper (really a reimplementation) around the
* TextControl component that adds support for affixes, i.e. the ability to display
* a fixed part either at the beginning or at the end of the text input.
*/
class TextControlWithAffixes extends Component {
render() {
const {
label,
value,
help,
className,
instanceId,
onChange,
prefix,
suffix,
type,
...props
} = this.props;
const id = `inspector-text-control-with-affixes-${ instanceId }`;
const onChangeValue = ( event ) => onChange( event.target.value );
const describedby = [];
if ( help ) {
describedby.push( `${ id }__help` );
}
if ( prefix ) {
describedby.push( `${ id }__prefix` );
}
if ( suffix ) {
describedby.push( `${ id }__suffix` );
}
return (
<BaseControl label={ label } id={ id } help={ help } className={ className }>
<div className="text-control-with-affixes">
{ prefix && (
<span
id={ `${ id }__prefix` }
className="text-control-with-affixes__prefix"
>
{ prefix }
</span>
) }
<input
className="components-text-control__input"
type={ type }
id={ id }
value={ value }
onChange={ onChangeValue }
aria-describedby={ describedby.join( ' ' ) }
{ ...props }
/>
{ suffix && (
<span
id={ `${ id }__suffix` }
className="text-control-with-affixes__suffix"
>
{ suffix }
</span>
) }
</div>
</BaseControl>
);
}
}
TextControlWithAffixes.defaultProps = {
type: 'text',
};
TextControlWithAffixes.propTypes = {
/**
* If this property is added, a label will be generated using label property as the content.
*/
label: PropTypes.string,
/**
* If this property is added, a help text will be generated using help property as the content.
*/
help: PropTypes.string,
/**
* Type of the input element to render. Defaults to "text".
*/
type: PropTypes.string,
/**
* The current value of the input.
*/
value: PropTypes.string.isRequired,
/**
* The class that will be added with "components-base-control" to the classes of the wrapper div.
* If no className is passed only components-base-control is used.
*/
className: PropTypes.string,
/**
* A function that receives the value of the input.
*/
onChange: PropTypes.func.isRequired,
/**
* Markup to be inserted at the beginning of the input.
*/
prefix: PropTypes.node,
/**
* Markup to be appended at the end of the input.
*/
suffix: PropTypes.node,
};
export default withInstanceId( TextControlWithAffixes );

View File

@ -0,0 +1,54 @@
.text-control-with-affixes {
display: inline-flex;
flex-direction: row;
width: 100%;
input[type='email'],
input[type='password'],
input[type='url'],
input[type='text'],
input[type='number'] {
flex-grow: 1;
margin: 0;
&:disabled {
border-right-width: 0;
& + .text-control-with-affixes__suffix {
border-left: 1px solid $core-grey-light-500;
}
}
}
}
.text-control-with-affixes__prefix,
.text-control-with-affixes__suffix {
position: relative;
background: $white;
border: 1px solid $core-grey-light-500;
color: $gray-text;
padding: 7px 14px;
white-space: nowrap;
flex: 1 0 auto;
font-size: 14px;
line-height: 1.5;
}
.text-control-with-affixes__prefix {
border-right: none;
& + input[type='email'],
& + input[type='password'],
& + input[type='url'],
& + input[type='text'],
& + input[type='number'] {
&:disabled {
border-left-color: $core-grey-light-500;
border-right-width: 1px;
}
}
}
.text-control-with-affixes__suffix {
border-left: none;
}

View File

@ -1,3 +1,7 @@
# 1.0.3
- Fix missing comma seperator in date inside tooltips.
# 1.0.2 # 1.0.2
- Add `getChartTypeForQuery` function to ensure chart type is always `bar` or `line` - Add `getChartTypeForQuery` function to ensure chart type is always `bar` or `line`

View File

@ -3,5 +3,6 @@
"config:base" "config:base"
], ],
"lockFileMaintenance": { "enabled": true }, "lockFileMaintenance": { "enabled": true },
"ignoreDeps": ["phpunit/phpunit"] "ignoreDeps": ["phpunit/phpunit"],
"schedule": ["before 3am on wednesday"]
} }

View File

@ -0,0 +1,114 @@
<?php
/**
* Reports Stock REST API Test
*
* @package WooCommerce\Tests\API
* @since 3.5.0
*/
class WC_Tests_API_Reports_Stock extends WC_REST_Unit_Test_Case {
/**
* Endpoints.
*
* @var string
*/
protected $endpoint = '/wc/v3/reports/stock';
/**
* Setup test reports stock data.
*/
public function setUp() {
parent::setUp();
$this->user = $this->factory->user->create(
array(
'role' => 'administrator',
)
);
}
/**
* Test route registration.
*/
public function test_register_routes() {
$routes = $this->server->get_routes();
$this->assertArrayHasKey( $this->endpoint, $routes );
}
/**
* Test getting reports.
*/
public function test_get_reports() {
wp_set_current_user( $this->user );
WC_Helper_Reports::reset_stats_dbs();
// Populate all of the data.
$low_stock = new WC_Product_Simple();
$low_stock->set_name( 'Test low stock' );
$low_stock->set_regular_price( 5 );
$low_stock->set_manage_stock( true );
$low_stock->set_stock_quantity( 1 );
$low_stock->set_stock_status( 'instock' );
$low_stock->save();
$out_of_stock = new WC_Product_Simple();
$out_of_stock->set_name( 'Test out of stock' );
$out_of_stock->set_regular_price( 5 );
$out_of_stock->set_stock_status( 'outofstock' );
$out_of_stock->save();
$request = new WP_REST_Request( 'GET', $this->endpoint );
$request->set_param( 'include', implode( ',', array( $low_stock->get_id(), $out_of_stock->get_id() ) ) );
$request->set_param( 'orderby', 'id' );
$response = $this->server->dispatch( $request );
$reports = $response->get_data();
$this->assertEquals( 200, $response->get_status() );
$this->assertEquals( 2, count( $reports ) );
$this->assertEquals( $low_stock->get_id(), $reports[0]['id'] );
$this->assertEquals( 'instock', $reports[0]['stock_status'] );
$this->assertEquals( 1, $reports[0]['stock_quantity'] );
$this->assertArrayHasKey( '_links', $reports[0] );
$this->assertArrayHasKey( 'product', $reports[0]['_links'] );
$request = new WP_REST_Request( 'GET', $this->endpoint );
$request->set_param( 'include', implode( ',', array( $low_stock->get_id(), $out_of_stock->get_id() ) ) );
$request->set_param( 'type', 'lowstock' );
$response = $this->server->dispatch( $request );
$reports = $response->get_data();
$this->assertEquals( 200, $response->get_status() );
$this->assertEquals( 1, count( $reports ) );
}
/**
* Test getting reports without valid permissions.
*/
public function test_get_reports_without_permission() {
wp_set_current_user( 0 );
$response = $this->server->dispatch( new WP_REST_Request( 'GET', $this->endpoint ) );
$this->assertEquals( 401, $response->get_status() );
}
/**
* Test reports schema.
*/
public function test_reports_schema() {
wp_set_current_user( $this->user );
$request = new WP_REST_Request( 'OPTIONS', $this->endpoint );
$response = $this->server->dispatch( $request );
$data = $response->get_data();
$properties = $data['schema']['properties'];
$this->assertEquals( 6, count( $properties ) );
$this->assertArrayHasKey( 'id', $properties );
$this->assertArrayHasKey( 'parent_id', $properties );
$this->assertArrayHasKey( 'name', $properties );
$this->assertArrayHasKey( 'sku', $properties );
$this->assertArrayHasKey( 'stock_status', $properties );
$this->assertArrayHasKey( 'stock_quantity', $properties );
}
}

View File

@ -114,7 +114,7 @@ class WC_Tests_API_Reports_Taxes_Stats extends WC_REST_Unit_Test_Case {
$this->assertEquals( 16, $tax_report['total_tax'] ); $this->assertEquals( 16, $tax_report['total_tax'] );
$this->assertEquals( 13, $tax_report['order_tax'] ); $this->assertEquals( 13, $tax_report['order_tax'] );
$this->assertEquals( 3, $tax_report['shipping_tax'] ); $this->assertEquals( 3, $tax_report['shipping_tax'] );
$this->assertEquals( 2, $tax_report['orders'] ); $this->assertEquals( 2, $tax_report['orders_count'] );
} }
/** /**
@ -145,7 +145,7 @@ class WC_Tests_API_Reports_Taxes_Stats extends WC_REST_Unit_Test_Case {
$this->assertEquals( 5, count( $totals ) ); $this->assertEquals( 5, count( $totals ) );
$this->assertArrayHasKey( 'order_tax', $totals ); $this->assertArrayHasKey( 'order_tax', $totals );
$this->assertArrayHasKey( 'order_count', $totals ); $this->assertArrayHasKey( 'orders_count', $totals );
$this->assertArrayHasKey( 'shipping_tax', $totals ); $this->assertArrayHasKey( 'shipping_tax', $totals );
$this->assertArrayHasKey( 'tax_codes', $totals ); $this->assertArrayHasKey( 'tax_codes', $totals );
$this->assertArrayHasKey( 'total_tax', $totals ); $this->assertArrayHasKey( 'total_tax', $totals );
@ -162,7 +162,7 @@ class WC_Tests_API_Reports_Taxes_Stats extends WC_REST_Unit_Test_Case {
$subtotals = $properties['intervals']['items']['properties']['subtotals']['properties']; $subtotals = $properties['intervals']['items']['properties']['subtotals']['properties'];
$this->assertEquals( 5, count( $subtotals ) ); $this->assertEquals( 5, count( $subtotals ) );
$this->assertArrayHasKey( 'order_tax', $totals ); $this->assertArrayHasKey( 'order_tax', $totals );
$this->assertArrayHasKey( 'order_count', $totals ); $this->assertArrayHasKey( 'orders_count', $totals );
$this->assertArrayHasKey( 'shipping_tax', $totals ); $this->assertArrayHasKey( 'shipping_tax', $totals );
$this->assertArrayHasKey( 'tax_codes', $totals ); $this->assertArrayHasKey( 'tax_codes', $totals );
$this->assertArrayHasKey( 'total_tax', $totals ); $this->assertArrayHasKey( 'total_tax', $totals );

View File

@ -49,6 +49,19 @@ function wc_admin_plugins_notice() {
printf( '<div class="error"><p>%s</p></div>', $message ); /* WPCS: xss ok. */ printf( '<div class="error"><p>%s</p></div>', $message ); /* WPCS: xss ok. */
} }
/**
* Notify users that the plugin needs to be built
*/
function wc_admin_build_notice() {
$message_one = __( 'You have installed a development version of WooCommerce Admin which requires files to be built. From the plugin directory, run <code>npm install</code> to install dependencies, <code>npm run build</code> to build the files.', 'wc-admin' );
$message_two = sprintf(
/* translators: 1: URL of GitHub Repository build page */
__( 'Or you can download a pre-built version of the plugin by visiting <a href="%1$s">the releases page in the repository</a>.', 'wc-admin' ),
'https://github.com/woocommerce/wc-admin/releases'
);
printf( '<div class="error"><p>%s %s</p></div>', $message_one, $message_two ); /* WPCS: xss ok. */
}
/** /**
* Returns true if all dependencies for the wc-admin plugin are loaded. * Returns true if all dependencies for the wc-admin plugin are loaded.
* *
@ -67,6 +80,15 @@ function dependencies_satisfied() {
return $wordpress_includes_gutenberg || $gutenberg_plugin_active; return $wordpress_includes_gutenberg || $gutenberg_plugin_active;
} }
/**
* Returns true if build file exists.
*
* @return bool
*/
function wc_admin_build_file_exists() {
return file_exists( plugin_dir_path( __FILE__ ) . '/dist/app/index.js' );
}
/** /**
* Daily events to run. * Daily events to run.
*/ */
@ -123,7 +145,7 @@ function wc_admin_init() {
} }
// Only create/update tables on init if WP_DEBUG is true. // Only create/update tables on init if WP_DEBUG is true.
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { if ( defined( 'WP_DEBUG' ) && WP_DEBUG && wc_admin_build_file_exists() ) {
WC_Admin_Api_Init::create_db_tables(); WC_Admin_Api_Init::create_db_tables();
} }
} }
@ -144,16 +166,22 @@ function wc_admin_plugins_loaded() {
// Some common utilities. // Some common utilities.
require_once dirname( __FILE__ ) . '/lib/common.php'; require_once dirname( __FILE__ ) . '/lib/common.php';
// Admin note providers.
require_once dirname( __FILE__ ) . '/includes/class-wc-admin-notes-new-sales-record.php';
require_once dirname( __FILE__ ) . '/includes/class-wc-admin-notes-settings-notes.php';
require_once dirname( __FILE__ ) . '/includes/class-wc-admin-notes-woo-subscriptions-notes.php';
// Verify we have a proper build.
if ( ! wc_admin_build_file_exists() ) {
add_action( 'admin_notices', 'wc_admin_build_notice' );
return;
}
// Register script files. // Register script files.
require_once dirname( __FILE__ ) . '/lib/client-assets.php'; require_once dirname( __FILE__ ) . '/lib/client-assets.php';
// Create the Admin pages. // Create the Admin pages.
require_once dirname( __FILE__ ) . '/lib/admin.php'; require_once dirname( __FILE__ ) . '/lib/admin.php';
// Admin note providers.
require_once dirname( __FILE__ ) . '/includes/class-wc-admin-notes-new-sales-record.php';
require_once dirname( __FILE__ ) . '/includes/class-wc-admin-notes-settings-notes.php';
require_once dirname( __FILE__ ) . '/includes/class-wc-admin-notes-woo-subscriptions-notes.php';
} }
add_action( 'plugins_loaded', 'wc_admin_plugins_loaded' ); add_action( 'plugins_loaded', 'wc_admin_plugins_loaded' );