Variation switching when deleting (#40780)

* Add variation switcher hook for easier use across multiple components

* Add unregister function for validation

* Add changelogs

* Remove stray console

* Add unRegisterValidator function
This commit is contained in:
louwie17 2023-10-13 20:25:58 -03:00 committed by GitHub
parent b4474fc633
commit 7f25060044
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 178 additions and 60 deletions

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Add unregister function to the validation provider and trigger this from the useValidation hook.

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Add useVariationSwitcher hook and make use of it in the VariationSwitcherFooter.

View File

@ -6,64 +6,46 @@ import { Button } from '@wordpress/components';
import { createElement } from '@wordpress/element'; import { createElement } from '@wordpress/element';
import { arrowLeft, arrowRight, Icon } from '@wordpress/icons'; import { arrowLeft, arrowRight, Icon } from '@wordpress/icons';
import { useSelect } from '@wordpress/data'; import { useSelect } from '@wordpress/data';
import { Product, ProductVariation } from '@woocommerce/data'; import { ProductVariation } from '@woocommerce/data';
import { recordEvent } from '@woocommerce/tracks'; import { recordEvent } from '@woocommerce/tracks';
import { getNewPath, navigateTo } from '@woocommerce/navigation';
/** /**
* Internal dependencies * Internal dependencies
*/ */
import { SwitcherLoadingPlaceholder } from './switcher-loading-placeholder'; import { SwitcherLoadingPlaceholder } from './switcher-loading-placeholder';
import { VariationImagePlaceholder } from './variation-image-placeholder'; import { VariationImagePlaceholder } from './variation-image-placeholder';
import { useVariationSwitcher } from '../../hooks/use-variation-switcher';
export type VariationSwitcherProps = { export type VariationSwitcherProps = {
productType?: string; parentProductType?: string;
variationId: number; variationId: number;
parentId: number; parentId: number;
}; };
function getVariationName( variation: ProductVariation ): string {
return variation.attributes.map( ( attr ) => attr.option ).join( ', ' );
}
export function VariationSwitcherFooter( { export function VariationSwitcherFooter( {
parentProductType,
variationId, variationId,
parentId, parentId,
}: VariationSwitcherProps ) { }: VariationSwitcherProps ) {
const { const {
previousVariation,
nextVariation,
numberOfVariations, numberOfVariations,
...variationIndexes nextVariationId,
} = useSelect( previousVariationId,
( select ) => {
const { getEntityRecord } = select( 'core' );
const parentProduct = getEntityRecord< Product >(
'postType',
'product',
parentId
);
if ( parentProduct && parentProduct.variations ) {
const activeVariationIndex =
parentProduct.variations.indexOf( variationId );
const previousVariationIndex =
activeVariationIndex > 0
? activeVariationIndex - 1
: parentProduct.variations.length - 1;
const nextVariationIndex =
activeVariationIndex !== parentProduct.variations.length - 1
? activeVariationIndex + 1
: 0;
const previousVariationId =
parentProduct.variations[ previousVariationIndex ];
const nextVariationId =
parentProduct.variations[ nextVariationIndex ];
return {
activeVariationIndex, activeVariationIndex,
nextVariationIndex, nextVariationIndex,
previousVariationIndex, previousVariationIndex,
numberOfVariations: parentProduct.variations.length, goToNextVariation,
goToPreviousVariation,
} = useVariationSwitcher( {
variationId,
parentId,
parentProductType,
} );
const { previousVariation, nextVariation } = useSelect(
( select ) => {
const { getEntityRecord } = select( 'core' );
if ( numberOfVariations && numberOfVariations > 0 ) {
return {
previousVariation: getEntityRecord< ProductVariation >( previousVariation: getEntityRecord< ProductVariation >(
'postType', 'postType',
'product_variation', 'product_variation',
@ -78,36 +60,26 @@ export function VariationSwitcherFooter( {
} }
return {}; return {};
}, },
[ variationId, parentId ] [ nextVariationId, previousVariationId, numberOfVariations ]
); );
function onPrevious() { function onPrevious() {
recordEvent( 'product_variation_switch_previous', { recordEvent( 'product_variation_switch_previous', {
variation_length: numberOfVariations, variation_length: numberOfVariations,
variation_id: previousVariation?.id, variation_id: previousVariation?.id,
variation_index: variationIndexes.activeVariationIndex, variation_index: activeVariationIndex,
previous_variation_index: variationIndexes.previousVariationIndex, previous_variation_index: previousVariationIndex,
} );
navigateTo( {
url: getNewPath(
{},
`/product/${ parentId }/variation/${ previousVariation?.id }`
),
} ); } );
goToPreviousVariation();
} }
function onNext() { function onNext() {
recordEvent( 'product_variation_switch_next', { recordEvent( 'product_variation_switch_next', {
variation_length: numberOfVariations, variation_length: numberOfVariations,
variation_id: nextVariation?.id, variation_id: nextVariation?.id,
variation_index: variationIndexes.activeVariationIndex, variation_index: activeVariationIndex,
next_variation_index: variationIndexes.nextVariationIndex, next_variation_index: nextVariationIndex,
} );
navigateTo( {
url: getNewPath(
{},
`/product/${ parentId }/variation/${ nextVariation?.id }`
),
} ); } );
goToNextVariation();
} }
if ( ! numberOfVariations || numberOfVariations < 2 ) { if ( ! numberOfVariations || numberOfVariations < 2 ) {
@ -132,7 +104,7 @@ export function VariationSwitcherFooter( {
) : ( ) : (
<VariationImagePlaceholder className="woocommerce-product-variation-switcher-footer__product-image" /> <VariationImagePlaceholder className="woocommerce-product-variation-switcher-footer__product-image" />
) } ) }
{ getVariationName( previousVariation ) } { previousVariation.name }
</Button> </Button>
) : ( ) : (
<SwitcherLoadingPlaceholder position="left" /> <SwitcherLoadingPlaceholder position="left" />
@ -143,7 +115,7 @@ export function VariationSwitcherFooter( {
label={ __( 'Next', 'woocommerce' ) } label={ __( 'Next', 'woocommerce' ) }
onClick={ onNext } onClick={ onNext }
> >
{ getVariationName( nextVariation ) } { nextVariation.name }
{ nextVariation.image ? ( { nextVariation.image ? (
<img <img
alt={ nextVariation.image.alt || '' } alt={ nextVariation.image.alt || '' }

View File

@ -11,6 +11,7 @@ export type ValidationContextProps< T > = {
validatorId: string, validatorId: string,
validator: Validator< T > validator: Validator< T >
): React.Ref< HTMLElement >; ): React.Ref< HTMLElement >;
unRegisterValidator( validatorId: string ): void;
validateField( name: string ): ValidatorResponse; validateField( name: string ): ValidatorResponse;
validateAll( newData?: Partial< T > ): Promise< ValidationErrors >; validateAll( newData?: Partial< T > ): Promise< ValidationErrors >;
}; };

View File

@ -1,7 +1,7 @@
/** /**
* External dependencies * External dependencies
*/ */
import { useContext, useMemo, useState } from '@wordpress/element'; import { useContext, useMemo, useState, useEffect } from '@wordpress/element';
import { DependencyList } from 'react'; import { DependencyList } from 'react';
/** /**
@ -23,6 +23,12 @@ export function useValidation< T >(
[ validatorId, ...deps ] [ validatorId, ...deps ]
); );
useEffect( () => {
return () => {
context.unRegisterValidator( validatorId );
};
}, [] );
return { return {
ref, ref,
error: context.errors[ validatorId ], error: context.errors[ validatorId ],

View File

@ -13,6 +13,7 @@ export const ValidationContext = createContext< ValidationContextProps< any > >(
{ {
errors: {}, errors: {},
registerValidator: () => () => {}, registerValidator: () => () => {},
unRegisterValidator: () => () => {},
validateField: () => Promise.resolve( undefined ), validateField: () => Promise.resolve( undefined ),
validateAll: () => Promise.resolve( {} ), validateAll: () => Promise.resolve( {} ),
} }

View File

@ -38,6 +38,15 @@ export function ValidationProvider< T >( {
}; };
} }
function unRegisterValidator( validatorId: string ): void {
if ( validatorsRef.current[ validatorId ] ) {
delete validatorsRef.current[ validatorId ];
}
if ( fieldRefs.current[ validatorId ] ) {
delete fieldRefs.current[ validatorId ];
}
}
async function validateField( async function validateField(
validatorId: string, validatorId: string,
newData?: Partial< T > newData?: Partial< T >
@ -89,6 +98,7 @@ export function ValidationProvider< T >( {
value={ { value={ {
errors, errors,
registerValidator, registerValidator,
unRegisterValidator,
validateField, validateField,
validateAll, validateAll,
} } } }

View File

@ -2,3 +2,4 @@ export { useProductHelper as __experimentalUseProductHelper } from './use-produc
export { useFeedbackBar as __experimentalUseFeedbackBar } from './use-feedback-bar'; export { useFeedbackBar as __experimentalUseFeedbackBar } from './use-feedback-bar';
export { useVariationsOrder as __experimentalUseVariationsOrder } from './use-variations-order'; export { useVariationsOrder as __experimentalUseVariationsOrder } from './use-variations-order';
export { useCurrencyInputProps as __experimentalUseCurrencyInputProps } from './use-currency-input-props'; export { useCurrencyInputProps as __experimentalUseCurrencyInputProps } from './use-currency-input-props';
export { useVariationSwitcher as __experimentalUseVariationSwitcher } from './use-variation-switcher';

View File

@ -0,0 +1,101 @@
/**
* External dependencies
*/
import { useSelect, useDispatch } from '@wordpress/data';
import { Product } from '@woocommerce/data';
import { getNewPath, navigateTo } from '@woocommerce/navigation';
type VariationSwitcherProps = {
parentProductType?: string;
variationId?: number;
parentId?: number;
};
export function useVariationSwitcher( {
variationId,
parentId,
parentProductType,
}: VariationSwitcherProps ) {
const { invalidateResolution } = useDispatch( 'core' );
const variationValues = useSelect(
( select ) => {
if ( parentId === undefined ) {
return {};
}
const { getEntityRecord } = select( 'core' );
const parentProduct = getEntityRecord< Product >(
'postType',
parentProductType || 'product',
parentId
);
if (
variationId !== undefined &&
parentProduct &&
parentProduct.variations
) {
const activeVariationIndex =
parentProduct.variations.indexOf( variationId );
const previousVariationIndex =
activeVariationIndex > 0
? activeVariationIndex - 1
: parentProduct.variations.length - 1;
const nextVariationIndex =
activeVariationIndex !== parentProduct.variations.length - 1
? activeVariationIndex + 1
: 0;
return {
activeVariationIndex,
nextVariationIndex,
previousVariationIndex,
numberOfVariations: parentProduct.variations.length,
previousVariationId:
parentProduct.variations[ previousVariationIndex ],
nextVariationId:
parentProduct.variations[ nextVariationIndex ],
};
}
return {};
},
[ variationId, parentId ]
);
function invalidateVariationList() {
invalidateResolution( 'getEntityRecord', [
'postType',
parentProductType || 'product',
parentId,
] );
}
function goToNextVariation() {
if ( variationValues.nextVariationId === undefined ) {
return false;
}
navigateTo( {
url: getNewPath(
{},
`/product/${ parentId }/variation/${ variationValues.nextVariationId }`
),
} );
}
function goToPreviousVariation() {
if ( variationValues.previousVariationId === undefined ) {
return false;
}
navigateTo( {
url: getNewPath(
{},
`/product/${ parentId }/variation/${ variationValues.previousVariationId }`
),
} );
}
return {
...variationValues,
invalidateVariationList,
goToNextVariation,
goToPreviousVariation,
};
}

View File

@ -11,7 +11,10 @@ import {
ProductVariation, ProductVariation,
} from '@woocommerce/data'; } from '@woocommerce/data';
import { getNewPath, navigateTo } from '@woocommerce/navigation'; import { getNewPath, navigateTo } from '@woocommerce/navigation';
import { RemoveConfirmationModal } from '@woocommerce/product-editor'; import {
RemoveConfirmationModal,
__experimentalUseVariationSwitcher as useVariationSwitcher,
} from '@woocommerce/product-editor';
import { recordEvent } from '@woocommerce/tracks'; import { recordEvent } from '@woocommerce/tracks';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore No types for this exist yet. // @ts-ignore No types for this exist yet.
@ -29,6 +32,12 @@ export const DeleteVariationMenuItem = ( {
const variationId = useEntityId( 'postType', 'product_variation' ); const variationId = useEntityId( 'postType', 'product_variation' );
const { invalidateVariationList, goToNextVariation, numberOfVariations } =
useVariationSwitcher( {
parentId: productId ? parseInt( productId, 10 ) : undefined,
variationId,
} );
const [ name ] = useEntityProp< string >( const [ name ] = useEntityProp< string >(
'postType', 'postType',
'product_variation', 'product_variation',
@ -82,9 +91,14 @@ export const DeleteVariationMenuItem = ( {
setShowModal( false ); setShowModal( false );
onClose(); onClose();
invalidateVariationList();
if ( numberOfVariations && numberOfVariations > 1 ) {
goToNextVariation();
} else {
navigateTo( { navigateTo( {
url: getNewPath( {}, `/product/${ productId }` ), url: getNewPath( {}, `/product/${ productId }` ),
} ); } );
}
} ) } )
.catch( () => { .catch( () => {
createErrorNotice( createErrorNotice(

View File

@ -0,0 +1,4 @@
Significance: minor
Type: update
Redirect to next variation if deleting a variation on the edit variation page.