@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.
- package/dist/dito-admin.css +1 -1
- package/dist/dito-admin.es.js +3555 -2719
- package/dist/dito-admin.umd.js +5 -6
- package/package.json +84 -46
- package/src/DitoAdmin.js +1 -65
- package/src/DitoComponent.js +1 -1
- package/src/DitoContext.js +51 -0
- package/src/DitoTypeComponent.js +23 -1
- package/src/components/DitoAffixes.vue +6 -3
- package/src/components/DitoButtons.vue +14 -1
- package/src/components/DitoContainer.vue +19 -10
- package/src/components/DitoCreateButton.vue +11 -2
- package/src/components/DitoDialog.vue +21 -2
- package/src/components/DitoForm.vue +152 -84
- package/src/components/DitoLabel.vue +3 -2
- package/src/components/DitoPane.vue +55 -11
- package/src/components/DitoPanel.vue +34 -5
- package/src/components/DitoPanels.vue +2 -1
- package/src/components/DitoRoot.vue +6 -6
- package/src/components/DitoRouterView.vue +27 -0
- package/src/components/DitoSchema.vue +278 -145
- package/src/components/DitoSchemaInlined.vue +1 -1
- package/src/components/DitoTabs.vue +16 -18
- package/src/components/DitoTrail.vue +5 -4
- package/src/components/DitoTreeItem.vue +17 -6
- package/src/components/DitoView.vue +21 -3
- package/src/components/index.js +1 -0
- package/src/mixins/DitoMixin.js +53 -7
- package/src/mixins/ItemMixin.js +3 -6
- package/src/mixins/NumberMixin.js +47 -31
- package/src/mixins/OptionsMixin.js +132 -71
- package/src/mixins/ResourceMixin.js +5 -2
- package/src/mixins/RouteMixin.js +39 -1
- package/src/mixins/SortableMixin.js +0 -13
- package/src/mixins/SourceMixin.js +155 -96
- package/src/mixins/TextMixin.js +1 -1
- package/src/mixins/TypeMixin.js +37 -22
- package/src/mixins/ValidationMixin.js +9 -27
- package/src/mixins/ValidatorMixin.js +5 -7
- package/src/mixins/ValueMixin.js +5 -2
- package/src/types/DitoTypeButton.vue +1 -1
- package/src/types/DitoTypeCheckbox.vue +1 -1
- package/src/types/DitoTypeCheckboxes.vue +1 -1
- package/src/types/DitoTypeCode.vue +8 -1
- package/src/types/DitoTypeColor.vue +20 -34
- package/src/types/DitoTypeComponent.vue +1 -1
- package/src/types/DitoTypeComputed.vue +4 -27
- package/src/types/DitoTypeDate.vue +16 -5
- package/src/types/DitoTypeLabel.vue +1 -4
- package/src/types/DitoTypeList.vue +9 -5
- package/src/types/DitoTypeMarkup.vue +25 -13
- package/src/types/DitoTypeMultiselect.vue +37 -6
- package/src/types/DitoTypeNumber.vue +1 -1
- package/src/types/DitoTypeObject.vue +10 -7
- package/src/types/DitoTypePanel.vue +20 -6
- package/src/types/DitoTypeProgress.vue +1 -1
- package/src/types/DitoTypeRadio.vue +1 -1
- package/src/types/DitoTypeSection.vue +32 -4
- package/src/types/DitoTypeSelect.vue +1 -1
- package/src/types/DitoTypeSlider.vue +1 -1
- package/src/types/DitoTypeSwitch.vue +1 -1
- package/src/types/DitoTypeText.vue +13 -15
- package/src/types/DitoTypeTextarea.vue +1 -1
- package/src/types/DitoTypeTreeList.vue +5 -7
- package/src/types/DitoTypeUpload.vue +1 -2
- package/src/utils/DataSchemaResolver.js +154 -0
- package/src/utils/FormModel.js +615 -0
- package/src/utils/data.js +20 -0
- package/src/utils/filter.js +3 -2
- package/src/utils/options.js +3 -0
- package/src/utils/request.js +70 -0
- package/src/utils/schema/data.js +659 -0
- package/src/utils/schema/lookup.js +250 -0
- package/src/utils/schema/setup.js +354 -0
- package/src/utils/schema/structure.js +172 -0
- package/src/utils/schema/types.js +34 -0
- package/src/utils/schema/validation.js +142 -0
- package/src/utils/units.js +70 -3
- package/types/index.d.ts +174 -59
- package/src/mixins/DataMixin.js +0 -131
- 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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
201
|
-
//
|
|
202
|
-
let data = this.
|
|
203
|
-
if (data) {
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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
|
-
|
|
251
|
-
|
|
252
|
-
this.
|
|
253
|
-
|
|
254
|
-
|
|
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
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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="
|
|
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
|
-
|
|
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="
|
|
5
|
+
:id="tabPanelId"
|
|
6
6
|
v-resize="onResizePane"
|
|
7
7
|
:role="tab ? 'tabpanel' : null"
|
|
8
|
-
:aria-labelledby="
|
|
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="
|
|
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
|
-
|
|
213
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
53
|
+
setupView,
|
|
54
54
|
resolveViews,
|
|
55
|
-
|
|
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
|
|
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]) =>
|
|
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>
|