@conduction/nextcloud-vue 2.56.0-beta.12 → 2.56.0-beta.14
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/CnActionsBar/CnActionsBar.vue2.js +19 -0
- package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js.map +1 -1
- package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js +10 -3
- package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js.map +1 -1
- package/dist/esm/components/CnAppNav/CnAppNav.vue2.js +101 -5
- package/dist/esm/components/CnAppNav/CnAppNav.vue2.js.map +1 -1
- package/dist/esm/components/CnAppNav/CnAppNav.vue3.js +23 -7
- package/dist/esm/components/CnAppNav/CnAppNav.vue3.js.map +1 -1
- package/dist/esm/components/CnBreadcrumbs/CnBreadcrumbs.vue2.js +13 -0
- package/dist/esm/components/CnBreadcrumbs/CnBreadcrumbs.vue2.js.map +1 -1
- package/dist/esm/components/CnBreadcrumbs/CnBreadcrumbs.vue3.js +2 -1
- package/dist/esm/components/CnBreadcrumbs/CnBreadcrumbs.vue3.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +125 -4
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +439 -381
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue2.js +9 -3
- package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue2.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue3.js +1 -1
- package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue3.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +4 -4
- package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +142 -26
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +195 -124
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
- package/dist/esm/components/CnFkResolveCell/CnFkResolveCell.vue2.js +3 -1
- package/dist/esm/components/CnFkResolveCell/CnFkResolveCell.vue2.js.map +1 -1
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js +144 -4
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
- package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue2.js +84 -11
- package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue3.js +3 -2
- package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +31 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +335 -313
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
- package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue2.js +11 -0
- package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue2.js.map +1 -1
- package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue3.js +2 -1
- package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue3.js.map +1 -1
- package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js +5 -11
- package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js.map +1 -1
- package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue3.js.map +1 -1
- package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js +22 -3
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js +10 -3
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js.map +1 -1
- package/dist/esm/composables/useDependencyCheck.js +44 -1
- package/dist/esm/composables/useDependencyCheck.js.map +1 -1
- package/dist/esm/composables/useListView.js +43 -1
- package/dist/esm/composables/useListView.js.map +1 -1
- package/dist/esm/composables/useSetupStatus.js +26 -3
- package/dist/esm/composables/useSetupStatus.js.map +1 -1
- package/dist/esm/l10n/en.json.js +7 -1
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +7 -1
- package/dist/esm/l10n/nl.json.js.map +1 -1
- package/dist/esm/mixins/pageViews.js +400 -0
- package/dist/esm/mixins/pageViews.js.map +1 -0
- package/dist/esm/nextcloud-vue.css +202 -2
- package/dist/esm/store/plugins/liveUpdates.js +19 -9
- package/dist/esm/store/plugins/liveUpdates.js.map +1 -1
- package/dist/esm/store/useObjectStore.js +138 -42
- package/dist/esm/store/useObjectStore.js.map +1 -1
- package/dist/esm/utils/actionsDispatcher.js +25 -2
- package/dist/esm/utils/actionsDispatcher.js.map +1 -1
- package/dist/esm/utils/columnFilters.js +27 -3
- package/dist/esm/utils/columnFilters.js.map +1 -1
- package/dist/esm/utils/schema.js +40 -2
- package/dist/esm/utils/schema.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/nextcloud-vue.cjs.js +2857 -1335
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +202 -2
- package/l10n/en.json +7 -1
- package/l10n/nl.json +7 -1
- package/package.json +1 -1
- package/src/components/CnActionsBar/CnActionsBar.md +2 -1
- package/src/components/CnActionsBar/CnActionsBar.vue +25 -1
- package/src/components/CnAppNav/CnAppNav.vue +150 -9
- package/src/components/CnBreadcrumbs/CnBreadcrumbs.vue +14 -0
- package/src/components/CnDashboardPage/CnDashboardPage.vue +194 -10
- package/src/components/CnDataTable/CnColumnFilterPopover.vue +11 -5
- package/src/components/CnDataTable/CnDataTable.md +1 -1
- package/src/components/CnDataTable/CnDataTable.vue +4 -4
- package/src/components/CnDetailPage/CnDetailPage.md +5 -1
- package/src/components/CnDetailPage/CnDetailPage.vue +208 -11
- package/src/components/CnFkResolveCell/CnFkResolveCell.vue +3 -1
- package/src/components/CnFormDialog/CnFormDialog.vue +144 -4
- package/src/components/CnHeaderWidget/CnHeaderWidget.vue +100 -13
- package/src/components/CnIndexPage/CnIndexPage.md +2 -1
- package/src/components/CnIndexPage/CnIndexPage.vue +47 -1
- package/src/components/CnSegmentedControl/CnSegmentedControl.md +1 -1
- package/src/components/CnSegmentedControl/CnSegmentedControl.vue +12 -0
- package/src/components/CnSetupWizard/CnSetupWizard.vue +5 -11
- package/src/components/CnStagesWidget/CnStagesWidget.vue +9 -0
- package/src/components/CnStatWidget/CnStatWidget.vue +33 -5
- package/src/composables/useDependencyCheck.js +43 -0
- package/src/composables/useListView.js +43 -1
- package/src/composables/useSetupStatus.js +26 -3
- package/src/css/actions-bar.css +20 -2
- package/src/css/detail-page.css +27 -0
- package/src/css/kpi-card.css +47 -0
- package/src/mixins/pageViews.js +396 -0
- package/src/schemas/app-manifest-v2.schema.json +53 -4
- package/src/schemas/app-manifest.schema.json +1 -1
- package/src/store/plugins/liveUpdates.js +19 -9
- package/src/store/useObjectStore.js +137 -41
- package/src/utils/actionsDispatcher.js +25 -2
- package/src/utils/columnFilters.js +29 -2
- package/src/utils/schema.js +40 -2
- package/src/utils/validateManifestV2.compiled.js +2 -2
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
-->
|
|
5
5
|
|
|
6
6
|
<template>
|
|
7
|
-
<div class="cn-header-widget" :class="{ 'cn-header-widget--plain': isPlain, 'cn-header-widget--with-views': viewOptions.length > 0 }" :style="wrapperStyle">
|
|
7
|
+
<div class="cn-header-widget" :class="{ 'cn-header-widget--plain': isPlain, 'cn-header-widget--ground': isGround, 'cn-header-widget--with-views': viewOptions.length > 0 }" :style="wrapperStyle">
|
|
8
8
|
<div
|
|
9
9
|
v-if="hasOverlay"
|
|
10
10
|
class="cn-header-widget__overlay"
|
|
@@ -35,12 +35,15 @@
|
|
|
35
35
|
</a>
|
|
36
36
|
</div>
|
|
37
37
|
<!-- A view switch at the right of the heading (`content.views`): "My
|
|
38
|
-
work / My team" as a segmented control whose options are routes
|
|
38
|
+
work / My team" as a segmented control whose options are routes,
|
|
39
|
+
or views of the page the widget sits on (`option.view`); a view
|
|
40
|
+
option controls the page's view region. -->
|
|
39
41
|
<div v-if="viewOptions.length > 0" class="cn-header-widget__views">
|
|
40
42
|
<CnSegmentedControl
|
|
41
43
|
:options="viewOptions"
|
|
42
44
|
:modelValue="activeView"
|
|
43
45
|
:ariaLabel="viewsLabel"
|
|
46
|
+
:controls="viewsRegionId"
|
|
44
47
|
data-testid="cn-header-widget-views"
|
|
45
48
|
@update:modelValue="onViewChange" />
|
|
46
49
|
</div>
|
|
@@ -66,6 +69,9 @@ const ALLOWED_TEXT_ALIGN = ['left', 'center', 'right']
|
|
|
66
69
|
const ALLOWED_VERTICAL_ALIGN = ['top', 'middle', 'bottom']
|
|
67
70
|
const ALLOWED_CTA_STYLES = ['primary', 'secondary', 'ghost']
|
|
68
71
|
|
|
72
|
+
/** Marks a switch option that selects a page view rather than a route. */
|
|
73
|
+
const VIEW_OPTION_PREFIX = 'view:'
|
|
74
|
+
|
|
69
75
|
const HEIGHT_PIXELS = Object.freeze({
|
|
70
76
|
small: 120,
|
|
71
77
|
medium: 200,
|
|
@@ -121,6 +127,12 @@ export default {
|
|
|
121
127
|
* untranslated key renders as itself.
|
|
122
128
|
*/
|
|
123
129
|
cnTranslate: { default: () => (key) => key },
|
|
130
|
+
/**
|
|
131
|
+
* The views of the page this widget sits on, provided by
|
|
132
|
+
* CnDashboardPage / CnDetailPage (`views`). An option with a `view`
|
|
133
|
+
* selects one of them. Null outside such a page.
|
|
134
|
+
*/
|
|
135
|
+
cnPageViews: { default: null },
|
|
124
136
|
},
|
|
125
137
|
|
|
126
138
|
props: {
|
|
@@ -128,12 +140,18 @@ export default {
|
|
|
128
140
|
* Persisted widget content: `{title, subtitle, backgroundImageUrl,
|
|
129
141
|
* backgroundImageFileId, backgroundColor, overlayMode, overlayColor,
|
|
130
142
|
* overlayOpacity, textColor, textAlign, verticalAlign, height, cta,
|
|
131
|
-
* greeting, showDate, plain, views}`. All fields are optional except
|
|
143
|
+
* greeting, showDate, plain, ground, views}`. All fields are optional except
|
|
132
144
|
* `title` (or `greeting`); unknown enum values collapse to documented
|
|
133
145
|
* defaults and the renderer never throws. `views` is
|
|
134
|
-
* `{ ariaLabel?, options: [{ label, route, params? }] }`:
|
|
135
|
-
* control at the right of the heading whose checked option
|
|
136
|
-
* current route; choosing another pushes its route.
|
|
146
|
+
* `{ ariaLabel?, options: [{ label, route, params? } | { label, view }] }`:
|
|
147
|
+
* a segmented control at the right of the heading whose checked option
|
|
148
|
+
* is the current route; choosing another pushes its route. An option
|
|
149
|
+
* with `view` selects that view of the page the widget sits on (the
|
|
150
|
+
* page's `views`) instead, and the page then draws no switch of its
|
|
151
|
+
* own. `ground: true`
|
|
152
|
+
* draws the greeting on the page ground: the plain look with no
|
|
153
|
+
* padding, a 32px heading and the date line 6px above it (a dashboard
|
|
154
|
+
* also drops the widget's card for it).
|
|
137
155
|
*
|
|
138
156
|
* @type {object}
|
|
139
157
|
*/
|
|
@@ -226,12 +244,45 @@ export default {
|
|
|
226
244
|
*/
|
|
227
245
|
viewOptions() {
|
|
228
246
|
const raw = this.content && this.content.views && this.content.views.options
|
|
229
|
-
if (!Array.isArray(raw)
|
|
247
|
+
if (!Array.isArray(raw)) {
|
|
230
248
|
return []
|
|
231
249
|
}
|
|
250
|
+
const pageViewIds = this.pageViewIds
|
|
232
251
|
return raw
|
|
233
|
-
.filter((o) => o && typeof o.label === 'string' && o.label !== ''
|
|
234
|
-
.map((o) =>
|
|
252
|
+
.filter((o) => o && typeof o.label === 'string' && o.label !== '')
|
|
253
|
+
.map((o) => {
|
|
254
|
+
if (typeof o.view === 'string' && o.view !== '') {
|
|
255
|
+
return pageViewIds.includes(o.view) ? { value: VIEW_OPTION_PREFIX + o.view, label: this.cnTranslate(o.label) } : null
|
|
256
|
+
}
|
|
257
|
+
if (typeof o.route === 'string' && o.route !== '' && this.$router) {
|
|
258
|
+
return { value: o.route, label: this.cnTranslate(o.label) }
|
|
259
|
+
}
|
|
260
|
+
return null
|
|
261
|
+
})
|
|
262
|
+
.filter(Boolean)
|
|
263
|
+
},
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* The ids of the page's views, empty outside a page with views.
|
|
267
|
+
*
|
|
268
|
+
* @spec openspec/changes/view-switch-containers/specs/view-switch-containers/spec.md#requirement-a-greeting-header-can-switch-the-pages-views
|
|
269
|
+
* @return {string[]}
|
|
270
|
+
*/
|
|
271
|
+
pageViewIds() {
|
|
272
|
+
const views = this.cnPageViews && Array.isArray(this.cnPageViews.views) ? this.cnPageViews.views : []
|
|
273
|
+
return views.map((view) => view.id)
|
|
274
|
+
},
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* The id of the page's view region when an option selects a view,
|
|
278
|
+
* for the options' `aria-controls`; '' otherwise.
|
|
279
|
+
*
|
|
280
|
+
* @spec openspec/changes/view-switch-containers/specs/view-switch-containers/spec.md#requirement-the-view-switch-is-accessible
|
|
281
|
+
* @return {string}
|
|
282
|
+
*/
|
|
283
|
+
viewsRegionId() {
|
|
284
|
+
const hasViewOption = this.viewOptions.some((o) => o.value.startsWith(VIEW_OPTION_PREFIX))
|
|
285
|
+
return hasViewOption && this.cnPageViews ? (this.cnPageViews.regionId || '') : ''
|
|
235
286
|
},
|
|
236
287
|
|
|
237
288
|
/**
|
|
@@ -246,6 +297,11 @@ export default {
|
|
|
246
297
|
if (options.length === 0) {
|
|
247
298
|
return null
|
|
248
299
|
}
|
|
300
|
+
const activeView = this.cnPageViews ? this.cnPageViews.activeId : null
|
|
301
|
+
const viewMatch = activeView ? options.find((o) => o.value === VIEW_OPTION_PREFIX + activeView) : null
|
|
302
|
+
if (viewMatch) {
|
|
303
|
+
return viewMatch.value
|
|
304
|
+
}
|
|
249
305
|
const current = this.$route && this.$route.name
|
|
250
306
|
const match = options.find((o) => o.value === current)
|
|
251
307
|
return match ? match.value : options[0].value
|
|
@@ -260,7 +316,7 @@ export default {
|
|
|
260
316
|
*/
|
|
261
317
|
viewsLabel() {
|
|
262
318
|
const label = this.content && this.content.views && this.content.views.ariaLabel
|
|
263
|
-
return label ? this.cnTranslate(label) : t('nextcloud-vue', '
|
|
319
|
+
return label ? this.cnTranslate(label) : t('nextcloud-vue', 'Views')
|
|
264
320
|
},
|
|
265
321
|
|
|
266
322
|
/**
|
|
@@ -269,7 +325,20 @@ export default {
|
|
|
269
325
|
* @return {boolean}
|
|
270
326
|
*/
|
|
271
327
|
isPlain() {
|
|
272
|
-
return Boolean(this.content && this.content.plain === true)
|
|
328
|
+
return Boolean(this.content && (this.content.plain === true || this.content.ground === true))
|
|
329
|
+
},
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Whether the greeting sits on the page ground (`content.ground`):
|
|
333
|
+
* the plain look without the card padding, so the date line and the
|
|
334
|
+
* heading align with the page edge as the board draws them. Off by
|
|
335
|
+
* default.
|
|
336
|
+
*
|
|
337
|
+
* @spec openspec/changes/zuiddrecht-pixel-gaps-2/specs/zuiddrecht-pixel-gaps-2/spec.md#requirement-a-greeting-can-sit-on-the-page-ground
|
|
338
|
+
* @return {boolean}
|
|
339
|
+
*/
|
|
340
|
+
isGround() {
|
|
341
|
+
return Boolean(this.content && this.content.ground === true)
|
|
273
342
|
},
|
|
274
343
|
|
|
275
344
|
/**
|
|
@@ -565,9 +634,10 @@ export default {
|
|
|
565
634
|
'justify-content': VERTICAL_ALIGN_FLEX[this.verticalAlign],
|
|
566
635
|
// Plain drops the coloured background, not the card padding:
|
|
567
636
|
// at 0 the greeting sat flush against the card's left edge.
|
|
568
|
-
|
|
637
|
+
// Ground has no card, so it has no padding either.
|
|
638
|
+
padding: this.isGround ? '0' : (this.isPlain ? '16px' : '16px 24px'),
|
|
569
639
|
'box-sizing': 'border-box',
|
|
570
|
-
gap: '8px',
|
|
640
|
+
gap: this.isGround ? '6px' : '8px',
|
|
571
641
|
'text-align': this.textAlign,
|
|
572
642
|
}
|
|
573
643
|
},
|
|
@@ -680,6 +750,12 @@ export default {
|
|
|
680
750
|
* @return {void}
|
|
681
751
|
*/
|
|
682
752
|
onViewChange(route) {
|
|
753
|
+
if (typeof route === 'string' && route.startsWith(VIEW_OPTION_PREFIX)) {
|
|
754
|
+
if (this.cnPageViews && typeof this.cnPageViews.select === 'function') {
|
|
755
|
+
this.cnPageViews.select(route.slice(VIEW_OPTION_PREFIX.length))
|
|
756
|
+
}
|
|
757
|
+
return
|
|
758
|
+
}
|
|
683
759
|
if (!this.$router || !route || route === (this.$route && this.$route.name)) {
|
|
684
760
|
return
|
|
685
761
|
}
|
|
@@ -765,6 +841,17 @@ export default {
|
|
|
765
841
|
padding: 16px;
|
|
766
842
|
}
|
|
767
843
|
|
|
844
|
+
/* On the page ground (`content.ground`): no card, so no inset around the
|
|
845
|
+
view switch, and the heading at the board's 32px. */
|
|
846
|
+
.cn-header-widget--ground .cn-header-widget__views {
|
|
847
|
+
padding: 0;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
.cn-header-widget--ground .cn-header-widget__title {
|
|
851
|
+
font-size: var(--cn-header-ground-title-size, 32px);
|
|
852
|
+
letter-spacing: -0.01em;
|
|
853
|
+
}
|
|
854
|
+
|
|
768
855
|
.cn-header-widget__date {
|
|
769
856
|
font-size: 14px;
|
|
770
857
|
line-height: 1.4;
|
|
@@ -386,7 +386,8 @@ export default {
|
|
|
386
386
|
| `fieldOverrides` | Object | `{}` | Per-field config overrides passed to `CnFormDialog` |
|
|
387
387
|
| `customComponents` | Object | `null` | Custom-component / handler registry. When set, takes precedence over the injected `cnCustomComponents` from CnAppRoot. Used to resolve `actions[].handler` registry names declared in the manifest (manifest-actions-dispatch). Named handlers resolve out of the v2 `registry` (a `kind: "handler"` entry) FIRST and fall back to this map. |
|
|
388
388
|
| `showViewToggle` | Boolean | `true` | Whether to show the Cards/Table view toggle |
|
|
389
|
-
| `inlineSearch` | Boolean | `false` | Show an inline search field in the actions bar (in addition to / instead of the sidebar search). Fed from the manifest as `pages[].config.inlineSearch`. |
|
|
389
|
+
| `inlineSearch` | Boolean | `false` | Show an inline search field in the actions bar (in addition to / instead of the sidebar search). Fed from the manifest as `pages[].config.inlineSearch`. The field takes the place of the "Showing X of Y" counter unless `showCountWithSearch` is set. |
|
|
390
|
+
| `showCountWithSearch` | Boolean | `false` | Keep the "Showing X of Y" counter visible beside the inline search field, after the search and any `#after-search` controls (only relevant with `inlineSearch`, which otherwise takes the counter's place). Fed from the manifest as `pages[].config.showCountWithSearch`. |
|
|
390
391
|
| `searchPlaceholder` | String | `''` | Placeholder for the inline search field (manifest `config.searchPlaceholder`). |
|
|
391
392
|
| `cardsLabel` | String | `''` | Label for the cards view-toggle option (manifest `config.cardsLabel`, e.g. "Tiles"). |
|
|
392
393
|
| `tableLabel` | String | `''` | Label for the table view-toggle option (manifest `config.tableLabel`, e.g. "List"). |
|
|
@@ -59,6 +59,7 @@
|
|
|
59
59
|
:showSearch="inlineSearch"
|
|
60
60
|
:searchValue="effectiveSearchValue"
|
|
61
61
|
:searchPlaceholder="searchPlaceholder"
|
|
62
|
+
:showCountWithSearch="showCountWithSearch"
|
|
62
63
|
:refreshing="effectiveRefreshing"
|
|
63
64
|
:refreshDisabled="refreshDisabled"
|
|
64
65
|
:addDisabled="addDisabled"
|
|
@@ -417,6 +418,21 @@
|
|
|
417
418
|
<NcLoadingIcon :size="32" :name="loadingText" />
|
|
418
419
|
</div>
|
|
419
420
|
|
|
421
|
+
<!-- Error state: the latest self-fetch failed (e.g. a search term the server rejects) -->
|
|
422
|
+
<div v-else-if="selfFetchFailed"
|
|
423
|
+
class="cn-index-page__empty"
|
|
424
|
+
role="status"
|
|
425
|
+
data-testid="cn-index-page-fetch-error">
|
|
426
|
+
<NcEmptyContent :name="t('nextcloud-vue', 'An error occurred')"
|
|
427
|
+
:description="effectiveSearchValue
|
|
428
|
+
? t('nextcloud-vue', 'Change the search or try again.')
|
|
429
|
+
: t('nextcloud-vue', 'Try again later.')">
|
|
430
|
+
<template #icon>
|
|
431
|
+
<AlertCircleOutline :size="64" />
|
|
432
|
+
</template>
|
|
433
|
+
</NcEmptyContent>
|
|
434
|
+
</div>
|
|
435
|
+
|
|
420
436
|
<!-- Empty state -->
|
|
421
437
|
<div v-else-if="effectiveObjects.length === 0" class="cn-index-page__empty">
|
|
422
438
|
<slot name="empty">
|
|
@@ -774,6 +790,7 @@ import { getCurrentUser } from '@nextcloud/auth'
|
|
|
774
790
|
import { translate as t } from '@nextcloud/l10n'
|
|
775
791
|
import { NcActionButton, NcActionCaption, NcActionCheckbox, NcActions, NcButton, NcEmptyContent, NcLoadingIcon } from '@nextcloud/vue'
|
|
776
792
|
import { getCurrentInstance, inject, markRaw, ref } from 'vue'
|
|
793
|
+
import AlertCircleOutline from 'vue-material-design-icons/AlertCircleOutline.vue'
|
|
777
794
|
import Close from 'vue-material-design-icons/Close.vue'
|
|
778
795
|
import Cog from 'vue-material-design-icons/Cog.vue'
|
|
779
796
|
import DatabaseSearch from 'vue-material-design-icons/DatabaseSearch.vue'
|
|
@@ -1054,6 +1071,7 @@ export default {
|
|
|
1054
1071
|
NcActionCaption,
|
|
1055
1072
|
NcActionCheckbox,
|
|
1056
1073
|
NcButton,
|
|
1074
|
+
AlertCircleOutline,
|
|
1057
1075
|
Close,
|
|
1058
1076
|
Cog,
|
|
1059
1077
|
DatabaseSearch,
|
|
@@ -2034,6 +2052,17 @@ export default {
|
|
|
2034
2052
|
default: false,
|
|
2035
2053
|
},
|
|
2036
2054
|
|
|
2055
|
+
/**
|
|
2056
|
+
* Keep the "Showing X of Y" counter visible beside the inline search field,
|
|
2057
|
+
* after any `#after-search` controls (only relevant with `inlineSearch`,
|
|
2058
|
+
* which otherwise takes the counter's place). Fed from the manifest as
|
|
2059
|
+
* `pages[].config.showCountWithSearch`.
|
|
2060
|
+
*/
|
|
2061
|
+
showCountWithSearch: {
|
|
2062
|
+
type: Boolean,
|
|
2063
|
+
default: false,
|
|
2064
|
+
},
|
|
2065
|
+
|
|
2037
2066
|
/** Placeholder for the inline search field (manifest `config.searchPlaceholder`) */
|
|
2038
2067
|
searchPlaceholder: {
|
|
2039
2068
|
type: String,
|
|
@@ -2934,11 +2963,23 @@ export default {
|
|
|
2934
2963
|
return this.isSelfFetch && !!this.list
|
|
2935
2964
|
},
|
|
2936
2965
|
|
|
2966
|
+
/**
|
|
2967
|
+
* Whether the latest self-fetch failed. The store keeps the previous
|
|
2968
|
+
* rows and pagination on a failed fetch, so while this holds the page
|
|
2969
|
+
* shows its error state instead of results for an earlier query.
|
|
2970
|
+
*/
|
|
2971
|
+
selfFetchFailed() {
|
|
2972
|
+
return this.isSelfFetchMode && !!this.list.error?.value
|
|
2973
|
+
},
|
|
2974
|
+
|
|
2937
2975
|
/** Rows: store collection in self-fetch mode, else the `objects` prop. */
|
|
2938
2976
|
effectiveObjects() {
|
|
2939
2977
|
if (this.isNamedSource) {
|
|
2940
2978
|
return this.namedRows
|
|
2941
2979
|
}
|
|
2980
|
+
if (this.selfFetchFailed) {
|
|
2981
|
+
return []
|
|
2982
|
+
}
|
|
2942
2983
|
return this.isSelfFetchMode ? (this.list.objects.value || []) : this.objects
|
|
2943
2984
|
},
|
|
2944
2985
|
|
|
@@ -3342,7 +3383,9 @@ export default {
|
|
|
3342
3383
|
s: this.exportSchemaSlug,
|
|
3343
3384
|
f: this.filter || null,
|
|
3344
3385
|
e: this.countEntries,
|
|
3345
|
-
|
|
3386
|
+
// The store's total, not the error override in effectivePagination,
|
|
3387
|
+
// so a failed fetch and its recovery do not refetch the counts.
|
|
3388
|
+
t: Number((this.isSelfFetchMode ? this.list.pagination.value : this.effectivePagination)?.total ?? 0),
|
|
3346
3389
|
})
|
|
3347
3390
|
},
|
|
3348
3391
|
|
|
@@ -3748,6 +3791,9 @@ export default {
|
|
|
3748
3791
|
|
|
3749
3792
|
/** Pagination: store pagination in self-fetch mode, else the `pagination` prop. */
|
|
3750
3793
|
effectivePagination() {
|
|
3794
|
+
if (this.selfFetchFailed) {
|
|
3795
|
+
return { ...this.list.pagination.value, total: 0, page: 1, pages: 1 }
|
|
3796
|
+
}
|
|
3751
3797
|
return this.isSelfFetchMode ? this.list.pagination.value : this.pagination
|
|
3752
3798
|
},
|
|
3753
3799
|
|
|
@@ -43,4 +43,4 @@ export default {
|
|
|
43
43
|
</script>
|
|
44
44
|
```
|
|
45
45
|
|
|
46
|
-
Props: `options` lists the choices, `modelValue` is the chosen value (use `v-model`), `aria-label` names the group (or point `aria-labelledby` at a visible heading), and `stretch` fills the width.
|
|
46
|
+
Props: `options` lists the choices, `modelValue` is the chosen value (use `v-model`), `aria-label` names the group (or point `aria-labelledby` at a visible heading), `controls` names the id of the element the options switch (set as `aria-controls` on every option), and `stretch` fills the width.
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
:aria-checked="isChecked(option) ? 'true' : 'false'"
|
|
21
21
|
:tabindex="index === focusableIndex ? 0 : -1"
|
|
22
22
|
:disabled="option.disabled || null"
|
|
23
|
+
:aria-controls="controls || null"
|
|
23
24
|
:data-value="option.key"
|
|
24
25
|
@click="select(option)">
|
|
25
26
|
<!-- @slot option Replaces the content of one option. -->
|
|
@@ -95,6 +96,17 @@ export default {
|
|
|
95
96
|
default: '',
|
|
96
97
|
},
|
|
97
98
|
|
|
99
|
+
/**
|
|
100
|
+
* Id of the element the options switch, such as the region holding a
|
|
101
|
+
* page view's widgets. Set as `aria-controls` on every option.
|
|
102
|
+
*
|
|
103
|
+
* @spec openspec/changes/view-switch-containers/specs/view-switch-containers/spec.md#requirement-the-view-switch-is-accessible
|
|
104
|
+
*/
|
|
105
|
+
controls: {
|
|
106
|
+
type: String,
|
|
107
|
+
default: '',
|
|
108
|
+
},
|
|
109
|
+
|
|
98
110
|
/** Stretch the control to the full width, with options of equal width. */
|
|
99
111
|
stretch: {
|
|
100
112
|
type: Boolean,
|
|
@@ -199,13 +199,12 @@
|
|
|
199
199
|
|
|
200
200
|
<script>
|
|
201
201
|
import { getCurrentUser } from '@nextcloud/auth'
|
|
202
|
-
import { loadState } from '@nextcloud/initial-state'
|
|
203
202
|
import { translate as t } from '@nextcloud/l10n'
|
|
204
203
|
import { NcButton, NcCheckboxRadioSwitch, NcLoadingIcon, NcNoteCard, NcSelect, NcTextField } from '@nextcloud/vue'
|
|
205
204
|
import CnChoiceCards from '../CnChoiceCards/CnChoiceCards.vue'
|
|
206
205
|
import CnLeafDependencySettings from '../CnLeafDependencySettings/CnLeafDependencySettings.vue'
|
|
207
206
|
import CnWizardDialog from '../CnWizardDialog/CnWizardDialog.vue'
|
|
208
|
-
import { checkDependencies, isDependencyResolved } from '../../composables/useDependencyCheck.js'
|
|
207
|
+
import { checkDependencies, isDependencyResolved, missingRequiredApps, readServerAppStatuses } from '../../composables/useDependencyCheck.js'
|
|
209
208
|
import { useSetupStatus } from '../../composables/useSetupStatus.js'
|
|
210
209
|
import { fieldsFromSchema } from '../../utils/schema.js'
|
|
211
210
|
|
|
@@ -474,16 +473,15 @@ export default {
|
|
|
474
473
|
*
|
|
475
474
|
* @return {Array<{ step: object, missing: Array<string> }>}
|
|
476
475
|
* @spec openspec/changes/setup-wizard-card-load-and-dependency-gate/specs/cn-setup-wizard/spec.md#requirement-the-wizard-checks-dependencies-before-any-step
|
|
476
|
+
* @spec openspec/changes/optional-step-requires/specs/cn-setup-wizard/spec.md#requirement-a-step-whose-required-apps-are-absent-is-not-applicable
|
|
477
477
|
*/
|
|
478
478
|
skippedSteps() {
|
|
479
479
|
return this.declaredSteps
|
|
480
480
|
.filter((s) => Array.isArray(s.requires) && s.requires.length > 0)
|
|
481
481
|
.map((step) => {
|
|
482
482
|
const known = Object.fromEntries(this.dependencyRows.map((d) => [d.id, d]))
|
|
483
|
-
const missing =
|
|
484
|
-
.map((row) => known[row.id] || row)
|
|
485
|
-
.filter((row) => !isDependencyResolved(row))
|
|
486
|
-
.map((row) => row.name || row.id)
|
|
483
|
+
const missing = missingRequiredApps(step.requires, this.serverAppStatuses)
|
|
484
|
+
.map((row) => (known[row.id] || row).name || row.id)
|
|
487
485
|
return { step, missing }
|
|
488
486
|
})
|
|
489
487
|
.filter((entry) => entry.missing.length > 0)
|
|
@@ -523,11 +521,7 @@ export default {
|
|
|
523
521
|
* @return {object}
|
|
524
522
|
*/
|
|
525
523
|
serverAppStatuses() {
|
|
526
|
-
|
|
527
|
-
return loadState(this.appId, 'dependency_statuses', {}) || {}
|
|
528
|
-
} catch {
|
|
529
|
-
return {}
|
|
530
|
-
}
|
|
524
|
+
return readServerAppStatuses(this.appId)
|
|
531
525
|
},
|
|
532
526
|
|
|
533
527
|
/**
|
|
@@ -1489,7 +1489,16 @@ export default {
|
|
|
1489
1489
|
white-space: nowrap;
|
|
1490
1490
|
}
|
|
1491
1491
|
|
|
1492
|
+
/* A clickable label is a <button>, and Nextcloud's core stylesheet gives
|
|
1493
|
+
every `button:not(.button-vue)` a min-height of the clickable area (34 to
|
|
1494
|
+
44px) with the text centred in it. That pushed each reachable stage's label
|
|
1495
|
+
about 12px below the current one, which is a <span>. Zeroed here so all
|
|
1496
|
+
labels sit 8px under their bar, as the board draws them. The visually
|
|
1497
|
+
hidden "(current step)" is absolutely positioned and takes no space. */
|
|
1492
1498
|
button.cn-stages-widget__bar-label {
|
|
1499
|
+
min-height: 0;
|
|
1500
|
+
height: auto;
|
|
1501
|
+
line-height: inherit;
|
|
1493
1502
|
cursor: pointer;
|
|
1494
1503
|
}
|
|
1495
1504
|
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
]"
|
|
26
26
|
v-bind="linkAttrs">
|
|
27
27
|
<div
|
|
28
|
-
v-if="resolvedIcon"
|
|
28
|
+
v-if="resolvedIcon && !isStacked"
|
|
29
29
|
class="cn-kpi-card__icon cn-stat-widget__icon"
|
|
30
30
|
:style="iconCircleStyle">
|
|
31
31
|
<CnWidgetIcon :name="resolvedIcon" :size="24" />
|
|
@@ -95,13 +95,22 @@
|
|
|
95
95
|
</span>
|
|
96
96
|
</template>
|
|
97
97
|
<span
|
|
98
|
-
v-if="!displayLoading && !displayError && resolvedCaption"
|
|
98
|
+
v-if="!isStacked && !displayLoading && !displayError && resolvedCaption"
|
|
99
99
|
class="cn-kpi-card__label cn-stat-widget__caption"
|
|
100
100
|
:class="captionVariantClass"
|
|
101
101
|
data-testid="cn-stat-widget-caption">
|
|
102
102
|
{{ resolvedCaption }}
|
|
103
103
|
</span>
|
|
104
104
|
</div>
|
|
105
|
+
<!-- Stacked (`content.layout: "stacked"`): the caption is a line of
|
|
106
|
+
its own under the number, as the board's "3 new this week". -->
|
|
107
|
+
<span
|
|
108
|
+
v-if="isStacked && !displayLoading && !displayError && resolvedCaption"
|
|
109
|
+
class="cn-kpi-card__label cn-stat-widget__caption cn-kpi-card__caption-line"
|
|
110
|
+
:class="captionVariantClass"
|
|
111
|
+
data-testid="cn-stat-widget-caption">
|
|
112
|
+
{{ resolvedCaption }}
|
|
113
|
+
</span>
|
|
105
114
|
</div>
|
|
106
115
|
</component>
|
|
107
116
|
</template>
|
|
@@ -1242,12 +1251,31 @@ export default {
|
|
|
1242
1251
|
/**
|
|
1243
1252
|
* Card orientation. Horizontal (icon beside the number) is the
|
|
1244
1253
|
* canonical KPI card; `content.layout: 'vertical'` stacks the icon
|
|
1245
|
-
* above a centred number for a tile taller than it is wide
|
|
1254
|
+
* above a centred number for a tile taller than it is wide;
|
|
1255
|
+
* `content.layout: 'stacked'` is the board look (see `isStacked`).
|
|
1246
1256
|
*
|
|
1247
|
-
* @
|
|
1257
|
+
* @spec openspec/changes/zuiddrecht-pixel-gaps-2/specs/zuiddrecht-pixel-gaps-2/spec.md#requirement-a-stat-tile-can-take-the-stacked-board-look
|
|
1258
|
+
* @return {'horizontal'|'vertical'|'stacked'}
|
|
1248
1259
|
*/
|
|
1249
1260
|
cardLayout() {
|
|
1250
|
-
|
|
1261
|
+
const layout = (this.content || {}).layout
|
|
1262
|
+
if (layout === 'vertical' || layout === 'stacked') {
|
|
1263
|
+
return layout
|
|
1264
|
+
}
|
|
1265
|
+
return 'horizontal'
|
|
1266
|
+
},
|
|
1267
|
+
|
|
1268
|
+
/**
|
|
1269
|
+
* Whether the tile takes the stacked board look (`content.layout:
|
|
1270
|
+
* "stacked"`): a plain muted label, the number at 34px in the text
|
|
1271
|
+
* colour, the caption on a line of its own, and no icon circle. Off by
|
|
1272
|
+
* default.
|
|
1273
|
+
*
|
|
1274
|
+
* @spec openspec/changes/zuiddrecht-pixel-gaps-2/specs/zuiddrecht-pixel-gaps-2/spec.md#requirement-a-stat-tile-can-take-the-stacked-board-look
|
|
1275
|
+
* @return {boolean}
|
|
1276
|
+
*/
|
|
1277
|
+
isStacked() {
|
|
1278
|
+
return this.cardLayout === 'stacked'
|
|
1251
1279
|
},
|
|
1252
1280
|
|
|
1253
1281
|
/**
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* SPDX-License-Identifier: EUPL-1.2
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { loadState } from '@nextcloud/initial-state'
|
|
6
7
|
import { useAppStatus } from './useAppStatus.js'
|
|
7
8
|
|
|
8
9
|
/**
|
|
@@ -59,3 +60,45 @@ export function checkDependencies(entries, serverStatuses = {}) {
|
|
|
59
60
|
export function isDependencyResolved(row) {
|
|
60
61
|
return !!row && row.installed === true && row.enabled === true
|
|
61
62
|
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Read the `dependency_statuses` initial state an app may inject from PHP.
|
|
66
|
+
*
|
|
67
|
+
* Keyed by app id: `{ installed, enabled }`. Returns `{}` when the app injects
|
|
68
|
+
* none or the state cannot be read, so callers fall back to `useAppStatus`.
|
|
69
|
+
*
|
|
70
|
+
* @param {string} appId The app whose initial state to read.
|
|
71
|
+
* @return {object} Server-reported statuses keyed by app id.
|
|
72
|
+
*
|
|
73
|
+
* @spec openspec/changes/optional-step-requires/specs/cn-setup-wizard/spec.md#requirement-a-step-whose-required-apps-are-absent-is-not-applicable
|
|
74
|
+
*/
|
|
75
|
+
export function readServerAppStatuses(appId) {
|
|
76
|
+
try {
|
|
77
|
+
const statuses = loadState(appId, 'dependency_statuses', {})
|
|
78
|
+
return (statuses && typeof statuses === 'object') ? statuses : {}
|
|
79
|
+
} catch {
|
|
80
|
+
return {}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* The apps in a setup step's `requires` list that are not installed and
|
|
86
|
+
* enabled.
|
|
87
|
+
*
|
|
88
|
+
* This is the one resolution every setup surface uses: `CnSetupWizard` skips a
|
|
89
|
+
* step when it is non-empty, and `useSetupStatus` then counts that step as not
|
|
90
|
+
* applicable rather than unmet. Keeping both on one function is what stops the
|
|
91
|
+
* wizard and the status from disagreeing about the same step.
|
|
92
|
+
*
|
|
93
|
+
* @param {Array<string>} requires The step's `requires` app ids.
|
|
94
|
+
* @param {object} [serverStatuses] Server-reported statuses keyed by app id.
|
|
95
|
+
* @return {Array<{id: string, name: string, required: boolean, installed: boolean, enabled: boolean}>} The missing apps.
|
|
96
|
+
*
|
|
97
|
+
* @spec openspec/changes/optional-step-requires/specs/cn-setup-wizard/spec.md#requirement-a-step-whose-required-apps-are-absent-is-not-applicable
|
|
98
|
+
*/
|
|
99
|
+
export function missingRequiredApps(requires, serverStatuses = {}) {
|
|
100
|
+
if (!Array.isArray(requires) || requires.length === 0) {
|
|
101
|
+
return []
|
|
102
|
+
}
|
|
103
|
+
return checkDependencies(requires, serverStatuses).filter((row) => !isDependencyResolved(row))
|
|
104
|
+
}
|
|
@@ -94,6 +94,13 @@ export function useListView(objectTypeOrOptions, options) {
|
|
|
94
94
|
// field (a folder pane grouping by it, say) reads them here rather than
|
|
95
95
|
// deriving them from `objects`, which is only the current page.
|
|
96
96
|
const facets = computed(() => objectStore.facets[objectType] || {})
|
|
97
|
+
// Outcome of the latest `refresh()`: the store's error for it, or null once
|
|
98
|
+
// it succeeded or a later fetch stored new rows. The store keeps the previous
|
|
99
|
+
// rows on a failed fetch, so this is how a consumer tells stale rows from
|
|
100
|
+
// current ones.
|
|
101
|
+
const error = ref(null)
|
|
102
|
+
let refreshSeq = 0
|
|
103
|
+
let pendingRefreshes = 0
|
|
97
104
|
|
|
98
105
|
let searchTimeout = null
|
|
99
106
|
|
|
@@ -178,14 +185,48 @@ export function useListView(objectTypeOrOptions, options) {
|
|
|
178
185
|
|
|
179
186
|
/**
|
|
180
187
|
* Fetch the collection using current state params and update sidebar facet data.
|
|
188
|
+
* Only the most recently started refresh sets `error`, so an older response
|
|
189
|
+
* settling late cannot override a newer one's outcome.
|
|
181
190
|
*
|
|
182
191
|
* @param {number} [page] Page to fetch
|
|
183
192
|
* @return {Promise<void>}
|
|
184
193
|
*/
|
|
185
194
|
async function refresh(page = 1) {
|
|
186
|
-
|
|
195
|
+
const seq = ++refreshSeq
|
|
196
|
+
// This call's own result. `errors[objectType]` is shared by every call
|
|
197
|
+
// for the type and is not cleared on success, so it cannot say which
|
|
198
|
+
// request failed; it is only the fallback for a store that ignores
|
|
199
|
+
// `options.outcome`.
|
|
200
|
+
const outcome = {}
|
|
201
|
+
pendingRefreshes++
|
|
202
|
+
try {
|
|
203
|
+
await objectStore.fetchCollection(objectType, buildParams(page), { outcome })
|
|
204
|
+
} catch (e) {
|
|
205
|
+
if (seq === refreshSeq) {
|
|
206
|
+
error.value = e
|
|
207
|
+
}
|
|
208
|
+
throw e
|
|
209
|
+
} finally {
|
|
210
|
+
pendingRefreshes--
|
|
211
|
+
}
|
|
212
|
+
if (seq === refreshSeq) {
|
|
213
|
+
error.value = ('error' in outcome) ? outcome.error : (objectStore.errors?.[objectType] || null)
|
|
214
|
+
}
|
|
187
215
|
}
|
|
188
216
|
|
|
217
|
+
// A fetch outside `refresh()` (e.g. the live-updates refetch) that stores
|
|
218
|
+
// new rows recovers the list from an earlier failure. Rows written while
|
|
219
|
+
// a refresh is pending are left to that refresh, which decides `error`.
|
|
220
|
+
watch(
|
|
221
|
+
() => objectStore.collections[objectType],
|
|
222
|
+
() => {
|
|
223
|
+
if (pendingRefreshes === 0) {
|
|
224
|
+
error.value = null
|
|
225
|
+
}
|
|
226
|
+
},
|
|
227
|
+
{ flush: 'sync' },
|
|
228
|
+
)
|
|
229
|
+
|
|
189
230
|
// ── Event handlers ───────────────────────────────────────────────────
|
|
190
231
|
|
|
191
232
|
/**
|
|
@@ -323,6 +364,7 @@ export function useListView(objectTypeOrOptions, options) {
|
|
|
323
364
|
loading,
|
|
324
365
|
pagination,
|
|
325
366
|
facets,
|
|
367
|
+
error,
|
|
326
368
|
// Local state
|
|
327
369
|
searchTerm,
|
|
328
370
|
sortKey,
|