@conduction/nextcloud-vue 2.59.0 → 2.61.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 (254) hide show
  1. package/dist/esm/components/CnActionsMenu/CnActionsMenu.vue2.js +14 -1
  2. package/dist/esm/components/CnActionsMenu/CnActionsMenu.vue2.js.map +1 -1
  3. package/dist/esm/components/CnActionsMenu/CnActionsMenu.vue3.js.map +1 -1
  4. package/dist/esm/components/CnAppNav/CnAppNav.vue2.js +80 -1
  5. package/dist/esm/components/CnAppNav/CnAppNav.vue2.js.map +1 -1
  6. package/dist/esm/components/CnAppNav/CnAppNav.vue3.js +46 -4
  7. package/dist/esm/components/CnAppNav/CnAppNav.vue3.js.map +1 -1
  8. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js +232 -10
  9. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js.map +1 -1
  10. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue3.js +79 -25
  11. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue3.js.map +1 -1
  12. package/dist/esm/components/CnBannerWidgetForm/CnBannerWidgetForm.vue2.js +42 -3
  13. package/dist/esm/components/CnBannerWidgetForm/CnBannerWidgetForm.vue2.js.map +1 -1
  14. package/dist/esm/components/CnBannerWidgetForm/CnBannerWidgetForm.vue3.js +39 -6
  15. package/dist/esm/components/CnBannerWidgetForm/CnBannerWidgetForm.vue3.js.map +1 -1
  16. package/dist/esm/components/CnBoardView/CnBoardView.vue2.js +46 -0
  17. package/dist/esm/components/CnBoardView/CnBoardView.vue2.js.map +1 -1
  18. package/dist/esm/components/CnBoardView/CnBoardView.vue3.js +11 -4
  19. package/dist/esm/components/CnBoardView/CnBoardView.vue3.js.map +1 -1
  20. package/dist/esm/components/CnBrandStripe/CnBrandStripe.vue.js +10 -0
  21. package/dist/esm/components/CnBrandStripe/CnBrandStripe.vue.js.map +1 -0
  22. package/dist/esm/components/CnBrandStripe/CnBrandStripe.vue2.js +39 -0
  23. package/dist/esm/components/CnBrandStripe/CnBrandStripe.vue2.js.map +1 -0
  24. package/dist/esm/components/CnBrandStripe/CnBrandStripe.vue3.js +16 -0
  25. package/dist/esm/components/CnBrandStripe/CnBrandStripe.vue3.js.map +1 -0
  26. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js +117 -1
  27. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js.map +1 -1
  28. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js +147 -89
  29. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js.map +1 -1
  30. package/dist/esm/components/CnConversationThread/CnConversationThread.vue.js +10 -0
  31. package/dist/esm/components/CnConversationThread/CnConversationThread.vue.js.map +1 -0
  32. package/dist/esm/components/CnConversationThread/CnConversationThread.vue2.js +259 -0
  33. package/dist/esm/components/CnConversationThread/CnConversationThread.vue2.js.map +1 -0
  34. package/dist/esm/components/CnConversationThread/CnConversationThread.vue3.js +131 -0
  35. package/dist/esm/components/CnConversationThread/CnConversationThread.vue3.js.map +1 -0
  36. package/dist/esm/components/CnConversationThread/CnConversationWidget.vue.js +10 -0
  37. package/dist/esm/components/CnConversationThread/CnConversationWidget.vue.js.map +1 -0
  38. package/dist/esm/components/CnConversationThread/CnConversationWidget.vue2.js +116 -0
  39. package/dist/esm/components/CnConversationThread/CnConversationWidget.vue2.js.map +1 -0
  40. package/dist/esm/components/CnConversationThread/CnConversationWidget.vue3.js +24 -0
  41. package/dist/esm/components/CnConversationThread/CnConversationWidget.vue3.js.map +1 -0
  42. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +10 -2
  43. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  44. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  45. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +503 -28
  46. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  47. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +303 -144
  48. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  49. package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewList.vue.js +10 -0
  50. package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewList.vue.js.map +1 -0
  51. package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewList.vue2.js +237 -0
  52. package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewList.vue2.js.map +1 -0
  53. package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewList.vue3.js +120 -0
  54. package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewList.vue3.js.map +1 -0
  55. package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewWidget.vue.js +10 -0
  56. package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewWidget.vue.js.map +1 -0
  57. package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewWidget.vue2.js +77 -0
  58. package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewWidget.vue2.js.map +1 -0
  59. package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewWidget.vue3.js +27 -0
  60. package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewWidget.vue3.js.map +1 -0
  61. package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue2.js +171 -12
  62. package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue2.js.map +1 -1
  63. package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue3.js +15 -7
  64. package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue3.js.map +1 -1
  65. package/dist/esm/components/CnHeaderWidgetForm/CnHeaderWidgetForm.vue2.js +37 -3
  66. package/dist/esm/components/CnHeaderWidgetForm/CnHeaderWidgetForm.vue2.js.map +1 -1
  67. package/dist/esm/components/CnHeaderWidgetForm/CnHeaderWidgetForm.vue3.js +44 -16
  68. package/dist/esm/components/CnHeaderWidgetForm/CnHeaderWidgetForm.vue3.js.map +1 -1
  69. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +132 -3
  70. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  71. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +6 -3
  72. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  73. package/dist/esm/components/CnLinkCardsPage/CnLinkCardsPage.vue.js +10 -0
  74. package/dist/esm/components/CnLinkCardsPage/CnLinkCardsPage.vue.js.map +1 -0
  75. package/dist/esm/components/CnLinkCardsPage/CnLinkCardsPage.vue2.js +316 -0
  76. package/dist/esm/components/CnLinkCardsPage/CnLinkCardsPage.vue2.js.map +1 -0
  77. package/dist/esm/components/CnLinkCardsPage/CnLinkCardsPage.vue3.js +105 -0
  78. package/dist/esm/components/CnLinkCardsPage/CnLinkCardsPage.vue3.js.map +1 -0
  79. package/dist/esm/components/CnNextStepCard/CnNextStepCard.vue.js +10 -0
  80. package/dist/esm/components/CnNextStepCard/CnNextStepCard.vue.js.map +1 -0
  81. package/dist/esm/components/CnNextStepCard/CnNextStepCard.vue2.js +150 -0
  82. package/dist/esm/components/CnNextStepCard/CnNextStepCard.vue2.js.map +1 -0
  83. package/dist/esm/components/CnNextStepCard/CnNextStepCard.vue3.js +106 -0
  84. package/dist/esm/components/CnNextStepCard/CnNextStepCard.vue3.js.map +1 -0
  85. package/dist/esm/components/CnNextStepCard/CnNextStepWidget.vue.js +9 -0
  86. package/dist/esm/components/CnNextStepCard/CnNextStepWidget.vue.js.map +1 -0
  87. package/dist/esm/components/CnNextStepCard/CnNextStepWidget.vue2.js +47 -0
  88. package/dist/esm/components/CnNextStepCard/CnNextStepWidget.vue2.js.map +1 -0
  89. package/dist/esm/components/CnNextStepCard/CnNextStepWidget.vue3.js +17 -0
  90. package/dist/esm/components/CnNextStepCard/CnNextStepWidget.vue3.js.map +1 -0
  91. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js +49 -0
  92. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js.map +1 -1
  93. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue3.js.map +1 -1
  94. package/dist/esm/components/CnObjectDataWidgetForm/CnObjectDataWidgetForm.vue2.js +6 -0
  95. package/dist/esm/components/CnObjectDataWidgetForm/CnObjectDataWidgetForm.vue2.js.map +1 -1
  96. package/dist/esm/components/CnObjectDataWidgetForm/CnObjectDataWidgetForm.vue3.js.map +1 -1
  97. package/dist/esm/components/CnObjectKanban/CnObjectKanban.vue2.js +46 -0
  98. package/dist/esm/components/CnObjectKanban/CnObjectKanban.vue2.js.map +1 -1
  99. package/dist/esm/components/CnObjectKanban/CnObjectKanban.vue3.js +15 -3
  100. package/dist/esm/components/CnObjectKanban/CnObjectKanban.vue3.js.map +1 -1
  101. package/dist/esm/components/CnPageRenderer/pageTypes.js +1 -0
  102. package/dist/esm/components/CnPageRenderer/pageTypes.js.map +1 -1
  103. package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue2.js +37 -1
  104. package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue2.js.map +1 -1
  105. package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue3.js +29 -13
  106. package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue3.js.map +1 -1
  107. package/dist/esm/components/CnSavedViewsControl/CnSavedViewsControl.vue2.js +50 -3
  108. package/dist/esm/components/CnSavedViewsControl/CnSavedViewsControl.vue2.js.map +1 -1
  109. package/dist/esm/components/CnSavedViewsControl/CnSavedViewsControl.vue3.js.map +1 -1
  110. package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue.js +10 -0
  111. package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue.js.map +1 -0
  112. package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue2.js +186 -0
  113. package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue2.js.map +1 -0
  114. package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue3.js +46 -0
  115. package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue3.js.map +1 -0
  116. package/dist/esm/components/CnStackedBarWidget/CnStackedBarWidget.vue.js +11 -0
  117. package/dist/esm/components/CnStackedBarWidget/CnStackedBarWidget.vue.js.map +1 -0
  118. package/dist/esm/components/CnStackedBarWidget/CnStackedBarWidget.vue2.js +298 -0
  119. package/dist/esm/components/CnStackedBarWidget/CnStackedBarWidget.vue2.js.map +1 -0
  120. package/dist/esm/components/CnStackedBarWidget/CnStackedBarWidget.vue3.js +77 -0
  121. package/dist/esm/components/CnStackedBarWidget/CnStackedBarWidget.vue3.js.map +1 -0
  122. package/dist/esm/components/CnStackedBarWidgetForm/CnStackedBarWidgetForm.vue.js +10 -0
  123. package/dist/esm/components/CnStackedBarWidgetForm/CnStackedBarWidgetForm.vue.js.map +1 -0
  124. package/dist/esm/components/CnStackedBarWidgetForm/CnStackedBarWidgetForm.vue2.js +169 -0
  125. package/dist/esm/components/CnStackedBarWidgetForm/CnStackedBarWidgetForm.vue2.js.map +1 -0
  126. package/dist/esm/components/CnStackedBarWidgetForm/CnStackedBarWidgetForm.vue3.js +48 -0
  127. package/dist/esm/components/CnStackedBarWidgetForm/CnStackedBarWidgetForm.vue3.js.map +1 -0
  128. package/dist/esm/components/CnTabs/CnTab.vue2.js +35 -0
  129. package/dist/esm/components/CnTabs/CnTab.vue2.js.map +1 -1
  130. package/dist/esm/components/CnTabs/CnTab.vue3.js.map +1 -1
  131. package/dist/esm/components/CnTabs/CnTabs.vue2.js +120 -7
  132. package/dist/esm/components/CnTabs/CnTabs.vue2.js.map +1 -1
  133. package/dist/esm/components/CnTabs/CnTabs.vue3.js +57 -5
  134. package/dist/esm/components/CnTabs/CnTabs.vue3.js.map +1 -1
  135. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +59 -4
  136. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
  137. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js +6 -2
  138. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
  139. package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue.js +10 -0
  140. package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue.js.map +1 -0
  141. package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue2.js +484 -0
  142. package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue2.js.map +1 -0
  143. package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue3.js +115 -0
  144. package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue3.js.map +1 -0
  145. package/dist/esm/components/CnWeekStripWidgetForm/CnWeekStripWidgetForm.vue.js +10 -0
  146. package/dist/esm/components/CnWeekStripWidgetForm/CnWeekStripWidgetForm.vue.js.map +1 -0
  147. package/dist/esm/components/CnWeekStripWidgetForm/CnWeekStripWidgetForm.vue2.js +189 -0
  148. package/dist/esm/components/CnWeekStripWidgetForm/CnWeekStripWidgetForm.vue2.js.map +1 -0
  149. package/dist/esm/components/CnWeekStripWidgetForm/CnWeekStripWidgetForm.vue3.js +74 -0
  150. package/dist/esm/components/CnWeekStripWidgetForm/CnWeekStripWidgetForm.vue3.js.map +1 -0
  151. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js +82 -8
  152. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js.map +1 -1
  153. package/dist/esm/index.js +16 -0
  154. package/dist/esm/index.js.map +1 -1
  155. package/dist/esm/l10n/en.json.js +55 -1
  156. package/dist/esm/l10n/en.json.js.map +1 -1
  157. package/dist/esm/l10n/nl.json.js +55 -1
  158. package/dist/esm/l10n/nl.json.js.map +1 -1
  159. package/dist/esm/nextcloud-vue.css +1036 -19
  160. package/dist/esm/utils/dateVariant.js +124 -0
  161. package/dist/esm/utils/dateVariant.js.map +1 -0
  162. package/dist/esm/utils/detailActionModel.js +260 -0
  163. package/dist/esm/utils/detailActionModel.js.map +1 -0
  164. package/dist/esm/utils/dueRule.js +84 -0
  165. package/dist/esm/utils/dueRule.js.map +1 -0
  166. package/dist/esm/utils/fetchAggregate.js +37 -1
  167. package/dist/esm/utils/fetchAggregate.js.map +1 -1
  168. package/dist/esm/utils/fetchFilterCounts.js +186 -0
  169. package/dist/esm/utils/fetchFilterCounts.js.map +1 -0
  170. package/dist/esm/utils/headers.js +28 -2
  171. package/dist/esm/utils/headers.js.map +1 -1
  172. package/dist/esm/utils/libraryWidgetKeys.js +5 -0
  173. package/dist/esm/utils/libraryWidgetKeys.js.map +1 -1
  174. package/dist/esm/utils/readPath.js +30 -0
  175. package/dist/esm/utils/readPath.js.map +1 -0
  176. package/dist/esm/utils/validateManifestV2.compiled.js +2 -2
  177. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  178. package/dist/nextcloud-vue.cjs.js +16019 -9528
  179. package/dist/nextcloud-vue.cjs.js.map +2 -2
  180. package/dist/nextcloud-vue.css +1036 -19
  181. package/l10n/en.json +55 -1
  182. package/l10n/nl.json +55 -1
  183. package/package.json +1 -1
  184. package/scripts/.jsdoc-baselines.json +4 -0
  185. package/src/components/CnActionsMenu/CnActionsMenu.vue +14 -1
  186. package/src/components/CnAppNav/CnAppNav.md +26 -0
  187. package/src/components/CnAppNav/CnAppNav.vue +145 -2
  188. package/src/components/CnBannerWidget/CnBannerWidget.vue +373 -11
  189. package/src/components/CnBannerWidgetForm/CnBannerWidgetForm.vue +65 -3
  190. package/src/components/CnBoardView/CnBoardView.vue +77 -0
  191. package/src/components/CnBrandStripe/CnBrandStripe.md +22 -0
  192. package/src/components/CnBrandStripe/CnBrandStripe.vue +100 -0
  193. package/src/components/CnBrandStripe/index.js +4 -0
  194. package/src/components/CnCellRenderer/CnCellRenderer.vue +207 -1
  195. package/src/components/CnConversationThread/CnConversationThread.md +51 -0
  196. package/src/components/CnConversationThread/CnConversationThread.vue +457 -0
  197. package/src/components/CnConversationThread/CnConversationWidget.vue +144 -0
  198. package/src/components/CnConversationThread/index.js +4 -0
  199. package/src/components/CnDashboardPage/CnDashboardPage.vue +10 -2
  200. package/src/components/CnDetailPage/CnDetailPage.md +16 -0
  201. package/src/components/CnDetailPage/CnDetailPage.vue +687 -49
  202. package/src/components/CnDocumentReviewList/CnDocumentReviewList.md +55 -0
  203. package/src/components/CnDocumentReviewList/CnDocumentReviewList.vue +384 -0
  204. package/src/components/CnDocumentReviewList/CnDocumentReviewWidget.vue +108 -0
  205. package/src/components/CnDocumentReviewList/index.js +4 -0
  206. package/src/components/CnHeaderWidget/CnHeaderWidget.vue +191 -14
  207. package/src/components/CnHeaderWidgetForm/CnHeaderWidgetForm.vue +53 -3
  208. package/src/components/CnIndexPage/CnIndexPage.vue +135 -3
  209. package/src/components/CnLinkCardsPage/CnLinkCardsPage.vue +484 -0
  210. package/src/components/CnLinkCardsPage/index.js +4 -0
  211. package/src/components/CnNextStepCard/CnNextStepCard.md +40 -0
  212. package/src/components/CnNextStepCard/CnNextStepCard.vue +319 -0
  213. package/src/components/CnNextStepCard/CnNextStepWidget.vue +59 -0
  214. package/src/components/CnNextStepCard/index.js +4 -0
  215. package/src/components/CnObjectDataWidget/CnObjectDataWidget.vue +49 -0
  216. package/src/components/CnObjectDataWidgetForm/CnObjectDataWidgetForm.vue +6 -0
  217. package/src/components/CnObjectKanban/CnObjectKanban.vue +86 -2
  218. package/src/components/CnPageRenderer/pageTypes.js +1 -0
  219. package/src/components/CnQuickFilterBar/CnQuickFilterBar.md +3 -0
  220. package/src/components/CnQuickFilterBar/CnQuickFilterBar.vue +60 -1
  221. package/src/components/CnSavedViewsControl/CnSavedViewsControl.vue +50 -3
  222. package/src/components/CnSegmentedControl/CnSegmentedControl.md +46 -0
  223. package/src/components/CnSegmentedControl/CnSegmentedControl.vue +291 -0
  224. package/src/components/CnSegmentedControl/index.js +4 -0
  225. package/src/components/CnStackedBarWidget/CnStackedBarWidget.md +15 -0
  226. package/src/components/CnStackedBarWidget/CnStackedBarWidget.vue +414 -0
  227. package/src/components/CnStackedBarWidget/index.js +14 -0
  228. package/src/components/CnStackedBarWidgetForm/CnStackedBarWidgetForm.vue +224 -0
  229. package/src/components/CnStackedBarWidgetForm/index.js +4 -0
  230. package/src/components/CnTabs/CnTab.vue +35 -0
  231. package/src/components/CnTabs/CnTabs.vue +244 -10
  232. package/src/components/CnTabsWidget/CnTabsWidget.vue +63 -4
  233. package/src/components/CnWeekStripWidget/CnWeekStripWidget.md +31 -0
  234. package/src/components/CnWeekStripWidget/CnWeekStripWidget.vue +668 -0
  235. package/src/components/CnWeekStripWidget/index.js +14 -0
  236. package/src/components/CnWeekStripWidgetForm/CnWeekStripWidgetForm.vue +265 -0
  237. package/src/components/CnWeekStripWidgetForm/index.js +4 -0
  238. package/src/components/CnWidgetGrid/registerDashboardWidgets.js +74 -0
  239. package/src/components/index.js +12 -0
  240. package/src/css/detail-page.css +57 -2
  241. package/src/index.js +16 -0
  242. package/src/schemas/app-manifest-v2.schema.json +433 -3
  243. package/src/schemas/app-manifest.schema.json +2 -1
  244. package/src/types/manifest.d.ts +1 -1
  245. package/src/utils/dateVariant.js +120 -0
  246. package/src/utils/detailActionModel.js +256 -0
  247. package/src/utils/dueRule.js +94 -0
  248. package/src/utils/fetchAggregate.js +36 -0
  249. package/src/utils/fetchFilterCounts.js +182 -0
  250. package/src/utils/headers.js +28 -2
  251. package/src/utils/index.js +1 -0
  252. package/src/utils/libraryWidgetKeys.js +5 -0
  253. package/src/utils/readPath.js +27 -0
  254. package/src/utils/validateManifestV2.compiled.js +2 -2
