@ditojs/admin 2.100.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/dist/dito-admin.css +1 -1
  2. package/dist/dito-admin.es.js +3555 -2719
  3. package/dist/dito-admin.umd.js +5 -6
  4. package/package.json +84 -46
  5. package/src/DitoAdmin.js +1 -65
  6. package/src/DitoComponent.js +1 -1
  7. package/src/DitoContext.js +51 -0
  8. package/src/DitoTypeComponent.js +23 -1
  9. package/src/components/DitoAffixes.vue +6 -3
  10. package/src/components/DitoButtons.vue +14 -1
  11. package/src/components/DitoContainer.vue +19 -10
  12. package/src/components/DitoCreateButton.vue +11 -2
  13. package/src/components/DitoDialog.vue +21 -2
  14. package/src/components/DitoForm.vue +152 -84
  15. package/src/components/DitoLabel.vue +3 -2
  16. package/src/components/DitoPane.vue +55 -11
  17. package/src/components/DitoPanel.vue +34 -5
  18. package/src/components/DitoPanels.vue +2 -1
  19. package/src/components/DitoRoot.vue +6 -6
  20. package/src/components/DitoRouterView.vue +27 -0
  21. package/src/components/DitoSchema.vue +278 -145
  22. package/src/components/DitoSchemaInlined.vue +1 -1
  23. package/src/components/DitoTabs.vue +16 -18
  24. package/src/components/DitoTrail.vue +5 -4
  25. package/src/components/DitoTreeItem.vue +17 -6
  26. package/src/components/DitoView.vue +21 -3
  27. package/src/components/index.js +1 -0
  28. package/src/mixins/DitoMixin.js +53 -7
  29. package/src/mixins/ItemMixin.js +3 -6
  30. package/src/mixins/NumberMixin.js +47 -31
  31. package/src/mixins/OptionsMixin.js +132 -71
  32. package/src/mixins/ResourceMixin.js +5 -2
  33. package/src/mixins/RouteMixin.js +39 -1
  34. package/src/mixins/SortableMixin.js +0 -13
  35. package/src/mixins/SourceMixin.js +155 -96
  36. package/src/mixins/TextMixin.js +1 -1
  37. package/src/mixins/TypeMixin.js +37 -22
  38. package/src/mixins/ValidationMixin.js +9 -27
  39. package/src/mixins/ValidatorMixin.js +5 -7
  40. package/src/mixins/ValueMixin.js +5 -2
  41. package/src/types/DitoTypeButton.vue +1 -1
  42. package/src/types/DitoTypeCheckbox.vue +1 -1
  43. package/src/types/DitoTypeCheckboxes.vue +1 -1
  44. package/src/types/DitoTypeCode.vue +8 -1
  45. package/src/types/DitoTypeColor.vue +20 -34
  46. package/src/types/DitoTypeComponent.vue +1 -1
  47. package/src/types/DitoTypeComputed.vue +4 -27
  48. package/src/types/DitoTypeDate.vue +16 -5
  49. package/src/types/DitoTypeLabel.vue +1 -4
  50. package/src/types/DitoTypeList.vue +9 -5
  51. package/src/types/DitoTypeMarkup.vue +25 -13
  52. package/src/types/DitoTypeMultiselect.vue +37 -6
  53. package/src/types/DitoTypeNumber.vue +1 -1
  54. package/src/types/DitoTypeObject.vue +10 -7
  55. package/src/types/DitoTypePanel.vue +20 -6
  56. package/src/types/DitoTypeProgress.vue +1 -1
  57. package/src/types/DitoTypeRadio.vue +1 -1
  58. package/src/types/DitoTypeSection.vue +32 -4
  59. package/src/types/DitoTypeSelect.vue +1 -1
  60. package/src/types/DitoTypeSlider.vue +1 -1
  61. package/src/types/DitoTypeSwitch.vue +1 -1
  62. package/src/types/DitoTypeText.vue +13 -15
  63. package/src/types/DitoTypeTextarea.vue +1 -1
  64. package/src/types/DitoTypeTreeList.vue +5 -7
  65. package/src/types/DitoTypeUpload.vue +1 -2
  66. package/src/utils/DataSchemaResolver.js +154 -0
  67. package/src/utils/FormModel.js +615 -0
  68. package/src/utils/data.js +20 -0
  69. package/src/utils/filter.js +3 -2
  70. package/src/utils/options.js +3 -0
  71. package/src/utils/request.js +70 -0
  72. package/src/utils/schema/data.js +659 -0
  73. package/src/utils/schema/lookup.js +250 -0
  74. package/src/utils/schema/setup.js +354 -0
  75. package/src/utils/schema/structure.js +172 -0
  76. package/src/utils/schema/types.js +34 -0
  77. package/src/utils/schema/validation.js +142 -0
  78. package/src/utils/units.js +70 -3
  79. package/types/index.d.ts +174 -59
  80. package/src/mixins/DataMixin.js +0 -131
  81. package/src/utils/schema.js +0 -1121
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ditojs/admin",
3
- "version": "2.100.0",
3
+ "version": "3.1.0",
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,13 +22,14 @@
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",
29
31
  "watch": "pnpm run build --mode 'development' --watch",
