@conduction/nextcloud-vue 2.78.0 → 2.80.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 (259) hide show
  1. package/dist/bundled-packages.json +0 -2
  2. package/dist/esm/bundled-packages.json +0 -2
  3. package/dist/esm/components/CnActionButtons/CnActionButtons.vue2.js.map +1 -1
  4. package/dist/esm/components/CnActionButtons/CnActionButtons.vue3.js +3 -1
  5. package/dist/esm/components/CnActionButtons/CnActionButtons.vue3.js.map +1 -1
  6. package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js +32 -3
  7. package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js.map +1 -1
  8. package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js +115 -104
  9. package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js.map +1 -1
  10. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js +12 -2
  11. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
  12. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js.map +1 -1
  13. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js +39 -3
  14. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js.map +1 -1
  15. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue3.js.map +1 -1
  16. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js +158 -0
  17. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js.map +1 -1
  18. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js +157 -96
  19. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js.map +1 -1
  20. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +139 -1
  21. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  22. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +140 -92
  23. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  24. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +77 -0
  25. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
  26. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +5 -2
  27. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
  28. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +62 -4
  29. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  30. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  31. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js +85 -1
  32. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
  33. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js +43 -4
  34. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
  35. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue.js +11 -0
  36. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue.js.map +1 -0
  37. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js +736 -0
  38. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js.map +1 -0
  39. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js +238 -0
  40. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js.map +1 -0
  41. package/dist/esm/components/CnFactsWidget/index.js +30 -0
  42. package/dist/esm/components/CnFactsWidget/index.js.map +1 -0
  43. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue.js +10 -0
  44. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue.js.map +1 -0
  45. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js +551 -0
  46. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js.map +1 -0
  47. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js +183 -0
  48. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js.map +1 -0
  49. package/dist/esm/components/CnFieldListWidget/index.js +29 -0
  50. package/dist/esm/components/CnFieldListWidget/index.js.map +1 -0
  51. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js +173 -5
  52. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js.map +1 -1
  53. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js +181 -99
  54. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js.map +1 -1
  55. package/dist/esm/components/CnFilesBrowser/filesBrowser.js +29 -11
  56. package/dist/esm/components/CnFilesBrowser/filesBrowser.js.map +1 -1
  57. package/dist/esm/components/CnFilesBrowser/openRegisterSource.js +252 -0
  58. package/dist/esm/components/CnFilesBrowser/openRegisterSource.js.map +1 -0
  59. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js +66 -18
  60. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
  61. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js +4 -4
  62. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
  63. package/dist/esm/components/CnFormPage/CnFormPage.vue2.js +18 -3
  64. package/dist/esm/components/CnFormPage/CnFormPage.vue2.js.map +1 -1
  65. package/dist/esm/components/CnFormPage/CnFormPage.vue3.js.map +1 -1
  66. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +330 -74
  67. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  68. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +34 -15
  69. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  70. package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue.js +10 -0
  71. package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue.js.map +1 -0
  72. package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue2.js +99 -0
  73. package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue2.js.map +1 -0
  74. package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue3.js +48 -0
  75. package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue3.js.map +1 -0
  76. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js +100 -3
  77. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js.map +1 -1
  78. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js +4 -2
  79. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js.map +1 -1
  80. package/dist/esm/components/CnPageRenderer/CnPageRenderer.vue2.js +29 -1
  81. package/dist/esm/components/CnPageRenderer/CnPageRenderer.vue2.js.map +1 -1
  82. package/dist/esm/components/CnPageRenderer/CnPageRenderer.vue3.js.map +1 -1
  83. package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue2.js +3 -2
  84. package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue2.js.map +1 -1
  85. package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue3.js.map +1 -1
  86. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +10 -0
  87. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js.map +1 -0
  88. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +387 -0
  89. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -0
  90. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js +169 -0
  91. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -0
  92. package/dist/esm/components/CnRecordListWidget/index.js +31 -0
  93. package/dist/esm/components/CnRecordListWidget/index.js.map +1 -0
  94. package/dist/esm/components/CnSavedViewsControl/CnSavedViewsControl.vue2.js.map +1 -1
  95. package/dist/esm/components/CnSavedViewsControl/CnSavedViewsControl.vue3.js +1 -1
  96. package/dist/esm/components/CnSavedViewsControl/CnSavedViewsControl.vue3.js.map +1 -1
  97. package/dist/esm/components/CnSectionsWidget/CnSectionsWidget.vue.js +10 -0
  98. package/dist/esm/components/CnSectionsWidget/CnSectionsWidget.vue.js.map +1 -0
  99. package/dist/esm/components/CnSectionsWidget/CnSectionsWidget.vue2.js +351 -0
  100. package/dist/esm/components/CnSectionsWidget/CnSectionsWidget.vue2.js.map +1 -0
  101. package/dist/esm/components/CnSectionsWidget/CnSectionsWidget.vue3.js +71 -0
  102. package/dist/esm/components/CnSectionsWidget/CnSectionsWidget.vue3.js.map +1 -0
  103. package/dist/esm/components/CnSectionsWidget/index.js +35 -0
  104. package/dist/esm/components/CnSectionsWidget/index.js.map +1 -0
  105. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue2.js +81 -2
  106. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue2.js.map +1 -1
  107. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue3.js.map +1 -1
  108. package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js.map +1 -1
  109. package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js +1 -0
  110. package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js.map +1 -1
  111. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +152 -8
  112. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
  113. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js +84 -23
  114. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
  115. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js +4 -0
  116. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js.map +1 -1
  117. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js +13 -0
  118. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js.map +1 -1
  119. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue3.js.map +1 -1
  120. package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue2.js +106 -2
  121. package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue2.js.map +1 -1
  122. package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue3.js +105 -53
  123. package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue3.js.map +1 -1
  124. package/dist/esm/composables/useAppManifest.js +8 -3
  125. package/dist/esm/composables/useAppManifest.js.map +1 -1
  126. package/dist/esm/composables/useFormDraft.js +140 -9
  127. package/dist/esm/composables/useFormDraft.js.map +1 -1
  128. package/dist/esm/index.js +5 -0
  129. package/dist/esm/index.js.map +1 -1
  130. package/dist/esm/integrations/builtin/field-inspection/CnFieldInspectionCard.vue2.js +28 -20
  131. package/dist/esm/integrations/builtin/field-inspection/CnFieldInspectionCard.vue2.js.map +1 -1
  132. package/dist/esm/integrations/builtin/field-inspection/CnFieldInspectionCard.vue3.js +9 -0
  133. package/dist/esm/integrations/builtin/field-inspection/CnFieldInspectionCard.vue3.js.map +1 -1
  134. package/dist/esm/integrations/builtin/field-inspection.js +21 -0
  135. package/dist/esm/integrations/builtin/field-inspection.js.map +1 -1
  136. package/dist/esm/integrations/offline/checklistSubmission.js +141 -0
  137. package/dist/esm/integrations/offline/checklistSubmission.js.map +1 -0
  138. package/dist/esm/integrations/offline/fieldCollectionHelpers.js +70 -5
  139. package/dist/esm/integrations/offline/fieldCollectionHelpers.js.map +1 -1
  140. package/dist/esm/integrations/offline/index.js +2 -1
  141. package/dist/esm/integrations/offline/index.js.map +1 -1
  142. package/dist/esm/integrations/offline/offlineDb.js +6 -1
  143. package/dist/esm/integrations/offline/offlineDb.js.map +1 -1
  144. package/dist/esm/integrations/offline/syncReplayService.js +8 -2
  145. package/dist/esm/integrations/offline/syncReplayService.js.map +1 -1
  146. package/dist/esm/l10n/en.json.js +72 -1
  147. package/dist/esm/l10n/en.json.js.map +1 -1
  148. package/dist/esm/l10n/nl.json.js +120 -2
  149. package/dist/esm/l10n/nl.json.js.map +1 -1
  150. package/dist/esm/nextcloud-vue.css +1314 -74
  151. package/dist/esm/utils/boardDate.js +193 -0
  152. package/dist/esm/utils/boardDate.js.map +1 -0
  153. package/dist/esm/utils/detailActionModel.js +60 -3
  154. package/dist/esm/utils/detailActionModel.js.map +1 -1
  155. package/dist/esm/utils/dynamicProperties.js +5 -1
  156. package/dist/esm/utils/dynamicProperties.js.map +1 -1
  157. package/dist/esm/utils/fillTemplate.js +114 -0
  158. package/dist/esm/utils/fillTemplate.js.map +1 -0
  159. package/dist/esm/utils/libraryWidgetKeys.js +4 -0
  160. package/dist/esm/utils/libraryWidgetKeys.js.map +1 -1
  161. package/dist/esm/utils/queryPresets.js +96 -0
  162. package/dist/esm/utils/queryPresets.js.map +1 -0
  163. package/dist/esm/utils/resolveFilterTokens.js +11 -0
  164. package/dist/esm/utils/resolveFilterTokens.js.map +1 -1
  165. package/dist/esm/utils/schema.js +21 -0
  166. package/dist/esm/utils/schema.js.map +1 -1
  167. package/dist/esm/utils/sentinelTokens.js +3 -3
  168. package/dist/esm/utils/sentinelTokens.js.map +1 -1
  169. package/dist/esm/utils/validateManifestV2.compiled.js +57 -6
  170. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  171. package/dist/esm/utils/widgetDispatch.js +7 -1
  172. package/dist/esm/utils/widgetDispatch.js.map +1 -1
  173. package/dist/nextcloud-vue.cjs.js +14278 -8169
  174. package/dist/nextcloud-vue.cjs.js.map +2 -2
  175. package/dist/nextcloud-vue.css +1314 -74
  176. package/l10n/en.json +72 -1
  177. package/l10n/nl.json +120 -2
  178. package/package.json +1 -1
  179. package/scripts/build-validators.js +26 -0
  180. package/src/components/CnActionButtons/CnActionButtons.vue +2 -0
  181. package/src/components/CnActionsBar/CnActionsBar.md +4 -0
  182. package/src/components/CnActionsBar/CnActionsBar.vue +83 -42
  183. package/src/components/CnAppRoot/CnAppRoot.vue +12 -2
  184. package/src/components/CnBannerWidget/CnBannerWidget.vue +39 -3
  185. package/src/components/CnCellRenderer/CnCellRenderer.vue +306 -2
  186. package/src/components/CnDashboardPage/CnDashboardPage.vue +219 -4
  187. package/src/components/CnDataTable/CnDataTable.md +2 -0
  188. package/src/components/CnDataTable/CnDataTable.vue +82 -2
  189. package/src/components/CnDetailPage/CnDetailPage.md +1 -1
  190. package/src/components/CnDetailPage/CnDetailPage.vue +63 -3
  191. package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +127 -1
  192. package/src/components/CnFactsWidget/CnFactsWidget.vue +1050 -0
  193. package/src/components/CnFactsWidget/index.js +29 -0
  194. package/src/components/CnFieldListWidget/CnFieldListWidget.vue +746 -0
  195. package/src/components/CnFieldListWidget/index.js +28 -0
  196. package/src/components/CnFilesBrowser/CnFilesBrowser.vue +232 -11
  197. package/src/components/CnFilesBrowser/filesBrowser.js +43 -2
  198. package/src/components/CnFilesBrowser/index.js +1 -0
  199. package/src/components/CnFilesBrowser/openRegisterSource.js +248 -0
  200. package/src/components/CnFormDialog/CnFormDialog.md +9 -3
  201. package/src/components/CnFormDialog/CnFormDialog.vue +70 -22
  202. package/src/components/CnFormPage/CnFormPage.vue +18 -3
  203. package/src/components/CnIndexPage/CnIndexPage.vue +281 -9
  204. package/src/components/CnIndexPage/CnIndexPageWidgets.vue +169 -0
  205. package/src/components/CnObjectSidebar/CnFilesTab.vue +103 -4
  206. package/src/components/CnPageRenderer/CnPageRenderer.vue +29 -1
  207. package/src/components/CnQuickFilterBar/CnQuickFilterBar.vue +3 -2
  208. package/src/components/CnRecordListWidget/CnRecordListWidget.vue +603 -0
  209. package/src/components/CnRecordListWidget/index.js +30 -0
  210. package/src/components/CnSavedViewsControl/CnSavedViewsControl.vue +1 -1
  211. package/src/components/CnSectionsWidget/CnSectionsWidget.vue +489 -0
  212. package/src/components/CnSectionsWidget/index.js +34 -0
  213. package/src/components/CnStagesWidget/CnStagesWidget.vue +81 -2
  214. package/src/components/CnStatWidget/CnStatWidget.vue +1 -1
  215. package/src/components/CnTabsWidget/CnTabsWidget.vue +214 -9
  216. package/src/components/CnWidgetGrid/registerDashboardWidgets.js +4 -0
  217. package/src/components/CnWidgetObjectTable/CnWidgetObjectTable.vue +13 -0
  218. package/src/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue +188 -4
  219. package/src/components/index.js +5 -1
  220. package/src/composables/__tests__/useFormDraft.spec.js +69 -0
  221. package/src/composables/useAppManifest.js +8 -3
  222. package/src/composables/useFormDraft.js +139 -8
  223. package/src/css/form-field.css +14 -0
  224. package/src/css/index-page.css +71 -0
  225. package/src/css/kpi-card.css +32 -0
  226. package/src/css/look-board-detail.css +87 -0
  227. package/src/css/look-board-index.css +143 -14
  228. package/src/css/look-board.css +114 -2
  229. package/src/css/table.css +56 -0
  230. package/src/index.js +8 -0
  231. package/src/integrations/builtin/field-inspection/CnFieldInspectionCard.vue +35 -20
  232. package/src/integrations/builtin/field-inspection.js +21 -0
  233. package/src/integrations/offline/checklistSubmission.js +137 -0
  234. package/src/integrations/offline/fieldCollectionHelpers.js +69 -4
  235. package/src/integrations/offline/index.js +3 -0
  236. package/src/integrations/offline/offlineDb.js +6 -1
  237. package/src/integrations/offline/syncReplayService.js +8 -2
  238. package/src/schemas/app-manifest-v2.schema.json +149 -5
  239. package/src/utils/boardDate.js +189 -0
  240. package/src/utils/detailActionModel.js +59 -2
  241. package/src/utils/dynamicProperties.js +5 -1
  242. package/src/utils/fillTemplate.js +110 -0
  243. package/src/utils/libraryWidgetKeys.js +4 -0
  244. package/src/utils/queryPresets.js +93 -0
  245. package/src/utils/resolveFilterTokens.js +11 -0
  246. package/src/utils/schema.js +21 -0
  247. package/src/utils/sentinelTokens.js +3 -3
  248. package/src/utils/validateManifestV2.compiled.js +57 -2
  249. package/src/utils/widgetDispatch.js +7 -1
  250. package/dist/esm/_virtual/equal.js +0 -4
  251. package/dist/esm/_virtual/equal.js.map +0 -1
  252. package/dist/esm/_virtual/ucs2length.js +0 -4
  253. package/dist/esm/_virtual/ucs2length.js.map +0 -1
  254. package/dist/esm/node_modules/ajv/dist/runtime/equal.js +0 -9
  255. package/dist/esm/node_modules/ajv/dist/runtime/equal.js.map +0 -1
  256. package/dist/esm/node_modules/ajv/dist/runtime/ucs2length.js +0 -25
  257. package/dist/esm/node_modules/ajv/dist/runtime/ucs2length.js.map +0 -1
  258. package/dist/esm/node_modules/fast-deep-equal/index.js +0 -47
  259. package/dist/esm/node_modules/fast-deep-equal/index.js.map +0 -1
