@ditojs/admin 2.100.0 → 3.0.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.
Files changed (84) hide show
  1. package/dist/dito-admin.css +1 -1
  2. package/dist/dito-admin.es.js +3421 -2662
  3. package/dist/dito-admin.umd.js +6 -6
  4. package/package.json +81 -45
  5. package/src/DitoComponent.js +1 -1
  6. package/src/DitoContext.js +51 -0
  7. package/src/DitoTypeComponent.js +6 -1
  8. package/src/components/DitoAffixes.vue +6 -3
  9. package/src/components/DitoContainer.vue +19 -10
  10. package/src/components/DitoCreateButton.vue +11 -2
  11. package/src/components/DitoDialog.vue +21 -2
  12. package/src/components/DitoForm.vue +156 -83
  13. package/src/components/DitoLabel.vue +3 -2
  14. package/src/components/DitoPane.vue +55 -11
  15. package/src/components/DitoPanel.vue +33 -5
  16. package/src/components/DitoPanels.vue +2 -1
  17. package/src/components/DitoRoot.vue +13 -6
  18. package/src/components/DitoSchema.vue +278 -145
  19. package/src/components/DitoSchemaInlined.vue +1 -1
  20. package/src/components/DitoTabs.vue +16 -18
  21. package/src/components/DitoTrail.vue +5 -4
  22. package/src/components/DitoTreeItem.vue +17 -6
  23. package/src/components/DitoView.vue +25 -2
  24. package/src/mixins/DitoMixin.js +42 -7
  25. package/src/mixins/ItemMixin.js +3 -6
  26. package/src/mixins/NumberMixin.js +47 -31
  27. package/src/mixins/OptionsMixin.js +122 -71
  28. package/src/mixins/ResourceMixin.js +5 -2
  29. package/src/mixins/RouteMixin.js +39 -1
  30. package/src/mixins/SortableMixin.js +0 -13
  31. package/src/mixins/SourceMixin.js +142 -95
  32. package/src/mixins/TextMixin.js +1 -1
  33. package/src/mixins/TypeMixin.js +37 -22
  34. package/src/mixins/ValidationMixin.js +9 -27
  35. package/src/mixins/ValidatorMixin.js +5 -7
  36. package/src/mixins/ValueMixin.js +5 -2
  37. package/src/types/DitoTypeButton.vue +1 -1
  38. package/src/types/DitoTypeCheckbox.vue +1 -1
  39. package/src/types/DitoTypeCheckboxes.vue +1 -1
  40. package/src/types/DitoTypeCode.vue +8 -1
  41. package/src/types/DitoTypeColor.vue +20 -34
  42. package/src/types/DitoTypeComponent.vue +1 -1
  43. package/src/types/DitoTypeComputed.vue +4 -27
  44. package/src/types/DitoTypeDate.vue +16 -5
  45. package/src/types/DitoTypeLabel.vue +1 -4
  46. package/src/types/DitoTypeList.vue +9 -5
  47. package/src/types/DitoTypeMarkup.vue +25 -13
  48. package/src/types/DitoTypeMultiselect.vue +26 -6
  49. package/src/types/DitoTypeNumber.vue +1 -1
  50. package/src/types/DitoTypeObject.vue +10 -7
  51. package/src/types/DitoTypePanel.vue +19 -6
  52. package/src/types/DitoTypeProgress.vue +1 -1
  53. package/src/types/DitoTypeRadio.vue +1 -1
  54. package/src/types/DitoTypeSection.vue +32 -4
  55. package/src/types/DitoTypeSelect.vue +1 -1
  56. package/src/types/DitoTypeSlider.vue +1 -1
  57. package/src/types/DitoTypeSwitch.vue +1 -1
  58. package/src/types/DitoTypeText.vue +13 -15
  59. package/src/types/DitoTypeTextarea.vue +1 -1
  60. package/src/types/DitoTypeTreeList.vue +12 -7
  61. package/src/types/DitoTypeUpload.vue +1 -2
  62. package/src/utils/DataSchemaResolver.js +141 -0
  63. package/src/utils/DataSchemaResolver.test.js +132 -0
  64. package/src/utils/FormModel.js +556 -0
  65. package/src/utils/FormModel.test.js +808 -0
  66. package/src/utils/data.js +20 -0
  67. package/src/utils/data.test.js +28 -0
  68. package/src/utils/filter.js +3 -2
  69. package/src/utils/options.js +3 -0
  70. package/src/utils/schema/data.js +617 -0
  71. package/src/utils/schema/data.test.js +471 -0
  72. package/src/utils/schema/lookup.js +266 -0
  73. package/src/utils/schema/lookup.test.js +61 -0
  74. package/src/utils/schema/setup.js +354 -0
  75. package/src/utils/schema/structure.js +172 -0
  76. package/src/utils/schema/structure.test.js +57 -0
  77. package/src/utils/schema/types.js +34 -0
  78. package/src/utils/schema/validation.js +142 -0
  79. package/src/utils/schema/validation.test.js +395 -0
  80. package/src/utils/units.js +70 -3
  81. package/src/utils/units.test.js +71 -0
  82. package/types/index.d.ts +149 -59
  83. package/src/mixins/DataMixin.js +0 -131
  84. package/src/utils/schema.js +0 -1121
