Skip to content

feat(next): Create x-jsf-ui alias to x-jsf-presentation #190

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
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
6 changes: 3 additions & 3 deletions next/src/errors/messages.ts
Original file line number Diff line number Diff line change
@@ -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(
Expand All @@ -10,13 +10,13 @@ export function getErrorMessage(
validation: SchemaValidationErrorType,
customErrorMessage?: string,
): string {
const presentation = schema['x-jsf-presentation']
const presentation = getUiPresentation(schema)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@lukad @dragidavid @antoniocapelo @eng-almeida from now on, let's remember to use this helper rather than typing directly x-jsf-presentation. I wish we had a linter against it... maybe one day ;)

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'
Expand Down
11 changes: 6 additions & 5 deletions next/src/field/schema.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -119,7 +119,7 @@ function convertToOptions(nodeOptions: JsfSchema[]): Array<FieldOption> {
.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: {
Expand All @@ -137,7 +137,7 @@ function convertToOptions(nodeOptions: JsfSchema[]): Array<FieldOption> {
}

// 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 }
})
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion next/src/form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions next/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string>
'x-jsf-logic'?: JsonLogicSchema
Expand Down
5 changes: 5 additions & 0 deletions next/src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Field } from './field/type'
import type { JsfPresentation, JsfSchema } from './types'

type DiskSizeUnit = 'Bytes' | 'KB' | 'MB'

Expand Down Expand Up @@ -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']
}
9 changes: 6 additions & 3 deletions next/src/validation/custom/date.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

help: tell me there's a better way :x


if (minDate && !validateMinDate(value, minDate)) {
errors.push({ path, validation: 'minDate', schema, value })
Expand Down
3 changes: 2 additions & 1 deletion next/src/validation/file.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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'
Expand Down
3 changes: 2 additions & 1 deletion next/src/validation/schema.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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[] = []
Expand Down
31 changes: 31 additions & 0 deletions next/test/fields.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down