@conduction/nextcloud-vue 2.100.0 → 2.102.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 (168) hide show
  1. package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js +22 -0
  2. package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js.map +1 -1
  3. package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js +8 -6
  4. package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js.map +1 -1
  5. package/dist/esm/components/CnChartWidget/CnChartWidget.vue.js +1 -1
  6. package/dist/esm/components/CnChartWidget/CnChartWidget.vue2.js +29 -4
  7. package/dist/esm/components/CnChartWidget/CnChartWidget.vue2.js.map +1 -1
  8. package/dist/esm/components/CnChartWidget/CnChartWidget.vue3.js +9 -1
  9. package/dist/esm/components/CnChartWidget/CnChartWidget.vue3.js.map +1 -1
  10. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue.js +1 -1
  11. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue2.js +22 -1
  12. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue2.js.map +1 -1
  13. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue3.js +26 -16
  14. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue3.js.map +1 -1
  15. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue.js +1 -1
  16. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +70 -6
  17. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  18. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +105 -74
  19. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  20. package/dist/esm/components/CnExportsWidget/CnExportsWidget.vue.js +9 -0
  21. package/dist/esm/components/CnExportsWidget/CnExportsWidget.vue.js.map +1 -0
  22. package/dist/esm/components/CnExportsWidget/CnExportsWidget.vue2.js +139 -0
  23. package/dist/esm/components/CnExportsWidget/CnExportsWidget.vue2.js.map +1 -0
  24. package/dist/esm/components/CnExportsWidget/CnExportsWidget.vue3.js +76 -0
  25. package/dist/esm/components/CnExportsWidget/CnExportsWidget.vue3.js.map +1 -0
  26. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue.js +1 -1
  27. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js +160 -2
  28. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js.map +1 -1
  29. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js +206 -94
  30. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js.map +1 -1
  31. package/dist/esm/components/CnFilesBrowser/filesReview.js +61 -0
  32. package/dist/esm/components/CnFilesBrowser/filesReview.js.map +1 -0
  33. package/dist/esm/components/CnGroupedCardsWidget/CnGroupedCardsWidget.vue.js +9 -0
  34. package/dist/esm/components/CnGroupedCardsWidget/CnGroupedCardsWidget.vue.js.map +1 -0
  35. package/dist/esm/components/CnGroupedCardsWidget/CnGroupedCardsWidget.vue2.js +279 -0
  36. package/dist/esm/components/CnGroupedCardsWidget/CnGroupedCardsWidget.vue2.js.map +1 -0
  37. package/dist/esm/components/CnGroupedCardsWidget/CnGroupedCardsWidget.vue3.js +122 -0
  38. package/dist/esm/components/CnGroupedCardsWidget/CnGroupedCardsWidget.vue3.js.map +1 -0
  39. package/dist/esm/components/CnGroupedCardsWidget/index.js +29 -0
  40. package/dist/esm/components/CnGroupedCardsWidget/index.js.map +1 -0
  41. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +320 -74
  42. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  43. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +323 -213
  44. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  45. package/dist/esm/components/CnIndexPage/useNamedSource.js +30 -3
  46. package/dist/esm/components/CnIndexPage/useNamedSource.js.map +1 -1
  47. package/dist/esm/components/CnMapWidget/CnMapWidget.vue.js +1 -1
  48. package/dist/esm/components/CnMapWidget/CnMapWidget.vue2.js +15 -0
  49. package/dist/esm/components/CnMapWidget/CnMapWidget.vue2.js.map +1 -1
  50. package/dist/esm/components/CnMapWidget/CnMapWidget.vue3.js.map +1 -1
  51. package/dist/esm/components/CnMenuWidget/CnMenuWidget.vue.js +1 -1
  52. package/dist/esm/components/CnMenuWidget/CnMenuWidget.vue2.js +25 -1
  53. package/dist/esm/components/CnMenuWidget/CnMenuWidget.vue2.js.map +1 -1
  54. package/dist/esm/components/CnMenuWidget/CnMenuWidget.vue3.js.map +1 -1
  55. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue.js +1 -1
  56. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +14 -1
  57. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
  58. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js +14 -6
  59. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js.map +1 -1
  60. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue.js +1 -1
  61. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js +10 -0
  62. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js.map +1 -1
  63. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js +6 -1
  64. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js.map +1 -1
  65. package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue.js +1 -1
  66. package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue2.js +91 -4
  67. package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue2.js.map +1 -1
  68. package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue3.js +63 -38
  69. package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue3.js.map +1 -1
  70. package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue.js +1 -1
  71. package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue2.js +106 -4
  72. package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue2.js.map +1 -1
  73. package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue3.js +70 -25
  74. package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue3.js.map +1 -1
  75. package/dist/esm/components/CnSavedViewAlertWidget/CnSavedViewAlertWidget.vue.js +9 -0
  76. package/dist/esm/components/CnSavedViewAlertWidget/CnSavedViewAlertWidget.vue.js.map +1 -0
  77. package/dist/esm/components/CnSavedViewAlertWidget/CnSavedViewAlertWidget.vue2.js +175 -0
  78. package/dist/esm/components/CnSavedViewAlertWidget/CnSavedViewAlertWidget.vue2.js.map +1 -0
  79. package/dist/esm/components/CnSavedViewAlertWidget/CnSavedViewAlertWidget.vue3.js +77 -0
  80. package/dist/esm/components/CnSavedViewAlertWidget/CnSavedViewAlertWidget.vue3.js.map +1 -0
  81. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue.js +1 -1
  82. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js +35 -2
  83. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js.map +1 -1
  84. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue3.js.map +1 -1
  85. package/dist/esm/components/CnWidgetGrid/builtInWidgets.js +4 -0
  86. package/dist/esm/components/CnWidgetGrid/builtInWidgets.js.map +1 -1
  87. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js +1 -0
  88. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js.map +1 -1
  89. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue.js +1 -1
  90. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js +2 -23
  91. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js.map +1 -1
  92. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue3.js.map +1 -1
  93. package/dist/esm/composables/indexSources.js +176 -20
  94. package/dist/esm/composables/indexSources.js.map +1 -1
  95. package/dist/esm/composables/useEndpointSource.js +28 -6
  96. package/dist/esm/composables/useEndpointSource.js.map +1 -1
  97. package/dist/esm/index.js +1 -0
  98. package/dist/esm/index.js.map +1 -1
  99. package/dist/esm/l10n/en.json.js +41 -1
  100. package/dist/esm/l10n/en.json.js.map +1 -1
  101. package/dist/esm/l10n/nl.json.js +39 -1
  102. package/dist/esm/l10n/nl.json.js.map +1 -1
  103. package/dist/esm/library-style.js +1 -1
  104. package/dist/esm/nextcloud-vue.css +939 -376
  105. package/dist/esm/utils/builtInFormatters.js +24 -4
  106. package/dist/esm/utils/builtInFormatters.js.map +1 -1
  107. package/dist/esm/utils/languageMapText.js +50 -0
  108. package/dist/esm/utils/languageMapText.js.map +1 -0
  109. package/dist/esm/utils/libraryWidgetKeys.js +3 -0
  110. package/dist/esm/utils/libraryWidgetKeys.js.map +1 -1
  111. package/dist/esm/utils/mapList.js +71 -0
  112. package/dist/esm/utils/mapList.js.map +1 -0
  113. package/dist/esm/utils/rowClassRules.js +43 -0
  114. package/dist/esm/utils/rowClassRules.js.map +1 -0
  115. package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
  116. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  117. package/dist/esm/utils/visibleWhen.js +19 -2
  118. package/dist/esm/utils/visibleWhen.js.map +1 -1
  119. package/dist/nextcloud-vue.cjs.js +9681 -7136
  120. package/dist/nextcloud-vue.cjs.js.map +2 -2
  121. package/dist/nextcloud-vue.css +939 -376
  122. package/l10n/en.json +41 -1
  123. package/l10n/nl.json +39 -1
  124. package/package.json +1 -1
  125. package/src/components/CnActionsBar/CnActionsBar.md +1 -0
  126. package/src/components/CnActionsBar/CnActionsBar.vue +31 -4
  127. package/src/components/CnChartWidget/CnChartWidget.md +3 -2
  128. package/src/components/CnChartWidget/CnChartWidget.vue +42 -4
  129. package/src/components/CnChoiceCards/CnChoiceCards.md +39 -0
  130. package/src/components/CnChoiceCards/CnChoiceCards.vue +93 -16
  131. package/src/components/CnDashboardPage/CnDashboardPage.vue +110 -21
  132. package/src/components/CnExportsWidget/CnExportsWidget.vue +233 -0
  133. package/src/components/CnExportsWidget/index.js +7 -0
  134. package/src/components/CnFilesBrowser/CnFilesBrowser.vue +324 -0
  135. package/src/components/CnFilesBrowser/filesReview.js +57 -0
  136. package/src/components/CnGroupedCardsWidget/CnGroupedCardsWidget.vue +433 -0
  137. package/src/components/CnGroupedCardsWidget/index.js +28 -0
  138. package/src/components/CnIndexPage/CnIndexPage.md +5 -1
  139. package/src/components/CnIndexPage/CnIndexPage.vue +313 -9
  140. package/src/components/CnIndexPage/useNamedSource.js +30 -3
  141. package/src/components/CnMapWidget/CnMapWidget.vue +37 -0
  142. package/src/components/CnMenuWidget/CnMenuWidget.vue +25 -1
  143. package/src/components/CnObjectListWidget/CnObjectListWidget.vue +29 -1
  144. package/src/components/CnObjectSidebar/CnFilesTab.vue +15 -0
  145. package/src/components/CnOrderedListField/CnOrderedListField.vue +151 -5
  146. package/src/components/CnQuickFilterBar/CnQuickFilterBar.md +1 -0
  147. package/src/components/CnQuickFilterBar/CnQuickFilterBar.vue +159 -5
  148. package/src/components/CnSavedViewAlertWidget/CnSavedViewAlertWidget.vue +272 -0
  149. package/src/components/CnSavedViewAlertWidget/index.js +7 -0
  150. package/src/components/CnSetupWizard/CnSetupWizard.vue +35 -2
  151. package/src/components/CnWidgetGrid/builtInWidgets.js +4 -0
  152. package/src/components/CnWidgetGrid/registerDashboardWidgets.js +1 -0
  153. package/src/components/CnWidgetObjectTable/CnWidgetObjectTable.vue +2 -23
  154. package/src/components/index.js +3 -0
  155. package/src/composables/indexSources.js +175 -19
  156. package/src/composables/useEndpointSource.js +28 -6
  157. package/src/css/index-page.css +117 -0
  158. package/src/css/look-board-index.css +10 -0
  159. package/src/css/look-board.css +44 -0
  160. package/src/index.js +1 -0
  161. package/src/schemas/app-manifest-v2.schema.json +85 -3
  162. package/src/utils/builtInFormatters.js +24 -4
  163. package/src/utils/languageMapText.js +46 -0
  164. package/src/utils/libraryWidgetKeys.js +3 -0
  165. package/src/utils/mapList.js +67 -0
  166. package/src/utils/rowClassRules.js +39 -0
  167. package/src/utils/validateManifestV2.compiled.js +3 -3
  168. package/src/utils/visibleWhen.js +19 -2
