@conduction/nextcloud-vue 2.93.0 → 2.94.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 (105) hide show
  1. package/dist/esm/components/CnActionsMenu/CnActionsMenu.vue2.js +1 -1
  2. package/dist/esm/components/CnActionsMenu/CnActionsMenu.vue2.js.map +1 -1
  3. package/dist/esm/components/CnActionsMenu/CnActionsMenu.vue3.js +2 -1
  4. package/dist/esm/components/CnActionsMenu/CnActionsMenu.vue3.js.map +1 -1
  5. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue.js +1 -1
  6. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue2.js +28 -8
  7. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue2.js.map +1 -1
  8. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue3.js +37 -2
  9. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue3.js.map +1 -1
  10. package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue.js +1 -1
  11. package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue2.js +139 -3
  12. package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue2.js.map +1 -1
  13. package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue3.js +60 -12
  14. package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue3.js.map +1 -1
  15. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue.js +1 -1
  16. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +91 -36
  17. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  18. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +115 -72
  19. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  20. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +32 -3
  21. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
  22. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +7 -7
  23. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
  24. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +141 -7
  25. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  26. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +5 -2
  27. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  28. package/dist/esm/components/CnPageHeader/CnHeaderTrail.vue.js +8 -0
  29. package/dist/esm/components/CnPageHeader/CnHeaderTrail.vue.js.map +1 -0
  30. package/dist/esm/components/CnPageHeader/CnHeaderTrail.vue2.js +114 -0
  31. package/dist/esm/components/CnPageHeader/CnHeaderTrail.vue2.js.map +1 -0
  32. package/dist/esm/components/CnPageHeader/CnHeaderTrail.vue3.js +45 -0
  33. package/dist/esm/components/CnPageHeader/CnHeaderTrail.vue3.js.map +1 -0
  34. package/dist/esm/components/CnPageHeader/CnPageHeader.vue2.js +38 -2
  35. package/dist/esm/components/CnPageHeader/CnPageHeader.vue2.js.map +1 -1
  36. package/dist/esm/components/CnPageHeader/CnPageHeader.vue3.js +12 -2
  37. package/dist/esm/components/CnPageHeader/CnPageHeader.vue3.js.map +1 -1
  38. package/dist/esm/components/CnPagination/CnPagination.vue2.js.map +1 -1
  39. package/dist/esm/components/CnPagination/CnPagination.vue3.js +56 -51
  40. package/dist/esm/components/CnPagination/CnPagination.vue3.js.map +1 -1
  41. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
  42. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +3 -3
  43. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
  44. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
  45. package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue.js +1 -1
  46. package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue2.js +24 -0
  47. package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue2.js.map +1 -1
  48. package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue3.js +3 -1
  49. package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue3.js.map +1 -1
  50. package/dist/esm/components/CnTabs/CnTabs.vue.js +1 -1
  51. package/dist/esm/components/CnTabs/CnTabs.vue2.js +39 -2
  52. package/dist/esm/components/CnTabs/CnTabs.vue2.js.map +1 -1
  53. package/dist/esm/components/CnTabs/CnTabs.vue3.js.map +1 -1
  54. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue.js +1 -1
  55. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +85 -8
  56. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
  57. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js +37 -9
  58. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
  59. package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue.js +1 -1
  60. package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue2.js.map +1 -1
  61. package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue3.js +4 -3
  62. package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue3.js.map +1 -1
  63. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue.js +1 -1
  64. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue2.js +20 -0
  65. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue2.js.map +1 -1
  66. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue3.js +31 -20
  67. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue3.js.map +1 -1
  68. package/dist/esm/composables/indexSources.js +9 -0
  69. package/dist/esm/composables/indexSources.js.map +1 -1
  70. package/dist/esm/library-style.js +1 -1
  71. package/dist/esm/nextcloud-vue.css +402 -262
  72. package/dist/esm/utils/dueText.js +49 -8
  73. package/dist/esm/utils/dueText.js.map +1 -1
  74. package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
  75. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  76. package/dist/nextcloud-vue.cjs.js +5779 -4827
  77. package/dist/nextcloud-vue.cjs.js.map +2 -2
  78. package/dist/nextcloud-vue.css +402 -262
  79. package/package.json +1 -1
  80. package/src/components/CnActionsMenu/CnActionsMenu.vue +3 -1
  81. package/src/components/CnAdminSettingsShell/CnAdminSettingsShell.vue +39 -1
  82. package/src/components/CnCountdownWidget/CnCountdownWidget.vue +222 -6
  83. package/src/components/CnDashboardPage/CnDashboardPage.vue +96 -2
  84. package/src/components/CnDataTable/CnDataTable.md +1 -0
  85. package/src/components/CnDataTable/CnDataTable.vue +39 -10
  86. package/src/components/CnIndexPage/CnIndexPage.vue +144 -7
  87. package/src/components/CnPageHeader/CnHeaderTrail.vue +140 -0
  88. package/src/components/CnPageHeader/CnPageHeader.md +11 -0
  89. package/src/components/CnPageHeader/CnPageHeader.vue +42 -0
  90. package/src/components/CnPagination/CnPagination.vue +10 -2
  91. package/src/components/CnRecordListWidget/CnRecordListWidget.vue +3 -3
  92. package/src/components/CnSettingsPage/CnSettingsPage.vue +26 -0
  93. package/src/components/CnTabs/CnTabs.vue +39 -2
  94. package/src/components/CnTabsWidget/CnTabsWidget.vue +106 -13
  95. package/src/components/CnVersionInfoCard/CnVersionInfoCard.vue +4 -3
  96. package/src/components/CnWidgetWrapper/CnWidgetWrapper.md +8 -0
  97. package/src/components/CnWidgetWrapper/CnWidgetWrapper.vue +31 -2
  98. package/src/composables/indexSources.js +9 -0
  99. package/src/css/dialog.css +14 -0
  100. package/src/css/look-board-index.css +27 -1
  101. package/src/css/look-board.css +27 -0
  102. package/src/css/page-header.css +16 -0
  103. package/src/schemas/app-manifest-v2.schema.json +22 -15
  104. package/src/utils/dueText.js +49 -8
  105. package/src/utils/validateManifestV2.compiled.js +3 -3
