From 196d6cf37b9665b2f9c8fbf4b3c52cb40e25a320 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien=20Loix?= Date: Thu, 27 Aug 2020 15:50:34 +0200 Subject: [PATCH 01/15] Add useFormData() hook + tests --- .../forms/hook_form_lib/components/form.tsx | 17 +- .../forms/hook_form_lib/form_data_context.tsx | 52 +++++ .../hook_form_lib/hooks/use_form.test.tsx | 2 +- .../hooks/use_form_data.test.tsx | 195 ++++++++++++++++++ .../hook_form_lib/hooks/use_form_data.ts | 91 ++++++++ 5 files changed, 351 insertions(+), 6 deletions(-) create mode 100644 src/plugins/es_ui_shared/static/forms/hook_form_lib/form_data_context.tsx create mode 100644 src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form_data.test.tsx create mode 100644 src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form_data.ts diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form.tsx b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form.tsx index b3a15fea8b187..a86ce26018c10 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form.tsx +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form.tsx @@ -21,6 +21,7 @@ import React, { ReactNode } from 'react'; import { EuiForm } from '@elastic/eui'; import { FormProvider } from '../form_context'; +import { FormDataContextProvider } from '../form_data_context'; import { FormHook } from '../types'; interface Props { @@ -30,8 +31,14 @@ interface Props { [key: string]: any; } -export const Form = ({ form, FormWrapper = EuiForm, ...rest }: Props) => ( - - - -); +export const Form = ({ form, FormWrapper = EuiForm, ...rest }: Props) => { + const { getFormData, __getFormData$ } = form; + + return ( + + + + + + ); +}; diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/form_data_context.tsx b/src/plugins/es_ui_shared/static/forms/hook_form_lib/form_data_context.tsx new file mode 100644 index 0000000000000..0a21cc50753ac --- /dev/null +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/form_data_context.tsx @@ -0,0 +1,52 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { createContext, useContext, useMemo } from 'react'; + +import { FormData, FormHook } from './types'; +import { Subject } from './lib'; + +export interface Context { + getFormData$: () => Subject; + getFormData: FormHook['getFormData']; +} + +const FormDataContext = createContext | undefined>(undefined); + +interface Props extends Context { + children: React.ReactNode; +} + +export const FormDataContextProvider = React.memo( + ({ children, getFormData$, getFormData }: Props) => { + const value = useMemo( + () => ({ + getFormData, + getFormData$, + }), + [getFormData, getFormData$] + ); + + return {children}; + } +); + +export function useFormDataContext() { + return useContext | undefined>(FormDataContext); +} 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 007e492243bac..4a880415b6d22 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 @@ -39,7 +39,7 @@ const onFormHook = (_form: FormHook) => { formHook = _form; }; -describe('use_form() hook', () => { +describe('useForm() hook', () => { beforeEach(() => { formHook = null; }); diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form_data.test.tsx b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form_data.test.tsx new file mode 100644 index 0000000000000..f8045cb9236f5 --- /dev/null +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form_data.test.tsx @@ -0,0 +1,195 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { useEffect } from 'react'; +import { act } from 'react-dom/test-utils'; + +import { registerTestBed, TestBed } from '../shared_imports'; +import { Form, UseField } from '../components'; +import { useForm } from './use_form'; +import { useFormData, HookReturn } from './use_form_data'; + +interface Props { + onChange(data: HookReturn): void; + watch?: string | string[]; +} + +describe('useFormData() hook', () => { + const HookListenerComp = React.memo(({ onChange, watch }: Props) => { + const hookValue = useFormData({ watch }); + + useEffect(() => { + onChange(hookValue); + }, [hookValue, onChange]); + + return null; + }); + + describe('form data updates', () => { + let testBed: TestBed; + let onChangeSpy: jest.Mock; + + const getLastMockValue = () => { + return onChangeSpy.mock.calls[onChangeSpy.mock.calls.length - 1][0] as HookReturn; + }; + + const TestComp = (props: Props) => { + const { form } = useForm(); + + return ( +
+ + + + ); + }; + + const setup = registerTestBed(TestComp, { + memoryRouter: { wrapComponent: false }, + }); + + beforeEach(() => { + onChangeSpy = jest.fn(); + testBed = setup({ onChange: onChangeSpy }) as TestBed; + }); + + test('should return the form data', () => { + // Called twice: + // once when the hook is called and once when the fields have mounted and updated the form data + expect(onChangeSpy).toBeCalledTimes(2); + const [data] = getLastMockValue(); + expect(data).toEqual({ title: 'titleInitialValue' }); + }); + + test('should listen to field changes', async () => { + const { + form: { setInputValue }, + } = testBed; + + await act(async () => { + setInputValue('titleField', 'titleChanged'); + }); + + expect(onChangeSpy).toBeCalledTimes(3); + const [data] = getLastMockValue(); + expect(data).toEqual({ title: 'titleChanged' }); + }); + }); + + describe('options', () => { + describe('watch', () => { + let testBed: TestBed; + let onChangeSpy: jest.Mock; + + const getLastMockValue = () => { + return onChangeSpy.mock.calls[onChangeSpy.mock.calls.length - 1][0] as HookReturn; + }; + + const TestComp = (props: Props) => { + const { form } = useForm(); + + return ( +
+ + + + + ); + }; + + const setup = registerTestBed(TestComp, { + memoryRouter: { wrapComponent: false }, + }); + + beforeEach(() => { + onChangeSpy = jest.fn(); + testBed = setup({ watch: 'title', onChange: onChangeSpy }) as TestBed; + }); + + test('should not listen to changes on fields we are not interested in', async () => { + const { + form: { setInputValue }, + } = testBed; + + await act(async () => { + // Changing a field we are **not** interested in + setInputValue('subTitleField', 'subTitleChanged'); + // Changing a field we **are** interested in + setInputValue('titleField', 'titleChanged'); + }); + + const [data] = getLastMockValue(); + expect(data).toEqual({ title: 'titleChanged', subTitle: 'subTitleInitialValue' }); + }); + }); + + describe('form', () => { + let testBed: TestBed; + let onChangeSpy: jest.Mock; + + const getLastMockValue = () => { + return onChangeSpy.mock.calls[onChangeSpy.mock.calls.length - 1][0] as HookReturn; + }; + + const TestComp = ({ onChange }: Props) => { + const { form } = useForm(); + const hookValue = useFormData({ form }); + + useEffect(() => { + onChange(hookValue); + }, [hookValue, onChange]); + + return ( +
+ + + ); + }; + + const setup = registerTestBed(TestComp, { + memoryRouter: { wrapComponent: false }, + }); + + beforeEach(() => { + onChangeSpy = jest.fn(); + testBed = setup({ onChange: onChangeSpy }) as TestBed; + }); + + test('should allow a form to be provided when the hook is called outside of the FormDataContext', async () => { + const { + form: { setInputValue }, + } = testBed; + + const [initialData] = getLastMockValue(); + expect(initialData).toEqual({ title: 'titleInitialValue' }); + + await act(async () => { + setInputValue('titleField', 'titleChanged'); + }); + + const [updatedData] = getLastMockValue(); + expect(updatedData).toEqual({ title: 'titleChanged' }); + }); + }); + }); +}); diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form_data.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form_data.ts new file mode 100644 index 0000000000000..47f06895cc189 --- /dev/null +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form_data.ts @@ -0,0 +1,91 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { useState, useEffect, useRef } from 'react'; + +import { FormData, FormHook } from '../types'; +import { useFormDataContext, Context } from '../form_data_context'; + +interface Options { + watch?: string | string[]; + form?: FormHook; +} + +export type HookReturn = [ + FormData, + FormHook['getFormData'], + boolean +]; + +export const useFormData = (options: Options = {}): HookReturn => { + const { watch, form } = options; + const ctx = useFormDataContext(); + + let getFormData: Context['getFormData']; + let getFormData$: Context['getFormData$']; + + if (form !== undefined) { + getFormData = form.getFormData; + getFormData$ = form.__getFormData$; + } else if (ctx !== undefined) { + ({ getFormData, getFormData$ } = ctx); + } else { + throw new Error( + 'useFormData() must be used within a or you need to pass FormHook object in the options.' + ); + } + + const initialValue = getFormData$().value; + + const previousRawData = useRef(initialValue); + const isMounted = useRef(false); + const [formData, setFormData] = useState(previousRawData.current); + + useEffect(() => { + const subscription = getFormData$().subscribe((raw) => { + if (watch) { + const valuesToWatchArray = Array.isArray(watch) + ? (watch as string[]) + : ([watch] as string[]); + + if (valuesToWatchArray.some((value) => previousRawData.current[value] !== raw[value])) { + previousRawData.current = raw; + // Only update the state if one of the field we watch has changed. + setFormData(raw); + } + } else { + setFormData(raw); + } + }); + return subscription.unsubscribe; + }, [getFormData$, watch]); + + useEffect(() => { + isMounted.current = true; + return () => { + isMounted.current = false; + }; + }, []); + + if (!isMounted.current && Object.keys(formData).length === 0) { + // No field has mounted yet + return [formData, getFormData, false]; + } + + return [formData, getFormData, true]; +}; From 320938e6ce28ccc0e288e77be92767d0eb72966b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien=20Loix?= Date: Thu, 27 Aug 2020 16:07:13 +0200 Subject: [PATCH 02/15] Update FormDataProvider to use the hook --- .../components/form_data_provider.test.tsx | 19 +++----- .../components/form_data_provider.ts | 45 ++----------------- .../static/forms/hook_form_lib/hooks/index.ts | 1 + 3 files changed, 10 insertions(+), 55 deletions(-) diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form_data_provider.test.tsx b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form_data_provider.test.tsx index 25448dff18e8a..d9095944eaa33 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form_data_provider.test.tsx +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form_data_provider.test.tsx @@ -75,16 +75,7 @@ describe('', () => { setInputValue('lastNameField', 'updated value'); }); - /** - * The children will be rendered three times: - * - Twice for each input value that has changed - * - once because after updating both fields, the **form** isValid state changes (from "undefined" to "true") - * causing a new "form" object to be returned and thus a re-render. - * - * When the form object will be memoized (in a future PR), te bellow call count should only be 2 as listening - * to form data changes should not receive updates when the "isValid" state of the form changes. - */ - expect(onFormData.mock.calls.length).toBe(3); + expect(onFormData).toBeCalledTimes(2); const [formDataUpdated] = onFormData.mock.calls[onFormData.mock.calls.length - 1] as Parameters< OnUpdateHandler @@ -130,7 +121,7 @@ describe('', () => { find, } = setup() as TestBed; - expect(onFormData.mock.calls.length).toBe(0); // Not present in the DOM yet + expect(onFormData).toBeCalledTimes(0); // Not present in the DOM yet // Make some changes to the form fields await act(async () => { @@ -188,7 +179,7 @@ describe('', () => { setInputValue('lastNameField', 'updated value'); }); - expect(onFormData.mock.calls.length).toBe(0); + expect(onFormData).toBeCalledTimes(0); }); test('props.pathsToWatch (Array): should not re-render the children when the field that changed is not in the watch list', async () => { @@ -228,14 +219,14 @@ describe('', () => { }); // No re-render - expect(onFormData.mock.calls.length).toBe(0); + expect(onFormData).toBeCalledTimes(0); // Make some changes to fields in the watch list await act(async () => { setInputValue('nameField', 'updated value'); }); - expect(onFormData.mock.calls.length).toBe(1); + expect(onFormData).toBeCalledTimes(1); onFormData.mockReset(); diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form_data_provider.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form_data_provider.ts index 3630b902f0564..ac141baf8fc71 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form_data_provider.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form_data_provider.ts @@ -17,10 +17,10 @@ * under the License. */ -import React, { useState, useEffect, useRef, useCallback } from 'react'; +import React from 'react'; import { FormData } from '../types'; -import { useFormContext } from '../form_context'; +import { useFormData } from '../hooks'; interface Props { children: (formData: FormData) => JSX.Element | null; @@ -28,46 +28,9 @@ interface Props { } export const FormDataProvider = React.memo(({ children, pathsToWatch }: Props) => { - const form = useFormContext(); - const { subscribe } = form; - const previousRawData = useRef(form.__getFormData$().value); - const isMounted = useRef(false); - const [formData, setFormData] = useState(previousRawData.current); + const { 0: formData, 2: isReady } = useFormData({ watch: pathsToWatch }); - const onFormData = useCallback( - ({ data: { raw } }) => { - // To avoid re-rendering the children for updates on the form data - // that we are **not** interested in, we can specify one or multiple path(s) - // to watch. - if (pathsToWatch) { - const valuesToWatchArray = Array.isArray(pathsToWatch) - ? (pathsToWatch as string[]) - : ([pathsToWatch] as string[]); - - if (valuesToWatchArray.some((value) => previousRawData.current[value] !== raw[value])) { - previousRawData.current = raw; - setFormData(raw); - } - } else { - setFormData(raw); - } - }, - [pathsToWatch] - ); - - useEffect(() => { - const subscription = subscribe(onFormData); - return subscription.unsubscribe; - }, [subscribe, onFormData]); - - useEffect(() => { - isMounted.current = true; - return () => { - isMounted.current = false; - }; - }, []); - - if (!isMounted.current && Object.keys(formData).length === 0) { + if (!isReady) { // No field has mounted yet, don't render anything return null; } diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/index.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/index.ts index 6a04a592227f9..45c11dd6272e4 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/index.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/index.ts @@ -19,3 +19,4 @@ export { useField } from './use_field'; export { useForm } from './use_form'; +export { useFormData } from './use_form_data'; From 917a7a1739b375ab6d20796dbd9c13fceb2ea3e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien=20Loix?= Date: Thu, 27 Aug 2020 17:34:56 +0200 Subject: [PATCH 03/15] Expose hook from lib and update stepLogistics to use it --- .../forms/hook_form_lib/form_data_context.tsx | 24 ++++---- .../static/forms/hook_form_lib/index.ts | 2 +- .../template_form/steps/step_logistics.tsx | 56 +++++++++---------- .../template_form/template_form_schemas.tsx | 2 +- .../index_management/public/shared_imports.ts | 1 + 5 files changed, 42 insertions(+), 43 deletions(-) diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/form_data_context.tsx b/src/plugins/es_ui_shared/static/forms/hook_form_lib/form_data_context.tsx index 0a21cc50753ac..0e6a75e9c5065 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/form_data_context.tsx +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/form_data_context.tsx @@ -33,19 +33,17 @@ interface Props extends Context { children: React.ReactNode; } -export const FormDataContextProvider = React.memo( - ({ children, getFormData$, getFormData }: Props) => { - const value = useMemo( - () => ({ - getFormData, - getFormData$, - }), - [getFormData, getFormData$] - ); - - return {children}; - } -); +export const FormDataContextProvider = ({ children, getFormData$, getFormData }: Props) => { + const value = useMemo( + () => ({ + getFormData, + getFormData$, + }), + [getFormData, getFormData$] + ); + + return {children}; +}; export function useFormDataContext() { return useContext | undefined>(FormDataContext); diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/index.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/index.ts index 3079814c9ad14..8d6b57fbeb315 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/index.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/index.ts @@ -19,7 +19,7 @@ // Only export the useForm hook. The "useField" hook is for internal use // as the consumer of the library must use the component -export { useForm } from './hooks'; +export { useForm, useFormData } from './hooks'; export { getFieldValidityAndErrorMessage } from './helpers'; export * from './form_context'; diff --git a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_logistics.tsx b/x-pack/plugins/index_management/public/application/components/template_form/steps/step_logistics.tsx index fcc9795617ebb..1b527e10988b5 100644 --- a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_logistics.tsx +++ b/x-pack/plugins/index_management/public/application/components/template_form/steps/step_logistics.tsx @@ -17,6 +17,7 @@ import { i18n } from '@kbn/i18n'; import { useForm, + useFormData, Form, getUseField, getFormRow, @@ -118,7 +119,7 @@ interface LogisticsForm { } interface LogisticsFormInternal extends LogisticsForm { - __internal__: { + __int__: { addMeta: boolean; }; } @@ -133,14 +134,14 @@ interface Props { function formDeserializer(formData: LogisticsForm): LogisticsFormInternal { return { ...formData, - __internal__: { + __int__: { addMeta: Boolean(formData._meta && Object.keys(formData._meta).length), }, }; } function formSerializer(formData: LogisticsFormInternal): LogisticsForm { - const { __internal__, ...rest } = formData; + const { __int__, ...rest } = formData; return rest; } @@ -155,6 +156,11 @@ export const StepLogistics: React.FunctionComponent = React.memo( }); const { subscribe, submit, isSubmitted, isValid: isFormValid, getErrors: getFormErrors } = form; + const [{ '__int__.addMeta': addMeta }] = useFormData({ + form, + watch: '__int__.addMeta', + }); + /** * When the consumer call validate() on this step, we submit the form so it enters the "isSubmitted" state * and we can display the form errors on top of the forms if there are any. @@ -296,34 +302,28 @@ export const StepLogistics: React.FunctionComponent = React.memo( defaultMessage="Use the _meta field to store any metadata you want." /> - + } > - - {({ '__internal__.addMeta': addMeta }) => { - return ( - addMeta && ( - - ) - ); - }} - + {addMeta && ( + + )} )} diff --git a/x-pack/plugins/index_management/public/application/components/template_form/template_form_schemas.tsx b/x-pack/plugins/index_management/public/application/components/template_form/template_form_schemas.tsx index 0d9ce57a64c84..2200c120e5c97 100644 --- a/x-pack/plugins/index_management/public/application/components/template_form/template_form_schemas.tsx +++ b/x-pack/plugins/index_management/public/application/components/template_form/template_form_schemas.tsx @@ -213,7 +213,7 @@ export const schemas: Record = { } }, }, - __internal__: { + __int__: { addMeta: { type: FIELD_TYPES.TOGGLE, label: i18n.translate('xpack.idxMgmt.templateForm.stepLogistics.addMetadataLabel', { diff --git a/x-pack/plugins/index_management/public/shared_imports.ts b/x-pack/plugins/index_management/public/shared_imports.ts index 16dcab18c3caf..ff1cf34e1507e 100644 --- a/x-pack/plugins/index_management/public/shared_imports.ts +++ b/x-pack/plugins/index_management/public/shared_imports.ts @@ -21,6 +21,7 @@ export { VALIDATION_TYPES, FieldConfig, useForm, + useFormData, Form, getUseField, UseField, From f40926ace6214ef45e7069a69710313e69d25db8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien=20Loix?= Date: Thu, 27 Aug 2020 17:41:09 +0200 Subject: [PATCH 04/15] Add test for exposed "format()" handler --- .../hooks/use_form_data.test.tsx | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form_data.test.tsx b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form_data.test.tsx index f8045cb9236f5..a75cb1bbac297 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form_data.test.tsx +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form_data.test.tsx @@ -92,6 +92,46 @@ describe('useFormData() hook', () => { }); }); + describe('format form data', () => { + let testBed: TestBed; + let onChangeSpy: jest.Mock; + + const getLastMockValue = () => { + return onChangeSpy.mock.calls[onChangeSpy.mock.calls.length - 1][0] as HookReturn; + }; + + const TestComp = (props: Props) => { + const { form } = useForm(); + + return ( +
+ + + + + ); + }; + + const setup = registerTestBed(TestComp, { + memoryRouter: { wrapComponent: false }, + }); + + beforeEach(() => { + onChangeSpy = jest.fn(); + testBed = setup({ onChange: onChangeSpy }) as TestBed; + }); + + test('should expose a handler to build the form data', () => { + const [_, format] = getLastMockValue(); + expect(format()).toEqual({ + user: { + firstName: 'John', + lastName: 'Snow', + }, + }); + }); + }); + describe('options', () => { describe('watch', () => { let testBed: TestBed; From 06a4f053fbf67c1e29a60420d8d8090992c9f635 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien=20Loix?= Date: Fri, 28 Aug 2020 09:21:18 +0200 Subject: [PATCH 05/15] Form: Invert order of context provider --- .../static/forms/hook_form_lib/components/form.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form.tsx b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form.tsx index a86ce26018c10..287ac56243446 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form.tsx +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form.tsx @@ -35,10 +35,10 @@ export const Form = ({ form, FormWrapper = EuiForm, ...rest }: Props) => { const { getFormData, __getFormData$ } = form; return ( - - + + - - + +
); }; From 31a7c509075af59612270822b18ed131b14b11be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien=20Loix?= Date: Fri, 28 Aug 2020 09:51:46 +0200 Subject: [PATCH 06/15] Fix TS issues --- .../static/forms/hook_form_lib/hooks/use_form_data.test.tsx | 5 ++--- .../components/template_form/steps/step_logistics.tsx | 1 - 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form_data.test.tsx b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form_data.test.tsx index a75cb1bbac297..0fb65daecf2f4 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form_data.test.tsx +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form_data.test.tsx @@ -93,7 +93,6 @@ describe('useFormData() hook', () => { }); describe('format form data', () => { - let testBed: TestBed; let onChangeSpy: jest.Mock; const getLastMockValue = () => { @@ -118,11 +117,11 @@ describe('useFormData() hook', () => { beforeEach(() => { onChangeSpy = jest.fn(); - testBed = setup({ onChange: onChangeSpy }) as TestBed; + setup({ onChange: onChangeSpy }); }); test('should expose a handler to build the form data', () => { - const [_, format] = getLastMockValue(); + const { 1: format } = getLastMockValue(); expect(format()).toEqual({ user: { firstName: 'John', diff --git a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_logistics.tsx b/x-pack/plugins/index_management/public/application/components/template_form/steps/step_logistics.tsx index 1b527e10988b5..9af9a3fdf0836 100644 --- a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_logistics.tsx +++ b/x-pack/plugins/index_management/public/application/components/template_form/steps/step_logistics.tsx @@ -24,7 +24,6 @@ import { Field, Forms, JsonEditorField, - FormDataProvider, } from '../../../../shared_imports'; import { documentationService } from '../../../services/documentation'; import { schemas, nameConfig, nameConfigWithoutValidations } from '../template_form_schemas'; From 62289f6f491e3191cd7f7d05c9bd8beeecaada50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien=20Loix?= Date: Fri, 28 Aug 2020 09:58:06 +0200 Subject: [PATCH 07/15] Mutate the formatFormData() handler whenever the state changes --- .../hook_form_lib/hooks/use_form_data.ts | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form_data.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form_data.ts index 47f06895cc189..811474d7ac6f6 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form_data.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form_data.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { useState, useEffect, useRef } from 'react'; +import { useState, useEffect, useRef, useCallback } from 'react'; import { FormData, FormHook } from '../types'; import { useFormDataContext, Context } from '../form_data_context'; @@ -26,11 +26,7 @@ interface Options { form?: FormHook; } -export type HookReturn = [ - FormData, - FormHook['getFormData'], - boolean -]; +export type HookReturn = [FormData, () => T, boolean]; export const useFormData = (options: Options = {}): HookReturn => { const { watch, form } = options; @@ -56,6 +52,14 @@ export const useFormData = (options: Options = {}): const isMounted = useRef(false); const [formData, setFormData] = useState(previousRawData.current); + // Create a new instance of the "formatFormData()" handler whenever the "formData" state changes + // so the consumer can interchangeably declare the "formData" state or the "format()" handler as + // dependency in his useEffect(). + // This is the reason why we disable the react-hooks/exhaustive-deps rule + const formatFormData = useCallback(() => { + return getFormData({ unflatten: true }); + }, [formData, getFormData]); // eslint-disable-line react-hooks/exhaustive-deps + useEffect(() => { const subscription = getFormData$().subscribe((raw) => { if (watch) { @@ -84,8 +88,8 @@ export const useFormData = (options: Options = {}): if (!isMounted.current && Object.keys(formData).length === 0) { // No field has mounted yet - return [formData, getFormData, false]; + return [formData, formatFormData, false]; } - return [formData, getFormData, true]; + return [formData, formatFormData, true]; }; From 8e82be6202eb91b90f099ea197521f5e31fa62bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien=20Loix?= Date: Fri, 28 Aug 2020 10:08:48 +0200 Subject: [PATCH 08/15] Revert update handler on each state change. Bad idea :) --- .../static/forms/hook_form_lib/hooks/use_form_data.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form_data.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form_data.ts index 811474d7ac6f6..fb4a0984438ad 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form_data.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form_data.ts @@ -52,13 +52,9 @@ export const useFormData = (options: Options = {}): const isMounted = useRef(false); const [formData, setFormData] = useState(previousRawData.current); - // Create a new instance of the "formatFormData()" handler whenever the "formData" state changes - // so the consumer can interchangeably declare the "formData" state or the "format()" handler as - // dependency in his useEffect(). - // This is the reason why we disable the react-hooks/exhaustive-deps rule const formatFormData = useCallback(() => { return getFormData({ unflatten: true }); - }, [formData, getFormData]); // eslint-disable-line react-hooks/exhaustive-deps + }, [getFormData]); useEffect(() => { const subscription = getFormData$().subscribe((raw) => { From 278dc7c7cb860adca1542f51ee57e5c14d001aa5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien=20Loix?= Date: Mon, 17 Aug 2020 17:46:29 +0200 Subject: [PATCH 09/15] [Form lib] Expose a "validate()" function in the public API of the form For some reason this handler was not exposed directly on the form. The way to call the validate() method was by first subscribing to form changes and save a ref of the handler. A very indirect process when we do have the handler ready to be public. --- .../es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts | 2 ++ src/plugins/es_ui_shared/static/forms/hook_form_lib/types.ts | 1 + 2 files changed, 3 insertions(+) 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 35bac5b9a58c6..54c59bd171686 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 @@ -389,6 +389,7 @@ export function useForm( isValid, id, submit: submitForm, + validate: validateAllFields, subscribe, setFieldValue, setFieldErrors, @@ -428,6 +429,7 @@ export function useForm( addField, removeField, validateFields, + validateAllFields, ]); useEffect(() => { diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/types.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/types.ts index dc495f6eb56b4..14eeebf695809 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/types.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/types.ts @@ -30,6 +30,7 @@ export interface FormHook { readonly isValid: boolean | undefined; readonly id: string; submit: (e?: FormEvent | MouseEvent) => Promise<{ data: T; isValid: boolean }>; + validate: () => Promise; subscribe: (handler: OnUpdateHandler) => Subscription; setFieldValue: (fieldName: string, value: FieldValue) => void; setFieldErrors: (fieldName: string, errors: ValidationError[]) => void; From 12d02812357529b3e46b1e35a4b6d8012784b937 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien=20Loix?= Date: Mon, 31 Aug 2020 09:59:49 +0200 Subject: [PATCH 10/15] Simplify StepLogistics to make use of new API --- .../template_form/steps/step_logistics.tsx | 38 +++++++++---------- .../template_form/template_form_schemas.tsx | 12 +++--- 2 files changed, 24 insertions(+), 26 deletions(-) diff --git a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_logistics.tsx b/x-pack/plugins/index_management/public/application/components/template_form/steps/step_logistics.tsx index 9af9a3fdf0836..afa0a0817d946 100644 --- a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_logistics.tsx +++ b/x-pack/plugins/index_management/public/application/components/template_form/steps/step_logistics.tsx @@ -118,9 +118,7 @@ interface LogisticsForm { } interface LogisticsFormInternal extends LogisticsForm { - __int__: { - addMeta: boolean; - }; + addMeta: boolean; } interface Props { @@ -133,14 +131,12 @@ interface Props { function formDeserializer(formData: LogisticsForm): LogisticsFormInternal { return { ...formData, - __int__: { - addMeta: Boolean(formData._meta && Object.keys(formData._meta).length), - }, + addMeta: Boolean(formData._meta && Object.keys(formData._meta).length), }; } function formSerializer(formData: LogisticsFormInternal): LogisticsForm { - const { __int__, ...rest } = formData; + const { addMeta, ...rest } = formData; return rest; } @@ -153,11 +149,18 @@ export const StepLogistics: React.FunctionComponent = React.memo( serializer: formSerializer, deserializer: formDeserializer, }); - const { subscribe, submit, isSubmitted, isValid: isFormValid, getErrors: getFormErrors } = form; + const { + subscribe, + submit, + isSubmitted, + isValid: isFormValid, + getErrors: getFormErrors, + getFormData, + } = form; - const [{ '__int__.addMeta': addMeta }] = useFormData({ + const [{ addMeta }] = useFormData({ form, - watch: '__int__.addMeta', + watch: 'addMeta', }); /** @@ -169,15 +172,12 @@ export const StepLogistics: React.FunctionComponent = React.memo( }, [submit]); useEffect(() => { - const subscription = subscribe(({ data, isValid }) => { - onChange({ - isValid, - validate, - getData: data.format, - }); + onChange({ + isValid: isFormValid, + getData: getFormData, + validate, }); - return subscription.unsubscribe; - }, [onChange, validate, subscribe]); + }, [onChange, isFormValid, validate, getFormData]); const { name, indexPatterns, dataStream, order, priority, version } = getFieldsMeta( documentationService.getEsDocsBase() @@ -301,7 +301,7 @@ export const StepLogistics: React.FunctionComponent = React.memo( defaultMessage="Use the _meta field to store any metadata you want." /> - + } > diff --git a/x-pack/plugins/index_management/public/application/components/template_form/template_form_schemas.tsx b/x-pack/plugins/index_management/public/application/components/template_form/template_form_schemas.tsx index 2200c120e5c97..cf26ca5cd2500 100644 --- a/x-pack/plugins/index_management/public/application/components/template_form/template_form_schemas.tsx +++ b/x-pack/plugins/index_management/public/application/components/template_form/template_form_schemas.tsx @@ -213,13 +213,11 @@ export const schemas: Record = { } }, }, - __int__: { - addMeta: { - type: FIELD_TYPES.TOGGLE, - label: i18n.translate('xpack.idxMgmt.templateForm.stepLogistics.addMetadataLabel', { - defaultMessage: 'Add metadata', - }), - }, + addMeta: { + type: FIELD_TYPES.TOGGLE, + label: i18n.translate('xpack.idxMgmt.templateForm.stepLogistics.addMetadataLabel', { + defaultMessage: 'Add metadata', + }), }, }, }; From 6bd2b24f8b3df9b586c42bad9a4d3b4d15d8d93b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien=20Loix?= Date: Mon, 31 Aug 2020 10:22:32 +0200 Subject: [PATCH 11/15] useForm(): Reset "isSubmitted" state when adding new fields --- .../static/forms/hook_form_lib/hooks/use_form.ts | 6 ++++++ .../document_fields/fields/edit_field/edit_field.tsx | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) 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 54c59bd171686..7b72a9eeacf7b 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 @@ -240,6 +240,12 @@ export function useForm( if (!field.isValidated) { setIsValid(undefined); + + // When we submit the form (and set "isSubmitted" to "true"), we validate **all fields**. + // If a field is added and it is not validated it means that we have swapped fields and added new ones: + // --> we have basically have a new form in front of us. + // For that reason we make sure that the "isSubmitted" state is false. + setIsSubmitted(false); } }, [updateFormDataAt] 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 6b5a848ce85d3..95575124b6abd 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 @@ -163,7 +163,7 @@ export const EditField = React.memo(({ form, field, allFields, exitEdit, updateF - {form.isSubmitted && form.isValid === false && ( + {form.isSubmitted && !form.isValid && ( <> {i18n.translate('xpack.idxMgmt.mappingsEditor.editFieldUpdateButtonLabel', { From 9bb4caa16f30a0fad8fd092d723d2e717073a8c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien=20Loix?= Date: Mon, 31 Aug 2020 11:32:36 +0200 Subject: [PATCH 12/15] Fix TS issue --- .../components/template_form/steps/step_logistics.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_logistics.tsx b/x-pack/plugins/index_management/public/application/components/template_form/steps/step_logistics.tsx index afa0a0817d946..56f040fc59a7b 100644 --- a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_logistics.tsx +++ b/x-pack/plugins/index_management/public/application/components/template_form/steps/step_logistics.tsx @@ -150,7 +150,6 @@ export const StepLogistics: React.FunctionComponent = React.memo( deserializer: formDeserializer, }); const { - subscribe, submit, isSubmitted, isValid: isFormValid, From fbd6e7f296882ecb37aa52156906a0a5ffb621ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien=20Loix?= Date: Tue, 1 Sep 2020 09:59:43 +0200 Subject: [PATCH 13/15] Fix bug of _meta not being cleared --- .../application/components/template_form/template_form.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx b/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx index 537f421173358..3a03835e85970 100644 --- a/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx +++ b/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx @@ -192,8 +192,8 @@ export const TemplateForm = ({ wizardData: WizardContent ): TemplateDeserialized => { const outputTemplate = { - ...initialTemplate, ...wizardData.logistics, + _kbnMeta: initialTemplate._kbnMeta, composedOf: wizardData.components, template: { settings: wizardData.settings, From 567f6dfca43310ee2241b6d38c021452d8e8ab29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien=20Loix?= Date: Tue, 1 Sep 2020 10:36:35 +0200 Subject: [PATCH 14/15] Fix ComboBox clearing error issue by adding a new "isBlocking" param to a validation --- .../forms/components/fields/combobox_field.tsx | 2 +- .../forms/hook_form_lib/hooks/use_field.ts | 18 ++++++++++++++++-- .../static/forms/hook_form_lib/types.ts | 7 +++++++ .../components/template_form/template_form.tsx | 1 - .../template_form/template_form_schemas.tsx | 1 + 5 files changed, 25 insertions(+), 4 deletions(-) diff --git a/src/plugins/es_ui_shared/static/forms/components/fields/combobox_field.tsx b/src/plugins/es_ui_shared/static/forms/components/fields/combobox_field.tsx index 9fb804eb7fafa..b2f1a70341315 100644 --- a/src/plugins/es_ui_shared/static/forms/components/fields/combobox_field.tsx +++ b/src/plugins/es_ui_shared/static/forms/components/fields/combobox_field.tsx @@ -74,7 +74,7 @@ export const ComboBoxField = ({ field, euiFieldProps = {}, ...rest }: Props) => }; const onSearchComboChange = (value: string) => { - if (value) { + if (value !== undefined) { field.clearErrors(VALIDATION_TYPES.ARRAY_ITEM); } }; 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 9d22e4eb2ee5e..fa29f900af2ef 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 @@ -254,6 +254,8 @@ export const useField = ( validationErrors.push({ ...validationResult, + // See comment below that explains why we add "__isBlocking__". + __isBlocking__: validationResult.__isBlocking__ ?? validation.isBlocking, validationType: validationType || VALIDATION_TYPES.FIELD, }); @@ -306,6 +308,11 @@ export const useField = ( validationErrors.push({ ...(validationResult as ValidationError), + // We add an "__isBlocking__" property to know if this error is a blocker or no. + // Most validation errors are blockers but in some cases a validation is more a warning than an error + // like with the ComboBox items when they are added. + __isBlocking__: + (validationResult as ValidationError).__isBlocking__ ?? validation.isBlocking, validationType: validationType || VALIDATION_TYPES.FIELD, }); @@ -394,7 +401,13 @@ export const useField = ( ); const _setErrors: FieldHook['setErrors'] = useCallback((_errors) => { - setErrors(_errors.map((error) => ({ validationType: VALIDATION_TYPES.FIELD, ...error }))); + setErrors( + _errors.map((error) => ({ + validationType: VALIDATION_TYPES.FIELD, + __isBlocking__: true, + ...error, + })) + ); }, []); /** @@ -463,7 +476,8 @@ export const useField = ( [setValue, deserializeValue, defaultValue] ); - const isValid = errors.length === 0; + // Don't take into account non blocker validation. Some are just warning (like trying to add a wrong ComboBox item) + const isValid = errors.filter((e) => e.__isBlocking__ !== false).length === 0; const field = useMemo>(() => { return { diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/types.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/types.ts index 14eeebf695809..4b343ec5e9f2e 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/types.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/types.ts @@ -148,6 +148,7 @@ export interface ValidationError { message: string; code?: T; validationType?: string; + __isBlocking__?: boolean; [key: string]: any; } @@ -186,5 +187,11 @@ type FieldValue = unknown; export interface ValidationConfig { validator: ValidationFunc; type?: string; + /** + * By default all validation are blockers, which means that if they fail, the field is invalid. + * In some cases, like when trying to add an item to the ComboBox, if the item is not valid we want + * to show a validation error. But this validation is **not** blocking. Simply, the item has not been added. + */ + isBlocking?: boolean; exitOnFail?: boolean; } diff --git a/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx b/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx index 3a03835e85970..273b213fd7d61 100644 --- a/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx +++ b/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx @@ -180,7 +180,6 @@ export const TemplateForm = ({ delete outputTemplate.template.aliases; } if (Object.keys(outputTemplate.template).length === 0) { - // @ts-expect-error delete outputTemplate.template; } diff --git a/x-pack/plugins/index_management/public/application/components/template_form/template_form_schemas.tsx b/x-pack/plugins/index_management/public/application/components/template_form/template_form_schemas.tsx index cf26ca5cd2500..c85126f08685e 100644 --- a/x-pack/plugins/index_management/public/application/components/template_form/template_form_schemas.tsx +++ b/x-pack/plugins/index_management/public/application/components/template_form/template_form_schemas.tsx @@ -125,6 +125,7 @@ export const schemas: Record = { { validator: indexPatternField(i18n), type: VALIDATION_TYPES.ARRAY_ITEM, + isBlocking: false, }, ], }, From 8f8bbc87d9f3c428c42f637b9f378182e7908af1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien=20Loix?= Date: Tue, 1 Sep 2020 11:23:53 +0200 Subject: [PATCH 15/15] Fix TS issue --- .../application/components/template_form/template_form.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx b/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx index 273b213fd7d61..3a03835e85970 100644 --- a/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx +++ b/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx @@ -180,6 +180,7 @@ export const TemplateForm = ({ delete outputTemplate.template.aliases; } if (Object.keys(outputTemplate.template).length === 0) { + // @ts-expect-error delete outputTemplate.template; }