[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
This commit is contained in:
Maikel Perez 2024-04-19 08:39:52 -04:00 committed by GitHub
parent eb4873dcd3
commit 46d87d427a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 74 additions and 30 deletions

View File

@ -0,0 +1,4 @@
Significance: patch
Type: fix
Fix validation for the custom fields

View File

@ -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( {
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', {

View File

@ -12,6 +12,7 @@ export type CreateModalProps = Omit<
Modal.Props,
'title' | 'onRequestClose' | 'children'
> & {
values: Metadata< string >[];
onCreate( value: Metadata< string >[] ): void;
onCancel(): void;
};

View File

@ -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( {
</tr>
</thead>
<tbody>
{ customFields.map( ( customField ) => (
{ customFields.map( ( customField, index ) => (
<tr
className="woocommerce-product-custom-fields__table-row"
key={ customField.id ?? customField.key }
@ -138,7 +138,7 @@ export function CustomFields( {
<Button
variant="tertiary"
onClick={ customFieldEditButtonClickHandler(
customField
index
) }
>
{ __( 'Edit', 'woocommerce' ) }
@ -163,14 +163,16 @@ export function CustomFields( {
{ showCreateModal && (
<CreateModal
values={ customFields }
onCreate={ handleCreateModalCreate }
onCancel={ handleCreateModalCancel }
/>
) }
{ selectedCustomField && (
{ selectedCustomFieldIndex !== undefined && (
<EditModal
initialValue={ selectedCustomField }
initialValue={ customFields[ selectedCustomFieldIndex ] }
values={ customFields }
onUpdate={ handleEditModalUpdate }
onCancel={ handleEditModalCancel }
/>

View File

@ -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( {
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,

View File

@ -13,6 +13,7 @@ export type EditModalProps = Omit<
'title' | 'onRequestClose' | 'children'
> & {
initialValue: Metadata< string >;
values: Metadata< string >[];
onUpdate( value: Metadata< string > ): void;
onCancel(): void;
};

View File

@ -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 ) {

View File

@ -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;
} )
);