[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:
parent
eb4873dcd3
commit
46d87d427a
|
@ -0,0 +1,4 @@
|
||||||
|
Significance: patch
|
||||||
|
Type: fix
|
||||||
|
|
||||||
|
Fix validation for the custom fields
|
|
@ -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', {
|
||||||
|
|
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
|
@ -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 }
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
|
@ -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 ) {
|
||||||
|
|
|
@ -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;
|
||||||
} )
|
} )
|
||||||
);
|
);
|
||||||
|
|
Loading…
Reference in New Issue