From 8e58d7090da744f89687291ecf86e04e7089fa77 Mon Sep 17 00:00:00 2001 From: Elena Stoeva Date: Tue, 1 Oct 2024 21:37:38 +0100 Subject: [PATCH] Preserve set values in request --- .../forms/hook_form_lib/hooks/use_form.ts | 23 +++++------- .../static/forms/hook_form_lib/types.ts | 4 +++ .../configuration_form/configuration_form.tsx | 15 +++++--- .../index_management/index_template_wizard.ts | 35 +++++++++++++++++++ .../page_objects/index_management_page.ts | 15 ++++++++ 5 files changed, 73 insertions(+), 19 deletions(-) diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts index f2d14074550a8..da68c584ad61f 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts @@ -25,7 +25,7 @@ import { createArrayItem, getInternalArrayFieldPath } from '../components/use_ar const DEFAULT_OPTIONS = { valueChangeDebounceTime: 500, stripEmptyFields: true, - stripUnmodifiedFields: false, + stripUnsetFields: false, }; export interface UseFormReturn { @@ -70,15 +70,15 @@ export function useForm( const { valueChangeDebounceTime, stripEmptyFields: doStripEmptyFields, - stripUnmodifiedFields, + stripUnsetFields, } = options ?? {}; const formOptions = useMemo( () => ({ stripEmptyFields: doStripEmptyFields ?? DEFAULT_OPTIONS.stripEmptyFields, valueChangeDebounceTime: valueChangeDebounceTime ?? DEFAULT_OPTIONS.valueChangeDebounceTime, - stripUnmodifiedFields: stripUnmodifiedFields ?? DEFAULT_OPTIONS.stripUnmodifiedFields + stripUnsetFields: stripUnsetFields ?? DEFAULT_OPTIONS.stripUnsetFields, }), - [valueChangeDebounceTime, doStripEmptyFields, stripUnmodifiedFields] + [valueChangeDebounceTime, doStripEmptyFields, stripUnsetFields] ); const [isSubmitted, setIsSubmitted] = useState(false); @@ -186,7 +186,7 @@ export function useForm( const getFieldsForOutput = useCallback( ( fields: FieldsMap, - opts: { stripEmptyFields: boolean; stripUnmodifiedFields: boolean } + opts: { stripEmptyFields: boolean; stripUnsetFields: boolean } ): FieldsMap => { return Object.entries(fields).reduce((acc, [key, field]) => { if (!field.__isIncludedInOutput) { @@ -200,8 +200,8 @@ export function useForm( } } - if (opts.stripUnmodifiedFields) { - if (!field.isModified) { + if (opts.stripUnsetFields) { + if (!field.isDirty && getFieldDefaultValue(field.path) === undefined) { return acc; } } @@ -411,18 +411,13 @@ export function useForm( const getFormData: FormHook['getFormData'] = useCallback(() => { const fieldsToOutput = getFieldsForOutput(fieldsRefs.current, { stripEmptyFields: formOptions.stripEmptyFields, - stripUnmodifiedFields: formOptions.stripUnmodifiedFields, + stripUnsetFields: formOptions.stripUnsetFields, }); const fieldsValue = mapFormFields(fieldsToOutput, (field) => field.__serializeValue()); return serializer ? serializer(unflattenObject(fieldsValue)) : unflattenObject(fieldsValue); - }, [ - getFieldsForOutput, - formOptions.stripEmptyFields, - formOptions.stripUnmodifiedFields, - serializer, - ]); + }, [getFieldsForOutput, formOptions.stripEmptyFields, formOptions.stripUnsetFields, serializer]); const getErrors: FormHook['getErrors'] = useCallback(() => { if (isValid === true) { diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/types.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/types.ts index f700faa66e6fd..282b9de8d3ee4 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/types.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/types.ts @@ -141,6 +141,10 @@ export interface FormOptions { * Remove empty string field ("") from form data */ stripEmptyFields?: boolean; + /** + * Remove fields from form data that don't have initial value and are not modified by the user. + */ + stripUnsetFields?: boolean; } export interface FieldHook { diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/configuration_form.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/configuration_form.tsx index f9e786906d88f..e3e32c55aada0 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/configuration_form.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/configuration_form.tsx @@ -75,7 +75,7 @@ const formDeserializer = (formData: GenericObject) => { return { dynamicMapping: { enabled: dynamic === 'strict' ? false : dynamic, - throwErrorsForUnmappedFields: dynamic === 'strict', + throwErrorsForUnmappedFields: dynamic === 'strict' ? true : undefined, numeric_detection, date_detection, dynamic_date_formats, @@ -108,11 +108,12 @@ export const ConfigurationForm = React.memo(({ value, esNodesPlugins }: Props) = schema: configurationFormSchema, serializer: serializerCallback, deserializer: formDeserializer, + defaultValue: value, id: 'configurationForm', - options: { stripUnmodifiedFields: true }, + options: { stripUnsetFields: true }, }); const dispatch = useDispatch(); - const { subscribe, submit, getFormData, updateFieldValues } = form; + const { subscribe, submit, reset, getFormData } = form; const isMapperSizeSectionVisible = value?._size !== undefined || esNodesPlugins.includes(MapperSizePluginId); @@ -134,8 +135,12 @@ export const ConfigurationForm = React.memo(({ value, esNodesPlugins }: Props) = }, [dispatch, subscribe, submit]); useEffect(() => { - updateFieldValues(value); - }, [value, updateFieldValues]); + if (isMounted.current) { + // If the value has changed (it probably means that we have loaded a new JSON) + // we need to reset the form to update the fields values. + reset({ resetValues: true, defaultValue: value }); + } + }, [value, reset]); useEffect(() => { isMounted.current = true; diff --git a/x-pack/test/functional/apps/index_management/index_template_wizard.ts b/x-pack/test/functional/apps/index_management/index_template_wizard.ts index e9eb68f749e78..5b49286f6182b 100644 --- a/x-pack/test/functional/apps/index_management/index_template_wizard.ts +++ b/x-pack/test/functional/apps/index_management/index_template_wizard.ts @@ -15,6 +15,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const comboBox = getService('comboBox'); const find = getService('find'); const browser = getService('browser'); + const log = getService('log'); describe('Index template wizard', function () { before(async () => { @@ -162,6 +163,40 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { expect(await testSubjects.exists('fieldSubType')).to.be(true); expect(await testSubjects.exists('nextButton')).to.be(true); }); + + it("advanced options tab doesn't add default values to request by default", async () => { + await pageObjects.indexManagement.changeMappingsEditorTab('advancedOptions'); + await testSubjects.click('previewIndexTemplate'); + const templatePreview = await testSubjects.getVisibleText('simulateTemplatePreview'); + + await log.debug(`Template preview text: ${templatePreview}`); + + // All advanced options should not be part of the request + expect(templatePreview).to.not.contain('"dynamic"'); + expect(templatePreview).to.not.contain('"subobjects"'); + expect(templatePreview).to.not.contain('"dynamic_date_formats"'); + expect(templatePreview).to.not.contain('"date_detection"'); + expect(templatePreview).to.not.contain('"numeric_detection"'); + }); + + it('advanced options tab adds the set values to the request', async () => { + await pageObjects.indexManagement.changeMappingsEditorTab('advancedOptions'); + + // Toggle the subobjects field to false + await testSubjects.click('subobjectsToggle'); + + await testSubjects.click('previewIndexTemplate'); + const templatePreview = await testSubjects.getVisibleText('simulateTemplatePreview'); + + await log.debug(`Template preview text: ${templatePreview}`); + + // Only the subobjects option should be part of the request + expect(templatePreview).to.contain('"subobjects": false'); + expect(templatePreview).to.not.contain('"dynamic"'); + expect(templatePreview).to.not.contain('"dynamic_date_formats"'); + expect(templatePreview).to.not.contain('"date_detection"'); + expect(templatePreview).to.not.contain('"numeric_detection"'); + }); }); }); }; diff --git a/x-pack/test/functional/page_objects/index_management_page.ts b/x-pack/test/functional/page_objects/index_management_page.ts index 8730d2807cd21..990ddd694f3ed 100644 --- a/x-pack/test/functional/page_objects/index_management_page.ts +++ b/x-pack/test/functional/page_objects/index_management_page.ts @@ -6,6 +6,7 @@ */ import expect from '@kbn/expect'; import { FtrProviderContext } from '../ftr_provider_context'; +import { act } from "react-dom/test-utils"; export function IndexManagementPageProvider({ getService }: FtrProviderContext) { const retry = getService('retry'); @@ -118,6 +119,20 @@ export function IndexManagementPageProvider({ getService }: FtrProviderContext) await testSubjects.click(tab); }, + async changeMappingsEditorTab( + tab: 'mappedFields' | 'runtimeFields' | 'dynamicTemplates' | 'advancedOptions' + ) { + const index = [ + 'mappedFields', + 'runtimeFields', + 'dynamicTemplates', + 'advancedOptions', + ].indexOf(tab); + + const tabs = await testSubjects.findAll('formTab'); + await tabs[index].click(); + }, + async clickNextButton() { await testSubjects.click('nextButton'); },