feat: edit an existing custom type

This commit is contained in:
Florian Sylvain committed 2026-10-20 04:42:50 +02:00
1 parent 083cc9e23e
commit 8e344b2500
57 files changed
+5648 -425

No files matched your search

@@ -0,0 +1,413 @@
import { describe, expect, it } from 'vitest'
import {
baselineSignatures,
causeText,
changeText,
changeTitle,
diffFields,
flaggedChanges,
isFieldChanged,
summaryLine,
} from '@/lib/schemaBuilderDiff'
import type { ContentType } from '@/composables/useContentTypes'
import { baselinePayload, formFromType } from '@/lib/schemaBuilderLoad'
import { createField, type BuilderField } from '@/lib/schemaBuilderModel'
import { eventsFields } from './schemaFixtures'
const events: ContentType = {
id: 1,
key: 'events',
name: 'Events',
icon: null,
fields: eventsFields,
created_at: '',
updated_at: '',
}
const baseline = baselinePayload(events)
const signatures = baselineSignatures(baseline)
function loaded() {
let next = 1
return formFromType(events, () => next++).fields
}
function edit(fields: BuilderField[], key: string, patch: Partial<BuilderField>) {
return fields.map((field) => (field.key === key ? { ...field, ...patch } : field))
}
function only(fields: BuilderField[]) {
return diffFields(baseline, fields)
}
describe('change detection per saved field', () => {
it('marks nothing on the loaded fields', () => {
expect(loaded().some((field) => isFieldChanged(field, signatures))).toBe(false)
expect(only(loaded())).toEqual([])
})
it.each([
['Capacity', 'capacity', { max: '2000' }, { max: '5000' }],
['Title', 'title', { label: 'Name of event' }, { label: 'Title' }],
['Starts on', 'starts_on', { required: false }, { required: true }],
[
'Category',
'category',
{ options: ['Talk', 'Workshop', 'Meetup', 'Panel'] },
{ options: ['Talk', 'Workshop', 'Meetup'] },
],
['Online', 'online', { defaultOn: true }, { defaultOn: false }],
])(
'marks %s changed, then clears it when the value is restored',
(_, key, edited, original) => {
const changed = edit(loaded(), key, edited)
expect(
changed
.filter((field) => isFieldChanged(field, signatures))
.map((field) => field.key),
).toEqual([key])
const restored = edit(changed, key, original)
expect(restored.some((field) => isFieldChanged(field, signatures))).toBe(false)
},
)
it('does not count a reorder as a change of any field', () => {
const [first, ...rest] = loaded()
const reordered = [...rest, first!]
expect(reordered.some((field) => isFieldChanged(field, signatures))).toBe(false)
expect(only(reordered)).toEqual([])
})
it('does not count surrounding spaces in a label', () => {
expect(only(edit(loaded(), 'title', { label: ' Title ' }))).toEqual([])
})
})
describe('review rows', () => {
function row(fields: BuilderField[]) {
const [change] = only(fields)
return {
title: changeTitle(change!),
text: changeText(change!),
flag: change!.flag,
}
}
const added = (patch: Partial<BuilderField>) => [
...loaded(),
createField(100, { label: 'New', key: 'new', ...patch }),
]
it.each([
[
'adds an optional Text',
added({ label: 'Speaker', key: 'speaker' }),
'Add Speaker',
'Text, optional.',
null,
],
[
'adds a required Choice with a default',
added({
label: 'Room',
key: 'room',
type: 'select',
required: true,
options: ['Main hall', 'Studio'],
defaultValue: 'Main hall',
}),
'Add Room',
'Choice, required, default "Main hall". Existing entries get the default, so they still fit.',
null,
],
[
'adds a required Number with no default',
added({
label: 'Seats',
key: 'seats',
type: 'number',
required: true,
}),
'Add Seats',
'Number, required. Existing entries have no value for it.',
'mayNotFit',
],
[
'lowers the highest value',
edit(loaded(), 'capacity', { max: '2000' }),
'Change Capacity',
'Highest value goes from 5000 to 2000.',
'mayNotFit',
],
[
'raises the lowest value',
edit(loaded(), 'capacity', { min: '10' }),
'Change Capacity',
'Lowest value goes from 1 to 10.',
'mayNotFit',
],
[
'sets a first longest',
edit(loaded(), 'description', { max: '80' }),
'Change Description',
'Longest is now 80.',
'mayNotFit',
],
[
'turns Required on',
edit(loaded(), 'ends_on', { required: true }),
'Change Ends on',
'Now required.',
'mayNotFit',
],
[
'turns Unique on',
edit(loaded(), 'category', { unique: true }),
'Change Category',
'Values must now be unique.',
'mayNotFit',
],
[
'removes a choice',
edit(loaded(), 'category', { options: ['Talk', 'Workshop'] }),
'Change Category',
'Choice removed: "Meetup".',
'mayNotFit',
],
[
'loosens the highest value',
edit(loaded(), 'capacity', { max: '8000' }),
'Change Capacity',
'Highest value goes from 5000 to 8000.',
null,
],
[
'renames a label',
edit(loaded(), 'title', { label: 'Name' }),
'Change Title',
'Label goes from "Title" to "Name".',
null,
],
[
'adds a choice',
edit(loaded(), 'category', {
options: ['Talk', 'Workshop', 'Meetup', 'Panel'],
}),
'Change Category',
'Choice added: "Panel".',
null,
],
[
'removes a field',
loaded().filter((field) => field.key !== 'description'),
'Remove Description',
'Entries stop returning its values.',
'removesData',
],
])('%s', (_, fields, title, text, flag) => {
expect(row(fields)).toEqual({ title, text, flag })
})
it('describes a default, an unset bound and a reordered choice list', () => {
expect(row(edit(loaded(), 'capacity', { defaultValue: '20' })).text).toBe(
'Default value is now "20".',
)
expect(row(edit(loaded(), 'capacity', { max: '' })).text).toBe(
'Highest value is no longer set.',
)
expect(
row(
edit(loaded(), 'category', {
options: ['Meetup', 'Talk', 'Workshop'],
}),
).text,
).toBe('Choices were reordered.')
expect(row(edit(loaded(), 'online', { defaultOn: true })).text).toBe(
'On by default is now on.',
)
})
it.each([
[
'loosens the lowest value',
'capacity',
{ min: '0' },
'Lowest value goes from 1 to 0.',
null,
],
[
'sets a first lowest value',
'description',
{ min: '2' },
'Shortest is now 2.',
'mayNotFit',
],
[
'unsets the lowest value',
'capacity',
{ min: '' },
'Lowest value is no longer set.',
null,
],
[
'turns Unique off',
'title',
{ unique: false },
'Values no longer need to be unique.',
null,
],
['turns Required off', 'starts_on', { required: false }, 'No longer required.', null],
['turns the default off', 'online', { defaultOn: false }, '', null],
])('%s', (_, key, patch, text, flag) => {
const fields = edit(loaded(), key, patch)
if (text === '') {
expect(only(fields)).toEqual([])
return
}
expect(row(fields)).toMatchObject({ text, flag })
})
it('says the default was removed', () => {
const withDefault = { ...eventsFields[4]!, default: 20 }
const form = formFromType({ ...events, fields: [withDefault] }, () => 1).fields
const [removed] = diffFields(
baselinePayload({ ...events, fields: [withDefault] }),
edit(form, 'capacity', { defaultValue: '' }),
)
expect(changeText(removed!)).toBe('Default value removed.')
expect(removed!.flag).toBeNull()
})
it.each([
[
'starts_on',
{ max: '2027-01-01' },
'Latest date is now 2027-01-01. An entry may have a later date.',
],
[
'starts_on',
{ min: '2027-01-01' },
'Earliest date is now 2027-01-01. An entry may have an earlier date.',
],
['description', { max: '80' }, 'Longest is now 80. An entry may be longer.'],
['description', { min: '3' }, 'Shortest is now 3. An entry may be shorter.'],
[
'capacity',
{ min: '10' },
'Lowest value goes from 1 to 10. An entry may have a lower value.',
],
[
'capacity',
{ max: '2000' },
'Highest value goes from 5000 to 2000. An entry may have a higher value.',
],
])('explains the tighter bound of %s with its hint', (key, patch, cause) => {
const [change] = flaggedChanges(only(edit(loaded(), key, patch)))
expect(causeText(change!)).toBe(cause)
})
it('reports a choice removed together with its hint', () => {
const [change] = flaggedChanges(
only(edit(loaded(), 'category', { options: ['Talk', 'Workshop'] })),
)
expect(causeText(change!)).toBe('Choice removed: "Meetup". An entry may still use it.')
})
it('summarises a lone removal and one added field alongside a change', () => {
const base = loaded().filter((field) => field.key !== 'description')
expect(summaryLine(only(base))).toBe('Remove 1')
expect(summaryLine(only(edit(base, 'capacity', { max: '2000' })))).toBe(
'Change 1, remove 1',
)
})
it('describes an added field with a boolean or text default', () => {
const added = (patch: Partial<BuilderField>) =>
createField(100, { label: 'New', key: 'new', ...patch })
expect(row([...loaded(), added({ type: 'boolean', defaultOn: true })]).text).toBe(
'Yes or no, optional, default on.',
)
expect(row([...loaded(), added({ defaultValue: 'x' })]).text).toBe(
'Text, optional, default "x".',
)
})
it('joins several notes of one field and flags it when one of them is flagged', () => {
const [change] = only(edit(loaded(), 'capacity', { max: '2000', label: 'Seats' }))
expect(changeText(change!)).toBe(
'Label goes from "Capacity" to "Seats". Highest value goes from 5000 to 2000.',
)
expect(change!.flag).toBe('mayNotFit')
})
})
describe('order of the review and the summary', () => {
const added = [
createField(100, { label: 'Speaker', key: 'speaker' }),
createField(101, { label: 'Room', key: 'room' }),
]
const fields = edit(
[...loaded().filter((field) => field.key !== 'description'), ...added],
'capacity',
{ max: '2000' },
)
it('lists additions, changes then removals', () => {
expect(only(fields).map(changeTitle)).toEqual([
'Add Speaker',
'Add Room',
'Change Capacity',
'Remove Description',
])
})
it.each([
['2 added, 1 changed, 1 removed', 'Add 2 fields, change 1, remove 1'],
['1 added', 'Add 1 field'],
['1 changed, 2 removed', 'Change 1, remove 2'],
['1 changed', 'Change 1'],
])('writes the summary for %s', (changes, summary) => {
const base = loaded()
const wanted: Record<string, BuilderField[]> = {
'2 added, 1 changed, 1 removed': fields,
'1 added': [...base, added[0]!],
'1 changed, 2 removed': edit(
base.filter((field) => field.key !== 'description' && field.key !== 'ends_on'),
'capacity',
{ max: '2000' },
),
'1 changed': edit(base, 'capacity', { max: '2000' }),
}
expect(summaryLine(only(wanted[changes]!))).toBe(summary)
})
it('keeps only the flagged additions and changes for the refusal', () => {
const flagged = flaggedChanges(only(fields))
expect(flagged.map(changeTitle)).toEqual(['Change Capacity'])
expect(causeText(flagged[0]!)).toBe(
'Highest value goes from 5000 to 2000. An entry may have a higher value.',
)
})
it('names the cause of a field added with no default without a hint', () => {
const seats = createField(100, {
label: 'Seats',
key: 'seats',
type: 'number',
required: true,
})
const [change] = flaggedChanges(only([...loaded(), seats]))
expect(causeText(change!)).toBe('Number, required. Existing entries have no value for it.')
})
})
@@ -0,0 +1,125 @@
import { describe, expect, it } from 'vitest'
import type { ContentType } from '@/composables/useContentTypes'
import { baselinePayload, fieldFromApi, formFromType } from '@/lib/schemaBuilderLoad'
import { buildUpdatePayload, payloadField } from '@/lib/schemaBuilderPayload'
import { apiField, eventsFields } from './schemaFixtures'
const events: ContentType = {
id: 1,
key: 'events',
name: 'Events',
icon: 'calendar',
fields: eventsFields,
created_at: '2026-09-01T09:30:00Z',
updated_at: '2026-10-15T09:30:00Z',
}
function counter() {
let next = 1
return () => next++
}
describe('form from a content type', () => {
it('locks the key, marks every field saved and keeps the saved order', () => {
const form = formFromType(events, counter())
expect(form.name).toBe('Events')
expect(form.key).toBe('events')
expect(form.keyLocked).toBe(true)
expect(form.fields.map((field) => field.key)).toEqual(
eventsFields.map((field) => field.key),
)
expect(form.fields.every((field) => field.saved && field.keyMode === 'manual')).toBe(true)
})
it('reuses the given uids by position, then draws new ones', () => {
const form = formFromType(events, counter(), [10, 11])
expect(form.fields.map((field) => field.uid)).toEqual([10, 11, 1, 2, 3, 4, 5])
})
it('maps each API field to its builder fields', () => {
const [title, startsOn, , online, capacity, category] = formFromType(
events,
counter(),
).fields
expect(title).toMatchObject({
required: true,
unique: true,
max: '80',
min: '',
})
expect(startsOn).toMatchObject({ type: 'date', required: true })
expect(online).toMatchObject({
type: 'boolean',
defaultOn: false,
defaultValue: '',
})
expect(capacity).toMatchObject({ min: '1', max: '5000' })
expect(category).toMatchObject({
options: ['Talk', 'Workshop', 'Meetup'],
})
})
it.each([
['a number default', apiField('N', 'n', 'number', { default: 0, options: [] }), '0'],
['a text default', apiField('T', 't', 'text', { default: 'x', options: [] }), 'x'],
[
'a date default as a plain day',
apiField('D', 'd', 'date', {
default: '2026-11-21T00:00:00Z',
options: [],
}),
'2026-11-21',
],
])('reads %s', (_, source, expected) => {
expect(fieldFromApi(source, 1).defaultValue).toBe(expected)
})
it('turns a true boolean default on and anything else off', () => {
expect(fieldFromApi(apiField('B', 'b', 'boolean', { default: true }), 1).defaultOn).toBe(
true,
)
expect(fieldFromApi(apiField('B', 'b', 'boolean', { default: false }), 1).defaultOn).toBe(
false,
)
expect(fieldFromApi(apiField('B', 'b', 'boolean'), 1).defaultOn).toBe(false)
})
it('reads date bounds as plain days and Choice options as text', () => {
const date = fieldFromApi(
apiField('D', 'd', 'date', {
min: '2026-01-01T00:00:00Z',
max: '2027-12-31',
options: [],
}),
1,
)
expect(date).toMatchObject({ min: '2026-01-01', max: '2027-12-31' })
expect(fieldFromApi(apiField('C', 'c', 'select', { options: null }), 1).options).toEqual([])
})
})
describe('the round trip', () => {
it('sends back the loaded fields with no spurious difference', () => {
const form = formFromType(events, counter())
const payload = buildUpdatePayload(form, events.icon)
expect(payload.fields).toEqual(
eventsFields.map((field) => ({
...field,
default: field.type === 'boolean' ? null : field.default,
})),
)
expect(payload.fields).toEqual(baselinePayload(events))
})
it('has the same payload for a field and for its reloaded copy', () => {
const field = fieldFromApi(eventsFields[4]!, 1)
expect(payloadField(field)).toEqual(payloadField(fieldFromApi(eventsFields[4]!, 2)))
})
})
@@ -1,13 +1,20 @@
import { describe, expect, it } from 'vitest'
import { createField, type BuilderForm } from '@/lib/schemaBuilderModel'
import { buildPayload } from '@/lib/schemaBuilderPayload'
import { buildPayload, buildUpdatePayload } from '@/lib/schemaBuilderPayload'
const EVENTS: BuilderForm = {
name: ' Events ',
key: 'events',
keyMode: 'auto',
keyLocked: false,
fields: [
createField(1, { label: 'Title', key: 'title', required: true, unique: true, max: '80' }),
createField(1, {
label: 'Title',
key: 'title',
required: true,
unique: true,
max: '80',
}),
createField(2, {
label: 'Starts on',
key: 'starts_on',
@@ -16,7 +23,12 @@ const EVENTS: BuilderForm = {
min: '2026-01-01',
max: '2027-12-31',
}),
createField(3, { label: 'Online', key: 'online', type: 'boolean', defaultOn: true }),
createField(3, {
label: 'Online',
key: 'online',
type: 'boolean',
defaultOn: true,
}),
createField(4, {
label: 'Capacity',
key: 'capacity',
@@ -32,12 +44,24 @@ const EVENTS: BuilderForm = {
defaultValue: 'Talk',
options: [' Talk', 'Workshop ', 'Meetup'],
}),
createField(6, { label: 'Description', key: 'description', type: 'richtext', max: '5000' }),
createField(6, {
label: 'Description',
key: 'description',
type: 'richtext',
max: '5000',
}),
],
}
function field(key: string, rest: object) {
return { key, label: key, type: 'text', required: false, unique: false, ...rest }
return {
key,
label: key,
type: 'text',
required: false,
unique: false,
...rest,
}
}
describe('create payload', () => {
@@ -146,7 +170,14 @@ describe('create payload', () => {
it('does not send the default of a unique field', () => {
const body = buildPayload({
...EVENTS,
fields: [createField(1, { label: 'T', key: 't', unique: true, defaultValue: 'Draft' })],
fields: [
createField(1, {
label: 'T',
key: 't',
unique: true,
defaultValue: 'Draft',
}),
],
})
expect(body.fields[0]!.default).toBeNull()
@@ -156,8 +187,18 @@ describe('create payload', () => {
const body = buildPayload({
...EVENTS,
fields: [
createField(1, { label: 'R', key: 'r', type: 'richtext', unique: true }),
createField(2, { label: 'B', key: 'b', type: 'boolean', unique: true }),
createField(1, {
label: 'R',
key: 'r',
type: 'richtext',
unique: true,
}),
createField(2, {
label: 'B',
key: 'b',
type: 'boolean',
unique: true,
}),
],
})
@@ -168,8 +209,20 @@ describe('create payload', () => {
const body = buildPayload({
...EVENTS,
fields: [
createField(1, { label: 'B', key: 'b', type: 'boolean', min: '1', max: '2' }),
createField(2, { label: 'S', key: 's', type: 'select', min: '1', options: ['A'] }),
createField(1, {
label: 'B',
key: 'b',
type: 'boolean',
min: '1',
max: '2',
}),
createField(2, {
label: 'S',
key: 's',
type: 'select',
min: '1',
options: ['A'],
}),
],
})
@@ -188,3 +241,18 @@ describe('create payload', () => {
expect(body.fields[0]!.min).toBe(0)
})
})
describe('buildUpdatePayload', () => {
it('sends exactly name, icon and fields, with no key and the name trimmed', () => {
const payload = buildUpdatePayload(EVENTS, 'calendar')
expect(Object.keys(payload)).toEqual(['name', 'icon', 'fields'])
expect(payload.name).toBe('Events')
expect(payload.icon).toBe('calendar')
expect(payload.fields).toEqual(buildPayload(EVENTS).fields)
})
it('keeps a null icon null', () => {
expect(buildUpdatePayload(EVENTS, null).icon).toBeNull()
})
})
@@ -0,0 +1,40 @@
import { describe, expect, it } from 'vitest'
import { createField } from '@/lib/schemaBuilderModel'
import { reservedKeys, restoreIndex } from '@/lib/schemaBuilderRemoval'
const keys = ['title', 'starts_on', 'ends_on', 'online', 'capacity', 'category', 'description']
const saved = (key: string) => createField(keys.indexOf(key), { key, saved: true })
describe('where an undo puts a field back', () => {
it('restores the last field at the end', () => {
const current = keys.slice(0, 6).map(saved)
expect(restoreIndex(current, keys, 'description')).toBe(6)
})
it('restores a middle field after the saved field that came before it', () => {
const current = ['title', 'starts_on', 'online', 'capacity', 'category', 'description'].map(
saved,
)
expect(restoreIndex(current, keys, 'ends_on')).toBe(2)
})
it('restores the first field at the top', () => {
const current = keys.slice(1).map(saved)
expect(restoreIndex(current, keys, 'title')).toBe(0)
})
it('ignores fields added since the last save when looking for the slot', () => {
const current = [saved('title'), createField(99, { key: 'speaker' }), saved('online')]
expect(restoreIndex(current, keys, 'ends_on')).toBe(1)
})
it('keeps the keys of the removed fields reserved', () => {
expect(
reservedKeys([{ field: saved('description') }, { field: saved('ends_on') }]),
).toEqual(['description', 'ends_on'])
})
})
@@ -19,6 +19,7 @@ function form(overrides: Partial<BuilderForm> = {}): BuilderForm {
name: 'Events',
key: 'events',
keyMode: 'auto',
keyLocked: false,
fields: [starterField(1)],
...overrides,
}
@@ -109,8 +110,16 @@ describe('field validation', () => {
})
it('flags only the second of two equal keys', () => {
const first = createField(1, { label: 'A', key: 'title', keyMode: 'manual' })
const second = createField(2, { label: 'B', key: 'title', keyMode: 'manual' })
const first = createField(1, {
label: 'A',
key: 'title',
keyMode: 'manual',
})
const second = createField(2, {
label: 'B',
key: 'title',
keyMode: 'manual',
})
const errors = validateForm(form({ fields: [first, second] }))
@@ -162,7 +171,11 @@ describe('field validation', () => {
],
[
'date',
{ min: '2026-01-01', max: '2027-12-31', defaultValue: '2028-01-01' },
{
min: '2026-01-01',
max: '2027-12-31',
defaultValue: '2028-01-01',
},
'default',
'The default value is outside the limits.',
],
@@ -238,7 +251,11 @@ describe('problem counting', () => {
})
it('counts one field with a bad key as one problem', () => {
const bad = createField(1, { label: 'S', key: 'start-date', keyMode: 'manual' })
const bad = createField(1, {
label: 'S',
key: 'start-date',
keyMode: 'manual',
})
expect(countProblems(validateForm(form({ fields: [bad] })))).toBe(1)
})
@@ -247,3 +264,45 @@ describe('problem counting', () => {
expect(countProblems(validateForm(form()))).toBe(0)
})
})
describe('validation of a saved type', () => {
const saved = (key: string, overrides: Partial<BuilderField> = {}) =>
createField(1, {
label: key,
key,
keyMode: 'manual',
saved: true,
...overrides,
})
it('shows no key error for a locked type key', () => {
expect(validateType(form({ key: 'E', keyLocked: true })).key).toBeUndefined()
expect(Object.keys(validateType(form({ key: 'E', keyLocked: true })))).toEqual([])
})
it('never shows a key error on a saved field', () => {
const odd = saved('Old-Key')
expect(validateForm(form({ fields: [odd] })).fields.size).toBe(0)
})
it('flags a new field whose key is a saved key, whatever the order', () => {
const copy = createField(2, { label: 'Title', key: 'title' })
const errors = validateForm(form({ fields: [copy, saved('title')] }))
expect(errors.fields.get(2)?.key).toBe('Another field already uses this key.')
expect(errors.fields.has(1)).toBe(false)
})
it('flags a new field that takes the key of a removed field', () => {
const fresh = createField(2, {
label: 'Description',
key: 'description',
})
expect(validateForm(form({ fields: [fresh] }), ['description']).fields.get(2)?.key).toBe(
'Another field already uses this key.',
)
expect(validateForm(form({ fields: [fresh] })).fields.size).toBe(0)
})
})
@@ -0,0 +1,17 @@
import { describe, expect, it } from 'vitest'
import { entriesNotice } from '@/lib/schemaEntries'
describe('the entries notice', () => {
it.each([
[-3, ''],
[0, ''],
[1, '1 entry uses this content type. Changes are checked against it when you save.'],
[37, '37 entries use this content type. Changes are checked against them when you save.'],
[
1200,
'1,200 entries use this content type. Changes are checked against them when you save.',
],
])('reads for %s entries', (total, copy) => {
expect(entriesNotice(total)).toBe(copy)
})
})
+42
View File
@@ -0,0 +1,42 @@
export function apiField(label: string, key: string, type: string, rest: object = {}) {
return {
key,
label,
type,
required: false,
unique: false,
default: null,
min: null,
max: null,
options: null,
...rest,
}
}
export const eventsFields = [
apiField('Title', 'title', 'text', {
required: true,
unique: true,
max: 80,
options: [],
}),
apiField('Starts on', 'starts_on', 'date', { required: true, options: [] }),
apiField('Ends on', 'ends_on', 'date', { options: [] }),
apiField('Online', 'online', 'boolean', { default: false, options: [] }),
apiField('Capacity', 'capacity', 'number', {
min: 1,
max: 5000,
options: [],
}),
apiField('Category', 'category', 'select', {
options: ['Talk', 'Workshop', 'Meetup'],
}),
apiField('Description', 'description', 'richtext', { options: [] }),
]
export const recipesFields = [
apiField('Title', 'title', 'text', { required: true, options: [] }),
apiField('Servings', 'servings', 'number', { min: 1, options: [] }),
apiField('Vegetarian', 'vegetarian', 'boolean', { options: [] }),
apiField('Method', 'method', 'richtext', { options: [] }),
]