@conduction/nextcloud-vue 2.76.0 → 2.77.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 (119) hide show
  1. package/dist/esm/components/CnAppNav/CnAppNav.vue2.js +18 -3
  2. package/dist/esm/components/CnAppNav/CnAppNav.vue2.js.map +1 -1
  3. package/dist/esm/components/CnAppNav/CnAppNav.vue3.js +49 -3
  4. package/dist/esm/components/CnAppNav/CnAppNav.vue3.js.map +1 -1
  5. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js +42 -1
  6. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js.map +1 -1
  7. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js +228 -203
  8. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js.map +1 -1
  9. package/dist/esm/components/CnCellRenderer/CnGroupNameCell.vue.js +9 -0
  10. package/dist/esm/components/CnCellRenderer/CnGroupNameCell.vue.js.map +1 -0
  11. package/dist/esm/components/CnCellRenderer/CnGroupNameCell.vue2.js +50 -0
  12. package/dist/esm/components/CnCellRenderer/CnGroupNameCell.vue2.js.map +1 -0
  13. package/dist/esm/components/CnCellRenderer/CnGroupNameCell.vue3.js +13 -0
  14. package/dist/esm/components/CnCellRenderer/CnGroupNameCell.vue3.js.map +1 -0
  15. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +41 -4
  16. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
  17. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +90 -51
  18. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
  19. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +16 -28
  20. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  21. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +12 -24
  22. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  23. package/dist/esm/components/CnFavouriteToggle/CnFavouriteToggle.vue2.js +7 -2
  24. package/dist/esm/components/CnFavouriteToggle/CnFavouriteToggle.vue2.js.map +1 -1
  25. package/dist/esm/components/CnFavouriteToggle/CnFavouriteToggle.vue3.js.map +1 -1
  26. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js +114 -1
  27. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js.map +1 -1
  28. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js +115 -62
  29. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js.map +1 -1
  30. package/dist/esm/components/CnFollowToggle/CnFollowToggle.vue2.js +89 -7
  31. package/dist/esm/components/CnFollowToggle/CnFollowToggle.vue2.js.map +1 -1
  32. package/dist/esm/components/CnFollowToggle/CnFollowToggle.vue3.js +62 -30
  33. package/dist/esm/components/CnFollowToggle/CnFollowToggle.vue3.js.map +1 -1
  34. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +98 -13
  35. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  36. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +13 -9
  37. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  38. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js +199 -7
  39. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js.map +1 -1
  40. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue3.js +155 -119
  41. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue3.js.map +1 -1
  42. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js +6 -0
  43. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js.map +1 -1
  44. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js +4 -1
  45. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js.map +1 -1
  46. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js +13 -0
  47. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js.map +1 -1
  48. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue3.js.map +1 -1
  49. package/dist/esm/composables/useListView.js +4 -0
  50. package/dist/esm/composables/useListView.js.map +1 -1
  51. package/dist/esm/index.js +2 -1
  52. package/dist/esm/index.js.map +1 -1
  53. package/dist/esm/l10n/en.json.js +25 -1
  54. package/dist/esm/l10n/en.json.js.map +1 -1
  55. package/dist/esm/l10n/nl.json.js +25 -1
  56. package/dist/esm/l10n/nl.json.js.map +1 -1
  57. package/dist/esm/nextcloud-vue.css +288 -74
  58. package/dist/esm/store/plugins/interactions.js +20 -10
  59. package/dist/esm/store/plugins/interactions.js.map +1 -1
  60. package/dist/esm/store/useObjectStore.js +17 -0
  61. package/dist/esm/store/useObjectStore.js.map +1 -1
  62. package/dist/esm/utils/builtInFormatters.js +15 -7
  63. package/dist/esm/utils/builtInFormatters.js.map +1 -1
  64. package/dist/esm/utils/groupAutocomplete.js +63 -1
  65. package/dist/esm/utils/groupAutocomplete.js.map +1 -1
  66. package/dist/esm/utils/lensAvailability.js +90 -0
  67. package/dist/esm/utils/lensAvailability.js.map +1 -0
  68. package/dist/esm/utils/personalLenses.js +13 -6
  69. package/dist/esm/utils/personalLenses.js.map +1 -1
  70. package/dist/esm/utils/recordInteractions.js +31 -13
  71. package/dist/esm/utils/recordInteractions.js.map +1 -1
  72. package/dist/esm/utils/resolveFilterTokens.js +46 -3
  73. package/dist/esm/utils/resolveFilterTokens.js.map +1 -1
  74. package/dist/esm/utils/schema.js +1 -1
  75. package/dist/esm/utils/schema.js.map +1 -1
  76. package/dist/esm/utils/sentinelTokens.js +4 -4
  77. package/dist/esm/utils/sentinelTokens.js.map +1 -1
  78. package/dist/esm/utils/validateManifestV2.compiled.js +2 -2
  79. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  80. package/dist/esm/utils/widgetVisibility.js +46 -1
  81. package/dist/esm/utils/widgetVisibility.js.map +1 -1
  82. package/dist/nextcloud-vue.cjs.js +3873 -2674
  83. package/dist/nextcloud-vue.cjs.js.map +2 -2
  84. package/dist/nextcloud-vue.css +288 -74
  85. package/l10n/en.json +25 -1
  86. package/l10n/nl.json +25 -1
  87. package/package.json +1 -1
  88. package/src/components/CnAppNav/CnAppNav.vue +44 -5
  89. package/src/components/CnCellRenderer/CnCellRenderer.vue +56 -1
  90. package/src/components/CnCellRenderer/CnGroupNameCell.vue +61 -0
  91. package/src/components/CnDataTable/CnDataTable.md +9 -0
  92. package/src/components/CnDataTable/CnDataTable.vue +80 -20
  93. package/src/components/CnDetailPage/CnDetailPage.vue +10 -27
  94. package/src/components/CnFavouriteToggle/CnFavouriteToggle.vue +7 -2
  95. package/src/components/CnFilesBrowser/CnFilesBrowser.vue +204 -5
  96. package/src/components/CnFollowToggle/CnFollowToggle.vue +118 -13
  97. package/src/components/CnIndexPage/CnIndexPage.md +36 -0
  98. package/src/components/CnIndexPage/CnIndexPage.vue +106 -19
  99. package/src/components/CnObjectDataWidget/CnObjectDataWidget.vue +226 -2
  100. package/src/components/CnObjectSidebar/CnFilesTab.vue +9 -0
  101. package/src/components/CnWidgetObjectTable/CnWidgetObjectTable.vue +13 -0
  102. package/src/composables/useListView.js +4 -0
  103. package/src/css/look-board-detail.css +40 -2
  104. package/src/css/table.css +82 -17
  105. package/src/index.js +2 -1
  106. package/src/schemas/app-manifest-v2.schema.json +11 -7
  107. package/src/store/plugins/interactions.js +20 -10
  108. package/src/store/useObjectStore.js +17 -0
  109. package/src/utils/builtInFormatters.js +15 -7
  110. package/src/utils/groupAutocomplete.js +75 -0
  111. package/src/utils/index.js +1 -1
  112. package/src/utils/lensAvailability.js +86 -0
  113. package/src/utils/personalLenses.js +13 -6
  114. package/src/utils/recordInteractions.js +31 -13
  115. package/src/utils/resolveFilterTokens.js +45 -2
  116. package/src/utils/schema.js +1 -1
  117. package/src/utils/sentinelTokens.js +4 -4
  118. package/src/utils/validateManifestV2.compiled.js +2 -2
  119. package/src/utils/widgetVisibility.js +45 -0
