@ditojs/admin 3.2.6 → 3.3.0

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.
Files changed (112) hide show
  1. package/dist/dito-admin.css +1 -1
  2. package/dist/dito-admin.es.js +6012 -5827
  3. package/dist/dito-admin.umd.js +3 -7
  4. package/package.json +18 -17
  5. package/src/DitoAdmin.js +44 -24
  6. package/src/DitoContext.js +28 -2
  7. package/src/DitoTypeComponent.js +1 -0
  8. package/src/appState.js +6 -3
  9. package/src/components/DitoAccount.vue +35 -27
  10. package/src/components/DitoAffixes.vue +9 -2
  11. package/src/components/DitoClipboard.vue +59 -93
  12. package/src/components/DitoConfirmMessage.vue +18 -0
  13. package/src/components/DitoContainer.vue +56 -18
  14. package/src/components/DitoCreateButton.vue +36 -84
  15. package/src/components/DitoDialog.vue +20 -15
  16. package/src/components/DitoDragHandle.vue +44 -0
  17. package/src/components/DitoDraggable.vue +43 -16
  18. package/src/components/DitoEditButtons.vue +48 -24
  19. package/src/components/DitoErrors.vue +9 -56
  20. package/src/components/DitoForm.vue +82 -42
  21. package/src/components/DitoInputAffixes.vue +50 -0
  22. package/src/components/DitoLabel.vue +1 -15
  23. package/src/components/DitoMenu.vue +45 -52
  24. package/src/components/DitoNotifications.vue +38 -14
  25. package/src/components/DitoOptionList.vue +81 -0
  26. package/src/components/DitoOptionsEditButtons.vue +57 -0
  27. package/src/components/DitoPagination.vue +1 -8
  28. package/src/components/DitoPane.vue +51 -39
  29. package/src/components/DitoPanel.vue +12 -26
  30. package/src/components/DitoPanels.vue +11 -5
  31. package/src/components/DitoResizeHandle.vue +47 -0
  32. package/src/components/DitoRoot.vue +113 -320
  33. package/src/components/DitoSchema.vue +72 -63
  34. package/src/components/DitoSchemaInlined.vue +8 -9
  35. package/src/components/DitoScopes.vue +14 -18
  36. package/src/components/DitoSidebar.vue +4 -29
  37. package/src/components/DitoSourceItem.vue +114 -0
  38. package/src/components/DitoTableCell.vue +4 -0
  39. package/src/components/DitoTableHead.vue +15 -17
  40. package/src/components/DitoTabs.vue +25 -0
  41. package/src/components/DitoTrail.vue +25 -25
  42. package/src/components/DitoTreeItem.vue +102 -141
  43. package/src/components/DitoUploadFile.vue +5 -2
  44. package/src/components/DitoView.vue +33 -24
  45. package/src/index.js +4 -1
  46. package/src/mixins/DitoMixin.js +77 -110
  47. package/src/mixins/DomMixin.js +12 -14
  48. package/src/mixins/EmitterMixin.js +3 -2
  49. package/src/mixins/ItemMixin.js +38 -94
  50. package/src/mixins/OptionsMixin.js +38 -25
  51. package/src/mixins/ResourceMixin.js +182 -111
  52. package/src/mixins/RouteMixin.js +61 -44
  53. package/src/mixins/SchemaParentMixin.js +4 -1
  54. package/src/mixins/SourceMixin.js +118 -100
  55. package/src/mixins/TypeMixin.js +42 -40
  56. package/src/mixins/ValidationMixin.js +16 -4
  57. package/src/mixins/ValidatorMixin.js +1 -1
  58. package/src/styles/_settings.scss +4 -0
  59. package/src/styles/_tippy.scss +0 -11
  60. package/src/styles/style.scss +0 -1
  61. package/src/types/DitoTypeButton.vue +11 -14
  62. package/src/types/DitoTypeCheckboxes.vue +35 -17
  63. package/src/types/DitoTypeCode.vue +19 -41
  64. package/src/types/DitoTypeColor.vue +67 -19
  65. package/src/types/DitoTypeDate.vue +11 -14
  66. package/src/types/DitoTypeList.vue +50 -62
  67. package/src/types/DitoTypeMarkup.vue +41 -54
  68. package/src/types/DitoTypeMultiselect.vue +82 -52
  69. package/src/types/DitoTypeNumber.vue +12 -15
  70. package/src/types/DitoTypeObject.vue +4 -38
  71. package/src/types/DitoTypeRadio.vue +19 -6
  72. package/src/types/DitoTypeSection.vue +23 -9
  73. package/src/types/DitoTypeSelect.vue +39 -23
  74. package/src/types/DitoTypeSlider.vue +9 -1
  75. package/src/types/DitoTypeSwitch.vue +1 -1
  76. package/src/types/DitoTypeText.vue +11 -14
  77. package/src/types/DitoTypeTreeList.vue +46 -8
  78. package/src/types/DitoTypeUpload.vue +190 -67
  79. package/src/utils/DataModel.js +368 -146
  80. package/src/utils/FileDropTargets.js +97 -0
  81. package/src/utils/ItemStores.js +31 -0
  82. package/src/utils/ListQuery.js +115 -0
  83. package/src/utils/LoadingTracker.js +76 -0
  84. package/src/utils/SchemaGraph.js +3 -2
  85. package/src/utils/Session.js +151 -0
  86. package/src/utils/ViewRegistry.js +85 -0
  87. package/src/utils/accessor.js +12 -4
  88. package/src/utils/affixes.js +33 -0
  89. package/src/utils/changeOnceEdited.js +33 -0
  90. package/src/utils/clipboard.js +55 -0
  91. package/src/utils/data.js +10 -5
  92. package/src/utils/dialogs.js +59 -0
  93. package/src/utils/dragResize.js +61 -0
  94. package/src/utils/filter.js +36 -2
  95. package/src/utils/html.js +9 -0
  96. package/src/utils/list.js +10 -0
  97. package/src/utils/options.js +4 -0
  98. package/src/utils/registration.js +18 -0
  99. package/src/utils/request.js +108 -13
  100. package/src/utils/route.js +6 -17
  101. package/src/utils/schema/data.js +29 -2
  102. package/src/utils/schema/events.js +19 -0
  103. package/src/utils/schema/item.js +114 -0
  104. package/src/utils/schema/lookup.js +39 -14
  105. package/src/utils/schema/setup.js +5 -5
  106. package/src/utils/schema/validation.js +16 -2
  107. package/src/utils/uid.js +105 -8
  108. package/src/validators/_decimals.js +16 -5
  109. package/types/index.d.ts +168 -41
  110. package/src/mixins/LoadingMixin.js +0 -23
  111. package/src/mixins/PulldownMixin.js +0 -72
  112. package/src/styles/_pulldown.scss +0 -38
