@conduction/nextcloud-vue 2.56.0-beta.11 → 2.56.0-beta.13

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 (138) hide show
  1. package/dist/esm/components/CnAppNav/CnAppNav.vue2.js +253 -10
  2. package/dist/esm/components/CnAppNav/CnAppNav.vue2.js.map +1 -1
  3. package/dist/esm/components/CnAppNav/CnAppNav.vue3.js +139 -46
  4. package/dist/esm/components/CnAppNav/CnAppNav.vue3.js.map +1 -1
  5. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js +87 -0
  6. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
  7. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js +26 -24
  8. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js.map +1 -1
  9. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue2.js +61 -16
  10. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue2.js.map +1 -1
  11. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue3.js.map +1 -1
  12. package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue2.js +20 -0
  13. package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue2.js.map +1 -1
  14. package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue3.js +3 -2
  15. package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue3.js.map +1 -1
  16. package/dist/esm/components/CnBreadcrumbs/CnBreadcrumbs.vue2.js +13 -0
  17. package/dist/esm/components/CnBreadcrumbs/CnBreadcrumbs.vue2.js.map +1 -1
  18. package/dist/esm/components/CnBreadcrumbs/CnBreadcrumbs.vue3.js +2 -1
  19. package/dist/esm/components/CnBreadcrumbs/CnBreadcrumbs.vue3.js.map +1 -1
  20. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js +33 -2
  21. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js.map +1 -1
  22. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js.map +1 -1
  23. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +154 -4
  24. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  25. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +555 -480
  26. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  27. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue2.js +9 -3
  28. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue2.js.map +1 -1
  29. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue3.js +1 -1
  30. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue3.js.map +1 -1
  31. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +37 -1
  32. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
  33. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +12 -4
  34. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
  35. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +187 -19
  36. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  37. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +215 -135
  38. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  39. package/dist/esm/components/CnFkResolveCell/CnFkResolveCell.vue2.js +3 -1
  40. package/dist/esm/components/CnFkResolveCell/CnFkResolveCell.vue2.js.map +1 -1
  41. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js +144 -4
  42. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
  43. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
  44. package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue2.js +157 -7
  45. package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue2.js.map +1 -1
  46. package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue3.js +23 -5
  47. package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue3.js.map +1 -1
  48. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +30 -0
  49. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  50. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +2 -2
  51. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  52. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +5 -1
  53. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
  54. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js +4 -2
  55. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js.map +1 -1
  56. package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue2.js +11 -0
  57. package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue2.js.map +1 -1
  58. package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue3.js +2 -1
  59. package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue3.js.map +1 -1
  60. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue2.js +63 -2
  61. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue2.js.map +1 -1
  62. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue3.js +96 -37
  63. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue3.js.map +1 -1
  64. package/dist/esm/components/CnStagesWidget/stagesModel.js +5 -2
  65. package/dist/esm/components/CnStagesWidget/stagesModel.js.map +1 -1
  66. package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js +74 -5
  67. package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js.map +1 -1
  68. package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js +15 -8
  69. package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js.map +1 -1
  70. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +14 -2
  71. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
  72. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js +2 -1
  73. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
  74. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue2.js +73 -1
  75. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue2.js.map +1 -1
  76. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue3.js +14 -2
  77. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue3.js.map +1 -1
  78. package/dist/esm/l10n/en.json.js +5 -1
  79. package/dist/esm/l10n/en.json.js.map +1 -1
  80. package/dist/esm/l10n/nl.json.js +5 -1
  81. package/dist/esm/l10n/nl.json.js.map +1 -1
  82. package/dist/esm/mixins/pageViews.js +400 -0
  83. package/dist/esm/mixins/pageViews.js.map +1 -0
  84. package/dist/esm/nextcloud-vue.css +513 -81
  85. package/dist/esm/store/useObjectStore.js +118 -33
  86. package/dist/esm/store/useObjectStore.js.map +1 -1
  87. package/dist/esm/utils/actionsDispatcher.js +25 -2
  88. package/dist/esm/utils/actionsDispatcher.js.map +1 -1
  89. package/dist/esm/utils/columnFilters.js +27 -3
  90. package/dist/esm/utils/columnFilters.js.map +1 -1
  91. package/dist/esm/utils/schema.js +40 -2
  92. package/dist/esm/utils/schema.js.map +1 -1
  93. package/dist/esm/utils/schemaRefSlug.js +19 -3
  94. package/dist/esm/utils/schemaRefSlug.js.map +1 -1
  95. package/dist/esm/utils/validateManifestV2.compiled.js +2 -2
  96. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  97. package/dist/nextcloud-vue.cjs.js +8858 -6566
  98. package/dist/nextcloud-vue.cjs.js.map +2 -2
  99. package/dist/nextcloud-vue.css +513 -81
  100. package/l10n/en.json +5 -0
  101. package/l10n/nl.json +5 -0
  102. package/package.json +1 -1
  103. package/src/components/CnAppNav/CnAppNav.vue +441 -14
  104. package/src/components/CnAppRoot/CnAppRoot.md +4 -0
  105. package/src/components/CnAppRoot/CnAppRoot.vue +94 -1
  106. package/src/components/CnAuditTrailCard/CnAuditTrailCard.vue +61 -16
  107. package/src/components/CnAuditTrailWidget/CnAuditTrailWidget.vue +22 -1
  108. package/src/components/CnBreadcrumbs/CnBreadcrumbs.vue +14 -0
  109. package/src/components/CnCellRenderer/CnCellRenderer.vue +33 -2
  110. package/src/components/CnDashboardPage/CnDashboardPage.vue +237 -12
  111. package/src/components/CnDataTable/CnColumnFilterPopover.vue +11 -5
  112. package/src/components/CnDataTable/CnDataTable.vue +45 -1
  113. package/src/components/CnDetailPage/CnDetailPage.md +8 -0
  114. package/src/components/CnDetailPage/CnDetailPage.vue +263 -7
  115. package/src/components/CnFkResolveCell/CnFkResolveCell.vue +3 -1
  116. package/src/components/CnFormDialog/CnFormDialog.vue +144 -4
  117. package/src/components/CnHeaderWidget/CnHeaderWidget.vue +206 -9
  118. package/src/components/CnIndexPage/CnIndexPage.vue +32 -2
  119. package/src/components/CnObjectListWidget/CnObjectListWidget.vue +7 -1
  120. package/src/components/CnSegmentedControl/CnSegmentedControl.md +1 -1
  121. package/src/components/CnSegmentedControl/CnSegmentedControl.vue +12 -0
  122. package/src/components/CnStagesWidget/CnStagesWidget.vue +189 -2
  123. package/src/components/CnStagesWidget/stagesModel.js +5 -2
  124. package/src/components/CnStatWidget/CnStatWidget.vue +89 -7
  125. package/src/components/CnTabsWidget/CnTabsWidget.vue +15 -2
  126. package/src/components/CnWidgetWrapper/CnWidgetWrapper.md +4 -0
  127. package/src/components/CnWidgetWrapper/CnWidgetWrapper.vue +107 -1
  128. package/src/css/detail-page.css +37 -4
  129. package/src/css/kpi-card.css +71 -1
  130. package/src/css/table.css +18 -6
  131. package/src/mixins/pageViews.js +396 -0
  132. package/src/schemas/app-manifest-v2.schema.json +162 -3
  133. package/src/store/useObjectStore.js +118 -33
  134. package/src/utils/actionsDispatcher.js +25 -2
  135. package/src/utils/columnFilters.js +29 -2
  136. package/src/utils/schema.js +40 -2
  137. package/src/utils/schemaRefSlug.js +19 -3
  138. package/src/utils/validateManifestV2.compiled.js +2 -2
