diff --git a/src/plugins/es_ui_shared/public/forms/multi_content/multi_content_context.tsx b/src/plugins/es_ui_shared/public/forms/multi_content/multi_content_context.tsx index 210b0cedccd06..c5659745f229a 100644 --- a/src/plugins/es_ui_shared/public/forms/multi_content/multi_content_context.tsx +++ b/src/plugins/es_ui_shared/public/forms/multi_content/multi_content_context.tsx @@ -17,7 +17,7 @@ * under the License. */ -import React, { useEffect, useCallback, createContext, useContext } from 'react'; +import React, { useEffect, useCallback, createContext, useContext, useRef } from 'react'; import { useMultiContent, HookProps, Content, MultiContent } from './use_multi_content'; @@ -55,7 +55,14 @@ export function useMultiContentContext(contentId: K) { - const { updateContentAt, saveSnapshotAndRemoveContent, getData } = useMultiContentContext(); + const isMounted = useRef(false); + const defaultValue = useRef(undefined); + const { + updateContentAt, + saveSnapshotAndRemoveContent, + getData, + getSingleContentData, + } = useMultiContentContext(); const updateContent = useCallback( (content: Content) => { @@ -71,12 +78,22 @@ export function useContent(contentId: K) { }; }, [contentId, saveSnapshotAndRemoveContent]); - const data = getData(); - const defaultValue = data[contentId]; + useEffect(() => { + if (isMounted.current === false) { + isMounted.current = true; + } + }, []); + + if (isMounted.current === false) { + // Only read the default value once, on component mount to avoid re-rendering the + // consumer each time the multi-content validity ("isValid") changes. + defaultValue.current = getSingleContentData(contentId); + } return { - defaultValue, + defaultValue: defaultValue.current!, updateContent, getData, + getSingleContentData, }; } diff --git a/src/plugins/es_ui_shared/public/forms/multi_content/use_multi_content.ts b/src/plugins/es_ui_shared/public/forms/multi_content/use_multi_content.ts index adc68a39a4a5b..8d470f6454b0e 100644 --- a/src/plugins/es_ui_shared/public/forms/multi_content/use_multi_content.ts +++ b/src/plugins/es_ui_shared/public/forms/multi_content/use_multi_content.ts @@ -45,6 +45,7 @@ export interface MultiContent { updateContentAt: (id: keyof T, content: Content) => void; saveSnapshotAndRemoveContent: (id: keyof T) => void; getData: () => T; + getSingleContentData: (contentId: K) => T[K]; validate: () => Promise; validation: Validation; } @@ -109,9 +110,22 @@ export function useMultiContent({ }; }, [stateData, validation]); + /** + * Read a single content data. + */ + const getSingleContentData = useCallback( + (contentId: K): T[K] => { + if (contents.current[contentId]) { + return contents.current[contentId].getData(); + } + return stateData[contentId]; + }, + [stateData] + ); + const updateContentValidity = useCallback( (updatedData: { [key in keyof T]?: boolean | undefined }): boolean | undefined => { - let allContentValidity: boolean | undefined; + let isAllContentValid: boolean | undefined = validation.isValid; setValidation((prev) => { if ( @@ -120,7 +134,7 @@ export function useMultiContent({ ) ) { // No change in validation, nothing to update - allContentValidity = prev.isValid; + isAllContentValid = prev.isValid; return prev; } @@ -129,21 +143,21 @@ export function useMultiContent({ ...updatedData, }; - allContentValidity = Object.values(nextContentsValidityState).some( + isAllContentValid = Object.values(nextContentsValidityState).some( (_isValid) => _isValid === undefined ) ? undefined : Object.values(nextContentsValidityState).every(Boolean); return { - isValid: allContentValidity, + isValid: isAllContentValid, contents: nextContentsValidityState, }; }); - return allContentValidity; + return isAllContentValid; }, - [] + [validation.isValid] ); /** @@ -163,7 +177,7 @@ export function useMultiContent({ } return Boolean(updateContentValidity(updatedValidation)); - }, [updateContentValidity]); + }, [validation.isValid, updateContentValidity]); /** * Update a content. It replaces the content in our "contents" map and update @@ -186,7 +200,7 @@ export function useMultiContent({ }); } }, - [updateContentValidity, onChange] + [updateContentValidity, onChange, getData, validate] ); /** @@ -211,6 +225,7 @@ export function useMultiContent({ return { getData, + getSingleContentData, validate, validation, updateContentAt, 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 4c4a7f0642022..4c8e91b13b1b7 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 @@ -29,6 +29,7 @@ interface Props { export const FormDataProvider = React.memo(({ children, pathsToWatch }: Props) => { const form = useFormContext(); + const { subscribe } = form; const previousRawData = useRef(form.__getFormData$().value); const [formData, setFormData] = useState(previousRawData.current); @@ -54,9 +55,9 @@ export const FormDataProvider = React.memo(({ children, pathsToWatch }: Props) = ); useEffect(() => { - const subscription = form.subscribe(onFormData); + const subscription = subscribe(onFormData); return subscription.unsubscribe; - }, [form.subscribe, onFormData]); + }, [subscribe, onFormData]); return children(formData); }); 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 1605c09f575f6..3688421964d2e 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 @@ -17,7 +17,7 @@ * under the License. */ -import { useState, useEffect, useRef } from 'react'; +import { useState, useEffect, useRef, useCallback } from 'react'; import { useFormContext } from '../form_context'; @@ -83,14 +83,18 @@ export const UseArray = ({ const [items, setItems] = useState(initialState); - const updatePaths = (_rows: ArrayItem[]) => - _rows.map( - (row, index) => - ({ - ...row, - path: `${path}[${index}]`, - } as ArrayItem) - ); + const updatePaths = useCallback( + (_rows: ArrayItem[]) => { + return _rows.map( + (row, index) => + ({ + ...row, + path: `${path}[${index}]`, + } as ArrayItem) + ); + }, + [path] + ); const addItem = () => { setItems((previousItems) => { @@ -108,11 +112,13 @@ export const UseArray = ({ useEffect(() => { if (didMountRef.current) { - setItems(updatePaths(items)); + setItems((prev) => { + return updatePaths(prev); + }); } else { didMountRef.current = true; } - }, [path]); + }, [path, updatePaths]); return children({ items, addItem, removeItem }); }; 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 7ad32cb0bc3f0..f00beb470a9fc 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 @@ -30,8 +30,9 @@ describe('', () => { const TestComp = ({ onData }: { onData: OnUpdateHandler }) => { const { form } = useForm(); + const { subscribe } = form; - useEffect(() => form.subscribe(onData).unsubscribe, [form]); + useEffect(() => subscribe(onData).unsubscribe, [subscribe, onData]); return (
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 b83006c6cec52..b2f00610a3d33 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 @@ -17,7 +17,7 @@ * under the License. */ -import { useState, useEffect, useRef, useMemo } from 'react'; +import { useMemo, useState, useEffect, useRef, useCallback } from 'react'; import { FormHook, FieldHook, FieldConfig, FieldValidateResponse, ValidationError } from '../types'; import { FIELD_TYPES, VALIDATION_TYPES } from '../constants'; @@ -34,21 +34,21 @@ export const useField = ( label = '', labelAppend = '', helpText = '', - validations = [], - formatters = [], - fieldsToValidateOnChange = [path], + validations, + formatters, + fieldsToValidateOnChange, errorDisplayDelay = form.__options.errorDisplayDelay, - serializer = (value: unknown) => value, - deserializer = (value: unknown) => value, + serializer, + deserializer, } = config; + const { getFormData, __removeField, __updateFormDataAt, __validateFields } = form; - const initialValue = useMemo( - () => - typeof defaultValue === 'function' - ? deserializer(defaultValue()) - : deserializer(defaultValue), - [defaultValue] - ) as T; + const initialValue = useMemo(() => { + if (typeof defaultValue === 'function') { + return deserializer ? deserializer(defaultValue()) : defaultValue(); + } + return deserializer ? deserializer(defaultValue) : defaultValue; + }, [defaultValue, deserializer]) as T; const [value, setStateValue] = useState(initialValue); const [errors, setErrors] = useState([]); @@ -64,6 +64,12 @@ export const useField = ( // -- HELPERS // ---------------------------------- + const serializeOutput: FieldHook['__serializeOutput'] = useCallback( + (rawValue = value) => { + return serializer ? serializer(rawValue) : rawValue; + }, + [serializer, value] + ); /** * Filter an array of errors with specific validation type on them @@ -84,19 +90,22 @@ export const useField = ( ); }; - const formatInputValue = (inputValue: unknown): T => { - const isEmptyString = typeof inputValue === 'string' && inputValue.trim() === ''; + const formatInputValue = useCallback( + (inputValue: unknown): T => { + const isEmptyString = typeof inputValue === 'string' && inputValue.trim() === ''; - if (isEmptyString) { - return inputValue as T; - } + if (isEmptyString || !formatters) { + return inputValue as T; + } - const formData = form.getFormData({ unflatten: false }); + const formData = getFormData({ unflatten: false }); - return formatters.reduce((output, formatter) => formatter(output, formData), inputValue) as T; - }; + return formatters.reduce((output, formatter) => formatter(output, formData), inputValue) as T; + }, + [formatters, getFormData] + ); - const onValueChange = async () => { + const onValueChange = useCallback(async () => { const changeIteration = ++changeCounter.current; const startTime = Date.now(); @@ -116,10 +125,10 @@ export const useField = ( } // Update the form data observable - form.__updateFormDataAt(path, newValue); + __updateFormDataAt(path, newValue); - // Validate field(s) and set form.isValid flag - await form.__validateFields(fieldsToValidateOnChange); + // Validate field(s) and update form.isValid state + await __validateFields(fieldsToValidateOnChange ?? [path]); if (isUnmounted.current) { return; @@ -142,9 +151,18 @@ export const useField = ( setIsChangingValue(false); } } - }; + }, [ + serializeOutput, + valueChangeListener, + errorDisplayDelay, + path, + value, + fieldsToValidateOnChange, + __updateFormDataAt, + __validateFields, + ]); - const cancelInflightValidation = () => { + const cancelInflightValidation = useCallback(() => { // Cancel any inflight validation (like an HTTP Request) if ( inflightValidation.current && @@ -153,209 +171,232 @@ export const useField = ( (inflightValidation.current as any).cancel(); inflightValidation.current = null; } - }; + }, []); - const runValidations = ({ - formData, - value: valueToValidate, - validationTypeToValidate, - }: { - formData: any; - value: unknown; - validationTypeToValidate?: string; - }): ValidationError[] | Promise => { - // 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]); - - cancelInflightValidation(); - - const runAsync = async () => { - const validationErrors: ValidationError[] = []; - - for (const validation of validations) { - inflightValidation.current = null; - - const { - validator, - exitOnFail = true, - type: validationType = VALIDATION_TYPES.FIELD, - } = validation; - - if ( - typeof validationTypeToValidate !== 'undefined' && - validationType !== validationTypeToValidate - ) { - continue; - } - - inflightValidation.current = validator({ - value: (valueToValidate as unknown) as string, - errors: validationErrors, - form, - formData, - path, - }) as Promise; - - const validationResult = await inflightValidation.current; - - if (!validationResult) { - continue; - } - - validationErrors.push({ - ...validationResult, - validationType: validationType || VALIDATION_TYPES.FIELD, - }); + const clearErrors: FieldHook['clearErrors'] = useCallback( + (validationType = VALIDATION_TYPES.FIELD) => { + setErrors((previousErrors) => filterErrors(previousErrors, validationType)); + }, + [] + ); - if (exitOnFail) { - break; - } + const runValidations = useCallback( + ({ + formData, + value: valueToValidate, + validationTypeToValidate, + }: { + formData: any; + value: unknown; + validationTypeToValidate?: string; + }): ValidationError[] | Promise => { + if (!validations) { + return []; } - return validationErrors; - }; - - const runSync = () => { - const validationErrors: ValidationError[] = []; - // Sequentially execute all the validations for the field - for (const validation of validations) { - const { - validator, - exitOnFail = true, - type: validationType = VALIDATION_TYPES.FIELD, - } = validation; - - if ( - typeof validationTypeToValidate !== 'undefined' && - validationType !== validationTypeToValidate - ) { - continue; - } - - const validationResult = validator({ - value: (valueToValidate as unknown) as string, - errors: validationErrors, - form, - formData, - path, - }); - - if (!validationResult) { - continue; + // 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]); + + cancelInflightValidation(); + + const runAsync = async () => { + const validationErrors: ValidationError[] = []; + + for (const validation of validations) { + inflightValidation.current = null; + + const { + validator, + exitOnFail = true, + type: validationType = VALIDATION_TYPES.FIELD, + } = validation; + + if ( + typeof validationTypeToValidate !== 'undefined' && + validationType !== validationTypeToValidate + ) { + continue; + } + + inflightValidation.current = validator({ + value: (valueToValidate as unknown) as string, + errors: validationErrors, + form, + formData, + path, + }) as Promise; + + const validationResult = await inflightValidation.current; + + if (!validationResult) { + continue; + } + + validationErrors.push({ + ...validationResult, + validationType: validationType || VALIDATION_TYPES.FIELD, + }); + + if (exitOnFail) { + break; + } } - if (!!validationResult.then) { - // The validator returned a Promise: abort and run the validations asynchronously - // We keep a reference to the onflith promise so we can cancel it. - - inflightValidation.current = validationResult as Promise; - cancelInflightValidation(); - - return runAsync(); - } - - validationErrors.push({ - ...(validationResult as ValidationError), - validationType: validationType || VALIDATION_TYPES.FIELD, - }); + return validationErrors; + }; - if (exitOnFail) { - break; + const runSync = () => { + const validationErrors: ValidationError[] = []; + // Sequentially execute all the validations for the field + for (const validation of validations) { + const { + validator, + exitOnFail = true, + type: validationType = VALIDATION_TYPES.FIELD, + } = validation; + + if ( + typeof validationTypeToValidate !== 'undefined' && + validationType !== validationTypeToValidate + ) { + continue; + } + + const validationResult = validator({ + value: (valueToValidate as unknown) as string, + errors: validationErrors, + form, + formData, + path, + }); + + if (!validationResult) { + continue; + } + + if (!!validationResult.then) { + // The validator returned a Promise: abort and run the validations asynchronously + // We keep a reference to the onflith promise so we can cancel it. + + inflightValidation.current = validationResult as Promise; + cancelInflightValidation(); + + return runAsync(); + } + + validationErrors.push({ + ...(validationResult as ValidationError), + validationType: validationType || VALIDATION_TYPES.FIELD, + }); + + if (exitOnFail) { + break; + } } - } - return validationErrors; - }; + return validationErrors; + }; - // We first try to run the validations synchronously - return runSync(); - }; + // We first try to run the validations synchronously + return runSync(); + }, + [clearErrors, cancelInflightValidation, validations, form, path] + ); // -- API // ---------------------------------- - const clearErrors: FieldHook['clearErrors'] = (validationType = VALIDATION_TYPES.FIELD) => { - setErrors((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'] = (validationData = {}) => { - const { - formData = form.getFormData({ unflatten: false }), - value: valueToValidate = value, - validationType, - } = validationData; - - setIsValidated(true); - setValidating(true); - - // By the time our validate function has reached completion, it’s possible - // that validate() will have been called again. If this is the case, we need - // to ignore the results of this invocation and only use the results of - // the most recent invocation to update the error state for a field - const validateIteration = ++validateCounter.current; - - const onValidationErrors = (_validationErrors: ValidationError[]): FieldValidateResponse => { - if (validateIteration === validateCounter.current) { - // This is the most recent invocation - setValidating(false); - // Update the errors array - const filteredErrors = filterErrors(errors, validationType); - setErrors([...filteredErrors, ..._validationErrors]); - } + const validate: FieldHook['validate'] = useCallback( + (validationData = {}) => { + const { + formData = getFormData({ unflatten: false }), + value: valueToValidate = value, + validationType, + } = validationData; + + setIsValidated(true); + setValidating(true); + + // By the time our validate function has reached completion, it’s possible + // that validate() will have been called again. If this is the case, we need + // to ignore the results of this invocation and only use the results of + // the most recent invocation to update the error state for a field + const validateIteration = ++validateCounter.current; + + const onValidationErrors = (_validationErrors: ValidationError[]): FieldValidateResponse => { + if (validateIteration === validateCounter.current) { + // This is the most recent invocation + setValidating(false); + // Update the errors array + setErrors((prev) => { + const filteredErrors = filterErrors(prev, validationType); + return [...filteredErrors, ..._validationErrors]; + }); + } - return { - isValid: _validationErrors.length === 0, - errors: _validationErrors, + return { + isValid: _validationErrors.length === 0, + errors: _validationErrors, + }; }; - }; - const validationErrors = runValidations({ - formData, - value: valueToValidate, - validationTypeToValidate: validationType, - }); + const validationErrors = runValidations({ + formData, + value: valueToValidate, + validationTypeToValidate: validationType, + }); - if (Reflect.has(validationErrors, 'then')) { - return (validationErrors as Promise).then(onValidationErrors); - } - return onValidationErrors(validationErrors as ValidationError[]); - }; + if (Reflect.has(validationErrors, 'then')) { + return (validationErrors as Promise).then(onValidationErrors); + } + return onValidationErrors(validationErrors as ValidationError[]); + }, + [getFormData, value, runValidations] + ); /** * Handler to change the field value * * @param newValue The new value to assign to the field */ - const setValue: FieldHook['setValue'] = (newValue) => { - if (isPristine) { - setPristine(false); - } + const setValue: FieldHook['setValue'] = useCallback( + (newValue) => { + if (isPristine) { + setPristine(false); + } - const formattedValue = formatInputValue(newValue); - setStateValue(formattedValue); - }; + const formattedValue = formatInputValue(newValue); + setStateValue(formattedValue); + return formattedValue; + }, + [formatInputValue, isPristine] + ); - const _setErrors: FieldHook['setErrors'] = (_errors) => { + const _setErrors: FieldHook['setErrors'] = useCallback((_errors) => { setErrors(_errors.map((error) => ({ validationType: VALIDATION_TYPES.FIELD, ...error }))); - }; + }, []); /** * Form "onChange" event handler * * @param event Form input change event */ - const onChange: FieldHook['onChange'] = (event) => { - const newValue = {}.hasOwnProperty.call(event!.target, 'checked') - ? event.target.checked - : event.target.value; + const onChange: FieldHook['onChange'] = useCallback( + (event) => { + const newValue = {}.hasOwnProperty.call(event!.target, 'checked') + ? event.target.checked + : event.target.value; - setValue((newValue as unknown) as T); - }; + setValue((newValue as unknown) as T); + }, + [setValue] + ); /** * As we can have multiple validation types (FIELD, ASYNC, ARRAY_ITEM), this @@ -367,48 +408,50 @@ export const useField = ( * * @param validationType The validation type to return error messages from */ - const getErrorsMessages: FieldHook['getErrorsMessages'] = (args = {}) => { - const { errorCode, validationType = VALIDATION_TYPES.FIELD } = args; - const errorMessages = errors.reduce((messages, error) => { - const isSameErrorCode = errorCode && error.code === errorCode; - const isSamevalidationType = - error.validationType === validationType || - (validationType === VALIDATION_TYPES.FIELD && - !{}.hasOwnProperty.call(error, 'validationType')); - - if (isSameErrorCode || (typeof errorCode === 'undefined' && isSamevalidationType)) { - return messages ? `${messages}, ${error.message}` : (error.message as string); + const getErrorsMessages: FieldHook['getErrorsMessages'] = useCallback( + (args = {}) => { + const { errorCode, validationType = VALIDATION_TYPES.FIELD } = args; + const errorMessages = errors.reduce((messages, error) => { + const isSameErrorCode = errorCode && error.code === errorCode; + const isSamevalidationType = + error.validationType === validationType || + (validationType === VALIDATION_TYPES.FIELD && + !{}.hasOwnProperty.call(error, 'validationType')); + + if (isSameErrorCode || (typeof errorCode === 'undefined' && isSamevalidationType)) { + return messages ? `${messages}, ${error.message}` : (error.message as string); + } + return messages; + }, ''); + + return errorMessages ? errorMessages : null; + }, + [errors] + ); + + const reset: FieldHook['reset'] = useCallback( + (resetOptions = { resetValue: true }) => { + const { resetValue = true } = resetOptions; + + setPristine(true); + setValidating(false); + setIsChangingValue(false); + setIsValidated(false); + setErrors([]); + + if (resetValue) { + setValue(initialValue); + /** + * Having to call serializeOutput() is a current bug of the lib and will be fixed + * in a future PR. The serializer function should only be called when outputting + * the form data. If we need to continuously format the data while it changes, + * we need to use the field `formatter` config. + */ + return serializeOutput(initialValue); } - return messages; - }, ''); - - return errorMessages ? errorMessages : null; - }; - - const reset: FieldHook['reset'] = (resetOptions = { resetValue: true }) => { - const { resetValue = true } = resetOptions; - - setPristine(true); - setValidating(false); - setIsChangingValue(false); - setIsValidated(false); - setErrors([]); - - if (resetValue) { - setValue(initialValue); - /** - * Having to call serializeOutput() is a current bug of the lib and will be fixed - * in a future PR. The serializer function should only be called when outputting - * the form data. If we need to continuously format the data while it changes, - * we need to use the field `formatter` config. - */ - return serializeOutput(initialValue); - } - return value; - }; - - const serializeOutput: FieldHook['__serializeOutput'] = (rawValue = value) => - serializer(rawValue); + }, + [setValue, serializeOutput, initialValue] + ); // -- EFFECTS // ---------------------------------- @@ -425,54 +468,64 @@ export const useField = ( clearTimeout(debounceTimeout.current); } }; - }, [value]); - - const field: FieldHook = { + }, [isPristine, onValueChange]); + + const field: FieldHook = useMemo(() => { + return { + path, + type, + label, + labelAppend, + helpText, + value, + errors, + form, + isPristine, + isValid: errors.length === 0, + isValidating, + isValidated, + isChangingValue, + onChange, + getErrorsMessages, + setValue, + setErrors: _setErrors, + clearErrors, + validate, + reset, + __serializeOutput: serializeOutput, + }; + }, [ path, type, label, labelAppend, helpText, value, - errors, form, isPristine, - isValid: errors.length === 0, + errors, isValidating, isValidated, isChangingValue, onChange, getErrorsMessages, setValue, - setErrors: _setErrors, + _setErrors, clearErrors, validate, reset, - __serializeOutput: serializeOutput, - }; + serializeOutput, + ]); - form.__addField(field as FieldHook); // Executed first (1) + form.__addField(field as FieldHook); useEffect(() => { - /** - * NOTE: effect cleanup actually happens *after* the new component has been mounted, - * but before the next effect callback is run. - * Ref: https://kentcdodds.com/blog/understanding-reacts-key-prop - * - * This means that, the "form.__addField(field)" outside the effect will be called *before* - * the cleanup `form.__removeField(path);` creating a race condition. - * - * TODO: See how we could refactor "use_field" & "use_form" to avoid having the - * `form.__addField(field)` call outside the effect. - */ - form.__addField(field as FieldHook); // Executed third (3) - return () => { // Remove field from the form when it is unmounted or if its path changes. isUnmounted.current = true; - form.__removeField(path); // Executed second (2) + __removeField(path); }; - }, [path]); + }, [path, __removeField]); return field; }; 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 f332d2e6ea604..216c7974a9679 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 @@ -135,12 +135,13 @@ describe('use_form() 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 { subscribe } = form; useEffect(() => { // Any time the form value changes, forward the data to the consumer - const subscription = form.subscribe(onData); + const subscription = subscribe(onData); return subscription.unsubscribe; - }, [form]); + }, [subscribe, onData]); return ( @@ -200,8 +201,9 @@ describe('use_form() hook', () => { const TestComp = ({ onData }: { onData: OnUpdateHandler }) => { const { form } = useForm({ defaultValue }); + const { subscribe } = form; - useEffect(() => form.subscribe(onData).unsubscribe, [form]); + useEffect(() => subscribe(onData).unsubscribe, [subscribe, onData]); 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 f9286d99cbf80..46b8958491e56 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 @@ -17,7 +17,7 @@ * under the License. */ -import { useState, useRef, useEffect, useMemo } from 'react'; +import { useState, useRef, useEffect, useMemo, useCallback } from 'react'; import { get } from 'lodash'; import { FormHook, FieldHook, FormData, FieldConfig, FieldsMap, FormConfig } from '../types'; @@ -34,28 +34,34 @@ interface UseFormReturn { } export function useForm( - formConfig: FormConfig | undefined = {} + formConfig?: FormConfig ): UseFormReturn { - const { - onSubmit, - schema, - serializer = (data: T): T => data, - deserializer = (data: T): T => data, - options = {}, - id = 'default', - } = formConfig; - - const formDefaultValue = - formConfig.defaultValue === undefined || Object.keys(formConfig.defaultValue).length === 0 - ? {} - : Object.entries(formConfig.defaultValue as object) - .filter(({ 1: value }) => value !== undefined) - .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}); - - const formOptions = { ...DEFAULT_OPTIONS, ...options }; - const defaultValueDeserialized = useMemo(() => deserializer(formDefaultValue), [ - formConfig.defaultValue, - ]); + const { onSubmit, schema, serializer, deserializer, options, id = 'default', defaultValue } = + formConfig ?? {}; + + const formDefaultValue = useMemo(() => { + if (defaultValue === undefined || Object.keys(defaultValue).length === 0) { + return {}; + } + + return Object.entries(defaultValue as object) + .filter(({ 1: value }) => value !== undefined) + .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}); + }, [defaultValue]); + + const { errorDisplayDelay, stripEmptyFields: doStripEmptyFields } = options ?? {}; + const formOptions = useMemo( + () => ({ + stripEmptyFields: doStripEmptyFields ?? DEFAULT_OPTIONS.stripEmptyFields, + errorDisplayDelay: errorDisplayDelay ?? DEFAULT_OPTIONS.errorDisplayDelay, + }), + [errorDisplayDelay, doStripEmptyFields] + ); + + const defaultValueDeserialized = useMemo( + () => (deserializer ? deserializer(formDefaultValue) : formDefaultValue), + [formDefaultValue, deserializer] + ); const [isSubmitted, setIsSubmitted] = useState(false); const [isSubmitting, setSubmitting] = useState(false); @@ -81,55 +87,68 @@ export function useForm( // -- HELPERS // ---------------------------------- - const getFormData$ = (): Subject => { + const getFormData$ = useCallback((): Subject => { if (formData$.current === null) { formData$.current = new Subject({} as T); } return formData$.current; - }; - const fieldsToArray = () => Object.values(fieldsRefs.current); + }, []); - const stripEmptyFields = (fields: FieldsMap): FieldsMap => { - if (formOptions.stripEmptyFields) { - return Object.entries(fields).reduce((acc, [key, field]) => { - if (typeof field.value !== 'string' || field.value.trim() !== '') { - acc[key] = field; - } - return acc; - }, {} as FieldsMap); - } - return fields; - }; + const fieldsToArray = useCallback(() => Object.values(fieldsRefs.current), []); + + const stripEmptyFields = useCallback( + (fields: FieldsMap): FieldsMap => { + if (formOptions.stripEmptyFields) { + return Object.entries(fields).reduce((acc, [key, field]) => { + if (typeof field.value !== 'string' || field.value.trim() !== '') { + acc[key] = field; + } + return acc; + }, {} as FieldsMap); + } + return fields; + }, + [formOptions] + ); + + const updateFormDataAt: FormHook['__updateFormDataAt'] = useCallback( + (path, value) => { + const _formData$ = getFormData$(); + const currentFormData = _formData$.value; + + if (currentFormData[path] !== value) { + _formData$.next({ ...currentFormData, [path]: value }); + } - const updateFormDataAt: FormHook['__updateFormDataAt'] = (path, value) => { - const _formData$ = getFormData$(); - const currentFormData = _formData$.value; - const nextValue = { ...currentFormData, [path]: value }; - _formData$.next(nextValue); - return _formData$.value; - }; + return _formData$.value; + }, + [getFormData$] + ); // -- API // ---------------------------------- - const getFormData: FormHook['getFormData'] = ( - getDataOptions: Parameters['getFormData']>[0] = { unflatten: true } - ) => { - if (getDataOptions.unflatten) { - const nonEmptyFields = stripEmptyFields(fieldsRefs.current); - const fieldsValue = mapFormFields(nonEmptyFields, (field) => field.__serializeOutput()); - return serializer(unflattenObject(fieldsValue)) as T; - } - - return Object.entries(fieldsRefs.current).reduce( - (acc, [key, field]) => ({ - ...acc, - [key]: field.__serializeOutput(), - }), - {} as T - ); - }; + const getFormData: FormHook['getFormData'] = useCallback( + (getDataOptions: Parameters['getFormData']>[0] = { unflatten: true }) => { + if (getDataOptions.unflatten) { + const nonEmptyFields = stripEmptyFields(fieldsRefs.current); + const fieldsValue = mapFormFields(nonEmptyFields, (field) => field.__serializeOutput()); + return serializer + ? (serializer(unflattenObject(fieldsValue)) as T) + : (unflattenObject(fieldsValue) as T); + } - const getErrors: FormHook['getErrors'] = () => { + return Object.entries(fieldsRefs.current).reduce( + (acc, [key, field]) => ({ + ...acc, + [key]: field.__serializeOutput(), + }), + {} as T + ); + }, + [stripEmptyFields, serializer] + ); + + const getErrors: FormHook['getErrors'] = useCallback(() => { if (isValid === true) { return []; } @@ -141,11 +160,15 @@ export function useForm( } return [...acc, fieldError]; }, [] as string[]); - }; + }, [isValid, fieldsToArray]); const isFieldValid = (field: FieldHook) => field.isValid && !field.isValidating; - const updateFormValidity = () => { + const updateFormValidity = useCallback(() => { + if (isUnmounted.current) { + return; + } + const fieldsArray = fieldsToArray(); const areAllFieldsValidated = fieldsArray.every((field) => field.isValidated); @@ -158,176 +181,220 @@ export function useForm( setIsValid(isFormValid); return isFormValid; - }; + }, [fieldsToArray]); - const validateFields: FormHook['__validateFields'] = async (fieldNames) => { - const fieldsToValidate = fieldNames - .map((name) => fieldsRefs.current[name]) - .filter((field) => field !== undefined); + const validateFields: FormHook['__validateFields'] = useCallback( + async (fieldNames) => { + const fieldsToValidate = fieldNames + .map((name) => fieldsRefs.current[name]) + .filter((field) => field !== undefined); - if (fieldsToValidate.length === 0) { - // Nothing to validate - return { areFieldsValid: true, isFormValid: true }; - } + if (fieldsToValidate.length === 0) { + // Nothing to validate + return { areFieldsValid: true, isFormValid: true }; + } - const formData = getFormData({ unflatten: false }); - await Promise.all(fieldsToValidate.map((field) => field.validate({ formData }))); + const formData = getFormData({ unflatten: false }); + await Promise.all(fieldsToValidate.map((field) => field.validate({ formData }))); - const isFormValid = updateFormValidity(); - const areFieldsValid = fieldsToValidate.every(isFieldValid); + const isFormValid = updateFormValidity(); + const areFieldsValid = fieldsToValidate.every(isFieldValid); - return { areFieldsValid, isFormValid }; - }; + return { areFieldsValid, isFormValid }; + }, + [getFormData, updateFormValidity] + ); - const validateAllFields = async (): Promise => { + const validateAllFields = useCallback(async (): Promise => { const fieldsArray = fieldsToArray(); const fieldsToValidate = fieldsArray.filter((field) => !field.isValidated); - let isFormValid: boolean | undefined = isValid; + let isFormValid: boolean | undefined; if (fieldsToValidate.length === 0) { - if (isFormValid === undefined) { - // We should never enter this condition as the form validity is updated each time - // a field is validated. But sometimes, during tests it does not happen and we need - // to wait the next tick (hooks lifecycle being tricky) to make sure the "isValid" state is updated. - // In order to avoid this unintentional behaviour, we add this if condition here. - isFormValid = fieldsArray.every(isFieldValid); - setIsValid(isFormValid); - } + // We should never enter this condition as the form validity is updated each time + // a field is validated. But sometimes, during tests or race conditions it does not happen and we need + // to wait the next tick (hooks lifecycle being tricky) to make sure the "isValid" state is updated. + // In order to avoid this unintentional behaviour, we add this if condition here. + + // TODO: Fix this when adding tests to the form lib. + isFormValid = fieldsArray.every(isFieldValid); + setIsValid(isFormValid); return isFormValid; } ({ isFormValid } = await validateFields(fieldsToValidate.map((field) => field.path))); return isFormValid!; - }; + }, [fieldsToArray, validateFields]); - const addField: FormHook['__addField'] = (field) => { - fieldsRefs.current[field.path] = field; + const addField: FormHook['__addField'] = useCallback( + (field) => { + fieldsRefs.current[field.path] = field; - if (!{}.hasOwnProperty.call(getFormData$().value, field.path)) { - const fieldValue = field.__serializeOutput(); - updateFormDataAt(field.path, fieldValue); - } - }; + if (!{}.hasOwnProperty.call(getFormData$().value, field.path)) { + const fieldValue = field.__serializeOutput(); + updateFormDataAt(field.path, fieldValue); + } + }, + [getFormData$, updateFormDataAt] + ); - const removeField: FormHook['__removeField'] = (_fieldNames) => { - const fieldNames = Array.isArray(_fieldNames) ? _fieldNames : [_fieldNames]; - const currentFormData = { ...getFormData$().value } as FormData; + const removeField: FormHook['__removeField'] = useCallback( + (_fieldNames) => { + const fieldNames = Array.isArray(_fieldNames) ? _fieldNames : [_fieldNames]; + const currentFormData = { ...getFormData$().value } as FormData; - fieldNames.forEach((name) => { - delete fieldsRefs.current[name]; - delete currentFormData[name]; - }); + fieldNames.forEach((name) => { + delete fieldsRefs.current[name]; + delete currentFormData[name]; + }); - getFormData$().next(currentFormData as T); + getFormData$().next(currentFormData as T); - /** - * After removing a field, the form validity might have changed - * (an invalid field might have been removed and now the form is valid) - */ - updateFormValidity(); - }; + /** + * After removing a field, the form validity might have changed + * (an invalid field might have been removed and now the form is valid) + */ + updateFormValidity(); + }, + [getFormData$, updateFormValidity] + ); - const setFieldValue: FormHook['setFieldValue'] = (fieldName, value) => { + const setFieldValue: FormHook['setFieldValue'] = useCallback((fieldName, value) => { if (fieldsRefs.current[fieldName] === undefined) { return; } fieldsRefs.current[fieldName].setValue(value); - }; + }, []); - const setFieldErrors: FormHook['setFieldErrors'] = (fieldName, errors) => { + const setFieldErrors: FormHook['setFieldErrors'] = useCallback((fieldName, errors) => { if (fieldsRefs.current[fieldName] === undefined) { return; } fieldsRefs.current[fieldName].setErrors(errors); - }; + }, []); - const getFields: FormHook['getFields'] = () => fieldsRefs.current; + const getFields: FormHook['getFields'] = useCallback(() => fieldsRefs.current, []); - const getFieldDefaultValue: FormHook['getFieldDefaultValue'] = (fieldName) => - get(defaultValueDeserialized, fieldName); + const getFieldDefaultValue: FormHook['getFieldDefaultValue'] = useCallback( + (fieldName) => get(defaultValueDeserialized, fieldName), + [defaultValueDeserialized] + ); - const readFieldConfigFromSchema: FormHook['__readFieldConfigFromSchema'] = (fieldName) => { - const config = (get(schema ? schema : {}, fieldName) as FieldConfig) || {}; + const readFieldConfigFromSchema: FormHook['__readFieldConfigFromSchema'] = useCallback( + (fieldName) => { + const config = (get(schema ?? {}, fieldName) as FieldConfig) || {}; - return config; - }; + return config; + }, + [schema] + ); - const submitForm: FormHook['submit'] = async (e) => { - if (e) { - e.preventDefault(); - } + const submitForm: FormHook['submit'] = useCallback( + async (e) => { + if (e) { + e.preventDefault(); + } - if (!isSubmitted) { setIsSubmitted(true); // User has attempted to submit the form at least once - } - setSubmitting(true); + setSubmitting(true); - const isFormValid = await validateAllFields(); - const formData = getFormData(); - - if (onSubmit) { - await onSubmit(formData, isFormValid!); - } - - if (isUnmounted.current === false) { - setSubmitting(false); - } + const isFormValid = await validateAllFields(); + const formData = getFormData(); - return { data: formData, isValid: isFormValid! }; - }; + if (onSubmit) { + await onSubmit(formData, isFormValid!); + } - const subscribe: FormHook['subscribe'] = (handler) => { - const subscription = getFormData$().subscribe((raw) => { - if (!isUnmounted.current) { - handler({ isValid, data: { raw, format: getFormData }, validate: validateAllFields }); + if (isUnmounted.current === false) { + setSubmitting(false); } - }); - formUpdateSubscribers.current.push(subscription); + return { data: formData, isValid: isFormValid! }; + }, + [validateAllFields, getFormData, onSubmit] + ); - return { - unsubscribe() { - formUpdateSubscribers.current = formUpdateSubscribers.current.filter( - (sub) => sub !== subscription - ); - return subscription.unsubscribe(); - }, - }; - }; + const subscribe: FormHook['subscribe'] = useCallback( + (handler) => { + const subscription = getFormData$().subscribe((raw) => { + if (!isUnmounted.current) { + handler({ isValid, data: { raw, format: getFormData }, validate: validateAllFields }); + } + }); + + formUpdateSubscribers.current.push(subscription); + + return { + unsubscribe() { + formUpdateSubscribers.current = formUpdateSubscribers.current.filter( + (sub) => sub !== subscription + ); + return subscription.unsubscribe(); + }, + }; + }, + [getFormData$, isValid, getFormData, validateAllFields] + ); /** * Reset all the fields of the form to their default values * and reset all the states to their original value. */ - const reset: FormHook['reset'] = (resetOptions = { resetValues: true }) => { - const { resetValues = true } = resetOptions; - const currentFormData = { ...getFormData$().value } as FormData; - Object.entries(fieldsRefs.current).forEach(([path, field]) => { - // By resetting the form, some field might be unmounted. In order - // to avoid a race condition, we check that the field still exists. - const isFieldMounted = fieldsRefs.current[path] !== undefined; - if (isFieldMounted) { - const fieldValue = field.reset({ resetValue: resetValues }); - currentFormData[path] = fieldValue; + const reset: FormHook['reset'] = useCallback( + (resetOptions = { resetValues: true }) => { + const { resetValues = true } = resetOptions; + const currentFormData = { ...getFormData$().value } as FormData; + Object.entries(fieldsRefs.current).forEach(([path, field]) => { + // By resetting the form, some field might be unmounted. In order + // to avoid a race condition, we check that the field still exists. + const isFieldMounted = fieldsRefs.current[path] !== undefined; + if (isFieldMounted) { + const fieldValue = field.reset({ resetValue: resetValues }) ?? currentFormData[path]; + currentFormData[path] = fieldValue; + } + }); + if (resetValues) { + getFormData$().next(currentFormData as T); } - }); - if (resetValues) { - getFormData$().next(currentFormData as T); - } - setIsSubmitted(false); - setSubmitting(false); - setIsValid(undefined); - }; + setIsSubmitted(false); + setSubmitting(false); + setIsValid(undefined); + }, + [getFormData$] + ); - const form: FormHook = { + const form = useMemo>(() => { + return { + isSubmitted, + isSubmitting, + isValid, + id, + submit: submitForm, + subscribe, + setFieldValue, + setFieldErrors, + getFields, + getFormData, + getErrors, + getFieldDefaultValue, + reset, + __options: formOptions, + __getFormData$: getFormData$, + __updateFormDataAt: updateFormDataAt, + __readFieldConfigFromSchema: readFieldConfigFromSchema, + __addField: addField, + __removeField: removeField, + __validateFields: validateFields, + }; + }, [ isSubmitted, isSubmitting, isValid, id, - submit: submitForm, + submitForm, subscribe, setFieldValue, setFieldErrors, @@ -336,14 +403,14 @@ export function useForm( getErrors, getFieldDefaultValue, reset, - __options: formOptions, - __getFormData$: getFormData$, - __updateFormDataAt: updateFormDataAt, - __readFieldConfigFromSchema: readFieldConfigFromSchema, - __addField: addField, - __removeField: removeField, - __validateFields: validateFields, - }; + formOptions, + getFormData$, + updateFormDataAt, + readFieldConfigFromSchema, + addField, + removeField, + validateFields, + ]); return { form, 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 f11b61edaddf4..7e38a33f0c684 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 @@ -107,7 +107,7 @@ export interface FieldHook { errorCode?: string; }) => string | null; onChange: (event: ChangeEvent<{ name?: string; value: string; checked?: boolean }>) => void; - setValue: (value: T) => void; + setValue: (value: T) => T; setErrors: (errors: ValidationError[]) => void; clearErrors: (type?: string | string[]) => void; validate: (validateData?: { @@ -115,7 +115,7 @@ export interface FieldHook { value?: unknown; validationType?: string; }) => FieldValidateResponse | Promise; - reset: (options?: { resetValue: boolean }) => unknown; + reset: (options?: { resetValue: boolean }) => unknown | undefined; __serializeOutput: (rawValue?: unknown) => unknown; } diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_logistics.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_logistics.tsx index c48a23226a371..032eb93f7f9f9 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_logistics.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_logistics.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 React, { useEffect, useState } from 'react'; +import React, { useEffect, useState, useCallback } from 'react'; import { EuiFlexGroup, EuiFlexItem, @@ -44,26 +44,28 @@ export const StepLogistics: React.FunctionComponent = React.memo( options: { stripEmptyFields: false }, }); + const { isValid: isFormValid, submit, getFormData, subscribe } = form; + const { documentation } = useComponentTemplatesContext(); const [isMetaVisible, setIsMetaVisible] = useState( Boolean(defaultValue._meta && Object.keys(defaultValue._meta).length) ); - const validate = async () => { - return (await form.submit()).isValid; - }; + const validate = useCallback(async () => { + return (await submit()).isValid; + }, [submit]); useEffect(() => { onChange({ - isValid: form.isValid, + isValid: isFormValid, validate, - getData: form.getFormData, + getData: getFormData, }); - }, [form.isValid, onChange]); // eslint-disable-line react-hooks/exhaustive-deps + }, [isFormValid, getFormData, validate, onChange]); useEffect(() => { - const subscription = form.subscribe(({ data, isValid }) => { + const subscription = subscribe(({ data, isValid }) => { onChange({ isValid, validate, @@ -71,7 +73,7 @@ export const StepLogistics: React.FunctionComponent = React.memo( }); }); return subscription.unsubscribe; - }, [onChange]); // eslint-disable-line react-hooks/exhaustive-deps + }, [subscribe, validate, onChange]); return ( diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/configuration_form.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/configuration_form.tsx index 098e530bddb3c..86bcc796a88eb 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/configuration_form.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/configuration_form.tsx @@ -94,22 +94,23 @@ export const ConfigurationForm = React.memo(({ value }: Props) => { id: 'configurationForm', }); const dispatch = useDispatch(); + const { subscribe, submit, reset, getFormData } = form; useEffect(() => { - const subscription = form.subscribe(({ data, isValid, validate }) => { + const subscription = subscribe(({ data, isValid, validate }) => { dispatch({ type: 'configuration.update', value: { data, isValid, validate, - submitForm: form.submit, + submitForm: submit, }, }); }); return subscription.unsubscribe; - }, [dispatch]); // eslint-disable-line react-hooks/exhaustive-deps + }, [dispatch, subscribe, submit]); useEffect(() => { if (isMounted.current === undefined) { @@ -125,18 +126,18 @@ export const ConfigurationForm = React.memo(({ value }: Props) => { // If the value has changed (it probably means that we have loaded a new JSON) // we need to reset the form to update the fields values. - form.reset({ resetValues: true }); - }, [value]); // eslint-disable-line react-hooks/exhaustive-deps + reset({ resetValues: true }); + }, [value, reset]); useEffect(() => { return () => { isMounted.current = false; // Save a snapshot of the form state so we can get back to it when navigating back to the tab - const configurationData = form.getFormData(); + const configurationData = getFormData(); dispatch({ type: 'configuration.save', value: configurationData }); }; - }, [dispatch]); // eslint-disable-line react-hooks/exhaustive-deps + }, [getFormData, dispatch]); return ( { - const subscription = form.subscribe((updatedFieldForm) => { + const subscription = subscribe((updatedFieldForm) => { dispatch({ type: 'fieldForm.update', value: updatedFieldForm }); }); return subscription.unsubscribe; - }, [dispatch]); // eslint-disable-line react-hooks/exhaustive-deps + }, [dispatch, subscribe]); const cancel = () => { dispatch({ type: 'documentField.changeStatus', value: 'idle' }); diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field_container.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field_container.tsx index d543e49d23be9..5105a2a157a6d 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field_container.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field_container.tsx @@ -26,13 +26,15 @@ export const EditFieldContainer = React.memo(({ field, allFields }: Props) => { options: { stripEmptyFields: false }, }); + const { subscribe } = form; + useEffect(() => { - const subscription = form.subscribe((updatedFieldForm) => { + const subscription = subscribe((updatedFieldForm) => { dispatch({ type: 'fieldForm.update', value: updatedFieldForm }); }); return subscription.unsubscribe; - }, [dispatch]); // eslint-disable-line react-hooks/exhaustive-deps + }, [subscribe, dispatch]); const exitEdit = useCallback(() => { dispatch({ type: 'documentField.changeStatus', value: 'idle' }); diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/templates_form/templates_form.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/templates_form/templates_form.tsx index 79685d46b6bdd..a95579a8a141e 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/templates_form/templates_form.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/templates_form/templates_form.tsx @@ -61,17 +61,18 @@ export const TemplatesForm = React.memo(({ value }: Props) => { deserializer: formDeserializer, defaultValue: value, }); + const { subscribe, getFormData, submit: submitForm, reset } = form; const dispatch = useDispatch(); useEffect(() => { - const subscription = form.subscribe(({ data, isValid, validate }) => { + const subscription = subscribe(({ data, isValid, validate }) => { dispatch({ type: 'templates.update', - value: { data, isValid, validate, submitForm: form.submit }, + value: { data, isValid, validate, submitForm }, }); }); return subscription.unsubscribe; - }, [dispatch]); // eslint-disable-line react-hooks/exhaustive-deps + }, [subscribe, dispatch, submitForm]); useEffect(() => { if (isMounted.current === undefined) { @@ -87,18 +88,18 @@ export const TemplatesForm = React.memo(({ value }: Props) => { // If the value has changed (it probably means that we have loaded a new JSON) // we need to reset the form to update the fields values. - form.reset({ resetValues: true }); - }, [value]); // eslint-disable-line react-hooks/exhaustive-deps + reset({ resetValues: true }); + }, [value, reset]); useEffect(() => { return () => { isMounted.current = false; // On unmount => save in the state a snapshot of the current form data. - const dynamicTemplatesData = form.getFormData(); + const dynamicTemplatesData = getFormData(); dispatch({ type: 'templates.save', value: dynamicTemplatesData }); }; - }, [dispatch]); // eslint-disable-line react-hooks/exhaustive-deps + }, [getFormData, dispatch]); return (
diff --git a/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_mappings_container.tsx b/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_mappings_container.tsx index 38c4a85bbe0ff..b0675c1412259 100644 --- a/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_mappings_container.tsx +++ b/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_mappings_container.tsx @@ -14,15 +14,16 @@ interface Props { } export const StepMappingsContainer: React.FunctionComponent = ({ esDocsBase }) => { - const { defaultValue, updateContent, getData } = Forms.useContent( + const { defaultValue, updateContent, getSingleContentData } = Forms.useContent< + CommonWizardSteps, 'mappings' - ); + >('mappings'); return ( ); 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 980083e8e9d20..a54cf142c18b7 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 @@ -47,7 +47,7 @@ export const AddComment = React.memo( options: { stripEmptyFields: false }, schema, }); - + const { getFormData, setFieldValue, reset, submit } = form; const { handleCursorChange, handleOnTimelineChange } = useInsertTimeline( form, 'comment' @@ -55,26 +55,23 @@ export const AddComment = React.memo( useEffect(() => { if (insertQuote !== null) { - const { comment } = form.getFormData(); - form.setFieldValue( - 'comment', - `${comment}${comment.length > 0 ? '\n\n' : ''}${insertQuote}` - ); + const { comment } = getFormData(); + setFieldValue('comment', `${comment}${comment.length > 0 ? '\n\n' : ''}${insertQuote}`); } - }, [form, insertQuote]); + }, [getFormData, insertQuote, setFieldValue]); const handleTimelineClick = useTimelineClick(); const onSubmit = useCallback(async () => { - const { isValid, data } = await form.submit(); + const { isValid, data } = await submit(); if (isValid) { if (onCommentSaving != null) { onCommentSaving(); } postComment(data, onCommentPosted); - form.reset(); + reset(); } - }, [form, onCommentPosted, onCommentSaving, postComment]); + }, [onCommentPosted, onCommentSaving, postComment, reset, submit]); return ( diff --git a/x-pack/plugins/security_solution/public/cases/components/create/index.tsx b/x-pack/plugins/security_solution/public/cases/components/create/index.tsx index 1a2697bb132b0..31e6da4269ead 100644 --- a/x-pack/plugins/security_solution/public/cases/components/create/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/create/index.tsx @@ -69,6 +69,7 @@ export const Create = React.memo(() => { options: { stripEmptyFields: false }, schema, }); + const { submit } = form; const { tags: tagOptions } = useGetTags(); const [options, setOptions] = useState( tagOptions.map((label) => ({ @@ -91,12 +92,12 @@ export const Create = React.memo(() => { const handleTimelineClick = useTimelineClick(); const onSubmit = useCallback(async () => { - const { isValid, data } = await form.submit(); + const { isValid, data } = await submit(); if (isValid) { // `postCase`'s type is incorrect, it actually returns a promise await postCase(data); } - }, [form, postCase]); + }, [submit, postCase]); const handleSetIsCancel = useCallback(() => { history.push('/'); diff --git a/x-pack/plugins/security_solution/public/cases/components/edit_connector/index.tsx b/x-pack/plugins/security_solution/public/cases/components/edit_connector/index.tsx index ba0b97b6088a8..11938a55181d3 100644 --- a/x-pack/plugins/security_solution/public/cases/components/edit_connector/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/edit_connector/index.tsx @@ -46,11 +46,13 @@ export const EditConnector = React.memo( onSubmit, selectedConnector, }: EditConnectorProps) => { + const initialState = { connectors }; const { form } = useForm({ - defaultValue: { connectors }, + defaultValue: initialState, options: { stripEmptyFields: false }, schema, }); + const { setFieldValue, submit } = form; const [connectorHasChanged, setConnectorHasChanged] = useState(false); const onChangeConnector = useCallback( (connectorId) => { @@ -60,17 +62,18 @@ export const EditConnector = React.memo( ); const onCancelConnector = useCallback(() => { - form.setFieldValue('connector', selectedConnector); + setFieldValue('connector', selectedConnector); setConnectorHasChanged(false); - }, [form, selectedConnector]); + }, [selectedConnector, setFieldValue]); const onSubmitConnector = useCallback(async () => { - const { isValid, data: newData } = await form.submit(); + const { isValid, data: newData } = await submit(); if (isValid && newData.connector) { onSubmit(newData.connector); setConnectorHasChanged(false); } - }, [form, onSubmit]); + }, [submit, onSubmit]); + return ( diff --git a/x-pack/plugins/security_solution/public/cases/components/tag_list/index.tsx b/x-pack/plugins/security_solution/public/cases/components/tag_list/index.tsx index 5f8404ca2dcc4..7bb10c743a418 100644 --- a/x-pack/plugins/security_solution/public/cases/components/tag_list/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/tag_list/index.tsx @@ -42,20 +42,23 @@ const MyFlexGroup = styled(EuiFlexGroup)` export const TagList = React.memo( ({ disabled = false, isLoading, onSubmit, tags }: TagListProps) => { + const initialState = { tags }; const { form } = useForm({ - defaultValue: { tags }, + defaultValue: initialState, options: { stripEmptyFields: false }, schema, }); + const { submit } = form; const [isEditTags, setIsEditTags] = useState(false); const onSubmitTags = useCallback(async () => { - const { isValid, data: newData } = await form.submit(); + const { isValid, data: newData } = await submit(); if (isValid && newData.tags) { onSubmit(newData.tags); setIsEditTags(false); } - }, [form, onSubmit]); + }, [onSubmit, submit]); + const { tags: tagOptions } = useGetTags(); const [options, setOptions] = useState( tagOptions.map((label) => ({ diff --git a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_markdown.tsx b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_markdown.tsx index 0a8167049266f..da081fea5eac0 100644 --- a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_markdown.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_markdown.tsx @@ -6,7 +6,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty, EuiButton } from '@elastic/eui'; import React, { useCallback } from 'react'; -import styled, { css } from 'styled-components'; +import styled from 'styled-components'; import * as i18n from '../case_view/translations'; import { Markdown } from '../../../common/components/markdown'; @@ -18,9 +18,7 @@ import { MarkdownEditorForm } from '../../../common/components//markdown_editor/ import { useTimelineClick } from '../utils/use_timeline_click'; const ContentWrapper = styled.div` - ${({ theme }) => css` - padding: ${theme.eui.euiSizeM} ${theme.eui.euiSizeL}; - `} + padding: ${({ theme }) => `${theme.eui.euiSizeM} ${theme.eui.euiSizeL}`}; `; interface UserActionMarkdownProps { @@ -37,11 +35,13 @@ export const UserActionMarkdown = ({ onChangeEditable, onSaveContent, }: UserActionMarkdownProps) => { + const initialState = { content }; const { form } = useForm({ - defaultValue: { content }, + defaultValue: initialState, options: { stripEmptyFields: false }, schema, }); + const { submit } = form; const { handleCursorChange, handleOnTimelineChange } = useInsertTimeline( form, 'content' @@ -53,45 +53,43 @@ export const UserActionMarkdown = ({ const handleTimelineClick = useTimelineClick(); const handleSaveAction = useCallback(async () => { - const { isValid, data } = await form.submit(); + const { isValid, data } = await submit(); if (isValid) { onSaveContent(data.content); } onChangeEditable(id); - }, [form, id, onChangeEditable, onSaveContent]); + }, [id, onChangeEditable, onSaveContent, submit]); const renderButtons = useCallback( - ({ cancelAction, saveAction }) => { - return ( - - - - {i18n.CANCEL} - - - - - {i18n.SAVE} - - - - ); - }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [handleCancelAction, handleSaveAction] + ({ cancelAction, saveAction }) => ( + + + + {i18n.CANCEL} + + + + + {i18n.SAVE} + + + + ), + [] ); + return isEditable ? ( = ({ setForm, setStepData, }) => { - const [myStepData, setMyStepData] = useState(stepAboutDefaultValue); + const initialState = defaultValues ?? stepAboutDefaultValue; + const [myStepData, setMyStepData] = useState(initialState); const [{ isLoading: indexPatternLoading, indexPatterns }] = useFetchIndexPatterns( defineRuleData?.index ?? [] ); const { form } = useForm({ - defaultValue: myStepData, + defaultValue: initialState, options: { stripEmptyFields: false }, schema, }); + const { getFields, submit } = form; const onSubmit = useCallback(async () => { if (setStepData) { setStepData(RuleStep.aboutRule, null, false); - const { isValid, data } = await form.submit(); + const { isValid, data } = await submit(); if (isValid) { setStepData(RuleStep.aboutRule, data, isValid); setMyStepData({ ...data, isNew: false } as AboutStepRule); } } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [form]); + }, [setStepData, submit]); useEffect(() => { - const { isNew, ...initDefaultValue } = myStepData; - if (defaultValues != null && !deepEqual(initDefaultValue, defaultValues)) { - const myDefaultValues = { - ...defaultValues, - isNew: false, - }; - setMyStepData(myDefaultValues); - setFieldValue(form, schema, myDefaultValues); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [defaultValues]); - - useEffect(() => { - if (setForm != null) { + if (setForm) { setForm(RuleStep.aboutRule, form); } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [form]); + }, [setForm, form]); return isReadOnlyView && myStepData.name != null ? ( @@ -338,8 +323,8 @@ const StepAboutRuleComponent: FC = ({ {({ severity }) => { const newRiskScore = defaultRiskScoreBySeverity[severity as SeverityValue]; - const severityField = form.getFields().severity; - const riskScoreField = form.getFields().riskScore; + const severityField = getFields().severity; + const riskScoreField = getFields().riskScore; if ( severityField.value !== severity && newRiskScore != null && 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 c7d70684b34cf..51e9291f31941 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 @@ -17,7 +17,6 @@ import { useFetchIndexPatterns } from '../../../containers/detection_engine/rule import { DEFAULT_TIMELINE_TITLE } from '../../../../timelines/components/timeline/translations'; import { useMlCapabilities } from '../../../../common/components/ml_popover/hooks/use_ml_capabilities'; import { useUiSetting$ } from '../../../../common/lib/kibana'; -import { setFieldValue } from '../../../pages/detection_engine/rules/helpers'; import { filterRuleFieldsForType, RuleFields, @@ -109,58 +108,46 @@ const StepDefineRuleComponent: FC = ({ const mlCapabilities = useMlCapabilities(); const [openTimelineSearch, setOpenTimelineSearch] = useState(false); const [indexModified, setIndexModified] = useState(false); - const [localRuleType, setLocalRuleType] = useState( - defaultValues?.ruleType || stepDefineDefaultValue.ruleType - ); const [indicesConfig] = useUiSetting$(DEFAULT_INDEX_KEY); - const [myStepData, setMyStepData] = useState({ + const initialState = defaultValues ?? { ...stepDefineDefaultValue, index: indicesConfig ?? [], - }); + }; + const [localRuleType, setLocalRuleType] = useState(initialState.ruleType); + const [myStepData, setMyStepData] = useState(initialState); const [ { browserFields, indexPatterns: indexPatternQueryBar, isLoading: indexPatternLoadingQueryBar }, ] = useFetchIndexPatterns(myStepData.index); const { form } = useForm({ - defaultValue: myStepData, + defaultValue: initialState, options: { stripEmptyFields: false }, schema, }); - const clearErrors = useCallback(() => form.reset({ resetValues: false }), [form]); + const { getFields, reset, submit } = form; + const clearErrors = useCallback(() => reset({ resetValues: false }), [reset]); const onSubmit = useCallback(async () => { if (setStepData) { setStepData(RuleStep.defineRule, null, false); - const { isValid, data } = await form.submit(); + const { isValid, data } = await submit(); if (isValid && setStepData) { setStepData(RuleStep.defineRule, data, isValid); setMyStepData({ ...data, isNew: false } as DefineStepRule); } } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [form]); - - useEffect(() => { - const { isNew, ...values } = myStepData; - if (defaultValues != null && !deepEqual(values, defaultValues)) { - const newValues = { ...values, ...defaultValues, isNew: false }; - setMyStepData(newValues); - setFieldValue(form, schema, newValues); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [defaultValues, setMyStepData, setFieldValue]); + }, [setStepData, submit]); useEffect(() => { - if (setForm != null) { + if (setForm) { setForm(RuleStep.defineRule, form); } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [form]); + }, [form, setForm]); const handleResetIndices = useCallback(() => { - const indexField = form.getFields().index; + const indexField = getFields().index; indexField.setValue(indicesConfig); - }, [form, indicesConfig]); + }, [getFields, indicesConfig]); const handleOpenTimelineSearch = useCallback(() => { setOpenTimelineSearch(true); @@ -281,11 +268,11 @@ const StepDefineRuleComponent: FC = ({ fields={{ thresholdField: { path: 'threshold.field', - defaultValue: defaultValues?.threshold?.field, + defaultValue: initialState.threshold.field, }, thresholdValue: { path: 'threshold.value', - defaultValue: defaultValues?.threshold?.value, + defaultValue: initialState.threshold.value, }, }} > 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 7005bfb25f4a6..7bf151adde5cc 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 @@ -14,9 +14,7 @@ import { } from '@elastic/eui'; import { findIndex } from 'lodash/fp'; import React, { FC, memo, useCallback, useEffect, useMemo, useState } from 'react'; -import deepEqual from 'fast-deep-equal'; -import { setFieldValue } from '../../../pages/detection_engine/rules/helpers'; import { RuleStep, RuleStepProps, @@ -71,7 +69,8 @@ const StepRuleActionsComponent: FC = ({ setForm, actionMessageParams, }) => { - const [myStepData, setMyStepData] = useState(stepActionsDefaultValue); + const initialState = defaultValues ?? stepActionsDefaultValue; + const [myStepData, setMyStepData] = useState(initialState); const { services: { application, @@ -81,10 +80,11 @@ const StepRuleActionsComponent: FC = ({ const schema = useMemo(() => getSchema({ actionTypeRegistry }), [actionTypeRegistry]); const { form } = useForm({ - defaultValue: myStepData, + defaultValue: initialState, options: { stripEmptyFields: false }, schema, }); + const { submit } = form; // TO DO need to make sure that logic is still valid const kibanaAbsoluteUrl = useMemo(() => { @@ -101,36 +101,21 @@ const StepRuleActionsComponent: FC = ({ async (enabled: boolean) => { if (setStepData) { setStepData(RuleStep.ruleActions, null, false); - const { isValid: newIsValid, data } = await form.submit(); + const { isValid: newIsValid, data } = await submit(); if (newIsValid) { setStepData(RuleStep.ruleActions, { ...data, enabled }, newIsValid); setMyStepData({ ...data, isNew: false } as ActionsStepRule); } } }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [form] + [setStepData, submit] ); useEffect(() => { - const { isNew, ...initDefaultValue } = myStepData; - if (defaultValues != null && !deepEqual(initDefaultValue, defaultValues)) { - const myDefaultValues = { - ...defaultValues, - isNew: false, - }; - setMyStepData(myDefaultValues); - setFieldValue(form, schema, myDefaultValues); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [defaultValues]); - - useEffect(() => { - if (setForm != null) { + if (setForm) { setForm(RuleStep.ruleActions, form); } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [form]); + }, [form, setForm]); const updateThrottle = useCallback((throttle) => setMyStepData({ ...myStepData, throttle }), [ myStepData, diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_schedule_rule/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_schedule_rule/index.tsx index fa0f4dbd3668c..52f04f8423bec 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_schedule_rule/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_schedule_rule/index.tsx @@ -5,9 +5,7 @@ */ import React, { FC, memo, useCallback, useEffect, useState } from 'react'; -import deepEqual from 'fast-deep-equal'; -import { setFieldValue } from '../../../pages/detection_engine/rules/helpers'; import { RuleStep, RuleStepProps, @@ -40,45 +38,32 @@ const StepScheduleRuleComponent: FC = ({ setStepData, setForm, }) => { - const [myStepData, setMyStepData] = useState(stepScheduleDefaultValue); + const initialState = defaultValues ?? stepScheduleDefaultValue; + const [myStepData, setMyStepData] = useState(initialState); const { form } = useForm({ - defaultValue: myStepData, + defaultValue: initialState, options: { stripEmptyFields: false }, schema, }); + const { submit } = form; const onSubmit = useCallback(async () => { if (setStepData) { setStepData(RuleStep.scheduleRule, null, false); - const { isValid: newIsValid, data } = await form.submit(); + const { isValid: newIsValid, data } = await submit(); if (newIsValid) { setStepData(RuleStep.scheduleRule, { ...data }, newIsValid); setMyStepData({ ...data, isNew: false } as ScheduleStepRule); } } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [form]); + }, [setStepData, submit]); useEffect(() => { - const { isNew, ...initDefaultValue } = myStepData; - if (defaultValues != null && !deepEqual(initDefaultValue, defaultValues)) { - const myDefaultValues = { - ...defaultValues, - isNew: false, - }; - setMyStepData(myDefaultValues); - setFieldValue(form, schema, myDefaultValues); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [defaultValues]); - - useEffect(() => { - if (setForm != null) { + if (setForm) { setForm(RuleStep.scheduleRule, form); } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [form]); + }, [form, setForm]); return isReadOnlyView && myStepData != null ? ( diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.tsx index f6e13786e98d0..6ba65ceca8fe9 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.tsx @@ -109,10 +109,10 @@ const CreateRulePageComponent: React.FC = () => { [RuleStep.ruleActions]: null, }); const stepsData = useRef>({ - [RuleStep.defineRule]: { isValid: false, data: {} }, - [RuleStep.aboutRule]: { isValid: false, data: {} }, - [RuleStep.scheduleRule]: { isValid: false, data: {} }, - [RuleStep.ruleActions]: { isValid: false, data: {} }, + [RuleStep.defineRule]: { isValid: false, data: undefined }, + [RuleStep.aboutRule]: { isValid: false, data: undefined }, + [RuleStep.scheduleRule]: { isValid: false, data: undefined }, + [RuleStep.ruleActions]: { isValid: false, data: undefined }, }); const [isStepRuleInReadOnlyView, setIsStepRuleInEditView] = useState>({ [RuleStep.defineRule]: false, @@ -123,7 +123,7 @@ const CreateRulePageComponent: React.FC = () => { const [{ isLoading, isSaved }, setRule] = usePersistRule(); const actionMessageParams = useMemo( () => - getActionMessageParams((stepsData.current['define-rule'].data as DefineStepRule).ruleType), + getActionMessageParams((stepsData.current['define-rule'].data as DefineStepRule)?.ruleType), // eslint-disable-next-line react-hooks/exhaustive-deps [stepsData.current['define-rule'].data] ); @@ -335,9 +335,7 @@ const CreateRulePageComponent: React.FC = () => { { { , - schema: FormSchema, - defaultValues: unknown -) => - Object.keys(schema).forEach((key) => { - const val = get(key, defaultValues); - if (val != null) { - form.setFieldValue(key, val); - } - }); export const redirectToDetections = ( isSignalIndexExists: boolean | null,