@ditojs/admin 3.2.7 → 3.3.1

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 (112) hide show
  1. package/dist/dito-admin.css +1 -1
  2. package/dist/dito-admin.es.js +6011 -5826
  3. package/dist/dito-admin.umd.js +3 -7
  4. package/package.json +18 -17
  5. package/src/DitoAdmin.js +44 -24
  6. package/src/DitoContext.js +28 -2
  7. package/src/DitoTypeComponent.js +1 -0
  8. package/src/appState.js +6 -3
  9. package/src/components/DitoAccount.vue +35 -27
  10. package/src/components/DitoAffixes.vue +9 -2
  11. package/src/components/DitoClipboard.vue +59 -93
  12. package/src/components/DitoConfirmMessage.vue +18 -0
  13. package/src/components/DitoContainer.vue +56 -18
  14. package/src/components/DitoCreateButton.vue +36 -84
  15. package/src/components/DitoDialog.vue +20 -15
  16. package/src/components/DitoDragHandle.vue +44 -0
  17. package/src/components/DitoDraggable.vue +43 -16
  18. package/src/components/DitoEditButtons.vue +48 -24
  19. package/src/components/DitoErrors.vue +9 -56
  20. package/src/components/DitoForm.vue +82 -42
  21. package/src/components/DitoInputAffixes.vue +50 -0
  22. package/src/components/DitoLabel.vue +1 -15
  23. package/src/components/DitoMenu.vue +45 -52
  24. package/src/components/DitoNotifications.vue +38 -14
  25. package/src/components/DitoOptionList.vue +81 -0
  26. package/src/components/DitoOptionsEditButtons.vue +57 -0
  27. package/src/components/DitoPagination.vue +1 -8
  28. package/src/components/DitoPane.vue +51 -39
  29. package/src/components/DitoPanel.vue +12 -26
  30. package/src/components/DitoPanels.vue +11 -5
  31. package/src/components/DitoResizeHandle.vue +47 -0
  32. package/src/components/DitoRoot.vue +113 -320
  33. package/src/components/DitoSchema.vue +72 -63
  34. package/src/components/DitoSchemaInlined.vue +8 -9
  35. package/src/components/DitoScopes.vue +14 -18
  36. package/src/components/DitoSidebar.vue +4 -29
  37. package/src/components/DitoSourceItem.vue +114 -0
  38. package/src/components/DitoTableCell.vue +4 -0
  39. package/src/components/DitoTableHead.vue +15 -17
  40. package/src/components/DitoTabs.vue +25 -0
  41. package/src/components/DitoTrail.vue +25 -25
  42. package/src/components/DitoTreeItem.vue +102 -141
  43. package/src/components/DitoUploadFile.vue +5 -2
  44. package/src/components/DitoView.vue +33 -24
  45. package/src/index.js +4 -1
  46. package/src/mixins/DitoMixin.js +77 -110
  47. package/src/mixins/DomMixin.js +12 -14
  48. package/src/mixins/EmitterMixin.js +3 -2
  49. package/src/mixins/ItemMixin.js +38 -94
  50. package/src/mixins/OptionsMixin.js +38 -25
  51. package/src/mixins/ResourceMixin.js +182 -111
  52. package/src/mixins/RouteMixin.js +61 -44
  53. package/src/mixins/SchemaParentMixin.js +4 -1
  54. package/src/mixins/SourceMixin.js +118 -100
  55. package/src/mixins/TypeMixin.js +42 -40
  56. package/src/mixins/ValidationMixin.js +16 -4
  57. package/src/mixins/ValidatorMixin.js +1 -1
  58. package/src/styles/_settings.scss +4 -0
  59. package/src/styles/_tippy.scss +0 -11
  60. package/src/styles/style.scss +0 -1
  61. package/src/types/DitoTypeButton.vue +11 -14
  62. package/src/types/DitoTypeCheckboxes.vue +35 -17
  63. package/src/types/DitoTypeCode.vue +19 -41
  64. package/src/types/DitoTypeColor.vue +67 -19
  65. package/src/types/DitoTypeDate.vue +11 -14
  66. package/src/types/DitoTypeList.vue +50 -62
  67. package/src/types/DitoTypeMarkup.vue +41 -54
  68. package/src/types/DitoTypeMultiselect.vue +82 -52
  69. package/src/types/DitoTypeNumber.vue +12 -15
  70. package/src/types/DitoTypeObject.vue +4 -38
  71. package/src/types/DitoTypeRadio.vue +19 -6
  72. package/src/types/DitoTypeSection.vue +23 -9
  73. package/src/types/DitoTypeSelect.vue +39 -23
  74. package/src/types/DitoTypeSlider.vue +9 -1
  75. package/src/types/DitoTypeSwitch.vue +1 -1
  76. package/src/types/DitoTypeText.vue +11 -14
  77. package/src/types/DitoTypeTreeList.vue +46 -8
  78. package/src/types/DitoTypeUpload.vue +190 -67
  79. package/src/utils/DataModel.js +368 -146
  80. package/src/utils/FileDropTargets.js +97 -0
  81. package/src/utils/ItemStores.js +31 -0
  82. package/src/utils/ListQuery.js +115 -0
  83. package/src/utils/LoadingTracker.js +76 -0
  84. package/src/utils/SchemaGraph.js +3 -2
  85. package/src/utils/Session.js +151 -0
  86. package/src/utils/ViewRegistry.js +85 -0
  87. package/src/utils/accessor.js +12 -4
  88. package/src/utils/affixes.js +33 -0
  89. package/src/utils/changeOnceEdited.js +33 -0
  90. package/src/utils/clipboard.js +55 -0
  91. package/src/utils/data.js +10 -5
  92. package/src/utils/dialogs.js +59 -0
  93. package/src/utils/dragResize.js +61 -0
  94. package/src/utils/filter.js +36 -2
  95. package/src/utils/html.js +9 -0
  96. package/src/utils/list.js +10 -0
  97. package/src/utils/options.js +4 -0
  98. package/src/utils/registration.js +18 -0
  99. package/src/utils/request.js +108 -13
  100. package/src/utils/route.js +6 -17
  101. package/src/utils/schema/data.js +24 -2
  102. package/src/utils/schema/events.js +19 -0
  103. package/src/utils/schema/item.js +114 -0
  104. package/src/utils/schema/lookup.js +39 -14
  105. package/src/utils/schema/setup.js +5 -5
  106. package/src/utils/schema/validation.js +16 -2
  107. package/src/utils/uid.js +105 -8
  108. package/src/validators/_decimals.js +16 -5
  109. package/types/index.d.ts +168 -41
  110. package/src/mixins/LoadingMixin.js +0 -23
  111. package/src/mixins/PulldownMixin.js +0 -72
  112. package/src/styles/_pulldown.scss +0 -38
