@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,524 @@
1
+ <template lang="pug">
2
+ .dito-root(
3
+ :data-agent-browser="appState.agent.browser"
4
+ :data-agent-platform="appState.agent.platform"
5
+ :data-agent-version="appState.agent.versionNumber"
6
+ )
7
+ Transition(name="dito-drag")
8
+ .dito-drag-overlay(
9
+ v-if="isDraggingFiles"
10
+ )
11
+ TransitionGroup(name="dito-dialog")
12
+ DitoDialog(
13
+ v-for="(dialog, key) in dialogs"
14
+ :key="key"
15
+ :components="dialog.components"
16
+ :buttons="dialog.buttons"
17
+ :promise="dialog.promise"
18
+ :data="dialog.data"
19
+ :settings="dialog.settings"
20
+ @remove="removeDialog(key)"
21
+ )
22
+ DitoNavigation
23
+ main.dito-page.dito-scroll-parent(
24
+ v-resize="onResizePage"
25
+ :class="pageClasses"
26
+ )
27
+ DitoHeader(
28
+ :spinner="options.spinner"
29
+ :isLoading="isLoading"
30
+ )
31
+ RouterView
32
+ DitoSidebar
33
+ DitoAccount(
34
+ v-if="user"
35
+ )
36
+ a.dito-login(
37
+ v-else-if="allowLogin"
38
+ @click="rootComponent.login()"
39
+ )
40
+ span Login
41
+ DitoNotifications(ref="notifications")
42
+ </template>
43
+
44
+ <script>
45
+ import { delegate as tippyDelegate } from 'tippy.js'
46
+ import { mapConcurrently } from '@ditojs/utils'
47
+ import DitoComponent from '../DitoComponent.js'
48
+ import DomMixin from '../mixins/DomMixin.js'
49
+ import DitoUser from '../DitoUser.js'
50
+ import DitoView from '../components/DitoView.vue'
51
+ import DitoDialog from './DitoDialog.vue'
52
+ import {
53
+ processView,
54
+ resolveViews,
55
+ processSchemaComponents
56
+ } from '../utils/schema.js'
57
+
58
+ // @vue/component
59
+ export default DitoComponent.component('DitoRoot', {
60
+ mixins: [DomMixin],
61
+ components: { DitoDialog },
62
+
63
+ provide() {
64
+ return {
65
+ $views: () => this.resolvedViews
66
+ }
67
+ },
68
+
69
+ props: {
70
+ unresolvedViews: { type: [Object, Function, Promise], required: true },
71
+ options: { type: Object, default: () => ({}) }
72
+ },
73
+
74
+ data() {
75
+ return {
76
+ resolvedViews: {},
77
+ removeRoutes: null,
78
+ dialogs: {},
79
+ pageWidth: 0,
80
+ loadingCount: 0,
81
+ allowLogin: false,
82
+ isDraggingFiles: false
83
+ }
84
+ },
85
+
86
+ computed: {
87
+ notifications() {
88
+ return this.isMounted && this.$refs.notifications
89
+ },
90
+
91
+ isLoading() {
92
+ return this.loadingCount > 0
93
+ },
94
+
95
+ pageClasses() {
96
+ const prefix = 'dito-page'
97
+ // NOTE: Keep synced with $content-width in SCSS:
98
+ const contentWidth = 900
99
+ return [
100
+ this.appState.pageClass,
101
+ {
102
+ [`${prefix}--width-80`]: this.pageWidth <= contentWidth * 0.8,
103
+ [`${prefix}--width-60`]: this.pageWidth <= contentWidth * 0.6
104
+ }
105
+ ]
106
+ }
107
+ },
108
+
109
+ created() {
110
+ this.appState.title = document.title || 'Dito.js Admin'
111
+ // With hot-reloading, it looks like destroyed hooks aren't always called
112
+ // for route components so reset the array of registered components instead.
113
+ this.appState.routeComponents = []
114
+ },
115
+
116
+ async mounted() {
117
+ this.setupDragAndDrop()
118
+
119
+ tippyDelegate(this.$el, {
120
+ target: '.dito-info',
121
+ theme: 'info',
122
+ animation: 'shift-away-subtle',
123
+ interactive: true,
124
+ delay: 250,
125
+ zIndex: 1,
126
+ appendTo: node => node.closest('.dito-pane'),
127
+ onShow: instance => instance.setContent(instance.reference.dataset.info)
128
+ })
129
+
130
+ // Clear the label marked as active on all mouse and keyboard events, except
131
+ // the ones that DitoLabel itself intercepts.
132
+ this.domOn(document, {
133
+ click: event => {
134
+ if (!event.target.closest('.dito-label')) {
135
+ this.appState.activeLabel = null
136
+ }
137
+ },
138
+
139
+ keyup: event => {
140
+ if (event.code === 'Tab') {
141
+ this.appState.activeLabel = null
142
+ }
143
+ }
144
+ })
145
+
146
+ try {
147
+ this.allowLogin = false
148
+ if (await this.fetchUser()) {
149
+ await this.resolveViews()
150
+ } else {
151
+ await this.login()
152
+ }
153
+ } catch (err) {
154
+ console.error(err)
155
+ }
156
+ this.allowLogin = true
157
+ },
158
+
159
+ methods: {
160
+ setupDragAndDrop() {
161
+ // This code only happens the visual effects around dragging and dropping
162
+ // files into a `DitoTypeUpload` component. The actual uploading is
163
+ // handled by the `DitoTypeUpload` component itself.
164
+
165
+ let dragCount = 0
166
+ let uploads = []
167
+
168
+ const toggleDropTargetClass = enabled => {
169
+ for (const upload of uploads) {
170
+ upload
171
+ .closest('.dito-container')
172
+ .classList.toggle('dito-drop-target', enabled)
173
+ }
174
+ if (!enabled) {
175
+ uploads = []
176
+ }
177
+ }
178
+
179
+ const setDraggingFiles = enabled => {
180
+ this.isDraggingFiles = enabled
181
+ if (enabled) {
182
+ toggleDropTargetClass(true)
183
+ } else {
184
+ setTimeout(() => toggleDropTargetClass(false), 150)
185
+ }
186
+ }
187
+
188
+ this.domOn(document, {
189
+ dragenter: event => {
190
+ if (!dragCount && event.dataTransfer) {
191
+ uploads = document.querySelectorAll('.dito-upload')
192
+ const hasUploads = uploads.length > 0
193
+ event.dataTransfer.effectAllowed = hasUploads ? 'copy' : 'none'
194
+ if (hasUploads) {
195
+ setDraggingFiles(true)
196
+ } else {
197
+ event.preventDefault()
198
+ event.stopPropagation()
199
+ return
200
+ }
201
+ }
202
+ dragCount++
203
+ },
204
+
205
+ dragleave: event => {
206
+ dragCount--
207
+ if (!dragCount && event.dataTransfer) {
208
+ setDraggingFiles(false)
209
+ }
210
+ },
211
+
212
+ dragover: event => {
213
+ if (event.dataTransfer) {
214
+ const canDrop = event.target.closest(
215
+ '.dito-container:has(.dito-upload)'
216
+ )
217
+ event.dataTransfer.dropEffect = canDrop ? 'copy' : 'none'
218
+ if (!canDrop) {
219
+ event.preventDefault()
220
+ event.stopPropagation()
221
+ }
222
+ }
223
+ },
224
+
225
+ drop: event => {
226
+ dragCount = 0
227
+ if (event.dataTransfer) {
228
+ setDraggingFiles(false)
229
+ }
230
+ }
231
+ })
232
+ },
233
+
234
+ notify({ type = 'info', title, text, error, duration } = {}) {
235
+ this.notifications.notify({ type, title, text, error, duration })
236
+ },
237
+
238
+ closeNotifications() {
239
+ this.notifications.destroyAll()
240
+ },
241
+
242
+ registerLoading(isLoading) {
243
+ this.loadingCount += isLoading ? 1 : -1
244
+ },
245
+
246
+ showDialog({ components, buttons, data, settings }) {
247
+ // Shows a dito-dialog component and wraps it in a promise so that the
248
+ // buttons in the dialog can use `dialog.resolve()` and `dialog.reject()`
249
+ // to close the modal dialog and resolve / reject the promise at once.
250
+ return new Promise(
251
+ async (resolve, reject) => {
252
+ // Process components to resolve async schemas.
253
+ const routes = []
254
+ await processSchemaComponents(
255
+ this.api,
256
+ { type: 'dialog', components },
257
+ routes,
258
+ 0
259
+ )
260
+ if (routes.length > 0) {
261
+ throw new Error(
262
+ 'Dialogs do not support components that produce routes'
263
+ )
264
+ }
265
+ const key = `dialog-${++dialogId}`
266
+ this.dialogs[key] = {
267
+ components,
268
+ buttons,
269
+ data,
270
+ settings,
271
+ promise: { resolve, reject }
272
+ }
273
+ }
274
+ )
275
+ },
276
+
277
+ removeDialog(key) {
278
+ delete this.dialogs[key]
279
+ },
280
+
281
+ async login() {
282
+ this.allowLogin = true
283
+ const {
284
+ additionalComponents,
285
+ redirectAfterLogin
286
+ } = this.options.login || {}
287
+ const loginData = await this.showDialog({
288
+ components: {
289
+ username: {
290
+ type: 'text',
291
+ autofocus: true
292
+ },
293
+ password: {
294
+ type: 'password'
295
+ },
296
+ ...additionalComponents
297
+ },
298
+ // NOTE: Login must come before cancel in DOM order so that password
299
+ // managers (e.g. 1Password) target the submit button instead of cancel.
300
+ // DitoDialog uses CSS order to visually place cancel first.
301
+ buttons: {
302
+ login: {
303
+ type: 'submit',
304
+ text: 'Login'
305
+ },
306
+
307
+ cancel: {
308
+ type: 'button',
309
+ text: 'Cancel'
310
+ // NOTE: The click event is added in DitoDialog.buttonSchemas()
311
+ }
312
+ }
313
+ })
314
+ if (loginData) {
315
+ try {
316
+ const response = await this.sendRequest({
317
+ resource: this.api.users.login,
318
+ data: loginData,
319
+ internal: true
320
+ })
321
+ if (redirectAfterLogin) {
322
+ location.replace(redirectAfterLogin)
323
+ } else {
324
+ this.setUser(response.data.user)
325
+ await this.resolveViews()
326
+ }
327
+ } catch (err) {
328
+ const error = err.response?.data?.error || err
329
+ this.notify({
330
+ type: 'error',
331
+ error,
332
+ title: 'Authentication Error',
333
+ text: error
334
+ })
335
+ this.login()
336
+ }
337
+ }
338
+ },
339
+
340
+ navigateHome() {
341
+ return this.navigate('/')
342
+ },
343
+
344
+ async logout() {
345
+ try {
346
+ const response = await this.sendRequest({
347
+ resource: this.api.users.logout,
348
+ internal: true
349
+ })
350
+ if (response.data.success) {
351
+ this.setUser(null)
352
+ this.navigateHome()
353
+ }
354
+ } catch (err) {
355
+ console.error(err)
356
+ }
357
+ },
358
+
359
+ async fetchUser() {
360
+ let user = null
361
+ try {
362
+ const response = await this.sendRequest({
363
+ resource: this.api.users.session,
364
+ internal: true
365
+ })
366
+ user = response.data.user || null
367
+ } catch (err) {
368
+ const error = err.response?.data?.error || err
369
+ this.notify({
370
+ type: 'error',
371
+ error,
372
+ title: 'Authentication Error',
373
+ text: error
374
+ })
375
+ }
376
+ this.setUser(user)
377
+ return user
378
+ },
379
+
380
+ setUser(user) {
381
+ this.appState.user = (
382
+ user &&
383
+ Object.setPrototypeOf(user, DitoUser.prototype)
384
+ )
385
+ // Clear resolved views when user is logged out.
386
+ if (!user) {
387
+ this.resolvedViews = {}
388
+ this.navigateHome()
389
+ }
390
+ },
391
+
392
+ async ensureUser() {
393
+ if (!(await this.fetchUser())) {
394
+ await this.login()
395
+ }
396
+ },
397
+
398
+ async resolveViews() {
399
+ try {
400
+ this.resolvedViews = await resolveViews(this.unresolvedViews)
401
+ } catch (error) {
402
+ if (!error.request) {
403
+ console.error(error)
404
+ }
405
+ return this.login()
406
+ }
407
+ // Collect all routes from the root schema components
408
+ const routes = await mapConcurrently(
409
+ Object.entries(this.resolvedViews),
410
+ ([name, schema]) => processView(DitoView, this.api, schema, name)
411
+ )
412
+ // Now that the routes are loaded, replace all existing routes with the
413
+ // new routes, and restore the current path.
414
+ const { fullPath } = this.$route
415
+ this.removeRoutes?.()
416
+ this.removeRoutes = addRoutes(this.$router, [
417
+ {
418
+ name: 'root',
419
+ path: '/',
420
+ components: {}
421
+ },
422
+ ...routes.flat()
423
+ ])
424
+ this.$router.replace(fullPath)
425
+ },
426
+
427
+ onResizePage({ contentRect: { width } }) {
428
+ this.pageWidth = width
429
+ }
430
+ }
431
+ })
432
+
433
+ let dialogId = 0
434
+
435
+ function addRoutes(router, routes) {
436
+ const removers = []
437
+ for (const route of routes) {
438
+ removers.push(
439
+ router.addRoute(route)
440
+ )
441
+ }
442
+
443
+ return () => {
444
+ for (const remove of removers) {
445
+ remove()
446
+ }
447
+ }
448
+ }
449
+ </script>
450
+
451
+ <style lang="scss">
452
+ @import '../styles/style';
453
+
454
+ .dito-app,
455
+ .dito-root {
456
+ width: 100%;
457
+ height: 100%;
458
+ display: flex;
459
+ }
460
+
461
+ .dito-page {
462
+ --max-content-width: #{$content-width};
463
+ --max-page-width: calc(var(--max-content-width) + 2 * #{$content-padding});
464
+
465
+ flex: 0 1 var(--max-page-width);
466
+ background: $content-color-background;
467
+ min-width: 0%;
468
+ max-width: var(--max-page-width);
469
+ overflow: visible; // For .dito-header full-width background.
470
+
471
+ &--wide {
472
+ --max-content-width: #{$content-width-wide};
473
+ }
474
+ }
475
+
476
+ .dito-account,
477
+ .dito-login {
478
+ cursor: pointer;
479
+ }
480
+
481
+ .dito-drag-overlay {
482
+ position: fixed;
483
+ top: 0;
484
+ left: 0;
485
+ width: 100%;
486
+ height: 100%;
487
+ z-index: $z-index-drag-overlay;
488
+ background: rgb(0, 0, 0, 0.25);
489
+ pointer-events: none;
490
+ backdrop-filter: blur(8px);
491
+ }
492
+
493
+ .dito-drop-target {
494
+ --shadow-alpha: 0.25;
495
+
496
+ background: $content-color-background;
497
+ border-radius: $border-radius;
498
+ z-index: $z-index-drag-overlay + 1;
499
+ filter: drop-shadow(0 4px 8px rgb(0, 0, 0, var(--shadow-alpha)));
500
+ }
501
+
502
+ .dito-drag-enter-active,
503
+ .dito-drag-leave-active {
504
+ $duration: 0.15s;
505
+
506
+ transition:
507
+ opacity $duration,
508
+ backdrop-filter $duration;
509
+
510
+ ~ * .dito-drop-target {
511
+ transition: filter $duration;
512
+ }
513
+ }
514
+
515
+ .dito-drag-enter-from,
516
+ .dito-drag-leave-to {
517
+ opacity: 0;
518
+ backdrop-filter: blur(0);
519
+
520
+ ~ * .dito-drop-target {
521
+ --shadow-alpha: 0;
522
+ }
523
+ }
524
+ </style>