Convert validation context to data store (https://github.com/woocommerce/woocommerce-blocks/pull/6402)
* Add validation reducers, actions, and action types * Add selector for getValidationErrors * Export store key and register store * Export validation store key * Move TextInput files to checkout package * Export ValidatedTextInput from blocks-checkout package * Update imports of ValidatedTextInput to reflect new location * Use the validation wp-data store for showing error messages * Export getValidationError in checkout package * Move validation store to checkout package * Move ValidationInputError to blocks-checkout package * Only export "exposedSelectors" from validation * Convert validation context to data store * Fixed linting error * Fixed linting error * Change the validation selectors to return a function * Convert reducer and selectors to TS * Remove superfluous comments and improve test titles * Test to ensure visible errors remain visible * Make test for hasValidationErrors more robust * Augment the wp-data module to include our selectors and actions * Removed unused `exposedSelectors` variable * Remove TS error because of `instanceId` on props * Remove unnecessary as const * Use function returned by getValidationError * Use correct selector/action names now context has been decoupled * hide validation error when input value changes * Add correct aria-describedBy now we can get error id from store * Clear validation error from store when component unmounts * Clear validation error if input is valid * convert ValidationInputError to TS and get correct id/error from store * Ensure checkout block doesn't break when there are no errors * Get validation data from the store instead of context * Update country input to remove validation context * Move validation store out of checkout package * Move TextInput and ValidationInputError back out of the checkout package * Remove duplicate internal styles comment * Remove exports that no longer exist * Get validation store key from block-data * Make attribute-select-control use validation data store * Export FieldValidationStatus type * Make combobox use validation store not context * Make Address use validation store not context * Make Address use validation store not context * Use hasValidationErrors selector as a function in shipping calculator * Remove validation context from coupon story * Import VALIDATION_STORE_KEY from correct location * Stop coupon story from erroring * Update useStoreCartCoupons to use validation store not context * Make TotalsCoupon use validation store instead of context * Make AddToCartFormContext use validation store not context * Remove ValidationContext * Import FieldValidationStatus from correct location * Import ValidatedTextInput and ValidatedTextInput from correct location * Remove ValidationContextProvider * Update components to use validation store not context * Update useValidation to use the data store * Replace the validation context in checkout-events file * Use the re-mapped path for the store key import * Use "register" instead of the deprecated "registerStore" * Fix import error of the "FieldValidationStatus" type * Use TS instead of React's "PropTypes" * Fix the type of "ValidationInputError" in the "payment-method-interface" * Fix error not showing on the first place order click bug We were mutating the state in the reducer, which prevented re-rendering on state change * Fix state mutation issue in the Validation reducer Co-authored-by: Thomas Roberts <thomas.roberts@automattic.com> Co-authored-by: Saad Tarhi <saad.trh@gmail.com>
This commit is contained in:
parent
22f61cf4d9
commit
4de3bdb0f1
|
@ -6,10 +6,9 @@ import { decodeEntities } from '@wordpress/html-entities';
|
||||||
import { SelectControl } from 'wordpress-components';
|
import { SelectControl } from 'wordpress-components';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import {
|
import { ValidationInputError } from '@woocommerce/base-components/validation-input-error';
|
||||||
ValidationInputError,
|
import { VALIDATION_STORE_KEY } from '@woocommerce/block-data';
|
||||||
useValidationContext,
|
import { useDispatch, useSelect } from '@wordpress/data';
|
||||||
} from '@woocommerce/base-context';
|
|
||||||
|
|
||||||
// Default option for select boxes.
|
// Default option for select boxes.
|
||||||
const selectAnOption = {
|
const selectAnOption = {
|
||||||
|
@ -32,8 +31,16 @@ const AttributeSelectControl = ( {
|
||||||
'woo-gutenberg-products-block'
|
'woo-gutenberg-products-block'
|
||||||
),
|
),
|
||||||
} ) => {
|
} ) => {
|
||||||
const { getValidationError, setValidationErrors, clearValidationError } =
|
const { setValidationErrors, clearValidationError } = useDispatch(
|
||||||
useValidationContext();
|
VALIDATION_STORE_KEY
|
||||||
|
);
|
||||||
|
|
||||||
|
const { getValidationError } = useSelect( ( select ) => {
|
||||||
|
const store = select( VALIDATION_STORE_KEY );
|
||||||
|
return {
|
||||||
|
getValidationError: store.getValidationError(),
|
||||||
|
};
|
||||||
|
} );
|
||||||
const errorId = attributeName;
|
const errorId = attributeName;
|
||||||
const error = getValidationError( errorId ) || {};
|
const error = getValidationError( errorId ) || {};
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,6 @@ import {
|
||||||
BillingStateInput,
|
BillingStateInput,
|
||||||
ShippingStateInput,
|
ShippingStateInput,
|
||||||
} from '@woocommerce/base-components/state-input';
|
} from '@woocommerce/base-components/state-input';
|
||||||
import { useValidationContext } from '@woocommerce/base-context';
|
|
||||||
import { useEffect, useMemo } from '@wordpress/element';
|
import { useEffect, useMemo } from '@wordpress/element';
|
||||||
import { __ } from '@wordpress/i18n';
|
import { __ } from '@wordpress/i18n';
|
||||||
import { withInstanceId } from '@wordpress/compose';
|
import { withInstanceId } from '@wordpress/compose';
|
||||||
|
@ -22,6 +21,11 @@ import {
|
||||||
defaultAddressFields,
|
defaultAddressFields,
|
||||||
EnteredAddress,
|
EnteredAddress,
|
||||||
} from '@woocommerce/settings';
|
} from '@woocommerce/settings';
|
||||||
|
import { useSelect, useDispatch } from '@wordpress/data';
|
||||||
|
import {
|
||||||
|
VALIDATION_STORE_KEY,
|
||||||
|
FieldValidationStatus,
|
||||||
|
} from '@woocommerce/block-data';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
|
@ -32,7 +36,9 @@ import prepareAddressFields from './prepare-address-fields';
|
||||||
// values without having set the country first, show an error.
|
// values without having set the country first, show an error.
|
||||||
const validateShippingCountry = (
|
const validateShippingCountry = (
|
||||||
values: EnteredAddress,
|
values: EnteredAddress,
|
||||||
setValidationErrors: ( errors: Record< string, unknown > ) => void,
|
setValidationErrors: (
|
||||||
|
errors: Record< string, FieldValidationStatus >
|
||||||
|
) => void,
|
||||||
clearValidationError: ( error: string ) => void,
|
clearValidationError: ( error: string ) => void,
|
||||||
hasValidationError: boolean
|
hasValidationError: boolean
|
||||||
): void => {
|
): void => {
|
||||||
|
@ -87,8 +93,14 @@ const AddressForm = ( {
|
||||||
type = 'shipping',
|
type = 'shipping',
|
||||||
values,
|
values,
|
||||||
}: AddressFormProps ): JSX.Element => {
|
}: AddressFormProps ): JSX.Element => {
|
||||||
const { getValidationError, setValidationErrors, clearValidationError } =
|
const { setValidationErrors, clearValidationError } = useDispatch(
|
||||||
useValidationContext();
|
VALIDATION_STORE_KEY
|
||||||
|
);
|
||||||
|
|
||||||
|
const getValidationError = useSelect( ( select ) => {
|
||||||
|
const store = select( VALIDATION_STORE_KEY );
|
||||||
|
return store.getValidationError();
|
||||||
|
} );
|
||||||
|
|
||||||
const currentFields = useShallowEqual( fields );
|
const currentFields = useShallowEqual( fields );
|
||||||
|
|
||||||
|
|
|
@ -5,8 +5,9 @@ import { __ } from '@wordpress/i18n';
|
||||||
import Button from '@woocommerce/base-components/button';
|
import Button from '@woocommerce/base-components/button';
|
||||||
import { useState } from '@wordpress/element';
|
import { useState } from '@wordpress/element';
|
||||||
import isShallowEqual from '@wordpress/is-shallow-equal';
|
import isShallowEqual from '@wordpress/is-shallow-equal';
|
||||||
import { useValidationContext } from '@woocommerce/base-context';
|
|
||||||
import type { EnteredAddress, AddressFields } from '@woocommerce/settings';
|
import type { EnteredAddress, AddressFields } from '@woocommerce/settings';
|
||||||
|
import { VALIDATION_STORE_KEY } from '@woocommerce/block-data';
|
||||||
|
import { useDispatch, useSelect } from '@wordpress/data';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
|
@ -25,12 +26,18 @@ const ShippingCalculatorAddress = ( {
|
||||||
addressFields,
|
addressFields,
|
||||||
}: ShippingCalculatorAddressProps ): JSX.Element => {
|
}: ShippingCalculatorAddressProps ): JSX.Element => {
|
||||||
const [ address, setAddress ] = useState( initialAddress );
|
const [ address, setAddress ] = useState( initialAddress );
|
||||||
const { hasValidationErrors, showAllValidationErrors } =
|
const { showAllValidationErrors } = useDispatch( VALIDATION_STORE_KEY );
|
||||||
useValidationContext();
|
|
||||||
|
const { hasValidationErrors } = useSelect( ( select ) => {
|
||||||
|
const store = select( VALIDATION_STORE_KEY );
|
||||||
|
return {
|
||||||
|
hasValidationErrors: store.hasValidationErrors,
|
||||||
|
};
|
||||||
|
} );
|
||||||
|
|
||||||
const validateSubmit = () => {
|
const validateSubmit = () => {
|
||||||
showAllValidationErrors();
|
showAllValidationErrors();
|
||||||
return ! hasValidationErrors;
|
return ! hasValidationErrors();
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -4,15 +4,14 @@
|
||||||
import { __ } from '@wordpress/i18n';
|
import { __ } from '@wordpress/i18n';
|
||||||
import { useState, useEffect, useRef } from '@wordpress/element';
|
import { useState, useEffect, useRef } from '@wordpress/element';
|
||||||
import Button from '@woocommerce/base-components/button';
|
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 Label from '@woocommerce/base-components/label';
|
||||||
import LoadingMask from '@woocommerce/base-components/loading-mask';
|
import LoadingMask from '@woocommerce/base-components/loading-mask';
|
||||||
import { withInstanceId } from '@wordpress/compose';
|
import { withInstanceId } from '@wordpress/compose';
|
||||||
import {
|
import { ValidatedTextInput } from '@woocommerce/base-components/text-input';
|
||||||
ValidationInputError,
|
import ValidationInputError from '@woocommerce/base-components/validation-input-error';
|
||||||
useValidationContext,
|
import { useSelect } from '@wordpress/data';
|
||||||
} from '@woocommerce/base-context';
|
import { VALIDATION_STORE_KEY } from '@woocommerce/block-data';
|
||||||
import { Panel } from '@woocommerce/blocks-checkout';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
|
@ -46,7 +45,16 @@ export const TotalsCoupon = ( {
|
||||||
}: TotalsCouponProps ): JSX.Element => {
|
}: TotalsCouponProps ): JSX.Element => {
|
||||||
const [ couponValue, setCouponValue ] = useState( '' );
|
const [ couponValue, setCouponValue ] = useState( '' );
|
||||||
const currentIsLoading = useRef( false );
|
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' );
|
const validationError = getValidationError( 'coupon' );
|
||||||
|
|
||||||
useEffect( () => {
|
useEffect( () => {
|
||||||
|
|
|
@ -3,11 +3,9 @@
|
||||||
*/
|
*/
|
||||||
import { useArgs } from '@storybook/client-api';
|
import { useArgs } from '@storybook/client-api';
|
||||||
import { Story, Meta } from '@storybook/react';
|
import { Story, Meta } from '@storybook/react';
|
||||||
import {
|
|
||||||
useValidationContext,
|
|
||||||
ValidationContextProvider,
|
|
||||||
} from '@woocommerce/base-context';
|
|
||||||
import { INTERACTION_TIMEOUT } from '@woocommerce/storybook-controls';
|
import { INTERACTION_TIMEOUT } from '@woocommerce/storybook-controls';
|
||||||
|
import { useDispatch } from '@wordpress/data';
|
||||||
|
import { VALIDATION_STORE_KEY } from '@woocommerce/block-data';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
|
@ -52,7 +50,7 @@ LoadingState.args = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ErrorState: Story< TotalsCouponProps > = ( args ) => {
|
export const ErrorState: Story< TotalsCouponProps > = ( args ) => {
|
||||||
const { setValidationErrors } = useValidationContext();
|
const { setValidationErrors } = useDispatch( VALIDATION_STORE_KEY );
|
||||||
|
|
||||||
setValidationErrors( { coupon: INVALID_COUPON_ERROR } );
|
setValidationErrors( { coupon: INVALID_COUPON_ERROR } );
|
||||||
|
|
||||||
|
@ -61,10 +59,6 @@ export const ErrorState: Story< TotalsCouponProps > = ( args ) => {
|
||||||
|
|
||||||
ErrorState.decorators = [
|
ErrorState.decorators = [
|
||||||
( StoryComponent ) => {
|
( StoryComponent ) => {
|
||||||
return (
|
return <StoryComponent />;
|
||||||
<ValidationContextProvider>
|
|
||||||
<StoryComponent />
|
|
||||||
</ValidationContextProvider>
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
|
@ -6,11 +6,10 @@ import { __ } from '@wordpress/i18n';
|
||||||
import { useEffect, useRef } from '@wordpress/element';
|
import { useEffect, useRef } from '@wordpress/element';
|
||||||
import { withInstanceId } from '@wordpress/compose';
|
import { withInstanceId } from '@wordpress/compose';
|
||||||
import { ComboboxControl } from 'wordpress-components';
|
import { ComboboxControl } from 'wordpress-components';
|
||||||
import {
|
import { ValidationInputError } from '@woocommerce/base-components/validation-input-error';
|
||||||
ValidationInputError,
|
|
||||||
useValidationContext,
|
|
||||||
} from '@woocommerce/base-context';
|
|
||||||
import { isObject } from '@woocommerce/types';
|
import { isObject } from '@woocommerce/types';
|
||||||
|
import { useDispatch, useSelect } from '@wordpress/data';
|
||||||
|
import { VALIDATION_STORE_KEY } from '@woocommerce/block-data';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
|
@ -55,8 +54,13 @@ const Combobox = ( {
|
||||||
instanceId = '0',
|
instanceId = '0',
|
||||||
autoComplete = 'off',
|
autoComplete = 'off',
|
||||||
}: ComboboxProps ): JSX.Element => {
|
}: ComboboxProps ): JSX.Element => {
|
||||||
const { getValidationError, setValidationErrors, clearValidationError } =
|
const { setValidationErrors, clearValidationError } = useDispatch(
|
||||||
useValidationContext();
|
VALIDATION_STORE_KEY
|
||||||
|
);
|
||||||
|
const getValidationError = useSelect( ( select ) => {
|
||||||
|
const store = select( VALIDATION_STORE_KEY );
|
||||||
|
return store.getValidationError();
|
||||||
|
} );
|
||||||
|
|
||||||
const controlRef = useRef< HTMLDivElement >( null );
|
const controlRef = useRef< HTMLDivElement >( null );
|
||||||
const controlId = id || 'control-' + instanceId;
|
const controlId = id || 'control-' + instanceId;
|
||||||
|
|
|
@ -2,11 +2,9 @@
|
||||||
* External dependencies
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
import { Story, Meta } from '@storybook/react';
|
import { Story, Meta } from '@storybook/react';
|
||||||
import {
|
import { useDispatch } from '@wordpress/data';
|
||||||
useValidationContext,
|
|
||||||
ValidationContextProvider,
|
|
||||||
} from '@woocommerce/base-context';
|
|
||||||
import { useState, useEffect } from '@wordpress/element';
|
import { useState, useEffect } from '@wordpress/element';
|
||||||
|
import { VALIDATION_STORE_KEY } from '@woocommerce/block-data';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
|
@ -31,21 +29,16 @@ export default {
|
||||||
options: { table: { disable: true } },
|
options: { table: { disable: true } },
|
||||||
value: { control: false },
|
value: { control: false },
|
||||||
},
|
},
|
||||||
decorators: [
|
decorators: [ ( StoryComponent ) => <StoryComponent /> ],
|
||||||
( StoryComponent ) => (
|
|
||||||
<ValidationContextProvider>
|
|
||||||
<StoryComponent />
|
|
||||||
</ValidationContextProvider>
|
|
||||||
),
|
|
||||||
],
|
|
||||||
} as Meta< CountryInputWithCountriesProps >;
|
} as Meta< CountryInputWithCountriesProps >;
|
||||||
|
|
||||||
const Template: Story< CountryInputWithCountriesProps > = ( args ) => {
|
const Template: Story< CountryInputWithCountriesProps > = ( args ) => {
|
||||||
const [ selectedCountry, selectCountry ] = useState< CountryCode | '' >(
|
const [ selectedCountry, selectCountry ] = useState< CountryCode | '' >(
|
||||||
''
|
''
|
||||||
);
|
);
|
||||||
const { clearValidationError, showValidationError } =
|
const { clearValidationError, showValidationError } = useDispatch(
|
||||||
useValidationContext();
|
VALIDATION_STORE_KEY
|
||||||
|
);
|
||||||
|
|
||||||
useEffect( () => {
|
useEffect( () => {
|
||||||
showValidationError( 'country' );
|
showValidationError( 'country' );
|
||||||
|
|
|
@ -5,11 +5,11 @@ import { __ } from '@wordpress/i18n';
|
||||||
import { decodeEntities } from '@wordpress/html-entities';
|
import { decodeEntities } from '@wordpress/html-entities';
|
||||||
import { useCallback, useMemo, useEffect, useRef } from '@wordpress/element';
|
import { useCallback, useMemo, useEffect, useRef } from '@wordpress/element';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
|
import { ValidatedTextInput } from '@woocommerce/base-components/text-input';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import { ValidatedTextInput } from '../text-input';
|
|
||||||
import Combobox from '../combobox';
|
import Combobox from '../combobox';
|
||||||
import './style.scss';
|
import './style.scss';
|
||||||
import type { StateInputWithStatesProps } from './StateInputProps';
|
import type { StateInputWithStatesProps } from './StateInputProps';
|
||||||
|
|
|
@ -4,33 +4,21 @@
|
||||||
import { __ } from '@wordpress/i18n';
|
import { __ } from '@wordpress/i18n';
|
||||||
import { useCallback, useRef, useEffect, useState } from 'react';
|
import { useCallback, useRef, useEffect, useState } from 'react';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import {
|
|
||||||
ValidationInputError,
|
|
||||||
useValidationContext,
|
|
||||||
} from '@woocommerce/base-context';
|
|
||||||
import { withInstanceId } from '@wordpress/compose';
|
import { withInstanceId } from '@wordpress/compose';
|
||||||
import { isString } from '@woocommerce/types';
|
import { isString } from '@woocommerce/types';
|
||||||
|
import { dispatch, useSelect } from '@wordpress/data';
|
||||||
|
import { VALIDATION_STORE_KEY } from '@woocommerce/block-data';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import TextInput from './text-input';
|
import TextInput from './text-input';
|
||||||
import './style.scss';
|
import './style.scss';
|
||||||
|
import { ValidationInputError } from '../validation-input-error';
|
||||||
|
|
||||||
interface ValidatedTextInputPropsWithId {
|
interface ValidatedTextInputProps {
|
||||||
instanceId?: string;
|
|
||||||
id: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ValidatedTextInputPropsWithInstanceId {
|
|
||||||
instanceId: string;
|
|
||||||
id?: string;
|
id?: string;
|
||||||
}
|
instanceId: string;
|
||||||
|
|
||||||
type ValidatedTextInputProps = (
|
|
||||||
| ValidatedTextInputPropsWithId
|
|
||||||
| ValidatedTextInputPropsWithInstanceId
|
|
||||||
) & {
|
|
||||||
className?: string;
|
className?: string;
|
||||||
ariaDescribedBy?: string;
|
ariaDescribedBy?: string;
|
||||||
errorId?: string;
|
errorId?: string;
|
||||||
|
@ -39,7 +27,7 @@ type ValidatedTextInputProps = (
|
||||||
errorMessage?: string;
|
errorMessage?: string;
|
||||||
onChange: ( newValue: string ) => void;
|
onChange: ( newValue: string ) => void;
|
||||||
value: string;
|
value: string;
|
||||||
};
|
}
|
||||||
|
|
||||||
const ValidatedTextInput = ( {
|
const ValidatedTextInput = ( {
|
||||||
className,
|
className,
|
||||||
|
@ -53,20 +41,26 @@ const ValidatedTextInput = ( {
|
||||||
errorMessage: passedErrorMessage = '',
|
errorMessage: passedErrorMessage = '',
|
||||||
value = '',
|
value = '',
|
||||||
...rest
|
...rest
|
||||||
}: ValidatedTextInputProps ) => {
|
}: ValidatedTextInputProps ): JSX.Element => {
|
||||||
const [ isPristine, setIsPristine ] = useState( true );
|
const [ isPristine, setIsPristine ] = useState( true );
|
||||||
const inputRef = useRef< HTMLInputElement >( null );
|
const inputRef = useRef< HTMLInputElement >( null );
|
||||||
const {
|
|
||||||
getValidationError,
|
const { setValidationErrors, hideValidationError, clearValidationError } =
|
||||||
hideValidationError,
|
dispatch( VALIDATION_STORE_KEY );
|
||||||
setValidationErrors,
|
|
||||||
clearValidationError,
|
|
||||||
getValidationErrorId,
|
|
||||||
} = useValidationContext();
|
|
||||||
const textInputId =
|
const textInputId =
|
||||||
typeof id !== 'undefined' ? id : 'textinput-' + instanceId;
|
typeof id !== 'undefined' ? id : 'textinput-' + instanceId;
|
||||||
const errorIdString = errorId !== undefined ? errorId : textInputId;
|
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(
|
const validateInput = useCallback(
|
||||||
( errorsHidden = true ) => {
|
( errorsHidden = true ) => {
|
||||||
const inputObject = inputRef.current || null;
|
const inputObject = inputRef.current || null;
|
||||||
|
@ -79,7 +73,7 @@ const ValidatedTextInput = ( {
|
||||||
if ( inputIsValid ) {
|
if ( inputIsValid ) {
|
||||||
clearValidationError( errorIdString );
|
clearValidationError( errorIdString );
|
||||||
} else {
|
} else {
|
||||||
setValidationErrors( {
|
const validationErrors = {
|
||||||
[ errorIdString ]: {
|
[ errorIdString ]: {
|
||||||
message:
|
message:
|
||||||
inputObject.validationMessage ||
|
inputObject.validationMessage ||
|
||||||
|
@ -89,7 +83,8 @@ const ValidatedTextInput = ( {
|
||||||
),
|
),
|
||||||
hidden: errorsHidden,
|
hidden: errorsHidden,
|
||||||
},
|
},
|
||||||
} );
|
};
|
||||||
|
setValidationErrors( validationErrors );
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[ clearValidationError, errorIdString, setValidationErrors ]
|
[ clearValidationError, errorIdString, setValidationErrors ]
|
||||||
|
@ -129,17 +124,13 @@ const ValidatedTextInput = ( {
|
||||||
};
|
};
|
||||||
}, [ clearValidationError, errorIdString ] );
|
}, [ clearValidationError, errorIdString ] );
|
||||||
|
|
||||||
// @todo - When useValidationContext is converted to TypeScript, remove this cast and use the correct type.
|
const errorMessage = getValidationError( errorIdString );
|
||||||
const errorMessage = ( getValidationError( errorIdString ) || {} ) as {
|
|
||||||
message?: string;
|
|
||||||
hidden?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
if ( isString( passedErrorMessage ) && passedErrorMessage !== '' ) {
|
if ( isString( passedErrorMessage ) && passedErrorMessage !== '' ) {
|
||||||
errorMessage.message = passedErrorMessage;
|
errorMessage.message = passedErrorMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasError = errorMessage.message && ! errorMessage.hidden;
|
const hasError = errorMessage?.message && ! errorMessage?.hidden;
|
||||||
const describedBy =
|
const describedBy =
|
||||||
showError && hasError && getValidationErrorId( errorIdString )
|
showError && hasError && getValidationErrorId( errorIdString )
|
||||||
? getValidationErrorId( errorIdString )
|
? getValidationErrorId( errorIdString )
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
"../hocs",
|
"../hocs",
|
||||||
"../../atomic/utils",
|
"../../atomic/utils",
|
||||||
"../../atomic/blocks/component-init.js",
|
"../../atomic/blocks/component-init.js",
|
||||||
|
"../../data",
|
||||||
"../../shared/context"
|
"../../shared/context"
|
||||||
],
|
],
|
||||||
"exclude": [ "**/test/**" ]
|
"exclude": [ "**/test/**" ]
|
||||||
|
|
|
@ -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 (
|
||||||
|
<div className="wc-block-components-validation-error" role="alert">
|
||||||
|
<p id={ getValidationErrorId( elementId ) }>{ errorMessage }</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ValidationInputError;
|
|
@ -5,7 +5,10 @@
|
||||||
*/
|
*/
|
||||||
import { __, sprintf } from '@wordpress/i18n';
|
import { __, sprintf } from '@wordpress/i18n';
|
||||||
import { useDispatch, useSelect } from '@wordpress/data';
|
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 { decodeEntities } from '@wordpress/html-entities';
|
||||||
import type { StoreCartCoupon } from '@woocommerce/types';
|
import type { StoreCartCoupon } from '@woocommerce/types';
|
||||||
|
|
||||||
|
@ -13,7 +16,6 @@ import type { StoreCartCoupon } from '@woocommerce/types';
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import { useStoreCart } from './use-store-cart';
|
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
|
* 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 { cartCoupons, cartIsLoading } = useStoreCart();
|
||||||
const { createErrorNotice } = useDispatch( 'core/notices' );
|
const { createErrorNotice } = useDispatch( 'core/notices' );
|
||||||
const { createNotice } = useDispatch( 'core/notices' );
|
const { createNotice } = useDispatch( 'core/notices' );
|
||||||
const { setValidationErrors } = useValidationContext();
|
const { setValidationErrors } = useDispatch( VALIDATION_STORE_KEY );
|
||||||
|
|
||||||
const {
|
const {
|
||||||
applyCoupon,
|
applyCoupon,
|
||||||
|
|
|
@ -12,11 +12,11 @@ import LoadingMask from '@woocommerce/base-components/loading-mask';
|
||||||
import type { PaymentMethodInterface } from '@woocommerce/types';
|
import type { PaymentMethodInterface } from '@woocommerce/types';
|
||||||
import { useSelect } from '@wordpress/data';
|
import { useSelect } from '@wordpress/data';
|
||||||
import { CHECKOUT_STORE_KEY } from '@woocommerce/block-data';
|
import { CHECKOUT_STORE_KEY } from '@woocommerce/block-data';
|
||||||
|
import { ValidationInputError } from '@woocommerce/base-components/validation-input-error';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import { ValidationInputError } from '../../providers/validation';
|
|
||||||
import { useStoreCart } from '../cart/use-store-cart';
|
import { useStoreCart } from '../cart/use-store-cart';
|
||||||
import { useStoreCartCoupons } from '../cart/use-store-cart-coupons';
|
import { useStoreCartCoupons } from '../cart/use-store-cart-coupons';
|
||||||
import { useEmitResponse } from '../use-emit-response';
|
import { useEmitResponse } from '../use-emit-response';
|
||||||
|
|
|
@ -6,27 +6,31 @@ import type {
|
||||||
ValidationData,
|
ValidationData,
|
||||||
ValidationContextError,
|
ValidationContextError,
|
||||||
} from '@woocommerce/type-defs/contexts';
|
} from '@woocommerce/type-defs/contexts';
|
||||||
|
import { useDispatch, useSelect } from '@wordpress/data';
|
||||||
/**
|
import { VALIDATION_STORE_KEY } from '@woocommerce/block-data';
|
||||||
* Internal dependencies
|
|
||||||
*/
|
|
||||||
import { useValidationContext } from '../providers/validation/';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom hook for setting for adding errors to the validation system.
|
* Custom hook for setting for adding errors to the validation system.
|
||||||
*/
|
*/
|
||||||
export const useValidation = (): ValidationData => {
|
export const useValidation = (): ValidationData => {
|
||||||
const {
|
const {
|
||||||
hasValidationErrors,
|
|
||||||
getValidationError,
|
|
||||||
clearValidationError,
|
clearValidationError,
|
||||||
hideValidationError,
|
hideValidationError,
|
||||||
setValidationErrors,
|
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';
|
const prefix = 'extensions-errors';
|
||||||
|
|
||||||
return {
|
return {
|
||||||
hasValidationErrors,
|
hasValidationErrors: hasValidationErrors(),
|
||||||
getValidationError: useCallback(
|
getValidationError: useCallback(
|
||||||
( validationErrorId: string ) =>
|
( validationErrorId: string ) =>
|
||||||
getValidationError( `${ prefix }-${ validationErrorId }` ),
|
getValidationError( `${ prefix }-${ validationErrorId }` ),
|
||||||
|
|
|
@ -15,6 +15,8 @@ import {
|
||||||
productSupportsAddToCartForm,
|
productSupportsAddToCartForm,
|
||||||
} from '@woocommerce/base-utils';
|
} from '@woocommerce/base-utils';
|
||||||
import { useDispatch } from '@wordpress/data';
|
import { useDispatch } from '@wordpress/data';
|
||||||
|
import { VALIDATION_STORE_KEY } from '@woocommerce/block-data';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
|
@ -28,7 +30,6 @@ import {
|
||||||
emitEventWithAbort,
|
emitEventWithAbort,
|
||||||
reducer as emitReducer,
|
reducer as emitReducer,
|
||||||
} from './event-emit';
|
} from './event-emit';
|
||||||
import { useValidationContext } from '../../validation';
|
|
||||||
import { useEmitResponse } from '../../../hooks/use-emit-response';
|
import { useEmitResponse } from '../../../hooks/use-emit-response';
|
||||||
import { removeNoticesByStatus } from '../../../../../utils/notices';
|
import { removeNoticesByStatus } from '../../../../../utils/notices';
|
||||||
|
|
||||||
|
@ -100,9 +101,12 @@ export const AddToCartFormStateContextProvider = ( {
|
||||||
const [ observers, observerDispatch ] = useReducer( emitReducer, {} );
|
const [ observers, observerDispatch ] = useReducer( emitReducer, {} );
|
||||||
const currentObservers = useShallowEqual( observers );
|
const currentObservers = useShallowEqual( observers );
|
||||||
const { createErrorNotice } = useDispatch( 'core/notices' );
|
const { createErrorNotice } = useDispatch( 'core/notices' );
|
||||||
const { setValidationErrors } = useValidationContext();
|
const { setValidationErrors } = useDispatch( VALIDATION_STORE_KEY );
|
||||||
const { isSuccessResponse, isErrorResponse, isFailResponse } =
|
const {
|
||||||
useEmitResponse();
|
isSuccessResponse,
|
||||||
|
isErrorResponse,
|
||||||
|
isFailResponse,
|
||||||
|
} = useEmitResponse();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {AddToCartFormEventRegistration}
|
* @type {AddToCartFormEventRegistration}
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import { AddToCartFormStateContextProvider } from '../form-state';
|
import { AddToCartFormStateContextProvider } from '../form-state';
|
||||||
import { ValidationContextProvider } from '../../validation';
|
|
||||||
import FormSubmit from './submit';
|
import FormSubmit from './submit';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -21,7 +20,6 @@ export const AddToCartFormContextProvider = ( {
|
||||||
showFormElements,
|
showFormElements,
|
||||||
} ) => {
|
} ) => {
|
||||||
return (
|
return (
|
||||||
<ValidationContextProvider>
|
|
||||||
<AddToCartFormStateContextProvider
|
<AddToCartFormStateContextProvider
|
||||||
product={ product }
|
product={ product }
|
||||||
showFormElements={ showFormElements }
|
showFormElements={ showFormElements }
|
||||||
|
@ -29,6 +27,5 @@ export const AddToCartFormContextProvider = ( {
|
||||||
{ children }
|
{ children }
|
||||||
<FormSubmit />
|
<FormSubmit />
|
||||||
</AddToCartFormStateContextProvider>
|
</AddToCartFormStateContextProvider>
|
||||||
</ValidationContextProvider>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -6,13 +6,13 @@ import triggerFetch from '@wordpress/api-fetch';
|
||||||
import { useEffect, useCallback, useState } from '@wordpress/element';
|
import { useEffect, useCallback, useState } from '@wordpress/element';
|
||||||
import { decodeEntities } from '@wordpress/html-entities';
|
import { decodeEntities } from '@wordpress/html-entities';
|
||||||
import { triggerAddedToCartEvent } from '@woocommerce/base-utils';
|
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
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import { useAddToCartFormContext } from '../../form-state';
|
import { useAddToCartFormContext } from '../../form-state';
|
||||||
import { useValidationContext } from '../../../validation';
|
|
||||||
import { useStoreCart } from '../../../../hooks/cart/use-store-cart';
|
import { useStoreCart } from '../../../../hooks/cart/use-store-cart';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -30,15 +30,18 @@ const FormSubmit = () => {
|
||||||
isProcessing,
|
isProcessing,
|
||||||
requestParams,
|
requestParams,
|
||||||
} = useAddToCartFormContext();
|
} = useAddToCartFormContext();
|
||||||
const { hasValidationErrors, showAllValidationErrors } =
|
const { showAllValidationErrors } = useDispatch( VALIDATION_STORE_KEY );
|
||||||
useValidationContext();
|
const hasValidationErrors = useSelect( ( select ) => {
|
||||||
|
const store = select( VALIDATION_STORE_KEY );
|
||||||
|
return store.hasValidationErrors;
|
||||||
|
} );
|
||||||
const { createErrorNotice, removeNotice } = useDispatch( 'core/notices' );
|
const { createErrorNotice, removeNotice } = useDispatch( 'core/notices' );
|
||||||
const { receiveCart } = useStoreCart();
|
const { receiveCart } = useStoreCart();
|
||||||
const [ isSubmitting, setIsSubmitting ] = useState( false );
|
const [ isSubmitting, setIsSubmitting ] = useState( false );
|
||||||
const doSubmit = ! hasError && isProcessing;
|
const doSubmit = ! hasError && isProcessing;
|
||||||
|
|
||||||
const checkValidationContext = useCallback( () => {
|
const checkValidationContext = useCallback( () => {
|
||||||
if ( hasValidationErrors ) {
|
if ( hasValidationErrors() ) {
|
||||||
showAllValidationErrors();
|
showAllValidationErrors();
|
||||||
return {
|
return {
|
||||||
type: 'error',
|
type: 'error',
|
||||||
|
|
|
@ -14,7 +14,10 @@ import {
|
||||||
import { usePrevious } from '@woocommerce/base-hooks';
|
import { usePrevious } from '@woocommerce/base-hooks';
|
||||||
import deprecated from '@wordpress/deprecated';
|
import deprecated from '@wordpress/deprecated';
|
||||||
import { useDispatch, useSelect } from '@wordpress/data';
|
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
|
* Internal dependencies
|
||||||
|
@ -22,7 +25,6 @@ import { CHECKOUT_STORE_KEY } from '@woocommerce/block-data';
|
||||||
import type { CheckoutEventsContextType } from './types';
|
import type { CheckoutEventsContextType } from './types';
|
||||||
import { useEventEmitters, reducer as emitReducer } from './event-emit';
|
import { useEventEmitters, reducer as emitReducer } from './event-emit';
|
||||||
import { STATUS } from '../../../../../data/checkout/constants';
|
import { STATUS } from '../../../../../data/checkout/constants';
|
||||||
import { useValidationContext } from '../../validation';
|
|
||||||
import { useStoreEvents } from '../../../hooks/use-store-events';
|
import { useStoreEvents } from '../../../hooks/use-store-events';
|
||||||
import { useCheckoutNotices } from '../../../hooks/use-checkout-notices';
|
import { useCheckoutNotices } from '../../../hooks/use-checkout-notices';
|
||||||
import { useEmitResponse } from '../../../hooks/use-emit-response';
|
import { useEmitResponse } from '../../../hooks/use-emit-response';
|
||||||
|
@ -64,7 +66,7 @@ export const CheckoutEventsProvider = ( {
|
||||||
checkoutActions.setRedirectUrl( redirectUrl );
|
checkoutActions.setRedirectUrl( redirectUrl );
|
||||||
}
|
}
|
||||||
|
|
||||||
const { setValidationErrors } = useValidationContext();
|
const { setValidationErrors } = useDispatch( VALIDATION_STORE_KEY );
|
||||||
const { createErrorNotice } = useDispatch( 'core/notices' );
|
const { createErrorNotice } = useDispatch( 'core/notices' );
|
||||||
|
|
||||||
const { dispatchCheckoutEvent } = useStoreEvents();
|
const { dispatchCheckoutEvent } = useStoreEvents();
|
||||||
|
|
|
@ -15,7 +15,10 @@ import {
|
||||||
formatStoreApiErrorMessage,
|
formatStoreApiErrorMessage,
|
||||||
} from '@woocommerce/base-utils';
|
} from '@woocommerce/base-utils';
|
||||||
import { useDispatch, useSelect } from '@wordpress/data';
|
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
|
* Internal dependencies
|
||||||
|
@ -25,9 +28,9 @@ import { useCheckoutEventsContext } from './checkout-events';
|
||||||
import { useShippingDataContext } from './shipping';
|
import { useShippingDataContext } from './shipping';
|
||||||
import { useCustomerDataContext } from './customer';
|
import { useCustomerDataContext } from './customer';
|
||||||
import { usePaymentMethodDataContext } from './payment-methods';
|
import { usePaymentMethodDataContext } from './payment-methods';
|
||||||
import { useValidationContext } from '../validation';
|
|
||||||
import { useStoreCart } from '../../hooks/cart/use-store-cart';
|
import { useStoreCart } from '../../hooks/cart/use-store-cart';
|
||||||
import { useStoreNoticesContext } from '../store-notices';
|
import { useStoreNoticesContext } from '../store-notices';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* CheckoutProcessor component.
|
* CheckoutProcessor component.
|
||||||
*
|
*
|
||||||
|
@ -58,7 +61,9 @@ const CheckoutProcessor = () => {
|
||||||
const { setHasError, processCheckoutResponse } =
|
const { setHasError, processCheckoutResponse } =
|
||||||
useDispatch( CHECKOUT_STORE_KEY );
|
useDispatch( CHECKOUT_STORE_KEY );
|
||||||
|
|
||||||
const { hasValidationErrors } = useValidationContext();
|
const hasValidationErrors = useSelect(
|
||||||
|
( select ) => select( VALIDATION_STORE_KEY ).hasValidationErrors
|
||||||
|
);
|
||||||
const { shippingErrorStatus } = useShippingDataContext();
|
const { shippingErrorStatus } = useShippingDataContext();
|
||||||
const { billingAddress, shippingAddress } = useCustomerDataContext();
|
const { billingAddress, shippingAddress } = useCustomerDataContext();
|
||||||
const { cartNeedsPayment, cartNeedsShipping, receiveCart } = useStoreCart();
|
const { cartNeedsPayment, cartNeedsShipping, receiveCart } = useStoreCart();
|
||||||
|
@ -87,7 +92,7 @@ const CheckoutProcessor = () => {
|
||||||
}, [ activePaymentMethod, expressPaymentMethods, paymentMethods ] );
|
}, [ activePaymentMethod, expressPaymentMethods, paymentMethods ] );
|
||||||
|
|
||||||
const checkoutWillHaveError =
|
const checkoutWillHaveError =
|
||||||
( hasValidationErrors && ! isExpressPaymentMethodActive ) ||
|
( hasValidationErrors() && ! isExpressPaymentMethodActive ) ||
|
||||||
currentPaymentStatus.hasError ||
|
currentPaymentStatus.hasError ||
|
||||||
shippingErrorStatus.hasError;
|
shippingErrorStatus.hasError;
|
||||||
|
|
||||||
|
@ -128,7 +133,7 @@ const CheckoutProcessor = () => {
|
||||||
}, [ billingAddress, shippingAddress, redirectUrl ] );
|
}, [ billingAddress, shippingAddress, redirectUrl ] );
|
||||||
|
|
||||||
const checkValidation = useCallback( () => {
|
const checkValidation = useCallback( () => {
|
||||||
if ( hasValidationErrors ) {
|
if ( hasValidationErrors() ) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if ( currentPaymentStatus.hasError ) {
|
if ( currentPaymentStatus.hasError ) {
|
||||||
|
|
|
@ -12,7 +12,10 @@ import {
|
||||||
} from '@wordpress/element';
|
} from '@wordpress/element';
|
||||||
import { objectHasProp } from '@woocommerce/types';
|
import { objectHasProp } from '@woocommerce/types';
|
||||||
import { useDispatch, useSelect } from '@wordpress/data';
|
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
|
* Internal dependencies
|
||||||
|
@ -39,7 +42,6 @@ import {
|
||||||
emitEventWithAbort,
|
emitEventWithAbort,
|
||||||
reducer as emitReducer,
|
reducer as emitReducer,
|
||||||
} from './event-emit';
|
} from './event-emit';
|
||||||
import { useValidationContext } from '../../validation';
|
|
||||||
import { useEmitResponse } from '../../../hooks/use-emit-response';
|
import { useEmitResponse } from '../../../hooks/use-emit-response';
|
||||||
import { getCustomerPaymentMethods } from './utils';
|
import { getCustomerPaymentMethods } from './utils';
|
||||||
|
|
||||||
|
@ -77,9 +79,10 @@ export const PaymentMethodDataProvider = ( {
|
||||||
};
|
};
|
||||||
} );
|
} );
|
||||||
const { isEditor, getPreviewData } = useEditorContext();
|
const { isEditor, getPreviewData } = useEditorContext();
|
||||||
const { setValidationErrors } = useValidationContext();
|
const { setValidationErrors } = useDispatch( VALIDATION_STORE_KEY );
|
||||||
const { createErrorNotice: addErrorNotice, removeNotice } =
|
const { createErrorNotice: addErrorNotice, removeNotice } = useDispatch(
|
||||||
useDispatch( 'core/notices' );
|
'core/notices'
|
||||||
|
);
|
||||||
const {
|
const {
|
||||||
isSuccessResponse,
|
isSuccessResponse,
|
||||||
isErrorResponse,
|
isErrorResponse,
|
||||||
|
|
|
@ -3,7 +3,6 @@ export * from './add-to-cart-form';
|
||||||
export * from './cart-checkout';
|
export * from './cart-checkout';
|
||||||
export * from './store-notices';
|
export * from './store-notices';
|
||||||
export * from './store-snackbar-notices';
|
export * from './store-snackbar-notices';
|
||||||
export * from './validation';
|
|
||||||
export * from './container-width-context';
|
export * from './container-width-context';
|
||||||
export * from './editor-context';
|
export * from './editor-context';
|
||||||
export * from './query-state-context';
|
export * from './query-state-context';
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
export * from './validation-input-error';
|
|
|
@ -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 (
|
|
||||||
<div className="wc-block-components-validation-error" role="alert">
|
|
||||||
<p id={ getValidationErrorId( elementId ) }>{ errorMessage }</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
ValidationInputError.propTypes = {
|
|
||||||
errorMessage: PropTypes.string,
|
|
||||||
propertyName: PropTypes.string,
|
|
||||||
elementId: PropTypes.string,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ValidationInputError;
|
|
|
@ -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 (
|
|
||||||
<ValidationContext.Provider value={ context }>
|
|
||||||
{ children }
|
|
||||||
</ValidationContext.Provider>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,2 +0,0 @@
|
||||||
export * from './context';
|
|
||||||
export * from './components';
|
|
|
@ -6,7 +6,6 @@ import { useStoreCart } from '@woocommerce/base-context/hooks';
|
||||||
import { useEffect } from '@wordpress/element';
|
import { useEffect } from '@wordpress/element';
|
||||||
import LoadingMask from '@woocommerce/base-components/loading-mask';
|
import LoadingMask from '@woocommerce/base-components/loading-mask';
|
||||||
import {
|
import {
|
||||||
ValidationContextProvider,
|
|
||||||
StoreNoticesContainer,
|
StoreNoticesContainer,
|
||||||
SnackbarNoticesContainer,
|
SnackbarNoticesContainer,
|
||||||
} from '@woocommerce/base-context';
|
} from '@woocommerce/base-context';
|
||||||
|
@ -39,9 +38,7 @@ const Cart = ( { children, attributes = {} } ) => {
|
||||||
hasDarkControls,
|
hasDarkControls,
|
||||||
} }
|
} }
|
||||||
>
|
>
|
||||||
<ValidationContextProvider>
|
|
||||||
{ children }
|
{ children }
|
||||||
</ValidationContextProvider>
|
|
||||||
</CartBlockContext.Provider>
|
</CartBlockContext.Provider>
|
||||||
</LoadingMask>
|
</LoadingMask>
|
||||||
);
|
);
|
||||||
|
|
|
@ -6,8 +6,6 @@ import classnames from 'classnames';
|
||||||
import { createInterpolateElement, useEffect } from '@wordpress/element';
|
import { createInterpolateElement, useEffect } from '@wordpress/element';
|
||||||
import { useStoreCart } from '@woocommerce/base-context/hooks';
|
import { useStoreCart } from '@woocommerce/base-context/hooks';
|
||||||
import {
|
import {
|
||||||
useValidationContext,
|
|
||||||
ValidationContextProvider,
|
|
||||||
CheckoutProvider,
|
CheckoutProvider,
|
||||||
SnackbarNoticesContainer,
|
SnackbarNoticesContainer,
|
||||||
} from '@woocommerce/base-context';
|
} 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 { CURRENT_USER_IS_ADMIN, getSetting } from '@woocommerce/settings';
|
||||||
import { SlotFillProvider } from '@woocommerce/blocks-checkout';
|
import { SlotFillProvider } from '@woocommerce/blocks-checkout';
|
||||||
import withScrollToTop from '@woocommerce/base-hocs/with-scroll-to-top';
|
import withScrollToTop from '@woocommerce/base-hocs/with-scroll-to-top';
|
||||||
import { useSelect } from '@wordpress/data';
|
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
|
* Internal dependencies
|
||||||
|
@ -120,8 +121,13 @@ const ScrollOnError = ( {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
const { hasValidationErrors, showAllValidationErrors } =
|
const { hasValidationErrors } = useSelect( ( select ) => {
|
||||||
useValidationContext();
|
const store = select( VALIDATION_STORE_KEY );
|
||||||
|
return {
|
||||||
|
hasValidationErrors: store.hasValidationErrors(),
|
||||||
|
};
|
||||||
|
} );
|
||||||
|
const { showAllValidationErrors } = useDispatch( VALIDATION_STORE_KEY );
|
||||||
|
|
||||||
const hasErrorsToDisplay =
|
const hasErrorsToDisplay =
|
||||||
checkoutIsIdle &&
|
checkoutIsIdle &&
|
||||||
|
@ -181,14 +187,12 @@ const Block = ( {
|
||||||
<SnackbarNoticesContainer context="wc/checkout" />
|
<SnackbarNoticesContainer context="wc/checkout" />
|
||||||
<StoreNoticesProvider>
|
<StoreNoticesProvider>
|
||||||
<StoreNoticesContainer context="wc/checkout" />
|
<StoreNoticesContainer context="wc/checkout" />
|
||||||
<ValidationContextProvider>
|
|
||||||
{ /* SlotFillProvider need to be defined before CheckoutProvider so fills have the SlotFill context ready when they mount. */ }
|
{ /* SlotFillProvider need to be defined before CheckoutProvider so fills have the SlotFill context ready when they mount. */ }
|
||||||
<SlotFillProvider>
|
<SlotFillProvider>
|
||||||
<CheckoutProvider>
|
<CheckoutProvider>
|
||||||
<SidebarLayout
|
<SidebarLayout
|
||||||
className={ classnames( 'wc-block-checkout', {
|
className={ classnames( 'wc-block-checkout', {
|
||||||
'has-dark-controls':
|
'has-dark-controls': attributes.hasDarkControls,
|
||||||
attributes.hasDarkControls,
|
|
||||||
} ) }
|
} ) }
|
||||||
>
|
>
|
||||||
<Checkout attributes={ attributes }>
|
<Checkout attributes={ attributes }>
|
||||||
|
@ -198,7 +202,6 @@ const Block = ( {
|
||||||
</SidebarLayout>
|
</SidebarLayout>
|
||||||
</CheckoutProvider>
|
</CheckoutProvider>
|
||||||
</SlotFillProvider>
|
</SlotFillProvider>
|
||||||
</ValidationContextProvider>
|
|
||||||
</StoreNoticesProvider>
|
</StoreNoticesProvider>
|
||||||
</BlockErrorBoundary>
|
</BlockErrorBoundary>
|
||||||
);
|
);
|
||||||
|
|
|
@ -12,6 +12,7 @@ import type { CheckoutState } from './default-state';
|
||||||
import type { DispatchFromMap, SelectFromMap } from '../mapped-types';
|
import type { DispatchFromMap, SelectFromMap } from '../mapped-types';
|
||||||
import * as selectors from './selectors';
|
import * as selectors from './selectors';
|
||||||
import * as actions from './actions';
|
import * as actions from './actions';
|
||||||
|
import { FieldValidationStatus } from '../types';
|
||||||
|
|
||||||
export type CheckoutAfterProcessingWithErrorEventData = {
|
export type CheckoutAfterProcessingWithErrorEventData = {
|
||||||
redirectUrl: CheckoutState[ 'redirectUrl' ];
|
redirectUrl: CheckoutState[ 'redirectUrl' ];
|
||||||
|
@ -53,7 +54,9 @@ export type emitValidateEventType = ( {
|
||||||
setValidationErrors,
|
setValidationErrors,
|
||||||
}: {
|
}: {
|
||||||
observers: EventObserversType;
|
observers: EventObserversType;
|
||||||
setValidationErrors: ( errors: Array< unknown > ) => void;
|
setValidationErrors: (
|
||||||
|
errors: Record< string, FieldValidationStatus >
|
||||||
|
) => void;
|
||||||
} ) => ( {
|
} ) => ( {
|
||||||
dispatch,
|
dispatch,
|
||||||
registry,
|
registry,
|
||||||
|
|
|
@ -10,6 +10,7 @@ export { SCHEMA_STORE_KEY } from './schema';
|
||||||
export { COLLECTIONS_STORE_KEY } from './collections';
|
export { COLLECTIONS_STORE_KEY } from './collections';
|
||||||
export { CART_STORE_KEY } from './cart';
|
export { CART_STORE_KEY } from './cart';
|
||||||
export { CHECKOUT_STORE_KEY } from './checkout';
|
export { CHECKOUT_STORE_KEY } from './checkout';
|
||||||
|
export { VALIDATION_STORE_KEY } from './validation';
|
||||||
export { QUERY_STATE_STORE_KEY } from './query-state';
|
export { QUERY_STATE_STORE_KEY } from './query-state';
|
||||||
export * from './constants';
|
export * from './constants';
|
||||||
export * from './types';
|
export * from './types';
|
||||||
|
|
|
@ -42,3 +42,8 @@ export function assertResponseIsValid(
|
||||||
}
|
}
|
||||||
throw new Error( 'Response not valid' );
|
throw new Error( 'Response not valid' );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface FieldValidationStatus {
|
||||||
|
message: string;
|
||||||
|
hidden: boolean;
|
||||||
|
}
|
||||||
|
|
|
@ -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;
|
|
@ -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
|
||||||
|
>;
|
|
@ -0,0 +1 @@
|
||||||
|
export const STORE_KEY = 'wc/store/validation';
|
|
@ -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;
|
||||||
|
};
|
||||||
|
}
|
|
@ -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;
|
|
@ -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;
|
||||||
|
};
|
|
@ -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,
|
||||||
|
},
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
} );
|
|
@ -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 );
|
||||||
|
} );
|
||||||
|
} );
|
|
@ -5,6 +5,7 @@
|
||||||
import type PaymentMethodLabel from '@woocommerce/base-components/cart-checkout/payment-method-label';
|
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 PaymentMethodIcons from '@woocommerce/base-components/cart-checkout/payment-method-icons';
|
||||||
import type LoadingMask from '@woocommerce/base-components/loading-mask';
|
import type LoadingMask from '@woocommerce/base-components/loading-mask';
|
||||||
|
import type ValidationInputError from '@woocommerce/base-components/validation-input-error';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
|
@ -76,7 +77,7 @@ export interface ComponentProps {
|
||||||
// A component used for displaying payment method labels, including an icon.
|
// A component used for displaying payment method labels, including an icon.
|
||||||
PaymentMethodLabel: typeof PaymentMethodLabel;
|
PaymentMethodLabel: typeof PaymentMethodLabel;
|
||||||
// A container for holding validation errors
|
// A container for holding validation errors
|
||||||
ValidationInputError: () => JSX.Element | null;
|
ValidationInputError: typeof ValidationInputError;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EmitResponseProps {
|
export interface EmitResponseProps {
|
||||||
|
|
Loading…
Reference in New Issue