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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (153) hide show
  1. package/README.md +180 -0
  2. package/dist/dito-admin.css +1 -0
  3. package/dist/dito-admin.es.js +12106 -0
  4. package/dist/dito-admin.umd.js +7 -0
  5. package/package.json +96 -0
  6. package/src/DitoAdmin.js +293 -0
  7. package/src/DitoComponent.js +34 -0
  8. package/src/DitoContext.js +318 -0
  9. package/src/DitoTypeComponent.js +42 -0
  10. package/src/DitoUser.js +12 -0
  11. package/src/appState.js +12 -0
  12. package/src/components/DitoAccount.vue +60 -0
  13. package/src/components/DitoAffix.vue +68 -0
  14. package/src/components/DitoAffixes.vue +200 -0
  15. package/src/components/DitoButtons.vue +80 -0
  16. package/src/components/DitoClipboard.vue +186 -0
  17. package/src/components/DitoContainer.vue +374 -0
  18. package/src/components/DitoCreateButton.vue +146 -0
  19. package/src/components/DitoDialog.vue +242 -0
  20. package/src/components/DitoDraggable.vue +117 -0
  21. package/src/components/DitoEditButtons.vue +135 -0
  22. package/src/components/DitoErrors.vue +83 -0
  23. package/src/components/DitoForm.vue +521 -0
  24. package/src/components/DitoFormInner.vue +26 -0
  25. package/src/components/DitoFormNested.vue +17 -0
  26. package/src/components/DitoHeader.vue +84 -0
  27. package/src/components/DitoLabel.vue +200 -0
  28. package/src/components/DitoMenu.vue +186 -0
  29. package/src/components/DitoNavigation.vue +40 -0
  30. package/src/components/DitoNotifications.vue +170 -0
  31. package/src/components/DitoPagination.vue +42 -0
  32. package/src/components/DitoPane.vue +334 -0
  33. package/src/components/DitoPanel.vue +256 -0
  34. package/src/components/DitoPanels.vue +61 -0
  35. package/src/components/DitoRoot.vue +524 -0
  36. package/src/components/DitoSchema.vue +846 -0
  37. package/src/components/DitoSchemaInlined.vue +97 -0
  38. package/src/components/DitoScopes.vue +76 -0
  39. package/src/components/DitoSidebar.vue +50 -0
  40. package/src/components/DitoSpinner.vue +95 -0
  41. package/src/components/DitoTableCell.vue +64 -0
  42. package/src/components/DitoTableHead.vue +121 -0
  43. package/src/components/DitoTabs.vue +103 -0
  44. package/src/components/DitoTrail.vue +124 -0
  45. package/src/components/DitoTreeItem.vue +420 -0
  46. package/src/components/DitoUploadFile.vue +199 -0
  47. package/src/components/DitoVNode.vue +14 -0
  48. package/src/components/DitoView.vue +143 -0
  49. package/src/components/index.js +42 -0
  50. package/src/directives/resize.js +83 -0
  51. package/src/index.js +1 -0
  52. package/src/mixins/ContextMixin.js +68 -0
  53. package/src/mixins/DataMixin.js +131 -0
  54. package/src/mixins/DitoMixin.js +591 -0
  55. package/src/mixins/DomMixin.js +29 -0
  56. package/src/mixins/EmitterMixin.js +158 -0
  57. package/src/mixins/ItemMixin.js +144 -0
  58. package/src/mixins/LoadingMixin.js +23 -0
  59. package/src/mixins/NumberMixin.js +118 -0
  60. package/src/mixins/OptionsMixin.js +304 -0
  61. package/src/mixins/PulldownMixin.js +63 -0
  62. package/src/mixins/ResourceMixin.js +398 -0
  63. package/src/mixins/RouteMixin.js +190 -0
  64. package/src/mixins/SchemaParentMixin.js +33 -0
  65. package/src/mixins/SortableMixin.js +49 -0
  66. package/src/mixins/SourceMixin.js +734 -0
  67. package/src/mixins/TextMixin.js +26 -0
  68. package/src/mixins/TypeMixin.js +280 -0
  69. package/src/mixins/ValidationMixin.js +119 -0
  70. package/src/mixins/ValidatorMixin.js +57 -0
  71. package/src/mixins/ValueMixin.js +31 -0
  72. package/src/styles/_base.scss +17 -0
  73. package/src/styles/_button.scss +191 -0
  74. package/src/styles/_imports.scss +3 -0
  75. package/src/styles/_info.scss +19 -0
  76. package/src/styles/_layout.scss +19 -0
  77. package/src/styles/_pulldown.scss +38 -0
  78. package/src/styles/_scroll.scss +13 -0
  79. package/src/styles/_settings.scss +88 -0
  80. package/src/styles/_table.scss +223 -0
  81. package/src/styles/_tippy.scss +45 -0
  82. package/src/styles/style.scss +9 -0
  83. package/src/types/DitoTypeButton.vue +143 -0
  84. package/src/types/DitoTypeCheckbox.vue +27 -0
  85. package/src/types/DitoTypeCheckboxes.vue +65 -0
  86. package/src/types/DitoTypeCode.vue +199 -0
  87. package/src/types/DitoTypeColor.vue +272 -0
  88. package/src/types/DitoTypeComponent.vue +31 -0
  89. package/src/types/DitoTypeComputed.vue +50 -0
  90. package/src/types/DitoTypeDate.vue +99 -0
  91. package/src/types/DitoTypeLabel.vue +23 -0
  92. package/src/types/DitoTypeList.vue +364 -0
  93. package/src/types/DitoTypeMarkup.vue +700 -0
  94. package/src/types/DitoTypeMultiselect.vue +522 -0
  95. package/src/types/DitoTypeNumber.vue +66 -0
  96. package/src/types/DitoTypeObject.vue +136 -0
  97. package/src/types/DitoTypePanel.vue +18 -0
  98. package/src/types/DitoTypeProgress.vue +40 -0
  99. package/src/types/DitoTypeRadio.vue +45 -0
  100. package/src/types/DitoTypeSection.vue +80 -0
  101. package/src/types/DitoTypeSelect.vue +133 -0
  102. package/src/types/DitoTypeSlider.vue +66 -0
  103. package/src/types/DitoTypeSpacer.vue +11 -0
  104. package/src/types/DitoTypeSwitch.vue +40 -0
  105. package/src/types/DitoTypeText.vue +101 -0
  106. package/src/types/DitoTypeTextarea.vue +48 -0
  107. package/src/types/DitoTypeTreeList.vue +193 -0
  108. package/src/types/DitoTypeUpload.vue +503 -0
  109. package/src/types/index.js +30 -0
  110. package/src/utils/SchemaGraph.js +147 -0
  111. package/src/utils/accessor.js +75 -0
  112. package/src/utils/agent.js +47 -0
  113. package/src/utils/data.js +92 -0
  114. package/src/utils/filter.js +266 -0
  115. package/src/utils/math.js +14 -0
  116. package/src/utils/options.js +48 -0
  117. package/src/utils/path.js +5 -0
  118. package/src/utils/resource.js +44 -0
  119. package/src/utils/route.js +53 -0
  120. package/src/utils/schema.js +1121 -0
  121. package/src/utils/type.js +81 -0
  122. package/src/utils/uid.js +15 -0
  123. package/src/utils/units.js +5 -0
  124. package/src/validators/_creditcard.js +6 -0
  125. package/src/validators/_decimals.js +11 -0
  126. package/src/validators/_domain.js +6 -0
  127. package/src/validators/_email.js +6 -0
  128. package/src/validators/_hostname.js +6 -0
  129. package/src/validators/_integer.js +6 -0
  130. package/src/validators/_max.js +6 -0
  131. package/src/validators/_min.js +6 -0
  132. package/src/validators/_password.js +5 -0
  133. package/src/validators/_range.js +6 -0
  134. package/src/validators/_required.js +9 -0
  135. package/src/validators/_url.js +6 -0
  136. package/src/validators/index.js +12 -0
  137. package/src/verbs.js +17 -0
  138. package/types/index.d.ts +3298 -0
  139. package/types/tests/admin.test-d.ts +27 -0
  140. package/types/tests/component-buttons.test-d.ts +44 -0
  141. package/types/tests/component-list.test-d.ts +159 -0
  142. package/types/tests/component-misc.test-d.ts +137 -0
  143. package/types/tests/component-object.test-d.ts +69 -0
  144. package/types/tests/component-section.test-d.ts +174 -0
  145. package/types/tests/component-select.test-d.ts +107 -0
  146. package/types/tests/components.test-d.ts +81 -0
  147. package/types/tests/context.test-d.ts +31 -0
  148. package/types/tests/fixtures.ts +24 -0
  149. package/types/tests/form.test-d.ts +109 -0
  150. package/types/tests/instance.test-d.ts +20 -0
  151. package/types/tests/schema-features.test-d.ts +402 -0
  152. package/types/tests/variance.test-d.ts +125 -0
  153. package/types/tests/view.test-d.ts +146 -0
@@ -0,0 +1,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
+ }