Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/jsonforms-renderers/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ This package provides a collection of JSON Forms controls and layout renderers t

### Controls

- **[DateControl](src/renderers/DateControl.tsx)**: Formats `date`, `time`, and `date-time` inputs. Combines native inputs that split space equally in `date-time` combination mode and expand to `100%` container width otherwise.
- **[DateControl](src/renderers/DateControl.tsx)**: Formats `date`, `time`, and `date-time` inputs. Combines native inputs that split space equally in `date-time` combination mode and expand to `100%` container width otherwise. Supports `x-notLaterThan` on `date` / `date-time` (inline error under `ValidateAndShow`). Export `collectNotLaterThanErrors` to gate submit; if you also pass it as `additionalErrors`, DateControl dedupes so the message shows once. The collector walks plain `properties` / `items` only (no `$ref` / combinators).
- **[TextControl](src/renderers/TextControl.tsx)**: Handles standard string schemas with optional placeholders.
- **[NumberControl](src/renderers/NumberControl.tsx)**: Handles numeric and integer inputs.
- **[BooleanControl](src/renderers/BooleanControl.tsx)**: Toggle switch control for boolean values.
Expand Down
1 change: 1 addition & 0 deletions packages/jsonforms-renderers/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ import './index.css'
export * from './contexts/UploadContext'
export * from './contexts/SearchServiceContext'
export * from './renderers'
export * from './utils/notLaterThan'
90 changes: 90 additions & 0 deletions packages/jsonforms-renderers/src/renderers/DateControl.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// @vitest-environment jsdom
import { afterEach, describe, expect, it } from 'vitest'
import { useState } from 'react'
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
import { JsonForms } from '@jsonforms/react'
import { Theme } from '@radix-ui/themes'
import type { JsonSchema, UISchemaElement } from '@jsonforms/core'
import { radixRenderers } from './index'
import { collectNotLaterThanErrors, type NotLaterThanError } from '../utils/notLaterThan'

afterEach(() => {
cleanup()
})

const schema = {
type: 'object',
properties: {
date_of_containerization: {
type: 'string',
format: 'date',
title: 'Date of Containerization',
'x-notLaterThan': 'date_of_sailing',
},
date_of_sailing: { type: 'string', format: 'date', title: 'Date of Sailing' },
},
} as unknown as JsonSchema

const uischema = {
type: 'VerticalLayout',
elements: [
{ type: 'Control', scope: '#/properties/date_of_containerization' },
{ type: 'Control', scope: '#/properties/date_of_sailing' },
],
} as UISchemaElement

function renderForm(
seed: Record<string, unknown>,
additionalErrors?: NotLaterThanError[],
validationMode: 'ValidateAndShow' | 'ValidateAndHide' | 'NoValidation' = 'ValidateAndShow',
) {
function Harness() {
const [data, setData] = useState(seed)
return (
<Theme>
<JsonForms
schema={schema}
uischema={uischema}
data={data}
renderers={radixRenderers}
additionalErrors={additionalErrors}
validationMode={validationMode}
onChange={({ data: next }) => setData(next as Record<string, unknown>)}
/>
</Theme>
)
}
return render(<Harness />)
}

describe('DateControl x-notLaterThan', () => {
it('shows the order error and clears it when the sibling moves later', async () => {
renderForm({ date_of_containerization: '2026-06-20', date_of_sailing: '2026-06-12' })

expect(await screen.findByText('Date of Containerization cannot be later than Date of Sailing')).toBeTruthy()

const sailing = document.getElementById('date_of_sailing') as HTMLInputElement
fireEvent.change(sailing, { target: { value: '2026-06-25' } })

await waitFor(() => {
expect(screen.queryByText('Date of Containerization cannot be later than Date of Sailing')).toBeNull()
})
})

it('shows the order error only once when additionalErrors is also passed', async () => {
const additionalErrors = collectNotLaterThanErrors(schema, {
date_of_containerization: '2026-06-20',
date_of_sailing: '2026-06-12',
})

renderForm({ date_of_containerization: '2026-06-20', date_of_sailing: '2026-06-12' }, additionalErrors)

const matches = await screen.findAllByText('Date of Containerization cannot be later than Date of Sailing')
expect(matches).toHaveLength(1)
})

it('hides the order error when validationMode is ValidateAndHide', () => {
renderForm({ date_of_containerization: '2026-06-20', date_of_sailing: '2026-06-12' }, undefined, 'ValidateAndHide')
expect(screen.queryByText('Date of Containerization cannot be later than Date of Sailing')).toBeNull()
})
})
49 changes: 43 additions & 6 deletions packages/jsonforms-renderers/src/renderers/DateControl.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,13 @@ import {
type RankedTester,
rankWith,
} from '@jsonforms/core'
import { withJsonFormsControlProps } from '@jsonforms/react'
import { useJsonForms, withJsonFormsControlProps } from '@jsonforms/react'
import { TextField, Text, Flex, Box } from '@radix-ui/themes'
import { useEffect, type ReactNode } from 'react'
import dayjs from 'dayjs'

