@ditojs/admin 3.1.3 → 3.2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ditojs/admin",
3
- "version": "3.1.3",
3
+ "version": "3.2.1",
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",
@@ -41,8 +41,8 @@
41
41
  "not ie_mob > 0"
42
42
  ],
43
43
  "dependencies": {
44
- "@ditojs/ui": "^3.1.3",
45
- "@ditojs/utils": "^3.1.3"
44
+ "@ditojs/ui": "^3.2.0",
45
+ "@ditojs/utils": "^3.2.0"
46
46
  },
47
47
  "peerDependencies": {
48
48
  "@kyvg/vue3-notification": "^3.4.2",
@@ -87,7 +87,7 @@
87
87
  "vue-upload-component": "^3.2.2"
88
88
  },
89
89
  "devDependencies": {
90
- "@ditojs/build": "^3.1.3",
90
+ "@ditojs/build": "^3.2.0",
91
91
  "@kyvg/vue3-notification": "^3.4.2",
92
92
  "@tiptap/core": "^3.31.4",
93
93
  "@tiptap/extension-blockquote": "^3.31.4",
@@ -131,5 +131,5 @@
131
131
  "vue-router": "^5.3.1",
132
132
  "vue-upload-component": "^3.2.2"
133
133
  },
134
- "gitHead": "3dba854ad248a56da1614122b960ad8a7eda70dd"
134
+ "gitHead": "c81ea5a206255d00588ea8b23f9df34af52cae01"
135
135
  }
