@conduction/nextcloud-vue 2.76.0 → 2.78.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 (173) hide show
  1. package/dist/esm/components/CnAiCompanion/CnAiCompanion.vue2.js +23 -0
  2. package/dist/esm/components/CnAiCompanion/CnAiCompanion.vue2.js.map +1 -1
  3. package/dist/esm/components/CnAiCompanion/CnAiCompanion.vue3.js.map +1 -1
  4. package/dist/esm/components/CnAppNav/CnAppNav.vue2.js +18 -3
  5. package/dist/esm/components/CnAppNav/CnAppNav.vue2.js.map +1 -1
  6. package/dist/esm/components/CnAppNav/CnAppNav.vue3.js +49 -3
  7. package/dist/esm/components/CnAppNav/CnAppNav.vue3.js.map +1 -1
  8. package/dist/esm/components/CnBoardView/CnBoardView.vue2.js +2 -1
  9. package/dist/esm/components/CnBoardView/CnBoardView.vue2.js.map +1 -1
  10. package/dist/esm/components/CnBoardView/CnBoardView.vue3.js.map +1 -1
  11. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js +79 -1
  12. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js.map +1 -1
  13. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js +229 -203
  14. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js.map +1 -1
  15. package/dist/esm/components/CnCellRenderer/CnGroupNameCell.vue.js +9 -0
  16. package/dist/esm/components/CnCellRenderer/CnGroupNameCell.vue.js.map +1 -0
  17. package/dist/esm/components/CnCellRenderer/CnGroupNameCell.vue2.js +50 -0
  18. package/dist/esm/components/CnCellRenderer/CnGroupNameCell.vue2.js.map +1 -0
  19. package/dist/esm/components/CnCellRenderer/CnGroupNameCell.vue3.js +13 -0
  20. package/dist/esm/components/CnCellRenderer/CnGroupNameCell.vue3.js.map +1 -0
  21. package/dist/esm/components/CnChartWidget/CnChartWidget.vue2.js +37 -0
  22. package/dist/esm/components/CnChartWidget/CnChartWidget.vue2.js.map +1 -1
  23. package/dist/esm/components/CnChartWidget/CnChartWidget.vue3.js +2 -2
  24. package/dist/esm/components/CnChartWidget/CnChartWidget.vue3.js.map +1 -1
  25. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +29 -4
  26. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  27. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  28. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +48 -6
  29. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
  30. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +90 -51
  31. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
  32. package/dist/esm/components/CnDateRangePicker/CnDateRangePicker.vue.js +1 -1
  33. package/dist/esm/components/CnDateRangePicker/CnDateRangePicker.vue2.js +45 -2
  34. package/dist/esm/components/CnDateRangePicker/CnDateRangePicker.vue2.js.map +1 -1
  35. package/dist/esm/components/CnDateRangePicker/CnDateRangePicker.vue3.js.map +1 -1
  36. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +16 -28
  37. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  38. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +12 -24
  39. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  40. package/dist/esm/components/CnFavouriteToggle/CnFavouriteToggle.vue2.js +7 -2
  41. package/dist/esm/components/CnFavouriteToggle/CnFavouriteToggle.vue2.js.map +1 -1
  42. package/dist/esm/components/CnFavouriteToggle/CnFavouriteToggle.vue3.js.map +1 -1
  43. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js +114 -1
  44. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js.map +1 -1
  45. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js +115 -62
  46. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js.map +1 -1
  47. package/dist/esm/components/CnFollowToggle/CnFollowToggle.vue2.js +89 -7
  48. package/dist/esm/components/CnFollowToggle/CnFollowToggle.vue2.js.map +1 -1
  49. package/dist/esm/components/CnFollowToggle/CnFollowToggle.vue3.js +62 -30
  50. package/dist/esm/components/CnFollowToggle/CnFollowToggle.vue3.js.map +1 -1
  51. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js +7 -1
  52. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
  53. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
  54. package/dist/esm/components/CnFormPage/CnFormPage.vue2.js +4 -1
  55. package/dist/esm/components/CnFormPage/CnFormPage.vue2.js.map +1 -1
  56. package/dist/esm/components/CnFormPage/CnFormPage.vue3.js.map +1 -1
  57. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +104 -13
  58. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  59. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +13 -9
  60. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  61. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js +199 -7
  62. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js.map +1 -1
  63. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue3.js +155 -119
  64. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue3.js.map +1 -1
  65. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js +6 -0
  66. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js.map +1 -1
  67. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js +4 -1
  68. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js.map +1 -1
  69. package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js +36 -0
  70. package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js.map +1 -1
  71. package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js +35 -23
  72. package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js.map +1 -1
  73. package/dist/esm/components/CnStatusBadge/CnStatusBadge.vue2.js +6 -2
  74. package/dist/esm/components/CnStatusBadge/CnStatusBadge.vue2.js.map +1 -1
  75. package/dist/esm/components/CnStatusBadge/CnStatusBadge.vue3.js.map +1 -1
  76. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js +13 -0
  77. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js.map +1 -1
  78. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue3.js.map +1 -1
  79. package/dist/esm/composables/useFormDraft.js +11 -1
  80. package/dist/esm/composables/useFormDraft.js.map +1 -1
  81. package/dist/esm/composables/useListView.js +4 -0
  82. package/dist/esm/composables/useListView.js.map +1 -1
  83. package/dist/esm/composables/useObjectPresence.js +15 -4
  84. package/dist/esm/composables/useObjectPresence.js.map +1 -1
  85. package/dist/esm/index.js +2 -1
  86. package/dist/esm/index.js.map +1 -1
  87. package/dist/esm/l10n/en.json.js +32 -1
  88. package/dist/esm/l10n/en.json.js.map +1 -1
  89. package/dist/esm/l10n/nl.json.js +32 -1
  90. package/dist/esm/l10n/nl.json.js.map +1 -1
  91. package/dist/esm/nextcloud-vue.css +500 -75
  92. package/dist/esm/store/plugins/interactions.js +20 -10
  93. package/dist/esm/store/plugins/interactions.js.map +1 -1
  94. package/dist/esm/store/useObjectStore.js +17 -0
  95. package/dist/esm/store/useObjectStore.js.map +1 -1
  96. package/dist/esm/utils/badgeVariants.js +32 -0
  97. package/dist/esm/utils/badgeVariants.js.map +1 -0
  98. package/dist/esm/utils/builtInFormatters.js +15 -7
  99. package/dist/esm/utils/builtInFormatters.js.map +1 -1
  100. package/dist/esm/utils/groupAutocomplete.js +63 -1
  101. package/dist/esm/utils/groupAutocomplete.js.map +1 -1
  102. package/dist/esm/utils/lensAvailability.js +90 -0
  103. package/dist/esm/utils/lensAvailability.js.map +1 -0
  104. package/dist/esm/utils/personalLenses.js +13 -6
  105. package/dist/esm/utils/personalLenses.js.map +1 -1
  106. package/dist/esm/utils/recordInteractions.js +31 -13
  107. package/dist/esm/utils/recordInteractions.js.map +1 -1
  108. package/dist/esm/utils/resolveFilterTokens.js +46 -3
  109. package/dist/esm/utils/resolveFilterTokens.js.map +1 -1
  110. package/dist/esm/utils/schema.js +1 -1
  111. package/dist/esm/utils/schema.js.map +1 -1
  112. package/dist/esm/utils/sentinelTokens.js +4 -4
  113. package/dist/esm/utils/sentinelTokens.js.map +1 -1
  114. package/dist/esm/utils/validateManifestV2.compiled.js +2 -2
  115. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  116. package/dist/esm/utils/widgetVisibility.js +46 -1
  117. package/dist/esm/utils/widgetVisibility.js.map +1 -1
  118. package/dist/nextcloud-vue.cjs.js +4389 -2890
  119. package/dist/nextcloud-vue.cjs.js.map +2 -2
  120. package/dist/nextcloud-vue.css +500 -75
  121. package/l10n/en.json +32 -1
  122. package/l10n/nl.json +32 -1
  123. package/package.json +1 -1
  124. package/src/components/CnAiCompanion/CnAiCompanion.vue +23 -0
  125. package/src/components/CnAppNav/CnAppNav.vue +44 -5
  126. package/src/components/CnBoardView/CnBoardView.vue +2 -1
  127. package/src/components/CnCellRenderer/CnCellRenderer.md +1 -0
  128. package/src/components/CnCellRenderer/CnCellRenderer.vue +106 -9
  129. package/src/components/CnCellRenderer/CnGroupNameCell.vue +61 -0
  130. package/src/components/CnChartWidget/CnChartWidget.vue +39 -2
  131. package/src/components/CnDashboardPage/CnDashboardPage.vue +29 -4
  132. package/src/components/CnDataTable/CnDataTable.md +9 -0
  133. package/src/components/CnDataTable/CnDataTable.vue +87 -22
  134. package/src/components/CnDateRangePicker/CnDateRangePicker.vue +44 -1
  135. package/src/components/CnDetailPage/CnDetailPage.vue +10 -27
  136. package/src/components/CnFavouriteToggle/CnFavouriteToggle.vue +7 -2
  137. package/src/components/CnFilesBrowser/CnFilesBrowser.vue +204 -5
  138. package/src/components/CnFollowToggle/CnFollowToggle.vue +118 -13
  139. package/src/components/CnFormDialog/CnFormDialog.vue +7 -1
  140. package/src/components/CnFormPage/CnFormPage.vue +4 -1
  141. package/src/components/CnIndexPage/CnIndexPage.md +36 -0
  142. package/src/components/CnIndexPage/CnIndexPage.vue +112 -19
  143. package/src/components/CnObjectDataWidget/CnObjectDataWidget.vue +226 -2
  144. package/src/components/CnObjectSidebar/CnFilesTab.vue +9 -0
  145. package/src/components/CnStatWidget/CnStatWidget.vue +52 -2
  146. package/src/components/CnStatusBadge/CnStatusBadge.vue +6 -2
  147. package/src/components/CnWidgetObjectTable/CnWidgetObjectTable.vue +13 -0
  148. package/src/composables/__tests__/useObjectPresence.spec.js +24 -0
  149. package/src/composables/useFormDraft.js +11 -1
  150. package/src/composables/useListView.js +4 -0
  151. package/src/composables/useObjectPresence.js +15 -4
  152. package/src/css/badge.css +31 -0
  153. package/src/css/kpi-card.css +51 -0
  154. package/src/css/look-board-detail.css +40 -2
  155. package/src/css/look-board-index.css +36 -0
  156. package/src/css/look-board.css +8 -0
  157. package/src/css/table.css +82 -17
  158. package/src/index.js +2 -1
  159. package/src/schemas/app-manifest-v2.schema.json +15 -11
  160. package/src/store/plugins/interactions.js +20 -10
  161. package/src/store/useObjectStore.js +17 -0
  162. package/src/utils/badgeVariants.js +29 -0
  163. package/src/utils/builtInFormatters.js +15 -7
  164. package/src/utils/groupAutocomplete.js +75 -0
  165. package/src/utils/index.js +1 -1
  166. package/src/utils/lensAvailability.js +86 -0
  167. package/src/utils/personalLenses.js +13 -6
  168. package/src/utils/recordInteractions.js +31 -13
  169. package/src/utils/resolveFilterTokens.js +45 -2
  170. package/src/utils/schema.js +1 -1
  171. package/src/utils/sentinelTokens.js +4 -4
  172. package/src/utils/validateManifestV2.compiled.js +2 -2
  173. package/src/utils/widgetVisibility.js +45 -0
