@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,189 @@
1
+ /**
2
+ * SPDX-FileCopyrightText: 2026 Conduction B.V.
3
+ * SPDX-License-Identifier: EUPL-1.2
4
+ *
5
+ * boardDate: the two short time readings the drawn boards put in a table cell.
6
+ *
7
+ * - A date ("5 okt", "30 okt", "14 feb 2024"): day and short month in the
8
+ * user's language, the year only when it is not the current year.
9
+ * - An age ("4 uur", "1 dag", "3 dagen"): how long something has been
10
+ * waiting, in hours under a day and in whole calendar days after that.
11
+ *
12
+ * Both read the Nextcloud user LANGUAGE for the month and unit names. A
13
+ * Nextcloud account keeps its locale (date order) apart from its language,
14
+ * and a Dutch reader whose locale was left at the en_US default saw
15
+ * "Feb 14, 2024": the language decides the words, so it decides here, and the
16
+ * locale is only used when it is a region of that same language.
17
+ *
18
+ * @module utils/boardDate
19
+ * @spec openspec/changes/screens-cell-date-parity/specs/cell-date-age/spec.md
20
+ */
21
+
22
+ import { getCanonicalLocale, getLanguage, translatePlural as n } from '@nextcloud/l10n'
23
+ import { parseDateValue } from './dateVariant.js'
24
+ import { compareVisibleWhen } from './visibleWhen.js'
25
+
26
+ const MS_PER_HOUR = 3600000
27
+ const MS_PER_DAY = 86400000
28
+
29
+ /** Variants an age rule may name. `danger` is accepted as an alias of `error`. */
30
+ export const AGE_VARIANTS = Object.freeze(['default', 'success', 'warning', 'error'])
31
+
32
+ /**
33
+ * Read a language or locale tag from an @nextcloud/l10n getter, which throws
34
+ * when Nextcloud has not set one (outside a Nextcloud page, in a test).
35
+ *
36
+ * @param {() => string} getter `getLanguage` or `getCanonicalLocale`.
37
+ * @return {string} The tag with `-` separators, or ''.
38
+ */
39
+ function readTag(getter) {
40
+ try {
41
+ return String(getter() || '').replace(/_/g, '-')
42
+ } catch {
43
+ return ''
44
+ }
45
+ }
46
+
47
+ /**
48
+ * The BCP 47 tag the board dates are written in: the user's locale when it is
49
+ * a region of the user's language (`nl-BE` for language `nl`), else the
50
+ * language itself.
51
+ *
52
+ * @return {string|undefined} The tag, or undefined to let Intl choose.
53
+ * @spec openspec/changes/screens-cell-date-parity/specs/cell-date-age/spec.md#requirement-a-board-date-cell-reads-day-and-short-month-in-the-user-language
54
+ */
55
+ export function boardDateLocale() {
56
+ const language = readTag(getLanguage)
57
+ const locale = readTag(getCanonicalLocale)
58
+ const base = (tag) => tag.split('-')[0].toLowerCase()
59
+ if (language && locale && base(language) === base(locale)) {
60
+ return locale
61
+ }
62
+ return language || locale || undefined
63
+ }
64
+
65
+ /**
66
+ * A date in the board's short form: "5 okt", or "14 feb 2024" outside the
67
+ * current year.
68
+ *
69
+ * @param {unknown} value A Date, a timestamp or an ISO string.
70
+ * @param {Date} [now] The reference moment (defaults to the current time).
71
+ * @param {string} [locale] BCP 47 tag (defaults to `boardDateLocale()`).
72
+ * @return {string} The short date, or '' when the value is not a date.
73
+ * @spec openspec/changes/screens-cell-date-parity/specs/cell-date-age/spec.md#requirement-a-board-date-cell-reads-day-and-short-month-in-the-user-language
74
+ */
75
+ export function formatBoardDate(value, now = new Date(), locale = boardDateLocale()) {
76
+ const date = parseDateValue(value)
77
+ if (date === null) {
78
+ return ''
79
+ }
80
+ const options = { day: 'numeric', month: 'short' }
81
+ if (date.getFullYear() !== now.getFullYear()) {
82
+ options.year = 'numeric'
83
+ }
84
+ try {
85
+ return new Intl.DateTimeFormat(locale, options).format(date)
86
+ } catch {
87
+ return new Intl.DateTimeFormat(undefined, options).format(date)
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Whole calendar days from the date until `now` (0 today, 1 yesterday), or
93
+ * null when the value is not a date. A future date gives a negative number.
94
+ *
95
+ * @param {unknown} value The date.
96
+ * @param {Date} [now] The reference moment.
97
+ * @return {number|null} The day count.
98
+ * @spec openspec/changes/screens-cell-date-parity/specs/cell-date-age/spec.md#requirement-an-age-cell-says-how-long-something-has-waited
99
+ */
100
+ export function ageInDays(value, now = new Date()) {
101
+ const date = parseDateValue(value)
102
+ if (date === null) {
103
+ return null
104
+ }
105
+ // UTC midnights of the two LOCAL days, so a daylight-saving hour never
106
+ // turns one day into zero or two.
107
+ const start = Date.UTC(date.getFullYear(), date.getMonth(), date.getDate())
108
+ const today = Date.UTC(now.getFullYear(), now.getMonth(), now.getDate())
109
+ return Math.round((today - start) / MS_PER_DAY)
110
+ }
111
+
112
+ /**
113
+ * How long something has waited: "4 uur" under 24 hours (at least 1), else
114
+ * "1 dag" / "3 dagen" in whole calendar days. Plural forms come from the
115
+ * library's own catalogue (`nextcloud-vue`). A date in the future reads as
116
+ * waiting no time: "0 hours".
117
+ *
118
+ * @param {unknown} value The moment the wait started.
119
+ * @param {Date} [now] The reference moment.
120
+ * @return {string} The age, or '' when the value is not a date.
121
+ * @spec openspec/changes/screens-cell-date-parity/specs/cell-date-age/spec.md#requirement-an-age-cell-says-how-long-something-has-waited
122
+ */
123
+ export function formatAge(value, now = new Date()) {
124
+ const date = parseDateValue(value)
125
+ if (date === null) {
126
+ return ''
127
+ }
128
+ const elapsed = now.getTime() - date.getTime()
129
+ if (elapsed < MS_PER_DAY) {
130
+ const hours = elapsed <= 0 ? 0 : Math.max(1, Math.floor(elapsed / MS_PER_HOUR))
131
+ return n('nextcloud-vue', '%n hour', '%n hours', hours)
132
+ }
133
+ const days = shownAgeDays(date, now)
134
+ return n('nextcloud-vue', '%n day', '%n days', days)
135
+ }
136
+
137
+ /**
138
+ * The number of days an age cell shows: 0 under 24 hours (the cell then reads
139
+ * hours), else the whole calendar days, at least 1. The threshold rules
140
+ * compare against this number, so the colour always agrees with the text.
141
+ *
142
+ * @param {unknown} value The moment the wait started.
143
+ * @param {Date} [now] The reference moment.
144
+ * @return {number|null} The day count, or null when the value is not a date.
145
+ * @spec openspec/changes/screens-cell-date-parity/specs/cell-date-age/spec.md#requirement-an-age-cell-turns-red-past-its-threshold
146
+ */
147
+ export function shownAgeDays(value, now = new Date()) {
148
+ const date = parseDateValue(value)
149
+ if (date === null) {
150
+ return null
151
+ }
152
+ if (now.getTime() - date.getTime() < MS_PER_DAY) {
153
+ return 0
154
+ }
155
+ return Math.max(1, ageInDays(date, now))
156
+ }
157
+
158
+ /**
159
+ * The variant of the first `variantWhen` rule the age in days matches, or ''
160
+ * when none does. Same rule shape as the `date` widget (`{ op, value,
161
+ * variant }`, first match wins), compared against the number of days the cell
162
+ * shows (`shownAgeDays`): `[{ op: "gte", value: 3, variant: "error" }]` turns a wait of three
163
+ * days or more red.
164
+ *
165
+ * @param {unknown} value The moment the wait started.
166
+ * @param {Array<{op: string, value: number, variant: string}>} rules The rules.
167
+ * @param {Date} [now] The reference moment.
168
+ * @return {string} `success | warning | error | default`, or ''.
169
+ * @spec openspec/changes/screens-cell-date-parity/specs/cell-date-age/spec.md#requirement-an-age-cell-turns-red-past-its-threshold
170
+ */
171
+ export function resolveAgeVariant(value, rules, now = new Date()) {
172
+ if (!Array.isArray(rules) || rules.length === 0) {
173
+ return ''
174
+ }
175
+ const days = shownAgeDays(value, now)
176
+ if (days === null) {
177
+ return ''
178
+ }
179
+ for (const rule of rules) {
180
+ if (!rule || typeof rule !== 'object') {
181
+ continue
182
+ }
183
+ if (compareVisibleWhen(days, rule.op || 'eq', rule.value) === true) {
184
+ const variant = rule.variant === 'danger' ? 'error' : rule.variant
185
+ return AGE_VARIANTS.includes(variant) ? variant : ''
186
+ }
187
+ }
188
+ return ''
189
+ }
@@ -201,13 +201,29 @@ export function resolveNextStep(config, object, stageField = 'status') {
201
201
  /**
202
202
  * Resolve a header pill from the record.
203
203
  *
204
- * @param {{field?: string, colorMap?: object, labels?: object, variant?: string}|null} config The pill config.
204
+ * A pill with `field` reads it off the record; one without `field` and with
205
+ * `text` is a constant. A count pill (`countFrom`) is resolved by
206
+ * {@link resolveCountPill} once its count is known.
207
+ *
208
+ * @param {{field?: string, text?: string, colorMap?: object, labels?: object, variant?: string}|null} config The pill config.
205
209
  * @param {object|null} object The record.
206
210
  * @return {{label: string, colorKey: string, colorMap: object|null, variant: string}|null} What CnStatusBadge needs, or null when the field is empty.
207
211
  * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-header-pills
208
212
  */
209
213
  export function resolvePill(config, object) {
210
- if (!config || typeof config !== 'object' || typeof config.field !== 'string' || config.field === '') {
214
+ if (!config || typeof config !== 'object') {
215
+ return null
216
+ }
217
+ // A constant pill (`text`): "Inwoner", "Klant" (screens-relation-pages).
218
+ if ((typeof config.field !== 'string' || config.field === '') && typeof config.text === 'string' && config.text !== '') {
219
+ return {
220
+ label: config.text,
221
+ colorKey: config.text,
222
+ colorMap: config.colorMap && typeof config.colorMap === 'object' ? config.colorMap : null,
223
+ variant: typeof config.variant === 'string' && config.variant !== '' ? config.variant : 'default',
224
+ }
225
+ }
226
+ if (typeof config.field !== 'string' || config.field === '') {
211
227
  return null
212
228
  }
213
229
  const raw = stageOf(object, config.field)
@@ -260,3 +276,44 @@ export function resolveTabCount(tab, object) {
260
276
  const number = Number(value)
261
277
  return Number.isFinite(number) ? number : null
262
278
  }
279
+
280
+ /**
281
+ * The list a count pill counts (`countFrom`), or null for any other pill.
282
+ *
283
+ * @param {object|null} config The pill config.
284
+ * @return {{register: string, schema: string, filter: object}|null} The list.
285
+ * @spec openspec/changes/screens-relation-pages/specs/relation-pages/spec.md#requirement-a-header-pill-can-be-a-constant-or-a-count
286
+ */
287
+ export function pillCountSource(config) {
288
+ const from = config && typeof config === 'object' && !config.field ? config.countFrom : null
289
+ if (!from || typeof from !== 'object' || typeof from.register !== 'string' || from.register === '' || typeof from.schema !== 'string' || from.schema === '') {
290
+ return null
291
+ }
292
+ const filter = from.filter && typeof from.filter === 'object' && !Array.isArray(from.filter) ? from.filter : {}
293
+ return { register: from.register, schema: from.schema, filter }
294
+ }
295
+
296
+ /**
297
+ * A count pill once its count is known: "1 lopende zaak", "2 lopende zaken".
298
+ * Nothing for a count of 0, an unknown count or a failed one.
299
+ *
300
+ * @param {{countLabel?: {one?: string, other?: string}, colorMap?: object, variant?: string}|null} config The pill config.
301
+ * @param {number|null|undefined} count The count.
302
+ * @param {(text: string) => string} [translate] Applied to the label before `{count}` is filled.
303
+ * @return {{label: string, colorKey: string, colorMap: object|null, variant: string}|null} What CnStatusBadge needs, or null.
304
+ * @spec openspec/changes/screens-relation-pages/specs/relation-pages/spec.md#requirement-a-header-pill-can-be-a-constant-or-a-count
305
+ */
306
+ export function resolveCountPill(config, count, translate = (text) => text) {
307
+ if (!pillCountSource(config) || typeof count !== 'number' || !Number.isFinite(count) || count <= 0) {
308
+ return null
309
+ }
310
+ const labels = config.countLabel && typeof config.countLabel === 'object' ? config.countLabel : {}
311
+ const template = (count === 1 ? labels.one : labels.other) || labels.other || labels.one || '{count}'
312
+ const label = translate(template).replace(/\{count\}/g, String(count))
313
+ return {
314
+ label,
315
+ colorKey: '',
316
+ colorMap: config.colorMap && typeof config.colorMap === 'object' ? config.colorMap : null,
317
+ variant: typeof config.variant === 'string' && config.variant !== '' ? config.variant : 'default',
318
+ }
319
+ }
@@ -101,6 +101,7 @@ const DEFAULT_MAP = {
101
101
  enum: 'enumValues',
102
102
  required: 'isRequired',
103
103
  default: 'defaultValue',
104
+ hideOnCreate: 'hideOnCreate',
104
105
  }
105
106
 
106
107
  /**
@@ -312,6 +313,8 @@ export function definitionQueryParams(config, value, formData = {}) {
312
313
  * @param {object} [config] The `x-openregister-extends-form` block (`map`, `typeMap`).
313
314
  * @param {object} [options] Extra options.
314
315
  * @param {number} [options.orderFrom] Order assigned to the first field; each subsequent field takes the next integer. Dynamic fields sort after the schema's own, so pass a number above every declared `order`.
316
+ * @param {boolean} [options.create] The form creates a new object: a definition whose `hideOnCreate` (or the field `config.map.hideOnCreate` names) is `true` gets no field, because the app fills that answer itself at creation. Edit forms still show it.
317
+ * @spec openspec/changes/dynamic-question-hide-on-create/specs/dynamic-question-hide-on-create/spec.md#requirement-a-question-can-stay-off-the-create-form
315
318
  * @return {{properties: object, required: string[]}} Properties keyed by prefixed id, plus the required key list.
316
319
  */
317
320
  export function propertiesFromDefinitions(definitions, config = {}, options = {}) {
@@ -321,7 +324,8 @@ export function propertiesFromDefinitions(definitions, config = {}, options = {}
321
324
 
322
325
  const properties = {}
323
326
  const required = []
324
- const list = Array.isArray(definitions) ? definitions : []
327
+ const list = (Array.isArray(definitions) ? definitions : [])
328
+ .filter((record) => !(options.create === true && record && mapped(record, map, 'hideOnCreate') === true))
325
329
 
326
330
  list.forEach((record, index) => {
327
331
  if (!record || !record.id) {
@@ -0,0 +1,110 @@
1
+ // SPDX-FileCopyrightText: 2026 Conduction B.V.
2
+ // SPDX-License-Identifier: EUPL-1.2
3
+
4
+ import { boardDateLocale, formatBoardDate } from './boardDate.js'
5
+ import { parseDateValue } from './dateVariant.js'
6
+ import { readPath } from './readPath.js'
7
+
8
+ const TOKEN = /\{([^{}\s|]+)(?:\|([a-z]+))?\}/g
9
+
10
+ /**
11
+ * A moment in the board's short form with the time: "4 okt, 10:52", or
12
+ * "14 feb 2024, 09:00" outside the current year.
13
+ *
14
+ * @param {unknown} value A Date, a timestamp or an ISO string.
15
+ * @param {Date} [now] The reference moment.
16
+ * @return {string} The text, or '' when the value is not a date.
17
+ * @spec openspec/changes/screens-relation-pages/specs/relation-pages/spec.md#requirement-a-record-list-shows-the-objects-that-point-at-this-record
18
+ */
19
+ export function formatBoardDateTime(value, now = new Date()) {
20
+ const day = formatBoardDate(value, now)
21
+ const date = parseDateValue(value)
22
+ if (day === '' || date === null) {
23
+ return ''
24
+ }
25
+ let time
26
+ try {
27
+ time = new Intl.DateTimeFormat(boardDateLocale(), { hour: '2-digit', minute: '2-digit' }).format(date)
28
+ } catch {
29
+ time = new Intl.DateTimeFormat(undefined, { hour: '2-digit', minute: '2-digit' }).format(date)
30
+ }
31
+ return `${day}, ${time}`
32
+ }
33
+
34
+ /**
35
+ * Format one token value.
36
+ *
37
+ * @param {unknown} value The raw value.
38
+ * @param {string} format `date`, `datetime` or '' (as is).
39
+ * @param {Date} now The reference moment.
40
+ * @return {string} The text, '' when nothing can be shown.
41
+ */
42
+ function formatValue(value, format, now) {
43
+ if (value === null || value === undefined || value === '' || typeof value === 'object') {
44
+ return ''
45
+ }
46
+ if (format === 'date') {
47
+ return formatBoardDate(value, now)
48
+ }
49
+ if (format === 'datetime') {
50
+ return formatBoardDateTime(value, now)
51
+ }
52
+ return String(value)
53
+ }
54
+
55
+ /**
56
+ * Fill a row template such as `"{identifier} · {caseTypeName}"` or
57
+ * `"Termijn {deadline|date}"` from an object.
58
+ *
59
+ * Every `{path}` token is a dot path into the object; `{path|date}` and
60
+ * `{path|datetime}` format it as the boards write dates. A token without a
61
+ * value empties the WHOLE template, because "Termijn " is worse than nothing.
62
+ * A template without tokens is returned as written (a constant label).
63
+ *
64
+ * @param {string} template The template, already translated.
65
+ * @param {object|null} object The object to read.
66
+ * @param {Date} [now] The reference moment for dates.
67
+ * @return {string} The filled text, or ''.
68
+ * @spec openspec/changes/screens-relation-pages/specs/relation-pages/spec.md#requirement-a-record-list-shows-the-objects-that-point-at-this-record
69
+ */
70
+ export function fillTemplate(template, object, now = new Date()) {
71
+ if (typeof template !== 'string' || template.trim() === '') {
72
+ return ''
73
+ }
74
+ let missing = false
75
+ const text = template.replace(TOKEN, (_match, path, format) => {
76
+ const value = object && typeof object === 'object' ? readPath(object, path) : undefined
77
+ const shown = formatValue(value, format || '', now)
78
+ if (shown === '') {
79
+ missing = true
80
+ }
81
+ return shown
82
+ })
83
+ return missing ? '' : text.trim()
84
+ }
85
+
86
+ /**
87
+ * The first of several templates that fills: `["Klaar {closedAt|date}",
88
+ * "Termijn {deadline|date}"]` reads "Klaar ..." once the case is closed and
89
+ * "Termijn ..." while it is open. A single string is one candidate.
90
+ *
91
+ * @param {string|string[]} templates The candidates, in order.
92
+ * @param {object|null} object The object to read.
93
+ * @param {(key: string) => string} [translate] Applied to each template before filling.
94
+ * @param {Date} [now] The reference moment for dates.
95
+ * @return {string} The first filled text, or ''.
96
+ * @spec openspec/changes/screens-relation-pages/specs/relation-pages/spec.md#requirement-a-record-list-shows-the-objects-that-point-at-this-record
97
+ */
98
+ export function firstFilled(templates, object, translate = (key) => key, now = new Date()) {
99
+ const list = Array.isArray(templates) ? templates : [templates]
100
+ for (const template of list) {
101
+ if (typeof template !== 'string') {
102
+ continue
103
+ }
104
+ const text = fillTemplate(translate(template), object, now)
105
+ if (text !== '') {
106
+ return text
107
+ }
108
+ }
109
+ return ''
110
+ }
@@ -100,6 +100,8 @@ export const DASHBOARD_CATALOG_WIDGET_KEYS = Object.freeze([
100
100
  'delta',
101
101
  'divider',
102
102
  'document-review',
103
+ 'facts',
104
+ 'field-list',
103
105
  'files',
104
106
  'flow-runs',
105
107
  'gauge',
@@ -123,8 +125,10 @@ export const DASHBOARD_CATALOG_WIDGET_KEYS = Object.freeze([
123
125
  'object-table',
124
126
  'people',
125
127
  'quicklinks',
128
+ 'record-list',
126
129
  'related',
127
130
  'saved-view',
131
+ 'sections',
128
132
  'spend-analytics',
129
133
  'stacked-bar',
130
134
  'stages',
@@ -0,0 +1,93 @@
1
+ /**
2
+ * SPDX-FileCopyrightText: 2026 Conduction B.V.
3
+ * SPDX-License-Identifier: EUPL-1.2
4
+ *
5
+ * Query presets of an index page (`config.queryPresets`): a menu entry that
6
+ * deep-links to a list with a query (`{ caseType: '<uuid>' }`, the shape hydra
7
+ * gate-68 asks for instead of a second page) can bring its own lenses,
8
+ * columns and copy. The page names, per preset, the query it answers to and
9
+ * the keys it replaces; the first preset whose every `match` pair is in the
10
+ * current route query wins.
11
+ *
12
+ * @module utils/queryPresets
13
+ * @spec openspec/changes/screens-index-query-presets/specs/index-query-presets/spec.md#requirement-a-query-preset-overlays-lenses-columns-and-copy
14
+ */
15
+
16
+ /**
17
+ * The config keys a preset may replace. Anything else in a preset is ignored:
18
+ * a preset changes what the list shows, never which register or schema it
19
+ * reads or what it may write.
20
+ *
21
+ * @type {ReadonlyArray<string>}
22
+ */
23
+ export const QUERY_PRESET_KEYS = Object.freeze([
24
+ 'title',
25
+ 'quickFilters',
26
+ 'columns',
27
+ 'cardFields',
28
+ 'countText',
29
+ 'searchPlaceholder',
30
+ 'footerNote',
31
+ 'defaultSort',
32
+ 'viewSwitch',
33
+ ])
34
+
35
+ /**
36
+ * Whether one route query value satisfies one `match` value. A repeated query
37
+ * parameter (an array) matches when it holds the value.
38
+ *
39
+ * @param {unknown} actual The route query value.
40
+ * @param {string|number|boolean} expected The preset's value.
41
+ * @return {boolean} True when they match.
42
+ */
43
+ function queryValueMatches(actual, expected) {
44
+ const want = String(expected)
45
+ if (Array.isArray(actual)) {
46
+ return actual.some((v) => String(v) === want)
47
+ }
48
+ return actual !== undefined && actual !== null && String(actual) === want
49
+ }
50
+
51
+ /**
52
+ * The index of the first preset whose every `match` pair is in the query.
53
+ * A preset without a non-empty `match` never matches.
54
+ *
55
+ * @param {Array<object>} presets The page's `queryPresets`.
56
+ * @param {object} query The current route query.
57
+ * @return {number} The preset index, or -1.
58
+ */
59
+ export function matchQueryPreset(presets, query) {
60
+ if (!Array.isArray(presets)) {
61
+ return -1
62
+ }
63
+ const q = query && typeof query === 'object' ? query : {}
64
+ return presets.findIndex((preset) => {
65
+ const match = preset && typeof preset.match === 'object' && preset.match !== null ? preset.match : null
66
+ if (!match || Object.keys(match).length === 0) {
67
+ return false
68
+ }
69
+ return Object.entries(match).every(([key, value]) => queryValueMatches(q[key], value))
70
+ })
71
+ }
72
+
73
+ /**
74
+ * The page config with the preset's allowed keys laid over it, and without
75
+ * `queryPresets` itself (the page component has no such prop).
76
+ *
77
+ * @param {object} config The page config.
78
+ * @param {object|null} preset The matched preset, or null.
79
+ * @return {object} The effective config.
80
+ */
81
+ export function applyQueryPreset(config, preset) {
82
+ const { queryPresets, ...rest } = config || {}
83
+ if (!preset || typeof preset !== 'object') {
84
+ return rest
85
+ }
86
+ const out = { ...rest }
87
+ for (const key of QUERY_PRESET_KEYS) {
88
+ if (key !== 'title' && preset[key] !== undefined) {
89
+ out[key] = preset[key]
90
+ }
91
+ }
92
+ return out
93
+ }
@@ -22,6 +22,10 @@
22
22
  * - `@today±Nd` → relative-date arithmetic at day granularity: N days
23
23
  * from today (e.g. `@today-30d`, `@today+7d`)
24
24
  * - `@monthStart` → first day of the current month, `YYYY-MM-DD`
25
+ * - `@monthEnd` → last day of the current month, `YYYY-MM-DD`; on a
26
+ * date-time field use `lt: '@nextMonthStart'` instead,
27
+ * since `lte` a date stops at that day's midnight
28
+ * - `@nextMonthStart` → first day of the next month, `YYYY-MM-DD`
25
29
  * - `@quarterStart` → first day of the current quarter, `YYYY-MM-DD`
26
30
  * - `@yearStart` → first day of the current year, `YYYY-MM-DD`
27
31
  * - `@currentFiscalYear` → the current calendar year as a number string, e.g. `2026`
@@ -214,6 +218,13 @@ export function resolveFilterValue(v, ctx) {
214
218
  if (v === '@monthStart') {
215
219
  return ymd(new Date(now.getFullYear(), now.getMonth(), 1))
216
220
  }
221
+ if (v === '@monthEnd') {
222
+ // Day 0 of the next month is the last day of this one.
223
+ return ymd(new Date(now.getFullYear(), now.getMonth() + 1, 0))
224
+ }
225
+ if (v === '@nextMonthStart') {
226
+ return ymd(new Date(now.getFullYear(), now.getMonth() + 1, 1))
227
+ }
217
228
  if (v === '@quarterStart') {
218
229
  return ymd(new Date(now.getFullYear(), Math.floor(now.getMonth() / 3) * 3, 1))
219
230
  }
@@ -1050,12 +1050,33 @@ export function fieldsFromSchema(schema, options = {}) {
1050
1050
  if (overrides[key]) {
1051
1051
  Object.assign(field, overrides[key])
1052
1052
  applyPickerOverride(field, overrides[key])
1053
+ translateOverrideText(field, overrides[key], tr)
1053
1054
  }
1054
1055
 
1055
1056
  return field
1056
1057
  })
1057
1058
  }
1058
1059
 
1060
+ /**
1061
+ * Run an override's display text through the translate function, as the
1062
+ * schema's own title and description are. A manifest writes `label`,
1063
+ * `description` and `placeholder` in its source language; without this a
1064
+ * form showed "Requester" to a Dutch user whose catalogue holds "Aanvrager".
1065
+ *
1066
+ * @param {object} field The field, with the override already merged in.
1067
+ * @param {object} override The per-key override.
1068
+ * @param {(text: string) => string} tr The translate function (identity when none).
1069
+ * @return {void}
1070
+ * @spec openspec/changes/screens-form-override-labels-parity/specs/form-override-labels/spec.md#requirement-override-text-reads-in-the-users-language
1071
+ */
1072
+ function translateOverrideText(field, override, tr) {
1073
+ for (const name of ['label', 'description', 'placeholder']) {
1074
+ if (typeof override[name] === 'string' && override[name] !== '') {
1075
+ field[name] = tr(override[name])
1076
+ }
1077
+ }
1078
+ }
1079
+
1059
1080
  /**
1060
1081
  * Generate faceted filter definitions from a schema's facetable properties.
1061
1082
  *
@@ -26,7 +26,7 @@
26
26
  * A token is valid only in the context whose resolver runs on it:
27
27
  *
28
28
  * - **filter** — `resolveFilterTokens`, resolved at fetch time:
29
- * `@me`, `@myGroups`, `@now`, `@today`, `@today±Nd`, `@monthStart`,
29
+ * `@me`, `@myGroups`, `@now`, `@today`, `@today±Nd`, `@monthStart`, `@monthEnd`, `@nextMonthStart`,
30
30
  * `@quarterStart`, `@yearStart`.
31
31
  * - **config** — `IAppConfig`-sourced: `@resolve:<key>`, `@config.<key>`
32
32
  * (trailing `?` marks optional).
@@ -67,7 +67,7 @@
67
67
  * @type {Readonly<Record<string, string>>}
68
68
  */
69
69
  export const SENTINEL_TOKEN_PATTERNS = Object.freeze({
70
- filter: '^@(?:me(?:\\.(?:displayName|email))?|myGroups|now|today|monthStart|quarterStart|yearStart)$|^@today[+-][0-9]+d$',
70
+ filter: '^@(?:me(?:\\.(?:displayName|email))?|myGroups|now|today|monthStart|monthEnd|nextMonthStart|quarterStart|yearStart)$|^@today[+-][0-9]+d$',
71
71
  config: '^@resolve:[a-z][a-z0-9_-]*$|^@config\\.[A-Za-z][A-Za-z0-9_.]*\\??$',
72
72
  object: '^@objectId$|^@object\\.[A-Za-z][A-Za-z0-9_]*$',
73
73
  workspace: '^@workspace\\.[A-Za-z][A-Za-z0-9_]*\\??$',
@@ -117,7 +117,7 @@ export const SENTINEL_VOCABULARY = Object.freeze({
117
117
  filter: {
118
118
  resolver: 'resolveFilterTokens',
119
119
  description: 'Relative fetch-time tokens (current user, the current user\'s groups as an IN list, relative dates).',
120
- members: ['@me', '@me.displayName', '@me.email', '@myGroups', '@now', '@today', '@today±Nd', '@monthStart', '@quarterStart', '@yearStart'],
120
+ members: ['@me', '@me.displayName', '@me.email', '@myGroups', '@now', '@today', '@today±Nd', '@monthStart', '@monthEnd', '@nextMonthStart', '@quarterStart', '@yearStart'],
121
121
  },
122
122
  config: {
123
123
  resolver: 'resolveManifestSentinels / resolveFilterTokens',