Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
67 commits
Select commit Hold shift + click to select a range
bcf377a
Move mappings editor component to its own folder
sebelga Jul 9, 2019
f279201
Initial version of the mappings editor
sebelga Jul 11, 2019
bb98a2e
Nested properties for "object" data type
sebelga Jul 11, 2019
9c4cee9
Add FormDataProvider to listen to data change
sebelga Jul 12, 2019
8576632
Add support for "array" data type
sebelga Jul 12, 2019
f220709
Add serializer & deSerializer to form
sebelga Jul 17, 2019
27ff056
Use checkbox instead of toggle
sebelga Jul 25, 2019
e453dea
Add dynamic dates format
sebelga Jul 25, 2019
eefe40b
Remove margin left on first level of nested properties
sebelga Jul 25, 2019
a9d5a3e
Allow subType to be declared for a type
sebelga Jul 25, 2019
b063a8d
Add date, range, boolean, binary types
sebelga Jul 25, 2019
4066ae7
Add nested properties inside accordion
sebelga Jul 26, 2019
7a36eb4
Fix defaultValue for parameters
sebelga Jul 26, 2019
c3c9b10
Rename UseArray child props
sebelga Jul 26, 2019
d4caf4e
Add styles for nested properties
sebelga Jul 26, 2019
8c51b76
Style PropertyEditor with hover
sebelga Jul 30, 2019
bf94053
Use custom <Name /> component to render the field name parameter
sebelga Jul 30, 2019
b2af545
Validate name conflict inside <Name /> component
sebelga Jul 30, 2019
a65dd77
Update validation of property name
sebelga Jul 31, 2019
11b7987
Add text and keyword child field support
sebelga Aug 1, 2019
80c64d3
Add advanced settings for Text type
sebelga Aug 1, 2019
344344a
Add all advanced settings for text type
sebelga Aug 2, 2019
6f91b24
Add validation to advanced text settings
sebelga Aug 2, 2019
50f153f
Add advanced settings for "keyword"
sebelga Aug 2, 2019
ad8f739
Add validation for "boost" and."ignore_above"
sebelga Aug 2, 2019
ea546ee
Sanitize property parameter before outputting form
sebelga Aug 2, 2019
08cb961
Add "ip", "rank_feature", "dense_vector", "sparse_vector" types
sebelga Aug 2, 2019
eb5a867
Fix text copy
sebelga Aug 4, 2019
b9fed7e
Update UX with Readonly mode of Tree component (#12)
sebelga Aug 9, 2019
6afa76f
Update styles.scss
sebelga Aug 9, 2019
af93fea
Add Hook form lib (To be reverted)
sebelga Aug 9, 2019
67b3a11
Revert "Add Hook form lib (To be reverted)"
sebelga Aug 19, 2019
f716805
Refactor to use FormProvider component
sebelga Aug 29, 2019
88c0b17
Fix name parameter component
sebelga Aug 29, 2019
ccda3f3
Fix forward mappings editor data
sebelga Aug 30, 2019
3c4c425
Refactor to use the new <Form /> context from the hook lib
sebelga Sep 6, 2019
2747f18
Update import paths to form lib
sebelga Sep 26, 2019
a17b470
Reset before refactor to v3
sebelga Sep 27, 2019
d885bfe
Add subscribe method to form lib + update validation logic
sebelga Sep 30, 2019
ee8419f
Refactor logic to update configuration form and send to consumer
sebelga Sep 30, 2019
3186bf2
Use reducer to manage editor state
sebelga Sep 30, 2019
5a8ce4b
Add DocumentFields component with Properties
sebelga Sep 30, 2019
f4d4e7f
Move reducer to its own file
sebelga Sep 30, 2019
1128dbe
Add create property and child property
sebelga Oct 2, 2019
e4ee1bd
Normalize properties to 1 level deep object
sebelga Oct 2, 2019
5a28df1
Rename topLevelFields --> rootLevelFields
sebelga Oct 2, 2019
293d9b0
Move property meta to normalize() call
sebelga Oct 3, 2019
22f54a3
Add reset() method to hook form
sebelga Oct 3, 2019
99ebcf5
Leave <CreateProperty /> component after adding a prop
sebelga Oct 3, 2019
183c6be
Allow create property by hitting ENTER key
sebelga Oct 3, 2019
a48b481
Denormalize properties
sebelga Oct 3, 2019
ed11862
Remove test validation
sebelga Oct 3, 2019
a855468
Update reducer to remove a property
sebelga Oct 3, 2019
2302d94
Refactor: "properties" --> "fields"
sebelga Oct 4, 2019
6bb5baa
Edit field basic
sebelga Oct 4, 2019
5ed3291
Use unique id for fields + core edit functionality
sebelga Oct 4, 2019
44449c9
Merge remote-tracking branch 'upstream/master' into feature/mappings-…
sebelga Oct 4, 2019
d53d722
Add name collision validation
sebelga Oct 4, 2019
4e5e37e
Add confirm modal when deleting a field with children
sebelga Oct 5, 2019
090c82a
Add max nested depth + invalid field form validation
sebelga Oct 7, 2019
a9aea0a
Reset state when updating documentFields status
sebelga Oct 7, 2019
21d6339
Fix form lib hook use_field reset()
sebelga Oct 7, 2019
c58edc1
Merge remote-tracking branch 'upstream/master' into feature/mappings-…
sebelga Oct 7, 2019
13dd203
Fix add field reducer
sebelga Oct 7, 2019
8e631ac
Fix Index template step logistics
sebelga Oct 7, 2019
3dcb609
Fix 18n keys
sebelga Oct 7, 2019
a36a3a1
Merge remote-tracking branch 'upstream/master' into feature/mappings-…
sebelga Oct 7, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,15 @@ export const useField = (form: FormHook, path: string, config: FieldConfig = {})
deserializer = (value: unknown) => value,
} = config;