@@ -21,7 +21,11 @@
21
21
  - Content sections via slots
22
22
  -->
23
23
  <template>
24
- <div class="cn-detail-page" data-testid="cn-detail-page" :style="{ maxWidth: maxWidth }">
24
+ <div
25
+ class="cn-detail-page"
26
+ :class="{ 'cn-detail-page--with-side': showsSideColumn }"
27
+ data-testid="cn-detail-page"
28
+ :style="{ maxWidth: maxWidth }">
25
29
  <!-- Skip link to the page's primary action. Off screen until it takes
26
30
  focus, so it costs a sighted mouse user nothing and saves a
27
31
  keyboard user every widget on the page. Targets the action the
@@ -29,7 +33,7 @@
29
33
  that lands somewhere arbitrary is worse than none, because it is
30
34
  the one link a keyboard user is told to trust. -->
31
35
  <a
32
- v-if="primaryActionLabel && !objectNotFound"
36
+ v-if="effectivePrimaryLabel && !objectNotFound"
33
37
  :href="`#${primaryActionAnchorId}`"
34
38
  class="cn-detail-page__skip-link"
35
39
  data-testid="cn-detail-page-skip-link"
@@ -72,6 +76,23 @@
72
76
  class="cn-detail-page__icon" />
73
77
  </slot>
