Skip to content

Commit 07112a7

Browse files
committed
allow non-object array items
This also simplifies field generation a bit by removing the object special case as well as the array special case which
1 parent 611b334 commit 07112a7

File tree

6 files changed

+103
-144
lines changed

6 files changed

+103
-144
lines changed

next/src/field/array.ts

Lines changed: 0 additions & 59 deletions
This file was deleted.

next/src/field/object.ts

Lines changed: 0 additions & 50 deletions
This file was deleted.

next/src/field/schema.ts

Lines changed: 73 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import type { JsfObjectSchema, JsfSchema, JsfSchemaType, NonBooleanJsfSchema } from '../types'
22
import type { Field, FieldOption, FieldType } from './type'
3-
import { buildFieldArray } from './array'
4-
import { buildFieldObject } from './object'
3+
import { setCustomOrder } from '../custom/order'
54

65
/**
76
* Add checkbox attributes to a field
@@ -49,13 +48,8 @@ function getInputTypeFromSchema(type: JsfSchemaType, schema: NonBooleanJsfSchema
4948
return 'number'
5049
case 'object':
5150
return 'fieldset'
52-
case 'array': {
53-
const { items } = schema
54-
if (items?.properties) {
55-
return 'group-array'
56-
}
57-
return 'select'
58-
}
51+
case 'array':
52+
return 'group-array'
5953
case 'boolean':
6054
return 'checkbox'
6155
default:
@@ -68,7 +62,7 @@ function getInputTypeFromSchema(type: JsfSchemaType, schema: NonBooleanJsfSchema
6862
* @param schema - The non boolean schema of the field
6963
* @returns The input type for the field, based schema type. Default to 'text'
7064
*/
71-
function getInputType(schema: NonBooleanJsfSchema, strictInputType?: boolean): FieldType {
65+
export function getInputType(schema: NonBooleanJsfSchema, strictInputType?: boolean): FieldType {
7266
const presentation = schema['x-jsf-presentation']
7367
if (presentation?.inputType) {
7468
return presentation.inputType as FieldType
@@ -170,6 +164,68 @@ function getFieldOptions(schema: NonBooleanJsfSchema) {
170164
return null
171165
}
172166

167+
function getObjectFields(schema: NonBooleanJsfSchema, strictInputType?: boolean): Field[] | null {
168+
const fields: Field[] = []
169+
170+
for (const key in schema.properties) {
171+
const isRequired = schema.required?.includes(key) || false
172+
const field = buildFieldSchema(schema.properties[key], key, isRequired, strictInputType)
173+
if (field) {
174+
fields.push(field)
175+
}
176+
}
177+
178+
const orderedFields = setCustomOrder({ fields, schema })
179+
180+
return orderedFields
181+
}
182+
183+
function getArrayFields(schema: NonBooleanJsfSchema, strictInputType?: boolean): Field[] {
184+
const fields: Field[] = []
185+
186+
if (typeof schema.items !== 'object' || schema.items === null) {
187+
return []
188+
}
189+
190+
if (schema.items?.type === 'object') {
191+
const objectSchema = schema.items as JsfObjectSchema
192+
193+
for (const key in objectSchema.properties) {
194+
const isFieldRequired = objectSchema.required?.includes(key) || false
195+
const field = buildFieldSchema(objectSchema.properties[key], key, isFieldRequired, strictInputType)
196+
if (field) {
197+
field.nameKey = key
198+
fields.push(field)
199+
}
200+
}
201+
}
202+
else {
203+
const field = buildFieldSchema(schema.items, 'item', false, strictInputType)
204+
if (field) {
205+
fields.push(field)
206+
}
207+
}
208+
209+
return fields
210+
}
211+
212+
/**
213+
* Get the fields for a schema from either `items` or `properties`
214+
* @param schema - The schema of the field
215+
* @param strictInputType - Whether to strictly enforce the input type
216+
* @returns The fields for the schema
217+
*/
218+
function getFields(schema: NonBooleanJsfSchema, strictInputType?: boolean): Field[] | null {
219+
if (typeof schema.items === 'object') {
220+
return getArrayFields(schema, strictInputType)
221+
}
222+
else if (schema.type === 'object') {
223+
return getObjectFields(schema, strictInputType)
224+
}
225+
226+
return null
227+
}
228+
173229
/**
174230
* List of schema properties that should be excluded from the final field or handled specially
175231
*/
@@ -180,7 +236,7 @@ const excludedSchemaProps = [
180236
'x-jsf-presentation', // Handled separately
181237
'oneOf', // Transformed to 'options'
182238
'anyOf', // Transformed to 'options'
183-
'items', // Handled specially for arrays
239+
'properties', // Handled separately
184240
]
185241

186242
/**
@@ -210,16 +266,6 @@ export function buildFieldSchema(
210266
return null
211267
}
212268

213-
if (schema.type === 'object') {
214-
const objectSchema: JsfObjectSchema = { ...schema, type: 'object' }
215-
return buildFieldObject(objectSchema, name, required)
216-
}
217-
218-
if (schema.type === 'array') {
219-
const arraySchema = schema as NonBooleanJsfSchema & { type: 'array' }
220-
return buildFieldArray(arraySchema, name, required, strictInputType)
221-
}
222-
223269
const presentation = schema['x-jsf-presentation'] || {}
224270
const errorMessage = schema['x-jsf-errorMessage']
225271

@@ -264,5 +310,11 @@ export function buildFieldSchema(
264310
field.options = options
265311
}
266312

313+
// Handle array fields
314+
const fields = getFields(schema, strictInputType)
315+
if (fields) {
316+
field.fields = fields
317+
}
318+
267319
return field
268320
}

next/src/form.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type { Field } from './field/type'
33
import type { JsfObjectSchema, JsfSchema, SchemaValue } from './types'
44
import type { ValidationOptions } from './validation/schema'
55
import { getErrorMessage } from './errors/messages'
6-
import { buildFieldObject } from './field/object'
6+
import { buildFieldSchema } from './field/schema'
77
import { mutateFields } from './mutations'
88
import { validateSchema } from './validation/schema'
99

@@ -233,7 +233,7 @@ export interface CreateHeadlessFormOptions {
233233

234234
function buildFields(params: { schema: JsfObjectSchema, strictInputType?: boolean }): Field[] {
235235
const { schema, strictInputType } = params
236-
const fields = buildFieldObject(schema, 'root', true, strictInputType).fields || []
236+
const fields = buildFieldSchema(schema, 'root', true, strictInputType)?.fields || []
237237
return fields
238238
}
239239

@@ -281,7 +281,7 @@ function buildFieldsInPlace(fields: Field[], schema: JsfObjectSchema): void {
281281
fields.length = 0
282282

283283
// Get new fields from schema
284-
const newFields = buildFieldObject(schema, 'root', true).fields || []
284+
const newFields = buildFieldSchema(schema, 'root', true)?.fields || []
285285

286286
// Push all new fields into existing array
287287
fields.push(...newFields)

next/src/types.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,3 +93,11 @@ export type NonBooleanJsfSchema = Exclude<JsfSchema, boolean>
9393
export type JsfObjectSchema = NonBooleanJsfSchema & {
9494
type: 'object'
9595
}
96+
97+
/**
98+
* JSON Schema Form type specifically for array schemas.
99+
* This type ensures the schema has type 'array'.
100+
*/
101+
export type JsfArraySchema = NonBooleanJsfSchema & {
102+
type: 'array'
103+
}

next/test/fields/array.test.ts

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -138,12 +138,17 @@ describe('buildFieldArray', () => {
138138
expect(nestedFields?.[0]?.name).toBe('name')
139139
})
140140

141-
it('throws an error if the items schema is not an object', () => {
142-
expect(() => buildFieldSchema({ type: 'array', items: { type: 'string' } }, 'root', true)).toThrow()
143-
expect(() => buildFieldSchema({ type: 'array', items: { type: 'number' } }, 'root', true)).toThrow()
144-
expect(() => buildFieldSchema({ type: 'array', items: { type: 'array' } }, 'root', true)).toThrow()
145-
expect(() => buildFieldSchema({ type: 'array', items: { type: 'enum' } }, 'root', true)).toThrow()
146-
expect(() => buildFieldSchema({ type: 'array', items: { type: 'boolean' } }, 'root', true)).toThrow()
141+
it('allows non-object items', () => {
142+
const groupArray = () => expect.objectContaining({
143+
inputType: 'group-array',
144+
fields: [expect.anything()],
145+
})
146+
147+
expect(buildFieldSchema({ type: 'array', items: { type: 'string' } }, 'root', true)).toEqual(groupArray())
148+
expect(buildFieldSchema({ type: 'array', items: { type: 'number' } }, 'root', true)).toEqual(groupArray())
149+
expect(buildFieldSchema({ type: 'array', items: { type: 'array' } }, 'root', true)).toEqual(groupArray())
150+
expect(buildFieldSchema({ type: 'array', items: { type: 'enum' } }, 'root', true)).toEqual(groupArray())
151+
expect(buildFieldSchema({ type: 'array', items: { type: 'boolean' } }, 'root', true)).toEqual(groupArray())
147152
})
148153

149154
it('creates correct form errors validation errors in array items', () => {
@@ -269,8 +274,6 @@ describe('buildFieldArray', () => {
269274

270275
const result = form.handleValidation(data)
271276

272-
console.warn(JSON.stringify(result, null, 2))
273-
274277
expect(result.formErrors).toEqual({
275278
departments: [
276279
{
@@ -654,7 +657,7 @@ describe('buildFieldArray', () => {
654657
it('propagates schema properties to the field', () => {
655658
const schema: JsfSchema & Record<string, unknown> = {
656659
'type': 'array',
657-
'title': 'My array',
660+
'label': 'My array',
658661
'description': 'My array description',
659662
'x-jsf-presentation': {
660663
inputType: 'group-array',
@@ -672,7 +675,7 @@ describe('buildFieldArray', () => {
672675
const field = buildFieldSchema(schema, 'myArray', true)
673676

674677
expect(field).toBeDefined()
675-
expect(field?.title).toBe('My array')
678+
expect(field?.label).toBe('My array')
676679
expect(field?.description).toBe('My array description')
677680
})
678681

@@ -701,7 +704,12 @@ describe('buildFieldArray', () => {
701704

702705
expect(field).toBeDefined()
703706
expect(field?.label).toBe('My array')
704-
expect(field?.items).toBeUndefined()
707+
expect(field?.items).toEqual({
708+
type: 'object',
709+
properties: {
710+
name: { type: 'string' },
711+
},
712+
})
705713
expect(field?.minItems).toBe(1)
706714
expect(field?.['x-jsf-errorMessage']).toBeUndefined()
707715
expect(field?.['x-jsf-presentation']).toBeUndefined()

0 commit comments

Comments
 (0)