@conduction/nextcloud-vue 2.56.0-beta.3 → 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/CnAppRoot/CnAppRoot.vue2.js +21 -1
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js +1 -1
- package/dist/esm/components/CnAppRoot/CnAppRoot.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/CnSetupWizard/CnSetupWizard.vue2.js +49 -5
- package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js.map +1 -1
- package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue3.js +1 -1
- package/dist/esm/components/CnSetupWizard/CnSetupWizard.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 +2 -0
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +2 -0
- package/dist/esm/l10n/nl.json.js.map +1 -1
- package/dist/esm/nextcloud-vue.css +78 -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/esm/utils/validateManifestV2.compiled.js +2 -2
- package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
- package/dist/nextcloud-vue.cjs.js +885 -393
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +78 -7
- package/l10n/en.json +2 -0
- package/l10n/nl.json +2 -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/CnAppRoot/CnAppRoot.vue +31 -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/CnSetupWizard/CnSetupWizard.vue +57 -6
- 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/schemas/app-manifest-v2.schema.json +2 -1
- 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
- package/src/utils/validateManifestV2.compiled.js +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CnDataTable.vue3.js","sources":["../../../../src/components/CnDataTable/CnDataTable.vue?vue&type=template&id=54ce98fa&lang.js"],"sourcesContent":["<template>\n\t<div\n\t\tclass=\"cn-table-container\"\n\t\tdata-testid=\"cn-object-list\"\n\t\t:class=\"{\n\t\t\t'cn-table-container--scrollable': scrollable,\n\t\t\t'cn-table-container--borderless': borderless,\n\t\t\t'cn-table-container--fill': fillHeight,\n\t\t}\">\n\t\t<!-- Optional card header (folded from the retired CnTableWidget): a title\n\t\t + total-count badge. Only rendered when `title` is set; bare table\n\t\t usage is unchanged. -->\n\t\t<div v-if=\"title\" class=\"cn-data-table__header\">\n\t\t\t<h3 class=\"cn-data-table__title\">\n\t\t\t\t{{ title }}\n\t\t\t</h3>\n\t\t\t<span v-if=\"totalRowCount > 0\" class=\"cn-data-table__count\">\n\t\t\t\t{{ totalRowCount }}\n\t\t\t</span>\n\t\t</div>\n\n\t\t<!-- Loading State -->\n\t\t<div v-if=\"isLoading\" class=\"cn-table-loading\" data-testid=\"cn-object-list-loading\">\n\t\t\t<!-- Decorative: the adjacent <p> already carries the accessible\n\t\t\t name (loadingText), so the spinner is hidden from the\n\t\t\t accessibility tree rather than exposed as an unlabelled\n\t\t\t role=\"img\" (WCAG 1.1.1 / axe \"role-img-alt\"). -->\n\t\t\t<NcLoadingIcon :size=\"32\" aria-hidden=\"true\" />\n\t\t\t<p>{{ loadingText }}</p>\n\t\t</div>\n\n\t\t<!-- Table. The horizontal scroll lives on this wrapper, NOT on\n\t\t `.cn-table-container`: `overflow-x: auto` on the container coerces\n\t\t its `overflow-y` from `visible` to `auto` (CSS forbids mixing\n\t\t `visible` with a non-visible value), which silently made the\n\t\t container the nearest scrollport. The sticky footer then anchored\n\t\t to a container that never scrolls, so \"View all\" scrolled away with\n\t\t the rows instead of pinning to the bottom of the enclosing widget. -->\n\t\t<!-- The horizontal scrollport must be reachable by keyboard: a region\n\t\t that scrolls but cannot take focus gives a keyboard-only user no\n\t\t way to reach the columns past the fold (axe\n\t\t `scrollable-region-focusable`, serious; WCAG 2.1.1).\n\n\t\t Gated on ACTUAL overflow rather than applied unconditionally, for\n\t\t two reasons. A table that fits needs no scrolling, so a permanent\n\t\t tab stop would put every table in the fleet on the keyboard path\n\t\t for nothing. And the axe rule itself only applies to elements that\n\t\t really are scrollable, so the conditional attribute is present in\n\t\t exactly the cases the rule evaluates.\n\n\t\t `role=\"group\"`, NOT `role=\"region\"`: this element is nested inside\n\t\t `.cn-widget-wrapper__content[role=\"region\"]` on the dashboard-widget\n\t\t path, and a second landmark there would surface as a duplicate.\n\t\t `group` is nameable but is not a landmark. A name is required\n\t\t rather than optional — `aria-label` is PROHIBITED on a role-less\n\t\t generic element, so a bare `tabindex` div would trade this\n\t\t violation for `aria-prohibited-attr` plus an unlabelled mystery\n\t\t stop in the tab order. -->\n\t\t<div\n\t\t\tv-else\n\t\t\tref=\"scrollEl\"\n\t\t\tclass=\"cn-data-table__scroll\"\n\t\t\t:tabindex=\"isScrollable ? 0 : undefined\"\n\t\t\t:role=\"isScrollable ? 'group' : undefined\"\n\t\t\t:aria-label=\"isScrollable ? scrollRegionLabel : undefined\">\n\t\t\t<table\n\t\t\t\tclass=\"cn-data-table\"\n\t\t\t\t:class=\"{ 'cn-data-table--fixed': fixedLayout }\"\n\t\t\t\tdata-testid=\"cn-object-list-table\">\n\t\t\t\t<thead v-if=\"!hideHeader\">\n\t\t\t\t\t<tr>\n\t\t\t\t\t\t<!-- Checkbox column -->\n\t\t\t\t\t\t<th v-if=\"selectable\" class=\"cn-table-col--checkbox\">\n\t\t\t\t\t\t\t<NcCheckboxRadioSwitch\n\t\t\t\t\t\t\t\t:modelValue=\"allSelected\"\n\t\t\t\t\t\t\t\t:indeterminate=\"someSelected && !allSelected\"\n\t\t\t\t\t\t\t\t:aria-label=\"selectAllLabel\"\n\t\t\t\t\t\t\t\t@update:modelValue=\"toggleSelectAll\" />\n\t\t\t\t\t\t</th>\n\n\t\t\t\t\t\t<!-- Leading icon column (header is intentionally blank) -->\n\t\t\t\t\t\t<th v-if=\"rowIcon\" class=\"cn-table-col--icon\" />\n\n\t\t\t\t\t\t<!-- Data columns -->\n\t\t\t\t\t\t<th\n\t\t\t\t\t\t\tv-for=\"col in effectiveColumns\"\n\t\t\t\t\t\t\t:key=\"col.key\"\n\t\t\t\t\t\t\t:class=\"[\n\t\t\t\t\t\t\t\tcol.sortable ? 'cn-table-header--sortable' : '',\n\t\t\t\t\t\t\t\tcol.class || '',\n\t\t\t\t\t\t\t]\"\n\t\t\t\t\t\t\t:style=\"col.width ? { width: col.width } : {}\"\n\t\t\t\t\t\t\t:tabindex=\"col.sortable ? 0 : null\"\n\t\t\t\t\t\t\t:aria-sort=\"ariaSortFor(col)\"\n\t\t\t\t\t\t\t:title=\"translateLabel(col.description) || null\"\n\t\t\t\t\t\t\t@click=\"col.sortable ? onHeaderClick(col.key, $event) : null\"\n\t\t\t\t\t\t\t@keydown.enter=\"col.sortable ? onHeaderKeydown(col.key, $event) : null\">\n\t\t\t\t\t\t\t<span :class=\"col.description ? 'cn-table-header--described' : ''\">\n\t\t\t\t\t\t\t\t{{ translateLabel(col.label) }}\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\tv-if=\"col.sortable && sortKeyIndex(col.key) !== -1\"\n\t\t\t\t\t\t\t\tclass=\"cn-table-sort-indicator\">\n\t\t\t\t\t\t\t\t{{ effectiveSortKeys[sortKeyIndex(col.key)].order === 'asc' ? '▲' : '▼' }}\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\tv-if=\"col.sortable && effectiveSortKeys.length > 1 && sortKeyIndex(col.key) !== -1\"\n\t\t\t\t\t\t\t\tclass=\"cn-table-sort-badge\">\n\t\t\t\t\t\t\t\t{{ sortKeyIndex(col.key) + 1 }}\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t</th>\n\n\t\t\t\t\t\t<!-- Actions column -->\n\t\t\t\t\t\t<th v-if=\"$slots['row-actions']\" class=\"cn-table-col--actions\">\n\t\t\t\t\t\t\t<!-- @slot Header cell content above the row-actions column (blank by default). -->\n\t\t\t\t\t\t\t<slot name=\"actions-header\" />\n\t\t\t\t\t\t</th>\n\t\t\t\t\t</tr>\n\t\t\t\t</thead>\n\n\t\t\t\t<tbody>\n\t\t\t\t\t<!-- Empty state -->\n\t\t\t\t\t<tr v-if=\"effectiveRows.length === 0\" class=\"cn-table-empty\" data-testid=\"cn-object-list-empty\">\n\t\t\t\t\t\t<td :colspan=\"totalColumns\">\n\t\t\t\t\t\t\t<!-- @slot Empty-state content shown when there are no rows (defaults to `emptyText`). -->\n\t\t\t\t\t\t\t<slot name=\"empty\">\n\t\t\t\t\t\t\t\t{{ translateLabel(emptyText) }}\n\t\t\t\t\t\t\t</slot>\n\t\t\t\t\t\t</td>\n\t\t\t\t\t</tr>\n\n\t\t\t\t\t<!-- Data rows -->\n\t\t\t\t\t<tr\n\t\t\t\t\t\tv-for=\"row in effectiveRows\"\n\t\t\t\t\t\tv-else\n\t\t\t\t\t\t:key=\"row[rowKey]\"\n\t\t\t\t\t\tclass=\"cn-table-row\"\n\t\t\t\t\t\tdata-testid=\"cn-object-row\"\n\t\t\t\t\t\t:data-testid-row-id=\"row[rowKey]\"\n\t\t\t\t\t\t:class=\"[\n\t\t\t\t\t\t\tisSelected(row) ? 'cn-table-row--selected' : '',\n\t\t\t\t\t\t\trowClass ? rowClass(row) : '',\n\t\t\t\t\t\t]\"\n\t\t\t\t\t\t@mousedown=\"onPointerDown\"\n\t\t\t\t\t\t@click=\"onRowClick(row, $event)\"\n\t\t\t\t\t\t@auxclick=\"onRowAuxClick(row, $event)\"\n\t\t\t\t\t\t@contextmenu.prevent=\"onRowContextMenu(row, $event)\">\n\t\t\t\t\t\t<!-- Checkbox -->\n\t\t\t\t\t\t<td v-if=\"selectable\"\n\t\t\t\t\t\t\tclass=\"cn-table-col--checkbox\"\n\t\t\t\t\t\t\t@click.stop\n\t\t\t\t\t\t\t@auxclick.stop>\n\t\t\t\t\t\t\t<NcCheckboxRadioSwitch\n\t\t\t\t\t\t\t\t:modelValue=\"isSelected(row)\"\n\t\t\t\t\t\t\t\t:aria-label=\"selectRowLabel\"\n\t\t\t\t\t\t\t\t@update:modelValue=\"toggleSelect(row)\" />\n\t\t\t\t\t\t</td>\n\n\t\t\t\t\t\t<!-- Leading icon -->\n\t\t\t\t\t\t<td v-if=\"rowIcon\" class=\"cn-table-col--icon\">\n\t\t\t\t\t\t\t<CnIcon :name=\"getRowIcon(row)\" :size=\"20\" />\n\t\t\t\t\t\t</td>\n\n\t\t\t\t\t\t<!-- Data cells -->\n\t\t\t\t\t\t<td\n\t\t\t\t\t\t\tv-for=\"(col, colIndex) in effectiveColumns\"\n\t\t\t\t\t\t\t:key=\"col.key\"\n\t\t\t\t\t\t\t:class=\"[col.class || '', col.cellClass || '', cellClass ? cellClass(row, col) : '']\"\n\t\t\t\t\t\t\t:style=\"col.width ? { maxWidth: col.width } : {}\"\n\t\t\t\t\t\t\t@mouseenter=\"titleWhenClipped\">\n\t\t\t\t\t\t\t<!-- The padlock rides the FIRST data cell, beside whatever\n\t\t\t\t\t\t\t names the row. Deliberately OUTSIDE the #column-<key>\n\t\t\t\t\t\t\t slot: a consumer overriding that column's rendering is\n\t\t\t\t\t\t\t customising their own data, not opting out of being told\n\t\t\t\t\t\t\t the record is locked. It renders nothing when unlocked,\n\t\t\t\t\t\t\t so an unlocked table is byte-for-byte what it was. -->\n\t\t\t\t\t\t\t<CnLockIndicator\n\t\t\t\t\t\t\t\tv-if=\"colIndex === 0\"\n\t\t\t\t\t\t\t\t:object=\"row\"\n\t\t\t\t\t\t\t\t:size=\"16\" />\n\t\t\t\t\t\t\t<!-- Declared state indicators ride the first data cell\n\t\t\t\t\t\t\t beside the padlock. Each one renders as an icon WITH a\n\t\t\t\t\t\t\t text alternative and a tooltip, never as colour alone,\n\t\t\t\t\t\t\t and an indicator the page did not declare cannot appear\n\t\t\t\t\t\t\t however the record is shaped. Past the cap the rest move\n\t\t\t\t\t\t\t into the row menu, because a row of nine icons is not a\n\t\t\t\t\t\t\t row anyone can triage at a glance. -->\n\t\t\t\t\t\t\t<span v-if=\"colIndex === 0 && indicatorsFor(row).shown.length > 0\"\n\t\t\t\t\t\t\t\tclass=\"cn-table-row-indicators\"\n\t\t\t\t\t\t\t\tdata-testid=\"cn-row-indicators\">\n\t\t\t\t\t\t\t\t<span v-for=\"indicator in indicatorsFor(row).shown\"\n\t\t\t\t\t\t\t\t\t:key=\"indicator.id || indicator.field\"\n\t\t\t\t\t\t\t\t\tclass=\"cn-table-row-indicator\"\n\t\t\t\t\t\t\t\t\t:title=\"indicator.tooltip || indicator.text\"\n\t\t\t\t\t\t\t\t\t:data-testid=\"`cn-row-indicator-${indicator.id || indicator.field}`\">\n\t\t\t\t\t\t\t\t\t<CnIcon :name=\"indicator.icon || 'InformationOutline'\" :size=\"16\" />\n\t\t\t\t\t\t\t\t\t<span class=\"hidden-visually\">{{ indicator.text }}</span>\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t<!-- @slot Per-column cell override (`#column-<key>`), scoped with { row, value }. Wins over CnCellRenderer. -->\n\t\t\t\t\t\t\t<slot :name=\"'column-' + col.key\" :row=\"row\" :value=\"cellValue(row, col)\">\n\t\t\t\t\t\t\t\t<!-- Every column renders through CnCellRenderer: it resolves\n\t\t\t\t\t\t\t col.formatter / col.widget against the injected registries\n\t\t\t\t\t\t\t (cnFormatters / cnCellWidgets), uses the schema property when\n\t\t\t\t\t\t\t one is available (else {}) for type-aware rendering, and\n\t\t\t\t\t\t\t falls back to formatValue(). Columns with `aggregate` get a\n\t\t\t\t\t\t\t count of related objects (see cellValue/loadAggregates). The\n\t\t\t\t\t\t\t #column-{key} slot still wins. -->\n\t\t\t\t\t\t\t\t<CnCellRenderer\n\t\t\t\t\t\t\t\t\t:value=\"cellValue(row, col)\"\n\t\t\t\t\t\t\t\t\t:property=\"columnProperty(col)\"\n\t\t\t\t\t\t\t\t\t:formatter=\"col.formatter || null\"\n\t\t\t\t\t\t\t\t\t:formatterOptions=\"col.formatterOptions || null\"\n\t\t\t\t\t\t\t\t\t:widget=\"col.widget || null\"\n\t\t\t\t\t\t\t\t\t:widgetProps=\"col.widgetProps || undefined\"\n\t\t\t\t\t\t\t\t\t:format=\"columnFormat(col)\"\n\t\t\t\t\t\t\t\t\t:row=\"row\"\n\t\t\t\t\t\t\t\t\t:rowKey=\"rowKey\" />\n\t\t\t\t\t\t\t</slot>\n\t\t\t\t\t\t</td>\n\n\t\t\t\t\t\t<!-- Row actions -->\n\t\t\t\t\t\t<td v-if=\"$slots['row-actions']\"\n\t\t\t\t\t\t\tclass=\"cn-table-col--actions\"\n\t\t\t\t\t\t\t:class=\"[cellClass ? cellClass(row, { key: 'actions' }) : '']\"\n\t\t\t\t\t\t\t@click.stop\n\t\t\t\t\t\t\t@auxclick.stop>\n\t\t\t\t\t\t\t<!-- @slot Per-row actions menu (e.g. a CnRowActions), scoped with { row }. Supplying it adds the trailing actions column. -->\n\t\t\t\t\t\t\t<slot name=\"row-actions\" :row=\"row\" />\n\t\t\t\t\t\t</td>\n\t\t\t\t\t</tr>\n\t\t\t\t</tbody>\n\t\t\t</table>\n\t\t</div>\n\n\t\t<!-- Optional footer. A `#footer` scoped slot lets a host render its own\n\t\t footer link (e.g. a \"+ New\" create action or an always-shown\n\t\t \"View all\") with its own click handler — useful when the widget runs\n\t\t outside a vue-router context (the built-in link uses $router). When\n\t\t no slot is given, the built-in \"View all\" control (folded from the\n\t\t retired CnTableWidget) is shown for a `limit`-ed subset.\n\n\t\t The control is a real link (with an href) when the router resolves\n\t\t `viewAllRoute` to a URL, and a real button otherwise. An `<a>`\n\t\t without an href has no link role and is not keyboard focusable\n\t\t (WCAG 2.1.1, 4.1.2), so it is never rendered. -->\n\t\t<div\n\t\t\tv-if=\"$slots.footer || (viewAllRoute && totalRowCount > effectiveRows.length)\"\n\t\t\tclass=\"cn-data-table__footer\">\n\t\t\t<!-- @slot Custom footer content, scoped with { total, shown } (defaults to the built-in \"View all\" link). -->\n\t\t\t<slot name=\"footer\" :total=\"totalRowCount\" :shown=\"effectiveRows.length\">\n\t\t\t\t<a\n\t\t\t\t\tv-if=\"viewAllHref\"\n\t\t\t\t\tclass=\"cn-data-table__view-all\"\n\t\t\t\t\t:href=\"viewAllHref\"\n\t\t\t\t\t@click=\"onViewAll\">\n\t\t\t\t\t{{ viewAllLabel }}\n\t\t\t\t</a>\n\t\t\t\t<button\n\t\t\t\t\tv-else\n\t\t\t\t\ttype=\"button\"\n\t\t\t\t\tclass=\"cn-data-table__view-all\"\n\t\t\t\t\t@click=\"onViewAll\">\n\t\t\t\t\t{{ viewAllLabel }}\n\t\t\t\t</button>\n\t\t\t</slot>\n\t\t</div>\n\t</div>\n</template>\n\n<script>\nimport axios from '@nextcloud/axios'\nimport { translate as t } from '@nextcloud/l10n'\nimport { generateUrl } from '@nextcloud/router'\nimport { NcCheckboxRadioSwitch, NcLoadingIcon } from '@nextcloud/vue'\nimport { useClickDragGuard } from '../../composables/useClickDragGuard.js'\nimport { openRowTarget } from '../../utils/linkNavigation.js'\nimport { nextSortState } from '../../utils/multiColumnSort.js'\nimport { isNewTabHandled, isRowMiddleClick, markNewTabHandled } from '../../utils/rowAuxClick.js'\nimport { DEFAULT_ROW_INDICATOR_CAP, resolveRowIndicators } from '../../utils/rowIndicators.js'\nimport { columnsFromSchema } from '../../utils/schema.js'\nimport { CnCellRenderer } from '../CnCellRenderer/index.js'\nimport { CnIcon } from '../CnIcon/index.js'\nimport { CnLockIndicator } from '../CnLockIndicator/index.js'\n\n// CnDataTable has no scoped styles of its own — its entire look lives in the\n// shared table stylesheet. Import it here so the table is styled even when the\n// consuming app does not pull in the library's global css/index.css.\nimport '../../css/table.css'\n\n/**\n * CnDataTable — Generic sortable data table for list views.\n *\n * Replaces the copy-pasted `<table class=\"viewTable\">` HTML pattern found in\n * every list view across OpenRegister, Pipelinq, and Dossiq. Supports sorting,\n * row selection, custom cell rendering via scoped slots, loading states,\n * and empty states.\n *\n * Sorting: a plain click on a sortable header is single-sort (cycle asc →\n * desc → cleared), unchanged from before. Shift+click (or Shift+Enter on a\n * focused header) appends the column as a secondary/tertiary sort key —\n * capped at 3 — with numbered priority badges (1, 2, 3) once more than one\n * key is active. Pass `sortKeys: [{key, order}, ...]` for multi-sort (falls\n * back to the legacy `sortKey`/`sortOrder` props when empty). The `sort`\n * event payload is extended, not replaced: `{key, order}` still mirrors the\n * primary key exactly as before; a new `keys` field carries the full\n * ordered list. See `src/utils/multiColumnSort.js` for the state machine.\n *\n * When a `schema` prop is provided, columns are auto-generated from schema\n * properties and cells render through CnCellRenderer for type-aware formatting\n * (dates, booleans, UUIDs, enums, etc.). Scoped slots still override individual\n * columns when needed.\n *\n * Manual columns (backwards compatible)\n * ```vue\n * <CnDataTable\n * :columns=\"[\n * { key: 'name', label: 'Name', sortable: true },\n * { key: 'email', label: 'Email' },\n * ]\"\n * :rows=\"clients\"\n * @row-click=\"openClient\" />\n * ```\n *\n * Schema-driven (auto columns)\n * ```vue\n * <CnDataTable :schema=\"schema\" :rows=\"objects\" />\n * ```\n *\n * Schema with overrides and custom cell\n * ```vue\n * <CnDataTable\n * :schema=\"schema\"\n * :exclude-columns=\"['description']\"\n * :column-overrides=\"{ status: { width: '200px' } }\"\n * :rows=\"objects\">\n * <template #column-status=\"{ row, value }\">\n * <QuickStatusDropdown :case-obj=\"row\" />\n * </template>\n * </CnDataTable>\n * ```\n */\nexport default {\n\tname: 'CnDataTable',\n\n\tcomponents: {\n\t\tNcLoadingIcon,\n\t\tNcCheckboxRadioSwitch,\n\t\tCnCellRenderer,\n\t\tCnIcon,\n\t\tCnLockIndicator,\n\t},\n\n\tinject: {\n\t\t/**\n\t\t * Consumer translation function, provided by CnAppRoot as\n\t\t * `cnTranslate: this.translate` (bound to the host app's id, e.g.\n\t\t * `t.bind(null, 'docudesk')`). Column headers come from schema\n\t\t * property titles, which are authored in English as the canonical\n\t\t * source (API predictability); the visible label is resolved through\n\t\t * this function so it follows the user's language, with the English\n\t\t * source key living in each app's l10n files. Defaults to identity\n\t\t * when the table is used standalone (no CnAppRoot ancestor).\n\t\t */\n\t\tcnTranslate: { default: () => (key) => key },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * Column definitions (manual mode).\n\t\t * Not required when `schema` is provided.\n\t\t * Each entry may be a full column object OR a bare string key; bare strings\n\t\t * are normalised to `{ key, label }` by `effectiveColumns` so manifest-driven\n\t\t * pages that pass `config.columns` as a string array work without extra mapping.\n\t\t *\n\t\t * `description` renders as the header cell's tooltip and marks the label with a\n\t\t * dotted underline, so a column whose meaning is not obvious from its name (a\n\t\t * maturity level, a computed score, a domain term) can explain itself where the\n\t\t * reader is looking. `columnsFromSchema` fills it from the JSON Schema property\n\t\t * description automatically, so schema-driven tables get it for free.\n\t\t *\n\t\t * @type {Array<{key: string, label: string, description: string, sortable: boolean, width: string, class: string, cellClass: string}|string>}\n\t\t */\n\t\tcolumns: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * Optional leading icon shown at the start of every row. Either a static\n\t\t * MDI icon name (PascalCase, e.g. `'FileDocumentOutline'`) applied to all\n\t\t * rows, or a function `(row) => iconName` to vary it per row. The icon is\n\t\t * resolved through the shared CnIcon registry. Unset = no icon column.\n\t\t *\n\t\t * @type {string | ((row: object) => string) | null}\n\t\t */\n\t\trowIcon: {\n\t\t\ttype: [String, Function],\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * State indicators a page declares for its rows. Each entry is\n\t\t * `{ id, field, equals?, in?, icon, text, tooltip? }`: `field` is a\n\t\t * dotted path on the row, the condition is `equals`, `in`, or plain\n\t\t * truthiness when neither is given, `icon` is a CnIcon name, and `text`\n\t\t * is the text alternative. An entry without `text` does not render,\n\t\t * because an icon with no text is colour and shape alone.\n\t\t *\n\t\t * The page declares which indicators exist. A record cannot add one the\n\t\t * page has not declared, however it is shaped. A page declaring none\n\t\t * renders its rows exactly as before.\n\t\t *\n\t\t * @type {Array<{id?: string, field: string, equals?: (string|number|boolean|null), in?: Array, icon?: string, text: string, tooltip?: string}>}\n\t\t */\n\t\trowIndicators: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * How many declared indicators render on the row itself. The rest are\n\t\t * available from `indicatorsFor(row).overflow` for the row menu.\n\t\t *\n\t\t * @type {number}\n\t\t */\n\t\trowIndicatorCap: {\n\t\t\ttype: Number,\n\t\t\tdefault: DEFAULT_ROW_INDICATOR_CAP,\n\t\t},\n\n\t\t/**\n\t\t * Schema object with `properties` field (schema-driven mode).\n\t\t * When provided, columns are auto-generated from schema properties.\n\t\t */\n\t\tschema: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Per-column overrides when using schema mode: { key: { width, label, sortable, ... } } */\n\t\tcolumnOverrides: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/** Column keys to exclude when using schema mode */\n\t\texcludeColumns: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/** Column keys to include when using schema mode (whitelist) */\n\t\tincludeColumns: {\n\t\t\ttype: Array,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Row data array. Each row should have a unique identifier (see rowKey). */\n\t\trows: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/** Whether data is loading (shows loading spinner) */\n\t\tloading: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Current sort column key */\n\t\tsortKey: {\n\t\t\ttype: String,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Current sort order: 'asc', 'desc', or null (no sort) */\n\t\tsortOrder: {\n\t\t\ttype: String,\n\t\t\tdefault: 'asc',\n\t\t\tvalidator: (v) => v === null || ['asc', 'desc'].includes(v),\n\t\t},\n\n\t\t/**\n\t\t * Ordered multi-column sort state: `[{ key, order }, ...]` (priority\n\t\t * order, 0 to 3 entries). Optional — when empty (the default), the\n\t\t * table falls back to the legacy `sortKey`/`sortOrder` props, so\n\t\t * single-sort hosts are completely unaffected. Shift+click a\n\t\t * sortable header to append/cycle a secondary or tertiary key (see\n\t\t * `src/utils/multiColumnSort.js`).\n\t\t *\n\t\t * @type {Array<{key: string, order: 'asc'|'desc'}>}\n\t\t */\n\t\tsortKeys: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/** Whether rows can be selected with checkboxes */\n\t\tselectable: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Array of currently selected row IDs */\n\t\tselectedIds: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/** Property name used as unique row identifier */\n\t\trowKey: {\n\t\t\ttype: String,\n\t\t\tdefault: 'id',\n\t\t},\n\n\t\t/** Text shown when there are no rows */\n\t\temptyText: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'No items found'),\n\t\t},\n\n\t\t/** Function returning CSS class(es) for a row: (row) => string|object */\n\t\trowClass: {\n\t\t\ttype: Function,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Function returning CSS class(es) for a data cell: (row, col) => string|object */\n\t\tcellClass: {\n\t\t\ttype: Function,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Whether to constrain table height and make it scrollable */\n\t\tscrollable: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Text shown while loading */\n\t\tloadingText: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'Loading…'),\n\t\t},\n\n\t\t/**\n\t\t * Accessible name for the select-all checkbox in the header row.\n\t\t * Used as the checkbox's `aria-label` so screen readers announce a\n\t\t * named control (WCAG 4.1.2). Defaults to the lib's translation of\n\t\t * \"Select all rows\".\n\t\t */\n\t\tselectAllLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'Select all rows'),\n\t\t},\n\n\t\t/**\n\t\t * Accessible name for a per-row select checkbox. Used as the\n\t\t * checkbox's `aria-label` so screen readers announce a named control\n\t\t * (WCAG 4.1.2). Defaults to the lib's translation of \"Select row\".\n\t\t */\n\t\tselectRowLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'Select row'),\n\t\t},\n\n\t\t/**\n\t\t * Optional card title rendered in a header above the table. When set, the\n\t\t * table reads as a self-contained card (the container's own border/radius\n\t\t * is the card chrome). Folded in from the retired CnTableWidget.\n\t\t */\n\t\ttitle: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Drop the container's card chrome (border, radius, shadow) so the table\n\t\t * sits flush inside a parent that already provides a card (e.g. a\n\t\t * CnWidgetWrapper dashboard slot). Folded in from CnTableWidget.\n\t\t */\n\t\tborderless: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * Fill the height of the parent (a flex-column card / widget content\n\t\t * area) so the optional `#footer` is pushed to the bottom instead of\n\t\t * floating directly under a short list. When the list is long enough to\n\t\t * overflow, the footer stays pinned via its sticky rule. No-op outside a\n\t\t * height-constrained parent. Opt-in so ordinary in-flow tables are\n\t\t * unaffected.\n\t\t */\n\t\tfillHeight: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * Hide the column-header row (`<thead>`). Useful for compact dashboard\n\t\t * list widgets that want a plain bordered-row list without column labels.\n\t\t */\n\t\thideHeader: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * Switch the table to `table-layout: fixed`, making each column's `width`\n\t\t * authoritative instead of a hint the browser may override. Opt in when a\n\t\t * column's content would otherwise dictate the layout — a long unbreakable\n\t\t * value (a PHP FQCN, a UUID) widens its column under the default auto\n\t\t * layout and can render past the cell box into its neighbour, while any\n\t\t * column left unsized soaks up all remaining width. Cells also break long\n\t\t * words rather than overflowing. Columns with no `width` share whatever\n\t\t * space is left, so size every column when you want exact control.\n\t\t *\n\t\t * @type {boolean}\n\t\t */\n\t\tfixedLayout: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * Max number of rows to display. When the total exceeds it, only the first\n\t\t * `limit` render and the \"View all\" footer appears (with `viewAllRoute`).\n\t\t * 0 = show all. Folded in from CnTableWidget.\n\t\t */\n\t\tlimit: {\n\t\t\ttype: Number,\n\t\t\tdefault: 0,\n\t\t},\n\n\t\t/**\n\t\t * vue-router route object for the \"View all\" footer link. The footer only\n\t\t * shows when set AND the rows are a `limit`-ed subset. Folded in from CnTableWidget.\n\t\t *\n\t\t * @type {object|null}\n\t\t */\n\t\tviewAllRoute: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Pre-translated \"View all\" footer label. */\n\t\tviewAllLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'View all'),\n\t\t},\n\n\t\t/**\n\t\t * Self-fetch mode (folded from CnTableWidget): the OpenRegister register\n\t\t * id/slug. When `register` + `schemaId` are set and no `rows` are passed,\n\t\t * the table fetches `/apps/openregister/api/objects/{register}/{schemaId}`.\n\t\t *\n\t\t * @type {string|number|null}\n\t\t */\n\t\tregister: {\n\t\t\ttype: [String, Number],\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Self-fetch mode (folded from CnTableWidget): the OpenRegister schema\n\t\t * id used together with `register`. (Distinct from the `schema` prop,\n\t\t * which is a JSON Schema object for column generation.)\n\t\t *\n\t\t * @type {string|number|null}\n\t\t */\n\t\tschemaId: {\n\t\t\ttype: [String, Number],\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Extra query parameters sent with the self-fetch request (register +\n\t\t * schemaId mode) — e.g. a resolved filter map, `_order[field]` ordering,\n\t\t * or `_limit`. Changing it re-triggers the self-fetch, so a host widget\n\t\t * (CnWidgetObjectTable's declarative `source`) can drive filtering and\n\t\t * ordering without re-implementing the fetch. Ignored when external\n\t\t * `rows` are supplied.\n\t\t *\n\t\t * @type {object|null}\n\t\t */\n\t\tfetchParams: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Convenience navigation (folded from CnTableWidget): a function that\n\t\t * receives the clicked row and returns a vue-router route to push. When\n\t\t * set, a row click navigates there (the `row-click` event still fires);\n\t\t * a ctrl/cmd/shift or middle click opens it in a new tab.\n\t\t *\n\t\t * @type {((row: object) => object)|null}\n\t\t */\n\t\trowClickRoute: {\n\t\t\ttype: Function,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * When true, a row-body click emits `row-click` (for navigation) even\n\t\t * while `selectable` — selection then happens only via the checkbox\n\t\t * column. Lets \"click row = open, tick box = select\" coexist. Default\n\t\t * false keeps the legacy behaviour (selectable rows select on body click).\n\t\t *\n\t\t * @type {boolean}\n\t\t */\n\t\trowClickToView: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\t},\n\n\temits: ['row-click', 'row-context-menu', 'select', 'select-all', 'sort', 'view-all'],\n\n\tsetup() {\n\t\t// Tell a deliberate row click apart from a text-selection drag.\n\t\treturn useClickDragGuard()\n\t},\n\n\tdata() {\n\t\treturn {\n\t\t\t/**\n\t\t\t * Resolved aggregate-column values, keyed by `String(row[rowKey])`\n\t\t\t * then by column key. Populated by `loadAggregates()` for columns\n\t\t\t * that declare `aggregate` (see CnIndexPage's manifest config).\n\t\t\t *\n\t\t\t * @type {{[rowId: string]: {[colKey: string]: number}}}\n\t\t\t */\n\t\t\taggregateValues: {},\n\t\t\t/**\n\t\t\t * Whether the table currently overflows its scrollport horizontally.\n\t\t\t * Drives the keyboard tab stop on `.cn-data-table__scroll`; see the\n\t\t\t * comment on that element.\n\t\t\t *\n\t\t\t * @type {boolean}\n\t\t\t */\n\t\t\tisScrollable: false,\n\t\t\t/** True while a batch of aggregate-count requests is in flight. */\n\t\t\taggregateLoading: false,\n\t\t\t/** Monotonic id used to discard a stale aggregate batch when `rows` changes mid-flight. */\n\t\t\taggregateRequestId: 0,\n\t\t\t/** Rows fetched in self-fetch mode (register + schemaId). */\n\t\t\tfetchedRows: [],\n\t\t\t/** True while a self-fetch request is in flight. */\n\t\t\tselfFetchLoading: false,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * Accessible name for the horizontal scrollport when it becomes a tab\n\t\t * stop. Prefers the table's own `title` so the announcement identifies\n\t\t * WHICH table the user has landed in — several can share a dashboard.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tscrollRegionLabel() {\n\t\t\treturn this.title\n\t\t\t\t? t('nextcloud-vue', '{title} — scrollable table', { title: this.title })\n\t\t\t\t: t('nextcloud-vue', 'Scrollable table')\n\t\t},\n\n\t\t/**\n\t\t * The row source: external `rows` when provided, else the self-fetched\n\t\t * rows (register + schemaId mode). External rows always win.\n\t\t *\n\t\t * @return {Array<object>}\n\t\t */\n\t\tsourceRows() {\n\t\t\tif (this.rows && this.rows.length > 0) {\n\t\t\t\treturn this.rows\n\t\t\t}\n\t\t\tif (this.register !== null && this.register !== undefined && this.schemaId !== null && this.schemaId !== undefined) {\n\t\t\t\treturn this.fetchedRows\n\t\t\t}\n\t\t\treturn this.rows\n\t\t},\n\n\t\t/**\n\t\t * The rows actually rendered — `sourceRows` capped to `limit` (0 = all).\n\t\t *\n\t\t * @return {Array<object>}\n\t\t */\n\t\teffectiveRows() {\n\t\t\treturn this.limit > 0 ? this.sourceRows.slice(0, this.limit) : this.sourceRows\n\t\t},\n\n\t\t/**\n\t\t * Total row count before the `limit` cap (drives the count badge + the\n\t\t * \"View all\" footer condition).\n\t\t *\n\t\t * @return {number}\n\t\t */\n\t\ttotalRowCount() {\n\t\t\treturn this.sourceRows.length\n\t\t},\n\n\t\t/**\n\t\t * The URL the router resolves `viewAllRoute` to, or `null` when there is\n\t\t * no route or no router (a widget mounted outside a vue-router context).\n\t\t * Decides whether the \"View all\" footer renders as a link or a button.\n\t\t *\n\t\t * @return {string|null}\n\t\t */\n\t\tviewAllHref() {\n\t\t\tif (!this.viewAllRoute || !this.$router || typeof this.$router.resolve !== 'function') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tconst resolved = this.$router.resolve(this.viewAllRoute)\n\t\t\t\treturn (resolved && resolved.href) || null\n\t\t\t} catch {\n\t\t\t\treturn null\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Whether to show the loading state — the external `loading` prop OR a\n\t\t * self-fetch in flight.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\tisLoading() {\n\t\t\treturn this.loading || this.selfFetchLoading\n\t\t},\n\n\t\t/**\n\t\t * Effective columns: schema-generated or manually provided.\n\t\t * Schema columns take precedence when schema is provided and no manual columns given.\n\t\t */\n\t\teffectiveColumns() {\n\t\t\tconst cols = (this.schema && this.columns.length === 0)\n\t\t\t\t? columnsFromSchema(this.schema, {\n\t\t\t\t\t\texclude: this.excludeColumns,\n\t\t\t\t\t\tinclude: this.includeColumns,\n\t\t\t\t\t\toverrides: this.columnOverrides,\n\t\t\t\t\t})\n\t\t\t\t: this.columns\n\t\t\tif (!(cols || []).some((c) => typeof c === 'string')) {\n\t\t\t\treturn cols || []\n\t\t\t}\n\t\t\tconst schemaCols = this.schema\n\t\t\t\t? columnsFromSchema(this.schema, { overrides: this.columnOverrides })\n\t\t\t\t: []\n\t\t\tconst byKey = new Map(schemaCols.map((c) => [c.key, c]))\n\t\t\treturn (cols || []).map((c) => {\n\t\t\t\tif (typeof c !== 'string') {\n\t\t\t\t\treturn c\n\t\t\t\t}\n\t\t\t\treturn byKey.get(c) || { key: c, label: c, sortable: true }\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * The active ordered sort-key list: the `sortKeys` prop when non-empty,\n\t\t * else a single-entry list derived from the legacy `sortKey`/`sortOrder`\n\t\t * props (empty when neither is active). Every header/badge/aria-sort\n\t\t * computation reads this so single-sort hosts (no `sortKeys` passed)\n\t\t * render exactly as before.\n\t\t *\n\t\t * @return {Array<{key: string, order: 'asc'|'desc'}>}\n\t\t */\n\t\teffectiveSortKeys() {\n\t\t\tif (this.sortKeys && this.sortKeys.length > 0) {\n\t\t\t\treturn this.sortKeys\n\t\t\t}\n\t\t\tif (this.sortKey) {\n\t\t\t\treturn [{ key: this.sortKey, order: this.sortOrder || 'asc' }]\n\t\t\t}\n\t\t\treturn []\n\t\t},\n\n\t\ttotalColumns() {\n\t\t\tlet count = this.effectiveColumns.length\n\t\t\tif (this.selectable) {\n\t\t\t\tcount++\n\t\t\t}\n\t\t\tif (this.rowIcon) {\n\t\t\t\tcount++\n\t\t\t}\n\t\t\tif (this.$slots['row-actions']) {\n\t\t\t\tcount++\n\t\t\t}\n\t\t\treturn count\n\t\t},\n\n\t\t/**\n\t\t * Stable signature of the self-fetch inputs so the watcher refetches\n\t\t * only on a real change (register / schemaId / fetchParams).\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tselfFetchKey() {\n\t\t\treturn JSON.stringify({\n\t\t\t\tregister: this.register,\n\t\t\t\tschemaId: this.schemaId,\n\t\t\t\tfetchParams: this.fetchParams || null,\n\t\t\t})\n\t\t},\n\n\t\tallSelected() {\n\t\t\treturn this.effectiveRows.length > 0\n\t\t\t\t&& this.effectiveRows.every((row) => this.selectedIds.includes(row[this.rowKey]))\n\t\t},\n\n\t\tsomeSelected() {\n\t\t\treturn this.effectiveRows.some((row) => this.selectedIds.includes(row[this.rowKey]))\n\t\t},\n\t},\n\n\twatch: {\n\t\trows: {\n\t\t\thandler() {\n\t\t\t\tthis.loadAggregates()\n\t\t\t},\n\t\t},\n\n\t\teffectiveColumns: {\n\t\t\thandler() {\n\t\t\t\tthis.loadAggregates()\n\t\t\t},\n\n\t\t\tdeep: false,\n\t\t},\n\n\t\t/**\n\t\t * Re-run the self-fetch when its inputs (register, schemaId, or the\n\t\t * host-driven `fetchParams`) change — a token-resolved filter (e.g.\n\t\t * `@workspace.*`) can change after mount. External rows still win.\n\t\t */\n\t\tselfFetchKey() {\n\t\t\tif ((!this.rows || this.rows.length === 0) && this.register !== null && this.register !== undefined && this.schemaId !== null && this.schemaId !== undefined) {\n\t\t\t\tthis.fetchData()\n\t\t\t}\n\t\t},\n\t},\n\n\tmounted() {\n\t\tthis.loadAggregates()\n\t\t// Self-fetch mode: pull rows from OpenRegister when register + schemaId\n\t\t// are given and no external rows were passed (folded from CnTableWidget).\n\t\tif ((!this.rows || this.rows.length === 0) && this.register !== null && this.register !== undefined && this.schemaId !== null && this.schemaId !== undefined) {\n\t\t\tthis.fetchData()\n\t\t}\n\t\tthis.observeScrollOverflow()\n\t},\n\n\tupdated() {\n\t\t// Columns and rows can change after mount (self-fetch resolving, a\n\t\t// column toggled), and either can flip the table between fitting and\n\t\t// overflowing. ResizeObserver alone would miss a change that alters\n\t\t// content width without resizing the box.\n\t\tthis.measureScrollOverflow()\n\t},\n\n\tbeforeUnmount() {\n\t\tthis.disconnectScrollOverflow()\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * The declared indicators that apply to one row, split into the ones\n\t\t * that fit on the row and the ones that go to the row menu.\n\t\t *\n\t\t * @param {object} row The row.\n\t\t * @return {{shown: Array<object>, overflow: Array<object>}}\n\t\t * @spec openspec/changes/working-list-row-actions/specs/index-page/spec.md\n\t\t */\n\t\tindicatorsFor(row) {\n\t\t\treturn resolveRowIndicators(this.rowIndicators, row, this.rowIndicatorCap)\n\t\t},\n\n\t\t/**\n\t\t * Give a cell its full text on hover, and only when it is cut short.\n\t\t *\n\t\t * Cells are one line with an ellipsis, so a long value is readable only\n\t\t * as far as the column goes. A `title` on every cell would answer that,\n\t\t * and would also put a tooltip on the cells that need none — repeating\n\t\t * back what is already on screen.\n\t\t *\n\t\t * Read on ENTER rather than on render: `scrollWidth > clientWidth` forces\n\t\t * layout, and doing it per cell per paint would cost a reflow for a\n\t\t * tooltip almost none of them want. On enter it is one read of one cell,\n\t\t * and it is re-read every time, so a column resized since the last hover\n\t\t * answers for its new width rather than its old one.\n\t\t *\n\t\t * @param {MouseEvent} event The enter event.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\ttitleWhenClipped(event) {\n\t\t\tconst el = event.currentTarget\n\t\t\tif (!el) {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\t// 1px of tolerance, as the scrollport check uses: sub-pixel rounding\n\t\t\t// otherwise reports a cell that visually fits as clipped.\n\t\t\tif (el.scrollWidth - el.clientWidth > 1) {\n\t\t\t\tel.setAttribute('title', (el.textContent || '').trim())\n\t\t\t} else {\n\t\t\t\tel.removeAttribute('title')\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Start watching the scrollport for horizontal overflow.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tobserveScrollOverflow() {\n\t\t\tthis.measureScrollOverflow()\n\t\t\tif (typeof ResizeObserver === 'undefined') {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis._scrollObserver = new ResizeObserver(() => this.measureScrollOverflow())\n\t\t\tconst el = this.$refs.scrollEl\n\t\t\tif (el) {\n\t\t\t\tthis._scrollObserver.observe(el)\n\t\t\t\t// The table itself, not just the port: a column widening pushes\n\t\t\t\t// the content past the fold without the port changing size.\n\t\t\t\tconst table = el.querySelector('table')\n\t\t\t\tif (table) {\n\t\t\t\t\tthis._scrollObserver.observe(table)\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Recompute whether the scrollport overflows horizontally.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tmeasureScrollOverflow() {\n\t\t\tconst el = this.$refs.scrollEl\n\t\t\t// 1px of tolerance: sub-pixel layout rounding otherwise reports a\n\t\t\t// table that visually fits as scrollable, which would put a tab\n\t\t\t// stop on it for no reachable content.\n\t\t\tconst next = !!el && (el.scrollWidth - el.clientWidth) > 1\n\t\t\tif (next !== this.isScrollable) {\n\t\t\t\tthis.isScrollable = next\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Tear down the overflow observer.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tdisconnectScrollOverflow() {\n\t\t\tif (this._scrollObserver) {\n\t\t\t\tthis._scrollObserver.disconnect()\n\t\t\t\tthis._scrollObserver = null\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Resolve a column header label through the consumer's translation\n\t\t * function. Column labels originate from schema property titles\n\t\t * (English canonical source); this makes the rendered header follow\n\t\t * the user's language when the host app provides `cnTranslate`, and\n\t\t * returns the label unchanged when it does not.\n\t\t *\n\t\t * @param {string} label The English source label (schema property title).\n\t\t * @return {string} The translated label, or the input unchanged.\n\t\t */\n\t\ttranslateLabel(label) {\n\t\t\tif (!label) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tconst fn = typeof this.cnTranslate === 'function' ? this.cnTranslate : (k) => k\n\t\t\treturn fn(label)\n\t\t},\n\n\t\t/**\n\t\t * Self-fetch rows from OpenRegister (register + schemaId mode). Best-effort:\n\t\t * any failure leaves the fetched rows empty. Folded from CnTableWidget.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync fetchData() {\n\t\t\tthis.selfFetchLoading = true\n\t\t\ttry {\n\t\t\t\tconst url = generateUrl('/apps/openregister/api/objects/{register}/{schemaId}', {\n\t\t\t\t\tregister: String(this.register),\n\t\t\t\t\tschemaId: String(this.schemaId),\n\t\t\t\t})\n\t\t\t\tconst { data } = await axios.get(url, {\n\t\t\t\t\theaders: { 'OCS-APIREQUEST': 'true' },\n\t\t\t\t\t...(this.fetchParams ? { params: this.fetchParams } : {}),\n\t\t\t\t})\n\t\t\t\tthis.fetchedRows = (data && data.results) || (Array.isArray(data) ? data : [])\n\t\t\t} catch {\n\t\t\t\tthis.fetchedRows = []\n\t\t\t} finally {\n\t\t\t\tthis.selfFetchLoading = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Activate the \"View all\" footer control: emit `view-all`, then push\n\t\t * `viewAllRoute` through the router when there is one. A plain left\n\t\t * click on the link is handled in-app; a modified click (ctrl/cmd/\n\t\t * shift/middle) is left to the browser so \"open in new tab\" keeps\n\t\t * working on the real href.\n\t\t *\n\t\t * @param {MouseEvent} [event] The click event, when triggered by one.\n\t\t * @return {void}\n\t\t */\n\t\tonViewAll(event) {\n\t\t\tconst modified = Boolean(event && (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey || event.button))\n\t\t\tif (event && !modified) {\n\t\t\t\tevent.preventDefault()\n\t\t\t}\n\t\t\t/** @event view-all Emitted when the built-in \"View all\" footer control is activated. Payload: the `viewAllRoute` object. */\n\t\t\tthis.$emit('view-all', this.viewAllRoute)\n\t\t\tif (modified) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (this.viewAllRoute && this.$router) {\n\t\t\t\tthis.$router.push(this.viewAllRoute).catch(() => {})\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Resolve the leading-row icon name for a row. Returns the static\n\t\t * `rowIcon` string, or the result of the `rowIcon(row)` function.\n\t\t *\n\t\t * @param {object} row The row object.\n\t\t * @return {string} The MDI icon name (PascalCase).\n\t\t */\n\t\tgetRowIcon(row) {\n\t\t\treturn typeof this.rowIcon === 'function' ? this.rowIcon(row) : this.rowIcon\n\t\t},\n\n\t\t/**\n\t\t * Get a cell value from a row using dot-notation key.\n\t\t *\n\t\t * OpenRegister system/metadata fields (created, updated, owner, uri,\n\t\t * size, register, schema, ...) live under the object's `@self` block.\n\t\t * For a flat key we fall back to `@self` when the top level has no\n\t\t * value, so sidebar-enabled metadata columns resolve. Top-level fields\n\t\t * always win, keeping existing behaviour unchanged.\n\t\t *\n\t\t * @param {object} row The row data\n\t\t * @param {string} key The column key (supports dot notation: 'address.city')\n\t\t * @return {unknown} The cell value\n\t\t */\n\t\tgetCellValue(row, key) {\n\t\t\tif (typeof key !== 'string') {\n\t\t\t\treturn undefined\n\t\t\t}\n\t\t\tif (key.includes('.')) {\n\t\t\t\treturn key.split('.').reduce((obj, k) => obj?.[k], row)\n\t\t\t}\n\t\t\tif (row?.[key] === undefined && row?.['@self'] && typeof row['@self'] === 'object') {\n\t\t\t\treturn row['@self'][key]\n\t\t\t}\n\t\t\treturn row?.[key]\n\t\t},\n\n\t\t/**\n\t\t * Get the schema property definition for a column key, or `{}` when\n\t\t * there is no schema (manual mode) or no matching property. The result\n\t\t * is handed to `CnCellRenderer` for type-aware rendering; an empty\n\t\t * object makes it fall back to `formatValue()` (plain truncated text).\n\t\t *\n\t\t * @param {string} key Column key\n\t\t * @return {object} Property definition (possibly empty).\n\t\t */\n\t\tgetSchemaProperty(key) {\n\t\t\treturn this.schema?.properties?.[key] || {}\n\t\t},\n\n\t\t/**\n\t\t * Effective property definition handed to CnCellRenderer for a column:\n\t\t * the schema property augmented with the column's own `type`/`format`/\n\t\t * `enum`/`enumLabels` hints. Lets synthesized columns that have no\n\t\t * schema property (e.g. metadata fields with `format: 'date-time'` /\n\t\t * `'uri'`, or a dotted path into a reference this table was never\n\t\t * handed a `:schema` for at all) still get type-aware rendering.\n\t\t * `enumLabels` rides with `enum` rather than getting its own `if`\n\t\t * branch: a column can't usefully declare labels without also\n\t\t * declaring the codes they label.\n\t\t *\n\t\t * @param {object} col Column definition.\n\t\t * @return {object} Property definition for CnCellRenderer.\n\t\t */\n\t\tcolumnProperty(col) {\n\t\t\tconst base = this.getSchemaProperty(col.key)\n\t\t\tif (col && (col.format || col.type || col.enum)) {\n\t\t\t\treturn {\n\t\t\t\t\t...base,\n\t\t\t\t\t...(col.type ? { type: col.type } : {}),\n\t\t\t\t\t...(col.format ? { format: col.format } : {}),\n\t\t\t\t\t...(col.enum ? { enum: col.enum, enumLabels: col.enumLabels || base.enumLabels } : {}),\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn base\n\t\t},\n\n\t\t/**\n\t\t * Declarative cell-format spec handed to CnCellRenderer's `format` prop\n\t\t * (an object: `{ style, currency, decimals, ... }`). `col.format` is\n\t\t * overloaded — for schema-derived columns it is the schema format STRING\n\t\t * (`'date'`, `'uri'`, ...), which is NOT a declarative spec and drives\n\t\t * type-aware rendering via `columnProperty()` instead. Only forward an\n\t\t * object here, so a string schema-format no longer trips CnCellRenderer's\n\t\t * `Object`-typed `format` prop (Vue prop-type warning).\n\t\t *\n\t\t * @param {object} col Column definition.\n\t\t * @return {object|null} The declarative format spec, or null.\n\t\t */\n\t\tcolumnFormat(col) {\n\t\t\treturn (col && typeof col.format === 'object') ? col.format : null\n\t\t},\n\n\t\t/**\n\t\t * Value to render in a cell. For a column that declares `aggregate`\n\t\t * (a count of related objects), returns the cached count once\n\t\t * `loadAggregates()` has resolved it (or `'…'` while pending, `'—'`\n\t\t * if it failed / there's nothing to count). Otherwise the row's\n\t\t * property value via `getCellValue`.\n\t\t *\n\t\t * @param {object} row The row data.\n\t\t * @param {object} col The column definition.\n\t\t * @return {unknown} The value handed to the slot / CnCellRenderer.\n\t\t */\n\t\tcellValue(row, col) {\n\t\t\tif (col && col.aggregate) {\n\t\t\t\tconst cached = this.aggregateValues[String(row[this.rowKey])]\n\t\t\t\tconst v = cached ? cached[col.key] : undefined\n\t\t\t\tif (v === undefined) {\n\t\t\t\t\treturn this.aggregateLoading ? '…' : '—'\n\t\t\t\t}\n\t\t\t\treturn v\n\t\t\t}\n\t\t\treturn this.getCellValue(row, col.key)\n\t\t},\n\n\t\t/**\n\t\t * Interpolate a column's `aggregate.where` map for one row: any string\n\t\t * value of the form `\"@self.<path>\"` is replaced with\n\t\t * `getCellValue(row, path)`; everything else is passed through.\n\t\t *\n\t\t * @param {object} where The `aggregate.where` map (may be undefined).\n\t\t * @param {object} row The parent row.\n\t\t * @return {object} The resolved filter map.\n\t\t */\n\t\tresolveAggregateWhere(where, row) {\n\t\t\tconst out = {}\n\t\t\tfor (const [k, v] of Object.entries(where || {})) {\n\t\t\t\tif (typeof v === 'string' && v.startsWith('@self.')) {\n\t\t\t\t\tout[k] = this.getCellValue(row, v.slice('@self.'.length))\n\t\t\t\t} else {\n\t\t\t\t\tout[k] = v\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn out\n\t\t},\n\n\t\t/**\n\t\t * For every column that declares `aggregate` (currently `op: \"count\"`),\n\t\t * issue one `_limit=0` count request per visible row against the related\n\t\t * OpenRegister collection and cache the totals in `aggregateValues`.\n\t\t * Batched with `Promise.all`; a per-request failure degrades that one\n\t\t * cell to `'—'` (logged), never the page. A monotonic request id\n\t\t * discards a stale batch when `rows` / columns change mid-flight.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync loadAggregates() {\n\t\t\tconst aggCols = this.effectiveColumns.filter((c) => c && c.aggregate && c.aggregate.op === 'count')\n\t\t\tif (aggCols.length === 0) {\n\t\t\t\tif (Object.keys(this.aggregateValues).length > 0) {\n\t\t\t\t\tthis.aggregateValues = {}\n\t\t\t\t}\n\t\t\t\tthis.aggregateLoading = false\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst id = ++this.aggregateRequestId\n\t\t\tthis.aggregateLoading = true\n\t\t\tconst next = {}\n\t\t\tconst jobs = []\n\t\t\tfor (const row of this.rows) {\n\t\t\t\tconst rowKey = String(row[this.rowKey])\n\t\t\t\tnext[rowKey] = {}\n\t\t\t\tfor (const col of aggCols) {\n\t\t\t\t\tconst agg = col.aggregate\n\t\t\t\t\tif (!agg.register || !agg.schema) {\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\t\t\t\t\tconst where = this.resolveAggregateWhere(agg.where, row)\n\t\t\t\t\tjobs.push(axios.get(generateUrl(`/apps/openregister/api/objects/${agg.register}/${agg.schema}`), {\n\t\t\t\t\t\tparams: { ...where, _limit: 0 },\n\t\t\t\t\t})\n\t\t\t\t\t\t.then((res) => {\n\t\t\t\t\t\t\tconst d = res && res.data\n\t\t\t\t\t\t\tnext[rowKey][col.key] = (d && (d.total ?? (Array.isArray(d.results) ? d.results.length : undefined))) ?? 0\n\t\t\t\t\t\t})\n\t\t\t\t\t\t.catch((e) => {\n\t\t\t\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\t\t\t\tconsole.warn(`[CnDataTable] aggregate \"${col.key}\" count failed for row ${rowKey}`, e)\n\t\t\t\t\t\t\tnext[rowKey][col.key] = undefined\n\t\t\t\t\t\t}))\n\t\t\t\t}\n\t\t\t}\n\t\t\tawait Promise.all(jobs)\n\t\t\tif (id !== this.aggregateRequestId) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.aggregateValues = next\n\t\t\tthis.aggregateLoading = false\n\t\t},\n\n\t\tisSelected(row) {\n\t\t\treturn this.selectedIds.includes(row[this.rowKey])\n\t\t},\n\n\t\t/**\n\t\t * Row-body click: toggles selection when `selectable` (ignoring drags),\n\t\t * otherwise emits `row-click` for navigation.\n\t\t *\n\t\t * @param {object} row The clicked row object\n\t\t * @param {MouseEvent} [event] The originating click event.\n\t\t */\n\t\tonRowClick(row, event) {\n\t\t\tif (this.wasDrag(event)) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (this.selectable && !this.rowClickToView) {\n\t\t\t\tthis.toggleSelect(row)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.emitRowClick(row, event)\n\t\t},\n\n\t\t/**\n\t\t * Row-body middle click: emits `row-click` like a click, so the host\n\t\t * (or `rowClickRoute`) can open the row in a new tab. Ignored for other\n\t\t * buttons, nested controls, drags and select-on-click tables.\n\t\t *\n\t\t * @param {object} row The clicked row object\n\t\t * @param {MouseEvent} event The originating auxclick event.\n\t\t */\n\t\tonRowAuxClick(row, event) {\n\t\t\tif (!isRowMiddleClick(event) || this.wasDrag(event)) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (this.selectable && !this.rowClickToView) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.emitRowClick(row, event)\n\t\t},\n\n\t\t/**\n\t\t * Emit `row-click` and follow `rowClickRoute` when set.\n\t\t *\n\t\t * @param {object} row The clicked row object\n\t\t * @param {MouseEvent} [event] The originating click or auxclick event.\n\t\t */\n\t\temitRowClick(row, event) {\n\t\t\t/**\n\t\t\t * @event row-click Emitted on a row-body click for navigation, and on a middle click (auxclick). Fires when `selectable` is false, OR when `rowClickToView` is set (selection then happens via the checkbox column). Payload: `(row, event)` — the clicked row object and the native click/auxclick event, so a host can open the row in a new tab on a ctrl/cmd/shift or middle click (see `openRowTarget`).\n\t\t\t * @type {object} The clicked row object.\n\t\t\t */\n\t\t\tthis.$emit('row-click', row, event)\n\t\t\t// A ctrl/cmd/shift or middle click opens the route in a new tab,\n\t\t\t// unless a row-click listener already did.\n\t\t\tif (this.rowClickRoute && this.$router && !isNewTabHandled(event)) {\n\t\t\t\tconst route = this.rowClickRoute(row)\n\t\t\t\tif (route) {\n\t\t\t\t\tmarkNewTabHandled(event, openRowTarget(event, typeof route === 'string' ? { path: route } : route, this.$router))\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Row right-click: forward the row + originating event so a host can\n\t\t * open a context menu (the browser default is prevented).\n\t\t *\n\t\t * @param {object} row The right-clicked row object.\n\t\t * @param {MouseEvent} event The originating contextmenu event.\n\t\t */\n\t\tonRowContextMenu(row, event) {\n\t\t\t/**\n\t\t\t * @event row-context-menu Emitted on a row right-click (contextmenu) for hosts that render a context menu.\n\t\t\t * @type {{ row: object, event: MouseEvent }}\n\t\t\t */\n\t\t\tthis.$emit('row-context-menu', { row, event })\n\t\t},\n\n\t\t/**\n\t\t * Index of `key` within `effectiveSortKeys`, or -1 when not active.\n\t\t * Used by the template for the arrow, the numbered badge, and aria-sort.\n\t\t *\n\t\t * @param {string} key Column key.\n\t\t * @return {number}\n\t\t */\n\t\tsortKeyIndex(key) {\n\t\t\treturn this.effectiveSortKeys.findIndex((k) => k && k.key === key)\n\t\t},\n\n\t\t/**\n\t\t * `aria-sort` value for a column header: `'ascending'`/`'descending'`\n\t\t * for the PRIMARY (index 0) active sort key only — secondary/tertiary\n\t\t * keys carry the visible numbered badge instead, per WCAG guidance\n\t\t * that `aria-sort` describes single-column sort state. `null` omits\n\t\t * the attribute entirely (unsorted / not sortable).\n\t\t *\n\t\t * @param {object} col Column definition.\n\t\t * @return {string|null}\n\t\t */\n\t\tariaSortFor(col) {\n\t\t\tif (!col.sortable) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst primary = this.effectiveSortKeys[0]\n\t\t\tif (!primary || primary.key !== col.key) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\treturn primary.order === 'asc' ? 'ascending' : 'descending'\n\t\t},\n\n\t\t/**\n\t\t * Header click: plain click = single-sort (existing behavior,\n\t\t * unchanged); shift+click appends/cycles the column as a secondary or\n\t\t * tertiary sort key. See `src/utils/multiColumnSort.js`.\n\t\t *\n\t\t * @param {string} key Column key.\n\t\t * @param {MouseEvent} [event] The originating click event.\n\t\t */\n\t\tonHeaderClick(key, event) {\n\t\t\tthis.applySort(key, !!(event && event.shiftKey))\n\t\t},\n\n\t\t/**\n\t\t * Header keydown: `Enter` = plain click, `Shift+Enter` = shift+click.\n\t\t *\n\t\t * @param {string} key Column key.\n\t\t * @param {KeyboardEvent} event The originating keydown event.\n\t\t */\n\t\tonHeaderKeydown(key, event) {\n\t\t\tif (event.key !== 'Enter') {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tevent.preventDefault()\n\t\t\tthis.applySort(key, !!event.shiftKey)\n\t\t},\n\n\t\t/**\n\t\t * Compute and emit the next sort state for a header interaction.\n\t\t *\n\t\t * @param {string} key Column key.\n\t\t * @param {boolean} append `true` for a shift+click/shift+Enter (append/cycle a secondary key).\n\t\t */\n\t\tapplySort(key, append) {\n\t\t\tconst keys = nextSortState(this.effectiveSortKeys, key, { append })\n\t\t\tconst primary = keys[0] || null\n\t\t\t/**\n\t\t\t * @event sort Emitted when a sortable column header is clicked (plain click) or shift-clicked (multi-sort).\n\t\t\t * @type {{ key: string|null, order: 'asc'|'desc'|null, keys: Array<{key: string, order: 'asc'|'desc'}> }}\n\t\t\t * `key`/`order` mirror the PRIMARY (first) active sort key exactly as the pre-multi-sort single-key\n\t\t\t * contract did (`null`/`null` when cleared) — existing listeners destructuring `{ key, order }` are\n\t\t\t * unaffected. `keys` is new: the full ordered list (0 to 3 entries) for multi-sort-aware hosts.\n\t\t\t */\n\t\t\tthis.$emit('sort', {\n\t\t\t\tkey: primary ? primary.key : null,\n\t\t\t\torder: primary ? primary.order : null,\n\t\t\t\tkeys,\n\t\t\t})\n\t\t},\n\n\t\ttoggleSelect(row) {\n\t\t\tconst id = row[this.rowKey]\n\t\t\tconst newIds = this.isSelected(row)\n\t\t\t\t? this.selectedIds.filter((i) => i !== id)\n\t\t\t\t: [...this.selectedIds, id]\n\t\t\t/** @event select Emitted when row selection changes. Payload: array of selected IDs. */\n\t\t\tthis.$emit('select', newIds)\n\t\t},\n\n\t\ttoggleSelectAll() {\n\t\t\tif (this.allSelected) {\n\t\t\t\t// Remove only current page IDs, preserving cross-page selections\n\t\t\t\tconst currentPageIds = new Set(this.rows.map((row) => row[this.rowKey]))\n\t\t\t\tthis.$emit('select', this.selectedIds.filter((id) => !currentPageIds.has(id)))\n\t\t\t} else {\n\t\t\t\t// Add current page IDs to existing selections\n\t\t\t\tconst merged = new Set([...this.selectedIds, ...this.rows.map((row) => row[this.rowKey])])\n\t\t\t\tthis.$emit('select', [...merged])\n\t\t\t}\n\t\t\t/** @event select-all Emitted when select-all checkbox is toggled. */\n\t\t\tthis.$emit('select-all', !this.allSelected)\n\t\t},\n\t},\n}\n</script>\n"],"names":["_createElementBlock","_openBlock","_createElementVNode","_toDisplayString","_createVNode","_normalizeClass","_Fragment","_renderList","_withKeys","_renderSlot","_createTextVNode","_withModifiers","_createBlock"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"CnDataTable.vue3.js","sources":["../../../../src/components/CnDataTable/CnDataTable.vue?vue&type=template&id=54ce98fa&lang.js"],"sourcesContent":["<template>\n\t<div\n\t\tclass=\"cn-table-container\"\n\t\tdata-testid=\"cn-object-list\"\n\t\t:class=\"{\n\t\t\t'cn-table-container--scrollable': scrollable,\n\t\t\t'cn-table-container--borderless': borderless,\n\t\t\t'cn-table-container--fill': fillHeight,\n\t\t}\">\n\t\t<!-- Optional card header (folded from the retired CnTableWidget): a title\n\t\t + total-count badge. Only rendered when `title` is set; bare table\n\t\t usage is unchanged. -->\n\t\t<div v-if=\"title\" class=\"cn-data-table__header\">\n\t\t\t<h3 class=\"cn-data-table__title\">\n\t\t\t\t{{ title }}\n\t\t\t</h3>\n\t\t\t<span v-if=\"totalRowCount > 0\" class=\"cn-data-table__count\">\n\t\t\t\t{{ totalRowCount }}\n\t\t\t</span>\n\t\t</div>\n\n\t\t<!-- Loading State -->\n\t\t<div v-if=\"isLoading\" class=\"cn-table-loading\" data-testid=\"cn-object-list-loading\">\n\t\t\t<!-- Decorative: the adjacent <p> already carries the accessible\n\t\t\t name (loadingText), so the spinner is hidden from the\n\t\t\t accessibility tree rather than exposed as an unlabelled\n\t\t\t role=\"img\" (WCAG 1.1.1 / axe \"role-img-alt\"). -->\n\t\t\t<NcLoadingIcon :size=\"32\" aria-hidden=\"true\" />\n\t\t\t<p>{{ loadingText }}</p>\n\t\t</div>\n\n\t\t<!-- Table. The horizontal scroll lives on this wrapper, NOT on\n\t\t `.cn-table-container`: `overflow-x: auto` on the container coerces\n\t\t its `overflow-y` from `visible` to `auto` (CSS forbids mixing\n\t\t `visible` with a non-visible value), which silently made the\n\t\t container the nearest scrollport. The sticky footer then anchored\n\t\t to a container that never scrolls, so \"View all\" scrolled away with\n\t\t the rows instead of pinning to the bottom of the enclosing widget. -->\n\t\t<!-- The horizontal scrollport must be reachable by keyboard: a region\n\t\t that scrolls but cannot take focus gives a keyboard-only user no\n\t\t way to reach the columns past the fold (axe\n\t\t `scrollable-region-focusable`, serious; WCAG 2.1.1).\n\n\t\t Gated on ACTUAL overflow rather than applied unconditionally, for\n\t\t two reasons. A table that fits needs no scrolling, so a permanent\n\t\t tab stop would put every table in the fleet on the keyboard path\n\t\t for nothing. And the axe rule itself only applies to elements that\n\t\t really are scrollable, so the conditional attribute is present in\n\t\t exactly the cases the rule evaluates.\n\n\t\t `role=\"group\"`, NOT `role=\"region\"`: this element is nested inside\n\t\t `.cn-widget-wrapper__content[role=\"region\"]` on the dashboard-widget\n\t\t path, and a second landmark there would surface as a duplicate.\n\t\t `group` is nameable but is not a landmark. A name is required\n\t\t rather than optional — `aria-label` is PROHIBITED on a role-less\n\t\t generic element, so a bare `tabindex` div would trade this\n\t\t violation for `aria-prohibited-attr` plus an unlabelled mystery\n\t\t stop in the tab order. -->\n\t\t<div\n\t\t\tv-else\n\t\t\tref=\"scrollEl\"\n\t\t\tclass=\"cn-data-table__scroll\"\n\t\t\t:tabindex=\"isScrollable ? 0 : undefined\"\n\t\t\t:role=\"isScrollable ? 'group' : undefined\"\n\t\t\t:aria-label=\"isScrollable ? scrollRegionLabel : undefined\">\n\t\t\t<table\n\t\t\t\tclass=\"cn-data-table\"\n\t\t\t\t:class=\"{ 'cn-data-table--fixed': fixedLayout }\"\n\t\t\t\tdata-testid=\"cn-object-list-table\">\n\t\t\t\t<thead v-if=\"!hideHeader\">\n\t\t\t\t\t<tr>\n\t\t\t\t\t\t<!-- Checkbox column -->\n\t\t\t\t\t\t<th v-if=\"selectable\" class=\"cn-table-col--checkbox\">\n\t\t\t\t\t\t\t<NcCheckboxRadioSwitch\n\t\t\t\t\t\t\t\t:modelValue=\"allSelected\"\n\t\t\t\t\t\t\t\t:indeterminate=\"someSelected && !allSelected\"\n\t\t\t\t\t\t\t\t:aria-label=\"selectAllLabel\"\n\t\t\t\t\t\t\t\t@update:modelValue=\"toggleSelectAll\" />\n\t\t\t\t\t\t</th>\n\n\t\t\t\t\t\t<!-- Leading icon column (header is intentionally blank) -->\n\t\t\t\t\t\t<th v-if=\"rowIcon\" class=\"cn-table-col--icon\" />\n\n\t\t\t\t\t\t<!-- Data columns -->\n\t\t\t\t\t\t<th\n\t\t\t\t\t\t\tv-for=\"col in effectiveColumns\"\n\t\t\t\t\t\t\t:key=\"col.key\"\n\t\t\t\t\t\t\t:class=\"[\n\t\t\t\t\t\t\t\tcol.sortable ? 'cn-table-header--sortable' : '',\n\t\t\t\t\t\t\t\tcol.class || '',\n\t\t\t\t\t\t\t]\"\n\t\t\t\t\t\t\t:style=\"col.width ? { width: col.width } : {}\"\n\t\t\t\t\t\t\t:tabindex=\"col.sortable ? 0 : null\"\n\t\t\t\t\t\t\t:aria-sort=\"ariaSortFor(col)\"\n\t\t\t\t\t\t\t:title=\"translateLabel(col.description) || null\"\n\t\t\t\t\t\t\t@click=\"col.sortable ? onHeaderClick(col.key, $event) : null\"\n\t\t\t\t\t\t\t@keydown.enter=\"col.sortable ? onHeaderKeydown(col.key, $event) : null\">\n\t\t\t\t\t\t\t<span :class=\"col.description ? 'cn-table-header--described' : ''\">\n\t\t\t\t\t\t\t\t{{ translateLabel(col.label) }}\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\tv-if=\"col.sortable && sortKeyIndex(col.key) !== -1\"\n\t\t\t\t\t\t\t\tclass=\"cn-table-sort-indicator\">\n\t\t\t\t\t\t\t\t{{ effectiveSortKeys[sortKeyIndex(col.key)].order === 'asc' ? '▲' : '▼' }}\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\tv-if=\"col.sortable && effectiveSortKeys.length > 1 && sortKeyIndex(col.key) !== -1\"\n\t\t\t\t\t\t\t\tclass=\"cn-table-sort-badge\">\n\t\t\t\t\t\t\t\t{{ sortKeyIndex(col.key) + 1 }}\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t</th>\n\n\t\t\t\t\t\t<!-- Actions column -->\n\t\t\t\t\t\t<th v-if=\"$slots['row-actions']\" class=\"cn-table-col--actions\">\n\t\t\t\t\t\t\t<!-- @slot Header cell content above the row-actions column (blank by default). -->\n\t\t\t\t\t\t\t<slot name=\"actions-header\" />\n\t\t\t\t\t\t</th>\n\t\t\t\t\t</tr>\n\t\t\t\t</thead>\n\n\t\t\t\t<tbody>\n\t\t\t\t\t<!-- Empty state -->\n\t\t\t\t\t<tr v-if=\"effectiveRows.length === 0\" class=\"cn-table-empty\" data-testid=\"cn-object-list-empty\">\n\t\t\t\t\t\t<td :colspan=\"totalColumns\">\n\t\t\t\t\t\t\t<!-- @slot Empty-state content shown when there are no rows (defaults to `emptyText`). -->\n\t\t\t\t\t\t\t<slot name=\"empty\">\n\t\t\t\t\t\t\t\t{{ translateLabel(emptyText) }}\n\t\t\t\t\t\t\t</slot>\n\t\t\t\t\t\t</td>\n\t\t\t\t\t</tr>\n\n\t\t\t\t\t<!-- Data rows -->\n\t\t\t\t\t<tr\n\t\t\t\t\t\tv-for=\"row in effectiveRows\"\n\t\t\t\t\t\tv-else\n\t\t\t\t\t\t:key=\"row[rowKey]\"\n\t\t\t\t\t\tclass=\"cn-table-row\"\n\t\t\t\t\t\tdata-testid=\"cn-object-row\"\n\t\t\t\t\t\t:data-testid-row-id=\"row[rowKey]\"\n\t\t\t\t\t\t:class=\"[\n\t\t\t\t\t\t\tisSelected(row) ? 'cn-table-row--selected' : '',\n\t\t\t\t\t\t\trowLinks[String(row[rowKey])] ? 'cn-table-row--linked' : '',\n\t\t\t\t\t\t\trowClass ? rowClass(row) : '',\n\t\t\t\t\t\t]\"\n\t\t\t\t\t\t@mousedown=\"onRowMouseDown\"\n\t\t\t\t\t\t@click=\"onRowClick(row, $event)\"\n\t\t\t\t\t\t@auxclick=\"onRowAuxClick(row, $event)\"\n\t\t\t\t\t\t@contextmenu=\"onRowContextMenu(row, $event)\">\n\t\t\t\t\t\t<!-- Checkbox -->\n\t\t\t\t\t\t<td v-if=\"selectable\"\n\t\t\t\t\t\t\tclass=\"cn-table-col--checkbox\"\n\t\t\t\t\t\t\t@click.stop\n\t\t\t\t\t\t\t@auxclick.stop>\n\t\t\t\t\t\t\t<NcCheckboxRadioSwitch\n\t\t\t\t\t\t\t\t:modelValue=\"isSelected(row)\"\n\t\t\t\t\t\t\t\t:aria-label=\"selectRowLabel\"\n\t\t\t\t\t\t\t\t@update:modelValue=\"toggleSelect(row)\" />\n\t\t\t\t\t\t</td>\n\n\t\t\t\t\t\t<!-- Leading icon -->\n\t\t\t\t\t\t<td v-if=\"rowIcon\" class=\"cn-table-col--icon\">\n\t\t\t\t\t\t\t<CnIcon :name=\"getRowIcon(row)\" :size=\"20\" />\n\t\t\t\t\t\t</td>\n\n\t\t\t\t\t\t<!-- Data cells -->\n\t\t\t\t\t\t<td\n\t\t\t\t\t\t\tv-for=\"(col, colIndex) in effectiveColumns\"\n\t\t\t\t\t\t\t:key=\"col.key\"\n\t\t\t\t\t\t\t:class=\"[col.class || '', col.cellClass || '', cellClass ? cellClass(row, col) : '']\"\n\t\t\t\t\t\t\t:style=\"col.width ? { maxWidth: col.width } : {}\"\n\t\t\t\t\t\t\t@mouseenter=\"titleWhenClipped\">\n\t\t\t\t\t\t\t<!-- A row with a `rowClickRoute` is a real link: this anchor\n\t\t\t\t\t\t\t is stretched over the whole row by CSS, so hovering shows\n\t\t\t\t\t\t\t the URL, a middle or ctrl click opens a new tab natively,\n\t\t\t\t\t\t\t and the row is reachable with Tab. The row's own click\n\t\t\t\t\t\t\t handlers treat it as a nested control and leave it be. -->\n\t\t\t\t\t\t\t<a v-if=\"colIndex === 0 && rowLinks[String(row[rowKey])]\"\n\t\t\t\t\t\t\t\tclass=\"cn-table-row__link\"\n\t\t\t\t\t\t\t\t:href=\"rowLinks[String(row[rowKey])].href\"\n\t\t\t\t\t\t\t\t:aria-label=\"rowLinkLabel(row, col)\"\n\t\t\t\t\t\t\t\tdraggable=\"false\"\n\t\t\t\t\t\t\t\tdata-testid=\"cn-row-link\"\n\t\t\t\t\t\t\t\t@click=\"onRowLinkClick(row, $event)\" />\n\t\t\t\t\t\t\t<!-- The padlock rides the FIRST data cell, beside whatever\n\t\t\t\t\t\t\t names the row. Deliberately OUTSIDE the #column-<key>\n\t\t\t\t\t\t\t slot: a consumer overriding that column's rendering is\n\t\t\t\t\t\t\t customising their own data, not opting out of being told\n\t\t\t\t\t\t\t the record is locked. It renders nothing when unlocked,\n\t\t\t\t\t\t\t so an unlocked table is byte-for-byte what it was. -->\n\t\t\t\t\t\t\t<CnLockIndicator\n\t\t\t\t\t\t\t\tv-if=\"colIndex === 0\"\n\t\t\t\t\t\t\t\t:object=\"row\"\n\t\t\t\t\t\t\t\t:size=\"16\" />\n\t\t\t\t\t\t\t<!-- Declared state indicators ride the first data cell\n\t\t\t\t\t\t\t beside the padlock. Each one renders as an icon WITH a\n\t\t\t\t\t\t\t text alternative and a tooltip, never as colour alone,\n\t\t\t\t\t\t\t and an indicator the page did not declare cannot appear\n\t\t\t\t\t\t\t however the record is shaped. Past the cap the rest move\n\t\t\t\t\t\t\t into the row menu, because a row of nine icons is not a\n\t\t\t\t\t\t\t row anyone can triage at a glance. -->\n\t\t\t\t\t\t\t<span v-if=\"colIndex === 0 && indicatorsFor(row).shown.length > 0\"\n\t\t\t\t\t\t\t\tclass=\"cn-table-row-indicators\"\n\t\t\t\t\t\t\t\tdata-testid=\"cn-row-indicators\">\n\t\t\t\t\t\t\t\t<span v-for=\"indicator in indicatorsFor(row).shown\"\n\t\t\t\t\t\t\t\t\t:key=\"indicator.id || indicator.field\"\n\t\t\t\t\t\t\t\t\tclass=\"cn-table-row-indicator\"\n\t\t\t\t\t\t\t\t\t:title=\"indicator.tooltip || indicator.text\"\n\t\t\t\t\t\t\t\t\t:data-testid=\"`cn-row-indicator-${indicator.id || indicator.field}`\">\n\t\t\t\t\t\t\t\t\t<CnIcon :name=\"indicator.icon || 'InformationOutline'\" :size=\"16\" />\n\t\t\t\t\t\t\t\t\t<span class=\"hidden-visually\">{{ indicator.text }}</span>\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t<!-- @slot Per-column cell override (`#column-<key>`), scoped with { row, value }. Wins over CnCellRenderer. -->\n\t\t\t\t\t\t\t<slot :name=\"'column-' + col.key\" :row=\"row\" :value=\"cellValue(row, col)\">\n\t\t\t\t\t\t\t\t<!-- Every column renders through CnCellRenderer: it resolves\n\t\t\t\t\t\t\t col.formatter / col.widget against the injected registries\n\t\t\t\t\t\t\t (cnFormatters / cnCellWidgets), uses the schema property when\n\t\t\t\t\t\t\t one is available (else {}) for type-aware rendering, and\n\t\t\t\t\t\t\t falls back to formatValue(). Columns with `aggregate` get a\n\t\t\t\t\t\t\t count of related objects (see cellValue/loadAggregates). The\n\t\t\t\t\t\t\t #column-{key} slot still wins. -->\n\t\t\t\t\t\t\t\t<CnCellRenderer\n\t\t\t\t\t\t\t\t\t:value=\"cellValue(row, col)\"\n\t\t\t\t\t\t\t\t\t:property=\"columnProperty(col)\"\n\t\t\t\t\t\t\t\t\t:formatter=\"col.formatter || null\"\n\t\t\t\t\t\t\t\t\t:formatterOptions=\"col.formatterOptions || null\"\n\t\t\t\t\t\t\t\t\t:widget=\"col.widget || null\"\n\t\t\t\t\t\t\t\t\t:widgetProps=\"col.widgetProps || undefined\"\n\t\t\t\t\t\t\t\t\t:format=\"columnFormat(col)\"\n\t\t\t\t\t\t\t\t\t:row=\"row\"\n\t\t\t\t\t\t\t\t\t:rowKey=\"rowKey\" />\n\t\t\t\t\t\t\t</slot>\n\t\t\t\t\t\t</td>\n\n\t\t\t\t\t\t<!-- Row actions -->\n\t\t\t\t\t\t<td v-if=\"$slots['row-actions']\"\n\t\t\t\t\t\t\tclass=\"cn-table-col--actions\"\n\t\t\t\t\t\t\t:class=\"[cellClass ? cellClass(row, { key: 'actions' }) : '']\"\n\t\t\t\t\t\t\t@click.stop\n\t\t\t\t\t\t\t@auxclick.stop>\n\t\t\t\t\t\t\t<!-- @slot Per-row actions menu (e.g. a CnRowActions), scoped with { row }. Supplying it adds the trailing actions column. -->\n\t\t\t\t\t\t\t<slot name=\"row-actions\" :row=\"row\" />\n\t\t\t\t\t\t</td>\n\t\t\t\t\t</tr>\n\t\t\t\t</tbody>\n\t\t\t</table>\n\t\t</div>\n\n\t\t<!-- Optional footer. A `#footer` scoped slot lets a host render its own\n\t\t footer link (e.g. a \"+ New\" create action or an always-shown\n\t\t \"View all\") with its own click handler — useful when the widget runs\n\t\t outside a vue-router context (the built-in link uses $router). When\n\t\t no slot is given, the built-in \"View all\" control (folded from the\n\t\t retired CnTableWidget) is shown for a `limit`-ed subset.\n\n\t\t The control is a real link (with an href) when the router resolves\n\t\t `viewAllRoute` to a URL, and a real button otherwise. An `<a>`\n\t\t without an href has no link role and is not keyboard focusable\n\t\t (WCAG 2.1.1, 4.1.2), so it is never rendered. -->\n\t\t<div\n\t\t\tv-if=\"$slots.footer || (viewAllRoute && totalRowCount > effectiveRows.length)\"\n\t\t\tclass=\"cn-data-table__footer\">\n\t\t\t<!-- @slot Custom footer content, scoped with { total, shown } (defaults to the built-in \"View all\" link). -->\n\t\t\t<slot name=\"footer\" :total=\"totalRowCount\" :shown=\"effectiveRows.length\">\n\t\t\t\t<a\n\t\t\t\t\tv-if=\"viewAllHref\"\n\t\t\t\t\tclass=\"cn-data-table__view-all\"\n\t\t\t\t\t:href=\"viewAllHref\"\n\t\t\t\t\t@click=\"onViewAll\">\n\t\t\t\t\t{{ viewAllLabel }}\n\t\t\t\t</a>\n\t\t\t\t<button\n\t\t\t\t\tv-else\n\t\t\t\t\ttype=\"button\"\n\t\t\t\t\tclass=\"cn-data-table__view-all\"\n\t\t\t\t\t@click=\"onViewAll\">\n\t\t\t\t\t{{ viewAllLabel }}\n\t\t\t\t</button>\n\t\t\t</slot>\n\t\t</div>\n\t</div>\n</template>\n\n<script>\nimport axios from '@nextcloud/axios'\nimport { translate as t } from '@nextcloud/l10n'\nimport { generateUrl } from '@nextcloud/router'\nimport { NcCheckboxRadioSwitch, NcLoadingIcon } from '@nextcloud/vue'\nimport { useClickDragGuard } from '../../composables/useClickDragGuard.js'\nimport { followLinkClick, openRowTarget, resolveHref } from '../../utils/linkNavigation.js'\nimport { nextSortState } from '../../utils/multiColumnSort.js'\nimport { isNewTabClick, isNewTabHandled, isRowMiddleClick, markNewTabHandled, preventMiddleClickAutoscroll } from '../../utils/rowAuxClick.js'\nimport { DEFAULT_ROW_INDICATOR_CAP, resolveRowIndicators } from '../../utils/rowIndicators.js'\nimport { columnsFromSchema } from '../../utils/schema.js'\nimport { CnCellRenderer } from '../CnCellRenderer/index.js'\nimport { CnIcon } from '../CnIcon/index.js'\nimport { CnLockIndicator } from '../CnLockIndicator/index.js'\n\n// CnDataTable has no scoped styles of its own — its entire look lives in the\n// shared table stylesheet. Import it here so the table is styled even when the\n// consuming app does not pull in the library's global css/index.css.\nimport '../../css/table.css'\n\n/**\n * CnDataTable — Generic sortable data table for list views.\n *\n * Replaces the copy-pasted `<table class=\"viewTable\">` HTML pattern found in\n * every list view across OpenRegister, Pipelinq, and Dossiq. Supports sorting,\n * row selection, custom cell rendering via scoped slots, loading states,\n * and empty states.\n *\n * Sorting: a plain click on a sortable header is single-sort (cycle asc →\n * desc → cleared), unchanged from before. Shift+click (or Shift+Enter on a\n * focused header) appends the column as a secondary/tertiary sort key —\n * capped at 3 — with numbered priority badges (1, 2, 3) once more than one\n * key is active. Pass `sortKeys: [{key, order}, ...]` for multi-sort (falls\n * back to the legacy `sortKey`/`sortOrder` props when empty). The `sort`\n * event payload is extended, not replaced: `{key, order}` still mirrors the\n * primary key exactly as before; a new `keys` field carries the full\n * ordered list. See `src/utils/multiColumnSort.js` for the state machine.\n *\n * When a `schema` prop is provided, columns are auto-generated from schema\n * properties and cells render through CnCellRenderer for type-aware formatting\n * (dates, booleans, UUIDs, enums, etc.). Scoped slots still override individual\n * columns when needed.\n *\n * Manual columns (backwards compatible)\n * ```vue\n * <CnDataTable\n * :columns=\"[\n * { key: 'name', label: 'Name', sortable: true },\n * { key: 'email', label: 'Email' },\n * ]\"\n * :rows=\"clients\"\n * @row-click=\"openClient\" />\n * ```\n *\n * Schema-driven (auto columns)\n * ```vue\n * <CnDataTable :schema=\"schema\" :rows=\"objects\" />\n * ```\n *\n * Schema with overrides and custom cell\n * ```vue\n * <CnDataTable\n * :schema=\"schema\"\n * :exclude-columns=\"['description']\"\n * :column-overrides=\"{ status: { width: '200px' } }\"\n * :rows=\"objects\">\n * <template #column-status=\"{ row, value }\">\n * <QuickStatusDropdown :case-obj=\"row\" />\n * </template>\n * </CnDataTable>\n * ```\n */\nexport default {\n\tname: 'CnDataTable',\n\n\tcomponents: {\n\t\tNcLoadingIcon,\n\t\tNcCheckboxRadioSwitch,\n\t\tCnCellRenderer,\n\t\tCnIcon,\n\t\tCnLockIndicator,\n\t},\n\n\tinject: {\n\t\t/**\n\t\t * Consumer translation function, provided by CnAppRoot as\n\t\t * `cnTranslate: this.translate` (bound to the host app's id, e.g.\n\t\t * `t.bind(null, 'docudesk')`). Column headers come from schema\n\t\t * property titles, which are authored in English as the canonical\n\t\t * source (API predictability); the visible label is resolved through\n\t\t * this function so it follows the user's language, with the English\n\t\t * source key living in each app's l10n files. Defaults to identity\n\t\t * when the table is used standalone (no CnAppRoot ancestor).\n\t\t */\n\t\tcnTranslate: { default: () => (key) => key },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * Column definitions (manual mode).\n\t\t * Not required when `schema` is provided.\n\t\t * Each entry may be a full column object OR a bare string key; bare strings\n\t\t * are normalised to `{ key, label }` by `effectiveColumns` so manifest-driven\n\t\t * pages that pass `config.columns` as a string array work without extra mapping.\n\t\t *\n\t\t * `description` renders as the header cell's tooltip and marks the label with a\n\t\t * dotted underline, so a column whose meaning is not obvious from its name (a\n\t\t * maturity level, a computed score, a domain term) can explain itself where the\n\t\t * reader is looking. `columnsFromSchema` fills it from the JSON Schema property\n\t\t * description automatically, so schema-driven tables get it for free.\n\t\t *\n\t\t * @type {Array<{key: string, label: string, description: string, sortable: boolean, width: string, class: string, cellClass: string}|string>}\n\t\t */\n\t\tcolumns: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * Optional leading icon shown at the start of every row. Either a static\n\t\t * MDI icon name (PascalCase, e.g. `'FileDocumentOutline'`) applied to all\n\t\t * rows, or a function `(row) => iconName` to vary it per row. The icon is\n\t\t * resolved through the shared CnIcon registry. Unset = no icon column.\n\t\t *\n\t\t * @type {string | ((row: object) => string) | null}\n\t\t */\n\t\trowIcon: {\n\t\t\ttype: [String, Function],\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * State indicators a page declares for its rows. Each entry is\n\t\t * `{ id, field, equals?, in?, icon, text, tooltip? }`: `field` is a\n\t\t * dotted path on the row, the condition is `equals`, `in`, or plain\n\t\t * truthiness when neither is given, `icon` is a CnIcon name, and `text`\n\t\t * is the text alternative. An entry without `text` does not render,\n\t\t * because an icon with no text is colour and shape alone.\n\t\t *\n\t\t * The page declares which indicators exist. A record cannot add one the\n\t\t * page has not declared, however it is shaped. A page declaring none\n\t\t * renders its rows exactly as before.\n\t\t *\n\t\t * @type {Array<{id?: string, field: string, equals?: (string|number|boolean|null), in?: Array, icon?: string, text: string, tooltip?: string}>}\n\t\t */\n\t\trowIndicators: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * How many declared indicators render on the row itself. The rest are\n\t\t * available from `indicatorsFor(row).overflow` for the row menu.\n\t\t *\n\t\t * @type {number}\n\t\t */\n\t\trowIndicatorCap: {\n\t\t\ttype: Number,\n\t\t\tdefault: DEFAULT_ROW_INDICATOR_CAP,\n\t\t},\n\n\t\t/**\n\t\t * Schema object with `properties` field (schema-driven mode).\n\t\t * When provided, columns are auto-generated from schema properties.\n\t\t */\n\t\tschema: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Per-column overrides when using schema mode: { key: { width, label, sortable, ... } } */\n\t\tcolumnOverrides: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/** Column keys to exclude when using schema mode */\n\t\texcludeColumns: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/** Column keys to include when using schema mode (whitelist) */\n\t\tincludeColumns: {\n\t\t\ttype: Array,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Row data array. Each row should have a unique identifier (see rowKey). */\n\t\trows: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/** Whether data is loading (shows loading spinner) */\n\t\tloading: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Current sort column key */\n\t\tsortKey: {\n\t\t\ttype: String,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Current sort order: 'asc', 'desc', or null (no sort) */\n\t\tsortOrder: {\n\t\t\ttype: String,\n\t\t\tdefault: 'asc',\n\t\t\tvalidator: (v) => v === null || ['asc', 'desc'].includes(v),\n\t\t},\n\n\t\t/**\n\t\t * Ordered multi-column sort state: `[{ key, order }, ...]` (priority\n\t\t * order, 0 to 3 entries). Optional — when empty (the default), the\n\t\t * table falls back to the legacy `sortKey`/`sortOrder` props, so\n\t\t * single-sort hosts are completely unaffected. Shift+click a\n\t\t * sortable header to append/cycle a secondary or tertiary key (see\n\t\t * `src/utils/multiColumnSort.js`).\n\t\t *\n\t\t * @type {Array<{key: string, order: 'asc'|'desc'}>}\n\t\t */\n\t\tsortKeys: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/** Whether rows can be selected with checkboxes */\n\t\tselectable: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Array of currently selected row IDs */\n\t\tselectedIds: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/** Property name used as unique row identifier */\n\t\trowKey: {\n\t\t\ttype: String,\n\t\t\tdefault: 'id',\n\t\t},\n\n\t\t/** Text shown when there are no rows */\n\t\temptyText: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'No items found'),\n\t\t},\n\n\t\t/** Function returning CSS class(es) for a row: (row) => string|object */\n\t\trowClass: {\n\t\t\ttype: Function,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Function returning CSS class(es) for a data cell: (row, col) => string|object */\n\t\tcellClass: {\n\t\t\ttype: Function,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Whether to constrain table height and make it scrollable */\n\t\tscrollable: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Text shown while loading */\n\t\tloadingText: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'Loading…'),\n\t\t},\n\n\t\t/**\n\t\t * Accessible name for the select-all checkbox in the header row.\n\t\t * Used as the checkbox's `aria-label` so screen readers announce a\n\t\t * named control (WCAG 4.1.2). Defaults to the lib's translation of\n\t\t * \"Select all rows\".\n\t\t */\n\t\tselectAllLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'Select all rows'),\n\t\t},\n\n\t\t/**\n\t\t * Accessible name for a per-row select checkbox. Used as the\n\t\t * checkbox's `aria-label` so screen readers announce a named control\n\t\t * (WCAG 4.1.2). Defaults to the lib's translation of \"Select row\".\n\t\t */\n\t\tselectRowLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'Select row'),\n\t\t},\n\n\t\t/**\n\t\t * Optional card title rendered in a header above the table. When set, the\n\t\t * table reads as a self-contained card (the container's own border/radius\n\t\t * is the card chrome). Folded in from the retired CnTableWidget.\n\t\t */\n\t\ttitle: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Drop the container's card chrome (border, radius, shadow) so the table\n\t\t * sits flush inside a parent that already provides a card (e.g. a\n\t\t * CnWidgetWrapper dashboard slot). Folded in from CnTableWidget.\n\t\t */\n\t\tborderless: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * Fill the height of the parent (a flex-column card / widget content\n\t\t * area) so the optional `#footer` is pushed to the bottom instead of\n\t\t * floating directly under a short list. When the list is long enough to\n\t\t * overflow, the footer stays pinned via its sticky rule. No-op outside a\n\t\t * height-constrained parent. Opt-in so ordinary in-flow tables are\n\t\t * unaffected.\n\t\t */\n\t\tfillHeight: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * Hide the column-header row (`<thead>`). Useful for compact dashboard\n\t\t * list widgets that want a plain bordered-row list without column labels.\n\t\t */\n\t\thideHeader: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * Switch the table to `table-layout: fixed`, making each column's `width`\n\t\t * authoritative instead of a hint the browser may override. Opt in when a\n\t\t * column's content would otherwise dictate the layout — a long unbreakable\n\t\t * value (a PHP FQCN, a UUID) widens its column under the default auto\n\t\t * layout and can render past the cell box into its neighbour, while any\n\t\t * column left unsized soaks up all remaining width. Cells also break long\n\t\t * words rather than overflowing. Columns with no `width` share whatever\n\t\t * space is left, so size every column when you want exact control.\n\t\t *\n\t\t * @type {boolean}\n\t\t */\n\t\tfixedLayout: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * Max number of rows to display. When the total exceeds it, only the first\n\t\t * `limit` render and the \"View all\" footer appears (with `viewAllRoute`).\n\t\t * 0 = show all. Folded in from CnTableWidget.\n\t\t */\n\t\tlimit: {\n\t\t\ttype: Number,\n\t\t\tdefault: 0,\n\t\t},\n\n\t\t/**\n\t\t * vue-router route object for the \"View all\" footer link. The footer only\n\t\t * shows when set AND the rows are a `limit`-ed subset. Folded in from CnTableWidget.\n\t\t *\n\t\t * @type {object|null}\n\t\t */\n\t\tviewAllRoute: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Pre-translated \"View all\" footer label. */\n\t\tviewAllLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'View all'),\n\t\t},\n\n\t\t/**\n\t\t * Self-fetch mode (folded from CnTableWidget): the OpenRegister register\n\t\t * id/slug. When `register` + `schemaId` are set and no `rows` are passed,\n\t\t * the table fetches `/apps/openregister/api/objects/{register}/{schemaId}`.\n\t\t *\n\t\t * @type {string|number|null}\n\t\t */\n\t\tregister: {\n\t\t\ttype: [String, Number],\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Self-fetch mode (folded from CnTableWidget): the OpenRegister schema\n\t\t * id used together with `register`. (Distinct from the `schema` prop,\n\t\t * which is a JSON Schema object for column generation.)\n\t\t *\n\t\t * @type {string|number|null}\n\t\t */\n\t\tschemaId: {\n\t\t\ttype: [String, Number],\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Extra query parameters sent with the self-fetch request (register +\n\t\t * schemaId mode) — e.g. a resolved filter map, `_order[field]` ordering,\n\t\t * or `_limit`. Changing it re-triggers the self-fetch, so a host widget\n\t\t * (CnWidgetObjectTable's declarative `source`) can drive filtering and\n\t\t * ordering without re-implementing the fetch. Ignored when external\n\t\t * `rows` are supplied.\n\t\t *\n\t\t * @type {object|null}\n\t\t */\n\t\tfetchParams: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Convenience navigation (folded from CnTableWidget): a function that\n\t\t * receives a row and returns the vue-router route it opens. When set,\n\t\t * each row renders as a real link to that route, stretched over the\n\t\t * row: a click navigates there (the `row-click` event still fires), and\n\t\t * a ctrl/cmd/shift or middle click opens it in a new tab. A table whose\n\t\t * row click selects (`selectable` without `rowClickToView`) gets no link.\n\t\t *\n\t\t * @type {((row: object) => object)|null}\n\t\t */\n\t\trowClickRoute: {\n\t\t\ttype: Function,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * When true, a row-body click emits `row-click` (for navigation) even\n\t\t * while `selectable` — selection then happens only via the checkbox\n\t\t * column. Lets \"click row = open, tick box = select\" coexist. Default\n\t\t * false keeps the legacy behaviour (selectable rows select on body click).\n\t\t *\n\t\t * @type {boolean}\n\t\t */\n\t\trowClickToView: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\t},\n\n\temits: ['row-click', 'row-aux-click', 'row-context-menu', 'select', 'select-all', 'sort', 'view-all'],\n\n\tsetup() {\n\t\t// Tell a deliberate row click apart from a text-selection drag.\n\t\treturn useClickDragGuard()\n\t},\n\n\tdata() {\n\t\treturn {\n\t\t\t/**\n\t\t\t * Resolved aggregate-column values, keyed by `String(row[rowKey])`\n\t\t\t * then by column key. Populated by `loadAggregates()` for columns\n\t\t\t * that declare `aggregate` (see CnIndexPage's manifest config).\n\t\t\t *\n\t\t\t * @type {{[rowId: string]: {[colKey: string]: number}}}\n\t\t\t */\n\t\t\taggregateValues: {},\n\t\t\t/**\n\t\t\t * Whether the table currently overflows its scrollport horizontally.\n\t\t\t * Drives the keyboard tab stop on `.cn-data-table__scroll`; see the\n\t\t\t * comment on that element.\n\t\t\t *\n\t\t\t * @type {boolean}\n\t\t\t */\n\t\t\tisScrollable: false,\n\t\t\t/** True while a batch of aggregate-count requests is in flight. */\n\t\t\taggregateLoading: false,\n\t\t\t/** Monotonic id used to discard a stale aggregate batch when `rows` changes mid-flight. */\n\t\t\taggregateRequestId: 0,\n\t\t\t/** Rows fetched in self-fetch mode (register + schemaId). */\n\t\t\tfetchedRows: [],\n\t\t\t/** True while a self-fetch request is in flight. */\n\t\t\tselfFetchLoading: false,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * Accessible name for the horizontal scrollport when it becomes a tab\n\t\t * stop. Prefers the table's own `title` so the announcement identifies\n\t\t * WHICH table the user has landed in — several can share a dashboard.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tscrollRegionLabel() {\n\t\t\treturn this.title\n\t\t\t\t? t('nextcloud-vue', '{title} — scrollable table', { title: this.title })\n\t\t\t\t: t('nextcloud-vue', 'Scrollable table')\n\t\t},\n\n\t\t/**\n\t\t * The row source: external `rows` when provided, else the self-fetched\n\t\t * rows (register + schemaId mode). External rows always win.\n\t\t *\n\t\t * @return {Array<object>}\n\t\t */\n\t\tsourceRows() {\n\t\t\tif (this.rows && this.rows.length > 0) {\n\t\t\t\treturn this.rows\n\t\t\t}\n\t\t\tif (this.register !== null && this.register !== undefined && this.schemaId !== null && this.schemaId !== undefined) {\n\t\t\t\treturn this.fetchedRows\n\t\t\t}\n\t\t\treturn this.rows\n\t\t},\n\n\t\t/**\n\t\t * The rows actually rendered — `sourceRows` capped to `limit` (0 = all).\n\t\t *\n\t\t * @return {Array<object>}\n\t\t */\n\t\teffectiveRows() {\n\t\t\treturn this.limit > 0 ? this.sourceRows.slice(0, this.limit) : this.sourceRows\n\t\t},\n\n\t\t/**\n\t\t * Total row count before the `limit` cap (drives the count badge + the\n\t\t * \"View all\" footer condition).\n\t\t *\n\t\t * @return {number}\n\t\t */\n\t\ttotalRowCount() {\n\t\t\treturn this.sourceRows.length\n\t\t},\n\n\t\t/**\n\t\t * The URL the router resolves `viewAllRoute` to, or `null` when there is\n\t\t * no route or no router (a widget mounted outside a vue-router context).\n\t\t * Decides whether the \"View all\" footer renders as a link or a button.\n\t\t *\n\t\t * @return {string|null}\n\t\t */\n\t\tviewAllHref() {\n\t\t\tif (!this.viewAllRoute || !this.$router || typeof this.$router.resolve !== 'function') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tconst resolved = this.$router.resolve(this.viewAllRoute)\n\t\t\t\treturn (resolved && resolved.href) || null\n\t\t\t} catch {\n\t\t\t\treturn null\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Whether to show the loading state — the external `loading` prop OR a\n\t\t * self-fetch in flight.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\tisLoading() {\n\t\t\treturn this.loading || this.selfFetchLoading\n\t\t},\n\n\t\t/**\n\t\t * Effective columns: schema-generated or manually provided.\n\t\t * Schema columns take precedence when schema is provided and no manual columns given.\n\t\t */\n\t\teffectiveColumns() {\n\t\t\tconst cols = (this.schema && this.columns.length === 0)\n\t\t\t\t? columnsFromSchema(this.schema, {\n\t\t\t\t\t\texclude: this.excludeColumns,\n\t\t\t\t\t\tinclude: this.includeColumns,\n\t\t\t\t\t\toverrides: this.columnOverrides,\n\t\t\t\t\t})\n\t\t\t\t: this.columns\n\t\t\tif (!(cols || []).some((c) => typeof c === 'string')) {\n\t\t\t\treturn cols || []\n\t\t\t}\n\t\t\tconst schemaCols = this.schema\n\t\t\t\t? columnsFromSchema(this.schema, { overrides: this.columnOverrides })\n\t\t\t\t: []\n\t\t\tconst byKey = new Map(schemaCols.map((c) => [c.key, c]))\n\t\t\treturn (cols || []).map((c) => {\n\t\t\t\tif (typeof c !== 'string') {\n\t\t\t\t\treturn c\n\t\t\t\t}\n\t\t\t\treturn byKey.get(c) || { key: c, label: c, sortable: true }\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * The active ordered sort-key list: the `sortKeys` prop when non-empty,\n\t\t * else a single-entry list derived from the legacy `sortKey`/`sortOrder`\n\t\t * props (empty when neither is active). Every header/badge/aria-sort\n\t\t * computation reads this so single-sort hosts (no `sortKeys` passed)\n\t\t * render exactly as before.\n\t\t *\n\t\t * @return {Array<{key: string, order: 'asc'|'desc'}>}\n\t\t */\n\t\t/**\n\t\t * The link each row opens, keyed by row key: the `rowClickRoute`\n\t\t * location and its href. Empty when rows have no route, or when a row\n\t\t * click selects instead of navigating.\n\t\t *\n\t\t * @return {{[key: string]: {target: object, href: string}}}\n\t\t */\n\t\trowLinks() {\n\t\t\tconst links = {}\n\t\t\tif (!this.rowClickRoute || !this.$router || (this.selectable && !this.rowClickToView)) {\n\t\t\t\treturn links\n\t\t\t}\n\t\t\tfor (const row of this.effectiveRows) {\n\t\t\t\tconst route = this.rowClickRoute(row)\n\t\t\t\tif (!route) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tconst target = typeof route === 'string' ? { path: route } : route\n\t\t\t\tconst href = resolveHref(target, this.$router)\n\t\t\t\tif (href) {\n\t\t\t\t\tlinks[String(row[this.rowKey])] = { target, href }\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn links\n\t\t},\n\n\t\teffectiveSortKeys() {\n\t\t\tif (this.sortKeys && this.sortKeys.length > 0) {\n\t\t\t\treturn this.sortKeys\n\t\t\t}\n\t\t\tif (this.sortKey) {\n\t\t\t\treturn [{ key: this.sortKey, order: this.sortOrder || 'asc' }]\n\t\t\t}\n\t\t\treturn []\n\t\t},\n\n\t\ttotalColumns() {\n\t\t\tlet count = this.effectiveColumns.length\n\t\t\tif (this.selectable) {\n\t\t\t\tcount++\n\t\t\t}\n\t\t\tif (this.rowIcon) {\n\t\t\t\tcount++\n\t\t\t}\n\t\t\tif (this.$slots['row-actions']) {\n\t\t\t\tcount++\n\t\t\t}\n\t\t\treturn count\n\t\t},\n\n\t\t/**\n\t\t * Stable signature of the self-fetch inputs so the watcher refetches\n\t\t * only on a real change (register / schemaId / fetchParams).\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tselfFetchKey() {\n\t\t\treturn JSON.stringify({\n\t\t\t\tregister: this.register,\n\t\t\t\tschemaId: this.schemaId,\n\t\t\t\tfetchParams: this.fetchParams || null,\n\t\t\t})\n\t\t},\n\n\t\tallSelected() {\n\t\t\treturn this.effectiveRows.length > 0\n\t\t\t\t&& this.effectiveRows.every((row) => this.selectedIds.includes(row[this.rowKey]))\n\t\t},\n\n\t\tsomeSelected() {\n\t\t\treturn this.effectiveRows.some((row) => this.selectedIds.includes(row[this.rowKey]))\n\t\t},\n\t},\n\n\twatch: {\n\t\trows: {\n\t\t\thandler() {\n\t\t\t\tthis.loadAggregates()\n\t\t\t},\n\t\t},\n\n\t\teffectiveColumns: {\n\t\t\thandler() {\n\t\t\t\tthis.loadAggregates()\n\t\t\t},\n\n\t\t\tdeep: false,\n\t\t},\n\n\t\t/**\n\t\t * Re-run the self-fetch when its inputs (register, schemaId, or the\n\t\t * host-driven `fetchParams`) change — a token-resolved filter (e.g.\n\t\t * `@workspace.*`) can change after mount. External rows still win.\n\t\t */\n\t\tselfFetchKey() {\n\t\t\tif ((!this.rows || this.rows.length === 0) && this.register !== null && this.register !== undefined && this.schemaId !== null && this.schemaId !== undefined) {\n\t\t\t\tthis.fetchData()\n\t\t\t}\n\t\t},\n\t},\n\n\tmounted() {\n\t\tthis.loadAggregates()\n\t\t// Self-fetch mode: pull rows from OpenRegister when register + schemaId\n\t\t// are given and no external rows were passed (folded from CnTableWidget).\n\t\tif ((!this.rows || this.rows.length === 0) && this.register !== null && this.register !== undefined && this.schemaId !== null && this.schemaId !== undefined) {\n\t\t\tthis.fetchData()\n\t\t}\n\t\tthis.observeScrollOverflow()\n\t},\n\n\tupdated() {\n\t\t// Columns and rows can change after mount (self-fetch resolving, a\n\t\t// column toggled), and either can flip the table between fitting and\n\t\t// overflowing. ResizeObserver alone would miss a change that alters\n\t\t// content width without resizing the box.\n\t\tthis.measureScrollOverflow()\n\t},\n\n\tbeforeUnmount() {\n\t\tthis.disconnectScrollOverflow()\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * The declared indicators that apply to one row, split into the ones\n\t\t * that fit on the row and the ones that go to the row menu.\n\t\t *\n\t\t * @param {object} row The row.\n\t\t * @return {{shown: Array<object>, overflow: Array<object>}}\n\t\t * @spec openspec/changes/working-list-row-actions/specs/index-page/spec.md\n\t\t */\n\t\tindicatorsFor(row) {\n\t\t\treturn resolveRowIndicators(this.rowIndicators, row, this.rowIndicatorCap)\n\t\t},\n\n\t\t/**\n\t\t * Give a cell its full text on hover, and only when it is cut short.\n\t\t *\n\t\t * Cells are one line with an ellipsis, so a long value is readable only\n\t\t * as far as the column goes. A `title` on every cell would answer that,\n\t\t * and would also put a tooltip on the cells that need none — repeating\n\t\t * back what is already on screen.\n\t\t *\n\t\t * Read on ENTER rather than on render: `scrollWidth > clientWidth` forces\n\t\t * layout, and doing it per cell per paint would cost a reflow for a\n\t\t * tooltip almost none of them want. On enter it is one read of one cell,\n\t\t * and it is re-read every time, so a column resized since the last hover\n\t\t * answers for its new width rather than its old one.\n\t\t *\n\t\t * @param {MouseEvent} event The enter event.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\ttitleWhenClipped(event) {\n\t\t\tconst el = event.currentTarget\n\t\t\tif (!el) {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\t// 1px of tolerance, as the scrollport check uses: sub-pixel rounding\n\t\t\t// otherwise reports a cell that visually fits as clipped.\n\t\t\tif (el.scrollWidth - el.clientWidth > 1) {\n\t\t\t\tel.setAttribute('title', (el.textContent || '').trim())\n\t\t\t} else {\n\t\t\t\tel.removeAttribute('title')\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Start watching the scrollport for horizontal overflow.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tobserveScrollOverflow() {\n\t\t\tthis.measureScrollOverflow()\n\t\t\tif (typeof ResizeObserver === 'undefined') {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis._scrollObserver = new ResizeObserver(() => this.measureScrollOverflow())\n\t\t\tconst el = this.$refs.scrollEl\n\t\t\tif (el) {\n\t\t\t\tthis._scrollObserver.observe(el)\n\t\t\t\t// The table itself, not just the port: a column widening pushes\n\t\t\t\t// the content past the fold without the port changing size.\n\t\t\t\tconst table = el.querySelector('table')\n\t\t\t\tif (table) {\n\t\t\t\t\tthis._scrollObserver.observe(table)\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Recompute whether the scrollport overflows horizontally.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tmeasureScrollOverflow() {\n\t\t\tconst el = this.$refs.scrollEl\n\t\t\t// 1px of tolerance: sub-pixel layout rounding otherwise reports a\n\t\t\t// table that visually fits as scrollable, which would put a tab\n\t\t\t// stop on it for no reachable content.\n\t\t\tconst next = !!el && (el.scrollWidth - el.clientWidth) > 1\n\t\t\tif (next !== this.isScrollable) {\n\t\t\t\tthis.isScrollable = next\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Tear down the overflow observer.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tdisconnectScrollOverflow() {\n\t\t\tif (this._scrollObserver) {\n\t\t\t\tthis._scrollObserver.disconnect()\n\t\t\t\tthis._scrollObserver = null\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Resolve a column header label through the consumer's translation\n\t\t * function. Column labels originate from schema property titles\n\t\t * (English canonical source); this makes the rendered header follow\n\t\t * the user's language when the host app provides `cnTranslate`, and\n\t\t * returns the label unchanged when it does not.\n\t\t *\n\t\t * @param {string} label The English source label (schema property title).\n\t\t * @return {string} The translated label, or the input unchanged.\n\t\t */\n\t\ttranslateLabel(label) {\n\t\t\tif (!label) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tconst fn = typeof this.cnTranslate === 'function' ? this.cnTranslate : (k) => k\n\t\t\treturn fn(label)\n\t\t},\n\n\t\t/**\n\t\t * Self-fetch rows from OpenRegister (register + schemaId mode). Best-effort:\n\t\t * any failure leaves the fetched rows empty. Folded from CnTableWidget.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync fetchData() {\n\t\t\tthis.selfFetchLoading = true\n\t\t\ttry {\n\t\t\t\tconst url = generateUrl('/apps/openregister/api/objects/{register}/{schemaId}', {\n\t\t\t\t\tregister: String(this.register),\n\t\t\t\t\tschemaId: String(this.schemaId),\n\t\t\t\t})\n\t\t\t\tconst { data } = await axios.get(url, {\n\t\t\t\t\theaders: { 'OCS-APIREQUEST': 'true' },\n\t\t\t\t\t...(this.fetchParams ? { params: this.fetchParams } : {}),\n\t\t\t\t})\n\t\t\t\tthis.fetchedRows = (data && data.results) || (Array.isArray(data) ? data : [])\n\t\t\t} catch {\n\t\t\t\tthis.fetchedRows = []\n\t\t\t} finally {\n\t\t\t\tthis.selfFetchLoading = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Activate the \"View all\" footer control: emit `view-all`, then push\n\t\t * `viewAllRoute` through the router when there is one. A plain left\n\t\t * click on the link is handled in-app; a modified click (ctrl/cmd/\n\t\t * shift/middle) is left to the browser so \"open in new tab\" keeps\n\t\t * working on the real href.\n\t\t *\n\t\t * @param {MouseEvent} [event] The click event, when triggered by one.\n\t\t * @return {void}\n\t\t */\n\t\tonViewAll(event) {\n\t\t\tconst modified = Boolean(event && (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey || event.button))\n\t\t\tif (event && !modified) {\n\t\t\t\tevent.preventDefault()\n\t\t\t}\n\t\t\t/** @event view-all Emitted when the built-in \"View all\" footer control is activated. Payload: the `viewAllRoute` object. */\n\t\t\tthis.$emit('view-all', this.viewAllRoute)\n\t\t\tif (modified) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (this.viewAllRoute && this.$router) {\n\t\t\t\tthis.$router.push(this.viewAllRoute).catch(() => {})\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Resolve the leading-row icon name for a row. Returns the static\n\t\t * `rowIcon` string, or the result of the `rowIcon(row)` function.\n\t\t *\n\t\t * @param {object} row The row object.\n\t\t * @return {string} The MDI icon name (PascalCase).\n\t\t */\n\t\tgetRowIcon(row) {\n\t\t\treturn typeof this.rowIcon === 'function' ? this.rowIcon(row) : this.rowIcon\n\t\t},\n\n\t\t/**\n\t\t * Get a cell value from a row using dot-notation key.\n\t\t *\n\t\t * OpenRegister system/metadata fields (created, updated, owner, uri,\n\t\t * size, register, schema, ...) live under the object's `@self` block.\n\t\t * For a flat key we fall back to `@self` when the top level has no\n\t\t * value, so sidebar-enabled metadata columns resolve. Top-level fields\n\t\t * always win, keeping existing behaviour unchanged.\n\t\t *\n\t\t * @param {object} row The row data\n\t\t * @param {string} key The column key (supports dot notation: 'address.city')\n\t\t * @return {unknown} The cell value\n\t\t */\n\t\tgetCellValue(row, key) {\n\t\t\tif (typeof key !== 'string') {\n\t\t\t\treturn undefined\n\t\t\t}\n\t\t\tif (key.includes('.')) {\n\t\t\t\treturn key.split('.').reduce((obj, k) => obj?.[k], row)\n\t\t\t}\n\t\t\tif (row?.[key] === undefined && row?.['@self'] && typeof row['@self'] === 'object') {\n\t\t\t\treturn row['@self'][key]\n\t\t\t}\n\t\t\treturn row?.[key]\n\t\t},\n\n\t\t/**\n\t\t * Get the schema property definition for a column key, or `{}` when\n\t\t * there is no schema (manual mode) or no matching property. The result\n\t\t * is handed to `CnCellRenderer` for type-aware rendering; an empty\n\t\t * object makes it fall back to `formatValue()` (plain truncated text).\n\t\t *\n\t\t * @param {string} key Column key\n\t\t * @return {object} Property definition (possibly empty).\n\t\t */\n\t\tgetSchemaProperty(key) {\n\t\t\treturn this.schema?.properties?.[key] || {}\n\t\t},\n\n\t\t/**\n\t\t * Effective property definition handed to CnCellRenderer for a column:\n\t\t * the schema property augmented with the column's own `type`/`format`/\n\t\t * `enum`/`enumLabels` hints. Lets synthesized columns that have no\n\t\t * schema property (e.g. metadata fields with `format: 'date-time'` /\n\t\t * `'uri'`, or a dotted path into a reference this table was never\n\t\t * handed a `:schema` for at all) still get type-aware rendering.\n\t\t * `enumLabels` rides with `enum` rather than getting its own `if`\n\t\t * branch: a column can't usefully declare labels without also\n\t\t * declaring the codes they label.\n\t\t *\n\t\t * @param {object} col Column definition.\n\t\t * @return {object} Property definition for CnCellRenderer.\n\t\t */\n\t\tcolumnProperty(col) {\n\t\t\tconst base = this.getSchemaProperty(col.key)\n\t\t\tif (col && (col.format || col.type || col.enum)) {\n\t\t\t\treturn {\n\t\t\t\t\t...base,\n\t\t\t\t\t...(col.type ? { type: col.type } : {}),\n\t\t\t\t\t...(col.format ? { format: col.format } : {}),\n\t\t\t\t\t...(col.enum ? { enum: col.enum, enumLabels: col.enumLabels || base.enumLabels } : {}),\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn base\n\t\t},\n\n\t\t/**\n\t\t * Declarative cell-format spec handed to CnCellRenderer's `format` prop\n\t\t * (an object: `{ style, currency, decimals, ... }`). `col.format` is\n\t\t * overloaded — for schema-derived columns it is the schema format STRING\n\t\t * (`'date'`, `'uri'`, ...), which is NOT a declarative spec and drives\n\t\t * type-aware rendering via `columnProperty()` instead. Only forward an\n\t\t * object here, so a string schema-format no longer trips CnCellRenderer's\n\t\t * `Object`-typed `format` prop (Vue prop-type warning).\n\t\t *\n\t\t * @param {object} col Column definition.\n\t\t * @return {object|null} The declarative format spec, or null.\n\t\t */\n\t\tcolumnFormat(col) {\n\t\t\treturn (col && typeof col.format === 'object') ? col.format : null\n\t\t},\n\n\t\t/**\n\t\t * Value to render in a cell. For a column that declares `aggregate`\n\t\t * (a count of related objects), returns the cached count once\n\t\t * `loadAggregates()` has resolved it (or `'…'` while pending, `'—'`\n\t\t * if it failed / there's nothing to count). Otherwise the row's\n\t\t * property value via `getCellValue`.\n\t\t *\n\t\t * @param {object} row The row data.\n\t\t * @param {object} col The column definition.\n\t\t * @return {unknown} The value handed to the slot / CnCellRenderer.\n\t\t */\n\t\tcellValue(row, col) {\n\t\t\tif (col && col.aggregate) {\n\t\t\t\tconst cached = this.aggregateValues[String(row[this.rowKey])]\n\t\t\t\tconst v = cached ? cached[col.key] : undefined\n\t\t\t\tif (v === undefined) {\n\t\t\t\t\treturn this.aggregateLoading ? '…' : '—'\n\t\t\t\t}\n\t\t\t\treturn v\n\t\t\t}\n\t\t\treturn this.getCellValue(row, col.key)\n\t\t},\n\n\t\t/**\n\t\t * Interpolate a column's `aggregate.where` map for one row: any string\n\t\t * value of the form `\"@self.<path>\"` is replaced with\n\t\t * `getCellValue(row, path)`; everything else is passed through.\n\t\t *\n\t\t * @param {object} where The `aggregate.where` map (may be undefined).\n\t\t * @param {object} row The parent row.\n\t\t * @return {object} The resolved filter map.\n\t\t */\n\t\tresolveAggregateWhere(where, row) {\n\t\t\tconst out = {}\n\t\t\tfor (const [k, v] of Object.entries(where || {})) {\n\t\t\t\tif (typeof v === 'string' && v.startsWith('@self.')) {\n\t\t\t\t\tout[k] = this.getCellValue(row, v.slice('@self.'.length))\n\t\t\t\t} else {\n\t\t\t\t\tout[k] = v\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn out\n\t\t},\n\n\t\t/**\n\t\t * For every column that declares `aggregate` (currently `op: \"count\"`),\n\t\t * issue one `_limit=0` count request per visible row against the related\n\t\t * OpenRegister collection and cache the totals in `aggregateValues`.\n\t\t * Batched with `Promise.all`; a per-request failure degrades that one\n\t\t * cell to `'—'` (logged), never the page. A monotonic request id\n\t\t * discards a stale batch when `rows` / columns change mid-flight.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync loadAggregates() {\n\t\t\tconst aggCols = this.effectiveColumns.filter((c) => c && c.aggregate && c.aggregate.op === 'count')\n\t\t\tif (aggCols.length === 0) {\n\t\t\t\tif (Object.keys(this.aggregateValues).length > 0) {\n\t\t\t\t\tthis.aggregateValues = {}\n\t\t\t\t}\n\t\t\t\tthis.aggregateLoading = false\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst id = ++this.aggregateRequestId\n\t\t\tthis.aggregateLoading = true\n\t\t\tconst next = {}\n\t\t\tconst jobs = []\n\t\t\tfor (const row of this.rows) {\n\t\t\t\tconst rowKey = String(row[this.rowKey])\n\t\t\t\tnext[rowKey] = {}\n\t\t\t\tfor (const col of aggCols) {\n\t\t\t\t\tconst agg = col.aggregate\n\t\t\t\t\tif (!agg.register || !agg.schema) {\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\t\t\t\t\tconst where = this.resolveAggregateWhere(agg.where, row)\n\t\t\t\t\tjobs.push(axios.get(generateUrl(`/apps/openregister/api/objects/${agg.register}/${agg.schema}`), {\n\t\t\t\t\t\tparams: { ...where, _limit: 0 },\n\t\t\t\t\t})\n\t\t\t\t\t\t.then((res) => {\n\t\t\t\t\t\t\tconst d = res && res.data\n\t\t\t\t\t\t\tnext[rowKey][col.key] = (d && (d.total ?? (Array.isArray(d.results) ? d.results.length : undefined))) ?? 0\n\t\t\t\t\t\t})\n\t\t\t\t\t\t.catch((e) => {\n\t\t\t\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\t\t\t\tconsole.warn(`[CnDataTable] aggregate \"${col.key}\" count failed for row ${rowKey}`, e)\n\t\t\t\t\t\t\tnext[rowKey][col.key] = undefined\n\t\t\t\t\t\t}))\n\t\t\t\t}\n\t\t\t}\n\t\t\tawait Promise.all(jobs)\n\t\t\tif (id !== this.aggregateRequestId) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.aggregateValues = next\n\t\t\tthis.aggregateLoading = false\n\t\t},\n\n\t\tisSelected(row) {\n\t\t\treturn this.selectedIds.includes(row[this.rowKey])\n\t\t},\n\n\t\t/**\n\t\t * Row-body click: toggles selection when `selectable` (ignoring drags),\n\t\t * otherwise emits `row-click` for navigation.\n\t\t *\n\t\t * @param {object} row The clicked row object\n\t\t * @param {MouseEvent} [event] The originating click event.\n\t\t */\n\t\tonRowClick(row, event) {\n\t\t\tif (this.wasDrag(event)) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (this.selectable && !this.rowClickToView) {\n\t\t\t\tthis.toggleSelect(row)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.emitRowClick(row, event)\n\t\t},\n\n\t\t/**\n\t\t * Row mousedown: record the press for the drag guard, and on a row a\n\t\t * middle click opens, keep the browser from starting autoscroll.\n\t\t *\n\t\t * @param {MouseEvent} event The mousedown event.\n\t\t */\n\t\tonRowMouseDown(event) {\n\t\t\tthis.onPointerDown(event)\n\t\t\tif (!this.selectable || this.rowClickToView) {\n\t\t\t\tpreventMiddleClickAutoscroll(event)\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Row-body middle click: emits `row-aux-click`, not `row-click`, so an\n\t\t * existing `row-click` listener that navigates never moves the current\n\t\t * tab away; `rowClickRoute` still opens the row in a new tab. Ignored\n\t\t * for other buttons, nested controls, drags and select-on-click tables.\n\t\t *\n\t\t * @param {object} row The clicked row object\n\t\t * @param {MouseEvent} event The originating auxclick event.\n\t\t */\n\t\tonRowAuxClick(row, event) {\n\t\t\tif (!isRowMiddleClick(event) || this.wasDrag(event)) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (this.selectable && !this.rowClickToView) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\t/**\n\t\t\t * @event row-aux-click Emitted on a row-body middle click, under the same conditions as `row-click`, so a host can open the row in a new tab (see `openRowTarget`). Payload: `(row, event)` — the clicked row object and the native auxclick event.\n\t\t\t * @type {object} The clicked row object.\n\t\t\t */\n\t\t\tthis.$emit('row-aux-click', row, event)\n\t\t\tthis.followRowClickRoute(row, event)\n\t\t},\n\n\t\t/**\n\t\t * Emit `row-click` and follow `rowClickRoute` when set.\n\t\t *\n\t\t * @param {object} row The clicked row object\n\t\t * @param {MouseEvent} [event] The originating click event.\n\t\t */\n\t\temitRowClick(row, event) {\n\t\t\t/**\n\t\t\t * @event row-click Emitted on a row-body click for navigation. Fires when `selectable` is false, OR when `rowClickToView` is set (selection then happens via the checkbox column). Payload: `(row, event)` — the clicked row object and the native click event, so a host can open the row in a new tab on a ctrl/cmd/shift click (see `openRowTarget`). A middle click emits `row-aux-click` instead.\n\t\t\t * @type {object} The clicked row object.\n\t\t\t */\n\t\t\tthis.$emit('row-click', row, event)\n\t\t\tthis.followRowClickRoute(row, event)\n\t\t},\n\n\t\t/**\n\t\t * Follow `rowClickRoute` for a row click: in place, or in a new tab on\n\t\t * a ctrl/cmd/shift or middle click.\n\t\t *\n\t\t * @param {object} row The clicked row object\n\t\t * @param {MouseEvent} [event] The originating click or auxclick event.\n\t\t */\n\t\tfollowRowClickRoute(row, event) {\n\t\t\t// A new-tab click opens the route in a new tab, unless a listener\n\t\t\t// already did.\n\t\t\tif (this.rowClickRoute && this.$router && !isNewTabHandled(event)) {\n\t\t\t\tconst route = this.rowClickRoute(row)\n\t\t\t\tif (route) {\n\t\t\t\t\tmarkNewTabHandled(event, openRowTarget(event, typeof route === 'string' ? { path: route } : route, this.$router))\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * A plain or alt click on a row link routes in place; a new-tab click\n\t\t * is left to the browser, which opens the href itself.\n\t\t *\n\t\t * @param {object} row The row the link belongs to.\n\t\t * @param {MouseEvent} event The click event.\n\t\t */\n\t\tonRowLinkClick(row, event) {\n\t\t\tconst link = this.rowLinks[String(row[this.rowKey])]\n\t\t\tif (!link) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\t// Alt-click on a link downloads it; a row opens like a plain click.\n\t\t\tif (event.altKey && !isNewTabClick(event) && !event.defaultPrevented) {\n\t\t\t\tevent.preventDefault()\n\t\t\t\tthis.$router.push(link.target).catch(() => {})\n\t\t\t\treturn\n\t\t\t}\n\t\t\tfollowLinkClick(event, link.target, this.$router)\n\t\t},\n\n\t\t/**\n\t\t * The row link's accessible name: the row's first cell when it holds\n\t\t * plain text, else a generic name.\n\t\t *\n\t\t * @param {object} row The row.\n\t\t * @param {object} col The first data column.\n\t\t * @return {string}\n\t\t */\n\t\trowLinkLabel(row, col) {\n\t\t\tconst value = this.cellValue(row, col)\n\t\t\tif (['string', 'number'].includes(typeof value) && String(value).trim() !== '') {\n\t\t\t\treturn String(value)\n\t\t\t}\n\t\t\treturn t('nextcloud-vue', 'Open row')\n\t\t},\n\n\t\t/**\n\t\t * Row right-click: forward the row + originating event so a host can\n\t\t * open a context menu. The browser menu is prevented, except on a row\n\t\t * link when no host listens, so its link actions stay available.\n\t\t *\n\t\t * @param {object} row The right-clicked row object.\n\t\t * @param {MouseEvent} event The originating contextmenu event.\n\t\t */\n\t\tonRowContextMenu(row, event) {\n\t\t\tconst fromRowLink = event.target?.closest?.('.cn-table-row__link')\n\t\t\t// `$.vnode.props`, not `$attrs`: a declared emit is stripped from `$attrs`.\n\t\t\tif (!fromRowLink || this.$.vnode.props?.onRowContextMenu) {\n\t\t\t\tevent.preventDefault()\n\t\t\t}\n\t\t\t/**\n\t\t\t * @event row-context-menu Emitted on a row right-click (contextmenu) for hosts that render a context menu.\n\t\t\t * @type {{ row: object, event: MouseEvent }}\n\t\t\t */\n\t\t\tthis.$emit('row-context-menu', { row, event })\n\t\t},\n\n\t\t/**\n\t\t * Index of `key` within `effectiveSortKeys`, or -1 when not active.\n\t\t * Used by the template for the arrow, the numbered badge, and aria-sort.\n\t\t *\n\t\t * @param {string} key Column key.\n\t\t * @return {number}\n\t\t */\n\t\tsortKeyIndex(key) {\n\t\t\treturn this.effectiveSortKeys.findIndex((k) => k && k.key === key)\n\t\t},\n\n\t\t/**\n\t\t * `aria-sort` value for a column header: `'ascending'`/`'descending'`\n\t\t * for the PRIMARY (index 0) active sort key only — secondary/tertiary\n\t\t * keys carry the visible numbered badge instead, per WCAG guidance\n\t\t * that `aria-sort` describes single-column sort state. `null` omits\n\t\t * the attribute entirely (unsorted / not sortable).\n\t\t *\n\t\t * @param {object} col Column definition.\n\t\t * @return {string|null}\n\t\t */\n\t\tariaSortFor(col) {\n\t\t\tif (!col.sortable) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst primary = this.effectiveSortKeys[0]\n\t\t\tif (!primary || primary.key !== col.key) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\treturn primary.order === 'asc' ? 'ascending' : 'descending'\n\t\t},\n\n\t\t/**\n\t\t * Header click: plain click = single-sort (existing behavior,\n\t\t * unchanged); shift+click appends/cycles the column as a secondary or\n\t\t * tertiary sort key. See `src/utils/multiColumnSort.js`.\n\t\t *\n\t\t * @param {string} key Column key.\n\t\t * @param {MouseEvent} [event] The originating click event.\n\t\t */\n\t\tonHeaderClick(key, event) {\n\t\t\tthis.applySort(key, !!(event && event.shiftKey))\n\t\t},\n\n\t\t/**\n\t\t * Header keydown: `Enter` = plain click, `Shift+Enter` = shift+click.\n\t\t *\n\t\t * @param {string} key Column key.\n\t\t * @param {KeyboardEvent} event The originating keydown event.\n\t\t */\n\t\tonHeaderKeydown(key, event) {\n\t\t\tif (event.key !== 'Enter') {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tevent.preventDefault()\n\t\t\tthis.applySort(key, !!event.shiftKey)\n\t\t},\n\n\t\t/**\n\t\t * Compute and emit the next sort state for a header interaction.\n\t\t *\n\t\t * @param {string} key Column key.\n\t\t * @param {boolean} append `true` for a shift+click/shift+Enter (append/cycle a secondary key).\n\t\t */\n\t\tapplySort(key, append) {\n\t\t\tconst keys = nextSortState(this.effectiveSortKeys, key, { append })\n\t\t\tconst primary = keys[0] || null\n\t\t\t/**\n\t\t\t * @event sort Emitted when a sortable column header is clicked (plain click) or shift-clicked (multi-sort).\n\t\t\t * @type {{ key: string|null, order: 'asc'|'desc'|null, keys: Array<{key: string, order: 'asc'|'desc'}> }}\n\t\t\t * `key`/`order` mirror the PRIMARY (first) active sort key exactly as the pre-multi-sort single-key\n\t\t\t * contract did (`null`/`null` when cleared) — existing listeners destructuring `{ key, order }` are\n\t\t\t * unaffected. `keys` is new: the full ordered list (0 to 3 entries) for multi-sort-aware hosts.\n\t\t\t */\n\t\t\tthis.$emit('sort', {\n\t\t\t\tkey: primary ? primary.key : null,\n\t\t\t\torder: primary ? primary.order : null,\n\t\t\t\tkeys,\n\t\t\t})\n\t\t},\n\n\t\ttoggleSelect(row) {\n\t\t\tconst id = row[this.rowKey]\n\t\t\tconst newIds = this.isSelected(row)\n\t\t\t\t? this.selectedIds.filter((i) => i !== id)\n\t\t\t\t: [...this.selectedIds, id]\n\t\t\t/** @event select Emitted when row selection changes. Payload: array of selected IDs. */\n\t\t\tthis.$emit('select', newIds)\n\t\t},\n\n\t\ttoggleSelectAll() {\n\t\t\tif (this.allSelected) {\n\t\t\t\t// Remove only current page IDs, preserving cross-page selections\n\t\t\t\tconst currentPageIds = new Set(this.rows.map((row) => row[this.rowKey]))\n\t\t\t\tthis.$emit('select', this.selectedIds.filter((id) => !currentPageIds.has(id)))\n\t\t\t} else {\n\t\t\t\t// Add current page IDs to existing selections\n\t\t\t\tconst merged = new Set([...this.selectedIds, ...this.rows.map((row) => row[this.rowKey])])\n\t\t\t\tthis.$emit('select', [...merged])\n\t\t\t}\n\t\t\t/** @event select-all Emitted when select-all checkbox is toggled. */\n\t\t\tthis.$emit('select-all', !this.allSelected)\n\t\t},\n\t},\n}\n</script>\n"],"names":["_createElementBlock","_openBlock","_createElementVNode","_toDisplayString","_createVNode","_normalizeClass","_Fragment","_renderList","_withKeys","_renderSlot","_createTextVNode","_createBlock"],"mappings":"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"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { translate } from '@nextcloud/l10n';
|
|
2
2
|
import { buildDateAxisLanes } from '../../utils/dateAxisLanes.js';
|
|
3
|
-
import { isRowMiddleClick } from '../../utils/rowAuxClick.js';
|
|
3
|
+
import { preventMiddleClickAutoscroll, isRowMiddleClick } from '../../utils/rowAuxClick.js';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* CnDateAxisView — the index page's rows on a time scale.
|
|
@@ -61,7 +61,7 @@ var script = {
|
|
|
61
61
|
},
|
|
62
62
|
},
|
|
63
63
|
|
|
64
|
-
emits: ['row-click'],
|
|
64
|
+
emits: ['row-click', 'row-aux-click'],
|
|
65
65
|
|
|
66
66
|
computed: {
|
|
67
67
|
/** @return {object} The layout. */
|
|
@@ -104,23 +104,27 @@ var script = {
|
|
|
104
104
|
methods: {
|
|
105
105
|
t: translate,
|
|
106
106
|
|
|
107
|
+
preventMiddleClickAutoscroll,
|
|
108
|
+
|
|
107
109
|
/**
|
|
108
110
|
* Open one record. Both the bar on the axis and the label of an
|
|
109
111
|
* undated row come through here, so the two paths cannot drift.
|
|
110
112
|
*
|
|
111
113
|
* @param {object} row The record the reader picked.
|
|
112
|
-
* @param {MouseEvent} [event] The click
|
|
114
|
+
* @param {MouseEvent} [event] The click event.
|
|
113
115
|
* @return {void} Nothing.
|
|
114
116
|
*/
|
|
115
117
|
openRow(row, event) {
|
|
116
118
|
/**
|
|
117
|
-
* @event row-click Emitted when a reader opens a record, from its bar on the axis or from the label of an undated row
|
|
119
|
+
* @event row-click Emitted when a reader opens a record, from its bar on the axis or from the label of an undated row. Payload: `(row, event)` — the row and the native click event, so the host can open it in a new tab on a ctrl/cmd/shift click. A middle click emits `row-aux-click` instead.
|
|
118
120
|
*/
|
|
119
121
|
this.$emit('row-click', row, event);
|
|
120
122
|
},
|
|
121
123
|
|
|
122
124
|
/**
|
|
123
|
-
* Middle click on a bar
|
|
125
|
+
* Middle click on a bar or an undated row's label: emits
|
|
126
|
+
* `row-aux-click`, not `row-click`, so an existing listener that
|
|
127
|
+
* navigates never moves the current tab away.
|
|
124
128
|
*
|
|
125
129
|
* @param {object} row The record the reader picked.
|
|
126
130
|
* @param {MouseEvent} event The auxclick event.
|
|
@@ -128,7 +132,10 @@ var script = {
|
|
|
128
132
|
*/
|
|
129
133
|
onAuxClick(row, event) {
|
|
130
134
|
if (isRowMiddleClick(event)) {
|
|
131
|
-
|
|
135
|
+
/**
|
|
136
|
+
* @event row-aux-click Emitted when a reader middle-clicks a record's bar or undated-row label, for opening it in a new tab (see `openRowTarget`). Payload: `(row, event)` — the row and the native auxclick event.
|
|
137
|
+
*/
|
|
138
|
+
this.$emit('row-aux-click', row, event);
|
|
132
139
|
}
|
|
133
140
|
},
|
|
134
141
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CnDateAxisView.vue2.js","sources":["../../../../src/components/CnDateAxisView/CnDateAxisView.vue"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n<template>\n\t<div class=\"cn-date-axis-view\" data-testid=\"cn-date-axis-view\">\n\t\t<p v-if=\"empty\" class=\"cn-date-axis-view__empty\" data-testid=\"cn-date-axis-empty\">\n\t\t\t{{ emptyText }}\n\t\t</p>\n\n\t\t<template v-else>\n\t\t\t<div\n\t\t\t\tv-for=\"lane in lanes\"\n\t\t\t\t:key=\"`lane-${lane.key}`\"\n\t\t\t\tclass=\"cn-date-axis-view__lane\"\n\t\t\t\tdata-testid=\"cn-date-axis-lane\"\n\t\t\t\t:data-lane=\"lane.key\">\n\t\t\t\t<h3 v-if=\"lane.key !== ''\" class=\"cn-date-axis-view__lane-header\">\n\t\t\t\t\t{{ lane.label }}\n\t\t\t\t</h3>\n\n\t\t\t\t<div\n\t\t\t\t\tv-for=\"(track, index) in lane.tracks\"\n\t\t\t\t\t:key=\"`track-${lane.key}-${index}`\"\n\t\t\t\t\tclass=\"cn-date-axis-view__track\"\n\t\t\t\t\tdata-testid=\"cn-date-axis-track\"\n\t\t\t\t\trole=\"list\">\n\t\t\t\t\t<button\n\t\t\t\t\t\tv-for=\"bar in track\"\n\t\t\t\t\t\t:key=\"`bar-${barKey(bar)}`\"\n\t\t\t\t\t\tclass=\"cn-date-axis-view__bar\"\n\t\t\t\t\t\tdata-testid=\"cn-date-axis-bar\"\n\t\t\t\t\t\trole=\"listitem\"\n\t\t\t\t\t\t:data-bar-id=\"barKey(bar)\"\n\t\t\t\t\t\t:style=\"barStyle(bar)\"\n\t\t\t\t\t\t:aria-label=\"barLabel(bar)\"\n\t\t\t\t\t\t@click=\"openRow(bar.row, $event)\"\n\t\t\t\t\t\t@auxclick=\"onAuxClick(bar.row, $event)\">\n\t\t\t\t\t\t{{ barText(bar) }}\n\t\t\t\t\t</button>\n\t\t\t\t</div>\n\t\t\t</div>\n\n\t\t\t<!--\n\t\t\t\tWork with no usable dates. A time scale has nowhere to put it,\n\t\t\t\tand a planner opening this view is looking for exactly this, so\n\t\t\t\tit is a visible lane rather than a row that never renders.\n\t\t\t-->\n\t\t\t<div\n\t\t\t\tv-if=\"unplanned.rows.length > 0\"\n\t\t\t\tclass=\"cn-date-axis-view__lane cn-date-axis-view__lane--unplanned\"\n\t\t\t\tdata-testid=\"cn-date-axis-unplanned\">\n\t\t\t\t<h3 class=\"cn-date-axis-view__lane-header\">\n\t\t\t\t\t{{ unplanned.label }} ({{ unplanned.rows.length }})\n\t\t\t\t</h3>\n\t\t\t\t<button\n\t\t\t\t\tv-for=\"row in unplanned.rows\"\n\t\t\t\t\t:key=\"`unplanned-${rowKeyOf(row)}`\"\n\t\t\t\t\tclass=\"cn-date-axis-view__bar cn-date-axis-view__bar--unplanned\"\n\t\t\t\t\tdata-testid=\"cn-date-axis-unplanned-row\"\n\t\t\t\t\t:data-row-id=\"rowKeyOf(row)\"\n\t\t\t\t\t:aria-label=\"unplannedLabelFor(row)\"\n\t\t\t\t\t@click=\"openRow(row, $event)\"\n\t\t\t\t\t@auxclick=\"onAuxClick(row, $event)\">\n\t\t\t\t\t{{ nameOf(row) }}\n\t\t\t\t</button>\n\t\t\t</div>\n\t\t</template>\n\t</div>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { buildDateAxisLanes } from '../../utils/dateAxisLanes.js'\nimport { isRowMiddleClick } from '../../utils/rowAuxClick.js'\n\n/**\n * CnDateAxisView — the index page's rows on a time scale.\n *\n * 🔴 IT HIDES NOTHING, AND IT RESCHEDULES NOTHING. A planner opens this to find\n * the week where three things land on one person, so overlapping bars get\n * their own track and the lane grows taller rather than losing a bar. Nothing\n * here writes: a date-axis view that moved a bar on drag would be changing\n * statutory dates from a picture, and the one gesture is opening the row.\n *\n * 🔴 EVERY BAR IS A BUTTON WITH AN ACCESSIBLE NAME. A bar positioned by\n * percentage says nothing at all to a reader who cannot see it, so the name\n * carries the row, the lane and both dates in words. `title` alone would not:\n * it is not read reliably and it cannot be reached by keyboard.\n *\n * `buildDateAxisLanes` decides the layout and is tested on its own.\n *\n * @event {object} row-click — A bar was opened. Payload: the row.\n */\nexport default {\n\tname: 'CnDateAxisView',\n\n\tprops: {\n\t\t/** The rows the list holds. */\n\t\trows: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/** Where a row's start is. */\n\t\tstartField: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** Where a row's end is. */\n\t\tendField: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** What to put in a lane together. */\n\t\tlaneField: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** The field a bar is named by. */\n\t\tlabelField: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** The row key. */\n\t\trowKey: {\n\t\t\ttype: String,\n\t\t\tdefault: 'id',\n\t\t},\n\t},\n\n\temits: ['row-click'],\n\n\tcomputed: {\n\t\t/** @return {object} The layout. */\n\t\tlayout() {\n\t\t\treturn buildDateAxisLanes({\n\t\t\t\trows: this.rows,\n\t\t\t\tstartField: this.startField,\n\t\t\t\tendField: this.endField,\n\t\t\t\tlaneField: this.laneField,\n\t\t\t\tunplannedLabel: t('nextcloud-vue', 'Unplanned'),\n\t\t\t})\n\t\t},\n\n\t\t/** @return {Array<object>} The lanes. */\n\t\tlanes() {\n\t\t\treturn this.layout.lanes\n\t\t},\n\n\t\t/** @return {object} The rows that could not be placed. */\n\t\tunplanned() {\n\t\t\treturn this.layout.unplanned\n\t\t},\n\n\t\t/** @return {object} The window every bar is positioned within. */\n\t\twindow() {\n\t\t\treturn this.layout.window\n\t\t},\n\n\t\t/** @return {boolean} Whether there is nothing at all to show. */\n\t\tempty() {\n\t\t\treturn this.lanes.length === 0 && this.unplanned.rows.length === 0\n\t\t},\n\n\t\t/** @return {string} What an empty view says. */\n\t\temptyText() {\n\t\t\treturn t('nextcloud-vue', 'Nothing to place on a time scale')\n\t\t},\n\t},\n\n\tmethods: {\n\t\tt,\n\n\t\t/**\n\t\t * Open one record. Both the bar on the axis and the label of an\n\t\t * undated row come through here, so the two paths cannot drift.\n\t\t *\n\t\t * @param {object} row The record the reader picked.\n\t\t * @param {MouseEvent} [event] The click or middle-click event.\n\t\t * @return {void} Nothing.\n\t\t */\n\t\topenRow(row, event) {\n\t\t\t/**\n\t\t\t * @event row-click Emitted when a reader opens a record, from its bar on the axis or from the label of an undated row, on a click or a middle click. Payload: `(row, event)` — the row and the native click/auxclick event, so the host can open it in a new tab on a ctrl/cmd/shift or middle click.\n\t\t\t */\n\t\t\tthis.$emit('row-click', row, event)\n\t\t},\n\n\t\t/**\n\t\t * Middle click on a bar: open the record like a click would.\n\t\t *\n\t\t * @param {object} row The record the reader picked.\n\t\t * @param {MouseEvent} event The auxclick event.\n\t\t * @return {void} Nothing.\n\t\t */\n\t\tonAuxClick(row, event) {\n\t\t\tif (isRowMiddleClick(event)) {\n\t\t\t\tthis.openRow(row, event)\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * A bar's identity.\n\t\t *\n\t\t * @param {object} bar The bar.\n\t\t * @return {string} Its key.\n\t\t */\n\t\tbarKey(bar) {\n\t\t\treturn this.rowKeyOf(bar.row)\n\t\t},\n\n\t\t/**\n\t\t * A row's identity.\n\t\t *\n\t\t * @param {object} row The row.\n\t\t * @return {string} Its key.\n\t\t */\n\t\trowKeyOf(row) {\n\t\t\treturn String(row?.[this.rowKey] ?? '')\n\t\t},\n\n\t\t/**\n\t\t * What a row is called.\n\t\t *\n\t\t * @param {object} row The row.\n\t\t * @return {string} Its name.\n\t\t */\n\t\tnameOf(row) {\n\t\t\tconst named = this.labelField === '' ? '' : String(row?.[this.labelField] ?? '')\n\t\t\treturn named || this.rowKeyOf(row)\n\t\t},\n\n\t\t/**\n\t\t * A bar's text.\n\t\t *\n\t\t * @param {object} bar The bar.\n\t\t * @return {string} The name.\n\t\t */\n\t\tbarText(bar) {\n\t\t\treturn this.nameOf(bar.row)\n\t\t},\n\n\t\t/**\n\t\t * Where a bar sits on the scale.\n\t\t *\n\t\t * A window of zero width is one bar, or several that all start and end\n\t\t * at the same instant. Dividing by it would be a division by zero, so\n\t\t * the bar takes the whole lane: one thing on a scale of itself is the\n\t\t * whole scale.\n\t\t *\n\t\t * @param {object} bar The bar.\n\t\t * @return {object} The style.\n\t\t */\n\t\tbarStyle(bar) {\n\t\t\tconst { from, to } = this.window\n\t\t\tconst span = (to ?? 0) - (from ?? 0)\n\t\t\tif (span <= 0) {\n\t\t\t\treturn { marginInlineStart: '0%', inlineSize: '100%' }\n\t\t\t}\n\n\t\t\tconst start = ((bar.start - from) / span) * 100\n\t\t\tconst width = ((bar.end - bar.start) / span) * 100\n\n\t\t\treturn {\n\t\t\t\tmarginInlineStart: `${start}%`,\n\t\t\t\t// A zero-length bar is a real thing (a one-day term) and a bar\n\t\t\t\t// of no width is invisible, so it keeps a floor.\n\t\t\t\tinlineSize: `${Math.max(width, 1)}%`,\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * What a bar is called to somebody who cannot see where it sits.\n\t\t *\n\t\t * @param {object} bar The bar.\n\t\t * @return {string} The accessible name.\n\t\t */\n\t\tbarLabel(bar) {\n\t\t\treturn t('nextcloud-vue', '{name}, {from} to {to}', {\n\t\t\t\tname: this.nameOf(bar.row),\n\t\t\t\tfrom: this.dayOf(bar.start),\n\t\t\t\tto: this.dayOf(bar.end),\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * What an unplanned row is called.\n\t\t *\n\t\t * @param {object} row The row.\n\t\t * @return {string} The accessible name.\n\t\t */\n\t\tunplannedLabelFor(row) {\n\t\t\treturn t('nextcloud-vue', '{name}, no dates', { name: this.nameOf(row) })\n\t\t},\n\n\t\t/**\n\t\t * One instant as a day.\n\t\t *\n\t\t * @param {number} instant Milliseconds.\n\t\t * @return {string} The day.\n\t\t */\n\t\tdayOf(instant) {\n\t\t\treturn new Date(instant).toISOString().slice(0, 10)\n\t\t},\n\t},\n}\n</script>\n\n<style scoped lang=\"scss\">\n.cn-date-axis-view__lane-header {\n\tfont-size: 1rem;\n\tmargin: 8px 0 4px;\n}\n\n.cn-date-axis-view__track {\n\tdisplay: block;\n\tposition: relative;\n\tmin-block-size: 28px;\n}\n\n.cn-date-axis-view__bar {\n\tdisplay: block;\n\tbackground-color: var(--color-primary-element-light);\n\tcolor: var(--color-main-text);\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius);\n\tpadding: 2px 6px;\n\tmargin-block-end: 4px;\n\ttext-align: start;\n\toverflow: hidden;\n\twhite-space: nowrap;\n\ttext-overflow: ellipsis;\n\tcursor: pointer;\n}\n\n.cn-date-axis-view__bar--unplanned {\n\tbackground-color: var(--color-background-hover);\n\tinline-size: auto;\n}\n\n.cn-date-axis-view__empty {\n\tcolor: var(--color-text-maxcontrast);\n}\n</style>\n"],"names":["t"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"CnDateAxisView.vue2.js","sources":["../../../../src/components/CnDateAxisView/CnDateAxisView.vue"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n<template>\n\t<div class=\"cn-date-axis-view\" data-testid=\"cn-date-axis-view\">\n\t\t<p v-if=\"empty\" class=\"cn-date-axis-view__empty\" data-testid=\"cn-date-axis-empty\">\n\t\t\t{{ emptyText }}\n\t\t</p>\n\n\t\t<template v-else>\n\t\t\t<div\n\t\t\t\tv-for=\"lane in lanes\"\n\t\t\t\t:key=\"`lane-${lane.key}`\"\n\t\t\t\tclass=\"cn-date-axis-view__lane\"\n\t\t\t\tdata-testid=\"cn-date-axis-lane\"\n\t\t\t\t:data-lane=\"lane.key\">\n\t\t\t\t<h3 v-if=\"lane.key !== ''\" class=\"cn-date-axis-view__lane-header\">\n\t\t\t\t\t{{ lane.label }}\n\t\t\t\t</h3>\n\n\t\t\t\t<div\n\t\t\t\t\tv-for=\"(track, index) in lane.tracks\"\n\t\t\t\t\t:key=\"`track-${lane.key}-${index}`\"\n\t\t\t\t\tclass=\"cn-date-axis-view__track\"\n\t\t\t\t\tdata-testid=\"cn-date-axis-track\"\n\t\t\t\t\trole=\"list\">\n\t\t\t\t\t<button\n\t\t\t\t\t\tv-for=\"bar in track\"\n\t\t\t\t\t\t:key=\"`bar-${barKey(bar)}`\"\n\t\t\t\t\t\tclass=\"cn-date-axis-view__bar\"\n\t\t\t\t\t\tdata-testid=\"cn-date-axis-bar\"\n\t\t\t\t\t\trole=\"listitem\"\n\t\t\t\t\t\t:data-bar-id=\"barKey(bar)\"\n\t\t\t\t\t\t:style=\"barStyle(bar)\"\n\t\t\t\t\t\t:aria-label=\"barLabel(bar)\"\n\t\t\t\t\t\t@click=\"openRow(bar.row, $event)\"\n\t\t\t\t\t\t@mousedown=\"preventMiddleClickAutoscroll\"\n\t\t\t\t\t\t@auxclick=\"onAuxClick(bar.row, $event)\">\n\t\t\t\t\t\t{{ barText(bar) }}\n\t\t\t\t\t</button>\n\t\t\t\t</div>\n\t\t\t</div>\n\n\t\t\t<!--\n\t\t\t\tWork with no usable dates. A time scale has nowhere to put it,\n\t\t\t\tand a planner opening this view is looking for exactly this, so\n\t\t\t\tit is a visible lane rather than a row that never renders.\n\t\t\t-->\n\t\t\t<div\n\t\t\t\tv-if=\"unplanned.rows.length > 0\"\n\t\t\t\tclass=\"cn-date-axis-view__lane cn-date-axis-view__lane--unplanned\"\n\t\t\t\tdata-testid=\"cn-date-axis-unplanned\">\n\t\t\t\t<h3 class=\"cn-date-axis-view__lane-header\">\n\t\t\t\t\t{{ unplanned.label }} ({{ unplanned.rows.length }})\n\t\t\t\t</h3>\n\t\t\t\t<button\n\t\t\t\t\tv-for=\"row in unplanned.rows\"\n\t\t\t\t\t:key=\"`unplanned-${rowKeyOf(row)}`\"\n\t\t\t\t\tclass=\"cn-date-axis-view__bar cn-date-axis-view__bar--unplanned\"\n\t\t\t\t\tdata-testid=\"cn-date-axis-unplanned-row\"\n\t\t\t\t\t:data-row-id=\"rowKeyOf(row)\"\n\t\t\t\t\t:aria-label=\"unplannedLabelFor(row)\"\n\t\t\t\t\t@click=\"openRow(row, $event)\"\n\t\t\t\t\t@mousedown=\"preventMiddleClickAutoscroll\"\n\t\t\t\t\t@auxclick=\"onAuxClick(row, $event)\">\n\t\t\t\t\t{{ nameOf(row) }}\n\t\t\t\t</button>\n\t\t\t</div>\n\t\t</template>\n\t</div>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { buildDateAxisLanes } from '../../utils/dateAxisLanes.js'\nimport { isRowMiddleClick, preventMiddleClickAutoscroll } from '../../utils/rowAuxClick.js'\n\n/**\n * CnDateAxisView — the index page's rows on a time scale.\n *\n * 🔴 IT HIDES NOTHING, AND IT RESCHEDULES NOTHING. A planner opens this to find\n * the week where three things land on one person, so overlapping bars get\n * their own track and the lane grows taller rather than losing a bar. Nothing\n * here writes: a date-axis view that moved a bar on drag would be changing\n * statutory dates from a picture, and the one gesture is opening the row.\n *\n * 🔴 EVERY BAR IS A BUTTON WITH AN ACCESSIBLE NAME. A bar positioned by\n * percentage says nothing at all to a reader who cannot see it, so the name\n * carries the row, the lane and both dates in words. `title` alone would not:\n * it is not read reliably and it cannot be reached by keyboard.\n *\n * `buildDateAxisLanes` decides the layout and is tested on its own.\n *\n * @event {object} row-click — A bar was opened. Payload: the row.\n */\nexport default {\n\tname: 'CnDateAxisView',\n\n\tprops: {\n\t\t/** The rows the list holds. */\n\t\trows: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/** Where a row's start is. */\n\t\tstartField: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** Where a row's end is. */\n\t\tendField: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** What to put in a lane together. */\n\t\tlaneField: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** The field a bar is named by. */\n\t\tlabelField: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** The row key. */\n\t\trowKey: {\n\t\t\ttype: String,\n\t\t\tdefault: 'id',\n\t\t},\n\t},\n\n\temits: ['row-click', 'row-aux-click'],\n\n\tcomputed: {\n\t\t/** @return {object} The layout. */\n\t\tlayout() {\n\t\t\treturn buildDateAxisLanes({\n\t\t\t\trows: this.rows,\n\t\t\t\tstartField: this.startField,\n\t\t\t\tendField: this.endField,\n\t\t\t\tlaneField: this.laneField,\n\t\t\t\tunplannedLabel: t('nextcloud-vue', 'Unplanned'),\n\t\t\t})\n\t\t},\n\n\t\t/** @return {Array<object>} The lanes. */\n\t\tlanes() {\n\t\t\treturn this.layout.lanes\n\t\t},\n\n\t\t/** @return {object} The rows that could not be placed. */\n\t\tunplanned() {\n\t\t\treturn this.layout.unplanned\n\t\t},\n\n\t\t/** @return {object} The window every bar is positioned within. */\n\t\twindow() {\n\t\t\treturn this.layout.window\n\t\t},\n\n\t\t/** @return {boolean} Whether there is nothing at all to show. */\n\t\tempty() {\n\t\t\treturn this.lanes.length === 0 && this.unplanned.rows.length === 0\n\t\t},\n\n\t\t/** @return {string} What an empty view says. */\n\t\temptyText() {\n\t\t\treturn t('nextcloud-vue', 'Nothing to place on a time scale')\n\t\t},\n\t},\n\n\tmethods: {\n\t\tt,\n\n\t\tpreventMiddleClickAutoscroll,\n\n\t\t/**\n\t\t * Open one record. Both the bar on the axis and the label of an\n\t\t * undated row come through here, so the two paths cannot drift.\n\t\t *\n\t\t * @param {object} row The record the reader picked.\n\t\t * @param {MouseEvent} [event] The click event.\n\t\t * @return {void} Nothing.\n\t\t */\n\t\topenRow(row, event) {\n\t\t\t/**\n\t\t\t * @event row-click Emitted when a reader opens a record, from its bar on the axis or from the label of an undated row. Payload: `(row, event)` — the row and the native click event, so the host can open it in a new tab on a ctrl/cmd/shift click. A middle click emits `row-aux-click` instead.\n\t\t\t */\n\t\t\tthis.$emit('row-click', row, event)\n\t\t},\n\n\t\t/**\n\t\t * Middle click on a bar or an undated row's label: emits\n\t\t * `row-aux-click`, not `row-click`, so an existing listener that\n\t\t * navigates never moves the current tab away.\n\t\t *\n\t\t * @param {object} row The record the reader picked.\n\t\t * @param {MouseEvent} event The auxclick event.\n\t\t * @return {void} Nothing.\n\t\t */\n\t\tonAuxClick(row, event) {\n\t\t\tif (isRowMiddleClick(event)) {\n\t\t\t\t/**\n\t\t\t\t * @event row-aux-click Emitted when a reader middle-clicks a record's bar or undated-row label, for opening it in a new tab (see `openRowTarget`). Payload: `(row, event)` — the row and the native auxclick event.\n\t\t\t\t */\n\t\t\t\tthis.$emit('row-aux-click', row, event)\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * A bar's identity.\n\t\t *\n\t\t * @param {object} bar The bar.\n\t\t * @return {string} Its key.\n\t\t */\n\t\tbarKey(bar) {\n\t\t\treturn this.rowKeyOf(bar.row)\n\t\t},\n\n\t\t/**\n\t\t * A row's identity.\n\t\t *\n\t\t * @param {object} row The row.\n\t\t * @return {string} Its key.\n\t\t */\n\t\trowKeyOf(row) {\n\t\t\treturn String(row?.[this.rowKey] ?? '')\n\t\t},\n\n\t\t/**\n\t\t * What a row is called.\n\t\t *\n\t\t * @param {object} row The row.\n\t\t * @return {string} Its name.\n\t\t */\n\t\tnameOf(row) {\n\t\t\tconst named = this.labelField === '' ? '' : String(row?.[this.labelField] ?? '')\n\t\t\treturn named || this.rowKeyOf(row)\n\t\t},\n\n\t\t/**\n\t\t * A bar's text.\n\t\t *\n\t\t * @param {object} bar The bar.\n\t\t * @return {string} The name.\n\t\t */\n\t\tbarText(bar) {\n\t\t\treturn this.nameOf(bar.row)\n\t\t},\n\n\t\t/**\n\t\t * Where a bar sits on the scale.\n\t\t *\n\t\t * A window of zero width is one bar, or several that all start and end\n\t\t * at the same instant. Dividing by it would be a division by zero, so\n\t\t * the bar takes the whole lane: one thing on a scale of itself is the\n\t\t * whole scale.\n\t\t *\n\t\t * @param {object} bar The bar.\n\t\t * @return {object} The style.\n\t\t */\n\t\tbarStyle(bar) {\n\t\t\tconst { from, to } = this.window\n\t\t\tconst span = (to ?? 0) - (from ?? 0)\n\t\t\tif (span <= 0) {\n\t\t\t\treturn { marginInlineStart: '0%', inlineSize: '100%' }\n\t\t\t}\n\n\t\t\tconst start = ((bar.start - from) / span) * 100\n\t\t\tconst width = ((bar.end - bar.start) / span) * 100\n\n\t\t\treturn {\n\t\t\t\tmarginInlineStart: `${start}%`,\n\t\t\t\t// A zero-length bar is a real thing (a one-day term) and a bar\n\t\t\t\t// of no width is invisible, so it keeps a floor.\n\t\t\t\tinlineSize: `${Math.max(width, 1)}%`,\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * What a bar is called to somebody who cannot see where it sits.\n\t\t *\n\t\t * @param {object} bar The bar.\n\t\t * @return {string} The accessible name.\n\t\t */\n\t\tbarLabel(bar) {\n\t\t\treturn t('nextcloud-vue', '{name}, {from} to {to}', {\n\t\t\t\tname: this.nameOf(bar.row),\n\t\t\t\tfrom: this.dayOf(bar.start),\n\t\t\t\tto: this.dayOf(bar.end),\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * What an unplanned row is called.\n\t\t *\n\t\t * @param {object} row The row.\n\t\t * @return {string} The accessible name.\n\t\t */\n\t\tunplannedLabelFor(row) {\n\t\t\treturn t('nextcloud-vue', '{name}, no dates', { name: this.nameOf(row) })\n\t\t},\n\n\t\t/**\n\t\t * One instant as a day.\n\t\t *\n\t\t * @param {number} instant Milliseconds.\n\t\t * @return {string} The day.\n\t\t */\n\t\tdayOf(instant) {\n\t\t\treturn new Date(instant).toISOString().slice(0, 10)\n\t\t},\n\t},\n}\n</script>\n\n<style scoped lang=\"scss\">\n.cn-date-axis-view__lane-header {\n\tfont-size: 1rem;\n\tmargin: 8px 0 4px;\n}\n\n.cn-date-axis-view__track {\n\tdisplay: block;\n\tposition: relative;\n\tmin-block-size: 28px;\n}\n\n.cn-date-axis-view__bar {\n\tdisplay: block;\n\tbackground-color: var(--color-primary-element-light);\n\tcolor: var(--color-main-text);\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius);\n\tpadding: 2px 6px;\n\tmargin-block-end: 4px;\n\ttext-align: start;\n\toverflow: hidden;\n\twhite-space: nowrap;\n\ttext-overflow: ellipsis;\n\tcursor: pointer;\n}\n\n.cn-date-axis-view__bar--unplanned {\n\tbackground-color: var(--color-background-hover);\n\tinline-size: auto;\n}\n\n.cn-date-axis-view__empty {\n\tcolor: var(--color-text-maxcontrast);\n}\n</style>\n"],"names":["t"],"mappings":"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"}
|
|
@@ -55,6 +55,7 @@ function render(_ctx, _cache, $props, $setup, $data, $options) {
|
|
|
55
55
|
style: normalizeStyle($options.barStyle(bar)),
|
|
56
56
|
"aria-label": $options.barLabel(bar),
|
|
57
57
|
onClick: $event => ($options.openRow(bar.row, $event)),
|
|
58
|
+
onMousedown: _cache[0] || (_cache[0] = (...args) => ($options.preventMiddleClickAutoscroll && $options.preventMiddleClickAutoscroll(...args))),
|
|
58
59
|
onAuxclick: $event => ($options.onAuxClick(bar.row, $event))
|
|
59
60
|
}, toDisplayString($options.barText(bar)), 45 /* TEXT, STYLE, PROPS, NEED_HYDRATION */, _hoisted_5))
|
|
60
61
|
}), 128 /* KEYED_FRAGMENT */))
|
|
@@ -73,6 +74,7 @@ function render(_ctx, _cache, $props, $setup, $data, $options) {
|
|
|
73
74
|
"data-row-id": $options.rowKeyOf(row),
|
|
74
75
|
"aria-label": $options.unplannedLabelFor(row),
|
|
75
76
|
onClick: $event => ($options.openRow(row, $event)),
|
|
77
|
+
onMousedown: _cache[1] || (_cache[1] = (...args) => ($options.preventMiddleClickAutoscroll && $options.preventMiddleClickAutoscroll(...args))),
|
|
76
78
|
onAuxclick: $event => ($options.onAuxClick(row, $event))
|
|
77
79
|
}, toDisplayString($options.nameOf(row)), 41 /* TEXT, PROPS, NEED_HYDRATION */, _hoisted_8))
|
|
78
80
|
}), 128 /* KEYED_FRAGMENT */))
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CnDateAxisView.vue3.js","sources":["../../../../src/components/CnDateAxisView/CnDateAxisView.vue?vue&type=template&id=7b82fcaf&lang.js"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n<template>\n\t<div class=\"cn-date-axis-view\" data-testid=\"cn-date-axis-view\">\n\t\t<p v-if=\"empty\" class=\"cn-date-axis-view__empty\" data-testid=\"cn-date-axis-empty\">\n\t\t\t{{ emptyText }}\n\t\t</p>\n\n\t\t<template v-else>\n\t\t\t<div\n\t\t\t\tv-for=\"lane in lanes\"\n\t\t\t\t:key=\"`lane-${lane.key}`\"\n\t\t\t\tclass=\"cn-date-axis-view__lane\"\n\t\t\t\tdata-testid=\"cn-date-axis-lane\"\n\t\t\t\t:data-lane=\"lane.key\">\n\t\t\t\t<h3 v-if=\"lane.key !== ''\" class=\"cn-date-axis-view__lane-header\">\n\t\t\t\t\t{{ lane.label }}\n\t\t\t\t</h3>\n\n\t\t\t\t<div\n\t\t\t\t\tv-for=\"(track, index) in lane.tracks\"\n\t\t\t\t\t:key=\"`track-${lane.key}-${index}`\"\n\t\t\t\t\tclass=\"cn-date-axis-view__track\"\n\t\t\t\t\tdata-testid=\"cn-date-axis-track\"\n\t\t\t\t\trole=\"list\">\n\t\t\t\t\t<button\n\t\t\t\t\t\tv-for=\"bar in track\"\n\t\t\t\t\t\t:key=\"`bar-${barKey(bar)}`\"\n\t\t\t\t\t\tclass=\"cn-date-axis-view__bar\"\n\t\t\t\t\t\tdata-testid=\"cn-date-axis-bar\"\n\t\t\t\t\t\trole=\"listitem\"\n\t\t\t\t\t\t:data-bar-id=\"barKey(bar)\"\n\t\t\t\t\t\t:style=\"barStyle(bar)\"\n\t\t\t\t\t\t:aria-label=\"barLabel(bar)\"\n\t\t\t\t\t\t@click=\"openRow(bar.row, $event)\"\n\t\t\t\t\t\t@auxclick=\"onAuxClick(bar.row, $event)\">\n\t\t\t\t\t\t{{ barText(bar) }}\n\t\t\t\t\t</button>\n\t\t\t\t</div>\n\t\t\t</div>\n\n\t\t\t<!--\n\t\t\t\tWork with no usable dates. A time scale has nowhere to put it,\n\t\t\t\tand a planner opening this view is looking for exactly this, so\n\t\t\t\tit is a visible lane rather than a row that never renders.\n\t\t\t-->\n\t\t\t<div\n\t\t\t\tv-if=\"unplanned.rows.length > 0\"\n\t\t\t\tclass=\"cn-date-axis-view__lane cn-date-axis-view__lane--unplanned\"\n\t\t\t\tdata-testid=\"cn-date-axis-unplanned\">\n\t\t\t\t<h3 class=\"cn-date-axis-view__lane-header\">\n\t\t\t\t\t{{ unplanned.label }} ({{ unplanned.rows.length }})\n\t\t\t\t</h3>\n\t\t\t\t<button\n\t\t\t\t\tv-for=\"row in unplanned.rows\"\n\t\t\t\t\t:key=\"`unplanned-${rowKeyOf(row)}`\"\n\t\t\t\t\tclass=\"cn-date-axis-view__bar cn-date-axis-view__bar--unplanned\"\n\t\t\t\t\tdata-testid=\"cn-date-axis-unplanned-row\"\n\t\t\t\t\t:data-row-id=\"rowKeyOf(row)\"\n\t\t\t\t\t:aria-label=\"unplannedLabelFor(row)\"\n\t\t\t\t\t@click=\"openRow(row, $event)\"\n\t\t\t\t\t@auxclick=\"onAuxClick(row, $event)\">\n\t\t\t\t\t{{ nameOf(row) }}\n\t\t\t\t</button>\n\t\t\t</div>\n\t\t</template>\n\t</div>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { buildDateAxisLanes } from '../../utils/dateAxisLanes.js'\nimport { isRowMiddleClick } from '../../utils/rowAuxClick.js'\n\n/**\n * CnDateAxisView — the index page's rows on a time scale.\n *\n * 🔴 IT HIDES NOTHING, AND IT RESCHEDULES NOTHING. A planner opens this to find\n * the week where three things land on one person, so overlapping bars get\n * their own track and the lane grows taller rather than losing a bar. Nothing\n * here writes: a date-axis view that moved a bar on drag would be changing\n * statutory dates from a picture, and the one gesture is opening the row.\n *\n * 🔴 EVERY BAR IS A BUTTON WITH AN ACCESSIBLE NAME. A bar positioned by\n * percentage says nothing at all to a reader who cannot see it, so the name\n * carries the row, the lane and both dates in words. `title` alone would not:\n * it is not read reliably and it cannot be reached by keyboard.\n *\n * `buildDateAxisLanes` decides the layout and is tested on its own.\n *\n * @event {object} row-click — A bar was opened. Payload: the row.\n */\nexport default {\n\tname: 'CnDateAxisView',\n\n\tprops: {\n\t\t/** The rows the list holds. */\n\t\trows: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/** Where a row's start is. */\n\t\tstartField: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** Where a row's end is. */\n\t\tendField: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** What to put in a lane together. */\n\t\tlaneField: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** The field a bar is named by. */\n\t\tlabelField: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** The row key. */\n\t\trowKey: {\n\t\t\ttype: String,\n\t\t\tdefault: 'id',\n\t\t},\n\t},\n\n\temits: ['row-click'],\n\n\tcomputed: {\n\t\t/** @return {object} The layout. */\n\t\tlayout() {\n\t\t\treturn buildDateAxisLanes({\n\t\t\t\trows: this.rows,\n\t\t\t\tstartField: this.startField,\n\t\t\t\tendField: this.endField,\n\t\t\t\tlaneField: this.laneField,\n\t\t\t\tunplannedLabel: t('nextcloud-vue', 'Unplanned'),\n\t\t\t})\n\t\t},\n\n\t\t/** @return {Array<object>} The lanes. */\n\t\tlanes() {\n\t\t\treturn this.layout.lanes\n\t\t},\n\n\t\t/** @return {object} The rows that could not be placed. */\n\t\tunplanned() {\n\t\t\treturn this.layout.unplanned\n\t\t},\n\n\t\t/** @return {object} The window every bar is positioned within. */\n\t\twindow() {\n\t\t\treturn this.layout.window\n\t\t},\n\n\t\t/** @return {boolean} Whether there is nothing at all to show. */\n\t\tempty() {\n\t\t\treturn this.lanes.length === 0 && this.unplanned.rows.length === 0\n\t\t},\n\n\t\t/** @return {string} What an empty view says. */\n\t\temptyText() {\n\t\t\treturn t('nextcloud-vue', 'Nothing to place on a time scale')\n\t\t},\n\t},\n\n\tmethods: {\n\t\tt,\n\n\t\t/**\n\t\t * Open one record. Both the bar on the axis and the label of an\n\t\t * undated row come through here, so the two paths cannot drift.\n\t\t *\n\t\t * @param {object} row The record the reader picked.\n\t\t * @param {MouseEvent} [event] The click or middle-click event.\n\t\t * @return {void} Nothing.\n\t\t */\n\t\topenRow(row, event) {\n\t\t\t/**\n\t\t\t * @event row-click Emitted when a reader opens a record, from its bar on the axis or from the label of an undated row, on a click or a middle click. Payload: `(row, event)` — the row and the native click/auxclick event, so the host can open it in a new tab on a ctrl/cmd/shift or middle click.\n\t\t\t */\n\t\t\tthis.$emit('row-click', row, event)\n\t\t},\n\n\t\t/**\n\t\t * Middle click on a bar: open the record like a click would.\n\t\t *\n\t\t * @param {object} row The record the reader picked.\n\t\t * @param {MouseEvent} event The auxclick event.\n\t\t * @return {void} Nothing.\n\t\t */\n\t\tonAuxClick(row, event) {\n\t\t\tif (isRowMiddleClick(event)) {\n\t\t\t\tthis.openRow(row, event)\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * A bar's identity.\n\t\t *\n\t\t * @param {object} bar The bar.\n\t\t * @return {string} Its key.\n\t\t */\n\t\tbarKey(bar) {\n\t\t\treturn this.rowKeyOf(bar.row)\n\t\t},\n\n\t\t/**\n\t\t * A row's identity.\n\t\t *\n\t\t * @param {object} row The row.\n\t\t * @return {string} Its key.\n\t\t */\n\t\trowKeyOf(row) {\n\t\t\treturn String(row?.[this.rowKey] ?? '')\n\t\t},\n\n\t\t/**\n\t\t * What a row is called.\n\t\t *\n\t\t * @param {object} row The row.\n\t\t * @return {string} Its name.\n\t\t */\n\t\tnameOf(row) {\n\t\t\tconst named = this.labelField === '' ? '' : String(row?.[this.labelField] ?? '')\n\t\t\treturn named || this.rowKeyOf(row)\n\t\t},\n\n\t\t/**\n\t\t * A bar's text.\n\t\t *\n\t\t * @param {object} bar The bar.\n\t\t * @return {string} The name.\n\t\t */\n\t\tbarText(bar) {\n\t\t\treturn this.nameOf(bar.row)\n\t\t},\n\n\t\t/**\n\t\t * Where a bar sits on the scale.\n\t\t *\n\t\t * A window of zero width is one bar, or several that all start and end\n\t\t * at the same instant. Dividing by it would be a division by zero, so\n\t\t * the bar takes the whole lane: one thing on a scale of itself is the\n\t\t * whole scale.\n\t\t *\n\t\t * @param {object} bar The bar.\n\t\t * @return {object} The style.\n\t\t */\n\t\tbarStyle(bar) {\n\t\t\tconst { from, to } = this.window\n\t\t\tconst span = (to ?? 0) - (from ?? 0)\n\t\t\tif (span <= 0) {\n\t\t\t\treturn { marginInlineStart: '0%', inlineSize: '100%' }\n\t\t\t}\n\n\t\t\tconst start = ((bar.start - from) / span) * 100\n\t\t\tconst width = ((bar.end - bar.start) / span) * 100\n\n\t\t\treturn {\n\t\t\t\tmarginInlineStart: `${start}%`,\n\t\t\t\t// A zero-length bar is a real thing (a one-day term) and a bar\n\t\t\t\t// of no width is invisible, so it keeps a floor.\n\t\t\t\tinlineSize: `${Math.max(width, 1)}%`,\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * What a bar is called to somebody who cannot see where it sits.\n\t\t *\n\t\t * @param {object} bar The bar.\n\t\t * @return {string} The accessible name.\n\t\t */\n\t\tbarLabel(bar) {\n\t\t\treturn t('nextcloud-vue', '{name}, {from} to {to}', {\n\t\t\t\tname: this.nameOf(bar.row),\n\t\t\t\tfrom: this.dayOf(bar.start),\n\t\t\t\tto: this.dayOf(bar.end),\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * What an unplanned row is called.\n\t\t *\n\t\t * @param {object} row The row.\n\t\t * @return {string} The accessible name.\n\t\t */\n\t\tunplannedLabelFor(row) {\n\t\t\treturn t('nextcloud-vue', '{name}, no dates', { name: this.nameOf(row) })\n\t\t},\n\n\t\t/**\n\t\t * One instant as a day.\n\t\t *\n\t\t * @param {number} instant Milliseconds.\n\t\t * @return {string} The day.\n\t\t */\n\t\tdayOf(instant) {\n\t\t\treturn new Date(instant).toISOString().slice(0, 10)\n\t\t},\n\t},\n}\n</script>\n\n<style scoped lang=\"scss\">\n.cn-date-axis-view__lane-header {\n\tfont-size: 1rem;\n\tmargin: 8px 0 4px;\n}\n\n.cn-date-axis-view__track {\n\tdisplay: block;\n\tposition: relative;\n\tmin-block-size: 28px;\n}\n\n.cn-date-axis-view__bar {\n\tdisplay: block;\n\tbackground-color: var(--color-primary-element-light);\n\tcolor: var(--color-main-text);\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius);\n\tpadding: 2px 6px;\n\tmargin-block-end: 4px;\n\ttext-align: start;\n\toverflow: hidden;\n\twhite-space: nowrap;\n\ttext-overflow: ellipsis;\n\tcursor: pointer;\n}\n\n.cn-date-axis-view__bar--unplanned {\n\tbackground-color: var(--color-background-hover);\n\tinline-size: auto;\n}\n\n.cn-date-axis-view__empty {\n\tcolor: var(--color-text-maxcontrast);\n}\n</style>\n"],"names":["_createElementBlock","_toDisplayString","_Fragment","_renderList","_openBlock","_normalizeStyle"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"CnDateAxisView.vue3.js","sources":["../../../../src/components/CnDateAxisView/CnDateAxisView.vue?vue&type=template&id=7b82fcaf&lang.js"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n<template>\n\t<div class=\"cn-date-axis-view\" data-testid=\"cn-date-axis-view\">\n\t\t<p v-if=\"empty\" class=\"cn-date-axis-view__empty\" data-testid=\"cn-date-axis-empty\">\n\t\t\t{{ emptyText }}\n\t\t</p>\n\n\t\t<template v-else>\n\t\t\t<div\n\t\t\t\tv-for=\"lane in lanes\"\n\t\t\t\t:key=\"`lane-${lane.key}`\"\n\t\t\t\tclass=\"cn-date-axis-view__lane\"\n\t\t\t\tdata-testid=\"cn-date-axis-lane\"\n\t\t\t\t:data-lane=\"lane.key\">\n\t\t\t\t<h3 v-if=\"lane.key !== ''\" class=\"cn-date-axis-view__lane-header\">\n\t\t\t\t\t{{ lane.label }}\n\t\t\t\t</h3>\n\n\t\t\t\t<div\n\t\t\t\t\tv-for=\"(track, index) in lane.tracks\"\n\t\t\t\t\t:key=\"`track-${lane.key}-${index}`\"\n\t\t\t\t\tclass=\"cn-date-axis-view__track\"\n\t\t\t\t\tdata-testid=\"cn-date-axis-track\"\n\t\t\t\t\trole=\"list\">\n\t\t\t\t\t<button\n\t\t\t\t\t\tv-for=\"bar in track\"\n\t\t\t\t\t\t:key=\"`bar-${barKey(bar)}`\"\n\t\t\t\t\t\tclass=\"cn-date-axis-view__bar\"\n\t\t\t\t\t\tdata-testid=\"cn-date-axis-bar\"\n\t\t\t\t\t\trole=\"listitem\"\n\t\t\t\t\t\t:data-bar-id=\"barKey(bar)\"\n\t\t\t\t\t\t:style=\"barStyle(bar)\"\n\t\t\t\t\t\t:aria-label=\"barLabel(bar)\"\n\t\t\t\t\t\t@click=\"openRow(bar.row, $event)\"\n\t\t\t\t\t\t@mousedown=\"preventMiddleClickAutoscroll\"\n\t\t\t\t\t\t@auxclick=\"onAuxClick(bar.row, $event)\">\n\t\t\t\t\t\t{{ barText(bar) }}\n\t\t\t\t\t</button>\n\t\t\t\t</div>\n\t\t\t</div>\n\n\t\t\t<!--\n\t\t\t\tWork with no usable dates. A time scale has nowhere to put it,\n\t\t\t\tand a planner opening this view is looking for exactly this, so\n\t\t\t\tit is a visible lane rather than a row that never renders.\n\t\t\t-->\n\t\t\t<div\n\t\t\t\tv-if=\"unplanned.rows.length > 0\"\n\t\t\t\tclass=\"cn-date-axis-view__lane cn-date-axis-view__lane--unplanned\"\n\t\t\t\tdata-testid=\"cn-date-axis-unplanned\">\n\t\t\t\t<h3 class=\"cn-date-axis-view__lane-header\">\n\t\t\t\t\t{{ unplanned.label }} ({{ unplanned.rows.length }})\n\t\t\t\t</h3>\n\t\t\t\t<button\n\t\t\t\t\tv-for=\"row in unplanned.rows\"\n\t\t\t\t\t:key=\"`unplanned-${rowKeyOf(row)}`\"\n\t\t\t\t\tclass=\"cn-date-axis-view__bar cn-date-axis-view__bar--unplanned\"\n\t\t\t\t\tdata-testid=\"cn-date-axis-unplanned-row\"\n\t\t\t\t\t:data-row-id=\"rowKeyOf(row)\"\n\t\t\t\t\t:aria-label=\"unplannedLabelFor(row)\"\n\t\t\t\t\t@click=\"openRow(row, $event)\"\n\t\t\t\t\t@mousedown=\"preventMiddleClickAutoscroll\"\n\t\t\t\t\t@auxclick=\"onAuxClick(row, $event)\">\n\t\t\t\t\t{{ nameOf(row) }}\n\t\t\t\t</button>\n\t\t\t</div>\n\t\t</template>\n\t</div>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { buildDateAxisLanes } from '../../utils/dateAxisLanes.js'\nimport { isRowMiddleClick, preventMiddleClickAutoscroll } from '../../utils/rowAuxClick.js'\n\n/**\n * CnDateAxisView — the index page's rows on a time scale.\n *\n * 🔴 IT HIDES NOTHING, AND IT RESCHEDULES NOTHING. A planner opens this to find\n * the week where three things land on one person, so overlapping bars get\n * their own track and the lane grows taller rather than losing a bar. Nothing\n * here writes: a date-axis view that moved a bar on drag would be changing\n * statutory dates from a picture, and the one gesture is opening the row.\n *\n * 🔴 EVERY BAR IS A BUTTON WITH AN ACCESSIBLE NAME. A bar positioned by\n * percentage says nothing at all to a reader who cannot see it, so the name\n * carries the row, the lane and both dates in words. `title` alone would not:\n * it is not read reliably and it cannot be reached by keyboard.\n *\n * `buildDateAxisLanes` decides the layout and is tested on its own.\n *\n * @event {object} row-click — A bar was opened. Payload: the row.\n */\nexport default {\n\tname: 'CnDateAxisView',\n\n\tprops: {\n\t\t/** The rows the list holds. */\n\t\trows: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/** Where a row's start is. */\n\t\tstartField: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** Where a row's end is. */\n\t\tendField: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** What to put in a lane together. */\n\t\tlaneField: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** The field a bar is named by. */\n\t\tlabelField: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** The row key. */\n\t\trowKey: {\n\t\t\ttype: String,\n\t\t\tdefault: 'id',\n\t\t},\n\t},\n\n\temits: ['row-click', 'row-aux-click'],\n\n\tcomputed: {\n\t\t/** @return {object} The layout. */\n\t\tlayout() {\n\t\t\treturn buildDateAxisLanes({\n\t\t\t\trows: this.rows,\n\t\t\t\tstartField: this.startField,\n\t\t\t\tendField: this.endField,\n\t\t\t\tlaneField: this.laneField,\n\t\t\t\tunplannedLabel: t('nextcloud-vue', 'Unplanned'),\n\t\t\t})\n\t\t},\n\n\t\t/** @return {Array<object>} The lanes. */\n\t\tlanes() {\n\t\t\treturn this.layout.lanes\n\t\t},\n\n\t\t/** @return {object} The rows that could not be placed. */\n\t\tunplanned() {\n\t\t\treturn this.layout.unplanned\n\t\t},\n\n\t\t/** @return {object} The window every bar is positioned within. */\n\t\twindow() {\n\t\t\treturn this.layout.window\n\t\t},\n\n\t\t/** @return {boolean} Whether there is nothing at all to show. */\n\t\tempty() {\n\t\t\treturn this.lanes.length === 0 && this.unplanned.rows.length === 0\n\t\t},\n\n\t\t/** @return {string} What an empty view says. */\n\t\temptyText() {\n\t\t\treturn t('nextcloud-vue', 'Nothing to place on a time scale')\n\t\t},\n\t},\n\n\tmethods: {\n\t\tt,\n\n\t\tpreventMiddleClickAutoscroll,\n\n\t\t/**\n\t\t * Open one record. Both the bar on the axis and the label of an\n\t\t * undated row come through here, so the two paths cannot drift.\n\t\t *\n\t\t * @param {object} row The record the reader picked.\n\t\t * @param {MouseEvent} [event] The click event.\n\t\t * @return {void} Nothing.\n\t\t */\n\t\topenRow(row, event) {\n\t\t\t/**\n\t\t\t * @event row-click Emitted when a reader opens a record, from its bar on the axis or from the label of an undated row. Payload: `(row, event)` — the row and the native click event, so the host can open it in a new tab on a ctrl/cmd/shift click. A middle click emits `row-aux-click` instead.\n\t\t\t */\n\t\t\tthis.$emit('row-click', row, event)\n\t\t},\n\n\t\t/**\n\t\t * Middle click on a bar or an undated row's label: emits\n\t\t * `row-aux-click`, not `row-click`, so an existing listener that\n\t\t * navigates never moves the current tab away.\n\t\t *\n\t\t * @param {object} row The record the reader picked.\n\t\t * @param {MouseEvent} event The auxclick event.\n\t\t * @return {void} Nothing.\n\t\t */\n\t\tonAuxClick(row, event) {\n\t\t\tif (isRowMiddleClick(event)) {\n\t\t\t\t/**\n\t\t\t\t * @event row-aux-click Emitted when a reader middle-clicks a record's bar or undated-row label, for opening it in a new tab (see `openRowTarget`). Payload: `(row, event)` — the row and the native auxclick event.\n\t\t\t\t */\n\t\t\t\tthis.$emit('row-aux-click', row, event)\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * A bar's identity.\n\t\t *\n\t\t * @param {object} bar The bar.\n\t\t * @return {string} Its key.\n\t\t */\n\t\tbarKey(bar) {\n\t\t\treturn this.rowKeyOf(bar.row)\n\t\t},\n\n\t\t/**\n\t\t * A row's identity.\n\t\t *\n\t\t * @param {object} row The row.\n\t\t * @return {string} Its key.\n\t\t */\n\t\trowKeyOf(row) {\n\t\t\treturn String(row?.[this.rowKey] ?? '')\n\t\t},\n\n\t\t/**\n\t\t * What a row is called.\n\t\t *\n\t\t * @param {object} row The row.\n\t\t * @return {string} Its name.\n\t\t */\n\t\tnameOf(row) {\n\t\t\tconst named = this.labelField === '' ? '' : String(row?.[this.labelField] ?? '')\n\t\t\treturn named || this.rowKeyOf(row)\n\t\t},\n\n\t\t/**\n\t\t * A bar's text.\n\t\t *\n\t\t * @param {object} bar The bar.\n\t\t * @return {string} The name.\n\t\t */\n\t\tbarText(bar) {\n\t\t\treturn this.nameOf(bar.row)\n\t\t},\n\n\t\t/**\n\t\t * Where a bar sits on the scale.\n\t\t *\n\t\t * A window of zero width is one bar, or several that all start and end\n\t\t * at the same instant. Dividing by it would be a division by zero, so\n\t\t * the bar takes the whole lane: one thing on a scale of itself is the\n\t\t * whole scale.\n\t\t *\n\t\t * @param {object} bar The bar.\n\t\t * @return {object} The style.\n\t\t */\n\t\tbarStyle(bar) {\n\t\t\tconst { from, to } = this.window\n\t\t\tconst span = (to ?? 0) - (from ?? 0)\n\t\t\tif (span <= 0) {\n\t\t\t\treturn { marginInlineStart: '0%', inlineSize: '100%' }\n\t\t\t}\n\n\t\t\tconst start = ((bar.start - from) / span) * 100\n\t\t\tconst width = ((bar.end - bar.start) / span) * 100\n\n\t\t\treturn {\n\t\t\t\tmarginInlineStart: `${start}%`,\n\t\t\t\t// A zero-length bar is a real thing (a one-day term) and a bar\n\t\t\t\t// of no width is invisible, so it keeps a floor.\n\t\t\t\tinlineSize: `${Math.max(width, 1)}%`,\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * What a bar is called to somebody who cannot see where it sits.\n\t\t *\n\t\t * @param {object} bar The bar.\n\t\t * @return {string} The accessible name.\n\t\t */\n\t\tbarLabel(bar) {\n\t\t\treturn t('nextcloud-vue', '{name}, {from} to {to}', {\n\t\t\t\tname: this.nameOf(bar.row),\n\t\t\t\tfrom: this.dayOf(bar.start),\n\t\t\t\tto: this.dayOf(bar.end),\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * What an unplanned row is called.\n\t\t *\n\t\t * @param {object} row The row.\n\t\t * @return {string} The accessible name.\n\t\t */\n\t\tunplannedLabelFor(row) {\n\t\t\treturn t('nextcloud-vue', '{name}, no dates', { name: this.nameOf(row) })\n\t\t},\n\n\t\t/**\n\t\t * One instant as a day.\n\t\t *\n\t\t * @param {number} instant Milliseconds.\n\t\t * @return {string} The day.\n\t\t */\n\t\tdayOf(instant) {\n\t\t\treturn new Date(instant).toISOString().slice(0, 10)\n\t\t},\n\t},\n}\n</script>\n\n<style scoped lang=\"scss\">\n.cn-date-axis-view__lane-header {\n\tfont-size: 1rem;\n\tmargin: 8px 0 4px;\n}\n\n.cn-date-axis-view__track {\n\tdisplay: block;\n\tposition: relative;\n\tmin-block-size: 28px;\n}\n\n.cn-date-axis-view__bar {\n\tdisplay: block;\n\tbackground-color: var(--color-primary-element-light);\n\tcolor: var(--color-main-text);\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius);\n\tpadding: 2px 6px;\n\tmargin-block-end: 4px;\n\ttext-align: start;\n\toverflow: hidden;\n\twhite-space: nowrap;\n\ttext-overflow: ellipsis;\n\tcursor: pointer;\n}\n\n.cn-date-axis-view__bar--unplanned {\n\tbackground-color: var(--color-background-hover);\n\tinline-size: auto;\n}\n\n.cn-date-axis-view__empty {\n\tcolor: var(--color-text-maxcontrast);\n}\n</style>\n"],"names":["_createElementBlock","_toDisplayString","_Fragment","_renderList","_openBlock","_normalizeStyle"],"mappings":"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"}
|