@conduction/nextcloud-vue 2.137.0 → 2.139.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/CnCellRenderer/CnCellRenderer.vue.js +1 -1
  2. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js +6 -4
  3. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js.map +1 -1
  4. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js.map +1 -1
  5. package/dist/esm/components/CnChartWidget/CnChartWidget.vue.js +1 -1
  6. package/dist/esm/components/CnChartWidget/CnChartWidget.vue2.js.map +1 -1
  7. package/dist/esm/components/CnChartWidget/CnChartWidget.vue3.js.map +1 -1
  8. package/dist/esm/components/CnDetailCard/CnDetailCard.vue.js +1 -1
  9. package/dist/esm/components/CnDetailCard/CnDetailCard.vue2.js +6 -0
  10. package/dist/esm/components/CnDetailCard/CnDetailCard.vue2.js.map +1 -1
  11. package/dist/esm/components/CnDetailCard/CnDetailCard.vue3.js +24 -7
  12. package/dist/esm/components/CnDetailCard/CnDetailCard.vue3.js.map +1 -1
  13. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue.js +1 -1
  14. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js +2 -0
  15. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
  16. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
  17. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue.js +1 -1
  18. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js.map +1 -1
  19. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js.map +1 -1
  20. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue.js +1 -1
  21. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js +43 -1
  22. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js.map +1 -1
  23. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js.map +1 -1
  24. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +133 -6
  25. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  26. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +25 -17
  27. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  28. package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue2.js +2 -0
  29. package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue2.js.map +1 -1
  30. package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue3.js +13 -2
  31. package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue3.js.map +1 -1
  32. package/dist/esm/components/CnMailTab/dashboardRegistration.js +4 -0
  33. package/dist/esm/components/CnMailTab/dashboardRegistration.js.map +1 -1
  34. package/dist/esm/components/CnNotificationMatrix/CnNotificationMatrix.vue.js +1 -1
  35. package/dist/esm/components/CnNotificationMatrix/CnNotificationMatrix.vue2.js.map +1 -1
  36. package/dist/esm/components/CnNotificationMatrix/CnNotificationMatrix.vue3.js.map +1 -1
  37. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue.js +1 -1
  38. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +1 -1
  39. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
  40. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js.map +1 -1
  41. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue.js +1 -1
  42. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js +2 -0
  43. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js.map +1 -1
  44. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js +2 -1
  45. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js.map +1 -1
  46. package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue.js +1 -1
  47. package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue2.js +6 -0
  48. package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue2.js.map +1 -1
  49. package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue3.js +3 -3
  50. package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue3.js.map +1 -1
  51. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
  52. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +16 -0
  53. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
  54. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js +11 -5
  55. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
  56. package/dist/esm/components/CnSavedViewMenuSettings/CnSavedViewMenuSettings.vue.js +1 -1
  57. package/dist/esm/components/CnSavedViewMenuSettings/CnSavedViewMenuSettings.vue2.js.map +1 -1
  58. package/dist/esm/components/CnSavedViewMenuSettings/CnSavedViewMenuSettings.vue3.js +1 -0
  59. package/dist/esm/components/CnSavedViewMenuSettings/CnSavedViewMenuSettings.vue3.js.map +1 -1
  60. package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue.js +1 -1
  61. package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue2.js.map +1 -1
  62. package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue3.js.map +1 -1
  63. package/dist/esm/composables/indexSources.js +78 -12
  64. package/dist/esm/composables/indexSources.js.map +1 -1
  65. package/dist/esm/l10n/en.json.js +12 -1
  66. package/dist/esm/l10n/en.json.js.map +1 -1
  67. package/dist/esm/l10n/nl.json.js +12 -1
  68. package/dist/esm/l10n/nl.json.js.map +1 -1
  69. package/dist/esm/library-style.js +1 -1
  70. package/dist/esm/nextcloud-vue.css +685 -512
  71. package/dist/esm/utils/resolveFilterTokens.js +9 -0
  72. package/dist/esm/utils/resolveFilterTokens.js.map +1 -1
  73. package/dist/esm/utils/sentinelTokens.js +1 -1
  74. package/dist/esm/utils/sentinelTokens.js.map +1 -1
  75. package/dist/nextcloud-vue.cjs.js +580 -236
  76. package/dist/nextcloud-vue.cjs.js.map +2 -2
  77. package/dist/nextcloud-vue.css +685 -512
  78. package/l10n/en.json +12 -1
  79. package/l10n/nl.json +12 -1
  80. package/package.json +1 -1
  81. package/src/components/CnCellRenderer/CnCellRenderer.vue +6 -4
  82. package/src/components/CnChartWidget/CnChartWidget.vue +4 -0
  83. package/src/components/CnDetailCard/CnDetailCard.md +1 -0
  84. package/src/components/CnDetailCard/CnDetailCard.vue +25 -1
  85. package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +2 -0
  86. package/src/components/CnFactsWidget/CnFactsWidget.vue +6 -4
  87. package/src/components/CnFilesBrowser/CnFilesBrowser.vue +54 -1
  88. package/src/components/CnIndexPage/CnIndexPage.vue +135 -7
  89. package/src/components/CnIndexSidebar/CnIndexSidebar.vue +10 -2
  90. package/src/components/CnMailTab/dashboardRegistration.js +4 -0
  91. package/src/components/CnNotificationMatrix/CnNotificationMatrix.vue +4 -0
  92. package/src/components/CnObjectListWidget/CnObjectListWidget.vue +1 -1
  93. package/src/components/CnObjectSidebar/CnFilesTab.vue +3 -0
  94. package/src/components/CnOrderedListField/CnOrderedListField.vue +11 -1
  95. package/src/components/CnRecordListWidget/CnRecordListWidget.vue +22 -3
  96. package/src/components/CnSavedViewMenuSettings/CnSavedViewMenuSettings.vue +1 -0
  97. package/src/components/CnUserSettingsDialog/CnUserSettingsDialog.vue +3 -0
  98. package/src/composables/indexSources.js +78 -12
  99. package/src/css/dialog.css +26 -0
  100. package/src/css/index-page.css +5 -0
  101. package/src/css/look-board-detail.css +36 -0
  102. package/src/css/look-board-index.css +21 -0
  103. package/src/css/look-board.css +47 -0
  104. package/src/utils/resolveFilterTokens.js +9 -0
  105. package/src/utils/sentinelTokens.js +1 -1
