@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,137 @@
1
+ /**
2
+ * SPDX-FileCopyrightText: 2026 Conduction B.V. <info@conduction.nl>
3
+ * SPDX-License-Identifier: EUPL-1.2
4
+ *
5
+ * What a finished offline checklist run turns into in the queue.
6
+ *
7
+ * Two shapes, chosen by the leaf's `offlineConfig`:
8
+ *
9
+ * - `resultEndpoint` empty: an object create on `resultSchema`, the shape the
10
+ * leaf has always queued (`inspectionRef`, `checklistTemplateRef`,
11
+ * `items[]` with `questionId` / `answer` / `evidenceRefs`).
12
+ * - `resultEndpoint` set: a `submit` operation that replays the run as ONE
13
+ * POST to an app's own endpoint, for an app that stores a run as something
14
+ * other than a plain object (a completed task, say) and checks the run
15
+ * server side before it stores anything. The endpoint is a path under the
16
+ * Nextcloud root with `{field}` placeholders filled from the planned item,
17
+ * e.g. `/apps/myapp/api/cases/{caseRef}/checklist-run`.
18
+ *
19
+ * Pure: no axios, no IndexedDB, no DOM. The card queues what this returns and
20
+ * the replay service sends it.
21
+ *
22
+ * @module integrations/offline/checklistSubmission
23
+ */
24
+
25
+ import { translate as t } from '@nextcloud/l10n'
26
+
27
+ /**
28
+ * Fill a `{field}` endpoint template from a planned item.
29
+ *
30
+ * Each value is URL-encoded, so a planned item can never steer the request to
31
+ * another path. A placeholder the item does not fill is an error rather than
32
+ * an empty path segment: a run sent to the wrong case is worse than one that
33
+ * waits.
34
+ *
35
+ * @param {string} template The endpoint template.
36
+ * @param {object} plannedItem The planned item the run belongs to.
37
+ *
38
+ * @return {string} The filled path.
39
+ *
40
+ * @throws {Error} When a placeholder has no value on the item.
41
+ */
42
+ export function fillEndpoint(template, plannedItem) {
43
+ return String(template).replace(/\{([A-Za-z0-9_.]+)\}/g, (match, field) => {
44
+ const value = field.split('.').reduce((node, key) => (node === null || node === undefined ? undefined : node[key]), plannedItem)
45
+ if (value === undefined || value === null || String(value) === '') {
46
+ throw new Error(t('nextcloud-vue', 'This planned item has no {field}, so the answers have nowhere to go.', { field }))
47
+ }
48
+ return encodeURIComponent(String(value))
49
+ })
50
+ }
51
+
52
+ /**
53
+ * The id of an object, wherever OpenRegister carries it.
54
+ *
55
+ * @param {object|null} object The object.
56
+ *
57
+ * @return {string} The id, or an empty string.
58
+ */
59
+ function idOf(object) {
60
+ return String(object?.id ?? object?.['@self']?.id ?? object?.uuid ?? '')
61
+ }
62
+
63
+ /**
64
+ * Build the queue operation for a finished checklist run.
65
+ *
66
+ * @param {object} config The leaf's offline config.
67
+ * @param {object} run The run.
68
+ * @param {object} run.plannedItem The planned item.
69
+ * @param {object} run.template The normalised template (`items[]`).
70
+ * @param {object} run.answers Map of questionId to `{ answer, evidenceRefs }`.
71
+ * @param {object|null} [run.gps] The GPS fix at save, or null.
72
+ * @param {string} [run.gpsSource] `sensor` or `sensorless`.
73
+ * @param {string} [run.capturedAt] ISO time of the save.
74
+ * @param {boolean} [run.capturedOffline] Whether the device was offline at save.
75
+ *
76
+ * @return {{ operationType: string, schema: string, endpoint: (string|null), payload: object }}
77
+ * The operation fields for `enqueueMutation`.
78
+ *
79
+ * @throws {Error} When the endpoint cannot be filled from the planned item.
80
+ */
81
+ export function buildChecklistSubmission(config, run) {
82
+ const capturedAt = run.capturedAt || new Date().toISOString()
83
+ const gpsAtAnswer = run.gps ? { ...run.gps, source: run.gpsSource || 'sensor' } : { source: 'sensorless' }
84
+ const items = Array.isArray(run.template?.items) ? run.template.items : []
85
+ const answers = run.answers ?? {}
86
+ const plannedItemId = idOf(run.plannedItem)
87
+ const templateId = idOf(run.template)
88
+
89
+ if (!config.resultEndpoint) {
90
+ return {
91
+ operationType: 'create',
92
+ schema: config.resultSchema,
93
+ endpoint: null,
94
+ payload: {
95
+ inspectionRef: plannedItemId,
96
+ checklistTemplateRef: templateId,
97
+ items: items.map((q) => ({
98
+ questionId: q.questionId,
99
+ answer: answers[q.questionId]?.answer,
100
+ evidenceRefs: answers[q.questionId]?.evidenceRefs ?? [],
101
+ answeredAt: capturedAt,
102
+ gpsAtAnswer,
103
+ })),
104
+ },
105
+ }
106
+ }
107
+
108
+ const payload = {
109
+ [config.resultTemplateParam || 'templateId']: templateId,
110
+ [config.resultPlannedItemParam || 'plannedItem']: plannedItemId,
111
+ capturedAt,
112
+ capturedOffline: run.capturedOffline === true,
113
+ items: items
114
+ .filter((q) => {
115
+ const entry = answers[q.questionId] ?? {}
116
+ const hasAnswer = entry.answer !== undefined && entry.answer !== null && String(entry.answer).trim() !== ''
117
+ return hasAnswer || (Array.isArray(entry.evidenceRefs) && entry.evidenceRefs.length > 0)
118
+ })
119
+ .map((q) => ({
120
+ itemId: q.questionId,
121
+ value: answers[q.questionId]?.answer ?? '',
122
+ photos: answers[q.questionId]?.evidenceRefs ?? [],
123
+ answeredAt: capturedAt,
124
+ gpsAtAnswer,
125
+ })),
126
+ }
127
+ if (run.gps) {
128
+ payload.location = { ...run.gps, source: run.gpsSource || 'sensor' }
129
+ }
130
+
131
+ return {
132
+ operationType: 'submit',
133
+ schema: config.resultSchema || '',
134
+ endpoint: fillEndpoint(config.resultEndpoint, run.plannedItem),
135
+ payload,
136
+ }
137
+ }
@@ -46,11 +46,71 @@ export function classifyGps(fix, available = true) {
46
46
  return { quality: 'good', source: 'sensor', warning: null }
47
47
  }
