@conduction/nextcloud-vue 2.137.0 → 2.138.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 (86) 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/CnDetailCard/CnDetailCard.vue.js +1 -1
  6. package/dist/esm/components/CnDetailCard/CnDetailCard.vue2.js +6 -0
  7. package/dist/esm/components/CnDetailCard/CnDetailCard.vue2.js.map +1 -1
  8. package/dist/esm/components/CnDetailCard/CnDetailCard.vue3.js +24 -7
  9. package/dist/esm/components/CnDetailCard/CnDetailCard.vue3.js.map +1 -1
  10. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue.js +1 -1
  11. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js +2 -0
  12. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
  13. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
  14. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue.js +1 -1
  15. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js +43 -1
  16. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js.map +1 -1
  17. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js.map +1 -1
  18. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +34 -2
  19. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  20. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +23 -15
  21. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  22. package/dist/esm/components/CnMailTab/dashboardRegistration.js +4 -0
  23. package/dist/esm/components/CnMailTab/dashboardRegistration.js.map +1 -1
  24. package/dist/esm/components/CnNotificationMatrix/CnNotificationMatrix.vue.js +1 -1
  25. package/dist/esm/components/CnNotificationMatrix/CnNotificationMatrix.vue2.js.map +1 -1
  26. package/dist/esm/components/CnNotificationMatrix/CnNotificationMatrix.vue3.js.map +1 -1
  27. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue.js +1 -1
  28. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js +2 -0
  29. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js.map +1 -1
  30. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js +2 -1
  31. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js.map +1 -1
  32. package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue.js +1 -1
  33. package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue2.js +6 -0
  34. package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue2.js.map +1 -1
  35. package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue3.js +3 -3
  36. package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue3.js.map +1 -1
  37. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
  38. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +16 -0
  39. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
  40. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js +11 -5
  41. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
  42. package/dist/esm/components/CnSavedViewMenuSettings/CnSavedViewMenuSettings.vue.js +1 -1
  43. package/dist/esm/components/CnSavedViewMenuSettings/CnSavedViewMenuSettings.vue2.js.map +1 -1
  44. package/dist/esm/components/CnSavedViewMenuSettings/CnSavedViewMenuSettings.vue3.js +1 -0
  45. package/dist/esm/components/CnSavedViewMenuSettings/CnSavedViewMenuSettings.vue3.js.map +1 -1
  46. package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue.js +1 -1
  47. package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue2.js.map +1 -1
  48. package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue3.js.map +1 -1
  49. package/dist/esm/composables/indexSources.js +36 -9
  50. package/dist/esm/composables/indexSources.js.map +1 -1
  51. package/dist/esm/l10n/en.json.js +10 -1
  52. package/dist/esm/l10n/en.json.js.map +1 -1
  53. package/dist/esm/l10n/nl.json.js +10 -1
  54. package/dist/esm/l10n/nl.json.js.map +1 -1
  55. package/dist/esm/library-style.js +1 -1
  56. package/dist/esm/nextcloud-vue.css +431 -360
  57. package/dist/esm/utils/resolveFilterTokens.js +9 -0
  58. package/dist/esm/utils/resolveFilterTokens.js.map +1 -1
  59. package/dist/esm/utils/sentinelTokens.js +1 -1
  60. package/dist/esm/utils/sentinelTokens.js.map +1 -1
  61. package/dist/nextcloud-vue.cjs.js +418 -221
  62. package/dist/nextcloud-vue.cjs.js.map +2 -2
  63. package/dist/nextcloud-vue.css +431 -360
  64. package/l10n/en.json +10 -1
  65. package/l10n/nl.json +10 -1
  66. package/package.json +1 -1
  67. package/src/components/CnCellRenderer/CnCellRenderer.vue +6 -4
  68. package/src/components/CnDetailCard/CnDetailCard.md +1 -0
  69. package/src/components/CnDetailCard/CnDetailCard.vue +25 -1
  70. package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +2 -0
  71. package/src/components/CnFilesBrowser/CnFilesBrowser.vue +43 -1
  72. package/src/components/CnIndexPage/CnIndexPage.vue +34 -1
  73. package/src/components/CnMailTab/dashboardRegistration.js +4 -0
  74. package/src/components/CnNotificationMatrix/CnNotificationMatrix.vue +4 -0
  75. package/src/components/CnObjectSidebar/CnFilesTab.vue +3 -0
  76. package/src/components/CnOrderedListField/CnOrderedListField.vue +11 -1
  77. package/src/components/CnRecordListWidget/CnRecordListWidget.vue +22 -3
  78. package/src/components/CnSavedViewMenuSettings/CnSavedViewMenuSettings.vue +1 -0
  79. package/src/components/CnUserSettingsDialog/CnUserSettingsDialog.vue +3 -0
  80. package/src/composables/indexSources.js +36 -9
  81. package/src/css/dialog.css +8 -0
  82. package/src/css/index-page.css +5 -0
  83. package/src/css/look-board-detail.css +12 -0
  84. package/src/css/look-board.css +26 -0
  85. package/src/utils/resolveFilterTokens.js +9 -0
  86. package/src/utils/sentinelTokens.js +1 -1
