From b91847102dd780a6cac2b3ed51e9b97b66ec162e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien=20Loix?= Date: Tue, 15 Sep 2020 12:13:13 +0200 Subject: [PATCH 01/19] [Form lib] Refactor "errorDisplayDelay" --> "valueChangeDebounceTime" --- .../es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts | 3 +++ 1 file 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 be4535fec3669..7fd59499bc489 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 @@ -194,6 +194,9 @@ export function useForm( const validateFields: FormHook['__validateFields'] = useCallback( async (fieldNames) => { + // Meanwhile the fields are being validated, the form validity is undefined + setIsValid(undefined); + const fieldsToValidate = fieldNames .map((name) => fieldsRefs.current[name]) .filter((field) => field !== undefined); From 8bb0890e5d4c990b85f5f8a72584ef683f485745 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien=20Loix?= Date: Tue, 15 Sep 2020 12:59:03 +0200 Subject: [PATCH 02/19] [Form lib] Don't expose "getFieldDefaultValue" in the form API --- src/plugins/es_ui_shared/static/forms/hook_form_lib/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 ae731caff2881..67b5b627f9a86 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 @@ -52,7 +52,7 @@ export interface FormHook * If you are only interested in the raw form data, pass `unflatten: false` to the handler */ getFormData: (options?: { unflatten?: boolean }) => T; - /* Returns an array with of all errors in the form. */ + /* Returns a list of all errors in the form */ getErrors: () => string[]; /** Resets the form to its initial state. */ reset: (options?: { resetValues?: boolean; defaultValue?: Partial }) => void; From 36d546fea5fefb35106879a974fe7b10dd329f2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien=20Loix?= Date: Mon, 28 Sep 2020 14:52:18 +0200 Subject: [PATCH 03/19] [Form lib] Add FormHook TS docs --- src/plugins/es_ui_shared/static/forms/hook_form_lib/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 67b5b627f9a86..ae731caff2881 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 @@ -52,7 +52,7 @@ export interface FormHook * If you are only interested in the raw form data, pass `unflatten: false` to the handler */ getFormData: (options?: { unflatten?: boolean }) => T; - /* Returns a list of all errors in the form */ + /* Returns an array with of all errors in the form. */ getErrors: () => string[]; /** Resets the form to its initial state. */ reset: (options?: { resetValues?: boolean; defaultValue?: Partial }) => void; From 3df3ad3550d2578e76beeae687900216d93de40c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien=20Loix?= Date: Wed, 23 Sep 2020 12:09:14 +0200 Subject: [PATCH 04/19] [Form lib] Don't set the validity to undefined before validating --- .../es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts | 3 --- 1 file changed, 3 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 7fd59499bc489..be4535fec3669 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 @@ -194,9 +194,6 @@ export function useForm( const validateFields: FormHook['__validateFields'] = useCallback( async (fieldNames) => { - // Meanwhile the fields are being validated, the form validity is undefined - setIsValid(undefined); - const fieldsToValidate = fieldNames .map((name) => fieldsRefs.current[name]) .filter((field) => field !== undefined); From 927f800342542a083748df752d28652924a40e56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien=20Loix?= Date: Tue, 29 Sep 2020 19:47:27 +0200 Subject: [PATCH 05/19] Fix TS issues --- .../components/test_pipeline/test_pipeline_flyout.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_flyout.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_flyout.tsx index aa9e2879aaddf..f1d7f7a486894 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_flyout.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_flyout.tsx @@ -21,6 +21,7 @@ import { Document } from '../../types'; import { Tabs, TestPipelineFlyoutTab, OutputTab, DocumentsTab } from './test_pipeline_tabs'; import { TestPipelineFlyoutForm } from './test_pipeline_flyout.container'; + export interface Props { onClose: () => void; handleTestPipeline: ( From 70f18653616fccfc4a060e4ecbe947a01684b2f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien=20Loix?= Date: Fri, 2 Oct 2020 08:27:09 +0200 Subject: [PATCH 06/19] Update the lib to work with internal **unflatten** object. --- .../components/form_data_provider.ts | 15 +- .../hook_form_lib/components/use_array.ts | 2 +- .../forms/hook_form_lib/form_data_context.tsx | 8 +- .../forms/hook_form_lib/hooks/use_field.ts | 140 ++++++----- .../forms/hook_form_lib/hooks/use_form.ts | 237 +++++++++--------- .../hook_form_lib/hooks/use_form_data.ts | 38 ++- .../static/forms/hook_form_lib/lib/utils.ts | 18 +- .../static/forms/hook_form_lib/types.ts | 57 +++-- 8 files changed, 262 insertions(+), 253 deletions(-) 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 ac141baf8fc71..de4edc1edf873 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 @@ -22,13 +22,16 @@ import React from 'react'; import { FormData } from '../types'; import { useFormData } from '../hooks'; -interface Props { - children: (formData: FormData) => JSX.Element | null; +interface Props { + children: (formData: I) => JSX.Element | null; pathsToWatch?: string | string[]; } -export const FormDataProvider = React.memo(({ children, pathsToWatch }: Props) => { - const { 0: formData, 2: isReady } = useFormData({ watch: pathsToWatch }); +const FormDataProviderComp = function ({ + children, + pathsToWatch, +}: Props) { + const { 0: formData, 2: isReady } = useFormData({ watch: pathsToWatch }); if (!isReady) { // No field has mounted yet, don't render anything @@ -36,4 +39,6 @@ export const FormDataProvider = React.memo(({ children, pathsToWatch }: Props) = } return children(formData); -}); +}; + +export const FormDataProvider = React.memo(FormDataProviderComp) as typeof FormDataProviderComp; diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_array.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_array.ts index 3c4f9799bb1bc..812a18680d6b8 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_array.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_array.ts @@ -107,7 +107,7 @@ export const UseArray = ({ getNewItemAtIndex, ]); - // Create a new hook field with the "hasValue" set to false so we don't use its value to build the final form data. + // Create a new hook field with the "isIncludedInOutput" set to false so we don't use its value to build the final form data. // Apart from that the field behaves like a normal field and is hooked into the form validation lifecycle. const fieldConfigBase: FieldConfig & InternalFieldConfig = { defaultValue: fieldDefaultValue, 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 0670220ccd0c9..9166d1a28b7fc 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 @@ -22,9 +22,9 @@ import React, { createContext, useContext, useMemo } from 'react'; import { FormData, FormHook } from './types'; import { Subject } from './lib'; -export interface Context { +export interface Context { getFormData$: () => Subject; - getFormData: FormHook['getFormData']; + getFormData: FormHook['getFormData']; } const FormDataContext = createContext | undefined>(undefined); @@ -45,6 +45,6 @@ export const FormDataContextProvider = ({ children, getFormData$, getFormData }: return {children}; }; -export function useFormDataContext() { - return useContext | undefined>(FormDataContext); +export function useFormDataContext() { + return useContext | undefined>(FormDataContext); } 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 7b21b6638aeac..f4f13a698ee30 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 @@ -63,6 +63,7 @@ export const useField = ( __removeField, __updateFormDataAt, __validateFields, + __getFormData$, } = form; const deserializeValue = useCallback( @@ -76,7 +77,7 @@ export const useField = ( ); const [value, setStateValue] = useState(deserializeValue); - const [errors, setErrors] = useState([]); + const [errors, setStateErrors] = useState([]); const [isPristine, setPristine] = useState(true); const [isValidating, setValidating] = useState(false); const [isChangingValue, setIsChangingValue] = useState(false); @@ -86,18 +87,12 @@ export const useField = ( const validateCounter = useRef(0); const changeCounter = useRef(0); const hasBeenReset = useRef(false); - const inflightValidation = useRef | null>(null); + const inflightValidation = useRef<(Promise & { cancel?(): void }) | null>(null); const debounceTimeout = useRef(null); + // ---------------------------------- // -- HELPERS // ---------------------------------- - const serializeValue: FieldHook['__serializeValue'] = useCallback( - (internalValue: I = value) => { - return serializer ? serializer(internalValue) : ((internalValue as unknown) as T); - }, - [serializer, value] - ); - /** * Filter an array of errors with specific validation type on them * @@ -117,6 +112,11 @@ export const useField = ( ); }; + /** + * If the field has some "formatters" defined in its config, run them in series and return + * the transformed value. This handler is called whenever the field value changes, right before + * updating the "value" state. + */ const formatInputValue = useCallback( (inputValue: unknown): T => { const isEmptyString = typeof inputValue === 'string' && inputValue.trim() === ''; @@ -125,11 +125,11 @@ export const useField = ( return inputValue as T; } - const formData = getFormData({ unflatten: false }); + const formData = __getFormData$().value; return formatters.reduce((output, formatter) => formatter(output, formData), inputValue) as T; }, - [formatters, getFormData] + [formatters, __getFormData$] ); const onValueChange = useCallback(async () => { @@ -147,7 +147,7 @@ export const useField = ( // Update the form data observable __updateFormDataAt(path, value); - // Validate field(s) (that will update form.isValid state) + // Validate field(s) (this will update the form.isValid state) await __validateFields(fieldsToValidateOnChange ?? [path]); if (isMounted.current === false) { @@ -162,15 +162,18 @@ export const useField = ( */ if (changeIteration === changeCounter.current) { if (valueChangeDebounceTime > 0) { - const delta = Date.now() - startTime; - if (delta < valueChangeDebounceTime) { + const timeElapsed = Date.now() - startTime; + + if (timeElapsed < valueChangeDebounceTime) { + const timeLeftToWait = valueChangeDebounceTime - timeElapsed; debounceTimeout.current = setTimeout(() => { debounceTimeout.current = null; setIsChangingValue(false); - }, valueChangeDebounceTime - delta); + }, timeLeftToWait); return; } } + setIsChangingValue(false); } }, [ @@ -183,41 +186,34 @@ export const useField = ( __validateFields, ]); + // Cancel any inflight validation (e.g an HTTP Request) const cancelInflightValidation = useCallback(() => { - // Cancel any inflight validation (like an HTTP Request) - if ( - inflightValidation.current && - typeof (inflightValidation.current as any).cancel === 'function' - ) { - (inflightValidation.current as any).cancel(); + if (inflightValidation.current && typeof inflightValidation.current.cancel === 'function') { + inflightValidation.current.cancel(); inflightValidation.current = null; } }, []); - const clearErrors: FieldHook['clearErrors'] = useCallback( - (validationType = VALIDATION_TYPES.FIELD) => { - setErrors((previousErrors) => filterErrors(previousErrors, validationType)); - }, - [] - ); - const runValidations = useCallback( - ({ - formData, - value: valueToValidate, - validationTypeToValidate, - }: { - formData: any; - value: I; - validationTypeToValidate?: string; - }): ValidationError[] | Promise => { + ( + { + formData, + value: valueToValidate, + validationTypeToValidate, + }: { + formData: any; + value: I; + validationTypeToValidate?: string; + }, + clearFieldErrors: FieldHook['clearErrors'] + ): ValidationError[] | Promise => { if (!validations) { return []; } // By default, for fields that have an asynchronous validation // we will clear the errors as soon as the field value changes. - clearErrors([VALIDATION_TYPES.FIELD, VALIDATION_TYPES.ASYNC]); + clearFieldErrors([VALIDATION_TYPES.FIELD, VALIDATION_TYPES.ASYNC]); cancelInflightValidation(); @@ -329,21 +325,33 @@ export const useField = ( // We first try to run the validations synchronously return runSync(); }, - [clearErrors, cancelInflightValidation, validations, getFormData, getFields, path] + [cancelInflightValidation, validations, getFormData, getFields, path] ); - // -- API // ---------------------------------- + // -- Internal API + // ---------------------------------- + const serializeValue: FieldHook['__serializeValue'] = useCallback( + (internalValue: I = value) => { + return serializer ? serializer(internalValue) : ((internalValue as unknown) as T); + }, + [serializer, value] + ); + + // ---------------------------------- + // -- Public API + // ---------------------------------- + const clearErrors: FieldHook['clearErrors'] = useCallback( + (validationType = VALIDATION_TYPES.FIELD) => { + setStateErrors((previousErrors) => filterErrors(previousErrors, validationType)); + }, + [] + ); - /** - * Validate a form field, running all its validations. - * If a validationType is provided then only that validation will be executed, - * skipping the other type of validation that might exist. - */ const validate: FieldHook['validate'] = useCallback( (validationData = {}) => { const { - formData = getFormData({ unflatten: false }), + formData = __getFormData$().value, value: valueToValidate = value, validationType, } = validationData; @@ -362,7 +370,7 @@ export const useField = ( // This is the most recent invocation setValidating(false); // Update the errors array - setErrors((prev) => { + setStateErrors((prev) => { const filteredErrors = filterErrors(prev, validationType); return [...filteredErrors, ..._validationErrors]; }); @@ -374,25 +382,23 @@ export const useField = ( }; }; - const validationErrors = runValidations({ - formData, - value: valueToValidate, - validationTypeToValidate: validationType, - }); + const validationErrors = runValidations( + { + formData, + value: valueToValidate, + validationTypeToValidate: validationType, + }, + clearErrors + ); if (Reflect.has(validationErrors, 'then')) { return (validationErrors as Promise).then(onValidationResult); } return onValidationResult(validationErrors as ValidationError[]); }, - [getFormData, value, runValidations] + [__getFormData$, value, runValidations, clearErrors] ); - /** - * Handler to change the field value - * - * @param newValue The new value to assign to the field - */ const setValue: FieldHook['setValue'] = useCallback( (newValue) => { setStateValue((prev) => { @@ -408,8 +414,8 @@ export const useField = ( [formatInputValue] ); - const _setErrors: FieldHook['setErrors'] = useCallback((_errors) => { - setErrors( + const setErrors: FieldHook['setErrors'] = useCallback((_errors) => { + setStateErrors( _errors.map((error) => ({ validationType: VALIDATION_TYPES.FIELD, __isBlocking__: true, @@ -418,11 +424,6 @@ export const useField = ( ); }, []); - /** - * Form "onChange" event handler - * - * @param event Form input change event - */ const onChange: FieldHook['onChange'] = useCallback( (event) => { const newValue = {}.hasOwnProperty.call(event!.target, 'checked') @@ -485,7 +486,7 @@ export const useField = ( case 'value': return setValue(nextValue); case 'errors': - return setErrors(nextValue); + return setStateErrors(nextValue); case 'isChangingValue': return setIsChangingValue(nextValue); case 'isPristine': @@ -539,7 +540,7 @@ export const useField = ( onChange, getErrorsMessages, setValue, - setErrors: _setErrors, + setErrors, clearErrors, validate, reset, @@ -563,7 +564,7 @@ export const useField = ( onChange, getErrorsMessages, setValue, - _setErrors, + setErrors, clearErrors, validate, reset, @@ -585,7 +586,8 @@ export const useField = ( useEffect(() => { // If the field value has been reset, we don't want to call the "onValueChange()" - // as it will set the "isPristine" state to true or validate the field, which initially we don't want. + // as it will set the "isPristine" state to true or validate the field, which we don't want + // to occur right after resetting the field state. if (hasBeenReset.current) { hasBeenReset.current = false; return; 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 be4535fec3669..2ba060399eb3c 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 @@ -58,8 +58,6 @@ export function useForm( return initDefaultValue(defaultValue); }, [defaultValue, initDefaultValue]); - const defaultValueDeserialized = useRef(defaultValueMemoized); - const { valueChangeDebounceTime, stripEmptyFields: doStripEmptyFields } = options ?? {}; const formOptions = useMemo( () => ({ @@ -72,26 +70,36 @@ export function useForm( const [isSubmitted, setIsSubmitted] = useState(false); const [isSubmitting, setSubmitting] = useState(false); const [isValid, setIsValid] = useState(undefined); + const fieldsRefs = useRef({}); const formUpdateSubscribers = useRef([]); const isMounted = useRef(false); + const defaultValueDeserialized = useRef(defaultValueMemoized); // formData$ is an observable we can subscribe to in order to receive live // update of the raw form data. As an observable it does not trigger any React // render(). - // The component is the one in charge of reading this observable - // and updating its state to trigger the necessary view render. - const formData$ = useRef | null>(null); + // The "useFormData()" hook is the one in charge of reading this observable + // and updating its own state that will trigger the necessary re-renders in the UI. + const formData$ = useRef | null>(null); + // ---------------------------------- // -- HELPERS // ---------------------------------- - const getFormData$ = useCallback((): Subject => { + const getFormData$ = useCallback((): Subject => { if (formData$.current === null) { - formData$.current = new Subject({} as T); + formData$.current = new Subject({} as I); } return formData$.current; }, []); + const updateFormData$ = useCallback( + (nextValue: { [key: string]: any }) => { + getFormData$().next(unflattenObject(nextValue)); + }, + [getFormData$] + ); + const fieldsToArray = useCallback<() => FieldHook[]>(() => Object.values(fieldsRefs.current), []); const getFieldsForOutput = useCallback( @@ -115,63 +123,27 @@ export function useForm( [] ); - const updateFormDataAt: FormHook['__updateFormDataAt'] = useCallback( + const updateFormDataAt: FormHook['__updateFormDataAt'] = useCallback( (path, value) => { const _formData$ = getFormData$(); const currentFormData = _formData$.value; if (currentFormData[path] !== value) { - _formData$.next({ ...currentFormData, [path]: value }); + updateFormData$({ ...currentFormData, [path]: value }); } return _formData$.value; }, - [getFormData$] + [getFormData$, updateFormData$] ); - const updateDefaultValueAt: FormHook['__updateDefaultValueAt'] = useCallback((path, value) => { - set(defaultValueDeserialized.current, path, value); - }, []); - - // -- API - // ---------------------------------- - const getFormData: FormHook['getFormData'] = useCallback( - (getDataOptions: Parameters['getFormData']>[0] = { unflatten: true }) => { - if (getDataOptions.unflatten) { - const fieldsToOutput = getFieldsForOutput(fieldsRefs.current, { - stripEmptyFields: formOptions.stripEmptyFields, - }); - const fieldsValue = mapFormFields(fieldsToOutput, (field) => field.__serializeValue()); - return serializer - ? (serializer(unflattenObject(fieldsValue) as I) as T) - : (unflattenObject(fieldsValue) as T); - } - - return Object.entries(fieldsRefs.current).reduce( - (acc, [key, field]) => ({ - ...acc, - [key]: field.value, - }), - {} as T - ); + const updateDefaultValueAt: FormHook['__updateDefaultValueAt'] = useCallback( + (path, value) => { + set(defaultValueDeserialized.current, path, value); }, - [getFieldsForOutput, formOptions.stripEmptyFields, serializer] + [] ); - const getErrors: FormHook['getErrors'] = useCallback(() => { - if (isValid === true) { - return []; - } - - return fieldsToArray().reduce((acc, field) => { - const fieldError = field.getErrorsMessages(); - if (fieldError === null) { - return acc; - } - return [...acc, fieldError]; - }, [] as string[]); - }, [isValid, fieldsToArray]); - const isFieldValid = (field: FieldHook) => field.isValid && !field.isValidating; const waitForFieldsToFinishValidating = useCallback(async () => { @@ -192,13 +164,13 @@ export function useForm( }); }, [fieldsToArray]); - const validateFields: FormHook['__validateFields'] = useCallback( + const validateFields: FormHook['__validateFields'] = useCallback( async (fieldNames) => { const fieldsToValidate = fieldNames .map((name) => fieldsRefs.current[name]) .filter((field) => field !== undefined); - const formData = getFormData({ unflatten: false }); + const formData = getFormData$().value; const validationResult = await Promise.all( fieldsToValidate.map((field) => field.validate({ formData })) ); @@ -245,34 +217,13 @@ export function useForm( return { areFieldsValid, isFormValid }; }, - [getFormData, fieldsToArray] + [getFormData$, fieldsToArray] ); - const validateAllFields = useCallback(async (): Promise => { - // Maybe some field are being validated because of their async validation(s). - // We make sure those validations have finished executing before proceeding. - await waitForFieldsToFinishValidating(); - - if (!isMounted.current) { - return false; - } - - const fieldsArray = fieldsToArray(); - const fieldsToValidate = fieldsArray.filter((field) => !field.isValidated); - - let isFormValid: boolean | undefined; - - if (fieldsToValidate.length === 0) { - isFormValid = fieldsArray.every(isFieldValid); - } else { - ({ isFormValid } = await validateFields(fieldsToValidate.map((field) => field.path))); - } - - setIsValid(isFormValid); - return isFormValid!; - }, [fieldsToArray, validateFields, waitForFieldsToFinishValidating]); - - const addField: FormHook['__addField'] = useCallback( + // ---------------------------------- + // -- Internal API + // ---------------------------------- + const addField: FormHook['__addField'] = useCallback( (field) => { fieldsRefs.current[field.path] = field; @@ -291,17 +242,17 @@ export function useForm( [updateFormDataAt] ); - const removeField: FormHook['__removeField'] = useCallback( + const removeField: FormHook['__removeField'] = useCallback( (_fieldNames) => { const fieldNames = Array.isArray(_fieldNames) ? _fieldNames : [_fieldNames]; - const currentFormData = { ...getFormData$().value } as FormData; + const currentFormData = { ...getFormData$().value }; fieldNames.forEach((name) => { delete fieldsRefs.current[name]; delete currentFormData[name]; }); - getFormData$().next(currentFormData as T); + updateFormData$(currentFormData); /** * After removing a field, the form validity might have changed @@ -316,40 +267,91 @@ export function useForm( return prev; }); }, - [getFormData$, fieldsToArray] + [getFormData$, updateFormData$, fieldsToArray] + ); + + const getFieldDefaultValue: FormHook['__getFieldDefaultValue'] = useCallback( + (fieldName) => get(defaultValueDeserialized.current, fieldName), + [] ); - const setFieldValue: FormHook['setFieldValue'] = useCallback((fieldName, value) => { + const readFieldConfigFromSchema: FormHook['__readFieldConfigFromSchema'] = useCallback( + (fieldName) => { + const config = (get(schema ?? {}, fieldName) as FieldConfig) || {}; + + return config; + }, + [schema] + ); + + // ---------------------------------- + // -- Public API + // ---------------------------------- + const getFormData: FormHook['getFormData'] = useCallback(() => { + const fieldsToOutput = getFieldsForOutput(fieldsRefs.current, { + stripEmptyFields: formOptions.stripEmptyFields, + }); + const fieldsValue = mapFormFields(fieldsToOutput, (field) => field.__serializeValue()); + return serializer + ? (serializer(unflattenObject(fieldsValue) as I) as T) + : (unflattenObject(fieldsValue) as T); + }, [getFieldsForOutput, formOptions.stripEmptyFields, serializer]); + + const getErrors: FormHook['getErrors'] = useCallback(() => { + if (isValid === true) { + return []; + } + + return fieldsToArray().reduce((acc, field) => { + const fieldError = field.getErrorsMessages(); + if (fieldError === null) { + return acc; + } + return [...acc, fieldError]; + }, [] as string[]); + }, [isValid, fieldsToArray]); + + const validate: FormHook['validate'] = useCallback(async (): Promise => { + // Maybe some field are being validated because of their async validation(s). + // We make sure those validations have finished executing before proceeding. + await waitForFieldsToFinishValidating(); + + if (!isMounted.current) { + return false; + } + + const fieldsArray = fieldsToArray(); + const fieldsToValidate = fieldsArray.filter((field) => !field.isValidated); + + let isFormValid: boolean | undefined; + + if (fieldsToValidate.length === 0) { + isFormValid = fieldsArray.every(isFieldValid); + } else { + ({ isFormValid } = await validateFields(fieldsToValidate.map((field) => field.path))); + } + + setIsValid(isFormValid); + return isFormValid!; + }, [fieldsToArray, validateFields, waitForFieldsToFinishValidating]); + + const setFieldValue: FormHook['setFieldValue'] = useCallback((fieldName, value) => { if (fieldsRefs.current[fieldName] === undefined) { return; } fieldsRefs.current[fieldName].setValue(value); }, []); - const setFieldErrors: FormHook['setFieldErrors'] = useCallback((fieldName, errors) => { + const setFieldErrors: FormHook['setFieldErrors'] = useCallback((fieldName, errors) => { if (fieldsRefs.current[fieldName] === undefined) { return; } fieldsRefs.current[fieldName].setErrors(errors); }, []); - const getFields: FormHook['getFields'] = useCallback(() => fieldsRefs.current, []); - - const getFieldDefaultValue: FormHook['__getFieldDefaultValue'] = useCallback( - (fieldName) => get(defaultValueDeserialized.current, fieldName), - [] - ); + const getFields: FormHook['getFields'] = useCallback(() => fieldsRefs.current, []); - const readFieldConfigFromSchema: FormHook['__readFieldConfigFromSchema'] = useCallback( - (fieldName) => { - const config = (get(schema ?? {}, fieldName) as FieldConfig) || {}; - - return config; - }, - [schema] - ); - - const submitForm: FormHook['submit'] = useCallback( + const submit: FormHook['submit'] = useCallback( async (e) => { if (e) { e.preventDefault(); @@ -358,7 +360,7 @@ export function useForm( setIsSubmitted(true); // User has attempted to submit the form at least once setSubmitting(true); - const isFormValid = await validateAllFields(); + const isFormValid = await validate(); const formData = isFormValid ? getFormData() : ({} as T); if (onSubmit) { @@ -371,13 +373,13 @@ export function useForm( return { data: formData, isValid: isFormValid! }; }, - [validateAllFields, getFormData, onSubmit] + [validate, getFormData, onSubmit] ); - const subscribe: FormHook['subscribe'] = useCallback( + const subscribe: FormHook['subscribe'] = useCallback( (handler) => { const subscription = getFormData$().subscribe((raw) => { - handler({ isValid, data: { raw, format: getFormData }, validate: validateAllFields }); + handler({ isValid, data: { raw, format: getFormData }, validate }); }); formUpdateSubscribers.current.push(subscription); @@ -391,17 +393,13 @@ export function useForm( }, }; }, - [getFormData$, isValid, getFormData, validateAllFields] + [getFormData$, isValid, getFormData, validate] ); - /** - * Reset all the fields of the form to their default values - * and reset all the states to their original value. - */ - const reset: FormHook['reset'] = useCallback( + const reset: FormHook['reset'] = useCallback( (resetOptions = { resetValues: true }) => { const { resetValues = true, defaultValue: updatedDefaultValue } = resetOptions; - const currentFormData = { ...getFormData$().value } as FormData; + const currentFormData = { ...getFormData$().value }; if (updatedDefaultValue) { defaultValueDeserialized.current = initDefaultValue(updatedDefaultValue); @@ -414,28 +412,29 @@ export function useForm( if (isFieldMounted) { const fieldDefaultValue = getFieldDefaultValue(path); field.reset({ resetValue: resetValues, defaultValue: fieldDefaultValue }); - currentFormData[path] = fieldDefaultValue; + currentFormData[path as keyof I] = fieldDefaultValue; } }); + if (resetValues) { - getFormData$().next(currentFormData as T); + updateFormData$(currentFormData); } setIsSubmitted(false); setSubmitting(false); setIsValid(undefined); }, - [getFormData$, initDefaultValue, getFieldDefaultValue] + [getFormData$, updateFormData$, initDefaultValue, getFieldDefaultValue] ); - const form = useMemo>(() => { + const form = useMemo>(() => { return { isSubmitted, isSubmitting, isValid, id, - submit: submitForm, - validate: validateAllFields, + submit, + validate, subscribe, setFieldValue, setFieldErrors, @@ -458,7 +457,7 @@ export function useForm( isSubmitting, isValid, id, - submitForm, + submit, subscribe, setFieldValue, setFieldErrors, @@ -475,7 +474,7 @@ export function useForm( addField, removeField, validateFields, - validateAllFields, + validate, ]); useEffect(() => { 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 6c6dee3624979..1a828a04b4e23 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, useCallback } from 'react'; +import { useState, useEffect, useRef } from 'react'; import { FormData, FormHook } from '../types'; import { useFormDataContext, Context } from '../form_data_context'; @@ -26,14 +26,16 @@ interface Options { form?: FormHook; } -export type HookReturn = [FormData, () => T, boolean]; +export type HookReturn = [I, () => T, boolean]; -export const useFormData = (options: Options = {}): HookReturn => { +export const useFormData = ( + options: Options = {} +): HookReturn => { const { watch, form } = options; - const ctx = useFormDataContext(); + const ctx = useFormDataContext(); - let getFormData: Context['getFormData']; - let getFormData$: Context['getFormData$']; + let getFormData: Context['getFormData']; + let getFormData$: Context['getFormData$']; if (form !== undefined) { getFormData = form.getFormData; @@ -48,26 +50,18 @@ export const useFormData = (options: Options = {}): const initialValue = getFormData$().value; - const previousRawData = useRef(initialValue); + const previousRawData = useRef(initialValue); const isMounted = useRef(false); - const [formData, setFormData] = useState(previousRawData.current); - - const formatFormData = useCallback(() => { - return getFormData({ unflatten: true }); - }, [getFormData]); + 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[]); + const pathsToWatchArray: Array = Array.isArray(watch) + ? (watch as Array) + : ([watch] as Array); - if ( - valuesToWatchArray.some( - (value) => previousRawData.current[value] !== raw[value as keyof T] - ) - ) { + if (pathsToWatchArray.some((path) => previousRawData.current[path] !== raw[path])) { previousRawData.current = raw; // Only update the state if one of the field we watch has changed. setFormData(raw); @@ -88,8 +82,8 @@ export const useFormData = (options: Options = {}): if (!isMounted.current && Object.keys(formData).length === 0) { // No field has mounted yet - return [formData, formatFormData, false]; + return [formData, getFormData, false]; } - return [formData, formatFormData, true]; + return [formData, getFormData, true]; }; diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/lib/utils.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/lib/utils.ts index 7d506e28794fd..f67070c8746a1 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/lib/utils.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/lib/utils.ts @@ -20,25 +20,11 @@ import { set } from '@elastic/safer-lodash-set'; import { FieldHook } from '../types'; -export const unflattenObject = (object: any) => +export const unflattenObject = (object: object): T => Object.entries(object).reduce((acc, [key, value]) => { set(acc, key, value); return acc; - }, {}); - -export const flattenObject = ( - object: Record, - to: Record = {}, - paths: string[] = [] -): Record => - Object.entries(object).reduce((acc, [key, value]) => { - const updatedPaths = [...paths, key]; - if (value !== null && !Array.isArray(value) && typeof value === 'object') { - return flattenObject(value, to, updatedPaths); - } - acc[updatedPaths.join('.')] = value; - return acc; - }, to); + }, {} as T); /** * Helper to map the object of fields to any of its value 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 ae731caff2881..685f3b0c4b370 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 @@ -45,28 +45,31 @@ export interface FormHook setFieldValue: (fieldName: string, value: FieldValue) => void; /** Sets a field errors imperatively. */ setFieldErrors: (fieldName: string, errors: ValidationError[]) => void; - /** Access any field on the form. */ + /** Access the fields on the form. */ getFields: () => FieldsMap; /** * Return the form data. It accepts an optional options object with an `unflatten` parameter (defaults to `true`). * If you are only interested in the raw form data, pass `unflatten: false` to the handler */ - getFormData: (options?: { unflatten?: boolean }) => T; + getFormData: () => T; /* Returns an array with of all errors in the form. */ getErrors: () => string[]; - /** Resets the form to its initial state. */ + /** + * Reset the form states to their initial value and optionally + * all the fields to their initial values. + */ reset: (options?: { resetValues?: boolean; defaultValue?: Partial }) => void; readonly __options: Required; - __getFormData$: () => Subject; + __getFormData$: () => Subject; __addField: (field: FieldHook) => void; __removeField: (fieldNames: string | string[]) => void; __validateFields: ( fieldNames: string[] ) => Promise<{ areFieldsValid: boolean; isFormValid: boolean | undefined }>; - __updateFormDataAt: (field: string, value: unknown) => T; + __updateFormDataAt: (field: string, value: unknown) => I; __updateDefaultValueAt: (field: string, value: unknown) => void; - __readFieldConfigFromSchema: (fieldName: string) => FieldConfig; - __getFieldDefaultValue: (fieldName: string) => unknown; + __readFieldConfigFromSchema: (field: string) => FieldConfig; + __getFieldDefaultValue: (field: K) => I[K]; } export type FormSchema = { @@ -119,10 +122,26 @@ export interface FieldHook { validationType?: 'field' | string; errorCode?: string; }) => string | null; + /** + * Form "onChange" event handler + * + * @param event Form input change event + */ onChange: (event: ChangeEvent<{ name?: string; value: string; checked?: boolean }>) => void; + /** + * Handler to change the field value + * + * @param value The new value to assign to the field. If you provide a callback, you wil receive + * the previous value and you need to return the next value. + */ setValue: (value: I | ((prevValue: I) => I)) => void; setErrors: (errors: ValidationError[]) => void; clearErrors: (type?: string | string[]) => void; + /** + * Validate a form field, running all its validations. + * If a validationType is provided then only that validation will be executed, + * skipping the other type of validation that might exist. + */ validate: (validateData?: { formData?: any; value?: I; @@ -166,19 +185,23 @@ export interface ValidationError { [key: string]: any; } -export interface ValidationFuncArg { +export interface ValidationFuncArg { path: string; value: V; form: { - getFormData: FormHook['getFormData']; - getFields: FormHook['getFields']; + getFormData: FormHook['getFormData']; + getFields: FormHook['getFields']; }; - formData: T; + formData: I; errors: readonly ValidationError[]; } -export type ValidationFunc = ( - data: ValidationFuncArg +export type ValidationFunc< + I extends FormData = FormData, + E extends string = string, + V = unknown +> = ( + data: ValidationFuncArg ) => ValidationError | void | undefined | Promise | void | undefined>; export interface FieldValidateResponse { @@ -199,11 +222,11 @@ type FormatterFunc = (value: any, formData: FormData) => unknown; type FieldValue = unknown; export interface ValidationConfig< - FormType extends FormData = any, - Error extends string = string, - ValueType = unknown + I extends FormData = FormData, + E extends string = string, + V = unknown > { - validator: ValidationFunc; + validator: ValidationFunc; type?: string; /** * By default all validation are blockers, which means that if they fail, the field is invalid. From d8ddb5188c91720024fe1faaa28bb216b226eb43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien=20Loix?= Date: Fri, 16 Oct 2020 15:26:03 +0200 Subject: [PATCH 07/19] Unflatten internal state only in useFormData and subscribe() --- .../forms/hook_form_lib/form_data_context.tsx | 2 +- .../hook_form_lib/hooks/use_form.test.tsx | 4 +-- .../forms/hook_form_lib/hooks/use_form.ts | 27 ++++++++++--------- .../hook_form_lib/hooks/use_form_data.ts | 13 +++++---- .../static/forms/hook_form_lib/types.ts | 16 ++++++----- .../analyzer_parameter_selects.tsx | 2 +- .../mappings_editor/use_state_listener.tsx | 4 +-- 7 files changed, 35 insertions(+), 33 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 9166d1a28b7fc..6aef6d2b0d46a 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 @@ -23,7 +23,7 @@ import { FormData, FormHook } from './types'; import { Subject } from './lib'; export interface Context { - getFormData$: () => Subject; + getFormData$: () => Subject; getFormData: FormHook['getFormData']; } 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 b28c09d07fa98..9e7066ca39464 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 @@ -253,7 +253,7 @@ describe('useForm() hook', () => { OnUpdateHandler >; - expect(data.raw).toEqual({ 'user.name': 'John' }); + expect(data.internal).toEqual({ 'user.name': 'John' }); expect(data.format()).toEqual({ user: { name: 'John' } }); // As we have touched all fields, the validity went from "undefined" to "true" expect(isValid).toBe(true); @@ -302,7 +302,7 @@ describe('useForm() hook', () => { OnUpdateHandler >; - expect(data.raw).toEqual({ + expect(data.internal).toEqual({ title: defaultValue.title, subTitle: 'hasBeenOverridden', 'user.name': defaultValue.user.name, 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 2ba060399eb3c..869d1fac54b1e 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 @@ -81,21 +81,21 @@ export function useForm( // render(). // The "useFormData()" hook is the one in charge of reading this observable // and updating its own state that will trigger the necessary re-renders in the UI. - const formData$ = useRef | null>(null); + const formData$ = useRef | null>(null); // ---------------------------------- // -- HELPERS // ---------------------------------- - const getFormData$ = useCallback((): Subject => { + const getFormData$ = useCallback((): Subject => { if (formData$.current === null) { - formData$.current = new Subject({} as I); + formData$.current = new Subject({}); } return formData$.current; }, []); const updateFormData$ = useCallback( - (nextValue: { [key: string]: any }) => { - getFormData$().next(unflattenObject(nextValue)); + (nextValue: FormData) => { + getFormData$().next(nextValue); }, [getFormData$] ); @@ -125,14 +125,11 @@ export function useForm( const updateFormDataAt: FormHook['__updateFormDataAt'] = useCallback( (path, value) => { - const _formData$ = getFormData$(); - const currentFormData = _formData$.value; + const currentFormData = getFormData$().value; if (currentFormData[path] !== value) { updateFormData$({ ...currentFormData, [path]: value }); } - - return _formData$.value; }, [getFormData$, updateFormData$] ); @@ -293,8 +290,8 @@ export function useForm( }); const fieldsValue = mapFormFields(fieldsToOutput, (field) => field.__serializeValue()); return serializer - ? (serializer(unflattenObject(fieldsValue) as I) as T) - : (unflattenObject(fieldsValue) as T); + ? serializer(unflattenObject(fieldsValue)) + : unflattenObject(fieldsValue); }, [getFieldsForOutput, formOptions.stripEmptyFields, serializer]); const getErrors: FormHook['getErrors'] = useCallback(() => { @@ -379,7 +376,11 @@ export function useForm( const subscribe: FormHook['subscribe'] = useCallback( (handler) => { const subscription = getFormData$().subscribe((raw) => { - handler({ isValid, data: { raw, format: getFormData }, validate }); + handler({ + isValid, + data: { internal: unflattenObject(raw), format: getFormData }, + validate, + }); }); formUpdateSubscribers.current.push(subscription); @@ -412,7 +413,7 @@ export function useForm( if (isFieldMounted) { const fieldDefaultValue = getFieldDefaultValue(path); field.reset({ resetValue: resetValues, defaultValue: fieldDefaultValue }); - currentFormData[path as keyof I] = fieldDefaultValue; + currentFormData[path] = fieldDefaultValue; } }); 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 1a828a04b4e23..bed6da3fd8497 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 @@ -19,6 +19,7 @@ import { useState, useEffect, useRef } from 'react'; import { FormData, FormHook } from '../types'; +import { unflattenObject } from '../lib'; import { useFormDataContext, Context } from '../form_data_context'; interface Options { @@ -50,24 +51,22 @@ export const useFormData = ( const initialValue = getFormData$().value; - const previousRawData = useRef(initialValue); + const previousRawData = useRef(initialValue); const isMounted = useRef(false); - const [formData, setFormData] = useState(previousRawData.current); + const [formData, setFormData] = useState(() => unflattenObject(previousRawData.current)); useEffect(() => { const subscription = getFormData$().subscribe((raw) => { if (watch) { - const pathsToWatchArray: Array = Array.isArray(watch) - ? (watch as Array) - : ([watch] as Array); + const pathsToWatchArray: string[] = Array.isArray(watch) ? watch : [watch]; if (pathsToWatchArray.some((path) => previousRawData.current[path] !== raw[path])) { previousRawData.current = raw; // Only update the state if one of the field we watch has changed. - setFormData(raw); + setFormData(unflattenObject(raw)); } } else { - setFormData(raw); + setFormData(unflattenObject(raw)); } }); return subscription.unsubscribe; 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 685f3b0c4b370..6196ae83a84a6 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 @@ -40,7 +40,7 @@ export interface FormHook submit: (e?: FormEvent | MouseEvent) => Promise<{ data: T; isValid: boolean }>; /** Use this handler to get the validity of the form. */ validate: () => Promise; - subscribe: (handler: OnUpdateHandler) => Subscription; + subscribe: (handler: OnUpdateHandler) => Subscription; /** Sets a field value imperatively. */ setFieldValue: (fieldName: string, value: FieldValue) => void; /** Sets a field errors imperatively. */ @@ -60,16 +60,16 @@ export interface FormHook */ reset: (options?: { resetValues?: boolean; defaultValue?: Partial }) => void; readonly __options: Required; - __getFormData$: () => Subject; + __getFormData$: () => Subject; __addField: (field: FieldHook) => void; __removeField: (fieldNames: string | string[]) => void; __validateFields: ( fieldNames: string[] ) => Promise<{ areFieldsValid: boolean; isFormValid: boolean | undefined }>; - __updateFormDataAt: (field: string, value: unknown) => I; + __updateFormDataAt: (field: string, value: unknown) => void; __updateDefaultValueAt: (field: string, value: unknown) => void; __readFieldConfigFromSchema: (field: string) => FieldConfig; - __getFieldDefaultValue: (field: K) => I[K]; + __getFieldDefaultValue: (path: string) => unknown; } export type FormSchema = { @@ -86,16 +86,18 @@ export interface FormConfig { +export interface OnFormUpdateArg { data: { - raw: { [key: string]: any }; + internal: I; format: () => T; }; validate: () => Promise; isValid?: boolean; } -export type OnUpdateHandler = (arg: OnFormUpdateArg) => void; +export type OnUpdateHandler = ( + arg: OnFormUpdateArg +) => void; export interface FormOptions { valueChangeDebounceTime?: number; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/analyzer_parameter_selects.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/analyzer_parameter_selects.tsx index c966df82fb507..eb5af5eed46a0 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/analyzer_parameter_selects.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/analyzer_parameter_selects.tsx @@ -53,7 +53,7 @@ export const AnalyzerParameterSelects = ({ useEffect(() => { const subscription = subscribe((updateData) => { - const formData = updateData.data.raw; + const formData = updateData.data.internal; const value = formData.sub ? formData.sub : formData.main; onChange(value); }); diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/use_state_listener.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/use_state_listener.tsx index feba79ce85e85..8d039475f9cf8 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/use_state_listener.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/use_state_listener.tsx @@ -35,8 +35,8 @@ export const useMappingsStateListener = ({ onChange, value }: Args) => { const isFieldFormVisible = state.fieldForm !== undefined; const emptyNameValue = isFieldFormVisible && - (state.fieldForm!.data.raw.name === undefined || - state.fieldForm!.data.raw.name.trim() === ''); + (state.fieldForm!.data.internal.name === undefined || + state.fieldForm!.data.internal.name.trim() === ''); const bypassFieldFormValidation = state.documentFields.status === 'creatingField' && emptyNameValue; From d0fd4abf33c1ff9ae0fb3f2f297873a2b4af9b22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien=20Loix?= Date: Fri, 16 Oct 2020 16:59:47 +0200 Subject: [PATCH 08/19] Fix tests --- .../components/form_data_provider.test.tsx | 2 +- .../components/use_field.test.tsx | 10 +-- .../hook_form_lib/hooks/use_form.test.tsx | 17 +++-- .../hooks/use_form_data.test.tsx | 72 ++++++++++++------- .../hook_form_lib/hooks/use_form_data.ts | 4 ++ 5 files changed, 69 insertions(+), 36 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 d9095944eaa33..a086b447994eb 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 @@ -133,7 +133,7 @@ describe('', () => { find('btn').simulate('click').update(); }); - expect(onFormData.mock.calls.length).toBe(1); + expect(onFormData.mock.calls.length).toBe(2); const [formDataUpdated] = onFormData.mock.calls[onFormData.mock.calls.length - 1] as Parameters< OnUpdateHandler 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 dbf53a9f0a359..1a7f8832e4a4e 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 @@ -58,7 +58,7 @@ describe('', () => { OnUpdateHandler >; - expect(data.raw).toEqual({ + expect(data.internal).toEqual({ name: 'John', lastName: 'Snow', }); @@ -214,8 +214,8 @@ describe('', () => { expect(serializer).not.toBeCalled(); expect(formatter).not.toBeCalled(); - let formData = formHook.getFormData({ unflatten: false }); - expect(formData.name).toEqual('John-deserialized'); + const internalFormData = formHook.__getFormData$().value; + expect(internalFormData.name).toEqual('John-deserialized'); await act(async () => { form.setInputValue('myField', 'Mike'); @@ -224,9 +224,9 @@ describe('', () => { 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(); + const outputtedFormData = formHook.getFormData(); expect(serializer).toBeCalled(); - expect(formData.name).toEqual('MIKE-serialized'); + expect(outputtedFormData.name).toEqual('MIKE-serialized'); // Make sure that when we reset the form values, we don't serialize the fields serializer.mockReset(); 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 9e7066ca39464..9626aaa9b2459 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 @@ -211,7 +211,13 @@ describe('useForm() hook', () => { test('should allow subscribing to the form data changes and provide a handler to build the form data', async () => { const TestComp = ({ onData }: { onData: OnUpdateHandler }) => { - const { form } = useForm(); + const { form } = useForm({ + serializer: (value) => ({ + user: { + name: value.user.name.toUpperCase(), + }, + }), + }); const { subscribe } = form; useEffect(() => { @@ -253,8 +259,9 @@ describe('useForm() hook', () => { OnUpdateHandler >; - expect(data.internal).toEqual({ 'user.name': 'John' }); - expect(data.format()).toEqual({ user: { name: 'John' } }); + expect(data.internal).toEqual({ user: { name: 'John' } }); + // Transform name to uppercase as decalred in our serializer func + expect(data.format()).toEqual({ user: { name: 'JOHN' } }); // As we have touched all fields, the validity went from "undefined" to "true" expect(isValid).toBe(true); }); @@ -305,7 +312,9 @@ describe('useForm() hook', () => { expect(data.internal).toEqual({ title: defaultValue.title, subTitle: 'hasBeenOverridden', - 'user.name': defaultValue.user.name, + user: { + name: defaultValue.user.name, + }, }); }); }); 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 0fb65daecf2f4..beb8e58edbf49 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 @@ -17,7 +17,7 @@ * under the License. */ -import React, { useEffect } from 'react'; +import React, { useEffect, useRef } from 'react'; import { act } from 'react-dom/test-utils'; import { registerTestBed, TestBed } from '../shared_imports'; @@ -25,37 +25,59 @@ import { Form, UseField } from '../components'; import { useForm } from './use_form'; import { useFormData, HookReturn } from './use_form_data'; -interface Props { - onChange(data: HookReturn): void; +interface Props { + onChange(data: HookReturn): void; watch?: string | string[]; } +interface Form1 { + title: string; +} + +interface Form2 { + user: { + firstName: string; + lastName: string; + }; +} + +interface Form3 { + title: string; + subTitle: string; +} + describe('useFormData() hook', () => { - const HookListenerComp = React.memo(({ onChange, watch }: Props) => { - const hookValue = useFormData({ watch }); + const HookListenerComp = function ({ onChange, watch }: Props) { + const hookValue = useFormData({ watch }); + const isMounted = useRef(false); useEffect(() => { - onChange(hookValue); + if (isMounted.current) { + onChange(hookValue); + } + isMounted.current = true; }, [hookValue, onChange]); return null; - }); + }; + + const HookListener = React.memo(HookListenerComp); 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; + return onChangeSpy.mock.calls[onChangeSpy.mock.calls.length - 1][0] as HookReturn; }; - const TestComp = (props: Props) => { - const { form } = useForm(); + const TestComp = (props: Props) => { + const { form } = useForm(); return (
- + ); }; @@ -70,9 +92,7 @@ describe('useFormData() hook', () => { }); 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); + expect(onChangeSpy).toBeCalledTimes(1); const [data] = getLastMockValue(); expect(data).toEqual({ title: 'titleInitialValue' }); }); @@ -86,7 +106,7 @@ describe('useFormData() hook', () => { setInputValue('titleField', 'titleChanged'); }); - expect(onChangeSpy).toBeCalledTimes(3); + expect(onChangeSpy).toBeCalledTimes(2); const [data] = getLastMockValue(); expect(data).toEqual({ title: 'titleChanged' }); }); @@ -96,17 +116,17 @@ describe('useFormData() hook', () => { let onChangeSpy: jest.Mock; const getLastMockValue = () => { - return onChangeSpy.mock.calls[onChangeSpy.mock.calls.length - 1][0] as HookReturn; + return onChangeSpy.mock.calls[onChangeSpy.mock.calls.length - 1][0] as HookReturn; }; - const TestComp = (props: Props) => { - const { form } = useForm(); + const TestComp = (props: Props) => { + const { form } = useForm(); return (
- + ); }; @@ -121,8 +141,8 @@ describe('useFormData() hook', () => { }); test('should expose a handler to build the form data', () => { - const { 1: format } = getLastMockValue(); - expect(format()).toEqual({ + const [formData] = getLastMockValue(); + expect(formData).toEqual({ user: { firstName: 'John', lastName: 'Snow', @@ -137,11 +157,11 @@ describe('useFormData() hook', () => { let onChangeSpy: jest.Mock; const getLastMockValue = () => { - return onChangeSpy.mock.calls[onChangeSpy.mock.calls.length - 1][0] as HookReturn; + return onChangeSpy.mock.calls[onChangeSpy.mock.calls.length - 1][0] as HookReturn; }; - const TestComp = (props: Props) => { - const { form } = useForm(); + const TestComp = (props: Props) => { + const { form } = useForm(); return (
@@ -190,9 +210,9 @@ describe('useFormData() hook', () => { return onChangeSpy.mock.calls[onChangeSpy.mock.calls.length - 1][0] as HookReturn; }; - const TestComp = ({ onChange }: Props) => { + const TestComp = ({ onChange }: Props) => { const { form } = useForm(); - const hookValue = useFormData({ form }); + const hookValue = useFormData({ form }); useEffect(() => { onChange(hookValue); 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 bed6da3fd8497..17e55e9d680ff 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 @@ -57,6 +57,10 @@ export const useFormData = ( useEffect(() => { const subscription = getFormData$().subscribe((raw) => { + if (!isMounted.current && Object.keys(raw).length === 0) { + return; + } + if (watch) { const pathsToWatchArray: string[] = Array.isArray(watch) ? watch : [watch]; From 2dedeb5366d2c781264e9d98c2d56c98ad17bc6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien=20Loix?= Date: Mon, 19 Oct 2020 13:30:18 +0200 Subject: [PATCH 09/19] Update uses of FormDataProvider --- .../simulate_template/simulate_template_flyout.tsx | 4 ++-- .../dynamic_mapping_section/dynamic_mapping_section.tsx | 4 +--- .../source_field_section/source_field_section.tsx | 4 +++- .../document_fields/fields/edit_field/edit_field_form_row.tsx | 3 ++- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/index_management/public/application/components/index_templates/simulate_template/simulate_template_flyout.tsx b/x-pack/plugins/index_management/public/application/components/index_templates/simulate_template/simulate_template_flyout.tsx index f818f49d8aa59..8e8514d1b5165 100644 --- a/x-pack/plugins/index_management/public/application/components/index_templates/simulate_template/simulate_template_flyout.tsx +++ b/x-pack/plugins/index_management/public/application/components/index_templates/simulate_template/simulate_template_flyout.tsx @@ -125,9 +125,9 @@ export const SimulateTemplateFlyoutContent = ({ - + > {(formData) => { - return ; + return ; }} diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/dynamic_mapping_section/dynamic_mapping_section.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/dynamic_mapping_section/dynamic_mapping_section.tsx index c5001740c26c6..cbb39b70f965f 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/dynamic_mapping_section/dynamic_mapping_section.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/dynamic_mapping_section/dynamic_mapping_section.tsx @@ -54,9 +54,7 @@ export const DynamicMappingSection = () => ( {(formData) => { const { - 'dynamicMapping.enabled': enabled, - // eslint-disable-next-line @typescript-eslint/naming-convention - 'dynamicMapping.date_detection': dateDetection, + dynamicMapping: { enabled, date_detection: dateDetection }, } = formData; if (enabled === undefined) { diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/source_field_section/source_field_section.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/source_field_section/source_field_section.tsx index d195f1abfc444..aafdc7e58898d 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/source_field_section/source_field_section.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/source_field_section/source_field_section.tsx @@ -155,7 +155,9 @@ export const SourceFieldSection = () => { > {(formData) => { - const { 'sourceField.enabled': enabled } = formData; + const { + sourceField: { enabled }, + } = formData; if (enabled === undefined) { return null; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field_form_row.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field_form_row.tsx index ce349b2c6104f..0a03344a662ec 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field_form_row.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field_form_row.tsx @@ -5,6 +5,7 @@ */ import React, { useState } from 'react'; +import { get } from 'lodash'; import { EuiFlexGroup, EuiFlexItem, @@ -193,7 +194,7 @@ export const EditFieldFormRow = React.memo( return formFieldPath ? ( {(formData) => { - setIsContentVisible(formData[formFieldPath]); + setIsContentVisible(get(formData, formFieldPath)); return renderContent(); }} From 4c84272d0e589466798d564116b33d6cbdc1b28a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien=20Loix?= Date: Mon, 19 Oct 2020 13:38:35 +0200 Subject: [PATCH 10/19] Update uses of useFormData --- .../components/template_form/steps/step_logistics.tsx | 2 +- .../public/cases/components/add_comment/index.tsx | 2 +- .../components/rules/step_about_rule/index.tsx | 4 ++-- .../components/rules/step_define_rule/index.tsx | 10 +++++----- .../components/rules/step_rule_actions/index.tsx | 4 ++-- 5 files changed, 11 insertions(+), 11 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 56f040fc59a7b..89e857eec0bb3 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 @@ -157,7 +157,7 @@ export const StepLogistics: React.FunctionComponent = React.memo( getFormData, } = form; - const [{ addMeta }] = useFormData({ + const [{ addMeta }] = useFormData<{ addMeta: boolean }>({ form, watch: 'addMeta', }); diff --git a/x-pack/plugins/security_solution/public/cases/components/add_comment/index.tsx b/x-pack/plugins/security_solution/public/cases/components/add_comment/index.tsx index 3e3d21b9926d1..5b77c4d99a951 100644 --- a/x-pack/plugins/security_solution/public/cases/components/add_comment/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/add_comment/index.tsx @@ -54,7 +54,7 @@ export const AddComment = React.memo( const fieldName = 'comment'; const { setFieldValue, reset, submit } = form; - const [{ comment }] = useFormData({ form, watch: [fieldName] }); + const [{ comment }] = useFormData<{ comment: string }>({ form, watch: [fieldName] }); const onCommentChange = useCallback((newValue) => setFieldValue(fieldName, newValue), [ setFieldValue, diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx index fc03e07442f9e..eedb842412aa5 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx @@ -87,10 +87,10 @@ const StepAboutRuleComponent: FC = ({ schema, }); const { getFields, getFormData, submit } = form; - const [{ severity: formSeverity }] = (useFormData({ + const [{ severity: formSeverity }] = useFormData>({ form, watch: ['severity'], - }) as unknown) as [Partial]; + }); useEffect(() => { const formSeverityValue = formSeverity?.value; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx index 27d69c6887011..94f32eaa7ae83 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx @@ -141,17 +141,17 @@ const StepDefineRuleComponent: FC = ({ 'threshold.value': formThresholdValue, 'threshold.field': formThresholdField, }, - ] = (useFormData({ - form, - watch: ['index', 'ruleType', 'queryBar', 'threshold.value', 'threshold.field', 'threatIndex'], - }) as unknown) as [ + ] = useFormData< Partial< DefineStepRule & { 'threshold.value': number | undefined; 'threshold.field': string[] | undefined; } > - ]; + >({ + form, + watch: ['index', 'ruleType', 'queryBar', 'threshold.value', 'threshold.field', 'threatIndex'], + }); const [isQueryBarValid, setIsQueryBarValid] = useState(false); const index = formIndex || initialState.index; const threatIndex = formThreatIndex || initialState.threatIndex; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/index.tsx index 349a79d4e40f9..be63332233c2e 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/index.tsx @@ -93,10 +93,10 @@ const StepRuleActionsComponent: FC = ({ schema, }); const { getFields, getFormData, submit } = form; - const [{ throttle: formThrottle }] = (useFormData({ + const [{ throttle: formThrottle }] = useFormData>({ form, watch: ['throttle'], - }) as unknown) as [Partial]; + }); const throttle = formThrottle || initialState.throttle; const handleSubmit = useCallback( From 38555ef6c680b38c0bd82c0faa42b474248f59c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien=20Loix?= Date: Mon, 19 Oct 2020 16:42:10 +0200 Subject: [PATCH 11/19] useFormData() : update the serializer handler along with the state --- .../forms/hook_form_lib/hooks/use_form_data.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 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 17e55e9d680ff..9487e2d30c680 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 { unflattenObject } from '../lib'; @@ -55,6 +55,15 @@ export const useFormData = ( const isMounted = useRef(false); const [formData, setFormData] = useState(() => unflattenObject(previousRawData.current)); + /** + * We do want to offer to the consumer a handler to serialize the form data that changes each time + * the formData **state** changes. This is why we added the "formData" dep to the array and added the eslint override. + */ + const serializer = useCallback(() => { + return getFormData(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [getFormData, formData]); + useEffect(() => { const subscription = getFormData$().subscribe((raw) => { if (!isMounted.current && Object.keys(raw).length === 0) { @@ -85,8 +94,8 @@ export const useFormData = ( if (!isMounted.current && Object.keys(formData).length === 0) { // No field has mounted yet - return [formData, getFormData, false]; + return [formData, serializer, false]; } - return [formData, getFormData, true]; + return [formData, serializer, true]; }; From d1635088ab19fab281319f03f58b81c15edb0113 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien=20Loix?= Date: Mon, 19 Oct 2020 17:15:49 +0200 Subject: [PATCH 12/19] Remove unnecessary useFormData() from pipeline documents tab --- .../test_pipeline_tabs/tab_documents/tab_documents.tsx | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_tabs/tab_documents/tab_documents.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_tabs/tab_documents/tab_documents.tsx index cd82e0f4ff5ca..3a9242a1ce61b 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_tabs/tab_documents/tab_documents.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_tabs/tab_documents/tab_documents.tsx @@ -149,16 +149,15 @@ export const DocumentsTab: FunctionComponent = ({ resetTestOutput, }) => { const { services } = useKibana(); - - const [, formatData] = useFormData({ form }); + const { getFormData, reset } = form; const onAddDocumentHandler = useCallback( (document) => { - const { documents: existingDocuments = [] } = formatData(); + const { documents: existingDocuments = [] } = getFormData(); - form.reset({ defaultValue: { documents: [...existingDocuments, document] } }); + reset({ defaultValue: { documents: [...existingDocuments, document] } }); }, - [form, formatData] + [reset, getFormData] ); const [showResetModal, setShowResetModal] = useState(false); From 6ccef56e6cdb817afc97eed6a8b5d6eeb76529f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien=20Loix?= Date: Mon, 19 Oct 2020 17:18:51 +0200 Subject: [PATCH 13/19] Remove unnecessary Partial in type --- .../components/rules/step_about_rule/index.tsx | 2 +- .../components/rules/step_define_rule/index.tsx | 10 ++++------ .../components/rules/step_rule_actions/index.tsx | 2 +- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx index eedb842412aa5..2479a260872be 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx @@ -87,7 +87,7 @@ const StepAboutRuleComponent: FC = ({ schema, }); const { getFields, getFormData, submit } = form; - const [{ severity: formSeverity }] = useFormData>({ + const [{ severity: formSeverity }] = useFormData({ form, watch: ['severity'], }); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx index 94f32eaa7ae83..bd3578bce6912 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx @@ -142,12 +142,10 @@ const StepDefineRuleComponent: FC = ({ 'threshold.field': formThresholdField, }, ] = useFormData< - Partial< - DefineStepRule & { - 'threshold.value': number | undefined; - 'threshold.field': string[] | undefined; - } - > + DefineStepRule & { + 'threshold.value': number | undefined; + 'threshold.field': string[] | undefined; + } >({ form, watch: ['index', 'ruleType', 'queryBar', 'threshold.value', 'threshold.field', 'threatIndex'], diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/index.tsx index be63332233c2e..dd1d92e7e72a3 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/index.tsx @@ -93,7 +93,7 @@ const StepRuleActionsComponent: FC = ({ schema, }); const { getFields, getFormData, submit } = form; - const [{ throttle: formThrottle }] = useFormData>({ + const [{ throttle: formThrottle }] = useFormData({ form, watch: ['throttle'], }); From daa288256b270a4bc73af90a9a8878d1051e75b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien=20Loix?= Date: Mon, 19 Oct 2020 18:10:38 +0200 Subject: [PATCH 14/19] Fix scaling_factor additional field --- .../fields/field_types/numeric_type.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/numeric_type.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/numeric_type.tsx index 6ad3c9c5d0bd4..3a02a4db5f4c9 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/numeric_type.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/numeric_type.tsx @@ -6,7 +6,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; -import { NormalizedField, Field as FieldType } from '../../../../types'; +import { NormalizedField, Field as FieldType, ComboBoxOption } from '../../../../types'; import { getFieldConfig } from '../../../../lib'; import { UseField, FormDataProvider, NumericField, Field } from '../../../../shared_imports'; import { @@ -48,9 +48,9 @@ export const NumericType = ({ field }: Props) => { <> {/* scaling_factor */} - - {(formData) => - formData.subType === 'scaled_float' ? ( + pathsToWatch="subType"> + {(formData) => { + return formData.subType?.[0]?.value === 'scaled_float' ? ( { component={Field} /> - ) : null - } + ) : null; + }} From 2e6d167b2d58892a277df1797cb5b86c81027375 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien=20Loix?= Date: Mon, 19 Oct 2020 18:15:26 +0200 Subject: [PATCH 15/19] Fix date_range additional field --- .../document_fields/fields/field_types/range_type.tsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/range_type.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/range_type.tsx index 9a37f55ac8e9d..3f1215cc6619c 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/range_type.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/range_type.tsx @@ -5,7 +5,12 @@ */ import React from 'react'; -import { NormalizedField, Field as FieldType, ParameterName } from '../../../../types'; +import { + NormalizedField, + Field as FieldType, + ParameterName, + ComboBoxOption, +} from '../../../../types'; import { getFieldConfig } from '../../../../lib'; import { StoreParameter, @@ -33,9 +38,9 @@ export const RangeType = ({ field }: Props) => { - + pathsToWatch="subType"> {(formData) => - formData.subType === 'date_range' ? ( + formData.subType?.[0]?.value === 'date_range' ? ( Date: Mon, 19 Oct 2020 18:17:02 +0200 Subject: [PATCH 16/19] Fix date_range additional field (2) --- .../document_fields/fields/field_types/range_type.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/range_type.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/range_type.tsx index 3f1215cc6619c..b4bab6d35af34 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/range_type.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/range_type.tsx @@ -51,9 +51,9 @@ export const RangeType = ({ field }: Props) => { - + pathsToWatch="subType"> {(formData) => - formData.subType === 'date_range' ? ( + formData.subType?.[0]?.value === 'date_range' ? ( ) : null } From 24f0e4a22f98edbda650f56e30f81560fede967a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien=20Loix?= Date: Mon, 19 Oct 2020 18:40:37 +0200 Subject: [PATCH 17/19] Fix TS issues --- .../components/mappings_editor/mappings_state_context.tsx | 4 ++-- .../application/components/mappings_editor/reducer.ts | 8 ++++---- .../components/processor_form/processors/json.tsx | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_state_context.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_state_context.tsx index a402dec250056..4912b0963bc12 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_state_context.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_state_context.tsx @@ -18,7 +18,7 @@ export const StateProvider: React.FC = ({ children }) => { configuration: { defaultValue: {}, data: { - raw: {}, + internal: {}, format: () => ({}), }, validate: () => Promise.resolve(true), @@ -26,7 +26,7 @@ export const StateProvider: React.FC = ({ children }) => { templates: { defaultValue: {}, data: { - raw: {}, + internal: {}, format: () => ({}), }, validate: () => Promise.resolve(true), 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 e7efd6f28343b..47e9d5200ea08 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 @@ -176,7 +176,7 @@ export const reducer = (state: State, action: Action): State => { configuration: { ...state.configuration, data: { - raw: action.value.configuration, + internal: action.value.configuration, format: () => action.value.configuration, }, defaultValue: action.value.configuration, @@ -184,7 +184,7 @@ export const reducer = (state: State, action: Action): State => { templates: { ...state.templates, data: { - raw: action.value.templates, + internal: action.value.templates, format: () => action.value.templates, }, defaultValue: action.value.templates, @@ -217,7 +217,7 @@ export const reducer = (state: State, action: Action): State => { isValid: true, defaultValue: action.value, data: { - raw: action.value, + internal: action.value, format: () => action.value, }, validate: async () => true, @@ -241,7 +241,7 @@ export const reducer = (state: State, action: Action): State => { isValid: true, defaultValue: action.value, data: { - raw: action.value, + internal: action.value, format: () => action.value, }, validate: async () => true, diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/json.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/json.tsx index f01228a26297b..841654fcc429e 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/json.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/json.tsx @@ -6,7 +6,7 @@ import React, { FunctionComponent, useEffect, useState } from 'react'; import { i18n } from '@kbn/i18n'; - +import { get } from 'lodash'; import { FIELD_TYPES, UseField, @@ -44,8 +44,8 @@ export const Json: FunctionComponent = () => { const form = useFormContext(); const [isAddToPathDisabled, setIsAddToPathDisabled] = useState(false); useEffect(() => { - const subscription = form.subscribe(({ data: { raw: rawData } }) => { - const hasTargetField = !!rawData[TARGET_FIELD_PATH]; + const subscription = form.subscribe(({ data: { internal } }) => { + const hasTargetField = !!get(internal, TARGET_FIELD_PATH); if (hasTargetField && !isAddToPathDisabled) { setIsAddToPathDisabled(true); form.getFields()[ADD_TO_ROOT_FIELD_PATH].setValue(false); From 364855cf5525461c9c54c2d3c25a55edccceda60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien=20Loix?= Date: Mon, 19 Oct 2020 18:51:09 +0200 Subject: [PATCH 18/19] Update ILM form --- .../sections/edit_policy/components/phases/cold_phase.tsx | 5 ++++- .../edit_policy/components/phases/delete_phase.tsx | 7 +++++-- .../edit_policy/components/phases/hot_phase/hot_phase.tsx | 6 +++++- .../components/phases/shared/forcemerge_field.tsx | 8 +++++--- .../sections/edit_policy/components/phases/warm_phase.tsx | 8 ++++++-- 5 files changed, 25 insertions(+), 9 deletions(-) diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/cold_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/cold_phase.tsx index 7ed8a94403a9b..7a22fb5bc1f0c 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/cold_phase.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/cold_phase.tsx @@ -7,6 +7,7 @@ import React, { FunctionComponent, Fragment } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; +import { get } from 'lodash'; import { EuiFieldNumber, EuiDescribedFormGroup, EuiSwitch, EuiTextColor } from '@elastic/eui'; @@ -56,10 +57,12 @@ export const ColdPhase: FunctionComponent = ({ errors, isShowingErrors, }) => { - const [{ [useRolloverPath]: hotPhaseRolloverEnabled }] = useFormData({ + const [formData] = useFormData({ watch: [useRolloverPath], }); + const hotPhaseRolloverEnabled = get(formData, useRolloverPath); + return (
<> diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/delete_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/delete_phase.tsx index 59e4738657be4..78ae66327654c 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/delete_phase.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/delete_phase.tsx @@ -5,6 +5,7 @@ */ import React, { FunctionComponent, Fragment } from 'react'; +import { get } from 'lodash'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiDescribedFormGroup, EuiSwitch, EuiTextColor, EuiFormRow } from '@elastic/eui'; @@ -48,10 +49,12 @@ export const DeletePhase: FunctionComponent = ({ isShowingErrors, getUrlForApp, }) => { - const [{ [useRolloverPath]: hotPhaseRolloverEnabled }] = useFormData({ - watch: [useRolloverPath], + const [formData] = useFormData({ + watch: useRolloverPath, }); + const hotPhaseRolloverEnabled = get(formData, useRolloverPath); + return (
void }> = ({ setWarmPhaseOnRollover, }) => { - const [{ [useRolloverPath]: isRolloverEnabled }] = useFormData({ watch: [useRolloverPath] }); const form = useFormContext(); + const [formData] = useFormData({ + watch: useRolloverPath, + }); + const isRolloverEnabled = get(formData, useRolloverPath); const isShowingErrors = form.isValid === false; const [showEmptyRolloverFieldsError, setShowEmptyRolloverFieldsError] = useState(false); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/forcemerge_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/forcemerge_field.tsx index 987133fd652ac..c6f02fd219130 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/forcemerge_field.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/forcemerge_field.tsx @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - +import { get } from 'lodash'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiDescribedFormGroup, EuiSpacer, EuiTextColor } from '@elastic/eui'; import React from 'react'; @@ -23,9 +23,11 @@ interface Props { const forceMergeEnabledPath = '_meta.hot.forceMergeEnabled'; export const Forcemerge: React.FunctionComponent = ({ phase }) => { - const [{ [forceMergeEnabledPath]: forceMergeEnabled }] = useFormData({ - watch: [forceMergeEnabledPath], + const [formData] = useFormData({ + watch: forceMergeEnabledPath, }); + const forceMergeEnabled = get(formData, forceMergeEnabledPath); + return ( = ({ errors, isShowingErrors, }) => { - const [{ [useRolloverPath]: hotPhaseRolloverEnabled }] = useFormData({ - watch: [useRolloverPath], + const [formData] = useFormData({ + watch: useRolloverPath, }); + + const hotPhaseRolloverEnabled = get(formData, useRolloverPath); + return (
<> From 91260b6a8c829c04c7083b688fa4b41b4aefc1a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien=20Loix?= Date: Tue, 20 Oct 2020 09:53:01 +0200 Subject: [PATCH 19/19] Fix TS issue --- .../test_pipeline_tabs/tab_documents/tab_documents.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_tabs/tab_documents/tab_documents.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_tabs/tab_documents/tab_documents.tsx index 3a9242a1ce61b..6888f947b8606 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_tabs/tab_documents/tab_documents.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_tabs/tab_documents/tab_documents.tsx @@ -21,7 +21,6 @@ import { ValidationFuncArg, FormHook, Form, - useFormData, } from '../../../../../../../shared_imports'; import { Document } from '../../../../types'; import { AddDocumentsAccordion } from './add_documents_accordion';