@conduction/nextcloud-vue 2.59.0 → 2.60.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 +14 -1
- package/dist/esm/components/CnActionsMenu/CnActionsMenu.vue2.js.map +1 -1
- package/dist/esm/components/CnActionsMenu/CnActionsMenu.vue3.js.map +1 -1
- package/dist/esm/components/CnAppNav/CnAppNav.vue2.js +39 -0
- package/dist/esm/components/CnAppNav/CnAppNav.vue2.js.map +1 -1
- package/dist/esm/components/CnAppNav/CnAppNav.vue3.js +46 -4
- package/dist/esm/components/CnAppNav/CnAppNav.vue3.js.map +1 -1
- package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js +224 -9
- package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue3.js +79 -25
- package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnBannerWidgetForm/CnBannerWidgetForm.vue2.js +42 -3
- package/dist/esm/components/CnBannerWidgetForm/CnBannerWidgetForm.vue2.js.map +1 -1
- package/dist/esm/components/CnBannerWidgetForm/CnBannerWidgetForm.vue3.js +39 -6
- package/dist/esm/components/CnBannerWidgetForm/CnBannerWidgetForm.vue3.js.map +1 -1
- package/dist/esm/components/CnBoardView/CnBoardView.vue2.js +46 -0
- package/dist/esm/components/CnBoardView/CnBoardView.vue2.js.map +1 -1
- package/dist/esm/components/CnBoardView/CnBoardView.vue3.js +11 -4
- package/dist/esm/components/CnBoardView/CnBoardView.vue3.js.map +1 -1
- package/dist/esm/components/CnBrandStripe/CnBrandStripe.vue.js +10 -0
- package/dist/esm/components/CnBrandStripe/CnBrandStripe.vue.js.map +1 -0
- package/dist/esm/components/CnBrandStripe/CnBrandStripe.vue2.js +39 -0
- package/dist/esm/components/CnBrandStripe/CnBrandStripe.vue2.js.map +1 -0
- package/dist/esm/components/CnBrandStripe/CnBrandStripe.vue3.js +16 -0
- package/dist/esm/components/CnBrandStripe/CnBrandStripe.vue3.js.map +1 -0
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js +117 -1
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js.map +1 -1
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js +147 -89
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js.map +1 -1
- package/dist/esm/components/CnConversationThread/CnConversationThread.vue.js +10 -0
- package/dist/esm/components/CnConversationThread/CnConversationThread.vue.js.map +1 -0
- package/dist/esm/components/CnConversationThread/CnConversationThread.vue2.js +259 -0
- package/dist/esm/components/CnConversationThread/CnConversationThread.vue2.js.map +1 -0
- package/dist/esm/components/CnConversationThread/CnConversationThread.vue3.js +131 -0
- package/dist/esm/components/CnConversationThread/CnConversationThread.vue3.js.map +1 -0
- package/dist/esm/components/CnConversationThread/CnConversationWidget.vue.js +10 -0
- package/dist/esm/components/CnConversationThread/CnConversationWidget.vue.js.map +1 -0
- package/dist/esm/components/CnConversationThread/CnConversationWidget.vue2.js +116 -0
- package/dist/esm/components/CnConversationThread/CnConversationWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnConversationThread/CnConversationWidget.vue3.js +24 -0
- package/dist/esm/components/CnConversationThread/CnConversationWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +503 -28
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +300 -142
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
- package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewList.vue.js +10 -0
- package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewList.vue.js.map +1 -0
- package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewList.vue2.js +237 -0
- package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewList.vue2.js.map +1 -0
- package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewList.vue3.js +120 -0
- package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewList.vue3.js.map +1 -0
- package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewWidget.vue.js +10 -0
- package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewWidget.vue.js.map +1 -0
- package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewWidget.vue2.js +77 -0
- package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewWidget.vue3.js +27 -0
- package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue2.js +169 -12
- package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue3.js +15 -7
- package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnHeaderWidgetForm/CnHeaderWidgetForm.vue2.js +37 -3
- package/dist/esm/components/CnHeaderWidgetForm/CnHeaderWidgetForm.vue2.js.map +1 -1
- package/dist/esm/components/CnHeaderWidgetForm/CnHeaderWidgetForm.vue3.js +44 -16
- package/dist/esm/components/CnHeaderWidgetForm/CnHeaderWidgetForm.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +132 -3
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +6 -3
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
- package/dist/esm/components/CnNextStepCard/CnNextStepCard.vue.js +10 -0
- package/dist/esm/components/CnNextStepCard/CnNextStepCard.vue.js.map +1 -0
- package/dist/esm/components/CnNextStepCard/CnNextStepCard.vue2.js +150 -0
- package/dist/esm/components/CnNextStepCard/CnNextStepCard.vue2.js.map +1 -0
- package/dist/esm/components/CnNextStepCard/CnNextStepCard.vue3.js +106 -0
- package/dist/esm/components/CnNextStepCard/CnNextStepCard.vue3.js.map +1 -0
- package/dist/esm/components/CnNextStepCard/CnNextStepWidget.vue.js +9 -0
- package/dist/esm/components/CnNextStepCard/CnNextStepWidget.vue.js.map +1 -0
- package/dist/esm/components/CnNextStepCard/CnNextStepWidget.vue2.js +47 -0
- package/dist/esm/components/CnNextStepCard/CnNextStepWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnNextStepCard/CnNextStepWidget.vue3.js +17 -0
- package/dist/esm/components/CnNextStepCard/CnNextStepWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js +49 -0
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnObjectDataWidgetForm/CnObjectDataWidgetForm.vue2.js +6 -0
- package/dist/esm/components/CnObjectDataWidgetForm/CnObjectDataWidgetForm.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectDataWidgetForm/CnObjectDataWidgetForm.vue3.js.map +1 -1
- package/dist/esm/components/CnObjectKanban/CnObjectKanban.vue2.js +46 -0
- package/dist/esm/components/CnObjectKanban/CnObjectKanban.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectKanban/CnObjectKanban.vue3.js +15 -3
- package/dist/esm/components/CnObjectKanban/CnObjectKanban.vue3.js.map +1 -1
- package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue2.js +37 -1
- package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue2.js.map +1 -1
- package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue3.js +29 -13
- package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue3.js.map +1 -1
- package/dist/esm/components/CnSavedViewsControl/CnSavedViewsControl.vue2.js +50 -3
- package/dist/esm/components/CnSavedViewsControl/CnSavedViewsControl.vue2.js.map +1 -1
- package/dist/esm/components/CnSavedViewsControl/CnSavedViewsControl.vue3.js.map +1 -1
- package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue.js +10 -0
- package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue.js.map +1 -0
- package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue2.js +186 -0
- package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue2.js.map +1 -0
- package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue3.js +46 -0
- package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue3.js.map +1 -0
- package/dist/esm/components/CnStackedBarWidget/CnStackedBarWidget.vue.js +11 -0
- package/dist/esm/components/CnStackedBarWidget/CnStackedBarWidget.vue.js.map +1 -0
- package/dist/esm/components/CnStackedBarWidget/CnStackedBarWidget.vue2.js +298 -0
- package/dist/esm/components/CnStackedBarWidget/CnStackedBarWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnStackedBarWidget/CnStackedBarWidget.vue3.js +77 -0
- package/dist/esm/components/CnStackedBarWidget/CnStackedBarWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnStackedBarWidgetForm/CnStackedBarWidgetForm.vue.js +10 -0
- package/dist/esm/components/CnStackedBarWidgetForm/CnStackedBarWidgetForm.vue.js.map +1 -0
- package/dist/esm/components/CnStackedBarWidgetForm/CnStackedBarWidgetForm.vue2.js +169 -0
- package/dist/esm/components/CnStackedBarWidgetForm/CnStackedBarWidgetForm.vue2.js.map +1 -0
- package/dist/esm/components/CnStackedBarWidgetForm/CnStackedBarWidgetForm.vue3.js +48 -0
- package/dist/esm/components/CnStackedBarWidgetForm/CnStackedBarWidgetForm.vue3.js.map +1 -0
- package/dist/esm/components/CnTabs/CnTab.vue2.js +35 -0
- package/dist/esm/components/CnTabs/CnTab.vue2.js.map +1 -1
- package/dist/esm/components/CnTabs/CnTab.vue3.js.map +1 -1
- package/dist/esm/components/CnTabs/CnTabs.vue2.js +120 -7
- package/dist/esm/components/CnTabs/CnTabs.vue2.js.map +1 -1
- package/dist/esm/components/CnTabs/CnTabs.vue3.js +57 -5
- package/dist/esm/components/CnTabs/CnTabs.vue3.js.map +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +59 -4
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js +6 -2
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue.js +10 -0
- package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue.js.map +1 -0
- package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue2.js +484 -0
- package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue3.js +115 -0
- package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnWeekStripWidgetForm/CnWeekStripWidgetForm.vue.js +10 -0
- package/dist/esm/components/CnWeekStripWidgetForm/CnWeekStripWidgetForm.vue.js.map +1 -0
- package/dist/esm/components/CnWeekStripWidgetForm/CnWeekStripWidgetForm.vue2.js +189 -0
- package/dist/esm/components/CnWeekStripWidgetForm/CnWeekStripWidgetForm.vue2.js.map +1 -0
- package/dist/esm/components/CnWeekStripWidgetForm/CnWeekStripWidgetForm.vue3.js +74 -0
- package/dist/esm/components/CnWeekStripWidgetForm/CnWeekStripWidgetForm.vue3.js.map +1 -0
- package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js +82 -8
- package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js.map +1 -1
- package/dist/esm/index.js +10 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/l10n/en.json.js +54 -1
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +54 -1
- package/dist/esm/l10n/nl.json.js.map +1 -1
- package/dist/esm/nextcloud-vue.css +931 -4
- package/dist/esm/utils/dateVariant.js +124 -0
- package/dist/esm/utils/dateVariant.js.map +1 -0
- package/dist/esm/utils/detailActionModel.js +260 -0
- package/dist/esm/utils/detailActionModel.js.map +1 -0
- package/dist/esm/utils/dueRule.js +84 -0
- package/dist/esm/utils/dueRule.js.map +1 -0
- package/dist/esm/utils/fetchAggregate.js +37 -1
- package/dist/esm/utils/fetchAggregate.js.map +1 -1
- package/dist/esm/utils/fetchFilterCounts.js +186 -0
- package/dist/esm/utils/fetchFilterCounts.js.map +1 -0
- package/dist/esm/utils/libraryWidgetKeys.js +5 -0
- package/dist/esm/utils/libraryWidgetKeys.js.map +1 -1
- package/dist/esm/utils/readPath.js +30 -0
- package/dist/esm/utils/readPath.js.map +1 -0
- 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 +14970 -8991
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +931 -4
- package/l10n/en.json +54 -1
- package/l10n/nl.json +54 -1
- package/package.json +1 -1
- package/scripts/.jsdoc-baselines.json +3 -0
- package/src/components/CnActionsMenu/CnActionsMenu.vue +14 -1
- package/src/components/CnAppNav/CnAppNav.md +26 -0
- package/src/components/CnAppNav/CnAppNav.vue +104 -1
- package/src/components/CnBannerWidget/CnBannerWidget.vue +365 -10
- package/src/components/CnBannerWidgetForm/CnBannerWidgetForm.vue +65 -3
- package/src/components/CnBoardView/CnBoardView.vue +77 -0
- package/src/components/CnBrandStripe/CnBrandStripe.md +22 -0
- package/src/components/CnBrandStripe/CnBrandStripe.vue +100 -0
- package/src/components/CnBrandStripe/index.js +4 -0
- package/src/components/CnCellRenderer/CnCellRenderer.vue +207 -1
- package/src/components/CnConversationThread/CnConversationThread.md +51 -0
- package/src/components/CnConversationThread/CnConversationThread.vue +457 -0
- package/src/components/CnConversationThread/CnConversationWidget.vue +144 -0
- package/src/components/CnConversationThread/index.js +4 -0
- package/src/components/CnDetailPage/CnDetailPage.md +16 -0
- package/src/components/CnDetailPage/CnDetailPage.vue +683 -48
- package/src/components/CnDocumentReviewList/CnDocumentReviewList.md +55 -0
- package/src/components/CnDocumentReviewList/CnDocumentReviewList.vue +384 -0
- package/src/components/CnDocumentReviewList/CnDocumentReviewWidget.vue +108 -0
- package/src/components/CnDocumentReviewList/index.js +4 -0
- package/src/components/CnHeaderWidget/CnHeaderWidget.vue +189 -14
- package/src/components/CnHeaderWidgetForm/CnHeaderWidgetForm.vue +53 -3
- package/src/components/CnIndexPage/CnIndexPage.vue +135 -3
- package/src/components/CnNextStepCard/CnNextStepCard.md +40 -0
- package/src/components/CnNextStepCard/CnNextStepCard.vue +319 -0
- package/src/components/CnNextStepCard/CnNextStepWidget.vue +59 -0
- package/src/components/CnNextStepCard/index.js +4 -0
- package/src/components/CnObjectDataWidget/CnObjectDataWidget.vue +49 -0
- package/src/components/CnObjectDataWidgetForm/CnObjectDataWidgetForm.vue +6 -0
- package/src/components/CnObjectKanban/CnObjectKanban.vue +86 -2
- package/src/components/CnQuickFilterBar/CnQuickFilterBar.md +3 -0
- package/src/components/CnQuickFilterBar/CnQuickFilterBar.vue +60 -1
- package/src/components/CnSavedViewsControl/CnSavedViewsControl.vue +50 -3
- package/src/components/CnSegmentedControl/CnSegmentedControl.md +46 -0
- package/src/components/CnSegmentedControl/CnSegmentedControl.vue +291 -0
- package/src/components/CnSegmentedControl/index.js +4 -0
- package/src/components/CnStackedBarWidget/CnStackedBarWidget.md +15 -0
- package/src/components/CnStackedBarWidget/CnStackedBarWidget.vue +414 -0
- package/src/components/CnStackedBarWidget/index.js +14 -0
- package/src/components/CnStackedBarWidgetForm/CnStackedBarWidgetForm.vue +224 -0
- package/src/components/CnStackedBarWidgetForm/index.js +4 -0
- package/src/components/CnTabs/CnTab.vue +35 -0
- package/src/components/CnTabs/CnTabs.vue +244 -10
- package/src/components/CnTabsWidget/CnTabsWidget.vue +63 -4
- package/src/components/CnWeekStripWidget/CnWeekStripWidget.md +31 -0
- package/src/components/CnWeekStripWidget/CnWeekStripWidget.vue +668 -0
- package/src/components/CnWeekStripWidget/index.js +14 -0
- package/src/components/CnWeekStripWidgetForm/CnWeekStripWidgetForm.vue +265 -0
- package/src/components/CnWeekStripWidgetForm/index.js +4 -0
- package/src/components/CnWidgetGrid/registerDashboardWidgets.js +74 -0
- package/src/components/index.js +11 -0
- package/src/css/detail-page.css +47 -0
- package/src/index.js +9 -0
- package/src/schemas/app-manifest-v2.schema.json +389 -1
- package/src/schemas/app-manifest.schema.json +2 -1
- package/src/utils/dateVariant.js +120 -0
- package/src/utils/detailActionModel.js +256 -0
- package/src/utils/dueRule.js +94 -0
- package/src/utils/fetchAggregate.js +36 -0
- package/src/utils/fetchFilterCounts.js +182 -0
- package/src/utils/index.js +1 -0
- package/src/utils/libraryWidgetKeys.js +5 -0
- package/src/utils/readPath.js +27 -0
- package/src/utils/validateManifestV2.compiled.js +2 -2
|
@@ -4,7 +4,39 @@
|
|
|
4
4
|
-->
|
|
5
5
|
|
|
6
6
|
<template>
|
|
7
|
-
<
|
|
7
|
+
<section
|
|
8
|
+
v-if="visible && isAttention"
|
|
9
|
+
class="cn-banner-widget cn-banner-widget--attention"
|
|
10
|
+
:class="'cn-banner-widget--' + resolvedVariant"
|
|
11
|
+
:aria-labelledby="headingId"
|
|
12
|
+
data-testid="cn-banner-widget-attention">
|
|
13
|
+
<div class="cn-banner-widget__body">
|
|
14
|
+
<p v-if="resolvedKicker" class="cn-banner-widget__kicker">
|
|
15
|
+
{{ resolvedKicker }}
|
|
16
|
+
</p>
|
|
17
|
+
<h3 :id="headingId" class="cn-banner-widget__title">
|
|
18
|
+
{{ displayTitle }}
|
|
19
|
+
</h3>
|
|
20
|
+
<p v-if="displayReason" class="cn-banner-widget__reason">
|
|
21
|
+
{{ displayReason }}
|
|
22
|
+
</p>
|
|
23
|
+
</div>
|
|
24
|
+
<div v-if="resolvedActions.length > 0" class="cn-banner-widget__actions">
|
|
25
|
+
<component
|
|
26
|
+
:is="action.href ? 'a' : 'button'"
|
|
27
|
+
v-for="action in resolvedActions"
|
|
28
|
+
:key="action.key"
|
|
29
|
+
class="cn-banner-widget__action"
|
|
30
|
+
:class="action.primary ? 'cn-banner-widget__action--primary' : 'cn-banner-widget__action--secondary'"
|
|
31
|
+
:href="action.href || null"
|
|
32
|
+
:type="action.href ? null : 'button'"
|
|
33
|
+
data-testid="cn-banner-widget-action"
|
|
34
|
+
@click="onActionClick($event, action)">
|
|
35
|
+
{{ action.label }}
|
|
36
|
+
</component>
|
|
37
|
+
</div>
|
|
38
|
+
</section>
|
|
39
|
+
<div v-else-if="visible" class="cn-banner-widget">
|
|
8
40
|
<NcNoteCard :type="resolvedVariant" class="cn-banner-widget__card">
|
|
9
41
|
<component
|
|
10
42
|
:is="clickable ? 'a' : 'span'"
|
|
@@ -22,11 +54,19 @@
|
|
|
22
54
|
<script>
|
|
23
55
|
import { NcNoteCard } from '@nextcloud/vue'
|
|
24
56
|
import { followLinkClick, resolveHref } from '../../utils/linkNavigation.js'
|
|
57
|
+
import { safeHref } from '../../utils/safeHref.js'
|
|
58
|
+
import { nextUid } from '../../utils/uid.js'
|
|
25
59
|
import { compareVisibleWhen, readVisibleWhenValue } from '../../utils/visibleWhen.js'
|
|
26
60
|
|
|
27
61
|
/** Variants understood by NcNoteCard. */
|
|
28
62
|
const VARIANTS = ['info', 'warning', 'error', 'success']
|
|
29
63
|
|
|
64
|
+
/** Layouts: the note card (default) or the attention card. */
|
|
65
|
+
const LAYOUTS = ['banner', 'attention']
|
|
66
|
+
|
|
67
|
+
/** An attention card offers a primary and a secondary action, no more. */
|
|
68
|
+
const MAX_ACTIONS = 2
|
|
69
|
+
|
|
30
70
|
/**
|
|
31
71
|
* CnBannerWidget — declarative notice banner for dashboards and v2 pages
|
|
32
72
|
* (`banner` widget type, Wave 1 of nextcloud-vue#91).
|
|
@@ -56,6 +96,29 @@ const VARIANTS = ['info', 'warning', 'error', 'success']
|
|
|
56
96
|
* Fail-safe: with a `visibleWhen`, the banner stays HIDDEN until the
|
|
57
97
|
* condition evaluates true — a failed fetch never breaks (or spams) a
|
|
58
98
|
* dashboard.
|
|
99
|
+
*
|
|
100
|
+
* `layout: "attention"` turns the banner into an attention card: a card with
|
|
101
|
+
* a coloured edge by severity, a `kicker` label, a `title`, a `reason` line
|
|
102
|
+
* and up to two `actions` (the first is the primary one unless an action says
|
|
103
|
+
* `primary` itself). `visibleWhen` works the same way. Without `layout` the
|
|
104
|
+
* note card above renders, as it always did.
|
|
105
|
+
*
|
|
106
|
+
* ```json
|
|
107
|
+
* {
|
|
108
|
+
* "widgetKey": "banner",
|
|
109
|
+
* "props": {
|
|
110
|
+
* "layout": "attention",
|
|
111
|
+
* "variant": "error",
|
|
112
|
+
* "kicker": "First today",
|
|
113
|
+
* "title": "Parking permits city centre",
|
|
114
|
+
* "reason": "The deadline ends today.",
|
|
115
|
+
* "actions": [
|
|
116
|
+
* { "label": "Open case", "route": { "name": "CaseDetail", "params": { "id": "2026-0061" } } },
|
|
117
|
+
* { "label": "Suspend deadline", "route": "CaseSuspend" }
|
|
118
|
+
* ]
|
|
119
|
+
* }
|
|
120
|
+
* }
|
|
121
|
+
* ```
|
|
59
122
|
*/
|
|
60
123
|
export default {
|
|
61
124
|
name: 'CnBannerWidget',
|
|
@@ -111,10 +174,57 @@ export default {
|
|
|
111
174
|
default: null,
|
|
112
175
|
},
|
|
113
176
|
|
|
177
|
+
/**
|
|
178
|
+
* How the banner renders: `banner` (the note card, default) or
|
|
179
|
+
* `attention` (a card with a severity edge, kicker, title, reason and
|
|
180
|
+
* up to two actions). Empty falls back to the `content` blob, then
|
|
181
|
+
* `banner`.
|
|
182
|
+
*
|
|
183
|
+
* @type {''|'banner'|'attention'}
|
|
184
|
+
*/
|
|
185
|
+
layout: {
|
|
186
|
+
type: String,
|
|
187
|
+
default: '',
|
|
188
|
+
validator: (v) => v === '' || LAYOUTS.includes(v),
|
|
189
|
+
},
|
|
190
|
+
|
|
191
|
+
/** Attention card: the small label above the title ("First today"). */
|
|
192
|
+
kicker: {
|
|
193
|
+
type: String,
|
|
194
|
+
default: '',
|
|
195
|
+
},
|
|
196
|
+
|
|
197
|
+
/** Attention card: the title. Falls back to `text` when empty. */
|
|
198
|
+
title: {
|
|
199
|
+
type: String,
|
|
200
|
+
default: '',
|
|
201
|
+
},
|
|
202
|
+
|
|
203
|
+
/** Attention card: the line under the title that says why this needs attention. `{value}` is replaced like in `text`. */
|
|
204
|
+
reason: {
|
|
205
|
+
type: String,
|
|
206
|
+
default: '',
|
|
207
|
+
},
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Attention card: at most two actions. Each is `{ label, route?,
|
|
211
|
+
* href?, primary?, id? }`: `route` is a route name or location,
|
|
212
|
+
* `href` an URL. An action with neither renders a button that emits
|
|
213
|
+
* `action`. The first action is the primary one unless one sets
|
|
214
|
+
* `primary: true`.
|
|
215
|
+
*
|
|
216
|
+
* @type {Array<{label: string, route?: (string|object), href?: string, primary?: boolean, id?: string}>|null}
|
|
217
|
+
*/
|
|
218
|
+
actions: {
|
|
219
|
+
type: Array,
|
|
220
|
+
default: null,
|
|
221
|
+
},
|
|
222
|
+
|
|
114
223
|
/**
|
|
115
224
|
* Stored content blob (CnDashboardPage registry branch) carrying the
|
|
116
|
-
* same keys as the flat props: `{ variant, text, visibleWhen, route
|
|
117
|
-
* Explicit flat props win
|
|
225
|
+
* same keys as the flat props: `{ variant, text, visibleWhen, route,
|
|
226
|
+
* layout, kicker, title, reason, actions }`. Explicit flat props win
|
|
227
|
+
* on collision.
|
|
118
228
|
*/
|
|
119
229
|
content: {
|
|
120
230
|
type: Object,
|
|
@@ -138,8 +248,21 @@ export default {
|
|
|
138
248
|
},
|
|
139
249
|
},
|
|
140
250
|
|
|
251
|
+
emits: [
|
|
252
|
+
/**
|
|
253
|
+
* Emitted when an attention-card action without a `route` or `href`
|
|
254
|
+
* is clicked, so the host can run it.
|
|
255
|
+
*
|
|
256
|
+
* @event action
|
|
257
|
+
* @type {{id: (string|undefined), label: string, index: number}}
|
|
258
|
+
*/
|
|
259
|
+
'action',
|
|
260
|
+
],
|
|
261
|
+
|
|
141
262
|
data() {
|
|
142
263
|
return {
|
|
264
|
+
/** Id that ties the attention card's region to its title. */
|
|
265
|
+
headingId: `cn-banner-title-${nextUid()}`,
|
|
143
266
|
/** Evaluated visibleWhen outcome (null = not yet evaluated). */
|
|
144
267
|
conditionMet: null,
|
|
145
268
|
/**
|
|
@@ -179,13 +302,11 @@ export default {
|
|
|
179
302
|
* the text renders as written — a `{value}` placeholder only makes
|
|
180
303
|
* sense on a conditional banner, which stays hidden until its
|
|
181
304
|
* predicate (and therefore its value) resolves.
|
|
305
|
+
*
|
|
306
|
+
* @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-attention-card
|
|
182
307
|
*/
|
|
183
308
|
displayText() {
|
|
184
|
-
|
|
185
|
-
if (this.conditionValue === null || !text.includes('{value}')) {
|
|
186
|
-
return text
|
|
187
|
-
}
|
|
188
|
-
return text.replaceAll('{value}', String(this.conditionValue))
|
|
309
|
+
return this.fillValue(this.resolvedText)
|
|
189
310
|
},
|
|
190
311
|
|
|
191
312
|
/** The effective click-through route (prop → content → null). */
|
|
@@ -193,9 +314,101 @@ export default {
|
|
|
193
314
|
return this.route || (this.content && this.content.route) || null
|
|
194
315
|
},
|
|
195
316
|
|
|
196
|
-
/**
|
|
317
|
+
/**
|
|
318
|
+
* Whether the attention card renders instead of the note card.
|
|
319
|
+
*
|
|
320
|
+
* @return {boolean}
|
|
321
|
+
* @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-attention-card
|
|
322
|
+
*/
|
|
323
|
+
isAttention() {
|
|
324
|
+
return (this.layout || (this.content && this.content.layout) || 'banner') === 'attention'
|
|
325
|
+
},
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* The attention card's kicker (prop, else content).
|
|
329
|
+
*
|
|
330
|
+
* @return {string}
|
|
331
|
+
* @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-attention-card
|
|
332
|
+
*/
|
|
333
|
+
resolvedKicker() {
|
|
334
|
+
const kicker = this.kicker || (this.content && this.content.kicker) || ''
|
|
335
|
+
return kicker
|
|
336
|
+
},
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* The attention card's title: `title`, else `text`, with `{value}` filled in.
|
|
340
|
+
*
|
|
341
|
+
* @return {string}
|
|
342
|
+
* @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-attention-card
|
|
343
|
+
*/
|
|
344
|
+
displayTitle() {
|
|
345
|
+
const title = this.title || (this.content && this.content.title) || ''
|
|
346
|
+
return title ? this.fillValue(title) : this.displayText
|
|
347
|
+
},
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* The attention card's reason line. When the title already used
|
|
351
|
+
* `text`, the text is not repeated here.
|
|
352
|
+
*
|
|
353
|
+
* @return {string}
|
|
354
|
+
* @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-attention-card
|
|
355
|
+
*/
|
|
356
|
+
displayReason() {
|
|
357
|
+
const reason = this.reason || (this.content && this.content.reason) || ''
|
|
358
|
+
if (reason) {
|
|
359
|
+
return this.fillValue(reason)
|
|
360
|
+
}
|
|
361
|
+
const title = this.title || (this.content && this.content.title) || ''
|
|
362
|
+
return title ? this.displayText : ''
|
|
363
|
+
},
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* The attention card's actions: the first two that have a label, each
|
|
367
|
+
* with its href and whether it is the primary one.
|
|
368
|
+
*
|
|
369
|
+
* @return {Array<{key: string, id: (string|undefined), label: string, href: string, target: (object|null), primary: boolean, index: number}>}
|
|
370
|
+
* @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-attention-card
|
|
371
|
+
*/
|
|
372
|
+
resolvedActions() {
|
|
373
|
+
const raw = this.actions || (this.content && this.content.actions) || []
|
|
374
|
+
if (!Array.isArray(raw)) {
|
|
375
|
+
return []
|
|
376
|
+
}
|
|
377
|
+
const usable = raw
|
|
378
|
+
.filter((action) => action && typeof action === 'object' && typeof action.label === 'string' && action.label !== '')
|
|
379
|
+
.slice(0, MAX_ACTIONS)
|
|
380
|
+
const declaredPrimary = usable.findIndex((action) => action.primary === true)
|
|
381
|
+
const primaryIndex = declaredPrimary === -1 ? 0 : declaredPrimary
|
|
382
|
+
return usable.map((action, index) => {
|
|
383
|
+
let target = null
|
|
384
|
+
let href = ''
|
|
385
|
+
if (action.route) {
|
|
386
|
+
target = typeof action.route === 'string' ? { name: action.route } : action.route
|
|
387
|
+
href = resolveHref(target, this.$router)
|
|
388
|
+
} else if (typeof action.href === 'string' && action.href !== '') {
|
|
389
|
+
const safe = safeHref(action.href)
|
|
390
|
+
href = safe === '#' ? '' : safe
|
|
391
|
+
}
|
|
392
|
+
return {
|
|
393
|
+
key: `${index}-${action.id || action.label}`,
|
|
394
|
+
id: action.id,
|
|
395
|
+
label: action.label,
|
|
396
|
+
href,
|
|
397
|
+
target,
|
|
398
|
+
primary: index === primaryIndex,
|
|
399
|
+
index,
|
|
400
|
+
}
|
|
401
|
+
})
|
|
402
|
+
},
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Whether the banner renders: no condition = always; else the evaluated outcome.
|
|
406
|
+
*
|
|
407
|
+
* @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-attention-card
|
|
408
|
+
*/
|
|
197
409
|
visible() {
|
|
198
|
-
|
|
410
|
+
const title = this.title || (this.content && this.content.title) || ''
|
|
411
|
+
if (this.resolvedText === '' && !(this.isAttention && title !== '')) {
|
|
199
412
|
return false
|
|
200
413
|
}
|
|
201
414
|
if (!this.resolvedVisibleWhen) {
|
|
@@ -235,6 +448,40 @@ export default {
|
|
|
235
448
|
},
|
|
236
449
|
|
|
237
450
|
methods: {
|
|
451
|
+
/**
|
|
452
|
+
* Replace `{value}` in a text by the value the predicate read, once
|
|
453
|
+
* one is known.
|
|
454
|
+
*
|
|
455
|
+
* @param {string} text The text.
|
|
456
|
+
* @return {string} The text with the value filled in.
|
|
457
|
+
* @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-attention-card
|
|
458
|
+
*/
|
|
459
|
+
fillValue(text) {
|
|
460
|
+
if (this.conditionValue === null || !text.includes('{value}')) {
|
|
461
|
+
return text
|
|
462
|
+
}
|
|
463
|
+
return text.replaceAll('{value}', String(this.conditionValue))
|
|
464
|
+
},
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Run an attention-card action: route an in-app link, leave an
|
|
468
|
+
* external link to the browser, and emit `action` for a button.
|
|
469
|
+
*
|
|
470
|
+
* @param {MouseEvent} event The click.
|
|
471
|
+
* @param {object} action The resolved action.
|
|
472
|
+
* @return {void}
|
|
473
|
+
* @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-attention-card
|
|
474
|
+
*/
|
|
475
|
+
onActionClick(event, action) {
|
|
476
|
+
if (action.target) {
|
|
477
|
+
followLinkClick(event, action.target, this.$router)
|
|
478
|
+
return
|
|
479
|
+
}
|
|
480
|
+
if (!action.href) {
|
|
481
|
+
this.$emit('action', { id: action.id, label: action.label, index: action.index })
|
|
482
|
+
}
|
|
483
|
+
},
|
|
484
|
+
|
|
238
485
|
/**
|
|
239
486
|
* Resolve the `visibleWhen` verdict. A host-injected
|
|
240
487
|
* `conditionOutcome` wins outright — the host already ran the
|
|
@@ -302,4 +549,112 @@ export default {
|
|
|
302
549
|
text-decoration: underline;
|
|
303
550
|
color: inherit;
|
|
304
551
|
}
|
|
552
|
+
|
|
553
|
+
/* Attention card. The severity edge is an inset shadow, so it takes no width. */
|
|
554
|
+
.cn-banner-widget--attention {
|
|
555
|
+
--cn-banner-accent: var(--color-primary-element);
|
|
556
|
+
--cn-banner-accent-text: var(--color-primary-element);
|
|
557
|
+
display: flex;
|
|
558
|
+
flex-wrap: wrap;
|
|
559
|
+
align-items: center;
|
|
560
|
+
gap: 16px 24px;
|
|
561
|
+
box-sizing: border-box;
|
|
562
|
+
padding: 20px 24px 20px 28px;
|
|
563
|
+
border: 1px solid var(--color-border);
|
|
564
|
+
border-radius: var(--border-radius-container, var(--border-radius-large, 12px));
|
|
565
|
+
background: var(--color-main-background);
|
|
566
|
+
box-shadow: inset 4px 0 0 0 var(--cn-banner-accent);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
.cn-banner-widget--attention.cn-banner-widget--warning {
|
|
570
|
+
--cn-banner-accent: var(--color-element-warning, var(--color-warning));
|
|
571
|
+
--cn-banner-accent-text: var(--color-text-warning, var(--color-warning-text, var(--color-main-text)));
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
.cn-banner-widget--attention.cn-banner-widget--error {
|
|
575
|
+
--cn-banner-accent: var(--color-element-error, var(--color-error));
|
|
576
|
+
--cn-banner-accent-text: var(--color-text-error, var(--color-error-text));
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
.cn-banner-widget--attention.cn-banner-widget--success {
|
|
580
|
+
--cn-banner-accent: var(--color-element-success, var(--color-success));
|
|
581
|
+
--cn-banner-accent-text: var(--color-text-success, var(--color-success-text));
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
.cn-banner-widget__body {
|
|
585
|
+
display: flex;
|
|
586
|
+
flex: 1 1 320px;
|
|
587
|
+
flex-direction: column;
|
|
588
|
+
gap: 6px;
|
|
589
|
+
min-width: 0;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
.cn-banner-widget__kicker {
|
|
593
|
+
margin: 0;
|
|
594
|
+
font-size: 0.85em;
|
|
595
|
+
font-weight: 700;
|
|
596
|
+
letter-spacing: 0.06em;
|
|
597
|
+
text-transform: uppercase;
|
|
598
|
+
color: var(--cn-banner-accent-text);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
.cn-banner-widget__title {
|
|
602
|
+
margin: 0;
|
|
603
|
+
font-size: 1.3em;
|
|
604
|
+
font-weight: 700;
|
|
605
|
+
line-height: 1.3;
|
|
606
|
+
color: var(--color-main-text);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
.cn-banner-widget__reason {
|
|
610
|
+
margin: 0;
|
|
611
|
+
color: var(--color-text-maxcontrast);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
.cn-banner-widget__actions {
|
|
615
|
+
display: flex;
|
|
616
|
+
flex-wrap: wrap;
|
|
617
|
+
gap: 10px;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
.cn-banner-widget__action {
|
|
621
|
+
display: inline-flex;
|
|
622
|
+
align-items: center;
|
|
623
|
+
justify-content: center;
|
|
624
|
+
box-sizing: border-box;
|
|
625
|
+
min-height: var(--default-clickable-area, 40px);
|
|
626
|
+
margin: 0;
|
|
627
|
+
padding: 0 16px;
|
|
628
|
+
border: 1px solid var(--color-border-maxcontrast);
|
|
629
|
+
border-radius: var(--border-radius-element, var(--border-radius-large, 8px));
|
|
630
|
+
font: inherit;
|
|
631
|
+
font-weight: 600;
|
|
632
|
+
text-decoration: none;
|
|
633
|
+
cursor: pointer;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
.cn-banner-widget__action--secondary {
|
|
637
|
+
background: var(--color-main-background);
|
|
638
|
+
color: var(--color-main-text);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
.cn-banner-widget__action--secondary:hover {
|
|
642
|
+
background: var(--color-background-hover);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
.cn-banner-widget__action--primary {
|
|
646
|
+
border-color: var(--color-primary-element);
|
|
647
|
+
background: var(--color-primary-element);
|
|
648
|
+
color: var(--color-primary-element-text);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
.cn-banner-widget__action--primary:hover {
|
|
652
|
+
border-color: var(--color-primary-element-hover);
|
|
653
|
+
background: var(--color-primary-element-hover);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
.cn-banner-widget__action:focus-visible {
|
|
657
|
+
outline: 2px solid var(--color-main-text);
|
|
658
|
+
outline-offset: 2px;
|
|
659
|
+
}
|
|
305
660
|
</style>
|
|
@@ -5,6 +5,26 @@
|
|
|
5
5
|
|
|
6
6
|
<template>
|
|
7
7
|
<div class="cn-banner-form">
|
|
8
|
+
<NcSelect
|
|
9
|
+
:modelValue="layout"
|
|
10
|
+
:options="layoutOptions"
|
|
11
|
+
:inputLabel="t('nextcloud-vue', 'Layout')"
|
|
12
|
+
:clearable="false"
|
|
13
|
+
@update:modelValue="update('layout', $event)" />
|
|
14
|
+
<template v-if="layout === 'attention'">
|
|
15
|
+
<NcTextField
|
|
16
|
+
:modelValue="kicker"
|
|
17
|
+
:label="t('nextcloud-vue', 'Label above the title')"
|
|
18
|
+
@update:modelValue="update('kicker', $event)" />
|
|
19
|
+
<NcTextField
|
|
20
|
+
:modelValue="title"
|
|
21
|
+
:label="t('nextcloud-vue', 'Title')"
|
|
22
|
+
@update:modelValue="update('title', $event)" />
|
|
23
|
+
<NcTextField
|
|
24
|
+
:modelValue="reason"
|
|
25
|
+
:label="t('nextcloud-vue', 'Reason (why this needs attention)')"
|
|
26
|
+
@update:modelValue="update('reason', $event)" />
|
|
27
|
+
</template>
|
|
8
28
|
<NcTextField
|
|
9
29
|
:modelValue="text"
|
|
10
30
|
:label="t('nextcloud-vue', 'Banner text')"
|
|
@@ -22,6 +42,9 @@
|
|
|
22
42
|
<p class="cn-banner-form__hint">
|
|
23
43
|
{{ t('nextcloud-vue', 'A conditional banner (visibleWhen) is declared in the manifest.') }}
|
|
24
44
|
</p>
|
|
45
|
+
<p v-if="layout === 'attention'" class="cn-banner-form__hint">
|
|
46
|
+
{{ t('nextcloud-vue', 'The actions of an attention card are declared in the manifest.') }}
|
|
47
|
+
</p>
|
|
25
48
|
</div>
|
|
26
49
|
</template>
|
|
27
50
|
|
|
@@ -34,7 +57,9 @@ import { NcSelect, NcTextField } from '@nextcloud/vue'
|
|
|
34
57
|
*
|
|
35
58
|
* Edits the banner text, the severity variant, and the optional
|
|
36
59
|
* click-through route (a page id). The `visibleWhen` condition is a
|
|
37
|
-
* manifest-authored construct and is round-tripped untouched.
|
|
60
|
+
* manifest-authored construct and is round-tripped untouched. With the
|
|
61
|
+
* `attention` layout it also edits the kicker, the title and the reason; the
|
|
62
|
+
* card's `actions` are manifest-authored and round-tripped too. Emits
|
|
38
63
|
* `update:content` with the assembled blob on every change; `validate()`
|
|
39
64
|
* requires a non-empty text. Used by both `CnAddWidgetModal` and the cog
|
|
40
65
|
* `CnWidgetStyleEditorModal`.
|
|
@@ -85,6 +110,12 @@ export default {
|
|
|
85
110
|
route: typeof initial.route === 'string' ? initial.route : '',
|
|
86
111
|
// Round-tripped untouched — the form doesn't edit conditions.
|
|
87
112
|
visibleWhen: initial.visibleWhen ?? null,
|
|
113
|
+
layout: initial.layout === 'attention' ? 'attention' : 'banner',
|
|
114
|
+
kicker: initial.kicker ?? '',
|
|
115
|
+
title: initial.title ?? '',
|
|
116
|
+
reason: initial.reason ?? '',
|
|
117
|
+
// Round-tripped untouched: actions are written in the manifest.
|
|
118
|
+
actions: Array.isArray(initial.actions) ? initial.actions : null,
|
|
88
119
|
}
|
|
89
120
|
},
|
|
90
121
|
|
|
@@ -94,14 +125,40 @@ export default {
|
|
|
94
125
|
return ['info', 'warning', 'error']
|
|
95
126
|
},
|
|
96
127
|
|
|
97
|
-
/**
|
|
128
|
+
/**
|
|
129
|
+
* Selectable layouts: the note card or the attention card.
|
|
130
|
+
*
|
|
131
|
+
* @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-attention-card
|
|
132
|
+
*/
|
|
133
|
+
layoutOptions() {
|
|
134
|
+
return ['banner', 'attention']
|
|
135
|
+
},
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* The assembled content blob from the current field values. The
|
|
139
|
+
* attention keys are only written for an attention card, so a plain
|
|
140
|
+
* banner's stored content stays what it always was.
|
|
141
|
+
*
|
|
142
|
+
* @return {object}
|
|
143
|
+
* @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-attention-card
|
|
144
|
+
*/
|
|
98
145
|
assembledContent() {
|
|
99
|
-
|
|
146
|
+
const content = {
|
|
100
147
|
text: this.text,
|
|
101
148
|
variant: this.variant,
|
|
102
149
|
route: this.route || null,
|
|
103
150
|
visibleWhen: this.visibleWhen,
|
|
104
151
|
}
|
|
152
|
+
if (this.layout === 'attention') {
|
|
153
|
+
content.layout = 'attention'
|
|
154
|
+
content.kicker = this.kicker
|
|
155
|
+
content.title = this.title
|
|
156
|
+
content.reason = this.reason
|
|
157
|
+
if (this.actions !== null) {
|
|
158
|
+
content.actions = this.actions
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return content
|
|
105
162
|
},
|
|
106
163
|
},
|
|
107
164
|
|
|
@@ -123,8 +180,13 @@ export default {
|
|
|
123
180
|
* Validate the form; an empty array means valid.
|
|
124
181
|
*
|
|
125
182
|
* @return {string[]} the validation errors.
|
|
183
|
+
* @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-attention-card
|
|
126
184
|
*/
|
|
127
185
|
validate() {
|
|
186
|
+
if (this.layout === 'attention') {
|
|
187
|
+
const hasHeadline = (this.title && this.title.trim() !== '') || (this.text && this.text.trim() !== '')
|
|
188
|
+
return hasHeadline ? [] : [t('nextcloud-vue', 'An attention card needs a title')]
|
|
189
|
+
}
|
|
128
190
|
if (!this.text || this.text.trim() === '') {
|
|
129
191
|
return [t('nextcloud-vue', 'A banner text is required')]
|
|
130
192
|
}
|
|
@@ -83,11 +83,19 @@
|
|
|
83
83
|
v-for="card in column.cards"
|
|
84
84
|
:key="`card-${cardKey(card)}`"
|
|
85
85
|
class="cn-board-view__card"
|
|
86
|
+
:class="dueState(card) ? `cn-board-view__card--${dueState(card)}` : null"
|
|
86
87
|
role="listitem"
|
|
87
88
|
data-testid="cn-board-card"
|
|
88
89
|
:data-card-id="cardKey(card)"
|
|
89
90
|
:draggable="canMove"
|
|
90
91
|
@dragstart="onDragStart(card, column, $event)">
|
|
92
|
+
<span
|
|
93
|
+
v-if="dueLabel(card)"
|
|
94
|
+
class="cn-board-view__due"
|
|
95
|
+
:class="`cn-board-view__due--${dueState(card)}`"
|
|
96
|
+
data-testid="cn-board-due">
|
|
97
|
+
{{ dueLabel(card) }}
|
|
98
|
+
</span>
|
|
91
99
|
<span
|
|
92
100
|
v-for="field in cardFields"
|
|
93
101
|
:key="`f-${cardKey(card)}-${field}`"
|
|
@@ -162,6 +170,7 @@ import { translate as t } from '@nextcloud/l10n'
|
|
|
162
170
|
import { buildBoardColumns } from '../../utils/boardColumns.js'
|
|
163
171
|
import { buildSwimlanes } from '../../utils/boardSwimlanes.js'
|
|
164
172
|
import { DROP_OUTCOMES, runBoardDrop } from '../../utils/boardTransition.js'
|
|
173
|
+
import { dueStateForRow } from '../../utils/dueRule.js'
|
|
165
174
|
import { isRowMiddleClick, preventMiddleClickAutoscroll } from '../../utils/rowAuxClick.js'
|
|
166
175
|
|
|
167
176
|
/**
|
|
@@ -252,6 +261,22 @@ export default {
|
|
|
252
261
|
type: Boolean,
|
|
253
262
|
default: false,
|
|
254
263
|
},
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Marks late cards: `{ field, soonDays? }`. `field` is the dot-path to
|
|
267
|
+
* a card's due date. A date before today gets an error edge and the
|
|
268
|
+
* label "Overdue"; a date within `soonDays` (default 3) gets "Due
|
|
269
|
+
* soon" in the warning colour. `null` (the default) marks nothing.
|
|
270
|
+
*
|
|
271
|
+
* The same rule shape the table's date cell uses, so a list and its
|
|
272
|
+
* board agree on what late means.
|
|
273
|
+
*
|
|
274
|
+
* @type {{field: string, soonDays?: number}|null}
|
|
275
|
+
*/
|
|
276
|
+
dueRule: {
|
|
277
|
+
type: Object,
|
|
278
|
+
default: null,
|
|
279
|
+
},
|
|
255
280
|
},
|
|
256
281
|
|
|
257
282
|
emits: ['card-click', 'card-aux-click', 'moved', 'refused', 'stale'],
|
|
@@ -264,6 +289,8 @@ export default {
|
|
|
264
289
|
refusal: '',
|
|
265
290
|
/** The card being dragged, and where from. */
|
|
266
291
|
dragging: null,
|
|
292
|
+
/** The day late marking measures from. */
|
|
293
|
+
today: new Date(),
|
|
267
294
|
}
|
|
268
295
|
},
|
|
269
296
|
|
|
@@ -336,6 +363,33 @@ export default {
|
|
|
336
363
|
|
|
337
364
|
preventMiddleClickAutoscroll,
|
|
338
365
|
|
|
366
|
+
/**
|
|
367
|
+
* A card's due state under `dueRule`.
|
|
368
|
+
*
|
|
369
|
+
* @param {object} card The card's row.
|
|
370
|
+
* @return {'overdue'|'soon'|'ok'|null} The state, or null when no rule applies.
|
|
371
|
+
* @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-late-marking-on-board-cards
|
|
372
|
+
*/
|
|
373
|
+
dueState(card) {
|
|
374
|
+
return dueStateForRow(card, this.dueRule, this.today)
|
|
375
|
+
},
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* The words a late card carries. Colour alone would not reach a reader
|
|
379
|
+
* who cannot see it, or cannot tell red from amber.
|
|
380
|
+
*
|
|
381
|
+
* @param {object} card The card's row.
|
|
382
|
+
* @return {string} "Overdue", "Due soon", or '' for a card that is fine.
|
|
383
|
+
* @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-late-marking-on-board-cards
|
|
384
|
+
*/
|
|
385
|
+
dueLabel(card) {
|
|
386
|
+
const state = this.dueState(card)
|
|
387
|
+
if (state === 'overdue') {
|
|
388
|
+
return t('nextcloud-vue', 'Overdue')
|
|
389
|
+
}
|
|
390
|
+
return state === 'soon' ? t('nextcloud-vue', 'Due soon') : ''
|
|
391
|
+
},
|
|
392
|
+
|
|
339
393
|
/**
|
|
340
394
|
* Open one card. The pointer and the keyboard both come through here,
|
|
341
395
|
* so a reader on Enter reaches the same record as a reader on click.
|
|
@@ -561,6 +615,29 @@ export default {
|
|
|
561
615
|
display: block;
|
|
562
616
|
}
|
|
563
617
|
|
|
618
|
+
/* A late card: an edge in the error colour, drawn as an inset shadow so the
|
|
619
|
+
card keeps its size and its rounded corners. The label below carries the
|
|
620
|
+
meaning; the edge only helps a sighted reader find it. */
|
|
621
|
+
.cn-board-view__card--overdue {
|
|
622
|
+
border-color: var(--color-error);
|
|
623
|
+
box-shadow: inset 3px 0 0 var(--color-error);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
.cn-board-view__due {
|
|
627
|
+
display: block;
|
|
628
|
+
font-size: 0.85em;
|
|
629
|
+
font-weight: 700;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
.cn-board-view__due--overdue {
|
|
633
|
+
color: var(--color-error-text, var(--color-error));
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
.cn-board-view__due--soon {
|
|
637
|
+
color: var(--color-warning-text, var(--color-main-text));
|
|
638
|
+
font-weight: 600;
|
|
639
|
+
}
|
|
640
|
+
|
|
564
641
|
/*
|
|
565
642
|
* The card's opening control. The cursor and the focus ring live here now,
|
|
566
643
|
* on the thing that actually does something, rather than on the box.
|