const [value, setStateValue] = useState(
typeof defaultValue === 'function' ? deserializer(defaultValue()) : deserializer(defaultValue)
);
const initialValue =
typeof defaultValue === 'function' ? deserializer(defaultValue()) : deserializer(defaultValue);

const [value, setStateValue] = useState(initialValue);
const [errors, setErrors] = useState<ValidationError[]>([]);
const [isPristine, setPristine] = useState(true);
const [isValidating, setValidating] = useState(false);
const [isChangingValue, setIsChangingValue] = useState(false);
const [isValidated, setIsValidated] = useState(false);
const validateCounter = useRef(0);
const changeCounter = useRef(0);
const inflightValidation = useRef<Promise<any> | null>(null);
Expand Down Expand Up @@ -262,6 +264,7 @@ export const useField = (form: FormHook, path: string, config: FieldConfig = {})
validationType,
} = validationData;

setIsValidated(true);
setValidating(true);

// By the time our validate function has reached completion, it’s possible
Expand All @@ -275,12 +278,10 @@ export const useField = (form: FormHook, path: string, config: FieldConfig = {})
// This is the most recent invocation
setValidating(false);
// Update the errors array
setErrors(previousErrors => {
// First filter out the validation type we are currently validating
const filteredErrors = filterErrors(previousErrors, validationType);
return [...filteredErrors, ..._validationErrors];
});
const filteredErrors = filterErrors(errors, validationType);
setErrors([...filteredErrors, ..._validationErrors]);
}

return {
isValid: _validationErrors.length === 0,
errors: _validationErrors,
Expand Down Expand Up @@ -358,6 +359,15 @@ export const useField = (form: FormHook, path: string, config: FieldConfig = {})
return errorMessages ? errorMessages : null;
};

const reset = () => {
setPristine(true);
setValidating(false);
setIsChangingValue(false);
setIsValidated(false);
setErrors([]);
setValue(initialValue);
};

const serializeOutput: FieldHook['__serializeOutput'] = (rawValue = value) =>
serializer(rawValue);

