@conduction/nextcloud-vue 2.62.0 → 2.64.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 (209) hide show
  1. package/dist/esm/components/CnAdminActionCard/CnAdminActionCard.vue.js +10 -0
  2. package/dist/esm/components/CnAdminActionCard/CnAdminActionCard.vue.js.map +1 -0
  3. package/dist/esm/components/CnAdminActionCard/CnAdminActionCard.vue2.js +189 -0
  4. package/dist/esm/components/CnAdminActionCard/CnAdminActionCard.vue2.js.map +1 -0
  5. package/dist/esm/components/CnAdminActionCard/CnAdminActionCard.vue3.js +72 -0
  6. package/dist/esm/components/CnAdminActionCard/CnAdminActionCard.vue3.js.map +1 -0
  7. package/dist/esm/components/CnAppNav/CnAppNav.vue2.js +425 -39
  8. package/dist/esm/components/CnAppNav/CnAppNav.vue2.js.map +1 -1
  9. package/dist/esm/components/CnAppNav/CnAppNav.vue3.js +119 -42
  10. package/dist/esm/components/CnAppNav/CnAppNav.vue3.js.map +1 -1
  11. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js +186 -9
  12. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
  13. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js +65 -42
  14. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js.map +1 -1
  15. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue2.js +61 -16
  16. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue2.js.map +1 -1
  17. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue3.js.map +1 -1
  18. package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue2.js +20 -0
  19. package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue2.js.map +1 -1
  20. package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue3.js +3 -2
  21. package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue3.js.map +1 -1
  22. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js +33 -2
  23. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js.map +1 -1
  24. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js.map +1 -1
  25. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue2.js.map +1 -1
  26. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue3.js +53 -37
  27. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue3.js.map +1 -1
  28. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +29 -0
  29. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  30. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +152 -135
  31. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  32. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue.js +10 -0
  33. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue.js.map +1 -0
  34. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue2.js +293 -0
  35. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue2.js.map +1 -0
  36. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue3.js +189 -0
  37. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue3.js.map +1 -0
  38. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +253 -9
  39. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
  40. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +126 -50
  41. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
  42. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +66 -14
  43. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  44. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +23 -14
  45. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  46. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
  47. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js +2 -1
  48. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
  49. package/dist/esm/components/CnFieldHelper/CnFieldHelper.vue2.js +3 -1
  50. package/dist/esm/components/CnFieldHelper/CnFieldHelper.vue2.js.map +1 -1
  51. package/dist/esm/components/CnFieldHelper/CnFieldHelper.vue3.js +1 -1
  52. package/dist/esm/components/CnFieldHelper/CnFieldHelper.vue3.js.map +1 -1
  53. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js +234 -9
  54. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
  55. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js +96 -57
  56. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
  57. package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue2.js +81 -4
  58. package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue2.js.map +1 -1
  59. package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue3.js +22 -5
  60. package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue3.js.map +1 -1
  61. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +81 -2
  62. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  63. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +7 -3
  64. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  65. package/dist/esm/components/CnIndexPage/selfModeActions.js +3 -3
  66. package/dist/esm/components/CnIndexPage/selfModeActions.js.map +1 -1
  67. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +41 -2
  68. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
  69. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js +8 -3
  70. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js.map +1 -1
  71. package/dist/esm/components/CnResourceSelect/CnResourceSelect.vue2.js +121 -12
  72. package/dist/esm/components/CnResourceSelect/CnResourceSelect.vue2.js.map +1 -1
  73. package/dist/esm/components/CnResourceSelect/CnResourceSelect.vue3.js +3 -1
  74. package/dist/esm/components/CnResourceSelect/CnResourceSelect.vue3.js.map +1 -1
  75. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js +326 -1
  76. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js.map +1 -1
  77. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue3.js +112 -26
  78. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue3.js.map +1 -1
  79. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue2.js +63 -2
  80. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue2.js.map +1 -1
  81. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue3.js +96 -37
  82. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue3.js.map +1 -1
  83. package/dist/esm/components/CnStagesWidget/stagesModel.js +5 -2
  84. package/dist/esm/components/CnStagesWidget/stagesModel.js.map +1 -1
  85. package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js +52 -2
  86. package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js.map +1 -1
  87. package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js +6 -6
  88. package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js.map +1 -1
  89. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +14 -2
  90. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
  91. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js +2 -1
  92. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
  93. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue.js +10 -0
  94. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue.js.map +1 -0
  95. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue2.js +337 -0
  96. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue2.js.map +1 -0
  97. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue3.js +102 -0
  98. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue3.js.map +1 -0
  99. package/dist/esm/components/CnTimelineWidget/dashboardRegistration.js +34 -0
  100. package/dist/esm/components/CnTimelineWidget/dashboardRegistration.js.map +1 -0
  101. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue2.js +131 -8
  102. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue2.js.map +1 -1
  103. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue3.js +40 -38
  104. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue3.js.map +1 -1
  105. package/dist/esm/components/CnWidgetGrid/builtInWidgets.js +8 -6
  106. package/dist/esm/components/CnWidgetGrid/builtInWidgets.js.map +1 -1
  107. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js +1 -0
  108. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js.map +1 -1
  109. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue2.js +73 -1
  110. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue2.js.map +1 -1
  111. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue3.js +14 -2
  112. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue3.js.map +1 -1
  113. package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue2.js +19 -0
  114. package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue2.js.map +1 -1
  115. package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue3.js +1 -1
  116. package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue3.js.map +1 -1
  117. package/dist/esm/composables/useDependencyCheck.js +65 -0
  118. package/dist/esm/composables/useDependencyCheck.js.map +1 -0
  119. package/dist/esm/composables/useWalkthrough.js +218 -6
  120. package/dist/esm/composables/useWalkthrough.js.map +1 -1
  121. package/dist/esm/index.js +2 -0
  122. package/dist/esm/index.js.map +1 -1
  123. package/dist/esm/l10n/en.json.js +45 -1
  124. package/dist/esm/l10n/en.json.js.map +1 -1
  125. package/dist/esm/l10n/nl.json.js +45 -1
  126. package/dist/esm/l10n/nl.json.js.map +1 -1
  127. package/dist/esm/nextcloud-vue.css +780 -81
  128. package/dist/esm/store/useObjectStore.js +8 -0
  129. package/dist/esm/store/useObjectStore.js.map +1 -1
  130. package/dist/esm/utils/columnFilters.js +311 -0
  131. package/dist/esm/utils/columnFilters.js.map +1 -0
  132. package/dist/esm/utils/groupAutocomplete.js +123 -0
  133. package/dist/esm/utils/groupAutocomplete.js.map +1 -0
  134. package/dist/esm/utils/libraryWidgetKeys.js +2 -0
  135. package/dist/esm/utils/libraryWidgetKeys.js.map +1 -1
  136. package/dist/esm/utils/pickerOptions.js +198 -0
  137. package/dist/esm/utils/pickerOptions.js.map +1 -0
  138. package/dist/esm/utils/schema.js +83 -7
  139. package/dist/esm/utils/schema.js.map +1 -1
  140. package/dist/esm/utils/schemaRefSlug.js +19 -3
  141. package/dist/esm/utils/schemaRefSlug.js.map +1 -1
  142. package/dist/esm/utils/timelineEvents.js +174 -0
  143. package/dist/esm/utils/timelineEvents.js.map +1 -0
  144. package/dist/esm/utils/validateManifestV2.compiled.js +2 -2
  145. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  146. package/dist/esm/utils/walkthroughSignals.js +88 -0
  147. package/dist/esm/utils/walkthroughSignals.js.map +1 -0
  148. package/dist/nextcloud-vue.cjs.js +13088 -7937
  149. package/dist/nextcloud-vue.cjs.js.map +2 -2
  150. package/dist/nextcloud-vue.css +780 -81
  151. package/l10n/en.json +45 -0
  152. package/l10n/nl.json +45 -0
  153. package/package.json +1 -1
  154. package/src/components/CnAdminActionCard/CnAdminActionCard.vue +234 -0
  155. package/src/components/CnAdminActionCard/index.js +4 -0
  156. package/src/components/CnAppNav/CnAppNav.vue +568 -43
  157. package/src/components/CnAppRoot/CnAppRoot.md +4 -0
  158. package/src/components/CnAppRoot/CnAppRoot.vue +223 -16
  159. package/src/components/CnAuditTrailCard/CnAuditTrailCard.vue +61 -16
  160. package/src/components/CnAuditTrailWidget/CnAuditTrailWidget.vue +22 -1
  161. package/src/components/CnCellRenderer/CnCellRenderer.vue +33 -2
  162. package/src/components/CnChoiceCards/CnChoiceCards.vue +56 -31
  163. package/src/components/CnDashboardPage/CnDashboardPage.vue +45 -4
  164. package/src/components/CnDataTable/CnColumnFilterPopover.vue +524 -0
  165. package/src/components/CnDataTable/CnDataTable.md +3 -0
  166. package/src/components/CnDataTable/CnDataTable.vue +305 -5
  167. package/src/components/CnDetailPage/CnDetailPage.md +4 -0
  168. package/src/components/CnDetailPage/CnDetailPage.vue +60 -1
  169. package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +2 -2
  170. package/src/components/CnFieldHelper/CnFieldHelper.vue +4 -2
  171. package/src/components/CnFormDialog/CnFormDialog.vue +290 -31
  172. package/src/components/CnHeaderWidget/CnHeaderWidget.vue +116 -6
  173. package/src/components/CnIndexPage/CnIndexPage.vue +87 -4
  174. package/src/components/CnIndexPage/selfModeActions.js +3 -3
  175. package/src/components/CnObjectListWidget/CnObjectListWidget.vue +46 -2
  176. package/src/components/CnResourceSelect/CnResourceSelect.vue +123 -12
  177. package/src/components/CnSetupWizard/CnSetupWizard.vue +392 -3
  178. package/src/components/CnStagesWidget/CnStagesWidget.vue +180 -2
  179. package/src/components/CnStagesWidget/stagesModel.js +5 -2
  180. package/src/components/CnStatWidget/CnStatWidget.vue +57 -3
  181. package/src/components/CnTabsWidget/CnTabsWidget.vue +15 -2
  182. package/src/components/CnTimelineWidget/CnTimelineWidget.vue +477 -0
  183. package/src/components/CnTimelineWidget/dashboardRegistration.js +32 -0
  184. package/src/components/CnTimelineWidget/index.js +4 -0
  185. package/src/components/CnWalkthrough/CnWalkthrough.vue +143 -10
  186. package/src/components/CnWidgetGrid/builtInWidgets.js +2 -0
  187. package/src/components/CnWidgetGrid/registerDashboardWidgets.js +1 -0
  188. package/src/components/CnWidgetWrapper/CnWidgetWrapper.md +4 -0
  189. package/src/components/CnWidgetWrapper/CnWidgetWrapper.vue +107 -1
  190. package/src/components/CnWizardDialog/CnWizardDialog.vue +20 -1
  191. package/src/components/index.js +2 -0
  192. package/src/composables/useDependencyCheck.js +61 -0
  193. package/src/composables/useWalkthrough.js +217 -5
  194. package/src/css/dashboard.css +49 -0
  195. package/src/css/detail-page.css +29 -4
  196. package/src/css/kpi-card.css +24 -1
  197. package/src/css/table.css +99 -6
  198. package/src/index.js +2 -0
  199. package/src/schemas/app-manifest-v2.schema.json +135 -3
  200. package/src/store/useObjectStore.js +8 -0
  201. package/src/utils/columnFilters.js +324 -0
  202. package/src/utils/groupAutocomplete.js +119 -0
  203. package/src/utils/libraryWidgetKeys.js +2 -0
  204. package/src/utils/pickerOptions.js +194 -0
  205. package/src/utils/schema.js +83 -7
  206. package/src/utils/schemaRefSlug.js +19 -3
  207. package/src/utils/timelineEvents.js +171 -0
  208. package/src/utils/validateManifestV2.compiled.js +2 -2
  209. package/src/utils/walkthroughSignals.js +94 -0
