@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,734 @@
|
|
|
1
|
+
import DitoComponent from '../DitoComponent.js'
|
|
2
|
+
import ItemMixin from './ItemMixin.js'
|
|
3
|
+
import ResourceMixin from './ResourceMixin.js'
|
|
4
|
+
import SchemaParentMixin from '../mixins/SchemaParentMixin.js'
|
|
5
|
+
import { getSchemaAccessor, getStoreAccessor } from '../utils/accessor.js'
|
|
6
|
+
import { getMemberResource } from '../utils/resource.js'
|
|
7
|
+
import { replaceRoute } from '../utils/route.js'
|
|
8
|
+
import {
|
|
9
|
+
processRouteSchema,
|
|
10
|
+
processForms,
|
|
11
|
+
getNamedSchemas,
|
|
12
|
+
getButtonSchemas,
|
|
13
|
+
hasFormSchema,
|
|
14
|
+
getFormSchemas,
|
|
15
|
+
getViewSchema,
|
|
16
|
+
getViewPath,
|
|
17
|
+
isCompact,
|
|
18
|
+
isInlined,
|
|
19
|
+
isObjectSource,
|
|
20
|
+
isListSource
|
|
21
|
+
} from '../utils/schema.js'
|
|
22
|
+
import {
|
|
23
|
+
isObject,
|
|
24
|
+
isString,
|
|
25
|
+
isArray,
|
|
26
|
+
isNumber,
|
|
27
|
+
equals,
|
|
28
|
+
parseDataPath,
|
|
29
|
+
normalizeDataPath
|
|
30
|
+
} from '@ditojs/utils'
|
|
31
|
+
import { raw } from '@ditojs/ui'
|
|
32
|
+
|
|
33
|
+
// @vue/component
|
|
34
|
+
export default {
|
|
35
|
+
mixins: [ItemMixin, ResourceMixin, SchemaParentMixin],
|
|
36
|
+
|
|
37
|
+
defaultValue: context => (isListSource(context.schema) ? [] : null),
|
|
38
|
+
// Exclude all sources that have their own resource handling the data.
|
|
39
|
+
excludeValue: context => !!context.schema.resource,
|
|
40
|
+
|
|
41
|
+
provide() {
|
|
42
|
+
return {
|
|
43
|
+
$sourceComponent: () => this
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
data() {
|
|
48
|
+
return {
|
|
49
|
+
wrappedPrimitives: null,
|
|
50
|
+
unwrappingPrimitives: raw(false)
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
computed: {
|
|
55
|
+
sourceComponent() {
|
|
56
|
+
return this
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
isObjectSource() {
|
|
60
|
+
return isObjectSource(this.type)
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
isListSource() {
|
|
64
|
+
return isListSource(this.type)
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
// @override ResourceMixin.hasData()
|
|
68
|
+
hasData() {
|
|
69
|
+
return !!this.value
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
shouldRender() {
|
|
73
|
+
return this.sourceDepth < this.maxDepth
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
isReady() {
|
|
77
|
+
// Lists that have no data and no associated resource should still render,
|
|
78
|
+
// as they may be getting their data elsewhere, e.g. `compute()`.
|
|
79
|
+
return (
|
|
80
|
+
this.shouldRender &&
|
|
81
|
+
(this.hasData || !this.providesData)
|
|
82
|
+
)
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
isInView() {
|
|
86
|
+
return !!this.viewComponent
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
wrapPrimitives() {
|
|
90
|
+
return this.schema.wrapPrimitives
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
listData: {
|
|
94
|
+
get() {
|
|
95
|
+
let data = this.value
|
|
96
|
+
if (this.isObjectSource) {
|
|
97
|
+
// Convert to list array.
|
|
98
|
+
data = data != null ? [data] : []
|
|
99
|
+
} else {
|
|
100
|
+
// If data gets inherited from parent, unwrapping is not happening
|
|
101
|
+
// at the root in `setData()`, but here instead.
|
|
102
|
+
data = this.unwrapListData(data) || data
|
|
103
|
+
}
|
|
104
|
+
data ||= []
|
|
105
|
+
const { wrapPrimitives } = this
|
|
106
|
+
if (wrapPrimitives) {
|
|
107
|
+
if (this.unwrappingPrimitives.value) {
|
|
108
|
+
// We're done unwrapping once `listData` is reevaluated, so set
|
|
109
|
+
// this to `false` again. See `wrappedPrimitives` watcher above.
|
|
110
|
+
// TODO: Fix side-effects
|
|
111
|
+
// eslint-disable-next-line max-len
|
|
112
|
+
// eslint-disable-next-line vue/no-side-effects-in-computed-properties
|
|
113
|
+
this.unwrappingPrimitives.value = false
|
|
114
|
+
} else {
|
|
115
|
+
// Convert data to a list of wrapped primitives, and return it.
|
|
116
|
+
// TODO: Fix side-effects
|
|
117
|
+
// eslint-disable-next-line max-len
|
|
118
|
+
// eslint-disable-next-line vue/no-side-effects-in-computed-properties
|
|
119
|
+
this.wrappedPrimitives = data.map(value => ({
|
|
120
|
+
[wrapPrimitives]: value
|
|
121
|
+
}))
|
|
122
|
+
}
|
|
123
|
+
return this.wrappedPrimitives
|
|
124
|
+
}
|
|
125
|
+
return data
|
|
126
|
+
},
|
|
127
|
+
|
|
128
|
+
set(data) {
|
|
129
|
+
if (this.wrapPrimitives) {
|
|
130
|
+
this.wrappedPrimitives = data
|
|
131
|
+
} else {
|
|
132
|
+
this.value = this.isObjectSource
|
|
133
|
+
? data?.length > 0
|
|
134
|
+
? data[0]
|
|
135
|
+
: null
|
|
136
|
+
: data
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
|
|
141
|
+
objectData: {
|
|
142
|
+
get() {
|
|
143
|
+
// Always go through `listData` internally, which does all the
|
|
144
|
+
// processing of `wrapPrimitives`, etc.
|
|
145
|
+
return this.listData[0] || null
|
|
146
|
+
},
|
|
147
|
+
|
|
148
|
+
set(data) {
|
|
149
|
+
this.listData = data ? [data] : []
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
|
|
153
|
+
sourceSchema() {
|
|
154
|
+
// The sourceSchema of a list is the list's schema itself.
|
|
155
|
+
return this.schema
|
|
156
|
+
},
|
|
157
|
+
|
|
158
|
+
sourceDepth() {
|
|
159
|
+
return this.$route.matched.reduce(
|
|
160
|
+
(depth, record) => (
|
|
161
|
+
depth + (record.meta.schema === this.sourceSchema ? 1 : 0)
|
|
162
|
+
),
|
|
163
|
+
0
|
|
164
|
+
)
|
|
165
|
+
},
|
|
166
|
+
|
|
167
|
+
path() {
|
|
168
|
+
// This is used in TypeList for DitoFormChooser.
|
|
169
|
+
return this.routeComponent.getChildPath(this.schema.path)
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
defaultQuery() {
|
|
173
|
+
const { defaultOrder: order } = this
|
|
174
|
+
return order ? { order } : {}
|
|
175
|
+
},
|
|
176
|
+
|
|
177
|
+
query: getStoreAccessor('query', {
|
|
178
|
+
get(query) {
|
|
179
|
+
return {
|
|
180
|
+
...this.defaultQuery,
|
|
181
|
+
...query
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
|
|
185
|
+
set(query) {
|
|
186
|
+
// Always keep the displayed query parameters in sync with the stored
|
|
187
|
+
// ones. Use scope and page from the list schema as defaults, but allow
|
|
188
|
+
// the route query parameters to override them.
|
|
189
|
+
const {
|
|
190
|
+
scope = this.defaultScope?.name,
|
|
191
|
+
page = this.schema.page,
|
|
192
|
+
type
|
|
193
|
+
} = this.query
|
|
194
|
+
// Preserve / merge currently stored values, including any custom query
|
|
195
|
+
// parameters added by creatable.query
|
|
196
|
+
query = {
|
|
197
|
+
...this.query,
|
|
198
|
+
...(scope != null && { scope }),
|
|
199
|
+
...(page != null && { page }),
|
|
200
|
+
...(type != null && { type }),
|
|
201
|
+
...query
|
|
202
|
+
}
|
|
203
|
+
if (!equals(query, this.$route.query)) {
|
|
204
|
+
// Change the route query parameters, but don't trigger a route
|
|
205
|
+
// change, as that would cause the list to reload.
|
|
206
|
+
replaceRoute({ query })
|
|
207
|
+
}
|
|
208
|
+
return query // Let getStoreAccessor() do the actual setting
|
|
209
|
+
}
|
|
210
|
+
}),
|
|
211
|
+
|
|
212
|
+
total: getStoreAccessor('total'),
|
|
213
|
+
|
|
214
|
+
columns() {
|
|
215
|
+
return getNamedSchemas(this.schema.columns)
|
|
216
|
+
},
|
|
217
|
+
|
|
218
|
+
scopes() {
|
|
219
|
+
return getNamedSchemas(this.schema.scopes)
|
|
220
|
+
},
|
|
221
|
+
|
|
222
|
+
defaultScope() {
|
|
223
|
+
let first = null
|
|
224
|
+
if (this.scopes) {
|
|
225
|
+
for (const scope of Object.values(this.scopes)) {
|
|
226
|
+
if (scope.defaultScope) {
|
|
227
|
+
return scope
|
|
228
|
+
}
|
|
229
|
+
if (!first) {
|
|
230
|
+
first = scope
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return first
|
|
235
|
+
},
|
|
236
|
+
|
|
237
|
+
defaultOrder() {
|
|
238
|
+
if (this.columns) {
|
|
239
|
+
for (const column of Object.values(this.columns)) {
|
|
240
|
+
const { defaultSort } = column
|
|
241
|
+
if (defaultSort) {
|
|
242
|
+
const direction = isString(defaultSort) ? defaultSort : 'asc'
|
|
243
|
+
return `${column.name} ${direction}`
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return null
|
|
248
|
+
},
|
|
249
|
+
|
|
250
|
+
nestedMeta() {
|
|
251
|
+
return {
|
|
252
|
+
...this.meta,
|
|
253
|
+
schema: this.schema
|
|
254
|
+
}
|
|
255
|
+
},
|
|
256
|
+
|
|
257
|
+
forms() {
|
|
258
|
+
return Object.values(getFormSchemas(this.schema, this.context))
|
|
259
|
+
},
|
|
260
|
+
|
|
261
|
+
// Returns the linked view schema if this source edits it its items through
|
|
262
|
+
// a linked view.
|
|
263
|
+
view() {
|
|
264
|
+
return getViewSchema(this.schema, this.context)
|
|
265
|
+
},
|
|
266
|
+
|
|
267
|
+
linksToView() {
|
|
268
|
+
return !!this.view
|
|
269
|
+
},
|
|
270
|
+
|
|
271
|
+
buttonSchemas() {
|
|
272
|
+
return getButtonSchemas(this.schema.buttons)
|
|
273
|
+
},
|
|
274
|
+
|
|
275
|
+
isCompact() {
|
|
276
|
+
return this.forms.every(isCompact)
|
|
277
|
+
},
|
|
278
|
+
|
|
279
|
+
isInlined() {
|
|
280
|
+
return isInlined(this.schema)
|
|
281
|
+
},
|
|
282
|
+
|
|
283
|
+
paginate: getSchemaAccessor('paginate', {
|
|
284
|
+
type: Number
|
|
285
|
+
}),
|
|
286
|
+
|
|
287
|
+
render: getSchemaAccessor('render', {
|
|
288
|
+
type: Function,
|
|
289
|
+
default: null
|
|
290
|
+
}),
|
|
291
|
+
|
|
292
|
+
creatable: getSchemaAccessor('creatable', {
|
|
293
|
+
type: Boolean,
|
|
294
|
+
default: false,
|
|
295
|
+
get(creatable) {
|
|
296
|
+
return creatable && hasFormSchema(this.schema)
|
|
297
|
+
? this.isObjectSource
|
|
298
|
+
? !this.value
|
|
299
|
+
: true
|
|
300
|
+
: false
|
|
301
|
+
}
|
|
302
|
+
}),
|
|
303
|
+
|
|
304
|
+
editable: getSchemaAccessor('editable', {
|
|
305
|
+
type: Boolean,
|
|
306
|
+
default: false,
|
|
307
|
+
get(editable) {
|
|
308
|
+
return editable && !this.isInlined
|
|
309
|
+
}
|
|
310
|
+
}),
|
|
311
|
+
|
|
312
|
+
deletable: getSchemaAccessor('deletable', {
|
|
313
|
+
type: Boolean,
|
|
314
|
+
default: false
|
|
315
|
+
}),
|
|
316
|
+
|
|
317
|
+
draggable: getSchemaAccessor('draggable', {
|
|
318
|
+
type: Boolean,
|
|
319
|
+
default: false,
|
|
320
|
+
get(draggable) {
|
|
321
|
+
return this.isListSource && this.listData.length > 1 && draggable
|
|
322
|
+
}
|
|
323
|
+
}),
|
|
324
|
+
|
|
325
|
+
collapsible: getSchemaAccessor('collapsible', {
|
|
326
|
+
type: Boolean,
|
|
327
|
+
default: false,
|
|
328
|
+
get(collapsible) {
|
|
329
|
+
return collapsible && this.isInlined
|
|
330
|
+
}
|
|
331
|
+
}),
|
|
332
|
+
|
|
333
|
+
collapsed: getSchemaAccessor('collapsed', {
|
|
334
|
+
type: Boolean,
|
|
335
|
+
default: false,
|
|
336
|
+
get(collapsed) {
|
|
337
|
+
return collapsed && this.collapsible
|
|
338
|
+
}
|
|
339
|
+
}),
|
|
340
|
+
|
|
341
|
+
maxDepth: getSchemaAccessor('maxDepth', {
|
|
342
|
+
type: Number,
|
|
343
|
+
default: 1
|
|
344
|
+
}),
|
|
345
|
+
|
|
346
|
+
createPath() {
|
|
347
|
+
if (this.creatable) {
|
|
348
|
+
return (
|
|
349
|
+
getViewPath(this.schema, this.context) ||
|
|
350
|
+
this.path
|
|
351
|
+
)
|
|
352
|
+
}
|
|
353
|
+
return null
|
|
354
|
+
}
|
|
355
|
+
},
|
|
356
|
+
|
|
357
|
+
watch: {
|
|
358
|
+
$route: {
|
|
359
|
+
// https://github.com/vuejs/vue-router/issues/3393#issuecomment-1158470149
|
|
360
|
+
flush: 'post',
|
|
361
|
+
handler(to, from) {
|
|
362
|
+
if (this.providesData) {
|
|
363
|
+
if (
|
|
364
|
+
from.path === to.path &&
|
|
365
|
+
from.hash === to.hash
|
|
366
|
+
) {
|
|
367
|
+
// Paths and hashes remain the same, so only queries have changed.
|
|
368
|
+
// Update filter and reload data without clearing.
|
|
369
|
+
this.query = to.query
|
|
370
|
+
this.loadData(false)
|
|
371
|
+
} else if (
|
|
372
|
+
this.meta.reload &&
|
|
373
|
+
from.path !== to.path &&
|
|
374
|
+
from.path.startsWith(to.path)
|
|
375
|
+
) {
|
|
376
|
+
// Reload the source when navigating back to a parent-route after
|
|
377
|
+
// changing data in a child-route.
|
|
378
|
+
this.meta.reload = false
|
|
379
|
+
this.loadData(false)
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
},
|
|
384
|
+
|
|
385
|
+
wrappedPrimitives: {
|
|
386
|
+
deep: true,
|
|
387
|
+
handler(to, from) {
|
|
388
|
+
const { wrapPrimitives } = this
|
|
389
|
+
// Skip the initial setting of wrappedPrimitives array
|
|
390
|
+
if (wrapPrimitives && from !== null) {
|
|
391
|
+
// Whenever the wrappedPrimitives change, map their values back to the
|
|
392
|
+
// array of primitives, in a primitive way :)
|
|
393
|
+
// But set `unwrappingPrimitives` to true, so the `listData` computed
|
|
394
|
+
// property knows about it, which sets it to `false` again.
|
|
395
|
+
this.unwrappingPrimitives.value = true
|
|
396
|
+
this.value = to.map(object => object[wrapPrimitives])
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
},
|
|
401
|
+
|
|
402
|
+
methods: {
|
|
403
|
+
setupData() {
|
|
404
|
+
this.query = this.$route.query
|
|
405
|
+
this.ensureData()
|
|
406
|
+
},
|
|
407
|
+
|
|
408
|
+
// @override ResourceMixin.clearData()
|
|
409
|
+
clearData() {
|
|
410
|
+
this.total = 0
|
|
411
|
+
this.value = null
|
|
412
|
+
},
|
|
413
|
+
|
|
414
|
+
// @override ResourceMixin.setData()
|
|
415
|
+
setData(data) {
|
|
416
|
+
// When new data is loaded, we can store it right back in the data of the
|
|
417
|
+
// view or form that created this list component.
|
|
418
|
+
// Support two formats for list data:
|
|
419
|
+
// - Array: `[...]`
|
|
420
|
+
// - Object: `{ results: [...], total }`, see `unwrapListData()`
|
|
421
|
+
if (
|
|
422
|
+
!data ||
|
|
423
|
+
this.isListSource && isArray(data) ||
|
|
424
|
+
this.isObjectSource && isObject(data)
|
|
425
|
+
) {
|
|
426
|
+
this.value = data
|
|
427
|
+
} else if (this.unwrapListData(data)) {
|
|
428
|
+
// The format didn't match, see if we received a `{ results, total }`
|
|
429
|
+
// object, in which case `this.value` was already set by
|
|
430
|
+
// `unwrapListData()` and we're done now.
|
|
431
|
+
} else if (isObject(data) && this.isInView) {
|
|
432
|
+
// The controller is sending data for a full multi-component view,
|
|
433
|
+
// including the nested list data.
|
|
434
|
+
this.viewComponent.setData(data)
|
|
435
|
+
}
|
|
436
|
+
},
|
|
437
|
+
|
|
438
|
+
unwrapListData(data) {
|
|
439
|
+
if (
|
|
440
|
+
this.isListSource &&
|
|
441
|
+
isObject(data) &&
|
|
442
|
+
isNumber(data.total) &&
|
|
443
|
+
isArray(data.results)
|
|
444
|
+
) {
|
|
445
|
+
// If @ditojs/server sends data in the form of `{ results, total }`
|
|
446
|
+
// replace the value with result, but remember the total in the store.
|
|
447
|
+
this.total = data.total
|
|
448
|
+
this.value = data.results
|
|
449
|
+
return this.value
|
|
450
|
+
}
|
|
451
|
+
},
|
|
452
|
+
|
|
453
|
+
createItem(schema, type) {
|
|
454
|
+
const item = this.createData(schema, type)
|
|
455
|
+
if (this.isObjectSource) {
|
|
456
|
+
this.objectData = item
|
|
457
|
+
} else {
|
|
458
|
+
this.listData.push(item)
|
|
459
|
+
}
|
|
460
|
+
if (this.collapsible) {
|
|
461
|
+
this.$nextTick(() => this.openSchemaComponent(-1))
|
|
462
|
+
}
|
|
463
|
+
this.onChange()
|
|
464
|
+
return item
|
|
465
|
+
},
|
|
466
|
+
|
|
467
|
+
removeItem(item, index) {
|
|
468
|
+
let removed = false
|
|
469
|
+
if (this.isObjectSource) {
|
|
470
|
+
this.objectData = null
|
|
471
|
+
removed = true
|
|
472
|
+
} else {
|
|
473
|
+
const { listData } = this
|
|
474
|
+
if (index >= 0) {
|
|
475
|
+
listData.splice(index, 1)
|
|
476
|
+
removed = true
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
if (removed) {
|
|
480
|
+
this.removeItemStore(this.schema, item, index)
|
|
481
|
+
this.onChange()
|
|
482
|
+
}
|
|
483
|
+
},
|
|
484
|
+
|
|
485
|
+
deleteItem(item, index) {
|
|
486
|
+
const label = (
|
|
487
|
+
item &&
|
|
488
|
+
this.getItemLabel(this.schema, item, {
|
|
489
|
+
index,
|
|
490
|
+
extended: true
|
|
491
|
+
})
|
|
492
|
+
)
|
|
493
|
+
|
|
494
|
+
const notify = () =>
|
|
495
|
+
this.notify({
|
|
496
|
+
type: this.isTransient ? 'info' : 'success',
|
|
497
|
+
title: 'Successfully Removed',
|
|
498
|
+
text: [
|
|
499
|
+
`${label} was ${this.verbs.deleted}.`,
|
|
500
|
+
this.transientNote
|
|
501
|
+
]
|
|
502
|
+
})
|
|
503
|
+
|
|
504
|
+
if (
|
|
505
|
+
item &&
|
|
506
|
+
window.confirm(
|
|
507
|
+
`Do you really want to ${this.verbs.delete} ${label}?`
|
|
508
|
+
)
|
|
509
|
+
) {
|
|
510
|
+
if (this.isTransient) {
|
|
511
|
+
this.removeItem(item, index)
|
|
512
|
+
notify()
|
|
513
|
+
} else {
|
|
514
|
+
const itemId = this.getItemId(this.schema, item, index)
|
|
515
|
+
const method = 'delete'
|
|
516
|
+
const resource = getMemberResource(
|
|
517
|
+
itemId,
|
|
518
|
+
this.getResource({ method })
|
|
519
|
+
)
|
|
520
|
+
if (resource) {
|
|
521
|
+
this.handleRequest({ method, resource }, err => {
|
|
522
|
+
if (!err) {
|
|
523
|
+
this.removeItem(item, index)
|
|
524
|
+
notify()
|
|
525
|
+
}
|
|
526
|
+
this.reloadData()
|
|
527
|
+
})
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
},
|
|
532
|
+
|
|
533
|
+
getSchemaComponent(index) {
|
|
534
|
+
const { schemaComponents } = this
|
|
535
|
+
const { length } = schemaComponents
|
|
536
|
+
return schemaComponents[((index % length) + length) % length]
|
|
537
|
+
},
|
|
538
|
+
|
|
539
|
+
openSchemaComponent(index) {
|
|
540
|
+
const schemaComponent = this.getSchemaComponent(index)
|
|
541
|
+
if (schemaComponent) {
|
|
542
|
+
schemaComponent.opened = true
|
|
543
|
+
}
|
|
544
|
+
},
|
|
545
|
+
|
|
546
|
+
async navigateToComponent(dataPath, onComplete) {
|
|
547
|
+
if (this.collapsible) {
|
|
548
|
+
const index = dataPath.startsWith(this.dataPath)
|
|
549
|
+
? this.isListSource
|
|
550
|
+
? parseDataPath(dataPath.slice(this.dataPath.length + 1))[0] ?? null
|
|
551
|
+
: 0
|
|
552
|
+
: null
|
|
553
|
+
if (index !== null && isNumber(+index)) {
|
|
554
|
+
const schemaComponent = this.getSchemaComponent(+index)
|
|
555
|
+
if (schemaComponent) {
|
|
556
|
+
const { opened } = schemaComponent
|
|
557
|
+
if (!opened) {
|
|
558
|
+
schemaComponent.opened = true
|
|
559
|
+
await this.$nextTick()
|
|
560
|
+
}
|
|
561
|
+
const components = schemaComponent.getComponentsByDataPath(dataPath)
|
|
562
|
+
if (components.length > 0 && (onComplete?.(components) ?? true)) {
|
|
563
|
+
return true
|
|
564
|
+
} else {
|
|
565
|
+
schemaComponent.opened = opened
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
return this.navigateToRouteComponent(dataPath, onComplete)
|
|
571
|
+
},
|
|
572
|
+
|
|
573
|
+
navigateToRouteComponent(dataPath, onComplete) {
|
|
574
|
+
return new Promise((resolve, reject) => {
|
|
575
|
+
const callOnComplete = () => {
|
|
576
|
+
// Retrieve the last route component, which will be the component that
|
|
577
|
+
// we just navigated to, and pass it on to `onComplete()`
|
|
578
|
+
const { routeComponents } = this.appState
|
|
579
|
+
const routeComponent = routeComponents[routeComponents.length - 1]
|
|
580
|
+
resolve(onComplete?.([routeComponent]) ?? true)
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
const dataPathParts = parseDataPath(dataPath)
|
|
584
|
+
// See if we can find a route that can serve part of the given dataPath,
|
|
585
|
+
// and take it from there:
|
|
586
|
+
while (dataPathParts.length > 0) {
|
|
587
|
+
const path = this.routeComponent.getChildPath(
|
|
588
|
+
this.api.normalizePath(normalizeDataPath(dataPathParts))
|
|
589
|
+
)
|
|
590
|
+
// See if there actually is a route for this sub-component:
|
|
591
|
+
const { matched } = this.$router.resolve(path)
|
|
592
|
+
if (matched.length && matched[0].name !== 'catch-all') {
|
|
593
|
+
if (this.$route.path === path) {
|
|
594
|
+
// We're already there, so just call `onComplete()`:
|
|
595
|
+
callOnComplete()
|
|
596
|
+
} else {
|
|
597
|
+
// Navigate to the component's path, then call `onComplete()`_:
|
|
598
|
+
this.$router
|
|
599
|
+
.push({ path })
|
|
600
|
+
.catch(reject)
|
|
601
|
+
// Wait for the last route component to be mounted in the next
|
|
602
|
+
// tick before calling `onComplete()`
|
|
603
|
+
.then(() => {
|
|
604
|
+
this.$nextTick(callOnComplete)
|
|
605
|
+
})
|
|
606
|
+
}
|
|
607
|
+
return
|
|
608
|
+
}
|
|
609
|
+
// Keep removing the last part until we find a match.
|
|
610
|
+
dataPathParts.pop()
|
|
611
|
+
}
|
|
612
|
+
resolve(false)
|
|
613
|
+
})
|
|
614
|
+
}
|
|
615
|
+
}, // end of `methods`
|
|
616
|
+
|
|
617
|
+
async processSchema(
|
|
618
|
+
api,
|
|
619
|
+
schema,
|
|
620
|
+
name,
|
|
621
|
+
routes,
|
|
622
|
+
level,
|
|
623
|
+
nested = false,
|
|
624
|
+
flatten = false,
|
|
625
|
+
process = null
|
|
626
|
+
) {
|
|
627
|
+
processRouteSchema(api, schema, name)
|
|
628
|
+
const inlined = isInlined(schema)
|
|
629
|
+
if (inlined && schema.resource) {
|
|
630
|
+
throw new Error(
|
|
631
|
+
`Nested ${
|
|
632
|
+
this.isListSource
|
|
633
|
+
? 'lists'
|
|
634
|
+
: this.isObjectSource
|
|
635
|
+
? 'objects'
|
|
636
|
+
: 'schema'
|
|
637
|
+
} cannot load data from their own resources`
|
|
638
|
+
)
|
|
639
|
+
}
|
|
640
|
+
// Use differently named url parameters on each nested level for id as
|
|
641
|
+
// otherwise they would clash and override each other inside $route.params
|
|
642
|
+
// See: https://github.com/vuejs/vue-router/issues/1345
|
|
643
|
+
const param = `id${level + 1}`
|
|
644
|
+
const meta = {
|
|
645
|
+
api,
|
|
646
|
+
schema
|
|
647
|
+
}
|
|
648
|
+
const formMeta = {
|
|
649
|
+
...meta,
|
|
650
|
+
// When children are flattened (e.g. tree-lists), include the `flatten`
|
|
651
|
+
// setting also, for flattening below.
|
|
652
|
+
flatten,
|
|
653
|
+
nested,
|
|
654
|
+
param
|
|
655
|
+
}
|
|
656
|
+
const childRoutes = await processForms(api, schema, level)
|
|
657
|
+
if (process) {
|
|
658
|
+
await process(childRoutes, level + 1)
|
|
659
|
+
}
|
|
660
|
+
// Inlined forms don't need to actually add routes.
|
|
661
|
+
if (hasFormSchema(schema) && !inlined) {
|
|
662
|
+
// Lists in single-component-views (level === 0) use their view's path,
|
|
663
|
+
// while all others need their path prefixed with the parent's path:
|
|
664
|
+
const sourcePath = level === 0 ? '' : schema.path
|
|
665
|
+
const formRoute = {
|
|
666
|
+
path: getPathWithParam(
|
|
667
|
+
sourcePath,
|
|
668
|
+
// Object sources don't need id params in their form paths, as they
|
|
669
|
+
// directly edit one object.
|
|
670
|
+
isListSource(schema) ? param : null
|
|
671
|
+
),
|
|
672
|
+
component: DitoComponent.component(
|
|
673
|
+
nested ? 'DitoFormNested' : 'DitoForm'
|
|
674
|
+
),
|
|
675
|
+
meta: formMeta
|
|
676
|
+
}
|
|
677
|
+
if (isObjectSource(schema)) {
|
|
678
|
+
// Also add a param route, simply to handle '/create' links the same
|
|
679
|
+
// way that lists do, where it overlaps with :id for item ids.
|
|
680
|
+
routes.push({
|
|
681
|
+
...formRoute,
|
|
682
|
+
path: getPathWithParam(sourcePath, param)
|
|
683
|
+
})
|
|
684
|
+
}
|
|
685
|
+
if (sourcePath) {
|
|
686
|
+
// Just redirect back to the parent when a nested source route is hit.
|
|
687
|
+
routes.push({
|
|
688
|
+
path: sourcePath,
|
|
689
|
+
redirect: '.',
|
|
690
|
+
meta
|
|
691
|
+
})
|
|
692
|
+
}
|
|
693
|
+
// Partition childRoutes into those that need flattening (e.g. tree-lists)
|
|
694
|
+
// and those that don't, and process each group separately after.
|
|
695
|
+
const [flatRoutes, subRoutes] = childRoutes.reduce(
|
|
696
|
+
(res, route) => {
|
|
697
|
+
res[route.meta.flatten ? 0 : 1].push(route)
|
|
698
|
+
return res
|
|
699
|
+
},
|
|
700
|
+
[[], []]
|
|
701
|
+
)
|
|
702
|
+
if (subRoutes.length) {
|
|
703
|
+
formRoute.children = subRoutes
|
|
704
|
+
}
|
|
705
|
+
routes.push(formRoute)
|
|
706
|
+
// Add the prefixed formRoutes with their children for nested lists.
|
|
707
|
+
if (flatRoutes.length) {
|
|
708
|
+
for (const childRoute of flatRoutes) {
|
|
709
|
+
routes.push({
|
|
710
|
+
...(childRoute.redirect ? childRoute : formRoute),
|
|
711
|
+
path: `${formRoute.path}/${childRoute.path}`,
|
|
712
|
+
meta: {
|
|
713
|
+
...childRoute.meta,
|
|
714
|
+
flatten
|
|
715
|
+
}
|
|
716
|
+
})
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
},
|
|
721
|
+
|
|
722
|
+
processValue({ schema, value, dataPath }, graph) {
|
|
723
|
+
graph.addSource(dataPath, schema)
|
|
724
|
+
return value
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
function getPathWithParam(path, param) {
|
|
729
|
+
return param
|
|
730
|
+
? path
|
|
731
|
+
? `${path}/:${param}`
|
|
732
|
+
: `:${param}`
|
|
733
|
+
: path
|
|
734
|
+
}
|