74
78
  <div class="cn-detail-page__header-text">
79
+ <!-- Type and status pills (manifest `config.typePill` /
80
+ `config.statusPill`). What kind of record this is and
81
+ where it stands, readable before the title. Nothing
82
+ renders for a page that declares neither. -->
83
+ <div
84
+ v-if="headerPills.length > 0"
85
+ class="cn-detail-page__pills"
86
+ data-testid="cn-detail-page-pills">
87
+ <CnStatusBadge
88
+ v-for="pill in headerPills"
89
+ :key="pill.key"
90
+ :label="pill.label"
91
+ :colorKey="pill.colorKey"
92
+ :colorMap="pill.colorMap"
93
+ :variant="pill.variant"
94
+ :data-testid="`cn-detail-page-pill-${pill.key}`" />
95
+ </div>
75
96
  <!-- Type name as a small eyebrow above the record name, so
76
97
  the header reads "the record" not "the kind of record"
77
98
  (ADR-062). Only shown once the object resolves to a
@@ -82,7 +103,10 @@
82
103
  data-testid="cn-detail-page-type-eyebrow">
83
104
  {{ typeEyebrow }}
84
105
  </p>
85
- <h2 v-if="displayTitle" class="cn-detail-page__title">
106
+ <h2
107
+ v-if="displayTitle"
108
+ class="cn-detail-page__title"
109
+ :title="displayTitle">
86
110
  {{ displayTitle }}
87
111
  </h2>
88
112
  <!--
@@ -150,14 +174,38 @@
150
174
  </div>
151
175
  <!-- The action the page declares as its primary one, and the
152
176
  target the skip link above lands on. -->