48
48
 
49
+ /**
50
+ * Read a checklist template into the leaf's flat item contract.
51
+ *
52
+ * A consuming app's template does not have to carry `items[]` with
53
+ * `questionId` / `text` / `type`. Its `offlineConfig` names where those live:
54
+ * `sectionsField` when the items sit inside ordered sections, and the
55
+ * `item*Field` keys for the item's key, text and type. `itemTypeMap` translates
56
+ * the app's own type words onto the leaf's (`yes_no`, `photo_required`, or any
57
+ * other word, which renders as free text). `photoRequiredField` and
58
+ * `photoRequiredValue` mark an item whose answer also needs a photo.
59
+ *
60
+ * The template's own keys are kept, so the id and version stay readable; only
61
+ * `items` is replaced by the normalised list.
62
+ *
63
+ * @param {object|null} template The template as cached.
64
+ * @param {object} [config] The leaf's offline config.
65
+ *
66
+ * @return {object|null} The template with flat `items[]`, or null.
67
+ */
68
+ export function normaliseChecklistTemplate(template, config = {}) {
69
+ if (template === null || typeof template !== 'object') {
70
+ return null
71
+ }
72
+
73
+ const keyField = config.itemKeyField || 'questionId'
74
+ const textField = config.itemTextField || 'text'
75
+ const typeField = config.itemTypeField || 'type'
76
+ const typeMap = (config.itemTypeMap && typeof config.itemTypeMap === 'object') ? config.itemTypeMap : {}
77
+
78
+ const sections = config.sectionsField ? template[config.sectionsField] : null
79
+ const raw = Array.isArray(sections)
80
+ ? sections.flatMap((section) => (Array.isArray(section?.items) ? section.items : []))
81
+ : (Array.isArray(template.items) ? template.items : [])
82
+
83
+ const items = []
84
+ for (const item of raw) {
85
+ if (item === null || typeof item !== 'object') {
86
+ continue
87
+ }
88
+ const questionId = String(item[keyField] ?? '')
89
+ if (questionId === '') {
90
+ continue
91
+ }
92
+ const ownType = item[typeField]
93
+ const photoRequired = Boolean(config.photoRequiredField)
94
+ && item[config.photoRequiredField] === config.photoRequiredValue
95
+ items.push({
96
+ ...item,
97
+ questionId,
98
+ text: item[textField] ?? item.text ?? questionId,
99
+ type: typeMap[ownType] ?? ownType ?? 'text',
100
+ required: item.required === true,
101
+ photoRequired,
102
+ })
103
+ }
104
+
105
+ return { ...template, items }
106
+ }
107
+
49
108
  /**
50
109
  * Validate a set of checklist answers against the template's required items.
51
110
  *
52
- * A `required` item must have a non-empty answer; a `photo_required` item must
53
- * additionally have at least one evidence reference. Returns the per-question
111
+ * A `required` item must have a non-empty answer; a `photo_required` item, and
112
+ * any item flagged `photoRequired` by {@link normaliseChecklistTemplate}, must
113
+ * have at least one evidence reference. Returns the per-question
54
114
  * blocking errors so the UI can prevent save (and the engine never queues an
55
115
  * invalid result).
56
116
  *
@@ -66,7 +126,7 @@ export function validateChecklistAnswers(template, answersByQuestion) {
66
126
  const errors = []
67
127
 
68
128
  for (const item of items) {
69
- if (item?.required !== true) {
129
+ if (item?.required !== true && item?.photoRequired !== true) {
70
130
  continue
71
131
  }
72
132
  const entry = answers[item.questionId] ?? {}
@@ -80,7 +140,12 @@ export function validateChecklistAnswers(template, answersByQuestion) {
80
140
  continue
81
141
  }
82
142
 
83
- if (answer === undefined || answer === null || String(answer).trim() === '') {
143
+ if (item.photoRequired === true && evidenceRefs.length === 0) {
144
+ errors.push({ questionId: item.questionId, message: t('nextcloud-vue', 'Photo required for this question') })
145
+ continue
146
+ }
147
+
148
+ if (item.required === true && (answer === undefined || answer === null || String(answer).trim() === '')) {
84
149
  errors.push({ questionId: item.questionId, message: t('nextcloud-vue', 'This question is required') })
85
150
  }
86
151
  }
@@ -60,10 +60,13 @@ export {
60
60
  checklistProgress,
61
61
  classifyGps,
62
62
  GPS_POOR_ACCURACY_M,
63
+ normaliseChecklistTemplate,
63
64
  syncIndicator,
64
65
  validateChecklistAnswers,
65
66
  } from './fieldCollectionHelpers.js'
66
67
 
68
+ export { buildChecklistSubmission, fillEndpoint } from './checklistSubmission.js'
69
+
67
70
  export {
68
71
  CACHE_PREFIX,
69
72
  cacheNameFor,
@@ -257,10 +257,12 @@ export async function getPlanningMeta(register, schema, collection = 'planning')
257
257
  *
258
258
  * @param {object} operation The mutation operation.
259
259
  * @param {string} operation.deviceId Owning device id (IDOR scope).
260
- * @param {string} operation.operationType create / update / delete.
260
+ * @param {string} operation.operationType create / update / delete / submit.
261
261
  * @param {string} operation.register Target register slug or id.
262
262
  * @param {string} operation.schema Target schema slug or id.
263
263
  * @param {string} [operation.targetId] Existing object id (update/delete).
264
+ * @param {string} [operation.endpoint] Path under the Nextcloud root a
265
+ * `submit` row is POSTed to.
264
266
  * @param {object} [operation.payload] The object body (create/update).
265
267
  *
266
268
  * @return {Promise<string>} The queued operation id.
@@ -275,6 +277,9 @@ export async function enqueueMutation(operation) {
275
277
  register: operation.register,
276
278
  schema: operation.schema,
277
279
  targetId: operation.targetId ?? null,
280
+ // A `submit` operation replays to an app's own endpoint instead of the
281
+ // object API (see checklistSubmission.js); every other row has none.
282
+ endpoint: operation.endpoint ?? null,
278
283
  payload: operation.payload ?? {},
279
284
  queuedAt: operation.queuedAt || new Date().toISOString(),
280
285
  attemptCount: 0,
@@ -35,7 +35,8 @@ function objectsUrl(register, schema) {
35
35
  }
36
36
 
37
37
  /**
38
- * Replay one queued mutation against the OR object API.
38
+ * Replay one queued mutation against the OR object API, or, for a row that
39
+ * names an `endpoint`, as one POST to that endpoint.
39
40
  *
40
41
  * @param {object} operation The queue operation row.
41
42
  * @param {object} [offlineConfig] The leaf's offline config (`register`,
@@ -54,7 +55,12 @@ export async function replayOperation(operation, offlineConfig = {}) {
54
55
 
55
56
  try {
56
57
  let response
57
- if (operation.operationType === 'create' || operation.operationType === 'upload') {
58
+ if (typeof operation.endpoint === 'string' && operation.endpoint !== '') {
59
+ // A run the app checks and stores itself (a completed task, say):
60
+ // one POST to its endpoint under the Nextcloud root. The endpoint
61
+ // decides access from what it has stored, as the object API does.
62
+ response = await axios.post(generateUrl(operation.endpoint), payload)
63
+ } else if (operation.operationType === 'create' || operation.operationType === 'upload') {
58
64
  response = await axios.post(base, payload)
59
65
  } else if (operation.operationType === 'update') {
60
66
  response = await axios.put(`${base}/${operation.targetId}`, payload)
@@ -3,7 +3,7 @@
3
3
  "$id": "https://raw.githubusercontent.com/ConductionNL/nextcloud-vue/main/src/schemas/app-manifest-v2.schema.json",
4
4
  "title": "Conduction App Manifest v2",
5
5
  "description": "v2 schema for the JSON-driven page and navigation manifest consumed by @conduction/nextcloud-vue. Introduces a uniform widgets[] array on every page type with a per-slot grid coordinate system, a typed actions[] discriminator, and the required $schema field for version detection. v1 manifests continue to validate against app-manifest.schema.json (unchanged).",
6
- "version": "2.76.0",
6
+ "version": "2.85.0",
7
7
  "type": "object",
8
8
  "required": ["$schema", "version"],
9
9
  "allOf": [
@@ -760,8 +760,8 @@
760
760
  },
761
761
  "sentinelFilterToken": {
762
762
  "type": "string",
763
- "pattern": "^@(?:me(?:\\.(?:displayName|email))?|myGroups|now|today|monthStart|quarterStart|yearStart)$|^@today[+-][0-9]+d$",
764
- "description": "Filter-value context: relative fetch-time tokens resolved by resolveFilterTokens (@me, @me.displayName, @me.email, @myGroups (the current user's group ids, an IN list), @now, @today, @today±Nd, @monthStart, @quarterStart, @yearStart). Mirrors SENTINEL_TOKEN_PATTERNS.filter in src/utils/sentinelTokens.js (byte-equality enforced by a unit test)."
763
+ "pattern": "^@(?:me(?:\\.(?:displayName|email))?|myGroups|now|today|monthStart|monthEnd|nextMonthStart|quarterStart|yearStart)$|^@today[+-][0-9]+d$",
764
+ "description": "Filter-value context: relative fetch-time tokens resolved by resolveFilterTokens (@me, @me.displayName, @me.email, @myGroups (the current user's group ids, an IN list), @now, @today, @today±Nd, @monthStart, @monthEnd, @nextMonthStart, @quarterStart, @yearStart). Mirrors SENTINEL_TOKEN_PATTERNS.filter in src/utils/sentinelTokens.js (byte-equality enforced by a unit test)."
765
765
  },
766
766
  "sentinelConfigToken": {
767
767
  "type": "string",
@@ -1132,14 +1132,43 @@
1132
1132
  "headerPill": {
1133
1133
  "type": "object",
1134
1134
  "additionalProperties": false,
1135
- "required": ["field"],
1136
- "description": "A pill in a detail page header, rendered through CnStatusBadge from one field of the record. Nothing renders while the field is empty.",
1135
+ "anyOf": [
1136
+ { "required": ["field"] },
1137
+ { "required": ["text"] },
1138
+ { "required": ["countFrom"] }
1139
+ ],
1140
+ "description": "A pill in a detail page header, rendered through CnStatusBadge: from one field of the record (`field`; nothing renders while it is empty), a constant (`text`), or a count of a related list (`countFrom` with `countLabel`; nothing renders at 0, while counting or after a failed count). screens-relation-pages, 2.85.0.",
1137
1141
  "properties": {
1138
1142
  "field": {
1139
1143
  "type": "string",
1140
1144
  "minLength": 1,
1141
1145
  "description": "Dot-path to the value on the record."
1142
1146
  },
1147
+ "text": {
1148
+ "type": "string",
1149
+ "minLength": 1,
1150
+ "description": "A constant label (an i18n key), such as \"Inwoner\" or \"Klant\". Used when `field` is not set."
1151
+ },
1152
+ "countFrom": {
1153
+ "type": "object",
1154
+ "additionalProperties": false,
1155
+ "required": ["register", "schema"],
1156
+ "description": "The list the pill counts with one list request (`_limit=1`, the response's `total`). `filter` values take the `@objectId` and `@object.<field>` tokens. Used when `field` is not set.",
1157
+ "properties": {
1158
+ "register": { "type": "string", "minLength": 1 },
1159
+ "schema": { "type": "string", "minLength": 1 },
1160
+ "filter": { "type": "object", "additionalProperties": true }
1161
+ }
1162
+ },
1163
+ "countLabel": {
1164
+ "type": "object",
1165
+ "additionalProperties": false,
1166
+ "description": "The count pill's label for one and for more, i18n keys with `{count}`: `{ \"one\": \"{count} lopende zaak\", \"other\": \"{count} lopende zaken\" }`.",
1167
+ "properties": {
1168
+ "one": { "type": "string", "minLength": 1 },
1169
+ "other": { "type": "string", "minLength": 1 }
1170
+ }
1171
+ },
1143
1172
  "colorMap": {
1144
1173
  "type": "object",
1145
1174
  "description": "Raw value to badge variant. Keys are matched ignoring case.",
@@ -2582,6 +2611,14 @@
2582
2611
  "type": "string",
2583
2612
  "description": "open-form: overrides the create dialog's heading. Defaults to 'Create {schema title}'."
2584
2613
  },
2614
+ "formEyebrow": {
2615
+ "type": "string",
2616
+ "description": "open-form: the context line above the create dialog's heading (board look), e.g. the list the record joins. Through the host translate. Absent: no line."
2617
+ },
2618
+ "formSubtitle": {
2619
+ "type": "string",
2620
+ "description": "open-form: the sentence under the create dialog's heading (board look), e.g. which fields are required and what happens on save. Through the host translate. Absent: no sentence."
2621
+ },
2585
2622
  "size": {
2586
2623
  "type": "string",
2587
2624
  "enum": ["small", "normal", "large", "full"],
@@ -4004,6 +4041,40 @@
4004
4041
  "default": [],
4005
4042
  "description": "type='dashboard': widget ids drawn as a KPI row above the grid, in the listed order, in an auto-fit grid (`repeat(auto-fit, minmax(200px, 1fr))`, gap 16). The named widgets are not GridStack items, so they do not reflow with the grid and are not part of the emitted layout; an id that names no widget is skipped with a development warning (screens-dashboard-parity). Maps to CnDashboardPage `kpiRow`. `dateRange.control` also accepts \"segmented\": the presets as one compact segmented group under the header."
4006
4043
  },
4044
+ "greeting": {
4045
+ "oneOf": [
4046
+ { "type": "boolean" },
4047
+ { "type": "string", "enum": ["first", "full"] }
4048
+ ],
4049
+ "default": false,
4050
+ "description": "type='dashboard': greet the reader in the header subtitle. `true` or \"first\" writes \"Good afternoon, Pieter · Monday 5 October 2026\", \"full\" uses the whole display name; the page `description` follows after another middle dot (screens-dashboard-greeting-header). Maps to CnDashboardPage `greeting`."
4051
+ },
4052
+ "viewLinks": {
4053
+ "type": "array",
4054
+ "default": [],
4055
+ "description": "type='dashboard': links drawn as small pills on the switch row under the header, right of the view switch (screens-dashboard-greeting-header). `route` is a route name or a `{ name, params, query }` location, `href` a plain URL; `label` is an i18n key. An entry without a label or a target is skipped. Maps to CnDashboardPage `viewLinks`.",
4056
+ "items": {
4057
+ "type": "object",
4058
+ "required": ["label"],
4059
+ "additionalProperties": false,
4060
+ "properties": {
4061
+ "label": { "type": "string", "minLength": 1 },
4062
+ "icon": { "type": "string", "minLength": 1 },
4063
+ "route": {
4064
+ "oneOf": [
4065
+ { "type": "string", "minLength": 1 },
4066
+ { "type": "object", "required": ["name"], "properties": { "name": { "type": "string", "minLength": 1 }, "params": { "type": "object" }, "query": { "type": "object" } } }
4067
+ ]
4068
+ },
4069
+ "href": { "type": "string", "minLength": 1 }
4070
+ }
4071
+ }
4072
+ },
4073
+ "showActionsMenu": {
4074
+ "type": "boolean",
4075
+ "default": true,
4076
+ "description": "type='dashboard': `false` removes the page Actions menu from the header, for a board page whose header carries only the buildiq square and the primary (screens-dashboard-greeting-header). Maps to CnDashboardPage `showActionsMenu`."
4077
+ },
4007
4078
  "gridFloat": {
4008
4079
  "type": "boolean",
4009
4080
  "default": true,
@@ -4038,6 +4109,41 @@
4038
4109
  "minLength": 1,
4039
4110
  "description": "type='index', board look: free text after the footer's count, e.g. \"click a column header to sort\" (screens-index-list-parity). Maps to CnIndexPage `footerNote`."
4040
4111
  },
4112
+ "queryPresets": {
4113
+ "type": "array",
4114
+ "minItems": 1,
4115
+ "description": "type='index': per-query overlays of this page (screens-index-query-presets). A menu entry that deep-links here with a query (`menu[].query`, e.g. { caseType: '<uuid>' }, the shape hydra gate-68 asks for instead of a second page) gets its own lenses, columns and copy: the first preset whose every `match` pair is in the route query replaces those keys of this config. Only the listed keys can be replaced; register, schema and permissions stay the page's. Switching preset remounts the list.",
4116
+ "items": {
4117
+ "type": "object",
4118
+ "required": ["match"],
4119
+ "additionalProperties": false,
4120
+ "properties": {
4121
+ "id": { "type": "string", "minLength": 1, "description": "A name for the preset, for people reading the manifest." },
4122
+ "match": { "type": "object", "minProperties": 1, "additionalProperties": { "type": ["string", "number", "boolean"] }, "description": "Route query pairs that must ALL be present, e.g. { \"caseType\": \"<uuid>\" }. A repeated parameter matches when it holds the value." },
4123
+ "title": { "type": "string", "minLength": 1, "description": "The page title while the preset applies." },
4124
+ "quickFilters": { "type": "array", "description": "The lenses while the preset applies; same shape as config.quickFilters." },
4125
+ "columns": { "type": "array", "description": "The columns while the preset applies; same shape as config.columns." },
4126
+ "cardFields": { "type": "array", "items": { "type": "string" }, "description": "The card facts while the preset applies." },
4127
+ "countText": { "type": "string", "minLength": 1, "description": "The count line while the preset applies." },
4128
+ "searchPlaceholder": { "type": "string", "minLength": 1, "description": "The search placeholder while the preset applies." },
4129
+ "footerNote": { "type": "string", "minLength": 1, "description": "The footer note while the preset applies." },
4130
+ "defaultSort": { "type": "array", "description": "The default sort while the preset applies; same shape as config.defaultSort." },
4131
+ "viewSwitch": { "type": "array", "items": { "type": "string", "enum": ["table", "cards", "board", "map"] }, "description": "The drawn view segments while the preset applies." }
4132
+ }
4133
+ }
4134
+ },
4135
+ "viewSwitch": {
4136
+ "type": "array",
4137
+ "minItems": 1,
4138
+ "uniqueItems": true,
4139
+ "items": { "type": "string", "enum": ["table", "cards", "board", "map"] },
4140
+ "description": "type='index', board look: the segments the view switch draws, e.g. [\"table\", \"cards\", \"board\", \"map\"] as the boards do. A listed mode the page can open is an ordinary segment; one it cannot (no board columns, no map) is drawn disabled and named \"<mode>: not available on this list\". Order is always table, cards, board, map. Ignored without the board look (screens-view-switch-parity). Maps to CnIndexPage `viewSwitch`."
4141
+ },
4142
+ "searchPlaceholder": {
4143
+ "type": "string",
4144
+ "minLength": 1,
4145
+ "description": "type='index': the placeholder (and accessible name) of the list's search field, e.g. \"Search by case, number or requester\". Run through the app's translate, so an English key with a Dutch catalogue entry reads Dutch. Absent: the library's \"Search…\" (screens-index-toolbar-parity). Maps to CnIndexPage `searchPlaceholder`."
4146
+ },
4041
4147
  "bulkHint": {
4042
4148
  "type": "string",
4043
4149
  "minLength": 1,
@@ -4048,6 +4154,38 @@
4048
4154
  "description": "type='index': the property keys the cards view shows as the facts list of each card, in order; defaults to the first four list columns under the board look (screens-card-parity). Maps to CnIndexPage `cardFields`.",
4049
4155
  "items": { "type": "string", "minLength": 1 }
4050
4156
  },
4157
+ "statRow": {
4158
+ "type": "array",
4159
+ "default": [],
4160
+ "description": "type='index': KPI tiles above the list, between the header and the toolbar, in an auto-fit row (each at least 200px). Each entry is a widget definition, usually `type: \"stat\"`; the type resolves through the app's widget registry, then the dashboard catalog (screens-index-stat-row). Maps to CnIndexPage `statRow`.",
4161
+ "items": {
4162
+ "type": "object",
4163
+ "required": ["id", "type"],
4164
+ "properties": {
4165
+ "id": { "type": "string", "minLength": 1 },
4166
+ "type": { "type": "string", "minLength": 1 },
4167
+ "title": { "type": "string" },
4168
+ "content": { "type": "object" },
4169
+ "headerLink": { "type": "object" }
4170
+ }
4171
+ }
4172
+ },
4173
+ "sidePanel": {
4174
+ "type": "array",
4175
+ "default": [],
4176
+ "description": "type='index': titled cards in a 300px column right of the toolbar and the list (above them below 1024px). Each entry is a widget definition with an optional `title` (i18n key) and `headerLink`; the type resolves as for `statRow` (screens-index-stat-row). Maps to CnIndexPage `sidePanel`.",
4177
+ "items": {
4178
+ "type": "object",
4179
+ "required": ["id", "type"],
4180
+ "properties": {
4181
+ "id": { "type": "string", "minLength": 1 },
4182
+ "type": { "type": "string", "minLength": 1 },
4183
+ "title": { "type": "string" },
4184
+ "content": { "type": "object" },
4185
+ "headerLink": { "type": "object" }
4186
+ }
4187
+ }
4188
+ },
4051
4189
  "headerButtons": {
4052
4190
  "type": "array",
4053
4191
  "description": "type='index': buttons beside the page title, as the board's \"Export\" and \"New case\" (zuiddrecht-pixel-gaps-3). `action` is `add` (the Add flow; the label defaults to the Add label), `export` (the export leaf in `format`, else the export dialog; under the board look it reads \"Download\" by default), `actions-menu` (the page's header actions as one labelled menu button, absent when the page has none), `import`, `refresh`, or the id of a `headerActions` entry. Under the board look the buttons render in a fixed order whatever the manifest declares: export, actions-menu, other secondary buttons, the buildiq square, then the primary button. Shown only with `showTitle`; when they show, the actions bar drops its Views and Actions menus, and the Add button and Export menu when a button takes their action. Maps to CnIndexPage `headerButtons`.",
@@ -4065,6 +4203,12 @@
4065
4203
  }
4066
4204
  }
4067
4205
  },
4206
+ "rowTitle": {
4207
+ "type": "string",
4208
+ "enum": ["link", "plain"],
4209
+ "default": "link",
4210
+ "description": "type='index' (screens-table-rows-parity): how the board look draws the table's title column. `link` (the default) is the 15px weight 600 underlined title of the DqZaken board; `plain` is the 15px bold title without an underline over a 13px muted secondary line, as the PqTickets and OcPublicaties boards draw it. The row stays clickable either way. The Nextcloud look ignores it. Maps to CnIndexPage `rowTitle`."
4211
+ },
4068
4212
  "showTypeEyebrow": {
4069
4213
  "type": "boolean",
4070
4214
  "default": true,