@conduction/nextcloud-vue 2.80.0 → 2.81.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 (290) hide show
  1. package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js.map +1 -1
  2. package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js +3 -3
  3. package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js.map +1 -1
  4. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js +80 -4
  5. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
  6. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js +95 -91
  7. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js.map +1 -1
  8. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js +140 -10
  9. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js.map +1 -1
  10. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue3.js +52 -16
  11. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue3.js.map +1 -1
  12. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js +17 -3
  13. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js.map +1 -1
  14. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js +63 -52
  15. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js.map +1 -1
  16. package/dist/esm/components/CnChipsFieldWidget/CnChipsFieldWidget.vue.js +10 -0
  17. package/dist/esm/components/CnChipsFieldWidget/CnChipsFieldWidget.vue.js.map +1 -0
  18. package/dist/esm/components/CnChipsFieldWidget/CnChipsFieldWidget.vue2.js +252 -0
  19. package/dist/esm/components/CnChipsFieldWidget/CnChipsFieldWidget.vue2.js.map +1 -0
  20. package/dist/esm/components/CnChipsFieldWidget/CnChipsFieldWidget.vue3.js +140 -0
  21. package/dist/esm/components/CnChipsFieldWidget/CnChipsFieldWidget.vue3.js.map +1 -0
  22. package/dist/esm/components/CnChipsFieldWidget/index.js +26 -0
  23. package/dist/esm/components/CnChipsFieldWidget/index.js.map +1 -0
  24. package/dist/esm/components/CnChoicePickerWidget/CnChoicePickerWidget.vue.js +10 -0
  25. package/dist/esm/components/CnChoicePickerWidget/CnChoicePickerWidget.vue.js.map +1 -0
  26. package/dist/esm/components/CnChoicePickerWidget/CnChoicePickerWidget.vue2.js +354 -0
  27. package/dist/esm/components/CnChoicePickerWidget/CnChoicePickerWidget.vue2.js.map +1 -0
  28. package/dist/esm/components/CnChoicePickerWidget/CnChoicePickerWidget.vue3.js +172 -0
  29. package/dist/esm/components/CnChoicePickerWidget/CnChoicePickerWidget.vue3.js.map +1 -0
  30. package/dist/esm/components/CnChoicePickerWidget/index.js +27 -0
  31. package/dist/esm/components/CnChoicePickerWidget/index.js.map +1 -0
  32. package/dist/esm/components/CnConversationThread/CnConversationWidget.vue2.js +1 -1
  33. package/dist/esm/components/CnConversationThread/CnConversationWidget.vue2.js.map +1 -1
  34. package/dist/esm/components/CnConversationThread/CnConversationWidget.vue3.js.map +1 -1
  35. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue2.js +2 -2
  36. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue2.js.map +1 -1
  37. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue3.js.map +1 -1
  38. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +65 -5
  39. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  40. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +2 -1
  41. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  42. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +19 -11
  43. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
  44. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +3 -2
  45. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
  46. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +72 -0
  47. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  48. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +21 -4
  49. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  50. package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewWidget.vue2.js +1 -1
  51. package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewWidget.vue2.js.map +1 -1
  52. package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewWidget.vue3.js.map +1 -1
  53. package/dist/esm/components/CnFacetSidebar/CnFacetSidebar.vue2.js +7 -7
  54. package/dist/esm/components/CnFacetSidebar/CnFacetSidebar.vue2.js.map +1 -1
  55. package/dist/esm/components/CnFacetSidebar/CnFacetSidebar.vue3.js.map +1 -1
  56. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js +131 -0
  57. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js.map +1 -1
  58. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js.map +1 -1
  59. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js +80 -5
  60. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js.map +1 -1
  61. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js +282 -69
  62. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js.map +1 -1
  63. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js +302 -4
  64. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js.map +1 -1
  65. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js +417 -244
  66. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js.map +1 -1
  67. package/dist/esm/components/CnFilesBrowser/filesBrowser.js +101 -1
  68. package/dist/esm/components/CnFilesBrowser/filesBrowser.js.map +1 -1
  69. package/dist/esm/components/CnFilesBrowser/filesBrowserColumns.js +141 -1
  70. package/dist/esm/components/CnFilesBrowser/filesBrowserColumns.js.map +1 -1
  71. package/dist/esm/components/CnFormPage/CnFormPage.vue2.js +60 -0
  72. package/dist/esm/components/CnFormPage/CnFormPage.vue2.js.map +1 -1
  73. package/dist/esm/components/CnFormPage/CnFormPage.vue3.js +74 -49
  74. package/dist/esm/components/CnFormPage/CnFormPage.vue3.js.map +1 -1
  75. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +29 -2
  76. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  77. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +48 -4
  78. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  79. package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue2.js +7 -7
  80. package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue2.js.map +1 -1
  81. package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue3.js.map +1 -1
  82. package/dist/esm/components/CnNextStepCard/CnNextStepWidget.vue2.js +1 -1
  83. package/dist/esm/components/CnNextStepCard/CnNextStepWidget.vue2.js.map +1 -1
  84. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +65 -2
  85. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
  86. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js +6 -2
  87. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js.map +1 -1
  88. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js +63 -1
  89. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js.map +1 -1
  90. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js +8 -1
  91. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js.map +1 -1
  92. package/dist/esm/components/CnObjectSidebar/CnNotesTab.vue2.js +14 -0
  93. package/dist/esm/components/CnObjectSidebar/CnNotesTab.vue2.js.map +1 -1
  94. package/dist/esm/components/CnObjectSidebar/CnNotesTab.vue3.js +5 -1
  95. package/dist/esm/components/CnObjectSidebar/CnNotesTab.vue3.js.map +1 -1
  96. package/dist/esm/components/CnRadioCardsWidget/CnRadioCardsWidget.vue.js +10 -0
  97. package/dist/esm/components/CnRadioCardsWidget/CnRadioCardsWidget.vue.js.map +1 -0
  98. package/dist/esm/components/CnRadioCardsWidget/CnRadioCardsWidget.vue2.js +174 -0
  99. package/dist/esm/components/CnRadioCardsWidget/CnRadioCardsWidget.vue2.js.map +1 -0
  100. package/dist/esm/components/CnRadioCardsWidget/CnRadioCardsWidget.vue3.js +75 -0
  101. package/dist/esm/components/CnRadioCardsWidget/CnRadioCardsWidget.vue3.js.map +1 -0
  102. package/dist/esm/components/CnRadioCardsWidget/index.js +28 -0
  103. package/dist/esm/components/CnRadioCardsWidget/index.js.map +1 -0
  104. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +30 -6
  105. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
  106. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js +34 -25
  107. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
  108. package/dist/esm/components/CnSavedViewsWidget/CnSavedViewsWidget.vue.js +10 -0
  109. package/dist/esm/components/CnSavedViewsWidget/CnSavedViewsWidget.vue.js.map +1 -0
  110. package/dist/esm/components/CnSavedViewsWidget/CnSavedViewsWidget.vue2.js +222 -0
  111. package/dist/esm/components/CnSavedViewsWidget/CnSavedViewsWidget.vue2.js.map +1 -0
  112. package/dist/esm/components/CnSavedViewsWidget/CnSavedViewsWidget.vue3.js +64 -0
  113. package/dist/esm/components/CnSavedViewsWidget/CnSavedViewsWidget.vue3.js.map +1 -0
  114. package/dist/esm/components/CnSavedViewsWidget/index.js +30 -0
  115. package/dist/esm/components/CnSavedViewsWidget/index.js.map +1 -0
  116. package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue2.js.map +1 -1
  117. package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue3.js.map +1 -1
  118. package/dist/esm/components/CnSettingsFormWidget/CnSettingsFormWidget.vue.js +10 -0
  119. package/dist/esm/components/CnSettingsFormWidget/CnSettingsFormWidget.vue.js.map +1 -0
  120. package/dist/esm/components/CnSettingsFormWidget/CnSettingsFormWidget.vue2.js +347 -0
  121. package/dist/esm/components/CnSettingsFormWidget/CnSettingsFormWidget.vue2.js.map +1 -0
  122. package/dist/esm/components/CnSettingsFormWidget/CnSettingsFormWidget.vue3.js +184 -0
  123. package/dist/esm/components/CnSettingsFormWidget/CnSettingsFormWidget.vue3.js.map +1 -0
  124. package/dist/esm/components/CnSettingsFormWidget/index.js +27 -0
  125. package/dist/esm/components/CnSettingsFormWidget/index.js.map +1 -0
  126. package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js +87 -6
  127. package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js.map +1 -1
  128. package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js.map +1 -1
  129. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue2.js +84 -1
  130. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue2.js.map +1 -1
  131. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue3.js +85 -41
  132. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue3.js.map +1 -1
  133. package/dist/esm/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue.js +10 -0
  134. package/dist/esm/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue.js.map +1 -0
  135. package/dist/esm/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue2.js +159 -0
  136. package/dist/esm/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue2.js.map +1 -0
  137. package/dist/esm/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue3.js +60 -0
  138. package/dist/esm/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue3.js.map +1 -0
  139. package/dist/esm/components/CnSwitchRowsWidget/index.js +26 -0
  140. package/dist/esm/components/CnSwitchRowsWidget/index.js.map +1 -0
  141. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +47 -1
  142. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
  143. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js +16 -14
  144. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
  145. package/dist/esm/components/CnTasksWidget/CnTasksWidget.vue2.js +80 -1
  146. package/dist/esm/components/CnTasksWidget/CnTasksWidget.vue2.js.map +1 -1
  147. package/dist/esm/components/CnTasksWidget/CnTasksWidget.vue3.js +75 -32
  148. package/dist/esm/components/CnTasksWidget/CnTasksWidget.vue3.js.map +1 -1
  149. package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue.js +10 -0
  150. package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue.js.map +1 -0
  151. package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue2.js +269 -0
  152. package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue2.js.map +1 -0
  153. package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue3.js +106 -0
  154. package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue3.js.map +1 -0
  155. package/dist/esm/components/CnToggleTableWidget/index.js +27 -0
  156. package/dist/esm/components/CnToggleTableWidget/index.js.map +1 -0
  157. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js +7 -0
  158. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js.map +1 -1
  159. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js +7 -2
  160. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js.map +1 -1
  161. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue3.js.map +1 -1
  162. package/dist/esm/components/CnWidgetObjectTable/dashboardRegistration.js +5 -0
  163. package/dist/esm/components/CnWidgetObjectTable/dashboardRegistration.js.map +1 -1
  164. package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue2.js +197 -6
  165. package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue2.js.map +1 -1
  166. package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue3.js +96 -50
  167. package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue3.js.map +1 -1
  168. package/dist/esm/composables/useListView.js +4 -4
  169. package/dist/esm/composables/useListView.js.map +1 -1
  170. package/dist/esm/index.js +17 -1
  171. package/dist/esm/index.js.map +1 -1
  172. package/dist/esm/l10n/en.json.js +9 -1
  173. package/dist/esm/l10n/en.json.js.map +1 -1
  174. package/dist/esm/l10n/nl.json.js +55 -1
  175. package/dist/esm/l10n/nl.json.js.map +1 -1
  176. package/dist/esm/nextcloud-vue.css +1041 -12
  177. package/dist/esm/utils/contentHeaderLink.js +51 -0
  178. package/dist/esm/utils/contentHeaderLink.js.map +1 -0
  179. package/dist/esm/utils/facets.js +75 -3
  180. package/dist/esm/utils/facets.js.map +1 -1
  181. package/dist/esm/utils/fetchFilterCounts.js +40 -5
  182. package/dist/esm/utils/fetchFilterCounts.js.map +1 -1
  183. package/dist/esm/utils/fillTemplate.js +7 -1
  184. package/dist/esm/utils/fillTemplate.js.map +1 -1
  185. package/dist/esm/utils/initials.js +32 -0
  186. package/dist/esm/utils/initials.js.map +1 -0
  187. package/dist/esm/utils/libraryWidgetKeys.js +7 -0
  188. package/dist/esm/utils/libraryWidgetKeys.js.map +1 -1
  189. package/dist/esm/utils/objectWrite.js +59 -0
  190. package/dist/esm/utils/objectWrite.js.map +1 -0
  191. package/dist/esm/utils/pageControls.js +81 -0
  192. package/dist/esm/utils/pageControls.js.map +1 -0
  193. package/dist/esm/utils/personalLenses.js +40 -3
  194. package/dist/esm/utils/personalLenses.js.map +1 -1
  195. package/dist/esm/utils/resolveFilterTokens.js +1 -1
  196. package/dist/esm/utils/resolveFilterTokens.js.map +1 -1
  197. package/dist/esm/utils/resolveManifestTokens.js +2 -0
  198. package/dist/esm/utils/resolveManifestTokens.js.map +1 -1
  199. package/dist/esm/utils/sentinelTokens.js +19 -1
  200. package/dist/esm/utils/sentinelTokens.js.map +1 -1
  201. package/dist/esm/utils/userGroupsPeek.js +91 -0
  202. package/dist/esm/utils/userGroupsPeek.js.map +1 -0
  203. package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
  204. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  205. package/dist/esm/utils/visibleWhen.js +57 -5
  206. package/dist/esm/utils/visibleWhen.js.map +1 -1
  207. package/dist/esm/utils/widgetDispatch.js +1 -1
  208. package/dist/esm/utils/widgetDispatch.js.map +1 -1
  209. package/dist/esm/utils/widgetVisibility.js +4 -57
  210. package/dist/esm/utils/widgetVisibility.js.map +1 -1
  211. package/dist/nextcloud-vue.cjs.js +13589 -7833
  212. package/dist/nextcloud-vue.cjs.js.map +2 -2
  213. package/dist/nextcloud-vue.css +1041 -12
  214. package/l10n/en.json +9 -1
  215. package/l10n/nl.json +55 -1
  216. package/package.json +2 -1
  217. package/scripts/check-docs-mdx.mjs +108 -0
  218. package/src/components/CnActionsBar/CnActionsBar.vue +6 -1
  219. package/src/components/CnAppRoot/CnAppRoot.md +4 -2
  220. package/src/components/CnAppRoot/CnAppRoot.vue +151 -59
  221. package/src/components/CnBannerWidget/CnBannerWidget.vue +205 -20
  222. package/src/components/CnCellRenderer/CnCellRenderer.md +2 -0
  223. package/src/components/CnCellRenderer/CnCellRenderer.vue +47 -4
  224. package/src/components/CnChipsFieldWidget/CnChipsFieldWidget.vue +433 -0
  225. package/src/components/CnChipsFieldWidget/index.js +25 -0
  226. package/src/components/CnChoicePickerWidget/CnChoicePickerWidget.vue +556 -0
  227. package/src/components/CnChoicePickerWidget/index.js +26 -0
  228. package/src/components/CnConversationThread/CnConversationWidget.vue +1 -1
  229. package/src/components/CnDashboardGrid/CnDashboardGrid.vue +2 -2
  230. package/src/components/CnDashboardPage/CnDashboardPage.vue +78 -5
  231. package/src/components/CnDataTable/CnDataTable.vue +21 -12
  232. package/src/components/CnDetailPage/CnDetailPage.vue +89 -3
  233. package/src/components/CnDocumentReviewList/CnDocumentReviewWidget.vue +1 -1
  234. package/src/components/CnFacetSidebar/CnFacetSidebar.vue +7 -7
  235. package/src/components/CnFactsWidget/CnFactsWidget.vue +142 -2
  236. package/src/components/CnFieldListWidget/CnFieldListWidget.vue +268 -6
  237. package/src/components/CnFilesBrowser/CnFilesBrowser.vue +588 -117
  238. package/src/components/CnFilesBrowser/filesBrowser.js +100 -0
  239. package/src/components/CnFilesBrowser/filesBrowserColumns.js +140 -0
  240. package/src/components/CnFormPage/CnFormPage.vue +81 -0
  241. package/src/components/CnIndexPage/CnIndexPage.vue +56 -4
  242. package/src/components/CnIndexSidebar/CnIndexSidebar.vue +7 -7
  243. package/src/components/CnNextStepCard/CnNextStepWidget.vue +1 -1
  244. package/src/components/CnObjectListWidget/CnObjectListWidget.vue +69 -2
  245. package/src/components/CnObjectSidebar/CnFilesTab.vue +70 -1
  246. package/src/components/CnObjectSidebar/CnNotesTab.vue +16 -0
  247. package/src/components/CnRadioCardsWidget/CnRadioCardsWidget.vue +313 -0
  248. package/src/components/CnRadioCardsWidget/index.js +27 -0
  249. package/src/components/CnRecordListWidget/CnRecordListWidget.vue +55 -6
  250. package/src/components/CnSavedViewsWidget/CnSavedViewsWidget.vue +296 -0
  251. package/src/components/CnSavedViewsWidget/index.js +29 -0
  252. package/src/components/CnSegmentedControl/CnSegmentedControl.vue +13 -0
  253. package/src/components/CnSettingsFormWidget/CnSettingsFormWidget.vue +555 -0
  254. package/src/components/CnSettingsFormWidget/index.js +26 -0
  255. package/src/components/CnStatWidget/CnStatWidget.vue +87 -6
  256. package/src/components/CnStatsBlockWidget/CnStatsBlockWidget.vue +180 -3
  257. package/src/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue +226 -0
  258. package/src/components/CnSwitchRowsWidget/index.js +25 -0
  259. package/src/components/CnTabsWidget/CnTabsWidget.vue +82 -3
  260. package/src/components/CnTasksWidget/CnTasksWidget.vue +175 -15
  261. package/src/components/CnToggleTableWidget/CnToggleTableWidget.vue +386 -0
  262. package/src/components/CnToggleTableWidget/index.js +26 -0
  263. package/src/components/CnWidgetGrid/registerDashboardWidgets.js +7 -0
  264. package/src/components/CnWidgetObjectTable/CnWidgetObjectTable.vue +7 -2
  265. package/src/components/CnWidgetObjectTable/dashboardRegistration.js +5 -0
  266. package/src/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue +309 -11
  267. package/src/components/index.js +15 -0
  268. package/src/composables/useListView.js +4 -4
  269. package/src/css/look-board-detail.css +36 -2
  270. package/src/css/look-board-index.css +39 -8
  271. package/src/css/look-board.css +72 -0
  272. package/src/index.js +15 -0
  273. package/src/schemas/app-manifest-v2.schema.json +93 -9
  274. package/src/utils/contentHeaderLink.js +48 -0
  275. package/src/utils/facets.js +74 -2
  276. package/src/utils/fetchFilterCounts.js +39 -4
  277. package/src/utils/fillTemplate.js +7 -1
  278. package/src/utils/initials.js +29 -0
  279. package/src/utils/libraryWidgetKeys.js +7 -0
  280. package/src/utils/objectWrite.js +55 -0
  281. package/src/utils/pageControls.js +78 -0
  282. package/src/utils/personalLenses.js +39 -2
  283. package/src/utils/resolveFilterTokens.js +3 -1
  284. package/src/utils/resolveManifestTokens.js +2 -0
  285. package/src/utils/sentinelTokens.js +19 -1
  286. package/src/utils/userGroupsPeek.js +87 -0
  287. package/src/utils/validateManifestV2.compiled.js +3 -3
  288. package/src/utils/visibleWhen.js +56 -4
  289. package/src/utils/widgetDispatch.js +1 -1
  290. package/src/utils/widgetVisibility.js +7 -56