import { getErrorMessage } from '../utils/error'
import { notLaterThanControlError } from '../utils/notLaterThan'

// dayjs().format() defaults to RFC 3339 (e.g. 2026-06-05T12:30:00+05:30) with
// seconds + local offset, which ajv's strict "date-time" format check requires.
Expand Down Expand Up @@ -47,7 +48,9 @@ const FieldShell = ({ path, label, required, errors, description, children }: Sh
)
}

export const DateControl = ({
type ViewProps = ControlProps & { orderError?: string }

const DateControlView = ({
data,
handleChange,
path,
Expand All @@ -57,7 +60,8 @@ export const DateControl = ({
schema,
enabled,
visible = true,
}: ControlProps) => {
orderError,
}: ViewProps) => {
useEffect(() => {
if (visible === false) {
handleChange(path, undefined)
Expand All @@ -67,16 +71,29 @@ export const DateControl = ({
if (visible === false) {
return null
}
const isValid = errors.length === 0

// When the host also passes collectNotLaterThanErrors as additionalErrors,
// JSON Forms merges that message into `errors` — skip the duplicate.
const orderAlreadyShown =
Boolean(orderError) &&
errors
.split('\n')
.map((line) => line.trim())
.includes(orderError!)
const displayErrors = [errors, orderAlreadyShown ? undefined : orderError]
.filter((part) => typeof part === 'string' && part.length > 0)
.join('\n')
const isValid = displayErrors.length === 0
const value: string = typeof data === 'string' ? data : ''

const shell = (children: ReactNode) => (
<FieldShell path={path} label={label} required={required} errors={errors} description={schema.description}>
<FieldShell path={path} label={label} required={required} errors={displayErrors} description={schema.description}>
{children}
</FieldShell>
)

// Time-only: native picker is plenty.
// Time-only: native picker is plenty. x-notLaterThan does not apply to time
// (notLaterThanControlError guards on format === date | date-time).
if (schema.format === 'time') {
return shell(
<TextField.Root
Expand Down Expand Up @@ -130,6 +147,26 @@ export const DateControl = ({
)
}

/** Subscribes to form context only when this field declares x-notLaterThan. */
const DateControlWithNotLaterThan = (props: ControlProps) => {
const ctx = useJsonForms()
// Match JSON Forms: ValidateAndHide / NoValidation hide other control errors.
const validationMode = ctx.core?.validationMode ?? 'ValidateAndShow'
const orderError =
validationMode === 'ValidateAndShow'
? notLaterThanControlError(ctx.core?.data, props.path, props.data, props.schema, props.label, ctx.core?.schema)
: undefined
return <DateControlView {...props} orderError={orderError} />
}

export const DateControl = (props: ControlProps) => {
const limitField = (props.schema as { 'x-notLaterThan'?: unknown })['x-notLaterThan']
if (typeof limitField === 'string') {
return <DateControlWithNotLaterThan {...props} />
}
return <DateControlView {...props} />
}

export const DateControlTester: RankedTester = rankWith(2, or(isDateControl, isDateTimeControl, isTimeControl))

const JsonFormsDateControl = withJsonFormsControlProps(DateControl)
Expand Down
Loading
Loading