@@ -4,15 +4,16 @@ import {
4
4
  isFunction,
5
5
  equals,
6
6
  labelize,
7
- hyphenate,
8
7
  format
9
8
  } from '@ditojs/utils'
10
9
  import appState from '../appState.js'
11
10
  import DitoContext from '../DitoContext.js'
12
11
  import EmitterMixin from './EmitterMixin.js'
13
12
  import { flattenViews } from '../utils/schema/setup.js'
13
+ import { getSchemaEventEntries } from '../utils/schema/events.js'
14
14
  import { getSchemaValue, shouldRenderSchema } from '../utils/schema/data.js'
15
15
  import { getResource, getMemberResource } from '../utils/resource.js'
16
+ import { getRequestCacheKey } from '../utils/request.js'
16
17
  import { appendDataPath, getRelativeDataPath } from '../utils/data.js'
17
18
  import { computed, reactive } from 'vue'
18
19
 
@@ -22,6 +23,7 @@ export default {
22
23
 
23
24
  inject: [
24
25
  'api',
26
+ 'session',
25
27
  '$verbs',
26
28
  '$views',
27
29
  '$isPopulated',
@@ -34,7 +36,8 @@ export default {
34
36
  '$dialogComponent',
35
37
  '$dataModel',
36
38
  '$panelComponent',
37
- '$tabComponent'
39
+ '$tabComponent',
40
+ '$loadingTracker'
38
41
  ],
39
42
 
40
43
  provide() {
@@ -73,7 +76,7 @@ export default {
73
76
  },
74
77
 
75
78
  user() {
76
- return appState.user
79
+ return this.session.user
77
80
  },
78
81
 
79
82
  // $verbs, $verbs and $isPopulated are defined as functions, to preserve
@@ -206,11 +209,6 @@ export default {
206
209
  this.isMounted = true
207
210
  },
208
211
 
209
- beforeCreate() {
210
- const uid = nextUid++
211
- Object.defineProperty(this, '$uid', { get: () => uid })
212
- },
213
-
214
212
  methods: {
215
213
  labelize,
216
214
 
@@ -235,44 +233,11 @@ export default {
235
233
  delete this.store[key]
236
234
  },
237
235
 
238
- getStoreKeyByIndex(index) {
239
- return this.store.$keysByIndex?.[index]
240
- },
241
-
242
- setStoreKeyByIndex(index, key) {
243
- this.store.$keysByIndex ??= {}
244
- this.store.$keysByIndex[index] = key
245
- },
246
-
247
- getChildStore(key, index) {
248
- let store = this.getStore(key)
249
- if (!store && index != null) {
250
- // When storing, temporary ids change to permanent ones and thus the key
251
- // can change. To still find the store, we reference by index as well,
252
- // to be able to find the store again after the item was saved.
253
- const oldKey = this.getStoreKeyByIndex(index)
254
- store = this.getStore(oldKey)
255
- if (store) {
256
- this.setStore(key, store)
257
- this.removeStore(oldKey)
258
- }
259
- }
260
- if (!store) {
261
- store = this.setStore(key, reactive({}))
262
- }
263
- if (index != null) {
264
- // temporary uid keys will change between persistence, so we need to
265
- // assign the key to the index even when the store already existed.
266
- this.setStoreKeyByIndex(index, key)
267
- }
268
- return store
269
- },
270
-
271
- removeChildStore(key, index) {
272
- // GEt the child-store first, so that indices can be transferred over
273
- // temporary id changes during persistence.
274
- this.getChildStore(key, index)
275
- this.removeStore(key)
236
+ // Returns the store of the child component named `name`, e.g. of the
237
+ // components of a pane. The stores of list items are kept separately, see
238
+ // `ItemStores`.
239
+ getChildStore(name) {
240
+ return this.getStore(name) ?? this.setStore(name, reactive({}))
276
241
  },
277
242
 
278
243
  getSchemaValue(
@@ -301,21 +266,6 @@ export default {
301
266
  : labelize(name) || ''
302
267
  },
303
268
 
304
- getButtonAttributes(verb, subject = null, text = null) {
305
- // Buttons that display text are named by it, others by what they act on
306
- // if known, e.g. 'Add Section'.
307
- const label = text
308
- ? null
309
- : `${labelize(verb)}${subject ? ` ${subject}` : ''}`
310
- return {
311
- class: `dito-button--${verb}`,
312
- ...(label && {
313
- 'title': label,
314
- 'aria-label': label
315
- })
316
- }
317
- },
318
-
319
269
  // TODO: Rename *Link() to *Route().
320
270
  getQueryLink(query) {
321
271
  return {
@@ -375,7 +325,7 @@ export default {
375
325
  method ||= resource?.method
376
326
  const checkUser = !internal && this.api.isApiUrl(url)
377
327
  if (checkUser) {
378
- await this.rootComponent.ensureUser()
328
+ await this.session.ensureUser()
379
329
  }
380
330
  const response = await this.api.request({
381
331
  method,
@@ -390,7 +340,7 @@ export default {
390
340
  method === 'patch' &&
391
341
  equals(resource, getMemberResource(this.user.id, this.api.users))
392
342
  ) {
393
- await this.rootComponent.fetchUser()
343
+ await this.session.fetchUser()
394
344
  }
395
345
  return response
396
346
  },
@@ -407,28 +357,24 @@ export default {
407
357
  request({ cache, ...options }) {
408
358
  // Allow caching of loaded data on two levels:
409
359
  // - 'global': cache globally, for the entire admin session
410
- // - 'local': cache locally within the closest route component that is
411
- // associated with a resource and loads its own data.
412
- const cacheParent = (
413
- cache &&
414
- {
415
- global: this.appState,
416
- local: this.dataComponent
417
- }[cache]
418
- )
419
- const loadCache = cacheParent?.loadCache
420
- // Build a cache key from the config:
360
+ // - 'local': cache locally within the closest route component, i.e. the
361
+ // form or view, for as long as it is shown. Components outside of
362
+ // route components, e.g. in dialogs, don't cache locally.
363
+ const loadCache =
364
+ cache === 'global'
365
+ ? this.appState.loadCache
366
+ : cache === 'local'
367
+ ? this.routeComponent?.loadCache
368
+ : null
369
+ // Requests to resources are told apart by the URLs that they resolve to:
370
+ const { resource } = options
421
371
  const cacheKey = (
422
372
  loadCache &&
423
- `${
424
- options.method || 'get'
425
- } ${
426
- options.url
427
- } ${
428
- JSON.stringify(options.query || '')
429
- } ${
430
- JSON.stringify(options.data || '')
431
- }`
373
+ getRequestCacheKey({
374
+ ...options,
375
+ method: options.method || resource?.method,
376
+ url: options.url || (resource ? this.getResourceUrl(resource) : null)
377
+ })
432
378
  )
433
379
  if (loadCache && (cacheKey in loadCache)) {
434
380
  return loadCache[cacheKey]
@@ -438,7 +384,12 @@ export default {
438
384
  const res = this.sendRequest(options)
439
385
  .then(response => response.data)
440
386
  .catch(error => {
441
- // Convert axios errors to normal errors
387
+ // Failed requests aren't cached, so they can be retried:
388
+ if (loadCache?.[cacheKey] === res) {
389
+ delete loadCache[cacheKey]
390
+ }
391
+ // Convert errors of responses with data, e.g. Dito.js errors, to
392
+ // errors with the message and properties of that data:
442
393
  const data = error.response?.data
443
394
  throw data
444
395
  ? Object.assign(new Error(data.message), data)
@@ -514,6 +465,7 @@ export default {
514
465
  setupMethods() {
515
466
  for (const [key, value] of Object.entries(this.schema.methods || {})) {
516
467
  if (isFunction(value)) {
468
+ warnAboutOverriddenMember(this, key, 'method')
517
469
  this[key] = value
518
470
  } else {
519
471
  console.error(`Invalid method definition: ${key}: ${value}`)
@@ -537,6 +489,7 @@ export default {
537
489
  ? getComputedAccessor(item)
538
490
  : null
539
491
  if (accessor) {
492
+ warnAboutOverriddenMember(this, key, 'computed property')
540
493
  Object.defineProperty(this, key, accessor)
541
494
  } else {
542
495
  console.error(
@@ -547,7 +500,7 @@ export default {
547
500
  },
548
501
 
549
502
  setupEvents() {
550
- const { watch, events } = this.schema
503
+ const { watch } = this.schema
551
504
  if (watch) {
552
505
  const handlers = isFunction(watch) ? watch.call(this) : watch
553
506
  if (isObject(handlers)) {
@@ -565,33 +518,30 @@ export default {
565
518
  }
566
519
  }
567
520
 
568
- const addEvent = (key, event, callback) => {
521
+ const eventEntries = getSchemaEventEntries(this.schema)
522
+ for (const { key, event, callback } of eventEntries) {
569
523
  if (isFunction(callback)) {
570
- this.on(hyphenate(event), callback)
524
+ this.on(event, callback)
571
525
  } else {
572
526
  console.error(`Invalid event definition: ${key}: ${callback}`)
573
527
  }
574
528
  }
575
-
576
- if (events) {
577
- for (const [key, value] of Object.entries(events)) {
578
- addEvent(key, key, value)
579
- }
580
- }
581
- // Also scan schema for `on[A-Z]`-style callbacks and add them
582
- // TODO: Deprecate one format or the other, in favour of only one way of
583
- // doing things. Decide which one to remove.
584
- for (const [key, value] of Object.entries(this.schema)) {
585
- if (/^on[A-Z]/.test(key)) {
586
- addEvent(key, key.slice(2), value)
587
- }
588
- }
589
529
  },
590
530
 
591
531
  emitEvent(event, {
592
532
  context = null,
593
- parent = null
533
+ parent = null,
534
+ ...unsupportedOptions
594
535
  } = {}) {
536
+ const unsupportedKeys = Object.keys(unsupportedOptions)
537
+ if (unsupportedKeys.length > 0) {
538
+ // Values for the handlers are passed through `context`, e.g.
539
+ // `emitEvent('error', { context: { error } })`.
540
+ console.warn(
541
+ `Unsupported options for event '${event}': ` +
542
+ `${unsupportedKeys.join(', ')}. Pass them through \`context\`.`
543
+ )
544
+ }
595
545
  const hasListeners = this.hasListeners(event)
596
546
  const parentHasListeners = parent?.hasListeners(event)
597
547
  if (hasListeners || parentHasListeners) {
@@ -622,17 +572,15 @@ export default {
622
572
 
623
573
  // Waits until the data model settled: its loads finished, and it wrote the
624
574
  // 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.
575
+ // spinner in the header while loads are pending, like requests do, but
576
+ // without disabling the view, as the user may still be editing.
626
577
  async waitUntilDataModelSettled() {
627
578
  await this.$nextTick()
628
579
  const { dataModel } = this
629
580
  if (dataModel?.hasPendingLoads) {
630
- this.rootComponent.registerLoading(true)
631
- try {
632
- await dataModel.waitUntilSettled()
633
- } finally {
634
- this.rootComponent.registerLoading(false)
635
- }
581
+ await this.$loadingTracker().root.track(() =>
582
+ dataModel.waitUntilSettled()
583
+ )
636
584
  }
637
585
  },
638
586
 
@@ -642,7 +590,26 @@ export default {
642
590
  }
643
591
  }
644
592
 
645
- let nextUid = 0
593
+ // The keys of the members that the schemas define on components, through
594
+ // `methods` and `computed`, to tell them apart from the component's own ones
595
+ // when the schema is set up again.
596
+ const schemaMemberKeysByComponent = new WeakMap()
597
+
598
+ // Warns when the schema defines a method or computed property that overrides
599
+ // a member of the component, e.g. a method or property of Dito.js itself.
600
+ function warnAboutOverriddenMember(component, key, kind) {
601
+ let schemaMemberKeys = schemaMemberKeysByComponent.get(component)
602
+ if (!schemaMemberKeys) {
603
+ schemaMemberKeys = new Set()
604
+ schemaMemberKeysByComponent.set(component, schemaMemberKeys)
605
+ }
606
+ if (key in component && !schemaMemberKeys.has(key)) {
607
+ console.warn(
608
+ `The schema ${kind} '${key}' overrides a member of the component.`
609
+ )
610
+ }
611
+ schemaMemberKeys.add(key)
612
+ }
646
613
 
647
614
  function getParentComponent(component, key) {
648
615
  const current = component[key]
@@ -3,27 +3,25 @@ import { addEvents } from '@ditojs/ui/src'
3
3
 
4
4
  // @vue/component
5
5
  export default {
6
- data() {
7
- return {
8
- domHandlers: []
9
- }
6
+ created() {
7
+ // Aborted when the component is unmounted, which removes all handlers that
8
+ // are still added then. Not reactive, and nothing is kept per handler.
9
+ this.domAbortController = new AbortController()
10
10
  },
11
11
 
12
12
  unmounted() {
13
- for (const { remove } of this.domHandlers) {
14
- remove()
15
- }
16
- this.domHandlers = []
13
+ this.domAbortController.abort()
17
14
  },
18
15
 
19
16
  methods: {
17
+ // Adds the event handlers to `element`, and returns an object with a
18
+ // `remove()` method that removes them again. Handlers that are still added
19
+ // when the component is unmounted are removed then.
20
20
  domOn(element, type, handler) {
21
- const result = addEvents(
22
- element,
23
- isObject(type) ? type : { [type]: handler }
24
- )
25
- this.domHandlers.push(result)
26
- return result
21
+ const events = isObject(type) ? type : { [type]: handler }
22
+ return addEvents(element, events, {
23
+ signal: this.domAbortController.signal
24
+ })
27
25
  }
28
26
  }
29
27
  }
@@ -46,8 +46,9 @@ export default {
46
46
 
47
47
  off(event, callback) {
48
48
  if (!arguments.length) {
49
- // Remove all events
50
- delete this.listeners
49
+ // Remove all events. Reset rather than `delete` the data property,
50
+ // which Vue's component proxy doesn't support.
51
+ this.listeners = null
51
52
  } else if (isArray(event)) {
52
53
  for (const ev of event) {
53
54
  this.off(ev, callback)
@@ -1,10 +1,15 @@
1
1
  import DitoContext from '../DitoContext.js'
2
- import { isListSource } from '../utils/schema/structure.js'
3
2
  import { getItemFormSchema } from '../utils/schema/lookup.js'
4
- import { getItemId, getItemUid } from '../utils/schema/data.js'
3
+ import { getItemUid } from '../utils/schema/data.js'
4
+ import {
5
+ getItemIdOrIndex,
6
+ findItemIndexById,
7
+ getItemLabel
8
+ } from '../utils/schema/item.js'
5
9
  import { appendDataPath } from '../utils/data.js'
6
- import { isObject, isString, isNumber, isFunction } from '@ditojs/utils'
7
10
 
11
+ // Delegates to the item functions in `utils/schema/item.js`, with the data
12
+ // path, context and `isTransient` state of the component that mixes it in.
8
13
  // @vue/component
9
14
  export default {
10
15
  methods: {
@@ -13,9 +18,10 @@ export default {
13
18
  getItemUid,
14
19
 
15
20
  getItemId(sourceSchema, item, index = null) {
16
- return this.isTransient && index !== null
17
- ? String(index)
18
- : getItemId(sourceSchema, item)
21
+ return getItemIdOrIndex(sourceSchema, item, {
22
+ index,
23
+ isTransient: this.isTransient
24
+ })
19
25
  },
20
26
 
21
27
  getItemDataPath(sourceSchema, index) {
@@ -29,36 +35,18 @@ export default {
29
35
  return dataPath
30
36
  },
31
37
 
32
- getItemStore(sourceSchema, item, index) {
33
- return this.getChildStore(this.getItemUid(sourceSchema, item), index)
34
- },
35
-
36
- removeItemStore(sourceSchema, item, index) {
37
- this.removeChildStore(this.getItemUid(sourceSchema, item), index)
38
- },
39
-
40
38
  findItemIdIndex(sourceSchema, data, itemId) {
41
- const index = this.isTransient
42
- ? // For transient data, the index is used as the id
43
- itemId
44
- : data?.findIndex(
45
- (item, index) => (
46
- this.getItemId(sourceSchema, item, index) === itemId
47
- )
48
- )
49
- return index !== -1 ? index : null
39
+ return findItemIndexById(sourceSchema, data, itemId, {
40
+ isTransient: this.isTransient
41
+ })
50
42
  },
51
43
 
44
+ // Returns the label of `item` as HTML, see `getItemLabel()`.
52
45
  getItemLabel(sourceSchema, item, {
53
46
  index = null,
54
47
  extended = false,
55
48
  asObject = false
56
49
  } = {}) {
57
- const { itemLabel } = sourceSchema
58
- if (!item || !extended && itemLabel === false) {
59
- return null
60
- }
61
-
62
50
  let dataPath
63
51
  const getDataPath = () =>
64
52
  (dataPath ??= this.getItemDataPath(sourceSchema, index))
@@ -69,74 +57,30 @@ export default {
69
57
  getItemFormSchema(sourceSchema, item, this.context)
70
58
  ))
71
59
 
72
- let text
73
- let prefix
74
- let suffix
75
- if (isFunction(itemLabel)) {
76
- const label = itemLabel.call(
77
- this,
78
- new DitoContext(this, {
79
- nested: false,
80
- data: item,
81
- value: item,
82
- index,
60
+ return getItemLabel(sourceSchema, item, {
61
+ index,
62
+ extended,
63
+ asObject,
64
+ getFormLabel,
65
+ evaluateItemLabel: () =>
66
+ sourceSchema.itemLabel.call(
67
+ this,
68
+ new DitoContext(this, {
69
+ nested: false,
70
+ data: item,
71
+ value: item,
72
+ index,
83
73
 
84
- get dataPath() {
85
- return getDataPath()
86
- },
74
+ get dataPath() {
75
+ return getDataPath()
76
+ },
87
77
 
88
- get formLabel() {
89
- return getFormLabel()
90
- }
91
- })
92
- )
93
- if (isObject(label)) {
94
- ;({ text, prefix, suffix } = label)
95
- } else {
96
- text = label
97
- }
98
- // It's up to `itemLabel()` entirely to produce the label:
99
- extended = false
100
- } else if (isString(itemLabel) && !(itemLabel in item)) {
101
- // `itemLabel` can be both a key, or simply a label.
102
- text = itemLabel
103
- } else {
104
- // Look up the name on the item, by these rules:
105
- // 1. If `itemLabel` is a string, use it as the property key
106
- // 2. Otherwise, if there are columns, use the value of the first
107
- // 3. Otherwise, see if the item has a property named 'name'
108
- const { columns } = sourceSchema
109
- const key = (
110
- isString(itemLabel) && itemLabel ||
111
- isListSource(sourceSchema) && columns && Object.keys(columns)[0] ||
112
- 'name'
113
- )
114
- const value = item[key]
115
- // Only primitives display as a label. If the property holds an array
116
- // or object, fall through to the auto-generated label.
117
- text = isString(value) || isNumber(value) ? value : null
118
- }
119
- const hadLabel = !!text
120
- // If no label was found so far, try to produce one from the index.
121
- if (text == null) {
122
- // Always use extended style when auto-generating labels from index/id:
123
- extended = true
124
- text =
125
- isListSource(sourceSchema) && index !== null ? `${index + 1}` : ''
126
- }
127
- if (extended) {
128
- const formLabel = getFormLabel()
129
- if (formLabel) {
130
- // If a label was provided, put in quotes when prefixed with the
131
- // form label for the extended style:
132
- text = `${formLabel} ${hadLabel ? `'${text}'` : text}`
133
- }
134
- }
135
- return asObject
136
- ? text || prefix || suffix
137
- ? { text, prefix, suffix }
138
- : null
139
- : text
78
+ get formLabel() {
79
+ return getFormLabel()
80
+ }
81
+ })
82
+ )
83
+ })
140
84
  }
141
85
  }
142
86
  }
@@ -1,6 +1,6 @@
1
1
  import { toRaw } from 'vue'
2
2
  import DitoContext from '../DitoContext.js'
3
- import { hasViewSchema, getViewEditPath } from '../utils/schema/lookup.js'
3
+ import { hasViewSchema } from '../utils/schema/lookup.js'
4
4
  import { getMultipleValue } from '../utils/schema/data.js'
5
5
  import { getSchemaAccessor } from '../utils/accessor.js'
6
6
  import { setTemporaryId, isReference } from '../utils/data.js'
@@ -207,12 +207,6 @@ export default {
207
207
  }
208
208
  }),
209
209
 
210
- editPath() {
211
- return this.editable && this.selectedValue
212
- ? getViewEditPath(this.schema, this.selectedValue, this.context)
213
- : null
214
- },
215
-
216
210
  groupByLabel() {
217
211
  return this.groupBy ? 'label' : null
218
212
  },
@@ -241,16 +235,25 @@ export default {
241
235
  }
242
236
  },
243
237
 
244
- 'optionsResolver.lastLoadError'(error) {
238
+ // The error of the last load replaces the error of the previous one, e.g.
239
+ // when the options load again after the data is replaced.
240
+ 'optionsResolver.lastLoadError'(error, previousError) {
241
+ if (previousError) {
242
+ this.removeError(getLoadErrorMessage(previousError))
243
+ }
245
244
  if (error) {
246
- this.addError(error.message || error)
245
+ this.addError(getLoadErrorMessage(error))
247
246
  }
248
247
  }
249
248
  },
250
249
 
251
250
  methods: {
252
251
  getOptionKey(key) {
253
- const [option] = this.activeOptions
252
+ const [firstEntry] = this.activeOptions
253
+ // Grouped options are looked up in their first group.
254
+ const option = this.groupBy
255
+ ? firstEntry?.[this.groupByOptions]?.[0]
256
+ : firstEntry
254
257
  return isObject(option) && key in option ? key : null
255
258
  },
256
259
 
@@ -297,13 +300,19 @@ export default {
297
300
  return this.relate && isObject(option) && option.id == null
298
301
  },
299
302
 
303
+ // Returns the option of the value, or the value itself without
304
+ // `optionValue`, as plain value options are their own values.
300
305
  getOptionForValue(value) {
301
- const findOption = (options, value, groupBy) => {
302
- // Search for the option object with the given value and return the
303
- // whole object.
306
+ return this.optionValue ? this.findOptionForValue(value) : value
307
+ },
308
+
309
+ // Searches the options for the one with the given value, matched through
310
+ // `optionEquals` if provided, and returns it.
311
+ findOptionForValue(value) {
312
+ const findOption = (options, groupBy) => {
304
313
  for (const option of options) {
305
314
  if (groupBy) {
306
- const found = findOption(option.options, value, null)
315
+ const found = findOption(option.options, null)
307
316
  if (found) {
308
317
  return found
309
318
  }
@@ -318,17 +327,17 @@ export default {
318
327
  }
319
328
  }
320
329
 
321
- const { optionValue, groupBy } = this
322
- return optionValue
323
- ? (findOption(this.activeOptions, value, groupBy) ??
324
- // Options filtered out of the active options, e.g. by a search, are
325
- // still options of the value.
326
- (
327
- this.areOptionsFiltered
328
- ? findOption(this.options, value, groupBy)
329
- : undefined
330
- ))
331
- : value
330
+ const { groupBy } = this
331
+ return (
332
+ findOption(this.activeOptions, groupBy) ??
333
+ // Options filtered out of the active options, e.g. by a search, are
334
+ // still options of the value.
335
+ (
336
+ this.areOptionsFiltered
337
+ ? findOption(this.options, groupBy)
338
+ : undefined
339
+ )
340
+ )
332
341
  },
333
342
 
334
343
  // Returns the related object in the value with the given value of its
@@ -388,3 +397,7 @@ export default {
388
397
  return value
389
398
  }
390
399
  }
400
+
401
+ function getLoadErrorMessage(error) {
402
+ return error.message || error
403
+ }