diff --git a/assets/js/blocks/product-new/block.js b/assets/js/blocks/product-new/block.js index 6cb4b7dc8f9..9ad6dd78142 100644 --- a/assets/js/blocks/product-new/block.js +++ b/assets/js/blocks/product-new/block.js @@ -10,6 +10,7 @@ import PropTypes from 'prop-types'; import GridContentControl from '@woocommerce/editor-components/grid-content-control'; import GridLayoutControl from '@woocommerce/editor-components/grid-layout-control'; import ProductCategoryControl from '@woocommerce/editor-components/product-category-control'; +import ProductStockControl from '@woocommerce/editor-components/product-stock-control'; import { gridBlockPreview } from '@woocommerce/resource-previews'; import { getSetting } from '@woocommerce/settings'; @@ -26,6 +27,7 @@ class ProductNewestBlock extends Component { contentVisibility, rows, alignButtons, + stockStatus, } = attributes; return ( @@ -56,6 +58,20 @@ class ProductNewestBlock extends Component { } /> + { getSetting( 'hide_out_of_stock' ) !== 'yes' && ( + + + + ) } + { getSetting( 'hide_out_of_stock' ) !== 'yes' && ( + + + + ) } ); } diff --git a/assets/js/blocks/product-tag/block.js b/assets/js/blocks/product-tag/block.js index 5d82c4879d9..44ee90c9d40 100644 --- a/assets/js/blocks/product-tag/block.js +++ b/assets/js/blocks/product-tag/block.js @@ -18,6 +18,7 @@ import GridContentControl from '@woocommerce/editor-components/grid-content-cont import GridLayoutControl from '@woocommerce/editor-components/grid-layout-control'; import ProductTagControl from '@woocommerce/editor-components/product-tag-control'; import ProductOrderbyControl from '@woocommerce/editor-components/product-orderby-control'; +import ProductStockControl from '@woocommerce/editor-components/product-stock-control'; import { Icon, more } from '@woocommerce/icons'; import { gridBlockPreview } from '@woocommerce/resource-previews'; import { getSetting } from '@woocommerce/settings'; @@ -91,6 +92,7 @@ class ProductsByTagBlock extends Component { orderby, rows, alignButtons, + stockStatus, } = attributes; return ( @@ -150,6 +152,20 @@ class ProductsByTagBlock extends Component { value={ orderby } /> + { getSetting( 'hide_out_of_stock' ) !== 'yes' && ( + + + + ) } ); } diff --git a/assets/js/blocks/product-tag/index.js b/assets/js/blocks/product-tag/index.js index 87056fbc579..ba74f098bf2 100644 --- a/assets/js/blocks/product-tag/index.js +++ b/assets/js/blocks/product-tag/index.js @@ -109,6 +109,14 @@ registerBlockType( 'woocommerce/product-tag', { type: 'boolean', default: false, }, + + /** + * Whether to display in stock, out of stock or backorder products. + */ + stockStatus: { + type: 'string', + default: 'any', + }, }, /** diff --git a/assets/js/blocks/product-top-rated/block.js b/assets/js/blocks/product-top-rated/block.js index 795ea3e384f..fedd9775380 100644 --- a/assets/js/blocks/product-top-rated/block.js +++ b/assets/js/blocks/product-top-rated/block.js @@ -10,6 +10,7 @@ import PropTypes from 'prop-types'; import GridContentControl from '@woocommerce/editor-components/grid-content-control'; import GridLayoutControl from '@woocommerce/editor-components/grid-layout-control'; import ProductCategoryControl from '@woocommerce/editor-components/product-category-control'; +import ProductStockControl from '@woocommerce/editor-components/product-stock-control'; import { gridBlockPreview } from '@woocommerce/resource-previews'; import { getSetting } from '@woocommerce/settings'; @@ -26,6 +27,7 @@ class ProductTopRatedBlock extends Component { contentVisibility, rows, alignButtons, + stockStatus, } = attributes; return ( @@ -75,6 +77,20 @@ class ProductTopRatedBlock extends Component { } /> + { getSetting( 'hide_out_of_stock' ) !== 'yes' && ( + + + + ) } ); } diff --git a/assets/js/blocks/products-by-attribute/block.js b/assets/js/blocks/products-by-attribute/block.js index 62a7b472e4e..a028c58a841 100644 --- a/assets/js/blocks/products-by-attribute/block.js +++ b/assets/js/blocks/products-by-attribute/block.js @@ -19,6 +19,7 @@ import GridContentControl from '@woocommerce/editor-components/grid-content-cont import GridLayoutControl from '@woocommerce/editor-components/grid-layout-control'; import ProductAttributeTermControl from '@woocommerce/editor-components/product-attribute-term-control'; import ProductOrderbyControl from '@woocommerce/editor-components/product-orderby-control'; +import ProductStockControl from '@woocommerce/editor-components/product-stock-control'; import { gridBlockPreview } from '@woocommerce/resource-previews'; import { getSetting } from '@woocommerce/settings'; @@ -36,6 +37,7 @@ class ProductsByAttributeBlock extends Component { orderby, rows, alignButtons, + stockStatus, } = this.props.attributes; return ( @@ -100,6 +102,20 @@ class ProductsByAttributeBlock extends Component { value={ orderby } /> + { getSetting( 'hide_out_of_stock' ) !== 'yes' && ( + + + + ) } ); } diff --git a/assets/js/blocks/products-by-attribute/index.js b/assets/js/blocks/products-by-attribute/index.js index 6f9b7c798aa..6edf6c716ff 100644 --- a/assets/js/blocks/products-by-attribute/index.js +++ b/assets/js/blocks/products-by-attribute/index.js @@ -116,6 +116,14 @@ registerBlockType( blockTypeName, { type: 'boolean', default: false, }, + + /** + * Whether to display in stock, out of stock or backorder products. + */ + stockStatus: { + type: 'string', + default: 'any', + }, }, /** diff --git a/assets/js/blocks/products/attributes.js b/assets/js/blocks/products/attributes.js index c83ea501d70..37bfb5a86d8 100644 --- a/assets/js/blocks/products/attributes.js +++ b/assets/js/blocks/products/attributes.js @@ -18,6 +18,7 @@ export const defaults = { orderby: 'date', layoutConfig: DEFAULT_PRODUCT_LIST_LAYOUT, isPreview: false, + stockStatus: 'any', }; export const attributes = { @@ -64,4 +65,12 @@ export const attributes = { type: 'boolean', default: false, }, + + /** + * Whether to display in stock, out of stock or backorder products. + */ + stockStatus: { + type: 'string', + default: 'any', + }, }; diff --git a/assets/js/blocks/products/edit.js b/assets/js/blocks/products/edit.js index 1d7d57dd493..9b581248765 100644 --- a/assets/js/blocks/products/edit.js +++ b/assets/js/blocks/products/edit.js @@ -3,6 +3,7 @@ */ import { __ } from '@wordpress/i18n'; import { ToggleControl, SelectControl } from '@wordpress/components'; +import { getSetting } from '@woocommerce/settings'; export const getSharedContentControls = ( attributes, setAttributes ) => { const { contentVisibility } = attributes; @@ -27,48 +28,98 @@ export const getSharedContentControls = ( attributes, setAttributes ) => { export const getSharedListControls = ( attributes, setAttributes ) => { return ( - + setAttributes( { orderby } ) } + /> + { getSetting( 'hide_out_of_stock' ) !== 'yes' && ( + setAttributes( { orderby } ) } - /> + ) } + value={ attributes.stockStatus } + options={ [ + { + label: __( + 'All stock levels', + 'woo-gutenberg-products-block' + ), + value: 'any', + }, + { + label: __( + 'In stock', + 'woo-gutenberg-products-block' + ), + value: 'instock', + }, + { + label: __( + 'Out of stock', + 'woo-gutenberg-products-block' + ), + value: 'outofstock', + }, + { + label: __( + 'On backorder', + 'woo-gutenberg-products-block' + ), + value: 'onbackorder', + }, + ] } + onChange={ ( stockStatus ) => + setAttributes( { stockStatus } ) + } + /> + ) } + ); }; diff --git a/assets/js/editor-components/product-stock-control/index.js b/assets/js/editor-components/product-stock-control/index.js new file mode 100644 index 00000000000..73e9db008aa --- /dev/null +++ b/assets/js/editor-components/product-stock-control/index.js @@ -0,0 +1,60 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import { SelectControl } from '@wordpress/components'; +import PropTypes from 'prop-types'; + +/** + * A pre-configured SelectControl for product stock settings. + * + * @param {Object} props Incoming props for the component. + * @param {string} props.value + * @param {function(any):any} props.setAttributes Setter for block attributes. + */ +const ProductStockControl = ( { value, setAttributes } ) => { + return ( + setAttributes( { stockStatus } ) } + /> + ); +}; + +ProductStockControl.propTypes = { + /** + * Callback to update the stock status setting. + */ + setAttributes: PropTypes.func.isRequired, + /** + * The selected stock status setting. + */ + value: PropTypes.string.isRequired, +}; + +export default ProductStockControl; diff --git a/assets/js/utils/shared-attributes.js b/assets/js/utils/shared-attributes.js index 5533b7022ea..3597f015679 100644 --- a/assets/js/utils/shared-attributes.js +++ b/assets/js/utils/shared-attributes.js @@ -72,4 +72,12 @@ export default { type: 'boolean', default: false, }, + + /** + * Whether to display in stock, out of stock or backorder products. + */ + stockStatus: { + type: 'string', + default: 'any', + }, }; diff --git a/src/BlockTypes/AbstractProductGrid.php b/src/BlockTypes/AbstractProductGrid.php index aa441c94f5b..f9263aa195e 100644 --- a/src/BlockTypes/AbstractProductGrid.php +++ b/src/BlockTypes/AbstractProductGrid.php @@ -31,6 +31,13 @@ abstract class AbstractProductGrid extends AbstractDynamicBlock { */ protected $query_args = array(); + /** + * Meta query args. + * + * @var array + */ + protected $meta_query = array(); + /** * Get a set of attributes shared across most of the grid blocks. * @@ -50,6 +57,7 @@ protected function get_block_type_attributes() { 'align' => $this->get_schema_align(), 'alignButtons' => $this->get_schema_boolean( false ), 'isPreview' => $this->get_schema_boolean( false ), + 'stockStatus' => $this->get_schema_string( 'any' ), ); } @@ -161,6 +169,7 @@ protected function parse_attributes( $attributes ) { 'rating' => true, 'button' => true, ), + 'stockStatus' => 'any', ); return wp_parse_args( $attributes, $defaults ); @@ -172,6 +181,9 @@ protected function parse_attributes( $attributes ) { * @return array */ protected function parse_query_args() { + // Store the original meta query. + $this->meta_query = WC()->query->get_meta_query(); // phpcs:ignore WordPress.DB.SlowDBQuery + $query_args = array( 'post_type' => 'product', 'post_status' => 'publish', @@ -180,7 +192,7 @@ protected function parse_query_args() { 'no_found_rows' => false, 'orderby' => '', 'order' => '', - 'meta_query' => WC()->query->get_meta_query(), // phpcs:ignore WordPress.DB.SlowDBQuery + 'meta_query' => $this->meta_query, // phpcs:ignore WordPress.DB.SlowDBQuery 'tax_query' => array(), // phpcs:ignore WordPress.DB.SlowDBQuery 'posts_per_page' => $this->get_products_limit(), ); @@ -189,6 +201,7 @@ protected function parse_query_args() { $this->set_ordering_query_args( $query_args ); $this->set_categories_query_args( $query_args ); $this->set_visibility_query_args( $query_args ); + $this->set_stock_status_query_args( $query_args ); return $query_args; } @@ -272,6 +285,30 @@ protected function set_visibility_query_args( &$query_args ) { ); } + /** + * Set which stock status to use when displaying products. + * + * @param array $query_args Query args. + * @return void + */ + protected function set_stock_status_query_args( &$query_args ) { + // phpcs:disable WordPress.DB.SlowDBQuery.slow_db_query_meta_query + if ( 'yes' !== get_option( 'woocommerce_hide_out_of_stock_items' ) && + isset( $this->attributes['stockStatus'] ) && + ( 'any' !== $this->attributes['stockStatus'] && '' !== $this->attributes['stockStatus'] ) + ) { + // Reset meta_query then update with our stock status. + $query_args['meta_query'] = $this->meta_query; + $query_args['meta_query'][] = array( + 'key' => '_stock_status', + 'value' => $this->attributes['stockStatus'], + ); + } else { + $query_args['meta_query'] = $this->meta_query; + } + // phpcs:enable WordPress.DB.SlowDBQuery.slow_db_query_meta_query + } + /** * Works out the item limit based on rows and columns, or returns default. * @@ -498,6 +535,7 @@ protected function get_title_html( $product ) { if ( empty( $this->attributes['contentVisibility']['title'] ) ) { return ''; } + return '
' . wp_kses_post( $product->get_title() ) . '
'; } @@ -621,5 +659,6 @@ protected function enqueue_data( array $attributes = [] ) { $this->asset_data_registry->add( 'min_rows', wc_get_theme_support( 'product_blocks::min_rows', 1 ), true ); $this->asset_data_registry->add( 'max_rows', wc_get_theme_support( 'product_blocks::max_rows', 6 ), true ); $this->asset_data_registry->add( 'default_rows', wc_get_theme_support( 'product_blocks::default_rows', 3 ), true ); + $this->asset_data_registry->add( 'hide_out_of_stock', get_option( 'woocommerce_hide_out_of_stock_items' ), true ); } } diff --git a/src/BlockTypes/ProductTag.php b/src/BlockTypes/ProductTag.php index f4be1af9545..164b7cc8898 100644 --- a/src/BlockTypes/ProductTag.php +++ b/src/BlockTypes/ProductTag.php @@ -48,6 +48,7 @@ protected function get_block_type_attributes() { 'default' => 'any', ), 'isPreview' => $this->get_schema_boolean( false ), + 'stockStatus' => $this->get_schema_string( 'any' ), ); } diff --git a/src/BlockTypes/ProductsByAttribute.php b/src/BlockTypes/ProductsByAttribute.php index 9d559b89cc2..9a18103e5f1 100644 --- a/src/BlockTypes/ProductsByAttribute.php +++ b/src/BlockTypes/ProductsByAttribute.php @@ -67,6 +67,7 @@ protected function get_block_type_attributes() { 'orderby' => $this->get_schema_orderby(), 'rows' => $this->get_schema_number( wc_get_theme_support( 'product_blocks::default_rows', 3 ) ), 'isPreview' => $this->get_schema_boolean( false ), + 'stockStatus' => $this->get_schema_string( 'any' ), ); } }