Add link into the error snackbar (#49246)
* Add link to snack bar # Conflicts: # packages/js/product-editor/src/hooks/use-product-manager/use-product-manager.ts # Conflicts: # packages/js/product-editor/src/contexts/validation-context/validation-provider.tsx * Fix variations * Add useBlocksHelper * Remove old tests * Remove timeout * Fix typo * Fix errors * Add changelog
This commit is contained in:
parent
09bf08e1b1
commit
a9de74df6f
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: dev
|
||||
|
||||
Add link into the error snackbar #49246
|
|
@ -33,10 +33,7 @@ import { ProductEditorSettings } from '../../../components';
|
|||
import { BlockFill } from '../../../components/block-slot-fill';
|
||||
import { useValidations } from '../../../contexts/validation-context';
|
||||
import { TRACKS_SOURCE } from '../../../constants';
|
||||
import {
|
||||
WPError,
|
||||
getProductErrorMessageAndProps,
|
||||
} from '../../../utils/get-product-error-message-and-props';
|
||||
import { WPError, useErrorHandler } from '../../../hooks/use-error-handler';
|
||||
import type {
|
||||
ProductEditorBlockEditProps,
|
||||
ProductFormPostProps,
|
||||
|
@ -54,6 +51,8 @@ export function ProductDetailsSectionDescriptionBlockEdit( {
|
|||
}: ProductEditorBlockEditProps< ProductDetailsSectionDescriptionBlockAttributes > ) {
|
||||
const blockProps = useWooBlockProps( attributes );
|
||||
|
||||
const { getProductErrorMessageAndProps } = useErrorHandler();
|
||||
|
||||
const { productTemplates, productTemplate: selectedProductTemplate } =
|
||||
useSelect( ( select ) => {
|
||||
const { getEditorSettings } = select( 'core/editor' );
|
||||
|
|
|
@ -13,7 +13,7 @@ import { MouseEvent } from 'react';
|
|||
* Internal dependencies
|
||||
*/
|
||||
import { useValidations } from '../../../../contexts/validation-context';
|
||||
import { WPError } from '../../../../utils/get-product-error-message-and-props';
|
||||
import { WPError } from '../../../../hooks/use-error-handler';
|
||||
import { useProductURL } from '../../../../hooks/use-product-url';
|
||||
import { PreviewButtonProps } from '../../preview-button';
|
||||
import { errorHandler } from '../../../../hooks/use-product-manager';
|
||||
|
|
|
@ -13,7 +13,7 @@ import { useShortcut } from '@wordpress/keyboard-shortcuts';
|
|||
*/
|
||||
import { useProductManager } from '../../../../hooks/use-product-manager';
|
||||
import { useProductScheduled } from '../../../../hooks/use-product-scheduled';
|
||||
import type { WPError } from '../../../../utils/get-product-error-message-and-props';
|
||||
import type { WPError } from '../../../../hooks/use-error-handler';
|
||||
import type { PublishButtonProps } from '../../publish-button';
|
||||
|
||||
export function usePublish< T = Product >( {
|
||||
|
|
|
@ -15,7 +15,7 @@ import { useShortcut } from '@wordpress/keyboard-shortcuts';
|
|||
* Internal dependencies
|
||||
*/
|
||||
import { useValidations } from '../../../../contexts/validation-context';
|
||||
import { WPError } from '../../../../utils/get-product-error-message-and-props';
|
||||
import { WPError } from '../../../../hooks/use-error-handler';
|
||||
import { SaveDraftButtonProps } from '../../save-draft-button';
|
||||
import { recordProductEvent } from '../../../../utils/record-product-event';
|
||||
import { errorHandler } from '../../../../hooks/use-product-manager';
|
||||
|
|
|
@ -11,7 +11,7 @@ import { useDispatch } from '@wordpress/data';
|
|||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { getProductErrorMessageAndProps } from '../../../utils/get-product-error-message-and-props';
|
||||
import { useErrorHandler } from '../../../hooks/use-error-handler';
|
||||
import { usePreview } from '../hooks/use-preview';
|
||||
import { PreviewButtonProps } from './types';
|
||||
import { TRACKS_SOURCE } from '../../../constants';
|
||||
|
@ -22,6 +22,7 @@ export function PreviewButton( {
|
|||
...props
|
||||
}: PreviewButtonProps ) {
|
||||
const { createErrorNotice } = useDispatch( 'core/notices' );
|
||||
const { getProductErrorMessageAndProps } = useErrorHandler();
|
||||
|
||||
const previewButtonProps = usePreview( {
|
||||
productStatus,
|
||||
|
|
|
@ -17,7 +17,7 @@ import { recordEvent } from '@woocommerce/tracks';
|
|||
import { useProductManager } from '../../../../hooks/use-product-manager';
|
||||
import { useProductScheduled } from '../../../../hooks/use-product-scheduled';
|
||||
import { recordProductEvent } from '../../../../utils/record-product-event';
|
||||
import { getProductErrorMessageAndProps } from '../../../../utils/get-product-error-message-and-props';
|
||||
import { useErrorHandler } from '../../../../hooks/use-error-handler';
|
||||
import { ButtonWithDropdownMenu } from '../../../button-with-dropdown-menu';
|
||||
import { SchedulePublishModal } from '../../../schedule-publish-modal';
|
||||
import { showSuccessNotice } from '../utils';
|
||||
|
@ -42,6 +42,7 @@ export function PublishButtonMenu( {
|
|||
postType,
|
||||
'status'
|
||||
);
|
||||
const { getProductErrorMessageAndProps } = useErrorHandler();
|
||||
|
||||
function scheduleProduct( dateString?: string ) {
|
||||
schedule( dateString )
|
||||
|
|
|
@ -14,7 +14,7 @@ import { recordEvent } from '@woocommerce/tracks';
|
|||
* Internal dependencies
|
||||
*/
|
||||
import { store as productEditorUiStore } from '../../../store/product-editor-ui';
|
||||
import { getProductErrorMessageAndProps } from '../../../utils/get-product-error-message-and-props';
|
||||
import { useErrorHandler } from '../../../hooks/use-error-handler';
|
||||
import { recordProductEvent } from '../../../utils/record-product-event';
|
||||
import { useFeedbackBar } from '../../../hooks/use-feedback-bar';
|
||||
import { TRACKS_SOURCE } from '../../../constants';
|
||||
|
@ -33,6 +33,7 @@ export function PublishButton( {
|
|||
const { createErrorNotice } = useDispatch( 'core/notices' );
|
||||
const { maybeShowFeedbackBar } = useFeedbackBar();
|
||||
const { openPrepublishPanel } = useDispatch( productEditorUiStore );
|
||||
const { getProductErrorMessageAndProps } = useErrorHandler();
|
||||
|
||||
const [ editedStatus, , prevStatus ] = useEntityProp< Product[ 'status' ] >(
|
||||
'postType',
|
||||
|
|
|
@ -11,7 +11,7 @@ import { useDispatch } from '@wordpress/data';
|
|||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { getProductErrorMessageAndProps } from '../../../utils/get-product-error-message-and-props';
|
||||
import { useErrorHandler } from '../../../hooks/use-error-handler';
|
||||
import { recordProductEvent } from '../../../utils/record-product-event';
|
||||
import { useSaveDraft } from '../hooks/use-save-draft';
|
||||
import { SaveDraftButtonProps } from './types';
|
||||
|
@ -28,6 +28,8 @@ export function SaveDraftButton( {
|
|||
|
||||
const { maybeShowFeedbackBar } = useFeedbackBar();
|
||||
|
||||
const { getProductErrorMessageAndProps } = useErrorHandler();
|
||||
|
||||
const saveDraftButtonProps = useSaveDraft( {
|
||||
productStatus,
|
||||
productType,
|
||||
|
|
|
@ -12,6 +12,7 @@ export type ValidationContextProps< T > = {
|
|||
validator: Validator< T >
|
||||
): React.Ref< HTMLElement >;
|
||||
unRegisterValidator( validatorId: string ): void;
|
||||
getFieldByValidatorId: ( validatorId: string ) => Promise< HTMLElement >;
|
||||
validateField(
|
||||
name: string,
|
||||
newData?: Record< string, unknown >
|
||||
|
|
|
@ -17,6 +17,13 @@ export function useValidations< T = unknown >() {
|
|||
const context = useContext( ValidationContext );
|
||||
const [ isValidating, setIsValidating ] = useState( false );
|
||||
|
||||
async function focusByValidatorId( validatorId: string ) {
|
||||
const field = await context.getFieldByValidatorId( validatorId );
|
||||
if ( field ) {
|
||||
field.focus();
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
isValidating,
|
||||
async validate( newData?: Partial< T > ) {
|
||||
|
@ -38,5 +45,6 @@ export function useValidations< T = unknown >() {
|
|||
setIsValidating( false );
|
||||
} );
|
||||
},
|
||||
focusByValidatorId,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ import { ValidationContextProps } from './types';
|
|||
export const ValidationContext = createContext< ValidationContextProps< any > >(
|
||||
{
|
||||
errors: {},
|
||||
getFieldByValidatorId: () => ( {} as Promise< HTMLElement > ),
|
||||
registerValidator: () => () => {},
|
||||
unRegisterValidator: () => () => {},
|
||||
validateField: () => Promise.resolve( undefined ),
|
||||
|
|
|
@ -78,6 +78,12 @@ export function ValidationProvider< T >( {
|
|||
return Promise.resolve( undefined );
|
||||
}
|
||||
|
||||
async function getFieldByValidatorId(
|
||||
validatorId: string
|
||||
): Promise< HTMLElement > {
|
||||
return fieldRefs.current[ validatorId ];
|
||||
}
|
||||
|
||||
async function validateAll(
|
||||
newData: Partial< T >
|
||||
): Promise< ValidationErrors > {
|
||||
|
@ -107,6 +113,7 @@ export function ValidationProvider< T >( {
|
|||
<ValidationContext.Provider
|
||||
value={ {
|
||||
errors,
|
||||
getFieldByValidatorId,
|
||||
registerValidator,
|
||||
unRegisterValidator,
|
||||
validateField,
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
export * from './use-blocks-helper';
|
|
@ -0,0 +1,28 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { select } from '@wordpress/data';
|
||||
|
||||
export function useBlocksHelper() {
|
||||
function getParentTabId( clientId: string ) {
|
||||
const [ closestParentClientId ] =
|
||||
// @ts-expect-error Outdated type definition.
|
||||
select( 'core/block-editor' ).getBlockParentsByBlockName(
|
||||
clientId,
|
||||
'woocommerce/product-tab',
|
||||
true
|
||||
);
|
||||
if ( ! closestParentClientId ) {
|
||||
return '';
|
||||
}
|
||||
// @ts-expect-error Outdated type definition.
|
||||
const { attributes } = select( 'core/block-editor' ).getBlock(
|
||||
closestParentClientId
|
||||
);
|
||||
return attributes?.id;
|
||||
}
|
||||
|
||||
return {
|
||||
getParentTabId,
|
||||
};
|
||||
}
|
|
@ -0,0 +1,170 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { useCallback } from '@wordpress/element';
|
||||
import { getNewPath, navigateTo } from '@woocommerce/navigation';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { useValidations } from '../contexts/validation-context';
|
||||
import { useBlocksHelper } from './use-blocks-helper';
|
||||
|
||||
export type WPErrorCode =
|
||||
| 'variable_product_no_variation_prices'
|
||||
| 'product_form_field_error'
|
||||
| 'product_invalid_sku'
|
||||
| 'product_invalid_global_unique_id'
|
||||
| 'product_create_error'
|
||||
| 'product_publish_error'
|
||||
| 'product_preview_error';
|
||||
|
||||
export type WPError = {
|
||||
code: WPErrorCode;
|
||||
message: string;
|
||||
validatorId?: string;
|
||||
context?: string;
|
||||
};
|
||||
|
||||
type ErrorProps = {
|
||||
explicitDismiss: boolean;
|
||||
actions?: ErrorAction[];
|
||||
};
|
||||
|
||||
type ErrorAction = {
|
||||
label: string;
|
||||
onClick: () => void;
|
||||
};
|
||||
|
||||
type UseErrorHandlerTypes = {
|
||||
getProductErrorMessageAndProps: (
|
||||
error: WPError,
|
||||
visibleTab: string | null
|
||||
) => {
|
||||
message: string;
|
||||
errorProps: ErrorProps;
|
||||
};
|
||||
};
|
||||
|
||||
function getUrl( tab: string ): string {
|
||||
return getNewPath( { tab } );
|
||||
}
|
||||
|
||||
function getErrorPropsWithActions(
|
||||
errorContext = '',
|
||||
validatorId: string,
|
||||
focusByValidatorId: ( validatorId: string ) => void
|
||||
): ErrorProps {
|
||||
return {
|
||||
explicitDismiss: true,
|
||||
actions: [
|
||||
{
|
||||
label: __( 'View error', 'woocommerce' ),
|
||||
onClick: () => {
|
||||
navigateTo( {
|
||||
url: getUrl( errorContext ),
|
||||
} );
|
||||
focusByValidatorId( validatorId );
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
export const useErrorHandler = (): UseErrorHandlerTypes => {
|
||||
const { focusByValidatorId } = useValidations();
|
||||
const { getParentTabId } = useBlocksHelper();
|
||||
|
||||
const getProductErrorMessageAndProps = useCallback(
|
||||
( error: WPError, visibleTab: string | null ) => {
|
||||
const response = {
|
||||
message: '',
|
||||
errorProps: {} as ErrorProps,
|
||||
};
|
||||
const {
|
||||
code,
|
||||
context = '',
|
||||
message: errorMessage,
|
||||
validatorId = '',
|
||||
} = error;
|
||||
const errorContext = getParentTabId( context );
|
||||
switch ( code ) {
|
||||
case 'variable_product_no_variation_prices':
|
||||
response.message = errorMessage;
|
||||
if ( visibleTab !== 'variations' ) {
|
||||
response.errorProps = getErrorPropsWithActions(
|
||||
errorContext,
|
||||
validatorId,
|
||||
focusByValidatorId
|
||||
);
|
||||
}
|
||||
break;
|
||||
case 'product_form_field_error':
|
||||
response.message = errorMessage;
|
||||
if ( visibleTab !== errorContext ) {
|
||||
response.errorProps = getErrorPropsWithActions(
|
||||
errorContext,
|
||||
validatorId,
|
||||
focusByValidatorId
|
||||
);
|
||||
}
|
||||
break;
|
||||
case 'product_invalid_sku':
|
||||
response.message = __(
|
||||
'Invalid or duplicated SKU.',
|
||||
'woocommerce'
|
||||
);
|
||||
if ( visibleTab !== 'inventory' ) {
|
||||
response.errorProps = getErrorPropsWithActions(
|
||||
'inventory',
|
||||
validatorId,
|
||||
focusByValidatorId
|
||||
);
|
||||
}
|
||||
break;
|
||||
case 'product_invalid_global_unique_id':
|
||||
response.message = __(
|
||||
'Invalid or duplicated GTIN, UPC, EAN or ISBN.',
|
||||
'woocommerce'
|
||||
);
|
||||
if ( visibleTab !== 'inventory' ) {
|
||||
response.errorProps = getErrorPropsWithActions(
|
||||
'inventory',
|
||||
validatorId,
|
||||
focusByValidatorId
|
||||
);
|
||||
}
|
||||
break;
|
||||
case 'product_create_error':
|
||||
response.message = __(
|
||||
'Failed to create product.',
|
||||
'woocommerce'
|
||||
);
|
||||
break;
|
||||
case 'product_publish_error':
|
||||
response.message = __(
|
||||
'Failed to publish product.',
|
||||
'woocommerce'
|
||||
);
|
||||
break;
|
||||
case 'product_preview_error':
|
||||
response.message = __(
|
||||
'Failed to preview product.',
|
||||
'woocommerce'
|
||||
);
|
||||
break;
|
||||
default:
|
||||
response.message = __(
|
||||
'Failed to save product.',
|
||||
'woocommerce'
|
||||
);
|
||||
break;
|
||||
}
|
||||
return response;
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
return { getProductErrorMessageAndProps };
|
||||
};
|
|
@ -10,7 +10,7 @@ import { Product, ProductStatus, PRODUCTS_STORE_NAME } from '@woocommerce/data';
|
|||
* Internal dependencies
|
||||
*/
|
||||
import { useValidations } from '../../contexts/validation-context';
|
||||
import type { WPError } from '../../utils/get-product-error-message-and-props';
|
||||
import type { WPError } from '../../hooks/use-error-handler';
|
||||
import { AUTO_DRAFT_NAME } from '../../utils/constants';
|
||||
|
||||
export function errorHandler( error: WPError, productStatus: ProductStatus ) {
|
||||
|
|
|
@ -1,89 +0,0 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
|
||||
export type WPErrorCode =
|
||||
| 'variable_product_no_variation_prices'
|
||||
| 'product_form_field_error'
|
||||
| 'product_invalid_sku'
|
||||
| 'product_invalid_global_unique_id'
|
||||
| 'product_create_error'
|
||||
| 'product_publish_error'
|
||||
| 'product_preview_error';
|
||||
|
||||
export type WPError = {
|
||||
code: WPErrorCode;
|
||||
message: string;
|
||||
data: {
|
||||
[ key: string ]: unknown;
|
||||
};
|
||||
};
|
||||
|
||||
type ErrorProps = {
|
||||
explicitDismiss: boolean;
|
||||
};
|
||||
|
||||
export function getProductErrorMessageAndProps(
|
||||
error: WPError,
|
||||
visibleTab: string | null
|
||||
): {
|
||||
message: string;
|
||||
errorProps: ErrorProps;
|
||||
} {
|
||||
const response = {
|
||||
message: '',
|
||||
errorProps: {} as ErrorProps,
|
||||
};
|
||||
switch ( error.code ) {
|
||||
case 'variable_product_no_variation_prices':
|
||||
response.message = error.message;
|
||||
if ( visibleTab !== 'variations' ) {
|
||||
response.errorProps = { explicitDismiss: true };
|
||||
}
|
||||
break;
|
||||
case 'product_form_field_error':
|
||||
response.message = error.message;
|
||||
if ( visibleTab !== 'general' ) {
|
||||
response.errorProps = { explicitDismiss: true };
|
||||
}
|
||||
break;
|
||||
case 'product_invalid_sku':
|
||||
response.message = __(
|
||||
'Invalid or duplicated SKU.',
|
||||
'woocommerce'
|
||||
);
|
||||
if ( visibleTab !== 'inventory' ) {
|
||||
response.errorProps = { explicitDismiss: true };
|
||||
}
|
||||
break;
|
||||
case 'product_invalid_global_unique_id':
|
||||
response.message = __(
|
||||
'Invalid or duplicated GTIN, UPC, EAN or ISBN.',
|
||||
'woocommerce'
|
||||
);
|
||||
if ( visibleTab !== 'inventory' ) {
|
||||
response.errorProps = { explicitDismiss: true };
|
||||
}
|
||||
break;
|
||||
case 'product_create_error':
|
||||
response.message = __( 'Failed to create product.', 'woocommerce' );
|
||||
break;
|
||||
case 'product_publish_error':
|
||||
response.message = __(
|
||||
'Failed to publish product.',
|
||||
'woocommerce'
|
||||
);
|
||||
break;
|
||||
case 'product_preview_error':
|
||||
response.message = __(
|
||||
'Failed to preview product.',
|
||||
'woocommerce'
|
||||
);
|
||||
break;
|
||||
default:
|
||||
response.message = __( 'Failed to save product.', 'woocommerce' );
|
||||
break;
|
||||
}
|
||||
return response;
|
||||
}
|
|
@ -1,46 +0,0 @@
|
|||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import {
|
||||
getProductErrorMessageAndProps,
|
||||
WPError,
|
||||
} from '../get-product-error-message-and-props';
|
||||
|
||||
describe( 'getProductErrorMessageAndProps.message', () => {
|
||||
it( 'should return the correct error message and props when exists and the field is visible', () => {
|
||||
const error = {
|
||||
code: 'product_invalid_sku',
|
||||
} as WPError;
|
||||
const visibleTab = 'inventory';
|
||||
const { message, errorProps } = getProductErrorMessageAndProps(
|
||||
error,
|
||||
visibleTab
|
||||
);
|
||||
expect( message ).toBe( 'Invalid or duplicated SKU.' );
|
||||
expect( errorProps.explicitDismiss ).toBeFalsy();
|
||||
} );
|
||||
|
||||
it( 'should return the correct error message and props when exists and the field is not visible', () => {
|
||||
const error = {
|
||||
code: 'product_invalid_sku',
|
||||
} as WPError;
|
||||
const visibleTab = 'general';
|
||||
const { message, errorProps } = getProductErrorMessageAndProps(
|
||||
error,
|
||||
visibleTab
|
||||
);
|
||||
expect( message ).toBe( 'Invalid or duplicated SKU.' );
|
||||
expect( errorProps.explicitDismiss ).toBeTruthy();
|
||||
} );
|
||||
|
||||
it( 'should return a default message and props when the error code is not mapped', () => {
|
||||
const error = {} as WPError;
|
||||
const visibleTab = 'general';
|
||||
const { message, errorProps } = getProductErrorMessageAndProps(
|
||||
error,
|
||||
visibleTab
|
||||
);
|
||||
expect( message ).toBe( 'Failed to save product.' );
|
||||
expect( errorProps.explicitDismiss ).toBeFalsy();
|
||||
} );
|
||||
} );
|
Loading…
Reference in New Issue