[Form lib] Memoize form hook object and fix hook array deps (#71237)
Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com> Co-authored-by: Patryk Kopycinski <contact@patrykkopycinski.com>
This commit is contained in:
parent
1ac56d7bfc
commit
99255d824d
|
@ -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<T extends object = { [key: string]: any }
|
|||
* @param contentId The content id to be added to the "contents" map
|
||||
*/
|
||||
export function useContent<T extends object, K extends keyof T>(contentId: K) {
|
||||
const { updateContentAt, saveSnapshotAndRemoveContent, getData } = useMultiContentContext<T>();
|
||||
const isMounted = useRef(false);
|
||||
const defaultValue = useRef<T[K] | undefined>(undefined);
|
||||
const {
|
||||
updateContentAt,
|
||||
saveSnapshotAndRemoveContent,
|
||||
getData,
|
||||
getSingleContentData,
|
||||
} = useMultiContentContext<T>();
|
||||
|
||||
const updateContent = useCallback(
|
||||
(content: Content) => {
|
||||
|
@ -71,12 +78,22 @@ export function useContent<T extends object, K extends keyof T>(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,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -45,6 +45,7 @@ export interface MultiContent<T extends object> {
|
|||
updateContentAt: (id: keyof T, content: Content) => void;
|
||||
saveSnapshotAndRemoveContent: (id: keyof T) => void;
|
||||
getData: () => T;
|
||||
getSingleContentData: <K extends keyof T>(contentId: K) => T[K];
|
||||
validate: () => Promise<boolean>;
|
||||
validation: Validation<T>;
|
||||
}
|
||||
|
@ -109,9 +110,22 @@ export function useMultiContent<T extends object>({
|
|||
};
|
||||
}, [stateData, validation]);
|
||||
|
||||
/**
|
||||
* Read a single content data.
|
||||
*/
|
||||
const getSingleContentData = useCallback(
|
||||
<K extends keyof T>(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<T extends object>({
|
|||
)
|
||||
) {
|
||||
// No change in validation, nothing to update
|
||||
allContentValidity = prev.isValid;
|
||||
isAllContentValid = prev.isValid;
|
||||
return prev;
|
||||
}
|
||||
|
||||
|
@ -129,21 +143,21 @@ export function useMultiContent<T extends object>({
|
|||
...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<T extends object>({
|
|||
}
|
||||
|
||||
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<T extends object>({
|
|||
});
|
||||
}
|
||||
},
|
||||
[updateContentValidity, onChange]
|
||||
[updateContentValidity, onChange, getData, validate]
|
||||
);
|
||||
|
||||
/**
|
||||
|
@ -211,6 +225,7 @@ export function useMultiContent<T extends object>({
|
|||
|
||||
return {
|
||||
getData,
|
||||
getSingleContentData,
|
||||
validate,
|
||||
validation,
|
||||
updateContentAt,
|
||||
|
|
|
@ -29,6 +29,7 @@ interface Props {
|
|||
|
||||
export const FormDataProvider = React.memo(({ children, pathsToWatch }: Props) => {
|
||||
const form = useFormContext();
|
||||
const { subscribe } = form;
|
||||
const previousRawData = useRef<FormData>(form.__getFormData$().value);
|
||||
const [formData, setFormData] = useState<FormData>(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);
|
||||
});
|
||||
|
|
|
@ -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<ArrayItem[]>(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 });
|
||||
};
|
||||
|
|
|
@ -30,8 +30,9 @@ describe('<UseField />', () => {
|
|||
|
||||
const TestComp = ({ onData }: { onData: OnUpdateHandler }) => {
|
||||
const { form } = useForm();
|
||||
const { subscribe } = form;
|
||||
|
||||
useEffect(() => form.subscribe(onData).unsubscribe, [form]);
|
||||
useEffect(() => subscribe(onData).unsubscribe, [subscribe, onData]);
|
||||
|
||||
return (
|
||||
<Form form={form}>
|
||||
|
|
|
@ -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 = <T>(
|
|||
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<T>(initialValue);
|
||||
const [errors, setErrors] = useState<ValidationError[]>([]);
|
||||
|
@ -64,6 +64,12 @@ export const useField = <T>(
|
|||
|
||||
// -- HELPERS
|
||||
// ----------------------------------
|
||||
const serializeOutput: FieldHook<T>['__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 = <T>(
|
|||
);
|
||||
};
|
||||
|
||||
const formatInputValue = <T>(inputValue: unknown): T => {
|
||||
const isEmptyString = typeof inputValue === 'string' && inputValue.trim() === '';
|
||||
const formatInputValue = useCallback(
|
||||
<T>(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 = <T>(
|
|||
}
|
||||
|
||||
// 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 = <T>(
|
|||
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 = <T>(
|
|||
(inflightValidation.current as any).cancel();
|
||||
inflightValidation.current = null;
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const runValidations = ({
|
||||
formData,
|
||||
value: valueToValidate,
|
||||
validationTypeToValidate,
|
||||
}: {
|
||||
formData: any;
|
||||
value: unknown;
|
||||
validationTypeToValidate?: string;
|
||||
}): ValidationError[] | Promise<ValidationError[]> => {
|
||||
// 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]);
|
||||
const clearErrors: FieldHook['clearErrors'] = useCallback(
|
||||
(validationType = VALIDATION_TYPES.FIELD) => {
|
||||
setErrors((previousErrors) => filterErrors(previousErrors, validationType));
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
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<ValidationError>;
|
||||
|
||||
const validationResult = await inflightValidation.current;
|
||||
|
||||
if (!validationResult) {
|
||||
continue;
|
||||
}
|
||||
|
||||
validationErrors.push({
|
||||
...validationResult,
|
||||
validationType: validationType || VALIDATION_TYPES.FIELD,
|
||||
});
|
||||
|
||||
if (exitOnFail) {
|
||||
break;
|
||||
}
|
||||
const runValidations = useCallback(
|
||||
({
|
||||
formData,
|
||||
value: valueToValidate,
|
||||
validationTypeToValidate,
|
||||
}: {
|
||||
formData: any;
|
||||
value: unknown;
|
||||
validationTypeToValidate?: string;
|
||||
}): ValidationError[] | Promise<ValidationError[]> => {
|
||||
if (!validations) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return validationErrors;
|
||||
};
|
||||
// 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]);
|
||||
|
||||
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;
|
||||
cancelInflightValidation();
|
||||
|
||||
if (
|
||||
typeof validationTypeToValidate !== 'undefined' &&
|
||||
validationType !== validationTypeToValidate
|
||||
) {
|
||||
continue;
|
||||
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<ValidationError>;
|
||||
|
||||
const validationResult = await inflightValidation.current;
|
||||
|
||||
if (!validationResult) {
|
||||
continue;
|
||||
}
|
||||
|
||||
validationErrors.push({
|
||||
...validationResult,
|
||||
validationType: validationType || VALIDATION_TYPES.FIELD,
|
||||
});
|
||||
|
||||
if (exitOnFail) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const validationResult = validator({
|
||||
value: (valueToValidate as unknown) as string,
|
||||
errors: validationErrors,
|
||||
form,
|
||||
formData,
|
||||
path,
|
||||
});
|
||||
return validationErrors;
|
||||
};
|
||||
|
||||
if (!validationResult) {
|
||||
continue;
|
||||
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<ValidationError>;
|
||||
cancelInflightValidation();
|
||||
|
||||
return runAsync();
|
||||
}
|
||||
|
||||
validationErrors.push({
|
||||
...(validationResult as ValidationError),
|
||||
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.
|
||||
return validationErrors;
|
||||
};
|
||||
|
||||
inflightValidation.current = validationResult as Promise<ValidationError>;
|
||||
cancelInflightValidation();
|
||||
|
||||
return runAsync();
|
||||
}
|
||||
|
||||
validationErrors.push({
|
||||
...(validationResult as ValidationError),
|
||||
validationType: validationType || VALIDATION_TYPES.FIELD,
|
||||
});
|
||||
|
||||
if (exitOnFail) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
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<T>['validate'] = (validationData = {}) => {
|
||||
const {
|
||||
formData = form.getFormData({ unflatten: false }),
|
||||
value: valueToValidate = value,
|
||||
validationType,
|
||||
} = validationData;
|
||||
const validate: FieldHook<T>['validate'] = useCallback(
|
||||
(validationData = {}) => {
|
||||
const {
|
||||
formData = getFormData({ unflatten: false }),
|
||||
value: valueToValidate = value,
|
||||
validationType,
|
||||
} = validationData;
|
||||
|
||||
setIsValidated(true);
|
||||
setValidating(true);
|
||||
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;
|
||||
// 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 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<ValidationError[]>).then(onValidationErrors);
|
||||
}
|
||||
return onValidationErrors(validationErrors as ValidationError[]);
|
||||
};
|
||||
if (Reflect.has(validationErrors, 'then')) {
|
||||
return (validationErrors as Promise<ValidationError[]>).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<T>['setValue'] = (newValue) => {
|
||||
if (isPristine) {
|
||||
setPristine(false);
|
||||
}
|
||||
const setValue: FieldHook<T>['setValue'] = useCallback(
|
||||
(newValue) => {
|
||||
if (isPristine) {
|
||||
setPristine(false);
|
||||
}
|
||||
|
||||
const formattedValue = formatInputValue<T>(newValue);
|
||||
setStateValue(formattedValue);
|
||||
};
|
||||
const formattedValue = formatInputValue<T>(newValue);
|
||||
setStateValue(formattedValue);
|
||||
return formattedValue;
|
||||
},
|
||||
[formatInputValue, isPristine]
|
||||
);
|
||||
|
||||
const _setErrors: FieldHook<T>['setErrors'] = (_errors) => {
|
||||
const _setErrors: FieldHook<T>['setErrors'] = useCallback((_errors) => {
|
||||
setErrors(_errors.map((error) => ({ validationType: VALIDATION_TYPES.FIELD, ...error })));
|
||||
};
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Form <input /> "onChange" event handler
|
||||
*
|
||||
* @param event Form input change event
|
||||
*/
|
||||
const onChange: FieldHook<T>['onChange'] = (event) => {
|
||||
const newValue = {}.hasOwnProperty.call(event!.target, 'checked')
|
||||
? event.target.checked
|
||||
: event.target.value;
|
||||
const onChange: FieldHook<T>['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 = <T>(
|
|||
*
|
||||
* @param validationType The validation type to return error messages from
|
||||
*/
|
||||
const getErrorsMessages: FieldHook<T>['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'));
|
||||
const getErrorsMessages: FieldHook<T>['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);
|
||||
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<T>['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<T>['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<T>['__serializeOutput'] = (rawValue = value) =>
|
||||
serializer(rawValue);
|
||||
},
|
||||
[setValue, serializeOutput, initialValue]
|
||||
);
|
||||
|
||||
// -- EFFECTS
|
||||
// ----------------------------------
|
||||
|
@ -425,54 +468,64 @@ export const useField = <T>(
|
|||
clearTimeout(debounceTimeout.current);
|
||||
}
|
||||
};
|
||||
}, [value]);
|
||||
}, [isPristine, onValueChange]);
|
||||
|
||||
const field: FieldHook<T> = {
|
||||
const field: FieldHook<T> = 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<any>); // Executed first (1)
|
||||
form.__addField(field as FieldHook<any>);
|
||||
|
||||
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<any>); // 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;
|
||||
};
|
||||
|
|
|
@ -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 (
|
||||
<Form form={form}>
|
||||
|
@ -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 (
|
||||
<Form form={form}>
|
||||
|
|
|
@ -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<T extends FormData> {
|
|||
}
|
||||
|
||||
export function useForm<T extends FormData = FormData>(
|
||||
formConfig: FormConfig<T> | undefined = {}
|
||||
formConfig?: FormConfig<T>
|
||||
): UseFormReturn<T> {
|
||||
const {
|
||||
onSubmit,
|
||||
schema,
|
||||
serializer = <T>(data: T): T => data,
|
||||
deserializer = <T>(data: T): T => data,
|
||||
options = {},
|
||||
id = 'default',
|
||||
} = formConfig;
|
||||
const { onSubmit, schema, serializer, deserializer, options, id = 'default', defaultValue } =
|
||||
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 formDefaultValue = useMemo(() => {
|
||||
if (defaultValue === undefined || Object.keys(defaultValue).length === 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const formOptions = { ...DEFAULT_OPTIONS, ...options };
|
||||
const defaultValueDeserialized = useMemo(() => deserializer(formDefaultValue), [
|
||||
formConfig.defaultValue,
|
||||
]);
|
||||
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<T extends FormData = FormData>(
|
|||
|
||||
// -- HELPERS
|
||||
// ----------------------------------
|
||||
const getFormData$ = (): Subject<T> => {
|
||||
const getFormData$ = useCallback((): Subject<T> => {
|
||||
if (formData$.current === null) {
|
||||
formData$.current = new Subject<T>({} 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 updateFormDataAt: FormHook<T>['__updateFormDataAt'] = (path, value) => {
|
||||
const _formData$ = getFormData$();
|
||||
const currentFormData = _formData$.value;
|
||||
const nextValue = { ...currentFormData, [path]: value };
|
||||
_formData$.next(nextValue);
|
||||
return _formData$.value;
|
||||
};
|
||||
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<T>['__updateFormDataAt'] = useCallback(
|
||||
(path, value) => {
|
||||
const _formData$ = getFormData$();
|
||||
const currentFormData = _formData$.value;
|
||||
|
||||
if (currentFormData[path] !== value) {
|
||||
_formData$.next({ ...currentFormData, [path]: value });
|
||||
}
|
||||
|
||||
return _formData$.value;
|
||||
},
|
||||
[getFormData$]
|
||||
);
|
||||
|
||||
// -- API
|
||||
// ----------------------------------
|
||||
const getFormData: FormHook<T>['getFormData'] = (
|
||||
getDataOptions: Parameters<FormHook<T>['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;
|
||||
}
|
||||
const getFormData: FormHook<T>['getFormData'] = useCallback(
|
||||
(getDataOptions: Parameters<FormHook<T>['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);
|
||||
}
|
||||
|
||||
return Object.entries(fieldsRefs.current).reduce(
|
||||
(acc, [key, field]) => ({
|
||||
...acc,
|
||||
[key]: field.__serializeOutput(),
|
||||
}),
|
||||
{} as T
|
||||
);
|
||||
};
|
||||
return Object.entries(fieldsRefs.current).reduce(
|
||||
(acc, [key, field]) => ({
|
||||
...acc,
|
||||
[key]: field.__serializeOutput(),
|
||||
}),
|
||||
{} as T
|
||||
);
|
||||
},
|
||||
[stripEmptyFields, serializer]
|
||||
);
|
||||
|
||||
const getErrors: FormHook['getErrors'] = () => {
|
||||
const getErrors: FormHook['getErrors'] = useCallback(() => {
|
||||
if (isValid === true) {
|
||||
return [];
|
||||
}
|
||||
|
@ -141,11 +160,15 @@ export function useForm<T extends FormData = FormData>(
|
|||
}
|
||||
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<T extends FormData = FormData>(
|
|||
|
||||
setIsValid(isFormValid);
|
||||
return isFormValid;
|
||||
};
|
||||
}, [fieldsToArray]);
|
||||
|
||||
const validateFields: FormHook<T>['__validateFields'] = async (fieldNames) => {
|
||||
const fieldsToValidate = fieldNames
|
||||
.map((name) => fieldsRefs.current[name])
|
||||
.filter((field) => field !== undefined);
|
||||
const validateFields: FormHook<T>['__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<boolean> => {
|
||||
const validateAllFields = useCallback(async (): Promise<boolean> => {
|
||||
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<T>['__addField'] = (field) => {
|
||||
fieldsRefs.current[field.path] = field;
|
||||
const addField: FormHook<T>['__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<T>['__removeField'] = (_fieldNames) => {
|
||||
const fieldNames = Array.isArray(_fieldNames) ? _fieldNames : [_fieldNames];
|
||||
const currentFormData = { ...getFormData$().value } as FormData;
|
||||
const removeField: FormHook<T>['__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<T>['setFieldValue'] = (fieldName, value) => {
|
||||
const setFieldValue: FormHook<T>['setFieldValue'] = useCallback((fieldName, value) => {
|
||||
if (fieldsRefs.current[fieldName] === undefined) {
|
||||
return;
|
||||
}
|
||||
fieldsRefs.current[fieldName].setValue(value);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const setFieldErrors: FormHook<T>['setFieldErrors'] = (fieldName, errors) => {
|
||||
const setFieldErrors: FormHook<T>['setFieldErrors'] = useCallback((fieldName, errors) => {
|
||||
if (fieldsRefs.current[fieldName] === undefined) {
|
||||
return;
|
||||
}
|
||||
fieldsRefs.current[fieldName].setErrors(errors);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const getFields: FormHook<T>['getFields'] = () => fieldsRefs.current;
|
||||
const getFields: FormHook<T>['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<T>['__readFieldConfigFromSchema'] = (fieldName) => {
|
||||
const config = (get(schema ? schema : {}, fieldName) as FieldConfig) || {};
|
||||
const readFieldConfigFromSchema: FormHook<T>['__readFieldConfigFromSchema'] = useCallback(
|
||||
(fieldName) => {
|
||||
const config = (get(schema ?? {}, fieldName) as FieldConfig) || {};
|
||||
|
||||
return config;
|
||||
};
|
||||
return config;
|
||||
},
|
||||
[schema]
|
||||
);
|
||||
|
||||
const submitForm: FormHook<T>['submit'] = async (e) => {
|
||||
if (e) {
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
if (!isSubmitted) {
|
||||
setIsSubmitted(true); // User has attempted to submit the form at least once
|
||||
}
|
||||
setSubmitting(true);
|
||||
|
||||
const isFormValid = await validateAllFields();
|
||||
const formData = getFormData();
|
||||
|
||||
if (onSubmit) {
|
||||
await onSubmit(formData, isFormValid!);
|
||||
}
|
||||
|
||||
if (isUnmounted.current === false) {
|
||||
setSubmitting(false);
|
||||
}
|
||||
|
||||
return { data: formData, isValid: isFormValid! };
|
||||
};
|
||||
|
||||
const subscribe: FormHook<T>['subscribe'] = (handler) => {
|
||||
const subscription = getFormData$().subscribe((raw) => {
|
||||
if (!isUnmounted.current) {
|
||||
handler({ isValid, data: { raw, format: getFormData }, validate: validateAllFields });
|
||||
const submitForm: FormHook<T>['submit'] = useCallback(
|
||||
async (e) => {
|
||||
if (e) {
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
formUpdateSubscribers.current.push(subscription);
|
||||
setIsSubmitted(true); // User has attempted to submit the form at least once
|
||||
setSubmitting(true);
|
||||
|
||||
return {
|
||||
unsubscribe() {
|
||||
formUpdateSubscribers.current = formUpdateSubscribers.current.filter(
|
||||
(sub) => sub !== subscription
|
||||
);
|
||||
return subscription.unsubscribe();
|
||||
},
|
||||
};
|
||||
};
|
||||
const isFormValid = await validateAllFields();
|
||||
const formData = getFormData();
|
||||
|
||||
if (onSubmit) {
|
||||
await onSubmit(formData, isFormValid!);
|
||||
}
|
||||
|
||||
if (isUnmounted.current === false) {
|
||||
setSubmitting(false);
|
||||
}
|
||||
|
||||
return { data: formData, isValid: isFormValid! };
|
||||
},
|
||||
[validateAllFields, getFormData, onSubmit]
|
||||
);
|
||||
|
||||
const subscribe: FormHook<T>['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<T>['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<T>['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<T> = {
|
||||
const form = useMemo<FormHook<T>>(() => {
|
||||
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<T extends FormData = FormData>(
|
|||
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,
|
||||
|
|
|
@ -107,7 +107,7 @@ export interface FieldHook<T = unknown> {
|
|||
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<T = unknown> {
|
|||
value?: unknown;
|
||||
validationType?: string;
|
||||
}) => FieldValidateResponse | Promise<FieldValidateResponse>;
|
||||
reset: (options?: { resetValue: boolean }) => unknown;
|
||||
reset: (options?: { resetValue: boolean }) => unknown | undefined;
|
||||
__serializeOutput: (rawValue?: unknown) => unknown;
|
||||
}
|
||||
|
||||
|
|
|
@ -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<Props> = React.memo(
|
|||
options: { stripEmptyFields: false },
|
||||
});
|
||||
|
||||
const { isValid: isFormValid, submit, getFormData, subscribe } = form;
|
||||
|
||||
const { documentation } = useComponentTemplatesContext();
|
||||
|
||||
const [isMetaVisible, setIsMetaVisible] = useState<boolean>(
|
||||
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<Props> = React.memo(
|
|||
});
|
||||
});
|
||||
return subscription.unsubscribe;
|
||||
}, [onChange]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
}, [subscribe, validate, onChange]);
|
||||
|
||||
return (
|
||||
<Form form={form} data-test-subj="stepLogistics">
|
||||
|
|
|
@ -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 (
|
||||
<Form
|
||||
|
|
|
@ -50,13 +50,15 @@ export const CreateField = React.memo(function CreateFieldComponent({
|
|||
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
|
||||
}, [dispatch, subscribe]);
|
||||
|
||||
const cancel = () => {
|
||||
dispatch({ type: 'documentField.changeStatus', value: 'idle' });
|
||||
|
|
|
@ -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' });
|
||||
|
|
|
@ -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 (
|
||||
<div data-test-subj="dynamicTemplates">
|
||||
|
|
|
@ -14,15 +14,16 @@ interface Props {
|
|||
}
|
||||
|
||||
export const StepMappingsContainer: React.FunctionComponent<Props> = ({ esDocsBase }) => {
|
||||
const { defaultValue, updateContent, getData } = Forms.useContent<CommonWizardSteps, 'mappings'>(
|
||||
const { defaultValue, updateContent, getSingleContentData } = Forms.useContent<
|
||||
CommonWizardSteps,
|
||||
'mappings'
|
||||
);
|
||||
>('mappings');
|
||||
|
||||
return (
|
||||
<StepMappings
|
||||
defaultValue={defaultValue}
|
||||
onChange={updateContent}
|
||||
indexSettings={getData().settings}
|
||||
indexSettings={getSingleContentData('settings')}
|
||||
esDocsBase={esDocsBase}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -47,7 +47,7 @@ export const AddComment = React.memo<AddCommentProps>(
|
|||
options: { stripEmptyFields: false },
|
||||
schema,
|
||||
});
|
||||
|
||||
const { getFormData, setFieldValue, reset, submit } = form;
|
||||
const { handleCursorChange, handleOnTimelineChange } = useInsertTimeline<CommentRequest>(
|
||||
form,
|
||||
'comment'
|
||||
|
@ -55,26 +55,23 @@ export const AddComment = React.memo<AddCommentProps>(
|
|||
|
||||
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 (
|
||||
<span id="add-comment-permLink">
|
||||
|
|
|
@ -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('/');
|
||||
|
|
|
@ -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 (
|
||||
<EuiText>
|
||||
<MyFlexGroup alignItems="center" gutterSize="xs" justifyContent="spaceBetween">
|
||||
|
|
|
@ -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) => ({
|
||||
|
|
|
@ -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<Content>({
|
||||
defaultValue: { content },
|
||||
defaultValue: initialState,
|
||||
options: { stripEmptyFields: false },
|
||||
schema,
|
||||
});
|
||||
const { submit } = form;
|
||||
const { handleCursorChange, handleOnTimelineChange } = useInsertTimeline<Content>(
|
||||
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 (
|
||||
<EuiFlexGroup gutterSize="s" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
data-test-subj="user-action-cancel-markdown"
|
||||
size="s"
|
||||
onClick={cancelAction}
|
||||
iconType="cross"
|
||||
>
|
||||
{i18n.CANCEL}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
data-test-subj="user-action-save-markdown"
|
||||
color="secondary"
|
||||
fill
|
||||
iconType="save"
|
||||
onClick={saveAction}
|
||||
size="s"
|
||||
>
|
||||
{i18n.SAVE}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[handleCancelAction, handleSaveAction]
|
||||
({ cancelAction, saveAction }) => (
|
||||
<EuiFlexGroup gutterSize="s" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
data-test-subj="user-action-cancel-markdown"
|
||||
size="s"
|
||||
onClick={cancelAction}
|
||||
iconType="cross"
|
||||
>
|
||||
{i18n.CANCEL}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
data-test-subj="user-action-save-markdown"
|
||||
color="secondary"
|
||||
fill
|
||||
iconType="save"
|
||||
onClick={saveAction}
|
||||
size="s"
|
||||
>
|
||||
{i18n.SAVE}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
),
|
||||
[]
|
||||
);
|
||||
|
||||
return isEditable ? (
|
||||
<Form form={form} data-test-subj="user-action-markdown-form">
|
||||
<UseField
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
import { EuiAccordion, EuiFlexItem, EuiSpacer, EuiButtonEmpty, EuiFormRow } from '@elastic/eui';
|
||||
import React, { FC, memo, useCallback, useEffect, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
|
||||
import {
|
||||
RuleStepProps,
|
||||
|
@ -35,7 +34,6 @@ import * as I18n from './translations';
|
|||
import { StepContentWrapper } from '../step_content_wrapper';
|
||||
import { NextStep } from '../next_step';
|
||||
import { MarkdownEditorForm } from '../../../../common/components/markdown_editor/form';
|
||||
import { setFieldValue } from '../../../pages/detection_engine/rules/helpers';
|
||||
import { SeverityField } from '../severity_mapping';
|
||||
import { RiskScoreField } from '../risk_score_mapping';
|
||||
import { useFetchIndexPatterns } from '../../../containers/detection_engine/rules';
|
||||
|
@ -44,8 +42,8 @@ import { AutocompleteField } from '../autocomplete_field';
|
|||
const CommonUseField = getUseField({ component: Field });
|
||||
|
||||
interface StepAboutRuleProps extends RuleStepProps {
|
||||
defaultValues?: AboutStepRule | null;
|
||||
defineRuleData?: DefineStepRule | null;
|
||||
defaultValues?: AboutStepRule;
|
||||
defineRuleData?: DefineStepRule;
|
||||
}
|
||||
|
||||
const ThreeQuartersContainer = styled.div`
|
||||
|
@ -91,48 +89,35 @@ const StepAboutRuleComponent: FC<StepAboutRuleProps> = ({
|
|||
setForm,
|
||||
setStepData,
|
||||
}) => {
|
||||
const [myStepData, setMyStepData] = useState<AboutStepRule>(stepAboutDefaultValue);
|
||||
const initialState = defaultValues ?? stepAboutDefaultValue;
|
||||
const [myStepData, setMyStepData] = useState<AboutStepRule>(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 ? (
|
||||
<StepContentWrapper data-test-subj="aboutStep" addPadding={addPadding}>
|
||||
|
@ -338,8 +323,8 @@ const StepAboutRuleComponent: FC<StepAboutRuleProps> = ({
|
|||
<FormDataProvider pathsToWatch="severity">
|
||||
{({ 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 &&
|
||||
|
|
|
@ -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<StepDefineRuleProps> = ({
|
|||
const mlCapabilities = useMlCapabilities();
|
||||
const [openTimelineSearch, setOpenTimelineSearch] = useState(false);
|
||||
const [indexModified, setIndexModified] = useState(false);
|
||||
const [localRuleType, setLocalRuleType] = useState(
|
||||
defaultValues?.ruleType || stepDefineDefaultValue.ruleType
|
||||
);
|
||||
const [indicesConfig] = useUiSetting$<string[]>(DEFAULT_INDEX_KEY);
|
||||
const [myStepData, setMyStepData] = useState<DefineStepRule>({
|
||||
const initialState = defaultValues ?? {
|
||||
...stepDefineDefaultValue,
|
||||
index: indicesConfig ?? [],
|
||||
});
|
||||
};
|
||||
const [localRuleType, setLocalRuleType] = useState(initialState.ruleType);
|
||||
const [myStepData, setMyStepData] = useState<DefineStepRule>(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]);
|
||||
}, [setStepData, submit]);
|
||||
|
||||
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]);
|
||||
|
||||
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<StepDefineRuleProps> = ({
|
|||
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,
|
||||
},
|
||||
}}
|
||||
>
|
||||
|
|
|
@ -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<StepRuleActionsProps> = ({
|
|||
setForm,
|
||||
actionMessageParams,
|
||||
}) => {
|
||||
const [myStepData, setMyStepData] = useState<ActionsStepRule>(stepActionsDefaultValue);
|
||||
const initialState = defaultValues ?? stepActionsDefaultValue;
|
||||
const [myStepData, setMyStepData] = useState<ActionsStepRule>(initialState);
|
||||
const {
|
||||
services: {
|
||||
application,
|
||||
|
@ -81,10 +80,11 @@ const StepRuleActionsComponent: FC<StepRuleActionsProps> = ({
|
|||
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<StepRuleActionsProps> = ({
|
|||
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,
|
||||
|
|
|
@ -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<StepScheduleRuleProps> = ({
|
|||
setStepData,
|
||||
setForm,
|
||||
}) => {
|
||||
const [myStepData, setMyStepData] = useState<ScheduleStepRule>(stepScheduleDefaultValue);
|
||||
const initialState = defaultValues ?? stepScheduleDefaultValue;
|
||||
const [myStepData, setMyStepData] = useState<ScheduleStepRule>(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 ? (
|
||||
<StepContentWrapper addPadding={addPadding}>
|
||||
|
|
|
@ -109,10 +109,10 @@ const CreateRulePageComponent: React.FC = () => {
|
|||
[RuleStep.ruleActions]: null,
|
||||
});
|
||||
const stepsData = useRef<Record<RuleStep, RuleStepData>>({
|
||||
[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<Record<RuleStep, boolean>>({
|
||||
[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 = () => {
|
|||
<EuiHorizontalRule margin="m" />
|
||||
<StepDefineRule
|
||||
addPadding={true}
|
||||
defaultValues={
|
||||
(stepsData.current[RuleStep.defineRule].data as DefineStepRule) ?? null
|
||||
}
|
||||
defaultValues={stepsData.current[RuleStep.defineRule].data as DefineStepRule}
|
||||
isReadOnlyView={isStepRuleInReadOnlyView[RuleStep.defineRule]}
|
||||
isLoading={isLoading || loading}
|
||||
setForm={setStepsForm}
|
||||
|
@ -370,10 +368,8 @@ const CreateRulePageComponent: React.FC = () => {
|
|||
<EuiHorizontalRule margin="m" />
|
||||
<StepAboutRule
|
||||
addPadding={true}
|
||||
defaultValues={(stepsData.current[RuleStep.aboutRule].data as AboutStepRule) ?? null}
|
||||
defineRuleData={
|
||||
(stepsData.current[RuleStep.defineRule].data as DefineStepRule) ?? null
|
||||
}
|
||||
defaultValues={stepsData.current[RuleStep.aboutRule].data as AboutStepRule}
|
||||
defineRuleData={stepsData.current[RuleStep.defineRule].data as DefineStepRule}
|
||||
descriptionColumns="singleSplit"
|
||||
isReadOnlyView={isStepRuleInReadOnlyView[RuleStep.aboutRule]}
|
||||
isLoading={isLoading || loading}
|
||||
|
@ -406,9 +402,7 @@ const CreateRulePageComponent: React.FC = () => {
|
|||
<EuiHorizontalRule margin="m" />
|
||||
<StepScheduleRule
|
||||
addPadding={true}
|
||||
defaultValues={
|
||||
(stepsData.current[RuleStep.scheduleRule].data as ScheduleStepRule) ?? null
|
||||
}
|
||||
defaultValues={stepsData.current[RuleStep.scheduleRule].data as ScheduleStepRule}
|
||||
descriptionColumns="singleSplit"
|
||||
isReadOnlyView={isStepRuleInReadOnlyView[RuleStep.scheduleRule]}
|
||||
isLoading={isLoading || loading}
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
*/
|
||||
|
||||
import dateMath from '@elastic/datemath';
|
||||
import { get } from 'lodash/fp';
|
||||
import moment from 'moment';
|
||||
import memoizeOne from 'memoize-one';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
@ -15,7 +14,6 @@ import { isMlRule } from '../../../../../common/machine_learning/helpers';
|
|||
import { transformRuleToAlertAction } from '../../../../../common/detection_engine/transform_actions';
|
||||
import { Filter } from '../../../../../../../../src/plugins/data/public';
|
||||
import { Rule } from '../../../containers/detection_engine/rules';
|
||||
import { FormData, FormHook, FormSchema } from '../../../../shared_imports';
|
||||
import {
|
||||
AboutStepRule,
|
||||
AboutStepRuleDetails,
|
||||
|
@ -273,17 +271,6 @@ export const getPrePackagedTimelineStatus = (
|
|||
}
|
||||
return 'unknown';
|
||||
};
|
||||
export const setFieldValue = (
|
||||
form: FormHook<FormData>,
|
||||
schema: FormSchema<FormData>,
|
||||
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,
|
||||
|
|
Loading…
Reference in a new issue