@@ -11,8 +11,14 @@
11
11
  -->
12
12
  <template>
13
13
  <div class="cn-dashboard-page" data-testid="cn-dashboard-page">
14
- <!-- Header -->
15
- <div class="cn-dashboard-page__header" data-testid="cn-dashboard-page-header">
14
+ <!-- Header. `showHeader: false` drops the whole row (title, description,
15
+ header actions, edit toggle) for a page that opens with its own
16
+ heading, such as a greeting; the title then stays as a visually
17
+ hidden heading so the main landmark keeps its name. -->
18
+ <h2 v-if="!showHeader && title" class="hidden-visually" data-testid="cn-dashboard-page-hidden-title">
19
+ {{ resolvedTitle }}
20
+ </h2>
21
+ <div v-if="showHeader" class="cn-dashboard-page__header" data-testid="cn-dashboard-page-header">
16
22
  <div class="cn-dashboard-page__header-left">
17
23
  <div v-if="title || $slots['title-meta']" class="cn-dashboard-page__title-row">
18
24
  <h2 v-if="title" class="cn-dashboard-page__title">
@@ -26,6 +32,18 @@
26
32
  </p>
27
33
  </div>
28
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" />
29
47
  <!-- Declarative header actions (#91 Wave 3): a manifest
30
48
  `headerActions[]` renders as buttons (open-form / api-call /
31
49
  toggle / navigate / refresh) with visibleWhen gating,
@@ -212,7 +230,7 @@
212
230
  sections (before-grid / after-grid / end): a page can legitimately
213
231
  have no grid widgets yet still show bodyWidget content, so
214
232
  "No widgets configured" would be a false negative. -->
215
- <div v-else-if="!hasWidgets && !hasBodyWidgets" class="cn-dashboard-page__empty">
233
+ <div v-else-if="!hasWidgets && !hasBodyWidgets && !hasViews" class="cn-dashboard-page__empty">
216
234
  <!-- @slot empty Replaces the default empty state shown when the dashboard has no widgets. Defaults to an `NcEmptyContent` block. -->
217
235
  <slot name="empty">
218
236
  <NcEmptyContent :description="emptyLabel">
@@ -247,15 +265,34 @@
247
265
  to the layout/widgets arrays — in-place pushes (Add widget) keep
248
266
  the same array identity and can't re-render it. One remount per
249
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. -->
250
286
  <CnDashboardGrid
251
- v-else
252
- :key="`cn-dashboard-grid-${gridEditable ? 'editing' : 'live'}`"
253
- :layout="displayLayout"
254
- :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"
255
292
  :columns="columns"
256
293
  :cellHeight="cellHeight"
257
294
  :margin="gridMargin"
258
- @layoutChange="onLayoutChange">
295
+ @layoutChange="section.view ? onViewLayoutChange($event) : onLayoutChange($event)">
259
296
  <template #widget="{ item }">
260
297
  <!-- In-app edit overlay (ADR-041): a single launchpad-style
261
298
  configure cog opens the per-widget style/config editor,
@@ -320,6 +357,7 @@
320
357
  :borderless="widgetBorderless(item)"
321
358
  :flush="item.flush !== false"
322
359
  :buttons="getWidgetButtons(item)"
360
+ :headerLink="getWidgetHeaderLink(item)"
323
361
  :styleConfig="item.styleConfig || {}"
324
362
  :titleIconPosition="getWidgetTitleIconPosition(item)"
325
363
  :titleIconColor="getWidgetTitleIconColor(item)"
@@ -397,9 +435,11 @@
397
435
  :iconUrl="getWidgetIconUrl(item)"
398
436
  :iconClass="getWidgetIconClass(item)"
399
437
  :showTitle="widgetShowTitle(item)"
438
+ :showActions="widgetShowActions(item)"
400
439
  :borderless="widgetBorderless(item)"
401
440
  :flush="item.flush !== false"
402
441
  :buttons="getWidgetButtons(item)"
442
+ :headerLink="getWidgetHeaderLink(item)"
403
443
  :styleConfig="item.styleConfig || {}"
404
444
  :titleIconPosition="getWidgetTitleIconPosition(item)"
405
445
  :titleIconColor="getWidgetTitleIconColor(item)"
@@ -478,6 +518,7 @@
478
518
  :flush="item.flush !== false"
479
519
  :class="{ 'cn-dashboard-page__card-fit': isCardWidget(item) }"
480
520
  :buttons="getWidgetButtons(item)"
521
+ :headerLink="getWidgetHeaderLink(item)"
481
522
  :styleConfig="item.styleConfig || {}"
482
523
  :documentationUrl="getWidgetDocumentationUrl(item)"
483
524
  :docsAnchor="getWidgetDocsAnchor(item)"
@@ -499,9 +540,11 @@
499
540
  :iconUrl="getWidgetIconUrl(item)"
500
541
  :iconClass="getWidgetIconClass(item)"
501
542
  :showTitle="widgetShowTitle(item)"
543
+ :showActions="widgetShowActions(item)"
502
544
  :borderless="widgetBorderless(item)"
503
545
  :flush="item.flush !== false"
504
546
  :buttons="getWidgetButtons(item)"
547
+ :headerLink="getWidgetHeaderLink(item)"
505
548
  :styleConfig="item.styleConfig || {}"
506
549
  :titleIconPosition="getWidgetTitleIconPosition(item)"
507
550
  :titleIconColor="getWidgetTitleIconColor(item)"
@@ -536,7 +579,9 @@
536
579
  :iconUrl="getWidgetIconUrl(item)"
537
580
  :iconClass="getWidgetIconClass(item)"
538
581
  :showTitle="widgetShowTitle(item)"
582
+ :showActions="widgetShowActions(item)"
539
583
  :buttons="getWidgetButtons(item)"
584
+ :headerLink="getWidgetHeaderLink(item)"
540
585
  :styleConfig="item.styleConfig || {}"
541
586
  :showRefresh="getWidgetShowRefresh(item)"
542
587
  :documentationUrl="getWidgetDocumentationUrl(item)"
@@ -566,6 +611,7 @@
566
611
  :titleIconVariant="getWidgetTitleIconVariant(item)"
567
612
  :class="{ 'cn-dashboard-page__card-fit': isCardWidget(item) }"
568
613
  :buttons="getWidgetButtons(item)"
614
+ :headerLink="getWidgetHeaderLink(item)"
569
615
  :styleConfig="item.styleConfig || {}"
570
616
  :documentationUrl="getWidgetDocumentationUrl(item)"
571
617
  :docsAnchor="getWidgetDocsAnchor(item)"
@@ -640,6 +686,22 @@
640
686
  </template>
641
687
  </CnDashboardGrid>
642
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
+
643
705
  <!-- Declarative in-body sections, `placement: "after-grid"` — host-app
644
706
  section components rendered BELOW the widget grid. -->
645
707
  <CnBodySections
@@ -699,6 +761,7 @@ import CnBuildiqEditButton from '../CnBuildiqEditButton/CnBuildiqEditButton.vue'
699
761
  import CnChartWidget from '../CnChartWidget/CnChartWidget.vue'
700
762
  import CnDashboardGrid from '../CnDashboardGrid/CnDashboardGrid.vue'
701
763
  import CnDateRangePicker, { DEFAULT_DATE_RANGE_PRESETS, resolvePresetWindow } from '../CnDateRangePicker/CnDateRangePicker.vue'
764
+ import CnSegmentedControl from '../CnSegmentedControl/CnSegmentedControl.vue'
702
765
  import CnStatsBlockWidget from '../CnStatsBlockWidget/CnStatsBlockWidget.vue'
703
766
  import CnTileWidget from '../CnTileWidget/CnTileWidget.vue'
704
767
  import CnWidgetRefItem from '../CnWidgetRefItem/CnWidgetRefItem.vue'
@@ -706,6 +769,7 @@ import CnWidgetRenderer from '../CnWidgetRenderer/CnWidgetRenderer.vue'
706
769
  import CnWidgetWrapper from '../CnWidgetWrapper/CnWidgetWrapper.vue'
707
770
  import { useIntegrationRegistry } from '../../composables/useIntegrationRegistry.js'
708
771
  import { readUserPreference, writeUserPreference } from '../../composables/useUserPreferences.js'
772
+ import { pageViews } from '../../mixins/pageViews.js'
709
773
  import { dashboardLayoutKey, mergeUserLayout } from '../../store/plugins/dashboardLayouts.js'
710
774
  import { isAppInstalled } from '../../utils/appInstalled.js'
711
775
  import { compareVisibleWhen, readVisibleWhenValue } from '../../utils/visibleWhen.js'
@@ -860,8 +924,11 @@ export default {
860
924
  CnBuildiqEditButton,
861
925
  CnWidgetStyleEditorModal,
862
926
  CnLeafMountHost,
927
+ CnSegmentedControl,
863
928
  },
864
929
 
930
+ mixins: [pageViews],
931
+
865
932
  inject: {
866
933
  /**
867
934
  * Buildiq in-app edit state (ADR-041), provided by CnAppRoot as a ref.
@@ -921,6 +988,34 @@ export default {
921
988
  default: '',
922
989
  },
923
990
 
991
+ /**
992
+ * Whether the header row renders: title, description, header actions
993
+ * and the edit toggle. `false` (manifest `config.showHeader: false`)
994
+ * drops it for a page whose first widget is its heading; the title
995
+ * then renders visually hidden so the page keeps an accessible name.
996
+ *
997
+ * @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-a-dashboard-page-can-hide-its-header
998
+ */
999
+ showHeader: {
1000
+ type: Boolean,
1001
+ default: true,
1002
+ },
1003
+
1004
+ /**
1005
+ * Whether a widget draws its overflow Actions menu when neither its
1006
+ * placement nor its definition says (`showActions`). `false` (manifest
1007
+ * `config.showWidgetActions: false`) drops the menu from every such
1008
+ * widget, for a dashboard whose widgets carry a header link and
1009
+ * nothing else. A widget that sets `showActions: true` keeps its menu.
1010
+ * Card widgets have no menu either way.
1011
+ *
1012
+ * @spec openspec/changes/zuiddrecht-pixel-gaps-2/specs/zuiddrecht-pixel-gaps-2/spec.md#requirement-a-dashboard-can-drop-the-widget-actions-menu
1013
+ */
1014
+ showWidgetActions: {
1015
+ type: Boolean,
1016
+ default: true,
1017
+ },
1018
+
924
1019
  /**
925
1020
  * Widget definitions array. Each widget defines metadata for rendering.
926
1021
  *
@@ -1836,6 +1931,89 @@ export default {
1836
1931
  return this.layout.length > 0 || this.widgetRefItems.length > 0
1837
1932
  },
1838
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
+
1839
2017
  /**
1840
2018
  * The widget settings object handed to CnWidgetStyleEditorModal,
1841
2019
  * built from the widget definition currently being configured
@@ -1966,6 +2144,14 @@ export default {
1966
2144
  this.evaluateWidgetConditions()
1967
2145
  },
1968
2146
  },
2147
+
2148
+ /** A view's widgets carry predicates too. */
2149
+ views: {
2150
+ deep: true,
2151
+ handler() {
2152
+ this.evaluateWidgetConditions()
2153
+ },
2154
+ },
1969
2155
  },
1970
2156
 
1971
2157
  created() {
@@ -2830,7 +3016,8 @@ export default {
2830
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
2831
3017
  */
2832
3018
  async evaluateWidgetConditions() {
2833
- 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
2834
3021
  && this.widgetDisplayConfig(def).visibleWhen)
2835
3022
  // Skip when the CONDITIONAL SET is unchanged: the deep `widgets`
2836
3023
  // watch fires on any def edit, but only an added/removed/edited
@@ -2964,7 +3151,7 @@ export default {
2964
3151
  */
2965
3152
  getWidgetDef(widgetId) {
2966
3153
  const list = Array.isArray(this.widgets) ? this.widgets : []
2967
- return list.find((w) => w && w.id === widgetId) || null
3154
+ return list.find((w) => w && w.id === widgetId) || this.findViewWidget(widgetId)
2968
3155
  },
2969
3156
 
2970
3157
  /**
@@ -3355,11 +3542,20 @@ export default {
3355
3542
  * exactly that reason; there is now one rule for every family, so a
3356
3543
  * custom slot and a registered widget cannot disagree about it.
3357
3544
  *
3545
+ * A greeting header that declares `content.ground: true` sits on the
3546
+ * page ground, so it asks for no card either: one key on the widget
3547
+ * instead of a second one on its placement.
3548
+ *
3549
+ * @spec openspec/changes/zuiddrecht-pixel-gaps-2/specs/zuiddrecht-pixel-gaps-2/spec.md#requirement-a-greeting-can-sit-on-the-page-ground
3358
3550
  * @param {object} item the layout placement.
3359
3551
  * @return {boolean}
3360
3552
  */
3361
3553
  widgetBorderless(item) {
3362
- return item.borderless === true
3554
+ if (item.borderless === true) {
3555
+ return true
3556
+ }
3557
+ const def = this.getWidgetDef(item.widgetId)
3558
+ return Boolean(def && def.type === 'header' && this.getWidgetContent(item).ground === true)
3363
3559
  },
3364
3560
 
3365
3561
  /**
@@ -3376,7 +3572,7 @@ export default {
3376
3572
  const def = this.getWidgetDef(item.widgetId)
3377
3573
  const value = item.showActions !== undefined ? item.showActions : def?.showActions
3378
3574
  if (value === undefined || value === null) {
3379
- return !this.isCardWidget(item)
3575
+ return this.showWidgetActions && !this.isCardWidget(item)
3380
3576
  }
3381
3577
  return value !== false
3382
3578
  },
@@ -3410,6 +3606,22 @@ export default {
3410
3606
  return def?.buttons || []
3411
3607
  },
3412
3608
 
3609
+ /**
3610
+ * The widget's header text link (`headerLink` on the layout entry,
3611
+ * else on the widget definition), forwarded to CnWidgetWrapper.
3612
+ *
3613
+ * @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-a-widget-header-carries-a-text-link
3614
+ * @param {object} item Layout placement entry.
3615
+ * @return {object|null}
3616
+ */
3617
+ getWidgetHeaderLink(item) {
3618
+ if (item?.headerLink && typeof item.headerLink === 'object') {
3619
+ return item.headerLink
3620
+ }
3621
+ const def = this.getWidgetDef(item.widgetId)
3622
+ return (def?.headerLink && typeof def.headerLink === 'object') ? def.headerLink : null
3623
+ },
3624
+
3413
3625
  /**
3414
3626
  * Whether a widget's overflow Actions menu shows the Refresh item.
3415
3627
  * An explicit per-widget flag wins — on the widget definition or
@@ -3953,6 +4165,19 @@ export default {
3953
4165
  padding: 60px 20px;
3954
4166
  }
3955
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
+
3956
4181
  .cn-dashboard-page__page-filters {
3957
4182
  display: flex;
3958
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() {
@@ -278,6 +278,14 @@
278
278
  :row="row"
279
279
  :rowKey="rowKey" />
280
280
  </slot>
281
+ <!-- A second, muted line under the value, from `col.secondary`:
282
+ a field key, a "{field} · {other}" template, or a function
283
+ of the row. Lets one column read "title" over "number ·
284
+ requester" without a custom cell. -->
285
+ <span
286
+ v-if="secondaryValue(row, col)"
287
+ class="cn-table-cell__secondary"
288
+ data-testid="cn-cell-secondary">{{ secondaryValue(row, col) }}</span>
281
289
  </td>
282
290
 
283
291
  <!-- Row actions -->
@@ -444,7 +452,13 @@ export default {
444
452
  * reader is looking. `columnsFromSchema` fills it from the JSON Schema property
445
453
  * description automatically, so schema-driven tables get it for free.
446
454
  *
447
- * @type {Array<{key: string, label: string, description: string, sortable: boolean, width: string, class: string, cellClass: string}|string>}
455
+ * `secondary` draws a second, muted line under the cell's value: a field
456
+ * key (`"number"`), a template with `{field}` placeholders
457
+ * (`"{number} · {requester}"`, dotted paths allowed), or a function of
458
+ * the row. Empty fields leave their placeholder blank; a line that
459
+ * resolves to nothing is not drawn.
460
+ *
461
+ * @type {Array<{key: string, label: string, description: string, sortable: boolean, width: string, class: string, cellClass: string, secondary: (string|((row: object) => string))}|string>}
448
462
  */
449
463
  columns: {
450
464
  type: Array,
@@ -1415,6 +1429,36 @@ export default {
1415
1429
  * @param {object} col The column definition.
1416
1430
  * @return {unknown} The value handed to the slot / CnCellRenderer.
1417
1431
  */
1432
+ /**
1433
+ * The secondary line of a cell, from `col.secondary`: a function is
1434
+ * called with the row; a string with `{field}` placeholders is a
1435
+ * template filled from the row (dotted paths allowed); any other
1436
+ * string is a field key. Returns '' when there is nothing to draw.
1437
+ *
1438
+ * @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-a-table-column-carries-a-secondary-line
1439
+ * @param {object} row The row.
1440
+ * @param {object} col The column definition.
1441
+ * @return {string} The secondary text, or ''.
1442
+ */
1443
+ secondaryValue(row, col) {
1444
+ const spec = col?.secondary
1445
+ if (typeof spec === 'function') {
1446
+ const out = spec(row)
1447
+ return out === null || out === undefined ? '' : String(out)
1448
+ }
1449
+ if (typeof spec !== 'string' || spec === '') {
1450
+ return ''
1451
+ }
1452
+ const asText = (v) => (v === null || v === undefined ? '' : String(v))
1453
+ if (spec.includes('{')) {
1454
+ const filled = spec.replace(/\{([^}]+)\}/g, (_, key) => asText(this.getCellValue(row, key.trim())))
1455
+ // A template whose every field is empty leaves only separators:
1456
+ // nothing worth a line.
1457
+ return /[\p{L}\p{N}]/u.test(filled) ? filled.trim() : ''
1458
+ }
1459
+ return asText(this.getCellValue(row, spec))
1460
+ },
1461
+
1418
1462
  cellValue(row, col) {
1419
1463
  if (col && col.aggregate) {
1420
1464
  const cached = this.aggregateValues[String(row[this.rowKey])]
@@ -430,3 +430,11 @@ Four levels of action and the case surfaces around them. All opt in. See [the re
430
430
  ## Not found
431
431
 
432
432
  When the schema-driven fetch answers 404 the page shows a not-found state whose back button is a router link to `notFoundRoute` (default `null`, which means the app root `/`). `notFoundRouteLabel` names that page in the button's "Back to …" label.
433
+
434
+ ### Type eyebrow and breadcrumb
435
+
436
+ `showTypeEyebrow` (default `true`) can drop the type label above the record name. `breadcrumb` (`{ label, route?, params?, href?, icon? }`) draws a breadcrumb line above the header: that crumb, then the record's name as the current crumb. The label shows as text; `icon` draws an icon instead.
437
+
438
+ ### Header card and header widget
439
+
440
+ `headerCard` (default `false`) draws the header as a bordered card. `headerWidget` (default `''`) names a widget in `widgets` to render inside the header under the title, without a card of its own and out of the body grid: the stages bars of the board's case card.