@conduction/nextcloud-vue 2.62.0 → 2.63.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 (154) 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 +271 -32
  8. package/dist/esm/components/CnAppNav/CnAppNav.vue2.js.map +1 -1
  9. package/dist/esm/components/CnAppNav/CnAppNav.vue3.js.map +1 -1
  10. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js +99 -9
  11. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
  12. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js +39 -18
  13. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js.map +1 -1
  14. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue2.js.map +1 -1
  15. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue3.js +53 -37
  16. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue3.js.map +1 -1
  17. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  18. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +2 -2
  19. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  20. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue.js +10 -0
  21. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue.js.map +1 -0
  22. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue2.js +293 -0
  23. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue2.js.map +1 -0
  24. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue3.js +189 -0
  25. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue3.js.map +1 -0
  26. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +216 -8
  27. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
  28. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +118 -50
  29. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
  30. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
  31. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js +2 -1
  32. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
  33. package/dist/esm/components/CnFieldHelper/CnFieldHelper.vue2.js +3 -1
  34. package/dist/esm/components/CnFieldHelper/CnFieldHelper.vue2.js.map +1 -1
  35. package/dist/esm/components/CnFieldHelper/CnFieldHelper.vue3.js +1 -1
  36. package/dist/esm/components/CnFieldHelper/CnFieldHelper.vue3.js.map +1 -1
  37. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js +234 -9
  38. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
  39. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js +96 -57
  40. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
  41. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +51 -2
  42. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  43. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +5 -1
  44. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  45. package/dist/esm/components/CnIndexPage/selfModeActions.js +3 -3
  46. package/dist/esm/components/CnIndexPage/selfModeActions.js.map +1 -1
  47. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +36 -1
  48. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
  49. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js +4 -1
  50. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js.map +1 -1
  51. package/dist/esm/components/CnResourceSelect/CnResourceSelect.vue2.js +121 -12
  52. package/dist/esm/components/CnResourceSelect/CnResourceSelect.vue2.js.map +1 -1
  53. package/dist/esm/components/CnResourceSelect/CnResourceSelect.vue3.js +3 -1
  54. package/dist/esm/components/CnResourceSelect/CnResourceSelect.vue3.js.map +1 -1
  55. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js +326 -1
  56. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js.map +1 -1
  57. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue3.js +112 -26
  58. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue3.js.map +1 -1
  59. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue.js +10 -0
  60. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue.js.map +1 -0
  61. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue2.js +337 -0
  62. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue2.js.map +1 -0
  63. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue3.js +102 -0
  64. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue3.js.map +1 -0
  65. package/dist/esm/components/CnTimelineWidget/dashboardRegistration.js +34 -0
  66. package/dist/esm/components/CnTimelineWidget/dashboardRegistration.js.map +1 -0
  67. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue2.js +131 -8
  68. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue2.js.map +1 -1
  69. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue3.js +40 -38
  70. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue3.js.map +1 -1
  71. package/dist/esm/components/CnWidgetGrid/builtInWidgets.js +8 -6
  72. package/dist/esm/components/CnWidgetGrid/builtInWidgets.js.map +1 -1
  73. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js +1 -0
  74. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js.map +1 -1
  75. package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue2.js +19 -0
  76. package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue2.js.map +1 -1
  77. package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue3.js +1 -1
  78. package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue3.js.map +1 -1
  79. package/dist/esm/composables/useDependencyCheck.js +65 -0
  80. package/dist/esm/composables/useDependencyCheck.js.map +1 -0
  81. package/dist/esm/composables/useWalkthrough.js +218 -6
  82. package/dist/esm/composables/useWalkthrough.js.map +1 -1
  83. package/dist/esm/index.js +2 -0
  84. package/dist/esm/index.js.map +1 -1
  85. package/dist/esm/l10n/en.json.js +43 -0
  86. package/dist/esm/l10n/en.json.js.map +1 -1
  87. package/dist/esm/l10n/nl.json.js +43 -0
  88. package/dist/esm/l10n/nl.json.js.map +1 -1
  89. package/dist/esm/nextcloud-vue.css +449 -0
  90. package/dist/esm/store/useObjectStore.js +8 -0
  91. package/dist/esm/store/useObjectStore.js.map +1 -1
  92. package/dist/esm/utils/columnFilters.js +311 -0
  93. package/dist/esm/utils/columnFilters.js.map +1 -0
  94. package/dist/esm/utils/groupAutocomplete.js +123 -0
  95. package/dist/esm/utils/groupAutocomplete.js.map +1 -0
  96. package/dist/esm/utils/libraryWidgetKeys.js +2 -0
  97. package/dist/esm/utils/libraryWidgetKeys.js.map +1 -1
  98. package/dist/esm/utils/pickerOptions.js +198 -0
  99. package/dist/esm/utils/pickerOptions.js.map +1 -0
  100. package/dist/esm/utils/schema.js +83 -7
  101. package/dist/esm/utils/schema.js.map +1 -1
  102. package/dist/esm/utils/timelineEvents.js +174 -0
  103. package/dist/esm/utils/timelineEvents.js.map +1 -0
  104. package/dist/esm/utils/validateManifestV2.compiled.js +2 -2
  105. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  106. package/dist/esm/utils/walkthroughSignals.js +88 -0
  107. package/dist/esm/utils/walkthroughSignals.js.map +1 -0
  108. package/dist/nextcloud-vue.cjs.js +9369 -5193
  109. package/dist/nextcloud-vue.cjs.js.map +2 -2
  110. package/dist/nextcloud-vue.css +449 -0
  111. package/l10n/en.json +43 -0
  112. package/l10n/nl.json +43 -0
  113. package/package.json +1 -1
  114. package/src/components/CnAdminActionCard/CnAdminActionCard.vue +234 -0
  115. package/src/components/CnAdminActionCard/index.js +4 -0
  116. package/src/components/CnAppNav/CnAppNav.vue +271 -32
  117. package/src/components/CnAppRoot/CnAppRoot.vue +129 -15
  118. package/src/components/CnChoiceCards/CnChoiceCards.vue +56 -31
  119. package/src/components/CnDashboardPage/CnDashboardPage.vue +2 -2
  120. package/src/components/CnDataTable/CnColumnFilterPopover.vue +524 -0
  121. package/src/components/CnDataTable/CnDataTable.md +3 -0
  122. package/src/components/CnDataTable/CnDataTable.vue +260 -4
  123. package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +2 -2
  124. package/src/components/CnFieldHelper/CnFieldHelper.vue +4 -2
  125. package/src/components/CnFormDialog/CnFormDialog.vue +290 -31
  126. package/src/components/CnIndexPage/CnIndexPage.vue +55 -2
  127. package/src/components/CnIndexPage/selfModeActions.js +3 -3
  128. package/src/components/CnObjectListWidget/CnObjectListWidget.vue +39 -1
  129. package/src/components/CnResourceSelect/CnResourceSelect.vue +123 -12
  130. package/src/components/CnSetupWizard/CnSetupWizard.vue +392 -3
  131. package/src/components/CnTimelineWidget/CnTimelineWidget.vue +477 -0
  132. package/src/components/CnTimelineWidget/dashboardRegistration.js +32 -0
  133. package/src/components/CnTimelineWidget/index.js +4 -0
  134. package/src/components/CnWalkthrough/CnWalkthrough.vue +143 -10
  135. package/src/components/CnWidgetGrid/builtInWidgets.js +2 -0
  136. package/src/components/CnWidgetGrid/registerDashboardWidgets.js +1 -0
  137. package/src/components/CnWizardDialog/CnWizardDialog.vue +20 -1
  138. package/src/components/index.js +2 -0
  139. package/src/composables/useDependencyCheck.js +61 -0
  140. package/src/composables/useWalkthrough.js +217 -5
  141. package/src/css/dashboard.css +49 -0
  142. package/src/css/detail-page.css +19 -0
  143. package/src/css/table.css +81 -0
  144. package/src/index.js +2 -0
  145. package/src/schemas/app-manifest-v2.schema.json +23 -1
  146. package/src/store/useObjectStore.js +8 -0
  147. package/src/utils/columnFilters.js +324 -0
  148. package/src/utils/groupAutocomplete.js +119 -0
  149. package/src/utils/libraryWidgetKeys.js +2 -0
  150. package/src/utils/pickerOptions.js +194 -0
  151. package/src/utils/schema.js +83 -7
  152. package/src/utils/timelineEvents.js +171 -0
  153. package/src/utils/validateManifestV2.compiled.js +2 -2
  154. package/src/utils/walkthroughSignals.js +94 -0
