diff --git a/next/src/errors/messages.ts b/next/src/errors/messages.ts index 0a3f1abb6..ccd31cecb 100644 --- a/next/src/errors/messages.ts +++ b/next/src/errors/messages.ts @@ -1,7 +1,7 @@ import type { SchemaValidationErrorType } from '.' import type { JsfSchemaType, NonBooleanJsfSchema, SchemaValue } from '../types' import { randexp } from 'randexp' -import { convertKBToMB } from '../utils' +import { convertKBToMB, getUiPresentation } from '../utils' import { DATE_FORMAT } from '../validation/custom/date' export function getErrorMessage( @@ -10,13 +10,13 @@ export function getErrorMessage( validation: SchemaValidationErrorType, customErrorMessage?: string, ): string { - const presentation = schema['x-jsf-presentation'] + const presentation = getUiPresentation(schema) switch (validation) { // Core validation case 'type': return getTypeErrorMessage(schema.type) case 'required': - if (schema['x-jsf-presentation']?.inputType === 'checkbox') { + if (presentation?.inputType === 'checkbox') { return 'Please acknowledge this field' } return 'Required field' diff --git a/next/src/field/schema.ts b/next/src/field/schema.ts index 27ae20fb4..0f38002f3 100644 --- a/next/src/field/schema.ts +++ b/next/src/field/schema.ts @@ -1,7 +1,7 @@ import type { JsfObjectSchema, JsfSchema, JsfSchemaType, NonBooleanJsfSchema } from '../types' import type { Field, FieldOption, FieldType } from './type' import { setCustomOrder } from '../custom/order' - +import { getUiPresentation } from '../utils' /** * Add checkbox attributes to a field * @param inputType - The input type of the field @@ -72,7 +72,7 @@ function getInputTypeFromSchema(type: JsfSchemaType, schema: NonBooleanJsfSchema * @throws If the input type is missing and strictInputType is true with the exception of the root field */ export function getInputType(type: JsfSchemaType, name: string, schema: NonBooleanJsfSchema, strictInputType?: boolean): FieldType { - const presentation = schema['x-jsf-presentation'] + const presentation = getUiPresentation(schema) if (presentation?.inputType) { return presentation.inputType as FieldType } @@ -119,7 +119,7 @@ function convertToOptions(nodeOptions: JsfSchema[]): Array { .map((schemaOption) => { const title = schemaOption.title const value = schemaOption.const - const presentation = schemaOption['x-jsf-presentation'] + const presentation = getUiPresentation(schemaOption) const meta = presentation?.meta const result: { @@ -137,7 +137,7 @@ function convertToOptions(nodeOptions: JsfSchema[]): Array { } // Add other properties, without known ones we already handled above - const { title: _, const: __, 'x-jsf-presentation': ___, ...rest } = schemaOption + const { title: _, const: __, 'x-jsf-presentation': ___, 'x-jsf-ui': ____, ...rest } = schemaOption return { ...result, ...rest } }) @@ -257,6 +257,7 @@ const excludedSchemaProps = [ 'type', // Handled separately 'x-jsf-errorMessage', // Handled separately 'x-jsf-presentation', // Handled separately + 'x-jsf-ui', // Handled separately 'oneOf', // Transformed to 'options' 'anyOf', // Transformed to 'options' 'properties', // Handled separately @@ -290,7 +291,7 @@ export function buildFieldSchema( return null } - const presentation = schema['x-jsf-presentation'] || {} + const presentation = getUiPresentation(schema) || {} const errorMessage = schema['x-jsf-errorMessage'] // Get input type from presentation or fallback to schema type diff --git a/next/src/form.ts b/next/src/form.ts index d65ea6a3a..02561e1f9 100644 --- a/next/src/form.ts +++ b/next/src/form.ts @@ -224,7 +224,7 @@ export interface CreateHeadlessFormOptions { */ validationOptions?: ValidationOptions /** - * When enabled, ['x-jsf-presentation'].inputType is required for all properties. + * When enabled, ['x-jsf-presentation'|'x-jsf-ui'].inputType is required for all properties. * @default false */ strictInputType?: boolean diff --git a/next/src/types.ts b/next/src/types.ts index a08f87fe1..542540791 100644 --- a/next/src/types.ts +++ b/next/src/types.ts @@ -70,6 +70,8 @@ export type JsfSchema = JSONSchema & { 'x-jsf-order'?: string[] // Defines the presentation of the field in the form. 'x-jsf-presentation'?: JsfPresentation + // Alias to x-jsf-presentation - easier to type + 'x-jsf-ui'?: JsfPresentation // Defines the error message of the field in the form. 'x-jsf-errorMessage'?: Record 'x-jsf-logic'?: JsonLogicSchema diff --git a/next/src/utils.ts b/next/src/utils.ts index 2d78d6e5e..daba530d8 100644 --- a/next/src/utils.ts +++ b/next/src/utils.ts @@ -1,4 +1,5 @@ import type { Field } from './field/type' +import type { JsfPresentation, JsfSchema } from './types' type DiskSizeUnit = 'Bytes' | 'KB' | 'MB' @@ -51,3 +52,7 @@ export function convertKBToMB(kb: number): number { const mb = kb / 1024 // KB to MB return Number.parseFloat(mb.toFixed(2)) // Keep 2 decimal places } + +export function getUiPresentation(schema: JsfSchema): JsfPresentation | undefined { + return schema['x-jsf-presentation'] || schema['x-jsf-ui'] +} diff --git a/next/src/validation/custom/date.ts b/next/src/validation/custom/date.ts index f81cf1d74..80dbeb1ae 100644 --- a/next/src/validation/custom/date.ts +++ b/next/src/validation/custom/date.ts @@ -1,6 +1,7 @@ import type { ValidationError, ValidationErrorPath } from '../../errors' -import type { NonBooleanJsfSchema, SchemaValue } from '../../types' +import type { JsfPresentation, NonBooleanJsfSchema, SchemaValue } from '../../types' import type { ValidationOptions } from '../schema' +import { getUiPresentation } from '../../utils' export const DATE_FORMAT = 'yyyy-MM-dd' type DateComparisonResult = 'LESSER' | 'GREATER' | 'EQUAL' @@ -71,11 +72,13 @@ export function validateDate( const isEmpty = isEmptyString || isUndefined const errors: ValidationError[] = [] - if (!isString || isEmpty || schema['x-jsf-presentation'] === undefined) { + if (!isString || isEmpty || getUiPresentation(schema) === undefined) { return errors } - const { minDate, maxDate } = schema['x-jsf-presentation'] + // TODO: Why do we need to cast to JsfPresentation, + // even though we know it's not undefined (from the if above)? + const { minDate, maxDate } = getUiPresentation(schema) as JsfPresentation if (minDate && !validateMinDate(value, minDate)) { errors.push({ path, validation: 'minDate', schema, value }) diff --git a/next/src/validation/file.ts b/next/src/validation/file.ts index f3aff0a26..3460ad94c 100644 --- a/next/src/validation/file.ts +++ b/next/src/validation/file.ts @@ -1,5 +1,6 @@ import type { ValidationError, ValidationErrorPath } from '../errors' import type { NonBooleanJsfSchema, SchemaValue } from '../types' +import { getUiPresentation } from '../utils' import { isObjectValue } from './util' // Represents a file-like object, either a browser native File or a plain object. @@ -25,7 +26,7 @@ export function validateFile( ): ValidationError[] { // Early exit conditions // 1. Check if schema indicates a potential file input - const presentation = schema['x-jsf-presentation'] + const presentation = getUiPresentation(schema) const isExplicitFileInput = presentation?.inputType === 'file' const hasFileKeywords = typeof presentation?.maxFileSize === 'number' || typeof presentation?.accept === 'string' diff --git a/next/src/validation/schema.ts b/next/src/validation/schema.ts index 5c9cfc7d9..eb4acc7ec 100644 --- a/next/src/validation/schema.ts +++ b/next/src/validation/schema.ts @@ -1,5 +1,6 @@ import type { ValidationError, ValidationErrorPath } from '../errors' import type { JsfSchema, JsfSchemaType, JsonLogicContext, JsonLogicRootSchema, JsonLogicRules, SchemaValue } from '../types' +import { getUiPresentation } from '../utils' import { validateArray } from './array' import { validateAllOf, validateAnyOf, validateNot, validateOneOf } from './composition' import { validateCondition } from './conditions' @@ -199,7 +200,7 @@ export function validateSchema( } // Check if it is a file input (needed early for null check) - const presentation = schema['x-jsf-presentation'] + const presentation = getUiPresentation(schema) const isExplicitFileInput = presentation?.inputType === 'file' let typeValidationErrors: ValidationError[] = [] diff --git a/next/test/fields.test.ts b/next/test/fields.test.ts index 2c0634326..8a8798cfc 100644 --- a/next/test/fields.test.ts +++ b/next/test/fields.test.ts @@ -150,6 +150,37 @@ describe('fields', () => { ]) }) + it('should handle custom x-jsf-presentation properties - alias x-jsf-ui', () => { + const schema: JsfSchema = { + type: 'object', + properties: { + file: { + 'type': 'string', + 'title': 'Some field', + 'x-jsf-ui': { + inputType: 'text', + foo: 123, + }, + }, + }, + } + + const fields = buildFieldSchema(schema, 'root', true)!.fields! + + expect(fields).toEqual([ + { + inputType: 'text', + type: 'text', + jsonType: 'string', + isVisible: true, + name: 'file', + label: 'Some field', + required: false, + foo: 123, + }, + ]) + }) + it('should handle boolean schema', () => { const schema = { type: 'object',