@@ -130,7 +130,7 @@
130
130
  col.class || '',
131
131
  pinClass(leadingCount + colIndex),
132
132
  isGrowColumn(col) ? 'cn-table-col--grow' : '',
133
- colIndex === 0 ? 'cn-table-head--title' : '',
133
+ colIndex === titleColumnIndex ? 'cn-table-head--title' : '',
134
134
  ]"
135
135
  :style="{ ...(col.width ? { width: col.width } : {}), ...pinStyle(leadingCount + colIndex) }"
136
136
  :aria-sort="ariaSortFor(col)"
@@ -259,7 +259,7 @@
259
259
  <td
260
260
  v-for="(col, colIndex) in effectiveColumns"
261
261
  :key="col.key"
262
- :class="[col.class || '', col.cellClass || '', cellClass ? cellClass(row, col) : '', pinClass(leadingCount + colIndex), colIndex === 0 ? 'cn-table-col--title' : '', isGrowColumn(col) ? 'cn-table-col--grow' : '']"
262
+ :class="[col.class || '', col.cellClass || '', cellClass ? cellClass(row, col) : '', pinClass(leadingCount + colIndex), colIndex === titleColumnIndex ? 'cn-table-col--title' : '', isGrowColumn(col) ? 'cn-table-col--grow' : '']"
263
263
  :style="{ ...(col.width ? { maxWidth: col.width } : {}), ...pinStyle(leadingCount + colIndex) }"
264
264
  @mouseenter="titleWhenClipped">
265
265
  <!-- A row with a `rowClickRoute` is a real link: this anchor
@@ -267,7 +267,7 @@
267
267
  the URL, a middle or ctrl click opens a new tab natively,
268
268
  and the row is reachable with Tab. The row's own click
269
269
  handlers treat it as a nested control and leave it be. -->
270
- <a v-if="colIndex === 0 && rowLinks[String(row[rowKey])]"
270
+ <a v-if="colIndex === titleColumnIndex && rowLinks[String(row[rowKey])]"
271
271
  class="cn-table-row__link"
272
272
  :href="rowLinks[String(row[rowKey])].href"
273
273
  :aria-label="rowLinkLabel(row, col)"
@@ -281,9 +281,9 @@
281
281
  the record is locked. It renders nothing when unlocked,
282
282
  so an unlocked table is byte-for-byte what it was. -->
283
283
  <CnUnreadMarker
284
- v-if="colIndex === 0 && isUnreadRow(row)" />
284
+ v-if="colIndex === titleColumnIndex && isUnreadRow(row)" />
285
285
  <CnLockIndicator
286
- v-if="colIndex === 0"
286
+ v-if="colIndex === titleColumnIndex"
287
287
  :object="row"
288
288
  :size="16" />
289
289
  <!-- Declared state indicators ride the first data cell
@@ -293,7 +293,7 @@
293
293
  however the record is shaped. Past the cap the rest move
294
294
  into the row menu, because a row of nine icons is not a
295
295
  row anyone can triage at a glance. -->
296
- <span v-if="colIndex === 0 && indicatorsFor(row).shown.length > 0"
296
+ <span v-if="colIndex === titleColumnIndex && indicatorsFor(row).shown.length > 0"
297
297
  class="cn-table-row-indicators"
298
298
  data-testid="cn-row-indicators">
299
299
  <span v-for="indicator in indicatorsFor(row).shown"
@@ -338,7 +338,7 @@
338
338
  file names it (OpenRegister `@self.matchedFile`): plain text,
339
339
  file name only, under the first cell. -->
