@conduction/nextcloud-vue 2.56.0-beta.5 → 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/CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue2.js +100 -3
- package/dist/esm/components/CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnRelatedObjectsWidget/CnRelatedObjectsWidget.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 +15070 -8994
- 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/CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue +100 -3
- 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,298 @@
|
|
|
1
|
+
import { translate } from '@nextcloud/l10n';
|
|
2
|
+
import { NcLoadingIcon } from '@nextcloud/vue';
|
|
3
|
+
import { fetchGroupedCounts } from '../../utils/fetchAggregate.js';
|
|
4
|
+
|
|
5
|
+
/** The lightest step keeps this share of the primary colour, so it still stands out from the page. */
|
|
6
|
+
const LIGHTEST_PRIMARY_SHARE = 70;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The colour of step `index` out of `count`, on one ramp that runs from a
|
|
10
|
+
* light tint of the primary colour, through the primary colour, to the text
|
|
11
|
+
* colour. Steps differ in LIGHTNESS, so they stay apart for somebody who
|
|
12
|
+
* cannot tell hues apart, and the ramp follows the theme because it is built
|
|
13
|
+
* from Nextcloud's own variables.
|
|
14
|
+
*
|
|
15
|
+
* @param {number} index The step, 0-based.
|
|
16
|
+
* @param {number} count The number of steps.
|
|
17
|
+
* @return {string} A CSS colour.
|
|
18
|
+
* @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-stacked-bar-widget
|
|
19
|
+
*/
|
|
20
|
+
function rampColor(index, count) {
|
|
21
|
+
if (count <= 1) {
|
|
22
|
+
return 'var(--color-primary-element)'
|
|
23
|
+
}
|
|
24
|
+
const position = index / (count - 1);
|
|
25
|
+
if (position < 0.5) {
|
|
26
|
+
const share = Math.round(LIGHTEST_PRIMARY_SHARE + (position / 0.5) * (100 - LIGHTEST_PRIMARY_SHARE));
|
|
27
|
+
return `color-mix(in srgb, var(--color-primary-element) ${share}%, var(--color-main-background))`
|
|
28
|
+
}
|
|
29
|
+
const textShare = Math.round(((position - 0.5) / 0.5) * 100);
|
|
30
|
+
if (textShare === 0) {
|
|
31
|
+
return 'var(--color-primary-element)'
|
|
32
|
+
}
|
|
33
|
+
if (textShare === 100) {
|
|
34
|
+
return 'var(--color-main-text)'
|
|
35
|
+
}
|
|
36
|
+
return `color-mix(in srgb, var(--color-main-text) ${textShare}%, var(--color-primary-element))`
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* CnStackedBarWidget shows how a total divides over a few groups: ONE bar cut
|
|
41
|
+
* into segments, with a legend below it that names every segment and gives
|
|
42
|
+
* its count. Use it for "my cases per step" or "requests per channel".
|
|
43
|
+
*
|
|
44
|
+
* Segments come from one grouped count request against an OpenRegister
|
|
45
|
+
* source (`source.groupBy`), or from static `segments`. `order` fixes the
|
|
46
|
+
* order of the segments, and `labels` gives a group key a readable name.
|
|
47
|
+
* Colours come from one ramp built on the theme's primary colour; the steps
|
|
48
|
+
* differ in lightness, never in hue alone. Resolved by its registry type key
|
|
49
|
+
* `stacked-bar`.
|
|
50
|
+
*
|
|
51
|
+
* ```json
|
|
52
|
+
* {
|
|
53
|
+
* "widgetKey": "stacked-bar",
|
|
54
|
+
* "props": {
|
|
55
|
+
* "content": {
|
|
56
|
+
* "source": { "register": "dossiq", "schema": "case", "groupBy": "status", "filter": { "assignee": "@me" } },
|
|
57
|
+
* "order": ["received", "in_progress", "decision", "publish"],
|
|
58
|
+
* "labels": { "received": "Received", "in_progress": "In progress", "decision": "Decision", "publish": "Publish" }
|
|
59
|
+
* }
|
|
60
|
+
* }
|
|
61
|
+
* }
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
var script = {
|
|
65
|
+
name: 'CnStackedBarWidget',
|
|
66
|
+
|
|
67
|
+
components: { NcLoadingIcon },
|
|
68
|
+
|
|
69
|
+
inject: {
|
|
70
|
+
/**
|
|
71
|
+
* Host translate function provided by CnAppRoot. Segment labels and
|
|
72
|
+
* the empty text run through it. Identity by default.
|
|
73
|
+
*/
|
|
74
|
+
cnTranslate: { default: () => (key) => key },
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
props: {
|
|
78
|
+
/**
|
|
79
|
+
* The widget's configuration. `source` names the OpenRegister
|
|
80
|
+
* register and schema, the `groupBy` field and an optional `filter`
|
|
81
|
+
* (the shared @-token grammar). `segments` are static entries
|
|
82
|
+
* `{ key?, label, value }` used when there is no source. `order` lists
|
|
83
|
+
* group keys in the order they render; groups it does not list follow
|
|
84
|
+
* in the order they arrive, and a listed key without records renders
|
|
85
|
+
* with a count of 0. `labels` maps a group key to its label.
|
|
86
|
+
* `emptyText` is shown when there is nothing to count.
|
|
87
|
+
*
|
|
88
|
+
* @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}}
|
|
89
|
+
*/
|
|
90
|
+
content: {
|
|
91
|
+
type: Object,
|
|
92
|
+
default: () => ({}),
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Translate function. Falls back to the injected `cnTranslate`. Pass it
|
|
97
|
+
* when mounting the widget outside a CnAppRoot.
|
|
98
|
+
*
|
|
99
|
+
* @type {((key: string) => string)|null}
|
|
100
|
+
*/
|
|
101
|
+
translate: {
|
|
102
|
+
type: Function,
|
|
103
|
+
default: null,
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
data() {
|
|
108
|
+
return {
|
|
109
|
+
groups: [],
|
|
110
|
+
loading: false,
|
|
111
|
+
error: '',
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
computed: {
|
|
116
|
+
/**
|
|
117
|
+
* The translate function in use.
|
|
118
|
+
*
|
|
119
|
+
* @return {(key: string) => string}
|
|
120
|
+
* @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-stacked-bar-widget
|
|
121
|
+
*/
|
|
122
|
+
effectiveTranslate() {
|
|
123
|
+
return this.translate ?? this.cnTranslate
|
|
124
|
+
},
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Whether the segments come from OpenRegister.
|
|
128
|
+
*
|
|
129
|
+
* @return {boolean}
|
|
130
|
+
* @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-stacked-bar-widget
|
|
131
|
+
*/
|
|
132
|
+
hasSource() {
|
|
133
|
+
const s = this.content.source || {};
|
|
134
|
+
return Boolean(s.register && s.schema && s.groupBy)
|
|
135
|
+
},
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* The raw groups, from the source or the static list, as `{ key, label, count }`.
|
|
139
|
+
*
|
|
140
|
+
* @return {Array<{key: string, label: string, count: number}>}
|
|
141
|
+
* @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-stacked-bar-widget
|
|
142
|
+
*/
|
|
143
|
+
rawGroups() {
|
|
144
|
+
if (this.hasSource) {
|
|
145
|
+
return this.groups.map((group) => ({ key: group.key, label: group.key, count: group.count }))
|
|
146
|
+
}
|
|
147
|
+
const list = Array.isArray(this.content.segments) ? this.content.segments : [];
|
|
148
|
+
return list
|
|
149
|
+
.filter((segment) => segment && typeof segment === 'object')
|
|
150
|
+
.map((segment, index) => {
|
|
151
|
+
const count = Number(segment.value);
|
|
152
|
+
return {
|
|
153
|
+
key: String(segment.key ?? segment.label ?? index),
|
|
154
|
+
label: String(segment.label ?? segment.key ?? ''),
|
|
155
|
+
count: Number.isFinite(count) && count > 0 ? count : 0,
|
|
156
|
+
}
|
|
157
|
+
})
|
|
158
|
+
},
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* The segments in render order, each with its label and colour.
|
|
162
|
+
*
|
|
163
|
+
* @return {Array<{key: string, label: string, count: number, color: string}>}
|
|
164
|
+
* @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-stacked-bar-widget
|
|
165
|
+
*/
|
|
166
|
+
segments() {
|
|
167
|
+
const order = Array.isArray(this.content.order) ? this.content.order.map(String) : [];
|
|
168
|
+
const labels = (this.content.labels && typeof this.content.labels === 'object') ? this.content.labels : {};
|
|
169
|
+
const byKey = {};
|
|
170
|
+
for (const group of this.rawGroups) {
|
|
171
|
+
byKey[group.key] = group;
|
|
172
|
+
}
|
|
173
|
+
const ordered = [];
|
|
174
|
+
for (const key of order) {
|
|
175
|
+
ordered.push(byKey[key] || { key, label: key, count: 0 });
|
|
176
|
+
}
|
|
177
|
+
for (const group of this.rawGroups) {
|
|
178
|
+
if (!order.includes(group.key)) {
|
|
179
|
+
ordered.push(group);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return ordered.map((group, index) => ({
|
|
183
|
+
key: group.key,
|
|
184
|
+
label: this.effectiveTranslate(typeof labels[group.key] === 'string' ? labels[group.key] : group.label),
|
|
185
|
+
count: group.count,
|
|
186
|
+
color: rampColor(index, ordered.length),
|
|
187
|
+
}))
|
|
188
|
+
},
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* The segments that take up room in the bar.
|
|
192
|
+
*
|
|
193
|
+
* @return {Array<object>}
|
|
194
|
+
* @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-stacked-bar-widget
|
|
195
|
+
*/
|
|
196
|
+
barSegments() {
|
|
197
|
+
return this.segments.filter((segment) => segment.count > 0)
|
|
198
|
+
},
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* The sum of all counts.
|
|
202
|
+
*
|
|
203
|
+
* @return {number}
|
|
204
|
+
* @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-stacked-bar-widget
|
|
205
|
+
*/
|
|
206
|
+
total() {
|
|
207
|
+
return this.segments.reduce((sum, segment) => sum + segment.count, 0)
|
|
208
|
+
},
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* The text shown when there is nothing to count.
|
|
212
|
+
*
|
|
213
|
+
* @return {string}
|
|
214
|
+
* @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-stacked-bar-widget
|
|
215
|
+
*/
|
|
216
|
+
emptyLabel() {
|
|
217
|
+
const text = this.content.emptyText;
|
|
218
|
+
return (typeof text === 'string' && text !== '')
|
|
219
|
+
? this.effectiveTranslate(text)
|
|
220
|
+
: translate('nextcloud-vue', 'Nothing to show yet')
|
|
221
|
+
},
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* @return {string} The loading text.
|
|
225
|
+
*
|
|
226
|
+
* @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-stacked-bar-widget
|
|
227
|
+
*/
|
|
228
|
+
loadingLabel() {
|
|
229
|
+
return translate('nextcloud-vue', 'Loading…')
|
|
230
|
+
},
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* @return {string} The text shown when the counts could not be loaded.
|
|
234
|
+
*
|
|
235
|
+
* @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-stacked-bar-widget
|
|
236
|
+
*/
|
|
237
|
+
errorLabel() {
|
|
238
|
+
return translate('nextcloud-vue', 'The numbers could not be loaded.')
|
|
239
|
+
},
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Changes whenever a new fetch is needed.
|
|
243
|
+
*
|
|
244
|
+
* @return {string}
|
|
245
|
+
* @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-stacked-bar-widget
|
|
246
|
+
*/
|
|
247
|
+
sourceKey() {
|
|
248
|
+
return JSON.stringify(this.content.source || {})
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
|
|
252
|
+
watch: {
|
|
253
|
+
/** @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-stacked-bar-widget */
|
|
254
|
+
sourceKey() {
|
|
255
|
+
this.fetchGroups();
|
|
256
|
+
},
|
|
257
|
+
},
|
|
258
|
+
|
|
259
|
+
mounted() {
|
|
260
|
+
this.fetchGroups();
|
|
261
|
+
},
|
|
262
|
+
|
|
263
|
+
methods: {
|
|
264
|
+
/**
|
|
265
|
+
* Load the count per group in one request.
|
|
266
|
+
*
|
|
267
|
+
* @return {Promise<void>}
|
|
268
|
+
* @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-stacked-bar-widget
|
|
269
|
+
*/
|
|
270
|
+
async fetchGroups() {
|
|
271
|
+
if (!this.hasSource) {
|
|
272
|
+
this.groups = [];
|
|
273
|
+
this.error = '';
|
|
274
|
+
return
|
|
275
|
+
}
|
|
276
|
+
const requestKey = this.sourceKey;
|
|
277
|
+
this.loading = true;
|
|
278
|
+
this.error = '';
|
|
279
|
+
try {
|
|
280
|
+
const groups = await fetchGroupedCounts(this.content.source);
|
|
281
|
+
if (requestKey !== this.sourceKey) {
|
|
282
|
+
return
|
|
283
|
+
}
|
|
284
|
+
this.groups = groups;
|
|
285
|
+
} catch (e) {
|
|
286
|
+
// eslint-disable-next-line no-console
|
|
287
|
+
console.warn('[CnStackedBarWidget] failed to load counts:', e);
|
|
288
|
+
this.error = (e && e.message) || 'error';
|
|
289
|
+
this.groups = [];
|
|
290
|
+
} finally {
|
|
291
|
+
this.loading = false;
|
|
292
|
+
}
|
|
293
|
+
},
|
|
294
|
+
},
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
export { script as default, rampColor };
|
|
298
|
+
//# sourceMappingURL=CnStackedBarWidget.vue2.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CnStackedBarWidget.vue2.js","sources":["../../../../src/components/CnStackedBarWidget/CnStackedBarWidget.vue"],"sourcesContent":["<!--\n SPDX-FileCopyrightText: 2026 Conduction B.V.\n SPDX-License-Identifier: EUPL-1.2\n-->\n<template>\n\t<div class=\"cn-stacked-bar\" data-testid=\"cn-stacked-bar\">\n\t\t<div v-if=\"loading\" class=\"cn-stacked-bar__state\" role=\"status\">\n\t\t\t<NcLoadingIcon :size=\"20\" />\n\t\t\t<span>{{ loadingLabel }}</span>\n\t\t</div>\n\t\t<p v-else-if=\"error\" class=\"cn-stacked-bar__state\" role=\"status\">\n\t\t\t{{ errorLabel }}\n\t\t</p>\n\t\t<p v-else-if=\"total === 0\" class=\"cn-stacked-bar__state\" data-testid=\"cn-stacked-bar-empty\">\n\t\t\t{{ emptyLabel }}\n\t\t</p>\n\t\t<template v-else>\n\t\t\t<!-- The bar is a picture of the legend below it. The legend holds\n\t\t\t every label and number, so the bar itself is hidden from\n\t\t\t assistive technology rather than described twice. -->\n\t\t\t<div class=\"cn-stacked-bar__bar\" aria-hidden=\"true\">\n\t\t\t\t<span\n\t\t\t\t\tv-for=\"segment in barSegments\"\n\t\t\t\t\t:key=\"segment.key\"\n\t\t\t\t\tclass=\"cn-stacked-bar__segment\"\n\t\t\t\t\t:style=\"{ flexGrow: segment.count, backgroundColor: segment.color }\" />\n\t\t\t</div>\n\t\t\t<ul class=\"cn-stacked-bar__legend\">\n\t\t\t\t<li\n\t\t\t\t\tv-for=\"segment in segments\"\n\t\t\t\t\t:key=\"segment.key\"\n\t\t\t\t\tclass=\"cn-stacked-bar__legend-item\"\n\t\t\t\t\tdata-testid=\"cn-stacked-bar-legend-item\">\n\t\t\t\t\t<span class=\"cn-stacked-bar__label\">\n\t\t\t\t\t\t<span\n\t\t\t\t\t\t\tclass=\"cn-stacked-bar__dot\"\n\t\t\t\t\t\t\taria-hidden=\"true\"\n\t\t\t\t\t\t\t:style=\"{ backgroundColor: segment.color }\" />\n\t\t\t\t\t\t{{ segment.label }}\n\t\t\t\t\t</span>\n\t\t\t\t\t<!-- The leading space keeps label and count two words for a\n\t\t\t\t\t screen reader; it is not drawn in this flex column. -->\n\t\t\t\t\t<strong class=\"cn-stacked-bar__count\">{{ ' ' + segment.count }}</strong>\n\t\t\t\t</li>\n\t\t\t</ul>\n\t\t</template>\n\t</div>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcLoadingIcon } from '@nextcloud/vue'\nimport { fetchGroupedCounts } from '../../utils/fetchAggregate.js'\n\n/** The lightest step keeps this share of the primary colour, so it still stands out from the page. */\nconst LIGHTEST_PRIMARY_SHARE = 70\n\n/**\n * The colour of step `index` out of `count`, on one ramp that runs from a\n * light tint of the primary colour, through the primary colour, to the text\n * colour. Steps differ in LIGHTNESS, so they stay apart for somebody who\n * cannot tell hues apart, and the ramp follows the theme because it is built\n * from Nextcloud's own variables.\n *\n * @param {number} index The step, 0-based.\n * @param {number} count The number of steps.\n * @return {string} A CSS colour.\n * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-stacked-bar-widget\n */\nexport function rampColor(index, count) {\n\tif (count <= 1) {\n\t\treturn 'var(--color-primary-element)'\n\t}\n\tconst position = index / (count - 1)\n\tif (position < 0.5) {\n\t\tconst share = Math.round(LIGHTEST_PRIMARY_SHARE + (position / 0.5) * (100 - LIGHTEST_PRIMARY_SHARE))\n\t\treturn `color-mix(in srgb, var(--color-primary-element) ${share}%, var(--color-main-background))`\n\t}\n\tconst textShare = Math.round(((position - 0.5) / 0.5) * 100)\n\tif (textShare === 0) {\n\t\treturn 'var(--color-primary-element)'\n\t}\n\tif (textShare === 100) {\n\t\treturn 'var(--color-main-text)'\n\t}\n\treturn `color-mix(in srgb, var(--color-main-text) ${textShare}%, var(--color-primary-element))`\n}\n\n/**\n * CnStackedBarWidget shows how a total divides over a few groups: ONE bar cut\n * into segments, with a legend below it that names every segment and gives\n * its count. Use it for \"my cases per step\" or \"requests per channel\".\n *\n * Segments come from one grouped count request against an OpenRegister\n * source (`source.groupBy`), or from static `segments`. `order` fixes the\n * order of the segments, and `labels` gives a group key a readable name.\n * Colours come from one ramp built on the theme's primary colour; the steps\n * differ in lightness, never in hue alone. Resolved by its registry type key\n * `stacked-bar`.\n *\n * ```json\n * {\n * \"widgetKey\": \"stacked-bar\",\n * \"props\": {\n * \"content\": {\n * \"source\": { \"register\": \"dossiq\", \"schema\": \"case\", \"groupBy\": \"status\", \"filter\": { \"assignee\": \"@me\" } },\n * \"order\": [\"received\", \"in_progress\", \"decision\", \"publish\"],\n * \"labels\": { \"received\": \"Received\", \"in_progress\": \"In progress\", \"decision\": \"Decision\", \"publish\": \"Publish\" }\n * }\n * }\n * }\n * ```\n */\nexport default {\n\tname: 'CnStackedBarWidget',\n\n\tcomponents: { NcLoadingIcon },\n\n\tinject: {\n\t\t/**\n\t\t * Host translate function provided by CnAppRoot. Segment labels and\n\t\t * the empty text run through it. Identity by default.\n\t\t */\n\t\tcnTranslate: { default: () => (key) => key },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The widget's configuration. `source` names the OpenRegister\n\t\t * register and schema, the `groupBy` field and an optional `filter`\n\t\t * (the shared @-token grammar). `segments` are static entries\n\t\t * `{ key?, label, value }` used when there is no source. `order` lists\n\t\t * group keys in the order they render; groups it does not list follow\n\t\t * in the order they arrive, and a listed key without records renders\n\t\t * with a count of 0. `labels` maps a group key to its label.\n\t\t * `emptyText` is shown when there is nothing to count.\n\t\t *\n\t\t * @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}}\n\t\t */\n\t\tcontent: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * Translate function. Falls back to the injected `cnTranslate`. Pass it\n\t\t * when mounting the widget outside a CnAppRoot.\n\t\t *\n\t\t * @type {((key: string) => string)|null}\n\t\t */\n\t\ttranslate: {\n\t\t\ttype: Function,\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\tdata() {\n\t\treturn {\n\t\t\tgroups: [],\n\t\t\tloading: false,\n\t\t\terror: '',\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * The translate function in use.\n\t\t *\n\t\t * @return {(key: string) => string}\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-stacked-bar-widget\n\t\t */\n\t\teffectiveTranslate() {\n\t\t\treturn this.translate ?? this.cnTranslate\n\t\t},\n\n\t\t/**\n\t\t * Whether the segments come from OpenRegister.\n\t\t *\n\t\t * @return {boolean}\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-stacked-bar-widget\n\t\t */\n\t\thasSource() {\n\t\t\tconst s = this.content.source || {}\n\t\t\treturn Boolean(s.register && s.schema && s.groupBy)\n\t\t},\n\n\t\t/**\n\t\t * The raw groups, from the source or the static list, as `{ key, label, count }`.\n\t\t *\n\t\t * @return {Array<{key: string, label: string, count: number}>}\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-stacked-bar-widget\n\t\t */\n\t\trawGroups() {\n\t\t\tif (this.hasSource) {\n\t\t\t\treturn this.groups.map((group) => ({ key: group.key, label: group.key, count: group.count }))\n\t\t\t}\n\t\t\tconst list = Array.isArray(this.content.segments) ? this.content.segments : []\n\t\t\treturn list\n\t\t\t\t.filter((segment) => segment && typeof segment === 'object')\n\t\t\t\t.map((segment, index) => {\n\t\t\t\t\tconst count = Number(segment.value)\n\t\t\t\t\treturn {\n\t\t\t\t\t\tkey: String(segment.key ?? segment.label ?? index),\n\t\t\t\t\t\tlabel: String(segment.label ?? segment.key ?? ''),\n\t\t\t\t\t\tcount: Number.isFinite(count) && count > 0 ? count : 0,\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * The segments in render order, each with its label and colour.\n\t\t *\n\t\t * @return {Array<{key: string, label: string, count: number, color: string}>}\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-stacked-bar-widget\n\t\t */\n\t\tsegments() {\n\t\t\tconst order = Array.isArray(this.content.order) ? this.content.order.map(String) : []\n\t\t\tconst labels = (this.content.labels && typeof this.content.labels === 'object') ? this.content.labels : {}\n\t\t\tconst byKey = {}\n\t\t\tfor (const group of this.rawGroups) {\n\t\t\t\tbyKey[group.key] = group\n\t\t\t}\n\t\t\tconst ordered = []\n\t\t\tfor (const key of order) {\n\t\t\t\tordered.push(byKey[key] || { key, label: key, count: 0 })\n\t\t\t}\n\t\t\tfor (const group of this.rawGroups) {\n\t\t\t\tif (!order.includes(group.key)) {\n\t\t\t\t\tordered.push(group)\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn ordered.map((group, index) => ({\n\t\t\t\tkey: group.key,\n\t\t\t\tlabel: this.effectiveTranslate(typeof labels[group.key] === 'string' ? labels[group.key] : group.label),\n\t\t\t\tcount: group.count,\n\t\t\t\tcolor: rampColor(index, ordered.length),\n\t\t\t}))\n\t\t},\n\n\t\t/**\n\t\t * The segments that take up room in the bar.\n\t\t *\n\t\t * @return {Array<object>}\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-stacked-bar-widget\n\t\t */\n\t\tbarSegments() {\n\t\t\treturn this.segments.filter((segment) => segment.count > 0)\n\t\t},\n\n\t\t/**\n\t\t * The sum of all counts.\n\t\t *\n\t\t * @return {number}\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-stacked-bar-widget\n\t\t */\n\t\ttotal() {\n\t\t\treturn this.segments.reduce((sum, segment) => sum + segment.count, 0)\n\t\t},\n\n\t\t/**\n\t\t * The text shown when there is nothing to count.\n\t\t *\n\t\t * @return {string}\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-stacked-bar-widget\n\t\t */\n\t\temptyLabel() {\n\t\t\tconst text = this.content.emptyText\n\t\t\treturn (typeof text === 'string' && text !== '')\n\t\t\t\t? this.effectiveTranslate(text)\n\t\t\t\t: t('nextcloud-vue', 'Nothing to show yet')\n\t\t},\n\n\t\t/**\n\t\t * @return {string} The loading text.\n\t\t *\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-stacked-bar-widget\n\t\t */\n\t\tloadingLabel() {\n\t\t\treturn t('nextcloud-vue', 'Loading…')\n\t\t},\n\n\t\t/**\n\t\t * @return {string} The text shown when the counts could not be loaded.\n\t\t *\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-stacked-bar-widget\n\t\t */\n\t\terrorLabel() {\n\t\t\treturn t('nextcloud-vue', 'The numbers could not be loaded.')\n\t\t},\n\n\t\t/**\n\t\t * Changes whenever a new fetch is needed.\n\t\t *\n\t\t * @return {string}\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-stacked-bar-widget\n\t\t */\n\t\tsourceKey() {\n\t\t\treturn JSON.stringify(this.content.source || {})\n\t\t},\n\t},\n\n\twatch: {\n\t\t/** @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-stacked-bar-widget */\n\t\tsourceKey() {\n\t\t\tthis.fetchGroups()\n\t\t},\n\t},\n\n\tmounted() {\n\t\tthis.fetchGroups()\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Load the count per group in one request.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-stacked-bar-widget\n\t\t */\n\t\tasync fetchGroups() {\n\t\t\tif (!this.hasSource) {\n\t\t\t\tthis.groups = []\n\t\t\t\tthis.error = ''\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst requestKey = this.sourceKey\n\t\t\tthis.loading = true\n\t\t\tthis.error = ''\n\t\t\ttry {\n\t\t\t\tconst groups = await fetchGroupedCounts(this.content.source)\n\t\t\t\tif (requestKey !== this.sourceKey) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tthis.groups = groups\n\t\t\t} catch (e) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.warn('[CnStackedBarWidget] failed to load counts:', e)\n\t\t\t\tthis.error = (e && e.message) || 'error'\n\t\t\t\tthis.groups = []\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-stacked-bar {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 16px;\n\twidth: 100%;\n}\n\n.cn-stacked-bar__state {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 8px;\n\tmargin: 0;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-stacked-bar__bar {\n\tdisplay: flex;\n\tgap: 3px;\n\theight: 12px;\n}\n\n.cn-stacked-bar__segment {\n\tflex-basis: 0;\n\tmin-width: 4px;\n\tborder-radius: 6px;\n}\n\n.cn-stacked-bar__legend {\n\tdisplay: grid;\n\tgrid-template-columns: repeat(auto-fit, minmax(150px, 1fr));\n\tgap: 12px;\n\tmargin: 0;\n\tpadding: 0;\n\tlist-style: none;\n}\n\n.cn-stacked-bar__legend-item {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 2px;\n\tmin-width: 0;\n}\n\n.cn-stacked-bar__label {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 8px;\n\tfont-size: 0.95em;\n\tcolor: var(--color-text-maxcontrast);\n\toverflow-wrap: anywhere;\n}\n\n.cn-stacked-bar__dot {\n\tflex: none;\n\twidth: 10px;\n\theight: 10px;\n\tborder-radius: 3px;\n}\n\n.cn-stacked-bar__count {\n\tfont-size: 1.5em;\n\tfont-weight: 700;\n\tline-height: 1.2;\n\tcolor: var(--color-main-text);\n}\n</style>\n"],"names":["t"],"mappings":"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"}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { resolveComponent, openBlock, createElementBlock, createVNode, createElementVNode, toDisplayString, Fragment, renderList, normalizeStyle, createTextVNode } from 'vue';
|
|
2
|
+
|
|
3
|
+
const _hoisted_1 = {
|
|
4
|
+
class: "cn-stacked-bar",
|
|
5
|
+
"data-testid": "cn-stacked-bar"
|
|
6
|
+
};
|
|
7
|
+
const _hoisted_2 = {
|
|
8
|
+
key: 0,
|
|
9
|
+
class: "cn-stacked-bar__state",
|
|
10
|
+
role: "status"
|
|
11
|
+
};
|
|
12
|
+
const _hoisted_3 = {
|
|
13
|
+
key: 1,
|
|
14
|
+
class: "cn-stacked-bar__state",
|
|
15
|
+
role: "status"
|
|
16
|
+
};
|
|
17
|
+
const _hoisted_4 = {
|
|
18
|
+
key: 2,
|
|
19
|
+
class: "cn-stacked-bar__state",
|
|
20
|
+
"data-testid": "cn-stacked-bar-empty"
|
|
21
|
+
};
|
|
22
|
+
const _hoisted_5 = {
|
|
23
|
+
class: "cn-stacked-bar__bar",
|
|
24
|
+
"aria-hidden": "true"
|
|
25
|
+
};
|
|
26
|
+
const _hoisted_6 = { class: "cn-stacked-bar__legend" };
|
|
27
|
+
const _hoisted_7 = { class: "cn-stacked-bar__label" };
|
|
28
|
+
const _hoisted_8 = { class: "cn-stacked-bar__count" };
|
|
29
|
+
|
|
30
|
+
function render(_ctx, _cache, $props, $setup, $data, $options) {
|
|
31
|
+
const _component_NcLoadingIcon = resolveComponent("NcLoadingIcon");
|
|
32
|
+
|
|
33
|
+
return (openBlock(), createElementBlock("div", _hoisted_1, [
|
|
34
|
+
($data.loading)
|
|
35
|
+
? (openBlock(), createElementBlock("div", _hoisted_2, [
|
|
36
|
+
createVNode(_component_NcLoadingIcon, { size: 20 }),
|
|
37
|
+
createElementVNode("span", null, toDisplayString($options.loadingLabel), 1 /* TEXT */)
|
|
38
|
+
]))
|
|
39
|
+
: ($data.error)
|
|
40
|
+
? (openBlock(), createElementBlock("p", _hoisted_3, toDisplayString($options.errorLabel), 1 /* TEXT */))
|
|
41
|
+
: ($options.total === 0)
|
|
42
|
+
? (openBlock(), createElementBlock("p", _hoisted_4, toDisplayString($options.emptyLabel), 1 /* TEXT */))
|
|
43
|
+
: (openBlock(), createElementBlock(Fragment, { key: 3 }, [
|
|
44
|
+
createElementVNode("div", _hoisted_5, [
|
|
45
|
+
(openBlock(true), createElementBlock(Fragment, null, renderList($options.barSegments, (segment) => {
|
|
46
|
+
return (openBlock(), createElementBlock("span", {
|
|
47
|
+
key: segment.key,
|
|
48
|
+
class: "cn-stacked-bar__segment",
|
|
49
|
+
style: normalizeStyle({ flexGrow: segment.count, backgroundColor: segment.color })
|
|
50
|
+
}, null, 4 /* STYLE */))
|
|
51
|
+
}), 128 /* KEYED_FRAGMENT */))
|
|
52
|
+
]),
|
|
53
|
+
createElementVNode("ul", _hoisted_6, [
|
|
54
|
+
(openBlock(true), createElementBlock(Fragment, null, renderList($options.segments, (segment) => {
|
|
55
|
+
return (openBlock(), createElementBlock("li", {
|
|
56
|
+
key: segment.key,
|
|
57
|
+
class: "cn-stacked-bar__legend-item",
|
|
58
|
+
"data-testid": "cn-stacked-bar-legend-item"
|
|
59
|
+
}, [
|
|
60
|
+
createElementVNode("span", _hoisted_7, [
|
|
61
|
+
createElementVNode("span", {
|
|
62
|
+
class: "cn-stacked-bar__dot",
|
|
63
|
+
"aria-hidden": "true",
|
|
64
|
+
style: normalizeStyle({ backgroundColor: segment.color })
|
|
65
|
+
}, null, 4 /* STYLE */),
|
|
66
|
+
createTextVNode(" " + toDisplayString(segment.label), 1 /* TEXT */)
|
|
67
|
+
]),
|
|
68
|
+
createElementVNode("strong", _hoisted_8, toDisplayString(' ' + segment.count), 1 /* TEXT */)
|
|
69
|
+
]))
|
|
70
|
+
}), 128 /* KEYED_FRAGMENT */))
|
|
71
|
+
])
|
|
72
|
+
], 64 /* STABLE_FRAGMENT */))
|
|
73
|
+
]))
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export { render };
|
|
77
|
+
//# sourceMappingURL=CnStackedBarWidget.vue3.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CnStackedBarWidget.vue3.js","sources":["../../../../src/components/CnStackedBarWidget/CnStackedBarWidget.vue?vue&type=template&id=c0bac1a2&lang.js"],"sourcesContent":["<!--\n SPDX-FileCopyrightText: 2026 Conduction B.V.\n SPDX-License-Identifier: EUPL-1.2\n-->\n<template>\n\t<div class=\"cn-stacked-bar\" data-testid=\"cn-stacked-bar\">\n\t\t<div v-if=\"loading\" class=\"cn-stacked-bar__state\" role=\"status\">\n\t\t\t<NcLoadingIcon :size=\"20\" />\n\t\t\t<span>{{ loadingLabel }}</span>\n\t\t</div>\n\t\t<p v-else-if=\"error\" class=\"cn-stacked-bar__state\" role=\"status\">\n\t\t\t{{ errorLabel }}\n\t\t</p>\n\t\t<p v-else-if=\"total === 0\" class=\"cn-stacked-bar__state\" data-testid=\"cn-stacked-bar-empty\">\n\t\t\t{{ emptyLabel }}\n\t\t</p>\n\t\t<template v-else>\n\t\t\t<!-- The bar is a picture of the legend below it. The legend holds\n\t\t\t every label and number, so the bar itself is hidden from\n\t\t\t assistive technology rather than described twice. -->\n\t\t\t<div class=\"cn-stacked-bar__bar\" aria-hidden=\"true\">\n\t\t\t\t<span\n\t\t\t\t\tv-for=\"segment in barSegments\"\n\t\t\t\t\t:key=\"segment.key\"\n\t\t\t\t\tclass=\"cn-stacked-bar__segment\"\n\t\t\t\t\t:style=\"{ flexGrow: segment.count, backgroundColor: segment.color }\" />\n\t\t\t</div>\n\t\t\t<ul class=\"cn-stacked-bar__legend\">\n\t\t\t\t<li\n\t\t\t\t\tv-for=\"segment in segments\"\n\t\t\t\t\t:key=\"segment.key\"\n\t\t\t\t\tclass=\"cn-stacked-bar__legend-item\"\n\t\t\t\t\tdata-testid=\"cn-stacked-bar-legend-item\">\n\t\t\t\t\t<span class=\"cn-stacked-bar__label\">\n\t\t\t\t\t\t<span\n\t\t\t\t\t\t\tclass=\"cn-stacked-bar__dot\"\n\t\t\t\t\t\t\taria-hidden=\"true\"\n\t\t\t\t\t\t\t:style=\"{ backgroundColor: segment.color }\" />\n\t\t\t\t\t\t{{ segment.label }}\n\t\t\t\t\t</span>\n\t\t\t\t\t<!-- The leading space keeps label and count two words for a\n\t\t\t\t\t screen reader; it is not drawn in this flex column. -->\n\t\t\t\t\t<strong class=\"cn-stacked-bar__count\">{{ ' ' + segment.count }}</strong>\n\t\t\t\t</li>\n\t\t\t</ul>\n\t\t</template>\n\t</div>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcLoadingIcon } from '@nextcloud/vue'\nimport { fetchGroupedCounts } from '../../utils/fetchAggregate.js'\n\n/** The lightest step keeps this share of the primary colour, so it still stands out from the page. */\nconst LIGHTEST_PRIMARY_SHARE = 70\n\n/**\n * The colour of step `index` out of `count`, on one ramp that runs from a\n * light tint of the primary colour, through the primary colour, to the text\n * colour. Steps differ in LIGHTNESS, so they stay apart for somebody who\n * cannot tell hues apart, and the ramp follows the theme because it is built\n * from Nextcloud's own variables.\n *\n * @param {number} index The step, 0-based.\n * @param {number} count The number of steps.\n * @return {string} A CSS colour.\n * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-stacked-bar-widget\n */\nexport function rampColor(index, count) {\n\tif (count <= 1) {\n\t\treturn 'var(--color-primary-element)'\n\t}\n\tconst position = index / (count - 1)\n\tif (position < 0.5) {\n\t\tconst share = Math.round(LIGHTEST_PRIMARY_SHARE + (position / 0.5) * (100 - LIGHTEST_PRIMARY_SHARE))\n\t\treturn `color-mix(in srgb, var(--color-primary-element) ${share}%, var(--color-main-background))`\n\t}\n\tconst textShare = Math.round(((position - 0.5) / 0.5) * 100)\n\tif (textShare === 0) {\n\t\treturn 'var(--color-primary-element)'\n\t}\n\tif (textShare === 100) {\n\t\treturn 'var(--color-main-text)'\n\t}\n\treturn `color-mix(in srgb, var(--color-main-text) ${textShare}%, var(--color-primary-element))`\n}\n\n/**\n * CnStackedBarWidget shows how a total divides over a few groups: ONE bar cut\n * into segments, with a legend below it that names every segment and gives\n * its count. Use it for \"my cases per step\" or \"requests per channel\".\n *\n * Segments come from one grouped count request against an OpenRegister\n * source (`source.groupBy`), or from static `segments`. `order` fixes the\n * order of the segments, and `labels` gives a group key a readable name.\n * Colours come from one ramp built on the theme's primary colour; the steps\n * differ in lightness, never in hue alone. Resolved by its registry type key\n * `stacked-bar`.\n *\n * ```json\n * {\n * \"widgetKey\": \"stacked-bar\",\n * \"props\": {\n * \"content\": {\n * \"source\": { \"register\": \"dossiq\", \"schema\": \"case\", \"groupBy\": \"status\", \"filter\": { \"assignee\": \"@me\" } },\n * \"order\": [\"received\", \"in_progress\", \"decision\", \"publish\"],\n * \"labels\": { \"received\": \"Received\", \"in_progress\": \"In progress\", \"decision\": \"Decision\", \"publish\": \"Publish\" }\n * }\n * }\n * }\n * ```\n */\nexport default {\n\tname: 'CnStackedBarWidget',\n\n\tcomponents: { NcLoadingIcon },\n\n\tinject: {\n\t\t/**\n\t\t * Host translate function provided by CnAppRoot. Segment labels and\n\t\t * the empty text run through it. Identity by default.\n\t\t */\n\t\tcnTranslate: { default: () => (key) => key },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The widget's configuration. `source` names the OpenRegister\n\t\t * register and schema, the `groupBy` field and an optional `filter`\n\t\t * (the shared @-token grammar). `segments` are static entries\n\t\t * `{ key?, label, value }` used when there is no source. `order` lists\n\t\t * group keys in the order they render; groups it does not list follow\n\t\t * in the order they arrive, and a listed key without records renders\n\t\t * with a count of 0. `labels` maps a group key to its label.\n\t\t * `emptyText` is shown when there is nothing to count.\n\t\t *\n\t\t * @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}}\n\t\t */\n\t\tcontent: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * Translate function. Falls back to the injected `cnTranslate`. Pass it\n\t\t * when mounting the widget outside a CnAppRoot.\n\t\t *\n\t\t * @type {((key: string) => string)|null}\n\t\t */\n\t\ttranslate: {\n\t\t\ttype: Function,\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\tdata() {\n\t\treturn {\n\t\t\tgroups: [],\n\t\t\tloading: false,\n\t\t\terror: '',\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * The translate function in use.\n\t\t *\n\t\t * @return {(key: string) => string}\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-stacked-bar-widget\n\t\t */\n\t\teffectiveTranslate() {\n\t\t\treturn this.translate ?? this.cnTranslate\n\t\t},\n\n\t\t/**\n\t\t * Whether the segments come from OpenRegister.\n\t\t *\n\t\t * @return {boolean}\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-stacked-bar-widget\n\t\t */\n\t\thasSource() {\n\t\t\tconst s = this.content.source || {}\n\t\t\treturn Boolean(s.register && s.schema && s.groupBy)\n\t\t},\n\n\t\t/**\n\t\t * The raw groups, from the source or the static list, as `{ key, label, count }`.\n\t\t *\n\t\t * @return {Array<{key: string, label: string, count: number}>}\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-stacked-bar-widget\n\t\t */\n\t\trawGroups() {\n\t\t\tif (this.hasSource) {\n\t\t\t\treturn this.groups.map((group) => ({ key: group.key, label: group.key, count: group.count }))\n\t\t\t}\n\t\t\tconst list = Array.isArray(this.content.segments) ? this.content.segments : []\n\t\t\treturn list\n\t\t\t\t.filter((segment) => segment && typeof segment === 'object')\n\t\t\t\t.map((segment, index) => {\n\t\t\t\t\tconst count = Number(segment.value)\n\t\t\t\t\treturn {\n\t\t\t\t\t\tkey: String(segment.key ?? segment.label ?? index),\n\t\t\t\t\t\tlabel: String(segment.label ?? segment.key ?? ''),\n\t\t\t\t\t\tcount: Number.isFinite(count) && count > 0 ? count : 0,\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * The segments in render order, each with its label and colour.\n\t\t *\n\t\t * @return {Array<{key: string, label: string, count: number, color: string}>}\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-stacked-bar-widget\n\t\t */\n\t\tsegments() {\n\t\t\tconst order = Array.isArray(this.content.order) ? this.content.order.map(String) : []\n\t\t\tconst labels = (this.content.labels && typeof this.content.labels === 'object') ? this.content.labels : {}\n\t\t\tconst byKey = {}\n\t\t\tfor (const group of this.rawGroups) {\n\t\t\t\tbyKey[group.key] = group\n\t\t\t}\n\t\t\tconst ordered = []\n\t\t\tfor (const key of order) {\n\t\t\t\tordered.push(byKey[key] || { key, label: key, count: 0 })\n\t\t\t}\n\t\t\tfor (const group of this.rawGroups) {\n\t\t\t\tif (!order.includes(group.key)) {\n\t\t\t\t\tordered.push(group)\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn ordered.map((group, index) => ({\n\t\t\t\tkey: group.key,\n\t\t\t\tlabel: this.effectiveTranslate(typeof labels[group.key] === 'string' ? labels[group.key] : group.label),\n\t\t\t\tcount: group.count,\n\t\t\t\tcolor: rampColor(index, ordered.length),\n\t\t\t}))\n\t\t},\n\n\t\t/**\n\t\t * The segments that take up room in the bar.\n\t\t *\n\t\t * @return {Array<object>}\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-stacked-bar-widget\n\t\t */\n\t\tbarSegments() {\n\t\t\treturn this.segments.filter((segment) => segment.count > 0)\n\t\t},\n\n\t\t/**\n\t\t * The sum of all counts.\n\t\t *\n\t\t * @return {number}\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-stacked-bar-widget\n\t\t */\n\t\ttotal() {\n\t\t\treturn this.segments.reduce((sum, segment) => sum + segment.count, 0)\n\t\t},\n\n\t\t/**\n\t\t * The text shown when there is nothing to count.\n\t\t *\n\t\t * @return {string}\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-stacked-bar-widget\n\t\t */\n\t\temptyLabel() {\n\t\t\tconst text = this.content.emptyText\n\t\t\treturn (typeof text === 'string' && text !== '')\n\t\t\t\t? this.effectiveTranslate(text)\n\t\t\t\t: t('nextcloud-vue', 'Nothing to show yet')\n\t\t},\n\n\t\t/**\n\t\t * @return {string} The loading text.\n\t\t *\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-stacked-bar-widget\n\t\t */\n\t\tloadingLabel() {\n\t\t\treturn t('nextcloud-vue', 'Loading…')\n\t\t},\n\n\t\t/**\n\t\t * @return {string} The text shown when the counts could not be loaded.\n\t\t *\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-stacked-bar-widget\n\t\t */\n\t\terrorLabel() {\n\t\t\treturn t('nextcloud-vue', 'The numbers could not be loaded.')\n\t\t},\n\n\t\t/**\n\t\t * Changes whenever a new fetch is needed.\n\t\t *\n\t\t * @return {string}\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-stacked-bar-widget\n\t\t */\n\t\tsourceKey() {\n\t\t\treturn JSON.stringify(this.content.source || {})\n\t\t},\n\t},\n\n\twatch: {\n\t\t/** @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-stacked-bar-widget */\n\t\tsourceKey() {\n\t\t\tthis.fetchGroups()\n\t\t},\n\t},\n\n\tmounted() {\n\t\tthis.fetchGroups()\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Load the count per group in one request.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-stacked-bar-widget\n\t\t */\n\t\tasync fetchGroups() {\n\t\t\tif (!this.hasSource) {\n\t\t\t\tthis.groups = []\n\t\t\t\tthis.error = ''\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst requestKey = this.sourceKey\n\t\t\tthis.loading = true\n\t\t\tthis.error = ''\n\t\t\ttry {\n\t\t\t\tconst groups = await fetchGroupedCounts(this.content.source)\n\t\t\t\tif (requestKey !== this.sourceKey) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tthis.groups = groups\n\t\t\t} catch (e) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.warn('[CnStackedBarWidget] failed to load counts:', e)\n\t\t\t\tthis.error = (e && e.message) || 'error'\n\t\t\t\tthis.groups = []\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-stacked-bar {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 16px;\n\twidth: 100%;\n}\n\n.cn-stacked-bar__state {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 8px;\n\tmargin: 0;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-stacked-bar__bar {\n\tdisplay: flex;\n\tgap: 3px;\n\theight: 12px;\n}\n\n.cn-stacked-bar__segment {\n\tflex-basis: 0;\n\tmin-width: 4px;\n\tborder-radius: 6px;\n}\n\n.cn-stacked-bar__legend {\n\tdisplay: grid;\n\tgrid-template-columns: repeat(auto-fit, minmax(150px, 1fr));\n\tgap: 12px;\n\tmargin: 0;\n\tpadding: 0;\n\tlist-style: none;\n}\n\n.cn-stacked-bar__legend-item {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 2px;\n\tmin-width: 0;\n}\n\n.cn-stacked-bar__label {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 8px;\n\tfont-size: 0.95em;\n\tcolor: var(--color-text-maxcontrast);\n\toverflow-wrap: anywhere;\n}\n\n.cn-stacked-bar__dot {\n\tflex: none;\n\twidth: 10px;\n\theight: 10px;\n\tborder-radius: 3px;\n}\n\n.cn-stacked-bar__count {\n\tfont-size: 1.5em;\n\tfont-weight: 700;\n\tline-height: 1.2;\n\tcolor: var(--color-main-text);\n}\n</style>\n"],"names":["_openBlock","_createElementBlock","_createVNode","_createElementVNode","_toDisplayString","_Fragment","_renderList","_normalizeStyle"],"mappings":";;;AASM,EAAA,KAAC,EAAA,gBAAA;AAAA,EAAA,aAAA,EAAA,gBAAA;;;;AACgB,EAAA,KAAI,EAAC,uBAAuB;AAAA,EAAA,IAAK,EAAC,QAAQ;;;;AAI/C,EAAA,KAAA,EAAA,uBAAA;AAAA,EAAA,IAAA,EAAA,QAAA;;;;AAGW,EAAA,KAAK,EAAE,uBAAsB;AAAC,EAAA,aAAS,EAAA,sBAAA;;;AAO5D,EAAA,KAAK,EAAC,qBAAqB;AAAA,EAAA,aAAA,EAAA,MAAA;;AAO5B,MAAA,UAAA,GAAA,EAAA,KAAK,EAAC,wBAAwB,GAAA;AAM1B,MAAA,UAAA,GAAA,EAAA,KAAI,EAAE,uBAAuB,GAAA;AASlC,MAAA,UAAA,GAAA,EAAA,KAAA,EAAA,uBAAA,GAAA;;;;;;KApCO,KAAO,CAAA,OAAA;AAAlB,SAAAA,SAAA,EAAA,EAAAC,kBAAA,CAGM,OAHN,UAGM,EAAA;UAFLC,WAAe,CAAA,wBAAA,EAAA,EAAA,IAAA,EAAA,EAAA,EAAA,CAAA;AACf,UAAAC,kBAAA,CAAE,8BAAA,QAAA,CAAA,YAAA,CAAA,EAAA,CAAA,YAAA;;SAEU,KAAG,CAAA,KAAA;0CAEZ,CAAA,GAAA,YAAA,EAAAC,eAAA,CADD,QAAA,CAAA,UAAA,CAAA,EAAA,CAAA,YAAA;WAEW,QAAK,CAAA,KAAA,KAAA,CAAA;8EACb,QAAQ,CAAA,UAAA,CAAA,EAAA,CAAA,YAAA;;AAMX,cAAAD,kBAAA,CAMI,OANJ,UAMI,EAAA;kCALJF,kBAI0B,CAAAI,QAAA,EAAA,IAAA,EAAAC,UAAA,CAHvB,QAAA,CAAA,WAAA,EAAA,CAAA,OAAA,KAAA;uCADHL,kBAI0B,CAAA,MAAA,EAAA;oBAF1B,GAAI,EAAE,OAAO,CAAC,GAAG;AACjB,oBAAA,KAAC,EAAA,yBAAA;AACA,oBAAA,KAAK,6BAAc,OAAM,CAAA,KAAA,EAAA,eAAA,EAAA,OAAA,CAAA,KAAA,EAAA,CAAA;;;;AAE1B,cAAAE,kBAAA,OAAA;kCACAF,8CACkB,QAAQ,CAAA,QAAA,EAAA,CAAnB,OAAO,KAAA;uCADdA;oBAEC,GAAG,EAAC,OAAA,CAAA,GAAA;yBACA,EAAC,6BAAsB;AAC1B,oBAAA,aAAS,EAAC,4BAAO;;AACjB,oBAAAE,kBAAA,CAMC,QAND,UAMC,EAAA;sBALFA,kBAGgD,CAAA,MAAA,EAAA;AAF/C,wBAAA,KAAI,EAAA,qBAAA;AACJ,wBAAA,aAAW,EAAC,MAAM;wBACf,KAAG,EAAAI,cAAA,CAAA,EAAA,eAAA,SAA4B,CAAC,KAAK,EAAA,CAAA;;sCAAO,GAChD,GAAAH,eAAA,CAAG,OAAO,CAAC,KAAK,CAAA,EAAA,CAAA,YAAA;;AAIjB,oBAAAD,kBAAA,CAAC,QAAD,EAAA,UAAC,EAAAC,eAAA,CAAA,GAAA,GAAA,OAAA,CAAA,KAAA,CAAA,EAAA,CAAA,YAAA;;;;;;;;;;"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import script from './CnStackedBarWidgetForm.vue2.js';
|
|
2
|
+
import { render } from './CnStackedBarWidgetForm.vue3.js';
|
|
3
|
+
|
|
4
|
+
script.render = render;
|
|
5
|
+
script.__scopeId = "data-v-56e6c6a2";
|
|
6
|
+
script.__file = "src/components/CnStackedBarWidgetForm/CnStackedBarWidgetForm.vue";
|
|
7
|
+
const __sfc_main = script;
|
|
8
|
+
|
|
9
|
+
export { __sfc_main as default };
|
|
10
|
+
//# sourceMappingURL=CnStackedBarWidgetForm.vue.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CnStackedBarWidgetForm.vue.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;"}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { translate } from '@nextcloud/l10n';
|
|
2
|
+
import { NcTextField } from '@nextcloud/vue';
|
|
3
|
+
import __sfc_main from '../CnFieldPicker/CnFieldPicker.vue.js';
|
|
4
|
+
import __sfc_main$1 from '../CnFilterRowsEditor/CnFilterRowsEditor.vue.js';
|
|
5
|
+
import __sfc_main$2 from '../CnRegisterSchemaSelect/CnRegisterSchemaSelect.vue.js';
|
|
6
|
+
import { fetchSchemaProperties } from '../../utils/fetchSchemaProperties.js';
|
|
7
|
+
import { filterToRows, rowsToFilter } from '../CnFilterRowsEditor/filterRows.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* CnStackedBarWidgetForm is the settings form of a `stacked-bar` widget. It
|
|
11
|
+
* edits the source (register, schema, filter), the field the records are
|
|
12
|
+
* grouped by, the order of the segments and the empty text. Static `segments`
|
|
13
|
+
* and the `labels` map are written in the manifest and pass through
|
|
14
|
+
* untouched. Emits `update:content` on every change; `validate()` asks for a
|
|
15
|
+
* register, a schema and a group field unless the widget has static segments.
|
|
16
|
+
*/
|
|
17
|
+
var script = {
|
|
18
|
+
name: 'CnStackedBarWidgetForm',
|
|
19
|
+
|
|
20
|
+
components: { NcTextField, CnFieldPicker: __sfc_main, CnFilterRowsEditor: __sfc_main$1, CnRegisterSchemaSelect: __sfc_main$2 },
|
|
21
|
+
|
|
22
|
+
props: {
|
|
23
|
+
/**
|
|
24
|
+
* The placement being edited (pre-fills from `editingWidget.content`), or null.
|
|
25
|
+
*
|
|
26
|
+
* @type {{content: object}|null}
|
|
27
|
+
*/
|
|
28
|
+
editingWidget: { type: Object, default: null },
|
|
29
|
+
/**
|
|
30
|
+
* Initial content values when not editing (registry defaults).
|
|
31
|
+
*
|
|
32
|
+
* @type {object}
|
|
33
|
+
*/
|
|
34
|
+
value: { type: Object, default: () => ({}) },
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
emits: [
|
|
38
|
+
/**
|
|
39
|
+
* Emitted with the assembled content blob on every field change.
|
|
40
|
+
*
|
|
41
|
+
* @event update:content
|
|
42
|
+
* @type {object}
|
|
43
|
+
*/
|
|
44
|
+
'update:content',
|
|
45
|
+
],
|
|
46
|
+
|
|
47
|
+
data() {
|
|
48
|
+
const initial = this.editingWidget?.content || this.value || {};
|
|
49
|
+
const src = initial.source || {};
|
|
50
|
+
return {
|
|
51
|
+
initial,
|
|
52
|
+
source: { register: src.register ?? '', schema: src.schema ?? '' },
|
|
53
|
+
groupBy: src.groupBy ?? '',
|
|
54
|
+
filterRows: filterToRows(src.filter || {}),
|
|
55
|
+
orderText: Array.isArray(initial.order) ? initial.order.join(', ') : '',
|
|
56
|
+
emptyText: initial.emptyText ?? '',
|
|
57
|
+
availableFields: [],
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
computed: {
|
|
62
|
+
/**
|
|
63
|
+
* The assembled content blob. Keys the form does not edit (static
|
|
64
|
+
* segments, labels) are carried over from the initial content.
|
|
65
|
+
*
|
|
66
|
+
* @return {object}
|
|
67
|
+
* @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-stacked-bar-widget
|
|
68
|
+
*/
|
|
69
|
+
assembledContent() {
|
|
70
|
+
return {
|
|
71
|
+
...this.initial,
|
|
72
|
+
source: {
|
|
73
|
+
register: this.source.register,
|
|
74
|
+
schema: this.source.schema,
|
|
75
|
+
groupBy: this.groupBy,
|
|
76
|
+
filter: rowsToFilter(this.filterRows),
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
order: this.orderText.split(',').map((key) => key.trim()).filter((key) => key !== ''),
|
|
80
|
+
emptyText: this.emptyText,
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
watch: {
|
|
86
|
+
'source.register': 'loadFields',
|
|
87
|
+
'source.schema': 'loadFields',
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
mounted() {
|
|
91
|
+
this.loadFields();
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
methods: {
|
|
95
|
+
t: translate,
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Load the schema's field names for the picker.
|
|
99
|
+
*
|
|
100
|
+
* @return {Promise<void>}
|
|
101
|
+
* @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-stacked-bar-widget
|
|
102
|
+
*/
|
|
103
|
+
async loadFields() {
|
|
104
|
+
this.availableFields = await fetchSchemaProperties(this.source.register, this.source.schema);
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Set one field and emit the assembled content.
|
|
109
|
+
*
|
|
110
|
+
* @param {string} field The data key to write.
|
|
111
|
+
* @param {string} value The new value.
|
|
112
|
+
* @return {void}
|
|
113
|
+
* @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-stacked-bar-widget
|
|
114
|
+
*/
|
|
115
|
+
updateField(field, value) {
|
|
116
|
+
this[field] = value;
|
|
117
|
+
this.$emit('update:content', this.assembledContent);
|
|
118
|
+
},
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Set the register or the schema and emit.
|
|
122
|
+
*
|
|
123
|
+
* @param {'register'|'schema'} field The source key to write.
|
|
124
|
+
* @param {string} value The chosen slug.
|
|
125
|
+
* @return {void}
|
|
126
|
+
* @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-stacked-bar-widget
|
|
127
|
+
*/
|
|
128
|
+
updateSource(field, value) {
|
|
129
|
+
this.source[field] = value;
|
|
130
|
+
this.$emit('update:content', this.assembledContent);
|
|
131
|
+
},
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Take the filter editor's rows and emit.
|
|
135
|
+
*
|
|
136
|
+
* @param {Array<{key: string, op: string, value: string}>} rows The rows.
|
|
137
|
+
* @return {void}
|
|
138
|
+
* @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-stacked-bar-widget
|
|
139
|
+
*/
|
|
140
|
+
onFilterRows(rows) {
|
|
141
|
+
this.filterRows = rows;
|
|
142
|
+
this.$emit('update:content', this.assembledContent);
|
|
143
|
+
},
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Validate the form; an empty array means valid.
|
|
147
|
+
*
|
|
148
|
+
* @return {string[]} The validation errors.
|
|
149
|
+
* @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-stacked-bar-widget
|
|
150
|
+
*/
|
|
151
|
+
validate() {
|
|
152
|
+
const hasStatic = Array.isArray(this.initial.segments) && this.initial.segments.length > 0;
|
|
153
|
+
if (hasStatic && !this.source.register && !this.source.schema) {
|
|
154
|
+
return []
|
|
155
|
+
}
|
|
156
|
+
const errors = [];
|
|
157
|
+
if (!this.source.register || !this.source.schema) {
|
|
158
|
+
errors.push(translate('nextcloud-vue', 'A register and schema are required'));
|
|
159
|
+
}
|
|
160
|
+
if (!this.groupBy) {
|
|
161
|
+
errors.push(translate('nextcloud-vue', 'A field to group by is required'));
|
|
162
|
+
}
|
|
163
|
+
return errors
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
export { script as default };
|
|
169
|
+
//# sourceMappingURL=CnStackedBarWidgetForm.vue2.js.map
|