@@ -3,7 +3,7 @@
3
3
  "$id": "https://raw.githubusercontent.com/ConductionNL/nextcloud-vue/main/src/schemas/app-manifest-v2.schema.json",
4
4
  "title": "Conduction App Manifest v2",
5
5
  "description": "v2 schema for the JSON-driven page and navigation manifest consumed by @conduction/nextcloud-vue. Introduces a uniform widgets[] array on every page type with a per-slot grid coordinate system, a typed actions[] discriminator, and the required $schema field for version detection. v1 manifests continue to validate against app-manifest.schema.json (unchanged).",
6
- "version": "2.43.0",
6
+ "version": "2.46.0",
7
7
  "type": "object",
8
8
  "required": ["$schema", "version"],
9
9
  "allOf": [
@@ -97,6 +97,8 @@
97
97
  "suggestFrom": { "type": "string", "description": "choice: the configKey of an earlier choice this step pre-fills a suggestion from. When that value is set and the user has not yet picked here, the option matching suggestMap[parentValue] is auto-selected (still user-overridable)." },
98
98
  "suggestMap": { "type": "object", "description": "choice: map of suggestFrom value → suggested option value. Used to auto-suggest a default (e.g. RGS template suggested from organisation type).", "additionalProperties": { "type": "string" } },
99
99
  "multiple": { "type": "boolean", "default": false, "description": "choice: allow multi-select." },
100
+ "loadAction": { "type": "string", "minLength": 1, "description": "choice with display cards: an actionId each option card loads itself with. Every card except the one whose value is `none` gets its own Load button, which POSTs { dataset: <value> } to /api/setup/action/{loadAction}, spins while it runs and shows the result on that card. A successful load also selects the card. With loadAction the separate run-action step that loaded the picked dataset is no longer needed; manifests that keep it still work." },
101
+ "requires": { "type": "array", "items": { "type": "string", "minLength": 1 }, "uniqueItems": true, "description": "App ids this step needs. When any of them is not installed and enabled, the wizard skips the step and the summary shows it as skipped, naming the missing apps. Do not combine with required: true; setup status would keep reporting the skipped step as unmet." },
100
102
  "action": { "type": "string", "description": "run-action: actionId POSTed to /api/setup/action/{action}." },
101
103
  "onDemand": { "type": "boolean", "default": false, "description": "run-action: the step runs only when the user clicks Run, never automatically, and is never outstanding work (it does not open the wizard or decide where a resumed wizard starts). The summary ticks it only when its action succeeded in the current session and shows it as not run otherwise, whatever the server reports. For destructive steps an app reports as done on purpose so they never auto-run, such as removing example data." },
102
104
  "healthCheck": { "type": "boolean", "default": false, "description": "summary: render a health recap." },
@@ -236,6 +238,40 @@
236
238
  "type": "string",
237
239
  "description": "Override label for the settings foldout's gear button. Defaults to 'Settings'."
238
240
  },
