@conduction/nextcloud-vue 2.80.0 → 2.81.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (290) hide show
  1. package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js.map +1 -1
  2. package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js +3 -3
  3. package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js.map +1 -1
  4. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js +80 -4
  5. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
  6. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js +95 -91
  7. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js.map +1 -1
  8. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js +140 -10
  9. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js.map +1 -1
  10. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue3.js +52 -16
  11. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue3.js.map +1 -1
  12. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js +17 -3
  13. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js.map +1 -1
  14. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js +63 -52
  15. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js.map +1 -1
  16. package/dist/esm/components/CnChipsFieldWidget/CnChipsFieldWidget.vue.js +10 -0
  17. package/dist/esm/components/CnChipsFieldWidget/CnChipsFieldWidget.vue.js.map +1 -0
  18. package/dist/esm/components/CnChipsFieldWidget/CnChipsFieldWidget.vue2.js +252 -0
  19. package/dist/esm/components/CnChipsFieldWidget/CnChipsFieldWidget.vue2.js.map +1 -0
  20. package/dist/esm/components/CnChipsFieldWidget/CnChipsFieldWidget.vue3.js +140 -0
  21. package/dist/esm/components/CnChipsFieldWidget/CnChipsFieldWidget.vue3.js.map +1 -0
  22. package/dist/esm/components/CnChipsFieldWidget/index.js +26 -0
  23. package/dist/esm/components/CnChipsFieldWidget/index.js.map +1 -0
  24. package/dist/esm/components/CnChoicePickerWidget/CnChoicePickerWidget.vue.js +10 -0
  25. package/dist/esm/components/CnChoicePickerWidget/CnChoicePickerWidget.vue.js.map +1 -0
  26. package/dist/esm/components/CnChoicePickerWidget/CnChoicePickerWidget.vue2.js +354 -0
  27. package/dist/esm/components/CnChoicePickerWidget/CnChoicePickerWidget.vue2.js.map +1 -0
  28. package/dist/esm/components/CnChoicePickerWidget/CnChoicePickerWidget.vue3.js +172 -0
  29. package/dist/esm/components/CnChoicePickerWidget/CnChoicePickerWidget.vue3.js.map +1 -0
  30. package/dist/esm/components/CnChoicePickerWidget/index.js +27 -0
  31. package/dist/esm/components/CnChoicePickerWidget/index.js.map +1 -0
  32. package/dist/esm/components/CnConversationThread/CnConversationWidget.vue2.js +1 -1
  33. package/dist/esm/components/CnConversationThread/CnConversationWidget.vue2.js.map +1 -1
  34. package/dist/esm/components/CnConversationThread/CnConversationWidget.vue3.js.map +1 -1
  35. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue2.js +2 -2
  36. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue2.js.map +1 -1
  37. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue3.js.map +1 -1
  38. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +65 -5
  39. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  40. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +2 -1
  41. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  42. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +19 -11
  43. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
  44. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +3 -2
  45. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
  46. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +72 -0
  47. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  48. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +21 -4
  49. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  50. package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewWidget.vue2.js +1 -1
  51. package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewWidget.vue2.js.map +1 -1
  52. package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewWidget.vue3.js.map +1 -1
  53. package/dist/esm/components/CnFacetSidebar/CnFacetSidebar.vue2.js +7 -7
  54. package/dist/esm/components/CnFacetSidebar/CnFacetSidebar.vue2.js.map +1 -1
  55. package/dist/esm/components/CnFacetSidebar/CnFacetSidebar.vue3.js.map +1 -1
  56. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js +131 -0
  57. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js.map +1 -1
  58. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js.map +1 -1
  59. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js +80 -5
  60. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js.map +1 -1
  61. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js +282 -69
  62. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js.map +1 -1
  63. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js +302 -4
  64. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js.map +1 -1
  65. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js +417 -244
  66. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js.map +1 -1
  67. package/dist/esm/components/CnFilesBrowser/filesBrowser.js +101 -1
  68. package/dist/esm/components/CnFilesBrowser/filesBrowser.js.map +1 -1
  69. package/dist/esm/components/CnFilesBrowser/filesBrowserColumns.js +141 -1
  70. package/dist/esm/components/CnFilesBrowser/filesBrowserColumns.js.map +1 -1
  71. package/dist/esm/components/CnFormPage/CnFormPage.vue2.js +60 -0
  72. package/dist/esm/components/CnFormPage/CnFormPage.vue2.js.map +1 -1
  73. package/dist/esm/components/CnFormPage/CnFormPage.vue3.js +74 -49
  74. package/dist/esm/components/CnFormPage/CnFormPage.vue3.js.map +1 -1
  75. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +29 -2
  76. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  77. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +48 -4
  78. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  79. package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue2.js +7 -7
  80. package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue2.js.map +1 -1
  81. package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue3.js.map +1 -1
  82. package/dist/esm/components/CnNextStepCard/CnNextStepWidget.vue2.js +1 -1
  83. package/dist/esm/components/CnNextStepCard/CnNextStepWidget.vue2.js.map +1 -1
  84. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +65 -2
  85. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
  86. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js +6 -2
  87. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js.map +1 -1
  88. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js +63 -1
  89. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js.map +1 -1
  90. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js +8 -1
  91. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js.map +1 -1
  92. package/dist/esm/components/CnObjectSidebar/CnNotesTab.vue2.js +14 -0
  93. package/dist/esm/components/CnObjectSidebar/CnNotesTab.vue2.js.map +1 -1
  94. package/dist/esm/components/CnObjectSidebar/CnNotesTab.vue3.js +5 -1
  95. package/dist/esm/components/CnObjectSidebar/CnNotesTab.vue3.js.map +1 -1
  96. package/dist/esm/components/CnRadioCardsWidget/CnRadioCardsWidget.vue.js +10 -0
  97. package/dist/esm/components/CnRadioCardsWidget/CnRadioCardsWidget.vue.js.map +1 -0
  98. package/dist/esm/components/CnRadioCardsWidget/CnRadioCardsWidget.vue2.js +174 -0
  99. package/dist/esm/components/CnRadioCardsWidget/CnRadioCardsWidget.vue2.js.map +1 -0
  100. package/dist/esm/components/CnRadioCardsWidget/CnRadioCardsWidget.vue3.js +75 -0
  101. package/dist/esm/components/CnRadioCardsWidget/CnRadioCardsWidget.vue3.js.map +1 -0
  102. package/dist/esm/components/CnRadioCardsWidget/index.js +28 -0
  103. package/dist/esm/components/CnRadioCardsWidget/index.js.map +1 -0
  104. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +30 -6
  105. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
  106. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js +34 -25
  107. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
  108. package/dist/esm/components/CnSavedViewsWidget/CnSavedViewsWidget.vue.js +10 -0
  109. package/dist/esm/components/CnSavedViewsWidget/CnSavedViewsWidget.vue.js.map +1 -0
  110. package/dist/esm/components/CnSavedViewsWidget/CnSavedViewsWidget.vue2.js +222 -0
  111. package/dist/esm/components/CnSavedViewsWidget/CnSavedViewsWidget.vue2.js.map +1 -0
  112. package/dist/esm/components/CnSavedViewsWidget/CnSavedViewsWidget.vue3.js +64 -0
  113. package/dist/esm/components/CnSavedViewsWidget/CnSavedViewsWidget.vue3.js.map +1 -0
  114. package/dist/esm/components/CnSavedViewsWidget/index.js +30 -0
  115. package/dist/esm/components/CnSavedViewsWidget/index.js.map +1 -0
  116. package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue2.js.map +1 -1
  117. package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue3.js.map +1 -1
  118. package/dist/esm/components/CnSettingsFormWidget/CnSettingsFormWidget.vue.js +10 -0
  119. package/dist/esm/components/CnSettingsFormWidget/CnSettingsFormWidget.vue.js.map +1 -0
  120. package/dist/esm/components/CnSettingsFormWidget/CnSettingsFormWidget.vue2.js +347 -0
  121. package/dist/esm/components/CnSettingsFormWidget/CnSettingsFormWidget.vue2.js.map +1 -0
  122. package/dist/esm/components/CnSettingsFormWidget/CnSettingsFormWidget.vue3.js +184 -0
  123. package/dist/esm/components/CnSettingsFormWidget/CnSettingsFormWidget.vue3.js.map +1 -0
  124. package/dist/esm/components/CnSettingsFormWidget/index.js +27 -0
  125. package/dist/esm/components/CnSettingsFormWidget/index.js.map +1 -0
  126. package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js +87 -6
  127. package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js.map +1 -1
  128. package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js.map +1 -1
  129. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue2.js +84 -1
  130. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue2.js.map +1 -1
  131. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue3.js +85 -41
  132. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue3.js.map +1 -1
  133. package/dist/esm/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue.js +10 -0
  134. package/dist/esm/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue.js.map +1 -0
  135. package/dist/esm/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue2.js +159 -0
  136. package/dist/esm/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue2.js.map +1 -0
  137. package/dist/esm/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue3.js +60 -0
  138. package/dist/esm/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue3.js.map +1 -0
  139. package/dist/esm/components/CnSwitchRowsWidget/index.js +26 -0
  140. package/dist/esm/components/CnSwitchRowsWidget/index.js.map +1 -0
  141. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +47 -1
  142. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
  143. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js +16 -14
  144. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
  145. package/dist/esm/components/CnTasksWidget/CnTasksWidget.vue2.js +80 -1
  146. package/dist/esm/components/CnTasksWidget/CnTasksWidget.vue2.js.map +1 -1
  147. package/dist/esm/components/CnTasksWidget/CnTasksWidget.vue3.js +75 -32
  148. package/dist/esm/components/CnTasksWidget/CnTasksWidget.vue3.js.map +1 -1
  149. package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue.js +10 -0
  150. package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue.js.map +1 -0
  151. package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue2.js +269 -0
  152. package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue2.js.map +1 -0
  153. package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue3.js +106 -0
  154. package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue3.js.map +1 -0
  155. package/dist/esm/components/CnToggleTableWidget/index.js +27 -0
  156. package/dist/esm/components/CnToggleTableWidget/index.js.map +1 -0
  157. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js +7 -0
  158. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js.map +1 -1
  159. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js +7 -2
  160. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js.map +1 -1
  161. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue3.js.map +1 -1
  162. package/dist/esm/components/CnWidgetObjectTable/dashboardRegistration.js +5 -0
  163. package/dist/esm/components/CnWidgetObjectTable/dashboardRegistration.js.map +1 -1
  164. package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue2.js +197 -6
  165. package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue2.js.map +1 -1
  166. package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue3.js +96 -50
  167. package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue3.js.map +1 -1
  168. package/dist/esm/composables/useListView.js +4 -4
  169. package/dist/esm/composables/useListView.js.map +1 -1
  170. package/dist/esm/index.js +17 -1
  171. package/dist/esm/index.js.map +1 -1
  172. package/dist/esm/l10n/en.json.js +9 -1
  173. package/dist/esm/l10n/en.json.js.map +1 -1
  174. package/dist/esm/l10n/nl.json.js +55 -1
  175. package/dist/esm/l10n/nl.json.js.map +1 -1
  176. package/dist/esm/nextcloud-vue.css +1041 -12
  177. package/dist/esm/utils/contentHeaderLink.js +51 -0
  178. package/dist/esm/utils/contentHeaderLink.js.map +1 -0
  179. package/dist/esm/utils/facets.js +75 -3
  180. package/dist/esm/utils/facets.js.map +1 -1
  181. package/dist/esm/utils/fetchFilterCounts.js +40 -5
  182. package/dist/esm/utils/fetchFilterCounts.js.map +1 -1
  183. package/dist/esm/utils/fillTemplate.js +7 -1
  184. package/dist/esm/utils/fillTemplate.js.map +1 -1
  185. package/dist/esm/utils/initials.js +32 -0
  186. package/dist/esm/utils/initials.js.map +1 -0
  187. package/dist/esm/utils/libraryWidgetKeys.js +7 -0
  188. package/dist/esm/utils/libraryWidgetKeys.js.map +1 -1
  189. package/dist/esm/utils/objectWrite.js +59 -0
  190. package/dist/esm/utils/objectWrite.js.map +1 -0
  191. package/dist/esm/utils/pageControls.js +81 -0
  192. package/dist/esm/utils/pageControls.js.map +1 -0
  193. package/dist/esm/utils/personalLenses.js +40 -3
  194. package/dist/esm/utils/personalLenses.js.map +1 -1
  195. package/dist/esm/utils/resolveFilterTokens.js +1 -1
  196. package/dist/esm/utils/resolveFilterTokens.js.map +1 -1
  197. package/dist/esm/utils/resolveManifestTokens.js +2 -0
  198. package/dist/esm/utils/resolveManifestTokens.js.map +1 -1
  199. package/dist/esm/utils/sentinelTokens.js +19 -1
  200. package/dist/esm/utils/sentinelTokens.js.map +1 -1
  201. package/dist/esm/utils/userGroupsPeek.js +91 -0
  202. package/dist/esm/utils/userGroupsPeek.js.map +1 -0
  203. package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
  204. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  205. package/dist/esm/utils/visibleWhen.js +57 -5
  206. package/dist/esm/utils/visibleWhen.js.map +1 -1
  207. package/dist/esm/utils/widgetDispatch.js +1 -1
  208. package/dist/esm/utils/widgetDispatch.js.map +1 -1
  209. package/dist/esm/utils/widgetVisibility.js +4 -57
  210. package/dist/esm/utils/widgetVisibility.js.map +1 -1
  211. package/dist/nextcloud-vue.cjs.js +13589 -7833
  212. package/dist/nextcloud-vue.cjs.js.map +2 -2
  213. package/dist/nextcloud-vue.css +1041 -12
  214. package/l10n/en.json +9 -1
  215. package/l10n/nl.json +55 -1
  216. package/package.json +2 -1
  217. package/scripts/check-docs-mdx.mjs +108 -0
  218. package/src/components/CnActionsBar/CnActionsBar.vue +6 -1
  219. package/src/components/CnAppRoot/CnAppRoot.md +4 -2
  220. package/src/components/CnAppRoot/CnAppRoot.vue +151 -59
  221. package/src/components/CnBannerWidget/CnBannerWidget.vue +205 -20
  222. package/src/components/CnCellRenderer/CnCellRenderer.md +2 -0
  223. package/src/components/CnCellRenderer/CnCellRenderer.vue +47 -4
  224. package/src/components/CnChipsFieldWidget/CnChipsFieldWidget.vue +433 -0
  225. package/src/components/CnChipsFieldWidget/index.js +25 -0
  226. package/src/components/CnChoicePickerWidget/CnChoicePickerWidget.vue +556 -0
  227. package/src/components/CnChoicePickerWidget/index.js +26 -0
  228. package/src/components/CnConversationThread/CnConversationWidget.vue +1 -1
  229. package/src/components/CnDashboardGrid/CnDashboardGrid.vue +2 -2
  230. package/src/components/CnDashboardPage/CnDashboardPage.vue +78 -5
  231. package/src/components/CnDataTable/CnDataTable.vue +21 -12
  232. package/src/components/CnDetailPage/CnDetailPage.vue +89 -3
  233. package/src/components/CnDocumentReviewList/CnDocumentReviewWidget.vue +1 -1
  234. package/src/components/CnFacetSidebar/CnFacetSidebar.vue +7 -7
  235. package/src/components/CnFactsWidget/CnFactsWidget.vue +142 -2
  236. package/src/components/CnFieldListWidget/CnFieldListWidget.vue +268 -6
  237. package/src/components/CnFilesBrowser/CnFilesBrowser.vue +588 -117
  238. package/src/components/CnFilesBrowser/filesBrowser.js +100 -0
  239. package/src/components/CnFilesBrowser/filesBrowserColumns.js +140 -0
  240. package/src/components/CnFormPage/CnFormPage.vue +81 -0
  241. package/src/components/CnIndexPage/CnIndexPage.vue +56 -4
  242. package/src/components/CnIndexSidebar/CnIndexSidebar.vue +7 -7
  243. package/src/components/CnNextStepCard/CnNextStepWidget.vue +1 -1
  244. package/src/components/CnObjectListWidget/CnObjectListWidget.vue +69 -2
  245. package/src/components/CnObjectSidebar/CnFilesTab.vue +70 -1
  246. package/src/components/CnObjectSidebar/CnNotesTab.vue +16 -0
  247. package/src/components/CnRadioCardsWidget/CnRadioCardsWidget.vue +313 -0
  248. package/src/components/CnRadioCardsWidget/index.js +27 -0
  249. package/src/components/CnRecordListWidget/CnRecordListWidget.vue +55 -6
  250. package/src/components/CnSavedViewsWidget/CnSavedViewsWidget.vue +296 -0
  251. package/src/components/CnSavedViewsWidget/index.js +29 -0
  252. package/src/components/CnSegmentedControl/CnSegmentedControl.vue +13 -0
  253. package/src/components/CnSettingsFormWidget/CnSettingsFormWidget.vue +555 -0
  254. package/src/components/CnSettingsFormWidget/index.js +26 -0
  255. package/src/components/CnStatWidget/CnStatWidget.vue +87 -6
  256. package/src/components/CnStatsBlockWidget/CnStatsBlockWidget.vue +180 -3
  257. package/src/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue +226 -0
  258. package/src/components/CnSwitchRowsWidget/index.js +25 -0
  259. package/src/components/CnTabsWidget/CnTabsWidget.vue +82 -3
  260. package/src/components/CnTasksWidget/CnTasksWidget.vue +175 -15
  261. package/src/components/CnToggleTableWidget/CnToggleTableWidget.vue +386 -0
  262. package/src/components/CnToggleTableWidget/index.js +26 -0
  263. package/src/components/CnWidgetGrid/registerDashboardWidgets.js +7 -0
  264. package/src/components/CnWidgetObjectTable/CnWidgetObjectTable.vue +7 -2
  265. package/src/components/CnWidgetObjectTable/dashboardRegistration.js +5 -0
  266. package/src/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue +309 -11
  267. package/src/components/index.js +15 -0
  268. package/src/composables/useListView.js +4 -4
  269. package/src/css/look-board-detail.css +36 -2
  270. package/src/css/look-board-index.css +39 -8
  271. package/src/css/look-board.css +72 -0
  272. package/src/index.js +15 -0
  273. package/src/schemas/app-manifest-v2.schema.json +93 -9
  274. package/src/utils/contentHeaderLink.js +48 -0
  275. package/src/utils/facets.js +74 -2
  276. package/src/utils/fetchFilterCounts.js +39 -4
  277. package/src/utils/fillTemplate.js +7 -1
  278. package/src/utils/initials.js +29 -0
  279. package/src/utils/libraryWidgetKeys.js +7 -0
  280. package/src/utils/objectWrite.js +55 -0
  281. package/src/utils/pageControls.js +78 -0
  282. package/src/utils/personalLenses.js +39 -2
  283. package/src/utils/resolveFilterTokens.js +3 -1
  284. package/src/utils/resolveManifestTokens.js +2 -0
  285. package/src/utils/sentinelTokens.js +19 -1
  286. package/src/utils/userGroupsPeek.js +87 -0
  287. package/src/utils/validateManifestV2.compiled.js +3 -3
  288. package/src/utils/visibleWhen.js +56 -4
  289. package/src/utils/widgetDispatch.js +1 -1
  290. package/src/utils/widgetVisibility.js +7 -56
