@ditojs/admin 2.100.0 → 3.1.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 (81) hide show
  1. package/dist/dito-admin.css +1 -1
  2. package/dist/dito-admin.es.js +3555 -2719
  3. package/dist/dito-admin.umd.js +5 -6
  4. package/package.json +84 -46
  5. package/src/DitoAdmin.js +1 -65
  6. package/src/DitoComponent.js +1 -1
  7. package/src/DitoContext.js +51 -0
  8. package/src/DitoTypeComponent.js +23 -1
  9. package/src/components/DitoAffixes.vue +6 -3
  10. package/src/components/DitoButtons.vue +14 -1
  11. package/src/components/DitoContainer.vue +19 -10
  12. package/src/components/DitoCreateButton.vue +11 -2
  13. package/src/components/DitoDialog.vue +21 -2
  14. package/src/components/DitoForm.vue +152 -84
  15. package/src/components/DitoLabel.vue +3 -2
  16. package/src/components/DitoPane.vue +55 -11
  17. package/src/components/DitoPanel.vue +34 -5
  18. package/src/components/DitoPanels.vue +2 -1
  19. package/src/components/DitoRoot.vue +6 -6
  20. package/src/components/DitoRouterView.vue +27 -0
  21. package/src/components/DitoSchema.vue +278 -145
  22. package/src/components/DitoSchemaInlined.vue +1 -1
  23. package/src/components/DitoTabs.vue +16 -18
  24. package/src/components/DitoTrail.vue +5 -4
  25. package/src/components/DitoTreeItem.vue +17 -6
  26. package/src/components/DitoView.vue +21 -3
  27. package/src/components/index.js +1 -0
  28. package/src/mixins/DitoMixin.js +53 -7
  29. package/src/mixins/ItemMixin.js +3 -6
  30. package/src/mixins/NumberMixin.js +47 -31
  31. package/src/mixins/OptionsMixin.js +132 -71
  32. package/src/mixins/ResourceMixin.js +5 -2
  33. package/src/mixins/RouteMixin.js +39 -1
  34. package/src/mixins/SortableMixin.js +0 -13
  35. package/src/mixins/SourceMixin.js +155 -96
  36. package/src/mixins/TextMixin.js +1 -1
  37. package/src/mixins/TypeMixin.js +37 -22
  38. package/src/mixins/ValidationMixin.js +9 -27
  39. package/src/mixins/ValidatorMixin.js +5 -7
  40. package/src/mixins/ValueMixin.js +5 -2
  41. package/src/types/DitoTypeButton.vue +1 -1
  42. package/src/types/DitoTypeCheckbox.vue +1 -1
  43. package/src/types/DitoTypeCheckboxes.vue +1 -1
  44. package/src/types/DitoTypeCode.vue +8 -1
  45. package/src/types/DitoTypeColor.vue +20 -34
  46. package/src/types/DitoTypeComponent.vue +1 -1
  47. package/src/types/DitoTypeComputed.vue +4 -27
  48. package/src/types/DitoTypeDate.vue +16 -5
  49. package/src/types/DitoTypeLabel.vue +1 -4
  50. package/src/types/DitoTypeList.vue +9 -5
  51. package/src/types/DitoTypeMarkup.vue +25 -13
  52. package/src/types/DitoTypeMultiselect.vue +37 -6
  53. package/src/types/DitoTypeNumber.vue +1 -1
  54. package/src/types/DitoTypeObject.vue +10 -7
  55. package/src/types/DitoTypePanel.vue +20 -6
  56. package/src/types/DitoTypeProgress.vue +1 -1
  57. package/src/types/DitoTypeRadio.vue +1 -1
  58. package/src/types/DitoTypeSection.vue +32 -4
  59. package/src/types/DitoTypeSelect.vue +1 -1
  60. package/src/types/DitoTypeSlider.vue +1 -1
  61. package/src/types/DitoTypeSwitch.vue +1 -1
  62. package/src/types/DitoTypeText.vue +13 -15
  63. package/src/types/DitoTypeTextarea.vue +1 -1
  64. package/src/types/DitoTypeTreeList.vue +5 -7
  65. package/src/types/DitoTypeUpload.vue +1 -2
  66. package/src/utils/DataSchemaResolver.js +154 -0
  67. package/src/utils/FormModel.js +615 -0
  68. package/src/utils/data.js +20 -0
  69. package/src/utils/filter.js +3 -2
  70. package/src/utils/options.js +3 -0
  71. package/src/utils/request.js +70 -0
  72. package/src/utils/schema/data.js +659 -0
  73. package/src/utils/schema/lookup.js +250 -0
  74. package/src/utils/schema/setup.js +354 -0
  75. package/src/utils/schema/structure.js +172 -0
  76. package/src/utils/schema/types.js +34 -0
  77. package/src/utils/schema/validation.js +142 -0
  78. package/src/utils/units.js +70 -3
  79. package/types/index.d.ts +174 -59
  80. package/src/mixins/DataMixin.js +0 -131
  81. package/src/utils/schema.js +0 -1121
