Skip to content

Commit

Permalink
feature(DateTimeControl): add control, tests and stories
Browse files Browse the repository at this point in the history
  • Loading branch information
ashmortar committed Oct 21, 2024
1 parent 2c8373e commit 5db15e4
Show file tree
Hide file tree
Showing 7 changed files with 313 additions and 1 deletion.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
"@jsonforms/core": "^3.3.0",
"@jsonforms/react": "^3.3.0",
"antd": "^5.14.0",
"dayjs": "^1",
"react": "^17 || ^18"
},
"devDependencies": {
Expand Down
4 changes: 3 additions & 1 deletion pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

80 changes: 80 additions & 0 deletions src/controls/DateTimeControl.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
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(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(TITLE)
expect(input).toHaveValue(EXAMPLE_DATESTRING)
})

test("updates jsonforms data as expected", async () => {
let data: Record<string, unknown> = {}
render({
schema: dateTimeSchema,
data,
onChange: (result) => {
data = result.data as Record<string, unknown>
},
})
const input = await screen.findByLabelText(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()
})
})
75 changes: 75 additions & 0 deletions src/controls/DateTimeControl.tsx
Original file line number Diff line number Diff line change
@@ -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<JSFControlProps, "uischema"> & {
uischema: ControlUISchema<unknown> | 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

Check warning on line 24 in src/controls/DateTimeControl.tsx

View check run for this annotation

Codecov / codecov/patch

src/controls/DateTimeControl.tsx#L24

Added line #L24 was not covered by tests
}
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 (
<Form.Item
label={label}
id={id}
name={path}
required={required}
validateTrigger={["onBlur"]}
rules={rules}
initialValue={initialValue}
{...formItemProps}
>
<DatePicker
format={DEFAULT_PROPS.format}
onChange={onChange}
{...options}
/>
</Form.Item>
)
}

export const DateTimeRenderer = withJsonFormsControlProps(memo(DateTimeControl))
6 changes: 6 additions & 0 deletions src/renderer-registry-entries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
isOneOfControl,
isAnyOfControl,
isEnumControl,
isDateTimeControl,
} from "@jsonforms/core"
import { withJsonFormsCellProps } from "@jsonforms/react"

Expand All @@ -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[] = [
Expand Down Expand Up @@ -113,6 +115,10 @@ export const rendererRegistryEntries: JsonFormsRendererRegistryEntry[] = [
tester: rankWith(30, isPrimitiveArrayControl),
renderer: PrimitiveArrayRenderer,
},
{
tester: rankWith(3, isDateTimeControl),
renderer: DateTimeRenderer,
},
]

export const cellRegistryEntries: JsonFormsCellRendererRegistryEntry[] = [
Expand Down
65 changes: 65 additions & 0 deletions src/stories/controls/DateTimeControl.stories.tsx
Original file line number Diff line number Diff line change
@@ -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<typeof StorybookAntDJsonForm> = {
title: "Control/DateTime",
component: StorybookAntDJsonForm,
tags: ["autodocs"],
args: {
jsonSchema: dateTimeSchema,
uiSchema: dateTimeUISchema,
},
argTypes: {},
}

export default meta

type Story = StoryObj<typeof StorybookAntDJsonForm>

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,
},
}
83 changes: 83 additions & 0 deletions src/testSchemas/dateTimeSchema.ts
Original file line number Diff line number Diff line change
@@ -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<typeof dateTimeSchema>

export const dateTimeShowTimeUISchema = {
type: "VerticalLayout",
elements: [
{
type: "Control",
scope: "#/properties/dateTime",
label: "Date Time",
options: {
showTime: true,
},
},
],
} satisfies UISchema<typeof dateTimeSchema>

export const dateTimeShowMillisecondHideNowUISchema = {
type: "VerticalLayout",
elements: [
{
type: "Control",
scope: "#/properties/dateTime",
label: "Date Time",
options: {
showTime: true,
showMillisecond: true,
showNow: false,
},
},
],
} satisfies UISchema<typeof dateTimeSchema>

export const dateTimeOverrideDefaultFormatUISchema = {
type: "VerticalLayout",
elements: [
{
type: "Control",
scope: "#/properties/dateTime",
label: "Date Time",
options: {
format: {
format: "MM/DD",
},
},
},
],
} satisfies UISchema<typeof dateTimeSchema>

0 comments on commit 5db15e4

Please sign in to comment.