@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.
- package/dist/esm/components/CnAppNav/CnAppNav.vue2.js +18 -3
- package/dist/esm/components/CnAppNav/CnAppNav.vue2.js.map +1 -1
- package/dist/esm/components/CnAppNav/CnAppNav.vue3.js +49 -3
- package/dist/esm/components/CnAppNav/CnAppNav.vue3.js.map +1 -1
- package/dist/esm/components/CnCardGrid/CnCardGrid.vue2.js +6 -0
- package/dist/esm/components/CnCardGrid/CnCardGrid.vue2.js.map +1 -1
- package/dist/esm/components/CnCardGrid/CnCardGrid.vue3.js +3 -2
- package/dist/esm/components/CnCardGrid/CnCardGrid.vue3.js.map +1 -1
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js +42 -1
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js.map +1 -1
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js +228 -203
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js.map +1 -1
- package/dist/esm/components/CnCellRenderer/CnGroupNameCell.vue.js +9 -0
- package/dist/esm/components/CnCellRenderer/CnGroupNameCell.vue.js.map +1 -0
- package/dist/esm/components/CnCellRenderer/CnGroupNameCell.vue2.js +50 -0
- package/dist/esm/components/CnCellRenderer/CnGroupNameCell.vue2.js.map +1 -0
- package/dist/esm/components/CnCellRenderer/CnGroupNameCell.vue3.js +13 -0
- package/dist/esm/components/CnCellRenderer/CnGroupNameCell.vue3.js.map +1 -0
- package/dist/esm/components/CnCredentials/CnCredentials.vue2.js +11 -10
- package/dist/esm/components/CnCredentials/CnCredentials.vue2.js.map +1 -1
- package/dist/esm/components/CnCredentials/CnCredentials.vue3.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +41 -4
- package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +90 -51
- package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +24 -33
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +12 -24
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
- package/dist/esm/components/CnFavouriteToggle/CnFavouriteToggle.vue2.js +7 -2
- package/dist/esm/components/CnFavouriteToggle/CnFavouriteToggle.vue2.js.map +1 -1
- package/dist/esm/components/CnFavouriteToggle/CnFavouriteToggle.vue3.js.map +1 -1
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js +114 -1
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js.map +1 -1
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js +115 -62
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js.map +1 -1
- package/dist/esm/components/CnFollowToggle/CnFollowToggle.vue2.js +89 -7
- package/dist/esm/components/CnFollowToggle/CnFollowToggle.vue2.js.map +1 -1
- package/dist/esm/components/CnFollowToggle/CnFollowToggle.vue3.js +62 -30
- package/dist/esm/components/CnFollowToggle/CnFollowToggle.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +242 -19
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +25 -20
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/selfModeActions.js +90 -14
- package/dist/esm/components/CnIndexPage/selfModeActions.js.map +1 -1
- package/dist/esm/components/CnIndexPage/useSelfFetchList.js +15 -3
- package/dist/esm/components/CnIndexPage/useSelfFetchList.js.map +1 -1
- package/dist/esm/components/CnObjectCard/CnObjectCard.vue2.js +26 -0
- package/dist/esm/components/CnObjectCard/CnObjectCard.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectCard/CnObjectCard.vue3.js +26 -9
- package/dist/esm/components/CnObjectCard/CnObjectCard.vue3.js.map +1 -1
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js +199 -7
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue3.js +155 -119
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +19 -3
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js +6 -0
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js +4 -1
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js.map +1 -1
- package/dist/esm/components/CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue2.js +153 -38
- package/dist/esm/components/CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue3.js +74 -56
- package/dist/esm/components/CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js +13 -0
- package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js.map +1 -1
- package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue3.js.map +1 -1
- package/dist/esm/composables/useListView.js +4 -0
- package/dist/esm/composables/useListView.js.map +1 -1
- package/dist/esm/composables/useObjectLock.js +41 -10
- package/dist/esm/composables/useObjectLock.js.map +1 -1
- package/dist/esm/index.js +3 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/l10n/en.json.js +31 -1
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +32 -2
- package/dist/esm/l10n/nl.json.js.map +1 -1
- package/dist/esm/nextcloud-vue.css +376 -75
- package/dist/esm/store/plugins/interactions.js +20 -10
- package/dist/esm/store/plugins/interactions.js.map +1 -1
- package/dist/esm/store/useObjectStore.js +54 -9
- package/dist/esm/store/useObjectStore.js.map +1 -1
- package/dist/esm/utils/builtInFormatters.js +15 -7
- package/dist/esm/utils/builtInFormatters.js.map +1 -1
- package/dist/esm/utils/groupAutocomplete.js +63 -1
- package/dist/esm/utils/groupAutocomplete.js.map +1 -1
- package/dist/esm/utils/lensAvailability.js +90 -0
- package/dist/esm/utils/lensAvailability.js.map +1 -0
- package/dist/esm/utils/objectSignals.js +44 -0
- package/dist/esm/utils/objectSignals.js.map +1 -0
- package/dist/esm/utils/personalLenses.js +13 -6
- package/dist/esm/utils/personalLenses.js.map +1 -1
- package/dist/esm/utils/recordInteractions.js +31 -13
- package/dist/esm/utils/recordInteractions.js.map +1 -1
- package/dist/esm/utils/resolveFilterTokens.js +46 -3
- package/dist/esm/utils/resolveFilterTokens.js.map +1 -1
- package/dist/esm/utils/schema.js +1 -1
- package/dist/esm/utils/schema.js.map +1 -1
- package/dist/esm/utils/sentinelTokens.js +4 -4
- package/dist/esm/utils/sentinelTokens.js.map +1 -1
- package/dist/esm/utils/validateManifestV2.compiled.js +2 -2
- package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
- package/dist/esm/utils/widgetVisibility.js +46 -1
- package/dist/esm/utils/widgetVisibility.js.map +1 -1
- package/dist/nextcloud-vue.cjs.js +4732 -2990
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +376 -75
- package/l10n/en.json +31 -1
- package/l10n/nl.json +32 -2
- package/package.json +1 -1
- package/src/components/CnAppNav/CnAppNav.vue +44 -5
- package/src/components/CnCardGrid/CnCardGrid.md +1 -0
- package/src/components/CnCardGrid/CnCardGrid.vue +7 -0
- package/src/components/CnCellRenderer/CnCellRenderer.vue +56 -1
- package/src/components/CnCellRenderer/CnGroupNameCell.vue +61 -0
- package/src/components/CnCredentials/CnCredentials.vue +17 -16
- package/src/components/CnDataTable/CnDataTable.md +9 -0
- package/src/components/CnDataTable/CnDataTable.vue +80 -20
- package/src/components/CnDetailPage/CnDetailPage.vue +18 -32
- package/src/components/CnFavouriteToggle/CnFavouriteToggle.vue +7 -2
- package/src/components/CnFilesBrowser/CnFilesBrowser.vue +204 -5
- package/src/components/CnFollowToggle/CnFollowToggle.vue +118 -13
- package/src/components/CnIndexPage/CnIndexPage.md +51 -0
- package/src/components/CnIndexPage/CnIndexPage.vue +261 -35
- package/src/components/CnIndexPage/selfModeActions.js +89 -13
- package/src/components/CnIndexPage/useSelfFetchList.js +15 -3
- package/src/components/CnObjectCard/CnObjectCard.md +1 -0
- package/src/components/CnObjectCard/CnObjectCard.vue +89 -1
- package/src/components/CnObjectDataWidget/CnObjectDataWidget.vue +226 -2
- package/src/components/CnObjectListWidget/CnObjectListWidget.vue +19 -3
- package/src/components/CnObjectSidebar/CnFilesTab.vue +9 -0
- package/src/components/CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue +181 -38
- package/src/components/CnWidgetObjectTable/CnWidgetObjectTable.vue +13 -0
- package/src/composables/__tests__/useObjectLock.endpoints.spec.js +83 -0
- package/src/composables/useListView.js +4 -0
- package/src/composables/useObjectLock.js +41 -10
- package/src/css/look-board-detail.css +40 -2
- package/src/css/look-board-index.css +35 -1
- package/src/css/table.css +82 -17
- package/src/index.js +3 -1
- package/src/schemas/app-manifest-v2.schema.json +11 -7
- package/src/store/plugins/interactions.js +20 -10
- package/src/store/useObjectStore.js +54 -9
- package/src/utils/builtInFormatters.js +15 -7
- package/src/utils/groupAutocomplete.js +75 -0
- package/src/utils/index.js +2 -1
- package/src/utils/lensAvailability.js +86 -0
- package/src/utils/objectSignals.js +41 -0
- package/src/utils/personalLenses.js +13 -6
- package/src/utils/recordInteractions.js +31 -13
- package/src/utils/resolveFilterTokens.js +45 -2
- package/src/utils/schema.js +1 -1
- package/src/utils/sentinelTokens.js +4 -4
- package/src/utils/validateManifestV2.compiled.js +2 -2
- 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
|
-
|
|
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
|
-
<
|
|
167
|
-
<
|
|
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
|
-
{{
|
|
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
|
|
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
|
-
|
|
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 ||
|
|
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
|
-
<!--
|
|
128
|
-
|
|
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="
|
|
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
|
-
*
|
|
1888
|
-
*
|
|
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
|
-
|
|
2731
|
-
|
|
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
|
-
|
|
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.
|
|
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="
|
|
11
|
-
@dragover.prevent="
|
|
12
|
-
@dragleave.prevent="
|
|
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;
|