@ditojs/admin 2.99.1 → 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 +3532 -2763
  3. package/dist/dito-admin.umd.js +6 -6
  4. package/package.json +82 -46
  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
@@ -8,7 +8,13 @@
8
8
  RouterView(
9
9
  v-if="!isLastUnnestedRoute && !isNestedRoute"
10
10
  v-show="!isActiveRoute"
11
+ v-slot="{ Component, route }"
11
12
  )
13
+ component(
14
+ v-if="Component"
15
+ :is="Component"
16
+ :key="getRouteComponentKey(route, routeLevel + 1)"
17
+ )
12
18
  //- NOTE: Nested form components are kept alive by using `v-show` instead of
13
19
  //- `v-if` here, so event handling and other things still work with nested
14
20
  //- editing.
@@ -55,8 +61,11 @@ import DitoComponent from '../DitoComponent.js'
55
61
  import RouteMixin from '../mixins/RouteMixin.js'
56
62
  import ResourceMixin from '../mixins/ResourceMixin.js'
57
63
  import { getResource, getMemberResource } from '../utils/resource.js'
58
- import { getButtonSchemas, isObjectSource } from '../utils/schema.js'
64
+ import { isObjectSource } from '../utils/schema/structure.js'
65
+ import { getButtonSchemas } from '../utils/schema/lookup.js'
66
+ import { getComponentPathByDataPath } from '../utils/schema/data.js'
59
67
  import { resolvePath } from '../utils/path.js'
68
+ import { FormModel } from '../utils/FormModel.js'
60
69
 
61
70
  // @vue/component
