@conduction/nextcloud-vue 2.65.0 → 2.67.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 (103) hide show
  1. package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js +19 -0
  2. package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js.map +1 -1
  3. package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js +10 -3
  4. package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js.map +1 -1
  5. package/dist/esm/components/CnAppNav/CnAppNav.vue2.js +2 -2
  6. package/dist/esm/components/CnAppNav/CnAppNav.vue2.js.map +1 -1
  7. package/dist/esm/components/CnAppNav/CnAppNav.vue3.js.map +1 -1
  8. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +99 -2
  9. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  10. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +438 -383
  11. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  12. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue2.js +9 -3
  13. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue2.js.map +1 -1
  14. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue3.js +1 -1
  15. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue3.js.map +1 -1
  16. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +4 -4
  17. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
  18. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
  19. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +78 -21
  20. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  21. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +129 -96
  22. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  23. package/dist/esm/components/CnFkResolveCell/CnFkResolveCell.vue2.js +3 -1
  24. package/dist/esm/components/CnFkResolveCell/CnFkResolveCell.vue2.js.map +1 -1
  25. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js +144 -4
  26. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
  27. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
  28. package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue2.js +63 -7
  29. package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue2.js.map +1 -1
  30. package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue3.js +2 -1
  31. package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue3.js.map +1 -1
  32. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +31 -1
  33. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  34. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +335 -313
  35. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  36. package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue2.js +11 -0
  37. package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue2.js.map +1 -1
  38. package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue3.js +2 -1
  39. package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue3.js.map +1 -1
  40. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js +5 -11
  41. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js.map +1 -1
  42. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue3.js.map +1 -1
  43. package/dist/esm/composables/useDependencyCheck.js +44 -1
  44. package/dist/esm/composables/useDependencyCheck.js.map +1 -1
  45. package/dist/esm/composables/useListView.js +43 -1
  46. package/dist/esm/composables/useListView.js.map +1 -1
  47. package/dist/esm/composables/useSetupStatus.js +26 -3
  48. package/dist/esm/composables/useSetupStatus.js.map +1 -1
  49. package/dist/esm/l10n/en.json.js +7 -1
  50. package/dist/esm/l10n/en.json.js.map +1 -1
  51. package/dist/esm/l10n/nl.json.js +7 -1
  52. package/dist/esm/l10n/nl.json.js.map +1 -1
  53. package/dist/esm/mixins/pageViews.js +400 -0
  54. package/dist/esm/mixins/pageViews.js.map +1 -0
  55. package/dist/esm/nextcloud-vue.css +38 -2
  56. package/dist/esm/store/plugins/liveUpdates.js +19 -9
  57. package/dist/esm/store/plugins/liveUpdates.js.map +1 -1
  58. package/dist/esm/store/useObjectStore.js +20 -9
  59. package/dist/esm/store/useObjectStore.js.map +1 -1
  60. package/dist/esm/utils/actionsDispatcher.js +11 -2
  61. package/dist/esm/utils/actionsDispatcher.js.map +1 -1
  62. package/dist/esm/utils/columnFilters.js +27 -3
  63. package/dist/esm/utils/columnFilters.js.map +1 -1
  64. package/dist/esm/utils/schema.js +40 -2
  65. package/dist/esm/utils/schema.js.map +1 -1
  66. package/dist/esm/utils/validateManifestV2.compiled.js +2 -2
  67. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  68. package/dist/nextcloud-vue.cjs.js +2377 -1247
  69. package/dist/nextcloud-vue.cjs.js.map +2 -2
  70. package/dist/nextcloud-vue.css +38 -2
  71. package/l10n/en.json +7 -1
  72. package/l10n/nl.json +7 -1
  73. package/package.json +1 -1
  74. package/src/components/CnActionsBar/CnActionsBar.md +2 -1
  75. package/src/components/CnActionsBar/CnActionsBar.vue +25 -1
  76. package/src/components/CnAppNav/CnAppNav.vue +2 -2
  77. package/src/components/CnDashboardPage/CnDashboardPage.vue +165 -8
  78. package/src/components/CnDataTable/CnColumnFilterPopover.vue +11 -5
  79. package/src/components/CnDataTable/CnDataTable.md +1 -1
  80. package/src/components/CnDataTable/CnDataTable.vue +4 -4
  81. package/src/components/CnDetailPage/CnDetailPage.vue +94 -5
  82. package/src/components/CnFkResolveCell/CnFkResolveCell.vue +3 -1
  83. package/src/components/CnFormDialog/CnFormDialog.vue +144 -4
  84. package/src/components/CnHeaderWidget/CnHeaderWidget.vue +67 -8
  85. package/src/components/CnIndexPage/CnIndexPage.md +2 -1
  86. package/src/components/CnIndexPage/CnIndexPage.vue +47 -1
  87. package/src/components/CnSegmentedControl/CnSegmentedControl.md +1 -1
  88. package/src/components/CnSegmentedControl/CnSegmentedControl.vue +12 -0
  89. package/src/components/CnSetupWizard/CnSetupWizard.vue +5 -11
  90. package/src/composables/useDependencyCheck.js +43 -0
  91. package/src/composables/useListView.js +43 -1
  92. package/src/composables/useSetupStatus.js +26 -3
  93. package/src/css/actions-bar.css +20 -2
  94. package/src/css/detail-page.css +5 -0
  95. package/src/mixins/pageViews.js +396 -0
  96. package/src/schemas/app-manifest-v2.schema.json +31 -3
  97. package/src/schemas/app-manifest.schema.json +1 -1
  98. package/src/store/plugins/liveUpdates.js +19 -9
  99. package/src/store/useObjectStore.js +19 -8
  100. package/src/utils/actionsDispatcher.js +11 -2
  101. package/src/utils/columnFilters.js +29 -2
  102. package/src/utils/schema.js +40 -2
  103. package/src/utils/validateManifestV2.compiled.js +2 -2