@@ -189,6 +189,7 @@ export default {
189
189
  created() {
190
190
  this._register(true)
191
191
  this.setupSchemaFields()
192
+ this.warnAboutUncomputedValue()
192
193
  },
193
194
 
194
195
  unmounted() {
@@ -196,6 +197,21 @@ export default {
196
197
  },
197
198
 
198
199
  methods: {
200
+ // Only the form model calls `compute()`, see `computeValue()`. Components
201
+ // that it doesn't visit, e.g. in panels with their own data, never get
202
+ // their values computed.
203
+ warnAboutUncomputedValue() {
204
+ if (
205
+ this.schema.compute &&
206
+ !this.formModel.hasComputedValueEntry(this.componentPath)
207
+ ) {
208
+ console.warn(
209
+ `The value of the component at '${this.componentPath}' isn't ` +
210
+ `computed, as its data isn't part of a form model.`
211
+ )
212
+ }
213
+ },
214
+
199
215
  _register(add) {
200
216
  // Provide component to container for schema accessor evaluation.
201
217
  this.$emit('update:component', add ? this : null)
@@ -1,17 +1,25 @@
1
1
  import { isFunction } from '@ditojs/utils'
2
2
  import DitoContext from '../DitoContext.js'
3
- import { computeValue } from '../utils/schema/data.js'
3
+ import { getValueOrDefault } from '../utils/schema/data.js'
4
4
 
5
5
  export default {
6
6
  computed: {
7
7
  value: {
8
8
  get() {
9
- const value = computeValue(
9
+ // `FormModel` writes computed values into the data, see
10
+ // `computeValue()`.
11
+ const value = getValueOrDefault(
10
12
  this.schema,
11
13
  this.data,
12
14
  this.name,
13
- this.dataPath,
14
- { component: this }
15
+ () =>
16
+ DitoContext.createForSchema(this, {
17
+ schema: this.schema,
18
+ name: this.name,
19
+ data: this.data,
20
+ dataPath: this.dataPath,
21
+ rootData: this.rootData
22
+ })
15
23
  )
16
24
  // Only call `format()` if it's a function, as some types use `format`
17
25
  // for other purposes, e.g. `DitoTypeColor` for the color format.
@@ -92,7 +92,7 @@ export class FormModel {
92
92
  watch(
93
93
  [getSchema, getData],
94
94
  ([schema, data]) => this.initializeData(schema, data),
95
- { immediate: true }
95
+ modelWatchOptions
96
96
  )
97
97
  // The entries are read through a computed property, so that the watchers
98
98
  // of the entries can check synchronously whether their entry is still
@@ -101,21 +101,23 @@ export class FormModel {
101
101
  watch(
102
102
  () => this.dataEntries.value.computedValueEntries,
103
103
  entries => this.updateComputedValueRecords(entries),
104
- { immediate: true }
104
+ modelWatchOptions
105
105
  )
106
106
  // Values that go missing after the data was set up, e.g. in items that
107
107
  // code adds, get their defaults too:
108
108
  watch(
109
109
  () => this.dataEntries.value.entriesWithMissingValues,
110
110
  entries => this.setDefaultValues(entries),
111
- { immediate: true }
111
+ modelWatchOptions
112
112
  )
113
113
  if (getSourceSchema) {
114
114
  // Data that is set up, e.g. loaded, saved or applied, isn't dirty:
115
+ // After the data is set up, as the watchers run in the order in which
116
+ // they're created, see `modelWatchOptions`.
115
117
  watch(
116
118
  getData,
117
119
  () => this.takeProcessedDataSnapshot().catch(console.error),
118
- { immediate: true }
120
+ modelWatchOptions
119
121
  )
120
122
  }
121
123
  })
@@ -234,14 +236,14 @@ export class FormModel {
234
236
  const data = this.getData()
235
237
  return data && !isEmptySchema(schema)
236
238
  ? processData(schema, this.getSourceSchema(), data, this.dataPath, {
237
- // Like `getComputedValue()`:
239
+ // Like `getComputedValueResult()`:
238
240
  component: this.component.mainSchemaComponent ?? this.component,
239
241
  rootData: this.rootData,
240
242
  schemaOnly: true,
241
243
  target: 'clipboard',
242
- // Reading the dirty state doesn't call `compute()` and `process()`,
243
- // see `processData()`:
244
- shouldCallComputeAndProcess: false
244
+ // Reading the dirty state doesn't call `process()`, see
245
+ // `processData()`:
246
+ shouldCallProcess: false
245
247
  })
246
248
  : null
247
249
  }
@@ -417,15 +419,13 @@ export class FormModel {
417
419
  // Return a new object each time, so that the value is also written
418
420
  // when only the value in the data changed, e.g. through user input.
419
421
  () =>
420
- isEntryCurrent.value
421
- ? { value: this.getComputedValue(entry) }
422
- : null,
422
+ isEntryCurrent.value ? this.getComputedValueResult(entry) : null,
423
423
  computedResult => {
424
424
  if (computedResult) {
425
425
  this.writeComputedValue(entry, computedResult.value)
426
426
  }
427
427
  },
428
- { immediate: true }
428
+ modelWatchOptions
429
429
  )
430
430
  }
431
431
  if (hasValueFromDataSchema(entry.schema)) {
@@ -443,13 +443,19 @@ export class FormModel {
443
443
  this.writeComputedValue(entry, resolved.value)
444
444
  }
445
445
  },
446
- { immediate: true }
446
+ modelWatchOptions
447
447
  )
448
448
  }
449
449
  })
450
450
  return scope
451
451
  }
452
452
 
453
+ // Returns whether the model computes the value of the component at the
454
+ // component path, i.e. whether its walk of the data visits the component.
455
+ hasComputedValueEntry(componentPath) {
456
+ return this.dataEntries.value.computedValueEntries.has(componentPath)
457
+ }
458
+
453
459
  // Returns whether the entry is still one of the current entries, with the
454
460
  // same schema and data, see `getDataEntries()`.
455
461
  isEntryCurrent({ componentPath, schema, data }) {
@@ -460,14 +466,37 @@ export class FormModel {
460
466
 
461
467
  // Calls `schema.compute()` with the main schema component of the data, like
462
468
  // `processData()`, and with the options resolved by the model, so that the
463
- // result doesn't depend on whether the component is rendered.
464
- getComputedValue(entry) {
469
+ // result doesn't depend on whether the component is rendered. Returns the
470
+ // value as `{ value }`, or `null` if `compute()` reads options that aren't
471
+ // loaded, which aborts it, so that the current value is kept until they are
472
+ // and `compute()` can rely on them. The watcher that calls it depends on the
473
+ // options through reading them, and calls it again once they're loaded.
474
+ getComputedValueResult(entry) {
465
475
  const { schema, data, name, dataPath } = entry
466
- return computeValue(schema, data, name, dataPath, {
467
- component: this.component.mainSchemaComponent ?? this.component,
468
- rootData: this.rootData,
469
- getOptions: schema.options ? () => this.getOptions(entry) : null
470
- })
476
+ try {
477
+ const value = computeValue(schema, data, name, dataPath, {
478
+ component: this.component.mainSchemaComponent ?? this.component,
479
+ rootData: this.rootData,
480
+ getOptions: schema.options ? () => this.getLoadedOptions(entry) : null
481
+ })
482
+ return { value }
483
+ } catch (error) {
484
+ if (error === optionsNotLoaded) {
485
+ return null
486
+ }
487
+ throw error
488
+ }
489
+ }
490
+
491
+ // Returns the options of the component of the entry, or aborts the
492
+ // `compute()` that reads them, while they aren't loaded, see
493
+ // `getComputedValueResult()`.
494
+ getLoadedOptions(entry) {
495
+ const options = this.getOptions(entry)
496
+ if (options === undefined) {
497
+ throw optionsNotLoaded
498
+ }
499
+ return options
471
500
  }
472
501
 
473
502
  // Returns the options of the component of the entry, resolved from
@@ -612,6 +641,19 @@ function takeOverChangedValues(target, before, after) {
612
641
 
613
642
  const notFound = Symbol('notFound')
614
643
 
644
+ // The watchers of the model run after the post-flush hooks, so that they write
645
+ // the values that they derive from the data after the components that display
646
+ // them are mounted. Otherwise, the `mounted` hooks of `v-model` directives
647
+ // would restore the values that inputs had when they were mounted, see
648
+ // https://github.com/vuejs/core/issues/15774. Vue processes the updates that
649
+ // the written values cause in the same flush, before the browser renders them.
650
+ // The watchers run in the order in which they're created.
651
+ const modelWatchOptions = { immediate: true, flush: 'post' }
652
+
653
+ // Aborts `compute()` when it reads options that aren't loaded, see
654
+ // `FormModel.getComputedValueResult()`.
655
+ const optionsNotLoaded = Symbol('optionsNotLoaded')
656
+
615
657
  // Returns whether the schema is a source of computed values, through
616
658
  // `schema.compute()` or a data schema of the `computed` types.
617
659
  function hasComputedValueSource(schema) {
@@ -227,9 +227,9 @@ export function initializeData(schema, data = {}, component, {
227
227
  * Returns the value of the component described by `schema` and `name` in
228
228
  * `data`: the result of `schema.compute()` if it returns a value, else the
229
229
  * value in `data`, or its default if it's missing. Never writes into `data`:
230
- * Computed values, including the defaults of components with `compute()`, are
231
- * written by `FormModel`, and other defaults when the data is set up, see
232
- * `initializeData()`, or when they go missing later, see `FormModel`. If
230
+ * `FormModel` is the only caller and writes the computed values, including the
231
+ * defaults of components with `compute()`. Other defaults are written when the
232
+ * data is set up, see `initializeData()`, or when they go missing later. If
233
233
  * provided, `getOptions()` returns the options for `context.options`, called
234
234
  * only when they're read.
235
235
  */
@@ -287,8 +287,10 @@ export function computeValue(schema, data, name, dataPath, {
287
287
  }
288
288
 
289
289
  // Returns the value in `data`, or its default if it's missing. `context` can be
290
- // a function that creates the context, called only when it's needed.
291
- function getValueOrDefault(schema, data, name, context) {
290
+ // a function that creates the context, called only when it's needed. Computed
291
+ // values are in `data` already, as `FormModel` writes them, see
292
+ // `computeValue()`.
293
+ export function getValueOrDefault(schema, data, name, context) {
292
294
  const shouldUseDefault = (
293
295
  isMissingValue(schema, data, name, context) &&
294
296
  !shouldIgnoreMissingValue(schema, context)
@@ -322,35 +324,33 @@ export function processData(schema, sourceSchema, data, dataPath, {
322
324
  rootData,
323
325
  schemaOnly, // whether to only include data covered by the schema, or all data
324
326
  target,
325
- // Whether to call the schema callbacks `compute()` and `process()`. Data that
326
- // is compared while it's edited, e.g. by `FormModel.isDirty`, isn't validated
327
- // yet, which the callbacks may rely on, and `FormModel` writes the computed
328
- // values into the data. Without them, the values in the data are used, or
329
- // their defaults if they're missing, the types still process them through
330
- // `processValue()`, and the excluded values of components with `process()`
331
- // are kept, as `process()` may store them elsewhere through `processedItem`.
332
- shouldCallComputeAndProcess = true
327
+ // Whether to call the schema callbacks `process()`. Data that is compared
328
+ // while it's edited, e.g. by `FormModel.isDirty`, isn't validated yet, which
329
+ // the callbacks may rely on. Without them, the types still process the values
330
+ // through `processValue()`, and the excluded values of components with
331
+ // `process()` are kept, as `process()` may store them elsewhere through
332
+ // `processedItem`. Computed values are in the data already, as `FormModel`
333
+ // writes them, see `computeValue()`.
334
+ shouldCallProcess = true
333
335
  } = {}) {
334
336
  const options = { component, rootData, schemaOnly, target }
335
337
  const processedData = cloneItem(sourceSchema, data, options)
336
338
  const graph = new SchemaGraph()
337
339
 
338
340
  const before = ({ schema, data, name, dataPath, processedData }) => {
339
- let value = shouldCallComputeAndProcess
340
- ? computeValue(schema, data, name, dataPath, options)
341
- : getValueOrDefault(
341
+ let value = getValueOrDefault(
342
+ schema,
343
+ data,
344
+ name,
345
+ () =>
346
+ DitoContext.createForSchema(component, {
342
347
  schema,
343
- data,
344
348
  name,
345
- () =>
346
- new DitoContext(component, {
347
- schema,
348
- name,
349
- data,
350
- dataPath,
351
- rootData: options.rootData
352
- })
353
- )
349
+ data,
350
+ dataPath,
351
+ rootData: options.rootData
352
+ })
353
+ )
354
354
  // The schema expects the `wrapPrimitives` transformations to be present on
355
355
  // the data that it is applied on, so warp before and unwrap after.
356
356
  if (isArray(value)) {
@@ -400,13 +400,13 @@ export function processData(schema, sourceSchema, data, dataPath, {
400
400
 
401
401
  // Handle the user's `process()` callback next, if one is provided, so that
402
402
  // it can modify data in `processedData` even if it provides `exclude: true`
403
- if (process && shouldCallComputeAndProcess) {
403
+ if (process && shouldCallProcess) {
404
404
  value = process(getContext(context))
405
405
  }
406
406
 
407
407
  // Without calling `process()`, keep the excluded values that it may store
408
- // elsewhere, see `shouldCallComputeAndProcess`:
409
- const shouldKeepExcludedValue = !!process && !shouldCallComputeAndProcess
408
+ // elsewhere, see `shouldCallProcess`:
409
+ const shouldKeepExcludedValue = !!process && !shouldCallProcess
410
410
  if (!shouldKeepExcludedValue && shouldExcludeValue(schema, context)) {
411
411
  delete processedData[name]
412
412
  } else {
package/types/index.d.ts CHANGED
@@ -343,7 +343,11 @@ export interface BaseSchema<$Item>
343
343
  * Computes and sets the field value reactively, also
344
344
  * when the field isn't rendered, unless its `if`
345
345
  * evaluates to `false`. If the callback returns
346
- * `undefined`, the current value is preserved.
346
+ * `undefined`, the current value is preserved. The
347
+ * value is computed into the data, which is what is
348
+ * displayed and saved. As the callback runs whenever
349
+ * the data it reads changes, it should only return a
350
+ * value and not change data.
347
351
  *
348
352
  * As it doesn't depend on the field's component, the
349
353
  * context's component properties, e.g. `component`
@@ -1995,9 +1999,10 @@ export type DitoContext<$Item = any> = {
1995
1999
  option: any
1996
2000
  /**
1997
2001
  * All available options in a select. In `compute()`, they are
1998
- * loaded when first read, also when the field isn't rendered,
1999
- * and are `undefined` while loading; the callback runs again
2000
- * once they are loaded.
2002
+ * loaded when first read, also when the field isn't rendered.
2003
+ * Reading them while they load ends the callback, which keeps
2004
+ * the current value and runs again once they are loaded, so
2005
+ * they are always available in it.
2001
2006
  */
2002
2007
  options: any
2003
2008
  /**