@conduction/nextcloud-vue 2.78.0 → 2.80.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (259) hide show
  1. package/dist/bundled-packages.json +0 -2
  2. package/dist/esm/bundled-packages.json +0 -2
  3. package/dist/esm/components/CnActionButtons/CnActionButtons.vue2.js.map +1 -1
  4. package/dist/esm/components/CnActionButtons/CnActionButtons.vue3.js +3 -1
  5. package/dist/esm/components/CnActionButtons/CnActionButtons.vue3.js.map +1 -1
  6. package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js +32 -3
  7. package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js.map +1 -1
  8. package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js +115 -104
  9. package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js.map +1 -1
  10. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js +12 -2
  11. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
  12. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js.map +1 -1
  13. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js +39 -3
  14. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js.map +1 -1
  15. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue3.js.map +1 -1
  16. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js +158 -0
  17. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js.map +1 -1
  18. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js +157 -96
  19. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js.map +1 -1
  20. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +139 -1
  21. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  22. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +140 -92
  23. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  24. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +77 -0
  25. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
  26. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +5 -2
  27. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
  28. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +62 -4
  29. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  30. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  31. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js +85 -1
  32. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
  33. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js +43 -4
  34. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
  35. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue.js +11 -0
  36. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue.js.map +1 -0
  37. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js +736 -0
  38. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js.map +1 -0
  39. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js +238 -0
  40. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js.map +1 -0
  41. package/dist/esm/components/CnFactsWidget/index.js +30 -0
  42. package/dist/esm/components/CnFactsWidget/index.js.map +1 -0
  43. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue.js +10 -0
  44. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue.js.map +1 -0
  45. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js +551 -0
  46. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js.map +1 -0
  47. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js +183 -0
  48. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js.map +1 -0
  49. package/dist/esm/components/CnFieldListWidget/index.js +29 -0
  50. package/dist/esm/components/CnFieldListWidget/index.js.map +1 -0
  51. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js +173 -5
  52. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js.map +1 -1
  53. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js +181 -99
  54. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js.map +1 -1
  55. package/dist/esm/components/CnFilesBrowser/filesBrowser.js +29 -11
  56. package/dist/esm/components/CnFilesBrowser/filesBrowser.js.map +1 -1
  57. package/dist/esm/components/CnFilesBrowser/openRegisterSource.js +252 -0
  58. package/dist/esm/components/CnFilesBrowser/openRegisterSource.js.map +1 -0
  59. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js +66 -18
  60. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
  61. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js +4 -4
  62. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
  63. package/dist/esm/components/CnFormPage/CnFormPage.vue2.js +18 -3
  64. package/dist/esm/components/CnFormPage/CnFormPage.vue2.js.map +1 -1
  65. package/dist/esm/components/CnFormPage/CnFormPage.vue3.js.map +1 -1
  66. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +330 -74
  67. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  68. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +34 -15
  69. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  70. package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue.js +10 -0
  71. package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue.js.map +1 -0
  72. package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue2.js +99 -0
  73. package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue2.js.map +1 -0
  74. package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue3.js +48 -0
  75. package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue3.js.map +1 -0
  76. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js +100 -3
  77. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js.map +1 -1
  78. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js +4 -2
  79. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js.map +1 -1
  80. package/dist/esm/components/CnPageRenderer/CnPageRenderer.vue2.js +29 -1
  81. package/dist/esm/components/CnPageRenderer/CnPageRenderer.vue2.js.map +1 -1
  82. package/dist/esm/components/CnPageRenderer/CnPageRenderer.vue3.js.map +1 -1
  83. package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue2.js +3 -2
  84. package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue2.js.map +1 -1
  85. package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue3.js.map +1 -1
  86. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +10 -0
  87. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js.map +1 -0
  88. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +387 -0
  89. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -0
  90. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js +169 -0
  91. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -0
  92. package/dist/esm/components/CnRecordListWidget/index.js +31 -0
  93. package/dist/esm/components/CnRecordListWidget/index.js.map +1 -0
  94. package/dist/esm/components/CnSavedViewsControl/CnSavedViewsControl.vue2.js.map +1 -1
  95. package/dist/esm/components/CnSavedViewsControl/CnSavedViewsControl.vue3.js +1 -1
  96. package/dist/esm/components/CnSavedViewsControl/CnSavedViewsControl.vue3.js.map +1 -1
  97. package/dist/esm/components/CnSectionsWidget/CnSectionsWidget.vue.js +10 -0
  98. package/dist/esm/components/CnSectionsWidget/CnSectionsWidget.vue.js.map +1 -0
  99. package/dist/esm/components/CnSectionsWidget/CnSectionsWidget.vue2.js +351 -0
  100. package/dist/esm/components/CnSectionsWidget/CnSectionsWidget.vue2.js.map +1 -0
  101. package/dist/esm/components/CnSectionsWidget/CnSectionsWidget.vue3.js +71 -0
  102. package/dist/esm/components/CnSectionsWidget/CnSectionsWidget.vue3.js.map +1 -0
  103. package/dist/esm/components/CnSectionsWidget/index.js +35 -0
  104. package/dist/esm/components/CnSectionsWidget/index.js.map +1 -0
  105. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue2.js +81 -2
  106. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue2.js.map +1 -1
  107. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue3.js.map +1 -1
  108. package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js.map +1 -1
  109. package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js +1 -0
  110. package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js.map +1 -1
  111. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +152 -8
  112. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
  113. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js +84 -23
  114. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
  115. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js +4 -0
  116. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js.map +1 -1
  117. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js +13 -0
  118. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js.map +1 -1
  119. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue3.js.map +1 -1
  120. package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue2.js +106 -2
  121. package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue2.js.map +1 -1
  122. package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue3.js +105 -53
  123. package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue3.js.map +1 -1
  124. package/dist/esm/composables/useAppManifest.js +8 -3
  125. package/dist/esm/composables/useAppManifest.js.map +1 -1
  126. package/dist/esm/composables/useFormDraft.js +140 -9
  127. package/dist/esm/composables/useFormDraft.js.map +1 -1
  128. package/dist/esm/index.js +5 -0
  129. package/dist/esm/index.js.map +1 -1
  130. package/dist/esm/integrations/builtin/field-inspection/CnFieldInspectionCard.vue2.js +28 -20
  131. package/dist/esm/integrations/builtin/field-inspection/CnFieldInspectionCard.vue2.js.map +1 -1
  132. package/dist/esm/integrations/builtin/field-inspection/CnFieldInspectionCard.vue3.js +9 -0
  133. package/dist/esm/integrations/builtin/field-inspection/CnFieldInspectionCard.vue3.js.map +1 -1
  134. package/dist/esm/integrations/builtin/field-inspection.js +21 -0
  135. package/dist/esm/integrations/builtin/field-inspection.js.map +1 -1
  136. package/dist/esm/integrations/offline/checklistSubmission.js +141 -0
  137. package/dist/esm/integrations/offline/checklistSubmission.js.map +1 -0
  138. package/dist/esm/integrations/offline/fieldCollectionHelpers.js +70 -5
  139. package/dist/esm/integrations/offline/fieldCollectionHelpers.js.map +1 -1
  140. package/dist/esm/integrations/offline/index.js +2 -1
  141. package/dist/esm/integrations/offline/index.js.map +1 -1
  142. package/dist/esm/integrations/offline/offlineDb.js +6 -1
  143. package/dist/esm/integrations/offline/offlineDb.js.map +1 -1
  144. package/dist/esm/integrations/offline/syncReplayService.js +8 -2
  145. package/dist/esm/integrations/offline/syncReplayService.js.map +1 -1
  146. package/dist/esm/l10n/en.json.js +72 -1
  147. package/dist/esm/l10n/en.json.js.map +1 -1
  148. package/dist/esm/l10n/nl.json.js +120 -2
  149. package/dist/esm/l10n/nl.json.js.map +1 -1
  150. package/dist/esm/nextcloud-vue.css +1314 -74
  151. package/dist/esm/utils/boardDate.js +193 -0
  152. package/dist/esm/utils/boardDate.js.map +1 -0
  153. package/dist/esm/utils/detailActionModel.js +60 -3
  154. package/dist/esm/utils/detailActionModel.js.map +1 -1
  155. package/dist/esm/utils/dynamicProperties.js +5 -1
  156. package/dist/esm/utils/dynamicProperties.js.map +1 -1
  157. package/dist/esm/utils/fillTemplate.js +114 -0
  158. package/dist/esm/utils/fillTemplate.js.map +1 -0
  159. package/dist/esm/utils/libraryWidgetKeys.js +4 -0
  160. package/dist/esm/utils/libraryWidgetKeys.js.map +1 -1
  161. package/dist/esm/utils/queryPresets.js +96 -0
  162. package/dist/esm/utils/queryPresets.js.map +1 -0
  163. package/dist/esm/utils/resolveFilterTokens.js +11 -0
  164. package/dist/esm/utils/resolveFilterTokens.js.map +1 -1
  165. package/dist/esm/utils/schema.js +21 -0
  166. package/dist/esm/utils/schema.js.map +1 -1
  167. package/dist/esm/utils/sentinelTokens.js +3 -3
  168. package/dist/esm/utils/sentinelTokens.js.map +1 -1
  169. package/dist/esm/utils/validateManifestV2.compiled.js +57 -6
  170. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  171. package/dist/esm/utils/widgetDispatch.js +7 -1
  172. package/dist/esm/utils/widgetDispatch.js.map +1 -1
  173. package/dist/nextcloud-vue.cjs.js +14278 -8169
  174. package/dist/nextcloud-vue.cjs.js.map +2 -2
  175. package/dist/nextcloud-vue.css +1314 -74
  176. package/l10n/en.json +72 -1
  177. package/l10n/nl.json +120 -2
  178. package/package.json +1 -1
  179. package/scripts/build-validators.js +26 -0
  180. package/src/components/CnActionButtons/CnActionButtons.vue +2 -0
  181. package/src/components/CnActionsBar/CnActionsBar.md +4 -0
  182. package/src/components/CnActionsBar/CnActionsBar.vue +83 -42
  183. package/src/components/CnAppRoot/CnAppRoot.vue +12 -2
  184. package/src/components/CnBannerWidget/CnBannerWidget.vue +39 -3
  185. package/src/components/CnCellRenderer/CnCellRenderer.vue +306 -2
  186. package/src/components/CnDashboardPage/CnDashboardPage.vue +219 -4
  187. package/src/components/CnDataTable/CnDataTable.md +2 -0
  188. package/src/components/CnDataTable/CnDataTable.vue +82 -2
  189. package/src/components/CnDetailPage/CnDetailPage.md +1 -1
  190. package/src/components/CnDetailPage/CnDetailPage.vue +63 -3
  191. package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +127 -1
  192. package/src/components/CnFactsWidget/CnFactsWidget.vue +1050 -0
  193. package/src/components/CnFactsWidget/index.js +29 -0
  194. package/src/components/CnFieldListWidget/CnFieldListWidget.vue +746 -0
  195. package/src/components/CnFieldListWidget/index.js +28 -0
  196. package/src/components/CnFilesBrowser/CnFilesBrowser.vue +232 -11
  197. package/src/components/CnFilesBrowser/filesBrowser.js +43 -2
  198. package/src/components/CnFilesBrowser/index.js +1 -0
  199. package/src/components/CnFilesBrowser/openRegisterSource.js +248 -0
  200. package/src/components/CnFormDialog/CnFormDialog.md +9 -3
  201. package/src/components/CnFormDialog/CnFormDialog.vue +70 -22
  202. package/src/components/CnFormPage/CnFormPage.vue +18 -3
  203. package/src/components/CnIndexPage/CnIndexPage.vue +281 -9
  204. package/src/components/CnIndexPage/CnIndexPageWidgets.vue +169 -0
  205. package/src/components/CnObjectSidebar/CnFilesTab.vue +103 -4
  206. package/src/components/CnPageRenderer/CnPageRenderer.vue +29 -1
  207. package/src/components/CnQuickFilterBar/CnQuickFilterBar.vue +3 -2
  208. package/src/components/CnRecordListWidget/CnRecordListWidget.vue +603 -0
  209. package/src/components/CnRecordListWidget/index.js +30 -0
  210. package/src/components/CnSavedViewsControl/CnSavedViewsControl.vue +1 -1
  211. package/src/components/CnSectionsWidget/CnSectionsWidget.vue +489 -0
  212. package/src/components/CnSectionsWidget/index.js +34 -0
  213. package/src/components/CnStagesWidget/CnStagesWidget.vue +81 -2
  214. package/src/components/CnStatWidget/CnStatWidget.vue +1 -1
  215. package/src/components/CnTabsWidget/CnTabsWidget.vue +214 -9
  216. package/src/components/CnWidgetGrid/registerDashboardWidgets.js +4 -0
  217. package/src/components/CnWidgetObjectTable/CnWidgetObjectTable.vue +13 -0
  218. package/src/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue +188 -4
  219. package/src/components/index.js +5 -1
  220. package/src/composables/__tests__/useFormDraft.spec.js +69 -0
  221. package/src/composables/useAppManifest.js +8 -3
  222. package/src/composables/useFormDraft.js +139 -8
  223. package/src/css/form-field.css +14 -0
  224. package/src/css/index-page.css +71 -0
  225. package/src/css/kpi-card.css +32 -0
  226. package/src/css/look-board-detail.css +87 -0
  227. package/src/css/look-board-index.css +143 -14
  228. package/src/css/look-board.css +114 -2
  229. package/src/css/table.css +56 -0
  230. package/src/index.js +8 -0
  231. package/src/integrations/builtin/field-inspection/CnFieldInspectionCard.vue +35 -20
  232. package/src/integrations/builtin/field-inspection.js +21 -0
  233. package/src/integrations/offline/checklistSubmission.js +137 -0
  234. package/src/integrations/offline/fieldCollectionHelpers.js +69 -4
  235. package/src/integrations/offline/index.js +3 -0
  236. package/src/integrations/offline/offlineDb.js +6 -1
  237. package/src/integrations/offline/syncReplayService.js +8 -2
  238. package/src/schemas/app-manifest-v2.schema.json +149 -5
  239. package/src/utils/boardDate.js +189 -0
  240. package/src/utils/detailActionModel.js +59 -2
  241. package/src/utils/dynamicProperties.js +5 -1
  242. package/src/utils/fillTemplate.js +110 -0
  243. package/src/utils/libraryWidgetKeys.js +4 -0
  244. package/src/utils/queryPresets.js +93 -0
  245. package/src/utils/resolveFilterTokens.js +11 -0
  246. package/src/utils/schema.js +21 -0
  247. package/src/utils/sentinelTokens.js +3 -3
  248. package/src/utils/validateManifestV2.compiled.js +57 -2
  249. package/src/utils/widgetDispatch.js +7 -1
  250. package/dist/esm/_virtual/equal.js +0 -4
  251. package/dist/esm/_virtual/equal.js.map +0 -1
  252. package/dist/esm/_virtual/ucs2length.js +0 -4
  253. package/dist/esm/_virtual/ucs2length.js.map +0 -1
  254. package/dist/esm/node_modules/ajv/dist/runtime/equal.js +0 -9
  255. package/dist/esm/node_modules/ajv/dist/runtime/equal.js.map +0 -1
  256. package/dist/esm/node_modules/ajv/dist/runtime/ucs2length.js +0 -25
  257. package/dist/esm/node_modules/ajv/dist/runtime/ucs2length.js.map +0 -1
  258. package/dist/esm/node_modules/fast-deep-equal/index.js +0 -47
  259. package/dist/esm/node_modules/fast-deep-equal/index.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"CnWorkspaceFilterWidget.vue2.js","sources":["../../../../src/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n\n<template>\n\t<div class=\"cn-workspace-filter-widget\" data-testid=\"cn-workspace-filter-widget\">\n\t\t<p v-if=\"label\" class=\"cn-workspace-filter-widget__label\">\n\t\t\t{{ label }}\n\t\t</p>\n\n\t\t<div v-if=\"loading\" class=\"cn-workspace-filter-widget__status\">\n\t\t\t{{ loadingLabel }}\n\t\t</div>\n\n\t\t<!-- Select style -->\n\t\t<NcSelect\n\t\t\tv-else-if=\"style === 'select'\"\n\t\t\t:modelValue=\"selectedOption\"\n\t\t\t:options=\"displayOptions\"\n\t\t\t:clearable=\"false\"\n\t\t\t:inputLabel=\"label || writeKey\"\n\t\t\tlabel=\"label\"\n\t\t\tdata-testid=\"cn-workspace-filter-select\"\n\t\t\t@update:modelValue=\"onSelect\" />\n\n\t\t<!-- Radio-list style (default) -->\n\t\t<ul v-else\n\t\t\tclass=\"cn-workspace-filter-widget__list\"\n\t\t\trole=\"radiogroup\"\n\t\t\t:aria-label=\"label || writeKey\">\n\t\t\t<li\n\t\t\t\tv-for=\"option in displayOptions\"\n\t\t\t\t:key=\"String(option.value)\"\n\t\t\t\tclass=\"cn-workspace-filter-widget__item\">\n\t\t\t\t<NcCheckboxRadioSwitch\n\t\t\t\t\ttype=\"radio\"\n\t\t\t\t\t:name=\"radioGroupName\"\n\t\t\t\t\t:modelValue=\"isActive(option) ? String(option.value) : ''\"\n\t\t\t\t\t:value=\"String(option.value)\"\n\t\t\t\t\t:data-testid=\"`cn-workspace-filter-option-${option.value}`\"\n\t\t\t\t\t@update:modelValue=\"onRadioPick(option)\">\n\t\t\t\t\t<span class=\"cn-workspace-filter-widget__option-label\">{{ option.label }}</span>\n\t\t\t\t\t<span\n\t\t\t\t\t\tv-if=\"showCounts && option.count !== undefined && option.count !== null\"\n\t\t\t\t\t\tclass=\"cn-workspace-filter-widget__count\">{{ option.count }}</span>\n\t\t\t\t</NcCheckboxRadioSwitch>\n\t\t\t</li>\n\t\t</ul>\n\t</div>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcCheckboxRadioSwitch, NcSelect } from '@nextcloud/vue'\nimport { fetchEndpointSource } from '../../composables/useEndpointSource.js'\nimport { resolveFilterTokens } from '../../utils/resolveFilterTokens.js'\n\n/**\n * CnWorkspaceFilterWidget — a dashboard choice-list that writes the selected\n * value into the page workspace context so sibling widgets refetch (#91\n * Wave 3, the `workspace-filter` widget).\n *\n * Options come from ONE of three sources (checked in order):\n * 1. static `options[]` — each `{ value, label, count? }`.\n * 2. an OpenRegister `source` — `{ register, schema, groupBy, filter? }`,\n * fetched from OR's `/grouped` facet so each distinct value becomes an\n * option with its object `count`.\n * 3. an `endpointSource` — `{ url, params?, responsePath? }` returning an\n * array of `{ value, label, count? }` (or `{ id, name }` / bare strings,\n * normalised).\n *\n * The chosen value is written to the workspace context key named by `writes`\n * (`\"@workspace.<key>\"` or a bare `\"<key>\"`). CnDashboardPage provides\n * `cnWorkspaceContext`; every sibling widget whose declarative source\n * interpolates `@workspace.<key>` re-resolves its tokens and refetches when\n * the selection changes — the pipelinq WerkplekQueueFilter contract.\n *\n * An optional leading \"All\" option (`allLabel`) clears the key (writes the\n * empty string), so an optional `@workspace.<key>?` token drops and siblings\n * show the unfiltered set.\n *\n * ```js\n * content: {\n * label: 'Queue',\n * writes: '@workspace.queue',\n * style: 'radio', // 'radio' (default) | 'select'\n * allLabel: 'All',\n * showCounts: true,\n * source: { register: 'pipelinq', schema: 'werkitem', groupBy: 'queue' },\n * }\n * ```\n */\nexport default {\n\tname: 'CnWorkspaceFilterWidget',\n\n\tcomponents: { NcSelect, NcCheckboxRadioSwitch },\n\n\tinject: {\n\t\t/**\n\t\t * Page-level workspace context. CnDashboardPage provides a `ref({})`;\n\t\t * Vue 2.7's Options-API inject AUTO-UNWRAPS it, so this is the plain\n\t\t * reactive bag (the raw-ref `{ value }` shape is also handled on write).\n\t\t * Null on pages without one (the widget then just emits `@change`).\n\t\t */\n\t\tcnWorkspaceContext: { default: null },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * Persisted configuration blob.\n\t\t *\n\t\t * @type {{label?: string, writes?: string, style?: string, allLabel?: string, showCounts?: boolean, options?: Array, source?: object, endpointSource?: object, default?: (string|number)}}\n\t\t */\n\t\tcontent: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\t},\n\n\temits: ['change'],\n\n\tdata() {\n\t\treturn {\n\t\t\t/** Fetched options (from source / endpointSource); static options use content.options directly. */\n\t\t\tfetchedOptions: null,\n\t\t\t/** True while options are being fetched. */\n\t\t\tloading: false,\n\t\t\t/** The currently-selected value (mirrors the workspace key). */\n\t\t\tselected: undefined,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/** Section label. */\n\t\tlabel() {\n\t\t\treturn this.content.label || ''\n\t\t},\n\n\t\t/** Choice style: 'select' or 'radio' (default). */\n\t\tstyle() {\n\t\t\treturn this.content.style === 'select' ? 'select' : 'radio'\n\t\t},\n\n\t\t/** Whether to render per-option counts. */\n\t\tshowCounts() {\n\t\t\treturn this.content.showCounts !== false\n\t\t},\n\n\t\t/** The bare workspace key the selection writes (strips a `@workspace.` / `@page.` prefix). */\n\t\twriteKey() {\n\t\t\tconst w = this.content.writes || ''\n\t\t\treturn String(w).replace(/^@(workspace|page)\\./, '')\n\t\t},\n\n\t\t/** A stable radiogroup name so the radios are mutually exclusive. */\n\t\tradioGroupName() {\n\t\t\treturn `cn-wsf-${this.writeKey || 'filter'}`\n\t\t},\n\n\t\t/** The option list actually rendered — the optional \"All\" entry prepended. */\n\t\tdisplayOptions() {\n\t\t\tconst base = this.normalisedOptions\n\t\t\tif (this.content.allLabel) {\n\t\t\t\treturn [{ value: '', label: this.content.allLabel }, ...base]\n\t\t\t}\n\t\t\treturn base\n\t\t},\n\n\t\t/** Normalised `{ value, label, count? }` options from whichever source is configured. */\n\t\tnormalisedOptions() {\n\t\t\tconst raw = this.fetchedOptions !== null\n\t\t\t\t? this.fetchedOptions\n\t\t\t\t: (Array.isArray(this.content.options) ? this.content.options : [])\n\t\t\treturn raw.map((o) => this.normaliseOption(o)).filter((o) => o.value !== undefined && o.value !== null)\n\t\t},\n\n\t\t/** The selected option object (for NcSelect). */\n\t\tselectedOption() {\n\t\t\treturn this.displayOptions.find((o) => String(o.value) === String(this.selected)) || null\n\t\t},\n\n\t\t/**\n\t\t * The reactive workspace value for this key — watched so an external\n\t\t * write (another widget) syncs the local selection.\n\t\t *\n\t\t * @return {unknown}\n\t\t */\n\t\tworkspaceValue() {\n\t\t\treturn this.readWorkspace()\n\t\t},\n\n\t\tloadingLabel() {\n\t\t\treturn t('nextcloud-vue', 'Loading…')\n\t\t},\n\t},\n\n\twatch: {\n\t\t/** Re-fetch options when a dynamic source changes. */\n\t\tcontent: {\n\t\t\tdeep: true,\n\t\t\thandler() {\n\t\t\t\tthis.loadOptions()\n\t\t\t},\n\t\t},\n\n\t\t/**\n\t\t * Track external workspace changes (another widget writing the same key).\n\t\t *\n\t\t * @param {unknown} val The new workspace value for this key.\n\t\t */\n\t\tworkspaceValue(val) {\n\t\t\tif (val !== undefined) {\n\t\t\t\tthis.selected = val\n\t\t\t}\n\t\t},\n\t},\n\n\tcreated() {\n\t\t// Seed the selection: existing workspace value → content.default → the\n\t\t// \"All\" clear option (empty) when present, else the first option once\n\t\t// loaded (handled in loadOptions).\n\t\tconst existing = this.readWorkspace()\n\t\tif (existing !== undefined) {\n\t\t\tthis.selected = existing\n\t\t} else if (this.content.default !== undefined) {\n\t\t\tthis.selected = this.content.default\n\t\t\tthis.writeWorkspace(this.content.default)\n\t\t}\n\t\tthis.loadOptions()\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Normalise a raw option into `{ value, label, count? }`. Accepts\n\t\t * `{ value, label, count }`, `{ id, name }`, `{ key, count }` (OR\n\t\t * facet), or a bare string/number.\n\t\t *\n\t\t * @param {unknown} o The raw option.\n\t\t * @return {{value: unknown, label: string, count?: number}}\n\t\t */\n\t\tnormaliseOption(o) {\n\t\t\tif (o === null || o === undefined) {\n\t\t\t\treturn { value: undefined, label: '' }\n\t\t\t}\n\t\t\tif (typeof o !== 'object') {\n\t\t\t\treturn { value: o, label: String(o) }\n\t\t\t}\n\t\t\tconst value = o.value !== undefined ? o.value : (o.id !== undefined ? o.id : o.key)\n\t\t\tconst label = o.label !== undefined ? o.label : (o.name !== undefined ? o.name : String(value))\n\t\t\tconst out = { value, label: String(label) }\n\t\t\tif (o.count !== undefined) {\n\t\t\t\tout.count = o.count\n\t\t\t}\n\t\t\treturn out\n\t\t},\n\n\t\t/**\n\t\t * Load options for the active source (static content.options need no\n\t\t * fetch). Seeds the first option as the selection when nothing is set\n\t\t * and no \"All\" clear option exists.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync loadOptions() {\n\t\t\tif (this.content.endpointSource && this.content.endpointSource.url) {\n\t\t\t\tawait this.loadFromEndpoint()\n\t\t\t} else if (this.content.source && this.content.source.schema) {\n\t\t\t\tawait this.loadFromOrSource()\n\t\t\t} else {\n\t\t\t\tthis.fetchedOptions = null\n\t\t\t}\n\t\t\t// Default selection: when nothing is selected and there's no \"All\"\n\t\t\t// clear affordance, adopt the first option so siblings have a value.\n\t\t\tif ((this.selected === undefined || this.selected === '') && !this.content.allLabel) {\n\t\t\t\tconst first = this.normalisedOptions[0]\n\t\t\t\tif (first && this.readWorkspace() === undefined && this.content.default === undefined) {\n\t\t\t\t\tthis.selected = first.value\n\t\t\t\t\tthis.writeWorkspace(first.value)\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Fetch options from an `endpointSource` (array of options at\n\t\t * responsePath). Fail-safe: a failed fetch leaves an empty list.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync loadFromEndpoint() {\n\t\t\tthis.loading = true\n\t\t\ttry {\n\t\t\t\tconst payload = await fetchEndpointSource(this.content.endpointSource, {\n\t\t\t\t\tworkspace: this.readWorkspaceBag(),\n\t\t\t\t})\n\t\t\t\tthis.fetchedOptions = Array.isArray(payload) ? payload : []\n\t\t\t} catch {\n\t\t\t\tthis.fetchedOptions = []\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Fetch options from an OpenRegister `/grouped` facet — each distinct\n\t\t * `groupBy` value becomes an option with its object count. Fail-safe.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync loadFromOrSource() {\n\t\t\tconst src = this.content.source\n\t\t\tthis.loading = true\n\t\t\ttry {\n\t\t\t\tconst [{ default: axios }, { generateUrl }] = await Promise.all([\n\t\t\t\t\timport('@nextcloud/axios'),\n\t\t\t\t\timport('@nextcloud/router'),\n\t\t\t\t])\n\t\t\t\tconst url = generateUrl(\n\t\t\t\t\t'/apps/openregister/api/objects/aggregations/{register}/{schema}/grouped',\n\t\t\t\t\t{ register: src.register, schema: src.schema },\n\t\t\t\t)\n\t\t\t\tconst params = { groupBy: src.groupBy, metric: 'count' }\n\t\t\t\tconst filter = resolveFilterTokens(src.filter || {}, { workspace: this.readWorkspaceBag() })\n\t\t\t\tfor (const [k, v] of Object.entries(filter)) {\n\t\t\t\t\tif (v !== '' && v !== null && v !== undefined) {\n\t\t\t\t\t\tparams[`filter[${k}]`] = v\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tconst res = await axios.get(url, { params })\n\t\t\t\tconst groups = (res && res.data && res.data.groups) || []\n\t\t\t\tthis.fetchedOptions = groups\n\t\t\t\t\t.filter((g) => g.key !== null && g.key !== undefined && g.key !== '')\n\t\t\t\t\t.map((g) => ({ value: g.key, label: String(g.key), count: Number(g.value) || 0 }))\n\t\t\t} catch {\n\t\t\t\tthis.fetchedOptions = []\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Whether an option is the active selection.\n\t\t *\n\t\t * @param {object} option The option.\n\t\t * @return {boolean}\n\t\t */\n\t\tisActive(option) {\n\t\t\treturn String(option.value) === String(this.selected ?? '')\n\t\t},\n\n\t\t/**\n\t\t * Radio-list pick handler.\n\t\t *\n\t\t * @param {object} option The picked option.\n\t\t * @return {void}\n\t\t */\n\t\tonRadioPick(option) {\n\t\t\tthis.applySelection(option.value)\n\t\t},\n\n\t\t/**\n\t\t * NcSelect input handler.\n\t\t *\n\t\t * @param {object|null} option The picked option object.\n\t\t * @return {void}\n\t\t */\n\t\tonSelect(option) {\n\t\t\tthis.applySelection(option ? option.value : '')\n\t\t},\n\n\t\t/**\n\t\t * Apply a new selection: mirror it locally, write it into the workspace\n\t\t * context (so sibling widgets refetch), and emit `@change`.\n\t\t *\n\t\t * @param {unknown} value The new value.\n\t\t * @return {void}\n\t\t */\n\t\tapplySelection(value) {\n\t\t\tthis.selected = value\n\t\t\tthis.writeWorkspace(value)\n\t\t\t/**\n\t\t\t * @event change Emitted when the selection changes. Payload: `{ key, value }`.\n\t\t\t */\n\t\t\tthis.$emit('change', { key: this.writeKey, value })\n\t\t},\n\n\t\t/**\n\t\t * The unwrapped workspace bag (or `{}`).\n\t\t *\n\t\t * @return {object}\n\t\t */\n\t\treadWorkspaceBag() {\n\t\t\tconst holder = this.cnWorkspaceContext\n\t\t\tif (!holder || typeof holder !== 'object') {\n\t\t\t\treturn {}\n\t\t\t}\n\t\t\treturn ('value' in holder) ? (holder.value || {}) : holder\n\t\t},\n\n\t\t/**\n\t\t * Read the current workspace value for this widget's key (undefined\n\t\t * when unset).\n\t\t *\n\t\t * @return {unknown}\n\t\t */\n\t\treadWorkspace() {\n\t\t\tif (!this.writeKey) {\n\t\t\t\treturn undefined\n\t\t\t}\n\t\t\tconst bag = this.readWorkspaceBag()\n\t\t\treturn bag[this.writeKey]\n\t\t},\n\n\t\t/**\n\t\t * Write a value into the reactive workspace bag under this widget's key\n\t\t * (the CnInteractionFormWidget write pattern — replace-in-place for the\n\t\t * unwrapped bag, `.value` for the raw-ref shape).\n\t\t *\n\t\t * @param {unknown} value The value to write.\n\t\t * @return {void}\n\t\t */\n\t\twriteWorkspace(value) {\n\t\t\tif (!this.writeKey) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst holder = this.cnWorkspaceContext\n\t\t\tif (!holder || typeof holder !== 'object') {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif ('value' in holder) {\n\t\t\t\tholder.value = { ...(holder.value || {}), [this.writeKey]: value }\n\t\t\t\treturn\n\t\t\t}\n\t\t\tholder[this.writeKey] = value\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-workspace-filter-widget {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 8px;\n\twidth: 100%;\n}\n\n.cn-workspace-filter-widget__label {\n\tmargin: 0;\n\tfont-weight: 600;\n\tcolor: var(--color-main-text);\n}\n\n.cn-workspace-filter-widget__status {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.9em;\n}\n\n.cn-workspace-filter-widget__list {\n\tlist-style: none;\n\tmargin: 0;\n\tpadding: 0;\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 2px;\n}\n\n.cn-workspace-filter-widget__count {\n\tmargin-inline-start: 6px;\n\tpadding: 0 6px;\n\tborder-radius: 999px;\n\tbackground: var(--color-background-hover);\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.85em;\n}\n</style>\n"],"names":["t"],"mappings":"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"}
