@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,591 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isObject,
|
|
3
|
+
isString,
|
|
4
|
+
isFunction,
|
|
5
|
+
equals,
|
|
6
|
+
labelize,
|
|
7
|
+
hyphenate,
|
|
8
|
+
format
|
|
9
|
+
} from '@ditojs/utils'
|
|
10
|
+
import appState from '../appState.js'
|
|
11
|
+
import DitoContext from '../DitoContext.js'
|
|
12
|
+
import EmitterMixin from './EmitterMixin.js'
|
|
13
|
+
import {
|
|
14
|
+
flattenViews,
|
|
15
|
+
getSchemaValue,
|
|
16
|
+
shouldRenderSchema
|
|
17
|
+
} from '../utils/schema.js'
|
|
18
|
+
import { getResource, getMemberResource } from '../utils/resource.js'
|
|
19
|
+
import { computed, reactive } from 'vue'
|
|
20
|
+
|
|
21
|
+
// @vue/component
|
|
22
|
+
export default {
|
|
23
|
+
mixins: [EmitterMixin],
|
|
24
|
+
|
|
25
|
+
inject: [
|
|
26
|
+
'api',
|
|
27
|
+
'$verbs',
|
|
28
|
+
'$views',
|
|
29
|
+
'$isPopulated',
|
|
30
|
+
'$parentComponent',
|
|
31
|
+
'$schemaComponent',
|
|
32
|
+
'$routeComponent',
|
|
33
|
+
'$dataComponent',
|
|
34
|
+
'$sourceComponent',
|
|
35
|
+
'$resourceComponent',
|
|
36
|
+
'$dialogComponent',
|
|
37
|
+
'$panelComponent',
|
|
38
|
+
'$tabComponent'
|
|
39
|
+
],
|
|
40
|
+
|
|
41
|
+
provide() {
|
|
42
|
+
const self = () => this
|
|
43
|
+
return this.providesData
|
|
44
|
+
? {
|
|
45
|
+
$parentComponent: self,
|
|
46
|
+
$dataComponent: self
|
|
47
|
+
}
|
|
48
|
+
: {
|
|
49
|
+
$parentComponent: self
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
data() {
|
|
54
|
+
return {
|
|
55
|
+
appState,
|
|
56
|
+
isMounted: false,
|
|
57
|
+
overrides: null // See accessor.js
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
computed: {
|
|
62
|
+
providesData() {
|
|
63
|
+
// NOTE: This is overridden in ResourceMixin, used by lists.
|
|
64
|
+
return false
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
sourceSchema() {
|
|
68
|
+
return this.meta?.schema
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
user() {
|
|
72
|
+
return appState.user
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
// $verbs, $verbs and $isPopulated are defined as functions, to preserve
|
|
76
|
+
// reactiveness across provide/inject.
|
|
77
|
+
// See: https://github.com/vuejs/vue/issues/7017#issuecomment-480906691
|
|
78
|
+
verbs() {
|
|
79
|
+
return this.$verbs()
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
views() {
|
|
83
|
+
return this.$views()
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
flattenedViews() {
|
|
87
|
+
return flattenViews(this.views)
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
isPopulated() {
|
|
91
|
+
return this.$isPopulated()
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
locale() {
|
|
95
|
+
return this.api.locale
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
context() {
|
|
99
|
+
return new DitoContext(this, { nested: false })
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
rootComponent() {
|
|
103
|
+
return this.$root.$refs.root
|
|
104
|
+
},
|
|
105
|
+
|
|
106
|
+
// Use computed properties as links to injects, so DitoSchema can
|
|
107
|
+
// override the property and return `this` instead of the parent.
|
|
108
|
+
parentComponent() {
|
|
109
|
+
return this.$parentComponent()
|
|
110
|
+
},
|
|
111
|
+
|
|
112
|
+
schemaComponent() {
|
|
113
|
+
return this.$schemaComponent()
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
routeComponent() {
|
|
117
|
+
return this.$routeComponent()
|
|
118
|
+
},
|
|
119
|
+
|
|
120
|
+
formComponent() {
|
|
121
|
+
const component = this.routeComponent
|
|
122
|
+
return component?.isForm ? component : null
|
|
123
|
+
},
|
|
124
|
+
|
|
125
|
+
viewComponent() {
|
|
126
|
+
const component = this.routeComponent
|
|
127
|
+
return component?.isView ? component : null
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
// Returns the first route component in the chain of parents, including
|
|
131
|
+
// this current component, that is linked to a resource (and thus loads its
|
|
132
|
+
// own data and doesn't hold nested data).
|
|
133
|
+
dataComponent() {
|
|
134
|
+
return this.providesData ? this : this.$dataComponent()
|
|
135
|
+
},
|
|
136
|
+
|
|
137
|
+
sourceComponent() {
|
|
138
|
+
return this.$sourceComponent()
|
|
139
|
+
},
|
|
140
|
+
|
|
141
|
+
resourceComponent() {
|
|
142
|
+
return this.$resourceComponent()
|
|
143
|
+
},
|
|
144
|
+
|
|
145
|
+
dialogComponent() {
|
|
146
|
+
return this.$dialogComponent()
|
|
147
|
+
},
|
|
148
|
+
|
|
149
|
+
panelComponent() {
|
|
150
|
+
return this.$panelComponent()
|
|
151
|
+
},
|
|
152
|
+
|
|
153
|
+
tabComponent() {
|
|
154
|
+
return this.$tabComponent()
|
|
155
|
+
},
|
|
156
|
+
|
|
157
|
+
parentSchemaComponent() {
|
|
158
|
+
return getParentComponent(this, 'schemaComponent')
|
|
159
|
+
},
|
|
160
|
+
|
|
161
|
+
parentRouteComponent() {
|
|
162
|
+
return getParentComponent(this, 'routeComponent')
|
|
163
|
+
},
|
|
164
|
+
|
|
165
|
+
parentFormComponent() {
|
|
166
|
+
return getParentComponent(this, 'formComponent')
|
|
167
|
+
},
|
|
168
|
+
|
|
169
|
+
parentResourceComponent() {
|
|
170
|
+
return getParentComponent(this, 'resourceComponent')
|
|
171
|
+
},
|
|
172
|
+
|
|
173
|
+
// Returns the data of the first route component in the chain of parents
|
|
174
|
+
// that loads its own data from an associated API resource.
|
|
175
|
+
rootData() {
|
|
176
|
+
return this.dataComponent?.data
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
|
|
180
|
+
mounted() {
|
|
181
|
+
this.isMounted = true
|
|
182
|
+
},
|
|
183
|
+
|
|
184
|
+
beforeCreate() {
|
|
185
|
+
const uid = nextUid++
|
|
186
|
+
Object.defineProperty(this, '$uid', { get: () => uid })
|
|
187
|
+
},
|
|
188
|
+
|
|
189
|
+
methods: {
|
|
190
|
+
labelize,
|
|
191
|
+
|
|
192
|
+
// The state of components is only available during the life-cycle of a
|
|
193
|
+
// component. Some information we need available longer than that, e.g.
|
|
194
|
+
// `query` & `total` on TypeList, so that when the user navigates back from
|
|
195
|
+
// editing an item in the list, the state of the list is still the same.
|
|
196
|
+
// We can't store this in `data`, as this is already the pure data from the
|
|
197
|
+
// API server. That's what the `store` is for: Memory that's available as
|
|
198
|
+
// long as the current editing path is still valid. For type components,
|
|
199
|
+
// this memory is provided by the parent, see RouteMixin and DitoPane.
|
|
200
|
+
getStore(key) {
|
|
201
|
+
return this.store[key]
|
|
202
|
+
},
|
|
203
|
+
|
|
204
|
+
setStore(key, value) {
|
|
205
|
+
this.store[key] = value
|
|
206
|
+
return value
|
|
207
|
+
},
|
|
208
|
+
|
|
209
|
+
removeStore(key) {
|
|
210
|
+
delete this.store[key]
|
|
211
|
+
},
|
|
212
|
+
|
|
213
|
+
getStoreKeyByIndex(index) {
|
|
214
|
+
return this.store.$keysByIndex?.[index]
|
|
215
|
+
},
|
|
216
|
+
|
|
217
|
+
setStoreKeyByIndex(index, key) {
|
|
218
|
+
this.store.$keysByIndex ??= {}
|
|
219
|
+
this.store.$keysByIndex[index] = key
|
|
220
|
+
},
|
|
221
|
+
|
|
222
|
+
getChildStore(key, index) {
|
|
223
|
+
let store = this.getStore(key)
|
|
224
|
+
if (!store && index != null) {
|
|
225
|
+
// When storing, temporary ids change to permanent ones and thus the key
|
|
226
|
+
// can change. To still find the store, we reference by index as well,
|
|
227
|
+
// to be able to find the store again after the item was saved.
|
|
228
|
+
const oldKey = this.getStoreKeyByIndex(index)
|
|
229
|
+
store = this.getStore(oldKey)
|
|
230
|
+
if (store) {
|
|
231
|
+
this.setStore(key, store)
|
|
232
|
+
this.removeStore(oldKey)
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if (!store) {
|
|
236
|
+
store = this.setStore(key, reactive({}))
|
|
237
|
+
}
|
|
238
|
+
if (index != null) {
|
|
239
|
+
// temporary uid keys will change between persistence, so we need to
|
|
240
|
+
// assign the key to the index even when the store already existed.
|
|
241
|
+
this.setStoreKeyByIndex(index, key)
|
|
242
|
+
}
|
|
243
|
+
return store
|
|
244
|
+
},
|
|
245
|
+
|
|
246
|
+
removeChildStore(key, index) {
|
|
247
|
+
// GEt the child-store first, so that indices can be transferred over
|
|
248
|
+
// temporary id changes during persistence.
|
|
249
|
+
this.getChildStore(key, index)
|
|
250
|
+
this.removeStore(key)
|
|
251
|
+
},
|
|
252
|
+
|
|
253
|
+
getSchemaValue(
|
|
254
|
+
keyOrDataPath,
|
|
255
|
+
{
|
|
256
|
+
type,
|
|
257
|
+
default: def,
|
|
258
|
+
schema = this.schema,
|
|
259
|
+
context = this.context,
|
|
260
|
+
callback = true
|
|
261
|
+
} = {}
|
|
262
|
+
) {
|
|
263
|
+
return getSchemaValue(keyOrDataPath, {
|
|
264
|
+
type,
|
|
265
|
+
schema,
|
|
266
|
+
context,
|
|
267
|
+
callback,
|
|
268
|
+
default: isFunction(def) ? () => def.call(this) : def
|
|
269
|
+
})
|
|
270
|
+
},
|
|
271
|
+
|
|
272
|
+
getLabel(schema, name) {
|
|
273
|
+
return schema
|
|
274
|
+
? this.getSchemaValue('label', { schema, type: [String, Object] }) ||
|
|
275
|
+
labelize(name || schema.name)
|
|
276
|
+
: labelize(name) || ''
|
|
277
|
+
},
|
|
278
|
+
|
|
279
|
+
getButtonAttributes(verb) {
|
|
280
|
+
return {
|
|
281
|
+
class: `dito-button--${verb}`,
|
|
282
|
+
title: labelize(verb)
|
|
283
|
+
}
|
|
284
|
+
},
|
|
285
|
+
|
|
286
|
+
// TODO: Rename *Link() to *Route().
|
|
287
|
+
getQueryLink(query) {
|
|
288
|
+
return {
|
|
289
|
+
query,
|
|
290
|
+
// Preserve hash for tabs:
|
|
291
|
+
hash: this.$route.hash
|
|
292
|
+
}
|
|
293
|
+
},
|
|
294
|
+
|
|
295
|
+
shouldRenderSchema(schema = null) {
|
|
296
|
+
return shouldRenderSchema(schema, this.context)
|
|
297
|
+
},
|
|
298
|
+
|
|
299
|
+
shouldShowSchema(schema = null) {
|
|
300
|
+
return this.getSchemaValue('visible', {
|
|
301
|
+
type: Boolean,
|
|
302
|
+
default: true,
|
|
303
|
+
schema
|
|
304
|
+
})
|
|
305
|
+
},
|
|
306
|
+
|
|
307
|
+
shouldDisableSchema(schema = null) {
|
|
308
|
+
return this.getSchemaValue('disabled', {
|
|
309
|
+
type: Boolean,
|
|
310
|
+
default: false,
|
|
311
|
+
schema
|
|
312
|
+
})
|
|
313
|
+
},
|
|
314
|
+
|
|
315
|
+
getResourcePath(resource) {
|
|
316
|
+
resource = getResource(resource, {
|
|
317
|
+
// Resources without a parent inherit the one from `dataComponent`
|
|
318
|
+
// automatically.
|
|
319
|
+
parent: this.dataComponent?.getResource({
|
|
320
|
+
method: resource?.method,
|
|
321
|
+
child: resource
|
|
322
|
+
}) ?? null
|
|
323
|
+
})
|
|
324
|
+
return this.api.resources.any(resource)
|
|
325
|
+
},
|
|
326
|
+
|
|
327
|
+
getResourceUrl(resource) {
|
|
328
|
+
const url = this.getResourcePath(resource)
|
|
329
|
+
return url ? this.api.getApiUrl({ url, query: resource.query }) : null
|
|
330
|
+
},
|
|
331
|
+
|
|
332
|
+
async sendRequest({
|
|
333
|
+
method,
|
|
334
|
+
url,
|
|
335
|
+
resource,
|
|
336
|
+
query,
|
|
337
|
+
data,
|
|
338
|
+
signal,
|
|
339
|
+
internal
|
|
340
|
+
}) {
|
|
341
|
+
url ||= this.getResourceUrl(resource)
|
|
342
|
+
method ||= resource?.method
|
|
343
|
+
const checkUser = !internal && this.api.isApiUrl(url)
|
|
344
|
+
if (checkUser) {
|
|
345
|
+
await this.rootComponent.ensureUser()
|
|
346
|
+
}
|
|
347
|
+
const response = await this.api.request({
|
|
348
|
+
method,
|
|
349
|
+
url,
|
|
350
|
+
query,
|
|
351
|
+
data,
|
|
352
|
+
signal
|
|
353
|
+
})
|
|
354
|
+
// Detect change of the own user, and fetch it again if it was changed.
|
|
355
|
+
if (
|
|
356
|
+
checkUser &&
|
|
357
|
+
method === 'patch' &&
|
|
358
|
+
equals(resource, getMemberResource(this.user.id, this.api.users))
|
|
359
|
+
) {
|
|
360
|
+
await this.rootComponent.fetchUser()
|
|
361
|
+
}
|
|
362
|
+
return response
|
|
363
|
+
},
|
|
364
|
+
|
|
365
|
+
showDialog({ components, buttons, data, settings }) {
|
|
366
|
+
return this.rootComponent.showDialog({
|
|
367
|
+
components,
|
|
368
|
+
buttons,
|
|
369
|
+
data,
|
|
370
|
+
settings
|
|
371
|
+
})
|
|
372
|
+
},
|
|
373
|
+
|
|
374
|
+
request({ cache, ...options }) {
|
|
375
|
+
// Allow caching of loaded data on two levels:
|
|
376
|
+
// - 'global': cache globally, for the entire admin session
|
|
377
|
+
// - 'local': cache locally within the closest route component that is
|
|
378
|
+
// associated with a resource and loads its own data.
|
|
379
|
+
const cacheParent = (
|
|
380
|
+
cache &&
|
|
381
|
+
{
|
|
382
|
+
global: this.appState,
|
|
383
|
+
local: this.dataComponent
|
|
384
|
+
}[cache]
|
|
385
|
+
)
|
|
386
|
+
const loadCache = cacheParent?.loadCache
|
|
387
|
+
// Build a cache key from the config:
|
|
388
|
+
const cacheKey = (
|
|
389
|
+
loadCache &&
|
|
390
|
+
`${
|
|
391
|
+
options.method || 'get'
|
|
392
|
+
} ${
|
|
393
|
+
options.url
|
|
394
|
+
} ${
|
|
395
|
+
JSON.stringify(options.query || '')
|
|
396
|
+
} ${
|
|
397
|
+
JSON.stringify(options.data || '')
|
|
398
|
+
}`
|
|
399
|
+
)
|
|
400
|
+
if (loadCache && (cacheKey in loadCache)) {
|
|
401
|
+
return loadCache[cacheKey]
|
|
402
|
+
}
|
|
403
|
+
// NOTE: No await here, res is a promise that we can easily cache.
|
|
404
|
+
// That's fine because promises can be resolved over and over again.
|
|
405
|
+
const res = this.sendRequest(options)
|
|
406
|
+
.then(response => response.data)
|
|
407
|
+
.catch(error => {
|
|
408
|
+
// Convert axios errors to normal errors
|
|
409
|
+
const data = error.response?.data
|
|
410
|
+
throw data
|
|
411
|
+
? Object.assign(new Error(data.message), data)
|
|
412
|
+
: error
|
|
413
|
+
})
|
|
414
|
+
if (loadCache) {
|
|
415
|
+
loadCache[cacheKey] = res
|
|
416
|
+
}
|
|
417
|
+
return res
|
|
418
|
+
},
|
|
419
|
+
|
|
420
|
+
format(value, {
|
|
421
|
+
locale = this.api.locale,
|
|
422
|
+
defaults = this.api.formats,
|
|
423
|
+
...options
|
|
424
|
+
} = {}) {
|
|
425
|
+
return format(value, {
|
|
426
|
+
locale,
|
|
427
|
+
defaults,
|
|
428
|
+
...options
|
|
429
|
+
})
|
|
430
|
+
},
|
|
431
|
+
|
|
432
|
+
async navigate(location) {
|
|
433
|
+
return this.$router.push(location)
|
|
434
|
+
},
|
|
435
|
+
|
|
436
|
+
download(options = {}) {
|
|
437
|
+
if (isString(options)) {
|
|
438
|
+
options = { url: options }
|
|
439
|
+
}
|
|
440
|
+
// See: https://stackoverflow.com/a/49917066/1163708
|
|
441
|
+
const a = document.createElement('a')
|
|
442
|
+
a.href = options.url?.startsWith('blob:')
|
|
443
|
+
? options.url
|
|
444
|
+
: this.api.getApiUrl(options)
|
|
445
|
+
a.download = options.filename ?? null
|
|
446
|
+
const { body } = document
|
|
447
|
+
body.appendChild(a)
|
|
448
|
+
a.click()
|
|
449
|
+
body.removeChild(a)
|
|
450
|
+
},
|
|
451
|
+
|
|
452
|
+
notify(options) {
|
|
453
|
+
this.rootComponent.notify(options)
|
|
454
|
+
},
|
|
455
|
+
|
|
456
|
+
closeNotifications() {
|
|
457
|
+
this.rootComponent.closeNotifications()
|
|
458
|
+
},
|
|
459
|
+
|
|
460
|
+
setupSchemaFields() {
|
|
461
|
+
this.setupMethods()
|
|
462
|
+
this.setupComputed()
|
|
463
|
+
this.setupEvents()
|
|
464
|
+
},
|
|
465
|
+
|
|
466
|
+
setupMethods() {
|
|
467
|
+
for (const [key, value] of Object.entries(this.schema.methods || {})) {
|
|
468
|
+
if (isFunction(value)) {
|
|
469
|
+
this[key] = value
|
|
470
|
+
} else {
|
|
471
|
+
console.error(`Invalid method definition: ${key}: ${value}`)
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
},
|
|
475
|
+
|
|
476
|
+
setupComputed() {
|
|
477
|
+
const getComputedAccessor = ({ get, set }) => {
|
|
478
|
+
const getter = computed(() => get.call(this))
|
|
479
|
+
return {
|
|
480
|
+
get: () => getter.value,
|
|
481
|
+
set: set ? value => set.call(this, value) : undefined
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
for (const [key, item] of Object.entries(this.schema.computed || {})) {
|
|
486
|
+
const accessor = isFunction(item)
|
|
487
|
+
? getComputedAccessor({ get: item })
|
|
488
|
+
: isObject(item) && isFunction(item.get)
|
|
489
|
+
? getComputedAccessor(item)
|
|
490
|
+
: null
|
|
491
|
+
if (accessor) {
|
|
492
|
+
Object.defineProperty(this, key, accessor)
|
|
493
|
+
} else {
|
|
494
|
+
console.error(
|
|
495
|
+
`Invalid computed property definition: ${key}: ${item}`
|
|
496
|
+
)
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
},
|
|
500
|
+
|
|
501
|
+
setupEvents() {
|
|
502
|
+
const { watch, events } = this.schema
|
|
503
|
+
if (watch) {
|
|
504
|
+
const handlers = isFunction(watch) ? watch.call(this) : watch
|
|
505
|
+
if (isObject(handlers)) {
|
|
506
|
+
// Install the watch handlers in the next tick, so all components are
|
|
507
|
+
// initialized and we can check against their names.
|
|
508
|
+
this.$nextTick(() => {
|
|
509
|
+
for (const [key, callback] of Object.entries(handlers)) {
|
|
510
|
+
// Expand property names to 'data.property':
|
|
511
|
+
const expr = this.schemaComponent.getComponentByName(key)
|
|
512
|
+
? `data.${key}`
|
|
513
|
+
: key
|
|
514
|
+
this.$watch(expr, callback)
|
|
515
|
+
}
|
|
516
|
+
})
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
const addEvent = (key, event, callback) => {
|
|
521
|
+
if (isFunction(callback)) {
|
|
522
|
+
this.on(hyphenate(event), callback)
|
|
523
|
+
} else {
|
|
524
|
+
console.error(`Invalid event definition: ${key}: ${callback}`)
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
if (events) {
|
|
529
|
+
for (const [key, value] of Object.entries(events)) {
|
|
530
|
+
addEvent(key, key, value)
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
// Also scan schema for `on[A-Z]`-style callbacks and add them
|
|
534
|
+
// TODO: Deprecate one format or the other, in favour of only one way of
|
|
535
|
+
// doing things. Decide which one to remove.
|
|
536
|
+
for (const [key, value] of Object.entries(this.schema)) {
|
|
537
|
+
if (/^on[A-Z]/.test(key)) {
|
|
538
|
+
addEvent(key, key.slice(2), value)
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
},
|
|
542
|
+
|
|
543
|
+
emitEvent(event, {
|
|
544
|
+
context = null,
|
|
545
|
+
parent = null
|
|
546
|
+
} = {}) {
|
|
547
|
+
const hasListeners = this.hasListeners(event)
|
|
548
|
+
const parentHasListeners = parent?.hasListeners(event)
|
|
549
|
+
if (hasListeners || parentHasListeners) {
|
|
550
|
+
const emitEvent = target =>
|
|
551
|
+
target.emit(event, (context = DitoContext.get(this, context)))
|
|
552
|
+
|
|
553
|
+
const handleParentListeners = result =>
|
|
554
|
+
// Don't bubble to parent if handled event returned `false`
|
|
555
|
+
parentHasListeners && result !== false
|
|
556
|
+
? emitEvent(parent).then(() => result)
|
|
557
|
+
: result
|
|
558
|
+
|
|
559
|
+
const handleListeners = () =>
|
|
560
|
+
hasListeners
|
|
561
|
+
? emitEvent(this).then(handleParentListeners)
|
|
562
|
+
: handleParentListeners(undefined)
|
|
563
|
+
|
|
564
|
+
return ['load', 'change'].includes(event)
|
|
565
|
+
? // The effects of some events need time to propagate through Vue.
|
|
566
|
+
// Use $nextTick() to make sure our handlers see these changes.
|
|
567
|
+
// For example, `processedItem` is only correct after components
|
|
568
|
+
// that are newly rendered due to data changes have registered.
|
|
569
|
+
// NOTE: The result of `handleListeners()` makes it through the
|
|
570
|
+
// `$nextTick()` call and will be returned as expected.
|
|
571
|
+
this.$nextTick(handleListeners)
|
|
572
|
+
: handleListeners()
|
|
573
|
+
}
|
|
574
|
+
},
|
|
575
|
+
|
|
576
|
+
emitSchemaEvent(event, params) {
|
|
577
|
+
return this.schemaComponent.emitEvent(event, params)
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
let nextUid = 0
|
|
583
|
+
|
|
584
|
+
function getParentComponent(component, key) {
|
|
585
|
+
const current = component[key]
|
|
586
|
+
let parent = component.parentComponent
|
|
587
|
+
while (parent && parent[key] === current) {
|
|
588
|
+
parent = parent.parentComponent
|
|
589
|
+
}
|
|
590
|
+
return parent?.[key] ?? null
|
|
591
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { isObject } from '@ditojs/utils'
|
|
2
|
+
import { addEvents } from '@ditojs/ui/src'
|
|
3
|
+
|
|
4
|
+
// @vue/component
|
|
5
|
+
export default {
|
|
6
|
+
data() {
|
|
7
|
+
return {
|
|
8
|
+
domHandlers: []
|
|
9
|
+
}
|
|
10
|
+
},
|
|
11
|
+
|
|
12
|
+
unmounted() {
|
|
13
|
+
for (const { remove } of this.domHandlers) {
|
|
14
|
+
remove()
|
|
15
|
+
}
|
|
16
|
+
this.domHandlers = []
|
|
17
|
+
},
|
|
18
|
+
|
|
19
|
+
methods: {
|
|
20
|
+
domOn(element, type, handler) {
|
|
21
|
+
const result = addEvents(
|
|
22
|
+
element,
|
|
23
|
+
isObject(type) ? type : { [type]: handler }
|
|
24
|
+
)
|
|
25
|
+
this.domHandlers.push(result)
|
|
26
|
+
return result
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|