Expand Down Expand Up @@ -388,13 +398,15 @@ export const useField = (form: FormHook, path: string, config: FieldConfig = {})
form,
isPristine,
isValidating,
isValidated,
isChangingValue,
onChange,
getErrorsMessages,
setValue,
setErrors: _setErrors,
clearErrors,
validate,
reset,
__serializeOutput: serializeOutput,
};

Expand Down
115 changes: 87 additions & 28 deletions src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@
* under the License.
*/

import { useState, useRef } from 'react';
import { useState, useRef, useEffect } from 'react';
import { get } from 'lodash';

import { FormHook, FormData, FieldConfig, FieldsMap, FormConfig } from '../types';
import { mapFormFields, flattenObject, unflattenObject, Subject } from '../lib';
import { FormHook, FieldHook, FormData, FieldConfig, FieldsMap, FormConfig } from '../types';
import { mapFormFields, flattenObject, unflattenObject, Subject, Subscription } from '../lib';

const DEFAULT_ERROR_DISPLAY_TIMEOUT = 500;
const DEFAULT_OPTIONS = {
Expand All @@ -47,10 +47,11 @@ export function useForm<T extends object = FormData>(
const formOptions = { ...DEFAULT_OPTIONS, ...options };
const defaultValueDeserialized =
Object.keys(defaultValue).length === 0 ? defaultValue : deserializer(defaultValue);
const [isSubmitted, setSubmitted] = useState(false);
const [isSubmitted, setIsSubmitted] = useState(false);
const [isSubmitting, setSubmitting] = useState(false);
const [isValid, setIsValid] = useState(true);
const [isValid, setIsValid] = useState<boolean | undefined>(undefined);
const fieldsRefs = useRef<FieldsMap>({});
const formUpdateSubscribers = useRef<Subscription[]>([]);

// 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
Expand All @@ -59,6 +60,13 @@ export function useForm<T extends object = FormData>(
// and updating its state to trigger the necessary view render.
const formData$ = useRef<Subject<T>>(new Subject<T>(flattenObject(defaultValue) as T));

useEffect(() => {
return () => {
formUpdateSubscribers.current.forEach(subscription => subscription.unsubscribe());
formUpdateSubscribers.current = [];
};
}, []);

// -- HELPERS
// ----------------------------------
const fieldsToArray = () => Object.values(fieldsRefs.current);
Expand All @@ -78,6 +86,12 @@ export function useForm<T extends object = FormData>(
return fields;
};

const updateFormDataAt: FormHook<T>['__updateFormDataAt'] = (path, value) => {
const currentFormData = formData$.current.value;
formData$.current.next({ ...currentFormData, [path]: value });
return formData$.current.value;
};

// -- API
// ----------------------------------
const getFormData: FormHook<T>['getFormData'] = (getDataOptions = { unflatten: true }) =>
Expand All @@ -93,34 +107,52 @@ export function useForm<T extends object = FormData>(
{} as T
);

const updateFormDataAt: FormHook<T>['__updateFormDataAt'] = (path, value) => {
const currentFormData = formData$.current.value;
formData$.current.next({ ...currentFormData, [path]: value });
return formData$.current.value;
const isFieldValid = (field: FieldHook) =>
field.getErrorsMessages() === null && !field.isValidating;

const updateFormValidity = () => {
const fieldsArray = fieldsToArray();
const areAllFieldsValidated = fieldsArray.every(field => field.isValidated);

if (!areAllFieldsValidated) {
// If *not* all the fiels have been validated, the validity of the form is unknown, thus still "undefined"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo: fiels

return;
}

const isFormValid = fieldsArray.every(isFieldValid);

setIsValid(isFormValid);
return isFormValid;
};

/**
* When a field value changes, validateFields() is called with the field name + any other fields
* declared in the "fieldsToValidateOnChange" (see the field config).
*
* When this method is called _without_ providing any fieldNames, we only need to validate fields that are pristine
* as the fields that are dirty have already been validated when their value changed.
*/
const validateFields: FormHook<T>['__validateFields'] = async fieldNames => {
const fieldsToValidate = fieldNames
? fieldNames.map(name => fieldsRefs.current[name]).filter(field => field !== undefined)
: fieldsToArray().filter(field => field.isPristine); // only validate fields that haven't been changed
.map(name => fieldsRefs.current[name])
.filter(field => field !== undefined);

const formData = getFormData({ unflatten: false });
if (fieldsToValidate.length === 0) {
// Nothing to validate
return true;
}

const formData = getFormData({ unflatten: false });
await Promise.all(fieldsToValidate.map(field => field.validate({ formData })));
updateFormValidity();

const isFormValid = fieldsToArray().every(
field => field.getErrorsMessages() === null && !field.isValidating
);
setIsValid(isFormValid);
return fieldsToValidate.every(isFieldValid);
};

return isFormValid;
const validateAllFields = async (): Promise<boolean> => {
const fieldsToValidate = fieldsToArray().filter(field => !field.isValidated);

if (fieldsToValidate.length === 0) {
// Nothing left to validate, all fields are already validated.
return isValid!;
}

await validateFields(fieldsToValidate.map(field => field.path));

return updateFormValidity()!;
Copy link
Contributor

@cjcenizal cjcenizal Oct 7, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for adding the isValidated state to useField -- I find this logic much easier to follow now!

One thing looks strange to me here. validateAllFields calls validateFields. Both of these functions call updateFormValidity. Are these two identical calls redundant? Can we just keep the one in validateFields and remove the one in validateAllFields?

Then on line 142, validateFields also returns the value of this call:

fieldsToValidate.every(isFieldValid)

This code looks suspiciously like it's checking that every field is valid. Yet updateFormValidity returns isFormValid. So in addition to making redundant function calls, are we also doing the same thing in two different ways?

Ideally, it seems like there should be a single place where the form validity state is set, and then any place we want to surface that state we just return isValid as set by the setIsValid state hook (we might need to use a ref to avoid returning a stale value). WDYT?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we just keep the one in validateFields and remove the one in validateAllFields?

Good catch, I will remove the one in validateAllFields().

This code looks suspiciously like it's checking that every field is valid. Yet updateFormValidity returns isFormValid. So in addition to making redundant function calls, are we also doing the same thing in two different ways?

validateFields() requires fieldNames to be provided and it returns the validity of those fields. At the same time, it updates the form validity in case it has changed.

Ideally, it seems like there should be a single place where the form validity state is set

This is the case, the only place where the form validity is set is inside updateFormValidity(). It is also unset in the new reset() method.

we might need to use a ref to avoid returning a stale value

Not sure why we would prefer a ref than a state. This is probably the most important state of the form 😊

};

const addField: FormHook<T>['__addField'] = field => {
Expand Down Expand Up @@ -170,32 +202,59 @@ export function useForm<T extends object = FormData>(
}

if (!isSubmitted) {
setSubmitted(true); // User has attempted to submit the form at least once
setIsSubmitted(true); // User has attempted to submit the form at least once
}
setSubmitting(true);

const isFormValid = await validateFields();
const isFormValid = await validateAllFields();
const formData = serializer(getFormData() as T);

if (onSubmit) {
await onSubmit(formData, isFormValid);
await onSubmit(formData, isFormValid!);
}

setSubmitting(false);

return { data: formData, isValid: isFormValid };
return { data: formData, isValid: isFormValid! };
};

const subscribe: FormHook<T>['subscribe'] = handler => {
const format = () => serializer(getFormData() as T);
const validate = async () => await validateAllFields();

const subscription = formData$.current.subscribe(raw => {
handler({ isValid, data: { raw, format }, validate });
});

formUpdateSubscribers.current.push(subscription);
return subscription;
};

/**
* 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'] = () => {
Object.entries(fieldsRefs.current).forEach(([path, field]) => {
field.reset();
});
setIsSubmitted(false);
setSubmitting(false);
setIsValid(undefined);
};

const form: FormHook<T> = {
isSubmitted,
isSubmitting,
isValid,
submit: submitForm,
subscribe,
setFieldValue,
setFieldErrors,
getFields,
getFormData,
getFieldDefaultValue,
reset,
__options: formOptions,
__formData$: formData$,
__updateFormDataAt: updateFormDataAt,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export const flattenObject = (
): Record<string, any> =>
Object.entries(object).reduce((acc, [key, value]) => {
const updatedPaths = [...paths, key];
if (value !== null && typeof value === 'object') {
if (value !== null && !Array.isArray(value) && typeof value === 'object') {
return flattenObject(value, to, updatedPaths);
}
acc[updatedPaths.join('.')] = value;
Expand Down
22 changes: 19 additions & 3 deletions src/plugins/es_ui_shared/static/forms/hook_form_lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
*/

import { ReactNode, ChangeEvent, FormEvent, MouseEvent, MutableRefObject } from 'react';
import { Subject } from './lib';
import { Subject, Subscription } from './lib';

// This type will convert all optional property to required ones
// Comes from https://github.com/microsoft/TypeScript/issues/15012#issuecomment-365453623
Expand All @@ -27,25 +27,28 @@ type Required<T> = T extends object ? { [P in keyof T]-?: NonNullable<T[P]> } :
export interface FormHook<T extends object = FormData> {
readonly isSubmitted: boolean;
readonly isSubmitting: boolean;
readonly isValid: boolean;
readonly isValid: boolean | undefined;
submit: (e?: FormEvent<HTMLFormElement> | MouseEvent) => Promise<{ data: T; isValid: boolean }>;
subscribe: (handler: OnUpdateHandler<T>) => Subscription;
setFieldValue: (fieldName: string, value: FieldValue) => void;
setFieldErrors: (fieldName: string, errors: ValidationError[]) => void;
getFields: () => FieldsMap;
getFormData: (options?: { unflatten?: boolean }) => T;
getFieldDefaultValue: (fieldName: string) => unknown;
reset: () => void;
readonly __options: Required<FormOptions>;
readonly __formData$: MutableRefObject<Subject<T>>;
__addField: (field: FieldHook) => void;
__removeField: (fieldNames: string | string[]) => void;
__validateFields: (fieldNames?: string[]) => Promise<boolean>;
__validateFields: (fieldNames: string[]) => Promise<boolean>;
__updateFormDataAt: (field: string, value: unknown) => T;
__readFieldConfigFromSchema: (fieldName: string) => FieldConfig;
}

export interface FormSchema<T extends object = FormData> {
[key: string]: FormSchemaEntry<T>;
}

type FormSchemaEntry<T extends object> =
| FieldConfig<T>
| Array<FieldConfig<T>>
Expand All @@ -60,6 +63,17 @@ export interface FormConfig<T extends object = FormData> {
options?: FormOptions;
}

export interface OnFormUpdateArg<T extends object> {
data: {
raw: { [key: string]: any };
format: () => T;
};
validate: () => Promise<boolean>;
isValid?: boolean;
}

export type OnUpdateHandler<T extends object> = (arg: OnFormUpdateArg<T>) => void;

export interface FormOptions {
errorDisplayDelay?: number;
/**
Expand All @@ -77,6 +91,7 @@ export interface FieldHook {
readonly errors: ValidationError[];
readonly isPristine: boolean;
readonly isValidating: boolean;
readonly isValidated: boolean;
readonly isChangingValue: boolean;
readonly form: FormHook<any>;
getErrorsMessages: (args?: {
Expand All @@ -92,6 +107,7 @@ export interface FieldHook {
value?: unknown;
validationType?: string;
}) => FieldValidateResponse | Promise<FieldValidateResponse>;
reset: () => void;
__serializeOutput: (rawValue?: unknown) => unknown;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ export const StepLogistics: React.FunctionComponent<StepProps> = ({
});

useEffect(() => {
onStepValidityChange(form.isValid);
onStepValidityChange(form.isValid === undefined ? true : form.isValid);
}, [form.isValid]);

useEffect(() => {
Expand Down
3 changes: 2 additions & 1 deletion x-pack/legacy/plugins/index_management/public/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@
// indChart__legend--small
// indChart__legend-isLoading

@import 'index_management';
@import 'index_management';
@import '../static/ui/styles'
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@import "./components/mappings_editor/styles"
Loading