@@ -113,6 +113,22 @@
113
113
  {{ hostLabel(action) }}
114
114
  </NcActionButton>
115
115
  </NcActions>
116
+ <!-- The host's own buttons in the bar ("Vraag een bestand aan een
117
+ partij", DqZaakDocumenten), dispatched like a New-menu entry with
118
+ the current folder; `#header-actions` adds anything else. -->
119
+ <NcButton v-for="action in headerActions"
120
+ :key="`header-${action.id}`"
121
+ :variant="action.variant === 'primary' ? 'primary' : 'secondary'"
122
+ :disabled="folder === null"
123
+ :data-testid="`cn-files-browser-header-action-${action.id}`"
124
+ @click="runHostAction(action, null)">
125
+ <template v-if="action.icon" #icon>
126
+ <CnIcon :name="action.icon" :size="20" />
127
+ </template>
128
+ {{ hostLabel(action) }}
129
+ </NcButton>
130
+ <!-- @slot header-actions Buttons in the bar, before the upload button. -->
131
+ <slot name="header-actions" />
116
132
  <!-- A plain upload as its own button, when the host names it
117
133
  (`uploadButton`). It takes the primary place and the New
118
134
  menu steps back beside it. -->
@@ -147,6 +163,31 @@
147
163
  </li>
