@conduction/nextcloud-vue 2.78.0 → 2.79.0

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 (115) hide show
  1. package/dist/bundled-packages.json +0 -2
  2. package/dist/esm/bundled-packages.json +0 -2
  3. package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js.map +1 -1
  4. package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js +113 -104
  5. package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js.map +1 -1
  6. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js +12 -2
  7. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
  8. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js.map +1 -1
  9. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js +80 -0
  10. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js.map +1 -1
  11. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js +115 -91
  12. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js.map +1 -1
  13. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +139 -1
  14. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  15. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +140 -92
  16. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  17. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +77 -0
  18. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
  19. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +4 -2
  20. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
  21. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js +61 -1
  22. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
  23. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
  24. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js +173 -5
  25. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js.map +1 -1
  26. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js +181 -99
  27. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js.map +1 -1
  28. package/dist/esm/components/CnFilesBrowser/filesBrowser.js +29 -11
  29. package/dist/esm/components/CnFilesBrowser/filesBrowser.js.map +1 -1
  30. package/dist/esm/components/CnFilesBrowser/openRegisterSource.js +252 -0
  31. package/dist/esm/components/CnFilesBrowser/openRegisterSource.js.map +1 -0
  32. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js +1 -1
  33. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
  34. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
  35. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +25 -2
  36. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  37. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +3 -2
  38. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  39. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js +100 -3
  40. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js.map +1 -1
  41. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js +4 -2
  42. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js.map +1 -1
  43. package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue2.js +3 -2
  44. package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue2.js.map +1 -1
  45. package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue3.js.map +1 -1
  46. package/dist/esm/components/CnSavedViewsControl/CnSavedViewsControl.vue2.js.map +1 -1
  47. package/dist/esm/components/CnSavedViewsControl/CnSavedViewsControl.vue3.js +1 -1
  48. package/dist/esm/components/CnSavedViewsControl/CnSavedViewsControl.vue3.js.map +1 -1
  49. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +30 -1
  50. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
  51. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
  52. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js +13 -0
  53. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js.map +1 -1
  54. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue3.js.map +1 -1
  55. package/dist/esm/composables/useAppManifest.js +8 -3
  56. package/dist/esm/composables/useAppManifest.js.map +1 -1
  57. package/dist/esm/index.js +1 -0
  58. package/dist/esm/index.js.map +1 -1
  59. package/dist/esm/l10n/en.json.js +61 -1
  60. package/dist/esm/l10n/en.json.js.map +1 -1
  61. package/dist/esm/l10n/nl.json.js +106 -2
  62. package/dist/esm/l10n/nl.json.js.map +1 -1
  63. package/dist/esm/nextcloud-vue.css +314 -15
  64. package/dist/esm/utils/boardDate.js +193 -0
  65. package/dist/esm/utils/boardDate.js.map +1 -0
  66. package/dist/esm/utils/dynamicProperties.js +5 -1
  67. package/dist/esm/utils/dynamicProperties.js.map +1 -1
  68. package/dist/esm/utils/validateManifestV2.compiled.js +57 -6
  69. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  70. package/dist/nextcloud-vue.cjs.js +2039 -581
  71. package/dist/nextcloud-vue.cjs.js.map +2 -2
  72. package/dist/nextcloud-vue.css +314 -15
  73. package/l10n/en.json +61 -1
  74. package/l10n/nl.json +106 -2
  75. package/package.json +1 -1
  76. package/scripts/build-validators.js +26 -0
  77. package/src/components/CnActionsBar/CnActionsBar.vue +48 -39
  78. package/src/components/CnAppRoot/CnAppRoot.vue +12 -2
  79. package/src/components/CnCellRenderer/CnCellRenderer.vue +119 -2
  80. package/src/components/CnDashboardPage/CnDashboardPage.vue +219 -4
  81. package/src/components/CnDataTable/CnDataTable.md +2 -0
  82. package/src/components/CnDataTable/CnDataTable.vue +81 -2
  83. package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +61 -1
  84. package/src/components/CnFilesBrowser/CnFilesBrowser.vue +232 -11
  85. package/src/components/CnFilesBrowser/filesBrowser.js +43 -2
  86. package/src/components/CnFilesBrowser/index.js +1 -0
  87. package/src/components/CnFilesBrowser/openRegisterSource.js +248 -0
  88. package/src/components/CnFormDialog/CnFormDialog.vue +1 -1
  89. package/src/components/CnIndexPage/CnIndexPage.vue +27 -3
  90. package/src/components/CnObjectSidebar/CnFilesTab.vue +103 -4
  91. package/src/components/CnQuickFilterBar/CnQuickFilterBar.vue +3 -2
  92. package/src/components/CnSavedViewsControl/CnSavedViewsControl.vue +1 -1
  93. package/src/components/CnTabsWidget/CnTabsWidget.vue +30 -1
  94. package/src/components/CnWidgetObjectTable/CnWidgetObjectTable.vue +13 -0
  95. package/src/components/index.js +1 -1
  96. package/src/composables/useAppManifest.js +8 -3
  97. package/src/css/look-board-detail.css +10 -0
  98. package/src/css/look-board-index.css +92 -14
  99. package/src/css/look-board.css +77 -0
  100. package/src/css/table.css +37 -0
  101. package/src/index.js +4 -0
  102. package/src/schemas/app-manifest-v2.schema.json +46 -1
  103. package/src/utils/boardDate.js +189 -0
  104. package/src/utils/dynamicProperties.js +5 -1
  105. package/src/utils/validateManifestV2.compiled.js +57 -2
  106. package/dist/esm/_virtual/equal.js +0 -4
  107. package/dist/esm/_virtual/equal.js.map +0 -1
  108. package/dist/esm/_virtual/ucs2length.js +0 -4
  109. package/dist/esm/_virtual/ucs2length.js.map +0 -1
  110. package/dist/esm/node_modules/ajv/dist/runtime/equal.js +0 -9
  111. package/dist/esm/node_modules/ajv/dist/runtime/equal.js.map +0 -1
  112. package/dist/esm/node_modules/ajv/dist/runtime/ucs2length.js +0 -25
  113. package/dist/esm/node_modules/ajv/dist/runtime/ucs2length.js.map +0 -1
  114. package/dist/esm/node_modules/fast-deep-equal/index.js +0 -47
  115. package/dist/esm/node_modules/fast-deep-equal/index.js.map +0 -1