241
+ "card": {
242
+ "type": "object",
243
+ "additionalProperties": false,
244
+ "required": ["title"],
245
+ "description": "A card above the footer entries, rendered by CnAppNav (zuiddrecht-pixel-gaps): a title, a line of text and one link. Title, text and link label are i18n keys. Omitted, nothing renders there.",
246
+ "properties": {
247
+ "title": { "type": "string", "minLength": 1 },
248
+ "text": { "type": "string" },
249
+ "link": {
250
+ "type": "object",
251
+ "additionalProperties": false,
252
+ "required": ["label"],
253
+ "description": "The card's link: a `route` (named page, optional `params`) is a router link, an `href` a plain anchor, an `action` id a button that emits `card-action`.",
254
+ "properties": {
255
+ "label": { "type": "string", "minLength": 1 },
256
+ "route": { "type": "string" },
257
+ "params": { "type": "object", "additionalProperties": true },
258
+ "href": { "type": "string" },
259
+ "action": { "type": "string" }
260
+ }
261
+ }
262
+ }
263
+ },
264
+ "help": {
265
+ "type": "object",
266
+ "additionalProperties": false,
267
+ "required": ["label"],
268
+ "description": "A help entry with a help icon in the navigation footer, above the settings foldout (zuiddrecht-pixel-gaps): `route` (named page) or `href` (opens in a new tab).",
269
+ "properties": {
270
+ "label": { "type": "string", "minLength": 1 },
271
+ "route": { "type": "string" },
272
+ "href": { "type": "string" }
273
+ }
274
+ },
239
275
  "primaryAction": {
240
276
  "$ref": "#/$defs/primaryAction",
241
277
  "description": "App-wide default primary action rendered as an NcAppNavigationNew button above the main menu list. A page-scoped pages[].primaryAction for the current route wins over this default."
@@ -249,6 +285,10 @@
249
285
  "type": "string",
250
286
  "description": "URL of the logo image. Decorative when `name` is set."
251
287
  },
