@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
@@ -533,7 +533,8 @@
533
533
  <!-- Empty state -->
534
534
  <div v-else-if="effectiveObjects.length === 0" class="cn-index-page__empty">
535
535
  <slot name="empty">
536
- <CnEmptyContent :name="resolvedEmptyText">
536
+ <CnEmptyContent :name="lensReasonText || resolvedEmptyText"
537
+ :data-lens-unavailable="lensReasonText ? 'true' : null">
537
538
  <template #icon>
538
539
  <CnIcon v-if="resolvedIcon" :name="resolvedIcon" :size="64" />
539
540
  <DatabaseSearch v-else :size="64" />
@@ -573,14 +574,15 @@
573
574
  @rowClick="onRowClick"
574
575
  @rowAuxClick="onRowAuxClick"
575
576
  @rowContextMenu="onRowContextMenu">
576
- <!-- Star column (showFavouriteColumn) -->
577
- <template v-if="showFavouriteColumn" #column-__favourite="{ row }">
578
- <CnFavouriteToggle
579
- v-if="row && row['@self'] && typeof row['@self'].favourite === 'boolean'"
577
+ <!-- Follow column (showFollowColumn, or the deprecated showFavouriteColumn) -->
578
+ <template v-if="followColumnOn" #column-__follow="{ row }">
579
+ <CnFollowToggle
580
+ v-if="row && row['@self'] && typeof row['@self'].watching === 'boolean'"
581
+ compact
580
582
  :register="typeof register === 'string' ? register : ''"
581
583
  :schema="favouriteSchemaSlug"
582
584
  :objectId="String(row['@self'].id || row.id || '')"
583
- :favourite="row['@self'].favourite === true" />
585
+ :watching="row['@self'].watching === true" />
584
586
  </template>
585
587
 
586
588
  <!-- Pass through column slots -->
@@ -955,7 +957,7 @@ import CnConfirmDialog from '../../dialogs/CnConfirmDialog.vue'
955
957
  import CnQuickEditDialog from '../../dialogs/CnQuickEditDialog.vue'
956
958
  import CnBuildiqEditButton from '../CnBuildiqEditButton/CnBuildiqEditButton.vue'
957
959
  import CnEmptyContent from '../CnEmptyContent/CnEmptyContent.vue'
958
- import CnFavouriteToggle from '../CnFavouriteToggle/CnFavouriteToggle.vue'
960
+ import CnFollowToggle from '../CnFollowToggle/CnFollowToggle.vue'
959
961
  import { useContextMenu } from '../../composables/index.js'
960
962
  import { useLook } from '../../composables/useLook.js'
961
963
  import { copyKindsOf } from '../../composables/useObjectCopy.js'
@@ -968,6 +970,7 @@ import { buildOnSuccessRoute, resolveRegisteredHandler } from '../../utils/actio
968
970
  import { reportBindingProblems } from '../../utils/diagnostics.js'
969
971
  import { fetchFilterCounts } from '../../utils/fetchFilterCounts.js'
970
972
  import { buildExportUrl } from '../../utils/indexExportHelpers.js'
973
+ import { lensUnavailableText } from '../../utils/lensAvailability.js'
971
974
  import { openRowTarget } from '../../utils/linkNavigation.js'
972
975
  import { resolveClaimedTeams, resolveClaimTokens, splitViewsIntoTabs, viewAsTab, viewIdOf } from '../../utils/listLenses.js'
973
976
  import { LIST_SHORTCUTS, listPaletteCommands, shortcutFor } from '../../utils/listShortcuts.js'
@@ -1037,8 +1040,8 @@ const CALENDAR_PAGE_SIZE = 200
1037
1040
  */
1038
1041
  const RANGE_SEPARATOR = '..'
1039
1042
 
1040
- /** Key of the synthetic star column (`showFavouriteColumn`). */
1041
- const FAVOURITE_COLUMN_KEY = '__favourite'
1043
+ /** Key of the synthetic follow column (`showFollowColumn`). */
1044
+ const FOLLOW_COLUMN_KEY = '__follow'
1042
1045
 
