@ditojs/admin 3.0.0 → 3.1.1
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.es.js +2043 -1956
- package/dist/dito-admin.umd.js +5 -6
- package/package.json +8 -6
- package/src/DitoAdmin.js +1 -65
- package/src/DitoTypeComponent.js +17 -0
- package/src/components/DitoButtons.vue +14 -1
- package/src/components/DitoDialog.vue +1 -1
- package/src/components/DitoForm.vue +3 -8
- package/src/components/DitoPanel.vue +3 -2
- package/src/components/DitoRoot.vue +1 -8
- package/src/components/DitoRouterView.vue +27 -0
- package/src/components/DitoView.vue +2 -7
- package/src/components/index.js +1 -0
- package/src/mixins/DitoMixin.js +28 -17
- package/src/mixins/OptionsMixin.js +14 -4
- package/src/mixins/ResourceMixin.js +4 -2
- package/src/mixins/RouteMixin.js +1 -1
- package/src/mixins/SourceMixin.js +13 -1
- package/src/types/DitoTypeMultiselect.vue +23 -12
- package/src/types/DitoTypePanel.vue +2 -1
- package/src/types/DitoTypeTreeList.vue +5 -11
- package/src/utils/DataSchemaResolver.js +13 -0
- package/src/utils/FormModel.js +91 -32
- package/src/utils/request.js +70 -0
- package/src/utils/schema/data.js +102 -37
- package/src/utils/schema/lookup.js +9 -25
- package/src/utils/schema/validation.js +2 -4
- package/types/index.d.ts +25 -0
- package/src/utils/DataSchemaResolver.test.js +0 -132
- package/src/utils/FormModel.test.js +0 -808
- package/src/utils/data.test.js +0 -28
- package/src/utils/schema/data.test.js +0 -471
- package/src/utils/schema/lookup.test.js +0 -61
- package/src/utils/schema/structure.test.js +0 -57
- package/src/utils/schema/validation.test.js +0 -395
- package/src/utils/units.test.js +0 -71
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ditojs/admin",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.1.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Dito.js Admin is a schema based admin interface for Dito.js Server, featuring auto-generated views and forms and built with Vue.js",
|
|
6
6
|
"repository": "https://github.com/ditojs/dito/tree/main/packages/admin",
|
|
@@ -22,7 +22,9 @@
|
|
|
22
22
|
"files": [
|
|
23
23
|
"src/",
|
|
24
24
|
"dist/",
|
|
25
|
-
"types/"
|
|
25
|
+
"types/",
|
|
26
|
+
"!src/**/*.test.js",
|
|
27
|
+
"!src/**/__snapshots__/"
|
|
26
28
|
],
|
|
27
29
|
"scripts": {
|
|
28
30
|
"build": "vite build",
|
|
@@ -39,8 +41,8 @@
|
|
|
39
41
|
"not ie_mob > 0"
|
|
40
42
|
],
|
|
41
43
|
"dependencies": {
|
|
42
|
-
"@ditojs/ui": "^3.
|
|
43
|
-
"@ditojs/utils": "^3.
|
|
44
|
+
"@ditojs/ui": "^3.1.1",
|
|
45
|
+
"@ditojs/utils": "^3.1.1"
|
|
44
46
|
},
|
|
45
47
|
"peerDependencies": {
|
|
46
48
|
"@kyvg/vue3-notification": "^3.4.2",
|
|
@@ -85,7 +87,7 @@
|
|
|
85
87
|
"vue-upload-component": "^3.2.2"
|
|
86
88
|
},
|
|
87
89
|
"devDependencies": {
|
|
88
|
-
"@ditojs/build": "^3.
|
|
90
|
+
"@ditojs/build": "^3.1.1",
|
|
89
91
|
"@kyvg/vue3-notification": "^3.4.2",
|
|
90
92
|
"@tiptap/core": "^3.31.4",
|
|
91
93
|
"@tiptap/extension-blockquote": "^3.31.4",
|
|
@@ -129,5 +131,5 @@
|
|
|
129
131
|
"vue-router": "^5.3.1",
|
|
130
132
|
"vue-upload-component": "^3.2.2"
|
|
131
133
|
},
|
|
132
|
-
"gitHead": "
|
|
134
|
+
"gitHead": "acce85bb38d817aab18f9d5d03b3aa9dff00c3f9"
|
|
133
135
|
}
|
package/src/DitoAdmin.js
CHANGED
|
@@ -3,7 +3,6 @@ import { createRouter, createWebHistory } from 'vue-router'
|
|
|
3
3
|
import VueNotifications from '@kyvg/vue3-notification'
|
|
4
4
|
import {
|
|
5
5
|
isString,
|
|
6
|
-
isAbsoluteUrl,
|
|
7
6
|
assignDeeply,
|
|
8
7
|
hyphenate,
|
|
9
8
|
camelize,
|
|
@@ -15,7 +14,7 @@ import DitoRoot from './components/DitoRoot.vue'
|
|
|
15
14
|
import DitoTypeComponent from './DitoTypeComponent.js'
|
|
16
15
|
import ResizeDirective from './directives/resize.js'
|
|
17
16
|
import { getResource } from './utils/resource.js'
|
|
18
|
-
import {
|
|
17
|
+
import { request, getApiUrl, isApiUrl } from './utils/request.js'
|
|
19
18
|
import verbs from './verbs.js'
|
|
20
19
|
|
|
21
20
|
export default class DitoAdmin {
|
|
@@ -228,66 +227,3 @@ export default class DitoAdmin {
|
|
|
228
227
|
return DitoTypeComponent.register(type, options)
|
|
229
228
|
}
|
|
230
229
|
}
|
|
231
|
-
|
|
232
|
-
class RequestError extends Error {
|
|
233
|
-
constructor(response) {
|
|
234
|
-
super(
|
|
235
|
-
`Request failed with status code: ${response.status} (${
|
|
236
|
-
response.statusText
|
|
237
|
-
})`
|
|
238
|
-
)
|
|
239
|
-
this.response = response
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
async function request(api, {
|
|
244
|
-
url,
|
|
245
|
-
method = 'get',
|
|
246
|
-
query = null,
|
|
247
|
-
headers = null,
|
|
248
|
-
data = null,
|
|
249
|
-
signal = null
|
|
250
|
-
}) {
|
|
251
|
-
const isApiUrl = api.isApiUrl(url)
|
|
252
|
-
|
|
253
|
-
const response = await fetch(api.getApiUrl({ url, query }), {
|
|
254
|
-
method: method.toUpperCase(),
|
|
255
|
-
...(data && { body: JSON.stringify(data) }),
|
|
256
|
-
headers: {
|
|
257
|
-
...(isApiUrl && api.headers),
|
|
258
|
-
...headers
|
|
259
|
-
},
|
|
260
|
-
credentials:
|
|
261
|
-
isApiUrl && api.cors?.credentials
|
|
262
|
-
? 'include'
|
|
263
|
-
: 'same-origin',
|
|
264
|
-
signal
|
|
265
|
-
})
|
|
266
|
-
|
|
267
|
-
if (response.headers.get('Content-Type')?.includes('application/json')) {
|
|
268
|
-
response.data = await response.json()
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
if (!response.ok) {
|
|
272
|
-
throw new RequestError(response)
|
|
273
|
-
}
|
|
274
|
-
return response
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
function isApiUrl(api, url) {
|
|
278
|
-
return !isAbsoluteUrl(url) || url.startsWith(api.url)
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
function getApiUrl(api, { url, query }) {
|
|
282
|
-
if (!url.startsWith(api.url) && !isAbsoluteUrl(url)) {
|
|
283
|
-
url = combineUrls(api.url, url)
|
|
284
|
-
}
|
|
285
|
-
// Support optional query parameters, to be are added to the URL.
|
|
286
|
-
const search = formatQuery(query)
|
|
287
|
-
return search ? `${url}${url.includes('?') ? '&' : '?'}${search}` : url
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
function combineUrls(baseUrl, relativeUrl) {
|
|
291
|
-
// Use same approach as axios `combineURLs()` to join baseUrl & relativeUrl:
|
|
292
|
-
return `${baseUrl.replace(/\/+$/, '')}/${relativeUrl.replace(/^\/+/, '')}`
|
|
293
|
-
}
|
package/src/DitoTypeComponent.js
CHANGED
|
@@ -35,6 +35,15 @@ export default {
|
|
|
35
35
|
|
|
36
36
|
register(types, definition = {}) {
|
|
37
37
|
types = asArray(types)
|
|
38
|
+
if (hasValidationsMethod(definition)) {
|
|
39
|
+
// Validation doesn't depend on components anymore, so that it also
|
|
40
|
+
// covers fields that aren't rendered, see `utils/schema/validation.js`.
|
|
41
|
+
console.warn(
|
|
42
|
+
`Type '${types[0]}': The \`getValidations()\` method isn't supported ` +
|
|
43
|
+
`anymore, use the static \`getTypeValidations(schema, context)\` ` +
|
|
44
|
+
`option instead.`
|
|
45
|
+
)
|
|
46
|
+
}
|
|
38
47
|
const component = this.component(
|
|
39
48
|
`DitoType${camelize(types[0], true)}`,
|
|
40
49
|
definition
|
|
@@ -45,3 +54,11 @@ export default {
|
|
|
45
54
|
return component
|
|
46
55
|
}
|
|
47
56
|
}
|
|
57
|
+
|
|
58
|
+
// Returns whether the component definition or one of its mixins defines the
|
|
59
|
+
// `getValidations()` method that types used to override.
|
|
60
|
+
function hasValidationsMethod(definition) {
|
|
61
|
+
return [definition, ...(definition.mixins ?? [])].some(
|
|
62
|
+
({ methods }) => !!methods?.getValidations
|
|
63
|
+
)
|
|
64
|
+
}
|
|
@@ -48,10 +48,23 @@ export default DitoComponent.component('DitoButtons', {
|
|
|
48
48
|
meta: { type: Object, default: () => ({}) },
|
|
49
49
|
store: { type: Object, default: () => ({}) },
|
|
50
50
|
nested: { type: Boolean, default: true },
|
|
51
|
-
disabled: { type: Boolean, default: false }
|
|
51
|
+
disabled: { type: Boolean, default: false },
|
|
52
|
+
// The name of the group of buttons, e.g. `panelButtons`, see
|
|
53
|
+
// `componentPath`.
|
|
54
|
+
buttonsName: { type: String, default: 'buttons' }
|
|
52
55
|
},
|
|
53
56
|
|
|
54
57
|
computed: {
|
|
58
|
+
componentPath() {
|
|
59
|
+
// Add the name of the group, so that buttons don't share their component
|
|
60
|
+
// paths with components of the same name, e.g. a `publish` button with a
|
|
61
|
+
// `publish` field, as component paths are used for registries.
|
|
62
|
+
return appendDataPath(
|
|
63
|
+
this.continueParentComponentPath(),
|
|
64
|
+
`$${this.buttonsName}`
|
|
65
|
+
)
|
|
66
|
+
},
|
|
67
|
+
|
|
55
68
|
buttonSchemas() {
|
|
56
69
|
// Compute a buttons list which has the dataPath baked into its keys.
|
|
57
70
|
const { dataPath, buttons } = this
|
|
@@ -179,7 +179,7 @@ export default DitoComponent.component('DitoDialog', {
|
|
|
179
179
|
|
|
180
180
|
async submit() {
|
|
181
181
|
// Wait for computed values that depend on pending loads, e.g. options.
|
|
182
|
-
await this.formModel
|
|
182
|
+
await this.waitForPendingLoadsOf(this.formModel)
|
|
183
183
|
this.resolve(this.dialogData)
|
|
184
184
|
},
|
|
185
185
|
|
|
@@ -5,16 +5,11 @@
|
|
|
5
5
|
)
|
|
6
6
|
//- Only render a router-view here if this isn't the last data route and not a
|
|
7
7
|
//- nested form route, which will appear elsewhere in its own view.
|
|
8
|
-
|
|
8
|
+
DitoRouterView(
|
|
9
9
|
v-if="!isLastUnnestedRoute && !isNestedRoute"
|
|
10
10
|
v-show="!isActiveRoute"
|
|
11
|
-
|
|
11
|
+
:routeLevel="routeLevel + 1"
|
|
12
12
|
)
|
|
13
|
-
component(
|
|
14
|
-
v-if="Component"
|
|
15
|
-
:is="Component"
|
|
16
|
-
:key="getRouteComponentKey(route, routeLevel + 1)"
|
|
17
|
-
)
|
|
18
13
|
//- NOTE: Nested form components are kept alive by using `v-show` instead of
|
|
19
14
|
//- `v-if` here, so event handling and other things still work with nested
|
|
20
15
|
//- editing.
|
|
@@ -467,7 +462,7 @@ export default DitoComponent.component('DitoForm', {
|
|
|
467
462
|
|
|
468
463
|
async submit(button, { validate = true, closeForm = false } = {}) {
|
|
469
464
|
// Wait for computed values that depend on pending loads, e.g. options.
|
|
470
|
-
await this.formModel
|
|
465
|
+
await this.waitForPendingLoadsOf(this.formModel)
|
|
471
466
|
if (validate && !this.validateAll()) {
|
|
472
467
|
return false
|
|
473
468
|
}
|
|
@@ -25,6 +25,7 @@ component.dito-panel(
|
|
|
25
25
|
span {{ getLabel(schema) }}
|
|
26
26
|
DitoButtons.dito-buttons--small(
|
|
27
27
|
:buttons="panelButtonSchemas"
|
|
28
|
+
buttonsName="panelButtons"
|
|
28
29
|
:dataPath="panelDataPath"
|
|
29
30
|
:data="panelData"
|
|
30
31
|
:meta="meta"
|
|
@@ -70,7 +71,7 @@ export default DitoComponent.component('DitoPanel', {
|
|
|
70
71
|
store: { type: Object, required: true },
|
|
71
72
|
disabled: { type: Boolean, required: true },
|
|
72
73
|
panelTabComponent: { type: Object, default: null },
|
|
73
|
-
// The component path of the panel's entry, see `
|
|
74
|
+
// The component path of the panel's entry, see `getAllPanelEntries()`:
|
|
74
75
|
panelComponentPath: { type: String, required: true }
|
|
75
76
|
},
|
|
76
77
|
|
|
@@ -83,7 +84,7 @@ export default DitoComponent.component('DitoPanel', {
|
|
|
83
84
|
computed: {
|
|
84
85
|
nested() {
|
|
85
86
|
// For `ContextMixin`: The panel's address, `dataPath`, points into the
|
|
86
|
-
// item that it displays, see `
|
|
87
|
+
// item that it displays, see `getAllPanelEntries()`.
|
|
87
88
|
return true
|
|
88
89
|
},
|
|
89
90
|
|
|
@@ -28,14 +28,7 @@
|
|
|
28
28
|
:spinner="options.spinner"
|
|
29
29
|
:isLoading="isLoading"
|
|
30
30
|
)
|
|
31
|
-
|
|
32
|
-
v-slot="{ Component, route }"
|
|
33
|
-
)
|
|
34
|
-
component(
|
|
35
|
-
v-if="Component"
|
|
36
|
-
:is="Component"
|
|
37
|
-
:key="getRouteComponentKey(route, 0)"
|
|
38
|
-
)
|
|
31
|
+
DitoRouterView(:routeLevel="0")
|
|
39
32
|
DitoSidebar
|
|
40
33
|
DitoAccount(
|
|
41
34
|
v-if="user"
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import { h } from 'vue'
|
|
3
|
+
import { RouterView } from 'vue-router'
|
|
4
|
+
import DitoComponent from '../DitoComponent.js'
|
|
5
|
+
|
|
6
|
+
// Renders the route components of `routeLevel` through `RouterView`, keyed by
|
|
7
|
+
// their route record, so that each record gets its own component instance, as
|
|
8
|
+
// their schema and data belong to it, while the component is reused when only
|
|
9
|
+
// the parameters change, e.g. to edit another item.
|
|
10
|
+
// NOTE: This is a functional component, so that it doesn't become the parent
|
|
11
|
+
// component of the route components, see `DitoMixin.provide()`.
|
|
12
|
+
// @vue/component
|
|
13
|
+
function DitoRouterView({ routeLevel }) {
|
|
14
|
+
return h(RouterView, null, {
|
|
15
|
+
default: ({ Component, route }) =>
|
|
16
|
+
Component
|
|
17
|
+
? h(Component, { key: route.matched[routeLevel]?.path })
|
|
18
|
+
: null
|
|
19
|
+
})
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
DitoRouterView.props = {
|
|
23
|
+
routeLevel: { type: Number, required: true }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export default DitoComponent.component('DitoRouterView', DitoRouterView)
|
|
27
|
+
</script>
|
|
@@ -6,15 +6,10 @@ 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
|
-
component(
|
|
14
|
-
v-if="Component"
|
|
15
|
-
:is="Component"
|
|
16
|
-
:key="getRouteComponentKey(route, routeLevel + 1)"
|
|
17
|
-
)
|
|
18
13
|
.dito-view.dito-scroll-parent(
|
|
19
14
|
v-else
|
|
20
15
|
:data-resource="sourceSchema.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
|
@@ -107,18 +107,11 @@ export default {
|
|
|
107
107
|
// is used for registries and DOM ids. Components continue the path of
|
|
108
108
|
// their parent with the item indices that their data path adds to the
|
|
109
109
|
// parent's, e.g. inlined list items. `DitoContainer`, `DitoPane` and
|
|
110
|
-
// `DitoPanel` add the names of components, tabs and panels, `
|
|
111
|
-
// and `DitoTreeItem` continue the
|
|
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.
|
|
112
113
|
componentPath() {
|
|
113
|
-
|
|
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
|
|
114
|
+
return this.continueParentComponentPath()
|
|
122
115
|
},
|
|
123
116
|
|
|
124
117
|
// The data path that `componentPath` corresponds to, which children
|
|
@@ -478,12 +471,30 @@ export default {
|
|
|
478
471
|
body.removeChild(a)
|
|
479
472
|
},
|
|
480
473
|
|
|
481
|
-
// Returns the
|
|
482
|
-
//
|
|
483
|
-
//
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
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
|
+
}
|
|
487
498
|
},
|
|
488
499
|
|
|
489
500
|
notify(options) {
|
|
@@ -82,13 +82,17 @@ export default {
|
|
|
82
82
|
return this.getOptionForValue(this.selectedValue)
|
|
83
83
|
},
|
|
84
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
|
+
|
|
85
90
|
// The resolver of the options in the form model of the data, which loads
|
|
86
91
|
// them and shares them with the computes of the form. It is looked up with
|
|
87
92
|
// the entry of this component, in the shape of the entries of
|
|
88
93
|
// `processSchemaData()`, see `FormModel`.
|
|
89
94
|
optionsResolver() {
|
|
90
|
-
|
|
91
|
-
return formModel.getOptionsResolver({
|
|
95
|
+
return this.formModel.getOptionsResolver({
|
|
92
96
|
schema: this.schema,
|
|
93
97
|
data: this.data,
|
|
94
98
|
name: this.name,
|
|
@@ -243,10 +247,16 @@ export default {
|
|
|
243
247
|
|
|
244
248
|
// Writes the selected value back into the data, through the setter of
|
|
245
249
|
// `selectedValue`, which converts it to the value, e.g. a reference id to
|
|
246
|
-
// its option with `relate`.
|
|
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.
|
|
247
253
|
replaceValueWithSelectedValue() {
|
|
248
254
|
const { selectedValue } = this
|
|
249
|
-
this.
|
|
255
|
+
this.formModel
|
|
256
|
+
.applyCleanChanges(() => {
|
|
257
|
+
this.selectedValue = selectedValue
|
|
258
|
+
})
|
|
259
|
+
.catch(console.error)
|
|
250
260
|
},
|
|
251
261
|
|
|
252
262
|
// Groups the options by the `groupBy` key, see `groupByLabel` and
|
|
@@ -54,8 +54,10 @@ export default {
|
|
|
54
54
|
// `isTransient()` to only return `!this.providesData`.
|
|
55
55
|
const form = this.formComponent
|
|
56
56
|
return (
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
(
|
|
58
|
+
!this.providesData &&
|
|
59
|
+
!this.linksToView
|
|
60
|
+
) ||
|
|
59
61
|
form && (
|
|
60
62
|
form.isTransient ||
|
|
61
63
|
form.isCreating
|
package/src/mixins/RouteMixin.js
CHANGED
|
@@ -126,7 +126,7 @@ export default {
|
|
|
126
126
|
flush: 'sync',
|
|
127
127
|
handler(routeRecord) {
|
|
128
128
|
// Components belong to the record that they're rendered for, see
|
|
129
|
-
// `
|
|
129
|
+
// `DitoRouterView`. Records of other paths, or none, mean that
|
|
130
130
|
// the component is being left, and it keeps its record until it's
|
|
131
131
|
// unmounted, as its schema and meta are still read in the meantime,
|
|
132
132
|
// e.g. by the watchers of its form model. Records of the same path
|
|
@@ -781,10 +781,22 @@ export default {
|
|
|
781
781
|
|
|
782
782
|
processValue({ schema, value, dataPath }, graph) {
|
|
783
783
|
graph.addSource(dataPath, schema)
|
|
784
|
-
|
|
784
|
+
// `SchemaGraph.process()` changes the ids of the items in the processed
|
|
785
|
+
// data. The items of sources with forms are processed into copies, but the
|
|
786
|
+
// ones of sources without forms are still the items of the data.
|
|
787
|
+
return hasFormSchema(schema) ? value : copyItems(value)
|
|
785
788
|
}
|
|
786
789
|
}
|
|
787
790
|
|
|
791
|
+
// Returns shallow copies of the items of a list, or of an object.
|
|
792
|
+
function copyItems(value) {
|
|
793
|
+
return isArray(value)
|
|
794
|
+
? value.map(item => (isObject(item) ? { ...item } : item))
|
|
795
|
+
: isObject(value)
|
|
796
|
+
? { ...value }
|
|
797
|
+
: value
|
|
798
|
+
}
|
|
799
|
+
|
|
788
800
|
function getPathWithParam(path, param) {
|
|
789
801
|
return param
|
|
790
802
|
? path
|
|
@@ -81,6 +81,8 @@ export default DitoTypeComponent.register('multiselect', {
|
|
|
81
81
|
return {
|
|
82
82
|
searchedOptions: null,
|
|
83
83
|
isLoadingSearchedOptions: false,
|
|
84
|
+
// The term of the current search, see `onSearchChange()`:
|
|
85
|
+
currentSearchTerm: null,
|
|
84
86
|
populate: false
|
|
85
87
|
}
|
|
86
88
|
},
|
|
@@ -229,34 +231,43 @@ export default DitoTypeComponent.register('multiselect', {
|
|
|
229
231
|
},
|
|
230
232
|
|
|
231
233
|
// Returns the options that `searchFilter()` returns for the search term,
|
|
232
|
-
//
|
|
234
|
+
// or `null` if it fails. Waits for the options, as the filter receives
|
|
235
|
+
// them, e.g. to search them when the search term is entered while they are
|
|
236
|
+
// still loading.
|
|
233
237
|
async loadSearchedOptions(searchTerm) {
|
|
234
|
-
// Use a timeout to allow already resolved promises to return options
|
|
235
|
-
// without showing a loading indicator.
|
|
236
|
-
const timer = setTimeout(() => {
|
|
237
|
-
this.isLoadingSearchedOptions = true
|
|
238
|
-
}, 0)
|
|
239
|
-
let options = null
|
|
240
238
|
try {
|
|
241
|
-
|
|
239
|
+
await this.optionsResolver.waitForValue()
|
|
240
|
+
return await this.searchFilter(new DitoContext(this, { searchTerm }))
|
|
242
241
|
} catch (error) {
|
|
243
242
|
this.addError(error.message || error)
|
|
243
|
+
return null
|
|
244
244
|
}
|
|
245
|
-
clearTimeout(timer)
|
|
246
|
-
this.isLoadingSearchedOptions = false
|
|
247
|
-
return options
|
|
248
245
|
},
|
|
249
246
|
|
|
250
247
|
async onSearchChange(searchTerm) {
|
|
251
248
|
if (this.searchFilter) {
|
|
249
|
+
this.currentSearchTerm = searchTerm
|
|
252
250
|
if (searchTerm) {
|
|
253
251
|
// Set `searchedOptions` to an empty array, before it will be
|
|
254
252
|
// populated asynchronously with the actual results.
|
|
255
253
|
this.searchedOptions = []
|
|
256
|
-
|
|
254
|
+
// Use a timeout to allow already resolved promises to return options
|
|
255
|
+
// without showing a loading indicator.
|
|
256
|
+
const timer = setTimeout(() => {
|
|
257
|
+
this.isLoadingSearchedOptions = true
|
|
258
|
+
}, 0)
|
|
259
|
+
const options = await this.loadSearchedOptions(searchTerm)
|
|
260
|
+
clearTimeout(timer)
|
|
261
|
+
// Searches that were replaced by newer ones in the meantime neither
|
|
262
|
+
// show their options nor end the loading state.
|
|
263
|
+
if (searchTerm === this.currentSearchTerm) {
|
|
264
|
+
this.searchedOptions = options
|
|
265
|
+
this.isLoadingSearchedOptions = false
|
|
266
|
+
}
|
|
257
267
|
} else {
|
|
258
268
|
// Clear `searchedOptions` when the query is cleared.
|
|
259
269
|
this.searchedOptions = null
|
|
270
|
+
this.isLoadingSearchedOptions = false
|
|
260
271
|
}
|
|
261
272
|
}
|
|
262
273
|
}
|
|
@@ -13,7 +13,8 @@ export default DitoTypeComponent.register('panel', {
|
|
|
13
13
|
getPanelSchema(api, schema) {
|
|
14
14
|
// For a TypePanel, the component schema is also the panel schema. Its name
|
|
15
15
|
// is added to the panel's data path and component path, see
|
|
16
|
-
// `
|
|
16
|
+
// `getAllPanelEntries()`, as unnested components don't add it to data
|
|
17
|
+
// paths.
|
|
17
18
|
return schema
|
|
18
19
|
},
|
|
19
20
|
|
|
@@ -18,14 +18,7 @@
|
|
|
18
18
|
v-if="editPath && hasEditableForms"
|
|
19
19
|
)
|
|
20
20
|
//- Include a router-view for the optional DitoFormInlined
|
|
21
|
-
|
|
22
|
-
v-slot="{ Component, route }"
|
|
23
|
-
)
|
|
24
|
-
component(
|
|
25
|
-
v-if="Component"
|
|
26
|
-
:is="Component"
|
|
27
|
-
:key="getRouteComponentKey(route, routeComponent.routeLevel + 1)"
|
|
28
|
-
)
|
|
21
|
+
DitoRouterView(:routeLevel="routeComponent.routeLevel + 1")
|
|
29
22
|
</template>
|
|
30
23
|
|
|
31
24
|
<script>
|
|
@@ -94,9 +87,10 @@ export default DitoTypeComponent.register(
|
|
|
94
87
|
type: Boolean,
|
|
95
88
|
default: false,
|
|
96
89
|
schema
|
|
97
|
-
}) ||
|
|
98
|
-
|
|
99
|
-
|
|
90
|
+
}) || (
|
|
91
|
+
schema.children &&
|
|
92
|
+
hasEditableForms(schema.children)
|
|
93
|
+
)
|
|
100
94
|
)
|
|
101
95
|
)
|
|
102
96
|
}
|
|
@@ -32,6 +32,8 @@ import {
|
|
|
32
32
|
export class DataSchemaResolver {
|
|
33
33
|
// The promise or loading function of the current load, see `startLoad()`:
|
|
34
34
|
currentPromiseOrLoader = null
|
|
35
|
+
// The promise of the current load, which resolves once it finished:
|
|
36
|
+
currentLoadPromise = null
|
|
35
37
|
// The promise or loading function of the last finished load, with its value
|
|
36
38
|
// or error, see `finishLoad()`:
|
|
37
39
|
lastFinishedLoad = shallowRef({
|
|
@@ -68,6 +70,16 @@ export class DataSchemaResolver {
|
|
|
68
70
|
)
|
|
69
71
|
}
|
|
70
72
|
|
|
73
|
+
// Returns the value, once the load of it finished if it is pending, also
|
|
74
|
+
// after loads that replace outdated ones, e.g. when the dependencies of the
|
|
75
|
+
// data schema change in the meantime. Returns `undefined` if loading fails.
|
|
76
|
+
async waitForValue() {
|
|
77
|
+
while (this.isLoading) {
|
|
78
|
+
await this.currentLoadPromise
|
|
79
|
+
}
|
|
80
|
+
return this.value
|
|
81
|
+
}
|
|
82
|
+
|
|
71
83
|
// The error of the last load, if it failed and isn't outdated:
|
|
72
84
|
get lastLoadError() {
|
|
73
85
|
const { promiseOrLoader, error } = this.lastFinishedLoad.value
|
|
@@ -132,6 +144,7 @@ export class DataSchemaResolver {
|
|
|
132
144
|
console.error(error)
|
|
133
145
|
finishLoad(undefined, error)
|
|
134
146
|
})
|
|
147
|
+
this.currentLoadPromise = promise
|
|
135
148
|
this.onLoadStart?.(promise)
|
|
136
149
|
}
|
|
137
150
|
}
|