@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
@@ -0,0 +1,603 @@
1
+ <!--
2
+ - SPDX-FileCopyrightText: 2026 Conduction B.V.
3
+ - SPDX-License-Identifier: EUPL-1.2
4
+ -->
5
+
6
+ <template>
7
+ <div
8
+ class="cn-record-list"
9
+ :class="`cn-record-list--${presentation}`"
10
+ data-testid="cn-record-list">
11
+ <p v-if="error" class="cn-record-list__error" data-testid="cn-record-list-error">
12
+ {{ errorLabel }}
13
+ </p>
14
+ <div v-else-if="loading && rows.length === 0" class="cn-record-list__loading">
15
+ <NcLoadingIcon :size="20" :name="loadingLabel" />
16
+ </div>
17
+ <p v-else-if="rows.length === 0" class="cn-record-list__empty" data-testid="cn-record-list-empty">
18
+ {{ emptyLabel }}
19
+ </p>
20
+ <ul v-else class="cn-record-list__list">
21
+ <li
22
+ v-for="item in items"
23
+ :key="item.key"
24
+ class="cn-record-list__item"
25
+ data-testid="cn-record-list-item">
26
+ <!-- rows: title and meta, then the pill, the date and a chevron. -->
27
+ <component
28
+ :is="item.tag"
29
+ v-if="presentation === 'rows'"
30
+ class="cn-record-list__row"
31
+ :class="{ 'cn-record-list__row--link': item.tag !== 'div' }"
32
+ v-bind="item.attrs">
33
+ <span class="cn-record-list__main">
34
+ <span v-if="item.title" class="cn-record-list__title">{{ item.title }}</span>
35
+ <span v-if="item.meta" class="cn-record-list__meta">{{ item.meta }}</span>
36
+ </span>
37
+ <span v-if="item.pill || item.trailing" class="cn-record-list__aside">
38
+ <CnStatusBadge
39
+ v-if="item.pill"
40
+ :label="item.pill.label"
41
+ :variant="item.pill.variant"
42
+ size="small" />
43
+ <span v-if="item.trailing" class="cn-record-list__trailing">{{ item.trailing }}</span>
44
+ </span>
45
+ <ChevronRight
46
+ v-if="item.tag !== 'div'"
47
+ class="cn-record-list__chevron"
48
+ :size="18"
49
+ aria-hidden="true" />
50
+ </component>
51
+
52
+ <!-- feed: a round icon, a meta line and the text. -->
53
+ <div v-else-if="presentation === 'feed'" class="cn-record-list__feed-row">
54
+ <span class="cn-record-list__feed-icon" aria-hidden="true">
55
+ <CnIcon :name="item.icon" :size="18" />
56
+ </span>
57
+ <component :is="item.tag" class="cn-record-list__main" v-bind="item.attrs">
58
+ <span v-if="item.meta" class="cn-record-list__meta">{{ item.meta }}</span>
59
+ <span v-if="item.text" class="cn-record-list__text">{{ item.text }}</span>
60
+ </component>
61
+ </div>
62
+
63
+ <!-- inline: a label and the title, the title a link. -->
64
+ <div v-else class="cn-record-list__inline-row">
65
+ <span v-if="item.label" class="cn-record-list__label">{{ item.label }}</span>
66
+ <component :is="item.tag" class="cn-record-list__inline-title" v-bind="item.attrs">
67
+ {{ item.title }}
68
+ </component>
69
+ </div>
70
+ </li>
71
+ </ul>
72
+ </div>
73
+ </template>
74
+
75
+ <script>
76
+ import { subscribe, unsubscribe } from '@nextcloud/event-bus'
77
+ import { translate as t } from '@nextcloud/l10n'
78
+ import { NcLoadingIcon } from '@nextcloud/vue'
79
+ import ChevronRight from 'vue-material-design-icons/ChevronRight.vue'
80
+ import CnIcon from '../CnIcon/CnIcon.vue'
81
+ import CnStatusBadge from '../CnStatusBadge/CnStatusBadge.vue'
82
+ import { isBadgeVariant } from '../../utils/badgeVariants.js'
83
+ import { fillTemplate, firstFilled } from '../../utils/fillTemplate.js'
84
+ import { readPath } from '../../utils/readPath.js'
85
+ import { dropOptionalUnresolved, hasUnresolvedTokens, resolveFilterTokens } from '../../utils/resolveFilterTokens.js'
86
+
87
+ const PAGE_REFRESH_CHANNEL = 'cn:page:refresh'
88
+ const PRESENTATIONS = ['rows', 'feed', 'inline']
89
+
90
+ /**
91
+ * Unwrap a value that may be a ref.
92
+ *
93
+ * @param {unknown} value The value.
94
+ * @return {unknown} The plain value.
95
+ */
96
+ function unwrap(value) {
97
+ return value && typeof value === 'object' && 'value' in value ? value.value : value
98
+ }
99
+
100
+ /**
101
+ * CnRecordListWidget — the objects of another schema that point at this
102
+ * record, drawn as the boards draw them (screens-relation-pages).
103
+ *
104
+ * A person page lists its open cases ("Verlichting fietspad Lindelaan,
105
+ * 2026-0082 · Woo-verzoek, In behandeling, Termijn 1 nov"), its latest contact
106
+ * moments (an icon by channel, a meta line, the summary) and its relations
107
+ * ("Secretaris van Buurtvereniging Oost"). Each is a list of another schema's
108
+ * objects filtered on a reference to this record. The source keys are those of
109
+ * `object-list` (`register`, `schema`, `filter` with the `@objectId` /
110
+ * `@object.<field>` / `@workspace.<key>` tokens, `sort`, `limit`, `extend`,
111
+ * `rowRoute`); what differs is the drawing: `presentation` is `rows`, `feed`
112
+ * or `inline`, and each line is a template (`"{identifier} · {caseTypeName}"`,
113
+ * `"Termijn {deadline|date}"`).
114
+ *
115
+ * A content-only catalog type: on a card surface the detail host draws the
116
+ * titled card. The widget emits `header` with `{ link, note }` (the
117
+ * `headerLink` and `headerNote` with `{total}` filled in) for that card's
118
+ * header.
119
+ */
120
+ export default {
121
+ name: 'CnRecordListWidget',
122
+
123
+ components: { ChevronRight, CnIcon, CnStatusBadge, NcLoadingIcon },
124
+
125
+ inject: {
126
+ /** Detail-page object context (`{ objectId, object, register, schema }`). */
127
+ cnObjectContext: { default: null },
128
+ /** Page workspace bag, for `@workspace.<key>` tokens. */
129
+ cnWorkspaceContext: { default: null },
130
+ /** The host app's translate function. */
131
+ cnTranslate: { default: () => (key) => key },
132
+ },
133
+
134
+ props: {
135
+ /**
136
+ * The list config: `{ register, schema, filter?, sort?, limit?, extend?,
137
+ * presentation?, title?, meta?, text?, label?, pill?, trailing?, icon?,
138
+ * rowRoute?, rowIdField?, headerLink?, headerNote?, emptyText?, errorText? }`.
139
+ *
140
+ * @type {object}
141
+ */
142
+ content: {
143
+ type: Object,
144
+ default: () => ({}),
145
+ },
146
+
147
+ /** The current record's id, for the `@objectId` token. */
148
+ objectId: {
149
+ type: [String, Number],
150
+ default: null,
151
+ },
152
+
153
+ /** The current record, for `@object.<field>` tokens. */
154
+ objectData: {
155
+ type: Object,
156
+ default: null,
157
+ },
158
+ },
159
+
160
+ emits: ['header'],
161
+
162
+ data() {
163
+ return {
164
+ rows: [],
165
+ total: 0,
166
+ loading: false,
167
+ error: false,
168
+ }
169
+ },
170
+
171
+ computed: {
172
+ /** @return {string} `rows`, `feed` or `inline`. */
173
+ presentation() {
174
+ const value = this.content?.presentation
175
+ return PRESENTATIONS.includes(value) ? value : 'rows'
176
+ },
177
+
178
+ /** @return {object} The token context: record, id and workspace. */
179
+ tokenCtx() {
180
+ const injected = unwrap(this.cnObjectContext) || {}
181
+ return {
182
+ ...injected,
183
+ objectId: this.objectId ?? injected.objectId ?? this.objectData?.id ?? null,
184
+ object: this.objectData || injected.object || null,
185
+ workspace: unwrap(this.cnWorkspaceContext) || {},
186
+ }
187
+ },
188
+
189
+ /** @return {object} The filter with its tokens resolved. */
190
+ resolvedFilter() {
191
+ const filter = this.content?.filter
192
+ return dropOptionalUnresolved(resolveFilterTokens(filter && typeof filter === 'object' ? filter : {}, this.tokenCtx))
193
+ },
194
+
195
+ /** @return {boolean} A required token (the record id, say) has no value yet. */
196
+ waiting() {
197
+ return hasUnresolvedTokens(this.resolvedFilter)
198
+ },
199
+
200
+ /** @return {number} The fetch cap. */
201
+ limit() {
202
+ const value = Number(this.content?.limit)
203
+ return Number.isFinite(value) && value > 0 ? Math.floor(value) : 5
204
+ },
205
+
206
+ /** @return {string} What a fetch depends on; a change fetches again. */
207
+ requestKey() {
208
+ const c = this.content || {}
209
+ return JSON.stringify([c.register, c.schema, this.resolvedFilter, c.sort, this.limit, c.extend, this.waiting])
210
+ },
211
+
212
+ /** @return {Array<object>} The rows ready to draw. */
213
+ items() {
214
+ const c = this.content || {}
215
+ return this.rows.slice(0, this.limit).map((row, index) => {
216
+ const link = this.rowLink(row)
217
+ return {
218
+ key: (row && (row.id || row['@self']?.id)) || `row-${index}`,
219
+ tag: link ? 'router-link' : 'div',
220
+ attrs: link ? { to: link } : {},
221
+ title: this.fill(c.title, row),
222
+ meta: this.fill(c.meta, row),
223
+ text: this.fill(c.text, row),
224
+ label: this.fill(c.label, row),
225
+ trailing: c.trailing ? firstFilled(c.trailing, row, this.translate) : '',
226
+ pill: this.pillOf(row),
227
+ icon: this.iconOf(row),
228
+ }
229
+ })
230
+ },
231
+
232
+ /** @return {object|null} The header link with `{total}` filled and tokens resolved. */
233
+ headerLink() {
234
+ const link = this.content?.headerLink
235
+ if (!link || typeof link !== 'object' || typeof link.label !== 'string' || link.label === '') {
236
+ return null
237
+ }
238
+ const resolved = { label: this.withTotal(link.label) }
239
+ if (link.route) {
240
+ resolved.route = link.route
241
+ if (link.params && typeof link.params === 'object') {
242
+ resolved.params = resolveFilterTokens(link.params, this.tokenCtx)
243
+ }
244
+ if (link.query && typeof link.query === 'object') {
245
+ resolved.query = resolveFilterTokens(link.query, this.tokenCtx)
246
+ }
247
+ } else if (typeof link.href === 'string') {
248
+ resolved.href = link.href
249
+ }
250
+ return resolved
251
+ },
252
+
253
+ /** @return {string} The header note with `{total}` filled. */
254
+ headerNote() {
255
+ const note = this.content?.headerNote
256
+ return typeof note === 'string' && note !== '' ? this.withTotal(note) : ''
257
+ },
258
+
259
+ /** @return {string} The empty line. */
260
+ emptyLabel() {
261
+ return this.content?.emptyText ? this.translate(this.content.emptyText) : t('nextcloud-vue', 'Nothing here yet')
262
+ },
263
+
264
+ /** @return {string} The error line. */
265
+ errorLabel() {
266
+ return this.content?.errorText ? this.translate(this.content.errorText) : t('nextcloud-vue', 'This list could not be loaded.')
267
+ },
268
+
269
+ /** @return {string} The spinner's accessible name. */
270
+ loadingLabel() {
271
+ return t('nextcloud-vue', 'Loading …')
272
+ },
273
+ },
274
+
275
+ watch: {
276
+ requestKey() {
277
+ this.fetchRows()
278
+ },
279
+ },
280
+
281
+ mounted() {
282
+ this.onRefresh = () => this.fetchRows()
283
+ subscribe(PAGE_REFRESH_CHANNEL, this.onRefresh)
284
+ this.fetchRows()
285
+ },
286
+
287
+ beforeUnmount() {
288
+ unsubscribe(PAGE_REFRESH_CHANNEL, this.onRefresh)
289
+ },
290
+
291
+ methods: {
292
+ /**
293
+ * Translate a manifest string through the host app.
294
+ *
295
+ * @param {string} text The text.
296
+ * @return {string} The translation.
297
+ */
298
+ translate(text) {
299
+ return typeof this.cnTranslate === 'function' ? this.cnTranslate(text) : text
300
+ },
301
+
302
+ /**
303
+ * Fill a template from a row, after translating it.
304
+ *
305
+ * @param {string} template The template.
306
+ * @param {object} row The row.
307
+ * @return {string} The text, '' when a token is empty.
308
+ */
309
+ fill(template, row) {
310
+ return typeof template === 'string' && template !== '' ? fillTemplate(this.translate(template), row) : ''
311
+ },
312
+
313
+ /**
314
+ * Replace `{total}` in a translated label.
315
+ *
316
+ * @param {string} label The label.
317
+ * @return {string} The text.
318
+ */
319
+ withTotal(label) {
320
+ return this.translate(label).replace(/\{total\}/g, String(this.total))
321
+ },
322
+
323
+ /**
324
+ * The route a row opens: `rowRoute` with the id at `rowIdField`.
325
+ *
326
+ * @param {object} row The row.
327
+ * @return {object|null} The router location, or null.
328
+ */
329
+ rowLink(row) {
330
+ const route = this.content?.rowRoute
331
+ if (!route || !this.$router || !row) {
332
+ return null
333
+ }
334
+ const field = this.content?.rowIdField
335
+ const id = field ? readPath(row, field) : (row.id || row['@self']?.id)
336
+ if (id === null || id === undefined || id === '' || typeof id === 'object') {
337
+ return null
338
+ }
339
+ return { name: route, params: { id: String(id) } }
340
+ },
341
+
342
+ /**
343
+ * The row's pill: `{ field, labels?, colorMap?, variant? }`.
344
+ *
345
+ * @param {object} row The row.
346
+ * @return {{label: string, variant: string}|null} The pill, or null.
347
+ */
348
+ pillOf(row) {
349
+ const pill = this.content?.pill
350
+ if (!pill || typeof pill !== 'object' || !pill.field) {
351
+ return null
352
+ }
353
+ const raw = readPath(row, pill.field)
354
+ if (raw === null || raw === undefined || raw === '' || typeof raw === 'object') {
355
+ return null
356
+ }
357
+ const key = String(raw)
358
+ const labels = pill.labels && typeof pill.labels === 'object' ? pill.labels : {}
359
+ const map = pill.colorMap && typeof pill.colorMap === 'object' ? pill.colorMap : {}
360
+ const found = Object.keys(map).find((k) => k.toLowerCase() === key.toLowerCase())
361
+ const variant = found ? map[found] : pill.variant
362
+ return {
363
+ label: labels[key] ? this.translate(labels[key]) : key,
364
+ variant: isBadgeVariant(variant) ? variant : 'default',
365
+ }
366
+ },
367
+
368
+ /**
369
+ * The feed icon: an icon name, or `{ field, map, default }`.
370
+ *
371
+ * @param {object} row The row.
372
+ * @return {string} The MDI icon name.
373
+ */
374
+ iconOf(row) {
375
+ const icon = this.content?.icon
376
+ if (typeof icon === 'string' && icon !== '') {
377
+ return icon
378
+ }
379
+ if (icon && typeof icon === 'object') {
380
+ const raw = icon.field ? readPath(row, icon.field) : undefined
381
+ const map = icon.map && typeof icon.map === 'object' ? icon.map : {}
382
+ if (raw !== null && raw !== undefined && map[String(raw)]) {
383
+ return map[String(raw)]
384
+ }
385
+ if (typeof icon.default === 'string' && icon.default !== '') {
386
+ return icon.default
387
+ }
388
+ }
389
+ return 'CircleSmall'
390
+ },
391
+
392
+ /** Tell the card around this list what its header shows. */
393
+ emitHeader() {
394
+ /**
395
+ * @event header Emitted after every read with the card header's parts.
396
+ * @type {{link: (object|null), note: string}} The header link (resolved, with `{total}` filled) and the note.
397
+ */
398
+ this.$emit('header', { link: this.headerLink, note: this.headerNote })
399
+ },
400
+
401
+ /**
402
+ * Fetch the list. Sends nothing while a required token is unresolved
403
+ * or the source is incomplete.
404
+ *
405
+ * @return {Promise<void>}
406
+ */
407
+ async fetchRows() {
408
+ const c = this.content || {}
409
+ if (!c.register || !c.schema || this.waiting) {
410
+ this.rows = []
411
+ this.total = 0
412
+ this.error = false
413
+ this.emitHeader()
414
+ return
415
+ }
416
+ this.loading = true
417
+ this.error = false
418
+ try {
419
+ const [{ default: axios }, { generateUrl }] = await Promise.all([
420
+ import('@nextcloud/axios'),
421
+ import('@nextcloud/router'),
422
+ ])
423
+ const url = generateUrl('/apps/openregister/api/objects/{register}/{schema}', { register: c.register, schema: c.schema })
424
+ const params = { _limit: this.limit }
425
+ if (c.sort && typeof c.sort === 'object' && c.sort.field) {
426
+ params[`_order[${c.sort.field}]`] = c.sort.dir === 'desc' ? 'desc' : 'asc'
427
+ }
428
+ if (Array.isArray(c.extend)) {
429
+ const extend = c.extend.filter((e) => typeof e === 'string' && e !== '')
430
+ if (extend.length > 0) {
431
+ params._extend = extend
432
+ }
433
+ }
434
+ for (const [key, value] of Object.entries(this.resolvedFilter)) {
435
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
436
+ for (const [op, operand] of Object.entries(value)) {
437
+ params[`${key}[${op}]`] = operand
438
+ }
439
+ } else if (value !== '' && value !== null && value !== undefined) {
440
+ params[key] = value
441
+ }
442
+ }
443
+ const response = await axios.get(url, { params })
444
+ const data = response && response.data ? response.data : {}
445
+ this.rows = Array.isArray(data.results) ? data.results : []
446
+ this.total = typeof data.total === 'number' ? data.total : this.rows.length
447
+ } catch (e) {
448
+ // eslint-disable-next-line no-console
449
+ console.warn('[CnRecordListWidget] failed to load objects:', e)
450
+ this.rows = []
451
+ this.total = 0
452
+ this.error = true
453
+ } finally {
454
+ this.loading = false
455
+ this.emitHeader()
456
+ }
457
+ },
458
+ },
459
+ }
460
+ </script>
461
+
462
+ <style scoped>
463
+ .cn-record-list__list {
464
+ list-style: none;
465
+ margin: 0;
466
+ padding: 0;
467
+ }
468
+
469
+ .cn-record-list__empty,
470
+ .cn-record-list__error {
471
+ color: var(--color-text-maxcontrast);
472
+ margin: 0;
473
+ }
474
+
475
+ .cn-record-list__loading {
476
+ display: flex;
477
+ justify-content: center;
478
+ padding: 8px 0;
479
+ }
480
+
481
+ /* rows */
482
+ .cn-record-list--rows .cn-record-list__item + .cn-record-list__item {
483
+ border-top: 1px solid var(--color-border);
484
+ }
485
+
486
+ .cn-record-list__row {
487
+ align-items: center;
488
+ color: var(--color-main-text);
489
+ display: flex;
490
+ gap: 16px;
491
+ padding: 12px 0;
492
+ text-decoration: none;
493
+ }
494
+
495
+ .cn-record-list__row--link:hover .cn-record-list__title,
496
+ .cn-record-list__row--link:focus-visible .cn-record-list__title {
497
+ text-decoration: underline;
498
+ }
499
+
500
+ .cn-record-list__main {
501
+ display: flex;
502
+ flex: 1 1 auto;
503
+ flex-direction: column;
504
+ gap: 2px;
505
+ min-width: 0;
506
+ }
507
+
508
+ .cn-record-list__title {
509
+ font-size: 15px;
510
+ font-weight: 700;
511
+ }
512
+
513
+ .cn-record-list__meta {
514
+ color: var(--color-text-maxcontrast);
515
+ font-size: 13px;
516
+ }
517
+
518
+ .cn-record-list__aside {
519
+ align-items: center;
520
+ display: flex;
521
+ flex: none;
522
+ gap: 16px;
523
+ }
524
+
525
+ .cn-record-list__trailing {
526
+ font-size: 14px;
527
+ white-space: nowrap;
528
+ }
529
+
530
+ .cn-record-list__chevron {
531
+ color: var(--color-text-maxcontrast);
532
+ flex: none;
533
+ }
534
+
535
+ /* feed */
536
+ .cn-record-list--feed .cn-record-list__item + .cn-record-list__item {
537
+ border-top: 1px solid var(--color-border);
538
+ }
539
+
540
+ .cn-record-list__feed-row {
541
+ align-items: flex-start;
542
+ display: flex;
543
+ gap: 14px;
544
+ padding: 12px 0;
545
+ }
546
+
547
+ .cn-record-list__feed-icon {
548
+ align-items: center;
549
+ background: var(--color-primary-element-light);
550
+ border-radius: 50%;
551
+ color: var(--color-primary-element-light-text, var(--color-primary-element));
552
+ display: flex;
553
+ flex: none;
554
+ height: 36px;
555
+ justify-content: center;
556
+ width: 36px;
557
+ }
558
+
559
+ .cn-record-list__text {
560
+ font-size: 15px;
561
+ }
562
+
563
+ .cn-record-list--feed .cn-record-list__main {
564
+ color: var(--color-main-text);
565
+ text-decoration: none;
566
+ }
567
+
568
+ /* inline */
569
+ .cn-record-list__inline-row {
570
+ padding: 4px 0;
571
+ }
572
+
573
+ .cn-record-list__label {
574
+ margin-inline-end: 4px;
575
+ }
576
+
577
+ .cn-record-list__inline-title {
578
+ color: var(--color-main-text);
579
+ }
580
+
581
+ a.cn-record-list__inline-title {
582
+ color: var(--color-primary-element);
583
+ text-decoration: underline;
584
+ }
585
+ </style>
586
+
587
+ <style>
588
+ /* In a narrow side column a row stacks: title, meta, then pill and date. */
589
+ .cn-detail-page__side .cn-record-list__row {
590
+ align-items: flex-start;
591
+ flex-direction: column;
592
+ gap: 6px;
593
+ }
594
+
595
+ .cn-detail-page__side .cn-record-list__row .cn-record-list__title {
596
+ color: var(--color-primary-element);
597
+ text-decoration: underline;
598
+ }
599
+
600
+ .cn-detail-page__side .cn-record-list__chevron {
601
+ display: none;
602
+ }
603
+ </style>
@@ -0,0 +1,30 @@
1
+ /**
2
+ * SPDX-FileCopyrightText: 2026 Conduction B.V.
3
+ * SPDX-License-Identifier: EUPL-1.2
4
+ *
5
+ * CnRecordListWidget — self-registers the `record-list` widget type (the
6
+ * objects of another schema that point at this record, as board rows, a feed
7
+ * or inline links) into the shared dashboardWidgetRegistry at module load.
8
+ * Renderer only: the list is declared in the manifest.
9
+ */
10
+
11
+ import CnRecordListWidget from './CnRecordListWidget.vue'
12
+ import { registerDashboardWidget } from '../CnWidgetGrid/dashboardWidgetRegistry.js'
13
+
14
+ registerDashboardWidget('record-list', {
15
+ renderer: CnRecordListWidget,
16
+ form: null,
17
+ defaultContent: {
18
+ register: '',
19
+ schema: '',
20
+ filter: {},
21
+ presentation: 'rows',
22
+ limit: 5,
23
+ },
24
+ displayName: 'Record list',
25
+ icon: 'FormatListBulleted',
26
+ surfaces: ['detail-page', 'app-dashboard'],
27
+ })
28
+
29
+ export { CnRecordListWidget }
30
+ export default CnRecordListWidget
@@ -41,7 +41,7 @@
41
41
  data-testid="cn-saved-views-control"
42
42
  :aria-label="isBoardLook ? t('nextcloud-vue', 'Save view') : menuLabel">
43
43
  <template #icon>
44
- <ContentSaveOutline v-if="isBoardLook" :size="18" />
44
+ <ContentSaveOutline v-if="isBoardLook" :size="14" />
45
45
  <BookmarkOutline v-else :size="20" />
46
46
  </template>
47
47