@ditojs/admin 3.2.4 → 3.2.5
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.es.js +1122 -1077
- package/dist/dito-admin.umd.js +5 -5
- package/package.json +2 -2
- package/src/components/DitoDialog.vue +1 -1
- package/src/components/DitoForm.vue +1 -1
- package/src/mixins/DitoMixin.js +13 -15
- package/src/utils/DataModel.js +100 -15
- package/src/utils/schema/data.js +12 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ditojs/admin",
|
|
3
|
-
"version": "3.2.
|
|
3
|
+
"version": "3.2.5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Dito.js Admin is a schema based admin interface for Dito.js Server, featuring auto-generated views and forms and built with Vue.js",
|
|
6
6
|
"repository": "https://github.com/ditojs/dito/tree/main/packages/admin",
|
|
@@ -131,5 +131,5 @@
|
|
|
131
131
|
"vue-router": "^5.3.1",
|
|
132
132
|
"vue-upload-component": "^3.2.2"
|
|
133
133
|
},
|
|
134
|
-
"gitHead": "
|
|
134
|
+
"gitHead": "f149113bc95317c74b17f4af56ccfebed789c47b"
|
|
135
135
|
}
|
|
@@ -182,7 +182,7 @@ export default DitoComponent.component('DitoDialog', {
|
|
|
182
182
|
|
|
183
183
|
async submit() {
|
|
184
184
|
// Wait for computed values that depend on pending loads, e.g. options.
|
|
185
|
-
await this.
|
|
185
|
+
await this.waitUntilDataModelSettled()
|
|
186
186
|
this.resolve(this.dialogData)
|
|
187
187
|
},
|
|
188
188
|
|
|
@@ -465,7 +465,7 @@ export default DitoComponent.component('DitoForm', {
|
|
|
465
465
|
|
|
466
466
|
async submit(button, { validate = true, closeForm = false } = {}) {
|
|
467
467
|
// Wait for computed values that depend on pending loads, e.g. options.
|
|
468
|
-
await this.
|
|
468
|
+
await this.waitUntilDataModelSettled()
|
|
469
469
|
if (validate && !this.validateAll()) {
|
|
470
470
|
return false
|
|
471
471
|
}
|
package/src/mixins/DitoMixin.js
CHANGED
|
@@ -497,17 +497,6 @@ export default {
|
|
|
497
497
|
: parentComponentPath
|
|
498
498
|
},
|
|
499
499
|
|
|
500
|
-
// Waits for the pending loads of the data model, e.g. before submitting,
|
|
501
|
-
// showing the spinner in the header in the meantime, like requests do.
|
|
502
|
-
async waitForPendingLoadsOf(dataModel) {
|
|
503
|
-
this.rootComponent.registerLoading(true)
|
|
504
|
-
try {
|
|
505
|
-
await dataModel.waitForPendingLoads()
|
|
506
|
-
} finally {
|
|
507
|
-
this.rootComponent.registerLoading(false)
|
|
508
|
-
}
|
|
509
|
-
},
|
|
510
|
-
|
|
511
500
|
notify(options) {
|
|
512
501
|
this.rootComponent.notify(options)
|
|
513
502
|
},
|
|
@@ -626,16 +615,25 @@ export default {
|
|
|
626
615
|
// data changes register, e.g. for `processedItem`, and the form
|
|
627
616
|
// model writes the values that it derives from the data.
|
|
628
617
|
// NOTE: The result of `handleListeners()` is returned as expected.
|
|
629
|
-
this.
|
|
618
|
+
this.waitUntilDataModelSettled().then(handleListeners)
|
|
630
619
|
: handleListeners()
|
|
631
620
|
}
|
|
632
621
|
},
|
|
633
622
|
|
|
634
623
|
// Waits until the data model settled: its loads finished, and it wrote the
|
|
635
|
-
// values that it derives from the data.
|
|
636
|
-
|
|
624
|
+
// values that it derives from the data, e.g. before submitting. Shows the
|
|
625
|
+
// spinner in the header while loads are pending, like requests do.
|
|
626
|
+
async waitUntilDataModelSettled() {
|
|
637
627
|
await this.$nextTick()
|
|
638
|
-
|
|
628
|
+
const { dataModel } = this
|
|
629
|
+
if (dataModel?.hasPendingLoads) {
|
|
630
|
+
this.rootComponent.registerLoading(true)
|
|
631
|
+
try {
|
|
632
|
+
await dataModel.waitUntilSettled()
|
|
633
|
+
} finally {
|
|
634
|
+
this.rootComponent.registerLoading(false)
|
|
635
|
+
}
|
|
636
|
+
}
|
|
639
637
|
},
|
|
640
638
|
|
|
641
639
|
emitSchemaEvent(event, params) {
|
package/src/utils/DataModel.js
CHANGED
|
@@ -9,12 +9,13 @@ import {
|
|
|
9
9
|
} from '@ditojs/utils'
|
|
10
10
|
import DitoContext from '../DitoContext.js'
|
|
11
11
|
import { DataSchemaResolver } from './DataSchemaResolver.js'
|
|
12
|
+
import { getParentDataPath } from './data.js'
|
|
12
13
|
import { isNested } from './schema/structure.js'
|
|
13
14
|
import { isEmptySchema } from './schema/lookup.js'
|
|
14
15
|
import {
|
|
15
16
|
getSchemaValue,
|
|
16
17
|
shouldSetDefaultValue,
|
|
17
|
-
|
|
18
|
+
setDefaultValue,
|
|
18
19
|
initializeData,
|
|
19
20
|
computeValue,
|
|
20
21
|
hasValueFromDataSchema,
|
|
@@ -41,7 +42,7 @@ import {
|
|
|
41
42
|
// displays them, see `getOptions()`. Both get the same option objects, and
|
|
42
43
|
// computes that read options only run once they are loaded.
|
|
43
44
|
// - Submitting waits for the loads of data schemas and options that are still
|
|
44
|
-
// pending, see `
|
|
45
|
+
// pending, see `waitUntilSettled()`.
|
|
45
46
|
// - The data is dirty when its processed data differs from a snapshot taken
|
|
46
47
|
// when the data was set up, saved or applied, see `isDirty`. The values that
|
|
47
48
|
// the model derives until it settled, e.g. from loaded options, don't count,
|
|
@@ -68,7 +69,7 @@ export class DataModel {
|
|
|
68
69
|
// of their options, by component path, see `getOptionsResolver()`:
|
|
69
70
|
optionsRecords = new Map()
|
|
70
71
|
// The promises of the loads of data schemas and options that are pending,
|
|
71
|
-
// see `
|
|
72
|
+
// see `waitUntilSettled()`:
|
|
72
73
|
pendingLoads = new Set()
|
|
73
74
|
// The data that the snapshot was taken of, and its processed data to compare
|
|
74
75
|
// with in `isDirty`, see `takeProcessedDataSnapshot()`:
|
|
@@ -77,6 +78,9 @@ export class DataModel {
|
|
|
77
78
|
// the snapshot is taken, which `isDirty` takes from the current data, `null`
|
|
78
79
|
// once it settled, see `takeProcessedDataSnapshot()`:
|
|
79
80
|
derivedValueDataPaths = null
|
|
81
|
+
// The number of writes of computed values per data path in the current
|
|
82
|
+
// microtask, see `isWriteLoop()`:
|
|
83
|
+
computedValueWriteCounts = null
|
|
80
84
|
|
|
81
85
|
// `getSchema()` and `getData()` return the schema and the data that the
|
|
82
86
|
// model handles. `getSourceSchema()` returns the schema of the source that
|
|
@@ -166,12 +170,18 @@ export class DataModel {
|
|
|
166
170
|
this.pendingLoads.clear()
|
|
167
171
|
}
|
|
168
172
|
|
|
173
|
+
// Whether loads of data schemas or options are pending, see
|
|
174
|
+
// `waitUntilSettled()`.
|
|
175
|
+
get hasPendingLoads() {
|
|
176
|
+
return this.pendingLoads.size > 0
|
|
177
|
+
}
|
|
178
|
+
|
|
169
179
|
// Waits for the pending loads of data schemas and options, including the
|
|
170
180
|
// ones that they cause, e.g. options that depend on computed values that
|
|
171
181
|
// depend on loaded options, and for the watchers to write the resulting
|
|
172
182
|
// computed values into the data.
|
|
173
|
-
async
|
|
174
|
-
while (this.
|
|
183
|
+
async waitUntilSettled() {
|
|
184
|
+
while (this.hasPendingLoads) {
|
|
175
185
|
await Promise.all(this.pendingLoads)
|
|
176
186
|
await nextTick()
|
|
177
187
|
}
|
|
@@ -180,7 +190,7 @@ export class DataModel {
|
|
|
180
190
|
// Takes the snapshot of the processed data that `isDirty` compares with,
|
|
181
191
|
// when the data is set up, saved or applied. Until the model settled, i.e.
|
|
182
192
|
// the pending loads finished and the computed values that depend on them are
|
|
183
|
-
// written into the data, see `
|
|
193
|
+
// written into the data, see `waitUntilSettled()`, the values that the
|
|
184
194
|
// model writes are recorded in `derivedValueDataPaths` and don't count, as
|
|
185
195
|
// they're derived from the data, e.g. from loaded options. Once settled, they
|
|
186
196
|
// are taken over into the snapshot, so that changes made in the meantime,
|
|
@@ -200,7 +210,7 @@ export class DataModel {
|
|
|
200
210
|
}
|
|
201
211
|
// Let the watchers and the rendering start their loads first:
|
|
202
212
|
await nextTick()
|
|
203
|
-
await this.
|
|
213
|
+
await this.waitUntilSettled()
|
|
204
214
|
// Snapshots that were taken since settle by themselves.
|
|
205
215
|
const isSnapshotReplaced = (
|
|
206
216
|
this.derivedValueDataPaths !==
|
|
@@ -265,7 +275,7 @@ export class DataModel {
|
|
|
265
275
|
if (hasSnapshotOfData) {
|
|
266
276
|
// Let the watchers write the values derived from the changes first:
|
|
267
277
|
await nextTick()
|
|
268
|
-
await this.
|
|
278
|
+
await this.waitUntilSettled()
|
|
269
279
|
// Take the changes over into the current snapshot of the data, which may
|
|
270
280
|
// have been replaced in the meantime, e.g. once the model settled.
|
|
271
281
|
const processedDataSnapshot = this.processedDataSnapshot.value
|
|
@@ -283,7 +293,7 @@ export class DataModel {
|
|
|
283
293
|
}
|
|
284
294
|
|
|
285
295
|
// Tracks the promise of a pending load until it settles, so that
|
|
286
|
-
// `
|
|
296
|
+
// `waitUntilSettled()` waits for it.
|
|
287
297
|
trackPendingLoad(promise) {
|
|
288
298
|
this.pendingLoads.add(promise)
|
|
289
299
|
promise.finally(() => this.pendingLoads.delete(promise))
|
|
@@ -339,16 +349,23 @@ export class DataModel {
|
|
|
339
349
|
// `false`. Unlike `shouldRenderSchema()`, it doesn't evaluate the components
|
|
340
350
|
// of sections and tabs, as the walk of the data visits them anyway, see
|
|
341
351
|
// `getDataEntries()`, which leads to the same entries.
|
|
352
|
+
// An `if` that fails only affects its own component, which is treated as
|
|
353
|
+
// shown, not the walk of all others, which would stop computing them.
|
|
342
354
|
isEntryShown(entry) {
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
355
|
+
if (entry.schema.if === undefined) {
|
|
356
|
+
return true
|
|
357
|
+
}
|
|
358
|
+
try {
|
|
359
|
+
return getSchemaValue('if', {
|
|
346
360
|
type: Boolean,
|
|
347
361
|
schema: entry.schema,
|
|
348
362
|
context: this.createEntryContext(entry),
|
|
349
363
|
default: true
|
|
350
364
|
})
|
|
351
|
-
)
|
|
365
|
+
} catch (error) {
|
|
366
|
+
console.error(error)
|
|
367
|
+
return true
|
|
368
|
+
}
|
|
352
369
|
}
|
|
353
370
|
|
|
354
371
|
// Returns whether the value of the entry is missing and needs its default.
|
|
@@ -371,7 +388,7 @@ export class DataModel {
|
|
|
371
388
|
for (const entry of entries) {
|
|
372
389
|
if (this.shouldSetDefaultValue(entry)) {
|
|
373
390
|
const { schema, data, name } = entry
|
|
374
|
-
|
|
391
|
+
setDefaultValue(schema, data, name, () =>
|
|
375
392
|
this.createEntryContext(entry)
|
|
376
393
|
)
|
|
377
394
|
}
|
|
@@ -532,18 +549,82 @@ export class DataModel {
|
|
|
532
549
|
})
|
|
533
550
|
}
|
|
534
551
|
|
|
552
|
+
// Writes the computed value into the data, with the defaults of its nested
|
|
553
|
+
// values, unless it equals the value in the data.
|
|
535
554
|
writeComputedValue(entry, value) {
|
|
536
555
|
const { data, name } = entry
|
|
556
|
+
this.setNestedDefaultValues(entry, value)
|
|
537
557
|
if (!equals(value, data[name])) {
|
|
558
|
+
const dataPath = this.getRelativeDataPath(entry)
|
|
559
|
+
if (this.isWriteLoop(dataPath)) {
|
|
560
|
+
return
|
|
561
|
+
}
|
|
538
562
|
// Values written while the model settles are derived, see
|
|
539
563
|
// `takeProcessedDataSnapshot()`:
|
|
540
|
-
this.derivedValueDataPaths?.add(
|
|
564
|
+
this.derivedValueDataPaths?.add(dataPath)
|
|
541
565
|
// Access `data[name]` directly to update the value without calling
|
|
542
566
|
// `parse()`, see `ValueMixin`:
|
|
543
567
|
data[name] = value
|
|
544
568
|
}
|
|
545
569
|
}
|
|
546
570
|
|
|
571
|
+
// Sets the missing defaults of the values nested in the computed value of
|
|
572
|
+
// the entry, e.g. of the items that `compute()` returns, as
|
|
573
|
+
// `setDefaultValues()` would once the value is written. Otherwise, the two
|
|
574
|
+
// would disagree on the value and keep replacing each other's writes.
|
|
575
|
+
setNestedDefaultValues(entry, value) {
|
|
576
|
+
const { schema, data, name, dataPath } = entry
|
|
577
|
+
if (!isPlainObject(value) && !isArray(value)) {
|
|
578
|
+
return
|
|
579
|
+
}
|
|
580
|
+
// Walk the computed value in place of the current one, in a copy of the
|
|
581
|
+
// data that holds it.
|
|
582
|
+
const valueData = { ...data, [name]: value }
|
|
583
|
+
processSchemaData({ components: { [name]: schema } }, valueData, {
|
|
584
|
+
dataPath: isNested(schema) ? getParentDataPath(dataPath) : dataPath,
|
|
585
|
+
shouldProcess: nestedEntry => this.isEntryShown(nestedEntry),
|
|
586
|
+
shouldSkipSourcesWithResource: true,
|
|
587
|
+
before: nestedEntry => {
|
|
588
|
+
if (
|
|
589
|
+
nestedEntry.data !== valueData &&
|
|
590
|
+
this.shouldSetDefaultValue(nestedEntry)
|
|
591
|
+
) {
|
|
592
|
+
setDefaultValue(
|
|
593
|
+
nestedEntry.schema,
|
|
594
|
+
nestedEntry.data,
|
|
595
|
+
nestedEntry.name,
|
|
596
|
+
() => this.createEntryContext(nestedEntry)
|
|
597
|
+
)
|
|
598
|
+
}
|
|
599
|
+
},
|
|
600
|
+
options: { component: this.component, rootData: this.rootData }
|
|
601
|
+
})
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// Returns whether the computed value at the data path was written too often
|
|
605
|
+
// in the current microtask, which happens when the sources of the value keep
|
|
606
|
+
// disagreeing on it, e.g. two computes. Reports the data path once, instead
|
|
607
|
+
// of letting the writes loop endlessly.
|
|
608
|
+
isWriteLoop(dataPath) {
|
|
609
|
+
if (!this.computedValueWriteCounts) {
|
|
610
|
+
this.computedValueWriteCounts = new Map()
|
|
611
|
+
queueMicrotask(() => {
|
|
612
|
+
this.computedValueWriteCounts = null
|
|
613
|
+
})
|
|
614
|
+
}
|
|
615
|
+
const count = (this.computedValueWriteCounts.get(dataPath) ?? 0) + 1
|
|
616
|
+
this.computedValueWriteCounts.set(dataPath, count)
|
|
617
|
+
if (count === maxComputedValueWrites + 1) {
|
|
618
|
+
console.error(
|
|
619
|
+
new Error(
|
|
620
|
+
`The computed value at '${dataPath}' keeps changing, as its ` +
|
|
621
|
+
`sources disagree on it. It isn't written anymore in this update.`
|
|
622
|
+
)
|
|
623
|
+
)
|
|
624
|
+
}
|
|
625
|
+
return count > maxComputedValueWrites
|
|
626
|
+
}
|
|
627
|
+
|
|
547
628
|
// Returns the data path of the value of the entry, relative to the model's
|
|
548
629
|
// data, as in its processed data.
|
|
549
630
|
getRelativeDataPath({ schema, name, dataPath }) {
|
|
@@ -660,6 +741,10 @@ const modelWatchOptions = { immediate: true, flush: 'post' }
|
|
|
660
741
|
// `DataModel.getComputedValueResult()`.
|
|
661
742
|
const optionsNotLoaded = Symbol('optionsNotLoaded')
|
|
662
743
|
|
|
744
|
+
// The number of writes of a computed value in one microtask after which they
|
|
745
|
+
// are considered a loop, see `DataModel.isWriteLoop()`.
|
|
746
|
+
const maxComputedValueWrites = 10
|
|
747
|
+
|
|
663
748
|
// Returns whether the schema is a source of computed values, through
|
|
664
749
|
// `schema.compute()` or a data schema of the `computed` types.
|
|
665
750
|
function hasComputedValueSource(schema) {
|
package/src/utils/schema/data.js
CHANGED
|
@@ -110,6 +110,17 @@ export function getDefaultValue(schema, context) {
|
|
|
110
110
|
: clone(defaultValue)
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
+
// Sets the value of the component described by `schema` and `name` in `data`
|
|
114
|
+
// to its default. Types without values, e.g. buttons, have no default, and
|
|
115
|
+
// don't get a key in the data, which would make the data differ from data
|
|
116
|
+
// without it, e.g. from items that `compute()` returns.
|
|
117
|
+
export function setDefaultValue(schema, data, name, context) {
|
|
118
|
+
const value = getDefaultValue(schema, context)
|
|
119
|
+
if (value !== undefined) {
|
|
120
|
+
data[name] = value
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
113
124
|
export function shouldExcludeValue(schema, context) {
|
|
114
125
|
const excludeValue =
|
|
115
126
|
schema.exclude !== undefined
|
|
@@ -207,7 +218,7 @@ export function initializeData(schema, data = {}, component, {
|
|
|
207
218
|
{ shouldSetDefaultsOfComponentsWithCompute }
|
|
208
219
|
)
|
|
209
220
|
if (shouldSetDefault) {
|
|
210
|
-
|
|
221
|
+
setDefaultValue(schema, data, name, context)
|
|
211
222
|
}
|
|
212
223
|
if (hasItemsNumberedByOrderKey(schema) && isArray(data[name])) {
|
|
213
224
|
updateOrder(schema, data[name])
|