@radio-garden/ditojs-admin 2.85.2-0.5067ad799

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 (153) hide show
  1. package/README.md +180 -0
  2. package/dist/dito-admin.css +1 -0
  3. package/dist/dito-admin.es.js +12106 -0
  4. package/dist/dito-admin.umd.js +7 -0
  5. package/package.json +96 -0
  6. package/src/DitoAdmin.js +293 -0
  7. package/src/DitoComponent.js +34 -0
  8. package/src/DitoContext.js +318 -0
  9. package/src/DitoTypeComponent.js +42 -0
  10. package/src/DitoUser.js +12 -0
  11. package/src/appState.js +12 -0
  12. package/src/components/DitoAccount.vue +60 -0
  13. package/src/components/DitoAffix.vue +68 -0
  14. package/src/components/DitoAffixes.vue +200 -0
  15. package/src/components/DitoButtons.vue +80 -0
  16. package/src/components/DitoClipboard.vue +186 -0
  17. package/src/components/DitoContainer.vue +374 -0
  18. package/src/components/DitoCreateButton.vue +146 -0
  19. package/src/components/DitoDialog.vue +242 -0
  20. package/src/components/DitoDraggable.vue +117 -0
  21. package/src/components/DitoEditButtons.vue +135 -0
  22. package/src/components/DitoErrors.vue +83 -0
  23. package/src/components/DitoForm.vue +521 -0
  24. package/src/components/DitoFormInner.vue +26 -0
  25. package/src/components/DitoFormNested.vue +17 -0
  26. package/src/components/DitoHeader.vue +84 -0
  27. package/src/components/DitoLabel.vue +200 -0
  28. package/src/components/DitoMenu.vue +186 -0
  29. package/src/components/DitoNavigation.vue +40 -0
  30. package/src/components/DitoNotifications.vue +170 -0
  31. package/src/components/DitoPagination.vue +42 -0
  32. package/src/components/DitoPane.vue +334 -0
  33. package/src/components/DitoPanel.vue +256 -0
  34. package/src/components/DitoPanels.vue +61 -0
  35. package/src/components/DitoRoot.vue +524 -0
  36. package/src/components/DitoSchema.vue +846 -0
  37. package/src/components/DitoSchemaInlined.vue +97 -0
  38. package/src/components/DitoScopes.vue +76 -0
  39. package/src/components/DitoSidebar.vue +50 -0
  40. package/src/components/DitoSpinner.vue +95 -0
  41. package/src/components/DitoTableCell.vue +64 -0
  42. package/src/components/DitoTableHead.vue +121 -0
  43. package/src/components/DitoTabs.vue +103 -0
  44. package/src/components/DitoTrail.vue +124 -0
  45. package/src/components/DitoTreeItem.vue +420 -0
  46. package/src/components/DitoUploadFile.vue +199 -0
  47. package/src/components/DitoVNode.vue +14 -0
  48. package/src/components/DitoView.vue +143 -0
  49. package/src/components/index.js +42 -0
  50. package/src/directives/resize.js +83 -0
  51. package/src/index.js +1 -0
  52. package/src/mixins/ContextMixin.js +68 -0
  53. package/src/mixins/DataMixin.js +131 -0
  54. package/src/mixins/DitoMixin.js +591 -0
  55. package/src/mixins/DomMixin.js +29 -0
  56. package/src/mixins/EmitterMixin.js +158 -0
  57. package/src/mixins/ItemMixin.js +144 -0
  58. package/src/mixins/LoadingMixin.js +23 -0
  59. package/src/mixins/NumberMixin.js +118 -0
  60. package/src/mixins/OptionsMixin.js +304 -0
  61. package/src/mixins/PulldownMixin.js +63 -0
  62. package/src/mixins/ResourceMixin.js +398 -0
  63. package/src/mixins/RouteMixin.js +190 -0
  64. package/src/mixins/SchemaParentMixin.js +33 -0
  65. package/src/mixins/SortableMixin.js +49 -0
  66. package/src/mixins/SourceMixin.js +734 -0
  67. package/src/mixins/TextMixin.js +26 -0
  68. package/src/mixins/TypeMixin.js +280 -0
  69. package/src/mixins/ValidationMixin.js +119 -0
  70. package/src/mixins/ValidatorMixin.js +57 -0
  71. package/src/mixins/ValueMixin.js +31 -0
  72. package/src/styles/_base.scss +17 -0
  73. package/src/styles/_button.scss +191 -0
  74. package/src/styles/_imports.scss +3 -0
  75. package/src/styles/_info.scss +19 -0
  76. package/src/styles/_layout.scss +19 -0
  77. package/src/styles/_pulldown.scss +38 -0
  78. package/src/styles/_scroll.scss +13 -0
  79. package/src/styles/_settings.scss +88 -0
  80. package/src/styles/_table.scss +223 -0
  81. package/src/styles/_tippy.scss +45 -0
  82. package/src/styles/style.scss +9 -0
  83. package/src/types/DitoTypeButton.vue +143 -0
  84. package/src/types/DitoTypeCheckbox.vue +27 -0
  85. package/src/types/DitoTypeCheckboxes.vue +65 -0
  86. package/src/types/DitoTypeCode.vue +199 -0
  87. package/src/types/DitoTypeColor.vue +272 -0
  88. package/src/types/DitoTypeComponent.vue +31 -0
  89. package/src/types/DitoTypeComputed.vue +50 -0
  90. package/src/types/DitoTypeDate.vue +99 -0
  91. package/src/types/DitoTypeLabel.vue +23 -0
  92. package/src/types/DitoTypeList.vue +364 -0
  93. package/src/types/DitoTypeMarkup.vue +700 -0
  94. package/src/types/DitoTypeMultiselect.vue +522 -0
  95. package/src/types/DitoTypeNumber.vue +66 -0
  96. package/src/types/DitoTypeObject.vue +136 -0
  97. package/src/types/DitoTypePanel.vue +18 -0
  98. package/src/types/DitoTypeProgress.vue +40 -0
  99. package/src/types/DitoTypeRadio.vue +45 -0
  100. package/src/types/DitoTypeSection.vue +80 -0
  101. package/src/types/DitoTypeSelect.vue +133 -0
  102. package/src/types/DitoTypeSlider.vue +66 -0
  103. package/src/types/DitoTypeSpacer.vue +11 -0
  104. package/src/types/DitoTypeSwitch.vue +40 -0
  105. package/src/types/DitoTypeText.vue +101 -0
  106. package/src/types/DitoTypeTextarea.vue +48 -0
  107. package/src/types/DitoTypeTreeList.vue +193 -0
  108. package/src/types/DitoTypeUpload.vue +503 -0
  109. package/src/types/index.js +30 -0
  110. package/src/utils/SchemaGraph.js +147 -0
  111. package/src/utils/accessor.js +75 -0
  112. package/src/utils/agent.js +47 -0
  113. package/src/utils/data.js +92 -0
  114. package/src/utils/filter.js +266 -0
  115. package/src/utils/math.js +14 -0
  116. package/src/utils/options.js +48 -0
  117. package/src/utils/path.js +5 -0
  118. package/src/utils/resource.js +44 -0
  119. package/src/utils/route.js +53 -0
  120. package/src/utils/schema.js +1121 -0
  121. package/src/utils/type.js +81 -0
  122. package/src/utils/uid.js +15 -0
  123. package/src/utils/units.js +5 -0
  124. package/src/validators/_creditcard.js +6 -0
  125. package/src/validators/_decimals.js +11 -0
  126. package/src/validators/_domain.js +6 -0
  127. package/src/validators/_email.js +6 -0
  128. package/src/validators/_hostname.js +6 -0
  129. package/src/validators/_integer.js +6 -0
  130. package/src/validators/_max.js +6 -0
  131. package/src/validators/_min.js +6 -0
  132. package/src/validators/_password.js +5 -0
  133. package/src/validators/_range.js +6 -0
  134. package/src/validators/_required.js +9 -0
  135. package/src/validators/_url.js +6 -0
  136. package/src/validators/index.js +12 -0
  137. package/src/verbs.js +17 -0
  138. package/types/index.d.ts +3298 -0
  139. package/types/tests/admin.test-d.ts +27 -0
  140. package/types/tests/component-buttons.test-d.ts +44 -0
  141. package/types/tests/component-list.test-d.ts +159 -0
  142. package/types/tests/component-misc.test-d.ts +137 -0
  143. package/types/tests/component-object.test-d.ts +69 -0
  144. package/types/tests/component-section.test-d.ts +174 -0
  145. package/types/tests/component-select.test-d.ts +107 -0
  146. package/types/tests/components.test-d.ts +81 -0
  147. package/types/tests/context.test-d.ts +31 -0
  148. package/types/tests/fixtures.ts +24 -0
  149. package/types/tests/form.test-d.ts +109 -0
  150. package/types/tests/instance.test-d.ts +20 -0
  151. package/types/tests/schema-features.test-d.ts +402 -0
  152. package/types/tests/variance.test-d.ts +125 -0
  153. package/types/tests/view.test-d.ts +146 -0