@@ -147,6 +147,61 @@
147
147
  </li>
148
148
  </ul>
149
149
 
150
+ <!-- The host's filter: per declared column, a chip per value in use
151
+ with how many files carry it. Chips narrow the list. -->
152
+ <div v-if="facetGroups.length > 0 && !loading && error === ''"
153
+ class="cn-files-browser__facets"
154
+ data-testid="cn-files-browser-facets">
155
+ <div v-for="facet in facetGroups"
156
+ :key="facet.key"
157
+ class="cn-files-browser__facet"
158
+ role="group"
159
+ :aria-label="t('nextcloud-vue', 'Filter by {column}', { column: facet.label })">
160
+ <button v-for="option in facet.values"
161
+ :key="option.value"
162
+ type="button"
163
+ class="cn-files-browser__facet-chip"
164
+ :class="{ 'cn-files-browser__facet-chip--active': option.active }"
165
+ :aria-pressed="option.active ? 'true' : 'false'"
166
+ data-testid="cn-files-browser-facet-chip"
167
+ @click="toggleFacet(facet.key, option.value)">
168
+ {{ option.value }}
169
+ <strong class="cn-files-browser__facet-count">{{ option.count }}</strong>
170
+ </button>
171
+ </div>
172
+ <NcButton v-if="facetActive"
173
+ variant="tertiary"
174
+ data-testid="cn-files-browser-facets-clear"
175
+ @click="clearFacets">
176
+ {{ t('nextcloud-vue', 'Clear filters') }}
177
+ </NcButton>
178
+ </div>
179
+
180
+ <!-- The host's bulk actions, once something is selected. -->
181
+ <div v-if="bulkEnabled && selectedIds.length > 0"
182
+ class="cn-files-browser__bulk-bar"
183
+ role="toolbar"
184
+ :aria-label="t('nextcloud-vue', 'Actions for the selected files')"
185
+ data-testid="cn-files-browser-bulk-bar">
186
+ <span class="cn-files-browser__bulk-count" aria-live="polite">
187
+ {{ t('nextcloud-vue', '{count} selected', { count: selectedIds.length }) }}
188
+ </span>
189
+ <NcButton v-for="action in bulkActions"
190
+ :key="action.id"
191
+ :data-testid="`cn-files-browser-bulk-action-${action.id}`"
192
+ @click="runBulkAction(action)">
193
+ <template #icon>
194
+ <CnIcon :name="action.icon || 'CheckboxMultipleMarkedOutline'" :size="20" />
195
+ </template>
196
+ {{ action.label }}
197
+ </NcButton>
198
+ <NcButton variant="tertiary"
199
+ data-testid="cn-files-browser-bulk-clear"
200
+ @click="clearSelection">
201
+ {{ t('nextcloud-vue', 'Clear selection') }}
202
+ </NcButton>
203
+ </div>
204
+
150
205
  <NcLoadingIcon v-if="loading" :size="32" class="cn-files-browser__loading" />
