-
Notifications
You must be signed in to change notification settings - Fork 221
Implement Hand-Picked Products block #7925
Changes from 7 commits
b6c8ee7
318549b
e26bf2e
b9cdf71
2787e92
f4879c8
7df46a5
db5377c
a57fde6
cd53e0c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,103 @@ | ||||||
/** | ||||||
* External dependencies | ||||||
*/ | ||||||
import { getProducts } from '@woocommerce/editor-components/utils'; | ||||||
import { ProductResponseItem } from '@woocommerce/types'; | ||||||
import { objectOmit } from '@woocommerce/utils'; | ||||||
import { useState, useEffect } from '@wordpress/element'; | ||||||
import { __ } from '@wordpress/i18n'; | ||||||
import { | ||||||
FormTokenField, | ||||||
// eslint-disable-next-line @wordpress/no-unsafe-wp-apis | ||||||
__experimentalToolsPanelItem as ToolsPanelItem, | ||||||
} from '@wordpress/components'; | ||||||
|
||||||
/** | ||||||
* Internal dependencies | ||||||
*/ | ||||||
import { ProductQueryBlock } from '../types'; | ||||||
import { setQueryAttribute } from '../utils'; | ||||||
|
||||||
export const ProductSelector = ( props: ProductQueryBlock ) => { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am nitpicking here, but how do you feel about restructuring the attributes property from the props object to simplify property access:
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for the suggestion! Normally, I'd be up for it, but in this case you can see that we use |
||||||
const { query } = props.attributes; | ||||||
|
||||||
const [ productsList, setProductsList ] = useState< ProductResponseItem[] >( | ||||||
[] | ||||||
); | ||||||
|
||||||
useEffect( () => { | ||||||
getProducts( { selected: [] } ).then( ( results ) => { | ||||||
setProductsList( results as ProductResponseItem[] ); | ||||||
} ); | ||||||
}, [] ); | ||||||
Comment on lines
+22
to
+30
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think it's needed here but just wanted to share my thoughts. Maybe we can create a custom hook to fetch and manage the productsList state to separate concerns and make the component more modular: const useProductsList = (): ProductResponseItem[] => {
const [productsList, setProductsList] = useState<ProductResponseItem[]>([]);
useEffect(() => {
getProducts({ selected: [] }).then((results) => {
setProductsList(results as ProductResponseItem[]);
});
}, []);
return productsList;
}; And use it in the component like this: const productsList = useProductsList(); |
||||||
|
||||||
return ( | ||||||
<ToolsPanelItem | ||||||
label={ __( | ||||||
'Hand-picked Products', | ||||||
'woo-gutenberg-products-block' | ||||||
) } | ||||||
hasValue={ () => query.include?.length } | ||||||
> | ||||||
<FormTokenField | ||||||
disabled={ ! productsList.length } | ||||||
displayTransform={ ( token: string ) => | ||||||
Number.isNaN( Number( token ) ) | ||||||
? token | ||||||
: productsList.find( | ||||||
( product ) => product.id === Number( token ) | ||||||
)?.name || '' | ||||||
} | ||||||
label={ __( | ||||||
'Pick some products', | ||||||
'woo-gutenberg-products-block' | ||||||
) } | ||||||
onChange={ ( values ) => { | ||||||
const ids = values | ||||||
.map( | ||||||
( nameOrId ) => | ||||||
productsList.find( | ||||||
( product ) => | ||||||
product.name === nameOrId || | ||||||
product.id === Number( nameOrId ) | ||||||
)?.id | ||||||
) | ||||||
.filter( Boolean ) | ||||||
.map( String ); | ||||||
|
||||||
if ( ! ids.length && props.attributes.query.include ) { | ||||||
const prunedQuery = objectOmit( | ||||||
props.attributes.query, | ||||||
'include' | ||||||
); | ||||||
|
||||||
setQueryAttribute( | ||||||
{ | ||||||
...props, | ||||||
attributes: { | ||||||
...props.attributes, | ||||||
query: prunedQuery, | ||||||
}, | ||||||
}, | ||||||
{} | ||||||
); | ||||||
} else { | ||||||
setQueryAttribute( props, { | ||||||
include: ids, | ||||||
} ); | ||||||
} | ||||||
} } | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe we can extract the logic of the onChange callback into a separate function to make the component more readable and maintainable? const handleTokensChange = (values: string[]) => {
...
};
...
<FormTokenField
...
onChange={handleTokensChange}
...
/> There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yea good call. |
||||||
suggestions={ productsList.map( ( product ) => product.name ) } | ||||||
validateInput={ ( value: string ) => | ||||||
productsList.find( ( product ) => product.name === value ) | ||||||
} | ||||||
value={ | ||||||
! productsList.length | ||||||
? [ __( 'Loading…', 'woo-gutenberg-products-block' ) ] | ||||||
: query?.include || [] | ||||||
} | ||||||
__experimentalExpandOnFocus={ true } | ||||||
/> | ||||||
</ToolsPanelItem> | ||||||
); | ||||||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
/** | ||
* 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; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great! Thanks for moving this to utils 🙌🏻