@@ -10,6 +10,20 @@
10
10
  :formatted="formattedValue"
11
11
  v-bind="widgetProps" />
12
12
 
13
+ <!-- Built-in "group" widget: a Nextcloud group id (or a list of them)
14
+ shown as the group's display name, cached per id. Used on its own
15
+ for a property marked `referenceType: nextcloud-group` /
16
+ `format: nc-group` unless the column has a formatter. -->
17
+ <template v-else-if="isGroupCell">
18
+ <span v-if="!groupIds.length" class="cn-cell-renderer__dash">—</span>
19
+ <span v-else class="cn-cell-renderer__groups">
20
+ <template v-for="(gid, i) in groupIds" :key="gid">
21
+ <span v-if="i > 0">, </span>
22
+ <CnGroupNameCell :gid="gid" />
23
+ </template>
24
+ </span>
25
+ </template>
26
+
13
27
  <!-- Built-in "badge" widget — renders the (possibly formatter-shaped) value as a status pill -->
14
28
  <template v-else-if="widget === 'badge'">
15
29
  <CnStatusBadge v-if="hasValue"
@@ -192,11 +206,12 @@ import { getCanonicalLocale } from '@nextcloud/l10n'
192
206
  import { NcAvatar, NcDateTime } from '@nextcloud/vue'
