@json-layout/vocabulary 0.9.1 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@json-layout/vocabulary",
3
- "version": "0.9.1",
3
+ "version": "0.11.0",
4
4
  "description": "Main JSON Layout vocabulary as JSON schemas and Typescript types. Also contains some small utility functions to validate and normalize annotations.",
5
5
  "type": "module",
6
6
  "exports": {
package/src/index.js CHANGED
@@ -1,4 +1,4 @@
1
1
  /** @typedef {import("./types.js").SchemaFragment} SchemaFragment */
2
2
  export * from './layout-keyword/index.js'
3
3
  export * from './normalized-layout/index.js'
4
- export { normalizeLayoutFragment } from './normalize.js'
4
+ export { normalizeLayoutFragment, getSchemaFragmentType } from './normalize.js'
package/src/normalize.js CHANGED
@@ -18,7 +18,7 @@ import { validateNormalizedLayout, compositeCompNames } from './normalized-layou
18
18
  * @returns {Children}
19
19
  */
20
20
  function getDefaultChildren (schemaFragment) {
21
- const { type } = getType(schemaFragment)
21
+ const { type } = getSchemaFragmentType(schemaFragment)
22
22
  /** @type {Children} */
23
23
  const children = []
24
24
  if (type === 'object') {
@@ -87,8 +87,8 @@ function getChildren (defaultChildren, partialChildren) {
87
87
  * @returns {import('./index.js').ComponentName}
88
88
  */
89
89
  function getDefaultComp (partial, schemaFragment, arrayChild) {
90
- const { type } = getType(schemaFragment)
91
- const hasSimpleType = ['string', 'integer', 'number'].includes(type)
90
+ const { type } = getSchemaFragmentType(schemaFragment)
91
+ const hasSimpleType = type && ['string', 'integer', 'number'].includes(type)
92
92
  if (arrayChild === 'oneOf') return 'one-of-select'
93
93
  if (hasSimpleType && schemaFragment.enum) return schemaFragment.enum.length > 20 ? 'autocomplete' : 'select'
94
94
  if (hasSimpleType && schemaFragment.oneOf) return schemaFragment.oneOf.length > 20 ? 'autocomplete' : 'select'
@@ -162,8 +162,8 @@ function normalizeExpression (expression, defaultType = 'js-eval') {
162
162
  */
163
163
  function getItemsFromSchema (schemaFragment) {
164
164
  if (!schemaFragment) return null
165
- const { type } = getType(schemaFragment)
166
- const hasSimpleType = ['string', 'integer', 'number'].includes(type)
165
+ const { type } = getSchemaFragmentType(schemaFragment)
166
+ const hasSimpleType = type && ['string', 'integer', 'number'].includes(type)
167
167
  if (schemaFragment.enum && hasSimpleType) {
168
168
  return schemaFragment.enum.map((/** @type {string} */ value) => ({ key: value + '', title: value + '', value }))
169
169
  }
@@ -194,13 +194,36 @@ function getItemsFromSchema (schemaFragment) {
194
194
 
195
195
  /**
196
196
  * @param {SchemaFragment} schemaFragment
197
- * @returns {{type: string, nullable: boolean}}
197
+ * @returns {{type: string | undefined, nullable: boolean}}
198
198
  */
199
- const getType = (schemaFragment) => {
199
+ export const getSchemaFragmentType = (schemaFragment) => {
200
200
  if (Array.isArray(schemaFragment.type) && schemaFragment.type.length === 2 && schemaFragment.type.includes('null')) {
201
201
  const type = schemaFragment.type.find(t => t !== 'null')
202
202
  return { type, nullable: true }
203
203
  }
204
+
205
+ if (!schemaFragment.type && schemaFragment.properties) {
206
+ return { type: 'object', nullable: false }
207
+ }
208
+
209
+ // case where type is not defined but can be deduced from children oneOf/allOf/anyOf
210
+ /** @type {any[]} */
211
+ if (!schemaFragment.type) {
212
+ /** @type {string[]} */
213
+ const combinationTypes = []
214
+ for (const combinationKey of ['allOf', 'anyOf', 'oneOf']) {
215
+ // @ts-ignore
216
+ if (schemaFragment[combinationKey]) {
217
+ // @ts-ignore
218
+ for (const subSchema of schemaFragment[combinationKey]) {
219
+ const { type: subType } = getSchemaFragmentType(subSchema)
220
+ if (subType && !combinationTypes.includes(subType)) combinationTypes.push(subType)
221
+ }
222
+ }
223
+ }
224
+ if (combinationTypes.length === 1) return { type: combinationTypes[0], nullable: false }
225
+ }
226
+
204
227
  return { type: schemaFragment.type, nullable: false }
205
228
  }
206
229
 
@@ -209,15 +232,16 @@ const getType = (schemaFragment) => {
209
232
  * @param {SchemaFragment} schemaFragment
210
233
  * @param {string} schemaPath
211
234
  * @param {(text: string) => string} markdown
235
+ * @param {string[]} optionsKeys
212
236
  * @param {'oneOf'} [arrayChild]
213
237
  * @returns {{normalized: CompObject, errors: string[]}}
214
238
  */
215
- function getCompObject (layoutKeyword, schemaFragment, schemaPath, markdown, arrayChild) {
239
+ function getCompObject (layoutKeyword, schemaFragment, schemaPath, markdown, optionsKeys, arrayChild) {
216
240
  /** @type {string[]} */
217
241
  const errors = []
218
242
  const key = schemaPath.slice(schemaPath.lastIndexOf('/') + 1)
219
243
 
220
- const { type, nullable } = getType(schemaFragment)
244
+ const { type, nullable } = getSchemaFragmentType(schemaFragment)
221
245
 
222
246
  if ('const' in schemaFragment) return { normalized: { comp: 'none' }, errors }
223
247
  if (!type) return { normalized: { comp: 'none' }, errors }
@@ -243,14 +267,14 @@ function getCompObject (layoutKeyword, schemaFragment, schemaPath, markdown, arr
243
267
 
244
268
  // @ts-ignore
245
269
  if (compositeCompNames.includes(partial.comp)) {
246
- partial.title = partial.title ?? schemaFragment.title ?? null
270
+ if (!('title' in partial)) partial.title = schemaFragment.title ?? null
247
271
  partial.children = getChildren(getDefaultChildren(schemaFragment), partial.children)
248
272
  } else if (partial.comp === 'list') {
249
- partial.title = partial.title ?? schemaFragment.title ?? key
273
+ if (!('title' in partial)) partial.title = schemaFragment.title ?? key
250
274
  partial.listEditMode = partial.listEditMode ?? (schemaFragment.items.type === 'object' ? 'inline-single' : 'inline')
251
275
  partial.listActions = partial.listActions ?? ['add', 'edit', 'delete', 'duplicate', 'sort']
252
276
  } else {
253
- partial.label = partial.label ?? schemaFragment.title ?? key
277
+ if (!('label' in partial)) partial.label = schemaFragment.title ?? key
254
278
  }
255
279
 
256
280
  if (['select', 'autocomplete', 'combobox'].includes(partial.comp) && !partial.items) {
@@ -298,6 +322,20 @@ function getCompObject (layoutKeyword, schemaFragment, schemaPath, markdown, arr
298
322
  partial.defaultData = ''
299
323
  }
300
324
 
325
+ // options can be set directly in layout and normalized in layout.options
326
+ // TODO: fetch this list from the schema ?
327
+ for (const optionKey of optionsKeys) {
328
+ if (optionKey in partial) {
329
+ partial.options = partial.options ?? {}
330
+ partial.options[optionKey] = partial[optionKey]
331
+ delete partial[optionKey]
332
+ }
333
+ }
334
+ if (schemaFragment.readOnly) {
335
+ partial.options = partial.options ?? {}
336
+ if (!('readOnly' in partial.options)) partial.options.readOnly = true
337
+ }
338
+
301
339
  if (partial.getOptions !== undefined) partial.getOptions = normalizeExpression(partial.getOptions)
302
340
  if (partial.getDefaultData !== undefined) partial.getDefaultData = normalizeExpression(partial.getDefaultData)
303
341
  if (partial.getConstData !== undefined) partial.getConstData = normalizeExpression(partial.getConstData)
@@ -371,10 +409,11 @@ function getCompObject (layoutKeyword, schemaFragment, schemaPath, markdown, arr
371
409
  * @param {SchemaFragment} schemaFragment
372
410
  * @param {string} schemaPath
373
411
  * @param {(text: string) => string} markdown
412
+ * @param {string[]} optionsKeys
374
413
  * @param {'oneOf'} [arrayChild]
375
414
  * @returns {{normalized: NormalizedLayout, errors: string[]}}
376
415
  */
377
- function getNormalizedLayout (layoutKeyword, schemaFragment, schemaPath, markdown, arrayChild) {
416
+ function getNormalizedLayout (layoutKeyword, schemaFragment, schemaPath, markdown, optionsKeys, arrayChild) {
378
417
  if (isPartialSwitch(layoutKeyword)) {
379
418
  /** @type {CompObject[]} */
380
419
  const normalizedSwitchCases = []
@@ -385,13 +424,13 @@ function getNormalizedLayout (layoutKeyword, schemaFragment, schemaPath, markdow
385
424
  }
386
425
  for (let i = 0; i < switchCases.length; i++) {
387
426
  const switchCase = switchCases[i]
388
- const compObjectResult = getCompObject(switchCase, schemaFragment, schemaPath, markdown, arrayChild)
427
+ const compObjectResult = getCompObject(switchCase, schemaFragment, schemaPath, markdown, optionsKeys, arrayChild)
389
428
  normalizedSwitchCases.push(compObjectResult.normalized)
390
429
  for (const error of compObjectResult.errors) errors.push(`switch ${i} - ${error}`)
391
430
  }
392
431
  return { normalized: { switch: normalizedSwitchCases }, errors: [] }
393
432
  } else {
394
- return getCompObject(layoutKeyword, schemaFragment, schemaPath, markdown, arrayChild)
433
+ return getCompObject(layoutKeyword, schemaFragment, schemaPath, markdown, optionsKeys, arrayChild)
395
434
  }
396
435
  }
397
436
 
@@ -423,14 +462,18 @@ function lighterValidationErrors (errors) {
423
462
  return errors.map(e => e.message ?? e.keyword)
424
463
  }
425
464
 
465
+ const defaultOptionsKeys = ['readOnly', 'summary', 'titleDepth', 'density', 'removeAdditional', 'validateOn', 'initialValidation', 'defaultOn', 'readOnlyPropertiesMode']
466
+
426
467
  /**
427
468
  * @param {SchemaFragment} schemaFragment
428
469
  * @param {string} schemaPath
429
470
  * @param {(text: string) => string} markdown
471
+ * @param {string[]} [optionsKeys]
430
472
  * @param {'oneOf'} [arrayChild]
431
473
  * @returns {{layout: NormalizedLayout, errors: string[]}}
432
474
  */
433
- export function normalizeLayoutFragment (schemaFragment, schemaPath, markdown = (src) => src, arrayChild) {
475
+ export function normalizeLayoutFragment (schemaFragment, schemaPath, markdown = (src) => src, optionsKeys, arrayChild) {
476
+ optionsKeys = optionsKeys ? optionsKeys.concat(defaultOptionsKeys) : defaultOptionsKeys
434
477
  let layoutKeyword
435
478
  if (arrayChild === 'oneOf') {
436
479
  layoutKeyword = schemaFragment.oneOfLayout ?? {}
@@ -440,15 +483,15 @@ export function normalizeLayoutFragment (schemaFragment, schemaPath, markdown =
440
483
  if (!validateLayoutKeyword(layoutKeyword)) {
441
484
  console.error(`layout keyword validation errors at path ${schemaPath}`, layoutKeyword, validateLayoutKeyword.errors)
442
485
  return {
443
- layout: getNormalizedLayout({}, schemaFragment, schemaPath, markdown, arrayChild).normalized,
486
+ layout: getNormalizedLayout({}, schemaFragment, schemaPath, markdown, optionsKeys, arrayChild).normalized,
444
487
  errors: lighterValidationErrors(validateLayoutKeyword.errors)
445
488
  }
446
489
  }
447
- const normalizedLayout = getNormalizedLayout(layoutKeyword, schemaFragment, schemaPath, markdown, arrayChild)
490
+ const normalizedLayout = getNormalizedLayout(layoutKeyword, schemaFragment, schemaPath, markdown, optionsKeys, arrayChild)
448
491
  if (!validateNormalizedLayout(normalizedLayout.normalized)) {
449
492
  console.error(`normalized layout validation errors at path ${schemaPath}`, normalizedLayout, validateNormalizedLayout.errors)
450
493
  return {
451
- layout: getNormalizedLayout({}, schemaFragment, schemaPath, markdown, arrayChild).normalized,
494
+ layout: getNormalizedLayout({}, schemaFragment, schemaPath, markdown, optionsKeys, arrayChild).normalized,
452
495
  errors: lighterValidationErrors(validateNormalizedLayout.errors)
453
496
  }
454
497
  // throw new Error(`invalid layout at path ${schemaPath}`, { cause: validateNormalizedLayout.errors })
@@ -1033,7 +1033,65 @@ export default {
1033
1033
  "default",
1034
1034
  "comfortable",
1035
1035
  "compact"
1036
+ ],
1037
+ "default": "default"
1038
+ },
1039
+ "removeAdditional": {
1040
+ "description": "true is the same as 'unknown', false is the same as 'none'",
1041
+ "default": "error",
1042
+ "oneOf": [
1043
+ {
1044
+ "type": "boolean"
1045
+ },
1046
+ {
1047
+ "type": "string",
1048
+ "enum": [
1049
+ "unknown",
1050
+ "error",
1051
+ "none"
1052
+ ]
1053
+ }
1036
1054
  ]
1055
+ },
1056
+ "validateOn": {
1057
+ "type": "string",
1058
+ "enum": [
1059
+ "input",
1060
+ "blur",
1061
+ "submit"
1062
+ ],
1063
+ "default": "input"
1064
+ },
1065
+ "initialValidation": {
1066
+ "type": "string",
1067
+ "enum": [
1068
+ "never",
1069
+ "always",
1070
+ "withData"
1071
+ ],
1072
+ "default": "withData"
1073
+ },
1074
+ "defaultOn": {
1075
+ "type": "string",
1076
+ "enum": [
1077
+ "missing",
1078
+ "empty",
1079
+ "never"
1080
+ ],
1081
+ "default": "empty"
1082
+ },
1083
+ "autofocus": {
1084
+ "type": "boolean",
1085
+ "default": false
1086
+ },
1087
+ "readOnlyPropertiesMode": {
1088
+ "type": "string",
1089
+ "enum": [
1090
+ "remove",
1091
+ "hide",
1092
+ "show"
1093
+ ],
1094
+ "default": "show"
1037
1095
  }
1038
1096
  }
1039
1097
  }
@@ -487,7 +487,39 @@
487
487
  "readOnly": {"type": "boolean", "default": false},
488
488
  "summary": {"type": "boolean", "default": false},
489
489
  "titleDepth": {"type": "integer", "minimum": 1, "maximum": 6, "default": 2},
490
- "density": {"type": "string", "enum": ["default", "comfortable", "compact"]}
490
+ "density": {"type": "string", "enum": ["default", "comfortable", "compact"], "default": "default"},
491
+ "removeAdditional": {
492
+ "description": "true is the same as 'unknown', false is the same as 'none'",
493
+ "default": "error",
494
+ "oneOf": [
495
+ {"type": "boolean"},
496
+ {
497
+ "type": "string",
498
+ "enum": ["unknown", "error", "none"]
499
+ }
500
+ ]
501
+ },
502
+ "validateOn": {
503
+ "type": "string",
504
+ "enum": ["input", "blur", "submit"],
505
+ "default": "input"
506
+ },
507
+ "initialValidation": {
508
+ "type": "string",
509
+ "enum": ["never", "always", "withData"],
510
+ "default": "withData"
511
+ },
512
+ "defaultOn": {
513
+ "type": "string",
514
+ "enum": ["missing", "empty", "never"],
515
+ "default": "empty"
516
+ },
517
+ "autofocus": {"type": "boolean", "default": false},
518
+ "readOnlyPropertiesMode": {
519
+ "type": "string",
520
+ "enum": ["remove", "hide", "show"],
521
+ "default": "show"
522
+ }
491
523
  }
492
524
  }
493
525
  ]
@@ -51,6 +51,15 @@ export type StateNodeOptionsBase = StateNodeOptionsBaseLib & {
51
51
  summary?: boolean;
52
52
  titleDepth?: number;
53
53
  density?: "default" | "comfortable" | "compact";
54
+ /**
55
+ * true is the same as 'unknown', false is the same as 'none'
56
+ */
57
+ removeAdditional?: boolean | ("unknown" | "error" | "none");
58
+ validateOn?: "input" | "blur" | "submit";
59
+ initialValidation?: "never" | "always" | "withData";
60
+ defaultOn?: "missing" | "empty" | "never";
61
+ autofocus?: boolean;
62
+ readOnlyPropertiesMode?: "remove" | "hide" | "show";
54
63
  [k: string]: unknown;
55
64
  };
56
65
  export type Cols = number;