Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
d5683d4
Add initial alias component
Oct 29, 2019
32f8a1c
Merge remote-tracking branch 'upstream/master' into feature/mappings-…
Nov 11, 2019
9bfcd6d
Initial commit "alias" type
Nov 12, 2019
bce3309
Update logic when deleting a field with aliases
Nov 13, 2019
173342f
Delete alias when changing field type
Nov 13, 2019
b912d87
Show required parameter form when creating field
Nov 13, 2019
c1b5f56
Fix bug when subType select was not reset
Nov 13, 2019
cde9127
Merge branch 'feature/mappings-editor' into feature/mappings-alias-ty…
Nov 14, 2019
478cb6f
Add i18n translate
Nov 14, 2019
9da3fd5
Fix bug: allow type change without deleting alias
Nov 14, 2019
e4d2abd
Fix: keep advanced settings value after collapse
Nov 14, 2019
7849551
Reset form when when toggle "off" and removing form row fields
Nov 14, 2019
43167af
Only allow number for null_value (numeric type)
Nov 14, 2019
00dadf1
Revert reject malformed --> ignore malformed
Nov 14, 2019
d04517d
Move documentation link to the right + truncate field length
Nov 14, 2019
ce71d86
Add lowercase validation for template name
Nov 14, 2019
6bd8a1b
Change BEM class naming to camelCase
Nov 14, 2019
cf9dcaf
Add missing i18n translations
Nov 14, 2019
5bfe46d
Fix issue when switching type not deleting aliases
Nov 14, 2019
a8fc1da
Refactor logic to recursively delete all aliases
Nov 14, 2019
4e09527
Fix small glitch in the UI
Nov 14, 2019
912856d
fix createField: reset subType when type has changed
Nov 15, 2019
bfa650f
fix createField: reset subType when type has changed (2)
Nov 15, 2019
713c1f3
Add callOut when trying to add an alias before other fields
Nov 15, 2019
2d7203b
Use common modal to render fields delete confirmation
Nov 16, 2019
de3e4a5
[Form lib]: Add SuperSelect field
Nov 18, 2019
ef43087
Replace Selects with SuperSelects (indexOptions, similarity, termVector)
Nov 18, 2019
47df5a2
Merge branch 'feature/mappings-editor' into fix/mappings-editor-bugs
Nov 18, 2019
28807e7
Fix typings
Nov 18, 2019
f5a8df3
[Form lib]: fix formData subscribe on addField
Nov 19, 2019
5062589
Add "language" analyzer and update UI layout
Nov 19, 2019
8dedd04
[Form lib]: Add onChange listener to <UseField />
Nov 19, 2019
dc7d61f
Add select for custom analyzer
Nov 19, 2019
66df0a1
Refactor CreateField and EditFieldHeader to use onChange listener
Nov 19, 2019
ffb4c35
Fix i18n issues
Nov 19, 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
8 changes: 5 additions & 3 deletions src/plugins/es_ui_shared/static/forms/components/field.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,12 @@
* under the License.
*/

import React from 'react';
import React, { ComponentType } from 'react';
import { FieldHook, FIELD_TYPES } from '../hook_form_lib';

interface Props {
field: FieldHook;
euiFieldProps?: Record<string, any>;
euiFieldProps?: { [key: string]: any };
idAria?: string;
[key: string]: any;
}
Expand All @@ -34,16 +34,18 @@ import {
ComboBoxField,
MultiSelectField,
SelectField,
SuperSelectField,
ToggleField,
} from './fields';

