* 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:
Joshua T Flowers 2019-03-29 10:45:19 +08:00 committed by GitHub
parent 20301d57d3
commit 6ff9d0673a
4 changed files with 179 additions and 127 deletions

View File

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

View File

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

View File

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

View File

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