From bd32969cd63e2e52daf89d6f58e89622638ac76c Mon Sep 17 00:00:00 2001 From: Drew Hoover Date: Sat, 16 Mar 2024 23:14:00 -0400 Subject: [PATCH 01/11] feat: add oneOf renderer --- package.json | 4 + pnpm-lock.yaml | 16 +- src/common/StorybookAntDJsonForm.tsx | 5 +- src/common/usePreviousValue.ts | 18 ++ src/controls/OneOfControl.tsx | 238 ++++++++++++++++++ src/renderers.ts | 7 + src/stories/controls/OneOfControl.stories.tsx | 139 ++++++++++ src/ui-schema.ts | 2 +- 8 files changed, 424 insertions(+), 5 deletions(-) create mode 100644 src/common/usePreviousValue.ts create mode 100644 src/controls/OneOfControl.tsx create mode 100644 src/stories/controls/OneOfControl.stories.tsx diff --git a/package.json b/package.json index 59b0a0e..23cb32a 100644 --- a/package.json +++ b/package.json @@ -95,5 +95,9 @@ "typescript": "^5.2.2", "vite": "^5.0.12", "vitest": "^1.2.2" + }, + "dependencies": { + "@types/lodash-es": "^4.17.12", + "lodash-es": "^4.17.21" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 98aecdd..fee7942 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,6 +4,14 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false +dependencies: + '@types/lodash-es': + specifier: ^4.17.12 + version: 4.17.12 + lodash-es: + specifier: ^4.17.21 + version: 4.17.21 + devDependencies: '@ant-design/icons': specifier: ^5.3.0 @@ -4302,6 +4310,12 @@ packages: resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} dev: true + /@types/lodash-es@4.17.12: + resolution: {integrity: sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==} + dependencies: + '@types/lodash': 4.14.202 + dev: false + /@types/lodash.isempty@4.4.9: resolution: {integrity: sha512-DPSFfnT2JmZiAWNWOU8IRZws/Ha6zyGF5m06TydfsY+0dVoQqby2J61Na2QU4YtwiZ+moC6cJS6zWYBJq4wBVw==} dependencies: @@ -4310,7 +4324,6 @@ packages: /@types/lodash@4.14.202: resolution: {integrity: sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ==} - dev: true /@types/mdx@2.0.10: resolution: {integrity: sha512-Rllzc5KHk0Al5/WANwgSPl1/CwjqCy+AZrGd78zuK+jO9aDM6ffblZ+zIjgPNAaEBmlO0RYDvLNh7wD0zKVgEg==} @@ -8033,7 +8046,6 @@ packages: /lodash-es@4.17.21: resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==} - dev: true /lodash.capitalize@4.2.1: resolution: {integrity: sha512-kZzYOKspf8XVX5AvmQF94gQW0lejFVgb80G85bU4ZWzoJ6C03PQg3coYAUpSTpQWelrZELd3XWgHzw4Ck5kaIw==} diff --git a/src/common/StorybookAntDJsonForm.tsx b/src/common/StorybookAntDJsonForm.tsx index c9e284d..e072d9a 100644 --- a/src/common/StorybookAntDJsonForm.tsx +++ b/src/common/StorybookAntDJsonForm.tsx @@ -6,10 +6,11 @@ import { import { UISchema } from "../ui-schema" import { AntDJsonForm } from "./AntDJsonForm" import { useState } from "react" +import { JSONSchema } from "json-schema-to-ts" type Props = { data?: Record - jsonSchema: JsonSchema7 + jsonSchema: JSONSchema rendererRegistryEntries: JsonFormsRendererRegistryEntry[] uiSchema?: UISchema uiSchemaRegistryEntries?: JsonFormsUISchemaRegistryEntry[] @@ -35,7 +36,7 @@ export function StorybookAntDJsonForm({ return ( updateData(newData)} uiSchemaRegistryEntries={uiSchemaRegistryEntries} diff --git a/src/common/usePreviousValue.ts b/src/common/usePreviousValue.ts new file mode 100644 index 0000000..3bb8bab --- /dev/null +++ b/src/common/usePreviousValue.ts @@ -0,0 +1,18 @@ +import { useEffect, useState } from "react" + +/** + * Hook that returns the what was passed in the previous render. + * On the first render, returns undefined. Eventually + * "catches up" to the current value. + */ +export function usePreviousValue(value: T): T | undefined { + const [prev, setPrev] = useState(undefined) + + useEffect(() => { + if (value !== prev) { + setPrev(value) + } + }, [value, prev]) + + return prev +} diff --git a/src/controls/OneOfControl.tsx b/src/controls/OneOfControl.tsx new file mode 100644 index 0000000..8939bde --- /dev/null +++ b/src/controls/OneOfControl.tsx @@ -0,0 +1,238 @@ +import { + CombinatorRendererProps, + CombinatorSubSchemaRenderInfo, + createCombinatorRenderInfos, + createDefaultValue, + JsonSchema, +} from "@jsonforms/core" +import { JsonFormsDispatch } from "@jsonforms/react" +import { + Radio, + RadioChangeEvent, + RadioGroupProps, + Select, + Space, + Switch, + Typography, +} from "antd" +import { merge, startCase } from "lodash-es" +import { useCallback, useEffect, useState } from "react" +import { + ControlElement, + LabelDescription, + OneOfControlOption, + OneOfControlOptions, +} from "../ui-schema" +import { usePreviousValue } from "../common/usePreviousValue" + +export function OneOfControl({ + handleChange, + schema, + path, + renderers, + cells, + rootSchema, + indexOfFittingSchema, + uischema, + uischemas, + config, +}: CombinatorRendererProps) { + const [selectedIndex, setSelectedIndex] = useState(indexOfFittingSchema || 0) + + const combinatorType = "oneOf" + const oneOfRenderInfos = createCombinatorRenderInfos( + schema.oneOf as JsonSchema[], + rootSchema, + combinatorType, + uischema, + path, + uischemas, + ) + const appliedUiSchemaOptions: OneOfControlOptions = merge( + {}, + config, + uischema.options, + ) as OneOfControlOptions + const oneOfOptionType = appliedUiSchemaOptions.optionType + const handleRadioChange = useCallback((e: RadioChangeEvent) => { + // not my fav way to do this but antd type for value is literally any + const combinatorIndex = e.target.value as number + setSelectedIndex(combinatorIndex) + }, []) + const handleSelectChange = useCallback((value: number) => { + const combinatorIndex = value + setSelectedIndex(combinatorIndex) + }, []) + + const handleSwitchChange = useCallback((value: boolean) => { + const combinatorIndex = value === false ? 0 : 1 + setSelectedIndex(combinatorIndex) + }, []) + + const prevSelectedIndex = usePreviousValue(selectedIndex) + useEffect(() => { + if ( + selectedIndex !== prevSelectedIndex && + prevSelectedIndex !== undefined + ) { + handleCombinatorTypeChange({ + handleChange, + combinatorIndex: selectedIndex, + renderInfos: oneOfRenderInfos, + path, + rootSchema + }) + } + }, [handleChange, oneOfRenderInfos, path, prevSelectedIndex, rootSchema, selectedIndex]) + + const computeLabel = (oneOfRenderInfo: CombinatorSubSchemaRenderInfo) => { + const label = oneOfRenderInfo.label.startsWith("oneOf") + ? oneOfRenderInfo.schema.title + : oneOfRenderInfo.label + return startCase(label) + } + const combinatorLabel = getCombinatorUiSchemaLabel(uischema as ControlElement) + + return ( + + {combinatorLabel && ( + {combinatorLabel} + )} + {shouldUseRadioGroupSwitcher(oneOfOptionType) && ( + ({ + // revert this when this is merged & released: https://github.com/eclipsesource/jsonforms/pull/2165#issuecomment-1640981617 + label: computeLabel(oneOfRenderInfo), + value: index, + }))} + onChange={handleRadioChange} + value={selectedIndex} + /> + )} + {oneOfOptionType === "dropdown" && ( + ({ + // revert this when this is merged & released: https://github.com/eclipsesource/jsonforms/pull/2165#issuecomment-1640981617 + label: computeLabel(renderInfo), + value: index, + }))} + onChange={handleSelectChange} + defaultValue={selectedIndex} + /> + ) + } + if (oneOfOptionType === "toggle") { + return ( + + + {appliedUiSchemaOptions.toggleLabel && ( + + {appliedUiSchemaOptions.toggleLabel} + + )} + + ) + } +} + +function shouldUseRadioGroupSwitcher( + optionType: OneOfControlOption | undefined, +) { + return ( + !optionType || + (["radio", "button"] satisfies OneOfControlOption[] as string[]).includes( + optionType, + ) + ) +} + +type RadioGroupSwitcherProps = { + radioProps: Partial + options: { label: string; value: number }[] + onChange: (e: RadioChangeEvent) => void + value: unknown +} + +function RadioGroupSwitcher({ + radioProps, + options, + onChange, + value, +}: RadioGroupSwitcherProps) { + return ( + + ) +} + +type HandleCombinatorTypeChangeArgs = { + handleChange: (path: string, data: unknown) => void + renderInfos: CombinatorSubSchemaRenderInfo[] + combinatorIndex: number + path: string + rootSchema: JsonSchema +} + +function handleCombinatorTypeChange({ + handleChange, + renderInfos, + combinatorIndex, + path, + rootSchema, +}: HandleCombinatorTypeChangeArgs) { + const newSchema = renderInfos[combinatorIndex]?.schema + let newData + if (newSchema?.type === "object") { + // const typeDiscriminator = newSchema.properties?.type?.default + + newData = { + ...createDefaultValue(newSchema, rootSchema), + // ...(typeDiscriminator ? { type: typeDiscriminator } : {}), + } as unknown + } else { + newData = createDefaultValue(newSchema, rootSchema) as unknown + } + + handleChange(path, newData) +} diff --git a/src/controls/OneOfControl.tsx b/src/controls/OneOfControl.tsx index 779799f..05868a0 100644 --- a/src/controls/OneOfControl.tsx +++ b/src/controls/OneOfControl.tsx @@ -1,32 +1,14 @@ import { CombinatorRendererProps, - CombinatorSubSchemaRenderInfo, createCombinatorRenderInfos, - createDefaultValue, JsonSchema, } from "@jsonforms/core" import { JsonFormsDispatch, withJsonFormsOneOfProps } from "@jsonforms/react" -import { - Radio, - RadioChangeEvent, - RadioGroupProps, - Select, - Space, - Switch, - Typography, -} from "antd" -import merge from "lodash.merge" -import startCase from "lodash.startcase" -import { useCallback, useEffect, useState } from "react" -import { - ControlUISchema, - LabelDescription, - OneOfControlOption, - OneOfControlOptions, - UISchema, -} from "../ui-schema" -import { usePreviousValue } from "../common/usePreviousValue" -import { assertNever } from "../common/assert-never" +import { Space } from "antd" +import { useState } from "react" +import { ControlUISchema } from "../ui-schema" +import { ControlLabelRenderer } from "../common/ControlLabelRenderer" +import { CombinatorSchemaSwitcher } from "./CombinatorSchemaSwitcher" export function OneOfControl({ handleChange, @@ -52,114 +34,29 @@ export function OneOfControl({ path, uischemas, ) - const appliedUiSchemaOptions: OneOfControlOptions = merge( - {}, - config as unknown, - uischema.options as Record, - ) as OneOfControlOptions - const oneOfOptionType = appliedUiSchemaOptions.optionType - const handleRadioChange = useCallback((e: RadioChangeEvent) => { - // not my fav way to do this but antd type for value is literally any - const combinatorIndex = e.target.value as number - setSelectedIndex(combinatorIndex) - }, []) - const handleSelectChange = useCallback((value: number) => { - const combinatorIndex = value - setSelectedIndex(combinatorIndex) - }, []) - - const handleSwitchChange = useCallback((value: boolean) => { - const combinatorIndex = value === false ? 0 : 1 - setSelectedIndex(combinatorIndex) - }, []) - - const prevSelectedIndex = usePreviousValue(selectedIndex) - const [dataForPreviousSchemas, setDataForPreviousSchemas] = useState< - Record - >({}) - useEffect(() => { - if ( - selectedIndex !== prevSelectedIndex && - prevSelectedIndex !== undefined - ) { - setDataForPreviousSchemas({ - ...dataForPreviousSchemas, - [prevSelectedIndex]: data as unknown, - }) - if (dataForPreviousSchemas[selectedIndex]) { - handleChange(path, dataForPreviousSchemas[selectedIndex]) - } else { - handleCombinatorTypeChange({ - handleChange, - combinatorIndex: selectedIndex, - renderInfos: oneOfRenderInfos, - path, - rootSchema, - }) - } - } - }, [ - data, - dataForPreviousSchemas, - handleChange, - oneOfRenderInfos, - path, - prevSelectedIndex, - rootSchema, - selectedIndex, - ]) - - const computeLabel = (oneOfRenderInfo: CombinatorSubSchemaRenderInfo) => { - const label = oneOfRenderInfo.label.startsWith("oneOf") - ? oneOfRenderInfo.schema.title - : oneOfRenderInfo.label - return startCase(label) - } return ( - - {shouldUseRadioGroupSwitcher(oneOfOptionType) && ( - ({ - // revert this when this is merged & released: https://github.com/eclipsesource/jsonforms/pull/2165#issuecomment-1640981617 - label: computeLabel(oneOfRenderInfo), - value: index, - }))} - onChange={handleRadioChange} - value={selectedIndex} + {uischema.type === "Control" && ( // I don't think it's possible for this to be false + // but until we improve the UISchema types a bit, it's hard to be sure + )} - {oneOfOptionType === "dropdown" && ( - ({ - // revert this when this is merged & released: https://github.com/eclipsesource/jsonforms/pull/2165#issuecomment-1640981617 - label: computeLabel(renderInfo), - value: index, - }))} - onChange={handleSelectChange} + options={options} + onChange={(combinatorIndex: number) => + setSelectedIndex(combinatorIndex) + } defaultValue={selectedIndex} /> ) @@ -156,7 +124,10 @@ export function CombinatorSchemaSwitcher({ return ( { + const combinatorIndex = value === false ? 0 : 1 + setSelectedIndex(combinatorIndex) + }} defaultChecked={indexOfFittingSchema === 1} /> {appliedUiSchemaOptions.toggleLabel && ( @@ -169,40 +140,6 @@ export function CombinatorSchemaSwitcher({ } } -function shouldUseRadioGroupSwitcher( - optionType: OneOfControlOption | undefined, -) { - return ( - !optionType || - (["radio", "button"] satisfies OneOfControlOption[] as string[]).includes( - optionType, - ) - ) -} - -type RadioGroupSwitcherProps = { - radioProps: Partial - options: { label: string; value: number }[] - onChange: (e: RadioChangeEvent) => void - value: unknown -} - -function RadioGroupSwitcher({ - radioProps, - options, - onChange, - value, -}: RadioGroupSwitcherProps) { - return ( - - ) -} - type HandleCombinatorTypeChangeArgs = { handleChange: (path: string, data: unknown) => void renderInfos: CombinatorSubSchemaRenderInfo[] diff --git a/src/controls/OneOfControl.test.tsx b/src/controls/combinators/OneOfControl.test.tsx similarity index 76% rename from src/controls/OneOfControl.test.tsx rename to src/controls/combinators/OneOfControl.test.tsx index 08bca17..14b93ea 100644 --- a/src/controls/OneOfControl.test.tsx +++ b/src/controls/combinators/OneOfControl.test.tsx @@ -1,9 +1,9 @@ import { JSONSchema } from "json-schema-to-ts" import { screen } from "@testing-library/react" import { test, expect, describe } from "vitest" -import { render } from "../common/test-render" +import { render } from "../../common/test-render" import userEvent from "@testing-library/user-event" -import { OneOfControlOptions } from "../ui-schema" +import { OneOfControlOptions } from "../../ui-schema" const schema = { type: "object", @@ -43,16 +43,19 @@ describe("OneOf control", () => { screen.getByLabelText("Address") }) test("OneOf Control with button UISchema allows switching between subschemas", async () => { - render({ schema, uischema: { - type: "VerticalLayout", - elements: [ - { - type: "Control", - scope: "#/properties/deliveryOption", - options: { optionType: "button" } satisfies OneOfControlOptions, - }, - ], - } }) + render({ + schema, + uischema: { + type: "VerticalLayout", + elements: [ + { + type: "Control", + scope: "#/properties/deliveryOption", + options: { optionType: "button" } satisfies OneOfControlOptions, + }, + ], + }, + }) await screen.findByText("Pickup") screen.getByLabelText("Location") @@ -60,16 +63,19 @@ describe("OneOf control", () => { screen.getByLabelText("Address") }) test("OneOf Control with dropdown UISchema allows switching between subschemas", async () => { - render({ schema, uischema: { - type: "VerticalLayout", - elements: [ - { - type: "Control", - scope: "#/properties/deliveryOption", - options: { optionType: "dropdown" } satisfies OneOfControlOptions, - }, - ], - } }) + render({ + schema, + uischema: { + type: "VerticalLayout", + elements: [ + { + type: "Control", + scope: "#/properties/deliveryOption", + options: { optionType: "dropdown" } satisfies OneOfControlOptions, + }, + ], + }, + }) await screen.findByText("Pickup") screen.getByLabelText("Location") diff --git a/src/controls/OneOfControl.tsx b/src/controls/combinators/OneOfControl.tsx similarity index 94% rename from src/controls/OneOfControl.tsx rename to src/controls/combinators/OneOfControl.tsx index 05868a0..437c390 100644 --- a/src/controls/OneOfControl.tsx +++ b/src/controls/combinators/OneOfControl.tsx @@ -6,8 +6,8 @@ import { import { JsonFormsDispatch, withJsonFormsOneOfProps } from "@jsonforms/react" import { Space } from "antd" import { useState } from "react" -import { ControlUISchema } from "../ui-schema" -import { ControlLabelRenderer } from "../common/ControlLabelRenderer" +import { ControlUISchema } from "../../ui-schema" +import { ControlLabelRenderer } from "../../common/ControlLabelRenderer" import { CombinatorSchemaSwitcher } from "./CombinatorSchemaSwitcher" export function OneOfControl({ diff --git a/src/controls/combinators/utils.ts b/src/controls/combinators/utils.ts new file mode 100644 index 0000000..61b1659 --- /dev/null +++ b/src/controls/combinators/utils.ts @@ -0,0 +1,12 @@ +import { OneOfControlOption } from "../../ui-schema" + +export function shouldUseRadioGroupSwitcher( + optionType: OneOfControlOption | undefined, +) { + return ( + !optionType || + (["radio", "button"] satisfies OneOfControlOption[] as string[]).includes( + optionType, + ) + ) +} diff --git a/src/renderer-registry-entries.ts b/src/renderer-registry-entries.ts index 5de4ebf..ddf413f 100644 --- a/src/renderer-registry-entries.ts +++ b/src/renderer-registry-entries.ts @@ -28,7 +28,7 @@ import { ObjectRenderer } from "./controls/ObjectControl" import { GroupLayoutRenderer } from "./layouts/GroupLayout" import { NumericRenderer } from "./controls/NumericControl" import { NumericSliderRenderer } from "./controls/NumericSliderControl" -import { OneOfRenderer } from "./controls/OneOfControl" +import { OneOfRenderer } from "./controls/combinators/OneOfControl" // Ordered from lowest rank to highest rank. Higher rank renderers will be preferred over lower rank renderers. export const rendererRegistryEntries: JsonFormsRendererRegistryEntry[] = [ diff --git a/src/ui-schema.ts b/src/ui-schema.ts index f329d1a..1893828 100644 --- a/src/ui-schema.ts +++ b/src/ui-schema.ts @@ -119,7 +119,11 @@ export type LabelDescription = { * Optional property that determines whether to show this label. */ show?: boolean -} & ({ type: "Title", titleProps: TitleProps } | { type: "Text", textProps: TextProps } | Record) +} & ( + | { type: "Title"; titleProps: TitleProps } + | { type: "Text"; textProps: TextProps } + | Record +) /** * A label element. */ @@ -182,9 +186,9 @@ type ControlOptions = */ export interface ControlUISchema extends UISchemaElement, - Scoped, - Labelable, - Internationalizable { + Scoped, + Labelable, + Internationalizable { type: "Control" } /** @@ -249,12 +253,12 @@ enum RuleEffect { type Condition = | Record // not documented in their type system AFAIK, but this is how you default a rule to "always true" | ( - | JFCondition - | LeafCondition - | SchemaBasedCondition - | OrCondition - | AndCondition - ) + | JFCondition + | LeafCondition + | SchemaBasedCondition + | OrCondition + | AndCondition + ) interface JFCondition { /** From c4e56e9063dddafa0ff446a44964594b67aa80a6 Mon Sep 17 00:00:00 2001 From: Drew Hoover Date: Wed, 20 Mar 2024 12:00:33 -0400 Subject: [PATCH 09/11] fix: simplify some code, remove toggle --- src/common/ControlLabel.test.tsx | 95 +++++++++++++++++++ ...trolLabelRenderer.tsx => ControlLabel.tsx} | 39 ++------ .../combinators/CombinatorSchemaSwitcher.tsx | 93 +++++++----------- src/controls/combinators/OneOfControl.tsx | 8 +- src/controls/combinators/utils.ts | 12 --- src/stories/controls/OneOfControl.stories.tsx | 31 +++++- src/ui-schema.ts | 23 ++--- 7 files changed, 182 insertions(+), 119 deletions(-) create mode 100644 src/common/ControlLabel.test.tsx rename src/common/{ControlLabelRenderer.tsx => ControlLabel.tsx} (54%) delete mode 100644 src/controls/combinators/utils.ts diff --git a/src/common/ControlLabel.test.tsx b/src/common/ControlLabel.test.tsx new file mode 100644 index 0000000..08d6eee --- /dev/null +++ b/src/common/ControlLabel.test.tsx @@ -0,0 +1,95 @@ +import { JSONSchema } from "json-schema-to-ts" +import { screen } from "@testing-library/react" +import { test, describe, expect } from "vitest" +import { render } from "../common/test-render" +import { UISchema } from "../ui-schema" + +const schema = { + type: "object", + properties: { + labelMe: { title: "Label Me", oneOf: [{ type: "string" }] }, + }, +} satisfies JSONSchema + +describe("ControlLabel", () => { + test("ControlLabel renders a title jsonschema property as the label", async () => { + render({ schema }) + await screen.findByText("Label Me") + }) + test("ControlLabel gives precedence to a string literal label property over the jsonschema title", async () => { + render({ + schema, + uischema: { + type: "VerticalLayout", + elements: [ + { + type: "Control", + scope: "#/properties/labelMe", + label: "No, Label ME", + }, + ], + } satisfies UISchema, + }) + await screen.findByText("No, Label ME") + }) + test("ControlLabel renders text from a label property that is a LabelDescription", async () => { + render({ + schema, + uischema: { + type: "VerticalLayout", + elements: [ + { + type: "Control", + scope: "#/properties/labelMe", + label: { text: "Srsly, label me instead" }, + }, + ], + } satisfies UISchema, + }) + await screen.findByText("Srsly, label me instead") + }) + test("ControlLabel renders an AntD Text component from a LabelDescription", async () => { + render({ + schema, + uischema: { + type: "VerticalLayout", + elements: [ + { + type: "Control", + scope: "#/properties/labelMe", + label: { + text: "Srsly, label me instead", + type: "Text", + textProps: { disabled: true }, + }, + }, + ], + } satisfies UISchema, + }) + await screen.findByText("Srsly, label me instead") + + expect(screen.getByText("Srsly, label me instead")).toHaveClass( + "ant-typography-disabled", + ) + }) + test("ControlLabel renders an AntD Title component from a LabelDescription", async () => { + render({ + schema, + uischema: { + type: "VerticalLayout", + elements: [ + { + type: "Control", + scope: "#/properties/labelMe", + label: { + text: "Srsly, label me instead", + type: "Title", + titleProps: { copyable: true }, + }, + }, + ], + } satisfies UISchema, + }) + await screen.findByLabelText("copy") + }) +}) diff --git a/src/common/ControlLabelRenderer.tsx b/src/common/ControlLabel.tsx similarity index 54% rename from src/common/ControlLabelRenderer.tsx rename to src/common/ControlLabel.tsx index fb6d002..baec64f 100644 --- a/src/common/ControlLabelRenderer.tsx +++ b/src/common/ControlLabel.tsx @@ -1,9 +1,10 @@ -import { JsonSchema } from "@jsonforms/core" -import { ControlUISchema, LabelDescription } from "../ui-schema" +import { JsonSchema, Helpers } from "@jsonforms/core" +import { ControlUISchema } from "../ui-schema" import { Typography } from "antd" import { assertNever } from "./assert-never" -export function ControlLabelRenderer({ +// This consumes a LabelDescription (+ other formats) in a Control UI Schema +export function ControlLabel({ uischema, schema, }: { @@ -11,17 +12,13 @@ export function ControlLabelRenderer({ schema: JsonSchema }) { const controlUISchema: ControlUISchema = uischema - const text = getUiSchemaLabel(controlUISchema) + const labelDescription = Helpers.createLabelDescriptionFrom(uischema, schema) + const text = labelDescription.show ? labelDescription.text : null - if (!text && !schema.title) { - return null - } - - if (!text && schema.title) { - return {schema.title} - } - - if (typeof controlUISchema.label === "object" && controlUISchema.label.type) { + if ( + typeof controlUISchema.label === "object" && + "type" in controlUISchema.label + ) { const labelType = controlUISchema.label.type switch (labelType) { case "Text": @@ -49,19 +46,3 @@ export function ControlLabelRenderer({ } return {text} } - -function getUiSchemaLabel(uischema: ControlUISchema): string | undefined { - const label = uischema.label - if (!label) { - return undefined - } - if ( - (label as LabelDescription).text && - (label as LabelDescription).show !== false - ) { - return (label as LabelDescription).text - } - if (typeof label === "string") { - return label - } -} diff --git a/src/controls/combinators/CombinatorSchemaSwitcher.tsx b/src/controls/combinators/CombinatorSchemaSwitcher.tsx index c583202..4912646 100644 --- a/src/controls/combinators/CombinatorSchemaSwitcher.tsx +++ b/src/controls/combinators/CombinatorSchemaSwitcher.tsx @@ -1,11 +1,4 @@ -import { - Radio, - RadioChangeEvent, - Select, - Space, - Switch, - Typography, -} from "antd" +import { Radio, RadioChangeEvent, Select } from "antd" import { OneOfControlOptions } from "../../ui-schema" import merge from "lodash.merge" import { useEffect, useState } from "react" @@ -16,7 +9,6 @@ import { createDefaultValue, } from "@jsonforms/core" import { usePreviousValue } from "../../common/usePreviousValue" -import { shouldUseRadioGroupSwitcher } from "./utils" type CombinatorSchemaSwitcherProps = { renderInfos: CombinatorSubSchemaRenderInfo[] @@ -24,18 +16,11 @@ type CombinatorSchemaSwitcherProps = { selectedIndex: number } & Pick< CombinatorRendererProps, - | "data" - | "handleChange" - | "path" - | "rootSchema" - | "uischema" - | "config" - | "indexOfFittingSchema" + "data" | "handleChange" | "path" | "rootSchema" | "uischema" | "config" > export function CombinatorSchemaSwitcher({ renderInfos, - indexOfFittingSchema, config, uischema, setSelectedIndex, @@ -93,50 +78,42 @@ export function CombinatorSchemaSwitcher({ value: index, })) - if (shouldUseRadioGroupSwitcher(oneOfOptionType)) { - return ( - { - const combinatorIndex = e.target.value as number - setSelectedIndex(combinatorIndex) - }} - value={selectedIndex} - /> - ) - } - if (oneOfOptionType === "dropdown") { - return ( - + setSelectedIndex(combinatorIndex) + } + defaultValue={selectedIndex} + /> + ) + case "radio": + default: + return ( + { + const combinatorIndex = e.target.value as number setSelectedIndex(combinatorIndex) }} - defaultChecked={indexOfFittingSchema === 1} + value={selectedIndex} /> - {appliedUiSchemaOptions.toggleLabel && ( - - {appliedUiSchemaOptions.toggleLabel} - - )} - - ) + ) } } diff --git a/src/controls/combinators/OneOfControl.tsx b/src/controls/combinators/OneOfControl.tsx index 437c390..d845422 100644 --- a/src/controls/combinators/OneOfControl.tsx +++ b/src/controls/combinators/OneOfControl.tsx @@ -7,7 +7,7 @@ import { JsonFormsDispatch, withJsonFormsOneOfProps } from "@jsonforms/react" import { Space } from "antd" import { useState } from "react" import { ControlUISchema } from "../../ui-schema" -import { ControlLabelRenderer } from "../../common/ControlLabelRenderer" +import { ControlLabel } from "../../common/ControlLabel" import { CombinatorSchemaSwitcher } from "./CombinatorSchemaSwitcher" export function OneOfControl({ @@ -39,14 +39,10 @@ export function OneOfControl({ {uischema.type === "Control" && ( // I don't think it's possible for this to be false // but until we improve the UISchema types a bit, it's hard to be sure - + )} -) +} /** * A label element. */ From fc2878a585998fb43c813c2c75d5c9f458510c9b Mon Sep 17 00:00:00 2001 From: Drew Hoover Date: Wed, 20 Mar 2024 14:29:29 -0400 Subject: [PATCH 10/11] fix: remove unused dependency --- package.json | 10 ++++------ pnpm-lock.yaml | 28 +++++++++------------------- 2 files changed, 13 insertions(+), 25 deletions(-) diff --git a/package.json b/package.json index 8924866..d1791eb 100644 --- a/package.json +++ b/package.json @@ -71,8 +71,8 @@ "@testing-library/user-event": "^14.5.2", "@types/lodash.isempty": "^4.4.9", "@types/lodash.merge": "^4.6.9", - "@types/lodash.startcase": "^4.4.9", "@types/lodash.range": "^3.2.9", + "@types/lodash.startcase": "^4.4.9", "@types/react": "^18.2.55", "@types/react-dom": "^18.2.19", "@typescript-eslint/eslint-plugin": "^6.14.0", @@ -97,11 +97,9 @@ "vitest": "^1.2.2" }, "dependencies": { - "@types/lodash-es": "^4.17.12", - "lodash-es": "^4.17.21", - "lodash.merge": "^4.6.2", - "lodash.startcase": "^4.4.0", "lodash.isempty": "^4.4.0", - "lodash.range": "^3.2.0" + "lodash.merge": "^4.6.2", + "lodash.range": "^3.2.0", + "lodash.startcase": "^4.4.0" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index eda3f30..54b82ca 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,12 +5,6 @@ settings: excludeLinksFromLockfile: false dependencies: - '@types/lodash-es': - specifier: ^4.17.12 - version: 4.17.12 - lodash-es: - specifier: ^4.17.21 - version: 4.17.21 lodash.isempty: specifier: ^4.4.0 version: 4.4.0 @@ -73,12 +67,12 @@ devDependencies: '@types/lodash.merge': specifier: ^4.6.9 version: 4.6.9 - '@types/lodash.startcase': - specifier: ^4.4.9 - version: 4.4.9 '@types/lodash.range': specifier: ^3.2.9 version: 3.2.9 + '@types/lodash.startcase': + specifier: ^4.4.9 + version: 4.4.9 '@types/react': specifier: ^18.2.55 version: 18.2.55 @@ -4328,12 +4322,6 @@ packages: resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} dev: true - /@types/lodash-es@4.17.12: - resolution: {integrity: sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==} - dependencies: - '@types/lodash': 4.14.202 - dev: false - /@types/lodash.isempty@4.4.9: resolution: {integrity: sha512-DPSFfnT2JmZiAWNWOU8IRZws/Ha6zyGF5m06TydfsY+0dVoQqby2J61Na2QU4YtwiZ+moC6cJS6zWYBJq4wBVw==} dependencies: @@ -4346,20 +4334,21 @@ packages: '@types/lodash': 4.14.202 dev: true - /@types/lodash.startcase@4.4.9: - resolution: {integrity: sha512-C0M4DlN1pnn2vEEhLHkTHxiRZ+3GlTegpoAEHHGXnuJkSOXyJMHGiSc+SLRzBlFZWHsBkixe6FqvEAEU04g14g==} + /@types/lodash.range@3.2.9: + resolution: {integrity: sha512-JNStPShiaR3ROKIAgtzChBhouPBCJxMI/Fj7Xa1cG51A2EMRyosvtL4fiGXOFiQddaxjZmNVYkZGsDRt8fFKPg==} dependencies: '@types/lodash': 4.14.202 dev: true - /@types/lodash.range@3.2.9: - resolution: {integrity: sha512-JNStPShiaR3ROKIAgtzChBhouPBCJxMI/Fj7Xa1cG51A2EMRyosvtL4fiGXOFiQddaxjZmNVYkZGsDRt8fFKPg==} + /@types/lodash.startcase@4.4.9: + resolution: {integrity: sha512-C0M4DlN1pnn2vEEhLHkTHxiRZ+3GlTegpoAEHHGXnuJkSOXyJMHGiSc+SLRzBlFZWHsBkixe6FqvEAEU04g14g==} dependencies: '@types/lodash': 4.14.202 dev: true /@types/lodash@4.14.202: resolution: {integrity: sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ==} + dev: true /@types/mdx@2.0.10: resolution: {integrity: sha512-Rllzc5KHk0Al5/WANwgSPl1/CwjqCy+AZrGd78zuK+jO9aDM6ffblZ+zIjgPNAaEBmlO0RYDvLNh7wD0zKVgEg==} @@ -8082,6 +8071,7 @@ packages: /lodash-es@4.17.21: resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==} + dev: true /lodash.capitalize@4.2.1: resolution: {integrity: sha512-kZzYOKspf8XVX5AvmQF94gQW0lejFVgb80G85bU4ZWzoJ6C03PQg3coYAUpSTpQWelrZELd3XWgHzw4Ck5kaIw==} From 2ca4503ff7e31784ff73d9e14362e61b125e99b4 Mon Sep 17 00:00:00 2001 From: Drew Hoover Date: Wed, 20 Mar 2024 14:31:48 -0400 Subject: [PATCH 11/11] fix: run format --- src/renderer-registry-entries.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/renderer-registry-entries.ts b/src/renderer-registry-entries.ts index 67aec2c..44afce3 100644 --- a/src/renderer-registry-entries.ts +++ b/src/renderer-registry-entries.ts @@ -83,7 +83,8 @@ export const rendererRegistryEntries: JsonFormsRendererRegistryEntry[] = [ { tester: rankWith(3, isOneOfControl), renderer: OneOfRenderer, - }, { + }, + { tester: rankWith( 3, or(isObjectArrayControl, isObjectArray, isObjectArrayWithNesting),