const mapTypeToFieldComponent = {
const mapTypeToFieldComponent: { [key: string]: ComponentType<any> } = {
[FIELD_TYPES.TEXT]: TextField,
[FIELD_TYPES.NUMBER]: NumericField,
[FIELD_TYPES.CHECKBOX]: CheckBoxField,
[FIELD_TYPES.COMBO_BOX]: ComboBoxField,
[FIELD_TYPES.MULTI_SELECT]: MultiSelectField,
[FIELD_TYPES.SELECT]: SelectField,
[FIELD_TYPES.SUPER_SELECT]: SuperSelectField,
[FIELD_TYPES.TOGGLE]: ToggleField,
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,6 @@ export * from './checkbox_field';
export * from './combobox_field';
export * from './multi_select_field';
export * from './select_field';
export * from './super_select_field';
export * from './toggle_field';
export * from './text_area_field';
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,25 @@
* under the License.
*/

import React from 'react';
import React, { ReactNode, OptionHTMLAttributes } from 'react';
import { EuiFormRow, EuiSelect } from '@elastic/eui';

import { FieldHook } from '../../hook_form_lib';
import { getFieldValidityAndErrorMessage } from '../helpers';

interface Props {
field: FieldHook;
euiFieldProps?: Record<string, any>;
euiFieldProps: {
options: Array<
{ text: string | ReactNode; [key: string]: any } & OptionHTMLAttributes<HTMLOptionElement>
>;
[key: string]: any;
};
idAria?: string;
[key: string]: any;
}

export const SelectField = ({ field, euiFieldProps = {}, ...rest }: Props) => {
export const SelectField = ({ field, euiFieldProps, ...rest }: Props) => {
const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field);

return (
Expand All @@ -52,7 +57,7 @@ export const SelectField = ({ field, euiFieldProps = {}, ...rest }: Props) => {
hasNoInitialSelection={true}
isInvalid={isInvalid}
data-test-subj="select"
{...(euiFieldProps as { options: any; [key: string]: any })}
{...euiFieldProps}
/>
</EuiFormRow>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import React from 'react';
import { EuiFormRow, EuiSuperSelect, EuiSuperSelectProps } from '@elastic/eui';

import { FieldHook } from '../../hook_form_lib';
import { getFieldValidityAndErrorMessage } from '../helpers';

interface Props {
field: FieldHook;
euiFieldProps: {
options: EuiSuperSelectProps<any>['options'];
[key: string]: any;
};
idAria?: string;
[key: string]: any;
}

export const SuperSelectField = ({ field, euiFieldProps, ...rest }: Props) => {
const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field);

return (
<EuiFormRow
label={field.label}
helpText={field.helpText}
error={errorMessage}
isInvalid={isInvalid}
fullWidth
data-test-subj={rest['data-test-subj']}
describedByIds={rest.idAria ? [rest.idAria] : undefined}
>
<EuiSuperSelect
fullWidth
hasDividers
itemLayoutAlign="top"
valueOfSelected={field.value as string}
onChange={value => {
field.setValue(value);
}}
isInvalid={isInvalid}
data-test-subj="superSelect"
options
{...euiFieldProps}
/>
</EuiFormRow>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,4 @@ export * from './index_name';
export * from './contains_char';
export * from './starts_with';
export * from './index_pattern_field';
export * from './lowercase_string';
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import { ValidationFunc } from '../../hook_form_lib';
import { isLowerCaseString } from '../../../validators/string';
import { ERROR_CODE } from './types';

export const lowerCaseStringField = (message: string) => (
...args: Parameters<ValidationFunc>
): ReturnType<ValidationFunc<any, ERROR_CODE>> => {
const [{ value }] = args;

if (typeof value !== 'string') {
return;
}

if (!isLowerCaseString(value)) {
return {
code: 'ERR_LOWERCASE_STRING',
message,
};
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,5 @@ export type ERROR_CODE =
| 'ERR_MIN_LENGTH'
| 'ERR_MAX_LENGTH'
| 'ERR_MIN_SELECTION'
| 'ERR_MAX_SELECTION';
| 'ERR_MAX_SELECTION'
| 'ERR_LOWERCASE_STRING';
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export const FormDataProvider = ({ children, pathsToWatch }: Props) => {
});

return subscription.unsubscribe;
}, [pathsToWatch]);
}, [form, pathsToWatch]);

return children(formData);
};
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export interface Props {
defaultValue?: unknown;
component?: FunctionComponent<any> | 'input';
componentProps?: Record<string, any>;
onChange?: (value: unknown) => void;
children?: (field: FieldHook) => JSX.Element;
}

Expand All @@ -38,6 +39,7 @@ export const UseField = ({
defaultValue,
component = 'input',
componentProps = {},
onChange,
children,
}: Props) => {
const form = useFormContext();
Expand All @@ -64,7 +66,7 @@ export const UseField = ({
}
}

const field = useField(form, path, configCopy);
const field = useField(form, path, configCopy, onChange);

// Remove field from form when it is unmounted or if its path changes
useEffect(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export const FIELD_TYPES = {
COMBO_BOX: 'comboBox',
SELECT: 'select',
MULTI_SELECT: 'multiSelect',
SUPER_SELECT: 'superSelect',
};

// Validation types
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

import React, { createContext, useContext } from 'react';

import { FormHook } from './types';
import { FormHook, FormData } from './types';

const FormContext = createContext<FormHook<any> | undefined>(undefined);

Expand All @@ -32,7 +32,7 @@ export const FormProvider = ({ children, form }: Props) => (
<FormContext.Provider value={form}>{children}</FormContext.Provider>
);

export const useFormContext = function<T extends object = Record<string, unknown>>() {
export const useFormContext = function<T extends FormData = FormData>() {
const context = useContext(FormContext) as FormHook<T>;
if (context === undefined) {
throw new Error('useFormContext must be used within a <FormProvider />');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,12 @@ import { useState, useEffect, useRef } from 'react';
import { FormHook, FieldHook, FieldConfig, FieldValidateResponse, ValidationError } from '../types';
import { FIELD_TYPES, VALIDATION_TYPES } from '../constants';

export const useField = (form: FormHook, path: string, config: FieldConfig = {}) => {
export const useField = (
form: FormHook,
path: string,
config: FieldConfig = {},
valueChangeListener?: (value: unknown) => void
) => {
const {
type = FIELD_TYPES.TEXT,
defaultValue = '',
Expand All @@ -49,6 +54,7 @@ export const useField = (form: FormHook, path: string, config: FieldConfig = {})
const changeCounter = useRef(0);
const inflightValidation = useRef<Promise<any> | null>(null);
const debounceTimeout = useRef<NodeJS.Timeout | null>(null);
const isUnmounted = useRef<boolean>(false);

// -- HELPERS
// ----------------------------------
Expand Down Expand Up @@ -96,12 +102,23 @@ export const useField = (form: FormHook, path: string, config: FieldConfig = {})
setIsChangingValue(true);
}

const newValue = serializeOutput(value);

// Notify listener
if (valueChangeListener) {
valueChangeListener(newValue);
}

// Update the form data observable
form.__updateFormDataAt(path, serializeOutput(value));
form.__updateFormDataAt(path, newValue);

// Validate field(s) and set form.isValid flag
await form.__validateFields(fieldsToValidateOnChange);

if (isUnmounted.current) {
return;
}

/**
* If we have set a delay to display the error message after the field value has changed,
* we first check that this is the last "change iteration" (=== the last keystroke from the user)
Expand Down Expand Up @@ -398,6 +415,15 @@ export const useField = (form: FormHook, path: string, config: FieldConfig = {})
};
}, [value]);

/**
* On unmount
*/
useEffect(() => {
return () => {
isUnmounted.current = true;
};
}, []);

const field: FieldHook = {
path,
type,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,11 @@ const DEFAULT_OPTIONS = {
stripEmptyFields: true,
};

interface UseFormReturn<T extends object> {
interface UseFormReturn<T extends FormData> {
form: FormHook<T>;
}

export function useForm<T extends object = FormData>(
export function useForm<T extends FormData = FormData>(
formConfig: FormConfig<T> | undefined = {}
): UseFormReturn<T> {
const {
Expand Down Expand Up @@ -159,10 +159,11 @@ export function useForm<T extends object = FormData>(
const addField: FormHook<T>['__addField'] = field => {
fieldsRefs.current[field.path] = field;

// Only update the formData if the path does not exist (it is the _first_ time
// the field is added), to avoid entering an infinite loop when the form is re-rendered.
if (!{}.hasOwnProperty.call(formData$.current.value, field.path)) {
updateFormDataAt(field.path, field.__serializeOutput());
const currentValue = formData$.current.value[field.path];
const fieldValue = field.__serializeOutput();

if (currentValue !== fieldValue) {
updateFormDataAt(field.path, fieldValue);
}
};

Expand All @@ -179,10 +180,16 @@ export function useForm<T extends object = FormData>(
};

const setFieldValue: FormHook<T>['setFieldValue'] = (fieldName, value) => {
if (fieldsRefs.current[fieldName] === undefined) {
return;
}
fieldsRefs.current[fieldName].setValue(value);
};

const setFieldErrors: FormHook<T>['setFieldErrors'] = (fieldName, errors) => {
if (fieldsRefs.current[fieldName] === undefined) {
return;
}
fieldsRefs.current[fieldName].setErrors(errors);
};

Expand Down Expand Up @@ -221,11 +228,10 @@ export function useForm<T extends object = FormData>(

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

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

Expand All @@ -241,8 +247,13 @@ export function useForm<T extends object = FormData>(
const { resetValues = true } = resetOptions;
const currentFormData = { ...formData$.current.value } as FormData;
Object.entries(fieldsRefs.current).forEach(([path, field]) => {
const fieldValue = field.reset({ resetValue: resetValues });
currentFormData[path] = fieldValue;
// 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;
}
});
if (resetValues) {
formData$.current.next(currentFormData as T);
Expand Down
Loading