@conduction/nextcloud-vue 2.56.0-beta.8 → 2.56.0-beta.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esm/components/CnAppNav/CnAppNav.vue2.js +128 -0
- package/dist/esm/components/CnAppNav/CnAppNav.vue2.js.map +1 -1
- package/dist/esm/components/CnAppNav/CnAppNav.vue3.js.map +1 -1
- package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js +79 -2
- package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue3.js +38 -25
- package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnBrandStripe/CnBrandStripe.vue2.js +21 -1
- package/dist/esm/components/CnBrandStripe/CnBrandStripe.vue2.js.map +1 -1
- package/dist/esm/components/CnBrandStripe/CnBrandStripe.vue3.js +4 -1
- package/dist/esm/components/CnBrandStripe/CnBrandStripe.vue3.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +30 -6
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +40 -15
- package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +2 -2
- package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
- package/dist/esm/components/CnLinkCardsPage/CnLinkCardsPage.vue2.js.map +1 -1
- package/dist/esm/components/CnLinkCardsPage/CnLinkCardsPage.vue3.js.map +1 -1
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js +53 -0
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js +22 -5
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js.map +1 -1
- package/dist/esm/components/CnReportsPage/CnReportsPage.vue2.js.map +1 -1
- package/dist/esm/components/CnReportsPage/CnReportsPage.vue3.js.map +1 -1
- package/dist/esm/components/CnStorePage/CnStorePage.vue2.js.map +1 -1
- package/dist/esm/components/CnStorePage/CnStorePage.vue3.js.map +1 -1
- package/dist/esm/components/CnWikiPage/CnWikiPage.vue2.js.map +1 -1
- package/dist/esm/components/CnWikiPage/CnWikiPage.vue3.js.map +1 -1
- package/dist/esm/l10n/en.json.js +5 -0
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +5 -0
- package/dist/esm/l10n/nl.json.js.map +1 -1
- package/dist/esm/nextcloud-vue.css +108 -9
- package/dist/esm/utils/validateManifestV2.compiled.js +1 -1
- package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
- package/dist/nextcloud-vue.cjs.js +857 -490
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +108 -9
- package/l10n/en.json +5 -0
- package/l10n/nl.json +5 -0
- package/package.json +3 -2
- package/scripts/manifest-schema-hash.js +41 -0
- package/scripts/record-manifest-schema-hash.js +52 -0
- package/src/components/CnAppNav/CnAppNav.vue +128 -0
- package/src/components/CnBannerWidget/CnBannerWidget.vue +113 -2
- package/src/components/CnBrandStripe/CnBrandStripe.md +16 -0
- package/src/components/CnBrandStripe/CnBrandStripe.vue +58 -5
- package/src/components/CnDashboardPage/CnDashboardPage.vue +30 -6
- package/src/components/CnDataTable/CnDataTable.md +1 -1
- package/src/components/CnDataTable/CnDataTable.vue +42 -17
- package/src/components/CnLinkCardsPage/CnLinkCardsPage.vue +35 -2
- package/src/components/CnObjectSidebar/CnFilesTab.vue +65 -0
- package/src/components/CnReportsPage/CnReportsPage.vue +7 -0
- package/src/components/CnStorePage/CnStorePage.vue +7 -0
- package/src/components/CnWikiPage/CnWikiPage.vue +7 -0
- package/src/css/table.css +1 -2
- package/src/public/index.js +5 -0
- package/src/schemas/app-manifest-v2.schema.json +1 -1
- package/src/utils/validateManifestV2.compiled.js +1 -1
|
@@ -36,6 +36,16 @@
|
|
|
36
36
|
</component>
|
|
37
37
|
</div>
|
|
38
38
|
</section>
|
|
39
|
+
<!-- The count could not be read. One quiet line, not the card and not an
|
|
40
|
+
alarm: hidden would read as "nothing to report", which is not known. -->
|
|
41
|
+
<p
|
|
42
|
+
v-else-if="checkFailed"
|
|
43
|
+
class="cn-banner-widget cn-banner-widget--unchecked"
|
|
44
|
+
:title="failureTooltip"
|
|
45
|
+
data-testid="cn-banner-widget-unchecked">
|
|
46
|
+
<span class="cn-banner-widget__unchecked-text">{{ uncheckedText }}</span>
|
|
47
|
+
<span class="cn-banner-widget__unchecked-reason">{{ failureTooltip }}</span>
|
|
48
|
+
</p>
|
|
39
49
|
<div v-else-if="visible" class="cn-banner-widget">
|
|
40
50
|
<NcNoteCard :type="resolvedVariant" class="cn-banner-widget__card">
|
|
41
51
|
<component
|
|
@@ -52,6 +62,7 @@
|
|
|
52
62
|
</template>
|
|
53
63
|
|
|
54
64
|
<script>
|
|
65
|
+
import { translate as t } from '@nextcloud/l10n'
|
|
55
66
|
import { NcNoteCard } from '@nextcloud/vue'
|
|
56
67
|
import { followLinkClick, resolveHref } from '../../utils/linkNavigation.js'
|
|
57
68
|
import { safeHref } from '../../utils/safeHref.js'
|
|
@@ -97,6 +108,12 @@ const MAX_ACTIONS = 2
|
|
|
97
108
|
* condition evaluates true — a failed fetch never breaks (or spams) a
|
|
98
109
|
* dashboard.
|
|
99
110
|
*
|
|
111
|
+
* An attention card (`layout: "attention"`) whose `visibleWhen` request
|
|
112
|
+
* FAILS renders one quiet line instead, "Could not check", with the reason as
|
|
113
|
+
* a tooltip. A hidden card reads as "nothing needs attention", and a failed
|
|
114
|
+
* count does not know that. A request that succeeds with a value that does
|
|
115
|
+
* not meet the condition still renders nothing.
|
|
116
|
+
*
|
|
100
117
|
* `layout: "attention"` turns the banner into an attention card: a card with
|
|
101
118
|
* a coloured edge by severity, a `kicker` label, a `title`, a `reason` line
|
|
102
119
|
* and up to two `actions` (the first is the primary one unless an action says
|
|
@@ -238,7 +255,9 @@ export default {
|
|
|
238
255
|
* the banner renders from this verdict instead of fetching again —
|
|
239
256
|
* no duplicate request and no hidden-until-self-evaluated flash —
|
|
240
257
|
* and `value` feeds the `{value}` text placeholder. `null` (the
|
|
241
|
-
* default) keeps the banner self-evaluating.
|
|
258
|
+
* default) keeps the banner self-evaluating. A host whose request
|
|
259
|
+
* failed passes `{ met: false, value: null, failed: true, reason }`,
|
|
260
|
+
* and an attention card then says it could not check.
|
|
242
261
|
*
|
|
243
262
|
* @type {object|null}
|
|
244
263
|
*/
|
|
@@ -259,6 +278,13 @@ export default {
|
|
|
259
278
|
'action',
|
|
260
279
|
],
|
|
261
280
|
|
|
281
|
+
/**
|
|
282
|
+
* The banner's own state: the evaluated condition, the value it read,
|
|
283
|
+
* and whether the request for it failed.
|
|
284
|
+
*
|
|
285
|
+
* @return {object} The state.
|
|
286
|
+
* @spec openspec/changes/live-check-follow-ups/specs/live-check-follow-ups/spec.md#requirement-an-attention-card-says-when-it-could-not-check
|
|
287
|
+
*/
|
|
262
288
|
data() {
|
|
263
289
|
return {
|
|
264
290
|
/** Id that ties the attention card's region to its title. */
|
|
@@ -271,6 +297,10 @@ export default {
|
|
|
271
297
|
* the text wherever it says `{value}`.
|
|
272
298
|
*/
|
|
273
299
|
conditionValue: null,
|
|
300
|
+
/** Whether the visibleWhen request itself failed (not: answered "no"). */
|
|
301
|
+
conditionFailed: false,
|
|
302
|
+
/** Why it failed, as the request reported it. */
|
|
303
|
+
conditionFailureReason: '',
|
|
274
304
|
}
|
|
275
305
|
},
|
|
276
306
|
|
|
@@ -419,6 +449,52 @@ export default {
|
|
|
419
449
|
return this.conditionMet === true
|
|
420
450
|
},
|
|
421
451
|
|
|
452
|
+
/**
|
|
453
|
+
* Whether to say the check failed: an attention card, with words to
|
|
454
|
+
* show, whose `visibleWhen` request failed. A plain banner stays
|
|
455
|
+
* hidden on failure, as it always did.
|
|
456
|
+
*
|
|
457
|
+
* @return {boolean}
|
|
458
|
+
* @spec openspec/changes/live-check-follow-ups/specs/live-check-follow-ups/spec.md#requirement-an-attention-card-says-when-it-could-not-check
|
|
459
|
+
*/
|
|
460
|
+
checkFailed() {
|
|
461
|
+
const title = this.title || (this.content && this.content.title) || ''
|
|
462
|
+
return this.isAttention
|
|
463
|
+
&& !!this.resolvedVisibleWhen
|
|
464
|
+
&& this.conditionFailed
|
|
465
|
+
&& (title !== '' || this.resolvedText !== '')
|
|
466
|
+
},
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* The one line shown when the check failed. It names the card by its
|
|
470
|
+
* title, unless the title needs the value that could not be read.
|
|
471
|
+
*
|
|
472
|
+
* @return {string}
|
|
473
|
+
* @spec openspec/changes/live-check-follow-ups/specs/live-check-follow-ups/spec.md#requirement-an-attention-card-says-when-it-could-not-check
|
|
474
|
+
*/
|
|
475
|
+
uncheckedText() {
|
|
476
|
+
const title = this.title || (this.content && this.content.title) || this.resolvedText
|
|
477
|
+
if (!title || title.includes('{value}')) {
|
|
478
|
+
return t('nextcloud-vue', 'Could not check')
|
|
479
|
+
}
|
|
480
|
+
// `escape: false`: the result is rendered through `{{ }}`, which escapes.
|
|
481
|
+
return t('nextcloud-vue', 'Could not check: {subject}', { subject: title }, undefined, { escape: false })
|
|
482
|
+
},
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* The reason the check failed, for the tooltip and for assistive
|
|
486
|
+
* technology.
|
|
487
|
+
*
|
|
488
|
+
* @return {string}
|
|
489
|
+
* @spec openspec/changes/live-check-follow-ups/specs/live-check-follow-ups/spec.md#requirement-an-attention-card-says-when-it-could-not-check
|
|
490
|
+
*/
|
|
491
|
+
failureTooltip() {
|
|
492
|
+
if (!this.conditionFailureReason) {
|
|
493
|
+
return t('nextcloud-vue', 'The check failed.')
|
|
494
|
+
}
|
|
495
|
+
return t('nextcloud-vue', 'The check failed: {reason}', { reason: this.conditionFailureReason }, undefined, { escape: false })
|
|
496
|
+
},
|
|
497
|
+
|
|
422
498
|
/** Whether the banner navigates on click (route set + router present). */
|
|
423
499
|
clickable() {
|
|
424
500
|
return !!this.resolvedRoute && !!this.$router
|
|
@@ -493,27 +569,36 @@ export default {
|
|
|
493
569
|
* modes — the Wave-1 banner shape is the canonical one, extracted to
|
|
494
570
|
* `utils/visibleWhen.js` in Wave 3 so manifest actions reuse it),
|
|
495
571
|
* keeping the read VALUE for the `{value}` text placeholder.
|
|
496
|
-
* Fail-safe: any fetch/shape error leaves the banner hidden.
|
|
572
|
+
* Fail-safe: any fetch/shape error leaves the banner hidden. The
|
|
573
|
+
* failure is kept (`conditionFailed`, with its reason) so an
|
|
574
|
+
* attention card can say it could not check.
|
|
497
575
|
*
|
|
498
576
|
* @return {Promise<void>}
|
|
577
|
+
* @spec openspec/changes/live-check-follow-ups/specs/live-check-follow-ups/spec.md#requirement-an-attention-card-says-when-it-could-not-check
|
|
499
578
|
*/
|
|
500
579
|
async evaluateCondition() {
|
|
501
580
|
const outcome = this.resolvedConditionOutcome
|
|
502
581
|
if (outcome) {
|
|
503
582
|
this.conditionMet = outcome.met === true
|
|
504
583
|
this.conditionValue = outcome.value !== undefined ? outcome.value : null
|
|
584
|
+
this.conditionFailed = outcome.failed === true
|
|
585
|
+
this.conditionFailureReason = outcome.failed === true && typeof outcome.reason === 'string' ? outcome.reason : ''
|
|
505
586
|
return
|
|
506
587
|
}
|
|
507
588
|
const cond = this.resolvedVisibleWhen
|
|
508
589
|
if (!cond) {
|
|
509
590
|
this.conditionMet = null
|
|
510
591
|
this.conditionValue = null
|
|
592
|
+
this.conditionFailed = false
|
|
593
|
+
this.conditionFailureReason = ''
|
|
511
594
|
return
|
|
512
595
|
}
|
|
513
596
|
try {
|
|
514
597
|
const value = await readVisibleWhenValue(cond)
|
|
515
598
|
this.conditionValue = value
|
|
516
599
|
this.conditionMet = compareVisibleWhen(value, cond.op || 'eq', cond.value)
|
|
600
|
+
this.conditionFailed = false
|
|
601
|
+
this.conditionFailureReason = ''
|
|
517
602
|
} catch (error) {
|
|
518
603
|
// Still hidden: a banner that cannot tell whether it applies
|
|
519
604
|
// should not claim attention. But not silently, because a
|
|
@@ -522,6 +607,8 @@ export default {
|
|
|
522
607
|
console.warn('[CnBannerWidget] visibleWhen could not be evaluated, banner hidden:', error?.message || error)
|
|
523
608
|
this.conditionMet = false
|
|
524
609
|
this.conditionValue = null
|
|
610
|
+
this.conditionFailed = true
|
|
611
|
+
this.conditionFailureReason = typeof error?.message === 'string' ? error.message : String(error ?? '')
|
|
525
612
|
}
|
|
526
613
|
},
|
|
527
614
|
|
|
@@ -557,6 +644,30 @@ export default {
|
|
|
557
644
|
color: inherit;
|
|
558
645
|
}
|
|
559
646
|
|
|
647
|
+
/* The check failed. Muted text, no fill, no severity colour: it is a note
|
|
648
|
+
that something is not known, not a warning. */
|
|
649
|
+
.cn-banner-widget--unchecked {
|
|
650
|
+
box-sizing: border-box;
|
|
651
|
+
margin: 0;
|
|
652
|
+
padding: 8px 12px;
|
|
653
|
+
color: var(--color-text-maxcontrast);
|
|
654
|
+
font-size: 0.9em;
|
|
655
|
+
cursor: help;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
/* The reason, for assistive technology; sighted readers get the tooltip. */
|
|
659
|
+
.cn-banner-widget__unchecked-reason {
|
|
660
|
+
position: absolute;
|
|
661
|
+
width: 1px;
|
|
662
|
+
height: 1px;
|
|
663
|
+
margin: -1px;
|
|
664
|
+
padding: 0;
|
|
665
|
+
overflow: hidden;
|
|
666
|
+
clip-path: inset(50%);
|
|
667
|
+
white-space: nowrap;
|
|
668
|
+
border: 0;
|
|
669
|
+
}
|
|
670
|
+
|
|
560
671
|
/* Attention card. The severity edge is an inset shadow, so it takes no width. */
|
|
561
672
|
.cn-banner-widget--attention {
|
|
562
673
|
--cn-banner-accent: var(--color-primary-element);
|
|
@@ -20,3 +20,19 @@ Down the side of a panel, with `orientation="vertical"`:
|
|
|
20
20
|
<span>Content beside the stripe</span>
|
|
21
21
|
</div>
|
|
22
22
|
```
|
|
23
|
+
|
|
24
|
+
A theme whose motif is not three bands names an image. Here the canal of a school: a blue line, its bank and the water:
|
|
25
|
+
|
|
26
|
+
```vue
|
|
27
|
+
<div style="--cn-brand-stripe-image: linear-gradient(to bottom, #1f4fd8 0 4px, transparent 4px 7px, #1fb5a8 7px 9px); --cn-brand-stripe-height: 9px">
|
|
28
|
+
<CnBrandStripe />
|
|
29
|
+
</div>
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
On a dark band, such as a footer, `variant="inverse"` draws the theme's `--cn-brand-stripe-image-inverse`, else the one image, else the bands:
|
|
33
|
+
|
|
34
|
+
```vue
|
|
35
|
+
<div style="background: #14338f; padding-bottom: 16px; --cn-brand-stripe-image: linear-gradient(to bottom, #1f4fd8 0 4px, transparent 4px 7px, #1fb5a8 7px 9px); --cn-brand-stripe-image-inverse: linear-gradient(to bottom, #1f4fd8 0 4px, #ffffff 4px 7px, #1fb5a8 7px 9px); --cn-brand-stripe-height: 9px">
|
|
36
|
+
<CnBrandStripe variant="inverse" />
|
|
37
|
+
</div>
|
|
38
|
+
```
|
|
@@ -5,7 +5,10 @@
|
|
|
5
5
|
<template>
|
|
6
6
|
<div
|
|
7
7
|
class="cn-brand-stripe"
|
|
8
|
-
:class="{
|
|
8
|
+
:class="{
|
|
9
|
+
'cn-brand-stripe--vertical': orientation === 'vertical',
|
|
10
|
+
'cn-brand-stripe--inverse': variant === 'inverse',
|
|
11
|
+
}"
|
|
9
12
|
aria-hidden="true"
|
|
10
13
|
data-testid="cn-brand-stripe">
|
|
11
14
|
<span class="cn-brand-stripe__band cn-brand-stripe__band--1" />
|
|
@@ -17,7 +20,8 @@
|
|
|
17
20
|
<script>
|
|
18
21
|
/**
|
|
19
22
|
* CnBrandStripe draws the organisation's brand stripe: up to three coloured
|
|
20
|
-
* bands next to each other,
|
|
23
|
+
* bands next to each other, or the organisation's own motif, for the top of a
|
|
24
|
+
* page, a card or a sidebar.
|
|
21
25
|
*
|
|
22
26
|
* The theme decides what it looks like, through the library's own custom
|
|
23
27
|
* properties: `--cn-brand-stripe-color-1/2/3` (the colours, start to end),
|
|
@@ -26,6 +30,12 @@
|
|
|
26
30
|
* tokens onto these; the library never reads a theme's tokens. Without them the
|
|
27
31
|
* stripe is one band in the Nextcloud primary colour, 4px high.
|
|
28
32
|
*
|
|
33
|
+
* A theme whose motif is not three bands (hanging twigs, a canal and its bank,
|
|
34
|
+
* a slanted cut) names it in `--cn-brand-stripe-image`, any background-image
|
|
35
|
+
* value; the stripe then draws that image in the same box instead of the
|
|
36
|
+
* bands. `--cn-brand-stripe-image-inverse` is the same motif for a dark band;
|
|
37
|
+
* the `inverse` variant draws it, else the one image, else the bands.
|
|
38
|
+
*
|
|
29
39
|
* The stripe is decoration: it carries no meaning and is hidden from
|
|
30
40
|
* assistive technology.
|
|
31
41
|
*
|
|
@@ -49,6 +59,19 @@ export default {
|
|
|
49
59
|
default: 'horizontal',
|
|
50
60
|
validator: (v) => ['horizontal', 'vertical'].includes(v),
|
|
51
61
|
},
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The ground the stripe sits on: `default` (a light surface) or
|
|
65
|
+
* `inverse` (a dark band such as a footer), which draws the theme's
|
|
66
|
+
* `--cn-brand-stripe-image-inverse` when it names one.
|
|
67
|
+
*
|
|
68
|
+
* @type {'default'|'inverse'}
|
|
69
|
+
*/
|
|
70
|
+
variant: {
|
|
71
|
+
type: String,
|
|
72
|
+
default: 'default',
|
|
73
|
+
validator: (v) => ['default', 'inverse'].includes(v),
|
|
74
|
+
},
|
|
52
75
|
},
|
|
53
76
|
}
|
|
54
77
|
</script>
|
|
@@ -56,19 +79,52 @@ export default {
|
|
|
56
79
|
<!--
|
|
57
80
|
The stripe reads the library's own `--cn-brand-stripe-*` properties, each
|
|
58
81
|
with a Nextcloud fallback. A theming app sets them from its own tokens.
|
|
82
|
+
|
|
83
|
+
THE ROOT PAINTS, NOT THE BANDS. The three bands are one background on the
|
|
84
|
+
root, a gradient with hard stops computed from the ratios, so a theme's
|
|
85
|
+
motif image can take its place with one `var()` fallback. A band painted on
|
|
86
|
+
its own element could not be switched off by the presence of an image: CSS
|
|
87
|
+
cannot ask whether a custom property is set. The band elements stay, empty,
|
|
88
|
+
so markup and tests that count them keep working.
|
|
59
89
|
-->
|
|
60
90
|
<style scoped>
|
|
61
91
|
.cn-brand-stripe {
|
|
92
|
+
--cn-brand-stripe-first: var(--cn-brand-stripe-ratio-1, 1);
|
|
93
|
+
--cn-brand-stripe-second: var(--cn-brand-stripe-ratio-2, 1);
|
|
94
|
+
--cn-brand-stripe-total: calc(
|
|
95
|
+
var(--cn-brand-stripe-first) + var(--cn-brand-stripe-second) + var(--cn-brand-stripe-ratio-3, 1)
|
|
96
|
+
);
|
|
97
|
+
--cn-brand-stripe-stop-1: calc(var(--cn-brand-stripe-first) / var(--cn-brand-stripe-total) * 100%);
|
|
98
|
+
--cn-brand-stripe-stop-2: calc(
|
|
99
|
+
(var(--cn-brand-stripe-first) + var(--cn-brand-stripe-second)) / var(--cn-brand-stripe-total) * 100%
|
|
100
|
+
);
|
|
101
|
+
--cn-brand-stripe-direction: to right;
|
|
102
|
+
--cn-brand-stripe-bands: linear-gradient(
|
|
103
|
+
var(--cn-brand-stripe-direction),
|
|
104
|
+
var(--cn-brand-stripe-color-1, var(--color-primary-element)) 0 var(--cn-brand-stripe-stop-1),
|
|
105
|
+
var(--cn-brand-stripe-color-2, var(--color-primary-element)) var(--cn-brand-stripe-stop-1) var(--cn-brand-stripe-stop-2),
|
|
106
|
+
var(--cn-brand-stripe-color-3, var(--color-primary-element)) var(--cn-brand-stripe-stop-2) 100%
|
|
107
|
+
);
|
|
62
108
|
display: flex;
|
|
63
109
|
flex: none;
|
|
64
110
|
width: 100%;
|
|
65
111
|
height: var(--cn-brand-stripe-height, 4px);
|
|
112
|
+
background-image: var(--cn-brand-stripe-image, var(--cn-brand-stripe-bands));
|
|
113
|
+
background-repeat: repeat-x;
|
|
114
|
+
background-size: auto 100%;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.cn-brand-stripe--inverse {
|
|
118
|
+
background-image: var(--cn-brand-stripe-image-inverse, var(--cn-brand-stripe-image, var(--cn-brand-stripe-bands)));
|
|
66
119
|
}
|
|
67
120
|
|
|
68
121
|
.cn-brand-stripe--vertical {
|
|
122
|
+
--cn-brand-stripe-direction: to bottom;
|
|
69
123
|
flex-direction: column;
|
|
70
124
|
width: var(--cn-brand-stripe-height, 4px);
|
|
71
125
|
height: 100%;
|
|
126
|
+
background-repeat: repeat-y;
|
|
127
|
+
background-size: 100% auto;
|
|
72
128
|
}
|
|
73
129
|
|
|
74
130
|
.cn-brand-stripe__band {
|
|
@@ -78,17 +134,14 @@ export default {
|
|
|
78
134
|
|
|
79
135
|
.cn-brand-stripe__band--1 {
|
|
80
136
|
flex-grow: var(--cn-brand-stripe-ratio-1, 1);
|
|
81
|
-
background-color: var(--cn-brand-stripe-color-1, var(--color-primary-element));
|
|
82
137
|
}
|
|
83
138
|
|
|
84
139
|
.cn-brand-stripe__band--2 {
|
|
85
140
|
flex-grow: var(--cn-brand-stripe-ratio-2, 1);
|
|
86
|
-
background-color: var(--cn-brand-stripe-color-2, var(--color-primary-element));
|
|
87
141
|
}
|
|
88
142
|
|
|
89
143
|
.cn-brand-stripe__band--3 {
|
|
90
144
|
flex-grow: var(--cn-brand-stripe-ratio-3, 1);
|
|
91
|
-
background-color: var(--cn-brand-stripe-color-3, var(--color-primary-element));
|
|
92
145
|
}
|
|
93
146
|
|
|
94
147
|
@media print {
|
|
@@ -2752,8 +2752,12 @@ export default {
|
|
|
2752
2752
|
* which for an attention card is its `title` when it has no `text`.
|
|
2753
2753
|
*
|
|
2754
2754
|
* @param {object} def Widget definition.
|
|
2755
|
+
* `layout` is returned so the page can tell an attention card from a
|
|
2756
|
+
* plain banner: only the former keeps its cell when its check fails.
|
|
2757
|
+
*
|
|
2755
2758
|
* @spec openspec/changes/link-cards-page/specs/link-cards-page/spec.md#requirement-an-attention-card-with-a-title-keeps-its-cell
|
|
2756
|
-
* @
|
|
2759
|
+
* @spec openspec/changes/live-check-follow-ups/specs/live-check-follow-ups/spec.md#requirement-an-attention-card-says-when-it-could-not-check
|
|
2760
|
+
* @return {{ text: string, visibleWhen: (object|null), layout: string }}
|
|
2757
2761
|
*/
|
|
2758
2762
|
widgetDisplayConfig(def) {
|
|
2759
2763
|
const content = (def && typeof def.content === 'object' && def.content) || {}
|
|
@@ -2767,6 +2771,7 @@ export default {
|
|
|
2767
2771
|
return {
|
|
2768
2772
|
text: content.text || props.text || title || '',
|
|
2769
2773
|
visibleWhen: (def && def.visibleWhen) || content.visibleWhen || props.visibleWhen || null,
|
|
2774
|
+
layout,
|
|
2770
2775
|
}
|
|
2771
2776
|
},
|
|
2772
2777
|
|
|
@@ -2776,15 +2781,22 @@ export default {
|
|
|
2776
2781
|
* text at all — and must therefore surrender its grid cell in live
|
|
2777
2782
|
* mode instead of leaving an empty card behind.
|
|
2778
2783
|
*
|
|
2784
|
+
* One exception: an attention card whose `visibleWhen` request FAILED
|
|
2785
|
+
* keeps its cell, so the card can say it could not check. A collapsed
|
|
2786
|
+
* cell reads as "nothing needs attention", which a failed count does
|
|
2787
|
+
* not know. A count that was read and does not meet the condition
|
|
2788
|
+
* still collapses.
|
|
2789
|
+
*
|
|
2779
2790
|
* @param {object} item Layout item.
|
|
2780
2791
|
* @return {boolean} true when the cell must collapse.
|
|
2792
|
+
* @spec openspec/changes/live-check-follow-ups/specs/live-check-follow-ups/spec.md#requirement-an-attention-card-says-when-it-could-not-check
|
|
2781
2793
|
*/
|
|
2782
2794
|
isCollapsedWidget(item) {
|
|
2783
2795
|
const def = this.getWidgetDef(item.widgetId)
|
|
2784
2796
|
if (!def || !def.type) {
|
|
2785
2797
|
return false
|
|
2786
2798
|
}
|
|
2787
|
-
const { text, visibleWhen } = this.widgetDisplayConfig(def)
|
|
2799
|
+
const { text, visibleWhen, layout } = this.widgetDisplayConfig(def)
|
|
2788
2800
|
if (this.isBannerDef(def) && text === '') {
|
|
2789
2801
|
return true
|
|
2790
2802
|
}
|
|
@@ -2792,6 +2804,9 @@ export default {
|
|
|
2792
2804
|
return false
|
|
2793
2805
|
}
|
|
2794
2806
|
const outcome = this.widgetConditionOutcome[item.widgetId]
|
|
2807
|
+
if (outcome && outcome.failed === true && layout === 'attention' && this.isBannerDef(def)) {
|
|
2808
|
+
return false
|
|
2809
|
+
}
|
|
2795
2810
|
return !outcome || outcome.met !== true
|
|
2796
2811
|
},
|
|
2797
2812
|
|
|
@@ -2802,13 +2817,17 @@ export default {
|
|
|
2802
2817
|
* Runs on created() and again whenever `widgets` changes (the
|
|
2803
2818
|
* in-app editor mutates defs in place). Fail-safe like the banner
|
|
2804
2819
|
* itself: any fetch/shape error counts as "not met", so a broken
|
|
2805
|
-
* predicate collapses the cell rather than breaking the page.
|
|
2820
|
+
* predicate collapses the cell rather than breaking the page. The
|
|
2821
|
+
* outcome of a failed read carries `failed: true` and the `reason`,
|
|
2822
|
+
* which `isCollapsedWidget` and CnBannerWidget use to show an
|
|
2823
|
+
* attention card's "Could not check" line instead of nothing.
|
|
2806
2824
|
*
|
|
2807
2825
|
* Flips `widgetConditionsSettled` when the initial round is done —
|
|
2808
2826
|
* synchronously when there is nothing to evaluate, so a dashboard
|
|
2809
2827
|
* without conditional widgets never waits.
|
|
2810
2828
|
*
|
|
2811
2829
|
* @return {Promise<void>}
|
|
2830
|
+
* @spec openspec/changes/live-check-follow-ups/specs/live-check-follow-ups/spec.md#requirement-an-attention-card-says-when-it-could-not-check
|
|
2812
2831
|
*/
|
|
2813
2832
|
async evaluateWidgetConditions() {
|
|
2814
2833
|
const conditional = (this.widgets || []).filter((def) => def && def.id && def.type
|
|
@@ -2830,12 +2849,17 @@ export default {
|
|
|
2830
2849
|
}
|
|
2831
2850
|
await Promise.all(conditional.map(async (def) => {
|
|
2832
2851
|
const cond = this.widgetDisplayConfig(def).visibleWhen
|
|
2833
|
-
let outcome
|
|
2852
|
+
let outcome
|
|
2834
2853
|
try {
|
|
2835
2854
|
const value = await readVisibleWhenValue(cond)
|
|
2836
2855
|
outcome = { met: compareVisibleWhen(value, cond.op || 'eq', cond.value), value }
|
|
2837
|
-
} catch {
|
|
2838
|
-
//
|
|
2856
|
+
} catch (error) {
|
|
2857
|
+
// Fail-safe: not met. But the failure is kept, and said
|
|
2858
|
+
// out loud: a failed count and "nothing to report" look
|
|
2859
|
+
// the same on screen.
|
|
2860
|
+
outcome = { met: false, value: null, failed: true, reason: typeof error?.message === 'string' ? error.message : String(error ?? '') }
|
|
2861
|
+
// eslint-disable-next-line no-console
|
|
2862
|
+
console.warn(`[CnDashboardPage] visibleWhen of widget "${def.id}" could not be evaluated:`, error?.message || error)
|
|
2839
2863
|
}
|
|
2840
2864
|
// A newer run owns the map now — a stale verdict (possibly for
|
|
2841
2865
|
// a def that no longer exists) must not overwrite its writes.
|
|
@@ -186,7 +186,7 @@ export default {
|
|
|
186
186
|
| `includeColumns` | Array | `null` | Column keys to include (whitelist) in schema mode |
|
|
187
187
|
| `sortKey` | String | `null` | Currently active sort column key (controlled) |
|
|
188
188
|
| `sortOrder` | String | `'asc'` | Current sort direction: `'asc'` or `'desc'` |
|
|
189
|
-
| `sortKeys` | Array | `[]` | Ordered multi-column ("shift+click") sort key list, `[{ key, order }, …]` (0–3 entries); takes precedence over `sortKey`/`sortOrder` when non-empty |
|
|
189
|
+
| `sortKeys` | Array | `[]` | Ordered multi-column ("shift+click") sort key list, `[{ key, order }, …]` (0–3 entries); takes precedence over `sortKey`/`sortOrder` when non-empty. Priority badges show only when two or more rendered, sortable columns are sorted; a key whose column is not rendered or not sortable (such as a `_uuid` tie-break) still sorts, but is not counted or numbered |
|
|
190
190
|
| `selectedIds` | Array | `[]` | Array of selected row IDs (controlled) |
|
|
191
191
|
| `rowClass` | Function | `null` | `(row) => string` — CSS class(es) applied to each `<tr>` |
|
|
192
192
|
| `rowClickToView` | Boolean | `false` | Emit `row-click` on a row-body click even while `selectable` (selection then via the checkbox column only) |
|
|
@@ -104,9 +104,9 @@
|
|
|
104
104
|
{{ effectiveSortKeys[sortKeyIndex(col.key)].order === 'asc' ? '▲' : '▼' }}
|
|
105
105
|
</span>
|
|
106
106
|
<span
|
|
107
|
-
v-if="col.sortable &&
|
|
107
|
+
v-if="col.sortable && sortBadgeFor(col.key) !== null"
|
|
108
108
|
class="cn-table-sort-badge">
|
|
109
|
-
{{
|
|
109
|
+
{{ sortBadgeFor(col.key) }}
|
|
110
110
|
</span>
|
|
111
111
|
</th>
|
|
112
112
|
|
|
@@ -309,15 +309,12 @@ import '../../css/table.css'
|
|
|
309
309
|
* row selection, custom cell rendering via scoped slots, loading states,
|
|
310
310
|
* and empty states.
|
|
311
311
|
*
|
|
312
|
-
* Sorting: a plain click on a sortable header is single-sort (cycle asc →
|
|
313
|
-
*
|
|
314
|
-
*
|
|
315
|
-
*
|
|
316
|
-
*
|
|
317
|
-
*
|
|
318
|
-
* event payload is extended, not replaced: `{key, order}` still mirrors the
|
|
319
|
-
* primary key exactly as before; a new `keys` field carries the full
|
|
320
|
-
* ordered list. See `src/utils/multiColumnSort.js` for the state machine.
|
|
312
|
+
* Sorting: a plain click on a sortable header is single-sort (cycle asc → desc → cleared), unchanged from before.
|
|
313
|
+
* Shift+click (or Shift+Enter on a focused header) appends the column as a secondary/tertiary sort key, capped at 3, with numbered priority badges (1, 2, 3) once two or more rendered, sortable columns are sorted.
|
|
314
|
+
* A sort key whose column is not rendered, or not sortable, still sorts, but is not counted or numbered.
|
|
315
|
+
* Pass `sortKeys: [{key, order}, ...]` for multi-sort (falls back to the legacy `sortKey`/`sortOrder` props when empty).
|
|
316
|
+
* The `sort` event payload is extended, not replaced: `{key, order}` still mirrors the primary key exactly as before; a new `keys` field carries the full ordered list.
|
|
317
|
+
* See `src/utils/multiColumnSort.js` for the state machine.
|
|
321
318
|
*
|
|
322
319
|
* When a `schema` prop is provided, columns are auto-generated from schema
|
|
323
320
|
* properties and cells render through CnCellRenderer for type-aware formatting
|
|
@@ -918,6 +915,23 @@ export default {
|
|
|
918
915
|
return []
|
|
919
916
|
},
|
|
920
917
|
|
|
918
|
+
/**
|
|
919
|
+
* The active sort keys whose column renders a sort indicator, in priority order and deduplicated by first occurrence. Only these are counted and numbered by the priority badge.
|
|
920
|
+
*
|
|
921
|
+
* @return {Array<{key: string, order: 'asc'|'desc'}>}
|
|
922
|
+
*/
|
|
923
|
+
renderedSortKeys() {
|
|
924
|
+
const shown = new Set(this.effectiveColumns.filter((c) => c && c.sortable).map((c) => c.key))
|
|
925
|
+
const seen = new Set()
|
|
926
|
+
return this.effectiveSortKeys.filter((k) => {
|
|
927
|
+
if (!k || !shown.has(k.key) || seen.has(k.key)) {
|
|
928
|
+
return false
|
|
929
|
+
}
|
|
930
|
+
seen.add(k.key)
|
|
931
|
+
return true
|
|
932
|
+
})
|
|
933
|
+
},
|
|
934
|
+
|
|
921
935
|
totalColumns() {
|
|
922
936
|
let count = this.effectiveColumns.length
|
|
923
937
|
if (this.selectable) {
|
|
@@ -1513,7 +1527,7 @@ export default {
|
|
|
1513
1527
|
|
|
1514
1528
|
/**
|
|
1515
1529
|
* Index of `key` within `effectiveSortKeys`, or -1 when not active.
|
|
1516
|
-
* Used by the template for the arrow
|
|
1530
|
+
* Used by the template for the arrow.
|
|
1517
1531
|
*
|
|
1518
1532
|
* @param {string} key Column key.
|
|
1519
1533
|
* @return {number}
|
|
@@ -1523,11 +1537,22 @@ export default {
|
|
|
1523
1537
|
},
|
|
1524
1538
|
|
|
1525
1539
|
/**
|
|
1526
|
-
*
|
|
1527
|
-
*
|
|
1528
|
-
*
|
|
1529
|
-
*
|
|
1530
|
-
|
|
1540
|
+
* The 1-based priority badge for `key` among the rendered sort keys, or null when fewer than two rendered columns are sorted or `key` is not one of them.
|
|
1541
|
+
*
|
|
1542
|
+
* @param {string} key Column key.
|
|
1543
|
+
* @return {number|null}
|
|
1544
|
+
*/
|
|
1545
|
+
sortBadgeFor(key) {
|
|
1546
|
+
if (this.renderedSortKeys.length < 2) {
|
|
1547
|
+
return null
|
|
1548
|
+
}
|
|
1549
|
+
const index = this.renderedSortKeys.findIndex((k) => k.key === key)
|
|
1550
|
+
return index === -1 ? null : index + 1
|
|
1551
|
+
},
|
|
1552
|
+
|
|
1553
|
+
/**
|
|
1554
|
+
* `aria-sort` value for a column header: `'ascending'`/`'descending'` for the PRIMARY (index 0) active sort key only, per WCAG guidance that `aria-sort` describes single-column sort state.
|
|
1555
|
+
* `null` omits the attribute entirely (unsorted, not sortable, or not the primary key).
|
|
1531
1556
|
*
|
|
1532
1557
|
* @param {object} col Column definition.
|
|
1533
1558
|
* @return {string|null}
|
|
@@ -395,6 +395,13 @@ export default {
|
|
|
395
395
|
padding: calc(5 * var(--default-grid-baseline));
|
|
396
396
|
}
|
|
397
397
|
|
|
398
|
+
/* Clear the Nextcloud navigation toggle button (44px wide, absolutely
|
|
399
|
+
positioned at the left edge of .app-content) plus 12px breathing room, as
|
|
400
|
+
CnPageHeader does. Only the header shifts; the cards keep the full width. */
|
|
401
|
+
.cn-link-cards-page__header {
|
|
402
|
+
padding-inline-start: 56px;
|
|
403
|
+
}
|
|
404
|
+
|
|
398
405
|
.cn-link-cards-page__title {
|
|
399
406
|
margin: 0 0 var(--default-grid-baseline);
|
|
400
407
|
}
|
|
@@ -445,12 +452,32 @@ export default {
|
|
|
445
452
|
gap: calc(3 * var(--default-grid-baseline));
|
|
446
453
|
min-width: 0;
|
|
447
454
|
padding: calc(4 * var(--default-grid-baseline));
|
|
448
|
-
|
|
455
|
+
transition: background-color var(--animation-quick, 100ms) ease, border-color var(--animation-quick, 100ms) ease, box-shadow var(--animation-quick, 100ms) ease;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/* A card is a link, and it must not look like a line of prose. A theme may
|
|
459
|
+
underline every link with `!important` (thematiq: `a { text-decoration:
|
|
460
|
+
underline !important }`, specificity 0,0,1). Among `!important` rules the
|
|
461
|
+
higher specificity wins, so this class rule has to carry it too; without it
|
|
462
|
+
the label and the description both rendered underlined on a live Nextcloud. */
|
|
463
|
+
.cn-link-cards-page__card,
|
|
464
|
+
.cn-link-cards-page__card:hover,
|
|
465
|
+
.cn-link-cards-page__card:focus,
|
|
466
|
+
.cn-link-cards-page__card:active {
|
|
467
|
+
text-decoration: none !important;
|
|
449
468
|
}
|
|
450
469
|
|
|
470
|
+
/* Hover moves the whole card: border, background and a soft lift. */
|
|
451
471
|
.cn-link-cards-page__card:hover {
|
|
452
472
|
background: var(--color-background-hover);
|
|
453
473
|
border-color: var(--color-primary-element);
|
|
474
|
+
box-shadow: 0 2px 6px var(--color-box-shadow);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
@media (prefers-reduced-motion: reduce) {
|
|
478
|
+
.cn-link-cards-page__card {
|
|
479
|
+
transition: none;
|
|
480
|
+
}
|
|
454
481
|
}
|
|
455
482
|
|
|
456
483
|
.cn-link-cards-page__card:focus-visible {
|
|
@@ -471,8 +498,14 @@ export default {
|
|
|
471
498
|
min-width: 0;
|
|
472
499
|
}
|
|
473
500
|
|
|
501
|
+
/* The label is the card's title. Its colour is set here, on the span, because
|
|
502
|
+
a theme may force its link colour on the `a` itself; an inherited link
|
|
503
|
+
colour made the title read as a hyperlink. */
|
|
474
504
|
.cn-link-cards-page__label {
|
|
475
|
-
|
|
505
|
+
color: var(--color-main-text);
|
|
506
|
+
font-size: 1.05em;
|
|
507
|
+
font-weight: 700;
|
|
508
|
+
line-height: 1.3;
|
|
476
509
|
overflow-wrap: anywhere;
|
|
477
510
|
}
|
|
478
511
|
|