From b6b51dc9239275d47fb39fafe7547cc59ffe3cbb Mon Sep 17 00:00:00 2001 From: Lucio Giannotta Date: Wed, 12 Oct 2022 07:11:59 +0200 Subject: [PATCH] Add Stock Status to Product Query block filters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Creates a new Tools Panel called “Product filters” where we can neatly organize our product specific settings. Eventually, this panel could be merged with the core “Filters” panel; however, at the time of this commit, this is impossible (see WordPress/gutenberg#43684 for a PoC). Also moved the “On Sale” setting under this newly created panel. --- assets/js/blocks/product-query/constants.ts | 17 ++- .../product-query/inspector-controls.tsx | 121 +++++++++++++++--- assets/js/blocks/product-query/types.ts | 27 ++-- assets/js/blocks/product-query/utils.tsx | 9 ++ 4 files changed, 136 insertions(+), 38 deletions(-) diff --git a/assets/js/blocks/product-query/constants.ts b/assets/js/blocks/product-query/constants.ts index 0ae12ff267d..691e7ee1782 100644 --- a/assets/js/blocks/product-query/constants.ts +++ b/assets/js/blocks/product-query/constants.ts @@ -1,22 +1,34 @@ /** * External dependencies */ +import { getSetting } from '@woocommerce/settings'; import type { InnerBlockTemplate } from '@wordpress/blocks'; /** * Internal dependencies */ import { QueryBlockAttributes } from './types'; +import { objectOmit } from './utils'; export const DEFAULT_CORE_ALLOWED_CONTROLS = [ 'order', 'taxQuery', 'search' ]; -export const ALL_PRODUCT_QUERY_CONTROLS = [ 'onSale' ]; +export const ALL_PRODUCT_QUERY_CONTROLS = [ 'onSale', 'stockStatus' ]; export const DEFAULT_ALLOWED_CONTROLS = [ ...DEFAULT_CORE_ALLOWED_CONTROLS, ...ALL_PRODUCT_QUERY_CONTROLS, ]; +export const STOCK_STATUS_OPTIONS = getSetting< Record< string, string > >( + 'stockStatusOptions', + [] +); + +const GLOBAL_HIDE_OUT_OF_STOCK = getSetting< boolean >( + 'hideOutOfStockItems', + false +); + export const QUERY_DEFAULT_ATTRIBUTES: QueryBlockAttributes = { allowControls: DEFAULT_ALLOWED_CONTROLS, displayLayout: { @@ -35,6 +47,9 @@ export const QUERY_DEFAULT_ATTRIBUTES: QueryBlockAttributes = { exclude: [], sticky: '', inherit: false, + __woocommerceStockStatus: GLOBAL_HIDE_OUT_OF_STOCK + ? Object.keys( objectOmit( STOCK_STATUS_OPTIONS, 'outofstock' ) ) + : Object.keys( STOCK_STATUS_OPTIONS ), }, }; diff --git a/assets/js/blocks/product-query/inspector-controls.tsx b/assets/js/blocks/product-query/inspector-controls.tsx index 56cfc24100a..89fd4eaf6c0 100644 --- a/assets/js/blocks/product-query/inspector-controls.tsx +++ b/assets/js/blocks/product-query/inspector-controls.tsx @@ -1,12 +1,19 @@ /** * External dependencies */ +import { ElementType } from 'react'; import { __ } from '@wordpress/i18n'; import { InspectorControls } from '@wordpress/block-editor'; -import { ToggleControl } from '@wordpress/components'; import { addFilter } from '@wordpress/hooks'; import { EditorBlock } from '@woocommerce/types'; -import { ElementType } from 'react'; +import { + FormTokenField, + ToggleControl, + // eslint-disable-next-line @wordpress/no-unsafe-wp-apis + __experimentalToolsPanel as ToolsPanel, + // eslint-disable-next-line @wordpress/no-unsafe-wp-apis + __experimentalToolsPanelItem as ToolsPanelItem, +} from '@wordpress/components'; /** * Internal dependencies @@ -17,36 +24,110 @@ import { setCustomQueryAttribute, useAllowedControls, } from './utils'; +import { STOCK_STATUS_OPTIONS } from './constants'; + +/** + * Gets the id of a specific stock status from its text label + * + * In theory, we could use a `saveTransform` function on the + * `FormFieldToken` component to do the conversion. However, plugins + * can add custom stock statii which don't conform to our naming + * conventions. + */ +function getStockStatusIdByLabel( statusLabel: FormTokenField.Value ) { + const label = + typeof statusLabel === 'string' ? statusLabel : statusLabel.value; + + return Object.entries( STOCK_STATUS_OPTIONS ).find( + ( [ , value ] ) => value === label + )?.[ 0 ]; +} export const INSPECTOR_CONTROLS = { - onSale: ( props: ProductQueryBlock ) => ( - { - setCustomQueryAttribute( props, { __woocommerceOnSale } ); - } } - /> - ), + onSale: ( props: ProductQueryBlock ) => { + const { query } = props.attributes; + + return ( + query.__woocommerceOnSale } + > + { + setCustomQueryAttribute( props, { + __woocommerceOnSale, + } ); + } } + /> + + ); + }, + stockStatus: ( props: ProductQueryBlock ) => { + const { query } = props.attributes; + + return ( + query.__woocommerceStockStatus } + > + { + const __woocommerceStockStatus = statusLabels + .map( getStockStatusIdByLabel ) + .filter( Boolean ) as string[]; + + setCustomQueryAttribute( props, { + __woocommerceStockStatus, + } ); + } } + suggestions={ Object.values( STOCK_STATUS_OPTIONS ) } + validateInput={ ( value: string ) => + Object.values( STOCK_STATUS_OPTIONS ).includes( value ) + } + value={ + query?.__woocommerceStockStatus?.map( + ( key ) => STOCK_STATUS_OPTIONS[ key ] + ) || [] + } + __experimentalExpandOnFocus={ true } + /> + + ); + }, }; export const withProductQueryControls = < T extends EditorBlock< T > >( BlockEdit: ElementType ) => ( props: ProductQueryBlock ) => { const allowedControls = useAllowedControls( props.attributes ); + return isWooQueryBlockVariation( props ) ? ( <> - { Object.entries( INSPECTOR_CONTROLS ).map( - ( [ key, Control ] ) => - allowedControls?.includes( key ) ? ( - - ) : null - ) } + + { Object.entries( INSPECTOR_CONTROLS ).map( + ( [ key, Control ] ) => + allowedControls?.includes( key ) ? ( + + ) : null + ) } + ) : ( diff --git a/assets/js/blocks/product-query/types.ts b/assets/js/blocks/product-query/types.ts index f311a3afdda..6ce0c7e3e58 100644 --- a/assets/js/blocks/product-query/types.ts +++ b/assets/js/blocks/product-query/types.ts @@ -3,6 +3,11 @@ */ import type { EditorBlock } from '@woocommerce/types'; +// The interface below disables the forbidden underscores +// naming convention because we are namespacing our +// custom attributes inside a core block. Prefixing with underscores +// will help signify our intentions. +/* eslint-disable @typescript-eslint/naming-convention */ export interface ProductQueryArguments { /** * Display only products on sale. @@ -27,27 +32,15 @@ export interface ProductQueryArguments { * ) * ``` */ - // Disabling naming convention because we are namespacing our - // custom attributes inside a core block. Prefixing with underscores - // will help signify our intentions. - // eslint-disable-next-line @typescript-eslint/naming-convention __woocommerceOnSale?: boolean; -} - -export type ProductQueryBlock = EditorBlock< QueryBlockAttributes >; - -export interface ProductQueryAttributes { /** - * An array of controls to disable in the inspector. - * - * @example `[ 'stockStatus' ]` will not render the dropdown for stock status. + * Filter products by their stock status. */ - disabledInspectorControls?: string[]; - /** - * Query attributes that define which products will be fetched. - */ - query?: ProductQueryArguments; + __woocommerceStockStatus?: string[]; } +/* eslint-enable */ + +export type ProductQueryBlock = EditorBlock< QueryBlockAttributes >; export interface QueryBlockAttributes { allowControls?: string[]; diff --git a/assets/js/blocks/product-query/utils.tsx b/assets/js/blocks/product-query/utils.tsx index 3fbade0fa47..9a764187dec 100644 --- a/assets/js/blocks/product-query/utils.tsx +++ b/assets/js/blocks/product-query/utils.tsx @@ -36,6 +36,15 @@ export function isWooQueryBlockVariation( block: ProductQueryBlock ) { ); } +/** + * Returns an object without a key. + */ +export function objectOmit< T, K extends keyof T >( obj: T, key: K ) { + const { [ key ]: omit, ...rest } = obj; + + return rest; +} + /** * Sets the new query arguments of a Product Query block *