diff --git a/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/add-to-cart/product-types/variable/variation-attributes/attribute-select-control.js b/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/add-to-cart/product-types/variable/variation-attributes/attribute-select-control.js index adaf84d472e..9ba96579089 100644 --- a/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/add-to-cart/product-types/variable/variation-attributes/attribute-select-control.js +++ b/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/add-to-cart/product-types/variable/variation-attributes/attribute-select-control.js @@ -6,10 +6,9 @@ import { decodeEntities } from '@wordpress/html-entities'; import { SelectControl } from 'wordpress-components'; import { useEffect } from 'react'; import classnames from 'classnames'; -import { - ValidationInputError, - useValidationContext, -} from '@woocommerce/base-context'; +import { ValidationInputError } from '@woocommerce/base-components/validation-input-error'; +import { VALIDATION_STORE_KEY } from '@woocommerce/block-data'; +import { useDispatch, useSelect } from '@wordpress/data'; // Default option for select boxes. const selectAnOption = { @@ -32,8 +31,16 @@ const AttributeSelectControl = ( { 'woo-gutenberg-products-block' ), } ) => { - const { getValidationError, setValidationErrors, clearValidationError } = - useValidationContext(); + const { setValidationErrors, clearValidationError } = useDispatch( + VALIDATION_STORE_KEY + ); + + const { getValidationError } = useSelect( ( select ) => { + const store = select( VALIDATION_STORE_KEY ); + return { + getValidationError: store.getValidationError(), + }; + } ); const errorId = attributeName; const error = getValidationError( errorId ) || {}; diff --git a/plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/address-form/address-form.tsx b/plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/address-form/address-form.tsx index 3b3a4e238be..4bec91362dc 100644 --- a/plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/address-form/address-form.tsx +++ b/plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/address-form/address-form.tsx @@ -10,7 +10,6 @@ import { BillingStateInput, ShippingStateInput, } from '@woocommerce/base-components/state-input'; -import { useValidationContext } from '@woocommerce/base-context'; import { useEffect, useMemo } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; import { withInstanceId } from '@wordpress/compose'; @@ -22,6 +21,11 @@ import { defaultAddressFields, EnteredAddress, } from '@woocommerce/settings'; +import { useSelect, useDispatch } from '@wordpress/data'; +import { + VALIDATION_STORE_KEY, + FieldValidationStatus, +} from '@woocommerce/block-data'; /** * Internal dependencies @@ -32,7 +36,9 @@ import prepareAddressFields from './prepare-address-fields'; // values without having set the country first, show an error. const validateShippingCountry = ( values: EnteredAddress, - setValidationErrors: ( errors: Record< string, unknown > ) => void, + setValidationErrors: ( + errors: Record< string, FieldValidationStatus > + ) => void, clearValidationError: ( error: string ) => void, hasValidationError: boolean ): void => { @@ -87,8 +93,14 @@ const AddressForm = ( { type = 'shipping', values, }: AddressFormProps ): JSX.Element => { - const { getValidationError, setValidationErrors, clearValidationError } = - useValidationContext(); + const { setValidationErrors, clearValidationError } = useDispatch( + VALIDATION_STORE_KEY + ); + + const getValidationError = useSelect( ( select ) => { + const store = select( VALIDATION_STORE_KEY ); + return store.getValidationError(); + } ); const currentFields = useShallowEqual( fields ); diff --git a/plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/shipping-calculator/address.tsx b/plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/shipping-calculator/address.tsx index 8daece96a6a..a4eafb9ed97 100644 --- a/plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/shipping-calculator/address.tsx +++ b/plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/shipping-calculator/address.tsx @@ -5,8 +5,9 @@ import { __ } from '@wordpress/i18n'; import Button from '@woocommerce/base-components/button'; import { useState } from '@wordpress/element'; import isShallowEqual from '@wordpress/is-shallow-equal'; -import { useValidationContext } from '@woocommerce/base-context'; import type { EnteredAddress, AddressFields } from '@woocommerce/settings'; +import { VALIDATION_STORE_KEY } from '@woocommerce/block-data'; +import { useDispatch, useSelect } from '@wordpress/data'; /** * Internal dependencies @@ -25,12 +26,18 @@ const ShippingCalculatorAddress = ( { addressFields, }: ShippingCalculatorAddressProps ): JSX.Element => { const [ address, setAddress ] = useState( initialAddress ); - const { hasValidationErrors, showAllValidationErrors } = - useValidationContext(); + const { showAllValidationErrors } = useDispatch( VALIDATION_STORE_KEY ); + + const { hasValidationErrors } = useSelect( ( select ) => { + const store = select( VALIDATION_STORE_KEY ); + return { + hasValidationErrors: store.hasValidationErrors, + }; + } ); const validateSubmit = () => { showAllValidationErrors(); - return ! hasValidationErrors; + return ! hasValidationErrors(); }; return ( diff --git a/plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/totals/coupon/index.tsx b/plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/totals/coupon/index.tsx index 87b6d29ed41..1d31203a279 100644 --- a/plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/totals/coupon/index.tsx +++ b/plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/totals/coupon/index.tsx @@ -4,15 +4,14 @@ import { __ } from '@wordpress/i18n'; import { useState, useEffect, useRef } from '@wordpress/element'; import Button from '@woocommerce/base-components/button'; -import { ValidatedTextInput } from '@woocommerce/base-components/text-input'; +import { Panel } from '@woocommerce/blocks-checkout'; import Label from '@woocommerce/base-components/label'; import LoadingMask from '@woocommerce/base-components/loading-mask'; import { withInstanceId } from '@wordpress/compose'; -import { - ValidationInputError, - useValidationContext, -} from '@woocommerce/base-context'; -import { Panel } from '@woocommerce/blocks-checkout'; +import { ValidatedTextInput } from '@woocommerce/base-components/text-input'; +import ValidationInputError from '@woocommerce/base-components/validation-input-error'; +import { useSelect } from '@wordpress/data'; +import { VALIDATION_STORE_KEY } from '@woocommerce/block-data'; /** * Internal dependencies @@ -46,7 +45,16 @@ export const TotalsCoupon = ( { }: TotalsCouponProps ): JSX.Element => { const [ couponValue, setCouponValue ] = useState( '' ); const currentIsLoading = useRef( false ); - const { getValidationError, getValidationErrorId } = useValidationContext(); + const { getValidationError, getValidationErrorId } = useSelect( + ( select ) => { + const store = select( VALIDATION_STORE_KEY ); + return { + getValidationError: store.getValidationError(), + getValidationErrorId: store.getValidationErrorId(), + }; + } + ); + const validationError = getValidationError( 'coupon' ); useEffect( () => { diff --git a/plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/totals/coupon/stories/index.tsx b/plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/totals/coupon/stories/index.tsx index cc4827e1bb5..d87bef2c6d4 100644 --- a/plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/totals/coupon/stories/index.tsx +++ b/plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/totals/coupon/stories/index.tsx @@ -3,11 +3,9 @@ */ import { useArgs } from '@storybook/client-api'; import { Story, Meta } from '@storybook/react'; -import { - useValidationContext, - ValidationContextProvider, -} from '@woocommerce/base-context'; import { INTERACTION_TIMEOUT } from '@woocommerce/storybook-controls'; +import { useDispatch } from '@wordpress/data'; +import { VALIDATION_STORE_KEY } from '@woocommerce/block-data'; /** * Internal dependencies @@ -52,7 +50,7 @@ LoadingState.args = { }; export const ErrorState: Story< TotalsCouponProps > = ( args ) => { - const { setValidationErrors } = useValidationContext(); + const { setValidationErrors } = useDispatch( VALIDATION_STORE_KEY ); setValidationErrors( { coupon: INVALID_COUPON_ERROR } ); @@ -61,10 +59,6 @@ export const ErrorState: Story< TotalsCouponProps > = ( args ) => { ErrorState.decorators = [ ( StoryComponent ) => { - return ( - - - - ); + return ; }, ]; diff --git a/plugins/woocommerce-blocks/assets/js/base/components/combobox/index.tsx b/plugins/woocommerce-blocks/assets/js/base/components/combobox/index.tsx index f0a492fa948..01d3a1fe181 100644 --- a/plugins/woocommerce-blocks/assets/js/base/components/combobox/index.tsx +++ b/plugins/woocommerce-blocks/assets/js/base/components/combobox/index.tsx @@ -6,11 +6,10 @@ import { __ } from '@wordpress/i18n'; import { useEffect, useRef } from '@wordpress/element'; import { withInstanceId } from '@wordpress/compose'; import { ComboboxControl } from 'wordpress-components'; -import { - ValidationInputError, - useValidationContext, -} from '@woocommerce/base-context'; +import { ValidationInputError } from '@woocommerce/base-components/validation-input-error'; import { isObject } from '@woocommerce/types'; +import { useDispatch, useSelect } from '@wordpress/data'; +import { VALIDATION_STORE_KEY } from '@woocommerce/block-data'; /** * Internal dependencies @@ -55,8 +54,13 @@ const Combobox = ( { instanceId = '0', autoComplete = 'off', }: ComboboxProps ): JSX.Element => { - const { getValidationError, setValidationErrors, clearValidationError } = - useValidationContext(); + const { setValidationErrors, clearValidationError } = useDispatch( + VALIDATION_STORE_KEY + ); + const getValidationError = useSelect( ( select ) => { + const store = select( VALIDATION_STORE_KEY ); + return store.getValidationError(); + } ); const controlRef = useRef< HTMLDivElement >( null ); const controlId = id || 'control-' + instanceId; diff --git a/plugins/woocommerce-blocks/assets/js/base/components/country-input/stories/index.tsx b/plugins/woocommerce-blocks/assets/js/base/components/country-input/stories/index.tsx index bc2ffa59475..bf2cc4c083c 100644 --- a/plugins/woocommerce-blocks/assets/js/base/components/country-input/stories/index.tsx +++ b/plugins/woocommerce-blocks/assets/js/base/components/country-input/stories/index.tsx @@ -2,11 +2,9 @@ * External dependencies */ import { Story, Meta } from '@storybook/react'; -import { - useValidationContext, - ValidationContextProvider, -} from '@woocommerce/base-context'; +import { useDispatch } from '@wordpress/data'; import { useState, useEffect } from '@wordpress/element'; +import { VALIDATION_STORE_KEY } from '@woocommerce/block-data'; /** * Internal dependencies @@ -31,21 +29,16 @@ export default { options: { table: { disable: true } }, value: { control: false }, }, - decorators: [ - ( StoryComponent ) => ( - - - - ), - ], + decorators: [ ( StoryComponent ) => ], } as Meta< CountryInputWithCountriesProps >; const Template: Story< CountryInputWithCountriesProps > = ( args ) => { const [ selectedCountry, selectCountry ] = useState< CountryCode | '' >( '' ); - const { clearValidationError, showValidationError } = - useValidationContext(); + const { clearValidationError, showValidationError } = useDispatch( + VALIDATION_STORE_KEY + ); useEffect( () => { showValidationError( 'country' ); diff --git a/plugins/woocommerce-blocks/assets/js/base/components/state-input/state-input.tsx b/plugins/woocommerce-blocks/assets/js/base/components/state-input/state-input.tsx index 9018bfb2316..4ef0eee4fe8 100644 --- a/plugins/woocommerce-blocks/assets/js/base/components/state-input/state-input.tsx +++ b/plugins/woocommerce-blocks/assets/js/base/components/state-input/state-input.tsx @@ -5,11 +5,11 @@ import { __ } from '@wordpress/i18n'; import { decodeEntities } from '@wordpress/html-entities'; import { useCallback, useMemo, useEffect, useRef } from '@wordpress/element'; import classnames from 'classnames'; +import { ValidatedTextInput } from '@woocommerce/base-components/text-input'; /** * Internal dependencies */ -import { ValidatedTextInput } from '../text-input'; import Combobox from '../combobox'; import './style.scss'; import type { StateInputWithStatesProps } from './StateInputProps'; diff --git a/plugins/woocommerce-blocks/assets/js/base/components/text-input/validated-text-input.tsx b/plugins/woocommerce-blocks/assets/js/base/components/text-input/validated-text-input.tsx index 536fed53c05..374393585d1 100644 --- a/plugins/woocommerce-blocks/assets/js/base/components/text-input/validated-text-input.tsx +++ b/plugins/woocommerce-blocks/assets/js/base/components/text-input/validated-text-input.tsx @@ -4,33 +4,21 @@ import { __ } from '@wordpress/i18n'; import { useCallback, useRef, useEffect, useState } from 'react'; import classnames from 'classnames'; -import { - ValidationInputError, - useValidationContext, -} from '@woocommerce/base-context'; import { withInstanceId } from '@wordpress/compose'; import { isString } from '@woocommerce/types'; +import { dispatch, useSelect } from '@wordpress/data'; +import { VALIDATION_STORE_KEY } from '@woocommerce/block-data'; /** * Internal dependencies */ import TextInput from './text-input'; import './style.scss'; +import { ValidationInputError } from '../validation-input-error'; -interface ValidatedTextInputPropsWithId { - instanceId?: string; - id: string; -} - -interface ValidatedTextInputPropsWithInstanceId { - instanceId: string; +interface ValidatedTextInputProps { id?: string; -} - -type ValidatedTextInputProps = ( - | ValidatedTextInputPropsWithId - | ValidatedTextInputPropsWithInstanceId - ) & { + instanceId: string; className?: string; ariaDescribedBy?: string; errorId?: string; @@ -39,7 +27,7 @@ type ValidatedTextInputProps = ( errorMessage?: string; onChange: ( newValue: string ) => void; value: string; -}; +} const ValidatedTextInput = ( { className, @@ -53,20 +41,26 @@ const ValidatedTextInput = ( { errorMessage: passedErrorMessage = '', value = '', ...rest -}: ValidatedTextInputProps ) => { +}: ValidatedTextInputProps ): JSX.Element => { const [ isPristine, setIsPristine ] = useState( true ); const inputRef = useRef< HTMLInputElement >( null ); - const { - getValidationError, - hideValidationError, - setValidationErrors, - clearValidationError, - getValidationErrorId, - } = useValidationContext(); + + const { setValidationErrors, hideValidationError, clearValidationError } = + dispatch( VALIDATION_STORE_KEY ); const textInputId = typeof id !== 'undefined' ? id : 'textinput-' + instanceId; const errorIdString = errorId !== undefined ? errorId : textInputId; + const { getValidationError, getValidationErrorId } = useSelect( + ( select ) => { + const store = select( VALIDATION_STORE_KEY ); + return { + getValidationError: store.getValidationError(), + getValidationErrorId: store.getValidationErrorId(), + }; + } + ); + const validateInput = useCallback( ( errorsHidden = true ) => { const inputObject = inputRef.current || null; @@ -79,7 +73,7 @@ const ValidatedTextInput = ( { if ( inputIsValid ) { clearValidationError( errorIdString ); } else { - setValidationErrors( { + const validationErrors = { [ errorIdString ]: { message: inputObject.validationMessage || @@ -89,7 +83,8 @@ const ValidatedTextInput = ( { ), hidden: errorsHidden, }, - } ); + }; + setValidationErrors( validationErrors ); } }, [ clearValidationError, errorIdString, setValidationErrors ] @@ -129,17 +124,13 @@ const ValidatedTextInput = ( { }; }, [ clearValidationError, errorIdString ] ); - // @todo - When useValidationContext is converted to TypeScript, remove this cast and use the correct type. - const errorMessage = ( getValidationError( errorIdString ) || {} ) as { - message?: string; - hidden?: boolean; - }; + const errorMessage = getValidationError( errorIdString ); if ( isString( passedErrorMessage ) && passedErrorMessage !== '' ) { errorMessage.message = passedErrorMessage; } - const hasError = errorMessage.message && ! errorMessage.hidden; + const hasError = errorMessage?.message && ! errorMessage?.hidden; const describedBy = showError && hasError && getValidationErrorId( errorIdString ) ? getValidationErrorId( errorIdString ) diff --git a/plugins/woocommerce-blocks/assets/js/base/components/tsconfig.json b/plugins/woocommerce-blocks/assets/js/base/components/tsconfig.json index 4488bff8cf0..6ba83ca004f 100644 --- a/plugins/woocommerce-blocks/assets/js/base/components/tsconfig.json +++ b/plugins/woocommerce-blocks/assets/js/base/components/tsconfig.json @@ -12,6 +12,7 @@ "../hocs", "../../atomic/utils", "../../atomic/blocks/component-init.js", + "../../data", "../../shared/context" ], "exclude": [ "**/test/**" ] diff --git a/plugins/woocommerce-blocks/assets/js/base/components/validation-input-error/index.tsx b/plugins/woocommerce-blocks/assets/js/base/components/validation-input-error/index.tsx new file mode 100644 index 00000000000..bd03afee153 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/base/components/validation-input-error/index.tsx @@ -0,0 +1,50 @@ +/** + * External dependencies + */ +import { useSelect } from '@wordpress/data'; +import { VALIDATION_STORE_KEY } from '@woocommerce/block-data'; + +/** + * Internal dependencies + */ +import './style.scss'; + +interface ValidationInputErrorProps { + errorMessage?: string; + propertyName?: string; + elementId?: string; +} + +export const ValidationInputError = ( { + errorMessage = '', + propertyName = '', + elementId = '', +}: ValidationInputErrorProps ): JSX.Element | null => { + const { getValidationError, getValidationErrorId } = useSelect( + ( select ) => { + const store = select( VALIDATION_STORE_KEY ); + return { + getValidationError: store.getValidationError(), + getValidationErrorId: store.getValidationErrorId(), + }; + } + ); + const validationError = getValidationError( propertyName ); + + if ( ! errorMessage || typeof errorMessage !== 'string' ) { + const error = validationError || {}; + if ( error.message && ! error.hidden ) { + errorMessage = error.message; + } else { + return null; + } + } + + return ( +
+

