Skip to content
Draft
Show file tree
Hide file tree
Changes from 2 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
8 changes: 4 additions & 4 deletions next/src/field/object.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { JsfObjectSchema } from '../types'
import type { Field } from './type'
import type { Field, FieldFile } from './type'
import { setCustomOrder } from '../custom/order'
import { buildFieldSchema } from './schema'

Expand All @@ -23,7 +23,7 @@ export function buildFieldObject(schema: JsfObjectSchema, name: string, required

const orderedFields = setCustomOrder({ fields, schema })

const field: Field = {
const field = {
...schema['x-jsf-presentation'],
type: schema['x-jsf-presentation']?.inputType || 'fieldset',
inputType: schema['x-jsf-presentation']?.inputType || 'fieldset',
Expand All @@ -32,7 +32,7 @@ export function buildFieldObject(schema: JsfObjectSchema, name: string, required
required,
fields: orderedFields,
isVisible: true,
}
} as Field

if (schema.title !== undefined) {
field.label = schema.title
Expand All @@ -43,7 +43,7 @@ export function buildFieldObject(schema: JsfObjectSchema, name: string, required
}

if (schema['x-jsf-presentation']?.accept) {
field.accept = schema['x-jsf-presentation']?.accept
(field as FieldFile).accept = schema['x-jsf-presentation']?.accept
}

return field
Expand Down
12 changes: 6 additions & 6 deletions next/src/field/schema.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { JsfObjectSchema, JsfSchema, JsfSchemaType, NonBooleanJsfSchema } from '../types'
import type { Field, FieldOption, FieldType } from './type'
import type { Field, FieldCheckbox, FieldOption, FieldType } from './type'
import { buildFieldObject } from './object'

/**
Expand All @@ -8,7 +8,7 @@ import { buildFieldObject } from './object'
* @param field - The field to add the attributes to
* @param schema - The schema of the field
*/
function addCheckboxAttributes(inputType: string, field: Field, schema: NonBooleanJsfSchema) {
function addCheckboxAttributes(inputType: string, field: FieldCheckbox, schema: NonBooleanJsfSchema) {
// The checkboxValue attribute indicates which is the valid value a checkbox can have (for example "acknowledge", or `true`)
// So, we set it to what's specified in the schema (if any)
field.checkboxValue = schema.const
Expand Down Expand Up @@ -120,7 +120,7 @@ function convertToOptions(nodeOptions: JsfSchema[]): Array<FieldOption> {

const result: {
label: string
value: unknown
value: string
[key: string]: unknown
} = {
label: title || '',
Expand Down Expand Up @@ -211,7 +211,7 @@ export function buildFieldSchema(
const inputType = getInputType(schema, strictInputType)

// Build field with all schema properties by default, excluding ones that need special handling
const field: Field = {
const field = {
// Spread all schema properties except excluded ones
...Object.entries(schema)
.filter(([key]) => !excludedSchemaProps.includes(key))
Expand All @@ -225,10 +225,10 @@ export function buildFieldSchema(
required,
isVisible: true,
...(errorMessage && { errorMessage }),
}
} as Field

if (inputType === 'checkbox') {
addCheckboxAttributes(inputType, field, schema)
addCheckboxAttributes(inputType, field as FieldCheckbox, schema)
}

if (schema.title) {
Expand Down
149 changes: 116 additions & 33 deletions next/src/field/type.ts
Original file line number Diff line number Diff line change
@@ -1,47 +1,130 @@
import type { JsfSchemaType } from '../types'
Copy link
Collaborator

@sandrina-p sandrina-p May 9, 2025

Choose a reason for hiding this comment

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

Hi @eng-almeida 👋 I'll review it this afternoon around 16:00!

Copy link
Collaborator

Choose a reason for hiding this comment

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

@eng-almeida when do you plan to finish this MR?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Hey @sandrina-p, sorry for dropping the ball here. I'll try to wrap this up next week 🤞


/**
* WIP type for UI field output that allows for all `x-jsf-presentation` properties to be splatted
* TODO/QUESTION: what are the required fields for a field? what are the things we want to deprecate, if any?
*/
export interface Field {
name: string
label?: string
description?: string
fields?: Field[]
// @deprecated in favor of inputType,
export type FieldType = 'text' | 'number' | 'select' | 'file' | 'radio' | 'group-array' | 'email' | 'date' | 'checkbox' | 'fieldset' | 'money' | 'country' | 'textarea'
Copy link
Collaborator

@sandrina-p sandrina-p May 9, 2025

Choose a reason for hiding this comment

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

todo: Here's my controversial opinion: JSF does not care about FieldType, it's whatever comes from JSON schema x-jsf-presentation.inputType. We do not provide any extra logic/validation just based on it, right? (right? 👀)

So answering @lukad question:

We don't actually have any sort of validation that checks that when for example 'x-js-presentation': { 'inputType': 'money'} is given there is also 'x-js-presentation': { 'currency': 'EUR'}. @sandrina-p, is that something we do in v0?

We don't and we shouldn't. That logic is a concern of Remote internals, not JSF as headless generator/validator. If we ever validate that, it's another layer on top of JSON-SCHEMA-FORM

Does it make sense to you both?

Copy link
Collaborator

Choose a reason for hiding this comment

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

P.S. I know, this kind affects most of your MR, as the field types should be based on the json schema type, not the inputType 😶

Copy link
Collaborator

Choose a reason for hiding this comment

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

Hi @eng-almeida , when do you plan to finish this PR? :)


interface BaseField {
type: FieldType
inputType: FieldType
name: string
label: string
required: boolean
inputType: FieldType
jsonType: JsfSchemaType
isVisible: boolean
accept?: string
errorMessage?: Record<string, string>
computedAttributes?: Record<string, unknown>
errorMessage: Record<string, string>
schema: any
isVisible: boolean
description?: string
statement?: {
title: string
inputType: 'statement'
severity: 'warning' | 'error' | 'info'
}
[key: string]: unknown
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This line hides a lot of TS errors that are hard to address without it. I'll keep investigating...

}

export interface FieldOption {
label: string
value: string
description?: string
Copy link
Collaborator

Choose a reason for hiding this comment

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

todo:

  1. The FieldOption is a mirror of oneOf, so if the oneOf has, foo inside, then the Type includes foo too.
  2. value is not necessarily a string, can be anything, number, bool, even object. It's a mirror of oneOf.const 🪩
  3. value can be optional too :p Look at this example with pattern

}

export interface FieldSelect extends BaseField {
type: 'select'
options: FieldOption[]
}

export interface FieldTextarea extends BaseField {
type: 'textarea'
maxLength?: number
minLength?: number
}

export interface FieldDate extends BaseField {
type: 'date'
format: string
minDate?: string
maxDate?: string
maxLength?: number
maxFileSize?: number
format?: string
anyOf?: unknown[]
options?: unknown[]
const?: unknown
checkboxValue?: unknown

// Allow additional properties from x-jsf-presentation (e.g. meta from oneOf/anyOf)
[key: string]: unknown
}

/**
* Field option
* @description
* Represents a key/value pair that is used to populate the options for a field.
* Will be created from the oneOf/anyOf elements in a schema.
*/
export interface FieldOption {
export interface FieldText extends BaseField {
type: 'text'
maxLength?: number
maskSecret?: number
}

export interface FieldRadio extends BaseField {
type: 'radio'
options: FieldOption[]
direction?: 'row' | 'column'
Copy link
Collaborator

@sandrina-p sandrina-p May 9, 2025

Choose a reason for hiding this comment

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

todo: Here's another example. This is Remote internal need, not a JSF concern. A few other examples down the line (currency, fileDownload, statement, etc... All of those Types can exist, but not at JSF.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

You're right 💯 ! Probably the right move is to extend these types on Remote SDK project which uses Remote JSON Schemas 👍

const?: string
}

export interface FieldNumber extends BaseField {
type: 'number'
minimum?: number
maximum?: number
}

export interface FieldMoney extends BaseField {
type: 'money'
currency: string
}

export interface FieldCheckbox extends BaseField {
type: 'checkbox'
options?: FieldOption[]
multiple?: boolean
direction?: 'row' | 'column'
checkboxValue?: string | boolean
const?: string
}

export interface FieldEmail extends BaseField {
type: 'email'
maxLength?: number
format: 'email'
}

export interface FieldFile extends BaseField {
type: 'file'
accept: string
multiple?: boolean
fileDownload: string
fileName: string
}
export interface FieldFieldSet extends BaseField {
type: 'fieldset'
valueGroupingDisabled?: boolean
visualGroupingDisabled?: boolean
variant?: 'card' | 'focused' | 'default'
fields: Field[]
}

export interface GroupArrayField extends BaseField {
type: 'group-array'
name: string
label: string
value: unknown
[key: string]: unknown
description: string
fields: () => Field[]
Copy link
Collaborator

Choose a reason for hiding this comment

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

bug: This is v0, in v1 is fields: Field[] (Once #177 is merged)) :D

addFieldText: string
}

export type FieldType = 'text' | 'number' | 'select' | 'file' | 'radio' | 'group-array' | 'email' | 'date' | 'checkbox' | 'fieldset' | 'money' | 'country' | 'textarea'
export interface FieldCountry extends BaseField {
type: 'country'
}

export type Field =
| FieldSelect
| FieldTextarea
| FieldDate
| FieldText
| FieldRadio
| FieldNumber
| FieldMoney
| FieldCheckbox
| FieldEmail
| FieldFile
| FieldFieldSet
| GroupArrayField
| FieldCountry
10 changes: 4 additions & 6 deletions next/src/form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ export interface CreateHeadlessFormOptions {
function buildFields(params: { schema: JsfObjectSchema, strictInputType?: boolean }): Field[] {
const { schema, strictInputType } = params
const fields = buildFieldObject(schema, 'root', true, strictInputType).fields || []
return fields
return fields as Field[]
}

export function createHeadlessForm(
Expand Down Expand Up @@ -262,14 +262,12 @@ function buildFieldsInPlace(fields: Field[], schema: JsfObjectSchema): void {
const newFields = buildFieldObject(schema, 'root', true).fields || []

// Push all new fields into existing array
fields.push(...newFields)
fields.push(...(newFields as Field[]))

// Recursively update any nested fields
for (const field of fields) {
// eslint-disable-next-line ts/ban-ts-comment
// @ts-expect-error
if (field.fields && schema.properties?.[field.name]?.type === 'object') {
buildFieldsInPlace(field.fields, schema.properties[field.name] as JsfObjectSchema)
if (field.fields && schema.properties?.[field.name] && typeof schema.properties[field.name] === 'object' && (schema.properties[field.name] as JsfObjectSchema).type === 'object') {
buildFieldsInPlace(field.fields as Field[], schema.properties[field.name] as JsfObjectSchema)
}
}
}
4 changes: 2 additions & 2 deletions next/src/mutations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export function mutateFields(
const field = fields.find(field => field.name === fieldName)

if (field?.fields) {
applySchemaRules(field.fields, values[fieldName], fieldSchema as JsfObjectSchema, options)
applySchemaRules(field.fields as Field[], values[fieldName], fieldSchema as JsfObjectSchema, options)
}
}
}
Expand Down Expand Up @@ -139,7 +139,7 @@ function processBranch(fields: Field[], values: SchemaValue, branch: JsfSchema,
}
// If the field has inner fields, we need to process them
else if (field?.fields) {
processBranch(field.fields, values, fieldSchema)
processBranch(field.fields as Field[], values, fieldSchema)
}
// If the field has properties being declared on this branch, we need to update the field
// with the new properties
Expand Down
2 changes: 1 addition & 1 deletion next/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export function getField(fields: Field[], name: string, ...subNames: string[]) {
if (!field?.fields) {
return undefined
}
return getField(field.fields, subNames[0], ...subNames.slice(1))
return getField(field.fields as Field[], subNames[0], ...subNames.slice(1))
}
return field
}
Expand Down
3 changes: 2 additions & 1 deletion next/test/custom/order.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { FieldFieldSet } from '../../src/field/type'
import type { JsfObjectSchema } from '../../src/types'
import { describe, expect, it } from '@jest/globals'
import { createHeadlessForm } from '../../src'
Expand Down Expand Up @@ -43,7 +44,7 @@ describe('custom order', () => {
const mainKeys = form.fields.map(field => field.name)
expect(mainKeys).toEqual(['name', 'address'])

const addressField = form.fields.find(field => field.name === 'address')
const addressField = form.fields.find(field => field.name === 'address') as FieldFieldSet
if (addressField === undefined)
throw new Error('Address field not found')

Expand Down
14 changes: 14 additions & 0 deletions next/test/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ describe('getField', () => {
label: 'Name',
required: false,
isVisible: true,
errorMessage: {},
schema: {},
},
{
name: 'address',
Expand All @@ -21,6 +23,8 @@ describe('getField', () => {
label: 'Address',
required: false,
isVisible: true,
errorMessage: {},
schema: {},
fields: [
{
name: 'street',
Expand All @@ -30,6 +34,8 @@ describe('getField', () => {
label: 'Street',
required: false,
isVisible: true,
errorMessage: {},
schema: {},
},
{
name: 'city',
Expand All @@ -39,6 +45,8 @@ describe('getField', () => {
label: 'City',
required: false,
isVisible: true,
errorMessage: {},
schema: {},
},
],
},
Expand Down Expand Up @@ -81,6 +89,8 @@ describe('getField', () => {
label: 'Level 1',
required: false,
isVisible: true,
errorMessage: {},
schema: {},
fields: [
{
name: 'level2',
Expand All @@ -90,6 +100,8 @@ describe('getField', () => {
label: 'Level 2',
required: false,
isVisible: true,
errorMessage: {},
schema: {},
fields: [
{
name: 'level3',
Expand All @@ -99,6 +111,8 @@ describe('getField', () => {
label: 'Level 3',
required: false,
isVisible: true,
errorMessage: {},
schema: {},
},
],
},
Expand Down