1043
1046
  /**
1044
1047
  * Whether a schema property wants a from/to pair rather than a value list.
@@ -1240,7 +1243,7 @@ export default {
1240
1243
 
1241
1244
  components: {
1242
1245
  CnBuildiqEditButton,
1243
- CnFavouriteToggle,
1246
+ CnFollowToggle,
1244
1247
  NcLoadingIcon,
1245
1248
  CnEmptyContent,
1246
1249
  NcActions,
@@ -1576,9 +1579,10 @@ export default {
1576
1579
  },
1577
1580
 
1578
1581
  /**
1579
- * Personal lenses appended to the quick filters: any of `favourite`
1580
- * (`_favourite`), `recent` (`_recent`), `watching` (`_watching`) and
1581
- * `unread` (`_unread`).
1582
+ * Personal lenses appended to the quick filters: any of `watching`
1583
+ * (`_watching`, labelled Following), `recent` (`_recent`) and `unread`
1584
+ * (`_unread`). `favourite` is a deprecated alias of `watching`: a
1585
+ * favourite is a follow now, and asking for both gives one tab.
1582
1586
  * They combine with every other filter. While Recent is active column
1583
1587
  * sorting is off, because the lens owns the order. Manifest
1584
1588
  * `config.personalLenses`.
@@ -1591,9 +1595,20 @@ export default {
1591
1595
  },
1592
1596
 
1593
1597
  /**
1594
- * Add a first column with a star per row (`CnFavouriteToggle`), bound to
1595
- * each row's `@self.favourite`. Clicking it does not open the row.
1596
- * Manifest `config.showFavouriteColumn`.
1598
+ * Add a first column with a follow toggle per row (`CnFollowToggle`,
1599
+ * compact), bound to each row's `@self.watching`. Clicking it does not
1600
+ * open the row. Manifest `config.showFollowColumn`.
1601
+ */
1602
+ showFollowColumn: {
1603
+ type: Boolean,
1604
+ default: false,
1605
+ },
1606
+
1607
+ /**
1608
+ * Deprecated alias of `showFollowColumn`: the star column became the
1609
+ * follow column (OpenRegister `merge-follow-and-favourites`).
1610
+ *
1611
+ * @deprecated Use `showFollowColumn`.
1597
1612
  */
