mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[Form lib] Export internal state instead of raw state (#80842)
This commit is contained in:
parent
08a6ddf25b
commit
702e0c7d73
35 changed files with 449 additions and 377 deletions
|
@ -133,7 +133,7 @@ describe('<FormDataProvider />', () => {
|
|||
find('btn').simulate('click').update();
|
||||
});
|
||||
|
||||
expect(onFormData.mock.calls.length).toBe(1);
|
||||
expect(onFormData.mock.calls.length).toBe(2);
|
||||
|
||||
const [formDataUpdated] = onFormData.mock.calls[onFormData.mock.calls.length - 1] as Parameters<
|
||||
OnUpdateHandler
|
||||
|
|
|
@ -22,13 +22,16 @@ import React from 'react';
|
|||
import { FormData } from '../types';
|
||||
import { useFormData } from '../hooks';
|
||||
|
||||
interface Props {
|
||||
children: (formData: FormData) => JSX.Element | null;
|
||||
interface Props<I> {
|
||||
children: (formData: I) => JSX.Element | null;
|
||||
pathsToWatch?: string | string[];
|
||||
}
|
||||
|
||||
export const FormDataProvider = React.memo(({ children, pathsToWatch }: Props) => {
|
||||
const { 0: formData, 2: isReady } = useFormData({ watch: pathsToWatch });
|
||||
const FormDataProviderComp = function <I extends FormData = FormData>({
|
||||
children,
|
||||
pathsToWatch,
|
||||
}: Props<I>) {
|
||||
const { 0: formData, 2: isReady } = useFormData<I>({ watch: pathsToWatch });
|
||||
|
||||
if (!isReady) {
|
||||
// No field has mounted yet, don't render anything
|
||||
|
@ -36,4 +39,6 @@ export const FormDataProvider = React.memo(({ children, pathsToWatch }: Props) =
|
|||
}
|
||||
|
||||
return children(formData);
|
||||
});
|
||||
};
|
||||
|
||||
export const FormDataProvider = React.memo(FormDataProviderComp) as typeof FormDataProviderComp;
|
||||
|
|
|
@ -107,7 +107,7 @@ export const UseArray = ({
|
|||
getNewItemAtIndex,
|
||||
]);
|
||||
|
||||
// Create a new hook field with the "hasValue" set to false so we don't use its value to build the final form data.
|
||||
// Create a new hook field with the "isIncludedInOutput" set to false so we don't use its value to build the final form data.
|
||||
// Apart from that the field behaves like a normal field and is hooked into the form validation lifecycle.
|
||||
const fieldConfigBase: FieldConfig<ArrayItem[]> & InternalFieldConfig<ArrayItem[]> = {
|
||||
defaultValue: fieldDefaultValue,
|
||||
|
|
|
@ -58,7 +58,7 @@ describe('<UseField />', () => {
|
|||
OnUpdateHandler
|
||||
>;
|
||||
|
||||
expect(data.raw).toEqual({
|
||||
expect(data.internal).toEqual({
|
||||
name: 'John',
|
||||
lastName: 'Snow',
|
||||
});
|
||||
|
@ -214,8 +214,8 @@ describe('<UseField />', () => {
|
|||
expect(serializer).not.toBeCalled();
|
||||
expect(formatter).not.toBeCalled();
|
||||
|
||||
let formData = formHook.getFormData({ unflatten: false });
|
||||
expect(formData.name).toEqual('John-deserialized');
|
||||
const internalFormData = formHook.__getFormData$().value;
|
||||
expect(internalFormData.name).toEqual('John-deserialized');
|
||||
|
||||
await act(async () => {
|
||||
form.setInputValue('myField', 'Mike');
|
||||
|
@ -224,9 +224,9 @@ describe('<UseField />', () => {
|
|||
expect(formatter).toBeCalled(); // Formatters are executed on each value change
|
||||
expect(serializer).not.toBeCalled(); // Serializer are executed *only** when outputting the form data
|
||||
|
||||
formData = formHook.getFormData();
|
||||
const outputtedFormData = formHook.getFormData();
|
||||
expect(serializer).toBeCalled();
|
||||
expect(formData.name).toEqual('MIKE-serialized');
|
||||
expect(outputtedFormData.name).toEqual('MIKE-serialized');
|
||||
|
||||
// Make sure that when we reset the form values, we don't serialize the fields
|
||||
serializer.mockReset();
|
||||
|
|
|
@ -22,9 +22,9 @@ import React, { createContext, useContext, useMemo } from 'react';
|
|||
import { FormData, FormHook } from './types';
|
||||
import { Subject } from './lib';
|
||||
|
||||
export interface Context<T extends FormData = FormData, I = T> {
|
||||
getFormData$: () => Subject<I>;
|
||||
getFormData: FormHook<T>['getFormData'];
|
||||
export interface Context<T extends FormData = FormData, I extends FormData = T> {
|
||||
getFormData$: () => Subject<FormData>;
|
||||
getFormData: FormHook<T, I>['getFormData'];
|
||||
}
|
||||
|
||||
const FormDataContext = createContext<Context<any> | undefined>(undefined);
|
||||
|
@ -45,6 +45,6 @@ export const FormDataContextProvider = ({ children, getFormData$, getFormData }:
|
|||
return <FormDataContext.Provider value={value}>{children}</FormDataContext.Provider>;
|
||||
};
|
||||
|
||||
export function useFormDataContext<T extends FormData = FormData>() {
|
||||
return useContext<Context<T> | undefined>(FormDataContext);
|
||||
export function useFormDataContext<T extends FormData = FormData, I extends FormData = T>() {
|
||||
return useContext<Context<T, I> | undefined>(FormDataContext);
|
||||
}
|
||||
|
|
|
@ -63,6 +63,7 @@ export const useField = <T, FormType = FormData, I = T>(
|
|||
__removeField,
|
||||
__updateFormDataAt,
|
||||
__validateFields,
|
||||
__getFormData$,
|
||||
} = form;
|
||||
|
||||
const deserializeValue = useCallback(
|
||||
|
@ -76,7 +77,7 @@ export const useField = <T, FormType = FormData, I = T>(
|
|||
);
|
||||
|
||||
const [value, setStateValue] = useState<I>(deserializeValue);
|
||||
const [errors, setErrors] = useState<ValidationError[]>([]);
|
||||
const [errors, setStateErrors] = useState<ValidationError[]>([]);
|
||||
const [isPristine, setPristine] = useState(true);
|
||||
const [isValidating, setValidating] = useState(false);
|
||||
const [isChangingValue, setIsChangingValue] = useState(false);
|
||||
|
@ -86,18 +87,12 @@ export const useField = <T, FormType = FormData, I = T>(
|
|||
const validateCounter = useRef(0);
|
||||
const changeCounter = useRef(0);
|
||||
const hasBeenReset = useRef<boolean>(false);
|
||||
const inflightValidation = useRef<Promise<any> | null>(null);
|
||||
const inflightValidation = useRef<(Promise<any> & { cancel?(): void }) | null>(null);
|
||||
const debounceTimeout = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
// ----------------------------------
|
||||
// -- HELPERS
|
||||
// ----------------------------------
|
||||
const serializeValue: FieldHook<T, I>['__serializeValue'] = useCallback(
|
||||
(internalValue: I = value) => {
|
||||
return serializer ? serializer(internalValue) : ((internalValue as unknown) as T);
|
||||
},
|
||||
[serializer, value]
|
||||
);
|
||||
|
||||
/**
|
||||
* Filter an array of errors with specific validation type on them
|
||||
*
|
||||
|
@ -117,6 +112,11 @@ export const useField = <T, FormType = FormData, I = T>(
|
|||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* If the field has some "formatters" defined in its config, run them in series and return
|
||||
* the transformed value. This handler is called whenever the field value changes, right before
|
||||
* updating the "value" state.
|
||||
*/
|
||||
const formatInputValue = useCallback(
|
||||
<T>(inputValue: unknown): T => {
|
||||
const isEmptyString = typeof inputValue === 'string' && inputValue.trim() === '';
|
||||
|
@ -125,11 +125,11 @@ export const useField = <T, FormType = FormData, I = T>(
|
|||
return inputValue as T;
|
||||
}
|
||||
|
||||
const formData = getFormData({ unflatten: false });
|
||||
const formData = __getFormData$().value;
|
||||
|
||||
return formatters.reduce((output, formatter) => formatter(output, formData), inputValue) as T;
|
||||
},
|
||||
[formatters, getFormData]
|
||||
[formatters, __getFormData$]
|
||||
);
|
||||
|
||||
const onValueChange = useCallback(async () => {
|
||||
|
@ -147,7 +147,7 @@ export const useField = <T, FormType = FormData, I = T>(
|
|||
// Update the form data observable
|
||||
__updateFormDataAt(path, value);
|
||||
|
||||
// Validate field(s) (that will update form.isValid state)
|
||||
// Validate field(s) (this will update the form.isValid state)
|
||||
await __validateFields(fieldsToValidateOnChange ?? [path]);
|
||||
|
||||
if (isMounted.current === false) {
|
||||
|
@ -162,15 +162,18 @@ export const useField = <T, FormType = FormData, I = T>(
|
|||
*/
|
||||
if (changeIteration === changeCounter.current) {
|
||||
if (valueChangeDebounceTime > 0) {
|
||||
const delta = Date.now() - startTime;
|
||||
if (delta < valueChangeDebounceTime) {
|
||||
const timeElapsed = Date.now() - startTime;
|
||||
|
||||
if (timeElapsed < valueChangeDebounceTime) {
|
||||
const timeLeftToWait = valueChangeDebounceTime - timeElapsed;
|
||||
debounceTimeout.current = setTimeout(() => {
|
||||
debounceTimeout.current = null;
|
||||
setIsChangingValue(false);
|
||||
}, valueChangeDebounceTime - delta);
|
||||
}, timeLeftToWait);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setIsChangingValue(false);
|
||||
}
|
||||
}, [
|
||||
|
@ -183,41 +186,34 @@ export const useField = <T, FormType = FormData, I = T>(
|
|||
__validateFields,
|
||||
]);
|
||||
|
||||
// Cancel any inflight validation (e.g an HTTP Request)
|
||||
const cancelInflightValidation = useCallback(() => {
|
||||
// Cancel any inflight validation (like an HTTP Request)
|
||||
if (
|
||||
inflightValidation.current &&
|
||||
typeof (inflightValidation.current as any).cancel === 'function'
|
||||
) {
|
||||
(inflightValidation.current as any).cancel();
|
||||
if (inflightValidation.current && typeof inflightValidation.current.cancel === 'function') {
|
||||
inflightValidation.current.cancel();
|
||||
inflightValidation.current = null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const clearErrors: FieldHook['clearErrors'] = useCallback(
|
||||
(validationType = VALIDATION_TYPES.FIELD) => {
|
||||
setErrors((previousErrors) => filterErrors(previousErrors, validationType));
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const runValidations = useCallback(
|
||||
({
|
||||
formData,
|
||||
value: valueToValidate,
|
||||
validationTypeToValidate,
|
||||
}: {
|
||||
formData: any;
|
||||
value: I;
|
||||
validationTypeToValidate?: string;
|
||||
}): ValidationError[] | Promise<ValidationError[]> => {
|
||||
(
|
||||
{
|
||||
formData,
|
||||
value: valueToValidate,
|
||||
validationTypeToValidate,
|
||||
}: {
|
||||
formData: any;
|
||||
value: I;
|
||||
validationTypeToValidate?: string;
|
||||
},
|
||||
clearFieldErrors: FieldHook['clearErrors']
|
||||
): ValidationError[] | Promise<ValidationError[]> => {
|
||||
if (!validations) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// By default, for fields that have an asynchronous validation
|
||||
// we will clear the errors as soon as the field value changes.
|
||||
clearErrors([VALIDATION_TYPES.FIELD, VALIDATION_TYPES.ASYNC]);
|
||||
clearFieldErrors([VALIDATION_TYPES.FIELD, VALIDATION_TYPES.ASYNC]);
|
||||
|
||||
cancelInflightValidation();
|
||||
|
||||
|
@ -329,21 +325,33 @@ export const useField = <T, FormType = FormData, I = T>(
|
|||
// We first try to run the validations synchronously
|
||||
return runSync();
|
||||
},
|
||||
[clearErrors, cancelInflightValidation, validations, getFormData, getFields, path]
|
||||
[cancelInflightValidation, validations, getFormData, getFields, path]
|
||||
);
|
||||
|
||||
// -- API
|
||||
// ----------------------------------
|
||||
// -- Internal API
|
||||
// ----------------------------------
|
||||
const serializeValue: FieldHook<T, I>['__serializeValue'] = useCallback(
|
||||
(internalValue: I = value) => {
|
||||
return serializer ? serializer(internalValue) : ((internalValue as unknown) as T);
|
||||
},
|
||||
[serializer, value]
|
||||
);
|
||||
|
||||
// ----------------------------------
|
||||
// -- Public API
|
||||
// ----------------------------------
|
||||
const clearErrors: FieldHook['clearErrors'] = useCallback(
|
||||
(validationType = VALIDATION_TYPES.FIELD) => {
|
||||
setStateErrors((previousErrors) => filterErrors(previousErrors, validationType));
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
/**
|
||||
* Validate a form field, running all its validations.
|
||||
* If a validationType is provided then only that validation will be executed,
|
||||
* skipping the other type of validation that might exist.
|
||||
*/
|
||||
const validate: FieldHook<T, I>['validate'] = useCallback(
|
||||
(validationData = {}) => {
|
||||
const {
|
||||
formData = getFormData({ unflatten: false }),
|
||||
formData = __getFormData$().value,
|
||||
value: valueToValidate = value,
|
||||
validationType,
|
||||
} = validationData;
|
||||
|
@ -362,7 +370,7 @@ export const useField = <T, FormType = FormData, I = T>(
|
|||
// This is the most recent invocation
|
||||
setValidating(false);
|
||||
// Update the errors array
|
||||
setErrors((prev) => {
|
||||
setStateErrors((prev) => {
|
||||
const filteredErrors = filterErrors(prev, validationType);
|
||||
return [...filteredErrors, ..._validationErrors];
|
||||
});
|
||||
|
@ -374,25 +382,23 @@ export const useField = <T, FormType = FormData, I = T>(
|
|||
};
|
||||
};
|
||||
|
||||
const validationErrors = runValidations({
|
||||
formData,
|
||||
value: valueToValidate,
|
||||
validationTypeToValidate: validationType,
|
||||
});
|
||||
const validationErrors = runValidations(
|
||||
{
|
||||
formData,
|
||||
value: valueToValidate,
|
||||
validationTypeToValidate: validationType,
|
||||
},
|
||||
clearErrors
|
||||
);
|
||||
|
||||
if (Reflect.has(validationErrors, 'then')) {
|
||||
return (validationErrors as Promise<ValidationError[]>).then(onValidationResult);
|
||||
}
|
||||
return onValidationResult(validationErrors as ValidationError[]);
|
||||
},
|
||||
[getFormData, value, runValidations]
|
||||
[__getFormData$, value, runValidations, clearErrors]
|
||||
);
|
||||
|
||||
/**
|
||||
* Handler to change the field value
|
||||
*
|
||||
* @param newValue The new value to assign to the field
|
||||
*/
|
||||
const setValue: FieldHook<T, I>['setValue'] = useCallback(
|
||||
(newValue) => {
|
||||
setStateValue((prev) => {
|
||||
|
@ -408,8 +414,8 @@ export const useField = <T, FormType = FormData, I = T>(
|
|||
[formatInputValue]
|
||||
);
|
||||
|
||||
const _setErrors: FieldHook<T, I>['setErrors'] = useCallback((_errors) => {
|
||||
setErrors(
|
||||
const setErrors: FieldHook<T, I>['setErrors'] = useCallback((_errors) => {
|
||||
setStateErrors(
|
||||
_errors.map((error) => ({
|
||||
validationType: VALIDATION_TYPES.FIELD,
|
||||
__isBlocking__: true,
|
||||
|
@ -418,11 +424,6 @@ export const useField = <T, FormType = FormData, I = T>(
|
|||
);
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Form <input /> "onChange" event handler
|
||||
*
|
||||
* @param event Form input change event
|
||||
*/
|
||||
const onChange: FieldHook<T, I>['onChange'] = useCallback(
|
||||
(event) => {
|
||||
const newValue = {}.hasOwnProperty.call(event!.target, 'checked')
|
||||
|
@ -485,7 +486,7 @@ export const useField = <T, FormType = FormData, I = T>(
|
|||
case 'value':
|
||||
return setValue(nextValue);
|
||||
case 'errors':
|
||||
return setErrors(nextValue);
|
||||
return setStateErrors(nextValue);
|
||||
case 'isChangingValue':
|
||||
return setIsChangingValue(nextValue);
|
||||
case 'isPristine':
|
||||
|
@ -539,7 +540,7 @@ export const useField = <T, FormType = FormData, I = T>(
|
|||
onChange,
|
||||
getErrorsMessages,
|
||||
setValue,
|
||||
setErrors: _setErrors,
|
||||
setErrors,
|
||||
clearErrors,
|
||||
validate,
|
||||
reset,
|
||||
|
@ -563,7 +564,7 @@ export const useField = <T, FormType = FormData, I = T>(
|
|||
onChange,
|
||||
getErrorsMessages,
|
||||
setValue,
|
||||
_setErrors,
|
||||
setErrors,
|
||||
clearErrors,
|
||||
validate,
|
||||
reset,
|
||||
|
@ -585,7 +586,8 @@ export const useField = <T, FormType = FormData, I = T>(
|
|||
|
||||
useEffect(() => {
|
||||
// If the field value has been reset, we don't want to call the "onValueChange()"
|
||||
// as it will set the "isPristine" state to true or validate the field, which initially we don't want.
|
||||
// as it will set the "isPristine" state to true or validate the field, which we don't want
|
||||
// to occur right after resetting the field state.
|
||||
if (hasBeenReset.current) {
|
||||
hasBeenReset.current = false;
|
||||
return;
|
||||
|
|
|
@ -211,7 +211,13 @@ describe('useForm() hook', () => {
|
|||
|
||||
test('should allow subscribing to the form data changes and provide a handler to build the form data', async () => {
|
||||
const TestComp = ({ onData }: { onData: OnUpdateHandler }) => {
|
||||
const { form } = useForm();
|
||||
const { form } = useForm({
|
||||
serializer: (value) => ({
|
||||
user: {
|
||||
name: value.user.name.toUpperCase(),
|
||||
},
|
||||
}),
|
||||
});
|
||||
const { subscribe } = form;
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -253,8 +259,9 @@ describe('useForm() hook', () => {
|
|||
OnUpdateHandler
|
||||
>;
|
||||
|
||||
expect(data.raw).toEqual({ 'user.name': 'John' });
|
||||
expect(data.format()).toEqual({ user: { name: 'John' } });
|
||||
expect(data.internal).toEqual({ user: { name: 'John' } });
|
||||
// Transform name to uppercase as decalred in our serializer func
|
||||
expect(data.format()).toEqual({ user: { name: 'JOHN' } });
|
||||
// As we have touched all fields, the validity went from "undefined" to "true"
|
||||
expect(isValid).toBe(true);
|
||||
});
|
||||
|
@ -302,10 +309,12 @@ describe('useForm() hook', () => {
|
|||
OnUpdateHandler
|
||||
>;
|
||||
|
||||
expect(data.raw).toEqual({
|
||||
expect(data.internal).toEqual({
|
||||
title: defaultValue.title,
|
||||
subTitle: 'hasBeenOverridden',
|
||||
'user.name': defaultValue.user.name,
|
||||
user: {
|
||||
name: defaultValue.user.name,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -58,8 +58,6 @@ export function useForm<T extends FormData = FormData, I extends FormData = T>(
|
|||
return initDefaultValue(defaultValue);
|
||||
}, [defaultValue, initDefaultValue]);
|
||||
|
||||
const defaultValueDeserialized = useRef(defaultValueMemoized);
|
||||
|
||||
const { valueChangeDebounceTime, stripEmptyFields: doStripEmptyFields } = options ?? {};
|
||||
const formOptions = useMemo(
|
||||
() => ({
|
||||
|
@ -72,26 +70,36 @@ export function useForm<T extends FormData = FormData, I extends FormData = T>(
|
|||
const [isSubmitted, setIsSubmitted] = useState(false);
|
||||
const [isSubmitting, setSubmitting] = useState(false);
|
||||
const [isValid, setIsValid] = useState<boolean | undefined>(undefined);
|
||||
|
||||
const fieldsRefs = useRef<FieldsMap>({});
|
||||
const formUpdateSubscribers = useRef<Subscription[]>([]);
|
||||
const isMounted = useRef<boolean>(false);
|
||||
const defaultValueDeserialized = useRef(defaultValueMemoized);
|
||||
|
||||
// formData$ is an observable we can subscribe to in order to receive live
|
||||
// update of the raw form data. As an observable it does not trigger any React
|
||||
// render().
|
||||
// The <FormDataProvider> component is the one in charge of reading this observable
|
||||
// and updating its state to trigger the necessary view render.
|
||||
const formData$ = useRef<Subject<T> | null>(null);
|
||||
// The "useFormData()" hook is the one in charge of reading this observable
|
||||
// and updating its own state that will trigger the necessary re-renders in the UI.
|
||||
const formData$ = useRef<Subject<FormData> | null>(null);
|
||||
|
||||
// ----------------------------------
|
||||
// -- HELPERS
|
||||
// ----------------------------------
|
||||
const getFormData$ = useCallback((): Subject<T> => {
|
||||
const getFormData$ = useCallback((): Subject<FormData> => {
|
||||
if (formData$.current === null) {
|
||||
formData$.current = new Subject<T>({} as T);
|
||||
formData$.current = new Subject<FormData>({});
|
||||
}
|
||||
return formData$.current;
|
||||
}, []);
|
||||
|
||||
const updateFormData$ = useCallback(
|
||||
(nextValue: FormData) => {
|
||||
getFormData$().next(nextValue);
|
||||
},
|
||||
[getFormData$]
|
||||
);
|
||||
|
||||
const fieldsToArray = useCallback<() => FieldHook[]>(() => Object.values(fieldsRefs.current), []);
|
||||
|
||||
const getFieldsForOutput = useCallback(
|
||||
|
@ -115,63 +123,24 @@ export function useForm<T extends FormData = FormData, I extends FormData = T>(
|
|||
[]
|
||||
);
|
||||
|
||||
const updateFormDataAt: FormHook<T>['__updateFormDataAt'] = useCallback(
|
||||
const updateFormDataAt: FormHook<T, I>['__updateFormDataAt'] = useCallback(
|
||||
(path, value) => {
|
||||
const _formData$ = getFormData$();
|
||||
const currentFormData = _formData$.value;
|
||||
const currentFormData = getFormData$().value;
|
||||
|
||||
if (currentFormData[path] !== value) {
|
||||
_formData$.next({ ...currentFormData, [path]: value });
|
||||
updateFormData$({ ...currentFormData, [path]: value });
|
||||
}
|
||||
|
||||
return _formData$.value;
|
||||
},
|
||||
[getFormData$]
|
||||
[getFormData$, updateFormData$]
|
||||
);
|
||||
|
||||
const updateDefaultValueAt: FormHook<T>['__updateDefaultValueAt'] = useCallback((path, value) => {
|
||||
set(defaultValueDeserialized.current, path, value);
|
||||
}, []);
|
||||
|
||||
// -- API
|
||||
// ----------------------------------
|
||||
const getFormData: FormHook<T>['getFormData'] = useCallback(
|
||||
(getDataOptions: Parameters<FormHook<T>['getFormData']>[0] = { unflatten: true }) => {
|
||||
if (getDataOptions.unflatten) {
|
||||
const fieldsToOutput = getFieldsForOutput(fieldsRefs.current, {
|
||||
stripEmptyFields: formOptions.stripEmptyFields,
|
||||
});
|
||||
const fieldsValue = mapFormFields(fieldsToOutput, (field) => field.__serializeValue());
|
||||
return serializer
|
||||
? (serializer(unflattenObject(fieldsValue) as I) as T)
|
||||
: (unflattenObject(fieldsValue) as T);
|
||||
}
|
||||
|
||||
return Object.entries(fieldsRefs.current).reduce(
|
||||
(acc, [key, field]) => ({
|
||||
...acc,
|
||||
[key]: field.value,
|
||||
}),
|
||||
{} as T
|
||||
);
|
||||
const updateDefaultValueAt: FormHook<T, I>['__updateDefaultValueAt'] = useCallback(
|
||||
(path, value) => {
|
||||
set(defaultValueDeserialized.current, path, value);
|
||||
},
|
||||
[getFieldsForOutput, formOptions.stripEmptyFields, serializer]
|
||||
[]
|
||||
);
|
||||
|
||||
const getErrors: FormHook['getErrors'] = useCallback(() => {
|
||||
if (isValid === true) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return fieldsToArray().reduce((acc, field) => {
|
||||
const fieldError = field.getErrorsMessages();
|
||||
if (fieldError === null) {
|
||||
return acc;
|
||||
}
|
||||
return [...acc, fieldError];
|
||||
}, [] as string[]);
|
||||
}, [isValid, fieldsToArray]);
|
||||
|
||||
const isFieldValid = (field: FieldHook) => field.isValid && !field.isValidating;
|
||||
|
||||
const waitForFieldsToFinishValidating = useCallback(async () => {
|
||||
|
@ -192,13 +161,13 @@ export function useForm<T extends FormData = FormData, I extends FormData = T>(
|
|||
});
|
||||
}, [fieldsToArray]);
|
||||
|
||||
const validateFields: FormHook<T>['__validateFields'] = useCallback(
|
||||
const validateFields: FormHook<T, I>['__validateFields'] = useCallback(
|
||||
async (fieldNames) => {
|
||||
const fieldsToValidate = fieldNames
|
||||
.map((name) => fieldsRefs.current[name])
|
||||
.filter((field) => field !== undefined);
|
||||
|
||||
const formData = getFormData({ unflatten: false });
|
||||
const formData = getFormData$().value;
|
||||
const validationResult = await Promise.all(
|
||||
fieldsToValidate.map((field) => field.validate({ formData }))
|
||||
);
|
||||
|
@ -245,10 +214,101 @@ export function useForm<T extends FormData = FormData, I extends FormData = T>(
|
|||
|
||||
return { areFieldsValid, isFormValid };
|
||||
},
|
||||
[getFormData, fieldsToArray]
|
||||
[getFormData$, fieldsToArray]
|
||||
);
|
||||
|
||||
const validateAllFields = useCallback(async (): Promise<boolean> => {
|
||||
// ----------------------------------
|
||||
// -- Internal API
|
||||
// ----------------------------------
|
||||
const addField: FormHook<T, I>['__addField'] = useCallback(
|
||||
(field) => {
|
||||
fieldsRefs.current[field.path] = field;
|
||||
|
||||
updateFormDataAt(field.path, field.value);
|
||||
|
||||
if (!field.isValidated) {
|
||||
setIsValid(undefined);
|
||||
|
||||
// When we submit the form (and set "isSubmitted" to "true"), we validate **all fields**.
|
||||
// If a field is added and it is not validated it means that we have swapped fields and added new ones:
|
||||
// --> we have basically have a new form in front of us.
|
||||
// For that reason we make sure that the "isSubmitted" state is false.
|
||||
setIsSubmitted(false);
|
||||
}
|
||||
},
|
||||
[updateFormDataAt]
|
||||
);
|
||||
|
||||
const removeField: FormHook<T, I>['__removeField'] = useCallback(
|
||||
(_fieldNames) => {
|
||||
const fieldNames = Array.isArray(_fieldNames) ? _fieldNames : [_fieldNames];
|
||||
const currentFormData = { ...getFormData$().value };
|
||||
|
||||
fieldNames.forEach((name) => {
|
||||
delete fieldsRefs.current[name];
|
||||
delete currentFormData[name];
|
||||
});
|
||||
|
||||
updateFormData$(currentFormData);
|
||||
|
||||
/**
|
||||
* After removing a field, the form validity might have changed
|
||||
* (an invalid field might have been removed and now the form is valid)
|
||||
*/
|
||||
setIsValid((prev) => {
|
||||
if (prev === false) {
|
||||
const isFormValid = fieldsToArray().every(isFieldValid);
|
||||
return isFormValid;
|
||||
}
|
||||
// If the form validity is "true" or "undefined", it does not change after removing a field
|
||||
return prev;
|
||||
});
|
||||
},
|
||||
[getFormData$, updateFormData$, fieldsToArray]
|
||||
);
|
||||
|
||||
const getFieldDefaultValue: FormHook<T, I>['__getFieldDefaultValue'] = useCallback(
|
||||
(fieldName) => get(defaultValueDeserialized.current, fieldName),
|
||||
[]
|
||||
);
|
||||
|
||||
const readFieldConfigFromSchema: FormHook<T, I>['__readFieldConfigFromSchema'] = useCallback(
|
||||
(fieldName) => {
|
||||
const config = (get(schema ?? {}, fieldName) as FieldConfig) || {};
|
||||
|
||||
return config;
|
||||
},
|
||||
[schema]
|
||||
);
|
||||
|
||||
// ----------------------------------
|
||||
// -- Public API
|
||||
// ----------------------------------
|
||||
const getFormData: FormHook<T, I>['getFormData'] = useCallback(() => {
|
||||
const fieldsToOutput = getFieldsForOutput(fieldsRefs.current, {
|
||||
stripEmptyFields: formOptions.stripEmptyFields,
|
||||
});
|
||||
const fieldsValue = mapFormFields(fieldsToOutput, (field) => field.__serializeValue());
|
||||
return serializer
|
||||
? serializer(unflattenObject<I>(fieldsValue))
|
||||
: unflattenObject<T>(fieldsValue);
|
||||
}, [getFieldsForOutput, formOptions.stripEmptyFields, serializer]);
|
||||
|
||||
const getErrors: FormHook<T, I>['getErrors'] = useCallback(() => {
|
||||
if (isValid === true) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return fieldsToArray().reduce((acc, field) => {
|
||||
const fieldError = field.getErrorsMessages();
|
||||
if (fieldError === null) {
|
||||
return acc;
|
||||
}
|
||||
return [...acc, fieldError];
|
||||
}, [] as string[]);
|
||||
}, [isValid, fieldsToArray]);
|
||||
|
||||
const validate: FormHook<T, I>['validate'] = useCallback(async (): Promise<boolean> => {
|
||||
// Maybe some field are being validated because of their async validation(s).
|
||||
// We make sure those validations have finished executing before proceeding.
|
||||
await waitForFieldsToFinishValidating();
|
||||
|
@ -272,84 +332,23 @@ export function useForm<T extends FormData = FormData, I extends FormData = T>(
|
|||
return isFormValid!;
|
||||
}, [fieldsToArray, validateFields, waitForFieldsToFinishValidating]);
|
||||
|
||||
const addField: FormHook<T>['__addField'] = useCallback(
|
||||
(field) => {
|
||||
fieldsRefs.current[field.path] = field;
|
||||
|
||||
updateFormDataAt(field.path, field.value);
|
||||
|
||||
if (!field.isValidated) {
|
||||
setIsValid(undefined);
|
||||
|
||||
// When we submit the form (and set "isSubmitted" to "true"), we validate **all fields**.
|
||||
// If a field is added and it is not validated it means that we have swapped fields and added new ones:
|
||||
// --> we have basically have a new form in front of us.
|
||||
// For that reason we make sure that the "isSubmitted" state is false.
|
||||
setIsSubmitted(false);
|
||||
}
|
||||
},
|
||||
[updateFormDataAt]
|
||||
);
|
||||
|
||||
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];
|
||||
});
|
||||
|
||||
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)
|
||||
*/
|
||||
setIsValid((prev) => {
|
||||
if (prev === false) {
|
||||
const isFormValid = fieldsToArray().every(isFieldValid);
|
||||
return isFormValid;
|
||||
}
|
||||
// If the form validity is "true" or "undefined", it does not change after removing a field
|
||||
return prev;
|
||||
});
|
||||
},
|
||||
[getFormData$, fieldsToArray]
|
||||
);
|
||||
|
||||
const setFieldValue: FormHook<T>['setFieldValue'] = useCallback((fieldName, value) => {
|
||||
const setFieldValue: FormHook<T, I>['setFieldValue'] = useCallback((fieldName, value) => {
|
||||
if (fieldsRefs.current[fieldName] === undefined) {
|
||||
return;
|
||||
}
|
||||
fieldsRefs.current[fieldName].setValue(value);
|
||||
}, []);
|
||||
|
||||
const setFieldErrors: FormHook<T>['setFieldErrors'] = useCallback((fieldName, errors) => {
|
||||
const setFieldErrors: FormHook<T, I>['setFieldErrors'] = useCallback((fieldName, errors) => {
|
||||
if (fieldsRefs.current[fieldName] === undefined) {
|
||||
return;
|
||||
}
|
||||
fieldsRefs.current[fieldName].setErrors(errors);
|
||||
}, []);
|
||||
|
||||
const getFields: FormHook<T>['getFields'] = useCallback(() => fieldsRefs.current, []);
|
||||
const getFields: FormHook<T, I>['getFields'] = useCallback(() => fieldsRefs.current, []);
|
||||
|
||||
const getFieldDefaultValue: FormHook['__getFieldDefaultValue'] = useCallback(
|
||||
(fieldName) => get(defaultValueDeserialized.current, fieldName),
|
||||
[]
|
||||
);
|
||||
|
||||
const readFieldConfigFromSchema: FormHook<T>['__readFieldConfigFromSchema'] = useCallback(
|
||||
(fieldName) => {
|
||||
const config = (get(schema ?? {}, fieldName) as FieldConfig) || {};
|
||||
|
||||
return config;
|
||||
},
|
||||
[schema]
|
||||
);
|
||||
|
||||
const submitForm: FormHook<T>['submit'] = useCallback(
|
||||
const submit: FormHook<T, I>['submit'] = useCallback(
|
||||
async (e) => {
|
||||
if (e) {
|
||||
e.preventDefault();
|
||||
|
@ -358,7 +357,7 @@ export function useForm<T extends FormData = FormData, I extends FormData = T>(
|
|||
setIsSubmitted(true); // User has attempted to submit the form at least once
|
||||
setSubmitting(true);
|
||||
|
||||
const isFormValid = await validateAllFields();
|
||||
const isFormValid = await validate();
|
||||
const formData = isFormValid ? getFormData() : ({} as T);
|
||||
|
||||
if (onSubmit) {
|
||||
|
@ -371,13 +370,17 @@ export function useForm<T extends FormData = FormData, I extends FormData = T>(
|
|||
|
||||
return { data: formData, isValid: isFormValid! };
|
||||
},
|
||||
[validateAllFields, getFormData, onSubmit]
|
||||
[validate, getFormData, onSubmit]
|
||||
);
|
||||
|
||||
const subscribe: FormHook<T>['subscribe'] = useCallback(
|
||||
const subscribe: FormHook<T, I>['subscribe'] = useCallback(
|
||||
(handler) => {
|
||||
const subscription = getFormData$().subscribe((raw) => {
|
||||
handler({ isValid, data: { raw, format: getFormData }, validate: validateAllFields });
|
||||
handler({
|
||||
isValid,
|
||||
data: { internal: unflattenObject<I>(raw), format: getFormData },
|
||||
validate,
|
||||
});
|
||||
});
|
||||
|
||||
formUpdateSubscribers.current.push(subscription);
|
||||
|
@ -391,17 +394,13 @@ export function useForm<T extends FormData = FormData, I extends FormData = T>(
|
|||
},
|
||||
};
|
||||
},
|
||||
[getFormData$, isValid, getFormData, validateAllFields]
|
||||
[getFormData$, isValid, getFormData, validate]
|
||||
);
|
||||
|
||||
/**
|
||||
* Reset all the fields of the form to their default values
|
||||
* and reset all the states to their original value.
|
||||
*/
|
||||
const reset: FormHook<T>['reset'] = useCallback(
|
||||
const reset: FormHook<T, I>['reset'] = useCallback(
|
||||
(resetOptions = { resetValues: true }) => {
|
||||
const { resetValues = true, defaultValue: updatedDefaultValue } = resetOptions;
|
||||
const currentFormData = { ...getFormData$().value } as FormData;
|
||||
const currentFormData = { ...getFormData$().value };
|
||||
|
||||
if (updatedDefaultValue) {
|
||||
defaultValueDeserialized.current = initDefaultValue(updatedDefaultValue);
|
||||
|
@ -417,25 +416,26 @@ export function useForm<T extends FormData = FormData, I extends FormData = T>(
|
|||
currentFormData[path] = fieldDefaultValue;
|
||||
}
|
||||
});
|
||||
|
||||
if (resetValues) {
|
||||
getFormData$().next(currentFormData as T);
|
||||
updateFormData$(currentFormData);
|
||||
}
|
||||
|
||||
setIsSubmitted(false);
|
||||
setSubmitting(false);
|
||||
setIsValid(undefined);
|
||||
},
|
||||
[getFormData$, initDefaultValue, getFieldDefaultValue]
|
||||
[getFormData$, updateFormData$, initDefaultValue, getFieldDefaultValue]
|
||||
);
|
||||
|
||||
const form = useMemo<FormHook<T>>(() => {
|
||||
const form = useMemo<FormHook<T, I>>(() => {
|
||||
return {
|
||||
isSubmitted,
|
||||
isSubmitting,
|
||||
isValid,
|
||||
id,
|
||||
submit: submitForm,
|
||||
validate: validateAllFields,
|
||||
submit,
|
||||
validate,
|
||||
subscribe,
|
||||
setFieldValue,
|
||||
setFieldErrors,
|
||||
|
@ -458,7 +458,7 @@ export function useForm<T extends FormData = FormData, I extends FormData = T>(
|
|||
isSubmitting,
|
||||
isValid,
|
||||
id,
|
||||
submitForm,
|
||||
submit,
|
||||
subscribe,
|
||||
setFieldValue,
|
||||
setFieldErrors,
|
||||
|
@ -475,7 +475,7 @@ export function useForm<T extends FormData = FormData, I extends FormData = T>(
|
|||
addField,
|
||||
removeField,
|
||||
validateFields,
|
||||
validateAllFields,
|
||||
validate,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import React, { useEffect } from 'react';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
|
||||
import { registerTestBed, TestBed } from '../shared_imports';
|
||||
|
@ -25,37 +25,59 @@ import { Form, UseField } from '../components';
|
|||
import { useForm } from './use_form';
|
||||
import { useFormData, HookReturn } from './use_form_data';
|
||||
|
||||
interface Props {
|
||||
onChange(data: HookReturn): void;
|
||||
interface Props<T extends object> {
|
||||
onChange(data: HookReturn<T>): void;
|
||||
watch?: string | string[];
|
||||
}
|
||||
|
||||
interface Form1 {
|
||||
title: string;
|
||||
}
|
||||
|
||||
interface Form2 {
|
||||
user: {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface Form3 {
|
||||
title: string;
|
||||
subTitle: string;
|
||||
}
|
||||
|
||||
describe('useFormData() hook', () => {
|
||||
const HookListenerComp = React.memo(({ onChange, watch }: Props) => {
|
||||
const hookValue = useFormData({ watch });
|
||||
const HookListenerComp = function <T extends object>({ onChange, watch }: Props<T>) {
|
||||
const hookValue = useFormData<T>({ watch });
|
||||
const isMounted = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
onChange(hookValue);
|
||||
if (isMounted.current) {
|
||||
onChange(hookValue);
|
||||
}
|
||||
isMounted.current = true;
|
||||
}, [hookValue, onChange]);
|
||||
|
||||
return null;
|
||||
});
|
||||
};
|
||||
|
||||
const HookListener = React.memo(HookListenerComp);
|
||||
|
||||
describe('form data updates', () => {
|
||||
let testBed: TestBed;
|
||||
let onChangeSpy: jest.Mock;
|
||||
|
||||
const getLastMockValue = () => {
|
||||
return onChangeSpy.mock.calls[onChangeSpy.mock.calls.length - 1][0] as HookReturn;
|
||||
return onChangeSpy.mock.calls[onChangeSpy.mock.calls.length - 1][0] as HookReturn<Form1>;
|
||||
};
|
||||
|
||||
const TestComp = (props: Props) => {
|
||||
const { form } = useForm();
|
||||
const TestComp = (props: Props<Form1>) => {
|
||||
const { form } = useForm<Form1>();
|
||||
|
||||
return (
|
||||
<Form form={form}>
|
||||
<UseField path="title" defaultValue="titleInitialValue" data-test-subj="titleField" />
|
||||
<HookListenerComp {...props} />
|
||||
<HookListener {...props} />
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
@ -70,9 +92,7 @@ describe('useFormData() hook', () => {
|
|||
});
|
||||
|
||||
test('should return the form data', () => {
|
||||
// Called twice:
|
||||
// once when the hook is called and once when the fields have mounted and updated the form data
|
||||
expect(onChangeSpy).toBeCalledTimes(2);
|
||||
expect(onChangeSpy).toBeCalledTimes(1);
|
||||
const [data] = getLastMockValue();
|
||||
expect(data).toEqual({ title: 'titleInitialValue' });
|
||||
});
|
||||
|
@ -86,7 +106,7 @@ describe('useFormData() hook', () => {
|
|||
setInputValue('titleField', 'titleChanged');
|
||||
});
|
||||
|
||||
expect(onChangeSpy).toBeCalledTimes(3);
|
||||
expect(onChangeSpy).toBeCalledTimes(2);
|
||||
const [data] = getLastMockValue();
|
||||
expect(data).toEqual({ title: 'titleChanged' });
|
||||
});
|
||||
|
@ -96,17 +116,17 @@ describe('useFormData() hook', () => {
|
|||
let onChangeSpy: jest.Mock;
|
||||
|
||||
const getLastMockValue = () => {
|
||||
return onChangeSpy.mock.calls[onChangeSpy.mock.calls.length - 1][0] as HookReturn;
|
||||
return onChangeSpy.mock.calls[onChangeSpy.mock.calls.length - 1][0] as HookReturn<Form2>;
|
||||
};
|
||||
|
||||
const TestComp = (props: Props) => {
|
||||
const { form } = useForm();
|
||||
const TestComp = (props: Props<Form2>) => {
|
||||
const { form } = useForm<Form2>();
|
||||
|
||||
return (
|
||||
<Form form={form}>
|
||||
<UseField path="user.firstName" defaultValue="John" />
|
||||
<UseField path="user.lastName" defaultValue="Snow" />
|
||||
<HookListenerComp {...props} />
|
||||
<HookListener {...props} />
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
@ -121,8 +141,8 @@ describe('useFormData() hook', () => {
|
|||
});
|
||||
|
||||
test('should expose a handler to build the form data', () => {
|
||||
const { 1: format } = getLastMockValue();
|
||||
expect(format()).toEqual({
|
||||
const [formData] = getLastMockValue();
|
||||
expect(formData).toEqual({
|
||||
user: {
|
||||
firstName: 'John',
|
||||
lastName: 'Snow',
|
||||
|
@ -137,11 +157,11 @@ describe('useFormData() hook', () => {
|
|||
let onChangeSpy: jest.Mock;
|
||||
|
||||
const getLastMockValue = () => {
|
||||
return onChangeSpy.mock.calls[onChangeSpy.mock.calls.length - 1][0] as HookReturn;
|
||||
return onChangeSpy.mock.calls[onChangeSpy.mock.calls.length - 1][0] as HookReturn<Form3>;
|
||||
};
|
||||
|
||||
const TestComp = (props: Props) => {
|
||||
const { form } = useForm();
|
||||
const TestComp = (props: Props<Form3>) => {
|
||||
const { form } = useForm<Form3>();
|
||||
|
||||
return (
|
||||
<Form form={form}>
|
||||
|
@ -190,9 +210,9 @@ describe('useFormData() hook', () => {
|
|||
return onChangeSpy.mock.calls[onChangeSpy.mock.calls.length - 1][0] as HookReturn;
|
||||
};
|
||||
|
||||
const TestComp = ({ onChange }: Props) => {
|
||||
const TestComp = ({ onChange }: Props<Form1>) => {
|
||||
const { form } = useForm();
|
||||
const hookValue = useFormData({ form });
|
||||
const hookValue = useFormData<Form1>({ form });
|
||||
|
||||
useEffect(() => {
|
||||
onChange(hookValue);
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||
|
||||
import { FormData, FormHook } from '../types';
|
||||
import { unflattenObject } from '../lib';
|
||||
import { useFormDataContext, Context } from '../form_data_context';
|
||||
|
||||
interface Options {
|
||||
|
@ -26,14 +27,16 @@ interface Options {
|
|||
form?: FormHook<any>;
|
||||
}
|
||||
|
||||
export type HookReturn<T extends object = FormData> = [FormData, () => T, boolean];
|
||||
export type HookReturn<I extends object = FormData, T extends object = I> = [I, () => T, boolean];
|
||||
|
||||
export const useFormData = <T extends object = FormData>(options: Options = {}): HookReturn<T> => {
|
||||
export const useFormData = <I extends object = FormData, T extends object = I>(
|
||||
options: Options = {}
|
||||
): HookReturn<I, T> => {
|
||||
const { watch, form } = options;
|
||||
const ctx = useFormDataContext<T>();
|
||||
const ctx = useFormDataContext<T, I>();
|
||||
|
||||
let getFormData: Context<T>['getFormData'];
|
||||
let getFormData$: Context<T>['getFormData$'];
|
||||
let getFormData: Context<T, I>['getFormData'];
|
||||
let getFormData$: Context<T, I>['getFormData$'];
|
||||
|
||||
if (form !== undefined) {
|
||||
getFormData = form.getFormData;
|
||||
|
@ -50,30 +53,33 @@ export const useFormData = <T extends object = FormData>(options: Options = {}):
|
|||
|
||||
const previousRawData = useRef<FormData>(initialValue);
|
||||
const isMounted = useRef(false);
|
||||
const [formData, setFormData] = useState<FormData>(previousRawData.current);
|
||||
const [formData, setFormData] = useState<I>(() => unflattenObject<I>(previousRawData.current));
|
||||
|
||||
const formatFormData = useCallback(() => {
|
||||
return getFormData({ unflatten: true });
|
||||
}, [getFormData]);
|
||||
/**
|
||||
* We do want to offer to the consumer a handler to serialize the form data that changes each time
|
||||
* the formData **state** changes. This is why we added the "formData" dep to the array and added the eslint override.
|
||||
*/
|
||||
const serializer = useCallback(() => {
|
||||
return getFormData();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [getFormData, formData]);
|
||||
|
||||
useEffect(() => {
|
||||
const subscription = getFormData$().subscribe((raw) => {
|
||||
if (watch) {
|
||||
const valuesToWatchArray = Array.isArray(watch)
|
||||
? (watch as string[])
|
||||
: ([watch] as string[]);
|
||||
if (!isMounted.current && Object.keys(raw).length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
valuesToWatchArray.some(
|
||||
(value) => previousRawData.current[value] !== raw[value as keyof T]
|
||||
)
|
||||
) {
|
||||
if (watch) {
|
||||
const pathsToWatchArray: string[] = Array.isArray(watch) ? watch : [watch];
|
||||
|
||||
if (pathsToWatchArray.some((path) => previousRawData.current[path] !== raw[path])) {
|
||||
previousRawData.current = raw;
|
||||
// Only update the state if one of the field we watch has changed.
|
||||
setFormData(raw);
|
||||
setFormData(unflattenObject<I>(raw));
|
||||
}
|
||||
} else {
|
||||
setFormData(raw);
|
||||
setFormData(unflattenObject<I>(raw));
|
||||
}
|
||||
});
|
||||
return subscription.unsubscribe;
|
||||
|
@ -88,8 +94,8 @@ export const useFormData = <T extends object = FormData>(options: Options = {}):
|
|||
|
||||
if (!isMounted.current && Object.keys(formData).length === 0) {
|
||||
// No field has mounted yet
|
||||
return [formData, formatFormData, false];
|
||||
return [formData, serializer, false];
|
||||
}
|
||||
|
||||
return [formData, formatFormData, true];
|
||||
return [formData, serializer, true];
|
||||
};
|
||||
|
|
|
@ -20,25 +20,11 @@
|
|||
import { set } from '@elastic/safer-lodash-set';
|
||||
import { FieldHook } from '../types';
|
||||
|
||||
export const unflattenObject = (object: any) =>
|
||||
export const unflattenObject = <T extends object = { [key: string]: any }>(object: object): T =>
|
||||
Object.entries(object).reduce((acc, [key, value]) => {
|
||||
set(acc, key, value);
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
export const flattenObject = (
|
||||
object: Record<string, any>,
|
||||
to: Record<string, any> = {},
|
||||
paths: string[] = []
|
||||
): Record<string, any> =>
|
||||
Object.entries(object).reduce((acc, [key, value]) => {
|
||||
const updatedPaths = [...paths, key];
|
||||
if (value !== null && !Array.isArray(value) && typeof value === 'object') {
|
||||
return flattenObject(value, to, updatedPaths);
|
||||
}
|
||||
acc[updatedPaths.join('.')] = value;
|
||||
return acc;
|
||||
}, to);
|
||||
}, {} as T);
|
||||
|
||||
/**
|
||||
* Helper to map the object of fields to any of its value
|
||||
|
|
|
@ -40,33 +40,36 @@ export interface FormHook<T extends FormData = FormData, I extends FormData = T>
|
|||
submit: (e?: FormEvent<HTMLFormElement> | MouseEvent) => Promise<{ data: T; isValid: boolean }>;
|
||||
/** Use this handler to get the validity of the form. */
|
||||
validate: () => Promise<boolean>;
|
||||
subscribe: (handler: OnUpdateHandler<T>) => Subscription;
|
||||
subscribe: (handler: OnUpdateHandler<T, I>) => Subscription;
|
||||
/** Sets a field value imperatively. */
|
||||
setFieldValue: (fieldName: string, value: FieldValue) => void;
|
||||
/** Sets a field errors imperatively. */
|
||||
setFieldErrors: (fieldName: string, errors: ValidationError[]) => void;
|
||||
/** Access any field on the form. */
|
||||
/** Access the fields on the form. */
|
||||
getFields: () => FieldsMap;
|
||||
/**
|
||||
* Return the form data. It accepts an optional options object with an `unflatten` parameter (defaults to `true`).
|
||||
* If you are only interested in the raw form data, pass `unflatten: false` to the handler
|
||||
*/
|
||||
getFormData: (options?: { unflatten?: boolean }) => T;
|
||||
getFormData: () => T;
|
||||
/* Returns an array with of all errors in the form. */
|
||||
getErrors: () => string[];
|
||||
/** Resets the form to its initial state. */
|
||||
/**
|
||||
* Reset the form states to their initial value and optionally
|
||||
* all the fields to their initial values.
|
||||
*/
|
||||
reset: (options?: { resetValues?: boolean; defaultValue?: Partial<T> }) => void;
|
||||
readonly __options: Required<FormOptions>;
|
||||
__getFormData$: () => Subject<T>;
|
||||
__getFormData$: () => Subject<FormData>;
|
||||
__addField: (field: FieldHook) => void;
|
||||
__removeField: (fieldNames: string | string[]) => void;
|
||||
__validateFields: (
|
||||
fieldNames: string[]
|
||||
) => Promise<{ areFieldsValid: boolean; isFormValid: boolean | undefined }>;
|
||||
__updateFormDataAt: (field: string, value: unknown) => T;
|
||||
__updateFormDataAt: (field: string, value: unknown) => void;
|
||||
__updateDefaultValueAt: (field: string, value: unknown) => void;
|
||||
__readFieldConfigFromSchema: (fieldName: string) => FieldConfig;
|
||||
__getFieldDefaultValue: (fieldName: string) => unknown;
|
||||
__readFieldConfigFromSchema: (field: string) => FieldConfig;
|
||||
__getFieldDefaultValue: (path: string) => unknown;
|
||||
}
|
||||
|
||||
export type FormSchema<T extends FormData = FormData> = {
|
||||
|
@ -83,16 +86,18 @@ export interface FormConfig<T extends FormData = FormData, I extends FormData =
|
|||
id?: string;
|
||||
}
|
||||
|
||||
export interface OnFormUpdateArg<T extends FormData> {
|
||||
export interface OnFormUpdateArg<T extends FormData, I extends FormData = T> {
|
||||
data: {
|
||||
raw: { [key: string]: any };
|
||||
internal: I;
|
||||
format: () => T;
|
||||
};
|
||||
validate: () => Promise<boolean>;
|
||||
isValid?: boolean;
|
||||
}
|
||||
|
||||
export type OnUpdateHandler<T extends FormData = FormData> = (arg: OnFormUpdateArg<T>) => void;
|
||||
export type OnUpdateHandler<T extends FormData = FormData, I extends FormData = T> = (
|
||||
arg: OnFormUpdateArg<T, I>
|
||||
) => void;
|
||||
|
||||
export interface FormOptions {
|
||||
valueChangeDebounceTime?: number;
|
||||
|
@ -119,10 +124,26 @@ export interface FieldHook<T = unknown, I = T> {
|
|||
validationType?: 'field' | string;
|
||||
errorCode?: string;
|
||||
}) => string | null;
|
||||
/**
|
||||
* Form <input /> "onChange" event handler
|
||||
*
|
||||
* @param event Form input change event
|
||||
*/
|
||||
onChange: (event: ChangeEvent<{ name?: string; value: string; checked?: boolean }>) => void;
|
||||
/**
|
||||
* Handler to change the field value
|
||||
*
|
||||
* @param value The new value to assign to the field. If you provide a callback, you wil receive
|
||||
* the previous value and you need to return the next value.
|
||||
*/
|
||||
setValue: (value: I | ((prevValue: I) => I)) => void;
|
||||
setErrors: (errors: ValidationError[]) => void;
|
||||
clearErrors: (type?: string | string[]) => void;
|
||||
/**
|
||||
* Validate a form field, running all its validations.
|
||||
* If a validationType is provided then only that validation will be executed,
|
||||
* skipping the other type of validation that might exist.
|
||||
*/
|
||||
validate: (validateData?: {
|
||||
formData?: any;
|
||||
value?: I;
|
||||
|
@ -166,19 +187,23 @@ export interface ValidationError<T = string> {
|
|||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface ValidationFuncArg<T extends FormData, V = unknown> {
|
||||
export interface ValidationFuncArg<I extends FormData, V = unknown> {
|
||||
path: string;
|
||||
value: V;
|
||||
form: {
|
||||
getFormData: FormHook<T>['getFormData'];
|
||||
getFields: FormHook<T>['getFields'];
|
||||
getFormData: FormHook<FormData, I>['getFormData'];
|
||||
getFields: FormHook<FormData, I>['getFields'];
|
||||
};
|
||||
formData: T;
|
||||
formData: I;
|
||||
errors: readonly ValidationError[];
|
||||
}
|
||||
|
||||
export type ValidationFunc<T extends FormData = any, E extends string = string, V = unknown> = (
|
||||
data: ValidationFuncArg<T, V>
|
||||
export type ValidationFunc<
|
||||
I extends FormData = FormData,
|
||||
E extends string = string,
|
||||
V = unknown
|
||||
> = (
|
||||
data: ValidationFuncArg<I, V>
|
||||
) => ValidationError<E> | void | undefined | Promise<ValidationError<E> | void | undefined>;
|
||||
|
||||
export interface FieldValidateResponse {
|
||||
|
@ -199,11 +224,11 @@ type FormatterFunc = (value: any, formData: FormData) => unknown;
|
|||
type FieldValue = unknown;
|
||||
|
||||
export interface ValidationConfig<
|
||||
FormType extends FormData = any,
|
||||
Error extends string = string,
|
||||
ValueType = unknown
|
||||
I extends FormData = FormData,
|
||||
E extends string = string,
|
||||
V = unknown
|
||||
> {
|
||||
validator: ValidationFunc<FormType, Error, ValueType>;
|
||||
validator: ValidationFunc<I, E, V>;
|
||||
type?: string;
|
||||
/**
|
||||
* By default all validation are blockers, which means that if they fail, the field is invalid.
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
import React, { FunctionComponent, Fragment } from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { get } from 'lodash';
|
||||
|
||||
import { EuiFieldNumber, EuiDescribedFormGroup, EuiSwitch, EuiTextColor } from '@elastic/eui';
|
||||
|
||||
|
@ -56,10 +57,12 @@ export const ColdPhase: FunctionComponent<Props> = ({
|
|||
errors,
|
||||
isShowingErrors,
|
||||
}) => {
|
||||
const [{ [useRolloverPath]: hotPhaseRolloverEnabled }] = useFormData({
|
||||
const [formData] = useFormData({
|
||||
watch: [useRolloverPath],
|
||||
});
|
||||
|
||||
const hotPhaseRolloverEnabled = get(formData, useRolloverPath);
|
||||
|
||||
return (
|
||||
<div id="coldPhaseContent" aria-live="polite" role="region">
|
||||
<>
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
*/
|
||||
|
||||
import React, { FunctionComponent, Fragment } from 'react';
|
||||
import { get } from 'lodash';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { EuiDescribedFormGroup, EuiSwitch, EuiTextColor, EuiFormRow } from '@elastic/eui';
|
||||
|
||||
|
@ -48,10 +49,12 @@ export const DeletePhase: FunctionComponent<Props> = ({
|
|||
isShowingErrors,
|
||||
getUrlForApp,
|
||||
}) => {
|
||||
const [{ [useRolloverPath]: hotPhaseRolloverEnabled }] = useFormData({
|
||||
watch: [useRolloverPath],
|
||||
const [formData] = useFormData({
|
||||
watch: useRolloverPath,
|
||||
});
|
||||
|
||||
const hotPhaseRolloverEnabled = get(formData, useRolloverPath);
|
||||
|
||||
return (
|
||||
<div id="deletePhaseContent" aria-live="polite" role="region">
|
||||
<EuiDescribedFormGroup
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
*/
|
||||
|
||||
import React, { Fragment, FunctionComponent, useEffect, useState } from 'react';
|
||||
import { get } from 'lodash';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
|
@ -45,8 +46,11 @@ const hotProperty: keyof Phases = 'hot';
|
|||
export const HotPhase: FunctionComponent<{ setWarmPhaseOnRollover: (v: boolean) => void }> = ({
|
||||
setWarmPhaseOnRollover,
|
||||
}) => {
|
||||
const [{ [useRolloverPath]: isRolloverEnabled }] = useFormData({ watch: [useRolloverPath] });
|
||||
const form = useFormContext();
|
||||
const [formData] = useFormData({
|
||||
watch: useRolloverPath,
|
||||
});
|
||||
const isRolloverEnabled = get(formData, useRolloverPath);
|
||||
|
||||
const isShowingErrors = form.isValid === false;
|
||||
const [showEmptyRolloverFieldsError, setShowEmptyRolloverFieldsError] = useState(false);
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { get } from 'lodash';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { EuiDescribedFormGroup, EuiSpacer, EuiTextColor } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
|
@ -23,9 +23,11 @@ interface Props {
|
|||
const forceMergeEnabledPath = '_meta.hot.forceMergeEnabled';
|
||||
|
||||
export const Forcemerge: React.FunctionComponent<Props> = ({ phase }) => {
|
||||
const [{ [forceMergeEnabledPath]: forceMergeEnabled }] = useFormData({
|
||||
watch: [forceMergeEnabledPath],
|
||||
const [formData] = useFormData({
|
||||
watch: forceMergeEnabledPath,
|
||||
});
|
||||
const forceMergeEnabled = get(formData, forceMergeEnabledPath);
|
||||
|
||||
return (
|
||||
<EuiDescribedFormGroup
|
||||
title={
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
*/
|
||||
|
||||
import React, { Fragment, FunctionComponent } from 'react';
|
||||
import { get } from 'lodash';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
|
@ -73,9 +74,12 @@ export const WarmPhase: FunctionComponent<Props> = ({
|
|||
errors,
|
||||
isShowingErrors,
|
||||
}) => {
|
||||
const [{ [useRolloverPath]: hotPhaseRolloverEnabled }] = useFormData({
|
||||
watch: [useRolloverPath],
|
||||
const [formData] = useFormData({
|
||||
watch: useRolloverPath,
|
||||
});
|
||||
|
||||
const hotPhaseRolloverEnabled = get(formData, useRolloverPath);
|
||||
|
||||
return (
|
||||
<div id="warmPhaseContent" aria-live="polite" role="region" aria-relevant="additions">
|
||||
<>
|
||||
|
|
|
@ -125,9 +125,9 @@ export const SimulateTemplateFlyoutContent = ({
|
|||
|
||||
<EuiSpacer />
|
||||
|
||||
<FormDataProvider>
|
||||
<FormDataProvider<Filters>>
|
||||
{(formData) => {
|
||||
return <SimulateTemplate template={template} filters={formData as Filters} />;
|
||||
return <SimulateTemplate template={template} filters={formData} />;
|
||||
}}
|
||||
</FormDataProvider>
|
||||
</Form>
|
||||
|
|
|
@ -54,9 +54,7 @@ export const DynamicMappingSection = () => (
|
|||
<FormDataProvider pathsToWatch={['dynamicMapping.enabled', 'dynamicMapping.date_detection']}>
|
||||
{(formData) => {
|
||||
const {
|
||||
'dynamicMapping.enabled': enabled,
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
'dynamicMapping.date_detection': dateDetection,
|
||||
dynamicMapping: { enabled, date_detection: dateDetection },
|
||||
} = formData;
|
||||
|
||||
if (enabled === undefined) {
|
||||
|
|
|
@ -155,7 +155,9 @@ export const SourceFieldSection = () => {
|
|||
>
|
||||
<FormDataProvider pathsToWatch={['sourceField.enabled']}>
|
||||
{(formData) => {
|
||||
const { 'sourceField.enabled': enabled } = formData;
|
||||
const {
|
||||
sourceField: { enabled },
|
||||
} = formData;
|
||||
|
||||
if (enabled === undefined) {
|
||||
return null;
|
||||
|
|
|
@ -53,7 +53,7 @@ export const AnalyzerParameterSelects = ({
|
|||
|
||||
useEffect(() => {
|
||||
const subscription = subscribe((updateData) => {
|
||||
const formData = updateData.data.raw;
|
||||
const formData = updateData.data.internal;
|
||||
const value = formData.sub ? formData.sub : formData.main;
|
||||
onChange(value);
|
||||
});
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { get } from 'lodash';
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
|
@ -193,7 +194,7 @@ export const EditFieldFormRow = React.memo(
|
|||
return formFieldPath ? (
|
||||
<FormDataProvider pathsToWatch={formFieldPath}>
|
||||
{(formData) => {
|
||||
setIsContentVisible(formData[formFieldPath]);
|
||||
setIsContentVisible(get(formData, formFieldPath));
|
||||
return renderContent();
|
||||
}}
|
||||
</FormDataProvider>
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
import React from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { NormalizedField, Field as FieldType } from '../../../../types';
|
||||
import { NormalizedField, Field as FieldType, ComboBoxOption } from '../../../../types';
|
||||
import { getFieldConfig } from '../../../../lib';
|
||||
import { UseField, FormDataProvider, NumericField, Field } from '../../../../shared_imports';
|
||||
import {
|
||||
|
@ -48,9 +48,9 @@ export const NumericType = ({ field }: Props) => {
|
|||
<>
|
||||
<BasicParametersSection>
|
||||
{/* scaling_factor */}
|
||||
<FormDataProvider pathsToWatch="subType">
|
||||
{(formData) =>
|
||||
formData.subType === 'scaled_float' ? (
|
||||
<FormDataProvider<{ subType?: ComboBoxOption[] }> pathsToWatch="subType">
|
||||
{(formData) => {
|
||||
return formData.subType?.[0]?.value === 'scaled_float' ? (
|
||||
<EditFieldFormRow
|
||||
title={PARAMETERS_DEFINITION.scaling_factor.title!}
|
||||
description={PARAMETERS_DEFINITION.scaling_factor.description}
|
||||
|
@ -62,8 +62,8 @@ export const NumericType = ({ field }: Props) => {
|
|||
component={Field}
|
||||
/>
|
||||
</EditFieldFormRow>
|
||||
) : null
|
||||
}
|
||||
) : null;
|
||||
}}
|
||||
</FormDataProvider>
|
||||
|
||||
<IndexParameter hasIndexOptions={false} />
|
||||
|
|
|
@ -5,7 +5,12 @@
|
|||
*/
|
||||
import React from 'react';
|
||||
|
||||
import { NormalizedField, Field as FieldType, ParameterName } from '../../../../types';
|
||||
import {
|
||||
NormalizedField,
|
||||
Field as FieldType,
|
||||
ParameterName,
|
||||
ComboBoxOption,
|
||||
} from '../../../../types';
|
||||
import { getFieldConfig } from '../../../../lib';
|
||||
import {
|
||||
StoreParameter,
|
||||
|
@ -33,9 +38,9 @@ export const RangeType = ({ field }: Props) => {
|
|||
<BasicParametersSection>
|
||||
<IndexParameter hasIndexOptions={false} />
|
||||
|
||||
<FormDataProvider pathsToWatch="subType">
|
||||
<FormDataProvider<{ subType?: ComboBoxOption[] }> pathsToWatch="subType">
|
||||
{(formData) =>
|
||||
formData.subType === 'date_range' ? (
|
||||
formData.subType?.[0]?.value === 'date_range' ? (
|
||||
<FormatParameter
|
||||
defaultValue={field.source.format as string}
|
||||
defaultToggleValue={getDefaultToggleValue('format', field.source)}
|
||||
|
@ -46,9 +51,9 @@ export const RangeType = ({ field }: Props) => {
|
|||
</BasicParametersSection>
|
||||
|
||||
<AdvancedParametersSection>
|
||||
<FormDataProvider pathsToWatch="subType">
|
||||
<FormDataProvider<{ subType?: ComboBoxOption[] }> pathsToWatch="subType">
|
||||
{(formData) =>
|
||||
formData.subType === 'date_range' ? (
|
||||
formData.subType?.[0]?.value === 'date_range' ? (
|
||||
<LocaleParameter defaultToggleValue={getDefaultToggleValue('locale', field.source)} />
|
||||
) : null
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@ export const StateProvider: React.FC = ({ children }) => {
|
|||
configuration: {
|
||||
defaultValue: {},
|
||||
data: {
|
||||
raw: {},
|
||||
internal: {},
|
||||
format: () => ({}),
|
||||
},
|
||||
validate: () => Promise.resolve(true),
|
||||
|
@ -26,7 +26,7 @@ export const StateProvider: React.FC = ({ children }) => {
|
|||
templates: {
|
||||
defaultValue: {},
|
||||
data: {
|
||||
raw: {},
|
||||
internal: {},
|
||||
format: () => ({}),
|
||||
},
|
||||
validate: () => Promise.resolve(true),
|
||||
|
|
|
@ -176,7 +176,7 @@ export const reducer = (state: State, action: Action): State => {
|
|||
configuration: {
|
||||
...state.configuration,
|
||||
data: {
|
||||
raw: action.value.configuration,
|
||||
internal: action.value.configuration,
|
||||
format: () => action.value.configuration,
|
||||
},
|
||||
defaultValue: action.value.configuration,
|
||||
|
@ -184,7 +184,7 @@ export const reducer = (state: State, action: Action): State => {
|
|||
templates: {
|
||||
...state.templates,
|
||||
data: {
|
||||
raw: action.value.templates,
|
||||
internal: action.value.templates,
|
||||
format: () => action.value.templates,
|
||||
},
|
||||
defaultValue: action.value.templates,
|
||||
|
@ -217,7 +217,7 @@ export const reducer = (state: State, action: Action): State => {
|
|||
isValid: true,
|
||||
defaultValue: action.value,
|
||||
data: {
|
||||
raw: action.value,
|
||||
internal: action.value,
|
||||
format: () => action.value,
|
||||
},
|
||||
validate: async () => true,
|
||||
|
@ -241,7 +241,7 @@ export const reducer = (state: State, action: Action): State => {
|
|||
isValid: true,
|
||||
defaultValue: action.value,
|
||||
data: {
|
||||
raw: action.value,
|
||||
internal: action.value,
|
||||
format: () => action.value,
|
||||
},
|
||||
validate: async () => true,
|
||||
|
|
|
@ -35,8 +35,8 @@ export const useMappingsStateListener = ({ onChange, value }: Args) => {
|
|||
const isFieldFormVisible = state.fieldForm !== undefined;
|
||||
const emptyNameValue =
|
||||
isFieldFormVisible &&
|
||||
(state.fieldForm!.data.raw.name === undefined ||
|
||||
state.fieldForm!.data.raw.name.trim() === '');
|
||||
(state.fieldForm!.data.internal.name === undefined ||
|
||||
state.fieldForm!.data.internal.name.trim() === '');
|
||||
|
||||
const bypassFieldFormValidation =
|
||||
state.documentFields.status === 'creatingField' && emptyNameValue;
|
||||
|
|
|
@ -157,7 +157,7 @@ export const StepLogistics: React.FunctionComponent<Props> = React.memo(
|
|||
getFormData,
|
||||
} = form;
|
||||
|
||||
const [{ addMeta }] = useFormData({
|
||||
const [{ addMeta }] = useFormData<{ addMeta: boolean }>({
|
||||
form,
|
||||
watch: 'addMeta',
|
||||
});
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
import React, { FunctionComponent, useEffect, useState } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { get } from 'lodash';
|
||||
import {
|
||||
FIELD_TYPES,
|
||||
UseField,
|
||||
|
@ -44,8 +44,8 @@ export const Json: FunctionComponent = () => {
|
|||
const form = useFormContext();
|
||||
const [isAddToPathDisabled, setIsAddToPathDisabled] = useState<boolean>(false);
|
||||
useEffect(() => {
|
||||
const subscription = form.subscribe(({ data: { raw: rawData } }) => {
|
||||
const hasTargetField = !!rawData[TARGET_FIELD_PATH];
|
||||
const subscription = form.subscribe(({ data: { internal } }) => {
|
||||
const hasTargetField = !!get(internal, TARGET_FIELD_PATH);
|
||||
if (hasTargetField && !isAddToPathDisabled) {
|
||||
setIsAddToPathDisabled(true);
|
||||
form.getFields()[ADD_TO_ROOT_FIELD_PATH].setValue(false);
|
||||
|
|
|
@ -21,6 +21,7 @@ import { Document } from '../../types';
|
|||
|
||||
import { Tabs, TestPipelineFlyoutTab, OutputTab, DocumentsTab } from './test_pipeline_tabs';
|
||||
import { TestPipelineFlyoutForm } from './test_pipeline_flyout.container';
|
||||
|
||||
export interface Props {
|
||||
onClose: () => void;
|
||||
handleTestPipeline: (
|
||||
|
|
|
@ -21,7 +21,6 @@ import {
|
|||
ValidationFuncArg,
|
||||
FormHook,
|
||||
Form,
|
||||
useFormData,
|
||||
} from '../../../../../../../shared_imports';
|
||||
import { Document } from '../../../../types';
|
||||
import { AddDocumentsAccordion } from './add_documents_accordion';
|
||||
|
@ -149,16 +148,15 @@ export const DocumentsTab: FunctionComponent<Props> = ({
|
|||
resetTestOutput,
|
||||
}) => {
|
||||
const { services } = useKibana();
|
||||
|
||||
const [, formatData] = useFormData({ form });
|
||||
const { getFormData, reset } = form;
|
||||
|
||||
const onAddDocumentHandler = useCallback(
|
||||
(document) => {
|
||||
const { documents: existingDocuments = [] } = formatData();
|
||||
const { documents: existingDocuments = [] } = getFormData();
|
||||
|
||||
form.reset({ defaultValue: { documents: [...existingDocuments, document] } });
|
||||
reset({ defaultValue: { documents: [...existingDocuments, document] } });
|
||||
},
|
||||
[form, formatData]
|
||||
[reset, getFormData]
|
||||
);
|
||||
|
||||
const [showResetModal, setShowResetModal] = useState<boolean>(false);
|
||||
|
|
|
@ -54,7 +54,7 @@ export const AddComment = React.memo(
|
|||
|
||||
const fieldName = 'comment';
|
||||
const { setFieldValue, reset, submit } = form;
|
||||
const [{ comment }] = useFormData({ form, watch: [fieldName] });
|
||||
const [{ comment }] = useFormData<{ comment: string }>({ form, watch: [fieldName] });
|
||||
|
||||
const onCommentChange = useCallback((newValue) => setFieldValue(fieldName, newValue), [
|
||||
setFieldValue,
|
||||
|
|
|
@ -87,10 +87,10 @@ const StepAboutRuleComponent: FC<StepAboutRuleProps> = ({
|
|||
schema,
|
||||
});
|
||||
const { getFields, getFormData, submit } = form;
|
||||
const [{ severity: formSeverity }] = (useFormData({
|
||||
const [{ severity: formSeverity }] = useFormData<AboutStepRule>({
|
||||
form,
|
||||
watch: ['severity'],
|
||||
}) as unknown) as [Partial<AboutStepRule>];
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const formSeverityValue = formSeverity?.value;
|
||||
|
|
|
@ -141,17 +141,15 @@ const StepDefineRuleComponent: FC<StepDefineRuleProps> = ({
|
|||
'threshold.value': formThresholdValue,
|
||||
'threshold.field': formThresholdField,
|
||||
},
|
||||
] = (useFormData({
|
||||
] = useFormData<
|
||||
DefineStepRule & {
|
||||
'threshold.value': number | undefined;
|
||||
'threshold.field': string[] | undefined;
|
||||
}
|
||||
>({
|
||||
form,
|
||||
watch: ['index', 'ruleType', 'queryBar', 'threshold.value', 'threshold.field', 'threatIndex'],
|
||||
}) as unknown) as [
|
||||
Partial<
|
||||
DefineStepRule & {
|
||||
'threshold.value': number | undefined;
|
||||
'threshold.field': string[] | undefined;
|
||||
}
|
||||
>
|
||||
];
|
||||
});
|
||||
const [isQueryBarValid, setIsQueryBarValid] = useState(false);
|
||||
const index = formIndex || initialState.index;
|
||||
const threatIndex = formThreatIndex || initialState.threatIndex;
|
||||
|
|
|
@ -93,10 +93,10 @@ const StepRuleActionsComponent: FC<StepRuleActionsProps> = ({
|
|||
schema,
|
||||
});
|
||||
const { getFields, getFormData, submit } = form;
|
||||
const [{ throttle: formThrottle }] = (useFormData({
|
||||
const [{ throttle: formThrottle }] = useFormData<ActionsStepRule>({
|
||||
form,
|
||||
watch: ['throttle'],
|
||||
}) as unknown) as [Partial<ActionsStepRule>];
|
||||
});
|
||||
const throttle = formThrottle || initialState.throttle;
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue