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 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

View File

@ -5,6 +5,7 @@
import { applyFilters } from '@wordpress/hooks';
import { Component } from '@wordpress/element';
import { compose } from '@wordpress/compose';
import { withDispatch } from '@wordpress/data';
import { get, orderBy } from 'lodash';
import PropTypes from 'prop-types';
@ -20,10 +21,33 @@ import { onQueryChange } from '@woocommerce/navigation';
import ReportError from 'analytics/components/report-error';
import { getReportChartData, getReportTableData } from 'store/reports/utils';
import withSelect from 'wc-api/with-select';
import { extendTableData } from './utils';
const TABLE_FILTER = 'woocommerce_admin_report_table';
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() {
const {
getHeadersContent,
@ -36,6 +60,7 @@ class ReportTable extends Component {
// so they are not included in the `tableProps` variable.
endpoint,
tableQuery,
userPrefColumns,
...tableProps
} = this.props;
@ -50,7 +75,7 @@ class ReportTable extends Component {
const isRequesting = tableData.isRequesting || primaryData.isRequesting;
const orderedItems = orderBy( items.data, query.orderby, query.order );
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, {
endpoint: endpoint,
headers: getHeadersContent(),
@ -58,20 +83,24 @@ class ReportTable extends Component {
ids: itemIdField ? orderedItems.map( item => item[ itemIdField ] ) : null,
rows: getRowsContent( orderedItems ),
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 (
<TableCard
downloadable
headers={ headers }
headers={ filteredHeaders }
ids={ ids }
isLoading={ isRequesting }
onQueryChange={ onQueryChange }
onColumnsChange={ this.onColumnsChange }
rows={ rows }
rowsPerPage={ parseInt( query.per_page ) }
summary={ summary }
totalRows={ totalCount }
totalRows={ totalResults }
{ ...tableProps }
/>
);
@ -79,10 +108,24 @@ class ReportTable extends Component {
}
ReportTable.propTypes = {
/**
* The key for user preferences settings for column visibility.
*/
columnPrefsKey: PropTypes.string,
/**
* The endpoint to use in API calls.
*/
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.
*/
@ -124,16 +167,33 @@ ReportTable.defaultProps = {
export default compose(
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 primaryData = getSummary
? getReportChartData( chartEndpoint, 'primary', query, select )
: {};
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 {
primaryData,
tableData: queriedTableData,
updateCurrentUserData,
};
} )
)( 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 ) {
return [];
}
return [
{
label: _n( 'category', 'categories', totalCount, 'wc-admin' ),
value: numberFormat( totalCount ),
label: _n( 'category', 'categories', totalResults, 'wc-admin' ),
value: numberFormat( totalResults ),
},
{
label: _n( 'item sold', 'items sold', totals.items_sold, 'wc-admin' ),
@ -136,6 +136,7 @@ export default class CategoriesReportTable extends Component {
itemIdField="category_id"
query={ query }
title={ __( 'Categories', 'wc-admin' ) }
columnPrefsKey="categories_report_columns"
/>
);
}

View File

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

View File

@ -3,8 +3,13 @@
* External dependencies
*/
import { __, _x } from '@wordpress/i18n';
import { getRequestByIdString } from '../../../lib/async-requests';
import { NAMESPACE } from '../../../store/constants';
import { decodeEntities } from '@wordpress/html-entities';
/**
* Internal dependencies
*/
import { getRequestByIdString } from 'lib/async-requests';
import { NAMESPACE } from 'store/constants';
export const filters = [
{
@ -30,7 +35,7 @@ export const advancedFilters = {
name: {
labels: {
add: __( 'Name', 'wc-admin' ),
placeholder: __( 'Search customer name', 'wc-admin' ),
placeholder: __( 'Search', 'wc-admin' ),
remove: __( 'Remove customer name filter', '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 */
@ -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*/

View File

@ -66,7 +66,7 @@ export default class CustomersReportTable extends Component {
{
label: __( 'AOV', 'wc-admin' ),
screenReaderLabel: __( 'Average Order Value', 'wc-admin' ),
key: 'average_order_value',
key: 'avg_order_value',
isNumeric: true,
},
{
@ -98,19 +98,20 @@ export default class CustomersReportTable extends Component {
return customers.map( customer => {
const {
average_order_value,
id,
city,
country,
avg_order_value,
billing,
date_last_active,
date_sign_up,
email,
name,
first_name,
id,
last_name,
orders_count,
postal_code,
username,
total_spend,
} = customer;
const { postcode, city, country } = billing || {};
const name = `${ first_name } ${ last_name }`;
const customerNameLink = (
<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 ),
},
{
display: average_order_value,
value: getCurrencyFormatDecimal( average_order_value ),
display: formatCurrency( avg_order_value ),
value: getCurrencyFormatDecimal( avg_order_value ),
},
{
display: formatDate( formats.tableFormat, date_last_active ),
@ -160,8 +161,8 @@ export default class CustomersReportTable extends Component {
value: city,
},
{
display: postal_code,
value: postal_code,
display: postcode,
value: postcode,
},
];
} );
@ -173,6 +174,11 @@ export default class CustomersReportTable extends Component {
return (
<ReportTable
endpoint="customers"
extendItemsMethodNames={ {
load: 'getCustomers',
getError: 'getCustomersError',
isRequesting: 'isGetCustomersRequesting',
} }
getHeadersContent={ this.getHeadersContent }
getRowsContent={ this.getRowsContent }
itemIdField="id"
@ -181,6 +187,7 @@ export default class CustomersReportTable extends Component {
searchBy="customers"
searchParam="name_includes"
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 CouponsReport from './coupons';
import TaxesReport from './taxes';
import DownloadsReport from './downloads';
import StockReport from './stock';
import CustomersReport from './customers';
const REPORTS_FILTER = 'woocommerce-reports-list';
@ -60,6 +62,16 @@ const getReports = () => {
title: __( 'Taxes', 'wc-admin' ),
component: TaxesReport,
},
{
report: 'downloads',
title: __( 'Downloads', 'wc-admin' ),
component: DownloadsReport,
},
{
report: 'stock',
title: __( 'Stock', 'wc-admin' ),
component: StockReport,
},
{
report: 'customers',
title: __( 'Customers', 'wc-admin' ),

View File

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

View File

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

View File

@ -214,6 +214,7 @@ export default class ProductsReportTable extends Component {
extended_info: true,
} }
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 ) {
return [];
}
return [
{
label: _n( 'day', 'days', totalCount, 'wc-admin' ),
value: numberFormat( totalCount ),
label: _n( 'day', 'days', totalResults, 'wc-admin' ),
value: numberFormat( totalResults ),
},
{
label: _n( 'order', 'orders', totals.orders_count, 'wc-admin' ),
@ -215,6 +215,7 @@ class RevenueReportTable extends Component {
query={ query }
tableData={ tableData }
title={ __( 'Revenue', 'wc-admin' ) }
columnPrefsKey="revenue_report_columns"
/>
);
}
@ -224,7 +225,7 @@ export default compose(
withSelect( ( select, props ) => {
const { query } = props;
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
const tableQuery = {
@ -237,14 +238,14 @@ export default compose(
before: appendTimestamp( datesFromQuery.primary.before, 'end' ),
};
const revenueData = getReportStats( 'revenue', tableQuery );
const isError = isReportStatsError( 'revenue', tableQuery );
const isError = Boolean( getReportStatsError( 'revenue', tableQuery ) );
const isRequesting = isReportStatsRequesting( 'revenue', tableQuery );
return {
tableData: {
items: {
data: get( revenueData, [ 'data', 'intervals' ] ),
totalCount: get( revenueData, [ 'totalResults' ] ),
totalResults: get( revenueData, [ 'totalResults' ] ),
},
isError,
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 { formatCurrency, getCurrencyFormatDecimal } from '@woocommerce/currency';
import { getTaxCode } from './utils';
/**
* Internal dependencies
@ -71,25 +72,23 @@ export default class TaxesReportTable extends Component {
getRowsContent( taxes ) {
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
const taxLink = (
<Link href="" type="wc-admin">
{ tax_rate_id }
{ getTaxCode( tax ) }
</Link>
);
return [
// @TODO it should be the tax code, not the tax ID
{
display: taxLink,
value: tax_rate_id,
},
{
// @TODO add `rate` once it's returned by the API
display: '',
value: '',
display: tax_rate.toFixed( 2 ) + '%',
value: tax_rate,
},
{
display: formatCurrency( total_tax ),
@ -115,12 +114,10 @@ export default class TaxesReportTable extends Component {
if ( ! totals ) {
return [];
}
// @TODO the number of total rows should come from the API
const totalRows = 0;
return [
{
label: _n( 'tax code', 'tax codes', totalRows, 'wc-admin' ),
value: numberFormat( totalRows ),
label: _n( 'tax code', 'tax codes', totals.tax_codes, 'wc-admin' ),
value: numberFormat( totals.tax_codes ),
},
{
label: __( 'total tax', 'wc-admin' ),
@ -135,7 +132,7 @@ export default class TaxesReportTable extends Component {
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 ),
},
];
@ -153,7 +150,11 @@ export default class TaxesReportTable extends Component {
getSummary={ this.getSummary }
itemIdField="tax_rate_id"
query={ query }
tableQuery={ {
orderby: query.orderby || 'tax_rate_id',
} }
title={ __( 'Taxes', 'wc-admin' ) }
columnPrefsKey="taxes_report_columns"
/>
);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -141,7 +141,7 @@ function getRequestQuery( endpoint, dataType, query ) {
* @return {Object} Object containing summary number responses.
*/
export function getSummaryNumbers( endpoint, query, select ) {
const { getReportStats, isReportStatsRequesting, isReportStatsError } = select( 'wc-api' );
const { getReportStats, getReportStatsError, isReportStatsRequesting } = select( 'wc-api' );
const response = {
isRequesting: false,
isError: false,
@ -155,7 +155,7 @@ export function getSummaryNumbers( endpoint, query, select ) {
const primary = getReportStats( endpoint, primaryQuery );
if ( isReportStatsRequesting( endpoint, primaryQuery ) ) {
return { ...response, isRequesting: true };
} else if ( isReportStatsError( endpoint, primaryQuery ) ) {
} else if ( getReportStatsError( endpoint, primaryQuery ) ) {
return { ...response, isError: true };
}
@ -165,7 +165,7 @@ export function getSummaryNumbers( endpoint, query, select ) {
const secondary = getReportStats( endpoint, secondaryQuery );
if ( isReportStatsRequesting( endpoint, secondaryQuery ) ) {
return { ...response, isRequesting: true };
} else if ( isReportStatsError( endpoint, secondaryQuery ) ) {
} else if ( getReportStatsError( endpoint, secondaryQuery ) ) {
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)
*/
export function getReportChartData( endpoint, dataType, query, select ) {
const { getReportStats, isReportStatsRequesting, isReportStatsError } = select( 'wc-api' );
const { getReportStats, getReportStatsError, isReportStatsRequesting } = select( 'wc-api' );
const response = {
isEmpty: false,
@ -201,7 +201,7 @@ export function getReportChartData( endpoint, dataType, query, select ) {
if ( isReportStatsRequesting( endpoint, requestQuery ) ) {
return { ...response, isRequesting: true };
} else if ( isReportStatsError( endpoint, requestQuery ) ) {
} else if ( getReportStatsError( endpoint, requestQuery ) ) {
return { ...response, isError: true };
} else if ( isReportDataEmpty( stats ) ) {
return { ...response, isEmpty: true };
@ -224,7 +224,7 @@ export function getReportChartData( endpoint, dataType, query, select ) {
if ( isReportStatsRequesting( endpoint, nextQuery ) ) {
continue;
}
if ( isReportStatsError( endpoint, nextQuery ) ) {
if ( getReportStatsError( endpoint, nextQuery ) ) {
isError = true;
isFetching = false;
break;
@ -298,7 +298,7 @@ export function getReportTableQuery( endpoint, urlQuery, query ) {
* @return {Object} Object Table data response
*/
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 response = {
@ -313,7 +313,7 @@ export function getReportTableData( endpoint, urlQuery, select, query = {} ) {
const items = getReportItems( endpoint, tableQuery );
if ( isReportItemsRequesting( endpoint, tableQuery ) ) {
return { ...response, isRequesting: true };
} else if ( isReportItemsError( endpoint, tableQuery ) ) {
} else if ( getReportItemsError( endpoint, tableQuery ) ) {
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;
};
const getNotesError = getResource => ( query = {} ) => {
const resourceName = getResourceName( 'note-query', query );
return getResource( resourceName ).error;
};
const isGetNotesRequesting = getResource => ( query = {} ) => {
const resourceName = getResourceName( 'note-query', query );
const { lastRequested, lastReceived } = getResource( resourceName );
@ -36,13 +41,8 @@ const isGetNotesRequesting = getResource => ( query = {} ) => {
return lastRequested > lastReceived;
};
const isGetNotesError = getResource => ( query = {} ) => {
const resourceName = getResourceName( 'note-query', query );
return getResource( resourceName ).error;
};
export default {
getNotes,
getNotesError,
isGetNotesRequesting,
isGetNotesError,
};

View File

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

View File

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

View File

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

View File

@ -16,9 +16,9 @@ import { getResourceIdentifier, getResourcePrefix } from '../../utils';
import { NAMESPACE } from '../../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.
const swaggerEndpoints = [ 'categories', 'coupons', 'taxes' ];
const swaggerEndpoints = [ 'categories', 'coupons' ];
const typeEndpointMap = {
'report-stats-query-orders': 'orders',

View File

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

View File

@ -29,6 +29,11 @@ const getReviewsTotalCount = ( getResource, requireResource ) => (
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 resourceName = getResourceName( 'review-query', query );
const { lastRequested, lastReceived } = getResource( resourceName );
@ -40,14 +45,9 @@ const isGetReviewsRequesting = getResource => ( query = {} ) => {
return lastRequested > lastReceived;
};
const isGetReviewsError = getResource => ( query = {} ) => {
const resourceName = getResourceName( 'review-query', query );
return getResource( resourceName ).error;
};
export default {
getReviews,
getReviewsError,
getReviewsTotalCount,
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
*/
import customers from './customers';
import notes from './notes';
import orders from './orders';
import reportItems from './reports/items';
import reportStats from './reports/stats';
import reviews from './reviews';
import user from './user';
function createWcApiSpec() {
return {
mutations: {
...user.mutations,
},
selectors: {
...customers.selectors,
...notes.selectors,
...orders.selectors,
...reportItems.selectors,
...reportStats.selectors,
...reviews.selectors,
...user.selectors,
},
operations: {
read( resourceNames ) {
return [
...customers.operations.read( resourceNames ),
...notes.operations.read( resourceNames ),
...orders.operations.read( resourceNames ),
...reportItems.operations.read( resourceNames ),
...reportStats.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 );
this.onStoreChange = this.onStoreChange.bind( this );
this.subscribe( props.registry );
this.onUnmounts = {};
this.mergeProps = this.getNextMergeProps( props );
}
@ -59,6 +59,7 @@ const withSelect = mapSelectToProps =>
getNextMergeProps( props ) {
const storeSelectors = {};
const onCompletes = [];
const onUnmounts = {};
const componentContext = { component: this };
const getStoreFromRegistry = ( key, registry, context ) => {
@ -69,8 +70,9 @@ const withSelect = mapSelectToProps =>
if ( isFunction( selectorsForKey ) ) {
// This store has special handling for its selectors.
// 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 );
onUnmount && ( onUnmounts[ key ] = onUnmount );
storeSelectors[ key ] = selectors;
} else {
storeSelectors[ key ] = selectorsForKey;
@ -107,6 +109,7 @@ const withSelect = mapSelectToProps =>
componentWillUnmount() {
this.canRunSelection = false;
this.unsubscribe();
Object.keys( this.onUnmounts ).forEach( key => this.onUnmounts[ key ]() );
}
shouldComponentUpdate( nextProps, nextState ) {

View File

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

View File

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

View File

@ -27,4 +27,5 @@
* [Summary](components/summary.md)
* [Table](components/table.md)
* [Tag](components/tag.md)
* [TextControlWithAffixes](components/text-control-with-affixes.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';
/**
* 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.
*
@ -150,7 +180,7 @@ class WC_Admin_REST_Reports_Taxes_Stats_Controller extends WC_REST_Reports_Contr
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'order_count' => array(
'orders_count' => array(
'description' => __( 'Amount of orders.', 'wc-admin' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),

View File

@ -76,6 +76,7 @@ class WC_Admin_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-customers-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-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-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-stock-controller.php';
$controllers = array(
'WC_Admin_REST_Admin_Notes_Controller',
'WC_Admin_REST_Customers_Controller',
'WC_Admin_REST_Products_Controller',
'WC_Admin_REST_Product_Reviews_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_Coupons_Controller',
'WC_Admin_REST_Reports_Coupons_Stats_Controller',
'WC_Admin_REST_Reports_Stock_Controller',
);
foreach ( $controllers as $controller ) {
@ -165,6 +169,17 @@ class WC_Admin_Api_Init {
$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.
if ( isset( $endpoints['/wc/v3/products/(?P<id>[\d]+)'] )
&& 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',
'order_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',
'order_tax' => 'SUM(order_tax) as order_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] );
$this->update_interval_boundary_dates( $query_args['after'], $query_args['before'], $query_args['interval'], $intervals );
$this->create_interval_subtotals( $intervals );
$data = (object) array(
'totals' => $totals,
'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(
array(
'title' => __( 'Customers', 'wc-admin' ),
@ -338,5 +354,80 @@ function woocommerce_embed_page_header() {
</div>
<?php
}
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 .= "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
* `wcAssetUrl` can be used in its place throughout the codebase.
@ -163,6 +179,10 @@ function wc_admin_print_script_settings() {
'siteTitle' => get_bloginfo( 'name' ),
'trackingEnabled' => $tracking_enabled,
);
foreach ( $preload_data_endpoints as $key => $endpoint ) {
$settings['dataEndpoints'][ $key ] = $preload_data[ $endpoint ]['body'];
}
?>
<script type="text/javascript">
<?php

View File

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

View File

@ -1289,7 +1289,7 @@
},
"globby": {
"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==",
"dev": true,
"requires": {
@ -1740,7 +1740,7 @@
},
"globby": {
"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==",
"dev": true,
"requires": {
@ -3546,11 +3546,6 @@
"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": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz",
@ -3686,7 +3681,7 @@
},
"util": {
"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=",
"dev": true,
"requires": {
@ -4759,11 +4754,6 @@
"parse5": "^3.0.1"
}
},
"chickencurry": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/chickencurry/-/chickencurry-1.1.1.tgz",
"integrity": "sha1-AmVfKyazvC7hrh5TFoht4463lzg="
},
"chokidar": {
"version": "2.0.4",
"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",
"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": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@ -6313,7 +6295,7 @@
},
"regexpu-core": {
"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=",
"dev": true,
"requires": {
@ -6951,8 +6933,9 @@
}
},
"docsify-cli": {
"version": "github:docsifyjs/docsify-cli#5df389962a0eb69ac02701b297b77b2fbb85ec28",
"from": "github:docsifyjs/docsify-cli#5df38996",
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/docsify-cli/-/docsify-cli-4.3.0.tgz",
"integrity": "sha512-88O1sMeoZv4lb5GPSJzDtOAv2KzBjpQaSqVlVqY+6hGJfb2wpz9PvlUhvlgPq54zu4kPDeCCyUYgqa/llhKg3w==",
"dev": true,
"requires": {
"chalk": "^1.1.3",
@ -7831,7 +7814,7 @@
},
"espree": {
"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==",
"requires": {
"acorn": "^5.5.0",
@ -7883,7 +7866,7 @@
},
"events": {
"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=",
"dev": true
},
@ -10458,15 +10441,15 @@
"dev": true
},
"html-to-react": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/html-to-react/-/html-to-react-1.3.3.tgz",
"integrity": "sha512-4Qi5/t8oBr6c1t1kBJKyxEeJu0lb7ctvq29oFZioiUHH0Wz88VWGwoXuH26HDt9v64bDHA4NMPNTH8bVrcaJWA==",
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/html-to-react/-/html-to-react-1.3.4.tgz",
"integrity": "sha512-/tWDdb/8Koi/QEP5YUY1653PcDpBnnMblXRhotnTuhFDjI1Fc6Wzox5d4sw73Xk5rM2OdM5np4AYjT/US/Wj7Q==",
"requires": {
"domhandler": "^2.3.0",
"domhandler": "^2.4.2",
"escape-string-regexp": "^1.0.5",
"htmlparser2": "^3.8.3",
"ramda": "^0.25.0",
"underscore.string.fp": "^1.0.4"
"htmlparser2": "^3.10.0",
"lodash.camelcase": "^4.3.0",
"ramda": "^0.26"
}
},
"htmlparser2": {
@ -13079,6 +13062,11 @@
"integrity": "sha1-DZnzzNem0mHRm9rrkkUAXShYCOc=",
"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": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
@ -14082,7 +14070,7 @@
},
"camelcase-keys": {
"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=",
"dev": true,
"requires": {
@ -14130,7 +14118,7 @@
},
"meow": {
"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=",
"dev": true,
"requires": {
@ -15977,11 +15965,24 @@
}
},
"prismjs": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.6.0.tgz",
"integrity": "sha1-EY2V+3pm26InLjQ7NF9SNmWds2U=",
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.15.0.tgz",
"integrity": "sha512-Lf2JrFYx8FanHrjoV5oL8YHCclLQgbJcVZR+gikGGMqz6ub5QVWDTM6YIwm3BuPxM/LOV+rKns3LssXNLIf+DA==",
"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": {
@ -16179,9 +16180,9 @@
"integrity": "sha1-635iZ1SN3t+4mcG5Dlc3RVnN234="
},
"ramda": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/ramda/-/ramda-0.25.0.tgz",
"integrity": "sha512-GXpfrYVPwx3K7RQ6aYT8KPS8XViSXUVJT1ONhoKPE9VAleW42YE+U+8VEyGWt41EnEQW7gwecYJriTI0pKoecQ=="
"version": "0.26.1",
"resolved": "https://registry.npmjs.org/ramda/-/ramda-0.26.1.tgz",
"integrity": "sha512-hLWjpy7EnsDBb0p+Z3B7rPi3GDeRG5ZtiI33kJhTt+ORCd38AbAIjB/9zRIUoeTbE/AVX5ZkU7m6bznsvrf8eQ=="
},
"randexp": {
"version": "0.4.6",
@ -16240,10 +16241,33 @@
"dev": true
},
"raw-loader": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/raw-loader/-/raw-loader-0.5.1.tgz",
"integrity": "sha1-DD0L6u2KAclm2Xh793goElKpeao=",
"dev": true
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/raw-loader/-/raw-loader-1.0.0.tgz",
"integrity": "sha512-Uqy5AqELpytJTRxYT4fhltcKPj0TyaEpzJDcGz7DFJi+pQOOi3GjR/DOdxTkTsF+NzhnldIoG6TORaBlInUuqA==",
"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": {
"version": "1.2.8",
@ -16441,6 +16465,16 @@
"prismjs": "1.6",
"prop-types": "^15.5.8",
"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": {
@ -17501,11 +17535,6 @@
"integrity": "sha1-52OI0heZLCUnUCQdPTlW/tmNj/Q=",
"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": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/rgb/-/rgb-0.1.0.tgz",
@ -18077,7 +18106,7 @@
"dependencies": {
"source-map": {
"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=",
"dev": true,
"requires": {
@ -18643,7 +18672,7 @@
},
"stream-browserify": {
"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=",
"dev": true,
"requires": {
@ -20141,22 +20170,6 @@
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.4.4.tgz",
"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": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/unescape/-/unescape-0.2.0.tgz",
@ -21437,7 +21450,7 @@
},
"yargs": {
"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==",
"requires": {
"cliui": "^4.0.0",

View File

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

View File

@ -2,6 +2,14 @@
- 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 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

View File

@ -27,6 +27,7 @@ import Card from '../../card';
import Link from '../../link';
import SelectFilter from './select-filter';
import SearchFilter from './search-filter';
import NumberFilter from './number-filter';
const matches = [
{ value: 'all', label: __( 'All', 'wc-admin' ) },
@ -191,6 +192,15 @@ class AdvancedFilters extends Component {
query={ query }
/>
) }
{ 'Number' === input.component && (
<NumberFilter
filter={ filter }
config={ config.filters[ key ] }
onFilterChange={ this.onFilterChange }
isEnglish={ isEnglish }
query={ query }
/>
) }
<IconButton
className="woocommerce-filters-advanced__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',
},
},
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 TableSummary } from './table/summary';
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 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 { default as countries } from './countries';
export { default as coupons } from './coupons';
export { default as customers } from './customers';
export { default as emails } from './emails';
export { default as product } from './product';
export { default as productCategory } from './product-cat';
export { default as variations } from './variations';

View File

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

View File

@ -1,4 +1,9 @@
/** @format */
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
/**
* Parse a string suggestion, split apart by where the first matching query is.
* Used to display matched partial in bold.
@ -19,3 +24,15 @@ export function computeSuggestionMatch( suggestion, query ) {
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
*/
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';
/**
@ -66,18 +66,22 @@ class Search extends Component {
getAutocompleter() {
switch ( this.props.type ) {
case 'countries':
return countries;
case 'coupons':
return coupons;
case 'customers':
return customers;
case 'emails':
return emails;
case 'products':
return product;
case 'product_cats':
return productCategory;
case 'coupons':
return coupons;
case 'taxes':
return taxes;
case 'variations':
return variations;
case 'customers':
return customers;
default:
return {};
}
@ -219,11 +223,13 @@ Search.propTypes = {
* The object type to be used in searching.
*/
type: PropTypes.oneOf( [
'countries',
'coupons',
'customers',
'emails',
'orders',
'products',
'product_cats',
'orders',
'customers',
'coupons',
'taxes',
'variations',
] ).isRequired,
@ -238,7 +244,10 @@ Search.propTypes = {
*/
selected: PropTypes.arrayOf(
PropTypes.shape( {
id: PropTypes.number.isRequired,
id: PropTypes.oneOfType( [
PropTypes.number,
PropTypes.string,
] ).isRequired,
label: PropTypes.string,
} )
),

View File

@ -28,4 +28,5 @@
@import 'summary/style.scss';
@import 'table/style.scss';
@import 'tag/style.scss';
@import 'text-control-with-affixes/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.
*/
id: PropTypes.number,
id: PropTypes.oneOfType( [
PropTypes.number,
PropTypes.string,
] ),
/**
* 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
- Add `getChartTypeForQuery` function to ensure chart type is always `bar` or `line`

View File

@ -3,5 +3,6 @@
"config:base"
],
"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( 13, $tax_report['order_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->assertArrayHasKey( 'order_tax', $totals );
$this->assertArrayHasKey( 'order_count', $totals );
$this->assertArrayHasKey( 'orders_count', $totals );
$this->assertArrayHasKey( 'shipping_tax', $totals );
$this->assertArrayHasKey( 'tax_codes', $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'];
$this->assertEquals( 5, count( $subtotals ) );
$this->assertArrayHasKey( 'order_tax', $totals );
$this->assertArrayHasKey( 'order_count', $totals );
$this->assertArrayHasKey( 'orders_count', $totals );
$this->assertArrayHasKey( 'shipping_tax', $totals );
$this->assertArrayHasKey( 'tax_codes', $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. */
}
/**
* 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.
*
@ -67,6 +80,15 @@ function dependencies_satisfied() {
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.
*/
@ -123,7 +145,7 @@ function wc_admin_init() {
}
// 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();
}
}
@ -144,16 +166,22 @@ function wc_admin_plugins_loaded() {
// Some common utilities.
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.
require_once dirname( __FILE__ ) . '/lib/client-assets.php';
// Create the Admin pages.
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' );