288
+ "emblem": {
289
+ "type": ["string", "boolean"],
290
+ "description": "An emblem drawn in place of the logo (zuiddrecht-pixel-gaps): the municipality's mark beside the app name, distinct from the wordmark in the top bar. A URL, or `true` to draw the theme's emblem from the `--nldesign-emblem-url` custom property. Decorative when `name` is set."
291
+ },
252
292
  "name": {
253
293
  "type": "string",
254
294
  "description": "i18n key / text for the app or organisation name."
@@ -1156,6 +1196,23 @@
1156
1196
  },
1157
1197
  "payload": {
1158
1198
  "description": "Free-form payload echoed back to the host inside the @primary-action-click event."
1199
+ },
1200
+ "action": {
1201
+ "$ref": "#/$defs/action",
1202
+ "description": "A page action to run on click (zuiddrecht-pixel-gaps): CnAppNav then renders the button through CnActionButtons, so `type: \"open-form\"` (with `register` + `schema`) opens the create dialog without a create page, `navigate` pushes a route, `api-call` calls an endpoint. Drawn as one solid full-width primary button. `route` / `href` on the primary action itself are ignored while `action` is set."
1203
+ },
1204
+ "solid": {
1205
+ "type": "boolean",
1206
+ "default": false,
1207
+ "description": "Draw the primary action as a solid, full-width primary button whatever its form (zuiddrecht-pixel-gaps). Default: a link action is already a primary button; a plain one is NcAppNavigationNew."
1208
+ },
1209
+ "permission": {
1210
+ "type": "string",
1211
+ "description": "Permission the reader must hold for the action to render, checked like `menuItem.permission` (zuiddrecht-pixel-gaps)."
1212
+ },
1213
+ "visibleIf": {
1214
+ "$ref": "#/$defs/visibleIfCondition",
1215
+ "description": "Display condition evaluated at render time, like `menuItem.visibleIf` (zuiddrecht-pixel-gaps). A gated-out page-scoped action falls back to `nav.primaryAction`."
1159
1216
  }
1160
1217
  }
1161
1218
  },
@@ -1173,6 +1230,11 @@
1173
1230
  "type": "string",
1174
1231
  "description": "i18n translation key resolved by the consuming app's t() function at render time."
1175
1232
  },
1233
+ "translateLabel": {
1234
+ "type": "boolean",
1235
+ "default": true,
1236
+ "description": "Whether CnAppNav runs `label` through the translate function. Set false for a user-authored title (e.g. a catalog name) that must render as written even when it equals a translation key such as 'Search'. Rendered as text either way, never as HTML."
1237
+ },
1176
1238
  "icon": {
1177
1239
  "type": "string",
1178
1240
  "description": "CSS class for the icon (e.g. 'icon-checkmark') or MDI component name."
@@ -1186,6 +1248,11 @@
1186
1248
  "additionalProperties": { "type": ["string", "number", "boolean"] },
1187
1249
  "description": "Optional vue-router query parameters merged into the target route link (e.g. a filter like { caseType: '<uuid>' }). Deep-links a nav entry to a pre-filtered index page."
1188
1250
  },
