[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 { 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', {
|
||||
|
|
|
@ -12,6 +12,7 @@ export type CreateModalProps = Omit<
|
|||
Modal.Props,
|
||||
'title' | 'onRequestClose' | 'children'
|
||||
> & {
|
||||
values: Metadata< string >[];
|
||||
onCreate( value: Metadata< string >[] ): void;
|
||||
onCancel(): void;
|
||||
};
|
||||
|
|
|
@ -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 }
|
||||
/>
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -13,6 +13,7 @@ export type EditModalProps = Omit<
|
|||
'title' | 'onRequestClose' | 'children'
|
||||
> & {
|
||||
initialValue: Metadata< string >;
|
||||
values: Metadata< string >[];
|
||||
onUpdate( value: Metadata< string > ): void;
|
||||
onCancel(): void;
|
||||
};
|
||||
|
|
|
@ -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 ) {
|
||||
|
|
|
@ -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;
|
||||
} )
|
||||
);
|
||||
|
|
Loading…
Reference in New Issue