@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ditojs/admin",
3
- "version": "3.2.4",
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": "32147a4ee71f5bca6c051af04be9c15a16f37567"
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.waitForPendingLoadsOf(this.dataModel)
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.waitForPendingLoadsOf(this.dataModel)
468
+ await this.waitUntilDataModelSettled()
469
469
  if (validate && !this.validateAll()) {
470
470
  return false
471
471
  }
@@ -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.waitForDataModel().then(handleListeners)
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
- async waitForDataModel() {
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
- await this.dataModel?.waitForPendingLoads()
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) {
@@ -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
- getDefaultValue,
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 `waitForPendingLoads()`.
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 `waitForPendingLoads()`:
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 waitForPendingLoads() {
174
- while (this.pendingLoads.size > 0) {
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 `waitForPendingLoads()`, the values that the
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.waitForPendingLoads()
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.waitForPendingLoads()
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
- // `waitForPendingLoads()` waits for it.
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
- return (
344
- entry.schema.if === undefined ||
345
- getSchemaValue('if', {
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
- data[name] = getDefaultValue(schema, () =>
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(this.getRelativeDataPath(entry))
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) {
@@ -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
- data[name] = getDefaultValue(schema, context)
221
+ setDefaultValue(schema, data, name, context)
211
222
  }
212
223
  if (hasItemsNumberedByOrderKey(schema) && isArray(data[name])) {
213
224
  updateOrder(schema, data[name])