@ditojs/admin 3.2.3 → 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 +1903 -1865
- package/dist/dito-admin.umd.js +5 -5
- package/package.json +2 -2
- package/src/DitoContext.js +10 -11
- package/src/components/DitoDialog.vue +10 -13
- package/src/components/DitoForm.vue +10 -18
- package/src/components/DitoView.vue +9 -12
- package/src/mixins/DitoMixin.js +24 -27
- package/src/mixins/OptionsMixin.js +20 -4
- package/src/utils/DataModel.js +118 -27
- 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
|
}
|
package/src/DitoContext.js
CHANGED
|
@@ -149,17 +149,16 @@ export default class DitoContext {
|
|
|
149
149
|
}
|
|
150
150
|
|
|
151
151
|
get value() {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
)
|
|
152
|
+
const object = toObject(this)
|
|
153
|
+
// Type components hold their value, also while it's `undefined`, e.g. a
|
|
154
|
+
// computed value that the data model hasn't written yet. Other components
|
|
155
|
+
// don't hold a value, but if they're nested, we can get it from the root,
|
|
156
|
+
// or `undefined` if it's missing there.
|
|
157
|
+
return 'value' in object
|
|
158
|
+
? object.value
|
|
159
|
+
: this.nested
|
|
160
|
+
? getValueAtDataPath(this.rootItem, this.dataPath, () => undefined)
|
|
161
|
+
: undefined
|
|
163
162
|
}
|
|
164
163
|
|
|
165
164
|
get dataPath() {
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
</template>
|
|
33
33
|
|
|
34
34
|
<script>
|
|
35
|
+
import { markRaw } from 'vue'
|
|
35
36
|
import { clone } from '@ditojs/utils'
|
|
36
37
|
import DitoComponent from '../DitoComponent.js'
|
|
37
38
|
import DomMixin from '../mixins/DomMixin.js'
|
|
@@ -47,8 +48,7 @@ export default DitoComponent.component('DitoDialog', {
|
|
|
47
48
|
|
|
48
49
|
provide() {
|
|
49
50
|
return {
|
|
50
|
-
$dialogComponent: () => this
|
|
51
|
-
$dataModel: () => this.ownDataModel
|
|
51
|
+
$dialogComponent: () => this
|
|
52
52
|
}
|
|
53
53
|
},
|
|
54
54
|
|
|
@@ -80,11 +80,6 @@ export default DitoComponent.component('DitoDialog', {
|
|
|
80
80
|
},
|
|
81
81
|
|
|
82
82
|
computed: {
|
|
83
|
-
// @override DitoMixin.dataModel
|
|
84
|
-
dataModel() {
|
|
85
|
-
return this.ownDataModel
|
|
86
|
-
},
|
|
87
|
-
|
|
88
83
|
dialogComponent() {
|
|
89
84
|
return this
|
|
90
85
|
},
|
|
@@ -136,11 +131,13 @@ export default DitoComponent.component('DitoDialog', {
|
|
|
136
131
|
created() {
|
|
137
132
|
// Writes defaults and computed values into the dialog's data and resolves
|
|
138
133
|
// options, see `DataModel`:
|
|
139
|
-
this.ownDataModel =
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
134
|
+
this.ownDataModel = markRaw(
|
|
135
|
+
new DataModel({
|
|
136
|
+
component: this,
|
|
137
|
+
getSchema: () => this.schema,
|
|
138
|
+
getData: () => this.dialogData
|
|
139
|
+
})
|
|
140
|
+
)
|
|
144
141
|
},
|
|
145
142
|
|
|
146
143
|
mounted() {
|
|
@@ -185,7 +182,7 @@ export default DitoComponent.component('DitoDialog', {
|
|
|
185
182
|
|
|
186
183
|
async submit() {
|
|
187
184
|
// Wait for computed values that depend on pending loads, e.g. options.
|
|
188
|
-
await this.
|
|
185
|
+
await this.waitUntilDataModelSettled()
|
|
189
186
|
this.resolve(this.dialogData)
|
|
190
187
|
},
|
|
191
188
|
|
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
</template>
|
|
52
52
|
|
|
53
53
|
<script>
|
|
54
|
+
import { markRaw } from 'vue'
|
|
54
55
|
import { clone, capitalize, parseDataPath, assignDeeply } from '@ditojs/utils'
|
|
55
56
|
import DitoComponent from '../DitoComponent.js'
|
|
56
57
|
import RouteMixin from '../mixins/RouteMixin.js'
|
|
@@ -66,12 +67,6 @@ import { DataModel } from '../utils/DataModel.js'
|
|
|
66
67
|
export default DitoComponent.component('DitoForm', {
|
|
67
68
|
mixins: [RouteMixin, ResourceMixin],
|
|
68
69
|
|
|
69
|
-
provide() {
|
|
70
|
-
return {
|
|
71
|
-
$dataModel: () => this.ownDataModel
|
|
72
|
-
}
|
|
73
|
-
},
|
|
74
|
-
|
|
75
70
|
data() {
|
|
76
71
|
return {
|
|
77
72
|
createdData: null,
|
|
@@ -80,11 +75,6 @@ export default DitoComponent.component('DitoForm', {
|
|
|
80
75
|
},
|
|
81
76
|
|
|
82
77
|
computed: {
|
|
83
|
-
// @override DitoMixin.dataModel
|
|
84
|
-
dataModel() {
|
|
85
|
-
return this.ownDataModel
|
|
86
|
-
},
|
|
87
|
-
|
|
88
78
|
isForm() {
|
|
89
79
|
return true
|
|
90
80
|
},
|
|
@@ -366,12 +356,14 @@ export default DitoComponent.component('DitoForm', {
|
|
|
366
356
|
created() {
|
|
367
357
|
// Writes defaults and computed values into the form's data, resolves
|
|
368
358
|
// options and tracks whether the data is dirty, see `DataModel`:
|
|
369
|
-
this.ownDataModel =
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
359
|
+
this.ownDataModel = markRaw(
|
|
360
|
+
new DataModel({
|
|
361
|
+
component: this,
|
|
362
|
+
getSchema: () => this.schema,
|
|
363
|
+
getData: () => this.data,
|
|
364
|
+
getSourceSchema: () => this.sourceSchema
|
|
365
|
+
})
|
|
366
|
+
)
|
|
375
367
|
},
|
|
376
368
|
|
|
377
369
|
beforeUnmount() {
|
|
@@ -473,7 +465,7 @@ export default DitoComponent.component('DitoForm', {
|
|
|
473
465
|
|
|
474
466
|
async submit(button, { validate = true, closeForm = false } = {}) {
|
|
475
467
|
// Wait for computed values that depend on pending loads, e.g. options.
|
|
476
|
-
await this.
|
|
468
|
+
await this.waitUntilDataModelSettled()
|
|
477
469
|
if (validate && !this.validateAll()) {
|
|
478
470
|
return false
|
|
479
471
|
}
|
|
@@ -28,6 +28,7 @@ template(
|
|
|
28
28
|
</template>
|
|
29
29
|
|
|
30
30
|
<script>
|
|
31
|
+
import { markRaw } from 'vue'
|
|
31
32
|
import DitoComponent from '../DitoComponent.js'
|
|
32
33
|
import RouteMixin from '../mixins/RouteMixin.js'
|
|
33
34
|
import {
|
|
@@ -45,8 +46,7 @@ export default DitoComponent.component('DitoView', {
|
|
|
45
46
|
// Redirect $sourceComponent and $resourceComponent to the main component:
|
|
46
47
|
return {
|
|
47
48
|
$sourceComponent: () => this.mainComponent?.sourceComponent || null,
|
|
48
|
-
$resourceComponent: () => this.mainComponent?.resourceComponent || null
|
|
49
|
-
$dataModel: () => this.ownDataModel
|
|
49
|
+
$resourceComponent: () => this.mainComponent?.resourceComponent || null
|
|
50
50
|
}
|
|
51
51
|
},
|
|
52
52
|
|
|
@@ -61,11 +61,6 @@ export default DitoComponent.component('DitoView', {
|
|
|
61
61
|
},
|
|
62
62
|
|
|
63
63
|
computed: {
|
|
64
|
-
// @override DitoMixin.dataModel
|
|
65
|
-
dataModel() {
|
|
66
|
-
return this.ownDataModel
|
|
67
|
-
},
|
|
68
|
-
|
|
69
64
|
schema() {
|
|
70
65
|
return this.meta.schema ?? {}
|
|
71
66
|
},
|
|
@@ -126,11 +121,13 @@ export default DitoComponent.component('DitoView', {
|
|
|
126
121
|
created() {
|
|
127
122
|
// Writes defaults and computed values into the view's data and resolves
|
|
128
123
|
// options, see `DataModel`:
|
|
129
|
-
this.ownDataModel =
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
124
|
+
this.ownDataModel = markRaw(
|
|
125
|
+
new DataModel({
|
|
126
|
+
component: this,
|
|
127
|
+
getSchema: () => this.viewSchema,
|
|
128
|
+
getData: () => this.data
|
|
129
|
+
})
|
|
130
|
+
)
|
|
134
131
|
},
|
|
135
132
|
|
|
136
133
|
mounted() {
|
package/src/mixins/DitoMixin.js
CHANGED
|
@@ -39,29 +39,28 @@ export default {
|
|
|
39
39
|
|
|
40
40
|
provide() {
|
|
41
41
|
const self = () => this
|
|
42
|
-
return
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
: {
|
|
48
|
-
$parentComponent: self
|
|
49
|
-
}
|
|
42
|
+
return {
|
|
43
|
+
$parentComponent: self,
|
|
44
|
+
...(this.providesData && { $dataComponent: self }),
|
|
45
|
+
$dataModel: () => this.dataModel
|
|
46
|
+
}
|
|
50
47
|
},
|
|
51
48
|
|
|
52
49
|
data() {
|
|
53
50
|
return {
|
|
54
51
|
appState,
|
|
55
52
|
isMounted: false,
|
|
56
|
-
overrides: null // See accessor.js
|
|
53
|
+
overrides: null, // See accessor.js
|
|
54
|
+
// The data model that the component owns, see `dataModel`.
|
|
55
|
+
ownDataModel: null
|
|
57
56
|
}
|
|
58
57
|
},
|
|
59
58
|
|
|
60
59
|
computed: {
|
|
61
|
-
// The data model
|
|
62
|
-
//
|
|
60
|
+
// The data model of the data: the component's own, e.g. of a form, view
|
|
61
|
+
// or dialog, or else the one of its closest ancestor that owns one.
|
|
63
62
|
dataModel() {
|
|
64
|
-
return this.$dataModel()
|
|
63
|
+
return this.ownDataModel ?? this.$dataModel()
|
|
65
64
|
},
|
|
66
65
|
|
|
67
66
|
providesData() {
|
|
@@ -498,17 +497,6 @@ export default {
|
|
|
498
497
|
: parentComponentPath
|
|
499
498
|
},
|
|
500
499
|
|
|
501
|
-
// Waits for the pending loads of the data model, e.g. before submitting,
|
|
502
|
-
// showing the spinner in the header in the meantime, like requests do.
|
|
503
|
-
async waitForPendingLoadsOf(dataModel) {
|
|
504
|
-
this.rootComponent.registerLoading(true)
|
|
505
|
-
try {
|
|
506
|
-
await dataModel.waitForPendingLoads()
|
|
507
|
-
} finally {
|
|
508
|
-
this.rootComponent.registerLoading(false)
|
|
509
|
-
}
|
|
510
|
-
},
|
|
511
|
-
|
|
512
500
|
notify(options) {
|
|
513
501
|
this.rootComponent.notify(options)
|
|
514
502
|
},
|
|
@@ -627,16 +615,25 @@ export default {
|
|
|
627
615
|
// data changes register, e.g. for `processedItem`, and the form
|
|
628
616
|
// model writes the values that it derives from the data.
|
|
629
617
|
// NOTE: The result of `handleListeners()` is returned as expected.
|
|
630
|
-
this.
|
|
618
|
+
this.waitUntilDataModelSettled().then(handleListeners)
|
|
631
619
|
: handleListeners()
|
|
632
620
|
}
|
|
633
621
|
},
|
|
634
622
|
|
|
635
623
|
// Waits until the data model settled: its loads finished, and it wrote the
|
|
636
|
-
// values that it derives from the data.
|
|
637
|
-
|
|
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() {
|
|
638
627
|
await this.$nextTick()
|
|
639
|
-
|
|
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
|
+
}
|
|
640
637
|
},
|
|
641
638
|
|
|
642
639
|
emitSchemaEvent(event, params) {
|
|
@@ -68,10 +68,12 @@ export default {
|
|
|
68
68
|
// Whether the value needs to be replaced with the selected value, once the
|
|
69
69
|
// options are available: if the value is forced to `null` because its
|
|
70
70
|
// option disappeared, or if the value is a reference, so that it'll hold
|
|
71
|
-
// actual data, not just a reference id. See `watch`.
|
|
71
|
+
// actual data, not just a reference id. See `watch`. Filtered options, e.g.
|
|
72
|
+
// the results of a search, don't tell whether the value's option exists.
|
|
72
73
|
shouldReplaceValueWithSelectedValue() {
|
|
73
74
|
return (
|
|
74
|
-
this.hasOptions &&
|
|
75
|
+
this.hasOptions &&
|
|
76
|
+
!this.areOptionsFiltered && (
|
|
75
77
|
this.selectedValue === null && this.value !== null ||
|
|
76
78
|
isReference(this.value)
|
|
77
79
|
)
|
|
@@ -114,6 +116,12 @@ export default {
|
|
|
114
116
|
return this.options
|
|
115
117
|
},
|
|
116
118
|
|
|
119
|
+
// Whether the active options are a filtered subset of the options, e.g.
|
|
120
|
+
// the results of a search, see `activeOptions`.
|
|
121
|
+
areOptionsFiltered() {
|
|
122
|
+
return this.activeOptions !== this.options
|
|
123
|
+
},
|
|
124
|
+
|
|
117
125
|
hasOptions() {
|
|
118
126
|
return this.activeOptions.length > 0
|
|
119
127
|
},
|
|
@@ -304,8 +312,16 @@ export default {
|
|
|
304
312
|
}
|
|
305
313
|
}
|
|
306
314
|
|
|
307
|
-
|
|
308
|
-
|
|
315
|
+
const { optionValue, groupBy } = this
|
|
316
|
+
return optionValue
|
|
317
|
+
? (findOption(this.activeOptions, value, groupBy) ??
|
|
318
|
+
// Options filtered out of the active options, e.g. by a search, are
|
|
319
|
+
// still options of the value.
|
|
320
|
+
(
|
|
321
|
+
this.areOptionsFiltered
|
|
322
|
+
? findOption(this.options, value, groupBy)
|
|
323
|
+
: undefined
|
|
324
|
+
))
|
|
309
325
|
: value
|
|
310
326
|
},
|
|
311
327
|
|
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
|
|
@@ -92,7 +96,7 @@ export class DataModel {
|
|
|
92
96
|
watch(
|
|
93
97
|
[getSchema, getData],
|
|
94
98
|
([schema, data]) => this.initializeData(schema, data),
|
|
95
|
-
|
|
99
|
+
modelSetupWatchOptions
|
|
96
100
|
)
|
|
97
101
|
// The entries are read through a computed property, so that the watchers
|
|
98
102
|
// of the entries can check synchronously whether their entry is still
|
|
@@ -101,19 +105,18 @@ export class DataModel {
|
|
|
101
105
|
watch(
|
|
102
106
|
() => this.dataEntries.value.computedValueEntries,
|
|
103
107
|
entries => this.updateComputedValueRecords(entries),
|
|
104
|
-
|
|
108
|
+
modelSetupWatchOptions
|
|
105
109
|
)
|
|
106
110
|
// Values that go missing after the data was set up, e.g. in items that
|
|
107
111
|
// code adds, get their defaults too:
|
|
108
112
|
watch(
|
|
109
113
|
() => this.dataEntries.value.entriesWithMissingValues,
|
|
110
114
|
entries => this.setDefaultValues(entries),
|
|
111
|
-
|
|
115
|
+
modelSetupWatchOptions
|
|
112
116
|
)
|
|
113
117
|
if (getSourceSchema) {
|
|
114
118
|
// Data that is set up, e.g. loaded, saved or applied, isn't dirty:
|
|
115
|
-
// After the data is set up,
|
|
116
|
-
// they're created, see `modelWatchOptions`.
|
|
119
|
+
// After the data is set up, see `modelSetupWatchOptions`.
|
|
117
120
|
watch(
|
|
118
121
|
getData,
|
|
119
122
|
() => this.takeProcessedDataSnapshot().catch(console.error),
|
|
@@ -167,12 +170,18 @@ export class DataModel {
|
|
|
167
170
|
this.pendingLoads.clear()
|
|
168
171
|
}
|
|
169
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
|
+
|
|
170
179
|
// Waits for the pending loads of data schemas and options, including the
|
|
171
180
|
// ones that they cause, e.g. options that depend on computed values that
|
|
172
181
|
// depend on loaded options, and for the watchers to write the resulting
|
|
173
182
|
// computed values into the data.
|
|
174
|
-
async
|
|
175
|
-
while (this.
|
|
183
|
+
async waitUntilSettled() {
|
|
184
|
+
while (this.hasPendingLoads) {
|
|
176
185
|
await Promise.all(this.pendingLoads)
|
|
177
186
|
await nextTick()
|
|
178
187
|
}
|
|
@@ -181,7 +190,7 @@ export class DataModel {
|
|
|
181
190
|
// Takes the snapshot of the processed data that `isDirty` compares with,
|
|
182
191
|
// when the data is set up, saved or applied. Until the model settled, i.e.
|
|
183
192
|
// the pending loads finished and the computed values that depend on them are
|
|
184
|
-
// written into the data, see `
|
|
193
|
+
// written into the data, see `waitUntilSettled()`, the values that the
|
|
185
194
|
// model writes are recorded in `derivedValueDataPaths` and don't count, as
|
|
186
195
|
// they're derived from the data, e.g. from loaded options. Once settled, they
|
|
187
196
|
// are taken over into the snapshot, so that changes made in the meantime,
|
|
@@ -201,7 +210,7 @@ export class DataModel {
|
|
|
201
210
|
}
|
|
202
211
|
// Let the watchers and the rendering start their loads first:
|
|
203
212
|
await nextTick()
|
|
204
|
-
await this.
|
|
213
|
+
await this.waitUntilSettled()
|
|
205
214
|
// Snapshots that were taken since settle by themselves.
|
|
206
215
|
const isSnapshotReplaced = (
|
|
207
216
|
this.derivedValueDataPaths !==
|
|
@@ -266,7 +275,7 @@ export class DataModel {
|
|
|
266
275
|
if (hasSnapshotOfData) {
|
|
267
276
|
// Let the watchers write the values derived from the changes first:
|
|
268
277
|
await nextTick()
|
|
269
|
-
await this.
|
|
278
|
+
await this.waitUntilSettled()
|
|
270
279
|
// Take the changes over into the current snapshot of the data, which may
|
|
271
280
|
// have been replaced in the meantime, e.g. once the model settled.
|
|
272
281
|
const processedDataSnapshot = this.processedDataSnapshot.value
|
|
@@ -284,7 +293,7 @@ export class DataModel {
|
|
|
284
293
|
}
|
|
285
294
|
|
|
286
295
|
// Tracks the promise of a pending load until it settles, so that
|
|
287
|
-
// `
|
|
296
|
+
// `waitUntilSettled()` waits for it.
|
|
288
297
|
trackPendingLoad(promise) {
|
|
289
298
|
this.pendingLoads.add(promise)
|
|
290
299
|
promise.finally(() => this.pendingLoads.delete(promise))
|
|
@@ -340,16 +349,23 @@ export class DataModel {
|
|
|
340
349
|
// `false`. Unlike `shouldRenderSchema()`, it doesn't evaluate the components
|
|
341
350
|
// of sections and tabs, as the walk of the data visits them anyway, see
|
|
342
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.
|
|
343
354
|
isEntryShown(entry) {
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
355
|
+
if (entry.schema.if === undefined) {
|
|
356
|
+
return true
|
|
357
|
+
}
|
|
358
|
+
try {
|
|
359
|
+
return getSchemaValue('if', {
|
|
347
360
|
type: Boolean,
|
|
348
361
|
schema: entry.schema,
|
|
349
362
|
context: this.createEntryContext(entry),
|
|
350
363
|
default: true
|
|
351
364
|
})
|
|
352
|
-
)
|
|
365
|
+
} catch (error) {
|
|
366
|
+
console.error(error)
|
|
367
|
+
return true
|
|
368
|
+
}
|
|
353
369
|
}
|
|
354
370
|
|
|
355
371
|
// Returns whether the value of the entry is missing and needs its default.
|
|
@@ -372,7 +388,7 @@ export class DataModel {
|
|
|
372
388
|
for (const entry of entries) {
|
|
373
389
|
if (this.shouldSetDefaultValue(entry)) {
|
|
374
390
|
const { schema, data, name } = entry
|
|
375
|
-
|
|
391
|
+
setDefaultValue(schema, data, name, () =>
|
|
376
392
|
this.createEntryContext(entry)
|
|
377
393
|
)
|
|
378
394
|
}
|
|
@@ -533,18 +549,82 @@ export class DataModel {
|
|
|
533
549
|
})
|
|
534
550
|
}
|
|
535
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.
|
|
536
554
|
writeComputedValue(entry, value) {
|
|
537
555
|
const { data, name } = entry
|
|
556
|
+
this.setNestedDefaultValues(entry, value)
|
|
538
557
|
if (!equals(value, data[name])) {
|
|
558
|
+
const dataPath = this.getRelativeDataPath(entry)
|
|
559
|
+
if (this.isWriteLoop(dataPath)) {
|
|
560
|
+
return
|
|
561
|
+
}
|
|
539
562
|
// Values written while the model settles are derived, see
|
|
540
563
|
// `takeProcessedDataSnapshot()`:
|
|
541
|
-
this.derivedValueDataPaths?.add(
|
|
564
|
+
this.derivedValueDataPaths?.add(dataPath)
|
|
542
565
|
// Access `data[name]` directly to update the value without calling
|
|
543
566
|
// `parse()`, see `ValueMixin`:
|
|
544
567
|
data[name] = value
|
|
545
568
|
}
|
|
546
569
|
}
|
|
547
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
|
+
|
|
548
628
|
// Returns the data path of the value of the entry, relative to the model's
|
|
549
629
|
// data, as in its processed data.
|
|
550
630
|
getRelativeDataPath({ schema, name, dataPath }) {
|
|
@@ -641,19 +721,30 @@ function takeOverChangedValues(target, before, after) {
|
|
|
641
721
|
|
|
642
722
|
const notFound = Symbol('notFound')
|
|
643
723
|
|
|
644
|
-
// The watchers of the model
|
|
645
|
-
//
|
|
646
|
-
//
|
|
647
|
-
//
|
|
648
|
-
//
|
|
649
|
-
|
|
650
|
-
|
|
724
|
+
// The watchers of the model that set up the data and the scopes of its
|
|
725
|
+
// components run before the components render, so that components of new
|
|
726
|
+
// data, e.g. loaded data or added items, receive their defaults and computed
|
|
727
|
+
// values from the start: The scopes compute their values right away, see
|
|
728
|
+
// `createComputedValueScope()`.
|
|
729
|
+
const modelSetupWatchOptions = { immediate: true, flush: 'pre' }
|
|
730
|
+
|
|
731
|
+
// The other watchers of the model run after the post-flush hooks, so that they
|
|
732
|
+
// write the values that they derive from the data after the components that
|
|
733
|
+
// display them are mounted. Otherwise, the `mounted` hooks of `v-model`
|
|
734
|
+
// directives would restore the values that inputs had when they were mounted,
|
|
735
|
+
// see https://github.com/vuejs/core/issues/15774. Vue processes the updates
|
|
736
|
+
// that the written values cause in the same flush, before the browser renders
|
|
737
|
+
// them.
|
|
651
738
|
const modelWatchOptions = { immediate: true, flush: 'post' }
|
|
652
739
|
|
|
653
740
|
// Aborts `compute()` when it reads options that aren't loaded, see
|
|
654
741
|
// `DataModel.getComputedValueResult()`.
|
|
655
742
|
const optionsNotLoaded = Symbol('optionsNotLoaded')
|
|
656
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
|
+
|
|
657
748
|
// Returns whether the schema is a source of computed values, through
|
|
658
749
|
// `schema.compute()` or a data schema of the `computed` types.
|
|
659
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])
|