@conduction/nextcloud-vue 2.92.0 → 2.94.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/CnActionsMenu/CnActionsMenu.vue2.js +1 -1
- package/dist/esm/components/CnActionsMenu/CnActionsMenu.vue2.js.map +1 -1
- package/dist/esm/components/CnActionsMenu/CnActionsMenu.vue3.js +2 -1
- package/dist/esm/components/CnActionsMenu/CnActionsMenu.vue3.js.map +1 -1
- package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue.js +1 -1
- package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue2.js +28 -8
- package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue2.js.map +1 -1
- package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue3.js +37 -2
- package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue3.js.map +1 -1
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js +57 -4
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js +44 -17
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js.map +1 -1
- package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue.js +1 -1
- package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue2.js +44 -2
- package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue2.js.map +1 -1
- package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue3.js +140 -59
- package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue3.js.map +1 -1
- package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue.js +1 -1
- package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue2.js +139 -3
- package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue3.js +60 -12
- package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue.js +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +111 -42
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +117 -73
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +34 -39
- package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +7 -7
- package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +5 -3
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue.js +1 -1
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js +6 -4
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnFeaturesAndRoadmapPage/CnFeaturesAndRoadmapPage.vue2.js +46 -0
- package/dist/esm/components/CnFeaturesAndRoadmapPage/CnFeaturesAndRoadmapPage.vue2.js.map +1 -1
- package/dist/esm/components/CnFeaturesAndRoadmapPage/CnFeaturesAndRoadmapPage.vue3.js +6 -2
- package/dist/esm/components/CnFeaturesAndRoadmapPage/CnFeaturesAndRoadmapPage.vue3.js.map +1 -1
- package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue.js +1 -1
- package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue2.js +82 -9
- package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue2.js.map +1 -1
- package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue3.js +104 -90
- package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue3.js.map +1 -1
- package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue.js +9 -0
- package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue.js.map +1 -0
- package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue2.js +355 -0
- package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue2.js.map +1 -0
- package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue3.js +462 -0
- package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue3.js.map +1 -0
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue.js +1 -1
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js +21 -0
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js +402 -391
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
- package/dist/esm/components/CnIcon/semanticIcons.js +2 -2
- package/dist/esm/components/CnIcon/semanticIcons.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +141 -7
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +5 -2
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
- package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue.js +1 -1
- package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue2.js +22 -1
- package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue3.js +20 -14
- package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnNotificationPreferences/CnNotificationPreferences.vue.js +1 -1
- package/dist/esm/components/CnNotificationPreferences/CnNotificationPreferences.vue2.js +2 -0
- package/dist/esm/components/CnNotificationPreferences/CnNotificationPreferences.vue2.js.map +1 -1
- package/dist/esm/components/CnNotificationPreferences/CnNotificationPreferences.vue3.js +15 -5
- package/dist/esm/components/CnNotificationPreferences/CnNotificationPreferences.vue3.js.map +1 -1
- package/dist/esm/components/CnPageHeader/CnHeaderTrail.vue.js +8 -0
- package/dist/esm/components/CnPageHeader/CnHeaderTrail.vue.js.map +1 -0
- package/dist/esm/components/CnPageHeader/CnHeaderTrail.vue2.js +114 -0
- package/dist/esm/components/CnPageHeader/CnHeaderTrail.vue2.js.map +1 -0
- package/dist/esm/components/CnPageHeader/CnHeaderTrail.vue3.js +45 -0
- package/dist/esm/components/CnPageHeader/CnHeaderTrail.vue3.js.map +1 -0
- package/dist/esm/components/CnPageHeader/CnPageHeader.vue2.js +38 -2
- package/dist/esm/components/CnPageHeader/CnPageHeader.vue2.js.map +1 -1
- package/dist/esm/components/CnPageHeader/CnPageHeader.vue3.js +12 -2
- package/dist/esm/components/CnPageHeader/CnPageHeader.vue3.js.map +1 -1
- package/dist/esm/components/CnPagination/CnPagination.vue2.js.map +1 -1
- package/dist/esm/components/CnPagination/CnPagination.vue3.js +56 -51
- package/dist/esm/components/CnPagination/CnPagination.vue3.js.map +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +3 -3
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnRoadmapTab/CnRoadmapTab.vue.js +1 -1
- package/dist/esm/components/CnRoadmapTab/CnRoadmapTab.vue2.js +8 -30
- package/dist/esm/components/CnRoadmapTab/CnRoadmapTab.vue2.js.map +1 -1
- package/dist/esm/components/CnRoadmapTab/CnRoadmapTab.vue3.js.map +1 -1
- package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue.js +1 -1
- package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue2.js +24 -0
- package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue2.js.map +1 -1
- package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue3.js +3 -1
- package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue3.js.map +1 -1
- package/dist/esm/components/CnTabs/CnTabs.vue.js +1 -1
- package/dist/esm/components/CnTabs/CnTabs.vue2.js +39 -2
- package/dist/esm/components/CnTabs/CnTabs.vue2.js.map +1 -1
- package/dist/esm/components/CnTabs/CnTabs.vue3.js.map +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue.js +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +85 -8
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js +37 -9
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue.js +9 -0
- package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue.js.map +1 -0
- package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue2.js +164 -0
- package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue2.js.map +1 -0
- package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue3.js +93 -0
- package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue3.js.map +1 -0
- package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue.js +1 -1
- package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue2.js.map +1 -1
- package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue3.js +4 -3
- package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue3.js.map +1 -1
- package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue.js +1 -1
- package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue2.js +32 -0
- package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue2.js.map +1 -1
- package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue3.js +31 -20
- package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue3.js.map +1 -1
- package/dist/esm/composables/indexSources.js +9 -0
- package/dist/esm/composables/indexSources.js.map +1 -1
- package/dist/esm/l10n/en.json.js +24 -1
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +24 -1
- package/dist/esm/l10n/nl.json.js.map +1 -1
- package/dist/esm/library-style.js +1 -1
- package/dist/esm/nextcloud-vue.css +1190 -410
- package/dist/esm/utils/dueText.js +49 -8
- package/dist/esm/utils/dueText.js.map +1 -1
- package/dist/esm/utils/featureCatalog.js +135 -0
- package/dist/esm/utils/featureCatalog.js.map +1 -0
- package/dist/esm/utils/fillTemplate.js +74 -3
- package/dist/esm/utils/fillTemplate.js.map +1 -1
- package/dist/esm/utils/headerMeta.js +62 -12
- package/dist/esm/utils/headerMeta.js.map +1 -1
- package/dist/esm/utils/roadmapIssues.js +86 -0
- package/dist/esm/utils/roadmapIssues.js.map +1 -0
- package/dist/esm/utils/userDisplayName.js +47 -0
- package/dist/esm/utils/userDisplayName.js.map +1 -0
- package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
- package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
- package/dist/nextcloud-vue.cjs.js +16229 -13423
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +1190 -410
- package/l10n/en.json +24 -1
- package/l10n/nl.json +24 -1
- package/package.json +1 -1
- package/src/components/CnActionsMenu/CnActionsMenu.vue +3 -1
- package/src/components/CnAdminSettingsShell/CnAdminSettingsShell.vue +39 -1
- package/src/components/CnAppRoot/CnAppRoot.md +2 -0
- package/src/components/CnAppRoot/CnAppRoot.vue +66 -5
- package/src/components/CnChoiceCards/CnChoiceCards.md +27 -0
- package/src/components/CnChoiceCards/CnChoiceCards.vue +168 -3
- package/src/components/CnCountdownWidget/CnCountdownWidget.vue +222 -6
- package/src/components/CnDashboardPage/CnDashboardPage.vue +141 -8
- package/src/components/CnDataTable/CnDataTable.md +1 -0
- package/src/components/CnDataTable/CnDataTable.vue +41 -46
- package/src/components/CnDetailPage/CnDetailPage.vue +5 -3
- package/src/components/CnFactsWidget/CnFactsWidget.vue +6 -4
- package/src/components/CnFeaturesAndRoadmapPage/CnFeaturesAndRoadmapPage.vue +51 -1
- package/src/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue +98 -13
- package/src/components/CnFeaturesBoard/CnFeaturesBoard.vue +1053 -0
- package/src/components/CnFormDialog/CnFormDialog.vue +606 -562
- package/src/components/CnIndexPage/CnIndexPage.vue +144 -7
- package/src/components/CnNcWidgetWidget/CnNcWidgetWidget.vue +23 -2
- package/src/components/CnNotificationPreferences/CnNotificationPreferences.vue +5 -0
- package/src/components/CnPageHeader/CnHeaderTrail.vue +140 -0
- package/src/components/CnPageHeader/CnPageHeader.md +11 -0
- package/src/components/CnPageHeader/CnPageHeader.vue +42 -0
- package/src/components/CnPagination/CnPagination.vue +10 -2
- package/src/components/CnRecordListWidget/CnRecordListWidget.vue +3 -3
- package/src/components/CnRoadmapTab/CnRoadmapTab.vue +6 -28
- package/src/components/CnSettingsPage/CnSettingsPage.vue +26 -0
- package/src/components/CnTabs/CnTabs.vue +39 -2
- package/src/components/CnTabsWidget/CnTabsWidget.vue +106 -13
- package/src/components/CnUserSettingsDialog/CnUserSettingsDialog.vue +305 -0
- package/src/components/CnVersionInfoCard/CnVersionInfoCard.vue +4 -3
- package/src/components/CnWidgetWrapper/CnWidgetWrapper.md +8 -0
- package/src/components/CnWidgetWrapper/CnWidgetWrapper.vue +43 -2
- package/src/composables/indexSources.js +9 -0
- package/src/css/dialog.css +61 -0
- package/src/css/look-board-detail.css +8 -0
- package/src/css/look-board-index.css +27 -1
- package/src/css/look-board.css +60 -0
- package/src/css/page-header.css +16 -0
- package/src/schemas/app-manifest-v2.schema.json +30 -17
- package/src/utils/dueText.js +49 -8
- package/src/utils/featureCatalog.js +132 -0
- package/src/utils/fillTemplate.js +73 -2
- package/src/utils/headerMeta.js +62 -12
- package/src/utils/roadmapIssues.js +82 -0
- package/src/utils/userDisplayName.js +54 -0
- package/src/utils/validateManifestV2.compiled.js +3 -3
|
@@ -4,8 +4,10 @@
|
|
|
4
4
|
-->
|
|
5
5
|
|
|
6
6
|
<template>
|
|
7
|
-
<div
|
|
8
|
-
|
|
7
|
+
<div
|
|
8
|
+
class="cn-kpi-card cn-countdown-widget"
|
|
9
|
+
:class="[`cn-countdown-widget--${variant}`, { 'cn-kpi-card--board cn-countdown-widget--board': isBoardLook }]">
|
|
10
|
+
<div v-if="!isBoardLook" class="cn-kpi-card__icon cn-countdown-widget__icon" :style="iconCircleStyle">
|
|
9
11
|
<CnWidgetIcon :name="resolvedIcon" :size="24" />
|
|
10
12
|
</div>
|
|
11
13
|
|
|
@@ -16,9 +18,35 @@
|
|
|
16
18
|
<div class="cn-kpi-card__value cn-countdown-widget__value" :style="{ color: valueColor }">
|
|
17
19
|
{{ headline }}
|
|
18
20
|
</div>
|
|
19
|
-
<div v-if="subLabel" class="cn-countdown-widget__sub">
|
|
21
|
+
<div v-if="subLabel" class="cn-countdown-widget__sub" data-testid="cn-countdown-sub">
|
|
20
22
|
{{ subLabel }}
|
|
21
23
|
</div>
|
|
24
|
+
<dl v-if="facts.length" class="cn-countdown-widget__facts" data-testid="cn-countdown-facts">
|
|
25
|
+
<template v-for="fact in facts" :key="fact.label">
|
|
26
|
+
<dt>{{ fact.label }}</dt>
|
|
27
|
+
<dd>{{ fact.value }}</dd>
|
|
28
|
+
</template>
|
|
29
|
+
</dl>
|
|
30
|
+
<span
|
|
31
|
+
v-if="progress !== null"
|
|
32
|
+
class="cn-countdown-widget__progress"
|
|
33
|
+
role="meter"
|
|
34
|
+
:aria-label="progressLabel"
|
|
35
|
+
:aria-valuenow="Math.round(progress)"
|
|
36
|
+
aria-valuemin="0"
|
|
37
|
+
aria-valuemax="100"
|
|
38
|
+
data-testid="cn-countdown-progress">
|
|
39
|
+
<span class="cn-countdown-widget__progress-fill" :style="{ width: `${progress}%` }" />
|
|
40
|
+
</span>
|
|
41
|
+
<button
|
|
42
|
+
v-if="actionLabel"
|
|
43
|
+
type="button"
|
|
44
|
+
class="cn-countdown-widget__action"
|
|
45
|
+
:class="{ 'cn-countdown-widget__action--primary': content.action && content.action.variant === 'primary' }"
|
|
46
|
+
data-testid="cn-countdown-action"
|
|
47
|
+
@click="runAction">
|
|
48
|
+
{{ actionLabel }}
|
|
49
|
+
</button>
|
|
22
50
|
</div>
|
|
23
51
|
</div>
|
|
24
52
|
</template>
|
|
@@ -28,7 +56,10 @@ import { translate as t } from '@nextcloud/l10n'
|
|
|
28
56
|
import { inject } from 'vue'
|
|
29
57
|
import CnWidgetIcon from '../CnWidgetGrid/CnWidgetIcon.vue'
|
|
30
58
|
import { getByPath } from '../../composables/useEndpointSource.js'
|
|
59
|
+
import { normalizeLook } from '../../composables/useLook.js'
|
|
60
|
+
import { dispatchAction } from '../../utils/actionsDispatcher.js'
|
|
31
61
|
import { resolveObjectTokenContext } from '../../utils/detailObjectContext.js'
|
|
62
|
+
import { resolveDeepTokens } from '../../utils/resolveFilterTokens.js'
|
|
32
63
|
import { userLocale } from '../../utils/userLocale.js'
|
|
33
64
|
|
|
34
65
|
const MS_PER_DAY = 24 * 60 * 60 * 1000
|
|
@@ -106,8 +137,15 @@ export default {
|
|
|
106
137
|
* - `showDate` — render the absolute date underneath (default true).
|
|
107
138
|
* - `settledField` — property on the record that is truthy once the work is done; the tile then reads `settledLabel` and never counts overdue.
|
|
108
139
|
* - `settledLabel` — the headline of a settled record, run through the host's translate function (default "Closed").
|
|
140
|
+
* - `daysLabel` — `{ one, other }` with `{count}`, the headline while days remain ("27 dagen").
|
|
141
|
+
* - `caption` — the line under the headline instead of the date, with `{date}` (the long date) and `{count}` (days left).
|
|
142
|
+
* - `startField` — property holding the start of the term; a bar then shows how much of it has passed.
|
|
143
|
+
* - `action` — one manifest action under the card (`{ label, type, variant?, ... }`, the headerActions shape; `variant: "primary"` draws it in the primary tint).
|
|
144
|
+
* - `emptyLabel` — the headline while the date is unset (default a dash).
|
|
145
|
+
* - `emptyCaption` — the line under it while the date is unset, with `{date}` read from `emptyDateField`.
|
|
146
|
+
* - `facts` — `[{ label, field }]` rows under the caption.
|
|
109
147
|
*
|
|
110
|
-
* @type {{ field?: string, label?: string, icon?: string, thresholds?: {warn?: number, danger?: number}, showDate?: boolean, settledField?: string, settledLabel?: string }}
|
|
148
|
+
* @type {{ field?: string, label?: string, icon?: string, thresholds?: {warn?: number, danger?: number}, showDate?: boolean, settledField?: string, settledLabel?: string, daysLabel?: {one?: string, other?: string}, caption?: string, startField?: string, action?: object, emptyLabel?: string, emptyCaption?: string, emptyDateField?: string, facts?: Array<{label: string, field: string}> }}
|
|
111
149
|
*/
|
|
112
150
|
content: {
|
|
113
151
|
type: Object,
|
|
@@ -131,6 +169,8 @@ export default {
|
|
|
131
169
|
cnObjectContextInjected: inject('cnObjectContext', null),
|
|
132
170
|
cnDetailObjectContextInjected: inject('cnDetailObjectContext', null),
|
|
133
171
|
cnTranslate: inject('cnTranslate', null),
|
|
172
|
+
cnLook: inject('cnLook', 'nextcloud'),
|
|
173
|
+
cnDispatchAction: inject('cnDispatchAction', null),
|
|
134
174
|
}
|
|
135
175
|
},
|
|
136
176
|
|
|
@@ -222,7 +262,7 @@ export default {
|
|
|
222
262
|
}
|
|
223
263
|
const days = this.daysRemaining
|
|
224
264
|
if (days === null) {
|
|
225
|
-
return '—'
|
|
265
|
+
return this.content?.emptyLabel ? this.translate(this.content.emptyLabel) : '—'
|
|
226
266
|
}
|
|
227
267
|
if (days < 0) {
|
|
228
268
|
const overdue = Math.abs(days)
|
|
@@ -233,12 +273,89 @@ export default {
|
|
|
233
273
|
if (days === 0) {
|
|
234
274
|
return t('nextcloud-vue', 'Due today')
|
|
235
275
|
}
|
|
276
|
+
const labels = this.content?.daysLabel
|
|
277
|
+
if (labels && typeof labels === 'object' && (labels.one || labels.other)) {
|
|
278
|
+
const template = this.translate((days === 1 ? labels.one : labels.other) || labels.other || labels.one)
|
|
279
|
+
return template.replace(/\{count\}/g, String(days))
|
|
280
|
+
}
|
|
236
281
|
if (days === 1) {
|
|
237
282
|
return t('nextcloud-vue', '1 day left')
|
|
238
283
|
}
|
|
239
284
|
return t('nextcloud-vue', '{count} days left', { count: days })
|
|
240
285
|
},
|
|
241
286
|
|
|
287
|
+
/**
|
|
288
|
+
* Whether the app takes the board look: no icon circle.
|
|
289
|
+
*
|
|
290
|
+
* @return {boolean} True in the board look.
|
|
291
|
+
* @spec openspec/changes/countdown-board-look/specs/countdown-board-look/spec.md#requirement-the-countdown-draws-the-boards-term-card
|
|
292
|
+
*/
|
|
293
|
+
isBoardLook() {
|
|
294
|
+
return normalizeLook(this.cnLook) === 'board'
|
|
295
|
+
},
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* How much of the term has passed, from `startField` to the date, in
|
|
299
|
+
* whole days and as a percentage (0 to 100), or null without a usable
|
|
300
|
+
* start.
|
|
301
|
+
*
|
|
302
|
+
* @return {number|null} The share passed.
|
|
303
|
+
* @spec openspec/changes/countdown-board-look/specs/countdown-board-look/spec.md#requirement-the-countdown-draws-the-boards-term-card
|
|
304
|
+
*/
|
|
305
|
+
progress() {
|
|
306
|
+
const field = this.content?.startField
|
|
307
|
+
if (!field || !this.record || !this.targetDate || this.isSettled) {
|
|
308
|
+
return null
|
|
309
|
+
}
|
|
310
|
+
const raw = getByPath(this.record, field)
|
|
311
|
+
const start = raw === null || raw === undefined || raw === '' ? null : new Date(raw)
|
|
312
|
+
if (!start || Number.isNaN(start.getTime())) {
|
|
313
|
+
return null
|
|
314
|
+
}
|
|
315
|
+
const startOfDay = (d) => Date.UTC(d.getFullYear(), d.getMonth(), d.getDate())
|
|
316
|
+
const span = startOfDay(this.targetDate) - startOfDay(start)
|
|
317
|
+
if (span <= 0) {
|
|
318
|
+
return null
|
|
319
|
+
}
|
|
320
|
+
return Math.min(100, Math.max(0, ((startOfDay(new Date()) - startOfDay(start)) / span) * 100))
|
|
321
|
+
},
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* The `facts` rows under the caption (`[{ label, field }]`), with
|
|
325
|
+
* their values read off the record; a row without a value is left out.
|
|
326
|
+
*
|
|
327
|
+
* @return {Array<{label: string, value: string}>} The rows.
|
|
328
|
+
* @spec openspec/changes/countdown-board-look/specs/countdown-board-look/spec.md#requirement-the-countdown-draws-the-boards-term-card
|
|
329
|
+
*/
|
|
330
|
+
facts() {
|
|
331
|
+
const rows = Array.isArray(this.content?.facts) ? this.content.facts : []
|
|
332
|
+
return rows
|
|
333
|
+
.filter((row) => row && typeof row.label === 'string' && typeof row.field === 'string')
|
|
334
|
+
.map((row) => {
|
|
335
|
+
const value = this.record ? getByPath(this.record, row.field) : null
|
|
336
|
+
return { label: this.translate(row.label), value: value === null || value === undefined ? '' : this.translate(String(value)) }
|
|
337
|
+
})
|
|
338
|
+
.filter((row) => row.value !== '')
|
|
339
|
+
},
|
|
340
|
+
|
|
341
|
+
/** @return {string} The accessible name of the progress bar. */
|
|
342
|
+
progressLabel() {
|
|
343
|
+
return t('nextcloud-vue', 'Time passed')
|
|
344
|
+
},
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* The label of `content.action`, translated, or ''.
|
|
348
|
+
*
|
|
349
|
+
* @return {string} The label.
|
|
350
|
+
* @spec openspec/changes/countdown-board-look/specs/countdown-board-look/spec.md#requirement-the-countdown-draws-the-boards-term-card
|
|
351
|
+
*/
|
|
352
|
+
actionLabel() {
|
|
353
|
+
const action = this.content?.action
|
|
354
|
+
return action && typeof action === 'object' && typeof action.label === 'string' && action.label !== ''
|
|
355
|
+
? this.translate(action.label)
|
|
356
|
+
: ''
|
|
357
|
+
},
|
|
358
|
+
|
|
242
359
|
/**
|
|
243
360
|
* The absolute date under the headline, so the tile answers "how long"
|
|
244
361
|
* and "when" at once.
|
|
@@ -246,10 +363,21 @@ export default {
|
|
|
246
363
|
* @return {string} The formatted date, or ''.
|
|
247
364
|
*/
|
|
248
365
|
subLabel() {
|
|
366
|
+
if (!this.targetDate && typeof this.content?.emptyCaption === 'string' && this.content.emptyCaption !== '') {
|
|
367
|
+
const raw = this.content.emptyDateField && this.record ? getByPath(this.record, this.content.emptyDateField) : null
|
|
368
|
+
const when = raw ? new Date(raw) : null
|
|
369
|
+
const date = when && !Number.isNaN(when.getTime()) ? when.toLocaleDateString(userLocale(), { day: 'numeric', month: 'long', year: 'numeric' }) : ''
|
|
370
|
+
return this.translate(this.content.emptyCaption).replace(/\{date\}/g, date)
|
|
371
|
+
}
|
|
249
372
|
if (!this.targetDate || this.content?.showDate === false) {
|
|
250
373
|
return ''
|
|
251
374
|
}
|
|
252
|
-
|
|
375
|
+
const date = this.targetDate.toLocaleDateString(userLocale(), { day: 'numeric', month: 'long', year: 'numeric' })
|
|
376
|
+
const caption = this.content?.caption
|
|
377
|
+
if (typeof caption === 'string' && caption !== '') {
|
|
378
|
+
return this.translate(caption).replace(/\{date\}/g, date).replace(/\{count\}/g, String(this.daysRemaining ?? ''))
|
|
379
|
+
}
|
|
380
|
+
return date
|
|
253
381
|
},
|
|
254
382
|
|
|
255
383
|
/**
|
|
@@ -278,6 +406,9 @@ export default {
|
|
|
278
406
|
|
|
279
407
|
/** @return {string} The value colour for the current band. */
|
|
280
408
|
valueColor() {
|
|
409
|
+
if (this.isBoardLook && this.variant === 'default') {
|
|
410
|
+
return 'var(--color-main-text)'
|
|
411
|
+
}
|
|
281
412
|
return VARIANT_COLORS[this.variant] || VARIANT_COLORS.default
|
|
282
413
|
},
|
|
283
414
|
|
|
@@ -296,6 +427,39 @@ export default {
|
|
|
296
427
|
return { color: this.valueColor }
|
|
297
428
|
},
|
|
298
429
|
},
|
|
430
|
+
|
|
431
|
+
methods: {
|
|
432
|
+
/**
|
|
433
|
+
* The host's translate, or the text unchanged.
|
|
434
|
+
*
|
|
435
|
+
* @param {string} text The text.
|
|
436
|
+
* @return {string} The translation.
|
|
437
|
+
*/
|
|
438
|
+
translate(text) {
|
|
439
|
+
return typeof this.cnTranslate === 'function' ? this.cnTranslate(text) : text
|
|
440
|
+
},
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Dispatch `content.action` with the record's tokens resolved, through
|
|
444
|
+
* the page's dispatcher when there is one.
|
|
445
|
+
*
|
|
446
|
+
* @return {unknown}
|
|
447
|
+
* @spec openspec/changes/countdown-board-look/specs/countdown-board-look/spec.md#requirement-the-countdown-draws-the-boards-term-card
|
|
448
|
+
*/
|
|
449
|
+
runAction() {
|
|
450
|
+
const action = this.content?.action
|
|
451
|
+
if (!action || typeof action !== 'object') {
|
|
452
|
+
return undefined
|
|
453
|
+
}
|
|
454
|
+
const ctx = resolveObjectTokenContext(this.cnObjectContextInjected, this.cnDetailObjectContextInjected) || {}
|
|
455
|
+
const tokenCtx = { ...ctx, object: this.record || ctx.object || null, objectId: this.record?.id ?? ctx.objectId ?? null }
|
|
456
|
+
const resolved = resolveDeepTokens(action, tokenCtx)
|
|
457
|
+
if (typeof this.cnDispatchAction === 'function') {
|
|
458
|
+
return this.cnDispatchAction(resolved)
|
|
459
|
+
}
|
|
460
|
+
return dispatchAction(resolved, { router: this.$router || null, tokenCtx })
|
|
461
|
+
},
|
|
462
|
+
},
|
|
299
463
|
}
|
|
300
464
|
</script>
|
|
301
465
|
|
|
@@ -304,6 +468,58 @@ export default {
|
|
|
304
468
|
white-space: nowrap;
|
|
305
469
|
}
|
|
306
470
|
|
|
471
|
+
.cn-countdown-widget__progress {
|
|
472
|
+
background: var(--color-border);
|
|
473
|
+
border-radius: 3px;
|
|
474
|
+
display: block;
|
|
475
|
+
height: 6px;
|
|
476
|
+
margin-block-start: 10px;
|
|
477
|
+
overflow: hidden;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
.cn-countdown-widget__progress-fill {
|
|
481
|
+
background: var(--color-primary-element);
|
|
482
|
+
display: block;
|
|
483
|
+
height: 100%;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
.cn-countdown-widget__action {
|
|
487
|
+
align-self: flex-start;
|
|
488
|
+
background: var(--color-background-dark);
|
|
489
|
+
border: 0;
|
|
490
|
+
border-radius: var(--border-radius-pill, 17px);
|
|
491
|
+
color: var(--color-main-text);
|
|
492
|
+
font-weight: 600;
|
|
493
|
+
margin-block-start: 10px;
|
|
494
|
+
min-height: 34px;
|
|
495
|
+
padding: 0 12px;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
.cn-countdown-widget__action--primary {
|
|
499
|
+
background: var(--color-primary-element-light);
|
|
500
|
+
color: var(--color-primary-element-light-text);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
.cn-countdown-widget__facts {
|
|
504
|
+
display: grid;
|
|
505
|
+
gap: 8px 16px;
|
|
506
|
+
grid-template-columns: max-content 1fr;
|
|
507
|
+
margin: 10px 0 0;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
.cn-countdown-widget__facts dt {
|
|
511
|
+
color: var(--color-text-maxcontrast);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
.cn-countdown-widget__facts dd {
|
|
515
|
+
margin: 0;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
.cn-countdown-widget__action:hover,
|
|
519
|
+
.cn-countdown-widget__action:focus-visible {
|
|
520
|
+
background: var(--color-background-hover);
|
|
521
|
+
}
|
|
522
|
+
|
|
307
523
|
.cn-countdown-widget__sub {
|
|
308
524
|
color: var(--color-text-maxcontrast);
|
|
309
525
|
font-size: 0.85em;
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
<div
|
|
14
14
|
class="cn-dashboard-page"
|
|
15
15
|
:class="{ 'cn-dashboard-page--board': isBoardLook }"
|
|
16
|
+
:style="{ '--cn-dashboard-grid-margin': `${gridMargin}px` }"
|
|
16
17
|
data-testid="cn-dashboard-page">
|
|
17
18
|
<!-- Header. `showHeader: false` drops the whole row (title, description,
|
|
18
19
|
header actions, edit toggle) for a page that opens with its own
|
|
@@ -33,7 +34,29 @@
|
|
|
33
34
|
<!-- @slot title-meta Content on the title's row, right of the title (e.g. a page-wide picker). Fill it from a manifest with `page.slots: { "title-meta": "<RegistryName>" }`. -->
|
|
34
35
|
<slot name="title-meta" />
|
|
35
36
|
</div>
|
|
36
|
-
<
|
|
37
|
+
<CnHeaderTrail
|
|
38
|
+
:breadcrumb="breadcrumb"
|
|
39
|
+
:current="title ? resolvedTitle : ''"
|
|
40
|
+
:meta="headerMeta ? effectiveTranslate(headerMeta) : ''" />
|
|
41
|
+
<!-- `dateRange.placement: "description"`: the segmented period
|
|
42
|
+
group ends the description line ("Week 41 · alle teams ·
|
|
43
|
+
[Deze week | Dit kwartaal | Dit jaar]"). -->
|
|
44
|
+
<div
|
|
45
|
+
v-if="datePeriodInDescription"
|
|
46
|
+
class="cn-dashboard-page__description cn-dashboard-page__description--with-period"
|
|
47
|
+
data-testid="cn-dashboard-page-subtitle">
|
|
48
|
+
<span v-if="subtitleText">{{ subtitleText }}</span>
|
|
49
|
+
<span v-if="subtitleText" aria-hidden="true">·</span>
|
|
50
|
+
<CnSegmentedControl
|
|
51
|
+
size="compact"
|
|
52
|
+
mode="toggle"
|
|
53
|
+
:options="segmentedOptions"
|
|
54
|
+
:modelValue="segmentedValue"
|
|
55
|
+
:ariaLabel="datePeriodLabel"
|
|
56
|
+
data-testid="cn-dashboard-page-date-segmented"
|
|
57
|
+
@update:modelValue="onSegmentPick" />
|
|
58
|
+
</div>
|
|
59
|
+
<p v-else-if="subtitleText" class="cn-dashboard-page__description" data-testid="cn-dashboard-page-subtitle">
|
|
37
60
|
{{ subtitleText }}
|
|
38
61
|
</p>
|
|
39
62
|
</div>
|
|
@@ -184,7 +207,7 @@
|
|
|
184
207
|
handling, persistence, and event emission lives in
|
|
185
208
|
`onDateRangeChange`. -->
|
|
186
209
|
<div
|
|
187
|
-
v-if="showHeaderDateRange"
|
|
210
|
+
v-if="showHeaderDateRange && !datePeriodInDescription"
|
|
188
211
|
class="cn-dashboard-page__date-range"
|
|
189
212
|
:class="{
|
|
190
213
|
'cn-dashboard-page__date-range--pills': dateRangeControl === 'pills',
|
|
@@ -435,6 +458,7 @@
|
|
|
435
458
|
v-else
|
|
436
459
|
:title="getWidgetTitle(item)"
|
|
437
460
|
:eyebrow="getWidgetEyebrow(item)"
|
|
461
|
+
:subtitle="getWidgetSubtitle(item)"
|
|
438
462
|
:showTitle="false"
|
|
439
463
|
:showRefresh="false">
|
|
440
464
|
<div class="cn-dashboard-page__unknown">
|
|
@@ -497,6 +521,7 @@
|
|
|
497
521
|
v-if="missingRequiredApp(item)"
|
|
498
522
|
:title="getWidgetTitle(item)"
|
|
499
523
|
:eyebrow="getWidgetEyebrow(item)"
|
|
524
|
+
:subtitle="getWidgetSubtitle(item)"
|
|
500
525
|
:showTitle="widgetShowTitle(item)">
|
|
501
526
|
<!-- Compact on purpose: a two-row tile is about 150px, less
|
|
502
527
|
a title row. NcEmptyContent's 64px icon, 20px name,
|
|
@@ -536,6 +561,7 @@
|
|
|
536
561
|
:widgetId="item.widgetId"
|
|
537
562
|
:title="getWidgetTitle(item)"
|
|
538
563
|
:eyebrow="getWidgetEyebrow(item)"
|
|
564
|
+
:subtitle="getWidgetSubtitle(item)"
|
|
539
565
|
:iconUrl="getWidgetIconUrl(item)"
|
|
540
566
|
:iconClass="getWidgetIconClass(item)"
|
|
541
567
|
:showTitle="widgetShowTitle(item)"
|
|
@@ -618,6 +644,7 @@
|
|
|
618
644
|
:class="{ 'cn-dashboard-page__chart-fit': isChartFitted(item) }"
|
|
619
645
|
:title="getWidgetTitle(item)"
|
|
620
646
|
:eyebrow="getWidgetEyebrow(item)"
|
|
647
|
+
:subtitle="getWidgetSubtitle(item)"
|
|
621
648
|
:iconUrl="getWidgetIconUrl(item)"
|
|
622
649
|
:iconClass="getWidgetIconClass(item)"
|
|
623
650
|
:showTitle="widgetShowTitle(item)"
|
|
@@ -697,6 +724,7 @@
|
|
|
697
724
|
:widgetId="item.widgetId"
|
|
698
725
|
:title="getWidgetTitle(item)"
|
|
699
726
|
:eyebrow="getWidgetEyebrow(item)"
|
|
727
|
+
:subtitle="getWidgetSubtitle(item)"
|
|
700
728
|
:iconUrl="getWidgetIconUrl(item)"
|
|
701
729
|
:iconClass="getWidgetIconClass(item)"
|
|
702
730
|
:showTitle="widgetShowTitle(item)"
|
|
@@ -725,6 +753,7 @@
|
|
|
725
753
|
:widgetId="item.widgetId"
|
|
726
754
|
:title="getWidgetTitle(item)"
|
|
727
755
|
:eyebrow="getWidgetEyebrow(item)"
|
|
756
|
+
:subtitle="getWidgetSubtitle(item)"
|
|
728
757
|
:iconUrl="getWidgetIconUrl(item)"
|
|
729
758
|
:iconClass="getWidgetIconClass(item)"
|
|
730
759
|
:showTitle="widgetShowTitle(item)"
|
|
@@ -765,6 +794,7 @@
|
|
|
765
794
|
:widgetId="item.widgetId"
|
|
766
795
|
:title="getWidgetTitle(item)"
|
|
767
796
|
:eyebrow="getWidgetEyebrow(item)"
|
|
797
|
+
:subtitle="getWidgetSubtitle(item)"
|
|
768
798
|
:iconUrl="getWidgetIconUrl(item)"
|
|
769
799
|
:iconClass="getWidgetIconClass(item)"
|
|
770
800
|
:showTitle="widgetShowTitle(item)"
|
|
@@ -791,6 +821,7 @@
|
|
|
791
821
|
:widgetId="item.widgetId"
|
|
792
822
|
:title="getWidgetTitle(item)"
|
|
793
823
|
:eyebrow="getWidgetEyebrow(item)"
|
|
824
|
+
:subtitle="getWidgetSubtitle(item)"
|
|
794
825
|
:showTitle="widgetShowTitle(item)"
|
|
795
826
|
:showActions="widgetShowActions(item)"
|
|
796
827
|
:showRefresh="getWidgetShowRefresh(item)"
|
|
@@ -892,6 +923,7 @@
|
|
|
892
923
|
v-else
|
|
893
924
|
:title="getWidgetTitle(item)"
|
|
894
925
|
:eyebrow="getWidgetEyebrow(item)"
|
|
926
|
+
:subtitle="getWidgetSubtitle(item)"
|
|
895
927
|
:showTitle="widgetShowTitle(item)"
|
|
896
928
|
:showRefresh="false">
|
|
897
929
|
<div class="cn-dashboard-page__unknown">
|
|
@@ -971,6 +1003,7 @@ import CnDashboardGrid from '../CnDashboardGrid/CnDashboardGrid.vue'
|
|
|
971
1003
|
import CnDateRangePicker, { DEFAULT_DATE_RANGE_PRESETS, resolvePresetWindow, translatePresetLabel } from '../CnDateRangePicker/CnDateRangePicker.vue'
|
|
972
1004
|
import CnEmptyContent from '../CnEmptyContent/CnEmptyContent.vue'
|
|
973
1005
|
import CnKpiGrid from '../CnKpiGrid/CnKpiGrid.vue'
|
|
1006
|
+
import CnHeaderTrail from '../CnPageHeader/CnHeaderTrail.vue'
|
|
974
1007
|
import CnSegmentedControl from '../CnSegmentedControl/CnSegmentedControl.vue'
|
|
975
1008
|
import CnStatsBlockWidget from '../CnStatsBlockWidget/CnStatsBlockWidget.vue'
|
|
976
1009
|
import CnTileWidget from '../CnTileWidget/CnTileWidget.vue'
|
|
@@ -1114,6 +1147,7 @@ export default {
|
|
|
1114
1147
|
name: 'CnDashboardPage',
|
|
1115
1148
|
|
|
1116
1149
|
components: {
|
|
1150
|
+
CnHeaderTrail,
|
|
1117
1151
|
NcActions,
|
|
1118
1152
|
NcActionButton,
|
|
1119
1153
|
NcActionInput,
|
|
@@ -1213,6 +1247,30 @@ export default {
|
|
|
1213
1247
|
default: '',
|
|
1214
1248
|
},
|
|
1215
1249
|
|
|
1250
|
+
/**
|
|
1251
|
+
* A breadcrumb under the title (manifest `config.breadcrumb`): one
|
|
1252
|
+
* crumb `{ label, route?, params?, href?, icon?, separator? }` or a
|
|
1253
|
+
* list; the title follows as the current crumb. Drawn as the detail
|
|
1254
|
+
* header's breadcrumb row. Null (the default) draws none.
|
|
1255
|
+
*
|
|
1256
|
+
* @type {object|Array<object>|null}
|
|
1257
|
+
* @spec openspec/changes/page-header-breadcrumb/specs/page-header/spec.md#requirement-a-list-dashboard-or-settings-page-can-show-a-breadcrumb-and-a-meta-note
|
|
1258
|
+
*/
|
|
1259
|
+
breadcrumb: {
|
|
1260
|
+
type: [Object, Array],
|
|
1261
|
+
default: null,
|
|
1262
|
+
},
|
|
1263
|
+
|
|
1264
|
+
/**
|
|
1265
|
+
* A note after the breadcrumb and a middle dot (manifest `config.headerMeta`).
|
|
1266
|
+
*
|
|
1267
|
+
* @spec openspec/changes/page-header-breadcrumb/specs/page-header/spec.md#requirement-a-list-dashboard-or-settings-page-can-show-a-breadcrumb-and-a-meta-note
|
|
1268
|
+
*/
|
|
1269
|
+
headerMeta: {
|
|
1270
|
+
type: String,
|
|
1271
|
+
default: '',
|
|
1272
|
+
},
|
|
1273
|
+
|
|
1216
1274
|
/** Page description (shown below title) */
|
|
1217
1275
|
description: {
|
|
1218
1276
|
type: String,
|
|
@@ -1252,12 +1310,16 @@ export default {
|
|
|
1252
1310
|
* October 2026", `"full"` uses the whole display name. The page
|
|
1253
1311
|
* `description`, when set, follows after another middle dot. Off by
|
|
1254
1312
|
* default: the subtitle is the description alone, as before.
|
|
1313
|
+
* `"date"` leads with the date alone, and `{ label }` puts the
|
|
1314
|
+
* translated label where the greeting would be ("Customer contact ·
|
|
1315
|
+
* Wednesday 7 October 2026 · ...").
|
|
1255
1316
|
*
|
|
1256
1317
|
* @spec openspec/changes/screens-dashboard-greeting-header/specs/dashboard-page/spec.md#requirement-the-header-subtitle-can-greet-the-reader
|
|
1257
|
-
* @
|
|
1318
|
+
* @spec openspec/changes/dashboard-greeting-label/specs/dashboard-page/spec.md#requirement-the-header-subtitle-can-lead-with-a-label-or-the-date
|
|
1319
|
+
* @type {boolean|string|object}
|
|
1258
1320
|
*/
|
|
1259
1321
|
greeting: {
|
|
1260
|
-
type: [Boolean, String],
|
|
1322
|
+
type: [Boolean, String, Object],
|
|
1261
1323
|
default: false,
|
|
1262
1324
|
},
|
|
1263
1325
|
|
|
@@ -1992,6 +2054,10 @@ export default {
|
|
|
1992
2054
|
* @return {string}
|
|
1993
2055
|
*/
|
|
1994
2056
|
greetingLine() {
|
|
2057
|
+
if (this.greeting && typeof this.greeting === 'object') {
|
|
2058
|
+
const label = typeof this.greeting.label === 'string' ? this.greeting.label.trim() : ''
|
|
2059
|
+
return label ? this.effectiveTranslate(label) : ''
|
|
2060
|
+
}
|
|
1995
2061
|
if (this.greeting !== true && this.greeting !== 'first' && this.greeting !== 'full') {
|
|
1996
2062
|
return ''
|
|
1997
2063
|
}
|
|
@@ -2014,17 +2080,20 @@ export default {
|
|
|
2014
2080
|
},
|
|
2015
2081
|
|
|
2016
2082
|
/**
|
|
2017
|
-
* The header subtitle: with `greeting` on, the greeting
|
|
2018
|
-
* written out and the description, joined by a
|
|
2019
|
-
*
|
|
2083
|
+
* The header subtitle: with `greeting` on, the greeting (or the
|
|
2084
|
+
* label), today's date written out and the description, joined by a
|
|
2085
|
+
* middle dot; with `greeting: "date"` the date leads, capitalised;
|
|
2086
|
+
* else the description alone, as before.
|
|
2020
2087
|
*
|
|
2021
2088
|
* @spec openspec/changes/screens-dashboard-greeting-header/specs/dashboard-page/spec.md#requirement-the-header-subtitle-can-greet-the-reader
|
|
2089
|
+
* @spec openspec/changes/dashboard-greeting-label/specs/dashboard-page/spec.md#requirement-the-header-subtitle-can-lead-with-a-label-or-the-date
|
|
2022
2090
|
* @return {string}
|
|
2023
2091
|
*/
|
|
2024
2092
|
subtitleText() {
|
|
2025
2093
|
const description = this.description ? this.resolvedDescription : ''
|
|
2094
|
+
const dateOnly = this.greeting === 'date'
|
|
2026
2095
|
const greeting = this.greetingLine
|
|
2027
|
-
if (greeting === '') {
|
|
2096
|
+
if (greeting === '' && !dateOnly) {
|
|
2028
2097
|
return description
|
|
2029
2098
|
}
|
|
2030
2099
|
const options = { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' }
|
|
@@ -2034,6 +2103,9 @@ export default {
|
|
|
2034
2103
|
} catch {
|
|
2035
2104
|
date = new Intl.DateTimeFormat(undefined, options).format(new Date())
|
|
2036
2105
|
}
|
|
2106
|
+
if (dateOnly) {
|
|
2107
|
+
date = date.charAt(0).toLocaleUpperCase() + date.slice(1)
|
|
2108
|
+
}
|
|
2037
2109
|
return [greeting, date, description].filter((part) => part !== '').join(' · ')
|
|
2038
2110
|
},
|
|
2039
2111
|
|
|
@@ -2246,6 +2318,20 @@ export default {
|
|
|
2246
2318
|
return this.dateRangeEnabled && this.dateRange.showHeaderPicker !== false
|
|
2247
2319
|
},
|
|
2248
2320
|
|
|
2321
|
+
/**
|
|
2322
|
+
* Whether the segmented period group sits at the end of the
|
|
2323
|
+
* description line (`dateRange.placement: "description"`) instead of
|
|
2324
|
+
* in a row of its own. Only for the segmented control without a
|
|
2325
|
+
* custom range, whose from/to popover keeps the row of its own.
|
|
2326
|
+
*
|
|
2327
|
+
* @return {boolean}
|
|
2328
|
+
* @spec openspec/changes/page-header-breadcrumb/specs/page-header/spec.md#requirement-a-dashboard-can-show-its-period-on-the-description-line
|
|
2329
|
+
*/
|
|
2330
|
+
datePeriodInDescription() {
|
|
2331
|
+
return this.showHeaderDateRange && this.dateRangeControl === 'segmented'
|
|
2332
|
+
&& this.dateRange.placement === 'description' && !this.hasCustomPreset
|
|
2333
|
+
},
|
|
2334
|
+
|
|
2249
2335
|
/**
|
|
2250
2336
|
* The preset list passed to the picker. Falls back to the
|
|
2251
2337
|
* `DEFAULT_DATE_RANGE_PRESETS` constant when the consumer
|
|
@@ -2957,6 +3043,7 @@ export default {
|
|
|
2957
3043
|
widgetId: item.widgetId,
|
|
2958
3044
|
title: this.getWidgetTitle(item),
|
|
2959
3045
|
eyebrow: this.getWidgetEyebrow(item),
|
|
3046
|
+
subtitle: this.getWidgetSubtitle(item),
|
|
2960
3047
|
showTitle: this.widgetShowTitle(item),
|
|
2961
3048
|
showActions: this.widgetShowActions(item),
|
|
2962
3049
|
showRefresh: this.getWidgetShowRefresh(item),
|
|
@@ -4075,6 +4162,20 @@ export default {
|
|
|
4075
4162
|
this.showWidgetConfig = false
|
|
4076
4163
|
},
|
|
4077
4164
|
|
|
4165
|
+
/**
|
|
4166
|
+
* The line under a widget's title (`subtitle` on the widget
|
|
4167
|
+
* definition), translated; empty when it carries none.
|
|
4168
|
+
*
|
|
4169
|
+
* @param {object} item Layout item.
|
|
4170
|
+
* @return {string}
|
|
4171
|
+
* @spec openspec/changes/page-header-breadcrumb/specs/page-header/spec.md#requirement-a-dashboard-widget-card-can-carry-a-line-under-its-title
|
|
4172
|
+
*/
|
|
4173
|
+
getWidgetSubtitle(item) {
|
|
4174
|
+
const def = this.getWidgetDef(item.widgetId)
|
|
4175
|
+
const subtitle = def && typeof def.subtitle === 'string' ? def.subtitle : ''
|
|
4176
|
+
return subtitle ? this.effectiveTranslate(subtitle) : ''
|
|
4177
|
+
},
|
|
4178
|
+
|
|
4078
4179
|
/**
|
|
4079
4180
|
* The line above a widget's title (`eyebrow` on the widget
|
|
4080
4181
|
* definition: "Saldo per week" above "Prognose 13 weken"), through the
|
|
@@ -4888,6 +4989,14 @@ export default {
|
|
|
4888
4989
|
margin-bottom: 16px;
|
|
4889
4990
|
}
|
|
4890
4991
|
|
|
4992
|
+
/* The period group at the end of the description line. */
|
|
4993
|
+
.cn-dashboard-page__description--with-period {
|
|
4994
|
+
align-items: center;
|
|
4995
|
+
display: flex;
|
|
4996
|
+
flex-wrap: wrap;
|
|
4997
|
+
gap: 8px;
|
|
4998
|
+
}
|
|
4999
|
+
|
|
4891
5000
|
.cn-dashboard-page__date-segmented {
|
|
4892
5001
|
display: flex;
|
|
4893
5002
|
align-items: center;
|
|
@@ -4899,6 +5008,15 @@ export default {
|
|
|
4899
5008
|
margin-bottom: var(--cn-board-section-gap, 16px);
|
|
4900
5009
|
}
|
|
4901
5010
|
|
|
5011
|
+
/* Board look: GridStack insets every card by the grid margin on each side, so
|
|
5012
|
+
the cards started a margin to the right of the KPI row and the header. The
|
|
5013
|
+
grid reaches out by that margin, so the card edges line up with the page's
|
|
5014
|
+
(dashboard-grid-flush). */
|
|
5015
|
+
.cn-dashboard-page--board :deep(.cn-dashboard-grid) {
|
|
5016
|
+
width: auto;
|
|
5017
|
+
margin-inline: calc(-1 * var(--cn-dashboard-grid-margin, 12px));
|
|
5018
|
+
}
|
|
5019
|
+
|
|
4902
5020
|
.cn-dashboard-page__kpi-cell {
|
|
4903
5021
|
display: flex;
|
|
4904
5022
|
min-width: 0;
|
|
@@ -5129,6 +5247,21 @@ export default {
|
|
|
5129
5247
|
.cn-dashboard-page--board .cn-dashboard-page__kpi-cell {
|
|
5130
5248
|
min-height: 44px;
|
|
5131
5249
|
}
|
|
5250
|
+
|
|
5251
|
+
/* The widget Actions menu shows its icon only, so the card title keeps
|
|
5252
|
+
the room; the name stays for assistive tech. */
|
|
5253
|
+
.cn-dashboard-page--board :deep(.cn-widget-wrapper__actions .action-item__menutoggle) {
|
|
5254
|
+
min-width: 44px;
|
|
5255
|
+
}
|
|
5256
|
+
|
|
5257
|
+
.cn-dashboard-page--board :deep(.cn-widget-wrapper__actions .action-item__menutoggle .button-vue__text) {
|
|
5258
|
+
position: absolute;
|
|
5259
|
+
width: 1px;
|
|
5260
|
+
height: 1px;
|
|
5261
|
+
overflow: hidden;
|
|
5262
|
+
clip-path: inset(50%);
|
|
5263
|
+
white-space: nowrap;
|
|
5264
|
+
}
|
|
5132
5265
|
}
|
|
5133
5266
|
|
|
5134
5267
|
/* What a registry widget reports for its card header (`header` event): the
|
|
@@ -198,6 +198,7 @@ export default {
|
|
|
198
198
|
| `rowIndicatorCap` | Number | `3` | How many declared indicators render on the row before the rest move into the row menu, reachable as `indicatorsFor(row).overflow`. |
|
|
199
199
|
| `selectAllLabel` | String | `'Select all rows'` | Accessible name (`aria-label`) for the header select-all checkbox, so screen readers announce a named control (WCAG 4.1.2) |
|
|
200
200
|
| `selectRowLabel` | String | `'Select row'` | Accessible name (`aria-label`) for each per-row select checkbox, so screen readers announce a named control (WCAG 4.1.2) |
|
|
201
|
+
| `showUnread` | Boolean | `true` | Draw an unread record (`@self.unread`) in bold with the unread dot. Off, every row reads alike. CnIndexPage turns it off under the board look unless the page offers the Unread lens. |
|
|
201
202
|
| `rowTitle` | String | `'link'` | Board look only: how the title column draws its title. `link` underlines the title (not its secondary line). `plain` draws it as 15px bold text without an underline over a 13px muted secondary line, as the PqTickets board does; the row stays clickable. The Nextcloud look ignores it. |
|
|
202
203
|
| `hideHeader` | Boolean | `false` | Hide the column-header row (`<thead>`) — for compact dashboard list widgets that want a plain bordered-row list without column labels |
|
|
203
204
|
| `pinnedCount` | Number | `0` | How many data columns are pinned to the start of the table. They, and the selection and icon columns, stay in view when the table scrolls sideways |
|