package/l10n/en.json CHANGED
@@ -2377,7 +2377,10 @@
2377
2377
  "Talk to {agent}": "Talk to {agent}",
2378
2378
  "Search a person or group": "Search a person or group",
2379
2379
  "Now: by {order}.": "Now: by {order}.",
2380
- "You save the order along with a view.": "You save the order along with a view."
2380
+ "You save the order along with a view.": "You save the order along with a view.",
2381
+ "me": "me",
2382
+ "Open profile": "Open profile",
2383
+ "Link a contact from your address book.": "Link a contact from your address book."
2381
2384
  },
2382
2385
  "plurals": {
2383
2386
  "{count} member": [
@@ -2578,6 +2581,14 @@
2578
2581
  "within %n day": [
2579
2582
  "within %n day",
2580
2583
  "within %n days"
2584
+ ],
2585
+ "%n other": [
2586
+ "%n other",
2587
+ "%n others"
2588
+ ],
2589
+ "{names} has no location, so it is not on the map.": [
2590
+ "{names} has no location, so it is not on the map.",
2591
+ "{names} have no location, so they are not on the map."
2581
2592
  ]
2582
2593
  }
2583
2594
  }
package/l10n/nl.json CHANGED
@@ -2627,7 +2627,10 @@
2627
2627
  "Talk to {agent}": "Praten met {agent}",
2628
2628
  "Search a person or group": "Zoek een persoon of groep",
2629
2629
  "Now: by {order}.": "Nu: op {order}.",
2630
- "You save the order along with a view.": "De sortering bewaart u mee in een weergave."
2630
+ "You save the order along with a view.": "De sortering bewaart u mee in een weergave.",
2631
+ "me": "ik",
2632
+ "Open profile": "Profiel openen",
2633
+ "Link a contact from your address book.": "Koppel een contact uit je adresboek."
2631
2634
  },