@@ -71,7 +71,7 @@
71
71
  {{ columnLabel(column) }}
72
72
  </NcActionCheckbox>
73
73
  </NcActions>
74
- <NcActions
74
+ <NcActions v-if="canChange"
75
75
  :menuName="newLabel"
76
76
  :forceMenu="true"
77
77
  :forceName="true"
@@ -116,7 +116,7 @@
116
116
  <!-- A plain upload as its own button, when the host names it
117
117
  (`uploadButton`). It takes the primary place and the New
118
118
  menu steps back beside it. -->
119
- <NcButton v-if="uploadButton"
119
+ <NcButton v-if="uploadButton && canChange"
120
120
  variant="primary"
121
121
  :disabled="folder === null"
122
122
  data-testid="cn-files-browser-upload-button"
@@ -166,7 +166,7 @@
166
166
  <CnEmptyContent
167
167
  v-else-if="sorted.length === 0 && linkedItems.length === 0"
168
168
  :name="emptyLabel"
169
- :description="emptyHint"
169
+ :description="canChange ? emptyHint : ''"
170
170
  class="cn-files-browser__state">
171
171
  <template #icon>
172
172
  <FolderOutline :size="44" />
@@ -284,14 +284,40 @@
284
284
  <NcActionSeparator v-if="actionsFor(node).length > 0 || (!isFolder(node) && rowActions.length > 0)" />
285
285
  <!-- The Files app's own rename is its list's inline input,
286
286
  which is not here; this one is a dialog over a DAV move. -->
