From a64618076253010744deb31fb90d26577bed1a0c Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 16 Mar 2022 17:50:19 +0100 Subject: [PATCH 1/3] Revert "2870 removing options mapping" This reverts commit d9ba8b335895436d87974f26f5dfa4e71e81075d. --- .../formBuilder/FormBuilder.test.tsx | 2 + .../formBuilder/formBuilderHelpers.test.ts | 103 ++++++++++++++++++ .../formBuilder/formBuilderHelpers.ts | 11 +- 3 files changed, 114 insertions(+), 2 deletions(-) diff --git a/src/components/formBuilder/FormBuilder.test.tsx b/src/components/formBuilder/FormBuilder.test.tsx index a5841804f3..aae7679d4e 100644 --- a/src/components/formBuilder/FormBuilder.test.tsx +++ b/src/components/formBuilder/FormBuilder.test.tsx @@ -146,6 +146,8 @@ describe("Dropdown with labels field", () => { fireTextInput(firstOptionLabelInput, "Test option"); await waitForEffect(); + screen.debug(rendered.container.querySelector(".rjsf")); + // Validate the rendered option const optionElement = screen.queryByRole("option", { name: "Test option" }); expect(optionElement).not.toBeNull(); diff --git a/src/components/formBuilder/formBuilderHelpers.test.ts b/src/components/formBuilder/formBuilderHelpers.test.ts index d8aa082ee6..bb718a132c 100644 --- a/src/components/formBuilder/formBuilderHelpers.test.ts +++ b/src/components/formBuilder/formBuilderHelpers.test.ts @@ -215,3 +215,106 @@ describe("normalizeUiOrder", () => { expect(actual).toEqual(["propA", "propC", "*"]); }); }); + +describe("produceSchemaOnUiTypeChange", () => { + test("converts Dropdown to Dropdown with labels", () => { + const schema: RJSFSchema = { + schema: { + ...MINIMAL_SCHEMA, + properties: { + field1: { + title: "Field 1", + type: "string", + enum: ["foo", "bar", "baz"], + }, + }, + }, + uiSchema: { + ...MINIMAL_UI_SCHEMA, + field1: { + [UI_WIDGET]: "select", + }, + }, + }; + + const nextSchema = produceSchemaOnUiTypeChange( + schema, + "field1", + stringifyUiType({ + propertyType: "string", + uiWidget: "select", + extra: "selectWithLabels", + }) + ); + + expect( + (nextSchema.schema.properties.field1 as Schema).enum + ).toBeUndefined(); + expect((nextSchema.schema.properties.field1 as Schema).oneOf).toEqual([ + { + const: "foo", + title: "foo", + }, + { + const: "bar", + title: "bar", + }, + { + const: "baz", + title: "baz", + }, + ]); + }); + + test("converts Dropdown with labels to Dropdown", () => { + const schema: RJSFSchema = { + schema: { + ...MINIMAL_SCHEMA, + properties: { + field1: { + title: "Field 1", + type: "string", + oneOf: [ + { + const: "foo", + title: "Foo", + }, + { + const: "bar", + title: "Bar", + }, + { + const: "baz", + title: "Baz", + }, + ], + }, + }, + }, + uiSchema: { + ...MINIMAL_UI_SCHEMA, + field1: { + [UI_WIDGET]: "select", + }, + }, + }; + + const nextSchema = produceSchemaOnUiTypeChange( + schema, + "field1", + stringifyUiType({ + propertyType: "string", + uiWidget: "select", + }) + ); + + expect( + (nextSchema.schema.properties.field1 as Schema).oneOf + ).toBeUndefined(); + expect((nextSchema.schema.properties.field1 as Schema).enum).toEqual([ + "foo", + "bar", + "baz", + ]); + }); +}); diff --git a/src/components/formBuilder/formBuilderHelpers.ts b/src/components/formBuilder/formBuilderHelpers.ts index 6d1c07e532..0a57eefebc 100644 --- a/src/components/formBuilder/formBuilderHelpers.ts +++ b/src/components/formBuilder/formBuilderHelpers.ts @@ -19,6 +19,7 @@ import { KEYS_OF_UI_SCHEMA, SafeString, Schema, + SchemaDefinition, SchemaPropertyType, UiSchema, } from "@/core"; @@ -297,11 +298,17 @@ export const produceSchemaOnUiTypeChange = ( if (uiWidget === "select") { if (extra === "selectWithLabels") { - draftPropertySchema.oneOf = []; + // If switching from Dropdown, convert the enum to options with labels + draftPropertySchema.oneOf = (draftPropertySchema.enum ?? []).map( + (item) => ({ const: item, title: item } as SchemaDefinition) + ); draftPropertySchema.default = ""; delete draftPropertySchema.enum; } else { - draftPropertySchema.enum = []; + // If switching from Dropdown with labels, convert the values to enum + draftPropertySchema.enum = (draftPropertySchema.oneOf ?? []).map( + (item: Schema) => item.const + ); delete draftPropertySchema.oneOf; } } else { From b4a40953a2a14272d061fc8b6c8642ae6ccbf8e7 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 16 Mar 2022 20:52:15 +0100 Subject: [PATCH 2/3] 2870 map DD to DD with labels. Tests --- .../formBuilder/FormBuilder.test.tsx | 182 ++++++++++++++---- .../formBuilder/formBuilderHelpers.test.ts | 7 +- .../formBuilder/formBuilderHelpers.ts | 14 +- .../preview/FormPreviewSchemaField.tsx | 38 ++-- 4 files changed, 180 insertions(+), 61 deletions(-) diff --git a/src/components/formBuilder/FormBuilder.test.tsx b/src/components/formBuilder/FormBuilder.test.tsx index aae7679d4e..7cb7d4adb0 100644 --- a/src/components/formBuilder/FormBuilder.test.tsx +++ b/src/components/formBuilder/FormBuilder.test.tsx @@ -69,6 +69,26 @@ async function selectUiType(uiType: string) { } describe("Dropdown field", () => { + async function addOption() { + // Add a text option + screen.getByText("Add Item").click(); + const firstOption = rendered.container.querySelector( + `[name="form.schema.properties.${defaultFieldName}.enum.0"]` + ); + fireTextInput(firstOption, "Test option"); + await waitForEffect(); + } + + async function setVarValue() { + // Switch to @var and inset "@data" + await selectSchemaFieldType( + `form.schema.properties.${defaultFieldName}.enum`, + "var" + ); + fireTextInput(screen.getByLabelText("Options"), "@data"); + await waitForEffect(); + } + let rendered: RenderResult; beforeEach(async () => { rendered = renderFormBuilder(); @@ -85,13 +105,7 @@ describe("Dropdown field", () => { }); test("can add an option", async () => { - // Add a text option - screen.getByText("Add Item").click(); - const firstOption = rendered.container.querySelector( - `[name="form.schema.properties.${defaultFieldName}.enum.0"]` - ); - fireTextInput(firstOption, "Test option"); - await waitForEffect(); + await addOption(); // Expect the dropdown option rendered in the preview expect( @@ -100,35 +114,70 @@ describe("Dropdown field", () => { }); test("can use @var", async () => { - // Switch to @var and inset "@data" - await selectSchemaFieldType( - `form.schema.properties.${defaultFieldName}.enum`, - "var" - ); - fireTextInput(screen.getByLabelText("Options"), "@data"); - await waitForEffect(); + await setVarValue(); // Expect the dropdown option rendered in the preview expect(screen.queryByRole("option", { name: "@data" })).not.toBeNull(); }); -}); -describe("Dropdown with labels field", () => { - let rendered: RenderResult; - beforeEach(async () => { - rendered = renderFormBuilder(); + describe("can be switched to Dropdown with labels", () => { + test("with items", async () => { + await addOption(); - // Switch to Dropdown widget - await selectUiType("Dropdown with labels"); - }); - test("doesn't fail when field type changed to Dropdown with labels", async () => { - // Expect the dropdown rendered in the preview - expect( - rendered.container.querySelector(`select#root_${defaultFieldName}`) - ).not.toBeNull(); + // Switch to Dropdown widget + await selectUiType("Dropdown with labels"); + + // Expect the ui type has changed + expect( + screen.queryByLabelText("Input Type")?.parentElement?.parentElement + ).toHaveTextContent("Dropdown with labels"); + + // Expect the dropdown option added in the Editor + const firstOptionValueInput = rendered.container.querySelector( + `[name="form.schema.properties.${defaultFieldName}.oneOf.0.const"]` + ); + // Option value mapped from Dropdown + expect(firstOptionValueInput).toHaveValue("Test option"); + + const firstOptionTitleInput = rendered.container.querySelector( + `[name="form.schema.properties.${defaultFieldName}.oneOf.0.title"]` + ); + // Option title is empty + expect(firstOptionTitleInput).toHaveValue(""); + + // Expect the dropdown option rendered in the preview + expect( + screen.queryByRole("option", { name: "Test option" }) + ).not.toBeNull(); + }); + + test("with @var", async () => { + await setVarValue(); + + // Switch to Dropdown widget + await selectUiType("Dropdown with labels"); + + // Expect the ui type has changed + expect( + screen.queryByLabelText("Input Type")?.parentElement?.parentElement + ).toHaveTextContent("Dropdown with labels"); + + // Expect the dropdown options is empty + const firstOptionValueInput = rendered.container.querySelector( + `[name="form.schema.properties.${defaultFieldName}.oneOf.0.const"]` + ); + expect(firstOptionValueInput).toBeNull(); + + const firstOptionTitleInput = rendered.container.querySelector( + `[name="form.schema.properties.${defaultFieldName}.oneOf.0.title"]` + ); + expect(firstOptionTitleInput).toBeNull(); + }); }); +}); - test("can add an option", async () => { +describe("Dropdown with labels field", () => { + async function addOption() { // Add a text option screen.getByText("Add Item").click(); @@ -145,8 +194,34 @@ describe("Dropdown with labels field", () => { ); fireTextInput(firstOptionLabelInput, "Test option"); await waitForEffect(); + } + + async function setVarValue() { + // Switch to @var and inset "@data" + await selectSchemaFieldType( + `form.schema.properties.${defaultFieldName}.oneOf`, + "var" + ); + fireTextInput(screen.getByLabelText("Options"), "@data"); + await waitForEffect(); + } - screen.debug(rendered.container.querySelector(".rjsf")); + let rendered: RenderResult; + beforeEach(async () => { + rendered = renderFormBuilder(); + + // Switch to Dropdown widget + await selectUiType("Dropdown with labels"); + }); + test("doesn't fail when field type changed to Dropdown with labels", async () => { + // Expect the dropdown rendered in the preview + expect( + rendered.container.querySelector(`select#root_${defaultFieldName}`) + ).not.toBeNull(); + }); + + test("can add an option", async () => { + await addOption(); // Validate the rendered option const optionElement = screen.queryByRole("option", { name: "Test option" }); @@ -155,15 +230,50 @@ describe("Dropdown with labels field", () => { }); test("can use @var in Dropdown", async () => { - // Switch to @var and inset "@data" - await selectSchemaFieldType( - `form.schema.properties.${defaultFieldName}.oneOf`, - "var" - ); - fireTextInput(screen.getByLabelText("Options"), "@data"); - await waitForEffect(); + await setVarValue(); // Expect the dropdown option rendered in the preview expect(screen.queryByRole("option", { name: "@data" })).not.toBeNull(); }); + + describe("can be switched to regular Dropdown", () => { + test("with items", async () => { + await addOption(); + + // Switch to Dropdown widget + await selectUiType("Dropdown"); + + // Expect the ui type has changed + expect( + screen.queryByLabelText("Input Type")?.parentElement?.parentElement + ).toHaveTextContent("Dropdown"); + + // Expect the dropdown option added in the Editor + const firstOptionInput = rendered.container.querySelector( + `[name="form.schema.properties.${defaultFieldName}.enum.0"]` + ); + expect(firstOptionInput).toHaveValue("1"); + + // Expect the dropdown option rendered in the preview + expect(screen.queryByRole("option", { name: "1" })).not.toBeNull(); + }); + + test("with @var", async () => { + await setVarValue(); + + // Switch to Dropdown widget + await selectUiType("Dropdown"); + + // Expect the ui type has changed + expect( + screen.queryByLabelText("Input Type")?.parentElement?.parentElement + ).toHaveTextContent("Dropdown"); + + // Expect the dropdown options is empty + const firstOptionInput = rendered.container.querySelector( + `[name="form.schema.properties.${defaultFieldName}.enum.0"]` + ); + expect(firstOptionInput).toBeNull(); + }); + }); }); diff --git a/src/components/formBuilder/formBuilderHelpers.test.ts b/src/components/formBuilder/formBuilderHelpers.test.ts index bb718a132c..c5ea313327 100644 --- a/src/components/formBuilder/formBuilderHelpers.test.ts +++ b/src/components/formBuilder/formBuilderHelpers.test.ts @@ -22,13 +22,15 @@ import { MINIMAL_UI_SCHEMA, normalizeUiOrder, produceSchemaOnPropertyNameChange, + produceSchemaOnUiTypeChange, replaceStringInArray, + stringifyUiType, updateRjsfSchemaWithDefaultsIfNeeded, validateNextPropertyName, } from "./formBuilderHelpers"; import { RJSFSchema } from "./formBuilderTypes"; import { initRenamingCases } from "./formEditor.testCases"; -import { UI_ORDER } from "./schemaFieldNames"; +import { UI_ORDER, UI_WIDGET } from "./schemaFieldNames"; describe("replaceStringInArray", () => { let array: string[]; @@ -253,15 +255,12 @@ describe("produceSchemaOnUiTypeChange", () => { expect((nextSchema.schema.properties.field1 as Schema).oneOf).toEqual([ { const: "foo", - title: "foo", }, { const: "bar", - title: "bar", }, { const: "baz", - title: "baz", }, ]); }); diff --git a/src/components/formBuilder/formBuilderHelpers.ts b/src/components/formBuilder/formBuilderHelpers.ts index 0a57eefebc..cdd511300e 100644 --- a/src/components/formBuilder/formBuilderHelpers.ts +++ b/src/components/formBuilder/formBuilderHelpers.ts @@ -299,16 +299,18 @@ export const produceSchemaOnUiTypeChange = ( if (uiWidget === "select") { if (extra === "selectWithLabels") { // If switching from Dropdown, convert the enum to options with labels - draftPropertySchema.oneOf = (draftPropertySchema.enum ?? []).map( - (item) => ({ const: item, title: item } as SchemaDefinition) - ); + draftPropertySchema.oneOf = Array.isArray(draftPropertySchema.enum) + ? draftPropertySchema.enum.map( + (item) => ({ const: item } as SchemaDefinition) + ) + : []; draftPropertySchema.default = ""; delete draftPropertySchema.enum; } else { // If switching from Dropdown with labels, convert the values to enum - draftPropertySchema.enum = (draftPropertySchema.oneOf ?? []).map( - (item: Schema) => item.const - ); + draftPropertySchema.enum = Array.isArray(draftPropertySchema.oneOf) + ? draftPropertySchema.oneOf.map((item: Schema) => item.const) + : []; delete draftPropertySchema.oneOf; } } else { diff --git a/src/components/formBuilder/preview/FormPreviewSchemaField.tsx b/src/components/formBuilder/preview/FormPreviewSchemaField.tsx index 133c61f36e..6f53fd9ee5 100644 --- a/src/components/formBuilder/preview/FormPreviewSchemaField.tsx +++ b/src/components/formBuilder/preview/FormPreviewSchemaField.tsx @@ -18,7 +18,6 @@ import { Theme as RjsfTheme } from "@rjsf/bootstrap-4"; import React from "react"; import { FormPreviewFieldProps } from "./FormPreviewFieldTemplate"; -import { produce } from "immer"; import { SchemaDefinition } from "@/core"; const RjsfSchemaField = RjsfTheme.fields.SchemaField; @@ -28,21 +27,30 @@ const FormPreviewSchemaField: React.FC = (props) => { // If we render a dropdown with @var value, use the name of the var and the single option if (typeof props.schema.oneOf === "string") { - fieldProps = produce(props, (draft) => { - draft.schema.oneOf = [ - { - const: props.schema.oneOf, - } as SchemaDefinition, - ]; - - draft.disabled = true; - }); + // Not using immer.produce to clone `props` because it accesses `props.key` that throws an error + fieldProps = { + ...props, + disabled: true, + schema: { + ...props.schema, + oneOf: [ + { + const: props.schema.oneOf, + } as SchemaDefinition, + ], + }, + }; } else if (typeof props.schema.enum === "string") { - fieldProps = produce(props, (draft) => { - draft.schema.enum = [props.schema.enum]; - draft.schema.default = props.schema.enum; - draft.disabled = true; - }); + // Not using immer.produce to clone `props` because it accesses `props.key` that throws an error + fieldProps = { + ...props, + disabled: true, + schema: { + ...props.schema, + enum: [props.schema.enum], + default: props.schema.enum, + }, + }; } else { fieldProps = props; } From 0d0fda4e00d0c625a05f7df9c6fff9deb80a4fd6 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 17 Mar 2022 13:54:52 +0100 Subject: [PATCH 3/3] 2870 typo --- src/components/formBuilder/FormBuilder.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/formBuilder/FormBuilder.test.tsx b/src/components/formBuilder/FormBuilder.test.tsx index 7cb7d4adb0..5336c3ef39 100644 --- a/src/components/formBuilder/FormBuilder.test.tsx +++ b/src/components/formBuilder/FormBuilder.test.tsx @@ -80,7 +80,7 @@ describe("Dropdown field", () => { } async function setVarValue() { - // Switch to @var and inset "@data" + // Switch to @var and insert "@data" await selectSchemaFieldType( `form.schema.properties.${defaultFieldName}.enum`, "var"