2632
2635
  "plurals": {
2633
2636
  "{count} member": [
@@ -2845,6 +2848,14 @@
2845
2848
  "within %n day": [
2846
2849
  "binnen %n dag",
2847
2850
  "binnen %n dagen"
2851
+ ],
2852
+ "%n other": [
2853
+ "%n andere",
2854
+ "%n andere"
2855
+ ],
2856
+ "{names} has no location, so it is not on the map.": [
2857
+ "{names} heeft geen locatie, dus staat niet op de kaart.",
2858
+ "{names} hebben geen locatie, dus staan ze niet op de kaart."
2848
2859
  ]
2849
2860
  }
2850
2861
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@conduction/nextcloud-vue",
3
- "version": "2.137.0",
3
+ "version": "2.139.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": {
@@ -639,7 +639,7 @@ export default {
639
639
  * type-aware rendering, so a manifest column can opt into currency or a
640
640
  * colour swatch without registering a function.
641
641
  *
642
- * @type {{style?: 'currency'|'number'|'percent'|'duration'|'swatch', currency?: string, decimals?: number, unit?: 'milliseconds'|'seconds'|'minutes'|'hours', prefix?: string, suffix?: string, colorField?: string}}
642
+ * @type {{style?: 'currency'|'number'|'decimal'|'percent'|'duration'|'swatch', currency?: string, decimals?: number, unit?: 'milliseconds'|'seconds'|'minutes'|'hours', prefix?: string, suffix?: string, colorField?: string}}
643
643
  */
644
644
  format: {
645
645
  type: Object,
@@ -1709,9 +1709,9 @@ export default {
1709
1709
  return typeof s === 'string' ? s : null
1710
1710
  },
1711
1711
 
1712
- /** True when a built-in NUMERIC format (currency/number/percent/duration) applies. */
1712
+ /** True when a built-in NUMERIC format (currency/number/decimal/percent/duration) applies. */
1713
1713
  hasBuiltinFormat() {
1714
- return ['currency', 'number', 'percent', 'duration'].includes(this.formatStyle)
1714
+ return ['currency', 'number', 'decimal', 'percent', 'duration'].includes(this.formatStyle)
1715
1715
  },
1716
1716
 
1717
1717
  /** True when this cell renders as a colour swatch (`format.style:"swatch"`). */
@@ -1916,9 +1916,11 @@ export default {
1916
1916
  if (!Number.isFinite(num)) {
1917
1917
  return formatValue(this.value, this.property, { truncate: this.truncate })
1918
1918
  }
1919
+ // `decimal` defaults to one fraction digit, as formatMetricValue does.
1920
+ const defaultDecimals = { currency: 2, decimal: 1 }
1919
1921
  const decimals = Number.isFinite(fmt.decimals)
1920
1922
  ? fmt.decimals
1921
- : (fmt.style === 'currency' ? 2 : 0)
1923
+ : (defaultDecimals[fmt.style] ?? 0)
1922
1924
  let body
1923
1925
  if (fmt.style === 'currency') {
1924
1926
  // `format.currencyField` reads the code from the row's own field.
@@ -2964,10 +2964,14 @@ export default {
2964
2964
  display: contents;
2965
2965
  }
2966
2966
 
2967
+ /* A column takes its share of the width (minmax(0, 1fr)), so a long phase
2968
+ name breaks inside its own column instead of running into the next. */
2967
2969
  .cn-chart-widget__matrix-head {
2968
2970
  --nldesign-color-on-surface: var(--color-text-maxcontrast);
2969
2971
  color: var(--color-text-maxcontrast);
2970
2972
  padding: 6px;
2973
+ hyphens: auto;
2974
+ overflow-wrap: anywhere;
2971
2975
  }
2972
2976
 
2973
2977
  .cn-chart-widget__matrix-label {
@@ -84,6 +84,7 @@ With footer slot:
84
84
  | Prop | Type | Default | Description |
85
85
  |------|------|---------|-------------|
86
86
  | `icon` | Object\|Function | `null` | Optional MDI icon component rendered in the card header |
87
+ | `subtitle` | String | `''` | A muted line under the title, as given (translate before passing). No line when empty. |
87
88
 
88
89
  | Slot | Description |
89
90
  |------|-------------|
@@ -22,7 +22,13 @@
22
22
  :size="20"
23
23
  class="cn-detail-card__icon" />
24
24
  </slot>
25
- <h3 class="cn-detail-card__title">
25
+ <div v-if="subtitle" class="cn-detail-card__heading">
26
+ <h3 class="cn-detail-card__title">
27
+ {{ title }}
28
+ </h3>
29
+ <span class="cn-detail-card__subtitle" data-testid="cn-detail-card-subtitle">{{ subtitle }}</span>
30
+ </div>
31
+ <h3 v-else class="cn-detail-card__title">
26
32
  {{ title }}
27
33
  </h3>
28
34
  </div>
@@ -125,6 +131,12 @@ export default {
125
131
  default: '',
126
132
  },
127
133
 
134
+ /** A muted line under the title, as given (translate before passing). */
135
+ subtitle: {
136
+ type: String,
137
+ default: '',
138
+ },
139
+
128
140
  /** Optional MDI icon component for the header */
129
141
  icon: {
130
142
  type: [Object, Function],
@@ -247,6 +259,18 @@ export default {
247
259
  flex-shrink: 0;
248
260
  }
249
261
 
262
+ .cn-detail-card__heading {
263
+ display: flex;
264
+ flex-direction: column;
265
+ min-width: 0;
266
+ }
267
+
268
+ .cn-detail-card__subtitle {
269
+ font-size: 13px;
270
+ line-height: 1.3;
271
+ color: var(--color-text-maxcontrast);
272
+ }
273
+
250
274
  .cn-detail-card__title {
251
275
  font-weight: 600;
252
276
  font-size: 14px;
@@ -1444,6 +1444,7 @@ export default {
1444
1444
  objectId: this.objectId,
1445
1445
  object: this.object,
1446
1446
  config: unwrapAppConfig(this.cnAppConfig),
1447
+ context: (this.cnDetailVisibilityRows && this.cnDetailVisibilityRows.value) || null,
1447
1448
  })
1448
1449
  },
1449
1450
 
@@ -1510,6 +1511,7 @@ export default {
1510
1511
  // prints its title as given.
1511
1512
  title: this.translateLabel(this.widget?.title || ''),
1512
1513
  ...this.leafTitleLabel,
1514
+ ...(this.widgetSubtitle ? { subtitle: this.translateLabel(this.widgetSubtitle) } : {}),
1513
1515
  // A provider opting into `bareWidget` is being rendered where the
1514
1516
  // tab strip already supplies the card and the title, so tell it to
1515
1517
  // drop its own. Without this the panel gets a titled card inside
@@ -1845,11 +1845,13 @@ export default {
1845
1845
  display: contents;
1846
1846
  }
1847
1847
 
1848
- /* Nextcloud core styles `dt` and `dd` (text-align end, 12px padding), which
1849
- pushed the label to the right of its column and added 12px around both. The
1850
- class is doubled so these resets outrank that rule, and a dossiq-style
1851
- `:deep()` override in an app is not needed. */
1848
+ /* Nextcloud core styles `dt` and `dd` (text-align end, 12px padding, a
1849
+ 130px-wide `dt`), which pushed the label to the right of its column, added
1850
+ 12px around both and widened the label column to 130px. The class is
1851
+ doubled so these resets outrank that rule, and a dossiq-style `:deep()`
1852
+ override in an app is not needed. */
1852
1853
  .cn-facts-widget__label.cn-facts-widget__label {
1854
+ width: auto;
1853
1855
  margin: 0;
1854
1856
  padding: 0;
1855
1857
  text-align: start;
@@ -1128,6 +1128,21 @@ export default {
1128
1128
  default: '',
1129
1129
  },
1130
1130
 
1131
+ /**
1132
+ * The second line as a list of fields joined with " · ", a field
1133
+ * without a value left out: the `nameSecondary` tokens without their
1134
+ * braces (`["type", "size", "modified|date"]`, a row-data field such as
1135
+ * `"senderName"`). Or a list of variants `{ when, fields }`: the first
1136
+ * whose `when` is `true`, or that has no `when`, is used (a case type
1137
+ * flag read through `@context`). Takes precedence over `nameSecondary`.
1138
+ *
1139
+ * @spec openspec/changes/files-second-line-fields/specs/files-second-line-fields/spec.md#requirement-a-files-second-line-is-a-list-of-fields
1140
+ */
1141
+ secondLine: {
1142
+ type: Array,
1143
+ default: null,
1144
+ },
1145
+
1131
1146
  /** The line `dropHint` shows. */
1132
1147
  dropHintLabel: {
1133
1148
  type: String,
@@ -1526,6 +1541,26 @@ export default {
1526
1541
  *
1527
1542
  * @return {Array<object>} The file nodes.
1528
1543
  */
1544
+ /**
1545
+ * The fields `secondLine` names for this browser: the list itself, or
1546
+ * the fields of its first variant whose `when` is `true` or absent.
1547
+ * Null without `secondLine` or a matching variant.
1548
+ *
1549
+ * @return {Array<string>|null}
1550
+ * @spec openspec/changes/files-second-line-fields/specs/files-second-line-fields/spec.md#requirement-a-files-second-line-is-a-list-of-fields
1551
+ */
1552
+ secondLineFields() {
1553
+ const list = Array.isArray(this.secondLine) ? this.secondLine : []
1554
+ if (list.length === 0) {
1555
+ return null
1556
+ }
1557
+ if (list.every((item) => typeof item === 'string')) {
1558
+ return list
1559
+ }
1560
+ const variant = list.find((item) => item && typeof item === 'object' && Array.isArray(item.fields) && (!('when' in item) || item.when === true))
1561
+ return variant ? variant.fields.filter((field) => typeof field === 'string') : null
1562
+ },
1563
+
1529
1564
  /**
1530
1565
  * The bulk bar's count text.
1531
1566
  *
@@ -2079,7 +2114,8 @@ export default {
2079
2114
  * @return {string}
2080
2115
  */
2081
2116
  nameSecondaryOf(node) {
2082
- if (!this.nameSecondary || this.isFolder(node)) {
2117
+ const fields = this.secondLineFields
2118
+ if ((!this.nameSecondary && fields === null) || this.isFolder(node)) {
2083
2119
  return ''
2084
2120
  }
2085
2121
  const byId = this.resolvedRowData && typeof this.resolvedRowData === 'object' ? this.resolvedRowData : {}
@@ -2093,6 +2129,12 @@ export default {
2093
2129
  type: this.typeLabelOf(node),
2094
2130
  ...(owner ? { owner } : {}),
2095
2131
  }
2132
+ if (fields !== null) {
2133
+ return fields
2134
+ .map((field) => fillTemplate(`{${field}}`, object))
2135
+ .filter((text) => text !== '')
2136
+ .join(' · ')
2137
+ }
2096
2138
  return fillTemplate(this.cnTranslate(this.nameSecondary), object)
2097
2139
  },
2098
2140
 
@@ -3236,6 +3278,17 @@ export default {
3236
3278
  font-size: 14px;
3237
3279
  }
3238
3280
 
3281
+ /* Core's apps.css draws every dt/dd inline-block with a fixed, end-aligned dt.
3282
+ @spec openspec/changes/files-side-facts-core-dl/specs/files-side-facts-core-dl/spec.md#requirement-a-file-side-cards-facts-are-not-drawn-by-cores-dl-rules */
3283
+ .cn-files-browser__side-facts dt,
3284
+ .cn-files-browser__side-facts dd {
3285
+ display: block;
3286
+ width: auto;
3287
+ padding: 0;
3288
+ text-align: start;
3289
+ white-space: normal;
3290
+ }
3291
+
3239
3292
  .cn-files-browser__side-facts dt {
3240
3293
  color: var(--color-text-maxcontrast);
3241
3294
  }
@@ -1,6 +1,6 @@
1
1
  <template>
2
2
  <div class="cn-index-page"
3
- :class="[lookClass, { 'cn-index-page--with-side-panel': hasSidePanel, 'cn-index-page--quick-filters-toolbar': effectiveQuickFilterPlacement === 'toolbar' }]"
3
+ :class="[lookClass, { 'cn-index-page--with-side-panel': hasSidePanel, 'cn-index-page--sidebar-open': sidebarPanelOpen, 'cn-index-page--quick-filters-toolbar': effectiveQuickFilterPlacement === 'toolbar' }]"
4
4
  data-testid="cn-index-page"
5
5
  @keydown="onListKeydown">
6
6
  <!-- Header — overridable via #header slot. CnPageHeader ALWAYS renders:
@@ -172,7 +172,7 @@
172
172
  :maxVisible="quickFilterMaxVisible"
173
173
  :fit="false"
174
174
  :multiple="quickFilterMultiple"
175
- :activeIndex="activeQuickFilterIndex"
175
+ :activeIndex="shownQuickFilterIndex"
176
176
  :selectedIndices="selectedQuickFilterIndices"
177
177
  :counts="effectiveTabCounts"
178
178
  @update:activeIndex="onQuickFilterChange"
@@ -1034,6 +1034,7 @@
1034
1034
  </ol>
1035
1035
  <div v-if="mapList.missing.length > 0" class="cn-index-page__map-missing" data-testid="cn-index-map-missing">
1036
1036
  <strong>{{ mapMissingTitle }}</strong>
1037
+ <span v-if="mapMissingNote" class="cn-index-page__map-missing-note" data-testid="cn-index-map-missing-note">{{ mapMissingNote }}</span>
1037
1038
  <NcButton variant="tertiary-no-background" data-testid="cn-index-map-missing-table" @click="onViewModeChange('table')">
1038
1039
  {{ mapMissingLink }}
1039
1040
  </NcButton>
@@ -1459,6 +1460,7 @@ import { countLine } from '../../utils/countLine.js'
1459
1460
  import { reportBindingProblems } from '../../utils/diagnostics.js'
1460
1461
  import { fetchGroupedCounts } from '../../utils/fetchAggregate.js'
1461
1462
  import { fetchFilterCounts, fetchListTotal } from '../../utils/fetchFilterCounts.js'
1463
+ import { fillTemplate } from '../../utils/fillTemplate.js'
1462
1464
  import { listText, rangeChipText, rangeEnds, relativeRangeText } from '../../utils/filterRangeLabel.js'
1463
1465
  import { unwrapAppConfig } from '../../utils/formatMetric.js'
1464
1466
  import { groupDisplayName, loadGroupDisplayName } from '../../utils/groupAutocomplete.js'
@@ -2238,6 +2240,21 @@ export default {
2238
2240
  default: '',
2239
2241
  },
2240
2242
 
2243
+ /**
2244
+ * Board look: the count line while a saved view is applied (manifest
2245
+ * `config.savedViewCountText`), a template as `countText` that may name
2246
+ * the view's conditions as `{view}`: its description, else its filters
2247
+ * in words (DqFilterInMenu "3 van 48 lopende zaken · termijn binnen 7
2248
+ * dagen, van mij · gesorteerd op termijn"). A view's own `countText`
2249
+ * override and a scope's come first.
2250
+ *
2251
+ * @spec openspec/changes/applied-view-lights-its-lens/specs/applied-view-lights-its-lens/spec.md
2252
+ */
2253
+ savedViewCountText: {
2254
+ type: String,
2255
+ default: '',
2256
+ },
2257
+
2241
2258
  /**
2242
2259
  * Board look: a count line per view (manifest `config.countTextByView`),
2243
2260
  * `{ table?, cards?, list?, board?, map? }`, each a template as
@@ -4578,6 +4595,8 @@ export default {
4578
4595
  savedViewCounts: null,
4579
4596
  // The id (or slug) of the saved view that is applied; its chip shows selected under the board look.
4580
4597
  appliedSavedViewId: '',
4598
+ /** Whether the reader picked a lens chip after the applied view (then their chip shows). */
4599
+ lensChosenByHand: false,
4581
4600
  // The sort option the applied view's `overrides.sortOption` selected, so leaving the view can undo it.
4582
4601
  viewSortOption: '',
4583
4602
  // The applied view's filters as one comparable key, so the address keeps naming the view until they change.
@@ -6417,13 +6436,53 @@ export default {
6417
6436
  * @spec openspec/changes/working-list-row-actions/specs/index-page/spec.md
6418
6437
  */
6419
6438
  /**
6420
- * The chip the strip lights: the active tab, else the default tab of
6421
- * tabs that arrived after setup (`quickFiltersFrom`), which is the
6422
- * one the fetch applies.
6439
+ * The lens chip an applied saved view narrows within: the non-default
6440
+ * tab whose every condition the view's own conditions (its filters
6441
+ * over its locked filters, tokens as written) hold, the most specific
6442
+ * first (DqFilterInMenu: "Termijn deze week" lit for the view of that
6443
+ * name). Null without an applied view or a fitting tab.
6444
+ *
6445
+ * @return {number|null} The tab index.
6446
+ * @spec openspec/changes/applied-view-lights-its-lens/specs/applied-view-lights-its-lens/spec.md
6447
+ */
6448
+ appliedViewLensIndex() {
6449
+ const view = this.appliedSavedView
6450
+ const tabs = Array.isArray(this.tabStripEntries) ? this.tabStripEntries : []
6451
+ if (!view || tabs.length === 0) {
6452
+ return null
6453
+ }
6454
+ const query = view.query && typeof view.query === 'object' ? view.query : {}
6455
+ const locked = query.overrides && typeof query.overrides.lockedFilters === 'object' ? query.overrides.lockedFilters : {}
6456
+ const conditions = { ...(locked || {}), ...(query.filters || view.filters || {}) }
6457
+ const same = (a, b) => JSON.stringify(Array.isArray(a) ? a.map(String) : String(a)) === JSON.stringify(Array.isArray(b) ? b.map(String) : String(b))
6458
+ let best = null
6459
+ let bestSize = 0
6460
+ tabs.forEach((tab, index) => {
6461
+ const filter = tab && tab.filter && typeof tab.filter === 'object' ? tab.filter : {}
6462
+ const keys = Object.keys(filter)
6463
+ if (tab.default === true || keys.length === 0 || keys.length <= bestSize) {
6464
+ return
6465
+ }
6466
+ if (keys.every((key) => Object.hasOwn(conditions, key) && same(conditions[key], filter[key]))) {
6467
+ best = index
6468
+ bestSize = keys.length
6469
+ }
6470
+ })
6471
+ return best
6472
+ },
6473
+
6474
+ /**
6475
+ * The chip the strip lights: the lens an applied saved view narrows
6476
+ * within (until the reader picks a chip), else the active tab, else
6477
+ * the default tab of tabs that arrived after setup
6478
+ * (`quickFiltersFrom`), which is the one the fetch applies.
6423
6479
  *
6424
6480
  * @return {number|null} The tab index, or null without tabs.
6425
6481
  */
6426
6482
  shownQuickFilterIndex() {
6483
+ if (!this.quickFilterMultiple && !this.lensChosenByHand && this.appliedViewLensIndex !== null) {
6484
+ return this.appliedViewLensIndex
6485
+ }
6427
6486
  if (this.activeQuickFilterIndex !== null && this.activeQuickFilterIndex !== undefined) {
6428
6487
  return this.activeQuickFilterIndex
6429
6488
  }
@@ -7217,6 +7276,31 @@ export default {
7217
7276
  return n('nextcloud-vue', '%n without an address', '%n without an address', this.mapList.missing.length)
7218
7277
  },
7219
7278
 
7279
+ /**
7280
+ * The sentence on that card naming the rows without a place by the
7281
+ * list's title template, three at most and the rest counted: "A, B and
7282
+ * C have no location, so they are not on the map." Empty without a
7283
+ * title template.
7284
+ *
7285
+ * @spec openspec/changes/map-missing-card-names-the-cases/specs/map-missing-card-names-the-cases/spec.md#requirement-the-missing-address-card-names-the-cases-it-counts
7286
+ * @return {string} The sentence.
7287
+ */
7288
+ mapMissingNote() {
7289
+ const list = this.mapConfig && this.mapConfig.list
7290
+ const template = list && typeof list.title === 'string' ? this.cnTranslate(list.title) : ''
7291
+ const missing = this.mapList.missing
7292
+ if (template === '' || missing.length === 0) {
7293
+ return ''
7294
+ }
7295
+ const shown = missing.slice(0, 3).map((row) => fillTemplate(template, row, undefined, this.cnFormatters ? { formatters: this.cnFormatters } : null)).filter((title) => title !== '')
7296
+ if (shown.length === 0) {
7297
+ return ''
7298
+ }
7299
+ const rest = missing.length - shown.length
7300
+ const names = listText(rest > 0 ? [...shown, n('nextcloud-vue', '%n other', '%n others', rest)] : shown, getCanonicalLocale() || undefined)
7301
+ return n('nextcloud-vue', '{names} has no location, so it is not on the map.', '{names} have no location, so they are not on the map.', missing.length, { names })
7302
+ },
7303
+
7220
7304
  /** @return {string} The link from that card to the table. */
7221
7305
  mapMissingLink() {
7222
7306
  return t('nextcloud-vue', 'Show them in the table')
@@ -8788,6 +8872,11 @@ export default {
8788
8872
  * @return {string}
8789
8873
  */
8790
8874
  presentedCountText() {
8875
+ const viewLine = this.savedViewCountText
8876
+ const view = this.appliedSavedView
8877
+ if (view && typeof viewLine === 'string' && viewLine !== '' && this.activeScopePresentation.countText === undefined && !savedViewOverrides(view)?.countText) {
8878
+ return viewLine
8879
+ }
8791
8880
  if (this.activeScopePresentation.countText === undefined) {
8792
8881
  const byView = this.countTextByView
8793
8882
  const own = byView && typeof byView === 'object' ? byView[this.currentViewMode] : undefined
@@ -8798,6 +8887,23 @@ export default {
8798
8887
  return this.presented('countText')
8799
8888
  },
8800
8889
 
8890
+ /**
8891
+ * The applied saved view's conditions for the count line's `{view}`:
8892
+ * its description, else its filters in words, starting lower case.
8893
+ *
8894
+ * @return {string}
8895
+ * @spec openspec/changes/applied-view-lights-its-lens/specs/applied-view-lights-its-lens/spec.md
8896
+ */
8897
+ appliedViewConditionsText() {
8898
+ const view = this.appliedSavedView
8899
+ if (!view) {
8900
+ return ''
8901
+ }
8902
+ const own = typeof view.description === 'string' ? view.description.trim() : ''
8903
+ const text = own !== '' ? own : String(this.viewFilterSummary(view) || '')
8904
+ return text.charAt(0).toLowerCase() + text.slice(1)
8905
+ },
8906
+
8801
8907
  headerDescriptionBase() {
8802
8908
  const total = this.effectivePagination?.total
8803
8909
  // Board look: the count line is a template over the rows on this page
@@ -8821,6 +8927,7 @@ export default {
8821
8927
  .replace('{shown}', String(shown))
8822
8928
  .replace('{total}', String(total))
8823
8929
  .replace(/\{term\}/g, this.sourceTerm)
8930
+ .replace(/\{view\}/g, this.appliedViewConditionsText)
8824
8931
  }
8825
8932
  return this.countLine(this.countSubtitle, total, shown)
8826
8933
  }
@@ -8886,6 +8993,17 @@ export default {
8886
8993
  return !!this.resolvedSidebar.enabled && this.resolvedSidebar.show !== false
8887
8994
  },
8888
8995
 
8996
+ /**
8997
+ * Whether the page's side panel is open now; the board look then lays
8998
+ * the header out narrower (DqZakenSorteren).
8999
+ *
9000
+ * @spec openspec/changes/board-header-buttons-under-title-beside-side-panel/specs/board-header-buttons-under-title-beside-side-panel/spec.md#requirement-the-header-buttons-sit-under-the-title-while-the-side-panel-is-open
9001
+ * @return {boolean}
9002
+ */
9003
+ sidebarPanelOpen() {
9004
+ return Boolean(this.sidebarOpen && this.hasSidebar)
9005
+ },
9006
+
8889
9007
  /**
8890
9008
  * Whether the board toolbar draws the Filter button as a menu: the
8891
9009
  * board look, `filterMenu` on, something to filter by, and no filter
@@ -8928,7 +9046,9 @@ export default {
8928
9046
 
8929
9047
  /** Search props forwarded to the embedded CnIndexSidebar (defaults applied per CnIndexSidebar). */
8930
9048
  sidebarSearchProps() {
8931
- return (this.sidebar && this.sidebar.search) || {}
9049
+ // The page's own search hint, unless `sidebar.search` names one.
9050
+ const own = this.shownSearchPlaceholder ? { searchPlaceholder: this.shownSearchPlaceholder } : {}
9051
+ return { ...own, ...((this.sidebar && this.sidebar.search) || {}) }
8932
9052
  },
8933
9053
 
8934
9054
  /**
@@ -10751,6 +10871,7 @@ export default {
10751
10871
  // `activeQuickFilterIndex` is a setup-returned ref; the
10752
10872
  // Vue 2 ref-unwrap proxy makes plain assignment work.
10753
10873
  this.activeQuickFilterIndex = index
10874
+ this.lensChosenByHand = true
10754
10875
  this.$emit('quick-filter-change', index)
10755
10876
  },
10756
10877
 
@@ -11519,10 +11640,16 @@ export default {
11519
11640
  const bucket = facet && Array.isArray(facet.values)
11520
11641
  ? facet.values.find((fv) => fv && typeof fv === 'object' && String(fv.value) === id)
11521
11642
  : null
11522
- if (bucket && typeof bucket.label === 'string' && bucket.label !== '') {
11643
+ // A facet that only echoes the value names nothing; a group or an
11644
+ // option below may.
11645
+ if (bucket && typeof bucket.label === 'string' && bucket.label !== '' && bucket.label !== id) {
11523
11646
  return this.cnTranslate(bucket.label)
11524
11647
  }
11525
11648
  const prop = this.effectiveSchema?.properties?.[key]
11649
+ // The reader on a user filter is "me" (DqFilterInMenu "Behandelaar: ik").
11650
+ if (prop && prop.referenceType === 'nextcloud-user' && id === (getCurrentUser()?.uid ?? '')) {
11651
+ return t('nextcloud-vue', 'me')
11652
+ }
11526
11653
  if (prop && prop.referenceType === 'nextcloud-group') {
11527
11654
  const name = groupDisplayName(id)
11528
11655
  if (name === null) {
@@ -13180,6 +13307,7 @@ export default {
13180
13307
  */
13181
13308
  onApplySavedView(view, { keepSavedViewKey = false } = {}) {
13182
13309
  this.appliedSavedViewId = view && (view.id || view.slug) ? String(view.id || view.slug) : ''
13310
+ this.lensChosenByHand = false
13183
13311
  const state = extractViewState(view)
13184
13312
  // A view's filters may carry tokens (`@me`, `@myTeam`): resolved here,
13185
13313
  // and an optional one that answers nothing (no team) is dropped. A
@@ -32,11 +32,17 @@
32
32
  </div>
33
33
 
34
34
  <div class="cn-index-sidebar__section">
35
+ <!-- The board look names the field by its hint, as the list's
36
+ own search field, with the magnifier (DqZakenSorteren). -->
35
37
  <NcTextField
36
38
  :modelValue="searchValue || ''"
37
39
  :placeholder="searchPlaceholder"
38
- :label="searchLabel"
39
- @update:modelValue="$emit('search', $event)" />
40
+ :label="isBoardLook ? searchPlaceholder : searchLabel"
41
+ @update:modelValue="$emit('search', $event)">
42
+ <template v-if="isBoardLook" #icon>
43
+ <Magnify :size="18" />
44
+ </template>
45
+ </NcTextField>
40
46
  </div>
41
47
 
42
48
  <!-- Sort (list, card and map views; the table sorts by its headers):
@@ -355,6 +361,7 @@ import DragVertical from 'vue-material-design-icons/DragVertical.vue'
355
361
  import FilterOutline from 'vue-material-design-icons/FilterOutline.vue'
356
362
  import FilterRemoveOutline from 'vue-material-design-icons/FilterRemoveOutline.vue'
357
363
  import InformationOutline from 'vue-material-design-icons/InformationOutline.vue'
364
+ import Magnify from 'vue-material-design-icons/Magnify.vue'
358
365
  import Pin from 'vue-material-design-icons/Pin.vue'
359
366
  import PinOutline from 'vue-material-design-icons/PinOutline.vue'
360
367
  import ViewColumnOutline from 'vue-material-design-icons/ViewColumnOutline.vue'
@@ -410,6 +417,7 @@ export default {
410
417
  Pin,
411
418
  PinOutline,
412
419
  InformationOutline,
420
+ Magnify,
413
421
  },
414
422
 
415
423
  inject: {
@@ -25,4 +25,8 @@ registerDashboardWidget('mail', {
25
25
  displayName: 'Mail',
26
26
  icon: 'EmailOutline',
27
27
  surfaces: ['detail-page'],
28
+ // Draws its own title row; on a card surface it stands in an untitled card.
29
+ // @spec openspec/changes/mail-widget-card/specs/mail-widget-card/spec.md#requirement-the-mail-widget-stands-in-a-card-on-a-card-surface
30
+ ownsTitle: true,
31
+ chrome: 'card',
28
32
  })
@@ -1093,12 +1093,16 @@ export default {
1093
1093
  white-space: nowrap;
1094
1094
  }
1095
1095
 
1096
+ /* A theme that colours every th (thematiq's table header token) leaves
1097
+ the board grid's head white. */
1096
1098
  .cn-notification-matrix__table {
1099
+ --nldesign-component-table-header-background-color: transparent;
1097
1100
  font-size: 14px;
1098
1101
  }
1099
1102
 
1100
1103
  .cn-notification-matrix__table thead th {
1101
1104
  padding: 8px 10px 8px 0;
1105
+ line-height: 1.2;
1102
1106
  border-block-end: 1px solid var(--color-border);
1103
1107
  color: var(--color-text-maxcontrast);
1104
1108
  font-size: 13px;
@@ -860,7 +860,7 @@ export default {
860
860
  // `secondary` (a second, muted line under the cell) rides
861
861
  // along too: the table draws it, and dropping it here took the
862
862
  // list rows' second line away (zuiddrecht-pixel-gaps-3).
863
- for (const k of ['format', 'widget', 'widgetProps', 'formatter', 'align', 'width', 'type', 'enum', 'enumLabels', 'sortable', 'secondary', 'colorMap', 'grow']) {
863
+ for (const k of ['format', 'widget', 'widgetProps', 'formatter', 'align', 'width', 'type', 'enum', 'enumLabels', 'sortable', 'secondary', 'colorMap', 'grow', 'cellClass']) {
864
864
  if (c[k] !== undefined) {
865
865
  out[k] = c[k]
866
866
  }
@@ -24,6 +24,7 @@
24
24
  :reviewSummary="reviewSummary"
25
25
  :footnote="footnote"
26
26
  :nameSecondary="nameSecondary"
27
+ :secondLine="secondLine"
27
28
  :linkedItems="linkedItems"
28
29
  :columns="columns"
29
30
  :rowData="rowDataSource"
@@ -450,6 +451,8 @@ export default {
450
451
  footnote: { type: String, default: '' },
451
452
  /** Forwarded to CnFilesBrowser: the second line under each file's name. */
452
453
  nameSecondary: { type: String, default: '' },
454
+ /** Forwarded to CnFilesBrowser: the second line as a list of fields, or variants. */
455
+ secondLine: { type: Array, default: null },
453
456
  /** A dotted path to the list of records in the `rowDataUrl` answer; empty reads the answer itself. */
454
457
  rowDataPath: { type: String, default: '' },
455
458
  /** The record field that holds the Nextcloud file id. */