package/src/utils/data.js CHANGED
@@ -11,6 +11,26 @@ export function appendDataPath(dataPath, token) {
11
11
  : token
12
12
  }
13
13
 
14
+ /**
15
+ * Returns the part of `dataPath` inside `parentDataPath`, e.g. `0/title` for
16
+ * `chapters/0/title` in `chapters`, an empty string if both are the same, or
17
+ * `null` if `dataPath` isn't inside `parentDataPath`.
18
+ */
19
+ export function getRelativeDataPath(dataPath, parentDataPath) {
20
+ if (dataPath == null || parentDataPath == null) {
21
+ return null
22
+ }
23
+ const path = normalizeDataPath(dataPath)
24
+ const parentPath = normalizeDataPath(parentDataPath)
25
+ return path === parentPath
26
+ ? ''
27
+ : !parentPath
28
+ ? path
29
+ : path.startsWith(`${parentPath}/`)
30
+ ? path.slice(parentPath.length + 1)
31
+ : null
32
+ }
33
+
14
34
  export function parseParentDataPath(dataPath) {
15
35
  const path = parseDataPath(dataPath)
16
36
  path?.pop()
@@ -0,0 +1,28 @@
1
+ import { getRelativeDataPath } from './data.js'
2
+
3
+ describe('getRelativeDataPath()', () => {
4
+ it('returns the part of the data path inside the parent data path', () => {
5
+ expect(getRelativeDataPath('chapters/0/title', 'chapters')).toBe(
6
+ '0/title'
7
+ )
8
+ expect(getRelativeDataPath('chapters/0', '')).toBe('chapters/0')
9
+ })
10
+
11
+ it('returns an empty string for the same data paths', () => {
12
+ expect(getRelativeDataPath('chapters', 'chapters')).toBe('')
13
+ expect(getRelativeDataPath('', '')).toBe('')
14
+ })
15
+
16
+ it('returns `null` for data paths outside the parent data path', () => {
17
+ expect(getRelativeDataPath('chapters', 'chapters/0')).toBe(null)
18
+ expect(getRelativeDataPath('chaptersList/0', 'chapters')).toBe(null)
19
+ expect(getRelativeDataPath('chapters', undefined)).toBe(null)
20
+ expect(getRelativeDataPath(undefined, 'chapters')).toBe(null)
21
+ })
22
+
23
+ it('normalizes the data paths', () => {
24
+ expect(getRelativeDataPath('chapters[0].title', 'chapters/')).toBe(
25
+ '0/title'
26
+ )
27
+ })
28
+ })
@@ -1,5 +1,6 @@
1
1
  import { isArray, asArray, labelize } from '@ditojs/utils'
2
- import { getNamedSchemas, processNestedSchemaDefaults } from './schema'
2
+ import { getNamedSchemas } from './schema/lookup.js'
3
+ import { applyNestedSchemaDefaults } from './schema/setup.js'
3
4
 
4
5
  export const filterComponents = {
5
6
  'text'(filter) {
@@ -119,7 +120,7 @@ export function createFiltersPanel(api, filters, dataPath, query) {
119
120
  }
120
121
  }
121
122
  }
122
- processNestedSchemaDefaults(api, panel)
123
+ applyNestedSchemaDefaults(api, panel)
123
124
  return panel
124
125
  }