@@ -1,4 +1,4 @@
1
1
  [diffend] Oversized file quarantined before diffing.
2
2
  name: package/dist/nextcloud-vue.cjs.js.map
3
- size: 35252015 bytes
4
- sha256: fc6a53315288d3796fbfb1aabafee9aa7c5145a0eadbfa2e240269fa544b9764
3
+ size: 35372297 bytes
4
+ sha256: 7065df25e721d338e4612aac2ee75c58c87fb5e17477c811e7bcf66cee41783d
@@ -1940,6 +1940,23 @@ a.cn-kpi-card--stacked:focus-visible {
1940
1940
  color: var(--color-text-maxcontrast);
1941
1941
  }
1942
1942
 
1943
+ /* Beside the inline search (`showCountWithSearch`): the search field shrinks
1944
+ first and the counter stays on one line. */
1945
+ .cn-actions-bar__count--beside-search {
1946
+ flex-shrink: 0;
1947
+ white-space: nowrap;
1948
+ }
1949
+
1950
+ /* Taken out of the flex row so it adds no gap, but kept in the accessibility
1951
+ tree so the live region exists before a total appears. */
1952
+ .cn-actions-bar__count--empty {
1953
+ position: absolute;
1954
+ width: 1px;
1955
+ height: 1px;
1956
+ overflow: hidden;
1957
+ clip-path: inset(50%);
1958
+ }
1959
+
1943
1960
  /* Inline search field */