151
206
 
152
207
  <CnEmptyContent v-else-if="error !== ''"
@@ -181,6 +236,16 @@
181
236
  <table v-else class="cn-files-browser__table" data-testid="cn-files-browser-table">
182
237
  <thead>
183
238
  <tr>
239
+ <th v-if="bulkEnabled" class="cn-files-browser__col-select" scope="col">
240
+ <input type="checkbox"
241
+ class="cn-files-browser__select"
242
+ :checked="allSelected"
243
+ :indeterminate.prop="someSelected && !allSelected"
244
+ :disabled="selectableIds.length === 0"
245
+ :aria-label="t('nextcloud-vue', 'Select all files')"
246
+ data-testid="cn-files-browser-select-all"
247
+ @change="toggleAll($event.target.checked)">
248
+ </th>
184
249
  <th class="cn-files-browser__col-icon" scope="col">
185
250
  <span class="hidden-visually">{{ t('nextcloud-vue', 'Type') }}</span>
186
251
  </th>
@@ -207,126 +272,155 @@
207
272
  </tr>
208
273
  </thead>
209
274
  <tbody>
210
- <tr
211
- v-for="node in sorted"
212
- :key="node.source"
213
- class="cn-files-browser__row"
214
- :class="{ 'cn-files-browser__row--folder': isFolder(node) }"
215
- data-testid="cn-files-browser-row"
216
- :data-name="node.basename"
217
- @click="onRowClick(node, $event)"
218
- @mousedown="preventMiddleClickAutoscroll"
219
- @auxclick="onRowAuxClick(node, $event)">
220
- <td class="cn-files-browser__col-icon">
221
- <img
222
- v-if="previewUrlFor(node)"
223
- class="cn-files-browser__thumb"
224
- :src="previewUrlFor(node)"
225
- alt=""
226
- loading="lazy"
227
- @error="onPreviewError(node)">
228
- <img
229
- v-else-if="mimeIconFor(node)"
230
- class="cn-files-browser__mime"
231
- :src="mimeIconFor(node)"
232
- alt="">
233
- <FolderOutline v-else-if="isFolder(node)" :size="32" />
234
- <FileOutline v-else :size="32" />
235
- </td>
236
- <td v-for="column in renderedColumns"
237
- :key="column.key"
238
- :class="`cn-files-browser__col-${column.key}`"
239
- :data-testid="`cn-files-browser-cell-${column.key}`">
240
- <span v-if="column.key === 'name'" class="cn-files-browser__name">{{ node.basename }}</span>
241
- <template v-else-if="column.key === 'size'">
242
- {{ isFolder(node) ? '' : formatSize(node.size) }}
243
- </template>
244
- <NcDateTime v-else-if="column.key === 'modified'" :timestamp="node.mtime" :ignoreSeconds="true" />
245
- <!-- Everything the host declared renders through the same
246
- cell renderer the tables use, so a `formatter` means
247
- here what it means there. -->
248
- <CnCellRenderer v-else
249
- :value="cellValue(column, node)"
250
- :formatter="column.formatter || null"
251
- :formatterOptions="column.formatterOptions || null"
252
- :widget="column.widget || null"
253
- :row="node" />
254
- </td>
255
- <td class="cn-files-browser__col-actions" @click.stop @auxclick.stop>
256
- <NcActions :forceMenu="true" :ariaLabel="t('nextcloud-vue', 'Actions for {name}', { name: node.basename })">
257
- <NcActionButton
258
- v-for="action in actionsFor(node)"
259
- :key="action.id"
260
- :closeAfterClick="true"
261
- :data-testid="`cn-files-browser-action-${action.id}`"
262
- @click="run(action, node)">
263
- <template #icon>
264
- <NcIconSvgWrapper :svg="iconOf(action, node)" />
265
- </template>
266
- {{ labelOf(action, node) }}
267
- </NcActionButton>
268
- <!-- The host's own actions on a file (never on a folder): declared
269
- in its manifest, dispatched through the page's action runner
270
- with the node's file id, name and path merged in. -->
271
- <template v-if="!isFolder(node)">
275
+ <!-- A heading row per group when the host groups on a column; the
276
+ destructured entry is either a group or a node. -->
277
+ <template v-for="{ key, group, node } in displayRows" :key="key">
278
+ <tr v-if="group"
279
+ class="cn-files-browser__group"
280
+ data-testid="cn-files-browser-group">
281
+ <th :colspan="columnCount" scope="rowgroup" class="cn-files-browser__group-heading">
282
+ {{ group.label }}
283
+ <span class="cn-files-browser__group-count">{{ group.count }}</span>
284
+ </th>
285
+ </tr>
286
+ <tr
287
+ v-else
288
+ :key="key"
289
+ class="cn-files-browser__row"
290
+ :class="{ 'cn-files-browser__row--folder': isFolder(node) }"
291
+ data-testid="cn-files-browser-row"
292
+ :data-name="node.basename"
293
+ @click="onRowClick(node, $event)"
294
+ @mousedown="preventMiddleClickAutoscroll"
295
+ @auxclick="onRowAuxClick(node, $event)">
296
+ <td v-if="bulkEnabled"
297
+ class="cn-files-browser__col-select"
298
+ @click.stop
299
+ @auxclick.stop>
300
+ <input v-if="!isFolder(node)"
301
+ type="checkbox"
302
+ class="cn-files-browser__select"
303
+ :checked="isSelected(node)"
304
+ :aria-label="t('nextcloud-vue', 'Select {name}', { name: node.basename })"
305
+ data-testid="cn-files-browser-select"
306
+ @change="toggleSelected(node, $event.target.checked)">
307
+ </td>
308
+ <td class="cn-files-browser__col-icon">
309
+ <img
310
+ v-if="previewUrlFor(node)"
311
+ class="cn-files-browser__thumb"
312
+ :src="previewUrlFor(node)"
313
+ alt=""
314
+ loading="lazy"
315
+ @error="onPreviewError(node)">
316
+ <img
317
+ v-else-if="mimeIconFor(node)"
318
+ class="cn-files-browser__mime"
319
+ :src="mimeIconFor(node)"
320
+ alt="">
321
+ <FolderOutline v-else-if="isFolder(node)" :size="32" />
322
+ <FileOutline v-else :size="32" />
323
+ </td>
324
+ <td v-for="column in renderedColumns"
325
+ :key="column.key"
326
+ :class="`cn-files-browser__col-${column.key}`"
327
+ :data-testid="`cn-files-browser-cell-${column.key}`">
328
+ <span v-if="column.key === 'name'" class="cn-files-browser__name">{{ node.basename }}</span>
329
+ <template v-else-if="column.key === 'size'">
330
+ {{ isFolder(node) ? '' : formatSize(node.size) }}
331
+ </template>
332
+ <NcDateTime v-else-if="column.key === 'modified'" :timestamp="node.mtime" :ignoreSeconds="true" />
333
+ <!-- Everything the host declared renders through the same
334
+ cell renderer the tables use, so a `formatter` means
335
+ here what it means there. -->
336
+ <CnCellRenderer v-else
337
+ :value="cellValue(column, node)"
338
+ :formatter="column.formatter || null"
339
+ :formatterOptions="column.formatterOptions || null"
340
+ :widget="column.widget || null"
341
+ :row="node" />
342
+ </td>
343
+ <td class="cn-files-browser__col-actions" @click.stop @auxclick.stop>
344
+ <NcActions :forceMenu="true" :ariaLabel="t('nextcloud-vue', 'Actions for {name}', { name: node.basename })">
272
345
  <NcActionButton