@@ -163,6 +163,7 @@ import widgetLink from '../../mixins/widgetLink.js'
163
163
  import { useObjectStore } from '../../store/useObjectStore.js'
164
164
  import { resolveObjectOpType } from '../../utils/actionsDispatcher.js'
165
165
  import { resolveObjectTokenContext } from '../../utils/detailObjectContext.js'
166
+ import { fetchListTotal, needsListEndpoint } from '../../utils/fetchFilterCounts.js'
166
167
  import { formatMetricValue, unwrapAppConfig } from '../../utils/formatMetric.js'
167
168
  import { dropOptionalUnresolved, resolveFilterTokens } from '../../utils/resolveFilterTokens.js'
168
169
  import { STATUS_TEXT_COLORS } from '../../utils/statusColors.js'
@@ -1746,8 +1747,7 @@ export default {
1746
1747
  // drop any optional `@workspace.<key>?` / `@config.<key>?` that stayed
1747
1748
  // unresolved so an unset value omits the filter (show all) instead of
1748
1749
  // sending a literal token.
1749
- const ctx = { ...(this.objectCtx || {}), workspace: this.pageCtx, config: this.configCtx }
1750
- filter = dropOptionalUnresolved(resolveFilterTokens(filter, ctx))
1750
+ filter = this.resolveSourceFilter(filter)
1751
1751
  for (const [k, v] of Object.entries(filter)) {
1752
1752
  if (v && typeof v === 'object') {
1753
1753
  for (const [op, ov] of Object.entries(v)) {
@@ -1759,6 +1759,21 @@ export default {
1759
1759
  }
1760
1760
  },
1761
1761
 
1762
+ /**
1763
+ * A source filter with its @-tokens resolved against the object, page
1764
+ * and config contexts, and unresolved optional clauses dropped.
1765
+ *
1766
+ * @param {object} filter The raw filter map.
1767
+ * @return {object} The resolved filter.
1768
+ */
1769
+ resolveSourceFilter(filter) {
1770
+ if (!filter || typeof filter !== 'object') {
1771
+ return {}
1772
+ }
1773
+ const ctx = { ...(this.objectCtx || {}), workspace: this.pageCtx, config: this.configCtx }
1774
+ return dropOptionalUnresolved(resolveFilterTokens(filter, ctx))
1775
+ },
1776
+
1762
1777
  /**
1763
1778
  * Fetch one scalar from the OpenRegister `/value` aggregation endpoint.
1764
1779
  *
@@ -1768,9 +1783,19 @@ export default {
1768
1783
  * @param {string} metric The aggregation metric.
1769
1784
  * @param {?string} field The numeric field (non-count metrics).
1770
1785
  * @param {object} filter The filter map.
1786
+ * @param {string} [via] `"list"` counts through the list endpoint's total.
1771
1787
  * @return {Promise<number|null>} The aggregated value.
1772
1788
  */
1773
- async fetchAggregate(axios, generateUrl, s, metric, field, filter) {
1789
+ async fetchAggregate(axios, generateUrl, s, metric, field, filter, via) {
1790
+ // A count the aggregation endpoint cannot express (a filter holding a
1791
+ // list of values, a test for no value (`null` or `{ isnull: true }`),
1792
+ // or `via: "list"`) reads the list endpoint's total instead, because
1793
+ // the aggregation endpoint has no null operator.
1794
+ const resolved = this.resolveSourceFilter(filter)
1795
+ const listed = Object.values(resolved).some((v) => Array.isArray(v) || (v && typeof v === 'object' && Object.values(v).some(Array.isArray)))
1796
+ if ((!metric || metric === 'count') && (via === 'list' || listed || needsListEndpoint(resolved))) {
1797
+ return fetchListTotal(s.register, s.schema, resolved, {})
1798
+ }
1774
1799
  const url = generateUrl(
1775
1800
  '/apps/openregister/api/objects/aggregations/{register}/{schema}/value',
1776
1801
  { register: s.register, schema: s.schema },
@@ -1827,9 +1852,15 @@ export default {
1827
1852
  if (s.kind === 'endpoint') {
1828
1853
  this.value = await this.fetchEndpoint(s, refresh)
1829
1854
  } else if (s.kind === 'ratio') {
1830
- const num = await this.fetchAggregate(axios, generateUrl, s, s.metric, s.field, (s.numerator && s.numerator.filter) || {})
1831
- const den = await this.fetchAggregate(axios, generateUrl, s, s.metric, s.field, (s.denominator && s.denominator.filter) || {})
1855
+ // Each part may name its own metric, field and `via`, so "answered
1856
+ // in time" over "answered" is two counts, and a share of a sum is
1857
+ // two sums.
1858
+ const part = (p) => this.fetchAggregate(axios, generateUrl, s, (p && p.metric) || s.metric, (p && p.field) || s.field, (p && p.filter) || {}, (p && p.via) || s.via)
1859
+ const num = await part(s.numerator)
1860
+ const den = await part(s.denominator)
1832
1861
  this.value = (den && Number(den) !== 0) ? (Number(num) / Number(den)) * 100 : null
1862
+ } else if (s.kind === 'elapsed') {
1863
+ this.value = await this.fetchElapsed(axios, generateUrl, s)
1833
1864
  } else if (s.kind === 'computed') {
1834
1865
  // Fetch each named part, then evaluate the formula over them.
1835
1866
  const { evalFormula } = await import('../../utils/evalFormula.js')
@@ -1842,7 +1873,7 @@ export default {
1842
1873
  } else if (s.kind === 'weighted') {
1843
1874
  this.value = await this.fetchWeighted(axios, generateUrl, s)
1844
1875
  } else {
1845
- this.value = await this.fetchAggregate(axios, generateUrl, s, s.metric, s.field, s.filter || {})
1876
+ this.value = await this.fetchAggregate(axios, generateUrl, s, s.metric, s.field, s.filter || {}, s.via)
1846
1877
  }
1847
1878
  } catch (e) {
1848
1879
  this.error = (e && e.message) || 'error'
@@ -1852,6 +1883,56 @@ export default {
1852
1883
  }
1853
1884
  },
1854
1885
 
1886
+ /**
1887
+ * The average time between two date fields of the matching objects
1888
+ * (`{ from, to, unit?, filter?, limit? }`): an average answer time is
1889
+ * `occurredAt` to `resolvedAt`. Computed client-side over the fetched
1890
+ * objects (capped at `limit`, default 1000), because the aggregation
1891
+ * endpoint averages one numeric field, not a difference. Objects missing
1892
+ * either date, or with `to` before `from`, are left out; null when none
1893
+ * is left. `unit` is `days` (default), `hours` or `minutes`.
1894
+ *
1895
+ * @param {object} axios The axios instance.
1896
+ * @param {(url: string, params?: object) => string} generateUrl The router helper.
1897
+ * @param {object} s The elapsed source.
1898
+ * @return {Promise<number|null>} The average, in `unit`.
1899
+ * @spec openspec/changes/screens-ratio-stat/specs/ratio-stat/spec.md#requirement-a-stat-can-average-the-time-between-two-dates
1900
+ */
1901
+ async fetchElapsed(axios, generateUrl, s) {
1902
+ if (!s.from || !s.to) {
1903
+ return null
1904
+ }
1905
+ const url = generateUrl(
1906
+ '/apps/openregister/api/objects/{register}/{schema}',
1907
+ { register: s.register, schema: s.schema },
1908
+ )
1909
+ const params = { _limit: s.limit || 1000 }
1910
+ const filter = this.resolveSourceFilter(s.filter || {})
1911
+ for (const [k, v] of Object.entries(filter)) {
1912
+ if (v && typeof v === 'object' && !Array.isArray(v)) {
1913
+ for (const [op, operand] of Object.entries(v)) {
1914
+ params[`${k}[${op}]`] = operand
1915
+ }
1916
+ } else if (v !== '' && v !== null && v !== undefined) {
1917
+ params[k] = v
1918
+ }
1919
+ }
1920
+ const res = await axios.get(url, { params })
1921
+ const rows = (res && res.data && res.data.results) || []
1922
+ const per = { minutes: 60000, hours: 3600000, days: 86400000 }[s.unit] || 86400000
1923
+ let sum = 0
1924
+ let n = 0
1925
+ for (const r of rows) {
1926
+ const from = Date.parse(r && r[s.from])
1927
+ const to = Date.parse(r && r[s.to])
1928
+ if (Number.isFinite(from) && Number.isFinite(to) && to >= from) {
1929
+ sum += (to - from) / per
1930
+ n += 1
1931
+ }
1932
+ }
1933
+ return n > 0 ? sum / n : null
1934
+ },
1935
+
1855
1936
  /**
1856
1937
  * Compute a weighted sum `Σ (field × weightField) ÷ divisor` client-side
1857
1938
  * (no OpenRegister expression-aggregation primitive yet). Pulls the
@@ -17,10 +17,42 @@
17
17
  <template>
18
18
  <div
19
19
  class="cn-stats-block-widget"
20
- :class="[iconClass, { 'cn-stats-block-widget--multi': hasEntries }]">
20
+ :class="[iconClass, { 'cn-stats-block-widget--multi': hasEntries && !isRows, 'cn-stats-block-widget--rows': isRows }]">
21
+ <!-- Rows layout: a title and one label-left, number-right row per entry
22
+ (the board's "Mijn Zuiddrecht nu" card). -->
23
+ <template v-if="isRows">
24
+ <h2 v-if="title" class="cn-stats-block-widget__rows-title" data-testid="cn-stats-block-rows-title">
25
+ {{ tl(title) }}
26
+ </h2>
27
+ <dl class="cn-stats-block-widget__rows" data-testid="cn-stats-block-rows">
28
+ <div
29
+ v-for="view in rowViews"
30
+ :key="view.key"
31
+ class="cn-stats-block-widget__row"
32
+ data-testid="cn-stats-block-row">
33
+ <dt class="cn-stats-block-widget__row-label">
34
+ <a
35
+ v-if="view.href"
36
+ :href="view.href"
37
+ data-testid="cn-stats-block-row-link"
38
+ @click="onRowClick($event, view.route)">{{ view.label }}</a>
39
+ <template v-else>
40
+ {{ view.label }}
41
+ </template>
42
+ </dt>
43
+ <dd
44
+ class="cn-stats-block-widget__row-value"
45
+ :class="'cn-stats-block-widget__row-value--' + view.variant"
46
+ data-testid="cn-stats-block-row-value">
47
+ {{ view.display }}
48
+ </dd>
49
+ </div>
50
+ </dl>
51
+ </template>
52
+
21
53
  <!-- Multi-entry mode: one KPI per entry, all inside this widget card.
22
54
  An entry with hideWhenZero whose resolved count is 0 is omitted. -->
23
- <template v-if="hasEntries">
55
+ <template v-else-if="hasEntries">
24
56
  <CnStatsBlock
25
57
  v-for="view in entryViews"
26
58
  :key="view.key"
@@ -71,6 +103,7 @@ import { subscribe, unsubscribe } from '@nextcloud/event-bus'
71
103
  import CnStatsBlock from '../CnStatsBlock/CnStatsBlock.vue'
72
104
  import { useDataSource } from '../../composables/useDataSource.js'
73
105
  import { formatMetricValue, normalizeMetricFormat, unwrapAppConfig } from '../../utils/formatMetric.js'
106
+ import { followLinkClick, resolveHref } from '../../utils/linkNavigation.js'
74
107
  import { dropOptionalUnresolved, hasUnresolvedTokens, resolveFilterTokens } from '../../utils/resolveFilterTokens.js'
75
108
 
76
109
  // The canonical KPI scale (`--cn-kpi-*`) lives in one stylesheet. Imported
@@ -167,6 +200,8 @@ export default {
167
200
  * currency (`currency`) and `@config.<key>` tokens in `format`.
168
201
  */
169
202
  cnAppConfig: { default: () => ({}) },
203
+ /** The host's label lookup (CnAppRoot provides it); identity without one. */
204
+ cnTranslate: { default: () => (key) => key },
170
205
  },
171
206
 
172
207
  props: {
@@ -264,7 +299,10 @@ export default {
264
299
 
265
300
  /**
266
301
  * `stacked` gives every block the board look (see CnStatsBlock
267
- * `layout`). Empty (the default) keeps the KPI card.
302
+ * `layout`). `rows` draws the title and then one row per entry (or the
303
+ * single source): the label on the left, the number on the right, the
304
+ * label a link when the entry has a `route`. Empty (the default) keeps
305
+ * the KPI card.
268
306
  *
269
307
  * @spec openspec/changes/zuiddrecht-pixel-gaps-3/specs/zuiddrecht-pixel-gaps-3/spec.md#requirement-a-stats-block-can-take-the-stacked-board-look
270
308
  */
@@ -353,6 +391,52 @@ export default {
353
391
  return Array.isArray(this.entries) && this.entries.length > 0
354
392
  },
355
393
 
394
+ /**
395
+ * Whether the widget draws label/number rows (`layout: "rows"`).
396
+ *
397
+ * @return {boolean}
398
+ */
399
+ isRows() {
400
+ return this.layout === 'rows'
401
+ },
402
+
403
+ /**
404
+ * The rows of the rows layout: one per entry, or a single row from the
405
+ * `dataSource` (labelled by `countLabel`). A row is `{ key, label,
406
+ * display, variant, route, href }`; `display` is the formatted value,
407
+ * a dash while it loads or when the count could not be read.
408
+ *
409
+ * @return {Array<object>}
410
+ */
411
+ rowViews() {
412
+ if (!this.hasEntries) {
413
+ const formatted = this.formattedSingle
414
+ return [{
415
+ key: 'single',
416
+ label: this.tl(this.countLabel),
417
+ display: this.loading ? '\u2014' : (formatted !== null ? formatted : this.formatCount(this.resolvedCount)),
418
+ variant: this.variant || 'default',
419
+ route: this.route,
420
+ href: this.route ? resolveHref(this.route, this.$router) : '',
421
+ }]
422
+ }
423
+ return this.entryViews.map((view) => {
424
+ const resolved = view.resolved === true
425
+ let display = '\u2014'
426
+ if (resolved) {
427
+ display = view.formatted !== null ? view.formatted : this.formatCount(view.count)
428
+ }
429
+ return {
430
+ key: view.key,
431
+ label: this.tl(view.entry.title || ''),
432
+ display,
433
+ variant: view.entry.variant || 'default',
434
+ route: view.route,
435
+ href: view.route ? resolveHref(view.route, this.$router) : '',
436
+ }
437
+ })
438
+ },
439
+
356
440
  /**
357
441
  * The unwrapped detail-page object context for token resolution, or
358
442
  * null on surfaces (dashboards) that don't provide one.
@@ -566,6 +650,37 @@ export default {
566
650
  },
567
651
 
568
652
  methods: {
653
+ /**
654
+ * A manifest string through the host's label lookup.
655
+ *
656
+ * @param {string} text The text as written in the manifest.
657
+ * @return {string} The translated text.
658
+ */
659
+ tl(text) {
660
+ return typeof this.cnTranslate === 'function' ? this.cnTranslate(text) : text
661
+ },
662
+
663
+ /**
664
+ * A count in the user's locale.
665
+ *
666
+ * @param {number} count The count.
667
+ * @return {string} The localized count.
668
+ */
669
+ formatCount(count) {
670
+ return new Intl.NumberFormat().format(count)
671
+ },
672
+
673
+ /**
674
+ * Follow a row label's link in the router, leaving a modified click to the browser.
675
+ *
676
+ * @param {MouseEvent} event The click.
677
+ * @param {object} route The route location.
678
+ * @return {void}
679
+ */
680
+ onRowClick(event, route) {
681
+ followLinkClick(event, route, this.$router)
682
+ },
683
+
569
684
  /**
570
685
  * Re-query the tile's counts — the REST `/value` aggregation, the
571
686
  * per-entry counts of multi-entry mode, and the `dataSource` GraphQL
@@ -758,4 +873,66 @@ export default {
758
873
  flex-direction: column;
759
874
  gap: var(--cn-kpi-stack-gap, 12px);
760
875
  }
876
+
877
+ /* Rows layout: the board's "label left, number right" card body. */
878
+ .cn-stats-block-widget--rows {
879
+ display: flex;
880
+ flex-direction: column;
881
+ gap: 12px;
882
+ box-sizing: border-box;
883
+ padding: 20px 22px;
884
+ }
885
+
886
+ .cn-stats-block-widget__rows-title {
887
+ margin: 0;
888
+ font-size: 15px;
889
+ font-weight: 600;
890
+ color: var(--color-text-maxcontrast);
891
+ }
892
+
893
+ .cn-stats-block-widget__rows {
894
+ display: flex;
895
+ flex-direction: column;
896
+ gap: 8px;
897
+ margin: 0;
898
+ font-size: 14px;
899
+ }
900
+
901
+ .cn-stats-block-widget__row {
902
+ display: flex;
903
+ align-items: baseline;
904
+ justify-content: space-between;
905
+ gap: 16px;
906
+ }
907
+
908
+ .cn-stats-block-widget__row-label {
909
+ min-width: 0;
910
+ margin: 0;
911
+ overflow: hidden;
912
+ text-overflow: ellipsis;
913
+ }
914
+
915
+ .cn-stats-block-widget__row-value {
916
+ flex: none;
917
+ margin: 0;
918
+ font-size: 17px;
919
+ font-weight: 700;
920
+ text-align: right;
921
+ }
922
+
923
+ .cn-stats-block-widget__row-value--success {
924
+ color: var(--color-text-success, var(--color-success-text));
925
+ }
926
+
927
+ .cn-stats-block-widget__row-value--warning {
928
+ color: var(--color-text-warning, var(--color-warning-text));
929
+ }
930
+
931
+ .cn-stats-block-widget__row-value--error {
932
+ color: var(--color-text-error, var(--color-error-text));
933
+ }
934
+
935
+ .cn-stats-block-widget__row-value--primary {
936
+ color: var(--color-primary-element);
937
+ }
761
938
  </style>
@@ -0,0 +1,226 @@
1
+ <!--
2
+ - SPDX-FileCopyrightText: 2026 Conduction B.V.
3
+ - SPDX-License-Identifier: EUPL-1.2
4
+ -->
5
+
6
+ <template>
7
+ <div class="cn-switch-rows" data-testid="cn-switch-rows">
8
+ <ul class="cn-switch-rows__list">
9
+ <li
10
+ v-for="row in rows"
11
+ :key="row.key"
12
+ class="cn-switch-rows__row"
13
+ :data-testid="`cn-switch-rows-row-${row.key}`">
14
+ <NcCheckboxRadioSwitch
15
+ :modelValue="values[row.key] === true"
16
+ type="switch"
17
+ :disabled="readOnly || busy === row.key"
18
+ @update:modelValue="toggle(row, $event)">
19
+ <span class="cn-switch-rows__label">{{ row.label }}</span>
20
+ <span v-if="row.hint" class="cn-switch-rows__hint">{{ row.hint }}</span>
21
+ </NcCheckboxRadioSwitch>
22
+ </li>
23
+ </ul>
24
+ <NcNoteCard v-if="notice" type="error" data-testid="cn-switch-rows-notice">
25
+ {{ notice }}
26
+ </NcNoteCard>
27
+ </div>
28
+ </template>
29
+
30
+ <script>
31
+ import { NcCheckboxRadioSwitch, NcNoteCard } from '@nextcloud/vue'
32
+ import { objectUrlOf, patchObject } from '../../utils/objectWrite.js'
33
+ import { readPath } from '../../utils/readPath.js'
34
+ import { serverErrorMessage } from '../../utils/serverErrorMessage.js'
35
+
36
+ /**
37
+ * CnSwitchRowsWidget — the `switch-rows` widget: a card of switches, each with
38
+ * a label and a hint, bound to boolean fields of the CURRENT object. A toggle
39
+ * saves at once with ONE PATCH of that field; a refused PATCH puts the switch
40
+ * back and shows the server's words.
41
+ *
42
+ * @spec openspec/changes/abstract-switch-rows/specs/switch-rows/spec.md
43
+ */
44
+ export default {
45
+ name: 'CnSwitchRowsWidget',
46
+
47
+ components: { NcCheckboxRadioSwitch, NcNoteCard },
48
+
49
+ inject: {
50
+ cnTranslate: { default: () => (key) => key },
51
+ },
52
+
53
+ props: {
54
+ /**
55
+ * The widget configuration.
56
+ *
57
+ * - `rows` (required) — `[{ key, label, hint?, invert? }]`; `key` is a top-level boolean field. `invert` shows the switch on when the field is false (a stored `disabled` read as "enabled").
58
+ * - `readOnly`.
59
+ *
60
+ * @type {object}
61
+ */
62
+ content: {
63
+ type: Object,
64
+ default: () => ({}),
65
+ },
66
+
67
+ /** The current detail-page object. */
68
+ objectData: {
69
+ type: Object,
70
+ default: null,
71
+ },
72
+
73
+ /** The current object id. */
74
+ objectId: {
75
+ type: [String, Number],
76
+ default: '',
77
+ },
78
+
79
+ /** OpenRegister register slug of the surface. */
80
+ register: {
81
+ type: [String, Object],
82
+ default: '',
83
+ },
84
+
85
+ /** OpenRegister schema slug of the surface. */
86
+ schema: {
87
+ type: [String, Object],
88
+ default: '',
89
+ },
90
+ },
91
+
92
+ emits: ['saved'],
93
+
94
+ data() {
95
+ return {
96
+ /** The switch states as last saved (by key, as the switch shows them). */
97
+ saved: {},
98
+ values: {},
99
+ busy: '',
100
+ notice: '',
101
+ }
102
+ },
103
+
104
+ computed: {
105
+ rows() {
106
+ const list = Array.isArray(this.content.rows) ? this.content.rows : []
107
+ return list
108
+ .filter((row) => row && row.key)
109
+ .map((row) => ({ ...row, label: this.tr(row.label || row.key), hint: this.tr(row.hint || '') }))
110
+ },
111
+
112
+ readOnly() {
113
+ return this.content.readOnly === true
114
+ },
115
+
116
+ objectUrl() {
117
+ return objectUrlOf(this.register, this.schema, this.objectId || (this.objectData && this.objectData.id))
118
+ },
119
+ },
120
+
121
+ watch: {
122
+ objectData() {
123
+ this.adopt()
124
+ },
125
+ },
126
+
127
+ created() {
128
+ this.adopt()
129
+ },
130
+
131
+ methods: {
132
+ tr(text) {
133
+ if (typeof text !== 'string' || text === '') {
134
+ return ''
135
+ }
136
+ return typeof this.cnTranslate === 'function' ? this.cnTranslate(text) : text
137
+ },
138
+
139
+ /** Read every switch off the object. */
140
+ adopt() {
141
+ const values = {}
142
+ for (const row of this.rows) {
143
+ const stored = readPath(this.objectData || {}, row.key) === true
144
+ values[row.key] = row.invert ? !stored : stored
145
+ }
146
+ this.saved = values
147
+ this.values = { ...values }
148
+ },
149
+
150
+ /**
151
+ * Flip one switch and save that one field.
152
+ *
153
+ * @param {object} row The row.
154
+ * @param {boolean} on The new switch state.
155
+ * @return {Promise<void>}
156
+ * @spec openspec/changes/abstract-switch-rows/specs/switch-rows/spec.md#requirement-a-toggle-saves-that-one-field
157
+ */
158
+ async toggle(row, on) {
159
+ if (!this.objectUrl) {
160
+ this.notice = this.tr('This switch cannot be saved here.')
161
+ return
162
+ }
163
+ this.notice = ''
164
+ this.busy = row.key
165
+ this.values = { ...this.values, [row.key]: on }
166
+ const stored = row.invert ? !on : on
167
+ try {
168
+ const data = await patchObject(this.objectUrl, { [row.key]: stored })
169
+ const written = data[row.key] !== undefined ? data[row.key] === true : stored
170
+ const shown = row.invert ? !written : written
171
+ this.saved = { ...this.saved, [row.key]: shown }
172
+ this.values = { ...this.values, [row.key]: shown }
173
+ /**
174
+ * @event saved A switch was written.
175
+ * @type {{field: string, value: boolean}}
176
+ */
177
+ this.$emit('saved', { field: row.key, value: written })
178
+ } catch (e) {
179
+ this.values = { ...this.values, [row.key]: this.saved[row.key] }
180
+ this.notice = serverErrorMessage(e)
181
+ } finally {
182
+ this.busy = ''
183
+ }
184
+ },
185
+ },
186
+ }
187
+ </script>
188
+
189
+ <style scoped>
190
+ .cn-switch-rows {
191
+ display: flex;
192
+ flex-direction: column;
193
+ gap: 8px;
194
+ min-width: 0;
195
+ }
196
+
197
+ .cn-switch-rows__list {
198
+ list-style: none;
199
+ margin: 0;
200
+ padding: 0;
201
+ display: flex;
202
+ flex-direction: column;
203
+ }
204
+
205
+ .cn-switch-rows__row {
206
+ padding: 10px 0;
207
+ border-top: 1px solid var(--cn-board-hairline, var(--color-border));
208
+ }
209
+
210
+ .cn-switch-rows__row:first-child {
211
+ border-top: none;
212
+ padding-top: 0;
213
+ }
214
+
215
+ .cn-switch-rows__label {
216
+ display: block;
217
+ font-weight: 600;
218
+ }
219
+
220
+ .cn-switch-rows__hint {
221
+ display: block;
222
+ font-size: 12px;
223
+ font-weight: 400;
224
+ color: var(--color-text-maxcontrast);
225
+ }
226
+ </style>
@@ -0,0 +1,25 @@
1
+ /**
2
+ * SPDX-FileCopyrightText: 2026 Conduction B.V.
3
+ * SPDX-License-Identifier: EUPL-1.2
4
+ *
5
+ * CnSwitchRowsWidget — self-registers the `switch-rows` widget type (a card of
6
+ * switches, each with a label and a hint, bound to boolean fields of the
7
+ * current object and saved on toggle) into the shared dashboardWidgetRegistry.
8
+ */
9
+
10
+ import CnSwitchRowsWidget from './CnSwitchRowsWidget.vue'
11
+ import { registerDashboardWidget } from '../CnWidgetGrid/dashboardWidgetRegistry.js'
12
+
13
+ registerDashboardWidget('switch-rows', {
14
+ renderer: CnSwitchRowsWidget,
15
+ form: null,
16
+ defaultContent: {
17
+ rows: [],
18
+ },
19
+ displayName: 'Switch rows',
20
+ icon: 'ToggleSwitch',
21
+ surfaces: ['detail-page'],
22
+ })
23
+
24
+ export { CnSwitchRowsWidget }
25
+ export default CnSwitchRowsWidget