@@ -33,8 +33,8 @@
33
33
  <!-- @slot title-meta Content on the title's row, right of the title (e.g. a page-wide picker). Fill it from a manifest with `page.slots: { "title-meta": "<RegistryName>" }`. -->
34
34
  <slot name="title-meta" />
35
35
  </div>
36
- <p v-if="description" class="cn-dashboard-page__description">
37
- {{ resolvedDescription }}
36
+ <p v-if="subtitleText" class="cn-dashboard-page__description" data-testid="cn-dashboard-page-subtitle">
37
+ {{ subtitleText }}
38
38
  </p>
39
39
  </div>
40
40
  <div class="cn-dashboard-page__header-actions">
@@ -42,7 +42,7 @@
42
42
  widget on the page draws it. Its options control the view
43
43
  region below the grid. -->
44
44
  <CnSegmentedControl
45
- v-if="showsPageViewSwitch"
45
+ v-if="showsPageViewSwitch && !switchInRow"
46
46
  class="cn-page-view-switch"
47
47
  :options="viewSwitchOptions"
48
48
  :modelValue="activeViewId"
@@ -97,6 +97,7 @@
97
97
  Separate from the per-widget menus. `docs-anchor` deep-links
98
98
  the docs item to THIS page's section. -->
99
99
  <CnActionsMenu
