@conduction/nextcloud-vue 2.79.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 (197) hide show
  1. package/dist/esm/components/CnActionButtons/CnActionButtons.vue2.js.map +1 -1
  2. package/dist/esm/components/CnActionButtons/CnActionButtons.vue3.js +3 -1
  3. package/dist/esm/components/CnActionButtons/CnActionButtons.vue3.js.map +1 -1
  4. package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js +32 -3
  5. package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js.map +1 -1
  6. package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js +10 -8
  7. package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js.map +1 -1
  8. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js +39 -3
  9. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js.map +1 -1
  10. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue3.js.map +1 -1
  11. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js +79 -1
  12. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js.map +1 -1
  13. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js +133 -96
  14. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js.map +1 -1
  15. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
  16. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +1 -0
  17. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
  18. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +62 -4
  19. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  20. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  21. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js +24 -0
  22. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
  23. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js +43 -4
  24. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
  25. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue.js +11 -0
  26. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue.js.map +1 -0
  27. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js +736 -0
  28. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js.map +1 -0
  29. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js +238 -0
  30. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js.map +1 -0
  31. package/dist/esm/components/CnFactsWidget/index.js +30 -0
  32. package/dist/esm/components/CnFactsWidget/index.js.map +1 -0
  33. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue.js +10 -0
  34. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue.js.map +1 -0
  35. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js +551 -0
  36. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js.map +1 -0
  37. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js +183 -0
  38. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js.map +1 -0
  39. package/dist/esm/components/CnFieldListWidget/index.js +29 -0
  40. package/dist/esm/components/CnFieldListWidget/index.js.map +1 -0
  41. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js +65 -17
  42. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
  43. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js +4 -4
  44. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
  45. package/dist/esm/components/CnFormPage/CnFormPage.vue2.js +18 -3
  46. package/dist/esm/components/CnFormPage/CnFormPage.vue2.js.map +1 -1
  47. package/dist/esm/components/CnFormPage/CnFormPage.vue3.js.map +1 -1
  48. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +305 -72
  49. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  50. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +31 -13
  51. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  52. package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue.js +10 -0
  53. package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue.js.map +1 -0
  54. package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue2.js +99 -0
  55. package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue2.js.map +1 -0
  56. package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue3.js +48 -0
  57. package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue3.js.map +1 -0
  58. package/dist/esm/components/CnPageRenderer/CnPageRenderer.vue2.js +29 -1
  59. package/dist/esm/components/CnPageRenderer/CnPageRenderer.vue2.js.map +1 -1
  60. package/dist/esm/components/CnPageRenderer/CnPageRenderer.vue3.js.map +1 -1
  61. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +10 -0
  62. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js.map +1 -0
  63. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +387 -0
  64. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -0
  65. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js +169 -0
  66. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -0
  67. package/dist/esm/components/CnRecordListWidget/index.js +31 -0
  68. package/dist/esm/components/CnRecordListWidget/index.js.map +1 -0
  69. package/dist/esm/components/CnSectionsWidget/CnSectionsWidget.vue.js +10 -0
  70. package/dist/esm/components/CnSectionsWidget/CnSectionsWidget.vue.js.map +1 -0
  71. package/dist/esm/components/CnSectionsWidget/CnSectionsWidget.vue2.js +351 -0
  72. package/dist/esm/components/CnSectionsWidget/CnSectionsWidget.vue2.js.map +1 -0
  73. package/dist/esm/components/CnSectionsWidget/CnSectionsWidget.vue3.js +71 -0
  74. package/dist/esm/components/CnSectionsWidget/CnSectionsWidget.vue3.js.map +1 -0
  75. package/dist/esm/components/CnSectionsWidget/index.js +35 -0
  76. package/dist/esm/components/CnSectionsWidget/index.js.map +1 -0
  77. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue2.js +81 -2
  78. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue2.js.map +1 -1
  79. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue3.js.map +1 -1
  80. package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js.map +1 -1
  81. package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js +1 -0
  82. package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js.map +1 -1
  83. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +122 -7
  84. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
  85. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js +84 -23
  86. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
  87. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js +4 -0
  88. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js.map +1 -1
  89. package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue2.js +106 -2
  90. package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue2.js.map +1 -1
  91. package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue3.js +105 -53
  92. package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue3.js.map +1 -1
  93. package/dist/esm/composables/useFormDraft.js +140 -9
  94. package/dist/esm/composables/useFormDraft.js.map +1 -1
  95. package/dist/esm/index.js +4 -0
  96. package/dist/esm/index.js.map +1 -1
  97. package/dist/esm/integrations/builtin/field-inspection/CnFieldInspectionCard.vue2.js +28 -20
  98. package/dist/esm/integrations/builtin/field-inspection/CnFieldInspectionCard.vue2.js.map +1 -1
  99. package/dist/esm/integrations/builtin/field-inspection/CnFieldInspectionCard.vue3.js +9 -0
  100. package/dist/esm/integrations/builtin/field-inspection/CnFieldInspectionCard.vue3.js.map +1 -1
  101. package/dist/esm/integrations/builtin/field-inspection.js +21 -0
  102. package/dist/esm/integrations/builtin/field-inspection.js.map +1 -1
  103. package/dist/esm/integrations/offline/checklistSubmission.js +141 -0
  104. package/dist/esm/integrations/offline/checklistSubmission.js.map +1 -0
  105. package/dist/esm/integrations/offline/fieldCollectionHelpers.js +70 -5
  106. package/dist/esm/integrations/offline/fieldCollectionHelpers.js.map +1 -1
  107. package/dist/esm/integrations/offline/index.js +2 -1
  108. package/dist/esm/integrations/offline/index.js.map +1 -1
  109. package/dist/esm/integrations/offline/offlineDb.js +6 -1
  110. package/dist/esm/integrations/offline/offlineDb.js.map +1 -1
  111. package/dist/esm/integrations/offline/syncReplayService.js +8 -2
  112. package/dist/esm/integrations/offline/syncReplayService.js.map +1 -1
  113. package/dist/esm/l10n/en.json.js +12 -1
  114. package/dist/esm/l10n/en.json.js.map +1 -1
  115. package/dist/esm/l10n/nl.json.js +16 -2
  116. package/dist/esm/l10n/nl.json.js.map +1 -1
  117. package/dist/esm/nextcloud-vue.css +1000 -59
  118. package/dist/esm/utils/detailActionModel.js +60 -3
  119. package/dist/esm/utils/detailActionModel.js.map +1 -1
  120. package/dist/esm/utils/fillTemplate.js +114 -0
  121. package/dist/esm/utils/fillTemplate.js.map +1 -0
  122. package/dist/esm/utils/libraryWidgetKeys.js +4 -0
  123. package/dist/esm/utils/libraryWidgetKeys.js.map +1 -1
  124. package/dist/esm/utils/queryPresets.js +96 -0
  125. package/dist/esm/utils/queryPresets.js.map +1 -0
  126. package/dist/esm/utils/resolveFilterTokens.js +11 -0
  127. package/dist/esm/utils/resolveFilterTokens.js.map +1 -1
  128. package/dist/esm/utils/schema.js +21 -0
  129. package/dist/esm/utils/schema.js.map +1 -1
  130. package/dist/esm/utils/sentinelTokens.js +3 -3
  131. package/dist/esm/utils/sentinelTokens.js.map +1 -1
  132. package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
  133. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  134. package/dist/esm/utils/widgetDispatch.js +7 -1
  135. package/dist/esm/utils/widgetDispatch.js.map +1 -1
  136. package/dist/nextcloud-vue.cjs.js +12368 -7717
  137. package/dist/nextcloud-vue.cjs.js.map +2 -2
  138. package/dist/nextcloud-vue.css +1000 -59
  139. package/l10n/en.json +12 -1
  140. package/l10n/nl.json +16 -2
  141. package/package.json +1 -1
  142. package/src/components/CnActionButtons/CnActionButtons.vue +2 -0
  143. package/src/components/CnActionsBar/CnActionsBar.md +4 -0
  144. package/src/components/CnActionsBar/CnActionsBar.vue +42 -10
  145. package/src/components/CnBannerWidget/CnBannerWidget.vue +39 -3
  146. package/src/components/CnCellRenderer/CnCellRenderer.vue +188 -1
  147. package/src/components/CnDataTable/CnDataTable.vue +1 -0
  148. package/src/components/CnDetailPage/CnDetailPage.md +1 -1
  149. package/src/components/CnDetailPage/CnDetailPage.vue +63 -3
  150. package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +66 -0
  151. package/src/components/CnFactsWidget/CnFactsWidget.vue +1050 -0
  152. package/src/components/CnFactsWidget/index.js +29 -0
  153. package/src/components/CnFieldListWidget/CnFieldListWidget.vue +746 -0
  154. package/src/components/CnFieldListWidget/index.js +28 -0
  155. package/src/components/CnFormDialog/CnFormDialog.md +9 -3
  156. package/src/components/CnFormDialog/CnFormDialog.vue +69 -21
  157. package/src/components/CnFormPage/CnFormPage.vue +18 -3
  158. package/src/components/CnIndexPage/CnIndexPage.vue +254 -6
  159. package/src/components/CnIndexPage/CnIndexPageWidgets.vue +169 -0
  160. package/src/components/CnPageRenderer/CnPageRenderer.vue +29 -1
  161. package/src/components/CnRecordListWidget/CnRecordListWidget.vue +603 -0
  162. package/src/components/CnRecordListWidget/index.js +30 -0
  163. package/src/components/CnSectionsWidget/CnSectionsWidget.vue +489 -0
  164. package/src/components/CnSectionsWidget/index.js +34 -0
  165. package/src/components/CnStagesWidget/CnStagesWidget.vue +81 -2
  166. package/src/components/CnStatWidget/CnStatWidget.vue +1 -1
  167. package/src/components/CnTabsWidget/CnTabsWidget.vue +184 -8
  168. package/src/components/CnWidgetGrid/registerDashboardWidgets.js +4 -0
  169. package/src/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue +188 -4
  170. package/src/components/index.js +4 -0
  171. package/src/composables/__tests__/useFormDraft.spec.js +69 -0
  172. package/src/composables/useFormDraft.js +139 -8
  173. package/src/css/form-field.css +14 -0
  174. package/src/css/index-page.css +71 -0
  175. package/src/css/kpi-card.css +32 -0
  176. package/src/css/look-board-detail.css +77 -0
  177. package/src/css/look-board-index.css +51 -0
  178. package/src/css/look-board.css +37 -2
  179. package/src/css/table.css +19 -0
  180. package/src/index.js +4 -0
  181. package/src/integrations/builtin/field-inspection/CnFieldInspectionCard.vue +35 -20
  182. package/src/integrations/builtin/field-inspection.js +21 -0
  183. package/src/integrations/offline/checklistSubmission.js +137 -0
  184. package/src/integrations/offline/fieldCollectionHelpers.js +69 -4
  185. package/src/integrations/offline/index.js +3 -0
  186. package/src/integrations/offline/offlineDb.js +6 -1
  187. package/src/integrations/offline/syncReplayService.js +8 -2
  188. package/src/schemas/app-manifest-v2.schema.json +104 -5
  189. package/src/utils/detailActionModel.js +59 -2
  190. package/src/utils/fillTemplate.js +110 -0
  191. package/src/utils/libraryWidgetKeys.js +4 -0
  192. package/src/utils/queryPresets.js +93 -0
  193. package/src/utils/resolveFilterTokens.js +11 -0
  194. package/src/utils/schema.js +21 -0
  195. package/src/utils/sentinelTokens.js +3 -3
  196. package/src/utils/validateManifestV2.compiled.js +3 -3
  197. package/src/utils/widgetDispatch.js +7 -1
@@ -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
+ }
@@ -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',