@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.
- package/README.md +180 -0
- package/dist/dito-admin.css +1 -0
- package/dist/dito-admin.es.js +12106 -0
- package/dist/dito-admin.umd.js +7 -0
- package/package.json +96 -0
- package/src/DitoAdmin.js +293 -0
- package/src/DitoComponent.js +34 -0
- package/src/DitoContext.js +318 -0
- package/src/DitoTypeComponent.js +42 -0
- package/src/DitoUser.js +12 -0
- package/src/appState.js +12 -0
- package/src/components/DitoAccount.vue +60 -0
- package/src/components/DitoAffix.vue +68 -0
- package/src/components/DitoAffixes.vue +200 -0
- package/src/components/DitoButtons.vue +80 -0
- package/src/components/DitoClipboard.vue +186 -0
- package/src/components/DitoContainer.vue +374 -0
- package/src/components/DitoCreateButton.vue +146 -0
- package/src/components/DitoDialog.vue +242 -0
- package/src/components/DitoDraggable.vue +117 -0
- package/src/components/DitoEditButtons.vue +135 -0
- package/src/components/DitoErrors.vue +83 -0
- package/src/components/DitoForm.vue +521 -0
- package/src/components/DitoFormInner.vue +26 -0
- package/src/components/DitoFormNested.vue +17 -0
- package/src/components/DitoHeader.vue +84 -0
- package/src/components/DitoLabel.vue +200 -0
- package/src/components/DitoMenu.vue +186 -0
- package/src/components/DitoNavigation.vue +40 -0
- package/src/components/DitoNotifications.vue +170 -0
- package/src/components/DitoPagination.vue +42 -0
- package/src/components/DitoPane.vue +334 -0
- package/src/components/DitoPanel.vue +256 -0
- package/src/components/DitoPanels.vue +61 -0
- package/src/components/DitoRoot.vue +524 -0
- package/src/components/DitoSchema.vue +846 -0
- package/src/components/DitoSchemaInlined.vue +97 -0
- package/src/components/DitoScopes.vue +76 -0
- package/src/components/DitoSidebar.vue +50 -0
- package/src/components/DitoSpinner.vue +95 -0
- package/src/components/DitoTableCell.vue +64 -0
- package/src/components/DitoTableHead.vue +121 -0
- package/src/components/DitoTabs.vue +103 -0
- package/src/components/DitoTrail.vue +124 -0
- package/src/components/DitoTreeItem.vue +420 -0
- package/src/components/DitoUploadFile.vue +199 -0
- package/src/components/DitoVNode.vue +14 -0
- package/src/components/DitoView.vue +143 -0
- package/src/components/index.js +42 -0
- package/src/directives/resize.js +83 -0
- package/src/index.js +1 -0
- package/src/mixins/ContextMixin.js +68 -0
- package/src/mixins/DataMixin.js +131 -0
- package/src/mixins/DitoMixin.js +591 -0
- package/src/mixins/DomMixin.js +29 -0
- package/src/mixins/EmitterMixin.js +158 -0
- package/src/mixins/ItemMixin.js +144 -0
- package/src/mixins/LoadingMixin.js +23 -0
- package/src/mixins/NumberMixin.js +118 -0
- package/src/mixins/OptionsMixin.js +304 -0
- package/src/mixins/PulldownMixin.js +63 -0
- package/src/mixins/ResourceMixin.js +398 -0
- package/src/mixins/RouteMixin.js +190 -0
- package/src/mixins/SchemaParentMixin.js +33 -0
- package/src/mixins/SortableMixin.js +49 -0
- package/src/mixins/SourceMixin.js +734 -0
- package/src/mixins/TextMixin.js +26 -0
- package/src/mixins/TypeMixin.js +280 -0
- package/src/mixins/ValidationMixin.js +119 -0
- package/src/mixins/ValidatorMixin.js +57 -0
- package/src/mixins/ValueMixin.js +31 -0
- package/src/styles/_base.scss +17 -0
- package/src/styles/_button.scss +191 -0
- package/src/styles/_imports.scss +3 -0
- package/src/styles/_info.scss +19 -0
- package/src/styles/_layout.scss +19 -0
- package/src/styles/_pulldown.scss +38 -0
- package/src/styles/_scroll.scss +13 -0
- package/src/styles/_settings.scss +88 -0
- package/src/styles/_table.scss +223 -0
- package/src/styles/_tippy.scss +45 -0
- package/src/styles/style.scss +9 -0
- package/src/types/DitoTypeButton.vue +143 -0
- package/src/types/DitoTypeCheckbox.vue +27 -0
- package/src/types/DitoTypeCheckboxes.vue +65 -0
- package/src/types/DitoTypeCode.vue +199 -0
- package/src/types/DitoTypeColor.vue +272 -0
- package/src/types/DitoTypeComponent.vue +31 -0
- package/src/types/DitoTypeComputed.vue +50 -0
- package/src/types/DitoTypeDate.vue +99 -0
- package/src/types/DitoTypeLabel.vue +23 -0
- package/src/types/DitoTypeList.vue +364 -0
- package/src/types/DitoTypeMarkup.vue +700 -0
- package/src/types/DitoTypeMultiselect.vue +522 -0
- package/src/types/DitoTypeNumber.vue +66 -0
- package/src/types/DitoTypeObject.vue +136 -0
- package/src/types/DitoTypePanel.vue +18 -0
- package/src/types/DitoTypeProgress.vue +40 -0
- package/src/types/DitoTypeRadio.vue +45 -0
- package/src/types/DitoTypeSection.vue +80 -0
- package/src/types/DitoTypeSelect.vue +133 -0
- package/src/types/DitoTypeSlider.vue +66 -0
- package/src/types/DitoTypeSpacer.vue +11 -0
- package/src/types/DitoTypeSwitch.vue +40 -0
- package/src/types/DitoTypeText.vue +101 -0
- package/src/types/DitoTypeTextarea.vue +48 -0
- package/src/types/DitoTypeTreeList.vue +193 -0
- package/src/types/DitoTypeUpload.vue +503 -0
- package/src/types/index.js +30 -0
- package/src/utils/SchemaGraph.js +147 -0
- package/src/utils/accessor.js +75 -0
- package/src/utils/agent.js +47 -0
- package/src/utils/data.js +92 -0
- package/src/utils/filter.js +266 -0
- package/src/utils/math.js +14 -0
- package/src/utils/options.js +48 -0
- package/src/utils/path.js +5 -0
- package/src/utils/resource.js +44 -0
- package/src/utils/route.js +53 -0
- package/src/utils/schema.js +1121 -0
- package/src/utils/type.js +81 -0
- package/src/utils/uid.js +15 -0
- package/src/utils/units.js +5 -0
- package/src/validators/_creditcard.js +6 -0
- package/src/validators/_decimals.js +11 -0
- package/src/validators/_domain.js +6 -0
- package/src/validators/_email.js +6 -0
- package/src/validators/_hostname.js +6 -0
- package/src/validators/_integer.js +6 -0
- package/src/validators/_max.js +6 -0
- package/src/validators/_min.js +6 -0
- package/src/validators/_password.js +5 -0
- package/src/validators/_range.js +6 -0
- package/src/validators/_required.js +9 -0
- package/src/validators/_url.js +6 -0
- package/src/validators/index.js +12 -0
- package/src/verbs.js +17 -0
- package/types/index.d.ts +3298 -0
- package/types/tests/admin.test-d.ts +27 -0
- package/types/tests/component-buttons.test-d.ts +44 -0
- package/types/tests/component-list.test-d.ts +159 -0
- package/types/tests/component-misc.test-d.ts +137 -0
- package/types/tests/component-object.test-d.ts +69 -0
- package/types/tests/component-section.test-d.ts +174 -0
- package/types/tests/component-select.test-d.ts +107 -0
- package/types/tests/components.test-d.ts +81 -0
- package/types/tests/context.test-d.ts +31 -0
- package/types/tests/fixtures.ts +24 -0
- package/types/tests/form.test-d.ts +109 -0
- package/types/tests/instance.test-d.ts +20 -0
- package/types/tests/schema-features.test-d.ts +402 -0
- package/types/tests/variance.test-d.ts +125 -0
- 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>
|