273
- v-for="action in rowActions"
274
- :key="`host-${action.id}`"
346
+ v-for="action in actionsFor(node)"
347
+ :key="action.id"
275
348
  :closeAfterClick="true"
276
- :data-testid="`cn-files-browser-host-action-${action.id}`"
277
- @click="runHostAction(action, node)">
349
+ :data-testid="`cn-files-browser-action-${action.id}`"
350
+ @click="run(action, node)">
278
351
  <template #icon>
279
- <CnIcon :name="action.icon || 'FileDocumentEditOutline'" :size="20" />
352
+ <NcIconSvgWrapper :svg="iconOf(action, node)" />
280
353
  </template>
281
- {{ action.label }}
354
+ {{ labelOf(action, node) }}
282
355
  </NcActionButton>
283
- </template>
284
- <NcActionSeparator v-if="actionsFor(node).length > 0 || (!isFolder(node) && rowActions.length > 0)" />
285
- <!-- The Files app's own rename is its list's inline input,
286
- which is not here; this one is a dialog over a DAV move. -->
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)">
314
- <template #icon>
315
- <Pencil :size="20" />
316
- </template>
317
- {{ renameLabel }}
318
- </NcActionButton>
319
- <NcActionLink
320
- v-if="node.fileid && !source"
321
- :href="permalink(node)"
322
- target="_blank"
323
- :closeAfterClick="true">
324
- <template #icon>
325
- <OpenInNew :size="20" />
356
+ <!-- The host's own actions on a file (never on a folder): declared
357
+ in its manifest, dispatched through the page's action runner
358
+ with the node's file id, name and path merged in. -->
359
+ <template v-if="!isFolder(node)">
360
+ <NcActionButton
361
+ v-for="action in hostActionsFor(node)"
362
+ :key="`host-${action.id}`"
363
+ :closeAfterClick="true"
364
+ :data-testid="`cn-files-browser-host-action-${action.id}`"
365
+ @click="runHostAction(action, node)">
366
+ <template #icon>
367
+ <CnIcon :name="action.icon || 'FileDocumentEditOutline'" :size="20" />
368
+ </template>
369
+ {{ action.label }}
370
+ </NcActionButton>
326
371
  </template>
