Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
28cc490
initial group-array field support
lukad May 2, 2025
305d7cf
Merge branch 'main' into devxp-2540-group-array-fields
dragidavid May 2, 2025
e14d962
wip - generate formErrors in expected format for group-arrays, nested…
lukad May 2, 2025
f8aae5f
Merge branch 'main' into devxp-2540-group-array-fields
lukad May 5, 2025
87e4ba2
enable a previously skipped test that was waiting for group-arrays
lukad May 5, 2025
9c75ab4
propagate schema props to fields of group-arrays the same way as is d…
lukad May 7, 2025
611b334
Merge branch 'main' into devxp-2540-group-array-fields
lukad May 7, 2025
ca1b403
allow non-object array items
lukad May 8, 2025
541f8ee
remove commented code
lukad May 8, 2025
512f85f
remove propagatedSchemaProps
lukad May 8, 2025
fcea89c
fix inputType issues
lukad May 9, 2025
ff5222b
set multiple: true for checkbox groups
lukad May 9, 2025
0776b29
add JSDoc param docs to getInputType
lukad May 9, 2025
9fe1fdd
remove leftover comment
lukad May 9, 2025
7d25189
change setCustomOrder signature to individual args instead of an object
lukad May 12, 2025
40cfa56
remove unused value arg from validationErrorsToFormErrors
lukad May 12, 2025
ff262ed
Update next/test/fields/array.test.ts
lukad May 12, 2025
f66f5a0
refactor array tests and remove `type`
lukad May 12, 2025
2de92e9
simplify null/undefined check in validationErrorsToFormErrors
lukad May 12, 2025
8869fd5
wrap validation error handling tests into a describe block
lukad May 12, 2025
b7178d3
add `type` back
lukad May 12, 2025
f77aa0c
fix nested formErrors
lukad May 12, 2025
065a7dc
improve naming in required arrays test case
lukad May 13, 2025
f266b24
add stub tests for group-array mutations
lukad May 13, 2025
862aa20
fix group-array fields oredering
lukad May 13, 2025
f3280b5
simplify order test
lukad May 13, 2025
37f168f
change `false schema` error message to "Not allowed"
lukad May 13, 2025
34edd75
delete unused imports
lukad May 13, 2025
baba933
Update next/test/fields/array.test.ts
lukad May 13, 2025
e59d160
fix: group array test schema
dragidavid May 13, 2025
cbafae9
change 'valid' validation error type to 'forbidden'
lukad May 13, 2025
9f9124a
fix last 'valid' occurance
lukad May 13, 2025
285b40b
Release 0.11.14-dev.20250514093749
dragidavid May 14, 2025
91b07b3
chore: revert v0 version change
dragidavid May 14, 2025
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
20 changes: 5 additions & 15 deletions next/src/custom/order.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,14 @@
import type { Field } from '../field/type'
import type { JsfSchema } from '../types'