@@ -80,3 +80,52 @@
80
80
  .cn-dashboard-list__item:hover {
81
81
  background: var(--color-background-hover);
82
82
  }
83
+
84
+ /* "Install <app>" placeholder in a widget whose app is missing.
85
+ *
86
+ * NcEmptyContent centres its content with `justify-content: center`. In a
87
+ * two-row dashboard tile (about 150px inside an `overflow: hidden` wrapper)
88
+ * the icon, name, description and button are taller than the tile, and plain
89
+ * `center` overflows on BOTH sides, so the top ran out of the widget and the
90
+ * name was cut off. `safe center` falls back to the start edge when the
91
+ * content does not fit, the spacing is compact, and what still does not fit
92
+ * scrolls instead of being clipped. Shared by CnDashboardPage and
93
+ * CnDetailWidgetHost (`cn-requires-app`). The class is doubled to outrank
94
+ * NcEmptyContent's own scoped rules (`.empty-content[data-v-…]`) whatever
95
+ * order the stylesheets load in. */
96
+ .cn-requires-app.cn-requires-app.empty-content,
97
+ .cn-requires-app.cn-requires-app .empty-content {
98
+ justify-content: safe center;
99
+ min-height: 0;
100
+ max-height: 100%;
101
+ overflow: auto;
102
+ margin: 0;
103
+ padding: calc(2 * var(--default-grid-baseline, 4px));
104
+ gap: var(--default-grid-baseline, 4px);
105
+ box-sizing: border-box;
106
+ }
107
+
108
+ .cn-requires-app.cn-requires-app .empty-content__icon {
109
+ width: auto;
110
+ height: auto;
111
+ min-height: 0;
112
+ margin: 0;
113
+ opacity: 0.7;
114
+ }
115
+
116
+ .cn-requires-app.cn-requires-app .empty-content__name {
117
+ margin: 0;
118
+ font-size: var(--default-font-size, 15px);
119
+ font-weight: 600;
120
+ text-align: center;
121
+ }
122
+
123
+ .cn-requires-app.cn-requires-app .empty-content__description {
124
+ margin: 0;
125
+ text-align: center;
126
+ font-size: 0.9em;
127
+ }
128
+
129
+ .cn-requires-app.cn-requires-app .empty-content__action {
130
+ margin-top: var(--default-grid-baseline, 4px);
131
+ }
@@ -106,6 +106,25 @@
106
106
  flex-shrink: 0;
