@conduction/nextcloud-vue 2.60.0 → 2.62.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esm/components/CnAppNav/CnAppNav.vue2.js +169 -1
- 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 +88 -4
- 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 +40 -8
- 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/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +6 -5
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
- package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue2.js +3 -1
- package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnLinkCardsPage/CnLinkCardsPage.vue.js +10 -0
- package/dist/esm/components/CnLinkCardsPage/CnLinkCardsPage.vue.js.map +1 -0
- package/dist/esm/components/CnLinkCardsPage/CnLinkCardsPage.vue2.js +316 -0
- package/dist/esm/components/CnLinkCardsPage/CnLinkCardsPage.vue2.js.map +1 -0
- package/dist/esm/components/CnLinkCardsPage/CnLinkCardsPage.vue3.js +105 -0
- package/dist/esm/components/CnLinkCardsPage/CnLinkCardsPage.vue3.js.map +1 -0
- 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/CnPageRenderer/pageTypes.js +1 -0
- package/dist/esm/components/CnPageRenderer/pageTypes.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/index.js +6 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/l10n/en.json.js +6 -0
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +6 -0
- package/dist/esm/l10n/nl.json.js.map +1 -1
- package/dist/esm/nextcloud-vue.css +198 -9
- package/dist/esm/utils/headers.js +28 -2
- package/dist/esm/utils/headers.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 +9162 -8283
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +198 -9
- package/l10n/en.json +6 -0
- package/l10n/nl.json +6 -0
- package/package.json +3 -2
- package/scripts/.jsdoc-baselines.json +1 -0
- package/scripts/manifest-schema-hash.js +41 -0
- package/scripts/record-manifest-schema-hash.js +52 -0
- package/src/components/CnAppNav/CnAppNav.vue +169 -1
- package/src/components/CnBannerWidget/CnBannerWidget.vue +122 -4
- package/src/components/CnBrandStripe/CnBrandStripe.md +16 -0
- package/src/components/CnBrandStripe/CnBrandStripe.vue +58 -5
- package/src/components/CnDashboardPage/CnDashboardPage.vue +40 -8
- package/src/components/CnDataTable/CnDataTable.md +1 -1
- package/src/components/CnDataTable/CnDataTable.vue +42 -17
- package/src/components/CnDetailPage/CnDetailPage.vue +4 -1
- package/src/components/CnHeaderWidget/CnHeaderWidget.vue +3 -1
- package/src/components/CnLinkCardsPage/CnLinkCardsPage.vue +517 -0
- package/src/components/CnLinkCardsPage/index.js +4 -0
- package/src/components/CnObjectSidebar/CnFilesTab.vue +65 -0
- package/src/components/CnPageRenderer/pageTypes.js +1 -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/components/index.js +1 -0
- package/src/css/detail-page.css +10 -2
- package/src/css/table.css +1 -2
- package/src/index.js +7 -0
- package/src/public/index.js +5 -0
- package/src/schemas/app-manifest-v2.schema.json +45 -3
- package/src/types/manifest.d.ts +1 -1
- package/src/utils/headers.js +28 -2
- package/src/utils/validateManifestV2.compiled.js +2 -2
|
@@ -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
|
|
|
@@ -359,7 +389,9 @@ export default {
|
|
|
359
389
|
return this.fillValue(reason)
|
|
360
390
|
}
|
|
361
391
|
const title = this.title || (this.content && this.content.title) || ''
|
|
362
|
-
|
|
392
|
+
// A card that sets `text` to the same words as its `title` says
|
|
393
|
+
// them once.
|
|
394
|
+
return title && this.displayText !== this.displayTitle ? this.displayText : ''
|
|
363
395
|
},
|
|
364
396
|
|
|
365
397
|
/**
|
|
@@ -417,6 +449,52 @@ export default {
|
|
|
417
449
|
return this.conditionMet === true
|
|
418
450
|
},
|
|
419
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
|
+
|
|
420
498
|
/** Whether the banner navigates on click (route set + router present). */
|
|
421
499
|
clickable() {
|
|
422
500
|
return !!this.resolvedRoute && !!this.$router
|
|
@@ -491,30 +569,46 @@ export default {
|
|
|
491
569
|
* modes — the Wave-1 banner shape is the canonical one, extracted to
|
|
492
570
|
* `utils/visibleWhen.js` in Wave 3 so manifest actions reuse it),
|
|
493
571
|
* keeping the read VALUE for the `{value}` text placeholder.
|
|
494
|
-
* 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.
|
|
495
575
|
*
|
|
496
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
|
|
497
578
|
*/
|
|
498
579
|
async evaluateCondition() {
|
|
499
580
|
const outcome = this.resolvedConditionOutcome
|
|
500
581
|
if (outcome) {
|
|
501
582
|
this.conditionMet = outcome.met === true
|
|
502
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 : ''
|
|
503
586
|
return
|
|
504
587
|
}
|
|
505
588
|
const cond = this.resolvedVisibleWhen
|
|
506
589
|
if (!cond) {
|
|
507
590
|
this.conditionMet = null
|
|
508
591
|
this.conditionValue = null
|
|
592
|
+
this.conditionFailed = false
|
|
593
|
+
this.conditionFailureReason = ''
|
|
509
594
|
return
|
|
510
595
|
}
|
|
511
596
|
try {
|
|
512
597
|
const value = await readVisibleWhenValue(cond)
|
|
513
598
|
this.conditionValue = value
|
|
514
599
|
this.conditionMet = compareVisibleWhen(value, cond.op || 'eq', cond.value)
|
|
515
|
-
|
|
600
|
+
this.conditionFailed = false
|
|
601
|
+
this.conditionFailureReason = ''
|
|
602
|
+
} catch (error) {
|
|
603
|
+
// Still hidden: a banner that cannot tell whether it applies
|
|
604
|
+
// should not claim attention. But not silently, because a
|
|
605
|
+
// failed count and "nothing to report" look the same on screen.
|
|
606
|
+
// eslint-disable-next-line no-console
|
|
607
|
+
console.warn('[CnBannerWidget] visibleWhen could not be evaluated, banner hidden:', error?.message || error)
|
|
516
608
|
this.conditionMet = false
|
|
517
609
|
this.conditionValue = null
|
|
610
|
+
this.conditionFailed = true
|
|
611
|
+
this.conditionFailureReason = typeof error?.message === 'string' ? error.message : String(error ?? '')
|
|
518
612
|
}
|
|
519
613
|
},
|
|
520
614
|
|
|
@@ -550,6 +644,30 @@ export default {
|
|
|
550
644
|
color: inherit;
|
|
551
645
|
}
|
|
552
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
|
+
|
|
553
671
|
/* Attention card. The severity edge is an inset shadow, so it takes no width. */
|
|
554
672
|
.cn-banner-widget--attention {
|
|
555
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 {
|
|
@@ -2748,17 +2748,30 @@ export default {
|
|
|
2748
2748
|
* the registry branch forwards to CnBannerWidget), or in the legacy
|
|
2749
2749
|
* manifest `props` blob (the fallback getStatsBlockProps and
|
|
2750
2750
|
* getChartProps already honour for their own widget types). `text`
|
|
2751
|
-
* is only meaningful for banners
|
|
2751
|
+
* is only meaningful for banners: it is what the banner would show,
|
|
2752
|
+
* which for an attention card is its `title` when it has no `text`.
|
|
2752
2753
|
*
|
|
2753
2754
|
* @param {object} def Widget definition.
|
|
2754
|
-
*
|
|
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
|
+
*
|
|
2758
|
+
* @spec openspec/changes/link-cards-page/specs/link-cards-page/spec.md#requirement-an-attention-card-with-a-title-keeps-its-cell
|
|
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 }}
|
|
2755
2761
|
*/
|
|
2756
2762
|
widgetDisplayConfig(def) {
|
|
2757
2763
|
const content = (def && typeof def.content === 'object' && def.content) || {}
|
|
2758
2764
|
const props = (def && typeof def.props === 'object' && def.props) || {}
|
|
2765
|
+
// An attention card (`layout: "attention"`) is headed by `title`,
|
|
2766
|
+
// and its title only FALLS BACK to `text`. Reading `text` alone
|
|
2767
|
+
// called a card with a title and a reason empty, and gave its cell
|
|
2768
|
+
// up before its `visibleWhen` was even looked at.
|
|
2769
|
+
const layout = content.layout || props.layout || ''
|
|
2770
|
+
const title = layout === 'attention' ? (content.title || props.title || '') : ''
|
|
2759
2771
|
return {
|
|
2760
|
-
text: content.text || props.text || '',
|
|
2772
|
+
text: content.text || props.text || title || '',
|
|
2761
2773
|
visibleWhen: (def && def.visibleWhen) || content.visibleWhen || props.visibleWhen || null,
|
|
2774
|
+
layout,
|
|
2762
2775
|
}
|
|
2763
2776
|
},
|
|
2764
2777
|
|
|
@@ -2768,15 +2781,22 @@ export default {
|
|
|
2768
2781
|
* text at all — and must therefore surrender its grid cell in live
|
|
2769
2782
|
* mode instead of leaving an empty card behind.
|
|
2770
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
|
+
*
|
|
2771
2790
|
* @param {object} item Layout item.
|
|
2772
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
|
|
2773
2793
|
*/
|
|
2774
2794
|
isCollapsedWidget(item) {
|
|
2775
2795
|
const def = this.getWidgetDef(item.widgetId)
|
|
2776
2796
|
if (!def || !def.type) {
|
|
2777
2797
|
return false
|
|
2778
2798
|
}
|
|
2779
|
-
const { text, visibleWhen } = this.widgetDisplayConfig(def)
|
|
2799
|
+
const { text, visibleWhen, layout } = this.widgetDisplayConfig(def)
|
|
2780
2800
|
if (this.isBannerDef(def) && text === '') {
|
|
2781
2801
|
return true
|
|
2782
2802
|
}
|
|
@@ -2784,6 +2804,9 @@ export default {
|
|
|
2784
2804
|
return false
|
|
2785
2805
|
}
|
|
2786
2806
|
const outcome = this.widgetConditionOutcome[item.widgetId]
|
|
2807
|
+
if (outcome && outcome.failed === true && layout === 'attention' && this.isBannerDef(def)) {
|
|
2808
|
+
return false
|
|
2809
|
+
}
|
|
2787
2810
|
return !outcome || outcome.met !== true
|
|
2788
2811
|
},
|
|
2789
2812
|
|
|
@@ -2794,13 +2817,17 @@ export default {
|
|
|
2794
2817
|
* Runs on created() and again whenever `widgets` changes (the
|
|
2795
2818
|
* in-app editor mutates defs in place). Fail-safe like the banner
|
|
2796
2819
|
* itself: any fetch/shape error counts as "not met", so a broken
|
|
2797
|
-
* 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.
|
|
2798
2824
|
*
|
|
2799
2825
|
* Flips `widgetConditionsSettled` when the initial round is done —
|
|
2800
2826
|
* synchronously when there is nothing to evaluate, so a dashboard
|
|
2801
2827
|
* without conditional widgets never waits.
|
|
2802
2828
|
*
|
|
2803
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
|
|
2804
2831
|
*/
|
|
2805
2832
|
async evaluateWidgetConditions() {
|
|
2806
2833
|
const conditional = (this.widgets || []).filter((def) => def && def.id && def.type
|
|
@@ -2822,12 +2849,17 @@ export default {
|
|
|
2822
2849
|
}
|
|
2823
2850
|
await Promise.all(conditional.map(async (def) => {
|
|
2824
2851
|
const cond = this.widgetDisplayConfig(def).visibleWhen
|
|
2825
|
-
let outcome
|
|
2852
|
+
let outcome
|
|
2826
2853
|
try {
|
|
2827
2854
|
const value = await readVisibleWhenValue(cond)
|
|
2828
2855
|
outcome = { met: compareVisibleWhen(value, cond.op || 'eq', cond.value), value }
|
|
2829
|
-
} catch {
|
|
2830
|
-
//
|
|
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)
|
|
2831
2863
|
}
|
|
2832
2864
|
// A newer run owns the map now — a stale verdict (possibly for
|
|
2833
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}
|
|
@@ -103,7 +103,10 @@
|
|
|
103
103
|
data-testid="cn-detail-page-type-eyebrow">
|
|
104
104
|
{{ typeEyebrow }}
|
|
105
105
|
</p>
|
|
106
|
-
<h2
|
|
106
|
+
<h2
|
|
107
|
+
v-if="displayTitle"
|
|
108
|
+
class="cn-detail-page__title"
|
|
109
|
+
:title="displayTitle">
|
|
107
110
|
{{ displayTitle }}
|
|
108
111
|
</h2>
|
|
109
112
|
<!--
|
|
@@ -496,7 +496,9 @@ export default {
|
|
|
496
496
|
'flex-direction': 'column',
|
|
497
497
|
'align-items': this.flexAlignFromTextAlign,
|
|
498
498
|
'justify-content': VERTICAL_ALIGN_FLEX[this.verticalAlign],
|
|
499
|
-
|
|
499
|
+
// Plain drops the coloured background, not the card padding:
|
|
500
|
+
// at 0 the greeting sat flush against the card's left edge.
|
|
501
|
+
padding: this.isPlain ? '16px' : '16px 24px',
|
|
500
502
|
'box-sizing': 'border-box',
|
|
501
503
|
gap: '8px',
|
|
502
504
|
'text-align': this.textAlign,
|