@ditojs/admin 2.100.0 → 3.1.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 (81) hide show
  1. package/dist/dito-admin.css +1 -1
  2. package/dist/dito-admin.es.js +3555 -2719
  3. package/dist/dito-admin.umd.js +5 -6
  4. package/package.json +84 -46
  5. package/src/DitoAdmin.js +1 -65
  6. package/src/DitoComponent.js +1 -1
  7. package/src/DitoContext.js +51 -0
  8. package/src/DitoTypeComponent.js +23 -1
  9. package/src/components/DitoAffixes.vue +6 -3
  10. package/src/components/DitoButtons.vue +14 -1
  11. package/src/components/DitoContainer.vue +19 -10
  12. package/src/components/DitoCreateButton.vue +11 -2
  13. package/src/components/DitoDialog.vue +21 -2
  14. package/src/components/DitoForm.vue +152 -84
  15. package/src/components/DitoLabel.vue +3 -2
  16. package/src/components/DitoPane.vue +55 -11
  17. package/src/components/DitoPanel.vue +34 -5
  18. package/src/components/DitoPanels.vue +2 -1
  19. package/src/components/DitoRoot.vue +6 -6
  20. package/src/components/DitoRouterView.vue +27 -0
  21. package/src/components/DitoSchema.vue +278 -145
  22. package/src/components/DitoSchemaInlined.vue +1 -1
  23. package/src/components/DitoTabs.vue +16 -18
  24. package/src/components/DitoTrail.vue +5 -4
  25. package/src/components/DitoTreeItem.vue +17 -6
  26. package/src/components/DitoView.vue +21 -3
  27. package/src/components/index.js +1 -0
  28. package/src/mixins/DitoMixin.js +53 -7
  29. package/src/mixins/ItemMixin.js +3 -6
  30. package/src/mixins/NumberMixin.js +47 -31
  31. package/src/mixins/OptionsMixin.js +132 -71
  32. package/src/mixins/ResourceMixin.js +5 -2
  33. package/src/mixins/RouteMixin.js +39 -1
  34. package/src/mixins/SortableMixin.js +0 -13
  35. package/src/mixins/SourceMixin.js +155 -96
  36. package/src/mixins/TextMixin.js +1 -1
  37. package/src/mixins/TypeMixin.js +37 -22
  38. package/src/mixins/ValidationMixin.js +9 -27
  39. package/src/mixins/ValidatorMixin.js +5 -7
  40. package/src/mixins/ValueMixin.js +5 -2
  41. package/src/types/DitoTypeButton.vue +1 -1
  42. package/src/types/DitoTypeCheckbox.vue +1 -1
  43. package/src/types/DitoTypeCheckboxes.vue +1 -1
  44. package/src/types/DitoTypeCode.vue +8 -1
  45. package/src/types/DitoTypeColor.vue +20 -34
  46. package/src/types/DitoTypeComponent.vue +1 -1
  47. package/src/types/DitoTypeComputed.vue +4 -27
  48. package/src/types/DitoTypeDate.vue +16 -5
  49. package/src/types/DitoTypeLabel.vue +1 -4
  50. package/src/types/DitoTypeList.vue +9 -5
  51. package/src/types/DitoTypeMarkup.vue +25 -13
  52. package/src/types/DitoTypeMultiselect.vue +37 -6
  53. package/src/types/DitoTypeNumber.vue +1 -1
  54. package/src/types/DitoTypeObject.vue +10 -7
  55. package/src/types/DitoTypePanel.vue +20 -6
  56. package/src/types/DitoTypeProgress.vue +1 -1
  57. package/src/types/DitoTypeRadio.vue +1 -1
  58. package/src/types/DitoTypeSection.vue +32 -4
  59. package/src/types/DitoTypeSelect.vue +1 -1
  60. package/src/types/DitoTypeSlider.vue +1 -1
  61. package/src/types/DitoTypeSwitch.vue +1 -1
  62. package/src/types/DitoTypeText.vue +13 -15
  63. package/src/types/DitoTypeTextarea.vue +1 -1
  64. package/src/types/DitoTypeTreeList.vue +5 -7
  65. package/src/types/DitoTypeUpload.vue +1 -2
  66. package/src/utils/DataSchemaResolver.js +154 -0
  67. package/src/utils/FormModel.js +615 -0
  68. package/src/utils/data.js +20 -0
  69. package/src/utils/filter.js +3 -2
  70. package/src/utils/options.js +3 -0
  71. package/src/utils/request.js +70 -0
  72. package/src/utils/schema/data.js +659 -0
  73. package/src/utils/schema/lookup.js +250 -0
  74. package/src/utils/schema/setup.js +354 -0
  75. package/src/utils/schema/structure.js +172 -0
  76. package/src/utils/schema/types.js +34 -0
  77. package/src/utils/schema/validation.js +142 -0
  78. package/src/utils/units.js +70 -3
  79. package/types/index.d.ts +174 -59
  80. package/src/mixins/DataMixin.js +0 -131
  81. package/src/utils/schema.js +0 -1121
