Skip to content
This repository has been archived by the owner on Feb 23, 2024. It is now read-only.

Commit

Permalink
Add Stock Status to Product Query block filters
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
sunyatasattva committed Oct 12, 2022
1 parent ccdecfe commit b6b51dc
Show file tree
Hide file tree
Showing 4 changed files with 136 additions and 38 deletions.
17 changes: 16 additions & 1 deletion assets/js/blocks/product-query/constants.ts
Original file line number Diff line number Diff line change
@@ -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: {
Expand All @@ -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 ),
},
};

Expand Down
121 changes: 101 additions & 20 deletions assets/js/blocks/product-query/inspector-controls.tsx
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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 ) => (
<ToggleControl
label={ __(
'Show only products on sale',
'woo-gutenberg-products-block'
) }
checked={ props.attributes.query.__woocommerceOnSale || false }
onChange={ ( __woocommerceOnSale ) => {
setCustomQueryAttribute( props, { __woocommerceOnSale } );
} }
/>
),
onSale: ( props: ProductQueryBlock ) => {
const { query } = props.attributes;

return (
<ToolsPanelItem
label={ __( 'Sale status', 'woo-gutenberg-products-block' ) }
hasValue={ () => query.__woocommerceOnSale }
>
<ToggleControl
label={ __(
'Show only products on sale',
'woo-gutenberg-products-block'
) }
checked={ query.__woocommerceOnSale || false }
onChange={ ( __woocommerceOnSale ) => {
setCustomQueryAttribute( props, {
__woocommerceOnSale,
} );
} }
/>
</ToolsPanelItem>
);
},
stockStatus: ( props: ProductQueryBlock ) => {
const { query } = props.attributes;

return (
<ToolsPanelItem
label={ __( 'Stock status', 'woo-gutenberg-products-block' ) }
hasValue={ () => query.__woocommerceStockStatus }
>
<FormTokenField
label={ __(
'Stock status',
'woo-gutenberg-products-block'
) }
onChange={ ( statusLabels ) => {
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 }
/>
</ToolsPanelItem>
);
},
};

export const withProductQueryControls =
< T extends EditorBlock< T > >( BlockEdit: ElementType ) =>
( props: ProductQueryBlock ) => {
const allowedControls = useAllowedControls( props.attributes );

return isWooQueryBlockVariation( props ) ? (
<>
<BlockEdit { ...props } />
<InspectorControls>
{ Object.entries( INSPECTOR_CONTROLS ).map(
( [ key, Control ] ) =>
allowedControls?.includes( key ) ? (
<Control { ...props } />
) : null
) }
<ToolsPanel
class="woocommerce-product-query-toolspanel"
label={ __(
'Product filters',
'woo-gutenberg-products-block'
) }
>
{ Object.entries( INSPECTOR_CONTROLS ).map(
( [ key, Control ] ) =>
allowedControls?.includes( key ) ? (
<Control { ...props } />
) : null
) }
</ToolsPanel>
</InspectorControls>
</>
) : (
Expand Down
27 changes: 10 additions & 17 deletions assets/js/blocks/product-query/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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[];
Expand Down
9 changes: 9 additions & 0 deletions assets/js/blocks/product-query/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
*
Expand Down

0 comments on commit b6b51dc

Please sign in to comment.