148
164
  </ul>
149
165
 
166
+ <!-- The review summary (`reviewSummary`): how many files are reviewed,
167
+ a bar in the review tones and a legend with the count per value. -->
168
+ <div v-if="review && review.total > 0 && !loading && error === ''"
169
+ class="cn-files-browser__review"
170
+ data-testid="cn-files-browser-review">
171
+ <div class="cn-files-browser__review-head">
172
+ <strong class="cn-files-browser__review-title">{{ reviewTitle }}</strong>
173
+ <span v-if="reviewSubtitle" class="cn-files-browser__review-sub">{{ reviewSubtitle }}</span>
174
+ </div>
175
+ <span class="cn-files-browser__review-bar" role="img" :aria-label="reviewTitle">
176
+ <span v-for="segment in review.segments"
177
+ v-show="segment.count > 0 && !isOpenValue(segment.value)"
178
+ :key="segment.value"
179
+ :class="`cn-files-browser__review-part cn-files-browser__review-part--${segment.tone}`"
180
+ :style="{ width: `${segment.share}%` }" />
181
+ </span>
182
+ <ul class="cn-files-browser__review-legend">
183
+ <li v-for="segment in review.segments" :key="segment.value" data-testid="cn-files-browser-review-entry">
184
+ <span :class="`cn-files-browser__review-dot cn-files-browser__review-part--${segment.tone}`" aria-hidden="true" />
185
+ {{ hostLabel(segment) }}
186
+ <strong>{{ segment.count }}</strong>
187
+ </li>
188
+ </ul>
189
+ </div>
190
+
150
191
  <!-- The host's filter: per declared column, a chip per value in use
