diff --git a/plugins/woocommerce-admin/client/header/activity-panel/index.js b/plugins/woocommerce-admin/client/header/activity-panel/index.js
index c606b31749e..5ccc5460156 100644
--- a/plugins/woocommerce-admin/client/header/activity-panel/index.js
+++ b/plugins/woocommerce-admin/client/header/activity-panel/index.js
@@ -15,8 +15,13 @@ import { partial, uniqueId, find } from 'lodash';
*/
import './style.scss';
import ActivityPanelToggleBubble from './toggle-bubble';
-import { DEFAULT_ACTIONABLE_STATUSES, DEFAULT_REVIEW_STATUSES } from 'wc-api/constants';
import { H, Section } from '@woocommerce/components';
+import {
+ getUnreadNotes,
+ getUnreadOrders,
+ getUnreadReviews,
+ getUnreadStock,
+} from './unread-indicators';
import InboxPanel from './panels/inbox';
import OrdersPanel from './panels/orders';
import StockPanel from './panels/stock';
@@ -93,26 +98,26 @@ class ActivityPanel extends Component {
// @todo Pull in dynamic unread status/count
getTabs() {
- const { unreadNotes, unreadOrders, unreadReviews } = this.props;
+ const { hasUnreadNotes, hasUnreadOrders, hasUnreadReviews, hasUnreadStock } = this.props;
return [
{
name: 'inbox',
title: __( 'Inbox', 'woocommerce-admin' ),
icon: ,
- unread: unreadNotes,
+ unread: hasUnreadNotes,
},
{
name: 'orders',
title: __( 'Orders', 'woocommerce-admin' ),
icon: ,
- unread: unreadOrders,
+ unread: hasUnreadOrders,
},
'yes' === wcSettings.manageStock
? {
name: 'stock',
title: __( 'Stock', 'woocommerce-admin' ),
icon: ,
- unread: false,
+ unread: hasUnreadStock,
}
: null,
'yes' === wcSettings.reviewsEnabled
@@ -120,7 +125,7 @@ class ActivityPanel extends Component {
name: 'reviews',
title: __( 'Reviews', 'woocommerce-admin' ),
icon: ,
- unread: unreadReviews,
+ unread: hasUnreadReviews,
}
: null,
].filter( Boolean );
@@ -131,8 +136,8 @@ class ActivityPanel extends Component {
case 'inbox':
return ;
case 'orders':
- const { unreadOrders } = this.props;
- return ;
+ const { hasUnreadOrders } = this.props;
+ return ;
case 'stock':
return ;
case 'reviews':
@@ -264,101 +269,10 @@ class ActivityPanel extends Component {
}
export default withSelect( select => {
- const {
- getCurrentUserData,
- getNotes,
- getNotesError,
- getReportItems,
- getReportItemsError,
- isGetNotesRequesting,
- isReportItemsRequesting,
- getReviews,
- getReviewsTotalCount,
- getReviewsError,
- isGetReviewsRequesting,
- } = select( 'wc-api' );
- const userData = getCurrentUserData();
- const orderStatuses =
- wcSettings.wcAdminSettings.woocommerce_actionable_order_statuses || DEFAULT_ACTIONABLE_STATUSES;
+ const hasUnreadNotes = getUnreadNotes( select );
+ const hasUnreadOrders = getUnreadOrders( select );
+ const hasUnreadStock = getUnreadStock( select );
+ const { numberOfReviews, hasUnreadReviews } = getUnreadReviews( select );
- const notesQuery = {
- page: 1,
- per_page: 1,
- };
-
- const latestNote = getNotes( notesQuery );
- const unreadNotes =
- ! Boolean( getNotesError( notesQuery ) ) &&
- ! isGetNotesRequesting( notesQuery ) &&
- latestNote[ 0 ] &&
- new Date( latestNote[ 0 ].date_created_gmt ).getTime() >
- userData.activity_panel_inbox_last_read;
-
- let unreadOrders = null;
- if ( ! orderStatuses.length ) {
- unreadOrders = false;
- } else {
- const ordersQuery = {
- page: 1,
- per_page: 0,
- status_is: orderStatuses,
- };
-
- const totalOrders = getReportItems( 'orders', ordersQuery ).totalResults;
- const isOrdersError = Boolean( getReportItemsError( 'orders', ordersQuery ) );
- const isOrdersRequesting = isReportItemsRequesting( 'orders', ordersQuery );
-
- if ( ! isOrdersError && ! isOrdersRequesting ) {
- if ( totalOrders > 0 ) {
- unreadOrders = true;
- } else {
- unreadOrders = false;
- }
- }
- }
-
- let numberOfReviews = null;
- let unreadReviews = false;
- if ( 'yes' === wcSettings.reviewsEnabled ) {
- const reviewsQuery = {
- order: 'desc',
- orderby: 'date_gmt',
- page: 1,
- per_page: 1,
- status: DEFAULT_REVIEW_STATUSES,
- };
- const reviews = getReviews( reviewsQuery );
- const totalReviews = getReviewsTotalCount( reviewsQuery );
- const isReviewsError = Boolean( getReviewsError( reviewsQuery ) );
- const isReviewsRequesting = isGetReviewsRequesting( reviewsQuery );
-
- if ( ! isReviewsError && ! isReviewsRequesting ) {
- numberOfReviews = totalReviews;
- unreadReviews = Boolean(
- reviews.length &&
- reviews[ 0 ].date_created_gmt &&
- new Date( reviews[ 0 ].date_created_gmt + 'Z' ).getTime() >
- userData.activity_panel_reviews_last_read
- );
- }
-
- if ( ! unreadReviews && '1' === wcSettings.commentModeration ) {
- const actionableReviewsQuery = {
- page: 1,
- // @todo we are not using this review, so when the endpoint supports it,
- // it could be replaced with `per_page: 0`
- per_page: 1,
- status: 'hold',
- };
- const totalActionableReviews = getReviewsTotalCount( actionableReviewsQuery );
- const isActionableReviewsError = Boolean( getReviewsError( actionableReviewsQuery ) );
- const isActionableReviewsRequesting = isGetReviewsRequesting( actionableReviewsQuery );
-
- if ( ! isActionableReviewsError && ! isActionableReviewsRequesting ) {
- unreadReviews = totalActionableReviews > 0;
- }
- }
- }
-
- return { unreadNotes, unreadOrders, unreadReviews, numberOfReviews };
+ return { hasUnreadNotes, hasUnreadOrders, hasUnreadReviews, hasUnreadStock, numberOfReviews };
} )( clickOutside( ActivityPanel ) );
diff --git a/plugins/woocommerce-admin/client/header/activity-panel/unread-indicators.js b/plugins/woocommerce-admin/client/header/activity-panel/unread-indicators.js
new file mode 100644
index 00000000000..c90c33f643b
--- /dev/null
+++ b/plugins/woocommerce-admin/client/header/activity-panel/unread-indicators.js
@@ -0,0 +1,105 @@
+/** @format */
+
+/**
+ * Internal dependencies
+ */
+import { DEFAULT_ACTIONABLE_STATUSES, DEFAULT_REVIEW_STATUSES } from 'wc-api/constants';
+
+export function getUnreadNotes( select ) {
+ const { getCurrentUserData, getNotes, getNotesError, isGetNotesRequesting } = select( 'wc-api' );
+ const userData = getCurrentUserData();
+ const notesQuery = {
+ page: 1,
+ per_page: 1,
+ };
+
+ const latestNote = getNotes( notesQuery );
+ return (
+ ! Boolean( getNotesError( notesQuery ) ) &&
+ ! isGetNotesRequesting( notesQuery ) &&
+ latestNote[ 0 ] &&
+ new Date( latestNote[ 0 ].date_created_gmt ).getTime() > userData.activity_panel_inbox_last_read
+ );
+}
+
+export function getUnreadOrders( select ) {
+ const { getReportItems, getReportItemsError, isReportItemsRequesting } = select( 'wc-api' );
+ const orderStatuses = wcSettings.wcAdminSettings.woocommerce_actionable_order_statuses || DEFAULT_ACTIONABLE_STATUSES;
+
+ if ( ! orderStatuses.length ) {
+ return false;
+ }
+
+ const ordersQuery = {
+ page: 1,
+ per_page: 0,
+ status_is: orderStatuses,
+ };
+
+ const totalOrders = getReportItems( 'orders', ordersQuery ).totalResults;
+ const isError = Boolean( getReportItemsError( 'orders', ordersQuery ) );
+ const isRequesting = isReportItemsRequesting( 'orders', ordersQuery );
+
+ if ( ! isError && ! isRequesting ) {
+ if ( totalOrders > 0 ) {
+ return true;
+ }
+ return false;
+ }
+ return null;
+}
+
+export function getUnreadReviews( select ) {
+ const {
+ getCurrentUserData,
+ getReviews,
+ getReviewsTotalCount,
+ getReviewsError,
+ isGetReviewsRequesting,
+ } = select( 'wc-api' );
+ const userData = getCurrentUserData();
+ let numberOfReviews = null;
+ let hasUnreadReviews = false;
+
+ if ( 'yes' === wcSettings.reviewsEnabled ) {
+ const reviewsQuery = {
+ order: 'desc',
+ orderby: 'date_gmt',
+ page: 1,
+ per_page: 1,
+ status: DEFAULT_REVIEW_STATUSES,
+ };
+ const reviews = getReviews( reviewsQuery );
+ const totalReviews = getReviewsTotalCount( reviewsQuery );
+ const isReviewsError = Boolean( getReviewsError( reviewsQuery ) );
+ const isReviewsRequesting = isGetReviewsRequesting( reviewsQuery );
+
+ if ( ! isReviewsError && ! isReviewsRequesting ) {
+ numberOfReviews = totalReviews;
+ hasUnreadReviews =
+ reviews.length &&
+ reviews[ 0 ].date_created_gmt &&
+ new Date( reviews[ 0 ].date_created_gmt + 'Z' ).getTime() >
+ userData.activity_panel_reviews_last_read;
+ }
+ }
+
+ return { numberOfReviews, hasUnreadReviews };
+}
+
+export function getUnreadStock( select ) {
+ const { getItems, getItemsError, getItemsTotalCount, isGetItemsRequesting } = select( 'wc-api' );
+ const productsQuery = {
+ page: 1,
+ per_page: 1,
+ low_in_stock: true,
+ status: 'publish',
+ };
+ getItems( 'products', productsQuery );
+ const lowInStockCount = getItemsTotalCount( 'products', productsQuery );
+
+ return ! getItemsError( 'products', productsQuery ) &&
+ ! isGetItemsRequesting( 'products', productsQuery )
+ ? lowInStockCount > 0
+ : false;
+}
diff --git a/plugins/woocommerce-admin/client/wc-api/items/operations.js b/plugins/woocommerce-admin/client/wc-api/items/operations.js
index b6589727c7b..862c5c73e82 100644
--- a/plugins/woocommerce-admin/client/wc-api/items/operations.js
+++ b/plugins/woocommerce-admin/client/wc-api/items/operations.js
@@ -38,11 +38,14 @@ function read( resourceNames, fetch = apiFetch ) {
const url = NAMESPACE + `/${ endpoint }${ stringifyQuery( query ) }`;
try {
- const items = await fetch( {
+ const response = await fetch( {
+ parse: false,
path: url,
} );
+ const items = await response.json();
const ids = items.map( item => item.id );
+ const totalCount = parseInt( response.headers.get( 'x-wp-total' ) );
const itemResources = items.reduce( ( resources, item ) => {
resources[ getResourceName( `${ prefix }-item`, item.id ) ] = { data: item };
return resources;
@@ -51,7 +54,7 @@ function read( resourceNames, fetch = apiFetch ) {
return {
[ resourceName ]: {
data: ids,
- totalCount: ids.length,
+ totalCount,
},
...itemResources,
};
diff --git a/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-products-controller.php b/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-products-controller.php
index 12bcd9c002e..5c6da3a201d 100644
--- a/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-products-controller.php
+++ b/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-products-controller.php
@@ -55,8 +55,14 @@ class WC_Admin_REST_Products_Controller extends WC_REST_Products_Controller {
* @return array
*/
public function get_collection_params() {
- $params = parent::get_collection_params();
- $params['search'] = array(
+ $params = parent::get_collection_params();
+ $params['low_in_stock'] = array(
+ 'description' => __( 'Limit result set to products that are low or out of stock.', 'woocommerce-admin' ),
+ 'type' => 'boolean',
+ 'default' => false,
+ 'sanitize_callback' => 'wc_string_to_bool',
+ );
+ $params['search'] = array(
'description' => __( 'Search by similar product name or sku.', 'woocommerce-admin' ),
'type' => 'string',
'validate_callback' => 'rest_validate_request_arg',
@@ -75,9 +81,12 @@ class WC_Admin_REST_Products_Controller extends WC_REST_Products_Controller {
$args = parent::prepare_objects_query( $request );
if ( ! empty( $request['search'] ) ) {
- $args['search'] = $request['search'];
+ $args['search'] = trim( $request['search'] );
unset( $args['s'] );
}
+ if ( ! empty( $request['low_in_stock'] ) ) {
+ $args['low_in_stock'] = $request['low_in_stock'];
+ }
return $args;
}
@@ -89,50 +98,71 @@ class WC_Admin_REST_Products_Controller extends WC_REST_Products_Controller {
* @return WP_Error|WP_REST_Response
*/
public function get_items( $request ) {
- add_filter( 'posts_where', array( __CLASS__, 'add_wp_query_product_search_filter' ), 10, 2 );
- add_filter( 'posts_join', array( __CLASS__, 'add_wp_query_product_search_join' ), 10, 2 );
- add_filter( 'posts_groupby', array( __CLASS__, 'add_wp_query_product_search_group_by' ), 10, 2 );
+ add_filter( 'posts_where', array( __CLASS__, 'add_wp_query_filter' ), 10, 2 );
+ add_filter( 'posts_join', array( __CLASS__, 'add_wp_query_join' ), 10, 2 );
+ add_filter( 'posts_groupby', array( __CLASS__, 'add_wp_query_group_by' ), 10, 2 );
$response = parent::get_items( $request );
- remove_filter( 'posts_where', array( __CLASS__, 'add_wp_query_product_search_filter' ), 10 );
- remove_filter( 'posts_join', array( __CLASS__, 'add_wp_query_product_search_join' ), 10 );
- remove_filter( 'posts_groupby', array( __CLASS__, 'add_wp_query_product_search_group_by' ), 10 );
+ remove_filter( 'posts_where', array( __CLASS__, 'add_wp_query_filter' ), 10 );
+ remove_filter( 'posts_join', array( __CLASS__, 'add_wp_query_join' ), 10 );
+ remove_filter( 'posts_groupby', array( __CLASS__, 'add_wp_query_group_by' ), 10 );
return $response;
}
/**
- * Allow searching by product name or sku in WP Query
+ * Add in conditional search filters for products.
*
* @param string $where Where clause used to search posts.
* @param object $wp_query WP_Query object.
* @return string
*/
- public static function add_wp_query_product_search_filter( $where, $wp_query ) {
+ public static function add_wp_query_filter( $where, $wp_query ) {
global $wpdb;
- $search = trim( $wp_query->get( 'search' ) );
+ $search = $wp_query->get( 'search' );
if ( $search ) {
$search = $wpdb->esc_like( $search );
$search = "'%" . $search . "%'";
- $where .= ' AND (' . $wpdb->posts . '.post_title LIKE ' . $search;
+ $where .= " AND ({$wpdb->posts}.post_title LIKE {$search}";
$where .= wc_product_sku_enabled() ? ' OR ps_post_meta.meta_key = "_sku" AND ps_post_meta.meta_value LIKE ' . $search . ')' : ')';
}
+ if ( $wp_query->get( 'low_in_stock' ) ) {
+ $low_stock_amount = absint( max( get_option( 'woocommerce_notify_low_stock_amount' ), 1 ) );
+ $where .= " AND lis_postmeta2.meta_key = '_manage_stock'
+ AND lis_postmeta2.meta_value = 'yes'
+ AND lis_postmeta.meta_key = '_stock'
+ AND lis_postmeta.meta_value IS NOT NULL
+ AND lis_postmeta3.meta_key = '_low_stock_amount'
+ AND (
+ lis_postmeta3.meta_value > ''
+ AND CAST(lis_postmeta.meta_value AS SIGNED) <= CAST(lis_postmeta3.meta_value AS SIGNED)
+ OR lis_postmeta3.meta_value <= ''
+ AND CAST(lis_postmeta.meta_value AS SIGNED) <= {$low_stock_amount}
+ )";
+ }
+
return $where;
}
/**
- * Join posts meta table when product search is present and meta query is not present.
+ * Join posts meta tables when product search or low stock query is present.
*
* @param string $join Join clause used to search posts.
* @param object $wp_query WP_Query object.
* @return string
*/
- public static function add_wp_query_product_search_join( $join, $wp_query ) {
+ public static function add_wp_query_join( $join, $wp_query ) {
global $wpdb;
- $search = trim( $wp_query->get( 'search' ) );
+ $search = $wp_query->get( 'search' );
if ( $search && wc_product_sku_enabled() ) {
- $join .= ' INNER JOIN ' . $wpdb->postmeta . ' AS ps_post_meta ON ps_post_meta.post_id = ' . $wpdb->posts . '.ID';
+ $join .= " INNER JOIN {$wpdb->postmeta} AS ps_post_meta ON ps_post_meta.post_id = {$wpdb->posts}.ID";
+ }
+
+ if ( $wp_query->get( 'low_in_stock' ) ) {
+ $join .= " INNER JOIN {$wpdb->postmeta} AS lis_postmeta ON {$wpdb->posts}.ID = lis_postmeta.post_id
+ INNER JOIN {$wpdb->postmeta} AS lis_postmeta2 ON {$wpdb->posts}.ID = lis_postmeta2.post_id
+ INNER JOIN {$wpdb->postmeta} AS lis_postmeta3 ON {$wpdb->posts}.ID = lis_postmeta3.post_id";
}
return $join;
@@ -145,14 +175,14 @@ class WC_Admin_REST_Products_Controller extends WC_REST_Products_Controller {
* @param object $wp_query WP_Query object.
* @return string
*/
- public function add_wp_query_product_search_group_by( $groupby, $wp_query ) {
+ public static function add_wp_query_group_by( $groupby, $wp_query ) {
global $wpdb;
- $search = trim( $wp_query->get( 'search' ) );
- if ( $search && empty( $groupby ) ) {
+ $search = $wp_query->get( 'search' );
+ $low_in_stock = $wp_query->get( 'low_in_stock' );
+ if ( empty( $groupby ) && ( $search || $low_in_stock ) ) {
$groupby = $wpdb->posts . '.ID';
}
return $groupby;
}
-
}