@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,734 @@
1
+ import DitoComponent from '../DitoComponent.js'
2
+ import ItemMixin from './ItemMixin.js'
3
+ import ResourceMixin from './ResourceMixin.js'
4
+ import SchemaParentMixin from '../mixins/SchemaParentMixin.js'
5
+ import { getSchemaAccessor, getStoreAccessor } from '../utils/accessor.js'
6
+ import { getMemberResource } from '../utils/resource.js'
7
+ import { replaceRoute } from '../utils/route.js'
8
+ import {
9
+ processRouteSchema,
10
+ processForms,
11
+ getNamedSchemas,
12
+ getButtonSchemas,
13
+ hasFormSchema,
14
+ getFormSchemas,
15
+ getViewSchema,
16
+ getViewPath,
17
+ isCompact,
18
+ isInlined,
19
+ isObjectSource,
20
+ isListSource
21
+ } from '../utils/schema.js'
22
+ import {
23
+ isObject,
24
+ isString,
25
+ isArray,
26
+ isNumber,
27
+ equals,
28
+ parseDataPath,
29
+ normalizeDataPath
30
+ } from '@ditojs/utils'
31
+ import { raw } from '@ditojs/ui'
32
+
33
+ // @vue/component
34
+ export default {
35
+ mixins: [ItemMixin, ResourceMixin, SchemaParentMixin],
36
+
37
+ defaultValue: context => (isListSource(context.schema) ? [] : null),
38
+ // Exclude all sources that have their own resource handling the data.
39
+ excludeValue: context => !!context.schema.resource,
40
+
41
+ provide() {
42
+ return {
43
+ $sourceComponent: () => this
44
+ }
45
+ },
46
+
47
+ data() {
48
+ return {
49
+ wrappedPrimitives: null,
50
+ unwrappingPrimitives: raw(false)
51
+ }
52
+ },
53
+
54
+ computed: {
55
+ sourceComponent() {
56
+ return this
57
+ },
58
+
59
+ isObjectSource() {
60
+ return isObjectSource(this.type)
61
+ },
62
+
63
+ isListSource() {
64
+ return isListSource(this.type)
65
+ },
66
+
67
+ // @override ResourceMixin.hasData()
68
+ hasData() {
69
+ return !!this.value
70
+ },
71
+
72
+ shouldRender() {
73
+ return this.sourceDepth < this.maxDepth
74
+ },
75
+
76
+ isReady() {
77
+ // Lists that have no data and no associated resource should still render,
78
+ // as they may be getting their data elsewhere, e.g. `compute()`.
79
+ return (
80
+ this.shouldRender &&
81
+ (this.hasData || !this.providesData)
82
+ )
83
+ },
84
+
85
+ isInView() {
86
+ return !!this.viewComponent
87
+ },
88
+
89
+ wrapPrimitives() {
90
+ return this.schema.wrapPrimitives
91
+ },
92
+
93
+ listData: {
94
+ get() {
95
+ let data = this.value
96
+ if (this.isObjectSource) {
97
+ // Convert to list array.
98
+ data = data != null ? [data] : []
99
+ } else {
100
+ // If data gets inherited from parent, unwrapping is not happening
101
+ // at the root in `setData()`, but here instead.
102
+ data = this.unwrapListData(data) || data
103
+ }
104
+ data ||= []
105
+ const { wrapPrimitives } = this
106
+ if (wrapPrimitives) {
107
+ if (this.unwrappingPrimitives.value) {
108
+ // We're done unwrapping once `listData` is reevaluated, so set
109
+ // this to `false` again. See `wrappedPrimitives` watcher above.
110
+ // TODO: Fix side-effects
111
+ // eslint-disable-next-line max-len
112
+ // eslint-disable-next-line vue/no-side-effects-in-computed-properties
113
+ this.unwrappingPrimitives.value = false
114
+ } else {
115
+ // Convert data to a list of wrapped primitives, and return it.
116
+ // TODO: Fix side-effects
117
+ // eslint-disable-next-line max-len
118
+ // eslint-disable-next-line vue/no-side-effects-in-computed-properties
119
+ this.wrappedPrimitives = data.map(value => ({
120
+ [wrapPrimitives]: value
121
+ }))
122
+ }
123
+ return this.wrappedPrimitives
124
+ }
125
+ return data
126
+ },
127
+
128
+ set(data) {
129
+ if (this.wrapPrimitives) {
130
+ this.wrappedPrimitives = data
131
+ } else {
132
+ this.value = this.isObjectSource
133
+ ? data?.length > 0
134
+ ? data[0]
135
+ : null
136
+ : data
137
+ }
138
+ }
139
+ },
140
+
141
+ objectData: {
142
+ get() {
143
+ // Always go through `listData` internally, which does all the
144
+ // processing of `wrapPrimitives`, etc.
145
+ return this.listData[0] || null
146
+ },
147
+
148
+ set(data) {
149
+ this.listData = data ? [data] : []
150
+ }
151
+ },
152
+
153
+ sourceSchema() {
154
+ // The sourceSchema of a list is the list's schema itself.
155
+ return this.schema
156
+ },
157
+
158
+ sourceDepth() {
159
+ return this.$route.matched.reduce(
160
+ (depth, record) => (
161
+ depth + (record.meta.schema === this.sourceSchema ? 1 : 0)
162
+ ),
163
+ 0
164
+ )
165
+ },
166
+
167
+ path() {
168
+ // This is used in TypeList for DitoFormChooser.
169
+ return this.routeComponent.getChildPath(this.schema.path)
170
+ },
171
+
172
+ defaultQuery() {
173
+ const { defaultOrder: order } = this
174
+ return order ? { order } : {}
175
+ },
176
+
177
+ query: getStoreAccessor('query', {
178
+ get(query) {
179
+ return {
180
+ ...this.defaultQuery,
181
+ ...query
182
+ }
183
+ },
184
+
185
+ set(query) {
186
+ // Always keep the displayed query parameters in sync with the stored
187
+ // ones. Use scope and page from the list schema as defaults, but allow
188
+ // the route query parameters to override them.
189
+ const {
190
+ scope = this.defaultScope?.name,
191
+ page = this.schema.page,
192
+ type
193
+ } = this.query
194
+ // Preserve / merge currently stored values, including any custom query
195
+ // parameters added by creatable.query
196
+ query = {
197
+ ...this.query,
198
+ ...(scope != null && { scope }),
199
+ ...(page != null && { page }),
200
+ ...(type != null && { type }),
201
+ ...query
202
+ }
203
+ if (!equals(query, this.$route.query)) {
204
+ // Change the route query parameters, but don't trigger a route
205
+ // change, as that would cause the list to reload.
206
+ replaceRoute({ query })
207
+ }
208
+ return query // Let getStoreAccessor() do the actual setting
209
+ }
210
+ }),
211
+
212
+ total: getStoreAccessor('total'),
213
+
214
+ columns() {
215
+ return getNamedSchemas(this.schema.columns)
216
+ },
217
+
218
+ scopes() {
219
+ return getNamedSchemas(this.schema.scopes)
220
+ },
221
+
222
+ defaultScope() {
223
+ let first = null
224
+ if (this.scopes) {
225
+ for (const scope of Object.values(this.scopes)) {
226
+ if (scope.defaultScope) {
227
+ return scope
228
+ }
229
+ if (!first) {
230
+ first = scope
231
+ }
232
+ }
233
+ }
234
+ return first
235
+ },
236
+
237
+ defaultOrder() {
238
+ if (this.columns) {
239
+ for (const column of Object.values(this.columns)) {
240
+ const { defaultSort } = column
241
+ if (defaultSort) {
242
+ const direction = isString(defaultSort) ? defaultSort : 'asc'
243
+ return `${column.name} ${direction}`
244
+ }
245
+ }
246
+ }
247
+ return null
248
+ },
249
+
250
+ nestedMeta() {
251
+ return {
252
+ ...this.meta,
253
+ schema: this.schema
254
+ }
255
+ },
256
+
257
+ forms() {
258
+ return Object.values(getFormSchemas(this.schema, this.context))
259
+ },
260
+
261
+ // Returns the linked view schema if this source edits it its items through
262
+ // a linked view.
263
+ view() {
264
+ return getViewSchema(this.schema, this.context)
265
+ },
266
+
267
+ linksToView() {
268
+ return !!this.view
269
+ },
270
+
271
+ buttonSchemas() {
272
+ return getButtonSchemas(this.schema.buttons)
273
+ },
274
+
275
+ isCompact() {
276
+ return this.forms.every(isCompact)
277
+ },
278
+
279
+ isInlined() {
280
+ return isInlined(this.schema)
281
+ },
282
+
283
+ paginate: getSchemaAccessor('paginate', {
284
+ type: Number
285
+ }),
286
+
287
+ render: getSchemaAccessor('render', {
288
+ type: Function,
289
+ default: null
290
+ }),
291
+
292
+ creatable: getSchemaAccessor('creatable', {
293
+ type: Boolean,
294
+ default: false,
295
+ get(creatable) {
296
+ return creatable && hasFormSchema(this.schema)
297
+ ? this.isObjectSource
298
+ ? !this.value
299
+ : true
300
+ : false
301
+ }
302
+ }),
303
+
304
+ editable: getSchemaAccessor('editable', {
305
+ type: Boolean,
306
+ default: false,
307
+ get(editable) {
308
+ return editable && !this.isInlined
309
+ }
310
+ }),
311
+
312
+ deletable: getSchemaAccessor('deletable', {
313
+ type: Boolean,
314
+ default: false
315
+ }),
316
+
317
+ draggable: getSchemaAccessor('draggable', {
318
+ type: Boolean,
319
+ default: false,
320
+ get(draggable) {
321
+ return this.isListSource && this.listData.length > 1 && draggable
322
+ }
323
+ }),
324
+
325
+ collapsible: getSchemaAccessor('collapsible', {
326
+ type: Boolean,
327
+ default: false,
328
+ get(collapsible) {
329
+ return collapsible && this.isInlined
330
+ }
331
+ }),
332
+
333
+ collapsed: getSchemaAccessor('collapsed', {
334
+ type: Boolean,
335
+ default: false,
336
+ get(collapsed) {
337
+ return collapsed && this.collapsible
338
+ }
339
+ }),
340
+
341
+ maxDepth: getSchemaAccessor('maxDepth', {
342
+ type: Number,
343
+ default: 1
344
+ }),
345
+
346
+ createPath() {
347
+ if (this.creatable) {
348
+ return (
349
+ getViewPath(this.schema, this.context) ||
350
+ this.path
351
+ )
352
+ }
353
+ return null
354
+ }
355
+ },
356
+
357
+ watch: {
358
+ $route: {
359
+ // https://github.com/vuejs/vue-router/issues/3393#issuecomment-1158470149
360
+ flush: 'post',
361
+ handler(to, from) {
362
+ if (this.providesData) {
363
+ if (
364
+ from.path === to.path &&
365
+ from.hash === to.hash
366
+ ) {
367
+ // Paths and hashes remain the same, so only queries have changed.
368
+ // Update filter and reload data without clearing.
369
+ this.query = to.query
370
+ this.loadData(false)
371
+ } else if (
372
+ this.meta.reload &&
373
+ from.path !== to.path &&
374
+ from.path.startsWith(to.path)
375
+ ) {
376
+ // Reload the source when navigating back to a parent-route after
377
+ // changing data in a child-route.
378
+ this.meta.reload = false
379
+ this.loadData(false)
380
+ }
381
+ }
382
+ }
383
+ },
384
+
385
+ wrappedPrimitives: {
386
+ deep: true,
387
+ handler(to, from) {
388
+ const { wrapPrimitives } = this
389
+ // Skip the initial setting of wrappedPrimitives array
390
+ if (wrapPrimitives && from !== null) {
391
+ // Whenever the wrappedPrimitives change, map their values back to the
392
+ // array of primitives, in a primitive way :)
393
+ // But set `unwrappingPrimitives` to true, so the `listData` computed
394
+ // property knows about it, which sets it to `false` again.
395
+ this.unwrappingPrimitives.value = true
396
+ this.value = to.map(object => object[wrapPrimitives])
397
+ }
398
+ }
399
+ }
400
+ },
401
+
402
+ methods: {
403
+ setupData() {
404
+ this.query = this.$route.query
405
+ this.ensureData()
406
+ },
407
+
408
+ // @override ResourceMixin.clearData()
409
+ clearData() {
410
+ this.total = 0
411
+ this.value = null
412
+ },
413
+
414
+ // @override ResourceMixin.setData()
415
+ setData(data) {
416
+ // When new data is loaded, we can store it right back in the data of the
417
+ // view or form that created this list component.
418
+ // Support two formats for list data:
419
+ // - Array: `[...]`
420
+ // - Object: `{ results: [...], total }`, see `unwrapListData()`
421
+ if (
422
+ !data ||
423
+ this.isListSource && isArray(data) ||
424
+ this.isObjectSource && isObject(data)
425
+ ) {
426
+ this.value = data
427
+ } else if (this.unwrapListData(data)) {
428
+ // The format didn't match, see if we received a `{ results, total }`
429
+ // object, in which case `this.value` was already set by
430
+ // `unwrapListData()` and we're done now.
431
+ } else if (isObject(data) && this.isInView) {
432
+ // The controller is sending data for a full multi-component view,
433
+ // including the nested list data.
434
+ this.viewComponent.setData(data)
435
+ }
436
+ },
437
+
438
+ unwrapListData(data) {
439
+ if (
440
+ this.isListSource &&
441
+ isObject(data) &&
442
+ isNumber(data.total) &&
443
+ isArray(data.results)
444
+ ) {
445
+ // If @ditojs/server sends data in the form of `{ results, total }`
446
+ // replace the value with result, but remember the total in the store.
447
+ this.total = data.total
448
+ this.value = data.results
449
+ return this.value
450
+ }
451
+ },
452
+
453
+ createItem(schema, type) {
454
+ const item = this.createData(schema, type)
455
+ if (this.isObjectSource) {
456
+ this.objectData = item
457
+ } else {
458
+ this.listData.push(item)
459
+ }
460
+ if (this.collapsible) {
461
+ this.$nextTick(() => this.openSchemaComponent(-1))
462
+ }
463
+ this.onChange()
464
+ return item
465
+ },
466
+
467
+ removeItem(item, index) {
468
+ let removed = false
469
+ if (this.isObjectSource) {
470
+ this.objectData = null
471
+ removed = true
472
+ } else {
473
+ const { listData } = this
474
+ if (index >= 0) {
475
+ listData.splice(index, 1)
476
+ removed = true
477
+ }
478
+ }
479
+ if (removed) {
480
+ this.removeItemStore(this.schema, item, index)
481
+ this.onChange()
482
+ }
483
+ },
484
+
485
+ deleteItem(item, index) {
486
+ const label = (
487
+ item &&
488
+ this.getItemLabel(this.schema, item, {
489
+ index,
490
+ extended: true
491
+ })
492
+ )
493
+
494
+ const notify = () =>
495
+ this.notify({
496
+ type: this.isTransient ? 'info' : 'success',
497
+ title: 'Successfully Removed',
498
+ text: [
499
+ `${label} was ${this.verbs.deleted}.`,
500
+ this.transientNote
501
+ ]
502
+ })
503
+
504
+ if (
505
+ item &&
506
+ window.confirm(
507
+ `Do you really want to ${this.verbs.delete} ${label}?`
508
+ )
509
+ ) {
510
+ if (this.isTransient) {
511
+ this.removeItem(item, index)
512
+ notify()
513
+ } else {
514
+ const itemId = this.getItemId(this.schema, item, index)
515
+ const method = 'delete'
516
+ const resource = getMemberResource(
517
+ itemId,
518
+ this.getResource({ method })
519
+ )
520
+ if (resource) {
521
+ this.handleRequest({ method, resource }, err => {
522
+ if (!err) {
523
+ this.removeItem(item, index)
524
+ notify()
525
+ }
526
+ this.reloadData()
527
+ })
528
+ }
529
+ }
530
+ }
531
+ },
532
+
533
+ getSchemaComponent(index) {
534
+ const { schemaComponents } = this
535
+ const { length } = schemaComponents
536
+ return schemaComponents[((index % length) + length) % length]
537
+ },
538
+
539
+ openSchemaComponent(index) {
540
+ const schemaComponent = this.getSchemaComponent(index)
541
+ if (schemaComponent) {
542
+ schemaComponent.opened = true
543
+ }
544
+ },
545
+
546
+ async navigateToComponent(dataPath, onComplete) {
547
+ if (this.collapsible) {
548
+ const index = dataPath.startsWith(this.dataPath)
549
+ ? this.isListSource
550
+ ? parseDataPath(dataPath.slice(this.dataPath.length + 1))[0] ?? null
551
+ : 0
552
+ : null
553
+ if (index !== null && isNumber(+index)) {
554
+ const schemaComponent = this.getSchemaComponent(+index)
555
+ if (schemaComponent) {
556
+ const { opened } = schemaComponent
557
+ if (!opened) {
558
+ schemaComponent.opened = true
559
+ await this.$nextTick()
560
+ }
561
+ const components = schemaComponent.getComponentsByDataPath(dataPath)
562
+ if (components.length > 0 && (onComplete?.(components) ?? true)) {
563
+ return true
564
+ } else {
565
+ schemaComponent.opened = opened
566
+ }
567
+ }
568
+ }
569
+ }
570
+ return this.navigateToRouteComponent(dataPath, onComplete)
571
+ },
572
+
573
+ navigateToRouteComponent(dataPath, onComplete) {
574
+ return new Promise((resolve, reject) => {
575
+ const callOnComplete = () => {
576
+ // Retrieve the last route component, which will be the component that
577
+ // we just navigated to, and pass it on to `onComplete()`
578
+ const { routeComponents } = this.appState
579
+ const routeComponent = routeComponents[routeComponents.length - 1]
580
+ resolve(onComplete?.([routeComponent]) ?? true)
581
+ }
582
+
583
+ const dataPathParts = parseDataPath(dataPath)
584
+ // See if we can find a route that can serve part of the given dataPath,
585
+ // and take it from there:
586
+ while (dataPathParts.length > 0) {
587
+ const path = this.routeComponent.getChildPath(
588
+ this.api.normalizePath(normalizeDataPath(dataPathParts))
589
+ )
590
+ // See if there actually is a route for this sub-component:
591
+ const { matched } = this.$router.resolve(path)
592
+ if (matched.length && matched[0].name !== 'catch-all') {
593
+ if (this.$route.path === path) {
594
+ // We're already there, so just call `onComplete()`:
595
+ callOnComplete()
596
+ } else {
597
+ // Navigate to the component's path, then call `onComplete()`_:
598
+ this.$router
599
+ .push({ path })
600
+ .catch(reject)
601
+ // Wait for the last route component to be mounted in the next
602
+ // tick before calling `onComplete()`
603
+ .then(() => {
604
+ this.$nextTick(callOnComplete)
605
+ })
606
+ }
607
+ return
608
+ }
609
+ // Keep removing the last part until we find a match.
610
+ dataPathParts.pop()
611
+ }
612
+ resolve(false)
613
+ })
614
+ }
615
+ }, // end of `methods`
616
+
617
+ async processSchema(
618
+ api,
619
+ schema,
620
+ name,
621
+ routes,
622
+ level,
623
+ nested = false,
624
+ flatten = false,
625
+ process = null
626
+ ) {
627
+ processRouteSchema(api, schema, name)
628
+ const inlined = isInlined(schema)
629
+ if (inlined && schema.resource) {
630
+ throw new Error(
631
+ `Nested ${
632
+ this.isListSource
633
+ ? 'lists'
634
+ : this.isObjectSource
635
+ ? 'objects'
636
+ : 'schema'
637
+ } cannot load data from their own resources`
638
+ )
639
+ }
640
+ // Use differently named url parameters on each nested level for id as
641
+ // otherwise they would clash and override each other inside $route.params
642
+ // See: https://github.com/vuejs/vue-router/issues/1345
643
+ const param = `id${level + 1}`
644
+ const meta = {
645
+ api,
646
+ schema
647
+ }
648
+ const formMeta = {
649
+ ...meta,
650
+ // When children are flattened (e.g. tree-lists), include the `flatten`
651
+ // setting also, for flattening below.
652
+ flatten,
653
+ nested,
654
+ param
655
+ }
656
+ const childRoutes = await processForms(api, schema, level)
657
+ if (process) {
658
+ await process(childRoutes, level + 1)
659
+ }
660
+ // Inlined forms don't need to actually add routes.
661
+ if (hasFormSchema(schema) && !inlined) {
662
+ // Lists in single-component-views (level === 0) use their view's path,
663
+ // while all others need their path prefixed with the parent's path:
664
+ const sourcePath = level === 0 ? '' : schema.path
665
+ const formRoute = {
666
+ path: getPathWithParam(
667
+ sourcePath,
668
+ // Object sources don't need id params in their form paths, as they
669
+ // directly edit one object.
670
+ isListSource(schema) ? param : null
671
+ ),
672
+ component: DitoComponent.component(
673
+ nested ? 'DitoFormNested' : 'DitoForm'
674
+ ),
675
+ meta: formMeta
676
+ }
677
+ if (isObjectSource(schema)) {
678
+ // Also add a param route, simply to handle '/create' links the same
679
+ // way that lists do, where it overlaps with :id for item ids.
680
+ routes.push({
681
+ ...formRoute,
682
+ path: getPathWithParam(sourcePath, param)
683
+ })
684
+ }
685
+ if (sourcePath) {
686
+ // Just redirect back to the parent when a nested source route is hit.
687
+ routes.push({
688
+ path: sourcePath,
689
+ redirect: '.',
690
+ meta
691
+ })
692
+ }
693
+ // Partition childRoutes into those that need flattening (e.g. tree-lists)
694
+ // and those that don't, and process each group separately after.
695
+ const [flatRoutes, subRoutes] = childRoutes.reduce(
696
+ (res, route) => {
697
+ res[route.meta.flatten ? 0 : 1].push(route)
698
+ return res
699
+ },
700
+ [[], []]
701
+ )
702
+ if (subRoutes.length) {
703
+ formRoute.children = subRoutes
704
+ }
705
+ routes.push(formRoute)
706
+ // Add the prefixed formRoutes with their children for nested lists.
707
+ if (flatRoutes.length) {
708
+ for (const childRoute of flatRoutes) {
709
+ routes.push({
710
+ ...(childRoute.redirect ? childRoute : formRoute),
711
+ path: `${formRoute.path}/${childRoute.path}`,
712
+ meta: {
713
+ ...childRoute.meta,
714
+ flatten
715
+ }
716
+ })
717
+ }
718
+ }
719
+ }
720
+ },
721
+
722
+ processValue({ schema, value, dataPath }, graph) {
723
+ graph.addSource(dataPath, schema)
724
+ return value
725
+ }
726
+ }
727
+
728
+ function getPathWithParam(path, param) {
729
+ return param
730
+ ? path
731
+ ? `${path}/:${param}`
732
+ : `:${param}`
733
+ : path
734
+ }