125
126
 
@@ -36,8 +36,11 @@ const ditoOptionKeys = [
36
36
  'generateLabel',
37
37
  'excludeValue',
38
38
  'ignoreMissingValue',
39
+ 'treatNullAsMissing',
40
+ 'valueFromDataSchema',
39
41
  'omitSpacing',
40
42
  'processValue',
43
+ 'getTypeValidations',
41
44
  'processSchema',
42
45
  'getPanelSchema',
43
46
  'getFormSchemasForProcessing',
@@ -0,0 +1,617 @@
1
+ import DitoContext from '../../DitoContext.js'
2
+ import { getUid } from '../uid.js'
3
+ import { SchemaGraph } from '../SchemaGraph.js'
4
+ import { appendDataPath, getRelativeDataPath } from '../data.js'
5
+ import { isMatchingType, convertType } from '../type.js'
6
+ import {
7
+ isObject,
8
+ isArray,
9
+ isFunction,
10
+ asArray,
11
+ clone,
12
+ getValueAtDataPath
13
+ } from '@ditojs/utils'
14
+ import { getTypeOptions, getSourceType } from './types.js'
15
+ import {
16
+ someNestedSchemaComponent,
17
+ hasNestedSchemaComponents,
18
+ isPanelWithOwnData,
19
+ isNested
20
+ } from './structure.js'
21
+ import {
22
+ hasFormSchema,
23
+ hasMultipleFormSchemas,
24
+ getFormSchemas,
25
+ getItemFormSchemaFromForms
26
+ } from './lookup.js'
27
+
28
+ export function getSchemaValue(
29
+ keyOrDataPath,
30
+ { type, schema, callback = true, default: def, context } = {}
31
+ ) {
32
+ const types = type && asArray(type)
33
+ // For performance reasons, data-paths in `keyOrDataPath` can only be
34
+ // provided in in array format here:
35
+ let value = schema
36
+ ? isArray(keyOrDataPath)
37
+ ? getValueAtDataPath(schema, keyOrDataPath, () => undefined)
38
+ : schema[keyOrDataPath]
39
+ : undefined
40
+
41
+ if (value === undefined && def !== undefined) {
42
+ if (callback && isFunction(def) && !isMatchingType(types, def)) {
43
+ // Support `default()` functions for any type except `Function`:
44
+ def = def(context)
45
+ }
46
+ return def
47
+ }
48
+
49
+ if (isMatchingType(types, value)) {
50
+ return value
51
+ }
52
+ // Any schema value handled through `getSchemaValue()` can provide
53
+ // a function that's resolved when the value is evaluated:
54
+ if (callback && isFunction(value)) {
55
+ value = value(context)
56
+ }
57
+ // Now finally see if we can convert to the expect types.
58
+ if (types && value != null && !isMatchingType(types, value)) {
59
+ for (const type of types) {
60
+ const converted = convertType(type, value)
61
+ if (converted !== value) {
62
+ return converted
63
+ }
64
+ }
65
+ }
66
+ return value
67
+ }
68
+
69
+ export function shouldRenderSchema(schema, context) {
70
+ return (
71
+ getSchemaValue('if', {
72
+ type: Boolean,
73
+ schema,
74
+ context,
75
+ default: true
76
+ }) && (
77
+ !hasNestedSchemaComponents(schema) ||
78
+ // The components of sources, e.g. lists with inlined components, are
79
+ // rendered for each item, with the item's own context:
80
+ !!getSourceType(schema) ||
81
+ // Evaluate the components with their own contexts, like when rendered:
82
+ someNestedSchemaComponent(schema, (component, name) =>
83
+ shouldRenderSchema(
84
+ component,
85
+ context.createChildContext(component, name)
86
+ )
87
+ )
88
+ )
89
+ )
90
+ }
91
+
92
+ function getContext(context) {
93
+ return isFunction(context) ? context() : context
94
+ }
95
+
96
+ export function getDefaultValue(schema, context) {
97
+ // Support default values both on schema and on component level.
98
+ // NOTE: At the time of creation, components may not be instantiated, (e.g. if
99
+ // entries are created through nested forms, the parent form isn't mounted) so
100
+ // we can't use `dataPath` to get to components, and the `defaultValue` from
101
+ // there. That's why `defaultValue` is defined statically in the components:
102
+ const defaultValue =
103
+ schema.default !== undefined
104
+ ? schema.default
105
+ : getTypeOptions(schema)?.defaultValue
106
+ return isFunction(defaultValue)
107
+ ? defaultValue(getContext(context))
108
+ : clone(defaultValue)
109
+ }
110
+
111
+ export function shouldExcludeValue(schema, context) {
112
+ const excludeValue =
113
+ schema.exclude !== undefined
114
+ ? schema.exclude
115
+ : getTypeOptions(schema)?.excludeValue
116
+ return isFunction(excludeValue)
117
+ ? excludeValue(getContext(context))
118
+ : !!excludeValue
119
+ }
120
+
121
+ export function shouldIgnoreMissingValue(schema, context) {
122
+ return !!getTypeOptions(schema)?.ignoreMissingValue?.(getContext(context))
123
+ }
124
+
125
+ // Returns whether the value of the component described by `schema` and `name`
126
+ // is missing in `data`: if there's no value, or if the value is `null` and
127
+ // the type treats that as missing, e.g. nested sections.
128
+ export function isMissingValue(schema, data, name, context) {
129
+ return (
130
+ !(name in data) ||
131
+ data[name] === null &&
132
+ !!getTypeOptions(schema)?.treatNullAsMissing?.(getContext(context))
133
+ )
134
+ }
135
+
136
+ /**
137
+ * Returns whether the value of the component described by `schema` is
138
+ * resolved from its data schema, `schema.data` or `schema.dataPath`, as for
139
+ * the types with the `valueFromDataSchema` option, e.g. `computed`. See
140
+ * `FormModel`.
141
+ */
142
+ export function hasValueFromDataSchema(schema) {
143
+ return (
144
+ !!getTypeOptions(schema)?.valueFromDataSchema &&
145
+ !!(schema.data || schema.dataPath)
146
+ )
147
+ }
148
+
149
+ export function getMultipleValue(schema) {
150
+ return schema.multiple ?? !!getTypeOptions(schema)?.defaultMultiple
151
+ }
152
+
153
+ /**
154
+ * Initializes `data` for the components of `schema`: Sets missing values to
155
+ * their defaults, so they can be correctly watched for changes, and numbers
156
+ * the items of lists by their order key, see `updateOrder()`, so that their
157
+ * order is stored even if it never changes.
158
+ */
159
+ export function initializeData(schema, data = {}, component, {
160
+ dataPath = null,
161
+ rootData = data,
162
+ shouldProcess,
163
+ // Whether to also set the defaults of components with `compute()`. New data
164
+ // starts with all defaults, which `compute()` can rely on. In data that was
165
+ // loaded, `compute()` may derive missing values instead, and they fall back
166
+ // to their defaults only if it doesn't return a value, see `computeValue()`.
167
+ shouldSetDefaultsOfComponentsWithCompute = true
168
+ } = {}) {
169
+ const options = { component, rootData }
170
+
171
+ const before = ({ schema, data, name, dataPath }) => {
172
+ const context = () =>
173
+ new DitoContext(component, {
174
+ schema,
175
+ name,
176
+ data,
177
+ dataPath,
178
+ rootData
179
+ })
180
+ if (
181
+ (shouldSetDefaultsOfComponentsWithCompute || !schema.compute) &&
182
+ isMissingValue(schema, data, name, context) &&
183
+ !shouldIgnoreMissingValue(schema, context)
184
+ ) {
185
+ data[name] = getDefaultValue(schema, context)
186
+ }
187
+ if (hasItemsNumberedByOrderKey(schema) && isArray(data[name])) {
188
+ updateOrder(schema, data[name])
189
+ }
190
+ }
191
+
192
+ return processSchemaData(schema, data, {
193
+ dataPath,
194
+ before,
195
+ shouldProcess,
196
+ options
197
+ })
198
+ }
199
+
200
+ /**
201
+ * Returns the value of the component described by `schema` and `name` in
202
+ * `data`: the result of `schema.compute()` if it returns a value, else the
203
+ * value in `data`, or its default if it's missing. Never writes into `data`:
204
+ * Computed values, including the defaults of components with `compute()`, are
205
+ * written by `FormModel`, and other defaults when the data is set up, see
206
+ * `initializeData()`. If provided, `getOptions()` returns the
207
+ * options for `context.options`, called only when they're read.
208
+ */
209
+ export function computeValue(schema, data, name, dataPath, {
210
+ component = null,
211
+ rootData = component?.rootData,
212
+ getOptions = null
213
+ } = {}) {
214
+ const context = () =>
215
+ // Pass a function, so that the `options` getter isn't evaluated when the
216
+ // context is created.
217
+ new DitoContext(component, () => {
218
+ const properties = {
219
+ schema,
220
+ // Override value to prevent endless recursion through calling the
221
+ // getter for `this.value` in `DitoContext`. Read it only when it's
222
+ // used, so that `compute()` only depends on its own value if it reads
223
+ // it, and writing the computed value doesn't call it again.
224
+ get value() {
225
+ return data[name]
226
+ },
227
+ name,
228
+ data,
229
+ dataPath,
230
+ rootData
231
+ }
232
+ if (getOptions) {
233
+ Object.defineProperty(properties, 'options', {
234
+ get: getOptions,
235
+ enumerable: true
236
+ })
237
+ }
238
+ return properties
239
+ })
240
+ const { compute } = schema
241
+ // Like the form model, only compute values of components that are shown
242
+ // through their `if`, as `compute()` may rely on the same conditions, e.g.
243
+ // `if: ({ item }) => item.preview` with `compute: ({ item }) =>
244
+ // item.preview.name`.
245
+ if (
246
+ compute &&
247
+ getSchemaValue('if', {
248
+ type: Boolean,
249
+ schema,
250
+ context: getContext(context),
251
+ default: true
252
+ })
253
+ ) {
254
+ const value = compute(getContext(context))
255
+ if (value !== undefined) {
256
+ return value
257
+ }
258
+ }
259
+ return getValueOrDefault(schema, data, name, context)
260
+ }
261
+
262
+ // Returns the value in `data`, or its default if it's missing. `context` can be
263
+ // a function that creates the context, called only when it's needed.
264
+ function getValueOrDefault(schema, data, name, context) {
265
+ const shouldUseDefault = (
266
+ isMissingValue(schema, data, name, context) &&
267
+ !shouldIgnoreMissingValue(schema, context)
268
+ )
269
+ return shouldUseDefault ? getDefaultValue(schema, context) : data[name]
270
+ }
271
+
272
+ function cloneItem(sourceSchema, item, options) {
273
+ if (options.schemaOnly) {
274
+ const copy = {}
275
+ const { idKey = 'id', orderKey } = sourceSchema
276
+ const id = item[idKey]
277
+ if (id !== undefined) {
278
+ copy[idKey] = id
279
+ }
280
+ // Copy over type in case there are multiple forms to choose from.
281
+ if (hasMultipleFormSchemas(sourceSchema)) {
282
+ copy.type = item.type
283
+ }
284
+ if (orderKey) {
285
+ copy[orderKey] = item[orderKey]
286
+ }
287
+ return copy
288
+ } else {
289
+ return { ...item }
290
+ }
291
+ }
292
+
293
+ export function processData(schema, sourceSchema, data, dataPath, {
294
+ component,
295
+ rootData,
296
+ schemaOnly, // whether to only include data covered by the schema, or all data
297
+ target,
298
+ // Whether to call the schema callbacks `compute()` and `process()`. Data that
299
+ // is compared while it's edited, e.g. by `FormModel.isDirty`, isn't validated
300
+ // yet, which the callbacks may rely on, and `FormModel` writes the computed
301
+ // values into the data. Without them, the values in the data are used, or
302
+ // their defaults if they're missing, the types still process them through
303
+ // `processValue()`, and the excluded values of components with `process()`
304
+ // are kept, as `process()` may store them elsewhere through `processedItem`.
305
+ shouldCallComputeAndProcess = true
306
+ } = {}) {
307
+ const options = { component, rootData, schemaOnly, target }
308
+ const processedData = cloneItem(sourceSchema, data, options)
309
+ const graph = new SchemaGraph()
310
+
311
+ const before = ({ schema, data, name, dataPath, processedData }) => {
312
+ let value = shouldCallComputeAndProcess
313
+ ? computeValue(schema, data, name, dataPath, options)
314
+ : getValueOrDefault(
315
+ schema,
316
+ data,
317
+ name,
318
+ () =>
319
+ new DitoContext(component, {
320
+ schema,
321
+ name,
322
+ data,
323
+ dataPath,
324
+ rootData: options.rootData
325
+ })
326
+ )
327
+ // The schema expects the `wrapPrimitives` transformations to be present on
328
+ // the data that it is applied on, so warp before and unwrap after.
329
+ if (isArray(value)) {
330
+ const { wrapPrimitives } = schema
331
+ if (wrapPrimitives) {
332
+ value = value.map(entry => ({
333
+ [wrapPrimitives]: entry
334
+ }))
335
+ } else {
336
+ // Always shallow-clone array values:
337
+ value = [...value]
338
+ }
339
+ }
340
+ processedData[name] = value
341
+ }
342
+
343
+ const after = ({ schema, data, name, dataPath, processedData }) => {
344
+ const { wrapPrimitives, process } = schema
345
+ let value = processedData[name]
346
+
347
+ // NOTE: We don't cache this context, since `value` is changing.
348
+ const context = () =>
349
+ new DitoContext(component, {
350
+ schema,
351
+ value,
352
+ name,
353
+ data,
354
+ dataPath,
355
+ rootData: options.rootData,
356
+ // Pass the already processed data to `process()`, so it can be modified
357
+ // through `processedItem` from there.
358
+ processedData
359
+ })
360
+
361
+ // First unwrap the wrapped primitives again, to bring the data back into
362
+ // its native form. See `before()` for more details.
363
+ if (wrapPrimitives && isArray(value)) {
364
+ value = value.map(object => object[wrapPrimitives])
365
+ }
366
+
367
+ // Each component type can provide its own static `processValue()` method
368
+ // to convert the data for storage.
369
+ const processValue = getTypeOptions(schema)?.processValue
370
+ if (processValue) {
371
+ value = processValue(getContext(context), graph)
372
+ }
373
+
374
+ // Handle the user's `process()` callback next, if one is provided, so that
375
+ // it can modify data in `processedData` even if it provides `exclude: true`
376
+ if (process && shouldCallComputeAndProcess) {
377
+ value = process(getContext(context))
378
+ }
379
+
380
+ // Without calling `process()`, keep the excluded values that it may store
381
+ // elsewhere, see `shouldCallComputeAndProcess`:
382
+ const shouldKeepExcludedValue = !!process && !shouldCallComputeAndProcess
383
+ if (!shouldKeepExcludedValue && shouldExcludeValue(schema, context)) {
384
+ delete processedData[name]
385
+ } else {
386
+ processedData[name] = value
387
+ }
388
+ }
389
+
390
+ processSchemaData(schema, data, {
391
+ dataPath,
392
+ processedData,
393
+ before,
394
+ after,
395
+ options
396
+ })
397
+
398
+ return graph.process(sourceSchema, processedData, options)
399
+ }
400
+
401
+ /**
402
+ * Walks `data` along the components of `schema`, including nested forms of
403
+ * list and object items, calling `before()` and `after()` for each nested
404
+ * component, e.g. to process the data into `processedData`. `shouldProcess()`
405
+ * is called for all components, tabs and panels, and the ones for which it
406
+ * returns `false` are skipped along with their content. The entries passed to
407
+ * them contain the `componentPath` of the component, tab or panel, continuing
408
+ * `componentPath` like `DitoMixin.componentPath` does.
409
+ */
410
+ export function processSchemaData(schema, data, {
411
+ dataPath = null,
412
+ componentPath = '',
413
+ processedData = null,
414
+ before = null,
415
+ after = null,
416
+ shouldProcess = () => true,
417
+ options
418
+ }) {
419
+ const walkOptions = { before, after, shouldProcess, options }
420
+ const getDataPath = (dataPath, token) =>
421
+ dataPath != null
422
+ ? appendDataPath(dataPath, token)
423
+ : null
424
+
425
+ const processComponents = (components, parentComponentPath) => {
426
+ if (components) {
427
+ for (const [name, componentSchema] of Object.entries(components)) {
428
+ const isNestedComponent = isNested(componentSchema)
429
+ const componentDataPath = isNestedComponent
430
+ ? getDataPath(dataPath, name)
431
+ : dataPath
432
+ const entry = {
433
+ schema: componentSchema,
434
+ data,
435
+ name,
436
+ dataPath: componentDataPath,
437
+ componentPath: appendDataPath(parentComponentPath, name),
438
+ processedData
439
+ }
440
+ if (!shouldProcess(entry)) {
441
+ continue
442
+ }
443
+ if (isPanelWithOwnData(componentSchema)) {
444
+ // The components of panels with their own data don't edit `data`.
445
+ continue
446
+ }
447
+ if (!isNestedComponent) {
448
+ // Recursively process data on unnested components.
449
+ processSchemaData(componentSchema, data, {
450
+ ...walkOptions,
451
+ dataPath,
452
+ componentPath: entry.componentPath,
453
+ processedData
454
+ })
455
+ } else {
456
+ const processItem = (item, index = null) => {
457
+ if (!isObject(item)) {
458
+ // Items that aren't objects can't hold component values, e.g.
459
+ // the unwrapped values of lists with `wrapPrimitives` when
460
+ // walking data that isn't processed.
461
+ return item
462
+ }
463
+ const itemDataPath =
464
+ index !== null
465
+ ? getDataPath(componentDataPath, index)
466
+ : componentDataPath
467
+ const itemComponentPath =
468
+ index !== null
469
+ ? appendDataPath(entry.componentPath, index)
470
+ : entry.componentPath
471
+ const context = new DitoContext(options.component, {
472
+ schema: componentSchema,
473
+ data,
474
+ dataPath: componentDataPath,
475
+ name,
476
+ rootData: options.rootData
477
+ })
478
+ const getForms = (
479
+ getTypeOptions(componentSchema)?.getFormSchemasForProcessing ||
480
+ getFormSchemas
481
+ )
482
+ const forms = getForms(componentSchema, context)
483
+ const form = getItemFormSchemaFromForms(forms, item)
484
+ if (form) {
485
+ const processedItem = processedData
486
+ ? cloneItem(componentSchema, item, options)
487
+ : null
488
+ return processSchemaData(form, item, {
489
+ ...walkOptions,
490
+ dataPath: itemDataPath,
491
+ componentPath: itemComponentPath,
492
+ processedData: processedItem
493
+ })
494
+ } else {
495
+ // Items without a matching form, e.g. of an unknown type, are
496
+ // passed through as shallow clones.
497
+ return { ...item }
498
+ }
499
+ }
500
+
501
+ before?.(entry)
502
+
503
+ let value = processedData ? processedData[name] : data[name]
504
+ if (value != null && hasFormSchema(componentSchema)) {
505
+ // Recursively process data on nested form items.
506
+ if (isArray(value)) {
507
+ // Optimization: No need to collect values if we're not cloning!
508
+ value = processedData
509
+ ? value.map(processItem)
510
+ : value.forEach(processItem)
511
+ } else {
512
+ value = processItem(value)
513
+ }
514
+ if (processedData) {
515
+ processedData[name] = value
516
+ }
517
+ }
518
+
519
+ after?.(entry)
520
+ }
521
+ }
522
+ }
523
+ }
524
+
525
+ const processTabOrPanelSchemas = schemas => {
526
+ // Tabs and panels add their names to component paths, not to data paths.
527
+ for (const [name, tabOrPanelSchema] of Object.entries(schemas || {})) {
528
+ const entry = {
529
+ schema: tabOrPanelSchema,
530
+ data,
531
+ name: null,
532
+ dataPath,
533
+ componentPath: appendDataPath(componentPath, name),
534
+ processedData
535
+ }
536
+ // The components of panels with their own data don't edit `data`.
537
+ if (shouldProcess(entry) && !isPanelWithOwnData(tabOrPanelSchema)) {
538
+ processComponents(tabOrPanelSchema.components, entry.componentPath)
539
+ }
540
+ }
541
+ }
542
+
543
+ processComponents(schema.components, componentPath)
544
+ processTabOrPanelSchemas(schema.tabs)
545
+ processTabOrPanelSchemas(schema.panels)
546
+
547
+ return processedData || data
548
+ }
549
+
550
+ /**
551
+ * Returns the component path of the value at `dataPath` in `data`, by walking
552
+ * `data` along the components of `schema` to the deepest component that
553
+ * contains the value, e.g. `main/chapters/1` for `chapters/1`, with the list
554
+ * `chapters` in the tab `main`. The part of `dataPath` inside that component,
555
+ * e.g. item indices, is appended to its component path. Of several components
556
+ * that display the same data, the first in schema order is used.
557
+ */
558
+ export function getComponentPathByDataPath(schema, data, dataPath, {
559
+ componentPath = '',
560
+ component = null,
561
+ rootData = data
562
+ } = {}) {
563
+ let deepestEntry = { dataPath: '', componentPath }
564
+ if (data) {
565
+ processSchemaData(schema, data, {
566
+ dataPath: '',
567
+ componentPath,
568
+ // Only walk the components that contain the value at `dataPath`:
569
+ shouldProcess: entry => (
570
+ getRelativeDataPath(dataPath, entry.dataPath) !== null
571
+ ),
572
+ before: entry => {
573
+ if (entry.dataPath.length > deepestEntry.dataPath.length) {
574
+ deepestEntry = entry
575
+ }
576
+ },
577
+ options: { component, rootData }
578
+ })
579
+ }
580
+ const relativeDataPath = getRelativeDataPath(dataPath, deepestEntry.dataPath)
581
+ return relativeDataPath
582
+ ? appendDataPath(deepestEntry.componentPath, relativeDataPath)
583
+ : deepestEntry.componentPath
584
+ }
585
+
586
+ export function getItemId(sourceSchema, item) {
587
+ const id = item[sourceSchema.idKey || 'id']
588
+ return id != null ? String(id) : undefined
589
+ }
590
+
591
+ // Returns whether the items of the list described by `schema` are numbered by
592
+ // its `orderKey`, see `updateOrder()`. Lists of primitives have no items that
593
+ // could hold an order key.
594
+ export function hasItemsNumberedByOrderKey(schema) {
595
+ return !!schema.orderKey && !schema.wrapPrimitives
596
+ }
597
+
598
+ export function updateOrder(sourceSchema, list, paginationRange) {
599
+ const { orderKey } = sourceSchema
600
+ if (orderKey) {
601
+ // Reorder the changed entries by their order key, taking pagination
602
+ // offsets into account:
603
+ const offset = paginationRange?.[0] || 0
604
+ for (let i = 0; i < list.length; i++) {
605
+ list[i][orderKey] = i + offset
606
+ }
607
+ }
608
+ return list
609
+ }
610
+
611
+ export function getItemUid(sourceSchema, item) {
612
+ // Try to use the item id as the uid, falling back on auto-generated ids, but
613
+ // either way, pass through `getUid()` so that the ids are associated with the
614
+ // item through a weak map, as the ids can be filtered out in `processData()`
615
+ // while the components that use the uids as key are still visible.
616
+ return getUid(item, item => getItemId(sourceSchema, item))
617
+ }