107
107
  }
108
108
 
109
+ /* Every control in the header sits on one line at one height. The lifecycle
110
+ bar carries `margin: 0 0 8px` for its place above a page body; in the
111
+ header that margin pushed its buttons up and made the row 8px taller, so
112
+ the Edit button beside it read as the bigger one. A consumer's actions
113
+ component can bring its own margins too, so they are reset here as well. */
114
+ .cn-detail-page .cn-detail-page__header-actions .cn-lifecycle-actions {
115
+ margin: 0;
116
+ flex-wrap: nowrap;
117
+ }
118
+
119
+ .cn-detail-page .cn-detail-page__header-actions > * {
120
+ margin-block: 0;
121
+ }
122
+
123
+ .cn-detail-page .cn-detail-page__header-actions .button-vue {
124
+ min-height: var(--default-clickable-area);
125
+ block-size: var(--default-clickable-area);
126
+ }
127
+
109
128
  /* The sentence a refused lock acquire left behind, under the locked banner. */
110
129
  .cn-detail-page__lock-refusal {
111
130
  margin: 0 0 calc(3 * var(--default-grid-baseline));
package/src/css/table.css CHANGED
@@ -295,6 +295,87 @@
295
295
  outline-offset: -2px;
296
296
  }
297
297
 
298
+ /* Header filter button (CnDataTable `filterable`). 24px hit area with the
299
+ 16px icon; the filled icon plus the primary colour marks an active filter,
300
+ so the state never rests on colour alone. */
301
+ .cn-table-header__filter {
302
+ display: inline-flex;
303
+ align-items: center;
304
+ justify-content: center;
305
+ width: 24px;
306
+ min-width: 24px;
307
+ height: 24px;
308
+ min-height: 24px;
309
+ margin: 0 0 0 4px;
310
+ padding: 0;
311
+ border: none;
312
+ border-radius: var(--border-radius, 4px);
313
+ background: transparent;
314
+ color: var(--color-text-maxcontrast);
315
+ vertical-align: middle;
316
+ cursor: pointer;
317
+ }
318
+
319
+ .cn-table-header__filter:hover {
320
+ background-color: var(--color-background-hover);
321
+ color: var(--color-main-text);
322
+ }
323
+
324
+ .cn-table-header__filter:focus-visible {
325
+ outline: 2px solid var(--color-main-text);
326
+ outline-offset: 0;
327
+ }
328
+
329
+ .cn-table-header__filter--active {
330
+ color: var(--color-primary-element);
331
+ }
332
+
333
+ /* Active header filters above the table, one removable chip each. */
334
+ .cn-data-table__filter-chips {
335
+ display: flex;
336
+ flex-wrap: wrap;
337
+ gap: 6px;
338
+ margin: 0 0 8px;
339
+ padding: 0;
340
+ list-style: none;
341
+ }
342
+
343
+ .cn-data-table__filter-chip {
344
+ display: inline-flex;
345
+ align-items: center;
346
+ gap: 4px;
347
+ padding: 2px 4px 2px 10px;
348
+ border: 1px solid var(--color-border-dark);
349
+ border-radius: var(--border-radius-pill, 16px);
350
+ background-color: var(--color-background-dark);
351
+ color: var(--color-main-text);
352
+ }
353
+
354
+ .cn-data-table__filter-chip-remove {
355
+ display: inline-flex;
356
+ align-items: center;
357
+ justify-content: center;
358
+ width: 24px;
359
+ min-width: 24px;
360
+ height: 24px;
361
+ min-height: 24px;
362
+ margin: 0;
363
+ padding: 0;
364
+ border: none;
365
+ border-radius: 50%;
366
+ background: transparent;
367
+ color: var(--color-main-text);
368
+ cursor: pointer;
369
+ }
370
+
371
+ .cn-data-table__filter-chip-remove:hover {
372
+ background-color: var(--color-background-hover);
373
+ }
374
+
375
+ .cn-data-table__filter-chip-remove:focus-visible {
376
+ outline: 2px solid var(--color-main-text);
377
+ }
378
+
298
379
  /* Multi-column sort priority badge (1, 2, 3), rendered only when two or more rendered, sortable columns are sorted. */