{ errorMessage }

+
+ ); +}; + +export default ValidationInputError; diff --git a/plugins/woocommerce-blocks/assets/js/base/context/providers/validation/components/validation-input-error/style.scss b/plugins/woocommerce-blocks/assets/js/base/components/validation-input-error/style.scss similarity index 100% rename from plugins/woocommerce-blocks/assets/js/base/context/providers/validation/components/validation-input-error/style.scss rename to plugins/woocommerce-blocks/assets/js/base/components/validation-input-error/style.scss diff --git a/plugins/woocommerce-blocks/assets/js/base/context/hooks/cart/use-store-cart-coupons.ts b/plugins/woocommerce-blocks/assets/js/base/context/hooks/cart/use-store-cart-coupons.ts index aa5cd0bf851..b3ff5960823 100644 --- a/plugins/woocommerce-blocks/assets/js/base/context/hooks/cart/use-store-cart-coupons.ts +++ b/plugins/woocommerce-blocks/assets/js/base/context/hooks/cart/use-store-cart-coupons.ts @@ -5,7 +5,10 @@ */ import { __, sprintf } from '@wordpress/i18n'; import { useDispatch, useSelect } from '@wordpress/data'; -import { CART_STORE_KEY as storeKey } from '@woocommerce/block-data'; +import { + CART_STORE_KEY as storeKey, + VALIDATION_STORE_KEY, +} from '@woocommerce/block-data'; import { decodeEntities } from '@wordpress/html-entities'; import type { StoreCartCoupon } from '@woocommerce/types'; @@ -13,7 +16,6 @@ import type { StoreCartCoupon } from '@woocommerce/types'; * Internal dependencies */ import { useStoreCart } from './use-store-cart'; -import { useValidationContext } from '../../providers/validation'; /** * This is a custom hook for loading the Store API /cart/coupons endpoint and an @@ -27,7 +29,7 @@ export const useStoreCartCoupons = ( context = '' ): StoreCartCoupon => { const { cartCoupons, cartIsLoading } = useStoreCart(); const { createErrorNotice } = useDispatch( 'core/notices' ); const { createNotice } = useDispatch( 'core/notices' ); - const { setValidationErrors } = useValidationContext(); + const { setValidationErrors } = useDispatch( VALIDATION_STORE_KEY ); const { applyCoupon, diff --git a/plugins/woocommerce-blocks/assets/js/base/context/hooks/payment-methods/use-payment-method-interface.ts b/plugins/woocommerce-blocks/assets/js/base/context/hooks/payment-methods/use-payment-method-interface.ts index 309185629e4..5a1f73e3d79 100644 --- a/plugins/woocommerce-blocks/assets/js/base/context/hooks/payment-methods/use-payment-method-interface.ts +++ b/plugins/woocommerce-blocks/assets/js/base/context/hooks/payment-methods/use-payment-method-interface.ts @@ -12,11 +12,11 @@ import LoadingMask from '@woocommerce/base-components/loading-mask'; import type { PaymentMethodInterface } from '@woocommerce/types'; import { useSelect } from '@wordpress/data'; import { CHECKOUT_STORE_KEY } from '@woocommerce/block-data'; +import { ValidationInputError } from '@woocommerce/base-components/validation-input-error'; /** * Internal dependencies */ -import { ValidationInputError } from '../../providers/validation'; import { useStoreCart } from '../cart/use-store-cart'; import { useStoreCartCoupons } from '../cart/use-store-cart-coupons'; import { useEmitResponse } from '../use-emit-response'; diff --git a/plugins/woocommerce-blocks/assets/js/base/context/hooks/use-validation.ts b/plugins/woocommerce-blocks/assets/js/base/context/hooks/use-validation.ts index 8145d5a6dd3..a4902ba311f 100644 --- a/plugins/woocommerce-blocks/assets/js/base/context/hooks/use-validation.ts +++ b/plugins/woocommerce-blocks/assets/js/base/context/hooks/use-validation.ts @@ -6,27 +6,31 @@ import type { ValidationData, ValidationContextError, } from '@woocommerce/type-defs/contexts'; - -/** - * Internal dependencies - */ -import { useValidationContext } from '../providers/validation/'; +import { useDispatch, useSelect } from '@wordpress/data'; +import { VALIDATION_STORE_KEY } from '@woocommerce/block-data'; /** * Custom hook for setting for adding errors to the validation system. */ export const useValidation = (): ValidationData => { const { - hasValidationErrors, - getValidationError, clearValidationError, hideValidationError, setValidationErrors, - } = useValidationContext(); + } = useDispatch( VALIDATION_STORE_KEY ); + const { hasValidationErrors, getValidationError } = useSelect( + ( select ) => { + const store = select( VALIDATION_STORE_KEY ); + return { + hasValidationErrors: store.hasValidationErrors, + getValidationError: store.getValidationError(), + }; + } + ); const prefix = 'extensions-errors'; return { - hasValidationErrors, + hasValidationErrors: hasValidationErrors(), getValidationError: useCallback( ( validationErrorId: string ) => getValidationError( `${ prefix }-${ validationErrorId }` ), diff --git a/plugins/woocommerce-blocks/assets/js/base/context/providers/add-to-cart-form/form-state/index.js b/plugins/woocommerce-blocks/assets/js/base/context/providers/add-to-cart-form/form-state/index.js index 4f53876d59d..916dbd3b95e 100644 --- a/plugins/woocommerce-blocks/assets/js/base/context/providers/add-to-cart-form/form-state/index.js +++ b/plugins/woocommerce-blocks/assets/js/base/context/providers/add-to-cart-form/form-state/index.js @@ -15,6 +15,8 @@ import { productSupportsAddToCartForm, } from '@woocommerce/base-utils'; import { useDispatch } from '@wordpress/data'; +import { VALIDATION_STORE_KEY } from '@woocommerce/block-data'; + /** * Internal dependencies */ @@ -28,7 +30,6 @@ import { emitEventWithAbort, reducer as emitReducer, } from './event-emit'; -import { useValidationContext } from '../../validation'; import { useEmitResponse } from '../../../hooks/use-emit-response'; import { removeNoticesByStatus } from '../../../../../utils/notices'; @@ -100,9 +101,12 @@ export const AddToCartFormStateContextProvider = ( { const [ observers, observerDispatch ] = useReducer( emitReducer, {} ); const currentObservers = useShallowEqual( observers ); const { createErrorNotice } = useDispatch( 'core/notices' ); - const { setValidationErrors } = useValidationContext(); - const { isSuccessResponse, isErrorResponse, isFailResponse } = - useEmitResponse(); + const { setValidationErrors } = useDispatch( VALIDATION_STORE_KEY ); + const { + isSuccessResponse, + isErrorResponse, + isFailResponse, + } = useEmitResponse(); /** * @type {AddToCartFormEventRegistration} diff --git a/plugins/woocommerce-blocks/assets/js/base/context/providers/add-to-cart-form/form/index.js b/plugins/woocommerce-blocks/assets/js/base/context/providers/add-to-cart-form/form/index.js index 817d17405d3..16ae0ea744f 100644 --- a/plugins/woocommerce-blocks/assets/js/base/context/providers/add-to-cart-form/form/index.js +++ b/plugins/woocommerce-blocks/assets/js/base/context/providers/add-to-cart-form/form/index.js @@ -2,7 +2,6 @@ * Internal dependencies */ import { AddToCartFormStateContextProvider } from '../form-state'; -import { ValidationContextProvider } from '../../validation'; import FormSubmit from './submit'; /** @@ -21,14 +20,12 @@ export const AddToCartFormContextProvider = ( { showFormElements, } ) => { return ( - - - { children } - - - + + { children } + + ); }; diff --git a/plugins/woocommerce-blocks/assets/js/base/context/providers/add-to-cart-form/form/submit/index.js b/plugins/woocommerce-blocks/assets/js/base/context/providers/add-to-cart-form/form/submit/index.js index ba0c0736af9..601ac9acb6d 100644 --- a/plugins/woocommerce-blocks/assets/js/base/context/providers/add-to-cart-form/form/submit/index.js +++ b/plugins/woocommerce-blocks/assets/js/base/context/providers/add-to-cart-form/form/submit/index.js @@ -6,13 +6,13 @@ import triggerFetch from '@wordpress/api-fetch'; import { useEffect, useCallback, useState } from '@wordpress/element'; import { decodeEntities } from '@wordpress/html-entities'; import { triggerAddedToCartEvent } from '@woocommerce/base-utils'; -import { useDispatch } from '@wordpress/data'; +import { useDispatch, useSelect } from '@wordpress/data'; +import { VALIDATION_STORE_KEY } from '@woocommerce/block-data'; /** * Internal dependencies */ import { useAddToCartFormContext } from '../../form-state'; -import { useValidationContext } from '../../../validation'; import { useStoreCart } from '../../../../hooks/cart/use-store-cart'; /** @@ -30,15 +30,18 @@ const FormSubmit = () => { isProcessing, requestParams, } = useAddToCartFormContext(); - const { hasValidationErrors, showAllValidationErrors } = - useValidationContext(); + const { showAllValidationErrors } = useDispatch( VALIDATION_STORE_KEY ); + const hasValidationErrors = useSelect( ( select ) => { + const store = select( VALIDATION_STORE_KEY ); + return store.hasValidationErrors; + } ); const { createErrorNotice, removeNotice } = useDispatch( 'core/notices' ); const { receiveCart } = useStoreCart(); const [ isSubmitting, setIsSubmitting ] = useState( false ); const doSubmit = ! hasError && isProcessing; const checkValidationContext = useCallback( () => { - if ( hasValidationErrors ) { + if ( hasValidationErrors() ) { showAllValidationErrors(); return { type: 'error', diff --git a/plugins/woocommerce-blocks/assets/js/base/context/providers/cart-checkout/checkout-events/index.tsx b/plugins/woocommerce-blocks/assets/js/base/context/providers/cart-checkout/checkout-events/index.tsx index 7aad5b14623..bc4aa16c898 100644 --- a/plugins/woocommerce-blocks/assets/js/base/context/providers/cart-checkout/checkout-events/index.tsx +++ b/plugins/woocommerce-blocks/assets/js/base/context/providers/cart-checkout/checkout-events/index.tsx @@ -14,7 +14,10 @@ import { import { usePrevious } from '@woocommerce/base-hooks'; import deprecated from '@wordpress/deprecated'; import { useDispatch, useSelect } from '@wordpress/data'; -import { CHECKOUT_STORE_KEY } from '@woocommerce/block-data'; +import { + CHECKOUT_STORE_KEY, + VALIDATION_STORE_KEY, +} from '@woocommerce/block-data'; /** * Internal dependencies @@ -22,7 +25,6 @@ import { CHECKOUT_STORE_KEY } from '@woocommerce/block-data'; import type { CheckoutEventsContextType } from './types'; import { useEventEmitters, reducer as emitReducer } from './event-emit'; import { STATUS } from '../../../../../data/checkout/constants'; -import { useValidationContext } from '../../validation'; import { useStoreEvents } from '../../../hooks/use-store-events'; import { useCheckoutNotices } from '../../../hooks/use-checkout-notices'; import { useEmitResponse } from '../../../hooks/use-emit-response'; @@ -64,7 +66,7 @@ export const CheckoutEventsProvider = ( { checkoutActions.setRedirectUrl( redirectUrl ); } - const { setValidationErrors } = useValidationContext(); + const { setValidationErrors } = useDispatch( VALIDATION_STORE_KEY ); const { createErrorNotice } = useDispatch( 'core/notices' ); const { dispatchCheckoutEvent } = useStoreEvents(); diff --git a/plugins/woocommerce-blocks/assets/js/base/context/providers/cart-checkout/checkout-processor.js b/plugins/woocommerce-blocks/assets/js/base/context/providers/cart-checkout/checkout-processor.js index aad14e660ca..2a34e52c1e2 100644 --- a/plugins/woocommerce-blocks/assets/js/base/context/providers/cart-checkout/checkout-processor.js +++ b/plugins/woocommerce-blocks/assets/js/base/context/providers/cart-checkout/checkout-processor.js @@ -15,7 +15,10 @@ import { formatStoreApiErrorMessage, } from '@woocommerce/base-utils'; import { useDispatch, useSelect } from '@wordpress/data'; -import { CHECKOUT_STORE_KEY } from '@woocommerce/block-data'; +import { + CHECKOUT_STORE_KEY, + VALIDATION_STORE_KEY, +} from '@woocommerce/block-data'; /** * Internal dependencies @@ -25,9 +28,9 @@ import { useCheckoutEventsContext } from './checkout-events'; import { useShippingDataContext } from './shipping'; import { useCustomerDataContext } from './customer'; import { usePaymentMethodDataContext } from './payment-methods'; -import { useValidationContext } from '../validation'; import { useStoreCart } from '../../hooks/cart/use-store-cart'; import { useStoreNoticesContext } from '../store-notices'; + /** * CheckoutProcessor component. * @@ -58,7 +61,9 @@ const CheckoutProcessor = () => { const { setHasError, processCheckoutResponse } = useDispatch( CHECKOUT_STORE_KEY ); - const { hasValidationErrors } = useValidationContext(); + const hasValidationErrors = useSelect( + ( select ) => select( VALIDATION_STORE_KEY ).hasValidationErrors + ); const { shippingErrorStatus } = useShippingDataContext(); const { billingAddress, shippingAddress } = useCustomerDataContext(); const { cartNeedsPayment, cartNeedsShipping, receiveCart } = useStoreCart(); @@ -87,7 +92,7 @@ const CheckoutProcessor = () => { }, [ activePaymentMethod, expressPaymentMethods, paymentMethods ] ); const checkoutWillHaveError = - ( hasValidationErrors && ! isExpressPaymentMethodActive ) || + ( hasValidationErrors() && ! isExpressPaymentMethodActive ) || currentPaymentStatus.hasError || shippingErrorStatus.hasError; @@ -128,7 +133,7 @@ const CheckoutProcessor = () => { }, [ billingAddress, shippingAddress, redirectUrl ] ); const checkValidation = useCallback( () => { - if ( hasValidationErrors ) { + if ( hasValidationErrors() ) { return false; } if ( currentPaymentStatus.hasError ) { diff --git a/plugins/woocommerce-blocks/assets/js/base/context/providers/cart-checkout/payment-methods/payment-method-data-context.tsx b/plugins/woocommerce-blocks/assets/js/base/context/providers/cart-checkout/payment-methods/payment-method-data-context.tsx index 11c334872e4..7761bd53c7b 100644 --- a/plugins/woocommerce-blocks/assets/js/base/context/providers/cart-checkout/payment-methods/payment-method-data-context.tsx +++ b/plugins/woocommerce-blocks/assets/js/base/context/providers/cart-checkout/payment-methods/payment-method-data-context.tsx @@ -12,7 +12,10 @@ import { } from '@wordpress/element'; import { objectHasProp } from '@woocommerce/types'; import { useDispatch, useSelect } from '@wordpress/data'; -import { CHECKOUT_STORE_KEY } from '@woocommerce/block-data'; +import { + CHECKOUT_STORE_KEY, + VALIDATION_STORE_KEY, +} from '@woocommerce/block-data'; /** * Internal dependencies @@ -39,7 +42,6 @@ import { emitEventWithAbort, reducer as emitReducer, } from './event-emit'; -import { useValidationContext } from '../../validation'; import { useEmitResponse } from '../../../hooks/use-emit-response'; import { getCustomerPaymentMethods } from './utils'; @@ -77,9 +79,10 @@ export const PaymentMethodDataProvider = ( { }; } ); const { isEditor, getPreviewData } = useEditorContext(); - const { setValidationErrors } = useValidationContext(); - const { createErrorNotice: addErrorNotice, removeNotice } = - useDispatch( 'core/notices' ); + const { setValidationErrors } = useDispatch( VALIDATION_STORE_KEY ); + const { createErrorNotice: addErrorNotice, removeNotice } = useDispatch( + 'core/notices' + ); const { isSuccessResponse, isErrorResponse, diff --git a/plugins/woocommerce-blocks/assets/js/base/context/providers/index.js b/plugins/woocommerce-blocks/assets/js/base/context/providers/index.js index c0214fcb255..88dacc9223e 100644 --- a/plugins/woocommerce-blocks/assets/js/base/context/providers/index.js +++ b/plugins/woocommerce-blocks/assets/js/base/context/providers/index.js @@ -3,7 +3,6 @@ export * from './add-to-cart-form'; export * from './cart-checkout'; export * from './store-notices'; export * from './store-snackbar-notices'; -export * from './validation'; export * from './container-width-context'; export * from './editor-context'; export * from './query-state-context'; diff --git a/plugins/woocommerce-blocks/assets/js/base/context/providers/validation/components/index.js b/plugins/woocommerce-blocks/assets/js/base/context/providers/validation/components/index.js deleted file mode 100644 index ab9c392a215..00000000000 --- a/plugins/woocommerce-blocks/assets/js/base/context/providers/validation/components/index.js +++ /dev/null @@ -1 +0,0 @@ -export * from './validation-input-error'; diff --git a/plugins/woocommerce-blocks/assets/js/base/context/providers/validation/components/validation-input-error/index.js b/plugins/woocommerce-blocks/assets/js/base/context/providers/validation/components/validation-input-error/index.js deleted file mode 100644 index fbeca642859..00000000000 --- a/plugins/woocommerce-blocks/assets/js/base/context/providers/validation/components/validation-input-error/index.js +++ /dev/null @@ -1,41 +0,0 @@ -/** - * External dependencies - */ -import PropTypes from 'prop-types'; - -/** - * Internal dependencies - */ -import { useValidationContext } from '../../context'; -import './style.scss'; - -export const ValidationInputError = ( { - errorMessage = '', - propertyName = '', - elementId = '', -} ) => { - const { getValidationError, getValidationErrorId } = useValidationContext(); - - if ( ! errorMessage || typeof errorMessage !== 'string' ) { - const error = getValidationError( propertyName ) || {}; - if ( error.message && ! error.hidden ) { - errorMessage = error.message; - } else { - return null; - } - } - - return ( -
-

{ errorMessage }

-
- ); -}; - -ValidationInputError.propTypes = { - errorMessage: PropTypes.string, - propertyName: PropTypes.string, - elementId: PropTypes.string, -}; - -export default ValidationInputError; diff --git a/plugins/woocommerce-blocks/assets/js/base/context/providers/validation/context.js b/plugins/woocommerce-blocks/assets/js/base/context/providers/validation/context.js deleted file mode 100644 index 1ec1017d991..00000000000 --- a/plugins/woocommerce-blocks/assets/js/base/context/providers/validation/context.js +++ /dev/null @@ -1,257 +0,0 @@ -/** - * External dependencies - */ -import { - createContext, - useCallback, - useContext, - useState, -} from '@wordpress/element'; -import { pickBy } from 'lodash'; -import isShallowEqual from '@wordpress/is-shallow-equal'; - -/** - * @typedef { import('@woocommerce/type-defs/contexts').ValidationContext } ValidationContext - * @typedef {import('react')} React - */ - -const ValidationContext = createContext( { - getValidationError: () => '', - setValidationErrors: ( errors ) => void errors, - clearValidationError: ( property ) => void property, - clearAllValidationErrors: () => void null, - hideValidationError: () => void null, - showValidationError: () => void null, - showAllValidationErrors: () => void null, - hasValidationErrors: false, - getValidationErrorId: ( errorId ) => errorId, -} ); - -/** - * @return {ValidationContext} The context values for the validation context. - */ -export const useValidationContext = () => { - return useContext( ValidationContext ); -}; - -/** - * Validation context provider - * - * Any children of this context will be exposed to validation state and helpers - * for tracking validation. - * - * @param {Object} props Incoming props for the component. - * @param {JSX.Element} props.children What react elements are wrapped by this component. - */ -export const ValidationContextProvider = ( { children } ) => { - const [ validationErrors, updateValidationErrors ] = useState( {} ); - - /** - * This retrieves any validation error message that exists in state for the - * given property name. - * - * @param {string} property The property the error message is for. - * - * @return {Object} The error object for the given property. - */ - const getValidationError = useCallback( - ( property ) => validationErrors[ property ], - [ validationErrors ] - ); - - /** - * Provides an id for the validation error that can be used to fill out - * aria-describedby attribute values. - * - * @param {string} errorId The input css id the validation error is related - * to. - * @return {string} The id to use for the validation error container. - */ - const getValidationErrorId = useCallback( - ( errorId ) => { - const error = validationErrors[ errorId ]; - if ( ! error || error.hidden ) { - return ''; - } - return `validate-error-${ errorId }`; - }, - [ validationErrors ] - ); - - /** - * Clears any validation error that exists in state for the given property - * name. - * - * @param {string} property The name of the property to clear if exists in - * validation error state. - */ - const clearValidationError = useCallback( - /** - * Callback that is memoized. - * - * @param {string} property - */ - ( property ) => { - updateValidationErrors( - /** - * Callback for validation Errors handling. - * - * @param {Object} prevErrors - */ - ( prevErrors ) => { - if ( ! prevErrors[ property ] ) { - return prevErrors; - } - - const { - // eslint-disable-next-line no-unused-vars -- this is intentional to omit the dynamic property from the returned object. - [ property ]: clearedProperty, - ...newErrors - } = prevErrors; - return newErrors; - } - ); - }, - [] - ); - - /** - * Clears the entire validation error state. - */ - const clearAllValidationErrors = useCallback( - () => void updateValidationErrors( {} ), - [] - ); - - /** - * Used to record new validation errors in the state. - * - * @param {Object} newErrors An object where keys are the property names the - * validation error is for and values are the - * validation error message displayed to the user. - */ - const setValidationErrors = useCallback( ( newErrors ) => { - if ( ! newErrors ) { - return; - } - updateValidationErrors( ( prevErrors ) => { - newErrors = pickBy( newErrors, ( error, property ) => { - if ( typeof error.message !== 'string' ) { - return false; - } - if ( prevErrors.hasOwnProperty( property ) ) { - return ! isShallowEqual( prevErrors[ property ], error ); - } - return true; - } ); - if ( Object.values( newErrors ).length === 0 ) { - return prevErrors; - } - return { - ...prevErrors, - ...newErrors, - }; - } ); - }, [] ); - - /** - * Used to update a validation error. - * - * @param {string} property The name of the property to update. - * @param {Object} newError New validation error object. - */ - const updateValidationError = useCallback( ( property, newError ) => { - updateValidationErrors( ( prevErrors ) => { - if ( ! prevErrors.hasOwnProperty( property ) ) { - return prevErrors; - } - const updatedError = { - ...prevErrors[ property ], - ...newError, - }; - return isShallowEqual( prevErrors[ property ], updatedError ) - ? prevErrors - : { - ...prevErrors, - [ property ]: updatedError, - }; - } ); - }, [] ); - - /** - * Given a property name and if an associated error exists, it sets its - * `hidden` value to true. - * - * @param {string} property The name of the property to set the `hidden` - * value to true. - */ - const hideValidationError = useCallback( - ( property ) => - void updateValidationError( property, { - hidden: true, - } ), - [ updateValidationError ] - ); - - /** - * Given a property name and if an associated error exists, it sets its - * `hidden` value to false. - * - * @param {string} property The name of the property to set the `hidden` - * value to false. - */ - const showValidationError = useCallback( - ( property ) => - void updateValidationError( property, { - hidden: false, - } ), - [ updateValidationError ] - ); - - /** - * Sets the `hidden` value of all errors to `false`. - */ - const showAllValidationErrors = useCallback( - () => - void updateValidationErrors( ( prevErrors ) => { - const updatedErrors = {}; - - Object.keys( prevErrors ).forEach( ( property ) => { - if ( prevErrors[ property ].hidden ) { - updatedErrors[ property ] = { - ...prevErrors[ property ], - hidden: false, - }; - } - } ); - - if ( Object.values( updatedErrors ).length === 0 ) { - return prevErrors; - } - - return { - ...prevErrors, - ...updatedErrors, - }; - } ), - [] - ); - - const context = { - getValidationError, - setValidationErrors, - clearValidationError, - clearAllValidationErrors, - hideValidationError, - showValidationError, - showAllValidationErrors, - hasValidationErrors: Object.keys( validationErrors ).length > 0, - getValidationErrorId, - }; - - return ( - - { children } - - ); -}; diff --git a/plugins/woocommerce-blocks/assets/js/base/context/providers/validation/index.js b/plugins/woocommerce-blocks/assets/js/base/context/providers/validation/index.js deleted file mode 100644 index aba55551f67..00000000000 --- a/plugins/woocommerce-blocks/assets/js/base/context/providers/validation/index.js +++ /dev/null @@ -1,2 +0,0 @@ -export * from './context'; -export * from './components'; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/cart/block.js b/plugins/woocommerce-blocks/assets/js/blocks/cart/block.js index 98f1767f129..ac907d3cace 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/cart/block.js +++ b/plugins/woocommerce-blocks/assets/js/blocks/cart/block.js @@ -6,7 +6,6 @@ import { useStoreCart } from '@woocommerce/base-context/hooks'; import { useEffect } from '@wordpress/element'; import LoadingMask from '@woocommerce/base-components/loading-mask'; import { - ValidationContextProvider, StoreNoticesContainer, SnackbarNoticesContainer, } from '@woocommerce/base-context'; @@ -39,9 +38,7 @@ const Cart = ( { children, attributes = {} } ) => { hasDarkControls, } } > - - { children } - + { children } ); diff --git a/plugins/woocommerce-blocks/assets/js/blocks/checkout/block.tsx b/plugins/woocommerce-blocks/assets/js/blocks/checkout/block.tsx index 19a043789e1..4b96f45db57 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/checkout/block.tsx +++ b/plugins/woocommerce-blocks/assets/js/blocks/checkout/block.tsx @@ -6,8 +6,6 @@ import classnames from 'classnames'; import { createInterpolateElement, useEffect } from '@wordpress/element'; import { useStoreCart } from '@woocommerce/base-context/hooks'; import { - useValidationContext, - ValidationContextProvider, CheckoutProvider, SnackbarNoticesContainer, } from '@woocommerce/base-context'; @@ -17,8 +15,11 @@ import { SidebarLayout } from '@woocommerce/base-components/sidebar-layout'; import { CURRENT_USER_IS_ADMIN, getSetting } from '@woocommerce/settings'; import { SlotFillProvider } from '@woocommerce/blocks-checkout'; import withScrollToTop from '@woocommerce/base-hocs/with-scroll-to-top'; -import { useSelect } from '@wordpress/data'; -import { CHECKOUT_STORE_KEY } from '@woocommerce/block-data'; +import { useDispatch, useSelect } from '@wordpress/data'; +import { + CHECKOUT_STORE_KEY, + VALIDATION_STORE_KEY, +} from '@woocommerce/block-data'; /** * Internal dependencies @@ -120,8 +121,13 @@ const ScrollOnError = ( { }; } ); - const { hasValidationErrors, showAllValidationErrors } = - useValidationContext(); + const { hasValidationErrors } = useSelect( ( select ) => { + const store = select( VALIDATION_STORE_KEY ); + return { + hasValidationErrors: store.hasValidationErrors(), + }; + } ); + const { showAllValidationErrors } = useDispatch( VALIDATION_STORE_KEY ); const hasErrorsToDisplay = checkoutIsIdle && @@ -181,24 +187,21 @@ const Block = ( { - - { /* SlotFillProvider need to be defined before CheckoutProvider so fills have the SlotFill context ready when they mount. */ } - - - - - { children } - - - - - - + { /* SlotFillProvider need to be defined before CheckoutProvider so fills have the SlotFill context ready when they mount. */ } + + + + + { children } + + + + + ); diff --git a/plugins/woocommerce-blocks/assets/js/data/checkout/types.ts b/plugins/woocommerce-blocks/assets/js/data/checkout/types.ts index 94c2f6b0d92..64a0de63bc2 100644 --- a/plugins/woocommerce-blocks/assets/js/data/checkout/types.ts +++ b/plugins/woocommerce-blocks/assets/js/data/checkout/types.ts @@ -12,6 +12,7 @@ import type { CheckoutState } from './default-state'; import type { DispatchFromMap, SelectFromMap } from '../mapped-types'; import * as selectors from './selectors'; import * as actions from './actions'; +import { FieldValidationStatus } from '../types'; export type CheckoutAfterProcessingWithErrorEventData = { redirectUrl: CheckoutState[ 'redirectUrl' ]; @@ -53,7 +54,9 @@ export type emitValidateEventType = ( { setValidationErrors, }: { observers: EventObserversType; - setValidationErrors: ( errors: Array< unknown > ) => void; + setValidationErrors: ( + errors: Record< string, FieldValidationStatus > + ) => void; } ) => ( { dispatch, registry, diff --git a/plugins/woocommerce-blocks/assets/js/data/index.ts b/plugins/woocommerce-blocks/assets/js/data/index.ts index 767f979cf01..4b4daa2626c 100644 --- a/plugins/woocommerce-blocks/assets/js/data/index.ts +++ b/plugins/woocommerce-blocks/assets/js/data/index.ts @@ -10,6 +10,7 @@ export { SCHEMA_STORE_KEY } from './schema'; export { COLLECTIONS_STORE_KEY } from './collections'; export { CART_STORE_KEY } from './cart'; export { CHECKOUT_STORE_KEY } from './checkout'; +export { VALIDATION_STORE_KEY } from './validation'; export { QUERY_STATE_STORE_KEY } from './query-state'; export * from './constants'; export * from './types'; diff --git a/plugins/woocommerce-blocks/assets/js/data/types.ts b/plugins/woocommerce-blocks/assets/js/data/types.ts index 7529679a366..491ff5ed4ed 100644 --- a/plugins/woocommerce-blocks/assets/js/data/types.ts +++ b/plugins/woocommerce-blocks/assets/js/data/types.ts @@ -42,3 +42,8 @@ export function assertResponseIsValid( } throw new Error( 'Response not valid' ); } + +export interface FieldValidationStatus { + message: string; + hidden: boolean; +} diff --git a/plugins/woocommerce-blocks/assets/js/data/validation/action-types.ts b/plugins/woocommerce-blocks/assets/js/data/validation/action-types.ts new file mode 100644 index 00000000000..bb5ab4ef57c --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/data/validation/action-types.ts @@ -0,0 +1,8 @@ +export const ACTION_TYPES = { + SET_VALIDATION_ERRORS: 'SET_VALIDATION_ERRORS', + CLEAR_ALL_VALIDATION_ERRORS: 'CLEAR_ALL_VALIDATION_ERRORS', + CLEAR_VALIDATION_ERROR: 'CLEAR_VALIDATION_ERROR', + HIDE_VALIDATION_ERROR: 'HIDE_VALIDATION_ERROR', + SHOW_VALIDATION_ERROR: 'SHOW_VALIDATION_ERROR', + SHOW_ALL_VALIDATION_ERRORS: 'SHOW_ALL_VALIDATION_ERRORS', +} as const; diff --git a/plugins/woocommerce-blocks/assets/js/data/validation/actions.ts b/plugins/woocommerce-blocks/assets/js/data/validation/actions.ts new file mode 100644 index 00000000000..8dedd7d7b6d --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/data/validation/actions.ts @@ -0,0 +1,41 @@ +/** + * Internal dependencies + */ +import { ACTION_TYPES as types } from './action-types'; +import { ReturnOrGeneratorYieldUnion } from '../mapped-types'; +import { FieldValidationStatus } from '../types'; + +export const setValidationErrors = ( + errors: Record< string, FieldValidationStatus > +) => ( { + type: types.SET_VALIDATION_ERRORS, + errors, +} ); + +export const clearAllValidationErrors = () => ( { + type: types.CLEAR_ALL_VALIDATION_ERRORS, +} ); + +export const clearValidationError = ( error: string ) => ( { + type: types.CLEAR_VALIDATION_ERROR, + error, +} ); +export const hideValidationError = ( error: string ) => ( { + type: types.HIDE_VALIDATION_ERROR, + error, +} ); +export const showValidationError = ( error: string ) => ( { + type: types.SHOW_VALIDATION_ERROR, + error, +} ); +export const showAllValidationErrors = () => ( { + type: types.SHOW_ALL_VALIDATION_ERRORS, +} ); +export type ValidationAction = ReturnOrGeneratorYieldUnion< + | typeof setValidationErrors + | typeof clearAllValidationErrors + | typeof clearValidationError + | typeof hideValidationError + | typeof showValidationError + | typeof showAllValidationErrors +>; diff --git a/plugins/woocommerce-blocks/assets/js/data/validation/constants.ts b/plugins/woocommerce-blocks/assets/js/data/validation/constants.ts new file mode 100644 index 00000000000..a08fec25aac --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/data/validation/constants.ts @@ -0,0 +1 @@ +export const STORE_KEY = 'wc/store/validation'; diff --git a/plugins/woocommerce-blocks/assets/js/data/validation/index.ts b/plugins/woocommerce-blocks/assets/js/data/validation/index.ts new file mode 100644 index 00000000000..8241a611614 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/data/validation/index.ts @@ -0,0 +1,39 @@ +/** + * External dependencies + */ +import { createReduxStore, register } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import reducer from './reducers'; +import { STORE_KEY } from './constants'; +import * as actions from './actions'; +import * as selectors from './selectors'; +import { DispatchFromMap, SelectFromMap } from '../mapped-types'; + +export const config = { + reducer, + selectors, + actions, + // TODO: Gutenberg with Thunks was released in WP 6.0. Once 6.1 is released, remove the experimental flag here + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore We pass this in case there is an older version of Gutenberg running. + __experimentalUseThunks: true, +}; + +const store = createReduxStore( STORE_KEY, config ); +register( store ); + +export const VALIDATION_STORE_KEY = STORE_KEY; + +declare module '@wordpress/data' { + function dispatch( + key: typeof VALIDATION_STORE_KEY + ): DispatchFromMap< typeof actions >; + function select( key: typeof VALIDATION_STORE_KEY ): SelectFromMap< + typeof selectors + > & { + hasFinishedResolution: ( selector: string ) => boolean; + }; +} diff --git a/plugins/woocommerce-blocks/assets/js/data/validation/reducers.ts b/plugins/woocommerce-blocks/assets/js/data/validation/reducers.ts new file mode 100644 index 00000000000..0b857562e41 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/data/validation/reducers.ts @@ -0,0 +1,80 @@ +/** + * External dependencies + */ +import type { Reducer } from 'redux'; +import { pickBy } from 'lodash'; +import isShallowEqual from '@wordpress/is-shallow-equal'; +import { isString } from '@woocommerce/types'; + +/** + * Internal dependencies + */ +import { ValidationAction } from './actions'; +import { ACTION_TYPES as types } from './action-types'; +import { FieldValidationStatus } from '../types'; + +const reducer: Reducer< Record< string, FieldValidationStatus > > = ( + state: Record< string, FieldValidationStatus > = {}, + action: Partial< ValidationAction > +) => { + const newState = { ...state }; + switch ( action.type ) { + case types.SET_VALIDATION_ERRORS: + const newErrors = pickBy( action.errors, ( error, property ) => { + if ( typeof error.message !== 'string' ) { + return false; + } + if ( state.hasOwnProperty( property ) ) { + return ! isShallowEqual( state[ property ], error ); + } + return true; + } ); + if ( Object.values( newErrors ).length === 0 ) { + return state; + } + return { ...state, ...action.errors }; + case types.CLEAR_ALL_VALIDATION_ERRORS: + return {}; + + case types.CLEAR_VALIDATION_ERROR: + if ( + ! isString( action.error ) || + ! newState.hasOwnProperty( action.error ) + ) { + return newState; + } + delete newState[ action.error ]; + return newState; + case types.HIDE_VALIDATION_ERROR: + if ( + ! isString( action.error ) || + ! newState.hasOwnProperty( action.error ) + ) { + return newState; + } + newState[ action.error ].hidden = true; + return newState; + case types.SHOW_VALIDATION_ERROR: + if ( + ! isString( action.error ) || + ! newState.hasOwnProperty( action.error ) + ) { + return newState; + } + newState[ action.error ].hidden = false; + return newState; + case types.SHOW_ALL_VALIDATION_ERRORS: + Object.keys( newState ).forEach( ( property ) => { + if ( newState[ property ].hidden ) { + newState[ property ].hidden = false; + } + } ); + return { ...newState }; + + default: + return state; + } +}; + +export type State = ReturnType< typeof reducer >; +export default reducer; diff --git a/plugins/woocommerce-blocks/assets/js/data/validation/selectors.ts b/plugins/woocommerce-blocks/assets/js/data/validation/selectors.ts new file mode 100644 index 00000000000..d2697e21670 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/data/validation/selectors.ts @@ -0,0 +1,19 @@ +/** + * Internal dependencies + */ +import type { State } from './reducers'; + +export const getValidationError = ( state: State ) => { + return ( errorId: string ) => state[ errorId ]; +}; +export const getValidationErrorId = ( state: State ) => { + return ( errorId: string ) => { + if ( ! state.hasOwnProperty( errorId ) || state[ errorId ].hidden ) { + return; + } + return `validate-error-${ errorId }`; + }; +}; +export const hasValidationErrors = ( state: State ) => { + return Object.keys( state ).length > 0; +}; diff --git a/plugins/woocommerce-blocks/assets/js/data/validation/test/reducers.ts b/plugins/woocommerce-blocks/assets/js/data/validation/test/reducers.ts new file mode 100644 index 00000000000..6049b4288e0 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/data/validation/test/reducers.ts @@ -0,0 +1,248 @@ +/** + * Internal dependencies + */ +import reducer from '../reducers'; +import { FieldValidationStatus } from '../../types'; +import { ACTION_TYPES as types } from '.././action-types'; +import { ValidationAction } from '../actions'; + +describe( 'Validation reducer', () => { + it( 'Sets a single validation error', () => { + const singleValidationAction: ValidationAction = { + type: types.SET_VALIDATION_ERRORS, + errors: { + singleValidationError: { + message: 'This is a single validation error message', + hidden: false, + }, + }, + }; + const nextState = reducer( {}, singleValidationAction ); + expect( nextState ).toEqual( { + singleValidationError: { + message: 'This is a single validation error message', + hidden: false, + }, + } ); + } ); + + it( 'Does not add new errors if the same error already exists in state', () => { + const state: Record< string, FieldValidationStatus > = { + existingError: { + message: 'This is an existing error message', + hidden: false, + }, + }; + const existingErrorValidation: ValidationAction = { + type: types.SET_VALIDATION_ERRORS, + errors: { + existingError: { + message: 'This is an existing error message', + hidden: false, + }, + }, + }; + const nextState = reducer( state, existingErrorValidation ); + expect( nextState ).toEqual( { + existingError: { + message: 'This is an existing error message', + hidden: false, + }, + } ); + } ); + + it( 'Does not add new errors if error message is not string, but keeps existing errors', () => { + const integerErrorAction: ValidationAction = { + type: types.SET_VALIDATION_ERRORS, + errors: { + integerError: { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore ignoring because we're testing runtime errors with integers. + message: 1234, + hidden: false, + }, + }, + }; + const nextState = reducer( {}, integerErrorAction ); + expect( nextState ).not.toHaveProperty( 'integerError' ); + } ); + + it( 'Updates existing error if message or hidden property changes', () => { + const state: Record< string, FieldValidationStatus > = { + existingValidationError: { + message: 'This is an existing error message', + hidden: false, + }, + }; + const updateExistingErrorAction: ValidationAction = { + type: types.SET_VALIDATION_ERRORS, + errors: { + existingValidationError: { + message: 'This is an existing error message', + hidden: true, + }, + }, + }; + const nextState = reducer( state, updateExistingErrorAction ); + expect( nextState ).toEqual( { + existingValidationError: { + message: 'This is an existing error message', + hidden: true, + }, + } ); + } ); + + it( 'Appends new errors to list of existing errors', () => { + const state: Record< string, FieldValidationStatus > = { + existingError: { + message: 'This is an existing error message', + hidden: false, + }, + }; + const addNewError: ValidationAction = { + type: types.SET_VALIDATION_ERRORS, + errors: { + newError: { + message: 'This is a new error', + hidden: false, + }, + }, + }; + const nextState = reducer( state, addNewError ); + expect( nextState ).toEqual( { + existingError: { + message: 'This is an existing error message', + hidden: false, + }, + newError: { + message: 'This is a new error', + hidden: false, + }, + } ); + } ); + + it( 'Clears all validation errors', () => { + const state: Record< string, FieldValidationStatus > = { + existingError: { + message: 'This is an existing error message', + hidden: false, + }, + }; + const clearAllErrors: ValidationAction = { + type: types.CLEAR_ALL_VALIDATION_ERRORS, + }; + const nextState = reducer( state, clearAllErrors ); + expect( nextState ).toEqual( {} ); + } ); + + it( 'Clears a single validation error', () => { + const state: Record< string, FieldValidationStatus > = { + existingError: { + message: 'This is an existing error message', + hidden: false, + }, + testError: { + message: 'This is error should not be removed', + hidden: false, + }, + }; + const clearError: ValidationAction = { + type: types.CLEAR_VALIDATION_ERROR, + error: 'existingError', + }; + const nextState = reducer( state, clearError ); + expect( nextState ).not.toHaveProperty( 'existingError' ); + expect( nextState ).toHaveProperty( 'testError' ); + } ); + + it( 'Hides a single validation error', () => { + const state: Record< string, FieldValidationStatus > = { + existingError: { + message: 'This is an existing error message', + hidden: false, + }, + testError: { + message: 'This is error should not be removed', + hidden: false, + }, + }; + const testAction: ValidationAction = { + type: types.HIDE_VALIDATION_ERROR, + error: 'existingError', + }; + const nextState = reducer( state, testAction ); + expect( nextState ).toEqual( { + existingError: { + message: 'This is an existing error message', + hidden: true, + }, + testError: { + message: 'This is error should not be removed', + hidden: false, + }, + } ); + } ); + + it( 'Shows a single validation error', () => { + const state: Record< string, FieldValidationStatus > = { + existingError: { + message: 'This is an existing error message', + hidden: true, + }, + testError: { + message: 'This is error should not be removed', + hidden: true, + }, + visibleError: { + message: 'This is error should remain visible', + hidden: false, + }, + }; + const testAction: ValidationAction = { + type: types.SHOW_VALIDATION_ERROR, + error: 'existingError', + }; + const nextState = reducer( state, testAction ); + expect( nextState ).toEqual( { + existingError: { + message: 'This is an existing error message', + hidden: false, + }, + testError: { + message: 'This is error should not be removed', + hidden: true, + }, + visibleError: { + message: 'This is error should remain visible', + hidden: false, + }, + } ); + } ); + + it( 'Shows all validation errors', () => { + const state: Record< string, FieldValidationStatus > = { + firstExistingError: { + message: 'This is first existing error message', + hidden: true, + }, + secondExistingError: { + message: 'This is the second existing error message', + hidden: true, + }, + }; + const showAllErrors: ValidationAction = { + type: types.SHOW_ALL_VALIDATION_ERRORS, + }; + const nextState = reducer( state, showAllErrors ); + expect( nextState ).toEqual( { + firstExistingError: { + message: 'This is first existing error message', + hidden: false, + }, + secondExistingError: { + message: 'This is the second existing error message', + hidden: false, + }, + } ); + } ); +} ); diff --git a/plugins/woocommerce-blocks/assets/js/data/validation/test/selectors.ts b/plugins/woocommerce-blocks/assets/js/data/validation/test/selectors.ts new file mode 100644 index 00000000000..a62d5cfc9d9 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/data/validation/test/selectors.ts @@ -0,0 +1,56 @@ +/** + * Internal dependencies + */ +import { + getValidationErrorId, + getValidationError, + hasValidationErrors, +} from '../selectors'; +import { FieldValidationStatus } from '../../types'; + +describe( 'Validation selectors', () => { + it( 'Gets the validation error', () => { + const state: Record< string, FieldValidationStatus > = { + validationError: { + message: 'This is a test message', + hidden: false, + }, + }; + const validationError = getValidationError( state )( + 'validationError' + ); + expect( validationError ).toEqual( { + message: 'This is a test message', + hidden: false, + } ); + } ); + + it( 'Gets the generated validation error ID', () => { + const state: Record< string, FieldValidationStatus > = { + validationError: { + message: 'This is a test message', + hidden: false, + }, + }; + const validationErrorID = getValidationErrorId( state )( + 'validationError' + ); + expect( validationErrorID ).toEqual( `validate-error-validationError` ); + } ); + + it( 'Checks if state has any validation errors', () => { + const state: Record< string, FieldValidationStatus > = { + validationError: { + message: 'This is a test message', + hidden: false, + }, + }; + const validationErrors = hasValidationErrors( state ); + expect( validationErrors ).toEqual( true ); + const stateWithNoErrors: Record< string, FieldValidationStatus > = {}; + const stateWithNoErrorsCheckResult = hasValidationErrors( + stateWithNoErrors + ); + expect( stateWithNoErrorsCheckResult ).toEqual( false ); + } ); +} ); diff --git a/plugins/woocommerce-blocks/assets/js/types/type-defs/payment-method-interface.ts b/plugins/woocommerce-blocks/assets/js/types/type-defs/payment-method-interface.ts index f50f680497b..f7880339fb7 100644 --- a/plugins/woocommerce-blocks/assets/js/types/type-defs/payment-method-interface.ts +++ b/plugins/woocommerce-blocks/assets/js/types/type-defs/payment-method-interface.ts @@ -5,6 +5,7 @@ import type PaymentMethodLabel from '@woocommerce/base-components/cart-checkout/payment-method-label'; import type PaymentMethodIcons from '@woocommerce/base-components/cart-checkout/payment-method-icons'; import type LoadingMask from '@woocommerce/base-components/loading-mask'; +import type ValidationInputError from '@woocommerce/base-components/validation-input-error'; /** * Internal dependencies @@ -76,7 +77,7 @@ export interface ComponentProps { // A component used for displaying payment method labels, including an icon. PaymentMethodLabel: typeof PaymentMethodLabel; // A container for holding validation errors - ValidationInputError: () => JSX.Element | null; + ValidationInputError: typeof ValidationInputError; } export interface EmitResponseProps {