diff --git a/plugins/woocommerce-admin/client/analytics/report/categories/breadcrumbs.js b/plugins/woocommerce-admin/client/analytics/report/categories/breadcrumbs.js
new file mode 100644
index 00000000000..544434dc4a1
--- /dev/null
+++ b/plugins/woocommerce-admin/client/analytics/report/categories/breadcrumbs.js
@@ -0,0 +1,67 @@
+/** @format */
+/**
+ * External dependencies
+ */
+import { Component } from '@wordpress/element';
+import { first, last } from 'lodash';
+import { Spinner } from '@wordpress/components';
+
+/**
+ * WooCommerce dependencies
+ */
+import { Link } from '@woocommerce/components';
+
+export default class CategoryBreadcrumbs extends Component {
+ getCategoryAncestorIds( category, categories ) {
+ const ancestors = [];
+ let parent = category.parent;
+ while ( parent ) {
+ ancestors.unshift( parent );
+ parent = categories[ parent ].parent;
+ }
+ return ancestors;
+ }
+
+ getCategoryAncestors( category, categories ) {
+ const ancestorIds = this.getCategoryAncestorIds( category, categories );
+
+ if ( ! ancestorIds.length ) {
+ return;
+ }
+ if ( ancestorIds.length === 1 ) {
+ return categories[ first( ancestorIds ) ].name + ' › ';
+ }
+ if ( ancestorIds.length === 2 ) {
+ return (
+ categories[ first( ancestorIds ) ].name +
+ ' › ' +
+ categories[ last( ancestorIds ) ].name +
+ ' › '
+ );
+ }
+ return (
+ categories[ first( ancestorIds ) ].name +
+ ' … ' +
+ categories[ last( ancestorIds ) ].name +
+ ' › '
+ );
+ }
+
+ render() {
+ const { categories, category } = this.props;
+
+ return category ? (
+
+ { this.getCategoryAncestors( category, categories ) }
+
+ { category.name }
+
+
+ ) : (
+
+ );
+ }
+}
diff --git a/plugins/woocommerce-admin/client/analytics/report/categories/table.js b/plugins/woocommerce-admin/client/analytics/report/categories/table.js
index 8eb3632b8ce..30ab59dd82c 100644
--- a/plugins/woocommerce-admin/client/analytics/report/categories/table.js
+++ b/plugins/woocommerce-admin/client/analytics/report/categories/table.js
@@ -4,21 +4,23 @@
*/
import { __, _n } from '@wordpress/i18n';
import { Component } from '@wordpress/element';
+import { compose } from '@wordpress/compose';
import { map } from 'lodash';
/**
* WooCommerce dependencies
*/
-import { Link } from '@woocommerce/components';
import { formatCurrency, getCurrencyFormatDecimal } from '@woocommerce/currency';
/**
* Internal dependencies
*/
-import ReportTable from 'analytics/components/report-table';
+import CategoryBreacrumbs from './breadcrumbs';
import { numberFormat } from 'lib/number';
+import ReportTable from 'analytics/components/report-table';
+import withSelect from 'wc-api/with-select';
-export default class CategoriesReportTable extends Component {
+class CategoriesReportTable extends Component {
constructor( props ) {
super( props );
@@ -64,29 +66,16 @@ export default class CategoriesReportTable extends Component {
];
}
- getRowsContent( categories ) {
- return map( categories, category => {
- const {
- category_id,
- items_sold,
- net_revenue,
- products_count,
- orders_count,
- extended_info,
- } = category;
- const { name } = extended_info;
+ getRowsContent( categoryStats ) {
+ return map( categoryStats, categoryStat => {
+ const { category_id, items_sold, net_revenue, products_count, orders_count } = categoryStat;
+ const categories = this.props.categories;
+ const category = categories[ category_id ];
return [
{
- display: (
-
- { name }
-
- ),
- value: name,
+ display: ,
+ value: category && category.name,
},
{
display: numberFormat( items_sold ),
@@ -156,3 +145,15 @@ export default class CategoriesReportTable extends Component {
);
}
}
+
+export default compose(
+ withSelect( select => {
+ const { getCategories, getCategoriesError, isGetCategoriesRequesting } = select( 'wc-api' );
+
+ const categories = getCategories();
+ const isError = Boolean( getCategoriesError() );
+ const isRequesting = isGetCategoriesRequesting();
+
+ return { categories, isError, isRequesting };
+ } )
+)( CategoriesReportTable );
diff --git a/plugins/woocommerce-admin/client/analytics/report/products/style.scss b/plugins/woocommerce-admin/client/analytics/report/products/style.scss
new file mode 100644
index 00000000000..d2a80f105fa
--- /dev/null
+++ b/plugins/woocommerce-admin/client/analytics/report/products/style.scss
@@ -0,0 +1,16 @@
+/** @format */
+
+.woocommerce-table__product-categories {
+ > .woocommerce-table__breadcrumbs {
+ display: inline-block;
+ margin-right: $gap-small;
+ }
+ .components-popover__content {
+ padding: 0 $gap;
+ text-align: left;
+ }
+ .components-popover__content .woocommerce-table__breadcrumbs {
+ margin-top: $gap-small;
+ margin-bottom: $gap-small;
+ }
+}
diff --git a/plugins/woocommerce-admin/client/analytics/report/products/table.js b/plugins/woocommerce-admin/client/analytics/report/products/table.js
index 007875a96a3..9c9b4aa856e 100644
--- a/plugins/woocommerce-admin/client/analytics/report/products/table.js
+++ b/plugins/woocommerce-admin/client/analytics/report/products/table.js
@@ -2,25 +2,29 @@
/**
* External dependencies
*/
-import { __, _n, _x } from '@wordpress/i18n';
+import { __, _n, _x, sprintf } from '@wordpress/i18n';
import { Component } from '@wordpress/element';
+import { compose } from '@wordpress/compose';
import { map } from 'lodash';
/**
* WooCommerce dependencies
*/
-import { Link } from '@woocommerce/components';
import { formatCurrency, getCurrencyFormatDecimal } from '@woocommerce/currency';
import { getNewPath, getPersistedQuery } from '@woocommerce/navigation';
+import { Link, Tag } from '@woocommerce/components';
/**
* Internal dependencies
*/
-import ReportTable from 'analytics/components/report-table';
-import { numberFormat } from 'lib/number';
+import CategoryBreacrumbs from '../categories/breadcrumbs';
import { isLowStock } from './utils';
+import { numberFormat } from 'lib/number';
+import ReportTable from 'analytics/components/report-table';
+import withSelect from 'wc-api/with-select';
+import './style.scss';
-export default class ProductsReportTable extends Component {
+class ProductsReportTable extends Component {
constructor() {
super();
@@ -96,10 +100,9 @@ export default class ProductsReportTable extends Component {
items_sold,
net_revenue,
orders_count,
- categories = [], // @TODO
variations = [], // @TODO
} = row;
- const { name, stock_status, stock_quantity, low_stock_amount } = extended_info;
+ const { category_ids, low_stock_amount, name, stock_status, stock_quantity } = extended_info;
const ordersLink = getNewPath( persistedQuery, 'orders', {
filter: 'advanced',
product_includes: product_id,
@@ -108,6 +111,8 @@ export default class ProductsReportTable extends Component {
filter: 'single_product',
products: product_id,
} );
+ const categories = this.props.categories;
+ const productCategories = category_ids.map( category_id => categories[ category_id ] );
return [
{
@@ -139,10 +144,29 @@ export default class ProductsReportTable extends Component {
value: orders_count,
},
{
- display: Array.isArray( categories )
- ? categories.map( cat => cat.name ).join( ', ' )
- : '',
- value: Array.isArray( categories ) ? categories.map( cat => cat.name ).join( ', ' ) : '',
+ display: (
+
+ { productCategories[ 0 ] && (
+
+ ) }
+ { productCategories.length > 1 && (
+ (
+
+ ) ) }
+ />
+ ) }
+
+ ),
+ value: productCategories.map( category => category.name ).join( ', ' ),
},
{
display: numberFormat( variations.length ),
@@ -219,3 +243,15 @@ export default class ProductsReportTable extends Component {
);
}
}
+
+export default compose(
+ withSelect( select => {
+ const { getCategories, getCategoriesError, isGetCategoriesRequesting } = select( 'wc-api' );
+
+ const categories = getCategories();
+ const isError = Boolean( getCategoriesError() );
+ const isRequesting = isGetCategoriesRequesting();
+
+ return { categories, isError, isRequesting };
+ } )
+)( ProductsReportTable );
diff --git a/plugins/woocommerce-admin/client/wc-api/categories/index.js b/plugins/woocommerce-admin/client/wc-api/categories/index.js
new file mode 100644
index 00000000000..cd0cff2f807
--- /dev/null
+++ b/plugins/woocommerce-admin/client/wc-api/categories/index.js
@@ -0,0 +1,11 @@
+/** @format */
+/**
+ * Internal dependencies
+ */
+import operations from './operations';
+import selectors from './selectors';
+
+export default {
+ operations,
+ selectors,
+};
diff --git a/plugins/woocommerce-admin/client/wc-api/categories/operations.js b/plugins/woocommerce-admin/client/wc-api/categories/operations.js
new file mode 100644
index 00000000000..bcf891917ef
--- /dev/null
+++ b/plugins/woocommerce-admin/client/wc-api/categories/operations.js
@@ -0,0 +1,54 @@
+/** @format */
+
+/**
+ * External dependencies
+ */
+import apiFetch from '@wordpress/api-fetch';
+
+/**
+ * WooCommerce dependencies
+ */
+import { stringifyQuery } from '@woocommerce/navigation';
+
+/**
+ * Internal dependencies
+ */
+import { isResourcePrefix, getResourceIdentifier, getResourceName } from '../utils';
+
+function read( resourceNames, fetch = apiFetch ) {
+ const filteredNames = resourceNames.filter( name => isResourcePrefix( name, 'category-query' ) );
+
+ return filteredNames.map( async resourceName => {
+ const query = getResourceIdentifier( resourceName );
+ const url = `/wc/v3/products/categories${ stringifyQuery( query ) }`;
+
+ try {
+ const response = await fetch( {
+ parse: false,
+ path: url,
+ } );
+
+ const categories = await response.json();
+ const totalCount = parseInt( response.headers.get( 'x-wp-total' ) );
+ const ids = categories.map( category => category.id );
+ const categoryResources = categories.reduce( ( resources, category ) => {
+ resources[ getResourceName( 'category', category.id ) ] = { data: category };
+ return resources;
+ }, {} );
+
+ return {
+ [ resourceName ]: {
+ data: ids,
+ totalCount,
+ },
+ ...categoryResources,
+ };
+ } catch ( error ) {
+ return { [ resourceName ]: { error } };
+ }
+ } );
+}
+
+export default {
+ read,
+};
diff --git a/plugins/woocommerce-admin/client/wc-api/categories/selectors.js b/plugins/woocommerce-admin/client/wc-api/categories/selectors.js
new file mode 100644
index 00000000000..b5d0257d7b7
--- /dev/null
+++ b/plugins/woocommerce-admin/client/wc-api/categories/selectors.js
@@ -0,0 +1,56 @@
+/** @format */
+
+/**
+ * External dependencies
+ */
+import { isNil } from 'lodash';
+
+/**
+ * Internal dependencies
+ */
+import { getResourceName } from '../utils';
+import { DEFAULT_REQUIREMENT } from '../constants';
+
+const getCategories = ( getResource, requireResource ) => (
+ query = {},
+ requirement = DEFAULT_REQUIREMENT
+) => {
+ const resourceName = getResourceName( 'category-query', query );
+ const ids = requireResource( requirement, resourceName ).data || [];
+ const categories = ids.reduce(
+ ( acc, id ) => ( {
+ ...acc,
+ [ id ]: getResource( getResourceName( 'category', id ) ).data || {},
+ } ),
+ {}
+ );
+ return categories;
+};
+
+const getCategoriesTotalCount = getResource => ( query = {} ) => {
+ const resourceName = getResourceName( 'category-query', query );
+ return getResource( resourceName ).totalCount || 0;
+};
+
+const getCategoriesError = getResource => ( query = {} ) => {
+ const resourceName = getResourceName( 'category-query', query );
+ return getResource( resourceName ).error;
+};
+
+const isGetCategoriesRequesting = getResource => ( query = {} ) => {
+ const resourceName = getResourceName( 'category-query', query );
+ const { lastRequested, lastReceived } = getResource( resourceName );
+
+ if ( isNil( lastRequested ) || isNil( lastReceived ) ) {
+ return true;
+ }
+
+ return lastRequested > lastReceived;
+};
+
+export default {
+ getCategories,
+ getCategoriesError,
+ getCategoriesTotalCount,
+ isGetCategoriesRequesting,
+};
diff --git a/plugins/woocommerce-admin/client/wc-api/wc-api-spec.js b/plugins/woocommerce-admin/client/wc-api/wc-api-spec.js
index 6bc9b887a9c..b14f3a5ef07 100644
--- a/plugins/woocommerce-admin/client/wc-api/wc-api-spec.js
+++ b/plugins/woocommerce-admin/client/wc-api/wc-api-spec.js
@@ -3,6 +3,7 @@
/**
* Internal dependencies
*/
+import categories from './categories';
import customers from './customers';
import notes from './notes';
import orders from './orders';
@@ -17,6 +18,7 @@ function createWcApiSpec() {
...user.mutations,
},
selectors: {
+ ...categories.selectors,
...customers.selectors,
...notes.selectors,
...orders.selectors,
@@ -28,6 +30,7 @@ function createWcApiSpec() {
operations: {
read( resourceNames ) {
return [
+ ...categories.operations.read( resourceNames ),
...customers.operations.read( resourceNames ),
...notes.operations.read( resourceNames ),
...orders.operations.read( resourceNames ),
diff --git a/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php b/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php
index 95c2bd2d242..85823327952 100644
--- a/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php
+++ b/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php
@@ -29,6 +29,10 @@ class WC_Admin_Api_Init {
// Initialize Orders data store class's static vars.
add_action( 'woocommerce_after_register_post_type', array( 'WC_Admin_Api_Init', 'orders_data_store_init' ), 20 );
+ // Add taxonomy support for product categories.
+ add_filter( 'woocommerce_taxonomy_args_product_cat', array( 'WC_Admin_Api_Init', 'show_product_categories_in_rest' ) );
+ // Increase per_page limit in REST response.
+ add_filter( 'woocommerce_rest_product_cat_query', array( 'WC_Admin_Api_Init', 'increase_per_page_limit' ) );
}
/**
@@ -470,6 +474,28 @@ class WC_Admin_Api_Init {
add_action( 'woocommerce_after_register_post_type', array( 'WC_Admin_Api_Init', 'order_product_lookup_store_init' ), 20 );
}
+ /**
+ * Enables the WP REST API for product categories
+ *
+ * @param array $args Default arguments for product_cat taxonomy.
+ * @return array
+ */
+ public static function show_product_categories_in_rest( $args ) {
+ $args['show_in_rest'] = true;
+ return $args;
+ }
+
+ /**
+ * Increase per page limit for product categories
+ *
+ * @param array $prepared_args Prepared arguments for query.
+ * @return array
+ */
+ public static function increase_per_page_limit( $prepared_args ) {
+ $prepared_args['number'] = PHP_INT_MAX;
+ return $prepared_args;
+ }
+
}
new WC_Admin_Api_Init();
diff --git a/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-products-data-store.php b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-products-data-store.php
index 32ef3da268b..747073ef37f 100644
--- a/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-products-data-store.php
+++ b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-products-data-store.php
@@ -25,17 +25,21 @@ class WC_Admin_Reports_Products_Data_Store extends WC_Admin_Reports_Data_Store i
* @var array
*/
protected $column_types = array(
- 'date_start' => 'strval',
- 'date_end' => 'strval',
- 'product_id' => 'intval',
- 'items_sold' => 'intval',
- 'net_revenue' => 'floatval',
- 'orders_count' => 'intval',
+ 'date_start' => 'strval',
+ 'date_end' => 'strval',
+ 'product_id' => 'intval',
+ 'items_sold' => 'intval',
+ 'net_revenue' => 'floatval',
+ 'orders_count' => 'intval',
// Extended attributes.
- 'name' => 'strval',
- 'price' => 'floatval',
- 'image' => 'strval',
- 'permalink' => 'strval',
+ 'name' => 'strval',
+ 'price' => 'floatval',
+ 'image' => 'strval',
+ 'permalink' => 'strval',
+ 'stock_status' => 'strval',
+ 'stock_quantity' => 'intval',
+ 'low_stock_amount' => 'intval',
+ 'category_ids' => 'array_values',
);
/**
@@ -63,6 +67,7 @@ class WC_Admin_Reports_Products_Data_Store extends WC_Admin_Reports_Data_Store i
'stock_status',
'stock_quantity',
'low_stock_amount',
+ 'category_ids',
);
/**
diff --git a/plugins/woocommerce-admin/tests/reports/class-wc-tests-reports-products.php b/plugins/woocommerce-admin/tests/reports/class-wc-tests-reports-products.php
index d8036795063..d329f24593a 100644
--- a/plugins/woocommerce-admin/tests/reports/class-wc-tests-reports-products.php
+++ b/plugins/woocommerce-admin/tests/reports/class-wc-tests-reports-products.php
@@ -199,6 +199,9 @@ class WC_Tests_Reports_Products extends WC_Unit_Test_Case {
$product->set_low_stock_amount( 5 );
$product->save();
+ $term = wp_insert_term( 'Test Category', 'product_cat' );
+ wp_set_object_terms( $product->get_id(), $term['term_id'], 'product_cat' );
+
$order = WC_Helper_Order::create_order( 1, $product );
$order->set_status( 'completed' );
$order->set_shipping_total( 10 );
@@ -217,7 +220,9 @@ class WC_Tests_Reports_Products extends WC_Unit_Test_Case {
'extended_info' => 1,
);
// Test retrieving the stats through the data store.
- $data = $data_store->get_data( $args );
+ $data = $data_store->get_data( $args );
+ // Get updated product data.
+ $product = wc_get_product( $product->get_id() );
$expected_data = (object) array(
'total' => 1,
'pages' => 1,
@@ -234,8 +239,9 @@ class WC_Tests_Reports_Products extends WC_Unit_Test_Case {
'permalink' => $product->get_permalink(),
'price' => (float) $product->get_price(),
'stock_status' => $product->get_stock_status(),
- 'stock_quantity' => $product->get_stock_quantity() - 4, // subtract the ones purchased.
+ 'stock_quantity' => $product->get_stock_quantity(),
'low_stock_amount' => $product->get_low_stock_amount(),
+ 'category_ids' => array_values( $product->get_category_ids() ),
),
),
),