100
+ v-if="showActionsMenu"
100
101
  :showRefresh="showRefresh"
101
102
  :showRequestFeature="showRequestFeature"
102
103
  :showReportBug="showReportBug"
@@ -138,6 +139,39 @@
138
139
  </div>
139
140
  </div>
140
141
 
142
+ <!-- The switch row (screens-dashboard-greeting-header): in the board
143
+ look the page's view switch sits under the header, left, and the
144
+ page's `viewLinks` sit right of it as small pills (DqMijnWerk). -->
145
+ <div
146
+ v-if="showHeader && (switchInRow || resolvedViewLinks.length > 0)"
147
+ class="cn-dashboard-page__switch-row"
148
+ data-testid="cn-dashboard-page-switch-row">
149
+ <CnSegmentedControl
150
+ v-if="switchInRow"
151
+ class="cn-page-view-switch"
152
+ :options="viewSwitchOptions"
153
+ :modelValue="activeViewId"
154
+ :ariaLabel="viewSwitchLabel"
155
+ :controls="viewRegionId"
156
+ data-testid="cn-dashboard-page-view-switch"
157
+ @update:modelValue="selectView" />
158
+ <span class="cn-dashboard-page__switch-row-spacer" />
159
+ <component
160
+ :is="link.tag"
161
+ v-for="link in resolvedViewLinks"
162
+ :key="link.key"
163
+ v-bind="link.attrs"
164
+ class="cn-dashboard-page__view-link"
165
+ data-testid="cn-dashboard-page-view-link">
166
+ <CnWidgetIcon
167
+ v-if="link.icon"
168
+ :name="link.icon"
169
+ :size="14"
170
+ class="cn-dashboard-page__view-link-icon" />
171
+ {{ link.label }}
172
+ </component>
173
+ </div>
174
+
141
175
  <!-- Date-range header (optional).
