[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 { TRACKS_SOURCE } from '../../../constants';
import { TextControl } from '../../text-control'; 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 { Metadata } from '../../../types';
import type { CreateModalProps } from './types'; import type { CreateModalProps } from './types';
@ -25,6 +29,7 @@ const DEFAULT_CUSTOM_FIELD = {
} satisfies Metadata< string >; } satisfies Metadata< string >;
export function CreateModal( { export function CreateModal( {
values,
onCreate, onCreate,
onCancel, onCancel,
...props ...props
@ -86,14 +91,20 @@ export function CreateModal( {
prop: keyof Metadata< string > prop: keyof Metadata< string >
) { ) {
return function handleBlur( event: FocusEvent< HTMLInputElement > ) { return function handleBlur( event: FocusEvent< HTMLInputElement > ) {
const error = validate( { const error = validate(
...customField, {
[ prop ]: event.target.value, ...customField,
} ); [ prop ]: event.target.value,
},
[ ...customFields, ...values ]
);
const id = String( customField.id ); const id = String( customField.id );
setValidationError( ( current ) => ( { setValidationError( ( current ) => ( {
...current, ...current,
[ id ]: error, [ id ]: {
...( current[ id ] as ValidationError ),
[ prop ]: error[ prop ],
},
} ) ); } ) );
}; };
} }
@ -133,7 +144,10 @@ export function CreateModal( {
function handleAddButtonClick() { function handleAddButtonClick() {
const { errors, hasErrors } = customFields.reduce( const { errors, hasErrors } = customFields.reduce(
( prev, customField ) => { ( prev, customField ) => {
const _errors = validate( customField ); const _errors = validate( customField, [
...customFields,
...values,
] );
prev.errors[ String( customField.id ) ] = _errors; prev.errors[ String( customField.id ) ] = _errors;
if ( _errors.key ) { if ( _errors.key ) {
@ -165,7 +179,10 @@ export function CreateModal( {
} }
onCreate( 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', { recordEvent( 'product_custom_fields_add_new_button_click', {

View File

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

View File

@ -32,8 +32,8 @@ export function CustomFields( {
} = useCustomFields(); } = useCustomFields();
const [ showCreateModal, setShowCreateModal ] = useState( false ); const [ showCreateModal, setShowCreateModal ] = useState( false );
const [ selectedCustomField, setSelectedCustomField ] = const [ selectedCustomFieldIndex, setSelectedCustomFieldIndex ] =
useState< Metadata< string > >(); useState< number >();
function handleAddNewButtonClick() { function handleAddNewButtonClick() {
setShowCreateModal( true ); setShowCreateModal( true );
@ -43,11 +43,11 @@ export function CustomFields( {
} ); } );
} }
function customFieldEditButtonClickHandler( function customFieldEditButtonClickHandler( customFieldIndex: number ) {
customField: Metadata< string >
) {
return function handleCustomFieldEditButtonClick() { return function handleCustomFieldEditButtonClick() {
setSelectedCustomField( customField ); setSelectedCustomFieldIndex( customFieldIndex );
const customField = customFields[ customFieldIndex ];
recordEvent( 'product_custom_fields_show_edit_modal', { recordEvent( 'product_custom_fields_show_edit_modal', {
source: TRACKS_SOURCE, source: TRACKS_SOURCE,
@ -85,12 +85,12 @@ export function CustomFields( {
} }
function handleEditModalUpdate( customField: Metadata< string > ) { function handleEditModalUpdate( customField: Metadata< string > ) {
updateCustomField( customField ); updateCustomField( customField, selectedCustomFieldIndex );
setSelectedCustomField( undefined ); setSelectedCustomFieldIndex( undefined );
} }
function handleEditModalCancel() { function handleEditModalCancel() {
setSelectedCustomField( undefined ); setSelectedCustomFieldIndex( undefined );
recordEvent( 'product_custom_fields_cancel_edit_modal', { recordEvent( 'product_custom_fields_cancel_edit_modal', {
source: TRACKS_SOURCE, source: TRACKS_SOURCE,
@ -123,7 +123,7 @@ export function CustomFields( {
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{ customFields.map( ( customField ) => ( { customFields.map( ( customField, index ) => (
<tr <tr
className="woocommerce-product-custom-fields__table-row" className="woocommerce-product-custom-fields__table-row"
key={ customField.id ?? customField.key } key={ customField.id ?? customField.key }
@ -138,7 +138,7 @@ export function CustomFields( {
<Button <Button
variant="tertiary" variant="tertiary"
onClick={ customFieldEditButtonClickHandler( onClick={ customFieldEditButtonClickHandler(
customField index
) } ) }
> >
{ __( 'Edit', 'woocommerce' ) } { __( 'Edit', 'woocommerce' ) }
@ -163,14 +163,16 @@ export function CustomFields( {
{ showCreateModal && ( { showCreateModal && (
<CreateModal <CreateModal
values={ customFields }
onCreate={ handleCreateModalCreate } onCreate={ handleCreateModalCreate }
onCancel={ handleCreateModalCancel } onCancel={ handleCreateModalCancel }
/> />
) } ) }
{ selectedCustomField && ( { selectedCustomFieldIndex !== undefined && (
<EditModal <EditModal
initialValue={ selectedCustomField } initialValue={ customFields[ selectedCustomFieldIndex ] }
values={ customFields }
onUpdate={ handleEditModalUpdate } onUpdate={ handleEditModalUpdate }
onCancel={ handleEditModalCancel } onCancel={ handleEditModalCancel }
/> />

View File

@ -19,6 +19,7 @@ import type { EditModalProps } from './types';
export function EditModal( { export function EditModal( {
initialValue, initialValue,
values,
onUpdate, onUpdate,
onCancel, onCancel,
...props ...props
@ -49,16 +50,19 @@ export function EditModal( {
function blurHandler( prop: keyof Metadata< string > ) { function blurHandler( prop: keyof Metadata< string > ) {
return function handleBlur( event: FocusEvent< HTMLInputElement > ) { return function handleBlur( event: FocusEvent< HTMLInputElement > ) {
const error = validate( { const error = validate(
...customField, {
[ prop ]: event.target.value, ...customField,
} ); [ prop ]: event.target.value,
},
values
);
setValidationError( error ); setValidationError( error );
}; };
} }
function handleUpdateButtonClick() { function handleUpdateButtonClick() {
const errors = validate( customField ); const errors = validate( customField, values );
if ( errors.key || errors.value ) { if ( errors.key || errors.value ) {
setValidationError( errors ); setValidationError( errors );
@ -72,7 +76,11 @@ export function EditModal( {
return; return;
} }
onUpdate( customField ); onUpdate( {
...customField,
key: customField.key.trim(),
value: customField.value?.trim(),
} );
recordEvent( 'product_custom_fields_update_button_click', { recordEvent( 'product_custom_fields_update_button_click', {
source: TRACKS_SOURCE, source: TRACKS_SOURCE,

View File

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

View File

@ -10,7 +10,8 @@ import type { Metadata } from '../../../../types';
import type { ValidationError } from './types'; import type { ValidationError } from './types';
export function validate( export function validate(
customField: Partial< Metadata< string > > customField: Partial< Metadata< string > >,
customFields: Metadata< string >[]
): ValidationError { ): ValidationError {
const errors = {} as ValidationError; const errors = {} as ValidationError;
@ -21,6 +22,13 @@ export function validate(
'The name cannot begin with the underscore (_) character.', 'The name cannot begin with the underscore (_) character.',
'woocommerce' '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 ) { if ( ! customField.value ) {

View File

@ -41,12 +41,15 @@ export function useCustomFields<
setCustomFields( ( current ) => [ ...current, ...value ] ); setCustomFields( ( current ) => [ ...current, ...value ] );
} }
function updateCustomField( customField: T ) { function updateCustomField( customField: T, index?: number ) {
setCustomFields( ( current ) => setCustomFields( ( current ) =>
current.map( ( field ) => { current.map( ( field, fieldIndex ) => {
if ( customField.id && field.id === customField.id ) { if ( customField.id && field.id === customField.id ) {
return customField; return customField;
} }
if ( index === fieldIndex ) {
return customField;
}
return field; return field;
} ) } )
); );