1
+ {"version":3,"file":"CnWorkspaceFilterWidget.vue2.js","sources":["../../../../src/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n\n<template>\n\t<div\n\t\tclass=\"cn-workspace-filter-widget\"\n\t\t:class=\"{ 'cn-workspace-filter-widget--start': isStart }\"\n\t\tdata-testid=\"cn-workspace-filter-widget\">\n\t\t<!-- The start card (`layout: \"start\"`): a title and one line of\n\t\t guidance beside the control, as the \"Contact starten\" card. -->\n\t\t<div v-if=\"isStart && (title || description)\" class=\"cn-workspace-filter-widget__text\">\n\t\t\t<h3 v-if=\"title\" class=\"cn-workspace-filter-widget__title\">\n\t\t\t\t{{ title }}\n\t\t\t</h3>\n\t\t\t<p v-if=\"description\" class=\"cn-workspace-filter-widget__description\">\n\t\t\t\t{{ description }}\n\t\t\t</p>\n\t\t</div>\n\n\t\t<p v-if=\"label\" class=\"cn-workspace-filter-widget__label\">\n\t\t\t{{ label }}\n\t\t</p>\n\n\t\t<!-- An object picker (`objectSource`): searches OpenRegister on the\n\t\t server, so it reaches every object, not a fetched page. -->\n\t\t<CnResourceSelect\n\t\t\tv-if=\"objectSource\"\n\t\t\tclass=\"cn-workspace-filter-widget__control\"\n\t\t\t:register=\"objectSource.register\"\n\t\t\t:schema=\"objectSource.schema\"\n\t\t\t:labelField=\"objectSource.labelField || 'title'\"\n\t\t\t:modelValue=\"selected === undefined || selected === null ? '' : String(selected)\"\n\t\t\t:placeholder=\"tr(objectSource.placeholder || '')\"\n\t\t\t:ariaLabelCombobox=\"tr(objectSource.ariaLabel || '') || title || label || writeKey\"\n\t\t\t:preload=\"objectSource.preload !== false\"\n\t\t\t:clearable=\"Boolean(content.allLabel)\"\n\t\t\t:allowCreate=\"false\"\n\t\t\tdata-testid=\"cn-workspace-filter-object\"\n\t\t\t@update:modelValue=\"applySelection($event || '')\" />\n\n\t\t<div v-else-if=\"loading\" class=\"cn-workspace-filter-widget__status\">\n\t\t\t{{ loadingLabel }}\n\t\t</div>\n\n\t\t<!-- Select style -->\n\t\t<NcSelect\n\t\t\tv-else-if=\"style === 'select'\"\n\t\t\t:modelValue=\"selectedOption\"\n\t\t\t:options=\"displayOptions\"\n\t\t\t:clearable=\"false\"\n\t\t\t:inputLabel=\"label || writeKey\"\n\t\t\tlabel=\"label\"\n\t\t\tdata-testid=\"cn-workspace-filter-select\"\n\t\t\t@update:modelValue=\"onSelect\" />\n\n\t\t<!-- Radio-list style (default) -->\n\t\t<ul v-else\n\t\t\tclass=\"cn-workspace-filter-widget__list\"\n\t\t\trole=\"radiogroup\"\n\t\t\t:aria-label=\"label || writeKey\">\n\t\t\t<li\n\t\t\t\tv-for=\"option in displayOptions\"\n\t\t\t\t:key=\"String(option.value)\"\n\t\t\t\tclass=\"cn-workspace-filter-widget__item\">\n\t\t\t\t<NcCheckboxRadioSwitch\n\t\t\t\t\ttype=\"radio\"\n\t\t\t\t\t:name=\"radioGroupName\"\n\t\t\t\t\t:modelValue=\"isActive(option) ? String(option.value) : ''\"\n\t\t\t\t\t:value=\"String(option.value)\"\n\t\t\t\t\t:data-testid=\"`cn-workspace-filter-option-${option.value}`\"\n\t\t\t\t\t@update:modelValue=\"onRadioPick(option)\">\n\t\t\t\t\t<span class=\"cn-workspace-filter-widget__option-label\">{{ option.label }}</span>\n\t\t\t\t\t<span\n\t\t\t\t\t\tv-if=\"showCounts && option.count !== undefined && option.count !== null\"\n\t\t\t\t\t\tclass=\"cn-workspace-filter-widget__count\">{{ option.count }}</span>\n\t\t\t\t</NcCheckboxRadioSwitch>\n\t\t\t</li>\n\t\t</ul>\n\n\t\t<component\n\t\t\t:is=\"startLink.to ? 'router-link' : 'a'\"\n\t\t\tv-if=\"startLink\"\n\t\t\t:to=\"startLink.to || undefined\"\n\t\t\t:href=\"startLink.href || undefined\"\n\t\t\tclass=\"cn-workspace-filter-widget__link\"\n\t\t\tdata-testid=\"cn-workspace-filter-link\">\n\t\t\t{{ startLink.label }}\n\t\t</component>\n\t</div>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcCheckboxRadioSwitch, NcSelect } from '@nextcloud/vue'\nimport CnResourceSelect from '../CnResourceSelect/CnResourceSelect.vue'\nimport { fetchEndpointSource } from '../../composables/useEndpointSource.js'\nimport { resolveFilterTokens } from '../../utils/resolveFilterTokens.js'\n\n/**\n * CnWorkspaceFilterWidget — a dashboard choice-list that writes the selected\n * value into the page workspace context so sibling widgets refetch (#91\n * Wave 3, the `workspace-filter` widget).\n *\n * Options come from ONE of three sources (checked in order):\n * 1. static `options[]` — each `{ value, label, count? }`.\n * 2. an OpenRegister `source` — `{ register, schema, groupBy, filter? }`,\n * fetched from OR's `/grouped` facet so each distinct value becomes an\n * option with its object `count`.\n * 3. an `endpointSource` — `{ url, params?, responsePath? }` returning an\n * array of `{ value, label, count? }` (or `{ id, name }` / bare strings,\n * normalised).\n *\n * The chosen value is written to the workspace context key named by `writes`\n * (`\"@workspace.<key>\"` or a bare `\"<key>\"`). CnDashboardPage provides\n * `cnWorkspaceContext`; every sibling widget whose declarative source\n * interpolates `@workspace.<key>` re-resolves its tokens and refetches when\n * the selection changes — the pipelinq WerkplekQueueFilter contract.\n *\n * An optional leading \"All\" option (`allLabel`) clears the key (writes the\n * empty string), so an optional `@workspace.<key>?` token drops and siblings\n * show the unfiltered set.\n *\n * ```js\n * content: {\n * label: 'Queue',\n * writes: '@workspace.queue',\n * style: 'radio', // 'radio' (default) | 'select'\n * allLabel: 'All',\n * showCounts: true,\n * source: { register: 'pipelinq', schema: 'werkitem', groupBy: 'queue' },\n * }\n * ```\n */\nexport default {\n\tname: 'CnWorkspaceFilterWidget',\n\n\tcomponents: { CnResourceSelect, NcSelect, NcCheckboxRadioSwitch },\n\n\tinject: {\n\t\t/**\n\t\t * Page-level workspace context. CnDashboardPage provides a `ref({})`;\n\t\t * Vue 2.7's Options-API inject AUTO-UNWRAPS it, so this is the plain\n\t\t * reactive bag (the raw-ref `{ value }` shape is also handled on write).\n\t\t * Null on pages without one (the widget then just emits `@change`).\n\t\t */\n\t\tcnWorkspaceContext: { default: null },\n\t\t/** Host translate function, for the start card's texts. */\n\t\tcnTranslate: { default: () => (key) => key },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * Persisted configuration blob.\n\t\t *\n\t\t * Start card keys (abstract-start-card): `layout: \"start\"` draws\n\t\t * `title` and `description` beside the control, and `link`\n\t\t * (`{ label, route? | href? }`) after it. `objectSource`\n\t\t * (`{ register, schema, labelField?, placeholder?, ariaLabel?, preload? }`)\n\t\t * makes the control an OpenRegister object picker that searches on the\n\t\t * server and writes the picked object's id; without a selection and\n\t\t * without `allLabel` it selects the first object.\n\t\t *\n\t\t * @type {{label?: string, writes?: string, style?: string, allLabel?: string, showCounts?: boolean, options?: Array, source?: object, endpointSource?: object, objectSource?: object, default?: (string|number), layout?: string, title?: string, description?: string, link?: object}}\n\t\t */\n\t\tcontent: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\t},\n\n\temits: ['change'],\n\n\tdata() {\n\t\treturn {\n\t\t\t/** Fetched options (from source / endpointSource); static options use content.options directly. */\n\t\t\tfetchedOptions: null,\n\t\t\t/** True while options are being fetched. */\n\t\t\tloading: false,\n\t\t\t/** The currently-selected value (mirrors the workspace key). */\n\t\t\tselected: undefined,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/** Section label. */\n\t\tlabel() {\n\t\t\treturn this.content.label || ''\n\t\t},\n\n\t\t/** Whether the widget draws the start card. */\n\t\tisStart() {\n\t\t\treturn this.content.layout === 'start'\n\t\t},\n\n\t\t/** The start card's title, translated. */\n\t\ttitle() {\n\t\t\treturn this.tr(this.content.title || '')\n\t\t},\n\n\t\t/** The start card's guidance line, translated. */\n\t\tdescription() {\n\t\t\treturn this.tr(this.content.description || '')\n\t\t},\n\n\t\t/**\n\t\t * The link after the control, or null.\n\t\t *\n\t\t * @return {{label: string, to?: object, href?: string}|null}\n\t\t */\n\t\tstartLink() {\n\t\t\tconst link = this.content.link\n\t\t\tif (!link || typeof link !== 'object' || !link.label) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tif (link.route && typeof link.route === 'object' && link.route.name) {\n\t\t\t\treturn { label: this.tr(link.label), to: link.route }\n\t\t\t}\n\t\t\tif (typeof link.href === 'string' && link.href !== '') {\n\t\t\t\treturn { label: this.tr(link.label), href: link.href }\n\t\t\t}\n\t\t\treturn null\n\t\t},\n\n\t\t/**\n\t\t * The object picker declaration, or null.\n\t\t *\n\t\t * @return {{register: string, schema: string, labelField?: string}|null}\n\t\t */\n\t\tobjectSource() {\n\t\t\tconst src = this.content.objectSource\n\t\t\treturn src && typeof src === 'object' && src.register && src.schema ? src : null\n\t\t},\n\n\t\t/** Choice style: 'select' or 'radio' (default). */\n\t\tstyle() {\n\t\t\treturn this.content.style === 'select' ? 'select' : 'radio'\n\t\t},\n\n\t\t/** Whether to render per-option counts. */\n\t\tshowCounts() {\n\t\t\treturn this.content.showCounts !== false\n\t\t},\n\n\t\t/** The bare workspace key the selection writes (strips a `@workspace.` / `@page.` prefix). */\n\t\twriteKey() {\n\t\t\tconst w = this.content.writes || ''\n\t\t\treturn String(w).replace(/^@(workspace|page)\\./, '')\n\t\t},\n\n\t\t/** A stable radiogroup name so the radios are mutually exclusive. */\n\t\tradioGroupName() {\n\t\t\treturn `cn-wsf-${this.writeKey || 'filter'}`\n\t\t},\n\n\t\t/** The option list actually rendered — the optional \"All\" entry prepended. */\n\t\tdisplayOptions() {\n\t\t\tconst base = this.normalisedOptions\n\t\t\tif (this.content.allLabel) {\n\t\t\t\treturn [{ value: '', label: this.content.allLabel }, ...base]\n\t\t\t}\n\t\t\treturn base\n\t\t},\n\n\t\t/** Normalised `{ value, label, count? }` options from whichever source is configured. */\n\t\tnormalisedOptions() {\n\t\t\tconst raw = this.fetchedOptions !== null\n\t\t\t\t? this.fetchedOptions\n\t\t\t\t: (Array.isArray(this.content.options) ? this.content.options : [])\n\t\t\treturn raw.map((o) => this.normaliseOption(o)).filter((o) => o.value !== undefined && o.value !== null)\n\t\t},\n\n\t\t/** The selected option object (for NcSelect). */\n\t\tselectedOption() {\n\t\t\treturn this.displayOptions.find((o) => String(o.value) === String(this.selected)) || null\n\t\t},\n\n\t\t/**\n\t\t * The reactive workspace value for this key — watched so an external\n\t\t * write (another widget) syncs the local selection.\n\t\t *\n\t\t * @return {unknown}\n\t\t */\n\t\tworkspaceValue() {\n\t\t\treturn this.readWorkspace()\n\t\t},\n\n\t\tloadingLabel() {\n\t\t\treturn t('nextcloud-vue', 'Loading…')\n\t\t},\n\t},\n\n\twatch: {\n\t\t/** Re-fetch options when a dynamic source changes. */\n\t\tcontent: {\n\t\t\tdeep: true,\n\t\t\thandler() {\n\t\t\t\tthis.loadOptions()\n\t\t\t},\n\t\t},\n\n\t\t/**\n\t\t * Track external workspace changes (another widget writing the same key).\n\t\t *\n\t\t * @param {unknown} val The new workspace value for this key.\n\t\t */\n\t\tworkspaceValue(val) {\n\t\t\tif (val !== undefined) {\n\t\t\t\tthis.selected = val\n\t\t\t}\n\t\t},\n\t},\n\n\tcreated() {\n\t\t// Seed the selection: existing workspace value → content.default → the\n\t\t// \"All\" clear option (empty) when present, else the first option once\n\t\t// loaded (handled in loadOptions).\n\t\tconst existing = this.readWorkspace()\n\t\tif (existing !== undefined) {\n\t\t\tthis.selected = existing\n\t\t} else if (this.content.default !== undefined) {\n\t\t\tthis.selected = this.content.default\n\t\t\tthis.writeWorkspace(this.content.default)\n\t\t}\n\t\tthis.loadOptions()\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Normalise a raw option into `{ value, label, count? }`. Accepts\n\t\t * `{ value, label, count }`, `{ id, name }`, `{ key, count }` (OR\n\t\t * facet), or a bare string/number.\n\t\t *\n\t\t * @param {unknown} o The raw option.\n\t\t * @return {{value: unknown, label: string, count?: number}}\n\t\t */\n\t\tnormaliseOption(o) {\n\t\t\tif (o === null || o === undefined) {\n\t\t\t\treturn { value: undefined, label: '' }\n\t\t\t}\n\t\t\tif (typeof o !== 'object') {\n\t\t\t\treturn { value: o, label: String(o) }\n\t\t\t}\n\t\t\tconst value = o.value !== undefined ? o.value : (o.id !== undefined ? o.id : o.key)\n\t\t\tconst label = o.label !== undefined ? o.label : (o.name !== undefined ? o.name : String(value))\n\t\t\tconst out = { value, label: String(label) }\n\t\t\tif (o.count !== undefined) {\n\t\t\t\tout.count = o.count\n\t\t\t}\n\t\t\treturn out\n\t\t},\n\n\t\t/**\n\t\t * Load options for the active source (static content.options need no\n\t\t * fetch). Seeds the first option as the selection when nothing is set\n\t\t * and no \"All\" clear option exists.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync loadOptions() {\n\t\t\tif (this.objectSource) {\n\t\t\t\tawait this.seedFirstObject()\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (this.content.endpointSource && this.content.endpointSource.url) {\n\t\t\t\tawait this.loadFromEndpoint()\n\t\t\t} else if (this.content.source && this.content.source.schema) {\n\t\t\t\tawait this.loadFromOrSource()\n\t\t\t} else {\n\t\t\t\tthis.fetchedOptions = null\n\t\t\t}\n\t\t\t// Default selection: when nothing is selected and there's no \"All\"\n\t\t\t// clear affordance, adopt the first option so siblings have a value.\n\t\t\tif ((this.selected === undefined || this.selected === '') && !this.content.allLabel) {\n\t\t\t\tconst first = this.normalisedOptions[0]\n\t\t\t\tif (first && this.readWorkspace() === undefined && this.content.default === undefined) {\n\t\t\t\t\tthis.selected = first.value\n\t\t\t\t\tthis.writeWorkspace(first.value)\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Translate a manifest string through the host app.\n\t\t *\n\t\t * @param {string} text The text.\n\t\t * @return {string}\n\t\t */\n\t\ttr(text) {\n\t\t\tif (typeof text !== 'string' || text === '') {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn typeof this.cnTranslate === 'function' ? this.cnTranslate(text) : text\n\t\t},\n\n\t\t/**\n\t\t * The object picker's default: with nothing selected, no `default` and\n\t\t * no `allLabel`, select the first object the source lists, so the page\n\t\t * always has one in focus. Fail-safe: a failed read selects nothing.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync seedFirstObject() {\n\t\t\tif ((this.selected !== undefined && this.selected !== '') || this.content.allLabel || this.content.default !== undefined) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst src = this.objectSource\n\t\t\ttry {\n\t\t\t\tconst [{ default: axios }, { generateUrl }] = await Promise.all([\n\t\t\t\t\timport('@nextcloud/axios'),\n\t\t\t\t\timport('@nextcloud/router'),\n\t\t\t\t])\n\t\t\t\tconst url = generateUrl('/apps/openregister/api/objects/{register}/{schema}', { register: src.register, schema: src.schema })\n\t\t\t\tconst res = await axios.get(url, { params: { _limit: 1 } })\n\t\t\t\tconst data = res && res.data\n\t\t\t\tconst rows = Array.isArray(data) ? data : ((data && data.results) || [])\n\t\t\t\tconst first = rows[0]\n\t\t\t\tconst id = first && (first.id || (first['@self'] && first['@self'].id))\n\t\t\t\tif (id && (this.selected === undefined || this.selected === '') && this.readWorkspace() === undefined) {\n\t\t\t\t\tthis.selected = String(id)\n\t\t\t\t\tthis.writeWorkspace(String(id))\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// Nothing selected: the siblings wait for a pick.\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Fetch options from an `endpointSource` (array of options at\n\t\t * responsePath). Fail-safe: a failed fetch leaves an empty list.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync loadFromEndpoint() {\n\t\t\tthis.loading = true\n\t\t\ttry {\n\t\t\t\tconst payload = await fetchEndpointSource(this.content.endpointSource, {\n\t\t\t\t\tworkspace: this.readWorkspaceBag(),\n\t\t\t\t})\n\t\t\t\tthis.fetchedOptions = Array.isArray(payload) ? payload : []\n\t\t\t} catch {\n\t\t\t\tthis.fetchedOptions = []\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Fetch options from an OpenRegister `/grouped` facet — each distinct\n\t\t * `groupBy` value becomes an option with its object count. Fail-safe.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync loadFromOrSource() {\n\t\t\tconst src = this.content.source\n\t\t\tthis.loading = true\n\t\t\ttry {\n\t\t\t\tconst [{ default: axios }, { generateUrl }] = await Promise.all([\n\t\t\t\t\timport('@nextcloud/axios'),\n\t\t\t\t\timport('@nextcloud/router'),\n\t\t\t\t])\n\t\t\t\tconst url = generateUrl(\n\t\t\t\t\t'/apps/openregister/api/objects/aggregations/{register}/{schema}/grouped',\n\t\t\t\t\t{ register: src.register, schema: src.schema },\n\t\t\t\t)\n\t\t\t\tconst params = { groupBy: src.groupBy, metric: 'count' }\n\t\t\t\tconst filter = resolveFilterTokens(src.filter || {}, { workspace: this.readWorkspaceBag() })\n\t\t\t\tfor (const [k, v] of Object.entries(filter)) {\n\t\t\t\t\tif (v !== '' && v !== null && v !== undefined) {\n\t\t\t\t\t\tparams[`filter[${k}]`] = v\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tconst res = await axios.get(url, { params })\n\t\t\t\tconst groups = (res && res.data && res.data.groups) || []\n\t\t\t\tthis.fetchedOptions = groups\n\t\t\t\t\t.filter((g) => g.key !== null && g.key !== undefined && g.key !== '')\n\t\t\t\t\t.map((g) => ({ value: g.key, label: String(g.key), count: Number(g.value) || 0 }))\n\t\t\t} catch {\n\t\t\t\tthis.fetchedOptions = []\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Whether an option is the active selection.\n\t\t *\n\t\t * @param {object} option The option.\n\t\t * @return {boolean}\n\t\t */\n\t\tisActive(option) {\n\t\t\treturn String(option.value) === String(this.selected ?? '')\n\t\t},\n\n\t\t/**\n\t\t * Radio-list pick handler.\n\t\t *\n\t\t * @param {object} option The picked option.\n\t\t * @return {void}\n\t\t */\n\t\tonRadioPick(option) {\n\t\t\tthis.applySelection(option.value)\n\t\t},\n\n\t\t/**\n\t\t * NcSelect input handler.\n\t\t *\n\t\t * @param {object|null} option The picked option object.\n\t\t * @return {void}\n\t\t */\n\t\tonSelect(option) {\n\t\t\tthis.applySelection(option ? option.value : '')\n\t\t},\n\n\t\t/**\n\t\t * Apply a new selection: mirror it locally, write it into the workspace\n\t\t * context (so sibling widgets refetch), and emit `@change`.\n\t\t *\n\t\t * @param {unknown} value The new value.\n\t\t * @return {void}\n\t\t */\n\t\tapplySelection(value) {\n\t\t\tthis.selected = value\n\t\t\tthis.writeWorkspace(value)\n\t\t\t/**\n\t\t\t * @event change Emitted when the selection changes. Payload: `{ key, value }`.\n\t\t\t */\n\t\t\tthis.$emit('change', { key: this.writeKey, value })\n\t\t},\n\n\t\t/**\n\t\t * The unwrapped workspace bag (or `{}`).\n\t\t *\n\t\t * @return {object}\n\t\t */\n\t\treadWorkspaceBag() {\n\t\t\tconst holder = this.cnWorkspaceContext\n\t\t\tif (!holder || typeof holder !== 'object') {\n\t\t\t\treturn {}\n\t\t\t}\n\t\t\treturn ('value' in holder) ? (holder.value || {}) : holder\n\t\t},\n\n\t\t/**\n\t\t * Read the current workspace value for this widget's key (undefined\n\t\t * when unset).\n\t\t *\n\t\t * @return {unknown}\n\t\t */\n\t\treadWorkspace() {\n\t\t\tif (!this.writeKey) {\n\t\t\t\treturn undefined\n\t\t\t}\n\t\t\tconst bag = this.readWorkspaceBag()\n\t\t\treturn bag[this.writeKey]\n\t\t},\n\n\t\t/**\n\t\t * Write a value into the reactive workspace bag under this widget's key\n\t\t * (the CnInteractionFormWidget write pattern — replace-in-place for the\n\t\t * unwrapped bag, `.value` for the raw-ref shape).\n\t\t *\n\t\t * @param {unknown} value The value to write.\n\t\t * @return {void}\n\t\t */\n\t\twriteWorkspace(value) {\n\t\t\tif (!this.writeKey) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst holder = this.cnWorkspaceContext\n\t\t\tif (!holder || typeof holder !== 'object') {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif ('value' in holder) {\n\t\t\t\tholder.value = { ...(holder.value || {}), [this.writeKey]: value }\n\t\t\t\treturn\n\t\t\t}\n\t\t\tholder[this.writeKey] = value\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-workspace-filter-widget {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 8px;\n\twidth: 100%;\n}\n\n.cn-workspace-filter-widget__label {\n\tmargin: 0;\n\tfont-weight: 600;\n\tcolor: var(--color-main-text);\n}\n\n.cn-workspace-filter-widget__status {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.9em;\n}\n\n.cn-workspace-filter-widget__list {\n\tlist-style: none;\n\tmargin: 0;\n\tpadding: 0;\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 2px;\n}\n\n/* The start card: text left, control (and link) right, as PqDashboard's \"Contact starten\". */\n.cn-workspace-filter-widget--start {\n\tflex-direction: row;\n\tflex-wrap: wrap;\n\talign-items: center;\n\tgap: 16px 24px;\n}\n\n.cn-workspace-filter-widget__text {\n\tflex: 1 1 320px;\n\tmin-width: 0;\n}\n\n.cn-workspace-filter-widget__title {\n\tmargin: 0 0 4px;\n\tfont-size: 17px;\n\tfont-weight: 700;\n\tcolor: var(--color-main-text);\n}\n\n.cn-workspace-filter-widget__description {\n\tmargin: 0;\n\tmax-width: 420px;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-workspace-filter-widget--start .cn-workspace-filter-widget__control,\n.cn-workspace-filter-widget--start > .v-select {\n\tflex: 0 1 480px;\n\tmin-width: 240px;\n}\n\n.cn-workspace-filter-widget__link {\n\tfont-weight: 600;\n\tcolor: var(--color-primary-element);\n\ttext-decoration: underline;\n\twhite-space: nowrap;\n}\n\n.cn-workspace-filter-widget__count {\n\tmargin-inline-start: 6px;\n\tpadding: 0 6px;\n\tborder-radius: 999px;\n\tbackground: var(--color-background-hover);\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.85em;\n}\n</style>\n"],"names":["CnResourceSelect","t"],"mappings":"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"}
@@ -1,76 +1,128 @@
1
- import { resolveComponent, openBlock, createElementBlock, toDisplayString, createCommentVNode, createBlock, Fragment, renderList, createVNode, withCtx, createElementVNode } from 'vue';
1
+ import { resolveComponent, openBlock, createElementBlock, normalizeClass, toDisplayString, createCommentVNode, createBlock, Fragment, renderList, createVNode, withCtx, createElementVNode, resolveDynamicComponent, createTextVNode } from 'vue';
2
2
 