340
340
  <span
341
- v-if="colIndex === 0 && matchedFileOf(row)"
341
+ v-if="colIndex === titleColumnIndex && matchedFileOf(row)"
342
342
  class="cn-table-cell__secondary cn-table-cell__matched-file"
343
343
  data-testid="cn-row-matched-file">{{ matchedFileLabel(row) }}</span>
344
344
  </td>
@@ -532,7 +532,10 @@ export default {
532
532
  * formats through `fillTemplate`), or a function of the row. Empty fields leave their placeholder blank; a line that
533
533
  * resolves to nothing is not drawn.
534
534
  *
535
- * @type {Array<{key: string, label: string, description: string, sortable: boolean, width: string, class: string, cellClass: string, secondary: (string|((row: object) => string))}|string>}
535
+ * `link: true` makes that column the one that names the row: it carries
536
+ * the row link and the title look. Without it the first column does.
537
+ *
538
+ * @type {Array<{key: string, label: string, description: string, sortable: boolean, width: string, class: string, cellClass: string, link: boolean, secondary: (string|((row: object) => string))}|string>}
536
539
  */
537
540
  columns: {
538
541
  type: Array,
@@ -742,6 +745,17 @@ export default {
742
745
  default: '',
743
746
  },
744
747
 
748
+ /**
749
+ * Draw an unread record (`@self.unread`) in bold with the unread dot.
750
+ * Off, every row reads alike whatever its read state.
751
+ *
752
+ * @spec openspec/changes/screens-index-board-gaps/specs/index-list-board-look/spec.md#requirement-unread-rows-are-marked-only-where-the-reader-asked-for-them
753
+ */
754
+ showUnread: {
755
+ type: Boolean,
756
+ default: true,
757
+ },
758
+
745
759
  /**
746
760
  * How the board look draws the title column: `link` (the default) is
747
761
  * the 15px weight 600 underlined title of the DqZaken board; `plain` is
@@ -1045,6 +1059,20 @@ export default {
1045
1059
  * @return {string|null} The column key, or null when not fitting.
1046
1060
  * @spec openspec/changes/narrow-object-table-widgets/specs/narrow-object-table-widgets/spec.md#requirement-a-narrow-object-table-keeps-its-trailing-columns-in-view
1047
1061
  */
1062
+ /**
1063
+ * The column that names the row: it carries the row link, the title
1064
+ * look and the unread, lock and indicator marks. The first column with
1065
+ * `link: true`, else the first column (DqTeamwachtrij puts the waiting
1066
+ * time first and links the case title in the second).
1067
+ *
1068
+ * @return {number} The index in `effectiveColumns`.
1069
+ * @spec openspec/changes/screens-index-board-gaps/specs/index-list-board-look/spec.md#requirement-a-column-can-carry-the-row-link
1070
+ */
1071
+ titleColumnIndex() {
1072
+ const index = this.effectiveColumns.findIndex((c) => c && c.link === true)
1073
+ return index === -1 ? 0 : index
1074
+ },
1075
+
1048
1076
  growColumnKey() {
1049
1077
  if (!this.fitWidth || this.effectiveColumns.length === 0) {
1050
1078
  return null
@@ -1365,13 +1393,14 @@ export default {
1365
1393
 
1366
1394
  /**
1367
1395
  * Whether a row's record changed since the user last looked
1368
- * (`@self.unread`). A row without the marker renders as it always did.
1396
+ * (`@self.unread`), when `showUnread` is on. A row without the marker
1397
+ * renders as it always did.
1369
1398
  *
1370
1399
  * @param {object} row The row.
1371
1400
  * @return {boolean} True when the row is unread.
1372
1401
  */
1373
1402
  isUnreadRow(row) {
1374
- return !!(row && row['@self'] && row['@self'].unread === true)
1403
+ return this.showUnread && !!(row && row['@self'] && row['@self'].unread === true)
1375
1404
  },
1376
1405
 
1377
1406
  /**
@@ -21,6 +21,8 @@
21
21
  :title="title"
22
22
  :description="headerDescription"
23
23
  :icon="showTitleIcon && !isBoardLook ? resolvedIcon : ''"
24
+ :breadcrumb="breadcrumb"
25
+ :meta="headerMetaText"
24
26
  :visuallyHidden="!showTitle">
25
27
  <!-- The board's header buttons (`headerButtons`), beside the
26
28
  title. Declaring them takes the Views and Actions menus
@@ -734,6 +736,7 @@
734
736
  :columnOverrides="columnOverrides"
735
737
  :rowClass="rowClass"
736
738
  :rowTitle="rowTitle"
739
+ :showUnread="showUnreadRows"
737
740
  :filterable="tableHeaderFilters"
738
741
  :activeFilters="effectiveActiveFilters"
739
742
  :filterRegister="typeof register === 'string' ? register : ''"
@@ -1609,6 +1612,31 @@ export default {
1609
1612
  required: true,
1610
1613
  },
1611
1614
 
1615
+ /**
1616
+ * A breadcrumb under the title (manifest `config.breadcrumb`): one
1617
+ * crumb `{ label, route?, params?, href?, icon?, separator? }` or a
1618
+ * list; the title follows as the current crumb. Drawn as the detail
1619
+ * header's breadcrumb row. Null (the default) draws none.
1620
+ *
1621
+ * @type {object|Array<object>|null}
1622
+ * @spec openspec/changes/page-header-breadcrumb/specs/page-header/spec.md#requirement-a-list-dashboard-or-settings-page-can-show-a-breadcrumb-and-a-meta-note
1623
+ */
1624
+ breadcrumb: {
1625
+ type: [Object, Array],
1626
+ default: null,
1627
+ },
1628
+
1629
+ /**
1630
+ * A note after the breadcrumb and a middle dot (manifest `config.headerMeta`),
1631
+ * such as "{total} open": `{total}` and `{shown}` are the list's counts.
1632
+ *
1633
+ * @spec openspec/changes/page-header-breadcrumb/specs/page-header/spec.md#requirement-a-list-dashboard-or-settings-page-can-show-a-breadcrumb-and-a-meta-note
1634
+ */
1635
+ headerMeta: {
1636
+ type: String,
1637
+ default: '',
1638
+ },
1639
+
1612
1640
  /** Optional description shown below the title */
1613
1641
  description: {
1614
1642
  type: String,
@@ -2232,7 +2260,7 @@ export default {
2232
2260
  * can open is an ordinary segment; one it cannot (no board columns, no
2233
2261
  * map configured) is drawn disabled, named "<mode>: not available on
2234
2262
  * this list". Fed from `pages[].config.viewSwitch`. Ignored without the
2235
- * board look, and absent the switch is as before.
2263
+ * board look; absent, the board look draws all four.
2236
2264
  *
2237
2265
  * @type {Array<'table' | 'cards' | 'board' | 'map'>}
2238
2266
  * @spec openspec/changes/screens-view-switch-parity/specs/view-switch-board-look/spec.md#requirement-the-manifest-can-draw-segments-the-page-cannot-open
@@ -3555,6 +3583,8 @@ export default {
3555
3583
  data() {
3556
3584
  return {
3557
3585
  currentViewMode: this.viewMode,
3586
+ /** Whether `?_view=` names a mode the page did not offer yet, to open once it does. */
3587
+ routeViewPending: false,
3558
3588
  /** The column field the reader picked in the board's column switch; '' = the default. */
3559
3589
  boardColumnFieldChoice: '',
3560
3590
  /** The rows a reference column field is built from, by field. */
@@ -4118,11 +4148,14 @@ export default {
4118
4148
  * @spec openspec/changes/screens-view-switch-parity/specs/view-switch-board-look/spec.md#requirement-the-manifest-can-draw-segments-the-page-cannot-open
4119
4149
  */
4120
4150
  viewSwitchDisabledModes() {
4121
- if (!this.isBoardLook || !Array.isArray(this.viewSwitch)) {
4151
+ if (!this.isBoardLook) {
4122
4152
  return []
4123
4153
  }
4154
+ // The index boards draw all four segments, so the board look does
4155
+ // too unless the manifest names its own list.
4156
+ const listed = Array.isArray(this.viewSwitch) ? this.viewSwitch : ['table', 'cards', 'board', 'map']
4124
4157
  const offered = [...this.effectiveToggleModes, ...(this.showMapSegment ? ['map'] : [])]
4125
- return this.viewSwitch.filter((mode) => ['table', 'cards', 'board', 'map'].includes(mode) && !offered.includes(mode))
4158
+ return listed.filter((mode) => ['table', 'cards', 'board', 'map'].includes(mode) && !offered.includes(mode))
4126
4159
  },
4127
4160
 
4128
4161
  /**
@@ -4160,6 +4193,17 @@ export default {
4160
4193
  })
4161
4194
  },
4162
4195
 
4196
+ /**
4197
+ * The modes `?_view=` can open: the toggle's modes plus the map when
4198
+ * the page offers it.
4199
+ *
4200
+ * @return {Array<string>} The modes.
4201
+ * @spec openspec/changes/screens-index-board-gaps/specs/index-list-board-look/spec.md#requirement-the-address-opens-a-view-mode-that-arrives-later
4202
+ */
4203
+ routeViewModes() {
4204
+ return [...this.effectiveToggleModes, ...(this.showMapSegment ? ['map'] : [])]
4205
+ },
4206
+
4163
4207
  /**
4164
4208
  * The column fields the board offers (`board.columnFields`), for the
4165
4209
  * column switch (screens-board-reference-columns).
@@ -5380,6 +5424,21 @@ export default {
5380
5424
  return this.isSelfFetchMode ? this.list.pagination.value : this.pagination
5381
5425
  },
5382
5426
 
5427
+ /**
5428
+ * Whether the table marks unread rows. The boards draw every row alike,
5429
+ * so under the board look only a page that offers the Unread lens
5430
+ * (`personalLenses`) marks them; the Nextcloud look always does.
5431
+ *
5432
+ * @spec openspec/changes/screens-index-board-gaps/specs/index-list-board-look/spec.md#requirement-unread-rows-are-marked-only-where-the-reader-asked-for-them
5433
+ * @return {boolean} True when unread rows read in bold with the dot.
5434
+ */
5435
+ showUnreadRows() {
5436
+ if (!this.isBoardLook) {
5437
+ return true
5438
+ }
5439
+ return Array.isArray(this.personalLenses) && this.personalLenses.includes('unread')
5440
+ },
5441
+
5383
5442
  /** Resolved schema OBJECT (for column generation / icons / labels). */
5384
5443
  /**
5385
5444
  * Whether the table shows its header filters. A self-fetching page
@@ -6002,8 +6061,9 @@ export default {
6002
6061
  }
6003
6062
  // The board's header: Export reads "Download" with its icon and
6004
6063
  // the Actions menu reads "Actions", without the manifest saying so.
6005
- if (this.isBoardLook && label === '' && button.action === 'export') {
6006
- label = t('nextcloud-vue', 'Download')
6064
+ // A labelled Export keeps the icon too, unless it names its own.
6065
+ if (this.isBoardLook && button.action === 'export') {
6066
+ label = label || t('nextcloud-vue', 'Download')
6007
6067
  icon = icon || 'Download'
6008
6068
  }
6009
6069
  if (label === '' && button.action === 'actions-menu') {
@@ -6286,6 +6346,22 @@ export default {
6286
6346
  * @spec openspec/changes/screens-table-footer-and-system-dates-parity/specs/index-list-board-look/spec.md#requirement-the-count-reads-in-the-users-language
6287
6347
  * @return {string}
6288
6348
  */
6349
+ /**
6350
+ * The header meta note with the list's counts filled in.
6351
+ *
6352
+ * @spec openspec/changes/page-header-breadcrumb/specs/page-header/spec.md#requirement-a-list-dashboard-or-settings-page-can-show-a-breadcrumb-and-a-meta-note
6353
+ * @return {string}
6354
+ */
6355
+ headerMetaText() {
6356
+ if (!this.headerMeta) {
6357
+ return ''
6358
+ }
6359
+ const total = this.effectivePagination?.total
6360
+ return this.cnTranslate(this.headerMeta)
6361
+ .replace('{shown}', String(this.effectiveObjects.length))
6362
+ .replace('{total}', typeof total === 'number' ? String(total) : '')
6363
+ },
6364
+
6289
6365
  headerDescription() {
6290
6366
  const total = this.effectivePagination?.total
6291
6367
  // Board look: the count line is a template over the rows on this page
@@ -6476,6 +6552,12 @@ export default {
6476
6552
  this.applyViewModeFromRoute()
6477
6553
  },
6478
6554
 
6555
+ routeViewModes() {
6556
+ if (this.routeViewPending) {
6557
+ this.applyViewModeFromRoute()
6558
+ }
6559
+ },
6560
+
6479
6561
  boardFetchKey: {
6480
6562
  immediate: true,
6481
6563
  handler(key) {
@@ -6908,6 +6990,10 @@ export default {
6908
6990
  */
6909
6991
  async loadFilterCounts() {
6910
6992
  const requestKey = this.countRequestKey
6993
+ if (this.isNamedSource) {
6994
+ await this.loadNamedSourceCounts(requestKey)
6995
+ return
6996
+ }
6911
6997
  const schema = this.exportSchemaSlug
6912
6998
  if (requestKey === '' || !this.register || !schema) {
6913
6999
  this.tabCounts = null
@@ -6939,6 +7025,46 @@ export default {
6939
7025
  this.savedViewCounts = viewCounts
6940
7026
  },
6941
7027
 
7028
+ /**
7029
+ * The quick-filter counts of a named source (`entitySource`): each tab
7030
+ * that asks is counted by the source's own `count(config)`, with the
7031
+ * tab's filter over `sourceConfig`. A source without `count` shows
7032
+ * none; a failed count leaves that tab without a number.
7033
+ *
7034
+ * @param {string} requestKey The `countRequestKey` this load answers.
7035
+ * @return {Promise<void>}
7036
+ * @spec openspec/changes/screens-index-board-gaps/specs/index-list-board-look/spec.md#requirement-a-named-source-counts-its-quick-filters
7037
+ */
7038
+ async loadNamedSourceCounts(requestKey) {
7039
+ const count = this.namedSource ? this.namedSource.count : null
7040
+ if (requestKey === '' || typeof count !== 'function') {
7041
+ this.tabCounts = null
7042
+ this.savedViewCounts = null
7043
+ return
7044
+ }
7045
+ const params = (this.$route && this.$route.params) || {}
7046
+ const ctx = typeof this.selfFetchTokenCtx === 'function' ? this.selfFetchTokenCtx() : {}
7047
+ const tabCounts = {}
7048
+ await Promise.all(this.countEntries
7049
+ .filter((entry) => entry.key.startsWith('tab:'))
7050
+ .map(async (entry) => {
7051
+ const config = resolveFilterMap({ ...(this.sourceConfig || {}), ...(entry.filter || {}) }, params, ctx)
7052
+ try {
7053
+ const total = await count(config)
7054
+ if (Number.isFinite(total)) {
7055
+ tabCounts[Number(entry.key.slice(4))] = total
7056
+ }
7057
+ } catch {
7058
+ // No number for this tab.
7059
+ }
7060
+ }))
7061
+ if (requestKey !== this.countRequestKey) {
7062
+ return
7063
+ }
7064
+ this.tabCounts = tabCounts
7065
+ this.savedViewCounts = null
7066
+ },
7067
+
6942
7068
  // ── Split view (case-page-and-list-as-a-place) ──────────────────
6943
7069
 
6944
7070
  /**
@@ -9203,6 +9329,7 @@ export default {
9203
9329
  * @param {string} mode 'table' or 'cards'
9204
9330
  */
9205
9331
  onViewModeChange(mode) {
9332
+ this.routeViewPending = false
9206
9333
  this.currentViewMode = mode
9207
9334
  this.$emit('view-mode-change', mode)
9208
9335
  },
@@ -9212,16 +9339,26 @@ export default {
9212
9339
  * offers it. The key starts with `_`, like `_search` and `_page`, so it
9213
9340
  * is never read as a list filter. A menu entry's `query` can so open a
9214
9341
  * list as its board, on the one page that lists the schema
9215
- * (screens-board-reference-columns).
9342
+ * (screens-board-reference-columns). A mode the page offers only later
9343
+ * (the map, once rows with a geometry are in) opens when it arrives,
9344
+ * unless the reader picked a view first.
9216
9345
  *
9217
9346
  * @return {void}
9218
9347
  * @spec openspec/changes/screens-board-reference-columns/specs/board-view/spec.md#requirement-the-address-can-open-a-list-in-a-view-mode
9348
+ * @spec openspec/changes/screens-index-board-gaps/specs/index-list-board-look/spec.md#requirement-the-address-opens-a-view-mode-that-arrives-later
9219
9349
  */
9220
9350
  applyViewModeFromRoute() {
9221
9351
  const wanted = this.$route?.query?._view
9222
- if (typeof wanted === 'string' && wanted !== this.currentViewMode && this.effectiveToggleModes.includes(wanted)) {
9352
+ if (typeof wanted !== 'string' || wanted === '' || wanted === this.currentViewMode) {
9353
+ this.routeViewPending = false
9354
+ return
9355
+ }
9356
+ if (this.routeViewModes.includes(wanted)) {
9223
9357
  this.currentViewMode = wanted
9358
+ this.routeViewPending = false
9359
+ return
9224
9360
  }
9361
+ this.routeViewPending = true
9225
9362
  },
9226
9363
 
9227
9364
  /**
@@ -0,0 +1,140 @@
1
+ <!--
2
+ SPDX-FileCopyrightText: 2026 Conduction B.V.
3
+ SPDX-License-Identifier: EUPL-1.2
4
+ -->
5
+ <template>
6
+ <div
7
+ v-if="crumbs.length > 0 || meta"
8
+ class="cn-header-trail cn-detail-page__header-row2"
9
+ data-testid="cn-header-trail">
10
+ <CnBreadcrumbs
11
+ v-if="crumbs.length > 0"
12
+ class="cn-detail-page__breadcrumbs cn-detail-page__breadcrumbs--row2"
13
+ :crumbs="crumbs"
14
+ :rootText="crumbs[0].icon === undefined"
15
+ :separator="separator"
16
+ data-testid="cn-header-trail-breadcrumbs" />
17
+ <span
18
+ v-if="crumbs.length > 0 && meta"
19
+ class="cn-detail-page__header-dot"
20
+ aria-hidden="true">&middot;</span>
21
+ <span
22
+ v-if="meta"
23
+ class="cn-detail-page__header-meta"
24
+ data-testid="cn-header-trail-meta">{{ meta }}</span>
25
+ </div>
26
+ </template>
27
+
28
+ <script>
29
+ import { CnBreadcrumbs } from '../CnBreadcrumbs/index.js'
30
+
31
+ /**
32
+ * The breadcrumb and meta line under a page title: "Instellingen / Zaaktypen
33
+ * · 12 zaaktypen". It draws the same markup and classes as the board look's
34
+ * detail header row, so a list, dashboard or settings page carries the
35
+ * detail page's breadcrumb style rather than one of its own.
36
+ *
37
+ * `breadcrumb` is one crumb or a list, each `{ label, route?, params?, href?,
38
+ * icon?, separator? }`. The page title follows as the current crumb.
39
+ */
40
+ export default {
41
+ name: 'CnHeaderTrail',
42
+
43
+ components: { CnBreadcrumbs },
44
+
45
+ inject: {
46
+ cnTranslate: { default: () => (key) => key },
47
+ },
48
+
49
+ props: {
50
+ /**
51
+ * The crumbs before the current page, one object or a list.
52
+ *
53
+ * @type {object|Array<object>|null}
54
+ * @spec openspec/changes/page-header-breadcrumb/specs/page-header/spec.md#requirement-a-list-dashboard-or-settings-page-can-show-a-breadcrumb-and-a-meta-note
55
+ */
56
+ breadcrumb: {
57
+ type: [Object, Array],
58
+ default: null,
59
+ },
60
+
61
+ /** The current crumb: the page title, already translated. */
62
+ current: {
63
+ type: String,
64
+ default: '',
65
+ },
66
+
67
+ /**
68
+ * The note after the crumbs and a middle dot, already resolved.
69
+ *
70
+ * @spec openspec/changes/page-header-breadcrumb/specs/page-header/spec.md#requirement-a-list-dashboard-or-settings-page-can-show-a-breadcrumb-and-a-meta-note
71
+ */
72
+ meta: {
73
+ type: String,
74
+ default: '',
75
+ },
76
+
77
+ /**
78
+ * Translate function for the crumb labels; falls back to the injected
79
+ * `cnTranslate`.
80
+ *
81
+ * @type {((key: string) => string)|null}
82
+ */
83
+ translate: {
84
+ type: Function,
85
+ default: null,
86
+ },
87
+ },
88
+
89
+ computed: {
90
+ /**
91
+ * The declared crumbs that carry a label.
92
+ *
93
+ * @return {Array<object>}
94
+ */
95
+ declared() {
96
+ const list = Array.isArray(this.breadcrumb) ? this.breadcrumb : (this.breadcrumb ? [this.breadcrumb] : [])
97
+ return list.filter((crumb) => crumb && typeof crumb.label === 'string' && crumb.label !== '')
98
+ },
99
+
100
+ /**
101
+ * The crumbs for CnBreadcrumbs: each declared crumb as a link, then the
102
+ * current page. Empty without a declared crumb.
103
+ *
104
+ * @spec openspec/changes/page-header-breadcrumb/specs/page-header/spec.md#requirement-a-list-dashboard-or-settings-page-can-show-a-breadcrumb-and-a-meta-note
105
+ * @return {Array<object>}
106
+ */
107
+ crumbs() {
108
+ if (this.declared.length === 0) {
109
+ return []
110
+ }
111
+ const out = this.declared.map((crumb) => {
112
+ const entry = { label: (this.translate ?? this.cnTranslate)(crumb.label) }
113
+ if (typeof crumb.icon === 'string' && crumb.icon !== '') {
114
+ entry.icon = crumb.icon
115
+ }
116
+ if (crumb.route) {
117
+ entry.to = { name: crumb.route, ...(crumb.params ? { params: crumb.params } : {}) }
118
+ } else if (crumb.href) {
119
+ entry.href = crumb.href
120
+ }
121
+ return entry
122
+ })
123
+ if (this.current) {
124
+ out.push({ label: this.current })
125
+ }
126
+ return out
127
+ },
128
+
129
+ /**
130
+ * The text between the crumbs: the first crumb's `separator`, else `/`.
131
+ *
132
+ * @return {string}
133
+ */
134
+ separator() {
135
+ const declared = this.declared[0] && this.declared[0].separator
136
+ return typeof declared === 'string' && declared !== '' ? declared : '/'
137
+ },
138
+ },
139
+ }
140
+ </script>
@@ -81,3 +81,14 @@ With extra content (e.g., a badge or status indicator):
81
81
  </template>
82
82
  </CnPageHeader>
83
83
  ```
84
+
85
+ Breadcrumb and meta: `breadcrumb` (one crumb `{ label, route?, params?, href?, icon?, separator? }` or a list) draws a
86
+ line under the title in the detail header's breadcrumb style, with the title as the current crumb; `meta` follows after a
87
+ middle dot:
88
+
89
+ ```vue
90
+ <CnPageHeader
91
+ title="Zaaktypen"
92
+ :breadcrumb="{ label: 'Instellingen', href: '#instellingen' }"
93
+ meta="14 zaaktypen" />
94
+ ```
@@ -13,6 +13,12 @@
13
13
  <h1 class="cn-page-header__title" data-testid="cn-page-title">
14
14
  {{ resolvedTitle }}
15
15
  </h1>
16
+ <CnHeaderTrail
17
+ v-if="!visuallyHidden"
18
+ :breadcrumb="breadcrumb"
19
+ :current="resolvedTitle"
20
+ :meta="resolvedMeta"
21
+ :translate="effectiveTranslate" />
16
22
  <p v-if="description && !visuallyHidden" class="cn-page-header__description" data-testid="cn-page-description">
17
23
  {{ resolvedDescription }}
18
24
  </p>
@@ -22,6 +28,7 @@
22
28
  </template>
23
29
 
24
30
  <script>
31
+ import CnHeaderTrail from './CnHeaderTrail.vue'
25
32
  import { CnIcon } from '../CnIcon/index.js'
26
33
 
27
34
  /**
@@ -64,6 +71,7 @@ export default {
64
71
  name: 'CnPageHeader',
65
72
 
66
73
  components: {
74
+ CnHeaderTrail,
67
75
  CnIcon,
68
76
  },
69
77
 
@@ -90,6 +98,31 @@ export default {
90
98
  default: '',
91
99
  },
92
100
 
101
+ /**
102
+ * A breadcrumb under the title: one crumb or a list, each
103
+ * `{ label, route?, params?, href?, icon?, separator? }`. The title
104
+ * follows as the current crumb. Drawn as the board look's detail
105
+ * header row.
106
+ *
107
+ * @type {object|Array<object>|null}
108
+ * @spec openspec/changes/page-header-breadcrumb/specs/page-header/spec.md#requirement-a-list-dashboard-or-settings-page-can-show-a-breadcrumb-and-a-meta-note
109
+ */
110
+ breadcrumb: {
111
+ type: [Object, Array],
112
+ default: null,
113
+ },
114
+
115
+ /**
116
+ * A note after the breadcrumb and a middle dot, such as "12 open". Run
117
+ * through the host translate function.
118
+ *
119
+ * @spec openspec/changes/page-header-breadcrumb/specs/page-header/spec.md#requirement-a-list-dashboard-or-settings-page-can-show-a-breadcrumb-and-a-meta-note
120
+ */
121
+ meta: {
122
+ type: String,
123
+ default: '',
124
+ },
125
+
93
126
  /** Optional MDI icon name (rendered via CnIcon) */
94
127
  icon: {
95
128
  type: String,
@@ -153,6 +186,15 @@ export default {
153
186
  resolvedDescription() {
154
187
  return this.description ? this.effectiveTranslate(this.description) : this.description
155
188
  },
189
+
190
+ /**
191
+ * The meta note run through the host translate function.
192
+ *
193
+ * @return {string}
194
+ */
195
+ resolvedMeta() {
196
+ return this.meta ? this.effectiveTranslate(this.meta) : ''
197
+ },
156
198
  },
157
199
  }
158
200
  </script>
@@ -8,13 +8,13 @@
8
8
  Board variant (`variant: "board"`): the footer of the table card as the
9
9
  screens draw it. The count text at the start, numbered 34px links at
10
10
  the end, "Previous" only after the first page and "Next" only before
11
- the last. No First, no Last, no page-size select.
11
+ the last (inert on a single page). No First, no Last, no page-size select.
12
12
  -->
13
13
  <template v-if="isBoardVariant">
14
14
  <span class="cn-pagination__page-info cn-pagination__board-count" data-testid="cn-pagination-board-count">
15
15
  {{ boardCountText }}
16
16
  </span>
17
- <nav v-if="totalPages > 1" class="cn-pagination__board-pages" :aria-label="t('nextcloud-vue', 'Pagination')">
17
+ <nav class="cn-pagination__board-pages" :aria-label="t('nextcloud-vue', 'Pagination')">
18
18
  <button
19
19
  v-if="currentPage > 1"
20
20
  type="button"
@@ -43,6 +43,14 @@
43
43
  @click="changePage(currentPage + 1)">
44
44
  {{ nextLabel }}
45
45
  </button>
46
+ <!-- A single page still draws its "1" and an inert Next, as the boards do. -->
47
+ <span
48
+ v-else-if="totalPages <= 1"
49
+ class="cn-pagination__board-link cn-pagination__board-link--wide cn-pagination__board-link--disabled"
50
+ aria-disabled="true"
51
+ data-testid="cn-pagination-next">
52
+ {{ nextLabel }}
53
+ </span>
46
54
  </nav>
47
55
  </template>
48
56
 
@@ -727,9 +727,9 @@ export default {
727
727
  },
728
728
 
729
729
  /**
730
- * The row's due text (`due`: `{ field, todayLabel?, overdueLabel?, emptyText? }`):
731
- * "3 dagen te laat" in the danger tone, "Vandaag", the date otherwise,
732
- * `emptyText` when the field is empty.
730
+ * The row's due text (`due`: `{ field, todayLabel?, overdueLabel?, upcomingLabel?, upcomingWeeksLabel?, upcomingWeeksFrom?, emptyText? }`):
731
+ * "3 dagen te laat" in the danger tone, "Vandaag", the date or the time
732
+ * left ("nog 4 dagen") otherwise, `emptyText` when the field is empty.
733
733
  *
734
734
  * @param {object} row The row.
735
735
  * @return {{text: string, tone: string}|null} The text and tone, or null.