@conduction/nextcloud-vue 2.56.0-beta.4 → 2.56.0-beta.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js +12 -2
- package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js.map +1 -1
- package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js +1 -2
- package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js.map +1 -1
- package/dist/esm/components/CnActionsMenu/CnActionsMenu.vue2.js.map +1 -1
- package/dist/esm/components/CnActionsMenu/CnActionsMenu.vue3.js +0 -3
- package/dist/esm/components/CnActionsMenu/CnActionsMenu.vue3.js.map +1 -1
- package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue2.js.map +1 -1
- package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue3.js +1 -2
- package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue3.js.map +1 -1
- package/dist/esm/components/CnBoardView/CnBoardView.vue2.js +12 -5
- package/dist/esm/components/CnBoardView/CnBoardView.vue2.js.map +1 -1
- package/dist/esm/components/CnBoardView/CnBoardView.vue3.js +2 -1
- package/dist/esm/components/CnBoardView/CnBoardView.vue3.js.map +1 -1
- package/dist/esm/components/CnCardGrid/CnCardGrid.vue2.js +9 -1
- package/dist/esm/components/CnCardGrid/CnCardGrid.vue2.js.map +1 -1
- package/dist/esm/components/CnCardGrid/CnCardGrid.vue3.js.map +1 -1
- package/dist/esm/components/CnContextMenu/CnContextMenu.vue2.js +6 -1
- package/dist/esm/components/CnContextMenu/CnContextMenu.vue2.js.map +1 -1
- package/dist/esm/components/CnContextMenu/CnContextMenu.vue3.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +116 -15
- package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +27 -14
- package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
- package/dist/esm/components/CnDateAxisView/CnDateAxisView.vue2.js +13 -6
- package/dist/esm/components/CnDateAxisView/CnDateAxisView.vue2.js.map +1 -1
- package/dist/esm/components/CnDateAxisView/CnDateAxisView.vue3.js +2 -0
- package/dist/esm/components/CnDateAxisView/CnDateAxisView.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +29 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
- package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue2.js.map +1 -1
- package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue3.js +1 -2
- package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue3.js.map +1 -1
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js +3 -1
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js.map +1 -1
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js +17 -17
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js.map +1 -1
- package/dist/esm/components/CnFilesWidget/CnFilesWidget.vue2.js +4 -21
- package/dist/esm/components/CnFilesWidget/CnFilesWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnFilesWidget/CnFilesWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnFlowRunsWidget/CnFlowRunsWidget.vue2.js +0 -13
- package/dist/esm/components/CnFlowRunsWidget/CnFlowRunsWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnFlowRunsWidget/CnFlowRunsWidget.vue3.js +8 -8
- package/dist/esm/components/CnFlowRunsWidget/CnFlowRunsWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnFlowsPage/CnFlowsPage.vue2.js.map +1 -1
- package/dist/esm/components/CnFlowsPage/CnFlowsPage.vue3.js +3 -2
- package/dist/esm/components/CnFlowsPage/CnFlowsPage.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +67 -13
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +15 -8
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/manifestActionDispatch.js +31 -11
- package/dist/esm/components/CnIndexPage/manifestActionDispatch.js.map +1 -1
- package/dist/esm/components/CnIntegrationWidget/CnIntegrationWidgetEmpty.vue2.js.map +1 -1
- package/dist/esm/components/CnIntegrationWidget/CnIntegrationWidgetEmpty.vue3.js +1 -2
- package/dist/esm/components/CnIntegrationWidget/CnIntegrationWidgetEmpty.vue3.js.map +1 -1
- package/dist/esm/components/CnInteractionFormWidget/CnInteractionFormWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnInteractionFormWidget/CnInteractionFormWidget.vue3.js +0 -1
- package/dist/esm/components/CnInteractionFormWidget/CnInteractionFormWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnLinkButtonWidget/CnLinkButtonWidget.vue2.js +2 -17
- package/dist/esm/components/CnLinkButtonWidget/CnLinkButtonWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnLinkButtonWidget/CnLinkButtonWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnLogsPage/CnLogsPage.vue2.js +35 -11
- package/dist/esm/components/CnLogsPage/CnLogsPage.vue2.js.map +1 -1
- package/dist/esm/components/CnLogsPage/CnLogsPage.vue3.js +3 -2
- package/dist/esm/components/CnLogsPage/CnLogsPage.vue3.js.map +1 -1
- package/dist/esm/components/CnObjectCard/CnObjectCard.vue2.js +23 -5
- package/dist/esm/components/CnObjectCard/CnObjectCard.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectCard/CnObjectCard.vue3.js +1 -1
- package/dist/esm/components/CnObjectCard/CnObjectCard.vue3.js.map +1 -1
- package/dist/esm/components/CnObjectList/CnObjectList.vue2.js +19 -3
- package/dist/esm/components/CnObjectList/CnObjectList.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectList/CnObjectList.vue3.js.map +1 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +45 -15
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js +4 -2
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnObjectRow/CnObjectRow.vue2.js +24 -6
- package/dist/esm/components/CnObjectRow/CnObjectRow.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectRow/CnObjectRow.vue3.js +1 -1
- package/dist/esm/components/CnObjectRow/CnObjectRow.vue3.js.map +1 -1
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js +0 -1
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js.map +1 -1
- package/dist/esm/components/CnPageRenderer/CnPageRenderer.vue2.js.map +1 -1
- package/dist/esm/components/CnPageRenderer/CnPageRenderer.vue3.js +4 -2
- package/dist/esm/components/CnPageRenderer/CnPageRenderer.vue3.js.map +1 -1
- package/dist/esm/components/CnRowActions/CnRowActions.vue2.js +7 -2
- package/dist/esm/components/CnRowActions/CnRowActions.vue2.js.map +1 -1
- package/dist/esm/components/CnRowActions/CnRowActions.vue3.js.map +1 -1
- package/dist/esm/components/CnSpendAnalyticsWidget/CnSpendAnalyticsWidget.vue2.js +5 -3
- package/dist/esm/components/CnSpendAnalyticsWidget/CnSpendAnalyticsWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnSpendAnalyticsWidget/CnSpendAnalyticsWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnTabs/CnTabs.vue2.js.map +1 -1
- package/dist/esm/components/CnTabs/CnTabs.vue3.js.map +1 -1
- package/dist/esm/components/CnTasksWidget/CnTasksWidget.vue2.js +3 -1
- package/dist/esm/components/CnTasksWidget/CnTasksWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnTasksWidget/CnTasksWidget.vue3.js +1 -0
- package/dist/esm/components/CnTasksWidget/CnTasksWidget.vue3.js.map +1 -1
- package/dist/esm/integrations/builtin/analytics/CnAnalyticsCard.vue2.js +3 -1
- package/dist/esm/integrations/builtin/analytics/CnAnalyticsCard.vue2.js.map +1 -1
- package/dist/esm/integrations/builtin/analytics/CnAnalyticsCard.vue3.js.map +1 -1
- package/dist/esm/integrations/builtin/analytics/CnAnalyticsTab.vue2.js +3 -1
- package/dist/esm/integrations/builtin/analytics/CnAnalyticsTab.vue2.js.map +1 -1
- package/dist/esm/integrations/builtin/analytics/CnAnalyticsTab.vue3.js +2 -4
- package/dist/esm/integrations/builtin/analytics/CnAnalyticsTab.vue3.js.map +1 -1
- package/dist/esm/integrations/builtin/bookmarks/CnBookmarksCard.vue2.js +6 -0
- package/dist/esm/integrations/builtin/bookmarks/CnBookmarksCard.vue2.js.map +1 -1
- package/dist/esm/integrations/builtin/bookmarks/CnBookmarksCard.vue3.js +68 -53
- package/dist/esm/integrations/builtin/bookmarks/CnBookmarksCard.vue3.js.map +1 -1
- package/dist/esm/integrations/builtin/bookmarks/CnBookmarksTab.vue2.js +17 -0
- package/dist/esm/integrations/builtin/bookmarks/CnBookmarksTab.vue2.js.map +1 -1
- package/dist/esm/integrations/builtin/bookmarks/CnBookmarksTab.vue3.js +21 -19
- package/dist/esm/integrations/builtin/bookmarks/CnBookmarksTab.vue3.js.map +1 -1
- package/dist/esm/integrations/builtin/collectives/CnCollectivesCard.vue2.js +3 -1
- package/dist/esm/integrations/builtin/collectives/CnCollectivesCard.vue2.js.map +1 -1
- package/dist/esm/integrations/builtin/collectives/CnCollectivesCard.vue3.js.map +1 -1
- package/dist/esm/integrations/builtin/collectives/CnCollectivesTab.vue2.js +3 -1
- package/dist/esm/integrations/builtin/collectives/CnCollectivesTab.vue2.js.map +1 -1
- package/dist/esm/integrations/builtin/collectives/CnCollectivesTab.vue3.js +2 -4
- package/dist/esm/integrations/builtin/collectives/CnCollectivesTab.vue3.js.map +1 -1
- package/dist/esm/integrations/builtin/cospend/CnCospendCard.vue2.js +2 -1
- package/dist/esm/integrations/builtin/cospend/CnCospendCard.vue2.js.map +1 -1
- package/dist/esm/integrations/builtin/cospend/CnCospendCard.vue3.js.map +1 -1
- package/dist/esm/integrations/builtin/cospend/CnCospendTab.vue2.js +2 -1
- package/dist/esm/integrations/builtin/cospend/CnCospendTab.vue2.js.map +1 -1
- package/dist/esm/integrations/builtin/cospend/CnCospendTab.vue3.js +1 -2
- package/dist/esm/integrations/builtin/cospend/CnCospendTab.vue3.js.map +1 -1
- package/dist/esm/integrations/builtin/deck/CnDeckCard.vue2.js +2 -1
- package/dist/esm/integrations/builtin/deck/CnDeckCard.vue2.js.map +1 -1
- package/dist/esm/integrations/builtin/deck/CnDeckCard.vue3.js.map +1 -1
- package/dist/esm/integrations/builtin/deck/CnDeckTab.vue2.js +2 -1
- package/dist/esm/integrations/builtin/deck/CnDeckTab.vue2.js.map +1 -1
- package/dist/esm/integrations/builtin/deck/CnDeckTab.vue3.js +1 -2
- package/dist/esm/integrations/builtin/deck/CnDeckTab.vue3.js.map +1 -1
- package/dist/esm/integrations/builtin/email/CnEmailCard.vue2.js.map +1 -1
- package/dist/esm/integrations/builtin/email/CnEmailCard.vue3.js.map +1 -1
- package/dist/esm/integrations/builtin/email/CnEmailTab.vue2.js.map +1 -1
- package/dist/esm/integrations/builtin/email/CnEmailTab.vue3.js +1 -2
- package/dist/esm/integrations/builtin/email/CnEmailTab.vue3.js.map +1 -1
- package/dist/esm/integrations/builtin/flow/CnFlowCard.vue2.js +2 -1
- package/dist/esm/integrations/builtin/flow/CnFlowCard.vue2.js.map +1 -1
- package/dist/esm/integrations/builtin/flow/CnFlowCard.vue3.js.map +1 -1
- package/dist/esm/integrations/builtin/flow/CnFlowTab.vue2.js +2 -1
- package/dist/esm/integrations/builtin/flow/CnFlowTab.vue2.js.map +1 -1
- package/dist/esm/integrations/builtin/flow/CnFlowTab.vue3.js +1 -2
- package/dist/esm/integrations/builtin/flow/CnFlowTab.vue3.js.map +1 -1
- package/dist/esm/integrations/builtin/forms/CnFormsCard.vue2.js +3 -1
- package/dist/esm/integrations/builtin/forms/CnFormsCard.vue2.js.map +1 -1
- package/dist/esm/integrations/builtin/forms/CnFormsCard.vue3.js.map +1 -1
- package/dist/esm/integrations/builtin/forms/CnFormsTab.vue2.js +3 -1
- package/dist/esm/integrations/builtin/forms/CnFormsTab.vue2.js.map +1 -1
- package/dist/esm/integrations/builtin/forms/CnFormsTab.vue3.js +2 -4
- package/dist/esm/integrations/builtin/forms/CnFormsTab.vue3.js.map +1 -1
- package/dist/esm/integrations/builtin/maps/CnMapsCard.vue2.js +3 -1
- package/dist/esm/integrations/builtin/maps/CnMapsCard.vue2.js.map +1 -1
- package/dist/esm/integrations/builtin/maps/CnMapsCard.vue3.js.map +1 -1
- package/dist/esm/integrations/builtin/maps/CnMapsTab.vue2.js +3 -1
- package/dist/esm/integrations/builtin/maps/CnMapsTab.vue2.js.map +1 -1
- package/dist/esm/integrations/builtin/maps/CnMapsTab.vue3.js +2 -4
- package/dist/esm/integrations/builtin/maps/CnMapsTab.vue3.js.map +1 -1
- package/dist/esm/integrations/builtin/openproject/CnOpenprojectCard.vue2.js +3 -1
- package/dist/esm/integrations/builtin/openproject/CnOpenprojectCard.vue2.js.map +1 -1
- package/dist/esm/integrations/builtin/openproject/CnOpenprojectCard.vue3.js.map +1 -1
- package/dist/esm/integrations/builtin/openproject/CnOpenprojectTab.vue2.js +3 -1
- package/dist/esm/integrations/builtin/openproject/CnOpenprojectTab.vue2.js.map +1 -1
- package/dist/esm/integrations/builtin/openproject/CnOpenprojectTab.vue3.js +2 -4
- package/dist/esm/integrations/builtin/openproject/CnOpenprojectTab.vue3.js.map +1 -1
- package/dist/esm/integrations/builtin/photos/CnPhotosCard.vue2.js +2 -1
- package/dist/esm/integrations/builtin/photos/CnPhotosCard.vue2.js.map +1 -1
- package/dist/esm/integrations/builtin/photos/CnPhotosCard.vue3.js.map +1 -1
- package/dist/esm/integrations/builtin/photos/CnPhotosTab.vue2.js +2 -1
- package/dist/esm/integrations/builtin/photos/CnPhotosTab.vue2.js.map +1 -1
- package/dist/esm/integrations/builtin/photos/CnPhotosTab.vue3.js +1 -2
- package/dist/esm/integrations/builtin/photos/CnPhotosTab.vue3.js.map +1 -1
- package/dist/esm/integrations/builtin/polls/CnPollsCard.vue2.js +2 -1
- package/dist/esm/integrations/builtin/polls/CnPollsCard.vue2.js.map +1 -1
- package/dist/esm/integrations/builtin/polls/CnPollsCard.vue3.js.map +1 -1
- package/dist/esm/integrations/builtin/polls/CnPollsTab.vue2.js +2 -1
- package/dist/esm/integrations/builtin/polls/CnPollsTab.vue2.js.map +1 -1
- package/dist/esm/integrations/builtin/polls/CnPollsTab.vue3.js +1 -2
- package/dist/esm/integrations/builtin/polls/CnPollsTab.vue3.js.map +1 -1
- package/dist/esm/integrations/builtin/shares/CnSharesTab.vue2.js.map +1 -1
- package/dist/esm/integrations/builtin/shares/CnSharesTab.vue3.js +3 -6
- package/dist/esm/integrations/builtin/shares/CnSharesTab.vue3.js.map +1 -1
- package/dist/esm/integrations/builtin/talk/CnTalkCard.vue2.js +3 -1
- package/dist/esm/integrations/builtin/talk/CnTalkCard.vue2.js.map +1 -1
- package/dist/esm/integrations/builtin/talk/CnTalkCard.vue3.js.map +1 -1
- package/dist/esm/integrations/builtin/talk/CnTalkTab.vue2.js +3 -1
- package/dist/esm/integrations/builtin/talk/CnTalkTab.vue2.js.map +1 -1
- package/dist/esm/integrations/builtin/talk/CnTalkTab.vue3.js +2 -4
- package/dist/esm/integrations/builtin/talk/CnTalkTab.vue3.js.map +1 -1
- package/dist/esm/integrations/builtin/time-tracker/CnTimeTrackerCard.vue2.js +2 -1
- package/dist/esm/integrations/builtin/time-tracker/CnTimeTrackerCard.vue2.js.map +1 -1
- package/dist/esm/integrations/builtin/time-tracker/CnTimeTrackerCard.vue3.js.map +1 -1
- package/dist/esm/integrations/builtin/time-tracker/CnTimeTrackerTab.vue2.js +2 -1
- package/dist/esm/integrations/builtin/time-tracker/CnTimeTrackerTab.vue2.js.map +1 -1
- package/dist/esm/integrations/builtin/time-tracker/CnTimeTrackerTab.vue3.js +1 -2
- package/dist/esm/integrations/builtin/time-tracker/CnTimeTrackerTab.vue3.js.map +1 -1
- package/dist/esm/integrations/builtin/xwiki/CnXwikiCard.vue2.js +4 -1
- package/dist/esm/integrations/builtin/xwiki/CnXwikiCard.vue2.js.map +1 -1
- package/dist/esm/integrations/builtin/xwiki/CnXwikiCard.vue3.js.map +1 -1
- package/dist/esm/integrations/builtin/xwiki/CnXwikiTab.vue2.js +6 -3
- package/dist/esm/integrations/builtin/xwiki/CnXwikiTab.vue2.js.map +1 -1
- package/dist/esm/integrations/builtin/xwiki/CnXwikiTab.vue3.js +2 -4
- package/dist/esm/integrations/builtin/xwiki/CnXwikiTab.vue3.js.map +1 -1
- package/dist/esm/l10n/en.json.js +1 -0
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +1 -0
- package/dist/esm/l10n/nl.json.js.map +1 -1
- package/dist/esm/nextcloud-vue.css +71 -7
- package/dist/esm/utils/actionLink.js +5 -0
- package/dist/esm/utils/actionLink.js.map +1 -1
- package/dist/esm/utils/actionsDispatcher.js +16 -1
- package/dist/esm/utils/actionsDispatcher.js.map +1 -1
- package/dist/esm/utils/linkNavigation.js +6 -3
- package/dist/esm/utils/linkNavigation.js.map +1 -1
- package/dist/esm/utils/rowAuxClick.js +30 -5
- package/dist/esm/utils/rowAuxClick.js.map +1 -1
- package/dist/esm/utils/safeHref.js +18 -3
- package/dist/esm/utils/safeHref.js.map +1 -1
- package/dist/nextcloud-vue.cjs.js +809 -383
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +71 -7
- package/l10n/en.json +1 -0
- package/l10n/nl.json +1 -0
- package/package.json +1 -1
- package/src/components/CnActionsBar/CnActionsBar.vue +14 -5
- package/src/components/CnActionsMenu/CnActionsMenu.vue +0 -3
- package/src/components/CnAdminSettingsShell/CnAdminSettingsShell.vue +1 -2
- package/src/components/CnBoardView/CnBoardView.vue +13 -5
- package/src/components/CnCardGrid/CnCardGrid.vue +9 -1
- package/src/components/CnContextMenu/CnContextMenu.vue +6 -1
- package/src/components/CnDataTable/CnDataTable.md +1 -1
- package/src/components/CnDataTable/CnDataTable.vue +131 -17
- package/src/components/CnDateAxisView/CnDateAxisView.vue +15 -6
- package/src/components/CnDetailPage/CnDetailPage.md +1 -1
- package/src/components/CnDetailPage/CnDetailPage.vue +29 -1
- package/src/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue +1 -2
- package/src/components/CnFilesBrowser/CnFilesBrowser.vue +5 -3
- package/src/components/CnFilesWidget/CnFilesWidget.vue +4 -21
- package/src/components/CnFlowRunsWidget/CnFlowRunsWidget.vue +8 -21
- package/src/components/CnFlowsPage/CnFlowsPage.vue +2 -1
- package/src/components/CnIndexPage/CnIndexPage.vue +75 -14
- package/src/components/CnIndexPage/manifestActionDispatch.js +31 -11
- package/src/components/CnIntegrationWidget/CnIntegrationWidgetEmpty.vue +1 -2
- package/src/components/CnInteractionFormWidget/CnInteractionFormWidget.vue +0 -1
- package/src/components/CnLinkButtonWidget/CnLinkButtonWidget.vue +2 -17
- package/src/components/CnLogsPage/CnLogsPage.vue +37 -12
- package/src/components/CnObjectCard/CnObjectCard.vue +24 -6
- package/src/components/CnObjectList/CnObjectList.vue +19 -3
- package/src/components/CnObjectListWidget/CnObjectListWidget.vue +47 -15
- package/src/components/CnObjectRow/CnObjectRow.vue +25 -7
- package/src/components/CnObjectSidebar/CnFilesTab.vue +0 -1
- package/src/components/CnPageRenderer/CnPageRenderer.vue +2 -0
- package/src/components/CnRowActions/CnRowActions.vue +7 -2
- package/src/components/CnSpendAnalyticsWidget/CnSpendAnalyticsWidget.vue +5 -3
- package/src/components/CnTabs/CnTabs.vue +7 -3
- package/src/components/CnTasksWidget/CnTasksWidget.vue +4 -1
- package/src/css/table.css +26 -0
- package/src/integrations/builtin/analytics/CnAnalyticsCard.vue +3 -2
- package/src/integrations/builtin/analytics/CnAnalyticsTab.vue +5 -6
- package/src/integrations/builtin/bookmarks/CnBookmarksCard.vue +12 -1
- package/src/integrations/builtin/bookmarks/CnBookmarksTab.vue +23 -6
- package/src/integrations/builtin/bookmarks/__tests__/CnBookmarksTab.spec.js +7 -2
- package/src/integrations/builtin/collectives/CnCollectivesCard.vue +3 -2
- package/src/integrations/builtin/collectives/CnCollectivesTab.vue +5 -6
- package/src/integrations/builtin/cospend/CnCospendCard.vue +2 -2
- package/src/integrations/builtin/cospend/CnCospendTab.vue +3 -4
- package/src/integrations/builtin/deck/CnDeckCard.vue +2 -2
- package/src/integrations/builtin/deck/CnDeckTab.vue +3 -4
- package/src/integrations/builtin/email/CnEmailCard.vue +6 -2
- package/src/integrations/builtin/email/CnEmailTab.vue +7 -4
- package/src/integrations/builtin/flow/CnFlowCard.vue +2 -2
- package/src/integrations/builtin/flow/CnFlowTab.vue +3 -4
- package/src/integrations/builtin/forms/CnFormsCard.vue +3 -2
- package/src/integrations/builtin/forms/CnFormsTab.vue +5 -6
- package/src/integrations/builtin/maps/CnMapsCard.vue +3 -2
- package/src/integrations/builtin/maps/CnMapsTab.vue +5 -6
- package/src/integrations/builtin/maps/__tests__/CnMapsTab.spec.js +9 -0
- package/src/integrations/builtin/openproject/CnOpenprojectCard.vue +3 -2
- package/src/integrations/builtin/openproject/CnOpenprojectTab.vue +5 -6
- package/src/integrations/builtin/photos/CnPhotosCard.vue +2 -2
- package/src/integrations/builtin/photos/CnPhotosTab.vue +3 -4
- package/src/integrations/builtin/polls/CnPollsCard.vue +2 -2
- package/src/integrations/builtin/polls/CnPollsTab.vue +3 -4
- package/src/integrations/builtin/shares/CnSharesTab.vue +3 -6
- package/src/integrations/builtin/talk/CnTalkCard.vue +3 -2
- package/src/integrations/builtin/talk/CnTalkTab.vue +5 -6
- package/src/integrations/builtin/time-tracker/CnTimeTrackerCard.vue +2 -2
- package/src/integrations/builtin/time-tracker/CnTimeTrackerTab.vue +3 -4
- package/src/integrations/builtin/xwiki/CnXwikiCard.vue +4 -2
- package/src/integrations/builtin/xwiki/CnXwikiTab.vue +7 -7
- package/src/integrations/builtin/xwiki/__tests__/CnXwikiTab.spec.js +9 -0
- package/src/utils/actionLink.js +5 -0
- package/src/utils/actionsDispatcher.js +15 -0
- package/src/utils/linkNavigation.js +6 -3
- package/src/utils/rowAuxClick.js +29 -4
- package/src/utils/safeHref.js +18 -3
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CnIndexPage.vue3.js","sources":["../../../../src/components/CnIndexPage/CnIndexPage.vue?vue&type=template&id=9af0d906&lang.js"],"sourcesContent":["<template>\n\t<div class=\"cn-index-page\"\n\t\tdata-testid=\"cn-index-page\"\n\t\t@keydown=\"onListKeydown\">\n\t\t<!-- Header — overridable via #header slot. CnPageHeader ALWAYS renders:\n\t\t with showTitle it is the full visual header (icon, title,\n\t\t description); without it, `visuallyHidden` clips everything but the\n\t\t <h1> out of the layout. The page therefore looks identical either\n\t\t way, but the <main> landmark always carries a heading — the sidebar\n\t\t title this component used to rely on lives outside <main>, so\n\t\t screen-reader users and \"skip to main content\" got an unlabelled\n\t\t region (WCAG 2.4.6 / 1.3.1). -->\n\t\t<slot\n\t\t\tname=\"header\"\n\t\t\t:title=\"title\"\n\t\t\t:description=\"description\"\n\t\t\t:icon=\"resolvedIcon\"\n\t\t\t:showTitle=\"showTitle\">\n\t\t\t<CnPageHeader\n\t\t\t\t:title=\"title\"\n\t\t\t\t:description=\"description\"\n\t\t\t\t:icon=\"resolvedIcon\"\n\t\t\t\t:visuallyHidden=\"!showTitle\" />\n\t\t</slot>\n\n\t\t<!-- Optional content below header, above actions bar -->\n\t\t<div v-if=\"$slots['below-header']\" class=\"cn-index-page__below-header\">\n\t\t\t<slot name=\"below-header\" />\n\t\t</div>\n\n\t\t<!-- Actions bar -->\n\t\t<CnActionsBar\n\t\t\t:pagination=\"effectivePagination\"\n\t\t\t:objectCount=\"effectiveObjects.length\"\n\t\t\t:selectable=\"selectable\"\n\t\t\t:selectedIds=\"internalSelectedIds\"\n\t\t\t:addLabel=\"resolvedAddLabel\"\n\t\t\t:addIcon=\"resolvedIcon\"\n\t\t\t:inlineActionCount=\"inlineActionCount\"\n\t\t\t:showMassImport=\"showMassImport\"\n\t\t\t:showMassExport=\"showMassExport\"\n\t\t\t:showMassCopy=\"showMassCopy\"\n\t\t\t:showMassDelete=\"showMassDelete\"\n\t\t\t:viewMode=\"currentViewMode\"\n\t\t\t:showViewToggle=\"showViewToggle\"\n\t\t\t:availableViewModes=\"effectiveToggleModes\"\n\t\t\t:cardsLabel=\"cardsLabel\"\n\t\t\t:tableLabel=\"tableLabel\"\n\t\t\t:listLabel=\"listLabel\"\n\t\t\t:cardsIcon=\"cardsIcon\"\n\t\t\t:tableIcon=\"tableIcon\"\n\t\t\t:showMap=\"showMapSegment\"\n\t\t\t:mapLabel=\"mapLabel\"\n\t\t\t:mapIcon=\"mapIcon\"\n\t\t\t:listIcon=\"listIcon\"\n\t\t\t:showSortSelect=\"showSortSelect\"\n\t\t\t:sortOptions=\"sortSelectOptions\"\n\t\t\t:sortValue=\"sortSelectValue\"\n\t\t\t:showSearch=\"inlineSearch\"\n\t\t\t:searchValue=\"effectiveSearchValue\"\n\t\t\t:searchPlaceholder=\"searchPlaceholder\"\n\t\t\t:refreshing=\"effectiveRefreshing\"\n\t\t\t:refreshDisabled=\"refreshDisabled\"\n\t\t\t:addDisabled=\"addDisabled\"\n\t\t\t:addTo=\"addLinkTo\"\n\t\t\t:showAdd=\"effectiveShowAdd\"\n\t\t\t:showSidebarToggle=\"hasSidebar\"\n\t\t\t:sidebarOpen=\"sidebarOpen\"\n\t\t\t:headerActions=\"mergedHeaderActions\"\n\t\t\t:bulkActions=\"mergedBulkActions\"\n\t\t\t:documentationUrl=\"documentationUrl\"\n\t\t\t:documentationLabel=\"documentationLabel || undefined\"\n\t\t\t@sortChange=\"$emit('sort-change', $event)\"\n\t\t\t@add=\"onAddClick\"\n\t\t\t@clearSelection=\"onSelect([])\"\n\t\t\t@toggleSidebar=\"sidebarOpen = !sidebarOpen\"\n\t\t\t@refresh=\"onRefreshEvent\"\n\t\t\t@headerAction=\"onHeaderAction\"\n\t\t\t@bulkAction=\"onBulkAction\"\n\t\t\t@showImport=\"showImportDialog = true\"\n\t\t\t@showExport=\"showExportDialog = true\"\n\t\t\t@showCopy=\"showMassCopyDialog = true\"\n\t\t\t@showDelete=\"showMassDeleteDialog = true\"\n\t\t\t@search=\"onSearchEvent\"\n\t\t\t@viewModeChange=\"onViewModeChange\">\n\t\t\t<template v-if=\"$slots['mass-actions']\" #mass-actions=\"{ count, selectedIds: ids }\">\n\t\t\t\t<slot name=\"mass-actions\" :count=\"count\" :selectedIds=\"ids\" />\n\t\t\t</template>\n\t\t\t<template v-if=\"$slots['action-items']\" #action-items>\n\t\t\t\t<slot name=\"action-items\" />\n\t\t\t</template>\n\t\t\t<template v-if=\"$slots['after-search']\" #after-search>\n\t\t\t\t<slot name=\"after-search\" />\n\t\t\t</template>\n\t\t\t<template\n\t\t\t\tv-if=\"$slots['selection-actions']\"\n\t\t\t\t#selection-actions=\"{ count, selectedIds: ids }\">\n\t\t\t\t<slot name=\"selection-actions\" :count=\"count\" :selectedIds=\"ids\" />\n\t\t\t</template>\n\t\t\t<template v-if=\"$slots['header-actions'] || $slots['actions']\" #actions>\n\t\t\t\t<!--\n\t\t\t\t\t@slot header-actions\n\t\t\t\t\t@description Extra buttons in the page header, inline next to the Add button. Documented since the beginning but wired up only later — consumers passing it (hermiq's flow list's \"New flow\" button) rendered nothing while the page looked fine.\n\t\t\t\t-->\n\t\t\t\t<slot name=\"header-actions\" />\n\t\t\t\t<slot name=\"actions\" />\n\t\t\t</template>\n\t\t\t<!-- Rendered AFTER the primary Add button (CnActionsBar's `actions-end`\n\t\t\t slot), not before it: these are \"browse/manage\" controls — saved\n\t\t\t views, export, page config — grouped together and kept apart from\n\t\t\t the app-specific buttons in `#actions`, with Add sitting between\n\t\t\t the two groups rather than before both (dossiq Cases/Queue). -->\n\t\t\t<template v-if=\"isEditMode || showExportMenu || allowSavedViews\" #actions-end>\n\t\t\t\t<!-- Saved views (opt-in via `allowSavedViews`): lists the user's\n\t\t\t\t OpenRegister saved-search views; applying one writes its stored\n\t\t\t\t filters/search/sort into the route query. -->\n\t\t\t\t<CnSavedViewsControl\n\t\t\t\t\tv-if=\"allowSavedViews\"\n\t\t\t\t\t:views=\"viewsForControl\"\n\t\t\t\t\t:loading=\"savedViewsLoading\"\n\t\t\t\t\t:currentUserId=\"currentSavedViewsUserId\"\n\t\t\t\t\t@apply=\"onApplySavedView\"\n\t\t\t\t\t@saveRequest=\"showSaveViewDialog = true\"\n\t\t\t\t\t@deleteRequest=\"onDeleteViewRequest\" />\n\t\t\t\t<!-- Native Export menu (opt-in via `allowExport` + schema.exportable):\n\t\t\t\t CSV/Excel entries navigate to OR's export-leaf URL, passing the\n\t\t\t\t current route's query params through as filters. -->\n\t\t\t\t<NcActions\n\t\t\t\t\tv-if=\"showExportMenu\"\n\t\t\t\t\t:forceName=\"true\"\n\t\t\t\t\t:menuName=\"t('nextcloud-vue', 'Export')\"\n\t\t\t\t\tdata-testid=\"cn-index-export-menu\"\n\t\t\t\t\t:aria-label=\"t('nextcloud-vue', 'Export')\">\n\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t<Export :size=\"20\" />\n\t\t\t\t\t</template>\n\t\t\t\t\t<NcActionButton data-testid=\"cn-index-export-csv\" @click=\"onExportClick('csv')\">\n\t\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t\t<Export :size=\"20\" />\n\t\t\t\t\t\t</template>\n\t\t\t\t\t\t{{ t('nextcloud-vue', 'Export as CSV') }}\n\t\t\t\t\t</NcActionButton>\n\t\t\t\t\t<NcActionButton data-testid=\"cn-index-export-excel\" @click=\"onExportClick('excel')\">\n\t\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t\t<Export :size=\"20\" />\n\t\t\t\t\t\t</template>\n\t\t\t\t\t\t{{ t('nextcloud-vue', 'Export as Excel') }}\n\t\t\t\t\t</NcActionButton>\n\t\t\t\t</NcActions>\n\t\t\t\t<!-- Edit-mode config cog: opens the page's full config editor\n\t\t\t\t (CnPageRenderer wires @configure to CnPageConfigModal). -->\n\t\t\t\t<NcButton v-if=\"isEditMode\"\n\t\t\t\t\tvariant=\"tertiary\"\n\t\t\t\t\t:aria-label=\"t('nextcloud-vue', 'Configure page')\"\n\t\t\t\t\t@click=\"$emit('configure')\">\n\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t<Cog :size=\"20\" />\n\t\t\t\t\t</template>\n\t\t\t\t</NcButton>\n\t\t\t</template>\n\t\t\t<!-- Quick-filter tabs (REQ-MIPFU-1) rendered INSIDE the action bar\n\t\t\t (between the view toggle and the actions) when the manifest\n\t\t\t declares `config.quickFilters`. Switching tabs re-fetches with\n\t\t\t the merged filter; @event quick-filter-change. -->\n\t\t\t<template v-if=\"tabStripEntries && tabStripEntries.length > 0\" #filters>\n\t\t\t\t<CnQuickFilterBar\n\t\t\t\t\tinline\n\t\t\t\t\t:tabs=\"tabStripEntries\"\n\t\t\t\t\t:mode=\"quickFilterMode\"\n\t\t\t\t\t:maxVisible=\"quickFilterMaxVisible\"\n\t\t\t\t\t:multiple=\"quickFilterMultiple\"\n\t\t\t\t\t:activeIndex=\"activeQuickFilterIndex\"\n\t\t\t\t\t:selectedIndices=\"selectedQuickFilterIndices\"\n\t\t\t\t\t@update:activeIndex=\"onQuickFilterChange\"\n\t\t\t\t\t@update:selectedIndices=\"onQuickFilterMultiChange\" />\n\t\t\t</template>\n\t\t</CnActionsBar>\n\n\t\t<!-- Quick edit: a few fields of one row, over the list, with the list\n\t\t keeping its place. -->\n\t\t<CnQuickEditDialog\n\t\t\tv-if=\"quickEditRow !== null\"\n\t\t\t:object=\"quickEditRow\"\n\t\t\t:schema=\"effectiveSchema\"\n\t\t\t:register=\"register\"\n\t\t\t:fields=\"quickEditFields\"\n\t\t\t:writableField=\"writableField\"\n\t\t\t:serverObject=\"quickEditServerRow\"\n\t\t\t@save=\"onQuickEditSave\"\n\t\t\t@keepTheirs=\"onQuickEditKeepTheirs\"\n\t\t\t@close=\"quickEditRow = null\" />\n\n\t\t<!-- The help key's sheet. Every shortcut the list offers is here and in\n\t\t the command palette: one that only the handler knows about is one\n\t\t nobody can find. -->\n\t\t<CnConfirmDialog\n\t\t\tv-if=\"showShortcutHelp\"\n\t\t\t:name=\"shortcutHelpTitle\"\n\t\t\t:confirmLabel=\"closeLabel\"\n\t\t\tdata-testid=\"cn-list-shortcuts-help\"\n\t\t\t@confirm=\"showShortcutHelp = false\"\n\t\t\t@cancel=\"showShortcutHelp = false\">\n\t\t\t<dl class=\"cn-index-page__shortcuts\">\n\t\t\t\t<template v-for=\"entry in shortcutHelpEntries\" :key=\"entry.id\">\n\t\t\t\t\t<dt><kbd>{{ entry.keys }}</kbd></dt>\n\t\t\t\t\t<dd>{{ entry.description }}</dd>\n\t\t\t\t</template>\n\t\t\t</dl>\n\t\t</CnConfirmDialog>\n\n\t\t<!-- Mass delete dialog -->\n\t\t<CnMassDeleteDialog\n\t\t\tv-if=\"showMassDeleteDialog\"\n\t\t\tref=\"massDeleteDialog\"\n\t\t\t:items=\"selectedObjects\"\n\t\t\t:nameField=\"massActionNameField\"\n\t\t\t:nameFormatter=\"nameFormatter\"\n\t\t\t@confirm=\"onMassDeleteConfirm\"\n\t\t\t@close=\"showMassDeleteDialog = false\" />\n\n\t\t<!-- Mass copy dialog -->\n\t\t<CnMassCopyDialog\n\t\t\tv-if=\"showMassCopyDialog\"\n\t\t\tref=\"massCopyDialog\"\n\t\t\t:items=\"selectedObjects\"\n\t\t\t:nameField=\"massActionNameField\"\n\t\t\t:nameFormatter=\"nameFormatter\"\n\t\t\t@confirm=\"onMassCopyConfirm\"\n\t\t\t@close=\"showMassCopyDialog = false\" />\n\n\t\t<!-- Mass export dialog -->\n\t\t<CnMassExportDialog\n\t\t\tv-if=\"showExportDialog\"\n\t\t\tref=\"exportDialog\"\n\t\t\t:formats=\"exportFormats\"\n\t\t\t@confirm=\"onMassExportConfirm\"\n\t\t\t@close=\"showExportDialog = false\" />\n\n\t\t<!-- Mass import dialog -->\n\t\t<CnMassImportDialog\n\t\t\tv-if=\"showImportDialog\"\n\t\t\tref=\"importDialog\"\n\t\t\t:options=\"importOptions\"\n\t\t\t@confirm=\"onMassImportConfirm\"\n\t\t\t@close=\"showImportDialog = false\">\n\t\t\t<template v-if=\"$slots['import-fields']\" #fields=\"{ file }\">\n\t\t\t\t<slot name=\"import-fields\" :file=\"file\" />\n\t\t\t</template>\n\t\t</CnMassImportDialog>\n\n\t\t<!-- Save-current-view dialog (saved-views-ui) -->\n\t\t<CnSaveViewDialog\n\t\t\tv-if=\"showSaveViewDialog\"\n\t\t\tref=\"saveViewDialog\"\n\t\t\t@confirm=\"onSaveViewConfirm\"\n\t\t\t@close=\"showSaveViewDialog = false\" />\n\n\t\t<!-- Delete-saved-view confirm (saved-views-ui) -->\n\t\t<CnConfirmDialog\n\t\t\tv-if=\"viewPendingDelete\"\n\t\t\tref=\"deleteViewConfirmDialog\"\n\t\t\tvariant=\"error\"\n\t\t\t:dialogTitle=\"t('nextcloud-vue', 'Delete view')\"\n\t\t\t:message=\"deleteViewMessage\"\n\t\t\t:confirmLabel=\"t('nextcloud-vue', 'Delete')\"\n\t\t\t@confirm=\"onDeleteViewConfirm\"\n\t\t\t@close=\"viewPendingDelete = null\" />\n\n\t\t<!-- @slot delete-dialog Replace the single-item delete dialog. -->\n\t\t<!-- @binding {boolean} show Whether the delete dialog is currently visible. -->\n\t\t<!-- @binding {object} item The item targeted for deletion. -->\n\t\t<!-- @binding {Function} confirm Performs the delete — the same path the default dialog's `@confirm` runs. -->\n\t\t<!-- @binding {Function} close Closes the delete dialog. -->\n\t\t<!-- `show` and `confirm` are props for the same reason as `form-dialog`\n\t\t above: a manifest-mounted replacement receives props only. -->\n\t\t<slot\n\t\t\tname=\"delete-dialog\"\n\t\t\t:show=\"showSingleDeleteDialog\"\n\t\t\t:item=\"actionTargetItem\"\n\t\t\t:confirm=\"onSingleDeleteConfirm\"\n\t\t\t:close=\"closeSingleDelete\">\n\t\t\t<CnDeleteDialog\n\t\t\t\tv-if=\"showSingleDeleteDialog && actionTargetItem\"\n\t\t\t\tref=\"singleDeleteDialog\"\n\t\t\t\t:item=\"actionTargetItem\"\n\t\t\t\t:nameField=\"massActionNameField\"\n\t\t\t\t:nameFormatter=\"nameFormatter\"\n\t\t\t\t@confirm=\"onSingleDeleteConfirm\"\n\t\t\t\t@close=\"closeSingleDelete\" />\n\t\t</slot>\n\n\t\t<!-- @slot copy-dialog Replace the single-item copy dialog. -->\n\t\t<!-- @binding {boolean} show Whether the copy dialog is currently visible. -->\n\t\t<!-- @binding {object} item The item targeted for copy. -->\n\t\t<!-- @binding {Function} confirm Performs the copy — the same path the default dialog's `@confirm` runs. -->\n\t\t<!-- @binding {Function} close Closes the copy dialog. -->\n\t\t<!-- `show` and `confirm` are props for the same reason as `form-dialog`\n\t\t above: a manifest-mounted replacement receives props only. -->\n\t\t<slot\n\t\t\tname=\"copy-dialog\"\n\t\t\t:show=\"showSingleCopyDialog\"\n\t\t\t:item=\"actionTargetItem\"\n\t\t\t:confirm=\"onSingleCopyConfirm\"\n\t\t\t:close=\"closeSingleCopy\">\n\t\t\t<CnCopyDialog\n\t\t\t\tv-if=\"showSingleCopyDialog && actionTargetItem\"\n\t\t\t\tref=\"singleCopyDialog\"\n\t\t\t\t:item=\"actionTargetItem\"\n\t\t\t\t:nameField=\"massActionNameField\"\n\t\t\t\t:nameFormatter=\"nameFormatter\"\n\t\t\t\t@confirm=\"onSingleCopyConfirm\"\n\t\t\t\t@close=\"closeSingleCopy\" />\n\t\t</slot>\n\n\t\t<!-- @slot form-dialog Replace the create/edit form dialog (use CnFormDialog or CnAdvancedFormDialog). -->\n\t\t<!-- @binding {boolean} show Whether the form dialog is currently visible. -->\n\t\t<!-- @binding {?object} item The item being edited, or null in create mode. -->\n\t\t<!-- @binding {object} schema The effective JSON schema driving the form. -->\n\t\t<!-- @binding {Function} confirm Persists the form data through the page's own save path (store / self-store / createOverride) and emits `create`/`edit`. Call this instead of saving in the replacement dialog, so a create or edit made there behaves exactly like one made in the built-in dialog. Takes the complete object to save. List refresh is automatic on the self-fetch and `createOverride` paths; with the `store` prop, refresh is driven by the consumer's `create`/`edit` handler as usual. -->\n\t\t<!-- @binding {Function} close Closes the form dialog. -->\n\t\t<!-- @binding {Function} refresh Re-reads the list. For a replacement dialog that saves through its own endpoint rather than `confirm`, so the list still shows what it saved. -->\n\t\t<!--\n\t\t `confirm` is bound as a PROP, not left as an `@confirm` listener on\n\t\t the default child. A manifest-declared replacement is mounted by\n\t\t CnPageRenderer as `<component :is=… v-bind=\"slotProps\" />`, which\n\t\t binds props only — so a listener is unreachable from a manifest\n\t\t even in principle, and a replacement dialog could render and close\n\t\t but never save (openconnector#1150).\n\t\t-->\n\t\t<slot\n\t\t\tname=\"form-dialog\"\n\t\t\t:show=\"showFormDialogVisible\"\n\t\t\t:item=\"editItem\"\n\t\t\t:schema=\"effectiveSchema\"\n\t\t\t:confirm=\"onFormConfirm\"\n\t\t\t:close=\"closeFormDialog\"\n\t\t\t:refresh=\"onRefreshEvent\">\n\t\t\t<CnFormDialog\n\t\t\t\tv-if=\"showFormDialogVisible && !useAdvancedFormDialog\"\n\t\t\t\tref=\"formDialog\"\n\t\t\t\t:schema=\"effectiveSchema\"\n\t\t\t\t:item=\"editItem\"\n\t\t\t\t:register=\"register\"\n\t\t\t\t:excludeFields=\"excludeFields\"\n\t\t\t\t:includeFields=\"includeFields\"\n\t\t\t\t:fieldOverrides=\"fieldOverrides\"\n\t\t\t\t:nameField=\"massActionNameField\"\n\t\t\t\t:size=\"formSize\"\n\t\t\t\t:columns=\"formColumns\"\n\t\t\t\t:initialData=\"resolvedCreateDefaults\"\n\t\t\t\t@confirm=\"onFormConfirm\"\n\t\t\t\t@close=\"closeFormDialog\">\n\t\t\t\t<template v-if=\"$slots['form-fields']\" #form=\"scope\">\n\t\t\t\t\t<slot name=\"form-fields\" v-bind=\"scope\" />\n\t\t\t\t</template>\n\t\t\t</CnFormDialog>\n\t\t\t<CnAdvancedFormDialog\n\t\t\t\tv-if=\"showFormDialogVisible && useAdvancedFormDialog\"\n\t\t\t\tref=\"formDialog\"\n\t\t\t\t:schema=\"effectiveSchema\"\n\t\t\t\t:item=\"editItem\"\n\t\t\t\t:excludeFields=\"excludeFields\"\n\t\t\t\t:includeFields=\"includeFields\"\n\t\t\t\t:fieldOverrides=\"fieldOverrides\"\n\t\t\t\t:nameField=\"massActionNameField\"\n\t\t\t\t:initialValues=\"resolvedCreateDefaults\"\n\t\t\t\t@confirm=\"onFormConfirm\"\n\t\t\t\t@close=\"closeFormDialog\" />\n\t\t</slot>\n\n\t\t<!-- Body -->\n\t\t<div class=\"cn-index-page__body\" :class=\"{ 'cn-index-page__body--with-folders': folderSidebar }\">\n\t\t\t<!-- Optional folder navigation pane (opt-in via the `folderSidebar`\n\t\t\t config). Selecting a folder filters the list by the config's\n\t\t\t `filterField`; \"All\" clears it. -->\n\t\t\t<div v-if=\"folderSidebar\" class=\"cn-index-page__folder-pane\">\n\t\t\t\t<CnFolderSidebar\n\t\t\t\t\t:source=\"folderSidebarSource\"\n\t\t\t\t\t:folders=\"folderSidebarFolders\"\n\t\t\t\t\t:objects=\"effectiveObjects\"\n\t\t\t\t\t:groupBy=\"folderSidebar.groupBy || folderSidebar.field || ''\"\n\t\t\t\t\t:facetValues=\"folderSidebarFacetValues\"\n\t\t\t\t\t:partial=\"folderSidebarPartial\"\n\t\t\t\t\t:filesPath=\"folderSidebar.filesPath || '/'\"\n\t\t\t\t\t:selectedId=\"selectedFolderId\"\n\t\t\t\t\t:allLabel=\"folderSidebar.allLabel || undefined\"\n\t\t\t\t\t:title=\"folderSidebar.title || ''\"\n\t\t\t\t\t:idField=\"folderPassthroughIdField\"\n\t\t\t\t\t:nameField=\"folderPassthroughNameField\"\n\t\t\t\t\t:allowCreate=\"Boolean(folderSidebar.allowCreate)\"\n\t\t\t\t\t@select=\"onFolderSelect\"\n\t\t\t\t\t@create=\"$emit('folder-create', $event)\" />\n\t\t\t</div>\n\n\t\t\t<!-- The list. `v-show`, never `v-if`: the narrow layout hides it\n\t\t\t but must not unmount it, because unmounting throws away the\n\t\t\t scroll position, the selection and the loaded page, which is\n\t\t\t the whole thing the split view exists to keep. -->\n\t\t\t<div\n\t\t\t\tv-show=\"splitLayout !== 'detail'\"\n\t\t\t\tref=\"listScroll\"\n\t\t\t\tclass=\"cn-index-page__main\"\n\t\t\t\t:class=\"{\n\t\t\t\t\t'cn-index-page__main--map': currentViewMode === 'map',\n\t\t\t\t\t'cn-index-page__main--table': currentViewMode === 'table',\n\t\t\t\t\t'cn-index-page__main--split': splitLayout === 'split',\n\t\t\t\t}\"\n\t\t\t\t@scroll=\"onListScroll\">\n\t\t\t\t<!-- @slot before-collection Content rendered above the collection in EVERY view mode (table, cards, list, map) — e.g. a folder/group strip that must stay visually separate from the objects instead of masquerading as rows or cards. -->\n\t\t\t\t<slot name=\"before-collection\" />\n\n\t\t\t\t<!-- Loading state — initial fetch only; a background refresh keeps the table visible -->\n\t\t\t\t<div v-if=\"showInitialLoader\" class=\"cn-index-page__loading\">\n\t\t\t\t\t<!-- name gives NcLoadingIcon a non-empty aria-label (WCAG role-img-alt); empty name ships an unlabeled role=\"img\" -->\n\t\t\t\t\t<NcLoadingIcon :size=\"32\" :name=\"loadingText\" />\n\t\t\t\t</div>\n\n\t\t\t\t<!-- Empty state -->\n\t\t\t\t<div v-else-if=\"effectiveObjects.length === 0\" class=\"cn-index-page__empty\">\n\t\t\t\t\t<slot name=\"empty\">\n\t\t\t\t\t\t<NcEmptyContent :name=\"resolvedEmptyText\">\n\t\t\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t\t\t<CnIcon v-if=\"resolvedIcon\" :name=\"resolvedIcon\" :size=\"64\" />\n\t\t\t\t\t\t\t\t<DatabaseSearch v-else :size=\"64\" />\n\t\t\t\t\t\t\t</template>\n\t\t\t\t\t\t</NcEmptyContent>\n\t\t\t\t\t</slot>\n\t\t\t\t</div>\n\n\t\t\t\t<!-- Table view -->\n\t\t\t\t<CnDataTable\n\t\t\t\t\tv-else-if=\"currentViewMode === 'table'\"\n\t\t\t\t\t:schema=\"effectiveSchema\"\n\t\t\t\t\t:columns=\"tableColumns\"\n\t\t\t\t\t:rowIcon=\"rowIcon\"\n\t\t\t\t\t:rowIndicators=\"rowIndicators\"\n\t\t\t\t\t:rowIndicatorCap=\"rowIndicatorCap\"\n\t\t\t\t\t:rows=\"displayObjects\"\n\t\t\t\t\t:sortKey=\"effectiveSortKey\"\n\t\t\t\t\t:sortOrder=\"effectiveSortOrder\"\n\t\t\t\t\t:sortKeys=\"effectiveSortKeys\"\n\t\t\t\t\t:selectable=\"selectable\"\n\t\t\t\t\t:rowClickToView=\"rowClickOpens\"\n\t\t\t\t\t:selectedIds=\"internalSelectedIds\"\n\t\t\t\t\t:rowKey=\"rowKey\"\n\t\t\t\t\t:emptyText=\"emptyText\"\n\t\t\t\t\t:excludeColumns=\"excludeColumns\"\n\t\t\t\t\t:includeColumns=\"includeColumns\"\n\t\t\t\t\t:columnOverrides=\"columnOverrides\"\n\t\t\t\t\t:rowClass=\"rowClass\"\n\t\t\t\t\t@sort=\"onSortEvent\"\n\t\t\t\t\t@select=\"onSelect\"\n\t\t\t\t\t@rowClick=\"onRowClick\"\n\t\t\t\t\t@rowContextMenu=\"onRowContextMenu\">\n\t\t\t\t\t<!-- Pass through column slots -->\n\t\t\t\t\t<template\n\t\t\t\t\t\tv-for=\"col in slotColumns\"\n\t\t\t\t\t\t#[`column-${col}`]=\"{ row, value }\">\n\t\t\t\t\t\t<slot :name=\"'column-' + col\" :row=\"row\" :value=\"value\" />\n\t\t\t\t\t</template>\n\n\t\t\t\t\t<!-- Row actions -->\n\t\t\t\t\t<template v-if=\"hasRowActions\" #row-actions=\"{ row }\">\n\t\t\t\t\t\t<slot name=\"row-actions\" :row=\"row\">\n\t\t\t\t\t\t\t<CnRowActions\n\t\t\t\t\t\t\t\t:actions=\"rowActionsFor(row)\"\n\t\t\t\t\t\t\t\t:row=\"row\"\n\t\t\t\t\t\t\t\t@action=\"onRowAction\" />\n\t\t\t\t\t\t</slot>\n\t\t\t\t\t</template>\n\n\t\t\t\t\t<!-- Table-header filter + column menus (both opt-in): funnel and\n\t\t\t\t\t columns buttons above the row-actions column. The filter menu\n\t\t\t\t\t lists each enum column's values as toggleable facet filters;\n\t\t\t\t\t the column menu lists every governed column as a visibility\n\t\t\t\t\t checkbox — compact, in-table alternatives to the sidebar. -->\n\t\t\t\t\t<template\n\t\t\t\t\t\tv-if=\"(filterMenu && filterableFields.length) || (columnMenu && governedColumns.length)\"\n\t\t\t\t\t\t#actions-header>\n\t\t\t\t\t\t<NcActions\n\t\t\t\t\t\t\tv-if=\"filterMenu && filterableFields.length\"\n\t\t\t\t\t\t\t:forceMenu=\"true\"\n\t\t\t\t\t\t\t:aria-label=\"t('nextcloud-vue', 'Filter')\">\n\t\t\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t\t\t<FilterOutline :size=\"20\" />\n\t\t\t\t\t\t\t</template>\n\t\t\t\t\t\t\t<template v-for=\"field in filterableFields\" :key=\"field.key\">\n\t\t\t\t\t\t\t\t<NcActionCaption :name=\"field.label\" />\n\t\t\t\t\t\t\t\t<NcActionCheckbox\n\t\t\t\t\t\t\t\t\tv-for=\"val in field.values\"\n\t\t\t\t\t\t\t\t\t:key=\"`${field.key}-${val}`\"\n\t\t\t\t\t\t\t\t\t:modelValue=\"isFilterActive(field.key, val)\"\n\t\t\t\t\t\t\t\t\t@update:modelValue=\"toggleFilter(field.key, val)\">\n\t\t\t\t\t\t\t\t\t{{ val }}\n\t\t\t\t\t\t\t\t</NcActionCheckbox>\n\t\t\t\t\t\t\t</template>\n\t\t\t\t\t\t</NcActions>\n\t\t\t\t\t\t<NcActions\n\t\t\t\t\t\t\tv-if=\"columnMenu && governedColumns.length\"\n\t\t\t\t\t\t\t:forceMenu=\"true\"\n\t\t\t\t\t\t\t:aria-label=\"t('nextcloud-vue', 'Columns')\">\n\t\t\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t\t\t<ViewColumnOutline :size=\"20\" />\n\t\t\t\t\t\t\t</template>\n\t\t\t\t\t\t\t<NcActionCaption :name=\"t('nextcloud-vue', 'Columns')\" />\n\t\t\t\t\t\t\t<NcActionCheckbox\n\t\t\t\t\t\t\t\tv-for=\"col in governedColumns\"\n\t\t\t\t\t\t\t\t:key=\"`col-${col.key}`\"\n\t\t\t\t\t\t\t\t:modelValue=\"isColumnVisible(col.key)\"\n\t\t\t\t\t\t\t\t@update:modelValue=\"toggleColumn(col.key)\">\n\t\t\t\t\t\t\t\t{{ cnTranslate(col.label || col.key) }}\n\t\t\t\t\t\t\t</NcActionCheckbox>\n\t\t\t\t\t\t</NcActions>\n\t\t\t\t\t</template>\n\t\t\t\t</CnDataTable>\n\n\t\t\t\t<!-- Map view — plots the CURRENT filtered rows (displayObjects) as\n\t\t\t\t inline markers; no separate fetch path, so it reuses the same\n\t\t\t\t filter / sidebar / quick-filter machinery as table and cards.\n\t\t\t\t A marker click routes back through onRowClick for identical\n\t\t\t\t detail-page navigation. -->\n\t\t\t\t<CnMapWidget\n\t\t\t\t\tv-else-if=\"currentViewMode === 'map'\"\n\t\t\t\t\tclass=\"cn-index-page__map\"\n\t\t\t\t\t:center=\"mapCenter\"\n\t\t\t\t\t:layers=\"mapLayers\"\n\t\t\t\t\t:basemaps=\"mapBasemaps\"\n\t\t\t\t\t:markers=\"mapMarkers\"\n\t\t\t\t\t:clustering=\"mapClustering\"\n\t\t\t\t\t:autoFit=\"true\"\n\t\t\t\t\theight=\"100%\"\n\t\t\t\t\t@markerClick=\"onMarkerClick\" />\n\n\t\t\t\t<!--\n\t\t\t\t\tBoard view. The columns are the schema's stages and a move\n\t\t\t\t\tgoes through the host's transition; CnBoardView never\n\t\t\t\t\twrites the status field, so with no runTransition it is\n\t\t\t\t\tread-only rather than broken.\n\t\t\t\t-->\n\t\t\t\t<CnBoardView\n\t\t\t\t\tv-else-if=\"currentViewMode === 'board'\"\n\t\t\t\t\t:rows=\"displayObjects\"\n\t\t\t\t\t:statusFieldSchema=\"boardStatusFieldSchema\"\n\t\t\t\t\t:statusField=\"board.statusField || 'status'\"\n\t\t\t\t\t:cardFields=\"board.cardFields || []\"\n\t\t\t\t\t:swimlaneField=\"board.swimlaneField || ''\"\n\t\t\t\t\t:rowKey=\"rowKey\"\n\t\t\t\t\t:runTransition=\"runTransition\"\n\t\t\t\t\t:paged=\"isPaged\"\n\t\t\t\t\t@cardClick=\"onRowClick\"\n\t\t\t\t\t@moved=\"onBoardMoved\" />\n\n\t\t\t\t<!--\n\t\t\t\t\tDate axis. Reads only: nothing here reschedules, because a\n\t\t\t\t\tview that moved a bar on drag would be changing statutory\n\t\t\t\t\tdates from a picture.\n\t\t\t\t-->\n\t\t\t\t<CnDateAxisView\n\t\t\t\t\tv-else-if=\"currentViewMode === 'dateAxis'\"\n\t\t\t\t\t:rows=\"displayObjects\"\n\t\t\t\t\t:startField=\"dateAxis.startField || ''\"\n\t\t\t\t\t:endField=\"dateAxis.endField || ''\"\n\t\t\t\t\t:laneField=\"dateAxis.laneField || ''\"\n\t\t\t\t\t:labelField=\"dateAxis.labelField || ''\"\n\t\t\t\t\t:rowKey=\"rowKey\"\n\t\t\t\t\t@rowClick=\"onRowClick\" />\n\n\t\t\t\t<!-- List view -->\n\t\t\t\t<CnObjectList\n\t\t\t\t\tv-else-if=\"currentViewMode === 'list'\"\n\t\t\t\t\t:objects=\"displayObjects\"\n\t\t\t\t\t:schema=\"effectiveSchema\"\n\t\t\t\t\t:config=\"listConfig\"\n\t\t\t\t\t:selectable=\"selectable\"\n\t\t\t\t\t:selectedIds=\"internalSelectedIds\"\n\t\t\t\t\t:rowKey=\"rowKey\"\n\t\t\t\t\t:emptyText=\"emptyText\"\n\t\t\t\t\t@click=\"onRowClick\"\n\t\t\t\t\t@select=\"onSelect\">\n\t\t\t\t\t<!--\n\t\t\t\t\t\tList-item slot resolution priority (highest first):\n\t\t\t\t\t\t1. Parent-provided `#list-item` scoped slot — App.vue overrides win.\n\t\t\t\t\t\t2. `listComponent` prop (or manifest `pages[].config.listComponent`)\n\t\t\t\t\t\t resolved against the customComponents registry.\n\t\t\t\t\t\t3. CnObjectList's default CnObjectRow.\n\t\t\t\t\t-->\n\t\t\t\t\t<template v-if=\"$slots['list-item']\" #list-item=\"{ object, selected }\">\n\t\t\t\t\t\t<slot name=\"list-item\" :object=\"object\" :selected=\"selected\" />\n\t\t\t\t\t</template>\n\t\t\t\t\t<template v-else-if=\"resolvedListComponent\" #list-item=\"{ object, selected }\">\n\t\t\t\t\t\t<component\n\t\t\t\t\t\t\t:is=\"resolvedListComponent\"\n\t\t\t\t\t\t\t:item=\"object\"\n\t\t\t\t\t\t\t:object=\"object\"\n\t\t\t\t\t\t\t:schema=\"effectiveSchema\"\n\t\t\t\t\t\t\t:register=\"register\"\n\t\t\t\t\t\t\t:selected=\"selected\"\n\t\t\t\t\t\t\t@click=\"(...args) => onRowClick(object, args.find(isDomEvent))\"\n\t\t\t\t\t\t\t@auxclick=\"onCustomItemAuxClick(object, $event)\"\n\t\t\t\t\t\t\t@select=\"onSelect(toggleIdInArray(internalSelectedIds, object[rowKey]))\" />\n\t\t\t\t\t</template>\n\t\t\t\t\t<!-- Per-part row slots (list view only): forwarded to CnObjectRow\n\t\t\t\t\t so an app can override just the leading icon or the badge\n\t\t\t\t\t while keeping the config-driven title/subtitle. -->\n\t\t\t\t\t<template v-if=\"$slots['row-icon']\" #row-icon=\"{ object }\">\n\t\t\t\t\t\t<slot name=\"row-icon\" :object=\"object\" />\n\t\t\t\t\t</template>\n\t\t\t\t\t<template v-if=\"$slots['row-badges']\" #row-badges=\"{ object }\">\n\t\t\t\t\t\t<slot name=\"row-badges\" :object=\"object\" />\n\t\t\t\t\t</template>\n\t\t\t\t\t<template v-if=\"hasRowActions || $slots['row-actions']\" #row-actions=\"{ object }\">\n\t\t\t\t\t\t<slot name=\"row-actions\" :row=\"object\">\n\t\t\t\t\t\t\t<CnRowActions\n\t\t\t\t\t\t\t\t:actions=\"rowActionsFor(object)\"\n\t\t\t\t\t\t\t\t:row=\"object\"\n\t\t\t\t\t\t\t\t@action=\"onRowAction\" />\n\t\t\t\t\t\t</slot>\n\t\t\t\t\t</template>\n\t\t\t\t</CnObjectList>\n\n\t\t\t\t<!-- Card view -->\n\t\t\t\t<CnCardGrid\n\t\t\t\t\tv-else\n\t\t\t\t\t:objects=\"displayObjects\"\n\t\t\t\t\t:schema=\"effectiveSchema\"\n\t\t\t\t\t:selectable=\"selectable\"\n\t\t\t\t\t:clickToView=\"rowClickOpens\"\n\t\t\t\t\t:selectedIds=\"internalSelectedIds\"\n\t\t\t\t\t:rowKey=\"rowKey\"\n\t\t\t\t\t:emptyText=\"emptyText\"\n\t\t\t\t\t@click=\"onRowClick\"\n\t\t\t\t\t@select=\"onSelect\">\n\t\t\t\t\t<!--\n\t\t\t\t\t\tCard slot resolution priority (highest first):\n\t\t\t\t\t\t1. Parent-provided `#card` scoped slot — App.vue overrides win.\n\t\t\t\t\t\t2. `cardComponent` prop (or manifest `pages[].config.cardComponent`)\n\t\t\t\t\t\t resolved against the customComponents registry.\n\t\t\t\t\t\t3. CnCardGrid's default CnObjectCard.\n\t\t\t\t\t-->\n\t\t\t\t\t<template v-if=\"$slots.card\" #card=\"{ object, selected }\">\n\t\t\t\t\t\t<slot name=\"card\" :object=\"object\" :selected=\"selected\" />\n\t\t\t\t\t</template>\n\t\t\t\t\t<template v-else-if=\"resolvedCardComponent\" #card=\"{ object, selected }\">\n\t\t\t\t\t\t<component\n\t\t\t\t\t\t\t:is=\"resolvedCardComponent\"\n\t\t\t\t\t\t\t:item=\"object\"\n\t\t\t\t\t\t\t:object=\"object\"\n\t\t\t\t\t\t\t:schema=\"effectiveSchema\"\n\t\t\t\t\t\t\t:register=\"register\"\n\t\t\t\t\t\t\t:selected=\"selected\"\n\t\t\t\t\t\t\t@click=\"(...args) => onRowClick(object, args.find(isDomEvent))\"\n\t\t\t\t\t\t\t@auxclick=\"onCustomItemAuxClick(object, $event)\"\n\t\t\t\t\t\t\t@select=\"onSelect(toggleIdInArray(internalSelectedIds, object[rowKey]))\" />\n\t\t\t\t\t</template>\n\t\t\t\t\t<template v-if=\"hasRowActions\" #card-actions=\"{ object }\">\n\t\t\t\t\t\t<slot name=\"row-actions\" :row=\"object\">\n\t\t\t\t\t\t\t<CnRowActions\n\t\t\t\t\t\t\t\t:actions=\"rowActionsFor(object)\"\n\t\t\t\t\t\t\t\t:row=\"object\"\n\t\t\t\t\t\t\t\t@action=\"onRowAction\" />\n\t\t\t\t\t\t</slot>\n\t\t\t\t\t</template>\n\t\t\t\t</CnCardGrid>\n\n\t\t\t\t<!-- Right-click context menu (positioned at cursor via CSS) -->\n\t\t\t\t<CnContextMenu\n\t\t\t\t\tv-model:open=\"contextMenuOpen\"\n\t\t\t\t\t:actions=\"mergedActions\"\n\t\t\t\t\t:targetItem=\"contextMenuRow\"\n\t\t\t\t\t@action=\"onRowAction\"\n\t\t\t\t\t@close=\"closeContextMenu\" />\n\n\t\t\t\t<!-- Pagination -->\n\t\t\t\t<CnPagination\n\t\t\t\t\tv-if=\"effectivePagination && effectivePagination.pages > 1\"\n\t\t\t\t\t:currentPage=\"effectivePagination.page || 1\"\n\t\t\t\t\t:totalPages=\"effectivePagination.pages || 1\"\n\t\t\t\t\t:totalItems=\"effectivePagination.total || 0\"\n\t\t\t\t\t:currentPageSize=\"effectivePagination.limit || 20\"\n\t\t\t\t\tclass=\"cn-index-page__pagination\"\n\t\t\t\t\t@pageChanged=\"onPageEvent\"\n\t\t\t\t\t@pageSizeChanged=\"$emit('page-size-changed', $event)\" />\n\t\t\t</div>\n\n\t\t\t<!-- The open record, beside the list on a wide screen and instead\n\t\t\t of it on a narrow one. Same slot either way, and the host\n\t\t\t mounts the same detail component in it that the full route\n\t\t\t mounts: two detail implementations drift within a month. -->\n\t\t\t<div\n\t\t\t\tv-if=\"splitLayout !== 'list'\"\n\t\t\t\tclass=\"cn-index-page__split-pane\"\n\t\t\t\t:class=\"{ 'cn-index-page__split-pane--full': splitLayout === 'detail' }\"\n\t\t\t\t:style=\"splitLayout === 'split' ? { width: splitPaneWidth } : null\"\n\t\t\t\tdata-testid=\"cn-index-page-split-pane\"\n\t\t\t\t:data-split-layout=\"splitLayout\">\n\t\t\t\t<!-- An <a>, not a button: closing the pane pushes the list's\n\t\t\t\t route, so the browser supplies middle-click and open-in-new-tab\n\t\t\t\t while `.prevent` keeps the plain click in-app. -->\n\t\t\t\t<a\n\t\t\t\t\tv-if=\"splitCloseVisible\"\n\t\t\t\t\tclass=\"cn-index-page__split-close\"\n\t\t\t\t\t:href=\"splitCloseHref\"\n\t\t\t\t\t:title=\"splitCloseTitle\"\n\t\t\t\t\tdata-testid=\"cn-index-page-split-close\"\n\t\t\t\t\t@click.prevent=\"closeSplitPane\">\n\t\t\t\t\t<Close :size=\"16\" />\n\t\t\t\t\t<span>{{ splitCloseTitle }}</span>\n\t\t\t\t</a>\n\t\t\t\t<!-- @slot split-pane The open record, rendered beside the list. Mount the SAME detail component the full route mounts. -->\n\t\t\t\t<!-- @binding {string} id The record the address names. -->\n\t\t\t\t<!-- @binding {string} layout Either `split` (beside the list) or `detail` (the full page, below the breakpoint). -->\n\t\t\t\t<!-- @binding {Function} close Closes the pane and returns to the list, at the list's own address. -->\n\t\t\t\t<!-- @binding {Function} saved Call with the saved record to replace its row in the list without refetching the page. -->\n\t\t\t\t<slot\n\t\t\t\t\t:id=\"splitId\"\n\t\t\t\t\tname=\"split-pane\"\n\t\t\t\t\t:layout=\"splitLayout\"\n\t\t\t\t\t:close=\"closeSplitPane\"\n\t\t\t\t\t:saved=\"onSplitPaneSaved\" />\n\t\t\t</div>\n\t\t</div>\n\n\t\t<!-- Manifest-driven sidebar — auto-mounted when sidebar.enabled\n\t\t AND sidebar.show !== false.\n\n\t\t When CnAppRoot is the host, the sidebar config is\n\t\t PUBLISHED to the `cnIndexSidebarConfig` provide (see\n\t\t mounted/beforeDestroy), and CnAppRoot mounts the\n\t\t CnIndexSidebar at NcContent level — the only place where\n\t\t Nextcloud's NcAppSidebar slides correctly from the right.\n\n\t\t When no CnAppRoot ancestor exists (legacy apps mounting\n\t\t CnIndexPage standalone), the inject default is the no-op\n\t\t `{ value: null }` and we fall back to inline rendering\n\t\t here so the legacy contract still works. -->\n\t\t<CnIndexSidebar\n\t\t\tv-if=\"shouldRenderInlineSidebar\"\n\t\t\t:open=\"sidebarOpen\"\n\t\t\t:schema=\"effectiveSchema\"\n\t\t\t:title=\"title\"\n\t\t\t:icon=\"resolvedIcon\"\n\t\t\t:searchValue=\"effectiveSearchValue\"\n\t\t\t:visibleColumns=\"effectiveVisibleColumns\"\n\t\t\t:activeFilters=\"effectiveActiveFilters\"\n\t\t\t:columnGroups=\"resolvedSidebar.columnGroups || []\"\n\t\t\t:filterFields=\"resolvedSidebar.fields || null\"\n\t\t\t:facetData=\"effectiveFacetData\"\n\t\t\t:showMetadata=\"resolvedSidebar.showMetadata !== false\"\n\t\t\tv-bind=\"sidebarSearchProps\"\n\t\t\t@update:open=\"sidebarOpen = $event\"\n\t\t\t@search=\"onSearchEvent\"\n\t\t\t@columnsChange=\"onColumnsEvent\"\n\t\t\t@filterChange=\"onFilterEvent\"\n\t\t\t@clearFilters=\"onClearFilters\" />\n\t</div>\n</template>\n\n<script>\nimport { getCurrentUser } from '@nextcloud/auth'\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcActionButton, NcActionCaption, NcActionCheckbox, NcActions, NcButton, NcEmptyContent, NcLoadingIcon } from '@nextcloud/vue'\nimport { getCurrentInstance, inject, markRaw, ref } from 'vue'\nimport Close from 'vue-material-design-icons/Close.vue'\nimport Cog from 'vue-material-design-icons/Cog.vue'\nimport DatabaseSearch from 'vue-material-design-icons/DatabaseSearch.vue'\nimport Export from 'vue-material-design-icons/Export.vue'\nimport Eye from 'vue-material-design-icons/Eye.vue'\nimport FilterOutline from 'vue-material-design-icons/FilterOutline.vue'\nimport ViewColumnOutline from 'vue-material-design-icons/ViewColumnOutline.vue'\nimport CnConfirmDialog from '../../dialogs/CnConfirmDialog.vue'\nimport CnQuickEditDialog from '../../dialogs/CnQuickEditDialog.vue'\nimport { useContextMenu } from '../../composables/index.js'\nimport { useSavedViewsApi } from '../../composables/useSavedViewsApi.js'\nimport { METADATA_COLUMNS } from '../../constants/metadata.js'\nimport { routeHref } from '../../utils/actionLink.js'\nimport { buildOnSuccessRoute, resolveRegisteredHandler } from '../../utils/actionsDispatcher.js'\nimport { buildExportUrl } from '../../utils/indexExportHelpers.js'\nimport { openRowTarget } from '../../utils/linkNavigation.js'\nimport { resolveClaimedTeams, resolveClaimTokens, splitViewsIntoTabs, viewAsTab } from '../../utils/listLenses.js'\nimport { LIST_SHORTCUTS, listPaletteCommands, shortcutFor } from '../../utils/listShortcuts.js'\nimport { multiKeySort } from '../../utils/multiKeySort.js'\nimport { resolveDeepTokens, resolveFilterValue } from '../../utils/resolveFilterTokens.js'\nimport { availableRowActions, DEFAULT_ROW_ACTION_FIELD, refusalReasonFor, undeclaredRowActions } from '../../utils/rowActionAvailability.js'\nimport { isNewTabClick, isRowMiddleClick, markNewTabHandled } from '../../utils/rowAuxClick.js'\nimport { DEFAULT_ROW_INDICATOR_CAP } from '../../utils/rowIndicators.js'\nimport { buildRouteQueryFromViewState, buildViewCreatePayload, extractViewState, extractViewStateFromRouteQuery, savedViewScope, viewMatchesScope } from '../../utils/savedViewHelpers.js'\nimport { columnsFromSchema } from '../../utils/schema.js'\nimport { resolveScopeLayout } from '../../utils/scopeListLayout.js'\nimport { CnActionsBar } from '../CnActionsBar/index.js'\nimport { CnAdvancedFormDialog } from '../CnAdvancedFormDialog/index.js'\nimport { CnBoardView } from '../CnBoardView/index.js'\nimport { CnCardGrid } from '../CnCardGrid/index.js'\nimport { CnContextMenu } from '../CnContextMenu/index.js'\nimport { CnCopyDialog } from '../CnCopyDialog/index.js'\nimport { CnDataTable } from '../CnDataTable/index.js'\nimport { CnDateAxisView } from '../CnDateAxisView/index.js'\nimport { CnDeleteDialog } from '../CnDeleteDialog/index.js'\nimport { CnFolderSidebar } from '../CnFolderSidebar/index.js'\nimport { CnFormDialog } from '../CnFormDialog/index.js'\nimport { CnIcon } from '../CnIcon/index.js'\nimport { CnIndexSidebar } from '../CnIndexSidebar/index.js'\nimport { CnMapWidget } from '../CnMapWidget/index.js'\nimport { CnMassCopyDialog } from '../CnMassCopyDialog/index.js'\nimport { CnMassDeleteDialog } from '../CnMassDeleteDialog/index.js'\nimport { CnMassExportDialog } from '../CnMassExportDialog/index.js'\nimport { CnMassImportDialog } from '../CnMassImportDialog/index.js'\nimport { CnObjectList } from '../CnObjectList/index.js'\nimport { CnPageHeader } from '../CnPageHeader/index.js'\nimport { CnPagination } from '../CnPagination/index.js'\nimport { CnQuickFilterBar } from '../CnQuickFilterBar/index.js'\nimport { CnRowActions } from '../CnRowActions/index.js'\nimport { CnSavedViewsControl } from '../CnSavedViewsControl/index.js'\nimport { CnSaveViewDialog } from '../CnSaveViewDialog/index.js'\nimport { applyAiContext } from './aiContext.js'\nimport { buildDefaultActions } from './defaultActions.js'\nimport { dispatchAction } from './manifestActionDispatch.js'\nimport { applyManualOrder, dropInOrder, manualOrderKey, moveInOrder, visibleIdsOf } from './manualOrder.js'\nimport { createSelfModeActions } from './selfModeActions.js'\nimport { applyRowPatches, normalisePaneWidth, rowIdOf, splitLayoutFor } from './splitView.js'\nimport { useNamedSource } from './useNamedSource.js'\nimport { useSelfFetchList } from './useSelfFetchList.js'\n\n/**\n * The separator a date-range filter uses in the URL: `2026-09-21..2026-09-25`.\n *\n * One query parameter per FIELD, not per bound, so the link reads as the\n * question the person asked and an open-ended window still round-trips\n * (`..2026-09-25` is \"due before Friday\").\n *\n * @type {string}\n */\nconst RANGE_SEPARATOR = '..'\n\n/**\n * Whether a schema property wants a from/to pair rather than a value list.\n *\n * @param {object} prop The schema property.\n *\n * @return {boolean} True for a range control.\n */\nfunction isRangeControl(prop) {\n\tconst control = String((prop && prop.inputControl) || '')\n\treturn control === 'date-range' || control === 'range'\n}\n\n/**\n * The filter declarations a page carries on its sidebar config.\n *\n * `sidebar.fields` and not `config.schema`: the schema key names the\n * OpenRegister schema a page self-fetches from, and a named source has no\n * register or schema to point at. The manifest schema types it as a string\n * for exactly that reason.\n *\n * @param {object} props The CnIndexPage props.\n *\n * @return {object} `{ propertyName: declaration }`, empty when none.\n */\nfunction declaredFilterFields(props) {\n\tconst fields = props.sidebar && props.sidebar.fields\n\treturn (fields && typeof fields === 'object') ? fields : {}\n}\n\n/**\n * Read a named-source page's sidebar filters back out of `$route.query`.\n *\n * Only properties the page's own schema declares `facetable` are read, so an\n * unrelated query parameter (`?action=create`, a saved-view key) never\n * becomes a filter, and a hand-edited link cannot invent a field the source\n * has no argument for.\n *\n * @param {import('vue').ComponentInternalInstance|null} instance The instance, for `$route`.\n * @param {object} props The CnIndexPage props.\n *\n * @return {object} The `{ fieldKey: values }` map, empty when the link carries none.\n */\nfunction namedFiltersFromRoute(instance, props) {\n\tif (!props.entitySource) {\n\t\treturn {}\n\t}\n\tconst route = instance && instance.proxy && instance.proxy.$route\n\tconst query = (route && route.query) || {}\n\tconst properties = declaredFilterFields(props)\n\tconst out = {}\n\n\tfor (const [key, prop] of Object.entries(properties)) {\n\t\tif (!prop || prop.facetable !== true) {\n\t\t\tcontinue\n\t\t}\n\t\tconst raw = query[key]\n\t\tif (raw === undefined || raw === null || raw === '') {\n\t\t\tcontinue\n\t\t}\n\t\tconst value = Array.isArray(raw) ? raw.join(',') : String(raw)\n\n\t\tif (isRangeControl(prop)) {\n\t\t\tconst [from, to] = value.split(RANGE_SEPARATOR)\n\t\t\tconst range = {}\n\t\t\tif (from) {\n\t\t\t\trange.from = from\n\t\t\t}\n\t\t\tif (to) {\n\t\t\t\trange.to = to\n\t\t\t}\n\t\t\tif (Object.keys(range).length > 0) {\n\t\t\t\tout[key] = range\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tconst values = value.split(',').filter((entry) => entry !== '')\n\t\tif (values.length > 0) {\n\t\t\tout[key] = values\n\t\t}\n\t}\n\n\treturn out\n}\n\n/**\n * The query-parameter spelling of one named-source filter value.\n *\n * @param {unknown} values The chosen values, or a `{ from, to }` range.\n *\n * @return {string} The parameter value, or '' when nothing is chosen.\n */\nfunction namedFilterToQuery(values) {\n\tif (values && typeof values === 'object' && !Array.isArray(values)) {\n\t\tconst from = values.from || ''\n\t\tconst to = values.to || ''\n\t\treturn (from === '' && to === '') ? '' : `${from}${RANGE_SEPARATOR}${to}`\n\t}\n\tconst list = (Array.isArray(values) ? values : [values])\n\t\t.map((entry) => ((entry && typeof entry === 'object' && 'id' in entry) ? entry.id : entry))\n\t\t.filter((entry) => entry !== undefined && entry !== null && String(entry) !== '')\n\treturn list.join(',')\n}\n\n/**\n * CnIndexPage — Top-level schema-driven index page component.\n *\n * Assembles sub-components (CnPageHeader, CnActionsBar, table, cards,\n * pagination, mass actions, single-object dialogs) into a single\n * zero-config page.\n *\n * Dialogs are overridable via named slots:\n * - `#form-dialog` — Replace the create/edit dialog entirely\n * - `#delete-dialog` — Replace the single-item delete dialog\n * - `#copy-dialog` — Replace the single-item copy dialog\n * - `#form-fields` — Replace only the form content inside the built-in form dialog (CnFormDialog only)\n *\n * Use the `useAdvancedFormDialog` prop to use CnAdvancedFormDialog for create/edit (properties table, JSON tab, optional metadata).\n *\n * Multi-column sort (self-fetch mode: `register` + `schema`, no external\n * `objects`): shift+click a second/third sortable header in the embedded\n * CnDataTable to build a priority-ordered sort. The active key list is\n * translated into OpenRegister's `_order` query param (`{field: 'asc'|\n * 'desc', ...}`, key order = priority — the exact shape `ObjectsController::\n * normalizeOrderParameter` / `MagicMapper` consume) and persisted in\n * `$route.query._order` (JSON-encoded), so a reload or a shared link\n * restores the same sort. Host-controlled (non-self-fetch) pages pass their\n * own `sortKeys` prop instead.\n *\n * Minimal usage (auto-generated dialogs from schema)\n * ```vue\n * <CnIndexPage\n * title=\"Clients\"\n * :schema=\"effectiveSchema\"\n * :objects=\"clients\"\n * :pagination=\"effectivePagination\"\n * :loading=\"loading\"\n * @create=\"onCreate\"\n * @edit=\"onEdit\"\n * @delete=\"onDelete\"\n * @refresh=\"fetchClients\"\n * @row-click=\"openClient\"\n * @page-changed=\"onPage\" />\n * ```\n *\n * With custom form dialog\n * ```vue\n * <CnIndexPage ...>\n * <template #form-dialog=\"{ item, schema, confirm, close }\">\n * <MyCustomFormDialog :item=\"item\" @save=\"confirm\" @close=\"close\" />\n * </template>\n * </CnIndexPage>\n * ```\n *\n * @event {void} add — Add button clicked (backward compat, only if listener attached)\n * @event {object} create — Form dialog create confirmed. Payload: formData object\n * @event {object} edit — Form dialog edit confirmed. Payload: formData object (includes id)\n * @event {string} delete — Single delete confirmed. Payload: item ID\n * @event {{ id: string, newName: string }} copy — Single copy confirmed\n * @event {string[]} mass-delete — Mass delete confirmed. Payload: array of IDs\n * @event {object} mass-copy — Mass copy confirmed. Payload: { ids, pattern }\n * @event {object} mass-export — Mass export confirmed. Payload: { ids, format }\n * @event {object} mass-import — Mass import confirmed. Payload: import data\n * @event {void} refresh — Refresh button clicked\n * @event {object} row-click — Table row or card clicked or middle-clicked. Payload: (row, event) — the row object and the native click/auxclick event\n * @event {{ key: string, order: string }} sort — Column sort changed\n * @event {number} page-changed — Pagination page changed\n * @event {number} page-size-changed — Pagination page size changed\n * @event {string[]} select — Selection changed. Payload: array of selected IDs\n * @event {object} action — Row action triggered. Payload: { action, row }\n * @event {{ action: string, id: string, selectedIds: Array, count: number }} bulk-action — A declarative bulk action was triggered from the selection strip. The SELECTION travels with it: an action that has to go and find out what was selected is one re-render away from acting on a different set than the user saw highlighted.\n * @event {{ target: string, props: object }} open-modal — A bulk action of type `open-modal` asks the host to open a registered modal. `props` carries `selectedIds` and `count` merged under the action's own props.\n * @event {object} apply-view — A saved view was applied (saved-views-ui). Payload: the View API object. Only emitted when `allowSavedViews`.\n * @event {string} search — Search input changed in the embedded sidebar. Only emitted when `sidebar.enabled`.\n * @event {string[]} columns-change — Visible columns changed in the embedded sidebar. Only emitted when `sidebar.enabled`.\n * @event {{ key: string, values: Array<unknown> }} filter-change — Facet filter changed in the embedded sidebar. Only emitted when `sidebar.enabled`.\n * @event {void} clear-filters — \"Clear all\" clicked in the embedded sidebar. Search, every active filter and the folder-sidebar selection are reset (self-fetch mode); a consumer-managed page gets the bare event to handle itself.\n *\n * @slot mass-actions — Extra mass action buttons (shown when items are selected)\n * @slot action-items — Extra action bar buttons\n * @slot after-search — Refinement controls beside the search field on the bar's left side (e.g. a filter menu button)\n * @slot before-collection — Content above the collection in every view mode (e.g. a folder/group strip)\n * @slot selection-actions — Bulk-action buttons in the contextual selection strip shown while a selection is active. Scope: `{ count, selectedIds }`\n * @slot header-actions — Extra buttons in the page header\n * @slot delete-dialog — Replace the single-item delete dialog. Scope: `{ show, item, confirm, close }`. Call `confirm()` to perform the delete.\n * @slot copy-dialog — Replace the single-item copy dialog. Scope: `{ show, item, confirm, close }`. Call `confirm(payload)` to perform the copy.\n * @slot form-dialog — Replace the create/edit form dialog. Scope: `{ show, item, schema, confirm, close }`. Call `confirm(formData)` to save through the host's normal persistence path.\n * @slot form-fields — Replace form content inside the built-in CnFormDialog. Scope: `{ fields, formData, errors, updateField }`\n * @slot import-fields — Extra fields in the import dialog\n * @slot empty — Custom empty state content\n * @slot card — Custom card template for card view. Scope: `{ row }`\n * @slot row-actions — Custom row actions. Scope: `{ row }`\n * @slot column-{key} — Custom cell renderer for a specific column. Scope: `{ row, value }`\n */\nexport default {\n\tname: 'CnIndexPage',\n\n\tcomponents: {\n\t\tNcLoadingIcon,\n\t\tNcEmptyContent,\n\t\tNcActions,\n\t\tNcActionButton,\n\t\tNcActionCaption,\n\t\tNcActionCheckbox,\n\t\tNcButton,\n\t\tClose,\n\t\tCog,\n\t\tDatabaseSearch,\n\t\tExport,\n\t\tFilterOutline,\n\t\tViewColumnOutline,\n\t\tCnPageHeader,\n\t\tCnQuickFilterBar,\n\t\tCnActionsBar,\n\t\tCnIcon,\n\t\tCnDataTable,\n\t\tCnCardGrid,\n\t\tCnBoardView,\n\t\tCnDateAxisView,\n\t\tCnMapWidget,\n\t\tCnObjectList,\n\t\tCnFolderSidebar,\n\t\tCnPagination,\n\t\tCnRowActions,\n\t\tCnMassDeleteDialog,\n\t\tCnMassCopyDialog,\n\t\tCnMassExportDialog,\n\t\tCnMassImportDialog,\n\t\tCnDeleteDialog,\n\t\tCnCopyDialog,\n\t\tCnFormDialog,\n\t\tCnAdvancedFormDialog,\n\t\tCnContextMenu,\n\t\tCnIndexSidebar,\n\t\tCnSavedViewsControl,\n\t\tCnSaveViewDialog,\n\t\tCnConfirmDialog,\n\t\tCnQuickEditDialog,\n\t},\n\n\t/**\n\t * Inject the customComponents registry from a CnAppRoot ancestor.\n\t * Used by:\n\t * - REQ-MAD-3 / REQ-MAD-8 (manifest-actions-dispatch): resolves\n\t * `actions[].handler` registry names to functions called on\n\t * row-action click.\n\t * - The cardComponent + form-dialog override paths: when set, the\n\t * prop-level `customComponents` wins, but the inject is the\n\t * default. See `effectiveCustomComponents`.\n\t *\n\t * Falls back to an empty object so `CnIndexPage` works standalone\n\t * (unit tests, isolated mount) without `CnAppRoot`.\n\t */\n\tinject: {\n\t\tcnCustomComponents: { default: () => ({}) },\n\t\t/**\n\t\t * The v2 component registry, provided by CnAppRoot. Named handlers\n\t\t * (`actions[].handler`, `bulkActions[].handler`,\n\t\t * `headerActions[].handler`) resolve here first and fall back to\n\t\t * `cnCustomComponents`, so an app can register a `kind: 'handler'`\n\t\t * entry instead of a bare function in the legacy map.\n\t\t */\n\t\tcnRegistry: { default: () => ({}) },\n\t\t/**\n\t\t * The per-user preference reader and writer, provided by CnAppRoot.\n\t\t * Used for the manual row order, which belongs to the person and the\n\t\t * list rather than to the records. The default is null, and every read\n\t\t * and write is guarded, so a page mounted with no CnAppRoot ancestor\n\t\t * simply has no held order instead of failing to render.\n\t\t */\n\t\tcnUserPreferences: { default: null },\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). Column and\n\t\t * filter labels come from schema property titles, authored in English as\n\t\t * the canonical source; the visible label is resolved through this\n\t\t * function so it follows the user's language. Defaults to identity when\n\t\t * used standalone (no CnAppRoot ancestor). The embedded CnDataTable\n\t\t * resolves its own header labels through the same injection.\n\t\t */\n\t\tcnTranslate: { default: () => (key) => key },\n\t\t/**\n\t\t * Reactive edit-mode flag from CnAppRoot's manifest editor. When truthy\n\t\t * the page shows a config cog in its actions bar (emits `configure`).\n\t\t */\n\t\tcnEditingBody: { default: false },\n\t\t/** Opens a registry modal, provided by CnAppRoot. Used by `createModal`. */\n\t\tcnOpenModal: { default: null },\n\t\t/**\n\t\t * Reactive holder provided by CnAppRoot for hoisting the\n\t\t * embedded CnIndexSidebar to NcContent level. The default\n\t\t * `{ value: null }` is what we get when no CnAppRoot\n\t\t * ancestor exists; in that case we fall back to inline\n\t\t * rendering inside the cn-index-page wrapper. See\n\t\t * `shouldRenderInlineSidebar` and the mounted/beforeDestroy\n\t\t * hooks below.\n\t\t */\n\t\tcnIndexSidebarConfig: { default: () => ({ value: null }) },\n\t\t/**\n\t\t * Sentinel set to `true` when a CnAppRoot ancestor exists.\n\t\t * The default `false` is used for legacy apps that mount\n\t\t * CnIndexPage standalone — those keep the inline sidebar\n\t\t * render. See `shouldRenderInlineSidebar` for the gate.\n\t\t */\n\t\tcnHostsIndexSidebar: { default: false },\n\t\t/**\n\t\t * Reactive AI context holder provided by CnAppRoot. This page\n\t\t * component writes pageKind, registerSlug, schemaSlug in\n\t\t * created() and watches props for subsequent changes. On\n\t\t * beforeUnmount(), fields are reset to avoid stale context on\n\t\t * subsequent custom pages.\n\t\t */\n\t\tcnAiContext: { default: null },\n\t},\n\n\tprops: {\n\t\t/** Page title */\n\t\ttitle: {\n\t\t\ttype: String,\n\t\t\trequired: true,\n\t\t},\n\n\t\t/** Optional description shown below the title */\n\t\tdescription: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Whether to show the page header (icon, title, description) inline.\n\t\t * When false (default), only the sidebar header shows the title\n\t\t * visually — but the `<h1>` is still rendered visually-hidden inside\n\t\t * the page, so the `<main>` landmark always has an accessible heading.\n\t\t * This prop controls VISIBILITY only; it can no longer remove the\n\t\t * heading from the accessibility tree.\n\t\t */\n\t\tshowTitle: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Optional MDI icon name. Defaults to schema.icon when a schema is provided. */\n\t\ticon: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Schema. Either a resolved schema object (consumer-managed path) OR a\n\t\t * schema-slug string — when a string is given together with `register`\n\t\t * and no `objects` prop, the page enters self-fetch mode: it drives the\n\t\t * list via `useListView('${register}-${schema}', …)` and the column\n\t\t * generation uses the schema object that composable loads. Backwards-\n\t\t * compatible: `[Object, String]` still accepts an object.\n\t\t */\n\t\tschema: {\n\t\t\ttype: [Object, String],\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/* eslint-disable vue/no-unused-properties -- used in useSelfFetchList */\n\t\t/**\n\t\t * Base filter for the self-fetch path. String values of the form\n\t\t * `\"@route.<name>\"` / `\":<name>\"` are interpolated from `$route.params`.\n\t\t */\n\t\tfilter: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\t\t/* eslint-enable vue/no-unused-properties */\n\n\t\t/**\n\t\t * Self-fetch mode only — an array of clickable filter tabs rendered as\n\t\t * a strip above the table. Each entry is `{label, filter, default?, icon?}`;\n\t\t * clicking a tab merges its `filter` into the fetch — spread AFTER\n\t\t * `filter` (so the active tab wins) and BEFORE the user's `activeFilters`\n\t\t * (so user facets still narrow within the active tab). String values\n\t\t * in a tab's `filter` resolve `@route.<name>` / `:<name>` from\n\t\t * `$route.params` just like the `filter` prop. The first tab with\n\t\t * `default:true` (else index 0) is active on mount; changing tabs\n\t\t * re-fetches at page 1. Omit (or `null`) → no tab strip, behaviour\n\t\t * unchanged.\n\t\t */\n\t\tquickFilters: {\n\t\t\ttype: Array,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * How the quick filters render: `'chips'` (pill strip, default) or\n\t\t * `'dropdown'` (a single `NcSelect`). Sourced from the manifest as\n\t\t * `pages[].config.quickFilterMode`.\n\t\t *\n\t\t * @type {'chips'|'dropdown'}\n\t\t */\n\t\tquickFilterMode: {\n\t\t\ttype: String,\n\t\t\tdefault: 'chips',\n\t\t\tvalidator: (v) => ['chips', 'dropdown'].includes(v),\n\t\t},\n\n\t\t/**\n\t\t * Allow several quick filters active at once. Selected tabs' filters\n\t\t * are OR-ed together into the fetch (same field → array value →\n\t\t * `field[]=` IN query). Sourced from `pages[].config.quickFilterMultiple`.\n\t\t */\n\t\tquickFilterMultiple: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * Chips mode only: how many quick-filter pills render inline before the\n\t\t * rest move into an overflow menu. `0` (the default) renders every tab.\n\t\t * A long strip wraps the actions bar onto a second line and squeezes the\n\t\t * \"Showing X of Y\" count, so a page with many lenses caps this and keeps\n\t\t * the everyday few in view. Sourced from\n\t\t * `pages[].config.quickFilterMaxVisible`.\n\t\t */\n\t\tquickFilterMaxVisible: {\n\t\t\ttype: Number,\n\t\t\tdefault: 0,\n\t\t},\n\n\t\t/** Manual column definitions (used instead of schema when provided) */\n\t\tcolumns: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * OpenRegister `_extend[]` values forwarded on the fetch, e.g.\n\t\t * `[\"calculations\"]`. Sourced from `pages[].config.extend`.\n\t\t *\n\t\t * A schema's `x-openregister-calculations` entries declared\n\t\t * `materialise: false` are VIRTUAL: RenderObject evaluates them only\n\t\t * when the caller asks for them through `_extend`, so without this a\n\t\t * declared calculation is simply absent from every row and its column\n\t\t * renders empty. The same contract `objectTableSource.extend` already\n\t\t * carries for dashboard widgets, which is where it shipped first;\n\t\t * index pages could not ask for it at all.\n\t\t */\n\t\textend: { // eslint-disable-line vue/no-unused-properties -- read by useSelfFetchList.js off the props object, which this rule does not follow.\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/** Object/row data array */\n\t\tobjects: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * Name of a registered NON-OBJECT entity collection to list (e.g. `flows`).\n\t\t *\n\t\t * The third data mode. `register` + `schema` fetches OpenRegister\n\t\t * objects; `:objects` renders rows a parent already holds; `entitySource`\n\t\t * names a list that is neither — a flow definition is deliberately not\n\t\t * an OpenRegister object, so an index had nothing to point at and such\n\t\t * lists became bespoke `type: \"custom\"` pages instead.\n\t\t *\n\t\t * Non-empty `:objects` still wins, and an entity source wins over\n\t\t * register/schema. See `src/composables/indexSources.js`.\n\t\t */\n\t\tentitySource: { type: String, default: '' },\n\n\t\t/**\n\t\t * Route name a clicked row opens, overriding a named source's own\n\t\t * navigation.\n\t\t *\n\t\t * An entity source knows where its rows live and navigates itself, and\n\t\t * for a source whose detail page belongs to ANOTHER app that is right:\n\t\t * `tasks` sends a click to openregister's task page, because pushing\n\t\t * on this app's router could not reach it.\n\t\t *\n\t\t * It is wrong for an app that HAS its own page for those rows. Dossiq\n\t\t * keeps a task detail page on purpose, so its handlers see a task in\n\t\t * dossiq's vocabulary beside the case it belongs to. Without this prop\n\t\t * adopting `entitySource: \"tasks\"` would send every click out of the\n\t\t * app, and the `row-click` event cannot recover it: `openRow` calls\n\t\t * `window.location.assign()`, so a host's push never lands.\n\t\t *\n\t\t * Set it and the row pushes `{ name: rowRoute, params: { id } }` on\n\t\t * this app's router instead. Leave it unset and the source's own\n\t\t * navigation is unchanged, which is what every current consumer gets.\n\t\t */\n\t\trowRoute: { type: String, default: '' },\n\n\t\t/**\n\t\t * Config handed to the named source's loader (e.g. `{ app: 'dossiq' }`).\n\t\t */\n\t\tsourceConfig: { type: Object, default: null },\n\n\t\t/** Pagination state: { page, pages, total, limit } */\n\t\tpagination: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Whether data is loading */\n\t\tloading: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Whether rows/cards can be selected */\n\t\tselectable: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/**\n\t\t * When true, a row/card click emits `row-click` (to open/navigate) even\n\t\t * while `selectable` — selection then happens via the checkbox only.\n\t\t * Manifest-driven index pages set this when a matching detail page\n\t\t * exists, so clicking a row opens its detail. Default false preserves\n\t\t * the legacy select-on-click behaviour.\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\n\t\t/**\n\t\t * The page's `splitView` declaration: `{ enabled, breakpoint, paneWidth }`.\n\t\t * With `enabled`, opening a row renders it beside the list in the\n\t\t * `#split-pane` slot instead of navigating away, and the list keeps its\n\t\t * scroll position, its selection and its loaded page. Omit it and the\n\t\t * page renders exactly as it does today.\n\t\t *\n\t\t * @type {{ enabled?: boolean, breakpoint?: number, paneWidth?: string }}\n\t\t */\n\t\tsplitView: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * The record the split pane is showing, taken from the split address.\n\t\t * Empty closes the pane. Ignored unless `splitView.enabled`.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tsplitId: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Route name the pane's close button returns to. Defaults to the\n\t\t * current route's own list address when omitted.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tsplitCloseRoute: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Whether the pane draws its own close link. Set false when the\n\t\t * `#split-pane` slot draws one from the slot's `close`.\n\t\t *\n\t\t * @type {boolean}\n\t\t */\n\t\tsplitCloseButton: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/**\n\t\t * Accessible name and tooltip for the pane's close button.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tsplitCloseLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Lets this person drag the rows into an order of their own, held\n\t\t * against them and this list and never written onto the records. The\n\t\t * order stands down while a sort is active, because a sort the person\n\t\t * just chose is them asking for a different order.\n\t\t *\n\t\t * @type {boolean}\n\t\t */\n\t\tmanualOrder: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * Stable id this list's manual order is held under. Defaults to the\n\t\t * object type or the schema, so two lists of one schema on different\n\t\t * pages share an order only when the host says they should.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tmanualOrderId: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** Currently selected IDs */\n\t\tselectedIds: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * View mode: 'table', 'cards', 'list', 'map', 'board' or 'dateAxis'.\n\t\t * Default 'table'. List is opted in via `availableViewModes`; map via\n\t\t * `mapConfig` / `config.viewModes`; board and dateAxis via\n\t\t * `config.viewModes` and their own config blocks.\n\t\t */\n\t\tviewMode: {\n\t\t\ttype: String,\n\t\t\tdefault: 'table',\n\t\t\tvalidator: (v) => ['table', 'cards', 'list', 'map', 'board', 'dateAxis'].includes(v),\n\t\t},\n\n\t\t/**\n\t\t * Marker geometry mapping for the opt-in `map` view mode, mirroring the\n\t\t * manifest `config.map` block 1:1. When non-empty (and not excluded by an\n\t\t * explicit `config.viewModes`), a third \"Map\" segment appears in the view\n\t\t * toggle and the current filtered rows are plotted on a CnMapWidget.\n\t\t *\n\t\t * - `latField` / `lngField` — object (or `@self`) property paths holding the\n\t\t * marker's latitude / longitude. Dotted paths are supported (e.g.\n\t\t * `@self.geo.lat`).\n\t\t * - `geoField` — alternative single property holding a GeoJSON Point\n\t\t * geometry (`{ type: 'Point', coordinates: [lng, lat] }`); takes\n\t\t * precedence over lat/lngField when present and resolvable.\n\t\t * - `popupField` — object property rendered in the marker popup.\n\t\t * - `center` — optional `[lat, lng]` fallback centre when the filtered set\n\t\t * has no plottable rows.\n\t\t *\n\t\t * @type {{ latField?: string, lngField?: string, geoField?: string, popupField?: string, center?: [number, number] }}\n\t\t */\n\t\tmapConfig: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/** Label for the map view-toggle segment (defaults to \"Map\"). */\n\t\tmapLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** MDI icon name for the map view-toggle segment (defaults to the built-in map-marker icon). */\n\t\tmapIcon: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Explicit whitelist of view-toggle segments to offer, e.g.\n\t\t * `['table', 'cards', 'map']`. Fed from the manifest as\n\t\t * `pages[].config.viewModes`. When set it takes precedence over the\n\t\t * inferred availability (map otherwise appears iff `mapConfig` is\n\t\t * non-empty). Cards/table always render regardless of this list.\n\t\t *\n\t\t * @type {Array<'table' | 'cards' | 'list' | 'map' | 'board' | 'dateAxis'>}\n\t\t */\n\t\tviewModes: {\n\t\t\ttype: Array,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * The board's configuration, mirroring the manifest `config.board`\n\t\t * block: `{ statusField, cardFields, swimlaneField }`. The board is\n\t\t * offered only when this names a `statusField` AND `viewModes` lists\n\t\t * `board`: a segment that opens a board saying it cannot be one is\n\t\t * worse than no segment.\n\t\t *\n\t\t * @type {object}\n\t\t */\n\t\tboard: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * The date axis's configuration, mirroring the manifest\n\t\t * `config.dateAxis` block: `{ startField, endField, laneField,\n\t\t * labelField }`. Offered only when both date fields are named.\n\t\t *\n\t\t * @type {object}\n\t\t */\n\t\tdateAxis: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * The host's transition, handed to the board. Absent, the board is\n\t\t * read-only: it never writes the status field itself, so with no\n\t\t * transition there is nothing it can do.\n\t\t *\n\t\t * @type {((move: { card: object, toKey: string }) => Promise<object>)|null}\n\t\t */\n\t\trunTransition: {\n\t\t\ttype: Function,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Which view-mode toggle segments to expose (cards/table/list), in order.\n\t\t * Defaults to the historical Cards/Table pair; include `'list'` to offer the\n\t\t * list view. Fed from the manifest as `pages[].config.availableViewModes`.\n\t\t * Map is added separately via `mapConfig` / `viewModes`.\n\t\t *\n\t\t * @type {Array<'cards' | 'table' | 'list' | 'map' | 'board' | 'dateAxis'>}\n\t\t */\n\t\tavailableViewModes: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => ['cards', 'table'],\n\t\t\tvalidator: (modes) => modes.every((m) => ['cards', 'table', 'list', 'map', 'board', 'dateAxis'].includes(m)),\n\t\t},\n\n\t\t/** Current sort key */\n\t\tsortKey: {\n\t\t\ttype: String,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Current sort order */\n\t\tsortOrder: {\n\t\t\ttype: String,\n\t\t\tdefault: 'asc',\n\t\t},\n\n\t\t/**\n\t\t * Ordered multi-column sort state (host-controlled / non-self-fetch\n\t\t * mode): `[{ key, order }, ...]`, mirroring the `sortKey`/`sortOrder`\n\t\t * pair but for more than one active key. Ignored in self-fetch mode\n\t\t * (register + schema), which manages its own multi-sort state via\n\t\t * `useSelfFetchList`/`useListView` and persists it in the route query.\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/**\n\t\t * Optional declarative DEFAULT multi-key client-side sort, applied to the\n\t\t * already-loaded rows whenever no explicit column sort is active (no\n\t\t * `sortKey` selected by the user / passed in). Each entry is\n\t\t * `{ field, order }` with `order` one of `'asc'` / `'desc'` (default\n\t\t * `'asc'`); rows are compared by the first field, ties broken by the\n\t\t * next, and so on. Comparison is type-aware (numbers numerically, dates\n\t\t * by timestamp, strings via `localeCompare`). Clicking a sortable header\n\t\t * takes over and suppresses this default. Useful for a fixed presentation\n\t\t * order such as \"group by type, then name\".\n\t\t *\n\t\t * @type {Array<{field: string, order?: 'asc'|'desc'}>}\n\t\t */\n\t\tdefaultSort: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/** Unique row identifier property */\n\t\trowKey: {\n\t\t\ttype: String,\n\t\t\tdefault: 'id',\n\t\t},\n\n\t\t/**\n\t\t * Optional leading icon for every table row — a static MDI icon name or\n\t\t * a `(row) => iconName` function. Forwarded to CnDataTable. Fed from the\n\t\t * manifest as `pages[].config.rowIcon`. 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/** Columns to exclude in schema mode */\n\t\texcludeColumns: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/** Columns to include in schema mode (whitelist) */\n\t\tincludeColumns: {\n\t\t\ttype: Array,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Per-column overrides in schema mode */\n\t\tcolumnOverrides: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/** Row action definitions (app-provided, merged with built-in actions) */\n\t\tactions: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/** Text shown when no items found */\n\t\temptyText: {\n\t\t\ttype: String,\n\t\t\tdefault: 'No items found',\n\t\t},\n\n\t\t/** Accessible label for the loading spinner (NcLoadingIcon aria-label) */\n\t\tloadingText: {\n\t\t\ttype: String,\n\t\t\tdefault: 'Loading…',\n\t\t},\n\n\t\t/** Function returning CSS class(es) for a row */\n\t\trowClass: {\n\t\t\ttype: Function,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Override label for the Add button. Defaults to \"Add {schema.title}\" */\n\t\taddLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** How many action buttons to show inline (rest go in overflow dropdown) */\n\t\tinlineActionCount: {\n\t\t\ttype: Number,\n\t\t\tdefault: 0,\n\t\t},\n\n\t\t/** Whether to show the built-in mass Import action */\n\t\tshowMassImport: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/** Whether to show the built-in mass Export action */\n\t\tshowMassExport: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/** Whether to show the built-in mass Copy button */\n\t\tshowMassCopy: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/** Whether to show the built-in mass Delete button */\n\t\tshowMassDelete: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/**\n\t\t * Opt-in flag for the native Export menu (CSV/Excel) rendered in the\n\t\t * toolbar next to the Add button. Defaults to `false` — an app must\n\t\t * explicitly enable it per page. The menu only renders when this is\n\t\t * `true` AND the resolved schema is flagged `exportable: true`; it\n\t\t * navigates the browser to OpenRegister's export leaf\n\t\t * (`GET /apps/openregister/api/objects/{register}/{schema}/export`),\n\t\t * passing the current route's query params through as filters. This\n\t\t * is distinct from the `showMassExport` mass-action, which exports\n\t\t * only the fetched/selected rows via a blob download.\n\t\t */\n\t\tallowExport: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * Opt-in flag for the saved-views control (saved-views-ui) rendered\n\t\t * in the toolbar. Defaults to `false` — an app must explicitly\n\t\t * enable it per page. When `true`, a Views dropdown lists the\n\t\t * current user's OpenRegister saved-search views\n\t\t * (`GET /apps/openregister/api/views`); applying one writes its\n\t\t * stored filters/search/sort into the route query (reusing the\n\t\t * existing deep-link contract — non-underscore keys are filters,\n\t\t * `_search` and `_order` are reserved), \"Save current view…\"\n\t\t * persists the current route-query state via POST and toasts either\n\t\t * way naming the view (a failure also keeps the dialog open with the\n\t\t * reason), and own views can be deleted after confirmation.\n\t\t */\n\t\tallowSavedViews: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * Which pages share this page's saved views. By default a view is\n\t\t * shared by every page over the same register and schema and by no\n\t\t * other page, because a view is filters over one schema's fields. Set\n\t\t * this to share views across pages over different sources, or to\n\t\t * keep two pages over the same source apart. Written into the saved\n\t\t * view's `query.scope`; views saved before scoping stay visible\n\t\t * everywhere.\n\t\t */\n\t\tsavedViewsScope: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** Property name used to display item names in dialogs */\n\t\tmassActionNameField: {\n\t\t\ttype: String,\n\t\t\tdefault: 'title',\n\t\t},\n\n\t\t/** Optional function to format item names in dialogs. Receives the item, returns a string. Overrides massActionNameField when provided. */\n\t\tnameFormatter: {\n\t\t\ttype: Function,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Available export formats for the export dialog */\n\t\texportFormats: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [\n\t\t\t\t{ id: 'excel', label: 'Excel (.xlsx)' },\n\t\t\t\t{ id: 'csv', label: 'CSV (.csv)' },\n\t\t\t],\n\t\t},\n\n\t\t/** Import option definitions for the import dialog */\n\t\timportOptions: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/** Whether to show the built-in form dialog for Add/Edit */\n\t\tshowFormDialog: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/**\n\t\t * Registry key of a `kind: 'modal'` entry that Add (and `?action=create`)\n\t\t * opens instead of the built-in form dialog. Edits keep the form dialog.\n\t\t */\n\t\tcreateModal: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** Use CnAdvancedFormDialog (properties table, JSON tab, optional metadata) instead of CnFormDialog for Add/Edit */\n\t\tuseAdvancedFormDialog: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * NcDialog size for the built-in Add/Edit form dialog.\n\t\t *\n\t\t * The dialog itself has taken a `size` since it shipped, but this page\n\t\t * never passed one, so an index page's Add form was stuck at `normal`\n\t\t * however many properties its schema declared. A manifest-declared\n\t\t * `open-form` header action, which reaches CnFormDialog through\n\t\t * CnActionButtons, could already ask for `large` — which is why the\n\t\t * same app could have a roomy create form on a detail page and a\n\t\t * cramped one on its index.\n\t\t */\n\t\tformSize: {\n\t\t\ttype: String,\n\t\t\tdefault: 'normal',\n\t\t},\n\n\t\t/**\n\t\t * How many columns the built-in Add/Edit form flows its fields into.\n\t\t *\n\t\t * Pair `2` with `formSize: 'large'`, or the two columns are merely two\n\t\t * narrow ones. Worth it once the schema asks more questions than fit on\n\t\t * a screen; below 700px CnFormDialog collapses back to one column on\n\t\t * its own, so this is safe on a narrow viewport.\n\t\t */\n\t\tformColumns: {\n\t\t\ttype: Number,\n\t\t\tdefault: 1,\n\t\t\tvalidator: (value) => value === 1 || value === 2,\n\t\t},\n\n\t\t/**\n\t\t * Seed values for the built-in create dialog. Never applied on edit:\n\t\t * both dialogs consult `initialData` / `initialValues` only when there\n\t\t * is no item. Values resolve `@me` / `@now` / `@today` at any depth;\n\t\t * `@object.*` / `@workspace.*` / `@config.*` need a context this page\n\t\t * does not carry, so they pass through unresolved.\n\t\t *\n\t\t * @type {object|null}\n\t\t */\n\t\tcreateDefaults: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Where to go after a successful create from the built-in Add dialog.\n\t\t * A route name, or `{ name, paramField?, objectParam? }`; the created\n\t\t * object's id is merged into the params. Empty stays on the list.\n\t\t *\n\t\t * @type {string|object|null}\n\t\t */\n\t\tcreateSuccessRoute: {\n\t\t\ttype: [String, Object],\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Toast shown after a successful create from the built-in Add dialog,\n\t\t * run through the host `cnTranslate`. Empty shows none.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tcreateSuccessMessage: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Opt-in async create hook. When provided, a **create** (not edit)\n\t\t * confirmed from the built-in form dialog calls\n\t\t * `await createOverride(formData, ctx)` INSTEAD of persisting via the\n\t\t * store / self-store `saveObject`. The override owns persistence\n\t\t * (e.g. an app posting through a contact-aware endpoint that fills a\n\t\t * required FK before saving to OpenRegister) and MUST return the\n\t\t * created object (a truthy value) on success; return a falsy value to\n\t\t * signal failure. The returned object is used as the created result\n\t\t * (`@create` payload + dialog success). Throwing rejects with the\n\t\t * error surfaced in the form dialog. Edits are never routed here.\n\t\t *\n\t\t * `ctx` carries `{ register, schema, objectType, effectiveSchema }`\n\t\t * so a single handler can branch per schema.\n\t\t *\n\t\t * When absent, create behaviour is unchanged (store / self-store save).\n\t\t *\n\t\t * @type {((formData: object, ctx: { register: string, schema: (object|string), objectType: string, effectiveSchema: object }) => Promise<object>)|null}\n\t\t */\n\t\tcreateOverride: {\n\t\t\ttype: Function,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Whether to add a View action to row actions. The action emits a\n\t\t * dedicated `view` event — independent of `row-click`. Bind `@view`\n\t\t * to handle \"open detail\" and `@row-click` to handle row click\n\t\t * (selection, expand, etc.); they may share a handler when the app\n\t\t * wants click-to-view, but they are conceptually distinct.\n\t\t *\n\t\t * On a named `entitySource` page the effective default is FALSE: the\n\t\t * source's own open action navigates to the detail page, which is the\n\t\t * view. Passing the prop explicitly still wins.\n\t\t */\n\t\tshowViewAction: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/**\n\t\t * Where the View row action points, as `(row) => location | null`.\n\t\t * When it returns a location, View renders as a link to it (middle\n\t\t * click, copy link) and does not emit `view`; when it returns null,\n\t\t * View stays a button that emits `view`. CnPageRenderer sets it to\n\t\t * where a row click opens.\n\t\t */\n\t\tviewTo: {\n\t\t\ttype: Function,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Whether to add an Edit action to row actions.\n\t\t *\n\t\t * On a named `entitySource` page the effective default is FALSE: a\n\t\t * source has no schema, so the form modal this action opens could only\n\t\t * ever render empty — the source declares its own Edit, which\n\t\t * navigates. Passing the prop explicitly still wins.\n\t\t */\n\t\tshowEditAction: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/**\n\t\t * Send the Edit row action to the record's detail page instead of\n\t\t * opening the edit modal, by emitting `edit-open` rather than showing\n\t\t * the form dialog.\n\t\t *\n\t\t * OFF by default, and `CnPageRenderer` no longer derives it: the host\n\t\t * binds `@edit-open` to the same navigation as a row click, so an Edit\n\t\t * that routes is an Edit that does what clicking the row does — the\n\t\t * split pane, or the detail page — while the form it names becomes\n\t\t * unreachable from the list.\n\t\t *\n\t\t * Set it when the modal genuinely cannot express the record and you\n\t\t * accept the duplication: it renders the schema's flat scalars only, so\n\t\t * related rows, sub-resources and tabs stay uneditable there.\n\t\t */\n\t\teditOpensDetail: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * Whether to add a Copy action to row actions.\n\t\t *\n\t\t * On a named `entitySource` page the effective default is whether the\n\t\t * source implements `copyRow` — without it, the copy dialog's confirm\n\t\t * has nowhere to go. Passing the prop explicitly still wins.\n\t\t */\n\t\tshowCopyAction: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/**\n\t\t * Whether to add a Delete action to row actions.\n\t\t *\n\t\t * On a named `entitySource` page the effective default is whether the\n\t\t * source implements `deleteRow` — without it, the delete dialog's\n\t\t * confirm has nowhere to go. Passing the prop explicitly still wins.\n\t\t */\n\t\tshowDeleteAction: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/** Field keys to exclude from the form dialog */\n\t\texcludeFields: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/** Field keys to include in the form dialog (whitelist mode) */\n\t\tincludeFields: {\n\t\t\ttype: Array,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Per-field overrides passed to CnFormDialog */\n\t\tfieldOverrides: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/** Whether to show the Cards/Table view toggle in the actions bar */\n\t\tshowViewToggle: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/**\n\t\t * Show an inline search field in the actions bar (in addition to / instead\n\t\t * of the sidebar search). Fed from the manifest as `pages[].config.inlineSearch`.\n\t\t */\n\t\tinlineSearch: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Placeholder for the inline search field (manifest `config.searchPlaceholder`) */\n\t\tsearchPlaceholder: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** Label for the cards view-toggle option (manifest `config.cardsLabel`, e.g. \"Tiles\") */\n\t\tcardsLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** Label for the table view-toggle option (manifest `config.tableLabel`, e.g. \"List\") */\n\t\ttableLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** MDI icon name for the cards view-toggle option (manifest `config.cardsIcon`) */\n\t\tcardsIcon: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** MDI icon name for the table view-toggle option (manifest `config.tableIcon`) */\n\t\ttableIcon: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** Label for the list view-toggle option (manifest `config.listLabel`, e.g. \"Rows\") */\n\t\tlistLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Show a standalone sort dropdown in the actions bar (for card/list\n\t\t * views without sortable headers). Emits `@sort-change` with the value.\n\t\t * Fed from the manifest as `pages[].config.showSortSelect`.\n\t\t */\n\t\tshowSortSelect: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * Options for the standalone sort dropdown (manifest `config.sortSelectOptions`).\n\t\t *\n\t\t * @type {Array<{ value: string, label: string }>}\n\t\t */\n\t\tsortSelectOptions: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/** Selected value of the standalone sort dropdown (controlled). */\n\t\tsortSelectValue: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** MDI icon name for the list view-toggle option (manifest `config.listIcon`) */\n\t\tlistIcon: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Field mapping for the default list-view rows (CnObjectRow). Overrides\n\t\t * the schema-configuration defaults. Fed from the manifest as\n\t\t * `pages[].config.listConfig`.\n\t\t *\n\t\t * @type {{ titleField?: string, subtitleField?: string, imageField?: string, iconField?: string, iconName?: string, badgeField?: string, badgeVariantField?: string, badgeVariant?: string, badgeColorMap?: object }}\n\t\t */\n\t\tlistConfig: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * Opt-in folder navigation pane (rendered left of the list). Selecting a\n\t\t * folder filters the list by `filterField` (via the self-fetch filter);\n\t\t * \"All\" clears it. Fed from the manifest as `pages[].config.folderSidebar`.\n\t\t *\n\t\t * - `source` — `'register'` (fetch the folder list from an OpenRegister\n\t\t * register/schema), `'field'` (distinct values of the current rows'\n\t\t * `field`), `'custom'` (use `folders`), or `'files'` (NC folders).\n\t\t * - `register` / `schema` — the OR source for `source:'register'`.\n\t\t * - `idField` / `nameField` — which folder object props map to the folder\n\t\t * id (the filter value) and display name. Dotted paths supported.\n\t\t * - `field` / `filterField` — the row field to group/filter by (filterField\n\t\t * defaults to `field`).\n\t\t * - `folders` — explicit folder list for `source:'custom'`.\n\t\t * - `allLabel` / `title` / `allowCreate` — passed to CnFolderSidebar.\n\t\t *\n\t\t * A folder entry is also a SCOPE: it may carry `columns`, `defaultSort`\n\t\t * and `searchFields`, and while it is selected the list is shown that\n\t\t * way. `columns` takes the same shapes as the `columns` prop, and the\n\t\t * page's own `columns` still decides which columns this page has, so a\n\t\t * scope naming one the page does not declare is dropped rather than\n\t\t * rendered. A `source: 'register'` folder list reads the same three\n\t\t * keys off each row's `x-index` block, which the folder entry's own\n\t\t * value wins over, key by key.\n\t\t *\n\t\t * @type {object}\n\t\t */\n\t\tfolderSidebar: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Where a row carries the actions the server says this caller may run\n\t\t * on it: a dotted path, read off the rows the list already fetched, so\n\t\t * ninety rows cost one request rather than ninety.\n\t\t *\n\t\t * A row's menu is then the INTERSECTION of what this page declares and\n\t\t * what the server allows. An action the server allows but this page has\n\t\t * stopped declaring stays out, so a row cannot bring back a button the\n\t\t * page removed. An action the page declares but the server refuses\n\t\t * stays out too, and its reason is available from\n\t\t * `rowActionRefusal(row, action)`.\n\t\t *\n\t\t * A row carrying nothing at this path is a server that does not answer\n\t\t * about actions, and the page's declaration stands unchanged.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\trowActionField: {\n\t\t\ttype: String,\n\t\t\tdefault: DEFAULT_ROW_ACTION_FIELD,\n\t\t},\n\n\t\t/**\n\t\t * State indicators this page declares for its rows, handed straight to\n\t\t * CnDataTable. Each entry is `{ id, field, equals?, in?, icon, text,\n\t\t * tooltip? }`. The page declares which indicators exist; a record\n\t\t * cannot add one the page has not declared. A page declaring none\n\t\t * renders its rows as before. Fed from the manifest as\n\t\t * `pages[].config.rowIndicators`.\n\t\t *\n\t\t * @type {Array<object>}\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 before the\n\t\t * rest move into the row menu. Fed from the manifest as\n\t\t * `pages[].config.rowIndicatorCap`.\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 * The fields a quick edit asks for, opened from a row without leaving\n\t\t * the list. Empty means no quick edit. The page names the fields; a\n\t\t * record cannot open a form on one the page did not name. Fed from the\n\t\t * manifest as `pages[].config.quickEditFields`.\n\t\t *\n\t\t * @type {string[]}\n\t\t */\n\t\tquickEditFields: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * Where a record lists the fields this caller may write. A field the\n\t\t * page named that is not in that list renders read-only rather than\n\t\t * missing, so a person sees the value and learns it is not theirs to\n\t\t * change. A row carrying nothing there leaves every named field\n\t\t * editable.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\twritableField: {\n\t\t\ttype: String,\n\t\t\tdefault: '@self.writableFields',\n\t\t},\n\n\t\t/**\n\t\t * Saved view ids this page renders as tabs instead of as entries in the\n\t\t * views control. A view appears in one place or the other, never both.\n\t\t * An id naming a view that no longer exists produces no tab.\n\t\t *\n\t\t * @type {string[]}\n\t\t */\n\t\tviewTabs: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * The teams this person may claim. The instance decides this list; it\n\t\t * is the membership side of `claimedTeams`.\n\t\t *\n\t\t * @type {Array<string|object>}\n\t\t */\n\t\tofferedTeams: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * The teams this person stored as claimed, read as a personal\n\t\t * preference alongside the others. It is narrowed to `offeredTeams`, so\n\t\t * a team they claimed before it was taken away is passed over rather\n\t\t * than honoured.\n\t\t *\n\t\t * @type {string[]}\n\t\t */\n\t\tclaimedTeams: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * The field carrying a record's DERIVED priority, and the order its\n\t\t * values rank in, lowest first. The list reads the value and sorts on\n\t\t * it; it never computes one. A record with no priority sorts after\n\t\t * every ranked one, in both directions, and stays in the list.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tpriorityField: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * The priority values in rank order, lowest first, e.g.\n\t\t * `['low', 'medium', 'high']`. A value outside this list is unranked.\n\t\t *\n\t\t * @type {string[]}\n\t\t */\n\t\tpriorityLevels: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * Offer the list's keyboard shortcuts. Every one of them is also listed\n\t\t * in the command palette and on the help key, because a shortcut nobody\n\t\t * can find does not count.\n\t\t */\n\t\tlistShortcuts: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * Show a filter menu (funnel button) in the table header, above the\n\t\t * row-actions column. Its menu lists every enum/badge column's values as\n\t\t * toggleable facet filters — a compact alternative to the facet sidebar.\n\t\t * Fed from the manifest as `pages[].config.filterMenu`.\n\t\t */\n\t\tfilterMenu: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * Show a column menu (columns button) in the table header, above the\n\t\t * row-actions column. Its menu lists every governed column as a toggleable\n\t\t * checkbox — a compact, in-table alternative to the sidebar's Columns tab,\n\t\t * so the sidebar space can be reclaimed. Fed from the manifest as\n\t\t * `pages[].config.columnMenu`.\n\t\t */\n\t\tcolumnMenu: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Whether the refresh action is currently in progress */\n\t\trefreshing: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * Whether to auto-subscribe to live collection updates in self-fetch\n\t\t * mode. Defaults to true. When `register` + `schema` are set (and no\n\t\t * `objects` prop is passed), the page subscribes to the collection's\n\t\t * `or-collection-{register}-{schema}` scope on mount and refetches\n\t\t * (coalesced — events are hints) when an update event arrives; the\n\t\t * subscription is released on unmount. Set `false` (manifest:\n\t\t * `config.subscribe: false`) for static / read-once views. No-op in\n\t\t * consumer-managed mode (an `objects` prop was passed) and on stores\n\t\t * without live-updates support.\n\t\t *\n\t\t * @type {boolean}\n\t\t */\n\t\tsubscribe: { // eslint-disable-line vue/no-unused-properties -- read by useSelfFetchList.js off the props object, which this rule does not follow.\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/** Whether the refresh action is disabled (e.g. when required selections are missing) */\n\t\trefreshDisabled: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Whether the Add button is disabled (e.g. when required selections are missing) */\n\t\taddDisabled: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Whether to show the Add button in the actions bar */\n\t\tshowAdd: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/**\n\t\t * Store instance for automatic save integration. When provided alongside\n\t\t * objectType, the form dialog saves directly to the store instead of\n\t\t * emitting create/edit events. The object type must already be registered\n\t\t * in the store via registerObjectType() before passing the store here.\n\t\t */\n\t\tstore: { type: Object, default: null },\n\t\t/**\n\t\t * Object type slug for store integration (e.g. `${registerId}-${schemaId}`).\n\t\t * Required when store is set — a console warning is emitted if missing.\n\t\t */\n\t\tobjectType: { type: String, default: '' },\n\t\t/**\n\t\t * Manifest-driven sidebar configuration. When set with\n\t\t * `enabled: true`, CnIndexPage auto-mounts an embedded\n\t\t * CnIndexSidebar wired to the page's schema, search, columns,\n\t\t * and facet props. When unset or `enabled: false`, the\n\t\t * legacy slot-based interface is preserved — consumers\n\t\t * mount their own CnIndexSidebar at the App.vue level.\n\t\t *\n\t\t * Shape:\n\t\t * - `enabled` (boolean) — **existence gate**. Whether the\n\t\t * page configures an embedded sidebar at all. When `false`\n\t\t * or unset, the auto-mount path is bypassed (no\n\t\t * `<CnIndexSidebar>` rendered) and the consumer's slot\n\t\t * pattern stays active.\n\t\t * - `show` (boolean, default `true`) — **visibility gate**.\n\t\t * Even when `enabled: true`, `show: false` SUPPRESSES\n\t\t * rendering for this page so manifest authors can hide\n\t\t * the sidebar declaratively without removing the config.\n\t\t * Distinct from `enabled` so config can be retained\n\t\t * (e.g. for a watcher / responsive layout) while the\n\t\t * visible surface is hidden.\n\t\t * - `columnGroups` (array) — extra column groups beyond schema + Metadata.\n\t\t * - `facets` (object) — live facet data { fieldName: { values: [...] } }.\n\t\t * - `showMetadata` (boolean) — include the built-in Metadata column group (defaults true).\n\t\t * - `search` (object) — search-related label overrides forwarded to CnIndexSidebar.\n\t\t *\n\t\t * @type {{ enabled: boolean, show?: boolean, columnGroups?: Array, facets?: object, showMetadata?: boolean, search?: object }|null}\n\t\t */\n\t\tsidebar: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Current search term (forwarded to the embedded sidebar when sidebar.enabled). */\n\t\tsearchValue: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** Currently visible column keys (forwarded to the embedded sidebar). */\n\t\tvisibleColumns: {\n\t\t\ttype: Array,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Currently active facet filters: { fieldName: [values] } (forwarded to the embedded sidebar). */\n\t\tactiveFilters: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * Effective register slug for the page. Forwarded as a prop to\n\t\t * the resolved card component (when `cardComponent` is set) so\n\t\t * bespoke card UIs can match the schema → register pair.\n\t\t *\n\t\t * Manifest-driven path: `pages[].config.register` flows in via\n\t\t * CnPageRenderer's `v-bind=\"resolvedProps\"` spread.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tregister: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Optional name of a consumer-provided card component (registered\n\t\t * in the `customComponents` registry on `CnAppRoot`) to render in\n\t\t * place of the default `CnObjectCard` when the page is in\n\t\t * card-grid view mode.\n\t\t *\n\t\t * Resolution priority (highest first):\n\t\t * 1. The parent's `#card` scoped slot (always wins).\n\t\t * 2. The component resolved from `cardComponent` against the v2\n\t\t * `registry` (any kind carrying a `component`), then the legacy\n\t\t * customComponents map.\n\t\t * 3. The library default (`CnObjectCard`).\n\t\t *\n\t\t * Unknown names log `console.warn` once and fall back to the\n\t\t * default so a misconfigured manifest never blanks the grid.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tcardComponent: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Name of a custom row component for list view, resolved against the v2\n\t\t * `registry` and then the legacy customComponents map (manifest\n\t\t * `pages[].config.listComponent`).\n\t\t * Same resolution priority as `cardComponent`: the `#list-item` slot\n\t\t * wins, then this component, then the default `CnObjectRow`. Unknown\n\t\t * names warn once and fall back to the default.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tlistComponent: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Optional explicit customComponents registry. When set, this\n\t\t * overrides the registry injected from `CnAppRoot` via\n\t\t * `cnCustomComponents`. Provided primarily so unit tests can\n\t\t * pass a registry without mounting `CnAppRoot`.\n\t\t *\n\t\t * Used by:\n\t\t * - `cardComponent` resolution (REQ-MCI from manifest-card-index)\n\t\t * - `actions[].handler` registry name resolution (REQ-MAD-3 from\n\t\t * manifest-actions-dispatch — handler funcs called on row-action click)\n\t\t *\n\t\t * @type {object|null}\n\t\t */\n\t\tcustomComponents: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Manifest-driven page-level actions rendered inside the\n\t\t * NcActions overflow dropdown between Refresh and the\n\t\t * `#action-items` slot. Each entry is\n\t\t * `{ id, label, icon?, handler?, route?, disabled? }`. The\n\t\t * `handler` field mirrors the row-level\n\t\t * `actions[].handler` pattern: a function, the keyword\n\t\t * `'navigate'`, `'emit'`, `'none'`, or a string name looked\n\t\t * up in the v2 `registry` (a `kind: 'handler'` entry) and\n\t\t * then in the legacy `customComponents`. The page\n\t\t * dispatches the resolved handler via `onHeaderAction` AND\n\t\t * (unless the handler is the `'none'` keyword) emits\n\t\t * `@header-action({ action: id, id })`.\n\t\t *\n\t\t * Reserved ids (those used by built-ins on the bar — `refresh`,\n\t\t * `import`, `export`, `copy`, `delete`) are dropped from the\n\t\t * merged list with a `console.warn`.\n\t\t *\n\t\t * @type {Array<{ id: string, label: string, icon?: string, handler?: string|(() => void), route?: string, disabled?: boolean }>}\n\t\t */\n\t\theaderActions: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * Declarative bulk actions for the contextual selection strip.\n\t\t *\n\t\t * The strip has had a `#selection-actions` slot for a while, but a slot\n\t\t * can only be filled by a hand-written host component — so a page\n\t\t * declared in an app manifest could carry row actions and header\n\t\t * actions and never a bulk one. This prop is the missing vocabulary.\n\t\t *\n\t\t * Handler resolution mirrors `headerActions[]`, with one difference\n\t\t * that is the whole point: the handler is called with the SELECTION.\n\t\t * A bulk handler that has to go and find out what was selected is a\n\t\t * bulk handler waiting to disagree with the strip that invoked it.\n\t\t *\n\t\t * Reserved ids (`copy`, `delete`) are dropped with a warning — the\n\t\t * strip already ships those two as built-ins, and a second button with\n\t\t * the same name doing something else is worse than no button.\n\t\t *\n\t\t * @type {Array<{ id: string, label: string, icon?: string, handler?: string|((scope: { actionId: string, selectedIds: Array<string>, count: number }) => void), target?: string, props?: object, disabled?: boolean }>}\n\t\t */\n\t\tbulkActions: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * Active organisation entity (multi-tenancy-context). When\n\t\t * bound from a tenant-switcher higher in the tree, CnIndexPage\n\t\t * watches it and calls `store.setActiveTenantOrganisation(uuid)`\n\t\t * so the next fetchCollection() stamps the new tenant header\n\t\t * and the in-memory caches are cleared. When the prop is unset\n\t\t * the legacy single-tenant behaviour is preserved exactly.\n\t\t *\n\t\t * @type {object|null}\n\t\t */\n\t\tactiveOrganisation: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * When set, adds a Documentation entry to the Actions overflow\n\t\t * (after Refresh). Opens the URL in a new tab. Empty hides it.\n\t\t */\n\t\tdocumentationUrl: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** Label for the Documentation overflow entry. */\n\t\tdocumentationLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\t},\n\n\temits: [\n\t\t'action',\n\t\t'add',\n\t\t'apply-view',\n\t\t'board-move',\n\t\t'bulk-action',\n\t\t'clear-filters',\n\t\t'columns-change',\n\t\t'quick-edit-save',\n\t\t'configure',\n\t\t'copy',\n\t\t'create',\n\t\t'delete',\n\t\t'edit-open',\n\t\t'filter-change',\n\t\t'folder-change',\n\t\t'folder-create',\n\t\t'header-action',\n\t\t'mass-copy',\n\t\t'open-modal',\n\t\t'mass-delete',\n\t\t'mass-export',\n\t\t'mass-import',\n\t\t'page-changed',\n\t\t'page-size-changed',\n\t\t'quick-filter-change',\n\t\t'refresh',\n\t\t'row-click',\n\t\t'search',\n\t\t'select',\n\t\t'sort',\n\t\t'sort-change',\n\t\t'view',\n\t\t'view-mode-change',\n\t\t'split-close',\n\t\t'split-saved',\n\t\t'manual-order-change',\n\t],\n\n\tsetup(props) {\n\t\tconst {\n\t\t\tisOpen: contextMenuOpen,\n\t\t\ttargetItem: contextMenuRow,\n\t\t\topen: openContextMenu,\n\t\t\tclose: closeContextMenu,\n\t\t} = useContextMenu()\n\n\t\tconst {\n\t\t\tisSelfFetch,\n\t\t\tlist,\n\t\t\tselfObjectStore,\n\t\t\tselfObjectType,\n\t\t\tactiveQuickFilterIndex,\n\t\t\tselectedQuickFilterIndices,\n\t\t\tselfFetchTokenCtx,\n\t\t\tinitialQueryFilterKeys,\n\t\t} = useSelfFetchList(props, getCurrentInstance(), inject)\n\n\t\t// The sidebar's chosen values on a NAMED-SOURCE page. Self-fetch keeps\n\t\t// its own in `useSelfFetchList`; a named source had nowhere to put\n\t\t// them, so `activeFilters` was a prop only a consumer could fill and a\n\t\t// manifest page's sidebar rendered controls that narrowed nothing.\n\t\t//\n\t\t// Seeded from the route query so a shared link lands filtered, the\n\t\t// same way `_order` and a deep-link filter already do for self-fetch.\n\t\tconst namedActiveFilters = ref(namedFiltersFromRoute(getCurrentInstance(), props))\n\n\t\tconst {\n\t\t\tisNamedSource,\n\t\t\tnamedSource,\n\t\t\tnamedRows,\n\t\t\tnamedLoading,\n\t\t\tnamedQuickFilters,\n\t\t} = useNamedSource(props, { activeQuickFilterIndex, activeFilters: namedActiveFilters })\n\n\t\treturn {\n\t\t\tisNamedSource,\n\t\t\tnamedSource,\n\t\t\tnamedRows,\n\t\t\tnamedLoading,\n\t\t\tnamedQuickFilters,\n\t\t\tnamedActiveFilters,\n\t\t\tcontextMenuOpen,\n\t\t\tcontextMenuRow,\n\t\t\topenContextMenu,\n\t\t\tcloseContextMenu,\n\t\t\tisSelfFetch,\n\t\t\tlist,\n\t\t\tselfObjectStore,\n\t\t\tselfObjectType,\n\t\t\tactiveQuickFilterIndex,\n\t\t\tselectedQuickFilterIndices,\n\t\t\tselfFetchTokenCtx,\n\t\t\tinitialQueryFilterKeys,\n\t\t}\n\t},\n\n\tdata() {\n\t\treturn {\n\t\t\tcurrentViewMode: this.viewMode,\n\t\t\t/**\n\t\t\t * The non-`_` query keys this page owns, i.e. may clear on the next\n\t\t\t * persist. Seeded with what it adopted from the query on load, and\n\t\t\t * replaced by what it writes; a key it never claimed is somebody\n\t\t\t * else's and is left in the address bar untouched.\n\t\t\t *\n\t\t\t * @type {Array<string>}\n\t\t\t */\n\t\t\tpersistedFilterKeys: [...(this.initialQueryFilterKeys || [])],\n\t\t\tinternalSelectedIds: [...this.selectedIds],\n\t\t\t// Folder-sidebar state: selected folder id + the register-fetched list.\n\t\t\tselectedFolderId: null,\n\t\t\t/**\n\t\t\t * The grouping field's facet as last seen with NO folder selected:\n\t\t\t * the whole set of folders, kept on screen while one is selected.\n\t\t\t *\n\t\t\t * @type {Array<object>}\n\t\t\t */\n\t\t\tfolderSidebarAllValues: [],\n\t\t\tfolderRegisterList: [],\n\t\t\t// `x-index` blocks read off the schema rows a `source: 'register'`\n\t\t\t// folder list was built from, keyed by folder id. The layout a case\n\t\t\t// type carries travels with the record rather than being restated\n\t\t\t// in every manifest that lists it.\n\t\t\tfolderRowLayouts: {},\n\t\t\t/** The row the keyboard is on, or -1. */\n\t\t\tfocusedRowIndex: -1,\n\t\t\t/** The row a quick edit is open on, or null. */\n\t\t\tquickEditRow: null,\n\t\t\t/** The row as the server holds it, after a stale save. */\n\t\t\tquickEditServerRow: null,\n\t\t\t/** Whether the shortcut help sheet is open. */\n\t\t\tshowShortcutHelp: false,\n\t\t\t// Mass action dialogs\n\t\t\tshowMassDeleteDialog: false,\n\t\t\tshowMassCopyDialog: false,\n\t\t\tshowExportDialog: false,\n\t\t\tshowImportDialog: false,\n\t\t\t// Single-object dialogs\n\t\t\tshowSingleDeleteDialog: false,\n\t\t\tshowSingleCopyDialog: false,\n\t\t\tshowFormDialogVisible: false,\n\t\t\t// Dialog targets\n\t\t\tactionTargetItem: null,\n\t\t\teditItem: null,\n\t\t\t// Drives the Actions-menu Refresh spinner during a self-fetch\n\t\t\t// refresh, where the host has no promise to bind `:refreshing` to.\n\t\t\tinternalRefreshing: false,\n\t\t\t// Search/Columns sidebar open state. Defaults closed so the page\n\t\t\t// content (table / cards) starts at the top and fills the width;\n\t\t\t// opened on demand via the actions-bar toggle.\n\t\t\tsidebarOpen: false,\n\t\t\t// Saved views (saved-views-ui): the fetched view list, its\n\t\t\t// loading flag, the save-dialog toggle, and the view awaiting\n\t\t\t// delete confirmation.\n\t\t\tsavedViews: [],\n\t\t\tsavedViewsLoading: false,\n\t\t\tshowSaveViewDialog: false,\n\t\t\tviewPendingDelete: null,\n\t\t\t// Split view (case-page-and-list-as-a-place). `splitRowPatches` holds\n\t\t\t// records saved in the pane, keyed by row id, so a save lands on the\n\t\t\t// row without refetching the page and losing the scroll position.\n\t\t\t// `splitViewportWidth` is measured rather than guessed, so the narrow\n\t\t\t// fallback follows a window resize and not only a reload.\n\t\t\tsplitRowPatches: {},\n\t\t\t// Measured here rather than in `mounted`, so the FIRST render is\n\t\t\t// already the right layout. Measuring after the mount rendered\n\t\t\t// the split pane once on a phone and then swapped it for the full\n\t\t\t// page, which reads as the page flickering on every open.\n\t\t\tsplitViewportWidth: (typeof window !== 'undefined' && Number.isFinite(window.innerWidth)) ? window.innerWidth : 0,\n\t\t\tsplitScrollTop: 0,\n\t\t\t// Manual order: the row ids this person dragged into an order, read\n\t\t\t// from and written to their own preferences. Never on the records.\n\t\t\tmanualOrderIds: [],\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * The empty-state copy run through the host translate function\n\t\t * (the injected `cnTranslate`, identity by default). Only this\n\t\t * component's OWN `NcEmptyContent` uses it — the raw `emptyText`\n\t\t * keeps flowing to CnDataTable / CnObjectList / CnCardGrid, which\n\t\t * translate at their own render boundary, so the string is never\n\t\t * run through the translator twice.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tresolvedEmptyText() {\n\t\t\tconst fn = typeof this.cnTranslate === 'function' ? this.cnTranslate : (k) => k\n\t\t\treturn this.emptyText ? fn(this.emptyText) : this.emptyText\n\t\t},\n\n\t\t/**\n\t\t * Whether the host manifest editor is in edit mode (unwraps the injected\n\t\t * `cnEditingBody`, which may be a Vue ref or a plain boolean). Drives the\n\t\t * in-header config cog.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\tisEditMode() {\n\t\t\tconst e = this.cnEditingBody\n\t\t\treturn !!(e && typeof e === 'object' && 'value' in e ? e.value : e)\n\t\t},\n\n\t\t/**\n\t\t * The same map as `effectiveCustomComponents`, under the name the\n\t\t * header-action and bulk-action paths have always used. One of the two\n\t\t * computeds has to be the other, or a change to the resolution order\n\t\t * lands on half the surfaces.\n\t\t *\n\t\t * @return {object}\n\t\t */\n\t\tresolvedCustomComponents() {\n\t\t\treturn this.effectiveCustomComponents\n\t\t},\n\n\t\t/**\n\t\t * Whether a row click opens the row rather than selecting it:\n\t\t * `rowClickToView` is set AND something can open it (a `row-click`\n\t\t * listener, or a named source that routes its own rows). Otherwise a\n\t\t * click on a selectable page selects, so it is never dead.\n\t\t *\n\t\t * `$.vnode.props` is not reactive, so a `row-click` listener attached\n\t\t * or removed after mount does not re-evaluate this.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\trowClickOpens() {\n\t\t\tif (!this.rowClickToView) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tif (this.isNamedSource && (this.rowRoute\n\t\t\t\t|| typeof this.namedSource?.openRow === 'function'\n\t\t\t\t|| this.namedSource?.detailRoute)) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\treturn !!this.$.vnode.props?.onRowClick\n\t\t},\n\n\t\t/**\n\t\t * Merged page-level header actions: drops reserved ids and\n\t\t * resolves declarative `handler` keywords (`navigate` / `emit`\n\t\t * / `none` / registry name) to either a function or\n\t\t * no-handler (emit-only) entries. Function-typed handlers are\n\t\t * passed through untouched. The `none` keyword sets\n\t\t * `_dispatchSuppress: true` and provides a no-op function so\n\t\t * the bar still shows a clickable item.\n\t\t *\n\t\t * @return {Array<object>}\n\t\t */\n\t\tmergedHeaderActions() {\n\t\t\tconst reserved = new Set(['refresh', 'import', 'export', 'copy', 'delete'])\n\t\t\tconst merged = []\n\t\t\tfor (const entry of this.headerActions || []) {\n\t\t\t\tif (entry && reserved.has(entry.id)) {\n\t\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\t\tconsole.warn(`CnIndexPage: headerActions[].id \"${entry.id}\" is reserved by the built-in bar; dropping entry`)\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tmerged.push(this.resolveHeaderHandler(entry))\n\t\t\t}\n\t\t\treturn merged\n\t\t},\n\n\t\t/**\n\t\t * Where the Add button links to: the named source's `addRoute`, when\n\t\t * that is what Add does (no host `@add` listener) and the router can\n\t\t * resolve it. The bar then renders Add as a real link; null otherwise.\n\t\t *\n\t\t * @return {string|object|null}\n\t\t */\n\t\taddLinkTo() {\n\t\t\tif (\n\t\t\t\tthis.$.vnode.props?.onAdd\n\t\t\t\t|| !this.isNamedSource\n\t\t\t\t|| !this.namedSource\n\t\t\t\t|| !this.namedSource.addRoute\n\t\t\t) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\treturn routeHref(this.namedSource.addRoute, this.$router) ? this.namedSource.addRoute : null\n\t\t},\n\n\t\t/**\n\t\t * Declarative bulk actions, validated and normalised.\n\t\t *\n\t\t * `copy` and `delete` are reserved: the selection strip already ships\n\t\t * both as built-ins, so a declared action of the same name would put a\n\t\t * second button with the same word next to one that does something\n\t\t * else.\n\t\t *\n\t\t * Unlike `mergedHeaderActions` this does NOT pre-bind the handler.\n\t\t * A bulk handler needs the selection, and the selection is only known\n\t\t * at click time — binding it here would capture whatever was selected\n\t\t * when the list last re-rendered.\n\t\t *\n\t\t * @return {Array<object>}\n\t\t */\n\t\tmergedBulkActions() {\n\t\t\tconst reserved = new Set(['copy', 'delete'])\n\t\t\tconst merged = []\n\t\t\tfor (const entry of this.bulkActions || []) {\n\t\t\t\tif (!entry || typeof entry !== 'object' || !entry.id || !entry.label) {\n\t\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\t\tconsole.warn(`[CnIndexPage] Ignoring bulkActions entry ${JSON.stringify(entry)}: a bulk action needs an id and a label.`)\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tif (reserved.has(entry.id)) {\n\t\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\t\tconsole.warn(`CnIndexPage: bulkActions[].id \"${entry.id}\" is reserved by the built-in selection strip; dropping entry`)\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tmerged.push(entry)\n\t\t\t}\n\t\t\treturn merged\n\t\t},\n\n\t\t// ── Self-fetch ↔ consumer-managed: the \"effective\" source of each\n\t\t// list datum is the useListView instance in self-fetch mode, the\n\t\t// prop otherwise. The template binds to these.\n\t\t/** True when self-fetch mode is active and the useListView instance exists. */\n\t\tisSelfFetchMode() {\n\t\t\treturn this.isSelfFetch && !!this.list\n\t\t},\n\n\t\t/** Rows: store collection in self-fetch mode, else the `objects` prop. */\n\t\teffectiveObjects() {\n\t\t\tif (this.isNamedSource) {\n\t\t\t\treturn this.namedRows\n\t\t\t}\n\t\t\treturn this.isSelfFetchMode ? (this.list.objects.value || []) : this.objects\n\t\t},\n\n\t\t/**\n\t\t * Rows handed to the table / card grid — `effectiveObjects` re-sorted by\n\t\t * the declarative `defaultSort` spec whenever no explicit user column\n\t\t * sort is active. A live `sortKey` (user clicked a header, or one was\n\t\t * passed in) takes over and this falls through to the unsorted rows so\n\t\t * the server / prop order wins. No-op when `defaultSort` is empty.\n\t\t *\n\t\t * @return {object[]}\n\t\t * @spec openspec/changes/case-page-and-list-as-a-place/specs/index-page/spec.md\n\t\t */\n\t\tdisplayObjects() {\n\t\t\t// The pane's saves and this person's own order are applied over\n\t\t\t// the loaded rows, in that order, so neither needs a refetch and\n\t\t\t// neither writes anything onto the records.\n\t\t\tconst patched = applyRowPatches(this.sortedObjects, this.splitRowPatches, this.rowKey)\n\t\t\treturn this.manualOrderActive\n\t\t\t\t? applyManualOrder(patched, this.manualOrderIds, this.rowKey)\n\t\t\t\t: patched\n\t\t},\n\n\t\t/**\n\t\t * The rows in the order the server, the props or `defaultSort` put\n\t\t * them. Split out of `displayObjects` so the pane's patches and this\n\t\t * person's manual order stack on top of one sorted list rather than\n\t\t * each re-deriving it.\n\t\t *\n\t\t * @return {object[]}\n\t\t * @spec openspec/changes/case-page-and-list-as-a-place/specs/index-page/spec.md\n\t\t */\n\t\tsortedObjects() {\n\t\t\tconst spec = this.effectiveDefaultSort\n\t\t\tif (!spec || spec.length === 0) {\n\t\t\t\treturn this.effectiveObjects\n\t\t\t}\n\t\t\tif (this.effectiveSortKey) {\n\t\t\t\treturn this.effectiveObjects\n\t\t\t}\n\t\t\treturn multiKeySort(this.effectiveObjects, spec)\n\t\t},\n\n\t\t/**\n\t\t * Whether this page declares a working split view.\n\t\t *\n\t\t * @return {boolean}\n\t\t * @spec openspec/changes/case-page-and-list-as-a-place/specs/index-page/spec.md\n\t\t */\n\t\tsplitViewEnabled() {\n\t\t\treturn this.splitView?.enabled === true\n\t\t},\n\n\t\t/**\n\t\t * Which of the three layouts the page is in: the plain list, the list\n\t\t * with the pane beside it, or the record on its own below the\n\t\t * breakpoint.\n\t\t *\n\t\t * @return {'list'|'split'|'detail'}\n\t\t * @spec openspec/changes/case-page-and-list-as-a-place/specs/index-page/spec.md\n\t\t */\n\t\tsplitLayout() {\n\t\t\treturn splitLayoutFor({\n\t\t\t\tenabled: this.splitViewEnabled,\n\t\t\t\tsplitId: this.splitId || null,\n\t\t\t\tviewportWidth: this.splitViewportWidth,\n\t\t\t\tbreakpoint: this.splitView?.breakpoint,\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * The pane's CSS width, falling back when the page declared something\n\t\t * the browser would drop.\n\t\t *\n\t\t * @return {string}\n\t\t * @spec openspec/changes/case-page-and-list-as-a-place/specs/index-page/spec.md\n\t\t */\n\t\tsplitPaneWidth() {\n\t\t\treturn normalisePaneWidth(this.splitView?.paneWidth)\n\t\t},\n\n\t\t/**\n\t\t * Whether to draw the pane's close link. Needs a router: closing pushes\n\t\t * the list route, so without one it would do nothing.\n\t\t *\n\t\t * @return {boolean}\n\t\t * @spec openspec/changes/case-page-and-list-as-a-place/specs/index-page/spec.md\n\t\t */\n\t\tsplitCloseVisible() {\n\t\t\treturn this.splitCloseButton && Boolean(this.$router)\n\t\t},\n\n\t\t/**\n\t\t * Accessible name and tooltip for the pane's close link.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tsplitCloseTitle() {\n\t\t\treturn this.splitCloseLabel || t('nextcloud-vue', 'Close')\n\t\t},\n\n\t\t/**\n\t\t * The list's own address. Falls back to `#` rather than no href: an\n\t\t * `<a>` without one is not focusable.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tsplitCloseHref() {\n\t\t\tconst target = this.splitCloseRoute || this.$route?.meta?.cnPageId || null\n\t\t\tif (!target || typeof this.$router?.resolve !== 'function') {\n\t\t\t\treturn '#'\n\t\t\t}\n\t\t\ttry {\n\t\t\t\treturn this.$router.resolve({ name: target, query: this.$route?.query || {} }).href || '#'\n\t\t\t} catch {\n\t\t\t\t// An unknown route name throws rather than answering.\n\t\t\t\treturn '#'\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Whether a manual row order applies right now. A search, a filter or\n\t\t * a sort the person just chose all mean they asked for a different\n\t\t * order, and a held order that quietly overruled it would read as the\n\t\t * list ignoring them.\n\t\t *\n\t\t * @return {boolean}\n\t\t * @spec openspec/changes/case-page-and-list-as-a-place/specs/index-page/spec.md\n\t\t */\n\t\tmanualOrderActive() {\n\t\t\treturn this.manualOrder === true\n\t\t\t\t&& this.manualOrderIds.length > 0\n\t\t\t\t&& !this.effectiveSortKey\n\t\t},\n\n\t\t/**\n\t\t * Whether the `map` segment is offered in the view toggle. An explicit\n\t\t * `viewModes` whitelist wins; otherwise map is available iff `mapConfig`\n\t\t * carries at least one field. Cards/table are always available.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\tshowMapSegment() {\n\t\t\tif (Array.isArray(this.viewModes)) {\n\t\t\t\treturn this.viewModes.includes('map')\n\t\t\t}\n\t\t\treturn Object.keys(this.mapConfig || {}).length > 0\n\t\t},\n\n\t\t/**\n\t\t * The base view-toggle segments (cards/table/list) passed to CnActionsBar.\n\t\t * Beta's explicit `viewModes` whitelist wins when set; otherwise the\n\t\t * `availableViewModes` list. Map is excluded here — it's added by the\n\t\t * actions bar via the `showMap` bridge driven by `showMapSegment`.\n\t\t *\n\t\t * @return {Array<string>} The ordered toggle segments minus 'map'.\n\t\t */\n\t\teffectiveToggleModes() {\n\t\t\tconst list = (Array.isArray(this.viewModes) && this.viewModes.length)\n\t\t\t\t? this.viewModes\n\t\t\t\t: this.availableViewModes\n\n\t\t\t// 🔴 A SEGMENT ONLY WHEN THE MODE CAN ACTUALLY WORK. A board needs\n\t\t\t// a status field to build its columns from and a date axis needs\n\t\t\t// both dates; offering a segment that opens a view saying it\n\t\t\t// cannot be one is worse than not offering it, because the reader\n\t\t\t// has to click it to find out.\n\t\t\treturn list.filter((mode) => {\n\t\t\t\tif (mode === 'map') {\n\t\t\t\t\treturn false\n\t\t\t\t}\n\t\t\t\tif (mode === 'board') {\n\t\t\t\t\treturn Boolean(this.board?.statusField)\n\t\t\t\t}\n\t\t\t\tif (mode === 'dateAxis') {\n\t\t\t\t\treturn Boolean(this.dateAxis?.startField) && Boolean(this.dateAxis?.endField)\n\t\t\t\t}\n\t\t\t\treturn true\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * The status field's schema, for the board's columns.\n\t\t *\n\t\t * Read from the schema this page already holds rather than fetched: the\n\t\t * stages are a property of the type, and a second read of them could\n\t\t * disagree with the one the table is rendering from.\n\t\t *\n\t\t * @return {?object} The field schema, or null.\n\t\t */\n\t\tboardStatusFieldSchema() {\n\t\t\tconst field = this.board?.statusField\n\t\t\tif (!field) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\treturn this.effectiveSchema?.properties?.[field] || null\n\t\t},\n\n\t\t/**\n\t\t * Whether the list holds one page of more.\n\t\t *\n\t\t * The board says so beside its counts: a count of a page shown as\n\t\t * though it were the total is a number somebody quotes in a meeting.\n\t\t *\n\t\t * @return {boolean} True when there is more than this page.\n\t\t */\n\t\tisPaged() {\n\t\t\treturn Number(this.pagination?.pages || 1) > 1\n\t\t},\n\n\t\t/**\n\t\t * The CnFolderSidebar source. A `register` source is fetched by this\n\t\t * component and handed to CnFolderSidebar as a `custom` folder list.\n\t\t *\n\t\t * @return {string} The resolved CnFolderSidebar source.\n\t\t */\n\t\tfolderSidebarSource() {\n\t\t\tconst s = this.folderSidebar && this.folderSidebar.source\n\t\t\treturn (s === 'register' || !s) ? 'custom' : s\n\t\t},\n\n\t\t/**\n\t\t * The folder list for CnFolderSidebar's custom source: register-fetched\n\t\t * objects (source:'register') or the explicit `folders` (source:'custom').\n\t\t * Empty for the field/files sources, which derive their own list.\n\t\t *\n\t\t * @return {Array<object>} The folder list.\n\t\t */\n\t\tfolderSidebarFolders() {\n\t\t\tif (!this.folderSidebar) {\n\t\t\t\treturn []\n\t\t\t}\n\t\t\tif (this.folderSidebar.source === 'register') {\n\t\t\t\treturn this.folderRegisterList\n\t\t\t}\n\t\t\treturn this.folderSidebar.folders || []\n\t\t},\n\n\t\t/**\n\t\t * The folder entry for the folder currently selected in the sidebar,\n\t\t * or null while \"All\" is selected.\n\t\t *\n\t\t * @return {(object|null)} The active scope's folder entry.\n\t\t * @spec openspec/changes/index-columns-per-scope/specs/index-page/spec.md\n\t\t */\n\t\tactiveScope() {\n\t\t\tif (this.selectedFolderId === null || this.selectedFolderId === undefined) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst idField = this.folderPassthroughIdField\n\t\t\tconst match = this.folderSidebarFolders\n\t\t\t\t.find((f) => f && String(this.getByPath(f, idField)) === String(this.selectedFolderId))\n\t\t\treturn match || null\n\t\t},\n\n\t\t/**\n\t\t * The `x-index` block the active scope's own schema row carries, for a\n\t\t * folder list derived from a register. Empty for every other source:\n\t\t * a folder that is a distinct field value, or one written into the\n\t\t * manifest by hand, has no record behind it to read.\n\t\t *\n\t\t * @return {(object|null)} A row-shaped object carrying `x-index`, or null.\n\t\t * @spec openspec/changes/index-columns-per-scope/specs/index-page/spec.md\n\t\t */\n\t\tactiveScopeRow() {\n\t\t\tif (this.selectedFolderId === null || this.selectedFolderId === undefined) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst carried = this.folderRowLayouts[String(this.selectedFolderId)]\n\t\t\treturn carried ? { 'x-index': carried } : null\n\t\t},\n\n\t\t/**\n\t\t * The list layout the active scope asks for. Two declarations meet\n\t\t * here and they decide different things: this page's `columns` prop\n\t\t * decides which columns the page HAS, and the scope decides which of\n\t\t * them it shows, in what order, sorted by what and searched over what.\n\t\t * A scope naming a column this page does not declare is dropped, so a\n\t\t * scope written before a column was taken out cannot bring it back.\n\t\t *\n\t\t * @return {{columns: (Array|null), sortKeys: Array, searchFields: string[]}}\n\t\t * @spec openspec/changes/index-columns-per-scope/specs/index-page/spec.md\n\t\t */\n\t\tactiveScopeLayout() {\n\t\t\tif (!this.folderSidebar) {\n\t\t\t\treturn { columns: null, sortKeys: [], searchFields: [] }\n\t\t\t}\n\t\t\treturn resolveScopeLayout({\n\t\t\t\tscope: this.activeScope,\n\t\t\t\trow: this.activeScopeRow,\n\t\t\t\tpageColumns: this.declaredColumns,\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * The fields the search box searches over while a scope declaring\n\t\t * `searchFields` is active. Read by `useSelfFetchList`'s fixed-filter\n\t\t * getter off this instance, so a scope change re-scopes the next fetch\n\t\t * without a second search path.\n\t\t *\n\t\t * @return {string[]} The scope's search fields, or [].\n\t\t * @spec openspec/changes/index-columns-per-scope/specs/index-page/spec.md\n\t\t */\n\t\tactiveScopeSearchFields() {\n\t\t\treturn this.activeScopeLayout.searchFields\n\t\t},\n\n\t\t/**\n\t\t * The teams this person is treated as having claimed: what they stored,\n\t\t * narrowed to what the instance offers them now.\n\t\t *\n\t\t * @return {string[]} The claimed team ids.\n\t\t * @spec openspec/changes/working-list-row-actions/specs/index-page/spec.md\n\t\t */\n\t\tclaimedTeamIds() {\n\t\t\treturn resolveClaimedTeams(this.offeredTeams, this.claimedTeams)\n\t\t},\n\n\t\t/**\n\t\t * The saved views this page renders as tabs, and the ones that stay in\n\t\t * the views control. A view is in one or the other, never both.\n\t\t *\n\t\t * Split out of the SCOPED views, not the fetched ones: a view made on\n\t\t * another page names fields this schema does not have, and is no more\n\t\t * use as a tab than it is as a dropdown entry.\n\t\t *\n\t\t * @return {{tabs: Array<object>, control: Array<object>}}\n\t\t * @spec openspec/changes/working-list-row-actions/specs/index-page/spec.md\n\t\t */\n\t\tsplitSavedViews() {\n\t\t\treturn splitViewsIntoTabs(this.visibleSavedViews, this.viewTabs)\n\t\t},\n\n\t\t/**\n\t\t * The lens tabs, as the tab strip this page already has understands\n\t\t * them, with each lens's claim tokens resolved against this person.\n\t\t *\n\t\t * @return {Array<object>} The tabs.\n\t\t * @spec openspec/changes/working-list-row-actions/specs/index-page/spec.md\n\t\t */\n\t\tlensTabs() {\n\t\t\treturn this.splitSavedViews.tabs.map((view) => {\n\t\t\t\tconst tab = viewAsTab(view)\n\t\t\t\tconst { filter, narrowsToNothing } = resolveClaimTokens(tab.filter, { teams: this.claimedTeamIds })\n\t\t\t\treturn { ...tab, filter, narrowsToNothing }\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * The views the control lists: every saved view this page did not name\n\t\t * as a tab.\n\t\t *\n\t\t * @return {Array<object>} The views.\n\t\t */\n\t\tviewsForControl() {\n\t\t\treturn this.splitSavedViews.control\n\t\t},\n\n\t\t/**\n\t\t * What the tab strip shows: the page's lens tabs when it named any,\n\t\t * else its quick filters. One strip, never two, so a person is not\n\t\t * offered the same narrowing twice under two names.\n\t\t *\n\t\t * @return {(Array<object>|null)} The tabs.\n\t\t * @spec openspec/changes/working-list-row-actions/specs/index-page/spec.md\n\t\t */\n\t\ttabStripEntries() {\n\t\t\tif (this.lensTabs.length > 0) {\n\t\t\t\treturn this.lensTabs\n\t\t\t}\n\t\t\treturn this.effectiveQuickFilters\n\t\t},\n\n\t\t/** @return {Array<object>} The shortcuts the help sheet lists. */\n\t\tshortcutHelpEntries() {\n\t\t\treturn LIST_SHORTCUTS.filter((entry) => typeof this.listShortcutHandlers[entry.id] === 'function')\n\t\t},\n\n\t\t/** @return {string} Heading of the shortcut help sheet. */\n\t\tshortcutHelpTitle() {\n\t\t\treturn t('nextcloud-vue', 'Keyboard shortcuts')\n\t\t},\n\n\t\t/** @return {string} Label of the help sheet's close button. */\n\t\tcloseLabel() {\n\t\t\treturn t('nextcloud-vue', 'Close')\n\t\t},\n\n\t\t/**\n\t\t * Whether the active lens narrowed to nothing because this person has\n\t\t * claimed no teams. The page says that, rather than showing an empty\n\t\t * list under a heading that claims to be filtering by their teams.\n\t\t *\n\t\t * @return {boolean} True when the active lens has nothing to match.\n\t\t */\n\t\tactiveLensNarrowsToNothing() {\n\t\t\tconst index = this.activeQuickFilterIndex\n\t\t\tif (index === null || index === undefined || this.lensTabs.length === 0) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\treturn Boolean(this.lensTabs[index] && this.lensTabs[index].narrowsToNothing)\n\t\t},\n\n\t\t/**\n\t\t * The declarative sort this page applies, with the priority sort folded\n\t\t * in when the page names a priority field. The value is READ off the\n\t\t * record; nothing here derives one.\n\t\t *\n\t\t * @return {Array<object>} The sort spec.\n\t\t * @spec openspec/changes/working-list-row-actions/specs/index-page/spec.md\n\t\t */\n\t\teffectiveDefaultSort() {\n\t\t\tconst declared = Array.isArray(this.defaultSort) ? this.defaultSort : []\n\t\t\tif (this.priorityField === '') {\n\t\t\t\treturn declared\n\t\t\t}\n\t\t\tif (declared.some((entry) => entry && entry.field === this.priorityField)) {\n\t\t\t\treturn declared.map((entry) => (entry && entry.field === this.priorityField\n\t\t\t\t\t? { ...entry, levels: this.priorityLevels }\n\t\t\t\t\t: entry))\n\t\t\t}\n\t\t\treturn [{ field: this.priorityField, order: 'desc', levels: this.priorityLevels }, ...declared]\n\t\t},\n\n\t\t/**\n\t\t * The list's keyboard shortcuts, bound to what this page can do. A\n\t\t * shortcut with nothing behind it is left out rather than listed.\n\t\t *\n\t\t * @return {object} Handlers by shortcut id.\n\t\t * @spec openspec/changes/working-list-row-actions/specs/index-page/spec.md\n\t\t */\n\t\tlistShortcutHandlers() {\n\t\t\tconst handlers = {\n\t\t\t\t'row-next': () => this.moveFocusedRow(1),\n\t\t\t\t'row-previous': () => this.moveFocusedRow(-1),\n\t\t\t\t'row-open': () => this.focusedRow && this.onRowClick(this.focusedRow),\n\t\t\t\t'row-select': () => this.toggleFocusedRowSelection(),\n\t\t\t\t'list-help': () => { this.showShortcutHelp = true },\n\t\t\t}\n\t\t\tif (this.quickEditFields.length > 0) {\n\t\t\t\thandlers['row-quick-edit'] = () => this.focusedRow && this.openQuickEdit(this.focusedRow)\n\t\t\t}\n\t\t\tif (this.focusedRow && this.rowActionsFor(this.focusedRow).length > 0) {\n\t\t\t\thandlers['row-primary'] = () => this.runPrimaryRowAction()\n\t\t\t}\n\t\t\treturn handlers\n\t\t},\n\n\t\t/**\n\t\t * The shortcuts as command-palette entries, so someone who has never\n\t\t * used this list can find every one of them.\n\t\t *\n\t\t * @return {Array<object>} The palette entries.\n\t\t */\n\t\tlistPaletteEntries() {\n\t\t\treturn listPaletteCommands(this.listShortcutHandlers)\n\t\t},\n\n\t\t/**\n\t\t * The row the keyboard is on, or null.\n\t\t *\n\t\t * @return {(object|null)} The focused row.\n\t\t */\n\t\tfocusedRow() {\n\t\t\tconst rows = this.displayObjects || []\n\t\t\tif (this.focusedRowIndex < 0 || this.focusedRowIndex >= rows.length) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\treturn rows[this.focusedRowIndex]\n\t\t},\n\n\t\t/**\n\t\t * The grouping field for a `field`-source folder sidebar.\n\t\t *\n\t\t * @return {string} The property the folders group by, or ''.\n\t\t */\n\t\tfolderSidebarGroupBy() {\n\t\t\tif (!this.folderSidebar) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn this.folderSidebar.groupBy || this.folderSidebar.field || ''\n\t\t},\n\n\t\t/**\n\t\t * Facet buckets for the folder sidebar's grouping field.\n\t\t *\n\t\t * `:objects` is the current page, so folders derived from it alone list\n\t\t * only the values that happen to have a row on screen. The platform\n\t\t * already answers this properly: OpenRegister computes facets with the\n\t\t * pagination parameters removed, so a facet over the grouping field is\n\t\t * the complete set of distinct values with their true totals. Any\n\t\t * property marked `facetable: true` gets one under `_facets=extend`.\n\t\t *\n\t\t * Read from the live store first, so the folder pane does not depend on\n\t\t * the metadata sidebar being switched on, then from the sidebar config\n\t\t * a consumer-managed page supplies, then from the folder config itself.\n\t\t *\n\t\t * @return {Array<object>} Normalised facet values, or [] when none.\n\t\t */\n\t\tfolderSidebarLiveFacetValues() {\n\t\t\tconst field = this.folderSidebarGroupBy\n\t\t\tif (!field) {\n\t\t\t\treturn []\n\t\t\t}\n\n\t\t\tconst facets = this.storeFacets\n\t\t\t\t|| (this.folderSidebar && this.folderSidebar.facets)\n\t\t\t\t|| this.resolvedSidebar.facets\n\t\t\t\t|| {}\n\n\t\t\treturn facets[field]?.values || []\n\t\t},\n\n\t\t/**\n\t\t * The folders to show: the live facet, except while a folder is\n\t\t * selected. Selecting one filters the query by the grouping field, so\n\t\t * the live facet then holds that one value and every other folder\n\t\t * would vanish, which makes switching folders impossible. The set seen\n\t\t * before the selection stands in until it is cleared.\n\t\t *\n\t\t * @return {Array<object>} Normalised facet values.\n\t\t */\n\t\tfolderSidebarFacetValues() {\n\t\t\tconst selected = this.selectedFolderId !== null && this.selectedFolderId !== undefined\n\t\t\tif (selected && this.folderSidebarAllValues.length > 0) {\n\t\t\t\treturn this.folderSidebarAllValues\n\t\t\t}\n\t\t\treturn this.folderSidebarLiveFacetValues\n\t\t},\n\n\t\t/**\n\t\t * The facet buckets OpenRegister computed for THIS page's query, or\n\t\t * null when this page does not fetch its own rows.\n\t\t *\n\t\t * Only self-fetch mode has them: the store keys facets by object type\n\t\t * and rewrites that entry from the same response the rows came from, so\n\t\t * what this returns always describes the query currently on screen.\n\t\t * Consumer-managed, named-source and entity-source pages never fetch\n\t\t * through the store, so there is nothing here to read and the caller\n\t\t * falls back to whatever the consumer passed in.\n\t\t *\n\t\t * @return {object|null} The live facet map, or null.\n\t\t */\n\t\tstoreFacets() {\n\t\t\treturn this.isSelfFetchMode ? (this.list.facets?.value || null) : null\n\t\t},\n\n\t\t/**\n\t\t * Facet data for the index sidebar.\n\t\t *\n\t\t * The `sidebar.facets` key is a DATA channel, not a declaration of\n\t\t * which facets exist: its docblock calls it \"live facet data\" and it\n\t\t * feeds CnIndexSidebar's `facetData` prop unchanged. Which filters the\n\t\t * sidebar offers comes from the schema (`filtersFromSchema` over the\n\t\t * facetable properties), and a filter with no bucket falls back to its\n\t\t * own declarative `options`. So there is nothing to merge here — the\n\t\t * two sources answer the same question, and the fresher one wins.\n\t\t *\n\t\t * A manifest-driven page has no consumer to fill `sidebar.facets`, so\n\t\t * before this read the sidebar was handed the manifest's sidebar config\n\t\t * and every facet rendered \"No results\" while the response body carried\n\t\t * the buckets.\n\t\t *\n\t\t * Note the store wins even when it is empty (`{}` is truthy), which is\n\t\t * deliberate and the conservative direction: until the first response\n\t\t * lands, offering no options is right, and offering options left over\n\t\t * from a config that never described this query would be worse. Same\n\t\t * precedence `folderSidebarFacetValues` has always used.\n\t\t *\n\t\t * @return {object} `{ fieldName: { values: [...] } }`, possibly empty.\n\t\t */\n\t\teffectiveFacetData() {\n\t\t\treturn this.storeFacets || this.resolvedSidebar.facets || {}\n\t\t},\n\n\t\t/**\n\t\t * Whether the rows handed to the folder sidebar are only part of the\n\t\t * result set. True when the list is paged beyond what is loaded and no\n\t\t * facet is available to complete the picture, which is exactly when\n\t\t * folders can be missing and a per-page count would read as a total.\n\t\t *\n\t\t * @return {boolean} True when the folder list is knowingly incomplete.\n\t\t */\n\t\tfolderSidebarPartial() {\n\t\t\tif (this.folderSidebarFacetValues.length > 0) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tconst total = Number(this.effectivePagination?.total ?? 0)\n\t\t\treturn total > this.effectiveObjects.length\n\t\t},\n\n\t\t/**\n\t\t * id/name field names passed to CnFolderSidebar for the custom folder\n\t\t * list. The `register` source is pre-mapped to `{ id, name }` by\n\t\t * `loadFolderRegister`, so it uses the plain keys; a `custom` source\n\t\t * passes the objects through untouched and honours the config's fields.\n\t\t *\n\t\t * @return {string} The id field key.\n\t\t */\n\t\tfolderPassthroughIdField() {\n\t\t\tif (this.folderSidebar && this.folderSidebar.source === 'register') {\n\t\t\t\treturn 'id'\n\t\t\t}\n\t\t\treturn (this.folderSidebar && this.folderSidebar.idField) || 'id'\n\t\t},\n\n\t\t/**\n\t\t * @return {string} The name field key for CnFolderSidebar's custom list.\n\t\t */\n\t\tfolderPassthroughNameField() {\n\t\t\tif (this.folderSidebar && this.folderSidebar.source === 'register') {\n\t\t\t\treturn 'name'\n\t\t\t}\n\t\t\treturn (this.folderSidebar && this.folderSidebar.nameField) || 'name'\n\t\t},\n\n\t\t/**\n\t\t * Inline GeoJSON markers for the map view, built from the CURRENT filtered\n\t\t * `displayObjects` using `mapConfig`. Each feature stashes the row's\n\t\t * `rowKey` in `properties` so a marker click can resolve back to its source\n\t\t * row. Rows without finite, resolvable geometry are skipped silently.\n\t\t *\n\t\t * @return {{ features: object[], popupField: (string|undefined) }}\n\t\t */\n\t\tmapMarkers() {\n\t\t\tconst features = []\n\t\t\tfor (const row of this.displayObjects) {\n\t\t\t\tconst geometry = this.resolveRowGeometry(row)\n\t\t\t\tif (!geometry) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tfeatures.push({\n\t\t\t\t\ttype: 'Feature',\n\t\t\t\t\tgeometry,\n\t\t\t\t\tproperties: {\n\t\t\t\t\t\t[this.rowKey]: row[this.rowKey],\n\t\t\t\t\t\t...(this.mapConfig.popupField ? { [this.mapConfig.popupField]: row[this.mapConfig.popupField] } : {}),\n\t\t\t\t\t},\n\t\t\t\t})\n\t\t\t}\n\t\t\treturn { features, popupField: this.mapConfig.popupField }\n\t\t},\n\n\t\t/**\n\t\t * Whether to cluster the plotted markers. ON unless `mapConfig.clustering`\n\t\t * says otherwise, which is the opposite of `CnMapWidget`'s own default.\n\t\t *\n\t\t * An index map plots the whole filtered result set, and rows sharing one\n\t\t * address is the normal case here, not an edge case — several permits on a\n\t\t * building, a street of complaints. Unclustered, those markers stack at\n\t\t * identical pixels: the topmost swallows every click and the rest are\n\t\t * unreachable with nothing on screen saying they exist. Clustering shows the\n\t\t * count and, since all children stay in one cluster down to max zoom,\n\t\t * markercluster spiderfies them into a fan on click instead of zooming\n\t\t * uselessly. Non-point geometry skips the cluster group entirely, so areas\n\t\t * are unaffected.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\tmapClustering() {\n\t\t\treturn this.mapConfig.clustering !== false\n\t\t},\n\n\t\t/**\n\t\t * Extra (non-background) map layers — WMS / WFS / GeoJSON overlays declared in\n\t\t * `mapConfig.layers`. The background map comes from `mapBasemaps` instead, so\n\t\t * this is empty unless the consumer configured overlays.\n\t\t *\n\t\t * @return {Array<object>}\n\t\t */\n\t\tmapLayers() {\n\t\t\tif (Array.isArray(this.mapConfig.layers) && this.mapConfig.layers.length > 0) {\n\t\t\t\treturn this.mapConfig.layers\n\t\t\t}\n\t\t\treturn []\n\t\t},\n\n\t\t/**\n\t\t * Switchable background maps.\n\t\t *\n\t\t * Defaults to OpenStreetMap standard ONLY. Every extra basemap is an `<img>`\n\t\t * load from a third-party host, which a Nextcloud app's Content-Security-Policy\n\t\t * (`img-src`) blocks unless that app explicitly allowlists the host — so a\n\t\t * richer default would ship dead options to consumers that never widened their\n\t\t * CSP. Apps that want a switcher declare the set in `mapConfig.basemaps` AND\n\t\t * allowlist those hosts (see Dossiq's `relaxCspForMapTiles()`).\n\t\t *\n\t\t * @return {Array<object>}\n\t\t */\n\t\tmapBasemaps() {\n\t\t\tif (Array.isArray(this.mapConfig.basemaps) && this.mapConfig.basemaps.length > 0) {\n\t\t\t\treturn this.mapConfig.basemaps\n\t\t\t}\n\t\t\treturn [{\n\t\t\t\tname: t('nextcloud-vue', 'Standard'),\n\t\t\t\turl: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',\n\t\t\t\tattribution: '© OpenStreetMap contributors',\n\t\t\t}]\n\t\t},\n\n\t\t/**\n\t\t * Initial map centre `[lat, lng]`. Uses the centroid of the plotted markers\n\t\t * when any exist (CnMapWidget's autoFit then tightens to the bounds); falls\n\t\t * back to `mapConfig.center`, then a neutral world view when the set is empty.\n\t\t *\n\t\t * @return {[number, number]}\n\t\t */\n\t\tmapCenter() {\n\t\t\tconst feats = this.mapMarkers.features\n\t\t\tif (feats.length > 0) {\n\t\t\t\tlet sumLat = 0\n\t\t\t\tlet sumLng = 0\n\t\t\t\tfor (const f of feats) {\n\t\t\t\t\tconst _p = this.firstLatLng(f.geometry)\n\t\t\t\t\tif (!_p) {\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\t\t\t\t\tsumLng += _p.lng\n\t\t\t\t\tsumLat += _p.lat\n\t\t\t\t}\n\t\t\t\treturn [sumLat / feats.length, sumLng / feats.length]\n\t\t\t}\n\t\t\tif (Array.isArray(this.mapConfig.center) && this.mapConfig.center.length === 2) {\n\t\t\t\treturn this.mapConfig.center\n\t\t\t}\n\t\t\treturn [0, 0]\n\t\t},\n\n\t\t/** Loading flag: store loading in self-fetch mode, else the `loading` prop. */\n\t\teffectiveLoading() {\n\t\t\tif (this.isNamedSource) {\n\t\t\t\treturn this.namedLoading\n\t\t\t}\n\t\t\treturn this.isSelfFetchMode ? !!this.list.loading.value : this.loading\n\t\t},\n\n\t\t/**\n\t\t * Whether to replace the page with the full loading spinner. Only on an\n\t\t * INITIAL fetch — i.e. while loading AND there is no data to show yet.\n\t\t * A background refresh (e.g. the on-visibility refetch) keeps the\n\t\t * existing rows on screen and updates them in place instead of flashing\n\t\t * the spinner over already-rendered content (fetchCollection preserves\n\t\t * the prior collection until the new results arrive).\n\t\t */\n\t\tshowInitialLoader() {\n\t\t\treturn this.effectiveLoading && this.effectiveObjects.length === 0\n\t\t},\n\n\t\t/**\n\t\t * Refresh-spinner flag for the Actions menu: the `refreshing` prop\n\t\t * OR (self-fetch mode) the internally-tracked refresh. Lets manifest\n\t\t * and self-fetch pages spin the Refresh action without the host\n\t\t * wiring `:refreshing` (it has no fetch promise to await).\n\t\t */\n\t\teffectiveRefreshing() {\n\t\t\treturn this.refreshing || this.internalRefreshing\n\t\t},\n\n\t\t/** Pagination: store pagination in self-fetch mode, else the `pagination` prop. */\n\t\teffectivePagination() {\n\t\t\treturn this.isSelfFetchMode ? this.list.pagination.value : this.pagination\n\t\t},\n\n\t\t/** Resolved schema OBJECT (for column generation / icons / labels). */\n\t\teffectiveSchema() {\n\t\t\tif (this.isSelfFetchMode) {\n\t\t\t\treturn this.list.schema.value\n\t\t\t}\n\t\t\treturn (this.schema && typeof this.schema === 'object') ? this.schema : null\n\t\t},\n\n\t\t/**\n\t\t * Schema slug for the export-leaf URL — the `schema` prop directly\n\t\t * when it's a string (self-fetch mode's precondition), else the\n\t\t * resolved schema object's `slug`/`name`.\n\t\t */\n\t\texportSchemaSlug() {\n\t\t\tif (typeof this.schema === 'string') {\n\t\t\t\treturn this.schema\n\t\t\t}\n\t\t\treturn this.effectiveSchema?.slug || this.effectiveSchema?.name || ''\n\t\t},\n\n\t\t/**\n\t\t * Whether the native Export menu renders in the toolbar: opt-in via\n\t\t * `allowExport` AND the resolved schema flagged `exportable: true`.\n\t\t * Default-safe — false unless both hold.\n\t\t */\n\t\tshowExportMenu() {\n\t\t\treturn Boolean(this.allowExport) && Boolean(this.effectiveSchema?.exportable) && Boolean(this.register) && Boolean(this.exportSchemaSlug)\n\t\t},\n\n\t\t/**\n\t\t * The signed-in NC user id — passed to CnSavedViewsControl to gate\n\t\t * the per-view delete affordance (saved-views-ui).\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\t/**\n\t\t * The page as `savedViewScope` and `viewMatchesScope` take it.\n\t\t *\n\t\t * @return {{ scope: string, register: string, schema: object|string|null }} The page's source.\n\t\t */\n\t\tsavedViewsPage() {\n\t\t\treturn { scope: this.savedViewsScope, register: this.register, schema: this.schema }\n\t\t},\n\n\t\t/**\n\t\t * The fetched views that belong on this page.\n\t\t *\n\t\t * @return {Array<object>} The views to offer.\n\t\t */\n\t\tvisibleSavedViews() {\n\t\t\treturn this.savedViews.filter((view) => viewMatchesScope(view, this.savedViewsPage))\n\t\t},\n\n\t\tcurrentSavedViewsUserId() {\n\t\t\tconst user = getCurrentUser()\n\t\t\treturn (user && user.uid) || ''\n\t\t},\n\n\t\t/**\n\t\t * Confirmation message for the delete-saved-view dialog.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tdeleteViewMessage() {\n\t\t\tif (!this.viewPendingDelete) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn t('nextcloud-vue', 'Delete the view \"{name}\"? This cannot be undone.', { name: this.viewPendingDelete.name })\n\t\t},\n\n\t\t/** Sort key / order: list state in self-fetch mode, else the props. */\n\t\teffectiveSortKey() {\n\t\t\treturn this.isSelfFetchMode ? this.list.sortKey.value : this.sortKey\n\t\t},\n\n\t\teffectiveSortOrder() {\n\t\t\treturn this.isSelfFetchMode ? this.list.sortOrder.value : this.sortOrder\n\t\t},\n\n\t\t/**\n\t\t * Ordered multi-column sort state fed to CnDataTable: the self-fetch\n\t\t * list's `sortKeys` in self-fetch mode, else the host-controlled\n\t\t * `sortKeys` prop.\n\t\t *\n\t\t * @return {Array<{key: string, order: 'asc'|'desc'}>}\n\t\t */\n\t\teffectiveSortKeys() {\n\t\t\treturn this.isSelfFetchMode ? (this.list.sortKeys.value || []) : this.sortKeys\n\t\t},\n\n\t\t/** Search term / visible columns / active facet filters for the embedded sidebar. */\n\t\teffectiveSearchValue() {\n\t\t\treturn this.isSelfFetchMode ? (this.list.searchTerm.value || '') : (this.searchValue || '')\n\t\t},\n\n\t\teffectiveVisibleColumns() {\n\t\t\treturn this.isSelfFetchMode ? this.list.visibleColumns.value : this.visibleColumns\n\t\t},\n\n\t\teffectiveActiveFilters() {\n\t\t\tif (this.isSelfFetchMode) {\n\t\t\t\treturn this.list.activeFilters.value || {}\n\t\t\t}\n\t\t\t// A named source's own state wins over the prop, and only once it\n\t\t\t// holds something: a consumer-managed page that ALSO names a\n\t\t\t// source keeps its `activeFilters` until the sidebar is used.\n\t\t\tif (this.isNamedSource && Object.keys(this.namedActiveFilters || {}).length > 0) {\n\t\t\t\treturn this.namedActiveFilters\n\t\t\t}\n\t\t\treturn this.activeFilters || {}\n\t\t},\n\n\t\t/**\n\t\t * Enum schema columns offered in the header filter menu: one entry per\n\t\t * visible column whose schema property declares an `enum`, with its\n\t\t * values. Empty (so the menu hides) when there is no schema or no enum\n\t\t * column. Drives the `showFilterMenu` funnel button.\n\t\t *\n\t\t * @return {Array<{ key: string, label: string, values: string[] }>}\n\t\t */\n\t\tfilterableFields() {\n\t\t\tconst props = this.effectiveSchema?.properties || {}\n\t\t\tconst out = []\n\t\t\tfor (const col of this.tableColumns) {\n\t\t\t\tconst key = typeof col === 'string' ? col : col.key\n\t\t\t\tconst def = props[key] || {}\n\t\t\t\tconst colObj = typeof col === 'object' ? col : {}\n\t\t\t\t// Source the filter values, in priority order: schema enum, a\n\t\t\t\t// column `enum` hint, or a badge column's colorMap keys (so a\n\t\t\t\t// status column stays filterable even when the runtime schema\n\t\t\t\t// doesn't carry the enum).\n\t\t\t\tlet values = null\n\t\t\t\tif (Array.isArray(def.enum) && def.enum.length) {\n\t\t\t\t\tvalues = def.enum\n\t\t\t\t} else if (Array.isArray(colObj.enum) && colObj.enum.length) {\n\t\t\t\t\tvalues = colObj.enum\n\t\t\t\t} else if (colObj.widget === 'badge' && colObj.widgetProps && colObj.widgetProps.colorMap) {\n\t\t\t\t\tvalues = Object.keys(colObj.widgetProps.colorMap)\n\t\t\t\t}\n\t\t\t\tif (values && values.length) {\n\t\t\t\t\tout.push({ key, label: this.cnTranslate(colObj.label || def.title || key), values: values.map((v) => String(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 * Ordered column definitions the sidebar's Columns tab governs:\n\t\t * schema-derived columns, the built-in Metadata group (when shown),\n\t\t * and any external columnGroups. Mirrors CnIndexSidebar's column\n\t\t * universe, and doubles as the source of definitions for columns the\n\t\t * user enables that aren't in the configured `columns` list (metadata\n\t\t * fields, schema properties beyond the default set).\n\t\t */\n\t\tgovernedColumns() {\n\t\t\tconst defs = []\n\t\t\tif (this.effectiveSchema) {\n\t\t\t\t// English labels here: `governedColumns` feeds `tableColumns`,\n\t\t\t\t// which CnDataTable render-translates via its own `cnTranslate`\n\t\t\t\t// injection. Translating here too would double-translate. The\n\t\t\t\t// column-picker menu that renders these labels directly applies\n\t\t\t\t// `cnTranslate` at render instead.\n\t\t\t\tdefs.push(...columnsFromSchema(this.effectiveSchema, {}))\n\t\t\t\tif (this.resolvedSidebar.showMetadata !== false) {\n\t\t\t\t\tdefs.push(...METADATA_COLUMNS)\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst groups = this.resolvedSidebar.columnGroups || []\n\t\t\tgroups.forEach((g) => defs.push(...(g.columns || [])))\n\t\t\treturn defs\n\t\t},\n\n\t\t/**\n\t\t * Set of keys the sidebar governs (see `governedColumns`). Used so\n\t\t * tableColumns only hides columns the user can actually toggle back\n\t\t * on — custom columns outside this set are never silently dropped.\n\t\t */\n\t\tsidebarGovernedColumnKeys() {\n\t\t\treturn new Set(this.governedColumns.map((c) => c.key))\n\t\t},\n\n\t\t/**\n\t\t * The columns this page DECLARES it has: the `columns` prop, or a named\n\t\t * source's own list when the manifest set none. This is the membership\n\t\t * list, separate from `tableColumns`, which is what ends up rendered\n\t\t * after the sidebar scope and the user's visible-column set have had\n\t\t * their say over it.\n\t\t *\n\t\t * @return {Array} The declared column list.\n\t\t */\n\t\tdeclaredColumns() {\n\t\t\t// A NAMED SOURCE SUPPLIES ITS OWN COLUMNS when the manifest does not.\n\t\t\t// Without this the adapter's `columns` were defined and never read:\n\t\t\t// an `entitySource` page with no explicit `columns` fell through to\n\t\t\t// `governedColumns`, which derives from a SCHEMA — and a named\n\t\t\t// source has none. The table then rendered its rows with no columns\n\t\t\t// at all, which looks like an empty list rather than a missing\n\t\t\t// config. A manifest that DOES set columns still wins, which is\n\t\t\t// what makes the source a default rather than a constraint.\n\t\t\treturn (this.columns && this.columns.length > 0)\n\t\t\t\t? this.columns\n\t\t\t\t: ((this.isNamedSource && this.namedSource && this.namedSource.columns) || [])\n\t\t},\n\n\t\t/**\n\t\t * Columns handed to CnDataTable. Starts from `declaredColumns`, or from\n\t\t * the active folder-sidebar scope's selection of them (with any\n\t\t * `aggregate` block lacking a `register` defaulted to this page's\n\t\t * `register` slug, so manifests can omit `aggregate.register`). When a\n\t\t * visible-column set exists, governed columns the user toggled off are\n\t\t * hidden, and governed columns the user toggled on that aren't already\n\t\t * in the list (metadata fields, extra schema properties) are appended\n\t\t * using their sidebar definitions. Custom columns outside the sidebar's\n\t\t * universe are untouched.\n\t\t */\n\t\ttableColumns() {\n\t\t\tconst reg = typeof this.register === 'string' && this.register ? this.register : undefined\n\t\t\t// The active sidebar scope may narrow and reorder the page's\n\t\t\t// declared columns; it cannot add to them.\n\t\t\tlet cols = this.activeScopeLayout.columns || this.declaredColumns\n\t\t\tif (reg) {\n\t\t\t\tcols = cols.map((c) => (\n\t\t\t\t\tc && c.aggregate && !c.aggregate.register\n\t\t\t\t\t\t? { ...c, aggregate: { ...c.aggregate, register: reg } }\n\t\t\t\t\t\t: c\n\t\t\t\t))\n\t\t\t}\n\t\t\tconst visible = this.effectiveVisibleColumns\n\t\t\tif (!Array.isArray(visible)) {\n\t\t\t\treturn cols\n\t\t\t}\n\n\t\t\tconst governed = this.sidebarGovernedColumnKeys\n\t\t\tcols = cols.filter((c) => {\n\t\t\t\tconst key = typeof c === 'string' ? c : c.key\n\t\t\t\treturn !governed.has(key) || visible.includes(key)\n\t\t\t})\n\n\t\t\tconst present = new Set(cols.map((c) => (typeof c === 'string' ? c : c.key)))\n\t\t\tconst byKey = new Map(this.governedColumns.map((c) => [c.key, c]))\n\t\t\tvisible.forEach((key) => {\n\t\t\t\tif (present.has(key)) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tconst def = byKey.get(key)\n\t\t\t\tif (def) {\n\t\t\t\t\tcols.push({ ...def })\n\t\t\t\t\tpresent.add(key)\n\t\t\t\t}\n\t\t\t})\n\t\t\treturn cols\n\t\t},\n\n\t\t/** Resolved icon — explicit prop overrides schema.icon */\n\t\tresolvedIcon() {\n\t\t\tif (this.icon) {\n\t\t\t\treturn this.icon\n\t\t\t}\n\t\t\treturn this.effectiveSchema?.icon || ''\n\t\t},\n\n\t\t/**\n\t\t * Built-in row actions based on show*Action props.\n\t\t *\n\t\t * A NAMED SOURCE changes the defaults, as a contract rather than a\n\t\t * page-by-page opt-out (observed on the fleet's flows pages, where the\n\t\t * menu carried FIVE actions and three of them were broken):\n\t\t *\n\t\t * - View and the schema-form Edit are OFF. A source has no schema, so\n\t\t * the form modal can only ever render empty; and the source's own\n\t\t * open/Edit action already navigates to the detail page, which IS\n\t\t * the view. Rendering the built-ins beside it gave every flow list a\n\t\t * dead View and an Edit that opened an empty dialog.\n\t\t * - Copy and Delete render only when the source implements them\n\t\t * (`copyRow` / `deleteRow`): the built-in dialogs' confirm otherwise\n\t\t * emits into a manifest page with nobody listening, a dialog that\n\t\t * looks like it acted and did not.\n\t\t *\n\t\t * An EXPLICITLY passed show*Action prop always wins, both ways.\n\t\t */\n\t\tdefaultActions() {\n\t\t\treturn buildDefaultActions({\n\t\t\t\tflags: this.isNamedSource && this.namedSource\n\t\t\t\t\t? {\n\t\t\t\t\t\t\tview: this.hasExplicitProp('showViewAction') && this.showViewAction,\n\t\t\t\t\t\t\tedit: this.hasExplicitProp('showEditAction') && this.showEditAction,\n\t\t\t\t\t\t\tcopy: this.hasExplicitProp('showCopyAction')\n\t\t\t\t\t\t\t\t? this.showCopyAction\n\t\t\t\t\t\t\t\t: typeof this.namedSource.copyRow === 'function',\n\t\t\t\t\t\t\tdel: this.hasExplicitProp('showDeleteAction')\n\t\t\t\t\t\t\t\t? this.showDeleteAction\n\t\t\t\t\t\t\t\t: typeof this.namedSource.deleteRow === 'function',\n\t\t\t\t\t\t}\n\t\t\t\t\t: {\n\t\t\t\t\t\t\tview: this.showViewAction,\n\t\t\t\t\t\t\tedit: this.showEditAction,\n\t\t\t\t\t\t\tcopy: this.showCopyAction,\n\t\t\t\t\t\t\tdel: this.showDeleteAction,\n\t\t\t\t\t\t},\n\t\t\t\t// The View action is always an eye — a universal \"view\" affordance,\n\t\t\t\t// independent of the object's schema icon (which is the header icon).\n\t\t\t\tviewIcon: Eye,\n\t\t\t\tviewTo: this.viewTo,\n\t\t\t\thandlers: {\n\t\t\t\t\tonView: (row) => this.onView(row),\n\t\t\t\t\tonEdit: (row) => {\n\t\t\t\t\t\tif (this.editOpensDetail) {\n\t\t\t\t\t\t\t/**\n\t\t\t\t\t\t\t * @event edit-open Emitted instead of opening the edit modal when\n\t\t\t\t\t\t\t * `editOpensDetail` is set — the host navigates to the record's\n\t\t\t\t\t\t\t * detail page, where the full record (not just its scalars) is\n\t\t\t\t\t\t\t * editable.\n\t\t\t\t\t\t\t * @type {object} The row to open.\n\t\t\t\t\t\t\t */\n\t\t\t\t\t\t\tthis.$emit('edit-open', row)\n\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t}\n\t\t\t\t\t\tthis.editItem = row\n\t\t\t\t\t\tthis.showFormDialogVisible = true\n\t\t\t\t\t},\n\t\t\t\t\tonCopy: (row) => {\n\t\t\t\t\t\tthis.actionTargetItem = row\n\t\t\t\t\t\tthis.showSingleCopyDialog = true\n\t\t\t\t\t},\n\t\t\t\t\tonDelete: (row) => {\n\t\t\t\t\t\tthis.actionTargetItem = row\n\t\t\t\t\t\tthis.showSingleDeleteDialog = true\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * Effective customComponents registry — explicit prop wins over\n\t\t * the injected ancestor registry. Used to:\n\t\t * - Resolve `actions[].handler` registry names (REQ-MAD-3,\n\t\t * manifest-actions-dispatch).\n\t\t * - Resolve the `cardComponent` name for card-grid view (REQ-MCI,\n\t\t * manifest-card-index).\n\t\t *\n\t\t * @return {object}\n\t\t */\n\t\teffectiveCustomComponents() {\n\t\t\treturn this.customComponents ?? this.cnCustomComponents ?? {}\n\t\t},\n\n\t\t/**\n\t\t * The v2 registry a named handler resolves against before the legacy\n\t\t * customComponents map.\n\t\t *\n\t\t * @return {object}\n\t\t */\n\t\teffectiveRegistry() {\n\t\t\treturn this.cnRegistry ?? {}\n\t\t},\n\n\t\t/**\n\t\t * Merged actions: app-provided first, then built-in defaults.\n\t\t *\n\t\t * REQ-MAD-3 / REQ-MAD-4 / REQ-MAD-5 / REQ-MAD-6 / REQ-MAD-7\n\t\t * (manifest-actions-dispatch) — for any action whose `handler`\n\t\t * is a string, resolve it through `resolveHandler()` so\n\t\t * `CnRowActions` sees the same `{ handler: fn }` shape it does\n\t\t * for built-in defaults. Function-typed handlers (the existing\n\t\t * runtime path) pass through untouched.\n\t\t */\n\t\tmergedActions() {\n\t\t\tconst ctx = {\n\t\t\t\trouter: this.$router,\n\t\t\t\trowKey: this.rowKey,\n\t\t\t\tregistry: this.effectiveRegistry,\n\t\t\t\tcustomComponents: this.effectiveCustomComponents,\n\t\t\t}\n\t\t\t// Drop anything that is not an action OBJECT. A bare string — the\n\t\t\t// shorthand `\"edit\"` an author reasonably expects to mean \"show the\n\t\t\t// built-in Edit\" — has no `label` and no `icon`, so it rendered as a\n\t\t\t// full-height NcActionButton with nothing in it: an invisible,\n\t\t\t// clickable, inert row in the overflow menu. Observed on dossiq,\n\t\t\t// where `config.actions: [\"create\", \"edit\", \"delete\", {…}]` put three\n\t\t\t// blank rows above the real ones on four different index pages.\n\t\t\t//\n\t\t\t// Named, not silently swallowed: a dropped action is a capability the\n\t\t\t// author asked for and did not get, and the manifest schema now\n\t\t\t// rejects the shape at authoring time (`config.actions` is governed by\n\t\t\t// the same `action` definition as `pages[].actions`). Built-ins are\n\t\t\t// turned on with the `show*Action` toggles, not by naming them here.\n\t\t\t// A named source may supply its own row actions, on the same\n\t\t\t// precedence as its columns: what the manifest declares wins, and the\n\t\t\t// source is the DEFAULT for a page that declares none. Without this\n\t\t\t// the source's `rowActions` were declared and never read — the third\n\t\t\t// field of the same adapter to be wired late, after `columns` and\n\t\t\t// `addLabel`.\n\t\t\t//\n\t\t\t// Note what this still cannot do: these are MERGED with the built-ins\n\t\t\t// below, so a source can add an action but cannot say \"these and no\n\t\t\t// others\". Suppressing a built-in is the `show*Action` toggles' job.\n\t\t\tconst manifestDeclared = this.actions && this.actions.length > 0\n\t\t\tconst declaredActions = manifestDeclared\n\t\t\t\t? this.actions\n\t\t\t\t: ((this.isNamedSource && this.namedSource && this.namedSource.rowActions) || [])\n\n\t\t\tconst declared = []\n\t\t\tfor (const a of declaredActions) {\n\t\t\t\tif (a && typeof a === 'object' && !Array.isArray(a)) {\n\t\t\t\t\t// A SOURCE action may say `action: 'open'`: open this row the\n\t\t\t\t\t// way a row click does — the source's own navigation\n\t\t\t\t\t// (`openRow`/`detailRoute`), with `edit-open` emitted so a\n\t\t\t\t\t// host that configured a row target (`config.rowRoute`) wins.\n\t\t\t\t\t// Resolved here because a source is a composable with no\n\t\t\t\t\t// router; the flows source declared exactly this action and\n\t\t\t\t\t// it rendered as a dead menu entry, because `action` was a\n\t\t\t\t\t// key nothing read (dispatch reads `handler` and `type`).\n\t\t\t\t\t// Source-declared actions only: the manifest grammar for\n\t\t\t\t\t// navigation is `type`/`handler`, and stays so.\n\t\t\t\t\tif (!manifestDeclared && a.action === 'open' && typeof a.handler !== 'function') {\n\t\t\t\t\t\tdeclared.push({ ...a, handler: (row) => this.openSourceRow(row) })\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\t\t\t\t\tdeclared.push(dispatchAction(a, ctx))\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.warn(`[CnIndexPage] Ignoring action ${JSON.stringify(a)}: actions must be objects with an id and a label. To show a built-in action use the showViewAction / showEditAction / showCopyAction / showDeleteAction props.`)\n\t\t\t}\n\t\t\treturn [...declared, ...this.defaultActions]\n\t\t},\n\n\t\thasRowActions() {\n\t\t\treturn this.$slots['row-actions'] || this.mergedActions.length > 0\n\t\t},\n\n\t\t/**\n\t\t * The quick-filter tabs to render: the `quickFilters` prop when set,\n\t\t * else a named source's own tabs (`useNamedSource` resolved the same\n\t\t * precedence for the LOAD side, so strip and fetch cannot disagree).\n\t\t *\n\t\t * @return {Array<object>|null} The tabs, or null when there are none.\n\t\t */\n\t\teffectiveQuickFilters() {\n\t\t\tif (this.quickFilters && this.quickFilters.length > 0) {\n\t\t\t\treturn this.quickFilters\n\t\t\t}\n\t\t\treturn (this.isNamedSource && this.namedQuickFilters) || null\n\t\t},\n\n\t\t/**\n\t\t * Whether the Add button renders. A named source that declares\n\t\t * `showAdd: false` suppresses it (a task is created by a flow, never\n\t\t * by a person clicking Add); the source cannot force the button ON,\n\t\t * so an explicit `:show-add=\"false\"` prop always still wins.\n\t\t *\n\t\t * @return {boolean} True when the Add button should render.\n\t\t */\n\t\teffectiveShowAdd() {\n\t\t\tif (this.isNamedSource && this.namedSource && this.namedSource.showAdd === false) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\treturn this.showAdd\n\t\t},\n\n\t\t/** Whether all visible items are selected */\n\t\tallSelected() {\n\t\t\tif (this.effectiveObjects.length === 0 || this.internalSelectedIds.length === 0) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\treturn this.effectiveObjects.every((o) => this.internalSelectedIds.includes(o[this.rowKey]))\n\t\t},\n\n\t\t/** Full objects for the selected IDs (used by mass action dialogs) */\n\t\tselectedObjects() {\n\t\t\treturn this.effectiveObjects.filter((o) => this.internalSelectedIds.includes(o[this.rowKey]))\n\t\t},\n\n\t\t/** Column slot names that the parent has provided (for pass-through) */\n\t\tslotColumns() {\n\t\t\treturn Object.keys(this.$slots)\n\t\t\t\t.filter((name) => name.startsWith('column-'))\n\t\t\t\t.map((name) => name.replace('column-', ''))\n\t\t},\n\n\t\t/**\n\t\t * `createDefaults`, token-resolved. `resolveDeepTokens` rather than the\n\t\t * shallow filter-map resolver: a seed record is arbitrary JSON, so a\n\t\t * token can sit inside a nested object or an array of them.\n\t\t *\n\t\t * @return {object|null}\n\t\t */\n\t\tresolvedCreateDefaults() {\n\t\t\tconst seed = this.createDefaults\n\t\t\tif (!seed || typeof seed !== 'object' || Array.isArray(seed)) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\treturn resolveDeepTokens(seed, {})\n\t\t},\n\n\t\t/** Add button label — derived from schema.title if not explicitly set */\n\t\tresolvedAddLabel() {\n\t\t\tif (this.addLabel) {\n\t\t\t\treturn this.cnTranslate(this.addLabel)\n\t\t\t}\n\t\t\t// A named source names its own create action. Without this the button\n\t\t\t// falls back to a schema-derived noun, and a named source has no\n\t\t\t// schema — so the control the migration was supposed to preserve\n\t\t\t// simply is not there.\n\t\t\tif (this.isNamedSource && this.namedSource && this.namedSource.addLabel) {\n\t\t\t\treturn this.namedSource.addLabel\n\t\t\t}\n\t\t\t// Two catalogues, deliberately. The NOUN comes from the consumer's\n\t\t\t// schema, so it resolves against the consumer's catalogue via the\n\t\t\t// injected `cnTranslate`; the SENTENCE around it is library chrome, so\n\t\t\t// it resolves against the library's own — exactly the split\n\t\t\t// CnFormDialog already makes for \"Create {title}\".\n\t\t\t//\n\t\t\t// Getting that wrong is visible rather than subtle: routing the\n\t\t\t// sentence through the host catalogue too produced \"Add Urenboeking\"\n\t\t\t// on a Dutch instance, because no app catalogue carries a key that\n\t\t\t// belongs to the library.\n\t\t\tconst type = this.cnTranslate(this.effectiveSchema?.title || 'Item')\n\t\t\treturn t('nextcloud-vue', 'Add {type}', { type })\n\t\t},\n\n\t\t/**\n\t\t * Effective sidebar configuration. Returns the sidebar config object\n\t\t * when `sidebar.enabled === true`, otherwise an `{ enabled: false }`\n\t\t * stub so the embedded CnIndexSidebar is not mounted.\n\t\t */\n\t\tresolvedSidebar() {\n\t\t\tif (this.sidebar && this.sidebar.enabled !== false) {\n\t\t\t\treturn this.sidebar\n\t\t\t}\n\t\t\treturn { enabled: false }\n\t\t},\n\n\t\t/**\n\t\t * Whether a Search/Columns sidebar is configured for this page (mirrors\n\t\t * the mount gate used by `shouldRenderInlineSidebar` /\n\t\t * `publishHoistedSidebar`). Drives the actions-bar toggle visibility.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\thasSidebar() {\n\t\t\treturn !!this.resolvedSidebar.enabled && this.resolvedSidebar.show !== false\n\t\t},\n\n\t\t/** Search props forwarded to the embedded CnIndexSidebar (defaults applied per CnIndexSidebar). */\n\t\tsidebarSearchProps() {\n\t\t\treturn (this.sidebar && this.sidebar.search) || {}\n\t\t},\n\n\t\t/**\n\t\t * Whether the embedded sidebar should render inline inside the\n\t\t * cn-index-page wrapper. False when CnAppRoot has provided a\n\t\t * real `cnIndexSidebarConfig` holder — in that case CnAppRoot\n\t\t * mounts the sidebar at NcContent level (correct NcAppSidebar\n\t\t * parent). True when no CnAppRoot ancestor exists (legacy\n\t\t * apps), so the embedded sidebar still renders even though\n\t\t * its visual position is sub-optimal.\n\t\t */\n\t\tshouldRenderInlineSidebar() {\n\t\t\tif (!this.resolvedSidebar.enabled || this.resolvedSidebar.show === false) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\t// CnAppRoot ancestor present → hoist takes over.\n\t\t\treturn !this.cnHostsIndexSidebar\n\t\t},\n\n\t\t/**\n\t\t * Snapshot of every prop the hoisted CnIndexSidebar needs.\n\t\t * Reactive — the sidebar in CnAppRoot re-renders whenever\n\t\t * any of these change.\n\t\t */\n\t\thoistedSidebarProps() {\n\t\t\treturn {\n\t\t\t\topen: this.sidebarOpen,\n\t\t\t\tschema: this.effectiveSchema,\n\t\t\t\ttitle: this.title,\n\t\t\t\ticon: this.resolvedIcon,\n\t\t\t\tsearchValue: this.effectiveSearchValue,\n\t\t\t\tvisibleColumns: this.effectiveVisibleColumns,\n\t\t\t\tactiveFilters: this.effectiveActiveFilters,\n\t\t\t\tcolumnGroups: this.resolvedSidebar.columnGroups || [],\n\t\t\t\tfilterFields: this.resolvedSidebar.fields || null,\n\t\t\t\tfacetData: this.effectiveFacetData,\n\t\t\t\tshowMetadata: this.resolvedSidebar.showMetadata !== false,\n\t\t\t\t...this.sidebarSearchProps,\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Resolved card component for card-grid view mode. Returns\n\t\t * `null` when `cardComponent` is empty OR when the name is not\n\t\t * in the registry (the latter also logs `console.warn`).\n\t\t *\n\t\t * `null` makes the template fall through to `CnCardGrid`'s\n\t\t * default `CnObjectCard` rendering — exactly the legacy path.\n\t\t *\n\t\t * @return {object|null}\n\t\t */\n\t\tresolvedCardComponent() {\n\t\t\tif (!this.cardComponent) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst resolved = this.resolveNamedComponent(this.cardComponent)\n\t\t\tif (!resolved) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.warn(`[CnIndexPage] cardComponent \"${this.cardComponent}\" not found in the registry or customComponents. Falling back to CnObjectCard.`)\n\t\t\t\treturn null\n\t\t\t}\n\t\t\treturn resolved\n\t\t},\n\n\t\t/**\n\t\t * Custom list-row component resolved against the customComponents\n\t\t * registry, or `null` when `listComponent` is empty / unknown (default\n\t\t * `CnObjectRow` is used). Mirrors `resolvedCardComponent`.\n\t\t *\n\t\t * @return {?object} The resolved component, or null.\n\t\t */\n\t\tresolvedListComponent() {\n\t\t\tif (!this.listComponent) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst resolved = this.resolveNamedComponent(this.listComponent)\n\t\t\tif (!resolved) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.warn(`[CnIndexPage] listComponent \"${this.listComponent}\" not found in the registry or customComponents. Falling back to CnObjectRow.`)\n\t\t\t\treturn null\n\t\t\t}\n\t\t\treturn resolved\n\t\t},\n\t},\n\n\twatch: {\n\t\t// Remember the whole folder set whenever no folder narrows the query.\n\t\tfolderSidebarLiveFacetValues: {\n\t\t\timmediate: true,\n\t\t\thandler(values) {\n\t\t\t\tif (this.selectedFolderId === null || this.selectedFolderId === undefined) {\n\t\t\t\t\tthis.folderSidebarAllValues = values\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\n\t\tviewMode(val) {\n\t\t\tthis.currentViewMode = val\n\t\t},\n\n\t\tselectedIds(val) {\n\t\t\tthis.internalSelectedIds = [...val]\n\t\t},\n\n\t\t/**\n\t\t * Tenant change pipeline (multi-tenancy-context).\n\t\t * When the bound `activeOrganisation` prop changes, push the\n\t\t * new UUID into the bound object store (when there is one)\n\t\t * AND into the shared `useTenantContext()` context so every\n\t\t * other tenant-aware surface stays consistent.\n\t\t */\n\t\tactiveOrganisation: {\n\t\t\thandler(next) {\n\t\t\t\tif (!next) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tconst uuid = next.uuid || null\n\t\t\t\t// Update the object store when one is bound — sub-store\n\t\t\t\t// methods may not exist on non-OR stores; guard with typeof.\n\t\t\t\tconst store = this.list?.store ?? this.selfObjectStore ?? null\n\t\t\t\tif (store && typeof store.setActiveTenantOrganisation === 'function') {\n\t\t\t\t\tstore.setActiveTenantOrganisation(uuid)\n\t\t\t\t}\n\t\t\t},\n\n\t\t\tdeep: false,\n\t\t},\n\n\t\t/**\n\t\t * Keep the hoisted sidebar in sync with reactive props.\n\t\t * The watcher fires whenever any of the props snapshot\n\t\t * (`hoistedSidebarProps`) changes; we re-write the entire\n\t\t * config so Vue's NcAppSidebar in CnAppRoot picks up the\n\t\t * new values. Cheap because it's just an object swap.\n\t\t */\n\t\thoistedSidebarProps: {\n\t\t\thandler() {\n\t\t\t\tthis.publishHoistedSidebar()\n\t\t\t},\n\n\t\t\tdeep: false,\n\t\t},\n\n\t\tshouldRenderInlineSidebar() {\n\t\t\t// When the gate flips (e.g. sidebar.show toggled), keep\n\t\t\t// the hoist in sync.\n\t\t\tthis.publishHoistedSidebar()\n\t\t},\n\n\t\t// The gate itself, which neither watcher above catches: under a\n\t\t// CnAppRoot host `shouldRenderInlineSidebar` is false in BOTH states,\n\t\t// and `hoistedSidebarProps` carries none of `enabled` / `show`. So\n\t\t// toggling the sidebar on or off in CnEditSidebarModal — which mutates\n\t\t// `config.sidebar` in place — left the hoisted panel exactly as it was\n\t\t// until the page was reloaded.\n\t\thasSidebar() {\n\t\t\tthis.publishHoistedSidebar()\n\t\t},\n\n\t\t// Re-push AI context when relevant props change\n\t\tregister() {\n\t\t\tthis.pushAiContext()\n\t\t},\n\n\t\tschema() {\n\t\t\tthis.pushAiContext()\n\t\t},\n\n\t\t// In self-fetch mode, a same-component route-param change (e.g. the\n\t\t// `:id` of `/forms/:id/submissions`) must re-resolve `config.filter`\n\t\t// and re-fetch. useListView's `fixedFilters` getter re-reads $route on\n\t\t// each fetch; this watcher triggers the re-fetch.\n\t\t'$route.params': {\n\t\t\tdeep: true,\n\t\t\thandler() {\n\t\t\t\tif (this.isSelfFetchMode && typeof this.list.refresh === 'function') {\n\t\t\t\t\tthis.list.refresh(1)\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\n\t\t// A same-path `$route.query` change (e.g. a dashboard deep-link\n\t\t// `/cases?caseType=X`) must also re-fetch — `fixedFilters` merges the\n\t\t// query into the fetch (see useSelfFetchList.resolveQueryFilters).\n\t\t'$route.query': {\n\t\t\tdeep: true,\n\t\t\thandler() {\n\t\t\t\tif (this.isSelfFetchMode && typeof this.list.refresh === 'function') {\n\t\t\t\t\tthis.list.refresh(1)\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\n\t\t/**\n\t\t * Leaving the narrow full-detail layout puts the list back where it\n\t\t * was. The element was hidden, so the browser kept no scroll height\n\t\t * for it and would otherwise return the handler to row 1 of 400.\n\t\t *\n\t\t * @param {string} next The layout now.\n\t\t * @param {string} previous The layout before it.\n\t\t * @spec openspec/changes/case-page-and-list-as-a-place/specs/index-page/spec.md\n\t\t */\n\t\tsplitLayout(next, previous) {\n\t\t\tif (previous === 'detail' && next !== 'detail') {\n\t\t\t\tthis.restoreListScroll()\n\t\t\t}\n\t\t},\n\n\t\t// When `?action=create` is injected into the query via router navigation\n\t\t// (e.g. a \"New Case\" button on another page), auto-open the create dialog\n\t\t// so the user lands on the index page with the form already open.\n\t\t'$route.query.action': {\n\t\t\thandler(val) {\n\t\t\t\tif (val === 'create') {\n\t\t\t\t\tthis.maybeOpenCreateFromQuery()\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t},\n\n\t/**\n\t * Measure the viewport, follow a resize, and read this person's own\n\t * row order for this list.\n\t *\n\t * @spec openspec/changes/case-page-and-list-as-a-place/specs/index-page/spec.md\n\t */\n\tmounted() {\n\t\tthis.measureSplitViewport()\n\t\tif (this.splitViewEnabled && typeof window !== 'undefined') {\n\t\t\twindow.addEventListener('resize', this.measureSplitViewport)\n\t\t}\n\t\tthis.loadManualOrder()\n\t\tthis.publishHoistedSidebar()\n\t\tthis.pushAiContext()\n\t\tthis.maybeOpenCreateFromQuery()\n\t\tif (this.folderSidebar) {\n\t\t\tthis.loadFolderRegister()\n\t\t\t// Reflect a deep-link filter (e.g. ?caseType=<id>) as the active folder.\n\t\t\tconst key = this.folderSidebar.filterField || this.folderSidebar.field\n\t\t\tconst active = key && this.effectiveActiveFilters[key]\n\t\t\tif (active) {\n\t\t\t\tthis.selectedFolderId = Array.isArray(active) ? active[0] : active\n\t\t\t}\n\t\t}\n\t},\n\n\tcreated() {\n\t\tthis.pushAiContext()\n\t\tif (this.allowSavedViews) {\n\t\t\tthis.fetchSavedViews()\n\t\t}\n\t\tthis.selfActions = createSelfModeActions({\n\t\t\tisSelfFetchMode: () => this.isSelfFetchMode,\n\t\t\tselfObjectStore: () => this.selfObjectStore,\n\t\t\tselfObjectType: () => this.selfObjectType,\n\t\t\tlist: () => this.list,\n\t\t\tregister: () => this.register,\n\t\t\tschema: () => this.schema,\n\t\t\teffectiveObjects: () => this.effectiveObjects,\n\t\t\teffectiveSchema: () => this.effectiveSchema,\n\t\t\tmassActionNameField: () => this.massActionNameField,\n\t\t\teditItem: () => this.editItem,\n\t\t\temit: (event, payload) => this.$emit(event, payload),\n\t\t\tafterCreateSuccess: (saved) => this.afterCreateSuccess(saved),\n\t\t\tsetResults: {\n\t\t\t\tsingleDelete: (r) => this.setSingleDeleteResult(r),\n\t\t\t\tmassDelete: (r) => this.setMassDeleteResult(r),\n\t\t\t\tsingleCopy: (r) => this.setSingleCopyResult(r),\n\t\t\t\tmassCopy: (r) => this.setMassCopyResult(r),\n\t\t\t\tmassImport: (r) => this.setImportResult(r),\n\t\t\t\tmassExport: (r) => this.setExportResult(r),\n\t\t\t\tform: (r) => this.setFormResult(r),\n\t\t\t\tformValidation: (fieldErrors, message) => this.setFormValidationErrors(fieldErrors, message),\n\t\t\t},\n\t\t})\n\t},\n\n\t/**\n\t * Drop the resize listener and the hoisted sidebar.\n\t *\n\t * @spec openspec/changes/case-page-and-list-as-a-place/specs/index-page/spec.md\n\t */\n\tbeforeUnmount() {\n\t\tif (typeof window !== 'undefined') {\n\t\t\twindow.removeEventListener('resize', this.measureSplitViewport)\n\t\t}\n\t\t// Clear the holder so the hoisted sidebar disappears when\n\t\t// the user navigates away from the index page.\n\t\tif (this.cnHostsIndexSidebar && this.cnIndexSidebarConfig) {\n\t\t\tthis.cnIndexSidebarConfig.value = null\n\t\t}\n\t\tapplyAiContext(this.cnAiContext, 'custom')\n\t},\n\n\tmethods: {\n\t\t// ── Split view (case-page-and-list-as-a-place) ──────────────────\n\n\t\t/**\n\t\t * Measure the viewport so the narrow fallback follows a resize and\n\t\t * not only a reload. Measured rather than read off a media query\n\t\t * because the breakpoint is the page's own declaration, not a global.\n\t\t *\n\t\t * @spec openspec/changes/case-page-and-list-as-a-place/specs/index-page/spec.md\n\t\t */\n\t\tmeasureSplitViewport() {\n\t\t\tif (typeof window !== 'undefined' && Number.isFinite(window.innerWidth)) {\n\t\t\t\tthis.splitViewportWidth = window.innerWidth\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Remember where the list is scrolled to.\n\t\t *\n\t\t * The list never unmounts, so the browser keeps the scroll on its\n\t\t * own in the split layout. It does NOT keep it across the narrow\n\t\t * layout, where the element is hidden and has no scroll height at\n\t\t * all, so the position is held here and put back on the way out.\n\t\t *\n\t\t * @param {Event} [event] The scroll event.\n\t\t * @spec openspec/changes/case-page-and-list-as-a-place/specs/index-page/spec.md\n\t\t */\n\t\tonListScroll(event) {\n\t\t\tconst top = event?.target?.scrollTop\n\t\t\tif (Number.isFinite(top)) {\n\t\t\t\tthis.splitScrollTop = top\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Put the list back where it was.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t * @spec openspec/changes/case-page-and-list-as-a-place/specs/index-page/spec.md\n\t\t */\n\t\tasync restoreListScroll() {\n\t\t\tawait this.$nextTick()\n\t\t\tconst el = this.$refs.listScroll\n\t\t\tif (el && Number.isFinite(this.splitScrollTop) && this.splitScrollTop > 0) {\n\t\t\t\tel.scrollTop = this.splitScrollTop\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Close the pane and go back to the list's own address.\n\t\t *\n\t\t * The list is already mounted and already at row 180, so this is a\n\t\t * route change and nothing else: no refetch, no remount, no reset.\n\t\t *\n\t\t * @spec openspec/changes/case-page-and-list-as-a-place/specs/index-page/spec.md\n\t\t */\n\t\tcloseSplitPane() {\n\t\t\t/**\n\t\t\t * @event split-close Emitted when the split pane is closed. The host returns to the list address.\n\t\t\t */\n\t\t\tthis.$emit('split-close')\n\t\t\tconst router = this.$router\n\t\t\tconst target = this.splitCloseRoute || this.$route?.meta?.cnPageId || null\n\t\t\tif (router && target) {\n\t\t\t\trouter.push({ name: target, query: this.$route?.query || {} }).catch(() => {})\n\t\t\t}\n\t\t\tthis.restoreListScroll()\n\t\t},\n\n\t\t/**\n\t\t * A record was saved in the pane: replace its row where it sits.\n\t\t *\n\t\t * Deliberately not a refetch. Refetching the page would be correct\n\t\t * and would also scroll the handler back to the top of four hundred\n\t\t * cases, which is the failure the split view was built to end.\n\t\t *\n\t\t * @param {object} saved The saved record.\n\t\t * @spec openspec/changes/case-page-and-list-as-a-place/specs/index-page/spec.md\n\t\t */\n\t\tonSplitPaneSaved(saved) {\n\t\t\tconst id = rowIdOf(saved, this.rowKey) ?? (this.splitId || null)\n\t\t\tif (id === null || !saved || typeof saved !== 'object') {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.splitRowPatches = { ...this.splitRowPatches, [id]: saved }\n\t\t\t/**\n\t\t\t * @event split-saved Emitted after a record saved in the pane was written onto its row in the list.\n\t\t\t * @type {object}\n\t\t\t */\n\t\t\tthis.$emit('split-saved', saved)\n\t\t},\n\n\t\t// ── Manual order (case-page-and-list-as-a-place) ────────────────\n\n\t\t/**\n\t\t * The key this list's manual order is held under.\n\t\t *\n\t\t * @return {string} The preference key.\n\t\t * @spec openspec/changes/case-page-and-list-as-a-place/specs/index-page/spec.md\n\t\t */\n\t\tmanualOrderPreferenceKey() {\n\t\t\treturn manualOrderKey(this.manualOrderId || this.objectType || this.schema || 'default')\n\t\t},\n\n\t\t/**\n\t\t * Read this person's order for this list out of their preferences.\n\t\t *\n\t\t * A read that fails leaves the list in its loaded order rather than\n\t\t * blocking it: an order nobody can read is a nicety, and a list that\n\t\t * refuses to render because of one is not.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t * @spec openspec/changes/case-page-and-list-as-a-place/specs/index-page/spec.md\n\t\t */\n\t\tasync loadManualOrder() {\n\t\t\tif (!this.manualOrder) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst read = this.cnUserPreferences?.read\n\t\t\tif (typeof read !== 'function') {\n\t\t\t\treturn\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tconst stored = await read(this.manualOrderPreferenceKey(), [])\n\t\t\t\tthis.manualOrderIds = Array.isArray(stored) ? stored.map(String) : []\n\t\t\t} catch {\n\t\t\t\tthis.manualOrderIds = []\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Write this person's order back, and tell the host.\n\t\t *\n\t\t * @param {Array<string>} ids The new order.\n\t\t * @return {Promise<void>}\n\t\t * @spec openspec/changes/case-page-and-list-as-a-place/specs/index-page/spec.md\n\t\t */\n\t\tasync persistManualOrder(ids) {\n\t\t\tthis.manualOrderIds = ids\n\t\t\t/**\n\t\t\t * @event manual-order-change Emitted when this person reorders the list by hand. Payload is the row ids in their order.\n\t\t\t * @type {Array<string>}\n\t\t\t */\n\t\t\tthis.$emit('manual-order-change', ids)\n\t\t\tconst write = this.cnUserPreferences?.write\n\t\t\tif (typeof write === 'function') {\n\t\t\t\ttry {\n\t\t\t\t\tawait write(this.manualOrderPreferenceKey(), ids)\n\t\t\t\t} catch {\n\t\t\t\t\t// A preference that could not be written is a preference\n\t\t\t\t\t// that will not survive the reload. The list is still in\n\t\t\t\t\t// the order the person just chose, so nothing is said.\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Move a row one place up or down. The keyboard half of the drag, and\n\t\t * the one the drag itself calls, so the two cannot disagree.\n\t\t *\n\t\t * @param {string} id The row being moved.\n\t\t * @param {number} delta -1 for up, 1 for down.\n\t\t * @return {Promise<void>}\n\t\t * @spec openspec/changes/case-page-and-list-as-a-place/specs/index-page/spec.md\n\t\t */\n\t\tmoveRowByHand(id, delta) {\n\t\t\treturn this.persistManualOrder(moveInOrder(visibleIdsOf(this.displayObjects, this.rowKey), id, delta))\n\t\t},\n\n\t\t/**\n\t\t * Place a dragged row at an index.\n\t\t *\n\t\t * @param {string} id The row being dragged.\n\t\t * @param {number} toIndex The index it was dropped at.\n\t\t * @return {Promise<void>}\n\t\t * @spec openspec/changes/case-page-and-list-as-a-place/specs/index-page/spec.md\n\t\t */\n\t\tdropRowByHand(id, toIndex) {\n\t\t\treturn this.persistManualOrder(dropInOrder(visibleIdsOf(this.displayObjects, this.rowKey), id, toIndex))\n\t\t},\n\n\t\t/**\n\t\t * Resolve a component the page config names — `cardComponent`,\n\t\t * `listComponent` — out of the v2 registry (any kind carrying a\n\t\t * `component`, as a slot lookup does) and then the legacy\n\t\t * customComponents map.\n\t\t *\n\t\t * @param {string} name The registered component name.\n\t\t * @return {object|null} The component, or null when nothing answers.\n\t\t */\n\t\tresolveNamedComponent(name) {\n\t\t\tconst entry = this.effectiveRegistry[name]\n\t\t\tif (entry && entry.component) {\n\t\t\t\treturn entry.component\n\t\t\t}\n\t\t\treturn this.effectiveCustomComponents[name] || null\n\t\t},\n\n\t\t/**\n\t\t * Whether a handler name matches something registered that is not\n\t\t * callable — a named component where a function belongs. Tells a\n\t\t * typo (silent emit-only) apart from a mis-registration (warned).\n\t\t *\n\t\t * @param {string} name The handler name from the manifest.\n\t\t * @return {boolean}\n\t\t */\n\t\tnamesSomethingUnusable(name) {\n\t\t\tconst candidates = [this.effectiveRegistry[name], this.resolvedCustomComponents[name]]\n\t\t\treturn candidates.some((v) => v !== undefined && v !== null)\n\t\t},\n\n\t\t/**\n\t\t * Resolve a declarative `headerActions[]` entry's `handler`\n\t\t * field into the final dispatchable shape. Mirrors the\n\t\t * row-level `actions[].handler` keyword set used by\n\t\t * manifest-actions-dispatch — `navigate`, `emit`, `none`, or a\n\t\t * registry name (resolved through `resolveRegisteredHandler`).\n\t\t *\n\t\t * @param {object} entry Raw headerActions entry.\n\t\t * @return {object} Possibly-mutated copy: function-typed\n\t\t * handlers untouched; `'emit'` and unknown registry names\n\t\t * strip the `handler` (emit-only fall-through); `'navigate'`\n\t\t * becomes a `$router.push` thunk (with the entry's literal\n\t\t * `params` map when present); `'none'` becomes a no-op +\n\t\t * `_dispatchSuppress: true`; a registry name resolves to a\n\t\t * `fn({ actionId })` thunk.\n\t\t */\n\t\tresolveHeaderHandler(entry) {\n\t\t\tif (!entry) {\n\t\t\t\treturn entry\n\t\t\t}\n\t\t\tconst handler = entry.handler\n\t\t\tif (typeof handler === 'function') {\n\t\t\t\treturn { ...entry }\n\t\t\t}\n\t\t\tif (typeof handler !== 'string') {\n\t\t\t\t// No handler declared — emit-only fall-through.\n\t\t\t\treturn { ...entry }\n\t\t\t}\n\t\t\tif (handler === 'navigate') {\n\t\t\t\tif (!entry.route) {\n\t\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\t\tconsole.warn(`CnIndexPage: headerActions[].id \"${entry.id}\" handler:\"navigate\" requires \"route\"; falling back to emit-only`)\n\t\t\t\t\tconst { handler: _ignored, ...rest } = entry\n\t\t\t\t\treturn { ...rest }\n\t\t\t\t}\n\t\t\t\tconst route = entry.route\n\t\t\t\tconst router = this.$router\n\t\t\t\t// Literal params let a header action navigate to a detail route\n\t\t\t\t// with fixed params, e.g. a \"New X\" button → `{ id: \"new\" }`.\n\t\t\t\tconst params = (entry.params && typeof entry.params === 'object') ? entry.params : null\n\t\t\t\tconst location = params ? { name: route, params } : { name: route }\n\t\t\t\t// A resolvable route renders as a real link in the bar, which does\n\t\t\t\t// the navigating; the entry then carries no handler to push again.\n\t\t\t\tif (routeHref(location, router)) {\n\t\t\t\t\tconst { handler: _ignored, ...rest } = entry\n\t\t\t\t\treturn { ...rest, to: location }\n\t\t\t\t}\n\t\t\t\tconst out = { ...entry }\n\t\t\t\tout.handler = () => {\n\t\t\t\t\tif (router && typeof router.push === 'function') {\n\t\t\t\t\t\trouter.push(location)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn out\n\t\t\t}\n\t\t\tif (handler === 'emit') {\n\t\t\t\tconst { handler: _ignored, ...rest } = entry\n\t\t\t\treturn { ...rest }\n\t\t\t}\n\t\t\tif (handler === 'none') {\n\t\t\t\treturn { ...entry, handler: () => {}, _dispatchSuppress: true }\n\t\t\t}\n\t\t\t// Registry name lookup.\n\t\t\tconst resolved = resolveRegisteredHandler(handler, this.effectiveRegistry, this.resolvedCustomComponents)\n\t\t\tif (typeof resolved === 'function') {\n\t\t\t\tconst id = entry.id\n\t\t\t\treturn { ...entry, handler: () => resolved({ actionId: id }) }\n\t\t\t}\n\t\t\tif (this.namesSomethingUnusable(handler)) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.warn(`CnIndexPage: headerActions[].handler \"${handler}\" resolved to a non-function in the registry or customComponents; falling back to emit-only`)\n\t\t\t\tconst { handler: _ignored, ...rest } = entry\n\t\t\t\treturn { ...rest }\n\t\t\t}\n\t\t\t// Unknown registry name — silent emit-only fall-through.\n\t\t\tconst { handler: _ignored, ...rest } = entry\n\t\t\treturn { ...rest }\n\t\t},\n\n\t\t/**\n\t\t * Click dispatch from CnActionsBar's `@bulk-action`.\n\t\t *\n\t\t * 🔴 THE SELECTION IS PASSED, NEVER LOOKED UP. Everything a bulk\n\t\t * action does is defined by which rows were selected, and the strip\n\t\t * that raised the event is the thing that knows. A handler that reads\n\t\t * the selection from somewhere else is one re-render away from acting\n\t\t * on a different set than the user saw highlighted.\n\t\t *\n\t\t * Resolution mirrors `headerActions[]`:\n\t\t * - a function handler is called with `{ actionId, selectedIds, count }`\n\t\t * - `open-modal` (with `target`) opens the registered modal, with the\n\t\t * selection merged into its props\n\t\t * - a registry name resolves through the v2 registry, then the\n\t\t * legacy `customComponents`\n\t\t * - anything else falls through to emit-only\n\t\t *\n\t\t * `bulk-action` is emitted either way, so a host can listen instead of\n\t\t * declaring a handler.\n\t\t *\n\t\t * @param {{id: string, action: string, selectedIds: Array, count: number}} payload Bar payload.\n\t\t */\n\t\tonBulkAction(payload) {\n\t\t\tconst id = payload && (payload.id ?? payload.action)\n\t\t\tconst selectedIds = (payload && Array.isArray(payload.selectedIds)) ? payload.selectedIds : []\n\t\t\tconst count = selectedIds.length\n\t\t\tconst entry = this.mergedBulkActions.find((e) => e.id === id)\n\n\t\t\tif (entry) {\n\t\t\t\tconst handler = entry.handler\n\t\t\t\tconst scope = { actionId: id, selectedIds, count }\n\n\t\t\t\tif (typeof handler === 'function') {\n\t\t\t\t\thandler(scope)\n\t\t\t\t} else if (handler === 'open-modal' || (!handler && entry.target)) {\n\t\t\t\t\tthis.openBulkModal(entry, selectedIds, count)\n\t\t\t\t} else if (typeof handler === 'string' && handler !== 'emit' && handler !== 'none') {\n\t\t\t\t\tconst resolved = resolveRegisteredHandler(handler, this.effectiveRegistry, this.resolvedCustomComponents)\n\t\t\t\t\tif (typeof resolved === 'function') {\n\t\t\t\t\t\tresolved(scope)\n\t\t\t\t\t} else if (this.namesSomethingUnusable(handler)) {\n\t\t\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\t\t\tconsole.warn(`CnIndexPage: bulkActions[].handler \"${handler}\" resolved to a non-function in the registry or customComponents; falling back to emit-only`)\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tif (entry._dispatchSuppress || entry.handler === 'none') {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t/**\n\t\t\t * @event bulk-action A declarative bulk action was triggered from the selection strip. Payload: `{ action, id, selectedIds, count }`. The SELECTION travels with it, because an action that has to go and find out what was selected is one re-render away from acting on a different set than the user saw highlighted.\n\t\t\t */\n\t\t\tthis.$emit('bulk-action', { action: id, id, selectedIds, count })\n\t\t},\n\n\t\t/**\n\t\t * Open a bulk action's modal with the selection in its props.\n\t\t *\n\t\t * The selection is merged UNDER the entry's own props, so a manifest\n\t\t * that names a `selectedIds` prop of its own still wins — but it also\n\t\t * means such a manifest silently stops receiving the live selection,\n\t\t * which is why the collision is warned about rather than allowed\n\t\t * quietly.\n\t\t *\n\t\t * @param {object} entry The bulk action entry.\n\t\t * @param {Array} selectedIds The current selection.\n\t\t * @param {number} count Its length.\n\t\t */\n\t\topenBulkModal(entry, selectedIds, count) {\n\t\t\tconst target = entry.target\n\t\t\tif (!target) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.warn(`CnIndexPage: bulkActions[].id \"${entry.id}\" handler:\"open-modal\" requires \"target\"; falling back to emit-only`)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst own = (entry.props && typeof entry.props === 'object') ? entry.props : {}\n\t\t\tif (Object.hasOwn(own, 'selectedIds')) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.warn(`CnIndexPage: bulkActions[].id \"${entry.id}\" declares its own \"selectedIds\" prop, which shadows the live selection; the modal will not see what the user selected.`)\n\t\t\t}\n\t\t\t/**\n\t\t\t * @event open-modal A bulk action of type `open-modal` asks the host to open a registered modal. Payload: `{ target, props }`, where `props` carries `selectedIds` and `count` merged UNDER the action's own props.\n\t\t\t */\n\t\t\tthis.$emit('open-modal', { target, props: { selectedIds, count, ...own } })\n\t\t},\n\n\t\t/**\n\t\t * Click dispatch from CnActionsBar's `@header-action`. Looks\n\t\t * up the resolved entry by id, invokes its handler if any,\n\t\t * and (unless the entry is `'none'`-suppressed) emits\n\t\t * `@header-action({ action: id, id })` upward.\n\t\t *\n\t\t * @param {{action: string, id: string}} payload Bar payload.\n\t\t */\n\t\tonHeaderAction(payload) {\n\t\t\tconst id = payload && (payload.id ?? payload.action)\n\t\t\tconst entry = this.mergedHeaderActions.find((e) => e.id === id)\n\t\t\tif (entry && typeof entry.handler === 'function') {\n\t\t\t\tentry.handler()\n\t\t\t}\n\t\t\tif (entry && entry._dispatchSuppress) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.$emit('header-action', { action: id, id })\n\t\t},\n\n\t\tpushAiContext() {\n\t\t\tapplyAiContext(this.cnAiContext, 'index', {\n\t\t\t\tregister: this.register,\n\t\t\t\tschema: this.schema,\n\t\t\t\teffectiveSchema: this.effectiveSchema,\n\t\t\t})\n\t\t},\n\n\t\t// ── List-event handlers ──────────────────────────────────────────────\n\t\t// In self-fetch mode each routes to the useListView instance (re-fetch);\n\t\t// in every mode the event still bubbles via $emit so a host that wants\n\t\t// to observe (or take over) keeps working — unchanged for consumers.\n\t\t/**\n\t\t * @param {string} value Search term from the sidebar / header.\n\t\t * @return {void}\n\t\t */\n\t\tonSearchEvent(value) {\n\t\t\tif (this.isSelfFetchMode && typeof this.list.onSearch === 'function') {\n\t\t\t\tthis.list.onSearch(value)\n\t\t\t\tthis.persistViewStateToRoute(this.currentViewState())\n\t\t\t}\n\t\t\tthis.$emit('search', value)\n\t\t},\n\n\t\t/**\n\t\t * Quick-filter tab change. Updates the active index — the `setup()`\n\t\t * watcher then triggers `list.refresh(1)` so the new tab's filter\n\t\t * flows into the next fetch.\n\t\t *\n\t\t * @param {number} index Zero-based tab index (from CnQuickFilterBar).\n\t\t * @return {void}\n\t\t */\n\t\tonQuickFilterChange(index) {\n\t\t\t// `activeQuickFilterIndex` is a setup-returned ref; the\n\t\t\t// Vue 2 ref-unwrap proxy makes plain assignment work.\n\t\t\tthis.activeQuickFilterIndex = index\n\t\t\tthis.$emit('quick-filter-change', index)\n\t\t},\n\n\t\t/**\n\t\t * Multi-select quick-filter change (`quickFilterMultiple`). Updates the\n\t\t * selected-index array; the `setup()` watcher re-fetches with the\n\t\t * OR-ed union of the selected tabs' filters.\n\t\t *\n\t\t * @param {number[]} indices Selected tab indices (from CnQuickFilterBar).\n\t\t * @return {void}\n\t\t */\n\t\tonQuickFilterMultiChange(indices) {\n\t\t\tthis.selectedQuickFilterIndices = Array.isArray(indices) ? indices : []\n\t\t\tthis.$emit('quick-filter-change', this.selectedQuickFilterIndices)\n\t\t},\n\n\t\t/**\n\t\t * @param {{key: string, order: string, keys?: Array<{key: string, order: string}>}} payload Sort change from CnDataTable.\n\t\t * @return {void}\n\t\t */\n\t\tonSortEvent(payload) {\n\t\t\tif (this.isSelfFetchMode && typeof this.list.onSort === 'function') {\n\t\t\t\tthis.list.onSort(payload)\n\t\t\t\tthis.persistViewStateToRoute(this.currentViewState())\n\t\t\t}\n\t\t\tthis.$emit('sort', payload)\n\t\t},\n\n\t\t/**\n\t\t * Self-fetch mode's current filters/search/sort, straight from\n\t\t * useListView's own reactive state — the shape `persistViewStateToRoute`\n\t\t * and the saved-views helpers (`buildViewCreatePayload`,\n\t\t * `extractViewState`) all share.\n\t\t *\n\t\t * @return {{filters: object, search: string, sortKey: ?string, sortOrder: string, sortKeys: Array<{key: string, order: string}>}}\n\t\t */\n\t\tcurrentViewState() {\n\t\t\treturn {\n\t\t\t\tfilters: this.list.activeFilters.value,\n\t\t\t\tsearch: this.list.searchTerm.value,\n\t\t\t\tsortKeys: this.list.sortKeys?.value || [],\n\t\t\t\tsortKey: this.list.sortKey.value,\n\t\t\t\tsortOrder: this.list.sortOrder.value,\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * The spelling a filter goes back into the query as. A value that arrived\n\t\t * as an `@`-token keeps the TOKEN for as long as it still resolves to\n\t\t * what is active — otherwise a shared `?assignee=@me` link would be\n\t\t * rewritten to one named uid on the first filter change and stop meaning\n\t\t * \"me\" for whoever opens it next.\n\t\t *\n\t\t * @param {string} key The filter key, as it sits in the query.\n\t\t * @param {unknown} value The resolved value now active.\n\t\t * @return {unknown} The token, or the value.\n\t\t */\n\t\tfilterQuerySpelling(key, value) {\n\t\t\tconst raw = this.$route.query[key]\n\t\t\tif (typeof raw !== 'string' || raw.charAt(0) !== '@') {\n\t\t\t\treturn value\n\t\t\t}\n\t\t\tconst ctx = typeof this.selfFetchTokenCtx === 'function' ? this.selfFetchTokenCtx() : {}\n\t\t\treturn String(resolveFilterValue(raw, ctx)) === String(value) ? raw : value\n\t\t},\n\n\t\t/**\n\t\t * Persist filters + search + sort into `$route.query` in one replace,\n\t\t * so a reload or a shared/bookmarked link reproduces the exact same\n\t\t * view. Self-fetch mode only. The page's own filter keys are cleared and\n\t\t * re-applied wholesale each call rather than merged (otherwise a cleared\n\t\t * filter would never leave the query); a non-reserved key it never\n\t\t * claimed is left alone. Best-effort: a duplicate-navigation rejection\n\t\t * (same resulting path/query) is swallowed.\n\t\t *\n\t\t * @param {{filters?: object, search?: string, sortKey?: ?string, sortOrder?: string, sortKeys?: Array<{key: string, order: string}>}} state Current view state.\n\t\t * @return {void}\n\t\t */\n\t\tpersistViewStateToRoute(state) {\n\t\t\tif (!this.$router || !this.$route) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst query = { ...this.$route.query }\n\t\t\tfor (const key of this.persistedFilterKeys) {\n\t\t\t\tdelete query[key]\n\t\t\t}\n\t\t\tconst written = []\n\t\t\tfor (const [key, value] of Object.entries(state.filters || {})) {\n\t\t\t\tif (value === undefined || value === null || value === '' || (Array.isArray(value) && value.length === 0)) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tquery[key] = this.filterQuerySpelling(key, value)\n\t\t\t\twritten.push(key)\n\t\t\t}\n\t\t\tthis.persistedFilterKeys = written\n\t\t\tif (state.search) {\n\t\t\t\tquery._search = state.search\n\t\t\t} else {\n\t\t\t\tdelete query._search\n\t\t\t}\n\t\t\tconst sortKeys = Array.isArray(state.sortKeys) && state.sortKeys.length\n\t\t\t\t? state.sortKeys\n\t\t\t\t: (state.sortKey ? [{ key: state.sortKey, order: state.sortOrder || 'asc' }] : [])\n\t\t\tif (sortKeys.length) {\n\t\t\t\tquery._order = JSON.stringify(sortKeys.map((k) => ({ key: k.key, order: k.order || 'asc' })))\n\t\t\t} else {\n\t\t\t\tdelete query._order\n\t\t\t}\n\t\t\tthis.$router.replace({ query }).catch(() => {})\n\t\t},\n\n\t\t/**\n\t\t * \"Clear all\" from the sidebar: reset search, every active filter, the\n\t\t * sort and the folder-sidebar selection in one fetch + one route\n\t\t * replace, rather than one of each per field. Self-fetch mode only; a\n\t\t * consumer-managed page gets the bare event to handle itself.\n\t\t *\n\t\t * The SORT goes too, and that is the whole point: applying a saved view\n\t\t * sets a sort the reader never chose and cannot see the origin of, so a\n\t\t * clear that left it behind left the list in a state with no control\n\t\t * anywhere on the page to undo it.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tonClearFilters() {\n\t\t\tif (!this.isSelfFetchMode) {\n\t\t\t\tthis.$emit('clear-filters')\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.list.activeFilters.value = {}\n\t\t\tthis.list.searchTerm.value = ''\n\t\t\tthis.list.sortKeys.value = []\n\t\t\tthis.list.sortKey.value = null\n\t\t\tthis.list.sortOrder.value = 'asc'\n\t\t\tif (this.folderSidebar) {\n\t\t\t\tthis.selectedFolderId = null\n\t\t\t}\n\t\t\tthis.list.refresh(1)\n\t\t\tthis.persistViewStateToRoute(this.currentViewState())\n\t\t\tthis.$emit('clear-filters')\n\t\t},\n\n\t\t/**\n\t\t * @param {number} page Requested page from CnPagination.\n\t\t * @return {void}\n\t\t */\n\t\tonPageEvent(page) {\n\t\t\tif (this.isSelfFetchMode && typeof this.list.onPageChange === 'function') {\n\t\t\t\tthis.list.onPageChange(page)\n\t\t\t}\n\t\t\tthis.$emit('page-changed', page)\n\t\t},\n\n\t\t/**\n\t\t * @param {{key: string, values: Array}} payload Facet-filter change from the sidebar.\n\t\t * @return {void}\n\t\t */\n\t\tonFilterEvent(payload) {\n\t\t\tif (this.isSelfFetchMode && typeof this.list.onFilterChange === 'function') {\n\t\t\t\tthis.list.onFilterChange(payload.key, payload.values)\n\t\t\t\tthis.persistViewStateToRoute(this.currentViewState())\n\t\t\t}\n\t\t\t// A named source keeps its own filter state: nothing else holds it,\n\t\t\t// and the `activeFilters` PROP only reaches pages with a consumer\n\t\t\t// component behind them. A manifest page has none, which is why\n\t\t\t// its sidebar used to render controls that narrowed nothing.\n\t\t\tif (this.isNamedSource) {\n\t\t\t\tthis.setNamedFilter(payload.key, payload.values)\n\t\t\t}\n\t\t\tthis.$emit('filter-change', payload)\n\t\t},\n\n\t\t/**\n\t\t * Record one sidebar choice on a named-source page and mirror it into\n\t\t * the URL.\n\t\t *\n\t\t * The ref is REPLACED rather than mutated in place so the watcher in\n\t\t * `useNamedSource` fires on the first change too, and the reload it\n\t\t * triggers is the only place the new query is built.\n\t\t *\n\t\t * @param {string} key The sidebar field key.\n\t\t * @param {Array|object|null} values The chosen values, or a `{ from, to }` range.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tsetNamedFilter(key, values) {\n\t\t\tconst next = { ...(this.namedActiveFilters || {}) }\n\t\t\tconst asQuery = namedFilterToQuery(values)\n\t\t\tif (asQuery === '') {\n\t\t\t\tdelete next[key]\n\t\t\t} else {\n\t\t\t\tnext[key] = values\n\t\t\t}\n\t\t\tthis.namedActiveFilters = next\n\t\t\tthis.persistNamedFiltersToRoute(next)\n\t\t},\n\n\t\t/**\n\t\t * Write the active named-source filters into `$route.query`.\n\t\t *\n\t\t * So a filtered view can be SHARED, which is half of what a search\n\t\t * field is for: the colleague who opens the link sees the same list,\n\t\t * because `namedFiltersFromRoute` reads it back on mount.\n\t\t *\n\t\t * `replace`, not `push`: narrowing a list is not a place in the\n\t\t * history, and a back button that steps through six filter choices\n\t\t * never reaches the page before them. A rejected navigation (the same\n\t\t * path and query) is swallowed, matching `persistSortToRoute`.\n\t\t *\n\t\t * @param {object} filters The active `{ key: values }` map.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tpersistNamedFiltersToRoute(filters) {\n\t\t\tif (!this.$router || !this.$route) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst properties = declaredFilterFields(this)\n\t\t\tconst query = { ...this.$route.query }\n\t\t\tfor (const key of Object.keys(properties)) {\n\t\t\t\tif (properties[key] && properties[key].facetable === true) {\n\t\t\t\t\tdelete query[key]\n\t\t\t\t}\n\t\t\t}\n\t\t\tfor (const [key, values] of Object.entries(filters || {})) {\n\t\t\t\tconst value = namedFilterToQuery(values)\n\t\t\t\tif (value !== '') {\n\t\t\t\t\tquery[key] = value\n\t\t\t\t}\n\t\t\t}\n\t\t\tthis.$router.replace({ query }).catch(() => {})\n\t\t},\n\n\t\t/**\n\t\t * Folder selection from the opt-in folder sidebar: filter the list by the\n\t\t * config's `filterField` (or `field`); a null id clears it.\n\t\t *\n\t\t * @param {(string|number|null)} folderId The selected folder id (null = All).\n\t\t * @return {void}\n\t\t */\n\t\t/**\n\t\t * The actions one row offers: this page's declared actions narrowed to\n\t\t * the ones the server says this caller may run on that record. The\n\t\t * order, label and icon stay the page's.\n\t\t *\n\t\t * @param {object} row The row.\n\t\t * @return {Array<object>} The actions to render for that row.\n\t\t * @spec openspec/changes/working-list-row-actions/specs/index-page/spec.md\n\t\t */\n\t\t/**\n\t\t * Move the keyboard's focus one row, staying inside the list.\n\t\t *\n\t\t * @param {number} delta 1 for the next row, -1 for the previous one.\n\t\t * @return {void}\n\t\t * @spec openspec/changes/working-list-row-actions/specs/index-page/spec.md\n\t\t */\n\t\tmoveFocusedRow(delta) {\n\t\t\tconst rows = this.displayObjects || []\n\t\t\tif (rows.length === 0) {\n\t\t\t\tthis.focusedRowIndex = -1\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst next = this.focusedRowIndex + delta\n\t\t\tthis.focusedRowIndex = Math.min(Math.max(next, 0), rows.length - 1)\n\t\t},\n\n\t\t/**\n\t\t * Select or deselect the focused row.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\ttoggleFocusedRowSelection() {\n\t\t\tconst row = this.focusedRow\n\t\t\tif (!row) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst id = row[this.rowKey]\n\t\t\tconst selected = this.internalSelectedIds.includes(id)\n\t\t\tthis.onSelect(selected\n\t\t\t\t? this.internalSelectedIds.filter((other) => other !== id)\n\t\t\t\t: [...this.internalSelectedIds, id])\n\t\t},\n\n\t\t/**\n\t\t * Run the first action the focused row offers. \"First\" is the page's\n\t\t * order, narrowed by what the server allows on that record, so the\n\t\t * primary action is never one this caller may not run.\n\t\t *\n\t\t * @return {void}\n\t\t * @spec openspec/changes/working-list-row-actions/specs/index-page/spec.md\n\t\t */\n\t\trunPrimaryRowAction() {\n\t\t\tconst row = this.focusedRow\n\t\t\tif (!row) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst [first] = this.rowActionsFor(row)\n\t\t\tif (!first) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (typeof first.handler === 'function') {\n\t\t\t\tfirst.handler(row)\n\t\t\t}\n\t\t\tthis.$emit('action', { action: first.label, row })\n\t\t},\n\n\t\t/**\n\t\t * Handle a key pressed on the list.\n\t\t *\n\t\t * @param {KeyboardEvent} event The event.\n\t\t * @return {void}\n\t\t * @spec openspec/changes/working-list-row-actions/specs/index-page/spec.md\n\t\t */\n\t\tonListKeydown(event) {\n\t\t\tif (!this.listShortcuts) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst shortcut = shortcutFor(event)\n\t\t\tif (!shortcut) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst run = this.listShortcutHandlers[shortcut.id]\n\t\t\tif (typeof run !== 'function') {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tevent.preventDefault()\n\t\t\trun()\n\t\t},\n\n\t\t/**\n\t\t * Open the quick edit on one row.\n\t\t *\n\t\t * @param {object} row The row.\n\t\t * @return {void}\n\t\t * @spec openspec/changes/working-list-row-actions/specs/index-page/spec.md\n\t\t */\n\t\topenQuickEdit(row) {\n\t\t\tif (this.quickEditFields.length === 0 || !row) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.quickEditServerRow = null\n\t\t\tthis.quickEditRow = row\n\t\t},\n\n\t\t/**\n\t\t * Write a quick edit's patch onto its row in the list.\n\t\t *\n\t\t * The patch goes through the same row-patch map a record saved in the\n\t\t * split pane goes through, so the list keeps its scroll, its selection\n\t\t * and its page for the same reason it already did: nothing about the\n\t\t * list changed, one row's fields did.\n\t\t *\n\t\t * @param {object} payload The dialog's payload.\n\t\t * @param {(string|number)} payload.id The row id.\n\t\t * @param {object} payload.patch The fields to write.\n\t\t * @return {void}\n\t\t * @spec openspec/changes/working-list-row-actions/specs/index-page/spec.md\n\t\t */\n\t\tonQuickEditSave({ id, patch }) {\n\t\t\tif (id === undefined || id === null || !patch || Object.keys(patch).length === 0) {\n\t\t\t\tthis.quickEditRow = null\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst current = this.splitRowPatches[id] || {}\n\t\t\tthis.splitRowPatches = { ...this.splitRowPatches, [id]: { ...current, ...patch } }\n\t\t\tthis.quickEditRow = null\n\t\t\t/**\n\t\t\t * @event quick-edit-save Emitted with the fields a quick edit wrote onto a row.\n\t\t\t * @type {{id: (string|number), patch: object}}\n\t\t\t */\n\t\t\tthis.$emit('quick-edit-save', { id, patch })\n\t\t},\n\n\t\t/**\n\t\t * Take the server's version of a row after a conflict. Nothing of this\n\t\t * person's edit is written, and the row in the list is refreshed to\n\t\t * what the server holds, so the screen and the record agree.\n\t\t *\n\t\t * @param {object} saved The record as the server holds it.\n\t\t * @return {void}\n\t\t * @spec openspec/changes/working-list-row-actions/specs/index-page/spec.md\n\t\t */\n\t\tonQuickEditKeepTheirs(saved) {\n\t\t\tconst id = saved && saved[this.rowKey]\n\t\t\tif (id !== undefined && id !== null) {\n\t\t\t\tthis.splitRowPatches = { ...this.splitRowPatches, [id]: saved }\n\t\t\t}\n\t\t\tthis.quickEditRow = null\n\t\t\tthis.quickEditServerRow = null\n\t\t},\n\n\t\trowActionsFor(row) {\n\t\t\treturn availableRowActions(this.mergedActions, row, this.rowActionField)\n\t\t},\n\n\t\t/**\n\t\t * Why the server refused an action on a row, when it said. Available on\n\t\t * request rather than rendered, because a menu that lists what you may\n\t\t * not do is a menu that takes longer to read.\n\t\t *\n\t\t * @param {object} row The row.\n\t\t * @param {object} action The declared action.\n\t\t * @return {string} The refusal reason, or ''.\n\t\t * @spec openspec/changes/working-list-row-actions/specs/index-page/spec.md\n\t\t */\n\t\trowActionRefusal(row, action) {\n\t\t\treturn refusalReasonFor(action, row, this.rowActionField)\n\t\t},\n\n\t\t/**\n\t\t * The actions the server allowed on a row that this page does not\n\t\t * declare. Nothing renders them. They are here so a page can see what\n\t\t * it is ignoring without the menu growing a button nobody wrote.\n\t\t *\n\t\t * @param {object} row The row.\n\t\t * @return {string[]} The undeclared action ids.\n\t\t * @spec openspec/changes/working-list-row-actions/specs/index-page/spec.md\n\t\t */\n\t\trowActionsNotDeclared(row) {\n\t\t\treturn undeclaredRowActions(this.mergedActions, row, this.rowActionField)\n\t\t},\n\n\t\tonFolderSelect(folderId) {\n\t\t\tthis.selectedFolderId = folderId\n\t\t\tconst key = (this.folderSidebar && (this.folderSidebar.filterField || this.folderSidebar.field)) || ''\n\t\t\tif (key) {\n\t\t\t\tthis.onFilterEvent({ key, values: (folderId === null || folderId === undefined) ? [] : [folderId] })\n\t\t\t}\n\t\t\tthis.applyScopeSort()\n\t\t\t/**\n\t\t\t * @event folder-change Emitted when a folder is selected in the sidebar.\n\t\t\t * @type {(string|number|null)} The selected folder id (null = All).\n\t\t\t */\n\t\t\tthis.$emit('folder-change', folderId)\n\t\t},\n\n\t\t/**\n\t\t * Put the active scope's `defaultSort` on the list and refetch with it.\n\t\t *\n\t\t * The scope's sort is applied through the list's own `sortKeys`, the\n\t\t * same state a header click writes, rather than through a second sort\n\t\t * path — so the fetch carries it and the header shows it as sorted.\n\t\t * A scope declaring no sort leaves whatever is active alone: that is a\n\t\t * scope with nothing to say about sorting, not a scope asking for the\n\t\t * sort to be cleared.\n\t\t *\n\t\t * @return {void}\n\t\t * @spec openspec/changes/index-columns-per-scope/specs/index-page/spec.md\n\t\t */\n\t\tapplyScopeSort() {\n\t\t\tconst sortKeys = this.activeScopeLayout.sortKeys\n\t\t\tif (!sortKeys.length || !this.isSelfFetchMode || !this.list) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (this.list.sortKeys) {\n\t\t\t\tthis.list.sortKeys.value = sortKeys.map((entry) => ({ ...entry }))\n\t\t\t}\n\t\t\tif (typeof this.list.refresh === 'function') {\n\t\t\t\tthis.list.refresh(1)\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Load the folder list from an OpenRegister register/schema for a\n\t\t * `folderSidebar.source === 'register'` config. Maps each object to\n\t\t * `{ id: <idField|@self.uuid>, name: <nameField|title> }`.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync loadFolderRegister() {\n\t\t\tconst cfg = this.folderSidebar\n\t\t\tif (!cfg || cfg.source !== 'register' || !cfg.register || !cfg.schema) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tconst [{ default: axios }, { generateUrl }] = await Promise.all([\n\t\t\t\t\timport('@nextcloud/axios'),\n\t\t\t\t\timport('@nextcloud/router'),\n\t\t\t\t])\n\t\t\t\tconst url = generateUrl('/apps/openregister/api/objects/{register}/{schema}', { register: cfg.register, schema: cfg.schema })\n\t\t\t\tconst res = await axios.get(url, { params: { _limit: cfg.limit || 200 } })\n\t\t\t\tconst body = (res && res.data && (res.data.results || res.data)) || []\n\t\t\t\t// A response that is not a list is an empty folder list, not a\n\t\t\t\t// crash: the pane then shows only \"All\" rather than logging a\n\t\t\t\t// TypeError that names this component for the server's shape.\n\t\t\t\tconst rows = Array.isArray(body) ? body : []\n\t\t\t\tconst idField = cfg.idField || '@self.uuid'\n\t\t\t\tconst nameField = cfg.nameField || 'title'\n\t\t\t\tconst layouts = {}\n\t\t\t\trows.forEach((row) => {\n\t\t\t\t\tconst id = this.getByPath(row, idField)\n\t\t\t\t\tconst carried = row && row['x-index']\n\t\t\t\t\tif (id !== null && id !== undefined && carried && typeof carried === 'object') {\n\t\t\t\t\t\tlayouts[String(id)] = carried\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t\tthis.folderRowLayouts = layouts\n\t\t\t\tthis.folderRegisterList = rows\n\t\t\t\t\t.map((row) => ({ id: this.getByPath(row, idField), name: this.getByPath(row, nameField) || this.getByPath(row, idField) }))\n\t\t\t\t\t.filter((f) => f.id !== null && f.id !== undefined)\n\t\t\t\t\t.sort((a, b) => String(a.name).localeCompare(String(b.name)))\n\t\t\t} catch (e) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.error('[CnIndexPage] failed to load folder register', e)\n\t\t\t\tthis.folderRegisterList = []\n\t\t\t\tthis.folderRowLayouts = {}\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Whether a given value is currently an active facet filter for a field.\n\t\t *\n\t\t * @param {string} key The schema field key.\n\t\t * @param {string} val The enum value.\n\t\t * @return {boolean} True when the value is in the active filter set.\n\t\t */\n\t\tisFilterActive(key, val) {\n\t\t\tconst current = this.effectiveActiveFilters[key]\n\t\t\treturn Array.isArray(current) && current.includes(val)\n\t\t},\n\n\t\t/**\n\t\t * Toggle one enum value in a field's facet filter, then apply it through\n\t\t * the standard filter path (`onFilterEvent`).\n\t\t *\n\t\t * @param {string} key The schema field key.\n\t\t * @param {string} val The enum value to toggle.\n\t\t * @return {void}\n\t\t */\n\t\ttoggleFilter(key, val) {\n\t\t\tconst current = Array.isArray(this.effectiveActiveFilters[key]) ? this.effectiveActiveFilters[key] : []\n\t\t\tconst values = current.includes(val) ? current.filter((v) => v !== val) : [...current, val]\n\t\t\tthis.onFilterEvent({ key, values })\n\t\t},\n\n\t\t/**\n\t\t * Whether a governed column is currently visible. A null visible-column set\n\t\t * means \"all governed columns visible\" (the initial state).\n\t\t *\n\t\t * @param {string} key The column key.\n\t\t * @return {boolean} True when the column is shown in the table.\n\t\t */\n\t\tisColumnVisible(key) {\n\t\t\tconst visible = this.effectiveVisibleColumns\n\t\t\treturn Array.isArray(visible) ? visible.includes(key) : true\n\t\t},\n\n\t\t/**\n\t\t * Toggle a governed column on/off from the table-header column menu, then\n\t\t * apply it through the same path as the sidebar (`onColumnsEvent`).\n\t\t *\n\t\t * @param {string} key The column key to toggle.\n\t\t * @return {void}\n\t\t */\n\t\ttoggleColumn(key) {\n\t\t\tconst base = Array.isArray(this.effectiveVisibleColumns)\n\t\t\t\t? this.effectiveVisibleColumns\n\t\t\t\t: this.governedColumns.map((c) => c.key)\n\t\t\tconst next = base.includes(key) ? base.filter((k) => k !== key) : [...base, key]\n\t\t\tthis.onColumnsEvent(next)\n\t\t},\n\n\t\t/**\n\t\t * @param {Array} columns Visible-column change from the sidebar.\n\t\t * @return {void}\n\t\t */\n\t\tonColumnsEvent(columns) {\n\t\t\tif (this.isSelfFetchMode && this.list.visibleColumns) {\n\t\t\t\tthis.list.visibleColumns.value = columns\n\t\t\t}\n\t\t\tthis.$emit('columns-change', columns)\n\t\t},\n\n\t\t/** @return {Promise<void>} */\n\t\tasync onRefreshEvent() {\n\t\t\tthis.$emit('refresh')\n\t\t\tif (this.isSelfFetchMode && typeof this.list.refresh === 'function') {\n\t\t\t\tthis.internalRefreshing = true\n\t\t\t\ttry {\n\t\t\t\t\tawait this.list.refresh()\n\t\t\t\t} finally {\n\t\t\t\t\tthis.internalRefreshing = false\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Publish (or clear) the embedded CnIndexSidebar config to\n\t\t * the `cnIndexSidebarConfig` holder so CnAppRoot can mount\n\t\t * it at NcContent level. No-op when no CnAppRoot ancestor\n\t\t * exists — in that case `shouldRenderInlineSidebar` keeps\n\t\t * the inline render alive.\n\t\t */\n\t\tpublishHoistedSidebar() {\n\t\t\tif (!this.cnHostsIndexSidebar || !this.cnIndexSidebarConfig) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (!this.resolvedSidebar.enabled || this.resolvedSidebar.show === false) {\n\t\t\t\tthis.cnIndexSidebarConfig.value = null\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.cnIndexSidebarConfig.value = {\n\t\t\t\t// markRaw: the holder is CnAppRoot `data()`, so it is deeply\n\t\t\t\t// reactive and would proxy the component definition itself.\n\t\t\t\tcomponent: markRaw(CnIndexSidebar),\n\t\t\t\tprops: this.hoistedSidebarProps,\n\t\t\t\tlisteners: {\n\t\t\t\t\t'update:open': (val) => {\n\t\t\t\t\t\tthis.sidebarOpen = val\n\t\t\t\t\t},\n\n\t\t\t\t\tsearch: (event) => this.onSearchEvent(event),\n\t\t\t\t\t'columns-change': (event) => this.onColumnsEvent(event),\n\t\t\t\t\t'filter-change': (event) => this.onFilterEvent(event),\n\t\t\t\t\t'clear-filters': () => this.onClearFilters(),\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Intercepts CnRowActions' bubbled `@action` so `handler: \"none\"`\n\t\t * actions are dropped before re-emit.\n\t\t *\n\t\t * @param {{action: string, row: object}} payload The bubbled action payload.\n\t\t */\n\t\tonRowAction(payload) {\n\t\t\tconst matched = this.mergedActions.find((a) => a.label === payload.action)\n\t\t\tif (matched && matched._dispatchSuppress) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.$emit('action', payload)\n\t\t},\n\n\t\t/**\n\t\t * A card moved on the board.\n\t\t *\n\t\t * The list is refreshed rather than patched in place: the transition\n\t\t * may have changed more than the status (a date armed, an assignee\n\t\t * cleared), and a board that only moved the card would show a row that\n\t\t * disagrees with the table beside it.\n\t\t *\n\t\t * @param {object} move The `{ card, toKey }` the board emitted.\n\t\t * @return {Promise<void>} Nothing.\n\t\t */\n\t\tasync onBoardMoved(move) {\n\t\t\t/**\n\t\t\t * @event board-move A card moved through the host's transition.\n\t\t\t * @type {object}\n\t\t\t */\n\t\t\tthis.$emit('board-move', move)\n\t\t\tawait this.onRefreshEvent()\n\t\t},\n\n\t\t/**\n\t\t * Row/card click: toggles selection when `selectable` (covers the custom\n\t\t * `cardComponent` path), otherwise emits `row-click` for navigation. A\n\t\t * ctrl/cmd/shift or middle click opens a named source's row in a new tab.\n\t\t *\n\t\t * The `@event` block below sits directly against its `$emit`, and must\n\t\t * stay there. vue-docgen binds an event's description to the emit it\n\t\t * IMMEDIATELY precedes.\n\t\t *\n\t\t * @param {object} row The clicked row object\n\t\t * @param {MouseEvent|KeyboardEvent} [event] The originating click/auxclick event.\n\t\t */\n\t\tonRowClick(row, event) {\n\t\t\tif (this.selectable && !this.rowClickOpens) {\n\t\t\t\tif (!event || event.button === undefined || event.button === 0) {\n\t\t\t\t\tthis.onSelect(this.toggleIdInArray(this.internalSelectedIds, row[this.rowKey]))\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\t\t\t// A named source knows where its rows live. Emitting only would leave\n\t\t\t// the click inert on a manifest page, which has no listener to bind —\n\t\t\t// the very shape that left three apps with a dead `@rowClick`.\n\t\t\t//\n\t\t\t// `rowRoute` wins over everything: an app that declares its own\n\t\t\t// page for these rows means it. The source's `openRow` is the\n\t\t\t// right default precisely because a task's page is usually\n\t\t\t// openregister's, and it is wrong for an app that ships one of\n\t\t\t// its own. This cannot be done by listening to `row-click`,\n\t\t\t// because `openRow` calls `window.location.assign()` and the\n\t\t\t// host's push never lands.\n\t\t\t//\n\t\t\t// A ctrl/cmd/shift or middle click opens a new tab instead, from\n\t\t\t// `rowRoute`, the source's `rowTarget(row)` or `detailRoute`. A source\n\t\t\t// with only `openRow` opens the row in place on a ctrl/cmd/shift\n\t\t\t// click and ignores a middle click. The event is marked so the host\n\t\t\t// hearing `row-click` does not open a second tab.\n\t\t\tif (this.isNamedSource) {\n\t\t\t\tconst id = row?.id || row?.uuid\n\t\t\t\tconst source = this.namedSource\n\t\t\t\tconst newTab = isNewTabClick(event)\n\t\t\t\tconst openTarget = (target) => markNewTabHandled(event, openRowTarget(event, target, this.$router))\n\t\t\t\tif (this.rowRoute) {\n\t\t\t\t\tif (id && newTab) {\n\t\t\t\t\t\topenTarget({ name: this.rowRoute, params: { id: String(id) } })\n\t\t\t\t\t} else if (id) {\n\t\t\t\t\t\tthis.$router.push({ name: this.rowRoute, params: { id: String(id) } })\n\t\t\t\t\t}\n\t\t\t\t} else if (source && typeof source.openRow === 'function' && !newTab) {\n\t\t\t\t\tsource.openRow(row)\n\t\t\t\t} else if (source && typeof source.rowTarget === 'function') {\n\t\t\t\t\topenTarget(source.rowTarget(row))\n\t\t\t\t} else if (source && typeof source.openRow === 'function') {\n\t\t\t\t\t// No new-tab target: a modified click still opens the row in place.\n\t\t\t\t\tif (event?.button !== 1) {\n\t\t\t\t\t\tsource.openRow(row)\n\t\t\t\t\t}\n\t\t\t\t} else if (source && source.detailRoute && id) {\n\t\t\t\t\tif (newTab) {\n\t\t\t\t\t\topenTarget({ path: `${source.detailRoute}/${id}` })\n\t\t\t\t\t} else {\n\t\t\t\t\t\tthis.$router.push(`${source.detailRoute}/${id}`)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\t/**\n\t\t\t * @event row-click Emitted on a row/card click (or middle click) for navigation. Fires when `selectable` is false, OR when `rowClickToView` is set and something can open the row (selection then happens via the checkbox). Payload: `(row, event)` — the clicked row and the native click/auxclick event (absent for a keyboard shortcut or map marker), so a host can open the row in a new tab on a ctrl/cmd/shift or middle click with `openRowTarget`.\n\t\t\t * @type {object} The clicked row object.\n\t\t\t */\n\t\t\tthis.$emit('row-click', ...(event ? [row, event] : [row]))\n\t\t},\n\n\t\t/**\n\t\t * Whether a value is a DOM event. A custom list/card component's\n\t\t * `click` may carry the item, the native event, or both.\n\t\t *\n\t\t * @param {unknown} value A `click` listener argument.\n\t\t * @return {boolean}\n\t\t */\n\t\tisDomEvent(value) {\n\t\t\treturn typeof Event !== 'undefined' && value instanceof Event\n\t\t},\n\n\t\t/**\n\t\t * Middle click on a custom list/card component's root: open the row\n\t\t * like a click would.\n\t\t *\n\t\t * @param {object} row The component's row.\n\t\t * @param {MouseEvent} event The auxclick event.\n\t\t * @return {void}\n\t\t */\n\t\tonCustomItemAuxClick(row, event) {\n\t\t\tif (isRowMiddleClick(event)) {\n\t\t\t\tthis.onRowClick(row, event)\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Resolve a marker click on the map back to its source row and route it\n\t\t * through `onRowClick`, so `@row-click` fires with the identical payload a\n\t\t * table/card click would emit — giving uniform detail-page navigation\n\t\t * across all three view modes.\n\t\t *\n\t\t * @param {{ feature: object }} payload CnMapWidget marker-click payload.\n\t\t */\n\t\tonMarkerClick(payload) {\n\t\t\tconst feature = payload && payload.feature\n\t\t\tconst key = feature && feature.properties ? feature.properties[this.rowKey] : undefined\n\t\t\tif (key === undefined || key === null) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst row = this.displayObjects.find((o) => o[this.rowKey] === key)\n\t\t\tif (row) {\n\t\t\t\tthis.onRowClick(row)\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Extract a `{ lat, lng }` pair from a row using `mapConfig`. Prefers a\n\t\t * GeoJSON Point on `geoField` (`coordinates: [lng, lat]`), else reads the\n\t\t * dotted `latField` / `lngField` paths. Returns null when the geometry is\n\t\t * missing or non-finite, so callers can skip the row.\n\t\t *\n\t\t * @param {object} row The source object.\n\t\t * @return {{ lat: number, lng: number } | null}\n\t\t */\n\t\tresolveRowLatLng(row) {\n\t\t\tif (!row) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst cfg = this.mapConfig || {}\n\t\t\tif (cfg.geoField) {\n\t\t\t\tlet geo = this.getByPath(row, cfg.geoField)\n\t\t\t\t// OpenRegister often stores geometry as a JSON-encoded string on\n\t\t\t\t// object metadata; parse it before reading `coordinates`.\n\t\t\t\tif (typeof geo === 'string') {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tgeo = JSON.parse(geo)\n\t\t\t\t\t} catch {\n\t\t\t\t\t\tgeo = null\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tconst coords = geo && Array.isArray(geo.coordinates) ? geo.coordinates : null\n\t\t\t\tif (coords && Number.isFinite(coords[0]) && Number.isFinite(coords[1])) {\n\t\t\t\t\treturn { lat: Number(coords[1]), lng: Number(coords[0]) }\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst lat = Number(this.getByPath(row, cfg.latField))\n\t\t\tconst lng = Number(this.getByPath(row, cfg.lngField))\n\t\t\tif (Number.isFinite(lat) && Number.isFinite(lng)) {\n\t\t\t\treturn { lat, lng }\n\t\t\t}\n\t\t\treturn null\n\t\t},\n\n\t\t/**\n\t\t * Resolve a row's GeoJSON geometry for the map view. Prefers a full\n\t\t * geometry on `geoField`: a Point renders as a marker, a Polygon /\n\t\t * MultiPolygon / LineString renders as an area/line — so location cases\n\t\t * plot as both points AND areas. `geoField` is parsed from a JSON string\n\t\t * when OpenRegister stores it encoded. Falls back to a Point built from\n\t\t * `latField` / `lngField`. Returns null when nothing resolvable is present.\n\t\t *\n\t\t * @param {object} row The source object.\n\t\t * @return {object|null} A GeoJSON geometry object, or null.\n\t\t */\n\t\tresolveRowGeometry(row) {\n\t\t\tif (!row) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst cfg = this.mapConfig || {}\n\t\t\tconst GEO_TYPES = ['Point', 'MultiPoint', 'LineString', 'MultiLineString', 'Polygon', 'MultiPolygon', 'GeometryCollection']\n\t\t\tif (cfg.geoField) {\n\t\t\t\tlet geo = this.getByPath(row, cfg.geoField)\n\t\t\t\tif (typeof geo === 'string') {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tgeo = JSON.parse(geo)\n\t\t\t\t\t} catch {\n\t\t\t\t\t\tgeo = null\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (geo && GEO_TYPES.includes(geo.type)) {\n\t\t\t\t\tconst hasShape = geo.type === 'GeometryCollection'\n\t\t\t\t\t\t? Array.isArray(geo.geometries)\n\t\t\t\t\t\t: Array.isArray(geo.coordinates)\n\t\t\t\t\tif (hasShape) {\n\t\t\t\t\t\treturn geo\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst lat = Number(this.getByPath(row, cfg.latField))\n\t\t\tconst lng = Number(this.getByPath(row, cfg.lngField))\n\t\t\tif (Number.isFinite(lat) && Number.isFinite(lng)) {\n\t\t\t\treturn { type: 'Point', coordinates: [lng, lat] }\n\t\t\t}\n\t\t\treturn null\n\t\t},\n\n\t\t/**\n\t\t * Dig out the first `[lng, lat]` coordinate pair from any GeoJSON geometry\n\t\t * (Point, Polygon ring, GeometryCollection, …) for a rough centroid.\n\t\t *\n\t\t * @param {object} geometry A GeoJSON geometry.\n\t\t * @return {{lat: number, lng: number}|null}\n\t\t */\n\t\tfirstLatLng(geometry) {\n\t\t\tif (!geometry) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tif (geometry.type === 'GeometryCollection') {\n\t\t\t\tfor (const g of (geometry.geometries || [])) {\n\t\t\t\t\tconst p = this.firstLatLng(g)\n\t\t\t\t\tif (p) {\n\t\t\t\t\t\treturn p\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tlet c = geometry.coordinates\n\t\t\twhile (Array.isArray(c) && Array.isArray(c[0])) {\n\t\t\t\tc = c[0]\n\t\t\t}\n\t\t\tif (Array.isArray(c) && Number.isFinite(c[0]) && Number.isFinite(c[1])) {\n\t\t\t\treturn { lat: Number(c[1]), lng: Number(c[0]) }\n\t\t\t}\n\t\t\treturn null\n\t\t},\n\n\t\t/**\n\t\t * Read a possibly-dotted property path off an object (e.g. `@self.geo.lat`).\n\t\t * Returns undefined on any missing segment. No path = undefined.\n\t\t *\n\t\t * @param {object} obj The object to read from.\n\t\t * @param {string} path Dot-separated property path.\n\t\t * @return {unknown} The resolved value or undefined.\n\t\t */\n\t\tgetByPath(obj, path) {\n\t\t\tif (!obj || !path) {\n\t\t\t\treturn undefined\n\t\t\t}\n\t\t\tif (Object.hasOwn(obj, path)) {\n\t\t\t\treturn obj[path]\n\t\t\t}\n\t\t\treturn path.split('.').reduce((acc, seg) => (acc === null || acc === undefined ? undefined : acc[seg]), obj)\n\t\t},\n\n\t\t/**\n\t\t * Whether the consumer explicitly passed a prop, in either spelling.\n\t\t *\n\t\t * Needed where a named source changes a prop's EFFECTIVE default: the\n\t\t * declared default is unchanged (the prop interface must not break),\n\t\t * so the value alone cannot distinguish \"left to default\" from\n\t\t * \"explicitly asked for the default\".\n\t\t *\n\t\t * @param {string} name The camelCase prop name.\n\t\t * @return {boolean} True when the prop was passed explicitly.\n\t\t */\n\t\thasExplicitProp(name) {\n\t\t\tconst props = this.$.vnode.props || {}\n\t\t\tconst kebab = name.replace(/[A-Z]/g, (letter) => '-' + letter.toLowerCase())\n\t\t\treturn (name in props) || (kebab in props)\n\t\t},\n\n\t\t/**\n\t\t * Open a named-source row: the source's own navigation, then\n\t\t * `edit-open` for the host.\n\t\t *\n\t\t * This is what the source-action grammar `action: 'open'` resolves to\n\t\t * (see `mergedActions`). Navigation order is deliberate: the source's\n\t\t * `openRow`/`detailRoute` push happens FIRST, so when the host also\n\t\t * navigates (CnPageRenderer binds `@edit-open` → `onRowOpen`, which\n\t\t * honours `config.rowRoute`), the host's push lands last and wins —\n\t\t * the configured target beats the source's default, and a page with\n\t\t * no host listener still navigates.\n\t\t *\n\t\t * @param {object} row The row to open.\n\t\t * @return {void}\n\t\t */\n\t\topenSourceRow(row) {\n\t\t\tif (this.isNamedSource && this.namedSource && typeof this.namedSource.openRow === 'function') {\n\t\t\t\tthis.namedSource.openRow(row)\n\t\t\t} else if (this.isNamedSource && this.namedSource && this.namedSource.detailRoute) {\n\t\t\t\tconst id = row?.id || row?.uuid\n\t\t\t\tif (id && this.$router) {\n\t\t\t\t\tthis.$router.push(`${this.namedSource.detailRoute}/${id}`)\n\t\t\t\t}\n\t\t\t}\n\t\t\tthis.$emit('edit-open', row)\n\t\t},\n\n\t\t/**\n\t\t * Handle the built-in View action — emits a dedicated `view` event.\n\t\t * Kept distinct from `row-click` because the two are conceptually\n\t\t * different: a row click might mean select/expand/drilldown, while\n\t\t * View always means \"open the detail view of this row\".\n\t\t *\n\t\t * @param {object} row The row whose View action was triggered\n\t\t */\n\t\tonView(row) {\n\t\t\tthis.$emit('view', row)\n\t\t},\n\n\t\t/**\n\t\t * Handle the Add button click. If the consumer listens to `@add`,\n\t\t * emit the event (backward compatible). Otherwise open the form dialog.\n\t\t */\n\t\tonAddClick() {\n\t\t\t// A NAMED SOURCE CREATES BY NAVIGATING, not by the form dialog. That\n\t\t\t// dialog builds an OpenRegister object from a schema; a flow is not\n\t\t\t// one, which is why the page it replaces set `show-add=\"false\"` and\n\t\t\t// rendered its own button. An explicit @add listener still wins.\n\t\t\tif (\n\t\t\t\t!this.$.vnode.props?.onAdd\n\t\t\t\t&& this.isNamedSource\n\t\t\t\t&& this.namedSource\n\t\t\t\t&& this.namedSource.addRoute\n\t\t\t) {\n\t\t\t\t// With `addLinkTo` set, Add is a link that already navigated.\n\t\t\t\tif (!this.addLinkTo) {\n\t\t\t\t\tthis.$router.push(this.namedSource.addRoute)\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\t\t\t// `$.vnode.props`, not `$attrs`: `add` is a declared emit, and Vue\n\t\t\t// keeps declared emits out of `$attrs`.\n\t\t\tif (this.$.vnode.props?.onAdd) {\n\t\t\t\tthis.$emit('add')\n\t\t\t} else if (!this.openCreateModal() && this.showFormDialog) {\n\t\t\t\tthis.editItem = null\n\t\t\t\tthis.showFormDialogVisible = true\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Open the built-in create dialog when the route carries `?action=create`.\n\t\t * Called on `mounted()` and whenever `$route.query.action` changes, so it\n\t\t * works both on initial navigation and on same-page deep-links (e.g. a\n\t\t * \"New Case\" button on a sibling page that routes here with the query).\n\t\t *\n\t\t * After opening the dialog the query param is cleared via\n\t\t * `$router.replace` so a page refresh does not re-open it and browser\n\t\t * history stays clean. No-op when there is no router, when `showFormDialog`\n\t\t * is false (consumer manages its own dialog), or when the query param is\n\t\t * absent / not `'create'`.\n\t\t */\n\t\tmaybeOpenCreateFromQuery() {\n\t\t\tif (!this.$route || !this.$route.query || this.$route.query.action !== 'create') {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (!this.openCreateModal()) {\n\t\t\t\tif (!this.showFormDialog) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tthis.openFormDialog(null)\n\t\t\t}\n\t\t\t// Clear the query param; guard against redundant navigation errors.\n\t\t\tif (this.$router) {\n\t\t\t\tconst query = { ...this.$route.query }\n\t\t\t\tdelete query.action\n\t\t\t\tconst nav = this.$router.replace({ query })\n\t\t\t\t// $router.replace returns a Promise in Vue Router 3 but may\n\t\t\t\t// return undefined in mocked / legacy environments — guard.\n\t\t\t\tif (nav && typeof nav.catch === 'function') {\n\t\t\t\t\tnav.catch(() => {})\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Handle view mode toggle.\n\t\t *\n\t\t * @param {string} mode 'table' or 'cards'\n\t\t */\n\t\tonViewModeChange(mode) {\n\t\t\tthis.currentViewMode = mode\n\t\t\tthis.$emit('view-mode-change', mode)\n\t\t},\n\n\t\t/**\n\t\t * Handle selection changes from CnDataTable/CnCardGrid.\n\t\t * Updates internal state and re-emits for parent.\n\t\t *\n\t\t * @param {Array} ids Array of selected row IDs\n\t\t */\n\t\tonSelect(ids) {\n\t\t\tthis.internalSelectedIds = ids\n\t\t\tthis.$emit('select', ids)\n\t\t},\n\n\t\t// --- Mass action handlers ---\n\n\t\tasync onMassDeleteConfirm(ids) {\n\t\t\tif (await this.selfActions.handleMassDelete(ids)) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.$emit('mass-delete', ids)\n\t\t},\n\n\t\tasync onMassCopyConfirm(payload) {\n\t\t\tif (await this.selfActions.handleMassCopy(payload)) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.$emit('mass-copy', payload)\n\t\t},\n\n\t\tasync onMassExportConfirm(payload) {\n\t\t\tif (await this.selfActions.handleMassExport(payload)) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.$emit('mass-export', payload)\n\t\t},\n\n\t\t/**\n\t\t * Native Export menu entry click (`allowExport` + `schema.exportable`).\n\t\t * Navigates the browser to OpenRegister's export leaf, passing the\n\t\t * current route's query params through so a filtered index exports\n\t\t * only the visible rows.\n\t\t *\n\t\t * @param {'csv'|'excel'} format The requested export format.\n\t\t */\n\t\tonExportClick(format) {\n\t\t\tconst routeQuery = (this.$route && this.$route.query) || {}\n\t\t\tconst url = buildExportUrl(this.register, this.exportSchemaSlug, routeQuery, format)\n\t\t\twindow.location.assign(url)\n\t\t},\n\n\t\t// ── Saved views (saved-views-ui) ─────────────────────────────────────\n\n\t\t/**\n\t\t * Fetch the current user's saved views (own + public) from\n\t\t * OpenRegister's views API. Called from created() when\n\t\t * `allowSavedViews` is enabled.\n\t\t */\n\t\tasync fetchSavedViews() {\n\t\t\tthis.savedViewsLoading = true\n\t\t\ttry {\n\t\t\t\tthis.savedViews = await useSavedViewsApi().fetchViews()\n\t\t\t} catch (error) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.error('CnIndexPage: failed to fetch saved views', error)\n\t\t\t} finally {\n\t\t\t\tthis.savedViewsLoading = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Apply a saved view. Self-fetch mode's facet/folder filters and\n\t\t * search/sort live in useListView's own reactive state, not the route\n\t\t * query, so apply them there directly. Otherwise (consumer-managed\n\t\t * store mode) fall back to `$router.replace` — same precedent as the\n\t\t * `?action=create` query cleanup, and dropping the whole previous\n\t\t * query implicitly resets `_page` to 1.\n\t\t *\n\t\t * @param {object} view The View API object to apply.\n\t\t */\n\t\tonApplySavedView(view) {\n\t\t\tconst state = extractViewState(view)\n\t\t\tif (this.isSelfFetchMode) {\n\t\t\t\t// Set state directly (not via onFilterEvent/onSearchEvent per key)\n\t\t\t\t// so applying a view is one fetch + one route replace, not one\n\t\t\t\t// of each per changed field — and a filter the view doesn't\n\t\t\t\t// carry is actually cleared, not left over from before.\n\t\t\t\tthis.list.activeFilters.value = { ...state.filters }\n\t\t\t\tthis.list.searchTerm.value = state.search || ''\n\t\t\t\tconst keys = state.sortKeys\n\t\t\t\tthis.list.sortKeys.value = keys\n\t\t\t\tthis.list.sortKey.value = keys[0]?.key ?? null\n\t\t\t\tthis.list.sortOrder.value = keys[0]?.order ?? 'asc'\n\t\t\t\tthis.list.refresh(1)\n\t\t\t\t// Through `persistViewStateToRoute` rather than a raw replace,\n\t\t\t\t// because it is what records the keys this view wrote — without\n\t\t\t\t// that, a later \"clear all\" has nothing to delete them by.\n\t\t\t\tthis.persistViewStateToRoute(state)\n\t\t\t\tthis.$emit('apply-view', view)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (!this.$router) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst nav = this.$router.replace({ query: buildRouteQueryFromViewState(state) })\n\t\t\t// Swallow the duplicate-navigation rejection (Vue Router 3)\n\t\t\t// when the applied view matches the current query.\n\t\t\tif (nav && typeof nav.catch === 'function') {\n\t\t\t\tnav.catch(() => {})\n\t\t\t}\n\t\t\tthis.$emit('apply-view', view)\n\t\t},\n\n\t\t/**\n\t\t * Persist the current state as a named view via OpenRegister's views\n\t\t * API (CnSaveViewDialog `@confirm`). Self-fetch mode reads its active\n\t\t * filters/search/sort straight from useListView's own state (they\n\t\t * never reach the route query); otherwise falls back to the route\n\t\t * query. On success the new view joins the list and the dialog\n\t\t * closes; on failure the dialog stays open with the error surfaced.\n\t\t *\n\t\t * @param {{ name: string, isPublic: boolean }} payload Dialog payload.\n\t\t */\n\t\t/**\n\t\t * Toast, without letting the toast fail the thing it reports on.\n\t\t *\n\t\t * The import is dynamic so a page that never toasts does not carry the\n\t\t * chunk, and everything is swallowed: a chunk that will not load must\n\t\t * not turn a save that worked into an error, nor add an unhandled\n\t\t * rejection on top of one that already failed.\n\t\t *\n\t\t * @param {'success'|'error'} kind Which toast to show.\n\t\t * @param {string} message The message, already translated.\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync toastSavedView(kind, message) {\n\t\t\ttry {\n\t\t\t\tconst dialogs = await import('@nextcloud/dialogs')\n\t\t\t\tconst show = kind === 'error' ? dialogs.showError : dialogs.showSuccess\n\t\t\t\tif (typeof show === 'function') {\n\t\t\t\t\tshow(message)\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// No toast. What it was reporting on happened either way.\n\t\t\t}\n\t\t},\n\n\t\tasync onSaveViewConfirm({ name, isPublic }) {\n\t\t\tconst state = this.isSelfFetchMode\n\t\t\t\t? this.currentViewState()\n\t\t\t\t: extractViewStateFromRouteQuery((this.$route && this.$route.query) || {})\n\t\t\tconst payload = buildViewCreatePayload({\n\t\t\t\tname,\n\t\t\t\tdescription: '',\n\t\t\t\tisPublic,\n\t\t\t\tisDefault: false,\n\t\t\t\tstate,\n\t\t\t\tscope: savedViewScope(this.savedViewsPage),\n\t\t\t\tregister: this.register,\n\t\t\t\tschema: this.schema,\n\t\t\t})\n\t\t\ttry {\n\t\t\t\tconst view = await useSavedViewsApi().createView(payload)\n\t\t\t\tif (!view) {\n\t\t\t\t\t// A 2xx carrying no view. Axios has thrown on every real error\n\t\t\t\t\t// by now, so nothing else marks this one, and the list below is\n\t\t\t\t\t// not appended to: claiming success sends the person looking\n\t\t\t\t\t// for a view that is not in it.\n\t\t\t\t\tthrow new Error(t('nextcloud-vue', 'The server did not return the saved view'))\n\t\t\t\t}\n\t\t\t\tthis.savedViews = [...this.savedViews, view]\n\t\t\t\tthis.showSaveViewDialog = false\n\t\t\t\tthis.toastSavedView('success', t('nextcloud-vue', 'View \"{name}\" saved', { name }))\n\t\t\t} catch (error) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.error('CnIndexPage: failed to save view', error)\n\t\t\t\t// Both, and they say different things: the dialog stays open\n\t\t\t\t// carrying the reason, because that is where the name and the\n\t\t\t\t// toggle still are to correct and retry; the toast says the save\n\t\t\t\t// did not happen, because a dialog that simply stayed open reads\n\t\t\t\t// as one that has not been submitted yet.\n\t\t\t\tthis.$refs.saveViewDialog?.setError(error?.response?.data?.error || error?.message)\n\t\t\t\tthis.toastSavedView('error', t('nextcloud-vue', 'Could not save the view \"{name}\"', { name }))\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Open the delete-confirmation dialog for a saved view\n\t\t * (CnSavedViewsControl `@delete-request`).\n\t\t *\n\t\t * @param {object} view The View API object to delete.\n\t\t */\n\t\tonDeleteViewRequest(view) {\n\t\t\tthis.viewPendingDelete = view\n\t\t},\n\n\t\t/**\n\t\t * Delete the pending view after confirmation (CnConfirmDialog\n\t\t * `@confirm`), then report back via the dialog's setResult()\n\t\t * contract and remove it from the local list.\n\t\t */\n\t\tasync onDeleteViewConfirm() {\n\t\t\tconst view = this.viewPendingDelete\n\t\t\tif (!view) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tawait useSavedViewsApi().deleteView(view.id)\n\t\t\t\tthis.savedViews = this.savedViews.filter((v) => v.id !== view.id)\n\t\t\t\tthis.$refs.deleteViewConfirmDialog?.setResult({ success: true })\n\t\t\t} catch (error) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.error('CnIndexPage: failed to delete view', error)\n\t\t\t\tthis.$refs.deleteViewConfirmDialog?.setResult({ error: error?.response?.data?.error || error?.message || 'Failed to delete view' })\n\t\t\t}\n\t\t},\n\n\t\tasync onMassImportConfirm(payload) {\n\t\t\tif (await this.selfActions.handleMassImport(payload)) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.$emit('mass-import', payload)\n\t\t},\n\n\t\t_setResult(refName, resultData) {\n\t\t\tthis.$refs[refName]?.setResult(resultData)\n\t\t},\n\n\t\t/**\n\t\t * @param {{ success?: boolean, error?: string }} resultData Result data to pass to the dialog\n\t\t * @public\n\t\t */\n\t\tsetMassDeleteResult(resultData) {\n\t\t\tthis._setResult('massDeleteDialog', resultData)\n\t\t},\n\n\t\t/**\n\t\t * @param {{ success?: boolean, error?: string }} resultData Result data to pass to the dialog\n\t\t * @public\n\t\t */\n\t\tsetMassCopyResult(resultData) {\n\t\t\tthis._setResult('massCopyDialog', resultData)\n\t\t},\n\n\t\t/**\n\t\t * @param {{ success?: boolean, error?: string }} resultData Result data to pass to the dialog\n\t\t * @public\n\t\t */\n\t\tsetExportResult(resultData) {\n\t\t\tthis._setResult('exportDialog', resultData)\n\t\t},\n\n\t\t/**\n\t\t * @param {{ success?: boolean, error?: string }} resultData Result data to pass to the dialog\n\t\t * @public\n\t\t */\n\t\tsetImportResult(resultData) {\n\t\t\tthis._setResult('importDialog', resultData)\n\t\t},\n\n\t\t/**\n\t\t * @param {{ success?: boolean, error?: string }} resultData Result data to pass to the dialog\n\t\t * @public\n\t\t */\n\t\tsetDeleteResult(resultData) {\n\t\t\tthis._setResult('massDeleteDialog', resultData)\n\t\t},\n\n\t\t/**\n\t\t * @param {{ success?: boolean, error?: string }} resultData Result data to pass to the dialog\n\t\t * @public\n\t\t */\n\t\tsetCopyResult(resultData) {\n\t\t\tthis._setResult('massCopyDialog', resultData)\n\t\t},\n\n\t\t// --- Single-object dialog handlers ---\n\n\t\tasync onSingleDeleteConfirm(id) {\n\t\t\t// A named source deletes through its own hook. Emit-only would\n\t\t\t// leave the confirm inert on a manifest page (nobody listens),\n\t\t\t// which is why the Delete action only renders when the hook\n\t\t\t// exists — see `defaultActions`.\n\t\t\tif (this.isNamedSource && this.namedSource && typeof this.namedSource.deleteRow === 'function') {\n\t\t\t\ttry {\n\t\t\t\t\tawait this.namedSource.deleteRow(this.actionTargetItem || { id }, this.sourceConfig || {})\n\t\t\t\t\tthis.setSingleDeleteResult({ success: true })\n\t\t\t\t\tthis.$emit('delete', id)\n\t\t\t\t} catch (err) {\n\t\t\t\t\tthis.setSingleDeleteResult({ error: (err && err.message) || 'Delete failed' })\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (await this.selfActions.handleSingleDelete(id)) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.$emit('delete', id)\n\t\t},\n\n\t\tasync onSingleCopyConfirm(payload) {\n\t\t\t// Same contract as delete: the source owns the copy, or there is\n\t\t\t// no Copy action to confirm.\n\t\t\tif (this.isNamedSource && this.namedSource && typeof this.namedSource.copyRow === 'function') {\n\t\t\t\ttry {\n\t\t\t\t\tawait this.namedSource.copyRow(\n\t\t\t\t\t\tthis.actionTargetItem || { id: payload && payload.id },\n\t\t\t\t\t\tpayload && payload.newName,\n\t\t\t\t\t\tthis.sourceConfig || {},\n\t\t\t\t\t)\n\t\t\t\t\tthis.setSingleCopyResult({ success: true })\n\t\t\t\t\tthis.$emit('copy', payload)\n\t\t\t\t} catch (err) {\n\t\t\t\t\tthis.setSingleCopyResult({ error: (err && err.message) || 'Copy failed' })\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (await this.selfActions.handleSingleCopy(payload)) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.$emit('copy', payload)\n\t\t},\n\n\t\t// Lets an `advanceOn: \"object-created\"` walkthrough step auto-advance.\n\t\t// No dispatch site existed anywhere before this, so that advanceOn\n\t\t// type could never fire. `@self.register`/`@self.schema` are numeric\n\t\t// DB ids, not the slugs a manifest's advanceOn declares, so override\n\t\t// them with this page's own slug props before dispatching.\n\t\tnotifyWalkthroughObjectCreated(created) {\n\t\t\tif (typeof window === 'undefined' || !created) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst detail = { ...created, register: this.register, schema: this.exportSchemaSlug }\n\t\t\twindow.dispatchEvent(new CustomEvent('cn-walkthrough:object-created', { detail }))\n\t\t},\n\n\t\t/**\n\t\t * Toast and navigate after a create the built-in dialog confirmed, per\n\t\t * `createSuccessMessage` / `createSuccessRoute`. Both default off, so a\n\t\t * page that declares neither behaves exactly as before.\n\t\t *\n\t\t * Runs on every create path (store, self-store, `createOverride`), which\n\t\t * is what makes the built-in Add button a peer of a manifest\n\t\t * `open-form` header action: that action has always toasted and\n\t\t * navigated, so the same create reached two different endings depending\n\t\t * on which button opened it.\n\t\t *\n\t\t * @param {object} saved The created object, as the save path returned it.\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync afterCreateSuccess(saved) {\n\t\t\t// Callers do not await this — a toast and a navigation are not the\n\t\t\t// save — so nothing here may reject: an unhandled rejection from a\n\t\t\t// failed chunk load would surface as an error on a create that\n\t\t\t// actually succeeded.\n\t\t\tif (this.createSuccessMessage) {\n\t\t\t\ttry {\n\t\t\t\t\tconst { showSuccess } = await import('@nextcloud/dialogs')\n\t\t\t\t\tif (typeof showSuccess === 'function') {\n\t\t\t\t\t\tshowSuccess(this.cnTranslate(this.createSuccessMessage))\n\t\t\t\t\t}\n\t\t\t\t} catch {\n\t\t\t\t\t// No toast; the record is saved either way.\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (!this.createSuccessRoute) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\ttry {\n\t\t\t\t// `buildOnSuccessRoute` reads the id through `savedObjectId`, so a\n\t\t\t\t// response that carries it as `uuid` or `@self.id` still deep-links.\n\t\t\t\tconst location = buildOnSuccessRoute(this.createSuccessRoute, saved)\n\t\t\t\tif (location && this.$router) {\n\t\t\t\t\tthis.$router.push(location).catch(() => {})\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// The record is saved; staying on the list beats an error toast.\n\t\t\t}\n\t\t},\n\n\t\tasync onFormConfirm(formData) {\n\t\t\t// Opt-in create-override hook: an app supplies a custom async create\n\t\t\t// handler (e.g. a contact-aware endpoint that fills a required FK)\n\t\t\t// that owns persistence instead of saveObject. Create-only; edits\n\t\t\t// always fall through to the normal store / self-store path.\n\t\t\tif (!this.editItem && typeof this.createOverride === 'function') {\n\t\t\t\ttry {\n\t\t\t\t\tconst created = await this.createOverride(formData, {\n\t\t\t\t\t\tregister: this.register,\n\t\t\t\t\t\tschema: this.schema,\n\t\t\t\t\t\tobjectType: this.objectType || this.selfObjectType,\n\t\t\t\t\t\teffectiveSchema: this.effectiveSchema,\n\t\t\t\t\t})\n\t\t\t\t\tif (created) {\n\t\t\t\t\t\tthis.setFormResult({ success: true })\n\t\t\t\t\t\t/**\n\t\t\t\t\t\t * @event create Emitted after a create is confirmed (store, self-store, or createOverride).\n\t\t\t\t\t\t * @type {object} The created object.\n\t\t\t\t\t\t */\n\t\t\t\t\t\tthis.$emit('create', created)\n\t\t\t\t\t\tthis.notifyWalkthroughObjectCreated(created)\n\t\t\t\t\t\tif (this.list && typeof this.list.refresh === 'function') {\n\t\t\t\t\t\t\tthis.list.refresh()\n\t\t\t\t\t\t}\n\t\t\t\t\t\tthis.afterCreateSuccess(created)\n\t\t\t\t\t} else {\n\t\t\t\t\t\tthis.setFormResult({ error: 'Save failed' })\n\t\t\t\t\t}\n\t\t\t\t} catch (err) {\n\t\t\t\t\tthis.setFormResult({ error: (err && err.message) || 'Save failed' })\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (this.store) {\n\t\t\t\tif (!this.objectType) {\n\t\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\t\tconsole.warn('[CnIndexPage] store prop is set but objectType is missing. Cannot save to store.')\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tconst saved = await this.store.saveObject(this.objectType, formData)\n\t\t\t\tif (saved) {\n\t\t\t\t\tconst wasCreate = !this.editItem\n\t\t\t\t\tthis.setFormResult({ success: true })\n\t\t\t\t\tthis.$emit(wasCreate ? 'create' : 'edit', saved)\n\t\t\t\t\tif (wasCreate) {\n\t\t\t\t\t\tthis.notifyWalkthroughObjectCreated(saved)\n\t\t\t\t\t\tthis.afterCreateSuccess(saved)\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tconst err = this.store.getError?.(this.objectType)\n\t\t\t\t\tif (err && err.isValidation) {\n\t\t\t\t\t\t// Keep the form visible so the user can fix the invalid data.\n\t\t\t\t\t\tthis.setFormValidationErrors(err.fields, err.message || 'Validation failed')\n\t\t\t\t\t} else {\n\t\t\t\t\t\tthis.setFormResult({ error: (err && err.message) || 'Save failed' })\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (await this.selfActions.handleFormSave(formData)) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.$emit(this.editItem ? 'edit' : 'create', formData)\n\t\t},\n\n\t\tcloseSingleDelete() {\n\t\t\tthis.showSingleDeleteDialog = false\n\t\t\tthis.actionTargetItem = null\n\t\t},\n\n\t\tcloseSingleCopy() {\n\t\t\tthis.showSingleCopyDialog = false\n\t\t\tthis.actionTargetItem = null\n\t\t},\n\n\t\tcloseFormDialog() {\n\t\t\tthis.showFormDialogVisible = false\n\t\t\tthis.editItem = null\n\t\t},\n\n\t\t/**\n\t\t * @param {{ success?: boolean, error?: string }} resultData Result data to pass to the dialog\n\t\t * @public\n\t\t */\n\t\tsetSingleDeleteResult(resultData) {\n\t\t\tthis._setResult('singleDeleteDialog', resultData)\n\t\t},\n\n\t\t/**\n\t\t * @param {{ success?: boolean, error?: string }} resultData Result data to pass to the dialog\n\t\t * @public\n\t\t */\n\t\tsetSingleCopyResult(resultData) {\n\t\t\tthis._setResult('singleCopyDialog', resultData)\n\t\t},\n\n\t\t/**\n\t\t * @param {{ success?: boolean, error?: string }} resultData Result data to pass to the dialog\n\t\t * @public\n\t\t */\n\t\tsetFormResult(resultData) {\n\t\t\tthis._setResult('formDialog', resultData)\n\t\t},\n\n\t\t/**\n\t\t * Show a validation error in the form dialog while keeping the form\n\t\t * visible (so the user can fix the data), instead of replacing it with\n\t\t * a result note. Use this for 400/422 responses; use setFormResult for\n\t\t * terminal success/failure.\n\t\t *\n\t\t * @param {object} [fieldErrors] Per-field error messages keyed by field key\n\t\t * @param {string} [message] Form-level message shown above the fields\n\t\t * @public\n\t\t */\n\t\tsetFormValidationErrors(fieldErrors, message) {\n\t\t\tthis.$refs.formDialog?.setValidationErrors(fieldErrors || {}, message)\n\t\t},\n\n\t\t// --- Context menu handlers ---\n\n\t\tonRowContextMenu({ row, event }) {\n\t\t\tthis.openContextMenu({ item: row, event })\n\t\t},\n\n\t\t/**\n\t\t * Programmatically open the form dialog.\n\t\t *\n\t\t * @param {object|null} item Pass null for create mode, or an object for edit mode\n\t\t * @public\n\t\t */\n\t\topenFormDialog(item = null) {\n\t\t\tthis.editItem = item\n\t\t\tthis.showFormDialogVisible = true\n\t\t},\n\n\t\t/**\n\t\t * Open the `createModal` registry modal, when one is set and a CnAppRoot\n\t\t * ancestor can mount it.\n\t\t *\n\t\t * @return {boolean} Whether the modal was opened.\n\t\t */\n\t\topenCreateModal() {\n\t\t\tif (!this.createModal || typeof this.cnOpenModal !== 'function') {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tthis.cnOpenModal(this.createModal)\n\t\t\treturn true\n\t\t},\n\n\t\t/**\n\t\t * Programmatically open the single-item delete dialog.\n\t\t *\n\t\t * @param {object} item The item to delete\n\t\t * @public\n\t\t */\n\t\topenDeleteDialog(item) {\n\t\t\tthis.actionTargetItem = item\n\t\t\tthis.showSingleDeleteDialog = true\n\t\t},\n\n\t\t/**\n\t\t * Pure helper used by the cardComponent dispatch path to toggle\n\t\t * an id in the selected-ids array. Kept inline rather than\n\t\t * pulled into a util because the only call site is the\n\t\t * cardComponent `@select` listener template above.\n\t\t *\n\t\t * @param {Array} ids Current selection\n\t\t * @param {string|number} id The id to toggle\n\t\t * @return {Array} New array with `id` toggled in/out\n\t\t */\n\t\ttoggleIdInArray(ids, id) {\n\t\t\tif (ids.includes(id)) {\n\t\t\t\treturn ids.filter((existing) => existing !== id)\n\t\t\t}\n\t\t\treturn [...ids, id]\n\t\t},\n\t},\n}\n</script>\n\n<!-- Styles in css/index-page.css -->\n"],"names":["_createElementBlock","_renderSlot","_createVNode","_openBlock","_createBlock","_createTextVNode","_createElementVNode","_Fragment","_renderList","_toDisplayString","_withCtx","_normalizeClass","_resolveDynamicComponent","_normalizeStyle","_mergeProps"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"CnIndexPage.vue3.js","sources":["../../../../src/components/CnIndexPage/CnIndexPage.vue?vue&type=template&id=9af0d906&lang.js"],"sourcesContent":["<template>\n\t<div class=\"cn-index-page\"\n\t\tdata-testid=\"cn-index-page\"\n\t\t@keydown=\"onListKeydown\">\n\t\t<!-- Header — overridable via #header slot. CnPageHeader ALWAYS renders:\n\t\t with showTitle it is the full visual header (icon, title,\n\t\t description); without it, `visuallyHidden` clips everything but the\n\t\t <h1> out of the layout. The page therefore looks identical either\n\t\t way, but the <main> landmark always carries a heading — the sidebar\n\t\t title this component used to rely on lives outside <main>, so\n\t\t screen-reader users and \"skip to main content\" got an unlabelled\n\t\t region (WCAG 2.4.6 / 1.3.1). -->\n\t\t<slot\n\t\t\tname=\"header\"\n\t\t\t:title=\"title\"\n\t\t\t:description=\"description\"\n\t\t\t:icon=\"resolvedIcon\"\n\t\t\t:showTitle=\"showTitle\">\n\t\t\t<CnPageHeader\n\t\t\t\t:title=\"title\"\n\t\t\t\t:description=\"description\"\n\t\t\t\t:icon=\"resolvedIcon\"\n\t\t\t\t:visuallyHidden=\"!showTitle\" />\n\t\t</slot>\n\n\t\t<!-- Optional content below header, above actions bar -->\n\t\t<div v-if=\"$slots['below-header']\" class=\"cn-index-page__below-header\">\n\t\t\t<slot name=\"below-header\" />\n\t\t</div>\n\n\t\t<!-- Actions bar -->\n\t\t<CnActionsBar\n\t\t\t:pagination=\"effectivePagination\"\n\t\t\t:objectCount=\"effectiveObjects.length\"\n\t\t\t:selectable=\"selectable\"\n\t\t\t:selectedIds=\"internalSelectedIds\"\n\t\t\t:addLabel=\"resolvedAddLabel\"\n\t\t\t:addIcon=\"resolvedIcon\"\n\t\t\t:inlineActionCount=\"inlineActionCount\"\n\t\t\t:showMassImport=\"showMassImport\"\n\t\t\t:showMassExport=\"showMassExport\"\n\t\t\t:showMassCopy=\"showMassCopy\"\n\t\t\t:showMassDelete=\"showMassDelete\"\n\t\t\t:viewMode=\"currentViewMode\"\n\t\t\t:showViewToggle=\"showViewToggle\"\n\t\t\t:availableViewModes=\"effectiveToggleModes\"\n\t\t\t:cardsLabel=\"cardsLabel\"\n\t\t\t:tableLabel=\"tableLabel\"\n\t\t\t:listLabel=\"listLabel\"\n\t\t\t:cardsIcon=\"cardsIcon\"\n\t\t\t:tableIcon=\"tableIcon\"\n\t\t\t:showMap=\"showMapSegment\"\n\t\t\t:mapLabel=\"mapLabel\"\n\t\t\t:mapIcon=\"mapIcon\"\n\t\t\t:listIcon=\"listIcon\"\n\t\t\t:showSortSelect=\"showSortSelect\"\n\t\t\t:sortOptions=\"sortSelectOptions\"\n\t\t\t:sortValue=\"sortSelectValue\"\n\t\t\t:showSearch=\"inlineSearch\"\n\t\t\t:searchValue=\"effectiveSearchValue\"\n\t\t\t:searchPlaceholder=\"searchPlaceholder\"\n\t\t\t:refreshing=\"effectiveRefreshing\"\n\t\t\t:refreshDisabled=\"refreshDisabled\"\n\t\t\t:addDisabled=\"addDisabled\"\n\t\t\t:addTo=\"addLinkTo\"\n\t\t\t:showAdd=\"effectiveShowAdd\"\n\t\t\t:showSidebarToggle=\"hasSidebar\"\n\t\t\t:sidebarOpen=\"sidebarOpen\"\n\t\t\t:headerActions=\"mergedHeaderActions\"\n\t\t\t:bulkActions=\"mergedBulkActions\"\n\t\t\t:documentationUrl=\"documentationUrl\"\n\t\t\t:documentationLabel=\"documentationLabel || undefined\"\n\t\t\t@sortChange=\"$emit('sort-change', $event)\"\n\t\t\t@add=\"onAddClick\"\n\t\t\t@clearSelection=\"onSelect([])\"\n\t\t\t@toggleSidebar=\"sidebarOpen = !sidebarOpen\"\n\t\t\t@refresh=\"onRefreshEvent\"\n\t\t\t@headerAction=\"onHeaderAction\"\n\t\t\t@bulkAction=\"onBulkAction\"\n\t\t\t@showImport=\"showImportDialog = true\"\n\t\t\t@showExport=\"showExportDialog = true\"\n\t\t\t@showCopy=\"showMassCopyDialog = true\"\n\t\t\t@showDelete=\"showMassDeleteDialog = true\"\n\t\t\t@search=\"onSearchEvent\"\n\t\t\t@viewModeChange=\"onViewModeChange\">\n\t\t\t<template v-if=\"$slots['mass-actions']\" #mass-actions=\"{ count, selectedIds: ids }\">\n\t\t\t\t<slot name=\"mass-actions\" :count=\"count\" :selectedIds=\"ids\" />\n\t\t\t</template>\n\t\t\t<template v-if=\"$slots['action-items']\" #action-items>\n\t\t\t\t<slot name=\"action-items\" />\n\t\t\t</template>\n\t\t\t<template v-if=\"$slots['after-search']\" #after-search>\n\t\t\t\t<slot name=\"after-search\" />\n\t\t\t</template>\n\t\t\t<template\n\t\t\t\tv-if=\"$slots['selection-actions']\"\n\t\t\t\t#selection-actions=\"{ count, selectedIds: ids }\">\n\t\t\t\t<slot name=\"selection-actions\" :count=\"count\" :selectedIds=\"ids\" />\n\t\t\t</template>\n\t\t\t<template v-if=\"$slots['header-actions'] || $slots['actions']\" #actions>\n\t\t\t\t<!--\n\t\t\t\t\t@slot header-actions\n\t\t\t\t\t@description Extra buttons in the page header, inline next to the Add button. Documented since the beginning but wired up only later — consumers passing it (hermiq's flow list's \"New flow\" button) rendered nothing while the page looked fine.\n\t\t\t\t-->\n\t\t\t\t<slot name=\"header-actions\" />\n\t\t\t\t<slot name=\"actions\" />\n\t\t\t</template>\n\t\t\t<!-- Rendered AFTER the primary Add button (CnActionsBar's `actions-end`\n\t\t\t slot), not before it: these are \"browse/manage\" controls — saved\n\t\t\t views, export, page config — grouped together and kept apart from\n\t\t\t the app-specific buttons in `#actions`, with Add sitting between\n\t\t\t the two groups rather than before both (dossiq Cases/Queue). -->\n\t\t\t<template v-if=\"isEditMode || showExportMenu || allowSavedViews\" #actions-end>\n\t\t\t\t<!-- Saved views (opt-in via `allowSavedViews`): lists the user's\n\t\t\t\t OpenRegister saved-search views; applying one writes its stored\n\t\t\t\t filters/search/sort into the route query. -->\n\t\t\t\t<CnSavedViewsControl\n\t\t\t\t\tv-if=\"allowSavedViews\"\n\t\t\t\t\t:views=\"viewsForControl\"\n\t\t\t\t\t:loading=\"savedViewsLoading\"\n\t\t\t\t\t:currentUserId=\"currentSavedViewsUserId\"\n\t\t\t\t\t@apply=\"onApplySavedView\"\n\t\t\t\t\t@saveRequest=\"showSaveViewDialog = true\"\n\t\t\t\t\t@deleteRequest=\"onDeleteViewRequest\" />\n\t\t\t\t<!-- Native Export menu (opt-in via `allowExport` + schema.exportable):\n\t\t\t\t CSV/Excel entries navigate to OR's export-leaf URL, passing the\n\t\t\t\t current route's query params through as filters. -->\n\t\t\t\t<NcActions\n\t\t\t\t\tv-if=\"showExportMenu\"\n\t\t\t\t\t:forceName=\"true\"\n\t\t\t\t\t:menuName=\"t('nextcloud-vue', 'Export')\"\n\t\t\t\t\tdata-testid=\"cn-index-export-menu\"\n\t\t\t\t\t:aria-label=\"t('nextcloud-vue', 'Export')\">\n\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t<Export :size=\"20\" />\n\t\t\t\t\t</template>\n\t\t\t\t\t<NcActionButton data-testid=\"cn-index-export-csv\" @click=\"onExportClick('csv')\">\n\t\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t\t<Export :size=\"20\" />\n\t\t\t\t\t\t</template>\n\t\t\t\t\t\t{{ t('nextcloud-vue', 'Export as CSV') }}\n\t\t\t\t\t</NcActionButton>\n\t\t\t\t\t<NcActionButton data-testid=\"cn-index-export-excel\" @click=\"onExportClick('excel')\">\n\t\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t\t<Export :size=\"20\" />\n\t\t\t\t\t\t</template>\n\t\t\t\t\t\t{{ t('nextcloud-vue', 'Export as Excel') }}\n\t\t\t\t\t</NcActionButton>\n\t\t\t\t</NcActions>\n\t\t\t\t<!-- Edit-mode config cog: opens the page's full config editor\n\t\t\t\t (CnPageRenderer wires @configure to CnPageConfigModal). -->\n\t\t\t\t<NcButton v-if=\"isEditMode\"\n\t\t\t\t\tvariant=\"tertiary\"\n\t\t\t\t\t:aria-label=\"t('nextcloud-vue', 'Configure page')\"\n\t\t\t\t\t@click=\"$emit('configure')\">\n\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t<Cog :size=\"20\" />\n\t\t\t\t\t</template>\n\t\t\t\t</NcButton>\n\t\t\t</template>\n\t\t\t<!-- Quick-filter tabs (REQ-MIPFU-1) rendered INSIDE the action bar\n\t\t\t (between the view toggle and the actions) when the manifest\n\t\t\t declares `config.quickFilters`. Switching tabs re-fetches with\n\t\t\t the merged filter; @event quick-filter-change. -->\n\t\t\t<template v-if=\"tabStripEntries && tabStripEntries.length > 0\" #filters>\n\t\t\t\t<CnQuickFilterBar\n\t\t\t\t\tinline\n\t\t\t\t\t:tabs=\"tabStripEntries\"\n\t\t\t\t\t:mode=\"quickFilterMode\"\n\t\t\t\t\t:maxVisible=\"quickFilterMaxVisible\"\n\t\t\t\t\t:multiple=\"quickFilterMultiple\"\n\t\t\t\t\t:activeIndex=\"activeQuickFilterIndex\"\n\t\t\t\t\t:selectedIndices=\"selectedQuickFilterIndices\"\n\t\t\t\t\t@update:activeIndex=\"onQuickFilterChange\"\n\t\t\t\t\t@update:selectedIndices=\"onQuickFilterMultiChange\" />\n\t\t\t</template>\n\t\t</CnActionsBar>\n\n\t\t<!-- Quick edit: a few fields of one row, over the list, with the list\n\t\t keeping its place. -->\n\t\t<CnQuickEditDialog\n\t\t\tv-if=\"quickEditRow !== null\"\n\t\t\t:object=\"quickEditRow\"\n\t\t\t:schema=\"effectiveSchema\"\n\t\t\t:register=\"register\"\n\t\t\t:fields=\"quickEditFields\"\n\t\t\t:writableField=\"writableField\"\n\t\t\t:serverObject=\"quickEditServerRow\"\n\t\t\t@save=\"onQuickEditSave\"\n\t\t\t@keepTheirs=\"onQuickEditKeepTheirs\"\n\t\t\t@close=\"quickEditRow = null\" />\n\n\t\t<!-- The help key's sheet. Every shortcut the list offers is here and in\n\t\t the command palette: one that only the handler knows about is one\n\t\t nobody can find. -->\n\t\t<CnConfirmDialog\n\t\t\tv-if=\"showShortcutHelp\"\n\t\t\t:name=\"shortcutHelpTitle\"\n\t\t\t:confirmLabel=\"closeLabel\"\n\t\t\tdata-testid=\"cn-list-shortcuts-help\"\n\t\t\t@confirm=\"showShortcutHelp = false\"\n\t\t\t@cancel=\"showShortcutHelp = false\">\n\t\t\t<dl class=\"cn-index-page__shortcuts\">\n\t\t\t\t<template v-for=\"entry in shortcutHelpEntries\" :key=\"entry.id\">\n\t\t\t\t\t<dt><kbd>{{ entry.keys }}</kbd></dt>\n\t\t\t\t\t<dd>{{ entry.description }}</dd>\n\t\t\t\t</template>\n\t\t\t</dl>\n\t\t</CnConfirmDialog>\n\n\t\t<!-- Mass delete dialog -->\n\t\t<CnMassDeleteDialog\n\t\t\tv-if=\"showMassDeleteDialog\"\n\t\t\tref=\"massDeleteDialog\"\n\t\t\t:items=\"selectedObjects\"\n\t\t\t:nameField=\"massActionNameField\"\n\t\t\t:nameFormatter=\"nameFormatter\"\n\t\t\t@confirm=\"onMassDeleteConfirm\"\n\t\t\t@close=\"showMassDeleteDialog = false\" />\n\n\t\t<!-- Mass copy dialog -->\n\t\t<CnMassCopyDialog\n\t\t\tv-if=\"showMassCopyDialog\"\n\t\t\tref=\"massCopyDialog\"\n\t\t\t:items=\"selectedObjects\"\n\t\t\t:nameField=\"massActionNameField\"\n\t\t\t:nameFormatter=\"nameFormatter\"\n\t\t\t@confirm=\"onMassCopyConfirm\"\n\t\t\t@close=\"showMassCopyDialog = false\" />\n\n\t\t<!-- Mass export dialog -->\n\t\t<CnMassExportDialog\n\t\t\tv-if=\"showExportDialog\"\n\t\t\tref=\"exportDialog\"\n\t\t\t:formats=\"exportFormats\"\n\t\t\t@confirm=\"onMassExportConfirm\"\n\t\t\t@close=\"showExportDialog = false\" />\n\n\t\t<!-- Mass import dialog -->\n\t\t<CnMassImportDialog\n\t\t\tv-if=\"showImportDialog\"\n\t\t\tref=\"importDialog\"\n\t\t\t:options=\"importOptions\"\n\t\t\t@confirm=\"onMassImportConfirm\"\n\t\t\t@close=\"showImportDialog = false\">\n\t\t\t<template v-if=\"$slots['import-fields']\" #fields=\"{ file }\">\n\t\t\t\t<slot name=\"import-fields\" :file=\"file\" />\n\t\t\t</template>\n\t\t</CnMassImportDialog>\n\n\t\t<!-- Save-current-view dialog (saved-views-ui) -->\n\t\t<CnSaveViewDialog\n\t\t\tv-if=\"showSaveViewDialog\"\n\t\t\tref=\"saveViewDialog\"\n\t\t\t@confirm=\"onSaveViewConfirm\"\n\t\t\t@close=\"showSaveViewDialog = false\" />\n\n\t\t<!-- Delete-saved-view confirm (saved-views-ui) -->\n\t\t<CnConfirmDialog\n\t\t\tv-if=\"viewPendingDelete\"\n\t\t\tref=\"deleteViewConfirmDialog\"\n\t\t\tvariant=\"error\"\n\t\t\t:dialogTitle=\"t('nextcloud-vue', 'Delete view')\"\n\t\t\t:message=\"deleteViewMessage\"\n\t\t\t:confirmLabel=\"t('nextcloud-vue', 'Delete')\"\n\t\t\t@confirm=\"onDeleteViewConfirm\"\n\t\t\t@close=\"viewPendingDelete = null\" />\n\n\t\t<!-- @slot delete-dialog Replace the single-item delete dialog. -->\n\t\t<!-- @binding {boolean} show Whether the delete dialog is currently visible. -->\n\t\t<!-- @binding {object} item The item targeted for deletion. -->\n\t\t<!-- @binding {Function} confirm Performs the delete — the same path the default dialog's `@confirm` runs. -->\n\t\t<!-- @binding {Function} close Closes the delete dialog. -->\n\t\t<!-- `show` and `confirm` are props for the same reason as `form-dialog`\n\t\t above: a manifest-mounted replacement receives props only. -->\n\t\t<slot\n\t\t\tname=\"delete-dialog\"\n\t\t\t:show=\"showSingleDeleteDialog\"\n\t\t\t:item=\"actionTargetItem\"\n\t\t\t:confirm=\"onSingleDeleteConfirm\"\n\t\t\t:close=\"closeSingleDelete\">\n\t\t\t<CnDeleteDialog\n\t\t\t\tv-if=\"showSingleDeleteDialog && actionTargetItem\"\n\t\t\t\tref=\"singleDeleteDialog\"\n\t\t\t\t:item=\"actionTargetItem\"\n\t\t\t\t:nameField=\"massActionNameField\"\n\t\t\t\t:nameFormatter=\"nameFormatter\"\n\t\t\t\t@confirm=\"onSingleDeleteConfirm\"\n\t\t\t\t@close=\"closeSingleDelete\" />\n\t\t</slot>\n\n\t\t<!-- @slot copy-dialog Replace the single-item copy dialog. -->\n\t\t<!-- @binding {boolean} show Whether the copy dialog is currently visible. -->\n\t\t<!-- @binding {object} item The item targeted for copy. -->\n\t\t<!-- @binding {Function} confirm Performs the copy — the same path the default dialog's `@confirm` runs. -->\n\t\t<!-- @binding {Function} close Closes the copy dialog. -->\n\t\t<!-- `show` and `confirm` are props for the same reason as `form-dialog`\n\t\t above: a manifest-mounted replacement receives props only. -->\n\t\t<slot\n\t\t\tname=\"copy-dialog\"\n\t\t\t:show=\"showSingleCopyDialog\"\n\t\t\t:item=\"actionTargetItem\"\n\t\t\t:confirm=\"onSingleCopyConfirm\"\n\t\t\t:close=\"closeSingleCopy\">\n\t\t\t<CnCopyDialog\n\t\t\t\tv-if=\"showSingleCopyDialog && actionTargetItem\"\n\t\t\t\tref=\"singleCopyDialog\"\n\t\t\t\t:item=\"actionTargetItem\"\n\t\t\t\t:nameField=\"massActionNameField\"\n\t\t\t\t:nameFormatter=\"nameFormatter\"\n\t\t\t\t@confirm=\"onSingleCopyConfirm\"\n\t\t\t\t@close=\"closeSingleCopy\" />\n\t\t</slot>\n\n\t\t<!-- @slot form-dialog Replace the create/edit form dialog (use CnFormDialog or CnAdvancedFormDialog). -->\n\t\t<!-- @binding {boolean} show Whether the form dialog is currently visible. -->\n\t\t<!-- @binding {?object} item The item being edited, or null in create mode. -->\n\t\t<!-- @binding {object} schema The effective JSON schema driving the form. -->\n\t\t<!-- @binding {Function} confirm Persists the form data through the page's own save path (store / self-store / createOverride) and emits `create`/`edit`. Call this instead of saving in the replacement dialog, so a create or edit made there behaves exactly like one made in the built-in dialog. Takes the complete object to save. List refresh is automatic on the self-fetch and `createOverride` paths; with the `store` prop, refresh is driven by the consumer's `create`/`edit` handler as usual. -->\n\t\t<!-- @binding {Function} close Closes the form dialog. -->\n\t\t<!-- @binding {Function} refresh Re-reads the list. For a replacement dialog that saves through its own endpoint rather than `confirm`, so the list still shows what it saved. -->\n\t\t<!--\n\t\t `confirm` is bound as a PROP, not left as an `@confirm` listener on\n\t\t the default child. A manifest-declared replacement is mounted by\n\t\t CnPageRenderer as `<component :is=… v-bind=\"slotProps\" />`, which\n\t\t binds props only — so a listener is unreachable from a manifest\n\t\t even in principle, and a replacement dialog could render and close\n\t\t but never save (openconnector#1150).\n\t\t-->\n\t\t<slot\n\t\t\tname=\"form-dialog\"\n\t\t\t:show=\"showFormDialogVisible\"\n\t\t\t:item=\"editItem\"\n\t\t\t:schema=\"effectiveSchema\"\n\t\t\t:confirm=\"onFormConfirm\"\n\t\t\t:close=\"closeFormDialog\"\n\t\t\t:refresh=\"onRefreshEvent\">\n\t\t\t<CnFormDialog\n\t\t\t\tv-if=\"showFormDialogVisible && !useAdvancedFormDialog\"\n\t\t\t\tref=\"formDialog\"\n\t\t\t\t:schema=\"effectiveSchema\"\n\t\t\t\t:item=\"editItem\"\n\t\t\t\t:register=\"register\"\n\t\t\t\t:excludeFields=\"excludeFields\"\n\t\t\t\t:includeFields=\"includeFields\"\n\t\t\t\t:fieldOverrides=\"fieldOverrides\"\n\t\t\t\t:nameField=\"massActionNameField\"\n\t\t\t\t:size=\"formSize\"\n\t\t\t\t:columns=\"formColumns\"\n\t\t\t\t:initialData=\"resolvedCreateDefaults\"\n\t\t\t\t@confirm=\"onFormConfirm\"\n\t\t\t\t@close=\"closeFormDialog\">\n\t\t\t\t<template v-if=\"$slots['form-fields']\" #form=\"scope\">\n\t\t\t\t\t<slot name=\"form-fields\" v-bind=\"scope\" />\n\t\t\t\t</template>\n\t\t\t</CnFormDialog>\n\t\t\t<CnAdvancedFormDialog\n\t\t\t\tv-if=\"showFormDialogVisible && useAdvancedFormDialog\"\n\t\t\t\tref=\"formDialog\"\n\t\t\t\t:schema=\"effectiveSchema\"\n\t\t\t\t:item=\"editItem\"\n\t\t\t\t:excludeFields=\"excludeFields\"\n\t\t\t\t:includeFields=\"includeFields\"\n\t\t\t\t:fieldOverrides=\"fieldOverrides\"\n\t\t\t\t:nameField=\"massActionNameField\"\n\t\t\t\t:initialValues=\"resolvedCreateDefaults\"\n\t\t\t\t@confirm=\"onFormConfirm\"\n\t\t\t\t@close=\"closeFormDialog\" />\n\t\t</slot>\n\n\t\t<!-- Body -->\n\t\t<div class=\"cn-index-page__body\" :class=\"{ 'cn-index-page__body--with-folders': folderSidebar }\">\n\t\t\t<!-- Optional folder navigation pane (opt-in via the `folderSidebar`\n\t\t\t config). Selecting a folder filters the list by the config's\n\t\t\t `filterField`; \"All\" clears it. -->\n\t\t\t<div v-if=\"folderSidebar\" class=\"cn-index-page__folder-pane\">\n\t\t\t\t<CnFolderSidebar\n\t\t\t\t\t:source=\"folderSidebarSource\"\n\t\t\t\t\t:folders=\"folderSidebarFolders\"\n\t\t\t\t\t:objects=\"effectiveObjects\"\n\t\t\t\t\t:groupBy=\"folderSidebar.groupBy || folderSidebar.field || ''\"\n\t\t\t\t\t:facetValues=\"folderSidebarFacetValues\"\n\t\t\t\t\t:partial=\"folderSidebarPartial\"\n\t\t\t\t\t:filesPath=\"folderSidebar.filesPath || '/'\"\n\t\t\t\t\t:selectedId=\"selectedFolderId\"\n\t\t\t\t\t:allLabel=\"folderSidebar.allLabel || undefined\"\n\t\t\t\t\t:title=\"folderSidebar.title || ''\"\n\t\t\t\t\t:idField=\"folderPassthroughIdField\"\n\t\t\t\t\t:nameField=\"folderPassthroughNameField\"\n\t\t\t\t\t:allowCreate=\"Boolean(folderSidebar.allowCreate)\"\n\t\t\t\t\t@select=\"onFolderSelect\"\n\t\t\t\t\t@create=\"$emit('folder-create', $event)\" />\n\t\t\t</div>\n\n\t\t\t<!-- The list. `v-show`, never `v-if`: the narrow layout hides it\n\t\t\t but must not unmount it, because unmounting throws away the\n\t\t\t scroll position, the selection and the loaded page, which is\n\t\t\t the whole thing the split view exists to keep. -->\n\t\t\t<div\n\t\t\t\tv-show=\"splitLayout !== 'detail'\"\n\t\t\t\tref=\"listScroll\"\n\t\t\t\tclass=\"cn-index-page__main\"\n\t\t\t\t:class=\"{\n\t\t\t\t\t'cn-index-page__main--map': currentViewMode === 'map',\n\t\t\t\t\t'cn-index-page__main--table': currentViewMode === 'table',\n\t\t\t\t\t'cn-index-page__main--split': splitLayout === 'split',\n\t\t\t\t}\"\n\t\t\t\t@scroll=\"onListScroll\">\n\t\t\t\t<!-- @slot before-collection Content rendered above the collection in EVERY view mode (table, cards, list, map) — e.g. a folder/group strip that must stay visually separate from the objects instead of masquerading as rows or cards. -->\n\t\t\t\t<slot name=\"before-collection\" />\n\n\t\t\t\t<!-- Loading state — initial fetch only; a background refresh keeps the table visible -->\n\t\t\t\t<div v-if=\"showInitialLoader\" class=\"cn-index-page__loading\">\n\t\t\t\t\t<!-- name gives NcLoadingIcon a non-empty aria-label (WCAG role-img-alt); empty name ships an unlabeled role=\"img\" -->\n\t\t\t\t\t<NcLoadingIcon :size=\"32\" :name=\"loadingText\" />\n\t\t\t\t</div>\n\n\t\t\t\t<!-- Empty state -->\n\t\t\t\t<div v-else-if=\"effectiveObjects.length === 0\" class=\"cn-index-page__empty\">\n\t\t\t\t\t<slot name=\"empty\">\n\t\t\t\t\t\t<NcEmptyContent :name=\"resolvedEmptyText\">\n\t\t\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t\t\t<CnIcon v-if=\"resolvedIcon\" :name=\"resolvedIcon\" :size=\"64\" />\n\t\t\t\t\t\t\t\t<DatabaseSearch v-else :size=\"64\" />\n\t\t\t\t\t\t\t</template>\n\t\t\t\t\t\t</NcEmptyContent>\n\t\t\t\t\t</slot>\n\t\t\t\t</div>\n\n\t\t\t\t<!-- Table view -->\n\t\t\t\t<CnDataTable\n\t\t\t\t\tv-else-if=\"currentViewMode === 'table'\"\n\t\t\t\t\t:schema=\"effectiveSchema\"\n\t\t\t\t\t:columns=\"tableColumns\"\n\t\t\t\t\t:rowIcon=\"rowIcon\"\n\t\t\t\t\t:rowIndicators=\"rowIndicators\"\n\t\t\t\t\t:rowIndicatorCap=\"rowIndicatorCap\"\n\t\t\t\t\t:rows=\"displayObjects\"\n\t\t\t\t\t:sortKey=\"effectiveSortKey\"\n\t\t\t\t\t:sortOrder=\"effectiveSortOrder\"\n\t\t\t\t\t:sortKeys=\"effectiveSortKeys\"\n\t\t\t\t\t:selectable=\"selectable\"\n\t\t\t\t\t:rowClickToView=\"rowClickOpens\"\n\t\t\t\t\t:selectedIds=\"internalSelectedIds\"\n\t\t\t\t\t:rowKey=\"rowKey\"\n\t\t\t\t\t:emptyText=\"emptyText\"\n\t\t\t\t\t:excludeColumns=\"excludeColumns\"\n\t\t\t\t\t:includeColumns=\"includeColumns\"\n\t\t\t\t\t:columnOverrides=\"columnOverrides\"\n\t\t\t\t\t:rowClass=\"rowClass\"\n\t\t\t\t\t@sort=\"onSortEvent\"\n\t\t\t\t\t@select=\"onSelect\"\n\t\t\t\t\t@rowClick=\"onRowClick\"\n\t\t\t\t\t@rowAuxClick=\"onRowAuxClick\"\n\t\t\t\t\t@rowContextMenu=\"onRowContextMenu\">\n\t\t\t\t\t<!-- Pass through column slots -->\n\t\t\t\t\t<template\n\t\t\t\t\t\tv-for=\"col in slotColumns\"\n\t\t\t\t\t\t#[`column-${col}`]=\"{ row, value }\">\n\t\t\t\t\t\t<slot :name=\"'column-' + col\" :row=\"row\" :value=\"value\" />\n\t\t\t\t\t</template>\n\n\t\t\t\t\t<!-- Row actions -->\n\t\t\t\t\t<template v-if=\"hasRowActions\" #row-actions=\"{ row }\">\n\t\t\t\t\t\t<slot name=\"row-actions\" :row=\"row\">\n\t\t\t\t\t\t\t<CnRowActions\n\t\t\t\t\t\t\t\t:actions=\"rowActionsFor(row)\"\n\t\t\t\t\t\t\t\t:row=\"row\"\n\t\t\t\t\t\t\t\t@action=\"onRowAction\" />\n\t\t\t\t\t\t</slot>\n\t\t\t\t\t</template>\n\n\t\t\t\t\t<!-- Table-header filter + column menus (both opt-in): funnel and\n\t\t\t\t\t columns buttons above the row-actions column. The filter menu\n\t\t\t\t\t lists each enum column's values as toggleable facet filters;\n\t\t\t\t\t the column menu lists every governed column as a visibility\n\t\t\t\t\t checkbox — compact, in-table alternatives to the sidebar. -->\n\t\t\t\t\t<template\n\t\t\t\t\t\tv-if=\"(filterMenu && filterableFields.length) || (columnMenu && governedColumns.length)\"\n\t\t\t\t\t\t#actions-header>\n\t\t\t\t\t\t<NcActions\n\t\t\t\t\t\t\tv-if=\"filterMenu && filterableFields.length\"\n\t\t\t\t\t\t\t:forceMenu=\"true\"\n\t\t\t\t\t\t\t:aria-label=\"t('nextcloud-vue', 'Filter')\">\n\t\t\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t\t\t<FilterOutline :size=\"20\" />\n\t\t\t\t\t\t\t</template>\n\t\t\t\t\t\t\t<template v-for=\"field in filterableFields\" :key=\"field.key\">\n\t\t\t\t\t\t\t\t<NcActionCaption :name=\"field.label\" />\n\t\t\t\t\t\t\t\t<NcActionCheckbox\n\t\t\t\t\t\t\t\t\tv-for=\"val in field.values\"\n\t\t\t\t\t\t\t\t\t:key=\"`${field.key}-${val}`\"\n\t\t\t\t\t\t\t\t\t:modelValue=\"isFilterActive(field.key, val)\"\n\t\t\t\t\t\t\t\t\t@update:modelValue=\"toggleFilter(field.key, val)\">\n\t\t\t\t\t\t\t\t\t{{ val }}\n\t\t\t\t\t\t\t\t</NcActionCheckbox>\n\t\t\t\t\t\t\t</template>\n\t\t\t\t\t\t</NcActions>\n\t\t\t\t\t\t<NcActions\n\t\t\t\t\t\t\tv-if=\"columnMenu && governedColumns.length\"\n\t\t\t\t\t\t\t:forceMenu=\"true\"\n\t\t\t\t\t\t\t:aria-label=\"t('nextcloud-vue', 'Columns')\">\n\t\t\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t\t\t<ViewColumnOutline :size=\"20\" />\n\t\t\t\t\t\t\t</template>\n\t\t\t\t\t\t\t<NcActionCaption :name=\"t('nextcloud-vue', 'Columns')\" />\n\t\t\t\t\t\t\t<NcActionCheckbox\n\t\t\t\t\t\t\t\tv-for=\"col in governedColumns\"\n\t\t\t\t\t\t\t\t:key=\"`col-${col.key}`\"\n\t\t\t\t\t\t\t\t:modelValue=\"isColumnVisible(col.key)\"\n\t\t\t\t\t\t\t\t@update:modelValue=\"toggleColumn(col.key)\">\n\t\t\t\t\t\t\t\t{{ cnTranslate(col.label || col.key) }}\n\t\t\t\t\t\t\t</NcActionCheckbox>\n\t\t\t\t\t\t</NcActions>\n\t\t\t\t\t</template>\n\t\t\t\t</CnDataTable>\n\n\t\t\t\t<!-- Map view — plots the CURRENT filtered rows (displayObjects) as\n\t\t\t\t inline markers; no separate fetch path, so it reuses the same\n\t\t\t\t filter / sidebar / quick-filter machinery as table and cards.\n\t\t\t\t A marker click routes back through onRowClick for identical\n\t\t\t\t detail-page navigation. -->\n\t\t\t\t<CnMapWidget\n\t\t\t\t\tv-else-if=\"currentViewMode === 'map'\"\n\t\t\t\t\tclass=\"cn-index-page__map\"\n\t\t\t\t\t:center=\"mapCenter\"\n\t\t\t\t\t:layers=\"mapLayers\"\n\t\t\t\t\t:basemaps=\"mapBasemaps\"\n\t\t\t\t\t:markers=\"mapMarkers\"\n\t\t\t\t\t:clustering=\"mapClustering\"\n\t\t\t\t\t:autoFit=\"true\"\n\t\t\t\t\theight=\"100%\"\n\t\t\t\t\t@markerClick=\"onMarkerClick\" />\n\n\t\t\t\t<!--\n\t\t\t\t\tBoard view. The columns are the schema's stages and a move\n\t\t\t\t\tgoes through the host's transition; CnBoardView never\n\t\t\t\t\twrites the status field, so with no runTransition it is\n\t\t\t\t\tread-only rather than broken.\n\t\t\t\t-->\n\t\t\t\t<CnBoardView\n\t\t\t\t\tv-else-if=\"currentViewMode === 'board'\"\n\t\t\t\t\t:rows=\"displayObjects\"\n\t\t\t\t\t:statusFieldSchema=\"boardStatusFieldSchema\"\n\t\t\t\t\t:statusField=\"board.statusField || 'status'\"\n\t\t\t\t\t:cardFields=\"board.cardFields || []\"\n\t\t\t\t\t:swimlaneField=\"board.swimlaneField || ''\"\n\t\t\t\t\t:rowKey=\"rowKey\"\n\t\t\t\t\t:runTransition=\"runTransition\"\n\t\t\t\t\t:paged=\"isPaged\"\n\t\t\t\t\t@cardClick=\"onRowClick\"\n\t\t\t\t\t@cardAuxClick=\"onRowAuxClick\"\n\t\t\t\t\t@moved=\"onBoardMoved\" />\n\n\t\t\t\t<!--\n\t\t\t\t\tDate axis. Reads only: nothing here reschedules, because a\n\t\t\t\t\tview that moved a bar on drag would be changing statutory\n\t\t\t\t\tdates from a picture.\n\t\t\t\t-->\n\t\t\t\t<CnDateAxisView\n\t\t\t\t\tv-else-if=\"currentViewMode === 'dateAxis'\"\n\t\t\t\t\t:rows=\"displayObjects\"\n\t\t\t\t\t:startField=\"dateAxis.startField || ''\"\n\t\t\t\t\t:endField=\"dateAxis.endField || ''\"\n\t\t\t\t\t:laneField=\"dateAxis.laneField || ''\"\n\t\t\t\t\t:labelField=\"dateAxis.labelField || ''\"\n\t\t\t\t\t:rowKey=\"rowKey\"\n\t\t\t\t\t@rowClick=\"onRowClick\"\n\t\t\t\t\t@rowAuxClick=\"onRowAuxClick\" />\n\n\t\t\t\t<!-- List view -->\n\t\t\t\t<CnObjectList\n\t\t\t\t\tv-else-if=\"currentViewMode === 'list'\"\n\t\t\t\t\t:objects=\"displayObjects\"\n\t\t\t\t\t:schema=\"effectiveSchema\"\n\t\t\t\t\t:config=\"listConfig\"\n\t\t\t\t\t:selectable=\"selectable\"\n\t\t\t\t\t:selectedIds=\"internalSelectedIds\"\n\t\t\t\t\t:rowKey=\"rowKey\"\n\t\t\t\t\t:emptyText=\"emptyText\"\n\t\t\t\t\t@click=\"onRowClick\"\n\t\t\t\t\t@auxClick=\"onRowAuxClick\"\n\t\t\t\t\t@select=\"onSelect\">\n\t\t\t\t\t<!--\n\t\t\t\t\t\tList-item slot resolution priority (highest first):\n\t\t\t\t\t\t1. Parent-provided `#list-item` scoped slot — App.vue overrides win.\n\t\t\t\t\t\t2. `listComponent` prop (or manifest `pages[].config.listComponent`)\n\t\t\t\t\t\t resolved against the customComponents registry.\n\t\t\t\t\t\t3. CnObjectList's default CnObjectRow.\n\t\t\t\t\t-->\n\t\t\t\t\t<template v-if=\"$slots['list-item']\" #list-item=\"{ object, selected }\">\n\t\t\t\t\t\t<slot name=\"list-item\" :object=\"object\" :selected=\"selected\" />\n\t\t\t\t\t</template>\n\t\t\t\t\t<template v-else-if=\"resolvedListComponent\" #list-item=\"{ object, selected }\">\n\t\t\t\t\t\t<component\n\t\t\t\t\t\t\t:is=\"resolvedListComponent\"\n\t\t\t\t\t\t\t:item=\"object\"\n\t\t\t\t\t\t\t:object=\"object\"\n\t\t\t\t\t\t\t:schema=\"effectiveSchema\"\n\t\t\t\t\t\t\t:register=\"register\"\n\t\t\t\t\t\t\t:selected=\"selected\"\n\t\t\t\t\t\t\t@click=\"(...args) => onRowClick(object, args.find(isDomEvent))\"\n\t\t\t\t\t\t\t@mousedown=\"onCustomItemMouseDown\"\n\t\t\t\t\t\t\t@auxclick=\"onCustomItemAuxClick(object, $event)\"\n\t\t\t\t\t\t\t@select=\"onSelect(toggleIdInArray(internalSelectedIds, object[rowKey]))\" />\n\t\t\t\t\t</template>\n\t\t\t\t\t<!-- Per-part row slots (list view only): forwarded to CnObjectRow\n\t\t\t\t\t so an app can override just the leading icon or the badge\n\t\t\t\t\t while keeping the config-driven title/subtitle. -->\n\t\t\t\t\t<template v-if=\"$slots['row-icon']\" #row-icon=\"{ object }\">\n\t\t\t\t\t\t<slot name=\"row-icon\" :object=\"object\" />\n\t\t\t\t\t</template>\n\t\t\t\t\t<template v-if=\"$slots['row-badges']\" #row-badges=\"{ object }\">\n\t\t\t\t\t\t<slot name=\"row-badges\" :object=\"object\" />\n\t\t\t\t\t</template>\n\t\t\t\t\t<template v-if=\"hasRowActions || $slots['row-actions']\" #row-actions=\"{ object }\">\n\t\t\t\t\t\t<slot name=\"row-actions\" :row=\"object\">\n\t\t\t\t\t\t\t<CnRowActions\n\t\t\t\t\t\t\t\t:actions=\"rowActionsFor(object)\"\n\t\t\t\t\t\t\t\t:row=\"object\"\n\t\t\t\t\t\t\t\t@action=\"onRowAction\" />\n\t\t\t\t\t\t</slot>\n\t\t\t\t\t</template>\n\t\t\t\t</CnObjectList>\n\n\t\t\t\t<!-- Card view -->\n\t\t\t\t<CnCardGrid\n\t\t\t\t\tv-else\n\t\t\t\t\t:objects=\"displayObjects\"\n\t\t\t\t\t:schema=\"effectiveSchema\"\n\t\t\t\t\t:selectable=\"selectable\"\n\t\t\t\t\t:clickToView=\"rowClickOpens\"\n\t\t\t\t\t:selectedIds=\"internalSelectedIds\"\n\t\t\t\t\t:rowKey=\"rowKey\"\n\t\t\t\t\t:emptyText=\"emptyText\"\n\t\t\t\t\t@click=\"onRowClick\"\n\t\t\t\t\t@auxClick=\"onRowAuxClick\"\n\t\t\t\t\t@select=\"onSelect\">\n\t\t\t\t\t<!--\n\t\t\t\t\t\tCard slot resolution priority (highest first):\n\t\t\t\t\t\t1. Parent-provided `#card` scoped slot — App.vue overrides win.\n\t\t\t\t\t\t2. `cardComponent` prop (or manifest `pages[].config.cardComponent`)\n\t\t\t\t\t\t resolved against the customComponents registry.\n\t\t\t\t\t\t3. CnCardGrid's default CnObjectCard.\n\t\t\t\t\t-->\n\t\t\t\t\t<template v-if=\"$slots.card\" #card=\"{ object, selected }\">\n\t\t\t\t\t\t<slot name=\"card\" :object=\"object\" :selected=\"selected\" />\n\t\t\t\t\t</template>\n\t\t\t\t\t<template v-else-if=\"resolvedCardComponent\" #card=\"{ object, selected }\">\n\t\t\t\t\t\t<component\n\t\t\t\t\t\t\t:is=\"resolvedCardComponent\"\n\t\t\t\t\t\t\t:item=\"object\"\n\t\t\t\t\t\t\t:object=\"object\"\n\t\t\t\t\t\t\t:schema=\"effectiveSchema\"\n\t\t\t\t\t\t\t:register=\"register\"\n\t\t\t\t\t\t\t:selected=\"selected\"\n\t\t\t\t\t\t\t@click=\"(...args) => onRowClick(object, args.find(isDomEvent))\"\n\t\t\t\t\t\t\t@mousedown=\"onCustomItemMouseDown\"\n\t\t\t\t\t\t\t@auxclick=\"onCustomItemAuxClick(object, $event)\"\n\t\t\t\t\t\t\t@select=\"onSelect(toggleIdInArray(internalSelectedIds, object[rowKey]))\" />\n\t\t\t\t\t</template>\n\t\t\t\t\t<template v-if=\"hasRowActions\" #card-actions=\"{ object }\">\n\t\t\t\t\t\t<slot name=\"row-actions\" :row=\"object\">\n\t\t\t\t\t\t\t<CnRowActions\n\t\t\t\t\t\t\t\t:actions=\"rowActionsFor(object)\"\n\t\t\t\t\t\t\t\t:row=\"object\"\n\t\t\t\t\t\t\t\t@action=\"onRowAction\" />\n\t\t\t\t\t\t</slot>\n\t\t\t\t\t</template>\n\t\t\t\t</CnCardGrid>\n\n\t\t\t\t<!-- Right-click context menu (positioned at cursor via CSS) -->\n\t\t\t\t<CnContextMenu\n\t\t\t\t\tv-model:open=\"contextMenuOpen\"\n\t\t\t\t\t:actions=\"mergedActions\"\n\t\t\t\t\t:targetItem=\"contextMenuRow\"\n\t\t\t\t\t@action=\"onRowAction\"\n\t\t\t\t\t@close=\"closeContextMenu\" />\n\n\t\t\t\t<!-- Pagination -->\n\t\t\t\t<CnPagination\n\t\t\t\t\tv-if=\"effectivePagination && effectivePagination.pages > 1\"\n\t\t\t\t\t:currentPage=\"effectivePagination.page || 1\"\n\t\t\t\t\t:totalPages=\"effectivePagination.pages || 1\"\n\t\t\t\t\t:totalItems=\"effectivePagination.total || 0\"\n\t\t\t\t\t:currentPageSize=\"effectivePagination.limit || 20\"\n\t\t\t\t\tclass=\"cn-index-page__pagination\"\n\t\t\t\t\t@pageChanged=\"onPageEvent\"\n\t\t\t\t\t@pageSizeChanged=\"$emit('page-size-changed', $event)\" />\n\t\t\t</div>\n\n\t\t\t<!-- The open record, beside the list on a wide screen and instead\n\t\t\t of it on a narrow one. Same slot either way, and the host\n\t\t\t mounts the same detail component in it that the full route\n\t\t\t mounts: two detail implementations drift within a month. -->\n\t\t\t<div\n\t\t\t\tv-if=\"splitLayout !== 'list'\"\n\t\t\t\tclass=\"cn-index-page__split-pane\"\n\t\t\t\t:class=\"{ 'cn-index-page__split-pane--full': splitLayout === 'detail' }\"\n\t\t\t\t:style=\"splitLayout === 'split' ? { width: splitPaneWidth } : null\"\n\t\t\t\tdata-testid=\"cn-index-page-split-pane\"\n\t\t\t\t:data-split-layout=\"splitLayout\">\n\t\t\t\t<!-- An <a>, not a button: closing the pane pushes the list's\n\t\t\t\t route, so the browser supplies middle-click and open-in-new-tab\n\t\t\t\t while `.prevent` keeps the plain click in-app. -->\n\t\t\t\t<a\n\t\t\t\t\tv-if=\"splitCloseVisible\"\n\t\t\t\t\tclass=\"cn-index-page__split-close\"\n\t\t\t\t\t:href=\"splitCloseHref\"\n\t\t\t\t\t:title=\"splitCloseTitle\"\n\t\t\t\t\tdata-testid=\"cn-index-page-split-close\"\n\t\t\t\t\t@click.prevent=\"closeSplitPane\">\n\t\t\t\t\t<Close :size=\"16\" />\n\t\t\t\t\t<span>{{ splitCloseTitle }}</span>\n\t\t\t\t</a>\n\t\t\t\t<!-- @slot split-pane The open record, rendered beside the list. Mount the SAME detail component the full route mounts. -->\n\t\t\t\t<!-- @binding {string} id The record the address names. -->\n\t\t\t\t<!-- @binding {string} layout Either `split` (beside the list) or `detail` (the full page, below the breakpoint). -->\n\t\t\t\t<!-- @binding {Function} close Closes the pane and returns to the list, at the list's own address. -->\n\t\t\t\t<!-- @binding {Function} saved Call with the saved record to replace its row in the list without refetching the page. -->\n\t\t\t\t<slot\n\t\t\t\t\t:id=\"splitId\"\n\t\t\t\t\tname=\"split-pane\"\n\t\t\t\t\t:layout=\"splitLayout\"\n\t\t\t\t\t:close=\"closeSplitPane\"\n\t\t\t\t\t:saved=\"onSplitPaneSaved\" />\n\t\t\t</div>\n\t\t</div>\n\n\t\t<!-- Manifest-driven sidebar — auto-mounted when sidebar.enabled\n\t\t AND sidebar.show !== false.\n\n\t\t When CnAppRoot is the host, the sidebar config is\n\t\t PUBLISHED to the `cnIndexSidebarConfig` provide (see\n\t\t mounted/beforeDestroy), and CnAppRoot mounts the\n\t\t CnIndexSidebar at NcContent level — the only place where\n\t\t Nextcloud's NcAppSidebar slides correctly from the right.\n\n\t\t When no CnAppRoot ancestor exists (legacy apps mounting\n\t\t CnIndexPage standalone), the inject default is the no-op\n\t\t `{ value: null }` and we fall back to inline rendering\n\t\t here so the legacy contract still works. -->\n\t\t<CnIndexSidebar\n\t\t\tv-if=\"shouldRenderInlineSidebar\"\n\t\t\t:open=\"sidebarOpen\"\n\t\t\t:schema=\"effectiveSchema\"\n\t\t\t:title=\"title\"\n\t\t\t:icon=\"resolvedIcon\"\n\t\t\t:searchValue=\"effectiveSearchValue\"\n\t\t\t:visibleColumns=\"effectiveVisibleColumns\"\n\t\t\t:activeFilters=\"effectiveActiveFilters\"\n\t\t\t:columnGroups=\"resolvedSidebar.columnGroups || []\"\n\t\t\t:filterFields=\"resolvedSidebar.fields || null\"\n\t\t\t:facetData=\"effectiveFacetData\"\n\t\t\t:showMetadata=\"resolvedSidebar.showMetadata !== false\"\n\t\t\tv-bind=\"sidebarSearchProps\"\n\t\t\t@update:open=\"sidebarOpen = $event\"\n\t\t\t@search=\"onSearchEvent\"\n\t\t\t@columnsChange=\"onColumnsEvent\"\n\t\t\t@filterChange=\"onFilterEvent\"\n\t\t\t@clearFilters=\"onClearFilters\" />\n\t</div>\n</template>\n\n<script>\nimport { getCurrentUser } from '@nextcloud/auth'\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcActionButton, NcActionCaption, NcActionCheckbox, NcActions, NcButton, NcEmptyContent, NcLoadingIcon } from '@nextcloud/vue'\nimport { getCurrentInstance, inject, markRaw, ref } from 'vue'\nimport Close from 'vue-material-design-icons/Close.vue'\nimport Cog from 'vue-material-design-icons/Cog.vue'\nimport DatabaseSearch from 'vue-material-design-icons/DatabaseSearch.vue'\nimport Export from 'vue-material-design-icons/Export.vue'\nimport Eye from 'vue-material-design-icons/Eye.vue'\nimport FilterOutline from 'vue-material-design-icons/FilterOutline.vue'\nimport ViewColumnOutline from 'vue-material-design-icons/ViewColumnOutline.vue'\nimport CnConfirmDialog from '../../dialogs/CnConfirmDialog.vue'\nimport CnQuickEditDialog from '../../dialogs/CnQuickEditDialog.vue'\nimport { useContextMenu } from '../../composables/index.js'\nimport { useSavedViewsApi } from '../../composables/useSavedViewsApi.js'\nimport { METADATA_COLUMNS } from '../../constants/metadata.js'\nimport { routeHref } from '../../utils/actionLink.js'\nimport { buildOnSuccessRoute, resolveRegisteredHandler } from '../../utils/actionsDispatcher.js'\nimport { buildExportUrl } from '../../utils/indexExportHelpers.js'\nimport { openRowTarget } from '../../utils/linkNavigation.js'\nimport { resolveClaimedTeams, resolveClaimTokens, splitViewsIntoTabs, viewAsTab } from '../../utils/listLenses.js'\nimport { LIST_SHORTCUTS, listPaletteCommands, shortcutFor } from '../../utils/listShortcuts.js'\nimport { multiKeySort } from '../../utils/multiKeySort.js'\nimport { resolveDeepTokens, resolveFilterValue } from '../../utils/resolveFilterTokens.js'\nimport { availableRowActions, DEFAULT_ROW_ACTION_FIELD, refusalReasonFor, undeclaredRowActions } from '../../utils/rowActionAvailability.js'\nimport { isNewTabClick, isNewTabHandled, isRowMiddleClick, markNewTabHandled, preventMiddleClickAutoscroll } from '../../utils/rowAuxClick.js'\nimport { DEFAULT_ROW_INDICATOR_CAP } from '../../utils/rowIndicators.js'\nimport { buildRouteQueryFromViewState, buildViewCreatePayload, extractViewState, extractViewStateFromRouteQuery, savedViewScope, viewMatchesScope } from '../../utils/savedViewHelpers.js'\nimport { columnsFromSchema } from '../../utils/schema.js'\nimport { resolveScopeLayout } from '../../utils/scopeListLayout.js'\nimport { CnActionsBar } from '../CnActionsBar/index.js'\nimport { CnAdvancedFormDialog } from '../CnAdvancedFormDialog/index.js'\nimport { CnBoardView } from '../CnBoardView/index.js'\nimport { CnCardGrid } from '../CnCardGrid/index.js'\nimport { CnContextMenu } from '../CnContextMenu/index.js'\nimport { CnCopyDialog } from '../CnCopyDialog/index.js'\nimport { CnDataTable } from '../CnDataTable/index.js'\nimport { CnDateAxisView } from '../CnDateAxisView/index.js'\nimport { CnDeleteDialog } from '../CnDeleteDialog/index.js'\nimport { CnFolderSidebar } from '../CnFolderSidebar/index.js'\nimport { CnFormDialog } from '../CnFormDialog/index.js'\nimport { CnIcon } from '../CnIcon/index.js'\nimport { CnIndexSidebar } from '../CnIndexSidebar/index.js'\nimport { CnMapWidget } from '../CnMapWidget/index.js'\nimport { CnMassCopyDialog } from '../CnMassCopyDialog/index.js'\nimport { CnMassDeleteDialog } from '../CnMassDeleteDialog/index.js'\nimport { CnMassExportDialog } from '../CnMassExportDialog/index.js'\nimport { CnMassImportDialog } from '../CnMassImportDialog/index.js'\nimport { CnObjectList } from '../CnObjectList/index.js'\nimport { CnPageHeader } from '../CnPageHeader/index.js'\nimport { CnPagination } from '../CnPagination/index.js'\nimport { CnQuickFilterBar } from '../CnQuickFilterBar/index.js'\nimport { CnRowActions } from '../CnRowActions/index.js'\nimport { CnSavedViewsControl } from '../CnSavedViewsControl/index.js'\nimport { CnSaveViewDialog } from '../CnSaveViewDialog/index.js'\nimport { applyAiContext } from './aiContext.js'\nimport { buildDefaultActions } from './defaultActions.js'\nimport { dispatchAction } from './manifestActionDispatch.js'\nimport { applyManualOrder, dropInOrder, manualOrderKey, moveInOrder, visibleIdsOf } from './manualOrder.js'\nimport { createSelfModeActions } from './selfModeActions.js'\nimport { applyRowPatches, normalisePaneWidth, rowIdOf, splitLayoutFor } from './splitView.js'\nimport { useNamedSource } from './useNamedSource.js'\nimport { useSelfFetchList } from './useSelfFetchList.js'\n\n/**\n * The separator a date-range filter uses in the URL: `2026-09-21..2026-09-25`.\n *\n * One query parameter per FIELD, not per bound, so the link reads as the\n * question the person asked and an open-ended window still round-trips\n * (`..2026-09-25` is \"due before Friday\").\n *\n * @type {string}\n */\nconst RANGE_SEPARATOR = '..'\n\n/**\n * Whether a schema property wants a from/to pair rather than a value list.\n *\n * @param {object} prop The schema property.\n *\n * @return {boolean} True for a range control.\n */\nfunction isRangeControl(prop) {\n\tconst control = String((prop && prop.inputControl) || '')\n\treturn control === 'date-range' || control === 'range'\n}\n\n/**\n * The filter declarations a page carries on its sidebar config.\n *\n * `sidebar.fields` and not `config.schema`: the schema key names the\n * OpenRegister schema a page self-fetches from, and a named source has no\n * register or schema to point at. The manifest schema types it as a string\n * for exactly that reason.\n *\n * @param {object} props The CnIndexPage props.\n *\n * @return {object} `{ propertyName: declaration }`, empty when none.\n */\nfunction declaredFilterFields(props) {\n\tconst fields = props.sidebar && props.sidebar.fields\n\treturn (fields && typeof fields === 'object') ? fields : {}\n}\n\n/**\n * Read a named-source page's sidebar filters back out of `$route.query`.\n *\n * Only properties the page's own schema declares `facetable` are read, so an\n * unrelated query parameter (`?action=create`, a saved-view key) never\n * becomes a filter, and a hand-edited link cannot invent a field the source\n * has no argument for.\n *\n * @param {import('vue').ComponentInternalInstance|null} instance The instance, for `$route`.\n * @param {object} props The CnIndexPage props.\n *\n * @return {object} The `{ fieldKey: values }` map, empty when the link carries none.\n */\nfunction namedFiltersFromRoute(instance, props) {\n\tif (!props.entitySource) {\n\t\treturn {}\n\t}\n\tconst route = instance && instance.proxy && instance.proxy.$route\n\tconst query = (route && route.query) || {}\n\tconst properties = declaredFilterFields(props)\n\tconst out = {}\n\n\tfor (const [key, prop] of Object.entries(properties)) {\n\t\tif (!prop || prop.facetable !== true) {\n\t\t\tcontinue\n\t\t}\n\t\tconst raw = query[key]\n\t\tif (raw === undefined || raw === null || raw === '') {\n\t\t\tcontinue\n\t\t}\n\t\tconst value = Array.isArray(raw) ? raw.join(',') : String(raw)\n\n\t\tif (isRangeControl(prop)) {\n\t\t\tconst [from, to] = value.split(RANGE_SEPARATOR)\n\t\t\tconst range = {}\n\t\t\tif (from) {\n\t\t\t\trange.from = from\n\t\t\t}\n\t\t\tif (to) {\n\t\t\t\trange.to = to\n\t\t\t}\n\t\t\tif (Object.keys(range).length > 0) {\n\t\t\t\tout[key] = range\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tconst values = value.split(',').filter((entry) => entry !== '')\n\t\tif (values.length > 0) {\n\t\t\tout[key] = values\n\t\t}\n\t}\n\n\treturn out\n}\n\n/**\n * The query-parameter spelling of one named-source filter value.\n *\n * @param {unknown} values The chosen values, or a `{ from, to }` range.\n *\n * @return {string} The parameter value, or '' when nothing is chosen.\n */\nfunction namedFilterToQuery(values) {\n\tif (values && typeof values === 'object' && !Array.isArray(values)) {\n\t\tconst from = values.from || ''\n\t\tconst to = values.to || ''\n\t\treturn (from === '' && to === '') ? '' : `${from}${RANGE_SEPARATOR}${to}`\n\t}\n\tconst list = (Array.isArray(values) ? values : [values])\n\t\t.map((entry) => ((entry && typeof entry === 'object' && 'id' in entry) ? entry.id : entry))\n\t\t.filter((entry) => entry !== undefined && entry !== null && String(entry) !== '')\n\treturn list.join(',')\n}\n\n/**\n * CnIndexPage — Top-level schema-driven index page component.\n *\n * Assembles sub-components (CnPageHeader, CnActionsBar, table, cards,\n * pagination, mass actions, single-object dialogs) into a single\n * zero-config page.\n *\n * Dialogs are overridable via named slots:\n * - `#form-dialog` — Replace the create/edit dialog entirely\n * - `#delete-dialog` — Replace the single-item delete dialog\n * - `#copy-dialog` — Replace the single-item copy dialog\n * - `#form-fields` — Replace only the form content inside the built-in form dialog (CnFormDialog only)\n *\n * Use the `useAdvancedFormDialog` prop to use CnAdvancedFormDialog for create/edit (properties table, JSON tab, optional metadata).\n *\n * Multi-column sort (self-fetch mode: `register` + `schema`, no external\n * `objects`): shift+click a second/third sortable header in the embedded\n * CnDataTable to build a priority-ordered sort. The active key list is\n * translated into OpenRegister's `_order` query param (`{field: 'asc'|\n * 'desc', ...}`, key order = priority — the exact shape `ObjectsController::\n * normalizeOrderParameter` / `MagicMapper` consume) and persisted in\n * `$route.query._order` (JSON-encoded), so a reload or a shared link\n * restores the same sort. Host-controlled (non-self-fetch) pages pass their\n * own `sortKeys` prop instead.\n *\n * Minimal usage (auto-generated dialogs from schema)\n * ```vue\n * <CnIndexPage\n * title=\"Clients\"\n * :schema=\"effectiveSchema\"\n * :objects=\"clients\"\n * :pagination=\"effectivePagination\"\n * :loading=\"loading\"\n * @create=\"onCreate\"\n * @edit=\"onEdit\"\n * @delete=\"onDelete\"\n * @refresh=\"fetchClients\"\n * @row-click=\"openClient\"\n * @page-changed=\"onPage\" />\n * ```\n *\n * With custom form dialog\n * ```vue\n * <CnIndexPage ...>\n * <template #form-dialog=\"{ item, schema, confirm, close }\">\n * <MyCustomFormDialog :item=\"item\" @save=\"confirm\" @close=\"close\" />\n * </template>\n * </CnIndexPage>\n * ```\n *\n * @event {void} add — Add button clicked (backward compat, only if listener attached)\n * @event {object} create — Form dialog create confirmed. Payload: formData object\n * @event {object} edit — Form dialog edit confirmed. Payload: formData object (includes id)\n * @event {string} delete — Single delete confirmed. Payload: item ID\n * @event {{ id: string, newName: string }} copy — Single copy confirmed\n * @event {string[]} mass-delete — Mass delete confirmed. Payload: array of IDs\n * @event {object} mass-copy — Mass copy confirmed. Payload: { ids, pattern }\n * @event {object} mass-export — Mass export confirmed. Payload: { ids, format }\n * @event {object} mass-import — Mass import confirmed. Payload: import data\n * @event {void} refresh — Refresh button clicked\n * @event {object} row-click — Table row or card clicked. Payload: (row, event) — the row object and the native click event\n * @event {object} row-aux-click — Table row or card middle-clicked. Payload: (row, event) — the row object and the native auxclick event\n * @event {{ key: string, order: string }} sort — Column sort changed\n * @event {number} page-changed — Pagination page changed\n * @event {number} page-size-changed — Pagination page size changed\n * @event {string[]} select — Selection changed. Payload: array of selected IDs\n * @event {object} action — Row action triggered. Payload: { action, row }\n * @event {{ action: string, id: string, selectedIds: Array, count: number }} bulk-action — A declarative bulk action was triggered from the selection strip. The SELECTION travels with it: an action that has to go and find out what was selected is one re-render away from acting on a different set than the user saw highlighted.\n * @event {{ target: string, props: object }} open-modal — A bulk action of type `open-modal` asks the host to open a registered modal. `props` carries `selectedIds` and `count` merged under the action's own props.\n * @event {object} apply-view — A saved view was applied (saved-views-ui). Payload: the View API object. Only emitted when `allowSavedViews`.\n * @event {string} search — Search input changed in the embedded sidebar. Only emitted when `sidebar.enabled`.\n * @event {string[]} columns-change — Visible columns changed in the embedded sidebar. Only emitted when `sidebar.enabled`.\n * @event {{ key: string, values: Array<unknown> }} filter-change — Facet filter changed in the embedded sidebar. Only emitted when `sidebar.enabled`.\n * @event {void} clear-filters — \"Clear all\" clicked in the embedded sidebar. Search, every active filter and the folder-sidebar selection are reset (self-fetch mode); a consumer-managed page gets the bare event to handle itself.\n *\n * @slot mass-actions — Extra mass action buttons (shown when items are selected)\n * @slot action-items — Extra action bar buttons\n * @slot after-search — Refinement controls beside the search field on the bar's left side (e.g. a filter menu button)\n * @slot before-collection — Content above the collection in every view mode (e.g. a folder/group strip)\n * @slot selection-actions — Bulk-action buttons in the contextual selection strip shown while a selection is active. Scope: `{ count, selectedIds }`\n * @slot header-actions — Extra buttons in the page header\n * @slot delete-dialog — Replace the single-item delete dialog. Scope: `{ show, item, confirm, close }`. Call `confirm()` to perform the delete.\n * @slot copy-dialog — Replace the single-item copy dialog. Scope: `{ show, item, confirm, close }`. Call `confirm(payload)` to perform the copy.\n * @slot form-dialog — Replace the create/edit form dialog. Scope: `{ show, item, schema, confirm, close }`. Call `confirm(formData)` to save through the host's normal persistence path.\n * @slot form-fields — Replace form content inside the built-in CnFormDialog. Scope: `{ fields, formData, errors, updateField }`\n * @slot import-fields — Extra fields in the import dialog\n * @slot empty — Custom empty state content\n * @slot card — Custom card template for card view. Scope: `{ row }`\n * @slot row-actions — Custom row actions. Scope: `{ row }`\n * @slot column-{key} — Custom cell renderer for a specific column. Scope: `{ row, value }`\n */\nexport default {\n\tname: 'CnIndexPage',\n\n\tcomponents: {\n\t\tNcLoadingIcon,\n\t\tNcEmptyContent,\n\t\tNcActions,\n\t\tNcActionButton,\n\t\tNcActionCaption,\n\t\tNcActionCheckbox,\n\t\tNcButton,\n\t\tClose,\n\t\tCog,\n\t\tDatabaseSearch,\n\t\tExport,\n\t\tFilterOutline,\n\t\tViewColumnOutline,\n\t\tCnPageHeader,\n\t\tCnQuickFilterBar,\n\t\tCnActionsBar,\n\t\tCnIcon,\n\t\tCnDataTable,\n\t\tCnCardGrid,\n\t\tCnBoardView,\n\t\tCnDateAxisView,\n\t\tCnMapWidget,\n\t\tCnObjectList,\n\t\tCnFolderSidebar,\n\t\tCnPagination,\n\t\tCnRowActions,\n\t\tCnMassDeleteDialog,\n\t\tCnMassCopyDialog,\n\t\tCnMassExportDialog,\n\t\tCnMassImportDialog,\n\t\tCnDeleteDialog,\n\t\tCnCopyDialog,\n\t\tCnFormDialog,\n\t\tCnAdvancedFormDialog,\n\t\tCnContextMenu,\n\t\tCnIndexSidebar,\n\t\tCnSavedViewsControl,\n\t\tCnSaveViewDialog,\n\t\tCnConfirmDialog,\n\t\tCnQuickEditDialog,\n\t},\n\n\t/**\n\t * Inject the customComponents registry from a CnAppRoot ancestor.\n\t * Used by:\n\t * - REQ-MAD-3 / REQ-MAD-8 (manifest-actions-dispatch): resolves\n\t * `actions[].handler` registry names to functions called on\n\t * row-action click.\n\t * - The cardComponent + form-dialog override paths: when set, the\n\t * prop-level `customComponents` wins, but the inject is the\n\t * default. See `effectiveCustomComponents`.\n\t *\n\t * Falls back to an empty object so `CnIndexPage` works standalone\n\t * (unit tests, isolated mount) without `CnAppRoot`.\n\t */\n\tinject: {\n\t\tcnCustomComponents: { default: () => ({}) },\n\t\t/**\n\t\t * The v2 component registry, provided by CnAppRoot. Named handlers\n\t\t * (`actions[].handler`, `bulkActions[].handler`,\n\t\t * `headerActions[].handler`) resolve here first and fall back to\n\t\t * `cnCustomComponents`, so an app can register a `kind: 'handler'`\n\t\t * entry instead of a bare function in the legacy map.\n\t\t */\n\t\tcnRegistry: { default: () => ({}) },\n\t\t/**\n\t\t * The per-user preference reader and writer, provided by CnAppRoot.\n\t\t * Used for the manual row order, which belongs to the person and the\n\t\t * list rather than to the records. The default is null, and every read\n\t\t * and write is guarded, so a page mounted with no CnAppRoot ancestor\n\t\t * simply has no held order instead of failing to render.\n\t\t */\n\t\tcnUserPreferences: { default: null },\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). Column and\n\t\t * filter labels come from schema property titles, authored in English as\n\t\t * the canonical source; the visible label is resolved through this\n\t\t * function so it follows the user's language. Defaults to identity when\n\t\t * used standalone (no CnAppRoot ancestor). The embedded CnDataTable\n\t\t * resolves its own header labels through the same injection.\n\t\t */\n\t\tcnTranslate: { default: () => (key) => key },\n\t\t/**\n\t\t * Reactive edit-mode flag from CnAppRoot's manifest editor. When truthy\n\t\t * the page shows a config cog in its actions bar (emits `configure`).\n\t\t */\n\t\tcnEditingBody: { default: false },\n\t\t/** Opens a registry modal, provided by CnAppRoot. Used by `createModal`. */\n\t\tcnOpenModal: { default: null },\n\t\t/**\n\t\t * Reactive holder provided by CnAppRoot for hoisting the\n\t\t * embedded CnIndexSidebar to NcContent level. The default\n\t\t * `{ value: null }` is what we get when no CnAppRoot\n\t\t * ancestor exists; in that case we fall back to inline\n\t\t * rendering inside the cn-index-page wrapper. See\n\t\t * `shouldRenderInlineSidebar` and the mounted/beforeDestroy\n\t\t * hooks below.\n\t\t */\n\t\tcnIndexSidebarConfig: { default: () => ({ value: null }) },\n\t\t/**\n\t\t * Sentinel set to `true` when a CnAppRoot ancestor exists.\n\t\t * The default `false` is used for legacy apps that mount\n\t\t * CnIndexPage standalone — those keep the inline sidebar\n\t\t * render. See `shouldRenderInlineSidebar` for the gate.\n\t\t */\n\t\tcnHostsIndexSidebar: { default: false },\n\t\t/**\n\t\t * Reactive AI context holder provided by CnAppRoot. This page\n\t\t * component writes pageKind, registerSlug, schemaSlug in\n\t\t * created() and watches props for subsequent changes. On\n\t\t * beforeUnmount(), fields are reset to avoid stale context on\n\t\t * subsequent custom pages.\n\t\t */\n\t\tcnAiContext: { default: null },\n\t},\n\n\tprops: {\n\t\t/** Page title */\n\t\ttitle: {\n\t\t\ttype: String,\n\t\t\trequired: true,\n\t\t},\n\n\t\t/** Optional description shown below the title */\n\t\tdescription: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Whether to show the page header (icon, title, description) inline.\n\t\t * When false (default), only the sidebar header shows the title\n\t\t * visually — but the `<h1>` is still rendered visually-hidden inside\n\t\t * the page, so the `<main>` landmark always has an accessible heading.\n\t\t * This prop controls VISIBILITY only; it can no longer remove the\n\t\t * heading from the accessibility tree.\n\t\t */\n\t\tshowTitle: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Optional MDI icon name. Defaults to schema.icon when a schema is provided. */\n\t\ticon: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Schema. Either a resolved schema object (consumer-managed path) OR a\n\t\t * schema-slug string — when a string is given together with `register`\n\t\t * and no `objects` prop, the page enters self-fetch mode: it drives the\n\t\t * list via `useListView('${register}-${schema}', …)` and the column\n\t\t * generation uses the schema object that composable loads. Backwards-\n\t\t * compatible: `[Object, String]` still accepts an object.\n\t\t */\n\t\tschema: {\n\t\t\ttype: [Object, String],\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/* eslint-disable vue/no-unused-properties -- used in useSelfFetchList */\n\t\t/**\n\t\t * Base filter for the self-fetch path. String values of the form\n\t\t * `\"@route.<name>\"` / `\":<name>\"` are interpolated from `$route.params`.\n\t\t */\n\t\tfilter: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\t\t/* eslint-enable vue/no-unused-properties */\n\n\t\t/**\n\t\t * Self-fetch mode only — an array of clickable filter tabs rendered as\n\t\t * a strip above the table. Each entry is `{label, filter, default?, icon?}`;\n\t\t * clicking a tab merges its `filter` into the fetch — spread AFTER\n\t\t * `filter` (so the active tab wins) and BEFORE the user's `activeFilters`\n\t\t * (so user facets still narrow within the active tab). String values\n\t\t * in a tab's `filter` resolve `@route.<name>` / `:<name>` from\n\t\t * `$route.params` just like the `filter` prop. The first tab with\n\t\t * `default:true` (else index 0) is active on mount; changing tabs\n\t\t * re-fetches at page 1. Omit (or `null`) → no tab strip, behaviour\n\t\t * unchanged.\n\t\t */\n\t\tquickFilters: {\n\t\t\ttype: Array,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * How the quick filters render: `'chips'` (pill strip, default) or\n\t\t * `'dropdown'` (a single `NcSelect`). Sourced from the manifest as\n\t\t * `pages[].config.quickFilterMode`.\n\t\t *\n\t\t * @type {'chips'|'dropdown'}\n\t\t */\n\t\tquickFilterMode: {\n\t\t\ttype: String,\n\t\t\tdefault: 'chips',\n\t\t\tvalidator: (v) => ['chips', 'dropdown'].includes(v),\n\t\t},\n\n\t\t/**\n\t\t * Allow several quick filters active at once. Selected tabs' filters\n\t\t * are OR-ed together into the fetch (same field → array value →\n\t\t * `field[]=` IN query). Sourced from `pages[].config.quickFilterMultiple`.\n\t\t */\n\t\tquickFilterMultiple: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * Chips mode only: how many quick-filter pills render inline before the\n\t\t * rest move into an overflow menu. `0` (the default) renders every tab.\n\t\t * A long strip wraps the actions bar onto a second line and squeezes the\n\t\t * \"Showing X of Y\" count, so a page with many lenses caps this and keeps\n\t\t * the everyday few in view. Sourced from\n\t\t * `pages[].config.quickFilterMaxVisible`.\n\t\t */\n\t\tquickFilterMaxVisible: {\n\t\t\ttype: Number,\n\t\t\tdefault: 0,\n\t\t},\n\n\t\t/** Manual column definitions (used instead of schema when provided) */\n\t\tcolumns: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * OpenRegister `_extend[]` values forwarded on the fetch, e.g.\n\t\t * `[\"calculations\"]`. Sourced from `pages[].config.extend`.\n\t\t *\n\t\t * A schema's `x-openregister-calculations` entries declared\n\t\t * `materialise: false` are VIRTUAL: RenderObject evaluates them only\n\t\t * when the caller asks for them through `_extend`, so without this a\n\t\t * declared calculation is simply absent from every row and its column\n\t\t * renders empty. The same contract `objectTableSource.extend` already\n\t\t * carries for dashboard widgets, which is where it shipped first;\n\t\t * index pages could not ask for it at all.\n\t\t */\n\t\textend: { // eslint-disable-line vue/no-unused-properties -- read by useSelfFetchList.js off the props object, which this rule does not follow.\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/** Object/row data array */\n\t\tobjects: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * Name of a registered NON-OBJECT entity collection to list (e.g. `flows`).\n\t\t *\n\t\t * The third data mode. `register` + `schema` fetches OpenRegister\n\t\t * objects; `:objects` renders rows a parent already holds; `entitySource`\n\t\t * names a list that is neither — a flow definition is deliberately not\n\t\t * an OpenRegister object, so an index had nothing to point at and such\n\t\t * lists became bespoke `type: \"custom\"` pages instead.\n\t\t *\n\t\t * Non-empty `:objects` still wins, and an entity source wins over\n\t\t * register/schema. See `src/composables/indexSources.js`.\n\t\t */\n\t\tentitySource: { type: String, default: '' },\n\n\t\t/**\n\t\t * Route name a clicked row opens, overriding a named source's own\n\t\t * navigation.\n\t\t *\n\t\t * An entity source knows where its rows live and navigates itself, and\n\t\t * for a source whose detail page belongs to ANOTHER app that is right:\n\t\t * `tasks` sends a click to openregister's task page, because pushing\n\t\t * on this app's router could not reach it.\n\t\t *\n\t\t * It is wrong for an app that HAS its own page for those rows. Dossiq\n\t\t * keeps a task detail page on purpose, so its handlers see a task in\n\t\t * dossiq's vocabulary beside the case it belongs to. Without this prop\n\t\t * adopting `entitySource: \"tasks\"` would send every click out of the\n\t\t * app, and the `row-click` event cannot recover it: `openRow` calls\n\t\t * `window.location.assign()`, so a host's push never lands.\n\t\t *\n\t\t * Set it and the row pushes `{ name: rowRoute, params: { id } }` on\n\t\t * this app's router instead. Leave it unset and the source's own\n\t\t * navigation is unchanged, which is what every current consumer gets.\n\t\t */\n\t\trowRoute: { type: String, default: '' },\n\n\t\t/**\n\t\t * Config handed to the named source's loader (e.g. `{ app: 'dossiq' }`).\n\t\t */\n\t\tsourceConfig: { type: Object, default: null },\n\n\t\t/** Pagination state: { page, pages, total, limit } */\n\t\tpagination: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Whether data is loading */\n\t\tloading: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Whether rows/cards can be selected */\n\t\tselectable: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/**\n\t\t * When true, a row/card click emits `row-click` (to open/navigate) even\n\t\t * while `selectable` — selection then happens via the checkbox only.\n\t\t * Manifest-driven index pages set this when a matching detail page\n\t\t * exists, so clicking a row opens its detail. Default false preserves\n\t\t * the legacy select-on-click behaviour.\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\n\t\t/**\n\t\t * The page's `splitView` declaration: `{ enabled, breakpoint, paneWidth }`.\n\t\t * With `enabled`, opening a row renders it beside the list in the\n\t\t * `#split-pane` slot instead of navigating away, and the list keeps its\n\t\t * scroll position, its selection and its loaded page. Omit it and the\n\t\t * page renders exactly as it does today.\n\t\t *\n\t\t * @type {{ enabled?: boolean, breakpoint?: number, paneWidth?: string }}\n\t\t */\n\t\tsplitView: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * The record the split pane is showing, taken from the split address.\n\t\t * Empty closes the pane. Ignored unless `splitView.enabled`.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tsplitId: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Route name the pane's close button returns to. Defaults to the\n\t\t * current route's own list address when omitted.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tsplitCloseRoute: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Whether the pane draws its own close link. Set false when the\n\t\t * `#split-pane` slot draws one from the slot's `close`.\n\t\t *\n\t\t * @type {boolean}\n\t\t */\n\t\tsplitCloseButton: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/**\n\t\t * Accessible name and tooltip for the pane's close button.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tsplitCloseLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Lets this person drag the rows into an order of their own, held\n\t\t * against them and this list and never written onto the records. The\n\t\t * order stands down while a sort is active, because a sort the person\n\t\t * just chose is them asking for a different order.\n\t\t *\n\t\t * @type {boolean}\n\t\t */\n\t\tmanualOrder: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * Stable id this list's manual order is held under. Defaults to the\n\t\t * object type or the schema, so two lists of one schema on different\n\t\t * pages share an order only when the host says they should.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tmanualOrderId: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** Currently selected IDs */\n\t\tselectedIds: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * View mode: 'table', 'cards', 'list', 'map', 'board' or 'dateAxis'.\n\t\t * Default 'table'. List is opted in via `availableViewModes`; map via\n\t\t * `mapConfig` / `config.viewModes`; board and dateAxis via\n\t\t * `config.viewModes` and their own config blocks.\n\t\t */\n\t\tviewMode: {\n\t\t\ttype: String,\n\t\t\tdefault: 'table',\n\t\t\tvalidator: (v) => ['table', 'cards', 'list', 'map', 'board', 'dateAxis'].includes(v),\n\t\t},\n\n\t\t/**\n\t\t * Marker geometry mapping for the opt-in `map` view mode, mirroring the\n\t\t * manifest `config.map` block 1:1. When non-empty (and not excluded by an\n\t\t * explicit `config.viewModes`), a third \"Map\" segment appears in the view\n\t\t * toggle and the current filtered rows are plotted on a CnMapWidget.\n\t\t *\n\t\t * - `latField` / `lngField` — object (or `@self`) property paths holding the\n\t\t * marker's latitude / longitude. Dotted paths are supported (e.g.\n\t\t * `@self.geo.lat`).\n\t\t * - `geoField` — alternative single property holding a GeoJSON Point\n\t\t * geometry (`{ type: 'Point', coordinates: [lng, lat] }`); takes\n\t\t * precedence over lat/lngField when present and resolvable.\n\t\t * - `popupField` — object property rendered in the marker popup.\n\t\t * - `center` — optional `[lat, lng]` fallback centre when the filtered set\n\t\t * has no plottable rows.\n\t\t *\n\t\t * @type {{ latField?: string, lngField?: string, geoField?: string, popupField?: string, center?: [number, number] }}\n\t\t */\n\t\tmapConfig: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/** Label for the map view-toggle segment (defaults to \"Map\"). */\n\t\tmapLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** MDI icon name for the map view-toggle segment (defaults to the built-in map-marker icon). */\n\t\tmapIcon: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Explicit whitelist of view-toggle segments to offer, e.g.\n\t\t * `['table', 'cards', 'map']`. Fed from the manifest as\n\t\t * `pages[].config.viewModes`. When set it takes precedence over the\n\t\t * inferred availability (map otherwise appears iff `mapConfig` is\n\t\t * non-empty). Cards/table always render regardless of this list.\n\t\t *\n\t\t * @type {Array<'table' | 'cards' | 'list' | 'map' | 'board' | 'dateAxis'>}\n\t\t */\n\t\tviewModes: {\n\t\t\ttype: Array,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * The board's configuration, mirroring the manifest `config.board`\n\t\t * block: `{ statusField, cardFields, swimlaneField }`. The board is\n\t\t * offered only when this names a `statusField` AND `viewModes` lists\n\t\t * `board`: a segment that opens a board saying it cannot be one is\n\t\t * worse than no segment.\n\t\t *\n\t\t * @type {object}\n\t\t */\n\t\tboard: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * The date axis's configuration, mirroring the manifest\n\t\t * `config.dateAxis` block: `{ startField, endField, laneField,\n\t\t * labelField }`. Offered only when both date fields are named.\n\t\t *\n\t\t * @type {object}\n\t\t */\n\t\tdateAxis: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * The host's transition, handed to the board. Absent, the board is\n\t\t * read-only: it never writes the status field itself, so with no\n\t\t * transition there is nothing it can do.\n\t\t *\n\t\t * @type {((move: { card: object, toKey: string }) => Promise<object>)|null}\n\t\t */\n\t\trunTransition: {\n\t\t\ttype: Function,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Which view-mode toggle segments to expose (cards/table/list), in order.\n\t\t * Defaults to the historical Cards/Table pair; include `'list'` to offer the\n\t\t * list view. Fed from the manifest as `pages[].config.availableViewModes`.\n\t\t * Map is added separately via `mapConfig` / `viewModes`.\n\t\t *\n\t\t * @type {Array<'cards' | 'table' | 'list' | 'map' | 'board' | 'dateAxis'>}\n\t\t */\n\t\tavailableViewModes: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => ['cards', 'table'],\n\t\t\tvalidator: (modes) => modes.every((m) => ['cards', 'table', 'list', 'map', 'board', 'dateAxis'].includes(m)),\n\t\t},\n\n\t\t/** Current sort key */\n\t\tsortKey: {\n\t\t\ttype: String,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Current sort order */\n\t\tsortOrder: {\n\t\t\ttype: String,\n\t\t\tdefault: 'asc',\n\t\t},\n\n\t\t/**\n\t\t * Ordered multi-column sort state (host-controlled / non-self-fetch\n\t\t * mode): `[{ key, order }, ...]`, mirroring the `sortKey`/`sortOrder`\n\t\t * pair but for more than one active key. Ignored in self-fetch mode\n\t\t * (register + schema), which manages its own multi-sort state via\n\t\t * `useSelfFetchList`/`useListView` and persists it in the route query.\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/**\n\t\t * Optional declarative DEFAULT multi-key client-side sort, applied to the\n\t\t * already-loaded rows whenever no explicit column sort is active (no\n\t\t * `sortKey` selected by the user / passed in). Each entry is\n\t\t * `{ field, order }` with `order` one of `'asc'` / `'desc'` (default\n\t\t * `'asc'`); rows are compared by the first field, ties broken by the\n\t\t * next, and so on. Comparison is type-aware (numbers numerically, dates\n\t\t * by timestamp, strings via `localeCompare`). Clicking a sortable header\n\t\t * takes over and suppresses this default. Useful for a fixed presentation\n\t\t * order such as \"group by type, then name\".\n\t\t *\n\t\t * @type {Array<{field: string, order?: 'asc'|'desc'}>}\n\t\t */\n\t\tdefaultSort: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/** Unique row identifier property */\n\t\trowKey: {\n\t\t\ttype: String,\n\t\t\tdefault: 'id',\n\t\t},\n\n\t\t/**\n\t\t * Optional leading icon for every table row — a static MDI icon name or\n\t\t * a `(row) => iconName` function. Forwarded to CnDataTable. Fed from the\n\t\t * manifest as `pages[].config.rowIcon`. 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/** Columns to exclude in schema mode */\n\t\texcludeColumns: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/** Columns to include in schema mode (whitelist) */\n\t\tincludeColumns: {\n\t\t\ttype: Array,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Per-column overrides in schema mode */\n\t\tcolumnOverrides: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/** Row action definitions (app-provided, merged with built-in actions) */\n\t\tactions: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/** Text shown when no items found */\n\t\temptyText: {\n\t\t\ttype: String,\n\t\t\tdefault: 'No items found',\n\t\t},\n\n\t\t/** Accessible label for the loading spinner (NcLoadingIcon aria-label) */\n\t\tloadingText: {\n\t\t\ttype: String,\n\t\t\tdefault: 'Loading…',\n\t\t},\n\n\t\t/** Function returning CSS class(es) for a row */\n\t\trowClass: {\n\t\t\ttype: Function,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Override label for the Add button. Defaults to \"Add {schema.title}\" */\n\t\taddLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** How many action buttons to show inline (rest go in overflow dropdown) */\n\t\tinlineActionCount: {\n\t\t\ttype: Number,\n\t\t\tdefault: 0,\n\t\t},\n\n\t\t/** Whether to show the built-in mass Import action */\n\t\tshowMassImport: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/** Whether to show the built-in mass Export action */\n\t\tshowMassExport: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/** Whether to show the built-in mass Copy button */\n\t\tshowMassCopy: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/** Whether to show the built-in mass Delete button */\n\t\tshowMassDelete: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/**\n\t\t * Opt-in flag for the native Export menu (CSV/Excel) rendered in the\n\t\t * toolbar next to the Add button. Defaults to `false` — an app must\n\t\t * explicitly enable it per page. The menu only renders when this is\n\t\t * `true` AND the resolved schema is flagged `exportable: true`; it\n\t\t * navigates the browser to OpenRegister's export leaf\n\t\t * (`GET /apps/openregister/api/objects/{register}/{schema}/export`),\n\t\t * passing the current route's query params through as filters. This\n\t\t * is distinct from the `showMassExport` mass-action, which exports\n\t\t * only the fetched/selected rows via a blob download.\n\t\t */\n\t\tallowExport: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * Opt-in flag for the saved-views control (saved-views-ui) rendered\n\t\t * in the toolbar. Defaults to `false` — an app must explicitly\n\t\t * enable it per page. When `true`, a Views dropdown lists the\n\t\t * current user's OpenRegister saved-search views\n\t\t * (`GET /apps/openregister/api/views`); applying one writes its\n\t\t * stored filters/search/sort into the route query (reusing the\n\t\t * existing deep-link contract — non-underscore keys are filters,\n\t\t * `_search` and `_order` are reserved), \"Save current view…\"\n\t\t * persists the current route-query state via POST and toasts either\n\t\t * way naming the view (a failure also keeps the dialog open with the\n\t\t * reason), and own views can be deleted after confirmation.\n\t\t */\n\t\tallowSavedViews: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * Which pages share this page's saved views. By default a view is\n\t\t * shared by every page over the same register and schema and by no\n\t\t * other page, because a view is filters over one schema's fields. Set\n\t\t * this to share views across pages over different sources, or to\n\t\t * keep two pages over the same source apart. Written into the saved\n\t\t * view's `query.scope`; views saved before scoping stay visible\n\t\t * everywhere.\n\t\t */\n\t\tsavedViewsScope: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** Property name used to display item names in dialogs */\n\t\tmassActionNameField: {\n\t\t\ttype: String,\n\t\t\tdefault: 'title',\n\t\t},\n\n\t\t/** Optional function to format item names in dialogs. Receives the item, returns a string. Overrides massActionNameField when provided. */\n\t\tnameFormatter: {\n\t\t\ttype: Function,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Available export formats for the export dialog */\n\t\texportFormats: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [\n\t\t\t\t{ id: 'excel', label: 'Excel (.xlsx)' },\n\t\t\t\t{ id: 'csv', label: 'CSV (.csv)' },\n\t\t\t],\n\t\t},\n\n\t\t/** Import option definitions for the import dialog */\n\t\timportOptions: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/** Whether to show the built-in form dialog for Add/Edit */\n\t\tshowFormDialog: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/**\n\t\t * Registry key of a `kind: 'modal'` entry that Add (and `?action=create`)\n\t\t * opens instead of the built-in form dialog. Edits keep the form dialog.\n\t\t */\n\t\tcreateModal: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** Use CnAdvancedFormDialog (properties table, JSON tab, optional metadata) instead of CnFormDialog for Add/Edit */\n\t\tuseAdvancedFormDialog: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * NcDialog size for the built-in Add/Edit form dialog.\n\t\t *\n\t\t * The dialog itself has taken a `size` since it shipped, but this page\n\t\t * never passed one, so an index page's Add form was stuck at `normal`\n\t\t * however many properties its schema declared. A manifest-declared\n\t\t * `open-form` header action, which reaches CnFormDialog through\n\t\t * CnActionButtons, could already ask for `large` — which is why the\n\t\t * same app could have a roomy create form on a detail page and a\n\t\t * cramped one on its index.\n\t\t */\n\t\tformSize: {\n\t\t\ttype: String,\n\t\t\tdefault: 'normal',\n\t\t},\n\n\t\t/**\n\t\t * How many columns the built-in Add/Edit form flows its fields into.\n\t\t *\n\t\t * Pair `2` with `formSize: 'large'`, or the two columns are merely two\n\t\t * narrow ones. Worth it once the schema asks more questions than fit on\n\t\t * a screen; below 700px CnFormDialog collapses back to one column on\n\t\t * its own, so this is safe on a narrow viewport.\n\t\t */\n\t\tformColumns: {\n\t\t\ttype: Number,\n\t\t\tdefault: 1,\n\t\t\tvalidator: (value) => value === 1 || value === 2,\n\t\t},\n\n\t\t/**\n\t\t * Seed values for the built-in create dialog. Never applied on edit:\n\t\t * both dialogs consult `initialData` / `initialValues` only when there\n\t\t * is no item. Values resolve `@me` / `@now` / `@today` at any depth;\n\t\t * `@object.*` / `@workspace.*` / `@config.*` need a context this page\n\t\t * does not carry, so they pass through unresolved.\n\t\t *\n\t\t * @type {object|null}\n\t\t */\n\t\tcreateDefaults: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Where to go after a successful create from the built-in Add dialog.\n\t\t * A route name, or `{ name, paramField?, objectParam? }`; the created\n\t\t * object's id is merged into the params. Empty stays on the list.\n\t\t *\n\t\t * @type {string|object|null}\n\t\t */\n\t\tcreateSuccessRoute: {\n\t\t\ttype: [String, Object],\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Toast shown after a successful create from the built-in Add dialog,\n\t\t * run through the host `cnTranslate`. Empty shows none.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tcreateSuccessMessage: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Opt-in async create hook. When provided, a **create** (not edit)\n\t\t * confirmed from the built-in form dialog calls\n\t\t * `await createOverride(formData, ctx)` INSTEAD of persisting via the\n\t\t * store / self-store `saveObject`. The override owns persistence\n\t\t * (e.g. an app posting through a contact-aware endpoint that fills a\n\t\t * required FK before saving to OpenRegister) and MUST return the\n\t\t * created object (a truthy value) on success; return a falsy value to\n\t\t * signal failure. The returned object is used as the created result\n\t\t * (`@create` payload + dialog success). Throwing rejects with the\n\t\t * error surfaced in the form dialog. Edits are never routed here.\n\t\t *\n\t\t * `ctx` carries `{ register, schema, objectType, effectiveSchema }`\n\t\t * so a single handler can branch per schema.\n\t\t *\n\t\t * When absent, create behaviour is unchanged (store / self-store save).\n\t\t *\n\t\t * @type {((formData: object, ctx: { register: string, schema: (object|string), objectType: string, effectiveSchema: object }) => Promise<object>)|null}\n\t\t */\n\t\tcreateOverride: {\n\t\t\ttype: Function,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Whether to add a View action to row actions. The action emits a\n\t\t * dedicated `view` event — independent of `row-click`. Bind `@view`\n\t\t * to handle \"open detail\" and `@row-click` to handle row click\n\t\t * (selection, expand, etc.); they may share a handler when the app\n\t\t * wants click-to-view, but they are conceptually distinct.\n\t\t *\n\t\t * On a named `entitySource` page the effective default is FALSE: the\n\t\t * source's own open action navigates to the detail page, which is the\n\t\t * view. Passing the prop explicitly still wins.\n\t\t */\n\t\tshowViewAction: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/**\n\t\t * Where the View row action points, as `(row) => location | null`.\n\t\t * When it returns a location, View renders as a link to it (middle\n\t\t * click, copy link) and does not emit `view`; when it returns null,\n\t\t * View stays a button that emits `view`. CnPageRenderer sets it to\n\t\t * where a row click opens.\n\t\t */\n\t\tviewTo: {\n\t\t\ttype: Function,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Whether to add an Edit action to row actions.\n\t\t *\n\t\t * On a named `entitySource` page the effective default is FALSE: a\n\t\t * source has no schema, so the form modal this action opens could only\n\t\t * ever render empty — the source declares its own Edit, which\n\t\t * navigates. Passing the prop explicitly still wins.\n\t\t */\n\t\tshowEditAction: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/**\n\t\t * Send the Edit row action to the record's detail page instead of\n\t\t * opening the edit modal, by emitting `edit-open` rather than showing\n\t\t * the form dialog.\n\t\t *\n\t\t * OFF by default, and `CnPageRenderer` no longer derives it: the host\n\t\t * binds `@edit-open` to the same navigation as a row click, so an Edit\n\t\t * that routes is an Edit that does what clicking the row does — the\n\t\t * split pane, or the detail page — while the form it names becomes\n\t\t * unreachable from the list.\n\t\t *\n\t\t * Set it when the modal genuinely cannot express the record and you\n\t\t * accept the duplication: it renders the schema's flat scalars only, so\n\t\t * related rows, sub-resources and tabs stay uneditable there.\n\t\t */\n\t\teditOpensDetail: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * Whether to add a Copy action to row actions.\n\t\t *\n\t\t * On a named `entitySource` page the effective default is whether the\n\t\t * source implements `copyRow` — without it, the copy dialog's confirm\n\t\t * has nowhere to go. Passing the prop explicitly still wins.\n\t\t */\n\t\tshowCopyAction: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/**\n\t\t * Whether to add a Delete action to row actions.\n\t\t *\n\t\t * On a named `entitySource` page the effective default is whether the\n\t\t * source implements `deleteRow` — without it, the delete dialog's\n\t\t * confirm has nowhere to go. Passing the prop explicitly still wins.\n\t\t */\n\t\tshowDeleteAction: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/** Field keys to exclude from the form dialog */\n\t\texcludeFields: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/** Field keys to include in the form dialog (whitelist mode) */\n\t\tincludeFields: {\n\t\t\ttype: Array,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Per-field overrides passed to CnFormDialog */\n\t\tfieldOverrides: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/** Whether to show the Cards/Table view toggle in the actions bar */\n\t\tshowViewToggle: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/**\n\t\t * Show an inline search field in the actions bar (in addition to / instead\n\t\t * of the sidebar search). Fed from the manifest as `pages[].config.inlineSearch`.\n\t\t */\n\t\tinlineSearch: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Placeholder for the inline search field (manifest `config.searchPlaceholder`) */\n\t\tsearchPlaceholder: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** Label for the cards view-toggle option (manifest `config.cardsLabel`, e.g. \"Tiles\") */\n\t\tcardsLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** Label for the table view-toggle option (manifest `config.tableLabel`, e.g. \"List\") */\n\t\ttableLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** MDI icon name for the cards view-toggle option (manifest `config.cardsIcon`) */\n\t\tcardsIcon: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** MDI icon name for the table view-toggle option (manifest `config.tableIcon`) */\n\t\ttableIcon: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** Label for the list view-toggle option (manifest `config.listLabel`, e.g. \"Rows\") */\n\t\tlistLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Show a standalone sort dropdown in the actions bar (for card/list\n\t\t * views without sortable headers). Emits `@sort-change` with the value.\n\t\t * Fed from the manifest as `pages[].config.showSortSelect`.\n\t\t */\n\t\tshowSortSelect: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * Options for the standalone sort dropdown (manifest `config.sortSelectOptions`).\n\t\t *\n\t\t * @type {Array<{ value: string, label: string }>}\n\t\t */\n\t\tsortSelectOptions: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/** Selected value of the standalone sort dropdown (controlled). */\n\t\tsortSelectValue: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** MDI icon name for the list view-toggle option (manifest `config.listIcon`) */\n\t\tlistIcon: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Field mapping for the default list-view rows (CnObjectRow). Overrides\n\t\t * the schema-configuration defaults. Fed from the manifest as\n\t\t * `pages[].config.listConfig`.\n\t\t *\n\t\t * @type {{ titleField?: string, subtitleField?: string, imageField?: string, iconField?: string, iconName?: string, badgeField?: string, badgeVariantField?: string, badgeVariant?: string, badgeColorMap?: object }}\n\t\t */\n\t\tlistConfig: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * Opt-in folder navigation pane (rendered left of the list). Selecting a\n\t\t * folder filters the list by `filterField` (via the self-fetch filter);\n\t\t * \"All\" clears it. Fed from the manifest as `pages[].config.folderSidebar`.\n\t\t *\n\t\t * - `source` — `'register'` (fetch the folder list from an OpenRegister\n\t\t * register/schema), `'field'` (distinct values of the current rows'\n\t\t * `field`), `'custom'` (use `folders`), or `'files'` (NC folders).\n\t\t * - `register` / `schema` — the OR source for `source:'register'`.\n\t\t * - `idField` / `nameField` — which folder object props map to the folder\n\t\t * id (the filter value) and display name. Dotted paths supported.\n\t\t * - `field` / `filterField` — the row field to group/filter by (filterField\n\t\t * defaults to `field`).\n\t\t * - `folders` — explicit folder list for `source:'custom'`.\n\t\t * - `allLabel` / `title` / `allowCreate` — passed to CnFolderSidebar.\n\t\t *\n\t\t * A folder entry is also a SCOPE: it may carry `columns`, `defaultSort`\n\t\t * and `searchFields`, and while it is selected the list is shown that\n\t\t * way. `columns` takes the same shapes as the `columns` prop, and the\n\t\t * page's own `columns` still decides which columns this page has, so a\n\t\t * scope naming one the page does not declare is dropped rather than\n\t\t * rendered. A `source: 'register'` folder list reads the same three\n\t\t * keys off each row's `x-index` block, which the folder entry's own\n\t\t * value wins over, key by key.\n\t\t *\n\t\t * @type {object}\n\t\t */\n\t\tfolderSidebar: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Where a row carries the actions the server says this caller may run\n\t\t * on it: a dotted path, read off the rows the list already fetched, so\n\t\t * ninety rows cost one request rather than ninety.\n\t\t *\n\t\t * A row's menu is then the INTERSECTION of what this page declares and\n\t\t * what the server allows. An action the server allows but this page has\n\t\t * stopped declaring stays out, so a row cannot bring back a button the\n\t\t * page removed. An action the page declares but the server refuses\n\t\t * stays out too, and its reason is available from\n\t\t * `rowActionRefusal(row, action)`.\n\t\t *\n\t\t * A row carrying nothing at this path is a server that does not answer\n\t\t * about actions, and the page's declaration stands unchanged.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\trowActionField: {\n\t\t\ttype: String,\n\t\t\tdefault: DEFAULT_ROW_ACTION_FIELD,\n\t\t},\n\n\t\t/**\n\t\t * State indicators this page declares for its rows, handed straight to\n\t\t * CnDataTable. Each entry is `{ id, field, equals?, in?, icon, text,\n\t\t * tooltip? }`. The page declares which indicators exist; a record\n\t\t * cannot add one the page has not declared. A page declaring none\n\t\t * renders its rows as before. Fed from the manifest as\n\t\t * `pages[].config.rowIndicators`.\n\t\t *\n\t\t * @type {Array<object>}\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 before the\n\t\t * rest move into the row menu. Fed from the manifest as\n\t\t * `pages[].config.rowIndicatorCap`.\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 * The fields a quick edit asks for, opened from a row without leaving\n\t\t * the list. Empty means no quick edit. The page names the fields; a\n\t\t * record cannot open a form on one the page did not name. Fed from the\n\t\t * manifest as `pages[].config.quickEditFields`.\n\t\t *\n\t\t * @type {string[]}\n\t\t */\n\t\tquickEditFields: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * Where a record lists the fields this caller may write. A field the\n\t\t * page named that is not in that list renders read-only rather than\n\t\t * missing, so a person sees the value and learns it is not theirs to\n\t\t * change. A row carrying nothing there leaves every named field\n\t\t * editable.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\twritableField: {\n\t\t\ttype: String,\n\t\t\tdefault: '@self.writableFields',\n\t\t},\n\n\t\t/**\n\t\t * Saved view ids this page renders as tabs instead of as entries in the\n\t\t * views control. A view appears in one place or the other, never both.\n\t\t * An id naming a view that no longer exists produces no tab.\n\t\t *\n\t\t * @type {string[]}\n\t\t */\n\t\tviewTabs: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * The teams this person may claim. The instance decides this list; it\n\t\t * is the membership side of `claimedTeams`.\n\t\t *\n\t\t * @type {Array<string|object>}\n\t\t */\n\t\tofferedTeams: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * The teams this person stored as claimed, read as a personal\n\t\t * preference alongside the others. It is narrowed to `offeredTeams`, so\n\t\t * a team they claimed before it was taken away is passed over rather\n\t\t * than honoured.\n\t\t *\n\t\t * @type {string[]}\n\t\t */\n\t\tclaimedTeams: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * The field carrying a record's DERIVED priority, and the order its\n\t\t * values rank in, lowest first. The list reads the value and sorts on\n\t\t * it; it never computes one. A record with no priority sorts after\n\t\t * every ranked one, in both directions, and stays in the list.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tpriorityField: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * The priority values in rank order, lowest first, e.g.\n\t\t * `['low', 'medium', 'high']`. A value outside this list is unranked.\n\t\t *\n\t\t * @type {string[]}\n\t\t */\n\t\tpriorityLevels: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * Offer the list's keyboard shortcuts. Every one of them is also listed\n\t\t * in the command palette and on the help key, because a shortcut nobody\n\t\t * can find does not count.\n\t\t */\n\t\tlistShortcuts: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * Show a filter menu (funnel button) in the table header, above the\n\t\t * row-actions column. Its menu lists every enum/badge column's values as\n\t\t * toggleable facet filters — a compact alternative to the facet sidebar.\n\t\t * Fed from the manifest as `pages[].config.filterMenu`.\n\t\t */\n\t\tfilterMenu: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * Show a column menu (columns button) in the table header, above the\n\t\t * row-actions column. Its menu lists every governed column as a toggleable\n\t\t * checkbox — a compact, in-table alternative to the sidebar's Columns tab,\n\t\t * so the sidebar space can be reclaimed. Fed from the manifest as\n\t\t * `pages[].config.columnMenu`.\n\t\t */\n\t\tcolumnMenu: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Whether the refresh action is currently in progress */\n\t\trefreshing: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * Whether to auto-subscribe to live collection updates in self-fetch\n\t\t * mode. Defaults to true. When `register` + `schema` are set (and no\n\t\t * `objects` prop is passed), the page subscribes to the collection's\n\t\t * `or-collection-{register}-{schema}` scope on mount and refetches\n\t\t * (coalesced — events are hints) when an update event arrives; the\n\t\t * subscription is released on unmount. Set `false` (manifest:\n\t\t * `config.subscribe: false`) for static / read-once views. No-op in\n\t\t * consumer-managed mode (an `objects` prop was passed) and on stores\n\t\t * without live-updates support.\n\t\t *\n\t\t * @type {boolean}\n\t\t */\n\t\tsubscribe: { // eslint-disable-line vue/no-unused-properties -- read by useSelfFetchList.js off the props object, which this rule does not follow.\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/** Whether the refresh action is disabled (e.g. when required selections are missing) */\n\t\trefreshDisabled: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Whether the Add button is disabled (e.g. when required selections are missing) */\n\t\taddDisabled: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Whether to show the Add button in the actions bar */\n\t\tshowAdd: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/**\n\t\t * Store instance for automatic save integration. When provided alongside\n\t\t * objectType, the form dialog saves directly to the store instead of\n\t\t * emitting create/edit events. The object type must already be registered\n\t\t * in the store via registerObjectType() before passing the store here.\n\t\t */\n\t\tstore: { type: Object, default: null },\n\t\t/**\n\t\t * Object type slug for store integration (e.g. `${registerId}-${schemaId}`).\n\t\t * Required when store is set — a console warning is emitted if missing.\n\t\t */\n\t\tobjectType: { type: String, default: '' },\n\t\t/**\n\t\t * Manifest-driven sidebar configuration. When set with\n\t\t * `enabled: true`, CnIndexPage auto-mounts an embedded\n\t\t * CnIndexSidebar wired to the page's schema, search, columns,\n\t\t * and facet props. When unset or `enabled: false`, the\n\t\t * legacy slot-based interface is preserved — consumers\n\t\t * mount their own CnIndexSidebar at the App.vue level.\n\t\t *\n\t\t * Shape:\n\t\t * - `enabled` (boolean) — **existence gate**. Whether the\n\t\t * page configures an embedded sidebar at all. When `false`\n\t\t * or unset, the auto-mount path is bypassed (no\n\t\t * `<CnIndexSidebar>` rendered) and the consumer's slot\n\t\t * pattern stays active.\n\t\t * - `show` (boolean, default `true`) — **visibility gate**.\n\t\t * Even when `enabled: true`, `show: false` SUPPRESSES\n\t\t * rendering for this page so manifest authors can hide\n\t\t * the sidebar declaratively without removing the config.\n\t\t * Distinct from `enabled` so config can be retained\n\t\t * (e.g. for a watcher / responsive layout) while the\n\t\t * visible surface is hidden.\n\t\t * - `columnGroups` (array) — extra column groups beyond schema + Metadata.\n\t\t * - `facets` (object) — live facet data { fieldName: { values: [...] } }.\n\t\t * - `showMetadata` (boolean) — include the built-in Metadata column group (defaults true).\n\t\t * - `search` (object) — search-related label overrides forwarded to CnIndexSidebar.\n\t\t *\n\t\t * @type {{ enabled: boolean, show?: boolean, columnGroups?: Array, facets?: object, showMetadata?: boolean, search?: object }|null}\n\t\t */\n\t\tsidebar: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Current search term (forwarded to the embedded sidebar when sidebar.enabled). */\n\t\tsearchValue: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** Currently visible column keys (forwarded to the embedded sidebar). */\n\t\tvisibleColumns: {\n\t\t\ttype: Array,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Currently active facet filters: { fieldName: [values] } (forwarded to the embedded sidebar). */\n\t\tactiveFilters: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * Effective register slug for the page. Forwarded as a prop to\n\t\t * the resolved card component (when `cardComponent` is set) so\n\t\t * bespoke card UIs can match the schema → register pair.\n\t\t *\n\t\t * Manifest-driven path: `pages[].config.register` flows in via\n\t\t * CnPageRenderer's `v-bind=\"resolvedProps\"` spread.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tregister: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Optional name of a consumer-provided card component (registered\n\t\t * in the `customComponents` registry on `CnAppRoot`) to render in\n\t\t * place of the default `CnObjectCard` when the page is in\n\t\t * card-grid view mode.\n\t\t *\n\t\t * Resolution priority (highest first):\n\t\t * 1. The parent's `#card` scoped slot (always wins).\n\t\t * 2. The component resolved from `cardComponent` against the v2\n\t\t * `registry` (any kind carrying a `component`), then the legacy\n\t\t * customComponents map.\n\t\t * 3. The library default (`CnObjectCard`).\n\t\t *\n\t\t * Unknown names log `console.warn` once and fall back to the\n\t\t * default so a misconfigured manifest never blanks the grid.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tcardComponent: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Name of a custom row component for list view, resolved against the v2\n\t\t * `registry` and then the legacy customComponents map (manifest\n\t\t * `pages[].config.listComponent`).\n\t\t * Same resolution priority as `cardComponent`: the `#list-item` slot\n\t\t * wins, then this component, then the default `CnObjectRow`. Unknown\n\t\t * names warn once and fall back to the default.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tlistComponent: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Optional explicit customComponents registry. When set, this\n\t\t * overrides the registry injected from `CnAppRoot` via\n\t\t * `cnCustomComponents`. Provided primarily so unit tests can\n\t\t * pass a registry without mounting `CnAppRoot`.\n\t\t *\n\t\t * Used by:\n\t\t * - `cardComponent` resolution (REQ-MCI from manifest-card-index)\n\t\t * - `actions[].handler` registry name resolution (REQ-MAD-3 from\n\t\t * manifest-actions-dispatch — handler funcs called on row-action click)\n\t\t *\n\t\t * @type {object|null}\n\t\t */\n\t\tcustomComponents: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Manifest-driven page-level actions rendered inside the\n\t\t * NcActions overflow dropdown between Refresh and the\n\t\t * `#action-items` slot. Each entry is\n\t\t * `{ id, label, icon?, handler?, route?, disabled? }`. The\n\t\t * `handler` field mirrors the row-level\n\t\t * `actions[].handler` pattern: a function, the keyword\n\t\t * `'navigate'`, `'emit'`, `'none'`, or a string name looked\n\t\t * up in the v2 `registry` (a `kind: 'handler'` entry) and\n\t\t * then in the legacy `customComponents`. The page\n\t\t * dispatches the resolved handler via `onHeaderAction` AND\n\t\t * (unless the handler is the `'none'` keyword) emits\n\t\t * `@header-action({ action: id, id })`.\n\t\t *\n\t\t * Reserved ids (those used by built-ins on the bar — `refresh`,\n\t\t * `import`, `export`, `copy`, `delete`) are dropped from the\n\t\t * merged list with a `console.warn`.\n\t\t *\n\t\t * @type {Array<{ id: string, label: string, icon?: string, handler?: string|(() => void), route?: string, disabled?: boolean }>}\n\t\t */\n\t\theaderActions: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * Declarative bulk actions for the contextual selection strip.\n\t\t *\n\t\t * The strip has had a `#selection-actions` slot for a while, but a slot\n\t\t * can only be filled by a hand-written host component — so a page\n\t\t * declared in an app manifest could carry row actions and header\n\t\t * actions and never a bulk one. This prop is the missing vocabulary.\n\t\t *\n\t\t * Handler resolution mirrors `headerActions[]`, with one difference\n\t\t * that is the whole point: the handler is called with the SELECTION.\n\t\t * A bulk handler that has to go and find out what was selected is a\n\t\t * bulk handler waiting to disagree with the strip that invoked it.\n\t\t *\n\t\t * Reserved ids (`copy`, `delete`) are dropped with a warning — the\n\t\t * strip already ships those two as built-ins, and a second button with\n\t\t * the same name doing something else is worse than no button.\n\t\t *\n\t\t * @type {Array<{ id: string, label: string, icon?: string, handler?: string|((scope: { actionId: string, selectedIds: Array<string>, count: number }) => void), target?: string, props?: object, disabled?: boolean }>}\n\t\t */\n\t\tbulkActions: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * Active organisation entity (multi-tenancy-context). When\n\t\t * bound from a tenant-switcher higher in the tree, CnIndexPage\n\t\t * watches it and calls `store.setActiveTenantOrganisation(uuid)`\n\t\t * so the next fetchCollection() stamps the new tenant header\n\t\t * and the in-memory caches are cleared. When the prop is unset\n\t\t * the legacy single-tenant behaviour is preserved exactly.\n\t\t *\n\t\t * @type {object|null}\n\t\t */\n\t\tactiveOrganisation: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * When set, adds a Documentation entry to the Actions overflow\n\t\t * (after Refresh). Opens the URL in a new tab. Empty hides it.\n\t\t */\n\t\tdocumentationUrl: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** Label for the Documentation overflow entry. */\n\t\tdocumentationLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\t},\n\n\temits: [\n\t\t'action',\n\t\t'add',\n\t\t'apply-view',\n\t\t'board-move',\n\t\t'bulk-action',\n\t\t'clear-filters',\n\t\t'columns-change',\n\t\t'quick-edit-save',\n\t\t'configure',\n\t\t'copy',\n\t\t'create',\n\t\t'delete',\n\t\t'edit-open',\n\t\t'filter-change',\n\t\t'folder-change',\n\t\t'folder-create',\n\t\t'header-action',\n\t\t'mass-copy',\n\t\t'open-modal',\n\t\t'mass-delete',\n\t\t'mass-export',\n\t\t'mass-import',\n\t\t'page-changed',\n\t\t'page-size-changed',\n\t\t'quick-filter-change',\n\t\t'refresh',\n\t\t'row-click',\n\t\t'row-aux-click',\n\t\t'search',\n\t\t'select',\n\t\t'sort',\n\t\t'sort-change',\n\t\t'view',\n\t\t'view-mode-change',\n\t\t'split-close',\n\t\t'split-saved',\n\t\t'manual-order-change',\n\t],\n\n\tsetup(props) {\n\t\tconst {\n\t\t\tisOpen: contextMenuOpen,\n\t\t\ttargetItem: contextMenuRow,\n\t\t\topen: openContextMenu,\n\t\t\tclose: closeContextMenu,\n\t\t} = useContextMenu()\n\n\t\tconst {\n\t\t\tisSelfFetch,\n\t\t\tlist,\n\t\t\tselfObjectStore,\n\t\t\tselfObjectType,\n\t\t\tactiveQuickFilterIndex,\n\t\t\tselectedQuickFilterIndices,\n\t\t\tselfFetchTokenCtx,\n\t\t\tinitialQueryFilterKeys,\n\t\t} = useSelfFetchList(props, getCurrentInstance(), inject)\n\n\t\t// The sidebar's chosen values on a NAMED-SOURCE page. Self-fetch keeps\n\t\t// its own in `useSelfFetchList`; a named source had nowhere to put\n\t\t// them, so `activeFilters` was a prop only a consumer could fill and a\n\t\t// manifest page's sidebar rendered controls that narrowed nothing.\n\t\t//\n\t\t// Seeded from the route query so a shared link lands filtered, the\n\t\t// same way `_order` and a deep-link filter already do for self-fetch.\n\t\tconst namedActiveFilters = ref(namedFiltersFromRoute(getCurrentInstance(), props))\n\n\t\tconst {\n\t\t\tisNamedSource,\n\t\t\tnamedSource,\n\t\t\tnamedRows,\n\t\t\tnamedLoading,\n\t\t\tnamedQuickFilters,\n\t\t} = useNamedSource(props, { activeQuickFilterIndex, activeFilters: namedActiveFilters })\n\n\t\treturn {\n\t\t\tisNamedSource,\n\t\t\tnamedSource,\n\t\t\tnamedRows,\n\t\t\tnamedLoading,\n\t\t\tnamedQuickFilters,\n\t\t\tnamedActiveFilters,\n\t\t\tcontextMenuOpen,\n\t\t\tcontextMenuRow,\n\t\t\topenContextMenu,\n\t\t\tcloseContextMenu,\n\t\t\tisSelfFetch,\n\t\t\tlist,\n\t\t\tselfObjectStore,\n\t\t\tselfObjectType,\n\t\t\tactiveQuickFilterIndex,\n\t\t\tselectedQuickFilterIndices,\n\t\t\tselfFetchTokenCtx,\n\t\t\tinitialQueryFilterKeys,\n\t\t}\n\t},\n\n\tdata() {\n\t\treturn {\n\t\t\tcurrentViewMode: this.viewMode,\n\t\t\t/**\n\t\t\t * The non-`_` query keys this page owns, i.e. may clear on the next\n\t\t\t * persist. Seeded with what it adopted from the query on load, and\n\t\t\t * replaced by what it writes; a key it never claimed is somebody\n\t\t\t * else's and is left in the address bar untouched.\n\t\t\t *\n\t\t\t * @type {Array<string>}\n\t\t\t */\n\t\t\tpersistedFilterKeys: [...(this.initialQueryFilterKeys || [])],\n\t\t\tinternalSelectedIds: [...this.selectedIds],\n\t\t\t// Folder-sidebar state: selected folder id + the register-fetched list.\n\t\t\tselectedFolderId: null,\n\t\t\t/**\n\t\t\t * The grouping field's facet as last seen with NO folder selected:\n\t\t\t * the whole set of folders, kept on screen while one is selected.\n\t\t\t *\n\t\t\t * @type {Array<object>}\n\t\t\t */\n\t\t\tfolderSidebarAllValues: [],\n\t\t\tfolderRegisterList: [],\n\t\t\t// `x-index` blocks read off the schema rows a `source: 'register'`\n\t\t\t// folder list was built from, keyed by folder id. The layout a case\n\t\t\t// type carries travels with the record rather than being restated\n\t\t\t// in every manifest that lists it.\n\t\t\tfolderRowLayouts: {},\n\t\t\t/** The row the keyboard is on, or -1. */\n\t\t\tfocusedRowIndex: -1,\n\t\t\t/** The row a quick edit is open on, or null. */\n\t\t\tquickEditRow: null,\n\t\t\t/** The row as the server holds it, after a stale save. */\n\t\t\tquickEditServerRow: null,\n\t\t\t/** Whether the shortcut help sheet is open. */\n\t\t\tshowShortcutHelp: false,\n\t\t\t// Mass action dialogs\n\t\t\tshowMassDeleteDialog: false,\n\t\t\tshowMassCopyDialog: false,\n\t\t\tshowExportDialog: false,\n\t\t\tshowImportDialog: false,\n\t\t\t// Single-object dialogs\n\t\t\tshowSingleDeleteDialog: false,\n\t\t\tshowSingleCopyDialog: false,\n\t\t\tshowFormDialogVisible: false,\n\t\t\t// Dialog targets\n\t\t\tactionTargetItem: null,\n\t\t\teditItem: null,\n\t\t\t// Drives the Actions-menu Refresh spinner during a self-fetch\n\t\t\t// refresh, where the host has no promise to bind `:refreshing` to.\n\t\t\tinternalRefreshing: false,\n\t\t\t// Search/Columns sidebar open state. Defaults closed so the page\n\t\t\t// content (table / cards) starts at the top and fills the width;\n\t\t\t// opened on demand via the actions-bar toggle.\n\t\t\tsidebarOpen: false,\n\t\t\t// Saved views (saved-views-ui): the fetched view list, its\n\t\t\t// loading flag, the save-dialog toggle, and the view awaiting\n\t\t\t// delete confirmation.\n\t\t\tsavedViews: [],\n\t\t\tsavedViewsLoading: false,\n\t\t\tshowSaveViewDialog: false,\n\t\t\tviewPendingDelete: null,\n\t\t\t// Split view (case-page-and-list-as-a-place). `splitRowPatches` holds\n\t\t\t// records saved in the pane, keyed by row id, so a save lands on the\n\t\t\t// row without refetching the page and losing the scroll position.\n\t\t\t// `splitViewportWidth` is measured rather than guessed, so the narrow\n\t\t\t// fallback follows a window resize and not only a reload.\n\t\t\tsplitRowPatches: {},\n\t\t\t// Measured here rather than in `mounted`, so the FIRST render is\n\t\t\t// already the right layout. Measuring after the mount rendered\n\t\t\t// the split pane once on a phone and then swapped it for the full\n\t\t\t// page, which reads as the page flickering on every open.\n\t\t\tsplitViewportWidth: (typeof window !== 'undefined' && Number.isFinite(window.innerWidth)) ? window.innerWidth : 0,\n\t\t\tsplitScrollTop: 0,\n\t\t\t// Manual order: the row ids this person dragged into an order, read\n\t\t\t// from and written to their own preferences. Never on the records.\n\t\t\tmanualOrderIds: [],\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * The empty-state copy run through the host translate function\n\t\t * (the injected `cnTranslate`, identity by default). Only this\n\t\t * component's OWN `NcEmptyContent` uses it — the raw `emptyText`\n\t\t * keeps flowing to CnDataTable / CnObjectList / CnCardGrid, which\n\t\t * translate at their own render boundary, so the string is never\n\t\t * run through the translator twice.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tresolvedEmptyText() {\n\t\t\tconst fn = typeof this.cnTranslate === 'function' ? this.cnTranslate : (k) => k\n\t\t\treturn this.emptyText ? fn(this.emptyText) : this.emptyText\n\t\t},\n\n\t\t/**\n\t\t * Whether the host manifest editor is in edit mode (unwraps the injected\n\t\t * `cnEditingBody`, which may be a Vue ref or a plain boolean). Drives the\n\t\t * in-header config cog.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\tisEditMode() {\n\t\t\tconst e = this.cnEditingBody\n\t\t\treturn !!(e && typeof e === 'object' && 'value' in e ? e.value : e)\n\t\t},\n\n\t\t/**\n\t\t * The same map as `effectiveCustomComponents`, under the name the\n\t\t * header-action and bulk-action paths have always used. One of the two\n\t\t * computeds has to be the other, or a change to the resolution order\n\t\t * lands on half the surfaces.\n\t\t *\n\t\t * @return {object}\n\t\t */\n\t\tresolvedCustomComponents() {\n\t\t\treturn this.effectiveCustomComponents\n\t\t},\n\n\t\t/**\n\t\t * Whether a row click opens the row rather than selecting it:\n\t\t * `rowClickToView` is set AND something can open it (a `row-click`\n\t\t * listener, or a named source that routes its own rows). Otherwise a\n\t\t * click on a selectable page selects, so it is never dead.\n\t\t *\n\t\t * `$.vnode.props` is not reactive, so a `row-click` listener attached\n\t\t * or removed after mount does not re-evaluate this.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\trowClickOpens() {\n\t\t\tif (!this.rowClickToView) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tif (this.isNamedSource && (this.rowRoute\n\t\t\t\t|| typeof this.namedSource?.openRow === 'function'\n\t\t\t\t|| this.namedSource?.detailRoute)) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\treturn !!this.$.vnode.props?.onRowClick\n\t\t},\n\n\t\t/**\n\t\t * Merged page-level header actions: drops reserved ids and\n\t\t * resolves declarative `handler` keywords (`navigate` / `emit`\n\t\t * / `none` / registry name) to either a function or\n\t\t * no-handler (emit-only) entries. Function-typed handlers are\n\t\t * passed through untouched. The `none` keyword sets\n\t\t * `_dispatchSuppress: true` and provides a no-op function so\n\t\t * the bar still shows a clickable item.\n\t\t *\n\t\t * @return {Array<object>}\n\t\t */\n\t\tmergedHeaderActions() {\n\t\t\tconst reserved = new Set(['refresh', 'import', 'export', 'copy', 'delete'])\n\t\t\tconst merged = []\n\t\t\tfor (const entry of this.headerActions || []) {\n\t\t\t\tif (entry && reserved.has(entry.id)) {\n\t\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\t\tconsole.warn(`CnIndexPage: headerActions[].id \"${entry.id}\" is reserved by the built-in bar; dropping entry`)\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tmerged.push(this.resolveHeaderHandler(entry))\n\t\t\t}\n\t\t\treturn merged\n\t\t},\n\n\t\t/**\n\t\t * Where the Add button links to: the named source's `addRoute`, when\n\t\t * that is what Add does (no host `@add` listener) and the router can\n\t\t * resolve it. The bar then renders Add as a real link; null otherwise.\n\t\t *\n\t\t * `$.vnode.props` is not reactive, so an `add` listener attached or\n\t\t * removed after mount does not re-evaluate this.\n\t\t *\n\t\t * @return {string|object|null}\n\t\t */\n\t\taddLinkTo() {\n\t\t\tif (\n\t\t\t\tthis.$.vnode.props?.onAdd\n\t\t\t\t|| !this.isNamedSource\n\t\t\t\t|| !this.namedSource\n\t\t\t\t|| !this.namedSource.addRoute\n\t\t\t) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\treturn routeHref(this.namedSource.addRoute, this.$router) ? this.namedSource.addRoute : null\n\t\t},\n\n\t\t/**\n\t\t * Declarative bulk actions, validated and normalised.\n\t\t *\n\t\t * `copy` and `delete` are reserved: the selection strip already ships\n\t\t * both as built-ins, so a declared action of the same name would put a\n\t\t * second button with the same word next to one that does something\n\t\t * else.\n\t\t *\n\t\t * Unlike `mergedHeaderActions` this does NOT pre-bind the handler.\n\t\t * A bulk handler needs the selection, and the selection is only known\n\t\t * at click time — binding it here would capture whatever was selected\n\t\t * when the list last re-rendered.\n\t\t *\n\t\t * @return {Array<object>}\n\t\t */\n\t\tmergedBulkActions() {\n\t\t\tconst reserved = new Set(['copy', 'delete'])\n\t\t\tconst merged = []\n\t\t\tfor (const entry of this.bulkActions || []) {\n\t\t\t\tif (!entry || typeof entry !== 'object' || !entry.id || !entry.label) {\n\t\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\t\tconsole.warn(`[CnIndexPage] Ignoring bulkActions entry ${JSON.stringify(entry)}: a bulk action needs an id and a label.`)\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tif (reserved.has(entry.id)) {\n\t\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\t\tconsole.warn(`CnIndexPage: bulkActions[].id \"${entry.id}\" is reserved by the built-in selection strip; dropping entry`)\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tmerged.push(entry)\n\t\t\t}\n\t\t\treturn merged\n\t\t},\n\n\t\t// ── Self-fetch ↔ consumer-managed: the \"effective\" source of each\n\t\t// list datum is the useListView instance in self-fetch mode, the\n\t\t// prop otherwise. The template binds to these.\n\t\t/** True when self-fetch mode is active and the useListView instance exists. */\n\t\tisSelfFetchMode() {\n\t\t\treturn this.isSelfFetch && !!this.list\n\t\t},\n\n\t\t/** Rows: store collection in self-fetch mode, else the `objects` prop. */\n\t\teffectiveObjects() {\n\t\t\tif (this.isNamedSource) {\n\t\t\t\treturn this.namedRows\n\t\t\t}\n\t\t\treturn this.isSelfFetchMode ? (this.list.objects.value || []) : this.objects\n\t\t},\n\n\t\t/**\n\t\t * Rows handed to the table / card grid — `effectiveObjects` re-sorted by\n\t\t * the declarative `defaultSort` spec whenever no explicit user column\n\t\t * sort is active. A live `sortKey` (user clicked a header, or one was\n\t\t * passed in) takes over and this falls through to the unsorted rows so\n\t\t * the server / prop order wins. No-op when `defaultSort` is empty.\n\t\t *\n\t\t * @return {object[]}\n\t\t * @spec openspec/changes/case-page-and-list-as-a-place/specs/index-page/spec.md\n\t\t */\n\t\tdisplayObjects() {\n\t\t\t// The pane's saves and this person's own order are applied over\n\t\t\t// the loaded rows, in that order, so neither needs a refetch and\n\t\t\t// neither writes anything onto the records.\n\t\t\tconst patched = applyRowPatches(this.sortedObjects, this.splitRowPatches, this.rowKey)\n\t\t\treturn this.manualOrderActive\n\t\t\t\t? applyManualOrder(patched, this.manualOrderIds, this.rowKey)\n\t\t\t\t: patched\n\t\t},\n\n\t\t/**\n\t\t * The rows in the order the server, the props or `defaultSort` put\n\t\t * them. Split out of `displayObjects` so the pane's patches and this\n\t\t * person's manual order stack on top of one sorted list rather than\n\t\t * each re-deriving it.\n\t\t *\n\t\t * @return {object[]}\n\t\t * @spec openspec/changes/case-page-and-list-as-a-place/specs/index-page/spec.md\n\t\t */\n\t\tsortedObjects() {\n\t\t\tconst spec = this.effectiveDefaultSort\n\t\t\tif (!spec || spec.length === 0) {\n\t\t\t\treturn this.effectiveObjects\n\t\t\t}\n\t\t\tif (this.effectiveSortKey) {\n\t\t\t\treturn this.effectiveObjects\n\t\t\t}\n\t\t\treturn multiKeySort(this.effectiveObjects, spec)\n\t\t},\n\n\t\t/**\n\t\t * Whether this page declares a working split view.\n\t\t *\n\t\t * @return {boolean}\n\t\t * @spec openspec/changes/case-page-and-list-as-a-place/specs/index-page/spec.md\n\t\t */\n\t\tsplitViewEnabled() {\n\t\t\treturn this.splitView?.enabled === true\n\t\t},\n\n\t\t/**\n\t\t * Which of the three layouts the page is in: the plain list, the list\n\t\t * with the pane beside it, or the record on its own below the\n\t\t * breakpoint.\n\t\t *\n\t\t * @return {'list'|'split'|'detail'}\n\t\t * @spec openspec/changes/case-page-and-list-as-a-place/specs/index-page/spec.md\n\t\t */\n\t\tsplitLayout() {\n\t\t\treturn splitLayoutFor({\n\t\t\t\tenabled: this.splitViewEnabled,\n\t\t\t\tsplitId: this.splitId || null,\n\t\t\t\tviewportWidth: this.splitViewportWidth,\n\t\t\t\tbreakpoint: this.splitView?.breakpoint,\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * The pane's CSS width, falling back when the page declared something\n\t\t * the browser would drop.\n\t\t *\n\t\t * @return {string}\n\t\t * @spec openspec/changes/case-page-and-list-as-a-place/specs/index-page/spec.md\n\t\t */\n\t\tsplitPaneWidth() {\n\t\t\treturn normalisePaneWidth(this.splitView?.paneWidth)\n\t\t},\n\n\t\t/**\n\t\t * Whether to draw the pane's close link. Needs a router: closing pushes\n\t\t * the list route, so without one it would do nothing.\n\t\t *\n\t\t * @return {boolean}\n\t\t * @spec openspec/changes/case-page-and-list-as-a-place/specs/index-page/spec.md\n\t\t */\n\t\tsplitCloseVisible() {\n\t\t\treturn this.splitCloseButton && Boolean(this.$router)\n\t\t},\n\n\t\t/**\n\t\t * Accessible name and tooltip for the pane's close link.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tsplitCloseTitle() {\n\t\t\treturn this.splitCloseLabel || t('nextcloud-vue', 'Close')\n\t\t},\n\n\t\t/**\n\t\t * The list's own address. Falls back to `#` rather than no href: an\n\t\t * `<a>` without one is not focusable.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tsplitCloseHref() {\n\t\t\tconst target = this.splitCloseRoute || this.$route?.meta?.cnPageId || null\n\t\t\tif (!target || typeof this.$router?.resolve !== 'function') {\n\t\t\t\treturn '#'\n\t\t\t}\n\t\t\ttry {\n\t\t\t\treturn this.$router.resolve({ name: target, query: this.$route?.query || {} }).href || '#'\n\t\t\t} catch {\n\t\t\t\t// An unknown route name throws rather than answering.\n\t\t\t\treturn '#'\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Whether a manual row order applies right now. A search, a filter or\n\t\t * a sort the person just chose all mean they asked for a different\n\t\t * order, and a held order that quietly overruled it would read as the\n\t\t * list ignoring them.\n\t\t *\n\t\t * @return {boolean}\n\t\t * @spec openspec/changes/case-page-and-list-as-a-place/specs/index-page/spec.md\n\t\t */\n\t\tmanualOrderActive() {\n\t\t\treturn this.manualOrder === true\n\t\t\t\t&& this.manualOrderIds.length > 0\n\t\t\t\t&& !this.effectiveSortKey\n\t\t},\n\n\t\t/**\n\t\t * Whether the `map` segment is offered in the view toggle. An explicit\n\t\t * `viewModes` whitelist wins; otherwise map is available iff `mapConfig`\n\t\t * carries at least one field. Cards/table are always available.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\tshowMapSegment() {\n\t\t\tif (Array.isArray(this.viewModes)) {\n\t\t\t\treturn this.viewModes.includes('map')\n\t\t\t}\n\t\t\treturn Object.keys(this.mapConfig || {}).length > 0\n\t\t},\n\n\t\t/**\n\t\t * The base view-toggle segments (cards/table/list) passed to CnActionsBar.\n\t\t * Beta's explicit `viewModes` whitelist wins when set; otherwise the\n\t\t * `availableViewModes` list. Map is excluded here — it's added by the\n\t\t * actions bar via the `showMap` bridge driven by `showMapSegment`.\n\t\t *\n\t\t * @return {Array<string>} The ordered toggle segments minus 'map'.\n\t\t */\n\t\teffectiveToggleModes() {\n\t\t\tconst list = (Array.isArray(this.viewModes) && this.viewModes.length)\n\t\t\t\t? this.viewModes\n\t\t\t\t: this.availableViewModes\n\n\t\t\t// 🔴 A SEGMENT ONLY WHEN THE MODE CAN ACTUALLY WORK. A board needs\n\t\t\t// a status field to build its columns from and a date axis needs\n\t\t\t// both dates; offering a segment that opens a view saying it\n\t\t\t// cannot be one is worse than not offering it, because the reader\n\t\t\t// has to click it to find out.\n\t\t\treturn list.filter((mode) => {\n\t\t\t\tif (mode === 'map') {\n\t\t\t\t\treturn false\n\t\t\t\t}\n\t\t\t\tif (mode === 'board') {\n\t\t\t\t\treturn Boolean(this.board?.statusField)\n\t\t\t\t}\n\t\t\t\tif (mode === 'dateAxis') {\n\t\t\t\t\treturn Boolean(this.dateAxis?.startField) && Boolean(this.dateAxis?.endField)\n\t\t\t\t}\n\t\t\t\treturn true\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * The status field's schema, for the board's columns.\n\t\t *\n\t\t * Read from the schema this page already holds rather than fetched: the\n\t\t * stages are a property of the type, and a second read of them could\n\t\t * disagree with the one the table is rendering from.\n\t\t *\n\t\t * @return {?object} The field schema, or null.\n\t\t */\n\t\tboardStatusFieldSchema() {\n\t\t\tconst field = this.board?.statusField\n\t\t\tif (!field) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\treturn this.effectiveSchema?.properties?.[field] || null\n\t\t},\n\n\t\t/**\n\t\t * Whether the list holds one page of more.\n\t\t *\n\t\t * The board says so beside its counts: a count of a page shown as\n\t\t * though it were the total is a number somebody quotes in a meeting.\n\t\t *\n\t\t * @return {boolean} True when there is more than this page.\n\t\t */\n\t\tisPaged() {\n\t\t\treturn Number(this.pagination?.pages || 1) > 1\n\t\t},\n\n\t\t/**\n\t\t * The CnFolderSidebar source. A `register` source is fetched by this\n\t\t * component and handed to CnFolderSidebar as a `custom` folder list.\n\t\t *\n\t\t * @return {string} The resolved CnFolderSidebar source.\n\t\t */\n\t\tfolderSidebarSource() {\n\t\t\tconst s = this.folderSidebar && this.folderSidebar.source\n\t\t\treturn (s === 'register' || !s) ? 'custom' : s\n\t\t},\n\n\t\t/**\n\t\t * The folder list for CnFolderSidebar's custom source: register-fetched\n\t\t * objects (source:'register') or the explicit `folders` (source:'custom').\n\t\t * Empty for the field/files sources, which derive their own list.\n\t\t *\n\t\t * @return {Array<object>} The folder list.\n\t\t */\n\t\tfolderSidebarFolders() {\n\t\t\tif (!this.folderSidebar) {\n\t\t\t\treturn []\n\t\t\t}\n\t\t\tif (this.folderSidebar.source === 'register') {\n\t\t\t\treturn this.folderRegisterList\n\t\t\t}\n\t\t\treturn this.folderSidebar.folders || []\n\t\t},\n\n\t\t/**\n\t\t * The folder entry for the folder currently selected in the sidebar,\n\t\t * or null while \"All\" is selected.\n\t\t *\n\t\t * @return {(object|null)} The active scope's folder entry.\n\t\t * @spec openspec/changes/index-columns-per-scope/specs/index-page/spec.md\n\t\t */\n\t\tactiveScope() {\n\t\t\tif (this.selectedFolderId === null || this.selectedFolderId === undefined) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst idField = this.folderPassthroughIdField\n\t\t\tconst match = this.folderSidebarFolders\n\t\t\t\t.find((f) => f && String(this.getByPath(f, idField)) === String(this.selectedFolderId))\n\t\t\treturn match || null\n\t\t},\n\n\t\t/**\n\t\t * The `x-index` block the active scope's own schema row carries, for a\n\t\t * folder list derived from a register. Empty for every other source:\n\t\t * a folder that is a distinct field value, or one written into the\n\t\t * manifest by hand, has no record behind it to read.\n\t\t *\n\t\t * @return {(object|null)} A row-shaped object carrying `x-index`, or null.\n\t\t * @spec openspec/changes/index-columns-per-scope/specs/index-page/spec.md\n\t\t */\n\t\tactiveScopeRow() {\n\t\t\tif (this.selectedFolderId === null || this.selectedFolderId === undefined) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst carried = this.folderRowLayouts[String(this.selectedFolderId)]\n\t\t\treturn carried ? { 'x-index': carried } : null\n\t\t},\n\n\t\t/**\n\t\t * The list layout the active scope asks for. Two declarations meet\n\t\t * here and they decide different things: this page's `columns` prop\n\t\t * decides which columns the page HAS, and the scope decides which of\n\t\t * them it shows, in what order, sorted by what and searched over what.\n\t\t * A scope naming a column this page does not declare is dropped, so a\n\t\t * scope written before a column was taken out cannot bring it back.\n\t\t *\n\t\t * @return {{columns: (Array|null), sortKeys: Array, searchFields: string[]}}\n\t\t * @spec openspec/changes/index-columns-per-scope/specs/index-page/spec.md\n\t\t */\n\t\tactiveScopeLayout() {\n\t\t\tif (!this.folderSidebar) {\n\t\t\t\treturn { columns: null, sortKeys: [], searchFields: [] }\n\t\t\t}\n\t\t\treturn resolveScopeLayout({\n\t\t\t\tscope: this.activeScope,\n\t\t\t\trow: this.activeScopeRow,\n\t\t\t\tpageColumns: this.declaredColumns,\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * The fields the search box searches over while a scope declaring\n\t\t * `searchFields` is active. Read by `useSelfFetchList`'s fixed-filter\n\t\t * getter off this instance, so a scope change re-scopes the next fetch\n\t\t * without a second search path.\n\t\t *\n\t\t * @return {string[]} The scope's search fields, or [].\n\t\t * @spec openspec/changes/index-columns-per-scope/specs/index-page/spec.md\n\t\t */\n\t\tactiveScopeSearchFields() {\n\t\t\treturn this.activeScopeLayout.searchFields\n\t\t},\n\n\t\t/**\n\t\t * The teams this person is treated as having claimed: what they stored,\n\t\t * narrowed to what the instance offers them now.\n\t\t *\n\t\t * @return {string[]} The claimed team ids.\n\t\t * @spec openspec/changes/working-list-row-actions/specs/index-page/spec.md\n\t\t */\n\t\tclaimedTeamIds() {\n\t\t\treturn resolveClaimedTeams(this.offeredTeams, this.claimedTeams)\n\t\t},\n\n\t\t/**\n\t\t * The saved views this page renders as tabs, and the ones that stay in\n\t\t * the views control. A view is in one or the other, never both.\n\t\t *\n\t\t * Split out of the SCOPED views, not the fetched ones: a view made on\n\t\t * another page names fields this schema does not have, and is no more\n\t\t * use as a tab than it is as a dropdown entry.\n\t\t *\n\t\t * @return {{tabs: Array<object>, control: Array<object>}}\n\t\t * @spec openspec/changes/working-list-row-actions/specs/index-page/spec.md\n\t\t */\n\t\tsplitSavedViews() {\n\t\t\treturn splitViewsIntoTabs(this.visibleSavedViews, this.viewTabs)\n\t\t},\n\n\t\t/**\n\t\t * The lens tabs, as the tab strip this page already has understands\n\t\t * them, with each lens's claim tokens resolved against this person.\n\t\t *\n\t\t * @return {Array<object>} The tabs.\n\t\t * @spec openspec/changes/working-list-row-actions/specs/index-page/spec.md\n\t\t */\n\t\tlensTabs() {\n\t\t\treturn this.splitSavedViews.tabs.map((view) => {\n\t\t\t\tconst tab = viewAsTab(view)\n\t\t\t\tconst { filter, narrowsToNothing } = resolveClaimTokens(tab.filter, { teams: this.claimedTeamIds })\n\t\t\t\treturn { ...tab, filter, narrowsToNothing }\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * The views the control lists: every saved view this page did not name\n\t\t * as a tab.\n\t\t *\n\t\t * @return {Array<object>} The views.\n\t\t */\n\t\tviewsForControl() {\n\t\t\treturn this.splitSavedViews.control\n\t\t},\n\n\t\t/**\n\t\t * What the tab strip shows: the page's lens tabs when it named any,\n\t\t * else its quick filters. One strip, never two, so a person is not\n\t\t * offered the same narrowing twice under two names.\n\t\t *\n\t\t * @return {(Array<object>|null)} The tabs.\n\t\t * @spec openspec/changes/working-list-row-actions/specs/index-page/spec.md\n\t\t */\n\t\ttabStripEntries() {\n\t\t\tif (this.lensTabs.length > 0) {\n\t\t\t\treturn this.lensTabs\n\t\t\t}\n\t\t\treturn this.effectiveQuickFilters\n\t\t},\n\n\t\t/** @return {Array<object>} The shortcuts the help sheet lists. */\n\t\tshortcutHelpEntries() {\n\t\t\treturn LIST_SHORTCUTS.filter((entry) => typeof this.listShortcutHandlers[entry.id] === 'function')\n\t\t},\n\n\t\t/** @return {string} Heading of the shortcut help sheet. */\n\t\tshortcutHelpTitle() {\n\t\t\treturn t('nextcloud-vue', 'Keyboard shortcuts')\n\t\t},\n\n\t\t/** @return {string} Label of the help sheet's close button. */\n\t\tcloseLabel() {\n\t\t\treturn t('nextcloud-vue', 'Close')\n\t\t},\n\n\t\t/**\n\t\t * Whether the active lens narrowed to nothing because this person has\n\t\t * claimed no teams. The page says that, rather than showing an empty\n\t\t * list under a heading that claims to be filtering by their teams.\n\t\t *\n\t\t * @return {boolean} True when the active lens has nothing to match.\n\t\t */\n\t\tactiveLensNarrowsToNothing() {\n\t\t\tconst index = this.activeQuickFilterIndex\n\t\t\tif (index === null || index === undefined || this.lensTabs.length === 0) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\treturn Boolean(this.lensTabs[index] && this.lensTabs[index].narrowsToNothing)\n\t\t},\n\n\t\t/**\n\t\t * The declarative sort this page applies, with the priority sort folded\n\t\t * in when the page names a priority field. The value is READ off the\n\t\t * record; nothing here derives one.\n\t\t *\n\t\t * @return {Array<object>} The sort spec.\n\t\t * @spec openspec/changes/working-list-row-actions/specs/index-page/spec.md\n\t\t */\n\t\teffectiveDefaultSort() {\n\t\t\tconst declared = Array.isArray(this.defaultSort) ? this.defaultSort : []\n\t\t\tif (this.priorityField === '') {\n\t\t\t\treturn declared\n\t\t\t}\n\t\t\tif (declared.some((entry) => entry && entry.field === this.priorityField)) {\n\t\t\t\treturn declared.map((entry) => (entry && entry.field === this.priorityField\n\t\t\t\t\t? { ...entry, levels: this.priorityLevels }\n\t\t\t\t\t: entry))\n\t\t\t}\n\t\t\treturn [{ field: this.priorityField, order: 'desc', levels: this.priorityLevels }, ...declared]\n\t\t},\n\n\t\t/**\n\t\t * The list's keyboard shortcuts, bound to what this page can do. A\n\t\t * shortcut with nothing behind it is left out rather than listed.\n\t\t *\n\t\t * @return {object} Handlers by shortcut id.\n\t\t * @spec openspec/changes/working-list-row-actions/specs/index-page/spec.md\n\t\t */\n\t\tlistShortcutHandlers() {\n\t\t\tconst handlers = {\n\t\t\t\t'row-next': () => this.moveFocusedRow(1),\n\t\t\t\t'row-previous': () => this.moveFocusedRow(-1),\n\t\t\t\t'row-open': () => this.focusedRow && this.onRowClick(this.focusedRow),\n\t\t\t\t'row-select': () => this.toggleFocusedRowSelection(),\n\t\t\t\t'list-help': () => { this.showShortcutHelp = true },\n\t\t\t}\n\t\t\tif (this.quickEditFields.length > 0) {\n\t\t\t\thandlers['row-quick-edit'] = () => this.focusedRow && this.openQuickEdit(this.focusedRow)\n\t\t\t}\n\t\t\tif (this.focusedRow && this.rowActionsFor(this.focusedRow).length > 0) {\n\t\t\t\thandlers['row-primary'] = () => this.runPrimaryRowAction()\n\t\t\t}\n\t\t\treturn handlers\n\t\t},\n\n\t\t/**\n\t\t * The shortcuts as command-palette entries, so someone who has never\n\t\t * used this list can find every one of them.\n\t\t *\n\t\t * @return {Array<object>} The palette entries.\n\t\t */\n\t\tlistPaletteEntries() {\n\t\t\treturn listPaletteCommands(this.listShortcutHandlers)\n\t\t},\n\n\t\t/**\n\t\t * The row the keyboard is on, or null.\n\t\t *\n\t\t * @return {(object|null)} The focused row.\n\t\t */\n\t\tfocusedRow() {\n\t\t\tconst rows = this.displayObjects || []\n\t\t\tif (this.focusedRowIndex < 0 || this.focusedRowIndex >= rows.length) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\treturn rows[this.focusedRowIndex]\n\t\t},\n\n\t\t/**\n\t\t * The grouping field for a `field`-source folder sidebar.\n\t\t *\n\t\t * @return {string} The property the folders group by, or ''.\n\t\t */\n\t\tfolderSidebarGroupBy() {\n\t\t\tif (!this.folderSidebar) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn this.folderSidebar.groupBy || this.folderSidebar.field || ''\n\t\t},\n\n\t\t/**\n\t\t * Facet buckets for the folder sidebar's grouping field.\n\t\t *\n\t\t * `:objects` is the current page, so folders derived from it alone list\n\t\t * only the values that happen to have a row on screen. The platform\n\t\t * already answers this properly: OpenRegister computes facets with the\n\t\t * pagination parameters removed, so a facet over the grouping field is\n\t\t * the complete set of distinct values with their true totals. Any\n\t\t * property marked `facetable: true` gets one under `_facets=extend`.\n\t\t *\n\t\t * Read from the live store first, so the folder pane does not depend on\n\t\t * the metadata sidebar being switched on, then from the sidebar config\n\t\t * a consumer-managed page supplies, then from the folder config itself.\n\t\t *\n\t\t * @return {Array<object>} Normalised facet values, or [] when none.\n\t\t */\n\t\tfolderSidebarLiveFacetValues() {\n\t\t\tconst field = this.folderSidebarGroupBy\n\t\t\tif (!field) {\n\t\t\t\treturn []\n\t\t\t}\n\n\t\t\tconst facets = this.storeFacets\n\t\t\t\t|| (this.folderSidebar && this.folderSidebar.facets)\n\t\t\t\t|| this.resolvedSidebar.facets\n\t\t\t\t|| {}\n\n\t\t\treturn facets[field]?.values || []\n\t\t},\n\n\t\t/**\n\t\t * The folders to show: the live facet, except while a folder is\n\t\t * selected. Selecting one filters the query by the grouping field, so\n\t\t * the live facet then holds that one value and every other folder\n\t\t * would vanish, which makes switching folders impossible. The set seen\n\t\t * before the selection stands in until it is cleared.\n\t\t *\n\t\t * @return {Array<object>} Normalised facet values.\n\t\t */\n\t\tfolderSidebarFacetValues() {\n\t\t\tconst selected = this.selectedFolderId !== null && this.selectedFolderId !== undefined\n\t\t\tif (selected && this.folderSidebarAllValues.length > 0) {\n\t\t\t\treturn this.folderSidebarAllValues\n\t\t\t}\n\t\t\treturn this.folderSidebarLiveFacetValues\n\t\t},\n\n\t\t/**\n\t\t * The facet buckets OpenRegister computed for THIS page's query, or\n\t\t * null when this page does not fetch its own rows.\n\t\t *\n\t\t * Only self-fetch mode has them: the store keys facets by object type\n\t\t * and rewrites that entry from the same response the rows came from, so\n\t\t * what this returns always describes the query currently on screen.\n\t\t * Consumer-managed, named-source and entity-source pages never fetch\n\t\t * through the store, so there is nothing here to read and the caller\n\t\t * falls back to whatever the consumer passed in.\n\t\t *\n\t\t * @return {object|null} The live facet map, or null.\n\t\t */\n\t\tstoreFacets() {\n\t\t\treturn this.isSelfFetchMode ? (this.list.facets?.value || null) : null\n\t\t},\n\n\t\t/**\n\t\t * Facet data for the index sidebar.\n\t\t *\n\t\t * The `sidebar.facets` key is a DATA channel, not a declaration of\n\t\t * which facets exist: its docblock calls it \"live facet data\" and it\n\t\t * feeds CnIndexSidebar's `facetData` prop unchanged. Which filters the\n\t\t * sidebar offers comes from the schema (`filtersFromSchema` over the\n\t\t * facetable properties), and a filter with no bucket falls back to its\n\t\t * own declarative `options`. So there is nothing to merge here — the\n\t\t * two sources answer the same question, and the fresher one wins.\n\t\t *\n\t\t * A manifest-driven page has no consumer to fill `sidebar.facets`, so\n\t\t * before this read the sidebar was handed the manifest's sidebar config\n\t\t * and every facet rendered \"No results\" while the response body carried\n\t\t * the buckets.\n\t\t *\n\t\t * Note the store wins even when it is empty (`{}` is truthy), which is\n\t\t * deliberate and the conservative direction: until the first response\n\t\t * lands, offering no options is right, and offering options left over\n\t\t * from a config that never described this query would be worse. Same\n\t\t * precedence `folderSidebarFacetValues` has always used.\n\t\t *\n\t\t * @return {object} `{ fieldName: { values: [...] } }`, possibly empty.\n\t\t */\n\t\teffectiveFacetData() {\n\t\t\treturn this.storeFacets || this.resolvedSidebar.facets || {}\n\t\t},\n\n\t\t/**\n\t\t * Whether the rows handed to the folder sidebar are only part of the\n\t\t * result set. True when the list is paged beyond what is loaded and no\n\t\t * facet is available to complete the picture, which is exactly when\n\t\t * folders can be missing and a per-page count would read as a total.\n\t\t *\n\t\t * @return {boolean} True when the folder list is knowingly incomplete.\n\t\t */\n\t\tfolderSidebarPartial() {\n\t\t\tif (this.folderSidebarFacetValues.length > 0) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tconst total = Number(this.effectivePagination?.total ?? 0)\n\t\t\treturn total > this.effectiveObjects.length\n\t\t},\n\n\t\t/**\n\t\t * id/name field names passed to CnFolderSidebar for the custom folder\n\t\t * list. The `register` source is pre-mapped to `{ id, name }` by\n\t\t * `loadFolderRegister`, so it uses the plain keys; a `custom` source\n\t\t * passes the objects through untouched and honours the config's fields.\n\t\t *\n\t\t * @return {string} The id field key.\n\t\t */\n\t\tfolderPassthroughIdField() {\n\t\t\tif (this.folderSidebar && this.folderSidebar.source === 'register') {\n\t\t\t\treturn 'id'\n\t\t\t}\n\t\t\treturn (this.folderSidebar && this.folderSidebar.idField) || 'id'\n\t\t},\n\n\t\t/**\n\t\t * @return {string} The name field key for CnFolderSidebar's custom list.\n\t\t */\n\t\tfolderPassthroughNameField() {\n\t\t\tif (this.folderSidebar && this.folderSidebar.source === 'register') {\n\t\t\t\treturn 'name'\n\t\t\t}\n\t\t\treturn (this.folderSidebar && this.folderSidebar.nameField) || 'name'\n\t\t},\n\n\t\t/**\n\t\t * Inline GeoJSON markers for the map view, built from the CURRENT filtered\n\t\t * `displayObjects` using `mapConfig`. Each feature stashes the row's\n\t\t * `rowKey` in `properties` so a marker click can resolve back to its source\n\t\t * row. Rows without finite, resolvable geometry are skipped silently.\n\t\t *\n\t\t * @return {{ features: object[], popupField: (string|undefined) }}\n\t\t */\n\t\tmapMarkers() {\n\t\t\tconst features = []\n\t\t\tfor (const row of this.displayObjects) {\n\t\t\t\tconst geometry = this.resolveRowGeometry(row)\n\t\t\t\tif (!geometry) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tfeatures.push({\n\t\t\t\t\ttype: 'Feature',\n\t\t\t\t\tgeometry,\n\t\t\t\t\tproperties: {\n\t\t\t\t\t\t[this.rowKey]: row[this.rowKey],\n\t\t\t\t\t\t...(this.mapConfig.popupField ? { [this.mapConfig.popupField]: row[this.mapConfig.popupField] } : {}),\n\t\t\t\t\t},\n\t\t\t\t})\n\t\t\t}\n\t\t\treturn { features, popupField: this.mapConfig.popupField }\n\t\t},\n\n\t\t/**\n\t\t * Whether to cluster the plotted markers. ON unless `mapConfig.clustering`\n\t\t * says otherwise, which is the opposite of `CnMapWidget`'s own default.\n\t\t *\n\t\t * An index map plots the whole filtered result set, and rows sharing one\n\t\t * address is the normal case here, not an edge case — several permits on a\n\t\t * building, a street of complaints. Unclustered, those markers stack at\n\t\t * identical pixels: the topmost swallows every click and the rest are\n\t\t * unreachable with nothing on screen saying they exist. Clustering shows the\n\t\t * count and, since all children stay in one cluster down to max zoom,\n\t\t * markercluster spiderfies them into a fan on click instead of zooming\n\t\t * uselessly. Non-point geometry skips the cluster group entirely, so areas\n\t\t * are unaffected.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\tmapClustering() {\n\t\t\treturn this.mapConfig.clustering !== false\n\t\t},\n\n\t\t/**\n\t\t * Extra (non-background) map layers — WMS / WFS / GeoJSON overlays declared in\n\t\t * `mapConfig.layers`. The background map comes from `mapBasemaps` instead, so\n\t\t * this is empty unless the consumer configured overlays.\n\t\t *\n\t\t * @return {Array<object>}\n\t\t */\n\t\tmapLayers() {\n\t\t\tif (Array.isArray(this.mapConfig.layers) && this.mapConfig.layers.length > 0) {\n\t\t\t\treturn this.mapConfig.layers\n\t\t\t}\n\t\t\treturn []\n\t\t},\n\n\t\t/**\n\t\t * Switchable background maps.\n\t\t *\n\t\t * Defaults to OpenStreetMap standard ONLY. Every extra basemap is an `<img>`\n\t\t * load from a third-party host, which a Nextcloud app's Content-Security-Policy\n\t\t * (`img-src`) blocks unless that app explicitly allowlists the host — so a\n\t\t * richer default would ship dead options to consumers that never widened their\n\t\t * CSP. Apps that want a switcher declare the set in `mapConfig.basemaps` AND\n\t\t * allowlist those hosts (see Dossiq's `relaxCspForMapTiles()`).\n\t\t *\n\t\t * @return {Array<object>}\n\t\t */\n\t\tmapBasemaps() {\n\t\t\tif (Array.isArray(this.mapConfig.basemaps) && this.mapConfig.basemaps.length > 0) {\n\t\t\t\treturn this.mapConfig.basemaps\n\t\t\t}\n\t\t\treturn [{\n\t\t\t\tname: t('nextcloud-vue', 'Standard'),\n\t\t\t\turl: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',\n\t\t\t\tattribution: '© OpenStreetMap contributors',\n\t\t\t}]\n\t\t},\n\n\t\t/**\n\t\t * Initial map centre `[lat, lng]`. Uses the centroid of the plotted markers\n\t\t * when any exist (CnMapWidget's autoFit then tightens to the bounds); falls\n\t\t * back to `mapConfig.center`, then a neutral world view when the set is empty.\n\t\t *\n\t\t * @return {[number, number]}\n\t\t */\n\t\tmapCenter() {\n\t\t\tconst feats = this.mapMarkers.features\n\t\t\tif (feats.length > 0) {\n\t\t\t\tlet sumLat = 0\n\t\t\t\tlet sumLng = 0\n\t\t\t\tfor (const f of feats) {\n\t\t\t\t\tconst _p = this.firstLatLng(f.geometry)\n\t\t\t\t\tif (!_p) {\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\t\t\t\t\tsumLng += _p.lng\n\t\t\t\t\tsumLat += _p.lat\n\t\t\t\t}\n\t\t\t\treturn [sumLat / feats.length, sumLng / feats.length]\n\t\t\t}\n\t\t\tif (Array.isArray(this.mapConfig.center) && this.mapConfig.center.length === 2) {\n\t\t\t\treturn this.mapConfig.center\n\t\t\t}\n\t\t\treturn [0, 0]\n\t\t},\n\n\t\t/** Loading flag: store loading in self-fetch mode, else the `loading` prop. */\n\t\teffectiveLoading() {\n\t\t\tif (this.isNamedSource) {\n\t\t\t\treturn this.namedLoading\n\t\t\t}\n\t\t\treturn this.isSelfFetchMode ? !!this.list.loading.value : this.loading\n\t\t},\n\n\t\t/**\n\t\t * Whether to replace the page with the full loading spinner. Only on an\n\t\t * INITIAL fetch — i.e. while loading AND there is no data to show yet.\n\t\t * A background refresh (e.g. the on-visibility refetch) keeps the\n\t\t * existing rows on screen and updates them in place instead of flashing\n\t\t * the spinner over already-rendered content (fetchCollection preserves\n\t\t * the prior collection until the new results arrive).\n\t\t */\n\t\tshowInitialLoader() {\n\t\t\treturn this.effectiveLoading && this.effectiveObjects.length === 0\n\t\t},\n\n\t\t/**\n\t\t * Refresh-spinner flag for the Actions menu: the `refreshing` prop\n\t\t * OR (self-fetch mode) the internally-tracked refresh. Lets manifest\n\t\t * and self-fetch pages spin the Refresh action without the host\n\t\t * wiring `:refreshing` (it has no fetch promise to await).\n\t\t */\n\t\teffectiveRefreshing() {\n\t\t\treturn this.refreshing || this.internalRefreshing\n\t\t},\n\n\t\t/** Pagination: store pagination in self-fetch mode, else the `pagination` prop. */\n\t\teffectivePagination() {\n\t\t\treturn this.isSelfFetchMode ? this.list.pagination.value : this.pagination\n\t\t},\n\n\t\t/** Resolved schema OBJECT (for column generation / icons / labels). */\n\t\teffectiveSchema() {\n\t\t\tif (this.isSelfFetchMode) {\n\t\t\t\treturn this.list.schema.value\n\t\t\t}\n\t\t\treturn (this.schema && typeof this.schema === 'object') ? this.schema : null\n\t\t},\n\n\t\t/**\n\t\t * Schema slug for the export-leaf URL — the `schema` prop directly\n\t\t * when it's a string (self-fetch mode's precondition), else the\n\t\t * resolved schema object's `slug`/`name`.\n\t\t */\n\t\texportSchemaSlug() {\n\t\t\tif (typeof this.schema === 'string') {\n\t\t\t\treturn this.schema\n\t\t\t}\n\t\t\treturn this.effectiveSchema?.slug || this.effectiveSchema?.name || ''\n\t\t},\n\n\t\t/**\n\t\t * Whether the native Export menu renders in the toolbar: opt-in via\n\t\t * `allowExport` AND the resolved schema flagged `exportable: true`.\n\t\t * Default-safe — false unless both hold.\n\t\t */\n\t\tshowExportMenu() {\n\t\t\treturn Boolean(this.allowExport) && Boolean(this.effectiveSchema?.exportable) && Boolean(this.register) && Boolean(this.exportSchemaSlug)\n\t\t},\n\n\t\t/**\n\t\t * The signed-in NC user id — passed to CnSavedViewsControl to gate\n\t\t * the per-view delete affordance (saved-views-ui).\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\t/**\n\t\t * The page as `savedViewScope` and `viewMatchesScope` take it.\n\t\t *\n\t\t * @return {{ scope: string, register: string, schema: object|string|null }} The page's source.\n\t\t */\n\t\tsavedViewsPage() {\n\t\t\treturn { scope: this.savedViewsScope, register: this.register, schema: this.schema }\n\t\t},\n\n\t\t/**\n\t\t * The fetched views that belong on this page.\n\t\t *\n\t\t * @return {Array<object>} The views to offer.\n\t\t */\n\t\tvisibleSavedViews() {\n\t\t\treturn this.savedViews.filter((view) => viewMatchesScope(view, this.savedViewsPage))\n\t\t},\n\n\t\tcurrentSavedViewsUserId() {\n\t\t\tconst user = getCurrentUser()\n\t\t\treturn (user && user.uid) || ''\n\t\t},\n\n\t\t/**\n\t\t * Confirmation message for the delete-saved-view dialog.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tdeleteViewMessage() {\n\t\t\tif (!this.viewPendingDelete) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn t('nextcloud-vue', 'Delete the view \"{name}\"? This cannot be undone.', { name: this.viewPendingDelete.name })\n\t\t},\n\n\t\t/** Sort key / order: list state in self-fetch mode, else the props. */\n\t\teffectiveSortKey() {\n\t\t\treturn this.isSelfFetchMode ? this.list.sortKey.value : this.sortKey\n\t\t},\n\n\t\teffectiveSortOrder() {\n\t\t\treturn this.isSelfFetchMode ? this.list.sortOrder.value : this.sortOrder\n\t\t},\n\n\t\t/**\n\t\t * Ordered multi-column sort state fed to CnDataTable: the self-fetch\n\t\t * list's `sortKeys` in self-fetch mode, else the host-controlled\n\t\t * `sortKeys` prop.\n\t\t *\n\t\t * @return {Array<{key: string, order: 'asc'|'desc'}>}\n\t\t */\n\t\teffectiveSortKeys() {\n\t\t\treturn this.isSelfFetchMode ? (this.list.sortKeys.value || []) : this.sortKeys\n\t\t},\n\n\t\t/** Search term / visible columns / active facet filters for the embedded sidebar. */\n\t\teffectiveSearchValue() {\n\t\t\treturn this.isSelfFetchMode ? (this.list.searchTerm.value || '') : (this.searchValue || '')\n\t\t},\n\n\t\teffectiveVisibleColumns() {\n\t\t\treturn this.isSelfFetchMode ? this.list.visibleColumns.value : this.visibleColumns\n\t\t},\n\n\t\teffectiveActiveFilters() {\n\t\t\tif (this.isSelfFetchMode) {\n\t\t\t\treturn this.list.activeFilters.value || {}\n\t\t\t}\n\t\t\t// A named source's own state wins over the prop, and only once it\n\t\t\t// holds something: a consumer-managed page that ALSO names a\n\t\t\t// source keeps its `activeFilters` until the sidebar is used.\n\t\t\tif (this.isNamedSource && Object.keys(this.namedActiveFilters || {}).length > 0) {\n\t\t\t\treturn this.namedActiveFilters\n\t\t\t}\n\t\t\treturn this.activeFilters || {}\n\t\t},\n\n\t\t/**\n\t\t * Enum schema columns offered in the header filter menu: one entry per\n\t\t * visible column whose schema property declares an `enum`, with its\n\t\t * values. Empty (so the menu hides) when there is no schema or no enum\n\t\t * column. Drives the `showFilterMenu` funnel button.\n\t\t *\n\t\t * @return {Array<{ key: string, label: string, values: string[] }>}\n\t\t */\n\t\tfilterableFields() {\n\t\t\tconst props = this.effectiveSchema?.properties || {}\n\t\t\tconst out = []\n\t\t\tfor (const col of this.tableColumns) {\n\t\t\t\tconst key = typeof col === 'string' ? col : col.key\n\t\t\t\tconst def = props[key] || {}\n\t\t\t\tconst colObj = typeof col === 'object' ? col : {}\n\t\t\t\t// Source the filter values, in priority order: schema enum, a\n\t\t\t\t// column `enum` hint, or a badge column's colorMap keys (so a\n\t\t\t\t// status column stays filterable even when the runtime schema\n\t\t\t\t// doesn't carry the enum).\n\t\t\t\tlet values = null\n\t\t\t\tif (Array.isArray(def.enum) && def.enum.length) {\n\t\t\t\t\tvalues = def.enum\n\t\t\t\t} else if (Array.isArray(colObj.enum) && colObj.enum.length) {\n\t\t\t\t\tvalues = colObj.enum\n\t\t\t\t} else if (colObj.widget === 'badge' && colObj.widgetProps && colObj.widgetProps.colorMap) {\n\t\t\t\t\tvalues = Object.keys(colObj.widgetProps.colorMap)\n\t\t\t\t}\n\t\t\t\tif (values && values.length) {\n\t\t\t\t\tout.push({ key, label: this.cnTranslate(colObj.label || def.title || key), values: values.map((v) => String(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 * Ordered column definitions the sidebar's Columns tab governs:\n\t\t * schema-derived columns, the built-in Metadata group (when shown),\n\t\t * and any external columnGroups. Mirrors CnIndexSidebar's column\n\t\t * universe, and doubles as the source of definitions for columns the\n\t\t * user enables that aren't in the configured `columns` list (metadata\n\t\t * fields, schema properties beyond the default set).\n\t\t */\n\t\tgovernedColumns() {\n\t\t\tconst defs = []\n\t\t\tif (this.effectiveSchema) {\n\t\t\t\t// English labels here: `governedColumns` feeds `tableColumns`,\n\t\t\t\t// which CnDataTable render-translates via its own `cnTranslate`\n\t\t\t\t// injection. Translating here too would double-translate. The\n\t\t\t\t// column-picker menu that renders these labels directly applies\n\t\t\t\t// `cnTranslate` at render instead.\n\t\t\t\tdefs.push(...columnsFromSchema(this.effectiveSchema, {}))\n\t\t\t\tif (this.resolvedSidebar.showMetadata !== false) {\n\t\t\t\t\tdefs.push(...METADATA_COLUMNS)\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst groups = this.resolvedSidebar.columnGroups || []\n\t\t\tgroups.forEach((g) => defs.push(...(g.columns || [])))\n\t\t\treturn defs\n\t\t},\n\n\t\t/**\n\t\t * Set of keys the sidebar governs (see `governedColumns`). Used so\n\t\t * tableColumns only hides columns the user can actually toggle back\n\t\t * on — custom columns outside this set are never silently dropped.\n\t\t */\n\t\tsidebarGovernedColumnKeys() {\n\t\t\treturn new Set(this.governedColumns.map((c) => c.key))\n\t\t},\n\n\t\t/**\n\t\t * The columns this page DECLARES it has: the `columns` prop, or a named\n\t\t * source's own list when the manifest set none. This is the membership\n\t\t * list, separate from `tableColumns`, which is what ends up rendered\n\t\t * after the sidebar scope and the user's visible-column set have had\n\t\t * their say over it.\n\t\t *\n\t\t * @return {Array} The declared column list.\n\t\t */\n\t\tdeclaredColumns() {\n\t\t\t// A NAMED SOURCE SUPPLIES ITS OWN COLUMNS when the manifest does not.\n\t\t\t// Without this the adapter's `columns` were defined and never read:\n\t\t\t// an `entitySource` page with no explicit `columns` fell through to\n\t\t\t// `governedColumns`, which derives from a SCHEMA — and a named\n\t\t\t// source has none. The table then rendered its rows with no columns\n\t\t\t// at all, which looks like an empty list rather than a missing\n\t\t\t// config. A manifest that DOES set columns still wins, which is\n\t\t\t// what makes the source a default rather than a constraint.\n\t\t\treturn (this.columns && this.columns.length > 0)\n\t\t\t\t? this.columns\n\t\t\t\t: ((this.isNamedSource && this.namedSource && this.namedSource.columns) || [])\n\t\t},\n\n\t\t/**\n\t\t * Columns handed to CnDataTable. Starts from `declaredColumns`, or from\n\t\t * the active folder-sidebar scope's selection of them (with any\n\t\t * `aggregate` block lacking a `register` defaulted to this page's\n\t\t * `register` slug, so manifests can omit `aggregate.register`). When a\n\t\t * visible-column set exists, governed columns the user toggled off are\n\t\t * hidden, and governed columns the user toggled on that aren't already\n\t\t * in the list (metadata fields, extra schema properties) are appended\n\t\t * using their sidebar definitions. Custom columns outside the sidebar's\n\t\t * universe are untouched.\n\t\t */\n\t\ttableColumns() {\n\t\t\tconst reg = typeof this.register === 'string' && this.register ? this.register : undefined\n\t\t\t// The active sidebar scope may narrow and reorder the page's\n\t\t\t// declared columns; it cannot add to them.\n\t\t\tlet cols = this.activeScopeLayout.columns || this.declaredColumns\n\t\t\tif (reg) {\n\t\t\t\tcols = cols.map((c) => (\n\t\t\t\t\tc && c.aggregate && !c.aggregate.register\n\t\t\t\t\t\t? { ...c, aggregate: { ...c.aggregate, register: reg } }\n\t\t\t\t\t\t: c\n\t\t\t\t))\n\t\t\t}\n\t\t\tconst visible = this.effectiveVisibleColumns\n\t\t\tif (!Array.isArray(visible)) {\n\t\t\t\treturn cols\n\t\t\t}\n\n\t\t\tconst governed = this.sidebarGovernedColumnKeys\n\t\t\tcols = cols.filter((c) => {\n\t\t\t\tconst key = typeof c === 'string' ? c : c.key\n\t\t\t\treturn !governed.has(key) || visible.includes(key)\n\t\t\t})\n\n\t\t\tconst present = new Set(cols.map((c) => (typeof c === 'string' ? c : c.key)))\n\t\t\tconst byKey = new Map(this.governedColumns.map((c) => [c.key, c]))\n\t\t\tvisible.forEach((key) => {\n\t\t\t\tif (present.has(key)) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tconst def = byKey.get(key)\n\t\t\t\tif (def) {\n\t\t\t\t\tcols.push({ ...def })\n\t\t\t\t\tpresent.add(key)\n\t\t\t\t}\n\t\t\t})\n\t\t\treturn cols\n\t\t},\n\n\t\t/** Resolved icon — explicit prop overrides schema.icon */\n\t\tresolvedIcon() {\n\t\t\tif (this.icon) {\n\t\t\t\treturn this.icon\n\t\t\t}\n\t\t\treturn this.effectiveSchema?.icon || ''\n\t\t},\n\n\t\t/**\n\t\t * Built-in row actions based on show*Action props.\n\t\t *\n\t\t * A NAMED SOURCE changes the defaults, as a contract rather than a\n\t\t * page-by-page opt-out (observed on the fleet's flows pages, where the\n\t\t * menu carried FIVE actions and three of them were broken):\n\t\t *\n\t\t * - View and the schema-form Edit are OFF. A source has no schema, so\n\t\t * the form modal can only ever render empty; and the source's own\n\t\t * open/Edit action already navigates to the detail page, which IS\n\t\t * the view. Rendering the built-ins beside it gave every flow list a\n\t\t * dead View and an Edit that opened an empty dialog.\n\t\t * - Copy and Delete render only when the source implements them\n\t\t * (`copyRow` / `deleteRow`): the built-in dialogs' confirm otherwise\n\t\t * emits into a manifest page with nobody listening, a dialog that\n\t\t * looks like it acted and did not.\n\t\t *\n\t\t * An EXPLICITLY passed show*Action prop always wins, both ways.\n\t\t */\n\t\tdefaultActions() {\n\t\t\treturn buildDefaultActions({\n\t\t\t\tflags: this.isNamedSource && this.namedSource\n\t\t\t\t\t? {\n\t\t\t\t\t\t\tview: this.hasExplicitProp('showViewAction') && this.showViewAction,\n\t\t\t\t\t\t\tedit: this.hasExplicitProp('showEditAction') && this.showEditAction,\n\t\t\t\t\t\t\tcopy: this.hasExplicitProp('showCopyAction')\n\t\t\t\t\t\t\t\t? this.showCopyAction\n\t\t\t\t\t\t\t\t: typeof this.namedSource.copyRow === 'function',\n\t\t\t\t\t\t\tdel: this.hasExplicitProp('showDeleteAction')\n\t\t\t\t\t\t\t\t? this.showDeleteAction\n\t\t\t\t\t\t\t\t: typeof this.namedSource.deleteRow === 'function',\n\t\t\t\t\t\t}\n\t\t\t\t\t: {\n\t\t\t\t\t\t\tview: this.showViewAction,\n\t\t\t\t\t\t\tedit: this.showEditAction,\n\t\t\t\t\t\t\tcopy: this.showCopyAction,\n\t\t\t\t\t\t\tdel: this.showDeleteAction,\n\t\t\t\t\t\t},\n\t\t\t\t// The View action is always an eye — a universal \"view\" affordance,\n\t\t\t\t// independent of the object's schema icon (which is the header icon).\n\t\t\t\tviewIcon: Eye,\n\t\t\t\tviewTo: this.viewTo,\n\t\t\t\thandlers: {\n\t\t\t\t\tonView: (row) => this.onView(row),\n\t\t\t\t\tonEdit: (row) => {\n\t\t\t\t\t\tif (this.editOpensDetail) {\n\t\t\t\t\t\t\t/**\n\t\t\t\t\t\t\t * @event edit-open Emitted instead of opening the edit modal when\n\t\t\t\t\t\t\t * `editOpensDetail` is set — the host navigates to the record's\n\t\t\t\t\t\t\t * detail page, where the full record (not just its scalars) is\n\t\t\t\t\t\t\t * editable.\n\t\t\t\t\t\t\t * @type {object} The row to open.\n\t\t\t\t\t\t\t */\n\t\t\t\t\t\t\tthis.$emit('edit-open', row)\n\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t}\n\t\t\t\t\t\tthis.editItem = row\n\t\t\t\t\t\tthis.showFormDialogVisible = true\n\t\t\t\t\t},\n\t\t\t\t\tonCopy: (row) => {\n\t\t\t\t\t\tthis.actionTargetItem = row\n\t\t\t\t\t\tthis.showSingleCopyDialog = true\n\t\t\t\t\t},\n\t\t\t\t\tonDelete: (row) => {\n\t\t\t\t\t\tthis.actionTargetItem = row\n\t\t\t\t\t\tthis.showSingleDeleteDialog = true\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * Effective customComponents registry — explicit prop wins over\n\t\t * the injected ancestor registry. Used to:\n\t\t * - Resolve `actions[].handler` registry names (REQ-MAD-3,\n\t\t * manifest-actions-dispatch).\n\t\t * - Resolve the `cardComponent` name for card-grid view (REQ-MCI,\n\t\t * manifest-card-index).\n\t\t *\n\t\t * @return {object}\n\t\t */\n\t\teffectiveCustomComponents() {\n\t\t\treturn this.customComponents ?? this.cnCustomComponents ?? {}\n\t\t},\n\n\t\t/**\n\t\t * The v2 registry a named handler resolves against before the legacy\n\t\t * customComponents map.\n\t\t *\n\t\t * @return {object}\n\t\t */\n\t\teffectiveRegistry() {\n\t\t\treturn this.cnRegistry ?? {}\n\t\t},\n\n\t\t/**\n\t\t * Merged actions: app-provided first, then built-in defaults.\n\t\t *\n\t\t * REQ-MAD-3 / REQ-MAD-4 / REQ-MAD-5 / REQ-MAD-6 / REQ-MAD-7\n\t\t * (manifest-actions-dispatch) — for any action whose `handler`\n\t\t * is a string, resolve it through `resolveHandler()` so\n\t\t * `CnRowActions` sees the same `{ handler: fn }` shape it does\n\t\t * for built-in defaults. Function-typed handlers (the existing\n\t\t * runtime path) pass through untouched.\n\t\t */\n\t\tmergedActions() {\n\t\t\tconst ctx = {\n\t\t\t\trouter: this.$router,\n\t\t\t\trowKey: this.rowKey,\n\t\t\t\tregistry: this.effectiveRegistry,\n\t\t\t\tcustomComponents: this.effectiveCustomComponents,\n\t\t\t}\n\t\t\t// Drop anything that is not an action OBJECT. A bare string — the\n\t\t\t// shorthand `\"edit\"` an author reasonably expects to mean \"show the\n\t\t\t// built-in Edit\" — has no `label` and no `icon`, so it rendered as a\n\t\t\t// full-height NcActionButton with nothing in it: an invisible,\n\t\t\t// clickable, inert row in the overflow menu. Observed on dossiq,\n\t\t\t// where `config.actions: [\"create\", \"edit\", \"delete\", {…}]` put three\n\t\t\t// blank rows above the real ones on four different index pages.\n\t\t\t//\n\t\t\t// Named, not silently swallowed: a dropped action is a capability the\n\t\t\t// author asked for and did not get, and the manifest schema now\n\t\t\t// rejects the shape at authoring time (`config.actions` is governed by\n\t\t\t// the same `action` definition as `pages[].actions`). Built-ins are\n\t\t\t// turned on with the `show*Action` toggles, not by naming them here.\n\t\t\t// A named source may supply its own row actions, on the same\n\t\t\t// precedence as its columns: what the manifest declares wins, and the\n\t\t\t// source is the DEFAULT for a page that declares none. Without this\n\t\t\t// the source's `rowActions` were declared and never read — the third\n\t\t\t// field of the same adapter to be wired late, after `columns` and\n\t\t\t// `addLabel`.\n\t\t\t//\n\t\t\t// Note what this still cannot do: these are MERGED with the built-ins\n\t\t\t// below, so a source can add an action but cannot say \"these and no\n\t\t\t// others\". Suppressing a built-in is the `show*Action` toggles' job.\n\t\t\tconst manifestDeclared = this.actions && this.actions.length > 0\n\t\t\tconst declaredActions = manifestDeclared\n\t\t\t\t? this.actions\n\t\t\t\t: ((this.isNamedSource && this.namedSource && this.namedSource.rowActions) || [])\n\n\t\t\tconst declared = []\n\t\t\tfor (const a of declaredActions) {\n\t\t\t\tif (a && typeof a === 'object' && !Array.isArray(a)) {\n\t\t\t\t\t// A SOURCE action may say `action: 'open'`: open this row the\n\t\t\t\t\t// way a row click does — the source's own navigation\n\t\t\t\t\t// (`openRow`/`detailRoute`), with `edit-open` emitted so a\n\t\t\t\t\t// host that configured a row target (`config.rowRoute`) wins.\n\t\t\t\t\t// Resolved here because a source is a composable with no\n\t\t\t\t\t// router; the flows source declared exactly this action and\n\t\t\t\t\t// it rendered as a dead menu entry, because `action` was a\n\t\t\t\t\t// key nothing read (dispatch reads `handler` and `type`).\n\t\t\t\t\t// Source-declared actions only: the manifest grammar for\n\t\t\t\t\t// navigation is `type`/`handler`, and stays so.\n\t\t\t\t\tif (!manifestDeclared && a.action === 'open' && typeof a.handler !== 'function') {\n\t\t\t\t\t\tdeclared.push({ ...a, handler: (row) => this.openSourceRow(row) })\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\t\t\t\t\tdeclared.push(dispatchAction(a, ctx))\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.warn(`[CnIndexPage] Ignoring action ${JSON.stringify(a)}: actions must be objects with an id and a label. To show a built-in action use the showViewAction / showEditAction / showCopyAction / showDeleteAction props.`)\n\t\t\t}\n\t\t\treturn [...declared, ...this.defaultActions]\n\t\t},\n\n\t\thasRowActions() {\n\t\t\treturn this.$slots['row-actions'] || this.mergedActions.length > 0\n\t\t},\n\n\t\t/**\n\t\t * The quick-filter tabs to render: the `quickFilters` prop when set,\n\t\t * else a named source's own tabs (`useNamedSource` resolved the same\n\t\t * precedence for the LOAD side, so strip and fetch cannot disagree).\n\t\t *\n\t\t * @return {Array<object>|null} The tabs, or null when there are none.\n\t\t */\n\t\teffectiveQuickFilters() {\n\t\t\tif (this.quickFilters && this.quickFilters.length > 0) {\n\t\t\t\treturn this.quickFilters\n\t\t\t}\n\t\t\treturn (this.isNamedSource && this.namedQuickFilters) || null\n\t\t},\n\n\t\t/**\n\t\t * Whether the Add button renders. A named source that declares\n\t\t * `showAdd: false` suppresses it (a task is created by a flow, never\n\t\t * by a person clicking Add); the source cannot force the button ON,\n\t\t * so an explicit `:show-add=\"false\"` prop always still wins.\n\t\t *\n\t\t * @return {boolean} True when the Add button should render.\n\t\t */\n\t\teffectiveShowAdd() {\n\t\t\tif (this.isNamedSource && this.namedSource && this.namedSource.showAdd === false) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\treturn this.showAdd\n\t\t},\n\n\t\t/** Whether all visible items are selected */\n\t\tallSelected() {\n\t\t\tif (this.effectiveObjects.length === 0 || this.internalSelectedIds.length === 0) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\treturn this.effectiveObjects.every((o) => this.internalSelectedIds.includes(o[this.rowKey]))\n\t\t},\n\n\t\t/** Full objects for the selected IDs (used by mass action dialogs) */\n\t\tselectedObjects() {\n\t\t\treturn this.effectiveObjects.filter((o) => this.internalSelectedIds.includes(o[this.rowKey]))\n\t\t},\n\n\t\t/** Column slot names that the parent has provided (for pass-through) */\n\t\tslotColumns() {\n\t\t\treturn Object.keys(this.$slots)\n\t\t\t\t.filter((name) => name.startsWith('column-'))\n\t\t\t\t.map((name) => name.replace('column-', ''))\n\t\t},\n\n\t\t/**\n\t\t * `createDefaults`, token-resolved. `resolveDeepTokens` rather than the\n\t\t * shallow filter-map resolver: a seed record is arbitrary JSON, so a\n\t\t * token can sit inside a nested object or an array of them.\n\t\t *\n\t\t * @return {object|null}\n\t\t */\n\t\tresolvedCreateDefaults() {\n\t\t\tconst seed = this.createDefaults\n\t\t\tif (!seed || typeof seed !== 'object' || Array.isArray(seed)) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\treturn resolveDeepTokens(seed, {})\n\t\t},\n\n\t\t/** Add button label — derived from schema.title if not explicitly set */\n\t\tresolvedAddLabel() {\n\t\t\tif (this.addLabel) {\n\t\t\t\treturn this.cnTranslate(this.addLabel)\n\t\t\t}\n\t\t\t// A named source names its own create action. Without this the button\n\t\t\t// falls back to a schema-derived noun, and a named source has no\n\t\t\t// schema — so the control the migration was supposed to preserve\n\t\t\t// simply is not there.\n\t\t\tif (this.isNamedSource && this.namedSource && this.namedSource.addLabel) {\n\t\t\t\treturn this.namedSource.addLabel\n\t\t\t}\n\t\t\t// Two catalogues, deliberately. The NOUN comes from the consumer's\n\t\t\t// schema, so it resolves against the consumer's catalogue via the\n\t\t\t// injected `cnTranslate`; the SENTENCE around it is library chrome, so\n\t\t\t// it resolves against the library's own — exactly the split\n\t\t\t// CnFormDialog already makes for \"Create {title}\".\n\t\t\t//\n\t\t\t// Getting that wrong is visible rather than subtle: routing the\n\t\t\t// sentence through the host catalogue too produced \"Add Urenboeking\"\n\t\t\t// on a Dutch instance, because no app catalogue carries a key that\n\t\t\t// belongs to the library.\n\t\t\tconst type = this.cnTranslate(this.effectiveSchema?.title || 'Item')\n\t\t\treturn t('nextcloud-vue', 'Add {type}', { type })\n\t\t},\n\n\t\t/**\n\t\t * Effective sidebar configuration. Returns the sidebar config object\n\t\t * when `sidebar.enabled === true`, otherwise an `{ enabled: false }`\n\t\t * stub so the embedded CnIndexSidebar is not mounted.\n\t\t */\n\t\tresolvedSidebar() {\n\t\t\tif (this.sidebar && this.sidebar.enabled !== false) {\n\t\t\t\treturn this.sidebar\n\t\t\t}\n\t\t\treturn { enabled: false }\n\t\t},\n\n\t\t/**\n\t\t * Whether a Search/Columns sidebar is configured for this page (mirrors\n\t\t * the mount gate used by `shouldRenderInlineSidebar` /\n\t\t * `publishHoistedSidebar`). Drives the actions-bar toggle visibility.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\thasSidebar() {\n\t\t\treturn !!this.resolvedSidebar.enabled && this.resolvedSidebar.show !== false\n\t\t},\n\n\t\t/** Search props forwarded to the embedded CnIndexSidebar (defaults applied per CnIndexSidebar). */\n\t\tsidebarSearchProps() {\n\t\t\treturn (this.sidebar && this.sidebar.search) || {}\n\t\t},\n\n\t\t/**\n\t\t * Whether the embedded sidebar should render inline inside the\n\t\t * cn-index-page wrapper. False when CnAppRoot has provided a\n\t\t * real `cnIndexSidebarConfig` holder — in that case CnAppRoot\n\t\t * mounts the sidebar at NcContent level (correct NcAppSidebar\n\t\t * parent). True when no CnAppRoot ancestor exists (legacy\n\t\t * apps), so the embedded sidebar still renders even though\n\t\t * its visual position is sub-optimal.\n\t\t */\n\t\tshouldRenderInlineSidebar() {\n\t\t\tif (!this.resolvedSidebar.enabled || this.resolvedSidebar.show === false) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\t// CnAppRoot ancestor present → hoist takes over.\n\t\t\treturn !this.cnHostsIndexSidebar\n\t\t},\n\n\t\t/**\n\t\t * Snapshot of every prop the hoisted CnIndexSidebar needs.\n\t\t * Reactive — the sidebar in CnAppRoot re-renders whenever\n\t\t * any of these change.\n\t\t */\n\t\thoistedSidebarProps() {\n\t\t\treturn {\n\t\t\t\topen: this.sidebarOpen,\n\t\t\t\tschema: this.effectiveSchema,\n\t\t\t\ttitle: this.title,\n\t\t\t\ticon: this.resolvedIcon,\n\t\t\t\tsearchValue: this.effectiveSearchValue,\n\t\t\t\tvisibleColumns: this.effectiveVisibleColumns,\n\t\t\t\tactiveFilters: this.effectiveActiveFilters,\n\t\t\t\tcolumnGroups: this.resolvedSidebar.columnGroups || [],\n\t\t\t\tfilterFields: this.resolvedSidebar.fields || null,\n\t\t\t\tfacetData: this.effectiveFacetData,\n\t\t\t\tshowMetadata: this.resolvedSidebar.showMetadata !== false,\n\t\t\t\t...this.sidebarSearchProps,\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Resolved card component for card-grid view mode. Returns\n\t\t * `null` when `cardComponent` is empty OR when the name is not\n\t\t * in the registry (the latter also logs `console.warn`).\n\t\t *\n\t\t * `null` makes the template fall through to `CnCardGrid`'s\n\t\t * default `CnObjectCard` rendering — exactly the legacy path.\n\t\t *\n\t\t * @return {object|null}\n\t\t */\n\t\tresolvedCardComponent() {\n\t\t\tif (!this.cardComponent) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst resolved = this.resolveNamedComponent(this.cardComponent)\n\t\t\tif (!resolved) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.warn(`[CnIndexPage] cardComponent \"${this.cardComponent}\" not found in the registry or customComponents. Falling back to CnObjectCard.`)\n\t\t\t\treturn null\n\t\t\t}\n\t\t\treturn resolved\n\t\t},\n\n\t\t/**\n\t\t * Custom list-row component resolved against the customComponents\n\t\t * registry, or `null` when `listComponent` is empty / unknown (default\n\t\t * `CnObjectRow` is used). Mirrors `resolvedCardComponent`.\n\t\t *\n\t\t * @return {?object} The resolved component, or null.\n\t\t */\n\t\tresolvedListComponent() {\n\t\t\tif (!this.listComponent) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst resolved = this.resolveNamedComponent(this.listComponent)\n\t\t\tif (!resolved) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.warn(`[CnIndexPage] listComponent \"${this.listComponent}\" not found in the registry or customComponents. Falling back to CnObjectRow.`)\n\t\t\t\treturn null\n\t\t\t}\n\t\t\treturn resolved\n\t\t},\n\t},\n\n\twatch: {\n\t\t// Remember the whole folder set whenever no folder narrows the query.\n\t\tfolderSidebarLiveFacetValues: {\n\t\t\timmediate: true,\n\t\t\thandler(values) {\n\t\t\t\tif (this.selectedFolderId === null || this.selectedFolderId === undefined) {\n\t\t\t\t\tthis.folderSidebarAllValues = values\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\n\t\tviewMode(val) {\n\t\t\tthis.currentViewMode = val\n\t\t},\n\n\t\tselectedIds(val) {\n\t\t\tthis.internalSelectedIds = [...val]\n\t\t},\n\n\t\t/**\n\t\t * Tenant change pipeline (multi-tenancy-context).\n\t\t * When the bound `activeOrganisation` prop changes, push the\n\t\t * new UUID into the bound object store (when there is one)\n\t\t * AND into the shared `useTenantContext()` context so every\n\t\t * other tenant-aware surface stays consistent.\n\t\t */\n\t\tactiveOrganisation: {\n\t\t\thandler(next) {\n\t\t\t\tif (!next) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tconst uuid = next.uuid || null\n\t\t\t\t// Update the object store when one is bound — sub-store\n\t\t\t\t// methods may not exist on non-OR stores; guard with typeof.\n\t\t\t\tconst store = this.list?.store ?? this.selfObjectStore ?? null\n\t\t\t\tif (store && typeof store.setActiveTenantOrganisation === 'function') {\n\t\t\t\t\tstore.setActiveTenantOrganisation(uuid)\n\t\t\t\t}\n\t\t\t},\n\n\t\t\tdeep: false,\n\t\t},\n\n\t\t/**\n\t\t * Keep the hoisted sidebar in sync with reactive props.\n\t\t * The watcher fires whenever any of the props snapshot\n\t\t * (`hoistedSidebarProps`) changes; we re-write the entire\n\t\t * config so Vue's NcAppSidebar in CnAppRoot picks up the\n\t\t * new values. Cheap because it's just an object swap.\n\t\t */\n\t\thoistedSidebarProps: {\n\t\t\thandler() {\n\t\t\t\tthis.publishHoistedSidebar()\n\t\t\t},\n\n\t\t\tdeep: false,\n\t\t},\n\n\t\tshouldRenderInlineSidebar() {\n\t\t\t// When the gate flips (e.g. sidebar.show toggled), keep\n\t\t\t// the hoist in sync.\n\t\t\tthis.publishHoistedSidebar()\n\t\t},\n\n\t\t// The gate itself, which neither watcher above catches: under a\n\t\t// CnAppRoot host `shouldRenderInlineSidebar` is false in BOTH states,\n\t\t// and `hoistedSidebarProps` carries none of `enabled` / `show`. So\n\t\t// toggling the sidebar on or off in CnEditSidebarModal — which mutates\n\t\t// `config.sidebar` in place — left the hoisted panel exactly as it was\n\t\t// until the page was reloaded.\n\t\thasSidebar() {\n\t\t\tthis.publishHoistedSidebar()\n\t\t},\n\n\t\t// Re-push AI context when relevant props change\n\t\tregister() {\n\t\t\tthis.pushAiContext()\n\t\t},\n\n\t\tschema() {\n\t\t\tthis.pushAiContext()\n\t\t},\n\n\t\t// In self-fetch mode, a same-component route-param change (e.g. the\n\t\t// `:id` of `/forms/:id/submissions`) must re-resolve `config.filter`\n\t\t// and re-fetch. useListView's `fixedFilters` getter re-reads $route on\n\t\t// each fetch; this watcher triggers the re-fetch.\n\t\t'$route.params': {\n\t\t\tdeep: true,\n\t\t\thandler() {\n\t\t\t\tif (this.isSelfFetchMode && typeof this.list.refresh === 'function') {\n\t\t\t\t\tthis.list.refresh(1)\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\n\t\t// A same-path `$route.query` change (e.g. a dashboard deep-link\n\t\t// `/cases?caseType=X`) must also re-fetch — `fixedFilters` merges the\n\t\t// query into the fetch (see useSelfFetchList.resolveQueryFilters).\n\t\t'$route.query': {\n\t\t\tdeep: true,\n\t\t\thandler() {\n\t\t\t\tif (this.isSelfFetchMode && typeof this.list.refresh === 'function') {\n\t\t\t\t\tthis.list.refresh(1)\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\n\t\t/**\n\t\t * Leaving the narrow full-detail layout puts the list back where it\n\t\t * was. The element was hidden, so the browser kept no scroll height\n\t\t * for it and would otherwise return the handler to row 1 of 400.\n\t\t *\n\t\t * @param {string} next The layout now.\n\t\t * @param {string} previous The layout before it.\n\t\t * @spec openspec/changes/case-page-and-list-as-a-place/specs/index-page/spec.md\n\t\t */\n\t\tsplitLayout(next, previous) {\n\t\t\tif (previous === 'detail' && next !== 'detail') {\n\t\t\t\tthis.restoreListScroll()\n\t\t\t}\n\t\t},\n\n\t\t// When `?action=create` is injected into the query via router navigation\n\t\t// (e.g. a \"New Case\" button on another page), auto-open the create dialog\n\t\t// so the user lands on the index page with the form already open.\n\t\t'$route.query.action': {\n\t\t\thandler(val) {\n\t\t\t\tif (val === 'create') {\n\t\t\t\t\tthis.maybeOpenCreateFromQuery()\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t},\n\n\t/**\n\t * Measure the viewport, follow a resize, and read this person's own\n\t * row order for this list.\n\t *\n\t * @spec openspec/changes/case-page-and-list-as-a-place/specs/index-page/spec.md\n\t */\n\tmounted() {\n\t\tthis.measureSplitViewport()\n\t\tif (this.splitViewEnabled && typeof window !== 'undefined') {\n\t\t\twindow.addEventListener('resize', this.measureSplitViewport)\n\t\t}\n\t\tthis.loadManualOrder()\n\t\tthis.publishHoistedSidebar()\n\t\tthis.pushAiContext()\n\t\tthis.maybeOpenCreateFromQuery()\n\t\tif (this.folderSidebar) {\n\t\t\tthis.loadFolderRegister()\n\t\t\t// Reflect a deep-link filter (e.g. ?caseType=<id>) as the active folder.\n\t\t\tconst key = this.folderSidebar.filterField || this.folderSidebar.field\n\t\t\tconst active = key && this.effectiveActiveFilters[key]\n\t\t\tif (active) {\n\t\t\t\tthis.selectedFolderId = Array.isArray(active) ? active[0] : active\n\t\t\t}\n\t\t}\n\t},\n\n\tcreated() {\n\t\tthis.pushAiContext()\n\t\tif (this.allowSavedViews) {\n\t\t\tthis.fetchSavedViews()\n\t\t}\n\t\tthis.selfActions = createSelfModeActions({\n\t\t\tisSelfFetchMode: () => this.isSelfFetchMode,\n\t\t\tselfObjectStore: () => this.selfObjectStore,\n\t\t\tselfObjectType: () => this.selfObjectType,\n\t\t\tlist: () => this.list,\n\t\t\tregister: () => this.register,\n\t\t\tschema: () => this.schema,\n\t\t\teffectiveObjects: () => this.effectiveObjects,\n\t\t\teffectiveSchema: () => this.effectiveSchema,\n\t\t\tmassActionNameField: () => this.massActionNameField,\n\t\t\teditItem: () => this.editItem,\n\t\t\temit: (event, payload) => this.$emit(event, payload),\n\t\t\tafterCreateSuccess: (saved) => this.afterCreateSuccess(saved),\n\t\t\tsetResults: {\n\t\t\t\tsingleDelete: (r) => this.setSingleDeleteResult(r),\n\t\t\t\tmassDelete: (r) => this.setMassDeleteResult(r),\n\t\t\t\tsingleCopy: (r) => this.setSingleCopyResult(r),\n\t\t\t\tmassCopy: (r) => this.setMassCopyResult(r),\n\t\t\t\tmassImport: (r) => this.setImportResult(r),\n\t\t\t\tmassExport: (r) => this.setExportResult(r),\n\t\t\t\tform: (r) => this.setFormResult(r),\n\t\t\t\tformValidation: (fieldErrors, message) => this.setFormValidationErrors(fieldErrors, message),\n\t\t\t},\n\t\t})\n\t},\n\n\t/**\n\t * Drop the resize listener and the hoisted sidebar.\n\t *\n\t * @spec openspec/changes/case-page-and-list-as-a-place/specs/index-page/spec.md\n\t */\n\tbeforeUnmount() {\n\t\tif (typeof window !== 'undefined') {\n\t\t\twindow.removeEventListener('resize', this.measureSplitViewport)\n\t\t}\n\t\t// Clear the holder so the hoisted sidebar disappears when\n\t\t// the user navigates away from the index page.\n\t\tif (this.cnHostsIndexSidebar && this.cnIndexSidebarConfig) {\n\t\t\tthis.cnIndexSidebarConfig.value = null\n\t\t}\n\t\tapplyAiContext(this.cnAiContext, 'custom')\n\t},\n\n\tmethods: {\n\t\t// ── Split view (case-page-and-list-as-a-place) ──────────────────\n\n\t\t/**\n\t\t * Measure the viewport so the narrow fallback follows a resize and\n\t\t * not only a reload. Measured rather than read off a media query\n\t\t * because the breakpoint is the page's own declaration, not a global.\n\t\t *\n\t\t * @spec openspec/changes/case-page-and-list-as-a-place/specs/index-page/spec.md\n\t\t */\n\t\tmeasureSplitViewport() {\n\t\t\tif (typeof window !== 'undefined' && Number.isFinite(window.innerWidth)) {\n\t\t\t\tthis.splitViewportWidth = window.innerWidth\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Remember where the list is scrolled to.\n\t\t *\n\t\t * The list never unmounts, so the browser keeps the scroll on its\n\t\t * own in the split layout. It does NOT keep it across the narrow\n\t\t * layout, where the element is hidden and has no scroll height at\n\t\t * all, so the position is held here and put back on the way out.\n\t\t *\n\t\t * @param {Event} [event] The scroll event.\n\t\t * @spec openspec/changes/case-page-and-list-as-a-place/specs/index-page/spec.md\n\t\t */\n\t\tonListScroll(event) {\n\t\t\tconst top = event?.target?.scrollTop\n\t\t\tif (Number.isFinite(top)) {\n\t\t\t\tthis.splitScrollTop = top\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Put the list back where it was.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t * @spec openspec/changes/case-page-and-list-as-a-place/specs/index-page/spec.md\n\t\t */\n\t\tasync restoreListScroll() {\n\t\t\tawait this.$nextTick()\n\t\t\tconst el = this.$refs.listScroll\n\t\t\tif (el && Number.isFinite(this.splitScrollTop) && this.splitScrollTop > 0) {\n\t\t\t\tel.scrollTop = this.splitScrollTop\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Close the pane and go back to the list's own address.\n\t\t *\n\t\t * The list is already mounted and already at row 180, so this is a\n\t\t * route change and nothing else: no refetch, no remount, no reset.\n\t\t *\n\t\t * @spec openspec/changes/case-page-and-list-as-a-place/specs/index-page/spec.md\n\t\t */\n\t\tcloseSplitPane() {\n\t\t\t/**\n\t\t\t * @event split-close Emitted when the split pane is closed. The host returns to the list address.\n\t\t\t */\n\t\t\tthis.$emit('split-close')\n\t\t\tconst router = this.$router\n\t\t\tconst target = this.splitCloseRoute || this.$route?.meta?.cnPageId || null\n\t\t\tif (router && target) {\n\t\t\t\trouter.push({ name: target, query: this.$route?.query || {} }).catch(() => {})\n\t\t\t}\n\t\t\tthis.restoreListScroll()\n\t\t},\n\n\t\t/**\n\t\t * A record was saved in the pane: replace its row where it sits.\n\t\t *\n\t\t * Deliberately not a refetch. Refetching the page would be correct\n\t\t * and would also scroll the handler back to the top of four hundred\n\t\t * cases, which is the failure the split view was built to end.\n\t\t *\n\t\t * @param {object} saved The saved record.\n\t\t * @spec openspec/changes/case-page-and-list-as-a-place/specs/index-page/spec.md\n\t\t */\n\t\tonSplitPaneSaved(saved) {\n\t\t\tconst id = rowIdOf(saved, this.rowKey) ?? (this.splitId || null)\n\t\t\tif (id === null || !saved || typeof saved !== 'object') {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.splitRowPatches = { ...this.splitRowPatches, [id]: saved }\n\t\t\t/**\n\t\t\t * @event split-saved Emitted after a record saved in the pane was written onto its row in the list.\n\t\t\t * @type {object}\n\t\t\t */\n\t\t\tthis.$emit('split-saved', saved)\n\t\t},\n\n\t\t// ── Manual order (case-page-and-list-as-a-place) ────────────────\n\n\t\t/**\n\t\t * The key this list's manual order is held under.\n\t\t *\n\t\t * @return {string} The preference key.\n\t\t * @spec openspec/changes/case-page-and-list-as-a-place/specs/index-page/spec.md\n\t\t */\n\t\tmanualOrderPreferenceKey() {\n\t\t\treturn manualOrderKey(this.manualOrderId || this.objectType || this.schema || 'default')\n\t\t},\n\n\t\t/**\n\t\t * Read this person's order for this list out of their preferences.\n\t\t *\n\t\t * A read that fails leaves the list in its loaded order rather than\n\t\t * blocking it: an order nobody can read is a nicety, and a list that\n\t\t * refuses to render because of one is not.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t * @spec openspec/changes/case-page-and-list-as-a-place/specs/index-page/spec.md\n\t\t */\n\t\tasync loadManualOrder() {\n\t\t\tif (!this.manualOrder) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst read = this.cnUserPreferences?.read\n\t\t\tif (typeof read !== 'function') {\n\t\t\t\treturn\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tconst stored = await read(this.manualOrderPreferenceKey(), [])\n\t\t\t\tthis.manualOrderIds = Array.isArray(stored) ? stored.map(String) : []\n\t\t\t} catch {\n\t\t\t\tthis.manualOrderIds = []\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Write this person's order back, and tell the host.\n\t\t *\n\t\t * @param {Array<string>} ids The new order.\n\t\t * @return {Promise<void>}\n\t\t * @spec openspec/changes/case-page-and-list-as-a-place/specs/index-page/spec.md\n\t\t */\n\t\tasync persistManualOrder(ids) {\n\t\t\tthis.manualOrderIds = ids\n\t\t\t/**\n\t\t\t * @event manual-order-change Emitted when this person reorders the list by hand. Payload is the row ids in their order.\n\t\t\t * @type {Array<string>}\n\t\t\t */\n\t\t\tthis.$emit('manual-order-change', ids)\n\t\t\tconst write = this.cnUserPreferences?.write\n\t\t\tif (typeof write === 'function') {\n\t\t\t\ttry {\n\t\t\t\t\tawait write(this.manualOrderPreferenceKey(), ids)\n\t\t\t\t} catch {\n\t\t\t\t\t// A preference that could not be written is a preference\n\t\t\t\t\t// that will not survive the reload. The list is still in\n\t\t\t\t\t// the order the person just chose, so nothing is said.\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Move a row one place up or down. The keyboard half of the drag, and\n\t\t * the one the drag itself calls, so the two cannot disagree.\n\t\t *\n\t\t * @param {string} id The row being moved.\n\t\t * @param {number} delta -1 for up, 1 for down.\n\t\t * @return {Promise<void>}\n\t\t * @spec openspec/changes/case-page-and-list-as-a-place/specs/index-page/spec.md\n\t\t */\n\t\tmoveRowByHand(id, delta) {\n\t\t\treturn this.persistManualOrder(moveInOrder(visibleIdsOf(this.displayObjects, this.rowKey), id, delta))\n\t\t},\n\n\t\t/**\n\t\t * Place a dragged row at an index.\n\t\t *\n\t\t * @param {string} id The row being dragged.\n\t\t * @param {number} toIndex The index it was dropped at.\n\t\t * @return {Promise<void>}\n\t\t * @spec openspec/changes/case-page-and-list-as-a-place/specs/index-page/spec.md\n\t\t */\n\t\tdropRowByHand(id, toIndex) {\n\t\t\treturn this.persistManualOrder(dropInOrder(visibleIdsOf(this.displayObjects, this.rowKey), id, toIndex))\n\t\t},\n\n\t\t/**\n\t\t * Resolve a component the page config names — `cardComponent`,\n\t\t * `listComponent` — out of the v2 registry (any kind carrying a\n\t\t * `component`, as a slot lookup does) and then the legacy\n\t\t * customComponents map.\n\t\t *\n\t\t * @param {string} name The registered component name.\n\t\t * @return {object|null} The component, or null when nothing answers.\n\t\t */\n\t\tresolveNamedComponent(name) {\n\t\t\tconst entry = this.effectiveRegistry[name]\n\t\t\tif (entry && entry.component) {\n\t\t\t\treturn entry.component\n\t\t\t}\n\t\t\treturn this.effectiveCustomComponents[name] || null\n\t\t},\n\n\t\t/**\n\t\t * Whether a handler name matches something registered that is not\n\t\t * callable — a named component where a function belongs. Tells a\n\t\t * typo (silent emit-only) apart from a mis-registration (warned).\n\t\t *\n\t\t * @param {string} name The handler name from the manifest.\n\t\t * @return {boolean}\n\t\t */\n\t\tnamesSomethingUnusable(name) {\n\t\t\tconst candidates = [this.effectiveRegistry[name], this.resolvedCustomComponents[name]]\n\t\t\treturn candidates.some((v) => v !== undefined && v !== null)\n\t\t},\n\n\t\t/**\n\t\t * Resolve a declarative `headerActions[]` entry's `handler`\n\t\t * field into the final dispatchable shape. Mirrors the\n\t\t * row-level `actions[].handler` keyword set used by\n\t\t * manifest-actions-dispatch — `navigate`, `emit`, `none`, or a\n\t\t * registry name (resolved through `resolveRegisteredHandler`).\n\t\t *\n\t\t * @param {object} entry Raw headerActions entry.\n\t\t * @return {object} Possibly-mutated copy: function-typed\n\t\t * handlers untouched; `'emit'` and unknown registry names\n\t\t * strip the `handler` (emit-only fall-through); `'navigate'`\n\t\t * becomes a `$router.push` thunk (with the entry's literal\n\t\t * `params` map when present); `'none'` becomes a no-op +\n\t\t * `_dispatchSuppress: true`; a registry name resolves to a\n\t\t * `fn({ actionId })` thunk.\n\t\t */\n\t\tresolveHeaderHandler(entry) {\n\t\t\tif (!entry) {\n\t\t\t\treturn entry\n\t\t\t}\n\t\t\tconst handler = entry.handler\n\t\t\tif (typeof handler === 'function') {\n\t\t\t\treturn { ...entry }\n\t\t\t}\n\t\t\tif (typeof handler !== 'string') {\n\t\t\t\t// No handler declared — emit-only fall-through.\n\t\t\t\treturn { ...entry }\n\t\t\t}\n\t\t\tif (handler === 'navigate') {\n\t\t\t\tif (!entry.route) {\n\t\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\t\tconsole.warn(`CnIndexPage: headerActions[].id \"${entry.id}\" handler:\"navigate\" requires \"route\"; falling back to emit-only`)\n\t\t\t\t\tconst { handler: _ignored, ...rest } = entry\n\t\t\t\t\treturn { ...rest }\n\t\t\t\t}\n\t\t\t\tconst route = entry.route\n\t\t\t\tconst router = this.$router\n\t\t\t\t// Literal params let a header action navigate to a detail route\n\t\t\t\t// with fixed params, e.g. a \"New X\" button → `{ id: \"new\" }`.\n\t\t\t\tconst params = (entry.params && typeof entry.params === 'object') ? entry.params : null\n\t\t\t\tconst location = params ? { name: route, params } : { name: route }\n\t\t\t\t// A resolvable route renders as a real link in the bar, which does\n\t\t\t\t// the navigating; the entry then carries no handler to push again.\n\t\t\t\tif (routeHref(location, router)) {\n\t\t\t\t\tconst { handler: _ignored, ...rest } = entry\n\t\t\t\t\treturn { ...rest, to: location }\n\t\t\t\t}\n\t\t\t\tconst out = { ...entry }\n\t\t\t\tout.handler = () => {\n\t\t\t\t\tif (router && typeof router.push === 'function') {\n\t\t\t\t\t\trouter.push(location)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn out\n\t\t\t}\n\t\t\tif (handler === 'emit') {\n\t\t\t\tconst { handler: _ignored, ...rest } = entry\n\t\t\t\treturn { ...rest }\n\t\t\t}\n\t\t\tif (handler === 'none') {\n\t\t\t\treturn { ...entry, handler: () => {}, _dispatchSuppress: true }\n\t\t\t}\n\t\t\t// Registry name lookup.\n\t\t\tconst resolved = resolveRegisteredHandler(handler, this.effectiveRegistry, this.resolvedCustomComponents)\n\t\t\tif (typeof resolved === 'function') {\n\t\t\t\tconst id = entry.id\n\t\t\t\treturn { ...entry, handler: () => resolved({ actionId: id }) }\n\t\t\t}\n\t\t\tif (this.namesSomethingUnusable(handler)) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.warn(`CnIndexPage: headerActions[].handler \"${handler}\" resolved to a non-function in the registry or customComponents; falling back to emit-only`)\n\t\t\t\tconst { handler: _ignored, ...rest } = entry\n\t\t\t\treturn { ...rest }\n\t\t\t}\n\t\t\t// Unknown registry name — silent emit-only fall-through.\n\t\t\tconst { handler: _ignored, ...rest } = entry\n\t\t\treturn { ...rest }\n\t\t},\n\n\t\t/**\n\t\t * Click dispatch from CnActionsBar's `@bulk-action`.\n\t\t *\n\t\t * 🔴 THE SELECTION IS PASSED, NEVER LOOKED UP. Everything a bulk\n\t\t * action does is defined by which rows were selected, and the strip\n\t\t * that raised the event is the thing that knows. A handler that reads\n\t\t * the selection from somewhere else is one re-render away from acting\n\t\t * on a different set than the user saw highlighted.\n\t\t *\n\t\t * Resolution mirrors `headerActions[]`:\n\t\t * - a function handler is called with `{ actionId, selectedIds, count }`\n\t\t * - `open-modal` (with `target`) opens the registered modal, with the\n\t\t * selection merged into its props\n\t\t * - a registry name resolves through the v2 registry, then the\n\t\t * legacy `customComponents`\n\t\t * - anything else falls through to emit-only\n\t\t *\n\t\t * `bulk-action` is emitted either way, so a host can listen instead of\n\t\t * declaring a handler.\n\t\t *\n\t\t * @param {{id: string, action: string, selectedIds: Array, count: number}} payload Bar payload.\n\t\t */\n\t\tonBulkAction(payload) {\n\t\t\tconst id = payload && (payload.id ?? payload.action)\n\t\t\tconst selectedIds = (payload && Array.isArray(payload.selectedIds)) ? payload.selectedIds : []\n\t\t\tconst count = selectedIds.length\n\t\t\tconst entry = this.mergedBulkActions.find((e) => e.id === id)\n\n\t\t\tif (entry) {\n\t\t\t\tconst handler = entry.handler\n\t\t\t\tconst scope = { actionId: id, selectedIds, count }\n\n\t\t\t\tif (typeof handler === 'function') {\n\t\t\t\t\thandler(scope)\n\t\t\t\t} else if (handler === 'open-modal' || (!handler && entry.target)) {\n\t\t\t\t\tthis.openBulkModal(entry, selectedIds, count)\n\t\t\t\t} else if (typeof handler === 'string' && handler !== 'emit' && handler !== 'none') {\n\t\t\t\t\tconst resolved = resolveRegisteredHandler(handler, this.effectiveRegistry, this.resolvedCustomComponents)\n\t\t\t\t\tif (typeof resolved === 'function') {\n\t\t\t\t\t\tresolved(scope)\n\t\t\t\t\t} else if (this.namesSomethingUnusable(handler)) {\n\t\t\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\t\t\tconsole.warn(`CnIndexPage: bulkActions[].handler \"${handler}\" resolved to a non-function in the registry or customComponents; falling back to emit-only`)\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tif (entry._dispatchSuppress || entry.handler === 'none') {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t/**\n\t\t\t * @event bulk-action A declarative bulk action was triggered from the selection strip. Payload: `{ action, id, selectedIds, count }`. The SELECTION travels with it, because an action that has to go and find out what was selected is one re-render away from acting on a different set than the user saw highlighted.\n\t\t\t */\n\t\t\tthis.$emit('bulk-action', { action: id, id, selectedIds, count })\n\t\t},\n\n\t\t/**\n\t\t * Open a bulk action's modal with the selection in its props.\n\t\t *\n\t\t * The selection is merged UNDER the entry's own props, so a manifest\n\t\t * that names a `selectedIds` prop of its own still wins — but it also\n\t\t * means such a manifest silently stops receiving the live selection,\n\t\t * which is why the collision is warned about rather than allowed\n\t\t * quietly.\n\t\t *\n\t\t * @param {object} entry The bulk action entry.\n\t\t * @param {Array} selectedIds The current selection.\n\t\t * @param {number} count Its length.\n\t\t */\n\t\topenBulkModal(entry, selectedIds, count) {\n\t\t\tconst target = entry.target\n\t\t\tif (!target) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.warn(`CnIndexPage: bulkActions[].id \"${entry.id}\" handler:\"open-modal\" requires \"target\"; falling back to emit-only`)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst own = (entry.props && typeof entry.props === 'object') ? entry.props : {}\n\t\t\tif (Object.hasOwn(own, 'selectedIds')) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.warn(`CnIndexPage: bulkActions[].id \"${entry.id}\" declares its own \"selectedIds\" prop, which shadows the live selection; the modal will not see what the user selected.`)\n\t\t\t}\n\t\t\t/**\n\t\t\t * @event open-modal A bulk action of type `open-modal` asks the host to open a registered modal. Payload: `{ target, props }`, where `props` carries `selectedIds` and `count` merged UNDER the action's own props.\n\t\t\t */\n\t\t\tthis.$emit('open-modal', { target, props: { selectedIds, count, ...own } })\n\t\t},\n\n\t\t/**\n\t\t * Click dispatch from CnActionsBar's `@header-action`. Looks\n\t\t * up the resolved entry by id, invokes its handler if any,\n\t\t * and (unless the entry is `'none'`-suppressed) emits\n\t\t * `@header-action({ action: id, id })` upward.\n\t\t *\n\t\t * @param {{action: string, id: string}} payload Bar payload.\n\t\t */\n\t\tonHeaderAction(payload) {\n\t\t\tconst id = payload && (payload.id ?? payload.action)\n\t\t\tconst entry = this.mergedHeaderActions.find((e) => e.id === id)\n\t\t\tif (entry && typeof entry.handler === 'function') {\n\t\t\t\tentry.handler()\n\t\t\t}\n\t\t\tif (entry && entry._dispatchSuppress) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.$emit('header-action', { action: id, id })\n\t\t},\n\n\t\tpushAiContext() {\n\t\t\tapplyAiContext(this.cnAiContext, 'index', {\n\t\t\t\tregister: this.register,\n\t\t\t\tschema: this.schema,\n\t\t\t\teffectiveSchema: this.effectiveSchema,\n\t\t\t})\n\t\t},\n\n\t\t// ── List-event handlers ──────────────────────────────────────────────\n\t\t// In self-fetch mode each routes to the useListView instance (re-fetch);\n\t\t// in every mode the event still bubbles via $emit so a host that wants\n\t\t// to observe (or take over) keeps working — unchanged for consumers.\n\t\t/**\n\t\t * @param {string} value Search term from the sidebar / header.\n\t\t * @return {void}\n\t\t */\n\t\tonSearchEvent(value) {\n\t\t\tif (this.isSelfFetchMode && typeof this.list.onSearch === 'function') {\n\t\t\t\tthis.list.onSearch(value)\n\t\t\t\tthis.persistViewStateToRoute(this.currentViewState())\n\t\t\t}\n\t\t\tthis.$emit('search', value)\n\t\t},\n\n\t\t/**\n\t\t * Quick-filter tab change. Updates the active index — the `setup()`\n\t\t * watcher then triggers `list.refresh(1)` so the new tab's filter\n\t\t * flows into the next fetch.\n\t\t *\n\t\t * @param {number} index Zero-based tab index (from CnQuickFilterBar).\n\t\t * @return {void}\n\t\t */\n\t\tonQuickFilterChange(index) {\n\t\t\t// `activeQuickFilterIndex` is a setup-returned ref; the\n\t\t\t// Vue 2 ref-unwrap proxy makes plain assignment work.\n\t\t\tthis.activeQuickFilterIndex = index\n\t\t\tthis.$emit('quick-filter-change', index)\n\t\t},\n\n\t\t/**\n\t\t * Multi-select quick-filter change (`quickFilterMultiple`). Updates the\n\t\t * selected-index array; the `setup()` watcher re-fetches with the\n\t\t * OR-ed union of the selected tabs' filters.\n\t\t *\n\t\t * @param {number[]} indices Selected tab indices (from CnQuickFilterBar).\n\t\t * @return {void}\n\t\t */\n\t\tonQuickFilterMultiChange(indices) {\n\t\t\tthis.selectedQuickFilterIndices = Array.isArray(indices) ? indices : []\n\t\t\tthis.$emit('quick-filter-change', this.selectedQuickFilterIndices)\n\t\t},\n\n\t\t/**\n\t\t * @param {{key: string, order: string, keys?: Array<{key: string, order: string}>}} payload Sort change from CnDataTable.\n\t\t * @return {void}\n\t\t */\n\t\tonSortEvent(payload) {\n\t\t\tif (this.isSelfFetchMode && typeof this.list.onSort === 'function') {\n\t\t\t\tthis.list.onSort(payload)\n\t\t\t\tthis.persistViewStateToRoute(this.currentViewState())\n\t\t\t}\n\t\t\tthis.$emit('sort', payload)\n\t\t},\n\n\t\t/**\n\t\t * Self-fetch mode's current filters/search/sort, straight from\n\t\t * useListView's own reactive state — the shape `persistViewStateToRoute`\n\t\t * and the saved-views helpers (`buildViewCreatePayload`,\n\t\t * `extractViewState`) all share.\n\t\t *\n\t\t * @return {{filters: object, search: string, sortKey: ?string, sortOrder: string, sortKeys: Array<{key: string, order: string}>}}\n\t\t */\n\t\tcurrentViewState() {\n\t\t\treturn {\n\t\t\t\tfilters: this.list.activeFilters.value,\n\t\t\t\tsearch: this.list.searchTerm.value,\n\t\t\t\tsortKeys: this.list.sortKeys?.value || [],\n\t\t\t\tsortKey: this.list.sortKey.value,\n\t\t\t\tsortOrder: this.list.sortOrder.value,\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * The spelling a filter goes back into the query as. A value that arrived\n\t\t * as an `@`-token keeps the TOKEN for as long as it still resolves to\n\t\t * what is active — otherwise a shared `?assignee=@me` link would be\n\t\t * rewritten to one named uid on the first filter change and stop meaning\n\t\t * \"me\" for whoever opens it next.\n\t\t *\n\t\t * @param {string} key The filter key, as it sits in the query.\n\t\t * @param {unknown} value The resolved value now active.\n\t\t * @return {unknown} The token, or the value.\n\t\t */\n\t\tfilterQuerySpelling(key, value) {\n\t\t\tconst raw = this.$route.query[key]\n\t\t\tif (typeof raw !== 'string' || raw.charAt(0) !== '@') {\n\t\t\t\treturn value\n\t\t\t}\n\t\t\tconst ctx = typeof this.selfFetchTokenCtx === 'function' ? this.selfFetchTokenCtx() : {}\n\t\t\treturn String(resolveFilterValue(raw, ctx)) === String(value) ? raw : value\n\t\t},\n\n\t\t/**\n\t\t * Persist filters + search + sort into `$route.query` in one replace,\n\t\t * so a reload or a shared/bookmarked link reproduces the exact same\n\t\t * view. Self-fetch mode only. The page's own filter keys are cleared and\n\t\t * re-applied wholesale each call rather than merged (otherwise a cleared\n\t\t * filter would never leave the query); a non-reserved key it never\n\t\t * claimed is left alone. Best-effort: a duplicate-navigation rejection\n\t\t * (same resulting path/query) is swallowed.\n\t\t *\n\t\t * @param {{filters?: object, search?: string, sortKey?: ?string, sortOrder?: string, sortKeys?: Array<{key: string, order: string}>}} state Current view state.\n\t\t * @return {void}\n\t\t */\n\t\tpersistViewStateToRoute(state) {\n\t\t\tif (!this.$router || !this.$route) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst query = { ...this.$route.query }\n\t\t\tfor (const key of this.persistedFilterKeys) {\n\t\t\t\tdelete query[key]\n\t\t\t}\n\t\t\tconst written = []\n\t\t\tfor (const [key, value] of Object.entries(state.filters || {})) {\n\t\t\t\tif (value === undefined || value === null || value === '' || (Array.isArray(value) && value.length === 0)) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tquery[key] = this.filterQuerySpelling(key, value)\n\t\t\t\twritten.push(key)\n\t\t\t}\n\t\t\tthis.persistedFilterKeys = written\n\t\t\tif (state.search) {\n\t\t\t\tquery._search = state.search\n\t\t\t} else {\n\t\t\t\tdelete query._search\n\t\t\t}\n\t\t\tconst sortKeys = Array.isArray(state.sortKeys) && state.sortKeys.length\n\t\t\t\t? state.sortKeys\n\t\t\t\t: (state.sortKey ? [{ key: state.sortKey, order: state.sortOrder || 'asc' }] : [])\n\t\t\tif (sortKeys.length) {\n\t\t\t\tquery._order = JSON.stringify(sortKeys.map((k) => ({ key: k.key, order: k.order || 'asc' })))\n\t\t\t} else {\n\t\t\t\tdelete query._order\n\t\t\t}\n\t\t\tthis.$router.replace({ query }).catch(() => {})\n\t\t},\n\n\t\t/**\n\t\t * \"Clear all\" from the sidebar: reset search, every active filter, the\n\t\t * sort and the folder-sidebar selection in one fetch + one route\n\t\t * replace, rather than one of each per field. Self-fetch mode only; a\n\t\t * consumer-managed page gets the bare event to handle itself.\n\t\t *\n\t\t * The SORT goes too, and that is the whole point: applying a saved view\n\t\t * sets a sort the reader never chose and cannot see the origin of, so a\n\t\t * clear that left it behind left the list in a state with no control\n\t\t * anywhere on the page to undo it.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tonClearFilters() {\n\t\t\tif (!this.isSelfFetchMode) {\n\t\t\t\tthis.$emit('clear-filters')\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.list.activeFilters.value = {}\n\t\t\tthis.list.searchTerm.value = ''\n\t\t\tthis.list.sortKeys.value = []\n\t\t\tthis.list.sortKey.value = null\n\t\t\tthis.list.sortOrder.value = 'asc'\n\t\t\tif (this.folderSidebar) {\n\t\t\t\tthis.selectedFolderId = null\n\t\t\t}\n\t\t\tthis.list.refresh(1)\n\t\t\tthis.persistViewStateToRoute(this.currentViewState())\n\t\t\tthis.$emit('clear-filters')\n\t\t},\n\n\t\t/**\n\t\t * @param {number} page Requested page from CnPagination.\n\t\t * @return {void}\n\t\t */\n\t\tonPageEvent(page) {\n\t\t\tif (this.isSelfFetchMode && typeof this.list.onPageChange === 'function') {\n\t\t\t\tthis.list.onPageChange(page)\n\t\t\t}\n\t\t\tthis.$emit('page-changed', page)\n\t\t},\n\n\t\t/**\n\t\t * @param {{key: string, values: Array}} payload Facet-filter change from the sidebar.\n\t\t * @return {void}\n\t\t */\n\t\tonFilterEvent(payload) {\n\t\t\tif (this.isSelfFetchMode && typeof this.list.onFilterChange === 'function') {\n\t\t\t\tthis.list.onFilterChange(payload.key, payload.values)\n\t\t\t\tthis.persistViewStateToRoute(this.currentViewState())\n\t\t\t}\n\t\t\t// A named source keeps its own filter state: nothing else holds it,\n\t\t\t// and the `activeFilters` PROP only reaches pages with a consumer\n\t\t\t// component behind them. A manifest page has none, which is why\n\t\t\t// its sidebar used to render controls that narrowed nothing.\n\t\t\tif (this.isNamedSource) {\n\t\t\t\tthis.setNamedFilter(payload.key, payload.values)\n\t\t\t}\n\t\t\tthis.$emit('filter-change', payload)\n\t\t},\n\n\t\t/**\n\t\t * Record one sidebar choice on a named-source page and mirror it into\n\t\t * the URL.\n\t\t *\n\t\t * The ref is REPLACED rather than mutated in place so the watcher in\n\t\t * `useNamedSource` fires on the first change too, and the reload it\n\t\t * triggers is the only place the new query is built.\n\t\t *\n\t\t * @param {string} key The sidebar field key.\n\t\t * @param {Array|object|null} values The chosen values, or a `{ from, to }` range.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tsetNamedFilter(key, values) {\n\t\t\tconst next = { ...(this.namedActiveFilters || {}) }\n\t\t\tconst asQuery = namedFilterToQuery(values)\n\t\t\tif (asQuery === '') {\n\t\t\t\tdelete next[key]\n\t\t\t} else {\n\t\t\t\tnext[key] = values\n\t\t\t}\n\t\t\tthis.namedActiveFilters = next\n\t\t\tthis.persistNamedFiltersToRoute(next)\n\t\t},\n\n\t\t/**\n\t\t * Write the active named-source filters into `$route.query`.\n\t\t *\n\t\t * So a filtered view can be SHARED, which is half of what a search\n\t\t * field is for: the colleague who opens the link sees the same list,\n\t\t * because `namedFiltersFromRoute` reads it back on mount.\n\t\t *\n\t\t * `replace`, not `push`: narrowing a list is not a place in the\n\t\t * history, and a back button that steps through six filter choices\n\t\t * never reaches the page before them. A rejected navigation (the same\n\t\t * path and query) is swallowed, matching `persistSortToRoute`.\n\t\t *\n\t\t * @param {object} filters The active `{ key: values }` map.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tpersistNamedFiltersToRoute(filters) {\n\t\t\tif (!this.$router || !this.$route) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst properties = declaredFilterFields(this)\n\t\t\tconst query = { ...this.$route.query }\n\t\t\tfor (const key of Object.keys(properties)) {\n\t\t\t\tif (properties[key] && properties[key].facetable === true) {\n\t\t\t\t\tdelete query[key]\n\t\t\t\t}\n\t\t\t}\n\t\t\tfor (const [key, values] of Object.entries(filters || {})) {\n\t\t\t\tconst value = namedFilterToQuery(values)\n\t\t\t\tif (value !== '') {\n\t\t\t\t\tquery[key] = value\n\t\t\t\t}\n\t\t\t}\n\t\t\tthis.$router.replace({ query }).catch(() => {})\n\t\t},\n\n\t\t/**\n\t\t * Folder selection from the opt-in folder sidebar: filter the list by the\n\t\t * config's `filterField` (or `field`); a null id clears it.\n\t\t *\n\t\t * @param {(string|number|null)} folderId The selected folder id (null = All).\n\t\t * @return {void}\n\t\t */\n\t\t/**\n\t\t * The actions one row offers: this page's declared actions narrowed to\n\t\t * the ones the server says this caller may run on that record. The\n\t\t * order, label and icon stay the page's.\n\t\t *\n\t\t * @param {object} row The row.\n\t\t * @return {Array<object>} The actions to render for that row.\n\t\t * @spec openspec/changes/working-list-row-actions/specs/index-page/spec.md\n\t\t */\n\t\t/**\n\t\t * Move the keyboard's focus one row, staying inside the list.\n\t\t *\n\t\t * @param {number} delta 1 for the next row, -1 for the previous one.\n\t\t * @return {void}\n\t\t * @spec openspec/changes/working-list-row-actions/specs/index-page/spec.md\n\t\t */\n\t\tmoveFocusedRow(delta) {\n\t\t\tconst rows = this.displayObjects || []\n\t\t\tif (rows.length === 0) {\n\t\t\t\tthis.focusedRowIndex = -1\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst next = this.focusedRowIndex + delta\n\t\t\tthis.focusedRowIndex = Math.min(Math.max(next, 0), rows.length - 1)\n\t\t},\n\n\t\t/**\n\t\t * Select or deselect the focused row.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\ttoggleFocusedRowSelection() {\n\t\t\tconst row = this.focusedRow\n\t\t\tif (!row) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst id = row[this.rowKey]\n\t\t\tconst selected = this.internalSelectedIds.includes(id)\n\t\t\tthis.onSelect(selected\n\t\t\t\t? this.internalSelectedIds.filter((other) => other !== id)\n\t\t\t\t: [...this.internalSelectedIds, id])\n\t\t},\n\n\t\t/**\n\t\t * Run the first action the focused row offers. \"First\" is the page's\n\t\t * order, narrowed by what the server allows on that record, so the\n\t\t * primary action is never one this caller may not run.\n\t\t *\n\t\t * @return {void}\n\t\t * @spec openspec/changes/working-list-row-actions/specs/index-page/spec.md\n\t\t */\n\t\trunPrimaryRowAction() {\n\t\t\tconst row = this.focusedRow\n\t\t\tif (!row) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst [first] = this.rowActionsFor(row)\n\t\t\tif (!first) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (typeof first.handler === 'function') {\n\t\t\t\tfirst.handler(row)\n\t\t\t}\n\t\t\tthis.$emit('action', { action: first.label, row })\n\t\t},\n\n\t\t/**\n\t\t * Handle a key pressed on the list.\n\t\t *\n\t\t * @param {KeyboardEvent} event The event.\n\t\t * @return {void}\n\t\t * @spec openspec/changes/working-list-row-actions/specs/index-page/spec.md\n\t\t */\n\t\tonListKeydown(event) {\n\t\t\tif (!this.listShortcuts) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst shortcut = shortcutFor(event)\n\t\t\tif (!shortcut) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst run = this.listShortcutHandlers[shortcut.id]\n\t\t\tif (typeof run !== 'function') {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tevent.preventDefault()\n\t\t\trun()\n\t\t},\n\n\t\t/**\n\t\t * Open the quick edit on one row.\n\t\t *\n\t\t * @param {object} row The row.\n\t\t * @return {void}\n\t\t * @spec openspec/changes/working-list-row-actions/specs/index-page/spec.md\n\t\t */\n\t\topenQuickEdit(row) {\n\t\t\tif (this.quickEditFields.length === 0 || !row) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.quickEditServerRow = null\n\t\t\tthis.quickEditRow = row\n\t\t},\n\n\t\t/**\n\t\t * Write a quick edit's patch onto its row in the list.\n\t\t *\n\t\t * The patch goes through the same row-patch map a record saved in the\n\t\t * split pane goes through, so the list keeps its scroll, its selection\n\t\t * and its page for the same reason it already did: nothing about the\n\t\t * list changed, one row's fields did.\n\t\t *\n\t\t * @param {object} payload The dialog's payload.\n\t\t * @param {(string|number)} payload.id The row id.\n\t\t * @param {object} payload.patch The fields to write.\n\t\t * @return {void}\n\t\t * @spec openspec/changes/working-list-row-actions/specs/index-page/spec.md\n\t\t */\n\t\tonQuickEditSave({ id, patch }) {\n\t\t\tif (id === undefined || id === null || !patch || Object.keys(patch).length === 0) {\n\t\t\t\tthis.quickEditRow = null\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst current = this.splitRowPatches[id] || {}\n\t\t\tthis.splitRowPatches = { ...this.splitRowPatches, [id]: { ...current, ...patch } }\n\t\t\tthis.quickEditRow = null\n\t\t\t/**\n\t\t\t * @event quick-edit-save Emitted with the fields a quick edit wrote onto a row.\n\t\t\t * @type {{id: (string|number), patch: object}}\n\t\t\t */\n\t\t\tthis.$emit('quick-edit-save', { id, patch })\n\t\t},\n\n\t\t/**\n\t\t * Take the server's version of a row after a conflict. Nothing of this\n\t\t * person's edit is written, and the row in the list is refreshed to\n\t\t * what the server holds, so the screen and the record agree.\n\t\t *\n\t\t * @param {object} saved The record as the server holds it.\n\t\t * @return {void}\n\t\t * @spec openspec/changes/working-list-row-actions/specs/index-page/spec.md\n\t\t */\n\t\tonQuickEditKeepTheirs(saved) {\n\t\t\tconst id = saved && saved[this.rowKey]\n\t\t\tif (id !== undefined && id !== null) {\n\t\t\t\tthis.splitRowPatches = { ...this.splitRowPatches, [id]: saved }\n\t\t\t}\n\t\t\tthis.quickEditRow = null\n\t\t\tthis.quickEditServerRow = null\n\t\t},\n\n\t\trowActionsFor(row) {\n\t\t\treturn availableRowActions(this.mergedActions, row, this.rowActionField)\n\t\t},\n\n\t\t/**\n\t\t * Why the server refused an action on a row, when it said. Available on\n\t\t * request rather than rendered, because a menu that lists what you may\n\t\t * not do is a menu that takes longer to read.\n\t\t *\n\t\t * @param {object} row The row.\n\t\t * @param {object} action The declared action.\n\t\t * @return {string} The refusal reason, or ''.\n\t\t * @spec openspec/changes/working-list-row-actions/specs/index-page/spec.md\n\t\t */\n\t\trowActionRefusal(row, action) {\n\t\t\treturn refusalReasonFor(action, row, this.rowActionField)\n\t\t},\n\n\t\t/**\n\t\t * The actions the server allowed on a row that this page does not\n\t\t * declare. Nothing renders them. They are here so a page can see what\n\t\t * it is ignoring without the menu growing a button nobody wrote.\n\t\t *\n\t\t * @param {object} row The row.\n\t\t * @return {string[]} The undeclared action ids.\n\t\t * @spec openspec/changes/working-list-row-actions/specs/index-page/spec.md\n\t\t */\n\t\trowActionsNotDeclared(row) {\n\t\t\treturn undeclaredRowActions(this.mergedActions, row, this.rowActionField)\n\t\t},\n\n\t\tonFolderSelect(folderId) {\n\t\t\tthis.selectedFolderId = folderId\n\t\t\tconst key = (this.folderSidebar && (this.folderSidebar.filterField || this.folderSidebar.field)) || ''\n\t\t\tif (key) {\n\t\t\t\tthis.onFilterEvent({ key, values: (folderId === null || folderId === undefined) ? [] : [folderId] })\n\t\t\t}\n\t\t\tthis.applyScopeSort()\n\t\t\t/**\n\t\t\t * @event folder-change Emitted when a folder is selected in the sidebar.\n\t\t\t * @type {(string|number|null)} The selected folder id (null = All).\n\t\t\t */\n\t\t\tthis.$emit('folder-change', folderId)\n\t\t},\n\n\t\t/**\n\t\t * Put the active scope's `defaultSort` on the list and refetch with it.\n\t\t *\n\t\t * The scope's sort is applied through the list's own `sortKeys`, the\n\t\t * same state a header click writes, rather than through a second sort\n\t\t * path — so the fetch carries it and the header shows it as sorted.\n\t\t * A scope declaring no sort leaves whatever is active alone: that is a\n\t\t * scope with nothing to say about sorting, not a scope asking for the\n\t\t * sort to be cleared.\n\t\t *\n\t\t * @return {void}\n\t\t * @spec openspec/changes/index-columns-per-scope/specs/index-page/spec.md\n\t\t */\n\t\tapplyScopeSort() {\n\t\t\tconst sortKeys = this.activeScopeLayout.sortKeys\n\t\t\tif (!sortKeys.length || !this.isSelfFetchMode || !this.list) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (this.list.sortKeys) {\n\t\t\t\tthis.list.sortKeys.value = sortKeys.map((entry) => ({ ...entry }))\n\t\t\t}\n\t\t\tif (typeof this.list.refresh === 'function') {\n\t\t\t\tthis.list.refresh(1)\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Load the folder list from an OpenRegister register/schema for a\n\t\t * `folderSidebar.source === 'register'` config. Maps each object to\n\t\t * `{ id: <idField|@self.uuid>, name: <nameField|title> }`.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync loadFolderRegister() {\n\t\t\tconst cfg = this.folderSidebar\n\t\t\tif (!cfg || cfg.source !== 'register' || !cfg.register || !cfg.schema) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tconst [{ default: axios }, { generateUrl }] = await Promise.all([\n\t\t\t\t\timport('@nextcloud/axios'),\n\t\t\t\t\timport('@nextcloud/router'),\n\t\t\t\t])\n\t\t\t\tconst url = generateUrl('/apps/openregister/api/objects/{register}/{schema}', { register: cfg.register, schema: cfg.schema })\n\t\t\t\tconst res = await axios.get(url, { params: { _limit: cfg.limit || 200 } })\n\t\t\t\tconst body = (res && res.data && (res.data.results || res.data)) || []\n\t\t\t\t// A response that is not a list is an empty folder list, not a\n\t\t\t\t// crash: the pane then shows only \"All\" rather than logging a\n\t\t\t\t// TypeError that names this component for the server's shape.\n\t\t\t\tconst rows = Array.isArray(body) ? body : []\n\t\t\t\tconst idField = cfg.idField || '@self.uuid'\n\t\t\t\tconst nameField = cfg.nameField || 'title'\n\t\t\t\tconst layouts = {}\n\t\t\t\trows.forEach((row) => {\n\t\t\t\t\tconst id = this.getByPath(row, idField)\n\t\t\t\t\tconst carried = row && row['x-index']\n\t\t\t\t\tif (id !== null && id !== undefined && carried && typeof carried === 'object') {\n\t\t\t\t\t\tlayouts[String(id)] = carried\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t\tthis.folderRowLayouts = layouts\n\t\t\t\tthis.folderRegisterList = rows\n\t\t\t\t\t.map((row) => ({ id: this.getByPath(row, idField), name: this.getByPath(row, nameField) || this.getByPath(row, idField) }))\n\t\t\t\t\t.filter((f) => f.id !== null && f.id !== undefined)\n\t\t\t\t\t.sort((a, b) => String(a.name).localeCompare(String(b.name)))\n\t\t\t} catch (e) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.error('[CnIndexPage] failed to load folder register', e)\n\t\t\t\tthis.folderRegisterList = []\n\t\t\t\tthis.folderRowLayouts = {}\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Whether a given value is currently an active facet filter for a field.\n\t\t *\n\t\t * @param {string} key The schema field key.\n\t\t * @param {string} val The enum value.\n\t\t * @return {boolean} True when the value is in the active filter set.\n\t\t */\n\t\tisFilterActive(key, val) {\n\t\t\tconst current = this.effectiveActiveFilters[key]\n\t\t\treturn Array.isArray(current) && current.includes(val)\n\t\t},\n\n\t\t/**\n\t\t * Toggle one enum value in a field's facet filter, then apply it through\n\t\t * the standard filter path (`onFilterEvent`).\n\t\t *\n\t\t * @param {string} key The schema field key.\n\t\t * @param {string} val The enum value to toggle.\n\t\t * @return {void}\n\t\t */\n\t\ttoggleFilter(key, val) {\n\t\t\tconst current = Array.isArray(this.effectiveActiveFilters[key]) ? this.effectiveActiveFilters[key] : []\n\t\t\tconst values = current.includes(val) ? current.filter((v) => v !== val) : [...current, val]\n\t\t\tthis.onFilterEvent({ key, values })\n\t\t},\n\n\t\t/**\n\t\t * Whether a governed column is currently visible. A null visible-column set\n\t\t * means \"all governed columns visible\" (the initial state).\n\t\t *\n\t\t * @param {string} key The column key.\n\t\t * @return {boolean} True when the column is shown in the table.\n\t\t */\n\t\tisColumnVisible(key) {\n\t\t\tconst visible = this.effectiveVisibleColumns\n\t\t\treturn Array.isArray(visible) ? visible.includes(key) : true\n\t\t},\n\n\t\t/**\n\t\t * Toggle a governed column on/off from the table-header column menu, then\n\t\t * apply it through the same path as the sidebar (`onColumnsEvent`).\n\t\t *\n\t\t * @param {string} key The column key to toggle.\n\t\t * @return {void}\n\t\t */\n\t\ttoggleColumn(key) {\n\t\t\tconst base = Array.isArray(this.effectiveVisibleColumns)\n\t\t\t\t? this.effectiveVisibleColumns\n\t\t\t\t: this.governedColumns.map((c) => c.key)\n\t\t\tconst next = base.includes(key) ? base.filter((k) => k !== key) : [...base, key]\n\t\t\tthis.onColumnsEvent(next)\n\t\t},\n\n\t\t/**\n\t\t * @param {Array} columns Visible-column change from the sidebar.\n\t\t * @return {void}\n\t\t */\n\t\tonColumnsEvent(columns) {\n\t\t\tif (this.isSelfFetchMode && this.list.visibleColumns) {\n\t\t\t\tthis.list.visibleColumns.value = columns\n\t\t\t}\n\t\t\tthis.$emit('columns-change', columns)\n\t\t},\n\n\t\t/** @return {Promise<void>} */\n\t\tasync onRefreshEvent() {\n\t\t\tthis.$emit('refresh')\n\t\t\tif (this.isSelfFetchMode && typeof this.list.refresh === 'function') {\n\t\t\t\tthis.internalRefreshing = true\n\t\t\t\ttry {\n\t\t\t\t\tawait this.list.refresh()\n\t\t\t\t} finally {\n\t\t\t\t\tthis.internalRefreshing = false\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Publish (or clear) the embedded CnIndexSidebar config to\n\t\t * the `cnIndexSidebarConfig` holder so CnAppRoot can mount\n\t\t * it at NcContent level. No-op when no CnAppRoot ancestor\n\t\t * exists — in that case `shouldRenderInlineSidebar` keeps\n\t\t * the inline render alive.\n\t\t */\n\t\tpublishHoistedSidebar() {\n\t\t\tif (!this.cnHostsIndexSidebar || !this.cnIndexSidebarConfig) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (!this.resolvedSidebar.enabled || this.resolvedSidebar.show === false) {\n\t\t\t\tthis.cnIndexSidebarConfig.value = null\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.cnIndexSidebarConfig.value = {\n\t\t\t\t// markRaw: the holder is CnAppRoot `data()`, so it is deeply\n\t\t\t\t// reactive and would proxy the component definition itself.\n\t\t\t\tcomponent: markRaw(CnIndexSidebar),\n\t\t\t\tprops: this.hoistedSidebarProps,\n\t\t\t\tlisteners: {\n\t\t\t\t\t'update:open': (val) => {\n\t\t\t\t\t\tthis.sidebarOpen = val\n\t\t\t\t\t},\n\n\t\t\t\t\tsearch: (event) => this.onSearchEvent(event),\n\t\t\t\t\t'columns-change': (event) => this.onColumnsEvent(event),\n\t\t\t\t\t'filter-change': (event) => this.onFilterEvent(event),\n\t\t\t\t\t'clear-filters': () => this.onClearFilters(),\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Intercepts CnRowActions' bubbled `@action` so `handler: \"none\"`\n\t\t * actions are dropped before re-emit.\n\t\t *\n\t\t * @param {{action: string, row: object}} payload The bubbled action payload.\n\t\t */\n\t\tonRowAction(payload) {\n\t\t\tconst matched = this.mergedActions.find((a) => a.label === payload.action)\n\t\t\tif (matched && matched._dispatchSuppress) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.$emit('action', payload)\n\t\t},\n\n\t\t/**\n\t\t * A card moved on the board.\n\t\t *\n\t\t * The list is refreshed rather than patched in place: the transition\n\t\t * may have changed more than the status (a date armed, an assignee\n\t\t * cleared), and a board that only moved the card would show a row that\n\t\t * disagrees with the table beside it.\n\t\t *\n\t\t * @param {object} move The `{ card, toKey }` the board emitted.\n\t\t * @return {Promise<void>} Nothing.\n\t\t */\n\t\tasync onBoardMoved(move) {\n\t\t\t/**\n\t\t\t * @event board-move A card moved through the host's transition.\n\t\t\t * @type {object}\n\t\t\t */\n\t\t\tthis.$emit('board-move', move)\n\t\t\tawait this.onRefreshEvent()\n\t\t},\n\n\t\t/**\n\t\t * Row/card click: toggles selection when `selectable` (covers the custom\n\t\t * `cardComponent` path), otherwise emits `row-click` for navigation. A\n\t\t * ctrl/cmd/shift click opens a named source's row in a new tab.\n\t\t *\n\t\t * The `@event` block below sits directly against its `$emit`, and must\n\t\t * stay there. vue-docgen binds an event's description to the emit it\n\t\t * IMMEDIATELY precedes.\n\t\t *\n\t\t * @param {object} row The clicked row object\n\t\t * @param {MouseEvent|KeyboardEvent} [event] The originating click event.\n\t\t */\n\t\tonRowClick(row, event) {\n\t\t\tif (!this.routeClickedRow(row, event)) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\t/**\n\t\t\t * @event row-click Emitted on a row/card click for navigation. Fires when `selectable` is false, OR when `rowClickToView` is set and something can open the row (selection then happens via the checkbox). Payload: `(row, event)` — the clicked row and the native click event (absent for a keyboard shortcut or map marker), so a host can open the row in a new tab on a ctrl/cmd/shift click with `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', ...(event ? [row, event] : [row]))\n\t\t},\n\n\t\t/**\n\t\t * Row/card middle click: opens a named source's row in a new tab and\n\t\t * emits `row-aux-click`, not `row-click`, so a host whose `row-click`\n\t\t * listener navigates never moves the current tab away.\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 (!this.routeClickedRow(row, event)) {\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/card middle click, under the same conditions as `row-click`, so a host can open the row in a new tab with `openRowTarget`. Payload: `(row, event)` — the clicked row 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},\n\n\t\t/**\n\t\t * The shared part of a row click and a middle click: on a selectable\n\t\t * page a plain click toggles selection instead, and a named source opens\n\t\t * its own row.\n\t\t *\n\t\t * @param {object} row The clicked row object\n\t\t * @param {MouseEvent|KeyboardEvent} [event] The originating event.\n\t\t * @return {boolean} Whether the click navigates, so the caller emits.\n\t\t */\n\t\trouteClickedRow(row, event) {\n\t\t\tif (this.selectable && !this.rowClickOpens) {\n\t\t\t\tif (!event || event.button === undefined || event.button === 0) {\n\t\t\t\t\tthis.onSelect(this.toggleIdInArray(this.internalSelectedIds, row[this.rowKey]))\n\t\t\t\t}\n\t\t\t\treturn false\n\t\t\t}\n\t\t\t// A named source knows where its rows live. Emitting only would leave\n\t\t\t// the click inert on a manifest page, which has no listener to bind —\n\t\t\t// the very shape that left three apps with a dead `@rowClick`.\n\t\t\t//\n\t\t\t// `rowRoute` wins over everything: an app that declares its own\n\t\t\t// page for these rows means it. The source's `openRow` is the\n\t\t\t// right default precisely because a task's page is usually\n\t\t\t// openregister's, and it is wrong for an app that ships one of\n\t\t\t// its own. This cannot be done by listening to `row-click`,\n\t\t\t// because `openRow` calls `window.location.assign()` and the\n\t\t\t// host's push never lands.\n\t\t\t//\n\t\t\t// A ctrl/cmd/shift or middle click opens a new tab instead, from\n\t\t\t// `rowRoute`, the source's `rowTarget(row)` or `detailRoute`. A source\n\t\t\t// with only `openRow` opens the row in place on a ctrl/cmd/shift\n\t\t\t// click and ignores a middle click. The event is marked so the host\n\t\t\t// hearing `row-click` does not open a second tab.\n\t\t\tif (this.isNamedSource) {\n\t\t\t\tconst id = row?.id || row?.uuid\n\t\t\t\tconst source = this.namedSource\n\t\t\t\tconst newTab = isNewTabClick(event)\n\t\t\t\tconst openTarget = (target) => markNewTabHandled(event, openRowTarget(event, target, this.$router))\n\t\t\t\tif (this.rowRoute) {\n\t\t\t\t\tif (id && newTab) {\n\t\t\t\t\t\topenTarget({ name: this.rowRoute, params: { id: String(id) } })\n\t\t\t\t\t} else if (id) {\n\t\t\t\t\t\tthis.$router.push({ name: this.rowRoute, params: { id: String(id) } })\n\t\t\t\t\t}\n\t\t\t\t} else if (source && typeof source.openRow === 'function' && !newTab) {\n\t\t\t\t\tsource.openRow(row)\n\t\t\t\t} else if (source && typeof source.rowTarget === 'function') {\n\t\t\t\t\topenTarget(source.rowTarget(row))\n\t\t\t\t} else if (source && typeof source.openRow === 'function') {\n\t\t\t\t\t// No new-tab target: a modified click still opens the row in place.\n\t\t\t\t\tif (event?.button !== 1) {\n\t\t\t\t\t\tsource.openRow(row)\n\t\t\t\t\t}\n\t\t\t\t} else if (source && source.detailRoute && id) {\n\t\t\t\t\tif (newTab) {\n\t\t\t\t\t\topenTarget({ path: `${source.detailRoute}/${id}` })\n\t\t\t\t\t} else {\n\t\t\t\t\t\tthis.$router.push(`${source.detailRoute}/${id}`)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn true\n\t\t},\n\n\t\t/**\n\t\t * Whether a value is a DOM event. A custom list/card component's\n\t\t * `click` may carry the item, the native event, or both.\n\t\t *\n\t\t * @param {unknown} value A `click` listener argument.\n\t\t * @return {boolean}\n\t\t */\n\t\tisDomEvent(value) {\n\t\t\treturn typeof Event !== 'undefined' && value instanceof Event\n\t\t},\n\n\t\t/**\n\t\t * Mousedown on a custom list/card component's root: when a middle click\n\t\t * opens the row, keep the browser from starting autoscroll.\n\t\t *\n\t\t * @param {MouseEvent} event The mousedown event.\n\t\t * @return {void}\n\t\t */\n\t\tonCustomItemMouseDown(event) {\n\t\t\tif (!this.selectable || this.rowClickOpens) {\n\t\t\t\tpreventMiddleClickAutoscroll(event)\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Middle click on a custom list/card component's root: open the row\n\t\t * like a middle click on a built-in card would. Skipped when something\n\t\t * inside the component already opened it in a new tab.\n\t\t *\n\t\t * @param {object} row The component's row.\n\t\t * @param {MouseEvent} event The auxclick event.\n\t\t * @return {void}\n\t\t */\n\t\tonCustomItemAuxClick(row, event) {\n\t\t\tif (isRowMiddleClick(event) && !isNewTabHandled(event)) {\n\t\t\t\tthis.onRowAuxClick(row, event)\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Resolve a marker click on the map back to its source row and route it\n\t\t * through `onRowClick`, so `@row-click` fires with the identical payload a\n\t\t * table/card click would emit — giving uniform detail-page navigation\n\t\t * across all three view modes.\n\t\t *\n\t\t * @param {{ feature: object }} payload CnMapWidget marker-click payload.\n\t\t */\n\t\tonMarkerClick(payload) {\n\t\t\tconst feature = payload && payload.feature\n\t\t\tconst key = feature && feature.properties ? feature.properties[this.rowKey] : undefined\n\t\t\tif (key === undefined || key === null) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst row = this.displayObjects.find((o) => o[this.rowKey] === key)\n\t\t\tif (row) {\n\t\t\t\tthis.onRowClick(row)\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Extract a `{ lat, lng }` pair from a row using `mapConfig`. Prefers a\n\t\t * GeoJSON Point on `geoField` (`coordinates: [lng, lat]`), else reads the\n\t\t * dotted `latField` / `lngField` paths. Returns null when the geometry is\n\t\t * missing or non-finite, so callers can skip the row.\n\t\t *\n\t\t * @param {object} row The source object.\n\t\t * @return {{ lat: number, lng: number } | null}\n\t\t */\n\t\tresolveRowLatLng(row) {\n\t\t\tif (!row) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst cfg = this.mapConfig || {}\n\t\t\tif (cfg.geoField) {\n\t\t\t\tlet geo = this.getByPath(row, cfg.geoField)\n\t\t\t\t// OpenRegister often stores geometry as a JSON-encoded string on\n\t\t\t\t// object metadata; parse it before reading `coordinates`.\n\t\t\t\tif (typeof geo === 'string') {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tgeo = JSON.parse(geo)\n\t\t\t\t\t} catch {\n\t\t\t\t\t\tgeo = null\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tconst coords = geo && Array.isArray(geo.coordinates) ? geo.coordinates : null\n\t\t\t\tif (coords && Number.isFinite(coords[0]) && Number.isFinite(coords[1])) {\n\t\t\t\t\treturn { lat: Number(coords[1]), lng: Number(coords[0]) }\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst lat = Number(this.getByPath(row, cfg.latField))\n\t\t\tconst lng = Number(this.getByPath(row, cfg.lngField))\n\t\t\tif (Number.isFinite(lat) && Number.isFinite(lng)) {\n\t\t\t\treturn { lat, lng }\n\t\t\t}\n\t\t\treturn null\n\t\t},\n\n\t\t/**\n\t\t * Resolve a row's GeoJSON geometry for the map view. Prefers a full\n\t\t * geometry on `geoField`: a Point renders as a marker, a Polygon /\n\t\t * MultiPolygon / LineString renders as an area/line — so location cases\n\t\t * plot as both points AND areas. `geoField` is parsed from a JSON string\n\t\t * when OpenRegister stores it encoded. Falls back to a Point built from\n\t\t * `latField` / `lngField`. Returns null when nothing resolvable is present.\n\t\t *\n\t\t * @param {object} row The source object.\n\t\t * @return {object|null} A GeoJSON geometry object, or null.\n\t\t */\n\t\tresolveRowGeometry(row) {\n\t\t\tif (!row) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst cfg = this.mapConfig || {}\n\t\t\tconst GEO_TYPES = ['Point', 'MultiPoint', 'LineString', 'MultiLineString', 'Polygon', 'MultiPolygon', 'GeometryCollection']\n\t\t\tif (cfg.geoField) {\n\t\t\t\tlet geo = this.getByPath(row, cfg.geoField)\n\t\t\t\tif (typeof geo === 'string') {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tgeo = JSON.parse(geo)\n\t\t\t\t\t} catch {\n\t\t\t\t\t\tgeo = null\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (geo && GEO_TYPES.includes(geo.type)) {\n\t\t\t\t\tconst hasShape = geo.type === 'GeometryCollection'\n\t\t\t\t\t\t? Array.isArray(geo.geometries)\n\t\t\t\t\t\t: Array.isArray(geo.coordinates)\n\t\t\t\t\tif (hasShape) {\n\t\t\t\t\t\treturn geo\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst lat = Number(this.getByPath(row, cfg.latField))\n\t\t\tconst lng = Number(this.getByPath(row, cfg.lngField))\n\t\t\tif (Number.isFinite(lat) && Number.isFinite(lng)) {\n\t\t\t\treturn { type: 'Point', coordinates: [lng, lat] }\n\t\t\t}\n\t\t\treturn null\n\t\t},\n\n\t\t/**\n\t\t * Dig out the first `[lng, lat]` coordinate pair from any GeoJSON geometry\n\t\t * (Point, Polygon ring, GeometryCollection, …) for a rough centroid.\n\t\t *\n\t\t * @param {object} geometry A GeoJSON geometry.\n\t\t * @return {{lat: number, lng: number}|null}\n\t\t */\n\t\tfirstLatLng(geometry) {\n\t\t\tif (!geometry) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tif (geometry.type === 'GeometryCollection') {\n\t\t\t\tfor (const g of (geometry.geometries || [])) {\n\t\t\t\t\tconst p = this.firstLatLng(g)\n\t\t\t\t\tif (p) {\n\t\t\t\t\t\treturn p\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tlet c = geometry.coordinates\n\t\t\twhile (Array.isArray(c) && Array.isArray(c[0])) {\n\t\t\t\tc = c[0]\n\t\t\t}\n\t\t\tif (Array.isArray(c) && Number.isFinite(c[0]) && Number.isFinite(c[1])) {\n\t\t\t\treturn { lat: Number(c[1]), lng: Number(c[0]) }\n\t\t\t}\n\t\t\treturn null\n\t\t},\n\n\t\t/**\n\t\t * Read a possibly-dotted property path off an object (e.g. `@self.geo.lat`).\n\t\t * Returns undefined on any missing segment. No path = undefined.\n\t\t *\n\t\t * @param {object} obj The object to read from.\n\t\t * @param {string} path Dot-separated property path.\n\t\t * @return {unknown} The resolved value or undefined.\n\t\t */\n\t\tgetByPath(obj, path) {\n\t\t\tif (!obj || !path) {\n\t\t\t\treturn undefined\n\t\t\t}\n\t\t\tif (Object.hasOwn(obj, path)) {\n\t\t\t\treturn obj[path]\n\t\t\t}\n\t\t\treturn path.split('.').reduce((acc, seg) => (acc === null || acc === undefined ? undefined : acc[seg]), obj)\n\t\t},\n\n\t\t/**\n\t\t * Whether the consumer explicitly passed a prop, in either spelling.\n\t\t *\n\t\t * Needed where a named source changes a prop's EFFECTIVE default: the\n\t\t * declared default is unchanged (the prop interface must not break),\n\t\t * so the value alone cannot distinguish \"left to default\" from\n\t\t * \"explicitly asked for the default\".\n\t\t *\n\t\t * @param {string} name The camelCase prop name.\n\t\t * @return {boolean} True when the prop was passed explicitly.\n\t\t */\n\t\thasExplicitProp(name) {\n\t\t\tconst props = this.$.vnode.props || {}\n\t\t\tconst kebab = name.replace(/[A-Z]/g, (letter) => '-' + letter.toLowerCase())\n\t\t\treturn (name in props) || (kebab in props)\n\t\t},\n\n\t\t/**\n\t\t * Open a named-source row: the source's own navigation, then\n\t\t * `edit-open` for the host.\n\t\t *\n\t\t * This is what the source-action grammar `action: 'open'` resolves to\n\t\t * (see `mergedActions`). Navigation order is deliberate: the source's\n\t\t * `openRow`/`detailRoute` push happens FIRST, so when the host also\n\t\t * navigates (CnPageRenderer binds `@edit-open` → `onRowOpen`, which\n\t\t * honours `config.rowRoute`), the host's push lands last and wins —\n\t\t * the configured target beats the source's default, and a page with\n\t\t * no host listener still navigates.\n\t\t *\n\t\t * @param {object} row The row to open.\n\t\t * @return {void}\n\t\t */\n\t\topenSourceRow(row) {\n\t\t\tif (this.isNamedSource && this.namedSource && typeof this.namedSource.openRow === 'function') {\n\t\t\t\tthis.namedSource.openRow(row)\n\t\t\t} else if (this.isNamedSource && this.namedSource && this.namedSource.detailRoute) {\n\t\t\t\tconst id = row?.id || row?.uuid\n\t\t\t\tif (id && this.$router) {\n\t\t\t\t\tthis.$router.push(`${this.namedSource.detailRoute}/${id}`)\n\t\t\t\t}\n\t\t\t}\n\t\t\tthis.$emit('edit-open', row)\n\t\t},\n\n\t\t/**\n\t\t * Handle the built-in View action — emits a dedicated `view` event.\n\t\t * Kept distinct from `row-click` because the two are conceptually\n\t\t * different: a row click might mean select/expand/drilldown, while\n\t\t * View always means \"open the detail view of this row\".\n\t\t *\n\t\t * @param {object} row The row whose View action was triggered\n\t\t */\n\t\tonView(row) {\n\t\t\tthis.$emit('view', row)\n\t\t},\n\n\t\t/**\n\t\t * Handle the Add button click. If the consumer listens to `@add`,\n\t\t * emit the event (backward compatible). Otherwise open the form dialog.\n\t\t */\n\t\tonAddClick() {\n\t\t\t// A NAMED SOURCE CREATES BY NAVIGATING, not by the form dialog. That\n\t\t\t// dialog builds an OpenRegister object from a schema; a flow is not\n\t\t\t// one, which is why the page it replaces set `show-add=\"false\"` and\n\t\t\t// rendered its own button. An explicit @add listener still wins.\n\t\t\tif (\n\t\t\t\t!this.$.vnode.props?.onAdd\n\t\t\t\t&& this.isNamedSource\n\t\t\t\t&& this.namedSource\n\t\t\t\t&& this.namedSource.addRoute\n\t\t\t) {\n\t\t\t\t// With `addLinkTo` set, Add is a link that already navigated.\n\t\t\t\tif (!this.addLinkTo) {\n\t\t\t\t\tthis.$router.push(this.namedSource.addRoute)\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\t\t\t// `$.vnode.props`, not `$attrs`: `add` is a declared emit, and Vue\n\t\t\t// keeps declared emits out of `$attrs`.\n\t\t\tif (this.$.vnode.props?.onAdd) {\n\t\t\t\tthis.$emit('add')\n\t\t\t} else if (!this.openCreateModal() && this.showFormDialog) {\n\t\t\t\tthis.editItem = null\n\t\t\t\tthis.showFormDialogVisible = true\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Open the built-in create dialog when the route carries `?action=create`.\n\t\t * Called on `mounted()` and whenever `$route.query.action` changes, so it\n\t\t * works both on initial navigation and on same-page deep-links (e.g. a\n\t\t * \"New Case\" button on a sibling page that routes here with the query).\n\t\t *\n\t\t * After opening the dialog the query param is cleared via\n\t\t * `$router.replace` so a page refresh does not re-open it and browser\n\t\t * history stays clean. No-op when there is no router, when `showFormDialog`\n\t\t * is false (consumer manages its own dialog), or when the query param is\n\t\t * absent / not `'create'`.\n\t\t */\n\t\tmaybeOpenCreateFromQuery() {\n\t\t\tif (!this.$route || !this.$route.query || this.$route.query.action !== 'create') {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (!this.openCreateModal()) {\n\t\t\t\tif (!this.showFormDialog) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tthis.openFormDialog(null)\n\t\t\t}\n\t\t\t// Clear the query param; guard against redundant navigation errors.\n\t\t\tif (this.$router) {\n\t\t\t\tconst query = { ...this.$route.query }\n\t\t\t\tdelete query.action\n\t\t\t\tconst nav = this.$router.replace({ query })\n\t\t\t\t// $router.replace returns a Promise in Vue Router 3 but may\n\t\t\t\t// return undefined in mocked / legacy environments — guard.\n\t\t\t\tif (nav && typeof nav.catch === 'function') {\n\t\t\t\t\tnav.catch(() => {})\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Handle view mode toggle.\n\t\t *\n\t\t * @param {string} mode 'table' or 'cards'\n\t\t */\n\t\tonViewModeChange(mode) {\n\t\t\tthis.currentViewMode = mode\n\t\t\tthis.$emit('view-mode-change', mode)\n\t\t},\n\n\t\t/**\n\t\t * Handle selection changes from CnDataTable/CnCardGrid.\n\t\t * Updates internal state and re-emits for parent.\n\t\t *\n\t\t * @param {Array} ids Array of selected row IDs\n\t\t */\n\t\tonSelect(ids) {\n\t\t\tthis.internalSelectedIds = ids\n\t\t\tthis.$emit('select', ids)\n\t\t},\n\n\t\t// --- Mass action handlers ---\n\n\t\tasync onMassDeleteConfirm(ids) {\n\t\t\tif (await this.selfActions.handleMassDelete(ids)) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.$emit('mass-delete', ids)\n\t\t},\n\n\t\tasync onMassCopyConfirm(payload) {\n\t\t\tif (await this.selfActions.handleMassCopy(payload)) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.$emit('mass-copy', payload)\n\t\t},\n\n\t\tasync onMassExportConfirm(payload) {\n\t\t\tif (await this.selfActions.handleMassExport(payload)) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.$emit('mass-export', payload)\n\t\t},\n\n\t\t/**\n\t\t * Native Export menu entry click (`allowExport` + `schema.exportable`).\n\t\t * Navigates the browser to OpenRegister's export leaf, passing the\n\t\t * current route's query params through so a filtered index exports\n\t\t * only the visible rows.\n\t\t *\n\t\t * @param {'csv'|'excel'} format The requested export format.\n\t\t */\n\t\tonExportClick(format) {\n\t\t\tconst routeQuery = (this.$route && this.$route.query) || {}\n\t\t\tconst url = buildExportUrl(this.register, this.exportSchemaSlug, routeQuery, format)\n\t\t\twindow.location.assign(url)\n\t\t},\n\n\t\t// ── Saved views (saved-views-ui) ─────────────────────────────────────\n\n\t\t/**\n\t\t * Fetch the current user's saved views (own + public) from\n\t\t * OpenRegister's views API. Called from created() when\n\t\t * `allowSavedViews` is enabled.\n\t\t */\n\t\tasync fetchSavedViews() {\n\t\t\tthis.savedViewsLoading = true\n\t\t\ttry {\n\t\t\t\tthis.savedViews = await useSavedViewsApi().fetchViews()\n\t\t\t} catch (error) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.error('CnIndexPage: failed to fetch saved views', error)\n\t\t\t} finally {\n\t\t\t\tthis.savedViewsLoading = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Apply a saved view. Self-fetch mode's facet/folder filters and\n\t\t * search/sort live in useListView's own reactive state, not the route\n\t\t * query, so apply them there directly. Otherwise (consumer-managed\n\t\t * store mode) fall back to `$router.replace` — same precedent as the\n\t\t * `?action=create` query cleanup, and dropping the whole previous\n\t\t * query implicitly resets `_page` to 1.\n\t\t *\n\t\t * @param {object} view The View API object to apply.\n\t\t */\n\t\tonApplySavedView(view) {\n\t\t\tconst state = extractViewState(view)\n\t\t\tif (this.isSelfFetchMode) {\n\t\t\t\t// Set state directly (not via onFilterEvent/onSearchEvent per key)\n\t\t\t\t// so applying a view is one fetch + one route replace, not one\n\t\t\t\t// of each per changed field — and a filter the view doesn't\n\t\t\t\t// carry is actually cleared, not left over from before.\n\t\t\t\tthis.list.activeFilters.value = { ...state.filters }\n\t\t\t\tthis.list.searchTerm.value = state.search || ''\n\t\t\t\tconst keys = state.sortKeys\n\t\t\t\tthis.list.sortKeys.value = keys\n\t\t\t\tthis.list.sortKey.value = keys[0]?.key ?? null\n\t\t\t\tthis.list.sortOrder.value = keys[0]?.order ?? 'asc'\n\t\t\t\tthis.list.refresh(1)\n\t\t\t\t// Through `persistViewStateToRoute` rather than a raw replace,\n\t\t\t\t// because it is what records the keys this view wrote — without\n\t\t\t\t// that, a later \"clear all\" has nothing to delete them by.\n\t\t\t\tthis.persistViewStateToRoute(state)\n\t\t\t\tthis.$emit('apply-view', view)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (!this.$router) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst nav = this.$router.replace({ query: buildRouteQueryFromViewState(state) })\n\t\t\t// Swallow the duplicate-navigation rejection (Vue Router 3)\n\t\t\t// when the applied view matches the current query.\n\t\t\tif (nav && typeof nav.catch === 'function') {\n\t\t\t\tnav.catch(() => {})\n\t\t\t}\n\t\t\tthis.$emit('apply-view', view)\n\t\t},\n\n\t\t/**\n\t\t * Persist the current state as a named view via OpenRegister's views\n\t\t * API (CnSaveViewDialog `@confirm`). Self-fetch mode reads its active\n\t\t * filters/search/sort straight from useListView's own state (they\n\t\t * never reach the route query); otherwise falls back to the route\n\t\t * query. On success the new view joins the list and the dialog\n\t\t * closes; on failure the dialog stays open with the error surfaced.\n\t\t *\n\t\t * @param {{ name: string, isPublic: boolean }} payload Dialog payload.\n\t\t */\n\t\t/**\n\t\t * Toast, without letting the toast fail the thing it reports on.\n\t\t *\n\t\t * The import is dynamic so a page that never toasts does not carry the\n\t\t * chunk, and everything is swallowed: a chunk that will not load must\n\t\t * not turn a save that worked into an error, nor add an unhandled\n\t\t * rejection on top of one that already failed.\n\t\t *\n\t\t * @param {'success'|'error'} kind Which toast to show.\n\t\t * @param {string} message The message, already translated.\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync toastSavedView(kind, message) {\n\t\t\ttry {\n\t\t\t\tconst dialogs = await import('@nextcloud/dialogs')\n\t\t\t\tconst show = kind === 'error' ? dialogs.showError : dialogs.showSuccess\n\t\t\t\tif (typeof show === 'function') {\n\t\t\t\t\tshow(message)\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// No toast. What it was reporting on happened either way.\n\t\t\t}\n\t\t},\n\n\t\tasync onSaveViewConfirm({ name, isPublic }) {\n\t\t\tconst state = this.isSelfFetchMode\n\t\t\t\t? this.currentViewState()\n\t\t\t\t: extractViewStateFromRouteQuery((this.$route && this.$route.query) || {})\n\t\t\tconst payload = buildViewCreatePayload({\n\t\t\t\tname,\n\t\t\t\tdescription: '',\n\t\t\t\tisPublic,\n\t\t\t\tisDefault: false,\n\t\t\t\tstate,\n\t\t\t\tscope: savedViewScope(this.savedViewsPage),\n\t\t\t\tregister: this.register,\n\t\t\t\tschema: this.schema,\n\t\t\t})\n\t\t\ttry {\n\t\t\t\tconst view = await useSavedViewsApi().createView(payload)\n\t\t\t\tif (!view) {\n\t\t\t\t\t// A 2xx carrying no view. Axios has thrown on every real error\n\t\t\t\t\t// by now, so nothing else marks this one, and the list below is\n\t\t\t\t\t// not appended to: claiming success sends the person looking\n\t\t\t\t\t// for a view that is not in it.\n\t\t\t\t\tthrow new Error(t('nextcloud-vue', 'The server did not return the saved view'))\n\t\t\t\t}\n\t\t\t\tthis.savedViews = [...this.savedViews, view]\n\t\t\t\tthis.showSaveViewDialog = false\n\t\t\t\tthis.toastSavedView('success', t('nextcloud-vue', 'View \"{name}\" saved', { name }))\n\t\t\t} catch (error) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.error('CnIndexPage: failed to save view', error)\n\t\t\t\t// Both, and they say different things: the dialog stays open\n\t\t\t\t// carrying the reason, because that is where the name and the\n\t\t\t\t// toggle still are to correct and retry; the toast says the save\n\t\t\t\t// did not happen, because a dialog that simply stayed open reads\n\t\t\t\t// as one that has not been submitted yet.\n\t\t\t\tthis.$refs.saveViewDialog?.setError(error?.response?.data?.error || error?.message)\n\t\t\t\tthis.toastSavedView('error', t('nextcloud-vue', 'Could not save the view \"{name}\"', { name }))\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Open the delete-confirmation dialog for a saved view\n\t\t * (CnSavedViewsControl `@delete-request`).\n\t\t *\n\t\t * @param {object} view The View API object to delete.\n\t\t */\n\t\tonDeleteViewRequest(view) {\n\t\t\tthis.viewPendingDelete = view\n\t\t},\n\n\t\t/**\n\t\t * Delete the pending view after confirmation (CnConfirmDialog\n\t\t * `@confirm`), then report back via the dialog's setResult()\n\t\t * contract and remove it from the local list.\n\t\t */\n\t\tasync onDeleteViewConfirm() {\n\t\t\tconst view = this.viewPendingDelete\n\t\t\tif (!view) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tawait useSavedViewsApi().deleteView(view.id)\n\t\t\t\tthis.savedViews = this.savedViews.filter((v) => v.id !== view.id)\n\t\t\t\tthis.$refs.deleteViewConfirmDialog?.setResult({ success: true })\n\t\t\t} catch (error) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.error('CnIndexPage: failed to delete view', error)\n\t\t\t\tthis.$refs.deleteViewConfirmDialog?.setResult({ error: error?.response?.data?.error || error?.message || 'Failed to delete view' })\n\t\t\t}\n\t\t},\n\n\t\tasync onMassImportConfirm(payload) {\n\t\t\tif (await this.selfActions.handleMassImport(payload)) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.$emit('mass-import', payload)\n\t\t},\n\n\t\t_setResult(refName, resultData) {\n\t\t\tthis.$refs[refName]?.setResult(resultData)\n\t\t},\n\n\t\t/**\n\t\t * @param {{ success?: boolean, error?: string }} resultData Result data to pass to the dialog\n\t\t * @public\n\t\t */\n\t\tsetMassDeleteResult(resultData) {\n\t\t\tthis._setResult('massDeleteDialog', resultData)\n\t\t},\n\n\t\t/**\n\t\t * @param {{ success?: boolean, error?: string }} resultData Result data to pass to the dialog\n\t\t * @public\n\t\t */\n\t\tsetMassCopyResult(resultData) {\n\t\t\tthis._setResult('massCopyDialog', resultData)\n\t\t},\n\n\t\t/**\n\t\t * @param {{ success?: boolean, error?: string }} resultData Result data to pass to the dialog\n\t\t * @public\n\t\t */\n\t\tsetExportResult(resultData) {\n\t\t\tthis._setResult('exportDialog', resultData)\n\t\t},\n\n\t\t/**\n\t\t * @param {{ success?: boolean, error?: string }} resultData Result data to pass to the dialog\n\t\t * @public\n\t\t */\n\t\tsetImportResult(resultData) {\n\t\t\tthis._setResult('importDialog', resultData)\n\t\t},\n\n\t\t/**\n\t\t * @param {{ success?: boolean, error?: string }} resultData Result data to pass to the dialog\n\t\t * @public\n\t\t */\n\t\tsetDeleteResult(resultData) {\n\t\t\tthis._setResult('massDeleteDialog', resultData)\n\t\t},\n\n\t\t/**\n\t\t * @param {{ success?: boolean, error?: string }} resultData Result data to pass to the dialog\n\t\t * @public\n\t\t */\n\t\tsetCopyResult(resultData) {\n\t\t\tthis._setResult('massCopyDialog', resultData)\n\t\t},\n\n\t\t// --- Single-object dialog handlers ---\n\n\t\tasync onSingleDeleteConfirm(id) {\n\t\t\t// A named source deletes through its own hook. Emit-only would\n\t\t\t// leave the confirm inert on a manifest page (nobody listens),\n\t\t\t// which is why the Delete action only renders when the hook\n\t\t\t// exists — see `defaultActions`.\n\t\t\tif (this.isNamedSource && this.namedSource && typeof this.namedSource.deleteRow === 'function') {\n\t\t\t\ttry {\n\t\t\t\t\tawait this.namedSource.deleteRow(this.actionTargetItem || { id }, this.sourceConfig || {})\n\t\t\t\t\tthis.setSingleDeleteResult({ success: true })\n\t\t\t\t\tthis.$emit('delete', id)\n\t\t\t\t} catch (err) {\n\t\t\t\t\tthis.setSingleDeleteResult({ error: (err && err.message) || 'Delete failed' })\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (await this.selfActions.handleSingleDelete(id)) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.$emit('delete', id)\n\t\t},\n\n\t\tasync onSingleCopyConfirm(payload) {\n\t\t\t// Same contract as delete: the source owns the copy, or there is\n\t\t\t// no Copy action to confirm.\n\t\t\tif (this.isNamedSource && this.namedSource && typeof this.namedSource.copyRow === 'function') {\n\t\t\t\ttry {\n\t\t\t\t\tawait this.namedSource.copyRow(\n\t\t\t\t\t\tthis.actionTargetItem || { id: payload && payload.id },\n\t\t\t\t\t\tpayload && payload.newName,\n\t\t\t\t\t\tthis.sourceConfig || {},\n\t\t\t\t\t)\n\t\t\t\t\tthis.setSingleCopyResult({ success: true })\n\t\t\t\t\tthis.$emit('copy', payload)\n\t\t\t\t} catch (err) {\n\t\t\t\t\tthis.setSingleCopyResult({ error: (err && err.message) || 'Copy failed' })\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (await this.selfActions.handleSingleCopy(payload)) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.$emit('copy', payload)\n\t\t},\n\n\t\t// Lets an `advanceOn: \"object-created\"` walkthrough step auto-advance.\n\t\t// No dispatch site existed anywhere before this, so that advanceOn\n\t\t// type could never fire. `@self.register`/`@self.schema` are numeric\n\t\t// DB ids, not the slugs a manifest's advanceOn declares, so override\n\t\t// them with this page's own slug props before dispatching.\n\t\tnotifyWalkthroughObjectCreated(created) {\n\t\t\tif (typeof window === 'undefined' || !created) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst detail = { ...created, register: this.register, schema: this.exportSchemaSlug }\n\t\t\twindow.dispatchEvent(new CustomEvent('cn-walkthrough:object-created', { detail }))\n\t\t},\n\n\t\t/**\n\t\t * Toast and navigate after a create the built-in dialog confirmed, per\n\t\t * `createSuccessMessage` / `createSuccessRoute`. Both default off, so a\n\t\t * page that declares neither behaves exactly as before.\n\t\t *\n\t\t * Runs on every create path (store, self-store, `createOverride`), which\n\t\t * is what makes the built-in Add button a peer of a manifest\n\t\t * `open-form` header action: that action has always toasted and\n\t\t * navigated, so the same create reached two different endings depending\n\t\t * on which button opened it.\n\t\t *\n\t\t * @param {object} saved The created object, as the save path returned it.\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync afterCreateSuccess(saved) {\n\t\t\t// Callers do not await this — a toast and a navigation are not the\n\t\t\t// save — so nothing here may reject: an unhandled rejection from a\n\t\t\t// failed chunk load would surface as an error on a create that\n\t\t\t// actually succeeded.\n\t\t\tif (this.createSuccessMessage) {\n\t\t\t\ttry {\n\t\t\t\t\tconst { showSuccess } = await import('@nextcloud/dialogs')\n\t\t\t\t\tif (typeof showSuccess === 'function') {\n\t\t\t\t\t\tshowSuccess(this.cnTranslate(this.createSuccessMessage))\n\t\t\t\t\t}\n\t\t\t\t} catch {\n\t\t\t\t\t// No toast; the record is saved either way.\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (!this.createSuccessRoute) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\ttry {\n\t\t\t\t// `buildOnSuccessRoute` reads the id through `savedObjectId`, so a\n\t\t\t\t// response that carries it as `uuid` or `@self.id` still deep-links.\n\t\t\t\tconst location = buildOnSuccessRoute(this.createSuccessRoute, saved)\n\t\t\t\tif (location && this.$router) {\n\t\t\t\t\tthis.$router.push(location).catch(() => {})\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// The record is saved; staying on the list beats an error toast.\n\t\t\t}\n\t\t},\n\n\t\tasync onFormConfirm(formData) {\n\t\t\t// Opt-in create-override hook: an app supplies a custom async create\n\t\t\t// handler (e.g. a contact-aware endpoint that fills a required FK)\n\t\t\t// that owns persistence instead of saveObject. Create-only; edits\n\t\t\t// always fall through to the normal store / self-store path.\n\t\t\tif (!this.editItem && typeof this.createOverride === 'function') {\n\t\t\t\ttry {\n\t\t\t\t\tconst created = await this.createOverride(formData, {\n\t\t\t\t\t\tregister: this.register,\n\t\t\t\t\t\tschema: this.schema,\n\t\t\t\t\t\tobjectType: this.objectType || this.selfObjectType,\n\t\t\t\t\t\teffectiveSchema: this.effectiveSchema,\n\t\t\t\t\t})\n\t\t\t\t\tif (created) {\n\t\t\t\t\t\tthis.setFormResult({ success: true })\n\t\t\t\t\t\t/**\n\t\t\t\t\t\t * @event create Emitted after a create is confirmed (store, self-store, or createOverride).\n\t\t\t\t\t\t * @type {object} The created object.\n\t\t\t\t\t\t */\n\t\t\t\t\t\tthis.$emit('create', created)\n\t\t\t\t\t\tthis.notifyWalkthroughObjectCreated(created)\n\t\t\t\t\t\tif (this.list && typeof this.list.refresh === 'function') {\n\t\t\t\t\t\t\tthis.list.refresh()\n\t\t\t\t\t\t}\n\t\t\t\t\t\tthis.afterCreateSuccess(created)\n\t\t\t\t\t} else {\n\t\t\t\t\t\tthis.setFormResult({ error: 'Save failed' })\n\t\t\t\t\t}\n\t\t\t\t} catch (err) {\n\t\t\t\t\tthis.setFormResult({ error: (err && err.message) || 'Save failed' })\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (this.store) {\n\t\t\t\tif (!this.objectType) {\n\t\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\t\tconsole.warn('[CnIndexPage] store prop is set but objectType is missing. Cannot save to store.')\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tconst saved = await this.store.saveObject(this.objectType, formData)\n\t\t\t\tif (saved) {\n\t\t\t\t\tconst wasCreate = !this.editItem\n\t\t\t\t\tthis.setFormResult({ success: true })\n\t\t\t\t\tthis.$emit(wasCreate ? 'create' : 'edit', saved)\n\t\t\t\t\tif (wasCreate) {\n\t\t\t\t\t\tthis.notifyWalkthroughObjectCreated(saved)\n\t\t\t\t\t\tthis.afterCreateSuccess(saved)\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tconst err = this.store.getError?.(this.objectType)\n\t\t\t\t\tif (err && err.isValidation) {\n\t\t\t\t\t\t// Keep the form visible so the user can fix the invalid data.\n\t\t\t\t\t\tthis.setFormValidationErrors(err.fields, err.message || 'Validation failed')\n\t\t\t\t\t} else {\n\t\t\t\t\t\tthis.setFormResult({ error: (err && err.message) || 'Save failed' })\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (await this.selfActions.handleFormSave(formData)) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.$emit(this.editItem ? 'edit' : 'create', formData)\n\t\t},\n\n\t\tcloseSingleDelete() {\n\t\t\tthis.showSingleDeleteDialog = false\n\t\t\tthis.actionTargetItem = null\n\t\t},\n\n\t\tcloseSingleCopy() {\n\t\t\tthis.showSingleCopyDialog = false\n\t\t\tthis.actionTargetItem = null\n\t\t},\n\n\t\tcloseFormDialog() {\n\t\t\tthis.showFormDialogVisible = false\n\t\t\tthis.editItem = null\n\t\t},\n\n\t\t/**\n\t\t * @param {{ success?: boolean, error?: string }} resultData Result data to pass to the dialog\n\t\t * @public\n\t\t */\n\t\tsetSingleDeleteResult(resultData) {\n\t\t\tthis._setResult('singleDeleteDialog', resultData)\n\t\t},\n\n\t\t/**\n\t\t * @param {{ success?: boolean, error?: string }} resultData Result data to pass to the dialog\n\t\t * @public\n\t\t */\n\t\tsetSingleCopyResult(resultData) {\n\t\t\tthis._setResult('singleCopyDialog', resultData)\n\t\t},\n\n\t\t/**\n\t\t * @param {{ success?: boolean, error?: string }} resultData Result data to pass to the dialog\n\t\t * @public\n\t\t */\n\t\tsetFormResult(resultData) {\n\t\t\tthis._setResult('formDialog', resultData)\n\t\t},\n\n\t\t/**\n\t\t * Show a validation error in the form dialog while keeping the form\n\t\t * visible (so the user can fix the data), instead of replacing it with\n\t\t * a result note. Use this for 400/422 responses; use setFormResult for\n\t\t * terminal success/failure.\n\t\t *\n\t\t * @param {object} [fieldErrors] Per-field error messages keyed by field key\n\t\t * @param {string} [message] Form-level message shown above the fields\n\t\t * @public\n\t\t */\n\t\tsetFormValidationErrors(fieldErrors, message) {\n\t\t\tthis.$refs.formDialog?.setValidationErrors(fieldErrors || {}, message)\n\t\t},\n\n\t\t// --- Context menu handlers ---\n\n\t\tonRowContextMenu({ row, event }) {\n\t\t\tthis.openContextMenu({ item: row, event })\n\t\t},\n\n\t\t/**\n\t\t * Programmatically open the form dialog.\n\t\t *\n\t\t * @param {object|null} item Pass null for create mode, or an object for edit mode\n\t\t * @public\n\t\t */\n\t\topenFormDialog(item = null) {\n\t\t\tthis.editItem = item\n\t\t\tthis.showFormDialogVisible = true\n\t\t},\n\n\t\t/**\n\t\t * Open the `createModal` registry modal, when one is set and a CnAppRoot\n\t\t * ancestor can mount it.\n\t\t *\n\t\t * @return {boolean} Whether the modal was opened.\n\t\t */\n\t\topenCreateModal() {\n\t\t\tif (!this.createModal || typeof this.cnOpenModal !== 'function') {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tthis.cnOpenModal(this.createModal)\n\t\t\treturn true\n\t\t},\n\n\t\t/**\n\t\t * Programmatically open the single-item delete dialog.\n\t\t *\n\t\t * @param {object} item The item to delete\n\t\t * @public\n\t\t */\n\t\topenDeleteDialog(item) {\n\t\t\tthis.actionTargetItem = item\n\t\t\tthis.showSingleDeleteDialog = true\n\t\t},\n\n\t\t/**\n\t\t * Pure helper used by the cardComponent dispatch path to toggle\n\t\t * an id in the selected-ids array. Kept inline rather than\n\t\t * pulled into a util because the only call site is the\n\t\t * cardComponent `@select` listener template above.\n\t\t *\n\t\t * @param {Array} ids Current selection\n\t\t * @param {string|number} id The id to toggle\n\t\t * @return {Array} New array with `id` toggled in/out\n\t\t */\n\t\ttoggleIdInArray(ids, id) {\n\t\t\tif (ids.includes(id)) {\n\t\t\t\treturn ids.filter((existing) => existing !== id)\n\t\t\t}\n\t\t\treturn [...ids, id]\n\t\t},\n\t},\n}\n</script>\n\n<!-- Styles in css/index-page.css -->\n"],"names":["_createElementBlock","_renderSlot","_createVNode","_openBlock","_createBlock","_createTextVNode","_createElementVNode","_Fragment","_renderList","_toDisplayString","_withCtx","_normalizeClass","_resolveDynamicComponent","_normalizeStyle","_mergeProps"],"mappings":"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"}
|