1598
1613
  showFavouriteColumn: {
1599
1614
  type: Boolean,
@@ -2088,6 +2103,34 @@ export default {
2088
2103
  default: 'No items found',
2089
2104
  },
2090
2105
 
2106
+ /**
2107
+ * Personal-lens report of the list on screen, for a host that fetches
2108
+ * itself: the response's `@self.lenses` (`{ recent: { available,
2109
+ * reason } }`). In self-fetch mode the page reads it from the store
2110
+ * and this prop is ignored. When a lens reports `available: false`
2111
+ * the empty state explains why instead of showing `emptyText`.
2112
+ *
2113
+ * @type {object|null}
2114
+ * @spec openspec/changes/lens-says-why-it-is-empty/specs/personal-lens-availability/spec.md#requirement-an-unavailable-lens-explains-its-empty-page
2115
+ */
2116
+ lenses: {
2117
+ type: Object,
2118
+ default: null,
2119
+ },
2120
+
2121
+ /**
2122
+ * App wording for an unavailable lens, keyed `<lens>.<reason>`
2123
+ * (`recent.audit-trail-disabled`) or `<reason>`; the specific key
2124
+ * wins, and both win over the library's object-neutral text.
2125
+ *
2126
+ * @type {{[key: string]: string}|null}
2127
+ * @spec openspec/changes/lens-says-why-it-is-empty/specs/personal-lens-availability/spec.md#requirement-an-unavailable-lens-explains-its-empty-page
2128
+ */
2129
+ lensReasonTexts: {
2130
+ type: Object,
2131
+ default: null,
2132
+ },
2133
+
2091
2134
  /** Accessible label for the loading spinner (NcLoadingIcon aria-label) */
2092
2135
  loadingText: {
2093
2136
  type: String,
@@ -3279,6 +3322,40 @@ export default {
3279
3322
  return this.emptyText ? fn(this.emptyText) : this.emptyText
3280
3323
  },
3281
3324
 
3325
+ /**
3326
+ * The personal-lens reports of the list on screen: the store's copy
3327
+ * of the latest response in self-fetch mode, else the `lenses` prop.
3328
+ *
3329
+ * @return {object}
3330
+ * @spec openspec/changes/lens-says-why-it-is-empty/specs/personal-lens-availability/spec.md#requirement-a-list-response-carries-the-report-of-the-lenses-it-was-asked-for
3331
+ */
3332
+ effectiveLenses() {
3333
+ if (this.isNamedSource) {
3334
+ return {}
3335
+ }
3336
+ if (this.isSelfFetchMode) {
3337
+ return (this.list.lenses && this.list.lenses.value) || {}
3338
+ }
3339
+ return this.lenses || {}
3340
+ },
3341
+
3342
+ /**
3343
+ * Why the list is empty when a personal lens could not answer (the
3344
+ * audit trail is off, nobody is logged in, the history could not be
3345
+ * read), or `''` to keep the generic empty text.
3346
+ *
3347
+ * @return {string}
3348
+ * @spec openspec/changes/lens-says-why-it-is-empty/specs/personal-lens-availability/spec.md#requirement-an-unavailable-lens-explains-its-empty-page
3349
+ */
3350
+ lensReasonText() {
3351
+ const text = lensUnavailableText(this.effectiveLenses, this.lensReasonTexts)
3352
+ if (!text) {
3353
+ return ''
3354
+ }
3355
+ const fn = typeof this.cnTranslate === 'function' ? this.cnTranslate : (k) => k
3356
+ return fn(text)
3357
+ },
3358
+
3282
3359
  /**
3283
3360
  * Whether the host manifest editor is in edit mode (unwraps the injected
3284
3361
  * `cnEditingBody`, which may be a Vue ref or a plain boolean). Drives the
@@ -4172,12 +4249,22 @@ export default {
4172
4249
  if (this.recentLensActive) {
4173
4250
  cols = cols.map((col) => (col && typeof col === 'object' ? { ...col, sortable: false } : col))
4174
4251
  }
4175
- if (this.showFavouriteColumn && this.favouriteSchemaSlug !== '') {
4176
- cols = [{ key: FAVOURITE_COLUMN_KEY, label: '', sortable: false, width: '48px' }, ...cols]
4252
+ if (this.followColumnOn && this.favouriteSchemaSlug !== '') {
4253
+ cols = [{ key: FOLLOW_COLUMN_KEY, label: '', sortable: false, width: '48px' }, ...cols]
4177
4254
  }
4178
4255
  return cols
4179
4256
  },
4180
4257
 
4258
+ /**
4259
+ * Whether the follow column renders (`showFollowColumn`, or the deprecated `showFavouriteColumn`).
4260
+ *
4261
+ * @return {boolean}
4262
+ * @spec openspec/changes/one-follow-control/specs/record-follow/spec.md#requirement-index-pages-offer-one-following-lens-and-a-follow-column
4263
+ */
4264
+ followColumnOn() {
4265
+ return this.showFollowColumn || this.showFavouriteColumn
4266
+ },
4267
+
4181
4268
  /**
4182
4269
  * Whether the active quick filter is the Recent lens.
4183
4270
  *
@@ -4192,7 +4279,7 @@ export default {
4192
4279
  },
4193
4280
 
4194
4281
  /**
4195
- * Schema slug the star column calls with: the page's own slug in self-fetch
4282
+ * Schema slug the follow column calls with: the page's own slug in self-fetch
4196
4283
  * mode, else the schema object's `slug`. Empty when neither is known.
4197
4284
  *
4198
4285
  * @return {string}
@@ -5069,6 +5156,7 @@ export default {
5069
5156
  * stable key and a variant.
5070
5157
  *
5071
5158
  * @spec openspec/changes/zuiddrecht-pixel-gaps-3/specs/zuiddrecht-pixel-gaps-3/spec.md#requirement-an-index-page-can-take-the-board-header
5159
+ * @spec openspec/changes/screens-index-header-buttons-parity/specs/index-list-board-look/spec.md#requirement-the-add-header-button-carries-a-plus
5072
5160
  * @return {Array<{key: string, label: string, action: string, variant: string, icon: string, format: string}>}
5073
5161
  */
5074
5162
  resolvedHeaderButtons() {
@@ -5081,6 +5169,11 @@ export default {
5081
5169
  if (label === '' && button.action === 'add') {
5082
5170
  label = this.resolvedAddLabel
5083
5171
  }
5172
+ // The board's primary button reads "+ New request": the Add
5173
+ // button takes the plus unless the manifest names an icon.
5174
+ if (this.isBoardLook && button.action === 'add' && icon === '') {
5175
+ icon = 'Plus'
5176
+ }
5084
5177
  // The board's header: Export reads "Download" with its icon and
5085
5178
  // the Actions menu reads "Actions", without the manifest saying so.
5086
5179
  if (this.isBoardLook && label === '' && button.action === 'export') {
@@ -149,6 +149,28 @@
149
149
  @update:modelValue="onRelationChange(field, $event)"
150
150
  @close="commitEdit" />
151
151
 
152
+ <!-- Nextcloud group or user (`referenceType: nextcloud-group`
153
+ / `nextcloud-user`, one or a list): pick by display name,
154
+ store the group id or uid. The same search the create and
155
+ edit dialogs use. -->
156
+ <component
157
+ :is="isUserPicker(field) ? 'NcSelectUsers' : 'NcSelect'"
158
+ v-else-if="isPersonPicker(field)"
159
+ ref="activeEditor"
160
+ :inputLabel="field.label"
161
+ :labelOutside="true"
162
+ :options="personOptions[field.key] || []"
163
+ :modelValue="personSelected(field)"
164
+ :multiple="isPersonMultiple(field)"
165
+ :keepOpen="isPersonMultiple(field)"
166
+ :loading="personLoading === field.key"
167
+ :filterable="false"
168
+ :clearable="!field.required"
169
+ label="label"
170
+ @search="(q) => searchPeople(field, q)"
171
+ @update:modelValue="onPersonChange(field, $event)"
172
+ @close="isPersonMultiple(field) ? undefined : commitEdit()" />
173
+
152
174
  <!-- Select -->
153
175
  <NcSelect
154
176
  v-else-if="field.widget === 'select'"
@@ -259,7 +281,17 @@
259
281
  :value="displayValues[field.key]"
260
282
  :raw="(objectData || {})[field.key]" />
261
283
  <template v-else>
262
- <img v-if="isImageField(field) && rawOf(field)"
284
+ <!-- A Nextcloud group (or list): its display name, cached
285
+ per id; the id while the name loads or when unknown. -->
286
+ <span
287
+ v-if="isGroupPicker(field) && groupIdsOf(field).length"
288
+ class="cn-object-data-widget__groups">
289
+ <template v-for="(gid, gi) in groupIdsOf(field)" :key="gid">
290
+ <span v-if="gi > 0">, </span>
291
+ <CnGroupNameCell :gid="gid" />
292
+ </template>
293
+ </span>
294
+ <img v-else-if="isImageField(field) && rawOf(field)"
263
295
  :src="rawOf(field)"
264
296
  :alt="field.label"
265
297
  class="cn-object-data-widget__image">
@@ -370,19 +402,22 @@
370
402
  import axios from '@nextcloud/axios'
371
403
  import { translate as t } from '@nextcloud/l10n'
372
404
  import { generateUrl } from '@nextcloud/router'
373
- import { NcActionButton, NcButton, NcCheckboxRadioSwitch, NcLoadingIcon, NcSelect, NcTextField } from '@nextcloud/vue'
405
+ import { NcActionButton, NcButton, NcCheckboxRadioSwitch, NcLoadingIcon, NcSelect, NcSelectUsers, NcTextField } from '@nextcloud/vue'
374
406
  import Check from 'vue-material-design-icons/Check.vue'
375
407
  import Close from 'vue-material-design-icons/Close.vue'
376
408
  import ContentSaveOutline from 'vue-material-design-icons/ContentSaveOutline.vue'
377
409
  import InformationOutline from 'vue-material-design-icons/InformationOutline.vue'
378
410
  import Pencil from 'vue-material-design-icons/Pencil.vue'
411
+ import CnGroupNameCell from '../CnCellRenderer/CnGroupNameCell.vue'
379
412
  import CnFormDialog from '../CnFormDialog/CnFormDialog.vue'
380
413
  import { normalizeLook } from '../../composables/useLook.js'
381
414
  import { useObjectStore } from '../../store/index.js'
415
+ import { groupDisplayName, loadGroupDisplayName, searchNextcloudGroups } from '../../utils/groupAutocomplete.js'
382
416
  import { PANEL_ACTION_SINK } from '../../utils/panelActions.js'
383
417
  import { resolveFilterTokens } from '../../utils/resolveFilterTokens.js'
384
418
  import { fieldsFromSchema, formatValue } from '../../utils/schema.js'
385
419
  import { schemaRefSlug } from '../../utils/schemaRefSlug.js'
420
+ import { resolveNextcloudUser, searchNextcloudUsers } from '../../utils/userAutocomplete.js'
386
421
  import { CnIcon } from '../CnIcon/index.js'
387
422
  import { CnObjectMetadataModal } from '../CnObjectMetadataModal/index.js'
388
423
  import { CnWidgetWrapper } from '../CnWidgetWrapper/index.js'
@@ -427,6 +462,8 @@ export default {
427
462
  NcLoadingIcon,
428
463
  NcTextField,
429
464
  NcSelect,
465
+ NcSelectUsers,
466
+ CnGroupNameCell,
430
467
  NcCheckboxRadioSwitch,
431
468
  NcActionButton,
432
469
  CnWidgetWrapper,
@@ -806,6 +843,12 @@ export default {
806
843
  relationOptions: {},
807
844
  /** Whether relation picker options are being fetched. */
808
845
  relationOptionsLoading: false,
846
+ /** Group and user picker options per field key ({ id, label }[]). */
847
+ personOptions: {},
848
+ /** Key of the group or user field whose options are loading, or null. */
849
+ personLoading: null,
850
+ /** Display names of users seen in a user picker, by uid. */
851
+ userLabels: {},
809
852
  /** Whether the user expanded the widget to see every field. */
810
853
  expanded: false,
811
854
  /** Fields that fit the cell while collapsed; null when all fit. */
@@ -1719,6 +1762,11 @@ export default {
1719
1762
  if (this.isSingleRelationField(field.key)) {
1720
1763
  this.loadRelationOptions(field.key)
1721
1764
  }
1765
+ // Group and user fields pick from Nextcloud's own directory.
1766
+ if (this.isPersonPicker(field)) {
1767
+ this.searchPeople(field, '')
1768
+ this.resolvePersonLabels(field, currentValue)
1769
+ }
1722
1770
 
1723
1771
  this.$nextTick(() => {
1724
1772
  // Focus the editor
@@ -1944,6 +1992,182 @@ export default {
1944
1992
  return options.find((opt) => opt.id === val) || { id: val, label: String(val) }
1945
1993
  },
1946
1994
 
1995
+ /**
1996
+ * Whether a field picks a Nextcloud group (widget `group` or
1997
+ * `group-multiselect`, from `referenceType: nextcloud-group`).
1998
+ *
1999
+ * @spec openspec/changes/nextcloud-group-surfaces/specs/data-display/spec.md#requirement-the-inline-editor-picks-a-nextcloud-group-or-user
2000
+ * @param {object} field Field descriptor.
2001
+ * @return {boolean}
2002
+ */
2003
+ isGroupPicker(field) {
2004
+ return !!field && (field.widget === 'group' || field.widget === 'group-multiselect')
2005
+ },
2006
+
2007
+ /**
2008
+ * Whether a field picks a Nextcloud user (widget `user` or
2009
+ * `user-multiselect`, from `referenceType: nextcloud-user`).
2010
+ *
2011
+ * @spec openspec/changes/nextcloud-group-surfaces/specs/data-display/spec.md#requirement-the-inline-editor-picks-a-nextcloud-group-or-user
2012
+ * @param {object} field Field descriptor.
2013
+ * @return {boolean}
2014
+ */
2015
+ isUserPicker(field) {
2016
+ return !!field && (field.widget === 'user' || field.widget === 'user-multiselect')
2017
+ },
2018
+
2019
+ /**
2020
+ * Whether a field picks a group or a user.
2021
+ *
2022
+ * @spec openspec/changes/nextcloud-group-surfaces/specs/data-display/spec.md#requirement-the-inline-editor-picks-a-nextcloud-group-or-user
2023
+ * @param {object} field Field descriptor.
2024
+ * @return {boolean}
2025
+ */
2026
+ isPersonPicker(field) {
2027
+ return this.isGroupPicker(field) || this.isUserPicker(field)
2028
+ },
2029
+
2030
+ /**
2031
+ * Whether a group or user field holds a list.
2032
+ *
2033
+ * @spec openspec/changes/nextcloud-group-surfaces/specs/data-display/spec.md#requirement-the-inline-editor-picks-a-nextcloud-group-or-user
2034
+ * @param {object} field Field descriptor.
2035
+ * @return {boolean}
2036
+ */
2037
+ isPersonMultiple(field) {
2038
+ return !!field && (field.widget === 'group-multiselect' || field.widget === 'user-multiselect')
2039
+ },
2040
+
2041
+ /**
2042
+ * The group ids a group field holds, for the display mode.
2043
+ *
2044
+ * @spec openspec/changes/nextcloud-group-surfaces/specs/data-display/spec.md#requirement-the-inline-editor-picks-a-nextcloud-group-or-user
2045
+ * @param {object} field Field descriptor.
2046
+ * @return {string[]}
2047
+ */
2048
+ groupIdsOf(field) {
2049
+ const raw = this.rawOf(field)
2050
+ const list = Array.isArray(raw) ? raw : [raw]
2051
+ return list.filter((v) => typeof v === 'string' && v !== '')
2052
+ },
2053
+
2054
+ /**
2055
+ * The display name of a group id or uid, falling back to the id.
2056
+ *
2057
+ * @spec openspec/changes/nextcloud-group-surfaces/specs/data-display/spec.md#requirement-the-inline-editor-picks-a-nextcloud-group-or-user
2058
+ * @param {object} field Field descriptor.
2059
+ * @param {string} id The group id or uid.
2060
+ * @return {string}
2061
+ */
2062
+ personLabel(field, id) {
2063
+ if (this.isGroupPicker(field)) {
2064
+ return groupDisplayName(id) || String(id)
2065
+ }
2066
+ return this.userLabels[id] || String(id)
2067
+ },
2068
+
2069
+ /**
2070
+ * Search Nextcloud groups or users for a picker. Fails soft (the
2071
+ * helpers return `[]`), so the picker stays usable with what it has.
2072
+ *
2073
+ * @spec openspec/changes/nextcloud-group-surfaces/specs/data-display/spec.md#requirement-the-inline-editor-picks-a-nextcloud-group-or-user
2074
+ * @param {object} field Field descriptor.
2075
+ * @param {string} query The search term.
2076
+ * @return {Promise<void>}
2077
+ */
2078
+ async searchPeople(field, query) {
2079
+ this.personLoading = field.key
2080
+ try {
2081
+ const options = this.isGroupPicker(field)
2082
+ ? await searchNextcloudGroups(query || '')
2083
+ : await searchNextcloudUsers(query || '')
2084
+ const list = Array.isArray(options) ? options : []
2085
+ if (this.isUserPicker(field)) {
2086
+ const labels = { ...this.userLabels }
2087
+ for (const opt of list) {
2088
+ labels[opt.id] = opt.label
2089
+ }
2090
+ this.userLabels = labels
2091
+ }
2092
+ this.personOptions = { ...this.personOptions, [field.key]: list }
2093
+ } finally {
2094
+ if (this.personLoading === field.key) {
2095
+ this.personLoading = null
2096
+ }
2097
+ }
2098
+ },
2099
+
2100
+ /**
2101
+ * Look up the display names of the value a group or user field holds,
2102
+ * so the open editor shows names rather than ids.
2103
+ *
2104
+ * @spec openspec/changes/nextcloud-group-surfaces/specs/data-display/spec.md#requirement-the-inline-editor-picks-a-nextcloud-group-or-user
2105
+ * @param {object} field Field descriptor.
2106
+ * @param {unknown} value The held value (an id or a list of ids).
2107
+ * @return {Promise<void>}
2108
+ */
2109
+ async resolvePersonLabels(field, value) {
2110
+ const ids = (Array.isArray(value) ? value : [value])
2111
+ .filter((v) => typeof v === 'string' && v !== '')
2112
+ if (this.isGroupPicker(field)) {
2113
+ await Promise.all(ids.map((gid) => loadGroupDisplayName(gid)))
2114
+ return
2115
+ }
2116
+ const unknown = ids.filter((uid) => !this.userLabels[uid])
2117
+ const options = await Promise.all(unknown.map((uid) => resolveNextcloudUser(uid)))
2118
+ if (options.length > 0) {
2119
+ const labels = { ...this.userLabels }
2120
+ for (const opt of options) {
2121
+ if (opt && opt.id) {
2122
+ labels[opt.id] = opt.label || opt.id
2123
+ }
2124
+ }
2125
+ this.userLabels = labels
2126
+ }
2127
+ },
2128
+
2129
+ /**
2130
+ * The selected option(s) of a group or user picker.
2131
+ *
2132
+ * @spec openspec/changes/nextcloud-group-surfaces/specs/data-display/spec.md#requirement-the-inline-editor-picks-a-nextcloud-group-or-user
2133
+ * @param {object} field Field descriptor.
2134
+ * @return {object|object[]|null}
2135
+ */
2136
+ personSelected(field) {
2137
+ const toOption = (id) => {
2138
+ const label = this.personLabel(field, id)
2139
+ return { id, label, displayName: label }
2140
+ }
2141
+ const v = this.editData[field.key]
2142
+ if (this.isPersonMultiple(field)) {
2143
+ return (Array.isArray(v) ? v : [])
2144
+ .filter((id) => typeof id === 'string' && id !== '')
2145
+ .map(toOption)
2146
+ }
2147
+ return (typeof v === 'string' && v !== '') ? toOption(v) : null
2148
+ },
2149
+
2150
+ /**
2151
+ * Store a group or user choice as the id (or list of ids).
2152
+ *
2153
+ * @spec openspec/changes/nextcloud-group-surfaces/specs/data-display/spec.md#requirement-the-inline-editor-picks-a-nextcloud-group-or-user
2154
+ * @param {object} field Field descriptor.
2155
+ * @param {object|object[]|null} selected The chosen option(s).
2156
+ */
2157
+ onPersonChange(field, selected) {
2158
+ if (this.isPersonMultiple(field)) {
2159
+ const ids = (Array.isArray(selected) ? selected : [])
2160
+ .map((opt) => (opt && typeof opt === 'object' ? opt.id : opt))
2161
+ .filter((id) => typeof id === 'string' && id !== '')
2162
+ this.updateField(field.key, ids)
2163
+ return
2164
+ }
2165
+ if (selected && this.isUserPicker(field) && selected.id) {
2166
+ this.userLabels = { ...this.userLabels, [selected.id]: selected.label || selected.id }
2167
+ }
2168
+ this.updateField(field.key, selected && selected.id ? selected.id : null)
2169
+ },
2170
+
1947
2171
  onSelectChange(field, option) {
1948
2172
  this.updateField(field.key, option ? option.id : null)
1949
2173
  },
@@ -8,6 +8,9 @@
8
8
  v-if="browserRoot !== null"
9
9
  :rootPath="browserRoot"
10
10
  :rootLabel="browserRootLabel"
11
+ :uploadButton="uploadButton"
12
+ :dropOverlay="dropOverlay"
13
+ :dropHint="dropHint"
11
14
  :rowActions="rowActions"
12
15
  :newActions="newActions"
13
16
  :linkedItems="linkedItems"
@@ -307,6 +310,12 @@ export default {
307
310
  deleteLabel: { type: String, default: () => t('nextcloud-vue', 'Delete') },
308
311
  /** What the files browser's root crumb reads; null shows the folder's own name, as the Files app does. */
309
312
  browserRootLabel: { type: String, default: null },
313
+ /** Forwarded to CnFilesBrowser: a primary "Add files" button beside the New menu. */
314
+ uploadButton: { type: Boolean, default: false },
315
+ /** Forwarded to CnFilesBrowser: the drop state drawn over the list ("Drop to add") and announced. */
316
+ dropOverlay: { type: Boolean, default: false },
317
+ /** Forwarded to CnFilesBrowser: "Or drag files onto this list." under the list. */
318
+ dropHint: { type: Boolean, default: false },
310
319
  /**
311
320
  * The host's own actions on each file row of the browser, forwarded
312
321
  * to CnFilesBrowser's `rowActions`: declared like any manifest action
@@ -33,11 +33,14 @@
33
33
  </div>
34
34
 
35
35
  <div class="cn-kpi-card__body cn-stat-widget__body">
36
- <div v-if="content.label || rangePresets.length" class="cn-kpi-card__title cn-stat-widget__label">
36
+ <div
37
+ v-if="content.label || rangePresets.length"
38
+ class="cn-kpi-card__title cn-stat-widget__label"
39
+ :class="{ 'cn-kpi-card__title--badge-end': hasEndBadge }">
37
40
  <!-- Board look: the icon is an 18px glyph before the label, and
38
41
  only when the tile links to a filtered list. -->
39
42
  <span
40
- v-if="isBoardLook && isLinked && resolvedIcon"
43
+ v-if="isBoardLook && isLinked && resolvedIcon && !hasEndBadge"
41
44
  class="cn-kpi-card__glyph cn-stat-widget__glyph"
42
45
  data-testid="cn-stat-widget-glyph"
43
46
  aria-hidden="true">
@@ -66,6 +69,17 @@
66
69
  {{ effectiveTranslate(preset.label || preset.id) }}
67
70
  </option>
68
71
  </select>
72
+ <!-- Board look, `content.iconPlacement: "end"`: the icon sits in a
73
+ 32px circle at the right of the label row, tinted by the
74
+ tile's state (DqDashboard). -->
75
+ <span
76
+ v-if="hasEndBadge"
77
+ class="cn-kpi-card__badge cn-stat-widget__badge-icon"
78
+ :class="'cn-kpi-card__badge--' + stateTone"
79
+ data-testid="cn-stat-widget-icon-badge"
80
+ aria-hidden="true">
81
+ <CnWidgetIcon :name="resolvedIcon" :size="18" />
82
+ </span>
69
83
  </div>
70
84
 
71
85
  <div class="cn-kpi-card__value-row cn-stat-widget__value-row">
@@ -1330,9 +1344,45 @@ export default {
1330
1344
  if (layout === 'vertical' || layout === 'stacked') {
1331
1345
  return layout
1332
1346
  }
1347
+ // Board look: every KPI tile on the screens is stacked, so a tile
1348
+ // that names no layout takes it. An explicit `horizontal` stays.
1349
+ if (this.isBoardLook && layout === undefined) {
1350
+ return 'stacked'
1351
+ }
1333
1352
  return 'horizontal'
1334
1353
  },
1335
1354
 
1355
+ /**
1356
+ * Whether the board look draws the icon in a tinted circle at the
1357
+ * right of the label row (`content.iconPlacement: "end"`), as the
1358
+ * DqDashboard tiles. Off without the board look and without an icon.
1359
+ *
1360
+ * @spec openspec/changes/screens-stat-tile-parity/specs/dashboard-page/spec.md#requirement-the-board-kpi-tile-can-carry-its-icon-at-the-end
1361
+ * @return {boolean}
1362
+ */
1363
+ hasEndBadge() {
1364
+ return this.isBoardLook
1365
+ && (this.content || {}).iconPlacement === 'end'
1366
+ && Boolean(this.resolvedIcon)
1367
+ },
1368
+
1369
+ /**
1370
+ * The tile's state as a tone name for the end badge: the variant the
1371
+ * value colour comes from (`success`, `warning`, `error`), else
1372
+ * `primary`. A custom `valueColor` has no tone and reads as primary.
1373
+ *
1374
+ * @spec openspec/changes/screens-stat-tile-parity/specs/dashboard-page/spec.md#requirement-the-board-kpi-tile-can-carry-its-icon-at-the-end
1375
+ * @return {'primary'|'success'|'warning'|'error'}
1376
+ */
1377
+ stateTone() {
1378
+ const color = this.variantColor
1379
+ if (!color) {
1380
+ return 'primary'
1381
+ }
1382
+ const tone = ['success', 'warning', 'error'].find((name) => VARIANT_COLORS[name] === color)
1383
+ return tone || 'primary'
1384
+ },
1385
+
1336
1386
  /**
1337
1387
  * Whether the tile takes the stacked board look (`content.layout:
1338
1388
  * "stacked"`): a plain muted label, the number at 34px in the text
@@ -10,6 +10,8 @@
10
10
  </template>
11
11
 
12
12
  <script>
13
+ import { isBadgeVariant } from '../../utils/badgeVariants.js'
14
+
13
15
  /**
14
16
  * CnStatusBadge — Color-coded pill badge for status, priority, or category display.
15
17
  *
@@ -39,12 +41,14 @@ export default {
39
41
  },
40
42
 
41
43
  /**
42
- * Color variant: 'default', 'primary', 'success', 'warning', 'error', 'info'
44
+ * Color variant: 'default', 'primary', 'success', 'warning', 'error',
45
+ * 'info', or one of the two board tones without a Nextcloud role,
46
+ * 'purple' (work in progress) and 'teal' (handed on).
43
47
  */
44
48
  variant: {
45
49
  type: String,
46
50
  default: 'default',
47
- validator: (v) => ['default', 'primary', 'success', 'warning', 'error', 'info'].includes(v),
51
+ validator: (v) => isBadgeVariant(v),
48
52
  },
49
53
 
50
54
  /** Size: 'small' or 'medium' */
@@ -370,6 +370,19 @@ export default {
370
370
  default: undefined,
371
371
  },
372
372
 
373
+ /**
374
+ * App wording for an unavailable personal lens (a `source.filter._recent`
375
+ * tile on an instance that does not log reads), keyed `<lens>.<reason>`
376
+ * or `<reason>`. Forwarded to CnDataTable, which shows the library's
377
+ * object-neutral text without it.
378
+ *
379
+ * @spec openspec/changes/lens-says-why-it-is-empty/specs/personal-lens-availability/spec.md#requirement-an-unavailable-lens-explains-its-empty-page
380
+ */
381
+ lensReasonTexts: {
382
+ type: Object,
383
+ default: undefined,
384
+ },
385
+
373
386
  /**
374
387
  * Leading per-row icon: a static MDI icon name string, or a function
375
388
  * `(row) => iconName`. Forwarded to CnDataTable.