177
+ <!-- With a "what now" card on the page the button lives in that
178
+ card, next to the checklist that explains it, and not here
179
+ as well: one primary button per page. -->
153
180
  <NcButton
154
- v-if="primaryActionLabel"
181
+ v-if="effectivePrimaryLabel && !showsNextStepCard"
155
182
  :id="primaryActionAnchorId"
156
183
  ref="primaryActionButton"
157
184
  variant="primary"
185
+ :href="primaryActionHref || undefined"
186
+ :target="primaryActionHref ? primaryActionTarget : undefined"
187
+ :disabled="primaryActionDisabled"
158
188
  data-testid="cn-detail-page-primary-action"
159
- @click="$emit('primary-action', primaryAction)">
160
- {{ primaryActionLabel }}
189
+ @click="onPrimaryActionClick">
190
+ {{ effectivePrimaryLabel }}
191
+ </NcButton>
192
+ <!-- Quick actions (manifest `config.quickActions`): the few
193
+ actions a handler reaches for on every record, always
194
+ visible. At most three, so the row cannot grow back into
195
+ the button wall the Actions menu replaced. -->
196
+ <NcButton
197
+ v-for="entry in quickActionEntries"
198
+ :key="`quick-${entry.id}`"
199
+ variant="secondary"
200
+ :href="entry.href || undefined"
201
+ :target="entry.href ? (entry.linkTarget || undefined) : undefined"
202
+ :disabled="entry.disabled"
203
+ :data-testid="`cn-detail-page-quick-${entry.id}`"
204
+ @click="onPinnedActionClick(entry)">
205
+ <template v-if="entry.iconName" #icon>
206
+ <CnIcon :name="entry.iconName" :size="20" />
207
+ </template>
208
+ {{ entry.label }}
161
209
  </NcButton>
162
210
  <!-- Declarative lifecycle transitions (manifest
163
211
  `config.lifecycleActions`). Status-gated; driven by the
@@ -245,11 +293,13 @@
245
293
  <!-- In-app edit button (ADR-041): icon-only, self-wires from CnAppRoot. -->
246
294
  <CnBuildiqEditButton />
247
295
  <CnActionsMenu
248
- :showRefresh="effectiveHeaderShowRefresh"
296
+ :showRefresh="menuShowsRefresh"
249
297
  :refreshing="effectiveRefreshing"
250
- :showRequestFeature="showRequestFeature"
251
- :showReportBug="showReportBug"
252
- :showDocumentation="showDocumentation"
298
+ :showRequestFeature="showRequestFeature && menuShowsHelpLinks"
299
+ :showReportBug="showReportBug && menuShowsHelpLinks"
300
+ :showDocumentation="showDocumentation && menuShowsHelpLinks"
301
+ :actionsMenuLabel="actionsMenuName"
302
+ :hasPrimaryItems="headerMenuGroups.length > 0"
253
303
  :documentationUrl="documentationUrl"
254
304
  :docsAnchor="resolvedPageId"
255
305
  :documentationLabel="documentationLabel || undefined"
@@ -260,40 +310,56 @@
260
310
  testidBase="cn-detail-page"
261
311
  @refresh="onHeaderRefresh"
262
312
  @requestFeature="onHeaderRequestFeature">
263
- <template v-if="headerMenuEntries.length" #primary-items>
264
- <template v-for="entry in headerMenuEntries">
265
- <!-- An action that goes to a URL is a LINK. The browser
266
- then supplies middle-click, "open in new tab" and the
267
- semantics assistive tech announces, none of which a
268
- click handler can. -->
269
- <NcActionLink
270
- v-if="entry.href"
271
- :key="`${entry.id}-link`"
272
- :href="entry.href"
273
- :target="entry.linkTarget || undefined"
274
- :data-testid="entry.testid">
275
- <template v-if="entry.iconName || entry.iconClass" #icon>
276
- <CnIcon v-if="entry.iconName" :name="entry.iconName" :size="20" />
277
- <span v-else :class="entry.iconClass" />
278
- </template>
279
- {{ entry.label }}
280
- </NcActionLink>
281
- <NcActionButton
282
- v-else
283
- :key="entry.id"
284
- :data-testid="entry.testid"
285
- :disabled="entry.disabled"
286
- :aria-pressed="entry.pressed === null ? null : String(entry.pressed)"
287
- :closeAfterClick="true"
288
- @click="entry.run()">
289
- <template v-if="entry.iconName || entry.iconClass" #icon>
290
- <CnIcon v-if="entry.iconName" :name="entry.iconName" :size="20" />
291
- <span v-else :class="entry.iconClass" />
292
- </template>
293
- {{ entry.label }}
294
- </NcActionButton>
313
+ <template v-if="headerMenuGroups.length" #primary-items>
314
+ <!-- One block per group. A page that groups nothing has
315
+ a single captionless group, and renders exactly the
316
+ flat list it always did. -->
317
+ <template v-for="(group, groupIndex) in headerMenuGroups">
318
+ <NcActionSeparator
319
+ v-if="groupIndex > 0"
320
+ :key="`${group.key}-separator`" />
321
+ <NcActionCaption
322
+ v-if="group.label"
323
+ :key="`${group.key}-caption`"
324
+ :name="group.label"
325
+ :data-testid="`cn-detail-page-action-group-${group.key}`" />
326
+ <template v-for="entry in group.entries">
327
+ <!-- An action that goes to a URL is a LINK. The browser
328
+ then supplies middle-click, "open in new tab" and the
329
+ semantics assistive tech announces, none of which a
330
+ click handler can. -->
331
+ <NcActionLink
332
+ v-if="entry.href"
333
+ :key="`${entry.id}-link`"
334
+ :href="entry.href"
335
+ :target="entry.linkTarget || undefined"
336
+ :data-testid="entry.testid">
337
+ <template v-if="entry.iconName || entry.iconClass" #icon>
338
+ <CnIcon v-if="entry.iconName" :name="entry.iconName" :size="20" />
339
+ <span v-else :class="entry.iconClass" />
340
+ </template>
341
+ {{ entry.label }}
342
+ </NcActionLink>
343
+ <NcActionButton
344
+ v-else
345
+ :key="entry.id"
346
+ :data-testid="entry.testid"
347
+ :disabled="entry.disabled"
348
+ :aria-pressed="entry.pressed === null ? null : String(entry.pressed)"
349
+ :closeAfterClick="true"
350
+ @click="entry.run()">
351
+ <template v-if="entry.iconName || entry.iconClass" #icon>
352
+ <CnIcon v-if="entry.iconName" :name="entry.iconName" :size="20" />
353
+ <span v-else :class="entry.iconClass" />
354
+ </template>
355
+ {{ entry.label }}
356
+ </NcActionButton>
357
+ </template>
295
358
  </template>
296
- <NcActionSeparator />
359
+ <!-- Divides the page's own actions from the help links
360
+ below. With the help links switched off there is
361
+ nothing below to divide from. -->
362
+ <NcActionSeparator v-if="menuShowsHelpLinks" />
297
363
  </template>
298
364
  </CnActionsMenu>
299
365
  </div>
@@ -326,6 +392,33 @@
326
392
  {{ editLockRefusal }}
327
393
  </p>
328
394
 