1251
+ "params": {
1252
+ "type": "object",
1253
+ "additionalProperties": { "type": ["string", "number"], "minLength": 1 },
1254
+ "description": "Optional vue-router route params for a parameterised route (e.g. { catalogSlug: 'news' } for a page at '/catalogs/:catalogSlug'). String values must be non-empty. CnAppNav links to { name: route, params, query }. The entry is active only while every declared param equals the current route's param (compared as strings), so sibling entries on one route that differ in params are never marked together. On pages whose path sits below its page path with these params filled in, it is active too: by the same params check when those routes carry the same param names, otherwise as the one entry marked below its route."
1255
+ },
1189
1256
  "order": {
1190
1257
  "type": "integer",
1191
1258
  "description": "Display order in the menu. Items without an order render last."
@@ -1209,9 +1276,20 @@
1209
1276
  "count": {
1210
1277
  "oneOf": [
1211
1278
  { "type": "integer", "minimum": 0 },
1212
- { "type": "string", "enum": ["auto"] }
1279
+ { "type": "string", "enum": ["auto"] },
1280
+ {
1281
+ "type": "object",
1282
+ "additionalProperties": false,
1283
+ "required": ["register", "schema"],
1284
+ "description": "A FILTERED count (zuiddrecht-pixel-gaps): the total of this register and schema with `filter` applied (filter-value tokens such as `@me` and `@today` resolve at fetch time), fetched by CnAppRoot under the entry's own key so an index page's whole-schema total is untouched. Renders \"My work 6\" beside \"All cases 48\".",
1285
+ "properties": {
1286
+ "register": { "type": "string", "minLength": 1 },
1287
+ "schema": { "type": "string", "minLength": 1 },
1288
+ "filter": { "type": "object", "additionalProperties": true }
1289
+ }
1290
+ }
1213
1291
  ],
1214
- "description": "Counter badge in the entry's #counter slot. Positive integer renders as-is; 'auto' resolves from cnMenuCounts (populated by CnAppRoot from useObjectStore totals) for the entry's resolved index-type page (register + schema)."
1292
+ "description": "Counter badge in the entry's #counter slot. Positive integer renders as-is; 'auto' resolves from cnMenuCounts (populated by CnAppRoot from useObjectStore totals) for the entry's resolved index-type page (register + schema); an object `{ register, schema, filter? }` is the total of that filtered list."
1215
1293
  },
1216
1294
  "pinned": {
1217
1295
  "type": "boolean",
@@ -1259,6 +1337,11 @@
1259
1337
  "properties": {
1260
1338
  "id": { "type": "string" },
1261
1339
  "label": { "type": "string" },
1340
+ "translateLabel": {
1341
+ "type": "boolean",
1342
+ "default": true,
1343
+ "description": "Same as menuItem.translateLabel: false renders `label` as written instead of translating it."
1344
+ },
1262
1345
  "icon": { "type": "string" },
1263
1346
  "route": { "type": "string" },
1264
1347
  "query": {
@@ -1266,6 +1349,11 @@
1266
1349
  "additionalProperties": { "type": ["string", "number", "boolean"] },
1267
1350
  "description": "Optional vue-router query parameters merged into the target route link (e.g. a filter like { caseType: '<uuid>' }). Deep-links a nav entry to a pre-filtered index page."
1268
1351
  },
1352
+ "params": {
1353
+ "type": "object",
1354
+ "additionalProperties": { "type": ["string", "number"], "minLength": 1 },
1355
+ "description": "Optional vue-router route params for a parameterised route; string values must be non-empty. Same contract as menuItem.params: part of the link, and the entry is active only while every declared param equals the current route's param."
1356
+ },
1269
1357
  "order": { "type": "integer" },
1270
1358
  "permission": { "type": "string" },
1271
1359
  "section": {
@@ -1463,6 +1551,19 @@
1463
1551
  "minimum": 1,
1464
1552
  "description": "Row span (>= 1)."
1465
1553
  },
1554
+ "headerLink": {
1555
+ "type": "object",
1556
+ "additionalProperties": false,
1557
+ "required": ["label"],
1558
+ "description": "One text link in the widget header's actions group, before the overflow menu (zuiddrecht-pixel-gaps): \"All deadlines\", \"To the board\". A `route` (named page, optional `params` / `query`) is a router link, an `href` a plain anchor. The label is an i18n key. Rendered by CnWidgetWrapper; CnDashboardPage reads it off the layout entry or the widget definition.",
1559
+ "properties": {
1560
+ "label": { "type": "string", "minLength": 1 },
1561
+ "route": { "type": "string" },
1562
+ "params": { "type": "object", "additionalProperties": true },
1563
+ "query": { "type": "object", "additionalProperties": true },
1564
+ "href": { "type": "string" }
1565
+ }
1566
+ },
1466
1567
  "sizeToContent": {
1467
1568
  "type": ["boolean", "integer"],
1468
1569
  "minimum": 1,
@@ -3497,6 +3598,37 @@
3497
3598
  "type": "string",
3498
3599
  "description": "type='index': accessible name and tooltip for the split pane's close button. Defaults to \"Close\". Maps to CnIndexPage `splitCloseLabel`."
3499
3600
  },
3601
+ "showHeader": {
3602
+ "type": "boolean",
3603
+ "default": true,
3604
+ "description": "type='dashboard': `false` drops the header row (title, description, header actions, edit toggle) for a page that opens with its own heading, such as a greeting widget; the title stays as a visually hidden heading (zuiddrecht-pixel-gaps). Maps to CnDashboardPage `showHeader`."
3605
+ },
3606
+ "showTitle": {
3607
+ "type": "boolean",
3608
+ "default": false,
3609
+ "description": "type='index': show the page header (icon, title, description) inline. Default false: the title renders visually hidden and the sidebar names the page. Maps to CnIndexPage `showTitle`."
3610
+ },
3611
+ "countSubtitle": {
3612
+ "type": "string",
3613
+ "description": "type='index': an i18n key for the description line with `{total}` replaced by the collection's total, e.g. \"{total} open cases\" (zuiddrecht-pixel-gaps). Shown in place of `description` once a total is known. Maps to CnIndexPage `countSubtitle`."
3614
+ },
3615
+ "showTypeEyebrow": {
3616
+ "type": "boolean",
3617
+ "default": true,
3618
+ "description": "type='detail': `false` drops the type eyebrow (the type label above the record name) once the record resolves, for a header that says the type in a pill instead (zuiddrecht-pixel-gaps). Maps to CnDetailPage `showTypeEyebrow`."
3619
+ },
3620
+ "breadcrumb": {
3621
+ "type": "object",
3622
+ "additionalProperties": false,
3623
+ "required": ["label"],
3624
+ "description": "type='detail': a breadcrumb line above the header (zuiddrecht-pixel-gaps): this crumb names the list the record belongs to (`route` + optional `params`, or `href`), the record's display name follows as the current crumb. Maps to CnDetailPage `breadcrumb`.",
3625
+ "properties": {
3626
+ "label": { "type": "string", "minLength": 1 },
3627
+ "route": { "type": "string" },
3628
+ "params": { "type": "object", "additionalProperties": true },
3629
+ "href": { "type": "string" }
3630
+ }
3631
+ },
3500
3632
  "mapConfig": {
3501
3633
  "type": "object",
3502
3634
  "additionalProperties": true,
@@ -5,6 +5,7 @@ import { genericError, networkError, parseResponseError } from '../utils/errors.
5
5
  import { normalizeFacets } from '../utils/facets.js'
6
6
  import { buildHeaders, buildQueryString, capitalize, prefixUrl } from '../utils/headers.js'
7
7
  import { extractId } from '../utils/id.js'
8
+ import { dispatchObjectCreated } from '../utils/walkthroughSignals.js'
8
9
  import { mergePluginActions, mergePluginGetters, mergePluginState } from './pluginMerge.js'
9
10
  import { liveUpdatesPlugin } from './plugins/liveUpdates.js'
10
11
 
@@ -830,6 +831,13 @@ const baseActions = {
830
831
  [type]: { ...(this.objects[type] || {}), [savedId]: data },
831
832
  }
832
833
 
834
+ // Every create through the store lets an `advanceOn:
835
+ // object-created` walkthrough step advance, whichever form made it.
836
+ if (!isUpdate) {
837
+ const config = this.objectTypeRegistry[type] || {}
838
+ dispatchObjectCreated({ register: config.registerSlug || config.register, schema: config.schemaSlug || config.schema, object: data })
839
+ }
840
+
833
841
  return data
834
842
  } catch (error) {
835
843
  this.errors = {
@@ -0,0 +1,324 @@
1
+ /**
2
+ * SPDX-FileCopyrightText: 2026 Conduction B.V.
3
+ * SPDX-License-Identifier: EUPL-1.2
4
+ */
5
+
6
+ /**
7
+ * Per-column sorting and filtering for table headers.
8
+ *
9
+ * Pure helpers: which column can sort, which can filter and how, and the
10
+ * translation between a header filter's state and the OpenRegister query
11
+ * parameters the facet sidebar already sends. A header filter writes into the
12
+ * SAME active-filter map (`{ paramKey: values[] }`) the sidebar uses, so the
13
+ * fetch, the route query and saved views need nothing new.
14
+ *
15
+ * Parameter shapes, as OpenRegister reads them:
16
+ * - equality and "any of": `key=value`, `key[]=a&key[]=b`
17
+ * - ranges: `key[gte]=from`, `key[lte]=to`
18
+ *
19
+ * @module utils/columnFilters
20
+ * @spec openspec/changes/table-header-sort-and-filter/specs/cn-data-table/spec.md
21
+ */
22
+
23
+ import { schemaRefSlug } from './schemaRefSlug.js'
24
+
25
+ /** Column keys that mark a value computed on the client, never stored. */
26
+ const COMPUTED_MARKERS = ['aggregate', 'compute', 'computed', 'virtual']
27
+
28
+ /**
29
+ * Normalise a `$ref` to the referenced schema slug, or null.
30
+ *
31
+ * @param {unknown} ref A `$ref` value.
32
+ * @return {string|number|null} The slug.
33
+ */
34
+ function refSlug(ref) {
35
+ if (typeof ref === 'number' && !Number.isNaN(ref)) {
36
+ return ref
37
+ }
38
+ if (typeof ref !== 'string' || ref === '') {
39
+ return null
40
+ }
41
+ const tail = ref.includes('/') ? ref.substring(ref.lastIndexOf('/') + 1) : ref
42
+ const slug = tail === '' ? '' : schemaRefSlug(tail)
43
+ return slug !== '' ? slug : null
44
+ }
45
+
46
+ /**
47
+ * The schema property behind a column, or null when the column has none.
48
+ *
49
+ * @param {object} column The column definition.
50
+ * @param {object|null} schema The table's schema.
51
+ * @return {object|null} The property.
52
+ */
53
+ export function columnProperty(column, schema) {
54
+ if (!column || !schema || !schema.properties || typeof column.key !== 'string') {
55
+ return null
56
+ }
57
+ const prop = schema.properties[column.key]
58
+ return (prop && typeof prop === 'object') ? prop : null
59
+ }
60
+
61
+ /**
62
+ * Whether a column computes its value on the client, so the server cannot
63
+ * sort or filter on it.
64
+ *
65
+ * @param {object} column The column definition.
66
+ * @return {boolean} True for a computed column.
67
+ */
68
+ function isComputedColumn(column) {
69
+ return COMPUTED_MARKERS.some((marker) => column[marker] !== undefined && column[marker] !== null && column[marker] !== false)
70
+ }
71
+
72
+ /**
73
+ * Whether a column header sorts.
74
+ *
75
+ * `sortable: false` always wins, and so does an explicit `sortable: true`.
76
+ * Otherwise, with a schema, a column sorts when a stored schema property backs
77
+ * it: OpenRegister orders on schema properties, so a widget or computed column
78
+ * with no property behind it stays unsortable. Without a schema nothing says
79
+ * the server can sort, so only an explicit flag makes the column sortable,
80
+ * which is how such tables behaved before.
81
+ *
82
+ * @param {object} column The column definition.
83
+ * @param {object|null} schema The table's schema.
84
+ * @return {boolean} True when the header sorts.
85
+ * @spec openspec/changes/table-header-sort-and-filter/specs/cn-data-table/spec.md#requirement-every-backed-column-sorts-by-default
86
+ */
87
+ export function isColumnSortable(column, schema) {
88
+ if (!column || typeof column !== 'object') {
89
+ return false
90
+ }
91
+ if (column.sortable === false) {
92
+ return false
93
+ }
94
+ if (column.sortable === true) {
95
+ return true
96
+ }
97
+ if (!schema || isComputedColumn(column)) {
98
+ return false
99
+ }
100
+ const prop = columnProperty(column, schema)
101
+ return !!prop && prop.type !== 'object'
102
+ }
103
+
104
+ /**
105
+ * Display options for an enum filter: schema `enum` (or `items.enum`) with
106
+ * `x-enum-labels`, else a column `enum` hint, else a badge column's colour
107
+ * map keys.
108
+ *
109
+ * @param {object} column The column definition.
110
+ * @param {object|null} prop The schema property.
111
+ * @return {Array<{value: string, label: string}>} The options, or [].
112
+ */
113
+ function enumOptions(column, prop) {
114
+ const p = prop || {}
115
+ const labels = p.enumLabels || p['x-enum-labels'] || (p.items && (p.items.enumLabels || p.items['x-enum-labels'])) || {}
116
+ let values = null
117
+ if (Array.isArray(p.enum) && p.enum.length > 0) {
118
+ values = p.enum
119
+ } else if (p.items && Array.isArray(p.items.enum) && p.items.enum.length > 0) {
120
+ values = p.items.enum
121
+ } else if (Array.isArray(column.enum) && column.enum.length > 0) {
122
+ values = column.enum
123
+ } else if (column.widget === 'badge' && column.widgetProps && column.widgetProps.colorMap) {
124
+ values = Object.keys(column.widgetProps.colorMap)
125
+ }
126
+ if (!values) {
127
+ return []
128
+ }
129
+ return values
130
+ .filter((v) => v !== null && v !== undefined && v !== '')
131
+ .map((v) => ({ value: String(v), label: String(labels[v] !== undefined ? labels[v] : v) }))
132
+ }
133
+
134
+ /**
135
+ * The reference a column points at, for a searchable object filter: a schema
136
+ * `$ref` (or `items.$ref`), else an `fkResolve` cell widget's config.
137
+ *
138
+ * @param {object} column The column definition.
139
+ * @param {object|null} prop The schema property.
140
+ * @return {{schema: (string|number), register: string, labelField: string}|null} The reference.
141
+ */
142
+ function referenceTarget(column, prop) {
143
+ const widgetProps = (column.widget === 'fkResolve' && column.widgetProps) || null
144
+ const p = prop || {}
145
+ const schemaRef = refSlug(p.$ref) ?? (p.items ? refSlug(p.items.$ref) : null)
146
+ const schema = schemaRef ?? (widgetProps && widgetProps.schema ? widgetProps.schema : null)
147
+ if (schema === null || schema === undefined || schema === '') {
148
+ return null
149
+ }
150
+ const register = (widgetProps && widgetProps.register)
151
+ || p['x-external-register']
152
+ || (p.items && p.items['x-external-register'])
153
+ || ''
154
+ const labelField = (widgetProps && widgetProps.labelField) || ''
155
+ return { schema, register, labelField }
156
+ }
157
+
158
+ /**
159
+ * How a column filters, or null when it does not.
160
+ *
161
+ * Kinds: `enum` (checkbox list), `boolean` (yes, no, any), `reference`
162
+ * (searchable list of the referenced objects), `number` and `date` (from and
163
+ * to), and `string` (equals). `filterable: false` on the column turns it
164
+ * off. A column with no schema property behind it only filters when it
165
+ * carries an enum hint (`enum`, or a badge widget's colour map), because the
166
+ * server cannot filter on a value it does not store.
167
+ *
168
+ * @param {object} column The column definition.
169
+ * @param {object|null} schema The table's schema.
170
+ * @return {{key: string, kind: string, label: string, options: Array<{value: string, label: string}>, reference: object|null, dateTime: boolean}|null} The filter definition.
171
+ * @spec openspec/changes/table-header-sort-and-filter/specs/cn-data-table/spec.md#requirement-every-backed-column-filters-from-its-header
172
+ */
173
+ export function columnFilterDef(column, schema) {
174
+ if (!column || typeof column !== 'object' || typeof column.key !== 'string' || column.key === '') {
175
+ return null
176
+ }
177
+ if (column.filterable === false || isComputedColumn(column)) {
178
+ return null
179
+ }
180
+ const prop = columnProperty(column, schema)
181
+ const base = { key: column.key, label: column.label || (prop && prop.title) || column.key, options: [], reference: null, dateTime: false }
182
+
183
+ const options = enumOptions(column, prop)
184
+ if (options.length > 0) {
185
+ return { ...base, kind: 'enum', options }
186
+ }
187
+ if (!prop) {
188
+ return null
189
+ }
190
+ const reference = referenceTarget(column, prop)
191
+ if (reference) {
192
+ return { ...base, kind: 'reference', reference }
193
+ }
194
+ const type = Array.isArray(prop.type) ? prop.type.find((t) => t !== 'null') : prop.type
195
+ const format = prop.format || ''
196
+ if (type === 'boolean') {
197
+ return { ...base, kind: 'boolean' }
198
+ }
199
+ if (type === 'number' || type === 'integer') {
200
+ return { ...base, kind: 'number' }
201
+ }
202
+ if (type === 'string' && (format === 'date' || format === 'date-time')) {
203
+ return { ...base, kind: 'date', dateTime: format === 'date-time' }
204
+ }
205
+ if (type === 'string') {
206
+ return { ...base, kind: 'string' }
207
+ }
208
+ return null
209
+ }
210
+
211
+ /**
212
+ * Read one active-filter entry as a list of strings.
213
+ *
214
+ * @param {unknown} raw An active-filter value: an array, a scalar, or absent.
215
+ * @return {Array<string>} The values.
216
+ */
217
+ function asList(raw) {
218
+ if (raw === undefined || raw === null || raw === '') {
219
+ return []
220
+ }
221
+ return (Array.isArray(raw) ? raw : [raw]).filter((v) => v !== undefined && v !== null && v !== '').map((v) => String(v))
222
+ }
223
+
224
+ /**
225
+ * The parameter keys one column filter owns.
226
+ *
227
+ * @param {object} def A filter definition from `columnFilterDef`.
228
+ * @return {Array<string>} The keys.
229
+ */
230
+ export function columnFilterParamKeys(def) {
231
+ if (!def) {
232
+ return []
233
+ }
234
+ if (def.kind === 'number' || def.kind === 'date') {
235
+ return [`${def.key}[gte]`, `${def.key}[lte]`]
236
+ }
237
+ return [def.key]
238
+ }
239
+
240
+ /**
241
+ * A column filter's current state, read from the active-filter map.
242
+ *
243
+ * @param {object} def A filter definition.
244
+ * @param {object} activeFilters The active-filter map (`{ paramKey: values }`).
245
+ * @return {object} `{ values }` for enum and reference, `{ value }` for boolean and string, `{ from, to }` for number and date.
246
+ */
247
+ export function columnFilterState(def, activeFilters) {
248
+ const filters = (activeFilters && typeof activeFilters === 'object') ? activeFilters : {}
249
+ if (!def) {
250
+ return {}
251
+ }
252
+ if (def.kind === 'enum' || def.kind === 'reference') {
253
+ return { values: asList(filters[def.key]) }
254
+ }
255
+ if (def.kind === 'number' || def.kind === 'date') {
256
+ const from = asList(filters[`${def.key}[gte]`])[0] || ''
257
+ const to = asList(filters[`${def.key}[lte]`])[0] || ''
258
+ // A date-time column is queried up to the end of the day; the input
259
+ // shows the date the person picked.
260
+ return {
261
+ from: def.kind === 'date' ? from.slice(0, 10) : from,
262
+ to: def.kind === 'date' ? to.slice(0, 10) : to,
263
+ }
264
+ }
265
+ return { value: asList(filters[def.key])[0] || '' }
266
+ }
267
+
268
+ /**
269
+ * Whether a column filter is active.
270
+ *
271
+ * @param {object} def A filter definition.
272
+ * @param {object} activeFilters The active-filter map.
273
+ * @return {boolean} True when any of its parameters is set.
274
+ */
275
+ export function isColumnFilterActive(def, activeFilters) {
276
+ const filters = (activeFilters && typeof activeFilters === 'object') ? activeFilters : {}
277
+ return columnFilterParamKeys(def).some((key) => asList(filters[key]).length > 0)
278
+ }
279
+
280
+ /**
281
+ * The active-filter entries a column filter state produces. Every key the
282
+ * column owns is present; an empty list clears that key, so changing a range
283
+ * from "from and to" to "from" drops the old upper bound.
284
+ *
285
+ * @param {object} def A filter definition.
286
+ * @param {object} state The column filter state (see `columnFilterState`).
287
+ * @return {object} `{ paramKey: values[] }`.
288
+ * @spec openspec/changes/table-header-sort-and-filter/specs/cn-data-table/spec.md#requirement-a-header-filter-speaks-the-sidebars-query-language
289
+ */
290
+ export function columnFilterParams(def, state) {
291
+ const s = state || {}
292
+ if (!def) {
293
+ return {}
294
+ }
295
+ if (def.kind === 'enum' || def.kind === 'reference') {
296
+ return { [def.key]: asList(s.values) }
297
+ }
298
+ if (def.kind === 'number' || def.kind === 'date') {
299
+ const from = s.from === undefined || s.from === null ? '' : String(s.from).trim()
300
+ let to = s.to === undefined || s.to === null ? '' : String(s.to).trim()
301
+ if (def.kind === 'date' && def.dateTime && to !== '' && to.length === 10) {
302
+ to = `${to}T23:59:59`
303
+ }
304
+ return {
305
+ [`${def.key}[gte]`]: from === '' ? [] : [from],
306
+ [`${def.key}[lte]`]: to === '' ? [] : [to],
307
+ }
308
+ }
309
+ if (def.kind === 'boolean') {
310
+ return { [def.key]: (s.value === 'true' || s.value === 'false') ? [s.value] : [] }
311
+ }
312
+ const value = s.value === undefined || s.value === null ? '' : String(s.value).trim()
313
+ return { [def.key]: value === '' ? [] : [value] }
314
+ }
315
+
316
+ /**
317
+ * Clear-all entries for one column: every key it owns, emptied.
318
+ *
319
+ * @param {object} def A filter definition.
320
+ * @return {object} `{ paramKey: [] }`.
321
+ */
322
+ export function clearedColumnFilterParams(def) {
323
+ return Object.fromEntries(columnFilterParamKeys(def).map((key) => [key, []]))
324
+ }