3
3
  const _hoisted_1 = {
4
- class: "cn-workspace-filter-widget",
5
- "data-testid": "cn-workspace-filter-widget"
4
+ key: 0,
5
+ class: "cn-workspace-filter-widget__text"
6
6
  };
7
7
  const _hoisted_2 = {
8
8
  key: 0,
9
- class: "cn-workspace-filter-widget__label"
9
+ class: "cn-workspace-filter-widget__title"
10
10
  };
11
11
  const _hoisted_3 = {
12
12
  key: 1,
13
+ class: "cn-workspace-filter-widget__description"
14
+ };
15
+ const _hoisted_4 = {
16
+ key: 1,
17
+ class: "cn-workspace-filter-widget__label"
18
+ };
19
+ const _hoisted_5 = {
20
+ key: 3,
13
21
  class: "cn-workspace-filter-widget__status"
14
22
  };
15
- const _hoisted_4 = ["aria-label"];
16
- const _hoisted_5 = { class: "cn-workspace-filter-widget__option-label" };
17
- const _hoisted_6 = {
23
+ const _hoisted_6 = ["aria-label"];
24
+ const _hoisted_7 = { class: "cn-workspace-filter-widget__option-label" };
25
+ const _hoisted_8 = {
18
26
  key: 0,
19
27
  class: "cn-workspace-filter-widget__count"
20
28
  };
21
29
 
22
30
  function render(_ctx, _cache, $props, $setup, $data, $options) {
31
+ const _component_CnResourceSelect = resolveComponent("CnResourceSelect");
23
32
  const _component_NcSelect = resolveComponent("NcSelect");
24
33
  const _component_NcCheckboxRadioSwitch = resolveComponent("NcCheckboxRadioSwitch");
25
34
 
26
- return (openBlock(), createElementBlock("div", _hoisted_1, [
35
+ return (openBlock(), createElementBlock("div", {
36
+ class: normalizeClass(["cn-workspace-filter-widget", { 'cn-workspace-filter-widget--start': $options.isStart }]),
37
+ "data-testid": "cn-workspace-filter-widget"
38
+ }, [
39
+ ($options.isStart && ($options.title || $options.description))
40
+ ? (openBlock(), createElementBlock("div", _hoisted_1, [
41
+ ($options.title)
42
+ ? (openBlock(), createElementBlock("h3", _hoisted_2, toDisplayString($options.title), 1 /* TEXT */))
43
+ : createCommentVNode("v-if", true),
44
+ ($options.description)
45
+ ? (openBlock(), createElementBlock("p", _hoisted_3, toDisplayString($options.description), 1 /* TEXT */))
46
+ : createCommentVNode("v-if", true)
47
+ ]))
48
+ : createCommentVNode("v-if", true),
27
49
  ($options.label)
28
- ? (openBlock(), createElementBlock("p", _hoisted_2, toDisplayString($options.label), 1 /* TEXT */))
50
+ ? (openBlock(), createElementBlock("p", _hoisted_4, toDisplayString($options.label), 1 /* TEXT */))
29
51
  : createCommentVNode("v-if", true),
30
- ($data.loading)
31
- ? (openBlock(), createElementBlock("div", _hoisted_3, toDisplayString($options.loadingLabel), 1 /* TEXT */))
32
- : ($options.style === 'select')
33
- ? (openBlock(), createBlock(_component_NcSelect, {
34
- key: 2,
35
- modelValue: $options.selectedOption,
36
- options: $options.displayOptions,
37
- clearable: false,
38
- inputLabel: $options.label || $options.writeKey,
39
- label: "label",
40
- "data-testid": "cn-workspace-filter-select",
41
- "onUpdate:modelValue": $options.onSelect
42
- }, null, 8 /* PROPS */, ["modelValue", "options", "inputLabel", "onUpdate:modelValue"]))
43
- : (openBlock(), createElementBlock("ul", {
44
- key: 3,
45
- class: "cn-workspace-filter-widget__list",
46
- role: "radiogroup",
47
- "aria-label": $options.label || $options.writeKey
48
- }, [
49
- (openBlock(true), createElementBlock(Fragment, null, renderList($options.displayOptions, (option) => {
50
- return (openBlock(), createElementBlock("li", {
51
- key: String(option.value),
52
- class: "cn-workspace-filter-widget__item"
53
- }, [
54
- createVNode(_component_NcCheckboxRadioSwitch, {
55
- type: "radio",
56
- name: $options.radioGroupName,
57
- modelValue: $options.isActive(option) ? String(option.value) : '',
58
- value: String(option.value),
59
- "data-testid": `cn-workspace-filter-option-${option.value}`,
60
- "onUpdate:modelValue": $event => ($options.onRadioPick(option))
61
- }, {
62
- default: withCtx(() => [
63
- createElementVNode("span", _hoisted_5, toDisplayString(option.label), 1 /* TEXT */),
64
- ($options.showCounts && option.count !== undefined && option.count !== null)
65
- ? (openBlock(), createElementBlock("span", _hoisted_6, toDisplayString(option.count), 1 /* TEXT */))
66
- : createCommentVNode("v-if", true)
67
- ]),
68
- _: 2 /* DYNAMIC */
69
- }, 1032 /* PROPS, DYNAMIC_SLOTS */, ["name", "modelValue", "value", "data-testid", "onUpdate:modelValue"])
70
- ]))
71
- }), 128 /* KEYED_FRAGMENT */))
72
- ], 8 /* PROPS */, _hoisted_4))
73
- ]))
52
+ ($options.objectSource)
53
+ ? (openBlock(), createBlock(_component_CnResourceSelect, {
54
+ key: 2,
55
+ class: "cn-workspace-filter-widget__control",
56
+ register: $options.objectSource.register,
57
+ schema: $options.objectSource.schema,
58
+ labelField: $options.objectSource.labelField || 'title',
59
+ modelValue: $data.selected === undefined || $data.selected === null ? '' : String($data.selected),
60
+ placeholder: $options.tr($options.objectSource.placeholder || ''),
61
+ ariaLabelCombobox: $options.tr($options.objectSource.ariaLabel || '') || $options.title || $options.label || $options.writeKey,
62
+ preload: $options.objectSource.preload !== false,
63
+ clearable: Boolean($props.content.allLabel),
64
+ allowCreate: false,
65
+ "data-testid": "cn-workspace-filter-object",
66
+ "onUpdate:modelValue": _cache[0] || (_cache[0] = $event => ($options.applySelection($event || '')))
67
+ }, null, 8 /* PROPS */, ["register", "schema", "labelField", "modelValue", "placeholder", "ariaLabelCombobox", "preload", "clearable"]))
68
+ : ($data.loading)
69
+ ? (openBlock(), createElementBlock("div", _hoisted_5, toDisplayString($options.loadingLabel), 1 /* TEXT */))
70
+ : ($options.style === 'select')
71
+ ? (openBlock(), createBlock(_component_NcSelect, {
72
+ key: 4,
73
+ modelValue: $options.selectedOption,
74
+ options: $options.displayOptions,
75
+ clearable: false,
76
+ inputLabel: $options.label || $options.writeKey,
77
+ label: "label",
78
+ "data-testid": "cn-workspace-filter-select",
79
+ "onUpdate:modelValue": $options.onSelect
80
+ }, null, 8 /* PROPS */, ["modelValue", "options", "inputLabel", "onUpdate:modelValue"]))
81
+ : (openBlock(), createElementBlock("ul", {
82
+ key: 5,
83
+ class: "cn-workspace-filter-widget__list",
84
+ role: "radiogroup",
85
+ "aria-label": $options.label || $options.writeKey
86
+ }, [
87
+ (openBlock(true), createElementBlock(Fragment, null, renderList($options.displayOptions, (option) => {
88
+ return (openBlock(), createElementBlock("li", {
89
+ key: String(option.value),
90
+ class: "cn-workspace-filter-widget__item"
91
+ }, [
92
+ createVNode(_component_NcCheckboxRadioSwitch, {
93
+ type: "radio",
94
+ name: $options.radioGroupName,
95
+ modelValue: $options.isActive(option) ? String(option.value) : '',
96
+ value: String(option.value),
97
+ "data-testid": `cn-workspace-filter-option-${option.value}`,
98
+ "onUpdate:modelValue": $event => ($options.onRadioPick(option))
99
+ }, {
100
+ default: withCtx(() => [
101
+ createElementVNode("span", _hoisted_7, toDisplayString(option.label), 1 /* TEXT */),
102
+ ($options.showCounts && option.count !== undefined && option.count !== null)
103
+ ? (openBlock(), createElementBlock("span", _hoisted_8, toDisplayString(option.count), 1 /* TEXT */))
104
+ : createCommentVNode("v-if", true)
105
+ ]),
106
+ _: 2 /* DYNAMIC */
107
+ }, 1032 /* PROPS, DYNAMIC_SLOTS */, ["name", "modelValue", "value", "data-testid", "onUpdate:modelValue"])
108
+ ]))
109
+ }), 128 /* KEYED_FRAGMENT */))
110
+ ], 8 /* PROPS */, _hoisted_6)),
111
+ ($options.startLink)
112
+ ? (openBlock(), createBlock(resolveDynamicComponent($options.startLink.to ? 'router-link' : 'a'), {
113
+ key: 6,
114
+ to: $options.startLink.to || undefined,
115
+ href: $options.startLink.href || undefined,
116
+ class: "cn-workspace-filter-widget__link",
117
+ "data-testid": "cn-workspace-filter-link"
118
+ }, {
119
+ default: withCtx(() => [
120
+ createTextVNode(toDisplayString($options.startLink.label), 1 /* TEXT */)
121
+ ]),
122
+ _: 1 /* STABLE */
123
+ }, 8 /* PROPS */, ["to", "href"]))
124
+ : createCommentVNode("v-if", true)
125
+ ], 2 /* CLASS */))
74
126
  }
75
127
 
76
128
  export { render };
@@ -1 +1 @@
1
- {"version":3,"file":"CnWorkspaceFilterWidget.vue3.js","sources":["../../../../src/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue?vue&type=template&id=f0d5c1c6&lang.js"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n\n<template>\n\t<div class=\"cn-workspace-filter-widget\" data-testid=\"cn-workspace-filter-widget\">\n\t\t<p v-if=\"label\" class=\"cn-workspace-filter-widget__label\">\n\t\t\t{{ label }}\n\t\t</p>\n\n\t\t<div v-if=\"loading\" class=\"cn-workspace-filter-widget__status\">\n\t\t\t{{ loadingLabel }}\n\t\t</div>\n\n\t\t<!-- Select style -->\n\t\t<NcSelect\n\t\t\tv-else-if=\"style === 'select'\"\n\t\t\t:modelValue=\"selectedOption\"\n\t\t\t:options=\"displayOptions\"\n\t\t\t:clearable=\"false\"\n\t\t\t:inputLabel=\"label || writeKey\"\n\t\t\tlabel=\"label\"\n\t\t\tdata-testid=\"cn-workspace-filter-select\"\n\t\t\t@update:modelValue=\"onSelect\" />\n\n\t\t<!-- Radio-list style (default) -->\n\t\t<ul v-else\n\t\t\tclass=\"cn-workspace-filter-widget__list\"\n\t\t\trole=\"radiogroup\"\n\t\t\t:aria-label=\"label || writeKey\">\n\t\t\t<li\n\t\t\t\tv-for=\"option in displayOptions\"\n\t\t\t\t:key=\"String(option.value)\"\n\t\t\t\tclass=\"cn-workspace-filter-widget__item\">\n\t\t\t\t<NcCheckboxRadioSwitch\n\t\t\t\t\ttype=\"radio\"\n\t\t\t\t\t:name=\"radioGroupName\"\n\t\t\t\t\t:modelValue=\"isActive(option) ? String(option.value) : ''\"\n\t\t\t\t\t:value=\"String(option.value)\"\n\t\t\t\t\t:data-testid=\"`cn-workspace-filter-option-${option.value}`\"\n\t\t\t\t\t@update:modelValue=\"onRadioPick(option)\">\n\t\t\t\t\t<span class=\"cn-workspace-filter-widget__option-label\">{{ option.label }}</span>\n\t\t\t\t\t<span\n\t\t\t\t\t\tv-if=\"showCounts && option.count !== undefined && option.count !== null\"\n\t\t\t\t\t\tclass=\"cn-workspace-filter-widget__count\">{{ option.count }}</span>\n\t\t\t\t</NcCheckboxRadioSwitch>\n\t\t\t</li>\n\t\t</ul>\n\t</div>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcCheckboxRadioSwitch, NcSelect } from '@nextcloud/vue'\nimport { fetchEndpointSource } from '../../composables/useEndpointSource.js'\nimport { resolveFilterTokens } from '../../utils/resolveFilterTokens.js'\n\n/**\n * CnWorkspaceFilterWidget — a dashboard choice-list that writes the selected\n * value into the page workspace context so sibling widgets refetch (#91\n * Wave 3, the `workspace-filter` widget).\n *\n * Options come from ONE of three sources (checked in order):\n * 1. static `options[]` — each `{ value, label, count? }`.\n * 2. an OpenRegister `source` — `{ register, schema, groupBy, filter? }`,\n * fetched from OR's `/grouped` facet so each distinct value becomes an\n * option with its object `count`.\n * 3. an `endpointSource` — `{ url, params?, responsePath? }` returning an\n * array of `{ value, label, count? }` (or `{ id, name }` / bare strings,\n * normalised).\n *\n * The chosen value is written to the workspace context key named by `writes`\n * (`\"@workspace.<key>\"` or a bare `\"<key>\"`). CnDashboardPage provides\n * `cnWorkspaceContext`; every sibling widget whose declarative source\n * interpolates `@workspace.<key>` re-resolves its tokens and refetches when\n * the selection changes — the pipelinq WerkplekQueueFilter contract.\n *\n * An optional leading \"All\" option (`allLabel`) clears the key (writes the\n * empty string), so an optional `@workspace.<key>?` token drops and siblings\n * show the unfiltered set.\n *\n * ```js\n * content: {\n * label: 'Queue',\n * writes: '@workspace.queue',\n * style: 'radio', // 'radio' (default) | 'select'\n * allLabel: 'All',\n * showCounts: true,\n * source: { register: 'pipelinq', schema: 'werkitem', groupBy: 'queue' },\n * }\n * ```\n */\nexport default {\n\tname: 'CnWorkspaceFilterWidget',\n\n\tcomponents: { NcSelect, NcCheckboxRadioSwitch },\n\n\tinject: {\n\t\t/**\n\t\t * Page-level workspace context. CnDashboardPage provides a `ref({})`;\n\t\t * Vue 2.7's Options-API inject AUTO-UNWRAPS it, so this is the plain\n\t\t * reactive bag (the raw-ref `{ value }` shape is also handled on write).\n\t\t * Null on pages without one (the widget then just emits `@change`).\n\t\t */\n\t\tcnWorkspaceContext: { default: null },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * Persisted configuration blob.\n\t\t *\n\t\t * @type {{label?: string, writes?: string, style?: string, allLabel?: string, showCounts?: boolean, options?: Array, source?: object, endpointSource?: object, default?: (string|number)}}\n\t\t */\n\t\tcontent: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\t},\n\n\temits: ['change'],\n\n\tdata() {\n\t\treturn {\n\t\t\t/** Fetched options (from source / endpointSource); static options use content.options directly. */\n\t\t\tfetchedOptions: null,\n\t\t\t/** True while options are being fetched. */\n\t\t\tloading: false,\n\t\t\t/** The currently-selected value (mirrors the workspace key). */\n\t\t\tselected: undefined,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/** Section label. */\n\t\tlabel() {\n\t\t\treturn this.content.label || ''\n\t\t},\n\n\t\t/** Choice style: 'select' or 'radio' (default). */\n\t\tstyle() {\n\t\t\treturn this.content.style === 'select' ? 'select' : 'radio'\n\t\t},\n\n\t\t/** Whether to render per-option counts. */\n\t\tshowCounts() {\n\t\t\treturn this.content.showCounts !== false\n\t\t},\n\n\t\t/** The bare workspace key the selection writes (strips a `@workspace.` / `@page.` prefix). */\n\t\twriteKey() {\n\t\t\tconst w = this.content.writes || ''\n\t\t\treturn String(w).replace(/^@(workspace|page)\\./, '')\n\t\t},\n\n\t\t/** A stable radiogroup name so the radios are mutually exclusive. */\n\t\tradioGroupName() {\n\t\t\treturn `cn-wsf-${this.writeKey || 'filter'}`\n\t\t},\n\n\t\t/** The option list actually rendered — the optional \"All\" entry prepended. */\n\t\tdisplayOptions() {\n\t\t\tconst base = this.normalisedOptions\n\t\t\tif (this.content.allLabel) {\n\t\t\t\treturn [{ value: '', label: this.content.allLabel }, ...base]\n\t\t\t}\n\t\t\treturn base\n\t\t},\n\n\t\t/** Normalised `{ value, label, count? }` options from whichever source is configured. */\n\t\tnormalisedOptions() {\n\t\t\tconst raw = this.fetchedOptions !== null\n\t\t\t\t? this.fetchedOptions\n\t\t\t\t: (Array.isArray(this.content.options) ? this.content.options : [])\n\t\t\treturn raw.map((o) => this.normaliseOption(o)).filter((o) => o.value !== undefined && o.value !== null)\n\t\t},\n\n\t\t/** The selected option object (for NcSelect). */\n\t\tselectedOption() {\n\t\t\treturn this.displayOptions.find((o) => String(o.value) === String(this.selected)) || null\n\t\t},\n\n\t\t/**\n\t\t * The reactive workspace value for this key — watched so an external\n\t\t * write (another widget) syncs the local selection.\n\t\t *\n\t\t * @return {unknown}\n\t\t */\n\t\tworkspaceValue() {\n\t\t\treturn this.readWorkspace()\n\t\t},\n\n\t\tloadingLabel() {\n\t\t\treturn t('nextcloud-vue', 'Loading…')\n\t\t},\n\t},\n\n\twatch: {\n\t\t/** Re-fetch options when a dynamic source changes. */\n\t\tcontent: {\n\t\t\tdeep: true,\n\t\t\thandler() {\n\t\t\t\tthis.loadOptions()\n\t\t\t},\n\t\t},\n\n\t\t/**\n\t\t * Track external workspace changes (another widget writing the same key).\n\t\t *\n\t\t * @param {unknown} val The new workspace value for this key.\n\t\t */\n\t\tworkspaceValue(val) {\n\t\t\tif (val !== undefined) {\n\t\t\t\tthis.selected = val\n\t\t\t}\n\t\t},\n\t},\n\n\tcreated() {\n\t\t// Seed the selection: existing workspace value → content.default → the\n\t\t// \"All\" clear option (empty) when present, else the first option once\n\t\t// loaded (handled in loadOptions).\n\t\tconst existing = this.readWorkspace()\n\t\tif (existing !== undefined) {\n\t\t\tthis.selected = existing\n\t\t} else if (this.content.default !== undefined) {\n\t\t\tthis.selected = this.content.default\n\t\t\tthis.writeWorkspace(this.content.default)\n\t\t}\n\t\tthis.loadOptions()\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Normalise a raw option into `{ value, label, count? }`. Accepts\n\t\t * `{ value, label, count }`, `{ id, name }`, `{ key, count }` (OR\n\t\t * facet), or a bare string/number.\n\t\t *\n\t\t * @param {unknown} o The raw option.\n\t\t * @return {{value: unknown, label: string, count?: number}}\n\t\t */\n\t\tnormaliseOption(o) {\n\t\t\tif (o === null || o === undefined) {\n\t\t\t\treturn { value: undefined, label: '' }\n\t\t\t}\n\t\t\tif (typeof o !== 'object') {\n\t\t\t\treturn { value: o, label: String(o) }\n\t\t\t}\n\t\t\tconst value = o.value !== undefined ? o.value : (o.id !== undefined ? o.id : o.key)\n\t\t\tconst label = o.label !== undefined ? o.label : (o.name !== undefined ? o.name : String(value))\n\t\t\tconst out = { value, label: String(label) }\n\t\t\tif (o.count !== undefined) {\n\t\t\t\tout.count = o.count\n\t\t\t}\n\t\t\treturn out\n\t\t},\n\n\t\t/**\n\t\t * Load options for the active source (static content.options need no\n\t\t * fetch). Seeds the first option as the selection when nothing is set\n\t\t * and no \"All\" clear option exists.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync loadOptions() {\n\t\t\tif (this.content.endpointSource && this.content.endpointSource.url) {\n\t\t\t\tawait this.loadFromEndpoint()\n\t\t\t} else if (this.content.source && this.content.source.schema) {\n\t\t\t\tawait this.loadFromOrSource()\n\t\t\t} else {\n\t\t\t\tthis.fetchedOptions = null\n\t\t\t}\n\t\t\t// Default selection: when nothing is selected and there's no \"All\"\n\t\t\t// clear affordance, adopt the first option so siblings have a value.\n\t\t\tif ((this.selected === undefined || this.selected === '') && !this.content.allLabel) {\n\t\t\t\tconst first = this.normalisedOptions[0]\n\t\t\t\tif (first && this.readWorkspace() === undefined && this.content.default === undefined) {\n\t\t\t\t\tthis.selected = first.value\n\t\t\t\t\tthis.writeWorkspace(first.value)\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Fetch options from an `endpointSource` (array of options at\n\t\t * responsePath). Fail-safe: a failed fetch leaves an empty list.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync loadFromEndpoint() {\n\t\t\tthis.loading = true\n\t\t\ttry {\n\t\t\t\tconst payload = await fetchEndpointSource(this.content.endpointSource, {\n\t\t\t\t\tworkspace: this.readWorkspaceBag(),\n\t\t\t\t})\n\t\t\t\tthis.fetchedOptions = Array.isArray(payload) ? payload : []\n\t\t\t} catch {\n\t\t\t\tthis.fetchedOptions = []\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Fetch options from an OpenRegister `/grouped` facet — each distinct\n\t\t * `groupBy` value becomes an option with its object count. Fail-safe.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync loadFromOrSource() {\n\t\t\tconst src = this.content.source\n\t\t\tthis.loading = true\n\t\t\ttry {\n\t\t\t\tconst [{ default: axios }, { generateUrl }] = await Promise.all([\n\t\t\t\t\timport('@nextcloud/axios'),\n\t\t\t\t\timport('@nextcloud/router'),\n\t\t\t\t])\n\t\t\t\tconst url = generateUrl(\n\t\t\t\t\t'/apps/openregister/api/objects/aggregations/{register}/{schema}/grouped',\n\t\t\t\t\t{ register: src.register, schema: src.schema },\n\t\t\t\t)\n\t\t\t\tconst params = { groupBy: src.groupBy, metric: 'count' }\n\t\t\t\tconst filter = resolveFilterTokens(src.filter || {}, { workspace: this.readWorkspaceBag() })\n\t\t\t\tfor (const [k, v] of Object.entries(filter)) {\n\t\t\t\t\tif (v !== '' && v !== null && v !== undefined) {\n\t\t\t\t\t\tparams[`filter[${k}]`] = v\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tconst res = await axios.get(url, { params })\n\t\t\t\tconst groups = (res && res.data && res.data.groups) || []\n\t\t\t\tthis.fetchedOptions = groups\n\t\t\t\t\t.filter((g) => g.key !== null && g.key !== undefined && g.key !== '')\n\t\t\t\t\t.map((g) => ({ value: g.key, label: String(g.key), count: Number(g.value) || 0 }))\n\t\t\t} catch {\n\t\t\t\tthis.fetchedOptions = []\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Whether an option is the active selection.\n\t\t *\n\t\t * @param {object} option The option.\n\t\t * @return {boolean}\n\t\t */\n\t\tisActive(option) {\n\t\t\treturn String(option.value) === String(this.selected ?? '')\n\t\t},\n\n\t\t/**\n\t\t * Radio-list pick handler.\n\t\t *\n\t\t * @param {object} option The picked option.\n\t\t * @return {void}\n\t\t */\n\t\tonRadioPick(option) {\n\t\t\tthis.applySelection(option.value)\n\t\t},\n\n\t\t/**\n\t\t * NcSelect input handler.\n\t\t *\n\t\t * @param {object|null} option The picked option object.\n\t\t * @return {void}\n\t\t */\n\t\tonSelect(option) {\n\t\t\tthis.applySelection(option ? option.value : '')\n\t\t},\n\n\t\t/**\n\t\t * Apply a new selection: mirror it locally, write it into the workspace\n\t\t * context (so sibling widgets refetch), and emit `@change`.\n\t\t *\n\t\t * @param {unknown} value The new value.\n\t\t * @return {void}\n\t\t */\n\t\tapplySelection(value) {\n\t\t\tthis.selected = value\n\t\t\tthis.writeWorkspace(value)\n\t\t\t/**\n\t\t\t * @event change Emitted when the selection changes. Payload: `{ key, value }`.\n\t\t\t */\n\t\t\tthis.$emit('change', { key: this.writeKey, value })\n\t\t},\n\n\t\t/**\n\t\t * The unwrapped workspace bag (or `{}`).\n\t\t *\n\t\t * @return {object}\n\t\t */\n\t\treadWorkspaceBag() {\n\t\t\tconst holder = this.cnWorkspaceContext\n\t\t\tif (!holder || typeof holder !== 'object') {\n\t\t\t\treturn {}\n\t\t\t}\n\t\t\treturn ('value' in holder) ? (holder.value || {}) : holder\n\t\t},\n\n\t\t/**\n\t\t * Read the current workspace value for this widget's key (undefined\n\t\t * when unset).\n\t\t *\n\t\t * @return {unknown}\n\t\t */\n\t\treadWorkspace() {\n\t\t\tif (!this.writeKey) {\n\t\t\t\treturn undefined\n\t\t\t}\n\t\t\tconst bag = this.readWorkspaceBag()\n\t\t\treturn bag[this.writeKey]\n\t\t},\n\n\t\t/**\n\t\t * Write a value into the reactive workspace bag under this widget's key\n\t\t * (the CnInteractionFormWidget write pattern — replace-in-place for the\n\t\t * unwrapped bag, `.value` for the raw-ref shape).\n\t\t *\n\t\t * @param {unknown} value The value to write.\n\t\t * @return {void}\n\t\t */\n\t\twriteWorkspace(value) {\n\t\t\tif (!this.writeKey) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst holder = this.cnWorkspaceContext\n\t\t\tif (!holder || typeof holder !== 'object') {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif ('value' in holder) {\n\t\t\t\tholder.value = { ...(holder.value || {}), [this.writeKey]: value }\n\t\t\t\treturn\n\t\t\t}\n\t\t\tholder[this.writeKey] = value\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-workspace-filter-widget {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 8px;\n\twidth: 100%;\n}\n\n.cn-workspace-filter-widget__label {\n\tmargin: 0;\n\tfont-weight: 600;\n\tcolor: var(--color-main-text);\n}\n\n.cn-workspace-filter-widget__status {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.9em;\n}\n\n.cn-workspace-filter-widget__list {\n\tlist-style: none;\n\tmargin: 0;\n\tpadding: 0;\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 2px;\n}\n\n.cn-workspace-filter-widget__count {\n\tmargin-inline-start: 6px;\n\tpadding: 0 6px;\n\tborder-radius: 999px;\n\tbackground: var(--color-background-hover);\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.85em;\n}\n</style>\n"],"names":["_createElementBlock","_toDisplayString","_createElementVNode","_openBlock"],"mappings":";;;AAWK,EAAA,KAAM,EAAC,4BAA4B;AAAC,EAAA,aAAW,EAAC,4BAAW;;;;OAC5C,EAAA,mCAAA;;;;AAIV,EAAA,KAAA,EAAA,oCAAA;;;AA+BH,MAAA,UAAA,GAAA,EAAA,KAAA,EAAA,0CAAA,GAAA;;;;;;;;;;;KAnCI,QAAK,CAAA,KAAA;sBAAbA,wBAAA,4BACG,QAAC,CAAA,KAAA,CAAA,EAAA,CAAA,YAAA;;KAGG,KAAA,CAAA,OAAA;sBAARA,kBAEM,CAAA,KAAA,EAFN,UAEM,EAAAC,eAAA,CADF,QAAY,CAAA,YAAA,CAAA,EAAA,CAAA,YAAA;SAKJ,QAAC,CAAA,KAAA,KAAA,QAAA;mCAOI,CAAA,mBAAA,EAAA;;AANf,YAAA,UAAU,EAAE,QAAc,CAAA,cAAA;AAC1B,YAAA,OAAO,EAAE,QAAc,CAAA,cAAA;;YAEvB,UAAU,EAAE,QAAK,CAAA,KAAA,IAAI,QAAS,CAAA,QAAA;AAC/B,YAAA,KAAK,EAAC,OAAE;AACR,YAAA,aAAW,EAAC,4BAA2B;AACtC,YAAA,qBAAe,EAAA,QAAA,CAAA,QAAA;;;;iBAIX,EAAC,kCAAqB;AAC1B,YAAA,IAAG,EAAC,YAAY;YAChB,YAAU,EAAE,QAAK,CAAA,KAAA,IAAI,QAAA,CAAA,QAAA;;4EAEJ,QAAK,CAAA,cAAA,EAAA,CAAf,MAAM,KAAA;;AACZ,gBAAA,GAAG,EAAE,MAAM,CAAC,MAAM,CAAC,KAAK,CAAA;qBACpB,EAAC,kCAAuB;;;AAE5B,kBAAA,IAAI,EAAC,OAAO;AACX,kBAAA,IAAG,EAAG,QAAc,CAAA,cAAA;kBACpB,UAAG,EAAA,iBAAA,CAAA,MAAA,IAAA,MAAA,CAAA,MAAA,CAAA,KAAA,CAAA,GAAA,EAAA;AACH,kBAAA,KAAK,EAAE,MAAM,CAAC,MAAM,CAAC,KAAK,CAAA;kBAC1B,aAAW,EAAA,CAAA,2BAAA,EAAgC,MAAM,CAAC,KAAK,CAAA,CAAA;kBACvD,qBAAiB,EAAA,MAAA,KAAE,QAAE,CAAA,WAAA,CAAA,MAAA,CAAA,CAAA;;mCACtB,MAAE;AAAF,oBAAAC,kBAAA,CAAE,MAAF,EAAA,UAAE,EAAAD,eAAA,CAAA,MAAA,CAAA,KAAA,CAAA,EAAA,CAAA,YAAA;qBAED,QAAA,CAAA,UAAA,IAAA,MAAA,CAAA,KAAA,KAAA,SAAA,IAAA,MAAA,CAAA,KAAA,KAAA,IAAA;AADD,yBAAAE,SAAA,EAAA,EAAAH,kBAAA,OAAA,EAAA;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"CnWorkspaceFilterWidget.vue3.js","sources":["../../../../src/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue?vue&type=template&id=f0d5c1c6&lang.js"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n\n<template>\n\t<div\n\t\tclass=\"cn-workspace-filter-widget\"\n\t\t:class=\"{ 'cn-workspace-filter-widget--start': isStart }\"\n\t\tdata-testid=\"cn-workspace-filter-widget\">\n\t\t<!-- The start card (`layout: \"start\"`): a title and one line of\n\t\t guidance beside the control, as the \"Contact starten\" card. -->\n\t\t<div v-if=\"isStart && (title || description)\" class=\"cn-workspace-filter-widget__text\">\n\t\t\t<h3 v-if=\"title\" class=\"cn-workspace-filter-widget__title\">\n\t\t\t\t{{ title }}\n\t\t\t</h3>\n\t\t\t<p v-if=\"description\" class=\"cn-workspace-filter-widget__description\">\n\t\t\t\t{{ description }}\n\t\t\t</p>\n\t\t</div>\n\n\t\t<p v-if=\"label\" class=\"cn-workspace-filter-widget__label\">\n\t\t\t{{ label }}\n\t\t</p>\n\n\t\t<!-- An object picker (`objectSource`): searches OpenRegister on the\n\t\t server, so it reaches every object, not a fetched page. -->\n\t\t<CnResourceSelect\n\t\t\tv-if=\"objectSource\"\n\t\t\tclass=\"cn-workspace-filter-widget__control\"\n\t\t\t:register=\"objectSource.register\"\n\t\t\t:schema=\"objectSource.schema\"\n\t\t\t:labelField=\"objectSource.labelField || 'title'\"\n\t\t\t:modelValue=\"selected === undefined || selected === null ? '' : String(selected)\"\n\t\t\t:placeholder=\"tr(objectSource.placeholder || '')\"\n\t\t\t:ariaLabelCombobox=\"tr(objectSource.ariaLabel || '') || title || label || writeKey\"\n\t\t\t:preload=\"objectSource.preload !== false\"\n\t\t\t:clearable=\"Boolean(content.allLabel)\"\n\t\t\t:allowCreate=\"false\"\n\t\t\tdata-testid=\"cn-workspace-filter-object\"\n\t\t\t@update:modelValue=\"applySelection($event || '')\" />\n\n\t\t<div v-else-if=\"loading\" class=\"cn-workspace-filter-widget__status\">\n\t\t\t{{ loadingLabel }}\n\t\t</div>\n\n\t\t<!-- Select style -->\n\t\t<NcSelect\n\t\t\tv-else-if=\"style === 'select'\"\n\t\t\t:modelValue=\"selectedOption\"\n\t\t\t:options=\"displayOptions\"\n\t\t\t:clearable=\"false\"\n\t\t\t:inputLabel=\"label || writeKey\"\n\t\t\tlabel=\"label\"\n\t\t\tdata-testid=\"cn-workspace-filter-select\"\n\t\t\t@update:modelValue=\"onSelect\" />\n\n\t\t<!-- Radio-list style (default) -->\n\t\t<ul v-else\n\t\t\tclass=\"cn-workspace-filter-widget__list\"\n\t\t\trole=\"radiogroup\"\n\t\t\t:aria-label=\"label || writeKey\">\n\t\t\t<li\n\t\t\t\tv-for=\"option in displayOptions\"\n\t\t\t\t:key=\"String(option.value)\"\n\t\t\t\tclass=\"cn-workspace-filter-widget__item\">\n\t\t\t\t<NcCheckboxRadioSwitch\n\t\t\t\t\ttype=\"radio\"\n\t\t\t\t\t:name=\"radioGroupName\"\n\t\t\t\t\t:modelValue=\"isActive(option) ? String(option.value) : ''\"\n\t\t\t\t\t:value=\"String(option.value)\"\n\t\t\t\t\t:data-testid=\"`cn-workspace-filter-option-${option.value}`\"\n\t\t\t\t\t@update:modelValue=\"onRadioPick(option)\">\n\t\t\t\t\t<span class=\"cn-workspace-filter-widget__option-label\">{{ option.label }}</span>\n\t\t\t\t\t<span\n\t\t\t\t\t\tv-if=\"showCounts && option.count !== undefined && option.count !== null\"\n\t\t\t\t\t\tclass=\"cn-workspace-filter-widget__count\">{{ option.count }}</span>\n\t\t\t\t</NcCheckboxRadioSwitch>\n\t\t\t</li>\n\t\t</ul>\n\n\t\t<component\n\t\t\t:is=\"startLink.to ? 'router-link' : 'a'\"\n\t\t\tv-if=\"startLink\"\n\t\t\t:to=\"startLink.to || undefined\"\n\t\t\t:href=\"startLink.href || undefined\"\n\t\t\tclass=\"cn-workspace-filter-widget__link\"\n\t\t\tdata-testid=\"cn-workspace-filter-link\">\n\t\t\t{{ startLink.label }}\n\t\t</component>\n\t</div>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcCheckboxRadioSwitch, NcSelect } from '@nextcloud/vue'\nimport CnResourceSelect from '../CnResourceSelect/CnResourceSelect.vue'\nimport { fetchEndpointSource } from '../../composables/useEndpointSource.js'\nimport { resolveFilterTokens } from '../../utils/resolveFilterTokens.js'\n\n/**\n * CnWorkspaceFilterWidget — a dashboard choice-list that writes the selected\n * value into the page workspace context so sibling widgets refetch (#91\n * Wave 3, the `workspace-filter` widget).\n *\n * Options come from ONE of three sources (checked in order):\n * 1. static `options[]` — each `{ value, label, count? }`.\n * 2. an OpenRegister `source` — `{ register, schema, groupBy, filter? }`,\n * fetched from OR's `/grouped` facet so each distinct value becomes an\n * option with its object `count`.\n * 3. an `endpointSource` — `{ url, params?, responsePath? }` returning an\n * array of `{ value, label, count? }` (or `{ id, name }` / bare strings,\n * normalised).\n *\n * The chosen value is written to the workspace context key named by `writes`\n * (`\"@workspace.<key>\"` or a bare `\"<key>\"`). CnDashboardPage provides\n * `cnWorkspaceContext`; every sibling widget whose declarative source\n * interpolates `@workspace.<key>` re-resolves its tokens and refetches when\n * the selection changes — the pipelinq WerkplekQueueFilter contract.\n *\n * An optional leading \"All\" option (`allLabel`) clears the key (writes the\n * empty string), so an optional `@workspace.<key>?` token drops and siblings\n * show the unfiltered set.\n *\n * ```js\n * content: {\n * label: 'Queue',\n * writes: '@workspace.queue',\n * style: 'radio', // 'radio' (default) | 'select'\n * allLabel: 'All',\n * showCounts: true,\n * source: { register: 'pipelinq', schema: 'werkitem', groupBy: 'queue' },\n * }\n * ```\n */\nexport default {\n\tname: 'CnWorkspaceFilterWidget',\n\n\tcomponents: { CnResourceSelect, NcSelect, NcCheckboxRadioSwitch },\n\n\tinject: {\n\t\t/**\n\t\t * Page-level workspace context. CnDashboardPage provides a `ref({})`;\n\t\t * Vue 2.7's Options-API inject AUTO-UNWRAPS it, so this is the plain\n\t\t * reactive bag (the raw-ref `{ value }` shape is also handled on write).\n\t\t * Null on pages without one (the widget then just emits `@change`).\n\t\t */\n\t\tcnWorkspaceContext: { default: null },\n\t\t/** Host translate function, for the start card's texts. */\n\t\tcnTranslate: { default: () => (key) => key },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * Persisted configuration blob.\n\t\t *\n\t\t * Start card keys (abstract-start-card): `layout: \"start\"` draws\n\t\t * `title` and `description` beside the control, and `link`\n\t\t * (`{ label, route? | href? }`) after it. `objectSource`\n\t\t * (`{ register, schema, labelField?, placeholder?, ariaLabel?, preload? }`)\n\t\t * makes the control an OpenRegister object picker that searches on the\n\t\t * server and writes the picked object's id; without a selection and\n\t\t * without `allLabel` it selects the first object.\n\t\t *\n\t\t * @type {{label?: string, writes?: string, style?: string, allLabel?: string, showCounts?: boolean, options?: Array, source?: object, endpointSource?: object, objectSource?: object, default?: (string|number), layout?: string, title?: string, description?: string, link?: object}}\n\t\t */\n\t\tcontent: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\t},\n\n\temits: ['change'],\n\n\tdata() {\n\t\treturn {\n\t\t\t/** Fetched options (from source / endpointSource); static options use content.options directly. */\n\t\t\tfetchedOptions: null,\n\t\t\t/** True while options are being fetched. */\n\t\t\tloading: false,\n\t\t\t/** The currently-selected value (mirrors the workspace key). */\n\t\t\tselected: undefined,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/** Section label. */\n\t\tlabel() {\n\t\t\treturn this.content.label || ''\n\t\t},\n\n\t\t/** Whether the widget draws the start card. */\n\t\tisStart() {\n\t\t\treturn this.content.layout === 'start'\n\t\t},\n\n\t\t/** The start card's title, translated. */\n\t\ttitle() {\n\t\t\treturn this.tr(this.content.title || '')\n\t\t},\n\n\t\t/** The start card's guidance line, translated. */\n\t\tdescription() {\n\t\t\treturn this.tr(this.content.description || '')\n\t\t},\n\n\t\t/**\n\t\t * The link after the control, or null.\n\t\t *\n\t\t * @return {{label: string, to?: object, href?: string}|null}\n\t\t */\n\t\tstartLink() {\n\t\t\tconst link = this.content.link\n\t\t\tif (!link || typeof link !== 'object' || !link.label) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tif (link.route && typeof link.route === 'object' && link.route.name) {\n\t\t\t\treturn { label: this.tr(link.label), to: link.route }\n\t\t\t}\n\t\t\tif (typeof link.href === 'string' && link.href !== '') {\n\t\t\t\treturn { label: this.tr(link.label), href: link.href }\n\t\t\t}\n\t\t\treturn null\n\t\t},\n\n\t\t/**\n\t\t * The object picker declaration, or null.\n\t\t *\n\t\t * @return {{register: string, schema: string, labelField?: string}|null}\n\t\t */\n\t\tobjectSource() {\n\t\t\tconst src = this.content.objectSource\n\t\t\treturn src && typeof src === 'object' && src.register && src.schema ? src : null\n\t\t},\n\n\t\t/** Choice style: 'select' or 'radio' (default). */\n\t\tstyle() {\n\t\t\treturn this.content.style === 'select' ? 'select' : 'radio'\n\t\t},\n\n\t\t/** Whether to render per-option counts. */\n\t\tshowCounts() {\n\t\t\treturn this.content.showCounts !== false\n\t\t},\n\n\t\t/** The bare workspace key the selection writes (strips a `@workspace.` / `@page.` prefix). */\n\t\twriteKey() {\n\t\t\tconst w = this.content.writes || ''\n\t\t\treturn String(w).replace(/^@(workspace|page)\\./, '')\n\t\t},\n\n\t\t/** A stable radiogroup name so the radios are mutually exclusive. */\n\t\tradioGroupName() {\n\t\t\treturn `cn-wsf-${this.writeKey || 'filter'}`\n\t\t},\n\n\t\t/** The option list actually rendered — the optional \"All\" entry prepended. */\n\t\tdisplayOptions() {\n\t\t\tconst base = this.normalisedOptions\n\t\t\tif (this.content.allLabel) {\n\t\t\t\treturn [{ value: '', label: this.content.allLabel }, ...base]\n\t\t\t}\n\t\t\treturn base\n\t\t},\n\n\t\t/** Normalised `{ value, label, count? }` options from whichever source is configured. */\n\t\tnormalisedOptions() {\n\t\t\tconst raw = this.fetchedOptions !== null\n\t\t\t\t? this.fetchedOptions\n\t\t\t\t: (Array.isArray(this.content.options) ? this.content.options : [])\n\t\t\treturn raw.map((o) => this.normaliseOption(o)).filter((o) => o.value !== undefined && o.value !== null)\n\t\t},\n\n\t\t/** The selected option object (for NcSelect). */\n\t\tselectedOption() {\n\t\t\treturn this.displayOptions.find((o) => String(o.value) === String(this.selected)) || null\n\t\t},\n\n\t\t/**\n\t\t * The reactive workspace value for this key — watched so an external\n\t\t * write (another widget) syncs the local selection.\n\t\t *\n\t\t * @return {unknown}\n\t\t */\n\t\tworkspaceValue() {\n\t\t\treturn this.readWorkspace()\n\t\t},\n\n\t\tloadingLabel() {\n\t\t\treturn t('nextcloud-vue', 'Loading…')\n\t\t},\n\t},\n\n\twatch: {\n\t\t/** Re-fetch options when a dynamic source changes. */\n\t\tcontent: {\n\t\t\tdeep: true,\n\t\t\thandler() {\n\t\t\t\tthis.loadOptions()\n\t\t\t},\n\t\t},\n\n\t\t/**\n\t\t * Track external workspace changes (another widget writing the same key).\n\t\t *\n\t\t * @param {unknown} val The new workspace value for this key.\n\t\t */\n\t\tworkspaceValue(val) {\n\t\t\tif (val !== undefined) {\n\t\t\t\tthis.selected = val\n\t\t\t}\n\t\t},\n\t},\n\n\tcreated() {\n\t\t// Seed the selection: existing workspace value → content.default → the\n\t\t// \"All\" clear option (empty) when present, else the first option once\n\t\t// loaded (handled in loadOptions).\n\t\tconst existing = this.readWorkspace()\n\t\tif (existing !== undefined) {\n\t\t\tthis.selected = existing\n\t\t} else if (this.content.default !== undefined) {\n\t\t\tthis.selected = this.content.default\n\t\t\tthis.writeWorkspace(this.content.default)\n\t\t}\n\t\tthis.loadOptions()\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Normalise a raw option into `{ value, label, count? }`. Accepts\n\t\t * `{ value, label, count }`, `{ id, name }`, `{ key, count }` (OR\n\t\t * facet), or a bare string/number.\n\t\t *\n\t\t * @param {unknown} o The raw option.\n\t\t * @return {{value: unknown, label: string, count?: number}}\n\t\t */\n\t\tnormaliseOption(o) {\n\t\t\tif (o === null || o === undefined) {\n\t\t\t\treturn { value: undefined, label: '' }\n\t\t\t}\n\t\t\tif (typeof o !== 'object') {\n\t\t\t\treturn { value: o, label: String(o) }\n\t\t\t}\n\t\t\tconst value = o.value !== undefined ? o.value : (o.id !== undefined ? o.id : o.key)\n\t\t\tconst label = o.label !== undefined ? o.label : (o.name !== undefined ? o.name : String(value))\n\t\t\tconst out = { value, label: String(label) }\n\t\t\tif (o.count !== undefined) {\n\t\t\t\tout.count = o.count\n\t\t\t}\n\t\t\treturn out\n\t\t},\n\n\t\t/**\n\t\t * Load options for the active source (static content.options need no\n\t\t * fetch). Seeds the first option as the selection when nothing is set\n\t\t * and no \"All\" clear option exists.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync loadOptions() {\n\t\t\tif (this.objectSource) {\n\t\t\t\tawait this.seedFirstObject()\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (this.content.endpointSource && this.content.endpointSource.url) {\n\t\t\t\tawait this.loadFromEndpoint()\n\t\t\t} else if (this.content.source && this.content.source.schema) {\n\t\t\t\tawait this.loadFromOrSource()\n\t\t\t} else {\n\t\t\t\tthis.fetchedOptions = null\n\t\t\t}\n\t\t\t// Default selection: when nothing is selected and there's no \"All\"\n\t\t\t// clear affordance, adopt the first option so siblings have a value.\n\t\t\tif ((this.selected === undefined || this.selected === '') && !this.content.allLabel) {\n\t\t\t\tconst first = this.normalisedOptions[0]\n\t\t\t\tif (first && this.readWorkspace() === undefined && this.content.default === undefined) {\n\t\t\t\t\tthis.selected = first.value\n\t\t\t\t\tthis.writeWorkspace(first.value)\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Translate a manifest string through the host app.\n\t\t *\n\t\t * @param {string} text The text.\n\t\t * @return {string}\n\t\t */\n\t\ttr(text) {\n\t\t\tif (typeof text !== 'string' || text === '') {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn typeof this.cnTranslate === 'function' ? this.cnTranslate(text) : text\n\t\t},\n\n\t\t/**\n\t\t * The object picker's default: with nothing selected, no `default` and\n\t\t * no `allLabel`, select the first object the source lists, so the page\n\t\t * always has one in focus. Fail-safe: a failed read selects nothing.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync seedFirstObject() {\n\t\t\tif ((this.selected !== undefined && this.selected !== '') || this.content.allLabel || this.content.default !== undefined) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst src = this.objectSource\n\t\t\ttry {\n\t\t\t\tconst [{ default: axios }, { generateUrl }] = await Promise.all([\n\t\t\t\t\timport('@nextcloud/axios'),\n\t\t\t\t\timport('@nextcloud/router'),\n\t\t\t\t])\n\t\t\t\tconst url = generateUrl('/apps/openregister/api/objects/{register}/{schema}', { register: src.register, schema: src.schema })\n\t\t\t\tconst res = await axios.get(url, { params: { _limit: 1 } })\n\t\t\t\tconst data = res && res.data\n\t\t\t\tconst rows = Array.isArray(data) ? data : ((data && data.results) || [])\n\t\t\t\tconst first = rows[0]\n\t\t\t\tconst id = first && (first.id || (first['@self'] && first['@self'].id))\n\t\t\t\tif (id && (this.selected === undefined || this.selected === '') && this.readWorkspace() === undefined) {\n\t\t\t\t\tthis.selected = String(id)\n\t\t\t\t\tthis.writeWorkspace(String(id))\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// Nothing selected: the siblings wait for a pick.\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Fetch options from an `endpointSource` (array of options at\n\t\t * responsePath). Fail-safe: a failed fetch leaves an empty list.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync loadFromEndpoint() {\n\t\t\tthis.loading = true\n\t\t\ttry {\n\t\t\t\tconst payload = await fetchEndpointSource(this.content.endpointSource, {\n\t\t\t\t\tworkspace: this.readWorkspaceBag(),\n\t\t\t\t})\n\t\t\t\tthis.fetchedOptions = Array.isArray(payload) ? payload : []\n\t\t\t} catch {\n\t\t\t\tthis.fetchedOptions = []\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Fetch options from an OpenRegister `/grouped` facet — each distinct\n\t\t * `groupBy` value becomes an option with its object count. Fail-safe.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync loadFromOrSource() {\n\t\t\tconst src = this.content.source\n\t\t\tthis.loading = true\n\t\t\ttry {\n\t\t\t\tconst [{ default: axios }, { generateUrl }] = await Promise.all([\n\t\t\t\t\timport('@nextcloud/axios'),\n\t\t\t\t\timport('@nextcloud/router'),\n\t\t\t\t])\n\t\t\t\tconst url = generateUrl(\n\t\t\t\t\t'/apps/openregister/api/objects/aggregations/{register}/{schema}/grouped',\n\t\t\t\t\t{ register: src.register, schema: src.schema },\n\t\t\t\t)\n\t\t\t\tconst params = { groupBy: src.groupBy, metric: 'count' }\n\t\t\t\tconst filter = resolveFilterTokens(src.filter || {}, { workspace: this.readWorkspaceBag() })\n\t\t\t\tfor (const [k, v] of Object.entries(filter)) {\n\t\t\t\t\tif (v !== '' && v !== null && v !== undefined) {\n\t\t\t\t\t\tparams[`filter[${k}]`] = v\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tconst res = await axios.get(url, { params })\n\t\t\t\tconst groups = (res && res.data && res.data.groups) || []\n\t\t\t\tthis.fetchedOptions = groups\n\t\t\t\t\t.filter((g) => g.key !== null && g.key !== undefined && g.key !== '')\n\t\t\t\t\t.map((g) => ({ value: g.key, label: String(g.key), count: Number(g.value) || 0 }))\n\t\t\t} catch {\n\t\t\t\tthis.fetchedOptions = []\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Whether an option is the active selection.\n\t\t *\n\t\t * @param {object} option The option.\n\t\t * @return {boolean}\n\t\t */\n\t\tisActive(option) {\n\t\t\treturn String(option.value) === String(this.selected ?? '')\n\t\t},\n\n\t\t/**\n\t\t * Radio-list pick handler.\n\t\t *\n\t\t * @param {object} option The picked option.\n\t\t * @return {void}\n\t\t */\n\t\tonRadioPick(option) {\n\t\t\tthis.applySelection(option.value)\n\t\t},\n\n\t\t/**\n\t\t * NcSelect input handler.\n\t\t *\n\t\t * @param {object|null} option The picked option object.\n\t\t * @return {void}\n\t\t */\n\t\tonSelect(option) {\n\t\t\tthis.applySelection(option ? option.value : '')\n\t\t},\n\n\t\t/**\n\t\t * Apply a new selection: mirror it locally, write it into the workspace\n\t\t * context (so sibling widgets refetch), and emit `@change`.\n\t\t *\n\t\t * @param {unknown} value The new value.\n\t\t * @return {void}\n\t\t */\n\t\tapplySelection(value) {\n\t\t\tthis.selected = value\n\t\t\tthis.writeWorkspace(value)\n\t\t\t/**\n\t\t\t * @event change Emitted when the selection changes. Payload: `{ key, value }`.\n\t\t\t */\n\t\t\tthis.$emit('change', { key: this.writeKey, value })\n\t\t},\n\n\t\t/**\n\t\t * The unwrapped workspace bag (or `{}`).\n\t\t *\n\t\t * @return {object}\n\t\t */\n\t\treadWorkspaceBag() {\n\t\t\tconst holder = this.cnWorkspaceContext\n\t\t\tif (!holder || typeof holder !== 'object') {\n\t\t\t\treturn {}\n\t\t\t}\n\t\t\treturn ('value' in holder) ? (holder.value || {}) : holder\n\t\t},\n\n\t\t/**\n\t\t * Read the current workspace value for this widget's key (undefined\n\t\t * when unset).\n\t\t *\n\t\t * @return {unknown}\n\t\t */\n\t\treadWorkspace() {\n\t\t\tif (!this.writeKey) {\n\t\t\t\treturn undefined\n\t\t\t}\n\t\t\tconst bag = this.readWorkspaceBag()\n\t\t\treturn bag[this.writeKey]\n\t\t},\n\n\t\t/**\n\t\t * Write a value into the reactive workspace bag under this widget's key\n\t\t * (the CnInteractionFormWidget write pattern — replace-in-place for the\n\t\t * unwrapped bag, `.value` for the raw-ref shape).\n\t\t *\n\t\t * @param {unknown} value The value to write.\n\t\t * @return {void}\n\t\t */\n\t\twriteWorkspace(value) {\n\t\t\tif (!this.writeKey) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst holder = this.cnWorkspaceContext\n\t\t\tif (!holder || typeof holder !== 'object') {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif ('value' in holder) {\n\t\t\t\tholder.value = { ...(holder.value || {}), [this.writeKey]: value }\n\t\t\t\treturn\n\t\t\t}\n\t\t\tholder[this.writeKey] = value\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-workspace-filter-widget {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 8px;\n\twidth: 100%;\n}\n\n.cn-workspace-filter-widget__label {\n\tmargin: 0;\n\tfont-weight: 600;\n\tcolor: var(--color-main-text);\n}\n\n.cn-workspace-filter-widget__status {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.9em;\n}\n\n.cn-workspace-filter-widget__list {\n\tlist-style: none;\n\tmargin: 0;\n\tpadding: 0;\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 2px;\n}\n\n/* The start card: text left, control (and link) right, as PqDashboard's \"Contact starten\". */\n.cn-workspace-filter-widget--start {\n\tflex-direction: row;\n\tflex-wrap: wrap;\n\talign-items: center;\n\tgap: 16px 24px;\n}\n\n.cn-workspace-filter-widget__text {\n\tflex: 1 1 320px;\n\tmin-width: 0;\n}\n\n.cn-workspace-filter-widget__title {\n\tmargin: 0 0 4px;\n\tfont-size: 17px;\n\tfont-weight: 700;\n\tcolor: var(--color-main-text);\n}\n\n.cn-workspace-filter-widget__description {\n\tmargin: 0;\n\tmax-width: 420px;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-workspace-filter-widget--start .cn-workspace-filter-widget__control,\n.cn-workspace-filter-widget--start > .v-select {\n\tflex: 0 1 480px;\n\tmin-width: 240px;\n}\n\n.cn-workspace-filter-widget__link {\n\tfont-weight: 600;\n\tcolor: var(--color-primary-element);\n\ttext-decoration: underline;\n\twhite-space: nowrap;\n}\n\n.cn-workspace-filter-widget__count {\n\tmargin-inline-start: 6px;\n\tpadding: 0 6px;\n\tborder-radius: 999px;\n\tbackground: var(--color-background-hover);\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.85em;\n}\n</style>\n"],"names":["_normalizeClass","_openBlock","_createElementBlock","_toDisplayString","_createBlock","_Fragment","_renderList","_createElementVNode"],"mappings":"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"}
@@ -205,6 +205,7 @@ function loadInMemory(input) {
205
205
  * @param {object} options Configuration options (endpoint / fetcher /
206
206
  * getAppConfigValue overrides).
207
207
  * @return {{ manifest: import('vue').Ref<object>, isLoading: import('vue').Ref<boolean>, validationErrors: import('vue').Ref<string[]|null>, unresolvedSentinels: import('vue').Ref<string[]> }}
208
+ * @spec openspec/changes/manifest-validator-self-contained/specs/manifest-validator-self-contained/spec.md#requirement-a-backend-manifest-delta-reaches-the-navigation
208
209
  */
209
210
  function loadFromBackend(appId, bundledManifest, options) {
210
211
  // Shallow by default (manifest-shallow-reactivity-by-default) — see
@@ -284,9 +285,13 @@ function loadFromBackend(appId, bundledManifest, options) {
284
285
  // The shallowRef reassignment is what re-renders consumers; the new
285
286
  // object stays shallow (not deep-observed) unless CnAppRoot upgrades it.
286
287
  manifest.value = resolved;
287
- } catch {
288
- // Defensive: any unexpected error leaves the bundled manifest in
289
- // place. Apps without a backend endpoint keep working.
288
+ } catch (error) {
289
+ // Any unexpected error leaves the bundled manifest in place, so
290
+ // apps keep working. Say so: a silent catch here once hid a
291
+ // validator that threw on every manifest, and the backend delta
292
+ // never reached the navigation.
293
+ // eslint-disable-next-line no-console
294
+ console.warn('[useAppManifest] Could not resolve the manifest; keeping the bundled manifest.', error);
290
295
  } finally {
291
296
  isLoading.value = false;
292
297
  }
@@ -1 +1 @@
1
- {"version":3,"file":"useAppManifest.js","sources":["../../../src/composables/useAppManifest.js"],"sourcesContent":["import axios from '@nextcloud/axios'\nimport { generateUrl } from '@nextcloud/router'\nimport { ref, shallowRef } from 'vue'\nimport { mergeManifestDelta } from '../utils/mergeManifestDelta.js'\nimport { resolveManifestSentinels } from '../utils/resolveManifestSentinels.js'\n\n/**\n * Lazily import the manifest validator. The validator plus its compiled Ajv\n * artifact weigh ~340KB minified and most apps never validate at runtime —\n * a static import makes every consumer bundle carry them. The dynamic import\n * splits them into an async chunk fetched on first validation only.\n *\n * @return {Promise<(manifest: object) => { valid: boolean, errors: Array<string> }>} The validateManifest function.\n */\nfunction loadValidator() {\n\treturn import(/* webpackChunkName: \"cn-manifest-validator\" */ '../utils/validateManifest.js')\n\t\t.then((mod) => mod.validateManifest)\n}\n\n/**\n * Composable that loads, resolves, and validates a Conduction app manifest.\n *\n * Two call shapes are supported:\n *\n * 1. **Legacy positional signature** —\n * `useAppManifest(appId, bundledManifest, options?)`.\n * Implements the four-phase flow specified in REQ-JMR-002 of the\n * `json-manifest-renderer` capability + the substitution step from the\n * `manifest-resolve-sentinel` capability:\n *\n * 1. Synchronous bundled load — `bundledManifest` is the immediate value.\n * 2. Async backend merge — fetches `/index.php/apps/{appId}/api/manifest`\n * and deep-merges any 200 response over the bundled manifest. 4xx /\n * 5xx / network errors are silently ignored so apps work without a\n * backend endpoint.\n * 3. Sentinel resolution — `@resolve:<key>` strings under\n * `pages[].config` are substituted with `IAppConfig` values via\n * the `resolveManifestSentinels` utility (see its module docs for\n * the resolution source chain). Unresolved keys surface on the\n * returned `unresolvedSentinels` ref.\n * 4. Validation — the resolved result is validated against\n * `app-manifest.schema.json`. On failure, the bundled manifest is\n * kept and a `console.warn` is emitted with the error list.\n *\n * 2. **In-memory signature** —\n * `useAppManifest({ manifest, validate? })`.\n * Mounts an already-constructed manifest object synchronously, with no\n * backend fetch, no deep-merge, and no sentinel resolution. Designed\n * for virtual-app hosts (e.g. the OpenBuilt app builder) that build\n * their manifests in memory from store state. When `validate: true`,\n * `validateManifest` is called synchronously before returning and any\n * failures populate `validationErrors`; validation is informational —\n * the input manifest is never replaced.\n *\n * The two shapes are discriminated by inspecting `typeof arguments[0]`:\n * - `string` → legacy fetch-and-merge branch\n * - non-null plain `object` → in-memory branch\n *\n * Both branches return the same shape:\n * `{ manifest, isLoading, validationErrors, unresolvedSentinels }`.\n *\n * The returned manifest ref is **shallow by default** (`shallowRef`) —\n * reactivity tracks `.value` reassignment (the hot-swap the future \"app\n * builder\" backend performs) but NOT deep mutation of the manifest's own\n * nested properties, so the whole manifest graph is not deep-observed on\n * boot. Every read-only consumer (`CnAppNav`, `CnPageRenderer`,\n * `CnDashboardPage`, …) only reads the tree and re-renders on the ref swap.\n * `CnAppRoot` upgrades the *same* object to deep-reactive IN PLACE (Vue 2.7\n * `reactive()`, preserving object identity) only when Buildiq in-app\n * editing is available — see `manifest-shallow-reactivity-by-default` and\n * `useManifestEditor.upgradeManifestToEditable`. The object is deliberately\n * NOT `markRaw`'d, because `markRaw` would make that in-place `reactive()`\n * upgrade a silent no-op and break ADR-041 live editing.\n *\n * @param {string | { manifest: object, validate?: boolean }} appIdOrOptions\n * Either a Nextcloud app ID (legacy positional signature) OR an options\n * object whose `manifest` field is the canonical, in-memory manifest\n * (in-memory signature).\n * @param {object} [bundledManifest] Manifest shipped with the app. Only\n * read when `appIdOrOptions` is a string (legacy signature); ignored in\n * the in-memory signature.\n * @param {object} [options] Configuration options. Only read in the\n * legacy signature; ignored in the in-memory signature.\n * @param {string} [options.endpoint] Override the backend fetch URL.\n * Useful for tests and alternative-host deployments.\n * @param {(url: string) => Promise<{ status: number, data: object }>} [options.fetcher] Override the fetch function. Must\n * return a promise resolving to `{ status: number, data: object }`.\n * Defaults to `axios.get` from `@nextcloud/axios` (which inherits the\n * Nextcloud CSRF token automatically).\n * @param {(appId: string, key: string) => Promise<string|null>} [options.getAppConfigValue] Override the\n * IAppConfig resolver consumed by `resolveManifestSentinels`. Useful\n * for tests that want to mount a fixture-driven config map.\n * @return {{ manifest: import('vue').Ref<object>, isLoading: import('vue').Ref<boolean>, validationErrors: import('vue').Ref<string[]|null>, unresolvedSentinels: import('vue').Ref<string[]> }}\n *\n * @example Basic usage (Composition API)\n * const { manifest, isLoading } = useAppManifest('decidesk', bundled)\n *\n * @example Inside an Options API component\n * export default {\n * setup() {\n * return useAppManifest('decidesk', bundled)\n * },\n * }\n *\n * @example Custom endpoint and fetcher (e.g. for tests)\n * useAppManifest('decidesk', bundled, {\n * endpoint: '/custom/manifest/url',\n * fetcher: (url) => Promise.resolve({ status: 200, data: { ... } }),\n * })\n *\n * @example With sentinel resolution + admin warning surface\n * const { manifest, unresolvedSentinels } = useAppManifest('softwarecatalog', bundled)\n * // unresolvedSentinels.value is e.g. ['voorzieningen_register']\n * // when that IAppConfig key is unset on the tenant.\n *\n * @example In-memory manifest (virtual-app host, e.g. OpenBuilt)\n * const builderManifest = buildManifestFromStore()\n * const { manifest, isLoading } = useAppManifest({ manifest: builderManifest })\n * // isLoading.value === false immediately; no HTTP fetch is issued.\n *\n * @example In-memory manifest with pre-mount validation\n * const { manifest, validationErrors } = useAppManifest({\n * manifest: builderManifest,\n * validate: true,\n * })\n * // validationErrors.value is null on success or string[] on failure.\n * // Validation is informational — the manifest is mounted either way.\n */\nexport function useAppManifest(appIdOrOptions, bundledManifest, options = {}) {\n\t// REQ-IMM-001 / REQ-IMM-004 — discriminate the call shape on the\n\t// first argument: a string enters the legacy fetch-and-merge branch;\n\t// a non-null plain object enters the new in-memory branch.\n\tif (isPlainObject(appIdOrOptions)) {\n\t\treturn loadInMemory(appIdOrOptions)\n\t}\n\n\treturn loadFromBackend(appIdOrOptions, bundledManifest, options)\n}\n\n/**\n * In-memory branch — mount a manifest object synchronously without any\n * backend fetch, deep-merge, or sentinel resolution.\n *\n * Implements REQ-IMM-001..REQ-IMM-003 of the\n * `in-memory-app-manifest-loader` capability:\n *\n * - `manifest` ref holds the input object by reference (no clone, no\n * mutation).\n * - `isLoading` is `false` from the first read because nothing is queued.\n * - `validationErrors` is `null` unless `options.validate === true` and\n * `validateManifest` returns a non-empty error list.\n * - `unresolvedSentinels` is always `[]` — sentinel resolution is a\n * backend-merge concern and does not apply to in-memory manifests.\n *\n * Validation is informational, mirroring the legacy branch's policy\n * (REQ-JMR-002 / Decision 2 in the change design): a failure emits a\n * `console.warn` and populates `validationErrors`, but the manifest is\n * never replaced.\n *\n * @param {{ manifest: object, validate?: boolean }} input The options\n * object passed to `useAppManifest`.\n * @return {{ manifest: import('vue').Ref<object>, isLoading: import('vue').Ref<boolean>, validationErrors: import('vue').Ref<string[]|null>, unresolvedSentinels: import('vue').Ref<string[]> }}\n */\nfunction loadInMemory(input) {\n\t// Shallow by default (manifest-shallow-reactivity-by-default): track the\n\t// ref reassignment, not deep mutation of the manifest tree. NOT markRaw'd —\n\t// CnAppRoot upgrades this same object in place via reactive() when Buildiq\n\t// editing is available, which markRaw would silently block.\n\tconst manifest = shallowRef(input.manifest)\n\tconst isLoading = ref(false)\n\tconst validationErrors = ref(null)\n\tconst unresolvedSentinels = ref([])\n\tconst orphanedDeltaPaths = ref([])\n\n\tif (input.validate === true) {\n\t\t// Fire-and-forget: validation here is informational (the manifest is\n\t\t// mounted unchanged either way), so the lazy validator load may\n\t\t// resolve after mount — validationErrors is a ref consumers watch.\n\t\tloadValidator().then((validateManifest) => {\n\t\t\tconst result = validateManifest(input.manifest)\n\t\t\tif (!result.valid) {\n\t\t\t\tvalidationErrors.value = result.errors\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.warn(\n\t\t\t\t\t'[useAppManifest] In-memory manifest failed schema validation; manifest is mounted unchanged (validation is informational).',\n\t\t\t\t\tresult.errors,\n\t\t\t\t)\n\t\t\t}\n\t\t})\n\t}\n\n\treturn { manifest, isLoading, validationErrors, unresolvedSentinels, orphanedDeltaPaths }\n}\n\n/**\n * Legacy fetch-and-merge branch — synchronous bundled load, async backend\n * merge, sentinel resolution, validation. Implements REQ-JMR-002 of the\n * `json-manifest-renderer` capability. Preserved verbatim for backwards\n * compatibility with all current consumers (OpenRegister, OpenCatalogi,\n * Dossiq, Pipelinq, Launchpad, Decidiq, Filinq, Larpinq,\n * Stackiq).\n *\n * @param {string} appId Nextcloud app ID.\n * @param {object} bundledManifest The manifest shipped with the app.\n * @param {object} options Configuration options (endpoint / fetcher /\n * getAppConfigValue overrides).\n * @return {{ manifest: import('vue').Ref<object>, isLoading: import('vue').Ref<boolean>, validationErrors: import('vue').Ref<string[]|null>, unresolvedSentinels: import('vue').Ref<string[]> }}\n */\nfunction loadFromBackend(appId, bundledManifest, options) {\n\t// Shallow by default (manifest-shallow-reactivity-by-default) — see\n\t// loadInMemory. The later `manifest.value = resolved` swap still triggers\n\t// reactivity because reassigning the shallowRef itself is tracked.\n\tconst manifest = shallowRef(bundledManifest)\n\tconst isLoading = ref(true)\n\tconst validationErrors = ref(null)\n\tconst unresolvedSentinels = ref([])\n\tconst orphanedDeltaPaths = ref([])\n\n\tconst endpoint = options.endpoint ?? generateUrl(`/apps/${appId}/api/manifest`)\n\tconst fetcher = options.fetcher ?? ((url) => axios.get(url))\n\n\t;(async () => {\n\t\ttry {\n\t\t\t// Phase 1: optional backend merge. The fetch is best-effort —\n\t\t\t// most apps DON'T serve `/api/manifest` (Nextcloud returns the\n\t\t\t// SPA index HTML with a 200, which is not a plain object), so\n\t\t\t// `base` stays the bundled manifest in the common case. Any\n\t\t\t// failure here is swallowed and we proceed with `bundledManifest`.\n\t\t\tlet base = bundledManifest\n\t\t\ttry {\n\t\t\t\tconst response = await fetcher(endpoint)\n\t\t\t\t// Only a 200 with a plain-object body is a real manifest\n\t\t\t\t// response. An HTML string (SPA fallback), a non-200, or a\n\t\t\t\t// missing body all mean \"no backend manifest\" → keep bundled.\n\t\t\t\tif (response && response.status === 200 && isPlainObject(response.data)) {\n\t\t\t\t\t// `delta` mode applies a keyed structural delta to the\n\t\t\t\t\t// bundled manifest (ADR-036 Amendment); default mode\n\t\t\t\t\t// deep-merges as before.\n\t\t\t\t\tif (options.mergeStrategy === 'delta') {\n\t\t\t\t\t\tconst deltaResult = mergeManifestDelta(bundledManifest, response.data)\n\t\t\t\t\t\tbase = deltaResult.manifest\n\t\t\t\t\t\torphanedDeltaPaths.value = deltaResult.orphanedDeltaPaths\n\t\t\t\t\t} else {\n\t\t\t\t\t\tbase = deepMerge(bundledManifest, response.data)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// Network / 404 / unauthenticated — keep the bundled manifest.\n\t\t\t}\n\n\t\t\t// Phase 2: sentinel resolution ALWAYS runs, on the bundled (or\n\t\t\t// merged) manifest, INDEPENDENT of whether the backend fetch\n\t\t\t// succeeded. `@resolve:<key>` sentinels resolve from\n\t\t\t// `@nextcloud/initial-state` (zero-network) then a per-key\n\t\t\t// `/api/configs/{key}` fetch — neither depends on `/api/manifest`\n\t\t\t// serving a valid body. Previously this only ran inside the\n\t\t\t// successful-fetch branch, so the >90% of apps that don't serve\n\t\t\t// `/api/manifest` never had their sentinels substituted.\n\t\t\t//\n\t\t\t// Runs BEFORE validation per REQ-MRS-002: the validator MUST\n\t\t\t// NEVER observe an unresolved sentinel. Resolution failures\n\t\t\t// (unset IAppConfig keys) substitute null and accumulate on\n\t\t\t// `unresolvedSentinels`; they do NOT block validation.\n\t\t\tconst { manifest: resolved, unresolved } = await resolveManifestSentinels(base, appId, {\n\t\t\t\tgetAppConfigValue: options.getAppConfigValue,\n\t\t\t})\n\t\t\tunresolvedSentinels.value = unresolved\n\n\t\t\t// Phase 3: validation. On failure, keep the bundled manifest\n\t\t\t// (informational policy — never replace with an invalid doc).\n\t\t\tconst validateManifest = await loadValidator()\n\t\t\tconst result = validateManifest(resolved)\n\t\t\tif (!result.valid) {\n\t\t\t\tvalidationErrors.value = result.errors\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.warn(\n\t\t\t\t\t'[useAppManifest] Resolved manifest failed schema validation; keeping the unresolved bundled manifest.',\n\t\t\t\t\tresult.errors,\n\t\t\t\t)\n\t\t\t\treturn\n\t\t\t}\n\t\t\t// Publish the resolved manifest whenever it differs from the\n\t\t\t// bundled input (sentinels substituted and/or backend merged).\n\t\t\t// The shallowRef reassignment is what re-renders consumers; the new\n\t\t\t// object stays shallow (not deep-observed) unless CnAppRoot upgrades it.\n\t\t\tmanifest.value = resolved\n\t\t} catch {\n\t\t\t// Defensive: any unexpected error leaves the bundled manifest in\n\t\t\t// place. Apps without a backend endpoint keep working.\n\t\t} finally {\n\t\t\tisLoading.value = false\n\t\t}\n\t})()\n\n\treturn { manifest, isLoading, validationErrors, unresolvedSentinels, orphanedDeltaPaths }\n}\n\n/**\n * Deep-merge `source` into `target`, returning a new object. Plain\n * objects are merged recursively; arrays are replaced (not concatenated)\n * to match typical deep-merge semantics expected by manifest overrides.\n *\n * @param {object} target Base object.\n * @param {object} source Object whose values take precedence.\n * @return {object} New merged object.\n */\nfunction deepMerge(target, source) {\n\tif (!isPlainObject(target)) {\n\t\treturn source\n\t}\n\tif (!isPlainObject(source)) {\n\t\treturn source\n\t}\n\tconst out = { ...target }\n\tfor (const key of Object.keys(source)) {\n\t\tif (isPlainObject(source[key]) && isPlainObject(target[key])) {\n\t\t\tout[key] = deepMerge(target[key], source[key])\n\t\t} else {\n\t\t\tout[key] = source[key]\n\t\t}\n\t}\n\treturn out\n}\n\nfunction isPlainObject(value) {\n\treturn value !== null && typeof value === 'object' && !Array.isArray(value)\n}\n"],"names":["axios"],"mappings":";;;;;;AAMA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,SAAS,aAAa,GAAG;AACzB,CAAC,OAAO,uDAAuD,8BAA8B,CAAC;AAC9F,GAAG,IAAI,CAAC,CAAC,GAAG,KAAK,GAAG,CAAC,gBAAgB,CAAC;AACtC,CAAC;AACD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAAS,cAAc,CAAC,cAAc,EAAE,eAAe,EAAE,OAAO,GAAG,EAAE,EAAE;AAC9E;AACA;AACA;AACA,CAAC,IAAI,aAAa,CAAC,cAAc,CAAC,EAAE;AACpC,EAAE,OAAO,YAAY,CAAC,cAAc,CAAC;AACrC,EAAE;AACF;AACA,CAAC,OAAO,eAAe,CAAC,cAAc,EAAE,eAAe,EAAE,OAAO,CAAC;AACjE,CAAC;AACD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,SAAS,YAAY,CAAC,KAAK,EAAE;AAC7B;AACA;AACA;AACA;AACA,CAAC,MAAM,QAAQ,GAAG,UAAU,CAAC,KAAK,CAAC,QAAQ,EAAC;AAC5C,CAAC,MAAM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"}
1
+ {"version":3,"file":"useAppManifest.js","sources":["../../../src/composables/useAppManifest.js"],"sourcesContent":["import axios from '@nextcloud/axios'\nimport { generateUrl } from '@nextcloud/router'\nimport { ref, shallowRef } from 'vue'\nimport { mergeManifestDelta } from '../utils/mergeManifestDelta.js'\nimport { resolveManifestSentinels } from '../utils/resolveManifestSentinels.js'\n\n/**\n * Lazily import the manifest validator. The validator plus its compiled Ajv\n * artifact weigh ~340KB minified and most apps never validate at runtime —\n * a static import makes every consumer bundle carry them. The dynamic import\n * splits them into an async chunk fetched on first validation only.\n *\n * @return {Promise<(manifest: object) => { valid: boolean, errors: Array<string> }>} The validateManifest function.\n */\nfunction loadValidator() {\n\treturn import(/* webpackChunkName: \"cn-manifest-validator\" */ '../utils/validateManifest.js')\n\t\t.then((mod) => mod.validateManifest)\n}\n\n/**\n * Composable that loads, resolves, and validates a Conduction app manifest.\n *\n * Two call shapes are supported:\n *\n * 1. **Legacy positional signature** —\n * `useAppManifest(appId, bundledManifest, options?)`.\n * Implements the four-phase flow specified in REQ-JMR-002 of the\n * `json-manifest-renderer` capability + the substitution step from the\n * `manifest-resolve-sentinel` capability:\n *\n * 1. Synchronous bundled load — `bundledManifest` is the immediate value.\n * 2. Async backend merge — fetches `/index.php/apps/{appId}/api/manifest`\n * and deep-merges any 200 response over the bundled manifest. 4xx /\n * 5xx / network errors are silently ignored so apps work without a\n * backend endpoint.\n * 3. Sentinel resolution — `@resolve:<key>` strings under\n * `pages[].config` are substituted with `IAppConfig` values via\n * the `resolveManifestSentinels` utility (see its module docs for\n * the resolution source chain). Unresolved keys surface on the\n * returned `unresolvedSentinels` ref.\n * 4. Validation — the resolved result is validated against\n * `app-manifest.schema.json`. On failure, the bundled manifest is\n * kept and a `console.warn` is emitted with the error list.\n *\n * 2. **In-memory signature** —\n * `useAppManifest({ manifest, validate? })`.\n * Mounts an already-constructed manifest object synchronously, with no\n * backend fetch, no deep-merge, and no sentinel resolution. Designed\n * for virtual-app hosts (e.g. the OpenBuilt app builder) that build\n * their manifests in memory from store state. When `validate: true`,\n * `validateManifest` is called synchronously before returning and any\n * failures populate `validationErrors`; validation is informational —\n * the input manifest is never replaced.\n *\n * The two shapes are discriminated by inspecting `typeof arguments[0]`:\n * - `string` → legacy fetch-and-merge branch\n * - non-null plain `object` → in-memory branch\n *\n * Both branches return the same shape:\n * `{ manifest, isLoading, validationErrors, unresolvedSentinels }`.\n *\n * The returned manifest ref is **shallow by default** (`shallowRef`) —\n * reactivity tracks `.value` reassignment (the hot-swap the future \"app\n * builder\" backend performs) but NOT deep mutation of the manifest's own\n * nested properties, so the whole manifest graph is not deep-observed on\n * boot. Every read-only consumer (`CnAppNav`, `CnPageRenderer`,\n * `CnDashboardPage`, …) only reads the tree and re-renders on the ref swap.\n * `CnAppRoot` upgrades the *same* object to deep-reactive IN PLACE (Vue 2.7\n * `reactive()`, preserving object identity) only when Buildiq in-app\n * editing is available — see `manifest-shallow-reactivity-by-default` and\n * `useManifestEditor.upgradeManifestToEditable`. The object is deliberately\n * NOT `markRaw`'d, because `markRaw` would make that in-place `reactive()`\n * upgrade a silent no-op and break ADR-041 live editing.\n *\n * @param {string | { manifest: object, validate?: boolean }} appIdOrOptions\n * Either a Nextcloud app ID (legacy positional signature) OR an options\n * object whose `manifest` field is the canonical, in-memory manifest\n * (in-memory signature).\n * @param {object} [bundledManifest] Manifest shipped with the app. Only\n * read when `appIdOrOptions` is a string (legacy signature); ignored in\n * the in-memory signature.\n * @param {object} [options] Configuration options. Only read in the\n * legacy signature; ignored in the in-memory signature.\n * @param {string} [options.endpoint] Override the backend fetch URL.\n * Useful for tests and alternative-host deployments.\n * @param {(url: string) => Promise<{ status: number, data: object }>} [options.fetcher] Override the fetch function. Must\n * return a promise resolving to `{ status: number, data: object }`.\n * Defaults to `axios.get` from `@nextcloud/axios` (which inherits the\n * Nextcloud CSRF token automatically).\n * @param {(appId: string, key: string) => Promise<string|null>} [options.getAppConfigValue] Override the\n * IAppConfig resolver consumed by `resolveManifestSentinels`. Useful\n * for tests that want to mount a fixture-driven config map.\n * @return {{ manifest: import('vue').Ref<object>, isLoading: import('vue').Ref<boolean>, validationErrors: import('vue').Ref<string[]|null>, unresolvedSentinels: import('vue').Ref<string[]> }}\n *\n * @example Basic usage (Composition API)\n * const { manifest, isLoading } = useAppManifest('decidesk', bundled)\n *\n * @example Inside an Options API component\n * export default {\n * setup() {\n * return useAppManifest('decidesk', bundled)\n * },\n * }\n *\n * @example Custom endpoint and fetcher (e.g. for tests)\n * useAppManifest('decidesk', bundled, {\n * endpoint: '/custom/manifest/url',\n * fetcher: (url) => Promise.resolve({ status: 200, data: { ... } }),\n * })\n *\n * @example With sentinel resolution + admin warning surface\n * const { manifest, unresolvedSentinels } = useAppManifest('softwarecatalog', bundled)\n * // unresolvedSentinels.value is e.g. ['voorzieningen_register']\n * // when that IAppConfig key is unset on the tenant.\n *\n * @example In-memory manifest (virtual-app host, e.g. OpenBuilt)\n * const builderManifest = buildManifestFromStore()\n * const { manifest, isLoading } = useAppManifest({ manifest: builderManifest })\n * // isLoading.value === false immediately; no HTTP fetch is issued.\n *\n * @example In-memory manifest with pre-mount validation\n * const { manifest, validationErrors } = useAppManifest({\n * manifest: builderManifest,\n * validate: true,\n * })\n * // validationErrors.value is null on success or string[] on failure.\n * // Validation is informational — the manifest is mounted either way.\n */\nexport function useAppManifest(appIdOrOptions, bundledManifest, options = {}) {\n\t// REQ-IMM-001 / REQ-IMM-004 — discriminate the call shape on the\n\t// first argument: a string enters the legacy fetch-and-merge branch;\n\t// a non-null plain object enters the new in-memory branch.\n\tif (isPlainObject(appIdOrOptions)) {\n\t\treturn loadInMemory(appIdOrOptions)\n\t}\n\n\treturn loadFromBackend(appIdOrOptions, bundledManifest, options)\n}\n\n/**\n * In-memory branch — mount a manifest object synchronously without any\n * backend fetch, deep-merge, or sentinel resolution.\n *\n * Implements REQ-IMM-001..REQ-IMM-003 of the\n * `in-memory-app-manifest-loader` capability:\n *\n * - `manifest` ref holds the input object by reference (no clone, no\n * mutation).\n * - `isLoading` is `false` from the first read because nothing is queued.\n * - `validationErrors` is `null` unless `options.validate === true` and\n * `validateManifest` returns a non-empty error list.\n * - `unresolvedSentinels` is always `[]` — sentinel resolution is a\n * backend-merge concern and does not apply to in-memory manifests.\n *\n * Validation is informational, mirroring the legacy branch's policy\n * (REQ-JMR-002 / Decision 2 in the change design): a failure emits a\n * `console.warn` and populates `validationErrors`, but the manifest is\n * never replaced.\n *\n * @param {{ manifest: object, validate?: boolean }} input The options\n * object passed to `useAppManifest`.\n * @return {{ manifest: import('vue').Ref<object>, isLoading: import('vue').Ref<boolean>, validationErrors: import('vue').Ref<string[]|null>, unresolvedSentinels: import('vue').Ref<string[]> }}\n */\nfunction loadInMemory(input) {\n\t// Shallow by default (manifest-shallow-reactivity-by-default): track the\n\t// ref reassignment, not deep mutation of the manifest tree. NOT markRaw'd —\n\t// CnAppRoot upgrades this same object in place via reactive() when Buildiq\n\t// editing is available, which markRaw would silently block.\n\tconst manifest = shallowRef(input.manifest)\n\tconst isLoading = ref(false)\n\tconst validationErrors = ref(null)\n\tconst unresolvedSentinels = ref([])\n\tconst orphanedDeltaPaths = ref([])\n\n\tif (input.validate === true) {\n\t\t// Fire-and-forget: validation here is informational (the manifest is\n\t\t// mounted unchanged either way), so the lazy validator load may\n\t\t// resolve after mount — validationErrors is a ref consumers watch.\n\t\tloadValidator().then((validateManifest) => {\n\t\t\tconst result = validateManifest(input.manifest)\n\t\t\tif (!result.valid) {\n\t\t\t\tvalidationErrors.value = result.errors\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.warn(\n\t\t\t\t\t'[useAppManifest] In-memory manifest failed schema validation; manifest is mounted unchanged (validation is informational).',\n\t\t\t\t\tresult.errors,\n\t\t\t\t)\n\t\t\t}\n\t\t})\n\t}\n\n\treturn { manifest, isLoading, validationErrors, unresolvedSentinels, orphanedDeltaPaths }\n}\n\n/**\n * Legacy fetch-and-merge branch — synchronous bundled load, async backend\n * merge, sentinel resolution, validation. Implements REQ-JMR-002 of the\n * `json-manifest-renderer` capability. Preserved verbatim for backwards\n * compatibility with all current consumers (OpenRegister, OpenCatalogi,\n * Dossiq, Pipelinq, Launchpad, Decidiq, Filinq, Larpinq,\n * Stackiq).\n *\n * @param {string} appId Nextcloud app ID.\n * @param {object} bundledManifest The manifest shipped with the app.\n * @param {object} options Configuration options (endpoint / fetcher /\n * getAppConfigValue overrides).\n * @return {{ manifest: import('vue').Ref<object>, isLoading: import('vue').Ref<boolean>, validationErrors: import('vue').Ref<string[]|null>, unresolvedSentinels: import('vue').Ref<string[]> }}\n * @spec openspec/changes/manifest-validator-self-contained/specs/manifest-validator-self-contained/spec.md#requirement-a-backend-manifest-delta-reaches-the-navigation\n */\nfunction loadFromBackend(appId, bundledManifest, options) {\n\t// Shallow by default (manifest-shallow-reactivity-by-default) — see\n\t// loadInMemory. The later `manifest.value = resolved` swap still triggers\n\t// reactivity because reassigning the shallowRef itself is tracked.\n\tconst manifest = shallowRef(bundledManifest)\n\tconst isLoading = ref(true)\n\tconst validationErrors = ref(null)\n\tconst unresolvedSentinels = ref([])\n\tconst orphanedDeltaPaths = ref([])\n\n\tconst endpoint = options.endpoint ?? generateUrl(`/apps/${appId}/api/manifest`)\n\tconst fetcher = options.fetcher ?? ((url) => axios.get(url))\n\n\t;(async () => {\n\t\ttry {\n\t\t\t// Phase 1: optional backend merge. The fetch is best-effort —\n\t\t\t// most apps DON'T serve `/api/manifest` (Nextcloud returns the\n\t\t\t// SPA index HTML with a 200, which is not a plain object), so\n\t\t\t// `base` stays the bundled manifest in the common case. Any\n\t\t\t// failure here is swallowed and we proceed with `bundledManifest`.\n\t\t\tlet base = bundledManifest\n\t\t\ttry {\n\t\t\t\tconst response = await fetcher(endpoint)\n\t\t\t\t// Only a 200 with a plain-object body is a real manifest\n\t\t\t\t// response. An HTML string (SPA fallback), a non-200, or a\n\t\t\t\t// missing body all mean \"no backend manifest\" → keep bundled.\n\t\t\t\tif (response && response.status === 200 && isPlainObject(response.data)) {\n\t\t\t\t\t// `delta` mode applies a keyed structural delta to the\n\t\t\t\t\t// bundled manifest (ADR-036 Amendment); default mode\n\t\t\t\t\t// deep-merges as before.\n\t\t\t\t\tif (options.mergeStrategy === 'delta') {\n\t\t\t\t\t\tconst deltaResult = mergeManifestDelta(bundledManifest, response.data)\n\t\t\t\t\t\tbase = deltaResult.manifest\n\t\t\t\t\t\torphanedDeltaPaths.value = deltaResult.orphanedDeltaPaths\n\t\t\t\t\t} else {\n\t\t\t\t\t\tbase = deepMerge(bundledManifest, response.data)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// Network / 404 / unauthenticated — keep the bundled manifest.\n\t\t\t}\n\n\t\t\t// Phase 2: sentinel resolution ALWAYS runs, on the bundled (or\n\t\t\t// merged) manifest, INDEPENDENT of whether the backend fetch\n\t\t\t// succeeded. `@resolve:<key>` sentinels resolve from\n\t\t\t// `@nextcloud/initial-state` (zero-network) then a per-key\n\t\t\t// `/api/configs/{key}` fetch — neither depends on `/api/manifest`\n\t\t\t// serving a valid body. Previously this only ran inside the\n\t\t\t// successful-fetch branch, so the >90% of apps that don't serve\n\t\t\t// `/api/manifest` never had their sentinels substituted.\n\t\t\t//\n\t\t\t// Runs BEFORE validation per REQ-MRS-002: the validator MUST\n\t\t\t// NEVER observe an unresolved sentinel. Resolution failures\n\t\t\t// (unset IAppConfig keys) substitute null and accumulate on\n\t\t\t// `unresolvedSentinels`; they do NOT block validation.\n\t\t\tconst { manifest: resolved, unresolved } = await resolveManifestSentinels(base, appId, {\n\t\t\t\tgetAppConfigValue: options.getAppConfigValue,\n\t\t\t})\n\t\t\tunresolvedSentinels.value = unresolved\n\n\t\t\t// Phase 3: validation. On failure, keep the bundled manifest\n\t\t\t// (informational policy — never replace with an invalid doc).\n\t\t\tconst validateManifest = await loadValidator()\n\t\t\tconst result = validateManifest(resolved)\n\t\t\tif (!result.valid) {\n\t\t\t\tvalidationErrors.value = result.errors\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.warn(\n\t\t\t\t\t'[useAppManifest] Resolved manifest failed schema validation; keeping the unresolved bundled manifest.',\n\t\t\t\t\tresult.errors,\n\t\t\t\t)\n\t\t\t\treturn\n\t\t\t}\n\t\t\t// Publish the resolved manifest whenever it differs from the\n\t\t\t// bundled input (sentinels substituted and/or backend merged).\n\t\t\t// The shallowRef reassignment is what re-renders consumers; the new\n\t\t\t// object stays shallow (not deep-observed) unless CnAppRoot upgrades it.\n\t\t\tmanifest.value = resolved\n\t\t} catch (error) {\n\t\t\t// Any unexpected error leaves the bundled manifest in place, so\n\t\t\t// apps keep working. Say so: a silent catch here once hid a\n\t\t\t// validator that threw on every manifest, and the backend delta\n\t\t\t// never reached the navigation.\n\t\t\t// eslint-disable-next-line no-console\n\t\t\tconsole.warn('[useAppManifest] Could not resolve the manifest; keeping the bundled manifest.', error)\n\t\t} finally {\n\t\t\tisLoading.value = false\n\t\t}\n\t})()\n\n\treturn { manifest, isLoading, validationErrors, unresolvedSentinels, orphanedDeltaPaths }\n}\n\n/**\n * Deep-merge `source` into `target`, returning a new object. Plain\n * objects are merged recursively; arrays are replaced (not concatenated)\n * to match typical deep-merge semantics expected by manifest overrides.\n *\n * @param {object} target Base object.\n * @param {object} source Object whose values take precedence.\n * @return {object} New merged object.\n */\nfunction deepMerge(target, source) {\n\tif (!isPlainObject(target)) {\n\t\treturn source\n\t}\n\tif (!isPlainObject(source)) {\n\t\treturn source\n\t}\n\tconst out = { ...target }\n\tfor (const key of Object.keys(source)) {\n\t\tif (isPlainObject(source[key]) && isPlainObject(target[key])) {\n\t\t\tout[key] = deepMerge(target[key], source[key])\n\t\t} else {\n\t\t\tout[key] = source[key]\n\t\t}\n\t}\n\treturn out\n}\n\nfunction isPlainObject(value) {\n\treturn value !== null && typeof value === 'object' && !Array.isArray(value)\n}\n"],"names":["axios"],"mappings":";;;;;;AAMA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,SAAS,aAAa,GAAG;AACzB,CAAC,OAAO,uDAAuD,8BAA8B,CAAC;AAC9F,GAAG,IAAI,CAAC,CAAC,GAAG,KAAK,GAAG,CAAC,gBAAgB,CAAC;AACtC,CAAC;AACD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAAS,cAAc,CAAC,cAAc,EAAE,eAAe,EAAE,OAAO,GAAG,EAAE,EAAE;AAC9E;AACA;AACA;AACA,CAAC,IAAI,aAAa,CAAC,cAAc,CAAC,EAAE;AACpC,EAAE,OAAO,YAAY,CAAC,cAAc,CAAC;AACrC,EAAE;AACF;AACA,CAAC,OAAO,eAAe,CAAC,cAAc,EAAE,eAAe,EAAE,OAAO,CAAC;AACjE,CAAC;AACD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,SAAS,YAAY,CAAC,KAAK,EAAE;AAC7B;AACA;AACA;AACA;AACA,CAAC,MAAM,QAAQ,GAAG,UAAU,CAAC,KAAK,CAAC,QAAQ,EAAC;AAC5C,CAAC,MAAM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"}