From 2daaddc3f5190037bd7f4973e80b1f560d0c5297 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 9 Mar 2022 16:34:52 +0100 Subject: [PATCH 01/13] 2870 using ArrayWidget for dropdown options in the Form Builder --- .../schemaFields/widgets/ArrayWidget.tsx | 2 +- src/components/formBuilder/FormBuilder.tsx | 4 ++-- .../{ => edit}/FieldEditor.module.scss | 0 .../formBuilder/{ => edit}/FieldEditor.tsx | 22 ++++++++++++------- .../{ => edit}/FormEditor.module.scss | 0 .../{ => edit}/FormEditor.test.tsx | 4 ++-- .../formBuilder/{ => edit}/FormEditor.tsx | 6 ++--- .../__snapshots__/FormEditor.test.tsx.snap | 0 .../{ => preview}/FormPreview.test.tsx | 0 .../formBuilder/{ => preview}/FormPreview.tsx | 13 ++++++----- .../FormPreviewBooleanField.module.scss | 0 .../{ => preview}/FormPreviewBooleanField.tsx | 0 .../FormPreviewFieldTemplate.module.scss | 0 .../FormPreviewFieldTemplate.tsx | 4 ++-- .../FormPreviewStringField.module.scss | 0 .../{ => preview}/FormPreviewStringField.tsx | 0 .../{ => preview}/ImageCropWidgetPreview.tsx | 0 .../__snapshots__/FormPreview.test.tsx.snap | 0 src/pageEditor/fields/FormModalOptions.tsx | 2 +- src/pageEditor/fields/FormRendererOptions.tsx | 2 +- .../BlueprintOptionsTab.tsx | 4 ++-- .../tabs/editTab/dataPanel/DataPanel.tsx | 2 +- 22 files changed, 37 insertions(+), 28 deletions(-) rename src/components/formBuilder/{ => edit}/FieldEditor.module.scss (100%) rename src/components/formBuilder/{ => edit}/FieldEditor.tsx (94%) rename src/components/formBuilder/{ => edit}/FormEditor.module.scss (100%) rename src/components/formBuilder/{ => edit}/FormEditor.test.tsx (98%) rename src/components/formBuilder/{ => edit}/FormEditor.tsx (97%) rename src/components/formBuilder/{ => edit}/__snapshots__/FormEditor.test.tsx.snap (100%) rename src/components/formBuilder/{ => preview}/FormPreview.test.tsx (100%) rename src/components/formBuilder/{ => preview}/FormPreview.tsx (89%) rename src/components/formBuilder/{ => preview}/FormPreviewBooleanField.module.scss (100%) rename src/components/formBuilder/{ => preview}/FormPreviewBooleanField.tsx (100%) rename src/components/formBuilder/{ => preview}/FormPreviewFieldTemplate.module.scss (100%) rename src/components/formBuilder/{ => preview}/FormPreviewFieldTemplate.tsx (92%) rename src/components/formBuilder/{ => preview}/FormPreviewStringField.module.scss (100%) rename src/components/formBuilder/{ => preview}/FormPreviewStringField.tsx (100%) rename src/components/formBuilder/{ => preview}/ImageCropWidgetPreview.tsx (100%) rename src/components/formBuilder/{ => preview}/__snapshots__/FormPreview.test.tsx.snap (100%) diff --git a/src/components/fields/schemaFields/widgets/ArrayWidget.tsx b/src/components/fields/schemaFields/widgets/ArrayWidget.tsx index 774febbe04..c35b37a3d2 100644 --- a/src/components/fields/schemaFields/widgets/ArrayWidget.tsx +++ b/src/components/fields/schemaFields/widgets/ArrayWidget.tsx @@ -80,7 +80,7 @@ const ArrayWidget: React.FC = ({ schema, name }) => { {({ push }) => ( <> -
    +
      {(field.value ?? []).map((item: unknown, index: number) => (
    • + )} diff --git a/src/components/formBuilder/FormEditor.module.scss b/src/components/formBuilder/edit/FormEditor.module.scss similarity index 100% rename from src/components/formBuilder/FormEditor.module.scss rename to src/components/formBuilder/edit/FormEditor.module.scss diff --git a/src/components/formBuilder/FormEditor.test.tsx b/src/components/formBuilder/edit/FormEditor.test.tsx similarity index 98% rename from src/components/formBuilder/FormEditor.test.tsx rename to src/components/formBuilder/edit/FormEditor.test.tsx index 57d0c798c3..02a8377765 100644 --- a/src/components/formBuilder/FormEditor.test.tsx +++ b/src/components/formBuilder/edit/FormEditor.test.tsx @@ -26,13 +26,13 @@ import { fireTextInput, fireFormSubmit, } from "@/tests/formHelpers"; -import { RJSFSchema } from "./formBuilderTypes"; +import { RJSFSchema } from "@/components/formBuilder/formBuilderTypes"; import FormEditor, { FormEditorProps } from "./FormEditor"; import { initAddingFieldCases, initOneFieldSchemaCase, initRenamingCases, -} from "./formEditor.testCases"; +} from "@/components/formBuilder/formEditor.testCases"; import selectEvent from "react-select-event"; import userEvent from "@testing-library/user-event"; diff --git a/src/components/formBuilder/FormEditor.tsx b/src/components/formBuilder/edit/FormEditor.tsx similarity index 97% rename from src/components/formBuilder/FormEditor.tsx rename to src/components/formBuilder/edit/FormEditor.tsx index d7b2566119..6dc102c3b4 100644 --- a/src/components/formBuilder/FormEditor.tsx +++ b/src/components/formBuilder/edit/FormEditor.tsx @@ -23,7 +23,7 @@ import { RJSFSchema, SelectStringOption, SetActiveField, -} from "./formBuilderTypes"; +} from "@/components/formBuilder/formBuilderTypes"; import { Button, Col, Row } from "react-bootstrap"; import FieldEditor from "./FieldEditor"; import { @@ -33,8 +33,8 @@ import { normalizeUiOrder, replaceStringInArray, updateRjsfSchemaWithDefaultsIfNeeded, -} from "./formBuilderHelpers"; -import { UI_ORDER } from "./schemaFieldNames"; +} from "@/components/formBuilder/formBuilderHelpers"; +import { UI_ORDER } from "@/components/formBuilder/schemaFieldNames"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faPlus, faTrash } from "@fortawesome/free-solid-svg-icons"; import { Schema } from "@/core"; diff --git a/src/components/formBuilder/__snapshots__/FormEditor.test.tsx.snap b/src/components/formBuilder/edit/__snapshots__/FormEditor.test.tsx.snap similarity index 100% rename from src/components/formBuilder/__snapshots__/FormEditor.test.tsx.snap rename to src/components/formBuilder/edit/__snapshots__/FormEditor.test.tsx.snap diff --git a/src/components/formBuilder/FormPreview.test.tsx b/src/components/formBuilder/preview/FormPreview.test.tsx similarity index 100% rename from src/components/formBuilder/FormPreview.test.tsx rename to src/components/formBuilder/preview/FormPreview.test.tsx diff --git a/src/components/formBuilder/FormPreview.tsx b/src/components/formBuilder/preview/FormPreview.tsx similarity index 89% rename from src/components/formBuilder/FormPreview.tsx rename to src/components/formBuilder/preview/FormPreview.tsx index cda4b68c39..0519747e9f 100644 --- a/src/components/formBuilder/FormPreview.tsx +++ b/src/components/formBuilder/preview/FormPreview.tsx @@ -19,16 +19,19 @@ import React, { useCallback, useEffect, useMemo, useState } from "react"; import JsonSchemaForm from "@rjsf/bootstrap-4"; import { FieldProps, IChangeEvent } from "@rjsf/core"; -import { RJSFSchema, SetActiveField } from "./formBuilderTypes"; +import { + RJSFSchema, + SetActiveField, +} from "@/components/formBuilder/formBuilderTypes"; import FormPreviewStringField from "./FormPreviewStringField"; -import { UI_SCHEMA_ACTIVE } from "./schemaFieldNames"; +import { UI_SCHEMA_ACTIVE } from "@/components/formBuilder/schemaFieldNames"; import { produce } from "immer"; import FormPreviewBooleanField from "./FormPreviewBooleanField"; import { getPreviewValues } from "@/components/fields/fieldUtils"; -import ImageCropWidgetPreview from "@/components/formBuilder/ImageCropWidgetPreview"; +import ImageCropWidgetPreview from "@/components/formBuilder/preview/ImageCropWidgetPreview"; // eslint-disable-next-line import/no-named-as-default -- need default export here -import DescriptionField from "./DescriptionField"; -import FieldTemplate from "./FieldTemplate"; +import DescriptionField from "@/components/formBuilder/DescriptionField"; +import FieldTemplate from "@/components/formBuilder/FieldTemplate"; export type FormPreviewProps = { rjsfSchema: RJSFSchema; diff --git a/src/components/formBuilder/FormPreviewBooleanField.module.scss b/src/components/formBuilder/preview/FormPreviewBooleanField.module.scss similarity index 100% rename from src/components/formBuilder/FormPreviewBooleanField.module.scss rename to src/components/formBuilder/preview/FormPreviewBooleanField.module.scss diff --git a/src/components/formBuilder/FormPreviewBooleanField.tsx b/src/components/formBuilder/preview/FormPreviewBooleanField.tsx similarity index 100% rename from src/components/formBuilder/FormPreviewBooleanField.tsx rename to src/components/formBuilder/preview/FormPreviewBooleanField.tsx diff --git a/src/components/formBuilder/FormPreviewFieldTemplate.module.scss b/src/components/formBuilder/preview/FormPreviewFieldTemplate.module.scss similarity index 100% rename from src/components/formBuilder/FormPreviewFieldTemplate.module.scss rename to src/components/formBuilder/preview/FormPreviewFieldTemplate.module.scss diff --git a/src/components/formBuilder/FormPreviewFieldTemplate.tsx b/src/components/formBuilder/preview/FormPreviewFieldTemplate.tsx similarity index 92% rename from src/components/formBuilder/FormPreviewFieldTemplate.tsx rename to src/components/formBuilder/preview/FormPreviewFieldTemplate.tsx index ef6da65b4f..15a7594cfe 100644 --- a/src/components/formBuilder/FormPreviewFieldTemplate.tsx +++ b/src/components/formBuilder/preview/FormPreviewFieldTemplate.tsx @@ -19,8 +19,8 @@ import styles from "./FormPreviewFieldTemplate.module.scss"; import { Field, FieldProps } from "@rjsf/core"; import React from "react"; -import { SetActiveField } from "./formBuilderTypes"; -import { UI_SCHEMA_ACTIVE } from "./schemaFieldNames"; +import { SetActiveField } from "@/components/formBuilder/formBuilderTypes"; +import { UI_SCHEMA_ACTIVE } from "@/components/formBuilder/schemaFieldNames"; import cx from "classnames"; export interface FormPreviewFieldProps extends FieldProps { diff --git a/src/components/formBuilder/FormPreviewStringField.module.scss b/src/components/formBuilder/preview/FormPreviewStringField.module.scss similarity index 100% rename from src/components/formBuilder/FormPreviewStringField.module.scss rename to src/components/formBuilder/preview/FormPreviewStringField.module.scss diff --git a/src/components/formBuilder/FormPreviewStringField.tsx b/src/components/formBuilder/preview/FormPreviewStringField.tsx similarity index 100% rename from src/components/formBuilder/FormPreviewStringField.tsx rename to src/components/formBuilder/preview/FormPreviewStringField.tsx diff --git a/src/components/formBuilder/ImageCropWidgetPreview.tsx b/src/components/formBuilder/preview/ImageCropWidgetPreview.tsx similarity index 100% rename from src/components/formBuilder/ImageCropWidgetPreview.tsx rename to src/components/formBuilder/preview/ImageCropWidgetPreview.tsx diff --git a/src/components/formBuilder/__snapshots__/FormPreview.test.tsx.snap b/src/components/formBuilder/preview/__snapshots__/FormPreview.test.tsx.snap similarity index 100% rename from src/components/formBuilder/__snapshots__/FormPreview.test.tsx.snap rename to src/components/formBuilder/preview/__snapshots__/FormPreview.test.tsx.snap diff --git a/src/pageEditor/fields/FormModalOptions.tsx b/src/pageEditor/fields/FormModalOptions.tsx index a066d65fd9..b707ba6cae 100644 --- a/src/pageEditor/fields/FormModalOptions.tsx +++ b/src/pageEditor/fields/FormModalOptions.tsx @@ -21,7 +21,7 @@ import React, { useEffect } from "react"; import { validateRegistryId } from "@/types/helpers"; import { actions as formBuilderActions } from "@/pageEditor/slices/formBuilderSlice"; import formBuilderSelectors from "@/pageEditor/slices/formBuilderSelectors"; -import FormEditor from "@/components/formBuilder/FormEditor"; +import FormEditor from "@/components/formBuilder/edit/FormEditor"; import useReduxState from "@/hooks/useReduxState"; import ConfigErrorBoundary from "@/pageEditor/fields/ConfigErrorBoundary"; diff --git a/src/pageEditor/fields/FormRendererOptions.tsx b/src/pageEditor/fields/FormRendererOptions.tsx index a7c8beea06..7b26cb1089 100644 --- a/src/pageEditor/fields/FormRendererOptions.tsx +++ b/src/pageEditor/fields/FormRendererOptions.tsx @@ -19,7 +19,7 @@ import SchemaField from "@/components/fields/schemaFields/SchemaField"; import { Schema } from "@/core"; import React, { useEffect } from "react"; import { validateRegistryId } from "@/types/helpers"; -import FormEditor from "@/components/formBuilder/FormEditor"; +import FormEditor from "@/components/formBuilder/edit/FormEditor"; import { actions as elementWizardActions } from "@/pageEditor/slices/formBuilderSlice"; import formBuilderSelectors from "@/pageEditor/slices/formBuilderSelectors"; import useReduxState from "@/hooks/useReduxState"; diff --git a/src/pageEditor/tabs/blueprintOptionsTab/BlueprintOptionsTab.tsx b/src/pageEditor/tabs/blueprintOptionsTab/BlueprintOptionsTab.tsx index 4a3e647c23..03adeb8ba0 100644 --- a/src/pageEditor/tabs/blueprintOptionsTab/BlueprintOptionsTab.tsx +++ b/src/pageEditor/tabs/blueprintOptionsTab/BlueprintOptionsTab.tsx @@ -23,8 +23,8 @@ import React, { useMemo, useState } from "react"; import { Alert, Col, Container, Nav, Row, Tab } from "react-bootstrap"; import { FormState } from "@/pageEditor/slices/editorSlice"; import { RJSFSchema } from "@/components/formBuilder/formBuilderTypes"; -import FormEditor from "@/components/formBuilder/FormEditor"; -import FormPreview from "@/components/formBuilder/FormPreview"; +import FormEditor from "@/components/formBuilder/edit/FormEditor"; +import FormPreview from "@/components/formBuilder/preview/FormPreview"; import Loader from "@/components/Loader"; import FieldRuntimeContext, { RuntimeContext, diff --git a/src/pageEditor/tabs/editTab/dataPanel/DataPanel.tsx b/src/pageEditor/tabs/editTab/dataPanel/DataPanel.tsx index ed9a30875e..9fe80f21d5 100644 --- a/src/pageEditor/tabs/editTab/dataPanel/DataPanel.tsx +++ b/src/pageEditor/tabs/editTab/dataPanel/DataPanel.tsx @@ -25,7 +25,7 @@ import { actions } from "@/pageEditor/slices/formBuilderSlice"; import { Alert, Button, Nav, Tab } from "react-bootstrap"; import JsonTree from "@/components/jsonTree/JsonTree"; import dataPanelStyles from "@/pageEditor/tabs/dataPanelTabs.module.scss"; -import FormPreview from "@/components/formBuilder/FormPreview"; +import FormPreview from "@/components/formBuilder/preview/FormPreview"; import ErrorBoundary from "@/components/ErrorBoundary"; import BlockPreview, { usePreviewInfo, From e96fa4971e53dbae3961b0c039e380f385f3e08b Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 9 Mar 2022 18:35:02 +0100 Subject: [PATCH 02/13] 2870 fixing preview --- .../formBuilder/edit/FieldEditor.tsx | 9 ++--- .../formBuilder/preview/FormPreview.tsx | 2 + .../preview/SelectWidgetPreview.tsx | 40 +++++++++++++++++++ 3 files changed, 45 insertions(+), 6 deletions(-) create mode 100644 src/components/formBuilder/preview/SelectWidgetPreview.tsx diff --git a/src/components/formBuilder/edit/FieldEditor.tsx b/src/components/formBuilder/edit/FieldEditor.tsx index d98965ced0..7001296855 100644 --- a/src/components/formBuilder/edit/FieldEditor.tsx +++ b/src/components/formBuilder/edit/FieldEditor.tsx @@ -253,17 +253,14 @@ const FieldEditor: React.FC<{ )} {propertySchema.enum && ( - // )} diff --git a/src/components/formBuilder/preview/FormPreview.tsx b/src/components/formBuilder/preview/FormPreview.tsx index 0519747e9f..b55f1648e3 100644 --- a/src/components/formBuilder/preview/FormPreview.tsx +++ b/src/components/formBuilder/preview/FormPreview.tsx @@ -32,6 +32,7 @@ import ImageCropWidgetPreview from "@/components/formBuilder/preview/ImageCropWi // eslint-disable-next-line import/no-named-as-default -- need default export here import DescriptionField from "@/components/formBuilder/DescriptionField"; import FieldTemplate from "@/components/formBuilder/FieldTemplate"; +import SelectWidgetPreview from "./SelectWidgetPreview"; export type FormPreviewProps = { rjsfSchema: RJSFSchema; @@ -102,6 +103,7 @@ const FormPreview: React.FC = ({ const widgets = { imageCrop: ImageCropWidgetPreview, + SelectWidget: SelectWidgetPreview, }; return ( diff --git a/src/components/formBuilder/preview/SelectWidgetPreview.tsx b/src/components/formBuilder/preview/SelectWidgetPreview.tsx new file mode 100644 index 0000000000..b22240e26c --- /dev/null +++ b/src/components/formBuilder/preview/SelectWidgetPreview.tsx @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2022 PixieBrix, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import { utils, WidgetProps } from "@rjsf/core"; +import React from "react"; +import { FormGroup, FormLabel } from "react-bootstrap"; + +const RjsfArrayField = utils.getDefaultRegistry().widgets.SelectWidget; + +const SelectWidgetPreview: React.VFC = (props) => { + console.log("FormPreviewArrayField props:", props); + if (typeof props.schema.enum === "string") { + return ( + + {props.schema.title} +
      + Dropdown options defined by a variable are not displayed in preview. +
      +
      + ); + } + + return ; +}; + +export default SelectWidgetPreview; From e16575ba9e7dfbd281688bdb23564041f8406ec8 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 11 Mar 2022 14:31:41 +0100 Subject: [PATCH 03/13] 2870 Pull fields and widgets from BS4 theme --- .../formBuilder/edit/FieldEditor.tsx | 33 ++++++++++----- .../FormPreviewBooleanField.module.scss | 4 -- .../preview/FormPreviewBooleanField.tsx | 4 +- .../preview/FormPreviewStringField.tsx | 4 +- .../preview/SelectWidgetPreview.tsx | 41 +++++++++++++------ 5 files changed, 56 insertions(+), 30 deletions(-) diff --git a/src/components/formBuilder/edit/FieldEditor.tsx b/src/components/formBuilder/edit/FieldEditor.tsx index 7001296855..5d4cf8455d 100644 --- a/src/components/formBuilder/edit/FieldEditor.tsx +++ b/src/components/formBuilder/edit/FieldEditor.tsx @@ -253,16 +253,29 @@ const FieldEditor: React.FC<{ )} {propertySchema.enum && ( - + <> + + + + )} . */ -import { utils } from "@rjsf/core"; +import { Theme as RjsfTheme } from "@rjsf/bootstrap-4"; import React from "react"; import FormPreviewFieldTemplate, { FormPreviewFieldProps, } from "./FormPreviewFieldTemplate"; import styles from "./FormPreviewBooleanField.module.scss"; -const RjsfBooleanField = utils.getDefaultRegistry().fields.BooleanField; +const RjsfBooleanField = RjsfTheme.fields.BooleanField; const FormPreviewBooleanField: React.FC = (props) => ( . */ -import { utils } from "@rjsf/core"; +import { Theme as RjsfTheme } from "@rjsf/bootstrap-4"; import React from "react"; import FormPreviewFieldTemplate, { FormPreviewFieldProps, } from "./FormPreviewFieldTemplate"; import styles from "./FormPreviewBooleanField.module.scss"; -const RjsfStringField = utils.getDefaultRegistry().fields.StringField; +const RjsfStringField = RjsfTheme.fields.StringField; const FormPreviewStringField: React.FC = (props) => ( . */ -import { utils, WidgetProps } from "@rjsf/core"; +import { WidgetProps } from "@rjsf/core"; +import { Theme as RjsfTheme } from "@rjsf/bootstrap-4"; import React from "react"; -import { FormGroup, FormLabel } from "react-bootstrap"; -const RjsfArrayField = utils.getDefaultRegistry().widgets.SelectWidget; +const RjsfSelectWidget = RjsfTheme.widgets.SelectWidget; const SelectWidgetPreview: React.VFC = (props) => { - console.log("FormPreviewArrayField props:", props); - if (typeof props.schema.enum === "string") { + // If Select Options is a variable, then `props.schema.enum` holds the name of the variable (i.e. string). + const { enum: values } = props.schema; + if (typeof values === "string") { + // @ts-expect-error -- enumNames is a valid property of the RJSF schema. + const { enumNames: labels } = props.schema; + const enumOptions = [ + { + value: values, + label: typeof labels === "string" ? labels : values, + }, + ]; + + const schema = { + ...props.schema, + enum: [values], + enumNames: typeof labels === "string" ? labels : undefined, + }; + return ( - - {props.schema.title} -
      - Dropdown options defined by a variable are not displayed in preview. -
      -
      + ); } - return ; + return ; }; export default SelectWidgetPreview; From de665d227a6d9f370dea2213fea4d2d0f76182cf Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 14 Mar 2022 17:55:21 +0100 Subject: [PATCH 04/13] 2870 Dropdown with labels --- .../formBuilder/edit/FieldEditor.tsx | 33 ++++++++++----- .../formBuilder/formBuilderHelpers.ts | 41 +++++++++++++++---- 2 files changed, 56 insertions(+), 18 deletions(-) diff --git a/src/components/formBuilder/edit/FieldEditor.tsx b/src/components/formBuilder/edit/FieldEditor.tsx index 5d4cf8455d..034dfebca5 100644 --- a/src/components/formBuilder/edit/FieldEditor.tsx +++ b/src/components/formBuilder/edit/FieldEditor.tsx @@ -33,6 +33,7 @@ import { replaceStringInArray, stringifyUiType, UiType, + UiTypeExtra, validateNextPropertyName, } from "@/components/formBuilder/formBuilderHelpers"; import { Schema, SchemaPropertyType } from "@/core"; @@ -134,11 +135,16 @@ const FieldEditor: React.FC<{ // eslint-disable-next-line security/detect-object-injection const uiWidget = uiSchema?.[propertyName]?.[UI_WIDGET]; const propertyFormat = propertySchema.format; + const extra: UiTypeExtra = + uiWidget === "select" && Array.isArray(propertySchema.oneOf) + ? "selectWithLabels" + : undefined; const uiType = stringifyUiType({ propertyType, uiWidget, propertyFormat, + extra, }); const selected = fieldTypes.find((option) => option.value === uiType); @@ -200,6 +206,7 @@ const FieldEditor: React.FC<{ propertyType: "null", uiWidget: undefined, propertyFormat: undefined, + extra: undefined, } : parseUiType(selectedUiTypeOption.value); @@ -264,18 +271,24 @@ const FieldEditor: React.FC<{ }, }} /> + + )} - - + }, + }} + /> )} { - const [propertyType, uiWidget, propertyFormat] = value.split(":"); + const [propertyType, uiWidget, propertyFormat, extra] = value.split(":"); return { propertyType: propertyType as SchemaPropertyType, uiWidget: uiWidget === "" ? undefined : uiWidget, propertyFormat: propertyFormat === "" ? undefined : propertyFormat, + extra: extra === "" ? undefined : (extra as UiTypeExtra), }; }; @@ -56,11 +61,9 @@ export const stringifyUiType = ({ propertyType, uiWidget, propertyFormat, -}: { - propertyType: SchemaPropertyType; - uiWidget?: string; - propertyFormat?: string; -}) => `${propertyType}:${uiWidget ?? ""}:${propertyFormat ?? ""}`; + extra, +}: Partial) => + `${propertyType}:${uiWidget ?? ""}:${propertyFormat ?? ""}:${extra ?? ""}`; export const FIELD_TYPE_OPTIONS: SelectStringOption[] = [ { @@ -105,6 +108,14 @@ export const FIELD_TYPE_OPTIONS: SelectStringOption[] = [ label: "Dropdown", value: stringifyUiType({ propertyType: "string", uiWidget: "select" }), }, + { + label: "Dropdown with labels", + value: stringifyUiType({ + propertyType: "string", + uiWidget: "select", + extra: "selectWithLabels", + }), + }, { label: "Checkbox", value: stringifyUiType({ propertyType: "boolean" }), @@ -251,7 +262,8 @@ export const produceSchemaOnUiTypeChange = ( propertyName: string, nextUiType: string ) => { - const { propertyType, uiWidget, propertyFormat } = parseUiType(nextUiType); + const { propertyType, uiWidget, propertyFormat, extra } = + parseUiType(nextUiType); return produce(rjsfSchema, (draft) => { // Relying on Immer to protect against object injections @@ -284,9 +296,22 @@ export const produceSchemaOnUiTypeChange = ( } if (uiWidget === "select") { - draftPropertySchema.enum = []; + if (extra === "selectWithLabels") { + // If switching from Dropdown, convert the enum to options with labels + draftPropertySchema.oneOf = (draftPropertySchema.enum ?? []).map( + (item) => ({ const: item, title: item }) + ); + delete draftPropertySchema.enum; + } else { + // If switching from Dropdown with labels, convert the values to enum + draftPropertySchema.enum = (draftPropertySchema.oneOf ?? []).map( + (item) => item.const + ); + delete draftPropertySchema.oneOf; + } } else { delete draftPropertySchema.enum; + delete draftPropertySchema.oneOf; } /* eslint-enable @typescript-eslint/no-dynamic-delete, security/detect-object-injection */ }); From 5be51ede86ee554215a61c43e9a64f4a9e95821e Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 14 Mar 2022 19:56:42 +0100 Subject: [PATCH 05/13] 2870 tests --- .../__snapshots__/FormEditor.test.tsx.snap | 2 +- .../formBuilder/formBuilderHelpers.test.ts | 105 +++++++++++++++++- 2 files changed, 104 insertions(+), 3 deletions(-) diff --git a/src/components/formBuilder/edit/__snapshots__/FormEditor.test.tsx.snap b/src/components/formBuilder/edit/__snapshots__/FormEditor.test.tsx.snap index 9e5384b3e5..d40ed02482 100644 --- a/src/components/formBuilder/edit/__snapshots__/FormEditor.test.tsx.snap +++ b/src/components/formBuilder/edit/__snapshots__/FormEditor.test.tsx.snap @@ -583,7 +583,7 @@ exports[`FormEditor renders simple schema 1`] = ` diff --git a/src/components/formBuilder/formBuilderHelpers.test.ts b/src/components/formBuilder/formBuilderHelpers.test.ts index 1a620c975c..cc5a9a6550 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[]; @@ -183,7 +185,7 @@ describe("validateNextPropertyName", () => { }); }); -describe("ormalizeUiOrder", () => { +describe("normalizeUiOrder", () => { test("init uiOrder", () => { const actual = normalizeUiOrder(["propA", "propB"], []); expect(actual).toEqual(["propA", "propB", "*"]); @@ -215,3 +217,102 @@ describe("ormalizeUiOrder", () => { 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.enum).toBeUndefined(); + expect(nextSchema.schema.properties.field1.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.oneOf).toBeUndefined(); + expect(nextSchema.schema.properties.field1.enum).toEqual([ + "foo", + "bar", + "baz", + ]); + }); +}); From 16deb546e93679045adbf516bef841bc82f06bc1 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 14 Mar 2022 20:01:36 +0100 Subject: [PATCH 06/13] 2870 fixing types --- src/components/formBuilder/formBuilderHelpers.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/formBuilder/formBuilderHelpers.ts b/src/components/formBuilder/formBuilderHelpers.ts index 1eee6be578..6a110b44a2 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"; @@ -299,13 +300,13 @@ export const produceSchemaOnUiTypeChange = ( if (extra === "selectWithLabels") { // If switching from Dropdown, convert the enum to options with labels draftPropertySchema.oneOf = (draftPropertySchema.enum ?? []).map( - (item) => ({ const: item, title: item }) + (item) => ({ const: item, title: item } as SchemaDefinition) ); delete draftPropertySchema.enum; } else { // If switching from Dropdown with labels, convert the values to enum draftPropertySchema.enum = (draftPropertySchema.oneOf ?? []).map( - (item) => item.const + (item: Schema) => item.const ); delete draftPropertySchema.oneOf; } From d82da8ccf07720657c58ba18f4af12916961bf51 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 14 Mar 2022 20:06:01 +0100 Subject: [PATCH 07/13] 2870 fixing types --- .../formBuilder/formBuilderHelpers.test.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/components/formBuilder/formBuilderHelpers.test.ts b/src/components/formBuilder/formBuilderHelpers.test.ts index cc5a9a6550..3d55f0a75b 100644 --- a/src/components/formBuilder/formBuilderHelpers.test.ts +++ b/src/components/formBuilder/formBuilderHelpers.test.ts @@ -249,8 +249,10 @@ describe("produceSchemaOnUiTypeChange", () => { }) ); - expect(nextSchema.schema.properties.field1.enum).toBeUndefined(); - expect(nextSchema.schema.properties.field1.oneOf).toEqual([ + expect( + (nextSchema.schema.properties.field1 as Schema).enum + ).toBeUndefined(); + expect((nextSchema.schema.properties.field1 as Schema).oneOf).toEqual([ { const: "foo", title: "foo", @@ -308,8 +310,10 @@ describe("produceSchemaOnUiTypeChange", () => { }) ); - expect(nextSchema.schema.properties.field1.oneOf).toBeUndefined(); - expect(nextSchema.schema.properties.field1.enum).toEqual([ + expect( + (nextSchema.schema.properties.field1 as Schema).oneOf + ).toBeUndefined(); + expect((nextSchema.schema.properties.field1 as Schema).enum).toEqual([ "foo", "bar", "baz", From d335cb81e29062ef2f6009f84e988dd0fc241d87 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 14 Mar 2022 22:01:44 +0100 Subject: [PATCH 08/13] 2870 Fixing default and no option errors --- .../formBuilder/edit/FieldEditor.tsx | 5 +++ .../formBuilder/formBuilderHelpers.ts | 1 + .../preview/SelectWidgetPreview.tsx | 36 ++++++++++++------- 3 files changed, 30 insertions(+), 12 deletions(-) diff --git a/src/components/formBuilder/edit/FieldEditor.tsx b/src/components/formBuilder/edit/FieldEditor.tsx index 034dfebca5..b7163f7484 100644 --- a/src/components/formBuilder/edit/FieldEditor.tsx +++ b/src/components/formBuilder/edit/FieldEditor.tsx @@ -219,6 +219,8 @@ const FieldEditor: React.FC<{ type: uiType.propertyType, }, label: "Default value", + // RJSF Form throws when Dropdown with labels selected, no options set and default is empty + isRequired: uiType.extra === "selectWithLabels", }; return ( @@ -270,6 +272,7 @@ const FieldEditor: React.FC<{ type: "string", }, }} + isRequired /> )} @@ -286,8 +289,10 @@ const FieldEditor: React.FC<{ const: { type: "string" }, title: { type: "string" }, }, + required: ["const"], }, }} + isRequired /> )} diff --git a/src/components/formBuilder/formBuilderHelpers.ts b/src/components/formBuilder/formBuilderHelpers.ts index 6a110b44a2..0a57eefebc 100644 --- a/src/components/formBuilder/formBuilderHelpers.ts +++ b/src/components/formBuilder/formBuilderHelpers.ts @@ -302,6 +302,7 @@ export const produceSchemaOnUiTypeChange = ( draftPropertySchema.oneOf = (draftPropertySchema.enum ?? []).map( (item) => ({ const: item, title: item } as SchemaDefinition) ); + draftPropertySchema.default = ""; delete draftPropertySchema.enum; } else { // If switching from Dropdown with labels, convert the values to enum diff --git a/src/components/formBuilder/preview/SelectWidgetPreview.tsx b/src/components/formBuilder/preview/SelectWidgetPreview.tsx index 8a44341aa2..a96d58e8aa 100644 --- a/src/components/formBuilder/preview/SelectWidgetPreview.tsx +++ b/src/components/formBuilder/preview/SelectWidgetPreview.tsx @@ -18,39 +18,51 @@ import { WidgetProps } from "@rjsf/core"; import { Theme as RjsfTheme } from "@rjsf/bootstrap-4"; import React from "react"; +import { Schema } from "@/core"; const RjsfSelectWidget = RjsfTheme.widgets.SelectWidget; const SelectWidgetPreview: React.VFC = (props) => { // If Select Options is a variable, then `props.schema.enum` holds the name of the variable (i.e. string). - const { enum: values } = props.schema; - if (typeof values === "string") { - // @ts-expect-error -- enumNames is a valid property of the RJSF schema. - const { enumNames: labels } = props.schema; - const enumOptions = [ + const { enum: enumValues, oneOf } = props.schema; + if (typeof enumValues === "string" || typeof oneOf === "string") { + // @ts-expect-error enumValues || oneOf is always a string here + const varValue: string = enumValues || oneOf; + const options = [ { - value: values, - label: typeof labels === "string" ? labels : values, + value: varValue, + label: varValue, }, ]; - const schema = { + const schema: Schema = { ...props.schema, - enum: [values], - enumNames: typeof labels === "string" ? labels : undefined, + enum: typeof enumValues === "string" ? [varValue] : undefined, + oneOf: + typeof oneOf === "string" + ? [ + { + const: varValue, + }, + ] + : undefined, }; return ( ); } + if (!Array.isArray(props.options.enumOptions)) { + return
      Please fill the values for each option.
      ; + } + return ; }; From 5761b04959bc843caafe42ab2b65c59e8188a1ec Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 15 Mar 2022 17:52:46 +0100 Subject: [PATCH 09/13] 2870 Dropdown text option tests --- .../formBuilder/FormBuilder.test.tsx | 101 ++++++++++++++++++ src/components/formBuilder/FormBuilder.tsx | 5 +- src/tests/formHelpers.tsx | 2 +- 3 files changed, 105 insertions(+), 3 deletions(-) create mode 100644 src/components/formBuilder/FormBuilder.test.tsx diff --git a/src/components/formBuilder/FormBuilder.test.tsx b/src/components/formBuilder/FormBuilder.test.tsx new file mode 100644 index 0000000000..7d36ceb131 --- /dev/null +++ b/src/components/formBuilder/FormBuilder.test.tsx @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2022 PixieBrix, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import { IBlock } from "@/core"; +import { getExampleBlockConfig } from "@/pageEditor/tabs/editTab/exampleBlockConfigs"; +import { createFormikTemplate, fireTextInput } from "@/tests/formHelpers"; +import { waitForEffect } from "@/tests/testHelpers"; +import { validateRegistryId } from "@/types/helpers"; +import { render, screen } from "@testing-library/react"; +import React from "react"; +import { act } from "react-dom/test-utils"; +import selectEvent from "react-select-event"; +import registerDefaultWidgets from "../fields/schemaFields/widgets/registerDefaultWidgets"; +import Form from "../form/Form"; +import FormBuilder from "./FormBuilder"; +import { MINIMAL_SCHEMA, MINIMAL_UI_SCHEMA } from "./formBuilderHelpers"; +import { RJSFSchema } from "./formBuilderTypes"; + +let exampleFormSchema: RJSFSchema; +let defaultFieldName: string; + +beforeAll(() => { + registerDefaultWidgets(); + const { schema, uiSchema } = getExampleBlockConfig({ + id: validateRegistryId("@pixiebrix/form"), + } as IBlock); + exampleFormSchema = { + schema, + uiSchema, + }; + defaultFieldName = "notes"; +}); + +function renderFormBuilder( + formBuilderSchema: RJSFSchema = exampleFormSchema, + activeField = defaultFieldName +) { + const initialValues = { + form: formBuilderSchema, + }; + const FormikTemplate = createFormikTemplate(initialValues); + return render( + + + + ); +} + +describe("Dropdown field", () => { + async function selectUiType(uiType: string) { + await act(async () => + selectEvent.select(screen.getByLabelText("Input Type"), uiType) + ); + } + + test("doesn't fail when field type changed to Dropdown", async () => { + const rendered = renderFormBuilder(); + + // Switch to Dropdown widget + await selectUiType("Dropdown"); + + // Expect the dropdown rendered in the preview + expect( + rendered.container.querySelector(`select#root_${defaultFieldName}`) + ).not.toBeNull(); + }); + + test("can add an option to Dropdown", async () => { + const rendered = renderFormBuilder(); + + // Switch to Dropdown widget + await selectUiType("Dropdown"); + + // 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(); + + // Expect the dropdown option rendered in the preview + expect( + screen.queryByRole("option", { name: "Test option" }) + ).not.toBeNull(); + }); +}); diff --git a/src/components/formBuilder/FormBuilder.tsx b/src/components/formBuilder/FormBuilder.tsx index 3634161e93..d6e548f14d 100644 --- a/src/components/formBuilder/FormBuilder.tsx +++ b/src/components/formBuilder/FormBuilder.tsx @@ -25,8 +25,9 @@ import { RJSFSchema } from "@/components/formBuilder/formBuilderTypes"; const FormBuilder: React.FC<{ name: string; -}> = ({ name }) => { - const [activeField, setActiveField] = useState(); + initialActiveField?: string; +}> = ({ name, initialActiveField }) => { + const [activeField, setActiveField] = useState(initialActiveField); const [{ value: rjsfSchema }] = useField(name); return ( diff --git a/src/tests/formHelpers.tsx b/src/tests/formHelpers.tsx index 1691dfa6a1..71a0bf487a 100644 --- a/src/tests/formHelpers.tsx +++ b/src/tests/formHelpers.tsx @@ -46,7 +46,7 @@ export const fireFormSubmit = async () => { await waitForEffect(); }; -export const fireTextInput = (input: HTMLElement, text: string) => { +export const fireTextInput = (input: Element, text: string) => { fireEvent.focus(input); fireEvent.change(input, { target: { value: text } }); fireEvent.blur(input); From 573b231fde09bd983075b7e202126e808676d7fb Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 15 Mar 2022 20:19:52 +0100 Subject: [PATCH 10/13] 2870 Enable var in Dropdown with labels --- .../formBuilder/FormBuilder.test.tsx | 104 +++++++++++++++--- .../formBuilder/edit/FieldEditor.tsx | 2 +- .../formBuilder/edit/FormEditor.test.tsx | 19 +--- .../formBuilder/preview/FormPreview.tsx | 2 + .../preview/FormPreviewSchemaField.tsx | 53 +++++++++ .../preview/SelectWidgetPreview.tsx | 7 +- src/tests/formHelpers.tsx | 16 +++ 7 files changed, 167 insertions(+), 36 deletions(-) create mode 100644 src/components/formBuilder/preview/FormPreviewSchemaField.tsx diff --git a/src/components/formBuilder/FormBuilder.test.tsx b/src/components/formBuilder/FormBuilder.test.tsx index 7d36ceb131..952f062a30 100644 --- a/src/components/formBuilder/FormBuilder.test.tsx +++ b/src/components/formBuilder/FormBuilder.test.tsx @@ -17,17 +17,19 @@ import { IBlock } from "@/core"; import { getExampleBlockConfig } from "@/pageEditor/tabs/editTab/exampleBlockConfigs"; -import { createFormikTemplate, fireTextInput } from "@/tests/formHelpers"; +import { + createFormikTemplate, + fireTextInput, + selectSchemaFieldType, +} from "@/tests/formHelpers"; import { waitForEffect } from "@/tests/testHelpers"; import { validateRegistryId } from "@/types/helpers"; -import { render, screen } from "@testing-library/react"; +import { render, RenderResult, screen } from "@testing-library/react"; import React from "react"; import { act } from "react-dom/test-utils"; import selectEvent from "react-select-event"; import registerDefaultWidgets from "../fields/schemaFields/widgets/registerDefaultWidgets"; -import Form from "../form/Form"; import FormBuilder from "./FormBuilder"; -import { MINIMAL_SCHEMA, MINIMAL_UI_SCHEMA } from "./formBuilderHelpers"; import { RJSFSchema } from "./formBuilderTypes"; let exampleFormSchema: RJSFSchema; @@ -60,31 +62,29 @@ function renderFormBuilder( ); } -describe("Dropdown field", () => { - async function selectUiType(uiType: string) { - await act(async () => - selectEvent.select(screen.getByLabelText("Input Type"), uiType) - ); - } +async function selectUiType(uiType: string) { + await act(async () => + selectEvent.select(screen.getByLabelText("Input Type"), uiType) + ); +} - test("doesn't fail when field type changed to Dropdown", async () => { - const rendered = renderFormBuilder(); +describe("Dropdown field", () => { + let rendered: RenderResult; + beforeEach(async () => { + rendered = renderFormBuilder(); // Switch to Dropdown widget await selectUiType("Dropdown"); + }); + test("doesn't fail when field type changed to Dropdown", async () => { // Expect the dropdown rendered in the preview expect( rendered.container.querySelector(`select#root_${defaultFieldName}`) ).not.toBeNull(); }); - test("can add an option to Dropdown", async () => { - const rendered = renderFormBuilder(); - - // Switch to Dropdown widget - await selectUiType("Dropdown"); - + test("can add an option", async () => { // Add a text option screen.getByText("Add Item").click(); const firstOption = rendered.container.querySelector( @@ -98,4 +98,72 @@ describe("Dropdown field", () => { screen.queryByRole("option", { name: "Test option" }) ).not.toBeNull(); }); + + 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(); + + // 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(); + + // 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 () => { + // Add a text option + screen.getByText("Add Item").click(); + + // Set option value + const firstOptionValueInput = rendered.container.querySelector( + `[name="form.schema.properties.${defaultFieldName}.oneOf.0.const"]` + ); + fireTextInput(firstOptionValueInput, "1"); + await waitForEffect(); + + // Set option label + const firstOptionLabelInput = rendered.container.querySelector( + `[name="form.schema.properties.${defaultFieldName}.oneOf.0.title"]` + ); + 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(); + expect(optionElement).toHaveValue("1"); + }); + + 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(); + + // Expect the dropdown option rendered in the preview + expect(screen.queryByRole("option", { name: "@data" })).not.toBeNull(); + }); }); diff --git a/src/components/formBuilder/edit/FieldEditor.tsx b/src/components/formBuilder/edit/FieldEditor.tsx index b7163f7484..e8eadf0cc8 100644 --- a/src/components/formBuilder/edit/FieldEditor.tsx +++ b/src/components/formBuilder/edit/FieldEditor.tsx @@ -136,7 +136,7 @@ const FieldEditor: React.FC<{ const uiWidget = uiSchema?.[propertyName]?.[UI_WIDGET]; const propertyFormat = propertySchema.format; const extra: UiTypeExtra = - uiWidget === "select" && Array.isArray(propertySchema.oneOf) + uiWidget === "select" && typeof propertySchema.oneOf !== "undefined" ? "selectWithLabels" : undefined; diff --git a/src/components/formBuilder/edit/FormEditor.test.tsx b/src/components/formBuilder/edit/FormEditor.test.tsx index c7486c5a77..306e6d92cb 100644 --- a/src/components/formBuilder/edit/FormEditor.test.tsx +++ b/src/components/formBuilder/edit/FormEditor.test.tsx @@ -25,6 +25,7 @@ import { createFormikTemplate, fireTextInput, fireFormSubmit, + selectSchemaFieldType, } from "@/tests/formHelpers"; import { RJSFSchema } from "@/components/formBuilder/formBuilderTypes"; import FormEditor, { FormEditorProps } from "./FormEditor"; @@ -347,20 +348,10 @@ describe("FormEditor", () => { ); - await waitForEffect(); - - const fieldToggleButton = screen - .getByTestId( - `toggle-${RJSF_SCHEMA_PROPERTY_NAME}.schema.properties.${fieldName}.default` - ) - .querySelector("button"); - expect(fieldToggleButton).not.toBeNull(); - userEvent.click(fieldToggleButton); - const textOption = screen.getByTestId("string"); - expect(textOption).not.toBeNull(); - await waitFor(() => { - userEvent.click(textOption); - }); + await selectSchemaFieldType( + `${RJSF_SCHEMA_PROPERTY_NAME}.schema.properties.${fieldName}.default`, + "string" + ); const defaultValue = "Initial default value"; const defaultValueInput = screen.getByLabelText("Default value"); diff --git a/src/components/formBuilder/preview/FormPreview.tsx b/src/components/formBuilder/preview/FormPreview.tsx index b55f1648e3..1d6d2d1c30 100644 --- a/src/components/formBuilder/preview/FormPreview.tsx +++ b/src/components/formBuilder/preview/FormPreview.tsx @@ -33,6 +33,7 @@ import ImageCropWidgetPreview from "@/components/formBuilder/preview/ImageCropWi import DescriptionField from "@/components/formBuilder/DescriptionField"; import FieldTemplate from "@/components/formBuilder/FieldTemplate"; import SelectWidgetPreview from "./SelectWidgetPreview"; +import FormPreviewSchemaField from "./FormPreviewSchemaField"; export type FormPreviewProps = { rjsfSchema: RJSFSchema; @@ -96,6 +97,7 @@ const FormPreview: React.FC = ({ } const fields = { + SchemaField: FormPreviewSchemaField, StringField, BooleanField, DescriptionField, diff --git a/src/components/formBuilder/preview/FormPreviewSchemaField.tsx b/src/components/formBuilder/preview/FormPreviewSchemaField.tsx new file mode 100644 index 0000000000..133c61f36e --- /dev/null +++ b/src/components/formBuilder/preview/FormPreviewSchemaField.tsx @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2022 PixieBrix, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +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; + +const FormPreviewSchemaField: React.FC = (props) => { + let fieldProps: FormPreviewFieldProps; + + // 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; + }); + } 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; + }); + } else { + fieldProps = props; + } + + return ; +}; + +export default FormPreviewSchemaField; diff --git a/src/components/formBuilder/preview/SelectWidgetPreview.tsx b/src/components/formBuilder/preview/SelectWidgetPreview.tsx index a96d58e8aa..24f198ffe1 100644 --- a/src/components/formBuilder/preview/SelectWidgetPreview.tsx +++ b/src/components/formBuilder/preview/SelectWidgetPreview.tsx @@ -27,8 +27,9 @@ const SelectWidgetPreview: React.VFC = (props) => { const { enum: enumValues, oneOf } = props.schema; if (typeof enumValues === "string" || typeof oneOf === "string") { // @ts-expect-error enumValues || oneOf is always a string here - const varValue: string = enumValues || oneOf; - const options = [ + const varValue: string = + typeof enumValues === "string" ? enumValues : oneOf; + const enumOptions = [ { value: varValue, label: varValue, @@ -52,7 +53,7 @@ const SelectWidgetPreview: React.VFC = (props) => { diff --git a/src/tests/formHelpers.tsx b/src/tests/formHelpers.tsx index 71a0bf487a..3a676d36a1 100644 --- a/src/tests/formHelpers.tsx +++ b/src/tests/formHelpers.tsx @@ -19,6 +19,7 @@ import { Form, Formik, FormikValues } from "formik"; import React, { PropsWithChildren } from "react"; import { fireEvent, screen } from "@testing-library/react"; import { waitForEffect } from "@/tests/testHelpers"; +import userEvent from "@testing-library/user-event"; export const RJSF_SCHEMA_PROPERTY_NAME = "rjsfSchema"; @@ -51,3 +52,18 @@ export const fireTextInput = (input: Element, text: string) => { fireEvent.change(input, { target: { value: text } }); fireEvent.blur(input); }; + +export const selectSchemaFieldType = async ( + fieldName: string, + typeToSelect: string +) => { + const fieldToggleButton = screen + .getByTestId(`toggle-${fieldName}`) + .querySelector("button"); + userEvent.click(fieldToggleButton); + await waitForEffect(); + + const textOption = screen.getByTestId(typeToSelect); + userEvent.click(textOption); + await waitForEffect(); +}; From d9ba8b335895436d87974f26f5dfa4e71e81075d Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 15 Mar 2022 20:47:47 +0100 Subject: [PATCH 11/13] 2870 removing options mapping --- .../formBuilder/FormBuilder.test.tsx | 2 - .../formBuilder/formBuilderHelpers.test.ts | 103 ------------------ .../formBuilder/formBuilderHelpers.ts | 11 +- 3 files changed, 2 insertions(+), 114 deletions(-) diff --git a/src/components/formBuilder/FormBuilder.test.tsx b/src/components/formBuilder/FormBuilder.test.tsx index 952f062a30..4ddc3be3fe 100644 --- a/src/components/formBuilder/FormBuilder.test.tsx +++ b/src/components/formBuilder/FormBuilder.test.tsx @@ -146,8 +146,6 @@ 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 3d55f0a75b..b16d3e14a5 100644 --- a/src/components/formBuilder/formBuilderHelpers.test.ts +++ b/src/components/formBuilder/formBuilderHelpers.test.ts @@ -217,106 +217,3 @@ 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 0a57eefebc..6d1c07e532 100644 --- a/src/components/formBuilder/formBuilderHelpers.ts +++ b/src/components/formBuilder/formBuilderHelpers.ts @@ -19,7 +19,6 @@ import { KEYS_OF_UI_SCHEMA, SafeString, Schema, - SchemaDefinition, SchemaPropertyType, UiSchema, } from "@/core"; @@ -298,17 +297,11 @@ 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 = []; 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 = []; delete draftPropertySchema.oneOf; } } else { From f145e93d44229de986f12f8eb94c4cfdc0fb1ff6 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 15 Mar 2022 21:11:49 +0100 Subject: [PATCH 12/13] 2870 Fixing merge conflicts --- src/components/formBuilder/edit/FormEditor.test.tsx | 3 +-- src/components/formBuilder/formBuilderHelpers.test.ts | 4 +--- src/pageEditor/tabs/RecipeOptions.tsx | 4 ++-- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/components/formBuilder/edit/FormEditor.test.tsx b/src/components/formBuilder/edit/FormEditor.test.tsx index 306e6d92cb..dadaa0d69c 100644 --- a/src/components/formBuilder/edit/FormEditor.test.tsx +++ b/src/components/formBuilder/edit/FormEditor.test.tsx @@ -18,7 +18,7 @@ import { Schema, UiSchema } from "@/core"; import { waitForEffect } from "@/tests/testHelpers"; import testItRenders, { ItRendersOptions } from "@/tests/testItRenders"; -import { fireEvent, render, screen, waitFor } from "@testing-library/react"; +import { fireEvent, render, screen } from "@testing-library/react"; import React from "react"; import { Except } from "type-fest"; import { @@ -35,7 +35,6 @@ import { initRenamingCases, } from "@/components/formBuilder/formEditor.testCases"; import selectEvent from "react-select-event"; -import userEvent from "@testing-library/user-event"; import registerDefaultWidgets from "@/components/fields/schemaFields/widgets/registerDefaultWidgets"; const RJSF_SCHEMA_PROPERTY_NAME = "rjsfSchema"; diff --git a/src/components/formBuilder/formBuilderHelpers.test.ts b/src/components/formBuilder/formBuilderHelpers.test.ts index b16d3e14a5..d8aa082ee6 100644 --- a/src/components/formBuilder/formBuilderHelpers.test.ts +++ b/src/components/formBuilder/formBuilderHelpers.test.ts @@ -22,15 +22,13 @@ 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, UI_WIDGET } from "./schemaFieldNames"; +import { UI_ORDER } from "./schemaFieldNames"; describe("replaceStringInArray", () => { let array: string[]; diff --git a/src/pageEditor/tabs/RecipeOptions.tsx b/src/pageEditor/tabs/RecipeOptions.tsx index 23788a5391..21fa360021 100644 --- a/src/pageEditor/tabs/RecipeOptions.tsx +++ b/src/pageEditor/tabs/RecipeOptions.tsx @@ -24,10 +24,10 @@ import Loader from "@/components/Loader"; import { isEmpty } from "lodash"; import styles from "./RecipeOptions.module.scss"; import ErrorBoundary from "@/components/ErrorBoundary"; -import FormEditor from "@/components/formBuilder/FormEditor"; +import FormEditor from "@/components/formBuilder/edit/FormEditor"; import dataPanelStyles from "@/pageEditor/tabs/dataPanelTabs.module.scss"; import cx from "classnames"; -import FormPreview from "@/components/formBuilder/FormPreview"; +import FormPreview from "@/components/formBuilder/preview/FormPreview"; import { RJSFSchema } from "@/components/formBuilder/formBuilderTypes"; import { FIELD_TYPE_OPTIONS } from "@/components/formBuilder/formBuilderHelpers"; import { useDispatch, useSelector } from "react-redux"; From da2c1419ee45ed72b9cdf5e2d985b0db9396abbf Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 15 Mar 2022 21:24:49 +0100 Subject: [PATCH 13/13] 2870 linting --- 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 4ddc3be3fe..a5841804f3 100644 --- a/src/components/formBuilder/FormBuilder.test.tsx +++ b/src/components/formBuilder/FormBuilder.test.tsx @@ -28,7 +28,7 @@ import { render, RenderResult, screen } from "@testing-library/react"; import React from "react"; import { act } from "react-dom/test-utils"; import selectEvent from "react-select-event"; -import registerDefaultWidgets from "../fields/schemaFields/widgets/registerDefaultWidgets"; +import registerDefaultWidgets from "@/components/fields/schemaFields/widgets/registerDefaultWidgets"; import FormBuilder from "./FormBuilder"; import { RJSFSchema } from "./formBuilderTypes";