Add product categories wc-api and breadcrumbs (https://github.com/woocommerce/woocommerce-admin/pull/1131)

* Add support for product cats in wp rest api

* Add categories to WC API

* Add category breadcrumbs component

* Increase per_page limit size for product cat API

* Use wc/v3 API to pull product categories

* Return category IDs in API

* Add categories to product reports

* Add category IDs test for REST API

* Switch to getResource instead of require for total count selector
This commit is contained in:
Joshua T Flowers 2018-12-21 10:44:27 +08:00 committed by GitHub
parent 47b674ace2
commit f83b0e0615
11 changed files with 327 additions and 46 deletions

View File

@ -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 ? (
<div className="woocommerce-table__breadcrumbs">
{ this.getCategoryAncestors( category, categories ) }
<Link
href={ 'term.php?taxonomy=product_cat&post_type=product&tag_ID=' + category.id }
type="wp-admin"
>
{ category.name }
</Link>
</div>
) : (
<Spinner />
);
}
}

View File

@ -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: (
<Link
href={ 'term.php?taxonomy=product_cat&post_type=product&tag_ID=' + category_id }
type="wp-admin"
>
{ name }
</Link>
),
value: name,
display: <CategoryBreacrumbs category={ category } categories={ categories } />,
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 );

View File

@ -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;
}
}

View File

@ -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: (
<div className="woocommerce-table__product-categories">
{ productCategories[ 0 ] && (
<CategoryBreacrumbs category={ productCategories[ 0 ] } categories={ categories } />
) }
{ productCategories.length > 1 && (
<Tag
label={ sprintf(
_x( '+%d more', 'categories', 'wc-admin' ),
productCategories.length - 1
) }
popoverContents={ productCategories.map( category => (
<CategoryBreacrumbs
category={ category }
categories={ categories }
key={ category.id }
/>
) ) }
/>
) }
</div>
),
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 );

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,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,
};

View File

@ -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,
};

View File

@ -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 ),

View File

@ -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();

View File

@ -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',
);
/**

View File

@ -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() ),
),
),
),