@ditojs/admin 2.99.1 → 3.0.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.
- package/dist/dito-admin.css +1 -1
- package/dist/dito-admin.es.js +3532 -2763
- package/dist/dito-admin.umd.js +6 -6
- package/package.json +82 -46
- package/src/DitoComponent.js +1 -1
- package/src/DitoContext.js +51 -0
- package/src/DitoTypeComponent.js +6 -1
- package/src/components/DitoAffixes.vue +6 -3
- package/src/components/DitoContainer.vue +19 -10
- package/src/components/DitoCreateButton.vue +11 -2
- package/src/components/DitoDialog.vue +21 -2
- package/src/components/DitoForm.vue +156 -83
- package/src/components/DitoLabel.vue +3 -2
- package/src/components/DitoPane.vue +55 -11
- package/src/components/DitoPanel.vue +33 -5
- package/src/components/DitoPanels.vue +2 -1
- package/src/components/DitoRoot.vue +13 -6
- package/src/components/DitoSchema.vue +278 -145
- package/src/components/DitoSchemaInlined.vue +1 -1
- package/src/components/DitoTabs.vue +16 -18
- package/src/components/DitoTrail.vue +5 -4
- package/src/components/DitoTreeItem.vue +17 -6
- package/src/components/DitoView.vue +25 -2
- package/src/mixins/DitoMixin.js +42 -7
- package/src/mixins/ItemMixin.js +3 -6
- package/src/mixins/NumberMixin.js +47 -31
- package/src/mixins/OptionsMixin.js +122 -71
- package/src/mixins/ResourceMixin.js +5 -2
- package/src/mixins/RouteMixin.js +39 -1
- package/src/mixins/SortableMixin.js +0 -13
- package/src/mixins/SourceMixin.js +142 -95
- package/src/mixins/TextMixin.js +1 -1
- package/src/mixins/TypeMixin.js +37 -22
- package/src/mixins/ValidationMixin.js +9 -27
- package/src/mixins/ValidatorMixin.js +5 -7
- package/src/mixins/ValueMixin.js +5 -2
- package/src/types/DitoTypeButton.vue +1 -1
- package/src/types/DitoTypeCheckbox.vue +1 -1
- package/src/types/DitoTypeCheckboxes.vue +1 -1
- package/src/types/DitoTypeCode.vue +8 -1
- package/src/types/DitoTypeColor.vue +20 -34
- package/src/types/DitoTypeComponent.vue +1 -1
- package/src/types/DitoTypeComputed.vue +4 -27
- package/src/types/DitoTypeDate.vue +16 -5
- package/src/types/DitoTypeLabel.vue +1 -4
- package/src/types/DitoTypeList.vue +9 -5
- package/src/types/DitoTypeMarkup.vue +25 -13
- package/src/types/DitoTypeMultiselect.vue +26 -6
- package/src/types/DitoTypeNumber.vue +1 -1
- package/src/types/DitoTypeObject.vue +10 -7
- package/src/types/DitoTypePanel.vue +19 -6
- package/src/types/DitoTypeProgress.vue +1 -1
- package/src/types/DitoTypeRadio.vue +1 -1
- package/src/types/DitoTypeSection.vue +32 -4
- package/src/types/DitoTypeSelect.vue +1 -1
- package/src/types/DitoTypeSlider.vue +1 -1
- package/src/types/DitoTypeSwitch.vue +1 -1
- package/src/types/DitoTypeText.vue +13 -15
- package/src/types/DitoTypeTextarea.vue +1 -1
- package/src/types/DitoTypeTreeList.vue +12 -7
- package/src/types/DitoTypeUpload.vue +1 -2
- package/src/utils/DataSchemaResolver.js +141 -0
- package/src/utils/DataSchemaResolver.test.js +132 -0
- package/src/utils/FormModel.js +556 -0
- package/src/utils/FormModel.test.js +808 -0
- package/src/utils/data.js +20 -0
- package/src/utils/data.test.js +28 -0
- package/src/utils/filter.js +3 -2
- package/src/utils/options.js +3 -0
- package/src/utils/schema/data.js +617 -0
- package/src/utils/schema/data.test.js +471 -0
- package/src/utils/schema/lookup.js +266 -0
- package/src/utils/schema/lookup.test.js +61 -0
- package/src/utils/schema/setup.js +354 -0
- package/src/utils/schema/structure.js +172 -0
- package/src/utils/schema/structure.test.js +57 -0
- package/src/utils/schema/types.js +34 -0
- package/src/utils/schema/validation.js +142 -0
- package/src/utils/schema/validation.test.js +395 -0
- package/src/utils/units.js +70 -3
- package/src/utils/units.test.js +71 -0
- package/types/index.d.ts +149 -59
- package/src/mixins/DataMixin.js +0 -131
- 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 =>
|
|
29
|
+
component => !component.isLeavingRoute
|
|
29
30
|
)
|
|
30
31
|
}
|
|
31
32
|
},
|
|
32
33
|
|
|
33
34
|
methods: {
|
|
34
|
-
|
|
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.
|
|
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.
|
|
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="
|
|
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="
|
|
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
|
-
|
|
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)
|
|
@@ -8,8 +8,13 @@ template(
|
|
|
8
8
|
//- used to always render forms even when other nested forms are present.
|
|
9
9
|
RouterView(
|
|
10
10
|
v-if="!isLastRoute"
|
|
11
|
-
|
|
11
|
+
v-slot="{ Component, route }"
|
|
12
12
|
)
|
|
13
|
+
component(
|
|
14
|
+
v-if="Component"
|
|
15
|
+
:is="Component"
|
|
16
|
+
:key="getRouteComponentKey(route, routeLevel + 1)"
|
|
17
|
+
)
|
|
13
18
|
.dito-view.dito-scroll-parent(
|
|
14
19
|
v-else
|
|
15
20
|
:data-resource="sourceSchema.path"
|
|
@@ -33,8 +38,9 @@ import RouteMixin from '../mixins/RouteMixin.js'
|
|
|
33
38
|
import {
|
|
34
39
|
isSingleComponentView,
|
|
35
40
|
someNestedSchemaComponent
|
|
36
|
-
} from '../utils/schema.js'
|
|
41
|
+
} from '../utils/schema/structure.js'
|
|
37
42
|
import { hasResource } from '../utils/resource.js'
|
|
43
|
+
import { FormModel } from '../utils/FormModel.js'
|
|
38
44
|
|
|
39
45
|
// @vue/component
|
|
40
46
|
export default DitoComponent.component('DitoView', {
|
|
@@ -116,6 +122,16 @@ export default DitoComponent.component('DitoView', {
|
|
|
116
122
|
}
|
|
117
123
|
},
|
|
118
124
|
|
|
125
|
+
created() {
|
|
126
|
+
// Writes defaults and computed values into the view's data and resolves
|
|
127
|
+
// options, see `FormModel`:
|
|
128
|
+
this.formModel = new FormModel({
|
|
129
|
+
component: this,
|
|
130
|
+
getSchema: () => this.viewSchema,
|
|
131
|
+
getData: () => this.data
|
|
132
|
+
})
|
|
133
|
+
},
|
|
134
|
+
|
|
119
135
|
mounted() {
|
|
120
136
|
// Prevent bypassing of if-condition by direct URL access.
|
|
121
137
|
if (!this.shouldRenderSchema(this.viewSchema)) {
|
|
@@ -123,9 +139,16 @@ export default DitoComponent.component('DitoView', {
|
|
|
123
139
|
}
|
|
124
140
|
},
|
|
125
141
|
|
|
142
|
+
beforeUnmount() {
|
|
143
|
+
// Stop the model before the component's own watchers are stopped, as the
|
|
144
|
+
// model's watchers aren't part of the component, see `FormModel`.
|
|
145
|
+
this.formModel.stop()
|
|
146
|
+
},
|
|
147
|
+
|
|
126
148
|
methods: {
|
|
127
149
|
setData(data) {
|
|
128
150
|
this.data = data
|
|
151
|
+
return this.data
|
|
129
152
|
},
|
|
130
153
|
|
|
131
154
|
getChildPath(path) {
|
package/src/mixins/DitoMixin.js
CHANGED
|
@@ -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
|
-
|
|
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,34 @@ 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, `DitoForm`
|
|
111
|
+
// and `DitoTreeItem` continue the path of the source of their item.
|
|
112
|
+
componentPath() {
|
|
113
|
+
const { parentComponent } = this
|
|
114
|
+
const parentComponentPath = parentComponent?.componentPath ?? ''
|
|
115
|
+
const relativeDataPath = getRelativeDataPath(
|
|
116
|
+
this.dataPath,
|
|
117
|
+
parentComponent?.dataPathForComponentPath
|
|
118
|
+
)
|
|
119
|
+
return relativeDataPath
|
|
120
|
+
? appendDataPath(parentComponentPath, relativeDataPath)
|
|
121
|
+
: parentComponentPath
|
|
122
|
+
},
|
|
123
|
+
|
|
124
|
+
// The data path that `componentPath` corresponds to, which children
|
|
125
|
+
// compare their data path with: the component's own, or for components
|
|
126
|
+
// without data path, e.g. `DitoDraggable` between lists and their items,
|
|
127
|
+
// the one of their parent.
|
|
128
|
+
dataPathForComponentPath() {
|
|
129
|
+
return this.dataPath ?? this.parentComponent?.dataPathForComponentPath
|
|
130
|
+
},
|
|
131
|
+
|
|
106
132
|
// Use computed properties as links to injects, so DitoSchema can
|
|
107
133
|
// override the property and return `this` instead of the parent.
|
|
108
134
|
parentComponent() {
|
|
@@ -276,8 +302,9 @@ export default {
|
|
|
276
302
|
: labelize(name) || ''
|
|
277
303
|
},
|
|
278
304
|
|
|
279
|
-
getButtonAttributes(verb) {
|
|
280
|
-
|
|
305
|
+
getButtonAttributes(verb, subject = null) {
|
|
306
|
+
// Name buttons by what they act on if known, e.g. 'Add Section'.
|
|
307
|
+
const label = subject ? `${labelize(verb)} ${subject}` : labelize(verb)
|
|
281
308
|
return {
|
|
282
309
|
'class': `dito-button--${verb}`,
|
|
283
310
|
'title': label,
|
|
@@ -451,6 +478,14 @@ export default {
|
|
|
451
478
|
body.removeChild(a)
|
|
452
479
|
},
|
|
453
480
|
|
|
481
|
+
// Returns the key of the route component that `RouterView` renders at
|
|
482
|
+
// `routeLevel`, so that each route record gets its own component instance,
|
|
483
|
+
// as their schema and data belong to it, while the component is reused
|
|
484
|
+
// when only the parameters change, e.g. to edit another item.
|
|
485
|
+
getRouteComponentKey(route, routeLevel) {
|
|
486
|
+
return route.matched[routeLevel]?.path
|
|
487
|
+
},
|
|
488
|
+
|
|
454
489
|
notify(options) {
|
|
455
490
|
this.rootComponent.notify(options)
|
|
456
491
|
},
|
package/src/mixins/ItemMixin.js
CHANGED
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
import DitoContext from '../DitoContext.js'
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
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
|
|
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
|
-
|
|
45
|
-
this.
|
|
46
|
-
|
|
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
|
-
|
|
70
|
-
|
|
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,48 @@ 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 resolver of the options in the form model of the data, which loads
|
|
86
|
+
// them and shares them with the computes of the form. It is looked up with
|
|
87
|
+
// the entry of this component, in the shape of the entries of
|
|
88
|
+
// `processSchemaData()`, see `FormModel`.
|
|
89
|
+
optionsResolver() {
|
|
90
|
+
const formModel = (this.dialogComponent ?? this.routeComponent).formModel
|
|
91
|
+
return formModel.getOptionsResolver({
|
|
92
|
+
schema: this.schema,
|
|
93
|
+
data: this.data,
|
|
94
|
+
name: this.name,
|
|
95
|
+
dataPath: this.dataPath,
|
|
96
|
+
componentPath: this.componentPath
|
|
97
|
+
})
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
isLoadingOptions() {
|
|
101
|
+
return this.optionsResolver.isLoading
|
|
102
|
+
},
|
|
103
|
+
|
|
83
104
|
options() {
|
|
84
|
-
const
|
|
85
|
-
if (!isArray(
|
|
86
|
-
throw new Error(`Invalid options data, should be array: ${
|
|
105
|
+
const options = this.optionsResolver.value ?? []
|
|
106
|
+
if (!isArray(options)) {
|
|
107
|
+
throw new Error(`Invalid options data, should be array: ${options}`)
|
|
87
108
|
}
|
|
88
|
-
return this.
|
|
109
|
+
return this.groupBy ? this.groupOptions(options) : options
|
|
89
110
|
},
|
|
90
111
|
|
|
91
112
|
activeOptions() {
|
|
@@ -188,48 +209,73 @@ export default {
|
|
|
188
209
|
}
|
|
189
210
|
},
|
|
190
211
|
|
|
212
|
+
watch: {
|
|
213
|
+
// Replace the value with the selected value as soon as the options are
|
|
214
|
+
// available, and also when the value changes while it still needs to be
|
|
215
|
+
// replaced, e.g. a relation without value that is set to a reference.
|
|
216
|
+
'shouldReplaceValueWithSelectedValue': {
|
|
217
|
+
handler(shouldReplace) {
|
|
218
|
+
if (shouldReplace) {
|
|
219
|
+
this.replaceValueWithSelectedValue()
|
|
220
|
+
}
|
|
221
|
+
},
|
|
222
|
+
immediate: true
|
|
223
|
+
},
|
|
224
|
+
|
|
225
|
+
'value'() {
|
|
226
|
+
if (this.shouldReplaceValueWithSelectedValue) {
|
|
227
|
+
this.replaceValueWithSelectedValue()
|
|
228
|
+
}
|
|
229
|
+
},
|
|
230
|
+
|
|
231
|
+
'optionsResolver.lastLoadError'(error) {
|
|
232
|
+
if (error) {
|
|
233
|
+
this.addError(error.message || error)
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
},
|
|
237
|
+
|
|
191
238
|
methods: {
|
|
192
239
|
getOptionKey(key) {
|
|
193
240
|
const [option] = this.activeOptions
|
|
194
241
|
return isObject(option) && key in option ? key : null
|
|
195
242
|
},
|
|
196
243
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
244
|
+
// Writes the selected value back into the data, through the setter of
|
|
245
|
+
// `selectedValue`, which converts it to the value, e.g. a reference id to
|
|
246
|
+
// its option with `relate`.
|
|
247
|
+
replaceValueWithSelectedValue() {
|
|
248
|
+
const { selectedValue } = this
|
|
249
|
+
this.selectedValue = selectedValue
|
|
250
|
+
},
|
|
251
|
+
|
|
252
|
+
// Groups the options by the `groupBy` key, see `groupByLabel` and
|
|
253
|
+
// `groupByOptions`.
|
|
254
|
+
groupOptions(options) {
|
|
255
|
+
const groups = {}
|
|
256
|
+
return options.reduce(
|
|
257
|
+
(results, option) => {
|
|
258
|
+
const groupName = option[this.groupBy]
|
|
259
|
+
let group = groups[groupName]
|
|
260
|
+
if (!group) {
|
|
261
|
+
group = groups[groupName] = {
|
|
262
|
+
[this.groupByLabel]: groupName,
|
|
263
|
+
[this.groupByOptions]: []
|
|
209
264
|
}
|
|
265
|
+
results.push(group)
|
|
210
266
|
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
results.push(entry)
|
|
224
|
-
}
|
|
225
|
-
entry[this.groupByOptions].push(option)
|
|
226
|
-
return results
|
|
227
|
-
},
|
|
228
|
-
[]
|
|
229
|
-
)
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
return options
|
|
267
|
+
group[this.groupByOptions].push(option)
|
|
268
|
+
return results
|
|
269
|
+
},
|
|
270
|
+
[]
|
|
271
|
+
)
|
|
272
|
+
},
|
|
273
|
+
|
|
274
|
+
// Whether the option needs a temporary id to be related to, as it has no
|
|
275
|
+
// id yet, e.g. a new item of the edited data. It gets one when selected,
|
|
276
|
+
// see `selectedValue`.
|
|
277
|
+
shouldSetTemporaryId(option) {
|
|
278
|
+
return this.relate && isObject(option) && option.id == null
|
|
233
279
|
},
|
|
234
280
|
|
|
235
281
|
getOptionForValue(value) {
|
|
@@ -259,6 +305,11 @@ export default {
|
|
|
259
305
|
},
|
|
260
306
|
|
|
261
307
|
getValueForOption(option) {
|
|
308
|
+
if (this.shouldSetTemporaryId(option)) {
|
|
309
|
+
// Until they're selected and get temporary ids, options without ids
|
|
310
|
+
// are their own values, compared by identity, see `selectedValue`.
|
|
311
|
+
return toRaw(option)
|
|
312
|
+
}
|
|
262
313
|
const { optionValue } = this
|
|
263
314
|
return isString(optionValue)
|
|
264
315
|
? option?.[optionValue] ?? null
|
|
@@ -281,7 +332,7 @@ export default {
|
|
|
281
332
|
if (schema.relate) {
|
|
282
333
|
// For internally relating data (`schema.options.dataPath`), we need to
|
|
283
334
|
// process both the options (for '#ref') and the value ('#id').
|
|
284
|
-
// See `
|
|
335
|
+
// See `DataSchemaResolver`:
|
|
285
336
|
const path = schema.options?.dataPath
|
|
286
337
|
const relatedDataPath = path
|
|
287
338
|
? normalizeDataPath(`${dataPath}/${path}`)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import ItemMixin from './ItemMixin.js'
|
|
2
2
|
import LoadingMixin from './LoadingMixin.js'
|
|
3
|
-
import {
|
|
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
|
|
202
|
+
return initializeData(schema, type ? { type } : {}, this)
|
|
200
203
|
},
|
|
201
204
|
|
|
202
205
|
requestData() {
|