@conduction/nextcloud-vue 2.75.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 (158) 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/CnCardGrid/CnCardGrid.vue2.js +6 -0
  6. package/dist/esm/components/CnCardGrid/CnCardGrid.vue2.js.map +1 -1
  7. package/dist/esm/components/CnCardGrid/CnCardGrid.vue3.js +3 -2
  8. package/dist/esm/components/CnCardGrid/CnCardGrid.vue3.js.map +1 -1
  9. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js +42 -1
  10. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js.map +1 -1
  11. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js +228 -203
  12. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js.map +1 -1
  13. package/dist/esm/components/CnCellRenderer/CnGroupNameCell.vue.js +9 -0
  14. package/dist/esm/components/CnCellRenderer/CnGroupNameCell.vue.js.map +1 -0
  15. package/dist/esm/components/CnCellRenderer/CnGroupNameCell.vue2.js +50 -0
  16. package/dist/esm/components/CnCellRenderer/CnGroupNameCell.vue2.js.map +1 -0
  17. package/dist/esm/components/CnCellRenderer/CnGroupNameCell.vue3.js +13 -0
  18. package/dist/esm/components/CnCellRenderer/CnGroupNameCell.vue3.js.map +1 -0
  19. package/dist/esm/components/CnCredentials/CnCredentials.vue2.js +11 -10
  20. package/dist/esm/components/CnCredentials/CnCredentials.vue2.js.map +1 -1
  21. package/dist/esm/components/CnCredentials/CnCredentials.vue3.js.map +1 -1
  22. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +41 -4
  23. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
  24. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +90 -51
  25. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
  26. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +24 -33
  27. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  28. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +12 -24
  29. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  30. package/dist/esm/components/CnFavouriteToggle/CnFavouriteToggle.vue2.js +7 -2
  31. package/dist/esm/components/CnFavouriteToggle/CnFavouriteToggle.vue2.js.map +1 -1
  32. package/dist/esm/components/CnFavouriteToggle/CnFavouriteToggle.vue3.js.map +1 -1
  33. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js +114 -1
  34. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js.map +1 -1
  35. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js +115 -62
  36. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js.map +1 -1
  37. package/dist/esm/components/CnFollowToggle/CnFollowToggle.vue2.js +89 -7
  38. package/dist/esm/components/CnFollowToggle/CnFollowToggle.vue2.js.map +1 -1
  39. package/dist/esm/components/CnFollowToggle/CnFollowToggle.vue3.js +62 -30
  40. package/dist/esm/components/CnFollowToggle/CnFollowToggle.vue3.js.map +1 -1
  41. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +242 -19
  42. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  43. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +25 -20
  44. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  45. package/dist/esm/components/CnIndexPage/selfModeActions.js +90 -14
  46. package/dist/esm/components/CnIndexPage/selfModeActions.js.map +1 -1
  47. package/dist/esm/components/CnIndexPage/useSelfFetchList.js +15 -3
  48. package/dist/esm/components/CnIndexPage/useSelfFetchList.js.map +1 -1
  49. package/dist/esm/components/CnObjectCard/CnObjectCard.vue2.js +26 -0
  50. package/dist/esm/components/CnObjectCard/CnObjectCard.vue2.js.map +1 -1
  51. package/dist/esm/components/CnObjectCard/CnObjectCard.vue3.js +26 -9
  52. package/dist/esm/components/CnObjectCard/CnObjectCard.vue3.js.map +1 -1
  53. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js +199 -7
  54. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js.map +1 -1
  55. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue3.js +155 -119
  56. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue3.js.map +1 -1
  57. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +19 -3
  58. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
  59. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js.map +1 -1
  60. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js +6 -0
  61. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js.map +1 -1
  62. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js +4 -1
  63. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js.map +1 -1
  64. package/dist/esm/components/CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue2.js +153 -38
  65. package/dist/esm/components/CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue2.js.map +1 -1
  66. package/dist/esm/components/CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue3.js +74 -56
  67. package/dist/esm/components/CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue3.js.map +1 -1
  68. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js +13 -0
  69. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js.map +1 -1
  70. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue3.js.map +1 -1
  71. package/dist/esm/composables/useListView.js +4 -0
  72. package/dist/esm/composables/useListView.js.map +1 -1
  73. package/dist/esm/composables/useObjectLock.js +41 -10
  74. package/dist/esm/composables/useObjectLock.js.map +1 -1
  75. package/dist/esm/index.js +3 -1
  76. package/dist/esm/index.js.map +1 -1
  77. package/dist/esm/l10n/en.json.js +31 -1
  78. package/dist/esm/l10n/en.json.js.map +1 -1
  79. package/dist/esm/l10n/nl.json.js +32 -2
  80. package/dist/esm/l10n/nl.json.js.map +1 -1
  81. package/dist/esm/nextcloud-vue.css +376 -75
  82. package/dist/esm/store/plugins/interactions.js +20 -10
  83. package/dist/esm/store/plugins/interactions.js.map +1 -1
  84. package/dist/esm/store/useObjectStore.js +54 -9
  85. package/dist/esm/store/useObjectStore.js.map +1 -1
  86. package/dist/esm/utils/builtInFormatters.js +15 -7
  87. package/dist/esm/utils/builtInFormatters.js.map +1 -1
  88. package/dist/esm/utils/groupAutocomplete.js +63 -1
  89. package/dist/esm/utils/groupAutocomplete.js.map +1 -1
  90. package/dist/esm/utils/lensAvailability.js +90 -0
  91. package/dist/esm/utils/lensAvailability.js.map +1 -0
  92. package/dist/esm/utils/objectSignals.js +44 -0
  93. package/dist/esm/utils/objectSignals.js.map +1 -0
  94. package/dist/esm/utils/personalLenses.js +13 -6
  95. package/dist/esm/utils/personalLenses.js.map +1 -1
  96. package/dist/esm/utils/recordInteractions.js +31 -13
  97. package/dist/esm/utils/recordInteractions.js.map +1 -1
  98. package/dist/esm/utils/resolveFilterTokens.js +46 -3
  99. package/dist/esm/utils/resolveFilterTokens.js.map +1 -1
  100. package/dist/esm/utils/schema.js +1 -1
  101. package/dist/esm/utils/schema.js.map +1 -1
  102. package/dist/esm/utils/sentinelTokens.js +4 -4
  103. package/dist/esm/utils/sentinelTokens.js.map +1 -1
  104. package/dist/esm/utils/validateManifestV2.compiled.js +2 -2
  105. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  106. package/dist/esm/utils/widgetVisibility.js +46 -1
  107. package/dist/esm/utils/widgetVisibility.js.map +1 -1
  108. package/dist/nextcloud-vue.cjs.js +4732 -2990
  109. package/dist/nextcloud-vue.cjs.js.map +2 -2
  110. package/dist/nextcloud-vue.css +376 -75
  111. package/l10n/en.json +31 -1
  112. package/l10n/nl.json +32 -2
  113. package/package.json +1 -1
  114. package/src/components/CnAppNav/CnAppNav.vue +44 -5
  115. package/src/components/CnCardGrid/CnCardGrid.md +1 -0
  116. package/src/components/CnCardGrid/CnCardGrid.vue +7 -0
  117. package/src/components/CnCellRenderer/CnCellRenderer.vue +56 -1
  118. package/src/components/CnCellRenderer/CnGroupNameCell.vue +61 -0
  119. package/src/components/CnCredentials/CnCredentials.vue +17 -16
  120. package/src/components/CnDataTable/CnDataTable.md +9 -0
  121. package/src/components/CnDataTable/CnDataTable.vue +80 -20
  122. package/src/components/CnDetailPage/CnDetailPage.vue +18 -32
  123. package/src/components/CnFavouriteToggle/CnFavouriteToggle.vue +7 -2
  124. package/src/components/CnFilesBrowser/CnFilesBrowser.vue +204 -5
  125. package/src/components/CnFollowToggle/CnFollowToggle.vue +118 -13
  126. package/src/components/CnIndexPage/CnIndexPage.md +51 -0
  127. package/src/components/CnIndexPage/CnIndexPage.vue +261 -35
  128. package/src/components/CnIndexPage/selfModeActions.js +89 -13
  129. package/src/components/CnIndexPage/useSelfFetchList.js +15 -3
  130. package/src/components/CnObjectCard/CnObjectCard.md +1 -0
  131. package/src/components/CnObjectCard/CnObjectCard.vue +89 -1
  132. package/src/components/CnObjectDataWidget/CnObjectDataWidget.vue +226 -2
  133. package/src/components/CnObjectListWidget/CnObjectListWidget.vue +19 -3
  134. package/src/components/CnObjectSidebar/CnFilesTab.vue +9 -0
  135. package/src/components/CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue +181 -38
  136. package/src/components/CnWidgetObjectTable/CnWidgetObjectTable.vue +13 -0
  137. package/src/composables/__tests__/useObjectLock.endpoints.spec.js +83 -0
  138. package/src/composables/useListView.js +4 -0
  139. package/src/composables/useObjectLock.js +41 -10
  140. package/src/css/look-board-detail.css +40 -2
  141. package/src/css/look-board-index.css +35 -1
  142. package/src/css/table.css +82 -17
  143. package/src/index.js +3 -1
  144. package/src/schemas/app-manifest-v2.schema.json +11 -7
  145. package/src/store/plugins/interactions.js +20 -10
  146. package/src/store/useObjectStore.js +54 -9
  147. package/src/utils/builtInFormatters.js +15 -7
  148. package/src/utils/groupAutocomplete.js +75 -0
  149. package/src/utils/index.js +2 -1
  150. package/src/utils/lensAvailability.js +86 -0
  151. package/src/utils/objectSignals.js +41 -0
  152. package/src/utils/personalLenses.js +13 -6
  153. package/src/utils/recordInteractions.js +31 -13
  154. package/src/utils/resolveFilterTokens.js +45 -2
  155. package/src/utils/schema.js +1 -1
  156. package/src/utils/sentinelTokens.js +4 -4
  157. package/src/utils/validateManifestV2.compiled.js +2 -2
  158. package/src/utils/widgetVisibility.js +45 -0
