diff --git a/package.json b/package.json index c05a376..bcba8d6 100644 --- a/package.json +++ b/package.json @@ -30,11 +30,13 @@ "build-storybook": "storybook build" }, "peerDependencies": { - "react": "^17 || ^18", + "@ant-design/icons": "^5.3.0", "@jsonforms/core": "^3.2.1", "@jsonforms/react": "^3.2.1", - "@ant-design/icons": "^5.3.0", - "antd": "^5.14.0" + "@types/lodash.isempty": "^4.4.9", + "antd": "^5.14.0", + "lodash.isempty": "^4.4.0", + "react": "^17 || ^18" }, "devDependencies": { "@ant-design/icons": "^5.3.0", @@ -51,6 +53,7 @@ "@testing-library/jest-dom": "^6.4.2", "@testing-library/react": "^14.2.1", "@testing-library/user-event": "^14.5.2", + "@types/lodash.isempty": "^4.4.9", "@types/react": "^18.2.55", "@types/react-dom": "^18.2.19", "@typescript-eslint/eslint-plugin": "^6.14.0", @@ -64,6 +67,7 @@ "eslint-plugin-storybook": "^0.6.15", "jsdom": "^24.0.0", "json-schema-to-ts": "^3.0.0", + "lodash.isempty": "^4.4.0", "react": "^18.2.0", "react-dom": "^18.2.0", "storybook": "^7.6.8", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0264107..ab22a4a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -47,6 +47,9 @@ devDependencies: '@testing-library/user-event': specifier: ^14.5.2 version: 14.5.2(@testing-library/dom@9.3.4) + '@types/lodash.isempty': + specifier: ^4.4.9 + version: 4.4.9 '@types/react': specifier: ^18.2.55 version: 18.2.55 @@ -86,6 +89,9 @@ devDependencies: json-schema-to-ts: specifier: ^3.0.0 version: 3.0.0 + lodash.isempty: + specifier: ^4.4.0 + version: 4.4.0 react: specifier: ^18.2.0 version: 18.2.0 @@ -3957,6 +3963,12 @@ packages: resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} dev: true + /@types/lodash.isempty@4.4.9: + resolution: {integrity: sha512-DPSFfnT2JmZiAWNWOU8IRZws/Ha6zyGF5m06TydfsY+0dVoQqby2J61Na2QU4YtwiZ+moC6cJS6zWYBJq4wBVw==} + dependencies: + '@types/lodash': 4.14.202 + dev: true + /@types/lodash@4.14.202: resolution: {integrity: sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ==} dev: true @@ -7290,6 +7302,10 @@ packages: resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} dev: true + /lodash.isempty@4.4.0: + resolution: {integrity: sha512-oKMuF3xEeqDltrGMfDxAPGIVMSSRv8tbRSODbrs4KGsRRLEhrW8N8Rd4DRgB2+621hY8A8XwwrTVhXWpxFvMzg==} + dev: true + /lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} dev: true diff --git a/src/controls/NumberControl.test.tsx b/src/controls/NumberControl.test.tsx deleted file mode 100644 index 8d16f6b..0000000 --- a/src/controls/NumberControl.test.tsx +++ /dev/null @@ -1,163 +0,0 @@ -import { describe, expect, test, it } from "vitest" -import { screen, waitFor } from "@testing-library/react" -import { userEvent } from "@testing-library/user-event" -import { render } from "../common/test-render" -import { JSONSchema } from "json-schema-to-ts" -import { - numberBasisPointsSchema, - numberBasisPointsUISchema, - numberMagnitudeSchema, - numberMagnitudeUISchema, - numberTheNumberSchema, - numberTheNumberUISchema, - numberHumiditySchema, - numberPercentageUISchema, - numberTemperatureSchema, - numberTemperatureUISchema, - numberUISchemaWithRule, -} from "../testSchemas/numberSchema" - -describe("NumberControl", () => { - test("renders a number input with no UISchema provided", () => { - render({ - schema: numberMagnitudeSchema, - }) - - expect(screen.getByText("Magnitude")).not.toBeNull() - }) - - it("Follows the hide rule", () => { - const data = { magnitude: 1000 } - render({ - data: data, - schema: numberMagnitudeSchema, - uischema: numberUISchemaWithRule, - }) - expect(screen.queryByText("Magnitude")).toBeNull() - }) - - it.each([[0], [100]])("renders when data of %s is included", (dataVal: number) => { - const data = { theNumber: dataVal} - render({ - data: data, - schema: numberTheNumberSchema, // this has a default of 42.42 - uischema: numberTheNumberUISchema, - }) - expect(screen.getByText("The Number")).not.toBeNull() - expect(screen.getByRole("spinbutton")).toHaveValue(`${dataVal}`) - }) - - it.each([[0], [100]])("renders default value of %s when no data is provided", (defaultValue: number) => { - // create a new schema with a default value set - const { properties, ...rest } = numberMagnitudeSchema - properties.magnitude = { ...properties.magnitude, ...{ default: defaultValue }} - render({ - schema: { ...rest, properties }, - uischema: numberMagnitudeUISchema, - }) - - expect(screen.getByText("Magnitude")).not.toBeNull() - expect(screen.getByRole("spinbutton")).toHaveValue(`${defaultValue}`) - }) - it("renders default value when no data is provided", () => { - render({ - schema: numberTheNumberSchema, - uischema: numberTheNumberUISchema, - }) - expect(screen.getByRole("spinbutton")).toHaveValue("42.42") - }) - - it("changes its value when users type", async () => { - let data: JSONSchema - render({ - schema: numberMagnitudeSchema, - uischema: numberMagnitudeUISchema, - onChange: (state: { data: JSONSchema }) => { - data = state.data - }, - }) - - await userEvent.clear(screen.getByRole("spinbutton")) - await userEvent.type(screen.getByRole("spinbutton"), "123") - - await waitFor(() => { - expect(data).toEqual({ magnitude: 123 }) - }) - }) - - it("renders slider when min max values are present", () => { - const data = { basisPoints: 1 } - render({ - data: data, - schema: numberBasisPointsSchema, - uischema: numberBasisPointsUISchema, - }) - expect(screen.getByText("Basis Points")).not.toBeNull() - expect(screen.getByRole("spinbutton")).toHaveValue("1") - expect(screen.getByRole("slider")).not.toBeNull() - expect(screen.getByRole("slider")).toHaveAttribute("aria-valuenow", "1") - }) - - it("doesn't show slider when min max values are not present", () => { - render({ - schema: numberMagnitudeSchema, - uischema: numberMagnitudeUISchema, - }) - expect(screen.queryByRole("slider")).toBeNull() - }) -}) - -it ("renders slider and input box when number of steps is greater than threshold", () => { - const data = { basisPoints: 1 } - render({ - data: data, - schema: numberBasisPointsSchema, // 10,000 steps created by multipleOf - uischema: numberBasisPointsUISchema, - }) - expect(screen.getByRole("slider")) - expect(screen.getByRole("spinbutton")) -}) - -it ("renders slider without input box when number of steps is less than threshold", () => { - const data = { basisPoints: 1 } - render({ - data: data, - schema: numberHumiditySchema, // 100 steps - uischema: numberPercentageUISchema, - }) - expect(screen.getByRole("slider")) - expect(screen.queryByRole("spinbutton")).toBeNull() -}) - -it ("shows error message onBlur when field is required and empty", async () => { - render({ - schema: numberTheNumberSchema, - uischema: numberTheNumberUISchema, - }) - const input = screen.getByRole("spinbutton") - await userEvent.clear(input) - await userEvent.tab() - expect(await screen.findByText("The Number is required")).not.toBeNull() -}) - -it ("shows units in text control if set in UI schema", async () => { - render({ - schema: numberTemperatureSchema, - uischema: numberTemperatureUISchema, - }) - expect(await screen.findByText("°F")).not.toBeNull() -}) - -it ("shows units in tooltip if set in UI schema", async () => { - render({ - schema: numberHumiditySchema, - uischema: numberPercentageUISchema, - }) - // we are using a schema that doesn't have the text input, - // so we can be sure the units are showing up in the tooltip - expect(screen.queryByRole("spinbutton")).toBeNull() - const slider = screen.getByRole("slider") - expect(screen.queryByText("0%")).toBeNull() - await userEvent.hover(slider) - expect(await screen.findByText("0%")).not.toBeNull() -}) \ No newline at end of file diff --git a/src/controls/NumberControl.tsx b/src/controls/NumberControl.tsx deleted file mode 100644 index cc0649a..0000000 --- a/src/controls/NumberControl.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { createNumericControl } from "./NumericControl" - -export const NumberControl = createNumericControl({ - coerceNumber: (value) => Number(value), -}) diff --git a/src/controls/NumericControl.test.tsx b/src/controls/NumericControl.test.tsx deleted file mode 100644 index 31222b9..0000000 --- a/src/controls/NumericControl.test.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { describe, expect, it } from "vitest" -import { screen, waitFor } from "@testing-library/react" -import { userEvent } from "@testing-library/user-event" -import { render } from "../common/test-render" -import { numberTheNumberSchema, numberWeightSchema } from "../testSchemas/numberSchema" - -describe("NumericControl", () => { - it("does not fall back to default if value is empty", () => { - render({ - schema: { ...numberTheNumberSchema }, - data: {}, - }) - - expect(screen.getByRole("spinbutton")).toHaveValue("") - }) - - it("calls onChange with number values", async () => { - let data = { theNumber: 42.00 } - render({ - schema: numberTheNumberSchema, - data, - onChange: (state) => { - data = state.data - }, - }) - - await userEvent.clear(screen.getByRole("spinbutton")) - await userEvent.type(screen.getByRole("spinbutton"), "42.00") - - await waitFor(() => { - expect(data).toBe(42.00) - }) - }) - - it("calls onChange with empty object and no errors when the input gets cleared out and optional", async () => { - const weight = {} - let state: Record = {} - render({ - schema: numberWeightSchema, - data: weight, - onChange: (newState) => { - state = newState - }, - }) - - await userEvent.clear(screen.getByRole("spinbutton")) - - await waitFor(() => { - expect(state.data).toBe(weight) - const errors = state.errors - console.log({errors}) - expect(state.errors).toHaveLength(0) - }) - }) -}) \ No newline at end of file diff --git a/src/controls/NumericControl.tsx b/src/controls/NumericControl.tsx deleted file mode 100644 index bdf1a7b..0000000 --- a/src/controls/NumericControl.tsx +++ /dev/null @@ -1,122 +0,0 @@ -import { ControlProps, RendererProps } from "@jsonforms/core" -import { Col, Form, InputNumber, Row, Slider } from "antd" -import { decimalToPercentage } from "./utils" - - - -export const createNumericControl = (args: { coerceNumber: (value: number) => number; pattern?: string }) => { - return function NumericControl({ - data, - handleChange, - path, - required, - label, - visible, - id, - schema, - uischema, - }: ControlProps & RendererProps) { - const arialLabelWithFallback = label || schema.description || "Value" - const isRequired = required || uischema.options?.required as boolean - - const maxStepsWithoutTextInput = 100 - const { maximum, minimum, multipleOf } = schema - const isRangeDefined = typeof maximum === "number" && typeof minimum === "number" - let step: number | undefined = undefined - let stepCount: number | undefined = undefined - if (isRangeDefined) { - const range = Math.abs(maximum - minimum) - step = multipleOf || (range / maxStepsWithoutTextInput) - stepCount = range / step - } - const isLargeStepCount = stepCount && stepCount > maxStepsWithoutTextInput - - const initialValue: number | undefined = typeof schema?.default === "number" ? schema.default : minimum - const isEmptyObj = typeof data === "object" && data !== undefined && data !== null ? Object.keys(data as object).length === 0 : false - const value = data === undefined || isEmptyObj ? initialValue : data as number | null - - const addonAfter = uischema.options?.addonAfter as string | undefined - const addonBefore = uischema.options?.addonBefore as string | undefined - const isPercentage = addonAfter?.trim() === "%" - - const onChange = (value: number | null) => { - if ((typeof value === "number" && (!isRangeDefined || (isRangeDefined && value >= minimum && value <= maximum))) || value === null) { - handleChange(path, value !== null ? args.coerceNumber(value) : value) - } - } - - const marginLeft = isRangeDefined ? 16 : 0 - const style = { marginLeft: marginLeft, width: "100%" } - const formatter = ((value?: number) => { - if (typeof value !== "undefined") { - if (isPercentage) { - return decimalToPercentage(value) - } else { - return value.toString() - } - } - return "" - }) - - const numberInput = ( - - ) - - if (!visible) return null - - const tooltip = { - formatter: (value?: number) => { - if (isPercentage) { - return `${decimalToPercentage(value || initialValue)}%` - } else { - return `${addonBefore ? addonBefore : ""}${value || initialValue}${addonAfter ? addonAfter : ""}` - } - } - } - - const slider = - - return ( - - {isRangeDefined ? ( - - {slider} - {isLargeStepCount ? {numberInput} : null} - - ) : ( - {numberInput} - )} - - ) - } -} diff --git a/src/controls/ObjectControl.test.tsx b/src/controls/ObjectControl.test.tsx new file mode 100644 index 0000000..f1b2e6f --- /dev/null +++ b/src/controls/ObjectControl.test.tsx @@ -0,0 +1,44 @@ +import { test, expect, describe } from "vitest"; +import { objectSchema, objectUISchemaWithName, objectUISchemaWithRule } from "../testSchemas/objectSchema" +import { render } from "../common/test-render"; +import { screen } from "@testing-library/react" + +describe("ObjectControl", () => { + test("renders nested fields", () => { + render({ schema: objectSchema }) + + expect(screen.getByText("Name")).not.toBeNull() + expect(screen.getByText("Last Name")).not.toBeNull() + }) + + describe("only renders when visible", () => { + test("property is not visible if not included in UISchema", () => { + render({ schema: objectSchema, uischema: objectUISchemaWithName }) + + expect(screen.queryByText("Last Name")).toBeNull() + }) + + describe("manage visibility with condition rules", () => { + test("hide field when condition matches", () => { + render({ + data: { name: "John", lastName: "Doe" }, + schema: objectSchema, + uischema: objectUISchemaWithRule, + }) + + expect(screen.queryByText("Last Name")).toBeNull() + }) + + test("render field when condition doesn't match", () => { + render({ + data: { name: "Bob", lastName: "Doe" }, + schema: objectSchema, + uischema: objectUISchemaWithRule, + }) + + expect(screen.getByText("Name")).not.toBeNull() + expect(screen.getByText("Last Name")).not.toBeNull() + }) + }) + }) +}) diff --git a/src/controls/ObjectControl.tsx b/src/controls/ObjectControl.tsx new file mode 100644 index 0000000..9f3e741 --- /dev/null +++ b/src/controls/ObjectControl.tsx @@ -0,0 +1,57 @@ +import { useMemo } from "react" +import { + findUISchema, + Generate, + StatePropsOfControlWithDetail +} from "@jsonforms/core" +import { JsonFormsDispatch } from "@jsonforms/react" +import isEmpty from "lodash.isempty" + + + +export function ObjectControl({ + renderers, + cells, + uischemas, + schema, + label, + path, + visible, + enabled, + uischema, + rootSchema, +}: StatePropsOfControlWithDetail) { + const detailUiSchema = useMemo( + () => + findUISchema( + uischemas ?? [], + schema, + uischema.scope, + path, + () => + isEmpty(path) + ? Generate.uiSchema(schema, "VerticalLayout") + : { ...Generate.uiSchema(schema, "Group"), label }, + uischema, + rootSchema, + ), + [uischemas, schema, path, label, uischema, rootSchema], + ) + + if (!visible) { + return null + } + + return ( + + ) +} diff --git a/src/layouts/GroupLayoutRenderer.tsx b/src/layouts/GroupLayoutRenderer.tsx new file mode 100644 index 0000000..8b28142 --- /dev/null +++ b/src/layouts/GroupLayoutRenderer.tsx @@ -0,0 +1,30 @@ +import { GroupLayout, OwnPropsOfRenderer } from "@jsonforms/core"; +import { UISchema } from "../ui-schema"; +import { Divider } from "antd"; +import { AntDLayoutRenderer } from "./LayoutRenderer"; + +export type LayoutRendererProps = OwnPropsOfRenderer & { + elements: UISchema[]; +}; + +export function GroupLayoutRenderer({ + visible, + enabled, + uischema, + ...props +}: LayoutRendererProps) { + const groupLayout = uischema as GroupLayout; + return ( + <> + + {groupLayout?.label && {groupLayout.label}} + + + + ); +} diff --git a/src/layouts/VerticalLayout.tsx b/src/layouts/VerticalLayoutRenderer.tsx similarity index 100% rename from src/layouts/VerticalLayout.tsx rename to src/layouts/VerticalLayoutRenderer.tsx diff --git a/src/renderers.ts b/src/renderers.ts index b595170..ff1d8fa 100644 --- a/src/renderers.ts +++ b/src/renderers.ts @@ -1,24 +1,67 @@ -import { JsonFormsRendererRegistryEntry,JsonFormsCellRendererRegistryEntry, isBooleanControl, isNumberControl, isStringControl, rankWith, uiTypeIs } from "@jsonforms/core"; -import { withJsonFormsControlProps, withJsonFormsLabelProps, withJsonFormsCellProps, withJsonFormsLayoutProps } from "@jsonforms/react"; +import { + JsonFormsRendererRegistryEntry, + JsonFormsCellRendererRegistryEntry, + isBooleanControl, + isStringControl, + rankWith, + uiTypeIs, + isObjectControl, + isLayout, + not, + and, +} from "@jsonforms/core"; +import { + withJsonFormsControlProps, + withJsonFormsLabelProps, + withJsonFormsCellProps, + withJsonFormsLayoutProps, + withJsonFormsDetailProps, +} from "@jsonforms/react"; import { BooleanControl } from "./controls/BooleanControl"; import { AlertControl } from "./controls/AlertControl"; import { TextControl } from "./controls/TextControl"; import { UnknownControl } from "./controls/UnknownControl"; -import { VerticalLayoutRenderer } from "./layouts/VerticalLayout"; -import { NumberControl } from "./controls/NumberControl"; - +import { VerticalLayoutRenderer } from "./layouts/VerticalLayoutRenderer"; +import { ObjectControl } from "./controls/ObjectControl"; +import { GroupLayoutRenderer } from "./layouts/GroupLayoutRenderer"; +import React from "react"; // Ordered from lowest rank to highest rank. Higher rank renderers will be preferred over lower rank renderers. export const rendererRegistryEntries: JsonFormsRendererRegistryEntry[] = [ - { tester: rankWith(1, () => true), renderer: withJsonFormsControlProps(UnknownControl) }, - { tester: rankWith(2, uiTypeIs("VerticalLayout")), renderer: withJsonFormsLayoutProps(VerticalLayoutRenderer) }, - { tester: rankWith(2, isBooleanControl), renderer: withJsonFormsControlProps(BooleanControl) }, - { tester: rankWith(2, isStringControl), renderer: withJsonFormsControlProps(TextControl) }, - { tester: rankWith(2, uiTypeIs("Label")), renderer: withJsonFormsLabelProps(AlertControl) }, - { tester: rankWith(2, isNumberControl), renderer: withJsonFormsControlProps(NumberControl) }, + { + tester: rankWith(1, () => true), + renderer: withJsonFormsControlProps(UnknownControl), + }, + { + tester: rankWith(1, uiTypeIs("Group")), + renderer: React.memo(GroupLayoutRenderer), + }, + { + tester: rankWith(2, uiTypeIs("VerticalLayout")), + renderer: withJsonFormsLayoutProps(VerticalLayoutRenderer), + }, + { + tester: rankWith(2, isBooleanControl), + renderer: withJsonFormsControlProps(BooleanControl), + }, + { + tester: rankWith(2, isStringControl), + renderer: withJsonFormsControlProps(TextControl), + }, + { + tester: rankWith(2, uiTypeIs("Label")), + renderer: withJsonFormsLabelProps(AlertControl), + }, + { + tester: rankWith(10, and(isObjectControl, not(isLayout))), + renderer: withJsonFormsDetailProps(ObjectControl), + }, ]; export const cellRegistryEntries: JsonFormsCellRendererRegistryEntry[] = [ - { tester: rankWith(1, () => true), cell: withJsonFormsCellProps(UnknownControl)} + { + tester: rankWith(1, () => true), + cell: withJsonFormsCellProps(UnknownControl), + }, ]; diff --git a/src/stories/controls/ObjectControl.stories.tsx b/src/stories/controls/ObjectControl.stories.tsx new file mode 100644 index 0000000..dab2c7a --- /dev/null +++ b/src/stories/controls/ObjectControl.stories.tsx @@ -0,0 +1,49 @@ +import { Meta, StoryObj } from "@storybook/react"; +import { rendererRegistryEntries } from "../../renderers"; +import { StorybookAntDJsonForm } from "../../common/StorybookAntDJsonForm"; +import { objectSchema, objectUISchemaWithName, objectUISchemaWithNameAndLastName, objectUISchemaWithRule } from "../../testSchemas/objectSchema"; + +const meta: Meta = { + title: "Control/Object", + component: StorybookAntDJsonForm, + tags: ["autodocs"], + args: { + jsonSchema: objectSchema, + rendererRegistryEntries: [ + ...rendererRegistryEntries, + ] + }, + argTypes: { + rendererRegistryEntries: {}, + jsonSchema: { + control: "object", + } + } +}; + +export default meta; +type Story = StoryObj; + +export const ObjectWithUISchemaContainingOnlyName: Story = { + tags: ["autodocs"], + args: { + jsonSchema: objectSchema, + uiSchema: objectUISchemaWithName, + }, +} + +export const ObjectWithUISchemaContainingBothNameAndLastName: Story = { + tags: ["autodocs"], + args: { + jsonSchema: objectSchema, + uiSchema: objectUISchemaWithNameAndLastName, + }, +} + +export const ObjectWithRuleHidingLastNameIfNameIsJohn: Story = { + tags: ["autodocs"], + args: { + jsonSchema: objectSchema, + uiSchema: objectUISchemaWithRule, + }, +} \ No newline at end of file diff --git a/src/testSchemas/numberSchema.ts b/src/testSchemas/numberSchema.ts deleted file mode 100644 index 11ce28d..0000000 --- a/src/testSchemas/numberSchema.ts +++ /dev/null @@ -1,218 +0,0 @@ -import { RuleEffect } from "@jsonforms/core" -import { JSONSchema } from "json-schema-to-ts" -import { UISchema } from "../ui-schema" - -export const numberMagnitudeSchema = { - type: "object", - properties: { - magnitude: { - title: "Magnitude", - type: "number", - }, - }, - required: ["magnitude"], -} satisfies JSONSchema - -export const numberTheNumberSchema = { - type: "number", - properties: { - theNumber: { - title: "The Number", - type: "number", - default: 42.42, - }, - }, - required: ["theNumber"], -} satisfies JSONSchema - -export const numberWeightSchema = { - type: "object", - properties: { - weight: { - title: "Weight", - type: "number", - }, - }, -} satisfies JSONSchema - -export const numberPriceSchema = { - type: "object", - properties: { - price: { - title: "Price", - type: "number", - }, - }, - required: ["price"], -} satisfies JSONSchema - -export const numberBasisPointsSchema = { - type: "object", - properties: { - basisPoints: { - title: "Basis Points", - type: "number", - minimum: 0, - maximum: 10000, - multipleOf: 1, - }, - }, -} satisfies JSONSchema - -export const numberTemperatureSchema = { - type: "number", - title: "Today's Temperature", - minimum: -50, - maximum: 150, - default: 70, - multipleOf: 1, -} satisfies JSONSchema - -export const numberHumiditySchema = { - type: "number", - title: "Humidity", - minimum: 0.0, - maximum: 1.0, -} satisfies JSONSchema - -export const numberRelativeChangeSchema = { - type: "number", - title: "Relative Change", - minimum: -1.0, - maximum: 10.0, - default: 0.0, - multipleOf: 0.01, -} satisfies JSONSchema - -export const numberFinalGradeSchema = { - type: "number", - title: "Final Grade", - minimum: 0.0, - maximum: 1.0, - default: 0.5, -} satisfies JSONSchema - - -export const numberDonateNowSchema = { - type: "object", - properties: { - donateNow: { - type: "number", - title: "Donate Now", - minimum: 5.00, - maximum: 1000.00, - default: 20.00, - multipleOf: 5.00, - } - }, - required: ["donateNow"], -} satisfies JSONSchema - - -export const numberMagnitudeUISchema = { - type: "VerticalLayout", - elements: [ - { - type: "Control", - scope: "#/properties/magnitude", - }, - ], -} satisfies UISchema - -export const numberPriceUISchema = { - type: "VerticalLayout", - elements: [ - { - type: "Control", - scope: "#/properties/price", - options: { - addonBefore: "$", - } - }, - ], -} satisfies UISchema - -export const numberTheNumberUISchema = { - type: "VerticalLayout", - elements: [ - { - type: "Control", - scope: "#/properties/theNumber", - }, - ], -} satisfies UISchema - - -export const numberWeightUISchema = { - type: "VerticalLayout", - elements: [ - { - type: "Control", - scope: "#/properties/weight", - }, - ], -} satisfies UISchema - -export const numberBasisPointsUISchema = { - type: "VerticalLayout", - elements: [ - { - type: "Control", - scope: "#/properties/basisPoints", - }, - ], -} satisfies UISchema - - -export const numberPercentageUISchema = { - type: "VerticalLayout", - elements: [ - { - type: "Control", - scope: "#", - options: { - addonAfter: "%", - } - }, - ], -} satisfies UISchema - -export const numberTemperatureUISchema = { - type: "VerticalLayout", - elements: [ - { - type: "Control", - scope: "#", - options: { - addonAfter: "°F", - } - }, - ], -} satisfies UISchema - -export const numberDonateNowUISchema = { - type: "VerticalLayout", - elements: [ - { - type: "Control", - scope: "#/properties/donateNow", - options: { - addonBefore: "$", - } - }, - ], -} satisfies UISchema - -export const numberUISchemaWithRule = { - type: "VerticalLayout", - elements: [ - { - type: "Control", - scope: "#", - rule: { - effect: RuleEffect.HIDE, - condition: {}, - }, - }, - ], -} satisfies UISchema \ No newline at end of file diff --git a/src/testSchemas/objectSchema.ts b/src/testSchemas/objectSchema.ts new file mode 100644 index 0000000..da25b72 --- /dev/null +++ b/src/testSchemas/objectSchema.ts @@ -0,0 +1,64 @@ +import { JSONSchema } from "json-schema-to-ts" +import { UISchema } from "../ui-schema" +import { RuleEffect } from "@jsonforms/core" + +export const objectSchema = { + type: "object", + title: "My Object", + properties: { + name: { + title: "Name", + type: "string", + }, + lastName: { + title: "Last Name", + type: "string", + }, + }, + additionalProperties: false, +} satisfies JSONSchema + +export const objectUISchemaWithName = { + type: "VerticalLayout", + elements: [ + { + type: "Control", + scope: "#/properties/name", + }, + ], +} satisfies UISchema + +export const objectUISchemaWithNameAndLastName = { + type: "VerticalLayout", + elements: [ + { + type: "Control", + scope: "#/properties/name", + }, + { + type: "Control", + scope: "#/properties/lastName", + }, + ], +} satisfies UISchema + +export const objectUISchemaWithRule = { + type: "VerticalLayout", + elements: [ + { + type: "Control", + scope: "#/properties/name", + }, + { + type: "Control", + scope: "#/properties/lastName", + rule: { + effect: RuleEffect.HIDE, + condition: { + scope: "#/properties/name", + schema: { const: "John" }, + }, + }, + }, + ], +} satisfies UISchema