287
- <NcActionButton :closeAfterClick="true" data-testid="cn-files-browser-action-rename" @click="askRename(node)">
287
+ <!-- With a `source`, the Files app's actions (which act over
288
+ WebDAV) are not offered; download and delete go through
289
+ the source instead. -->
290
+ <NcActionLink
291
+ v-if="source && sourceDownloadUrl(node)"
292
+ :href="sourceDownloadUrl(node)"
293
+ :download="node.basename"
294
+ :closeAfterClick="true"
295
+ data-testid="cn-files-browser-action-download">
296
+ <template #icon>
297
+ <Download :size="20" />
298
+ </template>
299
+ {{ downloadLabel }}
300
+ </NcActionLink>
301
+ <NcActionButton v-if="source && canChange"
302
+ :closeAfterClick="true"
303
+ data-testid="cn-files-browser-action-delete"
304
+ @click="askRemove(node)">
305
+ <template #icon>
306
+ <TrashCanOutline :size="20" />
307
+ </template>
308
+ {{ deleteLabel }}
309
+ </NcActionButton>
310
+ <NcActionButton v-if="canChange"
311
+ :closeAfterClick="true"
312
+ data-testid="cn-files-browser-action-rename"
313
+ @click="askRename(node)">
288
314
  <template #icon>
289
315
  <Pencil :size="20" />
290
316
  </template>
291
317
  {{ renameLabel }}
292
318
  </NcActionButton>
293
319
  <NcActionLink
294
- v-if="node.fileid"
320
+ v-if="node.fileid && !source"
295
321
  :href="permalink(node)"
296
322
  target="_blank"
297
323
  :closeAfterClick="true">
@@ -376,7 +402,7 @@
376
402
  </tbody>
377
403
  </table>
378
404
 
379
- <p v-if="dropHint && !loading && error === ''"
405
+ <p v-if="dropHint && canChange && !loading && error === ''"
380
406
  class="cn-files-browser__drop-hint"
381
407
  data-testid="cn-files-browser-drop-hint">
382
408
  <Upload :size="16" />
@@ -410,6 +436,26 @@
410
436
  </template>
411
437
  </NcDialog>
412
438
 
439
+ <NcDialog
440
+ v-if="removing !== null"
441
+ :name="deleteLabel"
442
+ size="small"
443
+ data-testid="cn-files-browser-delete"
444
+ @closing="removing = null">
445
+ <p>{{ t('nextcloud-vue', 'Delete {name}? This cannot be undone here.', { name: removing.basename }) }}</p>
446
+ <p v-if="removeError" class="cn-files-browser__upload-error" role="alert">
447
+ {{ removeError }}
448
+ </p>
449
+ <template #actions>
450
+ <NcButton @click="removing = null">
451
+ {{ t('nextcloud-vue', 'Cancel') }}
452
+ </NcButton>
453
+ <NcButton variant="error" data-testid="cn-files-browser-delete-confirm" @click="remove">
454
+ {{ deleteLabel }}
455
+ </NcButton>
456
+ </template>
457
+ </NcDialog>
458
+
413
459
  <NcDialog
414
460
  v-if="newFolderOpen"
415
461
  :name="newFolderLabel"
@@ -494,6 +540,7 @@ import FolderOutline from 'vue-material-design-icons/FolderOutline.vue'
494
540
  import OpenInNew from 'vue-material-design-icons/OpenInNew.vue'
495
541
  import Pencil from 'vue-material-design-icons/Pencil.vue'
496
542
  import Plus from 'vue-material-design-icons/Plus.vue'
543
+ import TrashCanOutline from 'vue-material-design-icons/TrashCanOutline.vue'
497
544
  import Upload from 'vue-material-design-icons/Upload.vue'
498
545
  import CnCellRenderer from '../CnCellRenderer/CnCellRenderer.vue'
499
546
  import CnEmptyContent from '../CnEmptyContent/CnEmptyContent.vue'
@@ -543,6 +590,7 @@ export default {
543
590
  OpenInNew,
544
591
  Pencil,
545
592
  Plus,
593
+ TrashCanOutline,
546
594
  Upload,
547
595
  },
548
596
 
@@ -553,6 +601,30 @@ export default {
553
601
  },
554
602
 