@@ -1,5 +1,8 @@
1
1
  <template lang="pug">
2
- .dito-upload
2
+ .dito-upload(
3
+ ref="dropTarget"
4
+ :class="{ 'dito-upload--drop-target': isDropTarget }"
5
+ )
3
6
  //- In order to handle upload buttons in multiple possible places, depending
4
7
  //- on whether they handle single or multiple uploads, render the upload
5
8
  //- component invisibly at the root, and delegate the click events to it from
@@ -11,11 +14,12 @@
11
14
  :name="dataPath"
12
15
  :disabled="disabled"
13
16
  :postAction="uploadPath"
17
+ :headers="uploadOptions?.headers"
14
18
  :extensions="extensions"
15
19
  :accept="accept"
16
20
  :multiple="multiple"
17
21
  :size="maxSize"
18
- :drop="$el?.closest('.dito-container')"
22
+ :drop="true"
19
23
  :dropDirectory="true"
20
24
  @input-filter="onInputFilter"
21
25
  @input-file="onInputFile"
@@ -41,9 +45,10 @@
41
45
  template(
42
46
  v-if="multiple || !isUploadActive"
43
47
  )
48
+ //- Stored files have unique keys, uploading files unique ids:
44
49
  tr(
45
50
  v-for="(file, index) in files"
46
- :key="file.name"
51
+ :key="file.key ?? file.id ?? file.name"
47
52
  )