30
- "prepare": "pnpm run build",
31
- "types": "tsc --noEmit types/index.d.ts"
32
+ "prepare": "pnpm run build"
32
33
  },
33
34
  "engines": {
34
35
  "node": ">= 24.0.0"
@@ -40,58 +41,95 @@
40
41
  "not ie_mob > 0"
41
42
  ],
42
43
  "dependencies": {
43
- "@ditojs/ui": "^2.100.0",
44
- "@ditojs/utils": "^2.100.0"
44
+ "@ditojs/ui": "^3.1.0",
45
+ "@ditojs/utils": "^3.1.0"
45
46
  },
46
47
  "peerDependencies": {
47
48
  "@kyvg/vue3-notification": "^3.4.2",
48
- "@lk77/vue3-color": "^3.0.6",
49
- "@tiptap/core": "^3.20.1",
50
- "@tiptap/extension-blockquote": "^3.20.1",
51
- "@tiptap/extension-bold": "^3.20.1",
52
- "@tiptap/extension-bullet-list": "^3.20.1",
53
- "@tiptap/extension-code": "^3.20.1",
54
- "@tiptap/extension-code-block": "^3.20.1",
55
- "@tiptap/extension-document": "^3.20.1",
56
- "@tiptap/extension-hard-break": "^3.20.1",
57
- "@tiptap/extension-heading": "^3.20.1",
58
- "@tiptap/extension-history": "^3.20.1",
59
- "@tiptap/extension-horizontal-rule": "^3.20.1",
60
- "@tiptap/extension-italic": "^3.20.1",
61
- "@tiptap/extension-link": "^3.20.1",
62
- "@tiptap/extension-list": "^3.20.1",
63
- "@tiptap/extension-list-item": "^3.20.1",
64
- "@tiptap/extension-ordered-list": "^3.20.1",
65
- "@tiptap/extension-paragraph": "^3.20.1",
66
- "@tiptap/extension-strike": "^3.20.1",
67
- "@tiptap/extension-subscript": "^3.20.1",
68
- "@tiptap/extension-superscript": "^3.20.1",
69
- "@tiptap/extension-text": "^3.20.1",
70
- "@tiptap/extension-text-style": "^3.20.1",
71
- "@tiptap/extension-underline": "^3.20.1",
72
- "@tiptap/extensions": "^3.20.1",
73
- "@tiptap/pm": "^3.20.1",
74
- "@tiptap/vue-3": "^3.20.1",
75
- "@vueuse/integrations": "^14.2.1",
49
+ "@tiptap/core": "^3.31.4",
50
+ "@tiptap/extension-blockquote": "^3.31.4",
51
+ "@tiptap/extension-bold": "^3.31.4",
52
+ "@tiptap/extension-bullet-list": "^3.31.4",
53
+ "@tiptap/extension-code": "^3.31.4",
54
+ "@tiptap/extension-code-block": "^3.31.4",
55
+ "@tiptap/extension-document": "^3.31.4",
56
+ "@tiptap/extension-hard-break": "^3.31.4",
57
+ "@tiptap/extension-heading": "^3.31.4",
58
+ "@tiptap/extension-history": "^3.31.4",
59
+ "@tiptap/extension-horizontal-rule": "^3.31.4",
60
+ "@tiptap/extension-italic": "^3.31.4",
61
+ "@tiptap/extension-link": "^3.31.4",
62
+ "@tiptap/extension-list": "^3.31.4",
63
+ "@tiptap/extension-list-item": "^3.31.4",
64
+ "@tiptap/extension-ordered-list": "^3.31.4",
65
+ "@tiptap/extension-paragraph": "^3.31.4",
66
+ "@tiptap/extension-strike": "^3.31.4",
67
+ "@tiptap/extension-subscript": "^3.31.4",
68
+ "@tiptap/extension-superscript": "^3.31.4",
69
+ "@tiptap/extension-text": "^3.31.4",
70
+ "@tiptap/extension-text-style": "^3.31.4",
71
+ "@tiptap/extension-underline": "^3.31.4",
72
+ "@tiptap/extensions": "^3.31.4",
73
+ "@tiptap/pm": "^3.31.4",
74
+ "@tiptap/vue-3": "^3.31.4",
75
+ "@vueuse/integrations": "^15.0.0",
76
76
  "codeflask": "^1.4.1",
77
- "filesize": "^11.0.13",
78
- "filesize-parser": "^1.5.1",
79
- "focus-trap": "^8.0.0",
77
+ "focus-trap": "^8.2.2",
80
78
  "nanoid": "^6.0.1",
81
79
  "sortablejs": "^1.15.7",
82
- "tinycolor2": "^1.6.0",
83
80
  "tippy.js": "^6.3.7",
84
81
  "tiptap-footnotes": "^3.0.1",
85
- "type-fest": "^5.4.4",
86
- "vue": "^3.5.30",
87
- "vue-multiselect": "^3.4.0",
88
- "vue-router": "^5.0.3",
89
- "vue-upload-component": "^3.1.17"
82
+ "type-fest": "^5.10.0",
83
+ "vue": "^3.5.43",
84
+ "vue-color": "^3.3.3",
85
+ "vue-multiselect": "^3.5.0",
86
+ "vue-router": "^5.3.1",
87
+ "vue-upload-component": "^3.2.2"
90
88
  },
