From e941fdfc3cb2790be4e39f7d31abf13bedf4c6dc Mon Sep 17 00:00:00 2001 From: Thu Pham Date: Thu, 7 Mar 2024 23:27:40 -0800 Subject: [PATCH 1/3] Adding ObjectControl --- package.json | 10 ++- pnpm-lock.yaml | 16 +++++ src/controls/ObjectControl.test.tsx | 44 +++++++++++++ src/controls/ObjectControl.tsx | 57 +++++++++++++++++ src/layouts/GroupRenderer.tsx | 20 ++++++ src/renderers.ts | 9 ++- .../controls/ObjectControl.stories.tsx | 49 ++++++++++++++ src/testSchemas/objectSchema.ts | 64 +++++++++++++++++++ 8 files changed, 264 insertions(+), 5 deletions(-) create mode 100644 src/controls/ObjectControl.test.tsx create mode 100644 src/controls/ObjectControl.tsx create mode 100644 src/layouts/GroupRenderer.tsx create mode 100644 src/stories/controls/ObjectControl.stories.tsx create mode 100644 src/testSchemas/objectSchema.ts 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/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/GroupRenderer.tsx b/src/layouts/GroupRenderer.tsx new file mode 100644 index 0000000..d108b67 --- /dev/null +++ b/src/layouts/GroupRenderer.tsx @@ -0,0 +1,20 @@ +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 GroupComponent({ visible, enabled, uischema, ...props }: LayoutRendererProps) { + const groupLayout = uischema as GroupLayout + return ( + <> + + {groupLayout?.label && {groupLayout.label}} + + + + ) +} diff --git a/src/renderers.ts b/src/renderers.ts index b595170..e365e05 100644 --- a/src/renderers.ts +++ b/src/renderers.ts @@ -1,5 +1,5 @@ -import { JsonFormsRendererRegistryEntry,JsonFormsCellRendererRegistryEntry, isBooleanControl, isNumberControl, isStringControl, rankWith, uiTypeIs } from "@jsonforms/core"; -import { withJsonFormsControlProps, withJsonFormsLabelProps, withJsonFormsCellProps, withJsonFormsLayoutProps } from "@jsonforms/react"; +import { JsonFormsRendererRegistryEntry,JsonFormsCellRendererRegistryEntry, isBooleanControl, isNumberControl, 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"; @@ -7,16 +7,21 @@ import { TextControl } from "./controls/TextControl"; import { UnknownControl } from "./controls/UnknownControl"; import { VerticalLayoutRenderer } from "./layouts/VerticalLayout"; import { NumberControl } from "./controls/NumberControl"; +import { ObjectControl } from "./controls/ObjectControl"; +import { GroupComponent } from "./layouts/GroupRenderer"; +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(1, uiTypeIs("Group")), renderer: React.memo(GroupComponent) }, { 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(10, and(isObjectControl, not(isLayout))), renderer: withJsonFormsDetailProps(ObjectControl) } ]; export const cellRegistryEntries: JsonFormsCellRendererRegistryEntry[] = [ 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/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 From 2a71c497e231d4353a133f32620409beb872bdb6 Mon Sep 17 00:00:00 2001 From: Thu Pham Date: Sat, 9 Mar 2024 09:21:01 -0800 Subject: [PATCH 2/3] Updating naming --- src/layouts/GroupLayoutRenderer.tsx | 30 ++++++++ src/layouts/GroupRenderer.tsx | 20 ------ ...lLayout.tsx => VerticalLayoutRenderer.tsx} | 0 src/renderers.ts | 72 +++++++++++++++---- 4 files changed, 88 insertions(+), 34 deletions(-) create mode 100644 src/layouts/GroupLayoutRenderer.tsx delete mode 100644 src/layouts/GroupRenderer.tsx rename src/layouts/{VerticalLayout.tsx => VerticalLayoutRenderer.tsx} (100%) 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/GroupRenderer.tsx b/src/layouts/GroupRenderer.tsx deleted file mode 100644 index d108b67..0000000 --- a/src/layouts/GroupRenderer.tsx +++ /dev/null @@ -1,20 +0,0 @@ -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 GroupComponent({ 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 e365e05..6e7b3f0 100644 --- a/src/renderers.ts +++ b/src/renderers.ts @@ -1,29 +1,73 @@ -import { JsonFormsRendererRegistryEntry,JsonFormsCellRendererRegistryEntry, isBooleanControl, isNumberControl, isStringControl, rankWith, uiTypeIs, isObjectControl, isLayout, not, and } from "@jsonforms/core"; -import { withJsonFormsControlProps, withJsonFormsLabelProps, withJsonFormsCellProps, withJsonFormsLayoutProps, withJsonFormsDetailProps } from "@jsonforms/react"; +import { + JsonFormsRendererRegistryEntry, + JsonFormsCellRendererRegistryEntry, + isBooleanControl, + isNumberControl, + 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 { VerticalLayoutRenderer } from "./layouts/VerticalLayoutRenderer"; import { NumberControl } from "./controls/NumberControl"; import { ObjectControl } from "./controls/ObjectControl"; -import { GroupComponent } from "./layouts/GroupRenderer"; +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(1, uiTypeIs("Group")), renderer: React.memo(GroupComponent) }, - { 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(10, and(isObjectControl, not(isLayout))), renderer: withJsonFormsDetailProps(ObjectControl) } + { + 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(2, isNumberControl), + renderer: withJsonFormsControlProps(NumberControl), + }, + { + 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), + }, ]; From 4ca5867de77def5251e688d2505e75e8eef1a12b Mon Sep 17 00:00:00 2001 From: T Pham Date: Sat, 9 Mar 2024 09:27:43 -0800 Subject: [PATCH 3/3] Update renderers.ts --- src/renderers.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/renderers.ts b/src/renderers.ts index 6e7b3f0..ff1d8fa 100644 --- a/src/renderers.ts +++ b/src/renderers.ts @@ -2,7 +2,6 @@ import { JsonFormsRendererRegistryEntry, JsonFormsCellRendererRegistryEntry, isBooleanControl, - isNumberControl, isStringControl, rankWith, uiTypeIs, @@ -24,7 +23,6 @@ import { AlertControl } from "./controls/AlertControl"; import { TextControl } from "./controls/TextControl"; import { UnknownControl } from "./controls/UnknownControl"; import { VerticalLayoutRenderer } from "./layouts/VerticalLayoutRenderer"; -import { NumberControl } from "./controls/NumberControl"; import { ObjectControl } from "./controls/ObjectControl"; import { GroupLayoutRenderer } from "./layouts/GroupLayoutRenderer"; import React from "react"; @@ -55,10 +53,6 @@ export const rendererRegistryEntries: JsonFormsRendererRegistryEntry[] = [ tester: rankWith(2, uiTypeIs("Label")), renderer: withJsonFormsLabelProps(AlertControl), }, - { - tester: rankWith(2, isNumberControl), - renderer: withJsonFormsControlProps(NumberControl), - }, { tester: rankWith(10, and(isObjectControl, not(isLayout))), renderer: withJsonFormsDetailProps(ObjectControl),