@@ -5,9 +5,10 @@
5
5
  )
6
6
  //- Only render a router-view here if this isn't the last data route and not a
7
7
  //- nested form route, which will appear elsewhere in its own view.
8
- RouterView(
8
+ DitoRouterView(
9
9
  v-if="!isLastUnnestedRoute && !isNestedRoute"
10
10
  v-show="!isActiveRoute"
11
+ :routeLevel="routeLevel + 1"
11
12
  )
12
13
  //- NOTE: Nested form components are kept alive by using `v-show` instead of
13
14
  //- `v-if` here, so event handling and other things still work with nested
@@ -55,8 +56,11 @@ import DitoComponent from '../DitoComponent.js'
55
56
  import RouteMixin from '../mixins/RouteMixin.js'
56
57
  import ResourceMixin from '../mixins/ResourceMixin.js'
57
58
  import { getResource, getMemberResource } from '../utils/resource.js'
58
- import { getButtonSchemas, isObjectSource } from '../utils/schema.js'
59
+ import { isObjectSource } from '../utils/schema/structure.js'
60
+ import { getButtonSchemas } from '../utils/schema/lookup.js'
61
+ import { getComponentPathByDataPath } from '../utils/schema/data.js'
59
62
  import { resolvePath } from '../utils/path.js'
63
+ import { FormModel } from '../utils/FormModel.js'
60
64
 
61
65
  // @vue/component
62
66
  export default DitoComponent.component('DitoForm', {
@@ -65,8 +69,7 @@ export default DitoComponent.component('DitoForm', {
65
69
  data() {
66
70
  return {
67
71
  createdData: null,
68
- clonedData: undefined,
69
- sourceKey: null
72
+ clonedData: undefined
70
73
  }
71
74
  },
72
75
 
@@ -144,7 +147,9 @@ export default DitoComponent.component('DitoForm', {
144
147
  },
145
148
 
146
149
  isDirty() {
147
- return !this.isMutating && !!this.mainSchemaComponent?.isDirty
150
+ // Forms that mutate the inherited data directly aren't dirty themselves,
151
+ // their changes make the form that owns the data dirty instead.
152
+ return !this.isMutating && this.formModel.isDirty
148
153
  },
149
154
 
150
155
  isMutating() {
@@ -193,87 +198,96 @@ export default DitoComponent.component('DitoForm', {
193
198
  return this.getDataPathFrom(this.dataComponent)
194
199
  },
195
200
 
196
- sourceData() {
201
+ componentPath() {
202
+ // Nested forms continue the component path of the source that displays
203
+ // their item in the parent route component, e.g. `main/chapters/1`.
204
+ // Forms that load their own data start a new one, like their data path.
205
+ const { parentRouteComponent } = this
206
+ return this.isTransient && parentRouteComponent
207
+ ? getComponentPathByDataPath(
208
+ parentRouteComponent.isView
209
+ ? parentRouteComponent.viewSchema
210
+ : parentRouteComponent.schema,
211
+ parentRouteComponent.data,
212
+ this.getDataPathFrom(parentRouteComponent),
213
+ {
214
+ componentPath: parentRouteComponent.componentPath,
215
+ component: this,
216
+ rootData: this.rootData
217
+ }
218
+ )
219
+ : ''
220
+ },
221
+
222
+ sourceLocation() {
197
223
  // Possible parents are DitoForm for forms, or DitoView for root lists.
198
224
  // Both have a data property which abstracts away loading and inheriting
199
225
  // 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
- }
226
+ // Forms that are being left don't match the current route anymore, and
227
+ // mustn't resolve their source against it until they're unmounted.
228
+ let data = this.isLeavingRoute ? null : this.parentRouteComponent.data
229
+ if (!data) {
230
+ return null
231
+ }
232
+ // Handle nested data by splitting the dataPath, iterate through the
233
+ // actual data and look nest child-data up.
234
+ const dataParts = parseDataPath(
235
+ this.getDataPathFrom(this.parentRouteComponent)
236
+ )
237
+ // Compare dataParts against matched routePath parts, to identify those
238
+ // parts that need to be treated like ids and mapped to indices in data.
239
+ const pathParts = this.routeRecord.path.split('/')
240
+ const routeParts = pathParts.slice(pathParts.length - dataParts.length)
241
+ const lastDataPart = dataParts[dataParts.length - 1]
242
+ if (isObjectSource(this.sourceSchema) && lastDataPart === 'create') {
243
+ // If we have an object source and are creating, the dataPath needs to
244
+ // be shortened by the 'create' entry. This isn't needed for list
245
+ // sources, as there the parameter is actually mapped to the item id.
246
+ dataParts.length--
247
+ }
248
+ let key = null
249
+ for (let i = 0, l = dataParts.length; i < l && data; i++) {
250
+ const dataPart = dataParts[i]
251
+ // If this is an :id part, find the index of the item with given id.
252
+ key = /^:id/.test(routeParts[i])
253
+ ? dataPart === 'create'
254
+ ? null // There's no index for entries about to be created
255
+ : this.findItemIdIndex(this.sourceSchema, data, dataPart)
256
+ : dataPart
257
+ // Skip the final lookup, as we want the parent data so we can replace
258
+ // the entry at `key` on it.
259
+ if (i < l - 1) {
260
+ data = data[key]
241
261
  }
242
262
  }
243
- return data
263
+ return data ? { data, key } : null
264
+ },
265
+
266
+ sourceData() {
267
+ // The parent data that holds the form's item, at `sourceKey`.
268
+ return this.sourceLocation?.data ?? null
269
+ },
270
+
271
+ sourceKey() {
272
+ return this.sourceLocation?.key ?? null
273
+ },
274
+
275
+ sourceItem() {
276
+ return this.sourceData && this.sourceKey !== null
277
+ ? this.sourceData[this.sourceKey]
278
+ : undefined
244
279
  },
245
280
 
246
281
  inheritedData() {
247
282
  // Data inherited from parent, and cloned to protect against reactive
248
283
  // changes until changes are applied through setSourceData(), unless
249
284
  // `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
285
+ // See the `sourceItem` watcher.
286
+ return this.isTransient
287
+ ? this.isMutating
288
+ ? this.sourceItem
289
+ : this.clonedData
290
+ : undefined
277
291
  },
278
292
 
279
293
  // @override ResourceMixin.hasData()
@@ -306,9 +320,53 @@ export default DitoComponent.component('DitoForm', {
306
320
  }
307
321
  },
308
322
 
309
- sourceData: 'clearClonedData',
310
- // Needed for the 'create' redirect in `inheritedData()` to work:
311
- create: 'setupData'
323
+ sourceItem: {
324
+ immediate: true,
325
+ handler(item) {
326
+ if (this.isTransient) {
327
+ if (!this.isMutating) {
328
+ // Edit a copy of the inherited item, so that changes are only
329
+ // applied through setSourceData(). Copy it again whenever the item
330
+ // itself is replaced, e.g. after applying the changes.
331
+ this.clonedData = clone(item)
332
+ }
333
+ if (
334
+ item === null &&
335
+ !this.isCreating &&
336
+ isObjectSource(this.sourceSchema)
337
+ ) {
338
+ // If data of an object source is null, redirect to its create
339
+ // route.
340
+ this.$router.push({ path: `${this.path}/create` })
341
+ }
342
+ }
343
+ }
344
+ },
345
+
346
+ isCreating(isCreating) {
347
+ // Set up the data to create when redirected to the create route, e.g.
348
+ // by the `sourceItem` watcher.
349
+ if (isCreating) {
350
+ this.setupData()
351
+ }
352
+ }
353
+ },
354
+
355
+ created() {
356
+ // Writes defaults and computed values into the form's data, resolves
357
+ // options and tracks whether the data is dirty, see `FormModel`:
358
+ this.formModel = new FormModel({
359
+ component: this,
360
+ getSchema: () => this.schema,
361
+ getData: () => this.data,
362
+ getSourceSchema: () => this.sourceSchema
363
+ })
364
+ },
365
+
366
+ beforeUnmount() {
367
+ // Stop the model before the component's own watchers are stopped, as the
368
+ // model's watchers aren't part of the component, see `FormModel`.
369
+ this.formModel.stop()
312
370
  },
313
371
 
314
372
  methods: {
@@ -366,19 +424,22 @@ export default DitoComponent.component('DitoForm', {
366
424
  setData(data) {
367
425
  // setData() is called after submit when data has changed.
368
426
  if (this.isTransient) {
369
- // For components with transient data, modify this.sourceData.
427
+ // For components with transient data, modify this.sourceData, and
428
+ // return the item stored there, as the form's own data is a copy.
370
429
  this.setSourceData(data)
430
+ return this.sourceItem
371
431
  } else {
372
432
  this.createdData = null
373
433
  this.loadedData = data
434
+ return this.data
374
435
  }
375
436
  },
376
437
 
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
- }
438
+ // Makes clean changes to the data, which don't make the form dirty, e.g. to
439
+ // apply what an action on the server already saved, see
440
+ // `FormModel.applyCleanChanges()`.
441
+ applyCleanChanges(makeChanges) {
442
+ return this.formModel.applyCleanChanges(makeChanges)
382
443
  },
383
444
 
384
445
  async cancel() {
@@ -400,6 +461,8 @@ export default DitoComponent.component('DitoForm', {
400
461
  },
401
462
 
402
463
  async submit(button, { validate = true, closeForm = false } = {}) {
464
+ // Wait for computed values that depend on pending loads, e.g. options.
465
+ await this.waitForPendingLoadsOf(this.formModel)
403
466
  if (validate && !this.validateAll()) {
404
467
  return false
405
468
  }
@@ -480,6 +543,11 @@ export default DitoComponent.component('DitoForm', {
480
543
  }
481
544
  if (success) {
482
545
  this.resetValidation()
546
+ // The saved or applied data isn't dirty, also when it isn't replaced,
547
+ // e.g. by a response without data, and before the closing below:
548
+ this.formModel
549
+ .takeProcessedDataSnapshot()
550
+ .catch(console.error)
483
551
  if (closeForm || button.closeForm) {
484
552
  this.close()
485
553
  } 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()
@@ -25,6 +25,7 @@ component.dito-panel(
25
25
  span {{ getLabel(schema) }}
26
26
  DitoButtons.dito-buttons--small(
27
27
  :buttons="panelButtonSchemas"
28
+ buttonsName="panelButtons"
28
29
  :dataPath="panelDataPath"
29
30
  :data="panelData"
30
31
  :meta="meta"
@@ -45,9 +46,10 @@ component.dito-panel(
45
46
  <script>
46
47
  import { isFunction } from '@ditojs/utils'
47
48
  import DitoComponent from '../DitoComponent.js'
49
+ import DitoContext from '../DitoContext.js'
48
50
  import ContextMixin from '../mixins/ContextMixin.js'
49
51
  import ValidatorMixin from '../mixins/ValidatorMixin.js'
50
- import { getButtonSchemas } from '../utils/schema.js'
52
+ import { getButtonSchemas } from '../utils/schema/lookup.js'
51
53
  import { getSchemaAccessor } from '../utils/accessor.js'
52
54
 
53
55
  // @vue/component
@@ -68,7 +70,9 @@ export default DitoComponent.component('DitoPanel', {
68
70
  meta: { type: Object, required: true },
69
71
  store: { type: Object, required: true },
70
72
  disabled: { type: Boolean, required: true },
71
- panelTabComponent: { type: Object, default: null }
73
+ panelTabComponent: { type: Object, default: null },
74
+ // The component path of the panel's entry, see `getAllPanelEntries()`:
75
+ panelComponentPath: { type: String, required: true }
72
76
  },
73
77
 
74
78
  data() {
@@ -79,14 +83,31 @@ export default DitoComponent.component('DitoPanel', {
79
83
 
80
84
  computed: {
81
85
  nested() {
82
- // For `ContextMixin`:
86
+ // For `ContextMixin`: The panel's address, `dataPath`, points into the
87
+ // item that it displays, see `getAllPanelEntries()`.
83
88
  return true
84
89
  },
85
90
 
91
+ // @override ContextMixin.context
92
+ context() {
93
+ // Evaluate the callbacks of the panel and of its components with the
94
+ // data that the panel displays: its own data, e.g. of `$filters`, or the
95
+ // data of its schema component, at that component's data path.
96
+ return new DitoContext(this, {
97
+ nested: false,
98
+ data: this.panelData,
99
+ dataPath: this.panelDataPath
100
+ })
101
+ },
102
+
86
103
  panelComponent() {
87
104
  return this
88
105
  },
89
106
 
107
+ componentPath() {
108
+ return this.panelComponentPath
109
+ },
110
+
90
111
  tabComponent() {
91
112
  return this.panelTabComponent
92
113
  },
@@ -145,6 +166,14 @@ export default DitoComponent.component('DitoPanel', {
145
166
  })
146
167
  },
147
168
 
169
+ watch: {
170
+ componentPath(componentPath, oldComponentPath) {
171
+ // Component paths change when list items move, see `_registerEntry()`.
172
+ this._register(false, oldComponentPath)
173
+ this._register(true, componentPath)
174
+ }
175
+ },
176
+
148
177
  created() {
149
178
  this._register(true)
150
179
  // NOTE: This is not the same as `schema.data` handling in DitoSchema,
@@ -162,10 +191,10 @@ export default DitoComponent.component('DitoPanel', {
162
191
  },
163
192
 
164
193
  methods: {
165
- _register(add) {
194
+ _register(add, componentPath = this.componentPath) {
166
195
  // Register the panels so that other components can find them by their
167
196
  // data-path, e.g. in TypeList.onFilterErrors()
168
- this.schemaComponent._registerPanel(this, add)
197
+ this.schemaComponent._registerPanel(this, add, componentPath)
169
198
  }
170
199
  }
171
200
  })
@@ -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,7 @@
28
28
  :spinner="options.spinner"
29
29
  :isLoading="isLoading"
30
30
  )
31
- RouterView
31
+ DitoRouterView(:routeLevel="0")
32
32
  DitoSidebar
33
33
  DitoAccount(
34
34
  v-if="user"
@@ -50,10 +50,10 @@ import DitoUser from '../DitoUser.js'
50
50
  import DitoView from '../components/DitoView.vue'
51
51
  import DitoDialog from './DitoDialog.vue'
52
52
  import {
53
- processView,
53
+ setupView,
54
54
  resolveViews,
55
- processSchemaComponents
56
- } from '../utils/schema.js'
55
+ setupSchemaComponents
56
+ } from '../utils/schema/setup.js'
57
57
 
58
58
  // @vue/component
59
59
  export default DitoComponent.component('DitoRoot', {
@@ -251,7 +251,7 @@ export default DitoComponent.component('DitoRoot', {
251
251
  async (resolve, reject) => {
252
252
  // Process components to resolve async schemas.
253
253
  const routes = []
254
- await processSchemaComponents(
254
+ await setupSchemaComponents(
255
255
  this.api,
256
256
  { type: 'dialog', components },
257
257
  routes,
@@ -407,7 +407,7 @@ export default DitoComponent.component('DitoRoot', {
407
407
  // Collect all routes from the root schema components
408
408
  const routes = await mapConcurrently(
409
409
  Object.entries(this.resolvedViews),
410
- ([name, schema]) => processView(DitoView, this.api, schema, name)
410
+ ([name, schema]) => setupView(DitoView, this.api, schema, name)
411
411
  )
412
412
  // Now that the routes are loaded, replace all existing routes with the
413
413
  // new routes, and restore the current path.
@@ -0,0 +1,27 @@
1
+ <script>
2
+ import { h } from 'vue'
3
+ import { RouterView } from 'vue-router'
4
+ import DitoComponent from '../DitoComponent.js'
5
+
6
+ // Renders the route components of `routeLevel` through `RouterView`, keyed by
7
+ // their route record, so that each record gets its own component instance, as
8
+ // their schema and data belong to it, while the component is reused when only
9
+ // the parameters change, e.g. to edit another item.
10
+ // NOTE: This is a functional component, so that it doesn't become the parent
11
+ // component of the route components, see `DitoMixin.provide()`.
12
+ // @vue/component
13
+ function DitoRouterView({ routeLevel }) {
14
+ return h(RouterView, null, {
15
+ default: ({ Component, route }) =>
16
+ Component
17
+ ? h(Component, { key: route.matched[routeLevel]?.path })
18
+ : null
19
+ })
20
+ }
21
+
22
+ DitoRouterView.props = {
23
+ routeLevel: { type: Number, required: true }
24
+ }
25
+
26
+ export default DitoComponent.component('DitoRouterView', DitoRouterView)
27
+ </script>