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/components/form.tsx b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form.tsx
index b3a15fea8b187..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
@@ -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/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/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..0e6a75e9c5065
--- /dev/null
+++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/form_data_context.tsx
@@ -0,0 +1,50 @@
+/*
+ * 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 = ({ 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/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';
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/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.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts
index 35bac5b9a58c6..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]
@@ -389,6 +395,7 @@ export function useForm(
isValid,
id,
submit: submitForm,
+ validate: validateAllFields,
subscribe,
setFieldValue,
setFieldErrors,
@@ -428,6 +435,7 @@ export function useForm(
addField,
removeField,
validateFields,
+ validateAllFields,
]);
useEffect(() => {
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..0fb65daecf2f4
--- /dev/null
+++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form_data.test.tsx
@@ -0,0 +1,234 @@
+/*
+ * 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('format form data', () => {
+ 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();
+ setup({ onChange: onChangeSpy });
+ });
+
+ test('should expose a handler to build the form data', () => {
+ const { 1: format } = getLastMockValue();
+ expect(format()).toEqual({
+ user: {
+ firstName: 'John',
+ lastName: 'Snow',
+ },
+ });
+ });
+ });
+
+ 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..fb4a0984438ad
--- /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, useCallback } 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, () => T, 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);
+
+ const formatFormData = useCallback(() => {
+ return getFormData({ unflatten: true });
+ }, [getFormData]);
+
+ 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, formatFormData, false];
+ }
+
+ return [formData, formatFormData, true];
+};
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/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..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
@@ -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;
@@ -147,6 +148,7 @@ export interface ValidationError {
message: string;
code?: T;
validationType?: string;
+ __isBlocking__?: boolean;
[key: string]: any;
}
@@ -185,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/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', {
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..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
@@ -17,13 +17,13 @@ import { i18n } from '@kbn/i18n';
import {
useForm,
+ useFormData,
Form,
getUseField,
getFormRow,
Field,
Forms,
JsonEditorField,
- FormDataProvider,
} from '../../../../shared_imports';
import { documentationService } from '../../../services/documentation';
import { schemas, nameConfig, nameConfigWithoutValidations } from '../template_form_schemas';
@@ -118,9 +118,7 @@ interface LogisticsForm {
}
interface LogisticsFormInternal extends LogisticsForm {
- __internal__: {
- addMeta: boolean;
- };
+ addMeta: boolean;
}
interface Props {
@@ -133,14 +131,12 @@ interface Props {
function formDeserializer(formData: LogisticsForm): LogisticsFormInternal {
return {
...formData,
- __internal__: {
- addMeta: Boolean(formData._meta && Object.keys(formData._meta).length),
- },
+ addMeta: Boolean(formData._meta && Object.keys(formData._meta).length),
};
}
function formSerializer(formData: LogisticsFormInternal): LogisticsForm {
- const { __internal__, ...rest } = formData;
+ const { addMeta, ...rest } = formData;
return rest;
}
@@ -153,7 +149,18 @@ export const StepLogistics: React.FunctionComponent = React.memo(
serializer: formSerializer,
deserializer: formDeserializer,
});
- const { subscribe, submit, isSubmitted, isValid: isFormValid, getErrors: getFormErrors } = form;
+ const {
+ submit,
+ isSubmitted,
+ isValid: isFormValid,
+ getErrors: getFormErrors,
+ getFormData,
+ } = form;
+
+ const [{ addMeta }] = useFormData({
+ form,
+ watch: 'addMeta',
+ });
/**
* When the consumer call validate() on this step, we submit the form so it enters the "isSubmitted" state
@@ -164,15 +171,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()
@@ -296,34 +300,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.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,
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..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,
},
],
},
@@ -213,13 +214,11 @@ export const schemas: Record = {
}
},
},
- __internal__: {
- 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',
+ }),
},
},
};
diff --git a/x-pack/plugins/index_management/public/shared_imports.ts b/x-pack/plugins/index_management/public/shared_imports.ts
index 2ba2a5c493c49..f7f992a090501 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,