@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.
- package/dist/dito-admin.css +1 -1
- package/dist/dito-admin.es.js +3532 -2763
- package/dist/dito-admin.umd.js +6 -6
- package/package.json +82 -46
- package/src/DitoComponent.js +1 -1
- package/src/DitoContext.js +51 -0
- package/src/DitoTypeComponent.js +6 -1
- package/src/components/DitoAffixes.vue +6 -3
- 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 +156 -83
- package/src/components/DitoLabel.vue +3 -2
- package/src/components/DitoPane.vue +55 -11
- package/src/components/DitoPanel.vue +33 -5
- package/src/components/DitoPanels.vue +2 -1
- package/src/components/DitoRoot.vue +13 -6
- 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 +25 -2
- package/src/mixins/DitoMixin.js +42 -7
- package/src/mixins/ItemMixin.js +3 -6
- package/src/mixins/NumberMixin.js +47 -31
- package/src/mixins/OptionsMixin.js +122 -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 +142 -95
- 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 +26 -6
- package/src/types/DitoTypeNumber.vue +1 -1
- package/src/types/DitoTypeObject.vue +10 -7
- package/src/types/DitoTypePanel.vue +19 -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 +12 -7
- package/src/types/DitoTypeUpload.vue +1 -2
- package/src/utils/DataSchemaResolver.js +141 -0
- package/src/utils/DataSchemaResolver.test.js +132 -0
- package/src/utils/FormModel.js +556 -0
- package/src/utils/FormModel.test.js +808 -0
- package/src/utils/data.js +20 -0
- package/src/utils/data.test.js +28 -0
- package/src/utils/filter.js +3 -2
- package/src/utils/options.js +3 -0
- package/src/utils/schema/data.js +617 -0
- package/src/utils/schema/data.test.js +471 -0
- package/src/utils/schema/lookup.js +266 -0
- package/src/utils/schema/lookup.test.js +61 -0
- package/src/utils/schema/setup.js +354 -0
- package/src/utils/schema/structure.js +172 -0
- package/src/utils/schema/structure.test.js +57 -0
- package/src/utils/schema/types.js +34 -0
- package/src/utils/schema/validation.js +142 -0
- package/src/utils/schema/validation.test.js +395 -0
- package/src/utils/units.js +70 -3
- package/src/utils/units.test.js +71 -0
- package/types/index.d.ts +149 -59
- package/src/mixins/DataMixin.js +0 -131
- 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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
}
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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="
|
|
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()
|
|
@@ -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
|
-
|
|
60
|
+
setupView,
|
|
54
61
|
resolveViews,
|
|
55
|
-
|
|
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
|
|
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]) =>
|
|
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.
|