@conduction/nextcloud-vue 2.56.0-beta.4 → 2.56.0-beta.5
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/CnActionsBar/CnActionsBar.vue2.js +12 -2
- package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js.map +1 -1
- package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js +1 -2
- package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js.map +1 -1
- package/dist/esm/components/CnActionsMenu/CnActionsMenu.vue2.js.map +1 -1
- package/dist/esm/components/CnActionsMenu/CnActionsMenu.vue3.js +0 -3
- package/dist/esm/components/CnActionsMenu/CnActionsMenu.vue3.js.map +1 -1
- package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue2.js.map +1 -1
- package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue3.js +1 -2
- package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue3.js.map +1 -1
- package/dist/esm/components/CnBoardView/CnBoardView.vue2.js +12 -5
- package/dist/esm/components/CnBoardView/CnBoardView.vue2.js.map +1 -1
- package/dist/esm/components/CnBoardView/CnBoardView.vue3.js +2 -1
- package/dist/esm/components/CnBoardView/CnBoardView.vue3.js.map +1 -1
- package/dist/esm/components/CnCardGrid/CnCardGrid.vue2.js +9 -1
- package/dist/esm/components/CnCardGrid/CnCardGrid.vue2.js.map +1 -1
- package/dist/esm/components/CnCardGrid/CnCardGrid.vue3.js.map +1 -1
- package/dist/esm/components/CnContextMenu/CnContextMenu.vue2.js +6 -1
- package/dist/esm/components/CnContextMenu/CnContextMenu.vue2.js.map +1 -1
- package/dist/esm/components/CnContextMenu/CnContextMenu.vue3.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +116 -15
- package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +27 -14
- package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
- package/dist/esm/components/CnDateAxisView/CnDateAxisView.vue2.js +13 -6
- package/dist/esm/components/CnDateAxisView/CnDateAxisView.vue2.js.map +1 -1
- package/dist/esm/components/CnDateAxisView/CnDateAxisView.vue3.js +2 -0
- package/dist/esm/components/CnDateAxisView/CnDateAxisView.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +29 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
- package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue2.js.map +1 -1
- package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue3.js +1 -2
- package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue3.js.map +1 -1
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js +3 -1
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js.map +1 -1
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js +17 -17
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js.map +1 -1
- package/dist/esm/components/CnFilesWidget/CnFilesWidget.vue2.js +4 -21
- package/dist/esm/components/CnFilesWidget/CnFilesWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnFilesWidget/CnFilesWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnFlowRunsWidget/CnFlowRunsWidget.vue2.js +0 -13
- package/dist/esm/components/CnFlowRunsWidget/CnFlowRunsWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnFlowRunsWidget/CnFlowRunsWidget.vue3.js +8 -8
- package/dist/esm/components/CnFlowRunsWidget/CnFlowRunsWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnFlowsPage/CnFlowsPage.vue2.js.map +1 -1
- package/dist/esm/components/CnFlowsPage/CnFlowsPage.vue3.js +3 -2
- package/dist/esm/components/CnFlowsPage/CnFlowsPage.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +67 -13
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +15 -8
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/manifestActionDispatch.js +31 -11
- package/dist/esm/components/CnIndexPage/manifestActionDispatch.js.map +1 -1
- package/dist/esm/components/CnIntegrationWidget/CnIntegrationWidgetEmpty.vue2.js.map +1 -1
- package/dist/esm/components/CnIntegrationWidget/CnIntegrationWidgetEmpty.vue3.js +1 -2
- package/dist/esm/components/CnIntegrationWidget/CnIntegrationWidgetEmpty.vue3.js.map +1 -1
- package/dist/esm/components/CnInteractionFormWidget/CnInteractionFormWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnInteractionFormWidget/CnInteractionFormWidget.vue3.js +0 -1
- package/dist/esm/components/CnInteractionFormWidget/CnInteractionFormWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnLinkButtonWidget/CnLinkButtonWidget.vue2.js +2 -17
- package/dist/esm/components/CnLinkButtonWidget/CnLinkButtonWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnLinkButtonWidget/CnLinkButtonWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnLogsPage/CnLogsPage.vue2.js +35 -11
- package/dist/esm/components/CnLogsPage/CnLogsPage.vue2.js.map +1 -1
- package/dist/esm/components/CnLogsPage/CnLogsPage.vue3.js +3 -2
- package/dist/esm/components/CnLogsPage/CnLogsPage.vue3.js.map +1 -1
- package/dist/esm/components/CnObjectCard/CnObjectCard.vue2.js +23 -5
- package/dist/esm/components/CnObjectCard/CnObjectCard.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectCard/CnObjectCard.vue3.js +1 -1
- package/dist/esm/components/CnObjectCard/CnObjectCard.vue3.js.map +1 -1
- package/dist/esm/components/CnObjectList/CnObjectList.vue2.js +19 -3
- package/dist/esm/components/CnObjectList/CnObjectList.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectList/CnObjectList.vue3.js.map +1 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +45 -15
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js +4 -2
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnObjectRow/CnObjectRow.vue2.js +24 -6
- package/dist/esm/components/CnObjectRow/CnObjectRow.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectRow/CnObjectRow.vue3.js +1 -1
- package/dist/esm/components/CnObjectRow/CnObjectRow.vue3.js.map +1 -1
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js +0 -1
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js.map +1 -1
- package/dist/esm/components/CnPageRenderer/CnPageRenderer.vue2.js.map +1 -1
- package/dist/esm/components/CnPageRenderer/CnPageRenderer.vue3.js +4 -2
- package/dist/esm/components/CnPageRenderer/CnPageRenderer.vue3.js.map +1 -1
- package/dist/esm/components/CnRowActions/CnRowActions.vue2.js +7 -2
- package/dist/esm/components/CnRowActions/CnRowActions.vue2.js.map +1 -1
- package/dist/esm/components/CnRowActions/CnRowActions.vue3.js.map +1 -1
- package/dist/esm/components/CnSpendAnalyticsWidget/CnSpendAnalyticsWidget.vue2.js +5 -3
- package/dist/esm/components/CnSpendAnalyticsWidget/CnSpendAnalyticsWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnSpendAnalyticsWidget/CnSpendAnalyticsWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnTabs/CnTabs.vue2.js.map +1 -1
- package/dist/esm/components/CnTabs/CnTabs.vue3.js.map +1 -1
- package/dist/esm/components/CnTasksWidget/CnTasksWidget.vue2.js +3 -1
- package/dist/esm/components/CnTasksWidget/CnTasksWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnTasksWidget/CnTasksWidget.vue3.js +1 -0
- package/dist/esm/components/CnTasksWidget/CnTasksWidget.vue3.js.map +1 -1
- package/dist/esm/integrations/builtin/analytics/CnAnalyticsCard.vue2.js +3 -1
- package/dist/esm/integrations/builtin/analytics/CnAnalyticsCard.vue2.js.map +1 -1
- package/dist/esm/integrations/builtin/analytics/CnAnalyticsCard.vue3.js.map +1 -1
- package/dist/esm/integrations/builtin/analytics/CnAnalyticsTab.vue2.js +3 -1
- package/dist/esm/integrations/builtin/analytics/CnAnalyticsTab.vue2.js.map +1 -1
- package/dist/esm/integrations/builtin/analytics/CnAnalyticsTab.vue3.js +2 -4
- package/dist/esm/integrations/builtin/analytics/CnAnalyticsTab.vue3.js.map +1 -1
- package/dist/esm/integrations/builtin/bookmarks/CnBookmarksCard.vue2.js +6 -0
- package/dist/esm/integrations/builtin/bookmarks/CnBookmarksCard.vue2.js.map +1 -1
- package/dist/esm/integrations/builtin/bookmarks/CnBookmarksCard.vue3.js +68 -53
- package/dist/esm/integrations/builtin/bookmarks/CnBookmarksCard.vue3.js.map +1 -1
- package/dist/esm/integrations/builtin/bookmarks/CnBookmarksTab.vue2.js +17 -0
- package/dist/esm/integrations/builtin/bookmarks/CnBookmarksTab.vue2.js.map +1 -1
- package/dist/esm/integrations/builtin/bookmarks/CnBookmarksTab.vue3.js +21 -19
- package/dist/esm/integrations/builtin/bookmarks/CnBookmarksTab.vue3.js.map +1 -1
- package/dist/esm/integrations/builtin/collectives/CnCollectivesCard.vue2.js +3 -1
- package/dist/esm/integrations/builtin/collectives/CnCollectivesCard.vue2.js.map +1 -1
- package/dist/esm/integrations/builtin/collectives/CnCollectivesCard.vue3.js.map +1 -1
- package/dist/esm/integrations/builtin/collectives/CnCollectivesTab.vue2.js +3 -1
- package/dist/esm/integrations/builtin/collectives/CnCollectivesTab.vue2.js.map +1 -1
- package/dist/esm/integrations/builtin/collectives/CnCollectivesTab.vue3.js +2 -4
- package/dist/esm/integrations/builtin/collectives/CnCollectivesTab.vue3.js.map +1 -1
- package/dist/esm/integrations/builtin/cospend/CnCospendCard.vue2.js +2 -1
- package/dist/esm/integrations/builtin/cospend/CnCospendCard.vue2.js.map +1 -1
- package/dist/esm/integrations/builtin/cospend/CnCospendCard.vue3.js.map +1 -1
- package/dist/esm/integrations/builtin/cospend/CnCospendTab.vue2.js +2 -1
- package/dist/esm/integrations/builtin/cospend/CnCospendTab.vue2.js.map +1 -1
- package/dist/esm/integrations/builtin/cospend/CnCospendTab.vue3.js +1 -2
- package/dist/esm/integrations/builtin/cospend/CnCospendTab.vue3.js.map +1 -1
- package/dist/esm/integrations/builtin/deck/CnDeckCard.vue2.js +2 -1
- package/dist/esm/integrations/builtin/deck/CnDeckCard.vue2.js.map +1 -1
- package/dist/esm/integrations/builtin/deck/CnDeckCard.vue3.js.map +1 -1
- package/dist/esm/integrations/builtin/deck/CnDeckTab.vue2.js +2 -1
- package/dist/esm/integrations/builtin/deck/CnDeckTab.vue2.js.map +1 -1
- package/dist/esm/integrations/builtin/deck/CnDeckTab.vue3.js +1 -2
- package/dist/esm/integrations/builtin/deck/CnDeckTab.vue3.js.map +1 -1
- package/dist/esm/integrations/builtin/email/CnEmailCard.vue2.js.map +1 -1
- package/dist/esm/integrations/builtin/email/CnEmailCard.vue3.js.map +1 -1
- package/dist/esm/integrations/builtin/email/CnEmailTab.vue2.js.map +1 -1
- package/dist/esm/integrations/builtin/email/CnEmailTab.vue3.js +1 -2
- package/dist/esm/integrations/builtin/email/CnEmailTab.vue3.js.map +1 -1
- package/dist/esm/integrations/builtin/flow/CnFlowCard.vue2.js +2 -1
- package/dist/esm/integrations/builtin/flow/CnFlowCard.vue2.js.map +1 -1
- package/dist/esm/integrations/builtin/flow/CnFlowCard.vue3.js.map +1 -1
- package/dist/esm/integrations/builtin/flow/CnFlowTab.vue2.js +2 -1
- package/dist/esm/integrations/builtin/flow/CnFlowTab.vue2.js.map +1 -1
- package/dist/esm/integrations/builtin/flow/CnFlowTab.vue3.js +1 -2
- package/dist/esm/integrations/builtin/flow/CnFlowTab.vue3.js.map +1 -1
- package/dist/esm/integrations/builtin/forms/CnFormsCard.vue2.js +3 -1
- package/dist/esm/integrations/builtin/forms/CnFormsCard.vue2.js.map +1 -1
- package/dist/esm/integrations/builtin/forms/CnFormsCard.vue3.js.map +1 -1
- package/dist/esm/integrations/builtin/forms/CnFormsTab.vue2.js +3 -1
- package/dist/esm/integrations/builtin/forms/CnFormsTab.vue2.js.map +1 -1
- package/dist/esm/integrations/builtin/forms/CnFormsTab.vue3.js +2 -4
- package/dist/esm/integrations/builtin/forms/CnFormsTab.vue3.js.map +1 -1
- package/dist/esm/integrations/builtin/maps/CnMapsCard.vue2.js +3 -1
- package/dist/esm/integrations/builtin/maps/CnMapsCard.vue2.js.map +1 -1
- package/dist/esm/integrations/builtin/maps/CnMapsCard.vue3.js.map +1 -1
- package/dist/esm/integrations/builtin/maps/CnMapsTab.vue2.js +3 -1
- package/dist/esm/integrations/builtin/maps/CnMapsTab.vue2.js.map +1 -1
- package/dist/esm/integrations/builtin/maps/CnMapsTab.vue3.js +2 -4
- package/dist/esm/integrations/builtin/maps/CnMapsTab.vue3.js.map +1 -1
- package/dist/esm/integrations/builtin/openproject/CnOpenprojectCard.vue2.js +3 -1
- package/dist/esm/integrations/builtin/openproject/CnOpenprojectCard.vue2.js.map +1 -1
- package/dist/esm/integrations/builtin/openproject/CnOpenprojectCard.vue3.js.map +1 -1
- package/dist/esm/integrations/builtin/openproject/CnOpenprojectTab.vue2.js +3 -1
- package/dist/esm/integrations/builtin/openproject/CnOpenprojectTab.vue2.js.map +1 -1
- package/dist/esm/integrations/builtin/openproject/CnOpenprojectTab.vue3.js +2 -4
- package/dist/esm/integrations/builtin/openproject/CnOpenprojectTab.vue3.js.map +1 -1
- package/dist/esm/integrations/builtin/photos/CnPhotosCard.vue2.js +2 -1
- package/dist/esm/integrations/builtin/photos/CnPhotosCard.vue2.js.map +1 -1
- package/dist/esm/integrations/builtin/photos/CnPhotosCard.vue3.js.map +1 -1
- package/dist/esm/integrations/builtin/photos/CnPhotosTab.vue2.js +2 -1
- package/dist/esm/integrations/builtin/photos/CnPhotosTab.vue2.js.map +1 -1
- package/dist/esm/integrations/builtin/photos/CnPhotosTab.vue3.js +1 -2
- package/dist/esm/integrations/builtin/photos/CnPhotosTab.vue3.js.map +1 -1
- package/dist/esm/integrations/builtin/polls/CnPollsCard.vue2.js +2 -1
- package/dist/esm/integrations/builtin/polls/CnPollsCard.vue2.js.map +1 -1
- package/dist/esm/integrations/builtin/polls/CnPollsCard.vue3.js.map +1 -1
- package/dist/esm/integrations/builtin/polls/CnPollsTab.vue2.js +2 -1
- package/dist/esm/integrations/builtin/polls/CnPollsTab.vue2.js.map +1 -1
- package/dist/esm/integrations/builtin/polls/CnPollsTab.vue3.js +1 -2
- package/dist/esm/integrations/builtin/polls/CnPollsTab.vue3.js.map +1 -1
- package/dist/esm/integrations/builtin/shares/CnSharesTab.vue2.js.map +1 -1
- package/dist/esm/integrations/builtin/shares/CnSharesTab.vue3.js +3 -6
- package/dist/esm/integrations/builtin/shares/CnSharesTab.vue3.js.map +1 -1
- package/dist/esm/integrations/builtin/talk/CnTalkCard.vue2.js +3 -1
- package/dist/esm/integrations/builtin/talk/CnTalkCard.vue2.js.map +1 -1
- package/dist/esm/integrations/builtin/talk/CnTalkCard.vue3.js.map +1 -1
- package/dist/esm/integrations/builtin/talk/CnTalkTab.vue2.js +3 -1
- package/dist/esm/integrations/builtin/talk/CnTalkTab.vue2.js.map +1 -1
- package/dist/esm/integrations/builtin/talk/CnTalkTab.vue3.js +2 -4
- package/dist/esm/integrations/builtin/talk/CnTalkTab.vue3.js.map +1 -1
- package/dist/esm/integrations/builtin/time-tracker/CnTimeTrackerCard.vue2.js +2 -1
- package/dist/esm/integrations/builtin/time-tracker/CnTimeTrackerCard.vue2.js.map +1 -1
- package/dist/esm/integrations/builtin/time-tracker/CnTimeTrackerCard.vue3.js.map +1 -1
- package/dist/esm/integrations/builtin/time-tracker/CnTimeTrackerTab.vue2.js +2 -1
- package/dist/esm/integrations/builtin/time-tracker/CnTimeTrackerTab.vue2.js.map +1 -1
- package/dist/esm/integrations/builtin/time-tracker/CnTimeTrackerTab.vue3.js +1 -2
- package/dist/esm/integrations/builtin/time-tracker/CnTimeTrackerTab.vue3.js.map +1 -1
- package/dist/esm/integrations/builtin/xwiki/CnXwikiCard.vue2.js +4 -1
- package/dist/esm/integrations/builtin/xwiki/CnXwikiCard.vue2.js.map +1 -1
- package/dist/esm/integrations/builtin/xwiki/CnXwikiCard.vue3.js.map +1 -1
- package/dist/esm/integrations/builtin/xwiki/CnXwikiTab.vue2.js +6 -3
- package/dist/esm/integrations/builtin/xwiki/CnXwikiTab.vue2.js.map +1 -1
- package/dist/esm/integrations/builtin/xwiki/CnXwikiTab.vue3.js +2 -4
- package/dist/esm/integrations/builtin/xwiki/CnXwikiTab.vue3.js.map +1 -1
- package/dist/esm/l10n/en.json.js +1 -0
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +1 -0
- package/dist/esm/l10n/nl.json.js.map +1 -1
- package/dist/esm/nextcloud-vue.css +71 -7
- package/dist/esm/utils/actionLink.js +5 -0
- package/dist/esm/utils/actionLink.js.map +1 -1
- package/dist/esm/utils/actionsDispatcher.js +16 -1
- package/dist/esm/utils/actionsDispatcher.js.map +1 -1
- package/dist/esm/utils/linkNavigation.js +6 -3
- package/dist/esm/utils/linkNavigation.js.map +1 -1
- package/dist/esm/utils/rowAuxClick.js +30 -5
- package/dist/esm/utils/rowAuxClick.js.map +1 -1
- package/dist/esm/utils/safeHref.js +18 -3
- package/dist/esm/utils/safeHref.js.map +1 -1
- package/dist/nextcloud-vue.cjs.js +809 -383
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +71 -7
- package/l10n/en.json +1 -0
- package/l10n/nl.json +1 -0
- package/package.json +1 -1
- package/src/components/CnActionsBar/CnActionsBar.vue +14 -5
- package/src/components/CnActionsMenu/CnActionsMenu.vue +0 -3
- package/src/components/CnAdminSettingsShell/CnAdminSettingsShell.vue +1 -2
- package/src/components/CnBoardView/CnBoardView.vue +13 -5
- package/src/components/CnCardGrid/CnCardGrid.vue +9 -1
- package/src/components/CnContextMenu/CnContextMenu.vue +6 -1
- package/src/components/CnDataTable/CnDataTable.md +1 -1
- package/src/components/CnDataTable/CnDataTable.vue +131 -17
- package/src/components/CnDateAxisView/CnDateAxisView.vue +15 -6
- package/src/components/CnDetailPage/CnDetailPage.md +1 -1
- package/src/components/CnDetailPage/CnDetailPage.vue +29 -1
- package/src/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue +1 -2
- package/src/components/CnFilesBrowser/CnFilesBrowser.vue +5 -3
- package/src/components/CnFilesWidget/CnFilesWidget.vue +4 -21
- package/src/components/CnFlowRunsWidget/CnFlowRunsWidget.vue +8 -21
- package/src/components/CnFlowsPage/CnFlowsPage.vue +2 -1
- package/src/components/CnIndexPage/CnIndexPage.vue +75 -14
- package/src/components/CnIndexPage/manifestActionDispatch.js +31 -11
- package/src/components/CnIntegrationWidget/CnIntegrationWidgetEmpty.vue +1 -2
- package/src/components/CnInteractionFormWidget/CnInteractionFormWidget.vue +0 -1
- package/src/components/CnLinkButtonWidget/CnLinkButtonWidget.vue +2 -17
- package/src/components/CnLogsPage/CnLogsPage.vue +37 -12
- package/src/components/CnObjectCard/CnObjectCard.vue +24 -6
- package/src/components/CnObjectList/CnObjectList.vue +19 -3
- package/src/components/CnObjectListWidget/CnObjectListWidget.vue +47 -15
- package/src/components/CnObjectRow/CnObjectRow.vue +25 -7
- package/src/components/CnObjectSidebar/CnFilesTab.vue +0 -1
- package/src/components/CnPageRenderer/CnPageRenderer.vue +2 -0
- package/src/components/CnRowActions/CnRowActions.vue +7 -2
- package/src/components/CnSpendAnalyticsWidget/CnSpendAnalyticsWidget.vue +5 -3
- package/src/components/CnTabs/CnTabs.vue +7 -3
- package/src/components/CnTasksWidget/CnTasksWidget.vue +4 -1
- package/src/css/table.css +26 -0
- package/src/integrations/builtin/analytics/CnAnalyticsCard.vue +3 -2
- package/src/integrations/builtin/analytics/CnAnalyticsTab.vue +5 -6
- package/src/integrations/builtin/bookmarks/CnBookmarksCard.vue +12 -1
- package/src/integrations/builtin/bookmarks/CnBookmarksTab.vue +23 -6
- package/src/integrations/builtin/bookmarks/__tests__/CnBookmarksTab.spec.js +7 -2
- package/src/integrations/builtin/collectives/CnCollectivesCard.vue +3 -2
- package/src/integrations/builtin/collectives/CnCollectivesTab.vue +5 -6
- package/src/integrations/builtin/cospend/CnCospendCard.vue +2 -2
- package/src/integrations/builtin/cospend/CnCospendTab.vue +3 -4
- package/src/integrations/builtin/deck/CnDeckCard.vue +2 -2
- package/src/integrations/builtin/deck/CnDeckTab.vue +3 -4
- package/src/integrations/builtin/email/CnEmailCard.vue +6 -2
- package/src/integrations/builtin/email/CnEmailTab.vue +7 -4
- package/src/integrations/builtin/flow/CnFlowCard.vue +2 -2
- package/src/integrations/builtin/flow/CnFlowTab.vue +3 -4
- package/src/integrations/builtin/forms/CnFormsCard.vue +3 -2
- package/src/integrations/builtin/forms/CnFormsTab.vue +5 -6
- package/src/integrations/builtin/maps/CnMapsCard.vue +3 -2
- package/src/integrations/builtin/maps/CnMapsTab.vue +5 -6
- package/src/integrations/builtin/maps/__tests__/CnMapsTab.spec.js +9 -0
- package/src/integrations/builtin/openproject/CnOpenprojectCard.vue +3 -2
- package/src/integrations/builtin/openproject/CnOpenprojectTab.vue +5 -6
- package/src/integrations/builtin/photos/CnPhotosCard.vue +2 -2
- package/src/integrations/builtin/photos/CnPhotosTab.vue +3 -4
- package/src/integrations/builtin/polls/CnPollsCard.vue +2 -2
- package/src/integrations/builtin/polls/CnPollsTab.vue +3 -4
- package/src/integrations/builtin/shares/CnSharesTab.vue +3 -6
- package/src/integrations/builtin/talk/CnTalkCard.vue +3 -2
- package/src/integrations/builtin/talk/CnTalkTab.vue +5 -6
- package/src/integrations/builtin/time-tracker/CnTimeTrackerCard.vue +2 -2
- package/src/integrations/builtin/time-tracker/CnTimeTrackerTab.vue +3 -4
- package/src/integrations/builtin/xwiki/CnXwikiCard.vue +4 -2
- package/src/integrations/builtin/xwiki/CnXwikiTab.vue +7 -7
- package/src/integrations/builtin/xwiki/__tests__/CnXwikiTab.spec.js +9 -0
- package/src/utils/actionLink.js +5 -0
- package/src/utils/actionsDispatcher.js +15 -0
- package/src/utils/linkNavigation.js +6 -3
- package/src/utils/rowAuxClick.js +29 -4
- package/src/utils/safeHref.js +18 -3
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CnRowActions.vue2.js","sources":["../../../../src/components/CnRowActions/CnRowActions.vue"],"sourcesContent":["<template>\n\t<NcActions :forceMenu=\"visibleActions.length > 3\"\n\t\t:primary=\"primary\"\n\t\t:menuName=\"menuName\"\n\t\tdata-testid=\"cn-row-actions\">\n\t\t<template v-for=\"{ action, link } in renderedActions\" :key=\"action.label\">\n\t\t\t<!-- A navigate-only action is a real link, so it can be middle-clicked, opened in a new tab or copied. -->\n\t\t\t<NcActionLink\n\t\t\t\tv-if=\"link\"\n\t\t\t\t:href=\"link.href\"\n\t\t\t\t:target=\"link.target\"\n\t\t\t\t:title=\"getTitle(action)\"\n\t\t\t\t:class=\"{ 'cn-row-action--destructive': action.destructive }\"\n\t\t\t\t:data-testid=\"`cn-action-item-${slugifyLabel(action.label)}`\"\n\t\t\t\tcloseAfterClick\n\t\t\t\t@click=\"onLinkAction(action, link, $event)\">\n\t\t\t\t<template v-if=\"action.icon\" #icon>\n\t\t\t\t\t<CnIcon v-if=\"typeof action.icon === 'string'\" :name=\"action.icon\" :size=\"20\" />\n\t\t\t\t\t<component :is=\"action.icon\" v-else :size=\"20\" />\n\t\t\t\t</template>\n\t\t\t\t{{ action.label }}\n\t\t\t</NcActionLink>\n\t\t\t<NcActionButton\n\t\t\t\tv-else\n\t\t\t\t:title=\"getTitle(action)\"\n\t\t\t\t:disabled=\"isDisabled(action)\"\n\t\t\t\t:class=\"{ 'cn-row-action--destructive': action.destructive }\"\n\t\t\t\t:data-testid=\"`cn-action-item-${slugifyLabel(action.label)}`\"\n\t\t\t\tcloseAfterClick\n\t\t\t\t@click=\"onAction(action)\">\n\t\t\t\t<template v-if=\"action.icon\" #icon>\n\t\t\t\t\t<CnIcon v-if=\"typeof action.icon === 'string'\" :name=\"action.icon\" :size=\"20\" />\n\t\t\t\t\t<component :is=\"action.icon\" v-else :size=\"20\" />\n\t\t\t\t</template>\n\t\t\t\t{{ action.label }}\n\t\t\t</NcActionButton>\n\t\t</template>\n\t</NcActions>\n</template>\n\n<script>\nimport { NcActionButton, NcActionLink, NcActions } from '@nextcloud/vue'\nimport { followItemActionLink, resolveItemActionLink } from '../../utils/actionLink.js'\nimport { evaluateVisibleWhenLocal, isLocallyDecidableVisibleWhen } from '../../utils/visibleWhen.js'\nimport { CnIcon } from '../CnIcon/index.js'\n\n/**\n * CnRowActions — Action menu wrapper for table rows and cards.\n *\n * Wraps NcActions + NcActionButton for consistent row/card action menus.\n * Actions are defined as an array of objects with label, icon, handler, etc.\n *\n * ```vue\n * <CnRowActions\n * :actions=\"[\n * { label: 'Edit', icon: PencilIcon, handler: (row) => editRow(row) },\n * { label: 'Delete', icon: TrashIcon, handler: (row) => deleteRow(row), destructive: true },\n * ]\"\n * :row=\"row\" />\n * ```\n *\n * An action whose only job is to go somewhere carries `href` (a URL) or `to`\n * (a vue-router location) instead of a navigating `handler`, and renders as a\n * real link — middle-click, \"open in new tab\" and \"copy link\" all work:\n *\n * ```vue\n * <CnRowActions\n * :actions=\"[\n * { label: 'View', icon: 'Eye', to: (row) => ({ name: 'LeadDetail', params: { id: row.id } }) },\n * { label: 'Website', icon: 'Web', href: (row) => row.url, linkTarget: '_blank' },\n * ]\"\n * :row=\"row\" />\n * ```\n */\nexport default {\n\tname: 'CnRowActions',\n\n\tcomponents: {\n\t\tNcActions,\n\t\tNcActionButton,\n\t\tNcActionLink,\n\t\tCnIcon,\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * Action definitions.\n\t\t *\n\t\t * Each action supports:\n\t\t * - `label` (string, required) — display text\n\t\t * - `icon` (component | string) — MDI icon. A component renders\n\t\t * directly; a string is treated as a registry name and rendered\n\t\t * via `CnIcon` (PascalCase, e.g. `\"Eye\"`), falling back to the\n\t\t * help-circle when unregistered. The string form lets manifest\n\t\t * (JSON) actions declare icons by name.\n\t\t * - `handler` (function) — called with `row` on click\n\t\t * - `disabled` (boolean | (row) => boolean) — gray out the entry\n\t\t * - `visible` (boolean | (row) => boolean) — when `false`, hide the entry from the menu (default: shown)\n\t\t * - `title` (string | (row) => string) — native tooltip shown on hover (useful to explain why an entry is disabled)\n\t\t * - `destructive` (boolean) — apply error color styling\n\t\t * - `href` (string | (row) => string) — render the entry as a link to this URL\n\t\t * - `to` (string | object | (row) => string | object) — render the entry as a link to this\n\t\t * vue-router location; a plain click routes in place. Ignored when the router cannot resolve it.\n\t\t * - `linkTarget` (string) — the link's `target`, e.g. `_blank`\n\t\t *\n\t\t * A link entry still emits `action`, but its `handler` is not called: the link is the\n\t\t * navigation. A disabled entry, or a `to` without a resolvable route, stays a button.\n\t\t *\n\t\t * @type {Array<{label: string, icon: object | string, handler: (targetItem: object) => void, disabled: boolean | ((targetItem: object) => boolean), visible: boolean | ((targetItem: object) => boolean), title: string | ((targetItem: object) => string), destructive: boolean, href: string | ((targetItem: object) => string), to: string | object | ((targetItem: object) => string | object), linkTarget: string}>}\n\t\t */\n\t\tactions: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/** The row/object data (passed to action handlers) */\n\t\trow: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Whether to use primary styling for the action menu trigger */\n\t\tprimary: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Label shown on the action menu trigger button */\n\t\tmenuName: {\n\t\t\ttype: String,\n\t\t\tdefault: undefined,\n\t\t},\n\t},\n\n\temits: ['action'],\n\n\tcomputed: {\n\t\t/**\n\t\t * Filter actions by their `visible` predicate. An action without a\n\t\t * `visible` field is always shown (backwards compatible).\n\t\t *\n\t\t * @return {Array} Visible actions for the current row.\n\t\t */\n\t\tvisibleActions() {\n\t\t\treturn this.actions.filter((action) => {\n\t\t\t\t// A manifest is JSON and cannot hold a function, so `visibleWhen` is\n\t\t\t\t// the only per-row gate an app configured from one can express. Only\n\t\t\t\t// a locally decidable condition is gated on: an endpoint/source one is\n\t\t\t\t// not this evaluator's question, and nothing gated a row action at all\n\t\t\t\t// before, so answering `false` to it would delete the entry outright.\n\t\t\t\tif (action.visibleWhen\n\t\t\t\t\t&& isLocallyDecidableVisibleWhen(action.visibleWhen)\n\t\t\t\t\t&& evaluateVisibleWhenLocal(action.visibleWhen, this.row) === false) {\n\t\t\t\t\treturn false\n\t\t\t\t}\n\t\t\t\tif (action.visible === undefined) {\n\t\t\t\t\treturn true\n\t\t\t\t}\n\t\t\t\tif (typeof action.visible === 'function') {\n\t\t\t\t\treturn !!action.visible(this.row)\n\t\t\t\t}\n\t\t\t\treturn !!action.visible\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * Visible actions paired with the link each renders as (null for a button).\n\t\t *\n\t\t * @return {Array<{action: object, link: object|null}>}\n\t\t */\n\t\trenderedActions() {\n\t\t\treturn this.visibleActions.map((action) => ({\n\t\t\t\taction,\n\t\t\t\tlink: this.isDisabled(action) ? null : resolveItemActionLink(action, this.row, this.$router),\n\t\t\t}))\n\t\t},\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Resolve disabled state for an action — supports both boolean and function.\n\t\t *\n\t\t * @param {object} action - The action definition\n\t\t * @return {boolean} Whether the action is disabled\n\t\t */\n\t\tisDisabled(action) {\n\t\t\tif (typeof action.disabled === 'function') {\n\t\t\t\treturn action.disabled(this.row)\n\t\t\t}\n\t\t\treturn !!action.disabled\n\t\t},\n\n\t\t/**\n\t\t * Resolve the title (native tooltip) for an action — supports both\n\t\t * string and function forms. Returns undefined when no title is\n\t\t * provided so the attribute is not rendered.\n\t\t *\n\t\t * @param {object} action - The action definition\n\t\t * @return {string|undefined} The resolved tooltip text, or undefined.\n\t\t */\n\t\tgetTitle(action) {\n\t\t\tif (typeof action.title === 'function') {\n\t\t\t\treturn action.title(this.row) || undefined\n\t\t\t}\n\t\t\treturn action.title || undefined\n\t\t},\n\n\t\tonAction(action) {\n\t\t\tif (action.handler && typeof action.handler === 'function') {\n\t\t\t\taction.handler(this.row)\n\t\t\t}\n\t\t\tthis.$emit('action', { action: action.label, row: this.row })\n\t\t},\n\n\t\t/**\n\t\t * A link entry was clicked: route a plain in-app click, leave the rest to\n\t\t * the browser, and emit `action` as a button would. The `handler` is not\n\t\t * called, since navigating is what the link already does.\n\t\t *\n\t\t * @param {object} action The action definition.\n\t\t * @param {object} link The resolved link.\n\t\t * @param {MouseEvent} event The click event.\n\t\t */\n\t\tonLinkAction(action, link, event) {\n\t\t\tfollowItemActionLink(event, link, this.$router)\n\t\t\t/**\n\t\t\t * @event action User picked an entry. Payload: the action's label and the row. A button entry has already run its `handler`; a link entry navigates instead.\n\t\t\t * @type {{ action: string, row: object|null }}\n\t\t\t */\n\t\t\tthis.$emit('action', { action: action.label, row: this.row })\n\t\t},\n\n\t\t/**\n\t\t * Slugify an action label for use in stable `data-testid` selectors.\n\t\t * Lowercase, kebab-case, strip non-alphanumeric. Used solely by the\n\t\t * `:data-testid` binding on NcActionButton — does not affect runtime\n\t\t * behaviour or rendered text.\n\t\t *\n\t\t * @param {string} label - The action's display label\n\t\t * @return {string} kebab-case slug suitable for a testid suffix.\n\t\t */\n\t\tslugifyLabel(label) {\n\t\t\treturn String(label || '')\n\t\t\t\t.toLowerCase()\n\t\t\t\t.replace(/[^a-z0-9]+/g, '-')\n\t\t\t\t.replace(/^-+|-+$/g, '')\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-row-action--destructive {\n\tcolor: var(--color-error) !important;\n}\n</style>\n"],"names":["CnIcon"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"CnRowActions.vue2.js","sources":["../../../../src/components/CnRowActions/CnRowActions.vue"],"sourcesContent":["<template>\n\t<NcActions :forceMenu=\"visibleActions.length > 3\"\n\t\t:primary=\"primary\"\n\t\t:menuName=\"menuName\"\n\t\tdata-testid=\"cn-row-actions\">\n\t\t<template v-for=\"{ action, link } in renderedActions\" :key=\"action.label\">\n\t\t\t<!-- A navigate-only action is a real link, so it can be middle-clicked, opened in a new tab or copied. -->\n\t\t\t<NcActionLink\n\t\t\t\tv-if=\"link\"\n\t\t\t\t:href=\"link.href\"\n\t\t\t\t:target=\"link.target\"\n\t\t\t\t:title=\"getTitle(action)\"\n\t\t\t\t:class=\"{ 'cn-row-action--destructive': action.destructive }\"\n\t\t\t\t:data-testid=\"`cn-action-item-${slugifyLabel(action.label)}`\"\n\t\t\t\tcloseAfterClick\n\t\t\t\t@click=\"onLinkAction(action, link, $event)\">\n\t\t\t\t<template v-if=\"action.icon\" #icon>\n\t\t\t\t\t<CnIcon v-if=\"typeof action.icon === 'string'\" :name=\"action.icon\" :size=\"20\" />\n\t\t\t\t\t<component :is=\"action.icon\" v-else :size=\"20\" />\n\t\t\t\t</template>\n\t\t\t\t{{ action.label }}\n\t\t\t</NcActionLink>\n\t\t\t<NcActionButton\n\t\t\t\tv-else\n\t\t\t\t:title=\"getTitle(action)\"\n\t\t\t\t:disabled=\"isDisabled(action)\"\n\t\t\t\t:class=\"{ 'cn-row-action--destructive': action.destructive }\"\n\t\t\t\t:data-testid=\"`cn-action-item-${slugifyLabel(action.label)}`\"\n\t\t\t\tcloseAfterClick\n\t\t\t\t@click=\"onAction(action)\">\n\t\t\t\t<template v-if=\"action.icon\" #icon>\n\t\t\t\t\t<CnIcon v-if=\"typeof action.icon === 'string'\" :name=\"action.icon\" :size=\"20\" />\n\t\t\t\t\t<component :is=\"action.icon\" v-else :size=\"20\" />\n\t\t\t\t</template>\n\t\t\t\t{{ action.label }}\n\t\t\t</NcActionButton>\n\t\t</template>\n\t</NcActions>\n</template>\n\n<script>\nimport { NcActionButton, NcActionLink, NcActions } from '@nextcloud/vue'\nimport { followItemActionLink, resolveItemActionLink } from '../../utils/actionLink.js'\nimport { isModifiedClick } from '../../utils/linkNavigation.js'\nimport { evaluateVisibleWhenLocal, isLocallyDecidableVisibleWhen } from '../../utils/visibleWhen.js'\nimport { CnIcon } from '../CnIcon/index.js'\n\n/**\n * CnRowActions — Action menu wrapper for table rows and cards.\n *\n * Wraps NcActions + NcActionButton for consistent row/card action menus.\n * Actions are defined as an array of objects with label, icon, handler, etc.\n *\n * ```vue\n * <CnRowActions\n * :actions=\"[\n * { label: 'Edit', icon: PencilIcon, handler: (row) => editRow(row) },\n * { label: 'Delete', icon: TrashIcon, handler: (row) => deleteRow(row), destructive: true },\n * ]\"\n * :row=\"row\" />\n * ```\n *\n * An action whose only job is to go somewhere carries `href` (a URL) or `to`\n * (a vue-router location) instead of a navigating `handler`, and renders as a\n * real link — middle-click, \"open in new tab\" and \"copy link\" all work:\n *\n * ```vue\n * <CnRowActions\n * :actions=\"[\n * { label: 'View', icon: 'Eye', to: (row) => ({ name: 'LeadDetail', params: { id: row.id } }) },\n * { label: 'Website', icon: 'Web', href: (row) => row.url, linkTarget: '_blank' },\n * ]\"\n * :row=\"row\" />\n * ```\n */\nexport default {\n\tname: 'CnRowActions',\n\n\tcomponents: {\n\t\tNcActions,\n\t\tNcActionButton,\n\t\tNcActionLink,\n\t\tCnIcon,\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * Action definitions.\n\t\t *\n\t\t * Each action supports:\n\t\t * - `label` (string, required) — display text\n\t\t * - `icon` (component | string) — MDI icon. A component renders\n\t\t * directly; a string is treated as a registry name and rendered\n\t\t * via `CnIcon` (PascalCase, e.g. `\"Eye\"`), falling back to the\n\t\t * help-circle when unregistered. The string form lets manifest\n\t\t * (JSON) actions declare icons by name.\n\t\t * - `handler` (function) — called with `row` on click\n\t\t * - `disabled` (boolean | (row) => boolean) — gray out the entry\n\t\t * - `visible` (boolean | (row) => boolean) — when `false`, hide the entry from the menu (default: shown)\n\t\t * - `title` (string | (row) => string) — native tooltip shown on hover (useful to explain why an entry is disabled)\n\t\t * - `destructive` (boolean) — apply error color styling\n\t\t * - `href` (string | (row) => string) — render the entry as a link to this URL\n\t\t * - `to` (string | object | (row) => string | object) — render the entry as a link to this\n\t\t * vue-router location; a plain click routes in place. Ignored when the router cannot resolve it.\n\t\t * - `linkTarget` (string) — the link's `target`, e.g. `_blank`\n\t\t *\n\t\t * A link entry still emits `action`, but its `handler` is not called: the link is the\n\t\t * navigation. A disabled entry, or a `to` without a resolvable route, stays a button.\n\t\t *\n\t\t * @type {Array<{label: string, icon: object | string, handler: (targetItem: object) => void, disabled: boolean | ((targetItem: object) => boolean), visible: boolean | ((targetItem: object) => boolean), title: string | ((targetItem: object) => string), destructive: boolean, href: string | ((targetItem: object) => string), to: string | object | ((targetItem: object) => string | object), linkTarget: string}>}\n\t\t */\n\t\tactions: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/** The row/object data (passed to action handlers) */\n\t\trow: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Whether to use primary styling for the action menu trigger */\n\t\tprimary: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Label shown on the action menu trigger button */\n\t\tmenuName: {\n\t\t\ttype: String,\n\t\t\tdefault: undefined,\n\t\t},\n\t},\n\n\temits: ['action'],\n\n\tcomputed: {\n\t\t/**\n\t\t * Filter actions by their `visible` predicate. An action without a\n\t\t * `visible` field is always shown (backwards compatible).\n\t\t *\n\t\t * @return {Array} Visible actions for the current row.\n\t\t */\n\t\tvisibleActions() {\n\t\t\treturn this.actions.filter((action) => {\n\t\t\t\t// A manifest is JSON and cannot hold a function, so `visibleWhen` is\n\t\t\t\t// the only per-row gate an app configured from one can express. Only\n\t\t\t\t// a locally decidable condition is gated on: an endpoint/source one is\n\t\t\t\t// not this evaluator's question, and nothing gated a row action at all\n\t\t\t\t// before, so answering `false` to it would delete the entry outright.\n\t\t\t\tif (action.visibleWhen\n\t\t\t\t\t&& isLocallyDecidableVisibleWhen(action.visibleWhen)\n\t\t\t\t\t&& evaluateVisibleWhenLocal(action.visibleWhen, this.row) === false) {\n\t\t\t\t\treturn false\n\t\t\t\t}\n\t\t\t\tif (action.visible === undefined) {\n\t\t\t\t\treturn true\n\t\t\t\t}\n\t\t\t\tif (typeof action.visible === 'function') {\n\t\t\t\t\treturn !!action.visible(this.row)\n\t\t\t\t}\n\t\t\t\treturn !!action.visible\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * Visible actions paired with the link each renders as (null for a button).\n\t\t *\n\t\t * @return {Array<{action: object, link: object|null}>}\n\t\t */\n\t\trenderedActions() {\n\t\t\treturn this.visibleActions.map((action) => ({\n\t\t\t\taction,\n\t\t\t\tlink: this.isDisabled(action) ? null : resolveItemActionLink(action, this.row, this.$router),\n\t\t\t}))\n\t\t},\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Resolve disabled state for an action — supports both boolean and function.\n\t\t *\n\t\t * @param {object} action - The action definition\n\t\t * @return {boolean} Whether the action is disabled\n\t\t */\n\t\tisDisabled(action) {\n\t\t\tif (typeof action.disabled === 'function') {\n\t\t\t\treturn action.disabled(this.row)\n\t\t\t}\n\t\t\treturn !!action.disabled\n\t\t},\n\n\t\t/**\n\t\t * Resolve the title (native tooltip) for an action — supports both\n\t\t * string and function forms. Returns undefined when no title is\n\t\t * provided so the attribute is not rendered.\n\t\t *\n\t\t * @param {object} action - The action definition\n\t\t * @return {string|undefined} The resolved tooltip text, or undefined.\n\t\t */\n\t\tgetTitle(action) {\n\t\t\tif (typeof action.title === 'function') {\n\t\t\t\treturn action.title(this.row) || undefined\n\t\t\t}\n\t\t\treturn action.title || undefined\n\t\t},\n\n\t\tonAction(action) {\n\t\t\tif (action.handler && typeof action.handler === 'function') {\n\t\t\t\taction.handler(this.row)\n\t\t\t}\n\t\t\tthis.$emit('action', { action: action.label, row: this.row })\n\t\t},\n\n\t\t/**\n\t\t * A link entry was clicked: route a plain in-app click, leave the rest to\n\t\t * the browser, and emit `action` as a button would. The `handler` is not\n\t\t * called, since navigating is what the link already does. A modified\n\t\t * click opens a new tab, so nothing is emitted in this one.\n\t\t *\n\t\t * @param {object} action The action definition.\n\t\t * @param {object} link The resolved link.\n\t\t * @param {MouseEvent} event The click event.\n\t\t */\n\t\tonLinkAction(action, link, event) {\n\t\t\tif (isModifiedClick(event)) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tfollowItemActionLink(event, link, this.$router)\n\t\t\t/**\n\t\t\t * @event action User picked an entry. Payload: the action's label and the row. A button entry has already run its `handler`; a link entry navigates instead, and a modified (new-tab) click on a link emits nothing.\n\t\t\t * @type {{ action: string, row: object|null }}\n\t\t\t */\n\t\t\tthis.$emit('action', { action: action.label, row: this.row })\n\t\t},\n\n\t\t/**\n\t\t * Slugify an action label for use in stable `data-testid` selectors.\n\t\t * Lowercase, kebab-case, strip non-alphanumeric. Used solely by the\n\t\t * `:data-testid` binding on NcActionButton — does not affect runtime\n\t\t * behaviour or rendered text.\n\t\t *\n\t\t * @param {string} label - The action's display label\n\t\t * @return {string} kebab-case slug suitable for a testid suffix.\n\t\t */\n\t\tslugifyLabel(label) {\n\t\t\treturn String(label || '')\n\t\t\t\t.toLowerCase()\n\t\t\t\t.replace(/[^a-z0-9]+/g, '-')\n\t\t\t\t.replace(/^-+|-+$/g, '')\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-row-action--destructive {\n\tcolor: var(--color-error) !important;\n}\n</style>\n"],"names":["CnIcon"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CnRowActions.vue3.js","sources":["../../../../src/components/CnRowActions/CnRowActions.vue?vue&type=template&id=5d9db5e2&lang.js"],"sourcesContent":["<template>\n\t<NcActions :forceMenu=\"visibleActions.length > 3\"\n\t\t:primary=\"primary\"\n\t\t:menuName=\"menuName\"\n\t\tdata-testid=\"cn-row-actions\">\n\t\t<template v-for=\"{ action, link } in renderedActions\" :key=\"action.label\">\n\t\t\t<!-- A navigate-only action is a real link, so it can be middle-clicked, opened in a new tab or copied. -->\n\t\t\t<NcActionLink\n\t\t\t\tv-if=\"link\"\n\t\t\t\t:href=\"link.href\"\n\t\t\t\t:target=\"link.target\"\n\t\t\t\t:title=\"getTitle(action)\"\n\t\t\t\t:class=\"{ 'cn-row-action--destructive': action.destructive }\"\n\t\t\t\t:data-testid=\"`cn-action-item-${slugifyLabel(action.label)}`\"\n\t\t\t\tcloseAfterClick\n\t\t\t\t@click=\"onLinkAction(action, link, $event)\">\n\t\t\t\t<template v-if=\"action.icon\" #icon>\n\t\t\t\t\t<CnIcon v-if=\"typeof action.icon === 'string'\" :name=\"action.icon\" :size=\"20\" />\n\t\t\t\t\t<component :is=\"action.icon\" v-else :size=\"20\" />\n\t\t\t\t</template>\n\t\t\t\t{{ action.label }}\n\t\t\t</NcActionLink>\n\t\t\t<NcActionButton\n\t\t\t\tv-else\n\t\t\t\t:title=\"getTitle(action)\"\n\t\t\t\t:disabled=\"isDisabled(action)\"\n\t\t\t\t:class=\"{ 'cn-row-action--destructive': action.destructive }\"\n\t\t\t\t:data-testid=\"`cn-action-item-${slugifyLabel(action.label)}`\"\n\t\t\t\tcloseAfterClick\n\t\t\t\t@click=\"onAction(action)\">\n\t\t\t\t<template v-if=\"action.icon\" #icon>\n\t\t\t\t\t<CnIcon v-if=\"typeof action.icon === 'string'\" :name=\"action.icon\" :size=\"20\" />\n\t\t\t\t\t<component :is=\"action.icon\" v-else :size=\"20\" />\n\t\t\t\t</template>\n\t\t\t\t{{ action.label }}\n\t\t\t</NcActionButton>\n\t\t</template>\n\t</NcActions>\n</template>\n\n<script>\nimport { NcActionButton, NcActionLink, NcActions } from '@nextcloud/vue'\nimport { followItemActionLink, resolveItemActionLink } from '../../utils/actionLink.js'\nimport { evaluateVisibleWhenLocal, isLocallyDecidableVisibleWhen } from '../../utils/visibleWhen.js'\nimport { CnIcon } from '../CnIcon/index.js'\n\n/**\n * CnRowActions — Action menu wrapper for table rows and cards.\n *\n * Wraps NcActions + NcActionButton for consistent row/card action menus.\n * Actions are defined as an array of objects with label, icon, handler, etc.\n *\n * ```vue\n * <CnRowActions\n * :actions=\"[\n * { label: 'Edit', icon: PencilIcon, handler: (row) => editRow(row) },\n * { label: 'Delete', icon: TrashIcon, handler: (row) => deleteRow(row), destructive: true },\n * ]\"\n * :row=\"row\" />\n * ```\n *\n * An action whose only job is to go somewhere carries `href` (a URL) or `to`\n * (a vue-router location) instead of a navigating `handler`, and renders as a\n * real link — middle-click, \"open in new tab\" and \"copy link\" all work:\n *\n * ```vue\n * <CnRowActions\n * :actions=\"[\n * { label: 'View', icon: 'Eye', to: (row) => ({ name: 'LeadDetail', params: { id: row.id } }) },\n * { label: 'Website', icon: 'Web', href: (row) => row.url, linkTarget: '_blank' },\n * ]\"\n * :row=\"row\" />\n * ```\n */\nexport default {\n\tname: 'CnRowActions',\n\n\tcomponents: {\n\t\tNcActions,\n\t\tNcActionButton,\n\t\tNcActionLink,\n\t\tCnIcon,\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * Action definitions.\n\t\t *\n\t\t * Each action supports:\n\t\t * - `label` (string, required) — display text\n\t\t * - `icon` (component | string) — MDI icon. A component renders\n\t\t * directly; a string is treated as a registry name and rendered\n\t\t * via `CnIcon` (PascalCase, e.g. `\"Eye\"`), falling back to the\n\t\t * help-circle when unregistered. The string form lets manifest\n\t\t * (JSON) actions declare icons by name.\n\t\t * - `handler` (function) — called with `row` on click\n\t\t * - `disabled` (boolean | (row) => boolean) — gray out the entry\n\t\t * - `visible` (boolean | (row) => boolean) — when `false`, hide the entry from the menu (default: shown)\n\t\t * - `title` (string | (row) => string) — native tooltip shown on hover (useful to explain why an entry is disabled)\n\t\t * - `destructive` (boolean) — apply error color styling\n\t\t * - `href` (string | (row) => string) — render the entry as a link to this URL\n\t\t * - `to` (string | object | (row) => string | object) — render the entry as a link to this\n\t\t * vue-router location; a plain click routes in place. Ignored when the router cannot resolve it.\n\t\t * - `linkTarget` (string) — the link's `target`, e.g. `_blank`\n\t\t *\n\t\t * A link entry still emits `action`, but its `handler` is not called: the link is the\n\t\t * navigation. A disabled entry, or a `to` without a resolvable route, stays a button.\n\t\t *\n\t\t * @type {Array<{label: string, icon: object | string, handler: (targetItem: object) => void, disabled: boolean | ((targetItem: object) => boolean), visible: boolean | ((targetItem: object) => boolean), title: string | ((targetItem: object) => string), destructive: boolean, href: string | ((targetItem: object) => string), to: string | object | ((targetItem: object) => string | object), linkTarget: string}>}\n\t\t */\n\t\tactions: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/** The row/object data (passed to action handlers) */\n\t\trow: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Whether to use primary styling for the action menu trigger */\n\t\tprimary: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Label shown on the action menu trigger button */\n\t\tmenuName: {\n\t\t\ttype: String,\n\t\t\tdefault: undefined,\n\t\t},\n\t},\n\n\temits: ['action'],\n\n\tcomputed: {\n\t\t/**\n\t\t * Filter actions by their `visible` predicate. An action without a\n\t\t * `visible` field is always shown (backwards compatible).\n\t\t *\n\t\t * @return {Array} Visible actions for the current row.\n\t\t */\n\t\tvisibleActions() {\n\t\t\treturn this.actions.filter((action) => {\n\t\t\t\t// A manifest is JSON and cannot hold a function, so `visibleWhen` is\n\t\t\t\t// the only per-row gate an app configured from one can express. Only\n\t\t\t\t// a locally decidable condition is gated on: an endpoint/source one is\n\t\t\t\t// not this evaluator's question, and nothing gated a row action at all\n\t\t\t\t// before, so answering `false` to it would delete the entry outright.\n\t\t\t\tif (action.visibleWhen\n\t\t\t\t\t&& isLocallyDecidableVisibleWhen(action.visibleWhen)\n\t\t\t\t\t&& evaluateVisibleWhenLocal(action.visibleWhen, this.row) === false) {\n\t\t\t\t\treturn false\n\t\t\t\t}\n\t\t\t\tif (action.visible === undefined) {\n\t\t\t\t\treturn true\n\t\t\t\t}\n\t\t\t\tif (typeof action.visible === 'function') {\n\t\t\t\t\treturn !!action.visible(this.row)\n\t\t\t\t}\n\t\t\t\treturn !!action.visible\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * Visible actions paired with the link each renders as (null for a button).\n\t\t *\n\t\t * @return {Array<{action: object, link: object|null}>}\n\t\t */\n\t\trenderedActions() {\n\t\t\treturn this.visibleActions.map((action) => ({\n\t\t\t\taction,\n\t\t\t\tlink: this.isDisabled(action) ? null : resolveItemActionLink(action, this.row, this.$router),\n\t\t\t}))\n\t\t},\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Resolve disabled state for an action — supports both boolean and function.\n\t\t *\n\t\t * @param {object} action - The action definition\n\t\t * @return {boolean} Whether the action is disabled\n\t\t */\n\t\tisDisabled(action) {\n\t\t\tif (typeof action.disabled === 'function') {\n\t\t\t\treturn action.disabled(this.row)\n\t\t\t}\n\t\t\treturn !!action.disabled\n\t\t},\n\n\t\t/**\n\t\t * Resolve the title (native tooltip) for an action — supports both\n\t\t * string and function forms. Returns undefined when no title is\n\t\t * provided so the attribute is not rendered.\n\t\t *\n\t\t * @param {object} action - The action definition\n\t\t * @return {string|undefined} The resolved tooltip text, or undefined.\n\t\t */\n\t\tgetTitle(action) {\n\t\t\tif (typeof action.title === 'function') {\n\t\t\t\treturn action.title(this.row) || undefined\n\t\t\t}\n\t\t\treturn action.title || undefined\n\t\t},\n\n\t\tonAction(action) {\n\t\t\tif (action.handler && typeof action.handler === 'function') {\n\t\t\t\taction.handler(this.row)\n\t\t\t}\n\t\t\tthis.$emit('action', { action: action.label, row: this.row })\n\t\t},\n\n\t\t/**\n\t\t * A link entry was clicked: route a plain in-app click, leave the rest to\n\t\t * the browser, and emit `action` as a button would. The `handler` is not\n\t\t * called, since navigating is what the link already does.\n\t\t *\n\t\t * @param {object} action The action definition.\n\t\t * @param {object} link The resolved link.\n\t\t * @param {MouseEvent} event The click event.\n\t\t */\n\t\tonLinkAction(action, link, event) {\n\t\t\tfollowItemActionLink(event, link, this.$router)\n\t\t\t/**\n\t\t\t * @event action User picked an entry. Payload: the action's label and the row. A button entry has already run its `handler`; a link entry navigates instead.\n\t\t\t * @type {{ action: string, row: object|null }}\n\t\t\t */\n\t\t\tthis.$emit('action', { action: action.label, row: this.row })\n\t\t},\n\n\t\t/**\n\t\t * Slugify an action label for use in stable `data-testid` selectors.\n\t\t * Lowercase, kebab-case, strip non-alphanumeric. Used solely by the\n\t\t * `:data-testid` binding on NcActionButton — does not affect runtime\n\t\t * behaviour or rendered text.\n\t\t *\n\t\t * @param {string} label - The action's display label\n\t\t * @return {string} kebab-case slug suitable for a testid suffix.\n\t\t */\n\t\tslugifyLabel(label) {\n\t\t\treturn String(label || '')\n\t\t\t\t.toLowerCase()\n\t\t\t\t.replace(/[^a-z0-9]+/g, '-')\n\t\t\t\t.replace(/^-+|-+$/g, '')\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-row-action--destructive {\n\tcolor: var(--color-error) !important;\n}\n</style>\n"],"names":["_createBlock","_openBlock","_createElementBlock","_Fragment","_renderList","_normalizeClass","_toDisplayString","_resolveDynamicComponent"],"mappings":";;;;;;;;uBACCA,WAoCW,CAAA,oBAAA,EAAA;IApCC,SAAS,EAAE,QAAc,CAAA,cAAA,CAAC,MAAK,GAAA,CAAA;AACzC,IAAA,OAAO,EAAE,MAAO,CAAA,OAAA;AAChB,IAAA,QAAQ,EAAE,MAAQ,CAAA,QAAA;AACnB,IAAA,aAAW,EAAC,gBAAgB;;qBAClB,MAA0C;AAApD,OAAAC,SAAA,CAAA,IAAA,CAAA,EAAAC,kBAAA,CA+BUC,QA/B2B,EAAA,IAAA,EAAAC,UAAA,CAAA,QAAA,CAAA,eAAe,EAAjC,CAAA,EAAA,MAAM,EAAE,IAAG,EAAA,KAAA;;AAA8B,UAAA,GAAA,EAAA,MAAM,CAAC,KAAK;;UAGhE,IAAI;4BADXJ,WAcc,CAAA,uBAAA,EAAA;;gBAZZ,IAAI,EAAE,IAAI,CAAC,IAAI;gBACf,MAAM,EAAE,IAAI,CAAC,MAAM;gBACnB,KAAK,EAAE,QAAQ,CAAA,QAAA,CAAC,MAAM,CAAA;gBACtB,KAAK,EAAAK,cAAA,CAAA,EAAA,4BAAA,EAAkC,MAAM,CAAC,WAAU,EAAA,CAAA;AACxD,gBAAA,aAAW,EAAoB,CAAA,eAAA,EAAA,QAAA,CAAA,YAAY,CAAC,MAAM,CAAC,KAAK,CAAA,CAAA,CAAA;AACzD,gBAAA,eAAc,EAAd,EAAc;AACb,gBAAA,OAAK,aAAE,QAAY,CAAA,YAAA,CAAC,MAAM,EAAE,IAAI,EAAE,MAAM,CAAA,CAAA;;iCAI/B,MACV;kCADU,GACV,GAAAC,eAAA,CAAG,MAAM,CAAC;;;;AAJM,gBAAA,CAAA,MAAM,CAAC,IAAI;;4BAAG,MAAI;kCACjC,MAA+E;AAA1D,wBAAA,CAAA,OAAA,MAAM,CAAC,IAAK,KAAA,QAAA;0CAAjCN,WAA+E,CAAA,iBAAA,EAAA;;8BAA/B,IAAI,EAAE,MAAM,CAAC,IAAI;AAAG,8BAAA,IAAI,EAAE,EAAE;;0CAC5EA,WAAgD,CAAAO,uBAAA,CAAhC,MAAM,CAAC,IAAI,CAAA,EAAA;;AAAU,8BAAA,IAAI,EAAE,EAAE;;;;;;;4BAI/CP,WAagB,CAAA,yBAAA,EAAA;;gBAXd,KAAK,EAAE,QAAQ,CAAA,QAAA,CAAC,MAAM,CAAA;gBACtB,QAAQ,EAAE,QAAU,CAAA,UAAA,CAAC,MAAM,CAAA;gBAC3B,KAAK,EAAAK,cAAA,CAAA,EAAA,4BAAA,EAAkC,MAAM,CAAC,WAAU,EAAA,CAAA;AACxD,gBAAA,aAAW,EAAoB,CAAA,eAAA,EAAA,QAAA,CAAA,YAAY,CAAC,MAAM,CAAC,KAAK,CAAA,CAAA,CAAA;AACzD,gBAAA,eAAc,EAAd,EAAc;gBACb,OAAK,EAAA,MAAA,KAAE,QAAQ,CAAA,QAAA,CAAC,MAAM,CAAA,CAAA;;iCAIb,MACV;kCADU,GACV,GAAAC,eAAA,CAAG,MAAM,CAAC;;;;AAJM,gBAAA,CAAA,MAAM,CAAC,IAAI;;4BAAG,MAAI;kCACjC,MAA+E;AAA1D,wBAAA,CAAA,OAAA,MAAM,CAAC,IAAK,KAAA,QAAA;0CAAjCN,WAA+E,CAAA,iBAAA,EAAA;;8BAA/B,IAAI,EAAE,MAAM,CAAC,IAAI;AAAG,8BAAA,IAAI,EAAE,EAAE;;0CAC5EA,WAAgD,CAAAO,uBAAA,CAAhC,MAAM,CAAC,IAAI,CAAA,EAAA;;AAAU,8BAAA,IAAI,EAAE,EAAE;;;;;;;;;;;;;;;;"}
|
|
1
|
+
{"version":3,"file":"CnRowActions.vue3.js","sources":["../../../../src/components/CnRowActions/CnRowActions.vue?vue&type=template&id=5d9db5e2&lang.js"],"sourcesContent":["<template>\n\t<NcActions :forceMenu=\"visibleActions.length > 3\"\n\t\t:primary=\"primary\"\n\t\t:menuName=\"menuName\"\n\t\tdata-testid=\"cn-row-actions\">\n\t\t<template v-for=\"{ action, link } in renderedActions\" :key=\"action.label\">\n\t\t\t<!-- A navigate-only action is a real link, so it can be middle-clicked, opened in a new tab or copied. -->\n\t\t\t<NcActionLink\n\t\t\t\tv-if=\"link\"\n\t\t\t\t:href=\"link.href\"\n\t\t\t\t:target=\"link.target\"\n\t\t\t\t:title=\"getTitle(action)\"\n\t\t\t\t:class=\"{ 'cn-row-action--destructive': action.destructive }\"\n\t\t\t\t:data-testid=\"`cn-action-item-${slugifyLabel(action.label)}`\"\n\t\t\t\tcloseAfterClick\n\t\t\t\t@click=\"onLinkAction(action, link, $event)\">\n\t\t\t\t<template v-if=\"action.icon\" #icon>\n\t\t\t\t\t<CnIcon v-if=\"typeof action.icon === 'string'\" :name=\"action.icon\" :size=\"20\" />\n\t\t\t\t\t<component :is=\"action.icon\" v-else :size=\"20\" />\n\t\t\t\t</template>\n\t\t\t\t{{ action.label }}\n\t\t\t</NcActionLink>\n\t\t\t<NcActionButton\n\t\t\t\tv-else\n\t\t\t\t:title=\"getTitle(action)\"\n\t\t\t\t:disabled=\"isDisabled(action)\"\n\t\t\t\t:class=\"{ 'cn-row-action--destructive': action.destructive }\"\n\t\t\t\t:data-testid=\"`cn-action-item-${slugifyLabel(action.label)}`\"\n\t\t\t\tcloseAfterClick\n\t\t\t\t@click=\"onAction(action)\">\n\t\t\t\t<template v-if=\"action.icon\" #icon>\n\t\t\t\t\t<CnIcon v-if=\"typeof action.icon === 'string'\" :name=\"action.icon\" :size=\"20\" />\n\t\t\t\t\t<component :is=\"action.icon\" v-else :size=\"20\" />\n\t\t\t\t</template>\n\t\t\t\t{{ action.label }}\n\t\t\t</NcActionButton>\n\t\t</template>\n\t</NcActions>\n</template>\n\n<script>\nimport { NcActionButton, NcActionLink, NcActions } from '@nextcloud/vue'\nimport { followItemActionLink, resolveItemActionLink } from '../../utils/actionLink.js'\nimport { isModifiedClick } from '../../utils/linkNavigation.js'\nimport { evaluateVisibleWhenLocal, isLocallyDecidableVisibleWhen } from '../../utils/visibleWhen.js'\nimport { CnIcon } from '../CnIcon/index.js'\n\n/**\n * CnRowActions — Action menu wrapper for table rows and cards.\n *\n * Wraps NcActions + NcActionButton for consistent row/card action menus.\n * Actions are defined as an array of objects with label, icon, handler, etc.\n *\n * ```vue\n * <CnRowActions\n * :actions=\"[\n * { label: 'Edit', icon: PencilIcon, handler: (row) => editRow(row) },\n * { label: 'Delete', icon: TrashIcon, handler: (row) => deleteRow(row), destructive: true },\n * ]\"\n * :row=\"row\" />\n * ```\n *\n * An action whose only job is to go somewhere carries `href` (a URL) or `to`\n * (a vue-router location) instead of a navigating `handler`, and renders as a\n * real link — middle-click, \"open in new tab\" and \"copy link\" all work:\n *\n * ```vue\n * <CnRowActions\n * :actions=\"[\n * { label: 'View', icon: 'Eye', to: (row) => ({ name: 'LeadDetail', params: { id: row.id } }) },\n * { label: 'Website', icon: 'Web', href: (row) => row.url, linkTarget: '_blank' },\n * ]\"\n * :row=\"row\" />\n * ```\n */\nexport default {\n\tname: 'CnRowActions',\n\n\tcomponents: {\n\t\tNcActions,\n\t\tNcActionButton,\n\t\tNcActionLink,\n\t\tCnIcon,\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * Action definitions.\n\t\t *\n\t\t * Each action supports:\n\t\t * - `label` (string, required) — display text\n\t\t * - `icon` (component | string) — MDI icon. A component renders\n\t\t * directly; a string is treated as a registry name and rendered\n\t\t * via `CnIcon` (PascalCase, e.g. `\"Eye\"`), falling back to the\n\t\t * help-circle when unregistered. The string form lets manifest\n\t\t * (JSON) actions declare icons by name.\n\t\t * - `handler` (function) — called with `row` on click\n\t\t * - `disabled` (boolean | (row) => boolean) — gray out the entry\n\t\t * - `visible` (boolean | (row) => boolean) — when `false`, hide the entry from the menu (default: shown)\n\t\t * - `title` (string | (row) => string) — native tooltip shown on hover (useful to explain why an entry is disabled)\n\t\t * - `destructive` (boolean) — apply error color styling\n\t\t * - `href` (string | (row) => string) — render the entry as a link to this URL\n\t\t * - `to` (string | object | (row) => string | object) — render the entry as a link to this\n\t\t * vue-router location; a plain click routes in place. Ignored when the router cannot resolve it.\n\t\t * - `linkTarget` (string) — the link's `target`, e.g. `_blank`\n\t\t *\n\t\t * A link entry still emits `action`, but its `handler` is not called: the link is the\n\t\t * navigation. A disabled entry, or a `to` without a resolvable route, stays a button.\n\t\t *\n\t\t * @type {Array<{label: string, icon: object | string, handler: (targetItem: object) => void, disabled: boolean | ((targetItem: object) => boolean), visible: boolean | ((targetItem: object) => boolean), title: string | ((targetItem: object) => string), destructive: boolean, href: string | ((targetItem: object) => string), to: string | object | ((targetItem: object) => string | object), linkTarget: string}>}\n\t\t */\n\t\tactions: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/** The row/object data (passed to action handlers) */\n\t\trow: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Whether to use primary styling for the action menu trigger */\n\t\tprimary: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Label shown on the action menu trigger button */\n\t\tmenuName: {\n\t\t\ttype: String,\n\t\t\tdefault: undefined,\n\t\t},\n\t},\n\n\temits: ['action'],\n\n\tcomputed: {\n\t\t/**\n\t\t * Filter actions by their `visible` predicate. An action without a\n\t\t * `visible` field is always shown (backwards compatible).\n\t\t *\n\t\t * @return {Array} Visible actions for the current row.\n\t\t */\n\t\tvisibleActions() {\n\t\t\treturn this.actions.filter((action) => {\n\t\t\t\t// A manifest is JSON and cannot hold a function, so `visibleWhen` is\n\t\t\t\t// the only per-row gate an app configured from one can express. Only\n\t\t\t\t// a locally decidable condition is gated on: an endpoint/source one is\n\t\t\t\t// not this evaluator's question, and nothing gated a row action at all\n\t\t\t\t// before, so answering `false` to it would delete the entry outright.\n\t\t\t\tif (action.visibleWhen\n\t\t\t\t\t&& isLocallyDecidableVisibleWhen(action.visibleWhen)\n\t\t\t\t\t&& evaluateVisibleWhenLocal(action.visibleWhen, this.row) === false) {\n\t\t\t\t\treturn false\n\t\t\t\t}\n\t\t\t\tif (action.visible === undefined) {\n\t\t\t\t\treturn true\n\t\t\t\t}\n\t\t\t\tif (typeof action.visible === 'function') {\n\t\t\t\t\treturn !!action.visible(this.row)\n\t\t\t\t}\n\t\t\t\treturn !!action.visible\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * Visible actions paired with the link each renders as (null for a button).\n\t\t *\n\t\t * @return {Array<{action: object, link: object|null}>}\n\t\t */\n\t\trenderedActions() {\n\t\t\treturn this.visibleActions.map((action) => ({\n\t\t\t\taction,\n\t\t\t\tlink: this.isDisabled(action) ? null : resolveItemActionLink(action, this.row, this.$router),\n\t\t\t}))\n\t\t},\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Resolve disabled state for an action — supports both boolean and function.\n\t\t *\n\t\t * @param {object} action - The action definition\n\t\t * @return {boolean} Whether the action is disabled\n\t\t */\n\t\tisDisabled(action) {\n\t\t\tif (typeof action.disabled === 'function') {\n\t\t\t\treturn action.disabled(this.row)\n\t\t\t}\n\t\t\treturn !!action.disabled\n\t\t},\n\n\t\t/**\n\t\t * Resolve the title (native tooltip) for an action — supports both\n\t\t * string and function forms. Returns undefined when no title is\n\t\t * provided so the attribute is not rendered.\n\t\t *\n\t\t * @param {object} action - The action definition\n\t\t * @return {string|undefined} The resolved tooltip text, or undefined.\n\t\t */\n\t\tgetTitle(action) {\n\t\t\tif (typeof action.title === 'function') {\n\t\t\t\treturn action.title(this.row) || undefined\n\t\t\t}\n\t\t\treturn action.title || undefined\n\t\t},\n\n\t\tonAction(action) {\n\t\t\tif (action.handler && typeof action.handler === 'function') {\n\t\t\t\taction.handler(this.row)\n\t\t\t}\n\t\t\tthis.$emit('action', { action: action.label, row: this.row })\n\t\t},\n\n\t\t/**\n\t\t * A link entry was clicked: route a plain in-app click, leave the rest to\n\t\t * the browser, and emit `action` as a button would. The `handler` is not\n\t\t * called, since navigating is what the link already does. A modified\n\t\t * click opens a new tab, so nothing is emitted in this one.\n\t\t *\n\t\t * @param {object} action The action definition.\n\t\t * @param {object} link The resolved link.\n\t\t * @param {MouseEvent} event The click event.\n\t\t */\n\t\tonLinkAction(action, link, event) {\n\t\t\tif (isModifiedClick(event)) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tfollowItemActionLink(event, link, this.$router)\n\t\t\t/**\n\t\t\t * @event action User picked an entry. Payload: the action's label and the row. A button entry has already run its `handler`; a link entry navigates instead, and a modified (new-tab) click on a link emits nothing.\n\t\t\t * @type {{ action: string, row: object|null }}\n\t\t\t */\n\t\t\tthis.$emit('action', { action: action.label, row: this.row })\n\t\t},\n\n\t\t/**\n\t\t * Slugify an action label for use in stable `data-testid` selectors.\n\t\t * Lowercase, kebab-case, strip non-alphanumeric. Used solely by the\n\t\t * `:data-testid` binding on NcActionButton — does not affect runtime\n\t\t * behaviour or rendered text.\n\t\t *\n\t\t * @param {string} label - The action's display label\n\t\t * @return {string} kebab-case slug suitable for a testid suffix.\n\t\t */\n\t\tslugifyLabel(label) {\n\t\t\treturn String(label || '')\n\t\t\t\t.toLowerCase()\n\t\t\t\t.replace(/[^a-z0-9]+/g, '-')\n\t\t\t\t.replace(/^-+|-+$/g, '')\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-row-action--destructive {\n\tcolor: var(--color-error) !important;\n}\n</style>\n"],"names":["_createBlock","_openBlock","_createElementBlock","_Fragment","_renderList","_normalizeClass","_toDisplayString","_resolveDynamicComponent"],"mappings":";;;;;;;;uBACCA,WAoCW,CAAA,oBAAA,EAAA;IApCC,SAAS,EAAE,QAAc,CAAA,cAAA,CAAC,MAAK,GAAA,CAAA;AACzC,IAAA,OAAO,EAAE,MAAO,CAAA,OAAA;AAChB,IAAA,QAAQ,EAAE,MAAQ,CAAA,QAAA;AACnB,IAAA,aAAW,EAAC,gBAAgB;;qBAClB,MAA0C;AAApD,OAAAC,SAAA,CAAA,IAAA,CAAA,EAAAC,kBAAA,CA+BUC,QA/B2B,EAAA,IAAA,EAAAC,UAAA,CAAA,QAAA,CAAA,eAAe,EAAjC,CAAA,EAAA,MAAM,EAAE,IAAG,EAAA,KAAA;;AAA8B,UAAA,GAAA,EAAA,MAAM,CAAC,KAAK;;UAGhE,IAAI;4BADXJ,WAcc,CAAA,uBAAA,EAAA;;gBAZZ,IAAI,EAAE,IAAI,CAAC,IAAI;gBACf,MAAM,EAAE,IAAI,CAAC,MAAM;gBACnB,KAAK,EAAE,QAAQ,CAAA,QAAA,CAAC,MAAM,CAAA;gBACtB,KAAK,EAAAK,cAAA,CAAA,EAAA,4BAAA,EAAkC,MAAM,CAAC,WAAU,EAAA,CAAA;AACxD,gBAAA,aAAW,EAAoB,CAAA,eAAA,EAAA,QAAA,CAAA,YAAY,CAAC,MAAM,CAAC,KAAK,CAAA,CAAA,CAAA;AACzD,gBAAA,eAAc,EAAd,EAAc;AACb,gBAAA,OAAK,aAAE,QAAY,CAAA,YAAA,CAAC,MAAM,EAAE,IAAI,EAAE,MAAM,CAAA,CAAA;;iCAI/B,MACV;kCADU,GACV,GAAAC,eAAA,CAAG,MAAM,CAAC;;;;AAJM,gBAAA,CAAA,MAAM,CAAC,IAAI;;4BAAG,MAAI;kCACjC,MAA+E;AAA1D,wBAAA,CAAA,OAAA,MAAM,CAAC,IAAK,KAAA,QAAA;0CAAjCN,WAA+E,CAAA,iBAAA,EAAA;;8BAA/B,IAAI,EAAE,MAAM,CAAC,IAAI;AAAG,8BAAA,IAAI,EAAE,EAAE;;0CAC5EA,WAAgD,CAAAO,uBAAA,CAAhC,MAAM,CAAC,IAAI,CAAA,EAAA;;AAAU,8BAAA,IAAI,EAAE,EAAE;;;;;;;4BAI/CP,WAagB,CAAA,yBAAA,EAAA;;gBAXd,KAAK,EAAE,QAAQ,CAAA,QAAA,CAAC,MAAM,CAAA;gBACtB,QAAQ,EAAE,QAAU,CAAA,UAAA,CAAC,MAAM,CAAA;gBAC3B,KAAK,EAAAK,cAAA,CAAA,EAAA,4BAAA,EAAkC,MAAM,CAAC,WAAU,EAAA,CAAA;AACxD,gBAAA,aAAW,EAAoB,CAAA,eAAA,EAAA,QAAA,CAAA,YAAY,CAAC,MAAM,CAAC,KAAK,CAAA,CAAA,CAAA;AACzD,gBAAA,eAAc,EAAd,EAAc;gBACb,OAAK,EAAA,MAAA,KAAE,QAAQ,CAAA,QAAA,CAAC,MAAM,CAAA,CAAA;;iCAIb,MACV;kCADU,GACV,GAAAC,eAAA,CAAG,MAAM,CAAC;;;;AAJM,gBAAA,CAAA,MAAM,CAAC,IAAI;;4BAAG,MAAI;kCACjC,MAA+E;AAA1D,wBAAA,CAAA,OAAA,MAAM,CAAC,IAAK,KAAA,QAAA;0CAAjCN,WAA+E,CAAA,iBAAA,EAAA;;8BAA/B,IAAI,EAAE,MAAM,CAAC,IAAI;AAAG,8BAAA,IAAI,EAAE,EAAE;;0CAC5EA,WAAgD,CAAAO,uBAAA,CAAhC,MAAM,CAAC,IAAI,CAAA,EAAA;;AAAU,8BAAA,IAAI,EAAE,EAAE;;;;;;;;;;;;;;;;"}
|
|
@@ -13,7 +13,8 @@ const DEBOUNCE_MS = 200;
|
|
|
13
13
|
* Data origin (decoupling seam): all data flows through a consumer-supplied
|
|
14
14
|
* `dataSource` prop OR the `cnSpendAnalyticsSource` injection — an object
|
|
15
15
|
* exposing `fetchSummary(args)`, `fetchVendorCommitments(args)`,
|
|
16
|
-
* `fetchNarrative(args)`, and optionally `resolveDeepLink(app, kind, id)
|
|
16
|
+
* `fetchNarrative(args)`, and optionally `resolveDeepLink(app, kind, id)`
|
|
17
|
+
* (pure and synchronous: it runs for every row on every render). The
|
|
17
18
|
* renderer imports NO financeq / procest / launchpad service module and never
|
|
18
19
|
* calls axios itself; the consuming app (typically over GraphQL) owns the
|
|
19
20
|
* transport. The `requires.graphql` hint lives only in the registry entry, not
|
|
@@ -50,8 +51,9 @@ var script = {
|
|
|
50
51
|
* Consumer-supplied data source overriding the
|
|
51
52
|
* `cnSpendAnalyticsSource` injection. Must expose `fetchSummary`,
|
|
52
53
|
* `fetchVendorCommitments`, and `fetchNarrative`; may expose
|
|
53
|
-
* `resolveDeepLink
|
|
54
|
-
*
|
|
54
|
+
* `resolveDeepLink`, which must be pure and synchronous, since it is
|
|
55
|
+
* called for every row on every render. When `null` the injection
|
|
56
|
+
* (then the empty state) is used.
|
|
55
57
|
*
|
|
56
58
|
* @type {object|null}
|
|
57
59
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CnSpendAnalyticsWidget.vue2.js","sources":["../../../../src/components/CnSpendAnalyticsWidget/CnSpendAnalyticsWidget.vue"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n\n<template>\n\t<div class=\"cn-spend-analytics-widget\">\n\t\t<header class=\"cn-spend-analytics-widget__header\">\n\t\t\t<span class=\"cn-spend-analytics-widget__title\">{{ t('nextcloud-vue', 'Spend analytics') }}</span>\n\t\t\t<select\n\t\t\t\tclass=\"cn-spend-analytics-widget__period\"\n\t\t\t\t:value=\"period\"\n\t\t\t\t:aria-label=\"t('nextcloud-vue', 'Reporting period')\"\n\t\t\t\t@change=\"onPeriodChange($event.target.value)\">\n\t\t\t\t<option v-for=\"opt in periodOptions\" :key=\"opt.value\" :value=\"opt.value\">\n\t\t\t\t\t{{ opt.label }}\n\t\t\t\t</option>\n\t\t\t</select>\n\t\t</header>\n\n\t\t<div class=\"cn-spend-analytics-widget__body\">\n\t\t\t<div v-if=\"loading\" class=\"cn-spend-analytics-widget__state\">\n\t\t\t\t{{ t('nextcloud-vue', 'Loading spend data…') }}\n\t\t\t</div>\n\n\t\t\t<!-- No data source wired at all. -->\n\t\t\t<div v-else-if=\"!hasSource\" class=\"cn-spend-analytics-widget__state\">\n\t\t\t\t{{ t('nextcloud-vue', 'No spend data source available') }}\n\t\t\t</div>\n\n\t\t\t<!-- financeq absent → empty-state naming the source. -->\n\t\t\t<div v-else-if=\"financeMissing\" class=\"cn-spend-analytics-widget__state\">\n\t\t\t\t{{ t('nextcloud-vue', 'No spend data — financeq is not installed') }}\n\t\t\t</div>\n\n\t\t\t<!-- financeq present but no data in window. -->\n\t\t\t<div v-else-if=\"financeEmpty\" class=\"cn-spend-analytics-widget__state\">\n\t\t\t\t{{ t('nextcloud-vue', 'No spend recorded for the selected period') }}\n\t\t\t</div>\n\n\t\t\t<template v-else>\n\t\t\t\t<section class=\"cn-spend-analytics-widget__cards\">\n\t\t\t\t\t<div class=\"cn-spend-analytics-widget__card\">\n\t\t\t\t\t\t<span class=\"cn-spend-analytics-widget__card-label\">{{ t('nextcloud-vue', 'Total spend') }}</span>\n\t\t\t\t\t\t<span class=\"cn-spend-analytics-widget__card-value\" :aria-label=\"totalAriaLabel\">\n\t\t\t\t\t\t\t{{ formattedTotal }}\n\t\t\t\t\t\t</span>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"cn-spend-analytics-widget__card\">\n\t\t\t\t\t\t<span class=\"cn-spend-analytics-widget__card-label\">{{ t('nextcloud-vue', 'Categories') }}</span>\n\t\t\t\t\t\t<span class=\"cn-spend-analytics-widget__card-value\">{{ finance.byCategory.length }}</span>\n\t\t\t\t\t</div>\n\t\t\t\t</section>\n\n\t\t\t\t<section v-if=\"viewMode === 'trend'\" class=\"cn-spend-analytics-widget__panel\">\n\t\t\t\t\t<h3 class=\"cn-spend-analytics-widget__panel-title\">\n\t\t\t\t\t\t{{ t('nextcloud-vue', 'Spend trend') }}\n\t\t\t\t\t</h3>\n\t\t\t\t\t<ul class=\"cn-spend-analytics-widget__rows\">\n\t\t\t\t\t\t<li v-for=\"row in finance.trend\" :key=\"row.month\" class=\"cn-spend-analytics-widget__row cn-spend-analytics-widget__row--static\">\n\t\t\t\t\t\t\t<span>{{ row.month }}</span>\n\t\t\t\t\t\t\t<span>{{ formatAmount(row.amount) }}</span>\n\t\t\t\t\t\t</li>\n\t\t\t\t\t</ul>\n\t\t\t\t</section>\n\n\t\t\t\t<section v-else-if=\"viewMode === 'top-categories'\" class=\"cn-spend-analytics-widget__panel\">\n\t\t\t\t\t<h3 class=\"cn-spend-analytics-widget__panel-title\">\n\t\t\t\t\t\t{{ t('nextcloud-vue', 'Top categories') }}\n\t\t\t\t\t</h3>\n\t\t\t\t\t<ul class=\"cn-spend-analytics-widget__rows\">\n\t\t\t\t\t\t<li v-for=\"(row, i) in finance.byCategory\" :key=\"row.category\">\n\t\t\t\t\t\t\t<a v-if=\"categoryLinks[i]\" class=\"cn-spend-analytics-widget__row\" :href=\"categoryLinks[i]\">\n\t\t\t\t\t\t\t\t<span>{{ row.category }}</span>\n\t\t\t\t\t\t\t\t<span>{{ formatAmount(row.amount) }}</span>\n\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t<div v-else class=\"cn-spend-analytics-widget__row cn-spend-analytics-widget__row--static\">\n\t\t\t\t\t\t\t\t<span>{{ row.category }}</span>\n\t\t\t\t\t\t\t\t<span>{{ formatAmount(row.amount) }}</span>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</li>\n\t\t\t\t\t</ul>\n\t\t\t\t</section>\n\n\t\t\t\t<section v-else-if=\"viewMode === 'top-vendors'\" class=\"cn-spend-analytics-widget__panel\">\n\t\t\t\t\t<h3 class=\"cn-spend-analytics-widget__panel-title\">\n\t\t\t\t\t\t{{ t('nextcloud-vue', 'Top vendors') }}\n\t\t\t\t\t</h3>\n\t\t\t\t\t<div v-if=\"vendorMissing\" class=\"cn-spend-analytics-widget__inline-empty\">\n\t\t\t\t\t\t{{ t('nextcloud-vue', 'No vendor data — procest is not installed') }}\n\t\t\t\t\t</div>\n\t\t\t\t\t<div v-else-if=\"vendorEmpty\" class=\"cn-spend-analytics-widget__inline-empty\">\n\t\t\t\t\t\t{{ t('nextcloud-vue', 'No vendor commitments for the selected period') }}\n\t\t\t\t\t</div>\n\t\t\t\t\t<ul v-else class=\"cn-spend-analytics-widget__rows\">\n\t\t\t\t\t\t<li v-for=\"(row, i) in vendor.topVendors\" :key=\"row.vendor\">\n\t\t\t\t\t\t\t<a v-if=\"vendorLinks[i]\" class=\"cn-spend-analytics-widget__row\" :href=\"vendorLinks[i]\">\n\t\t\t\t\t\t\t\t<span>{{ row.vendor }}</span>\n\t\t\t\t\t\t\t\t<span>{{ formatAmount(row.committed) }}</span>\n\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t<div v-else class=\"cn-spend-analytics-widget__row cn-spend-analytics-widget__row--static\">\n\t\t\t\t\t\t\t\t<span>{{ row.vendor }}</span>\n\t\t\t\t\t\t\t\t<span>{{ formatAmount(row.committed) }}</span>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</li>\n\t\t\t\t\t</ul>\n\t\t\t\t</section>\n\n\t\t\t\t<section v-if=\"aiEnabled\" class=\"cn-spend-analytics-widget__panel\">\n\t\t\t\t\t<h3 class=\"cn-spend-analytics-widget__panel-title\">\n\t\t\t\t\t\t{{ t('nextcloud-vue', 'AI insight') }}\n\t\t\t\t\t</h3>\n\t\t\t\t\t<div v-if=\"aiUnavailable\" class=\"cn-spend-analytics-widget__inline-empty\">\n\t\t\t\t\t\t{{ t('nextcloud-vue', 'AI insight unavailable — the source is not configured') }}\n\t\t\t\t\t</div>\n\t\t\t\t\t<p v-else-if=\"aiNarrative\" class=\"cn-spend-analytics-widget__ai-text\">\n\t\t\t\t\t\t{{ aiNarrative }}\n\t\t\t\t\t</p>\n\t\t\t\t\t<button\n\t\t\t\t\t\tv-else\n\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\tclass=\"cn-spend-analytics-widget__ai-btn\"\n\t\t\t\t\t\t@click=\"requestNarrative\">\n\t\t\t\t\t\t{{ t('nextcloud-vue', 'Generate insight') }}\n\t\t\t\t\t</button>\n\t\t\t\t</section>\n\t\t\t</template>\n\t\t</div>\n\t</div>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { safeCurrencyCode } from '../../utils/formatMetric.js'\n\nconst VIEW_MODES = ['summary', 'trend', 'top-vendors', 'top-categories']\nconst PERIODS = ['month', 'quarter', 'ytd', 'fy']\nconst DEBOUNCE_MS = 200\n\n/**\n * CnSpendAnalyticsWidget — dashboard renderer for a `spend-analytics`\n * placement. Renders summary / trend / top-vendors / top-categories views over\n * spend data with graceful per-source empty states and row drill-through.\n *\n * Data origin (decoupling seam): all data flows through a consumer-supplied\n * `dataSource` prop OR the `cnSpendAnalyticsSource` injection — an object\n * exposing `fetchSummary(args)`, `fetchVendorCommitments(args)`,\n * `fetchNarrative(args)`, and optionally `resolveDeepLink(app, kind, id)`. The\n * renderer imports NO financeq / procest / launchpad service module and never\n * calls axios itself; the consuming app (typically over GraphQL) owns the\n * transport. The `requires.graphql` hint lives only in the registry entry, not\n * in any manifest dependency.\n *\n * Graceful degradation: with no source the widget shows \"No spend data source\n * available\"; the source signals per-sibling availability via\n * `{ available, empty }` envelopes so a partial fleet still renders\n * (\"financeq is not installed\" / \"procest is not installed\").\n *\n * @spec openspec/changes/cn-widget-library/specs/cn-widget-library/spec.md\n */\nexport default {\n\tname: 'CnSpendAnalyticsWidget',\n\n\tinject: {\n\t\tcnSpendAnalyticsSource: {\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * Persisted widget content `{period, viewMode, filters, aiInsights}`.\n\t\t *\n\t\t * @type {object}\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 * Consumer-supplied data source overriding the\n\t\t * `cnSpendAnalyticsSource` injection. Must expose `fetchSummary`,\n\t\t * `fetchVendorCommitments`, and `fetchNarrative`; may expose\n\t\t * `resolveDeepLink`. When `null` the injection (then the empty state)\n\t\t * is used.\n\t\t *\n\t\t * @type {object|null}\n\t\t */\n\t\tdataSource: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\tdata() {\n\t\treturn {\n\t\t\tperiod: PERIODS.includes(this.content && this.content.period) ? this.content.period : 'quarter',\n\t\t\tloading: false,\n\t\t\tfinance: { available: false, empty: false, total: 0, currency: 'EUR', byCategory: [], trend: [] },\n\t\t\tvendor: { available: false, empty: false, topVendors: [] },\n\t\t\taiNarrative: '',\n\t\t\taiUnavailable: false,\n\t\t\tdebounceTimer: null,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * The active data source (prop wins over injection).\n\t\t *\n\t\t * @return {object|null} the resolved source, or `null`.\n\t\t */\n\t\tsource() {\n\t\t\tconst candidate = this.dataSource || this.cnSpendAnalyticsSource\n\t\t\treturn candidate && typeof candidate.fetchSummary === 'function' ? candidate : null\n\t\t},\n\n\t\t/**\n\t\t * Whether a usable spend source is configured.\n\t\t *\n\t\t * @return {boolean} true when a source exists.\n\t\t */\n\t\thasSource() {\n\t\t\treturn this.source !== null\n\t\t},\n\n\t\t/**\n\t\t * Deep link of each top-category row, by index ('' when unresolved).\n\t\t *\n\t\t * @return {string[]}\n\t\t */\n\t\tcategoryLinks() {\n\t\t\treturn (this.finance.byCategory || []).map((row) => this.deepLink('financeq', 'category', row.category))\n\t\t},\n\n\t\t/**\n\t\t * Deep link of each top-vendor row, by index ('' when unresolved).\n\t\t *\n\t\t * @return {string[]}\n\t\t */\n\t\tvendorLinks() {\n\t\t\treturn (this.vendor.topVendors || []).map((row) => this.deepLink('procest', 'vendor', row.vendor))\n\t\t},\n\n\t\t/**\n\t\t * The validated view mode.\n\t\t *\n\t\t * @return {string} the view mode.\n\t\t */\n\t\tviewMode() {\n\t\t\treturn VIEW_MODES.includes(this.content && this.content.viewMode) ? this.content.viewMode : 'summary'\n\t\t},\n\n\t\t/**\n\t\t * Whether the AI narrative panel is enabled.\n\t\t *\n\t\t * @return {boolean} the AI-enabled flag.\n\t\t */\n\t\taiEnabled() {\n\t\t\treturn !!(this.content && this.content.aiInsights && this.content.aiInsights.enabled === true)\n\t\t},\n\n\t\t/**\n\t\t * Whether financeq is absent.\n\t\t *\n\t\t * @return {boolean} true when finance is unavailable.\n\t\t */\n\t\tfinanceMissing() {\n\t\t\treturn this.finance.available === false\n\t\t},\n\n\t\t/**\n\t\t * Whether financeq is present but has no data in the window.\n\t\t *\n\t\t * @return {boolean} true when finance is available but empty.\n\t\t */\n\t\tfinanceEmpty() {\n\t\t\treturn this.finance.available === true && this.finance.empty === true\n\t\t},\n\n\t\t/**\n\t\t * Whether procest is absent.\n\t\t *\n\t\t * @return {boolean} true when vendor data is unavailable.\n\t\t */\n\t\tvendorMissing() {\n\t\t\treturn this.vendor.available === false\n\t\t},\n\n\t\t/**\n\t\t * Whether procest is present but has no vendor data in the window.\n\t\t *\n\t\t * @return {boolean} true when vendor is available but empty.\n\t\t */\n\t\tvendorEmpty() {\n\t\t\treturn this.vendor.available === true && this.vendor.empty === true\n\t\t},\n\n\t\t/**\n\t\t * The reporting-period dropdown options.\n\t\t *\n\t\t * @return {Array<{value: string, label: string}>} the options.\n\t\t */\n\t\tperiodOptions() {\n\t\t\treturn [\n\t\t\t\t{ value: 'month', label: t('nextcloud-vue', 'This month') },\n\t\t\t\t{ value: 'quarter', label: t('nextcloud-vue', 'This quarter') },\n\t\t\t\t{ value: 'ytd', label: t('nextcloud-vue', 'Year to date') },\n\t\t\t\t{ value: 'fy', label: t('nextcloud-vue', 'Fiscal year') },\n\t\t\t]\n\t\t},\n\n\t\t/**\n\t\t * The formatted total-spend amount.\n\t\t *\n\t\t * @return {string} the formatted total.\n\t\t */\n\t\tformattedTotal() {\n\t\t\treturn this.formatAmount(this.finance.total)\n\t\t},\n\n\t\t/**\n\t\t * The accessible label for the total-spend value.\n\t\t *\n\t\t * @return {string} the aria label.\n\t\t */\n\t\ttotalAriaLabel() {\n\t\t\treturn t('nextcloud-vue', 'Total spend: {amount}').replace('{amount}', this.formattedTotal)\n\t\t},\n\t},\n\n\twatch: {\n\t\tviewMode() {\n\t\t\tthis.scheduleFetch()\n\t\t},\n\t},\n\n\tmounted() {\n\t\tthis.fetchData()\n\t},\n\n\tbeforeUnmount() {\n\t\tif (this.debounceTimer) {\n\t\t\tclearTimeout(this.debounceTimer)\n\t\t}\n\t},\n\n\tmethods: {\n\t\tt,\n\n\t\t/**\n\t\t * Handle a reporting-period change.\n\t\t *\n\t\t * @param {string} value the new period.\n\t\t * @return {void}\n\t\t */\n\t\tonPeriodChange(value) {\n\t\t\tif (PERIODS.includes(value)) {\n\t\t\t\tthis.period = value\n\t\t\t\tthis.scheduleFetch()\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Debounce period / view changes before re-querying.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tscheduleFetch() {\n\t\t\tif (this.debounceTimer) {\n\t\t\t\tclearTimeout(this.debounceTimer)\n\t\t\t}\n\t\t\tthis.debounceTimer = setTimeout(() => {\n\t\t\t\tthis.fetchData()\n\t\t\t}, DEBOUNCE_MS)\n\t\t},\n\n\t\t/**\n\t\t * Fetch finance (always) + vendor (top-vendors view only) data in\n\t\t * parallel through the source. A missing source or a failure degrades\n\t\t * to the relevant empty state without throwing.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync fetchData() {\n\t\t\tif (!this.source) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.loading = true\n\t\t\tconst filters = (this.content && this.content.filters) || {}\n\t\t\ttry {\n\t\t\t\tconst tasks = [\n\t\t\t\t\tthis.source.fetchSummary({\n\t\t\t\t\t\tperiod: this.period,\n\t\t\t\t\t\tcategoryIds: Array.isArray(filters.categoryIds) ? filters.categoryIds : [],\n\t\t\t\t\t\tdepartmentIds: Array.isArray(filters.departmentIds) ? filters.departmentIds : [],\n\t\t\t\t\t}),\n\t\t\t\t]\n\t\t\t\tif (this.viewMode === 'top-vendors' && typeof this.source.fetchVendorCommitments === 'function') {\n\t\t\t\t\ttasks.push(this.source.fetchVendorCommitments({\n\t\t\t\t\t\tperiod: this.period,\n\t\t\t\t\t\tvendorIds: Array.isArray(filters.vendorIds) ? filters.vendorIds : [],\n\t\t\t\t\t}))\n\t\t\t\t}\n\t\t\t\tconst [finance, vendor] = await Promise.all(tasks)\n\t\t\t\tif (finance) {\n\t\t\t\t\tthis.finance = finance\n\t\t\t\t}\n\t\t\t\tif (vendor) {\n\t\t\t\t\tthis.vendor = vendor\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\tthis.finance = { available: false, empty: false, total: 0, currency: 'EUR', byCategory: [], trend: [] }\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Request an AI narrative through the source. When the source lacks\n\t\t * `fetchNarrative` or reports it unavailable, the panel disables\n\t\t * gracefully.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync requestNarrative() {\n\t\t\tif (!this.source || typeof this.source.fetchNarrative !== 'function') {\n\t\t\t\tthis.aiUnavailable = true\n\t\t\t\treturn\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tconst result = await this.source.fetchNarrative({\n\t\t\t\t\tsummary: { total: this.finance.total, currency: this.finance.currency, period: this.period },\n\t\t\t\t}) || {}\n\t\t\t\tif (result.available) {\n\t\t\t\t\tthis.aiNarrative = result.narrative\n\t\t\t\t\tthis.aiUnavailable = false\n\t\t\t\t} else {\n\t\t\t\t\tthis.aiUnavailable = true\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\tthis.aiUnavailable = true\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * The owning sibling app's URL for a row. The source may provide\n\t\t * `resolveDeepLink`; absent that, the row renders as plain text.\n\t\t *\n\t\t * @param {string} app the owning app id.\n\t\t * @param {string} kind the entity kind.\n\t\t * @param {string} id the entity id.\n\t\t * @return {string} the URL, or '' when unresolved.\n\t\t */\n\t\tdeepLink(app, kind, id) {\n\t\t\tif (this.source && typeof this.source.resolveDeepLink === 'function') {\n\t\t\t\tconst href = this.source.resolveDeepLink(app, kind, id)\n\t\t\t\tif (typeof href === 'string' && href !== '') {\n\t\t\t\t\treturn href\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn ''\n\t\t},\n\n\t\t/**\n\t\t * Format a monetary amount respecting the browser locale.\n\t\t *\n\t\t * @param {number} amount the amount.\n\t\t * @return {string} the locale-formatted currency string.\n\t\t */\n\t\tformatAmount(amount) {\n\t\t\tconst value = Number(amount) || 0\n\t\t\tconst locale = (typeof navigator !== 'undefined' && navigator.language) || 'en'\n\t\t\ttry {\n\t\t\t\treturn new Intl.NumberFormat(locale, {\n\t\t\t\t\tstyle: 'currency',\n\t\t\t\t\tcurrency: safeCurrencyCode(this.finance.currency),\n\t\t\t\t}).format(value)\n\t\t\t} catch {\n\t\t\t\treturn String(value)\n\t\t\t}\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-spend-analytics-widget {\n\tdisplay: flex;\n\tflex-direction: column;\n\theight: 100%;\n\tgap: 8px;\n\tpadding: 8px;\n\toverflow: hidden;\n}\n\n.cn-spend-analytics-widget__header {\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: space-between;\n}\n\n.cn-spend-analytics-widget__title {\n\tfont-weight: bold;\n}\n\n.cn-spend-analytics-widget__period {\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius);\n\tbackground: var(--color-main-background);\n\tcolor: var(--color-main-text);\n\tpadding: 2px 6px;\n}\n\n.cn-spend-analytics-widget__body {\n\tflex: 1;\n\toverflow: auto;\n}\n\n.cn-spend-analytics-widget__cards {\n\tdisplay: flex;\n\tgap: 12px;\n}\n\n.cn-spend-analytics-widget__card {\n\tdisplay: flex;\n\tflex-direction: column;\n\tpadding: 8px 12px;\n\tborder-radius: var(--border-radius);\n\tbackground: var(--color-background-hover);\n\tflex: 1;\n}\n\n.cn-spend-analytics-widget__card-label {\n\tfont-size: 0.85em;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-spend-analytics-widget__card-value {\n\tfont-size: 1.4em;\n\tfont-weight: bold;\n}\n\n.cn-spend-analytics-widget__panel-title {\n\tfont-size: 1em;\n\tmargin: 8px 0 4px;\n}\n\n.cn-spend-analytics-widget__rows {\n\tlist-style: none;\n\tmargin: 0;\n\tpadding: 0;\n}\n\n.cn-spend-analytics-widget__row {\n\tdisplay: flex;\n\tjustify-content: space-between;\n\tpadding: 4px 8px;\n\tborder-radius: var(--border-radius);\n\tcursor: pointer;\n\tcolor: inherit;\n\ttext-decoration: none;\n}\n\n.cn-spend-analytics-widget__row--static {\n\tcursor: default;\n}\n\n.cn-spend-analytics-widget__row:hover,\n.cn-spend-analytics-widget__row:focus {\n\tbackground: var(--color-background-hover);\n\toutline: none;\n}\n\n.cn-spend-analytics-widget__state {\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: center;\n\tmin-height: 64px;\n\tcolor: var(--color-text-maxcontrast);\n\tpadding: 12px;\n\ttext-align: center;\n}\n\n.cn-spend-analytics-widget__inline-empty {\n\tcolor: var(--color-text-maxcontrast);\n\tpadding: 6px;\n\ttext-align: left;\n}\n\n.cn-spend-analytics-widget__ai-text {\n\twhite-space: pre-wrap;\n}\n\n.cn-spend-analytics-widget__ai-btn {\n\tborder: 1px solid var(--color-border);\n\tbackground: var(--color-background-hover);\n\tborder-radius: var(--border-radius);\n\tcolor: var(--color-main-text);\n\tpadding: 4px 12px;\n\tcursor: pointer;\n}\n</style>\n"],"names":["t"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"CnSpendAnalyticsWidget.vue2.js","sources":["../../../../src/components/CnSpendAnalyticsWidget/CnSpendAnalyticsWidget.vue"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n\n<template>\n\t<div class=\"cn-spend-analytics-widget\">\n\t\t<header class=\"cn-spend-analytics-widget__header\">\n\t\t\t<span class=\"cn-spend-analytics-widget__title\">{{ t('nextcloud-vue', 'Spend analytics') }}</span>\n\t\t\t<select\n\t\t\t\tclass=\"cn-spend-analytics-widget__period\"\n\t\t\t\t:value=\"period\"\n\t\t\t\t:aria-label=\"t('nextcloud-vue', 'Reporting period')\"\n\t\t\t\t@change=\"onPeriodChange($event.target.value)\">\n\t\t\t\t<option v-for=\"opt in periodOptions\" :key=\"opt.value\" :value=\"opt.value\">\n\t\t\t\t\t{{ opt.label }}\n\t\t\t\t</option>\n\t\t\t</select>\n\t\t</header>\n\n\t\t<div class=\"cn-spend-analytics-widget__body\">\n\t\t\t<div v-if=\"loading\" class=\"cn-spend-analytics-widget__state\">\n\t\t\t\t{{ t('nextcloud-vue', 'Loading spend data…') }}\n\t\t\t</div>\n\n\t\t\t<!-- No data source wired at all. -->\n\t\t\t<div v-else-if=\"!hasSource\" class=\"cn-spend-analytics-widget__state\">\n\t\t\t\t{{ t('nextcloud-vue', 'No spend data source available') }}\n\t\t\t</div>\n\n\t\t\t<!-- financeq absent → empty-state naming the source. -->\n\t\t\t<div v-else-if=\"financeMissing\" class=\"cn-spend-analytics-widget__state\">\n\t\t\t\t{{ t('nextcloud-vue', 'No spend data — financeq is not installed') }}\n\t\t\t</div>\n\n\t\t\t<!-- financeq present but no data in window. -->\n\t\t\t<div v-else-if=\"financeEmpty\" class=\"cn-spend-analytics-widget__state\">\n\t\t\t\t{{ t('nextcloud-vue', 'No spend recorded for the selected period') }}\n\t\t\t</div>\n\n\t\t\t<template v-else>\n\t\t\t\t<section class=\"cn-spend-analytics-widget__cards\">\n\t\t\t\t\t<div class=\"cn-spend-analytics-widget__card\">\n\t\t\t\t\t\t<span class=\"cn-spend-analytics-widget__card-label\">{{ t('nextcloud-vue', 'Total spend') }}</span>\n\t\t\t\t\t\t<span class=\"cn-spend-analytics-widget__card-value\" :aria-label=\"totalAriaLabel\">\n\t\t\t\t\t\t\t{{ formattedTotal }}\n\t\t\t\t\t\t</span>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"cn-spend-analytics-widget__card\">\n\t\t\t\t\t\t<span class=\"cn-spend-analytics-widget__card-label\">{{ t('nextcloud-vue', 'Categories') }}</span>\n\t\t\t\t\t\t<span class=\"cn-spend-analytics-widget__card-value\">{{ finance.byCategory.length }}</span>\n\t\t\t\t\t</div>\n\t\t\t\t</section>\n\n\t\t\t\t<section v-if=\"viewMode === 'trend'\" class=\"cn-spend-analytics-widget__panel\">\n\t\t\t\t\t<h3 class=\"cn-spend-analytics-widget__panel-title\">\n\t\t\t\t\t\t{{ t('nextcloud-vue', 'Spend trend') }}\n\t\t\t\t\t</h3>\n\t\t\t\t\t<ul class=\"cn-spend-analytics-widget__rows\">\n\t\t\t\t\t\t<li v-for=\"row in finance.trend\" :key=\"row.month\" class=\"cn-spend-analytics-widget__row cn-spend-analytics-widget__row--static\">\n\t\t\t\t\t\t\t<span>{{ row.month }}</span>\n\t\t\t\t\t\t\t<span>{{ formatAmount(row.amount) }}</span>\n\t\t\t\t\t\t</li>\n\t\t\t\t\t</ul>\n\t\t\t\t</section>\n\n\t\t\t\t<section v-else-if=\"viewMode === 'top-categories'\" class=\"cn-spend-analytics-widget__panel\">\n\t\t\t\t\t<h3 class=\"cn-spend-analytics-widget__panel-title\">\n\t\t\t\t\t\t{{ t('nextcloud-vue', 'Top categories') }}\n\t\t\t\t\t</h3>\n\t\t\t\t\t<ul class=\"cn-spend-analytics-widget__rows\">\n\t\t\t\t\t\t<li v-for=\"(row, i) in finance.byCategory\" :key=\"row.category\">\n\t\t\t\t\t\t\t<a v-if=\"categoryLinks[i]\" class=\"cn-spend-analytics-widget__row\" :href=\"categoryLinks[i]\">\n\t\t\t\t\t\t\t\t<span>{{ row.category }}</span>\n\t\t\t\t\t\t\t\t<span>{{ formatAmount(row.amount) }}</span>\n\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t<div v-else class=\"cn-spend-analytics-widget__row cn-spend-analytics-widget__row--static\">\n\t\t\t\t\t\t\t\t<span>{{ row.category }}</span>\n\t\t\t\t\t\t\t\t<span>{{ formatAmount(row.amount) }}</span>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</li>\n\t\t\t\t\t</ul>\n\t\t\t\t</section>\n\n\t\t\t\t<section v-else-if=\"viewMode === 'top-vendors'\" class=\"cn-spend-analytics-widget__panel\">\n\t\t\t\t\t<h3 class=\"cn-spend-analytics-widget__panel-title\">\n\t\t\t\t\t\t{{ t('nextcloud-vue', 'Top vendors') }}\n\t\t\t\t\t</h3>\n\t\t\t\t\t<div v-if=\"vendorMissing\" class=\"cn-spend-analytics-widget__inline-empty\">\n\t\t\t\t\t\t{{ t('nextcloud-vue', 'No vendor data — procest is not installed') }}\n\t\t\t\t\t</div>\n\t\t\t\t\t<div v-else-if=\"vendorEmpty\" class=\"cn-spend-analytics-widget__inline-empty\">\n\t\t\t\t\t\t{{ t('nextcloud-vue', 'No vendor commitments for the selected period') }}\n\t\t\t\t\t</div>\n\t\t\t\t\t<ul v-else class=\"cn-spend-analytics-widget__rows\">\n\t\t\t\t\t\t<li v-for=\"(row, i) in vendor.topVendors\" :key=\"row.vendor\">\n\t\t\t\t\t\t\t<a v-if=\"vendorLinks[i]\" class=\"cn-spend-analytics-widget__row\" :href=\"vendorLinks[i]\">\n\t\t\t\t\t\t\t\t<span>{{ row.vendor }}</span>\n\t\t\t\t\t\t\t\t<span>{{ formatAmount(row.committed) }}</span>\n\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t<div v-else class=\"cn-spend-analytics-widget__row cn-spend-analytics-widget__row--static\">\n\t\t\t\t\t\t\t\t<span>{{ row.vendor }}</span>\n\t\t\t\t\t\t\t\t<span>{{ formatAmount(row.committed) }}</span>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</li>\n\t\t\t\t\t</ul>\n\t\t\t\t</section>\n\n\t\t\t\t<section v-if=\"aiEnabled\" class=\"cn-spend-analytics-widget__panel\">\n\t\t\t\t\t<h3 class=\"cn-spend-analytics-widget__panel-title\">\n\t\t\t\t\t\t{{ t('nextcloud-vue', 'AI insight') }}\n\t\t\t\t\t</h3>\n\t\t\t\t\t<div v-if=\"aiUnavailable\" class=\"cn-spend-analytics-widget__inline-empty\">\n\t\t\t\t\t\t{{ t('nextcloud-vue', 'AI insight unavailable — the source is not configured') }}\n\t\t\t\t\t</div>\n\t\t\t\t\t<p v-else-if=\"aiNarrative\" class=\"cn-spend-analytics-widget__ai-text\">\n\t\t\t\t\t\t{{ aiNarrative }}\n\t\t\t\t\t</p>\n\t\t\t\t\t<button\n\t\t\t\t\t\tv-else\n\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\tclass=\"cn-spend-analytics-widget__ai-btn\"\n\t\t\t\t\t\t@click=\"requestNarrative\">\n\t\t\t\t\t\t{{ t('nextcloud-vue', 'Generate insight') }}\n\t\t\t\t\t</button>\n\t\t\t\t</section>\n\t\t\t</template>\n\t\t</div>\n\t</div>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { safeCurrencyCode } from '../../utils/formatMetric.js'\n\nconst VIEW_MODES = ['summary', 'trend', 'top-vendors', 'top-categories']\nconst PERIODS = ['month', 'quarter', 'ytd', 'fy']\nconst DEBOUNCE_MS = 200\n\n/**\n * CnSpendAnalyticsWidget — dashboard renderer for a `spend-analytics`\n * placement. Renders summary / trend / top-vendors / top-categories views over\n * spend data with graceful per-source empty states and row drill-through.\n *\n * Data origin (decoupling seam): all data flows through a consumer-supplied\n * `dataSource` prop OR the `cnSpendAnalyticsSource` injection — an object\n * exposing `fetchSummary(args)`, `fetchVendorCommitments(args)`,\n * `fetchNarrative(args)`, and optionally `resolveDeepLink(app, kind, id)`\n * (pure and synchronous: it runs for every row on every render). The\n * renderer imports NO financeq / procest / launchpad service module and never\n * calls axios itself; the consuming app (typically over GraphQL) owns the\n * transport. The `requires.graphql` hint lives only in the registry entry, not\n * in any manifest dependency.\n *\n * Graceful degradation: with no source the widget shows \"No spend data source\n * available\"; the source signals per-sibling availability via\n * `{ available, empty }` envelopes so a partial fleet still renders\n * (\"financeq is not installed\" / \"procest is not installed\").\n *\n * @spec openspec/changes/cn-widget-library/specs/cn-widget-library/spec.md\n */\nexport default {\n\tname: 'CnSpendAnalyticsWidget',\n\n\tinject: {\n\t\tcnSpendAnalyticsSource: {\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * Persisted widget content `{period, viewMode, filters, aiInsights}`.\n\t\t *\n\t\t * @type {object}\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 * Consumer-supplied data source overriding the\n\t\t * `cnSpendAnalyticsSource` injection. Must expose `fetchSummary`,\n\t\t * `fetchVendorCommitments`, and `fetchNarrative`; may expose\n\t\t * `resolveDeepLink`, which must be pure and synchronous, since it is\n\t\t * called for every row on every render. When `null` the injection\n\t\t * (then the empty state) is used.\n\t\t *\n\t\t * @type {object|null}\n\t\t */\n\t\tdataSource: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\tdata() {\n\t\treturn {\n\t\t\tperiod: PERIODS.includes(this.content && this.content.period) ? this.content.period : 'quarter',\n\t\t\tloading: false,\n\t\t\tfinance: { available: false, empty: false, total: 0, currency: 'EUR', byCategory: [], trend: [] },\n\t\t\tvendor: { available: false, empty: false, topVendors: [] },\n\t\t\taiNarrative: '',\n\t\t\taiUnavailable: false,\n\t\t\tdebounceTimer: null,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * The active data source (prop wins over injection).\n\t\t *\n\t\t * @return {object|null} the resolved source, or `null`.\n\t\t */\n\t\tsource() {\n\t\t\tconst candidate = this.dataSource || this.cnSpendAnalyticsSource\n\t\t\treturn candidate && typeof candidate.fetchSummary === 'function' ? candidate : null\n\t\t},\n\n\t\t/**\n\t\t * Whether a usable spend source is configured.\n\t\t *\n\t\t * @return {boolean} true when a source exists.\n\t\t */\n\t\thasSource() {\n\t\t\treturn this.source !== null\n\t\t},\n\n\t\t/**\n\t\t * Deep link of each top-category row, by index ('' when unresolved).\n\t\t *\n\t\t * @return {string[]}\n\t\t */\n\t\tcategoryLinks() {\n\t\t\treturn (this.finance.byCategory || []).map((row) => this.deepLink('financeq', 'category', row.category))\n\t\t},\n\n\t\t/**\n\t\t * Deep link of each top-vendor row, by index ('' when unresolved).\n\t\t *\n\t\t * @return {string[]}\n\t\t */\n\t\tvendorLinks() {\n\t\t\treturn (this.vendor.topVendors || []).map((row) => this.deepLink('procest', 'vendor', row.vendor))\n\t\t},\n\n\t\t/**\n\t\t * The validated view mode.\n\t\t *\n\t\t * @return {string} the view mode.\n\t\t */\n\t\tviewMode() {\n\t\t\treturn VIEW_MODES.includes(this.content && this.content.viewMode) ? this.content.viewMode : 'summary'\n\t\t},\n\n\t\t/**\n\t\t * Whether the AI narrative panel is enabled.\n\t\t *\n\t\t * @return {boolean} the AI-enabled flag.\n\t\t */\n\t\taiEnabled() {\n\t\t\treturn !!(this.content && this.content.aiInsights && this.content.aiInsights.enabled === true)\n\t\t},\n\n\t\t/**\n\t\t * Whether financeq is absent.\n\t\t *\n\t\t * @return {boolean} true when finance is unavailable.\n\t\t */\n\t\tfinanceMissing() {\n\t\t\treturn this.finance.available === false\n\t\t},\n\n\t\t/**\n\t\t * Whether financeq is present but has no data in the window.\n\t\t *\n\t\t * @return {boolean} true when finance is available but empty.\n\t\t */\n\t\tfinanceEmpty() {\n\t\t\treturn this.finance.available === true && this.finance.empty === true\n\t\t},\n\n\t\t/**\n\t\t * Whether procest is absent.\n\t\t *\n\t\t * @return {boolean} true when vendor data is unavailable.\n\t\t */\n\t\tvendorMissing() {\n\t\t\treturn this.vendor.available === false\n\t\t},\n\n\t\t/**\n\t\t * Whether procest is present but has no vendor data in the window.\n\t\t *\n\t\t * @return {boolean} true when vendor is available but empty.\n\t\t */\n\t\tvendorEmpty() {\n\t\t\treturn this.vendor.available === true && this.vendor.empty === true\n\t\t},\n\n\t\t/**\n\t\t * The reporting-period dropdown options.\n\t\t *\n\t\t * @return {Array<{value: string, label: string}>} the options.\n\t\t */\n\t\tperiodOptions() {\n\t\t\treturn [\n\t\t\t\t{ value: 'month', label: t('nextcloud-vue', 'This month') },\n\t\t\t\t{ value: 'quarter', label: t('nextcloud-vue', 'This quarter') },\n\t\t\t\t{ value: 'ytd', label: t('nextcloud-vue', 'Year to date') },\n\t\t\t\t{ value: 'fy', label: t('nextcloud-vue', 'Fiscal year') },\n\t\t\t]\n\t\t},\n\n\t\t/**\n\t\t * The formatted total-spend amount.\n\t\t *\n\t\t * @return {string} the formatted total.\n\t\t */\n\t\tformattedTotal() {\n\t\t\treturn this.formatAmount(this.finance.total)\n\t\t},\n\n\t\t/**\n\t\t * The accessible label for the total-spend value.\n\t\t *\n\t\t * @return {string} the aria label.\n\t\t */\n\t\ttotalAriaLabel() {\n\t\t\treturn t('nextcloud-vue', 'Total spend: {amount}').replace('{amount}', this.formattedTotal)\n\t\t},\n\t},\n\n\twatch: {\n\t\tviewMode() {\n\t\t\tthis.scheduleFetch()\n\t\t},\n\t},\n\n\tmounted() {\n\t\tthis.fetchData()\n\t},\n\n\tbeforeUnmount() {\n\t\tif (this.debounceTimer) {\n\t\t\tclearTimeout(this.debounceTimer)\n\t\t}\n\t},\n\n\tmethods: {\n\t\tt,\n\n\t\t/**\n\t\t * Handle a reporting-period change.\n\t\t *\n\t\t * @param {string} value the new period.\n\t\t * @return {void}\n\t\t */\n\t\tonPeriodChange(value) {\n\t\t\tif (PERIODS.includes(value)) {\n\t\t\t\tthis.period = value\n\t\t\t\tthis.scheduleFetch()\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Debounce period / view changes before re-querying.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tscheduleFetch() {\n\t\t\tif (this.debounceTimer) {\n\t\t\t\tclearTimeout(this.debounceTimer)\n\t\t\t}\n\t\t\tthis.debounceTimer = setTimeout(() => {\n\t\t\t\tthis.fetchData()\n\t\t\t}, DEBOUNCE_MS)\n\t\t},\n\n\t\t/**\n\t\t * Fetch finance (always) + vendor (top-vendors view only) data in\n\t\t * parallel through the source. A missing source or a failure degrades\n\t\t * to the relevant empty state without throwing.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync fetchData() {\n\t\t\tif (!this.source) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.loading = true\n\t\t\tconst filters = (this.content && this.content.filters) || {}\n\t\t\ttry {\n\t\t\t\tconst tasks = [\n\t\t\t\t\tthis.source.fetchSummary({\n\t\t\t\t\t\tperiod: this.period,\n\t\t\t\t\t\tcategoryIds: Array.isArray(filters.categoryIds) ? filters.categoryIds : [],\n\t\t\t\t\t\tdepartmentIds: Array.isArray(filters.departmentIds) ? filters.departmentIds : [],\n\t\t\t\t\t}),\n\t\t\t\t]\n\t\t\t\tif (this.viewMode === 'top-vendors' && typeof this.source.fetchVendorCommitments === 'function') {\n\t\t\t\t\ttasks.push(this.source.fetchVendorCommitments({\n\t\t\t\t\t\tperiod: this.period,\n\t\t\t\t\t\tvendorIds: Array.isArray(filters.vendorIds) ? filters.vendorIds : [],\n\t\t\t\t\t}))\n\t\t\t\t}\n\t\t\t\tconst [finance, vendor] = await Promise.all(tasks)\n\t\t\t\tif (finance) {\n\t\t\t\t\tthis.finance = finance\n\t\t\t\t}\n\t\t\t\tif (vendor) {\n\t\t\t\t\tthis.vendor = vendor\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\tthis.finance = { available: false, empty: false, total: 0, currency: 'EUR', byCategory: [], trend: [] }\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Request an AI narrative through the source. When the source lacks\n\t\t * `fetchNarrative` or reports it unavailable, the panel disables\n\t\t * gracefully.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync requestNarrative() {\n\t\t\tif (!this.source || typeof this.source.fetchNarrative !== 'function') {\n\t\t\t\tthis.aiUnavailable = true\n\t\t\t\treturn\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tconst result = await this.source.fetchNarrative({\n\t\t\t\t\tsummary: { total: this.finance.total, currency: this.finance.currency, period: this.period },\n\t\t\t\t}) || {}\n\t\t\t\tif (result.available) {\n\t\t\t\t\tthis.aiNarrative = result.narrative\n\t\t\t\t\tthis.aiUnavailable = false\n\t\t\t\t} else {\n\t\t\t\t\tthis.aiUnavailable = true\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\tthis.aiUnavailable = true\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * The owning sibling app's URL for a row. The source may provide\n\t\t * `resolveDeepLink`; absent that, the row renders as plain text.\n\t\t *\n\t\t * @param {string} app the owning app id.\n\t\t * @param {string} kind the entity kind.\n\t\t * @param {string} id the entity id.\n\t\t * @return {string} the URL, or '' when unresolved.\n\t\t */\n\t\tdeepLink(app, kind, id) {\n\t\t\tif (this.source && typeof this.source.resolveDeepLink === 'function') {\n\t\t\t\tconst href = this.source.resolveDeepLink(app, kind, id)\n\t\t\t\tif (typeof href === 'string' && href !== '') {\n\t\t\t\t\treturn href\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn ''\n\t\t},\n\n\t\t/**\n\t\t * Format a monetary amount respecting the browser locale.\n\t\t *\n\t\t * @param {number} amount the amount.\n\t\t * @return {string} the locale-formatted currency string.\n\t\t */\n\t\tformatAmount(amount) {\n\t\t\tconst value = Number(amount) || 0\n\t\t\tconst locale = (typeof navigator !== 'undefined' && navigator.language) || 'en'\n\t\t\ttry {\n\t\t\t\treturn new Intl.NumberFormat(locale, {\n\t\t\t\t\tstyle: 'currency',\n\t\t\t\t\tcurrency: safeCurrencyCode(this.finance.currency),\n\t\t\t\t}).format(value)\n\t\t\t} catch {\n\t\t\t\treturn String(value)\n\t\t\t}\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-spend-analytics-widget {\n\tdisplay: flex;\n\tflex-direction: column;\n\theight: 100%;\n\tgap: 8px;\n\tpadding: 8px;\n\toverflow: hidden;\n}\n\n.cn-spend-analytics-widget__header {\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: space-between;\n}\n\n.cn-spend-analytics-widget__title {\n\tfont-weight: bold;\n}\n\n.cn-spend-analytics-widget__period {\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius);\n\tbackground: var(--color-main-background);\n\tcolor: var(--color-main-text);\n\tpadding: 2px 6px;\n}\n\n.cn-spend-analytics-widget__body {\n\tflex: 1;\n\toverflow: auto;\n}\n\n.cn-spend-analytics-widget__cards {\n\tdisplay: flex;\n\tgap: 12px;\n}\n\n.cn-spend-analytics-widget__card {\n\tdisplay: flex;\n\tflex-direction: column;\n\tpadding: 8px 12px;\n\tborder-radius: var(--border-radius);\n\tbackground: var(--color-background-hover);\n\tflex: 1;\n}\n\n.cn-spend-analytics-widget__card-label {\n\tfont-size: 0.85em;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-spend-analytics-widget__card-value {\n\tfont-size: 1.4em;\n\tfont-weight: bold;\n}\n\n.cn-spend-analytics-widget__panel-title {\n\tfont-size: 1em;\n\tmargin: 8px 0 4px;\n}\n\n.cn-spend-analytics-widget__rows {\n\tlist-style: none;\n\tmargin: 0;\n\tpadding: 0;\n}\n\n.cn-spend-analytics-widget__row {\n\tdisplay: flex;\n\tjustify-content: space-between;\n\tpadding: 4px 8px;\n\tborder-radius: var(--border-radius);\n\tcursor: pointer;\n\tcolor: inherit;\n\ttext-decoration: none;\n}\n\n.cn-spend-analytics-widget__row--static {\n\tcursor: default;\n}\n\n.cn-spend-analytics-widget__row:hover,\n.cn-spend-analytics-widget__row:focus {\n\tbackground: var(--color-background-hover);\n\toutline: none;\n}\n\n.cn-spend-analytics-widget__state {\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: center;\n\tmin-height: 64px;\n\tcolor: var(--color-text-maxcontrast);\n\tpadding: 12px;\n\ttext-align: center;\n}\n\n.cn-spend-analytics-widget__inline-empty {\n\tcolor: var(--color-text-maxcontrast);\n\tpadding: 6px;\n\ttext-align: left;\n}\n\n.cn-spend-analytics-widget__ai-text {\n\twhite-space: pre-wrap;\n}\n\n.cn-spend-analytics-widget__ai-btn {\n\tborder: 1px solid var(--color-border);\n\tbackground: var(--color-background-hover);\n\tborder-radius: var(--border-radius);\n\tcolor: var(--color-main-text);\n\tpadding: 4px 12px;\n\tcursor: pointer;\n}\n</style>\n"],"names":["t"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CnSpendAnalyticsWidget.vue3.js","sources":["../../../../src/components/CnSpendAnalyticsWidget/CnSpendAnalyticsWidget.vue?vue&type=template&id=9d12d4a2&lang.js"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n\n<template>\n\t<div class=\"cn-spend-analytics-widget\">\n\t\t<header class=\"cn-spend-analytics-widget__header\">\n\t\t\t<span class=\"cn-spend-analytics-widget__title\">{{ t('nextcloud-vue', 'Spend analytics') }}</span>\n\t\t\t<select\n\t\t\t\tclass=\"cn-spend-analytics-widget__period\"\n\t\t\t\t:value=\"period\"\n\t\t\t\t:aria-label=\"t('nextcloud-vue', 'Reporting period')\"\n\t\t\t\t@change=\"onPeriodChange($event.target.value)\">\n\t\t\t\t<option v-for=\"opt in periodOptions\" :key=\"opt.value\" :value=\"opt.value\">\n\t\t\t\t\t{{ opt.label }}\n\t\t\t\t</option>\n\t\t\t</select>\n\t\t</header>\n\n\t\t<div class=\"cn-spend-analytics-widget__body\">\n\t\t\t<div v-if=\"loading\" class=\"cn-spend-analytics-widget__state\">\n\t\t\t\t{{ t('nextcloud-vue', 'Loading spend data…') }}\n\t\t\t</div>\n\n\t\t\t<!-- No data source wired at all. -->\n\t\t\t<div v-else-if=\"!hasSource\" class=\"cn-spend-analytics-widget__state\">\n\t\t\t\t{{ t('nextcloud-vue', 'No spend data source available') }}\n\t\t\t</div>\n\n\t\t\t<!-- financeq absent → empty-state naming the source. -->\n\t\t\t<div v-else-if=\"financeMissing\" class=\"cn-spend-analytics-widget__state\">\n\t\t\t\t{{ t('nextcloud-vue', 'No spend data — financeq is not installed') }}\n\t\t\t</div>\n\n\t\t\t<!-- financeq present but no data in window. -->\n\t\t\t<div v-else-if=\"financeEmpty\" class=\"cn-spend-analytics-widget__state\">\n\t\t\t\t{{ t('nextcloud-vue', 'No spend recorded for the selected period') }}\n\t\t\t</div>\n\n\t\t\t<template v-else>\n\t\t\t\t<section class=\"cn-spend-analytics-widget__cards\">\n\t\t\t\t\t<div class=\"cn-spend-analytics-widget__card\">\n\t\t\t\t\t\t<span class=\"cn-spend-analytics-widget__card-label\">{{ t('nextcloud-vue', 'Total spend') }}</span>\n\t\t\t\t\t\t<span class=\"cn-spend-analytics-widget__card-value\" :aria-label=\"totalAriaLabel\">\n\t\t\t\t\t\t\t{{ formattedTotal }}\n\t\t\t\t\t\t</span>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"cn-spend-analytics-widget__card\">\n\t\t\t\t\t\t<span class=\"cn-spend-analytics-widget__card-label\">{{ t('nextcloud-vue', 'Categories') }}</span>\n\t\t\t\t\t\t<span class=\"cn-spend-analytics-widget__card-value\">{{ finance.byCategory.length }}</span>\n\t\t\t\t\t</div>\n\t\t\t\t</section>\n\n\t\t\t\t<section v-if=\"viewMode === 'trend'\" class=\"cn-spend-analytics-widget__panel\">\n\t\t\t\t\t<h3 class=\"cn-spend-analytics-widget__panel-title\">\n\t\t\t\t\t\t{{ t('nextcloud-vue', 'Spend trend') }}\n\t\t\t\t\t</h3>\n\t\t\t\t\t<ul class=\"cn-spend-analytics-widget__rows\">\n\t\t\t\t\t\t<li v-for=\"row in finance.trend\" :key=\"row.month\" class=\"cn-spend-analytics-widget__row cn-spend-analytics-widget__row--static\">\n\t\t\t\t\t\t\t<span>{{ row.month }}</span>\n\t\t\t\t\t\t\t<span>{{ formatAmount(row.amount) }}</span>\n\t\t\t\t\t\t</li>\n\t\t\t\t\t</ul>\n\t\t\t\t</section>\n\n\t\t\t\t<section v-else-if=\"viewMode === 'top-categories'\" class=\"cn-spend-analytics-widget__panel\">\n\t\t\t\t\t<h3 class=\"cn-spend-analytics-widget__panel-title\">\n\t\t\t\t\t\t{{ t('nextcloud-vue', 'Top categories') }}\n\t\t\t\t\t</h3>\n\t\t\t\t\t<ul class=\"cn-spend-analytics-widget__rows\">\n\t\t\t\t\t\t<li v-for=\"(row, i) in finance.byCategory\" :key=\"row.category\">\n\t\t\t\t\t\t\t<a v-if=\"categoryLinks[i]\" class=\"cn-spend-analytics-widget__row\" :href=\"categoryLinks[i]\">\n\t\t\t\t\t\t\t\t<span>{{ row.category }}</span>\n\t\t\t\t\t\t\t\t<span>{{ formatAmount(row.amount) }}</span>\n\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t<div v-else class=\"cn-spend-analytics-widget__row cn-spend-analytics-widget__row--static\">\n\t\t\t\t\t\t\t\t<span>{{ row.category }}</span>\n\t\t\t\t\t\t\t\t<span>{{ formatAmount(row.amount) }}</span>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</li>\n\t\t\t\t\t</ul>\n\t\t\t\t</section>\n\n\t\t\t\t<section v-else-if=\"viewMode === 'top-vendors'\" class=\"cn-spend-analytics-widget__panel\">\n\t\t\t\t\t<h3 class=\"cn-spend-analytics-widget__panel-title\">\n\t\t\t\t\t\t{{ t('nextcloud-vue', 'Top vendors') }}\n\t\t\t\t\t</h3>\n\t\t\t\t\t<div v-if=\"vendorMissing\" class=\"cn-spend-analytics-widget__inline-empty\">\n\t\t\t\t\t\t{{ t('nextcloud-vue', 'No vendor data — procest is not installed') }}\n\t\t\t\t\t</div>\n\t\t\t\t\t<div v-else-if=\"vendorEmpty\" class=\"cn-spend-analytics-widget__inline-empty\">\n\t\t\t\t\t\t{{ t('nextcloud-vue', 'No vendor commitments for the selected period') }}\n\t\t\t\t\t</div>\n\t\t\t\t\t<ul v-else class=\"cn-spend-analytics-widget__rows\">\n\t\t\t\t\t\t<li v-for=\"(row, i) in vendor.topVendors\" :key=\"row.vendor\">\n\t\t\t\t\t\t\t<a v-if=\"vendorLinks[i]\" class=\"cn-spend-analytics-widget__row\" :href=\"vendorLinks[i]\">\n\t\t\t\t\t\t\t\t<span>{{ row.vendor }}</span>\n\t\t\t\t\t\t\t\t<span>{{ formatAmount(row.committed) }}</span>\n\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t<div v-else class=\"cn-spend-analytics-widget__row cn-spend-analytics-widget__row--static\">\n\t\t\t\t\t\t\t\t<span>{{ row.vendor }}</span>\n\t\t\t\t\t\t\t\t<span>{{ formatAmount(row.committed) }}</span>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</li>\n\t\t\t\t\t</ul>\n\t\t\t\t</section>\n\n\t\t\t\t<section v-if=\"aiEnabled\" class=\"cn-spend-analytics-widget__panel\">\n\t\t\t\t\t<h3 class=\"cn-spend-analytics-widget__panel-title\">\n\t\t\t\t\t\t{{ t('nextcloud-vue', 'AI insight') }}\n\t\t\t\t\t</h3>\n\t\t\t\t\t<div v-if=\"aiUnavailable\" class=\"cn-spend-analytics-widget__inline-empty\">\n\t\t\t\t\t\t{{ t('nextcloud-vue', 'AI insight unavailable — the source is not configured') }}\n\t\t\t\t\t</div>\n\t\t\t\t\t<p v-else-if=\"aiNarrative\" class=\"cn-spend-analytics-widget__ai-text\">\n\t\t\t\t\t\t{{ aiNarrative }}\n\t\t\t\t\t</p>\n\t\t\t\t\t<button\n\t\t\t\t\t\tv-else\n\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\tclass=\"cn-spend-analytics-widget__ai-btn\"\n\t\t\t\t\t\t@click=\"requestNarrative\">\n\t\t\t\t\t\t{{ t('nextcloud-vue', 'Generate insight') }}\n\t\t\t\t\t</button>\n\t\t\t\t</section>\n\t\t\t</template>\n\t\t</div>\n\t</div>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { safeCurrencyCode } from '../../utils/formatMetric.js'\n\nconst VIEW_MODES = ['summary', 'trend', 'top-vendors', 'top-categories']\nconst PERIODS = ['month', 'quarter', 'ytd', 'fy']\nconst DEBOUNCE_MS = 200\n\n/**\n * CnSpendAnalyticsWidget — dashboard renderer for a `spend-analytics`\n * placement. Renders summary / trend / top-vendors / top-categories views over\n * spend data with graceful per-source empty states and row drill-through.\n *\n * Data origin (decoupling seam): all data flows through a consumer-supplied\n * `dataSource` prop OR the `cnSpendAnalyticsSource` injection — an object\n * exposing `fetchSummary(args)`, `fetchVendorCommitments(args)`,\n * `fetchNarrative(args)`, and optionally `resolveDeepLink(app, kind, id)`. The\n * renderer imports NO financeq / procest / launchpad service module and never\n * calls axios itself; the consuming app (typically over GraphQL) owns the\n * transport. The `requires.graphql` hint lives only in the registry entry, not\n * in any manifest dependency.\n *\n * Graceful degradation: with no source the widget shows \"No spend data source\n * available\"; the source signals per-sibling availability via\n * `{ available, empty }` envelopes so a partial fleet still renders\n * (\"financeq is not installed\" / \"procest is not installed\").\n *\n * @spec openspec/changes/cn-widget-library/specs/cn-widget-library/spec.md\n */\nexport default {\n\tname: 'CnSpendAnalyticsWidget',\n\n\tinject: {\n\t\tcnSpendAnalyticsSource: {\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * Persisted widget content `{period, viewMode, filters, aiInsights}`.\n\t\t *\n\t\t * @type {object}\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 * Consumer-supplied data source overriding the\n\t\t * `cnSpendAnalyticsSource` injection. Must expose `fetchSummary`,\n\t\t * `fetchVendorCommitments`, and `fetchNarrative`; may expose\n\t\t * `resolveDeepLink`. When `null` the injection (then the empty state)\n\t\t * is used.\n\t\t *\n\t\t * @type {object|null}\n\t\t */\n\t\tdataSource: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\tdata() {\n\t\treturn {\n\t\t\tperiod: PERIODS.includes(this.content && this.content.period) ? this.content.period : 'quarter',\n\t\t\tloading: false,\n\t\t\tfinance: { available: false, empty: false, total: 0, currency: 'EUR', byCategory: [], trend: [] },\n\t\t\tvendor: { available: false, empty: false, topVendors: [] },\n\t\t\taiNarrative: '',\n\t\t\taiUnavailable: false,\n\t\t\tdebounceTimer: null,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * The active data source (prop wins over injection).\n\t\t *\n\t\t * @return {object|null} the resolved source, or `null`.\n\t\t */\n\t\tsource() {\n\t\t\tconst candidate = this.dataSource || this.cnSpendAnalyticsSource\n\t\t\treturn candidate && typeof candidate.fetchSummary === 'function' ? candidate : null\n\t\t},\n\n\t\t/**\n\t\t * Whether a usable spend source is configured.\n\t\t *\n\t\t * @return {boolean} true when a source exists.\n\t\t */\n\t\thasSource() {\n\t\t\treturn this.source !== null\n\t\t},\n\n\t\t/**\n\t\t * Deep link of each top-category row, by index ('' when unresolved).\n\t\t *\n\t\t * @return {string[]}\n\t\t */\n\t\tcategoryLinks() {\n\t\t\treturn (this.finance.byCategory || []).map((row) => this.deepLink('financeq', 'category', row.category))\n\t\t},\n\n\t\t/**\n\t\t * Deep link of each top-vendor row, by index ('' when unresolved).\n\t\t *\n\t\t * @return {string[]}\n\t\t */\n\t\tvendorLinks() {\n\t\t\treturn (this.vendor.topVendors || []).map((row) => this.deepLink('procest', 'vendor', row.vendor))\n\t\t},\n\n\t\t/**\n\t\t * The validated view mode.\n\t\t *\n\t\t * @return {string} the view mode.\n\t\t */\n\t\tviewMode() {\n\t\t\treturn VIEW_MODES.includes(this.content && this.content.viewMode) ? this.content.viewMode : 'summary'\n\t\t},\n\n\t\t/**\n\t\t * Whether the AI narrative panel is enabled.\n\t\t *\n\t\t * @return {boolean} the AI-enabled flag.\n\t\t */\n\t\taiEnabled() {\n\t\t\treturn !!(this.content && this.content.aiInsights && this.content.aiInsights.enabled === true)\n\t\t},\n\n\t\t/**\n\t\t * Whether financeq is absent.\n\t\t *\n\t\t * @return {boolean} true when finance is unavailable.\n\t\t */\n\t\tfinanceMissing() {\n\t\t\treturn this.finance.available === false\n\t\t},\n\n\t\t/**\n\t\t * Whether financeq is present but has no data in the window.\n\t\t *\n\t\t * @return {boolean} true when finance is available but empty.\n\t\t */\n\t\tfinanceEmpty() {\n\t\t\treturn this.finance.available === true && this.finance.empty === true\n\t\t},\n\n\t\t/**\n\t\t * Whether procest is absent.\n\t\t *\n\t\t * @return {boolean} true when vendor data is unavailable.\n\t\t */\n\t\tvendorMissing() {\n\t\t\treturn this.vendor.available === false\n\t\t},\n\n\t\t/**\n\t\t * Whether procest is present but has no vendor data in the window.\n\t\t *\n\t\t * @return {boolean} true when vendor is available but empty.\n\t\t */\n\t\tvendorEmpty() {\n\t\t\treturn this.vendor.available === true && this.vendor.empty === true\n\t\t},\n\n\t\t/**\n\t\t * The reporting-period dropdown options.\n\t\t *\n\t\t * @return {Array<{value: string, label: string}>} the options.\n\t\t */\n\t\tperiodOptions() {\n\t\t\treturn [\n\t\t\t\t{ value: 'month', label: t('nextcloud-vue', 'This month') },\n\t\t\t\t{ value: 'quarter', label: t('nextcloud-vue', 'This quarter') },\n\t\t\t\t{ value: 'ytd', label: t('nextcloud-vue', 'Year to date') },\n\t\t\t\t{ value: 'fy', label: t('nextcloud-vue', 'Fiscal year') },\n\t\t\t]\n\t\t},\n\n\t\t/**\n\t\t * The formatted total-spend amount.\n\t\t *\n\t\t * @return {string} the formatted total.\n\t\t */\n\t\tformattedTotal() {\n\t\t\treturn this.formatAmount(this.finance.total)\n\t\t},\n\n\t\t/**\n\t\t * The accessible label for the total-spend value.\n\t\t *\n\t\t * @return {string} the aria label.\n\t\t */\n\t\ttotalAriaLabel() {\n\t\t\treturn t('nextcloud-vue', 'Total spend: {amount}').replace('{amount}', this.formattedTotal)\n\t\t},\n\t},\n\n\twatch: {\n\t\tviewMode() {\n\t\t\tthis.scheduleFetch()\n\t\t},\n\t},\n\n\tmounted() {\n\t\tthis.fetchData()\n\t},\n\n\tbeforeUnmount() {\n\t\tif (this.debounceTimer) {\n\t\t\tclearTimeout(this.debounceTimer)\n\t\t}\n\t},\n\n\tmethods: {\n\t\tt,\n\n\t\t/**\n\t\t * Handle a reporting-period change.\n\t\t *\n\t\t * @param {string} value the new period.\n\t\t * @return {void}\n\t\t */\n\t\tonPeriodChange(value) {\n\t\t\tif (PERIODS.includes(value)) {\n\t\t\t\tthis.period = value\n\t\t\t\tthis.scheduleFetch()\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Debounce period / view changes before re-querying.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tscheduleFetch() {\n\t\t\tif (this.debounceTimer) {\n\t\t\t\tclearTimeout(this.debounceTimer)\n\t\t\t}\n\t\t\tthis.debounceTimer = setTimeout(() => {\n\t\t\t\tthis.fetchData()\n\t\t\t}, DEBOUNCE_MS)\n\t\t},\n\n\t\t/**\n\t\t * Fetch finance (always) + vendor (top-vendors view only) data in\n\t\t * parallel through the source. A missing source or a failure degrades\n\t\t * to the relevant empty state without throwing.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync fetchData() {\n\t\t\tif (!this.source) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.loading = true\n\t\t\tconst filters = (this.content && this.content.filters) || {}\n\t\t\ttry {\n\t\t\t\tconst tasks = [\n\t\t\t\t\tthis.source.fetchSummary({\n\t\t\t\t\t\tperiod: this.period,\n\t\t\t\t\t\tcategoryIds: Array.isArray(filters.categoryIds) ? filters.categoryIds : [],\n\t\t\t\t\t\tdepartmentIds: Array.isArray(filters.departmentIds) ? filters.departmentIds : [],\n\t\t\t\t\t}),\n\t\t\t\t]\n\t\t\t\tif (this.viewMode === 'top-vendors' && typeof this.source.fetchVendorCommitments === 'function') {\n\t\t\t\t\ttasks.push(this.source.fetchVendorCommitments({\n\t\t\t\t\t\tperiod: this.period,\n\t\t\t\t\t\tvendorIds: Array.isArray(filters.vendorIds) ? filters.vendorIds : [],\n\t\t\t\t\t}))\n\t\t\t\t}\n\t\t\t\tconst [finance, vendor] = await Promise.all(tasks)\n\t\t\t\tif (finance) {\n\t\t\t\t\tthis.finance = finance\n\t\t\t\t}\n\t\t\t\tif (vendor) {\n\t\t\t\t\tthis.vendor = vendor\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\tthis.finance = { available: false, empty: false, total: 0, currency: 'EUR', byCategory: [], trend: [] }\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Request an AI narrative through the source. When the source lacks\n\t\t * `fetchNarrative` or reports it unavailable, the panel disables\n\t\t * gracefully.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync requestNarrative() {\n\t\t\tif (!this.source || typeof this.source.fetchNarrative !== 'function') {\n\t\t\t\tthis.aiUnavailable = true\n\t\t\t\treturn\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tconst result = await this.source.fetchNarrative({\n\t\t\t\t\tsummary: { total: this.finance.total, currency: this.finance.currency, period: this.period },\n\t\t\t\t}) || {}\n\t\t\t\tif (result.available) {\n\t\t\t\t\tthis.aiNarrative = result.narrative\n\t\t\t\t\tthis.aiUnavailable = false\n\t\t\t\t} else {\n\t\t\t\t\tthis.aiUnavailable = true\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\tthis.aiUnavailable = true\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * The owning sibling app's URL for a row. The source may provide\n\t\t * `resolveDeepLink`; absent that, the row renders as plain text.\n\t\t *\n\t\t * @param {string} app the owning app id.\n\t\t * @param {string} kind the entity kind.\n\t\t * @param {string} id the entity id.\n\t\t * @return {string} the URL, or '' when unresolved.\n\t\t */\n\t\tdeepLink(app, kind, id) {\n\t\t\tif (this.source && typeof this.source.resolveDeepLink === 'function') {\n\t\t\t\tconst href = this.source.resolveDeepLink(app, kind, id)\n\t\t\t\tif (typeof href === 'string' && href !== '') {\n\t\t\t\t\treturn href\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn ''\n\t\t},\n\n\t\t/**\n\t\t * Format a monetary amount respecting the browser locale.\n\t\t *\n\t\t * @param {number} amount the amount.\n\t\t * @return {string} the locale-formatted currency string.\n\t\t */\n\t\tformatAmount(amount) {\n\t\t\tconst value = Number(amount) || 0\n\t\t\tconst locale = (typeof navigator !== 'undefined' && navigator.language) || 'en'\n\t\t\ttry {\n\t\t\t\treturn new Intl.NumberFormat(locale, {\n\t\t\t\t\tstyle: 'currency',\n\t\t\t\t\tcurrency: safeCurrencyCode(this.finance.currency),\n\t\t\t\t}).format(value)\n\t\t\t} catch {\n\t\t\t\treturn String(value)\n\t\t\t}\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-spend-analytics-widget {\n\tdisplay: flex;\n\tflex-direction: column;\n\theight: 100%;\n\tgap: 8px;\n\tpadding: 8px;\n\toverflow: hidden;\n}\n\n.cn-spend-analytics-widget__header {\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: space-between;\n}\n\n.cn-spend-analytics-widget__title {\n\tfont-weight: bold;\n}\n\n.cn-spend-analytics-widget__period {\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius);\n\tbackground: var(--color-main-background);\n\tcolor: var(--color-main-text);\n\tpadding: 2px 6px;\n}\n\n.cn-spend-analytics-widget__body {\n\tflex: 1;\n\toverflow: auto;\n}\n\n.cn-spend-analytics-widget__cards {\n\tdisplay: flex;\n\tgap: 12px;\n}\n\n.cn-spend-analytics-widget__card {\n\tdisplay: flex;\n\tflex-direction: column;\n\tpadding: 8px 12px;\n\tborder-radius: var(--border-radius);\n\tbackground: var(--color-background-hover);\n\tflex: 1;\n}\n\n.cn-spend-analytics-widget__card-label {\n\tfont-size: 0.85em;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-spend-analytics-widget__card-value {\n\tfont-size: 1.4em;\n\tfont-weight: bold;\n}\n\n.cn-spend-analytics-widget__panel-title {\n\tfont-size: 1em;\n\tmargin: 8px 0 4px;\n}\n\n.cn-spend-analytics-widget__rows {\n\tlist-style: none;\n\tmargin: 0;\n\tpadding: 0;\n}\n\n.cn-spend-analytics-widget__row {\n\tdisplay: flex;\n\tjustify-content: space-between;\n\tpadding: 4px 8px;\n\tborder-radius: var(--border-radius);\n\tcursor: pointer;\n\tcolor: inherit;\n\ttext-decoration: none;\n}\n\n.cn-spend-analytics-widget__row--static {\n\tcursor: default;\n}\n\n.cn-spend-analytics-widget__row:hover,\n.cn-spend-analytics-widget__row:focus {\n\tbackground: var(--color-background-hover);\n\toutline: none;\n}\n\n.cn-spend-analytics-widget__state {\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: center;\n\tmin-height: 64px;\n\tcolor: var(--color-text-maxcontrast);\n\tpadding: 12px;\n\ttext-align: center;\n}\n\n.cn-spend-analytics-widget__inline-empty {\n\tcolor: var(--color-text-maxcontrast);\n\tpadding: 6px;\n\ttext-align: left;\n}\n\n.cn-spend-analytics-widget__ai-text {\n\twhite-space: pre-wrap;\n}\n\n.cn-spend-analytics-widget__ai-btn {\n\tborder: 1px solid var(--color-border);\n\tbackground: var(--color-background-hover);\n\tborder-radius: var(--border-radius);\n\tcolor: var(--color-main-text);\n\tpadding: 4px 12px;\n\tcursor: pointer;\n}\n</style>\n"],"names":["_openBlock","_createElementBlock","_createElementVNode","_toDisplayString","_Fragment","_renderList"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"CnSpendAnalyticsWidget.vue3.js","sources":["../../../../src/components/CnSpendAnalyticsWidget/CnSpendAnalyticsWidget.vue?vue&type=template&id=9d12d4a2&lang.js"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n\n<template>\n\t<div class=\"cn-spend-analytics-widget\">\n\t\t<header class=\"cn-spend-analytics-widget__header\">\n\t\t\t<span class=\"cn-spend-analytics-widget__title\">{{ t('nextcloud-vue', 'Spend analytics') }}</span>\n\t\t\t<select\n\t\t\t\tclass=\"cn-spend-analytics-widget__period\"\n\t\t\t\t:value=\"period\"\n\t\t\t\t:aria-label=\"t('nextcloud-vue', 'Reporting period')\"\n\t\t\t\t@change=\"onPeriodChange($event.target.value)\">\n\t\t\t\t<option v-for=\"opt in periodOptions\" :key=\"opt.value\" :value=\"opt.value\">\n\t\t\t\t\t{{ opt.label }}\n\t\t\t\t</option>\n\t\t\t</select>\n\t\t</header>\n\n\t\t<div class=\"cn-spend-analytics-widget__body\">\n\t\t\t<div v-if=\"loading\" class=\"cn-spend-analytics-widget__state\">\n\t\t\t\t{{ t('nextcloud-vue', 'Loading spend data…') }}\n\t\t\t</div>\n\n\t\t\t<!-- No data source wired at all. -->\n\t\t\t<div v-else-if=\"!hasSource\" class=\"cn-spend-analytics-widget__state\">\n\t\t\t\t{{ t('nextcloud-vue', 'No spend data source available') }}\n\t\t\t</div>\n\n\t\t\t<!-- financeq absent → empty-state naming the source. -->\n\t\t\t<div v-else-if=\"financeMissing\" class=\"cn-spend-analytics-widget__state\">\n\t\t\t\t{{ t('nextcloud-vue', 'No spend data — financeq is not installed') }}\n\t\t\t</div>\n\n\t\t\t<!-- financeq present but no data in window. -->\n\t\t\t<div v-else-if=\"financeEmpty\" class=\"cn-spend-analytics-widget__state\">\n\t\t\t\t{{ t('nextcloud-vue', 'No spend recorded for the selected period') }}\n\t\t\t</div>\n\n\t\t\t<template v-else>\n\t\t\t\t<section class=\"cn-spend-analytics-widget__cards\">\n\t\t\t\t\t<div class=\"cn-spend-analytics-widget__card\">\n\t\t\t\t\t\t<span class=\"cn-spend-analytics-widget__card-label\">{{ t('nextcloud-vue', 'Total spend') }}</span>\n\t\t\t\t\t\t<span class=\"cn-spend-analytics-widget__card-value\" :aria-label=\"totalAriaLabel\">\n\t\t\t\t\t\t\t{{ formattedTotal }}\n\t\t\t\t\t\t</span>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"cn-spend-analytics-widget__card\">\n\t\t\t\t\t\t<span class=\"cn-spend-analytics-widget__card-label\">{{ t('nextcloud-vue', 'Categories') }}</span>\n\t\t\t\t\t\t<span class=\"cn-spend-analytics-widget__card-value\">{{ finance.byCategory.length }}</span>\n\t\t\t\t\t</div>\n\t\t\t\t</section>\n\n\t\t\t\t<section v-if=\"viewMode === 'trend'\" class=\"cn-spend-analytics-widget__panel\">\n\t\t\t\t\t<h3 class=\"cn-spend-analytics-widget__panel-title\">\n\t\t\t\t\t\t{{ t('nextcloud-vue', 'Spend trend') }}\n\t\t\t\t\t</h3>\n\t\t\t\t\t<ul class=\"cn-spend-analytics-widget__rows\">\n\t\t\t\t\t\t<li v-for=\"row in finance.trend\" :key=\"row.month\" class=\"cn-spend-analytics-widget__row cn-spend-analytics-widget__row--static\">\n\t\t\t\t\t\t\t<span>{{ row.month }}</span>\n\t\t\t\t\t\t\t<span>{{ formatAmount(row.amount) }}</span>\n\t\t\t\t\t\t</li>\n\t\t\t\t\t</ul>\n\t\t\t\t</section>\n\n\t\t\t\t<section v-else-if=\"viewMode === 'top-categories'\" class=\"cn-spend-analytics-widget__panel\">\n\t\t\t\t\t<h3 class=\"cn-spend-analytics-widget__panel-title\">\n\t\t\t\t\t\t{{ t('nextcloud-vue', 'Top categories') }}\n\t\t\t\t\t</h3>\n\t\t\t\t\t<ul class=\"cn-spend-analytics-widget__rows\">\n\t\t\t\t\t\t<li v-for=\"(row, i) in finance.byCategory\" :key=\"row.category\">\n\t\t\t\t\t\t\t<a v-if=\"categoryLinks[i]\" class=\"cn-spend-analytics-widget__row\" :href=\"categoryLinks[i]\">\n\t\t\t\t\t\t\t\t<span>{{ row.category }}</span>\n\t\t\t\t\t\t\t\t<span>{{ formatAmount(row.amount) }}</span>\n\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t<div v-else class=\"cn-spend-analytics-widget__row cn-spend-analytics-widget__row--static\">\n\t\t\t\t\t\t\t\t<span>{{ row.category }}</span>\n\t\t\t\t\t\t\t\t<span>{{ formatAmount(row.amount) }}</span>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</li>\n\t\t\t\t\t</ul>\n\t\t\t\t</section>\n\n\t\t\t\t<section v-else-if=\"viewMode === 'top-vendors'\" class=\"cn-spend-analytics-widget__panel\">\n\t\t\t\t\t<h3 class=\"cn-spend-analytics-widget__panel-title\">\n\t\t\t\t\t\t{{ t('nextcloud-vue', 'Top vendors') }}\n\t\t\t\t\t</h3>\n\t\t\t\t\t<div v-if=\"vendorMissing\" class=\"cn-spend-analytics-widget__inline-empty\">\n\t\t\t\t\t\t{{ t('nextcloud-vue', 'No vendor data — procest is not installed') }}\n\t\t\t\t\t</div>\n\t\t\t\t\t<div v-else-if=\"vendorEmpty\" class=\"cn-spend-analytics-widget__inline-empty\">\n\t\t\t\t\t\t{{ t('nextcloud-vue', 'No vendor commitments for the selected period') }}\n\t\t\t\t\t</div>\n\t\t\t\t\t<ul v-else class=\"cn-spend-analytics-widget__rows\">\n\t\t\t\t\t\t<li v-for=\"(row, i) in vendor.topVendors\" :key=\"row.vendor\">\n\t\t\t\t\t\t\t<a v-if=\"vendorLinks[i]\" class=\"cn-spend-analytics-widget__row\" :href=\"vendorLinks[i]\">\n\t\t\t\t\t\t\t\t<span>{{ row.vendor }}</span>\n\t\t\t\t\t\t\t\t<span>{{ formatAmount(row.committed) }}</span>\n\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t<div v-else class=\"cn-spend-analytics-widget__row cn-spend-analytics-widget__row--static\">\n\t\t\t\t\t\t\t\t<span>{{ row.vendor }}</span>\n\t\t\t\t\t\t\t\t<span>{{ formatAmount(row.committed) }}</span>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</li>\n\t\t\t\t\t</ul>\n\t\t\t\t</section>\n\n\t\t\t\t<section v-if=\"aiEnabled\" class=\"cn-spend-analytics-widget__panel\">\n\t\t\t\t\t<h3 class=\"cn-spend-analytics-widget__panel-title\">\n\t\t\t\t\t\t{{ t('nextcloud-vue', 'AI insight') }}\n\t\t\t\t\t</h3>\n\t\t\t\t\t<div v-if=\"aiUnavailable\" class=\"cn-spend-analytics-widget__inline-empty\">\n\t\t\t\t\t\t{{ t('nextcloud-vue', 'AI insight unavailable — the source is not configured') }}\n\t\t\t\t\t</div>\n\t\t\t\t\t<p v-else-if=\"aiNarrative\" class=\"cn-spend-analytics-widget__ai-text\">\n\t\t\t\t\t\t{{ aiNarrative }}\n\t\t\t\t\t</p>\n\t\t\t\t\t<button\n\t\t\t\t\t\tv-else\n\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\tclass=\"cn-spend-analytics-widget__ai-btn\"\n\t\t\t\t\t\t@click=\"requestNarrative\">\n\t\t\t\t\t\t{{ t('nextcloud-vue', 'Generate insight') }}\n\t\t\t\t\t</button>\n\t\t\t\t</section>\n\t\t\t</template>\n\t\t</div>\n\t</div>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { safeCurrencyCode } from '../../utils/formatMetric.js'\n\nconst VIEW_MODES = ['summary', 'trend', 'top-vendors', 'top-categories']\nconst PERIODS = ['month', 'quarter', 'ytd', 'fy']\nconst DEBOUNCE_MS = 200\n\n/**\n * CnSpendAnalyticsWidget — dashboard renderer for a `spend-analytics`\n * placement. Renders summary / trend / top-vendors / top-categories views over\n * spend data with graceful per-source empty states and row drill-through.\n *\n * Data origin (decoupling seam): all data flows through a consumer-supplied\n * `dataSource` prop OR the `cnSpendAnalyticsSource` injection — an object\n * exposing `fetchSummary(args)`, `fetchVendorCommitments(args)`,\n * `fetchNarrative(args)`, and optionally `resolveDeepLink(app, kind, id)`\n * (pure and synchronous: it runs for every row on every render). The\n * renderer imports NO financeq / procest / launchpad service module and never\n * calls axios itself; the consuming app (typically over GraphQL) owns the\n * transport. The `requires.graphql` hint lives only in the registry entry, not\n * in any manifest dependency.\n *\n * Graceful degradation: with no source the widget shows \"No spend data source\n * available\"; the source signals per-sibling availability via\n * `{ available, empty }` envelopes so a partial fleet still renders\n * (\"financeq is not installed\" / \"procest is not installed\").\n *\n * @spec openspec/changes/cn-widget-library/specs/cn-widget-library/spec.md\n */\nexport default {\n\tname: 'CnSpendAnalyticsWidget',\n\n\tinject: {\n\t\tcnSpendAnalyticsSource: {\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * Persisted widget content `{period, viewMode, filters, aiInsights}`.\n\t\t *\n\t\t * @type {object}\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 * Consumer-supplied data source overriding the\n\t\t * `cnSpendAnalyticsSource` injection. Must expose `fetchSummary`,\n\t\t * `fetchVendorCommitments`, and `fetchNarrative`; may expose\n\t\t * `resolveDeepLink`, which must be pure and synchronous, since it is\n\t\t * called for every row on every render. When `null` the injection\n\t\t * (then the empty state) is used.\n\t\t *\n\t\t * @type {object|null}\n\t\t */\n\t\tdataSource: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\tdata() {\n\t\treturn {\n\t\t\tperiod: PERIODS.includes(this.content && this.content.period) ? this.content.period : 'quarter',\n\t\t\tloading: false,\n\t\t\tfinance: { available: false, empty: false, total: 0, currency: 'EUR', byCategory: [], trend: [] },\n\t\t\tvendor: { available: false, empty: false, topVendors: [] },\n\t\t\taiNarrative: '',\n\t\t\taiUnavailable: false,\n\t\t\tdebounceTimer: null,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * The active data source (prop wins over injection).\n\t\t *\n\t\t * @return {object|null} the resolved source, or `null`.\n\t\t */\n\t\tsource() {\n\t\t\tconst candidate = this.dataSource || this.cnSpendAnalyticsSource\n\t\t\treturn candidate && typeof candidate.fetchSummary === 'function' ? candidate : null\n\t\t},\n\n\t\t/**\n\t\t * Whether a usable spend source is configured.\n\t\t *\n\t\t * @return {boolean} true when a source exists.\n\t\t */\n\t\thasSource() {\n\t\t\treturn this.source !== null\n\t\t},\n\n\t\t/**\n\t\t * Deep link of each top-category row, by index ('' when unresolved).\n\t\t *\n\t\t * @return {string[]}\n\t\t */\n\t\tcategoryLinks() {\n\t\t\treturn (this.finance.byCategory || []).map((row) => this.deepLink('financeq', 'category', row.category))\n\t\t},\n\n\t\t/**\n\t\t * Deep link of each top-vendor row, by index ('' when unresolved).\n\t\t *\n\t\t * @return {string[]}\n\t\t */\n\t\tvendorLinks() {\n\t\t\treturn (this.vendor.topVendors || []).map((row) => this.deepLink('procest', 'vendor', row.vendor))\n\t\t},\n\n\t\t/**\n\t\t * The validated view mode.\n\t\t *\n\t\t * @return {string} the view mode.\n\t\t */\n\t\tviewMode() {\n\t\t\treturn VIEW_MODES.includes(this.content && this.content.viewMode) ? this.content.viewMode : 'summary'\n\t\t},\n\n\t\t/**\n\t\t * Whether the AI narrative panel is enabled.\n\t\t *\n\t\t * @return {boolean} the AI-enabled flag.\n\t\t */\n\t\taiEnabled() {\n\t\t\treturn !!(this.content && this.content.aiInsights && this.content.aiInsights.enabled === true)\n\t\t},\n\n\t\t/**\n\t\t * Whether financeq is absent.\n\t\t *\n\t\t * @return {boolean} true when finance is unavailable.\n\t\t */\n\t\tfinanceMissing() {\n\t\t\treturn this.finance.available === false\n\t\t},\n\n\t\t/**\n\t\t * Whether financeq is present but has no data in the window.\n\t\t *\n\t\t * @return {boolean} true when finance is available but empty.\n\t\t */\n\t\tfinanceEmpty() {\n\t\t\treturn this.finance.available === true && this.finance.empty === true\n\t\t},\n\n\t\t/**\n\t\t * Whether procest is absent.\n\t\t *\n\t\t * @return {boolean} true when vendor data is unavailable.\n\t\t */\n\t\tvendorMissing() {\n\t\t\treturn this.vendor.available === false\n\t\t},\n\n\t\t/**\n\t\t * Whether procest is present but has no vendor data in the window.\n\t\t *\n\t\t * @return {boolean} true when vendor is available but empty.\n\t\t */\n\t\tvendorEmpty() {\n\t\t\treturn this.vendor.available === true && this.vendor.empty === true\n\t\t},\n\n\t\t/**\n\t\t * The reporting-period dropdown options.\n\t\t *\n\t\t * @return {Array<{value: string, label: string}>} the options.\n\t\t */\n\t\tperiodOptions() {\n\t\t\treturn [\n\t\t\t\t{ value: 'month', label: t('nextcloud-vue', 'This month') },\n\t\t\t\t{ value: 'quarter', label: t('nextcloud-vue', 'This quarter') },\n\t\t\t\t{ value: 'ytd', label: t('nextcloud-vue', 'Year to date') },\n\t\t\t\t{ value: 'fy', label: t('nextcloud-vue', 'Fiscal year') },\n\t\t\t]\n\t\t},\n\n\t\t/**\n\t\t * The formatted total-spend amount.\n\t\t *\n\t\t * @return {string} the formatted total.\n\t\t */\n\t\tformattedTotal() {\n\t\t\treturn this.formatAmount(this.finance.total)\n\t\t},\n\n\t\t/**\n\t\t * The accessible label for the total-spend value.\n\t\t *\n\t\t * @return {string} the aria label.\n\t\t */\n\t\ttotalAriaLabel() {\n\t\t\treturn t('nextcloud-vue', 'Total spend: {amount}').replace('{amount}', this.formattedTotal)\n\t\t},\n\t},\n\n\twatch: {\n\t\tviewMode() {\n\t\t\tthis.scheduleFetch()\n\t\t},\n\t},\n\n\tmounted() {\n\t\tthis.fetchData()\n\t},\n\n\tbeforeUnmount() {\n\t\tif (this.debounceTimer) {\n\t\t\tclearTimeout(this.debounceTimer)\n\t\t}\n\t},\n\n\tmethods: {\n\t\tt,\n\n\t\t/**\n\t\t * Handle a reporting-period change.\n\t\t *\n\t\t * @param {string} value the new period.\n\t\t * @return {void}\n\t\t */\n\t\tonPeriodChange(value) {\n\t\t\tif (PERIODS.includes(value)) {\n\t\t\t\tthis.period = value\n\t\t\t\tthis.scheduleFetch()\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Debounce period / view changes before re-querying.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tscheduleFetch() {\n\t\t\tif (this.debounceTimer) {\n\t\t\t\tclearTimeout(this.debounceTimer)\n\t\t\t}\n\t\t\tthis.debounceTimer = setTimeout(() => {\n\t\t\t\tthis.fetchData()\n\t\t\t}, DEBOUNCE_MS)\n\t\t},\n\n\t\t/**\n\t\t * Fetch finance (always) + vendor (top-vendors view only) data in\n\t\t * parallel through the source. A missing source or a failure degrades\n\t\t * to the relevant empty state without throwing.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync fetchData() {\n\t\t\tif (!this.source) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.loading = true\n\t\t\tconst filters = (this.content && this.content.filters) || {}\n\t\t\ttry {\n\t\t\t\tconst tasks = [\n\t\t\t\t\tthis.source.fetchSummary({\n\t\t\t\t\t\tperiod: this.period,\n\t\t\t\t\t\tcategoryIds: Array.isArray(filters.categoryIds) ? filters.categoryIds : [],\n\t\t\t\t\t\tdepartmentIds: Array.isArray(filters.departmentIds) ? filters.departmentIds : [],\n\t\t\t\t\t}),\n\t\t\t\t]\n\t\t\t\tif (this.viewMode === 'top-vendors' && typeof this.source.fetchVendorCommitments === 'function') {\n\t\t\t\t\ttasks.push(this.source.fetchVendorCommitments({\n\t\t\t\t\t\tperiod: this.period,\n\t\t\t\t\t\tvendorIds: Array.isArray(filters.vendorIds) ? filters.vendorIds : [],\n\t\t\t\t\t}))\n\t\t\t\t}\n\t\t\t\tconst [finance, vendor] = await Promise.all(tasks)\n\t\t\t\tif (finance) {\n\t\t\t\t\tthis.finance = finance\n\t\t\t\t}\n\t\t\t\tif (vendor) {\n\t\t\t\t\tthis.vendor = vendor\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\tthis.finance = { available: false, empty: false, total: 0, currency: 'EUR', byCategory: [], trend: [] }\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Request an AI narrative through the source. When the source lacks\n\t\t * `fetchNarrative` or reports it unavailable, the panel disables\n\t\t * gracefully.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync requestNarrative() {\n\t\t\tif (!this.source || typeof this.source.fetchNarrative !== 'function') {\n\t\t\t\tthis.aiUnavailable = true\n\t\t\t\treturn\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tconst result = await this.source.fetchNarrative({\n\t\t\t\t\tsummary: { total: this.finance.total, currency: this.finance.currency, period: this.period },\n\t\t\t\t}) || {}\n\t\t\t\tif (result.available) {\n\t\t\t\t\tthis.aiNarrative = result.narrative\n\t\t\t\t\tthis.aiUnavailable = false\n\t\t\t\t} else {\n\t\t\t\t\tthis.aiUnavailable = true\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\tthis.aiUnavailable = true\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * The owning sibling app's URL for a row. The source may provide\n\t\t * `resolveDeepLink`; absent that, the row renders as plain text.\n\t\t *\n\t\t * @param {string} app the owning app id.\n\t\t * @param {string} kind the entity kind.\n\t\t * @param {string} id the entity id.\n\t\t * @return {string} the URL, or '' when unresolved.\n\t\t */\n\t\tdeepLink(app, kind, id) {\n\t\t\tif (this.source && typeof this.source.resolveDeepLink === 'function') {\n\t\t\t\tconst href = this.source.resolveDeepLink(app, kind, id)\n\t\t\t\tif (typeof href === 'string' && href !== '') {\n\t\t\t\t\treturn href\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn ''\n\t\t},\n\n\t\t/**\n\t\t * Format a monetary amount respecting the browser locale.\n\t\t *\n\t\t * @param {number} amount the amount.\n\t\t * @return {string} the locale-formatted currency string.\n\t\t */\n\t\tformatAmount(amount) {\n\t\t\tconst value = Number(amount) || 0\n\t\t\tconst locale = (typeof navigator !== 'undefined' && navigator.language) || 'en'\n\t\t\ttry {\n\t\t\t\treturn new Intl.NumberFormat(locale, {\n\t\t\t\t\tstyle: 'currency',\n\t\t\t\t\tcurrency: safeCurrencyCode(this.finance.currency),\n\t\t\t\t}).format(value)\n\t\t\t} catch {\n\t\t\t\treturn String(value)\n\t\t\t}\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-spend-analytics-widget {\n\tdisplay: flex;\n\tflex-direction: column;\n\theight: 100%;\n\tgap: 8px;\n\tpadding: 8px;\n\toverflow: hidden;\n}\n\n.cn-spend-analytics-widget__header {\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: space-between;\n}\n\n.cn-spend-analytics-widget__title {\n\tfont-weight: bold;\n}\n\n.cn-spend-analytics-widget__period {\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius);\n\tbackground: var(--color-main-background);\n\tcolor: var(--color-main-text);\n\tpadding: 2px 6px;\n}\n\n.cn-spend-analytics-widget__body {\n\tflex: 1;\n\toverflow: auto;\n}\n\n.cn-spend-analytics-widget__cards {\n\tdisplay: flex;\n\tgap: 12px;\n}\n\n.cn-spend-analytics-widget__card {\n\tdisplay: flex;\n\tflex-direction: column;\n\tpadding: 8px 12px;\n\tborder-radius: var(--border-radius);\n\tbackground: var(--color-background-hover);\n\tflex: 1;\n}\n\n.cn-spend-analytics-widget__card-label {\n\tfont-size: 0.85em;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-spend-analytics-widget__card-value {\n\tfont-size: 1.4em;\n\tfont-weight: bold;\n}\n\n.cn-spend-analytics-widget__panel-title {\n\tfont-size: 1em;\n\tmargin: 8px 0 4px;\n}\n\n.cn-spend-analytics-widget__rows {\n\tlist-style: none;\n\tmargin: 0;\n\tpadding: 0;\n}\n\n.cn-spend-analytics-widget__row {\n\tdisplay: flex;\n\tjustify-content: space-between;\n\tpadding: 4px 8px;\n\tborder-radius: var(--border-radius);\n\tcursor: pointer;\n\tcolor: inherit;\n\ttext-decoration: none;\n}\n\n.cn-spend-analytics-widget__row--static {\n\tcursor: default;\n}\n\n.cn-spend-analytics-widget__row:hover,\n.cn-spend-analytics-widget__row:focus {\n\tbackground: var(--color-background-hover);\n\toutline: none;\n}\n\n.cn-spend-analytics-widget__state {\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: center;\n\tmin-height: 64px;\n\tcolor: var(--color-text-maxcontrast);\n\tpadding: 12px;\n\ttext-align: center;\n}\n\n.cn-spend-analytics-widget__inline-empty {\n\tcolor: var(--color-text-maxcontrast);\n\tpadding: 6px;\n\ttext-align: left;\n}\n\n.cn-spend-analytics-widget__ai-text {\n\twhite-space: pre-wrap;\n}\n\n.cn-spend-analytics-widget__ai-btn {\n\tborder: 1px solid var(--color-border);\n\tbackground: var(--color-background-hover);\n\tborder-radius: var(--border-radius);\n\tcolor: var(--color-main-text);\n\tpadding: 4px 12px;\n\tcursor: pointer;\n}\n</style>\n"],"names":["_openBlock","_createElementBlock","_createElementVNode","_toDisplayString","_Fragment","_renderList"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CnTabs.vue2.js","sources":["../../../../src/components/CnTabs/CnTabs.vue"],"sourcesContent":["<template>\n\t<div class=\"cn-tabs\" :class=\"{ 'cn-tabs--card': card }\">\n\t\t<div class=\"cn-tabs__bar\">\n\t\t\t<div\n\t\t\t\tclass=\"cn-tabs__strip\"\n\t\t\t\t:class=\"{ 'cn-tabs__strip--more-start': moreStart, 'cn-tabs__strip--more-end': moreEnd }\">\n\t\t\t\t<div\n\t\t\t\t\tref=\"nav\"\n\t\t\t\t\tclass=\"cn-tabs__nav\"\n\t\t\t\t\t:class=\"{ 'cn-tabs__nav--justified': justified }\"\n\t\t\t\t\trole=\"tablist\"\n\t\t\t\t\t:aria-label=\"ariaLabel || null\"\n\t\t\t\t\t@keydown=\"onNavKeydown\"\n\t\t\t\t\t@wheel=\"onNavWheel\"\n\t\t\t\t\t@scroll=\"measureOverflow\">\n\t\t\t\t\t<button\n\t\t\t\t\t\tv-for=\"tab in tabs\"\n\t\t\t\t\t\t:id=\"tab.tabId\"\n\t\t\t\t\t\t:key=\"tab.uid\"\n\t\t\t\t\t\tref=\"navButtons\"\n\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\trole=\"tab\"\n\t\t\t\t\t\tclass=\"cn-tabs__nav-item\"\n\t\t\t\t\t\t:class=\"{ 'cn-tabs__nav-item--active': isActive(tab.uid) }\"\n\t\t\t\t\t\t:aria-selected=\"isActive(tab.uid) ? 'true' : 'false'\"\n\t\t\t\t\t\t:aria-controls=\"tab.panelId\"\n\t\t\t\t\t\t:tabindex=\"isActive(tab.uid) ? 0 : -1\"\n\t\t\t\t\t\t:disabled=\"tab.disabled || null\"\n\t\t\t\t\t\t@click=\"tab.onActivate()\">\n\t\t\t\t\t\t<component :is=\"tab.titleRender\" />\n\t\t\t\t\t</button>\n\t\t\t\t</div>\n\t\t\t\t<!-- Mouse-only affordances: keyboard users already have the arrow\n\t\t\t\t keys, and the tabs themselves stay in the accessibility tree. -->\n\t\t\t\t<button\n\t\t\t\t\tv-if=\"moreStart\"\n\t\t\t\t\ttype=\"button\"\n\t\t\t\t\tclass=\"cn-tabs__scroll cn-tabs__scroll--start\"\n\t\t\t\t\taria-hidden=\"true\"\n\t\t\t\t\ttabindex=\"-1\"\n\t\t\t\t\t@click=\"scrollStrip(-1)\">\n\t\t\t\t\t<ChevronLeft :size=\"20\" />\n\t\t\t\t</button>\n\t\t\t\t<button\n\t\t\t\t\tv-if=\"moreEnd\"\n\t\t\t\t\ttype=\"button\"\n\t\t\t\t\tclass=\"cn-tabs__scroll cn-tabs__scroll--end\"\n\t\t\t\t\taria-hidden=\"true\"\n\t\t\t\t\ttabindex=\"-1\"\n\t\t\t\t\t@click=\"scrollStrip(1)\">\n\t\t\t\t\t<ChevronRight :size=\"20\" />\n\t\t\t\t</button>\n\t\t\t</div>\n\t\t\t<div v-if=\"$slots['nav-end']\" class=\"cn-tabs__nav-end\">\n\t\t\t\t<!-- @slot nav-end Rendered at the right-hand end of the tab bar, deliberately OUTSIDE the `role=\"tablist\"` element. A widget Actions menu belongs beside the strip, not inside it: anything nested in the tablist is announced as one of the tabs, so a screen-reader user counting six tabs would hear seven. -->\n\t\t\t\t<!-- @binding {number} active-index Index of the currently selected tab. -->\n\t\t\t\t<slot name=\"nav-end\" :activeIndex=\"activeIndex\" />\n\t\t\t</div>\n\t\t</div>\n\t\t<div class=\"cn-tabs__content\" :class=\"contentClass\">\n\t\t\t<!-- @slot The CnTab children. Anything else is rendered into the panel area untouched. -->\n\t\t\t<slot />\n\t\t</div>\n\t</div>\n</template>\n\n<script>\n/**\n * CnTabs — a generic tab strip.\n *\n * Neither `@nextcloud/vue@9` nor this library previously shipped one: the only\n * tab component in either is `NcAppSidebarTab`, which is meaningless outside an\n * `NcAppSidebar`. Apps that needed a plain tab strip in a page, a dialog or a\n * detail view were on `bootstrap-vue@2` — a Vue-2-only package with no Vue 3\n * release (`bootstrap-vue-next` is a different package with a different API) —\n * and pulling a second UI framework in alongside `@nextcloud/vue` cuts against\n * the fleet rule that apps render with Nextcloud components. So zaakafhandelapp\n * wrote a local `Tabs`/`Tab` pair for eight views and opencatalogi and\n * openregister flagged the same gap. This is that pair, lifted and hardened.\n *\n * ```vue\n * <CnTabs aria-label=\"Case details\" justified>\n * <CnTab title=\"Documents\">…</CnTab>\n * <CnTab title=\"Tasks\" :active=\"showTasks\" @click=\"onTasks\">…</CnTab>\n * <CnTab>\n * <template #title><CnIcon name=\"close\" /> Draft</template>\n * …\n * </CnTab>\n * </CnTabs>\n * ```\n *\n * ## Putting controls beside the strip\n *\n * `#nav-end` fills the right-hand end of the tab bar. `CnTabsWidget` uses it to\n * carry the active child's Actions menu, so one menu serves every tab instead\n * of each panel drawing its own header:\n *\n * ```vue\n * <CnTabs aria-label=\"Case details\">\n * <template #nav-end><CnActionsMenu :title=\"activeTitle\" /></template>\n * <CnTab title=\"Notes\">…</CnTab>\n * </CnTabs>\n * ```\n *\n * The slot renders OUTSIDE the `role=\"tablist\"` element on purpose. See the\n * comment on it in the template.\n *\n * ## Migrating from `bootstrap-vue`\n *\n * `<BTabs>`/`<BTab>` map across directly for the subset apps actually used:\n * `content-class`, `justified` and `card` on the strip; `title`, the `#title`\n * slot, `active` and `@click` on a tab. Anything else (`lazy`, `no-fade`,\n * `pills`, `<b-tab disabled>`'s tooltip behaviour) is not reimplemented.\n *\n * ## How registration works\n *\n * Children register themselves on mount and hand back a *render function* for\n * their title rather than a plain string. That is what lets a child's `#title`\n * slot be rendered inside the parent's nav strip, and it keeps a computed title\n * reactive: the parent invokes it inside its own render effect, so a title\n * change re-renders the nav.\n *\n * Mount order is document order for both static children and `v-for`-generated\n * ones, so the nav strip matches the source. A tab that unmounts (a closable\n * tab) hands the selection to its neighbour.\n *\n * ## Accessibility\n *\n * Implements the WAI-ARIA tabs pattern: `role=\"tablist\"` / `role=\"tab\"` /\n * `role=\"tabpanel\"`, `aria-selected`, `aria-controls` ↔ `aria-labelledby`\n * wiring, a roving `tabindex` (only the selected tab is in the tab order), and\n * Left/Right/Home/End keyboard navigation within the strip. Pass `aria-label`\n * (or `ariaLabel`) so screen-reader users hear what the strip is for.\n */\nimport { computed, defineComponent, onBeforeUnmount, onMounted, onUpdated, provide, reactive, ref, watch } from 'vue'\nimport ChevronLeft from 'vue-material-design-icons/ChevronLeft.vue'\nimport ChevronRight from 'vue-material-design-icons/ChevronRight.vue'\nimport { CN_TABS_INJECTION_KEY } from './tabsKey.js'\n\nexport default defineComponent({\n\tname: 'CnTabs',\n\n\tcomponents: {\n\t\tChevronLeft,\n\t\tChevronRight,\n\t},\n\n\tprops: {\n\t\t/** Extra class applied to the panel container (bootstrap-vue's `content-class`). */\n\t\tcontentClass: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** Stretch the nav items to fill the strip (bootstrap-vue's `justified`). */\n\t\tjustified: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Card-style chrome (border + padding) around the panel area. */\n\t\tcard: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * Accessible name for the tab strip, applied to the `role=\"tablist\"`\n\t\t * element. Screen readers announce it when focus enters the strip.\n\t\t */\n\t\tariaLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\t},\n\n\temits: [\n\t\t/** The selected tab changed. Payload: the new active tab's index. */\n\t\t'update:activeIndex',\n\t],\n\n\tsetup(props, { emit }) {\n\t\t// Registered children, in mount order.\n\t\tconst tabs = reactive([])\n\t\tconst activeUid = ref(null)\n\t\tconst navButtons = ref([])\n\n\t\t/**\n\t\t * Register a child tab. The first child to register wins the initial\n\t\t * selection unless a later one declares itself `active`. Disabled tabs\n\t\t * never take the initial selection.\n\t\t *\n\t\t * @param {object} tab Child descriptor.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tfunction register(tab) {\n\t\t\ttabs.push(tab)\n\t\t\tif (tab.disabled) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (activeUid.value === null || tab.active) {\n\t\t\t\tactiveUid.value = tab.uid\n\t\t\t}\n\t\t}\n\n\t\t/**\n\t\t * Drop a child that is unmounting, moving the selection to its nearest\n\t\t * remaining neighbour when it was the active one. Closable tabs rely on\n\t\t * this; without it the strip would render with nothing selected.\n\t\t *\n\t\t * @param {number} uid The child's instance uid.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tfunction unregister(uid) {\n\t\t\tconst index = tabs.findIndex((tab) => tab.uid === uid)\n\t\t\tif (index !== -1) {\n\t\t\t\ttabs.splice(index, 1)\n\t\t\t}\n\t\t\tif (activeUid.value === uid) {\n\t\t\t\tconst neighbour = tabs[index] || tabs[index - 1] || tabs[0] || null\n\t\t\t\tactiveUid.value = neighbour ? neighbour.uid : null\n\t\t\t}\n\t\t}\n\n\t\t/**\n\t\t * Make a child the visible one.\n\t\t *\n\t\t * @param {number} uid The child's instance uid.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tfunction select(uid) {\n\t\t\tif (activeUid.value === uid) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tactiveUid.value = uid\n\t\t\temit('update:activeIndex', tabs.findIndex((tab) => tab.uid === uid))\n\t\t}\n\n\t\t/**\n\t\t * Whether a child is the visible one.\n\t\t *\n\t\t * @param {number} uid The child's instance uid.\n\t\t *\n\t\t * @return {boolean} True when this tab's panel should render.\n\t\t */\n\t\tfunction isActive(uid) {\n\t\t\treturn activeUid.value === uid\n\t\t}\n\n\t\tconst activeIndex = computed(() => tabs.findIndex((tab) => tab.uid === activeUid.value))\n\n\t\t/**\n\t\t * Move the selection and DOM focus to a tab by index, skipping disabled\n\t\t * ones. Focus has to move with the selection: in the WAI-ARIA tabs\n\t\t * pattern only the selected tab is in the tab order, so leaving focus on\n\t\t * a now-`tabindex=\"-1\"` button strands the keyboard user.\n\t\t *\n\t\t * @param {number} index Target index (clamped to the enabled tabs).\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tfunction focusTab(index) {\n\t\t\tconst enabled = tabs.filter((tab) => !tab.disabled)\n\t\t\tif (enabled.length === 0) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst wrapped = ((index % enabled.length) + enabled.length) % enabled.length\n\t\t\tconst target = enabled[wrapped]\n\t\t\tselect(target.uid)\n\t\t\tconst position = tabs.indexOf(target)\n\t\t\tconst button = navButtons.value?.[position]\n\t\t\tif (button && typeof button.focus === 'function') {\n\t\t\t\tbutton.focus()\n\t\t\t}\n\t\t}\n\n\t\t/**\n\t\t * Left/Right/Home/End navigation inside the strip.\n\t\t *\n\t\t * @param {KeyboardEvent} event The keydown event.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tfunction onNavKeydown(event) {\n\t\t\tconst enabled = tabs.filter((tab) => !tab.disabled)\n\t\t\tconst current = enabled.findIndex((tab) => tab.uid === activeUid.value)\n\t\t\tlet next = null\n\n\t\t\tif (event.key === 'ArrowRight') {\n\t\t\t\tnext = current + 1\n\t\t\t} else if (event.key === 'ArrowLeft') {\n\t\t\t\tnext = current - 1\n\t\t\t} else if (event.key === 'Home') {\n\t\t\t\tnext = 0\n\t\t\t} else if (event.key === 'End') {\n\t\t\t\tnext = enabled.length - 1\n\t\t\t}\n\n\t\t\tif (next === null) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tevent.preventDefault()\n\t\t\tfocusTab(next)\n\t\t}\n\n\t\tconst nav = ref(null)\n\n\t\t/**\n\t\t * Scroll the strip so the selected tab is wholly in view.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tfunction revealActiveTab() {\n\t\t\tconst strip = nav.value\n\t\t\tconst button = strip?.querySelector('[role=\"tab\"][aria-selected=\"true\"]')\n\t\t\tif (!strip || !button) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst stripRect = strip.getBoundingClientRect()\n\t\t\tconst rect = button.getBoundingClientRect()\n\t\t\tif (rect.left < stripRect.left) {\n\t\t\t\tstrip.scrollLeft -= stripRect.left - rect.left\n\t\t\t} else if (rect.right > stripRect.right) {\n\t\t\t\tstrip.scrollLeft += rect.right - stripRect.right\n\t\t\t}\n\t\t}\n\n\t\twatch(activeUid, revealActiveTab, { flush: 'post' })\n\n\t\t// Whether tabs are hidden past the start or end edge of the strip.\n\t\tconst moreStart = ref(false)\n\t\tconst moreEnd = ref(false)\n\n\t\t/**\n\t\t * Recompute which edges hide tabs; 1px of tolerance absorbs sub-pixel rounding.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tfunction measureOverflow() {\n\t\t\tconst strip = nav.value\n\t\t\tif (!strip) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tmoreStart.value = strip.scrollLeft > 1\n\t\t\tmoreEnd.value = strip.scrollWidth - strip.clientWidth - strip.scrollLeft > 1\n\t\t}\n\n\t\tlet resizeObserver = null\n\n\t\tonMounted(() => {\n\t\t\tmeasureOverflow()\n\t\t\tif (typeof ResizeObserver === 'undefined' || !nav.value) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tresizeObserver = new ResizeObserver(measureOverflow)\n\t\t\tresizeObserver.observe(nav.value)\n\t\t})\n\n\t\t// A title change or a tab added can tip the strip into overflowing without resizing it.\n\t\tonUpdated(measureOverflow)\n\n\t\tonBeforeUnmount(() => {\n\t\t\tresizeObserver?.disconnect()\n\t\t\tresizeObserver = null\n\t\t})\n\n\t\t/**\n\t\t * Scroll the strip most of a viewport towards one edge.\n\t\t *\n\t\t * @param {number} direction -1 towards the start, 1 towards the end.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tfunction scrollStrip(direction) {\n\t\t\tconst strip = nav.value\n\t\t\tif (!strip) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tstrip.scrollLeft += direction * Math.max(1, Math.round(strip.clientWidth * 0.6))\n\t\t}\n\n\t\t/**\n\t\t * Turn a vertical wheel into a horizontal scroll while the strip overflows.\n\t\t *\n\t\t * @param {WheelEvent} event The wheel event.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tfunction onNavWheel(event) {\n\t\t\tconst strip = event.currentTarget\n\t\t\tif (strip.scrollWidth <= strip.clientWidth || event.deltaX !== 0 || event.deltaY === 0) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tevent.preventDefault()\n\t\t\tstrip.scrollLeft += event.deltaY\n\t\t}\n\n\t\tprovide(CN_TABS_INJECTION_KEY, { register, unregister, select, isActive })\n\n\t\treturn {\n\t\t\ttabs,\n\t\t\tisActive,\n\t\t\tactiveIndex,\n\t\t\tnav,\n\t\t\tnavButtons,\n\t\t\tmoreStart,\n\t\t\tmoreEnd,\n\t\t\tmeasureOverflow,\n\t\t\tscrollStrip,\n\t\t\tonNavKeydown,\n\t\t\tonNavWheel,\n\t\t}\n\t},\n})\n</script>\n\n<style scoped>\n/* The bar owns the rule under the strip so a `#nav-end` surface sits ON the\n line rather than above a line that stops where the last tab does. With no\n nav-end content the nav is the bar's only child and the result is pixel\n identical to the rule living on the nav itself. */\n.cn-tabs__bar {\n\t/* Top-aligned so the `#nav-end` control sits on the tab row, not below it,\n\t should the two sides ever differ in height. */\n\talign-items: flex-start;\n\tborder-bottom: 1px solid var(--color-border);\n\tdisplay: flex;\n\tgap: 8px;\n}\n\n/* The strip holds the scrolling nav and the edge affordances drawn over it. */\n.cn-tabs__strip {\n\tflex: 1 1 auto;\n\tmin-width: 0;\n\tposition: relative;\n}\n\n.cn-tabs__nav {\n\tdisplay: flex;\n\tgap: 4px;\n\t/* One row that scrolls, never a wrap: in a narrow sidebar a nine-tab strip\n\t wrapped into a stack taller than the panel under it. The scrollbar is\n\t hidden because it would sit between the tabs and the bar's rule and break\n\t the open tab's join with its panel; a fade and a chevron on the clipped\n\t edge say there is more, and the strip follows the wheel. */\n\tflex-wrap: nowrap;\n\toverflow-x: auto;\n\toverflow-y: hidden;\n\tscroll-behavior: smooth;\n\tscrollbar-width: none;\n\t/* Overhang the bar's 1px rule so the open tab's own bottom edge, drawn in\n\t the panel's colour, covers it and the tab joins its panel. */\n\tmargin-bottom: -1px;\n}\n\n.cn-tabs__nav::-webkit-scrollbar {\n\tdisplay: none;\n}\n\n@media (prefers-reduced-motion: reduce) {\n\t.cn-tabs__nav {\n\t\tscroll-behavior: auto;\n\t}\n}\n\n/* A fade over the clipped edge, only on the side that hides tabs. Stops a\n pixel above the bar's rule so the rule keeps its full colour. */\n.cn-tabs__strip::before,\n.cn-tabs__strip::after {\n\tcontent: '';\n\tdisplay: none;\n\tinset-block: 0 1px;\n\tpointer-events: none;\n\tposition: absolute;\n\twidth: 56px;\n\tz-index: 1;\n}\n\n.cn-tabs__strip::before {\n\tbackground: linear-gradient(to right, var(--color-main-background) 30%, transparent);\n\tinset-inline-start: 0;\n}\n\n.cn-tabs__strip::after {\n\tbackground: linear-gradient(to left, var(--color-main-background) 30%, transparent);\n\tinset-inline-end: 0;\n}\n\n.cn-tabs__strip--more-start::before,\n.cn-tabs__strip--more-end::after {\n\tdisplay: block;\n}\n\n/* Doubled selector for the same reason as `.cn-tabs__nav .cn-tabs__nav-item`:\n Nextcloud's own `button` rule would otherwise give this a border, a fill\n and a margin. */\n.cn-tabs__strip .cn-tabs__scroll {\n\talign-items: center;\n\tbackground: none;\n\tborder: 0;\n\tborder-radius: 0;\n\tcolor: var(--color-main-text);\n\tcursor: pointer;\n\tdisplay: flex;\n\t/* Pushed down a little so the chevron sits level with the tab labels. */\n\tinset-block: 2px 0;\n\tjustify-content: center;\n\tmargin: 0;\n\tmin-height: 0;\n\tpadding: 0;\n\tposition: absolute;\n\twidth: 28px;\n\tz-index: 2;\n}\n\n/* Nextcloud's `button:hover` rule would otherwise fill the chevron. */\n.cn-tabs__strip .cn-tabs__scroll:hover,\n.cn-tabs__strip .cn-tabs__scroll:focus {\n\tbackground: none;\n}\n\n.cn-tabs__scroll--start {\n\tinset-inline-start: 0;\n}\n\n.cn-tabs__scroll--end {\n\tinset-inline-end: 0;\n}\n\n.cn-tabs__nav-end {\n\talign-items: center;\n\tdisplay: flex;\n\tflex: 0 0 auto;\n\tgap: 4px;\n\t/* No min-height. This used to hard-code 38px to mirror the old nav item's\n\t vertical box, but the folder-tab restyle changed that box to 33px, and\n\t because the bar is `align-items: flex-start` the taller nav-end then drove\n\t the BAR's height. The bar's bottom rule sank 6px below the tab row, so the\n\t open tab no longer met the panel it is drawn as joined to. Letting the\n\t control size to its own content keeps the rule on the tab row whatever the\n\t nav item's box becomes, so the two cannot drift apart again. */\n\tmin-height: 0;\n}\n\n/* Folder tabs, not underlined labels. An underline is the weakest possible\n affordance on a strip that already carries icons: with nine tabs on a dossiq\n case the row read as a sentence of links, and only the 2px rule said which\n one was open. Giving the inactive tabs their own darker surface makes the\n strip legible as a control at a glance, and the active tab, sharing the\n panel's background and punching a hole in the bar's rule, reads as the sheet\n in front. */\n.cn-tabs__nav-item {\n\tbackground-color: var(--color-background-dark);\n\t/* `--color-border-dark`, not `--color-border`: the latter is the same\n\t #ededed as `--color-background-dark` in the stock light theme, so a tab\n\t drawn with it has no visible edge against its own fill. */\n\tborder: 1px solid var(--color-border-dark);\n\tborder-bottom: none;\n\tborder-radius: var(--border-radius-large, 8px) var(--border-radius-large, 8px) 0 0;\n\t/* Full-contrast label, not `--color-text-maxcontrast`. On the old\n\t transparent strip maxcontrast sat on white at 5.3:1; on this darker\n\t surface the same pair measures 4.55:1, which clears WCAG AA by 0.05 and\n\t would not survive a themed instance shifting either token. A real tab\n\t strip carries its selection in the surface and the weight anyway, so\n\t nothing is lost by giving every label the readable colour. */\n\tcolor: var(--color-main-text);\n\tcursor: pointer;\n\tfont-weight: normal;\n\tpadding: 8px 12px;\n\twhite-space: nowrap;\n\t/* Match the `#nav-end` control's own height. The bar is a flex row, so\n\t whichever side is taller sets the bar's height and therefore where its\n\t bottom rule lands. When the control was the taller one the rule sat below\n\t the tab row and showed as a line under the open tab, breaking the join to\n\t the panel. Sizing both sides the same is what keeps the rule ON the tab\n\t row; `inline-flex` centres the label inside the taller box. */\n\talign-items: center;\n\tbox-sizing: border-box;\n\tdisplay: inline-flex;\n\tmin-height: var(--default-clickable-area, 34px);\n}\n\n/* Nextcloud's server stylesheet gives every plain `button` a 3px bottom margin\n with a selector that outscores the scoped rule above, which floated the tabs\n above the bar's rule inside a real page. The parent class wins on\n specificity rather than on `!important` or source order. */\n.cn-tabs__nav .cn-tabs__nav-item {\n\tmargin-block-end: 0;\n}\n\n.cn-tabs__nav--justified .cn-tabs__nav-item {\n\tflex: 1 1 0;\n}\n\n.cn-tabs__nav-item:hover,\n.cn-tabs__nav-item:focus-visible {\n\tbackground-color: var(--color-background-hover);\n\tcolor: var(--color-main-text);\n}\n\n/* Inside the box: the scrolling strip clips anything drawn outside a tab. */\n.cn-tabs__nav-item:focus-visible {\n\toutline-offset: -2px;\n}\n\n.cn-tabs__nav-item[disabled] {\n\tcursor: default;\n\topacity: 0.5;\n}\n\n.cn-tabs__nav-item[disabled]:hover {\n\tbackground-color: var(--color-background-dark);\n\tcolor: var(--color-main-text);\n}\n\n/* The open tab: the panel's own background, and a bottom border painted in\n that same background so it erases the bar's rule underneath and the tab\n joins the sheet below it. The primary-coloured top edge is what carries the\n selection at a glance, and it survives forced-colours mode, which drops\n background colours but keeps borders. */\n.cn-tabs__nav-item--active {\n\tbackground-color: var(--color-main-background);\n\tborder-bottom: 1px solid var(--color-main-background);\n\tborder-top: 2px solid var(--color-primary-element);\n\tcolor: var(--color-main-text);\n\tfont-weight: bold;\n\t/* Keep the text baseline identical to an inactive tab despite the 1px\n\t thicker top border, so the strip does not jog when the selection moves. */\n\tpadding-top: 7px;\n}\n\n.cn-tabs__content {\n\tpadding-top: 12px;\n}\n\n.cn-tabs--card .cn-tabs__content {\n\tborder: 1px solid var(--color-border);\n\tborder-top: none;\n\tpadding: 12px;\n}\n</style>\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"CnTabs.vue2.js","sources":["../../../../src/components/CnTabs/CnTabs.vue"],"sourcesContent":["<template>\n\t<div class=\"cn-tabs\" :class=\"{ 'cn-tabs--card': card }\">\n\t\t<div class=\"cn-tabs__bar\">\n\t\t\t<div\n\t\t\t\tclass=\"cn-tabs__strip\"\n\t\t\t\t:class=\"{ 'cn-tabs__strip--more-start': moreStart, 'cn-tabs__strip--more-end': moreEnd }\">\n\t\t\t\t<div\n\t\t\t\t\tref=\"nav\"\n\t\t\t\t\tclass=\"cn-tabs__nav\"\n\t\t\t\t\t:class=\"{ 'cn-tabs__nav--justified': justified }\"\n\t\t\t\t\trole=\"tablist\"\n\t\t\t\t\t:aria-label=\"ariaLabel || null\"\n\t\t\t\t\t@keydown=\"onNavKeydown\"\n\t\t\t\t\t@wheel=\"onNavWheel\"\n\t\t\t\t\t@scroll=\"measureOverflow\">\n\t\t\t\t\t<button\n\t\t\t\t\t\tv-for=\"tab in tabs\"\n\t\t\t\t\t\t:id=\"tab.tabId\"\n\t\t\t\t\t\t:key=\"tab.uid\"\n\t\t\t\t\t\tref=\"navButtons\"\n\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\trole=\"tab\"\n\t\t\t\t\t\tclass=\"cn-tabs__nav-item\"\n\t\t\t\t\t\t:class=\"{ 'cn-tabs__nav-item--active': isActive(tab.uid) }\"\n\t\t\t\t\t\t:aria-selected=\"isActive(tab.uid) ? 'true' : 'false'\"\n\t\t\t\t\t\t:aria-controls=\"tab.panelId\"\n\t\t\t\t\t\t:tabindex=\"isActive(tab.uid) ? 0 : -1\"\n\t\t\t\t\t\t:disabled=\"tab.disabled || null\"\n\t\t\t\t\t\t@click=\"tab.onActivate()\">\n\t\t\t\t\t\t<component :is=\"tab.titleRender\" />\n\t\t\t\t\t</button>\n\t\t\t\t</div>\n\t\t\t\t<!-- Mouse-only affordances: keyboard users already have the arrow\n\t\t\t\t keys, and the tabs themselves stay in the accessibility tree. -->\n\t\t\t\t<button\n\t\t\t\t\tv-if=\"moreStart\"\n\t\t\t\t\ttype=\"button\"\n\t\t\t\t\tclass=\"cn-tabs__scroll cn-tabs__scroll--start\"\n\t\t\t\t\taria-hidden=\"true\"\n\t\t\t\t\ttabindex=\"-1\"\n\t\t\t\t\t@click=\"scrollStrip(-1)\">\n\t\t\t\t\t<ChevronLeft :size=\"20\" />\n\t\t\t\t</button>\n\t\t\t\t<button\n\t\t\t\t\tv-if=\"moreEnd\"\n\t\t\t\t\ttype=\"button\"\n\t\t\t\t\tclass=\"cn-tabs__scroll cn-tabs__scroll--end\"\n\t\t\t\t\taria-hidden=\"true\"\n\t\t\t\t\ttabindex=\"-1\"\n\t\t\t\t\t@click=\"scrollStrip(1)\">\n\t\t\t\t\t<ChevronRight :size=\"20\" />\n\t\t\t\t</button>\n\t\t\t</div>\n\t\t\t<div v-if=\"$slots['nav-end']\" class=\"cn-tabs__nav-end\">\n\t\t\t\t<!-- @slot nav-end Rendered at the right-hand end of the tab bar, deliberately OUTSIDE the `role=\"tablist\"` element. A widget Actions menu belongs beside the strip, not inside it: anything nested in the tablist is announced as one of the tabs, so a screen-reader user counting six tabs would hear seven. -->\n\t\t\t\t<!-- @binding {number} active-index Index of the currently selected tab. -->\n\t\t\t\t<slot name=\"nav-end\" :activeIndex=\"activeIndex\" />\n\t\t\t</div>\n\t\t</div>\n\t\t<div class=\"cn-tabs__content\" :class=\"contentClass\">\n\t\t\t<!-- @slot The CnTab children. Anything else is rendered into the panel area untouched. -->\n\t\t\t<slot />\n\t\t</div>\n\t</div>\n</template>\n\n<script>\n/**\n * CnTabs — a generic tab strip.\n *\n * Neither `@nextcloud/vue@9` nor this library previously shipped one: the only\n * tab component in either is `NcAppSidebarTab`, which is meaningless outside an\n * `NcAppSidebar`. Apps that needed a plain tab strip in a page, a dialog or a\n * detail view were on `bootstrap-vue@2` — a Vue-2-only package with no Vue 3\n * release (`bootstrap-vue-next` is a different package with a different API) —\n * and pulling a second UI framework in alongside `@nextcloud/vue` cuts against\n * the fleet rule that apps render with Nextcloud components. So zaakafhandelapp\n * wrote a local `Tabs`/`Tab` pair for eight views and opencatalogi and\n * openregister flagged the same gap. This is that pair, lifted and hardened.\n *\n * ```vue\n * <CnTabs aria-label=\"Case details\" justified>\n * <CnTab title=\"Documents\">…</CnTab>\n * <CnTab title=\"Tasks\" :active=\"showTasks\" @click=\"onTasks\">…</CnTab>\n * <CnTab>\n * <template #title><CnIcon name=\"close\" /> Draft</template>\n * …\n * </CnTab>\n * </CnTabs>\n * ```\n *\n * ## Putting controls beside the strip\n *\n * `#nav-end` fills the right-hand end of the tab bar. `CnTabsWidget` uses it to\n * carry the active child's Actions menu, so one menu serves every tab instead\n * of each panel drawing its own header:\n *\n * ```vue\n * <CnTabs aria-label=\"Case details\">\n * <template #nav-end><CnActionsMenu :title=\"activeTitle\" /></template>\n * <CnTab title=\"Notes\">…</CnTab>\n * </CnTabs>\n * ```\n *\n * The slot renders OUTSIDE the `role=\"tablist\"` element on purpose. See the\n * comment on it in the template.\n *\n * ## Migrating from `bootstrap-vue`\n *\n * `<BTabs>`/`<BTab>` map across directly for the subset apps actually used:\n * `content-class`, `justified` and `card` on the strip; `title`, the `#title`\n * slot, `active` and `@click` on a tab. Anything else (`lazy`, `no-fade`,\n * `pills`, `<b-tab disabled>`'s tooltip behaviour) is not reimplemented.\n *\n * ## How registration works\n *\n * Children register themselves on mount and hand back a *render function* for\n * their title rather than a plain string. That is what lets a child's `#title`\n * slot be rendered inside the parent's nav strip, and it keeps a computed title\n * reactive: the parent invokes it inside its own render effect, so a title\n * change re-renders the nav.\n *\n * Mount order is document order for both static children and `v-for`-generated\n * ones, so the nav strip matches the source. A tab that unmounts (a closable\n * tab) hands the selection to its neighbour.\n *\n * ## Accessibility\n *\n * Implements the WAI-ARIA tabs pattern: `role=\"tablist\"` / `role=\"tab\"` /\n * `role=\"tabpanel\"`, `aria-selected`, `aria-controls` ↔ `aria-labelledby`\n * wiring, a roving `tabindex` (only the selected tab is in the tab order), and\n * Left/Right/Home/End keyboard navigation within the strip. Pass `aria-label`\n * (or `ariaLabel`) so screen-reader users hear what the strip is for.\n */\nimport { computed, defineComponent, onBeforeUnmount, onMounted, onUpdated, provide, reactive, ref, watch } from 'vue'\nimport ChevronLeft from 'vue-material-design-icons/ChevronLeft.vue'\nimport ChevronRight from 'vue-material-design-icons/ChevronRight.vue'\nimport { CN_TABS_INJECTION_KEY } from './tabsKey.js'\n\nexport default defineComponent({\n\tname: 'CnTabs',\n\n\tcomponents: {\n\t\tChevronLeft,\n\t\tChevronRight,\n\t},\n\n\tprops: {\n\t\t/** Extra class applied to the panel container (bootstrap-vue's `content-class`). */\n\t\tcontentClass: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** Stretch the nav items to fill the strip (bootstrap-vue's `justified`). */\n\t\tjustified: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Card-style chrome (border + padding) around the panel area. */\n\t\tcard: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * Accessible name for the tab strip, applied to the `role=\"tablist\"`\n\t\t * element. Screen readers announce it when focus enters the strip.\n\t\t */\n\t\tariaLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\t},\n\n\temits: [\n\t\t/** The selected tab changed. Payload: the new active tab's index. */\n\t\t'update:activeIndex',\n\t],\n\n\tsetup(props, { emit }) {\n\t\t// Registered children, in mount order.\n\t\tconst tabs = reactive([])\n\t\tconst activeUid = ref(null)\n\t\tconst navButtons = ref([])\n\n\t\t/**\n\t\t * Register a child tab. The first child to register wins the initial\n\t\t * selection unless a later one declares itself `active`. Disabled tabs\n\t\t * never take the initial selection.\n\t\t *\n\t\t * @param {object} tab Child descriptor.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tfunction register(tab) {\n\t\t\ttabs.push(tab)\n\t\t\tif (tab.disabled) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (activeUid.value === null || tab.active) {\n\t\t\t\tactiveUid.value = tab.uid\n\t\t\t}\n\t\t}\n\n\t\t/**\n\t\t * Drop a child that is unmounting, moving the selection to its nearest\n\t\t * remaining neighbour when it was the active one. Closable tabs rely on\n\t\t * this; without it the strip would render with nothing selected.\n\t\t *\n\t\t * @param {number} uid The child's instance uid.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tfunction unregister(uid) {\n\t\t\tconst index = tabs.findIndex((tab) => tab.uid === uid)\n\t\t\tif (index !== -1) {\n\t\t\t\ttabs.splice(index, 1)\n\t\t\t}\n\t\t\tif (activeUid.value === uid) {\n\t\t\t\tconst neighbour = tabs[index] || tabs[index - 1] || tabs[0] || null\n\t\t\t\tactiveUid.value = neighbour ? neighbour.uid : null\n\t\t\t}\n\t\t}\n\n\t\t/**\n\t\t * Make a child the visible one.\n\t\t *\n\t\t * @param {number} uid The child's instance uid.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tfunction select(uid) {\n\t\t\tif (activeUid.value === uid) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tactiveUid.value = uid\n\t\t\temit('update:activeIndex', tabs.findIndex((tab) => tab.uid === uid))\n\t\t}\n\n\t\t/**\n\t\t * Whether a child is the visible one.\n\t\t *\n\t\t * @param {number} uid The child's instance uid.\n\t\t *\n\t\t * @return {boolean} True when this tab's panel should render.\n\t\t */\n\t\tfunction isActive(uid) {\n\t\t\treturn activeUid.value === uid\n\t\t}\n\n\t\tconst activeIndex = computed(() => tabs.findIndex((tab) => tab.uid === activeUid.value))\n\n\t\t/**\n\t\t * Move the selection and DOM focus to a tab by index, skipping disabled\n\t\t * ones. Focus has to move with the selection: in the WAI-ARIA tabs\n\t\t * pattern only the selected tab is in the tab order, so leaving focus on\n\t\t * a now-`tabindex=\"-1\"` button strands the keyboard user.\n\t\t *\n\t\t * @param {number} index Target index (clamped to the enabled tabs).\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tfunction focusTab(index) {\n\t\t\tconst enabled = tabs.filter((tab) => !tab.disabled)\n\t\t\tif (enabled.length === 0) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst wrapped = ((index % enabled.length) + enabled.length) % enabled.length\n\t\t\tconst target = enabled[wrapped]\n\t\t\tselect(target.uid)\n\t\t\tconst position = tabs.indexOf(target)\n\t\t\tconst button = navButtons.value?.[position]\n\t\t\tif (button && typeof button.focus === 'function') {\n\t\t\t\tbutton.focus()\n\t\t\t}\n\t\t}\n\n\t\t/**\n\t\t * Left/Right/Home/End navigation inside the strip.\n\t\t *\n\t\t * @param {KeyboardEvent} event The keydown event.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tfunction onNavKeydown(event) {\n\t\t\tconst enabled = tabs.filter((tab) => !tab.disabled)\n\t\t\tconst current = enabled.findIndex((tab) => tab.uid === activeUid.value)\n\t\t\tlet next = null\n\n\t\t\tif (event.key === 'ArrowRight') {\n\t\t\t\tnext = current + 1\n\t\t\t} else if (event.key === 'ArrowLeft') {\n\t\t\t\tnext = current - 1\n\t\t\t} else if (event.key === 'Home') {\n\t\t\t\tnext = 0\n\t\t\t} else if (event.key === 'End') {\n\t\t\t\tnext = enabled.length - 1\n\t\t\t}\n\n\t\t\tif (next === null) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tevent.preventDefault()\n\t\t\tfocusTab(next)\n\t\t}\n\n\t\tconst nav = ref(null)\n\n\t\t/**\n\t\t * Scroll the strip so the selected tab is wholly in view.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tfunction revealActiveTab() {\n\t\t\tconst strip = nav.value\n\t\t\tconst button = strip?.querySelector('[role=\"tab\"][aria-selected=\"true\"]')\n\t\t\tif (!strip || !button) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst stripRect = strip.getBoundingClientRect()\n\t\t\tconst rect = button.getBoundingClientRect()\n\t\t\tif (rect.left < stripRect.left) {\n\t\t\t\tstrip.scrollLeft -= stripRect.left - rect.left\n\t\t\t} else if (rect.right > stripRect.right) {\n\t\t\t\tstrip.scrollLeft += rect.right - stripRect.right\n\t\t\t}\n\t\t}\n\n\t\twatch(activeUid, revealActiveTab, { flush: 'post' })\n\n\t\t// Whether tabs are hidden past the start or end edge of the strip.\n\t\tconst moreStart = ref(false)\n\t\tconst moreEnd = ref(false)\n\n\t\t/**\n\t\t * Recompute which edges hide tabs; 1px of tolerance absorbs sub-pixel rounding.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tfunction measureOverflow() {\n\t\t\tconst strip = nav.value\n\t\t\tif (!strip) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tmoreStart.value = strip.scrollLeft > 1\n\t\t\tmoreEnd.value = strip.scrollWidth - strip.clientWidth - strip.scrollLeft > 1\n\t\t}\n\n\t\tlet resizeObserver = null\n\n\t\tonMounted(() => {\n\t\t\tmeasureOverflow()\n\t\t\tif (typeof ResizeObserver === 'undefined' || !nav.value) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tresizeObserver = new ResizeObserver(measureOverflow)\n\t\t\tresizeObserver.observe(nav.value)\n\t\t})\n\n\t\t// A title change or a tab added can tip the strip into overflowing without resizing it.\n\t\tonUpdated(measureOverflow)\n\n\t\tonBeforeUnmount(() => {\n\t\t\tresizeObserver?.disconnect()\n\t\t\tresizeObserver = null\n\t\t})\n\n\t\t/**\n\t\t * Scroll the strip most of a viewport towards one edge.\n\t\t *\n\t\t * @param {number} direction -1 towards the start, 1 towards the end.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tfunction scrollStrip(direction) {\n\t\t\tconst strip = nav.value\n\t\t\tif (!strip) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tstrip.scrollLeft += direction * Math.max(1, Math.round(strip.clientWidth * 0.6))\n\t\t}\n\n\t\t/**\n\t\t * Turn a vertical wheel into a horizontal scroll while the strip overflows.\n\t\t *\n\t\t * @param {WheelEvent} event The wheel event.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tfunction onNavWheel(event) {\n\t\t\tconst strip = event.currentTarget\n\t\t\tif (strip.scrollWidth <= strip.clientWidth || event.deltaX !== 0 || event.deltaY === 0) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tevent.preventDefault()\n\t\t\tstrip.scrollLeft += event.deltaY\n\t\t}\n\n\t\tprovide(CN_TABS_INJECTION_KEY, { register, unregister, select, isActive })\n\n\t\treturn {\n\t\t\ttabs,\n\t\t\tisActive,\n\t\t\tactiveIndex,\n\t\t\tnav,\n\t\t\tnavButtons,\n\t\t\tmoreStart,\n\t\t\tmoreEnd,\n\t\t\tmeasureOverflow,\n\t\t\tscrollStrip,\n\t\t\tonNavKeydown,\n\t\t\tonNavWheel,\n\t\t}\n\t},\n})\n</script>\n\n<style scoped>\n/* The bar owns the rule under the strip so a `#nav-end` surface sits ON the\n line rather than above a line that stops where the last tab does. With no\n nav-end content the nav is the bar's only child and the result is pixel\n identical to the rule living on the nav itself. */\n.cn-tabs__bar {\n\t/* Top-aligned so the `#nav-end` control sits on the tab row, not below it,\n\t should the two sides ever differ in height. */\n\talign-items: flex-start;\n\tborder-bottom: 1px solid var(--color-border);\n\tdisplay: flex;\n\tgap: 8px;\n}\n\n/* The strip holds the scrolling nav and the edge affordances drawn over it. */\n.cn-tabs__strip {\n\tflex: 1 1 auto;\n\tmin-width: 0;\n\tposition: relative;\n}\n\n.cn-tabs__nav {\n\tdisplay: flex;\n\tgap: 4px;\n\t/* One row that scrolls, never a wrap: in a narrow sidebar a nine-tab strip\n\t wrapped into a stack taller than the panel under it. The scrollbar is\n\t hidden because it would sit between the tabs and the bar's rule and break\n\t the open tab's join with its panel; a fade and a chevron on the clipped\n\t edge say there is more, and the strip follows the wheel. */\n\tflex-wrap: nowrap;\n\toverflow-x: auto;\n\toverflow-y: hidden;\n\tscroll-behavior: smooth;\n\tscrollbar-width: none;\n\t/* Overhang the bar's 1px rule so the open tab's own bottom edge, drawn in\n\t the panel's colour, covers it and the tab joins its panel. */\n\tmargin-bottom: -1px;\n}\n\n.cn-tabs__nav::-webkit-scrollbar {\n\tdisplay: none;\n}\n\n@media (prefers-reduced-motion: reduce) {\n\t.cn-tabs__nav {\n\t\tscroll-behavior: auto;\n\t}\n}\n\n/* A fade over the clipped edge, only on the side that hides tabs. Stops a\n pixel above the bar's rule so the rule keeps its full colour.\n\n z-index 2 so the fades outrank a lifted tab: CnTabsWidget puts the open tab\n at 1, and a tie goes to DOM order, which would draw that tab over the start\n fade (it precedes the tabs) while the end fade still covered it. */\n.cn-tabs__strip::before,\n.cn-tabs__strip::after {\n\tcontent: '';\n\tdisplay: none;\n\tinset-block: 0 1px;\n\tpointer-events: none;\n\tposition: absolute;\n\twidth: 56px;\n\tz-index: 2;\n}\n\n.cn-tabs__strip::before {\n\tbackground: linear-gradient(to right, var(--color-main-background) 30%, transparent);\n\tinset-inline-start: 0;\n}\n\n.cn-tabs__strip::after {\n\tbackground: linear-gradient(to left, var(--color-main-background) 30%, transparent);\n\tinset-inline-end: 0;\n}\n\n.cn-tabs__strip--more-start::before,\n.cn-tabs__strip--more-end::after {\n\tdisplay: block;\n}\n\n/* Doubled selector for the same reason as `.cn-tabs__nav .cn-tabs__nav-item`:\n Nextcloud's own `button` rule would otherwise give this a border, a fill\n and a margin. */\n.cn-tabs__strip .cn-tabs__scroll {\n\talign-items: center;\n\tbackground: none;\n\tborder: 0;\n\tborder-radius: 0;\n\tcolor: var(--color-main-text);\n\tcursor: pointer;\n\tdisplay: flex;\n\t/* Pushed down a little so the chevron sits level with the tab labels. */\n\tinset-block: 2px 0;\n\tjustify-content: center;\n\tmargin: 0;\n\tmin-height: 0;\n\tpadding: 0;\n\tposition: absolute;\n\twidth: 28px;\n\tz-index: 3;\n}\n\n/* Nextcloud's `button:hover` rule would otherwise fill the chevron. */\n.cn-tabs__strip .cn-tabs__scroll:hover,\n.cn-tabs__strip .cn-tabs__scroll:focus {\n\tbackground: none;\n}\n\n.cn-tabs__scroll--start {\n\tinset-inline-start: 0;\n}\n\n.cn-tabs__scroll--end {\n\tinset-inline-end: 0;\n}\n\n.cn-tabs__nav-end {\n\talign-items: center;\n\tdisplay: flex;\n\tflex: 0 0 auto;\n\tgap: 4px;\n\t/* No min-height. This used to hard-code 38px to mirror the old nav item's\n\t vertical box, but the folder-tab restyle changed that box to 33px, and\n\t because the bar is `align-items: flex-start` the taller nav-end then drove\n\t the BAR's height. The bar's bottom rule sank 6px below the tab row, so the\n\t open tab no longer met the panel it is drawn as joined to. Letting the\n\t control size to its own content keeps the rule on the tab row whatever the\n\t nav item's box becomes, so the two cannot drift apart again. */\n\tmin-height: 0;\n}\n\n/* Folder tabs, not underlined labels. An underline is the weakest possible\n affordance on a strip that already carries icons: with nine tabs on a dossiq\n case the row read as a sentence of links, and only the 2px rule said which\n one was open. Giving the inactive tabs their own darker surface makes the\n strip legible as a control at a glance, and the active tab, sharing the\n panel's background and punching a hole in the bar's rule, reads as the sheet\n in front. */\n.cn-tabs__nav-item {\n\tbackground-color: var(--color-background-dark);\n\t/* `--color-border-dark`, not `--color-border`: the latter is the same\n\t #ededed as `--color-background-dark` in the stock light theme, so a tab\n\t drawn with it has no visible edge against its own fill. */\n\tborder: 1px solid var(--color-border-dark);\n\tborder-bottom: none;\n\tborder-radius: var(--border-radius-large, 8px) var(--border-radius-large, 8px) 0 0;\n\t/* Full-contrast label, not `--color-text-maxcontrast`. On the old\n\t transparent strip maxcontrast sat on white at 5.3:1; on this darker\n\t surface the same pair measures 4.55:1, which clears WCAG AA by 0.05 and\n\t would not survive a themed instance shifting either token. A real tab\n\t strip carries its selection in the surface and the weight anyway, so\n\t nothing is lost by giving every label the readable colour. */\n\tcolor: var(--color-main-text);\n\tcursor: pointer;\n\tfont-weight: normal;\n\tpadding: 8px 12px;\n\twhite-space: nowrap;\n\t/* Match the `#nav-end` control's own height. The bar is a flex row, so\n\t whichever side is taller sets the bar's height and therefore where its\n\t bottom rule lands. When the control was the taller one the rule sat below\n\t the tab row and showed as a line under the open tab, breaking the join to\n\t the panel. Sizing both sides the same is what keeps the rule ON the tab\n\t row; `inline-flex` centres the label inside the taller box. */\n\talign-items: center;\n\tbox-sizing: border-box;\n\tdisplay: inline-flex;\n\tmin-height: var(--default-clickable-area, 34px);\n}\n\n/* Nextcloud's server stylesheet gives every plain `button` a 3px bottom margin\n with a selector that outscores the scoped rule above, which floated the tabs\n above the bar's rule inside a real page. The parent class wins on\n specificity rather than on `!important` or source order. */\n.cn-tabs__nav .cn-tabs__nav-item {\n\tmargin-block-end: 0;\n}\n\n.cn-tabs__nav--justified .cn-tabs__nav-item {\n\tflex: 1 1 0;\n}\n\n.cn-tabs__nav-item:hover,\n.cn-tabs__nav-item:focus-visible {\n\tbackground-color: var(--color-background-hover);\n\tcolor: var(--color-main-text);\n}\n\n/* Inside the box: the scrolling strip clips anything drawn outside a tab. */\n.cn-tabs__nav-item:focus-visible {\n\toutline-offset: -2px;\n}\n\n.cn-tabs__nav-item[disabled] {\n\tcursor: default;\n\topacity: 0.5;\n}\n\n.cn-tabs__nav-item[disabled]:hover {\n\tbackground-color: var(--color-background-dark);\n\tcolor: var(--color-main-text);\n}\n\n/* The open tab: the panel's own background, and a bottom border painted in\n that same background so it erases the bar's rule underneath and the tab\n joins the sheet below it. The primary-coloured top edge is what carries the\n selection at a glance, and it survives forced-colours mode, which drops\n background colours but keeps borders. */\n.cn-tabs__nav-item--active {\n\tbackground-color: var(--color-main-background);\n\tborder-bottom: 1px solid var(--color-main-background);\n\tborder-top: 2px solid var(--color-primary-element);\n\tcolor: var(--color-main-text);\n\tfont-weight: bold;\n\t/* Keep the text baseline identical to an inactive tab despite the 1px\n\t thicker top border, so the strip does not jog when the selection moves. */\n\tpadding-top: 7px;\n}\n\n.cn-tabs__content {\n\tpadding-top: 12px;\n}\n\n.cn-tabs--card .cn-tabs__content {\n\tborder: 1px solid var(--color-border);\n\tborder-top: none;\n\tpadding: 12px;\n}\n</style>\n"],"names":[],"mappings":"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"}
|