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 { 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 ) || {};
|
||||
|
||||
|
|
|
@ -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 );
|
||||
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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( () => {
|
||||
|
|
|
@ -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 (
|
||||
<ValidationContextProvider>
|
||||
<StoryComponent />
|
||||
</ValidationContextProvider>
|
||||
);
|
||||
return <StoryComponent />;
|
||||
},
|
||||
];
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 ) => (
|
||||
<ValidationContextProvider>
|
||||
<StoryComponent />
|
||||
</ValidationContextProvider>
|
||||
),
|
||||
],
|
||||
decorators: [ ( StoryComponent ) => <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' );
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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 )
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
"../hocs",
|
||||
"../../atomic/utils",
|
||||
"../../atomic/blocks/component-init.js",
|
||||
"../../data",
|
||||
"../../shared/context"
|
||||
],
|
||||
"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 { 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,
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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 }` ),
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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 (
|
||||
<ValidationContextProvider>
|
||||
<AddToCartFormStateContextProvider
|
||||
product={ product }
|
||||
showFormElements={ showFormElements }
|
||||
>
|
||||
{ children }
|
||||
<FormSubmit />
|
||||
</AddToCartFormStateContextProvider>
|
||||
</ValidationContextProvider>
|
||||
<AddToCartFormStateContextProvider
|
||||
product={ product }
|
||||
showFormElements={ showFormElements }
|
||||
>
|
||||
{ children }
|
||||
<FormSubmit />
|
||||
</AddToCartFormStateContextProvider>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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 ) {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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 LoadingMask from '@woocommerce/base-components/loading-mask';
|
||||
import {
|
||||
ValidationContextProvider,
|
||||
StoreNoticesContainer,
|
||||
SnackbarNoticesContainer,
|
||||
} from '@woocommerce/base-context';
|
||||
|
@ -39,9 +38,7 @@ const Cart = ( { children, attributes = {} } ) => {
|
|||
hasDarkControls,
|
||||
} }
|
||||
>
|
||||
<ValidationContextProvider>
|
||||
{ children }
|
||||
</ValidationContextProvider>
|
||||
{ children }
|
||||
</CartBlockContext.Provider>
|
||||
</LoadingMask>
|
||||
);
|
||||
|
|
|
@ -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 = ( {
|
|||
<SnackbarNoticesContainer context="wc/checkout" />
|
||||
<StoreNoticesProvider>
|
||||
<StoreNoticesContainer context="wc/checkout" />
|
||||
<ValidationContextProvider>
|
||||
{ /* SlotFillProvider need to be defined before CheckoutProvider so fills have the SlotFill context ready when they mount. */ }
|
||||
<SlotFillProvider>
|
||||
<CheckoutProvider>
|
||||
<SidebarLayout
|
||||
className={ classnames( 'wc-block-checkout', {
|
||||
'has-dark-controls':
|
||||
attributes.hasDarkControls,
|
||||
} ) }
|
||||
>
|
||||
<Checkout attributes={ attributes }>
|
||||
{ children }
|
||||
</Checkout>
|
||||
<ScrollOnError scrollToTop={ scrollToTop } />
|
||||
</SidebarLayout>
|
||||
</CheckoutProvider>
|
||||
</SlotFillProvider>
|
||||
</ValidationContextProvider>
|
||||
{ /* SlotFillProvider need to be defined before CheckoutProvider so fills have the SlotFill context ready when they mount. */ }
|
||||
<SlotFillProvider>
|
||||
<CheckoutProvider>
|
||||
<SidebarLayout
|
||||
className={ classnames( 'wc-block-checkout', {
|
||||
'has-dark-controls': attributes.hasDarkControls,
|
||||
} ) }
|
||||
>
|
||||
<Checkout attributes={ attributes }>
|
||||
{ children }
|
||||
</Checkout>
|
||||
<ScrollOnError scrollToTop={ scrollToTop } />
|
||||
</SidebarLayout>
|
||||
</CheckoutProvider>
|
||||
</SlotFillProvider>
|
||||
</StoreNoticesProvider>
|
||||
</BlockErrorBoundary>
|
||||
);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -42,3 +42,8 @@ export function assertResponseIsValid(
|
|||
}
|
||||
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 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 {
|
||||
|
|
Loading…
Reference in New Issue