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:
parent
47b674ace2
commit
f83b0e0615
|
@ -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 />
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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 );
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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 );
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
/** @format */
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import operations from './operations';
|
||||
import selectors from './selectors';
|
||||
|
||||
export default {
|
||||
operations,
|
||||
selectors,
|
||||
};
|
|
@ -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,
|
||||
};
|
|
@ -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,
|
||||
};
|
|
@ -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 ),
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -36,6 +36,10 @@ class WC_Admin_Reports_Products_Data_Store extends WC_Admin_Reports_Data_Store i
|
|||
'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',
|
||||
);
|
||||
|
||||
/**
|
||||
|
|
|
@ -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 );
|
||||
|
@ -218,6 +221,8 @@ class WC_Tests_Reports_Products extends WC_Unit_Test_Case {
|
|||
);
|
||||
// Test retrieving the stats through the data store.
|
||||
$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() ),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
Loading…
Reference in New Issue