From 46d87d427a51e372db61d7fc487ef426daec32e2 Mon Sep 17 00:00:00 2001 From: Maikel Perez Date: Fri, 19 Apr 2024 08:39:52 -0400 Subject: [PATCH] [CFT Custom fields]: Show error message after pressing Add or Add another (#46703) * Show validation message in the field that lost the focus * Add unique validation for custom field names and trim values * Let edit custom fields when creating a new product * Add changelog file --- .../js/product-editor/changelog/fix-46610 | 4 +++ .../create-modal/create-modal.tsx | 33 ++++++++++++++----- .../custom-fields/create-modal/types.ts | 1 + .../custom-fields/custom-fields.tsx | 28 ++++++++-------- .../custom-fields/edit-modal/edit-modal.tsx | 20 +++++++---- .../custom-fields/edit-modal/types.ts | 1 + .../utils/validations/validate.ts | 10 +++++- .../use-custom-fields/use-custom-fields.ts | 7 ++-- 8 files changed, 74 insertions(+), 30 deletions(-) create mode 100644 packages/js/product-editor/changelog/fix-46610 diff --git a/packages/js/product-editor/changelog/fix-46610 b/packages/js/product-editor/changelog/fix-46610 new file mode 100644 index 00000000000..34120a7bfae --- /dev/null +++ b/packages/js/product-editor/changelog/fix-46610 @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Fix validation for the custom fields diff --git a/packages/js/product-editor/src/components/custom-fields/create-modal/create-modal.tsx b/packages/js/product-editor/src/components/custom-fields/create-modal/create-modal.tsx index 8c8f11bf468..e129ac86820 100644 --- a/packages/js/product-editor/src/components/custom-fields/create-modal/create-modal.tsx +++ b/packages/js/product-editor/src/components/custom-fields/create-modal/create-modal.tsx @@ -14,7 +14,11 @@ import type { FocusEvent } from 'react'; */ import { TRACKS_SOURCE } from '../../../constants'; import { TextControl } from '../../text-control'; -import { validate, type ValidationErrors } from '../utils/validations'; +import { + ValidationError, + validate, + type ValidationErrors, +} from '../utils/validations'; import type { Metadata } from '../../../types'; import type { CreateModalProps } from './types'; @@ -25,6 +29,7 @@ const DEFAULT_CUSTOM_FIELD = { } satisfies Metadata< string >; export function CreateModal( { + values, onCreate, onCancel, ...props @@ -86,14 +91,20 @@ export function CreateModal( { prop: keyof Metadata< string > ) { return function handleBlur( event: FocusEvent< HTMLInputElement > ) { - const error = validate( { - ...customField, - [ prop ]: event.target.value, - } ); + const error = validate( + { + ...customField, + [ prop ]: event.target.value, + }, + [ ...customFields, ...values ] + ); const id = String( customField.id ); setValidationError( ( current ) => ( { ...current, - [ id ]: error, + [ id ]: { + ...( current[ id ] as ValidationError ), + [ prop ]: error[ prop ], + }, } ) ); }; } @@ -133,7 +144,10 @@ export function CreateModal( { function handleAddButtonClick() { const { errors, hasErrors } = customFields.reduce( ( prev, customField ) => { - const _errors = validate( customField ); + const _errors = validate( customField, [ + ...customFields, + ...values, + ] ); prev.errors[ String( customField.id ) ] = _errors; if ( _errors.key ) { @@ -165,7 +179,10 @@ export function CreateModal( { } onCreate( - customFields.map( ( { id, ...customField } ) => customField ) + customFields.map( ( { id, ...customField } ) => ( { + key: customField.key.trim(), + value: customField.value?.trim(), + } ) ) ); recordEvent( 'product_custom_fields_add_new_button_click', { diff --git a/packages/js/product-editor/src/components/custom-fields/create-modal/types.ts b/packages/js/product-editor/src/components/custom-fields/create-modal/types.ts index 0a47208def4..106afb023ff 100644 --- a/packages/js/product-editor/src/components/custom-fields/create-modal/types.ts +++ b/packages/js/product-editor/src/components/custom-fields/create-modal/types.ts @@ -12,6 +12,7 @@ export type CreateModalProps = Omit< Modal.Props, 'title' | 'onRequestClose' | 'children' > & { + values: Metadata< string >[]; onCreate( value: Metadata< string >[] ): void; onCancel(): void; }; diff --git a/packages/js/product-editor/src/components/custom-fields/custom-fields.tsx b/packages/js/product-editor/src/components/custom-fields/custom-fields.tsx index d496405a5f7..53adee918d4 100644 --- a/packages/js/product-editor/src/components/custom-fields/custom-fields.tsx +++ b/packages/js/product-editor/src/components/custom-fields/custom-fields.tsx @@ -32,8 +32,8 @@ export function CustomFields( { } = useCustomFields(); const [ showCreateModal, setShowCreateModal ] = useState( false ); - const [ selectedCustomField, setSelectedCustomField ] = - useState< Metadata< string > >(); + const [ selectedCustomFieldIndex, setSelectedCustomFieldIndex ] = + useState< number >(); function handleAddNewButtonClick() { setShowCreateModal( true ); @@ -43,11 +43,11 @@ export function CustomFields( { } ); } - function customFieldEditButtonClickHandler( - customField: Metadata< string > - ) { + function customFieldEditButtonClickHandler( customFieldIndex: number ) { return function handleCustomFieldEditButtonClick() { - setSelectedCustomField( customField ); + setSelectedCustomFieldIndex( customFieldIndex ); + + const customField = customFields[ customFieldIndex ]; recordEvent( 'product_custom_fields_show_edit_modal', { source: TRACKS_SOURCE, @@ -85,12 +85,12 @@ export function CustomFields( { } function handleEditModalUpdate( customField: Metadata< string > ) { - updateCustomField( customField ); - setSelectedCustomField( undefined ); + updateCustomField( customField, selectedCustomFieldIndex ); + setSelectedCustomFieldIndex( undefined ); } function handleEditModalCancel() { - setSelectedCustomField( undefined ); + setSelectedCustomFieldIndex( undefined ); recordEvent( 'product_custom_fields_cancel_edit_modal', { source: TRACKS_SOURCE, @@ -123,7 +123,7 @@ export function CustomFields( { - { customFields.map( ( customField ) => ( + { customFields.map( ( customField, index ) => ( { __( 'Edit', 'woocommerce' ) } @@ -163,14 +163,16 @@ export function CustomFields( { { showCreateModal && ( ) } - { selectedCustomField && ( + { selectedCustomFieldIndex !== undefined && ( diff --git a/packages/js/product-editor/src/components/custom-fields/edit-modal/edit-modal.tsx b/packages/js/product-editor/src/components/custom-fields/edit-modal/edit-modal.tsx index 5b1c8f81e74..0a5ebba509b 100644 --- a/packages/js/product-editor/src/components/custom-fields/edit-modal/edit-modal.tsx +++ b/packages/js/product-editor/src/components/custom-fields/edit-modal/edit-modal.tsx @@ -19,6 +19,7 @@ import type { EditModalProps } from './types'; export function EditModal( { initialValue, + values, onUpdate, onCancel, ...props @@ -49,16 +50,19 @@ export function EditModal( { function blurHandler( prop: keyof Metadata< string > ) { return function handleBlur( event: FocusEvent< HTMLInputElement > ) { - const error = validate( { - ...customField, - [ prop ]: event.target.value, - } ); + const error = validate( + { + ...customField, + [ prop ]: event.target.value, + }, + values + ); setValidationError( error ); }; } function handleUpdateButtonClick() { - const errors = validate( customField ); + const errors = validate( customField, values ); if ( errors.key || errors.value ) { setValidationError( errors ); @@ -72,7 +76,11 @@ export function EditModal( { return; } - onUpdate( customField ); + onUpdate( { + ...customField, + key: customField.key.trim(), + value: customField.value?.trim(), + } ); recordEvent( 'product_custom_fields_update_button_click', { source: TRACKS_SOURCE, diff --git a/packages/js/product-editor/src/components/custom-fields/edit-modal/types.ts b/packages/js/product-editor/src/components/custom-fields/edit-modal/types.ts index ac325182ec4..c7f92cbe6a8 100644 --- a/packages/js/product-editor/src/components/custom-fields/edit-modal/types.ts +++ b/packages/js/product-editor/src/components/custom-fields/edit-modal/types.ts @@ -13,6 +13,7 @@ export type EditModalProps = Omit< 'title' | 'onRequestClose' | 'children' > & { initialValue: Metadata< string >; + values: Metadata< string >[]; onUpdate( value: Metadata< string > ): void; onCancel(): void; }; diff --git a/packages/js/product-editor/src/components/custom-fields/utils/validations/validate.ts b/packages/js/product-editor/src/components/custom-fields/utils/validations/validate.ts index 64c152cc112..70e3c803f6d 100644 --- a/packages/js/product-editor/src/components/custom-fields/utils/validations/validate.ts +++ b/packages/js/product-editor/src/components/custom-fields/utils/validations/validate.ts @@ -10,7 +10,8 @@ import type { Metadata } from '../../../../types'; import type { ValidationError } from './types'; export function validate( - customField: Partial< Metadata< string > > + customField: Partial< Metadata< string > >, + customFields: Metadata< string >[] ): ValidationError { const errors = {} as ValidationError; @@ -21,6 +22,13 @@ export function validate( 'The name cannot begin with the underscore (_) character.', 'woocommerce' ); + } else if ( + customFields.some( + ( field ) => + field.id !== customField.id && field.key === customField.key + ) + ) { + errors.key = __( 'The name must be unique.', 'woocommerce' ); } if ( ! customField.value ) { diff --git a/packages/js/product-editor/src/hooks/use-custom-fields/use-custom-fields.ts b/packages/js/product-editor/src/hooks/use-custom-fields/use-custom-fields.ts index 4f516edb5c6..33e73f5c1da 100644 --- a/packages/js/product-editor/src/hooks/use-custom-fields/use-custom-fields.ts +++ b/packages/js/product-editor/src/hooks/use-custom-fields/use-custom-fields.ts @@ -41,12 +41,15 @@ export function useCustomFields< setCustomFields( ( current ) => [ ...current, ...value ] ); } - function updateCustomField( customField: T ) { + function updateCustomField( customField: T, index?: number ) { setCustomFields( ( current ) => - current.map( ( field ) => { + current.map( ( field, fieldIndex ) => { if ( customField.id && field.id === customField.id ) { return customField; } + if ( index === fieldIndex ) { + return customField; + } return field; } ) );