@@ -883,7 +883,7 @@ import CnBodySections from '../CnBodySections/CnBodySections.vue'
883
883
  import CnBuildiqEditButton from '../CnBuildiqEditButton/CnBuildiqEditButton.vue'
884
884
  import CnChartWidget from '../CnChartWidget/CnChartWidget.vue'
885
885
  import CnDashboardGrid from '../CnDashboardGrid/CnDashboardGrid.vue'
886
- import CnDateRangePicker, { DEFAULT_DATE_RANGE_PRESETS, resolvePresetWindow } from '../CnDateRangePicker/CnDateRangePicker.vue'
886
+ import CnDateRangePicker, { DEFAULT_DATE_RANGE_PRESETS, resolvePresetWindow, translatePresetLabel } from '../CnDateRangePicker/CnDateRangePicker.vue'
887
887
  import CnEmptyContent from '../CnEmptyContent/CnEmptyContent.vue'
888
888
  import CnKpiGrid from '../CnKpiGrid/CnKpiGrid.vue'
889
889
  import CnSegmentedControl from '../CnSegmentedControl/CnSegmentedControl.vue'
@@ -2009,9 +2009,12 @@ export default {
2009
2009
  * doesn't supply one.
2010
2010
  */
2011
2011
  effectivePresets() {
2012
- return Array.isArray(this.dateRange?.presets) && this.dateRange.presets.length > 0
2012
+ const presets = Array.isArray(this.dateRange?.presets) && this.dateRange.presets.length > 0
2013
2013
  ? this.dateRange.presets
2014
- : DEFAULT_DATE_RANGE_PRESETS.map((p) => ({ ...p }))
2014
+ : DEFAULT_DATE_RANGE_PRESETS
2015
+ // Labels in the user's language (screens-dashboard-i18n): the app's
2016
+ // catalogue first, then the library's own for the default labels.
2017
+ return presets.map((p) => ({ ...p, label: translatePresetLabel(p.label, this.effectiveTranslate) }))
2015
2018
  },
2016
2019
 
2017
2020
  /**
@@ -3354,7 +3357,7 @@ export default {
3354
3357
  * @return {object}
3355
3358
  */
3356
3359
  registryWidgetBindings(item) {
3357
- const content = this.getWidgetContent(item)
3360
+ const content = this.collectionContent(this.getWidgetContent(item))
3358
3361
  const def = this.getWidgetDef(item.widgetId)
3359
3362
  if (!this.isBannerDef(def)) {
3360
3363
  return content
@@ -3363,6 +3366,28 @@ export default {
3363
3366
  return outcome ? { ...content, conditionOutcome: outcome } : content
3364
3367
  },
3365
3368
 
3369
+ /**
3370
+ * A widget's content with the board's quiet collection footer: in the
3371
+ * board look a dashboard that drops the widget Actions menu
3372
+ * (`showWidgetActions: false`) also drops the Add footer of its list
3373
+ * widgets, whose Add is the menu's create entry at the bottom of the
3374
+ * card. A widget that says `content.allowCreate` keeps its own value.
3375
+ * Otherwise the content is returned as is.
3376
+ *
3377
+ * @spec openspec/changes/screens-dashboard-legacy-widgets/specs/dashboard-page/spec.md#requirement-a-board-dashboard-without-widget-actions-drops-the-add-footer
3378
+ * @param {object} content The widget's stored content.
3379
+ * @return {object}
3380
+ */
3381
+ collectionContent(content) {
3382
+ if (!this.isBoardLook || this.showWidgetActions !== false) {
3383
+ return content
3384
+ }
3385
+ if (content && Object.hasOwn(content, 'allowCreate')) {
3386
+ return content
3387
+ }
3388
+ return { ...content, allowCreate: false }
3389
+ },
3390
+
3366
3391
  /**
3367
3392
  * Re-compact a filtered layout upward, display-only. GridStack runs
3368
3393
  * `float: true`, so removing an item does not close its row — this
@@ -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
  }
@@ -1586,19 +1646,24 @@ export default {
1586
1646
  * handed a `:schema` for at all) still get type-aware rendering.
1587
1647
  * `enumLabels` rides with `enum` rather than getting its own `if`
1588
1648
  * branch: a column can't usefully declare labels without also
1589
- * declaring the codes they label.
1649
+ * declaring the codes they label. A column `colorMap` (raw value to
1650
+ * badge variant) colours the enum pill from the manifest and wins over
1651
+ * the schema property's own `colorMap` / `x-color-map`.
1590
1652
  *
1591
1653
  * @param {object} col Column definition.
1592
1654
  * @return {object} Property definition for CnCellRenderer.
1655
+ * @spec openspec/changes/screens-cell-pill-parity/specs/cell-pill-tones/spec.md#requirement-a-manifest-column-colours-its-enum-pills
1593
1656
  */
1594
1657
  columnProperty(col) {
1595
1658
  const base = this.getSchemaProperty(col.key)
1596
- if (col && (col.format || col.type || col.enum)) {
1659
+ const colorMap = col && col.colorMap && typeof col.colorMap === 'object' && !Array.isArray(col.colorMap) ? col.colorMap : null
1660
+ if (col && (col.format || col.type || col.enum || colorMap)) {
1597
1661
  return {
1598
1662
  ...base,
1599
1663
  ...(col.type ? { type: col.type } : {}),
1600
1664
  ...(col.format ? { format: col.format } : {}),
1601
1665
  ...(col.enum ? { enum: col.enum, enumLabels: col.enumLabels || base.enumLabels } : {}),
1666
+ ...(colorMap ? { colorMap } : {}),
1602
1667
  }
1603
1668
  }
1604
1669
  return base
@@ -1975,8 +2040,8 @@ export default {
1975
2040
  filterButtonLabel(col) {
1976
2041
  const column = this.translateLabel(col.label)
1977
2042
  return this.isColumnFiltered(col)
1978
- ? t('nextcloud-vue', 'Filter {column}, active', { column })
1979
- : t('nextcloud-vue', 'Filter {column}', { column })
2043
+ ? t('nextcloud-vue', 'Filter by {column}, active', { column })
2044
+ : t('nextcloud-vue', 'Filter by {column}', { column })
1980
2045
  },
1981
2046
 
1982
2047
  /**
@@ -69,6 +69,44 @@ export const DEFAULT_DATE_RANGE_PRESETS = Object.freeze([
69
69
  { id: 'custom', label: 'Custom range', days: null },
70
70
  ])
71
71
 
72
+ /**
73
+ * The library's own translations of the default preset labels. Literal
74
+ * `t()` calls, so the strings are extracted into the `nextcloud-vue`
75
+ * catalogue.
76
+ *
77
+ * @type {{[label: string]: function(): string}}
78
+ */
79
+ const LIBRARY_PRESET_LABELS = Object.freeze({
80
+ 'Last 8 hours': () => t('nextcloud-vue', 'Last 8 hours'),
81
+ 'Last 24 hours': () => t('nextcloud-vue', 'Last 24 hours'),
82
+ Today: () => t('nextcloud-vue', 'Today'),
83
+ 'Last 7 days': () => t('nextcloud-vue', 'Last 7 days'),
84
+ 'Last 30 days': () => t('nextcloud-vue', 'Last 30 days'),
85
+ 'Last 90 days': () => t('nextcloud-vue', 'Last 90 days'),
86
+ 'Custom range': () => t('nextcloud-vue', 'Custom range'),
87
+ })
88
+
89
+ /**
90
+ * A preset label in the user's language: the host translation when the app's
91
+ * catalogue has the key, else the library's own translation of a default
92
+ * label ("Last 30 days"), else the label as written.
93
+ *
94
+ * @spec openspec/changes/screens-dashboard-i18n/specs/dashboard-page/spec.md#requirement-date-range-preset-labels-are-translated
95
+ * @param {string} label The preset label (an i18n key).
96
+ * @param {function(string): string} [translate] The host translate function.
97
+ * @return {string} The translated label.
98
+ */
99
+ export function translatePresetLabel(label, translate) {
100
+ if (typeof label !== 'string' || label === '') {
101
+ return ''
102
+ }
103
+ const own = typeof translate === 'function' ? translate(label) : label
104
+ if (typeof own === 'string' && own !== '' && own !== label) {
105
+ return own
106
+ }
107
+ return Object.hasOwn(LIBRARY_PRESET_LABELS, label) ? LIBRARY_PRESET_LABELS[label]() : label
108
+ }
109
+
72
110
  /**
73
111
  * Resolve a `period` preset into its calendar-aligned `{ from, to }`
74
112
  * window — the CURRENT week / month / quarter / year to date, not a
@@ -196,6 +234,11 @@ export default {
196
234
  NcSelect,
197
235
  },
198
236
 
237
+ inject: {
238
+ /** Host translate function provided by CnAppRoot, for app-authored preset labels. */
239
+ cnTranslate: { default: () => (key) => key },
240
+ },
241
+
199
242
  props: {
200
243
  /**
201
244
  * Current range value. `from` / `to` are ISO-8601 UTC strings;
@@ -327,7 +370,7 @@ export default {
327
370
  },
328
371
 
329
372
  presetOptions() {
330
- return this.presets.map((p) => ({ id: p.id, label: p.label, days: p.days }))
373
+ return this.presets.map((p) => ({ id: p.id, label: translatePresetLabel(p.label, this.cnTranslate), days: p.days }))
331
374
  },
332
375
 
333
376
  selectedPresetOption() {
@@ -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