@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
@@ -202,13 +202,29 @@ function resolveNextStep(config, object, stageField = 'status') {
202
202
  /**
203
203
  * Resolve a header pill from the record.
204
204
  *
205
- * @param {{field?: string, colorMap?: object, labels?: object, variant?: string}|null} config The pill config.
205
+ * A pill with `field` reads it off the record; one without `field` and with
206
+ * `text` is a constant. A count pill (`countFrom`) is resolved by
207
+ * {@link resolveCountPill} once its count is known.
208
+ *
209
+ * @param {{field?: string, text?: string, colorMap?: object, labels?: object, variant?: string}|null} config The pill config.
206
210
  * @param {object|null} object The record.
207
211
  * @return {{label: string, colorKey: string, colorMap: object|null, variant: string}|null} What CnStatusBadge needs, or null when the field is empty.
208
212
  * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-header-pills
209
213
  */
210
214
  function resolvePill(config, object) {
211
- if (!config || typeof config !== 'object' || typeof config.field !== 'string' || config.field === '') {
215
+ if (!config || typeof config !== 'object') {
216
+ return null
217
+ }
218
+ // A constant pill (`text`): "Inwoner", "Klant" (screens-relation-pages).
219
+ if ((typeof config.field !== 'string' || config.field === '') && typeof config.text === 'string' && config.text !== '') {
220
+ return {
221
+ label: config.text,
222
+ colorKey: config.text,
223
+ colorMap: config.colorMap && typeof config.colorMap === 'object' ? config.colorMap : null,
224
+ variant: typeof config.variant === 'string' && config.variant !== '' ? config.variant : 'default',
225
+ }
226
+ }
227
+ if (typeof config.field !== 'string' || config.field === '') {
212
228
  return null
213
229
  }
214
230
  const raw = stageOf(object, config.field);
@@ -262,5 +278,46 @@ function resolveTabCount(tab, object) {
262
278
  return Number.isFinite(number) ? number : null
263
279
  }
264
280
 
265
- export { MAX_QUICK_ACTIONS, groupMenuEntries, isChecklistItemDone, normalisePinnedAction, resolveNextStep, resolvePill, resolveTabCount, stageEntry, stageOf };
281
+ /**
282
+ * The list a count pill counts (`countFrom`), or null for any other pill.
283
+ *
284
+ * @param {object|null} config The pill config.
285
+ * @return {{register: string, schema: string, filter: object}|null} The list.
286
+ * @spec openspec/changes/screens-relation-pages/specs/relation-pages/spec.md#requirement-a-header-pill-can-be-a-constant-or-a-count
287
+ */
288
+ function pillCountSource(config) {
289
+ const from = config && typeof config === 'object' && !config.field ? config.countFrom : null;
290
+ if (!from || typeof from !== 'object' || typeof from.register !== 'string' || from.register === '' || typeof from.schema !== 'string' || from.schema === '') {
291
+ return null
292
+ }
293
+ const filter = from.filter && typeof from.filter === 'object' && !Array.isArray(from.filter) ? from.filter : {};
294
+ return { register: from.register, schema: from.schema, filter }
295
+ }
296
+
297
+ /**
298
+ * A count pill once its count is known: "1 lopende zaak", "2 lopende zaken".
299
+ * Nothing for a count of 0, an unknown count or a failed one.
300
+ *
301
+ * @param {{countLabel?: {one?: string, other?: string}, colorMap?: object, variant?: string}|null} config The pill config.
302
+ * @param {number|null|undefined} count The count.
303
+ * @param {(text: string) => string} [translate] Applied to the label before `{count}` is filled.
304
+ * @return {{label: string, colorKey: string, colorMap: object|null, variant: string}|null} What CnStatusBadge needs, or null.
305
+ * @spec openspec/changes/screens-relation-pages/specs/relation-pages/spec.md#requirement-a-header-pill-can-be-a-constant-or-a-count
306
+ */
307
+ function resolveCountPill(config, count, translate = (text) => text) {
308
+ if (!pillCountSource(config) || typeof count !== 'number' || !Number.isFinite(count) || count <= 0) {
309
+ return null
310
+ }
311
+ const labels = config.countLabel && typeof config.countLabel === 'object' ? config.countLabel : {};
312
+ const template = (count === 1 ? labels.one : labels.other) || labels.other || labels.one || '{count}';
313
+ const label = translate(template).replace(/\{count\}/g, String(count));
314
+ return {
315
+ label,
316
+ colorKey: '',
317
+ colorMap: config.colorMap && typeof config.colorMap === 'object' ? config.colorMap : null,
318
+ variant: typeof config.variant === 'string' && config.variant !== '' ? config.variant : 'default',
319
+ }
320
+ }
321
+
322
+ export { MAX_QUICK_ACTIONS, groupMenuEntries, isChecklistItemDone, normalisePinnedAction, pillCountSource, resolveCountPill, resolveNextStep, resolvePill, resolveTabCount, stageEntry, stageOf };
266
323
  //# sourceMappingURL=detailActionModel.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"detailActionModel.js","sources":["../../../src/utils/detailActionModel.js"],"sourcesContent":["/**\n * SPDX-FileCopyrightText: 2026 Conduction B.V.\n * SPDX-License-Identifier: EUPL-1.2\n *\n * detailActionModel: the rules behind a detail page's four action levels and\n * the case surfaces that read the record's stage.\n *\n * 1. the next step: one primary button, chosen by the record's stage,\n * 2. quick actions: at most three always visible buttons,\n * 3. the overflow menu: the rest, in named groups,\n * 4. admin actions: the last group, for admins only.\n *\n * Pure functions, no Vue and no fetch, so CnDetailPage, the widgets and the\n * tests all read one implementation.\n *\n * @module utils/detailActionModel\n */\n\nimport { readPath } from './readPath.js'\nimport { evaluateVisibleWhenLocal } from './visibleWhen.js'\n\n/** The most quick actions a page shows. Later entries are dropped. */\nexport const MAX_QUICK_ACTIONS = 3\n\n/**\n * The record's stage, as the string a stage map is keyed on.\n *\n * @param {object|null} object The record.\n * @param {string} [stageField] Dot-path to the stage. Defaults to `status`.\n * @return {string} The stage, or '' when the record has none.\n * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-primary-action-follows-the-stage\n */\nexport function stageOf(object, stageField = 'status') {\n\tif (!object || typeof object !== 'object') {\n\t\treturn ''\n\t}\n\tconst value = readPath(object, stageField || 'status')\n\tif (value === null || value === undefined || typeof value === 'object') {\n\t\treturn ''\n\t}\n\treturn String(value)\n}\n\n/**\n * Look a stage up in a stage-keyed map. An exact key wins; otherwise the\n * match ignores case, because a status is stored as `in_behandeling` in one\n * register and `In_Behandeling` in the next.\n *\n * @param {object|null} map The stage-keyed map.\n * @param {string} stage The stage.\n * @return {unknown} The entry, or undefined.\n * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-primary-action-follows-the-stage\n */\nexport function stageEntry(map, stage) {\n\tif (!map || typeof map !== 'object' || Array.isArray(map) || stage === '') {\n\t\treturn undefined\n\t}\n\tif (Object.hasOwn(map, stage)) {\n\t\treturn map[stage]\n\t}\n\tconst wanted = stage.toLowerCase()\n\tconst key = Object.keys(map).find((candidate) => candidate.toLowerCase() === wanted)\n\treturn key === undefined ? undefined : map[key]\n}\n\n/**\n * Turn a pinned action entry into a descriptor the page can dispatch.\n *\n * An entry is the id of a declared header action, or an action object of its\n * own. An object without an id gets `fallbackId`, since every dispatched\n * action is addressed by id.\n *\n * @param {string|object|null} entry The entry.\n * @param {string} fallbackId Id for an inline action that names none.\n * @return {{id: string, inline: object|null}|null} The id, plus the inline definition when the entry carried one.\n * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-quick-actions\n */\nexport function normalisePinnedAction(entry, fallbackId) {\n\tif (typeof entry === 'string') {\n\t\treturn entry === '' ? null : { id: entry, inline: null }\n\t}\n\tif (!entry || typeof entry !== 'object' || Array.isArray(entry)) {\n\t\treturn null\n\t}\n\tif (typeof entry.label !== 'string' || entry.label === '') {\n\t\treturn null\n\t}\n\tconst id = typeof entry.id === 'string' && entry.id !== '' ? entry.id : fallbackId\n\treturn { id, inline: { ...entry, id } }\n}\n\n/**\n * Split menu entries into the groups the overflow menu draws.\n *\n * Ungrouped entries come first, under no caption. Named groups follow in the\n * order they first appear. Admin only entries are dropped for everyone else\n * and form the last group for admins, whatever `group` they also name.\n *\n * `adminOnly` hides a control. It is never an authorization decision: the\n * endpoint behind the action decides who may call it.\n *\n * @param {Array<object>} entries Menu-ready entries, each with an `id`.\n * @param {object} actionsById The declared actions by id, carrying `group` / `adminOnly`.\n * @param {{isAdmin?: boolean, adminLabel?: string}} [options] Who is looking, and what the admin group is called.\n * @return {Array<{key: string, label: string, entries: Array<object>}>} The non-empty groups, in render order.\n * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-grouped-and-admin-only-menu-actions\n */\nexport function groupMenuEntries(entries, actionsById = {}, options = {}) {\n\tconst isAdmin = options.isAdmin === true\n\tconst plain = { key: '', label: '', entries: [] }\n\tconst named = []\n\tconst admin = { key: 'cn-admin', label: options.adminLabel || '', entries: [] }\n\tfor (const entry of Array.isArray(entries) ? entries : []) {\n\t\tconst declared = (entry && actionsById[entry.id]) || {}\n\t\tif (declared.adminOnly === true) {\n\t\t\tif (isAdmin) {\n\t\t\t\tadmin.entries.push(entry)\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tconst label = typeof declared.group === 'string' ? declared.group.trim() : ''\n\t\tif (label === '') {\n\t\t\tplain.entries.push(entry)\n\t\t\tcontinue\n\t\t}\n\t\tlet group = named.find((candidate) => candidate.label === label)\n\t\tif (!group) {\n\t\t\tgroup = { key: `cn-group-${named.length}`, label, entries: [] }\n\t\t\tnamed.push(group)\n\t\t}\n\t\tgroup.entries.push(entry)\n\t}\n\treturn [plain, ...named, admin].filter((group) => group.entries.length > 0)\n}\n\n/**\n * Whether one checklist item is done for this record.\n *\n * `done` (a literal) wins, then `doneWhen` (a local visibleWhen condition),\n * then `doneField` (truthy, and for a list: not empty).\n *\n * @param {object} item The checklist item.\n * @param {object|null} object The record.\n * @return {boolean} True when the item is done.\n * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-next-step-card\n */\nexport function isChecklistItemDone(item, object) {\n\tif (!item || typeof item !== 'object') {\n\t\treturn false\n\t}\n\tif (typeof item.done === 'boolean') {\n\t\treturn item.done\n\t}\n\tconst data = object && typeof object === 'object' ? object : {}\n\tif (item.doneWhen && typeof item.doneWhen === 'object') {\n\t\treturn evaluateVisibleWhenLocal(item.doneWhen, data)\n\t}\n\tif (typeof item.doneField === 'string' && item.doneField !== '') {\n\t\tconst value = readPath(data, item.doneField)\n\t\treturn Array.isArray(value) ? value.length > 0 : Boolean(value)\n\t}\n\treturn false\n}\n\n/**\n * Resolve the \"what now\" card for the record's stage.\n *\n * @param {{stages?: object, title?: string}|null} config The `nextStep` config.\n * @param {object|null} object The record.\n * @param {string} [stageField] Dot-path to the stage.\n * @return {{stage: string, title: string, items: Array<{label: string, done: boolean, hint: string}>, after: string}|null} The card, or null when this stage declares no checklist.\n * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-next-step-card\n */\nexport function resolveNextStep(config, object, stageField = 'status') {\n\tif (!config || typeof config !== 'object') {\n\t\treturn null\n\t}\n\tconst stage = stageOf(object, config.field || stageField)\n\tconst declared = stageEntry(config.stages, stage)\n\tif (!declared || typeof declared !== 'object' || !Array.isArray(declared.checklist)) {\n\t\treturn null\n\t}\n\tconst items = declared.checklist\n\t\t.filter((item) => item && typeof item === 'object' && typeof item.label === 'string' && item.label !== '')\n\t\t.map((item) => ({\n\t\t\tlabel: item.label,\n\t\t\tdone: isChecklistItemDone(item, object),\n\t\t\thint: typeof item.hint === 'string' ? item.hint : '',\n\t\t}))\n\tif (items.length === 0) {\n\t\treturn null\n\t}\n\treturn {\n\t\tstage,\n\t\ttitle: typeof declared.title === 'string' ? declared.title : (typeof config.title === 'string' ? config.title : ''),\n\t\titems,\n\t\tafter: typeof declared.after === 'string' ? declared.after : '',\n\t}\n}\n\n/**\n * Resolve a header pill from the record.\n *\n * @param {{field?: string, colorMap?: object, labels?: object, variant?: string}|null} config The pill config.\n * @param {object|null} object The record.\n * @return {{label: string, colorKey: string, colorMap: object|null, variant: string}|null} What CnStatusBadge needs, or null when the field is empty.\n * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-header-pills\n */\nexport function resolvePill(config, object) {\n\tif (!config || typeof config !== 'object' || typeof config.field !== 'string' || config.field === '') {\n\t\treturn null\n\t}\n\tconst raw = stageOf(object, config.field)\n\tif (raw === '') {\n\t\treturn null\n\t}\n\tconst mapped = stageEntry(config.labels, raw)\n\treturn {\n\t\tlabel: typeof mapped === 'string' && mapped !== '' ? mapped : raw,\n\t\tcolorKey: raw,\n\t\tcolorMap: config.colorMap && typeof config.colorMap === 'object' ? config.colorMap : null,\n\t\tvariant: typeof config.variant === 'string' && config.variant !== '' ? config.variant : 'default',\n\t}\n}\n\n/**\n * Resolve a tab's count.\n *\n * `count` (a number) wins. Otherwise `countField` is read off the record: a\n * list counts its items, a number is taken as is. A tab that declares neither\n * takes what is new on it from `@self.unreadCounts[<unreadKey>]` (the key\n * defaults to the tab's `id`, else its `widgetId`) when that is above 0;\n * absent or 0 is no count.\n *\n * @param {{count?: number, countField?: string, unreadKey?: string, id?: string, widgetId?: string}|null} tab The tab declaration.\n * @param {object|null} object The record.\n * @return {number|null} The count, or null when the tab declares none.\n * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-tab-counts-and-overflow\n */\nexport function resolveTabCount(tab, object) {\n\tif (!tab || typeof tab !== 'object') {\n\t\treturn null\n\t}\n\tif (typeof tab.count === 'number' && Number.isFinite(tab.count)) {\n\t\treturn tab.count\n\t}\n\tif (typeof tab.countField !== 'string' || tab.countField === '') {\n\t\tconst counts = object && typeof object === 'object' && object['@self'] && object['@self'].unreadCounts\n\t\tconst key = tab.unreadKey || tab.id || tab.widgetId\n\t\tconst unread = counts && typeof counts === 'object' && typeof key === 'string' ? Number(counts[key]) : 0\n\t\treturn Number.isFinite(unread) && unread > 0 ? unread : null\n\t}\n\tconst value = readPath(object && typeof object === 'object' ? object : {}, tab.countField)\n\tif (Array.isArray(value)) {\n\t\treturn value.length\n\t}\n\tif (value === null || value === undefined || value === '') {\n\t\treturn 0\n\t}\n\tconst number = Number(value)\n\treturn Number.isFinite(number) ? number : null\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"detailActionModel.js","sources":["../../../src/utils/detailActionModel.js"],"sourcesContent":["/**\n * SPDX-FileCopyrightText: 2026 Conduction B.V.\n * SPDX-License-Identifier: EUPL-1.2\n *\n * detailActionModel: the rules behind a detail page's four action levels and\n * the case surfaces that read the record's stage.\n *\n * 1. the next step: one primary button, chosen by the record's stage,\n * 2. quick actions: at most three always visible buttons,\n * 3. the overflow menu: the rest, in named groups,\n * 4. admin actions: the last group, for admins only.\n *\n * Pure functions, no Vue and no fetch, so CnDetailPage, the widgets and the\n * tests all read one implementation.\n *\n * @module utils/detailActionModel\n */\n\nimport { readPath } from './readPath.js'\nimport { evaluateVisibleWhenLocal } from './visibleWhen.js'\n\n/** The most quick actions a page shows. Later entries are dropped. */\nexport const MAX_QUICK_ACTIONS = 3\n\n/**\n * The record's stage, as the string a stage map is keyed on.\n *\n * @param {object|null} object The record.\n * @param {string} [stageField] Dot-path to the stage. Defaults to `status`.\n * @return {string} The stage, or '' when the record has none.\n * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-primary-action-follows-the-stage\n */\nexport function stageOf(object, stageField = 'status') {\n\tif (!object || typeof object !== 'object') {\n\t\treturn ''\n\t}\n\tconst value = readPath(object, stageField || 'status')\n\tif (value === null || value === undefined || typeof value === 'object') {\n\t\treturn ''\n\t}\n\treturn String(value)\n}\n\n/**\n * Look a stage up in a stage-keyed map. An exact key wins; otherwise the\n * match ignores case, because a status is stored as `in_behandeling` in one\n * register and `In_Behandeling` in the next.\n *\n * @param {object|null} map The stage-keyed map.\n * @param {string} stage The stage.\n * @return {unknown} The entry, or undefined.\n * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-primary-action-follows-the-stage\n */\nexport function stageEntry(map, stage) {\n\tif (!map || typeof map !== 'object' || Array.isArray(map) || stage === '') {\n\t\treturn undefined\n\t}\n\tif (Object.hasOwn(map, stage)) {\n\t\treturn map[stage]\n\t}\n\tconst wanted = stage.toLowerCase()\n\tconst key = Object.keys(map).find((candidate) => candidate.toLowerCase() === wanted)\n\treturn key === undefined ? undefined : map[key]\n}\n\n/**\n * Turn a pinned action entry into a descriptor the page can dispatch.\n *\n * An entry is the id of a declared header action, or an action object of its\n * own. An object without an id gets `fallbackId`, since every dispatched\n * action is addressed by id.\n *\n * @param {string|object|null} entry The entry.\n * @param {string} fallbackId Id for an inline action that names none.\n * @return {{id: string, inline: object|null}|null} The id, plus the inline definition when the entry carried one.\n * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-quick-actions\n */\nexport function normalisePinnedAction(entry, fallbackId) {\n\tif (typeof entry === 'string') {\n\t\treturn entry === '' ? null : { id: entry, inline: null }\n\t}\n\tif (!entry || typeof entry !== 'object' || Array.isArray(entry)) {\n\t\treturn null\n\t}\n\tif (typeof entry.label !== 'string' || entry.label === '') {\n\t\treturn null\n\t}\n\tconst id = typeof entry.id === 'string' && entry.id !== '' ? entry.id : fallbackId\n\treturn { id, inline: { ...entry, id } }\n}\n\n/**\n * Split menu entries into the groups the overflow menu draws.\n *\n * Ungrouped entries come first, under no caption. Named groups follow in the\n * order they first appear. Admin only entries are dropped for everyone else\n * and form the last group for admins, whatever `group` they also name.\n *\n * `adminOnly` hides a control. It is never an authorization decision: the\n * endpoint behind the action decides who may call it.\n *\n * @param {Array<object>} entries Menu-ready entries, each with an `id`.\n * @param {object} actionsById The declared actions by id, carrying `group` / `adminOnly`.\n * @param {{isAdmin?: boolean, adminLabel?: string}} [options] Who is looking, and what the admin group is called.\n * @return {Array<{key: string, label: string, entries: Array<object>}>} The non-empty groups, in render order.\n * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-grouped-and-admin-only-menu-actions\n */\nexport function groupMenuEntries(entries, actionsById = {}, options = {}) {\n\tconst isAdmin = options.isAdmin === true\n\tconst plain = { key: '', label: '', entries: [] }\n\tconst named = []\n\tconst admin = { key: 'cn-admin', label: options.adminLabel || '', entries: [] }\n\tfor (const entry of Array.isArray(entries) ? entries : []) {\n\t\tconst declared = (entry && actionsById[entry.id]) || {}\n\t\tif (declared.adminOnly === true) {\n\t\t\tif (isAdmin) {\n\t\t\t\tadmin.entries.push(entry)\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tconst label = typeof declared.group === 'string' ? declared.group.trim() : ''\n\t\tif (label === '') {\n\t\t\tplain.entries.push(entry)\n\t\t\tcontinue\n\t\t}\n\t\tlet group = named.find((candidate) => candidate.label === label)\n\t\tif (!group) {\n\t\t\tgroup = { key: `cn-group-${named.length}`, label, entries: [] }\n\t\t\tnamed.push(group)\n\t\t}\n\t\tgroup.entries.push(entry)\n\t}\n\treturn [plain, ...named, admin].filter((group) => group.entries.length > 0)\n}\n\n/**\n * Whether one checklist item is done for this record.\n *\n * `done` (a literal) wins, then `doneWhen` (a local visibleWhen condition),\n * then `doneField` (truthy, and for a list: not empty).\n *\n * @param {object} item The checklist item.\n * @param {object|null} object The record.\n * @return {boolean} True when the item is done.\n * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-next-step-card\n */\nexport function isChecklistItemDone(item, object) {\n\tif (!item || typeof item !== 'object') {\n\t\treturn false\n\t}\n\tif (typeof item.done === 'boolean') {\n\t\treturn item.done\n\t}\n\tconst data = object && typeof object === 'object' ? object : {}\n\tif (item.doneWhen && typeof item.doneWhen === 'object') {\n\t\treturn evaluateVisibleWhenLocal(item.doneWhen, data)\n\t}\n\tif (typeof item.doneField === 'string' && item.doneField !== '') {\n\t\tconst value = readPath(data, item.doneField)\n\t\treturn Array.isArray(value) ? value.length > 0 : Boolean(value)\n\t}\n\treturn false\n}\n\n/**\n * Resolve the \"what now\" card for the record's stage.\n *\n * @param {{stages?: object, title?: string}|null} config The `nextStep` config.\n * @param {object|null} object The record.\n * @param {string} [stageField] Dot-path to the stage.\n * @return {{stage: string, title: string, items: Array<{label: string, done: boolean, hint: string}>, after: string}|null} The card, or null when this stage declares no checklist.\n * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-next-step-card\n */\nexport function resolveNextStep(config, object, stageField = 'status') {\n\tif (!config || typeof config !== 'object') {\n\t\treturn null\n\t}\n\tconst stage = stageOf(object, config.field || stageField)\n\tconst declared = stageEntry(config.stages, stage)\n\tif (!declared || typeof declared !== 'object' || !Array.isArray(declared.checklist)) {\n\t\treturn null\n\t}\n\tconst items = declared.checklist\n\t\t.filter((item) => item && typeof item === 'object' && typeof item.label === 'string' && item.label !== '')\n\t\t.map((item) => ({\n\t\t\tlabel: item.label,\n\t\t\tdone: isChecklistItemDone(item, object),\n\t\t\thint: typeof item.hint === 'string' ? item.hint : '',\n\t\t}))\n\tif (items.length === 0) {\n\t\treturn null\n\t}\n\treturn {\n\t\tstage,\n\t\ttitle: typeof declared.title === 'string' ? declared.title : (typeof config.title === 'string' ? config.title : ''),\n\t\titems,\n\t\tafter: typeof declared.after === 'string' ? declared.after : '',\n\t}\n}\n\n/**\n * Resolve a header pill from the record.\n *\n * A pill with `field` reads it off the record; one without `field` and with\n * `text` is a constant. A count pill (`countFrom`) is resolved by\n * {@link resolveCountPill} once its count is known.\n *\n * @param {{field?: string, text?: string, colorMap?: object, labels?: object, variant?: string}|null} config The pill config.\n * @param {object|null} object The record.\n * @return {{label: string, colorKey: string, colorMap: object|null, variant: string}|null} What CnStatusBadge needs, or null when the field is empty.\n * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-header-pills\n */\nexport function resolvePill(config, object) {\n\tif (!config || typeof config !== 'object') {\n\t\treturn null\n\t}\n\t// A constant pill (`text`): \"Inwoner\", \"Klant\" (screens-relation-pages).\n\tif ((typeof config.field !== 'string' || config.field === '') && typeof config.text === 'string' && config.text !== '') {\n\t\treturn {\n\t\t\tlabel: config.text,\n\t\t\tcolorKey: config.text,\n\t\t\tcolorMap: config.colorMap && typeof config.colorMap === 'object' ? config.colorMap : null,\n\t\t\tvariant: typeof config.variant === 'string' && config.variant !== '' ? config.variant : 'default',\n\t\t}\n\t}\n\tif (typeof config.field !== 'string' || config.field === '') {\n\t\treturn null\n\t}\n\tconst raw = stageOf(object, config.field)\n\tif (raw === '') {\n\t\treturn null\n\t}\n\tconst mapped = stageEntry(config.labels, raw)\n\treturn {\n\t\tlabel: typeof mapped === 'string' && mapped !== '' ? mapped : raw,\n\t\tcolorKey: raw,\n\t\tcolorMap: config.colorMap && typeof config.colorMap === 'object' ? config.colorMap : null,\n\t\tvariant: typeof config.variant === 'string' && config.variant !== '' ? config.variant : 'default',\n\t}\n}\n\n/**\n * Resolve a tab's count.\n *\n * `count` (a number) wins. Otherwise `countField` is read off the record: a\n * list counts its items, a number is taken as is. A tab that declares neither\n * takes what is new on it from `@self.unreadCounts[<unreadKey>]` (the key\n * defaults to the tab's `id`, else its `widgetId`) when that is above 0;\n * absent or 0 is no count.\n *\n * @param {{count?: number, countField?: string, unreadKey?: string, id?: string, widgetId?: string}|null} tab The tab declaration.\n * @param {object|null} object The record.\n * @return {number|null} The count, or null when the tab declares none.\n * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-tab-counts-and-overflow\n */\nexport function resolveTabCount(tab, object) {\n\tif (!tab || typeof tab !== 'object') {\n\t\treturn null\n\t}\n\tif (typeof tab.count === 'number' && Number.isFinite(tab.count)) {\n\t\treturn tab.count\n\t}\n\tif (typeof tab.countField !== 'string' || tab.countField === '') {\n\t\tconst counts = object && typeof object === 'object' && object['@self'] && object['@self'].unreadCounts\n\t\tconst key = tab.unreadKey || tab.id || tab.widgetId\n\t\tconst unread = counts && typeof counts === 'object' && typeof key === 'string' ? Number(counts[key]) : 0\n\t\treturn Number.isFinite(unread) && unread > 0 ? unread : null\n\t}\n\tconst value = readPath(object && typeof object === 'object' ? object : {}, tab.countField)\n\tif (Array.isArray(value)) {\n\t\treturn value.length\n\t}\n\tif (value === null || value === undefined || value === '') {\n\t\treturn 0\n\t}\n\tconst number = Number(value)\n\treturn Number.isFinite(number) ? number : null\n}\n\n/**\n * The list a count pill counts (`countFrom`), or null for any other pill.\n *\n * @param {object|null} config The pill config.\n * @return {{register: string, schema: string, filter: object}|null} The list.\n * @spec openspec/changes/screens-relation-pages/specs/relation-pages/spec.md#requirement-a-header-pill-can-be-a-constant-or-a-count\n */\nexport function pillCountSource(config) {\n\tconst from = config && typeof config === 'object' && !config.field ? config.countFrom : null\n\tif (!from || typeof from !== 'object' || typeof from.register !== 'string' || from.register === '' || typeof from.schema !== 'string' || from.schema === '') {\n\t\treturn null\n\t}\n\tconst filter = from.filter && typeof from.filter === 'object' && !Array.isArray(from.filter) ? from.filter : {}\n\treturn { register: from.register, schema: from.schema, filter }\n}\n\n/**\n * A count pill once its count is known: \"1 lopende zaak\", \"2 lopende zaken\".\n * Nothing for a count of 0, an unknown count or a failed one.\n *\n * @param {{countLabel?: {one?: string, other?: string}, colorMap?: object, variant?: string}|null} config The pill config.\n * @param {number|null|undefined} count The count.\n * @param {(text: string) => string} [translate] Applied to the label before `{count}` is filled.\n * @return {{label: string, colorKey: string, colorMap: object|null, variant: string}|null} What CnStatusBadge needs, or null.\n * @spec openspec/changes/screens-relation-pages/specs/relation-pages/spec.md#requirement-a-header-pill-can-be-a-constant-or-a-count\n */\nexport function resolveCountPill(config, count, translate = (text) => text) {\n\tif (!pillCountSource(config) || typeof count !== 'number' || !Number.isFinite(count) || count <= 0) {\n\t\treturn null\n\t}\n\tconst labels = config.countLabel && typeof config.countLabel === 'object' ? config.countLabel : {}\n\tconst template = (count === 1 ? labels.one : labels.other) || labels.other || labels.one || '{count}'\n\tconst label = translate(template).replace(/\\{count\\}/g, String(count))\n\treturn {\n\t\tlabel,\n\t\tcolorKey: '',\n\t\tcolorMap: config.colorMap && typeof config.colorMap === 'object' ? config.colorMap : null,\n\t\tvariant: typeof config.variant === 'string' && config.variant !== '' ? config.variant : 'default',\n\t}\n}\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,114 @@
1
+ import { formatBoardDate, boardDateLocale } from './boardDate.js';
2
+ import { parseDateValue } from './dateVariant.js';
3
+ import { readPath } from './readPath.js';
4
+
5
+ // SPDX-FileCopyrightText: 2026 Conduction B.V.
6
+ // SPDX-License-Identifier: EUPL-1.2
7
+
8
+
9
+ const TOKEN = /\{([^{}\s|]+)(?:\|([a-z]+))?\}/g;
10
+
11
+ /**
12
+ * A moment in the board's short form with the time: "4 okt, 10:52", or
13
+ * "14 feb 2024, 09:00" outside the current year.
14
+ *
15
+ * @param {unknown} value A Date, a timestamp or an ISO string.
16
+ * @param {Date} [now] The reference moment.
17
+ * @return {string} The text, or '' when the value is not a date.
18
+ * @spec openspec/changes/screens-relation-pages/specs/relation-pages/spec.md#requirement-a-record-list-shows-the-objects-that-point-at-this-record
19
+ */
20
+ function formatBoardDateTime(value, now = new Date()) {
21
+ const day = formatBoardDate(value, now);
22
+ const date = parseDateValue(value);
23
+ if (day === '' || date === null) {
24
+ return ''
25
+ }
26
+ let time;
27
+ try {
28
+ time = new Intl.DateTimeFormat(boardDateLocale(), { hour: '2-digit', minute: '2-digit' }).format(date);
29
+ } catch {
30
+ time = new Intl.DateTimeFormat(undefined, { hour: '2-digit', minute: '2-digit' }).format(date);
31
+ }
32
+ return `${day}, ${time}`
33
+ }
34
+
35
+ /**
36
+ * Format one token value.
37
+ *
38
+ * @param {unknown} value The raw value.
39
+ * @param {string} format `date`, `datetime` or '' (as is).
40
+ * @param {Date} now The reference moment.
41
+ * @return {string} The text, '' when nothing can be shown.
42
+ */
43
+ function formatValue(value, format, now) {
44
+ if (value === null || value === undefined || value === '' || typeof value === 'object') {
45
+ return ''
46
+ }
47
+ if (format === 'date') {
48
+ return formatBoardDate(value, now)
49
+ }
50
+ if (format === 'datetime') {
51
+ return formatBoardDateTime(value, now)
52
+ }
53
+ return String(value)
54
+ }
55
+
56
+ /**
57
+ * Fill a row template such as `"{identifier} · {caseTypeName}"` or
58
+ * `"Termijn {deadline|date}"` from an object.
59
+ *
60
+ * Every `{path}` token is a dot path into the object; `{path|date}` and
61
+ * `{path|datetime}` format it as the boards write dates. A token without a
62
+ * value empties the WHOLE template, because "Termijn " is worse than nothing.
63
+ * A template without tokens is returned as written (a constant label).
64
+ *
65
+ * @param {string} template The template, already translated.
66
+ * @param {object|null} object The object to read.
67
+ * @param {Date} [now] The reference moment for dates.
68
+ * @return {string} The filled text, or ''.
69
+ * @spec openspec/changes/screens-relation-pages/specs/relation-pages/spec.md#requirement-a-record-list-shows-the-objects-that-point-at-this-record
70
+ */
71
+ function fillTemplate(template, object, now = new Date()) {
72
+ if (typeof template !== 'string' || template.trim() === '') {
73
+ return ''
74
+ }
75
+ let missing = false;
76
+ const text = template.replace(TOKEN, (_match, path, format) => {
77
+ const value = object && typeof object === 'object' ? readPath(object, path) : undefined;
78
+ const shown = formatValue(value, format || '', now);
79
+ if (shown === '') {
80
+ missing = true;
81
+ }
82
+ return shown
83
+ });
84
+ return missing ? '' : text.trim()
85
+ }
86
+
87
+ /**
88
+ * The first of several templates that fills: `["Klaar {closedAt|date}",
89
+ * "Termijn {deadline|date}"]` reads "Klaar ..." once the case is closed and
90
+ * "Termijn ..." while it is open. A single string is one candidate.
91
+ *
92
+ * @param {string|string[]} templates The candidates, in order.
93
+ * @param {object|null} object The object to read.
94
+ * @param {(key: string) => string} [translate] Applied to each template before filling.
95
+ * @param {Date} [now] The reference moment for dates.
96
+ * @return {string} The first filled text, or ''.
97
+ * @spec openspec/changes/screens-relation-pages/specs/relation-pages/spec.md#requirement-a-record-list-shows-the-objects-that-point-at-this-record
98
+ */
99
+ function firstFilled(templates, object, translate = (key) => key, now = new Date()) {
100
+ const list = Array.isArray(templates) ? templates : [templates];
101
+ for (const template of list) {
102
+ if (typeof template !== 'string') {
103
+ continue
104
+ }
105
+ const text = fillTemplate(translate(template), object, now);
106
+ if (text !== '') {
107
+ return text
108
+ }
109
+ }
110
+ return ''
111
+ }
112
+
113
+ export { fillTemplate, firstFilled, formatBoardDateTime };
114
+ //# sourceMappingURL=fillTemplate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fillTemplate.js","sources":["../../../src/utils/fillTemplate.js"],"sourcesContent":["// SPDX-FileCopyrightText: 2026 Conduction B.V.\n// SPDX-License-Identifier: EUPL-1.2\n\nimport { boardDateLocale, formatBoardDate } from './boardDate.js'\nimport { parseDateValue } from './dateVariant.js'\nimport { readPath } from './readPath.js'\n\nconst TOKEN = /\\{([^{}\\s|]+)(?:\\|([a-z]+))?\\}/g\n\n/**\n * A moment in the board's short form with the time: \"4 okt, 10:52\", or\n * \"14 feb 2024, 09:00\" outside the current year.\n *\n * @param {unknown} value A Date, a timestamp or an ISO string.\n * @param {Date} [now] The reference moment.\n * @return {string} The text, or '' when the value is not a date.\n * @spec openspec/changes/screens-relation-pages/specs/relation-pages/spec.md#requirement-a-record-list-shows-the-objects-that-point-at-this-record\n */\nexport function formatBoardDateTime(value, now = new Date()) {\n\tconst day = formatBoardDate(value, now)\n\tconst date = parseDateValue(value)\n\tif (day === '' || date === null) {\n\t\treturn ''\n\t}\n\tlet time\n\ttry {\n\t\ttime = new Intl.DateTimeFormat(boardDateLocale(), { hour: '2-digit', minute: '2-digit' }).format(date)\n\t} catch {\n\t\ttime = new Intl.DateTimeFormat(undefined, { hour: '2-digit', minute: '2-digit' }).format(date)\n\t}\n\treturn `${day}, ${time}`\n}\n\n/**\n * Format one token value.\n *\n * @param {unknown} value The raw value.\n * @param {string} format `date`, `datetime` or '' (as is).\n * @param {Date} now The reference moment.\n * @return {string} The text, '' when nothing can be shown.\n */\nfunction formatValue(value, format, now) {\n\tif (value === null || value === undefined || value === '' || typeof value === 'object') {\n\t\treturn ''\n\t}\n\tif (format === 'date') {\n\t\treturn formatBoardDate(value, now)\n\t}\n\tif (format === 'datetime') {\n\t\treturn formatBoardDateTime(value, now)\n\t}\n\treturn String(value)\n}\n\n/**\n * Fill a row template such as `\"{identifier} · {caseTypeName}\"` or\n * `\"Termijn {deadline|date}\"` from an object.\n *\n * Every `{path}` token is a dot path into the object; `{path|date}` and\n * `{path|datetime}` format it as the boards write dates. A token without a\n * value empties the WHOLE template, because \"Termijn \" is worse than nothing.\n * A template without tokens is returned as written (a constant label).\n *\n * @param {string} template The template, already translated.\n * @param {object|null} object The object to read.\n * @param {Date} [now] The reference moment for dates.\n * @return {string} The filled text, or ''.\n * @spec openspec/changes/screens-relation-pages/specs/relation-pages/spec.md#requirement-a-record-list-shows-the-objects-that-point-at-this-record\n */\nexport function fillTemplate(template, object, now = new Date()) {\n\tif (typeof template !== 'string' || template.trim() === '') {\n\t\treturn ''\n\t}\n\tlet missing = false\n\tconst text = template.replace(TOKEN, (_match, path, format) => {\n\t\tconst value = object && typeof object === 'object' ? readPath(object, path) : undefined\n\t\tconst shown = formatValue(value, format || '', now)\n\t\tif (shown === '') {\n\t\t\tmissing = true\n\t\t}\n\t\treturn shown\n\t})\n\treturn missing ? '' : text.trim()\n}\n\n/**\n * The first of several templates that fills: `[\"Klaar {closedAt|date}\",\n * \"Termijn {deadline|date}\"]` reads \"Klaar ...\" once the case is closed and\n * \"Termijn ...\" while it is open. A single string is one candidate.\n *\n * @param {string|string[]} templates The candidates, in order.\n * @param {object|null} object The object to read.\n * @param {(key: string) => string} [translate] Applied to each template before filling.\n * @param {Date} [now] The reference moment for dates.\n * @return {string} The first filled text, or ''.\n * @spec openspec/changes/screens-relation-pages/specs/relation-pages/spec.md#requirement-a-record-list-shows-the-objects-that-point-at-this-record\n */\nexport function firstFilled(templates, object, translate = (key) => key, now = new Date()) {\n\tconst list = Array.isArray(templates) ? templates : [templates]\n\tfor (const template of list) {\n\t\tif (typeof template !== 'string') {\n\t\t\tcontinue\n\t\t}\n\t\tconst text = fillTemplate(translate(template), object, now)\n\t\tif (text !== '') {\n\t\t\treturn text\n\t\t}\n\t}\n\treturn ''\n}\n"],"names":[],"mappings":"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"}
@@ -101,6 +101,8 @@ const DASHBOARD_CATALOG_WIDGET_KEYS = Object.freeze([
101
101
  'delta',
102
102
  'divider',
103
103
  'document-review',
104
+ 'facts',
105
+ 'field-list',
104
106
  'files',
105
107
  'flow-runs',
106
108
  'gauge',
@@ -124,8 +126,10 @@ const DASHBOARD_CATALOG_WIDGET_KEYS = Object.freeze([
124
126
  'object-table',
125
127
  'people',
126
128
  'quicklinks',
129
+ 'record-list',
127
130
  'related',
128
131
  'saved-view',
132
+ 'sections',
129
133
  'spend-analytics',
130
134
  'stacked-bar',
131
135
  'stages',
@@ -1 +1 @@
1
- {"version":3,"file":"libraryWidgetKeys.js","sources":["../../../src/utils/libraryWidgetKeys.js"],"sourcesContent":["/**\n * SPDX-FileCopyrightText: 2026 Conduction B.V.\n * SPDX-License-Identifier: EUPL-1.2\n *\n * libraryWidgetKeys — the one vocabulary of widget keys the LIBRARY itself\n * renders, and the only list `validateManifestV2` is allowed to consult.\n *\n * WHY THIS MODULE EXISTS\n * ----------------------\n * The validator used to carry its own hand-written array of \"library built-in\n * widget keys\", with a comment instructing whoever added a built-in to append\n * it \"in the same PR\". That instruction was followed for five keys and missed\n * for six, so the validator knew 11 keys while the library actually rendered\n * 45. Nothing failed when the two disagreed, because the only consumer is an\n * exemption: a key the validator does not recognise is treated as a CUSTOM\n * registry component, which makes a legitimate single-widget library dashboard\n * fail the ADR-036 Decision 1 anti-pattern rule with advice (\"declare it as\n * type:'custom'\") that is wrong for a library widget.\n *\n * A comment cannot enforce anything, so the array moved here and\n * `tests/utils/libraryWidgetKeys.spec.js` now binds it to the two registries\n * that actually resolve a key at runtime:\n *\n * 1. `BUILT_IN_WIDGETS` (components/CnWidgetGrid/builtInWidgets.js) — the v2\n * manifest widget set `CnWidgetGrid` resolves a `widgetKey` against.\n * 2. The dashboard widget catalog (`dashboardWidgetRegistry`), populated by\n * `registerDashboardWidgets.js`. `CnWidgetGrid` falls back to it (third in\n * the chain, after the consumer registry and `BUILT_IN_WIDGETS`), so a\n * catalog key renders on a v2 page exactly like a `BUILT_IN_WIDGETS` one.\n *\n * The test asserts SET EQUALITY in both directions against the live registries,\n * so adding a widget to either registry without touching this file fails CI,\n * and deleting one without removing it here fails too. The lists below are the\n * validator's view; the registries are the truth the test holds them to.\n *\n * THE CATALOG IS NOT LEGACY. It is a live half of the same vocabulary, and the\n * old array already conceded the point by exempting `chart` and `stats-block`,\n * which are catalog-only keys that `BUILT_IN_WIDGETS` has never held. What it\n * got wrong was doing that for two keys and not the other thirty-seven.\n * `widgetDispatch.js` unified the two resolution paths for the same reason: a\n * widget authored against one vocabulary was invisible to the other and said\n * nothing about why (101 of 236 detail widgets on hrmq).\n *\n * @module utils/libraryWidgetKeys\n */\n\nimport { WIDGET_TYPE_ALIASES } from './widgetTypeAliases.js'\n\n/**\n * Keys of `BUILT_IN_WIDGETS` — the v2 manifest widget set.\n *\n * Kept in the registry's own declaration order so a diff against\n * `builtInWidgets.js` reads straight down.\n *\n * @type {string[]} Frozen.\n */\nexport const BUILT_IN_WIDGET_KEYS = Object.freeze([\n\t'object-table',\n\t'form-renderer',\n\t'map-viewer',\n\t'object-geo',\n\t'card-grid',\n\t'nav-card-grid',\n\t'data',\n\t'metadata',\n\t'related',\n\t'integration',\n\t'banner',\n\t'audit-trail',\n\t'timeline',\n\t'header',\n\t'text',\n\t'divider',\n])\n\n/**\n * Widget types the LIBRARY registers into the dashboard catalog at import time\n * (`registerDashboardWidgets.js` and the widget `index.js` modules it pulls in).\n *\n * Deliberately a static list rather than a read of `dashboardWidgetRegistry`:\n * that object is mutable and consumer-extensible, so deriving the exemption\n * from it at call time would let an app register its own full-page component in\n * the catalog and walk straight through the ADR-036 Decision 1 rule the\n * exemption exists to enforce. Only keys the library itself ships are exempt.\n *\n * Sorted, because unlike `BUILT_IN_WIDGETS` these registrations are spread over\n * ~30 files and have no meaningful declaration order.\n *\n * @type {string[]} Frozen.\n */\nexport const DASHBOARD_CATALOG_WIDGET_KEYS = Object.freeze([\n\t'audit-trail',\n\t'banner',\n\t'calendar',\n\t'chart',\n\t'container',\n\t'conversation',\n\t'countdown',\n\t'data',\n\t'delta',\n\t'divider',\n\t'document-review',\n\t'files',\n\t'flow-runs',\n\t'gauge',\n\t'header',\n\t'image',\n\t'interaction-form',\n\t'kb-search',\n\t'label',\n\t'link',\n\t'links',\n\t'map',\n\t'markdown',\n\t'menu',\n\t'nc-widget',\n\t'next-step',\n\t'notepad',\n\t// `news` was here until the widget moved to LaunchPad, which owns the feed\n\t// fetching. An app that renders one now gets it from LaunchPad's registry.\n\t'object-geo',\n\t'object-list',\n\t'object-table',\n\t'people',\n\t'quicklinks',\n\t'related',\n\t'saved-view',\n\t'spend-analytics',\n\t'stacked-bar',\n\t'stages',\n\t'stat',\n\t'stats-block',\n\t'table',\n\t'tabs',\n\t'tasks',\n\t'text',\n\t'tile',\n\t'timeline',\n\t'video',\n\t'week-strip',\n\t'workspace-filter',\n])\n\n/**\n * Expand a key set over `WIDGET_TYPE_ALIASES` so both spellings of an aliased\n * widget are present whenever either one is.\n *\n * `CnWidgetGrid` resolves a key and its canonical form (`object-table` ⇄\n * `table`, `map-viewer` ⇄ `map`), so both spellings render and both must be\n * exempt. Today each registry happens to hold one side of each pair, which is\n * exactly the accident that would silently stop being true when the next alias\n * lands.\n *\n * @param {Set<string>} keys The key set to expand, mutated in place.\n * @return {Set<string>} The same set.\n */\nfunction expandAliases(keys) {\n\tfor (const [alias, canonical] of Object.entries(WIDGET_TYPE_ALIASES)) {\n\t\tif (keys.has(alias) || keys.has(canonical)) {\n\t\t\tkeys.add(alias)\n\t\t\tkeys.add(canonical)\n\t\t}\n\t}\n\treturn keys\n}\n\n/**\n * Every widget key the library renders itself: both registries, plus both\n * spellings of every alias.\n *\n * A `widgetKey` in this list is served by a library `Cn*` SFC and does NOT\n * count as a custom registry component for the single-12×12-widget dashboard\n * rule (ADR-036 Decision 1).\n *\n * @type {string[]} Frozen.\n */\nexport const LIBRARY_WIDGET_KEYS = Object.freeze([\n\t...expandAliases(new Set([\n\t\t...BUILT_IN_WIDGET_KEYS,\n\t\t...DASHBOARD_CATALOG_WIDGET_KEYS,\n\t])),\n])\n"],"names":[],"mappings":";;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACY,MAAC,oBAAoB,GAAG,MAAM,CAAC,MAAM,CAAC;AAClD,CAAC,cAAc;AACf,CAAC,eAAe;AAChB,CAAC,YAAY;AACb,CAAC,YAAY;AACb,CAAC,WAAW;AACZ,CAAC,eAAe;AAChB,CAAC,MAAM;AACP,CAAC,UAAU;AACX,CAAC,SAAS;AACV,CAAC,aAAa;AACd,CAAC,QAAQ;AACT,CAAC,aAAa;AACd,CAAC,UAAU;AACX,CAAC,QAAQ;AACT,CAAC,MAAM;AACP,CAAC,SAAS;AACV,CAAC,EAAC;AACF;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACY,MAAC,6BAA6B,GAAG,MAAM,CAAC,MAAM,CAAC;AAC3D,CAAC,aAAa;AACd,CAAC,QAAQ;AACT,CAAC,UAAU;AACX,CAAC,OAAO;AACR,CAAC,WAAW;AACZ,CAAC,cAAc;AACf,CAAC,WAAW;AACZ,CAAC,MAAM;AACP,CAAC,OAAO;AACR,CAAC,SAAS;AACV,CAAC,iBAAiB;AAClB,CAAC,OAAO;AACR,CAAC,WAAW;AACZ,CAAC,OAAO;AACR,CAAC,QAAQ;AACT,CAAC,OAAO;AACR,CAAC,kBAAkB;AACnB,CAAC,WAAW;AACZ,CAAC,OAAO;AACR,CAAC,MAAM;AACP,CAAC,OAAO;AACR,CAAC,KAAK;AACN,CAAC,UAAU;AACX,CAAC,MAAM;AACP,CAAC,WAAW;AACZ,CAAC,WAAW;AACZ,CAAC,SAAS;AACV;AACA;AACA,CAAC,YAAY;AACb,CAAC,aAAa;AACd,CAAC,cAAc;AACf,CAAC,QAAQ;AACT,CAAC,YAAY;AACb,CAAC,SAAS;AACV,CAAC,YAAY;AACb,CAAC,iBAAiB;AAClB,CAAC,aAAa;AACd,CAAC,QAAQ;AACT,CAAC,MAAM;AACP,CAAC,aAAa;AACd,CAAC,OAAO;AACR,CAAC,MAAM;AACP,CAAC,OAAO;AACR,CAAC,MAAM;AACP,CAAC,MAAM;AACP,CAAC,UAAU;AACX,CAAC,OAAO;AACR,CAAC,YAAY;AACb,CAAC,kBAAkB;AACnB,CAAC,EAAC;AACF;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,SAAS,aAAa,CAAC,IAAI,EAAE;AAC7B,CAAC,KAAK,MAAM,CAAC,KAAK,EAAE,SAAS,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,mBAAmB,CAAC,EAAE;AACvE,EAAE,IAAI,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE;AAC9C,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,EAAC;AAClB,GAAG,IAAI,CAAC,GAAG,CAAC,SAAS,EAAC;AACtB,GAAG;AACH,EAAE;AACF,CAAC,OAAO,IAAI;AACZ,CAAC;AACD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACY,MAAC,mBAAmB,GAAG,MAAM,CAAC,MAAM,CAAC;AACjD,CAAC,GAAG,aAAa,CAAC,IAAI,GAAG,CAAC;AAC1B,EAAE,GAAG,oBAAoB;AACzB,EAAE,GAAG,6BAA6B;AAClC,EAAE,CAAC,CAAC;AACJ,CAAC;;;;"}
1
+ {"version":3,"file":"libraryWidgetKeys.js","sources":["../../../src/utils/libraryWidgetKeys.js"],"sourcesContent":["/**\n * SPDX-FileCopyrightText: 2026 Conduction B.V.\n * SPDX-License-Identifier: EUPL-1.2\n *\n * libraryWidgetKeys — the one vocabulary of widget keys the LIBRARY itself\n * renders, and the only list `validateManifestV2` is allowed to consult.\n *\n * WHY THIS MODULE EXISTS\n * ----------------------\n * The validator used to carry its own hand-written array of \"library built-in\n * widget keys\", with a comment instructing whoever added a built-in to append\n * it \"in the same PR\". That instruction was followed for five keys and missed\n * for six, so the validator knew 11 keys while the library actually rendered\n * 45. Nothing failed when the two disagreed, because the only consumer is an\n * exemption: a key the validator does not recognise is treated as a CUSTOM\n * registry component, which makes a legitimate single-widget library dashboard\n * fail the ADR-036 Decision 1 anti-pattern rule with advice (\"declare it as\n * type:'custom'\") that is wrong for a library widget.\n *\n * A comment cannot enforce anything, so the array moved here and\n * `tests/utils/libraryWidgetKeys.spec.js` now binds it to the two registries\n * that actually resolve a key at runtime:\n *\n * 1. `BUILT_IN_WIDGETS` (components/CnWidgetGrid/builtInWidgets.js) — the v2\n * manifest widget set `CnWidgetGrid` resolves a `widgetKey` against.\n * 2. The dashboard widget catalog (`dashboardWidgetRegistry`), populated by\n * `registerDashboardWidgets.js`. `CnWidgetGrid` falls back to it (third in\n * the chain, after the consumer registry and `BUILT_IN_WIDGETS`), so a\n * catalog key renders on a v2 page exactly like a `BUILT_IN_WIDGETS` one.\n *\n * The test asserts SET EQUALITY in both directions against the live registries,\n * so adding a widget to either registry without touching this file fails CI,\n * and deleting one without removing it here fails too. The lists below are the\n * validator's view; the registries are the truth the test holds them to.\n *\n * THE CATALOG IS NOT LEGACY. It is a live half of the same vocabulary, and the\n * old array already conceded the point by exempting `chart` and `stats-block`,\n * which are catalog-only keys that `BUILT_IN_WIDGETS` has never held. What it\n * got wrong was doing that for two keys and not the other thirty-seven.\n * `widgetDispatch.js` unified the two resolution paths for the same reason: a\n * widget authored against one vocabulary was invisible to the other and said\n * nothing about why (101 of 236 detail widgets on hrmq).\n *\n * @module utils/libraryWidgetKeys\n */\n\nimport { WIDGET_TYPE_ALIASES } from './widgetTypeAliases.js'\n\n/**\n * Keys of `BUILT_IN_WIDGETS` — the v2 manifest widget set.\n *\n * Kept in the registry's own declaration order so a diff against\n * `builtInWidgets.js` reads straight down.\n *\n * @type {string[]} Frozen.\n */\nexport const BUILT_IN_WIDGET_KEYS = Object.freeze([\n\t'object-table',\n\t'form-renderer',\n\t'map-viewer',\n\t'object-geo',\n\t'card-grid',\n\t'nav-card-grid',\n\t'data',\n\t'metadata',\n\t'related',\n\t'integration',\n\t'banner',\n\t'audit-trail',\n\t'timeline',\n\t'header',\n\t'text',\n\t'divider',\n])\n\n/**\n * Widget types the LIBRARY registers into the dashboard catalog at import time\n * (`registerDashboardWidgets.js` and the widget `index.js` modules it pulls in).\n *\n * Deliberately a static list rather than a read of `dashboardWidgetRegistry`:\n * that object is mutable and consumer-extensible, so deriving the exemption\n * from it at call time would let an app register its own full-page component in\n * the catalog and walk straight through the ADR-036 Decision 1 rule the\n * exemption exists to enforce. Only keys the library itself ships are exempt.\n *\n * Sorted, because unlike `BUILT_IN_WIDGETS` these registrations are spread over\n * ~30 files and have no meaningful declaration order.\n *\n * @type {string[]} Frozen.\n */\nexport const DASHBOARD_CATALOG_WIDGET_KEYS = Object.freeze([\n\t'audit-trail',\n\t'banner',\n\t'calendar',\n\t'chart',\n\t'container',\n\t'conversation',\n\t'countdown',\n\t'data',\n\t'delta',\n\t'divider',\n\t'document-review',\n\t'facts',\n\t'field-list',\n\t'files',\n\t'flow-runs',\n\t'gauge',\n\t'header',\n\t'image',\n\t'interaction-form',\n\t'kb-search',\n\t'label',\n\t'link',\n\t'links',\n\t'map',\n\t'markdown',\n\t'menu',\n\t'nc-widget',\n\t'next-step',\n\t'notepad',\n\t// `news` was here until the widget moved to LaunchPad, which owns the feed\n\t// fetching. An app that renders one now gets it from LaunchPad's registry.\n\t'object-geo',\n\t'object-list',\n\t'object-table',\n\t'people',\n\t'quicklinks',\n\t'record-list',\n\t'related',\n\t'saved-view',\n\t'sections',\n\t'spend-analytics',\n\t'stacked-bar',\n\t'stages',\n\t'stat',\n\t'stats-block',\n\t'table',\n\t'tabs',\n\t'tasks',\n\t'text',\n\t'tile',\n\t'timeline',\n\t'video',\n\t'week-strip',\n\t'workspace-filter',\n])\n\n/**\n * Expand a key set over `WIDGET_TYPE_ALIASES` so both spellings of an aliased\n * widget are present whenever either one is.\n *\n * `CnWidgetGrid` resolves a key and its canonical form (`object-table` ⇄\n * `table`, `map-viewer` ⇄ `map`), so both spellings render and both must be\n * exempt. Today each registry happens to hold one side of each pair, which is\n * exactly the accident that would silently stop being true when the next alias\n * lands.\n *\n * @param {Set<string>} keys The key set to expand, mutated in place.\n * @return {Set<string>} The same set.\n */\nfunction expandAliases(keys) {\n\tfor (const [alias, canonical] of Object.entries(WIDGET_TYPE_ALIASES)) {\n\t\tif (keys.has(alias) || keys.has(canonical)) {\n\t\t\tkeys.add(alias)\n\t\t\tkeys.add(canonical)\n\t\t}\n\t}\n\treturn keys\n}\n\n/**\n * Every widget key the library renders itself: both registries, plus both\n * spellings of every alias.\n *\n * A `widgetKey` in this list is served by a library `Cn*` SFC and does NOT\n * count as a custom registry component for the single-12×12-widget dashboard\n * rule (ADR-036 Decision 1).\n *\n * @type {string[]} Frozen.\n */\nexport const LIBRARY_WIDGET_KEYS = Object.freeze([\n\t...expandAliases(new Set([\n\t\t...BUILT_IN_WIDGET_KEYS,\n\t\t...DASHBOARD_CATALOG_WIDGET_KEYS,\n\t])),\n])\n"],"names":[],"mappings":";;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACY,MAAC,oBAAoB,GAAG,MAAM,CAAC,MAAM,CAAC;AAClD,CAAC,cAAc;AACf,CAAC,eAAe;AAChB,CAAC,YAAY;AACb,CAAC,YAAY;AACb,CAAC,WAAW;AACZ,CAAC,eAAe;AAChB,CAAC,MAAM;AACP,CAAC,UAAU;AACX,CAAC,SAAS;AACV,CAAC,aAAa;AACd,CAAC,QAAQ;AACT,CAAC,aAAa;AACd,CAAC,UAAU;AACX,CAAC,QAAQ;AACT,CAAC,MAAM;AACP,CAAC,SAAS;AACV,CAAC,EAAC;AACF;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACY,MAAC,6BAA6B,GAAG,MAAM,CAAC,MAAM,CAAC;AAC3D,CAAC,aAAa;AACd,CAAC,QAAQ;AACT,CAAC,UAAU;AACX,CAAC,OAAO;AACR,CAAC,WAAW;AACZ,CAAC,cAAc;AACf,CAAC,WAAW;AACZ,CAAC,MAAM;AACP,CAAC,OAAO;AACR,CAAC,SAAS;AACV,CAAC,iBAAiB;AAClB,CAAC,OAAO;AACR,CAAC,YAAY;AACb,CAAC,OAAO;AACR,CAAC,WAAW;AACZ,CAAC,OAAO;AACR,CAAC,QAAQ;AACT,CAAC,OAAO;AACR,CAAC,kBAAkB;AACnB,CAAC,WAAW;AACZ,CAAC,OAAO;AACR,CAAC,MAAM;AACP,CAAC,OAAO;AACR,CAAC,KAAK;AACN,CAAC,UAAU;AACX,CAAC,MAAM;AACP,CAAC,WAAW;AACZ,CAAC,WAAW;AACZ,CAAC,SAAS;AACV;AACA;AACA,CAAC,YAAY;AACb,CAAC,aAAa;AACd,CAAC,cAAc;AACf,CAAC,QAAQ;AACT,CAAC,YAAY;AACb,CAAC,aAAa;AACd,CAAC,SAAS;AACV,CAAC,YAAY;AACb,CAAC,UAAU;AACX,CAAC,iBAAiB;AAClB,CAAC,aAAa;AACd,CAAC,QAAQ;AACT,CAAC,MAAM;AACP,CAAC,aAAa;AACd,CAAC,OAAO;AACR,CAAC,MAAM;AACP,CAAC,OAAO;AACR,CAAC,MAAM;AACP,CAAC,MAAM;AACP,CAAC,UAAU;AACX,CAAC,OAAO;AACR,CAAC,YAAY;AACb,CAAC,kBAAkB;AACnB,CAAC,EAAC;AACF;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,SAAS,aAAa,CAAC,IAAI,EAAE;AAC7B,CAAC,KAAK,MAAM,CAAC,KAAK,EAAE,SAAS,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,mBAAmB,CAAC,EAAE;AACvE,EAAE,IAAI,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE;AAC9C,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,EAAC;AAClB,GAAG,IAAI,CAAC,GAAG,CAAC,SAAS,EAAC;AACtB,GAAG;AACH,EAAE;AACF,CAAC,OAAO,IAAI;AACZ,CAAC;AACD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACY,MAAC,mBAAmB,GAAG,MAAM,CAAC,MAAM,CAAC;AACjD,CAAC,GAAG,aAAa,CAAC,IAAI,GAAG,CAAC;AAC1B,EAAE,GAAG,oBAAoB;AACzB,EAAE,GAAG,6BAA6B;AAClC,EAAE,CAAC,CAAC;AACJ,CAAC;;;;"}
@@ -0,0 +1,96 @@
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
+ 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
+ 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
+ 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
+ }
94
+
95
+ export { QUERY_PRESET_KEYS, applyQueryPreset, matchQueryPreset };
96
+ //# sourceMappingURL=queryPresets.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"queryPresets.js","sources":["../../../src/utils/queryPresets.js"],"sourcesContent":["/**\n * SPDX-FileCopyrightText: 2026 Conduction B.V.\n * SPDX-License-Identifier: EUPL-1.2\n *\n * Query presets of an index page (`config.queryPresets`): a menu entry that\n * deep-links to a list with a query (`{ caseType: '<uuid>' }`, the shape hydra\n * gate-68 asks for instead of a second page) can bring its own lenses,\n * columns and copy. The page names, per preset, the query it answers to and\n * the keys it replaces; the first preset whose every `match` pair is in the\n * current route query wins.\n *\n * @module utils/queryPresets\n * @spec openspec/changes/screens-index-query-presets/specs/index-query-presets/spec.md#requirement-a-query-preset-overlays-lenses-columns-and-copy\n */\n\n/**\n * The config keys a preset may replace. Anything else in a preset is ignored:\n * a preset changes what the list shows, never which register or schema it\n * reads or what it may write.\n *\n * @type {ReadonlyArray<string>}\n */\nexport const QUERY_PRESET_KEYS = Object.freeze([\n\t'title',\n\t'quickFilters',\n\t'columns',\n\t'cardFields',\n\t'countText',\n\t'searchPlaceholder',\n\t'footerNote',\n\t'defaultSort',\n\t'viewSwitch',\n])\n\n/**\n * Whether one route query value satisfies one `match` value. A repeated query\n * parameter (an array) matches when it holds the value.\n *\n * @param {unknown} actual The route query value.\n * @param {string|number|boolean} expected The preset's value.\n * @return {boolean} True when they match.\n */\nfunction queryValueMatches(actual, expected) {\n\tconst want = String(expected)\n\tif (Array.isArray(actual)) {\n\t\treturn actual.some((v) => String(v) === want)\n\t}\n\treturn actual !== undefined && actual !== null && String(actual) === want\n}\n\n/**\n * The index of the first preset whose every `match` pair is in the query.\n * A preset without a non-empty `match` never matches.\n *\n * @param {Array<object>} presets The page's `queryPresets`.\n * @param {object} query The current route query.\n * @return {number} The preset index, or -1.\n */\nexport function matchQueryPreset(presets, query) {\n\tif (!Array.isArray(presets)) {\n\t\treturn -1\n\t}\n\tconst q = query && typeof query === 'object' ? query : {}\n\treturn presets.findIndex((preset) => {\n\t\tconst match = preset && typeof preset.match === 'object' && preset.match !== null ? preset.match : null\n\t\tif (!match || Object.keys(match).length === 0) {\n\t\t\treturn false\n\t\t}\n\t\treturn Object.entries(match).every(([key, value]) => queryValueMatches(q[key], value))\n\t})\n}\n\n/**\n * The page config with the preset's allowed keys laid over it, and without\n * `queryPresets` itself (the page component has no such prop).\n *\n * @param {object} config The page config.\n * @param {object|null} preset The matched preset, or null.\n * @return {object} The effective config.\n */\nexport function applyQueryPreset(config, preset) {\n\tconst { queryPresets, ...rest } = config || {}\n\tif (!preset || typeof preset !== 'object') {\n\t\treturn rest\n\t}\n\tconst out = { ...rest }\n\tfor (const key of QUERY_PRESET_KEYS) {\n\t\tif (key !== 'title' && preset[key] !== undefined) {\n\t\t\tout[key] = preset[key]\n\t\t}\n\t}\n\treturn out\n}\n"],"names":[],"mappings":"AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACY,MAAC,iBAAiB,GAAG,MAAM,CAAC,MAAM,CAAC;AAC/C,CAAC,OAAO;AACR,CAAC,cAAc;AACf,CAAC,SAAS;AACV,CAAC,YAAY;AACb,CAAC,WAAW;AACZ,CAAC,mBAAmB;AACpB,CAAC,YAAY;AACb,CAAC,aAAa;AACd,CAAC,YAAY;AACb,CAAC,EAAC;AACF;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,SAAS,iBAAiB,CAAC,MAAM,EAAE,QAAQ,EAAE;AAC7C,CAAC,MAAM,IAAI,GAAG,MAAM,CAAC,QAAQ,EAAC;AAC9B,CAAC,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE;AAC5B,EAAE,OAAO,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,MAAM,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC;AAC/C,EAAE;AACF,CAAC,OAAO,MAAM,KAAK,SAAS,IAAI,MAAM,KAAK,IAAI,IAAI,MAAM,CAAC,MAAM,CAAC,KAAK,IAAI;AAC1E,CAAC;AACD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAAS,gBAAgB,CAAC,OAAO,EAAE,KAAK,EAAE;AACjD,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE;AAC9B,EAAE,OAAO,CAAC,CAAC;AACX,EAAE;AACF,CAAC,MAAM,CAAC,GAAG,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,GAAG,KAAK,GAAG,GAAE;AAC1D,CAAC,OAAO,OAAO,CAAC,SAAS,CAAC,CAAC,MAAM,KAAK;AACtC,EAAE,MAAM,KAAK,GAAG,MAAM,IAAI,OAAO,MAAM,CAAC,KAAK,KAAK,QAAQ,IAAI,MAAM,CAAC,KAAK,KAAK,IAAI,GAAG,MAAM,CAAC,KAAK,GAAG,KAAI;AACzG,EAAE,IAAI,CAAC,KAAK,IAAI,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,MAAM,KAAK,CAAC,EAAE;AACjD,GAAG,OAAO,KAAK;AACf,GAAG;AACH,EAAE,OAAO,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,KAAK,iBAAiB,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,KAAK,CAAC,CAAC;AACxF,EAAE,CAAC;AACH,CAAC;AACD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAAS,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE;AACjD,CAAC,MAAM,EAAE,YAAY,EAAE,GAAG,IAAI,EAAE,GAAG,MAAM,IAAI,GAAE;AAC/C,CAAC,IAAI,CAAC,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ,EAAE;AAC5C,EAAE,OAAO,IAAI;AACb,EAAE;AACF,CAAC,MAAM,GAAG,GAAG,EAAE,GAAG,IAAI,GAAE;AACxB,CAAC,KAAK,MAAM,GAAG,IAAI,iBAAiB,EAAE;AACtC,EAAE,IAAI,GAAG,KAAK,OAAO,IAAI,MAAM,CAAC,GAAG,CAAC,KAAK,SAAS,EAAE;AACpD,GAAG,GAAG,CAAC,GAAG,CAAC,GAAG,MAAM,CAAC,GAAG,EAAC;AACzB,GAAG;AACH,EAAE;AACF,CAAC,OAAO,GAAG;AACX;;;;"}
@@ -26,6 +26,10 @@ import { peekCurrentUserGroups } from './widgetVisibility.js';
26
26
  * - `@today±Nd` → relative-date arithmetic at day granularity: N days
27
27
  * from today (e.g. `@today-30d`, `@today+7d`)
28
28
  * - `@monthStart` → first day of the current month, `YYYY-MM-DD`
29
+ * - `@monthEnd` → last day of the current month, `YYYY-MM-DD`; on a
30
+ * date-time field use `lt: '@nextMonthStart'` instead,
31
+ * since `lte` a date stops at that day's midnight
32
+ * - `@nextMonthStart` → first day of the next month, `YYYY-MM-DD`
29
33
  * - `@quarterStart` → first day of the current quarter, `YYYY-MM-DD`
30
34
  * - `@yearStart` → first day of the current year, `YYYY-MM-DD`
31
35
  * - `@currentFiscalYear` → the current calendar year as a number string, e.g. `2026`
@@ -215,6 +219,13 @@ function resolveFilterValue(v, ctx) {
215
219
  if (v === '@monthStart') {
216
220
  return ymd(new Date(now.getFullYear(), now.getMonth(), 1))
217
221
  }
222
+ if (v === '@monthEnd') {
223
+ // Day 0 of the next month is the last day of this one.
224
+ return ymd(new Date(now.getFullYear(), now.getMonth() + 1, 0))
225
+ }
226
+ if (v === '@nextMonthStart') {
227
+ return ymd(new Date(now.getFullYear(), now.getMonth() + 1, 1))
228
+ }
218
229
  if (v === '@quarterStart') {
219
230
  return ymd(new Date(now.getFullYear(), Math.floor(now.getMonth() / 3) * 3, 1))
220
231
  }