@@ -24,14 +24,15 @@ import DitoComponent from '../DitoComponent.js'
24
24
  export default DitoComponent.component('DitoTrail', {
25
25
  computed: {
26
26
  trail() {
27
+ // Leave out the components that are being left, until they're unmounted.
27
28
  return this.appState.routeComponents.filter(
28
- component => !!component.routeRecord
29
+ component => !component.isLeavingRoute
29
30
  )
30
31
  }
31
32
  },
32
33
 
33
34
  methods: {
34
- getComponentPath(component) {
35
+ getComponentRoutePath(component) {
35
36
  // Do the same as in `DitoMenu`: Link menu items to the first children.
36
37
  const { schema } = component
37
38
  return schema.type === 'menu'
@@ -40,12 +41,12 @@ export default DitoComponent.component('DitoTrail', {
40
41
  },
41
42
 
42
43
  getComponentHref(component) {
43
- return this.$router.resolve(this.getComponentPath(component)).href
44
+ return this.$router.resolve(this.getComponentRoutePath(component)).href
44
45
  },
45
46
 
46
47
  onClickComponent(component) {
47
48
  this.$router.push({
48
- path: this.getComponentPath(component),
49
+ path: this.getComponentRoutePath(component),
49
50
  force: true
50
51
  })
51
52
  }
@@ -1,6 +1,6 @@
1
1
  <template lang="pug">
2
2
  .dito-tree-item(
3
- :id="dataPath"
3
+ :id="componentPath"
4
4
  :class=`{
5
5
  'dito-tree-item--dragging': isDragging,
6
6
  'dito-tree-item--active': active
@@ -66,7 +66,6 @@
66
66
  td
67
67
  DitoLabel(
68
68
  v-if="property.label !== false"
69
- :dataPath="getPropertyDataPath(property)"
70
69
  :label="getLabel(property)"
71
70
  )
72
71
  DitoTableCell(
@@ -83,7 +82,7 @@
83
82
  v-show="opened"
84
83
  :options="getDraggableOptions(true)"
85
84
  :draggable="childrenDraggable"
86
- :modelValue="updateOrder(childrenSchema, childrenList)"
85
+ :modelValue="childrenList"
87
86
  @update:modelValue="value => (childrenList = value)"
88
87
  )
89
88
  DitoTreeItem(
@@ -107,9 +106,10 @@
107
106
  import DitoComponent from '../DitoComponent.js'
108
107
  import ItemMixin from '../mixins/ItemMixin'
109
108
  import SortableMixin from '../mixins/SortableMixin.js'
110
- import { appendDataPath } from '../utils/data.js'
109
+ import { appendDataPath, getRelativeDataPath } from '../utils/data.js'
111
110
  import { getSchemaAccessor } from '../utils/accessor.js'
112
- import { getNamedSchemas, hasFormSchema } from '../utils/schema.js'
111
+ import { getNamedSchemas, hasFormSchema } from '../utils/schema/lookup.js'
112
+ import { updateOrder } from '../utils/schema/data.js'
113
113
  import { stripHtml } from '@ditojs/utils'
114
114
 
115
115
  // @vue/component
@@ -137,6 +137,17 @@ export default DitoComponent.component('DitoTreeItem', {
137
137
  },
138
138
 
139
139
  computed: {
140
+ componentPath() {
141
+ // Continue the path of the tree list with the item indices and children
142
+ // names of the data path. The root item's data path doesn't contain the
143
+ // list's name, see `DitoTypeTreeList.treeDataPath`.
144
+ const { componentPath, dataPath } = this.container
145
+ const relativeDataPath = getRelativeDataPath(this.dataPath, dataPath)
146
+ return relativeDataPath
147
+ ? appendDataPath(componentPath, relativeDataPath)
148
+ : componentPath
149
+ },
150
+
140
151
  meta() {
141
152
  return this.container.meta
142
153
  },
@@ -174,7 +185,7 @@ export default DitoComponent.component('DitoTreeItem', {
174
185
  set(value) {
175
186
  const name = this.childrenSchema?.name
176
187
  if (name) {
177
- this.updateOrder(this.childrenSchema, value)
188
+ updateOrder(this.childrenSchema, value)
178
189
  // eslint-disable-next-line vue/no-mutating-props
179
190
  this.data[name] = value
180
191
  this.$emit('update:data', value)
@@ -6,9 +6,9 @@ template(
6
6
  //- instead, to nest further route components.
7
7
  //- NOTE: This is different from the handling in DitoForm, where `v-show` is
8
8
  //- used to always render forms even when other nested forms are present.
9
- RouterView(
9
+ DitoRouterView(
10
10
  v-if="!isLastRoute"
11
- :key="name"
11
+ :routeLevel="routeLevel + 1"
12
12
  )
13
13
  .dito-view.dito-scroll-parent(
14
14
  v-else
@@ -33,8 +33,9 @@ import RouteMixin from '../mixins/RouteMixin.js'
33
33
  import {
34
34
  isSingleComponentView,
35
35
  someNestedSchemaComponent
36
- } from '../utils/schema.js'
36
+ } from '../utils/schema/structure.js'
37
37
  import { hasResource } from '../utils/resource.js'
38
+ import { FormModel } from '../utils/FormModel.js'
38
39
 
39
40
  // @vue/component
40
41
  export default DitoComponent.component('DitoView', {
@@ -116,6 +117,16 @@ export default DitoComponent.component('DitoView', {
116
117
  }
117
118
  },
118
119
 
120
+ created() {
121
+ // Writes defaults and computed values into the view's data and resolves
122
+ // options, see `FormModel`:
123
+ this.formModel = new FormModel({
124
+ component: this,
125
+ getSchema: () => this.viewSchema,
126
+ getData: () => this.data
127
+ })
128
+ },
129
+
119
130
  mounted() {
120
131
  // Prevent bypassing of if-condition by direct URL access.
121
132
  if (!this.shouldRenderSchema(this.viewSchema)) {
@@ -123,9 +134,16 @@ export default DitoComponent.component('DitoView', {
123
134
  }
124
135
  },
125
136
 
137
+ beforeUnmount() {
138
+ // Stop the model before the component's own watchers are stopped, as the
139
+ // model's watchers aren't part of the component, see `FormModel`.
140
+ this.formModel.stop()
141
+ },
142
+
126
143
  methods: {
127
144
  setData(data) {
128
145
  this.data = data
146
+ return this.data
129
147
  },
130
148
 
131
149
  getChildPath(path) {
@@ -24,6 +24,7 @@ export { default as DitoButtons } from './DitoButtons.vue'
24
24
  export { default as DitoEditButtons } from './DitoEditButtons.vue'
25
25
  export { default as DitoCreateButton } from './DitoCreateButton.vue'
26
26
  export { default as DitoClipboard } from './DitoClipboard.vue'
27
+ export { default as DitoRouterView } from './DitoRouterView.vue'
27
28
  export { default as DitoView } from './DitoView.vue'
28
29
  export { default as DitoForm } from './DitoForm.vue'
29
30
  export { default as DitoFormInner } from './DitoFormInner.vue'
@@ -10,12 +10,10 @@ import {
10
10
  import appState from '../appState.js'
11
11
  import DitoContext from '../DitoContext.js'
12
12
  import EmitterMixin from './EmitterMixin.js'
13
- import {
14
- flattenViews,
15
- getSchemaValue,
16
- shouldRenderSchema
17
- } from '../utils/schema.js'
13
+ import { flattenViews } from '../utils/schema/setup.js'
14
+ import { getSchemaValue, shouldRenderSchema } from '../utils/schema/data.js'
18
15
  import { getResource, getMemberResource } from '../utils/resource.js'
16
+ import { appendDataPath, getRelativeDataPath } from '../utils/data.js'
19
17
  import { computed, reactive } from 'vue'
20
18
 
21
19
  // @vue/component
@@ -103,6 +101,27 @@ export default {
103
101
  return this.$root.$refs.root
104
102
  },
105
103
 
104
+ // The path of component names through views, tabs, panels and forms, with
105
+ // item indices from the data path, e.g. `main/chapters/0/title`. Unlike
106
+ // `dataPath`, it tells apart components that display the same data, and
107
+ // is used for registries and DOM ids. Components continue the path of
108
+ // their parent with the item indices that their data path adds to the
109
+ // parent's, e.g. inlined list items. `DitoContainer`, `DitoPane` and
110
+ // `DitoPanel` add the names of components, tabs and panels, `DitoButtons`
111
+ // the names of button groups, `DitoForm` and `DitoTreeItem` continue the
112
+ // path of the source of their item.
113
+ componentPath() {
114
+ return this.continueParentComponentPath()
115
+ },
116
+
117
+ // The data path that `componentPath` corresponds to, which children
118
+ // compare their data path with: the component's own, or for components
119
+ // without data path, e.g. `DitoDraggable` between lists and their items,
120
+ // the one of their parent.
121
+ dataPathForComponentPath() {
122
+ return this.dataPath ?? this.parentComponent?.dataPathForComponentPath
123
+ },
124
+
106
125
  // Use computed properties as links to injects, so DitoSchema can
107
126
  // override the property and return `this` instead of the parent.
108
127
  parentComponent() {
@@ -276,8 +295,9 @@ export default {
276
295
  : labelize(name) || ''
277
296
  },
278
297
 
279
- getButtonAttributes(verb) {
280
- const label = labelize(verb)
298
+ getButtonAttributes(verb, subject = null) {
299
+ // Name buttons by what they act on if known, e.g. 'Add Section'.
300
+ const label = subject ? `${labelize(verb)} ${subject}` : labelize(verb)
281
301
  return {
282
302
  'class': `dito-button--${verb}`,
283
303
  'title': label,
@@ -451,6 +471,32 @@ export default {
451
471
  body.removeChild(a)
452
472
  },
453
473
 
474
+ // Returns the component path of the parent, continued with the item
475
+ // indices that the data path of this component adds to the parent's, see
476
+ // `componentPath`.
477
+ continueParentComponentPath() {
478
+ const { parentComponent } = this
479
+ const parentComponentPath = parentComponent?.componentPath ?? ''
480
+ const relativeDataPath = getRelativeDataPath(
481
+ this.dataPath,
482
+ parentComponent?.dataPathForComponentPath
483
+ )
484
+ return relativeDataPath
485
+ ? appendDataPath(parentComponentPath, relativeDataPath)
486
+ : parentComponentPath
487
+ },
488
+
489
+ // Waits for the pending loads of the form model, e.g. before submitting,
490
+ // showing the spinner in the header in the meantime, like requests do.
491
+ async waitForPendingLoadsOf(formModel) {
492
+ this.rootComponent.registerLoading(true)
493
+ try {
494
+ await formModel.waitForPendingLoads()
495
+ } finally {
496
+ this.rootComponent.registerLoading(false)
497
+ }
498
+ },
499
+
454
500
  notify(options) {
455
501
  this.rootComponent.notify(options)
456
502
  },
@@ -1,10 +1,7 @@
1
1
  import DitoContext from '../DitoContext.js'
2
- import {
3
- getItemFormSchema,
4
- getItemId,
5
- getItemUid,
6
- isListSource
7
- } from '../utils/schema.js'
2
+ import { isListSource } from '../utils/schema/structure.js'
3
+ import { getItemFormSchema } from '../utils/schema/lookup.js'
4
+ import { getItemId, getItemUid } from '../utils/schema/data.js'
8
5
  import { appendDataPath } from '../utils/data.js'
9
6
  import { isObject, isString, isNumber, isFunction } from '@ditojs/utils'
10
7
 
@@ -1,8 +1,55 @@
1
1
  import { getSchemaAccessor } from '../utils/accessor.js'
2
+ import { getSchemaValue } from '../utils/schema/data.js'
2
3
  import { isArray } from '@ditojs/utils'
3
4
 
4
5
  // @vue/component
5
6
  export default {
7
+ getTypeValidations(schema, context) {
8
+ // Mirrors the `min`, `max`, `range`, `step` and `decimals` accessors below,
9
+ // but based on the schema alone, so they can be used without a component.
10
+ const isInteger = schema.type === 'integer'
11
+ const getNumberSchemaValue = key =>
12
+ getSchemaValue(key, { type: Number, schema, context })
13
+ const roundIfInteger = (value, roundFunction) =>
14
+ isInteger && value != null ? roundFunction(value) : value
15
+ const range = getSchemaValue('range', { type: Array, schema, context })
16
+ const min = roundIfInteger(
17
+ getNumberSchemaValue('min') ?? range?.[0],
18
+ Math.floor
19
+ )
20
+ const max = roundIfInteger(
21
+ getNumberSchemaValue('max') ?? range?.[1],
22
+ Math.ceil
23
+ )
24
+ const step = roundIfInteger(getNumberSchemaValue('step'), Math.ceil)
25
+ const decimals = getNumberSchemaValue('decimals')
26
+ const validations = {}
27
+ if (min != null && max != null) {
28
+ validations.range = [min, max]
29
+ } else {
30
+ if (min != null) {
31
+ validations.min = min
32
+ }
33
+ if (max != null) {
34
+ validations.max = max
35
+ }
36
+ }
37
+ if (decimals != null) {
38
+ validations.decimals = decimals
39
+ } else if (step) {
40
+ const stepDecimals = (`${step}`.split('.')[1] || '').length
41
+ if (stepDecimals > 0) {
42
+ validations.decimals = stepDecimals
43
+ } else {
44
+ validations.integer = true
45
+ }
46
+ }
47
+ if (isInteger) {
48
+ validations.integer = true
49
+ }
50
+ return validations
51
+ },
52
+
6
53
  computed: {
7
54
  inputValue: {
8
55
  get() {
@@ -83,36 +130,5 @@ export default {
83
130
  }
84
131
  }
85
132
  })
86
- },
87
-
88
- methods: {
89
- getValidations() {
90
- const validations = {}
91
- const { range, min, max, decimals, step } = this
92
- if (range) {
93
- validations.range = range
94
- } else {
95
- if (min != null) {
96
- validations.min = min
97
- }
98
- if (max != null) {
99
- validations.max = max
100
- }
101
- }
102
- if (decimals != null) {
103
- validations.decimals = decimals
104
- } else if (step) {
105
- const decimals = (`${step}`.split('.')[1] || '').length
106
- if (decimals > 0) {
107
- validations.decimals = decimals
108
- } else {
109
- validations.integer = true
110
- }
111
- }
112
- if (this.isInteger) {
113
- validations.integer = true
114
- }
115
- return validations
116
- }
117
133
  }
118
134
  }
@@ -1,10 +1,7 @@
1
+ import { toRaw } from 'vue'
1
2
  import DitoContext from '../DitoContext.js'
2
- import DataMixin from './DataMixin.js'
3
- import {
4
- hasViewSchema,
5
- getViewEditPath,
6
- getMultipleValue
7
- } from '../utils/schema.js'
3
+ import { hasViewSchema, getViewEditPath } from '../utils/schema/lookup.js'
4
+ import { getMultipleValue } from '../utils/schema/data.js'
8
5
  import { getSchemaAccessor } from '../utils/accessor.js'
9
6
  import { setTemporaryId, isReference } from '../utils/data.js'
10
7
  import {
@@ -19,8 +16,6 @@ import {
19
16
 
20
17
  // @vue/component
21
18
  export default {
22
- mixins: [DataMixin],
23
-
24
19
  computed: {
25
20
  // @overridable
26
21
  multiple() {
@@ -41,33 +36,27 @@ export default {
41
36
  : value
42
37
  }
43
38
 
44
- const value =
45
- this.multiple && isArray(this.value)
46
- ? this.value.map(convertValue).filter(value => value !== null)
47
- : convertValue(this.value)
48
-
49
- if (
50
- // As soon as the options are available, and...
51
- this.hasOptions && (
52
- // ...if the value is forced to null because a disappeared option...
53
- value === null && this.value !== null ||
54
- // ...or if the value is a reference, replace it with its option
55
- // value, so that it'll hold actual data, not just a reference id.
56
- isReference(this.value)
57
- )
58
- ) {
59
- // TODO: Fix side-effects
60
- // eslint-disable-next-line vue/no-side-effects-in-computed-properties
61
- this.selectedValue = value
62
- }
63
- return value
39
+ return this.multiple && isArray(this.value)
40
+ ? this.value.map(convertValue).filter(value => value !== null)
41
+ : convertValue(this.value)
64
42
  },
65
43
 
66
44
  set(value) {
67
- const convertValue = value =>
68
- this.relate
69
- ? this.getOptionForValue(value)
70
- : value
45
+ const convertValue = value => {
46
+ if (!this.relate) {
47
+ return value
48
+ }
49
+ const option = this.getOptionForValue(value)
50
+ if (this.shouldSetTemporaryId(option)) {
51
+ // Options without ids, e.g. new items of the edited data, get
52
+ // temporary ids when selected, so that the relation can reference
53
+ // them when saving, see `SchemaGraph`.
54
+ // NOTE: We need to modify the actual data, making a copy won't
55
+ // work as it won't propagate.
56
+ setTemporaryId(option, 'id')
57
+ }
58
+ return option
59
+ }
71
60
 
72
61
  this.value =
73
62
  this.multiple && isArray(value)
@@ -76,16 +65,52 @@ export default {
76
65
  }
77
66
  },
78
67
 
68
+ // Whether the value needs to be replaced with the selected value, once the
69
+ // options are available: if the value is forced to `null` because its
70
+ // option disappeared, or if the value is a reference, so that it'll hold
71
+ // actual data, not just a reference id. See `watch`.
72
+ shouldReplaceValueWithSelectedValue() {
73
+ return (
74
+ this.hasOptions && (
75
+ this.selectedValue === null && this.value !== null ||
76
+ isReference(this.value)
77
+ )
78
+ )
79
+ },
80
+
79
81
  selectedOption() {
80
82
  return this.getOptionForValue(this.selectedValue)
81
83
  },
82
84
 
85
+ // The form model of the data, owned by the dialog or route component.
86
+ formModel() {
87
+ return (this.dialogComponent ?? this.routeComponent).formModel
88
+ },
89
+
90
+ // The resolver of the options in the form model of the data, which loads
91
+ // them and shares them with the computes of the form. It is looked up with
92
+ // the entry of this component, in the shape of the entries of
93
+ // `processSchemaData()`, see `FormModel`.
94
+ optionsResolver() {
95
+ return this.formModel.getOptionsResolver({
96
+ schema: this.schema,
97
+ data: this.data,
98
+ name: this.name,
99
+ dataPath: this.dataPath,
100
+ componentPath: this.componentPath
101
+ })
102
+ },
103
+
104
+ isLoadingOptions() {
105
+ return this.optionsResolver.isLoading
106
+ },
107
+
83
108
  options() {
84
- const data = this.handleDataSchema(this.schema.options, 'options') ?? []
85
- if (!isArray(data)) {
86
- throw new Error(`Invalid options data, should be array: ${data}`)
109
+ const options = this.optionsResolver.value ?? []
110
+ if (!isArray(options)) {
111
+ throw new Error(`Invalid options data, should be array: ${options}`)
87
112
  }
88
- return this.processOptions(data)
113
+ return this.groupBy ? this.groupOptions(options) : options
89
114
  },
90
115
 
91
116
  activeOptions() {
@@ -188,48 +213,79 @@ export default {
188
213
  }
189
214
  },
190
215
 
216
+ watch: {
217
+ // Replace the value with the selected value as soon as the options are
218
+ // available, and also when the value changes while it still needs to be
219
+ // replaced, e.g. a relation without value that is set to a reference.
220
+ 'shouldReplaceValueWithSelectedValue': {
221
+ handler(shouldReplace) {
222
+ if (shouldReplace) {
223
+ this.replaceValueWithSelectedValue()
224
+ }
225
+ },
226
+ immediate: true
227
+ },
228
+
229
+ 'value'() {
230
+ if (this.shouldReplaceValueWithSelectedValue) {
231
+ this.replaceValueWithSelectedValue()
232
+ }
233
+ },
234
+
235
+ 'optionsResolver.lastLoadError'(error) {
236
+ if (error) {
237
+ this.addError(error.message || error)
238
+ }
239
+ }
240
+ },
241
+
191
242
  methods: {
192
243
  getOptionKey(key) {
193
244
  const [option] = this.activeOptions
194
245
  return isObject(option) && key in option ? key : null
195
246
  },
196
247
 
197
- processOptions(options) {
198
- if (options.length) {
199
- if (this.relate) {
200
- // If ids are missing and we want to relate, set temporary ids.
201
- // NOTE: We need to modify the actual data, making a copy won't work
202
- // as it won't propagate.
203
- // NOTE: This only makes sense if the data is from the graph that
204
- // we're currently editing.
205
- for (const option of options) {
206
- if (!('id' in option)) {
207
- // TODO: Fix side-effects
208
- setTemporaryId(option, 'id')
248
+ // Writes the selected value back into the data, through the setter of
249
+ // `selectedValue`, which converts it to the value, e.g. a reference id to
250
+ // its option with `relate`, or a value whose option disappeared to `null`.
251
+ // This is derived from the options, not a change by the user, so it is
252
+ // made as a clean change, which doesn't make the form dirty.
253
+ replaceValueWithSelectedValue() {
254
+ const { selectedValue } = this
255
+ this.formModel
256
+ .applyCleanChanges(() => {
257
+ this.selectedValue = selectedValue
258
+ })
259
+ .catch(console.error)
260
+ },
261
+
262
+ // Groups the options by the `groupBy` key, see `groupByLabel` and
263
+ // `groupByOptions`.
264
+ groupOptions(options) {
265
+ const groups = {}
266
+ return options.reduce(
267
+ (results, option) => {
268
+ const groupName = option[this.groupBy]
269
+ let group = groups[groupName]
270
+ if (!group) {
271
+ group = groups[groupName] = {
272
+ [this.groupByLabel]: groupName,
273
+ [this.groupByOptions]: []
209
274
  }
275
+ results.push(group)
210
276
  }
211
- }
212
- if (this.groupBy) {
213
- const grouped = {}
214
- options = options.reduce(
215
- (results, option) => {
216
- const group = option[this.groupBy]
217
- let entry = grouped[group]
218
- if (!entry) {
219
- entry = grouped[group] = {
220
- [this.groupByLabel]: group,
221
- [this.groupByOptions]: []
222
- }
223
- results.push(entry)
224
- }
225
- entry[this.groupByOptions].push(option)
226
- return results
227
- },
228
- []
229
- )
230
- }
231
- }
232
- return options
277
+ group[this.groupByOptions].push(option)
278
+ return results
279
+ },
280
+ []
281
+ )
282
+ },
283
+
284
+ // Whether the option needs a temporary id to be related to, as it has no
285
+ // id yet, e.g. a new item of the edited data. It gets one when selected,
286
+ // see `selectedValue`.
287
+ shouldSetTemporaryId(option) {
288
+ return this.relate && isObject(option) && option.id == null
233
289
  },
234
290
 
235
291
  getOptionForValue(value) {
@@ -259,6 +315,11 @@ export default {
259
315
  },
260
316
 
261
317
  getValueForOption(option) {
318
+ if (this.shouldSetTemporaryId(option)) {
319
+ // Until they're selected and get temporary ids, options without ids
320
+ // are their own values, compared by identity, see `selectedValue`.
321
+ return toRaw(option)
322
+ }
262
323
  const { optionValue } = this
263
324
  return isString(optionValue)
264
325
  ? option?.[optionValue] ?? null
@@ -281,7 +342,7 @@ export default {
281
342
  if (schema.relate) {
282
343
  // For internally relating data (`schema.options.dataPath`), we need to
283
344
  // process both the options (for '#ref') and the value ('#id').
284
- // See `DataMixin.handleDataSchema()`:
345
+ // See `DataSchemaResolver`:
285
346
  const path = schema.options?.dataPath
286
347
  const relatedDataPath = path
287
348
  ? normalizeDataPath(`${dataPath}/${path}`)
@@ -1,6 +1,6 @@
1
1
  import ItemMixin from './ItemMixin.js'
2
2
  import LoadingMixin from './LoadingMixin.js'
3
- import { setDefaultValues } from '../utils/schema.js'
3
+ import { initializeData } from '../utils/schema/data.js'
4
4
  import { assignDeeply, isObject, isString, labelize } from '@ditojs/utils'
5
5
  import { getResource } from '../utils/resource.js'
6
6
  import DitoContext from '../DitoContext.js'
@@ -160,9 +160,12 @@ export default {
160
160
  this.loadedData = null
161
161
  },
162
162
 
163
+ // Sets the loaded data and returns it as the reactive data of the
164
+ // component, which can be modified to update it.
163
165
  // @overridable
164
166
  setData(data) {
165
167
  this.loadedData = data
168
+ return this.loadedData
166
169
  },
167
170
 
168
171
  setupData() {
@@ -196,7 +199,7 @@ export default {
196
199
  },
197
200
 
198
201
  createData(schema, type) {
199
- return setDefaultValues(schema, type ? { type } : {}, this)
202
+ return initializeData(schema, type ? { type } : {}, this)
200
203
  },
201
204
 
202
205
  requestData() {