function sort(params: {
fields: Field[]
order: string[]
}): Field[] {
const { fields: prevFields, order } = params

function sort(fields: Field[], order: string[]): Field[] {
// Map from field name to expected index
const indexMap: Record<string, number | undefined> = {}
order.forEach((key, index) => {
indexMap[key] = index
})

const nextFields = prevFields.sort((a, b) => {
const nextFields = fields.sort((a, b) => {
// Compare by index
const indexA = indexMap[a.name] ?? Infinity
const indexB = indexMap[b.name] ?? Infinity
Expand All @@ -24,7 +19,7 @@ function sort(params: {
return indexA - indexB

// If not specified, maintain original relative order
return prevFields.indexOf(a) - prevFields.indexOf(b)
return fields.indexOf(a) - fields.indexOf(b)
})

return nextFields
Expand All @@ -33,12 +28,7 @@ function sort(params: {
/**
* Sort fields by schema's `x-jsf-order`
*/
export function setCustomOrder(params: {
schema: JsfSchema
fields: Field[]
}): Field[] {
const { schema, fields } = params

export function setCustomOrder(schema: JsfSchema, fields: Field[]): Field[] {
// TypeScript does not yield if we remove this check,
// but it's only because our typing is likely not right.
// See internal discussion:
Expand All @@ -47,7 +37,7 @@ export function setCustomOrder(params: {
throw new Error('Schema must be an object')

if (schema['x-jsf-order'] !== undefined)
return sort({ fields, order: schema['x-jsf-order'] })
return sort(fields, schema['x-jsf-order'])

return fields
}
2 changes: 1 addition & 1 deletion next/src/errors/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export type SchemaValidationErrorType =
*/
| 'type'
| 'required'
| 'valid'
| 'forbidden'
| 'const'
| 'enum'
/**
Expand Down
16 changes: 8 additions & 8 deletions next/src/errors/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ export function getErrorMessage(
return 'Please acknowledge this field'
}
return 'Required field'
case 'valid':
return 'Always fails'
case 'forbidden':
return 'Not allowed'
case 'const':
return `The only accepted value is ${JSON.stringify(schema.const)}.`
case 'enum':
Expand Down Expand Up @@ -81,17 +81,17 @@ export function getErrorMessage(
}
// Arrays
case 'minItems':
throw new Error('Array support is not implemented yet')
return `Must have at least ${schema.minItems} items`
case 'maxItems':
throw new Error('Array support is not implemented yet')
return `Must have at most ${schema.maxItems} items`
case 'uniqueItems':
throw new Error('Array support is not implemented yet')
return 'Items must be unique'
case 'contains':
throw new Error('Array support is not implemented yet')
throw new Error('"contains" is not implemented yet')
case 'minContains':
throw new Error('Array support is not implemented yet')
throw new Error('"minContains" is not implemented yet')
case 'maxContains':
throw new Error('Array support is not implemented yet')
throw new Error('"maxContains" is not implemented yet')
case 'json-logic':
return customErrorMessage || 'The value is not valid'
}
Expand Down
50 changes: 0 additions & 50 deletions next/src/field/object.ts

This file was deleted.

137 changes: 117 additions & 20 deletions next/src/field/schema.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { JsfObjectSchema, JsfSchema, JsfSchemaType, NonBooleanJsfSchema } from '../types'
import type { Field, FieldOption, FieldType } from './type'
import { buildFieldObject } from './object'
import { setCustomOrder } from '../custom/order'

/**
* Add checkbox attributes to a field
Expand Down Expand Up @@ -64,16 +64,20 @@ function getInputTypeFromSchema(type: JsfSchemaType, schema: NonBooleanJsfSchema

/**
* Get the input type for a field
* @param type - The schema type
* @param name - The name of the field
* @param schema - The non boolean schema of the field
* @param strictInputType - Whether to strictly enforce the input type
* @returns The input type for the field, based schema type. Default to 'text'
* @throws If the input type is missing and strictInputType is true with the exception of the root field
*/
function getInputType(schema: NonBooleanJsfSchema, strictInputType?: boolean): FieldType {
export function getInputType(type: JsfSchemaType, name: string, schema: NonBooleanJsfSchema, strictInputType?: boolean): FieldType {
const presentation = schema['x-jsf-presentation']
if (presentation?.inputType) {
return presentation.inputType as FieldType
}

if (strictInputType) {
if (strictInputType && name !== 'root') {
throw new Error(`Strict error: Missing inputType to field "${schema.title}".
You can fix the json schema or skip this error by calling createHeadlessForm(schema, { strictInputType: false })`)
}
Expand All @@ -94,7 +98,7 @@ You can fix the json schema or skip this error by calling createHeadlessForm(sch
}

// Get input type from schema (fallback type is "string")
return getInputTypeFromSchema(schema.type || 'string', schema)
return getInputTypeFromSchema(type || schema.type || 'string', schema)
}

/**
Expand Down Expand Up @@ -169,6 +173,82 @@ function getFieldOptions(schema: NonBooleanJsfSchema) {
return null
}

/**
* Get the fields for an object schema
* @param schema - The schema of the field
* @param strictInputType - Whether to strictly enforce the input type
* @returns The fields for the schema or an empty array if the schema does not define any properties
*/
function getObjectFields(schema: NonBooleanJsfSchema, strictInputType?: boolean): Field[] | null {
const fields: Field[] = []

for (const key in schema.properties) {
const isRequired = schema.required?.includes(key) || false
const field = buildFieldSchema(schema.properties[key], key, isRequired, strictInputType)
if (field) {
fields.push(field)
}
}

const orderedFields = setCustomOrder(schema, fields)

return orderedFields
}

/**
* Get the fields for an array schema
* @param schema - The schema of the field
* @param strictInputType - Whether to strictly enforce the input type
* @returns The fields for the schema or an empty array if the schema does not define any items
*/
function getArrayFields(schema: NonBooleanJsfSchema, strictInputType?: boolean): Field[] {
const fields: Field[] = []

if (typeof schema.items !== 'object' || schema.items === null) {
return []
}

if (schema.items?.type === 'object') {
const objectSchema = schema.items as JsfObjectSchema

for (const key in objectSchema.properties) {
const isFieldRequired = objectSchema.required?.includes(key) || false
const field = buildFieldSchema(objectSchema.properties[key], key, isFieldRequired, strictInputType)
if (field) {
field.nameKey = key
fields.push(field)
}
}
}
else {
const field = buildFieldSchema(schema.items, 'item', false, strictInputType)
if (field) {
fields.push(field)
}
}

const orderedFields = setCustomOrder(schema.items, fields)

return orderedFields
}

/**
* Get the fields for a schema from either `items` or `properties`
* @param schema - The schema of the field
* @param strictInputType - Whether to strictly enforce the input type
* @returns The fields for the schema
*/
function getFields(schema: NonBooleanJsfSchema, strictInputType?: boolean): Field[] | null {
if (typeof schema.properties === 'object' && schema.properties !== null) {
return getObjectFields(schema, strictInputType)
}
else if (typeof schema.items === 'object' && schema.items !== null) {
return getArrayFields(schema, strictInputType)
}

return null
}

/**
* List of schema properties that should be excluded from the final field or handled specially
*/
Expand All @@ -179,7 +259,7 @@ const excludedSchemaProps = [
'x-jsf-presentation', // Handled separately
'oneOf', // Transformed to 'options'
'anyOf', // Transformed to 'options'
'items', // Handled specially for arrays
'properties', // Handled separately
]

/**
Expand All @@ -190,38 +270,43 @@ export function buildFieldSchema(
name: string,
required: boolean = false,
strictInputType: boolean = false,
type: JsfSchemaType = undefined,
): Field | null {
if (typeof schema === 'boolean') {
return null
}

if (schema.type === 'object') {
const objectSchema: JsfObjectSchema = { ...schema, type: 'object' }
return buildFieldObject(objectSchema, name, required)
// If schema is boolean false, return a field with isVisible=false
if (schema === false) {
const inputType = getInputType(type, name, schema, strictInputType)
return {
type: inputType,
name,
inputType,
jsonType: 'boolean',
required,
isVisible: false,
}
}

if (schema.type === 'array') {
throw new TypeError('Array type is not yet supported')
// If schema is any other boolean (true), just return null
if (typeof schema === 'boolean') {
return null
}

const presentation = schema['x-jsf-presentation'] || {}
const errorMessage = schema['x-jsf-errorMessage']

// Get input type from presentation or fallback to schema type
const inputType = getInputType(schema, strictInputType)
const inputType = getInputType(type, name, schema, strictInputType)

// Build field with all schema properties by default, excluding ones that need special handling
const field: Field = {
// Spread all schema properties except excluded ones
...Object.entries(schema)
.filter(([key]) => !excludedSchemaProps.includes(key))
.reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}),

// Add required field properties
type: inputType,
name,
inputType,
jsonType: schema.type,
type: inputType,
jsonType: type || schema.type,
required,
isVisible: true,
...(errorMessage && { errorMessage }),
Expand All @@ -239,16 +324,28 @@ export function buildFieldSchema(
if (Object.keys(presentation).length > 0) {
Object.entries(presentation).forEach(([key, value]) => {
// inputType is already handled above
if (key !== 'inputType') {
field[key] = value
if (key === 'inputType') {
return
}

field[key] = value
})
}

// Handle options
const options = getFieldOptions(schema)
if (options) {
field.options = options
if (schema.type === 'array') {
field.multiple = true
}
}
else {
// We did not find options, so we might have an array to generate fields from
const fields = getFields(schema, strictInputType)
if (fields) {
field.fields = fields
}
}

return field
Expand Down
Loading