@@ -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: {
@@ -2727,18 +2722,21 @@ export default {
2727
2722
  () => props.objectId,
2728
2723
  { enabled: () => Boolean(resolveType() && props.objectId) },
2729
2724
  )
2730
- const sidebarReg = props.sidebarProps?.register || props.resolvedSidebar?.register || props.register || ''
2731
- const sidebarSchema = props.sidebarProps?.schema || props.resolvedSidebar?.schema || props.schema || ''
2725
+ // Getters, read at call time: a register or schema that arrives after
2726
+ // mount (route params, a resolved sidebar) must reach the lock URL. They
2727
+ // used to be consts captured once at setup.
2728
+ const sidebarReg = () => props.sidebarProps?.register || props.resolvedSidebar?.register || props.register || ''
2729
+ const sidebarSchema = () => props.sidebarProps?.schema || props.resolvedSidebar?.schema || props.schema || ''
2732
2730
  const lock = useObjectLock(
2733
2731
  subscriptionStore,
2734
- () => sidebarReg,
2735
- () => resolveType() || sidebarSchema,
2732
+ sidebarReg,
2733
+ () => resolveType() || sidebarSchema(),
2736
2734
  () => props.objectId,
2737
2735
  // The third argument is the OBJECT CACHE KEY (`<register>-<schema>`)
2738
2736
  // and the lock URL needs the schema SLUG. They were one string, and
2739
2737
  // the acquire went to `/api/objects/<register>/<register>-<schema>/`,
2740
2738
  // which no register answers.
2741
- { schemaSlug: () => props.schema || sidebarSchema },
2739
+ { schemaSlug: () => props.schema || sidebarSchema() },
2742
2740
  )
2743
2741
  return {
2744
2742
  ...registryExposed,
@@ -3864,18 +3862,6 @@ export default {
3864
3862
  && this.register !== '' && this.schema !== '' && this.objectId !== ''
3865
3863
  },
3866
3864
 
3867
- /**
3868
- * Whether the star renders: the object carries `@self.favourite` and the page did not turn it off.
3869
- *
3870
- * @return {boolean}
3871
- * @spec openspec/changes/record-favourite-and-follow/tasks.md#task-3
3872
- */
3873
- showFavouriteToggle() {
3874
- return this.favourite !== false
3875
- && typeof this.resolvedSelf.favourite === 'boolean'
3876
- && this.register !== '' && this.schema !== '' && this.objectId !== ''
3877
- },
3878
-
3879
3865
  /**
3880
3866
  * Whether the Follow toggle renders: the object carries `@self.watching` and the page did not turn it off.
3881
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
@@ -7,10 +7,29 @@
7
7
  class="cn-files-browser"
8
8
  :class="{ 'cn-files-browser--dragging': dragging }"
9
9
  data-testid="cn-files-browser"
10
- @dragenter.prevent="dragging = true"
11
- @dragover.prevent="dragging = true"
12
- @dragleave.prevent="dragging = false"
10
+ @dragenter.prevent="onDragEnter"
11
+ @dragover.prevent="onDragOver"
12
+ @dragleave.prevent="onDragLeave"
13
13
  @drop.prevent="onDrop">
14
+ <!-- The drop state, drawn on the browser itself when the host names it
15
+ (`dropOverlay`): a dashed frame and one centred line over the list,
16
+ so it is plain where the files go. The overlay is decoration for
17
+ sighted users; the live region below says the same thing to a
18
+ screen reader, and the upload button stays the way in for anyone
19
+ who cannot drag. -->
20
+ <div v-if="dropOverlay && dragging"
21
+ class="cn-files-browser__drop-overlay"
22
+ aria-hidden="true"
23
+ data-testid="cn-files-browser-drop-overlay">
24
+ <span class="cn-files-browser__drop-icon"><Upload :size="28" /></span>
25
+ <strong class="cn-files-browser__drop-label">{{ dropLabel }}</strong>
26
+ </div>
27
+ <p v-if="dropOverlay"
28
+ class="hidden-visually"
29
+ aria-live="polite"
30
+ data-testid="cn-files-browser-drop-status">
31
+ {{ dropStatus }}
32
+ </p>
14
33
  <!-- The bar: where you are, and what you can add. The crumbs are the
15
34
  Files app's own breadcrumb component; the New menu lists what the
16
35
  Files app and its plugins registered for this folder (a new folder,
@@ -56,7 +75,7 @@
56
75
  :menuName="newLabel"
57
76
  :forceMenu="true"
58
77
  :forceName="true"
59
- variant="primary"
78
+ :variant="uploadButton ? 'secondary' : 'primary'"
60
79
  :disabled="folder === null"
61
80
  data-testid="cn-files-browser-new">
62
81
  <template #icon>
@@ -94,6 +113,19 @@
94
113
  {{ action.label }}
95
114
  </NcActionButton>
96
115
  </NcActions>
116
+ <!-- A plain upload as its own button, when the host names it
117
+ (`uploadButton`). It takes the primary place and the New
118
+ menu steps back beside it. -->
119
+ <NcButton v-if="uploadButton"
120
+ variant="primary"
121
+ :disabled="folder === null"
122
+ data-testid="cn-files-browser-upload-button"
123
+ @click="pickFiles">
124
+ <template #icon>
125
+ <Upload :size="20" />
126
+ </template>
127
+ {{ uploadButtonLabel }}
128
+ </NcButton>
97
129
  </div>
98
130
 
99
131
  <!-- `:ref` (dynamic), not `ref`: a static ref on a vnode with only static
@@ -344,6 +376,13 @@
344
376
  </tbody>
345
377
  </table>
346
378
 
379
+ <p v-if="dropHint && !loading && error === ''"
380
+ class="cn-files-browser__drop-hint"
381
+ data-testid="cn-files-browser-drop-hint">
382
+ <Upload :size="16" />
383
+ {{ dropHintLabel }}
384
+ </p>
385
+
347
386
  <NcDialog
348
387
  v-if="renaming !== null"
349
388
  :name="renameLabel"
@@ -443,7 +482,7 @@ import { getClient, getDefaultPropfind, getRemoteURL, getRootPath, resultToNode
443
482
  * copy read `window._nc_fileactions`, which the server's plugins never write,
444
483
  * and saw nothing while the page held thirty actions.
445
484
  */
446
- import { translate as t } from '@nextcloud/l10n'
485
+ import { translatePlural as n, translate as t } from '@nextcloud/l10n'
447
486
  import { generateUrl } from '@nextcloud/router'
448
487
  import { NcActionButton, NcActionCheckbox, NcActionLink, NcActions, NcActionSeparator, NcBreadcrumb, NcBreadcrumbs, NcButton, NcDateTime, NcDialog, NcIconSvgWrapper, NcLoadingIcon, NcProgressBar, NcTextField } from '@nextcloud/vue'
449
488
  import AlertCircleOutline from 'vue-material-design-icons/AlertCircleOutline.vue'
@@ -624,6 +663,51 @@ export default {
624
663
  default: () => t('nextcloud-vue', 'Show in Files'),
625
664
  },
626
665
 
666
+ /**
667
+ * Show a primary upload button beside the New menu, which then steps
668
+ * back to a secondary button. Off (the default) keeps the upload
669
+ * inside the New menu only.
670
+ */
671
+ uploadButton: {
672
+ type: Boolean,
673
+ default: false,
674
+ },
675
+
676
+ /** Label of the upload button `uploadButton` shows. */
677
+ uploadButtonLabel: {
678
+ type: String,
679
+ default: () => t('nextcloud-vue', 'Add files'),
680
+ },
681
+
682
+ /**
683
+ * Draw the drop state over the browser while files are dragged onto
684
+ * it (a dashed frame and `dropLabel`, centred) and announce it to
685
+ * screen readers. Off (the default) keeps the plain dashed outline and
686
+ * announces nothing.
687
+ */
688
+ dropOverlay: {
689
+ type: Boolean,
690
+ default: false,
691
+ },
692
+
693
+ /** The line `dropOverlay` draws and announces. */
694
+ dropLabel: {
695
+ type: String,
696
+ default: () => t('nextcloud-vue', 'Drop to add'),
697
+ },
698
+
699
+ /** Show a line under the list saying files can be dropped on it. */
700
+ dropHint: {
701
+ type: Boolean,
702
+ default: false,
703
+ },
704
+
705
+ /** The line `dropHint` shows. */
706
+ dropHintLabel: {
707
+ type: String,
708
+ default: () => t('nextcloud-vue', 'Or drag files onto this list.'),
709
+ },
710
+
627
711
  /** Title of the empty state. */
628
712
  emptyLabel: {
629
713
  type: String,
@@ -726,6 +810,10 @@ export default {
726
810
  */
727
811
  visibleColumnKeys: null,
728
812
  dragging: false,
813
+ /** Nested dragenter/dragleave pairs, so a child does not end the drag. */
814
+ dragDepth: 0,
815
+ /** What the live region says: the drop label, then what was added. */
816
+ dropStatus: '',
729
817
  uploads: [],
730
818
  previewFailed: {},
731
819
  newFolderOpen: false,
@@ -1265,12 +1353,76 @@ export default {
1265
1353
  */
1266
1354
  async onDrop(event) {
1267
1355
  this.dragging = false
1356
+ this.dragDepth = 0
1357
+ this.dropStatus = ''
1268
1358
  const files = event.dataTransfer?.files
1269
1359
  if (files?.length) {
1270
1360
  await this.uploadFiles(files)
1361
+ if (this.dropOverlay) {
1362
+ this.dropStatus = n('nextcloud-vue', '%n file added', '%n files added', files.length)
1363
+ }
1271
1364
  }
1272
1365
  },
1273
1366
 
1367
+ /**
1368
+ * Something is dragged into the browser: show the drop state when it
1369
+ * carries files, and say so once.
1370
+ *
1371
+ * @param {DragEvent} event The drag.
1372
+ * @return {void}
1373
+ */
1374
+ onDragEnter(event) {
1375
+ if (!this.carriesFiles(event)) {
1376
+ return
1377
+ }
1378
+ this.dragDepth++
1379
+ if (!this.dragging) {
1380
+ this.dragging = true
1381
+ this.dropStatus = this.dropOverlay ? this.dropLabel : ''
1382
+ }
1383
+ },
1384
+
1385
+ /**
1386
+ * Keep the drop state while files move over the browser.
1387
+ *
1388
+ * @param {DragEvent} event The drag.
1389
+ * @return {void}
1390
+ */
1391
+ onDragOver(event) {
1392
+ if (this.carriesFiles(event)) {
1393
+ this.dragging = true
1394
+ }
1395
+ },
1396
+
1397
+ /**
1398
+ * Something left the browser, or one of its children. The drop state
1399
+ * ends only when the drag has left the browser itself.
1400
+ *
1401
+ * @return {void}
1402
+ */
1403
+ onDragLeave() {
1404
+ this.dragDepth = Math.max(0, this.dragDepth - 1)
1405
+ if (this.dragDepth === 0) {
1406
+ this.dragging = false
1407
+ this.dropStatus = ''
1408
+ }
1409
+ },
1410
+
1411
+ /**
1412
+ * Whether a drag carries files (not text or a link from the page).
1413
+ * A drag whose types cannot be read counts as files, as before.
1414
+ *
1415
+ * @param {DragEvent} event The drag.
1416
+ * @return {boolean}
1417
+ */
1418
+ carriesFiles(event) {
1419
+ const types = event?.dataTransfer?.types
1420
+ if (!types) {
1421
+ return true
1422
+ }
1423
+ return Array.from(types).includes('Files')
1424
+ },
1425
+
1274
1426
  /**
1275
1427
  * PUT each file into the open folder over DAV, with a progress row
1276
1428
  * each, then re-read the folder once.
@@ -1660,6 +1812,7 @@ export default {
1660
1812
  flex-direction: column;
1661
1813
  gap: 8px;
1662
1814
  min-width: 0;
1815
+ position: relative;
1663
1816
  border-radius: var(--border-radius-large, 8px);
1664
1817
  }
1665
1818
 
@@ -1668,6 +1821,52 @@ export default {
1668
1821
  outline-offset: 4px;
1669
1822
  }
1670
1823
 
1824
+ .cn-files-browser__drop-overlay {
1825
+ position: absolute;
1826
+ inset: 0;
1827
+ z-index: 2;
1828
+ display: flex;
1829
+ flex-direction: column;
1830
+ align-items: center;
1831
+ justify-content: center;
1832
+ gap: 10px;
1833
+ padding: 24px;
1834
+ box-sizing: border-box;
1835
+ border: 2px dashed var(--color-primary-element);
1836
+ border-radius: var(--border-radius-large, 8px);
1837
+ background: var(--color-primary-element-light);
1838
+ color: var(--color-primary-element-light-text, var(--color-main-text));
1839
+ text-align: center;
1840
+ pointer-events: none;
1841
+ }
1842
+
1843
+ .cn-files-browser__drop-icon {
1844
+ display: flex;
1845
+ align-items: center;
1846
+ justify-content: center;
1847
+ width: 56px;
1848
+ height: 56px;
1849
+ border-radius: 50%;
1850
+ background: var(--color-main-background);
1851
+ color: var(--color-primary-element);
1852
+ }
1853
+
1854
+ .cn-files-browser__drop-label {
1855
+ font-size: 20px;
1856
+ font-weight: 700;
1857
+ }
1858
+
1859
+ .cn-files-browser__drop-hint {
1860
+ display: flex;
1861
+ align-items: center;
1862
+ gap: 8px;
1863
+ margin: 0;
1864
+ padding-top: 12px;
1865
+ border-top: 1px solid var(--color-border);
1866
+ font-size: 13px;
1867
+ color: var(--color-text-maxcontrast);
1868
+ }
1869
+
1671
1870
  .cn-files-browser__bar {
1672
1871
  display: flex;
1673
1872
  align-items: center;