142
176
  Rendered only when `dateRange.enabled === true` AND
143
177
  `dateRange.showHeaderPicker !== false`. The range STATE
@@ -868,7 +902,8 @@
868
902
  </template>
869
903
 
870
904
  <script>
871
- import { translate as t } from '@nextcloud/l10n'
905
+ import { getCurrentUser } from '@nextcloud/auth'
906
+ import { getCanonicalLocale, translate as t } from '@nextcloud/l10n'
872
907
  import { NcActionButton, NcActionInput, NcActions, NcActionSeparator, NcButton, NcLoadingIcon, NcSelect } from '@nextcloud/vue'
873
908
  import { provide, ref, watch } from 'vue'
874
909
  import CalendarRange from 'vue-material-design-icons/CalendarRange.vue'
@@ -889,6 +924,7 @@ import CnKpiGrid from '../CnKpiGrid/CnKpiGrid.vue'
889
924
  import CnSegmentedControl from '../CnSegmentedControl/CnSegmentedControl.vue'
890
925
  import CnStatsBlockWidget from '../CnStatsBlockWidget/CnStatsBlockWidget.vue'
891
926
  import CnTileWidget from '../CnTileWidget/CnTileWidget.vue'
927
+ import CnWidgetIcon from '../CnWidgetGrid/CnWidgetIcon.vue'
892
928
  import CnWidgetRefItem from '../CnWidgetRefItem/CnWidgetRefItem.vue'
893
929
  import CnWidgetRenderer from '../CnWidgetRenderer/CnWidgetRenderer.vue'
894
930
  import CnWidgetWrapper from '../CnWidgetWrapper/CnWidgetWrapper.vue'
@@ -1056,6 +1092,7 @@ export default {
1056
1092
  CnLeafMountHost,
1057
1093
  CnSegmentedControl,
1058
1094
  CnKpiGrid,
1095
+ CnWidgetIcon,
1059
1096
  },
1060
1097
 
1061
1098
  mixins: [pageViews],
@@ -1156,6 +1193,50 @@ export default {
1156
1193
  default: () => [],
1157
1194
  },
1158
1195
 