@@ -0,0 +1,521 @@
1
+ <template lang="pug">
2
+ .dito-form.dito-scroll-parent(
3
+ :class="{ 'dito-form-nested': isNestedRoute }"
4
+ :data-resource="sourceSchema.path"
5
+ )
6
+ //- Only render a router-view here if this isn't the last data route and not a
7
+ //- nested form route, which will appear elsewhere in its own view.
8
+ RouterView(
9
+ v-if="!isLastUnnestedRoute && !isNestedRoute"
10
+ v-show="!isActiveRoute"
11
+ )
12
+ //- NOTE: Nested form components are kept alive by using `v-show` instead of
13
+ //- `v-if` here, so event handling and other things still work with nested
14
+ //- editing.
15
+ DitoFormInner(
16
+ v-show="isActiveRoute"
17
+ :nested="isNestedRoute"
18
+ )
19
+ //- Prevent implicit submission of the form, for example when typing enter
20
+ //- in an input field.
21
+ //- https://stackoverflow.com/a/51507806
22
+ button(
23
+ v-show="false"
24
+ type="submit"
25
+ disabled
26
+ )
27
+ DitoSchema(
28
+ :schema="schema"
29
+ :dataPath="dataPath"
30
+ :data="data"
31
+ :meta="meta"
32
+ :store="store"
33
+ :padding="isNestedRoute ? 'nested' : 'root'"
34
+ :active="isActiveRoute"
35
+ :disabled="isLoading"
36
+ :scrollable="!isNestedRoute"
37
+ generateLabels
38
+ )
39
+ template(#buttons)
40
+ DitoButtons.dito-buttons--round.dito-buttons--large.dito-buttons--main(
41
+ :class="{ 'dito-buttons--sticky': !isNestedRoute }"
42
+ :buttons="buttonSchemas"
43
+ :dataPath="dataPath"
44
+ :data="data"
45
+ :meta="meta"
46
+ :store="store"
47
+ :disabled="isLoading"
48
+ )
49
+ </template>
50
+
51
+ <script>
52
+ import { clone, capitalize, parseDataPath, assignDeeply } from '@ditojs/utils'
53
+ import DitoComponent from '../DitoComponent.js'
54
+ import RouteMixin from '../mixins/RouteMixin.js'
55
+ import ResourceMixin from '../mixins/ResourceMixin.js'
56
+ import { getResource, getMemberResource } from '../utils/resource.js'
57
+ import { getButtonSchemas, isObjectSource } from '../utils/schema.js'
58
+ import { resolvePath } from '../utils/path.js'
59
+
60
+ // @vue/component
61
+ export default DitoComponent.component('DitoForm', {
62
+ mixins: [RouteMixin, ResourceMixin],
63
+
64
+ data() {
65
+ return {
66
+ createdData: null,
67
+ clonedData: undefined,
68
+ sourceKey: null,
69
+ isForm: true
70
+ }
71
+ },
72
+
73
+ computed: {
74
+ verbs() {
75
+ // Add submit / submitted to the verbs returned by ResourceMixin
76
+ // NOTE: These get passed on to children through:
77
+ // `provide() ... { $verbs: () => this.verbs }` in ResourceMixin
78
+ const verbs = this.getVerbs()
79
+ const { isCreating, providesData } = this
80
+ return {
81
+ ...verbs,
82
+ submit: isCreating ? verbs.create : verbs.save,
83
+ submitted: isCreating ? verbs.created : verbs.saved,
84
+ cancel: providesData ? verbs.cancel : verbs.close,
85
+ cancelled: providesData ? verbs.cancelled : verbs.closed
86
+ }
87
+ },
88
+
89
+ schema() {
90
+ return this.getItemFormSchema(
91
+ this.sourceSchema,
92
+ this.data || (
93
+ this.creationType
94
+ ? // If there is no data yet but the type to create a new item is
95
+ // is specified, provide a temporary empty object with just the
96
+ // type set, so `getItemFormSchema()` can determine the form.
97
+ { type: this.creationType }
98
+ : null
99
+ ),
100
+ this.context
101
+ )
102
+ },
103
+
104
+ buttonSchemas() {
105
+ return getButtonSchemas(
106
+ assignDeeply(
107
+ {
108
+ cancel: {
109
+ type: 'button',
110
+ events: {
111
+ click: () => this.cancel()
112
+ }
113
+ },
114
+
115
+ submit: !this.isMutating && {
116
+ type: 'submit',
117
+ // Submit buttons close the form by default:
118
+ closeForm: true,
119
+ events: {
120
+ click: ({ component: button }) => button.submit()
121
+ }
122
+ }
123
+ },
124
+ this.schema.buttons
125
+ )
126
+ )
127
+ },
128
+
129
+ isActiveRoute() {
130
+ return this.isLastRoute || this.isLastUnnestedRoute
131
+ },
132
+
133
+ isTransient() {
134
+ return !this.providesData
135
+ },
136
+
137
+ isCreating() {
138
+ // this.param is inherited from RouteMixin
139
+ return this.param === 'create'
140
+ },
141
+
142
+ isDirty() {
143
+ return !this.isMutating && !!this.mainSchemaComponent?.isDirty
144
+ },
145
+
146
+ isMutating() {
147
+ // When `sourceSchema.mutate` is true, the form edits the inherited data
148
+ // directly instead of making a copy for persistence upon submission.
149
+ // See `inheritedData()` computed property for more details.
150
+ return !!this.sourceSchema.mutate
151
+ },
152
+
153
+ selectedTab() {
154
+ return this.mainSchemaComponent?.selectedTab || null
155
+ },
156
+
157
+ creationType() {
158
+ // The type of form to create, if there are multiple forms to choose from.
159
+ return this.$route.query.type
160
+ },
161
+
162
+ itemId() {
163
+ return this.isCreating
164
+ ? null
165
+ : this.param ?? null
166
+ },
167
+
168
+ method() {
169
+ return this.isCreating ? 'post' : 'patch'
170
+ },
171
+
172
+ breadcrumbPrefix() {
173
+ return capitalize(this.isCreating ? this.verbs.create : this.verbs.edit)
174
+ },
175
+
176
+ data() {
177
+ // Return different data "containers" based on different scenarios:
178
+ // 1. createdData, if we're in a form for a newly created object.
179
+ // 2. loadedData, if the form itself is the root of the data (e.g. when
180
+ // directly loading an editing root).
181
+ // 3. The data inherited from the parent, which itself may be either a
182
+ // view that loaded the data, or a form that either loaded the data, or
183
+ // also inherited it from its parent. Note that we use a clone of it,
184
+ // so, data changes aren't applied until setSourceData() is called.
185
+ return this.createdData || this.loadedData || this.inheritedData || null
186
+ },
187
+
188
+ dataPath() {
189
+ return this.getDataPathFrom(this.dataComponent)
190
+ },
191
+
192
+ sourceData() {
193
+ // Possible parents are DitoForm for forms, or DitoView for root lists.
194
+ // Both have a data property which abstracts away loading and inheriting
195
+ // of data.
196
+ // Forms that are about to be destroyed due to navigation loose their
197
+ // route-record, but might still trigger this getter. Filter those out.
198
+ let data = this.routeRecord ? this.parentRouteComponent.data : null
199
+ if (data) {
200
+ // Handle nested data by splitting the dataPath, iterate through the
201
+ // actual data and look nest child-data up.
202
+ const dataParts = parseDataPath(
203
+ this.getDataPathFrom(this.parentRouteComponent)
204
+ )
205
+ // Compare dataParts against matched routePath parts, to identify those
206
+ // parts that need to be treated like ids and mapped to indices in data.
207
+ const pathParts = this.routeRecord.path.split('/')
208
+ const routeParts = pathParts.slice(pathParts.length - dataParts.length)
209
+ // TODO: Fix side-effects
210
+ // eslint-disable-next-line vue/no-side-effects-in-computed-properties
211
+ this.sourceKey = null
212
+ const lastDataPart = dataParts[dataParts.length - 1]
213
+ if (isObjectSource(this.sourceSchema) && lastDataPart === 'create') {
214
+ // If we have an object source and are creating, the dataPath needs to
215
+ // be shortened by the 'create' entry. This isn't needed for list
216
+ // sources, as there the parameter is actually mapped to the item id.
217
+ dataParts.length--
218
+ }
219
+ for (let i = 0, l = dataParts.length; i < l && data; i++) {
220
+ const dataPart = dataParts[i]
221
+ // If this is an :id part, find the index of the item with given id.
222
+ const key = /^:id/.test(routeParts[i])
223
+ ? dataPart === 'create'
224
+ ? null // There's no index for entries about to be created
225
+ : this.findItemIdIndex(this.sourceSchema, data, dataPart)
226
+ : dataPart
227
+ // Skip the final lookup but remember `sourceKey`, as we want the
228
+ // parent data so we can replace the entry at `sourceKey` on it.
229
+ if (i === l - 1) {
230
+ // TODO: Fix side-effects
231
+ // eslint-disable-next-line max-len
232
+ // eslint-disable-next-line vue/no-side-effects-in-computed-properties
233
+ this.sourceKey = key
234
+ } else {
235
+ data = data[key]
236
+ }
237
+ }
238
+ }
239
+ return data
240
+ },
241
+
242
+ inheritedData() {
243
+ // Data inherited from parent, and cloned to protect against reactive
244
+ // changes until changes are applied through setSourceData(), unless
245
+ // `sourceSchema.mutate` is true, in which case data is mutated directly.
246
+ if (
247
+ this.isTransient &&
248
+ this.clonedData === undefined &&
249
+ this.sourceData &&
250
+ this.sourceKey !== null
251
+ ) {
252
+ let data = this.sourceData[this.sourceKey]
253
+ if (!this.isMutating) {
254
+ // Use a trick to store cloned inherited data in clonedData, to make
255
+ // it reactive and prevent it from being cloned multiple times.
256
+ // TODO: Fix side-effects
257
+ // eslint-disable-next-line vue/no-side-effects-in-computed-properties
258
+ this.clonedData = data = clone(data)
259
+ }
260
+ if (
261
+ data === null &&
262
+ !this.isCreating &&
263
+ isObjectSource(this.sourceSchema)
264
+ ) {
265
+ // If data of an object source is null, redirect to its create route.
266
+ // TODO: Fix side-effects
267
+ // eslint-disable-next-line vue/no-side-effects-in-computed-properties
268
+ this.$router.push({ path: `${this.path}/create` })
269
+ }
270
+ return data
271
+ }
272
+ return this.clonedData
273
+ },
274
+
275
+ // @override ResourceMixin.hasData()
276
+ hasData() {
277
+ return !!this.data
278
+ },
279
+
280
+ itemLabel() {
281
+ return this.getItemLabel(this.sourceSchema, this.data, { extended: true })
282
+ }
283
+ },
284
+
285
+ watch: {
286
+ $route: {
287
+ // https://github.com/vuejs/vue-router/issues/3393#issuecomment-1158470149
288
+ flush: 'post',
289
+ handler(to, from) {
290
+ // Reload form data when navigating to a different entity in same form.
291
+ const param = this.meta?.param
292
+ if (
293
+ param &&
294
+ this.providesData &&
295
+ // TODO: See if we can remove this due to `flush: 'post'`.
296
+ from.matched[0].path === to.matched[0].path && // Staying on same form
297
+ from.params[param] !== 'create' && // But haven't been creating
298
+ to.params[param] !== from.params[param] // Going to a different entity
299
+ ) {
300
+ this.loadData(true)
301
+ }
302
+ }
303
+ },
304
+
305
+ sourceData: 'clearClonedData',
306
+ // Needed for the 'create' redirect in `inheritedData()` to work:
307
+ create: 'setupData'
308
+ },
309
+
310
+ methods: {
311
+ emitSchemaEvent(event, params) {
312
+ return this.mainSchemaComponent?.emitEvent(event, params)
313
+ },
314
+
315
+ getDataPathFrom(routeComponent) {
316
+ // Get the data path by denormalizePath the relative route path
317
+ return this.api.denormalizePath(
318
+ this.path
319
+ // DitoViews have nested routes, so don't remove their path.
320
+ .slice((routeComponent.isView ? 0 : routeComponent.path.length) + 1)
321
+ )
322
+ },
323
+
324
+ // @override ResourceMixin.getResource()
325
+ getResource(options) {
326
+ const resource = ResourceMixin.methods.getResource.call(this, options)
327
+ return getMemberResource(this.itemId, resource) || resource
328
+ },
329
+
330
+ // @override ResourceMixin.setupData()
331
+ setupData() {
332
+ if (this.isCreating) {
333
+ this.createdData ||= this.createData(this.schema, this.creationType)
334
+ } else {
335
+ this.ensureData()
336
+ }
337
+ },
338
+
339
+ setSourceData(data) {
340
+ if (this.sourceData && this.sourceKey !== null) {
341
+ const { mainSchemaComponent } = this
342
+ this.sourceData[this.sourceKey] =
343
+ mainSchemaComponent.filterData(data).localData
344
+ mainSchemaComponent.onChange()
345
+ return true
346
+ }
347
+ return false
348
+ },
349
+
350
+ addSourceData(data) {
351
+ return isObjectSource(this.sourceSchema)
352
+ ? this.setSourceData(data)
353
+ : !!this.sourceData?.push(data)
354
+ },
355
+
356
+ // @override ResourceMixin.clearData()
357
+ clearData() {
358
+ this.setData(null)
359
+ },
360
+
361
+ // @override ResourceMixin.setData()
362
+ setData(data) {
363
+ // setData() is called after submit when data has changed.
364
+ if (this.isTransient) {
365
+ // For components with transient data, modify this.sourceData.
366
+ this.setSourceData(data)
367
+ } else {
368
+ this.createdData = null
369
+ this.loadedData = data
370
+ }
371
+ },
372
+
373
+ clearClonedData(to, from) {
374
+ // Only clear if the watched sourceData itself changes in the form.
375
+ if (to !== from) {
376
+ this.clonedData = undefined
377
+ }
378
+ },
379
+
380
+ async cancel() {
381
+ return this.close()
382
+ },
383
+
384
+ async close() {
385
+ return this.navigate(this.parentRouteComponent.path)
386
+ },
387
+
388
+ getSubmitVerb(present = true) {
389
+ return this.isCreating
390
+ ? present
391
+ ? 'create'
392
+ : 'created'
393
+ : present
394
+ ? 'submit'
395
+ : 'submitted'
396
+ },
397
+
398
+ async submit(button, { validate = true, closeForm = false } = {}) {
399
+ if (validate && !this.validateAll()) {
400
+ return false
401
+ }
402
+
403
+ const getVerb = present => this.verbs[this.getSubmitVerb(present)]
404
+
405
+ // Allow buttons to override both method and resource path to submit to:
406
+ let { method } = this
407
+ let resource = this.getResource({ method })
408
+ const buttonResource = getResource(button.schema.resource, {
409
+ parent: resource
410
+ })
411
+ resource = buttonResource || resource
412
+ method = resource?.method || method
413
+ const data = this.getPayloadData(button, method)
414
+ let success
415
+ if (!buttonResource && this.isTransient) {
416
+ success = await this.submitTransient(button, resource, method, data, {
417
+ onSuccess: () => this.emitSchemaEvent(this.getSubmitVerb()),
418
+ onError: error =>
419
+ this.emitSchemaEvent('error', {
420
+ context: { error }
421
+ }),
422
+ notifySuccess: () => {
423
+ const verb = getVerb(false)
424
+ this.notify({
425
+ type: 'info',
426
+ title: this.isCreating
427
+ ? `Item ${capitalize(verb)}`
428
+ : `Change ${capitalize(verb)}`,
429
+ text: [
430
+ this.isCreating
431
+ ? `${this.itemLabel} was ${verb}.`
432
+ : `Changes to ${this.itemLabel} were ${verb}.`,
433
+ this.transientNote
434
+ ]
435
+ })
436
+ },
437
+ notifyError: error => {
438
+ const verb = getVerb(true)
439
+ this.notify({
440
+ type: 'error',
441
+ error,
442
+ title: 'Request Error',
443
+ text: `Unable to ${verb} ${this.itemLabel}.`
444
+ })
445
+ }
446
+ })
447
+ } else {
448
+ success = await this.submitResource(button, resource, method, data, {
449
+ setData: true,
450
+ onSuccess: () => this.emitSchemaEvent(this.getSubmitVerb()),
451
+ onError: error =>
452
+ this.emitSchemaEvent('error', {
453
+ context: { error }
454
+ }),
455
+ notifySuccess: () => {
456
+ const verb = getVerb(false)
457
+ this.notify({
458
+ type: 'success',
459
+ title: `Successfully ${capitalize(verb)}`,
460
+ text: `${this.itemLabel} was ${verb}.`
461
+ })
462
+ },
463
+ notifyError: error => {
464
+ const verb = getVerb(true)
465
+ this.notify({
466
+ type: 'error',
467
+ error,
468
+ title: 'Request Error',
469
+ text: [
470
+ `Unable to ${verb} ${this.itemLabel}${error ? ':' : ''}`,
471
+ error?.message || error
472
+ ]
473
+ })
474
+ }
475
+ })
476
+ }
477
+ if (success) {
478
+ this.resetValidation()
479
+ if (closeForm || button.closeForm) {
480
+ this.close()
481
+ } else if (this.isCreating) {
482
+ // Redirect to the form editing the newly created item:
483
+ const id = this.getItemId(this.schema, this.data)
484
+ this.$router.replace({
485
+ path: resolvePath(`${this.path}/../${id}`),
486
+ // Preserve hash for tabs:
487
+ hash: this.$route.hash
488
+ })
489
+ }
490
+ }
491
+ return success
492
+ },
493
+
494
+ async submitTransient(button, _resource, _method, data, {
495
+ onSuccess,
496
+ onError,
497
+ notifySuccess,
498
+ notifyError
499
+ }) {
500
+ // Handle the default "submitting" of transient, nested data:
501
+ const success = this.isCreating
502
+ ? this.addSourceData(data)
503
+ : this.setSourceData(data)
504
+ if (success) {
505
+ onSuccess?.()
506
+ await this.emitButtonEvent(button, 'success', {
507
+ notify: notifySuccess
508
+ })
509
+ } else {
510
+ const error = 'Could not submit transient item'
511
+ onError?.(error)
512
+ await this.emitButtonEvent(button, 'error', {
513
+ notify: notifyError,
514
+ error
515
+ })
516
+ }
517
+ return success
518
+ }
519
+ }
520
+ })
521
+ </script>
@@ -0,0 +1,26 @@
1
+ <template lang="pug">
2
+ //- Use a <div> for nested forms, as we shouldn't nest actual <form> tags.
3
+ div(
4
+ v-if="nested"
5
+ )
6
+ slot
7
+ form.dito-scroll-parent(
8
+ v-else
9
+ @submit.prevent
10
+ )
11
+ slot
12
+ </template>
13
+
14
+ <script>
15
+ import DitoComponent from '../DitoComponent.js'
16
+
17
+ // @vue/component
18
+ export default DitoComponent.component('DitoFormInner', {
19
+ props: {
20
+ nested: {
21
+ type: Boolean,
22
+ default: false
23
+ }
24
+ }
25
+ })
26
+ </script>
@@ -0,0 +1,17 @@
1
+ <script>
2
+ import DitoComponent from '../DitoComponent.js'
3
+ import DitoForm from './DitoForm.vue'
4
+
5
+ // @vue/component
6
+ export default DitoComponent.component('DitoFormNested', {
7
+ extends: DitoForm
8
+ })
9
+ </script>
10
+
11
+ <style lang="scss">
12
+ .dito-form-nested {
13
+ // No scrolling inside nested forms, and prevent open .multiselect from
14
+ // being cropped.
15
+ overflow: visible;
16
+ }
17
+ </style>
@@ -0,0 +1,84 @@
1
+ <template lang="pug">
2
+ nav.dito-header
3
+ DitoTrail
4
+ DitoSpinner(
5
+ v-if="isLoading"
6
+ :size="spinner?.size"
7
+ :color="spinner?.color"
8
+ )
9
+ //- Teleport target for `.dito-schema-header`:
10
+ .dito-header__teleport
11
+ slot
12
+ </template>
13
+
14
+ <script>
15
+ import DitoComponent from '../DitoComponent.js'
16
+
17
+ // @vue/component
18
+ export default DitoComponent.component('DitoHeader', {
19
+ props: {
20
+ spinner: {
21
+ type: Object,
22
+ default: null
23
+ },
24
+ isLoading: {
25
+ type: Boolean,
26
+ default: false
27
+ }
28
+ }
29
+ })
30
+ </script>
31
+
32
+ <style lang="scss">
33
+ @import '../styles/_imports';
34
+
35
+ .dito-header {
36
+ position: relative;
37
+ background: $color-black;
38
+ font-size: $header-font-size;
39
+ line-height: $header-line-height;
40
+ z-index: $z-index-header;
41
+ @include user-select(none);
42
+
43
+ &::after {
44
+ // Set the full-width header background to the header color.
45
+ content: '';
46
+ inset: 0;
47
+ width: 100vw;
48
+ position: absolute;
49
+ background: inherit;
50
+ z-index: -1;
51
+ }
52
+
53
+ span {
54
+ display: inline-block;
55
+ padding: $header-padding;
56
+ color: $color-white;
57
+
58
+ &:empty {
59
+ &::after {
60
+ content: '\200b';
61
+ }
62
+ }
63
+ }
64
+
65
+ &__teleport {
66
+ // Align the teleported schema headers on top of to the header menu.
67
+ position: absolute;
68
+ inset: 0;
69
+ display: flex;
70
+ justify-content: flex-end;
71
+ padding: 0 $header-padding-hor;
72
+ // Turn off pointer events so that DitoTrail keeps receiving events...
73
+ pointer-events: none;
74
+ // ...but move them to the children.
75
+ > * {
76
+ pointer-events: auto;
77
+ }
78
+
79
+ .dito-button {
80
+ margin: 0 0 $tab-margin $tab-margin;
81
+ }
82
+ }
83
+ }
84
+ </style>