193
207
  import CheckBold from 'vue-material-design-icons/CheckBold.vue'
194
208
  import CnFkResolveCell from '../CnFkResolveCell/CnFkResolveCell.vue'
209
+ import CnGroupNameCell from './CnGroupNameCell.vue'
195
210
  import { parseDateValue, resolveDateVariant } from '../../utils/dateVariant.js'
196
211
  import { safeCurrencyCode } from '../../utils/formatMetric.js'
197
212
  import { objectFieldValue } from '../../utils/objectName.js'
198
213
  import { safeHref } from '../../utils/safeHref.js'
199
- import { formatValue } from '../../utils/schema.js'
214
+ import { formatValue, isGroupProp } from '../../utils/schema.js'
200
215
  import { CnStatusBadge } from '../CnStatusBadge/index.js'
201
216
 
202
217
  /** A uuid in any version, the shape OpenRegister ids take. */
@@ -226,6 +241,7 @@ export default {
226
241
  components: {
227
242
  CnStatusBadge,
228
243
  CnFkResolveCell,
244
+ CnGroupNameCell,
229
245
  CheckBold,
230
246
  NcAvatar,
231
247
  NcDateTime,
@@ -307,6 +323,9 @@ export default {
307
323
  * a date whose colour follows `widgetProps.variantWhen`, rules on the
308
324
  * number of days until the date (`[{ op: "lt", value: 0, variant:
309
325
  * "error" }, { op: "lte", value: 5, variant: "warning" }]`).
326
+ * The built-in id `"group"` shows a Nextcloud group id (or a list of
327
+ * them) as the group's display name; a column whose property is
328
+ * marked `referenceType: "nextcloud-group"` gets it without asking.
310
329
  * Takes precedence over `formatter`/the type-aware rendering, but the
311
330
  * value handed to the widget is the formatter-shaped `formatted` when
312
331
  * `formatter` is also set.
@@ -577,6 +596,42 @@ export default {
577
596
  return c || null
578
597
  },
579
598
 
599
+ /**
600
+ * Whether this cell shows Nextcloud group names: the column asks for
601
+ * `widget: "group"`, or it names no widget and no formatter and its
602
+ * property is a group (or a list of groups).
603
+ *
604
+ * @spec openspec/changes/nextcloud-group-surfaces/specs/data-display/spec.md#requirement-a-group-cell-shows-the-groups-display-name
605
+ * @return {boolean}
606
+ */
607
+ isGroupCell() {
608
+ if (this.widget === 'group') {
609
+ return true
610
+ }
611
+ if (this.widget || this.hasFormatter) {
612
+ return false
613
+ }
614
+ const p = this.property
615
+ if (!p || typeof p !== 'object') {
616
+ return false
617
+ }
618
+ return isGroupProp(p) || (p.type === 'array' && isGroupProp(p.items))
619
+ },
620
+
621
+ /**
622
+ * The group ids in this cell, empty ones dropped.
623
+ *
624
+ * @spec openspec/changes/nextcloud-group-surfaces/specs/data-display/spec.md#requirement-a-group-cell-shows-the-groups-display-name
625
+ * @return {string[]}
626
+ */
627
+ groupIds() {
628
+ const raw = this.value
629
+ const list = Array.isArray(raw) ? raw : [raw]
630
+ return list
631
+ .filter((gid) => (typeof gid === 'string' && gid !== '') || typeof gid === 'number')
632
+ .map((gid) => String(gid))
633
+ },
634
+
580
635
  /** Variant for the built-in `badge` widget — `widgetProps.variant` or `'default'`. */
581
636
  badgeVariant() {
582
637
  return (this.widgetProps && this.widgetProps.variant) || 'default'
@@ -0,0 +1,61 @@
1
+ <!--
2
+ SPDX-FileCopyrightText: 2026 Conduction B.V.
3
+ SPDX-License-Identifier: EUPL-1.2
4
+
5
+ CnGroupNameCell: one Nextcloud group id shown as the group's display name.
6
+ The name is looked up once per page (utils/groupAutocomplete.js); until it
7
+ arrives, and when it cannot be found, the cell shows the id.
8
+ -->
9
+ <template>
10
+ <span class="cn-group-name-cell" :title="gid">{{ label }}</span>
11
+ </template>
12
+
13
+ <script>
14
+ import { groupDisplayName, loadGroupDisplayName } from '../../utils/groupAutocomplete.js'
15
+
16
+ /**
17
+ * CnGroupNameCell renders a Nextcloud group id as its display name.
18
+ *
19
+ * ```vue
20
+ * <CnGroupNameCell gid="toezicht" />
21
+ * ```
22
+ *
23
+ * @spec openspec/changes/nextcloud-group-surfaces/specs/data-display/spec.md#requirement-a-group-cell-shows-the-groups-display-name
24
+ */
25
+ export default {
26
+ name: 'CnGroupNameCell',
27
+
28
+ props: {
29
+ /** The group id to show. */
30
+ gid: {
31
+ type: String,
32
+ required: true,
33
+ },
34
+ },
35
+
36
+ computed: {
37
+ /**
38
+ * The cached display name, or the id while it loads or when unknown.
39
+ *
40
+ * @return {string}
41
+ */
42
+ label() {
43
+ return groupDisplayName(this.gid) || this.gid
44
+ },
45
+ },
46
+
47
+ watch: {
48
+ gid: {
49
+ immediate: true,
50
+ /**
51
+ * Ask for the name of a new id (cached, so once per id per page).
52
+ *
53
+ * @param {string} gid The group id.
54
+ */
55
+ handler(gid) {
56
+ loadGroupDisplayName(gid)
57
+ },
58
+ },
59
+ },
60
+ }
61
+ </script>
@@ -205,6 +205,7 @@ export default {
205
205
  | `filterable` | Boolean | `false` | Show a filter button in every header whose column can filter: enum, boolean, text, number, date and reference columns backed by a schema property. A column opts out with `filterable: false`. Applying a filter emits `column-filter`; the host owns the state and passes it back as `activeFilters`. `CnIndexPage` turns it on by default. |
206
206
  | `activeFilters` | Object | `{}` | The active filter map, `{ paramKey: values[] }`: the same map the facet sidebar writes, so a header filter and a sidebar filter on one field show the same state. |
207
207
  | `filterRegister` | String | `''` | Register slug a reference column's filter searches in when the column names none. |
208
+ | `lensReasonTexts` | Object \| null | `null` | App wording for an unavailable personal lens in self-fetch mode, keyed `<lens>.<reason>` or `<reason>`. Without it the library's text is used (see below). |
208
209
 
209
210
  ## Slots
210
211
 
@@ -233,6 +234,14 @@ features are folded in here as opt-in props (bare-table usage is unchanged):
233
234
  - `fetchParams` — extra query params for the self-fetch (a resolved filter map,
234
235
  `_order[field]` ordering, `_limit`); changing it re-triggers the fetch. Used by
235
236
  `CnWidgetObjectTable`'s declarative `source`.
237
+ - When the self-fetch asked for a personal lens (a "Recently opened" tile with
238
+ `source.filter._recent: true`) and the response reports it unavailable
239
+ (`@self.lenses.recent.available: false`, openregister#4514), the empty row
240
+ says why instead of `emptyText`: "This server does not keep track of what you
241
+ open." (`audit-trail-disabled`), "Log in to see what you opened recently."
242
+ (`anonymous`) or "Your recent items are not available right now."
243
+ (`read-history-unavailable`). `lensReasonTexts` replaces the wording; no
244
+ report, an available lens or an unknown reason keeps `emptyText`.
236
245
  - `rowClickRoute` — a function mapping a row to a vue-router route. Each such row renders a real `<a href>` in its first cell, stretched over the row, so hovering shows the URL, a plain or alt click routes in place, a ctrl/cmd/shift or middle click opens a new tab, and the row is reachable with Tab. The link is named by the first cell's text ("Open row" when that is empty or not plain text). There is no link on a `selectable` table without `rowClickToView`, where a row click selects. `CnWidgetObjectTable` rows inherit it. Trade-offs: cell text cannot be selected by dragging on a linked row, and a right-click on it shows the browser's link menu only when no host listens to `row-context-menu`.
237
246
  - `hideHeader` — drop the column-label row for a compact list widget.
238
247
  - `#footer` slot — supply a custom footer link (e.g. "+ New" or an always-shown
@@ -130,25 +130,42 @@
130
130
  pinClass(leadingCount + colIndex),
131
131
  ]"
132
132
  :style="{ ...(col.width ? { width: col.width } : {}), ...pinStyle(leadingCount + colIndex) }"
133
- :tabindex="col.sortable ? 0 : null"
134
133
  :aria-sort="ariaSortFor(col)"
135
134
  :data-filtered="isColumnFiltered(col) ? 'true' : null"
136
135
  :title="translateLabel(col.description) || null"
137
136
  @click="col.sortable ? onHeaderClick(col.key, $event) : null"
138
137
  @keydown.enter="col.sortable ? onHeaderKeydown(col.key, $event) : null">
139
- <span :class="col.description ? 'cn-table-header--described' : ''">
138
+ <!-- The sort control is a real button named by the column
139
+ label; the header's `aria-sort` carries the state. Its
140
+ click and Enter bubble to the header's own handlers, so
141
+ a click anywhere in the header still sorts. -->
142
+ <button
143
+ v-if="col.sortable"
144
+ type="button"
145
+ class="cn-table-header__sort"
146
+ data-testid="cn-table-header-sort">
147
+ <span :class="col.description ? 'cn-table-header--described' : ''">{{ translateLabel(col.label) }}</span>
148
+ <span
149
+ v-if="sortKeyIndex(col.key) !== -1"
150
+ class="cn-table-header__chevron cn-table-sort-indicator"
151
+ :data-sort-direction="effectiveSortKeys[sortKeyIndex(col.key)].order === 'asc' ? 'asc' : 'desc'">
152
+ <ChevronUp v-if="effectiveSortKeys[sortKeyIndex(col.key)].order === 'asc'" :size="14" />
153
+ <ChevronDown v-else :size="14" />
154
+ </span>
155
+ <span
156
+ v-else
157
+ class="cn-table-header__chevron cn-table-header__chevron--idle">
158
+ <ChevronDown :size="14" />
159
+ </span>
160
+ <span
161
+ v-if="sortBadgeFor(col.key) !== null"
162
+ class="cn-table-sort-badge">
163
+ {{ sortBadgeFor(col.key) }}
164
+ </span>
165
+ </button>
166
+ <span v-else :class="col.description ? 'cn-table-header--described' : ''">
140
167
  {{ translateLabel(col.label) }}
141
168
  </span>
142
- <span
143
- v-if="col.sortable && sortKeyIndex(col.key) !== -1"
144
- class="cn-table-sort-indicator">
145
- {{ effectiveSortKeys[sortKeyIndex(col.key)].order === 'asc' ? '▲' : '▼' }}
146
- </span>
147
- <span
148
- v-if="col.sortable && sortBadgeFor(col.key) !== null"
149
- class="cn-table-sort-badge">
150
- {{ sortBadgeFor(col.key) }}
151
- </span>
152
169
  <!-- Header filter: a real button, so it is reachable with Tab
153
170
  and its click and Enter never also sort the column. -->
154
171
  <button
@@ -163,8 +180,11 @@
163
180
  data-testid="cn-table-header-filter"
164
181
  @click.stop="toggleColumnFilter(col, $event)"
165
182
  @keydown.enter.stop>
166
- <FilterIcon v-if="isColumnFiltered(col)" :size="16" />
167
- <FilterOutline v-else :size="16" />
183
+ <FilterOutline :size="14" />
184
+ <span
185
+ v-if="isColumnFiltered(col)"
186
+ class="cn-table-header__filter-dot"
187
+ data-testid="cn-table-header-filter-dot" />
168
188
  </button>
169
189
  </th>
170
190
 
@@ -182,9 +202,12 @@
182
202
  <!-- Empty state -->
183
203
  <tr v-if="effectiveRows.length === 0" class="cn-table-empty" data-testid="cn-object-list-empty">
184
204
  <td :colspan="totalColumns">
185
- <!-- @slot Empty-state content shown when there are no rows (defaults to `emptyText`). -->
205
+ <!-- @slot Empty-state content shown when there are no rows (defaults to `emptyText`, or why an unavailable personal lens is empty). -->
186
206
  <slot name="empty">
187
- {{ translateLabel(emptyText) }}
207
+ <span v-if="lensReasonText" class="cn-table-empty__lens-reason" data-testid="cn-data-table-lens-reason">{{ lensReasonText }}</span>
208
+ <template v-else>
209
+ {{ translateLabel(emptyText) }}
210
+ </template>
188
211
  </slot>
189
212
  </td>
190
213
  </tr>
@@ -370,12 +393,14 @@ import axios from '@nextcloud/axios'
370
393
  import { translatePlural as n, translate as t } from '@nextcloud/l10n'
371
394
  import { generateUrl } from '@nextcloud/router'
372
395
  import { NcCheckboxRadioSwitch, NcLoadingIcon } from '@nextcloud/vue'
373
- import FilterIcon from 'vue-material-design-icons/Filter.vue'
396
+ import ChevronDown from 'vue-material-design-icons/ChevronDown.vue'
397
+ import ChevronUp from 'vue-material-design-icons/ChevronUp.vue'
374
398
  import FilterOutline from 'vue-material-design-icons/FilterOutline.vue'
375
399
  import CnColumnFilterPopover from './CnColumnFilterPopover.vue'
376
400
  import { useClickDragGuard } from '../../composables/useClickDragGuard.js'
377
401
  import { normalizeLook } from '../../composables/useLook.js'
378
402
  import { clearedColumnFilterParams, columnFilterDef, columnFilterParams, columnFilterState, isColumnFilterActive, isColumnSortable } from '../../utils/columnFilters.js'
403
+ import { lensUnavailableText, readLensReports } from '../../utils/lensAvailability.js'
379
404
  import { followLinkClick, openRowTarget, resolveHref } from '../../utils/linkNavigation.js'
380
405
  import { nextSortState } from '../../utils/multiColumnSort.js'
381
406
  import { isNewTabClick, isNewTabHandled, isRowMiddleClick, markNewTabHandled, preventMiddleClickAutoscroll } from '../../utils/rowAuxClick.js'
@@ -485,7 +510,8 @@ export default {
485
510
  CnIcon,
486
511
  CnLockIndicator,
487
512
  CnUnreadMarker,
488
- FilterIcon,
513
+ ChevronDown,
514
+ ChevronUp,
489
515
  FilterOutline,
490
516
  },
491
517
 
@@ -666,6 +692,20 @@ export default {
666
692
  default: () => t('nextcloud-vue', 'No items found'),
667
693
  },
668
694
 
695
+ /**
696
+ * App wording for an unavailable personal lens in self-fetch mode,
697
+ * keyed `<lens>.<reason>` (`recent.audit-trail-disabled`) or
698
+ * `<reason>`. Without it the library's object-neutral text is shown
699
+ * when the response reports `@self.lenses.<lens>.available: false`.
700
+ *
701
+ * @type {{[key: string]: string}|null}
702
+ * @spec openspec/changes/lens-says-why-it-is-empty/specs/personal-lens-availability/spec.md#requirement-an-unavailable-lens-explains-its-empty-page
703
+ */
704
+ lensReasonTexts: {
705
+ type: Object,
706
+ default: null,
707
+ },
708
+
669
709
  /** Function returning CSS class(es) for a row: (row) => string|object */
670
710
  rowClass: {
671
711
  type: Function,
@@ -942,6 +982,8 @@ export default {
942
982
  aggregateRequestId: 0,
943
983
  /** Rows fetched in self-fetch mode (register + schemaId). */
944
984
  fetchedRows: [],
985
+ /** Personal-lens reports (`@self.lenses`) of the latest self-fetch response. */
986
+ fetchedLenses: {},
945
987
  /** True while a self-fetch request is in flight. */
946
988
  selfFetchLoading: false,
947
989
  }
@@ -966,6 +1008,22 @@ export default {
966
1008
  return t('nextcloud-vue', 'Actions')
967
1009
  },
968
1010
 
1011
+ /**
1012
+ * Why the self-fetched list is empty when a personal lens could not
1013
+ * answer (openregister#4514), or `''` to keep `emptyText`. Only the
1014
+ * table's own fetch reports: external `rows` carry no report.
1015
+ *
1016
+ * @return {string}
1017
+ * @spec openspec/changes/lens-says-why-it-is-empty/specs/personal-lens-availability/spec.md#requirement-an-unavailable-lens-explains-its-empty-page
1018
+ */
1019
+ lensReasonText() {
1020
+ if (this.rows && this.rows.length > 0) {
1021
+ return ''
1022
+ }
1023
+ const text = lensUnavailableText(this.fetchedLenses, this.lensReasonTexts)
1024
+ return text ? this.translateLabel(text) : ''
1025
+ },
1026
+
969
1027
  /** @return {number} Leading cells before the data columns (selection, icon). */
970
1028
  leadingCount() {
971
1029
  return (this.selectable ? 1 : 0) + (this.rowIcon ? 1 : 0)
@@ -1495,8 +1553,10 @@ export default {
1495
1553
  ...(this.fetchParams ? { params: this.fetchParams } : {}),
1496
1554
  })
1497
1555
  this.fetchedRows = (data && data.results) || (Array.isArray(data) ? data : [])
1556
+ this.fetchedLenses = readLensReports(data)
1498
1557
  } catch {
1499
1558
  this.fetchedRows = []
1559
+ this.fetchedLenses = {}
1500
1560
  } finally {
1501
1561
  this.selfFetchLoading = false
1502
1562
  }
@@ -1975,8 +2035,8 @@ export default {
1975
2035
  filterButtonLabel(col) {
1976
2036
  const column = this.translateLabel(col.label)
1977
2037
  return this.isColumnFiltered(col)
1978
- ? t('nextcloud-vue', 'Filter {column}, active', { column })
1979
- : t('nextcloud-vue', 'Filter {column}', { column })
2038
+ ? t('nextcloud-vue', 'Filter by {column}, active', { column })
2039
+ : t('nextcloud-vue', 'Filter by {column}', { column })
1980
2040
  },
1981
2041
 
1982
2042
  /**
@@ -116,7 +116,7 @@
116
116
  data-testid="cn-detail-page-type-eyebrow">
117
117
  {{ typeEyebrow }}
118
118
  </p>
119
- <div v-if="displayTitle || showFavouriteToggle || showFollowToggle" class="cn-detail-page__title-row">
119
+ <div v-if="displayTitle || showFollowToggle" class="cn-detail-page__title-row">
120
120
  <component
121
121
  :is="isBoard ? 'h1' : 'h2'"
122
122
  v-if="displayTitle"
@@ -124,24 +124,19 @@
124
124
  :title="displayTitle">
125
125
  {{ displayTitle }}
126
126
  </component>
127
- <!-- Star and Follow beside the title (manifest `config.favourite` /
128
- `config.follow`): shown when the object carries the markers. -->
127
+ <!-- The one Follow control beside the title (manifest `config.follow`):
128
+ shown when the object carries `@self.watching`. There is no star:
129
+ a favourite is a follow with notifications off. -->
129
130
  <span
130
- v-if="showFavouriteToggle || showFollowToggle"
131
+ v-if="showFollowToggle"
131
132
  class="cn-detail-page__interactions"
132
133
  data-testid="cn-detail-page-interactions">
133
- <CnFavouriteToggle
134
- v-if="showFavouriteToggle"
135
- :register="register"
136
- :schema="schema"
137
- :objectId="String(objectId)"
138
- :favourite="resolvedSelf.favourite === true" />
139
134
  <CnFollowToggle
140
- v-if="showFollowToggle"
141
135
  :register="register"
142
136
  :schema="schema"
143
137
  :objectId="String(objectId)"
144
138
  :watching="resolvedSelf.watching === true"
139
+ :notify="typeof resolvedSelf.watchNotify === 'boolean' ? resolvedSelf.watchNotify : null"
145
140
  :watcherCount="typeof resolvedSelf.watcherCount === 'number' ? resolvedSelf.watcherCount : null"
146
141
  :canManage="!!(resolvedSelf.can && resolvedSelf.can.manage === true)"
147
142
  :notifies="followNotifies" />
@@ -1211,7 +1206,6 @@ import CnBodySections from '../CnBodySections/CnBodySections.vue'
1211
1206
  import CnBuildiqEditButton from '../CnBuildiqEditButton/CnBuildiqEditButton.vue'
1212
1207
  import CnDashboardGrid from '../CnDashboardGrid/CnDashboardGrid.vue'
1213
1208
  import CnDetailWidgetHost from '../CnDetailWidgetHost/CnDetailWidgetHost.vue'
1214
- import CnFavouriteToggle from '../CnFavouriteToggle/CnFavouriteToggle.vue'
1215
1209
  import CnFollowToggle from '../CnFollowToggle/CnFollowToggle.vue'
1216
1210
  import CnFormDialog from '../CnFormDialog/CnFormDialog.vue'
1217
1211
  import CnLifecycleActions from '../CnLifecycleActions/CnLifecycleActions.vue'
@@ -1413,7 +1407,6 @@ export default {
1413
1407
  CnStatusBadge,
1414
1408
  CnSummaryAggregates,
1415
1409
  CnDetailHeaderChips,
1416
- CnFavouriteToggle,
1417
1410
  CnFollowToggle,
1418
1411
  EyeOffOutline,
1419
1412
  CnRelatedCollections,
@@ -1884,9 +1877,11 @@ export default {
1884
1877
  },
1885
1878
 
1886
1879
  /**
1887
- * Star beside the title (manifest `config.favourite`). Automatic (null):
1888
- * shown when the object carries `@self.favourite`. `false` removes it.
1880
+ * Deprecated and ignored: the star is gone, a favourite is a follow with
1881
+ * notifications off (OpenRegister `merge-follow-and-favourites`). Kept so a
1882
+ * manifest that still sets `config.favourite` stays valid; use `follow`.
1889
1883
  *
1884
+ * @deprecated Use `follow`.
1890
1885
  * @type {boolean|null}
1891
1886
  */
1892
1887
  favourite: {
@@ -3867,18 +3862,6 @@ export default {
3867
3862
  && this.register !== '' && this.schema !== '' && this.objectId !== ''
3868
3863
  },
3869
3864
 
3870
- /**
3871
- * Whether the star renders: the object carries `@self.favourite` and the page did not turn it off.
3872
- *
3873
- * @return {boolean}
3874
- * @spec openspec/changes/record-favourite-and-follow/tasks.md#task-3
3875
- */
3876
- showFavouriteToggle() {
3877
- return this.favourite !== false
3878
- && typeof this.resolvedSelf.favourite === 'boolean'
3879
- && this.register !== '' && this.schema !== '' && this.objectId !== ''
3880
- },
3881
-
3882
3865
  /**
3883
3866
  * Whether the Follow toggle renders: the object carries `@self.watching` and the page did not turn it off.
3884
3867
  *
@@ -31,12 +31,17 @@ import { setFavourite } from '../../utils/recordInteractions.js'
31
31
  /**
32
32
  * CnFavouriteToggle — a star bound to a record's `@self.favourite` marker.
33
33
  *
34
+ * @deprecated Following and favourites are one feature since OpenRegister's
35
+ * `merge-follow-and-favourites`: a favourite is a follow with notifications
36
+ * off. Use `CnFollowToggle`. Neither `CnDetailPage` nor `CnIndexPage` renders
37
+ * this star any more; it stays exported for one minor line so an app that
38
+ * placed it itself keeps working (the server answers it as a quiet follow).
39
+ *
34
40
  * A click flips the star at once and sends `PUT` (star) or `DELETE` (unstar)
35
41
  * `/apps/openregister/api/objects/{register}/{schema}/{id}/favourite`; on
36
42
  * failure it flips back and shows the server's message. The server's answer is
37
43
  * written into the stored object's `@self`, so a list showing the same record
38
- * agrees. `CnDetailPage` renders it beside the title; `CnIndexPage` renders it
39
- * per row for `showFavouriteColumn`.
44
+ * agrees.
40
45
  *
41
46
  * Example:
42
47
  * ```vue