1196
+ /**
1197
+ * Greet the reader in the header subtitle (manifest `config.greeting`):
1198
+ * `true` (or `"first"`) writes "Good afternoon, Pieter · Monday 5
1199
+ * October 2026", `"full"` uses the whole display name. The page
1200
+ * `description`, when set, follows after another middle dot. Off by
1201
+ * default: the subtitle is the description alone, as before.
1202
+ *
1203
+ * @spec openspec/changes/screens-dashboard-greeting-header/specs/dashboard-page/spec.md#requirement-the-header-subtitle-can-greet-the-reader
1204
+ * @type {boolean|string}
1205
+ */
1206
+ greeting: {
1207
+ type: [Boolean, String],
1208
+ default: false,
1209
+ },
1210
+
1211
+ /**
1212
+ * Links drawn as small pills on the switch row under the header
1213
+ * (manifest `config.viewLinks`), as DqMijnWerk's "Uw wachtrij / Aan
1214
+ * mij toegewezen / Dag afsluiten". Each `{ label, icon?, route?,
1215
+ * href? }`: `route` is a route name or a `{ name, params?, query? }`
1216
+ * location, `href` a plain URL; `label` goes through the host
1217
+ * translate function. An entry without a label or a target is skipped.
1218
+ *
1219
+ * @spec openspec/changes/screens-dashboard-greeting-header/specs/dashboard-page/spec.md#requirement-a-switch-row-carries-the-view-switch-and-link-pills
1220
+ * @type {Array<object>}
1221
+ */
1222
+ viewLinks: {
1223
+ type: Array,
1224
+ default: () => [],
1225
+ },
1226
+
1227
+ /**
1228
+ * Whether the header draws the page Actions menu (manifest
1229
+ * `config.showActionsMenu`). `false` removes it, for a board page
1230
+ * whose header carries only the buildiq square and the primary.
1231
+ *
1232
+ * @spec openspec/changes/screens-dashboard-greeting-header/specs/dashboard-page/spec.md#requirement-the-header-can-drop-the-page-actions-menu
1233
+ * @type {boolean}
1234
+ */
1235
+ showActionsMenu: {
1236
+ type: Boolean,
1237
+ default: true,
1238
+ },
1239
+
1159
1240
  /**
1160
1241
  * Whether a widget draws its overflow Actions menu when neither its
1161
1242
  * placement nor its definition says (`showActions`). `false` (manifest
@@ -1848,6 +1929,97 @@ export default {
1848
1929
  return typeof this.cnTranslate === 'function' ? this.cnTranslate : (key) => key
1849
1930
  },
1850
1931
 
1932
+ /**
1933
+ * The greeting for the time of day, with the reader's first (or
1934
+ * full) name when known, or '' when `greeting` is off.
1935
+ *
1936
+ * @spec openspec/changes/screens-dashboard-greeting-header/specs/dashboard-page/spec.md#requirement-the-header-subtitle-can-greet-the-reader
1937
+ * @return {string}
1938
+ */
1939
+ greetingLine() {
1940
+ if (this.greeting !== true && this.greeting !== 'first' && this.greeting !== 'full') {
1941
+ return ''
1942
+ }
1943
+ let user
1944
+ try {
1945
+ user = getCurrentUser()
1946
+ } catch {
1947
+ user = null
1948
+ }
1949
+ const displayName = (user && typeof user.displayName === 'string') ? user.displayName.trim() : ''
1950
+ const name = this.greeting === 'full' ? displayName : displayName.split(/\s+/)[0]
1951
+ const hour = new Date().getHours()
1952
+ if (hour < 12) {
1953
+ return name ? t('nextcloud-vue', 'Good morning, {name}', { name }) : t('nextcloud-vue', 'Good morning')
1954
+ }
1955
+ if (hour < 18) {
1956
+ return name ? t('nextcloud-vue', 'Good afternoon, {name}', { name }) : t('nextcloud-vue', 'Good afternoon')
1957
+ }
1958
+ return name ? t('nextcloud-vue', 'Good evening, {name}', { name }) : t('nextcloud-vue', 'Good evening')
1959
+ },
1960
+
1961
+ /**
1962
+ * The header subtitle: with `greeting` on, the greeting, today's date
1963
+ * written out and the description, joined by a middle dot; else the
1964
+ * description alone, as before.
1965
+ *
1966
+ * @spec openspec/changes/screens-dashboard-greeting-header/specs/dashboard-page/spec.md#requirement-the-header-subtitle-can-greet-the-reader
1967
+ * @return {string}
1968
+ */
1969
+ subtitleText() {
1970
+ const description = this.description ? this.resolvedDescription : ''
1971
+ const greeting = this.greetingLine
1972
+ if (greeting === '') {
1973
+ return description
1974
+ }
1975
+ const options = { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' }
1976
+ let date
1977
+ try {
1978
+ date = new Intl.DateTimeFormat(getCanonicalLocale() || undefined, options).format(new Date())
1979
+ } catch {
1980
+ date = new Intl.DateTimeFormat(undefined, options).format(new Date())
1981
+ }
1982
+ return [greeting, date, description].filter((part) => part !== '').join(' · ')
1983
+ },
1984
+
1985
+ /**
1986
+ * Whether the page's view switch sits on the switch row under the
1987
+ * header (board look) rather than among the header actions.
1988
+ *
1989
+ * @spec openspec/changes/screens-dashboard-greeting-header/specs/dashboard-page/spec.md#requirement-a-switch-row-carries-the-view-switch-and-link-pills
1990
+ * @return {boolean}
1991
+ */
1992
+ switchInRow() {
1993
+ return this.isBoardLook && this.showsPageViewSwitch
1994
+ },
1995
+
1996
+ /**
1997
+ * `viewLinks` as render descriptors: a router link for a route, an
1998
+ * anchor for an href, the label translated. Entries without a label
1999
+ * or a target are dropped.
2000
+ *
2001
+ * @spec openspec/changes/screens-dashboard-greeting-header/specs/dashboard-page/spec.md#requirement-a-switch-row-carries-the-view-switch-and-link-pills
2002
+ * @return {Array<{key: string, tag: string, attrs: object, label: string, icon: string}>}
2003
+ */
2004
+ resolvedViewLinks() {
2005
+ const links = Array.isArray(this.viewLinks) ? this.viewLinks : []
2006
+ const out = []
2007
+ links.forEach((link, index) => {
2008
+ if (!link || typeof link.label !== 'string' || link.label === '') {
2009
+ return
2010
+ }
2011
+ const icon = typeof link.icon === 'string' ? link.icon : ''
2012
+ const label = this.effectiveTranslate(link.label)
2013
+ if (link.route && this.$router) {
2014
+ const to = typeof link.route === 'string' ? { name: link.route } : link.route
2015
+ out.push({ key: `route-${index}`, tag: 'router-link', attrs: { to }, label, icon })
2016
+ } else if (typeof link.href === 'string' && link.href !== '') {
2017
+ out.push({ key: `href-${index}`, tag: 'a', attrs: { href: link.href }, label, icon })
2018
+ }
2019
+ })
2020
+ return out
2021
+ },
2022
+
1851
2023
  /**
1852
2024
  * The page title run through the host translate function so a
1853
2025
  * manifest-authored source string localises.
@@ -4392,6 +4564,49 @@ export default {
4392
4564
  height: var(--cn-board-control-height, 40px);
4393
4565
  }
4394
4566
 
4567
+ /* The switch row under the header (screens-dashboard-greeting-header): the
4568
+ view switch left, the `viewLinks` pills right (DqMijnWerk: pills 34px high,
4569
+ radius 17, 13px/600, a 14px icon, primary light on primary deep). */
4570
+ .cn-dashboard-page__switch-row {
4571
+ display: flex;
4572
+ flex-wrap: wrap;
4573
+ align-items: center;
4574
+ gap: 10px;
4575
+ margin-bottom: var(--cn-board-section-gap, 20px);
4576
+ }
4577
+
4578
+ .cn-dashboard-page__switch-row-spacer {
4579
+ flex: 1 1 auto;
4580
+ }
4581
+
4582
+ .cn-dashboard-page__view-link {
4583
+ display: inline-flex;
4584
+ align-items: center;
4585
+ gap: 6px;
4586
+ height: 34px;
4587
+ padding: 0 12px;
4588
+ border-radius: 17px;
4589
+ background: var(--color-primary-element-light);
4590
+ color: var(--color-primary-element-light-text);
4591
+ font-size: 13px;
4592
+ font-weight: 600;
4593
+ white-space: nowrap;
4594
+ text-decoration: none !important;
4595
+ }
4596
+
4597
+ .cn-dashboard-page__view-link:hover {
4598
+ background: var(--color-primary-element-light-hover, var(--color-primary-element-light));
4599
+ }
4600
+
4601
+ .cn-dashboard-page__view-link:focus-visible {
4602
+ outline: 2px solid var(--color-main-text);
4603
+ outline-offset: 2px;
4604
+ }
4605
+
4606
+ .cn-dashboard-page__view-link-icon {
4607
+ display: inline-flex;
4608
+ }
4609
+
4395
4610
  /* The period group, in a row of its own between the header and the KPI row. */
4396
4611
  .cn-dashboard-page__date-range--segmented {
4397
4612
  margin-bottom: 16px;
@@ -198,9 +198,11 @@ export default {
198
198
  | `rowIndicatorCap` | Number | `3` | How many declared indicators render on the row before the rest move into the row menu, reachable as `indicatorsFor(row).overflow`. |
199
199
  | `selectAllLabel` | String | `'Select all rows'` | Accessible name (`aria-label`) for the header select-all checkbox, so screen readers announce a named control (WCAG 4.1.2) |
200
200
  | `selectRowLabel` | String | `'Select row'` | Accessible name (`aria-label`) for each per-row select checkbox, so screen readers announce a named control (WCAG 4.1.2) |
201
+ | `rowTitle` | String | `'link'` | Board look only: how the title column draws its title. `link` underlines the title (not its secondary line). `plain` draws it as 15px bold text without an underline over a 13px muted secondary line, as the PqTickets board does; the row stays clickable. The Nextcloud look ignores it. |
201
202
  | `hideHeader` | Boolean | `false` | Hide the column-header row (`<thead>`) — for compact dashboard list widgets that want a plain bordered-row list without column labels |
202
203
  | `pinnedCount` | Number | `0` | How many data columns are pinned to the start of the table. They, and the selection and icon columns, stay in view when the table scrolls sideways |
203
204
  | `fixedLayout` | Boolean | `false` | Switch to `table-layout: fixed` so each column's `width` is authoritative rather than a hint the browser may override from cell content. Long unbreakable values (a PHP FQCN, a UUID) then wrap inside their cell instead of widening the column or painting past it, and no single unsized column soaks up all remaining width. Size every column when using it — percentages summing to 100 are easiest to reason about |
205
+ | `fitWidth` | Boolean | `false` | Fit the table to its container instead of growing past it: one column (a column's `grow: true`, else `title`, else `name`, else the first) takes the room that is left and ends in an ellipsis, and every other column keeps its content on one line, so trailing columns stay visible in a narrow tile. `CnWidgetObjectTable` turns it on |
204
206
  | `fillHeight` | Boolean | `false` | Fill the parent's height (a flex-column card / widget content area) so an optional `#footer` is pushed to the bottom instead of floating under a short list; the footer stays pinned via its sticky rule when the list overflows. No-op outside a height-constrained parent — opt-in |
205
207
  | `filterable` | Boolean | `false` | Show a filter button in every header whose column can filter: enum, boolean, text, number, date and reference columns backed by a schema property. A column opts out with `filterable: false`. Applying a filter emits `column-filter`; the host owns the state and passes it back as `activeFilters`. `CnIndexPage` turns it on by default. |
206
208
  | `activeFilters` | Object | `{}` | The active filter map, `{ paramKey: values[] }`: the same map the facet sidebar writes, so a header filter and a sidebar filter on one field show the same state. |
@@ -4,6 +4,7 @@
4
4
  data-testid="cn-object-list"
5
5
  :class="{
6
6
  'cn-table-container--board': isBoardLook,
7
+ 'cn-table-container--title-plain': isBoardLook && rowTitle === 'plain',
7
8
  'cn-table-container--scrollable': scrollable,
8
9
  'cn-table-container--borderless': borderless,
9
10
  'cn-table-container--fill': fillHeight,
@@ -96,7 +97,7 @@
96
97
  :aria-label="isScrollable ? scrollRegionLabel : undefined">
97
98
  <table
98
99
  class="cn-data-table"
99
- :class="{ 'cn-data-table--fixed': fixedLayout }"
100
+ :class="{ 'cn-data-table--fixed': fixedLayout, 'cn-data-table--fit': fitWidth }"
100
101
  data-testid="cn-object-list-table">
101
102
  <thead v-if="!hideHeader">
102
103
  <tr>
@@ -128,6 +129,8 @@
128
129
  col.sortable ? 'cn-table-header--sortable' : '',
129
130
  col.class || '',
130
131
  pinClass(leadingCount + colIndex),
132
+ isGrowColumn(col) ? 'cn-table-col--grow' : '',
133
+ colIndex === 0 ? 'cn-table-head--title' : '',
131
134
  ]"
132
135
  :style="{ ...(col.width ? { width: col.width } : {}), ...pinStyle(leadingCount + colIndex) }"
133
136
  :aria-sort="ariaSortFor(col)"
@@ -256,7 +259,7 @@
256
259
  <td
257
260
  v-for="(col, colIndex) in effectiveColumns"
258
261
  :key="col.key"
259
- :class="[col.class || '', col.cellClass || '', cellClass ? cellClass(row, col) : '', pinClass(leadingCount + colIndex), colIndex === 0 ? 'cn-table-col--title' : '']"
262
+ :class="[col.class || '', col.cellClass || '', cellClass ? cellClass(row, col) : '', pinClass(leadingCount + colIndex), colIndex === 0 ? 'cn-table-col--title' : '', isGrowColumn(col) ? 'cn-table-col--grow' : '']"
260
263
  :style="{ ...(col.width ? { maxWidth: col.width } : {}), ...pinStyle(leadingCount + colIndex) }"
261
264
  @mouseenter="titleWhenClipped">
262
265
  <!-- A row with a `rowClickRoute` is a real link: this anchor
@@ -499,6 +502,16 @@ function fillSecondaryTemplate(template, valueOf) {
499
502
  * </CnDataTable>
500
503
  * ```
501
504
  */
505
+
506
+ /**
507
+ * Column keys that name an object's system dates in OpenRegister's `@self`
508
+ * block. A column may declare one as its key (`@self.created`); it renders as
509
+ * a date-time although the schema has no property behind it.
510
+ *
511
+ * @type {string[]}
512
+ */
513
+ const SYSTEM_DATE_KEYS = ['@self.created', '@self.updated', '@self.published', '@self.depublished']
514
+
502
515
  export default {
503
516
  name: 'CnDataTable',
504
517
 
@@ -761,6 +774,21 @@ export default {
761
774
  default: '',
762
775
  },
763
776
 
777
+ /**
778
+ * How the board look draws the title column: `link` (the default) is
779
+ * the 15px weight 600 underlined title of the DqZaken board; `plain` is
780
+ * the 15px bold title without an underline and a 13px muted secondary
781
+ * line, as the PqTickets and OcPublicaties boards draw it. The row stays
782
+ * clickable either way. The Nextcloud look ignores it.
783
+ *
784
+ * @spec openspec/changes/screens-table-rows-parity/specs/index-list-board-look/spec.md#requirement-a-row-title-can-be-plain-text
785
+ */
786
+ rowTitle: {
787
+ type: String,
788
+ default: 'link',
789
+ validator: (value) => ['link', 'plain'].includes(value),
790
+ },
791
+
764
792
  /**
765
793
  * Drop the container's card chrome (border, radius, shadow) so the table
766
794
  * sits flush inside a parent that already provides a card (e.g. a
@@ -820,6 +848,23 @@ export default {
820
848
  default: false,
821
849
  },
822
850
 
851
+ /**
852
+ * Fit the table to its container instead of letting it grow past it.
853
+ * One column takes the room that is left and cuts its text with an
854
+ * ellipsis (the full value stays in the tooltip); every other column
855
+ * keeps its content on one line at its own width, so trailing columns
856
+ * such as a date stay visible in a narrow tile. The column that grows
857
+ * is the first with `grow: true`, else the one keyed `title` or `name`,
858
+ * else the first column. CnWidgetObjectTable turns this on.
859
+ *
860
+ * @type {boolean}
861
+ * @spec openspec/changes/narrow-object-table-widgets/specs/narrow-object-table-widgets/spec.md#requirement-a-narrow-object-table-keeps-its-trailing-columns-in-view
862
+ */
863
+ fitWidth: {
864
+ type: Boolean,
865
+ default: false,
866
+ },
867
+
823
868
  /**
824
869
  * Max number of rows to display. When the total exceeds it, only the first
825
870
  * `limit` render and the "View all" footer appears (with `viewAllRoute`).
@@ -1025,6 +1070,25 @@ export default {
1025
1070
  },
1026
1071
 
1027
1072
  /** @return {number} Leading cells before the data columns (selection, icon). */
1073
+ /**
1074
+ * The key of the column that takes the spare width under `fitWidth`:
1075
+ * the first with `grow: true`, else `title` or `name`, else the first.
1076
+ *
1077
+ * @return {string|null} The column key, or null when not fitting.
1078
+ * @spec openspec/changes/narrow-object-table-widgets/specs/narrow-object-table-widgets/spec.md#requirement-a-narrow-object-table-keeps-its-trailing-columns-in-view
1079
+ */
1080
+ growColumnKey() {
1081
+ if (!this.fitWidth || this.effectiveColumns.length === 0) {
1082
+ return null
1083
+ }
1084
+ const cols = this.effectiveColumns
1085
+ const chosen = cols.find((c) => c && c.grow === true)
1086
+ || cols.find((c) => c && c.key === 'title')
1087
+ || cols.find((c) => c && c.key === 'name')
1088
+ || cols[0]
1089
+ return chosen ? chosen.key : null
1090
+ },
1091
+
1028
1092
  leadingCount() {
1029
1093
  return (this.selectable ? 1 : 0) + (this.rowIcon ? 1 : 0)
1030
1094
  },
@@ -1321,6 +1385,16 @@ export default {
1321
1385
  },
1322
1386
 
1323
1387
  methods: {
1388
+ /**
1389
+ * Whether a column is the one that takes the spare width (`fitWidth`).
1390
+ *
1391
+ * @param {object} col The column.
1392
+ * @return {boolean}
1393
+ */
1394
+ isGrowColumn(col) {
1395
+ return this.growColumnKey !== null && !!col && col.key === this.growColumnKey
1396
+ },
1397
+
1324
1398
  /**
1325
1399
  * Whether a row's record changed since the user last looked
1326
1400
  * (`@self.unread`). A row without the marker renders as it always did.
@@ -1651,11 +1725,17 @@ export default {
1651
1725
  * the schema property's own `colorMap` / `x-color-map`.
1652
1726
  *
1653
1727
  * @param {object} col Column definition.
1728
+ * @spec openspec/changes/screens-table-footer-and-system-dates-parity/specs/index-list-board-look/spec.md#requirement-a-system-date-can-be-a-column
1654
1729
  * @return {object} Property definition for CnCellRenderer.
1655
1730
  * @spec openspec/changes/screens-cell-pill-parity/specs/cell-pill-tones/spec.md#requirement-a-manifest-column-colours-its-enum-pills
1656
1731
  */
1657
1732
  columnProperty(col) {
1658
1733
  const base = this.getSchemaProperty(col.key)
1734
+ // A system date (`@self.created`, `@self.updated`, ...) has no
1735
+ // schema property behind it; it renders as a date-time.
1736
+ if (Object.keys(base).length === 0 && SYSTEM_DATE_KEYS.includes(col.key) && !col.type && !col.format) {
1737
+ return { type: 'string', format: 'date-time' }
1738
+ }
1659
1739
  const colorMap = col && col.colorMap && typeof col.colorMap === 'object' && !Array.isArray(col.colorMap) ? col.colorMap : null
1660
1740
  if (col && (col.format || col.type || col.enum || colorMap)) {
1661
1741
  return {
@@ -422,7 +422,7 @@ Four levels of action and the case surfaces around them. All opt in. See [the re
422
422
  | `quickActions` | Array | `[]` | At most three always visible header buttons. |
423
423
  | `actionsMenu` | Object \| null | `null` | `{ showRefresh?, showHelpLinks?, label? }` for the overflow menu. |
424
424
  | `nextStep` | Object \| null | `null` | The "what now" card per stage, rendered by `CnNextStepCard`. |
425
- | `typePill` | Object \| null | `null` | `{ field, colorMap?, labels?, variant? }`, a pill above the title. |
425
+ | `typePill` | Object \| null | `null` | `{ field, colorMap?, labels?, variant? }`, a pill above the title. Instead of `field`: `text` (a constant) or `countFrom` + `countLabel` (a count of a related list). |
426
426
  | `statusPill` | Object \| null | `null` | A second pill, same shape. |
427
427
  | `headerFields` | Array | `[]` | Fields shown as chips under the title (manifest `config.headerFields`). Each entry is a property key, or `{ key, format, labelField, colorField, warnWhenPast }`; `format` is `text` (default), `mono`, `badge`, `user` or `date`. A `$ref` value shows `labelField` of the referenced object and falls back to the raw id in mono; a badge maps `colorField` (`success`, `warning`, `error`, `info`, `neutral`) to a variant; `warnWhenPast` gives a past date the error variant. An empty value shows no chip, a page without the key shows no row. Each chip reads "Title: value" to a screen reader, and the row prints as plain text. |
428
428
  | `sideColumn` | Array | `[]` | Widget definitions or widget ids, rendered as a column of cards beside the body. |
@@ -1231,12 +1231,15 @@ import {
1231
1231
  groupMenuEntries,
1232
1232
  MAX_QUICK_ACTIONS,
1233
1233
  normalisePinnedAction,
1234
+ pillCountSource,
1235
+ resolveCountPill,
1234
1236
  resolveNextStep,
1235
1237
  resolvePill,
1236
1238
  stageEntry,
1237
1239
  stageOf,
1238
1240
  } from '../../utils/detailActionModel.js'
1239
1241
  import { reportBindingProblems } from '../../utils/diagnostics.js'
1242
+ import { fetchListTotal } from '../../utils/fetchFilterCounts.js'
1240
1243
  import { cnGridCellStyle, hasGridRow } from '../../utils/grid.js'
1241
1244
  import { isActivityWidget, resolveHeaderMeta } from '../../utils/headerMeta.js'
1242
1245
  import { patchStoredSelf } from '../../utils/patchStoredSelf.js'
@@ -2393,9 +2396,13 @@ export default {
2393
2396
  * A pill above the title saying what kind of record this is
2394
2397
  * (manifest `config.typePill`): `{ field, colorMap?, labels?, variant? }`.
2395
2398
  * Rendered through `CnStatusBadge`; `colorMap` is keyed on the field's
2396
- * raw value, `labels` maps a raw value to the text to show.
2399
+ * raw value, `labels` maps a raw value to the text to show. Instead of
2400
+ * `field`: `text`, a constant ("Inwoner"), or `countFrom`
2401
+ * (`{ register, schema, filter? }`) with `countLabel` (`{ one, other }`
2402
+ * with `{count}`), a count of a related list ("1 lopende zaak") that
2403
+ * shows nothing at 0.
2397
2404
  *
2398
- * @type {{field: string, colorMap?: object, labels?: object, variant?: string}|null}
2405
+ * @type {{field?: string, text?: string, countFrom?: object, countLabel?: object, colorMap?: object, labels?: object, variant?: string}|null}
2399
2406
  */
2400
2407
  typePill: {
2401
2408
  type: Object,
@@ -2747,6 +2754,10 @@ export default {
2747
2754
 
2748
2755
  data() {
2749
2756
  return {
2757
+ /** The counts of the header's count pills (`countFrom`), by pill key (`type`, `status`). */
2758
+ pillCounts: {},
2759
+ /** Sequence of the last pill count round, so a late answer does not overwrite a newer one. */
2760
+ pillCountSeq: 0,
2750
2761
  /** `register/schema/id` the read-state call was last made for, so it is sent once per load. */
2751
2762
  readStateSentFor: '',
2752
2763
  /** Whether the user is arranging the chosen view (`userLayout`). */
@@ -3259,12 +3270,25 @@ export default {
3259
3270
  headerPills() {
3260
3271
  return [['type', this.typePill], ['status', this.statusPill]]
3261
3272
  .map(([key, config]) => {
3262
- const pill = resolvePill(config, this.resolvedObject)
3273
+ const pill = pillCountSource(config)
3274
+ ? resolveCountPill(config, this.pillCounts[key], (text) => this.effectiveTranslate(text))
3275
+ : resolvePill(config, this.resolvedObject)
3263
3276
  return pill ? { ...pill, key, label: this.effectiveTranslate(pill.label) } : null
3264
3277
  })
3265
3278
  .filter(Boolean)
3266
3279
  },
3267
3280
 
3281
+ /**
3282
+ * What the count pills depend on: the record id and the lists they
3283
+ * count. A change counts again.
3284
+ *
3285
+ * @return {string} The key.
3286
+ * @spec openspec/changes/screens-relation-pages/specs/relation-pages/spec.md#requirement-a-header-pill-can-be-a-constant-or-a-count
3287
+ */
3288
+ pillCountKey() {
3289
+ return JSON.stringify([this.objectId, pillCountSource(this.typePill), pillCountSource(this.statusPill)])
3290
+ },
3291
+
3268
3292
  /**
3269
3293
  * The widget the header holds (`headerWidget`): the definition in
3270
3294
  * `widgets` with that id, or null without the key or a match.
@@ -4337,6 +4361,14 @@ export default {
4337
4361
  },
4338
4362
 
4339
4363
  watch: {
4364
+ // The header's count pills count again for another record or other lists.
4365
+ pillCountKey: {
4366
+ immediate: true,
4367
+ handler() {
4368
+ this.refreshPillCounts()
4369
+ },
4370
+ },
4371
+
4340
4372
  // Opening a record marks it read, once, after its data has rendered.
4341
4373
  readStateDue: {
4342
4374
  immediate: true,
@@ -4754,6 +4786,33 @@ export default {
4754
4786
  }
4755
4787
  },
4756
4788
 
4789
+ /**
4790
+ * Count the lists behind the header's count pills (`countFrom`): one
4791
+ * list request each (`_limit=1`, the `total`). A failed count, or a
4792
+ * record without an id yet, shows no pill.
4793
+ *
4794
+ * @return {Promise<void>}
4795
+ * @spec openspec/changes/screens-relation-pages/specs/relation-pages/spec.md#requirement-a-header-pill-can-be-a-constant-or-a-count
4796
+ */
4797
+ async refreshPillCounts() {
4798
+ const pills = [['type', this.typePill], ['status', this.statusPill]]
4799
+ .map(([key, config]) => [key, pillCountSource(config)])
4800
+ .filter(([, source]) => source)
4801
+ const seq = ++this.pillCountSeq
4802
+ if (pills.length === 0 || this.objectId === null || this.objectId === undefined || this.objectId === '') {
4803
+ this.pillCounts = {}
4804
+ return
4805
+ }
4806
+ const ctx = { objectId: this.objectId, object: this.resolvedObject || undefined }
4807
+ const counts = {}
4808
+ await Promise.all(pills.map(async ([key, source]) => {
4809
+ counts[key] = await fetchListTotal(source.register, source.schema, source.filter, ctx).catch(() => null)
4810
+ }))
4811
+ if (seq === this.pillCountSeq) {
4812
+ this.pillCounts = counts
4813
+ }
4814
+ },
4815
+
4757
4816
  /**
4758
4817
  * A `cn:page:refresh` reached the page — re-read the object so every
4759
4818
  * object-bound widget re-renders from what the backend now holds.
@@ -4768,6 +4827,7 @@ export default {
4768
4827
  * @return {void}
4769
4828
  */
4770
4829
  onPageRefreshBus(payload) {
4830
+ this.refreshPillCounts()
4771
4831
  if (!this.hasSchemaDrivenFetch) {
4772
4832
  return
4773
4833
  }