From 7f25060044a679fdd7157d52f5283a74381d2a53 Mon Sep 17 00:00:00 2001 From: louwie17 Date: Fri, 13 Oct 2023 20:25:58 -0300 Subject: [PATCH] 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 --- .../changelog/update-40591_validation | 4 + ...te-40591_variation_switching_when_deleting | 4 + .../variation-switcher-footer.tsx | 82 +++++--------- .../src/contexts/validation-context/types.ts | 1 + .../validation-context/use-validation.ts | 8 +- .../validation-context/validation-context.ts | 1 + .../validation-provider.tsx | 10 ++ packages/js/product-editor/src/hooks/index.ts | 1 + .../src/hooks/use-variation-switcher.ts | 101 ++++++++++++++++++ .../delete-variation-menu-item.tsx | 22 +++- ...te-40591_variation_switching_when_deleting | 4 + 11 files changed, 178 insertions(+), 60 deletions(-) create mode 100644 packages/js/product-editor/changelog/update-40591_validation create mode 100644 packages/js/product-editor/changelog/update-40591_variation_switching_when_deleting create mode 100644 packages/js/product-editor/src/hooks/use-variation-switcher.ts create mode 100644 plugins/woocommerce/changelog/update-40591_variation_switching_when_deleting diff --git a/packages/js/product-editor/changelog/update-40591_validation b/packages/js/product-editor/changelog/update-40591_validation new file mode 100644 index 00000000000..edde3d20f16 --- /dev/null +++ b/packages/js/product-editor/changelog/update-40591_validation @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Add unregister function to the validation provider and trigger this from the useValidation hook. diff --git a/packages/js/product-editor/changelog/update-40591_variation_switching_when_deleting b/packages/js/product-editor/changelog/update-40591_variation_switching_when_deleting new file mode 100644 index 00000000000..41c6961fd08 --- /dev/null +++ b/packages/js/product-editor/changelog/update-40591_variation_switching_when_deleting @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Add useVariationSwitcher hook and make use of it in the VariationSwitcherFooter. diff --git a/packages/js/product-editor/src/components/variation-switcher-footer/variation-switcher-footer.tsx b/packages/js/product-editor/src/components/variation-switcher-footer/variation-switcher-footer.tsx index 30d4eab1991..c2a516eacd4 100644 --- a/packages/js/product-editor/src/components/variation-switcher-footer/variation-switcher-footer.tsx +++ b/packages/js/product-editor/src/components/variation-switcher-footer/variation-switcher-footer.tsx @@ -6,64 +6,46 @@ import { Button } from '@wordpress/components'; import { createElement } from '@wordpress/element'; import { arrowLeft, arrowRight, Icon } from '@wordpress/icons'; import { useSelect } from '@wordpress/data'; -import { Product, ProductVariation } from '@woocommerce/data'; +import { ProductVariation } from '@woocommerce/data'; import { recordEvent } from '@woocommerce/tracks'; -import { getNewPath, navigateTo } from '@woocommerce/navigation'; /** * Internal dependencies */ import { SwitcherLoadingPlaceholder } from './switcher-loading-placeholder'; import { VariationImagePlaceholder } from './variation-image-placeholder'; +import { useVariationSwitcher } from '../../hooks/use-variation-switcher'; export type VariationSwitcherProps = { - productType?: string; + parentProductType?: string; variationId: number; parentId: number; }; -function getVariationName( variation: ProductVariation ): string { - return variation.attributes.map( ( attr ) => attr.option ).join( ', ' ); -} - export function VariationSwitcherFooter( { + parentProductType, variationId, parentId, }: VariationSwitcherProps ) { const { - previousVariation, - nextVariation, numberOfVariations, - ...variationIndexes - } = useSelect( + nextVariationId, + previousVariationId, + activeVariationIndex, + nextVariationIndex, + previousVariationIndex, + goToNextVariation, + goToPreviousVariation, + } = useVariationSwitcher( { + variationId, + parentId, + parentProductType, + } ); + const { previousVariation, nextVariation } = useSelect( ( 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 ]; - + if ( numberOfVariations && numberOfVariations > 0 ) { return { - activeVariationIndex, - nextVariationIndex, - previousVariationIndex, - numberOfVariations: parentProduct.variations.length, previousVariation: getEntityRecord< ProductVariation >( 'postType', 'product_variation', @@ -78,36 +60,26 @@ export function VariationSwitcherFooter( { } return {}; }, - [ variationId, parentId ] + [ nextVariationId, previousVariationId, numberOfVariations ] ); function onPrevious() { recordEvent( 'product_variation_switch_previous', { variation_length: numberOfVariations, variation_id: previousVariation?.id, - variation_index: variationIndexes.activeVariationIndex, - previous_variation_index: variationIndexes.previousVariationIndex, - } ); - navigateTo( { - url: getNewPath( - {}, - `/product/${ parentId }/variation/${ previousVariation?.id }` - ), + variation_index: activeVariationIndex, + previous_variation_index: previousVariationIndex, } ); + goToPreviousVariation(); } function onNext() { recordEvent( 'product_variation_switch_next', { variation_length: numberOfVariations, variation_id: nextVariation?.id, - variation_index: variationIndexes.activeVariationIndex, - next_variation_index: variationIndexes.nextVariationIndex, - } ); - navigateTo( { - url: getNewPath( - {}, - `/product/${ parentId }/variation/${ nextVariation?.id }` - ), + variation_index: activeVariationIndex, + next_variation_index: nextVariationIndex, } ); + goToNextVariation(); } if ( ! numberOfVariations || numberOfVariations < 2 ) { @@ -132,7 +104,7 @@ export function VariationSwitcherFooter( { ) : ( ) } - { getVariationName( previousVariation ) } + { previousVariation.name } ) : ( @@ -143,7 +115,7 @@ export function VariationSwitcherFooter( { label={ __( 'Next', 'woocommerce' ) } onClick={ onNext } > - { getVariationName( nextVariation ) } + { nextVariation.name } { nextVariation.image ? ( { = { validatorId: string, validator: Validator< T > ): React.Ref< HTMLElement >; + unRegisterValidator( validatorId: string ): void; validateField( name: string ): ValidatorResponse; validateAll( newData?: Partial< T > ): Promise< ValidationErrors >; }; diff --git a/packages/js/product-editor/src/contexts/validation-context/use-validation.ts b/packages/js/product-editor/src/contexts/validation-context/use-validation.ts index 3f4ed824e50..20bfbdb7cbe 100644 --- a/packages/js/product-editor/src/contexts/validation-context/use-validation.ts +++ b/packages/js/product-editor/src/contexts/validation-context/use-validation.ts @@ -1,7 +1,7 @@ /** * External dependencies */ -import { useContext, useMemo, useState } from '@wordpress/element'; +import { useContext, useMemo, useState, useEffect } from '@wordpress/element'; import { DependencyList } from 'react'; /** @@ -23,6 +23,12 @@ export function useValidation< T >( [ validatorId, ...deps ] ); + useEffect( () => { + return () => { + context.unRegisterValidator( validatorId ); + }; + }, [] ); + return { ref, error: context.errors[ validatorId ], diff --git a/packages/js/product-editor/src/contexts/validation-context/validation-context.ts b/packages/js/product-editor/src/contexts/validation-context/validation-context.ts index 750e1610a70..7cfe64d0f02 100644 --- a/packages/js/product-editor/src/contexts/validation-context/validation-context.ts +++ b/packages/js/product-editor/src/contexts/validation-context/validation-context.ts @@ -13,6 +13,7 @@ export const ValidationContext = createContext< ValidationContextProps< any > >( { errors: {}, registerValidator: () => () => {}, + unRegisterValidator: () => () => {}, validateField: () => Promise.resolve( undefined ), validateAll: () => Promise.resolve( {} ), } diff --git a/packages/js/product-editor/src/contexts/validation-context/validation-provider.tsx b/packages/js/product-editor/src/contexts/validation-context/validation-provider.tsx index 429569a63a7..c40a493b13c 100644 --- a/packages/js/product-editor/src/contexts/validation-context/validation-provider.tsx +++ b/packages/js/product-editor/src/contexts/validation-context/validation-provider.tsx @@ -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( validatorId: string, newData?: Partial< T > @@ -89,6 +98,7 @@ export function ValidationProvider< T >( { value={ { errors, registerValidator, + unRegisterValidator, validateField, validateAll, } } diff --git a/packages/js/product-editor/src/hooks/index.ts b/packages/js/product-editor/src/hooks/index.ts index 2f650780981..134765aa84b 100644 --- a/packages/js/product-editor/src/hooks/index.ts +++ b/packages/js/product-editor/src/hooks/index.ts @@ -2,3 +2,4 @@ export { useProductHelper as __experimentalUseProductHelper } from './use-produc export { useFeedbackBar as __experimentalUseFeedbackBar } from './use-feedback-bar'; export { useVariationsOrder as __experimentalUseVariationsOrder } from './use-variations-order'; export { useCurrencyInputProps as __experimentalUseCurrencyInputProps } from './use-currency-input-props'; +export { useVariationSwitcher as __experimentalUseVariationSwitcher } from './use-variation-switcher'; diff --git a/packages/js/product-editor/src/hooks/use-variation-switcher.ts b/packages/js/product-editor/src/hooks/use-variation-switcher.ts new file mode 100644 index 00000000000..28ec7ad1d00 --- /dev/null +++ b/packages/js/product-editor/src/hooks/use-variation-switcher.ts @@ -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, + }; +} diff --git a/plugins/woocommerce-admin/client/products/fills/more-menu-items/delete-variation-menu-item.tsx b/plugins/woocommerce-admin/client/products/fills/more-menu-items/delete-variation-menu-item.tsx index 67c200f753e..2de326af02a 100644 --- a/plugins/woocommerce-admin/client/products/fills/more-menu-items/delete-variation-menu-item.tsx +++ b/plugins/woocommerce-admin/client/products/fills/more-menu-items/delete-variation-menu-item.tsx @@ -11,7 +11,10 @@ import { ProductVariation, } from '@woocommerce/data'; 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'; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore No types for this exist yet. @@ -29,6 +32,12 @@ export const DeleteVariationMenuItem = ( { const variationId = useEntityId( 'postType', 'product_variation' ); + const { invalidateVariationList, goToNextVariation, numberOfVariations } = + useVariationSwitcher( { + parentId: productId ? parseInt( productId, 10 ) : undefined, + variationId, + } ); + const [ name ] = useEntityProp< string >( 'postType', 'product_variation', @@ -82,9 +91,14 @@ export const DeleteVariationMenuItem = ( { setShowModal( false ); onClose(); - navigateTo( { - url: getNewPath( {}, `/product/${ productId }` ), - } ); + invalidateVariationList(); + if ( numberOfVariations && numberOfVariations > 1 ) { + goToNextVariation(); + } else { + navigateTo( { + url: getNewPath( {}, `/product/${ productId }` ), + } ); + } } ) .catch( () => { createErrorNotice( diff --git a/plugins/woocommerce/changelog/update-40591_variation_switching_when_deleting b/plugins/woocommerce/changelog/update-40591_variation_switching_when_deleting new file mode 100644 index 00000000000..0d03d891294 --- /dev/null +++ b/plugins/woocommerce/changelog/update-40591_variation_switching_when_deleting @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Redirect to next variation if deleting a variation on the edit variation page.