@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
package/src/mixins/RouteMixin.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import ValidatorMixin from '../mixins/ValidatorMixin.js'
|
|
2
|
+
import { markRaw } from 'vue'
|
|
2
3
|
import { getCommonPrefix } from '@ditojs/utils'
|
|
3
4
|
|
|
4
5
|
// @vue/component
|
|
@@ -14,6 +15,9 @@ export default {
|
|
|
14
15
|
data() {
|
|
15
16
|
return {
|
|
16
17
|
reload: false,
|
|
18
|
+
// The record of the route that this component renders, see the
|
|
19
|
+
// `matchedRouteRecord` watcher:
|
|
20
|
+
routeRecord: null,
|
|
17
21
|
// Each route-component defines a store that gets passed on to its
|
|
18
22
|
// child components, so they can store values in them that live beyond
|
|
19
23
|
// their life-cycle. See: DitoPane, SourceMixin
|
|
@@ -37,10 +41,19 @@ export default {
|
|
|
37
41
|
return level
|
|
38
42
|
},
|
|
39
43
|
|
|
40
|
-
|
|
44
|
+
// The record that the current route matches at this component's level,
|
|
45
|
+
// which is another record or `undefined` while the component is being left,
|
|
46
|
+
// until it's unmounted, see `routeRecord`.
|
|
47
|
+
matchedRouteRecord() {
|
|
41
48
|
return this.$route.matched[this.routeLevel]
|
|
42
49
|
},
|
|
43
50
|
|
|
51
|
+
// Whether the current route doesn't match the component's record anymore,
|
|
52
|
+
// so that the component is about to be unmounted.
|
|
53
|
+
isLeavingRoute() {
|
|
54
|
+
return this.matchedRouteRecord?.path !== this.routeRecord?.path
|
|
55
|
+
},
|
|
56
|
+
|
|
44
57
|
isLastRoute() {
|
|
45
58
|
// Returns true when this router component is the last one in the route.
|
|
46
59
|
const { matched } = this.$route
|
|
@@ -106,6 +119,31 @@ export default {
|
|
|
106
119
|
}
|
|
107
120
|
},
|
|
108
121
|
|
|
122
|
+
watch: {
|
|
123
|
+
matchedRouteRecord: {
|
|
124
|
+
immediate: true,
|
|
125
|
+
// Update synchronously, so that the record changes with the route.
|
|
126
|
+
flush: 'sync',
|
|
127
|
+
handler(routeRecord) {
|
|
128
|
+
// Components belong to the record that they're rendered for, see
|
|
129
|
+
// `getRouteComponentKey()`. Records of other paths, or none, mean that
|
|
130
|
+
// the component is being left, and it keeps its record until it's
|
|
131
|
+
// unmounted, as its schema and meta are still read in the meantime,
|
|
132
|
+
// e.g. by the watchers of its form model. Records of the same path
|
|
133
|
+
// replace it, e.g. when the routes are set up again.
|
|
134
|
+
const isOwnRouteRecord = (
|
|
135
|
+
!!routeRecord &&
|
|
136
|
+
(!this.routeRecord || routeRecord.path === this.routeRecord.path)
|
|
137
|
+
)
|
|
138
|
+
if (isOwnRouteRecord) {
|
|
139
|
+
// Keep the record raw, as it's compared with the records in
|
|
140
|
+
// `$route.matched`, which aren't reactive.
|
|
141
|
+
this.routeRecord = markRaw(routeRecord)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
|
|
109
147
|
beforeRouteUpdate(to, from) {
|
|
110
148
|
return this?.beforeRouteChange(to, from)
|
|
111
149
|
},
|
|
@@ -33,19 +33,6 @@ export default {
|
|
|
33
33
|
if (oldIndex !== newIndex) {
|
|
34
34
|
this.onChange()
|
|
35
35
|
}
|
|
36
|
-
},
|
|
37
|
-
|
|
38
|
-
updateOrder(sourceSchema, list, paginationRange) {
|
|
39
|
-
const { orderKey } = sourceSchema
|
|
40
|
-
if (orderKey) {
|
|
41
|
-
// Reorder the changed entries by their order key, taking pagination
|
|
42
|
-
// offsets into account:
|
|
43
|
-
const offset = paginationRange?.[0] || 0
|
|
44
|
-
for (let i = 0; i < list.length; i++) {
|
|
45
|
-
list[i][orderKey] = i + offset
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
return list
|
|
49
36
|
}
|
|
50
37
|
}
|
|
51
38
|
}
|
|
@@ -6,19 +6,21 @@ import { getSchemaAccessor, getStoreAccessor } from '../utils/accessor.js'
|
|
|
6
6
|
import { getMemberResource } from '../utils/resource.js'
|
|
7
7
|
import { replaceRoute } from '../utils/route.js'
|
|
8
8
|
import {
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
isCompact,
|
|
10
|
+
isInlined,
|
|
11
|
+
isObjectSource,
|
|
12
|
+
isListSource
|
|
13
|
+
} from '../utils/schema/structure.js'
|
|
14
|
+
import { setupRouteSchema, setupForms } from '../utils/schema/setup.js'
|
|
15
|
+
import {
|
|
11
16
|
getNamedSchemas,
|
|
12
17
|
getButtonSchemas,
|
|
13
18
|
hasFormSchema,
|
|
14
19
|
getFormSchemas,
|
|
15
20
|
getViewSchema,
|
|
16
|
-
getViewPath
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
isObjectSource,
|
|
20
|
-
isListSource
|
|
21
|
-
} from '../utils/schema.js'
|
|
21
|
+
getViewPath
|
|
22
|
+
} from '../utils/schema/lookup.js'
|
|
23
|
+
import { updateOrder } from '../utils/schema/data.js'
|
|
22
24
|
import {
|
|
23
25
|
isObject,
|
|
24
26
|
isString,
|
|
@@ -28,7 +30,6 @@ import {
|
|
|
28
30
|
parseDataPath,
|
|
29
31
|
normalizeDataPath
|
|
30
32
|
} from '@ditojs/utils'
|
|
31
|
-
import { raw } from '@ditojs/ui'
|
|
32
33
|
|
|
33
34
|
// @vue/component
|
|
34
35
|
export default {
|
|
@@ -46,8 +47,7 @@ export default {
|
|
|
46
47
|
|
|
47
48
|
data() {
|
|
48
49
|
return {
|
|
49
|
-
wrappedPrimitives: null
|
|
50
|
-
unwrappingPrimitives: raw(false)
|
|
50
|
+
wrappedPrimitives: null
|
|
51
51
|
}
|
|
52
52
|
},
|
|
53
53
|
|
|
@@ -90,39 +90,27 @@ export default {
|
|
|
90
90
|
return this.schema.wrapPrimitives
|
|
91
91
|
},
|
|
92
92
|
|
|
93
|
+
valueListData() {
|
|
94
|
+
// The list of items held by `value`, before wrapping primitives.
|
|
95
|
+
const { value } = this
|
|
96
|
+
return this.isObjectSource
|
|
97
|
+
? value != null
|
|
98
|
+
? [value]
|
|
99
|
+
: []
|
|
100
|
+
: (this.isListResults(value) ? value.results : value) || []
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
primitiveValues() {
|
|
104
|
+
// The values of `wrapPrimitives` lists, copied so that changes of their
|
|
105
|
+
// entries are tracked too, see the `primitiveValues` watcher.
|
|
106
|
+
return this.wrapPrimitives ? [...this.valueListData] : null
|
|
107
|
+
},
|
|
108
|
+
|
|
93
109
|
listData: {
|
|
94
110
|
get() {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
data = data != null ? [data] : []
|
|
99
|
-
} else {
|
|
100
|
-
// If data gets inherited from parent, unwrapping is not happening
|
|
101
|
-
// at the root in `setData()`, but here instead.
|
|
102
|
-
data = this.unwrapListData(data) || data
|
|
103
|
-
}
|
|
104
|
-
data ||= []
|
|
105
|
-
const { wrapPrimitives } = this
|
|
106
|
-
if (wrapPrimitives) {
|
|
107
|
-
if (this.unwrappingPrimitives.value) {
|
|
108
|
-
// We're done unwrapping once `listData` is reevaluated, so set
|
|
109
|
-
// this to `false` again. See `wrappedPrimitives` watcher above.
|
|
110
|
-
// TODO: Fix side-effects
|
|
111
|
-
// eslint-disable-next-line max-len
|
|
112
|
-
// eslint-disable-next-line vue/no-side-effects-in-computed-properties
|
|
113
|
-
this.unwrappingPrimitives.value = false
|
|
114
|
-
} else {
|
|
115
|
-
// Convert data to a list of wrapped primitives, and return it.
|
|
116
|
-
// TODO: Fix side-effects
|
|
117
|
-
// eslint-disable-next-line max-len
|
|
118
|
-
// eslint-disable-next-line vue/no-side-effects-in-computed-properties
|
|
119
|
-
this.wrappedPrimitives = data.map(value => ({
|
|
120
|
-
[wrapPrimitives]: value
|
|
121
|
-
}))
|
|
122
|
-
}
|
|
123
|
-
return this.wrappedPrimitives
|
|
124
|
-
}
|
|
125
|
-
return data
|
|
111
|
+
return this.wrapPrimitives
|
|
112
|
+
? this.wrappedPrimitives || []
|
|
113
|
+
: this.valueListData
|
|
126
114
|
},
|
|
127
115
|
|
|
128
116
|
set(data) {
|
|
@@ -133,7 +121,7 @@ export default {
|
|
|
133
121
|
? data?.length > 0
|
|
134
122
|
? data[0]
|
|
135
123
|
: null
|
|
136
|
-
: data
|
|
124
|
+
: updateOrder(this.sourceSchema, data, this.paginationRange)
|
|
137
125
|
}
|
|
138
126
|
}
|
|
139
127
|
},
|
|
@@ -289,6 +277,11 @@ export default {
|
|
|
289
277
|
default: null
|
|
290
278
|
}),
|
|
291
279
|
|
|
280
|
+
hasItems() {
|
|
281
|
+
// Object sources only have an item to edit or delete once they're set.
|
|
282
|
+
return this.isListSource || !!this.value
|
|
283
|
+
},
|
|
284
|
+
|
|
292
285
|
creatable: getSchemaAccessor('creatable', {
|
|
293
286
|
type: Boolean,
|
|
294
287
|
default: false,
|
|
@@ -305,13 +298,16 @@ export default {
|
|
|
305
298
|
type: Boolean,
|
|
306
299
|
default: false,
|
|
307
300
|
get(editable) {
|
|
308
|
-
return editable && !this.isInlined
|
|
301
|
+
return editable && !this.isInlined && this.hasItems
|
|
309
302
|
}
|
|
310
303
|
}),
|
|
311
304
|
|
|
312
305
|
deletable: getSchemaAccessor('deletable', {
|
|
313
306
|
type: Boolean,
|
|
314
|
-
default: false
|
|
307
|
+
default: false,
|
|
308
|
+
get(deletable) {
|
|
309
|
+
return deletable && this.hasItems
|
|
310
|
+
}
|
|
315
311
|
}),
|
|
316
312
|
|
|
317
313
|
draggable: getSchemaAccessor('draggable', {
|
|
@@ -382,18 +378,38 @@ export default {
|
|
|
382
378
|
}
|
|
383
379
|
},
|
|
384
380
|
|
|
381
|
+
value: {
|
|
382
|
+
immediate: true,
|
|
383
|
+
handler(value) {
|
|
384
|
+
// If data gets inherited from parent, unwrapping `{ results, total }`
|
|
385
|
+
// isn't happening at the root in `setData()`, but here instead.
|
|
386
|
+
if (this.isListResults(value)) {
|
|
387
|
+
this.unwrapListData(value)
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
},
|
|
391
|
+
|
|
392
|
+
primitiveValues: {
|
|
393
|
+
immediate: true,
|
|
394
|
+
handler(values) {
|
|
395
|
+
// Wrap the primitive values in objects for the list's forms to edit,
|
|
396
|
+
// unless they're the values that were just unwrapped from them.
|
|
397
|
+
if (values && !equals(values, this.unwrapPrimitives())) {
|
|
398
|
+
const { wrapPrimitives } = this
|
|
399
|
+
this.wrappedPrimitives = values.map(value => ({
|
|
400
|
+
[wrapPrimitives]: value
|
|
401
|
+
}))
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
},
|
|
405
|
+
|
|
385
406
|
wrappedPrimitives: {
|
|
386
407
|
deep: true,
|
|
387
|
-
handler(
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
if (
|
|
391
|
-
|
|
392
|
-
// array of primitives, in a primitive way :)
|
|
393
|
-
// But set `unwrappingPrimitives` to true, so the `listData` computed
|
|
394
|
-
// property knows about it, which sets it to `false` again.
|
|
395
|
-
this.unwrappingPrimitives.value = true
|
|
396
|
-
this.value = to.map(object => object[wrapPrimitives])
|
|
408
|
+
handler() {
|
|
409
|
+
// Map edits of the wrapped primitives back to the primitive values.
|
|
410
|
+
const values = this.unwrapPrimitives()
|
|
411
|
+
if (values && !equals(values, this.primitiveValues)) {
|
|
412
|
+
this.value = values
|
|
397
413
|
}
|
|
398
414
|
}
|
|
399
415
|
}
|
|
@@ -418,11 +434,9 @@ export default {
|
|
|
418
434
|
// Support two formats for list data:
|
|
419
435
|
// - Array: `[...]`
|
|
420
436
|
// - Object: `{ results: [...], total }`, see `unwrapListData()`
|
|
421
|
-
if (
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
this.isObjectSource && isObject(data)
|
|
425
|
-
) {
|
|
437
|
+
if (this.isListSource && isArray(data)) {
|
|
438
|
+
this.setLoadedListItems(data)
|
|
439
|
+
} else if (!data || this.isObjectSource && isObject(data)) {
|
|
426
440
|
this.value = data
|
|
427
441
|
} else if (this.unwrapListData(data)) {
|
|
428
442
|
// The format didn't match, see if we received a `{ results, total }`
|
|
@@ -431,33 +445,60 @@ export default {
|
|
|
431
445
|
} else if (isObject(data) && this.isInView) {
|
|
432
446
|
// The controller is sending data for a full multi-component view,
|
|
433
447
|
// including the nested list data.
|
|
434
|
-
this.viewComponent.setData(data)
|
|
448
|
+
return this.viewComponent.setData(data)
|
|
435
449
|
}
|
|
450
|
+
return this.value
|
|
436
451
|
},
|
|
437
452
|
|
|
438
|
-
|
|
439
|
-
|
|
453
|
+
isListResults(data) {
|
|
454
|
+
// Lists can also be loaded as `{ results, total }`, see `setData()`.
|
|
455
|
+
return (
|
|
440
456
|
this.isListSource &&
|
|
441
457
|
isObject(data) &&
|
|
442
458
|
isNumber(data.total) &&
|
|
443
459
|
isArray(data.results)
|
|
444
|
-
)
|
|
460
|
+
)
|
|
461
|
+
},
|
|
462
|
+
|
|
463
|
+
unwrapPrimitives() {
|
|
464
|
+
const { wrapPrimitives, wrappedPrimitives } = this
|
|
465
|
+
return wrapPrimitives && wrappedPrimitives
|
|
466
|
+
? wrappedPrimitives.map(object => object[wrapPrimitives])
|
|
467
|
+
: null
|
|
468
|
+
},
|
|
469
|
+
|
|
470
|
+
unwrapListData(data) {
|
|
471
|
+
if (this.isListResults(data)) {
|
|
445
472
|
// If @ditojs/server sends data in the form of `{ results, total }`
|
|
446
473
|
// replace the value with result, but remember the total in the store.
|
|
447
474
|
this.total = data.total
|
|
448
|
-
this.
|
|
475
|
+
this.setLoadedListItems(data.results)
|
|
449
476
|
return this.value
|
|
450
477
|
}
|
|
451
478
|
},
|
|
452
479
|
|
|
480
|
+
// Sets the list items loaded through the source's resource, numbered by
|
|
481
|
+
// their order key with the offset of the loaded page, so that their order
|
|
482
|
+
// can be stored even if it never changes, e.g. by a button that saves the
|
|
483
|
+
// current order. Lists in the data of forms, views and dialogs are
|
|
484
|
+
// numbered by `FormModel`, see `initializeData()`.
|
|
485
|
+
setLoadedListItems(items) {
|
|
486
|
+
this.value = updateOrder(this.sourceSchema, items, this.paginationRange)
|
|
487
|
+
},
|
|
488
|
+
|
|
453
489
|
createItem(schema, type, index = null) {
|
|
454
490
|
const item = this.createData(schema, type)
|
|
455
491
|
if (this.isObjectSource) {
|
|
456
492
|
this.objectData = item
|
|
457
|
-
} else if (index != null) {
|
|
458
|
-
this.listData.splice(index, 0, item)
|
|
459
493
|
} else {
|
|
460
|
-
this
|
|
494
|
+
const { listData } = this
|
|
495
|
+
if (index != null) {
|
|
496
|
+
listData.splice(index, 0, item)
|
|
497
|
+
} else {
|
|
498
|
+
listData.push(item)
|
|
499
|
+
}
|
|
500
|
+
// Set the list back, as `listData` is a new array for `null` values.
|
|
501
|
+
this.listData = listData
|
|
461
502
|
}
|
|
462
503
|
if (this.collapsible) {
|
|
463
504
|
this.$nextTick(() => this.openSchemaComponent(index ?? -1))
|
|
@@ -475,6 +516,8 @@ export default {
|
|
|
475
516
|
const { listData } = this
|
|
476
517
|
if (index >= 0) {
|
|
477
518
|
listData.splice(index, 1)
|
|
519
|
+
// Set the list back, to update the order of the remaining items.
|
|
520
|
+
this.listData = listData
|
|
478
521
|
removed = true
|
|
479
522
|
}
|
|
480
523
|
}
|
|
@@ -556,31 +599,34 @@ export default {
|
|
|
556
599
|
}
|
|
557
600
|
},
|
|
558
601
|
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
602
|
+
// See `DitoSchema.navigateToComponent()`, and `shouldRevealInlinedOnly`
|
|
603
|
+
// there.
|
|
604
|
+
async navigateToComponent(
|
|
605
|
+
dataPath,
|
|
606
|
+
onComplete,
|
|
607
|
+
{ shouldRevealInlinedOnly = false } = {}
|
|
608
|
+
) {
|
|
609
|
+
const index = dataPath.startsWith(this.dataPath)
|
|
610
|
+
? this.isListSource
|
|
611
|
+
? parseDataPath(dataPath.slice(this.dataPath.length + 1))[0] ?? null
|
|
612
|
+
: 0
|
|
613
|
+
: null
|
|
614
|
+
if (index !== null && isNumber(+index)) {
|
|
615
|
+
// Inlined items reveal the component themselves, opening collapsed
|
|
616
|
+
// items and sections, see `DitoSchema.navigateToComponent()`.
|
|
617
|
+
const schemaComponent = this.getSchemaComponent(+index)
|
|
618
|
+
if (
|
|
619
|
+
await schemaComponent?.navigateToComponent(dataPath, onComplete, {
|
|
620
|
+
shouldRevealInlinedOnly
|
|
621
|
+
})
|
|
622
|
+
) {
|
|
623
|
+
return true
|
|
581
624
|
}
|
|
582
625
|
}
|
|
583
|
-
return
|
|
626
|
+
return (
|
|
627
|
+
!shouldRevealInlinedOnly &&
|
|
628
|
+
this.navigateToRouteComponent(dataPath, onComplete)
|
|
629
|
+
)
|
|
584
630
|
},
|
|
585
631
|
|
|
586
632
|
navigateToRouteComponent(dataPath, onComplete) {
|
|
@@ -637,7 +683,7 @@ export default {
|
|
|
637
683
|
flatten = false,
|
|
638
684
|
process = null
|
|
639
685
|
) {
|
|
640
|
-
|
|
686
|
+
setupRouteSchema(api, schema, name)
|
|
641
687
|
const inlined = isInlined(schema)
|
|
642
688
|
if (inlined && schema.resource) {
|
|
643
689
|
throw new Error(
|
|
@@ -666,7 +712,7 @@ export default {
|
|
|
666
712
|
nested,
|
|
667
713
|
param
|
|
668
714
|
}
|
|
669
|
-
const childRoutes = await
|
|
715
|
+
const childRoutes = await setupForms(api, schema, level)
|
|
670
716
|
if (process) {
|
|
671
717
|
await process(childRoutes, level + 1)
|
|
672
718
|
}
|
|
@@ -695,8 +741,9 @@ export default {
|
|
|
695
741
|
path: getPathWithParam(sourcePath, param)
|
|
696
742
|
})
|
|
697
743
|
}
|
|
698
|
-
if (sourcePath) {
|
|
699
|
-
// Just redirect back to the parent when a nested
|
|
744
|
+
if (sourcePath && isListSource(schema)) {
|
|
745
|
+
// Just redirect back to the parent when a nested list route is hit.
|
|
746
|
+
// Object sources use this path for their form route instead.
|
|
700
747
|
routes.push({
|
|
701
748
|
path: sourcePath,
|
|
702
749
|
redirect: '.',
|
package/src/mixins/TextMixin.js
CHANGED
package/src/mixins/TypeMixin.js
CHANGED
|
@@ -2,7 +2,8 @@ import ValueMixin from './ValueMixin.js'
|
|
|
2
2
|
import ContextMixin from './ContextMixin.js'
|
|
3
3
|
import ValidationMixin from './ValidationMixin.js'
|
|
4
4
|
import { getSchemaAccessor } from '../utils/accessor.js'
|
|
5
|
-
import {
|
|
5
|
+
import { getValidations } from '../utils/schema/validation.js'
|
|
6
|
+
import { asArray, camelize, equals } from '@ditojs/utils'
|
|
6
7
|
|
|
7
8
|
// @vue/component
|
|
8
9
|
export default {
|
|
@@ -131,7 +132,8 @@ export default {
|
|
|
131
132
|
|
|
132
133
|
const attributes = {
|
|
133
134
|
...this.events,
|
|
134
|
-
|
|
135
|
+
// Only text fields support `readonly`, disable all other components.
|
|
136
|
+
disabled: this.disabled || (this.readonly && !textField)
|
|
135
137
|
}
|
|
136
138
|
|
|
137
139
|
if (nativeField) {
|
|
@@ -152,19 +154,7 @@ export default {
|
|
|
152
154
|
},
|
|
153
155
|
|
|
154
156
|
validations() {
|
|
155
|
-
|
|
156
|
-
if (this.required) {
|
|
157
|
-
validations.required = true
|
|
158
|
-
}
|
|
159
|
-
// Allow schema to override default rules and add any new ones:
|
|
160
|
-
for (const [key, value] of Object.entries(this.schema.rules || {})) {
|
|
161
|
-
if (value === undefined) {
|
|
162
|
-
delete validations[key]
|
|
163
|
-
} else {
|
|
164
|
-
validations[key] = value
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
return validations
|
|
157
|
+
return getValidations(this.schema, this.context)
|
|
168
158
|
},
|
|
169
159
|
|
|
170
160
|
showClearButton() {
|
|
@@ -172,6 +162,25 @@ export default {
|
|
|
172
162
|
}
|
|
173
163
|
},
|
|
174
164
|
|
|
165
|
+
watch: {
|
|
166
|
+
value(value) {
|
|
167
|
+
// Displayed errors refer to the value that they were added for, see
|
|
168
|
+
// `addError()`, so they're cleared when the value changes, whether
|
|
169
|
+
// through input or by code, e.g. by a button that generates a value.
|
|
170
|
+
// Errors that were added for the new value already, e.g. by `validate()`
|
|
171
|
+
// on blur right after the change, are kept.
|
|
172
|
+
if (this.hasErrors && !equals(value, this.valueWithErrors)) {
|
|
173
|
+
this.clearErrors()
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
|
|
177
|
+
componentPath(componentPath, oldComponentPath) {
|
|
178
|
+
// Component paths change when list items move, see `_registerEntry()`.
|
|
179
|
+
this._registerWithSchemaComponent(false, oldComponentPath)
|
|
180
|
+
this._registerWithSchemaComponent(true, componentPath)
|
|
181
|
+
}
|
|
182
|
+
},
|
|
183
|
+
|
|
175
184
|
created() {
|
|
176
185
|
this._register(true)
|
|
177
186
|
this.setupSchemaFields()
|
|
@@ -185,9 +194,20 @@ export default {
|
|
|
185
194
|
_register(add) {
|
|
186
195
|
// Provide component to container for schema accessor evaluation.
|
|
187
196
|
this.$emit('update:component', add ? this : null)
|
|
188
|
-
|
|
197
|
+
this._registerWithSchemaComponent(add, this.componentPath)
|
|
198
|
+
},
|
|
199
|
+
|
|
200
|
+
_registerWithSchemaComponent(add, componentPath) {
|
|
201
|
+
// Register unnested type components separately, to prevent them from
|
|
202
|
+
// overriding parent data paths.
|
|
189
203
|
if (this.nested) {
|
|
190
|
-
this.schemaComponent._registerComponent(this, add)
|
|
204
|
+
this.schemaComponent._registerComponent(this, add, componentPath)
|
|
205
|
+
} else {
|
|
206
|
+
this.schemaComponent?._registerUnnestedComponent(
|
|
207
|
+
this,
|
|
208
|
+
add,
|
|
209
|
+
componentPath
|
|
210
|
+
)
|
|
191
211
|
}
|
|
192
212
|
},
|
|
193
213
|
|
|
@@ -197,11 +217,6 @@ export default {
|
|
|
197
217
|
return { onFocus, onBlur, onInput, onChange }
|
|
198
218
|
},
|
|
199
219
|
|
|
200
|
-
// @overridable
|
|
201
|
-
getValidations() {
|
|
202
|
-
return null
|
|
203
|
-
},
|
|
204
|
-
|
|
205
220
|
// @overridable
|
|
206
221
|
async scrollIntoView() {
|
|
207
222
|
await this.focusSchema()
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import * as validators from '../validators/index.js'
|
|
1
|
+
import { getValueValidationErrors } from '../utils/schema/validation.js'
|
|
3
2
|
|
|
4
3
|
// @vue/component
|
|
5
4
|
export default {
|
|
@@ -11,7 +10,9 @@ export default {
|
|
|
11
10
|
isDirty: false,
|
|
12
11
|
isValidated: false,
|
|
13
12
|
isValid: false,
|
|
14
|
-
errors: null
|
|
13
|
+
errors: null,
|
|
14
|
+
// The value that the displayed errors were added for, see `addError()`.
|
|
15
|
+
valueWithErrors: undefined
|
|
15
16
|
}
|
|
16
17
|
},
|
|
17
18
|
|
|
@@ -31,33 +32,13 @@ export default {
|
|
|
31
32
|
},
|
|
32
33
|
|
|
33
34
|
validate(notify = true) {
|
|
34
|
-
|
|
35
|
+
const errors = getValueValidationErrors(this.value, this.validations)
|
|
36
|
+
const isValid = errors.length === 0
|
|
35
37
|
if (notify) {
|
|
36
38
|
this.clearErrors()
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
for (const [rule, setting] of Object.entries(this.validations)) {
|
|
40
|
-
// eslint-disable-next-line import/namespace
|
|
41
|
-
const validator = validators[rule]
|
|
42
|
-
if (
|
|
43
|
-
validator &&
|
|
44
|
-
// Only apply 'required' validator to empty values.
|
|
45
|
-
// Apply all other validators only to non-empty values.
|
|
46
|
-
(rule === 'required' || value != null && value !== '')
|
|
47
|
-
) {
|
|
48
|
-
const { validate, message } = validator
|
|
49
|
-
if (!validate(value, setting, this.validations)) {
|
|
50
|
-
isValid = false
|
|
51
|
-
if (notify) {
|
|
52
|
-
const error = isFunction(message)
|
|
53
|
-
? message(value, setting, this)
|
|
54
|
-
: message
|
|
55
|
-
this.addError(error, true)
|
|
56
|
-
}
|
|
57
|
-
}
|
|
39
|
+
for (const { message } of errors) {
|
|
40
|
+
this.addError(message, true)
|
|
58
41
|
}
|
|
59
|
-
}
|
|
60
|
-
if (notify) {
|
|
61
42
|
this.isValidated = true
|
|
62
43
|
this.isValid = isValid
|
|
63
44
|
}
|
|
@@ -84,6 +65,7 @@ export default {
|
|
|
84
65
|
|
|
85
66
|
addError(error, addLabel = false) {
|
|
86
67
|
this.errors ||= []
|
|
68
|
+
this.valueWithErrors = this.value
|
|
87
69
|
if (addLabel) {
|
|
88
70
|
const label = this.label || this.placeholder || this.name
|
|
89
71
|
error = `The ${label} field ${error}.`
|
|
@@ -43,14 +43,12 @@ export default {
|
|
|
43
43
|
this.schemaComponents.forEach(it => it.clearErrors())
|
|
44
44
|
},
|
|
45
45
|
|
|
46
|
-
showValidationErrors(errors, focus, first = true) {
|
|
47
|
-
this.schemaComponents
|
|
48
|
-
schemaComponent
|
|
49
|
-
|
|
50
|
-
first = false
|
|
51
|
-
}
|
|
46
|
+
async showValidationErrors(errors, focus, first = true) {
|
|
47
|
+
for (const schemaComponent of this.schemaComponents) {
|
|
48
|
+
if (await schemaComponent.showValidationErrors(errors, focus, first)) {
|
|
49
|
+
first = false
|
|
52
50
|
}
|
|
53
|
-
|
|
51
|
+
}
|
|
54
52
|
return !first
|
|
55
53
|
}
|
|
56
54
|
}
|
package/src/mixins/ValueMixin.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { isFunction } from '@ditojs/utils'
|
|
1
2
|
import DitoContext from '../DitoContext.js'
|
|
2
|
-
import { computeValue } from '../utils/schema.js'
|
|
3
|
+
import { computeValue } from '../utils/schema/data.js'
|
|
3
4
|
|
|
4
5
|
export default {
|
|
5
6
|
computed: {
|
|
@@ -12,8 +13,10 @@ export default {
|
|
|
12
13
|
this.dataPath,
|
|
13
14
|
{ component: this }
|
|
14
15
|
)
|
|
16
|
+
// Only call `format()` if it's a function, as some types use `format`
|
|
17
|
+
// for other purposes, e.g. `DitoTypeColor` for the color format.
|
|
15
18
|
const { format } = this.schema
|
|
16
|
-
return format
|
|
19
|
+
return isFunction(format)
|
|
17
20
|
? format(new DitoContext(this, { value }))
|
|
18
21
|
: value
|
|
19
22
|
},
|