62
71
  export default DitoComponent.component('DitoForm', {
@@ -65,8 +74,7 @@ export default DitoComponent.component('DitoForm', {
65
74
  data() {
66
75
  return {
67
76
  createdData: null,
68
- clonedData: undefined,
69
- sourceKey: null
77
+ clonedData: undefined
70
78
  }
71
79
  },
72
80
 
@@ -144,7 +152,9 @@ export default DitoComponent.component('DitoForm', {
144
152
  },
145
153
 
146
154
  isDirty() {
147
- return !this.isMutating && !!this.mainSchemaComponent?.isDirty
155
+ // Forms that mutate the inherited data directly aren't dirty themselves,
156
+ // their changes make the form that owns the data dirty instead.
157
+ return !this.isMutating && this.formModel.isDirty
148
158
  },
149
159
 
150
160
  isMutating() {
@@ -193,87 +203,96 @@ export default DitoComponent.component('DitoForm', {
193
203
  return this.getDataPathFrom(this.dataComponent)
194
204
  },
195
205
 
196
- sourceData() {
206
+ componentPath() {
207
+ // Nested forms continue the component path of the source that displays
208
+ // their item in the parent route component, e.g. `main/chapters/1`.
209
+ // Forms that load their own data start a new one, like their data path.
210
+ const { parentRouteComponent } = this
211
+ return this.isTransient && parentRouteComponent
212
+ ? getComponentPathByDataPath(
213
+ parentRouteComponent.isView
214
+ ? parentRouteComponent.viewSchema
215
+ : parentRouteComponent.schema,
216
+ parentRouteComponent.data,
217
+ this.getDataPathFrom(parentRouteComponent),
218
+ {
219
+ componentPath: parentRouteComponent.componentPath,
220
+ component: this,
221
+ rootData: this.rootData
222
+ }
223
+ )
224
+ : ''
225
+ },
226
+
227
+ sourceLocation() {
197
228
  // Possible parents are DitoForm for forms, or DitoView for root lists.
198
229
  // Both have a data property which abstracts away loading and inheriting
199
230
  // of data.
200
- // Forms that are about to be destroyed due to navigation loose their
201
- // route-record, but might still trigger this getter. Filter those out.
202
- let data = this.routeRecord ? this.parentRouteComponent.data : null
203
- if (data) {
204
- // Handle nested data by splitting the dataPath, iterate through the
205
- // actual data and look nest child-data up.
206
- const dataParts = parseDataPath(
207
- this.getDataPathFrom(this.parentRouteComponent)
208
- )
209
- // Compare dataParts against matched routePath parts, to identify those
210
- // parts that need to be treated like ids and mapped to indices in data.
211
- const pathParts = this.routeRecord.path.split('/')
212
- const routeParts = pathParts.slice(pathParts.length - dataParts.length)
213
- // TODO: Fix side-effects
214
- // eslint-disable-next-line vue/no-side-effects-in-computed-properties
215
- this.sourceKey = null
216
- const lastDataPart = dataParts[dataParts.length - 1]
217
- if (isObjectSource(this.sourceSchema) && lastDataPart === 'create') {
218
- // If we have an object source and are creating, the dataPath needs to
219
- // be shortened by the 'create' entry. This isn't needed for list
220
- // sources, as there the parameter is actually mapped to the item id.
221
- dataParts.length--
222
- }
223
- for (let i = 0, l = dataParts.length; i < l && data; i++) {
224
- const dataPart = dataParts[i]
225
- // If this is an :id part, find the index of the item with given id.
226
- const key = /^:id/.test(routeParts[i])
227
- ? dataPart === 'create'
228
- ? null // There's no index for entries about to be created
229
- : this.findItemIdIndex(this.sourceSchema, data, dataPart)
230
- : dataPart
231
- // Skip the final lookup but remember `sourceKey`, as we want the
232
- // parent data so we can replace the entry at `sourceKey` on it.
233
- if (i === l - 1) {
234
- // TODO: Fix side-effects
235
- // eslint-disable-next-line max-len
236
- // eslint-disable-next-line vue/no-side-effects-in-computed-properties
237
- this.sourceKey = key
238
- } else {
239
- data = data[key]
240
- }
231
+ // Forms that are being left don't match the current route anymore, and
232
+ // mustn't resolve their source against it until they're unmounted.
233
+ let data = this.isLeavingRoute ? null : this.parentRouteComponent.data
234
+ if (!data) {
235
+ return null
236
+ }
237
+ // Handle nested data by splitting the dataPath, iterate through the
238
+ // actual data and look nest child-data up.
239
+ const dataParts = parseDataPath(
240
+ this.getDataPathFrom(this.parentRouteComponent)
241
+ )
242
+ // Compare dataParts against matched routePath parts, to identify those
243
+ // parts that need to be treated like ids and mapped to indices in data.
244
+ const pathParts = this.routeRecord.path.split('/')
245
+ const routeParts = pathParts.slice(pathParts.length - dataParts.length)
246
+ const lastDataPart = dataParts[dataParts.length - 1]
247
+ if (isObjectSource(this.sourceSchema) && lastDataPart === 'create') {
248
+ // If we have an object source and are creating, the dataPath needs to
249
+ // be shortened by the 'create' entry. This isn't needed for list
250
+ // sources, as there the parameter is actually mapped to the item id.
251
+ dataParts.length--
252
+ }
253
+ let key = null
254
+ for (let i = 0, l = dataParts.length; i < l && data; i++) {
255
+ const dataPart = dataParts[i]
256
+ // If this is an :id part, find the index of the item with given id.
257
+ key = /^:id/.test(routeParts[i])
258
+ ? dataPart === 'create'
259
+ ? null // There's no index for entries about to be created
260
+ : this.findItemIdIndex(this.sourceSchema, data, dataPart)
261
+ : dataPart
262
+ // Skip the final lookup, as we want the parent data so we can replace
263
+ // the entry at `key` on it.
264
+ if (i < l - 1) {
265
+ data = data[key]
241
266
  }
242
267
  }
243
- return data
268
+ return data ? { data, key } : null
269
+ },
270
+
271
+ sourceData() {
272
+ // The parent data that holds the form's item, at `sourceKey`.
273
+ return this.sourceLocation?.data ?? null
274
+ },
275
+
276
+ sourceKey() {
277
+ return this.sourceLocation?.key ?? null
278
+ },
279
+
280
+ sourceItem() {
281
+ return this.sourceData && this.sourceKey !== null
282
+ ? this.sourceData[this.sourceKey]
283
+ : undefined
244
284
  },
245
285
 
246
286
  inheritedData() {
247
287
  // Data inherited from parent, and cloned to protect against reactive
248
288
  // changes until changes are applied through setSourceData(), unless
249
289
  // `sourceSchema.mutate` is true, in which case data is mutated directly.
250
- if (
251
- this.isTransient &&
252
- this.clonedData === undefined &&
253
- this.sourceData &&
254
- this.sourceKey !== null
255
- ) {
256
- let data = this.sourceData[this.sourceKey]
257
- if (!this.isMutating) {
258
- // Use a trick to store cloned inherited data in clonedData, to make
259
- // it reactive and prevent it from being cloned multiple times.
260
- // TODO: Fix side-effects
261
- // eslint-disable-next-line vue/no-side-effects-in-computed-properties
262
- this.clonedData = data = clone(data)
263
- }
264
- if (
265
- data === null &&
266
- !this.isCreating &&
267
- isObjectSource(this.sourceSchema)
268
- ) {
269
- // If data of an object source is null, redirect to its create route.
270
- // TODO: Fix side-effects
271
- // eslint-disable-next-line vue/no-side-effects-in-computed-properties
272
- this.$router.push({ path: `${this.path}/create` })
273
- }
274
- return data
275
- }
276
- return this.clonedData
290
+ // See the `sourceItem` watcher.
291
+ return this.isTransient
292
+ ? this.isMutating
293
+ ? this.sourceItem
294
+ : this.clonedData
295
+ : undefined
277
296
  },
278
297
 
279
298
  // @override ResourceMixin.hasData()
@@ -306,9 +325,53 @@ export default DitoComponent.component('DitoForm', {
306
325
  }
307
326
  },
308
327
 
309
- sourceData: 'clearClonedData',
310
- // Needed for the 'create' redirect in `inheritedData()` to work:
311
- create: 'setupData'
328
+ sourceItem: {
329
+ immediate: true,
330
+ handler(item) {
331
+ if (this.isTransient) {
332
+ if (!this.isMutating) {
333
+ // Edit a copy of the inherited item, so that changes are only
334
+ // applied through setSourceData(). Copy it again whenever the item
335
+ // itself is replaced, e.g. after applying the changes.
336
+ this.clonedData = clone(item)
337
+ }
338
+ if (
339
+ item === null &&
340
+ !this.isCreating &&
341
+ isObjectSource(this.sourceSchema)
342
+ ) {
343
+ // If data of an object source is null, redirect to its create
344
+ // route.
345
+ this.$router.push({ path: `${this.path}/create` })
346
+ }
347
+ }
348
+ }
349
+ },
350
+
351
+ isCreating(isCreating) {
352
+ // Set up the data to create when redirected to the create route, e.g.
353
+ // by the `sourceItem` watcher.
354
+ if (isCreating) {
355
+ this.setupData()
356
+ }
357
+ }
358
+ },
359
+
360
+ created() {
361
+ // Writes defaults and computed values into the form's data, resolves
362
+ // options and tracks whether the data is dirty, see `FormModel`:
363
+ this.formModel = new FormModel({
364
+ component: this,
365
+ getSchema: () => this.schema,
366
+ getData: () => this.data,
367
+ getSourceSchema: () => this.sourceSchema
368
+ })
369
+ },
370
+
371
+ beforeUnmount() {
372
+ // Stop the model before the component's own watchers are stopped, as the
373
+ // model's watchers aren't part of the component, see `FormModel`.
374
+ this.formModel.stop()
312
375
  },
313
376
 
314
377
  methods: {
@@ -366,19 +429,22 @@ export default DitoComponent.component('DitoForm', {
366
429
  setData(data) {
367
430
  // setData() is called after submit when data has changed.
368
431
  if (this.isTransient) {
369
- // For components with transient data, modify this.sourceData.
432
+ // For components with transient data, modify this.sourceData, and
433
+ // return the item stored there, as the form's own data is a copy.
370
434
  this.setSourceData(data)
435
+ return this.sourceItem
371
436
  } else {
372
437
  this.createdData = null
373
438
  this.loadedData = data
439
+ return this.data
374
440
  }
375
441
  },
376
442
 
377
- clearClonedData(to, from) {
378
- // Only clear if the watched sourceData itself changes in the form.
379
- if (to !== from) {
380
- this.clonedData = undefined
381
- }
443
+ // Makes clean changes to the data, which don't make the form dirty, e.g. to
444
+ // apply what an action on the server already saved, see
445
+ // `FormModel.applyCleanChanges()`.
446
+ applyCleanChanges(makeChanges) {
447
+ return this.formModel.applyCleanChanges(makeChanges)
382
448
  },
383
449
 
384
450
  async cancel() {
@@ -400,6 +466,8 @@ export default DitoComponent.component('DitoForm', {
400
466
  },
401
467
 
402
468
  async submit(button, { validate = true, closeForm = false } = {}) {
469
+ // Wait for computed values that depend on pending loads, e.g. options.
470
+ await this.formModel.waitForPendingLoads()
403
471
  if (validate && !this.validateAll()) {
404
472
  return false
405
473
  }
@@ -480,6 +548,11 @@ export default DitoComponent.component('DitoForm', {
480
548
  }
481
549
  if (success) {
482
550
  this.resetValidation()
551
+ // The saved or applied data isn't dirty, also when it isn't replaced,
552
+ // e.g. by a response without data, and before the closing below:
553
+ this.formModel
554
+ .takeProcessedDataSnapshot()
555
+ .catch(console.error)
483
556
  if (closeForm || button.closeForm) {
484
557
  this.close()
485
558
  } else if (this.isCreating) {
@@ -20,7 +20,7 @@ component.dito-label(
20
20
  )
21
21
  label(
22
22
  v-if="text"
23
- :for="dataPath"
23
+ :for="htmlFor"
24
24
  v-html="text"
25
25
  )
26
26
  DitoAffixes(
@@ -48,7 +48,8 @@ export default DitoComponent.component('DitoLabel', {
48
48
 
49
49
  props: {
50
50
  label: { type: [String, Object], default: null },
51
- dataPath: { type: String, default: null },
51
+ // The DOM id of the labelled element, see `DitoContainer.labelHtmlFor`.
52
+ htmlFor: { type: String, default: null },
52
53
  collapsed: { type: Boolean, default: false },
53
54
  collapsible: { type: Boolean, default: false },
54
55
  info: { type: String, default: null }
@@ -2,10 +2,10 @@
2
2
  <template lang="pug">
3
3
  .dito-pane(
4
4
  v-if="isPopulated && componentSchemas.length > 0"
5
- :id="tab ? `${dataPath}-tabpanel-${tab}` : null"
5
+ :id="tabPanelId"
6
6
  v-resize="onResizePane"
7
7
  :role="tab ? 'tabpanel' : null"
8
- :aria-labelledby="tab ? `${dataPath}-tab-${tab}` : null"
8
+ :aria-labelledby="tabId"
9
9
  :class="classes"
10
10
  )
11
11
  template(
@@ -23,7 +23,7 @@
23
23
  v-if="['before', 'both'].includes(schema.break)"
24
24
  )
25
25
  DitoContainer(
26
- v-if="shouldRenderSchema(schema)"
26
+ v-if="shouldRenderComponent(schema, nested, dataPath)"
27
27
  ref="containers"
28
28
  :key="nestedDataPath"
29
29
  :data-index="index"
@@ -47,9 +47,11 @@
47
47
 
48
48
  <script>
49
49
  import DitoComponent from '../DitoComponent.js'
50
+ import DitoContext from '../DitoContext.js'
50
51
  import ContextMixin from '../mixins/ContextMixin.js'
51
52
  import { appendDataPath } from '../utils/data.js'
52
- import { isNested } from '../utils/schema.js'
53
+ import { isNested } from '../utils/schema/structure.js'
54
+ import { shouldRenderSchema } from '../utils/schema/data.js'
53
55
 
54
56
  // @vue/component
55
57
  export default DitoComponent.component('DitoPane', {
@@ -100,6 +102,23 @@ export default DitoComponent.component('DitoPane', {
100
102
  return this.tab ? this : this.$tabComponent()
101
103
  },
102
104
 
105
+ componentPath() {
106
+ // Tabs add their name, as they don't appear in data paths.
107
+ const { componentPath } = this.parentComponent
108
+ return this.tab ? appendDataPath(componentPath, this.tab) : componentPath
109
+ },
110
+
111
+ // See `DitoTabs.getTabId()` and `getTabPanelId()`:
112
+ tabId() {
113
+ const { componentPath } = this.schemaComponent
114
+ return this.tab ? `${componentPath}-tab-${this.tab}` : null
115
+ },
116
+
117
+ tabPanelId() {
118
+ const { componentPath } = this.schemaComponent
119
+ return this.tab ? `${componentPath}-tabpanel-${this.tab}` : null
120
+ },
121
+
103
122
  componentSchemas() {
104
123
  // Compute a components list which has the dataPath baked into its keys
105
124
  // and adds the key as the name to each component, used for labels, etc.
@@ -117,12 +136,13 @@ export default DitoComponent.component('DitoPane', {
117
136
  // Share dataPath and store with parent if not nested:
118
137
  const nested = isNested(schema)
119
138
  const nestedDataPath = appendDataPath(this.dataPath, name)
139
+ const dataPath =
140
+ nested && !wrapPrimitives
141
+ ? nestedDataPath
142
+ : this.dataPath
120
143
  return {
121
144
  schema,
122
- dataPath:
123
- nested && !wrapPrimitives
124
- ? nestedDataPath
125
- : this.dataPath,
145
+ dataPath,
126
146
  nestedDataPath,
127
147
  nested,
128
148
  store: this.getChildStore(name)
@@ -197,6 +217,12 @@ export default DitoComponent.component('DitoPane', {
197
217
  watch: {
198
218
  'componentSchemas.length'(length) {
199
219
  this.positions.length = length
220
+ },
221
+
222
+ 'componentPath'(componentPath, oldComponentPath) {
223
+ // Component paths change when list items move, see `_registerEntry()`.
224
+ this._register(false, oldComponentPath)
225
+ this._register(true, componentPath)
200
226
  }
201
227
  },
202
228
 
@@ -209,8 +235,24 @@ export default DitoComponent.component('DitoPane', {
209
235
  },
210
236
 
211
237
  methods: {
212
- _register(add) {
213
- this.schemaComponent._registerPane(this, add)
238
+ shouldRenderComponent(schema, nested, dataPath) {
239
+ // Evaluate the component's `if` before it is rendered, with its own
240
+ // context, the same as once it is rendered, not with the pane's.
241
+ return shouldRenderSchema(
242
+ schema,
243
+ DitoContext.createForSchema(this, {
244
+ schema,
245
+ name: schema.name,
246
+ nested,
247
+ data: this.data,
248
+ dataPath,
249
+ rootData: this.rootData
250
+ })
251
+ )
252
+ },
253
+
254
+ _register(add, componentPath = this.componentPath) {
255
+ this.schemaComponent._registerPane(this, add, componentPath)
214
256
  },
215
257
 
216
258
  focus() {
@@ -221,7 +263,9 @@ export default DitoComponent.component('DitoPane', {
221
263
 
222
264
  onResizePane() {
223
265
  this.$nextTick(() => {
224
- for (const container of this.$refs.containers) {
266
+ // The containers aren't rendered anymore if the pane was emptied or
267
+ // unmounted in the meantime.
268
+ for (const container of this.$refs.containers ?? []) {
225
269
  const node = container.$el
226
270
  const index = +node.dataset.index
227
271
  const bounds = node.getBoundingClientRect()
@@ -45,9 +45,10 @@ component.dito-panel(
45
45
  <script>
46
46
  import { isFunction } from '@ditojs/utils'
47
47
  import DitoComponent from '../DitoComponent.js'
48
+ import DitoContext from '../DitoContext.js'
48
49
  import ContextMixin from '../mixins/ContextMixin.js'
49
50
  import ValidatorMixin from '../mixins/ValidatorMixin.js'
50
- import { getButtonSchemas } from '../utils/schema.js'
51
+ import { getButtonSchemas } from '../utils/schema/lookup.js'
51
52
  import { getSchemaAccessor } from '../utils/accessor.js'
52
53
 
53
54
  // @vue/component
@@ -68,7 +69,9 @@ export default DitoComponent.component('DitoPanel', {
68
69
  meta: { type: Object, required: true },
69
70
  store: { type: Object, required: true },
70
71
  disabled: { type: Boolean, required: true },
71
- panelTabComponent: { type: Object, default: null }
72
+ panelTabComponent: { type: Object, default: null },
73
+ // The component path of the panel's entry, see `getPanelEntry()`:
74
+ panelComponentPath: { type: String, required: true }
72
75
  },
73
76
 
74
77
  data() {
@@ -79,14 +82,31 @@ export default DitoComponent.component('DitoPanel', {
79
82
 
80
83
  computed: {
81
84
  nested() {
82
- // For `ContextMixin`:
85
+ // For `ContextMixin`: The panel's address, `dataPath`, points into the
86
+ // item that it displays, see `getPanelEntry()`.
83
87
  return true
84
88
  },
85
89
 
90
+ // @override ContextMixin.context
91
+ context() {
92
+ // Evaluate the callbacks of the panel and of its components with the
93
+ // data that the panel displays: its own data, e.g. of `$filters`, or the
94
+ // data of its schema component, at that component's data path.
95
+ return new DitoContext(this, {
96
+ nested: false,
97
+ data: this.panelData,
98
+ dataPath: this.panelDataPath
99
+ })
100
+ },
101
+
86
102
  panelComponent() {
87
103
  return this
88
104
  },
89
105
 
106
+ componentPath() {
107
+ return this.panelComponentPath
108
+ },
109
+
90
110
  tabComponent() {
91
111
  return this.panelTabComponent
92
112
  },
@@ -145,6 +165,14 @@ export default DitoComponent.component('DitoPanel', {
145
165
  })
146
166
  },
147
167
 
168
+ watch: {
169
+ componentPath(componentPath, oldComponentPath) {
170
+ // Component paths change when list items move, see `_registerEntry()`.
171
+ this._register(false, oldComponentPath)
172
+ this._register(true, componentPath)
173
+ }
174
+ },
175
+
148
176
  created() {
149
177
  this._register(true)
150
178
  // NOTE: This is not the same as `schema.data` handling in DitoSchema,
@@ -162,10 +190,10 @@ export default DitoComponent.component('DitoPanel', {
162
190
  },
163
191
 
164
192
  methods: {
165
- _register(add) {
193
+ _register(add, componentPath = this.componentPath) {
166
194
  // Register the panels so that other components can find them by their
167
195
  // data-path, e.g. in TypeList.onFilterErrors()
168
- this.schemaComponent._registerPanel(this, add)
196
+ this.schemaComponent._registerPanel(this, add, componentPath)
169
197
  }
170
198
  }
171
199
  })
@@ -3,13 +3,14 @@
3
3
  v-if="panels.length > 0"
4
4
  )
5
5
  template(
6
- v-for="{ schema, dataPath, tabComponent } in panels"
6
+ v-for="{ schema, dataPath, componentPath, tabComponent } in panels"
7
7
  )
8
8
  DitoPanel(
9
9
  v-if="shouldRenderSchema(schema)"
10
10
  :key="getPanelKey(dataPath, tabComponent)"
11
11
  :schema="schema"
12
12
  :dataPath="dataPath"
13
+ :panelComponentPath="componentPath"
13
14
  :data="data"
14
15
  :meta="meta"
15
16
  :store="getChildStore(schema.name)"
@@ -28,7 +28,14 @@
28
28
  :spinner="options.spinner"
29
29
  :isLoading="isLoading"
30
30
  )
31
- RouterView
31
+ RouterView(
32
+ v-slot="{ Component, route }"
33
+ )
34
+ component(
35
+ v-if="Component"
36
+ :is="Component"
37
+ :key="getRouteComponentKey(route, 0)"
38
+ )
32
39
  DitoSidebar
33
40
  DitoAccount(
34
41
  v-if="user"
@@ -50,10 +57,10 @@ import DitoUser from '../DitoUser.js'
50
57
  import DitoView from '../components/DitoView.vue'
51
58
  import DitoDialog from './DitoDialog.vue'
52
59
  import {
53
- processView,
60
+ setupView,
54
61
  resolveViews,
55
- processSchemaComponents
56
- } from '../utils/schema.js'
62
+ setupSchemaComponents
63
+ } from '../utils/schema/setup.js'
57
64
 
58
65
  // @vue/component
59
66
  export default DitoComponent.component('DitoRoot', {
@@ -251,7 +258,7 @@ export default DitoComponent.component('DitoRoot', {
251
258
  async (resolve, reject) => {
252
259
  // Process components to resolve async schemas.
253
260
  const routes = []
254
- await processSchemaComponents(
261
+ await setupSchemaComponents(
255
262
  this.api,
256
263
  { type: 'dialog', components },
257
264
  routes,
@@ -407,7 +414,7 @@ export default DitoComponent.component('DitoRoot', {
407
414
  // Collect all routes from the root schema components
408
415
  const routes = await mapConcurrently(
409
416
  Object.entries(this.resolvedViews),
410
- ([name, schema]) => processView(DitoView, this.api, schema, name)
417
+ ([name, schema]) => setupView(DitoView, this.api, schema, name)
411
418
  )
412
419
  // Now that the routes are loaded, replace all existing routes with the
413
420
  // new routes, and restore the current path.