@conduction/nextcloud-vue 2.56.0-beta.6 → 2.56.0-beta.7
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
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
SPDX-FileCopyrightText: 2026 Conduction B.V.
|
|
3
|
+
SPDX-License-Identifier: EUPL-1.2
|
|
4
|
+
-->
|
|
5
|
+
<template>
|
|
6
|
+
<div class="cn-stacked-bar" data-testid="cn-stacked-bar">
|
|
7
|
+
<div v-if="loading" class="cn-stacked-bar__state" role="status">
|
|
8
|
+
<NcLoadingIcon :size="20" />
|
|
9
|
+
<span>{{ loadingLabel }}</span>
|
|
10
|
+
</div>
|
|
11
|
+
<p v-else-if="error" class="cn-stacked-bar__state" role="status">
|
|
12
|
+
{{ errorLabel }}
|
|
13
|
+
</p>
|
|
14
|
+
<p v-else-if="total === 0" class="cn-stacked-bar__state" data-testid="cn-stacked-bar-empty">
|
|
15
|
+
{{ emptyLabel }}
|
|
16
|
+
</p>
|
|
17
|
+
<template v-else>
|
|
18
|
+
<!-- The bar is a picture of the legend below it. The legend holds
|
|
19
|
+
every label and number, so the bar itself is hidden from
|
|
20
|
+
assistive technology rather than described twice. -->
|
|
21
|
+
<div class="cn-stacked-bar__bar" aria-hidden="true">
|
|
22
|
+
<span
|
|
23
|
+
v-for="segment in barSegments"
|
|
24
|
+
:key="segment.key"
|
|
25
|
+
class="cn-stacked-bar__segment"
|
|
26
|
+
:style="{ flexGrow: segment.count, backgroundColor: segment.color }" />
|
|
27
|
+
</div>
|
|
28
|
+
<ul class="cn-stacked-bar__legend">
|
|
29
|
+
<li
|
|
30
|
+
v-for="segment in segments"
|
|
31
|
+
:key="segment.key"
|
|
32
|
+
class="cn-stacked-bar__legend-item"
|
|
33
|
+
data-testid="cn-stacked-bar-legend-item">
|
|
34
|
+
<span class="cn-stacked-bar__label">
|
|
35
|
+
<span
|
|
36
|
+
class="cn-stacked-bar__dot"
|
|
37
|
+
aria-hidden="true"
|
|
38
|
+
:style="{ backgroundColor: segment.color }" />
|
|
39
|
+
{{ segment.label }}
|
|
40
|
+
</span>
|
|
41
|
+
<!-- The leading space keeps label and count two words for a
|
|
42
|
+
screen reader; it is not drawn in this flex column. -->
|
|
43
|
+
<strong class="cn-stacked-bar__count">{{ ' ' + segment.count }}</strong>
|
|
44
|
+
</li>
|
|
45
|
+
</ul>
|
|
46
|
+
</template>
|
|
47
|
+
</div>
|
|
48
|
+
</template>
|
|
49
|
+
|
|
50
|
+
<script>
|
|
51
|
+
import { translate as t } from '@nextcloud/l10n'
|
|
52
|
+
import { NcLoadingIcon } from '@nextcloud/vue'
|
|
53
|
+
import { fetchGroupedCounts } from '../../utils/fetchAggregate.js'
|
|
54
|
+
|
|
55
|
+
/** The lightest step keeps this share of the primary colour, so it still stands out from the page. */
|
|
56
|
+
const LIGHTEST_PRIMARY_SHARE = 70
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The colour of step `index` out of `count`, on one ramp that runs from a
|
|
60
|
+
* light tint of the primary colour, through the primary colour, to the text
|
|
61
|
+
* colour. Steps differ in LIGHTNESS, so they stay apart for somebody who
|
|
62
|
+
* cannot tell hues apart, and the ramp follows the theme because it is built
|
|
63
|
+
* from Nextcloud's own variables.
|
|
64
|
+
*
|
|
65
|
+
* @param {number} index The step, 0-based.
|
|
66
|
+
* @param {number} count The number of steps.
|
|
67
|
+
* @return {string} A CSS colour.
|
|
68
|
+
* @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-stacked-bar-widget
|
|
69
|
+
*/
|
|
70
|
+
export function rampColor(index, count) {
|
|
71
|
+
if (count <= 1) {
|
|
72
|
+
return 'var(--color-primary-element)'
|
|
73
|
+
}
|
|
74
|
+
const position = index / (count - 1)
|
|
75
|
+
if (position < 0.5) {
|
|
76
|
+
const share = Math.round(LIGHTEST_PRIMARY_SHARE + (position / 0.5) * (100 - LIGHTEST_PRIMARY_SHARE))
|
|
77
|
+
return `color-mix(in srgb, var(--color-primary-element) ${share}%, var(--color-main-background))`
|
|
78
|
+
}
|
|
79
|
+
const textShare = Math.round(((position - 0.5) / 0.5) * 100)
|
|
80
|
+
if (textShare === 0) {
|
|
81
|
+
return 'var(--color-primary-element)'
|
|
82
|
+
}
|
|
83
|
+
if (textShare === 100) {
|
|
84
|
+
return 'var(--color-main-text)'
|
|
85
|
+
}
|
|
86
|
+
return `color-mix(in srgb, var(--color-main-text) ${textShare}%, var(--color-primary-element))`
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* CnStackedBarWidget shows how a total divides over a few groups: ONE bar cut
|
|
91
|
+
* into segments, with a legend below it that names every segment and gives
|
|
92
|
+
* its count. Use it for "my cases per step" or "requests per channel".
|
|
93
|
+
*
|
|
94
|
+
* Segments come from one grouped count request against an OpenRegister
|
|
95
|
+
* source (`source.groupBy`), or from static `segments`. `order` fixes the
|
|
96
|
+
* order of the segments, and `labels` gives a group key a readable name.
|
|
97
|
+
* Colours come from one ramp built on the theme's primary colour; the steps
|
|
98
|
+
* differ in lightness, never in hue alone. Resolved by its registry type key
|
|
99
|
+
* `stacked-bar`.
|
|
100
|
+
*
|
|
101
|
+
* ```json
|
|
102
|
+
* {
|
|
103
|
+
* "widgetKey": "stacked-bar",
|
|
104
|
+
* "props": {
|
|
105
|
+
* "content": {
|
|
106
|
+
* "source": { "register": "dossiq", "schema": "case", "groupBy": "status", "filter": { "assignee": "@me" } },
|
|
107
|
+
* "order": ["received", "in_progress", "decision", "publish"],
|
|
108
|
+
* "labels": { "received": "Received", "in_progress": "In progress", "decision": "Decision", "publish": "Publish" }
|
|
109
|
+
* }
|
|
110
|
+
* }
|
|
111
|
+
* }
|
|
112
|
+
* ```
|
|
113
|
+
*/
|
|
114
|
+
export default {
|
|
115
|
+
name: 'CnStackedBarWidget',
|
|
116
|
+
|
|
117
|
+
components: { NcLoadingIcon },
|
|
118
|
+
|
|
119
|
+
inject: {
|
|
120
|
+
/**
|
|
121
|
+
* Host translate function provided by CnAppRoot. Segment labels and
|
|
122
|
+
* the empty text run through it. Identity by default.
|
|
123
|
+
*/
|
|
124
|
+
cnTranslate: { default: () => (key) => key },
|
|
125
|
+
},
|
|
126
|
+
|
|
127
|
+
props: {
|
|
128
|
+
/**
|
|
129
|
+
* The widget's configuration. `source` names the OpenRegister
|
|
130
|
+
* register and schema, the `groupBy` field and an optional `filter`
|
|
131
|
+
* (the shared @-token grammar). `segments` are static entries
|
|
132
|
+
* `{ key?, label, value }` used when there is no source. `order` lists
|
|
133
|
+
* group keys in the order they render; groups it does not list follow
|
|
134
|
+
* in the order they arrive, and a listed key without records renders
|
|
135
|
+
* with a count of 0. `labels` maps a group key to its label.
|
|
136
|
+
* `emptyText` is shown when there is nothing to count.
|
|
137
|
+
*
|
|
138
|
+
* @type {{source?: {register?: string, schema?: string, groupBy?: string, filter?: object}, segments?: Array<{key?: string, label: string, value: number}>, order?: Array<string>, labels?: {[key: string]: string}, emptyText?: string}}
|
|
139
|
+
*/
|
|
140
|
+
content: {
|
|
141
|
+
type: Object,
|
|
142
|
+
default: () => ({}),
|
|
143
|
+
},
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Translate function. Falls back to the injected `cnTranslate`. Pass it
|
|
147
|
+
* when mounting the widget outside a CnAppRoot.
|
|
148
|
+
*
|
|
149
|
+
* @type {((key: string) => string)|null}
|
|
150
|
+
*/
|
|
151
|
+
translate: {
|
|
152
|
+
type: Function,
|
|
153
|
+
default: null,
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
|
|
157
|
+
data() {
|
|
158
|
+
return {
|
|
159
|
+
groups: [],
|
|
160
|
+
loading: false,
|
|
161
|
+
error: '',
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
|
|
165
|
+
computed: {
|
|
166
|
+
/**
|
|
167
|
+
* The translate function in use.
|
|
168
|
+
*
|
|
169
|
+
* @return {(key: string) => string}
|
|
170
|
+
* @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-stacked-bar-widget
|
|
171
|
+
*/
|
|
172
|
+
effectiveTranslate() {
|
|
173
|
+
return this.translate ?? this.cnTranslate
|
|
174
|
+
},
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Whether the segments come from OpenRegister.
|
|
178
|
+
*
|
|
179
|
+
* @return {boolean}
|
|
180
|
+
* @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-stacked-bar-widget
|
|
181
|
+
*/
|
|
182
|
+
hasSource() {
|
|
183
|
+
const s = this.content.source || {}
|
|
184
|
+
return Boolean(s.register && s.schema && s.groupBy)
|
|
185
|
+
},
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* The raw groups, from the source or the static list, as `{ key, label, count }`.
|
|
189
|
+
*
|
|
190
|
+
* @return {Array<{key: string, label: string, count: number}>}
|
|
191
|
+
* @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-stacked-bar-widget
|
|
192
|
+
*/
|
|
193
|
+
rawGroups() {
|
|
194
|
+
if (this.hasSource) {
|
|
195
|
+
return this.groups.map((group) => ({ key: group.key, label: group.key, count: group.count }))
|
|
196
|
+
}
|
|
197
|
+
const list = Array.isArray(this.content.segments) ? this.content.segments : []
|
|
198
|
+
return list
|
|
199
|
+
.filter((segment) => segment && typeof segment === 'object')
|
|
200
|
+
.map((segment, index) => {
|
|
201
|
+
const count = Number(segment.value)
|
|
202
|
+
return {
|
|
203
|
+
key: String(segment.key ?? segment.label ?? index),
|
|
204
|
+
label: String(segment.label ?? segment.key ?? ''),
|
|
205
|
+
count: Number.isFinite(count) && count > 0 ? count : 0,
|
|
206
|
+
}
|
|
207
|
+
})
|
|
208
|
+
},
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* The segments in render order, each with its label and colour.
|
|
212
|
+
*
|
|
213
|
+
* @return {Array<{key: string, label: string, count: number, color: string}>}
|
|
214
|
+
* @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-stacked-bar-widget
|
|
215
|
+
*/
|
|
216
|
+
segments() {
|
|
217
|
+
const order = Array.isArray(this.content.order) ? this.content.order.map(String) : []
|
|
218
|
+
const labels = (this.content.labels && typeof this.content.labels === 'object') ? this.content.labels : {}
|
|
219
|
+
const byKey = {}
|
|
220
|
+
for (const group of this.rawGroups) {
|
|
221
|
+
byKey[group.key] = group
|
|
222
|
+
}
|
|
223
|
+
const ordered = []
|
|
224
|
+
for (const key of order) {
|
|
225
|
+
ordered.push(byKey[key] || { key, label: key, count: 0 })
|
|
226
|
+
}
|
|
227
|
+
for (const group of this.rawGroups) {
|
|
228
|
+
if (!order.includes(group.key)) {
|
|
229
|
+
ordered.push(group)
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return ordered.map((group, index) => ({
|
|
233
|
+
key: group.key,
|
|
234
|
+
label: this.effectiveTranslate(typeof labels[group.key] === 'string' ? labels[group.key] : group.label),
|
|
235
|
+
count: group.count,
|
|
236
|
+
color: rampColor(index, ordered.length),
|
|
237
|
+
}))
|
|
238
|
+
},
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* The segments that take up room in the bar.
|
|
242
|
+
*
|
|
243
|
+
* @return {Array<object>}
|
|
244
|
+
* @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-stacked-bar-widget
|
|
245
|
+
*/
|
|
246
|
+
barSegments() {
|
|
247
|
+
return this.segments.filter((segment) => segment.count > 0)
|
|
248
|
+
},
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* The sum of all counts.
|
|
252
|
+
*
|
|
253
|
+
* @return {number}
|
|
254
|
+
* @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-stacked-bar-widget
|
|
255
|
+
*/
|
|
256
|
+
total() {
|
|
257
|
+
return this.segments.reduce((sum, segment) => sum + segment.count, 0)
|
|
258
|
+
},
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* The text shown when there is nothing to count.
|
|
262
|
+
*
|
|
263
|
+
* @return {string}
|
|
264
|
+
* @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-stacked-bar-widget
|
|
265
|
+
*/
|
|
266
|
+
emptyLabel() {
|
|
267
|
+
const text = this.content.emptyText
|
|
268
|
+
return (typeof text === 'string' && text !== '')
|
|
269
|
+
? this.effectiveTranslate(text)
|
|
270
|
+
: t('nextcloud-vue', 'Nothing to show yet')
|
|
271
|
+
},
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* @return {string} The loading text.
|
|
275
|
+
*
|
|
276
|
+
* @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-stacked-bar-widget
|
|
277
|
+
*/
|
|
278
|
+
loadingLabel() {
|
|
279
|
+
return t('nextcloud-vue', 'Loading…')
|
|
280
|
+
},
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* @return {string} The text shown when the counts could not be loaded.
|
|
284
|
+
*
|
|
285
|
+
* @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-stacked-bar-widget
|
|
286
|
+
*/
|
|
287
|
+
errorLabel() {
|
|
288
|
+
return t('nextcloud-vue', 'The numbers could not be loaded.')
|
|
289
|
+
},
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Changes whenever a new fetch is needed.
|
|
293
|
+
*
|
|
294
|
+
* @return {string}
|
|
295
|
+
* @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-stacked-bar-widget
|
|
296
|
+
*/
|
|
297
|
+
sourceKey() {
|
|
298
|
+
return JSON.stringify(this.content.source || {})
|
|
299
|
+
},
|
|
300
|
+
},
|
|
301
|
+
|
|
302
|
+
watch: {
|
|
303
|
+
/** @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-stacked-bar-widget */
|
|
304
|
+
sourceKey() {
|
|
305
|
+
this.fetchGroups()
|
|
306
|
+
},
|
|
307
|
+
},
|
|
308
|
+
|
|
309
|
+
mounted() {
|
|
310
|
+
this.fetchGroups()
|
|
311
|
+
},
|
|
312
|
+
|
|
313
|
+
methods: {
|
|
314
|
+
/**
|
|
315
|
+
* Load the count per group in one request.
|
|
316
|
+
*
|
|
317
|
+
* @return {Promise<void>}
|
|
318
|
+
* @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-stacked-bar-widget
|
|
319
|
+
*/
|
|
320
|
+
async fetchGroups() {
|
|
321
|
+
if (!this.hasSource) {
|
|
322
|
+
this.groups = []
|
|
323
|
+
this.error = ''
|
|
324
|
+
return
|
|
325
|
+
}
|
|
326
|
+
const requestKey = this.sourceKey
|
|
327
|
+
this.loading = true
|
|
328
|
+
this.error = ''
|
|
329
|
+
try {
|
|
330
|
+
const groups = await fetchGroupedCounts(this.content.source)
|
|
331
|
+
if (requestKey !== this.sourceKey) {
|
|
332
|
+
return
|
|
333
|
+
}
|
|
334
|
+
this.groups = groups
|
|
335
|
+
} catch (e) {
|
|
336
|
+
// eslint-disable-next-line no-console
|
|
337
|
+
console.warn('[CnStackedBarWidget] failed to load counts:', e)
|
|
338
|
+
this.error = (e && e.message) || 'error'
|
|
339
|
+
this.groups = []
|
|
340
|
+
} finally {
|
|
341
|
+
this.loading = false
|
|
342
|
+
}
|
|
343
|
+
},
|
|
344
|
+
},
|
|
345
|
+
}
|
|
346
|
+
</script>
|
|
347
|
+
|
|
348
|
+
<style scoped>
|
|
349
|
+
.cn-stacked-bar {
|
|
350
|
+
display: flex;
|
|
351
|
+
flex-direction: column;
|
|
352
|
+
gap: 16px;
|
|
353
|
+
width: 100%;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
.cn-stacked-bar__state {
|
|
357
|
+
display: flex;
|
|
358
|
+
align-items: center;
|
|
359
|
+
gap: 8px;
|
|
360
|
+
margin: 0;
|
|
361
|
+
color: var(--color-text-maxcontrast);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
.cn-stacked-bar__bar {
|
|
365
|
+
display: flex;
|
|
366
|
+
gap: 3px;
|
|
367
|
+
height: 12px;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
.cn-stacked-bar__segment {
|
|
371
|
+
flex-basis: 0;
|
|
372
|
+
min-width: 4px;
|
|
373
|
+
border-radius: 6px;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
.cn-stacked-bar__legend {
|
|
377
|
+
display: grid;
|
|
378
|
+
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
|
379
|
+
gap: 12px;
|
|
380
|
+
margin: 0;
|
|
381
|
+
padding: 0;
|
|
382
|
+
list-style: none;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
.cn-stacked-bar__legend-item {
|
|
386
|
+
display: flex;
|
|
387
|
+
flex-direction: column;
|
|
388
|
+
gap: 2px;
|
|
389
|
+
min-width: 0;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
.cn-stacked-bar__label {
|
|
393
|
+
display: flex;
|
|
394
|
+
align-items: center;
|
|
395
|
+
gap: 8px;
|
|
396
|
+
font-size: 0.95em;
|
|
397
|
+
color: var(--color-text-maxcontrast);
|
|
398
|
+
overflow-wrap: anywhere;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
.cn-stacked-bar__dot {
|
|
402
|
+
flex: none;
|
|
403
|
+
width: 10px;
|
|
404
|
+
height: 10px;
|
|
405
|
+
border-radius: 3px;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
.cn-stacked-bar__count {
|
|
409
|
+
font-size: 1.5em;
|
|
410
|
+
font-weight: 700;
|
|
411
|
+
line-height: 1.2;
|
|
412
|
+
color: var(--color-main-text);
|
|
413
|
+
}
|
|
414
|
+
</style>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SPDX-FileCopyrightText: 2026 Conduction B.V.
|
|
3
|
+
* SPDX-License-Identifier: EUPL-1.2
|
|
4
|
+
*
|
|
5
|
+
* CnStackedBarWidget: the `stacked-bar` dashboard widget (one segmented bar
|
|
6
|
+
* with a legend that carries the numbers). It is registered in
|
|
7
|
+
* `CnWidgetGrid/registerDashboardWidgets.js`, inline, because a bare
|
|
8
|
+
* side-effect import of this module may be tree-shaken away.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import CnStackedBarWidget from './CnStackedBarWidget.vue'
|
|
12
|
+
|
|
13
|
+
export { CnStackedBarWidget }
|
|
14
|
+
export default CnStackedBarWidget
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
- SPDX-FileCopyrightText: 2026 Conduction B.V.
|
|
3
|
+
- SPDX-License-Identifier: EUPL-1.2
|
|
4
|
+
-->
|
|
5
|
+
|
|
6
|
+
<template>
|
|
7
|
+
<div class="cn-stacked-bar-form">
|
|
8
|
+
<div class="cn-stacked-bar-form__row2">
|
|
9
|
+
<CnRegisterSchemaSelect
|
|
10
|
+
:register="source.register"
|
|
11
|
+
:schema="source.schema"
|
|
12
|
+
@update:register="updateSource('register', $event)"
|
|
13
|
+
@update:schema="updateSource('schema', $event)" />
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
<CnFieldPicker
|
|
17
|
+
:value="groupBy"
|
|
18
|
+
:label="t('nextcloud-vue', 'Field to group by')"
|
|
19
|
+
:options="availableFields"
|
|
20
|
+
placeholder="status"
|
|
21
|
+
@update="updateField('groupBy', $event)" />
|
|
22
|
+
|
|
23
|
+
<CnFilterRowsEditor :value="filterRows" :fields="availableFields" @input="onFilterRows" />
|
|
24
|
+
|
|
25
|
+
<NcTextField
|
|
26
|
+
:modelValue="orderText"
|
|
27
|
+
:label="t('nextcloud-vue', 'Order of the segments (values, comma separated)')"
|
|
28
|
+
placeholder="received, in_progress, decision"
|
|
29
|
+
@update:modelValue="updateField('orderText', $event)" />
|
|
30
|
+
|
|
31
|
+
<NcTextField
|
|
32
|
+
:modelValue="emptyText"
|
|
33
|
+
:label="t('nextcloud-vue', 'Text when there is nothing to count')"
|
|
34
|
+
@update:modelValue="updateField('emptyText', $event)" />
|
|
35
|
+
</div>
|
|
36
|
+
</template>
|
|
37
|
+
|
|
38
|
+
<script>
|
|
39
|
+
import { translate as t } from '@nextcloud/l10n'
|
|
40
|
+
import { NcTextField } from '@nextcloud/vue'
|
|
41
|
+
import CnFieldPicker from '../CnFieldPicker/CnFieldPicker.vue'
|
|
42
|
+
import CnFilterRowsEditor from '../CnFilterRowsEditor/CnFilterRowsEditor.vue'
|
|
43
|
+
import CnRegisterSchemaSelect from '../CnRegisterSchemaSelect/CnRegisterSchemaSelect.vue'
|
|
44
|
+
import { fetchSchemaProperties } from '../../utils/fetchSchemaProperties.js'
|
|
45
|
+
import { filterToRows, rowsToFilter } from '../CnFilterRowsEditor/filterRows.js'
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* CnStackedBarWidgetForm is the settings form of a `stacked-bar` widget. It
|
|
49
|
+
* edits the source (register, schema, filter), the field the records are
|
|
50
|
+
* grouped by, the order of the segments and the empty text. Static `segments`
|
|
51
|
+
* and the `labels` map are written in the manifest and pass through
|
|
52
|
+
* untouched. Emits `update:content` on every change; `validate()` asks for a
|
|
53
|
+
* register, a schema and a group field unless the widget has static segments.
|
|
54
|
+
*/
|
|
55
|
+
export default {
|
|
56
|
+
name: 'CnStackedBarWidgetForm',
|
|
57
|
+
|
|
58
|
+
components: { NcTextField, CnFieldPicker, CnFilterRowsEditor, CnRegisterSchemaSelect },
|
|
59
|
+
|
|
60
|
+
props: {
|
|
61
|
+
/**
|
|
62
|
+
* The placement being edited (pre-fills from `editingWidget.content`), or null.
|
|
63
|
+
*
|
|
64
|
+
* @type {{content: object}|null}
|
|
65
|
+
*/
|
|
66
|
+
editingWidget: { type: Object, default: null },
|
|
67
|
+
/**
|
|
68
|
+
* Initial content values when not editing (registry defaults).
|
|
69
|
+
*
|
|
70
|
+
* @type {object}
|
|
71
|
+
*/
|
|
72
|
+
value: { type: Object, default: () => ({}) },
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
emits: [
|
|
76
|
+
/**
|
|
77
|
+
* Emitted with the assembled content blob on every field change.
|
|
78
|
+
*
|
|
79
|
+
* @event update:content
|
|
80
|
+
* @type {object}
|
|
81
|
+
*/
|
|
82
|
+
'update:content',
|
|
83
|
+
],
|
|
84
|
+
|
|
85
|
+
data() {
|
|
86
|
+
const initial = this.editingWidget?.content || this.value || {}
|
|
87
|
+
const src = initial.source || {}
|
|
88
|
+
return {
|
|
89
|
+
initial,
|
|
90
|
+
source: { register: src.register ?? '', schema: src.schema ?? '' },
|
|
91
|
+
groupBy: src.groupBy ?? '',
|
|
92
|
+
filterRows: filterToRows(src.filter || {}),
|
|
93
|
+
orderText: Array.isArray(initial.order) ? initial.order.join(', ') : '',
|
|
94
|
+
emptyText: initial.emptyText ?? '',
|
|
95
|
+
availableFields: [],
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
computed: {
|
|
100
|
+
/**
|
|
101
|
+
* The assembled content blob. Keys the form does not edit (static
|
|
102
|
+
* segments, labels) are carried over from the initial content.
|
|
103
|
+
*
|
|
104
|
+
* @return {object}
|
|
105
|
+
* @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-stacked-bar-widget
|
|
106
|
+
*/
|
|
107
|
+
assembledContent() {
|
|
108
|
+
return {
|
|
109
|
+
...this.initial,
|
|
110
|
+
source: {
|
|
111
|
+
register: this.source.register,
|
|
112
|
+
schema: this.source.schema,
|
|
113
|
+
groupBy: this.groupBy,
|
|
114
|
+
filter: rowsToFilter(this.filterRows),
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
order: this.orderText.split(',').map((key) => key.trim()).filter((key) => key !== ''),
|
|
118
|
+
emptyText: this.emptyText,
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
watch: {
|
|
124
|
+
'source.register': 'loadFields',
|
|
125
|
+
'source.schema': 'loadFields',
|
|
126
|
+
},
|
|
127
|
+
|
|
128
|
+
mounted() {
|
|
129
|
+
this.loadFields()
|
|
130
|
+
},
|
|
131
|
+
|
|
132
|
+
methods: {
|
|
133
|
+
t,
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Load the schema's field names for the picker.
|
|
137
|
+
*
|
|
138
|
+
* @return {Promise<void>}
|
|
139
|
+
* @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-stacked-bar-widget
|
|
140
|
+
*/
|
|
141
|
+
async loadFields() {
|
|
142
|
+
this.availableFields = await fetchSchemaProperties(this.source.register, this.source.schema)
|
|
143
|
+
},
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Set one field and emit the assembled content.
|
|
147
|
+
*
|
|
148
|
+
* @param {string} field The data key to write.
|
|
149
|
+
* @param {string} value The new value.
|
|
150
|
+
* @return {void}
|
|
151
|
+
* @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-stacked-bar-widget
|
|
152
|
+
*/
|
|
153
|
+
updateField(field, value) {
|
|
154
|
+
this[field] = value
|
|
155
|
+
this.$emit('update:content', this.assembledContent)
|
|
156
|
+
},
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Set the register or the schema and emit.
|
|
160
|
+
*
|
|
161
|
+
* @param {'register'|'schema'} field The source key to write.
|
|
162
|
+
* @param {string} value The chosen slug.
|
|
163
|
+
* @return {void}
|
|
164
|
+
* @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-stacked-bar-widget
|
|
165
|
+
*/
|
|
166
|
+
updateSource(field, value) {
|
|
167
|
+
this.source[field] = value
|
|
168
|
+
this.$emit('update:content', this.assembledContent)
|
|
169
|
+
},
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Take the filter editor's rows and emit.
|
|
173
|
+
*
|
|
174
|
+
* @param {Array<{key: string, op: string, value: string}>} rows The rows.
|
|
175
|
+
* @return {void}
|
|
176
|
+
* @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-stacked-bar-widget
|
|
177
|
+
*/
|
|
178
|
+
onFilterRows(rows) {
|
|
179
|
+
this.filterRows = rows
|
|
180
|
+
this.$emit('update:content', this.assembledContent)
|
|
181
|
+
},
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Validate the form; an empty array means valid.
|
|
185
|
+
*
|
|
186
|
+
* @return {string[]} The validation errors.
|
|
187
|
+
* @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-stacked-bar-widget
|
|
188
|
+
*/
|
|
189
|
+
validate() {
|
|
190
|
+
const hasStatic = Array.isArray(this.initial.segments) && this.initial.segments.length > 0
|
|
191
|
+
if (hasStatic && !this.source.register && !this.source.schema) {
|
|
192
|
+
return []
|
|
193
|
+
}
|
|
194
|
+
const errors = []
|
|
195
|
+
if (!this.source.register || !this.source.schema) {
|
|
196
|
+
errors.push(t('nextcloud-vue', 'A register and schema are required'))
|
|
197
|
+
}
|
|
198
|
+
if (!this.groupBy) {
|
|
199
|
+
errors.push(t('nextcloud-vue', 'A field to group by is required'))
|
|
200
|
+
}
|
|
201
|
+
return errors
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
}
|
|
205
|
+
</script>
|
|
206
|
+
|
|
207
|
+
<style scoped>
|
|
208
|
+
.cn-stacked-bar-form {
|
|
209
|
+
display: flex;
|
|
210
|
+
flex-direction: column;
|
|
211
|
+
gap: 12px;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.cn-stacked-bar-form__row2 {
|
|
215
|
+
display: flex;
|
|
216
|
+
gap: 12px;
|
|
217
|
+
align-items: flex-end;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
.cn-stacked-bar-form__row2 > * {
|
|
221
|
+
flex: 1;
|
|
222
|
+
min-width: 0;
|
|
223
|
+
}
|
|
224
|
+
</style>
|
|
@@ -89,6 +89,28 @@ export default defineComponent({
|
|
|
89
89
|
type: Boolean,
|
|
90
90
|
default: false,
|
|
91
91
|
},
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* A count shown after the title, for example the number of documents
|
|
95
|
+
* behind the tab. `null` (the default) shows none; `0` is shown.
|
|
96
|
+
*
|
|
97
|
+
* @type {number|string|null}
|
|
98
|
+
*/
|
|
99
|
+
count: {
|
|
100
|
+
type: [Number, String],
|
|
101
|
+
default: null,
|
|
102
|
+
},
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* List this tab under the strip's "More" menu instead of in the strip.
|
|
106
|
+
* Picking it there selects it, and while it is the selected tab it
|
|
107
|
+
* shows in the strip. Give an overflow tab a `title`: the menu needs
|
|
108
|
+
* plain text, and a `#title` slot is markup.
|
|
109
|
+
*/
|
|
110
|
+
overflow: {
|
|
111
|
+
type: Boolean,
|
|
112
|
+
default: false,
|
|
113
|
+
},
|
|
92
114
|
},
|
|
93
115
|
|
|
94
116
|
emits: [
|
|
@@ -125,6 +147,19 @@ export default defineComponent({
|
|
|
125
147
|
get disabled() {
|
|
126
148
|
return props.disabled
|
|
127
149
|
},
|
|
150
|
+
|
|
151
|
+
get count() {
|
|
152
|
+
return props.count
|
|
153
|
+
},
|
|
154
|
+
|
|
155
|
+
get overflow() {
|
|
156
|
+
return props.overflow
|
|
157
|
+
},
|
|
158
|
+
|
|
159
|
+
// Plain text for the "More" menu, where a `#title` slot cannot go.
|
|
160
|
+
get label() {
|
|
161
|
+
return props.title
|
|
162
|
+
},
|
|
128
163
|
}
|
|
129
164
|
|
|
130
165
|
onMounted(() => tabsApi?.register(entry))
|