555
603
  props: {
604
+ /**
605
+ * Where the folder is read from and written to. Null (the default)
606
+ * is WebDAV under the signed-in person's files root, with the Files
607
+ * app's own actions and New menu. An object from
608
+ * `createOpenRegisterSource()` reads one OpenRegister object's folder
609
+ * through OpenRegister's files API instead, under the object's access
610
+ * rule: `rootPath` is then `/`, the Files app's actions and New menu
611
+ * are not offered (they act over WebDAV), and upload, new folder,
612
+ * rename and delete appear only when the listing says the person may
613
+ * change the folder.
614
+ *
615
+ * @type {object|null}
616
+ */
617
+ source: {
618
+ type: Object,
619
+ default: null,
620
+ },
621
+
622
+ /** Label of the delete action and its dialog, with a `source`. */
623
+ deleteLabel: {
624
+ type: String,
625
+ default: () => t('nextcloud-vue', 'Delete'),
626
+ },
627
+
556
628
  /**
557
629
  * The host's own actions on each file row, declared the way a
558
630
  * manifest declares any action (`open-modal`, `handler`, ...), and
@@ -823,6 +895,9 @@ export default {
823
895
  renaming: null,
824
896
  renameName: '',
825
897
  renameError: '',
898
+ /** The row being deleted (with a `source`), or null. */
899
+ removing: null,
900
+ removeError: '',
826
901
  fileInputEl: null,
827
902
  /**
828
903
  * The view the Files app's actions are handed. They read `view.id`
@@ -842,6 +917,20 @@ export default {
842
917
  },
843
918
 
844
919
  computed: {
920
+ /**
921
+ * Whether this person may add to and change the open folder. Over
922
+ * WebDAV the server answers each write on its own, so this is true;
923
+ * with a `source` it is what the listing said.
924
+ *
925
+ * @return {boolean} True when upload, new folder, rename and delete are offered.
926
+ */
927
+ canChange() {
928
+ if (!this.source) {
929
+ return true
930
+ }
931
+ return this.folder !== null && this.folder.canChange === true
932
+ },
933
+
845
934
  /**
846
935
  * The breadcrumb trail from the root to the open folder.
847
936
  *
@@ -946,7 +1035,7 @@ export default {
946
1035
  * @return {Array<object>} The entries, in the plugins' order.
947
1036
  */
948
1037
  newMenuEntries() {
949
- if (this.folder === null) {
1038
+ if (this.folder === null || this.source) {
950
1039
  return []
951
1040
  }
952
1041
  try {
@@ -1004,6 +1093,10 @@ export default {
1004
1093
  async refresh() {
1005
1094
  this.loading = true
1006
1095
  this.error = ''
1096
+ if (this.source) {
1097
+ await this.refreshFromSource()
1098
+ return
1099
+ }
1007
1100
  try {
1008
1101
  const client = getClient()
1009
1102
  const { data } = await client.getDirectoryContents(this.davPath(this.currentPath), {
@@ -1031,6 +1124,70 @@ export default {
1031
1124
  }
1032
1125
  },
1033
1126
 
1127
+ /**
1128
+ * List the open folder through the `source`.
1129
+ *
1130
+ * @return {Promise<void>}
1131
+ */
1132
+ async refreshFromSource() {
1133
+ try {
1134
+ const { folder, nodes } = await this.source.list(this.currentPath)
1135
+ this.folder = folder
1136
+ this.nodes = nodes
1137
+ await this.resolveRowData(this.nodes)
1138
+ } catch (err) {
1139
+ const atRoot = (this.currentPath.replace(/\/+$/, '') || '/') === (this.rootPath.replace(/\/+$/, '') || '/')
1140
+ this.error = err?.status === 404
1141
+ ? (atRoot ? t('nextcloud-vue', 'You cannot see these files') : t('nextcloud-vue', 'This folder no longer exists'))
1142
+ : (err?.status === 403 ? t('nextcloud-vue', 'You cannot see this folder') : t('nextcloud-vue', 'The folder could not be read'))
1143
+ this.folder = null
1144
+ this.nodes = []
1145
+ } finally {
1146
+ this.loading = false
1147
+ }
1148
+ },
1149
+
1150
+ /**
1151
+ * Where a file downloads from, through the `source`.
1152
+ *
1153
+ * @param {object} node The row.
1154
+ * @return {string|null} The url.
1155
+ */
1156
+ sourceDownloadUrl(node) {
1157
+ return this.source && typeof this.source.downloadUrl === 'function' ? this.source.downloadUrl(node) : null
1158
+ },
1159
+
1160
+ /**
1161
+ * Open the delete dialog on a row.
1162
+ *
1163
+ * @param {object} node The row.
1164
+ * @return {void}
1165
+ */
1166
+ askRemove(node) {
1167
+ this.removing = node
1168
+ this.removeError = ''
1169
+ },
1170
+
1171
+ /**
1172
+ * Delete the row named in the dialog, through the `source`.
1173
+ *
1174
+ * @return {Promise<void>}
1175
+ */
1176
+ async remove() {
1177
+ const node = this.removing
1178
+ if (node === null || !this.source) {
1179
+ return
1180
+ }
1181
+ try {
1182
+ await this.source.remove(node)
1183
+ this.removing = null
1184
+ await this.refresh()
1185
+ this.$emit('changed')
1186
+ } catch (err) {
1187
+ this.removeError = err?.message || t('nextcloud-vue', '{action} did not succeed', { action: this.deleteLabel })
1188
+ }
1189
+ },
1190
+
1034
1191
  /**
1035
1192
  * Open a folder below the root.
1036
1193
  *
@@ -1068,6 +1225,13 @@ export default {
1068
1225
  this.navigate(node.path)
1069
1226
  return
1070
1227
  }
1228
+ if (this.source) {
1229
+ const url = this.sourceDownloadUrl(node)
1230
+ if (url) {
1231
+ window.open(url, '_blank', 'noopener,noreferrer')
1232
+ }
1233
+ return
1234
+ }
1071
1235
  const view = this.actionsFor(node).find((action) => action.id === 'view')
1072
1236
  if (view) {
1073
1237
  await this.run(view, node)
@@ -1114,6 +1278,16 @@ export default {
1114
1278
  * @return {void}
1115
1279
  */
1116
1280
  openInNewTab(node) {
1281
+ if (this.source) {
1282
+ if (this.isFolder(node)) {
1283
+ return
1284
+ }
1285
+ const url = this.sourceDownloadUrl(node)
1286
+ if (url) {
1287
+ window.open(url, '_blank', 'noopener,noreferrer')
1288
+ }
1289
+ return
1290
+ }
1117
1291
  if (node.fileid) {
1118
1292
  window.open(this.permalink(node), '_blank', 'noopener,noreferrer')
1119
1293
  }
@@ -1141,6 +1315,9 @@ export default {
1141
1315
  * @return {Array<object>} The actions.
1142
1316
  */
1143
1317
  actionsFor(node) {
1318
+ if (this.source) {
1319
+ return []
1320
+ }
1144
1321
  let actions
1145
1322
  try {
1146
1323
  actions = getFileActions()
@@ -1270,7 +1447,11 @@ export default {
1270
1447
  return
1271
1448
  }
1272
1449
  try {
1273
- await getClient().createDirectory(this.davPath(joinPath(this.currentPath, name)))
1450
+ if (this.source) {
1451
+ await this.source.createFolder(this.currentPath, name)
1452
+ } else {
1453
+ await getClient().createDirectory(this.davPath(joinPath(this.currentPath, name)))
1454
+ }
1274
1455
  this.newFolderOpen = false
1275
1456
  await this.refresh()
1276
1457
  this.$emit('changed')
@@ -1311,7 +1492,11 @@ export default {
1311
1492
  return
1312
1493
  }
1313
1494
  try {
1314
- await getClient().moveFile(this.davPath(node.path), this.davPath(joinPath(this.currentPath, name)))
1495
+ if (this.source) {
1496
+ await this.source.rename(node, name)
1497
+ } else {
1498
+ await getClient().moveFile(this.davPath(node.path), this.davPath(joinPath(this.currentPath, name)))
1499
+ }
1315
1500
  this.renaming = null
1316
1501
  await this.refresh()
1317
1502
  this.$emit('changed')
@@ -1355,6 +1540,9 @@ export default {
1355
1540
  this.dragging = false
1356
1541
  this.dragDepth = 0
1357
1542
  this.dropStatus = ''
1543
+ if (!this.canChange) {
1544
+ return
1545
+ }
1358
1546
  const files = event.dataTransfer?.files
1359
1547
  if (files?.length) {
1360
1548
  await this.uploadFiles(files)
@@ -1372,7 +1560,7 @@ export default {
1372
1560
  * @return {void}
1373
1561
  */
1374
1562
  onDragEnter(event) {
1375
- if (!this.carriesFiles(event)) {
1563
+ if (!this.canChange || !this.carriesFiles(event)) {
1376
1564
  return
1377
1565
  }
1378
1566
  this.dragDepth++
@@ -1389,7 +1577,7 @@ export default {
1389
1577
  * @return {void}
1390
1578
  */
1391
1579
  onDragOver(event) {
1392
- if (this.carriesFiles(event)) {
1580
+ if (this.canChange && this.carriesFiles(event)) {
1393
1581
  this.dragging = true
1394
1582
  }
1395
1583
  },
@@ -1442,6 +1630,10 @@ export default {
1442
1630
  const batch = [...files].map((file) => ({ key: `u${++uploadSeq}`, name: file.name, progress: 0, error: '', file }))
1443
1631
  this.uploads = [...this.uploads, ...batch]
1444
1632
  for (const upload of batch) {
1633
+ if (this.source) {
1634
+ await this.uploadThroughSource(upload)
1635
+ continue
1636
+ }
1445
1637
  try {
1446
1638
  await axios.put(`${getRemoteURL()}${this.davPath(joinPath(this.currentPath, upload.name))}`, upload.file, {
1447
1639
  headers: {
@@ -1470,6 +1662,25 @@ export default {
1470
1662
  }, 1500)
1471
1663
  },
1472
1664
 
1665
+ /**
1666
+ * Store one upload through the `source`, keeping its progress row.
1667
+ *
1668
+ * @param {object} upload The progress row, carrying the file.
1669
+ * @return {Promise<void>}
1670
+ */
1671
+ async uploadThroughSource(upload) {
1672
+ try {
1673
+ await this.source.upload(this.currentPath, upload.file, (percent) => {
1674
+ upload.progress = percent
1675
+ })
1676
+ upload.progress = 100
1677
+ } catch (err) {
1678
+ upload.error = err?.status === 409
1679
+ ? t('nextcloud-vue', 'A file with that name is already here')
1680
+ : (err?.message || t('nextcloud-vue', 'The upload failed'))
1681
+ }
1682
+ },
1683
+
1473
1684
  /**
1474
1685
  * Change the sort column, or flip the order on the same one.
1475
1686
  *
@@ -1702,6 +1913,9 @@ export default {
1702
1913
  if (!node.fileid || this.isFolder(node) || !/^image\//i.test(mime) || /svg/i.test(mime) || this.previewFailed[node.fileid]) {
1703
1914
  return null
1704
1915
  }
1916
+ if (this.source) {
1917
+ return typeof this.source.previewUrl === 'function' ? this.source.previewUrl(node) : null
1918
+ }
1705
1919
  return generateUrl('/core/preview?fileId={fileId}&x=64&y=64&a=1', { fileId: node.fileid })
1706
1920
  },
1707
1921
 
@@ -1880,6 +2094,13 @@ export default {
1880
2094
  flex: 1 1 auto;
1881
2095
  }
1882
2096
 
2097
+ /* The crumbs give way, the controls do not: a long crumb (a folder named
2098
+ after a uuid) shrank the upload button to "Bestanden..." while the crumbs
2099
+ kept their width. */
2100
+ .cn-files-browser__bar > :not(.cn-files-browser__crumbs) {
2101
+ flex-shrink: 0;
2102
+ }
2103
+
1883
2104
  .cn-files-browser__file-input {
1884
2105
  display: none;
1885
2106
  }
@@ -151,14 +151,55 @@ export function firstHrefIn(xml) {
151
151
  * @param {string} options.remoteUrl The DAV remote url, `…/remote.php/dav`.
152
152
  * @return {Promise<string|null>} The user-relative folder path, or null.
153
153
  */
154
- export async function resolveObjectFolder({ apiBase, register, schema, objectId, uid, remoteUrl }) {
154
+ export async function resolveObjectFolder(options) {
155
+ const info = await resolveObjectFolderInfo(options)
156
+ return info === null ? null : info.path
157
+ }
158
+
159
+ /**
160
+ * The object's display name: `@self.name`, else `title`, else `name`. A
161
+ * value equal to the object's uuid is no name (the folder already reads so).
162
+ *
163
+ * @param {object|null} object The object as OpenRegister returns it.
164
+ * @param {string} objectId The object's uuid.
165
+ * @return {string|null} The name, or null.
166
+ * @spec openspec/changes/dutch-library-strings-and-files-crumb/specs/dutch-library-strings-and-files-crumb/spec.md#requirement-the-files-browser-names-the-object-in-its-root-crumb
167
+ */
168
+ export function objectDisplayName(object, objectId) {
169
+ const candidates = [object?.['@self']?.name, object?.title, object?.name]
170
+ for (const candidate of candidates) {
171
+ if (typeof candidate === 'string' && candidate.trim() !== '' && candidate.trim() !== String(objectId)) {
172
+ return candidate.trim()
173
+ }
174
+ }
175
+ return null
176
+ }
177
+
178
+ /**
179
+ * `resolveObjectFolder`, plus the object's display name from the same read,
180
+ * so the browser can name the root crumb after the object instead of the
181
+ * uuid its folder is called on disk.
182
+ *
183
+ * @param {object} options The lookup, as for `resolveObjectFolder`.
184
+ * @param {string} options.apiBase OpenRegister's API base, `/apps/openregister/api`.
185
+ * @param {string} options.register The register slug.
186
+ * @param {string} options.schema The schema slug.
187
+ * @param {string} options.objectId The object's uuid.
188
+ * @param {string} options.uid The current user's id.
189
+ * @param {string} options.remoteUrl The DAV remote url, `…/remote.php/dav`.
190
+ * @return {Promise<{path: string, name: (string|null)}|null>} The folder and the object's name, or null.
191
+ * @spec openspec/changes/dutch-library-strings-and-files-crumb/specs/dutch-library-strings-and-files-crumb/spec.md#requirement-the-files-browser-names-the-object-in-its-root-crumb
192
+ */
193
+ export async function resolveObjectFolderInfo({ apiBase, register, schema, objectId, uid, remoteUrl }) {
155
194
  if (!register || !schema || !objectId || !uid) {
156
195
  return null
157
196
  }
158
197
  let folderId
198
+ let name
159
199
  try {
160
200
  const { data } = await axios.get(generateUrl(`${apiBase}/objects/{register}/{schema}/{objectId}`, { register, schema, objectId }))
161
201
  folderId = data?.['@self']?.folder ?? null
202
+ name = objectDisplayName(data, objectId)
162
203
  } catch {
163
204
  return null
164
205
  }
@@ -174,7 +215,7 @@ export async function resolveObjectFolder({ apiBase, register, schema, objectId,
174
215
  responseType: 'text',
175
216
  })
176
217
  const href = firstHrefIn(response.data)
177
- return href === null ? null : userRelativePathFromHref(href, uid)
218
+ return href === null ? null : { path: userRelativePathFromHref(href, uid), name }
178
219
  } catch {
179
220
  return null
180
221
  }
@@ -5,5 +5,6 @@
5
5
  import CnFilesBrowser from './CnFilesBrowser.vue'
6
6
 
7
7
  export { CnFilesBrowser }
8
+ export { createOpenRegisterSource, entryToNode, toApiPath } from './openRegisterSource.js'
8
9
  export { ACTIONS_NEEDING_THE_FILES_PAGE, crumbsFor, joinPath, resolveObjectFolder, userRelativePathFromHref } from './filesBrowser.js'
9
10
  export default CnFilesBrowser