package/l10n/en.json CHANGED
@@ -2377,7 +2377,8 @@
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"
2381
2382
  },
2382
2383
  "plurals": {
2383
2384
  "{count} member": [
@@ -2578,6 +2579,14 @@
2578
2579
  "within %n day": [
2579
2580
  "within %n day",
2580
2581
  "within %n days"
2582
+ ],
2583
+ "%n other": [
2584
+ "%n other",
2585
+ "%n others"
2586
+ ],
2587
+ "{names} has no location, so it is not on the map.": [
2588
+ "{names} has no location, so it is not on the map.",
2589
+ "{names} have no location, so they are not on the map."
2581
2590
  ]
2582
2591
  }
2583
2592
  }
package/l10n/nl.json CHANGED
@@ -2627,7 +2627,8 @@
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"
2631
2632
  },
2632
2633
  "plurals": {
2633
2634
  "{count} member": [
@@ -2845,6 +2846,14 @@
2845
2846
  "within %n day": [
2846
2847
  "binnen %n dag",
2847
2848
  "binnen %n dagen"
2849
+ ],
2850
+ "%n other": [
2851
+ "%n andere",
2852
+ "%n andere"
2853
+ ],
2854
+ "{names} has no location, so it is not on the map.": [
2855
+ "{names} heeft geen locatie, dus staat niet op de kaart.",
2856
+ "{names} hebben geen locatie, dus staan ze niet op de kaart."
2848
2857
  ]
2849
2858
  }
2850
2859
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@conduction/nextcloud-vue",
3
- "version": "2.137.0",
3
+ "version": "2.138.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.
@@ -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
@@ -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
 
@@ -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'
@@ -7217,6 +7219,31 @@ export default {
7217
7219
  return n('nextcloud-vue', '%n without an address', '%n without an address', this.mapList.missing.length)
7218
7220
  },
7219
7221
 
7222
+ /**
7223
+ * The sentence on that card naming the rows without a place by the
7224
+ * list's title template, three at most and the rest counted: "A, B and
7225
+ * C have no location, so they are not on the map." Empty without a
7226
+ * title template.
7227
+ *
7228
+ * @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
7229
+ * @return {string} The sentence.
7230
+ */
7231
+ mapMissingNote() {
7232
+ const list = this.mapConfig && this.mapConfig.list
7233
+ const template = list && typeof list.title === 'string' ? this.cnTranslate(list.title) : ''
7234
+ const missing = this.mapList.missing
7235
+ if (template === '' || missing.length === 0) {
7236
+ return ''
7237
+ }
7238
+ const shown = missing.slice(0, 3).map((row) => fillTemplate(template, row, undefined, this.cnFormatters ? { formatters: this.cnFormatters } : null)).filter((title) => title !== '')
7239
+ if (shown.length === 0) {
7240
+ return ''
7241
+ }
7242
+ const rest = missing.length - shown.length
7243
+ const names = listText(rest > 0 ? [...shown, n('nextcloud-vue', '%n other', '%n others', rest)] : shown, getCanonicalLocale() || undefined)
7244
+ 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 })
7245
+ },
7246
+
7220
7247
  /** @return {string} The link from that card to the table. */
