@conduction/nextcloud-vue 2.105.0 → 2.106.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/CnCardGrid/CnCardGrid.vue.js +1 -1
- package/dist/esm/components/CnCardGrid/CnCardGrid.vue2.js +18 -0
- package/dist/esm/components/CnCardGrid/CnCardGrid.vue2.js.map +1 -1
- package/dist/esm/components/CnCardGrid/CnCardGrid.vue3.js +3 -1
- package/dist/esm/components/CnCardGrid/CnCardGrid.vue3.js.map +1 -1
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue.js +1 -1
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js +18 -0
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js.map +1 -1
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js +1 -1
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +53 -4
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue.js +9 -0
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue.js.map +1 -0
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue2.js +24 -0
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue2.js.map +1 -0
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue3.js +49 -0
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue3.js.map +1 -0
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue.js +1 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js +36 -18
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js +17 -38
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue.js +1 -1
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue.js +1 -1
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js +53 -2
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js.map +1 -1
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js +119 -111
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js.map +1 -1
- package/dist/esm/components/CnFlowRunsWidget/CnFlowRunsWidget.vue.js +1 -1
- package/dist/esm/components/CnFlowRunsWidget/CnFlowRunsWidget.vue2.js +62 -1
- package/dist/esm/components/CnFlowRunsWidget/CnFlowRunsWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnFlowRunsWidget/CnFlowRunsWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +159 -50
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +121 -98
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue.js +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue2.js +24 -0
- package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue3.js +20 -4
- package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/useSelfFetchList.js +22 -4
- package/dist/esm/components/CnIndexPage/useSelfFetchList.js.map +1 -1
- package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue2.js +112 -1
- package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue3.js +100 -44
- package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue3.js.map +1 -1
- package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue.js +1 -1
- package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue2.js +67 -4
- package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue3.js +1 -1
- package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnObjectCard/CnObjectCard.vue.js +1 -1
- package/dist/esm/components/CnObjectCard/CnObjectCard.vue2.js +295 -12
- package/dist/esm/components/CnObjectCard/CnObjectCard.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectCard/CnObjectCard.vue3.js +267 -144
- package/dist/esm/components/CnObjectCard/CnObjectCard.vue3.js.map +1 -1
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue.js +1 -1
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js +4 -0
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js +3 -1
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js.map +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +93 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js +322 -246
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue.js +1 -1
- package/dist/esm/components/CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue2.js +122 -7
- package/dist/esm/components/CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue3.js +189 -93
- package/dist/esm/components/CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnTabs/CnTabs.vue.js +1 -1
- package/dist/esm/components/CnTabs/CnTabs.vue2.js.map +1 -1
- package/dist/esm/components/CnTabs/CnTabs.vue3.js.map +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue.js +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +42 -0
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js +38 -19
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnTeamWorkloadWidget/CnTeamWorkloadWidget.vue.js +9 -0
- package/dist/esm/components/CnTeamWorkloadWidget/CnTeamWorkloadWidget.vue.js.map +1 -0
- package/dist/esm/components/CnTeamWorkloadWidget/CnTeamWorkloadWidget.vue2.js +258 -0
- package/dist/esm/components/CnTeamWorkloadWidget/CnTeamWorkloadWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnTeamWorkloadWidget/CnTeamWorkloadWidget.vue3.js +69 -0
- package/dist/esm/components/CnTeamWorkloadWidget/CnTeamWorkloadWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js +12 -0
- package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js.map +1 -1
- package/dist/esm/index.js +1 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/l10n/en.json.js +21 -1
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +21 -1
- package/dist/esm/l10n/nl.json.js.map +1 -1
- package/dist/esm/library-style.js +1 -1
- package/dist/esm/nextcloud-vue.css +803 -453
- package/dist/esm/utils/dueText.js +18 -3
- package/dist/esm/utils/dueText.js.map +1 -1
- package/dist/esm/utils/libraryWidgetKeys.js +1 -0
- package/dist/esm/utils/libraryWidgetKeys.js.map +1 -1
- package/dist/esm/utils/sortOrder.js +11 -1
- package/dist/esm/utils/sortOrder.js.map +1 -1
- package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
- package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
- package/dist/nextcloud-vue.cjs.js +8281 -6350
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +803 -453
- package/l10n/en.json +21 -1
- package/l10n/nl.json +21 -1
- package/package.json +1 -1
- package/src/components/CnCardGrid/CnCardGrid.md +2 -0
- package/src/components/CnCardGrid/CnCardGrid.vue +20 -0
- package/src/components/CnCellRenderer/CnCellRenderer.vue +19 -1
- package/src/components/CnDetailPage/CnDetailPage.md +2 -2
- package/src/components/CnDetailPage/CnDetailPage.vue +53 -4
- package/src/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue +60 -0
- package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +36 -29
- package/src/components/CnFactsWidget/CnFactsWidget.vue +4 -0
- package/src/components/CnFilesBrowser/CnFilesBrowser.vue +70 -4
- package/src/components/CnFlowRunsWidget/CnFlowRunsWidget.vue +62 -1
- package/src/components/CnIndexPage/CnIndexPage.md +7 -5
- package/src/components/CnIndexPage/CnIndexPage.vue +187 -58
- package/src/components/CnIndexPage/CnIndexPageWidgets.vue +36 -1
- package/src/components/CnIndexPage/useSelfFetchList.js +22 -4
- package/src/components/CnIndexSidebar/CnIndexSidebar.md +2 -0
- package/src/components/CnIndexSidebar/CnIndexSidebar.vue +148 -1
- package/src/components/CnNcWidgetWidget/CnNcWidgetWidget.vue +68 -5
- package/src/components/CnObjectCard/CnObjectCard.md +1 -0
- package/src/components/CnObjectCard/CnObjectCard.vue +354 -13
- package/src/components/CnObjectSidebar/CnFilesTab.vue +6 -0
- package/src/components/CnRecordListWidget/CnRecordListWidget.vue +190 -4
- package/src/components/CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue +209 -9
- package/src/components/CnTabs/CnTabs.vue +11 -0
- package/src/components/CnTabsWidget/CnTabsWidget.vue +91 -18
- package/src/components/CnTeamWorkloadWidget/CnTeamWorkloadWidget.md +11 -0
- package/src/components/CnTeamWorkloadWidget/CnTeamWorkloadWidget.vue +382 -0
- package/src/components/CnTeamWorkloadWidget/index.js +14 -0
- package/src/components/CnWidgetGrid/registerDashboardWidgets.js +12 -0
- package/src/components/index.js +1 -0
- package/src/css/index-page.css +14 -2
- package/src/css/look-board-detail.css +48 -4
- package/src/css/look-board-index.css +64 -2
- package/src/css/look-board.css +25 -2
- package/src/index.js +1 -1
- package/src/schemas/app-manifest-v2.schema.json +75 -6
- package/src/utils/dueText.js +18 -3
- package/src/utils/libraryWidgetKeys.js +1 -0
- package/src/utils/sortOrder.js +10 -0
- package/src/utils/validateManifestV2.compiled.js +3 -3
|
@@ -30,11 +30,15 @@
|
|
|
30
30
|
:showRefresh="false"
|
|
31
31
|
:headerLink="entry.headerLink"
|
|
32
32
|
:flush="variant === 'stat-row'">
|
|
33
|
+
<template v-if="headerNotes[entry.id]" #actions>
|
|
34
|
+
<span class="cn-index-page-widgets__note" data-testid="cn-index-page-widget-note">{{ headerNotes[entry.id] }}</span>
|
|
35
|
+
</template>
|
|
33
36
|
<component
|
|
34
37
|
:is="entry.renderer"
|
|
35
38
|
:widgetId="entry.id"
|
|
36
39
|
:content="entry.content"
|
|
37
|
-
v-bind="declaredContent(entry)"
|
|
40
|
+
v-bind="declaredContent(entry)"
|
|
41
|
+
@header="onHeader(entry.id, $event)" />
|
|
38
42
|
</CnWidgetWrapper>
|
|
39
43
|
</div>
|
|
40
44
|
</component>
|
|
@@ -85,6 +89,13 @@ export default {
|
|
|
85
89
|
},
|
|
86
90
|
},
|
|
87
91
|
|
|
92
|
+
data() {
|
|
93
|
+
return {
|
|
94
|
+
/** The header note each widget reported (`header` event), by widget id. */
|
|
95
|
+
headerNotes: {},
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
|
|
88
99
|
computed: {
|
|
89
100
|
/**
|
|
90
101
|
* The renderable entries, in the listed order.
|
|
@@ -122,6 +133,23 @@ export default {
|
|
|
122
133
|
},
|
|
123
134
|
|
|
124
135
|
methods: {
|
|
136
|
+
/**
|
|
137
|
+
* Keep the header note a widget reports ("4 van 5 aanwezig"); only a
|
|
138
|
+
* changed note is stored, so a report on render does not loop.
|
|
139
|
+
*
|
|
140
|
+
* @param {string} id The widget id.
|
|
141
|
+
* @param {object} header `{ note?, link? }` from the widget.
|
|
142
|
+
* @return {void}
|
|
143
|
+
* @spec openspec/changes/side-panel-header-note/specs/side-panel-header-note/spec.md#requirement-a-side-panel-card-shows-the-note-its-widget-reports
|
|
144
|
+
*/
|
|
145
|
+
onHeader(id, header) {
|
|
146
|
+
const note = header && typeof header === 'object' && typeof header.note === 'string' ? header.note : ''
|
|
147
|
+
if ((this.headerNotes[id] || '') === note) {
|
|
148
|
+
return
|
|
149
|
+
}
|
|
150
|
+
this.headerNotes = { ...this.headerNotes, [id]: note }
|
|
151
|
+
},
|
|
152
|
+
|
|
125
153
|
/**
|
|
126
154
|
* The content keys an entry's renderer declares as props. An undeclared
|
|
127
155
|
* key would fall through onto the renderer's root; the whole content
|
|
@@ -158,6 +186,13 @@ export default {
|
|
|
158
186
|
</script>
|
|
159
187
|
|
|
160
188
|
<style scoped>
|
|
189
|
+
.cn-index-page-widgets__note {
|
|
190
|
+
align-self: center;
|
|
191
|
+
color: var(--color-text-maxcontrast);
|
|
192
|
+
font-size: 14px;
|
|
193
|
+
white-space: nowrap;
|
|
194
|
+
}
|
|
195
|
+
|
|
161
196
|
.cn-index-page-widgets--stat-row {
|
|
162
197
|
margin-bottom: var(--cn-board-section-gap, 20px);
|
|
163
198
|
}
|
|
@@ -9,7 +9,7 @@ import { withPersonalLenses } from '../../utils/personalLenses.js'
|
|
|
9
9
|
import { parseSortKeysFromQuery, resolveFilterMap, resolveQueryFilters } from '../../utils/routeFilters.js'
|
|
10
10
|
import { orderToSortKeys } from '../../utils/sortOrder.js'
|
|
11
11
|
|
|
12
|
-
function resolveInitialQuickFilterIndex(quickFilters) {
|
|
12
|
+
export function resolveInitialQuickFilterIndex(quickFilters) {
|
|
13
13
|
const tabs = Array.isArray(quickFilters) ? quickFilters : null
|
|
14
14
|
if (!tabs || tabs.length === 0) {
|
|
15
15
|
return null
|
|
@@ -228,6 +228,22 @@ export function useSelfFetchList(props, instance, inject, extras = {}) {
|
|
|
228
228
|
? props.sortKeys
|
|
229
229
|
: (props.sortKey ? [{ key: props.sortKey, order: props.sortOrder || 'asc' }] : defaultOrderKeys)
|
|
230
230
|
// The lens that is active on mount may set its own order.
|
|
231
|
+
/**
|
|
232
|
+
* The tabs the strip shows, so the fetch filters by the chip that is
|
|
233
|
+
* lit: the page's `effectiveQuickFilters` (declared, generated from
|
|
234
|
+
* `quickFiltersFrom`, or a named source's), else the prop while the
|
|
235
|
+
* page's computeds do not exist yet (setup).
|
|
236
|
+
*
|
|
237
|
+
* @return {Array<object>|null} The tabs, or null when there are none.
|
|
238
|
+
*/
|
|
239
|
+
function shownQuickFilters() {
|
|
240
|
+
const shown = instance && instance.proxy ? instance.proxy.effectiveQuickFilters : undefined
|
|
241
|
+
if (shown !== undefined) {
|
|
242
|
+
return shown
|
|
243
|
+
}
|
|
244
|
+
return withPersonalLenses(props.quickFilters, props.personalLenses)
|
|
245
|
+
}
|
|
246
|
+
|
|
231
247
|
const initialTabs = withPersonalLenses(props.quickFilters, props.personalLenses)
|
|
232
248
|
const initialTabSort = (!isMultiQuickFilter && Array.isArray(initialTabs))
|
|
233
249
|
? quickFilterSortKeys(initialTabs[activeQuickFilterIndex.value])
|
|
@@ -275,7 +291,7 @@ export function useSelfFetchList(props, instance, inject, extras = {}) {
|
|
|
275
291
|
// The visible calendar month, only while the page is in calendar mode.
|
|
276
292
|
const month = (instance && instance.proxy && instance.proxy.calendarRangeFilter) || {}
|
|
277
293
|
const scope = { ...(widen ? { ...scopeBase, _content_search: 'true' } : scopeBase), ...month }
|
|
278
|
-
const lensed =
|
|
294
|
+
const lensed = shownQuickFilters()
|
|
279
295
|
const tabs = Array.isArray(lensed) ? lensed : null
|
|
280
296
|
if (!tabs) {
|
|
281
297
|
return { ...queryFilters, ...scope, ...base }
|
|
@@ -290,7 +306,9 @@ export function useSelfFetchList(props, instance, inject, extras = {}) {
|
|
|
290
306
|
|
|
291
307
|
// Single mode: the active tab's filter spread last so it wins
|
|
292
308
|
// over a colliding props.filter entry.
|
|
293
|
-
|
|
309
|
+
// Tabs that arrived after setup (`quickFiltersFrom`) have no index
|
|
310
|
+
// yet: the default tab is the one in force, as the strip shows.
|
|
311
|
+
const activeIdx = activeQuickFilterIndex.value ?? resolveInitialQuickFilterIndex(tabs)
|
|
294
312
|
const tabFilter = (activeIdx !== null && activeIdx !== undefined) ? tabs[activeIdx]?.filter : null
|
|
295
313
|
return { ...queryFilters, ...scope, ...base, ...resolveFilterMap(tabFilter, params, ctx) }
|
|
296
314
|
},
|
|
@@ -310,7 +328,7 @@ export function useSelfFetchList(props, instance, inject, extras = {}) {
|
|
|
310
328
|
// for a lens without goes back to the page's own order. A column the
|
|
311
329
|
// person sorts by afterwards still wins until the next switch.
|
|
312
330
|
if (!isMultiQuickFilter && before && now[0] !== before[0]) {
|
|
313
|
-
const tabs =
|
|
331
|
+
const tabs = shownQuickFilters() || []
|
|
314
332
|
const keys = quickFilterSortKeys(tabs[now[0]]) || (quickFilterSortKeys(tabs[before[0]]) ? pageSortKeys : null)
|
|
315
333
|
if (keys && list.sortKeys) {
|
|
316
334
|
list.sortKeys.value = keys
|
|
@@ -197,6 +197,8 @@ With admin-only filter control:
|
|
|
197
197
|
| `visibleColumns` | Array | `null` | Currently visible column keys; `null` means all visible |
|
|
198
198
|
| `personalColumns` | Boolean | `true` | Show the Order and pin list and Reset columns in the Columns tab |
|
|
199
199
|
| `pinnedCount` | Number | `0` | How many of the first visible columns are pinned to the start of the table |
|
|
200
|
+
| `sortOptions` | Array | `[]` | The orders the Sort section in the Search tab offers: `[{ value, label, keys }]`, `keys` a `[{ key, order }]` list read as its ascending form. Empty draws no Sort section. Choosing a field or a direction emits `sort-change` with the new `[{ key, order }]`. |
|
|
201
|
+
| `currentSort` | Array | `[]` | The list's current order, `[{ key, order }]`: the Sort section shows its field, its direction and "Current order: ...". |
|
|
200
202
|
| `facetData` | Object | `{}` | Live facet data from the API: `{ fieldName: { values: [{ value, count }] } }` |
|
|
201
203
|
| `columnGroups` | Array | `[]` | Extra column groups beyond schema properties and built-in Metadata. Each: `{ id, label, columns: [{ key, label }], expanded? }` |
|
|
202
204
|
| `showMetadata` | Boolean | `true` | Whether to include the built-in Metadata column group |
|
|
@@ -37,6 +37,42 @@
|
|
|
37
37
|
@update:modelValue="$emit('search', $event)" />
|
|
38
38
|
</div>
|
|
39
39
|
|
|
40
|
+
<!-- Sort (list, card and map views; the table sorts by its headers):
|
|
41
|
+
the field, the direction and the order the list has now. -->
|
|
42
|
+
<div v-if="sortOptions.length > 0" class="cn-index-sidebar__section cn-index-sidebar__sort" data-testid="cn-index-sidebar-sort">
|
|
43
|
+
<h3>{{ t('nextcloud-vue', 'Sort') }}</h3>
|
|
44
|
+
<NcSelect
|
|
45
|
+
:modelValue="sortSelected"
|
|
46
|
+
:options="sortOptions"
|
|
47
|
+
label="label"
|
|
48
|
+
:clearable="false"
|
|
49
|
+
:inputLabel="t('nextcloud-vue', 'Sort by')"
|
|
50
|
+
@update:modelValue="onSortField" />
|
|
51
|
+
<div class="cn-index-sidebar__sort-direction" role="radiogroup" :aria-label="t('nextcloud-vue', 'Direction')">
|
|
52
|
+
<NcCheckboxRadioSwitch
|
|
53
|
+
type="radio"
|
|
54
|
+
name="cn-index-sidebar-sort-direction"
|
|
55
|
+
value="asc"
|
|
56
|
+
:modelValue="sortDirection"
|
|
57
|
+
:disabled="!sortSelected"
|
|
58
|
+
@update:modelValue="onSortDirection">
|
|
59
|
+
{{ t('nextcloud-vue', 'Ascending') }}
|
|
60
|
+
</NcCheckboxRadioSwitch>
|
|
61
|
+
<NcCheckboxRadioSwitch
|
|
62
|
+
type="radio"
|
|
63
|
+
name="cn-index-sidebar-sort-direction"
|
|
64
|
+
value="desc"
|
|
65
|
+
:modelValue="sortDirection"
|
|
66
|
+
:disabled="!sortSelected"
|
|
67
|
+
@update:modelValue="onSortDirection">
|
|
68
|
+
{{ t('nextcloud-vue', 'Descending') }}
|
|
69
|
+
</NcCheckboxRadioSwitch>
|
|
70
|
+
</div>
|
|
71
|
+
<p class="cn-index-sidebar__sort-current" data-testid="cn-index-sidebar-sort-current">
|
|
72
|
+
{{ currentSortText }}
|
|
73
|
+
</p>
|
|
74
|
+
</div>
|
|
75
|
+
|
|
40
76
|
<div v-if="hasActiveFilters" class="cn-index-sidebar__section">
|
|
41
77
|
<NcButton variant="tertiary" @click="onClearAll">
|
|
42
78
|
<template #icon>
|
|
@@ -273,6 +309,7 @@ import { METADATA_COLUMNS } from '../../constants/metadata.js'
|
|
|
273
309
|
import { facetOptions } from '../../utils/facets.js'
|
|
274
310
|
import { labelLang } from '../../utils/manifestTranslate.js'
|
|
275
311
|
import { columnsFromSchema, filtersFromSchema } from '../../utils/schema.js'
|
|
312
|
+
import { reverseSortKeys, sameSortKeys } from '../../utils/sortOrder.js'
|
|
276
313
|
import { CnDateRangePicker, DEFAULT_DATE_RANGE_PRESETS } from '../CnDateRangePicker/index.js'
|
|
277
314
|
import { CnIcon } from '../CnIcon/index.js'
|
|
278
315
|
|
|
@@ -366,6 +403,22 @@ export default {
|
|
|
366
403
|
default: 0,
|
|
367
404
|
},
|
|
368
405
|
|
|
406
|
+
/**
|
|
407
|
+
* The orders the Sort section offers, `[{ value, label, keys }]`, each
|
|
408
|
+
* `keys` a `[{ key, order }]` list read as its ascending form. Empty
|
|
409
|
+
* draws no Sort section (the table view sorts by its headers).
|
|
410
|
+
*/
|
|
411
|
+
sortOptions: {
|
|
412
|
+
type: Array,
|
|
413
|
+
default: () => [],
|
|
414
|
+
},
|
|
415
|
+
|
|
416
|
+
/** The list's current order, `[{ key, order }]`. */
|
|
417
|
+
currentSort: {
|
|
418
|
+
type: Array,
|
|
419
|
+
default: () => [],
|
|
420
|
+
},
|
|
421
|
+
|
|
369
422
|
/** Current search term */
|
|
370
423
|
searchValue: {
|
|
371
424
|
type: String,
|
|
@@ -508,7 +561,7 @@ export default {
|
|
|
508
561
|
},
|
|
509
562
|
},
|
|
510
563
|
|
|
511
|
-
emits: ['clear-filters', 'columns-change', 'columns-reorder', 'columns-reset', 'pin-change', 'filter-change', 'search', 'tab-change', 'update:open'],
|
|
564
|
+
emits: ['clear-filters', 'columns-change', 'columns-reorder', 'columns-reset', 'pin-change', 'filter-change', 'search', 'sort-change', 'tab-change', 'update:open'],
|
|
512
565
|
|
|
513
566
|
data() {
|
|
514
567
|
return {
|
|
@@ -535,6 +588,56 @@ export default {
|
|
|
535
588
|
},
|
|
536
589
|
|
|
537
590
|
computed: {
|
|
591
|
+
/**
|
|
592
|
+
* The Sort option the current order matches, as declared or reversed.
|
|
593
|
+
*
|
|
594
|
+
* @return {{option: object, direction: string}|null} The match, or null.
|
|
595
|
+
*/
|
|
596
|
+
sortMatch() {
|
|
597
|
+
const current = Array.isArray(this.currentSort) ? this.currentSort : []
|
|
598
|
+
for (const option of this.sortOptions) {
|
|
599
|
+
const keys = Array.isArray(option.keys) ? option.keys : []
|
|
600
|
+
if (sameSortKeys(keys, current)) {
|
|
601
|
+
return { option, direction: 'asc' }
|
|
602
|
+
}
|
|
603
|
+
if (sameSortKeys(reverseSortKeys(keys), current)) {
|
|
604
|
+
return { option, direction: 'desc' }
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
return null
|
|
608
|
+
},
|
|
609
|
+
|
|
610
|
+
/** @return {object|null} The Sort option shown as chosen. */
|
|
611
|
+
sortSelected() {
|
|
612
|
+
return this.sortMatch ? this.sortMatch.option : null
|
|
613
|
+
},
|
|
614
|
+
|
|
615
|
+
/** @return {string} The chosen direction, `asc` or `desc`. */
|
|
616
|
+
sortDirection() {
|
|
617
|
+
return this.sortMatch ? this.sortMatch.direction : 'asc'
|
|
618
|
+
},
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* The order the list has now, in words: "Current order: Created,
|
|
622
|
+
* descending", or that it is not sorted.
|
|
623
|
+
*
|
|
624
|
+
* @return {string} The sentence.
|
|
625
|
+
*/
|
|
626
|
+
currentSortText() {
|
|
627
|
+
const current = Array.isArray(this.currentSort) ? this.currentSort : []
|
|
628
|
+
if (current.length === 0) {
|
|
629
|
+
return t('nextcloud-vue', 'Not sorted')
|
|
630
|
+
}
|
|
631
|
+
const labelOf = (key) => {
|
|
632
|
+
const single = this.sortOptions.find((o) => Array.isArray(o.keys) && o.keys.length === 1 && o.keys[0].key === key)
|
|
633
|
+
return single ? single.label : key
|
|
634
|
+
}
|
|
635
|
+
const order = this.sortMatch && this.sortMatch.option.keys.length > 1
|
|
636
|
+
? `${this.sortMatch.option.label}, ${this.sortDirection === 'desc' ? t('nextcloud-vue', 'descending') : t('nextcloud-vue', 'ascending')}`
|
|
637
|
+
: current.map((entry) => `${labelOf(entry.key)}, ${entry.order === 'desc' ? t('nextcloud-vue', 'descending') : t('nextcloud-vue', 'ascending')}`).join('; ')
|
|
638
|
+
return t('nextcloud-vue', 'Current order: {order}', { order })
|
|
639
|
+
},
|
|
640
|
+
|
|
538
641
|
/** Resolved icon — explicit prop overrides schema.icon */
|
|
539
642
|
resolvedIcon() {
|
|
540
643
|
return this.icon || this.schema?.icon || ''
|
|
@@ -692,6 +795,50 @@ export default {
|
|
|
692
795
|
},
|
|
693
796
|
|
|
694
797
|
methods: {
|
|
798
|
+
t,
|
|
799
|
+
|
|
800
|
+
/**
|
|
801
|
+
* A field chosen in the Sort section: its order, in the direction
|
|
802
|
+
* already chosen.
|
|
803
|
+
*
|
|
804
|
+
* @param {object|null} option The chosen option.
|
|
805
|
+
* @return {void}
|
|
806
|
+
*/
|
|
807
|
+
onSortField(option) {
|
|
808
|
+
const chosen = option && this.sortOptions.find((o) => o.value === option.value)
|
|
809
|
+
if (!chosen) {
|
|
810
|
+
return
|
|
811
|
+
}
|
|
812
|
+
this.emitSort(chosen, this.sortDirection)
|
|
813
|
+
},
|
|
814
|
+
|
|
815
|
+
/**
|
|
816
|
+
* A direction chosen in the Sort section, for the chosen field.
|
|
817
|
+
*
|
|
818
|
+
* @param {string} direction `asc` or `desc`.
|
|
819
|
+
* @return {void}
|
|
820
|
+
*/
|
|
821
|
+
onSortDirection(direction) {
|
|
822
|
+
if (!this.sortSelected) {
|
|
823
|
+
return
|
|
824
|
+
}
|
|
825
|
+
this.emitSort(this.sortSelected, direction === 'desc' ? 'desc' : 'asc')
|
|
826
|
+
},
|
|
827
|
+
|
|
828
|
+
/**
|
|
829
|
+
* Emit the order an option stands for in a direction.
|
|
830
|
+
*
|
|
831
|
+
* @param {object} option The Sort option.
|
|
832
|
+
* @param {string} direction `asc` or `desc`.
|
|
833
|
+
* @return {void}
|
|
834
|
+
*/
|
|
835
|
+
emitSort(option, direction) {
|
|
836
|
+
const keys = Array.isArray(option.keys) ? option.keys : []
|
|
837
|
+
/**
|
|
838
|
+
* @event sort-change The Sort section chose an order. Payload: `[{ key, order }]`.
|
|
839
|
+
*/
|
|
840
|
+
this.$emit('sort-change', direction === 'desc' ? reverseSortKeys(keys) : keys.map((k) => ({ key: k.key, order: k.order || 'asc' })))
|
|
841
|
+
},
|
|
695
842
|
|
|
696
843
|
/**
|
|
697
844
|
* A manifest label through the injected lookup, or as written.
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
{{ t('nextcloud-vue', 'This widget can only be shown on the Nextcloud dashboard itself.') }}
|
|
47
47
|
</div>
|
|
48
48
|
|
|
49
|
-
<div v-else-if="
|
|
49
|
+
<div v-else-if="shownItems.length === 0" class="cn-nc-widget-widget__state">
|
|
50
50
|
{{ t('nextcloud-vue', 'No items available') }}
|
|
51
51
|
</div>
|
|
52
52
|
|
|
@@ -137,7 +137,10 @@ export default {
|
|
|
137
137
|
|
|
138
138
|
props: {
|
|
139
139
|
/**
|
|
140
|
-
* Persisted widget content `{widgetId, displayMode, limit?, showAllUrl?, showAllLabel?, showHeader?}`.
|
|
140
|
+
* Persisted widget content `{widgetId, displayMode, limit?, showAllUrl?, showAllLabel?, showHeader?, excludeApps?, excludeTypes?}`.
|
|
141
|
+
* `excludeApps` (`["systemtags"]`) and `excludeTypes` leave out the
|
|
142
|
+
* items of those apps or types (an item's own `app` / `type`, else the
|
|
143
|
+
* app in its link or icon URL); the list then comes from the items API.
|
|
141
144
|
* `showHeader: false` drops the widget's own header (icon and title);
|
|
142
145
|
* it is dropped too when the hosting card draws a title row.
|
|
143
146
|
* `limit` is the most items shown (the list then comes from the items
|
|
@@ -265,7 +268,38 @@ export default {
|
|
|
265
268
|
* @return {object[]} the items.
|
|
266
269
|
*/
|
|
267
270
|
shownItems() {
|
|
268
|
-
|
|
271
|
+
const kept = this.hasExclusions ? this.items.filter((item) => !this.isExcluded(item)) : this.items
|
|
272
|
+
return this.itemLimit > 0 ? kept.slice(0, this.itemLimit) : kept
|
|
273
|
+
},
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* The apps and item types `content.excludeApps` / `content.excludeTypes`
|
|
277
|
+
* leave out, lower-cased.
|
|
278
|
+
*
|
|
279
|
+
* @spec openspec/changes/nc-widget-exclude-apps/specs/nc-widget/spec.md#requirement-a-nextcloud-widget-can-leave-out-items-of-an-app-or-type
|
|
280
|
+
* @return {{apps: string[], types: string[]}}
|
|
281
|
+
*/
|
|
282
|
+
exclusions() {
|
|
283
|
+
const list = (value) => (Array.isArray(value) ? value : (typeof value === 'string' && value !== '' ? [value] : []))
|
|
284
|
+
.filter((entry) => typeof entry === 'string' && entry !== '')
|
|
285
|
+
.map((entry) => entry.toLowerCase())
|
|
286
|
+
return { apps: list(this.content && this.content.excludeApps), types: list(this.content && this.content.excludeTypes) }
|
|
287
|
+
},
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* How many items to ask the API for: the limit (or the default), and
|
|
291
|
+
* more while items are left out, so a filtered list still fills it.
|
|
292
|
+
*
|
|
293
|
+
* @return {number}
|
|
294
|
+
*/
|
|
295
|
+
requestLimit() {
|
|
296
|
+
const base = this.itemLimit > 0 ? this.itemLimit : API_ITEM_LIMIT
|
|
297
|
+
return this.hasExclusions ? Math.max(base * 3, 20) : base
|
|
298
|
+
},
|
|
299
|
+
|
|
300
|
+
/** @return {boolean} Whether any app or type is left out. */
|
|
301
|
+
hasExclusions() {
|
|
302
|
+
return this.exclusions.apps.length > 0 || this.exclusions.types.length > 0
|
|
269
303
|
},
|
|
270
304
|
|
|
271
305
|
/**
|
|
@@ -317,6 +351,34 @@ export default {
|
|
|
317
351
|
},
|
|
318
352
|
|
|
319
353
|
methods: {
|
|
354
|
+
/**
|
|
355
|
+
* Whether an item belongs to an app or type the content leaves out: its
|
|
356
|
+
* own `app` / `type` / `objectType`, else the app in an `/apps/<app>/`
|
|
357
|
+
* part of its link or icons.
|
|
358
|
+
*
|
|
359
|
+
* @spec openspec/changes/nc-widget-exclude-apps/specs/nc-widget/spec.md#requirement-a-nextcloud-widget-can-leave-out-items-of-an-app-or-type
|
|
360
|
+
* @param {object} item A widget item.
|
|
361
|
+
* @return {boolean}
|
|
362
|
+
*/
|
|
363
|
+
isExcluded(item) {
|
|
364
|
+
if (!item || typeof item !== 'object') {
|
|
365
|
+
return false
|
|
366
|
+
}
|
|
367
|
+
const { apps, types } = this.exclusions
|
|
368
|
+
const own = (value) => (typeof value === 'string' ? value.toLowerCase() : '')
|
|
369
|
+
const appsOf = [own(item.app)]
|
|
370
|
+
for (const url of [item.link, item.iconUrl, item.overlayIconUrl]) {
|
|
371
|
+
const match = typeof url === 'string' ? url.match(/\/apps\/([a-z0-9_-]+)\//i) : null
|
|
372
|
+
if (match) {
|
|
373
|
+
appsOf.push(match[1].toLowerCase())
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
if (apps.length > 0 && appsOf.some((app) => app !== '' && apps.includes(app))) {
|
|
377
|
+
return true
|
|
378
|
+
}
|
|
379
|
+
return types.length > 0 && [own(item.type), own(item.objectType)].some((type) => type !== '' && types.includes(type))
|
|
380
|
+
},
|
|
381
|
+
|
|
320
382
|
t,
|
|
321
383
|
|
|
322
384
|
/**
|
|
@@ -339,7 +401,8 @@ export default {
|
|
|
339
401
|
// where the widget has a registered native callback.
|
|
340
402
|
// A `limit` also takes the API list first: the native callback paints
|
|
341
403
|
// as many items as it likes. Its fallback to native stays.
|
|
342
|
-
|
|
404
|
+
// Leaving items out works on the API list only, so it skips native too.
|
|
405
|
+
if (this.displayMode !== 'horizontal' && this.itemLimit === 0 && !this.hasExclusions) {
|
|
343
406
|
const callback = this.resolveNativeCallback()
|
|
344
407
|
if (typeof callback === 'function') {
|
|
345
408
|
this.mountNative(callback)
|
|
@@ -424,7 +487,7 @@ export default {
|
|
|
424
487
|
// both the flat-array (older providers) and `{items}` (v2) shapes.
|
|
425
488
|
const url = generateOcsUrl('/apps/dashboard/api/v2/widget-items')
|
|
426
489
|
const response = await axios.get(url, {
|
|
427
|
-
params: { widgets: [this.widgetId], limit: this.
|
|
490
|
+
params: { widgets: [this.widgetId], limit: this.requestLimit },
|
|
428
491
|
})
|
|
429
492
|
const data = response && response.data && response.data.ocs ? response.data.ocs.data : null
|
|
430
493
|
const widgetData = data ? data[this.widgetId] : null
|
|
@@ -155,3 +155,4 @@ export default {
|
|
|
155
155
|
| `leading` | Object | `null` | Leading element: `{ initials }` in a circle or `{ icon }` on a tint |
|
|
156
156
|
| `footerAction` | Object | `null` | `{ label, meta?, ariaLabel? }`, one footer action; emits `footer-action` |
|
|
157
157
|
| `cardFields` | Array | `null` | Property keys of the facts list, in order |
|
|
158
|
+
| `layout` | Object | `null` | Board look: the record layout, `{ headPill, number, title, meta, footerPill, footerDate }`; each part names a field and is left out while it is empty. A part whose field names a page column (`cardFieldColumns`) reads through that column's formatter, and a meta part through its widget, as the table cell does. A pill with `due` (`{ soonWithin }`) or `variantWhen` reads its date: overdue as error, soon (or the matching rule) as warning, labelled by `labels[variant]` or `labels.overdue` / `labels.soon` through the host's translate, else Overdue / Soon; any other date draws no pill. |
|