151
192
  with how many files carry it. Chips narrow the list. -->
152
193
  <div v-if="facetGroups.length > 0 && !loading && error === ''"
@@ -330,6 +371,9 @@
330
371
  :class="`cn-files-browser__col-${column.key}`"
331
372
  :data-testid="`cn-files-browser-cell-${column.key}`">
332
373
  <span v-if="column.key === 'name'" class="cn-files-browser__name">{{ node.basename }}</span>
374
+ <span v-if="column.key === 'name' && nameSecondaryOf(node)"
375
+ class="cn-files-browser__name-secondary"
376
+ data-testid="cn-files-browser-name-secondary">{{ nameSecondaryOf(node) }}</span>
333
377
  <template v-else-if="column.key === 'size'">
334
378
  {{ isFolder(node) ? '' : formatSize(node.size) }}
335
379
  </template>
@@ -347,6 +391,16 @@
347
391
  :row="node" />
348
392
  </td>
349
393
  <td class="cn-files-browser__col-actions" @click.stop @auxclick.stop>
394
+ <!-- The host's one visible button on a file ("Beoordelen"),
395
+ primary while the row still asks for it. -->
396
+ <NcButton v-if="rowButton && !isFolder(node) && hostActionApplies(rowButton, node)"
397
+ :variant="rowButtonPrimary(node) ? 'primary' : 'secondary'"
398
+ class="cn-files-browser__row-button"
399
+ :aria-label="t('nextcloud-vue', '{action}: {name}', { action: hostLabel(rowButton), name: node.basename })"
400
+ data-testid="cn-files-browser-row-button"
401
+ @click="runHostAction(rowButton, node)">
402
+ {{ hostLabel(rowButton) }}
403
+ </NcButton>
350
404
  <NcActions :forceMenu="true" :ariaLabel="t('nextcloud-vue', 'Actions for {name}', { name: node.basename })">
351
405
  <NcActionButton
352
406
  v-for="action in actionsFor(node)"
@@ -548,6 +602,14 @@
548
602
  <Upload :size="16" />
549
603
  {{ dropHintLabel }}
550
604
  </p>
605
+ <!-- The host's own line under the list ("Elk bestand wordt bij het
606
+ uploaden op virussen gescand", DqDocumentenBeheer), apart from
607
+ the drop hint. -->
608
+ <p v-if="footnote && !loading && error === ''"
609
+ class="cn-files-browser__footnote"
610
+ data-testid="cn-files-browser-footnote">
611
+ {{ cnTranslate(footnote) }}
612
+ </p>
551
613
 