395
+ <!-- The "what now" card (manifest `config.nextStep`): what this stage
396
+ still needs, next to the button that takes the step. Above the
397
+ body and the side column, at the full width of both. Hidden for a
398
+ stage that declares no checklist. -->
399
+ <CnNextStepCard
400
+ v-if="showsNextStepCard"
401
+ class="cn-detail-page__next-step"
402
+ :title="nextStepCard.title"
403
+ titleTag="h3"
404
+ :items="nextStepCard.items"
405
+ :after="nextStepCard.after"
406
+ data-testid="cn-detail-page-next-step">
407
+ <template v-if="effectivePrimaryLabel" #action>
408
+ <NcButton
409
+ :id="primaryActionAnchorId"
410
+ ref="primaryActionButton"
411
+ variant="primary"
412
+ :href="primaryActionHref || undefined"
413
+ :target="primaryActionHref ? primaryActionTarget : undefined"
414
+ :disabled="primaryActionDisabled"
415
+ data-testid="cn-detail-page-primary-action"
416
+ @click="onPrimaryActionClick">
417
+ {{ effectivePrimaryLabel }}
418
+ </NcButton>
419
+ </template>
420
+ </CnNextStepCard>
421
+
329
422
  <!-- Not-found state: the record behind the address does not exist. -->
330
423
  <div v-if="objectNotFound" class="cn-detail-page__not-found" data-testid="cn-detail-page-not-found">
331
424
  <!--
@@ -709,6 +802,68 @@
709
802
  </div>
710
803
  </div>
711
804
 
805
+ <!-- Side column (manifest `config.sideColumn`): the record's facts as
806
+ a column of cards beside the body. A sibling of the body, after it
807
+ in the document, so it reads after the body and stacks under it
808
+ when the page is too narrow for both. -->
809
+ <aside
810
+ v-if="showsSideColumn"
811
+ class="cn-detail-page__side"
812
+ :aria-label="sideColumnLabel"
813
+ data-testid="cn-detail-page-side">
814
+ <div
815
+ v-for="widget in sideColumnWidgets"
816
+ :key="`side-${widget.id}`"
817
+ class="cn-detail-page__side-item"
818
+ :data-testid="`cn-detail-page-side-${widget.id}`">
819
+ <!--
820
+ @slot `widget-${widget.id}`
821
+ @description The same per-widget slot the body grid offers,
822
+ for a widget placed in the side column. Same name and same
823
+ bindings, so a custom widget placed here needs nothing it
824
+ would not need in the grid. Default content renders the
825
+ widget through `CnDetailWidgetHost` as a card.
826
+ @binding {object} item `{ id, widgetId }`, for parity with the grid slot.
827
+ @binding {object} widget The widget definition.
828
+ @binding {string|number} objectId This record's id.
829
+ @binding {object} object The loaded record, or null while it is still being fetched.
830
+ @binding {object} objectData Alias of `object`.
831
+ @binding {string} objectType Resolved object type slug.
832
+ @binding {string} register Register slug of this page.
833
+ @binding {string} schema Schema slug of this page.
834
+ -->
835
+ <slot
836
+ :name="`widget-${widget.id}`"
837
+ :item="{ id: widget.id, widgetId: widget.id }"
838
+ :widget="widget"
839
+ :objectId="objectId"
840
+ :object="resolvedObject"
841
+ :objectData="resolvedObject"
842
+ :objectType="resolvedObjectType"
843
+ :register="register"
844
+ :schema="schema">
845
+ <CnDetailWidgetHost
846
+ :widget="widget"
847
+ chrome="card"
848
+ :showCardTitle="true"
849
+ :objectId="objectId"
850
+ :object="currentObject"
851
+ :objectType="resolvedObjectType"
852
+ :schemaObject="currentSchema"
853
+ :register="register"
854
+ :schema="schema"
855
+ :store="effectiveObjectStore"
856
+ :surface="surface"
857
+ :integrationContext="effectiveIntegrationContext"
858
+ :hideEmpty="hideEmpty"
859
+ :cnRegistry="cnRegistry"
860
+ :availableWidgets="widgets"
861
+ @geoSaved="onGeoSaved"
862
+ @openIntegration="onAutoBodyOpenIntegration" />
863
+ </slot>
864
+ </div>
865
+ </aside>
866
+
712
867
  <!-- Per-widget style/config editor (ADR-041). Opened by the per-widget
713
868
  configure cog while in Buildiq edit mode; the modal's own Delete
714
869
  button removes the widget from the grid. -->
@@ -810,9 +965,10 @@
810
965
  </template>
811
966
 
812
967
  <script>
968
+ import { getCurrentUser } from '@nextcloud/auth'
813
969
  import { subscribe, unsubscribe } from '@nextcloud/event-bus'
814
970
  import { translate as t } from '@nextcloud/l10n'
815
- import { NcActionButton, NcActionLink, NcActionSeparator, NcButton, NcEmptyContent, NcLoadingIcon } from '@nextcloud/vue'
971
+ import { NcActionButton, NcActionCaption, NcActionLink, NcActionSeparator, NcButton, NcEmptyContent, NcLoadingIcon } from '@nextcloud/vue'
816
972
  import { provide, ref, watch } from 'vue'
817
973
  import AlertCircleOutline from 'vue-material-design-icons/AlertCircleOutline.vue'
818
974
  import ArrowLeft from 'vue-material-design-icons/ArrowLeft.vue'
@@ -834,6 +990,7 @@ import CnDetailWidgetHost from '../CnDetailWidgetHost/CnDetailWidgetHost.vue'
834
990
  import CnFormDialog from '../CnFormDialog/CnFormDialog.vue'
835
991
  import CnLifecycleActions from '../CnLifecycleActions/CnLifecycleActions.vue'
836
992
  import CnLockedBanner from '../CnLockedBanner/CnLockedBanner.vue'
993
+ import CnNextStepCard from '../CnNextStepCard/CnNextStepCard.vue'
837
994
  import CnRelatedCollections from '../CnRelatedCollections/CnRelatedCollections.vue'
838
995
  import CnSummaryAggregates from '../CnSummaryAggregates/CnSummaryAggregates.vue'
839
996
  import CnTranslatedBadge from '../CnTranslatedBadge/CnTranslatedBadge.vue'
@@ -846,6 +1003,15 @@ import { useObjectStore } from '../../store/index.js'
846
1003
  import { isAppInstalled } from '../../utils/appInstalled.js'
847
1004
  import { compactLayoutRows } from '../../utils/dashboardPlacement.js'
848
1005
  import { defaultDetailGrid } from '../../utils/defaultDetailGrid.js'
1006
+ import {
1007
+ groupMenuEntries,
1008
+ MAX_QUICK_ACTIONS,
1009
+ normalisePinnedAction,
1010
+ resolveNextStep,
1011
+ resolvePill,
1012
+ stageEntry,
1013
+ stageOf,
1014
+ } from '../../utils/detailActionModel.js'
849
1015
  import { cnGridCellStyle, hasGridRow } from '../../utils/grid.js'
850
1016
  import { slotRenders } from '../../utils/slotContent.js'