91
89
  "devDependencies": {
92
- "@ditojs/build": "^2.100.0",
90
+ "@ditojs/build": "^3.1.0",
91
+ "@kyvg/vue3-notification": "^3.4.2",
92
+ "@tiptap/core": "^3.31.4",
93
+ "@tiptap/extension-blockquote": "^3.31.4",
94
+ "@tiptap/extension-bold": "^3.31.4",
95
+ "@tiptap/extension-bullet-list": "^3.31.4",
96
+ "@tiptap/extension-code": "^3.31.4",
97
+ "@tiptap/extension-code-block": "^3.31.4",
98
+ "@tiptap/extension-document": "^3.31.4",
99
+ "@tiptap/extension-hard-break": "^3.31.4",
100
+ "@tiptap/extension-heading": "^3.31.4",
101
+ "@tiptap/extension-history": "^3.31.4",
102
+ "@tiptap/extension-horizontal-rule": "^3.31.4",
103
+ "@tiptap/extension-italic": "^3.31.4",
104
+ "@tiptap/extension-link": "^3.31.4",
105
+ "@tiptap/extension-list": "^3.31.4",
106
+ "@tiptap/extension-list-item": "^3.31.4",
107
+ "@tiptap/extension-ordered-list": "^3.31.4",
108
+ "@tiptap/extension-paragraph": "^3.31.4",
109
+ "@tiptap/extension-strike": "^3.31.4",
110
+ "@tiptap/extension-subscript": "^3.31.4",
111
+ "@tiptap/extension-superscript": "^3.31.4",
112
+ "@tiptap/extension-text": "^3.31.4",
113
+ "@tiptap/extension-text-style": "^3.31.4",
114
+ "@tiptap/extension-underline": "^3.31.4",
115
+ "@tiptap/extensions": "^3.31.4",
116
+ "@tiptap/pm": "^3.31.4",
117
+ "@tiptap/vue-3": "^3.31.4",
118
+ "@vueuse/integrations": "^15.0.0",
119
+ "codeflask": "^1.4.1",
120
+ "focus-trap": "^8.2.2",
121
+ "nanoid": "^6.0.1",
122
+ "sortablejs": "^1.15.7",
123
+ "tippy.js": "^6.3.7",
124
+ "tiptap-footnotes": "^3.0.1",
125
+ "type-fest": "^5.10.0",
93
126
  "typescript": "^6.0.3",
94
- "vite": "^8.3.2"
127
+ "vite": "^8.3.2",
128
+ "vue": "^3.5.43",
129
+ "vue-color": "^3.3.3",
130
+ "vue-multiselect": "^3.5.0",
131
+ "vue-router": "^5.3.1",
132
+ "vue-upload-component": "^3.2.2"
95
133
  },
