{ __( 'Dimensions', 'woocommerce' ) }
diff --git a/packages/js/product-editor/src/components/editor/editor.tsx b/packages/js/product-editor/src/components/editor/editor.tsx
index 69ca8144bbe42..b8a27b4366d14 100644
--- a/packages/js/product-editor/src/components/editor/editor.tsx
+++ b/packages/js/product-editor/src/components/editor/editor.tsx
@@ -36,6 +36,7 @@ import { FullscreenMode, InterfaceSkeleton } from '@wordpress/interface';
*/
import { Header } from '../header';
import { BlockEditor } from '../block-editor';
+import { ValidationProvider } from '../../contexts/validation-context';
export type ProductEditorSettings = Partial<
EditorSettings & EditorBlockListSettings
@@ -62,31 +63,33 @@ export function Editor( { product, settings }: EditorProps ) {
-
- }
- content={
- <>
-
+
- { /* @ts-expect-error 'scope' does exist. @types/wordpress__plugins is outdated. */ }
-
- >
- }
- />
+ }
+ content={
+ <>
+
+ { /* @ts-expect-error 'scope' does exist. @types/wordpress__plugins is outdated. */ }
+
+ >
+ }
+ />
-
+
+
diff --git a/packages/js/product-editor/src/components/header/hooks/use-preview/use-preview.tsx b/packages/js/product-editor/src/components/header/hooks/use-preview/use-preview.tsx
index 4ea6a32af8181..54f93a9b347f4 100644
--- a/packages/js/product-editor/src/components/header/hooks/use-preview/use-preview.tsx
+++ b/packages/js/product-editor/src/components/header/hooks/use-preview/use-preview.tsx
@@ -9,6 +9,11 @@ import { useRef } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { MouseEvent } from 'react';
+/**
+ * Internal dependencies
+ */
+import { useValidations } from '../../../../contexts/validation-context';
+
export function usePreview( {
disabled,
onClick,
@@ -41,8 +46,6 @@ export function usePreview( {
( select ) => {
const { hasEditsForEntityRecord, isSavingEntityRecord } =
select( 'core' );
- const { isPostSavingLocked } = select( 'core/editor' );
- const isSavingLocked = isPostSavingLocked();
const isSaving = isSavingEntityRecord< boolean >(
'postType',
'product',
@@ -50,7 +53,7 @@ export function usePreview( {
);
return {
- isDisabled: isSavingLocked || isSaving,
+ isDisabled: isSaving,
hasEdits: hasEditsForEntityRecord< boolean >(
'postType',
'product',
@@ -61,7 +64,9 @@ export function usePreview( {
[ productId ]
);
- const ariaDisabled = disabled || isDisabled;
+ const { isValidating, validate } = useValidations();
+
+ const ariaDisabled = disabled || isDisabled || isValidating;
const { editEntityRecord, saveEditedEntityRecord } = useDispatch( 'core' );
@@ -97,6 +102,8 @@ export function usePreview( {
event.preventDefault();
try {
+ await validate();
+
// If the product status is `auto-draft` it's not possible to
// reach the preview page, so the status is changed to `draft`
// before redirecting.
diff --git a/packages/js/product-editor/src/components/header/hooks/use-publish/use-publish.tsx b/packages/js/product-editor/src/components/header/hooks/use-publish/use-publish.tsx
index ba149acd9f6df..19218f0bcddbe 100644
--- a/packages/js/product-editor/src/components/header/hooks/use-publish/use-publish.tsx
+++ b/packages/js/product-editor/src/components/header/hooks/use-publish/use-publish.tsx
@@ -8,6 +8,11 @@ import { useDispatch, useSelect } from '@wordpress/data';
import { __ } from '@wordpress/i18n';
import { MouseEvent } from 'react';
+/**
+ * Internal dependencies
+ */
+import { useValidations } from '../../../../contexts/validation-context';
+
export function usePublish( {
disabled,
onClick,
@@ -29,22 +34,14 @@ export function usePublish( {
'status'
);
- const { hasEdits, isDisabled, isBusy } = useSelect(
+ const { isValidating, validate } = useValidations();
+
+ const { isSaving } = useSelect(
( select ) => {
- const { hasEditsForEntityRecord, isSavingEntityRecord } =
- select( 'core' );
- const { isPostSavingLocked } = select( 'core/editor' );
- const isSavingLocked = isPostSavingLocked();
- const isSaving = isSavingEntityRecord< boolean >(
- 'postType',
- 'product',
- productId
- );
+ const { isSavingEntityRecord } = select( 'core' );
return {
- isDisabled: isSavingLocked || isSaving,
- isBusy: isSaving,
- hasEdits: hasEditsForEntityRecord< boolean >(
+ isSaving: isSavingEntityRecord< boolean >(
'postType',
'product',
productId
@@ -54,22 +51,20 @@ export function usePublish( {
[ productId ]
);
+ const isBusy = isSaving || isValidating;
+
const isCreating = productStatus === 'auto-draft';
- const ariaDisabled =
- disabled || isDisabled || ( productStatus === 'publish' && ! hasEdits );
const { editEntityRecord, saveEditedEntityRecord } = useDispatch( 'core' );
async function handleClick( event: MouseEvent< HTMLButtonElement > ) {
- if ( ariaDisabled ) {
- return event.preventDefault();
- }
-
if ( onClick ) {
onClick( event );
}
try {
+ await validate();
+
// The publish button click not only change the status of the product
// but also save all the pending changes. So even if the status is
// publish it's possible to save the product too.
@@ -85,7 +80,7 @@ export function usePublish( {
productId
);
- if ( onPublishSuccess ) {
+ if ( publishedProduct && onPublishSuccess ) {
onPublishSuccess( publishedProduct );
}
} catch ( error ) {
@@ -100,7 +95,6 @@ export function usePublish( {
? __( 'Add', 'woocommerce' )
: __( 'Save', 'woocommerce' ),
...props,
- 'aria-disabled': ariaDisabled,
isBusy,
variant: 'primary',
onClick: handleClick,
diff --git a/packages/js/product-editor/src/components/header/hooks/use-save-draft/use-save-draft.tsx b/packages/js/product-editor/src/components/header/hooks/use-save-draft/use-save-draft.tsx
index 06aa6b00312ab..3a76c532f6fd7 100644
--- a/packages/js/product-editor/src/components/header/hooks/use-save-draft/use-save-draft.tsx
+++ b/packages/js/product-editor/src/components/header/hooks/use-save-draft/use-save-draft.tsx
@@ -10,6 +10,11 @@ import { check } from '@wordpress/icons';
import { createElement, Fragment } from '@wordpress/element';
import { MouseEvent, ReactNode } from 'react';
+/**
+ * Internal dependencies
+ */
+import { useValidations } from '../../../../contexts/validation-context';
+
export function useSaveDraft( {
disabled,
onClick,
@@ -35,8 +40,6 @@ export function useSaveDraft( {
( select ) => {
const { hasEditsForEntityRecord, isSavingEntityRecord } =
select( 'core' );
- const { isPostSavingLocked } = select( 'core/editor' );
- const isSavingLocked = isPostSavingLocked();
const isSaving = isSavingEntityRecord< boolean >(
'postType',
'product',
@@ -44,7 +47,7 @@ export function useSaveDraft( {
);
return {
- isDisabled: isSavingLocked || isSaving,
+ isDisabled: isSaving,
hasEdits: hasEditsForEntityRecord< boolean >(
'postType',
'product',
@@ -55,8 +58,13 @@ export function useSaveDraft( {
[ productId ]
);
+ const { isValidating, validate } = useValidations();
+
const ariaDisabled =
- disabled || isDisabled || ( productStatus !== 'publish' && ! hasEdits );
+ disabled ||
+ isDisabled ||
+ ( productStatus !== 'publish' && ! hasEdits ) ||
+ isValidating;
const { editEntityRecord, saveEditedEntityRecord } = useDispatch( 'core' );
@@ -70,6 +78,8 @@ export function useSaveDraft( {
}
try {
+ await validate();
+
await editEntityRecord( 'postType', 'product', productId, {
status: 'draft',
} );
diff --git a/packages/js/product-editor/src/contexts/validation-context/helpers.ts b/packages/js/product-editor/src/contexts/validation-context/helpers.ts
new file mode 100644
index 0000000000000..88068c73bcc52
--- /dev/null
+++ b/packages/js/product-editor/src/contexts/validation-context/helpers.ts
@@ -0,0 +1,33 @@
+/**
+ * Internal dependencies
+ */
+import { ValidationErrors } from './types';
+
+export function findFirstInvalidElement< E extends Element = Element >(
+ elementsMap: Record< string, E >,
+ errors: ValidationErrors
+): E | undefined {
+ const fieldRefsWithError = Object.entries( elementsMap ).filter(
+ ( [ validatorId, element ] ) =>
+ // Pick the element if it is under the selected tab.
+ element?.closest( '.is-selected[role="tabpanel"]' ) &&
+ Boolean( errors[ validatorId ] )
+ );
+
+ const [ firstFieldRefWithError ] = fieldRefsWithError.sort(
+ ( [ , firstElement ], [ , secondElement ] ) => {
+ if (
+ // eslint-disable-next-line no-bitwise
+ firstElement.compareDocumentPosition( secondElement ) &
+ Node.DOCUMENT_POSITION_FOLLOWING
+ ) {
+ return -1;
+ }
+ return 1;
+ }
+ );
+
+ const [ , firstElementWithError ] = firstFieldRefWithError ?? [];
+
+ return firstElementWithError;
+}
diff --git a/packages/js/product-editor/src/contexts/validation-context/index.ts b/packages/js/product-editor/src/contexts/validation-context/index.ts
new file mode 100644
index 0000000000000..ecee449db60a0
--- /dev/null
+++ b/packages/js/product-editor/src/contexts/validation-context/index.ts
@@ -0,0 +1,4 @@
+export * from './use-validation';
+export * from './use-validations';
+export * from './validation-provider';
+export * from './types';
diff --git a/packages/js/product-editor/src/contexts/validation-context/types.ts b/packages/js/product-editor/src/contexts/validation-context/types.ts
new file mode 100644
index 0000000000000..30aa1b8241be3
--- /dev/null
+++ b/packages/js/product-editor/src/contexts/validation-context/types.ts
@@ -0,0 +1,27 @@
+export type ValidatorResponse = Promise< ValidationError >;
+
+export type Validator< T > = ( initialValue?: T ) => ValidatorResponse;
+
+export type ValidationContextProps< T > = {
+ errors: ValidationErrors;
+ registerValidator(
+ validatorId: string,
+ validator: Validator< T >
+ ): React.Ref< HTMLElement >;
+ validateField( name: string ): ValidatorResponse;
+ validateAll(): Promise< ValidationErrors >;
+};
+
+export type ValidationProviderProps< T > = {
+ initialValue?: T;
+};
+
+export type ValidationError = string | undefined;
+export type ValidationErrors = Record< string, ValidationError >;
+
+export type ValidatorRegistration = {
+ name: string;
+ ref: React.Ref< HTMLElement >;
+ error?: ValidationError;
+ validate(): ValidatorResponse;
+};
diff --git a/packages/js/product-editor/src/contexts/validation-context/use-validation.ts b/packages/js/product-editor/src/contexts/validation-context/use-validation.ts
new file mode 100644
index 0000000000000..3f4ed824e501f
--- /dev/null
+++ b/packages/js/product-editor/src/contexts/validation-context/use-validation.ts
@@ -0,0 +1,37 @@
+/**
+ * External dependencies
+ */
+import { useContext, useMemo, useState } from '@wordpress/element';
+import { DependencyList } from 'react';
+
+/**
+ * Internal dependencies
+ */
+import { Validator } from './types';
+import { ValidationContext } from './validation-context';
+
+export function useValidation< T >(
+ validatorId: string,
+ validator: Validator< T >,
+ deps: DependencyList = []
+) {
+ const context = useContext( ValidationContext );
+ const [ isValidating, setIsValidating ] = useState( false );
+
+ const ref = useMemo(
+ () => context.registerValidator( validatorId, validator ),
+ [ validatorId, ...deps ]
+ );
+
+ return {
+ ref,
+ error: context.errors[ validatorId ],
+ isValidating,
+ async validate() {
+ setIsValidating( true );
+ return context.validateField( validatorId ).finally( () => {
+ setIsValidating( false );
+ } );
+ },
+ };
+}
diff --git a/packages/js/product-editor/src/contexts/validation-context/use-validations.ts b/packages/js/product-editor/src/contexts/validation-context/use-validations.ts
new file mode 100644
index 0000000000000..9efff3443aa8d
--- /dev/null
+++ b/packages/js/product-editor/src/contexts/validation-context/use-validations.ts
@@ -0,0 +1,42 @@
+/**
+ * External dependencies
+ */
+import { useContext, useState } from '@wordpress/element';
+
+/**
+ * Internal dependencies
+ */
+import { ValidationErrors } from './types';
+import { ValidationContext } from './validation-context';
+
+function isInvalid( errors: ValidationErrors ) {
+ return Object.values( errors ).some( Boolean );
+}
+
+export function useValidations() {
+ const context = useContext( ValidationContext );
+ const [ isValidating, setIsValidating ] = useState( false );
+
+ return {
+ isValidating,
+ async validate() {
+ setIsValidating( true );
+ return new Promise< void >( ( resolve, reject ) => {
+ context
+ .validateAll()
+ .then( ( errors ) => {
+ if ( isInvalid( errors ) ) {
+ reject( errors );
+ } else {
+ resolve();
+ }
+ } )
+ .catch( () => {
+ reject( context.errors );
+ } );
+ } ).finally( () => {
+ setIsValidating( false );
+ } );
+ },
+ };
+}
diff --git a/packages/js/product-editor/src/contexts/validation-context/validation-context.ts b/packages/js/product-editor/src/contexts/validation-context/validation-context.ts
new file mode 100644
index 0000000000000..750e1610a70a4
--- /dev/null
+++ b/packages/js/product-editor/src/contexts/validation-context/validation-context.ts
@@ -0,0 +1,19 @@
+/**
+ * External dependencies
+ */
+import { createContext } from '@wordpress/element';
+
+/**
+ * Internal dependencies
+ */
+import { ValidationContextProps } from './types';
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export const ValidationContext = createContext< ValidationContextProps< any > >(
+ {
+ errors: {},
+ registerValidator: () => () => {},
+ validateField: () => Promise.resolve( undefined ),
+ validateAll: () => Promise.resolve( {} ),
+ }
+);
diff --git a/packages/js/product-editor/src/contexts/validation-context/validation-provider.tsx b/packages/js/product-editor/src/contexts/validation-context/validation-provider.tsx
new file mode 100644
index 0000000000000..c700e269f5e84
--- /dev/null
+++ b/packages/js/product-editor/src/contexts/validation-context/validation-provider.tsx
@@ -0,0 +1,91 @@
+/**
+ * External dependencies
+ */
+import { createElement, useRef, useState } from '@wordpress/element';
+import { PropsWithChildren } from 'react';
+
+/**
+ * Internal dependencies
+ */
+import {
+ ValidationErrors,
+ ValidationProviderProps,
+ Validator,
+ ValidatorResponse,
+} from './types';
+import { ValidationContext } from './validation-context';
+import { findFirstInvalidElement } from './helpers';
+
+export function ValidationProvider< T >( {
+ initialValue,
+ children,
+}: PropsWithChildren< ValidationProviderProps< T > > ) {
+ const validatorsRef = useRef< Record< string, Validator< T > > >( {} );
+ const fieldRefs = useRef< Record< string, HTMLElement > >( {} );
+ const [ errors, setErrors ] = useState< ValidationErrors >( {} );
+
+ function registerValidator(
+ validatorId: string,
+ validator: Validator< T >
+ ): React.Ref< HTMLElement > {
+ validatorsRef.current = {
+ ...validatorsRef.current,
+ [ validatorId ]: validator,
+ };
+
+ return ( element: HTMLElement ) => {
+ fieldRefs.current[ validatorId ] = element;
+ };
+ }
+
+ async function validateField( validatorId: string ): ValidatorResponse {
+ const validators = validatorsRef.current;
+ if ( validatorId in validators ) {
+ const validator = validators[ validatorId ];
+ const result = validator( initialValue );
+
+ return result.then( ( error ) => {
+ setErrors( ( currentErrors ) => ( {
+ ...currentErrors,
+ [ validatorId ]: error,
+ } ) );
+ return error;
+ } );
+ }
+
+ return Promise.resolve( undefined );
+ }
+
+ async function validateAll(): Promise< ValidationErrors > {
+ const newErrors: ValidationErrors = {};
+ const validators = validatorsRef.current;
+
+ for ( const validatorId in validators ) {
+ newErrors[ validatorId ] = await validateField( validatorId );
+ }
+
+ setErrors( newErrors );
+
+ const firstElementWithError = findFirstInvalidElement(
+ fieldRefs.current,
+ newErrors
+ );
+
+ firstElementWithError?.focus();
+
+ return newErrors;
+ }
+
+ return (
+
+ { children }
+
+ );
+}
diff --git a/packages/js/product-editor/src/hooks/index.ts b/packages/js/product-editor/src/hooks/index.ts
index 646b526a553f9..e1ac9e9b7fc6b 100644
--- a/packages/js/product-editor/src/hooks/index.ts
+++ b/packages/js/product-editor/src/hooks/index.ts
@@ -2,7 +2,3 @@ export { useProductHelper as __experimentalUseProductHelper } from './use-produc
export { useProductMVPCESFooter as __experimentalUseProductMVPCESFooter } from './use-product-mvp-ces-footer';
export { useVariationsOrder as __experimentalUseVariationsOrder } from './use-variations-order';
export { useCurrencyInputProps as __experimentalUseCurrencyInputProps } from './use-currency-input-props';
-export {
- useValidation as __experimentalUseValidation,
- ValidationError,
-} from './use-validation';
diff --git a/packages/js/product-editor/src/hooks/use-validation/README.md b/packages/js/product-editor/src/hooks/use-validation/README.md
deleted file mode 100644
index 611f528792c80..0000000000000
--- a/packages/js/product-editor/src/hooks/use-validation/README.md
+++ /dev/null
@@ -1,46 +0,0 @@
-# useValidation
-
-This custom hook uses the helper functions `const { lockPostSaving, unlockPostSaving } = useDispatch( 'core/editor' );` to lock/unlock the current editing product before saving it.
-
-## Usage
-
-Syncronous validation
-
-```typescript
-import { useCallback } from '@wordpress/element';
-import { __ } from '@wordpress/i18n';
-import {
- __experimentalUseValidation as useValidation,
- ValidationError
-} from '@woocommerce/product-editor';
-
-const product = ...;
-
-const validateTitle = useCallback( (): ValidationError => {
- if ( product.title.length < 2 ) {
- return __( 'Title must be more than 1 character', 'text-domain' );
- }
-}, [ product.title ] );
-
-const validationError = useValidation( 'product/title', validateTitle );
-```
-
-Asyncronous validation
-
-```typescript
-import { useCallback } from '@wordpress/element';
-import {
- __experimentalUseValidation as useValidation,
- ValidationError
-} from '@woocommerce/product-editor';
-
-const product = ...;
-
-const validateSlug = useCallback( async (): Promise< ValidationError > => {
- return fetch( `.../validate-slug?slug=${ product.slug }` )
- .then( ( response ) => response.json() )
- .then( ( { errorMessage } ) => errorMessage );
-}, [ product.slug ] );
-
-const validationError = useValidation( 'product/slug', validateSlug );
-```
diff --git a/packages/js/product-editor/src/hooks/use-validation/index.ts b/packages/js/product-editor/src/hooks/use-validation/index.ts
deleted file mode 100644
index f7a6938360a99..0000000000000
--- a/packages/js/product-editor/src/hooks/use-validation/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export * from './use-validation';
-export * from './types';
diff --git a/packages/js/product-editor/src/hooks/use-validation/test/use-validation.test.ts b/packages/js/product-editor/src/hooks/use-validation/test/use-validation.test.ts
deleted file mode 100644
index e9e3761865e79..0000000000000
--- a/packages/js/product-editor/src/hooks/use-validation/test/use-validation.test.ts
+++ /dev/null
@@ -1,123 +0,0 @@
-/**
- * External dependencies
- */
-import {
- RenderHookResult,
- act,
- renderHook,
-} from '@testing-library/react-hooks';
-import { useDispatch } from '@wordpress/data';
-
-/**
- * Internal dependencies
- */
-import { useValidation } from '../use-validation';
-import { ValidationError } from '../types';
-
-jest.mock( '@wordpress/data', () => ( {
- useDispatch: jest.fn(),
-} ) );
-
-describe( 'useValidation', () => {
- const useDispatchMock = useDispatch as jest.Mock;
- const lockPostSaving = jest.fn();
- const unlockPostSaving = jest.fn();
- let hookResult = {} as RenderHookResult< unknown, ValidationError >;
-
- beforeEach( () => {
- useDispatchMock.mockReturnValue( {
- lockPostSaving,
- unlockPostSaving,
- } );
- } );
-
- afterEach( async () => {
- jest.clearAllMocks();
- } );
-
- describe( 'sync', () => {
- it( 'should lock the editor if validate returns an error', async () => {
- const validationError = 'Invalid name';
-
- await act( async () => {
- hookResult = renderHook( () =>
- useValidation( 'product/name', () => validationError )
- );
- } );
-
- const { result } = hookResult;
-
- expect( result.current ).toBe( validationError );
- expect( lockPostSaving ).toHaveBeenCalled();
- expect( unlockPostSaving ).not.toHaveBeenCalled();
- } );
-
- it( 'should unlock the editor if validate returns no error', async () => {
- await act( async () => {
- hookResult = renderHook( () =>
- useValidation( 'product/name', () => undefined )
- );
- } );
-
- const { result } = hookResult;
-
- expect( result.current ).toBeUndefined();
- expect( lockPostSaving ).not.toHaveBeenCalled();
- expect( unlockPostSaving ).toHaveBeenCalled();
- } );
- } );
-
- describe( 'async', () => {
- it( 'should lock the editor if validate resolves an error', async () => {
- const validationError = 'Invalid name';
-
- await act( async () => {
- hookResult = renderHook( () =>
- useValidation( 'product/name', () =>
- Promise.resolve( validationError )
- )
- );
- } );
-
- const { result } = hookResult;
-
- expect( result.current ).toBe( validationError );
- expect( lockPostSaving ).toHaveBeenCalled();
- expect( unlockPostSaving ).not.toHaveBeenCalled();
- } );
-
- it( 'should lock the editor if validate rejects', async () => {
- const validationError = 'Invalid name';
-
- await act( async () => {
- hookResult = renderHook( () =>
- useValidation( 'product/name', () =>
- Promise.reject( validationError )
- )
- );
- } );
-
- const { result } = hookResult;
-
- expect( result.current ).toBe( validationError );
- expect( lockPostSaving ).toHaveBeenCalled();
- expect( unlockPostSaving ).not.toHaveBeenCalled();
- } );
-
- it( 'should unlock the editor if validate resolves undefined', async () => {
- await act( async () => {
- hookResult = renderHook( () =>
- useValidation( 'product/name', () =>
- Promise.resolve( undefined )
- )
- );
- } );
-
- const { result } = hookResult;
-
- expect( result.current ).toBeUndefined();
- expect( lockPostSaving ).not.toHaveBeenCalled();
- expect( unlockPostSaving ).toHaveBeenCalled();
- } );
- } );
-} );
diff --git a/packages/js/product-editor/src/hooks/use-validation/types.ts b/packages/js/product-editor/src/hooks/use-validation/types.ts
deleted file mode 100644
index 4e62d30b92d53..0000000000000
--- a/packages/js/product-editor/src/hooks/use-validation/types.ts
+++ /dev/null
@@ -1 +0,0 @@
-export type ValidationError = undefined | string | Error;
diff --git a/packages/js/product-editor/src/hooks/use-validation/use-validation.ts b/packages/js/product-editor/src/hooks/use-validation/use-validation.ts
deleted file mode 100644
index b95ddc08d14ed..0000000000000
--- a/packages/js/product-editor/src/hooks/use-validation/use-validation.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-/**
- * External dependencies
- */
-import { useDispatch } from '@wordpress/data';
-import { useEffect, useState } from '@wordpress/element';
-
-/**
- * Internal dependencies
- */
-import { ValidationError } from './types';
-
-/**
- * Signals that product saving is locked.
- *
- * @param lockName The namespace used to lock the product saving if validation fails.
- * @param validate The validator function.
- * @return The error message.
- */
-export function useValidation(
- lockName: string,
- validate: () => ValidationError | Promise< ValidationError >
-): ValidationError {
- const [ validationError, setValidationError ] =
- useState< ValidationError >();
- const { lockPostSaving, unlockPostSaving } = useDispatch( 'core/editor' );
-
- useEffect( () => {
- let validationResponse = validate();
-
- if ( ! ( validationResponse instanceof Promise ) ) {
- validationResponse = Promise.resolve( validationResponse );
- }
-
- validationResponse
- .then( ( message ) => {
- if ( message ) {
- lockPostSaving( lockName );
- } else {
- unlockPostSaving( lockName );
- }
- setValidationError( message );
- } )
- .catch( ( error ) => {
- lockPostSaving( lockName );
- setValidationError( error.message ?? error );
- } );
- }, [ lockName, validate, lockPostSaving, unlockPostSaving ] );
-
- return validationError;
-}
diff --git a/packages/js/product-editor/src/index.ts b/packages/js/product-editor/src/index.ts
index c62b84726c467..6f5473d7367a2 100644
--- a/packages/js/product-editor/src/index.ts
+++ b/packages/js/product-editor/src/index.ts
@@ -14,3 +14,5 @@ export * from './utils';
* Hooks
*/
export * from './hooks';
+export { useValidation, useValidations } from './contexts/validation-context';
+export * from './contexts/validation-context/types';