@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.
Files changed (153) hide show
  1. package/dist/esm/components/CnCardGrid/CnCardGrid.vue.js +1 -1
  2. package/dist/esm/components/CnCardGrid/CnCardGrid.vue2.js +18 -0
  3. package/dist/esm/components/CnCardGrid/CnCardGrid.vue2.js.map +1 -1
  4. package/dist/esm/components/CnCardGrid/CnCardGrid.vue3.js +3 -1
  5. package/dist/esm/components/CnCardGrid/CnCardGrid.vue3.js.map +1 -1
  6. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue.js +1 -1
  7. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js +18 -0
  8. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js.map +1 -1
  9. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js +1 -1
  10. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js.map +1 -1
  11. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +53 -4
  12. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  13. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  14. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue.js +9 -0
  15. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue.js.map +1 -0
  16. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue2.js +24 -0
  17. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue2.js.map +1 -0
  18. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue3.js +49 -0
  19. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue3.js.map +1 -0
  20. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue.js +1 -1
  21. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js +36 -18
  22. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
  23. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js +17 -38
  24. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
  25. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue.js +1 -1
  26. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js.map +1 -1
  27. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js.map +1 -1
  28. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue.js +1 -1
  29. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js +53 -2
  30. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js.map +1 -1
  31. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js +119 -111
  32. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js.map +1 -1
  33. package/dist/esm/components/CnFlowRunsWidget/CnFlowRunsWidget.vue.js +1 -1
  34. package/dist/esm/components/CnFlowRunsWidget/CnFlowRunsWidget.vue2.js +62 -1
  35. package/dist/esm/components/CnFlowRunsWidget/CnFlowRunsWidget.vue2.js.map +1 -1
  36. package/dist/esm/components/CnFlowRunsWidget/CnFlowRunsWidget.vue3.js.map +1 -1
  37. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +159 -50
  38. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  39. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +121 -98
  40. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  41. package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue.js +1 -1
  42. package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue2.js +24 -0
  43. package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue2.js.map +1 -1
  44. package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue3.js +20 -4
  45. package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue3.js.map +1 -1
  46. package/dist/esm/components/CnIndexPage/useSelfFetchList.js +22 -4
  47. package/dist/esm/components/CnIndexPage/useSelfFetchList.js.map +1 -1
  48. package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue2.js +112 -1
  49. package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue2.js.map +1 -1
  50. package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue3.js +100 -44
  51. package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue3.js.map +1 -1
  52. package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue.js +1 -1
  53. package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue2.js +67 -4
  54. package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue2.js.map +1 -1
  55. package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue3.js +1 -1
  56. package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue3.js.map +1 -1
  57. package/dist/esm/components/CnObjectCard/CnObjectCard.vue.js +1 -1
  58. package/dist/esm/components/CnObjectCard/CnObjectCard.vue2.js +295 -12
  59. package/dist/esm/components/CnObjectCard/CnObjectCard.vue2.js.map +1 -1
  60. package/dist/esm/components/CnObjectCard/CnObjectCard.vue3.js +267 -144
  61. package/dist/esm/components/CnObjectCard/CnObjectCard.vue3.js.map +1 -1
  62. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue.js +1 -1
  63. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js +4 -0
  64. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js.map +1 -1
  65. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js +3 -1
  66. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js.map +1 -1
  67. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
  68. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +93 -1
  69. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
  70. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js +322 -246
  71. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
  72. package/dist/esm/components/CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue.js +1 -1
  73. package/dist/esm/components/CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue2.js +122 -7
  74. package/dist/esm/components/CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue2.js.map +1 -1
  75. package/dist/esm/components/CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue3.js +189 -93
  76. package/dist/esm/components/CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue3.js.map +1 -1
  77. package/dist/esm/components/CnTabs/CnTabs.vue.js +1 -1
  78. package/dist/esm/components/CnTabs/CnTabs.vue2.js.map +1 -1
  79. package/dist/esm/components/CnTabs/CnTabs.vue3.js.map +1 -1
  80. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue.js +1 -1
  81. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +42 -0
  82. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
  83. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js +38 -19
  84. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
  85. package/dist/esm/components/CnTeamWorkloadWidget/CnTeamWorkloadWidget.vue.js +9 -0
  86. package/dist/esm/components/CnTeamWorkloadWidget/CnTeamWorkloadWidget.vue.js.map +1 -0
  87. package/dist/esm/components/CnTeamWorkloadWidget/CnTeamWorkloadWidget.vue2.js +258 -0
  88. package/dist/esm/components/CnTeamWorkloadWidget/CnTeamWorkloadWidget.vue2.js.map +1 -0
  89. package/dist/esm/components/CnTeamWorkloadWidget/CnTeamWorkloadWidget.vue3.js +69 -0
  90. package/dist/esm/components/CnTeamWorkloadWidget/CnTeamWorkloadWidget.vue3.js.map +1 -0
  91. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js +12 -0
  92. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js.map +1 -1
  93. package/dist/esm/index.js +1 -0
  94. package/dist/esm/index.js.map +1 -1
  95. package/dist/esm/l10n/en.json.js +21 -1
  96. package/dist/esm/l10n/en.json.js.map +1 -1
  97. package/dist/esm/l10n/nl.json.js +21 -1
  98. package/dist/esm/l10n/nl.json.js.map +1 -1
  99. package/dist/esm/library-style.js +1 -1
  100. package/dist/esm/nextcloud-vue.css +803 -453
  101. package/dist/esm/utils/dueText.js +18 -3
  102. package/dist/esm/utils/dueText.js.map +1 -1
  103. package/dist/esm/utils/libraryWidgetKeys.js +1 -0
  104. package/dist/esm/utils/libraryWidgetKeys.js.map +1 -1
  105. package/dist/esm/utils/sortOrder.js +11 -1
  106. package/dist/esm/utils/sortOrder.js.map +1 -1
  107. package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
  108. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  109. package/dist/nextcloud-vue.cjs.js +8281 -6350
  110. package/dist/nextcloud-vue.cjs.js.map +2 -2
  111. package/dist/nextcloud-vue.css +803 -453
  112. package/l10n/en.json +21 -1
  113. package/l10n/nl.json +21 -1
  114. package/package.json +1 -1
  115. package/src/components/CnCardGrid/CnCardGrid.md +2 -0
  116. package/src/components/CnCardGrid/CnCardGrid.vue +20 -0
  117. package/src/components/CnCellRenderer/CnCellRenderer.vue +19 -1
  118. package/src/components/CnDetailPage/CnDetailPage.md +2 -2
  119. package/src/components/CnDetailPage/CnDetailPage.vue +53 -4
  120. package/src/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue +60 -0
  121. package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +36 -29
  122. package/src/components/CnFactsWidget/CnFactsWidget.vue +4 -0
  123. package/src/components/CnFilesBrowser/CnFilesBrowser.vue +70 -4
  124. package/src/components/CnFlowRunsWidget/CnFlowRunsWidget.vue +62 -1
  125. package/src/components/CnIndexPage/CnIndexPage.md +7 -5
  126. package/src/components/CnIndexPage/CnIndexPage.vue +187 -58
  127. package/src/components/CnIndexPage/CnIndexPageWidgets.vue +36 -1
  128. package/src/components/CnIndexPage/useSelfFetchList.js +22 -4
  129. package/src/components/CnIndexSidebar/CnIndexSidebar.md +2 -0
  130. package/src/components/CnIndexSidebar/CnIndexSidebar.vue +148 -1
  131. package/src/components/CnNcWidgetWidget/CnNcWidgetWidget.vue +68 -5
  132. package/src/components/CnObjectCard/CnObjectCard.md +1 -0
  133. package/src/components/CnObjectCard/CnObjectCard.vue +354 -13
  134. package/src/components/CnObjectSidebar/CnFilesTab.vue +6 -0
  135. package/src/components/CnRecordListWidget/CnRecordListWidget.vue +190 -4
  136. package/src/components/CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue +209 -9
  137. package/src/components/CnTabs/CnTabs.vue +11 -0
  138. package/src/components/CnTabsWidget/CnTabsWidget.vue +91 -18
  139. package/src/components/CnTeamWorkloadWidget/CnTeamWorkloadWidget.md +11 -0
  140. package/src/components/CnTeamWorkloadWidget/CnTeamWorkloadWidget.vue +382 -0
  141. package/src/components/CnTeamWorkloadWidget/index.js +14 -0
  142. package/src/components/CnWidgetGrid/registerDashboardWidgets.js +12 -0
  143. package/src/components/index.js +1 -0
  144. package/src/css/index-page.css +14 -2
  145. package/src/css/look-board-detail.css +48 -4
  146. package/src/css/look-board-index.css +64 -2
  147. package/src/css/look-board.css +25 -2
  148. package/src/index.js +1 -1
  149. package/src/schemas/app-manifest-v2.schema.json +75 -6
  150. package/src/utils/dueText.js +18 -3
  151. package/src/utils/libraryWidgetKeys.js +1 -0
  152. package/src/utils/sortOrder.js +10 -0
  153. 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 = withPersonalLenses(props.quickFilters, props.personalLenses)
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
- const activeIdx = activeQuickFilterIndex.value
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 = withPersonalLenses(props.quickFilters, props.personalLenses) || []
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="items.length === 0" class="cn-nc-widget-widget__state">
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
- return this.itemLimit > 0 ? this.items.slice(0, this.itemLimit) : this.items
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
- if (this.displayMode !== 'horizontal' && this.itemLimit === 0) {
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.itemLimit > 0 ? this.itemLimit : API_ITEM_LIMIT },
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. |