Convert `product-control` to TypeScript (https://github.com/woocommerce/woocommerce-blocks/pull/10552)
The main aim of this PR was to convert the product-control component to TypeScript. However, since the component is coupled with many other parts (it is itself wrapped into 4 HOCs), many refactoring changes were needed to make the types work. In particular: * Add `convertProductResponseItemToSearchItem` utility function The function allows to clean up the `ProductResponseItem` in order to make its shape compatible with the `SearchListItem`, instead of passing the entire object. Because data from the object might need to be used in certain circumstance (e.g. render functions), the data is now allowed to be scoped within a `details` key. This commit also converts `utils/products` to TypeScript. * Fix `ProductResponseItem` type missing `slug` The `slug` is returned by the API but didn't appear in the type def. * Improve certain HOCs types The `withSearchedProducts` and `withTransformSingleSelectToMultipleSelect` HOCs are now typed in such a way that it is clear what incoming props they expect and what they inject. * Refactor `ProductAttrributeTermControl` for further type safety Make sure that `count` is always a `number` as required by the component types. * Refactor `search-list-control.tsx` and `item.tsx` The new `SearchItem` allows for the `details` key to be passed. All the components are now aware of it.
This commit is contained in:
parent
a898a5b5ee
commit
5c4a757b6e
|
@ -56,6 +56,7 @@ const ProductAttributeTermControl = ( {
|
|||
|
||||
const renderItem = ( args: renderItemArgs ) => {
|
||||
const { item, search, depth = 0 } = args;
|
||||
const count = item.count || 0;
|
||||
const classes = [
|
||||
'woocommerce-product-attributes__item',
|
||||
'woocommerce-search-list__item',
|
||||
|
@ -79,21 +80,21 @@ const ProductAttributeTermControl = ( {
|
|||
_n(
|
||||
'%d term',
|
||||
'%d terms',
|
||||
item.count,
|
||||
count,
|
||||
'woo-gutenberg-products-block'
|
||||
),
|
||||
item.count
|
||||
count
|
||||
) }
|
||||
aria-label={ sprintf(
|
||||
/* translators: %1$s is the item name, %2$d is the count of terms for the item. */
|
||||
_n(
|
||||
'%1$s, has %2$d term',
|
||||
'%1$s, has %2$d terms',
|
||||
item.count,
|
||||
count,
|
||||
'woo-gutenberg-products-block'
|
||||
),
|
||||
item.name,
|
||||
item.count
|
||||
count
|
||||
) }
|
||||
/>
|
||||
);
|
||||
|
@ -111,21 +112,21 @@ const ProductAttributeTermControl = ( {
|
|||
_n(
|
||||
'%d product',
|
||||
'%d products',
|
||||
item.count,
|
||||
count,
|
||||
'woo-gutenberg-products-block'
|
||||
),
|
||||
item.count
|
||||
count
|
||||
) }
|
||||
aria-label={ sprintf(
|
||||
/* translators: %1$s is the attribute name, %2$d is the count of products for that attribute. */
|
||||
_n(
|
||||
'%1$s, has %2$d product',
|
||||
'%1$s, has %2$d products',
|
||||
item.count,
|
||||
count,
|
||||
'woo-gutenberg-products-block'
|
||||
),
|
||||
itemName,
|
||||
item.count
|
||||
count
|
||||
) }
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -3,26 +3,64 @@
|
|||
*/
|
||||
import { __, _n, sprintf } from '@wordpress/i18n';
|
||||
import { isEmpty } from '@woocommerce/types';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
SearchListControl,
|
||||
SearchListItem,
|
||||
} from '@woocommerce/editor-components/search-list-control';
|
||||
import type {
|
||||
SearchListControlProps,
|
||||
renderItemArgs,
|
||||
} from '@woocommerce/editor-components/search-list-control/types';
|
||||
import { withInstanceId } from '@wordpress/compose';
|
||||
import {
|
||||
withProductVariations,
|
||||
withSearchedProducts,
|
||||
withTransformSingleSelectToMultipleSelect,
|
||||
} from '@woocommerce/block-hocs';
|
||||
import type {
|
||||
ProductResponseItem,
|
||||
WithInjectedInstanceId,
|
||||
WithInjectedProductVariations,
|
||||
WithInjectedSearchedProducts,
|
||||
} from '@woocommerce/types';
|
||||
import { convertProductResponseItemToSearchItem } from '@woocommerce/utils';
|
||||
import ErrorMessage from '@woocommerce/editor-components/error-placeholder/error-message';
|
||||
import classNames from 'classnames';
|
||||
import ExpandableSearchListItem from '@woocommerce/editor-components/expandable-search-list-item/expandable-search-list-item.tsx';
|
||||
import ExpandableSearchListItem from '@woocommerce/editor-components/expandable-search-list-item/expandable-search-list-item';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import './style.scss';
|
||||
|
||||
interface ProductControlProps {
|
||||
/**
|
||||
* Callback to update the selected products.
|
||||
*/
|
||||
onChange: () => void;
|
||||
isCompact?: boolean;
|
||||
/**
|
||||
* The ID of the currently expanded product.
|
||||
*/
|
||||
expandedProduct: number | null;
|
||||
/**
|
||||
* Callback to search products by their name.
|
||||
*/
|
||||
onSearch: () => void;
|
||||
/**
|
||||
* Callback to render each item in the selection list, allows any custom object-type rendering.
|
||||
*/
|
||||
renderItem: SearchListControlProps[ 'renderItem' ] | null;
|
||||
/**
|
||||
* The ID of the currently selected item (product or variation).
|
||||
*/
|
||||
selected: number[];
|
||||
/**
|
||||
* Whether to show variations in the list of items available.
|
||||
*/
|
||||
showVariations?: boolean;
|
||||
}
|
||||
|
||||
const messages = {
|
||||
list: __( 'Products', 'woo-gutenberg-products-block' ),
|
||||
noItems: __(
|
||||
|
@ -39,26 +77,35 @@ const messages = {
|
|||
),
|
||||
};
|
||||
|
||||
const ProductControl = ( {
|
||||
expandedProduct,
|
||||
error,
|
||||
instanceId,
|
||||
isCompact,
|
||||
isLoading,
|
||||
onChange,
|
||||
onSearch,
|
||||
products,
|
||||
renderItem,
|
||||
selected,
|
||||
showVariations,
|
||||
variations,
|
||||
variationsLoading,
|
||||
} ) => {
|
||||
const renderItemWithVariations = ( args ) => {
|
||||
const ProductControl = (
|
||||
props: ProductControlProps &
|
||||
WithInjectedSearchedProducts &
|
||||
WithInjectedProductVariations &
|
||||
WithInjectedInstanceId
|
||||
) => {
|
||||
const {
|
||||
expandedProduct = null,
|
||||
error,
|
||||
instanceId,
|
||||
isCompact = false,
|
||||
isLoading,
|
||||
onChange,
|
||||
onSearch,
|
||||
products,
|
||||
renderItem,
|
||||
selected = [],
|
||||
showVariations = false,
|
||||
variations,
|
||||
variationsLoading,
|
||||
} = props;
|
||||
|
||||
const renderItemWithVariations = (
|
||||
args: renderItemArgs< ProductResponseItem >
|
||||
) => {
|
||||
const { item, search, depth = 0, isSelected, onSelect } = args;
|
||||
const variationsCount =
|
||||
item.variations && Array.isArray( item.variations )
|
||||
? item.variations.length
|
||||
item.details?.variations && Array.isArray( item.details.variations )
|
||||
? item.details.variations.length
|
||||
: 0;
|
||||
const classes = classNames(
|
||||
'woocommerce-search-product__item',
|
||||
|
@ -74,6 +121,9 @@ const ProductControl = ( {
|
|||
|
||||
// Top level items custom rendering based on SearchListItem.
|
||||
if ( ! item.breadcrumbs.length ) {
|
||||
const hasVariations =
|
||||
item.details?.variations && item.details.variations.length > 0;
|
||||
|
||||
return (
|
||||
<ExpandableSearchListItem
|
||||
{ ...args }
|
||||
|
@ -89,42 +139,47 @@ const ProductControl = ( {
|
|||
} }
|
||||
isLoading={ isLoading || variationsLoading }
|
||||
countLabel={
|
||||
item.variations.length > 0
|
||||
hasVariations
|
||||
? sprintf(
|
||||
/* translators: %1$d is the number of variations of a product product. */
|
||||
__(
|
||||
'%1$d variations',
|
||||
'woo-gutenberg-products-block'
|
||||
),
|
||||
item.variations.length
|
||||
item.details?.variations.length
|
||||
)
|
||||
: null
|
||||
}
|
||||
name={ `products-${ instanceId }` }
|
||||
aria-label={ sprintf(
|
||||
/* translators: %1$s is the product name, %2$d is the number of variations of that product. */
|
||||
_n(
|
||||
'%1$s, has %2$d variation',
|
||||
'%1$s, has %2$d variations',
|
||||
item.variations.length,
|
||||
'woo-gutenberg-products-block'
|
||||
),
|
||||
item.name,
|
||||
item.variations.length
|
||||
) }
|
||||
aria-label={
|
||||
hasVariations
|
||||
? sprintf(
|
||||
/* translators: %1$s is the product name, %2$d is the number of variations of that product. */
|
||||
_n(
|
||||
'%1$s, has %2$d variation',
|
||||
'%1$s, has %2$d variations',
|
||||
item.details?.variations
|
||||
?.length as number,
|
||||
'woo-gutenberg-products-block'
|
||||
),
|
||||
item.name,
|
||||
item.details?.variations.length
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const itemArgs = isEmpty( item.variation )
|
||||
const itemArgs = isEmpty( item.details?.variation )
|
||||
? args
|
||||
: {
|
||||
...args,
|
||||
item: {
|
||||
...args.item,
|
||||
name: item.variation,
|
||||
name: item.details?.variation as string,
|
||||
},
|
||||
'aria-label': `${ item.breadcrumbs[ 0 ] }: ${ item.variation }`,
|
||||
'aria-label': `${ item.breadcrumbs[ 0 ] }: ${ item.details?.variation }`,
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -142,7 +197,7 @@ const ProductControl = ( {
|
|||
} else if ( showVariations ) {
|
||||
return renderItemWithVariations;
|
||||
}
|
||||
return null;
|
||||
return undefined;
|
||||
};
|
||||
|
||||
if ( error ) {
|
||||
|
@ -150,10 +205,12 @@ const ProductControl = ( {
|
|||
}
|
||||
|
||||
const currentVariations =
|
||||
variations && variations[ expandedProduct ]
|
||||
variations && expandedProduct && variations[ expandedProduct ]
|
||||
? variations[ expandedProduct ]
|
||||
: [];
|
||||
const currentList = [ ...products, ...currentVariations ];
|
||||
const currentList = [ ...products, ...currentVariations ].map(
|
||||
convertProductResponseItemToSearchItem
|
||||
);
|
||||
|
||||
return (
|
||||
<SearchListControl
|
||||
|
@ -163,7 +220,7 @@ const ProductControl = ( {
|
|||
isLoading={ isLoading }
|
||||
isSingle
|
||||
selected={ currentList.filter( ( { id } ) =>
|
||||
selected.includes( id )
|
||||
selected.includes( Number( id ) )
|
||||
) }
|
||||
onChange={ onChange }
|
||||
renderItem={ getRenderItemFunc() }
|
||||
|
@ -174,45 +231,6 @@ const ProductControl = ( {
|
|||
);
|
||||
};
|
||||
|
||||
ProductControl.propTypes = {
|
||||
/**
|
||||
* Callback to update the selected products.
|
||||
*/
|
||||
onChange: PropTypes.func.isRequired,
|
||||
isCompact: PropTypes.bool,
|
||||
/**
|
||||
* The ID of the currently expanded product.
|
||||
*/
|
||||
expandedProduct: PropTypes.number,
|
||||
/**
|
||||
* Callback to search products by their name.
|
||||
*/
|
||||
onSearch: PropTypes.func,
|
||||
/**
|
||||
* Query args to pass to getProducts.
|
||||
*/
|
||||
queryArgs: PropTypes.object,
|
||||
/**
|
||||
* Callback to render each item in the selection list, allows any custom object-type rendering.
|
||||
*/
|
||||
renderItem: PropTypes.func,
|
||||
/**
|
||||
* The ID of the currently selected item (product or variation).
|
||||
*/
|
||||
selected: PropTypes.arrayOf( PropTypes.number ),
|
||||
/**
|
||||
* Whether to show variations in the list of items available.
|
||||
*/
|
||||
showVariations: PropTypes.bool,
|
||||
};
|
||||
|
||||
ProductControl.defaultProps = {
|
||||
isCompact: false,
|
||||
expandedProduct: null,
|
||||
selected: [],
|
||||
showVariations: false,
|
||||
};
|
||||
|
||||
export default withTransformSingleSelectToMultipleSelect(
|
||||
withSearchedProducts(
|
||||
withProductVariations( withInstanceId( ProductControl ) )
|
|
@ -40,7 +40,7 @@ const ItemLabel = ( props: { item: SearchListItemProps; search: string } ) => {
|
|||
);
|
||||
};
|
||||
|
||||
export const SearchListItem = ( {
|
||||
export const SearchListItem = < T extends object = object >( {
|
||||
countLabel,
|
||||
className,
|
||||
depth = 0,
|
||||
|
@ -53,7 +53,7 @@ export const SearchListItem = ( {
|
|||
selected,
|
||||
useExpandedPanelId,
|
||||
...props
|
||||
}: renderItemArgs ): JSX.Element => {
|
||||
}: renderItemArgs< T > ): JSX.Element => {
|
||||
const [ expandedPanelId, setExpandedPanelId ] = useExpandedPanelId;
|
||||
const showCount =
|
||||
countLabel !== undefined &&
|
||||
|
@ -165,6 +165,7 @@ export const SearchListItem = ( {
|
|||
{ isSingle ? (
|
||||
<>
|
||||
<input
|
||||
{ ...props }
|
||||
type="radio"
|
||||
id={ id }
|
||||
name={ name }
|
||||
|
@ -172,13 +173,13 @@ export const SearchListItem = ( {
|
|||
onChange={ onSelect( item ) }
|
||||
checked={ isSelected }
|
||||
className="woocommerce-search-list__item-input"
|
||||
{ ...props }
|
||||
></input>
|
||||
|
||||
<ItemLabel item={ item } search={ search } />
|
||||
</>
|
||||
) : (
|
||||
<CheckboxControl
|
||||
{ ...props }
|
||||
id={ id }
|
||||
name={ name }
|
||||
className="woocommerce-search-list__item-input"
|
||||
|
@ -189,7 +190,6 @@ export const SearchListItem = ( {
|
|||
) }
|
||||
onChange={ onSelect( item ) }
|
||||
checked={ isSelected }
|
||||
{ ...props }
|
||||
/>
|
||||
) }
|
||||
|
||||
|
|
|
@ -101,14 +101,14 @@ const ListItems = ( props: ListItemsProps ): JSX.Element | null => {
|
|||
);
|
||||
};
|
||||
|
||||
const SelectedListItems = ( {
|
||||
const SelectedListItems = < T extends object = object >( {
|
||||
isLoading,
|
||||
isSingle,
|
||||
selected,
|
||||
messages,
|
||||
onChange,
|
||||
onRemove,
|
||||
}: SearchListControlProps & {
|
||||
}: SearchListControlProps< T > & {
|
||||
messages: SearchListMessages;
|
||||
onRemove: ( itemId: string | number ) => () => void;
|
||||
} ) => {
|
||||
|
@ -148,14 +148,14 @@ const SelectedListItems = ( {
|
|||
);
|
||||
};
|
||||
|
||||
const ListItemsContainer = ( {
|
||||
const ListItemsContainer = < T extends object = object >( {
|
||||
filteredList,
|
||||
search,
|
||||
onSelect,
|
||||
instanceId,
|
||||
useExpandedPanelId,
|
||||
...props
|
||||
}: SearchListItemsContainerProps ) => {
|
||||
}: SearchListItemsContainerProps< T > ) => {
|
||||
const { messages, renderItem, selected, isSingle } = props;
|
||||
const renderItemCallback = renderItem || defaultRenderListItem;
|
||||
|
||||
|
@ -194,7 +194,9 @@ const ListItemsContainer = ( {
|
|||
/**
|
||||
* Component to display a searchable, selectable list of items.
|
||||
*/
|
||||
export const SearchListControl = ( props: SearchListControlProps ) => {
|
||||
export const SearchListControl = < T extends object = object >(
|
||||
props: SearchListControlProps< T >
|
||||
) => {
|
||||
const {
|
||||
className = '',
|
||||
isCompact,
|
||||
|
@ -250,22 +252,25 @@ export const SearchListControl = ( props: SearchListControlProps ) => {
|
|||
);
|
||||
|
||||
const onSelect = useCallback(
|
||||
( item: SearchListItemProps | SearchListItemProps[] ) => () => {
|
||||
if ( Array.isArray( item ) ) {
|
||||
onChange( item );
|
||||
return;
|
||||
}
|
||||
( item: SearchListItemProps< T > | SearchListItemProps< T >[] ) =>
|
||||
() => {
|
||||
if ( Array.isArray( item ) ) {
|
||||
onChange( item );
|
||||
return;
|
||||
}
|
||||
|
||||
if ( selected.findIndex( ( { id } ) => id === item.id ) !== -1 ) {
|
||||
onRemove( item.id )();
|
||||
return;
|
||||
}
|
||||
if ( isSingle ) {
|
||||
onChange( [ item ] );
|
||||
} else {
|
||||
onChange( [ ...selected, item ] );
|
||||
}
|
||||
},
|
||||
if (
|
||||
selected.findIndex( ( { id } ) => id === item.id ) !== -1
|
||||
) {
|
||||
onRemove( item.id )();
|
||||
return;
|
||||
}
|
||||
if ( isSingle ) {
|
||||
onChange( [ item ] );
|
||||
} else {
|
||||
onChange( [ ...selected, item ] );
|
||||
}
|
||||
},
|
||||
[ isSingle, onRemove, onChange, selected ]
|
||||
);
|
||||
|
||||
|
|
|
@ -1,14 +1,16 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import type { ReactNode } from 'react';
|
||||
import type { InputHTMLAttributes, ReactNode } from 'react';
|
||||
import { Require } from '@woocommerce/types';
|
||||
|
||||
interface ItemProps {
|
||||
interface ItemProps< T extends object = object > {
|
||||
// Depth, non-zero if the list is hierarchical.
|
||||
depth?: number;
|
||||
// Callback for selecting the item.
|
||||
onSelect: ( item: SearchListItem | SearchListItem[] ) => () => void;
|
||||
onSelect: (
|
||||
item: SearchListItem< T > | SearchListItem< T >[]
|
||||
) => () => void;
|
||||
// Search string, used to highlight the substring in the item name.
|
||||
search: string;
|
||||
useExpandedPanelId: [
|
||||
|
@ -17,15 +19,15 @@ interface ItemProps {
|
|||
];
|
||||
}
|
||||
|
||||
interface SearchListProps {
|
||||
interface SearchListProps< T extends object = object > {
|
||||
//Restrict selections to one item.
|
||||
isSingle: boolean;
|
||||
// A complete list of item objects, each with id, name properties. This is displayed as a clickable/keyboard-able list, and possibly filtered by the search term (searches name).
|
||||
list: SearchListItem[];
|
||||
list: SearchListItem< T >[];
|
||||
// Callback to render each item in the selection list, allows any custom object-type rendering.
|
||||
renderItem?: ( args: renderItemArgs ) => JSX.Element;
|
||||
renderItem?: ( args: renderItemArgs< T > ) => JSX.Element;
|
||||
// The list of currently selected items.
|
||||
selected: SearchListItem[];
|
||||
selected: SearchListItem< T >[];
|
||||
}
|
||||
|
||||
export interface ListItemsProps
|
||||
|
@ -34,21 +36,22 @@ export interface ListItemsProps
|
|||
instanceId: string | number;
|
||||
}
|
||||
|
||||
export type SearchListItem = {
|
||||
export type SearchListItem< T extends object = object > = {
|
||||
breadcrumbs: string[];
|
||||
children?: SearchListItem[];
|
||||
count: number;
|
||||
children?: SearchListItem< T >[];
|
||||
count?: number;
|
||||
details?: T;
|
||||
id: string | number;
|
||||
name: string;
|
||||
parent: number;
|
||||
value: string;
|
||||
};
|
||||
|
||||
export interface SearchListItemsContainerProps
|
||||
export interface SearchListItemsContainerProps< T extends object = object >
|
||||
extends SearchListControlProps,
|
||||
ItemProps {
|
||||
instanceId: string | number;
|
||||
filteredList: SearchListItem[];
|
||||
filteredList: SearchListItem< T >[];
|
||||
messages: SearchListMessages;
|
||||
}
|
||||
|
||||
|
@ -67,23 +70,30 @@ export interface SearchListMessages {
|
|||
updated: string;
|
||||
}
|
||||
|
||||
export interface renderItemArgs extends ItemProps {
|
||||
export interface renderItemArgs< T extends object = object >
|
||||
extends ItemProps,
|
||||
Partial<
|
||||
Omit<
|
||||
InputHTMLAttributes< HTMLInputElement >,
|
||||
'onChange' | 'onSelect'
|
||||
>
|
||||
> {
|
||||
// Additional CSS classes.
|
||||
className?: string | undefined;
|
||||
// Whether the item is disable.
|
||||
disabled?: boolean | undefined;
|
||||
className?: string;
|
||||
// Unique id of the parent control.
|
||||
controlId: string | number;
|
||||
// Label to display in the count bubble. Takes preference over `item.count`.
|
||||
countLabel?: ReactNode;
|
||||
// Whether the item is disabled.
|
||||
disabled?: boolean;
|
||||
// Current item to display.
|
||||
item: SearchListItem;
|
||||
item: SearchListItem< T >;
|
||||
// Whether this item is selected.
|
||||
isSelected: boolean;
|
||||
// Whether this should only display a single item (controls radio vs checkbox icon).
|
||||
isSingle: boolean;
|
||||
// The list of currently selected items.
|
||||
selected: SearchListItem[];
|
||||
// Unique id of the parent control.
|
||||
controlId: string | number;
|
||||
// Label to display in the count bubble. Takes preference over `item.count`.
|
||||
countLabel?: ReactNode;
|
||||
selected: SearchListItem< T >[];
|
||||
/**
|
||||
* Name of the inputs. Used to group input controls together. See:
|
||||
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#attr-name
|
||||
|
@ -94,7 +104,7 @@ export interface renderItemArgs extends ItemProps {
|
|||
ariaLabel?: string;
|
||||
}
|
||||
|
||||
export interface SearchListControlProps {
|
||||
export interface SearchListControlProps< T extends object = object > {
|
||||
// Additional CSS classes.
|
||||
className?: string;
|
||||
// Whether it should be displayed in a compact way, so it occupies less space.
|
||||
|
@ -106,17 +116,17 @@ export interface SearchListControlProps {
|
|||
//Restrict selections to one item.
|
||||
isSingle: boolean;
|
||||
// A complete list of item objects, each with id, name properties. This is displayed as a clickable/keyboard-able list, and possibly filtered by the search term (searches name).
|
||||
list: SearchListItem[];
|
||||
list: SearchListItem< T >[];
|
||||
// Messages displayed or read to the user. Configure these to reflect your object type.
|
||||
messages?: Partial< SearchListMessages >;
|
||||
// Callback fired when selected items change, whether added, cleared, or removed. Passed an array of item objects (as passed in via props.list).
|
||||
onChange: ( search: SearchListItem[] ) => void;
|
||||
onChange: ( search: SearchListItem< T >[] ) => void;
|
||||
// Callback fired when the search field is used.
|
||||
onSearch?: ( ( search: string ) => void ) | undefined;
|
||||
// Callback to render each item in the selection list, allows any custom object-type rendering.
|
||||
renderItem?: ( args: renderItemArgs ) => JSX.Element;
|
||||
renderItem?: ( ( args: renderItemArgs< T > ) => JSX.Element ) | undefined;
|
||||
// The list of currently selected items.
|
||||
selected: SearchListItem[];
|
||||
selected: SearchListItem< T >[];
|
||||
// Whether to show a text field or a token field as search
|
||||
// Defaults to `'text'`
|
||||
type?: 'text' | 'token';
|
||||
|
|
|
@ -5,3 +5,7 @@ export { default as withProduct } from './with-product';
|
|||
export { default as withProductVariations } from './with-product-variations';
|
||||
export { default as withSearchedProducts } from './with-searched-products';
|
||||
export { default as withTransformSingleSelectToMultipleSelect } from './with-transform-single-select-to-multiple-select';
|
||||
export {
|
||||
SelectedOption,
|
||||
WithMaybeSelectedOption,
|
||||
} from './with-transform-single-select-to-multiple-select';
|
||||
|
|
|
@ -5,20 +5,29 @@ import { useEffect, useState, useCallback, useRef } from '@wordpress/element';
|
|||
import { blocksConfig } from '@woocommerce/block-settings';
|
||||
import { getProducts } from '@woocommerce/editor-components/utils';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
import type { ProductResponseItem } from '@woocommerce/types';
|
||||
import type {
|
||||
ProductResponseItem,
|
||||
WithInjectedSearchedProducts,
|
||||
} from '@woocommerce/types';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { formatError } from '../base/utils/errors.js';
|
||||
|
||||
interface WithSearchedProductProps {
|
||||
selected: number[];
|
||||
}
|
||||
|
||||
/**
|
||||
* A higher order component that enhances the provided component with products from a search query.
|
||||
*/
|
||||
const withSearchedProducts = (
|
||||
OriginalComponent: React.FunctionComponent< Record< string, unknown > >
|
||||
const withSearchedProducts = <
|
||||
T extends Record< string, unknown > & WithSearchedProductProps
|
||||
>(
|
||||
OriginalComponent: React.ComponentType< T & WithInjectedSearchedProducts >
|
||||
) => {
|
||||
return ( { selected, ...props }: { selected: number[] } ): JSX.Element => {
|
||||
return ( { selected, ...props }: T ): JSX.Element => {
|
||||
const [ isLoading, setIsLoading ] = useState( true );
|
||||
const [ error, setError ] = useState< {
|
||||
message: string;
|
||||
|
@ -71,7 +80,7 @@ const withSearchedProducts = (
|
|||
|
||||
return (
|
||||
<OriginalComponent
|
||||
{ ...props }
|
||||
{ ...( props as T ) }
|
||||
selected={ selected }
|
||||
error={ error }
|
||||
products={ productsList }
|
||||
|
|
|
@ -6,21 +6,21 @@
|
|||
// eslint-disable-next-line
|
||||
import { FunctionComponent } from '@wordpress/element';
|
||||
|
||||
type SelectedOption = number | string | null | number[] | string[];
|
||||
export type SelectedOption = number | string | null | number[] | string[];
|
||||
|
||||
interface OriginalComponentProps {
|
||||
export interface WithMaybeSelectedOption {
|
||||
selected?: SelectedOption;
|
||||
}
|
||||
|
||||
/**
|
||||
* HOC that transforms a single select to a multiple select.
|
||||
*
|
||||
* @param {FunctionComponent< Record< string, unknown > >} OriginalComponent Component being wrapped.
|
||||
*/
|
||||
const withTransformSingleSelectToMultipleSelect = (
|
||||
OriginalComponent: FunctionComponent< Record< string, unknown > >
|
||||
const withTransformSingleSelectToMultipleSelect = <
|
||||
T extends Record< string, unknown >
|
||||
>(
|
||||
OriginalComponent: FunctionComponent< T & WithMaybeSelectedOption >
|
||||
) => {
|
||||
return ( props: OriginalComponentProps ): JSX.Element => {
|
||||
return ( props: T & WithMaybeSelectedOption ): JSX.Element => {
|
||||
let { selected } = props;
|
||||
selected = selected === undefined ? null : selected;
|
||||
const isNil = selected === null;
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { ErrorObject } from '@woocommerce/editor-components/error-placeholder';
|
||||
import type { ProductResponseItem } from '@woocommerce/types';
|
||||
|
||||
export interface WithInjectedProductVariations {
|
||||
error: ErrorObject | null;
|
||||
/**
|
||||
* The id of the currently expanded product
|
||||
*/
|
||||
expandedProduct: number | null;
|
||||
variations: Record< number, ProductResponseItem[] >;
|
||||
variationsLoading: boolean;
|
||||
}
|
||||
|
||||
export interface WithInjectedSearchedProducts {
|
||||
error: ErrorObject | null;
|
||||
isLoading: boolean;
|
||||
onSearch: ( ( search: string ) => void ) | null;
|
||||
products: ProductResponseItem[];
|
||||
selected: number[];
|
||||
}
|
||||
|
||||
export interface WithInjectedInstanceId {
|
||||
instanceId: string | number;
|
||||
}
|
|
@ -8,6 +8,7 @@ export * from './checkout';
|
|||
export * from './contexts';
|
||||
export * from './currency';
|
||||
export * from './events';
|
||||
export * from './hocs';
|
||||
export * from './hooks';
|
||||
export * from './notices';
|
||||
export * from './objects';
|
||||
|
|
|
@ -90,4 +90,5 @@ export interface ProductResponseItem {
|
|||
maximum: number;
|
||||
multiple_of: number;
|
||||
};
|
||||
slug: string;
|
||||
}
|
||||
|
|
|
@ -1,29 +0,0 @@
|
|||
/**
|
||||
* Get the src of the first image attached to a product (the featured image).
|
||||
*
|
||||
* @param {Object} product The product object to get the images from.
|
||||
* @param {Array} product.images The array of images, destructured from the product object.
|
||||
* @return {string} The full URL to the image.
|
||||
*/
|
||||
export function getImageSrcFromProduct( product ) {
|
||||
if ( ! product || ! product.images || ! product.images.length ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return product.images[ 0 ].src || '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ID of the first image attached to a product (the featured image).
|
||||
*
|
||||
* @param {Object} product The product object to get the images from.
|
||||
* @param {Array} product.images The array of images, destructured from the product object.
|
||||
* @return {number} The ID of the image.
|
||||
*/
|
||||
export function getImageIdFromProduct( product ) {
|
||||
if ( ! product || ! product.images || ! product.images.length ) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return product.images[ 0 ].id || 0;
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import type { SearchListItem } from '@woocommerce/editor-components/search-list-control/types';
|
||||
import type { ProductResponseItem } from '@woocommerce/types';
|
||||
|
||||
/**
|
||||
* Converts a Product object into a shape compatible with the `SearchListControl`
|
||||
*/
|
||||
export const convertProductResponseItemToSearchItem = (
|
||||
product: ProductResponseItem
|
||||
): SearchListItem< ProductResponseItem > => {
|
||||
const { id, name, parent } = product;
|
||||
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
parent,
|
||||
breadcrumbs: [],
|
||||
children: [],
|
||||
details: product,
|
||||
value: product.slug,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the src of the first image attached to a product (the featured image).
|
||||
*/
|
||||
export function getImageSrcFromProduct( product: ProductResponseItem ) {
|
||||
if ( ! product || ! product.images || ! product.images.length ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return product.images[ 0 ].src || '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ID of the first image attached to a product (the featured image).
|
||||
*/
|
||||
export function getImageIdFromProduct( product: ProductResponseItem ) {
|
||||
if ( ! product || ! product.images || ! product.images.length ) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return product.images[ 0 ].id || 0;
|
||||
}
|
Loading…
Reference in New Issue