diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_field.test.tsx b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_field.test.tsx index f00beb470a9f..eead90d2f75b 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_field.test.tsx +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_field.test.tsx @@ -17,9 +17,10 @@ * under the License. */ import React, { useEffect } from 'react'; +import { act } from 'react-dom/test-utils'; -import { registerTestBed } from '../shared_imports'; -import { OnUpdateHandler } from '../types'; +import { registerTestBed, TestBed } from '../shared_imports'; +import { FormHook, OnUpdateHandler, FieldConfig } from '../types'; import { useForm } from '../hooks/use_form'; import { Form } from './form'; import { UseField } from './use_field'; @@ -62,4 +63,91 @@ describe('', () => { lastName: 'Snow', }); }); + + describe('serializer(), deserializer(), formatter()', () => { + interface MyForm { + name: string; + } + + const serializer = jest.fn(); + const deserializer = jest.fn(); + const formatter = jest.fn(); + + const fieldConfig: FieldConfig = { + defaultValue: '', + serializer, + deserializer, + formatters: [formatter], + }; + + let formHook: FormHook | null = null; + + beforeEach(() => { + formHook = null; + serializer.mockReset().mockImplementation((value) => `${value}-serialized`); + deserializer.mockReset().mockImplementation((value) => `${value}-deserialized`); + formatter.mockReset().mockImplementation((value: string) => value.toUpperCase()); + }); + + const onFormHook = (_form: FormHook) => { + formHook = _form; + }; + + const TestComp = ({ onForm }: { onForm: (form: FormHook) => void }) => { + const { form } = useForm({ defaultValue: { name: 'John' } }); + + useEffect(() => { + onForm(form); + }, [form]); + + return ( +
+ + + ); + }; + + test('should call each handler at expected lifecycle', async () => { + const setup = registerTestBed(TestComp, { + memoryRouter: { wrapComponent: false }, + defaultProps: { onForm: onFormHook }, + }); + + const testBed = setup() as TestBed; + + if (!formHook) { + throw new Error( + `formHook is not defined. Use the onForm() prop to update the reference to the form hook.` + ); + } + + const { form } = testBed; + + expect(deserializer).toBeCalled(); + expect(serializer).not.toBeCalled(); + expect(formatter).not.toBeCalled(); + + let formData = formHook.getFormData({ unflatten: false }); + expect(formData.name).toEqual('John-deserialized'); + + await act(async () => { + form.setInputValue('myField', 'Mike'); + }); + + expect(formatter).toBeCalled(); // Formatters are executed on each value change + expect(serializer).not.toBeCalled(); // Serializer are executed *only** when outputting the form data + + formData = formHook.getFormData(); + expect(serializer).toBeCalled(); + expect(formData.name).toEqual('MIKE-serialized'); + + // Make sure that when we reset the form values, we don't serialize the fields + serializer.mockReset(); + + await act(async () => { + formHook!.reset(); + }); + expect(serializer).not.toBeCalled(); + }); + }); }); diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts index 15ea99eb6cc3..caf75b42598f 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts @@ -118,15 +118,13 @@ export const useField = ( setIsChangingValue(true); } - const newValue = serializeOutput(value); - // Notify listener if (valueChangeListener) { - valueChangeListener(newValue as T); + valueChangeListener(value); } // Update the form data observable - __updateFormDataAt(path, newValue); + __updateFormDataAt(path, value); // Validate field(s) and update form.isValid state await __validateFields(fieldsToValidateOnChange ?? [path]); @@ -153,7 +151,6 @@ export const useField = ( } } }, [ - serializeOutput, valueChangeListener, errorDisplayDelay, path, @@ -442,13 +439,7 @@ export const useField = ( if (resetValue) { setValue(initialValue); - /** - * Having to call serializeOutput() is a current bug of the lib and will be fixed - * in a future PR. The serializer function should only be called when outputting - * the form data. If we need to continuously format the data while it changes, - * we need to use the field `formatter` config. - */ - return serializeOutput(initialValue); + return initialValue; } }, [setValue, serializeOutput, initialValue] diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.test.tsx b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.test.tsx index 216c7974a967..b2cc91152b57 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.test.tsx +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.test.tsx @@ -22,7 +22,7 @@ import { act } from 'react-dom/test-utils'; import { registerTestBed, getRandomString, TestBed } from '../shared_imports'; import { Form, UseField } from '../components'; -import { FormSubmitHandler, OnUpdateHandler } from '../types'; +import { FormSubmitHandler, OnUpdateHandler, FormHook, ValidationFunc } from '../types'; import { useForm } from './use_form'; interface MyForm { @@ -123,6 +123,71 @@ describe('use_form() hook', () => { expect(formData).toEqual(expectedData); }); + + test('should not build the object if the form is not valid', async () => { + let formHook: FormHook | null = null; + + const onFormHook = (_form: FormHook) => { + formHook = _form; + }; + + const TestComp = ({ onForm }: { onForm: (form: FormHook) => void }) => { + const { form } = useForm({ defaultValue: { username: 'initialValue' } }); + const validator: ValidationFunc = ({ value }) => { + if (value === 'wrongValue') { + return { message: 'Error on the field' }; + } + }; + + useEffect(() => { + onForm(form); + }, [form]); + + return ( +
+ + + ); + }; + + const setup = registerTestBed(TestComp, { + defaultProps: { onForm: onFormHook }, + memoryRouter: { wrapComponent: false }, + }); + + const { + form: { setInputValue }, + } = setup() as TestBed; + + if (!formHook) { + throw new Error( + `formHook is not defined. Use the onForm() prop to update the reference to the form hook.` + ); + } + + let data; + let isValid; + + await act(async () => { + ({ data, isValid } = await formHook!.submit()); + }); + + expect(isValid).toBe(true); + expect(data).toEqual({ username: 'initialValue' }); + + setInputValue('myField', 'wrongValue'); // Validation will fail + + await act(async () => { + ({ data, isValid } = await formHook!.submit()); + }); + + expect(isValid).toBe(false); + expect(data).toEqual({}); // Don't build the object (and call the serializers()) when invalid + }); }); describe('form.subscribe()', () => { diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts index 46b8958491e5..1f51b75a80b2 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts @@ -140,7 +140,7 @@ export function useForm( return Object.entries(fieldsRefs.current).reduce( (acc, [key, field]) => ({ ...acc, - [key]: field.__serializeOutput(), + [key]: field.value, }), {} as T ); @@ -233,8 +233,7 @@ export function useForm( fieldsRefs.current[field.path] = field; if (!{}.hasOwnProperty.call(getFormData$().value, field.path)) { - const fieldValue = field.__serializeOutput(); - updateFormDataAt(field.path, fieldValue); + updateFormDataAt(field.path, field.value); } }, [getFormData$, updateFormDataAt] @@ -301,7 +300,7 @@ export function useForm( setSubmitting(true); const isFormValid = await validateAllFields(); - const formData = getFormData(); + const formData = isFormValid ? getFormData() : ({} as T); if (onSubmit) { await onSubmit(formData, isFormValid!); diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/create_field/create_field.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/create_field/create_field.tsx index ecaa40b398d0..ce5f2a60f516 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/create_field/create_field.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/create_field/create_field.tsx @@ -111,14 +111,17 @@ export const CreateField = React.memo(function CreateFieldComponent({ {/* Field subType (if any) */} - {({ type }) => ( - - )} + {({ type }) => { + const [fieldType] = type; + return ( + + ); + }} ); @@ -188,7 +191,10 @@ export const CreateField = React.memo(function CreateFieldComponent({ {({ type, subType }) => { - const ParametersForm = getParametersFormForType(type, subType); + const ParametersForm = getParametersFormForType( + type?.[0].value, + subType?.[0].value + ); if (!ParametersForm) { return null; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field.tsx index e6950ccfe253..a9bbf008e512 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field.tsx @@ -98,15 +98,15 @@ export const EditField = React.memo(({ form, field, allFields, exitEdit, updateF {({ type, subType }) => { const linkDocumentation = - documentationService.getTypeDocLink(subType) || - documentationService.getTypeDocLink(type); + documentationService.getTypeDocLink(subType?.[0].value) || + documentationService.getTypeDocLink(type?.[0].value); if (!linkDocumentation) { return null; } - const typeDefinition = TYPE_DEFINITION[type as MainType]; - const subTypeDefinition = TYPE_DEFINITION[subType as SubType]; + const typeDefinition = TYPE_DEFINITION[type[0].value as MainType]; + const subTypeDefinition = TYPE_DEFINITION[subType?.[0].value as SubType]; return ( @@ -148,7 +148,7 @@ export const EditField = React.memo(({ form, field, allFields, exitEdit, updateF {({ type, subType }) => { - const ParametersForm = getParametersFormForType(type, subType); + const ParametersForm = getParametersFormForType(type?.[0].value, subType?.[0].value); if (!ParametersForm) { return null; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field_header_form.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field_header_form.tsx index 5b969fa7ed82..b4b5bce21f76 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field_header_form.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field_header_form.tsx @@ -36,15 +36,18 @@ export const EditFieldHeaderForm = React.memo( {/* Field subType (if any) */} - {({ type }) => ( - - )} + {({ type }) => { + const [fieldType] = type; + return ( + + ); + }} @@ -52,7 +55,7 @@ export const EditFieldHeaderForm = React.memo( {({ type, subType }) => { - const typeDefinition = TYPE_DEFINITION[type as MainType]; + const typeDefinition = TYPE_DEFINITION[type[0].value as MainType]; const hasSubType = typeDefinition.subTypes !== undefined; if (hasSubType) { diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/reducer.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/reducer.ts index 18a8270117ea..2a8368c66685 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/reducer.ts +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/reducer.ts @@ -335,6 +335,11 @@ export const reducer = (state: State, action: Action): State => { return { ...state, fields: updatedFields, + documentFields: { + ...state.documentFields, + // If we removed the last field, show the "Create field" form + status: updatedFields.rootLevelFields.length === 0 ? 'creatingField' : 'idle', + }, // If we have a search in progress, we reexecute the search to update our result array search: Boolean(state.search.term) ? {