@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,398 @@
|
|
|
1
|
+
import ItemMixin from './ItemMixin.js'
|
|
2
|
+
import LoadingMixin from './LoadingMixin.js'
|
|
3
|
+
import { setDefaultValues } from '../utils/schema.js'
|
|
4
|
+
import { assignDeeply, isObject, isString, labelize } from '@ditojs/utils'
|
|
5
|
+
import { getResource } from '../utils/resource.js'
|
|
6
|
+
import DitoContext from '../DitoContext.js'
|
|
7
|
+
|
|
8
|
+
// @vue/component
|
|
9
|
+
export default {
|
|
10
|
+
mixins: [ItemMixin, LoadingMixin],
|
|
11
|
+
|
|
12
|
+
provide() {
|
|
13
|
+
return {
|
|
14
|
+
$resourceComponent: () => this,
|
|
15
|
+
// Pass local verbs overrides on to children, see verbs() computed prop.
|
|
16
|
+
$verbs: () => this.verbs,
|
|
17
|
+
$isPopulated: () => this.hasData
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
|
|
21
|
+
data() {
|
|
22
|
+
return {
|
|
23
|
+
loadedData: null,
|
|
24
|
+
abortController: null
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
computed: {
|
|
29
|
+
resourceComponent() {
|
|
30
|
+
return this
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
resource() {
|
|
34
|
+
return this.getResource()
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
providesData() {
|
|
38
|
+
// This component is a data-source if it has an associated API resource:
|
|
39
|
+
return !!this.resource
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
linksToView() {
|
|
43
|
+
// Returns `false`here, but is overridden to return `true` in
|
|
44
|
+
// `SourceMixin` for component that do not provide their own data, but
|
|
45
|
+
// edit their items through a linked view. In this case, real ids need to
|
|
46
|
+
// be used.
|
|
47
|
+
return false
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
isTransient() {
|
|
51
|
+
// Check the form that this component belongs to as well, since it may be
|
|
52
|
+
// in creation mode, which makes it transient.
|
|
53
|
+
// NOTE: This does not loop endlessly because DitoForm redefines
|
|
54
|
+
// `isTransient()` to only return `!this.providesData`.
|
|
55
|
+
const form = this.formComponent
|
|
56
|
+
return (
|
|
57
|
+
!this.providesData &&
|
|
58
|
+
!this.linksToView ||
|
|
59
|
+
form && (
|
|
60
|
+
form.isTransient ||
|
|
61
|
+
form.isCreating
|
|
62
|
+
)
|
|
63
|
+
)
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
transientNote() {
|
|
67
|
+
return (
|
|
68
|
+
this.isTransient && (
|
|
69
|
+
'<b>Note</b>: the parent still needs to be saved ' +
|
|
70
|
+
'in order to persist this change.'
|
|
71
|
+
)
|
|
72
|
+
)
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
shouldLoad() {
|
|
76
|
+
return (
|
|
77
|
+
!this.isTransient &&
|
|
78
|
+
!this.isLoading
|
|
79
|
+
)
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
// @overridable
|
|
83
|
+
hasData() {
|
|
84
|
+
// Base definition, will be overridden by DitoForm and SourceMixin
|
|
85
|
+
return !!this.loadedData
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
verbs() {
|
|
89
|
+
// The actual code is the `getVerbs()` method, for easier overriding of
|
|
90
|
+
// this computed property in components that use the ResourceMixin.
|
|
91
|
+
return this.getVerbs()
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
paginationRange() {
|
|
95
|
+
// Only apply pagination to lists.
|
|
96
|
+
const { paginate: amount } = this.sourceSchema
|
|
97
|
+
if (this.isListSource && amount) {
|
|
98
|
+
const { page = 0 } = this.query || {}
|
|
99
|
+
const start = page * amount
|
|
100
|
+
return [start, start + amount - 1]
|
|
101
|
+
}
|
|
102
|
+
return null
|
|
103
|
+
},
|
|
104
|
+
|
|
105
|
+
queryParams() {
|
|
106
|
+
const range = this.paginationRange
|
|
107
|
+
const { page, ...query } = this.query || {}
|
|
108
|
+
return {
|
|
109
|
+
...query, // Query may override scope.
|
|
110
|
+
...(range && {
|
|
111
|
+
// Pass pagination as range, so that we automatically get Objection's
|
|
112
|
+
// results counting:
|
|
113
|
+
range: range.join(',')
|
|
114
|
+
})
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
|
|
119
|
+
created() {
|
|
120
|
+
// When creating nested data, we still need to call setupData()
|
|
121
|
+
if (this.providesData || this.isCreating) {
|
|
122
|
+
this.setupData()
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
|
|
126
|
+
methods: {
|
|
127
|
+
getResource({ method = 'get', child } = {}) {
|
|
128
|
+
// Returns the resource object representing the resource for the
|
|
129
|
+
// associated source schema.
|
|
130
|
+
const resource = this.sourceSchema?.resource
|
|
131
|
+
return getResource(resource, {
|
|
132
|
+
type: 'collection',
|
|
133
|
+
method,
|
|
134
|
+
parent: this.parentResourceComponent?.getResource({
|
|
135
|
+
method,
|
|
136
|
+
child: resource
|
|
137
|
+
}) ?? null,
|
|
138
|
+
child
|
|
139
|
+
})
|
|
140
|
+
},
|
|
141
|
+
|
|
142
|
+
getVerbs() {
|
|
143
|
+
const verbs = this.$verbs()
|
|
144
|
+
return this.isTransient
|
|
145
|
+
? {
|
|
146
|
+
...verbs,
|
|
147
|
+
// Override default verbs with their transient versions:
|
|
148
|
+
create: 'add',
|
|
149
|
+
created: 'added',
|
|
150
|
+
save: 'apply',
|
|
151
|
+
saved: 'applied',
|
|
152
|
+
delete: 'remove',
|
|
153
|
+
deleted: 'removed'
|
|
154
|
+
}
|
|
155
|
+
: verbs
|
|
156
|
+
},
|
|
157
|
+
|
|
158
|
+
// @overridable
|
|
159
|
+
clearData() {
|
|
160
|
+
this.loadedData = null
|
|
161
|
+
},
|
|
162
|
+
|
|
163
|
+
// @overridable
|
|
164
|
+
setData(data) {
|
|
165
|
+
this.loadedData = data
|
|
166
|
+
},
|
|
167
|
+
|
|
168
|
+
setupData() {
|
|
169
|
+
// Actual code is in separate function so it's easer to override
|
|
170
|
+
// `setupData()` and and call `ensureData()` from the overrides,
|
|
171
|
+
// see DitoForm and SourceMixin.
|
|
172
|
+
this.ensureData()
|
|
173
|
+
},
|
|
174
|
+
|
|
175
|
+
ensureData() {
|
|
176
|
+
if (this.shouldLoad) {
|
|
177
|
+
if (this.hasData) {
|
|
178
|
+
this.reloadData()
|
|
179
|
+
} else {
|
|
180
|
+
this.loadData(true)
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
|
|
185
|
+
reloadData() {
|
|
186
|
+
this.loadData(false)
|
|
187
|
+
},
|
|
188
|
+
|
|
189
|
+
loadData(clear) {
|
|
190
|
+
if (!this.isTransient) {
|
|
191
|
+
if (clear) {
|
|
192
|
+
this.clearData()
|
|
193
|
+
}
|
|
194
|
+
this.requestData()
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
|
|
198
|
+
createData(schema, type) {
|
|
199
|
+
return setDefaultValues(schema, type ? { type } : {}, this)
|
|
200
|
+
},
|
|
201
|
+
|
|
202
|
+
requestData() {
|
|
203
|
+
const query = this.queryParams
|
|
204
|
+
this.handleRequest({ method: 'get', query }, (err, response) => {
|
|
205
|
+
if (err) {
|
|
206
|
+
if (response) {
|
|
207
|
+
const { data } = response
|
|
208
|
+
if (
|
|
209
|
+
data?.type === 'FilterValidation' &&
|
|
210
|
+
this.onFilterErrors?.(data.errors)
|
|
211
|
+
) {
|
|
212
|
+
return true
|
|
213
|
+
} else if (this.isUnauthorizedError(response)) {
|
|
214
|
+
// TODO: Can we really swallow these errors?
|
|
215
|
+
// Is calling `ensureUser()` in `onBeforeRequest()` enough?
|
|
216
|
+
return true
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
} else {
|
|
220
|
+
this.setData(response.data)
|
|
221
|
+
this.emitSchemaEvent('load')
|
|
222
|
+
}
|
|
223
|
+
})
|
|
224
|
+
},
|
|
225
|
+
|
|
226
|
+
isValidationError(response) {
|
|
227
|
+
return response?.status === 400
|
|
228
|
+
},
|
|
229
|
+
|
|
230
|
+
isUnauthorizedError(response) {
|
|
231
|
+
return response?.status === 401
|
|
232
|
+
},
|
|
233
|
+
|
|
234
|
+
async handleRequest(
|
|
235
|
+
{
|
|
236
|
+
method,
|
|
237
|
+
resource = this.getResource({ method }),
|
|
238
|
+
query,
|
|
239
|
+
data
|
|
240
|
+
},
|
|
241
|
+
callback
|
|
242
|
+
) {
|
|
243
|
+
const loadingOptions = {
|
|
244
|
+
updateRoot: true, // Display spinner in header when loading in resources
|
|
245
|
+
updateView: this.isInView // Notify view of loading for view components
|
|
246
|
+
}
|
|
247
|
+
this.abortController?.abort()
|
|
248
|
+
const controller = new AbortController()
|
|
249
|
+
this.abortController = controller
|
|
250
|
+
const { signal } = controller
|
|
251
|
+
method = resource.method || method
|
|
252
|
+
const request = { method, resource, query, data, signal }
|
|
253
|
+
this.setLoading(true, loadingOptions)
|
|
254
|
+
try {
|
|
255
|
+
const response = await this.sendRequest(request)
|
|
256
|
+
// Pass both request and response to the callback, so they can be
|
|
257
|
+
// exposed to further callbacks through DitoContext.
|
|
258
|
+
callback(null, response)
|
|
259
|
+
} catch (error) {
|
|
260
|
+
if (error.name !== 'AbortError') {
|
|
261
|
+
// If callback returns true, errors were already handled.
|
|
262
|
+
const { response } = error
|
|
263
|
+
if (!callback(error, response)) {
|
|
264
|
+
const data = response?.data
|
|
265
|
+
const title = isString(data?.type)
|
|
266
|
+
? labelize(data.type)
|
|
267
|
+
: 'Error'
|
|
268
|
+
const text = data?.message ?? error
|
|
269
|
+
this.notify({ type: 'error', error, title, text })
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
if (this.abortController === controller) {
|
|
274
|
+
// Only clear the loading state if this is still the current request.
|
|
275
|
+
this.abortController = null
|
|
276
|
+
this.setLoading(false, loadingOptions)
|
|
277
|
+
}
|
|
278
|
+
},
|
|
279
|
+
|
|
280
|
+
getPayloadData(button, method) {
|
|
281
|
+
// Convention: only post, put and patch requests pass the data as payload.
|
|
282
|
+
return (
|
|
283
|
+
['post', 'put', 'patch'].includes(method) && (
|
|
284
|
+
// TODO: Use `handleDataSchema()` asynchronously here instead, to
|
|
285
|
+
// offer the same amount of possibilities for data loading.
|
|
286
|
+
button.getSchemaValue(['resource', 'data']) ||
|
|
287
|
+
button.processedItem
|
|
288
|
+
)
|
|
289
|
+
)
|
|
290
|
+
},
|
|
291
|
+
|
|
292
|
+
async submit(button) {
|
|
293
|
+
let { resource } = button.schema
|
|
294
|
+
resource = getResource(resource, {
|
|
295
|
+
parent: this.getResource({
|
|
296
|
+
method: resource?.method,
|
|
297
|
+
child: resource
|
|
298
|
+
})
|
|
299
|
+
})
|
|
300
|
+
if (resource) {
|
|
301
|
+
const { method } = resource
|
|
302
|
+
const data = this.getPayloadData(button, method)
|
|
303
|
+
return this.submitResource(button, resource, method, data)
|
|
304
|
+
}
|
|
305
|
+
return false
|
|
306
|
+
},
|
|
307
|
+
|
|
308
|
+
async submitResource(button, resource, method, data, {
|
|
309
|
+
setData = false,
|
|
310
|
+
onSuccess,
|
|
311
|
+
onError,
|
|
312
|
+
notifySuccess = () =>
|
|
313
|
+
this.notify({
|
|
314
|
+
type: 'success',
|
|
315
|
+
title: 'Request Successful',
|
|
316
|
+
text: 'Request was successfully sent.'
|
|
317
|
+
}),
|
|
318
|
+
notifyError = error =>
|
|
319
|
+
this.notify({
|
|
320
|
+
type: 'error',
|
|
321
|
+
error,
|
|
322
|
+
title: 'Request Error',
|
|
323
|
+
text: [
|
|
324
|
+
`Unable to send request${error ? ':' : ''}`,
|
|
325
|
+
error?.message || error
|
|
326
|
+
]
|
|
327
|
+
})
|
|
328
|
+
} = {}) {
|
|
329
|
+
return new Promise(resolve => {
|
|
330
|
+
this.handleRequest(
|
|
331
|
+
{ method, resource, data },
|
|
332
|
+
async (err, response) => {
|
|
333
|
+
const data = response?.data
|
|
334
|
+
if (err) {
|
|
335
|
+
// See if we're dealing with a Dito.js validation error:
|
|
336
|
+
const errors = this.isValidationError(response) && data.errors
|
|
337
|
+
if (errors) {
|
|
338
|
+
await this.showValidationErrors(errors, true)
|
|
339
|
+
} else {
|
|
340
|
+
const error = isObject(data) ? data : err
|
|
341
|
+
onError?.(error)
|
|
342
|
+
await this.emitButtonEvent(button, 'error', {
|
|
343
|
+
notify: notifyError,
|
|
344
|
+
error
|
|
345
|
+
})
|
|
346
|
+
}
|
|
347
|
+
resolve(false)
|
|
348
|
+
} else {
|
|
349
|
+
// Update the underlying data before calling `notify()` or
|
|
350
|
+
// `this.itemLabel`, so id is set after creating new items.
|
|
351
|
+
if (setData && data) {
|
|
352
|
+
// Preserve the foreign data entries when updating the data.
|
|
353
|
+
const { foreignData } = this.mainSchemaComponent.filterData(
|
|
354
|
+
this.data
|
|
355
|
+
)
|
|
356
|
+
// Tell the parent route to reload its data, so that it can
|
|
357
|
+
// update its foreign data entries.
|
|
358
|
+
const parentMeta = this.parentRouteComponent?.routeRecord?.meta
|
|
359
|
+
if (parentMeta) {
|
|
360
|
+
parentMeta.reload = true
|
|
361
|
+
}
|
|
362
|
+
this.setData(assignDeeply({}, foreignData, data))
|
|
363
|
+
}
|
|
364
|
+
onSuccess?.()
|
|
365
|
+
await this.emitButtonEvent(button, 'success', {
|
|
366
|
+
notify: notifySuccess
|
|
367
|
+
})
|
|
368
|
+
resolve(true)
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
)
|
|
372
|
+
})
|
|
373
|
+
},
|
|
374
|
+
|
|
375
|
+
async emitButtonEvent(button, event, { notify, error }) {
|
|
376
|
+
// Create the context outside of `emitEvent()`, so that
|
|
377
|
+
// `context.wasNotified` can be checked after.
|
|
378
|
+
const context = new DitoContext(button, {
|
|
379
|
+
nested: false,
|
|
380
|
+
data: this.data,
|
|
381
|
+
itemLabel: this.itemLabel,
|
|
382
|
+
error
|
|
383
|
+
})
|
|
384
|
+
const res = await button.emitEvent(event, { context })
|
|
385
|
+
if (
|
|
386
|
+
notify &&
|
|
387
|
+
// Prevent default if anything was returned from the event handler.
|
|
388
|
+
res === undefined &&
|
|
389
|
+
// Do not display default notification if the event handler already
|
|
390
|
+
// displayed a notification.
|
|
391
|
+
!context.wasNotified
|
|
392
|
+
) {
|
|
393
|
+
notify(error)
|
|
394
|
+
}
|
|
395
|
+
return res
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import ValidatorMixin from '../mixins/ValidatorMixin.js'
|
|
2
|
+
import { getCommonPrefix } from '@ditojs/utils'
|
|
3
|
+
|
|
4
|
+
// @vue/component
|
|
5
|
+
export default {
|
|
6
|
+
mixins: [ValidatorMixin],
|
|
7
|
+
|
|
8
|
+
provide() {
|
|
9
|
+
return {
|
|
10
|
+
$routeComponent: () => this
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
|
|
14
|
+
data() {
|
|
15
|
+
return {
|
|
16
|
+
reload: false,
|
|
17
|
+
// Each route-component defines a store that gets passed on to its
|
|
18
|
+
// child components, so they can store values in them that live beyond
|
|
19
|
+
// their life-cycle. See: DitoPane, SourceMixin
|
|
20
|
+
store: {},
|
|
21
|
+
loadCache: {} // See TypeMixin.load()
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
computed: {
|
|
26
|
+
routeComponent() {
|
|
27
|
+
// Override DitoMixin's routeComponent() which uses the injected value.
|
|
28
|
+
return this
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
routeLevel() {
|
|
32
|
+
let level = 0
|
|
33
|
+
let routeComponent = this
|
|
34
|
+
while ((routeComponent = routeComponent.parentRouteComponent)) {
|
|
35
|
+
level++
|
|
36
|
+
}
|
|
37
|
+
return level
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
routeRecord() {
|
|
41
|
+
return this.$route.matched[this.routeLevel]
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
isLastRoute() {
|
|
45
|
+
// Returns true when this router component is the last one in the route.
|
|
46
|
+
const { matched } = this.$route
|
|
47
|
+
return this.routeRecord === matched[matched.length - 1]
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
isLastUnnestedRoute() {
|
|
51
|
+
// Returns true if this route component is the last one in the route that
|
|
52
|
+
// needs its own router-view (= is not nested).
|
|
53
|
+
const { matched } = this.$route
|
|
54
|
+
for (let i = matched.length - 1; i >= 0; i--) {
|
|
55
|
+
const record = matched[i]
|
|
56
|
+
if (!record.meta.nested) {
|
|
57
|
+
return this.routeRecord === record
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return false
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
isNestedRoute() {
|
|
64
|
+
return this.meta.nested
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
isView() {
|
|
68
|
+
return false
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
meta() {
|
|
72
|
+
return this.routeRecord?.meta
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
path() {
|
|
76
|
+
return this.getRoutePath(this.routeRecord?.path)
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
label() {
|
|
80
|
+
return this.getLabel(this.schema)
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
breadcrumb() {
|
|
84
|
+
const { breadcrumb } = this.schema || {}
|
|
85
|
+
return breadcrumb || `${this.breadcrumbPrefix} ${this.label}`
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
breadcrumbPrefix() {
|
|
89
|
+
return ''
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
param() {
|
|
93
|
+
// Workaround for vue-router not being able to map multiple url parameters
|
|
94
|
+
// with the same name to multiple components, see:
|
|
95
|
+
// https://github.com/vuejs/vue-router/issues/1345
|
|
96
|
+
return this.$route.params[this.meta?.param] || null
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
// @overridable, see DitoForm
|
|
100
|
+
isMutating() {
|
|
101
|
+
return false
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
|
|
105
|
+
beforeRouteUpdate(to, from) {
|
|
106
|
+
return this?.beforeRouteChange(to, from)
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
beforeRouteLeave(to, from) {
|
|
110
|
+
return this?.beforeRouteChange(to, from)
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
created() {
|
|
114
|
+
// Keep a shared stack of root components for DitoTrail to use to render
|
|
115
|
+
// labels. Can't rely on $route.matched[i].instances.default unfortunately,
|
|
116
|
+
// as instances aren't immediately ready, and instances is not reactive.
|
|
117
|
+
this.appState.routeComponents.push(this)
|
|
118
|
+
},
|
|
119
|
+
|
|
120
|
+
unmounted() {
|
|
121
|
+
const { routeComponents } = this.appState
|
|
122
|
+
routeComponents.splice(routeComponents.indexOf(this), 1)
|
|
123
|
+
},
|
|
124
|
+
|
|
125
|
+
methods: {
|
|
126
|
+
beforeRouteChange(to, from) {
|
|
127
|
+
let ok = true
|
|
128
|
+
const isClosing = (
|
|
129
|
+
// Only handle this route change if the form is actually mapped to the
|
|
130
|
+
// `from` route, but include parent forms of closing nested forms as as
|
|
131
|
+
// well, by matching the the start of from/to path against `this.path`:
|
|
132
|
+
from.path.startsWith(this.path) &&
|
|
133
|
+
!to.path.startsWith(this.path) &&
|
|
134
|
+
// Exclude hash changes only (= tab changes):
|
|
135
|
+
from.path !== to.path && (
|
|
136
|
+
this.isFullRouteChange(to, from) ||
|
|
137
|
+
// Decide if we're moving towards a new nested form, or closing /
|
|
138
|
+
// replacing an already open one by comparing path lengths.
|
|
139
|
+
// The case of `=` matches the replacing of an already open one.
|
|
140
|
+
to.path.length <= from.path.length
|
|
141
|
+
)
|
|
142
|
+
)
|
|
143
|
+
if (isClosing) {
|
|
144
|
+
if (this.isMutating) {
|
|
145
|
+
// For active directly mutating (nested) forms that were not validated
|
|
146
|
+
// yet, validate them once. If the user then still wants to leave
|
|
147
|
+
// them, they can click close / navigate away again.
|
|
148
|
+
ok = (
|
|
149
|
+
this.isValidated ||
|
|
150
|
+
this.validateAll()
|
|
151
|
+
)
|
|
152
|
+
} else {
|
|
153
|
+
// The form doesn't directly mutate data. If it is dirty, ask if user
|
|
154
|
+
// wants to persist data first.
|
|
155
|
+
if (this.isDirty) {
|
|
156
|
+
ok = window.confirm(
|
|
157
|
+
`You have unsaved changes. Do you really want to ${
|
|
158
|
+
this.verbs.cancel
|
|
159
|
+
}?`
|
|
160
|
+
)
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return ok
|
|
165
|
+
},
|
|
166
|
+
|
|
167
|
+
getRoutePath(recordPath) {
|
|
168
|
+
// Maps the route's actual path to the matched routes by counting its
|
|
169
|
+
// parts separated by '/', splitting the path into the mapped parts
|
|
170
|
+
// containing actual parameters.
|
|
171
|
+
const { path } = this.$route
|
|
172
|
+
return recordPath
|
|
173
|
+
? path
|
|
174
|
+
.split('/')
|
|
175
|
+
.slice(0, recordPath.split('/').length)
|
|
176
|
+
.join('/')
|
|
177
|
+
: path
|
|
178
|
+
},
|
|
179
|
+
|
|
180
|
+
getChildPath(path) {
|
|
181
|
+
return `${this.path}/${path}`
|
|
182
|
+
},
|
|
183
|
+
|
|
184
|
+
isFullRouteChange(to, from) {
|
|
185
|
+
// The route path is the path up to the first / (excluding the initial /):
|
|
186
|
+
const rootPath = this.path.match(/^(\/[^/]*)/)[1]
|
|
187
|
+
return !getCommonPrefix(to.path, from.path).startsWith(rootPath)
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// @vue/component
|
|
2
|
+
export default {
|
|
3
|
+
provide() {
|
|
4
|
+
return {
|
|
5
|
+
$schemaParentComponent: () => this
|
|
6
|
+
}
|
|
7
|
+
},
|
|
8
|
+
|
|
9
|
+
data() {
|
|
10
|
+
return {
|
|
11
|
+
schemaComponents: []
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
|
|
15
|
+
computed: {
|
|
16
|
+
mainSchemaComponent() {
|
|
17
|
+
return this.schemaComponents[0]
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
|
|
21
|
+
methods: {
|
|
22
|
+
// This method is called by `DitoSchema.created()/unmounted()` on its
|
|
23
|
+
// `$schemaParentComponent`, if the parent uses the `SchemaParentMixin`:
|
|
24
|
+
_registerSchemaComponent(schemaComponent, add) {
|
|
25
|
+
const { schemaComponents } = this
|
|
26
|
+
if (add) {
|
|
27
|
+
schemaComponents.push(schemaComponent)
|
|
28
|
+
} else {
|
|
29
|
+
schemaComponents.splice(schemaComponents.indexOf(schemaComponent), 1)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// @vue/component
|
|
2
|
+
export default {
|
|
3
|
+
data() {
|
|
4
|
+
return {
|
|
5
|
+
isDragging: false
|
|
6
|
+
}
|
|
7
|
+
},
|
|
8
|
+
|
|
9
|
+
methods: {
|
|
10
|
+
getDraggableOptions(forceFallback = false) {
|
|
11
|
+
const prefix = 'dito-draggable'
|
|
12
|
+
return {
|
|
13
|
+
animation: 150,
|
|
14
|
+
handle: '.dito-button--drag',
|
|
15
|
+
dragClass: `${prefix}__drag`,
|
|
16
|
+
chosenClass: `${prefix}__chosen`,
|
|
17
|
+
ghostClass: `${prefix}__ghost`,
|
|
18
|
+
fallbackClass: `${prefix}__fallback`,
|
|
19
|
+
forceFallback,
|
|
20
|
+
onStart: this.onStartDrag,
|
|
21
|
+
onEnd: this.onEndDrag
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
onStartDrag() {
|
|
26
|
+
this.isDragging = true
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
onEndDrag({ oldIndex, newIndex }) {
|
|
30
|
+
this.isDragging = false
|
|
31
|
+
if (oldIndex !== newIndex) {
|
|
32
|
+
this.onChange()
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
updateOrder(sourceSchema, list, paginationRange) {
|
|
37
|
+
const { orderKey } = sourceSchema
|
|
38
|
+
if (orderKey) {
|
|
39
|
+
// Reorder the changed entries by their order key, taking pagination
|
|
40
|
+
// offsets into account:
|
|
41
|
+
const offset = paginationRange?.[0] || 0
|
|
42
|
+
for (let i = 0; i < list.length; i++) {
|
|
43
|
+
list[i][orderKey] = i + offset
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return list
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|