7221
7248
  mapMissingLink() {
7222
7249
  return t('nextcloud-vue', 'Show them in the table')
@@ -11519,10 +11546,16 @@ export default {
11519
11546
  const bucket = facet && Array.isArray(facet.values)
11520
11547
  ? facet.values.find((fv) => fv && typeof fv === 'object' && String(fv.value) === id)
11521
11548
  : null
11522
- if (bucket && typeof bucket.label === 'string' && bucket.label !== '') {
11549
+ // A facet that only echoes the value names nothing; a group or an
11550
+ // option below may.
11551
+ if (bucket && typeof bucket.label === 'string' && bucket.label !== '' && bucket.label !== id) {
11523
11552
  return this.cnTranslate(bucket.label)
11524
11553
  }
11525
11554
  const prop = this.effectiveSchema?.properties?.[key]
11555
+ // The reader on a user filter is "me" (DqFilterInMenu "Behandelaar: ik").
11556
+ if (prop && prop.referenceType === 'nextcloud-user' && id === (getCurrentUser()?.uid ?? '')) {
11557
+ return t('nextcloud-vue', 'me')
11558
+ }
11526
11559
  if (prop && prop.referenceType === 'nextcloud-group') {
11527
11560
  const name = groupDisplayName(id)
11528
11561
  if (name === null) {
@@ -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;
@@ -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. */
@@ -5,7 +5,11 @@
5
5
 
6
6
  <template>
7
7
  <div class="cn-ordered-list-field" data-testid="cn-ordered-list-field">
8
- <p v-if="label" :id="labelId" class="cn-ordered-list-field__label">
8
+ <p
9
+ v-if="label"
10
+ :id="labelId"
11
+ class="cn-ordered-list-field__label"
12
+ :class="{ 'hidden-visually': hideLabel }">
9
13
  {{ label }}
10
14
  </p>
11
15
  <ol
@@ -192,6 +196,12 @@ export default {
192
196
  default: '',
193
197
  },
194
198
 
199
+ /** Keep the label for assistive technology only, when a heading above already says it. */
200
+ hideLabel: {
201
+ type: Boolean,
202
+ default: false,
203
+ },
204
+
195
205
  /** The picker's label. */
196
206
  addLabel: {
197
207
  type: String,
@@ -20,9 +20,11 @@
20
20
  <div v-else-if="loading && rows.length === 0" class="cn-record-list__loading">
21
21
  <NcLoadingIcon :size="20" :name="loadingLabel" />
22
22
  </div>
23
- <p v-else-if="rows.length === 0" class="cn-record-list__empty" data-testid="cn-record-list-empty">
24
- {{ emptyLabel }}
25
- </p>
23
+ <template v-else-if="rows.length === 0">
24
+ <p v-if="emptyLabel !== ''" class="cn-record-list__empty" data-testid="cn-record-list-empty">
25
+ {{ emptyLabel }}
26
+ </p>
27
+ </template>
26
28
  <!-- notice: the rows as sentences in one toned box ("2 waarschuwingen: ..."). -->
27
29
  <div
28
30
  v-else-if="presentation === 'notice'"
@@ -251,6 +253,7 @@
251
253
  <component
252
254
  :is="item.action.tag"
253
255
  class="cn-record-list__action"
256
+ :class="{ 'cn-record-list__action--outlined': rowActionOutlined }"
254
257
  v-bind="item.action.attrs"
255
258
  data-testid="cn-record-list-action"
256
259
  v-on="item.action.tag === 'button' ? { click: () => runRowAction(item.row) } : {}">
@@ -1469,6 +1472,18 @@ export default {
1469
1472
  * @return {string}
1470
1473
  * @spec openspec/changes/record-list-table-row-actions/specs/record-list-table/spec.md#requirement-a-table-row-can-carry-a-second-line-and-an-actions-menu
1471
1474
  */
1475
+ /**
1476
+ * Whether the row action is drawn as an outlined button sized to its
1477
+ * label (`rowAction.variant: "outlined"`, DqMijnTeam "Oppakken") instead
1478
+ * of the 150px pill (DcMijnActies "Stem uitbrengen").
1479
+ *
1480
+ * @return {boolean} True for the outlined variant.
1481
+ * @spec openspec/changes/board-row-action-outlined/specs/board-row-action-outlined/spec.md#requirement-a-dashboard-rows-action-is-an-outlined-button
1482
+ */
1483
+ rowActionOutlined() {
1484
+ return this.content?.rowAction?.variant === 'outlined'
1485
+ },
1486
+
1472
1487
  rowActionsHeader() {
1473
1488
  const own = this.content?.rowActionsHeader
1474
1489
  return typeof own === 'string' && own !== '' ? this.translate(own) : t('nextcloud-vue', 'Actions')
@@ -1511,6 +1526,10 @@ export default {
1511
1526
  if (this.conditionalEmptyText !== '') {
1512
1527
  return this.conditionalEmptyText
1513
1528
  }
1529
+ // `emptyText: ""` says nothing on an empty list.
1530
+ if (this.content?.emptyText === '') {
1531
+ return ''
1532
+ }
1514
1533
  return this.content?.emptyText ? this.translate(this.content.emptyText) : t('nextcloud-vue', 'Nothing here yet')
1515
1534
  },
1516
1535
 
@@ -12,6 +12,7 @@
12
12
  :modelValue="values"
13
13
  :options="options"
14
14
  :label="t('nextcloud-vue', 'Views in my menu')"
15
+ :hideLabel="true"
15
16
  :addLabel="t('nextcloud-vue', 'Add view')"
16
17
  :addPlaceholder="t('nextcloud-vue', 'Pick a saved view')"
17
18
  :addHint="t('nextcloud-vue', 'You see your own views, the views shared with you and the default views. A new view goes to the bottom.')"
@@ -400,7 +400,10 @@ export default {
400
400
  background: var(--color-main-background);
401
401
  }
402
402
 
403
+ /* A section's heading and description start at the text's edge, as the
404
+ board draws them (NcAppSettingsSection indents them 12px). */
403
405
  .cn-user-settings-dialog__sections :deep(.app-settings-section) {
406
+ --app-settings-section-text-offset: 0;
404
407
  margin: 0;
405
408
  padding: 0;
406
409
  border: 0;
@@ -1088,7 +1088,7 @@ function fillRowTemplate(template, row) {
1088
1088
  * and its count (DqZoekresultaten).
1089
1089
  *
1090
1090
  * `sourceConfig.kinds` lists what is searched:
1091
- * `{ id, label, register, schema, titleField?, subtitleField?, statusField?, statusResolve?, linkedField?, linkedTemplate?, href?, limit? }`
1091
+ * `{ id, label, register, schema, titleField?, subtitleField?, subtitleTemplate?, statusField?, statusResolve?, linkedField?, linkedTemplate?, href?, limit? }`
1092
1092
  * for objects (OpenRegister's `_search`, so the reader sees what they may
1093
1093
  * read), or `{ id, label, files: true, limit? }` for the reader's files
1094
1094
  * (Nextcloud's unified search). The term is `sourceConfig.term`, else the
@@ -1102,6 +1102,8 @@ function fillRowTemplate(template, row) {
1102
1102
  * coloured by `sourceConfig.statusColorMap`. `linkedTemplate` (a field
1103
1103
  * template, "{case.title}") or `linkedField` (a path) fills the "Linked"
1104
1104
  * column, drawn when a kind declares one; `sourceConfig.linkedLabel` names it.
1105
+ * `subtitleTemplate` ("Case {identifier}", translated) fills the line under a
1106
+ * result, else `subtitleField`.
1105
1107
  * The page's title and count line can name the term as `{term}`, and the
1106
1108
  * count is the active kind's total. Kind, link and status are filterable.
1107
1109
  *
@@ -1199,6 +1201,13 @@ function searchSource(config = {}, context = {}) {
1199
1201
  return ''
1200
1202
  }
1201
1203
 
1204
+ const subtitleOf = (kind, object) => {
1205
+ if (kind.subtitleTemplate) {
1206
+ return fillTemplate(translate(kind.subtitleTemplate), object)
1207
+ }
1208
+ return kind.subtitleField ? String(readPath(object, kind.subtitleField) ?? '') : ''
1209
+ }
1210
+
1202
1211
  const searchObjects = async (axios, kind, term) => {
1203
1212
  const url = generateUrl('/apps/openregister/api/objects/{register}/{schema}', { register: kind.register, schema: kind.schema })
1204
1213
  const res = await axios.get(url, { params: { _search: term, _limit: kind.limit || 20 } })
@@ -1219,7 +1228,7 @@ function searchSource(config = {}, context = {}) {
1219
1228
  kindLabel: kind.label,
1220
1229
  kindText: kindTextOf(kind, object),
1221
1230
  title: String(readPath(object, kind.titleField || 'title') ?? object.name ?? id ?? ''),
1222
- subtitle: kind.subtitleField ? String(readPath(object, kind.subtitleField) ?? '') : '',
1231
+ subtitle: subtitleOf(kind, object),
1223
1232
  status,
1224
1233
  linked: linkedOf(kind, object),
1225
1234
  href: kind.href ? generateUrl(fillRowTemplate(kind.href, { ...object, id })) : '',
@@ -1406,14 +1415,24 @@ function searchSource(config = {}, context = {}) {
1406
1415
  * the pager asks for.
1407
1416
  *
1408
1417
  * @param {Array<object>} members The members, by name.
1409
- * @param {object} config `search`, `sortKey`, `sortOrder`, `limit`, `offset`.
1418
+ * @param {object} config `search`, `nameLike`, `rolesLike`, `emailLike`, `today`, `sortKey`, `sortOrder`, `limit`, `offset`.
1410
1419
  * @return {{rows: Array<object>, total: number}} The page and how many match.
1411
1420
  */
1412
1421
  function membersPage(members, config = {}) {
1422
+ const has = (member, fields, text) => fields.some((field) => String(member[field] || '').toLowerCase().includes(text))
1413
1423
  const term = String(config.search || '').trim().toLowerCase()
1414
- let rows = term === ''
1415
- ? members.slice()
1416
- : members.filter((member) => ['displayname', 'function', 'roles', 'email'].some((field) => String(member[field] || '').toLowerCase().includes(term)))
1424
+ let rows = term === '' ? members.slice() : members.filter((member) => has(member, ['displayname', 'function', 'roles', 'email'], term))
1425
+ // The table heads' filters: text contains, and the Today values picked.
1426
+ for (const [param, fields] of [['nameLike', ['displayname', 'function']], ['rolesLike', ['roles']], ['emailLike', ['email']]]) {
1427
+ const text = String(config[param] || '').trim().toLowerCase()
1428
+ if (text !== '') {
1429
+ rows = rows.filter((member) => has(member, fields, text))
1430
+ }
1431
+ }
1432
+ const today = (Array.isArray(config.today) ? config.today : (config.today ? [config.today] : [])).map(String)
1433
+ if (today.length > 0) {
1434
+ rows = rows.filter((member) => today.includes(member.today))
1435
+ }
1417
1436
  if (config.sortKey) {
1418
1437
  const sign = config.sortOrder === 'desc' ? -1 : 1
1419
1438
  rows = rows.sort((a, b) => sign * String(a[config.sortKey] ?? '').localeCompare(String(b[config.sortKey] ?? '')))
@@ -1475,10 +1494,11 @@ function usersSource() {
1475
1494
  */
1476
1495
  load: async (config) => {
1477
1496
  const group = String((config && (config.group || config.defaultGroup)) || '')
1478
- const { search, sortKey, sortOrder, limit, offset, ...request } = config || {}
1497
+ const { search, sortKey, sortOrder, limit, offset, nameLike, rolesLike, emailLike, today, ...request } = config || {}
1479
1498
  const key = JSON.stringify({ ...request, group })
1480
- // A new page, term or order of the members already read: no request.
1481
- if (key === membersKey && (offset !== undefined || search !== undefined || sortKey !== undefined)) {
1499
+ const narrows = [offset, search, sortKey, nameLike, rolesLike, emailLike, today].some((value) => value !== undefined)
1500
+ // A new page, term, order or head filter of the members already read: no request.
1501
+ if (key === membersKey && narrows) {
1482
1502
  const page = membersPage(readMembers, config)
1483
1503
  state.rows = page.rows
1484
1504
  state.total = page.total
@@ -1574,8 +1594,15 @@ function usersSource() {
1574
1594
 
1575
1595
  searchFields: {
1576
1596
  group: { param: 'group', single: true },
1597
+ 'displayname[like]': { param: 'nameLike', single: true },
1598
+ 'roles[like]': { param: 'rolesLike', single: true },
1599
+ 'email[like]': { param: 'emailLike', single: true },
1600
+ today: { param: 'today' },
1577
1601
  },
1578
1602
 
1603
+ /** A filter button on every head (DqAfdelingen); the members read are narrowed here. */
1604
+ headerFilters: true,
1605
+
1579
1606
  showAdd: false,
1580
1607
  }
1581
1608
  }
@@ -212,6 +212,14 @@ body:has(.cn-look-board):not(:has(.cn-look-nextcloud)) .modal-mask {
212
212
  padding: 22px 24px 20px;
213
213
  }
214
214
 
215
+ /* NcDialog's scoped `.dialog__content[data-v-…]` sets padding-inline-end
216
+ 12px with the same specificity as the rule above, so wherever its chunk
217
+ loads later (an admin page's setup wizard) the body ran 12px wider than
218
+ the board (DqInstellen 684 for 672). This one outranks it. */
219
+ .cn-dialog-board .dialog .dialog__content {
220
+ padding-inline-end: 24px;
221
+ }
222
+
215
223
  .cn-dialog-board .dialog__content > .cn-dialog-header {
216
224
  margin-bottom: -2px;
217
225
  }
@@ -221,6 +221,11 @@
221
221
  font-size: 14px;
222
222
  }
223
223
 
224
+ .cn-index-page__map-missing-note {
225
+ color: var(--color-main-text);
226
+ line-height: 1.45;
227
+ }
228
+
224
229
  /* The selected folder's heading above the list. */
225
230
  /* A sort option whose endpoint failed: the note above the fallen-back list. */
226
231
  .cn-index-page__sort-source-note {
@@ -1270,6 +1270,18 @@
1270
1270
  line-height: normal;
1271
1271
  }
1272
1272
 
1273
+ /* A long task name wraps under itself, as the board draws it, rather than
1274
+ widening the table past the card. */
1275
+ .cn-look-board .cn-detail-page .cn-widget-wrapper__content > .cn-tasks-widget .cn-tasks-widget__table :is(.cn-tasks-widget__name, .cn-tasks-widget__meta) {
1276
+ white-space: normal;
1277
+ }
1278
+
1279
+ /* A row's button and its task menu stand on one line. */
1280
+ .cn-look-board .cn-detail-page .cn-widget-wrapper__content > .cn-tasks-widget .cn-tasks-widget__table .cn-tasks-widget__cell-actions > * {
1281
+ display: inline-flex;
1282
+ vertical-align: middle;
1283
+ }
1284
+
1273
1285
  /* ---- An object list table edge to edge (`content.tableBleed`) ------------ */
1274
1286
  /* The case-tab boards draw a list's table edge to edge in its card
1275
1287
  (DqZaakContact "Contactmomenten op deze zaak", DqZaakGerelateerd): it runs
@@ -2335,6 +2335,32 @@ body:has(.cn-look-board):not(:has(.cn-look-nextcloud)) .v-popper__popper.action-
2335
2335
  font-weight: 600;
2336
2336
  }
2337
2337
 
2338
+ /* A row whose one action is outlined (`rowAction.variant: "outlined"`,
2339
+ DqMijnTeam "Oppakken"): its title link reads as the row's title in the main
2340
+ ink (the theme's link rule reads --nldesign-color-on-surface), and the
2341
+ action is an outlined button sized to its label. Without the variant the
2342
+ action keeps its 150px pill (DcMijnActies).
2343
+ @spec openspec/changes/board-row-action-outlined/specs/board-row-action-outlined/spec.md#requirement-a-dashboard-rows-action-is-an-outlined-button */
2344
+ .cn-look-board .cn-dashboard-page:not(.cn-look-nextcloud *) .cn-record-list__row--action:has(.cn-record-list__action--outlined) .cn-record-list__title--link {
2345
+ --nldesign-color-on-surface: var(--color-main-text);
2346
+ color: var(--color-main-text);
2347
+ }
2348
+
2349
+ .cn-look-board .cn-dashboard-page:not(.cn-look-nextcloud *) .cn-record-list__action--outlined {
2350
+ --nldesign-color-on-surface: var(--color-main-text);
2351
+ flex: none;
2352
+ padding: 0 12px;
2353
+ border: 1px solid var(--cn-board-control-border);
2354
+ border-radius: 8px;
2355
+ background: var(--color-main-background);
2356
+ color: var(--color-main-text);
2357
+ }
2358
+
2359
+ .cn-look-board .cn-dashboard-page:not(.cn-look-nextcloud *) .cn-record-list__action--outlined:hover,
2360
+ .cn-look-board .cn-dashboard-page:not(.cn-look-nextcloud *) .cn-record-list__action--outlined:focus-visible {
2361
+ background: var(--color-background-hover);
2362
+ }
2363
+
2338
2364
  .cn-look-board .cn-dashboard-page:not(.cn-look-nextcloud *) .cn-record-list__meta {
2339
2365
  color: var(--color-text-maxcontrast);
2340
2366
  font-size: 13px;