From 6f621863f85c1f1ded9ef04e91ee798695295e5e Mon Sep 17 00:00:00 2001 From: Aaron Ross Date: Mon, 21 Oct 2024 14:13:16 -0700 Subject: [PATCH] feature(DateTimeControl): add control, tests and stories --- package.json | 1 + pnpm-lock.yaml | 4 +- src/controls/DateTimeControl.test.tsx | 86 +++++++++++++++++++ src/controls/DateTimeControl.tsx | 75 ++++++++++++++++ src/renderer-registry-entries.ts | 6 ++ .../controls/DateTimeControl.stories.tsx | 65 ++++++++++++++ src/testSchemas/dateTimeSchema.ts | 83 ++++++++++++++++++ 7 files changed, 319 insertions(+), 1 deletion(-) create mode 100644 src/controls/DateTimeControl.test.tsx create mode 100644 src/controls/DateTimeControl.tsx create mode 100644 src/stories/controls/DateTimeControl.stories.tsx create mode 100644 src/testSchemas/dateTimeSchema.ts diff --git a/package.json b/package.json index ac66e9e..0d7b073 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,7 @@ "@jsonforms/core": "^3.3.0", "@jsonforms/react": "^3.3.0", "antd": "^5.14.0", + "dayjs": "^1", "react": "^17 || ^18" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1ec2cec..81c60fa 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,6 +5,9 @@ settings: excludeLinksFromLockfile: false dependencies: + dayjs: + specifier: ^1 + version: 1.11.13 lodash.isempty: specifier: ^4.4.0 version: 4.4.0 @@ -6051,7 +6054,6 @@ packages: /dayjs@1.11.13: resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==} - dev: true /debug@2.6.9: resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} diff --git a/src/controls/DateTimeControl.test.tsx b/src/controls/DateTimeControl.test.tsx new file mode 100644 index 0000000..d331401 --- /dev/null +++ b/src/controls/DateTimeControl.test.tsx @@ -0,0 +1,86 @@ +import { test, expect } from "vitest" +import { screen, waitFor } from "@testing-library/react" +import { userEvent } from "@testing-library/user-event" + +import { render } from "../common/test-render" +import { dateTimeSchema } from "../testSchemas/dateTimeSchema" + +const EXAMPLE_DATESTRING = "2021-08-09 12:34:56" +const USER_DATESTRING = EXAMPLE_DATESTRING.replace(" ", "") +const TITLE = dateTimeSchema.properties.dateTime.title + +test("renders the date that the user selects", async () => { + render({ + schema: dateTimeSchema, + }) + const input = await screen.findByLabelText( + dateTimeSchema.properties.dateTime.title, + ) + await userEvent.type(input, USER_DATESTRING) + + await waitFor(() => expect(input).toHaveValue(EXAMPLE_DATESTRING)) +}) + +test("renders default date when present", async () => { + render({ + schema: { + ...dateTimeSchema, + properties: { + dateTime: { + ...dateTimeSchema.properties.dateTime, + default: EXAMPLE_DATESTRING, + }, + }, + }, + }) + const input = await screen.findByLabelText( + dateTimeSchema.properties.dateTime.title, + ) + expect(input).toHaveValue(EXAMPLE_DATESTRING) +}) + +test("updates jsonforms data as expected", async () => { + let data: Record = {} + render({ + schema: dateTimeSchema, + data, + onChange: (result) => { + data = result.data as Record + }, + }) + const input = await screen.findByLabelText( + dateTimeSchema.properties.dateTime.title, + ) + await userEvent.type(input, USER_DATESTRING) + await userEvent.click(screen.getByText("Submit")) + await waitFor(() => { + expect(data).toEqual({ + dateTime: EXAMPLE_DATESTRING, + }) + }) +}) + +test("renders required message if no value and interaction", async () => { + render({ + schema: { + ...dateTimeSchema, + required: ["dateTime"], + }, + }) + const input = await screen.findByLabelText(TITLE) + await userEvent.clear(input) + await userEvent.tab() + await screen.findByText(`${TITLE} is required`) +}) + +test(" does not show required message if not requried", async () => { + render({ + schema: dateTimeSchema, + }) + const input = await screen.findByLabelText(TITLE) + await userEvent.clear(input) + await userEvent.tab() + await waitFor(() => { + expect(screen.queryByText(`${TITLE} is required`)).toBeNull() + }) +}) diff --git a/src/controls/DateTimeControl.tsx b/src/controls/DateTimeControl.tsx new file mode 100644 index 0000000..b1571d9 --- /dev/null +++ b/src/controls/DateTimeControl.tsx @@ -0,0 +1,75 @@ +import { memo } from "react" +import type { ControlProps as JSFControlProps } from "@jsonforms/core" +import { withJsonFormsControlProps } from "@jsonforms/react" +import { DatePicker, type DatePickerProps, Form } from "antd" +import type { Rule } from "antd/es/form" +import dayjs from "dayjs" + +import { + ControlUISchema, + DateTimeControlOptions, + isDateTimeControlOptions, +} from "../ui-schema" + +type ControlProps = Omit & { + uischema: ControlUISchema | JSFControlProps["uischema"] +} +// initialize once +const DEFAULT_PROPS: DateTimeControlOptions = { + format: { format: "YYYY-MM-DD HH:mm:ss", type: "mask" }, +} as const + +function getProps(options: unknown): DateTimeControlOptions { + if (isDateTimeControlOptions(options)) { + return options + } + return DEFAULT_PROPS +} + +export function DateTimeControl({ + handleChange, + path, + label, + id, + required, + schema, + uischema, + visible, +}: ControlProps) { + if (!visible) return null + + const initialValue = + typeof schema.default === "string" ? dayjs(schema.default) : undefined + + const rules: Rule[] = [{ required, message: `${label} is required` }] + + const formItemProps = + "formItemProps" in uischema ? uischema.formItemProps : {} + + const onChange: DatePickerProps["onChange"] = (_dateObj, dateString) => { + handleChange(path, dateString) + } + + const options = getProps(uischema.options) + + return ( + + + + ) +} + +export const DateTimeRenderer = withJsonFormsControlProps(memo(DateTimeControl)) diff --git a/src/renderer-registry-entries.ts b/src/renderer-registry-entries.ts index 9e93be6..4ecf90d 100644 --- a/src/renderer-registry-entries.ts +++ b/src/renderer-registry-entries.ts @@ -21,6 +21,7 @@ import { isOneOfControl, isAnyOfControl, isEnumControl, + isDateTimeControl, } from "@jsonforms/core" import { withJsonFormsCellProps } from "@jsonforms/react" @@ -39,6 +40,7 @@ import { PrimitiveArrayRenderer } from "./controls/PrimitiveArrayControl" import { OneOfRenderer } from "./controls/combinators/OneOfControl" import { AnyOfRenderer } from "./controls/combinators/AnyOfControl" import { EnumRenderer } from "./controls/EnumControl" +import { DateTimeRenderer } from "./controls/DateTimeControl" // Ordered from lowest rank to highest rank. Higher rank renderers will be preferred over lower rank renderers. export const rendererRegistryEntries: JsonFormsRendererRegistryEntry[] = [ @@ -113,6 +115,10 @@ export const rendererRegistryEntries: JsonFormsRendererRegistryEntry[] = [ tester: rankWith(30, isPrimitiveArrayControl), renderer: PrimitiveArrayRenderer, }, + { + tester: rankWith(3, isDateTimeControl), + renderer: DateTimeRenderer, + }, ] export const cellRegistryEntries: JsonFormsCellRendererRegistryEntry[] = [ diff --git a/src/stories/controls/DateTimeControl.stories.tsx b/src/stories/controls/DateTimeControl.stories.tsx new file mode 100644 index 0000000..26e8157 --- /dev/null +++ b/src/stories/controls/DateTimeControl.stories.tsx @@ -0,0 +1,65 @@ +import { Meta, StoryObj } from "@storybook/react" +import { StorybookAntDJsonForm } from "../../common/StorybookAntDJsonForm" +import { + dateTimeSchema, + dateTimeShowTimeUISchema, + dateTimeUISchema, + dateTimeOverrideDefaultFormatUISchema, + dateTimeShowMillisecondHideNowUISchema, + dateTimeDefaultValueSchema, +} from "../../testSchemas/dateTimeSchema" + +const meta: Meta = { + title: "Control/DateTime", + component: StorybookAntDJsonForm, + tags: ["autodocs"], + args: { + jsonSchema: dateTimeSchema, + uiSchema: dateTimeUISchema, + }, + argTypes: {}, +} + +export default meta + +type Story = StoryObj + +export const RequiredDatetime: Story = { + tags: ["autodocs"], + args: { + jsonSchema: dateTimeSchema, + uiSchema: dateTimeUISchema, + }, +} + +export const ShowTime: Story = { + tags: ["autodocs"], + args: { + jsonSchema: dateTimeSchema, + uiSchema: dateTimeShowTimeUISchema, + }, +} + +export const ShowMillisecondHideNow: Story = { + tags: ["autodocs"], + args: { + jsonSchema: dateTimeSchema, + uiSchema: dateTimeShowMillisecondHideNowUISchema, + }, +} + +export const OverrideDefaultFormat: Story = { + tags: ["autodocs"], + args: { + jsonSchema: dateTimeSchema, + uiSchema: dateTimeOverrideDefaultFormatUISchema, + }, +} + +export const DefaultValue: Story = { + tags: ["autodocs"], + args: { + jsonSchema: dateTimeDefaultValueSchema, + uiSchema: dateTimeUISchema, + }, +} diff --git a/src/testSchemas/dateTimeSchema.ts b/src/testSchemas/dateTimeSchema.ts new file mode 100644 index 0000000..32c48cd --- /dev/null +++ b/src/testSchemas/dateTimeSchema.ts @@ -0,0 +1,83 @@ +import { JSONSchema } from "json-schema-to-ts" +import { UISchema } from "../ui-schema" + +export const dateTimeSchema = { + type: "object", + properties: { + dateTime: { + title: "Date Time", + type: "string", + format: "date-time", + }, + }, + required: ["dateTime"], +} as const satisfies JSONSchema + +export const dateTimeDefaultValueSchema = { + type: "object", + properties: { + dateTime: { + title: "Date Time", + type: "string", + format: "date-time", + default: "2021-08-09 12:34:56", + }, + }, +} as const satisfies JSONSchema + +export const dateTimeUISchema = { + type: "VerticalLayout", + elements: [ + { + type: "Control", + scope: "#/properties/dateTime", + label: "Date Time", + }, + ], +} satisfies UISchema + +export const dateTimeShowTimeUISchema = { + type: "VerticalLayout", + elements: [ + { + type: "Control", + scope: "#/properties/dateTime", + label: "Date Time", + options: { + showTime: true, + }, + }, + ], +} satisfies UISchema + +export const dateTimeShowMillisecondHideNowUISchema = { + type: "VerticalLayout", + elements: [ + { + type: "Control", + scope: "#/properties/dateTime", + label: "Date Time", + options: { + showTime: true, + showMillisecond: true, + showNow: false, + }, + }, + ], +} satisfies UISchema + +export const dateTimeOverrideDefaultFormatUISchema = { + type: "VerticalLayout", + elements: [ + { + type: "Control", + scope: "#/properties/dateTime", + label: "Date Time", + options: { + format: { + format: "MM/DD", + }, + }, + }, + ], +} satisfies UISchema