96
- "gitHead": "c6fdbe03971188779fc9cf5f1b4202e08bc75a65"
134
+ "gitHead": "97314265c5cbac6a248d7350f43fea8ea92b995f"
97
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 { formatQuery } from './utils/route.js'
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
- }
@@ -1,5 +1,5 @@
1
1
  import DitoMixin from './mixins/DitoMixin.js'
2
- import { getTypeComponent } from './utils/schema.js'
2
+ import { getTypeComponent } from './utils/schema/types.js'
3
3
  import { resolveMergedOptions } from './utils/options.js'
4
4
  import { isPlainObject } from '@ditojs/utils'
5
5
 
@@ -1,6 +1,7 @@
1
1
  import { toRaw } from 'vue'
2
2
  import { getValueAtDataPath, isFunction } from '@ditojs/utils'
3
3
  import {
4
+ appendDataPath,
4
5
  getItemDataPath,
5
6
  getParentItemDataPath,
6
7
  getParentItem,
@@ -8,6 +9,7 @@ import {
8
9
  getLastDataPathName,
9
10
  getLastDataPathIndex
10
11
  } from './utils/data.js'
12
+ import { isNested } from './utils/schema/structure.js'
11
13
 
12
14
  const { hasOwnProperty } = Object.prototype
13
15
 
@@ -81,6 +83,55 @@ export default class DitoContext {
81
83
  : new DitoContext(component, context)
82
84
  }
83
85
 
86
+ // Creates the context of the component described by `schema` and `name` in
87
+ // `data`, e.g. to evaluate its `if` before it's rendered, the same way as
88
+ // once it's rendered. Its `value` is read from `data` only when it's used,
89
+ // so that watchers only depend on it if it's used, and missing values don't
90
+ // need to be resolved through the data path.
91
+ static createForSchema(component, {
92
+ schema,
93
+ name,
94
+ nested = isNested(schema),
95
+ data,
96
+ dataPath,
97
+ rootData
98
+ }) {
99
+ // Pass a function, so that the `value` getter isn't evaluated when the
100
+ // context is created.
101
+ return new DitoContext(component, () => ({
102
+ schema,
103
+ name,
104
+ nested,
105
+ data,
106
+ dataPath,
107
+ rootData,
108
+ get value() {
109
+ return name != null ? data?.[name] : undefined
110
+ }
111
+ }))
112
+ }
113
+
114
+ // Creates the context of a component of this context's schema, e.g. of a
115
+ // section, the same way as when that component is rendered: Its data is the
116
+ // value of this context's component if that is nested, or else the same
117
+ // data. The data path adds `name` if the child component is nested.
118
+ createChildContext(schema, name) {
119
+ const nested = isNested(schema)
120
+ // Contexts without a schema don't hold a value either, e.g. the ones of
121
+ // `DitoMenu` and `DitoPanels`, evaluating views and panels.
122
+ const holdsOwnValue = !!this.schema && isNested(this.schema)
123
+ return DitoContext.createForSchema(this.component, {
124
+ schema,
125
+ name,
126
+ nested,
127
+ // Read the value from the item that holds it rather than through the data
128
+ // path, as items that are being created aren't part of the root data.
129
+ data: holdsOwnValue ? this.item?.[this.name] : this.item,
130
+ dataPath: nested ? appendDataPath(this.dataPath, name) : this.dataPath,
131
+ rootData: this.rootItem
132
+ })
133
+ }
134
+
84
135
  extend(object) {
85
136
  // Create a copy of this context that inherits from the real one, but
86
137
  // overrides some properties with the ones from the passed `object`.
@@ -4,7 +4,10 @@
4
4
  import { asArray, camelize } from '@ditojs/utils'
5
5
  import DitoComponent from './DitoComponent.js'
6
6
  import TypeMixin from './mixins/TypeMixin.js'
7
- import { registerTypeComponent, getTypeComponent } from './utils/schema.js'
7
+ import {
8
+ registerTypeComponent,
9
+ getTypeComponent
10
+ } from './utils/schema/types.js'
8
11
 
9
12
  // @vue/component
10
13
  export default {
@@ -22,7 +25,9 @@ export default {
22
25
  generateLabel: true,
23
26
  excludeValue: false,
24
27
  ignoreMissingValue: null,
28
+ treatNullAsMissing: null,
25
29
  omitSpacing: false,
30
+ getTypeValidations: null,
26
31
 
27
32
  component: DitoComponent.component,
28
33
 
@@ -30,6 +35,15 @@ export default {
30
35
 
31
36
  register(types, definition = {}) {
32
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
+ }
33
47
  const component = this.component(
34
48
  `DitoType${camelize(types[0], true)}`,
35
49
  definition
@@ -40,3 +54,11 @@ export default {
40
54
  return component
41
55
  }
42
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
+ }
@@ -32,7 +32,7 @@ import DitoComponent from '../DitoComponent.js'
32
32
  import DitoAffix from './DitoAffix.vue'
33
33
  import { asArray, isString } from '@ditojs/utils'
34
34
  import { hasSlotContent } from '@ditojs/ui/src'
35
- import { shouldRenderSchema } from '../utils/schema.js'
35
+ import { shouldRenderSchema } from '../utils/schema/data.js'
36
36
 
37
37
  export default DitoComponent.component('DitoAffixes', {
38
38
  components: { DitoAffix },
@@ -175,7 +175,10 @@ export default DitoComponent.component('DitoAffixes', {
175
175
  padding: 0;
176
176
  border: 0;
177
177
 
178
- @at-root .dito-component:not(:has(.dito-component)):hover & {
178
+ // Only show when hovering the component itself, not its open popup, e.g.
179
+ // the color picker.
180
+ @at-root .dito-component:not(:has(.dito-component, .dito-popup:hover)):hover
181
+ & {
179
182
  display: block;
180
183
  }
181
184
 
@@ -192,7 +195,7 @@ export default DitoComponent.component('DitoAffixes', {
192
195
 
193
196
  // Hide other affixes when clear button is shown
194
197
  // prettier-ignore
195
- @at-root .dito-component:hover:not(:has(.dito-component))
198
+ @at-root .dito-component:hover:not(:has(.dito-component, .dito-popup:hover))
196
199
  #{$self}:has(#{$self}__clear) > *:not(#{$self}__clear) {
197
200
  display: none;
198
201
  }
@@ -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
@@ -18,7 +18,7 @@
18
18
  DitoLabel(
19
19
  v-if="hasLabel"
20
20
  :class="labelClasses"
21
- :dataPath="labelDataPath"
21
+ :htmlFor="labelHtmlFor"
22
22
  :label="label"
23
23
  :info="info"
24
24
  )
@@ -50,13 +50,11 @@ import ValueMixin from '../mixins/ValueMixin.js'
50
50
  import ContextMixin from '../mixins/ContextMixin.js'
51
51
  import DitoContext from '../DitoContext.js'
52
52
  import { getSchemaAccessor } from '../utils/accessor.js'
53
- import {
54
- getAllPanelEntries,
55
- getTypeComponent,
56
- hasLabel,
57
- omitSpacing
58
- } from '../utils/schema.js'
53
+ import { getTypeComponent } from '../utils/schema/types.js'
54
+ import { hasLabel, omitSpacing } from '../utils/schema/structure.js'
55
+ import { getAllPanelEntries } from '../utils/schema/lookup.js'
59
56
  import { parseFraction } from '../utils/math.js'
57
+ import { appendDataPath } from '../utils/data.js'
60
58
 
61
59
  // @vue/component
62
60
  export default DitoComponent.component('DitoContainer', {
@@ -105,6 +103,12 @@ export default DitoComponent.component('DitoContainer', {
105
103
  return this.schema.name
106
104
  },
107
105
 
106
+ componentPath() {
107
+ // Add the component's name, also for unnested components that don't
108
+ // add it to their data path, see `DitoMixin.componentPath`.
109
+ return appendDataPath(this.parentComponent.componentPath, this.name)
110
+ },
111
+
108
112
  type() {
109
113
  return this.schema.type
110
114
  },
@@ -125,9 +129,10 @@ export default DitoComponent.component('DitoContainer', {
125
129
  return this.hasLabel ? this.getLabel(this.schema) : null
126
130
  },
127
131
 
128
- labelDataPath() {
129
- // Unnested types don't have a dataPath for themselves, don't use it:
130
- return this.nested ? this.dataPath : null
132
+ labelHtmlFor() {
133
+ // Type components use their component path as DOM id. Unnested types
134
+ // don't display a value of their own to label:
135
+ return this.nested ? this.componentPath : null
131
136
  },
132
137
 
133
138
  info: getSchemaAccessor('info', {
@@ -252,6 +257,10 @@ export default DitoComponent.component('DitoContainer', {
252
257
  this.api,
253
258
  this.schema,
254
259
  this.dataPath,
260
+ // The panel of the type is addressed relative to the data path, which
261
+ // unnested components don't add their name to:
262
+ this.nested ? this.componentPath : this.parentComponent.componentPath,
263
+ this.componentPath,
255
264
  this.$refs.component,
256
265
  this.tabComponent
257
266
  )
@@ -4,7 +4,7 @@
4
4
  v-if="creatableForm"
5
5
  :type="isInlined ? 'button' : 'submit'"
6
6
  :disabled="disabled"
7
- v-bind="getButtonAttributes(verb)"
7
+ v-bind="getButtonAttributes(verb, formLabel)"
8
8
  @click="createItem(creatableForm)"
9
9
  ) {{ text }}
10
10
  template(
@@ -38,7 +38,8 @@
38
38
  import DitoComponent from '../DitoComponent.js'
39
39
  import ContextMixin from '../mixins/ContextMixin.js'
40
40
  import PulldownMixin from '../mixins/PulldownMixin.js'
41
- import { getFormSchemas, isInlined } from '../utils/schema.js'
41
+ import { isInlined } from '../utils/schema/structure.js'
42
+ import { getFormSchemas } from '../utils/schema/lookup.js'
42
43
 
43
44
  // @vue/component
44
45
  export default DitoComponent.component('DitoCreateButton', {
@@ -93,6 +94,14 @@ export default DitoComponent.component('DitoCreateButton', {
93
94
  return forms && Object.keys(forms).length === 1 && forms.default || null
94
95
  },
95
96
 
97
+ formLabel() {
98
+ // Only use labels declared on the form, not ones derived from its name.
99
+ const form = this.creatableForm
100
+ return form
101
+ ? this.getSchemaValue('label', { schema: form, type: String })
102
+ : null
103
+ },
104
+
96
105
  isInlined() {
97
106
  return isInlined(this.schema)
98
107
  }
@@ -35,7 +35,8 @@
35
35
  import { clone } from '@ditojs/utils'
36
36
  import DitoComponent from '../DitoComponent.js'
37
37
  import DomMixin from '../mixins/DomMixin.js'
38
- import { getButtonSchemas } from '../utils/schema.js'
38
+ import { getButtonSchemas } from '../utils/schema/lookup.js'
39
+ import { FormModel } from '../utils/FormModel.js'
39
40
  import { UseFocusTrap } from '@vueuse/integrations/useFocusTrap/component'
40
41
 
41
42
  // @vue/component
@@ -126,6 +127,16 @@ export default DitoComponent.component('DitoDialog', {
126
127
  }
127
128
  },
128
129
 
130
+ created() {
131
+ // Writes defaults and computed values into the dialog's data and resolves
132
+ // options, see `FormModel`:
133
+ this.formModel = new FormModel({
134
+ component: this,
135
+ getSchema: () => this.schema,
136
+ getData: () => this.dialogData
137
+ })
138
+ },
139
+
129
140
  mounted() {
130
141
  this.domOn(window, {
131
142
  keyup: event => {
@@ -136,6 +147,12 @@ export default DitoComponent.component('DitoDialog', {
136
147
  })
137
148
  },
138
149
 
150
+ beforeUnmount() {
151
+ // Stop the model before the component's own watchers are stopped, as the
152
+ // model's watchers aren't part of the component, see `FormModel`.
153
+ this.formModel.stop()
154
+ },
155
+
139
156
  methods: {
140
157
  remove() {
141
158
  this.$emit('remove')
@@ -160,7 +177,9 @@ export default DitoComponent.component('DitoDialog', {
160
177
  }
161
178
  },
162
179
 
163
- submit() {
180
+ async submit() {
181
+ // Wait for computed values that depend on pending loads, e.g. options.
182
+ await this.waitForPendingLoadsOf(this.formModel)
164
183
  this.resolve(this.dialogData)
165
184
  },
166
185