Add low product stock indicator (https://github.com/woocommerce/woocommerce-admin/pull/1866)
* Move product low stock count to controller param instead of data store * Add totalCount to items API * Extract unread indicators to separate file and add unread stock * Trim initial search variable in wp_query * Parse low stock option as an absolute integer * Fix low stock meta comparison with product specific low stock amount * Make group by query function static * Check for empty strings and null values for product low_stock_amount
This commit is contained in:
parent
20301d57d3
commit
6ff9d0673a
|
@ -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: <Gridicon icon="mail" />,
|
||||
unread: unreadNotes,
|
||||
unread: hasUnreadNotes,
|
||||
},
|
||||
{
|
||||
name: 'orders',
|
||||
title: __( 'Orders', 'woocommerce-admin' ),
|
||||
icon: <Gridicon icon="pages" />,
|
||||
unread: unreadOrders,
|
||||
unread: hasUnreadOrders,
|
||||
},
|
||||
'yes' === wcSettings.manageStock
|
||||
? {
|
||||
name: 'stock',
|
||||
title: __( 'Stock', 'woocommerce-admin' ),
|
||||
icon: <Gridicon icon="clipboard" />,
|
||||
unread: false,
|
||||
unread: hasUnreadStock,
|
||||
}
|
||||
: null,
|
||||
'yes' === wcSettings.reviewsEnabled
|
||||
|
@ -120,7 +125,7 @@ class ActivityPanel extends Component {
|
|||
name: 'reviews',
|
||||
title: __( 'Reviews', 'woocommerce-admin' ),
|
||||
icon: <Gridicon icon="star" />,
|
||||
unread: unreadReviews,
|
||||
unread: hasUnreadReviews,
|
||||
}
|
||||
: null,
|
||||
].filter( Boolean );
|
||||
|
@ -131,8 +136,8 @@ class ActivityPanel extends Component {
|
|||
case 'inbox':
|
||||
return <InboxPanel />;
|
||||
case 'orders':
|
||||
const { unreadOrders } = this.props;
|
||||
return <OrdersPanel isEmpty={ unreadOrders === false } />;
|
||||
const { hasUnreadOrders } = this.props;
|
||||
return <OrdersPanel isEmpty={ hasUnreadOrders === false } />;
|
||||
case 'stock':
|
||||
return <StockPanel />;
|
||||
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 ) );
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue