@conduction/nextcloud-vue 2.83.0 → 2.85.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 (246) hide show
  1. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue.js +1 -1
  2. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue2.js +131 -3
  3. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue2.js.map +1 -1
  4. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue3.js +54 -18
  5. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue3.js.map +1 -1
  6. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue.js +1 -1
  7. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue2.js +1 -1
  8. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue2.js.map +1 -1
  9. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue3.js.map +1 -1
  10. package/dist/esm/components/CnBoardView/CnBoardView.vue.js +1 -1
  11. package/dist/esm/components/CnBoardView/CnBoardView.vue2.js +232 -4
  12. package/dist/esm/components/CnBoardView/CnBoardView.vue2.js.map +1 -1
  13. package/dist/esm/components/CnBoardView/CnBoardView.vue3.js +95 -51
  14. package/dist/esm/components/CnBoardView/CnBoardView.vue3.js.map +1 -1
  15. package/dist/esm/components/CnBodySections/CnBodySections.vue.js +1 -1
  16. package/dist/esm/components/CnBodySections/CnBodySections.vue2.js +22 -1
  17. package/dist/esm/components/CnBodySections/CnBodySections.vue2.js.map +1 -1
  18. package/dist/esm/components/CnBodySections/CnBodySections.vue3.js.map +1 -1
  19. package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue.js +1 -1
  20. package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue2.js +47 -4
  21. package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue2.js.map +1 -1
  22. package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue3.js.map +1 -1
  23. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue.js +1 -1
  24. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue2.js +239 -3
  25. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue2.js.map +1 -1
  26. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue3.js +5 -5
  27. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue3.js.map +1 -1
  28. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue.js +1 -1
  29. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +50 -0
  30. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  31. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +22 -12
  32. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  33. package/dist/esm/components/CnDeltaWidget/CnDeltaWidget.vue.js +1 -1
  34. package/dist/esm/components/CnDeltaWidget/CnDeltaWidget.vue2.js +1 -1
  35. package/dist/esm/components/CnDeltaWidget/CnDeltaWidget.vue2.js.map +1 -1
  36. package/dist/esm/components/CnDeltaWidget/CnDeltaWidget.vue3.js.map +1 -1
  37. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +226 -8
  38. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  39. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  40. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue.js +1 -1
  41. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js +3 -1
  42. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
  43. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
  44. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue.js +1 -1
  45. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js +18 -5
  46. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js.map +1 -1
  47. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js.map +1 -1
  48. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue.js +1 -1
  49. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js +38 -1
  50. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js.map +1 -1
  51. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js +45 -5
  52. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js.map +1 -1
  53. package/dist/esm/components/CnGaugeWidget/CnGaugeWidget.vue.js +1 -1
  54. package/dist/esm/components/CnGaugeWidget/CnGaugeWidget.vue2.js +1 -1
  55. package/dist/esm/components/CnGaugeWidget/CnGaugeWidget.vue2.js.map +1 -1
  56. package/dist/esm/components/CnGaugeWidget/CnGaugeWidget.vue3.js.map +1 -1
  57. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +548 -77
  58. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  59. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +191 -82
  60. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  61. package/dist/esm/components/CnIndexPage/useSelfFetchList.js +11 -2
  62. package/dist/esm/components/CnIndexPage/useSelfFetchList.js.map +1 -1
  63. package/dist/esm/components/CnInteractionFormWidget/CnInteractionFormWidget.vue2.js +29 -6
  64. package/dist/esm/components/CnInteractionFormWidget/CnInteractionFormWidget.vue2.js.map +1 -1
  65. package/dist/esm/components/CnInteractionFormWidget/CnInteractionFormWidget.vue3.js.map +1 -1
  66. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue.js +1 -1
  67. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js +126 -2
  68. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js.map +1 -1
  69. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue3.js +24 -2
  70. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue3.js.map +1 -1
  71. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
  72. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +106 -3
  73. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
  74. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
  75. package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue.js +1 -1
  76. package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue2.js +67 -5
  77. package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue2.js.map +1 -1
  78. package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue3.js +181 -124
  79. package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue3.js.map +1 -1
  80. package/dist/esm/components/CnReportsPage/CnReportsPage.vue.js +1 -1
  81. package/dist/esm/components/CnReportsPage/CnReportsPage.vue2.js +135 -2
  82. package/dist/esm/components/CnReportsPage/CnReportsPage.vue2.js.map +1 -1
  83. package/dist/esm/components/CnReportsPage/CnReportsPage.vue3.js +95 -43
  84. package/dist/esm/components/CnReportsPage/CnReportsPage.vue3.js.map +1 -1
  85. package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue.js +1 -1
  86. package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue2.js +5 -1
  87. package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue2.js.map +1 -1
  88. package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue3.js.map +1 -1
  89. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue.js +1 -1
  90. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue2.js +72 -5
  91. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue2.js.map +1 -1
  92. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue3.js +33 -8
  93. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue3.js.map +1 -1
  94. package/dist/esm/components/CnStatWidget/CnStatWidget.vue.js +1 -1
  95. package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js +135 -11
  96. package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js.map +1 -1
  97. package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js +6 -2
  98. package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js.map +1 -1
  99. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue.js +1 -1
  100. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue2.js +17 -2
  101. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue2.js.map +1 -1
  102. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue3.js +1 -1
  103. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue3.js.map +1 -1
  104. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue.js +1 -1
  105. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +39 -6
  106. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
  107. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
  108. package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue.js +1 -1
  109. package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue2.js +140 -7
  110. package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue2.js.map +1 -1
  111. package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue3.js.map +1 -1
  112. package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue.js +1 -1
  113. package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue2.js +37 -9
  114. package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue2.js.map +1 -1
  115. package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue3.js +6 -5
  116. package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue3.js.map +1 -1
  117. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue.js +1 -1
  118. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue2.js +16 -0
  119. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue2.js.map +1 -1
  120. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue3.js.map +1 -1
  121. package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue.js +1 -1
  122. package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue2.js +2 -1
  123. package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue2.js.map +1 -1
  124. package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue3.js.map +1 -1
  125. package/dist/esm/components/CnWidgetCardGrid/CnWidgetCardGrid.vue2.js +53 -1
  126. package/dist/esm/components/CnWidgetCardGrid/CnWidgetCardGrid.vue2.js.map +1 -1
  127. package/dist/esm/components/CnWidgetCardGrid/CnWidgetCardGrid.vue3.js +3 -2
  128. package/dist/esm/components/CnWidgetCardGrid/CnWidgetCardGrid.vue3.js.map +1 -1
  129. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue.js +1 -1
  130. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js +135 -0
  131. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js.map +1 -1
  132. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue3.js.map +1 -1
  133. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue.js +1 -1
  134. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue2.js +19 -0
  135. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue2.js.map +1 -1
  136. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue3.js +37 -19
  137. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue3.js.map +1 -1
  138. package/dist/esm/composables/useListView.js +21 -0
  139. package/dist/esm/composables/useListView.js.map +1 -1
  140. package/dist/esm/dialogs/CnTransitionInputDialog.vue.js +1 -1
  141. package/dist/esm/dialogs/CnTransitionInputDialog.vue2.js +68 -6
  142. package/dist/esm/dialogs/CnTransitionInputDialog.vue2.js.map +1 -1
  143. package/dist/esm/dialogs/CnTransitionInputDialog.vue3.js +133 -105
  144. package/dist/esm/dialogs/CnTransitionInputDialog.vue3.js.map +1 -1
  145. package/dist/esm/index.js +1 -0
  146. package/dist/esm/index.js.map +1 -1
  147. package/dist/esm/l10n/en.json.js +38 -1
  148. package/dist/esm/l10n/en.json.js.map +1 -1
  149. package/dist/esm/l10n/nl.json.js +78 -1
  150. package/dist/esm/l10n/nl.json.js.map +1 -1
  151. package/dist/esm/library-style.js +1 -1
  152. package/dist/esm/nextcloud-vue.css +1030 -533
  153. package/dist/esm/store/useObjectStore.js +16 -2
  154. package/dist/esm/store/useObjectStore.js.map +1 -1
  155. package/dist/esm/utils/boardColumns.js +8 -1
  156. package/dist/esm/utils/boardColumns.js.map +1 -1
  157. package/dist/esm/utils/boardReferenceColumns.js +278 -0
  158. package/dist/esm/utils/boardReferenceColumns.js.map +1 -0
  159. package/dist/esm/utils/buildManifest.js +41 -4
  160. package/dist/esm/utils/buildManifest.js.map +1 -1
  161. package/dist/esm/utils/detailWidgetVisibility.js +149 -0
  162. package/dist/esm/utils/detailWidgetVisibility.js.map +1 -0
  163. package/dist/esm/utils/formatMetric.js +39 -3
  164. package/dist/esm/utils/formatMetric.js.map +1 -1
  165. package/dist/esm/utils/gridStack.js +40 -1
  166. package/dist/esm/utils/gridStack.js.map +1 -1
  167. package/dist/esm/utils/humaniseKey.js +41 -0
  168. package/dist/esm/utils/humaniseKey.js.map +1 -0
  169. package/dist/esm/utils/notificationRuleLabel.js +6 -23
  170. package/dist/esm/utils/notificationRuleLabel.js.map +1 -1
  171. package/dist/esm/utils/resolveManifestTokens.js +1 -0
  172. package/dist/esm/utils/resolveManifestTokens.js.map +1 -1
  173. package/dist/esm/utils/sentinelTokens.js +9 -0
  174. package/dist/esm/utils/sentinelTokens.js.map +1 -1
  175. package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
  176. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  177. package/dist/esm/utils/widgetDispatch.js +8 -2
  178. package/dist/esm/utils/widgetDispatch.js.map +1 -1
  179. package/dist/nextcloud-vue.cjs.js +17159 -13422
  180. package/dist/nextcloud-vue.cjs.js.map +2 -2
  181. package/dist/nextcloud-vue.css +1030 -533
  182. package/l10n/en.json +38 -1
  183. package/l10n/nl.json +78 -1
  184. package/package.json +1 -1
  185. package/src/components/CnAdminSettingsShell/CnAdminSettingsShell.vue +160 -8
  186. package/src/components/CnAuditTrailCard/CnAuditTrailCard.vue +1 -1
  187. package/src/components/CnBoardView/CnBoardView.vue +262 -6
  188. package/src/components/CnBodySections/CnBodySections.vue +22 -1
  189. package/src/components/CnCountdownWidget/CnCountdownWidget.vue +47 -4
  190. package/src/components/CnDashboardGrid/CnDashboardGrid.vue +244 -8
  191. package/src/components/CnDashboardPage/CnDashboardPage.vue +62 -2
  192. package/src/components/CnDeltaWidget/CnDeltaWidget.vue +1 -1
  193. package/src/components/CnDetailPage/CnDetailPage.md +3 -1
  194. package/src/components/CnDetailPage/CnDetailPage.vue +226 -8
  195. package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +3 -1
  196. package/src/components/CnFactsWidget/CnFactsWidget.vue +18 -5
  197. package/src/components/CnFieldListWidget/CnFieldListWidget.vue +63 -1
  198. package/src/components/CnGaugeWidget/CnGaugeWidget.vue +1 -1
  199. package/src/components/CnIndexPage/CnIndexPage.vue +586 -25
  200. package/src/components/CnIndexPage/useSelfFetchList.js +10 -1
  201. package/src/components/CnInteractionFormWidget/CnInteractionFormWidget.vue +29 -6
  202. package/src/components/CnObjectDataWidget/CnObjectDataWidget.md +1 -1
  203. package/src/components/CnObjectDataWidget/CnObjectDataWidget.vue +154 -5
  204. package/src/components/CnRecordListWidget/CnRecordListWidget.vue +106 -3
  205. package/src/components/CnRegisterMapping/CnRegisterMapping.md +5 -0
  206. package/src/components/CnRegisterMapping/CnRegisterMapping.vue +231 -96
  207. package/src/components/CnReportsPage/CnReportsPage.vue +179 -1
  208. package/src/components/CnSettingsPage/CnSettingsPage.vue +5 -1
  209. package/src/components/CnStagesWidget/CnStagesWidget.vue +103 -6
  210. package/src/components/CnStatWidget/CnStatWidget.vue +137 -12
  211. package/src/components/CnStatsBlockWidget/CnStatsBlockWidget.vue +34 -3
  212. package/src/components/CnTabsWidget/CnTabsWidget.vue +39 -6
  213. package/src/components/CnToggleTableWidget/CnToggleTableWidget.vue +140 -7
  214. package/src/components/CnVersionInfoCard/CnVersionInfoCard.md +2 -1
  215. package/src/components/CnVersionInfoCard/CnVersionInfoCard.vue +42 -13
  216. package/src/components/CnWalkthrough/CnWalkthrough.vue +16 -0
  217. package/src/components/CnWeekStripWidget/CnWeekStripWidget.vue +2 -1
  218. package/src/components/CnWidgetCardGrid/CnWidgetCardGrid.vue +55 -2
  219. package/src/components/CnWidgetObjectTable/CnWidgetObjectTable.vue +135 -0
  220. package/src/components/CnWidgetWrapper/CnWidgetWrapper.md +10 -0
  221. package/src/components/CnWidgetWrapper/CnWidgetWrapper.vue +44 -1
  222. package/src/composables/useListView.js +21 -0
  223. package/src/css/board.css +65 -4
  224. package/src/css/dialog.css +4 -2
  225. package/src/css/form-field.css +14 -0
  226. package/src/css/form-widget.css +11 -0
  227. package/src/css/index-page.css +49 -0
  228. package/src/css/kpi-card.css +13 -0
  229. package/src/css/look-board-reports.css +58 -0
  230. package/src/css/look-board.css +108 -7
  231. package/src/dialogs/CnTransitionInputDialog.vue +87 -7
  232. package/src/index.js +2 -0
  233. package/src/schemas/app-manifest-v2.schema.json +137 -6
  234. package/src/store/useObjectStore.js +16 -2
  235. package/src/utils/boardColumns.js +8 -1
  236. package/src/utils/boardReferenceColumns.js +274 -0
  237. package/src/utils/buildManifest.js +40 -3
  238. package/src/utils/detailWidgetVisibility.js +145 -0
  239. package/src/utils/formatMetric.js +38 -2
  240. package/src/utils/gridStack.js +39 -0
  241. package/src/utils/humaniseKey.js +38 -0
  242. package/src/utils/notificationRuleLabel.js +5 -23
  243. package/src/utils/resolveManifestTokens.js +1 -0
  244. package/src/utils/sentinelTokens.js +9 -0
  245. package/src/utils/validateManifestV2.compiled.js +3 -3
  246. package/src/utils/widgetDispatch.js +8 -2
@@ -207,7 +207,53 @@
207
207
  <template v-if="$slots['action-items']" #action-items>
208
208
  <slot name="action-items" />
209
209
  </template>
210
- <template v-if="$slots['after-search'] || searchInFiles" #after-search>
210
+ <template v-if="$slots['after-search'] || searchInFiles || boardControlsShown" #after-search>
211
+ <!-- Board view controls, after the search (screens-board-reference-columns):
212
+ the narrowing select ("Case type:"), the column switch ("Columns:
213
+ status | type | assignee") and the board's hint sentence. -->
214
+ <template v-if="boardControlsShown">
215
+ <label
216
+ v-if="boardNarrow"
217
+ class="cn-index-page__board-control"
218
+ data-testid="cn-board-narrow">
219
+ <span class="cn-index-page__board-control-label">{{ boardNarrowLabel }}</span>
220
+ <select
221
+ class="cn-index-page__board-select"
222
+ data-testid="cn-board-narrow-select"
223
+ :value="boardNarrowValue"
224
+ @change="onBoardNarrowChange($event.target.value)">
225
+ <option value="">{{ boardNarrowAllLabel }}</option>
226
+ <option
227
+ v-for="option in boardNarrowOptions"
228
+ :key="`narrow-${option.value}`"
229
+ :value="option.value">
230
+ {{ option.label }}
231
+ </option>
232
+ </select>
233
+ </label>
234
+ <label
235
+ v-if="boardColumnFields.length > 1"
236
+ class="cn-index-page__board-control"
237
+ data-testid="cn-board-column-switch">
238
+ <span class="cn-index-page__board-control-label">{{ boardColumnSwitchLabel }}</span>
239
+ <select
240
+ class="cn-index-page__board-select cn-index-page__board-select--strong"
241
+ data-testid="cn-board-column-switch-select"
242
+ :value="boardColumnField"
243
+ @change="onBoardColumnFieldChange($event.target.value)">
244
+ <option
245
+ v-for="entry in boardColumnFields"
246
+ :key="`colfield-${entry.field}`"
247
+ :value="entry.field">
248
+ {{ cnTranslate(entry.label) }}
249
+ </option>
250
+ </select>
251
+ </label>
252
+ <span
253
+ v-if="board.hint"
254
+ class="cn-index-page__board-hint"
255
+ data-testid="cn-board-hint">{{ cnTranslate(board.hint) }}</span>
256
+ </template>
211
257
  <NcCheckboxRadioSwitch
212
258
  v-if="searchInFiles"
213
259
  :modelValue="contentSearch"
@@ -361,6 +407,17 @@
361
407
  </button>
362
408
  </div>
363
409
 
410
+ <!-- Header actions of `type: "open-form"` or `"open-modal"`: this
411
+ instance draws nothing and owns the create dialog, as on a detail
412
+ page, so a header button or menu entry naming one opens it. -->
413
+ <CnActionButtons
414
+ v-if="dialogHeaderActions.length > 0"
415
+ :actions="dialogHeaderActions"
416
+ display="menu"
417
+ data-testid="cn-index-header-dialog-actions"
418
+ @entries="dialogHeaderRuns = $event"
419
+ @created="onHeaderActionCreated" />
420
+
364
421
  <!-- Quick edit: a few fields of one row, over the list, with the list
365
422
  keeping its place. -->
366
423
  <CnQuickEditDialog
@@ -786,8 +843,13 @@
786
843
  <CnBoardView
787
844
  v-else-if="currentViewMode === 'board'"
788
845
  :rows="displayObjects"
789
- :statusFieldSchema="boardStatusFieldSchema"
790
- :statusField="board.statusField || 'status'"
846
+ :statusFieldSchema="boardFieldSchema"
847
+ :statusField="boardColumnField"
848
+ :columnKeyOf="boardColumnKeyOf"
849
+ :wrapColumns="board.wrap === true"
850
+ :selectable="selectable"
851
+ :selectedIds="internalSelectedIds"
852
+ :allowedMoves="boardAllowedMoves"
791
853
  :cardFields="board.cardFields || []"
792
854
  :swimlaneField="board.swimlaneField || ''"
793
855
  :dueRule="board.dueRule || null"
@@ -797,8 +859,9 @@
797
859
  :sumFormat="boardSumFormat"
798
860
  :columnLimit="board.columnLimit || 0"
799
861
  :rowKey="rowKey"
800
- :runTransition="runTransition"
862
+ :runTransition="boardRunTransition"
801
863
  :paged="isPaged"
864
+ @select="onSelect"
802
865
  @loadMore="onBoardLoadMore"
803
866
  @cardClick="onRowClick"
804
867
  @cardAuxClick="onRowAuxClick"
@@ -959,9 +1022,27 @@
959
1022
  {{ t('nextcloud-vue', 'File matches are limited to the best 50.') }}
960
1023
  </p>
961
1024
 
1025
+ <!-- The board view's footer: it holds every row it loaded, so it
1026
+ counts them and offers the next batch instead of pages. -->
1027
+ <div
1028
+ v-if="boardLoadsAll && boardLoadCounts.total > 0"
1029
+ class="cn-index-page__board-load"
1030
+ data-testid="cn-index-board-load">
1031
+ <span class="cn-index-page__board-load-count" data-testid="cn-index-board-load-count">
1032
+ {{ t('nextcloud-vue', 'Showing {count} of {total}', { count: boardLoadCounts.shown, total: boardLoadCounts.total }) }}
1033
+ </span>
1034
+ <NcButton
1035
+ v-if="isPaged"
1036
+ :disabled="effectiveLoading"
1037
+ data-testid="cn-index-board-load-more"
1038
+ @click="onBoardLoadMore">
1039
+ {{ t('nextcloud-vue', 'Load more') }}
1040
+ </NcButton>
1041
+ </div>
1042
+
962
1043
  <!-- Pagination -->
963
1044
  <CnPagination
964
- v-if="effectivePagination && (effectivePagination.pages > 1 || (isBoardLook && effectivePagination.total > 0))"
1045
+ v-else-if="effectivePagination && (effectivePagination.pages > 1 || (isBoardLook && effectivePagination.total > 0))"
965
1046
  :variant="isBoardLook ? 'board' : ''"
966
1047
  :footerNote="footerNote ? cnTranslate(footerNote) : footerNote"
967
1048
  :currentPage="effectivePagination.page || 1"
@@ -1053,7 +1134,7 @@
1053
1134
 
1054
1135
  <script>
1055
1136
  import { getCurrentUser } from '@nextcloud/auth'
1056
- import { translate as t } from '@nextcloud/l10n'
1137
+ import { translatePlural as n, translate as t } from '@nextcloud/l10n'
1057
1138
  import { NcActionButton, NcActionCaption, NcActionCheckbox, NcActionLink, NcActions, NcActionSeparator, NcButton, NcCheckboxRadioSwitch, NcLoadingIcon } from '@nextcloud/vue'
1058
1139
  import { getCurrentInstance, inject, markRaw, ref } from 'vue'
1059
1140
  import AlertCircleOutline from 'vue-material-design-icons/AlertCircleOutline.vue'
@@ -1083,6 +1164,16 @@ import { METADATA_COLUMNS } from '../../constants/metadata.js'
1083
1164
  import { useObjectStore } from '../../store/useObjectStore.js'
1084
1165
  import { followItemActionLink, resolveItemActionLink, routeHref } from '../../utils/actionLink.js'
1085
1166
  import { buildOnSuccessRoute, resolveRegisteredHandler } from '../../utils/actionsDispatcher.js'
1167
+ import {
1168
+ buildReferenceStages,
1169
+ buildValueStages,
1170
+ columnFieldsOf,
1171
+ columnKeyOfRow,
1172
+ fetchReferenceObjects,
1173
+ referenceTargetOf,
1174
+ refIdOf,
1175
+ resolveDropValue,
1176
+ } from '../../utils/boardReferenceColumns.js'
1086
1177
  import { reportBindingProblems } from '../../utils/diagnostics.js'
1087
1178
  import { fetchFilterCounts } from '../../utils/fetchFilterCounts.js'
1088
1179
  import { buildExportUrl } from '../../utils/indexExportHelpers.js'
@@ -1092,6 +1183,7 @@ import { resolveClaimedTeams, resolveClaimTokens, splitViewsIntoTabs, viewAsTab,
1092
1183
  import { LIST_SHORTCUTS, listPaletteCommands, shortcutFor } from '../../utils/listShortcuts.js'
1093
1184
  import { multiKeySort } from '../../utils/multiKeySort.js'
1094
1185
  import { OBJECTS_CHANGED_EVENT } from '../../utils/objectSignals.js'
1186
+ import { objectUrlOf, patchObject } from '../../utils/objectWrite.js'
1095
1187
  import { withPersonalLenses } from '../../utils/personalLenses.js'
1096
1188
  import { resolveDeepTokens, resolveFilterValue } from '../../utils/resolveFilterTokens.js'
1097
1189
  import { resolveRowActions } from '../../utils/resolveRowActions.js'
@@ -1104,6 +1196,7 @@ import { buildRouteQueryFromViewState, buildViewCreatePayload, extractViewState,
1104
1196
  import { columnsFromSchema, fieldsFromSchema } from '../../utils/schema.js'
1105
1197
  import { resolveScopeLayout } from '../../utils/scopeListLayout.js'
1106
1198
  import { dispatchObjectCreated } from '../../utils/walkthroughSignals.js'
1199
+ import { CnActionButtons } from '../CnActionButtons/index.js'
1107
1200
  import { CnActionsBar } from '../CnActionsBar/index.js'
1108
1201
  import { CnAdvancedFormDialog } from '../CnAdvancedFormDialog/index.js'
1109
1202
  import { CnBoardView } from '../CnBoardView/index.js'
@@ -1140,7 +1233,7 @@ import { orderColumns, personalColumnsKey, reconcilePersonalColumns } from './pe
1140
1233
  import { createSelfModeActions, resolveRowTarget } from './selfModeActions.js'
1141
1234
  import { applyRowPatches, normalisePaneWidth, rowIdOf, splitLayoutFor } from './splitView.js'
1142
1235
  import { useNamedSource } from './useNamedSource.js'
1143
- import { useSelfFetchList } from './useSelfFetchList.js'
1236
+ import { BOARD_PAGE_SIZE, useSelfFetchList } from './useSelfFetchList.js'
1144
1237
 
1145
1238
  /** Rows fetched for one month in calendar mode. */
1146
1239
  const CALENDAR_PAGE_SIZE = 200
@@ -1394,6 +1487,7 @@ export default {
1394
1487
  NcCheckboxRadioSwitch,
1395
1488
  CnPageHeader,
1396
1489
  CnQuickFilterBar,
1490
+ CnActionButtons,
1397
1491
  CnActionsBar,
1398
1492
  DotsHorizontal,
1399
1493
  CnIcon,
@@ -1440,6 +1534,8 @@ export default {
1440
1534
  */
1441
1535
  inject: {
1442
1536
  cnCustomComponents: { default: () => ({}) },
1537
+ /** The app id CnAppRoot provides, for the plural form of `countSubtitle`. */
1538
+ cnAppId: { default: '' },
1443
1539
  /**
1444
1540
  * The app manifest, provided by CnAppRoot. A column with `labelField`
1445
1541
  * and `link: true` finds the referenced schema's detail page here.
@@ -1526,10 +1622,17 @@ export default {
1526
1622
  * shows. Manifest key `config.countSubtitle`. Goes through the host
1527
1623
  * translate function.
1528
1624
  *
1625
+ * `{ singular, plural }` picks the form by the total ("1 invoice",
1626
+ * "12 invoices") through the app's plural translation
1627
+ * (`translatePlural` with the app id CnAppRoot provides), so a
1628
+ * language with other plural rules gets its own forms.
1629
+ *
1529
1630
  * @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-an-index-page-title-is-a-manifest-key
1631
+ * @spec openspec/changes/index-count-plural/specs/index-header/spec.md#requirement-the-count-line-takes-a-singular-and-a-plural-form
1632
+ * @type {string|{singular: string, plural: string}}
1530
1633
  */
1531
1634
  countSubtitle: {
1532
- type: String,
1635
+ type: [String, Object],
1533
1636
  default: '',
1534
1637
  },
1535
1638
 
@@ -2310,10 +2413,14 @@ export default {
2310
2413
  default: () => [],
2311
2414
  },
2312
2415
 
2313
- /** Text shown when no items found */
2416
+ /**
2417
+ * Text shown when no items found. The default is the library's own
2418
+ * string, so it reads in the user's language; a page's own text goes
2419
+ * through the app's translate function.
2420
+ */
2314
2421
  emptyText: {
2315
2422
  type: String,
2316
- default: 'No items found',
2423
+ default: () => t('nextcloud-vue', 'No items found'),
2317
2424
  },
2318
2425
 
2319
2426
  /**
@@ -3335,6 +3442,7 @@ export default {
3335
3442
  'add',
3336
3443
  'apply-view',
3337
3444
  'board-move',
3445
+ 'board-column-field-change',
3338
3446
  'bulk-action',
3339
3447
  'clear-filters',
3340
3448
  'columns-change',
@@ -3446,10 +3554,21 @@ export default {
3446
3554
  data() {
3447
3555
  return {
3448
3556
  currentViewMode: this.viewMode,
3557
+ /** The column field the reader picked in the board's column switch; '' = the default. */
3558
+ boardColumnFieldChoice: '',
3559
+ /** The rows a reference column field is built from, by field. */
3560
+ boardRefs: {},
3561
+ /** The options of the board's narrowing select (`board.narrow`). */
3562
+ boardNarrowOptions: [],
3563
+ /** The menu entries CnActionButtons reports for the dialog header actions, each with its `run()`. */
3564
+ dialogHeaderRuns: [],
3449
3565
  /** The visible calendar window `{ rangeStart, rangeEnd }` (ISO dates), or null outside calendar mode. */
3450
3566
  calendarRange: null,
3451
3567
  /** The page size to restore when leaving calendar mode. */
3452
3568
  calendarPrevPageSize: null,
3569
+ // The table's page size while the board view holds the list at
3570
+ // BOARD_PAGE_SIZE; restored on leaving the board.
3571
+ boardPrevPageSize: this.viewMode === 'board' ? ((this.pagination && this.pagination.limit) || 20) : null,
3453
3572
  /** Resolved labels of reference columns: `{ [columnKey]: { [id]: label|null } }`. */
3454
3573
  refLabels: {},
3455
3574
  /** Count per tab-strip index, for the entries that asked for one; null = none. */
@@ -3670,6 +3789,24 @@ export default {
3670
3789
  *
3671
3790
  * @return {Array<object>}
3672
3791
  */
3792
+ /**
3793
+ * The header actions that open a dialog: `type: "open-form"` (the
3794
+ * schema-driven create form, as the navigation's primary action opens
3795
+ * it) or `type: "open-modal"`, without a `handler` of their own.
3796
+ * Reserved ids are left out, as in `mergedHeaderActions`.
3797
+ *
3798
+ * @spec openspec/changes/index-header-open-form/specs/index-header/spec.md#requirement-an-index-header-action-can-open-the-create-form
3799
+ * @return {Array<object>}
3800
+ */
3801
+ dialogHeaderActions() {
3802
+ const reserved = new Set(['refresh', 'import', 'export', 'copy', 'delete'])
3803
+ return (this.headerActions || []).filter((entry) => entry
3804
+ && typeof entry.id === 'string'
3805
+ && !reserved.has(entry.id)
3806
+ && (entry.handler === undefined || entry.handler === null || entry.handler === '')
3807
+ && (entry.type === 'open-form' || entry.type === 'open-modal'))
3808
+ },
3809
+
3673
3810
  mergedHeaderActions() {
3674
3811
  const reserved = new Set(['refresh', 'import', 'export', 'copy', 'delete'])
3675
3812
  const merged = []
@@ -4022,6 +4159,241 @@ export default {
4022
4159
  })
4023
4160
  },
4024
4161
 
4162
+ /**
4163
+ * The column fields the board offers (`board.columnFields`), for the
4164
+ * column switch (screens-board-reference-columns).
4165
+ *
4166
+ * @return {Array<object>} The normalised fields; empty for a board on `statusField` only.
4167
+ * @spec openspec/changes/screens-board-reference-columns/specs/board-view/spec.md#requirement-a-board-can-switch-the-field-its-columns-come-from
4168
+ */
4169
+ boardColumnFields() {
4170
+ return columnFieldsOf(this.board)
4171
+ },
4172
+
4173
+ /**
4174
+ * The column field in use: the reader's pick, else the one on
4175
+ * `statusField`, else the first. Null on a board without `columnFields`.
4176
+ *
4177
+ * @return {object|null} The normalised column field.
4178
+ */
4179
+ boardActiveColumnField() {
4180
+ const fields = this.boardColumnFields
4181
+ if (fields.length === 0) {
4182
+ return null
4183
+ }
4184
+ return fields.find((entry) => entry.field === this.boardColumnFieldChoice)
4185
+ || fields.find((entry) => entry.field === this.board?.statusField)
4186
+ || fields[0]
4187
+ },
4188
+
4189
+ /** @return {string} The row field the columns come from. */
4190
+ boardColumnField() {
4191
+ return this.boardActiveColumnField?.field || this.board?.statusField || 'status'
4192
+ },
4193
+
4194
+ /**
4195
+ * The parent the board is narrowed to: the single value of the page
4196
+ * filter on the reference's `narrowBy.rowField`, '' when none or several.
4197
+ *
4198
+ * @return {string} The parent id.
4199
+ */
4200
+ boardNarrowValue() {
4201
+ const field = this.board?.narrow?.field || this.boardActiveColumnField?.source?.narrowBy?.rowField
4202
+ if (!field) {
4203
+ return ''
4204
+ }
4205
+ const current = this.effectiveActiveFilters?.[field]
4206
+ const values = Array.isArray(current) ? current : (current === undefined || current === null || current === '' ? [] : [current])
4207
+ return values.length === 1 ? refIdOf(values[0]) : ''
4208
+ },
4209
+
4210
+ /**
4211
+ * The stages of a reference or values column field, or null for a
4212
+ * field the schema's enum or lifecycle states back.
4213
+ *
4214
+ * @return {object|null} `{ states, keyByValue, refsByKey }`.
4215
+ * @spec openspec/changes/screens-board-reference-columns/specs/board-view/spec.md#requirement-columns-can-be-the-rows-a-field-references
4216
+ */
4217
+ boardStages() {
4218
+ const active = this.boardActiveColumnField
4219
+ if (!active) {
4220
+ return null
4221
+ }
4222
+ if (active.kind === 'values') {
4223
+ return buildValueStages({
4224
+ rows: this.displayObjects,
4225
+ field: active.field,
4226
+ valueLabels: active.valueLabels,
4227
+ emptyLabel: active.emptyLabel ? this.cnTranslate(active.emptyLabel) : t('nextcloud-vue', 'Nobody'),
4228
+ })
4229
+ }
4230
+ if (active.kind !== 'reference') {
4231
+ return null
4232
+ }
4233
+ const source = active.source || {}
4234
+ const narrowBy = source.narrowBy && typeof source.narrowBy === 'object' ? source.narrowBy : null
4235
+ return buildReferenceStages({
4236
+ refs: this.boardRefs[active.field] || [],
4237
+ labelField: source.labelField || '',
4238
+ orderField: source.orderField || '',
4239
+ colorField: source.colorField || '',
4240
+ mergeBy: source.mergeBy || '',
4241
+ narrowBy,
4242
+ narrowValue: narrowBy ? this.boardNarrowValue : '',
4243
+ })
4244
+ },
4245
+
4246
+ /**
4247
+ * The field schema the board reads its columns from: the stages of a
4248
+ * reference or values field as lifecycle `states`, else the schema's
4249
+ * own property.
4250
+ *
4251
+ * @return {?object} The field schema, or null.
4252
+ */
4253
+ boardFieldSchema() {
4254
+ if (this.boardStages) {
4255
+ return { states: this.boardStages.states }
4256
+ }
4257
+ const field = this.boardColumnField
4258
+ return this.effectiveSchema?.properties?.[field] || null
4259
+ },
4260
+
4261
+ /**
4262
+ * The column a card sits in, for a reference or values field.
4263
+ *
4264
+ * @return {((row: object) => string)|null} The mapper, or null.
4265
+ */
4266
+ boardColumnKeyOf() {
4267
+ const stages = this.boardStages
4268
+ if (!stages) {
4269
+ return null
4270
+ }
4271
+ const field = this.boardColumnField
4272
+ return (row) => columnKeyOfRow(row, field, stages.keyByValue)
4273
+ },
4274
+
4275
+ /**
4276
+ * The move the board runs on a drop or a menu pick.
4277
+ *
4278
+ * A named handler (`moveHandler`, from the app's registry) gets
4279
+ * `{ card, toKey, field, value, target }` and owns the write; the
4280
+ * status of a case goes through the app's transition this way. A
4281
+ * reference or values field without one has its value written on the
4282
+ * row (PATCH). `readOnly` gives no move. A board on `statusField` with
4283
+ * neither keeps the `runTransition` prop.
4284
+ *
4285
+ * @return {((move: {card: object, toKey: string}) => Promise<object>)|null} The move, or null for a read-only board.
4286
+ * @spec openspec/changes/screens-board-reference-columns/specs/board-view/spec.md#requirement-a-drop-writes-the-reference
4287
+ */
4288
+ boardRunTransition() {
4289
+ const active = this.boardActiveColumnField
4290
+ if (active?.readOnly) {
4291
+ return null
4292
+ }
4293
+ const onStatusField = !active || active.field === this.board?.statusField
4294
+ const name = active?.moveHandler || (onStatusField ? (this.board?.moveHandler || '') : '')
4295
+ const handler = name ? resolveRegisteredHandler(name, this.effectiveRegistry, this.resolvedCustomComponents) : null
4296
+ if (name && typeof handler !== 'function') {
4297
+ return null
4298
+ }
4299
+ const stages = this.boardStages
4300
+ if (!handler && !stages) {
4301
+ return this.runTransition
4302
+ }
4303
+ const field = this.boardColumnField
4304
+ const kind = active?.kind || 'schema'
4305
+ const narrowBy = active?.source?.narrowBy || null
4306
+ return async ({ card, toKey }) => {
4307
+ const drop = stages
4308
+ ? resolveDropValue({ card, toKey, kind, refsByKey: stages.refsByKey, narrowBy })
4309
+ : { value: toKey, target: null }
4310
+ if (drop === null) {
4311
+ throw new Error(t('nextcloud-vue', 'This card cannot move to that column.'))
4312
+ }
4313
+ if (handler) {
4314
+ return handler({ card, toKey, field, value: drop.value, target: drop.target })
4315
+ }
4316
+ const id = card?.id ?? card?.['@self']?.id ?? card?.[this.rowKey]
4317
+ const url = objectUrlOf(this.register, this.exportSchemaSlug, id)
4318
+ if (url === '') {
4319
+ throw new Error(t('nextcloud-vue', 'This card cannot move to that column.'))
4320
+ }
4321
+ return patchObject(url, { [field]: drop.value })
4322
+ }
4323
+ },
4324
+
4325
+ /**
4326
+ * Where a card may go, from the app's `moveTargetsHandler`: it gets
4327
+ * `{ card, field }` and answers the values (referenced ids) the card
4328
+ * can reach; the board maps them to its columns.
4329
+ *
4330
+ * @return {((card: object) => Promise<Array<string>|null>)|null} The question, or null.
4331
+ * @spec openspec/changes/screens-board-reference-columns/specs/board-view/spec.md#requirement-the-board-shows-where-a-card-can-go
4332
+ */
4333
+ boardAllowedMoves() {
4334
+ const active = this.boardActiveColumnField
4335
+ const onStatusField = !active || active.field === this.board?.statusField
4336
+ const name = active?.moveTargetsHandler || (onStatusField ? (this.board?.moveTargetsHandler || '') : '')
4337
+ const handler = name ? resolveRegisteredHandler(name, this.effectiveRegistry, this.resolvedCustomComponents) : null
4338
+ if (typeof handler !== 'function' || this.boardRunTransition === null) {
4339
+ return null
4340
+ }
4341
+ const field = this.boardColumnField
4342
+ const stages = this.boardStages
4343
+ return async (card) => {
4344
+ const values = await handler({ card, field })
4345
+ if (!Array.isArray(values)) {
4346
+ return null
4347
+ }
4348
+ return values.map((value) => {
4349
+ const id = refIdOf(value)
4350
+ return stages && stages.keyByValue.has(id) ? stages.keyByValue.get(id) : id
4351
+ })
4352
+ }
4353
+ },
4354
+
4355
+ /** @return {object|null} The board's narrowing select (`board.narrow`). */
4356
+ boardNarrow() {
4357
+ const narrow = this.board?.narrow
4358
+ return narrow && typeof narrow === 'object' && typeof narrow.field === 'string' && narrow.field !== '' ? narrow : null
4359
+ },
4360
+
4361
+ /** @return {string} The narrowing select's label, with its colon. */
4362
+ boardNarrowLabel() {
4363
+ const narrow = this.boardNarrow || {}
4364
+ const prop = (this.effectiveSchema?.properties || {})[narrow.field] || {}
4365
+ return t('nextcloud-vue', '{label}:', { label: this.cnTranslate(narrow.label || prop.title || narrow.field || '') })
4366
+ },
4367
+
4368
+ /** @return {string} The narrowing select's "all" option. */
4369
+ boardNarrowAllLabel() {
4370
+ return this.boardNarrow?.allLabel ? this.cnTranslate(this.boardNarrow.allLabel) : t('nextcloud-vue', 'All')
4371
+ },
4372
+
4373
+ /** @return {string} The column switch's label, with its colon. */
4374
+ boardColumnSwitchLabel() {
4375
+ const label = this.board?.columnSwitchLabel ? this.cnTranslate(this.board.columnSwitchLabel) : t('nextcloud-vue', 'Columns')
4376
+ return t('nextcloud-vue', '{label}:', { label })
4377
+ },
4378
+
4379
+ /** @return {boolean} Whether the board view draws its controls row. */
4380
+ boardControlsShown() {
4381
+ return this.currentViewMode === 'board'
4382
+ && (this.boardColumnFields.length > 1 || this.boardNarrow !== null || Boolean(this.board?.hint))
4383
+ },
4384
+
4385
+ /**
4386
+ * What the board needs fetched, as one key the watcher reacts to.
4387
+ *
4388
+ * @return {string} The key, '' outside the board view.
4389
+ */
4390
+ boardFetchKey() {
4391
+ if (this.currentViewMode !== 'board') {
4392
+ return ''
4393
+ }
4394
+ return [this.boardColumnField, this.boardActiveColumnField?.kind || '', this.effectiveSchema ? 1 : 0, this.boardNarrow ? this.boardNarrow.field : ''].join('|')
4395
+ },
4396
+
4025
4397
  /**
4026
4398
  * The status field's schema, for the board's columns.
4027
4399
  *
@@ -4068,9 +4440,36 @@ export default {
4068
4440
  * @return {boolean} True when there is more than this page.
4069
4441
  */
4070
4442
  isPaged() {
4443
+ if (this.boardLoadsAll) {
4444
+ const loaded = this.effectivePagination || {}
4445
+ return Number(loaded.page || 1) < Number(loaded.pages || 1)
4446
+ }
4071
4447
  return Number(this.pagination?.pages || 1) > 1
4072
4448
  },
4073
4449
 
4450
+ /**
4451
+ * Whether the board view gathers every row itself: in self-fetch mode
4452
+ * it adds page after page (`list.loadMore`) instead of paging, and
4453
+ * draws its own footer in place of the pagination.
4454
+ *
4455
+ * @return {boolean} True for a self-fetching page in board view.
4456
+ * @spec openspec/changes/board-loads-every-row/specs/board-view/spec.md#requirement-the-index-board-shows-every-matching-row
4457
+ */
4458
+ boardLoadsAll() {
4459
+ return this.currentViewMode === 'board' && this.isSelfFetchMode && !this.isNamedSource && typeof this.list.loadMore === 'function'
4460
+ },
4461
+
4462
+ /**
4463
+ * The rows the board holds against the rows that match, for the
4464
+ * board's footer.
4465
+ *
4466
+ * @return {{shown: number, total: number}} The counts.
4467
+ */
4468
+ boardLoadCounts() {
4469
+ const shown = this.effectiveObjects.length
4470
+ return { shown, total: Math.max(shown, Number(this.effectivePagination?.total || 0)) }
4471
+ },
4472
+
4074
4473
  /**
4075
4474
  * The CnFolderSidebar source. A `register` source is fetched by this
4076
4475
  * component and handed to CnFolderSidebar as a `custom` folder list.
@@ -5873,13 +6272,15 @@ export default {
5873
6272
  if (!this.countText && !this.countSubtitle) {
5874
6273
  return t('nextcloud-vue', '{shown} of {total}', { shown, total })
5875
6274
  }
5876
- const template = this.countText || this.countSubtitle
5877
- return this.cnTranslate(template)
5878
- .replace('{shown}', String(shown))
5879
- .replace('{total}', String(total))
6275
+ if (this.countText) {
6276
+ return this.cnTranslate(this.countText)
6277
+ .replace('{shown}', String(shown))
6278
+ .replace('{total}', String(total))
6279
+ }
6280
+ return this.countLine(this.countSubtitle, total, shown)
5880
6281
  }
5881
6282
  if (this.countSubtitle && typeof total === 'number' && total >= 0) {
5882
- return this.cnTranslate(this.countSubtitle).replace('{total}', String(total))
6283
+ return this.countLine(this.countSubtitle, total, null)
5883
6284
  }
5884
6285
  return this.description
5885
6286
  },
@@ -6045,6 +6446,19 @@ export default {
6045
6446
  },
6046
6447
 
6047
6448
  watch: {
6449
+ '$route.query._view': function() {
6450
+ this.applyViewModeFromRoute()
6451
+ },
6452
+
6453
+ boardFetchKey: {
6454
+ immediate: true,
6455
+ handler(key) {
6456
+ if (key !== '') {
6457
+ this.loadBoardSources()
6458
+ }
6459
+ },
6460
+ },
6461
+
6048
6462
  effectiveObjects: {
6049
6463
  immediate: true,
6050
6464
  handler(rows) {
@@ -6118,22 +6532,33 @@ export default {
6118
6532
  // Entering calendar mode fetches one page sized to the month; leaving
6119
6533
  // removes the month range and restores the page size, so the table is
6120
6534
  // never left narrowed.
6535
+ // The board does the same at BOARD_PAGE_SIZE, so it shows every row
6536
+ // rather than the table's first page.
6121
6537
  currentViewMode(mode, previous) {
6122
6538
  if (!this.isSelfFetchMode) {
6123
6539
  return
6124
6540
  }
6125
- if (mode === 'calendar') {
6126
- this.calendarPrevPageSize = this.list.pageSize.value
6127
- if (typeof this.list.onPageSizeChange === 'function') {
6128
- this.list.onPageSizeChange(CALENDAR_PAGE_SIZE)
6129
- }
6130
- } else if (previous === 'calendar') {
6541
+ let listSize = this.list.pageSize.value
6542
+ if (previous === 'calendar') {
6131
6543
  this.calendarRange = null
6132
- const restore = this.calendarPrevPageSize || 20
6544
+ listSize = this.calendarPrevPageSize || 20
6133
6545
  this.calendarPrevPageSize = null
6134
- if (typeof this.list.onPageSizeChange === 'function') {
6135
- this.list.onPageSizeChange(restore)
6136
- }
6546
+ } else if (previous === 'board') {
6547
+ listSize = this.boardPrevPageSize || 20
6548
+ this.boardPrevPageSize = null
6549
+ }
6550
+ let size = null
6551
+ if (mode === 'calendar') {
6552
+ this.calendarPrevPageSize = listSize
6553
+ size = CALENDAR_PAGE_SIZE
6554
+ } else if (mode === 'board') {
6555
+ this.boardPrevPageSize = listSize
6556
+ size = BOARD_PAGE_SIZE
6557
+ } else if (previous === 'calendar' || previous === 'board') {
6558
+ size = listSize
6559
+ }
6560
+ if (size !== null && typeof this.list.onPageSizeChange === 'function') {
6561
+ this.list.onPageSizeChange(size)
6137
6562
  }
6138
6563
  },
6139
6564
 
@@ -6288,6 +6713,7 @@ export default {
6288
6713
  },
6289
6714
 
6290
6715
  created() {
6716
+ this.applyViewModeFromRoute()
6291
6717
  this.pushAiContext()
6292
6718
  if (this.allowSavedViews) {
6293
6719
  this.fetchSavedViews()
@@ -7070,6 +7496,11 @@ export default {
7070
7496
  const entry = this.mergedHeaderActions.find((e) => e.id === id)
7071
7497
  if (entry && typeof entry.handler === 'function') {
7072
7498
  entry.handler()
7499
+ } else {
7500
+ const dialogRun = this.dialogHeaderRuns.find((e) => e.id === id)
7501
+ if (dialogRun && typeof dialogRun.run === 'function') {
7502
+ dialogRun.run()
7503
+ }
7073
7504
  }
7074
7505
  if (entry && entry._dispatchSuppress) {
7075
7506
  return
@@ -7077,6 +7508,49 @@ export default {
7077
7508
  this.$emit('header-action', { action: id, id })
7078
7509
  },
7079
7510
 
7511
+ /**
7512
+ * The `countSubtitle` line for a total. A string goes through the host
7513
+ * translate; `{ singular, plural }` takes the form for the total
7514
+ * through the app's plural translation, or by `total === 1` when no app
7515
+ * id is provided. `{total}` (and `{shown}` when given) are filled in.
7516
+ *
7517
+ * @spec openspec/changes/index-count-plural/specs/index-header/spec.md#requirement-the-count-line-takes-a-singular-and-a-plural-form
7518
+ * @param {string|{singular: string, plural: string}} subtitle The declared line.
7519
+ * @param {number} total The collection's total.
7520
+ * @param {number|null} shown The rows on this page, or null to leave `{shown}` alone.
7521
+ * @return {string} The line, '' for a declaration without both forms.
7522
+ */
7523
+ countLine(subtitle, total, shown) {
7524
+ const fill = (text) => {
7525
+ const out = String(text).replace('{total}', String(total))
7526
+ return shown === null ? out : out.replace('{shown}', String(shown))
7527
+ }
7528
+ if (typeof subtitle === 'string') {
7529
+ return fill(this.cnTranslate(subtitle))
7530
+ }
7531
+ const singular = subtitle && typeof subtitle.singular === 'string' ? subtitle.singular : ''
7532
+ const plural = subtitle && typeof subtitle.plural === 'string' ? subtitle.plural : ''
7533
+ if (singular === '' || plural === '') {
7534
+ return ''
7535
+ }
7536
+ if (typeof this.cnAppId === 'string' && this.cnAppId !== '') {
7537
+ const vars = shown === null ? { total } : { total, shown }
7538
+ return n(this.cnAppId, singular, plural, total, vars)
7539
+ }
7540
+ return fill(this.cnTranslate(total === 1 ? singular : plural))
7541
+ },
7542
+
7543
+ /**
7544
+ * An `open-form` header action saved a record: the list reloads so the
7545
+ * new row shows.
7546
+ *
7547
+ * @spec openspec/changes/index-header-open-form/specs/index-header/spec.md#requirement-an-index-header-action-can-open-the-create-form
7548
+ * @return {void}
7549
+ */
7550
+ onHeaderActionCreated() {
7551
+ this.onRefreshEvent()
7552
+ },
7553
+
7080
7554
  pushAiContext() {
7081
7555
  applyAiContext(this.cnAiContext, 'index', {
7082
7556
  register: this.register,
@@ -7292,6 +7766,72 @@ export default {
7292
7766
  this.$emit('clear-filters')
7293
7767
  },
7294
7768
 
7769
+ /**
7770
+ * Fetch what the board view needs: the rows of a reference column
7771
+ * field (once per field) and the narrowing select's options.
7772
+ *
7773
+ * @return {Promise<void>}
7774
+ * @spec openspec/changes/screens-board-reference-columns/specs/board-view/spec.md#requirement-columns-can-be-the-rows-a-field-references
7775
+ */
7776
+ async loadBoardSources() {
7777
+ const active = this.boardActiveColumnField
7778
+ if (active && active.kind === 'reference' && !Object.hasOwn(this.boardRefs, active.field)) {
7779
+ const property = this.effectiveSchema?.properties?.[active.field] || null
7780
+ const target = referenceTargetOf(active.source, { register: this.register, property })
7781
+ if (target.register && target.schema) {
7782
+ let rows
7783
+ try {
7784
+ rows = await fetchReferenceObjects({ ...target, filter: active.source?.filter || {} })
7785
+ } catch {
7786
+ rows = []
7787
+ }
7788
+ this.boardRefs = { ...this.boardRefs, [active.field]: rows }
7789
+ }
7790
+ }
7791
+ const narrow = this.boardNarrow
7792
+ if (narrow && this.boardNarrowOptions.length === 0) {
7793
+ const property = this.effectiveSchema?.properties?.[narrow.field] || null
7794
+ const target = referenceTargetOf(narrow.source || {}, { register: this.register, property })
7795
+ let rows
7796
+ try {
7797
+ rows = await fetchReferenceObjects({ ...target, filter: narrow.source?.filter || {} })
7798
+ } catch {
7799
+ rows = []
7800
+ }
7801
+ const labelField = narrow.source?.labelField || 'title'
7802
+ this.boardNarrowOptions = rows
7803
+ .map((row) => ({ value: refIdOf(row), label: String(row?.[labelField] ?? row?.name ?? row?.title ?? refIdOf(row)) }))
7804
+ .filter((option) => option.value !== '')
7805
+ .sort((a, b) => a.label.localeCompare(b.label))
7806
+ }
7807
+ },
7808
+
7809
+ /**
7810
+ * The column switch changed.
7811
+ *
7812
+ * @param {string} field The column field picked.
7813
+ * @return {void}
7814
+ * @spec openspec/changes/screens-board-reference-columns/specs/board-view/spec.md#requirement-a-board-can-switch-the-field-its-columns-come-from
7815
+ */
7816
+ onBoardColumnFieldChange(field) {
7817
+ this.boardColumnFieldChoice = field
7818
+ /**
7819
+ * @event board-column-field-change The board's column switch picked another field. Payload: the field name.
7820
+ */
7821
+ this.$emit('board-column-field-change', field)
7822
+ },
7823
+
7824
+ /**
7825
+ * The narrowing select changed: it sets the page filter on its field,
7826
+ * so the cards narrow with the columns and the active filter shows.
7827
+ *
7828
+ * @param {string} value The parent id, '' for all.
7829
+ * @return {void}
7830
+ */
7831
+ onBoardNarrowChange(value) {
7832
+ this.onFilterEvent({ key: this.boardNarrow.field, values: value === '' ? [] : [value] })
7833
+ },
7834
+
7295
7835
  /**
7296
7836
  * A paged board's "Show N more": ask for the next page.
7297
7837
  *
@@ -7299,6 +7839,10 @@ export default {
7299
7839
  * @spec openspec/changes/screens-kanban-parity/specs/board-view/spec.md#requirement-a-long-column-is-cut-with-a-show-more-button
7300
7840
  */
7301
7841
  onBoardLoadMore() {
7842
+ if (this.boardLoadsAll) {
7843
+ this.list.loadMore()
7844
+ return
7845
+ }
7302
7846
  const page = Number(this.pagination?.page || 1)
7303
7847
  if (page < Number(this.pagination?.pages || 1)) {
7304
7848
  this.onPageEvent(page + 1)
@@ -8637,6 +9181,23 @@ export default {
8637
9181
  this.$emit('view-mode-change', mode)
8638
9182
  },
8639
9183
 
9184
+ /**
9185
+ * Open in the mode the address names (`?_view=board`), when the page
9186
+ * offers it. The key starts with `_`, like `_search` and `_page`, so it
9187
+ * is never read as a list filter. A menu entry's `query` can so open a
9188
+ * list as its board, on the one page that lists the schema
9189
+ * (screens-board-reference-columns).
9190
+ *
9191
+ * @return {void}
9192
+ * @spec openspec/changes/screens-board-reference-columns/specs/board-view/spec.md#requirement-the-address-can-open-a-list-in-a-view-mode
9193
+ */
9194
+ applyViewModeFromRoute() {
9195
+ const wanted = this.$route?.query?._view
9196
+ if (typeof wanted === 'string' && wanted !== this.currentViewMode && this.effectiveToggleModes.includes(wanted)) {
9197
+ this.currentViewMode = wanted
9198
+ }
9199
+ },
9200
+
8640
9201
  /**
8641
9202
  * Handle selection changes from CnDataTable/CnCardGrid.
8642
9203
  * Updates internal state and re-emits for parent.