552
614
  <NcDialog
553
615
  v-if="renaming !== null"
@@ -688,6 +750,7 @@ import CnEmptyContent from '../CnEmptyContent/CnEmptyContent.vue'
688
750
  import CnIcon from '../CnIcon/CnIcon.vue'
689
751
  import { readUserPreference, writeUserPreference } from '../../composables/useUserPreferences.js'
690
752
  import { dispatchAction } from '../../utils/actionsDispatcher.js'
753
+ import { fillTemplate } from '../../utils/fillTemplate.js'
691
754
  import { isNewTabClick, isRowMiddleClick, preventMiddleClickAutoscroll } from '../../utils/rowAuxClick.js'
692
755
  import { ACTIONS_NEEDING_THE_FILES_PAGE, crumbsFor, hostActionApplies, joinPath } from './filesBrowser.js'
693
756
  import {
@@ -700,6 +763,7 @@ import {
700
763
  sortNodesByColumn,
701
764
  visibleFileColumns,
702
765
  } from './filesBrowserColumns.js'
766
+ import { reviewSummary } from './filesReview.js'
703
767
 
704
768
  let uploadSeq = 0
705
769
 
@@ -811,6 +875,50 @@ export default {
811
875
  default: () => [],
812
876
  },
813
877
 
878
+ /**
879
+ * The host's own buttons in the bar, before the upload button ("Vraag
880
+ * een bestand aan een partij", DqZaakDocumenten). Each is dispatched
881
+ * like a New-menu entry, with the current folder; `variant:
882
+ * 'primary'` draws it as the primary button.
883
+ *
884
+ * @type {Array<{id: string, label: string, icon?: string, variant?: string, type?: string, target?: string, props?: object, handler?: string, args?: Array}>}
885
+ */
886
+ headerActions: {
887
+ type: Array,
888
+ default: () => [],
889
+ },
890
+
891
+ /**
892
+ * One visible button on every file row ("Beoordelen"), dispatched like a
893
+ * row action. `primaryWhen: { key, values }` draws it as the primary
894
+ * button while the row-data field `key` holds one of `values` (a file
895
+ * still to review), and secondary otherwise. `visibleIf` works as on a
896
+ * row action.
897
+ *
898
+ * @type {{id: string, label: string, type?: string, target?: string, props?: object, handler?: string, args?: Array, primaryWhen?: {key: string, values: Array<string>}, visibleIf?: object}|null}
899
+ */
900
+ rowButton: {
901
+ type: Object,
902
+ default: null,
903
+ },
904
+
905
+ /**
906
+ * A review summary above the list (DqZaakDocumenten "4 van 7
907
+ * beoordeeld"): `{ key, open, values: [{ value, label?, tone? }], title?,
908
+ * subtitle? }`. `key` is the row-data field with the review state, `open`
909
+ * the value (or values) of a file still to review; a file without a
910
+ * listed value counts as open. `tone` is `success`, `warning`, `error`,
911
+ * `info` or `default`. `title` and `subtitle` take `{done}`, `{total}` and
912
+ * `{open}`, through the host's translate; they default to the library's
913
+ * "{done} of {total} reviewed" and "{open} still to review".
914
+ *
915
+ * @type {{key: string, open?: string|Array<string>, values: Array<{value: string, label?: string, tone?: string}>, title?: string, subtitle?: string}|null}
916
+ */
917
+ reviewSummary: {
918
+ type: Object,
919
+ default: null,
920
+ },
921
+
814
922
  /**
815
923
  * Rows that are not nodes of this folder: files the host joined from
816
924
  * another object's folder. Rendered after the folder's own rows with
@@ -930,6 +1038,29 @@ export default {
930
1038
  default: false,
931
1039
  },
932
1040
 
1041
+ /**
1042
+ * A line of the host's own under the list, apart from the drop hint
1043
+ * ("Elk bestand wordt bij het uploaden op virussen gescand en krijgt een
1044
+ * controlegetal", DqDocumentenBeheer). Translated with the host's
1045
+ * catalogue.
1046
+ */
1047
+ footnote: {
1048
+ type: String,
1049
+ default: '',
1050
+ },
1051
+
1052
+ /**
1053
+ * A second line under each file's name (DqDocumentenBeheer "Inwoner ·
1054
+ * 4 okt"): a field template read from the file's row data, plus
1055
+ * `name`, `modified` (the change date) and `owner` (the owner's display
1056
+ * name), e.g. `"{uploadedBy} · {modified|date}"`. A line with an empty
1057
+ * value is left out. Translated with the host's catalogue first.
1058
+ */
1059
+ nameSecondary: {
1060
+ type: String,
1061
+ default: '',
1062
+ },
1063
+
933
1064
  /** The line `dropHint` shows. */
934
1065
  dropHintLabel: {
935
1066
  type: String,
@@ -1331,6 +1462,28 @@ export default {
1331
1462
  return this.sorted.filter((node) => !this.isFolder(node))
1332
1463
  },
1333
1464
 
1465
+ /**
1466
+ * The review counts over the folder's files, or null without a summary.
1467
+ *
1468
+ * @return {object|null}
1469
+ */
1470
+ review() {
1471
+ return reviewSummary(this.reviewSummary, this.fileNodes, this.resolvedRowData)
1472
+ },
1473
+
1474
+ /** @return {string} The summary's title, counts filled in. */
1475
+ reviewTitle() {
1476
+ return this.reviewText('title', t('nextcloud-vue', '{done} of {total} reviewed'))
1477
+ },
1478
+
1479
+ /** @return {string} The summary's second line, or '' when nothing is open. */
1480
+ reviewSubtitle() {
1481
+ if (!this.review || this.review.open === 0) {
1482
+ return ''
1483
+ }
1484
+ return this.reviewText('subtitle', t('nextcloud-vue', '{open} still to review'))
1485
+ },
1486
+
1334
1487
  /**
1335
1488
  * Whether any facet value is chosen.
1336
1489
  *
@@ -1793,6 +1946,73 @@ export default {
1793
1946
  return this.cnTranslate(String(action?.label ?? ''))
1794
1947
  },
1795
1948
 
1949
+ /**
1950
+ * The second line under a file's name, or '' without `nameSecondary`,
1951
+ * for a folder, or when a value in it is empty.
1952
+ *
1953
+ * @param {object} node The file.
1954
+ * @return {string}
1955
+ */
1956
+ nameSecondaryOf(node) {
1957
+ if (!this.nameSecondary || this.isFolder(node)) {
1958
+ return ''
1959
+ }
1960
+ const byId = this.resolvedRowData && typeof this.resolvedRowData === 'object' ? this.resolvedRowData : {}
1961
+ const entry = byId[node.fileid] ?? byId[String(node.fileid)] ?? {}
1962
+ const owner = node.attributes ? node.attributes['owner-display-name'] : undefined
1963
+ const object = { ...entry, name: node.basename, modified: node.mtime, ...(owner ? { owner } : {}) }
1964
+ return fillTemplate(this.cnTranslate(this.nameSecondary), object)
1965
+ },
1966
+
1967
+ /**
1968
+ * A review summary line: the host's template through its translate, else
1969
+ * the library's, with the counts filled in.
1970
+ *
1971
+ * @param {string} key `title` or `subtitle`.
1972
+ * @param {string} fallback The library's line, already translated.
1973
+ * @return {string}
1974
+ */
1975
+ reviewText(key, fallback) {
1976
+ const own = this.reviewSummary && typeof this.reviewSummary[key] === 'string' && this.reviewSummary[key] !== ''
1977
+ ? this.cnTranslate(this.reviewSummary[key])
1978
+ : fallback
1979
+ const counts = this.review || { done: 0, total: 0, open: 0 }
1980
+ return own.replace(/\{(done|total|open)\}/g, (match, name) => String(counts[name]))
1981
+ },
1982
+
1983
+ /**
1984
+ * Whether a review value is one of the summary's open values.
1985
+ *
1986
+ * @param {string} value The value.
1987
+ * @return {boolean}
1988
+ */
1989
+ isOpenValue(value) {
1990
+ const open = this.reviewSummary ? this.reviewSummary.open : undefined
1991
+ const list = (Array.isArray(open) ? open : [open]).filter((v) => v !== undefined && v !== null)
1992
+ return list.some((v) => String(v).toLowerCase() === String(value).toLowerCase())
1993
+ },
1994
+
1995
+ /**
1996
+ * Whether a file's row button is the primary one: the row-data field
1997
+ * `primaryWhen.key` holds one of `primaryWhen.values`.
1998
+ *
1999
+ * @param {object} node The file.
2000
+ * @return {boolean}
2001
+ */
2002
+ rowButtonPrimary(node) {
2003
+ const when = this.rowButton && this.rowButton.primaryWhen
2004
+ if (!when || typeof when.key !== 'string' || !Array.isArray(when.values)) {
2005
+ return false
2006
+ }
2007
+ const raw = fileColumnValue({ key: when.key, source: 'row' }, node, this.resolvedRowData)
2008
+ if (raw === null || raw === undefined) {
2009
+ return false
2010
+ }
2011
+ return when.values.some((v) => String(v).toLowerCase() === String(raw).toLowerCase())
2012
+ },
2013
+
2014
+ hostActionApplies,
2015
+
1796
2016
  /**
1797
2017
  * Re-read the folder once after a burst of change signals. One upload
1798
2018
  * of ten files answers ten `files:node:created`, and one PROPFIND
@@ -2938,6 +3158,89 @@ export default {
2938
3158
  margin-inline-end: 8px;
2939
3159
  }
2940
3160
 
3161
+ .cn-files-browser__review {
3162
+ display: flex;
3163
+ flex-direction: column;
3164
+ gap: 10px;
3165
+ margin-block-end: 12px;
3166
+ padding: 14px 16px;
3167
+ border-radius: 10px;
3168
+ background: var(--color-background-hover);
3169
+ }
3170
+
3171
+ .cn-files-browser__review-head {
3172
+ display: flex;
3173
+ flex-wrap: wrap;
3174
+ align-items: baseline;
3175
+ justify-content: space-between;
3176
+ gap: 8px;
3177
+ }
3178
+
3179
+ .cn-files-browser__review-title {
3180
+ font-size: 15px;
3181
+ }
3182
+
3183
+ .cn-files-browser__review-sub {
3184
+ color: var(--color-text-maxcontrast);
3185
+ font-size: 13px;
3186
+ }
3187
+
3188
+ .cn-files-browser__review-bar {
3189
+ display: flex;
3190
+ height: 8px;
3191
+ overflow: hidden;
3192
+ border-radius: 4px;
3193
+ background: var(--color-border);
3194
+ }
3195
+
3196
+ .cn-files-browser__review-legend {
3197
+ display: flex;
3198
+ flex-wrap: wrap;
3199
+ gap: 8px 18px;
3200
+ margin: 0;
3201
+ padding: 0;
3202
+ font-size: 14px;
3203
+ list-style: none;
3204
+ }
3205
+
3206
+ .cn-files-browser__review-legend li {
3207
+ display: inline-flex;
3208
+ align-items: center;
3209
+ gap: 8px;
3210
+ }
3211
+
3212
+ .cn-files-browser__review-dot {
3213
+ width: 10px;
3214
+ height: 10px;
3215
+ border-radius: 5px;
3216
+ }
3217
+
3218
+ .cn-files-browser__review-part--success {
3219
+ background: var(--color-success-text, var(--color-success));
3220
+ }
3221
+
3222
+ .cn-files-browser__review-part--warning {
3223
+ background: var(--color-warning);
3224
+ }
3225
+
3226
+ .cn-files-browser__review-part--error {
3227
+ background: var(--color-error-text, var(--color-error));
3228
+ }
3229
+
3230
+ .cn-files-browser__review-part--info {
3231
+ background: var(--color-primary-element);
3232
+ }
3233
+
3234
+ .cn-files-browser__review-part--default {
3235
+ background: var(--color-border-dark);
3236
+ }
3237
+
3238
+ .cn-files-browser__row-button {
3239
+ display: inline-flex;
3240
+ margin-inline-end: 8px;
3241
+ vertical-align: middle;
3242
+ }
3243
+
2941
3244
  .cn-files-browser__facets {
2942
3245
  display: flex;
2943
3246
  flex-wrap: wrap;
@@ -2988,6 +3291,11 @@ export default {
2988
3291
 
2989
3292
  .cn-files-browser__col-actions {
2990
3293
  width: 48px;
3294
+ white-space: nowrap;
3295
+ }
3296
+
3297
+ .cn-files-browser__col-actions:has(.cn-files-browser__row-button) {
3298
+ width: auto;
2991
3299
  }
2992
3300
 
2993
3301
  .cn-files-browser__col-name {
@@ -3001,6 +3309,22 @@ export default {
3001
3309
  white-space: nowrap;
3002
3310
  }
3003
3311
 
3312
+ .cn-files-browser__name-secondary {
3313
+ display: block;
3314
+ overflow: hidden;
3315
+ color: var(--color-text-maxcontrast);
3316
+ font-size: 13px;
3317
+ text-overflow: ellipsis;
3318
+ white-space: nowrap;
3319
+ }
3320
+
3321
+ .cn-files-browser__footnote {
3322
+ margin: 0;
3323
+ padding-top: 12px;
3324
+ color: var(--color-text-maxcontrast);
3325
+ font-size: 13px;
3326
+ }
3327
+
3004
3328
  .cn-files-browser__row--linked {
3005
3329
  color: var(--color-text-maxcontrast);
3006
3330
  }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * SPDX-FileCopyrightText: 2026 Conduction B.V.
3
+ * SPDX-License-Identifier: EUPL-1.2
4
+ */
5
+ import { fileColumnValue } from './filesBrowserColumns.js'
6
+
7
+ /** The tones a review segment may take; anything else draws as `default`. */
8
+ const TONES = ['success', 'warning', 'error', 'info', 'default']
9
+
10
+ /**
11
+ * The review summary of a file list (DqZaakDocumenten "4 van 7 beoordeeld"):
12
+ * how many files carry each review value, read from the host's row data.
13
+ *
14
+ * `spec` is `{ key, open, values: [{ value, label?, tone? }] }`: `key` is the
15
+ * row-data field that holds the review state, `open` the value (or values) of
16
+ * a file still to review. A file with no value, or one not listed, counts as
17
+ * open. Values compare without case.
18
+ *
19
+ * @param {object} spec The summary declaration.
20
+ * @param {Array<object>} nodes The file nodes (folders already left out).
21
+ * @param {object|null} rowData The host's row data, by file id.
22
+ * @return {{total: number, done: number, open: number, segments: Array<{value: string, label: string, tone: string, count: number, share: number}>}|null}
23
+ * The counts, or null when the declaration names no key or no values.
24
+ */
25
+ export function reviewSummary(spec, nodes, rowData) {
26
+ if (!spec || typeof spec !== 'object' || typeof spec.key !== 'string' || spec.key === '' || !Array.isArray(spec.values) || spec.values.length === 0) {
27
+ return null
28
+ }
29
+ const files = Array.isArray(nodes) ? nodes : []
30
+ const openValues = (Array.isArray(spec.open) ? spec.open : [spec.open]).filter((v) => v !== undefined && v !== null).map((v) => String(v).toLowerCase())
31
+ const segments = spec.values
32
+ .filter((entry) => entry && entry.value !== undefined && entry.value !== null)
33
+ .map((entry) => ({
34
+ value: String(entry.value),
35
+ label: typeof entry.label === 'string' && entry.label !== '' ? entry.label : String(entry.value),
36
+ tone: TONES.includes(entry.tone) ? entry.tone : 'default',
37
+ count: 0,
38
+ share: 0,
39
+ }))
40
+ let open = 0
41
+ for (const node of files) {
42
+ const raw = fileColumnValue({ key: spec.key, source: 'row' }, node, rowData)
43
+ const value = raw === null || raw === undefined ? '' : String(raw).toLowerCase()
44
+ const segment = segments.find((s) => s.value.toLowerCase() === value)
45
+ if (segment) {
46
+ segment.count++
47
+ }
48
+ if (!segment || openValues.includes(value)) {
49
+ open++
50
+ }
51
+ }
52
+ const total = files.length
53
+ for (const segment of segments) {
54
+ segment.share = total > 0 ? Math.round((segment.count / total) * 1000) / 10 : 0
55
+ }
56
+ return { total, done: total - open, open, segments }
57
+ }