@radio-garden/ditojs-admin 2.85.2-0.5067ad799

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 (153) hide show
  1. package/README.md +180 -0
  2. package/dist/dito-admin.css +1 -0
  3. package/dist/dito-admin.es.js +12106 -0
  4. package/dist/dito-admin.umd.js +7 -0
  5. package/package.json +96 -0
  6. package/src/DitoAdmin.js +293 -0
  7. package/src/DitoComponent.js +34 -0
  8. package/src/DitoContext.js +318 -0
  9. package/src/DitoTypeComponent.js +42 -0
  10. package/src/DitoUser.js +12 -0
  11. package/src/appState.js +12 -0
  12. package/src/components/DitoAccount.vue +60 -0
  13. package/src/components/DitoAffix.vue +68 -0
  14. package/src/components/DitoAffixes.vue +200 -0
  15. package/src/components/DitoButtons.vue +80 -0
  16. package/src/components/DitoClipboard.vue +186 -0
  17. package/src/components/DitoContainer.vue +374 -0
  18. package/src/components/DitoCreateButton.vue +146 -0
  19. package/src/components/DitoDialog.vue +242 -0
  20. package/src/components/DitoDraggable.vue +117 -0
  21. package/src/components/DitoEditButtons.vue +135 -0
  22. package/src/components/DitoErrors.vue +83 -0
  23. package/src/components/DitoForm.vue +521 -0
  24. package/src/components/DitoFormInner.vue +26 -0
  25. package/src/components/DitoFormNested.vue +17 -0
  26. package/src/components/DitoHeader.vue +84 -0
  27. package/src/components/DitoLabel.vue +200 -0
  28. package/src/components/DitoMenu.vue +186 -0
  29. package/src/components/DitoNavigation.vue +40 -0
  30. package/src/components/DitoNotifications.vue +170 -0
  31. package/src/components/DitoPagination.vue +42 -0
  32. package/src/components/DitoPane.vue +334 -0
  33. package/src/components/DitoPanel.vue +256 -0
  34. package/src/components/DitoPanels.vue +61 -0
  35. package/src/components/DitoRoot.vue +524 -0
  36. package/src/components/DitoSchema.vue +846 -0
  37. package/src/components/DitoSchemaInlined.vue +97 -0
  38. package/src/components/DitoScopes.vue +76 -0
  39. package/src/components/DitoSidebar.vue +50 -0
  40. package/src/components/DitoSpinner.vue +95 -0
  41. package/src/components/DitoTableCell.vue +64 -0
  42. package/src/components/DitoTableHead.vue +121 -0
  43. package/src/components/DitoTabs.vue +103 -0
  44. package/src/components/DitoTrail.vue +124 -0
  45. package/src/components/DitoTreeItem.vue +420 -0
  46. package/src/components/DitoUploadFile.vue +199 -0
  47. package/src/components/DitoVNode.vue +14 -0
  48. package/src/components/DitoView.vue +143 -0
  49. package/src/components/index.js +42 -0
  50. package/src/directives/resize.js +83 -0
  51. package/src/index.js +1 -0
  52. package/src/mixins/ContextMixin.js +68 -0
  53. package/src/mixins/DataMixin.js +131 -0
  54. package/src/mixins/DitoMixin.js +591 -0
  55. package/src/mixins/DomMixin.js +29 -0
  56. package/src/mixins/EmitterMixin.js +158 -0
  57. package/src/mixins/ItemMixin.js +144 -0
  58. package/src/mixins/LoadingMixin.js +23 -0
  59. package/src/mixins/NumberMixin.js +118 -0
  60. package/src/mixins/OptionsMixin.js +304 -0
  61. package/src/mixins/PulldownMixin.js +63 -0
  62. package/src/mixins/ResourceMixin.js +398 -0
  63. package/src/mixins/RouteMixin.js +190 -0
  64. package/src/mixins/SchemaParentMixin.js +33 -0
  65. package/src/mixins/SortableMixin.js +49 -0
  66. package/src/mixins/SourceMixin.js +734 -0
  67. package/src/mixins/TextMixin.js +26 -0
  68. package/src/mixins/TypeMixin.js +280 -0
  69. package/src/mixins/ValidationMixin.js +119 -0
  70. package/src/mixins/ValidatorMixin.js +57 -0
  71. package/src/mixins/ValueMixin.js +31 -0
  72. package/src/styles/_base.scss +17 -0
  73. package/src/styles/_button.scss +191 -0
  74. package/src/styles/_imports.scss +3 -0
  75. package/src/styles/_info.scss +19 -0
  76. package/src/styles/_layout.scss +19 -0
  77. package/src/styles/_pulldown.scss +38 -0
  78. package/src/styles/_scroll.scss +13 -0
  79. package/src/styles/_settings.scss +88 -0
  80. package/src/styles/_table.scss +223 -0
  81. package/src/styles/_tippy.scss +45 -0
  82. package/src/styles/style.scss +9 -0
  83. package/src/types/DitoTypeButton.vue +143 -0
  84. package/src/types/DitoTypeCheckbox.vue +27 -0
  85. package/src/types/DitoTypeCheckboxes.vue +65 -0
  86. package/src/types/DitoTypeCode.vue +199 -0
  87. package/src/types/DitoTypeColor.vue +272 -0
  88. package/src/types/DitoTypeComponent.vue +31 -0
  89. package/src/types/DitoTypeComputed.vue +50 -0
  90. package/src/types/DitoTypeDate.vue +99 -0
  91. package/src/types/DitoTypeLabel.vue +23 -0
  92. package/src/types/DitoTypeList.vue +364 -0
  93. package/src/types/DitoTypeMarkup.vue +700 -0
  94. package/src/types/DitoTypeMultiselect.vue +522 -0
  95. package/src/types/DitoTypeNumber.vue +66 -0
  96. package/src/types/DitoTypeObject.vue +136 -0
  97. package/src/types/DitoTypePanel.vue +18 -0
  98. package/src/types/DitoTypeProgress.vue +40 -0
  99. package/src/types/DitoTypeRadio.vue +45 -0
  100. package/src/types/DitoTypeSection.vue +80 -0
  101. package/src/types/DitoTypeSelect.vue +133 -0
  102. package/src/types/DitoTypeSlider.vue +66 -0
  103. package/src/types/DitoTypeSpacer.vue +11 -0
  104. package/src/types/DitoTypeSwitch.vue +40 -0
  105. package/src/types/DitoTypeText.vue +101 -0
  106. package/src/types/DitoTypeTextarea.vue +48 -0
  107. package/src/types/DitoTypeTreeList.vue +193 -0
  108. package/src/types/DitoTypeUpload.vue +503 -0
  109. package/src/types/index.js +30 -0
  110. package/src/utils/SchemaGraph.js +147 -0
  111. package/src/utils/accessor.js +75 -0
  112. package/src/utils/agent.js +47 -0
  113. package/src/utils/data.js +92 -0
  114. package/src/utils/filter.js +266 -0
  115. package/src/utils/math.js +14 -0
  116. package/src/utils/options.js +48 -0
  117. package/src/utils/path.js +5 -0
  118. package/src/utils/resource.js +44 -0
  119. package/src/utils/route.js +53 -0
  120. package/src/utils/schema.js +1121 -0
  121. package/src/utils/type.js +81 -0
  122. package/src/utils/uid.js +15 -0
  123. package/src/utils/units.js +5 -0
  124. package/src/validators/_creditcard.js +6 -0
  125. package/src/validators/_decimals.js +11 -0
  126. package/src/validators/_domain.js +6 -0
  127. package/src/validators/_email.js +6 -0
  128. package/src/validators/_hostname.js +6 -0
  129. package/src/validators/_integer.js +6 -0
  130. package/src/validators/_max.js +6 -0
  131. package/src/validators/_min.js +6 -0
  132. package/src/validators/_password.js +5 -0
  133. package/src/validators/_range.js +6 -0
  134. package/src/validators/_required.js +9 -0
  135. package/src/validators/_url.js +6 -0
  136. package/src/validators/index.js +12 -0
  137. package/src/verbs.js +17 -0
  138. package/types/index.d.ts +3298 -0
  139. package/types/tests/admin.test-d.ts +27 -0
  140. package/types/tests/component-buttons.test-d.ts +44 -0
  141. package/types/tests/component-list.test-d.ts +159 -0
  142. package/types/tests/component-misc.test-d.ts +137 -0
  143. package/types/tests/component-object.test-d.ts +69 -0
  144. package/types/tests/component-section.test-d.ts +174 -0
  145. package/types/tests/component-select.test-d.ts +107 -0
  146. package/types/tests/components.test-d.ts +81 -0
  147. package/types/tests/context.test-d.ts +31 -0
  148. package/types/tests/fixtures.ts +24 -0
  149. package/types/tests/form.test-d.ts +109 -0
  150. package/types/tests/instance.test-d.ts +20 -0
  151. package/types/tests/schema-features.test-d.ts +402 -0
  152. package/types/tests/variance.test-d.ts +125 -0
  153. package/types/tests/view.test-d.ts +146 -0