1944
1961
  .cn-actions-bar__search {
1945
1962
  position: relative;
@@ -2199,8 +2216,9 @@ a.cn-kpi-card--stacked:focus-visible {
2199
2216
  display: contents;
2200
2217
  }
2201
2218
 
2202
- /* #after-search filter buttons + the view toggle ride beside the
2203
- search field (DOM order — search first via its own order). */
2219
+ /* #after-search filter buttons, the beside-search counter and the view
2220
+ toggle ride beside the search field (DOM order — search first via its
2221
+ own order). */
2204
2222
  .cn-actions-bar__info > * {
2205
2223
  order: 1;
2206
2224
  }
@@ -3279,6 +3297,11 @@ a.cn-kpi-card--stacked:focus-visible {
3279
3297
  font-size: 0.9em;
3280
3298
  white-space: nowrap;
3281
3299
  }
3300
+
3301
+ /* A page view the switch shows with nothing to draw (view-switch-containers). */
3302
+ .cn-detail-page .cn-page-view-region--empty {
3303
+ padding: 40px 20px;
3304
+ }
3282
3305
  /* CnReferencePreview — a reference that shows what it points at. */
3283
3306
 
3284
3307
  .cn-reference-preview {
@@ -30567,6 +30590,19 @@ to {
30567
30590
  .cn-dashboard-page__empty[data-v-824052fe] {
30568
30591
  padding: 60px 20px;
30569
30592
  }
30593
+
30594
+ /* The page's view switch on a page without a header row: at the end of its
30595
+ own row, above the grids, where the header would have put it. */
30596
+ .cn-dashboard-page__view-switch[data-v-824052fe] {
30597
+ display: flex;
30598
+ justify-content: flex-end;
30599
+ margin-bottom: 12px;
30600
+ }
30601
+
30602
+ /* A view the switch shows with nothing to draw. */
30603
+ .cn-page-view-region--empty[data-v-824052fe] {
30604
+ padding: 40px 20px;
30605
+ }
30570
30606
  .cn-dashboard-page__page-filters[data-v-824052fe] {
30571
30607
  display: flex;
30572
30608
  flex-wrap: wrap;
package/l10n/en.json CHANGED
@@ -1,5 +1,6 @@
1
1
  {
2
2
  "translations": {
3
+ "Advanced": "Advanced",
3
4
  "The item could not be created.": "The item could not be created.",
4
5
  "Start over": "Start over",
5
6
  "Continue where you left off": "Continue where you left off",
@@ -49,6 +50,7 @@
49
50
  "This integration is not available.": "This integration is not available.",
50
51
  "Integrations": "Integrations",
51
52
  "View": "View",
53
+ "Views": "Views",
52
54
  "current step": "current step",
53
55
  "{app} not available": "{app} not available",
54
56
  "{app} not configured": "{app} not configured",
@@ -408,6 +410,7 @@
408
410
  "Any": "Any",
409
411
  "To": "To",
410
412
  "Equals": "Equals",
413
+ "Contains": "Contains",
411
414
  "Clear": "Clear",
412
415
  "Apply": "Apply",
413
416
  "This column cannot list its objects.": "This column cannot list its objects.",
@@ -1088,6 +1091,7 @@
1088
1091
  "The actions of an attention card are declared in the manifest.": "The actions of an attention card are declared in the manifest.",
1089
1092
  "An attention card needs a title": "An attention card needs a title",
1090
1093
  "Good morning, {name}": "Good morning, {name}",
1094
+ "This view has no widgets yet.": "This view has no widgets yet.",
1091
1095
  "Good morning": "Good morning",
1092
1096
  "Good afternoon, {name}": "Good afternoon, {name}",
1093
1097
  "Good afternoon": "Good afternoon",
@@ -1097,7 +1101,9 @@
1097
1101
  "Show the date of today above the heading": "Show the date of today above the heading",
1098
1102
  "Plain header without a coloured background": "Plain header without a coloured background",
1099
1103
  "Loading…": "Loading…",
1100
- "A register and schema are required": "A register and schema are required"
1104
+ "A register and schema are required": "A register and schema are required",
1105
+ "Change the search or try again.": "Change the search or try again.",
1106
+ "Try again later.": "Try again later."
1101
1107
  },
1102
1108
  "plurals": {
1103
1109
  "%n year": ["%n year", "%n years"],
package/l10n/nl.json CHANGED
@@ -1,5 +1,6 @@
1
1
  {
2
2
  "translations": {
3
+ "Advanced": "Geavanceerd",
3
4
  "The item could not be created.": "Het item kon niet worden aangemaakt.",
4
5
  "Start over": "Opnieuw beginnen",
5
6
  "Continue where you left off": "Ga verder waar je was",
@@ -49,6 +50,7 @@
49
50
  "This integration is not available.": "Deze integratie is niet beschikbaar.",
50
51
  "Integrations": "Integraties",
51
52
  "View": "Weergave",
53
+ "Views": "Weergaven",
52
54
  "current step": "huidige stap",
53
55
  "{app} not available": "{app} niet beschikbaar",
54
56
  "{app} not configured": "{app} niet geconfigureerd",
@@ -408,6 +410,7 @@
408
410
  "Any": "Alle",
409
411
  "To": "Tot",
410
412
  "Equals": "Is gelijk aan",
413
+ "Contains": "Bevat",
411
414
  "Clear": "Wissen",
412
415
  "Apply": "Toepassen",
413
416
  "This column cannot list its objects.": "Deze kolom kan zijn objecten niet tonen.",
@@ -1132,6 +1135,7 @@
1132
1135
  "The actions of an attention card are declared in the manifest.": "De acties van een aandachtskaart staan in het manifest.",
1133
1136
  "An attention card needs a title": "Een aandachtskaart heeft een titel nodig",
1134
1137
  "Good morning, {name}": "Goedemorgen, {name}",
1138
+ "This view has no widgets yet.": "Deze weergave heeft nog geen widgets.",
1135
1139
  "Good morning": "Goedemorgen",
1136
1140
  "Good afternoon, {name}": "Goedemiddag, {name}",
1137
1141
  "Good afternoon": "Goedemiddag",
@@ -1141,7 +1145,9 @@
1141
1145
  "Show the date of today above the heading": "Toon de datum van vandaag boven de kop",
1142
1146
  "Plain header without a coloured background": "Eenvoudige kop zonder gekleurde achtergrond",
1143
1147
  "Loading…": "Laden…",
1144
- "A register and schema are required": "Een register en schema zijn verplicht"
1148
+ "A register and schema are required": "Een register en schema zijn verplicht",
1149
+ "Change the search or try again.": "Pas de zoekopdracht aan of probeer het opnieuw.",
1150
+ "Try again later.": "Probeer het later opnieuw."
1145
1151
  },
1146
1152
  "plurals": {
1147
1153
  "%n year": ["%n jaar", "%n jaar"],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@conduction/nextcloud-vue",
3
- "version": "2.65.0",
3
+ "version": "2.67.0",
4
4
  "description": "Shared Vue component library for Conduction Nextcloud apps — complements @nextcloud/vue with higher-level components, OpenRegister integration, and NL Design System support",
5
5
  "homepage": "https://github.com/ConductionNL/nextcloud-vue#readme",
6
6
  "bugs": {
@@ -152,9 +152,10 @@ Controlling the inline action button count — `inlineActionCount` sets how many
152
152
  | `showMap` | `Boolean` | `false` | Whether to render the third "Map" view-toggle segment. Off by default so existing two-segment consumers are unchanged |
153
153
  | `mapLabel` | `String` | `''` | Label for the map view-toggle option (defaults to "Map"). Only shown when `showMap` |
154
154
  | `mapIcon` | `String` | `''` | MDI icon name for the map option (defaults to the built-in map-marker icon). Resolved via `CnIcon` |
155
- | `showSearch` | `Boolean` | `false` | Whether to show the inline search field on the left of the bar |
155
+ | `showSearch` | `Boolean` | `false` | Whether to show the inline search field on the left of the bar. The field takes the place of the "Showing X of Y" counter unless `showCountWithSearch` is set |
156
156
  | `searchValue` | `String` | `''` | Current value of the inline search field (controlled) |
157
157
  | `searchPlaceholder` | `String` | `''` | Placeholder / accessible label for the inline search field |
158
+ | `showCountWithSearch` | `Boolean` | `false` | Keep the "Showing X of Y" counter visible beside the inline search field, after the search and any `#after-search` controls. No effect without `showSearch` |
158
159
  | `headerActions` | `Array` | `[]` | Manifest-declared page-level actions rendered inside the overflow dropdown between the built-in Refresh and the `#action-items` slot. Each entry is `{ id, label, icon?, disabled? }` — the bar emits `@header-action({ action: id, id })` on click and the parent (e.g. `CnIndexPage`) dispatches the resolved handler. The `icon` field accepts EITHER an MDI Vue component name (e.g. `'History'`) — rendered via `CnIcon` — OR a Nextcloud core CSS icon class (e.g. `'icon-history'`) — rendered as a `<span>` carrying that class. An entry with `href` (URL) or `to` (vue-router location) renders as a real link, `linkTarget` as its `target`; it still emits `@header-action`, so the parent must not navigate for it again. |
159
160
 
160
161
  ## Manifest header actions example
@@ -12,11 +12,16 @@
12
12
  :aria-label="searchPlaceholder || t('nextcloud-vue', 'Search')"
13
13
  @input="onSearchInput">
14
14
  </div>
15
- <span v-else-if="pagination && pagination.total > 0" class="cn-actions-bar__count">
15
+ <span v-else-if="hasTotal" class="cn-actions-bar__count">
16
16
  {{ countText }}
17
17
  </span>
18
18
  <!-- @slot after-search Refinement controls rendered beside the search field on the LEFT side of the bar (e.g. a filter menu button). Convention: the left side groups the VISUAL controls — search, filters, view toggle — while the right cluster holds the ACT controls (add, overflow); the standalone sort select is a display control too but keeps its legacy right-side placement. -->
19
19
  <slot name="after-search" />
20
+ <!-- Counter beside the search (`showCountWithSearch`): after the search and its #after-search controls. The live region stays mounted while there is no total, so results coming back after none are announced too. -->
21
+ <span v-if="showSearch && showCountWithSearch"
22
+ class="cn-actions-bar__count cn-actions-bar__count--beside-search"
23
+ :class="{ 'cn-actions-bar__count--empty': !hasTotal }"
24
+ aria-live="polite">{{ hasTotal ? countText : '' }}</span>
20
25
 
21
26
  <!-- View mode toggle (Cards / Table / List) — segmented control with
22
27
  a sliding thumb that animates between the N segments. Lives in
@@ -644,6 +649,16 @@ export default {
644
649
  default: '',
645
650
  },
646
651
 
652
+ /**
653
+ * Keep the "Showing X of Y" counter visible beside the inline search field,
654
+ * after any `#after-search` controls. By default the search field takes the
655
+ * counter's place. No effect without `showSearch`.
656
+ */
657
+ showCountWithSearch: {
658
+ type: Boolean,
659
+ default: false,
660
+ },
661
+
647
662
  /** Whether the refresh action is currently in progress */
648
663
  refreshing: {
649
664
  type: Boolean,
@@ -811,6 +826,15 @@ export default {
811
826
  }))
812
827
  },
813
828
 
829
+ /**
830
+ * Whether there is a non-empty total for the "Showing X of Y" counter.
831
+ *
832
+ * @return {boolean}
833
+ */
834
+ hasTotal() {
835
+ return Boolean(this.pagination && this.pagination.total > 0)
836
+ },
837
+
814
838
  countText() {
815
839
  if (!this.pagination) {
816
840
  return ''
@@ -1113,7 +1113,7 @@ export default {
1113
1113
 
1114
1114
  /**
1115
1115
  * Label for the foldout's gear button. Manifest override:
1116
- * `nav.settingsLabel`; defaults to "Settings".
1116
+ * `nav.settingsLabel`; defaults to "Advanced".
1117
1117
  *
1118
1118
  * @return {string}
1119
1119
  */
@@ -1122,7 +1122,7 @@ export default {
1122
1122
  if (typeof custom === 'string' && custom.length > 0) {
1123
1123
  return this.effectiveTranslate(custom)
1124
1124
  }
1125
- return t('nextcloud-vue', 'Settings')
1125
+ return t('nextcloud-vue', 'Advanced')
1126
1126
  },
1127
1127
 
1128
1128
  /**
@@ -32,6 +32,18 @@
32
32
  </p>
33
33
  </div>
34
34
  <div class="cn-dashboard-page__header-actions">
35
+ <!-- The page's view switch (`views`), unless a greeting header
36
+ widget on the page draws it. Its options control the view
37
+ region below the grid. -->
38
+ <CnSegmentedControl
39
+ v-if="showsPageViewSwitch"
40
+ class="cn-page-view-switch"
41
+ :options="viewSwitchOptions"
42
+ :modelValue="activeViewId"
43
+ :ariaLabel="viewSwitchLabel"
44
+ :controls="viewRegionId"
45
+ data-testid="cn-dashboard-page-view-switch"
46
+ @update:modelValue="selectView" />
35
47
  <!-- Declarative header actions (#91 Wave 3): a manifest
36
48
  `headerActions[]` renders as buttons (open-form / api-call /
37
49
  toggle / navigate / refresh) with visibleWhen gating,
@@ -218,7 +230,7 @@
218
230
  sections (before-grid / after-grid / end): a page can legitimately
219
231
  have no grid widgets yet still show bodyWidget content, so
220
232
  "No widgets configured" would be a false negative. -->
221
- <div v-else-if="!hasWidgets && !hasBodyWidgets" class="cn-dashboard-page__empty">
233
+ <div v-else-if="!hasWidgets && !hasBodyWidgets && !hasViews" class="cn-dashboard-page__empty">
222
234
  <!-- @slot empty Replaces the default empty state shown when the dashboard has no widgets. Defaults to an `NcEmptyContent` block. -->
223
235
  <slot name="empty">
224
236
  <NcEmptyContent :description="emptyLabel">
@@ -253,15 +265,34 @@
253
265
  to the layout/widgets arrays — in-place pushes (Add widget) keep
254
266
  the same array identity and can't re-render it. One remount per
255
267
  edit flip re-subscribes against the now-reactive graph. -->
268
+ <!-- The view switch for a page without a header row, when no greeting
269
+ header widget draws it. -->
270
+ <div
271
+ v-if="gridSectionsShown && !showHeader && showsPageViewSwitch"
272
+ class="cn-dashboard-page__view-switch">
273
+ <CnSegmentedControl
274
+ class="cn-page-view-switch"
275
+ :options="viewSwitchOptions"
276
+ :modelValue="activeViewId"
277
+ :ariaLabel="viewSwitchLabel"
278
+ :controls="viewRegionId"
279
+ data-testid="cn-dashboard-page-view-switch"
280
+ @update:modelValue="selectView" />
281
+ </div>
282
+ <!-- One grid per section: the page's own layout, then the chosen
283
+ view (`views`) in a region the view switch controls. Both are the
284
+ same grid with the same widget rendering below. A view grid is
285
+ keyed on its view, so switching mounts the other view's grid. -->
256
286
  <CnDashboardGrid
257
- v-else
258
- :key="`cn-dashboard-grid-${gridEditable ? 'editing' : 'live'}`"
259
- :layout="displayLayout"
260
- :editable="gridEditable"
287
+ v-for="section in gridSections"
288
+ :key="`cn-dashboard-grid-${section.key}-${gridEditable ? 'editing' : 'live'}`"
289
+ v-bind="section.attrs"
290
+ :layout="section.layout"
291
+ :editable="section.editable"
261
292
  :columns="columns"
262
293
  :cellHeight="cellHeight"
263
294
  :margin="gridMargin"
264
- @layoutChange="onLayoutChange">
295
+ @layoutChange="section.view ? onViewLayoutChange($event) : onLayoutChange($event)">
265
296
  <template #widget="{ item }">
266
297
  <!-- In-app edit overlay (ADR-041): a single launchpad-style
267
298
  configure cog opens the per-widget style/config editor,
@@ -655,6 +686,22 @@
655
686
  </template>
656
687
  </CnDashboardGrid>
657
688
 
689
+ <!-- The chosen view has nothing to draw: say so in the region, never
690
+ leave a blank area under the switch. -->
691
+ <div
692
+ v-if="gridSectionsShown && viewIsEmpty"
693
+ :id="viewRegionId"
694
+ role="region"
695
+ :aria-label="activeView ? activeView.label : null"
696
+ class="cn-page-view-region cn-page-view-region--empty"
697
+ data-testid="cn-dashboard-page-view-empty">
698
+ <NcEmptyContent :description="viewEmptyText">
699
+ <template #icon>
700
+ <ViewDashboardOutline :size="48" />
701
+ </template>
702
+ </NcEmptyContent>
703
+ </div>
704
+
658
705
  <!-- Declarative in-body sections, `placement: "after-grid"` — host-app
659
706
  section components rendered BELOW the widget grid. -->
660
707
  <CnBodySections
@@ -714,6 +761,7 @@ import CnBuildiqEditButton from '../CnBuildiqEditButton/CnBuildiqEditButton.vue'
714
761
  import CnChartWidget from '../CnChartWidget/CnChartWidget.vue'
715
762
  import CnDashboardGrid from '../CnDashboardGrid/CnDashboardGrid.vue'
716
763
  import CnDateRangePicker, { DEFAULT_DATE_RANGE_PRESETS, resolvePresetWindow } from '../CnDateRangePicker/CnDateRangePicker.vue'
764
+ import CnSegmentedControl from '../CnSegmentedControl/CnSegmentedControl.vue'
717
765
  import CnStatsBlockWidget from '../CnStatsBlockWidget/CnStatsBlockWidget.vue'
718
766
  import CnTileWidget from '../CnTileWidget/CnTileWidget.vue'
719
767
  import CnWidgetRefItem from '../CnWidgetRefItem/CnWidgetRefItem.vue'
@@ -721,6 +769,7 @@ import CnWidgetRenderer from '../CnWidgetRenderer/CnWidgetRenderer.vue'
721
769
  import CnWidgetWrapper from '../CnWidgetWrapper/CnWidgetWrapper.vue'
722
770
  import { useIntegrationRegistry } from '../../composables/useIntegrationRegistry.js'
723
771
  import { readUserPreference, writeUserPreference } from '../../composables/useUserPreferences.js'
772
+ import { pageViews } from '../../mixins/pageViews.js'
724
773
  import { dashboardLayoutKey, mergeUserLayout } from '../../store/plugins/dashboardLayouts.js'
725
774
  import { isAppInstalled } from '../../utils/appInstalled.js'
726
775
  import { compareVisibleWhen, readVisibleWhenValue } from '../../utils/visibleWhen.js'
@@ -875,8 +924,11 @@ export default {
875
924
  CnBuildiqEditButton,
876
925
  CnWidgetStyleEditorModal,
877
926
  CnLeafMountHost,
927
+ CnSegmentedControl,
878
928
  },
879
929
 
930
+ mixins: [pageViews],
931
+
880
932
  inject: {
881
933
  /**
882
934
  * Buildiq in-app edit state (ADR-041), provided by CnAppRoot as a ref.
@@ -1879,6 +1931,89 @@ export default {
1879
1931
  return this.layout.length > 0 || this.widgetRefItems.length > 0
1880
1932
  },
1881
1933
 
1934
+ /**
1935
+ * Whether the grid sections render at all: not while loading, not
1936
+ * while the widget predicates settle, not on the empty page and not
1937
+ * on a widget-ref content page.
1938
+ *
1939
+ * @return {boolean}
1940
+ * @spec openspec/changes/view-switch-containers/specs/view-switch-containers/spec.md#requirement-a-page-declares-views-that-each-hold-a-widget-grid
1941
+ */
1942
+ gridSectionsShown() {
1943
+ if (this.loading || (!this.widgetConditionsSettled && !this.gridEditable)) {
1944
+ return false
1945
+ }
1946
+ if (!this.hasWidgets && !this.hasBodyWidgets && !this.hasViews) {
1947
+ return false
1948
+ }
1949
+ return this.widgetRefItems.length === 0
1950
+ },
1951
+
1952
+ /**
1953
+ * The chosen view's layout as the grid draws it: in live mode the
1954
+ * widgets that would draw nothing leave and the rest close up, as
1955
+ * for the page's own layout.
1956
+ *
1957
+ * @return {Array<object>}
1958
+ * @spec openspec/changes/view-switch-containers/specs/view-switch-containers/spec.md#requirement-a-view-with-nothing-to-draw-says-so
1959
+ */
1960
+ viewDisplayLayout() {
1961
+ const items = this.activeView ? this.activeView.layout : []
1962
+ if (this.gridEditable) {
1963
+ return items
1964
+ }
1965
+ const visible = items.filter((item) => !this.isCollapsedWidget(item))
1966
+ return visible.length === items.length ? items : this.compactDisplayLayout(visible)
1967
+ },
1968
+
1969
+ /**
1970
+ * Whether the chosen view has nothing to draw.
1971
+ *
1972
+ * @return {boolean}
1973
+ * @spec openspec/changes/view-switch-containers/specs/view-switch-containers/spec.md#requirement-a-view-with-nothing-to-draw-says-so
1974
+ */
1975
+ viewIsEmpty() {
1976
+ return this.hasViews && this.viewDisplayLayout.length === 0
1977
+ },
1978
+
1979
+ /**
1980
+ * The grids this page draws. Without views: the page's own layout,
1981
+ * as it always was. With views: the page's own layout when it has
1982
+ * items, then the chosen view's layout in the region the switch
1983
+ * controls (left out when the view is empty; the empty state takes
1984
+ * the region then).
1985
+ *
1986
+ * @return {Array<{key: string, view: boolean, layout: Array<object>, editable: boolean, attrs: object}>}
1987
+ * @spec openspec/changes/view-switch-containers/specs/view-switch-containers/spec.md#requirement-a-page-declares-views-that-each-hold-a-widget-grid
1988
+ */
1989
+ gridSections() {
1990
+ if (!this.gridSectionsShown) {
1991
+ return []
1992
+ }
1993
+ const page = { key: 'page', view: false, layout: this.displayLayout, editable: this.gridEditable, attrs: {} }
1994
+ if (!this.hasViews) {
1995
+ return [page]
1996
+ }
1997
+ const sections = this.displayLayout.length > 0 ? [page] : []
1998
+ if (!this.viewIsEmpty) {
1999
+ sections.push({
2000
+ key: `view-${this.activeViewId}`,
2001
+ view: true,
2002
+ layout: this.viewDisplayLayout,
2003
+ // A user arrangement covers the page's own layout only.
2004
+ editable: this.gridEditable && !this.userLayout,
2005
+ attrs: {
2006
+ id: this.viewRegionId,
2007
+ role: 'region',
2008
+ 'aria-label': this.activeView.label,
2009
+ class: 'cn-page-view-region',
2010
+ 'data-testid': 'cn-dashboard-page-view-region',
2011
+ },
2012
+ })
2013
+ }
2014
+ return sections
2015
+ },
2016
+
1882
2017
  /**
1883
2018
  * The widget settings object handed to CnWidgetStyleEditorModal,
1884
2019
  * built from the widget definition currently being configured
@@ -2009,6 +2144,14 @@ export default {
2009
2144
  this.evaluateWidgetConditions()
2010
2145
  },
2011
2146
  },
2147
+
2148
+ /** A view's widgets carry predicates too. */
2149
+ views: {
2150
+ deep: true,
2151
+ handler() {
2152
+ this.evaluateWidgetConditions()
2153
+ },
2154
+ },
2012
2155
  },
2013
2156
 
2014
2157
  created() {
@@ -2873,7 +3016,8 @@ export default {
2873
3016
  * @spec openspec/changes/live-check-follow-ups/specs/live-check-follow-ups/spec.md#requirement-an-attention-card-says-when-it-could-not-check
2874
3017
  */
2875
3018
  async evaluateWidgetConditions() {
2876
- const conditional = (this.widgets || []).filter((def) => def && def.id && def.type
3019
+ const defs = [...(this.widgets || []), ...this.normalizedViews.flatMap((view) => view.widgets)]
3020
+ const conditional = defs.filter((def) => def && def.id && def.type
2877
3021
  && this.widgetDisplayConfig(def).visibleWhen)
2878
3022
  // Skip when the CONDITIONAL SET is unchanged: the deep `widgets`
2879
3023
  // watch fires on any def edit, but only an added/removed/edited
@@ -3007,7 +3151,7 @@ export default {
3007
3151
  */
3008
3152
  getWidgetDef(widgetId) {
3009
3153
  const list = Array.isArray(this.widgets) ? this.widgets : []
3010
- return list.find((w) => w && w.id === widgetId) || null
3154
+ return list.find((w) => w && w.id === widgetId) || this.findViewWidget(widgetId)
3011
3155
  },
3012
3156
 
3013
3157
  /**
@@ -4021,6 +4165,19 @@ export default {
4021
4165
  padding: 60px 20px;
4022
4166
  }
4023
4167
 
4168
+ /* The page's view switch on a page without a header row: at the end of its
4169
+ own row, above the grids, where the header would have put it. */
4170
+ .cn-dashboard-page__view-switch {
4171
+ display: flex;
4172
+ justify-content: flex-end;
4173
+ margin-bottom: 12px;
4174
+ }
4175
+
4176
+ /* A view the switch shows with nothing to draw. */
4177
+ .cn-page-view-region--empty {
4178
+ padding: 40px 20px;
4179
+ }
4180
+
4024
4181
  .cn-dashboard-page__page-filters {
4025
4182
  display: flex;
4026
4183
  flex-wrap: wrap;
@@ -91,9 +91,9 @@
91
91
  </label>
92
92
  </div>
93
93
 
94
- <!-- string: equals -->
94
+ <!-- string: contains (OpenRegister `[like]`, case-insensitive) -->
95
95
  <label v-else class="cn-column-filter__field">
96
- <span class="cn-column-filter__label">{{ equalsLabel }}</span>
96
+ <span class="cn-column-filter__label">{{ containsLabel }}</span>
97
97
  <input
98
98
  v-model="draft.value"
99
99
  class="cn-column-filter__input"
@@ -131,7 +131,7 @@ let popoverSeq = 0
131
131
  *
132
132
  * Renders the control that fits the column: a checkbox list for an enum, yes,
133
133
  * no or any for a boolean, from and to for a number or a date, a searchable
134
- * list of referenced objects for a reference, and an equals box for text.
134
+ * list of referenced objects for a reference, and a contains box for text.
135
135
  * Real inputs throughout, so every control works from the keyboard. Escape
136
136
  * closes it and the host returns focus to the filter button.
137
137
  *
@@ -265,8 +265,14 @@ export default {
265
265
  return t('nextcloud-vue', 'To')
266
266
  },
267
267
 
268
- equalsLabel() {
269
- return t('nextcloud-vue', 'Equals')
268
+ /**
269
+ * Label of the text box: the text filter matches on contains.
270
+ *
271
+ * @return {string}
272
+ * @spec openspec/changes/header-filter-contains/specs/cn-data-table/spec.md#requirement-a-text-header-filter-matches-on-contains
273
+ */
274
+ containsLabel() {
275
+ return t('nextcloud-vue', 'Contains')
270
276
  },
271
277
 
272
278
  clearLabel() {
@@ -186,7 +186,7 @@ export default {
186
186
  | `includeColumns` | Array | `null` | Column keys to include (whitelist) in schema mode |
187
187
  | `sortKey` | String | `null` | Currently active sort column key (controlled) |
188
188
  | `sortOrder` | String | `'asc'` | Current sort direction: `'asc'` or `'desc'` |
189
- | `sortKeys` | Array | `[]` | Ordered multi-column ("shift+click") sort key list, `[{ key, order }, …]` (0–3 entries); takes precedence over `sortKey`/`sortOrder` when non-empty. Priority badges show only when two or more rendered, sortable columns are sorted; a key whose column is not rendered or not sortable (such as a `_uuid` tie-break) still sorts, but is not counted or numbered |
189
+ | `sortKeys` | Array | `[]` | Ordered multi-column ("shift+click") sort key list, `[{ key, order }, …]` (0–3 entries); takes precedence over `sortKey`/`sortOrder` when non-empty. Priority badges show only when two or more rendered, sortable columns are sorted; a key whose column is not rendered or not sortable (such as a `_uuid` tie-break) still sorts, but is not counted or numbered, and `aria-sort` goes to the first sort key whose column is rendered and sortable |
190
190
  | `selectedIds` | Array | `[]` | Array of selected row IDs (controlled) |
191
191
  | `rowClass` | Function | `null` | `(row) => string` — CSS class(es) applied to each `<tr>` |
192
192
  | `rowClickToView` | Boolean | `false` | Emit `row-click` on a row-body click even while `selectable` (selection then via the checkbox column only) |
@@ -371,7 +371,7 @@ import '../../css/table.css'
371
371
  *
372
372
  * Sorting: a plain click on a sortable header is single-sort (cycle asc → desc → cleared), unchanged from before.
373
373
  * Shift+click (or Shift+Enter on a focused header) appends the column as a secondary/tertiary sort key, capped at 3, with numbered priority badges (1, 2, 3) once two or more rendered, sortable columns are sorted.
374
- * A sort key whose column is not rendered, or not sortable, still sorts, but is not counted or numbered.
374
+ * A sort key whose column is not rendered, or not sortable, still sorts, but is not counted or numbered, and `aria-sort` goes to the first sort key whose column is rendered and sortable.
375
375
  * Pass `sortKeys: [{key, order}, ...]` for multi-sort (falls back to the legacy `sortKey`/`sortOrder` props when empty).
376
376
  * The `sort` event payload is extended, not replaced: `{key, order}` still mirrors the primary key exactly as before; a new `keys` field carries the full ordered list.
377
377
  * See `src/utils/multiColumnSort.js` for the state machine.
@@ -1722,8 +1722,8 @@ export default {
1722
1722
  },
1723
1723
 
1724
1724
  /**
1725
- * `aria-sort` value for a column header: `'ascending'`/`'descending'` for the PRIMARY (index 0) active sort key only, per WCAG guidance that `aria-sort` describes single-column sort state.
1726
- * `null` omits the attribute entirely (unsorted, not sortable, or not the primary key).
1725
+ * `aria-sort` value for a column header: `'ascending'`/`'descending'` for the first rendered sort key only, per WCAG guidance that `aria-sort` describes single-column sort state.
1726
+ * `null` omits the attribute entirely (unsorted, not sortable, or not the first rendered sort key).
1727
1727
  *
1728
1728
  * @param {object} col Column definition.
1729
1729
  * @return {string|null}
@@ -1732,7 +1732,7 @@ export default {
1732
1732
  if (!col.sortable) {
1733
1733
  return null
1734
1734
  }
1735
- const primary = this.effectiveSortKeys[0]
1735
+ const primary = this.renderedSortKeys[0]
1736
1736
  if (!primary || primary.key !== col.key) {
1737
1737
  return null
1738
1738
  }