@conduction/nextcloud-vue 2.76.0 → 2.77.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/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/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 +16 -28
- 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 +98 -13
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +13 -9
- package/dist/esm/components/CnIndexPage/CnIndexPage.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/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/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/index.js +2 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/l10n/en.json.js +25 -1
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +25 -1
- package/dist/esm/l10n/nl.json.js.map +1 -1
- package/dist/esm/nextcloud-vue.css +288 -74
- 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 +17 -0
- 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/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 +3873 -2674
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +288 -74
- package/l10n/en.json +25 -1
- package/l10n/nl.json +25 -1
- package/package.json +1 -1
- package/src/components/CnAppNav/CnAppNav.vue +44 -5
- package/src/components/CnCellRenderer/CnCellRenderer.vue +56 -1
- package/src/components/CnCellRenderer/CnGroupNameCell.vue +61 -0
- package/src/components/CnDataTable/CnDataTable.md +9 -0
- package/src/components/CnDataTable/CnDataTable.vue +80 -20
- package/src/components/CnDetailPage/CnDetailPage.vue +10 -27
- 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 +36 -0
- package/src/components/CnIndexPage/CnIndexPage.vue +106 -19
- package/src/components/CnObjectDataWidget/CnObjectDataWidget.vue +226 -2
- package/src/components/CnObjectSidebar/CnFilesTab.vue +9 -0
- package/src/components/CnWidgetObjectTable/CnWidgetObjectTable.vue +13 -0
- package/src/composables/useListView.js +4 -0
- package/src/css/look-board-detail.css +40 -2
- package/src/css/table.css +82 -17
- package/src/index.js +2 -1
- package/src/schemas/app-manifest-v2.schema.json +11 -7
- package/src/store/plugins/interactions.js +20 -10
- package/src/store/useObjectStore.js +17 -0
- package/src/utils/builtInFormatters.js +15 -7
- package/src/utils/groupAutocomplete.js +75 -0
- package/src/utils/index.js +1 -1
- package/src/utils/lensAvailability.js +86 -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
|
@@ -10,6 +10,20 @@
|
|
|
10
10
|
:formatted="formattedValue"
|
|
11
11
|
v-bind="widgetProps" />
|
|
12
12
|
|
|
13
|
+
<!-- Built-in "group" widget: a Nextcloud group id (or a list of them)
|
|
14
|
+
shown as the group's display name, cached per id. Used on its own
|
|
15
|
+
for a property marked `referenceType: nextcloud-group` /
|
|
16
|
+
`format: nc-group` unless the column has a formatter. -->
|
|
17
|
+
<template v-else-if="isGroupCell">
|
|
18
|
+
<span v-if="!groupIds.length" class="cn-cell-renderer__dash">—</span>
|
|
19
|
+
<span v-else class="cn-cell-renderer__groups">
|
|
20
|
+
<template v-for="(gid, i) in groupIds" :key="gid">
|
|
21
|
+
<span v-if="i > 0">, </span>
|
|
22
|
+
<CnGroupNameCell :gid="gid" />
|
|
23
|
+
</template>
|
|
24
|
+
</span>
|
|
25
|
+
</template>
|
|
26
|
+
|
|
13
27
|
<!-- Built-in "badge" widget — renders the (possibly formatter-shaped) value as a status pill -->
|
|
14
28
|
<template v-else-if="widget === 'badge'">
|
|
15
29
|
<CnStatusBadge v-if="hasValue"
|
|
@@ -192,11 +206,12 @@ import { getCanonicalLocale } from '@nextcloud/l10n'
|
|
|
192
206
|
import { NcAvatar, NcDateTime } from '@nextcloud/vue'
|
|
193
207
|
import CheckBold from 'vue-material-design-icons/CheckBold.vue'
|
|
194
208
|
import CnFkResolveCell from '../CnFkResolveCell/CnFkResolveCell.vue'
|
|
209
|
+
import CnGroupNameCell from './CnGroupNameCell.vue'
|
|
195
210
|
import { parseDateValue, resolveDateVariant } from '../../utils/dateVariant.js'
|
|
196
211
|
import { safeCurrencyCode } from '../../utils/formatMetric.js'
|
|
197
212
|
import { objectFieldValue } from '../../utils/objectName.js'
|
|
198
213
|
import { safeHref } from '../../utils/safeHref.js'
|
|
199
|
-
import { formatValue } from '../../utils/schema.js'
|
|
214
|
+
import { formatValue, isGroupProp } from '../../utils/schema.js'
|
|
200
215
|
import { CnStatusBadge } from '../CnStatusBadge/index.js'
|
|
201
216
|
|
|
202
217
|
/** A uuid in any version, the shape OpenRegister ids take. */
|
|
@@ -226,6 +241,7 @@ export default {
|
|
|
226
241
|
components: {
|
|
227
242
|
CnStatusBadge,
|
|
228
243
|
CnFkResolveCell,
|
|
244
|
+
CnGroupNameCell,
|
|
229
245
|
CheckBold,
|
|
230
246
|
NcAvatar,
|
|
231
247
|
NcDateTime,
|
|
@@ -307,6 +323,9 @@ export default {
|
|
|
307
323
|
* a date whose colour follows `widgetProps.variantWhen`, rules on the
|
|
308
324
|
* number of days until the date (`[{ op: "lt", value: 0, variant:
|
|
309
325
|
* "error" }, { op: "lte", value: 5, variant: "warning" }]`).
|
|
326
|
+
* The built-in id `"group"` shows a Nextcloud group id (or a list of
|
|
327
|
+
* them) as the group's display name; a column whose property is
|
|
328
|
+
* marked `referenceType: "nextcloud-group"` gets it without asking.
|
|
310
329
|
* Takes precedence over `formatter`/the type-aware rendering, but the
|
|
311
330
|
* value handed to the widget is the formatter-shaped `formatted` when
|
|
312
331
|
* `formatter` is also set.
|
|
@@ -577,6 +596,42 @@ export default {
|
|
|
577
596
|
return c || null
|
|
578
597
|
},
|
|
579
598
|
|
|
599
|
+
/**
|
|
600
|
+
* Whether this cell shows Nextcloud group names: the column asks for
|
|
601
|
+
* `widget: "group"`, or it names no widget and no formatter and its
|
|
602
|
+
* property is a group (or a list of groups).
|
|
603
|
+
*
|
|
604
|
+
* @spec openspec/changes/nextcloud-group-surfaces/specs/data-display/spec.md#requirement-a-group-cell-shows-the-groups-display-name
|
|
605
|
+
* @return {boolean}
|
|
606
|
+
*/
|
|
607
|
+
isGroupCell() {
|
|
608
|
+
if (this.widget === 'group') {
|
|
609
|
+
return true
|
|
610
|
+
}
|
|
611
|
+
if (this.widget || this.hasFormatter) {
|
|
612
|
+
return false
|
|
613
|
+
}
|
|
614
|
+
const p = this.property
|
|
615
|
+
if (!p || typeof p !== 'object') {
|
|
616
|
+
return false
|
|
617
|
+
}
|
|
618
|
+
return isGroupProp(p) || (p.type === 'array' && isGroupProp(p.items))
|
|
619
|
+
},
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* The group ids in this cell, empty ones dropped.
|
|
623
|
+
*
|
|
624
|
+
* @spec openspec/changes/nextcloud-group-surfaces/specs/data-display/spec.md#requirement-a-group-cell-shows-the-groups-display-name
|
|
625
|
+
* @return {string[]}
|
|
626
|
+
*/
|
|
627
|
+
groupIds() {
|
|
628
|
+
const raw = this.value
|
|
629
|
+
const list = Array.isArray(raw) ? raw : [raw]
|
|
630
|
+
return list
|
|
631
|
+
.filter((gid) => (typeof gid === 'string' && gid !== '') || typeof gid === 'number')
|
|
632
|
+
.map((gid) => String(gid))
|
|
633
|
+
},
|
|
634
|
+
|
|
580
635
|
/** Variant for the built-in `badge` widget — `widgetProps.variant` or `'default'`. */
|
|
581
636
|
badgeVariant() {
|
|
582
637
|
return (this.widgetProps && this.widgetProps.variant) || 'default'
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
SPDX-FileCopyrightText: 2026 Conduction B.V.
|
|
3
|
+
SPDX-License-Identifier: EUPL-1.2
|
|
4
|
+
|
|
5
|
+
CnGroupNameCell: one Nextcloud group id shown as the group's display name.
|
|
6
|
+
The name is looked up once per page (utils/groupAutocomplete.js); until it
|
|
7
|
+
arrives, and when it cannot be found, the cell shows the id.
|
|
8
|
+
-->
|
|
9
|
+
<template>
|
|
10
|
+
<span class="cn-group-name-cell" :title="gid">{{ label }}</span>
|
|
11
|
+
</template>
|
|
12
|
+
|
|
13
|
+
<script>
|
|
14
|
+
import { groupDisplayName, loadGroupDisplayName } from '../../utils/groupAutocomplete.js'
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* CnGroupNameCell renders a Nextcloud group id as its display name.
|
|
18
|
+
*
|
|
19
|
+
* ```vue
|
|
20
|
+
* <CnGroupNameCell gid="toezicht" />
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* @spec openspec/changes/nextcloud-group-surfaces/specs/data-display/spec.md#requirement-a-group-cell-shows-the-groups-display-name
|
|
24
|
+
*/
|
|
25
|
+
export default {
|
|
26
|
+
name: 'CnGroupNameCell',
|
|
27
|
+
|
|
28
|
+
props: {
|
|
29
|
+
/** The group id to show. */
|
|
30
|
+
gid: {
|
|
31
|
+
type: String,
|
|
32
|
+
required: true,
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
computed: {
|
|
37
|
+
/**
|
|
38
|
+
* The cached display name, or the id while it loads or when unknown.
|
|
39
|
+
*
|
|
40
|
+
* @return {string}
|
|
41
|
+
*/
|
|
42
|
+
label() {
|
|
43
|
+
return groupDisplayName(this.gid) || this.gid
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
watch: {
|
|
48
|
+
gid: {
|
|
49
|
+
immediate: true,
|
|
50
|
+
/**
|
|
51
|
+
* Ask for the name of a new id (cached, so once per id per page).
|
|
52
|
+
*
|
|
53
|
+
* @param {string} gid The group id.
|
|
54
|
+
*/
|
|
55
|
+
handler(gid) {
|
|
56
|
+
loadGroupDisplayName(gid)
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
}
|
|
61
|
+
</script>
|
|
@@ -205,6 +205,7 @@ export default {
|
|
|
205
205
|
| `filterable` | Boolean | `false` | Show a filter button in every header whose column can filter: enum, boolean, text, number, date and reference columns backed by a schema property. A column opts out with `filterable: false`. Applying a filter emits `column-filter`; the host owns the state and passes it back as `activeFilters`. `CnIndexPage` turns it on by default. |
|
|
206
206
|
| `activeFilters` | Object | `{}` | The active filter map, `{ paramKey: values[] }`: the same map the facet sidebar writes, so a header filter and a sidebar filter on one field show the same state. |
|
|
207
207
|
| `filterRegister` | String | `''` | Register slug a reference column's filter searches in when the column names none. |
|
|
208
|
+
| `lensReasonTexts` | Object \| null | `null` | App wording for an unavailable personal lens in self-fetch mode, keyed `<lens>.<reason>` or `<reason>`. Without it the library's text is used (see below). |
|
|
208
209
|
|
|
209
210
|
## Slots
|
|
210
211
|
|
|
@@ -233,6 +234,14 @@ features are folded in here as opt-in props (bare-table usage is unchanged):
|
|
|
233
234
|
- `fetchParams` — extra query params for the self-fetch (a resolved filter map,
|
|
234
235
|
`_order[field]` ordering, `_limit`); changing it re-triggers the fetch. Used by
|
|
235
236
|
`CnWidgetObjectTable`'s declarative `source`.
|
|
237
|
+
- When the self-fetch asked for a personal lens (a "Recently opened" tile with
|
|
238
|
+
`source.filter._recent: true`) and the response reports it unavailable
|
|
239
|
+
(`@self.lenses.recent.available: false`, openregister#4514), the empty row
|
|
240
|
+
says why instead of `emptyText`: "This server does not keep track of what you
|
|
241
|
+
open." (`audit-trail-disabled`), "Log in to see what you opened recently."
|
|
242
|
+
(`anonymous`) or "Your recent items are not available right now."
|
|
243
|
+
(`read-history-unavailable`). `lensReasonTexts` replaces the wording; no
|
|
244
|
+
report, an available lens or an unknown reason keeps `emptyText`.
|
|
236
245
|
- `rowClickRoute` — a function mapping a row to a vue-router route. Each such row renders a real `<a href>` in its first cell, stretched over the row, so hovering shows the URL, a plain or alt click routes in place, a ctrl/cmd/shift or middle click opens a new tab, and the row is reachable with Tab. The link is named by the first cell's text ("Open row" when that is empty or not plain text). There is no link on a `selectable` table without `rowClickToView`, where a row click selects. `CnWidgetObjectTable` rows inherit it. Trade-offs: cell text cannot be selected by dragging on a linked row, and a right-click on it shows the browser's link menu only when no host listens to `row-context-menu`.
|
|
237
246
|
- `hideHeader` — drop the column-label row for a compact list widget.
|
|
238
247
|
- `#footer` slot — supply a custom footer link (e.g. "+ New" or an always-shown
|
|
@@ -130,25 +130,42 @@
|
|
|
130
130
|
pinClass(leadingCount + colIndex),
|
|
131
131
|
]"
|
|
132
132
|
:style="{ ...(col.width ? { width: col.width } : {}), ...pinStyle(leadingCount + colIndex) }"
|
|
133
|
-
:tabindex="col.sortable ? 0 : null"
|
|
134
133
|
:aria-sort="ariaSortFor(col)"
|
|
135
134
|
:data-filtered="isColumnFiltered(col) ? 'true' : null"
|
|
136
135
|
:title="translateLabel(col.description) || null"
|
|
137
136
|
@click="col.sortable ? onHeaderClick(col.key, $event) : null"
|
|
138
137
|
@keydown.enter="col.sortable ? onHeaderKeydown(col.key, $event) : null">
|
|
139
|
-
|
|
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: {
|
|
@@ -3867,18 +3862,6 @@ export default {
|
|
|
3867
3862
|
&& this.register !== '' && this.schema !== '' && this.objectId !== ''
|
|
3868
3863
|
},
|
|
3869
3864
|
|
|
3870
|
-
/**
|
|
3871
|
-
* Whether the star renders: the object carries `@self.favourite` and the page did not turn it off.
|
|
3872
|
-
*
|
|
3873
|
-
* @return {boolean}
|
|
3874
|
-
* @spec openspec/changes/record-favourite-and-follow/tasks.md#task-3
|
|
3875
|
-
*/
|
|
3876
|
-
showFavouriteToggle() {
|
|
3877
|
-
return this.favourite !== false
|
|
3878
|
-
&& typeof this.resolvedSelf.favourite === 'boolean'
|
|
3879
|
-
&& this.register !== '' && this.schema !== '' && this.objectId !== ''
|
|
3880
|
-
},
|
|
3881
|
-
|
|
3882
3865
|
/**
|
|
3883
3866
|
* Whether the Follow toggle renders: the object carries `@self.watching` and the page did not turn it off.
|
|
3884
3867
|
*
|
|
@@ -31,12 +31,17 @@ import { setFavourite } from '../../utils/recordInteractions.js'
|
|
|
31
31
|
/**
|
|
32
32
|
* CnFavouriteToggle — a star bound to a record's `@self.favourite` marker.
|
|
33
33
|
*
|
|
34
|
+
* @deprecated Following and favourites are one feature since OpenRegister's
|
|
35
|
+
* `merge-follow-and-favourites`: a favourite is a follow with notifications
|
|
36
|
+
* off. Use `CnFollowToggle`. Neither `CnDetailPage` nor `CnIndexPage` renders
|
|
37
|
+
* this star any more; it stays exported for one minor line so an app that
|
|
38
|
+
* placed it itself keeps working (the server answers it as a quiet follow).
|
|
39
|
+
*
|
|
34
40
|
* A click flips the star at once and sends `PUT` (star) or `DELETE` (unstar)
|
|
35
41
|
* `/apps/openregister/api/objects/{register}/{schema}/{id}/favourite`; on
|
|
36
42
|
* failure it flips back and shows the server's message. The server's answer is
|
|
37
43
|
* written into the stored object's `@self`, so a list showing the same record
|
|
38
|
-
* agrees.
|
|
39
|
-
* per row for `showFavouriteColumn`.
|
|
44
|
+
* agrees.
|
|
40
45
|
*
|
|
41
46
|
* Example:
|
|
42
47
|
* ```vue
|