48
53
  td(
49
54
  v-if="render"
@@ -60,6 +65,7 @@
60
65
  )
61
66
  DitoUploadFile(
62
67
  :file="file"
68
+ :upload="getUpload(file)"
63
69
  :thumbnail="thumbnails"
64
70
  :thumbnailUrl="thumbnailUrls[index]"
65
71
  )
@@ -68,6 +74,7 @@
68
74
  )
69
75
  DitoUploadFile(
70
76
  :file="file"
77
+ :upload="getUpload(file)"
71
78
  :thumbnail="thumbnails"
72
79
  :thumbnailUrl="thumbnailUrls[index]"
73
80
  )
@@ -77,57 +84,56 @@
77
84
  v-if="file.upload"
78
85
  )
79
86
  template(
80
- v-if="file.upload.error"
87
+ v-if="file.upload.success"
81
88
  )
82
- | Error: {{ file.upload.error }}
89
+ | Uploaded
83
90
  template(
84
- v-else-if="file.upload.active"
91
+ v-else-if="getUpload(file)?.error"
85
92
  )
86
- | Uploading...
93
+ | Error: {{ getUpload(file).error }}
87
94
  template(
88
- v-else-if="file.upload.success"
95
+ v-else-if="getUpload(file)?.active"
89
96
  )
90
- | Uploaded
97
+ | Uploading...
91
98
  template(
92
99
  v-else
93
100
  )
94
101
  | Stored
95
102
  td.dito-table__buttons
96
103
  .dito-buttons.dito-buttons--round