@@ -0,0 +1,1121 @@
1
+ import DitoContext from '../DitoContext.js'
2
+ import DitoMixin from '../mixins/DitoMixin.js'
3
+ import TypeMixin from '../mixins/TypeMixin.js'
4
+ import { getUid } from './uid.js'
5
+ import { SchemaGraph } from './SchemaGraph.js'
6
+ import { appendDataPath } from './data.js'
7
+ import { isMatchingType, convertType } from './type.js'
8
+ import {
9
+ isObject,
10
+ isString,
11
+ isArray,
12
+ isFunction,
13
+ isPromise,
14
+ isModule,
15
+ asArray,
16
+ clone,
17
+ camelize,
18
+ assignDeeply,
19
+ mapConcurrently,
20
+ getValueAtDataPath
21
+ } from '@ditojs/utils'
22
+ import { markRaw, reactive } from 'vue'
23
+
24
+ const typeComponents = {}
25
+ const unknownTypeReported = {}
26
+ const emptySchema = {}
27
+
28
+ export function registerTypeComponent(type, component) {
29
+ typeComponents[type] = component
30
+ }
31
+
32
+ export function getTypeComponent(type, allowNull = false) {
33
+ const component = typeComponents[type] || null
34
+ if (!component && !allowNull && !unknownTypeReported[type]) {
35
+ // Report each missing type only once, to avoid flooding the console:
36
+ unknownTypeReported[type] = true
37
+ throw new Error(`Unknown Dito.js component type: '${type}'`)
38
+ }
39
+ return component
40
+ }
41
+
42
+ export function iterateSchemaComponents(schemas, callback) {
43
+ for (const schema of schemas) {
44
+ if (isSingleComponentView(schema)) {
45
+ const res = callback(schema.component, schema.name, 0)
46
+ if (res !== undefined) {
47
+ return res
48
+ }
49
+ } else if (isSchema(schema)) {
50
+ for (const [name, component] of Object.entries(schema.components || {})) {
51
+ const res = callback(component, name, 1)
52
+ if (res !== undefined) {
53
+ return res
54
+ }
55
+ }
56
+ }
57
+ }
58
+ }
59
+
60
+ export function iterateNestedSchemaComponents(schema, callback) {
61
+ return schema
62
+ ? iterateSchemaComponents([schema, ...getTabSchemas(schema)], callback)
63
+ : undefined
64
+ }
65
+
66
+ export function findNestedSchemaComponent(schema, callback) {
67
+ return (
68
+ iterateNestedSchemaComponents(
69
+ schema,
70
+ component => (callback(component) ? component : undefined)
71
+ ) ?? null
72
+ )
73
+ }
74
+
75
+ export function someNestedSchemaComponent(schema, callback) {
76
+ return (
77
+ iterateNestedSchemaComponents(
78
+ schema,
79
+ component => (callback(component) ? true : undefined)
80
+ ) ?? false
81
+ )
82
+ }
83
+
84
+ export function everyNestedSchemaComponent(schema, callback) {
85
+ return (
86
+ iterateNestedSchemaComponents(
87
+ schema,
88
+ component => (callback(component) ? undefined : false)
89
+ ) ?? true
90
+ )
91
+ }
92
+
93
+ export function hasNestedSchemaComponents(schema) {
94
+ return someNestedSchemaComponent(schema, () => true) ?? false
95
+ }
96
+
97
+ export function isSchema(schema) {
98
+ return isObject(schema) && isString(schema.type)
99
+ }
100
+
101
+ export function isForm(schema) {
102
+ return isSchema(schema) && schema.type === 'form'
103
+ }
104
+
105
+ export function isView(schema) {
106
+ return isSchema(schema) && schema.type === 'view'
107
+ }
108
+
109
+ export function isTab(schema) {
110
+ return isSchema(schema) && schema.type === 'tab'
111
+ }
112
+
113
+ export function isPanel(schema) {
114
+ return isSchema(schema) && schema.type === 'panel'
115
+ }
116
+
117
+ export function isMenu(schema) {
118
+ return isSchema(schema) && schema.type === 'menu'
119
+ }
120
+
121
+ export function getSchemaIdentifier(schema) {
122
+ return JSON.stringify(schema)
123
+ }
124
+
125
+ const resolvedSchemas = new WeakMap()
126
+ export async function resolveSchema(value, unwrapModule = false) {
127
+ if (resolvedSchemas.has(value)) {
128
+ return resolvedSchemas.get(value)
129
+ }
130
+ let schema = value
131
+ if (isFunction(schema)) {
132
+ schema = schema()
133
+ }
134
+ if (isPromise(schema)) {
135
+ schema = await schema
136
+ }
137
+ if (isModule(schema)) {
138
+ // Copy to convert from module to object:
139
+ schema = { ...schema }
140
+ // Unwrap default or named schema
141
+ if (!schema.name && (unwrapModule || schema.default)) {
142
+ // Filter out internal key added by vite / vue 3 plugin when changing
143
+ // code in a dynamically imported vue component, see:
144
+ // https://github.com/vitejs/vite-plugin-vue/blob/abdf5f4f32d02af641e5f60871bde14535569b1e/packages/plugin-vue/src/main.ts#L133
145
+ const keys = Object.keys(schema).filter(key => key !== '_rerender_only')
146
+ if (keys.length === 1) {
147
+ const name = keys[0]
148
+ schema = schema[name]
149
+ if (name !== 'default') {
150
+ schema = { ...schema, name }
151
+ }
152
+ }
153
+ }
154
+ }
155
+ resolvedSchemas.set(value, schema)
156
+ return schema
157
+ }
158
+
159
+ export async function resolveSchemas(
160
+ unresolvedSchemas,
161
+ resolveItem = resolveSchema
162
+ ) {
163
+ let schemas = isFunction(unresolvedSchemas)
164
+ ? unresolvedSchemas()
165
+ : unresolvedSchemas
166
+ schemas = await resolveSchema(schemas, false)
167
+ if (isArray(schemas)) {
168
+ // Translate an array of dynamic import, each importing one named schema
169
+ // module to an object with named entries.
170
+ schemas = Object.fromEntries(
171
+ await mapConcurrently(
172
+ schemas,
173
+ async item => {
174
+ const schema = await resolveItem(item, true)
175
+ return [schema.name, schema]
176
+ }
177
+ )
178
+ )
179
+ } else if (isObject(schemas)) {
180
+ schemas = Object.fromEntries(
181
+ await mapConcurrently(
182
+ Object.entries(schemas),
183
+ async ([key, item]) => {
184
+ const schema = await resolveItem(item, true)
185
+ return [key, schema]
186
+ }
187
+ )
188
+ )
189
+ }
190
+ return schemas
191
+ }
192
+
193
+ export async function resolveViews(unresolvedViews) {
194
+ return resolveSchemas(unresolvedViews, async (schema, unwrapModule) => {
195
+ schema = await resolveSchema(schema, unwrapModule)
196
+ if (!schema.name && isMenu(schema)) {
197
+ // Generate a name for sub-menus from their label if it's missing.
198
+ // NOTE: This is never actually referenced from anywhere, but they need
199
+ // a name by which they're stored in the parent object.
200
+ schema = {
201
+ ...schema,
202
+ name: camelize(schema.label),
203
+ items: await resolveSchemas(schema.items)
204
+ }
205
+ }
206
+ return schema
207
+ })
208
+ }
209
+
210
+ export function flattenViews(views) {
211
+ return Object.fromEntries(
212
+ Object.entries(views).reduce(
213
+ (entries, [key, schema]) => {
214
+ if (isMenu(schema)) {
215
+ entries.push(...Object.entries(schema.items))
216
+ } else {
217
+ entries.push([key, schema])
218
+ }
219
+ return entries
220
+ },
221
+ []
222
+ )
223
+ )
224
+ }
225
+
226
+ export async function resolveSchemaComponent(schema) {
227
+ // Resolves async schema components and adds DitoMixin and TypeMixin to them.
228
+ let { component } = schema
229
+ if (component) {
230
+ component = await resolveSchema(component, true)
231
+ if (component) {
232
+ // Prevent warning: "Vue received a Component which was made a reactive
233
+ // object. This can lead to unnecessary performance overhead, and should
234
+ // be avoided by marking the component with `markRaw`":
235
+ schema.component = markRaw({
236
+ ...component,
237
+ mixins: [DitoMixin, TypeMixin, ...(component.mixins || [])]
238
+ })
239
+ }
240
+ }
241
+ }
242
+
243
+ export async function resolveSchemaComponents(schemas) {
244
+ // `schemas` are of the same possible forms as passed to `getNamedSchemas()`
245
+ await mapConcurrently(Object.values(schemas || {}), resolveSchemaComponent)
246
+ }
247
+
248
+ const processedSchemaDepths = new WeakMap()
249
+ export function processSchemaComponents(
250
+ api,
251
+ schema,
252
+ routes = null,
253
+ level = 0,
254
+ maxDepth = 1
255
+ ) {
256
+ if (schema) {
257
+ const depth = processedSchemaDepths.get(schema) ?? 0
258
+ if (depth < maxDepth) {
259
+ processedSchemaDepths.set(schema, depth + 1)
260
+ const promises = []
261
+ const process = (component, name, relativeLevel) => {
262
+ promises.push(
263
+ processSchemaComponent(
264
+ api,
265
+ component,
266
+ name,
267
+ routes,
268
+ level + relativeLevel
269
+ )
270
+ )
271
+ }
272
+
273
+ iterateNestedSchemaComponents(schema, process)
274
+ iterateSchemaComponents(getPanelSchemas(schema), process)
275
+
276
+ return Promise.all(promises)
277
+ }
278
+ }
279
+ }
280
+
281
+ export function processSchemaComponent(
282
+ api,
283
+ schema,
284
+ name,
285
+ routes = null,
286
+ level = 0
287
+ ) {
288
+ processSchemaDefaults(api, schema)
289
+
290
+ return Promise.all([
291
+ // Also process nested panel schemas.
292
+ mapConcurrently(
293
+ getPanelSchemas(schema),
294
+ panel => processSchemaComponents(api, panel, routes, level)
295
+ ),
296
+ // Delegate schema processing to the actual type components.
297
+ getTypeOptions(schema)?.processSchema?.(
298
+ api,
299
+ schema,
300
+ name,
301
+ routes,
302
+ level
303
+ )
304
+ ])
305
+ }
306
+
307
+ export async function processView(component, api, schema, name, fullPath = '') {
308
+ processSchemaDefaults(api, schema)
309
+ processRouteSchema(api, schema, name, fullPath)
310
+ let children = []
311
+ if (isView(schema)) {
312
+ await processNestedSchemas(api, schema)
313
+ await processSchemaComponents(api, schema, children)
314
+ } else if (isMenu(schema)) {
315
+ children = await Promise.all(
316
+ Object.entries(schema.items).map(async ([name, item]) =>
317
+ processView(component, api, item, name, schema.fullPath)
318
+ )
319
+ )
320
+ } else {
321
+ throw new Error(`Invalid view schema: '${getSchemaIdentifier(schema)}'`)
322
+ }
323
+ return {
324
+ path: schema.fullPath,
325
+ children,
326
+ component,
327
+ meta: {
328
+ api,
329
+ schema
330
+ }
331
+ }
332
+ }
333
+
334
+ export function processSchemaDefaults(api, schema) {
335
+ let defaults = (
336
+ api.defaults[schema.type] ||
337
+ api.defaults[camelize(schema.type)]
338
+ )
339
+ if (defaults) {
340
+ if (isFunction(defaults)) {
341
+ defaults = defaults(schema)
342
+ }
343
+ if (isObject(defaults)) {
344
+ for (const [key, value] of Object.entries(defaults)) {
345
+ if (schema[key] === undefined) {
346
+ schema[key] = value
347
+ } else {
348
+ schema[key] = assignDeeply(schema[key], value)
349
+ }
350
+ }
351
+ }
352
+ }
353
+ }
354
+
355
+ export function processNestedSchemaDefaults(api, schema) {
356
+ // Process defaults for nested schemas. Note that this is also done when
357
+ // calling `processSchemaComponents()`, but that function is async, and we
358
+ // need a sync version that only handles the defaults for filters, see
359
+ // `getFiltersPanel()`.
360
+ iterateNestedSchemaComponents(schema, component => {
361
+ processSchemaDefaults(api, component)
362
+ const forms = getFormSchemas(component)
363
+ for (const form of Object.values(forms)) {
364
+ processNestedSchemaDefaults(api, form)
365
+ }
366
+ })
367
+ }
368
+
369
+ export function processRouteSchema(api, schema, name, fullPath = null) {
370
+ // Used for view and source schemas, see SourceMixin.
371
+ schema.name ??= name
372
+ schema.path ??= api.normalizePath(name)
373
+ if (fullPath !== null) {
374
+ schema.fullPath = `${fullPath}/${schema.path}`
375
+ }
376
+ }
377
+
378
+ export async function processForms(api, schema, level) {
379
+ const routes = []
380
+ // First resolve the forms and store the results back on the schema.
381
+ const { form, forms, components, maxDepth = 1 } = schema
382
+ if (forms) {
383
+ schema.forms = await resolveSchemas(forms, form =>
384
+ processForm(api, form, routes, level, maxDepth)
385
+ )
386
+ } else if (form) {
387
+ schema.form = await processForm(api, form, routes, level, maxDepth)
388
+ } else if (isObject(components)) {
389
+ // NOTE: Processing forms in computed components is not supported, since it
390
+ // only can be computed in conjunction with actual data.
391
+ const form = {
392
+ type: 'form',
393
+ components
394
+ }
395
+ await processForm(api, form, routes, level, maxDepth)
396
+ }
397
+ return routes
398
+ }
399
+
400
+ export async function processForm(
401
+ api,
402
+ schema,
403
+ routes = null,
404
+ level = 0,
405
+ maxDepth = 1
406
+ ) {
407
+ schema = await resolveSchema(schema, true)
408
+ if (!isForm(schema)) {
409
+ throw new Error(`Invalid form schema: '${getSchemaIdentifier(schema)}'`)
410
+ }
411
+ processSchemaDefaults(api, schema)
412
+ await processNestedSchemas(api, schema)
413
+ await processSchemaComponents(api, schema, routes, level, maxDepth)
414
+ return schema
415
+ }
416
+
417
+ export async function processTab(api, schema) {
418
+ schema = await resolveSchema(schema, true)
419
+ if (!isTab(schema)) {
420
+ throw new Error(`Invalid tab schema: '${getSchemaIdentifier(schema)}'`)
421
+ }
422
+ processSchemaDefaults(api, schema)
423
+ return schema
424
+ }
425
+
426
+ export async function processPanel(api, schema) {
427
+ schema = await resolveSchema(schema, true)
428
+ if (!isPanel(schema)) {
429
+ throw new Error(`Invalid panel schema: '${getSchemaIdentifier(schema)}'`)
430
+ }
431
+ processSchemaDefaults(api, schema)
432
+ return schema
433
+ }
434
+
435
+ export async function processNestedSchemas(api, schema) {
436
+ const { tabs, panels } = schema
437
+ if (tabs) {
438
+ schema.tabs = await resolveSchemas(
439
+ tabs,
440
+ tab => processTab(api, tab)
441
+ )
442
+ }
443
+ if (panels) {
444
+ schema.panels = await resolveSchemas(
445
+ panels,
446
+ panel => processPanel(api, panel)
447
+ )
448
+ }
449
+ }
450
+
451
+ export function hasFormSchema(schema) {
452
+ // Support both single form and multiple forms notation, as well as inlined
453
+ // components.
454
+ return (
455
+ isSchema(schema) &&
456
+ isObject(schema.form || schema.forms || schema.components)
457
+ )
458
+ }
459
+
460
+ export function hasMultipleFormSchemas(schema) {
461
+ return (
462
+ isSchema(schema) &&
463
+ Object.keys(schema?.forms || {}).length > 1
464
+ )
465
+ }
466
+
467
+ export function isSingleComponentView(schema) {
468
+ return (
469
+ isView(schema) &&
470
+ isObject(schema.component)
471
+ )
472
+ }
473
+
474
+ export function getViewFormSchema(schema, context) {
475
+ const { view } = schema
476
+ const viewSchema = view && context.flattenedViews[view]
477
+ return viewSchema
478
+ ? // NOTE: Views can have tabs, in which case the view component is nested
479
+ // in one of the tabs, go find it.
480
+ findNestedSchemaComponent(viewSchema, hasFormSchema) || null
481
+ : null
482
+ }
483
+
484
+ export function getViewSchema(schema, context) {
485
+ return getViewFormSchema(schema, context)
486
+ ? context.flattenedViews[schema.view]
487
+ : null
488
+ }
489
+
490
+ export function hasViewSchema(schema, context) {
491
+ return !!getViewSchema(schema, context)
492
+ }
493
+
494
+ export function getViewPath(schema, context) {
495
+ const view = getViewSchema(schema, context)
496
+ if (view) {
497
+ return isSingleComponentView(view)
498
+ ? view.fullPath
499
+ : `${view.fullPath}/${view.path}`
500
+ }
501
+ return null
502
+ }
503
+
504
+ export function getViewEditPath(schema, id, context) {
505
+ const path = getViewPath(schema, context)
506
+ return path ? `${path}/${id}` : null
507
+ }
508
+
509
+ export function getFormSchemas(schema, context, modifyForm) {
510
+ const viewSchema = context && getViewFormSchema(schema, context)
511
+ if (viewSchema) {
512
+ schema = viewSchema
513
+ } else if (schema.view) {
514
+ throw new Error(`Unknown view: '${schema.view}'`)
515
+ }
516
+
517
+ let { form, forms } = schema
518
+ if (!form && !forms) {
519
+ const { name, compact, clipboard, tabs, components } = schema
520
+ if (components || tabs) {
521
+ // Convert inlined forms to stand-alone forms, supporting `name`,
522
+ // `compact`, `clipboard`, `tabs` and `components` settings.
523
+ form = { type: 'form', name, compact, clipboard, tabs, components }
524
+ } else {
525
+ // No `forms`, `form` or `components`, return and empty `forms` object.
526
+ return {}
527
+ }
528
+ }
529
+ forms ||= { default: form }
530
+ return Object.fromEntries(
531
+ Object.entries(forms).map(([type, form]) => {
532
+ // Support `schema.components` callbacks to create components on the fly.
533
+ if (context && isFunction(form.components)) {
534
+ // Make the form schema reactive since `processForm()` is async, so that
535
+ // the setting of defaults will be picked up by downstream code.
536
+ form = reactive({
537
+ ...form,
538
+ components: form.components(context)
539
+ })
540
+ // Process the form again, now that we have the components.
541
+ processForm(context.api, form).catch(console.error)
542
+ }
543
+ return [type, modifyForm?.(form) ?? form]
544
+ })
545
+ )
546
+ }
547
+
548
+ export function getItemFormSchemaFromForms(forms, item) {
549
+ return forms[item?.type] || forms.default || null
550
+ }
551
+
552
+ export function getItemFormSchema(schema, item, context) {
553
+ return (
554
+ getItemFormSchemaFromForms(getFormSchemas(schema, context), item) ||
555
+ // Always return a schema object so we don't need to check for it.
556
+ emptySchema
557
+ )
558
+ }
559
+
560
+ export function isEmptySchema(schema) {
561
+ return schema === emptySchema
562
+ }
563
+
564
+ export function isCompact(schema) {
565
+ return !!schema.compact
566
+ }
567
+
568
+ export function isInlined(schema) {
569
+ return !!(schema.inlined || schema.components)
570
+ }
571
+
572
+ export function isNested(schema) {
573
+ return !!(schema.nested || getTypeOptions(schema)?.defaultNested === true)
574
+ }
575
+
576
+ export function hasLabel(schema, generateLabels) {
577
+ const { label } = schema
578
+ return (
579
+ label !== false && (
580
+ !!label ||
581
+ generateLabels && getTypeOptions(schema)?.generateLabel
582
+ )
583
+ )
584
+ }
585
+
586
+ export function omitSpacing(schema) {
587
+ return !!getTypeOptions(schema)?.omitSpacing
588
+ }
589
+
590
+ export function getSchemaValue(
591
+ keyOrDataPath,
592
+ { type, schema, callback = true, default: def, context } = {}
593
+ ) {
594
+ const types = type && asArray(type)
595
+ // For performance reasons, data-paths in `keyOrDataPath` can only be
596
+ // provided in in array format here:
597
+ let value = schema
598
+ ? isArray(keyOrDataPath)
599
+ ? getValueAtDataPath(schema, keyOrDataPath, () => undefined)
600
+ : schema[keyOrDataPath]
601
+ : undefined
602
+
603
+ if (value === undefined && def !== undefined) {
604
+ if (callback && isFunction(def) && !isMatchingType(types, def)) {
605
+ // Support `default()` functions for any type except `Function`:
606
+ def = def(context)
607
+ }
608
+ return def
609
+ }
610
+
611
+ if (isMatchingType(types, value)) {
612
+ return value
613
+ }
614
+ // Any schema value handled through `getSchemaValue()` can provide
615
+ // a function that's resolved when the value is evaluated:
616
+ if (callback && isFunction(value)) {
617
+ value = value(context)
618
+ }
619
+ // Now finally see if we can convert to the expect types.
620
+ if (types && value != null && !isMatchingType(types, value)) {
621
+ for (const type of types) {
622
+ const converted = convertType(type, value)
623
+ if (converted !== value) {
624
+ return converted
625
+ }
626
+ }
627
+ }
628
+ return value
629
+ }
630
+
631
+ export function shouldRenderSchema(schema, context) {
632
+ return (
633
+ getSchemaValue('if', {
634
+ type: Boolean,
635
+ schema,
636
+ context,
637
+ default: true
638
+ }) && (
639
+ !hasNestedSchemaComponents(schema) ||
640
+ someNestedSchemaComponent(schema, component =>
641
+ shouldRenderSchema(component, context)
642
+ )
643
+ )
644
+ )
645
+ }
646
+
647
+ function getContext(context) {
648
+ return isFunction(context) ? context() : context
649
+ }
650
+
651
+ export function getDefaultValue(schema, context) {
652
+ // Support default values both on schema and on component level.
653
+ // NOTE: At the time of creation, components may not be instantiated, (e.g. if
654
+ // entries are created through nested forms, the parent form isn't mounted) so
655
+ // we can't use `dataPath` to get to components, and the `defaultValue` from
656
+ // there. That's why `defaultValue` is defined statically in the components:
657
+ const defaultValue =
658
+ schema.default !== undefined
659
+ ? schema.default
660
+ : getTypeOptions(schema)?.defaultValue
661
+ return isFunction(defaultValue)
662
+ ? defaultValue(getContext(context))
663
+ : clone(defaultValue)
664
+ }
665
+
666
+ export function shouldExcludeValue(schema, context) {
667
+ const excludeValue =
668
+ schema.exclude !== undefined
669
+ ? schema.exclude
670
+ : getTypeOptions(schema)?.excludeValue
671
+ return isFunction(excludeValue)
672
+ ? excludeValue(getContext(context))
673
+ : !!excludeValue
674
+ }
675
+
676
+ export function shouldIgnoreMissingValue(schema, context) {
677
+ return !!getTypeOptions(schema)?.ignoreMissingValue?.(getContext(context))
678
+ }
679
+
680
+ export function getMultipleValue(schema) {
681
+ return schema.multiple ?? !!getTypeOptions(schema)?.defaultMultiple
682
+ }
683
+
684
+ export function setDefaultValues(schema, data = {}, component) {
685
+ const options = { component, rootData: data }
686
+
687
+ const processBefore = (schema, data, name, dataPath) => {
688
+ const context = () =>
689
+ new DitoContext(component, {
690
+ schema,
691
+ name,
692
+ data,
693
+ dataPath,
694
+ rootData: options.rootData
695
+ })
696
+ if (!(name in data) && !shouldIgnoreMissingValue(schema, context)) {
697
+ data[name] = getDefaultValue(schema, context)
698
+ }
699
+ }
700
+
701
+ // Sets up a data object that has keys with default values for all
702
+ // form fields, so they can be correctly watched for changes.
703
+ return processSchemaData(
704
+ schema,
705
+ data,
706
+ null,
707
+ null,
708
+ processBefore,
709
+ null,
710
+ options
711
+ )
712
+ }
713
+
714
+ export function computeValue(schema, data, name, dataPath, {
715
+ component = null,
716
+ rootData = component?.rootData
717
+ } = {}) {
718
+ const context = () =>
719
+ new DitoContext(component, {
720
+ schema,
721
+ // Override value to prevent endless recursion through calling the
722
+ // getter for `this.value` in `DitoContext`:
723
+ value: data[name],
724
+ name,
725
+ data,
726
+ dataPath,
727
+ rootData
728
+ })
729
+ const { compute } = schema
730
+ if (compute) {
731
+ const value = compute(getContext(context))
732
+ if (value !== undefined) {
733
+ // Access `data[name]` directly to update the value without calling
734
+ // parse():
735
+ // TODO: Fix side-effects
736
+ data[name] = value
737
+ }
738
+ }
739
+ // If the value is still missing after compute, set the default for it:
740
+ if (!(name in data) && !shouldIgnoreMissingValue(schema, context)) {
741
+ // TODO: Fix side-effects
742
+ data[name] = getDefaultValue(schema, context)
743
+ }
744
+ // Now access the value. This is important for reactivity and needs to
745
+ // happen after all prior manipulation of `data[name]`, see above:
746
+ return data[name]
747
+ }
748
+
749
+ function cloneItem(sourceSchema, item, options) {
750
+ if (options.schemaOnly) {
751
+ const copy = {}
752
+ const { idKey = 'id', orderKey } = sourceSchema
753
+ const id = item[idKey]
754
+ if (id !== undefined) {
755
+ copy[idKey] = id
756
+ }
757
+ // Copy over type in case there are multiple forms to choose from.
758
+ if (hasMultipleFormSchemas(sourceSchema)) {
759
+ copy.type = item.type
760
+ }
761
+ if (orderKey) {
762
+ copy[orderKey] = item[orderKey]
763
+ }
764
+ return copy
765
+ } else {
766
+ return { ...item }
767
+ }
768
+ }
769
+
770
+ export function processData(schema, sourceSchema, data, dataPath, {
771
+ component,
772
+ rootData,
773
+ schemaOnly, // whether to only include data covered by the schema, or all data
774
+ target
775
+ } = {}) {
776
+ const options = { component, rootData, schemaOnly, target }
777
+ const processedData = cloneItem(sourceSchema, data, options)
778
+ const graph = new SchemaGraph()
779
+
780
+ const processBefore = (schema, data, name, dataPath, processedData) => {
781
+ let value = computeValue(schema, data, name, dataPath, options)
782
+ // The schema expects the `wrapPrimitives` transformations to be present on
783
+ // the data that it is applied on, so warp before and unwrap after.
784
+ if (isArray(value)) {
785
+ const { wrapPrimitives } = schema
786
+ if (wrapPrimitives) {
787
+ value = value.map(entry => ({
788
+ [wrapPrimitives]: entry
789
+ }))
790
+ } else {
791
+ // Always shallow-clone array values:
792
+ value = [...value]
793
+ }
794
+ }
795
+ processedData[name] = value
796
+ }
797
+
798
+ const processAfter = (schema, data, name, dataPath, processedData) => {
799
+ const { wrapPrimitives, process } = schema
800
+ let value = processedData[name]
801
+
802
+ // NOTE: We don't cache this context, since `value` is changing.
803
+ const context = () =>
804
+ new DitoContext(component, {
805
+ schema,
806
+ value,
807
+ name,
808
+ data,
809
+ dataPath,
810
+ rootData: options.rootData,
811
+ // Pass the already processed data to `process()`, so it can be modified
812
+ // through `processedItem` from there.
813
+ processedData
814
+ })
815
+
816
+ // First unwrap the wrapped primitives again, to bring the data back into
817
+ // its native form. Se `processBefore()` for more details.
818
+ if (wrapPrimitives && isArray(value)) {
819
+ value = value.map(object => object[wrapPrimitives])
820
+ }
821
+
822
+ // Each component type can provide its own static `processValue()` method
823
+ // to convert the data for storage.
824
+ const processValue = getTypeOptions(schema)?.processValue
825
+ if (processValue) {
826
+ value = processValue(getContext(context), graph)
827
+ }
828
+
829
+ // Handle the user's `process()` callback next, if one is provided, so that
830
+ // it can modify data in `processedData` even if it provides `exclude: true`
831
+ if (process) {
832
+ value = process(getContext(context))
833
+ }
834
+
835
+ if (shouldExcludeValue(schema, context)) {
836
+ delete processedData[name]
837
+ } else {
838
+ processedData[name] = value
839
+ }
840
+ }
841
+
842
+ processSchemaData(
843
+ schema,
844
+ data,
845
+ dataPath,
846
+ processedData,
847
+ processBefore,
848
+ processAfter,
849
+ options
850
+ )
851
+
852
+ return graph.process(sourceSchema, processedData, options)
853
+ }
854
+
855
+ export function processSchemaData(
856
+ schema,
857
+ data,
858
+ dataPath,
859
+ processedData,
860
+ processBefore,
861
+ processAfter,
862
+ options
863
+ ) {
864
+ const processComponents = components => {
865
+ const getDataPath = (dataPath, token) =>
866
+ dataPath != null
867
+ ? appendDataPath(dataPath, token)
868
+ : null
869
+
870
+ if (components) {
871
+ for (const [name, componentSchema] of Object.entries(components)) {
872
+ if (!isNested(componentSchema)) {
873
+ // Recursively process data on unnested components.
874
+ processSchemaData(
875
+ componentSchema,
876
+ data,
877
+ dataPath,
878
+ processedData,
879
+ processBefore,
880
+ processAfter,
881
+ options
882
+ )
883
+ } else {
884
+ const componentDataPath = getDataPath(dataPath, name)
885
+
886
+ const processItem = (item, index = null) => {
887
+ const itemDataPath =
888
+ index !== null
889
+ ? getDataPath(componentDataPath, index)
890
+ : componentDataPath
891
+ const context = new DitoContext(options.component, {
892
+ schema: componentSchema,
893
+ data,
894
+ dataPath: componentDataPath,
895
+ name,
896
+ rootData: options.rootData
897
+ })
898
+ const getForms = (
899
+ getTypeOptions(componentSchema)?.getFormSchemasForProcessing ||
900
+ getFormSchemas
901
+ )
902
+ const forms = getForms(componentSchema, context)
903
+ const form = getItemFormSchemaFromForms(forms, item)
904
+ if (form) {
905
+ const processedItem = processedData
906
+ ? cloneItem(componentSchema, item, options)
907
+ : null
908
+ return processSchemaData(
909
+ form,
910
+ item,
911
+ itemDataPath,
912
+ processedItem,
913
+ processBefore,
914
+ processAfter,
915
+ options
916
+ )
917
+ } else {
918
+ // Items without forms still get fully (but shallowly) cloned.
919
+ // TODO: Find out of this is actually needed / used at all?
920
+ return { ...item }
921
+ }
922
+ }
923
+
924
+ processBefore?.(
925
+ componentSchema,
926
+ data,
927
+ name,
928
+ componentDataPath,
929
+ processedData
930
+ )
931
+
932
+ let value = processedData ? processedData[name] : data[name]
933
+ if (value != null && hasFormSchema(componentSchema)) {
934
+ // Recursively process data on nested form items.
935
+ if (isArray(value)) {
936
+ // Optimization: No need to collect values if we're not cloning!
937
+ value = processedData
938
+ ? value.map(processItem)
939
+ : value.forEach(processItem)
940
+ } else {
941
+ value = processItem(value)
942
+ }
943
+ if (processedData) {
944
+ processedData[name] = value
945
+ }
946
+ }
947
+
948
+ processAfter?.(
949
+ componentSchema,
950
+ data,
951
+ name,
952
+ componentDataPath,
953
+ processedData
954
+ )
955
+ }
956
+ }
957
+ }
958
+ }
959
+
960
+ processComponents(schema.components)
961
+ for (const tab of getTabSchemas(schema)) {
962
+ processComponents(tab.components)
963
+ }
964
+ for (const panel of getPanelSchemas(schema)) {
965
+ processComponents(panel.components)
966
+ }
967
+
968
+ return processedData || data
969
+ }
970
+
971
+ export function getNamedSchemas(schemas, defaults) {
972
+ const toObject = (array, toSchema) => {
973
+ return array.length > 0
974
+ ? array.reduce((object, value) => {
975
+ const schema = toSchema(value)
976
+ if (schema) {
977
+ object[schema.name] =
978
+ schema && defaults
979
+ ? { ...defaults, ...schema }
980
+ : schema
981
+ }
982
+ return object
983
+ }, {})
984
+ : null
985
+ }
986
+
987
+ return isArray(schemas)
988
+ ? toObject(schemas, value =>
989
+ isObject(value)
990
+ ? value
991
+ : {
992
+ name: camelize(value, false)
993
+ }
994
+ )
995
+ : isObject(schemas)
996
+ ? toObject(
997
+ Object.entries(schemas),
998
+ ([name, value]) =>
999
+ isObject(value)
1000
+ ? {
1001
+ name,
1002
+ ...value
1003
+ }
1004
+ : isString(value)
1005
+ ? {
1006
+ name,
1007
+ label: value
1008
+ }
1009
+ : null
1010
+ )
1011
+ : null
1012
+ }
1013
+
1014
+ export function getButtonSchemas(buttons) {
1015
+ return getNamedSchemas(
1016
+ buttons,
1017
+ { type: 'button' } // Defaults
1018
+ )
1019
+ }
1020
+
1021
+ function getType(schemaOrType) {
1022
+ return isObject(schemaOrType) ? schemaOrType.type : schemaOrType
1023
+ }
1024
+
1025
+ export function getTypeOptions(schemaOrType) {
1026
+ return getTypeComponent(getType(schemaOrType), true) ?? null
1027
+ }
1028
+
1029
+ export function getSourceType(schemaOrType) {
1030
+ return (
1031
+ getTypeOptions(schemaOrType)?.getSourceType?.(getType(schemaOrType)) ??
1032
+ null
1033
+ )
1034
+ }
1035
+
1036
+ export function getPanelEntry(schema, dataPath = null, tabComponent = null) {
1037
+ return schema
1038
+ ? {
1039
+ schema,
1040
+ // If the panel provides its own name, append it to the dataPath.
1041
+ // This is used e.g. for $filters panels.
1042
+ dataPath:
1043
+ dataPath != null && schema.name
1044
+ ? appendDataPath(dataPath, schema.name)
1045
+ : dataPath,
1046
+ tabComponent
1047
+ }
1048
+ : null
1049
+ }
1050
+
1051
+ export function getPanelEntries(
1052
+ panelSchemas,
1053
+ dataPath,
1054
+ tabComponent = null,
1055
+ panelEntries = []
1056
+ ) {
1057
+ if (panelSchemas) {
1058
+ for (const [key, schema] of Object.entries(panelSchemas)) {
1059
+ const entry = getPanelEntry(
1060
+ schema,
1061
+ appendDataPath(dataPath, key),
1062
+ tabComponent
1063
+ )
1064
+ if (entry) {
1065
+ panelEntries.push(entry)
1066
+ }
1067
+ }
1068
+ }
1069
+ return panelEntries
1070
+ }
1071
+
1072
+ export function getTabSchemas(schema) {
1073
+ return schema?.tabs ? Object.values(schema.tabs) : []
1074
+ }
1075
+
1076
+ export function getPanelSchemas(schema) {
1077
+ return schema?.panels ? Object.values(schema.panels) : []
1078
+ }
1079
+
1080
+ export function getAllPanelEntries(
1081
+ api,
1082
+ schema,
1083
+ dataPath,
1084
+ component = null,
1085
+ tabComponent = null
1086
+ ) {
1087
+ const panelSchema = getTypeOptions(schema)?.getPanelSchema?.(
1088
+ api,
1089
+ schema,
1090
+ dataPath,
1091
+ component
1092
+ )
1093
+ const panelEntries = panelSchema
1094
+ ? [getPanelEntry(panelSchema, dataPath, tabComponent)]
1095
+ : []
1096
+ // Allow each component to provide its own set of panels, in
1097
+ // addition to the default one (e.g. getFiltersPanel(), $filters):
1098
+ getPanelEntries(schema?.panels, dataPath, tabComponent, panelEntries)
1099
+ return panelEntries
1100
+ }
1101
+
1102
+ export function isObjectSource(schemaOrType) {
1103
+ return getSourceType(schemaOrType) === 'object'
1104
+ }
1105
+
1106
+ export function isListSource(schemaOrType) {
1107
+ return getSourceType(schemaOrType) === 'list'
1108
+ }
1109
+
1110
+ export function getItemId(sourceSchema, item) {
1111
+ const id = item[sourceSchema.idKey || 'id']
1112
+ return id != null ? String(id) : undefined
1113
+ }
1114
+
1115
+ export function getItemUid(sourceSchema, item) {
1116
+ // Try to use the item id as the uid, falling back on auto-generated ids, but
1117
+ // either way, pass through `getUid()` so that the ids are associated with the
1118
+ // item through a weak map, as the ids can be filtered out in `processData()`
1119
+ // while the components that use the uids as key are still visible.
1120
+ return getUid(item, item => getItemId(sourceSchema, item))
1121
+ }