327
- {{ showInFilesLabel }}
328
- </NcActionLink>
329
- </NcActions>
372
+ <NcActionSeparator v-if="actionsFor(node).length > 0 || (!isFolder(node) && hostActionsFor(node).length > 0)" />
373
+ <!-- The Files app's own rename is its list's inline input,
374
+ which is not here; this one is a dialog over a DAV move. -->
375
+ <!-- With a `source`, the Files app's actions (which act over
376
+ WebDAV) are not offered; download and delete go through
377
+ the source instead. -->
378
+ <NcActionLink
379
+ v-if="source && sourceDownloadUrl(node)"
380
+ :href="sourceDownloadUrl(node)"
381
+ :download="node.basename"
382
+ :closeAfterClick="true"
383
+ data-testid="cn-files-browser-action-download">
384
+ <template #icon>
385
+ <Download :size="20" />
386
+ </template>
387
+ {{ downloadLabel }}
388
+ </NcActionLink>
389
+ <NcActionButton v-if="source && canChange"
390
+ :closeAfterClick="true"
391
+ data-testid="cn-files-browser-action-delete"
392
+ @click="askRemove(node)">
393
+ <template #icon>
394
+ <TrashCanOutline :size="20" />
395
+ </template>
396
+ {{ deleteLabel }}
397
+ </NcActionButton>
398
+ <NcActionButton v-if="canChange"
399
+ :closeAfterClick="true"
400
+ data-testid="cn-files-browser-action-rename"
401
+ @click="askRename(node)">
402
+ <template #icon>
403
+ <Pencil :size="20" />
404
+ </template>
405
+ {{ renameLabel }}
406
+ </NcActionButton>
407
+ <NcActionLink
408
+ v-if="node.fileid && !source"
409
+ :href="permalink(node)"
410
+ target="_blank"
411
+ :closeAfterClick="true">
412
+ <template #icon>
413
+ <OpenInNew :size="20" />
414
+ </template>
415
+ {{ showInFilesLabel }}
416
+ </NcActionLink>
417
+ </NcActions>
418
+ </td>
419
+ </tr>
420
+ </template>
421
+ <tr v-if="facetActive && filteredFiles.length === 0" class="cn-files-browser__no-match-row">
422
+ <td :colspan="columnCount" class="cn-files-browser__no-match" data-testid="cn-files-browser-no-match">
423
+ {{ t('nextcloud-vue', 'No files match the filter.') }}
330
424
  </td>