97
- button.dito-button.dito-button--upload(
104
+ DitoButton.dito-button--upload(
98
105
  v-if="!multiple"
99
106
  :title="uploadTitle"
100
107
  @click="onClickUpload"
101
108
  )
102
- //- Firefox doesn't like <button> here, so use <a> instead:
103
- a.dito-button(
109
+ DitoDragHandle(
104
110
  v-if="draggable"
105
- v-bind="getButtonAttributes(verbs.drag)"
111
+ :disabled="disabled"
112
+ @move="delta => moveFile(file, delta)"
106
113
  )
107
- button.dito-button(
114
+ DitoButton(
108
115
  v-if="deletable"
109
- type="button"
110
- v-bind="getButtonAttributes(verbs.delete)"
111
- @click="deleteFile(file, index)"
116
+ :verb="verbs.delete"
117
+ :disabled="disabled"
118
+ @click="deleteFile(file)"
112
119
  )
113
120
  tfoot(
114
121
  v-if="multiple || isUploadActive || !hasFiles"
115
122
  )
116
123
  tr
117
124
  td(:colspan="4")
118
- .dito-upload-footer
125
+ .dito-upload__footer
119
126
  progress.dito-progress(
120
127
  v-if="isUploadActive"
121
128
  :value="uploadProgress"
122
129
  max="100"
123
130
  )
124
131
  .dito-buttons.dito-buttons--round
125
- button.dito-button(
132
+ DitoButton(
126
133
  v-if="isUploadActive"
127
- type="button"
128
134
  @click.prevent="upload.active = false"
129
135
  ) Cancel
130
- button.dito-button.dito-button--upload(
136
+ DitoButton.dito-button--upload(
131
137
  v-if="multiple || !hasFiles"
132
138
  :title="uploadTitle"
133
139
  @click="onClickUpload"
@@ -137,25 +143,49 @@
137
143
  <script>
138
144
  import DitoTypeComponent from '../DitoTypeComponent.js'
139
145
  import DitoContext from '../DitoContext.js'
146
+ import DitoDragHandle from '../components/DitoDragHandle.vue'
147
+ import { DitoButton } from '@ditojs/ui/src'
140
148
  import SortableMixin from '../mixins/SortableMixin.js'
141
149
  import { getSchemaAccessor } from '../utils/accessor.js'
142
150
  import { formatFileSize, parseFileSize } from '../utils/units.js'
143
151
  import { appendDataPath } from '../utils/data.js'
144
- import { isArray, asArray } from '@ditojs/utils'
152
+ import {
153
+ fetchBlob,
154
+ getUploadOptions,
155
+ resolveDownloadUrl
156
+ } from '../utils/request.js'
157
+ import { confirmAndRemove } from '../utils/dialogs.js'
158
+ import { getListWithMovedItem } from '../utils/list.js'
159
+ import { isArray, asArray, escapeHtml } from '@ditojs/utils'
145
160
  import VueUpload from 'vue-upload-component'
146
161
 
147
162
  // @vue/component
148
163
  export default DitoTypeComponent.register('upload', {
149
164
  mixins: [SortableMixin],
150
- components: { VueUpload },
165
+ components: { VueUpload, DitoButton, DitoDragHandle },
166
+ inject: ['$fileDropTargets'],
151
167
 
152
168
  data() {
153
169
  return {
154
- uploads: []
170
+ uploads: [],
171
+ // Removes the upload from the targets of dragged files, see
172
+ // `updateDropTarget()`.
173
+ removeDropTarget: null
155
174
  }
156
175
  },
157
176
 
158
177
  computed: {
178
+ fileDropTargets() {
179
+ return this.$fileDropTargets()
180
+ },
181
+
182
+ // Whether files are dragged over the admin while the upload is enabled, for
183
+ // which it stands out above the overlay of `DitoRoot`. The dragged files'
184
+ // types can't be read before the drop, so `accept` isn't checked here.
185
+ isDropTarget() {
186
+ return this.fileDropTargets.isDraggingFiles && !this.disabled
187
+ },
188
+
159
189
  upload() {
160
190
  return this.$refs.upload
161
191
  },
@@ -164,12 +194,24 @@ export default DitoTypeComponent.register('upload', {
164
194
  return this.multiple ? 'Upload Files' : 'Upload File'
165
195
  },
166
196
 
167
- files() {
168
- return asFiles(this.value)
197
+ files: {
198
+ get() {
199
+ return asFiles(this.value)
200
+ },
201
+
202
+ // Writes the reordered files, see `DitoDraggable` and `moveFile()`.
203
+ set(files) {
204
+ this.value = this.multiple ? files : files[0] ?? null
205
+ }
169
206
  },
170
207
 
171
208
  downloadUrls() {
172
- return this.files.map((file, index) => this.getDownloadUrl(file, index))
209
+ // Resolve the URLs like `fetchBlob()` does, so that the links point to
210
+ // the same files that clicking them downloads:
211
+ return this.files.map((file, index) => {
212
+ const url = this.getDownloadUrl(file, index)
213
+ return url ? resolveDownloadUrl(this.api, url) : null
214
+ })
173
215
  },
174
216
 
175
217
  thumbnailUrls() {
@@ -233,6 +275,13 @@ export default DitoTypeComponent.register('upload', {
233
275
  return this.files.length > 0
234
276
  },
235
277
 
278
+ // The files of the upload component by their ids. It replaces a file's
279
+ // object on each update, so the files in the value only refer to their
280
+ // upload by id, see `onInputFile()` and `getUpload()`.
281
+ uploadsById() {
282
+ return new Map(this.uploads.map(upload => [upload.id, upload]))
283
+ },
284
+
236
285
  hasUploads() {
237
286
  return this.uploads.length > 0
238
287
  },
@@ -256,15 +305,27 @@ export default DitoTypeComponent.register('upload', {
256
305
  },
257
306
 
258
307
  uploadPath() {
259
- return this.getResourceUrl({
260
- type: 'upload',
261
- method: 'post',
262
- path: this.api.normalizePath(this.dataPath)
263
- })
308
+ // Uploads are posted to the resource of the data, see
309
+ // `api.resources.upload()`, which views without resource don't have.
310
+ return this.dataComponent?.resource
311
+ ? this.getResourceUrl({
312
+ type: 'upload',
313
+ method: 'post',
314
+ path: this.api.normalizePath(this.dataPath)
315
+ })
316
+ : null
317
+ },
318
+
319
+ uploadOptions() {
320
+ return this.uploadPath
321
+ ? getUploadOptions(this.api, this.uploadPath)
322
+ : null
264
323
  }
265
324
  },
266
325
 
267
326
  watch: {
327
+ disabled: 'updateDropTarget',
328
+
268
329
  isUploadReady(ready) {
269
330
  if (ready) {
270
331
  // Auto-upload.
@@ -275,9 +336,27 @@ export default DitoTypeComponent.register('upload', {
275
336
  }
276
337
  },
277
338
 
339
+ mounted() {
340
+ this.updateDropTarget()
341
+ },
342
+
343
+ unmounted() {
344
+ this.removeDropTarget?.()
345
+ },
346
+
278
347
  methods: {
279
348
  formatFileSize,
280
349
 
350
+ // Adds the upload to the targets of dragged files while it's enabled.
351
+ // `VueUpload` handles the files dropped on its parent, which is the same
352
+ // element, see `:drop="true"`.
353
+ updateDropTarget() {
354
+ this.removeDropTarget?.()
355
+ this.removeDropTarget = this.disabled
356
+ ? null
357
+ : this.fileDropTargets.add(this.$refs.dropTarget)
358
+ },
359
+
281
360
  getFileContext(file, index) {
282
361
  return this.multiple
283
362
  ? new DitoContext(this, {
@@ -293,12 +372,18 @@ export default DitoTypeComponent.register('upload', {
293
372
  return this.render(this.getFileContext(file, index))
294
373
  },
295
374
 
375
+ // Returns the current file object of the upload component for a file that
376
+ // was added through it, or `null`.
377
+ getUpload(file) {
378
+ return file.upload ? (this.uploadsById.get(file.upload.id) ?? null) : null
379
+ },
380
+
296
381
  getDownloadUrl(file, index) {
297
382
  return file.url
298
383
  ? file.url
299
384
  : !file.upload || file.upload.success
300
385
  ? this.getSchemaValue('downloadUrl', {
301
- type: 'String',
386
+ type: String,
302
387
  default: null,
303
388
  context: this.getFileContext(file, index)
304
389
  })
@@ -308,7 +393,7 @@ export default DitoTypeComponent.register('upload', {
308
393
  getThumbnailUrl(file, index) {
309
394
  return !file.upload || file.upload.success
310
395
  ? this.getSchemaValue('thumbnailUrl', {
311
- type: 'String',
396
+ type: String,
312
397
  default: null,
313
398
  context: this.getFileContext(file, index)
314
399
  }) || (
@@ -319,29 +404,40 @@ export default DitoTypeComponent.register('upload', {
319
404
  : null
320
405
  },
321
406
 
322
- deleteFile(file, index) {
323
- const { name } = file
324
-
325
- if (
326
- file &&
327
- window.confirm(
328
- `Do you really want to ${this.verbs.remove} ${name}?`
329
- )
330
- ) {
331
- if (this.multiple) {
332
- this.value.splice(index, 1)
333
- } else {
334
- this.value = null
335
- }
336
- if (file.upload) {
337
- this.upload.remove(file.upload)
407
+ async deleteFile(file) {
408
+ if (!file) return
409
+ await confirmAndRemove(this, {
410
+ label: escapeHtml(file.name),
411
+ // The file is only removed from the value, which still needs the form
412
+ // to be saved.
413
+ isTransient: true,
414
+ remove: () => {
415
+ // Look up the file by identity once confirmed, as the files may have
416
+ // changed while the dialog was open. Stored files may not have ids,
417
+ // see `getFileIndex()`.
418
+ const index = this.files.indexOf(file)
419
+ if (index < 0) {
420
+ return false
421
+ }
422
+ if (this.multiple) {
423
+ this.value.splice(index, 1)
424
+ } else {
425
+ this.value = null
426
+ }
427
+ if (file.upload) {
428
+ this.upload.remove(file.upload.id)
429
+ }
430
+ this.onChange()
338
431
  }
432
+ })
433
+ },
434
+
435
+ // Moves the file by `delta` positions, see `DitoDragHandle`.
436
+ moveFile(file, delta) {
437
+ const files = getListWithMovedItem(this.files, file, delta)
438
+ if (files) {
439
+ this.files = files
339
440
  this.onChange()
340
- this.notify({
341
- type: 'info',
342
- title: 'Successfully Removed',
343
- text: `${name} was ${this.verbs.deleted}.`
344
- })
345
441
  }
346
442
  },
347
443
 
@@ -382,10 +478,14 @@ export default DitoTypeComponent.register('upload', {
382
478
  this.replaceFile(file, null)
383
479
  },
384
480
 
481
+ // Files added through the upload component refer to their upload with
482
+ // `upload: { id, success }` in the value: `success` tells `processValue()`
483
+ // which files were stored, and the upload's state is read through
484
+ // `getUpload()`.
385
485
  onInputFile(newFile, oldFile) {
386
486
  if (newFile && !oldFile) {
387
487
  const { id, name, size } = newFile
388
- this.addFile({ id, name, size, upload: newFile })
488
+ this.addFile({ id, name, size, upload: { id, success: false } })
389
489
  }
390
490
  if (newFile && oldFile) {
391
491
  const { success, error } = newFile
@@ -393,9 +493,9 @@ export default DitoTypeComponent.register('upload', {
393
493
  this.onChange()
394
494
  const file = newFile.response[0]
395
495
  if (file) {
396
- file.upload = newFile
397
- // Replace the upload file object with the file object received
398
- // from the upload response.
496
+ file.upload = { id: newFile.id, success: true }
497
+ // Replace the file added for the upload with the file received
498
+ // in the upload response.
399
499
  this.replaceFile(newFile, file)
400
500
  } else {
401
501
  this.removeFile(newFile)
@@ -425,22 +525,32 @@ export default DitoTypeComponent.register('upload', {
425
525
  },
426
526
 
427
527
  onInputFilter(newFile /*, oldFile, prevent */) {
528
+ // `VueUpload` only creates the request when uploading, and has no setting
529
+ // for its credentials:
428
530
  const xhr = newFile?.xhr
429
- if (this.api.cors?.credentials && xhr && !xhr.withCredentials) {
531
+ if (this.uploadOptions?.withCredentials && xhr && !xhr.withCredentials) {
430
532
  xhr.withCredentials = true
431
533
  }
432
534
  },
433
535
 
434
536
  async onClickDownload(file, index) {
435
537
  try {
436
- const response = await fetch(this.downloadUrls[index])
437
- const blob = await response.blob()
438
- this.download({
439
- filename: file.name,
440
- url: URL.createObjectURL(blob)
441
- })
538
+ const blob = await fetchBlob(this.api, this.downloadUrls[index])
539
+ const url = URL.createObjectURL(blob)
540
+ try {
541
+ this.download({ filename: file.name, url })
542
+ } finally {
543
+ // Release the blob once the browser started the download, which
544
+ // some browsers don't do synchronously when the link is clicked.
545
+ setTimeout(() => URL.revokeObjectURL(url), 1000)
546
+ }
442
547
  } catch (error) {
443
- console.error(error)
548
+ this.notify({
549
+ type: 'error',
550
+ error,
551
+ title: 'File Download Error',
552
+ text: `Unable to download ${file.name}: ${error.message}`
553
+ })
444
554
  }
445
555
  },
446
556
 
@@ -470,6 +580,19 @@ function asFiles(value) {
470
580
  @import '../styles/_imports';
471
581
 
472
582
  .dito-upload {
583
+ // Positioned for `z-index`, to stay above the overlay of `DitoRoot` until it
584
+ // faded out:
585
+ position: relative;
586
+ transition:
587
+ filter $drag-overlay-duration,
588
+ z-index 0s $drag-overlay-duration;
589
+
590
+ &--drop-target {
591
+ z-index: $z-index-drag-overlay + 1;
592
+ filter: drop-shadow(0 4px 8px rgb(0, 0, 0, 0.25));
593
+ transition: filter $drag-overlay-duration;
594
+ }
595
+
473
596
  .dito-table {
474
597
  tr,
475
598
  .dito-table__buttons {