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