@ditojs/admin 3.2.7 → 3.3.1
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 +6011 -5826
- package/dist/dito-admin.umd.js +3 -7
- package/package.json +18 -17
- package/src/DitoAdmin.js +44 -24
- package/src/DitoContext.js +28 -2
- package/src/DitoTypeComponent.js +1 -0
- package/src/appState.js +6 -3
- package/src/components/DitoAccount.vue +35 -27
- package/src/components/DitoAffixes.vue +9 -2
- package/src/components/DitoClipboard.vue +59 -93
- package/src/components/DitoConfirmMessage.vue +18 -0
- package/src/components/DitoContainer.vue +56 -18
- package/src/components/DitoCreateButton.vue +36 -84
- package/src/components/DitoDialog.vue +20 -15
- package/src/components/DitoDragHandle.vue +44 -0
- package/src/components/DitoDraggable.vue +43 -16
- package/src/components/DitoEditButtons.vue +48 -24
- package/src/components/DitoErrors.vue +9 -56
- package/src/components/DitoForm.vue +82 -42
- package/src/components/DitoInputAffixes.vue +50 -0
- package/src/components/DitoLabel.vue +1 -15
- package/src/components/DitoMenu.vue +45 -52
- package/src/components/DitoNotifications.vue +38 -14
- package/src/components/DitoOptionList.vue +81 -0
- package/src/components/DitoOptionsEditButtons.vue +57 -0
- package/src/components/DitoPagination.vue +1 -8
- package/src/components/DitoPane.vue +51 -39
- package/src/components/DitoPanel.vue +12 -26
- package/src/components/DitoPanels.vue +11 -5
- package/src/components/DitoResizeHandle.vue +47 -0
- package/src/components/DitoRoot.vue +113 -320
- package/src/components/DitoSchema.vue +72 -63
- package/src/components/DitoSchemaInlined.vue +8 -9
- package/src/components/DitoScopes.vue +14 -18
- package/src/components/DitoSidebar.vue +4 -29
- package/src/components/DitoSourceItem.vue +114 -0
- package/src/components/DitoTableCell.vue +4 -0
- package/src/components/DitoTableHead.vue +15 -17
- package/src/components/DitoTabs.vue +25 -0
- package/src/components/DitoTrail.vue +25 -25
- package/src/components/DitoTreeItem.vue +102 -141
- package/src/components/DitoUploadFile.vue +5 -2
- package/src/components/DitoView.vue +33 -24
- package/src/index.js +4 -1
- package/src/mixins/DitoMixin.js +77 -110
- package/src/mixins/DomMixin.js +12 -14
- package/src/mixins/EmitterMixin.js +3 -2
- package/src/mixins/ItemMixin.js +38 -94
- package/src/mixins/OptionsMixin.js +38 -25
- package/src/mixins/ResourceMixin.js +182 -111
- package/src/mixins/RouteMixin.js +61 -44
- package/src/mixins/SchemaParentMixin.js +4 -1
- package/src/mixins/SourceMixin.js +118 -100
- package/src/mixins/TypeMixin.js +42 -40
- package/src/mixins/ValidationMixin.js +16 -4
- package/src/mixins/ValidatorMixin.js +1 -1
- package/src/styles/_settings.scss +4 -0
- package/src/styles/_tippy.scss +0 -11
- package/src/styles/style.scss +0 -1
- package/src/types/DitoTypeButton.vue +11 -14
- package/src/types/DitoTypeCheckboxes.vue +35 -17
- package/src/types/DitoTypeCode.vue +19 -41
- package/src/types/DitoTypeColor.vue +67 -19
- package/src/types/DitoTypeDate.vue +11 -14
- package/src/types/DitoTypeList.vue +50 -62
- package/src/types/DitoTypeMarkup.vue +41 -54
- package/src/types/DitoTypeMultiselect.vue +82 -52
- package/src/types/DitoTypeNumber.vue +12 -15
- package/src/types/DitoTypeObject.vue +4 -38
- package/src/types/DitoTypeRadio.vue +19 -6
- package/src/types/DitoTypeSection.vue +23 -9
- package/src/types/DitoTypeSelect.vue +39 -23
- package/src/types/DitoTypeSlider.vue +9 -1
- package/src/types/DitoTypeSwitch.vue +1 -1
- package/src/types/DitoTypeText.vue +11 -14
- package/src/types/DitoTypeTreeList.vue +46 -8
- package/src/types/DitoTypeUpload.vue +190 -67
- package/src/utils/DataModel.js +368 -146
- package/src/utils/FileDropTargets.js +97 -0
- package/src/utils/ItemStores.js +31 -0
- package/src/utils/ListQuery.js +115 -0
- package/src/utils/LoadingTracker.js +76 -0
- package/src/utils/SchemaGraph.js +3 -2
- package/src/utils/Session.js +151 -0
- package/src/utils/ViewRegistry.js +85 -0
- package/src/utils/accessor.js +12 -4
- package/src/utils/affixes.js +33 -0
- package/src/utils/changeOnceEdited.js +33 -0
- package/src/utils/clipboard.js +55 -0
- package/src/utils/data.js +10 -5
- package/src/utils/dialogs.js +59 -0
- package/src/utils/dragResize.js +61 -0
- package/src/utils/filter.js +36 -2
- package/src/utils/html.js +9 -0
- package/src/utils/list.js +10 -0
- package/src/utils/options.js +4 -0
- package/src/utils/registration.js +18 -0
- package/src/utils/request.js +108 -13
- package/src/utils/route.js +6 -17
- package/src/utils/schema/data.js +24 -2
- package/src/utils/schema/events.js +19 -0
- package/src/utils/schema/item.js +114 -0
- package/src/utils/schema/lookup.js +39 -14
- package/src/utils/schema/setup.js +5 -5
- package/src/utils/schema/validation.js +16 -2
- package/src/utils/uid.js +105 -8
- package/src/validators/_decimals.js +16 -5
- package/types/index.d.ts +168 -41
- package/src/mixins/LoadingMixin.js +0 -23
- package/src/mixins/PulldownMixin.js +0 -72
- package/src/styles/_pulldown.scss +0 -38
package/src/utils/DataModel.js
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
effectScope,
|
|
3
|
+
computed,
|
|
4
|
+
watch,
|
|
5
|
+
nextTick,
|
|
6
|
+
shallowRef,
|
|
7
|
+
toRaw
|
|
8
|
+
} from 'vue'
|
|
2
9
|
import {
|
|
3
10
|
isArray,
|
|
4
11
|
isPlainObject,
|
|
@@ -9,7 +16,11 @@ import {
|
|
|
9
16
|
} from '@ditojs/utils'
|
|
10
17
|
import DitoContext from '../DitoContext.js'
|
|
11
18
|
import { DataSchemaResolver } from './DataSchemaResolver.js'
|
|
12
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
appendDataPath,
|
|
21
|
+
getParentDataPath,
|
|
22
|
+
getRelativeDataPath
|
|
23
|
+
} from './data.js'
|
|
13
24
|
import { isNested } from './schema/structure.js'
|
|
14
25
|
import { isEmptySchema } from './schema/lookup.js'
|
|
15
26
|
import {
|
|
@@ -42,7 +53,7 @@ import {
|
|
|
42
53
|
// displays them, see `getOptions()`. Both get the same option objects, and
|
|
43
54
|
// computes that read options only run once they are loaded.
|
|
44
55
|
// - Submitting waits for the loads of data schemas and options that are still
|
|
45
|
-
// pending, see `waitUntilSettled()`.
|
|
56
|
+
// pending, for a limited time, see `waitUntilSettled()`.
|
|
46
57
|
// - The data is dirty when its processed data differs from a snapshot taken
|
|
47
58
|
// when the data was set up, saved or applied, see `isDirty`. The values that
|
|
48
59
|
// the model derives until it settled, e.g. from loaded options, don't count,
|
|
@@ -62,12 +73,12 @@ import {
|
|
|
62
73
|
// when present. The model needs to be stopped before the component unmounts.
|
|
63
74
|
|
|
64
75
|
export class DataModel {
|
|
65
|
-
// The
|
|
66
|
-
//
|
|
67
|
-
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
|
|
76
|
+
// The records of the components with computed values or options, by their
|
|
77
|
+
// data and name, so that they're kept when list items move, see
|
|
78
|
+
// `getComponentRecord()`. Records of components that the walk of the data
|
|
79
|
+
// doesn't visit anymore, e.g. of removed items or hidden components, are
|
|
80
|
+
// removed, see `updateComponentRecords()`:
|
|
81
|
+
componentRecords = new EntryMap()
|
|
71
82
|
// The promises of the loads of data schemas and options that are pending,
|
|
72
83
|
// see `waitUntilSettled()`:
|
|
73
84
|
pendingLoads = new Set()
|
|
@@ -78,6 +89,9 @@ export class DataModel {
|
|
|
78
89
|
// the snapshot is taken, which `isDirty` takes from the current data, `null`
|
|
79
90
|
// once it settled, see `takeProcessedDataSnapshot()`:
|
|
80
91
|
derivedValueDataPaths = null
|
|
92
|
+
// The sets of data paths of the values that the model writes while clean
|
|
93
|
+
// changes settle, one per pending `applyCleanChanges()`:
|
|
94
|
+
cleanChangesDerivedValueDataPaths = new Set()
|
|
81
95
|
// The number of writes of computed values per data path in the current
|
|
82
96
|
// microtask, see `isWriteLoop()`:
|
|
83
97
|
computedValueWriteCounts = null
|
|
@@ -99,12 +113,12 @@ export class DataModel {
|
|
|
99
113
|
modelSetupWatchOptions
|
|
100
114
|
)
|
|
101
115
|
// The entries are read through a computed property, so that the watchers
|
|
102
|
-
// of the
|
|
116
|
+
// of the records can check synchronously whether their record is still
|
|
103
117
|
// current, see `createComputedValueScope()`.
|
|
104
118
|
this.dataEntries = computed(() => this.getDataEntries())
|
|
105
119
|
watch(
|
|
106
|
-
() => this.dataEntries.value.
|
|
107
|
-
entries => this.
|
|
120
|
+
() => this.dataEntries.value.recordedEntries,
|
|
121
|
+
entries => this.updateComponentRecords(entries),
|
|
108
122
|
modelSetupWatchOptions
|
|
109
123
|
)
|
|
110
124
|
// Values that go missing after the data was set up, e.g. in items that
|
|
@@ -165,8 +179,7 @@ export class DataModel {
|
|
|
165
179
|
|
|
166
180
|
stop() {
|
|
167
181
|
this.rootScope.stop()
|
|
168
|
-
this.
|
|
169
|
-
this.optionsRecords.clear()
|
|
182
|
+
this.componentRecords.clear()
|
|
170
183
|
this.pendingLoads.clear()
|
|
171
184
|
}
|
|
172
185
|
|
|
@@ -180,10 +193,41 @@ export class DataModel {
|
|
|
180
193
|
// ones that they cause, e.g. options that depend on computed values that
|
|
181
194
|
// depend on loaded options, and for the watchers to write the resulting
|
|
182
195
|
// computed values into the data.
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
196
|
+
// Loads that never finish, e.g. hung requests, would block callers like
|
|
197
|
+
// `submit()` forever. After `timeout` milliseconds, the wait gives up with a
|
|
198
|
+
// warning and resolves, as the server validates the data anyway, while the
|
|
199
|
+
// loads keep running. `timeout: null` waits as long as it takes, for the
|
|
200
|
+
// model's own bookkeeping, which nothing waits for.
|
|
201
|
+
async waitUntilSettled({ timeout = settleTimeout } = {}) {
|
|
202
|
+
let timeoutId = null
|
|
203
|
+
const timedOut =
|
|
204
|
+
timeout != null
|
|
205
|
+
? new Promise(resolve => {
|
|
206
|
+
timeoutId = setTimeout(() => resolve(true), timeout)
|
|
207
|
+
})
|
|
208
|
+
: null
|
|
209
|
+
const settled = (async () => {
|
|
210
|
+
while (this.hasPendingLoads) {
|
|
211
|
+
await Promise.all(this.pendingLoads)
|
|
212
|
+
await nextTick()
|
|
213
|
+
}
|
|
214
|
+
return false
|
|
215
|
+
})()
|
|
216
|
+
try {
|
|
217
|
+
const hasTimedOut = await (
|
|
218
|
+
timedOut
|
|
219
|
+
? Promise.race([settled, timedOut])
|
|
220
|
+
: settled
|
|
221
|
+
)
|
|
222
|
+
if (hasTimedOut) {
|
|
223
|
+
console.warn(
|
|
224
|
+
`The data model didn't settle within ${timeout}ms, as ` +
|
|
225
|
+
`${this.pendingLoads.size} loads of data schemas or options are ` +
|
|
226
|
+
`still pending. Continuing without them.`
|
|
227
|
+
)
|
|
228
|
+
}
|
|
229
|
+
} finally {
|
|
230
|
+
clearTimeout(timeoutId)
|
|
187
231
|
}
|
|
188
232
|
}
|
|
189
233
|
|
|
@@ -210,7 +254,7 @@ export class DataModel {
|
|
|
210
254
|
}
|
|
211
255
|
// Let the watchers and the rendering start their loads first:
|
|
212
256
|
await nextTick()
|
|
213
|
-
await this.waitUntilSettled()
|
|
257
|
+
await this.waitUntilSettled({ timeout: null })
|
|
214
258
|
// Snapshots that were taken since settle by themselves.
|
|
215
259
|
const isSnapshotReplaced = (
|
|
216
260
|
this.derivedValueDataPaths !==
|
|
@@ -259,10 +303,14 @@ export class DataModel {
|
|
|
259
303
|
|
|
260
304
|
// Makes clean changes to the data, which don't make it dirty, while other
|
|
261
305
|
// changes still do, e.g. to apply what an action on the server already
|
|
262
|
-
// saved:
|
|
263
|
-
//
|
|
264
|
-
// the
|
|
265
|
-
//
|
|
306
|
+
// saved: The values that `makeChanges()` changes in the processed data are
|
|
307
|
+
// compared right after it is called synchronously, so that changes made
|
|
308
|
+
// while the model settles, e.g. by the user, aren't included. Once the model
|
|
309
|
+
// settled, they are taken over into the snapshot, along with the values that
|
|
310
|
+
// the model writes in the meantime, e.g. derived from the changes. Like in
|
|
311
|
+
// `takeProcessedDataSnapshot()`, these include the values derived from
|
|
312
|
+
// other changes. Arrays whose length changes are taken over as a whole, see
|
|
313
|
+
// `takeOverChangedValues()`.
|
|
266
314
|
async applyCleanChanges(makeChanges) {
|
|
267
315
|
const data = this.getData()
|
|
268
316
|
const hasSnapshotOfData = this.processedDataSnapshot.value?.data === data
|
|
@@ -273,25 +321,49 @@ export class DataModel {
|
|
|
273
321
|
: null
|
|
274
322
|
makeChanges()
|
|
275
323
|
if (hasSnapshotOfData) {
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
324
|
+
const processedDataAfterChanges = clone(
|
|
325
|
+
this.getProcessedDataForDirtyCheck()
|
|
326
|
+
)
|
|
327
|
+
const derivedValueDataPaths = new Set()
|
|
328
|
+
this.cleanChangesDerivedValueDataPaths.add(derivedValueDataPaths)
|
|
329
|
+
try {
|
|
330
|
+
// Let the watchers write the values derived from the changes first:
|
|
331
|
+
await nextTick()
|
|
332
|
+
await this.waitUntilSettled({ timeout: null })
|
|
333
|
+
} finally {
|
|
334
|
+
this.cleanChangesDerivedValueDataPaths.delete(derivedValueDataPaths)
|
|
335
|
+
}
|
|
279
336
|
// Take the changes over into the current snapshot of the data, which may
|
|
280
337
|
// have been replaced in the meantime, e.g. once the model settled.
|
|
281
338
|
const processedDataSnapshot = this.processedDataSnapshot.value
|
|
282
339
|
if (processedDataSnapshot?.data === data && this.rootScope.active) {
|
|
283
340
|
this.processedDataSnapshot.value = {
|
|
284
341
|
data,
|
|
285
|
-
processedData:
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
342
|
+
processedData: takeOverValuesAtDataPaths(
|
|
343
|
+
takeOverChangedValues(
|
|
344
|
+
processedDataSnapshot.processedData,
|
|
345
|
+
processedDataBeforeChanges,
|
|
346
|
+
processedDataAfterChanges
|
|
347
|
+
),
|
|
348
|
+
this.getProcessedDataForDirtyCheck(),
|
|
349
|
+
derivedValueDataPaths
|
|
289
350
|
)
|
|
290
351
|
}
|
|
291
352
|
}
|
|
292
353
|
}
|
|
293
354
|
}
|
|
294
355
|
|
|
356
|
+
// Records the data path of a computed value that the model writes, as
|
|
357
|
+
// derived while it settles, see `takeProcessedDataSnapshot()` and
|
|
358
|
+
// `applyCleanChanges()`. Defaults don't need to be recorded, as the processed
|
|
359
|
+
// data holds the defaults of missing values already, see `processData()`.
|
|
360
|
+
recordDerivedValueDataPath(dataPath) {
|
|
361
|
+
this.derivedValueDataPaths?.add(dataPath)
|
|
362
|
+
for (const dataPaths of this.cleanChangesDerivedValueDataPaths) {
|
|
363
|
+
dataPaths.add(dataPath)
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
295
367
|
// Tracks the promise of a pending load until it settles, so that
|
|
296
368
|
// `waitUntilSettled()` waits for it.
|
|
297
369
|
trackPendingLoad(promise) {
|
|
@@ -316,24 +388,24 @@ export class DataModel {
|
|
|
316
388
|
|
|
317
389
|
// Walks the data and returns the entries of `processSchemaData()` of the
|
|
318
390
|
// components whose `if` doesn't evaluate to `false`: the ones with computed
|
|
319
|
-
// values, by
|
|
320
|
-
// by a computed property, so that it runs
|
|
321
|
-
// changes, e.g. when list items are added
|
|
322
|
-
// `if` conditions change.
|
|
391
|
+
// values or options, by data and name, see `componentRecords`, and the ones
|
|
392
|
+
// whose values are missing. Called by a computed property, so that it runs
|
|
393
|
+
// again when the data structure changes, e.g. when list items are added,
|
|
394
|
+
// moved or removed, values go missing, or `if` conditions change.
|
|
323
395
|
getDataEntries() {
|
|
324
396
|
const schema = this.getSchema()
|
|
325
397
|
const data = this.getData()
|
|
326
|
-
const
|
|
398
|
+
const recordedEntries = new EntryMap()
|
|
327
399
|
const entriesWithMissingValues = []
|
|
328
400
|
if (data && !isEmptySchema(schema)) {
|
|
329
401
|
processSchemaData(schema, data, {
|
|
330
402
|
dataPath: this.dataPath,
|
|
331
403
|
componentPath: this.componentPath,
|
|
332
|
-
shouldProcess: entry => this.
|
|
404
|
+
shouldProcess: entry => this.shouldRenderEntry(entry),
|
|
333
405
|
shouldSkipSourcesWithResource: true,
|
|
334
406
|
before: entry => {
|
|
335
|
-
if (hasComputedValueSource(entry.schema)) {
|
|
336
|
-
|
|
407
|
+
if (hasComputedValueSource(entry.schema) || entry.schema.options) {
|
|
408
|
+
recordedEntries.set(entry, entry)
|
|
337
409
|
}
|
|
338
410
|
if (this.shouldSetDefaultValue(entry)) {
|
|
339
411
|
entriesWithMissingValues.push(entry)
|
|
@@ -342,7 +414,7 @@ export class DataModel {
|
|
|
342
414
|
options: { component: this.component, rootData: this.rootData }
|
|
343
415
|
})
|
|
344
416
|
}
|
|
345
|
-
return {
|
|
417
|
+
return { recordedEntries, entriesWithMissingValues }
|
|
346
418
|
}
|
|
347
419
|
|
|
348
420
|
// Returns whether the `if` of the entry's component doesn't evaluate to
|
|
@@ -351,7 +423,7 @@ export class DataModel {
|
|
|
351
423
|
// `getDataEntries()`, which leads to the same entries.
|
|
352
424
|
// An `if` that fails only affects its own component, which is treated as
|
|
353
425
|
// shown, not the walk of all others, which would stop computing them.
|
|
354
|
-
|
|
426
|
+
shouldRenderEntry(entry) {
|
|
355
427
|
if (entry.schema.if === undefined) {
|
|
356
428
|
return true
|
|
357
429
|
}
|
|
@@ -395,68 +467,87 @@ export class DataModel {
|
|
|
395
467
|
}
|
|
396
468
|
}
|
|
397
469
|
|
|
398
|
-
// Keeps the records of the components that are still present with
|
|
399
|
-
//
|
|
400
|
-
//
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
} else {
|
|
412
|
-
computedValueRecord = {
|
|
413
|
-
entry,
|
|
414
|
-
scope: this.createComputedValueScope(entry)
|
|
415
|
-
}
|
|
470
|
+
// Keeps the records of the components that are still present, with their
|
|
471
|
+
// current entries, e.g. with the data paths of items that moved, creates
|
|
472
|
+
// records for new ones, and removes the others, stopping their scopes. The
|
|
473
|
+
// scopes of records whose schema changed are replaced.
|
|
474
|
+
updateComponentRecords(entries) {
|
|
475
|
+
const previousRecords = this.componentRecords
|
|
476
|
+
this.componentRecords = new EntryMap()
|
|
477
|
+
for (const entry of entries.values()) {
|
|
478
|
+
const record = previousRecords.get(entry) ?? createComponentRecord(entry)
|
|
479
|
+
previousRecords.delete(entry)
|
|
480
|
+
if (record.entry.schema !== entry.schema) {
|
|
481
|
+
record.computedValueScope?.stop()
|
|
482
|
+
record.computedValueScope = null
|
|
416
483
|
}
|
|
417
|
-
|
|
484
|
+
record.entry = entry
|
|
485
|
+
record.dataPath.value = entry.dataPath
|
|
486
|
+
// Add the record before its scope is created, as its watchers may read
|
|
487
|
+
// its options right away, see `getOptionsResolver()`.
|
|
488
|
+
this.componentRecords.set(entry, record)
|
|
489
|
+
if (hasComputedValueSource(entry.schema)) {
|
|
490
|
+
record.computedValueScope ??= this.createComputedValueScope(record)
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
for (const { computedValueScope } of previousRecords.values()) {
|
|
494
|
+
computedValueScope?.stop()
|
|
418
495
|
}
|
|
419
|
-
|
|
420
|
-
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// Returns the record of the component of the entry, see `componentRecords`,
|
|
499
|
+
// and creates it if the walk of the data didn't, e.g. for components that
|
|
500
|
+
// read their options before it visits them.
|
|
501
|
+
getComponentRecord(entry) {
|
|
502
|
+
let record = this.componentRecords.get(entry)
|
|
503
|
+
if (!record) {
|
|
504
|
+
record = createComponentRecord(entry)
|
|
505
|
+
this.componentRecords.set(entry, record)
|
|
421
506
|
}
|
|
507
|
+
return record
|
|
422
508
|
}
|
|
423
509
|
|
|
424
510
|
// Creates the scope of the watchers that write the computed values of the
|
|
425
|
-
//
|
|
426
|
-
//
|
|
427
|
-
//
|
|
428
|
-
//
|
|
429
|
-
|
|
511
|
+
// record's component into the data, with its current entry, so that the
|
|
512
|
+
// values of items that move are only computed again if their computes read
|
|
513
|
+
// their position, see `getComputedValueResult()`. Records stop being
|
|
514
|
+
// current before their scope is stopped, as watchers that don't belong to
|
|
515
|
+
// components run in the order in which they're triggered, e.g. when list
|
|
516
|
+
// items are removed or `if` conditions change. The watchers skip these
|
|
517
|
+
// records, see `isRecordCurrent()`.
|
|
518
|
+
createComputedValueScope(record) {
|
|
519
|
+
const { schema } = record.entry
|
|
430
520
|
const scope = this.rootScope.run(() => effectScope())
|
|
431
521
|
scope.run(() => {
|
|
432
|
-
const
|
|
433
|
-
|
|
522
|
+
const isRecordCurrent = computed(() =>
|
|
523
|
+
this.isRecordCurrent(record, schema)
|
|
524
|
+
)
|
|
525
|
+
if (schema.compute) {
|
|
434
526
|
watch(
|
|
435
527
|
// Return a new object each time, so that the value is also written
|
|
436
528
|
// when only the value in the data changed, e.g. through user input.
|
|
437
529
|
() =>
|
|
438
|
-
|
|
530
|
+
isRecordCurrent.value
|
|
531
|
+
? this.getComputedValueResult(record)
|
|
532
|
+
: null,
|
|
439
533
|
computedResult => {
|
|
440
534
|
if (computedResult) {
|
|
441
|
-
this.writeComputedValue(entry, computedResult.value)
|
|
535
|
+
this.writeComputedValue(record.entry, computedResult.value)
|
|
442
536
|
}
|
|
443
537
|
},
|
|
444
538
|
modelWatchOptions
|
|
445
539
|
)
|
|
446
540
|
}
|
|
447
|
-
if (hasValueFromDataSchema(
|
|
448
|
-
const dataSchemaResolver = this.createDataSchemaResolver(
|
|
449
|
-
entry.schema,
|
|
450
|
-
entry
|
|
451
|
-
)
|
|
541
|
+
if (hasValueFromDataSchema(schema)) {
|
|
542
|
+
const dataSchemaResolver = this.createDataSchemaResolver(schema, record)
|
|
452
543
|
watch(
|
|
453
544
|
() =>
|
|
454
|
-
|
|
545
|
+
isRecordCurrent.value && !dataSchemaResolver.isLoading
|
|
455
546
|
? { value: dataSchemaResolver.value }
|
|
456
547
|
: null,
|
|
457
548
|
resolved => {
|
|
458
549
|
if (resolved) {
|
|
459
|
-
this.writeComputedValue(entry, resolved.value)
|
|
550
|
+
this.writeComputedValue(record.entry, resolved.value)
|
|
460
551
|
}
|
|
461
552
|
},
|
|
462
553
|
modelWatchOptions
|
|
@@ -466,18 +557,21 @@ export class DataModel {
|
|
|
466
557
|
return scope
|
|
467
558
|
}
|
|
468
559
|
|
|
469
|
-
// Returns whether the model computes the value of the component
|
|
470
|
-
//
|
|
471
|
-
|
|
472
|
-
|
|
560
|
+
// Returns whether the model computes the value of the component with the
|
|
561
|
+
// data and name of the entry, i.e. whether its walk of the data visits the
|
|
562
|
+
// component.
|
|
563
|
+
hasComputedValueEntry(entry) {
|
|
564
|
+
const currentEntry = this.dataEntries.value.recordedEntries.get(entry)
|
|
565
|
+
return !!currentEntry && hasComputedValueSource(currentEntry.schema)
|
|
473
566
|
}
|
|
474
567
|
|
|
475
|
-
// Returns whether the
|
|
476
|
-
// same schema
|
|
477
|
-
|
|
478
|
-
const currentEntry =
|
|
479
|
-
|
|
480
|
-
|
|
568
|
+
// Returns whether the record's component is still one of the current
|
|
569
|
+
// entries, with the same schema, see `getDataEntries()`.
|
|
570
|
+
isRecordCurrent(record, schema) {
|
|
571
|
+
const currentEntry = this.dataEntries.value.recordedEntries.get(
|
|
572
|
+
record.entry
|
|
573
|
+
)
|
|
574
|
+
return currentEntry?.schema === schema
|
|
481
575
|
}
|
|
482
576
|
|
|
483
577
|
// Calls `schema.compute()` with the main schema component of the data, like
|
|
@@ -487,14 +581,24 @@ export class DataModel {
|
|
|
487
581
|
// loaded, which aborts it, so that the current value is kept until they are
|
|
488
582
|
// and `compute()` can rely on them. The watcher that calls it depends on the
|
|
489
583
|
// options through reading them, and calls it again once they're loaded.
|
|
490
|
-
|
|
491
|
-
|
|
584
|
+
// Computes the value of the record's component. Its data path is read
|
|
585
|
+
// through the record, so that only computes that read their position are
|
|
586
|
+
// computed again when their items move, see `updateComponentRecords()`.
|
|
587
|
+
getComputedValueResult(record) {
|
|
588
|
+
const { entry } = record
|
|
589
|
+
const { schema, data, name } = entry
|
|
492
590
|
try {
|
|
493
|
-
const value = computeValue(
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
591
|
+
const value = computeValue(
|
|
592
|
+
schema,
|
|
593
|
+
data,
|
|
594
|
+
name,
|
|
595
|
+
() => record.dataPath.value,
|
|
596
|
+
{
|
|
597
|
+
component: this.component.mainSchemaComponent ?? this.component,
|
|
598
|
+
rootData: this.rootData,
|
|
599
|
+
getOptions: schema.options ? () => this.getLoadedOptions(entry) : null
|
|
600
|
+
}
|
|
601
|
+
)
|
|
498
602
|
return { value }
|
|
499
603
|
} catch (error) {
|
|
500
604
|
if (error === optionsNotLoaded) {
|
|
@@ -505,55 +609,56 @@ export class DataModel {
|
|
|
505
609
|
}
|
|
506
610
|
|
|
507
611
|
// Returns the options of the component of the entry, or aborts the
|
|
508
|
-
// `compute()` that reads them, while they
|
|
509
|
-
// `getComputedValueResult()`.
|
|
612
|
+
// `compute()` that reads them, while they're loading, see
|
|
613
|
+
// `getComputedValueResult()`. Options that resolve to `undefined` aren't
|
|
614
|
+
// loading, e.g. when their load failed, which logs the error, or when their
|
|
615
|
+
// data path points to a missing value: `compute()` sees them as `undefined`,
|
|
616
|
+
// and falls back on `options?.…` like components do, see `OptionsMixin`.
|
|
510
617
|
getLoadedOptions(entry) {
|
|
511
|
-
const
|
|
512
|
-
if (
|
|
618
|
+
const resolver = this.getOptionsResolver(entry)
|
|
619
|
+
if (resolver.isLoading) {
|
|
513
620
|
throw optionsNotLoaded
|
|
514
621
|
}
|
|
515
|
-
return
|
|
622
|
+
return resolver.value
|
|
516
623
|
}
|
|
517
624
|
|
|
518
625
|
// Returns the options of the component of the entry, resolved from
|
|
519
|
-
// `schema.options` when they're first read, `undefined` while loading
|
|
626
|
+
// `schema.options` when they're first read, `undefined` while loading, see
|
|
627
|
+
// `getLoadedOptions()`.
|
|
520
628
|
getOptions(entry) {
|
|
521
629
|
return this.getOptionsResolver(entry).value
|
|
522
630
|
}
|
|
523
631
|
|
|
524
632
|
// Returns the resolver of the options of the component of the entry, shared
|
|
525
|
-
// by all callers with the same
|
|
526
|
-
//
|
|
527
|
-
//
|
|
633
|
+
// by all callers with the same data, name and options, so that computes and
|
|
634
|
+
// the component that displays the options get the same option objects, and
|
|
635
|
+
// the options only load once, also when list items move. Components that
|
|
636
|
+
// keep displaying options of records that were removed keep their own
|
|
637
|
+
// resolvers, see `OptionsMixin`.
|
|
528
638
|
getOptionsResolver(entry) {
|
|
529
|
-
const
|
|
530
|
-
|
|
531
|
-
if (
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
optionsRecord.entry.data !== data
|
|
535
|
-
) {
|
|
536
|
-
optionsRecord = {
|
|
537
|
-
entry,
|
|
538
|
-
resolver: this.createDataSchemaResolver(schema.options, entry)
|
|
539
|
-
}
|
|
540
|
-
this.optionsRecords.set(componentPath, optionsRecord)
|
|
639
|
+
const record = this.getComponentRecord(entry)
|
|
640
|
+
const { options } = entry.schema
|
|
641
|
+
if (!record.optionsResolver || record.optionsSchema !== options) {
|
|
642
|
+
record.optionsSchema = options
|
|
643
|
+
record.optionsResolver = this.createDataSchemaResolver(options, record)
|
|
541
644
|
}
|
|
542
|
-
return
|
|
645
|
+
return record.optionsResolver
|
|
543
646
|
}
|
|
544
647
|
|
|
545
|
-
|
|
648
|
+
// Creates the resolver of a data schema of the record's component, with the
|
|
649
|
+
// context of its current entry.
|
|
650
|
+
createDataSchemaResolver(dataSchema, record) {
|
|
546
651
|
return new DataSchemaResolver(dataSchema, {
|
|
547
|
-
createContext: () => this.createEntryContext(entry),
|
|
652
|
+
createContext: () => this.createEntryContext(record.entry),
|
|
548
653
|
onLoadStart: promise => this.trackPendingLoad(promise)
|
|
549
654
|
})
|
|
550
655
|
}
|
|
551
656
|
|
|
552
657
|
// Writes the computed value into the data, with the defaults of its nested
|
|
553
658
|
// values, unless it equals the value in the data.
|
|
554
|
-
writeComputedValue(entry,
|
|
659
|
+
writeComputedValue(entry, computedValue) {
|
|
555
660
|
const { data, name } = entry
|
|
556
|
-
this.
|
|
661
|
+
const value = this.getValueWithNestedDefaultValues(entry, computedValue)
|
|
557
662
|
if (!equals(value, data[name])) {
|
|
558
663
|
const dataPath = this.getRelativeDataPath(entry)
|
|
559
664
|
if (this.isWriteLoop(dataPath)) {
|
|
@@ -561,40 +666,71 @@ export class DataModel {
|
|
|
561
666
|
}
|
|
562
667
|
// Values written while the model settles are derived, see
|
|
563
668
|
// `takeProcessedDataSnapshot()`:
|
|
564
|
-
this.
|
|
669
|
+
this.recordDerivedValueDataPath(dataPath)
|
|
565
670
|
// Access `data[name]` directly to update the value without calling
|
|
566
671
|
// `parse()`, see `ValueMixin`:
|
|
567
672
|
data[name] = value
|
|
568
673
|
}
|
|
569
674
|
}
|
|
570
675
|
|
|
571
|
-
//
|
|
572
|
-
//
|
|
573
|
-
// `setDefaultValues()` would once the value is written. Otherwise,
|
|
574
|
-
// would disagree on the value and keep replacing each other's
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
676
|
+
// Returns the computed value of the entry with the missing defaults of its
|
|
677
|
+
// nested values, e.g. of the items that `compute()` returns, as
|
|
678
|
+
// `setDefaultValues()` would set them once the value is written. Otherwise,
|
|
679
|
+
// the two would disagree on the value and keep replacing each other's
|
|
680
|
+
// writes. The value isn't modified, as it may be shared, e.g. a constant:
|
|
681
|
+
// The objects and arrays that hold missing values are copied, along with the
|
|
682
|
+
// ones that hold these, and the value is returned as it is when nothing is
|
|
683
|
+
// missing, keeping its identity, e.g. of options.
|
|
684
|
+
getValueWithNestedDefaultValues(entry, value) {
|
|
685
|
+
const { data, name } = entry
|
|
686
|
+
if (
|
|
687
|
+
!isPlainObject(value) && !isArray(value) ||
|
|
688
|
+
// The current value gets its defaults from `setDefaultValues()`:
|
|
689
|
+
toRaw(value) === toRaw(data[name])
|
|
690
|
+
) {
|
|
691
|
+
return value
|
|
579
692
|
}
|
|
580
|
-
|
|
581
|
-
|
|
693
|
+
const dataWithMissingValues = new Set()
|
|
694
|
+
this.processNestedEntries(entry, value, nestedEntry => {
|
|
695
|
+
if (this.shouldSetDefaultValue(nestedEntry)) {
|
|
696
|
+
dataWithMissingValues.add(nestedEntry.data)
|
|
697
|
+
}
|
|
698
|
+
})
|
|
699
|
+
if (dataWithMissingValues.size === 0) {
|
|
700
|
+
return value
|
|
701
|
+
}
|
|
702
|
+
const valueWithDefaults = copyContainersHolding(
|
|
703
|
+
value,
|
|
704
|
+
dataWithMissingValues
|
|
705
|
+
)
|
|
706
|
+
// Defaults may hold values with defaults of their own, which the walk
|
|
707
|
+
// visits after setting them:
|
|
708
|
+
this.processNestedEntries(entry, valueWithDefaults, nestedEntry => {
|
|
709
|
+
if (this.shouldSetDefaultValue(nestedEntry)) {
|
|
710
|
+
setDefaultValue(
|
|
711
|
+
nestedEntry.schema,
|
|
712
|
+
nestedEntry.data,
|
|
713
|
+
nestedEntry.name,
|
|
714
|
+
() => this.createEntryContext(nestedEntry)
|
|
715
|
+
)
|
|
716
|
+
}
|
|
717
|
+
})
|
|
718
|
+
return valueWithDefaults
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// Walks the entries nested in `value`, as the value of the entry's
|
|
722
|
+
// component, in place of its current one in a copy of the data that holds it,
|
|
723
|
+
// and calls `handleNestedEntry()` before visiting each of them.
|
|
724
|
+
processNestedEntries(entry, value, handleNestedEntry) {
|
|
725
|
+
const { schema, data, name, dataPath } = entry
|
|
582
726
|
const valueData = { ...data, [name]: value }
|
|
583
727
|
processSchemaData({ components: { [name]: schema } }, valueData, {
|
|
584
728
|
dataPath: isNested(schema) ? getParentDataPath(dataPath) : dataPath,
|
|
585
|
-
shouldProcess: nestedEntry => this.
|
|
729
|
+
shouldProcess: nestedEntry => this.shouldRenderEntry(nestedEntry),
|
|
586
730
|
shouldSkipSourcesWithResource: true,
|
|
587
731
|
before: nestedEntry => {
|
|
588
|
-
if (
|
|
589
|
-
nestedEntry
|
|
590
|
-
this.shouldSetDefaultValue(nestedEntry)
|
|
591
|
-
) {
|
|
592
|
-
setDefaultValue(
|
|
593
|
-
nestedEntry.schema,
|
|
594
|
-
nestedEntry.data,
|
|
595
|
-
nestedEntry.name,
|
|
596
|
-
() => this.createEntryContext(nestedEntry)
|
|
597
|
-
)
|
|
732
|
+
if (nestedEntry.data !== valueData) {
|
|
733
|
+
handleNestedEntry(nestedEntry)
|
|
598
734
|
}
|
|
599
735
|
},
|
|
600
736
|
options: { component: this.component, rootData: this.rootData }
|
|
@@ -629,10 +765,10 @@ export class DataModel {
|
|
|
629
765
|
// data, as in its processed data.
|
|
630
766
|
getRelativeDataPath({ schema, name, dataPath }) {
|
|
631
767
|
// The data paths of nested components include their own name.
|
|
632
|
-
const
|
|
633
|
-
?
|
|
634
|
-
:
|
|
635
|
-
return
|
|
768
|
+
const valueDataPath = isNested(schema)
|
|
769
|
+
? dataPath
|
|
770
|
+
: appendDataPath(dataPath, name)
|
|
771
|
+
return getRelativeDataPath(valueDataPath, this.dataPath)
|
|
636
772
|
}
|
|
637
773
|
|
|
638
774
|
// Returns the context for `if`, data schemas and options, with the component
|
|
@@ -719,6 +855,32 @@ function takeOverChangedValues(target, before, after) {
|
|
|
719
855
|
return result
|
|
720
856
|
}
|
|
721
857
|
|
|
858
|
+
// Returns `value` with the plain objects and arrays in it that are one of
|
|
859
|
+
// `objects` or hold one of them copied, shallowly, and the others as they are.
|
|
860
|
+
// Values that occur more than once are copied at each occurrence, so that
|
|
861
|
+
// setting defaults in the copies never reaches the originals. Only the values
|
|
862
|
+
// that hold themselves, `ancestorValues`, are left as they are.
|
|
863
|
+
function copyContainersHolding(value, objects, ancestorValues = new Set()) {
|
|
864
|
+
if (
|
|
865
|
+
!isPlainObject(value) && !isArray(value) ||
|
|
866
|
+
ancestorValues.has(value)
|
|
867
|
+
) {
|
|
868
|
+
return value
|
|
869
|
+
}
|
|
870
|
+
ancestorValues.add(value)
|
|
871
|
+
const copyValue = () => (isArray(value) ? [...value] : { ...value })
|
|
872
|
+
let copy = objects.has(value) ? copyValue() : null
|
|
873
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
874
|
+
const entryCopy = copyContainersHolding(entry, objects, ancestorValues)
|
|
875
|
+
if (entryCopy !== entry) {
|
|
876
|
+
copy ??= copyValue()
|
|
877
|
+
copy[key] = entryCopy
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
ancestorValues.delete(value)
|
|
881
|
+
return copy ?? value
|
|
882
|
+
}
|
|
883
|
+
|
|
722
884
|
const notFound = Symbol('notFound')
|
|
723
885
|
|
|
724
886
|
// The watchers of the model that set up the data and the scopes of its
|
|
@@ -745,6 +907,66 @@ const optionsNotLoaded = Symbol('optionsNotLoaded')
|
|
|
745
907
|
// are considered a loop, see `DataModel.isWriteLoop()`.
|
|
746
908
|
const maxComputedValueWrites = 10
|
|
747
909
|
|
|
910
|
+
// The milliseconds after which `DataModel.waitUntilSettled()` stops waiting
|
|
911
|
+
// for pending loads, long enough for slow requests.
|
|
912
|
+
const settleTimeout = 30_000
|
|
913
|
+
|
|
914
|
+
// Creates the record of the component of the entry, see
|
|
915
|
+
// `DataModel.componentRecords`: its current entry, the scope of the watchers
|
|
916
|
+
// that write its computed values, and the resolver of its options, with the
|
|
917
|
+
// options schema that it resolves, once they're read.
|
|
918
|
+
function createComponentRecord(entry) {
|
|
919
|
+
return {
|
|
920
|
+
entry,
|
|
921
|
+
// The data path of the entry, read by computes, see
|
|
922
|
+
// `getComputedValueResult()`. Unlike `entry`, it's reactive, as items
|
|
923
|
+
// that move keep their records.
|
|
924
|
+
dataPath: shallowRef(entry.dataPath),
|
|
925
|
+
computedValueScope: null,
|
|
926
|
+
optionsResolver: null,
|
|
927
|
+
optionsSchema: null
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
// Holds values by the data and name of the entries of components, see
|
|
932
|
+
// `DataModel.componentRecords`. The data is compared by its raw object, as it
|
|
933
|
+
// may be passed through reactive proxies.
|
|
934
|
+
class EntryMap {
|
|
935
|
+
valuesByData = new Map()
|
|
936
|
+
|
|
937
|
+
get({ data, name }) {
|
|
938
|
+
return this.valuesByData.get(toRaw(data))?.get(name)
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
set({ data, name }, value) {
|
|
942
|
+
const rawData = toRaw(data)
|
|
943
|
+
let valuesByName = this.valuesByData.get(rawData)
|
|
944
|
+
if (!valuesByName) {
|
|
945
|
+
valuesByName = new Map()
|
|
946
|
+
this.valuesByData.set(rawData, valuesByName)
|
|
947
|
+
}
|
|
948
|
+
valuesByName.set(name, value)
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
delete({ data, name }) {
|
|
952
|
+
const rawData = toRaw(data)
|
|
953
|
+
const valuesByName = this.valuesByData.get(rawData)
|
|
954
|
+
if (valuesByName?.delete(name) && valuesByName.size === 0) {
|
|
955
|
+
this.valuesByData.delete(rawData)
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
clear() {
|
|
960
|
+
this.valuesByData.clear()
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
*values() {
|
|
964
|
+
for (const valuesByName of this.valuesByData.values()) {
|
|
965
|
+
yield* valuesByName.values()
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
|
|
748
970
|
// Returns whether the schema is a source of computed values, through
|
|
749
971
|
// `schema.compute()` or a data schema of the `computed` types.
|
|
750
972
|
function hasComputedValueSource(schema) {
|