@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.
- package/dist/dito-admin.css +1 -1
- package/dist/dito-admin.es.js +3555 -2719
- package/dist/dito-admin.umd.js +5 -6
- package/package.json +84 -46
- package/src/DitoAdmin.js +1 -65
- package/src/DitoComponent.js +1 -1
- package/src/DitoContext.js +51 -0
- package/src/DitoTypeComponent.js +23 -1
- package/src/components/DitoAffixes.vue +6 -3
- package/src/components/DitoButtons.vue +14 -1
- 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 +152 -84
- package/src/components/DitoLabel.vue +3 -2
- package/src/components/DitoPane.vue +55 -11
- package/src/components/DitoPanel.vue +34 -5
- package/src/components/DitoPanels.vue +2 -1
- package/src/components/DitoRoot.vue +6 -6
- package/src/components/DitoRouterView.vue +27 -0
- 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 +21 -3
- package/src/components/index.js +1 -0
- package/src/mixins/DitoMixin.js +53 -7
- package/src/mixins/ItemMixin.js +3 -6
- package/src/mixins/NumberMixin.js +47 -31
- package/src/mixins/OptionsMixin.js +132 -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 +155 -96
- 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 +37 -6
- package/src/types/DitoTypeNumber.vue +1 -1
- package/src/types/DitoTypeObject.vue +10 -7
- package/src/types/DitoTypePanel.vue +20 -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 +5 -7
- package/src/types/DitoTypeUpload.vue +1 -2
- package/src/utils/DataSchemaResolver.js +154 -0
- package/src/utils/FormModel.js +615 -0
- package/src/utils/data.js +20 -0
- package/src/utils/filter.js +3 -2
- package/src/utils/options.js +3 -0
- package/src/utils/request.js +70 -0
- package/src/utils/schema/data.js +659 -0
- package/src/utils/schema/lookup.js +250 -0
- package/src/utils/schema/setup.js +354 -0
- package/src/utils/schema/structure.js +172 -0
- package/src/utils/schema/types.js +34 -0
- package/src/utils/schema/validation.js +142 -0
- package/src/utils/units.js +70 -3
- package/types/index.d.ts +174 -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)
|
|
@@ -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
|
-
|
|
9
|
+
DitoRouterView(
|
|
10
10
|
v-if="!isLastRoute"
|
|
11
|
-
:
|
|
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) {
|
package/src/components/index.js
CHANGED
|
@@ -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'
|
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,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
|
-
|
|
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
|
},
|
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,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
|
|
85
|
-
if (!isArray(
|
|
86
|
-
throw new Error(`Invalid options data, should be array: ${
|
|
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.
|
|
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
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
-
|
|
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
|
|
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 `
|
|
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 {
|
|
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() {
|