331
425
  </tr>
332
426
  <!-- Rows that are not nodes of this folder: documents the host
@@ -341,6 +435,7 @@
341
435
  @click="openLinked(item)"
342
436
  @mousedown="preventMiddleClickAutoscroll"
343
437
  @auxclick="onLinkedAuxClick(item, $event)">
438
+ <td v-if="bulkEnabled" class="cn-files-browser__col-select" />
344
439
  <td class="cn-files-browser__col-icon">
345
440
  <img
346
441
  v-if="mimeIconFor({ mime: item.mime })"
@@ -548,10 +643,13 @@ import CnIcon from '../CnIcon/CnIcon.vue'
548
643
  import { readUserPreference, writeUserPreference } from '../../composables/useUserPreferences.js'
549
644
  import { dispatchAction } from '../../utils/actionsDispatcher.js'
550
645
  import { isNewTabClick, isRowMiddleClick, preventMiddleClickAutoscroll } from '../../utils/rowAuxClick.js'
551
- import { ACTIONS_NEEDING_THE_FILES_PAGE, crumbsFor, joinPath } from './filesBrowser.js'
646
+ import { ACTIONS_NEEDING_THE_FILES_PAGE, crumbsFor, hostActionApplies, joinPath } from './filesBrowser.js'
552
647
  import {
553
648
  attributePropertiesFor,
649
+ facetCounts,
554
650
  fileColumnValue,
651
+ groupNodes,
652
+ nodeMatchesFacets,
555
653
  normaliseFileColumns,
556
654
  sortNodesByColumn,
557
655
  visibleFileColumns,
@@ -631,9 +729,11 @@ export default {
631
729
  * dispatched through the page's action runner with the node's
632
730
  * `fileId`, `fileName` and `path` merged into an `open-modal` action's
633
731
  * props (or appended as the node to a `handler` action's args). Folders
634
- * get none. `icon` is an MDI icon name.
732
+ * get none. `icon` is an MDI icon name. `visibleIf` offers the action
733
+ * only on some files: `extension` (a list, no case) and `mime` (a list;
734
+ * `image/` or `image/*` names the family), each that is named must hold.
635
735
  *
636
- * @type {Array<{id: string, label: string, icon?: string, type?: string, target?: string, props?: object, handler?: string, args?: Array}>}
736
+ * @type {Array<{id: string, label: string, icon?: string, type?: string, target?: string, props?: object, handler?: string, args?: Array, visibleIf?: {extension?: string[], mime?: string[]}}>}
637
737
  */
638
738
  rowActions: {
639
739
  type: Array,
@@ -853,6 +953,52 @@ export default {
853
953
  default: 'files-browser-columns',
854
954
  },
855
955
 
956
+ /**
957
+ * The host's actions on a selection of files, declared like
958
+ * `rowActions`. With at least one, every file row (never a folder or a
959
+ * linked row) gets a checkbox, the header a select-all, and a bar with
960
+ * these actions shows while files are selected. An `open-modal`
961
+ * action's props gain `files` (each `{ fileId, fileName, path }`) and
962
+ * `fileIds`; a `handler` action's args gain the selected nodes. The
963
+ * selection clears after an action and when another folder opens.
964
+ *
965
+ * @type {Array<{id: string, label: string, icon?: string, type?: string, target?: string, props?: object, handler?: string, args?: Array}>}
966
+ */
967
+ bulkActions: {
968
+ type: Array,
969
+ default: () => [],
970
+ },
971
+
972
+ /**
973
+ * The key of a declared column to group the files on: a heading row
974
+ * per value, in the order each is first met, with its count. Files
975
+ * with no value come last under `groupEmptyLabel`; folders stay above.
976
+ * Empty (the default) does not group.
977
+ */
978
+ groupBy: {
979
+ type: String,
980
+ default: '',
981
+ },
982
+
983
+ /** The heading of the group of files with no value in the `groupBy` column. */
984
+ groupEmptyLabel: {
985
+ type: String,
986
+ default: () => t('nextcloud-vue', 'Other'),
987
+ },
988
+
989
+ /**
990
+ * Keys of declared columns to filter on. Each gets a chip per value in
991
+ * use among the folder's files (a list counts per entry) with how many
992
+ * files carry it; chosen chips keep the files with any chosen value of
993
+ * that column, across every column with a choice.
994
+ *
995
+ * @type {string[]}
996
+ */
997
+ facets: {
998
+ type: Array,
999
+ default: () => [],
1000
+ },
1001
+
856
1002
  /** Label of the toolbar's Columns chooser. */
857
1003
  columnsLabel: {
858
1004
  type: String,
@@ -875,6 +1021,10 @@ export default {
875
1021
  sortAsc: true,
876
1022
  /** The host's per-file data for `row` columns, by file id. */
877
1023
  resolvedRowData: null,
1024
+ /** File ids (as strings) of the selected files. */
1025
+ selectedIds: [],
1026
+ /** Chosen facet values by column key. */
1027
+ facetSelections: {},
878
1028
  /**
879
1029
  * The column keys this user chose to see, or null while they have
880
1030
  * chosen nothing. Never a source of membership: it is intersected
@@ -1005,6 +1155,149 @@ export default {
1005
1155
  return visibleFileColumns(this.declaredColumns, this.visibleColumnKeys)
1006
1156
  },
1007
1157
 
1158
+ /**
1159
+ * The declared columns by key, for grouping and facets.
1160
+ *
1161
+ * @return {object} Resolved columns by key.
1162
+ */
1163
+ columnsByKey() {
1164
+ const byKey = {}
1165
+ this.declaredColumns.forEach((column) => {
1166
+ byKey[column.key] = column
1167
+ })
1168
+ return byKey
1169
+ },
1170
+
1171
+ /**
1172
+ * How many cells a row has, for a heading or message spanning the row.
1173
+ *
1174
+ * @return {number} The column count.
1175
+ */
1176
+ columnCount() {
1177
+ return this.renderedColumns.length + 2 + (this.bulkEnabled ? 1 : 0)
1178
+ },
1179
+
1180
+ /**
1181
+ * Whether the host gave bulk actions, which turns on the selection.
1182
+ *
1183
+ * @return {boolean} True with at least one bulk action.
1184
+ * @spec openspec/changes/files-browser-hosts-a-documents-list/specs/files-browser/spec.md
1185
+ */
1186
+ bulkEnabled() {
1187
+ return Array.isArray(this.bulkActions) && this.bulkActions.length > 0
1188
+ },
1189
+
1190
+ /**
1191
+ * The listed folders, in display order.
1192
+ *
1193
+ * @return {Array<object>} The folder nodes.
1194
+ */
1195
+ folderNodes() {
1196
+ return this.sorted.filter((node) => this.isFolder(node))
1197
+ },
1198
+
1199
+ /**
1200
+ * The listed files, in display order, before the filter.
1201
+ *
1202
+ * @return {Array<object>} The file nodes.
1203
+ */
1204
+ fileNodes() {
1205
+ return this.sorted.filter((node) => !this.isFolder(node))
1206
+ },
1207
+
1208
+ /**
1209
+ * Whether any facet value is chosen.
1210
+ *
1211
+ * @return {boolean} True when the filter narrows the list.
1212
+ */
1213
+ facetActive() {
1214
+ return Object.values(this.facetSelections).some((values) => Array.isArray(values) && values.length > 0)
1215
+ },
1216
+
1217
+ /**
1218
+ * The chips: per facet column, the values in use with their counts.
1219
+ *
1220
+ * @return {Array<{key: string, label: string, values: Array<{value: string, count: number, active: boolean}>}>} The facets.
1221
+ * @spec openspec/changes/files-browser-hosts-a-documents-list/specs/files-browser/spec.md
1222
+ */
1223
+ facetGroups() {
1224
+ const keys = Array.isArray(this.facets) ? this.facets : []
1225
+ return keys
1226
+ .map((key) => this.columnsByKey[key])
1227
+ .filter(Boolean)
1228
+ .map((column) => {
1229
+ const chosen = this.facetSelections[column.key] || []
1230
+ return {
1231
+ key: column.key,
1232
+ label: this.columnLabel(column),
1233
+ values: facetCounts(this.fileNodes, column, this.resolvedRowData)
1234
+ .map((entry) => ({ ...entry, active: chosen.includes(entry.value) })),
1235
+ }
1236
+ })
1237
+ .filter((facet) => facet.values.length > 0)
1238
+ },
1239
+
1240
+ /**
1241
+ * The files the filter keeps.
1242
+ *
1243
+ * @return {Array<object>} The file nodes.
1244
+ */
1245
+ filteredFiles() {
1246
+ if (!this.facetActive) {
1247
+ return this.fileNodes
1248
+ }
1249
+ return this.fileNodes.filter((node) => nodeMatchesFacets(node, this.facetSelections, this.columnsByKey, this.resolvedRowData))
1250
+ },
1251
+
1252
+ /**
1253
+ * What the table body renders, in order: folders, then the files,
1254
+ * under a heading per group when the host groups.
1255
+ *
1256
+ * @return {Array<{key: string, group?: object, node?: object}>} The rows.
1257
+ * @spec openspec/changes/files-browser-hosts-a-documents-list/specs/files-browser/spec.md
1258
+ */
1259
+ displayRows() {
1260
+ const rows = this.folderNodes.map((node) => ({ key: node.source, node }))
1261
+ const column = this.groupBy ? this.columnsByKey[this.groupBy] : null
1262
+ if (!column) {
1263
+ this.filteredFiles.forEach((node) => rows.push({ key: node.source, node }))
1264
+ return rows
1265
+ }
1266
+ groupNodes(this.filteredFiles, column, this.resolvedRowData).forEach((group) => {
1267
+ const label = group.value === null ? this.groupEmptyLabel : group.value
1268
+ rows.push({ key: `group-${group.value === null ? '' : group.value}`, group: { label, count: group.nodes.length } })
1269
+ group.nodes.forEach((node) => rows.push({ key: node.source, node }))
1270
+ })
1271
+ return rows
1272
+ },
1273
+
1274
+ /**
1275
+ * The ids of the files a select-all takes: the files the filter keeps.
1276
+ *
1277
+ * @return {string[]} File ids as strings.
1278
+ */
1279
+ selectableIds() {
1280
+ return this.filteredFiles.map((node) => String(node.fileid))
1281
+ },
1282
+
1283
+ /**
1284
+ * Whether every selectable file is selected.
1285
+ *
1286
+ * @return {boolean} True when all are.
1287
+ */
1288
+ allSelected() {
1289
+ return this.selectableIds.length > 0 && this.selectableIds.every((id) => this.selectedIds.includes(id))
1290
+ },
1291
+
1292
+ /**
1293
+ * Whether some file is selected.
1294
+ *
1295
+ * @return {boolean} True when one or more are.
1296
+ */
1297
+ someSelected() {
1298
+ return this.selectedIds.length > 0
1299
+ },
1300
+
1008
1301
  /**
1009
1302
  * The column the table is currently sorted on, or null when the sort
1010
1303
  * key belongs to no declared column.
@@ -1053,6 +1346,11 @@ export default {
1053
1346
  this.currentPath = next
1054
1347
  this.refresh()
1055
1348
  },
1349
+
1350
+ // A selection belongs to the folder it was made in.
1351
+ currentPath() {
1352
+ this.selectedIds = []
1353
+ },
1056
1354
  },
1057
1355
 
1058
1356
  mounted() {
@@ -1949,6 +2247,104 @@ export default {
1949
2247
  return generateUrl('/f/{fileid}', { fileid: node.fileid })
1950
2248
  },
1951
2249
 
2250
+ /**
2251
+ * Whether a file is selected.
2252
+ *
2253
+ * @param {object} node The file.
2254
+ * @return {boolean} True when selected.
2255
+ */
2256
+ isSelected(node) {
2257
+ return this.selectedIds.includes(String(node.fileid))
2258
+ },
2259
+
2260
+ /**
2261
+ * Select or deselect one file.
2262
+ *
2263
+ * @param {object} node The file.
2264
+ * @param {boolean} checked Whether it is now selected.
2265
+ * @return {void}
2266
+ */
2267
+ toggleSelected(node, checked) {
2268
+ const id = String(node.fileid)
2269
+ const rest = this.selectedIds.filter((entry) => entry !== id)
2270
+ this.selectedIds = checked ? [...rest, id] : rest
2271
+ },
2272
+
2273
+ /**
2274
+ * Select every file the filter keeps, or none.
2275
+ *
2276
+ * @param {boolean} checked Whether to select all.
2277
+ * @return {void}
2278
+ */
2279
+ toggleAll(checked) {
2280
+ this.selectedIds = checked ? [...this.selectableIds] : []
2281
+ },
2282
+
2283
+ /** Drop the selection. */
2284
+ clearSelection() {
2285
+ this.selectedIds = []
2286
+ },
2287
+
2288
+ /**
2289
+ * Run one of the host's bulk actions on the selected files, then drop
2290
+ * the selection. An `open-modal` action's props gain `files` and
2291
+ * `fileIds`; a `handler` action's args gain the selected nodes.
2292
+ *
2293
+ * @param {object} action The declared bulk action.
2294
+ * @return {void}
2295
+ * @spec openspec/changes/files-browser-hosts-a-documents-list/specs/files-browser/spec.md
2296
+ */
2297
+ runBulkAction(action) {
2298
+ if (!action || typeof action !== 'object') {
2299
+ return
2300
+ }
2301
+ const nodes = this.fileNodes.filter((node) => this.selectedIds.includes(String(node.fileid)))
2302
+ const type = action.type || 'handler'
2303
+ let wrapped = action
2304
+ if (type === 'open-modal') {
2305
+ const files = nodes.map((node) => ({ fileId: node.fileid, fileName: node.basename, path: node.path }))
2306
+ wrapped = { ...action, props: { ...(action.props || {}), files, fileIds: files.map((file) => file.fileId) } }
2307
+ } else if (type === 'handler') {
2308
+ wrapped = { ...action, args: [...(action.args || []), nodes] }
2309
+ }
2310
+ this.selectedIds = []
2311
+ if (typeof this.cnDispatchAction === 'function') {
2312
+ this.cnDispatchAction(wrapped)
2313
+ return
2314
+ }
2315
+ dispatchAction(wrapped, { router: this.$router || null })
2316
+ },
2317
+
2318
+ /**
2319
+ * Choose or drop one facet value.
2320
+ *
2321
+ * @param {string} key The column key.
2322
+ * @param {string} value The value.
2323
+ * @return {void}
2324
+ */
2325
+ toggleFacet(key, value) {
2326
+ const chosen = this.facetSelections[key] || []
2327
+ const next = chosen.includes(value) ? chosen.filter((entry) => entry !== value) : [...chosen, value]
2328
+ this.facetSelections = { ...this.facetSelections, [key]: next }
2329
+ },
2330
+
2331
+ /** Drop every facet choice. */
2332
+ clearFacets() {
2333
+ this.facetSelections = {}
2334
+ },
2335
+
2336
+ /**
2337
+ * The host's own row actions that belong on this file: those whose
2338
+ * `visibleIf` (extension, mime) holds for it, or that name none.
2339
+ *
2340
+ * @param {object} node The file.
2341
+ * @return {Array<object>} The actions to offer.
2342
+ * @spec openspec/changes/files-browser-hosts-a-documents-list/specs/files-browser/spec.md
2343
+ */
2344
+ hostActionsFor(node) {
2345
+ return this.rowActions.filter((action) => hostActionApplies(action, node))
2346
+ },
2347
+
1952
2348
  /**
1953
2349
  * Run one of the host's declared actions on a file.
1954
2350
  *
@@ -2195,6 +2591,81 @@ export default {
2195
2591
  color: var(--color-text-maxcontrast);
2196
2592
  }
2197
2593
 
2594
+ .cn-files-browser__col-select {
2595
+ width: 36px;
2596
+ text-align: center;
2597
+ }
2598
+
2599
+ .cn-files-browser__select {
2600
+ width: 18px;
2601
+ height: 18px;
2602
+ margin: 0;
2603
+ accent-color: var(--color-primary-element);
2604
+ }
2605
+
2606
+ .cn-files-browser__bulk-bar {
2607
+ display: flex;
2608
+ flex-wrap: wrap;
2609
+ align-items: center;
2610
+ gap: 8px;
2611
+ padding: 8px 12px;
2612
+ border-radius: var(--border-radius-large);
2613
+ background: var(--color-primary-element-light);
2614
+ }
2615
+
2616
+ .cn-files-browser__bulk-count {
2617
+ font-weight: 600;
2618
+ margin-inline-end: 8px;
2619
+ }
2620
+
2621
+ .cn-files-browser__facets {
2622
+ display: flex;
2623
+ flex-wrap: wrap;
2624
+ align-items: center;
2625
+ gap: 8px 16px;
2626
+ }
2627
+
2628
+ .cn-files-browser__facet {
2629
+ display: flex;
2630
+ flex-wrap: wrap;
2631
+ gap: 6px;
2632
+ }
2633
+
2634
+ .cn-files-browser__facet-chip {
2635
+ display: inline-flex;
2636
+ align-items: center;
2637
+ gap: 6px;
2638
+ min-height: 32px;
2639
+ padding: 0 12px;
2640
+ border: 1px solid var(--color-border-dark);
2641
+ border-radius: var(--border-radius-pill);
2642
+ background: var(--color-main-background);
2643
+ color: var(--color-main-text);
2644
+ cursor: pointer;
2645
+ }
2646
+
2647
+ .cn-files-browser__facet-chip--active {
2648
+ border-color: var(--color-primary-element);
2649
+ background: var(--color-primary-element-light);
2650
+ }
2651
+
2652
+ .cn-files-browser__group-heading {
2653
+ padding: 12px 8px 4px;
2654
+ text-align: start;
2655
+ font-weight: 700;
2656
+ }
2657
+
2658
+ .cn-files-browser__group-count {
2659
+ margin-inline-start: 6px;
2660
+ font-weight: 400;
2661
+ color: var(--color-text-maxcontrast);
2662
+ }
2663
+
2664
+ .cn-files-browser__no-match {
2665
+ padding: 16px 8px;
2666
+ color: var(--color-text-maxcontrast);
2667
+ }
2668
+
2198
2669
  .cn-files-browser__col-actions {
2199
2670
  width: 48px;
2200
2671
  }