299
380
  .cn-table-sort-badge {
300
381
  display: inline-flex;
package/src/index.js CHANGED
@@ -35,6 +35,7 @@ export {
35
35
  CnActionsBar,
36
36
  CnActionsMenu,
37
37
  CnAddWidgetModal,
38
+ CnAdminActionCard,
38
39
  CnAdminSettingsShell,
39
40
  CnAdvancedFormDialog,
40
41
  CnAppLoading,
@@ -268,6 +269,7 @@ export {
268
269
  CnTileWidget,
269
270
  CnTimelineStages,
270
271
  CnTimelineView,
272
+ CnTimelineWidget,
271
273
  CnTranslatedBadge,
272
274
  CnTreeView,
273
275
  CnUserActionMenu,
@@ -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.45.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." },
@@ -1173,6 +1175,11 @@
1173
1175
  "type": "string",
1174
1176
  "description": "i18n translation key resolved by the consuming app's t() function at render time."
1175
1177
  },
1178
+ "translateLabel": {
1179
+ "type": "boolean",
1180
+ "default": true,
1181
+ "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."
1182
+ },
1176
1183
  "icon": {
1177
1184
  "type": "string",
1178
1185
  "description": "CSS class for the icon (e.g. 'icon-checkmark') or MDI component name."
@@ -1186,6 +1193,11 @@
1186
1193
  "additionalProperties": { "type": ["string", "number", "boolean"] },
1187
1194
  "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
1195
  },
1196
+ "params": {
1197
+ "type": "object",
1198
+ "additionalProperties": { "type": ["string", "number"], "minLength": 1 },
1199
+ "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."
1200
+ },
1189
1201
  "order": {
1190
1202
  "type": "integer",
1191
1203
  "description": "Display order in the menu. Items without an order render last."
@@ -1259,6 +1271,11 @@
1259
1271
  "properties": {
1260
1272
  "id": { "type": "string" },
1261
1273
  "label": { "type": "string" },
1274
+ "translateLabel": {
1275
+ "type": "boolean",
1276
+ "default": true,
1277
+ "description": "Same as menuItem.translateLabel: false renders `label` as written instead of translating it."
1278
+ },
1262
1279
  "icon": { "type": "string" },
1263
1280
  "route": { "type": "string" },
1264
1281
  "query": {
@@ -1266,6 +1283,11 @@
1266
1283
  "additionalProperties": { "type": ["string", "number", "boolean"] },
1267
1284
  "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
1285
  },
1286
+ "params": {
1287
+ "type": "object",
1288
+ "additionalProperties": { "type": ["string", "number"], "minLength": 1 },
1289
+ "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."
1290
+ },
1269
1291
  "order": { "type": "integer" },
1270
1292
  "permission": { "type": "string" },
1271
1293
  "section": {
@@ -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
+ }