851
1017
  import {
@@ -860,6 +1026,7 @@ import {
860
1026
  } from '../../utils/widgetDispatch.js'
861
1027
  import { CnActionButtons } from '../CnActionButtons/index.js'
862
1028
  import { CnIcon } from '../CnIcon/index.js'
1029
+ import { CnStatusBadge } from '../CnStatusBadge/index.js'
863
1030
  import { getWidgetTypeEntry } from '../CnWidgetGrid/dashboardWidgetRegistry.js'
864
1031
 
865
1032
  import '../CnWidgetGrid/registerDashboardWidgets.js'
@@ -984,6 +1151,7 @@ export default {
984
1151
  ChevronLeft,
985
1152
  ChevronRight,
986
1153
  NcActionButton,
1154
+ NcActionCaption,
987
1155
  NcActionLink,
988
1156
  NcActionSeparator,
989
1157
  NcButton,
@@ -998,11 +1166,13 @@ export default {
998
1166
  CnActionsMenu,
999
1167
  CnBuildiqEditButton,
1000
1168
  CnLockedBanner,
1169
+ CnNextStepCard,
1001
1170
  CnDetailWidgetHost,
1002
1171
  CnFormDialog,
1003
1172
  CnDashboardGrid,
1004
1173
  CnLifecycleActions,
1005
1174
  CnActionButtons,
1175
+ CnStatusBadge,
1006
1176
  CnSummaryAggregates,
1007
1177
  CnRelatedCollections,
1008
1178
  CnBodySections,
@@ -1811,6 +1981,130 @@ export default {
1811
1981
  type: Boolean,
1812
1982
  default: false,
1813
1983
  },
1984
+
1985
+ // ── The action model (detail-action-model-and-case-surfaces) ────
1986
+
1987
+ /**
1988
+ * The primary action per stage (manifest `config.primaryActionByStage`):
1989
+ * a map from a stage value to an action. When the record's stage has
1990
+ * an entry, that action is the page's primary button, so the button
1991
+ * names the next step: "Take on" on a new case, "Publish" on a
1992
+ * decided one. An entry is an action object in the `headerActions`
1993
+ * shape, or the id of a declared header action. A stage without an
1994
+ * entry falls back to `primaryAction`.
1995
+ *
1996
+ * @type {Record<string, string|object>|null}
1997
+ */
1998
+ primaryActionByStage: {
1999
+ type: Object,
2000
+ default: null,
2001
+ },
2002
+
2003
+ /**
2004
+ * Dot-path to the field that holds the record's stage. Read by
2005
+ * `primaryActionByStage` and `nextStep`.
2006
+ *
2007
+ * @type {string}
2008
+ */
2009
+ stageField: {
2010
+ type: String,
2011
+ default: 'status',
2012
+ },
2013
+
2014
+ /**
2015
+ * Always visible buttons in the header (manifest `config.quickActions`)
2016
+ * for the few actions a handler uses on every record. At most three
2017
+ * render; later entries are dropped. An entry is an action object in
2018
+ * the `headerActions` shape, or the id of a declared header action,
2019
+ * which then leaves the Actions menu.
2020
+ *
2021
+ * @type {Array<string|object>}
2022
+ */
2023
+ quickActions: {
2024
+ type: Array,
2025
+ default: () => [],
2026
+ },
2027
+
2028
+ /**
2029
+ * What the Actions menu itself shows (manifest `config.actionsMenu`):
2030
+ * `{ showRefresh?, showHelpLinks?, label? }`. `showRefresh: false`
2031
+ * removes Refresh. `showHelpLinks: false` removes Request a feature,
2032
+ * Report a bug and Documentation, for an app that offers them in its
2033
+ * own help menu. `label` names the menu, for example "More". `null`
2034
+ * (the default) changes nothing.
2035
+ *
2036
+ * @type {{showRefresh?: boolean, showHelpLinks?: boolean, label?: string}|null}
2037
+ */
2038
+ actionsMenu: {
2039
+ type: Object,
2040
+ default: null,
2041
+ },
2042
+
2043
+ /**
2044
+ * The "what now" card (manifest `config.nextStep`):
2045
+ * `{ field?, stages: { <stage>: { title?, checklist, after? } } }`.
2046
+ * Each checklist item is `{ label, doneField?, doneWhen?, hint? }`.
2047
+ * Rendered above the body for a stage that declares a checklist, with
2048
+ * the primary button inside it. See `CnNextStepCard`.
2049
+ *
2050
+ * @type {object|null}
2051
+ */
2052
+ nextStep: {
2053
+ type: Object,
2054
+ default: null,
2055
+ },
2056
+
2057
+ /**
2058
+ * A pill above the title saying what kind of record this is
2059
+ * (manifest `config.typePill`): `{ field, colorMap?, labels?, variant? }`.
2060
+ * Rendered through `CnStatusBadge`; `colorMap` is keyed on the field's
2061
+ * raw value, `labels` maps a raw value to the text to show.
2062
+ *
2063
+ * @type {{field: string, colorMap?: object, labels?: object, variant?: string}|null}
2064
+ */
2065
+ typePill: {
2066
+ type: Object,
2067
+ default: null,
2068
+ },
2069
+
2070
+ /**
2071
+ * A pill above the title saying where the record stands (manifest
2072
+ * `config.statusPill`). Same shape as `typePill`.
2073
+ *
2074
+ * @type {{field: string, colorMap?: object, labels?: object, variant?: string}|null}
2075
+ */
2076
+ statusPill: {
2077
+ type: Object,
2078
+ default: null,
2079
+ },
2080
+
2081
+ /**
2082
+ * A column of cards beside the body (manifest `config.sideColumn`),
2083
+ * for the record's facts: deadline, requester, handler, links. Each
2084
+ * entry is a widget definition (`{ id?, type, title?, content? }`) or
2085
+ * the id of a widget declared in `widgets`. The column stacks under
2086
+ * the body on a narrow page. Empty (the default) leaves the layout as
2087
+ * it is.
2088
+ *
2089
+ * @type {Array<string|object>}
2090
+ */
2091
+ sideColumn: {
2092
+ type: Array,
2093
+ default: () => [],
2094
+ },
2095
+
2096
+ /**
2097
+ * Whether the current user administers this instance. Shows the
2098
+ * `adminOnly` header actions. `null` (the default) reads it from
2099
+ * `getCurrentUser()`. Visibility only, never an authorization
2100
+ * decision: the endpoint behind an action decides who may call it.
2101
+ *
2102
+ * @type {boolean|null}
2103
+ */
2104
+ isAdmin: {
2105
+ type: Boolean,
2106
+ default: null,
2107
+ },
1814
2108
  },
1815
2109
 
1816
2110
  emits: [
@@ -2078,7 +2372,7 @@ export default {
2078
2372
  * @spec openspec/changes/case-page-and-list-as-a-place/specs/index-page/spec.md
2079
2373
  */
2080
2374
  primaryActionAnchorId() {
2081
- const scope = this.primaryAction?.id || this.pageId || this.objectId || 'record'
2375
+ const scope = this.stagePrimaryEntry?.id || this.primaryAction?.id || this.pageId || this.objectId || 'record'
2082
2376
  return `cn-primary-action-${String(scope).replace(/[^A-Za-z0-9_-]/g, '-')}`
2083
2377
  },
2084
2378
 
@@ -2093,7 +2387,291 @@ export default {
2093
2387
  * @spec openspec/changes/case-page-and-list-as-a-place/specs/index-page/spec.md
2094
2388
  */
2095
2389
  skipLinkLabel() {
2096
- return `${this.effectiveTranslate('Skip to')} ${this.primaryActionLabel}`
2390
+ return `${this.effectiveTranslate('Skip to')} ${this.effectivePrimaryLabel}`
2391
+ },
2392
+
2393
+ // ── The action model (detail-action-model-and-case-surfaces) ────
2394
+
2395
+ /**
2396
+ * The record's stage, as `primaryActionByStage` and `nextStep` key it.
2397
+ *
2398
+ * @return {string} The stage, or '' while the record has none.
2399
+ * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-primary-action-follows-the-stage
2400
+ */
2401
+ recordStage() {
2402
+ return stageOf(this.currentObject, this.stageField)
2403
+ },
2404
+
2405
+ /**
2406
+ * The stage's primary action as an id, plus its definition when the
2407
+ * map carries one inline.
2408
+ *
2409
+ * @return {{id: string, inline: object|null}|null} The pinned action, or null when this stage declares none.
2410
+ * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-primary-action-follows-the-stage
2411
+ */
2412
+ stagePrimary() {
2413
+ return normalisePinnedAction(stageEntry(this.primaryActionByStage, this.recordStage), 'cn-stage-primary')
2414
+ },
2415
+
2416
+ /**
2417
+ * The quick actions as ids, plus definitions for the inline ones.
2418
+ * Capped at three, and never the action that is already the primary
2419
+ * button.
2420
+ *
2421
+ * @return {Array<{id: string, inline: object|null}>} The pinned actions.
2422
+ * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-quick-actions
2423
+ */
2424
+ pinnedQuickActions() {
2425
+ const primaryId = this.stagePrimary?.id
2426
+ return (Array.isArray(this.quickActions) ? this.quickActions : [])
2427
+ .map((entry, index) => normalisePinnedAction(entry, `cn-quick-${index}`))
2428
+ .filter((pinned) => pinned && pinned.id !== primaryId)
2429
+ .slice(0, MAX_QUICK_ACTIONS)
2430
+ },
2431
+
2432
+ /**
2433
+ * The stage's primary action in dispatchable form: the menu entry
2434
+ * CnActionButtons built for it, with its `run()`. Null while the
2435
+ * action is hidden by its own `visibleWhen`, in which case the page
2436
+ * falls back to `primaryAction`.
2437
+ *
2438
+ * @return {object|null} The entry, or null.
2439
+ * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-primary-action-follows-the-stage
2440
+ */
2441
+ stagePrimaryEntry() {
2442
+ const id = this.stagePrimary?.id
2443
+ return (id && this.menuHeaderActions.find((entry) => entry.id === id)) || null
2444
+ },
2445
+
2446
+ /**
2447
+ * The quick actions in dispatchable form, in declaration order.
2448
+ *
2449
+ * @return {Array<object>} The entries that are visible.
2450
+ * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-quick-actions
2451
+ */
2452
+ quickActionEntries() {
2453
+ return this.pinnedQuickActions
2454
+ .map((pinned) => this.menuHeaderActions.find((entry) => entry.id === pinned.id))
2455
+ .filter(Boolean)
2456
+ },
2457
+
2458
+ /**
2459
+ * What the primary button says: the stage's action when the stage
2460
+ * declares one, the page's `primaryAction` otherwise. Empty gates
2461
+ * both the button and the skip link.
2462
+ *
2463
+ * @return {string} The label, or an empty string.
2464
+ * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-primary-action-follows-the-stage
2465
+ */
2466
+ effectivePrimaryLabel() {
2467
+ return this.stagePrimaryEntry ? this.stagePrimaryEntry.label : this.primaryActionLabel
2468
+ },
2469
+
2470
+ /**
2471
+ * The URL the primary button links to, when its action is a link.
2472
+ *
2473
+ * @return {string} The href, or '' for an action that is dispatched.
2474
+ * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-primary-action-follows-the-stage
2475
+ */
2476
+ primaryActionHref() {
2477
+ return this.stagePrimaryEntry?.href || ''
2478
+ },
2479
+
2480
+ /**
2481
+ * The link target that goes with `primaryActionHref`.
2482
+ *
2483
+ * @return {string|undefined} The target, or undefined.
2484
+ * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-primary-action-follows-the-stage
2485
+ */
2486
+ primaryActionTarget() {
2487
+ return this.stagePrimaryEntry?.linkTarget || undefined
2488
+ },
2489
+
2490
+ /**
2491
+ * Whether the primary button waits for its own request.
2492
+ *
2493
+ * @return {boolean} True while the stage's action is in flight.
2494
+ * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-primary-action-follows-the-stage
2495
+ */
2496
+ primaryActionDisabled() {
2497
+ return Boolean(this.stagePrimaryEntry?.disabled)
2498
+ },
2499
+
2500
+ /**
2501
+ * Whether the viewer administers this instance, for `adminOnly`
2502
+ * actions. The prop wins; otherwise Nextcloud says.
2503
+ *
2504
+ * @return {boolean} True for an instance admin.
2505
+ * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-grouped-and-admin-only-menu-actions
2506
+ */
2507
+ viewerIsAdmin() {
2508
+ if (this.isAdmin !== null) {
2509
+ return this.isAdmin
2510
+ }
2511
+ try {
2512
+ return getCurrentUser()?.isAdmin === true
2513
+ } catch {
2514
+ return false
2515
+ }
2516
+ },
2517
+
2518
+ /**
2519
+ * The declared actions by id, for the `group` and `adminOnly` a menu
2520
+ * entry does not carry itself.
2521
+ *
2522
+ * @return {Record<string, object>} The lookup.
2523
+ * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-grouped-and-admin-only-menu-actions
2524
+ */
2525
+ headerActionsById() {
2526
+ return Object.fromEntries(this.effectiveHeaderActions
2527
+ .filter((action) => action && action.id)
2528
+ .map((action) => [action.id, action]))
2529
+ },
2530
+
2531
+ /**
2532
+ * The menu entries in the groups the menu draws: ungrouped first,
2533
+ * named groups in first appearance order, admin actions last and for
2534
+ * admins only. A page that groups nothing gets one captionless group.
2535
+ *
2536
+ * @return {Array<{key: string, label: string, entries: Array<object>}>} The groups.
2537
+ * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-grouped-and-admin-only-menu-actions
2538
+ */
2539
+ headerMenuGroups() {
2540
+ const groups = groupMenuEntries(this.headerMenuEntries, this.headerActionsById, {
2541
+ isAdmin: this.viewerIsAdmin,
2542
+ adminLabel: t('nextcloud-vue', 'Administration'),
2543
+ })
2544
+ return groups.map((group) => ({ ...group, label: group.label ? this.effectiveTranslate(group.label) : '' }))
2545
+ },
2546
+
2547
+ /**
2548
+ * Whether Refresh renders in the Actions menu.
2549
+ *
2550
+ * @return {boolean} The existing rule, unless `actionsMenu.showRefresh` is false.
2551
+ * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-actions-menu-built-ins-are-optional
2552
+ */
2553
+ menuShowsRefresh() {
2554
+ return this.actionsMenu?.showRefresh === false ? false : this.effectiveHeaderShowRefresh
2555
+ },
2556
+
2557
+ /**
2558
+ * Whether the help links render in the Actions menu.
2559
+ *
2560
+ * @return {boolean} True unless `actionsMenu.showHelpLinks` is false.
2561
+ * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-actions-menu-built-ins-are-optional
2562
+ */
2563
+ menuShowsHelpLinks() {
2564
+ return this.actionsMenu?.showHelpLinks !== false
2565
+ },
2566
+
2567
+ /**
2568
+ * The Actions menu's name.
2569
+ *
2570
+ * @return {string|undefined} `actionsMenu.label` translated, or undefined for the default "Actions".
2571
+ * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-actions-menu-built-ins-are-optional
2572
+ */
2573
+ actionsMenuName() {
2574
+ const label = this.actionsMenu?.label
2575
+ return typeof label === 'string' && label !== '' ? this.effectiveTranslate(label) : undefined
2576
+ },
2577
+
2578
+ /**
2579
+ * Whether the page is showing its body: not the not-found, loading,
2580
+ * error or empty state. The "what now" card and the side column are
2581
+ * siblings of the body and follow it.
2582
+ *
2583
+ * @return {boolean} True when the body renders.
2584
+ * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-next-step-card
2585
+ */
2586
+ showsBody() {
2587
+ return !this.objectNotFound && !this.showLoadingState && !this.error && !this.empty
2588
+ },
2589
+
2590
+ /**
2591
+ * The "what now" card for the record's stage, labels translated.
2592
+ *
2593
+ * @return {{stage: string, title: string, items: Array<object>, after: string}|null} The card, or null when the stage declares no checklist.
2594
+ * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-next-step-card
2595
+ */
2596
+ nextStepCard() {
2597
+ const card = resolveNextStep(this.nextStep, this.currentObject, this.stageField)
2598
+ if (!card) {
2599
+ return null
2600
+ }
2601
+ const tr = (text) => (text ? this.effectiveTranslate(text) : '')
2602
+ return {
2603
+ ...card,
2604
+ title: tr(card.title),
2605
+ after: tr(card.after),
2606
+ items: card.items.map((item) => ({ ...item, label: tr(item.label), hint: tr(item.hint) })),
2607
+ }
2608
+ },
2609
+
2610
+ /**
2611
+ * Whether the "what now" card renders.
2612
+ *
2613
+ * @return {boolean} True when the stage has a checklist and the body is showing.
2614
+ * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-next-step-card
2615
+ */
2616
+ showsNextStepCard() {
2617
+ return Boolean(this.nextStepCard) && this.showsBody
2618
+ },
2619
+
2620
+ /**
2621
+ * The pills above the title: type first, then status.
2622
+ *
2623
+ * @return {Array<{key: string, label: string, colorKey: string, colorMap: object|null, variant: string}>} The pills whose field has a value.
2624
+ * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-header-pills
2625
+ */
2626
+ headerPills() {
2627
+ return [['type', this.typePill], ['status', this.statusPill]]
2628
+ .map(([key, config]) => {
2629
+ const pill = resolvePill(config, this.resolvedObject)
2630
+ return pill ? { ...pill, key, label: this.effectiveTranslate(pill.label) } : null
2631
+ })
2632
+ .filter(Boolean)
2633
+ },
2634
+
2635
+ /**
2636
+ * The side column's widget definitions. A string names a widget
2637
+ * declared in `widgets`; an object is a definition of its own.
2638
+ *
2639
+ * @return {Array<object>} The definitions that resolve, each with an `id`.
2640
+ * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-side-column
2641
+ */
2642
+ sideColumnWidgets() {
2643
+ const declared = Array.isArray(this.widgets) ? this.widgets : []
2644
+ return (Array.isArray(this.sideColumn) ? this.sideColumn : [])
2645
+ .map((entry, index) => {
2646
+ if (typeof entry === 'string') {
2647
+ return declared.find((widget) => widget && widget.id === entry) || null
2648
+ }
2649
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
2650
+ return null
2651
+ }
2652
+ return entry.id ? entry : { ...entry, id: `cn-side-${index}` }
2653
+ })
2654
+ .filter(Boolean)
2655
+ },
2656
+
2657
+ /**
2658
+ * Whether the side column renders.
2659
+ *
2660
+ * @return {boolean} True when it has widgets and the body is showing.
2661
+ * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-side-column
2662
+ */
2663
+ showsSideColumn() {
2664
+ return this.sideColumnWidgets.length > 0 && this.showsBody
2665
+ },
2666
+
2667
+ /**
2668
+ * Accessible name of the side column.
2669
+ *
2670
+ * @return {string} The label.
2671
+ * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-side-column
2672
+ */
2673
+ sideColumnLabel() {
2674
+ return t('nextcloud-vue', 'Details')
2097
2675
  },
2098
2676
 
2099
2677
  /**
@@ -2297,7 +2875,16 @@ export default {
2297
2875
  */
2298
2876
  headerMenuEntries() {
2299
2877
  const lifecycle = this.showsLifecycleActions ? this.lifecycleMenuEntries : []
2300
- return [...lifecycle, ...this.menuHeaderActions]
2878
+ // An action drawn as a button, the stage's primary one or a quick
2879
+ // action, is not listed a second time in the menu.
2880
+ const pinned = new Set([
2881
+ ...(this.stagePrimaryEntry ? [this.stagePrimaryEntry.id] : []),
2882
+ ...this.quickActionEntries.map((entry) => entry.id),
2883
+ ])
2884
+ const inMenu = pinned.size > 0
2885
+ ? this.menuHeaderActions.filter((entry) => !pinned.has(entry.id))
2886
+ : this.menuHeaderActions
2887
+ return [...lifecycle, ...inMenu]
2301
2888
  },
2302
2889
 
2303
2890
  /**
@@ -2319,7 +2906,17 @@ export default {
2319
2906
  * @return {Array<object>} The action descriptors.
2320
2907
  */
2321
2908
  effectiveHeaderActions() {
2322
- const declared = Array.isArray(this.headerActions) ? this.headerActions : []
2909
+ const authored = Array.isArray(this.headerActions) ? this.headerActions : []
2910
+ // Inline pinned actions (a stage's primary action, a quick action
2911
+ // written out in place) join the list CnActionButtons dispatches,
2912
+ // so they get the same dialogs, toasts and `visibleWhen` gating as
2913
+ // any header action. An id that is already declared is not added
2914
+ // twice. With nothing pinned inline this is `headerActions` itself.
2915
+ const known = new Set(authored.map((action) => action && action.id))
2916
+ const inline = [this.stagePrimary, ...this.pinnedQuickActions]
2917
+ .filter((pinned) => pinned && pinned.inline && !known.has(pinned.id))
2918
+ .map((pinned) => pinned.inline)
2919
+ const declared = inline.length > 0 ? [...authored, ...inline] : authored
2323
2920
  if (!(this.foldsEditIntoActions && this.canEditRecord)) {
2324
2921
  return declared
2325
2922
  }
@@ -3132,7 +3729,11 @@ export default {
3132
3729
  * @spec openspec/changes/case-page-and-list-as-a-place/specs/index-page/spec.md
3133
3730
  */
3134
3731
  onSkipToPrimaryAction(event) {
3135
- const target = this.$refs.primaryActionButton?.$el ?? this.$refs.primaryActionButton
3732
+ // The ref covers the header button; the id also finds the button
3733
+ // when it sits inside the "what now" card.
3734
+ const target = this.$refs.primaryActionButton?.$el
3735
+ ?? this.$refs.primaryActionButton
3736
+ ?? this.$el?.querySelector?.(`[id="${this.primaryActionAnchorId}"]`)
3136
3737
  const focusable = target?.matches?.('button, a') ? target : target?.querySelector?.('button, a')
3137
3738
  if (focusable?.focus) {
3138
3739
  event.preventDefault()
@@ -3140,6 +3741,43 @@ export default {
3140
3741
  }
3141
3742
  },
3142
3743
 
3744
+ /**
3745
+ * The primary button was pressed.
3746
+ *
3747
+ * A stage's action is dispatched the way a header action is, through
3748
+ * the `run()` CnActionButtons bound for it, and announced to the host
3749
+ * with its declaration. A link action is left to the browser. Without
3750
+ * a stage action this is the page's `primaryAction`, announced as
3751
+ * before.
3752
+ *
3753
+ * @return {void}
3754
+ * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-primary-action-follows-the-stage
3755
+ */
3756
+ onPrimaryActionClick() {
3757
+ const entry = this.stagePrimaryEntry
3758
+ if (!entry) {
3759
+ this.$emit('primary-action', this.primaryAction)
3760
+ return
3761
+ }
3762
+ this.$emit('primary-action', this.headerActionsById[entry.id] || { id: entry.id, label: entry.label })
3763
+ this.onPinnedActionClick(entry)
3764
+ },
3765
+
3766
+ /**
3767
+ * A pinned action's button was pressed: run it, unless it is a link
3768
+ * the browser is already following.
3769
+ *
3770
+ * @param {object} entry The action's menu entry.
3771
+ * @return {void}
3772
+ * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-quick-actions
3773
+ */
3774
+ onPinnedActionClick(entry) {
3775
+ if (!entry || entry.href || typeof entry.run !== 'function') {
3776
+ return
3777
+ }
3778
+ entry.run()
3779
+ },
3780
+
3143
3781
  /**
3144
3782
  * The active tab changed: tell the host, and put it in the address
3145
3783
  * when the page declares that its tab is part of its address.