@conduction/nextcloud-vue 2.56.0-beta.3 → 2.56.0-beta.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js +12 -2
- package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js.map +1 -1
- package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js +1 -2
- package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js.map +1 -1
- package/dist/esm/components/CnActionsMenu/CnActionsMenu.vue2.js.map +1 -1
- package/dist/esm/components/CnActionsMenu/CnActionsMenu.vue3.js +0 -3
- package/dist/esm/components/CnActionsMenu/CnActionsMenu.vue3.js.map +1 -1
- package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue2.js.map +1 -1
- package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue3.js +1 -2
- package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue3.js.map +1 -1
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js +21 -1
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js +1 -1
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js.map +1 -1
- package/dist/esm/components/CnBoardView/CnBoardView.vue2.js +12 -5
- package/dist/esm/components/CnBoardView/CnBoardView.vue2.js.map +1 -1
- package/dist/esm/components/CnBoardView/CnBoardView.vue3.js +2 -1
- package/dist/esm/components/CnBoardView/CnBoardView.vue3.js.map +1 -1
- package/dist/esm/components/CnCardGrid/CnCardGrid.vue2.js +9 -1
- package/dist/esm/components/CnCardGrid/CnCardGrid.vue2.js.map +1 -1
- package/dist/esm/components/CnCardGrid/CnCardGrid.vue3.js.map +1 -1
- package/dist/esm/components/CnContextMenu/CnContextMenu.vue2.js +6 -1
- package/dist/esm/components/CnContextMenu/CnContextMenu.vue2.js.map +1 -1
- package/dist/esm/components/CnContextMenu/CnContextMenu.vue3.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +116 -15
- package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +27 -14
- package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
- package/dist/esm/components/CnDateAxisView/CnDateAxisView.vue2.js +13 -6
- package/dist/esm/components/CnDateAxisView/CnDateAxisView.vue2.js.map +1 -1
- package/dist/esm/components/CnDateAxisView/CnDateAxisView.vue3.js +2 -0
- package/dist/esm/components/CnDateAxisView/CnDateAxisView.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +29 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
- package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue2.js.map +1 -1
- package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue3.js +1 -2
- package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue3.js.map +1 -1
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js +3 -1
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js.map +1 -1
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js +17 -17
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js.map +1 -1
- package/dist/esm/components/CnFilesWidget/CnFilesWidget.vue2.js +4 -21
- package/dist/esm/components/CnFilesWidget/CnFilesWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnFilesWidget/CnFilesWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnFlowRunsWidget/CnFlowRunsWidget.vue2.js +0 -13
- package/dist/esm/components/CnFlowRunsWidget/CnFlowRunsWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnFlowRunsWidget/CnFlowRunsWidget.vue3.js +8 -8
- package/dist/esm/components/CnFlowRunsWidget/CnFlowRunsWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnFlowsPage/CnFlowsPage.vue2.js.map +1 -1
- package/dist/esm/components/CnFlowsPage/CnFlowsPage.vue3.js +3 -2
- package/dist/esm/components/CnFlowsPage/CnFlowsPage.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +67 -13
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +15 -8
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/manifestActionDispatch.js +31 -11
- package/dist/esm/components/CnIndexPage/manifestActionDispatch.js.map +1 -1
- package/dist/esm/components/CnIntegrationWidget/CnIntegrationWidgetEmpty.vue2.js.map +1 -1
- package/dist/esm/components/CnIntegrationWidget/CnIntegrationWidgetEmpty.vue3.js +1 -2
- package/dist/esm/components/CnIntegrationWidget/CnIntegrationWidgetEmpty.vue3.js.map +1 -1
- package/dist/esm/components/CnInteractionFormWidget/CnInteractionFormWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnInteractionFormWidget/CnInteractionFormWidget.vue3.js +0 -1
- package/dist/esm/components/CnInteractionFormWidget/CnInteractionFormWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnLinkButtonWidget/CnLinkButtonWidget.vue2.js +2 -17
- package/dist/esm/components/CnLinkButtonWidget/CnLinkButtonWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnLinkButtonWidget/CnLinkButtonWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnLogsPage/CnLogsPage.vue2.js +35 -11
- package/dist/esm/components/CnLogsPage/CnLogsPage.vue2.js.map +1 -1
- package/dist/esm/components/CnLogsPage/CnLogsPage.vue3.js +3 -2
- package/dist/esm/components/CnLogsPage/CnLogsPage.vue3.js.map +1 -1
- package/dist/esm/components/CnObjectCard/CnObjectCard.vue2.js +23 -5
- package/dist/esm/components/CnObjectCard/CnObjectCard.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectCard/CnObjectCard.vue3.js +1 -1
- package/dist/esm/components/CnObjectCard/CnObjectCard.vue3.js.map +1 -1
- package/dist/esm/components/CnObjectList/CnObjectList.vue2.js +19 -3
- package/dist/esm/components/CnObjectList/CnObjectList.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectList/CnObjectList.vue3.js.map +1 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +45 -15
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js +4 -2
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnObjectRow/CnObjectRow.vue2.js +24 -6
- package/dist/esm/components/CnObjectRow/CnObjectRow.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectRow/CnObjectRow.vue3.js +1 -1
- package/dist/esm/components/CnObjectRow/CnObjectRow.vue3.js.map +1 -1
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js +0 -1
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js.map +1 -1
- package/dist/esm/components/CnPageRenderer/CnPageRenderer.vue2.js.map +1 -1
- package/dist/esm/components/CnPageRenderer/CnPageRenderer.vue3.js +4 -2
- package/dist/esm/components/CnPageRenderer/CnPageRenderer.vue3.js.map +1 -1
- package/dist/esm/components/CnRowActions/CnRowActions.vue2.js +7 -2
- package/dist/esm/components/CnRowActions/CnRowActions.vue2.js.map +1 -1
- package/dist/esm/components/CnRowActions/CnRowActions.vue3.js.map +1 -1
- package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js +49 -5
- package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js.map +1 -1
- package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue3.js +1 -1
- package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue3.js.map +1 -1
- package/dist/esm/components/CnSpendAnalyticsWidget/CnSpendAnalyticsWidget.vue2.js +5 -3
- package/dist/esm/components/CnSpendAnalyticsWidget/CnSpendAnalyticsWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnSpendAnalyticsWidget/CnSpendAnalyticsWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnTabs/CnTabs.vue2.js.map +1 -1
- package/dist/esm/components/CnTabs/CnTabs.vue3.js.map +1 -1
- package/dist/esm/components/CnTasksWidget/CnTasksWidget.vue2.js +3 -1
- package/dist/esm/components/CnTasksWidget/CnTasksWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnTasksWidget/CnTasksWidget.vue3.js +1 -0
- package/dist/esm/components/CnTasksWidget/CnTasksWidget.vue3.js.map +1 -1
- package/dist/esm/integrations/builtin/analytics/CnAnalyticsCard.vue2.js +3 -1
- package/dist/esm/integrations/builtin/analytics/CnAnalyticsCard.vue2.js.map +1 -1
- package/dist/esm/integrations/builtin/analytics/CnAnalyticsCard.vue3.js.map +1 -1
- package/dist/esm/integrations/builtin/analytics/CnAnalyticsTab.vue2.js +3 -1
- package/dist/esm/integrations/builtin/analytics/CnAnalyticsTab.vue2.js.map +1 -1
- package/dist/esm/integrations/builtin/analytics/CnAnalyticsTab.vue3.js +2 -4
- package/dist/esm/integrations/builtin/analytics/CnAnalyticsTab.vue3.js.map +1 -1
- package/dist/esm/integrations/builtin/bookmarks/CnBookmarksCard.vue2.js +6 -0
- package/dist/esm/integrations/builtin/bookmarks/CnBookmarksCard.vue2.js.map +1 -1
- package/dist/esm/integrations/builtin/bookmarks/CnBookmarksCard.vue3.js +68 -53
- package/dist/esm/integrations/builtin/bookmarks/CnBookmarksCard.vue3.js.map +1 -1
- package/dist/esm/integrations/builtin/bookmarks/CnBookmarksTab.vue2.js +17 -0
- package/dist/esm/integrations/builtin/bookmarks/CnBookmarksTab.vue2.js.map +1 -1
- package/dist/esm/integrations/builtin/bookmarks/CnBookmarksTab.vue3.js +21 -19
- package/dist/esm/integrations/builtin/bookmarks/CnBookmarksTab.vue3.js.map +1 -1
- package/dist/esm/integrations/builtin/collectives/CnCollectivesCard.vue2.js +3 -1
- package/dist/esm/integrations/builtin/collectives/CnCollectivesCard.vue2.js.map +1 -1
- package/dist/esm/integrations/builtin/collectives/CnCollectivesCard.vue3.js.map +1 -1
- package/dist/esm/integrations/builtin/collectives/CnCollectivesTab.vue2.js +3 -1
- package/dist/esm/integrations/builtin/collectives/CnCollectivesTab.vue2.js.map +1 -1
- package/dist/esm/integrations/builtin/collectives/CnCollectivesTab.vue3.js +2 -4
- package/dist/esm/integrations/builtin/collectives/CnCollectivesTab.vue3.js.map +1 -1
- package/dist/esm/integrations/builtin/cospend/CnCospendCard.vue2.js +2 -1
- package/dist/esm/integrations/builtin/cospend/CnCospendCard.vue2.js.map +1 -1
- package/dist/esm/integrations/builtin/cospend/CnCospendCard.vue3.js.map +1 -1
- package/dist/esm/integrations/builtin/cospend/CnCospendTab.vue2.js +2 -1
- package/dist/esm/integrations/builtin/cospend/CnCospendTab.vue2.js.map +1 -1
- package/dist/esm/integrations/builtin/cospend/CnCospendTab.vue3.js +1 -2
- package/dist/esm/integrations/builtin/cospend/CnCospendTab.vue3.js.map +1 -1
- package/dist/esm/integrations/builtin/deck/CnDeckCard.vue2.js +2 -1
- package/dist/esm/integrations/builtin/deck/CnDeckCard.vue2.js.map +1 -1
- package/dist/esm/integrations/builtin/deck/CnDeckCard.vue3.js.map +1 -1
- package/dist/esm/integrations/builtin/deck/CnDeckTab.vue2.js +2 -1
- package/dist/esm/integrations/builtin/deck/CnDeckTab.vue2.js.map +1 -1
- package/dist/esm/integrations/builtin/deck/CnDeckTab.vue3.js +1 -2
- package/dist/esm/integrations/builtin/deck/CnDeckTab.vue3.js.map +1 -1
- package/dist/esm/integrations/builtin/email/CnEmailCard.vue2.js.map +1 -1
- package/dist/esm/integrations/builtin/email/CnEmailCard.vue3.js.map +1 -1
- package/dist/esm/integrations/builtin/email/CnEmailTab.vue2.js.map +1 -1
- package/dist/esm/integrations/builtin/email/CnEmailTab.vue3.js +1 -2
- package/dist/esm/integrations/builtin/email/CnEmailTab.vue3.js.map +1 -1
- package/dist/esm/integrations/builtin/flow/CnFlowCard.vue2.js +2 -1
- package/dist/esm/integrations/builtin/flow/CnFlowCard.vue2.js.map +1 -1
- package/dist/esm/integrations/builtin/flow/CnFlowCard.vue3.js.map +1 -1
- package/dist/esm/integrations/builtin/flow/CnFlowTab.vue2.js +2 -1
- package/dist/esm/integrations/builtin/flow/CnFlowTab.vue2.js.map +1 -1
- package/dist/esm/integrations/builtin/flow/CnFlowTab.vue3.js +1 -2
- package/dist/esm/integrations/builtin/flow/CnFlowTab.vue3.js.map +1 -1
- package/dist/esm/integrations/builtin/forms/CnFormsCard.vue2.js +3 -1
- package/dist/esm/integrations/builtin/forms/CnFormsCard.vue2.js.map +1 -1
- package/dist/esm/integrations/builtin/forms/CnFormsCard.vue3.js.map +1 -1
- package/dist/esm/integrations/builtin/forms/CnFormsTab.vue2.js +3 -1
- package/dist/esm/integrations/builtin/forms/CnFormsTab.vue2.js.map +1 -1
- package/dist/esm/integrations/builtin/forms/CnFormsTab.vue3.js +2 -4
- package/dist/esm/integrations/builtin/forms/CnFormsTab.vue3.js.map +1 -1
- package/dist/esm/integrations/builtin/maps/CnMapsCard.vue2.js +3 -1
- package/dist/esm/integrations/builtin/maps/CnMapsCard.vue2.js.map +1 -1
- package/dist/esm/integrations/builtin/maps/CnMapsCard.vue3.js.map +1 -1
- package/dist/esm/integrations/builtin/maps/CnMapsTab.vue2.js +3 -1
- package/dist/esm/integrations/builtin/maps/CnMapsTab.vue2.js.map +1 -1
- package/dist/esm/integrations/builtin/maps/CnMapsTab.vue3.js +2 -4
- package/dist/esm/integrations/builtin/maps/CnMapsTab.vue3.js.map +1 -1
- package/dist/esm/integrations/builtin/openproject/CnOpenprojectCard.vue2.js +3 -1
- package/dist/esm/integrations/builtin/openproject/CnOpenprojectCard.vue2.js.map +1 -1
- package/dist/esm/integrations/builtin/openproject/CnOpenprojectCard.vue3.js.map +1 -1
- package/dist/esm/integrations/builtin/openproject/CnOpenprojectTab.vue2.js +3 -1
- package/dist/esm/integrations/builtin/openproject/CnOpenprojectTab.vue2.js.map +1 -1
- package/dist/esm/integrations/builtin/openproject/CnOpenprojectTab.vue3.js +2 -4
- package/dist/esm/integrations/builtin/openproject/CnOpenprojectTab.vue3.js.map +1 -1
- package/dist/esm/integrations/builtin/photos/CnPhotosCard.vue2.js +2 -1
- package/dist/esm/integrations/builtin/photos/CnPhotosCard.vue2.js.map +1 -1
- package/dist/esm/integrations/builtin/photos/CnPhotosCard.vue3.js.map +1 -1
- package/dist/esm/integrations/builtin/photos/CnPhotosTab.vue2.js +2 -1
- package/dist/esm/integrations/builtin/photos/CnPhotosTab.vue2.js.map +1 -1
- package/dist/esm/integrations/builtin/photos/CnPhotosTab.vue3.js +1 -2
- package/dist/esm/integrations/builtin/photos/CnPhotosTab.vue3.js.map +1 -1
- package/dist/esm/integrations/builtin/polls/CnPollsCard.vue2.js +2 -1
- package/dist/esm/integrations/builtin/polls/CnPollsCard.vue2.js.map +1 -1
- package/dist/esm/integrations/builtin/polls/CnPollsCard.vue3.js.map +1 -1
- package/dist/esm/integrations/builtin/polls/CnPollsTab.vue2.js +2 -1
- package/dist/esm/integrations/builtin/polls/CnPollsTab.vue2.js.map +1 -1
- package/dist/esm/integrations/builtin/polls/CnPollsTab.vue3.js +1 -2
- package/dist/esm/integrations/builtin/polls/CnPollsTab.vue3.js.map +1 -1
- package/dist/esm/integrations/builtin/shares/CnSharesTab.vue2.js.map +1 -1
- package/dist/esm/integrations/builtin/shares/CnSharesTab.vue3.js +3 -6
- package/dist/esm/integrations/builtin/shares/CnSharesTab.vue3.js.map +1 -1
- package/dist/esm/integrations/builtin/talk/CnTalkCard.vue2.js +3 -1
- package/dist/esm/integrations/builtin/talk/CnTalkCard.vue2.js.map +1 -1
- package/dist/esm/integrations/builtin/talk/CnTalkCard.vue3.js.map +1 -1
- package/dist/esm/integrations/builtin/talk/CnTalkTab.vue2.js +3 -1
- package/dist/esm/integrations/builtin/talk/CnTalkTab.vue2.js.map +1 -1
- package/dist/esm/integrations/builtin/talk/CnTalkTab.vue3.js +2 -4
- package/dist/esm/integrations/builtin/talk/CnTalkTab.vue3.js.map +1 -1
- package/dist/esm/integrations/builtin/time-tracker/CnTimeTrackerCard.vue2.js +2 -1
- package/dist/esm/integrations/builtin/time-tracker/CnTimeTrackerCard.vue2.js.map +1 -1
- package/dist/esm/integrations/builtin/time-tracker/CnTimeTrackerCard.vue3.js.map +1 -1
- package/dist/esm/integrations/builtin/time-tracker/CnTimeTrackerTab.vue2.js +2 -1
- package/dist/esm/integrations/builtin/time-tracker/CnTimeTrackerTab.vue2.js.map +1 -1
- package/dist/esm/integrations/builtin/time-tracker/CnTimeTrackerTab.vue3.js +1 -2
- package/dist/esm/integrations/builtin/time-tracker/CnTimeTrackerTab.vue3.js.map +1 -1
- package/dist/esm/integrations/builtin/xwiki/CnXwikiCard.vue2.js +4 -1
- package/dist/esm/integrations/builtin/xwiki/CnXwikiCard.vue2.js.map +1 -1
- package/dist/esm/integrations/builtin/xwiki/CnXwikiCard.vue3.js.map +1 -1
- package/dist/esm/integrations/builtin/xwiki/CnXwikiTab.vue2.js +6 -3
- package/dist/esm/integrations/builtin/xwiki/CnXwikiTab.vue2.js.map +1 -1
- package/dist/esm/integrations/builtin/xwiki/CnXwikiTab.vue3.js +2 -4
- package/dist/esm/integrations/builtin/xwiki/CnXwikiTab.vue3.js.map +1 -1
- package/dist/esm/l10n/en.json.js +2 -0
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +2 -0
- package/dist/esm/l10n/nl.json.js.map +1 -1
- package/dist/esm/nextcloud-vue.css +78 -7
- package/dist/esm/utils/actionLink.js +5 -0
- package/dist/esm/utils/actionLink.js.map +1 -1
- package/dist/esm/utils/actionsDispatcher.js +16 -1
- package/dist/esm/utils/actionsDispatcher.js.map +1 -1
- package/dist/esm/utils/linkNavigation.js +6 -3
- package/dist/esm/utils/linkNavigation.js.map +1 -1
- package/dist/esm/utils/rowAuxClick.js +30 -5
- package/dist/esm/utils/rowAuxClick.js.map +1 -1
- package/dist/esm/utils/safeHref.js +18 -3
- package/dist/esm/utils/safeHref.js.map +1 -1
- package/dist/esm/utils/validateManifestV2.compiled.js +2 -2
- package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
- package/dist/nextcloud-vue.cjs.js +885 -393
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +78 -7
- package/l10n/en.json +2 -0
- package/l10n/nl.json +2 -0
- package/package.json +1 -1
- package/src/components/CnActionsBar/CnActionsBar.vue +14 -5
- package/src/components/CnActionsMenu/CnActionsMenu.vue +0 -3
- package/src/components/CnAdminSettingsShell/CnAdminSettingsShell.vue +1 -2
- package/src/components/CnAppRoot/CnAppRoot.vue +31 -2
- package/src/components/CnBoardView/CnBoardView.vue +13 -5
- package/src/components/CnCardGrid/CnCardGrid.vue +9 -1
- package/src/components/CnContextMenu/CnContextMenu.vue +6 -1
- package/src/components/CnDataTable/CnDataTable.md +1 -1
- package/src/components/CnDataTable/CnDataTable.vue +131 -17
- package/src/components/CnDateAxisView/CnDateAxisView.vue +15 -6
- package/src/components/CnDetailPage/CnDetailPage.md +1 -1
- package/src/components/CnDetailPage/CnDetailPage.vue +29 -1
- package/src/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue +1 -2
- package/src/components/CnFilesBrowser/CnFilesBrowser.vue +5 -3
- package/src/components/CnFilesWidget/CnFilesWidget.vue +4 -21
- package/src/components/CnFlowRunsWidget/CnFlowRunsWidget.vue +8 -21
- package/src/components/CnFlowsPage/CnFlowsPage.vue +2 -1
- package/src/components/CnIndexPage/CnIndexPage.vue +75 -14
- package/src/components/CnIndexPage/manifestActionDispatch.js +31 -11
- package/src/components/CnIntegrationWidget/CnIntegrationWidgetEmpty.vue +1 -2
- package/src/components/CnInteractionFormWidget/CnInteractionFormWidget.vue +0 -1
- package/src/components/CnLinkButtonWidget/CnLinkButtonWidget.vue +2 -17
- package/src/components/CnLogsPage/CnLogsPage.vue +37 -12
- package/src/components/CnObjectCard/CnObjectCard.vue +24 -6
- package/src/components/CnObjectList/CnObjectList.vue +19 -3
- package/src/components/CnObjectListWidget/CnObjectListWidget.vue +47 -15
- package/src/components/CnObjectRow/CnObjectRow.vue +25 -7
- package/src/components/CnObjectSidebar/CnFilesTab.vue +0 -1
- package/src/components/CnPageRenderer/CnPageRenderer.vue +2 -0
- package/src/components/CnRowActions/CnRowActions.vue +7 -2
- package/src/components/CnSetupWizard/CnSetupWizard.vue +57 -6
- package/src/components/CnSpendAnalyticsWidget/CnSpendAnalyticsWidget.vue +5 -3
- package/src/components/CnTabs/CnTabs.vue +7 -3
- package/src/components/CnTasksWidget/CnTasksWidget.vue +4 -1
- package/src/css/table.css +26 -0
- package/src/integrations/builtin/analytics/CnAnalyticsCard.vue +3 -2
- package/src/integrations/builtin/analytics/CnAnalyticsTab.vue +5 -6
- package/src/integrations/builtin/bookmarks/CnBookmarksCard.vue +12 -1
- package/src/integrations/builtin/bookmarks/CnBookmarksTab.vue +23 -6
- package/src/integrations/builtin/bookmarks/__tests__/CnBookmarksTab.spec.js +7 -2
- package/src/integrations/builtin/collectives/CnCollectivesCard.vue +3 -2
- package/src/integrations/builtin/collectives/CnCollectivesTab.vue +5 -6
- package/src/integrations/builtin/cospend/CnCospendCard.vue +2 -2
- package/src/integrations/builtin/cospend/CnCospendTab.vue +3 -4
- package/src/integrations/builtin/deck/CnDeckCard.vue +2 -2
- package/src/integrations/builtin/deck/CnDeckTab.vue +3 -4
- package/src/integrations/builtin/email/CnEmailCard.vue +6 -2
- package/src/integrations/builtin/email/CnEmailTab.vue +7 -4
- package/src/integrations/builtin/flow/CnFlowCard.vue +2 -2
- package/src/integrations/builtin/flow/CnFlowTab.vue +3 -4
- package/src/integrations/builtin/forms/CnFormsCard.vue +3 -2
- package/src/integrations/builtin/forms/CnFormsTab.vue +5 -6
- package/src/integrations/builtin/maps/CnMapsCard.vue +3 -2
- package/src/integrations/builtin/maps/CnMapsTab.vue +5 -6
- package/src/integrations/builtin/maps/__tests__/CnMapsTab.spec.js +9 -0
- package/src/integrations/builtin/openproject/CnOpenprojectCard.vue +3 -2
- package/src/integrations/builtin/openproject/CnOpenprojectTab.vue +5 -6
- package/src/integrations/builtin/photos/CnPhotosCard.vue +2 -2
- package/src/integrations/builtin/photos/CnPhotosTab.vue +3 -4
- package/src/integrations/builtin/polls/CnPollsCard.vue +2 -2
- package/src/integrations/builtin/polls/CnPollsTab.vue +3 -4
- package/src/integrations/builtin/shares/CnSharesTab.vue +3 -6
- package/src/integrations/builtin/talk/CnTalkCard.vue +3 -2
- package/src/integrations/builtin/talk/CnTalkTab.vue +5 -6
- package/src/integrations/builtin/time-tracker/CnTimeTrackerCard.vue +2 -2
- package/src/integrations/builtin/time-tracker/CnTimeTrackerTab.vue +3 -4
- package/src/integrations/builtin/xwiki/CnXwikiCard.vue +4 -2
- package/src/integrations/builtin/xwiki/CnXwikiTab.vue +7 -7
- package/src/integrations/builtin/xwiki/__tests__/CnXwikiTab.spec.js +9 -0
- package/src/schemas/app-manifest-v2.schema.json +2 -1
- package/src/utils/actionLink.js +5 -0
- package/src/utils/actionsDispatcher.js +15 -0
- package/src/utils/linkNavigation.js +6 -3
- package/src/utils/rowAuxClick.js +29 -4
- package/src/utils/safeHref.js +18 -3
- package/src/utils/validateManifestV2.compiled.js +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CnPhotosCard.vue2.js","sources":["../../../../../src/integrations/builtin/photos/CnPhotosCard.vue"],"sourcesContent":["<!--\n CnPhotosCard — bespoke surface-aware widget for the `photos`\n integration.\n\n Replaces the generic CnIntegrationCard for the `photos` leaf. Branches\n on `surface` per ADR-019 AD-19:\n - user-dashboard / app-dashboard : count headline \"N albums\" + the\n first album's cover thumbnail + a \"view all\" trail-off.\n - detail-page : a compact 2-column thumbnail grid\n of the linked albums (COMPACT_LIMIT cap) with \"view all\".\n - single-entity : a chip with the cover thumbnail +\n album name for reference-property auto-rendering.\n\n Pulls rows from the same OR pluggable-integration sub-resource as\n CnPhotosTab; for `single-entity` the optional `value` prop addresses a\n single album by id.\n\n See `openregister/openspec/changes/integration-photos/` for the spec\n delta and ADR-019 (registry mechanism), AD-19 (surface fallback).\n-->\n<template>\n\t<CnDetailCard :title=\"cardTitle\" :icon=\"cardIcon\" :collapsible=\"collapsible\">\n\t\t<NcLoadingIcon v-if=\"loading\" />\n\n\t\t<!-- single-entity surface: chip -->\n\t\t<template v-else-if=\"surface === 'single-entity'\">\n\t\t\t<span v-if=\"entity\" class=\"cn-photos-card__chip\" :title=\"albumName(entity)\">\n\t\t\t\t<img\n\t\t\t\t\tv-if=\"coverUrl(entity)\"\n\t\t\t\t\t:src=\"coverUrl(entity)\"\n\t\t\t\t\talt=\"\"\n\t\t\t\t\tclass=\"cn-photos-card__chip-cover\"\n\t\t\t\t\t@error=\"onCoverError(entity)\">\n\t\t\t\t<ImageIcon v-else :size=\"14\" />\n\t\t\t\t<a\n\t\t\t\t\t:href=\"albumUrl(entity)\"\n\t\t\t\t\ttarget=\"_blank\"\n\t\t\t\t\trel=\"noopener noreferrer\">{{ albumName(entity) }}</a>\n\t\t\t</span>\n\t\t\t<span v-else class=\"cn-photos-card__empty\">{{ emptyLabel }}</span>\n\t\t</template>\n\n\t\t<!-- dashboard surfaces: headline + first cover -->\n\t\t<template v-else-if=\"surface === 'user-dashboard' || surface === 'app-dashboard'\">\n\t\t\t<div v-if=\"degraded\" class=\"cn-photos-card__empty\">\n\t\t\t\t{{ degraded }}\n\t\t\t</div>\n\t\t\t<div v-else-if=\"albums.length === 0\" class=\"cn-photos-card__empty\">\n\t\t\t\t{{ emptyLabel }}\n\t\t\t</div>\n\t\t\t<div v-else class=\"cn-photos-card__headline\">\n\t\t\t\t<div class=\"cn-photos-card__headline-line\">\n\t\t\t\t\t<strong>{{ albumCountLabel }}</strong>\n\t\t\t\t\t<span v-if=\"totalPhotoCount !== null\" class=\"cn-photos-card__photo-total\">\n\t\t\t\t\t\t· {{ totalPhotosLabel }}\n\t\t\t\t\t</span>\n\t\t\t\t</div>\n\t\t\t\t<div v-if=\"firstAlbum\" class=\"cn-photos-card__headline-album\">\n\t\t\t\t\t<div class=\"cn-photos-card__headline-cover\">\n\t\t\t\t\t\t<img\n\t\t\t\t\t\t\tv-if=\"coverUrl(firstAlbum)\"\n\t\t\t\t\t\t\t:src=\"coverUrl(firstAlbum)\"\n\t\t\t\t\t\t\talt=\"\"\n\t\t\t\t\t\t\tclass=\"cn-photos-card__headline-cover-img\"\n\t\t\t\t\t\t\t@error=\"onCoverError(firstAlbum)\">\n\t\t\t\t\t\t<ImageMultiple v-else :size=\"18\" />\n\t\t\t\t\t</div>\n\t\t\t\t\t<a\n\t\t\t\t\t\t:href=\"albumUrl(firstAlbum)\"\n\t\t\t\t\t\ttarget=\"_blank\"\n\t\t\t\t\t\trel=\"noopener noreferrer\"\n\t\t\t\t\t\tclass=\"cn-photos-card__headline-name\">\n\t\t\t\t\t\t{{ albumName(firstAlbum) }}\n\t\t\t\t\t</a>\n\t\t\t\t</div>\n\t\t\t\t<div v-if=\"albums.length > 1\" class=\"cn-photos-card__view-all\">\n\t\t\t\t\t<a\n\t\t\t\t\t\t:href=\"photosAppUrl\"\n\t\t\t\t\t\ttarget=\"_blank\"\n\t\t\t\t\t\trel=\"noopener noreferrer\">{{ viewAllLabel }}</a>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</template>\n\n\t\t<!-- detail-page surface: compact grid -->\n\t\t<template v-else>\n\t\t\t<div v-if=\"degraded\" class=\"cn-photos-card__empty\">\n\t\t\t\t{{ degraded }}\n\t\t\t</div>\n\t\t\t<div v-else-if=\"albums.length === 0\" class=\"cn-photos-card__empty\">\n\t\t\t\t{{ emptyLabel }}\n\t\t\t</div>\n\t\t\t<div v-else class=\"cn-photos-card__grid\">\n\t\t\t\t<a\n\t\t\t\t\tv-for=\"album in displayedAlbums\"\n\t\t\t\t\t:key=\"albumKey(album)\"\n\t\t\t\t\t:href=\"albumUrl(album)\"\n\t\t\t\t\tclass=\"cn-photos-card__tile\"\n\t\t\t\t\ttarget=\"_blank\"\n\t\t\t\t\trel=\"noopener noreferrer\"\n\t\t\t\t\t:title=\"albumName(album)\">\n\t\t\t\t\t<div class=\"cn-photos-card__tile-cover\">\n\t\t\t\t\t\t<img\n\t\t\t\t\t\t\tv-if=\"coverUrl(album)\"\n\t\t\t\t\t\t\t:src=\"coverUrl(album)\"\n\t\t\t\t\t\t\talt=\"\"\n\t\t\t\t\t\t\tclass=\"cn-photos-card__tile-cover-img\"\n\t\t\t\t\t\t\tloading=\"lazy\"\n\t\t\t\t\t\t\t@error=\"onCoverError(album)\">\n\t\t\t\t\t\t<ImageMultiple v-else :size=\"20\" class=\"cn-photos-card__tile-fallback\" />\n\t\t\t\t\t</div>\n\t\t\t\t\t<span class=\"cn-photos-card__tile-name\">{{ albumName(album) }}</span>\n\t\t\t\t</a>\n\t\t\t</div>\n\t\t\t<div v-if=\"albums.length > COMPACT_LIMIT\" class=\"cn-photos-card__view-all\">\n\t\t\t\t<a\n\t\t\t\t\t:href=\"photosAppUrl\"\n\t\t\t\t\ttarget=\"_blank\"\n\t\t\t\t\trel=\"noopener noreferrer\">{{ viewAllLabel }}</a>\n\t\t\t</div>\n\t\t</template>\n\t</CnDetailCard>\n</template>\n\n<script>\nimport { translatePlural as n, translate as t } from '@nextcloud/l10n'\nimport { NcLoadingIcon } from '@nextcloud/vue'\nimport ImageIcon from 'vue-material-design-icons/Image.vue'\nimport ImageMultiple from 'vue-material-design-icons/ImageMultiple.vue'\nimport CnDetailCard from '../../../components/CnDetailCard/CnDetailCard.vue'\nimport { buildHeaders, prefixUrl } from '../../../utils/index.js'\n\nconst VALID_SURFACES = ['user-dashboard', 'app-dashboard', 'detail-page', 'single-entity']\nconst COMPACT_LIMIT = 4\n\n/**\n * CnPhotosCard — bespoke surface-aware widget for the `photos`\n * integration.\n *\n * Renders Photos-aware metadata (album count, cover thumbnail, total\n * photo count) across all four AD-19 surfaces. See the file-level\n * docblock for surface-by-surface behaviour.\n */\nexport default {\n\tname: 'CnPhotosCard',\n\n\tcomponents: { CnDetailCard, NcLoadingIcon, ImageIcon, ImageMultiple },\n\n\tprops: {\n\t\t/** Stable integration id (forwarded from the registry — always `'photos'`). */\n\t\tintegrationId: { type: String, default: 'photos' },\n\t\t/** OpenRegister register id (slug or uuid). */\n\t\tregister: { type: String, required: true },\n\t\t/** OpenRegister schema id (slug or uuid). */\n\t\tschema: { type: String, required: true },\n\t\t/** Parent object id. */\n\t\tobjectId: { type: String, required: true },\n\t\t/** Rendering surface (AD-19). */\n\t\tsurface: {\n\t\t\ttype: String,\n\t\t\tdefault: 'detail-page',\n\t\t\tvalidator: (s) => VALID_SURFACES.includes(s),\n\t\t},\n\n\t\t/** Optional single-entity reference (album id). */\n\t\tvalue: { type: [String, Number], default: '' },\n\t\t/** Pre-translated card title. */\n\t\ttitle: { type: String, default: () => t('nextcloud-vue', 'Photos') },\n\t\t/** Optional Material Design Icon component. */\n\t\ticon: { type: Object, default: () => ImageIcon },\n\t\t/** Base API URL. */\n\t\tapiBase: { type: String, default: '/apps/openregister/api' },\n\t\t/** Whether the card body is collapsible. */\n\t\tcollapsible: { type: Boolean, default: true },\n\t\t/** Pre-translated empty-state label. */\n\t\temptyLabel: { type: String, default: () => t('nextcloud-vue', 'No albums linked yet') },\n\t\t/** Pre-translated 503 unavailable label. */\n\t\tunavailableLabel: { type: String, default: () => t('nextcloud-vue', 'NC Photos is currently unavailable.') },\n\t\t/** Pre-translated \"view all\" trail-off link label. */\n\t\tviewAllLabel: { type: String, default: () => t('nextcloud-vue', 'View all in Photos') },\n\t\t/** URL of the NC Photos app entry. */\n\t\tphotosAppUrl: { type: String, default: '/index.php/apps/photos' },\n\t},\n\n\tdata() {\n\t\treturn {\n\t\t\tCOMPACT_LIMIT,\n\t\t\talbums: [],\n\t\t\tentity: null,\n\t\t\tloading: false,\n\t\t\tdegraded: '',\n\t\t\tbrokenCovers: {},\n\t\t}\n\t},\n\n\tcomputed: {\n\t\tcardTitle() {\n\t\t\treturn this.title || this.integrationId\n\t\t},\n\n\t\tcardIcon() {\n\t\t\treturn this.icon\n\t\t},\n\n\t\tdisplayedAlbums() {\n\t\t\treturn this.albums.slice(0, COMPACT_LIMIT)\n\t\t},\n\n\t\talbumCountLabel() {\n\t\t\tconst total = this.albums.length\n\t\t\treturn n('nextcloud-vue', '{count} album', '{count} albums', total, { count: total })\n\t\t},\n\n\t\ttotalPhotoCount() {\n\t\t\tlet total = 0\n\t\t\tlet any = false\n\t\t\tfor (const a of this.albums) {\n\t\t\t\tconst c = this.photoCount(a)\n\t\t\t\tif (c !== null) {\n\t\t\t\t\ttotal += c\n\t\t\t\t\tany = true\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn any ? total : null\n\t\t},\n\n\t\ttotalPhotosLabel() {\n\t\t\tconst count = this.totalPhotoCount ?? 0\n\t\t\treturn n('nextcloud-vue', '{count} photo', '{count} photos', count, { count })\n\t\t},\n\n\t\tfirstAlbum() {\n\t\t\treturn this.albums.length > 0 ? this.albums[0] : null\n\t\t},\n\t},\n\n\twatch: {\n\t\tobjectId: { immediate: true, handler() {\n\t\t\tthis.fetch()\n\t\t} },\n\n\t\tsurface() {\n\t\t\tthis.fetch()\n\t\t},\n\n\t\tvalue() {\n\t\t\tif (this.surface === 'single-entity') {\n\t\t\t\tthis.fetchSingle()\n\t\t\t}\n\t\t},\n\t},\n\n\tmethods: {\n\t\tbaseUrl() {\n\t\t\treturn `${this.apiBase}/objects/${this.register}/${this.schema}/${this.objectId}/integrations/${this.integrationId}`\n\t\t},\n\n\t\talbumKey(album) {\n\t\t\treturn (album && (album.id ?? album.data?.album_id)) || ''\n\t\t},\n\n\t\talbumName(album) {\n\t\t\tconst raw = (album && (album.title || album.name || album.data?.name)) || ''\n\t\t\treturn String(raw).replace(/\\s*\\[or:[^\\]]+\\]\\s*/g, ' ').trim() || t('nextcloud-vue', 'Untitled album')\n\t\t},\n\n\t\talbumUrl(album) {\n\t\t\tif (album && album.url) {\n\t\t\t\treturn album.url\n\t\t\t}\n\t\t\tconst name = this.albumName(album)\n\t\t\treturn `/index.php/apps/photos/albums/${encodeURIComponent(name)}`\n\t\t},\n\n\t\tcoverUrl(album) {\n\t\t\tif (!album || this.brokenCovers[this.albumKey(album)]) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn album.coverPhotoUrl || album.data?.coverPhotoUrl || ''\n\t\t},\n\n\t\tonCoverError(album) {\n\t\t\tthis.brokenCovers[this.albumKey(album)] = true\n\t\t},\n\n\t\tphotoCount(album) {\n\t\t\tconst raw = album?.photoCount ?? album?.data?.photoCount ?? album?.data?.count\n\t\t\tif (raw === null || raw === undefined || raw === '') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst num = Number(raw)\n\t\t\treturn Number.isFinite(num) ? num : null\n\t\t},\n\n\t\tfetch() {\n\t\t\tif (this.surface === 'single-entity') {\n\t\t\t\tthis.fetchSingle()\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.fetchList()\n\t\t},\n\n\t\tasync fetchList() {\n\t\t\tif (!this.register || !this.schema || !this.objectId) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.loading = true\n\t\t\tthis.degraded = ''\n\t\t\ttry {\n\t\t\t\tconst response = await fetch(prefixUrl(this.baseUrl()), { headers: buildHeaders() })\n\t\t\t\tif (response.ok) {\n\t\t\t\t\tconst data = await response.json()\n\t\t\t\t\tthis.albums = data.results || data.items || (Array.isArray(data) ? data : []) || []\n\t\t\t\t} else if (response.status === 503) {\n\t\t\t\t\tthis.albums = []\n\t\t\t\t\tthis.degraded = this.unavailableLabel\n\t\t\t\t} else {\n\t\t\t\t\tthis.albums = []\n\t\t\t\t}\n\t\t\t} catch (err) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.error('[CnPhotosCard] failed to fetch albums', err)\n\t\t\t\tthis.albums = []\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\t\t},\n\n\t\tasync fetchSingle() {\n\t\t\tif (this.value === '' || this.value === undefined || this.value === null || !this.register || !this.schema || !this.objectId) {\n\t\t\t\tthis.entity = null\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.loading = true\n\t\t\tthis.degraded = ''\n\t\t\ttry {\n\t\t\t\tconst response = await fetch(prefixUrl(`${this.baseUrl()}/${encodeURIComponent(this.value)}`), { headers: buildHeaders() })\n\t\t\t\tif (response.ok) {\n\t\t\t\t\tthis.entity = await response.json()\n\t\t\t\t} else if (response.status === 503) {\n\t\t\t\t\tthis.entity = null\n\t\t\t\t\tthis.degraded = this.unavailableLabel\n\t\t\t\t} else {\n\t\t\t\t\tthis.entity = null\n\t\t\t\t}\n\t\t\t} catch (err) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.error('[CnPhotosCard] failed to fetch single album', err)\n\t\t\t\tthis.entity = null\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-photos-card__empty {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.9em;\n\tpadding: 8px 0;\n}\n\n.cn-photos-card__headline {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 6px;\n}\n\n.cn-photos-card__headline-line {\n\tfont-size: 1.1em;\n\tcolor: var(--color-main-text);\n}\n\n.cn-photos-card__photo-total {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.9em;\n\tfont-weight: 400;\n}\n\n.cn-photos-card__headline-album {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 8px;\n\toverflow: hidden;\n}\n\n.cn-photos-card__headline-cover {\n\twidth: 32px;\n\theight: 32px;\n\tborder-radius: var(--border-radius);\n\toverflow: hidden;\n\tflex-shrink: 0;\n\tbackground: var(--color-background-darker, var(--color-background-dark));\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: center;\n}\n\n.cn-photos-card__headline-cover-img {\n\twidth: 100%;\n\theight: 100%;\n\tobject-fit: cover;\n}\n\n.cn-photos-card__headline-name {\n\tcolor: var(--color-main-text);\n\ttext-decoration: none;\n\toverflow: hidden;\n\ttext-overflow: ellipsis;\n\twhite-space: nowrap;\n\tfont-size: 0.95em;\n}\n\n.cn-photos-card__headline-name:hover {\n\ttext-decoration: underline;\n}\n\n.cn-photos-card__chip {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 6px;\n\tpadding: 4px 10px 4px 4px;\n\tborder-radius: 14px;\n\tbackground: var(--color-background-hover);\n\tfont-size: 0.9em;\n\tmax-width: 100%;\n}\n\n.cn-photos-card__chip-cover {\n\twidth: 20px;\n\theight: 20px;\n\tborder-radius: 50%;\n\tobject-fit: cover;\n\tflex-shrink: 0;\n}\n\n.cn-photos-card__chip a {\n\tcolor: var(--color-main-text);\n\ttext-decoration: none;\n\toverflow: hidden;\n\ttext-overflow: ellipsis;\n\twhite-space: nowrap;\n}\n\n.cn-photos-card__chip a:hover {\n\ttext-decoration: underline;\n}\n\n.cn-photos-card__grid {\n\tdisplay: grid;\n\tgrid-template-columns: repeat(2, 1fr);\n\tgap: 8px;\n}\n\n.cn-photos-card__tile {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 4px;\n\ttext-decoration: none;\n\tcolor: inherit;\n\tborder-radius: var(--border-radius);\n\toverflow: hidden;\n\tbackground: var(--color-background-hover);\n}\n\n.cn-photos-card__tile:hover,\n.cn-photos-card__tile:focus-within {\n\tbackground: var(--color-background-dark, var(--color-background-hover));\n}\n\n.cn-photos-card__tile-cover {\n\twidth: 100%;\n\taspect-ratio: 1 / 1;\n\tbackground: var(--color-background-darker, var(--color-background-dark));\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: center;\n\toverflow: hidden;\n}\n\n.cn-photos-card__tile-cover-img {\n\twidth: 100%;\n\theight: 100%;\n\tobject-fit: cover;\n}\n\n.cn-photos-card__tile-fallback {\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-photos-card__tile-name {\n\tpadding: 4px 6px 6px;\n\tfont-size: 12px;\n\tcolor: var(--color-main-text);\n\twhite-space: nowrap;\n\toverflow: hidden;\n\ttext-overflow: ellipsis;\n}\n\n.cn-photos-card__view-all {\n\tmargin-top: 6px;\n\tfont-size: 0.8em;\n}\n\n/* stylelint-disable-next-line no-descending-specificity */\n.cn-photos-card__view-all a {\n\tcolor: var(--color-primary-element);\n\ttext-decoration: none;\n}\n\n.cn-photos-card__view-all a:hover {\n\ttext-decoration: underline;\n}\n</style>\n"],"names":["CnDetailCard","t","n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"CnPhotosCard.vue2.js","sources":["../../../../../src/integrations/builtin/photos/CnPhotosCard.vue"],"sourcesContent":["<!--\n CnPhotosCard — bespoke surface-aware widget for the `photos`\n integration.\n\n Replaces the generic CnIntegrationCard for the `photos` leaf. Branches\n on `surface` per ADR-019 AD-19:\n - user-dashboard / app-dashboard : count headline \"N albums\" + the\n first album's cover thumbnail + a \"view all\" trail-off.\n - detail-page : a compact 2-column thumbnail grid\n of the linked albums (COMPACT_LIMIT cap) with \"view all\".\n - single-entity : a chip with the cover thumbnail +\n album name for reference-property auto-rendering.\n\n Pulls rows from the same OR pluggable-integration sub-resource as\n CnPhotosTab; for `single-entity` the optional `value` prop addresses a\n single album by id.\n\n See `openregister/openspec/changes/integration-photos/` for the spec\n delta and ADR-019 (registry mechanism), AD-19 (surface fallback).\n-->\n<template>\n\t<CnDetailCard :title=\"cardTitle\" :icon=\"cardIcon\" :collapsible=\"collapsible\">\n\t\t<NcLoadingIcon v-if=\"loading\" />\n\n\t\t<!-- single-entity surface: chip -->\n\t\t<template v-else-if=\"surface === 'single-entity'\">\n\t\t\t<span v-if=\"entity\" class=\"cn-photos-card__chip\" :title=\"albumName(entity)\">\n\t\t\t\t<img\n\t\t\t\t\tv-if=\"coverUrl(entity)\"\n\t\t\t\t\t:src=\"coverUrl(entity)\"\n\t\t\t\t\talt=\"\"\n\t\t\t\t\tclass=\"cn-photos-card__chip-cover\"\n\t\t\t\t\t@error=\"onCoverError(entity)\">\n\t\t\t\t<ImageIcon v-else :size=\"14\" />\n\t\t\t\t<a\n\t\t\t\t\t:href=\"albumUrl(entity)\"\n\t\t\t\t\ttarget=\"_blank\"\n\t\t\t\t\trel=\"noopener noreferrer\">{{ albumName(entity) }}</a>\n\t\t\t</span>\n\t\t\t<span v-else class=\"cn-photos-card__empty\">{{ emptyLabel }}</span>\n\t\t</template>\n\n\t\t<!-- dashboard surfaces: headline + first cover -->\n\t\t<template v-else-if=\"surface === 'user-dashboard' || surface === 'app-dashboard'\">\n\t\t\t<div v-if=\"degraded\" class=\"cn-photos-card__empty\">\n\t\t\t\t{{ degraded }}\n\t\t\t</div>\n\t\t\t<div v-else-if=\"albums.length === 0\" class=\"cn-photos-card__empty\">\n\t\t\t\t{{ emptyLabel }}\n\t\t\t</div>\n\t\t\t<div v-else class=\"cn-photos-card__headline\">\n\t\t\t\t<div class=\"cn-photos-card__headline-line\">\n\t\t\t\t\t<strong>{{ albumCountLabel }}</strong>\n\t\t\t\t\t<span v-if=\"totalPhotoCount !== null\" class=\"cn-photos-card__photo-total\">\n\t\t\t\t\t\t· {{ totalPhotosLabel }}\n\t\t\t\t\t</span>\n\t\t\t\t</div>\n\t\t\t\t<div v-if=\"firstAlbum\" class=\"cn-photos-card__headline-album\">\n\t\t\t\t\t<div class=\"cn-photos-card__headline-cover\">\n\t\t\t\t\t\t<img\n\t\t\t\t\t\t\tv-if=\"coverUrl(firstAlbum)\"\n\t\t\t\t\t\t\t:src=\"coverUrl(firstAlbum)\"\n\t\t\t\t\t\t\talt=\"\"\n\t\t\t\t\t\t\tclass=\"cn-photos-card__headline-cover-img\"\n\t\t\t\t\t\t\t@error=\"onCoverError(firstAlbum)\">\n\t\t\t\t\t\t<ImageMultiple v-else :size=\"18\" />\n\t\t\t\t\t</div>\n\t\t\t\t\t<a\n\t\t\t\t\t\t:href=\"albumUrl(firstAlbum)\"\n\t\t\t\t\t\ttarget=\"_blank\"\n\t\t\t\t\t\trel=\"noopener noreferrer\"\n\t\t\t\t\t\tclass=\"cn-photos-card__headline-name\">\n\t\t\t\t\t\t{{ albumName(firstAlbum) }}\n\t\t\t\t\t</a>\n\t\t\t\t</div>\n\t\t\t\t<div v-if=\"albums.length > 1\" class=\"cn-photos-card__view-all\">\n\t\t\t\t\t<a\n\t\t\t\t\t\t:href=\"photosAppUrl\"\n\t\t\t\t\t\ttarget=\"_blank\"\n\t\t\t\t\t\trel=\"noopener noreferrer\">{{ viewAllLabel }}</a>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</template>\n\n\t\t<!-- detail-page surface: compact grid -->\n\t\t<template v-else>\n\t\t\t<div v-if=\"degraded\" class=\"cn-photos-card__empty\">\n\t\t\t\t{{ degraded }}\n\t\t\t</div>\n\t\t\t<div v-else-if=\"albums.length === 0\" class=\"cn-photos-card__empty\">\n\t\t\t\t{{ emptyLabel }}\n\t\t\t</div>\n\t\t\t<div v-else class=\"cn-photos-card__grid\">\n\t\t\t\t<a\n\t\t\t\t\tv-for=\"album in displayedAlbums\"\n\t\t\t\t\t:key=\"albumKey(album)\"\n\t\t\t\t\t:href=\"albumUrl(album)\"\n\t\t\t\t\tclass=\"cn-photos-card__tile\"\n\t\t\t\t\ttarget=\"_blank\"\n\t\t\t\t\trel=\"noopener noreferrer\"\n\t\t\t\t\t:title=\"albumName(album)\">\n\t\t\t\t\t<div class=\"cn-photos-card__tile-cover\">\n\t\t\t\t\t\t<img\n\t\t\t\t\t\t\tv-if=\"coverUrl(album)\"\n\t\t\t\t\t\t\t:src=\"coverUrl(album)\"\n\t\t\t\t\t\t\talt=\"\"\n\t\t\t\t\t\t\tclass=\"cn-photos-card__tile-cover-img\"\n\t\t\t\t\t\t\tloading=\"lazy\"\n\t\t\t\t\t\t\t@error=\"onCoverError(album)\">\n\t\t\t\t\t\t<ImageMultiple v-else :size=\"20\" class=\"cn-photos-card__tile-fallback\" />\n\t\t\t\t\t</div>\n\t\t\t\t\t<span class=\"cn-photos-card__tile-name\">{{ albumName(album) }}</span>\n\t\t\t\t</a>\n\t\t\t</div>\n\t\t\t<div v-if=\"albums.length > COMPACT_LIMIT\" class=\"cn-photos-card__view-all\">\n\t\t\t\t<a\n\t\t\t\t\t:href=\"photosAppUrl\"\n\t\t\t\t\ttarget=\"_blank\"\n\t\t\t\t\trel=\"noopener noreferrer\">{{ viewAllLabel }}</a>\n\t\t\t</div>\n\t\t</template>\n\t</CnDetailCard>\n</template>\n\n<script>\nimport { translatePlural as n, translate as t } from '@nextcloud/l10n'\nimport { NcLoadingIcon } from '@nextcloud/vue'\nimport ImageIcon from 'vue-material-design-icons/Image.vue'\nimport ImageMultiple from 'vue-material-design-icons/ImageMultiple.vue'\nimport CnDetailCard from '../../../components/CnDetailCard/CnDetailCard.vue'\nimport { buildHeaders, prefixUrl, safeHref } from '../../../utils/index.js'\n\nconst VALID_SURFACES = ['user-dashboard', 'app-dashboard', 'detail-page', 'single-entity']\nconst COMPACT_LIMIT = 4\n\n/**\n * CnPhotosCard — bespoke surface-aware widget for the `photos`\n * integration.\n *\n * Renders Photos-aware metadata (album count, cover thumbnail, total\n * photo count) across all four AD-19 surfaces. See the file-level\n * docblock for surface-by-surface behaviour.\n */\nexport default {\n\tname: 'CnPhotosCard',\n\n\tcomponents: { CnDetailCard, NcLoadingIcon, ImageIcon, ImageMultiple },\n\n\tprops: {\n\t\t/** Stable integration id (forwarded from the registry — always `'photos'`). */\n\t\tintegrationId: { type: String, default: 'photos' },\n\t\t/** OpenRegister register id (slug or uuid). */\n\t\tregister: { type: String, required: true },\n\t\t/** OpenRegister schema id (slug or uuid). */\n\t\tschema: { type: String, required: true },\n\t\t/** Parent object id. */\n\t\tobjectId: { type: String, required: true },\n\t\t/** Rendering surface (AD-19). */\n\t\tsurface: {\n\t\t\ttype: String,\n\t\t\tdefault: 'detail-page',\n\t\t\tvalidator: (s) => VALID_SURFACES.includes(s),\n\t\t},\n\n\t\t/** Optional single-entity reference (album id). */\n\t\tvalue: { type: [String, Number], default: '' },\n\t\t/** Pre-translated card title. */\n\t\ttitle: { type: String, default: () => t('nextcloud-vue', 'Photos') },\n\t\t/** Optional Material Design Icon component. */\n\t\ticon: { type: Object, default: () => ImageIcon },\n\t\t/** Base API URL. */\n\t\tapiBase: { type: String, default: '/apps/openregister/api' },\n\t\t/** Whether the card body is collapsible. */\n\t\tcollapsible: { type: Boolean, default: true },\n\t\t/** Pre-translated empty-state label. */\n\t\temptyLabel: { type: String, default: () => t('nextcloud-vue', 'No albums linked yet') },\n\t\t/** Pre-translated 503 unavailable label. */\n\t\tunavailableLabel: { type: String, default: () => t('nextcloud-vue', 'NC Photos is currently unavailable.') },\n\t\t/** Pre-translated \"view all\" trail-off link label. */\n\t\tviewAllLabel: { type: String, default: () => t('nextcloud-vue', 'View all in Photos') },\n\t\t/** URL of the NC Photos app entry. */\n\t\tphotosAppUrl: { type: String, default: '/index.php/apps/photos' },\n\t},\n\n\tdata() {\n\t\treturn {\n\t\t\tCOMPACT_LIMIT,\n\t\t\talbums: [],\n\t\t\tentity: null,\n\t\t\tloading: false,\n\t\t\tdegraded: '',\n\t\t\tbrokenCovers: {},\n\t\t}\n\t},\n\n\tcomputed: {\n\t\tcardTitle() {\n\t\t\treturn this.title || this.integrationId\n\t\t},\n\n\t\tcardIcon() {\n\t\t\treturn this.icon\n\t\t},\n\n\t\tdisplayedAlbums() {\n\t\t\treturn this.albums.slice(0, COMPACT_LIMIT)\n\t\t},\n\n\t\talbumCountLabel() {\n\t\t\tconst total = this.albums.length\n\t\t\treturn n('nextcloud-vue', '{count} album', '{count} albums', total, { count: total })\n\t\t},\n\n\t\ttotalPhotoCount() {\n\t\t\tlet total = 0\n\t\t\tlet any = false\n\t\t\tfor (const a of this.albums) {\n\t\t\t\tconst c = this.photoCount(a)\n\t\t\t\tif (c !== null) {\n\t\t\t\t\ttotal += c\n\t\t\t\t\tany = true\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn any ? total : null\n\t\t},\n\n\t\ttotalPhotosLabel() {\n\t\t\tconst count = this.totalPhotoCount ?? 0\n\t\t\treturn n('nextcloud-vue', '{count} photo', '{count} photos', count, { count })\n\t\t},\n\n\t\tfirstAlbum() {\n\t\t\treturn this.albums.length > 0 ? this.albums[0] : null\n\t\t},\n\t},\n\n\twatch: {\n\t\tobjectId: { immediate: true, handler() {\n\t\t\tthis.fetch()\n\t\t} },\n\n\t\tsurface() {\n\t\t\tthis.fetch()\n\t\t},\n\n\t\tvalue() {\n\t\t\tif (this.surface === 'single-entity') {\n\t\t\t\tthis.fetchSingle()\n\t\t\t}\n\t\t},\n\t},\n\n\tmethods: {\n\t\tbaseUrl() {\n\t\t\treturn `${this.apiBase}/objects/${this.register}/${this.schema}/${this.objectId}/integrations/${this.integrationId}`\n\t\t},\n\n\t\talbumKey(album) {\n\t\t\treturn (album && (album.id ?? album.data?.album_id)) || ''\n\t\t},\n\n\t\talbumName(album) {\n\t\t\tconst raw = (album && (album.title || album.name || album.data?.name)) || ''\n\t\t\treturn String(raw).replace(/\\s*\\[or:[^\\]]+\\]\\s*/g, ' ').trim() || t('nextcloud-vue', 'Untitled album')\n\t\t},\n\n\t\talbumUrl(album) {\n\t\t\tif (album && album.url && safeHref(album.url) !== '#') {\n\t\t\t\treturn album.url\n\t\t\t}\n\t\t\tconst name = this.albumName(album)\n\t\t\treturn `/index.php/apps/photos/albums/${encodeURIComponent(name)}`\n\t\t},\n\n\t\tcoverUrl(album) {\n\t\t\tif (!album || this.brokenCovers[this.albumKey(album)]) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn album.coverPhotoUrl || album.data?.coverPhotoUrl || ''\n\t\t},\n\n\t\tonCoverError(album) {\n\t\t\tthis.brokenCovers[this.albumKey(album)] = true\n\t\t},\n\n\t\tphotoCount(album) {\n\t\t\tconst raw = album?.photoCount ?? album?.data?.photoCount ?? album?.data?.count\n\t\t\tif (raw === null || raw === undefined || raw === '') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst num = Number(raw)\n\t\t\treturn Number.isFinite(num) ? num : null\n\t\t},\n\n\t\tfetch() {\n\t\t\tif (this.surface === 'single-entity') {\n\t\t\t\tthis.fetchSingle()\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.fetchList()\n\t\t},\n\n\t\tasync fetchList() {\n\t\t\tif (!this.register || !this.schema || !this.objectId) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.loading = true\n\t\t\tthis.degraded = ''\n\t\t\ttry {\n\t\t\t\tconst response = await fetch(prefixUrl(this.baseUrl()), { headers: buildHeaders() })\n\t\t\t\tif (response.ok) {\n\t\t\t\t\tconst data = await response.json()\n\t\t\t\t\tthis.albums = data.results || data.items || (Array.isArray(data) ? data : []) || []\n\t\t\t\t} else if (response.status === 503) {\n\t\t\t\t\tthis.albums = []\n\t\t\t\t\tthis.degraded = this.unavailableLabel\n\t\t\t\t} else {\n\t\t\t\t\tthis.albums = []\n\t\t\t\t}\n\t\t\t} catch (err) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.error('[CnPhotosCard] failed to fetch albums', err)\n\t\t\t\tthis.albums = []\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\t\t},\n\n\t\tasync fetchSingle() {\n\t\t\tif (this.value === '' || this.value === undefined || this.value === null || !this.register || !this.schema || !this.objectId) {\n\t\t\t\tthis.entity = null\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.loading = true\n\t\t\tthis.degraded = ''\n\t\t\ttry {\n\t\t\t\tconst response = await fetch(prefixUrl(`${this.baseUrl()}/${encodeURIComponent(this.value)}`), { headers: buildHeaders() })\n\t\t\t\tif (response.ok) {\n\t\t\t\t\tthis.entity = await response.json()\n\t\t\t\t} else if (response.status === 503) {\n\t\t\t\t\tthis.entity = null\n\t\t\t\t\tthis.degraded = this.unavailableLabel\n\t\t\t\t} else {\n\t\t\t\t\tthis.entity = null\n\t\t\t\t}\n\t\t\t} catch (err) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.error('[CnPhotosCard] failed to fetch single album', err)\n\t\t\t\tthis.entity = null\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-photos-card__empty {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.9em;\n\tpadding: 8px 0;\n}\n\n.cn-photos-card__headline {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 6px;\n}\n\n.cn-photos-card__headline-line {\n\tfont-size: 1.1em;\n\tcolor: var(--color-main-text);\n}\n\n.cn-photos-card__photo-total {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.9em;\n\tfont-weight: 400;\n}\n\n.cn-photos-card__headline-album {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 8px;\n\toverflow: hidden;\n}\n\n.cn-photos-card__headline-cover {\n\twidth: 32px;\n\theight: 32px;\n\tborder-radius: var(--border-radius);\n\toverflow: hidden;\n\tflex-shrink: 0;\n\tbackground: var(--color-background-darker, var(--color-background-dark));\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: center;\n}\n\n.cn-photos-card__headline-cover-img {\n\twidth: 100%;\n\theight: 100%;\n\tobject-fit: cover;\n}\n\n.cn-photos-card__headline-name {\n\tcolor: var(--color-main-text);\n\ttext-decoration: none;\n\toverflow: hidden;\n\ttext-overflow: ellipsis;\n\twhite-space: nowrap;\n\tfont-size: 0.95em;\n}\n\n.cn-photos-card__headline-name:hover {\n\ttext-decoration: underline;\n}\n\n.cn-photos-card__chip {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 6px;\n\tpadding: 4px 10px 4px 4px;\n\tborder-radius: 14px;\n\tbackground: var(--color-background-hover);\n\tfont-size: 0.9em;\n\tmax-width: 100%;\n}\n\n.cn-photos-card__chip-cover {\n\twidth: 20px;\n\theight: 20px;\n\tborder-radius: 50%;\n\tobject-fit: cover;\n\tflex-shrink: 0;\n}\n\n.cn-photos-card__chip a {\n\tcolor: var(--color-main-text);\n\ttext-decoration: none;\n\toverflow: hidden;\n\ttext-overflow: ellipsis;\n\twhite-space: nowrap;\n}\n\n.cn-photos-card__chip a:hover {\n\ttext-decoration: underline;\n}\n\n.cn-photos-card__grid {\n\tdisplay: grid;\n\tgrid-template-columns: repeat(2, 1fr);\n\tgap: 8px;\n}\n\n.cn-photos-card__tile {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 4px;\n\ttext-decoration: none;\n\tcolor: inherit;\n\tborder-radius: var(--border-radius);\n\toverflow: hidden;\n\tbackground: var(--color-background-hover);\n}\n\n.cn-photos-card__tile:hover,\n.cn-photos-card__tile:focus-within {\n\tbackground: var(--color-background-dark, var(--color-background-hover));\n}\n\n.cn-photos-card__tile-cover {\n\twidth: 100%;\n\taspect-ratio: 1 / 1;\n\tbackground: var(--color-background-darker, var(--color-background-dark));\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: center;\n\toverflow: hidden;\n}\n\n.cn-photos-card__tile-cover-img {\n\twidth: 100%;\n\theight: 100%;\n\tobject-fit: cover;\n}\n\n.cn-photos-card__tile-fallback {\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-photos-card__tile-name {\n\tpadding: 4px 6px 6px;\n\tfont-size: 12px;\n\tcolor: var(--color-main-text);\n\twhite-space: nowrap;\n\toverflow: hidden;\n\ttext-overflow: ellipsis;\n}\n\n.cn-photos-card__view-all {\n\tmargin-top: 6px;\n\tfont-size: 0.8em;\n}\n\n/* stylelint-disable-next-line no-descending-specificity */\n.cn-photos-card__view-all a {\n\tcolor: var(--color-primary-element);\n\ttext-decoration: none;\n}\n\n.cn-photos-card__view-all a:hover {\n\ttext-decoration: underline;\n}\n</style>\n"],"names":["CnDetailCard","t","n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CnPhotosCard.vue3.js","sources":["../../../../../src/integrations/builtin/photos/CnPhotosCard.vue?vue&type=template&id=3ab3b432&lang.js"],"sourcesContent":["<!--\n CnPhotosCard — bespoke surface-aware widget for the `photos`\n integration.\n\n Replaces the generic CnIntegrationCard for the `photos` leaf. Branches\n on `surface` per ADR-019 AD-19:\n - user-dashboard / app-dashboard : count headline \"N albums\" + the\n first album's cover thumbnail + a \"view all\" trail-off.\n - detail-page : a compact 2-column thumbnail grid\n of the linked albums (COMPACT_LIMIT cap) with \"view all\".\n - single-entity : a chip with the cover thumbnail +\n album name for reference-property auto-rendering.\n\n Pulls rows from the same OR pluggable-integration sub-resource as\n CnPhotosTab; for `single-entity` the optional `value` prop addresses a\n single album by id.\n\n See `openregister/openspec/changes/integration-photos/` for the spec\n delta and ADR-019 (registry mechanism), AD-19 (surface fallback).\n-->\n<template>\n\t<CnDetailCard :title=\"cardTitle\" :icon=\"cardIcon\" :collapsible=\"collapsible\">\n\t\t<NcLoadingIcon v-if=\"loading\" />\n\n\t\t<!-- single-entity surface: chip -->\n\t\t<template v-else-if=\"surface === 'single-entity'\">\n\t\t\t<span v-if=\"entity\" class=\"cn-photos-card__chip\" :title=\"albumName(entity)\">\n\t\t\t\t<img\n\t\t\t\t\tv-if=\"coverUrl(entity)\"\n\t\t\t\t\t:src=\"coverUrl(entity)\"\n\t\t\t\t\talt=\"\"\n\t\t\t\t\tclass=\"cn-photos-card__chip-cover\"\n\t\t\t\t\t@error=\"onCoverError(entity)\">\n\t\t\t\t<ImageIcon v-else :size=\"14\" />\n\t\t\t\t<a\n\t\t\t\t\t:href=\"albumUrl(entity)\"\n\t\t\t\t\ttarget=\"_blank\"\n\t\t\t\t\trel=\"noopener noreferrer\">{{ albumName(entity) }}</a>\n\t\t\t</span>\n\t\t\t<span v-else class=\"cn-photos-card__empty\">{{ emptyLabel }}</span>\n\t\t</template>\n\n\t\t<!-- dashboard surfaces: headline + first cover -->\n\t\t<template v-else-if=\"surface === 'user-dashboard' || surface === 'app-dashboard'\">\n\t\t\t<div v-if=\"degraded\" class=\"cn-photos-card__empty\">\n\t\t\t\t{{ degraded }}\n\t\t\t</div>\n\t\t\t<div v-else-if=\"albums.length === 0\" class=\"cn-photos-card__empty\">\n\t\t\t\t{{ emptyLabel }}\n\t\t\t</div>\n\t\t\t<div v-else class=\"cn-photos-card__headline\">\n\t\t\t\t<div class=\"cn-photos-card__headline-line\">\n\t\t\t\t\t<strong>{{ albumCountLabel }}</strong>\n\t\t\t\t\t<span v-if=\"totalPhotoCount !== null\" class=\"cn-photos-card__photo-total\">\n\t\t\t\t\t\t· {{ totalPhotosLabel }}\n\t\t\t\t\t</span>\n\t\t\t\t</div>\n\t\t\t\t<div v-if=\"firstAlbum\" class=\"cn-photos-card__headline-album\">\n\t\t\t\t\t<div class=\"cn-photos-card__headline-cover\">\n\t\t\t\t\t\t<img\n\t\t\t\t\t\t\tv-if=\"coverUrl(firstAlbum)\"\n\t\t\t\t\t\t\t:src=\"coverUrl(firstAlbum)\"\n\t\t\t\t\t\t\talt=\"\"\n\t\t\t\t\t\t\tclass=\"cn-photos-card__headline-cover-img\"\n\t\t\t\t\t\t\t@error=\"onCoverError(firstAlbum)\">\n\t\t\t\t\t\t<ImageMultiple v-else :size=\"18\" />\n\t\t\t\t\t</div>\n\t\t\t\t\t<a\n\t\t\t\t\t\t:href=\"albumUrl(firstAlbum)\"\n\t\t\t\t\t\ttarget=\"_blank\"\n\t\t\t\t\t\trel=\"noopener noreferrer\"\n\t\t\t\t\t\tclass=\"cn-photos-card__headline-name\">\n\t\t\t\t\t\t{{ albumName(firstAlbum) }}\n\t\t\t\t\t</a>\n\t\t\t\t</div>\n\t\t\t\t<div v-if=\"albums.length > 1\" class=\"cn-photos-card__view-all\">\n\t\t\t\t\t<a\n\t\t\t\t\t\t:href=\"photosAppUrl\"\n\t\t\t\t\t\ttarget=\"_blank\"\n\t\t\t\t\t\trel=\"noopener noreferrer\">{{ viewAllLabel }}</a>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</template>\n\n\t\t<!-- detail-page surface: compact grid -->\n\t\t<template v-else>\n\t\t\t<div v-if=\"degraded\" class=\"cn-photos-card__empty\">\n\t\t\t\t{{ degraded }}\n\t\t\t</div>\n\t\t\t<div v-else-if=\"albums.length === 0\" class=\"cn-photos-card__empty\">\n\t\t\t\t{{ emptyLabel }}\n\t\t\t</div>\n\t\t\t<div v-else class=\"cn-photos-card__grid\">\n\t\t\t\t<a\n\t\t\t\t\tv-for=\"album in displayedAlbums\"\n\t\t\t\t\t:key=\"albumKey(album)\"\n\t\t\t\t\t:href=\"albumUrl(album)\"\n\t\t\t\t\tclass=\"cn-photos-card__tile\"\n\t\t\t\t\ttarget=\"_blank\"\n\t\t\t\t\trel=\"noopener noreferrer\"\n\t\t\t\t\t:title=\"albumName(album)\">\n\t\t\t\t\t<div class=\"cn-photos-card__tile-cover\">\n\t\t\t\t\t\t<img\n\t\t\t\t\t\t\tv-if=\"coverUrl(album)\"\n\t\t\t\t\t\t\t:src=\"coverUrl(album)\"\n\t\t\t\t\t\t\talt=\"\"\n\t\t\t\t\t\t\tclass=\"cn-photos-card__tile-cover-img\"\n\t\t\t\t\t\t\tloading=\"lazy\"\n\t\t\t\t\t\t\t@error=\"onCoverError(album)\">\n\t\t\t\t\t\t<ImageMultiple v-else :size=\"20\" class=\"cn-photos-card__tile-fallback\" />\n\t\t\t\t\t</div>\n\t\t\t\t\t<span class=\"cn-photos-card__tile-name\">{{ albumName(album) }}</span>\n\t\t\t\t</a>\n\t\t\t</div>\n\t\t\t<div v-if=\"albums.length > COMPACT_LIMIT\" class=\"cn-photos-card__view-all\">\n\t\t\t\t<a\n\t\t\t\t\t:href=\"photosAppUrl\"\n\t\t\t\t\ttarget=\"_blank\"\n\t\t\t\t\trel=\"noopener noreferrer\">{{ viewAllLabel }}</a>\n\t\t\t</div>\n\t\t</template>\n\t</CnDetailCard>\n</template>\n\n<script>\nimport { translatePlural as n, translate as t } from '@nextcloud/l10n'\nimport { NcLoadingIcon } from '@nextcloud/vue'\nimport ImageIcon from 'vue-material-design-icons/Image.vue'\nimport ImageMultiple from 'vue-material-design-icons/ImageMultiple.vue'\nimport CnDetailCard from '../../../components/CnDetailCard/CnDetailCard.vue'\nimport { buildHeaders, prefixUrl } from '../../../utils/index.js'\n\nconst VALID_SURFACES = ['user-dashboard', 'app-dashboard', 'detail-page', 'single-entity']\nconst COMPACT_LIMIT = 4\n\n/**\n * CnPhotosCard — bespoke surface-aware widget for the `photos`\n * integration.\n *\n * Renders Photos-aware metadata (album count, cover thumbnail, total\n * photo count) across all four AD-19 surfaces. See the file-level\n * docblock for surface-by-surface behaviour.\n */\nexport default {\n\tname: 'CnPhotosCard',\n\n\tcomponents: { CnDetailCard, NcLoadingIcon, ImageIcon, ImageMultiple },\n\n\tprops: {\n\t\t/** Stable integration id (forwarded from the registry — always `'photos'`). */\n\t\tintegrationId: { type: String, default: 'photos' },\n\t\t/** OpenRegister register id (slug or uuid). */\n\t\tregister: { type: String, required: true },\n\t\t/** OpenRegister schema id (slug or uuid). */\n\t\tschema: { type: String, required: true },\n\t\t/** Parent object id. */\n\t\tobjectId: { type: String, required: true },\n\t\t/** Rendering surface (AD-19). */\n\t\tsurface: {\n\t\t\ttype: String,\n\t\t\tdefault: 'detail-page',\n\t\t\tvalidator: (s) => VALID_SURFACES.includes(s),\n\t\t},\n\n\t\t/** Optional single-entity reference (album id). */\n\t\tvalue: { type: [String, Number], default: '' },\n\t\t/** Pre-translated card title. */\n\t\ttitle: { type: String, default: () => t('nextcloud-vue', 'Photos') },\n\t\t/** Optional Material Design Icon component. */\n\t\ticon: { type: Object, default: () => ImageIcon },\n\t\t/** Base API URL. */\n\t\tapiBase: { type: String, default: '/apps/openregister/api' },\n\t\t/** Whether the card body is collapsible. */\n\t\tcollapsible: { type: Boolean, default: true },\n\t\t/** Pre-translated empty-state label. */\n\t\temptyLabel: { type: String, default: () => t('nextcloud-vue', 'No albums linked yet') },\n\t\t/** Pre-translated 503 unavailable label. */\n\t\tunavailableLabel: { type: String, default: () => t('nextcloud-vue', 'NC Photos is currently unavailable.') },\n\t\t/** Pre-translated \"view all\" trail-off link label. */\n\t\tviewAllLabel: { type: String, default: () => t('nextcloud-vue', 'View all in Photos') },\n\t\t/** URL of the NC Photos app entry. */\n\t\tphotosAppUrl: { type: String, default: '/index.php/apps/photos' },\n\t},\n\n\tdata() {\n\t\treturn {\n\t\t\tCOMPACT_LIMIT,\n\t\t\talbums: [],\n\t\t\tentity: null,\n\t\t\tloading: false,\n\t\t\tdegraded: '',\n\t\t\tbrokenCovers: {},\n\t\t}\n\t},\n\n\tcomputed: {\n\t\tcardTitle() {\n\t\t\treturn this.title || this.integrationId\n\t\t},\n\n\t\tcardIcon() {\n\t\t\treturn this.icon\n\t\t},\n\n\t\tdisplayedAlbums() {\n\t\t\treturn this.albums.slice(0, COMPACT_LIMIT)\n\t\t},\n\n\t\talbumCountLabel() {\n\t\t\tconst total = this.albums.length\n\t\t\treturn n('nextcloud-vue', '{count} album', '{count} albums', total, { count: total })\n\t\t},\n\n\t\ttotalPhotoCount() {\n\t\t\tlet total = 0\n\t\t\tlet any = false\n\t\t\tfor (const a of this.albums) {\n\t\t\t\tconst c = this.photoCount(a)\n\t\t\t\tif (c !== null) {\n\t\t\t\t\ttotal += c\n\t\t\t\t\tany = true\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn any ? total : null\n\t\t},\n\n\t\ttotalPhotosLabel() {\n\t\t\tconst count = this.totalPhotoCount ?? 0\n\t\t\treturn n('nextcloud-vue', '{count} photo', '{count} photos', count, { count })\n\t\t},\n\n\t\tfirstAlbum() {\n\t\t\treturn this.albums.length > 0 ? this.albums[0] : null\n\t\t},\n\t},\n\n\twatch: {\n\t\tobjectId: { immediate: true, handler() {\n\t\t\tthis.fetch()\n\t\t} },\n\n\t\tsurface() {\n\t\t\tthis.fetch()\n\t\t},\n\n\t\tvalue() {\n\t\t\tif (this.surface === 'single-entity') {\n\t\t\t\tthis.fetchSingle()\n\t\t\t}\n\t\t},\n\t},\n\n\tmethods: {\n\t\tbaseUrl() {\n\t\t\treturn `${this.apiBase}/objects/${this.register}/${this.schema}/${this.objectId}/integrations/${this.integrationId}`\n\t\t},\n\n\t\talbumKey(album) {\n\t\t\treturn (album && (album.id ?? album.data?.album_id)) || ''\n\t\t},\n\n\t\talbumName(album) {\n\t\t\tconst raw = (album && (album.title || album.name || album.data?.name)) || ''\n\t\t\treturn String(raw).replace(/\\s*\\[or:[^\\]]+\\]\\s*/g, ' ').trim() || t('nextcloud-vue', 'Untitled album')\n\t\t},\n\n\t\talbumUrl(album) {\n\t\t\tif (album && album.url) {\n\t\t\t\treturn album.url\n\t\t\t}\n\t\t\tconst name = this.albumName(album)\n\t\t\treturn `/index.php/apps/photos/albums/${encodeURIComponent(name)}`\n\t\t},\n\n\t\tcoverUrl(album) {\n\t\t\tif (!album || this.brokenCovers[this.albumKey(album)]) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn album.coverPhotoUrl || album.data?.coverPhotoUrl || ''\n\t\t},\n\n\t\tonCoverError(album) {\n\t\t\tthis.brokenCovers[this.albumKey(album)] = true\n\t\t},\n\n\t\tphotoCount(album) {\n\t\t\tconst raw = album?.photoCount ?? album?.data?.photoCount ?? album?.data?.count\n\t\t\tif (raw === null || raw === undefined || raw === '') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst num = Number(raw)\n\t\t\treturn Number.isFinite(num) ? num : null\n\t\t},\n\n\t\tfetch() {\n\t\t\tif (this.surface === 'single-entity') {\n\t\t\t\tthis.fetchSingle()\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.fetchList()\n\t\t},\n\n\t\tasync fetchList() {\n\t\t\tif (!this.register || !this.schema || !this.objectId) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.loading = true\n\t\t\tthis.degraded = ''\n\t\t\ttry {\n\t\t\t\tconst response = await fetch(prefixUrl(this.baseUrl()), { headers: buildHeaders() })\n\t\t\t\tif (response.ok) {\n\t\t\t\t\tconst data = await response.json()\n\t\t\t\t\tthis.albums = data.results || data.items || (Array.isArray(data) ? data : []) || []\n\t\t\t\t} else if (response.status === 503) {\n\t\t\t\t\tthis.albums = []\n\t\t\t\t\tthis.degraded = this.unavailableLabel\n\t\t\t\t} else {\n\t\t\t\t\tthis.albums = []\n\t\t\t\t}\n\t\t\t} catch (err) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.error('[CnPhotosCard] failed to fetch albums', err)\n\t\t\t\tthis.albums = []\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\t\t},\n\n\t\tasync fetchSingle() {\n\t\t\tif (this.value === '' || this.value === undefined || this.value === null || !this.register || !this.schema || !this.objectId) {\n\t\t\t\tthis.entity = null\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.loading = true\n\t\t\tthis.degraded = ''\n\t\t\ttry {\n\t\t\t\tconst response = await fetch(prefixUrl(`${this.baseUrl()}/${encodeURIComponent(this.value)}`), { headers: buildHeaders() })\n\t\t\t\tif (response.ok) {\n\t\t\t\t\tthis.entity = await response.json()\n\t\t\t\t} else if (response.status === 503) {\n\t\t\t\t\tthis.entity = null\n\t\t\t\t\tthis.degraded = this.unavailableLabel\n\t\t\t\t} else {\n\t\t\t\t\tthis.entity = null\n\t\t\t\t}\n\t\t\t} catch (err) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.error('[CnPhotosCard] failed to fetch single album', err)\n\t\t\t\tthis.entity = null\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-photos-card__empty {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.9em;\n\tpadding: 8px 0;\n}\n\n.cn-photos-card__headline {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 6px;\n}\n\n.cn-photos-card__headline-line {\n\tfont-size: 1.1em;\n\tcolor: var(--color-main-text);\n}\n\n.cn-photos-card__photo-total {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.9em;\n\tfont-weight: 400;\n}\n\n.cn-photos-card__headline-album {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 8px;\n\toverflow: hidden;\n}\n\n.cn-photos-card__headline-cover {\n\twidth: 32px;\n\theight: 32px;\n\tborder-radius: var(--border-radius);\n\toverflow: hidden;\n\tflex-shrink: 0;\n\tbackground: var(--color-background-darker, var(--color-background-dark));\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: center;\n}\n\n.cn-photos-card__headline-cover-img {\n\twidth: 100%;\n\theight: 100%;\n\tobject-fit: cover;\n}\n\n.cn-photos-card__headline-name {\n\tcolor: var(--color-main-text);\n\ttext-decoration: none;\n\toverflow: hidden;\n\ttext-overflow: ellipsis;\n\twhite-space: nowrap;\n\tfont-size: 0.95em;\n}\n\n.cn-photos-card__headline-name:hover {\n\ttext-decoration: underline;\n}\n\n.cn-photos-card__chip {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 6px;\n\tpadding: 4px 10px 4px 4px;\n\tborder-radius: 14px;\n\tbackground: var(--color-background-hover);\n\tfont-size: 0.9em;\n\tmax-width: 100%;\n}\n\n.cn-photos-card__chip-cover {\n\twidth: 20px;\n\theight: 20px;\n\tborder-radius: 50%;\n\tobject-fit: cover;\n\tflex-shrink: 0;\n}\n\n.cn-photos-card__chip a {\n\tcolor: var(--color-main-text);\n\ttext-decoration: none;\n\toverflow: hidden;\n\ttext-overflow: ellipsis;\n\twhite-space: nowrap;\n}\n\n.cn-photos-card__chip a:hover {\n\ttext-decoration: underline;\n}\n\n.cn-photos-card__grid {\n\tdisplay: grid;\n\tgrid-template-columns: repeat(2, 1fr);\n\tgap: 8px;\n}\n\n.cn-photos-card__tile {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 4px;\n\ttext-decoration: none;\n\tcolor: inherit;\n\tborder-radius: var(--border-radius);\n\toverflow: hidden;\n\tbackground: var(--color-background-hover);\n}\n\n.cn-photos-card__tile:hover,\n.cn-photos-card__tile:focus-within {\n\tbackground: var(--color-background-dark, var(--color-background-hover));\n}\n\n.cn-photos-card__tile-cover {\n\twidth: 100%;\n\taspect-ratio: 1 / 1;\n\tbackground: var(--color-background-darker, var(--color-background-dark));\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: center;\n\toverflow: hidden;\n}\n\n.cn-photos-card__tile-cover-img {\n\twidth: 100%;\n\theight: 100%;\n\tobject-fit: cover;\n}\n\n.cn-photos-card__tile-fallback {\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-photos-card__tile-name {\n\tpadding: 4px 6px 6px;\n\tfont-size: 12px;\n\tcolor: var(--color-main-text);\n\twhite-space: nowrap;\n\toverflow: hidden;\n\ttext-overflow: ellipsis;\n}\n\n.cn-photos-card__view-all {\n\tmargin-top: 6px;\n\tfont-size: 0.8em;\n}\n\n/* stylelint-disable-next-line no-descending-specificity */\n.cn-photos-card__view-all a {\n\tcolor: var(--color-primary-element);\n\ttext-decoration: none;\n}\n\n.cn-photos-card__view-all a:hover {\n\ttext-decoration: underline;\n}\n</style>\n"],"names":["_createBlock","_Fragment","_createElementBlock","_createElementVNode","_toDisplayString","_openBlock"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"CnPhotosCard.vue3.js","sources":["../../../../../src/integrations/builtin/photos/CnPhotosCard.vue?vue&type=template&id=3ab3b432&lang.js"],"sourcesContent":["<!--\n CnPhotosCard — bespoke surface-aware widget for the `photos`\n integration.\n\n Replaces the generic CnIntegrationCard for the `photos` leaf. Branches\n on `surface` per ADR-019 AD-19:\n - user-dashboard / app-dashboard : count headline \"N albums\" + the\n first album's cover thumbnail + a \"view all\" trail-off.\n - detail-page : a compact 2-column thumbnail grid\n of the linked albums (COMPACT_LIMIT cap) with \"view all\".\n - single-entity : a chip with the cover thumbnail +\n album name for reference-property auto-rendering.\n\n Pulls rows from the same OR pluggable-integration sub-resource as\n CnPhotosTab; for `single-entity` the optional `value` prop addresses a\n single album by id.\n\n See `openregister/openspec/changes/integration-photos/` for the spec\n delta and ADR-019 (registry mechanism), AD-19 (surface fallback).\n-->\n<template>\n\t<CnDetailCard :title=\"cardTitle\" :icon=\"cardIcon\" :collapsible=\"collapsible\">\n\t\t<NcLoadingIcon v-if=\"loading\" />\n\n\t\t<!-- single-entity surface: chip -->\n\t\t<template v-else-if=\"surface === 'single-entity'\">\n\t\t\t<span v-if=\"entity\" class=\"cn-photos-card__chip\" :title=\"albumName(entity)\">\n\t\t\t\t<img\n\t\t\t\t\tv-if=\"coverUrl(entity)\"\n\t\t\t\t\t:src=\"coverUrl(entity)\"\n\t\t\t\t\talt=\"\"\n\t\t\t\t\tclass=\"cn-photos-card__chip-cover\"\n\t\t\t\t\t@error=\"onCoverError(entity)\">\n\t\t\t\t<ImageIcon v-else :size=\"14\" />\n\t\t\t\t<a\n\t\t\t\t\t:href=\"albumUrl(entity)\"\n\t\t\t\t\ttarget=\"_blank\"\n\t\t\t\t\trel=\"noopener noreferrer\">{{ albumName(entity) }}</a>\n\t\t\t</span>\n\t\t\t<span v-else class=\"cn-photos-card__empty\">{{ emptyLabel }}</span>\n\t\t</template>\n\n\t\t<!-- dashboard surfaces: headline + first cover -->\n\t\t<template v-else-if=\"surface === 'user-dashboard' || surface === 'app-dashboard'\">\n\t\t\t<div v-if=\"degraded\" class=\"cn-photos-card__empty\">\n\t\t\t\t{{ degraded }}\n\t\t\t</div>\n\t\t\t<div v-else-if=\"albums.length === 0\" class=\"cn-photos-card__empty\">\n\t\t\t\t{{ emptyLabel }}\n\t\t\t</div>\n\t\t\t<div v-else class=\"cn-photos-card__headline\">\n\t\t\t\t<div class=\"cn-photos-card__headline-line\">\n\t\t\t\t\t<strong>{{ albumCountLabel }}</strong>\n\t\t\t\t\t<span v-if=\"totalPhotoCount !== null\" class=\"cn-photos-card__photo-total\">\n\t\t\t\t\t\t· {{ totalPhotosLabel }}\n\t\t\t\t\t</span>\n\t\t\t\t</div>\n\t\t\t\t<div v-if=\"firstAlbum\" class=\"cn-photos-card__headline-album\">\n\t\t\t\t\t<div class=\"cn-photos-card__headline-cover\">\n\t\t\t\t\t\t<img\n\t\t\t\t\t\t\tv-if=\"coverUrl(firstAlbum)\"\n\t\t\t\t\t\t\t:src=\"coverUrl(firstAlbum)\"\n\t\t\t\t\t\t\talt=\"\"\n\t\t\t\t\t\t\tclass=\"cn-photos-card__headline-cover-img\"\n\t\t\t\t\t\t\t@error=\"onCoverError(firstAlbum)\">\n\t\t\t\t\t\t<ImageMultiple v-else :size=\"18\" />\n\t\t\t\t\t</div>\n\t\t\t\t\t<a\n\t\t\t\t\t\t:href=\"albumUrl(firstAlbum)\"\n\t\t\t\t\t\ttarget=\"_blank\"\n\t\t\t\t\t\trel=\"noopener noreferrer\"\n\t\t\t\t\t\tclass=\"cn-photos-card__headline-name\">\n\t\t\t\t\t\t{{ albumName(firstAlbum) }}\n\t\t\t\t\t</a>\n\t\t\t\t</div>\n\t\t\t\t<div v-if=\"albums.length > 1\" class=\"cn-photos-card__view-all\">\n\t\t\t\t\t<a\n\t\t\t\t\t\t:href=\"photosAppUrl\"\n\t\t\t\t\t\ttarget=\"_blank\"\n\t\t\t\t\t\trel=\"noopener noreferrer\">{{ viewAllLabel }}</a>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</template>\n\n\t\t<!-- detail-page surface: compact grid -->\n\t\t<template v-else>\n\t\t\t<div v-if=\"degraded\" class=\"cn-photos-card__empty\">\n\t\t\t\t{{ degraded }}\n\t\t\t</div>\n\t\t\t<div v-else-if=\"albums.length === 0\" class=\"cn-photos-card__empty\">\n\t\t\t\t{{ emptyLabel }}\n\t\t\t</div>\n\t\t\t<div v-else class=\"cn-photos-card__grid\">\n\t\t\t\t<a\n\t\t\t\t\tv-for=\"album in displayedAlbums\"\n\t\t\t\t\t:key=\"albumKey(album)\"\n\t\t\t\t\t:href=\"albumUrl(album)\"\n\t\t\t\t\tclass=\"cn-photos-card__tile\"\n\t\t\t\t\ttarget=\"_blank\"\n\t\t\t\t\trel=\"noopener noreferrer\"\n\t\t\t\t\t:title=\"albumName(album)\">\n\t\t\t\t\t<div class=\"cn-photos-card__tile-cover\">\n\t\t\t\t\t\t<img\n\t\t\t\t\t\t\tv-if=\"coverUrl(album)\"\n\t\t\t\t\t\t\t:src=\"coverUrl(album)\"\n\t\t\t\t\t\t\talt=\"\"\n\t\t\t\t\t\t\tclass=\"cn-photos-card__tile-cover-img\"\n\t\t\t\t\t\t\tloading=\"lazy\"\n\t\t\t\t\t\t\t@error=\"onCoverError(album)\">\n\t\t\t\t\t\t<ImageMultiple v-else :size=\"20\" class=\"cn-photos-card__tile-fallback\" />\n\t\t\t\t\t</div>\n\t\t\t\t\t<span class=\"cn-photos-card__tile-name\">{{ albumName(album) }}</span>\n\t\t\t\t</a>\n\t\t\t</div>\n\t\t\t<div v-if=\"albums.length > COMPACT_LIMIT\" class=\"cn-photos-card__view-all\">\n\t\t\t\t<a\n\t\t\t\t\t:href=\"photosAppUrl\"\n\t\t\t\t\ttarget=\"_blank\"\n\t\t\t\t\trel=\"noopener noreferrer\">{{ viewAllLabel }}</a>\n\t\t\t</div>\n\t\t</template>\n\t</CnDetailCard>\n</template>\n\n<script>\nimport { translatePlural as n, translate as t } from '@nextcloud/l10n'\nimport { NcLoadingIcon } from '@nextcloud/vue'\nimport ImageIcon from 'vue-material-design-icons/Image.vue'\nimport ImageMultiple from 'vue-material-design-icons/ImageMultiple.vue'\nimport CnDetailCard from '../../../components/CnDetailCard/CnDetailCard.vue'\nimport { buildHeaders, prefixUrl, safeHref } from '../../../utils/index.js'\n\nconst VALID_SURFACES = ['user-dashboard', 'app-dashboard', 'detail-page', 'single-entity']\nconst COMPACT_LIMIT = 4\n\n/**\n * CnPhotosCard — bespoke surface-aware widget for the `photos`\n * integration.\n *\n * Renders Photos-aware metadata (album count, cover thumbnail, total\n * photo count) across all four AD-19 surfaces. See the file-level\n * docblock for surface-by-surface behaviour.\n */\nexport default {\n\tname: 'CnPhotosCard',\n\n\tcomponents: { CnDetailCard, NcLoadingIcon, ImageIcon, ImageMultiple },\n\n\tprops: {\n\t\t/** Stable integration id (forwarded from the registry — always `'photos'`). */\n\t\tintegrationId: { type: String, default: 'photos' },\n\t\t/** OpenRegister register id (slug or uuid). */\n\t\tregister: { type: String, required: true },\n\t\t/** OpenRegister schema id (slug or uuid). */\n\t\tschema: { type: String, required: true },\n\t\t/** Parent object id. */\n\t\tobjectId: { type: String, required: true },\n\t\t/** Rendering surface (AD-19). */\n\t\tsurface: {\n\t\t\ttype: String,\n\t\t\tdefault: 'detail-page',\n\t\t\tvalidator: (s) => VALID_SURFACES.includes(s),\n\t\t},\n\n\t\t/** Optional single-entity reference (album id). */\n\t\tvalue: { type: [String, Number], default: '' },\n\t\t/** Pre-translated card title. */\n\t\ttitle: { type: String, default: () => t('nextcloud-vue', 'Photos') },\n\t\t/** Optional Material Design Icon component. */\n\t\ticon: { type: Object, default: () => ImageIcon },\n\t\t/** Base API URL. */\n\t\tapiBase: { type: String, default: '/apps/openregister/api' },\n\t\t/** Whether the card body is collapsible. */\n\t\tcollapsible: { type: Boolean, default: true },\n\t\t/** Pre-translated empty-state label. */\n\t\temptyLabel: { type: String, default: () => t('nextcloud-vue', 'No albums linked yet') },\n\t\t/** Pre-translated 503 unavailable label. */\n\t\tunavailableLabel: { type: String, default: () => t('nextcloud-vue', 'NC Photos is currently unavailable.') },\n\t\t/** Pre-translated \"view all\" trail-off link label. */\n\t\tviewAllLabel: { type: String, default: () => t('nextcloud-vue', 'View all in Photos') },\n\t\t/** URL of the NC Photos app entry. */\n\t\tphotosAppUrl: { type: String, default: '/index.php/apps/photos' },\n\t},\n\n\tdata() {\n\t\treturn {\n\t\t\tCOMPACT_LIMIT,\n\t\t\talbums: [],\n\t\t\tentity: null,\n\t\t\tloading: false,\n\t\t\tdegraded: '',\n\t\t\tbrokenCovers: {},\n\t\t}\n\t},\n\n\tcomputed: {\n\t\tcardTitle() {\n\t\t\treturn this.title || this.integrationId\n\t\t},\n\n\t\tcardIcon() {\n\t\t\treturn this.icon\n\t\t},\n\n\t\tdisplayedAlbums() {\n\t\t\treturn this.albums.slice(0, COMPACT_LIMIT)\n\t\t},\n\n\t\talbumCountLabel() {\n\t\t\tconst total = this.albums.length\n\t\t\treturn n('nextcloud-vue', '{count} album', '{count} albums', total, { count: total })\n\t\t},\n\n\t\ttotalPhotoCount() {\n\t\t\tlet total = 0\n\t\t\tlet any = false\n\t\t\tfor (const a of this.albums) {\n\t\t\t\tconst c = this.photoCount(a)\n\t\t\t\tif (c !== null) {\n\t\t\t\t\ttotal += c\n\t\t\t\t\tany = true\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn any ? total : null\n\t\t},\n\n\t\ttotalPhotosLabel() {\n\t\t\tconst count = this.totalPhotoCount ?? 0\n\t\t\treturn n('nextcloud-vue', '{count} photo', '{count} photos', count, { count })\n\t\t},\n\n\t\tfirstAlbum() {\n\t\t\treturn this.albums.length > 0 ? this.albums[0] : null\n\t\t},\n\t},\n\n\twatch: {\n\t\tobjectId: { immediate: true, handler() {\n\t\t\tthis.fetch()\n\t\t} },\n\n\t\tsurface() {\n\t\t\tthis.fetch()\n\t\t},\n\n\t\tvalue() {\n\t\t\tif (this.surface === 'single-entity') {\n\t\t\t\tthis.fetchSingle()\n\t\t\t}\n\t\t},\n\t},\n\n\tmethods: {\n\t\tbaseUrl() {\n\t\t\treturn `${this.apiBase}/objects/${this.register}/${this.schema}/${this.objectId}/integrations/${this.integrationId}`\n\t\t},\n\n\t\talbumKey(album) {\n\t\t\treturn (album && (album.id ?? album.data?.album_id)) || ''\n\t\t},\n\n\t\talbumName(album) {\n\t\t\tconst raw = (album && (album.title || album.name || album.data?.name)) || ''\n\t\t\treturn String(raw).replace(/\\s*\\[or:[^\\]]+\\]\\s*/g, ' ').trim() || t('nextcloud-vue', 'Untitled album')\n\t\t},\n\n\t\talbumUrl(album) {\n\t\t\tif (album && album.url && safeHref(album.url) !== '#') {\n\t\t\t\treturn album.url\n\t\t\t}\n\t\t\tconst name = this.albumName(album)\n\t\t\treturn `/index.php/apps/photos/albums/${encodeURIComponent(name)}`\n\t\t},\n\n\t\tcoverUrl(album) {\n\t\t\tif (!album || this.brokenCovers[this.albumKey(album)]) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn album.coverPhotoUrl || album.data?.coverPhotoUrl || ''\n\t\t},\n\n\t\tonCoverError(album) {\n\t\t\tthis.brokenCovers[this.albumKey(album)] = true\n\t\t},\n\n\t\tphotoCount(album) {\n\t\t\tconst raw = album?.photoCount ?? album?.data?.photoCount ?? album?.data?.count\n\t\t\tif (raw === null || raw === undefined || raw === '') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst num = Number(raw)\n\t\t\treturn Number.isFinite(num) ? num : null\n\t\t},\n\n\t\tfetch() {\n\t\t\tif (this.surface === 'single-entity') {\n\t\t\t\tthis.fetchSingle()\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.fetchList()\n\t\t},\n\n\t\tasync fetchList() {\n\t\t\tif (!this.register || !this.schema || !this.objectId) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.loading = true\n\t\t\tthis.degraded = ''\n\t\t\ttry {\n\t\t\t\tconst response = await fetch(prefixUrl(this.baseUrl()), { headers: buildHeaders() })\n\t\t\t\tif (response.ok) {\n\t\t\t\t\tconst data = await response.json()\n\t\t\t\t\tthis.albums = data.results || data.items || (Array.isArray(data) ? data : []) || []\n\t\t\t\t} else if (response.status === 503) {\n\t\t\t\t\tthis.albums = []\n\t\t\t\t\tthis.degraded = this.unavailableLabel\n\t\t\t\t} else {\n\t\t\t\t\tthis.albums = []\n\t\t\t\t}\n\t\t\t} catch (err) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.error('[CnPhotosCard] failed to fetch albums', err)\n\t\t\t\tthis.albums = []\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\t\t},\n\n\t\tasync fetchSingle() {\n\t\t\tif (this.value === '' || this.value === undefined || this.value === null || !this.register || !this.schema || !this.objectId) {\n\t\t\t\tthis.entity = null\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.loading = true\n\t\t\tthis.degraded = ''\n\t\t\ttry {\n\t\t\t\tconst response = await fetch(prefixUrl(`${this.baseUrl()}/${encodeURIComponent(this.value)}`), { headers: buildHeaders() })\n\t\t\t\tif (response.ok) {\n\t\t\t\t\tthis.entity = await response.json()\n\t\t\t\t} else if (response.status === 503) {\n\t\t\t\t\tthis.entity = null\n\t\t\t\t\tthis.degraded = this.unavailableLabel\n\t\t\t\t} else {\n\t\t\t\t\tthis.entity = null\n\t\t\t\t}\n\t\t\t} catch (err) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.error('[CnPhotosCard] failed to fetch single album', err)\n\t\t\t\tthis.entity = null\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-photos-card__empty {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.9em;\n\tpadding: 8px 0;\n}\n\n.cn-photos-card__headline {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 6px;\n}\n\n.cn-photos-card__headline-line {\n\tfont-size: 1.1em;\n\tcolor: var(--color-main-text);\n}\n\n.cn-photos-card__photo-total {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.9em;\n\tfont-weight: 400;\n}\n\n.cn-photos-card__headline-album {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 8px;\n\toverflow: hidden;\n}\n\n.cn-photos-card__headline-cover {\n\twidth: 32px;\n\theight: 32px;\n\tborder-radius: var(--border-radius);\n\toverflow: hidden;\n\tflex-shrink: 0;\n\tbackground: var(--color-background-darker, var(--color-background-dark));\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: center;\n}\n\n.cn-photos-card__headline-cover-img {\n\twidth: 100%;\n\theight: 100%;\n\tobject-fit: cover;\n}\n\n.cn-photos-card__headline-name {\n\tcolor: var(--color-main-text);\n\ttext-decoration: none;\n\toverflow: hidden;\n\ttext-overflow: ellipsis;\n\twhite-space: nowrap;\n\tfont-size: 0.95em;\n}\n\n.cn-photos-card__headline-name:hover {\n\ttext-decoration: underline;\n}\n\n.cn-photos-card__chip {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 6px;\n\tpadding: 4px 10px 4px 4px;\n\tborder-radius: 14px;\n\tbackground: var(--color-background-hover);\n\tfont-size: 0.9em;\n\tmax-width: 100%;\n}\n\n.cn-photos-card__chip-cover {\n\twidth: 20px;\n\theight: 20px;\n\tborder-radius: 50%;\n\tobject-fit: cover;\n\tflex-shrink: 0;\n}\n\n.cn-photos-card__chip a {\n\tcolor: var(--color-main-text);\n\ttext-decoration: none;\n\toverflow: hidden;\n\ttext-overflow: ellipsis;\n\twhite-space: nowrap;\n}\n\n.cn-photos-card__chip a:hover {\n\ttext-decoration: underline;\n}\n\n.cn-photos-card__grid {\n\tdisplay: grid;\n\tgrid-template-columns: repeat(2, 1fr);\n\tgap: 8px;\n}\n\n.cn-photos-card__tile {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 4px;\n\ttext-decoration: none;\n\tcolor: inherit;\n\tborder-radius: var(--border-radius);\n\toverflow: hidden;\n\tbackground: var(--color-background-hover);\n}\n\n.cn-photos-card__tile:hover,\n.cn-photos-card__tile:focus-within {\n\tbackground: var(--color-background-dark, var(--color-background-hover));\n}\n\n.cn-photos-card__tile-cover {\n\twidth: 100%;\n\taspect-ratio: 1 / 1;\n\tbackground: var(--color-background-darker, var(--color-background-dark));\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: center;\n\toverflow: hidden;\n}\n\n.cn-photos-card__tile-cover-img {\n\twidth: 100%;\n\theight: 100%;\n\tobject-fit: cover;\n}\n\n.cn-photos-card__tile-fallback {\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-photos-card__tile-name {\n\tpadding: 4px 6px 6px;\n\tfont-size: 12px;\n\tcolor: var(--color-main-text);\n\twhite-space: nowrap;\n\toverflow: hidden;\n\ttext-overflow: ellipsis;\n}\n\n.cn-photos-card__view-all {\n\tmargin-top: 6px;\n\tfont-size: 0.8em;\n}\n\n/* stylelint-disable-next-line no-descending-specificity */\n.cn-photos-card__view-all a {\n\tcolor: var(--color-primary-element);\n\ttext-decoration: none;\n}\n\n.cn-photos-card__view-all a:hover {\n\ttext-decoration: underline;\n}\n</style>\n"],"names":["_createBlock","_Fragment","_createElementBlock","_createElementVNode","_toDisplayString","_openBlock"],"mappings":"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"}
|
|
@@ -8,6 +8,7 @@ import LinkVariant from 'vue-material-design-icons/LinkVariant.vue';
|
|
|
8
8
|
import Plus from 'vue-material-design-icons/Plus.vue';
|
|
9
9
|
import __sfc_main$1 from '../../../components/CnPhotoAlbumCreate/CnPhotoAlbumCreate.vue.js';
|
|
10
10
|
import __sfc_main from '../../../components/CnPhotoAlbumPicker/CnPhotoAlbumPicker.vue.js';
|
|
11
|
+
import { safeHref } from '../../../utils/safeHref.js';
|
|
11
12
|
import { prefixUrl, buildHeaders } from '../../../utils/headers.js';
|
|
12
13
|
|
|
13
14
|
/**
|
|
@@ -116,7 +117,7 @@ var script = {
|
|
|
116
117
|
},
|
|
117
118
|
|
|
118
119
|
albumUrl(album) {
|
|
119
|
-
if (album && album.url) {
|
|
120
|
+
if (album && album.url && safeHref(album.url) !== '#') {
|
|
120
121
|
return album.url
|
|
121
122
|
}
|
|
122
123
|
const id = this.albumKey(album);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CnPhotosTab.vue2.js","sources":["../../../../../src/integrations/builtin/photos/CnPhotosTab.vue"],"sourcesContent":["<!--\n CnPhotosTab — bespoke sidebar tab for the `photos` integration leaf.\n\n Replaces the generic `CnIntegrationTab` for the `photos` leaf: renders\n the linked NC Photos albums as a card grid (cover thumbnail, album\n name, photo count, last-edited date). Clicking a card deep-links into\n the NC Photos app at `/index.php/apps/photos/albums/{albumId}`.\n\n Tier-2 surface (this commit):\n - \"Link existing album\" → opens CnPhotoAlbumPicker (modal)\n - \"Create new album\" → opens CnPhotoAlbumCreate (modal)\n - Per-tile unlink → DELETE …/photos/{albumId}\n\n Talks to the OpenRegister Tier-2 photo-link endpoints\n GET /api/objects/{r}/{s}/{id}/photos — list\n POST /api/objects/{r}/{s}/{id}/photos — link existing\n POST /api/objects/{r}/{s}/{id}/photos/new — create + link\n DELETE /api/objects/{r}/{s}/{id}/photos/{albumId} — unlink\n GET /api/integrations/photos/available — picker source\n served by `OCA\\OpenRegister\\Controller\\PhotoLinksController`.\n\n The endpoint returns rows shaped:\n { id, title, url, albumId, albumName, coverPhotoUrl, photoCount,\n lastEdited, data: { … } }\n\n Empty + loading + error + 503 unavailable states follow ADR-017 and\n AD-23 graceful degradation. All UI strings are passed through\n `t('nextcloud-vue', ...)` per ADR-007. Styling uses Nextcloud CSS\n variables only so the nldesign overrides apply transparently\n (ADR-010).\n\n Bespoke-vs-generic rationale: the generic tab renders a flat link list\n which loses the cover thumbnail + photo count + recency — the three\n signals users rely on when triaging which album is relevant for a\n case. The bespoke tab surfaces them per card.\n-->\n<template>\n\t<div class=\"cn-sidebar-tab cn-photos-tab\">\n\t\t<!-- 503 unavailable banner (AD-23) -->\n\t\t<div v-if=\"degraded\" class=\"cn-photos-tab__banner\" role=\"alert\">\n\t\t\t<AlertCircleOutline :size=\"18\" />\n\t\t\t<span>{{ degraded }}</span>\n\t\t</div>\n\n\t\t<div class=\"cn-photos-tab__actions\">\n\t\t\t<NcButton variant=\"secondary\" @click=\"openPicker\">\n\t\t\t\t<template #icon>\n\t\t\t\t\t<LinkVariant :size=\"18\" />\n\t\t\t\t</template>\n\t\t\t\t{{ t('nextcloud-vue', 'Link existing album') }}\n\t\t\t</NcButton>\n\t\t\t<NcButton variant=\"primary\" @click=\"openCreate\">\n\t\t\t\t<template #icon>\n\t\t\t\t\t<Plus :size=\"18\" />\n\t\t\t\t</template>\n\t\t\t\t{{ t('nextcloud-vue', 'Create new album') }}\n\t\t\t</NcButton>\n\t\t</div>\n\n\t\t<!-- Loading -->\n\t\t<NcLoadingIcon v-if=\"loading\" />\n\n\t\t<!-- Error -->\n\t\t<div v-else-if=\"error\" class=\"cn-photos-tab__error\" role=\"alert\">\n\t\t\t{{ error }}\n\t\t</div>\n\n\t\t<!-- Empty -->\n\t\t<div v-else-if=\"albums.length === 0\" class=\"cn-sidebar-tab__empty cn-photos-tab__empty\">\n\t\t\t<ImageIcon :size=\"32\" class=\"cn-photos-tab__empty-icon\" />\n\t\t\t<p>{{ emptyLabel }}</p>\n\t\t\t<NcButton\n\t\t\t\tvariant=\"primary\"\n\t\t\t\t:href=\"photosAppUrl\"\n\t\t\t\ttarget=\"_blank\"\n\t\t\t\trel=\"noopener noreferrer\">\n\t\t\t\t<template #icon>\n\t\t\t\t\t<ImageIcon :size=\"20\" />\n\t\t\t\t</template>\n\t\t\t\t{{ openPhotosLabel }}\n\t\t\t</NcButton>\n\t\t</div>\n\n\t\t<!-- Photo mosaic grid (mirrors the NC Photos timeline) -->\n\t\t<div v-else class=\"cn-photos-tab__grid\">\n\t\t\t<div\n\t\t\t\tv-for=\"album in albums\"\n\t\t\t\t:key=\"albumKey(album)\"\n\t\t\t\tclass=\"cn-photos-tab__tile\">\n\t\t\t\t<a\n\t\t\t\t\t:href=\"albumUrl(album)\"\n\t\t\t\t\tclass=\"cn-photos-tab__tile-link\"\n\t\t\t\t\ttarget=\"_blank\"\n\t\t\t\t\trel=\"noopener noreferrer\"\n\t\t\t\t\t:title=\"albumName(album)\">\n\t\t\t\t\t<div class=\"cn-photos-tab__cover\">\n\t\t\t\t\t\t<img\n\t\t\t\t\t\t\tv-if=\"coverUrl(album)\"\n\t\t\t\t\t\t\t:src=\"coverUrl(album)\"\n\t\t\t\t\t\t\t:alt=\"albumName(album)\"\n\t\t\t\t\t\t\tclass=\"cn-photos-tab__cover-img\"\n\t\t\t\t\t\t\tloading=\"lazy\"\n\t\t\t\t\t\t\t@error=\"onCoverError(album)\">\n\t\t\t\t\t\t<span v-else class=\"cn-photos-tab__cover-placeholder\">\n\t\t\t\t\t\t\t<ImageMultiple :size=\"36\" class=\"cn-photos-tab__cover-fallback\" />\n\t\t\t\t\t\t</span>\n\t\t\t\t\t\t<!-- Photo-count badge, top-left (mirrors album stack overlay) -->\n\t\t\t\t\t\t<span v-if=\"photoCount(album) !== null\" class=\"cn-photos-tab__badge\">\n\t\t\t\t\t\t\t<ImageMultiple :size=\"12\" />\n\t\t\t\t\t\t\t<span class=\"cn-photos-tab__badge-count\">{{ photoCountLabel(album) }}</span>\n\t\t\t\t\t\t</span>\n\t\t\t\t\t\t<!-- Hover scrim with name + date -->\n\t\t\t\t\t\t<div class=\"cn-photos-tab__meta\">\n\t\t\t\t\t\t\t<span class=\"cn-photos-tab__name\">{{ albumName(album) }}</span>\n\t\t\t\t\t\t\t<span v-if=\"lastEdited(album)\" class=\"cn-photos-tab__when\">\n\t\t\t\t\t\t\t\t{{ formatWhen(lastEdited(album)) }}\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</a>\n\t\t\t\t<NcButton\n\t\t\t\t\tvariant=\"tertiary-no-background\"\n\t\t\t\t\t:aria-label=\"t('nextcloud-vue', 'Unlink album')\"\n\t\t\t\t\tclass=\"cn-photos-tab__unlink\"\n\t\t\t\t\t@click=\"unlinkAlbum(album)\">\n\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t<LinkOff :size=\"16\" />\n\t\t\t\t\t</template>\n\t\t\t\t</NcButton>\n\t\t\t</div>\n\t\t</div>\n\n\t\t<CnPhotoAlbumPicker\n\t\t\tv-if=\"pickerOpen\"\n\t\t\t:apiBase=\"apiBase\"\n\t\t\t@close=\"pickerOpen = false\"\n\t\t\t@link=\"onLinkPick\" />\n\n\t\t<CnPhotoAlbumCreate\n\t\t\tv-if=\"createOpen\"\n\t\t\t@close=\"createOpen = false\"\n\t\t\t@create=\"onCreatePick\" />\n\t</div>\n</template>\n\n<script>\nimport { translatePlural as n, translate as t } from '@nextcloud/l10n'\nimport { NcButton, NcLoadingIcon } from '@nextcloud/vue'\nimport AlertCircleOutline from 'vue-material-design-icons/AlertCircleOutline.vue'\nimport ImageIcon from 'vue-material-design-icons/Image.vue'\nimport ImageMultiple from 'vue-material-design-icons/ImageMultiple.vue'\nimport LinkOff from 'vue-material-design-icons/LinkOff.vue'\nimport LinkVariant from 'vue-material-design-icons/LinkVariant.vue'\nimport Plus from 'vue-material-design-icons/Plus.vue'\nimport CnPhotoAlbumCreate from '../../../components/CnPhotoAlbumCreate/CnPhotoAlbumCreate.vue'\nimport CnPhotoAlbumPicker from '../../../components/CnPhotoAlbumPicker/CnPhotoAlbumPicker.vue'\nimport { buildHeaders, prefixUrl } from '../../../utils/index.js'\n\n/**\n * CnPhotosTab — bespoke album grid for the `photos` integration.\n *\n * Renders one tile per linked NC Photos album with cover thumbnail,\n * album name, photo count, and last-edited date. Clicking a tile opens\n * the album in the NC Photos app in a new tab. Tier-2: includes\n * link/create modals and per-tile unlink.\n */\nexport default {\n\tname: 'CnPhotosTab',\n\n\tcomponents: {\n\t\tNcButton,\n\t\tNcLoadingIcon,\n\t\tAlertCircleOutline,\n\t\tImageIcon,\n\t\tImageMultiple,\n\t\tLinkOff,\n\t\tLinkVariant,\n\t\tPlus,\n\t\tCnPhotoAlbumPicker,\n\t\tCnPhotoAlbumCreate,\n\t},\n\n\tprops: {\n\t\t/* eslint-disable vue/no-unused-properties -- the integration dispatch binds integrationId on every integration component (see CnIntegrationWidgetGrid), so declaring it keeps it out of $attrs */\n\t\t/** Stable integration id (forwarded from the registry — always `'photos'`). */\n\t\tintegrationId: { type: String, default: 'photos' },\n\t\t/* eslint-enable vue/no-unused-properties */\n\t\t/** Parent object id. */\n\t\tobjectId: { type: String, required: true },\n\t\t/** OpenRegister register id (slug or uuid). */\n\t\tregister: { type: String, default: '' },\n\t\t/** OpenRegister schema id (slug or uuid). */\n\t\tschema: { type: String, default: '' },\n\t\t/** Base API URL. */\n\t\tapiBase: { type: String, default: '/apps/openregister/api' },\n\t\t/** Pre-translated empty-state label. */\n\t\temptyLabel: { type: String, default: () => t('nextcloud-vue', 'No albums linked yet') },\n\t\t/** Pre-translated \"Open Photos\" CTA label. */\n\t\topenPhotosLabel: { type: String, default: () => t('nextcloud-vue', 'Open Photos') },\n\t\t/** Pre-translated 503 unavailable banner copy. */\n\t\tunavailableLabel: { type: String, default: () => t('nextcloud-vue', 'NC Photos is currently unavailable.') },\n\t\t/** URL of the NC Photos app entry. */\n\t\tphotosAppUrl: { type: String, default: '/index.php/apps/photos' },\n\t},\n\n\tdata() {\n\t\treturn {\n\t\t\talbums: [],\n\t\t\tloading: false,\n\t\t\terror: '',\n\t\t\tdegraded: '',\n\t\t\tbrokenCovers: {},\n\t\t\tpickerOpen: false,\n\t\t\tcreateOpen: false,\n\t\t}\n\t},\n\n\twatch: {\n\t\tobjectId: { immediate: true, handler(id) {\n\t\t\tif (id) {\n\t\t\t\tthis.fetchAlbums()\n\t\t\t}\n\t\t} },\n\n\t\tregister() {\n\t\t\tthis.fetchAlbums()\n\t\t},\n\n\t\tschema() {\n\t\t\tthis.fetchAlbums()\n\t\t},\n\t},\n\n\tmethods: {\n\t\tt,\n\n\t\t/**\n\t\t * Base for the Tier-2 photos endpoints (list/link/new/destroy).\n\t\t *\n\t\t * @return {string} The endpoint URL.\n\t\t */\n\t\tphotosEndpoint() {\n\t\t\treturn `${this.apiBase}/objects/${this.register}/${this.schema}/${this.objectId}/photos`\n\t\t},\n\n\t\talbumKey(album) {\n\t\t\treturn (album && (album.albumId ?? album.id ?? album.data?.album_id)) || ''\n\t\t},\n\n\t\t/**\n\t\t * Resolve the displayable album name from the row, stripping any\n\t\t * legacy `[or:{uuid}]` marker (pre-Tier-2 albums embedded the\n\t\t * marker inside `name`).\n\t\t *\n\t\t * @param {object} album Provider row.\n\t\t *\n\t\t * @return {string} The display name.\n\t\t */\n\t\talbumName(album) {\n\t\t\tconst raw = (album && (album.albumName || album.title || album.name || album.data?.name)) || ''\n\t\t\treturn String(raw).replace(/\\s*\\[or:[^\\]]+\\]\\s*/g, ' ').trim() || t('nextcloud-vue', 'Untitled album')\n\t\t},\n\n\t\talbumUrl(album) {\n\t\t\tif (album && album.url) {\n\t\t\t\treturn album.url\n\t\t\t}\n\t\t\tconst id = this.albumKey(album)\n\t\t\treturn `/index.php/apps/photos/albums/${encodeURIComponent(id)}`\n\t\t},\n\n\t\tcoverUrl(album) {\n\t\t\tif (!album || this.brokenCovers[this.albumKey(album)]) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn album.coverPhotoUrl || album.data?.coverPhotoUrl || ''\n\t\t},\n\n\t\tonCoverError(album) {\n\t\t\tthis.brokenCovers[this.albumKey(album)] = true\n\t\t},\n\n\t\tphotoCount(album) {\n\t\t\tconst raw = album?.photoCount ?? album?.data?.photoCount ?? album?.data?.count\n\t\t\tif (raw === null || raw === undefined || raw === '') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst num = Number(raw)\n\t\t\treturn Number.isFinite(num) ? num : null\n\t\t},\n\n\t\tphotoCountLabel(album) {\n\t\t\tconst count = this.photoCount(album)\n\t\t\tif (count === null) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn n('nextcloud-vue', '{count} photo', '{count} photos', count, { count })\n\t\t},\n\n\t\tlastEdited(album) {\n\t\t\treturn album?.lastEdited\n\t\t\t\t|| album?.data?.lastEdited\n\t\t\t\t|| album?.data?.created\n\t\t\t\t|| ''\n\t\t},\n\n\t\tformatWhen(value) {\n\t\t\tif (!value) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\ttry {\n\t\t\t\t// NC Photos stores `created` as a unix timestamp in\n\t\t\t\t// seconds; accept ISO strings transparently too.\n\t\t\t\tconst num = Number(value)\n\t\t\t\tconst d = Number.isFinite(num) && num > 0 && String(value).length <= 12\n\t\t\t\t\t? new Date(num * 1000)\n\t\t\t\t\t: new Date(value)\n\t\t\t\tif (Number.isNaN(d.getTime())) {\n\t\t\t\t\treturn String(value)\n\t\t\t\t}\n\t\t\t\treturn d.toLocaleDateString(undefined, { dateStyle: 'medium' })\n\t\t\t} catch {\n\t\t\t\treturn String(value)\n\t\t\t}\n\t\t},\n\n\t\topenPicker() {\n\t\t\tthis.pickerOpen = true\n\t\t},\n\n\t\topenCreate() {\n\t\t\tthis.createOpen = true\n\t\t},\n\n\t\tasync onLinkPick(payload) {\n\t\t\tthis.pickerOpen = false\n\t\t\ttry {\n\t\t\t\tconst response = await fetch(prefixUrl(this.photosEndpoint()), {\n\t\t\t\t\tmethod: 'POST',\n\t\t\t\t\theaders: { ...buildHeaders(), 'Content-Type': 'application/json' },\n\t\t\t\t\tbody: JSON.stringify(payload),\n\t\t\t\t})\n\t\t\t\tif (response.ok) {\n\t\t\t\t\tawait this.fetchAlbums()\n\t\t\t\t} else if (response.status === 409) {\n\t\t\t\t\tthis.error = t('nextcloud-vue', 'This album is already linked.')\n\t\t\t\t} else {\n\t\t\t\t\tthis.error = t('nextcloud-vue', 'Could not link album.')\n\t\t\t\t}\n\t\t\t} catch (err) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.error('[CnPhotosTab] link failed', err)\n\t\t\t\tthis.error = t('nextcloud-vue', 'Could not link album.')\n\t\t\t}\n\t\t},\n\n\t\tasync onCreatePick(payload) {\n\t\t\tthis.createOpen = false\n\t\t\ttry {\n\t\t\t\tconst response = await fetch(prefixUrl(`${this.photosEndpoint()}/new`), {\n\t\t\t\t\tmethod: 'POST',\n\t\t\t\t\theaders: { ...buildHeaders(), 'Content-Type': 'application/json' },\n\t\t\t\t\tbody: JSON.stringify(payload),\n\t\t\t\t})\n\t\t\t\tif (response.ok) {\n\t\t\t\t\tawait this.fetchAlbums()\n\t\t\t\t} else {\n\t\t\t\t\tthis.error = t('nextcloud-vue', 'Could not create album.')\n\t\t\t\t}\n\t\t\t} catch (err) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.error('[CnPhotosTab] create failed', err)\n\t\t\t\tthis.error = t('nextcloud-vue', 'Could not create album.')\n\t\t\t}\n\t\t},\n\n\t\tasync unlinkAlbum(album) {\n\t\t\tconst albumId = this.albumKey(album)\n\t\t\tif (!albumId) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tconst response = await fetch(prefixUrl(`${this.photosEndpoint()}/${albumId}`), {\n\t\t\t\t\tmethod: 'DELETE',\n\t\t\t\t\theaders: buildHeaders(),\n\t\t\t\t})\n\t\t\t\tif (response.ok) {\n\t\t\t\t\tawait this.fetchAlbums()\n\t\t\t\t} else {\n\t\t\t\t\tthis.error = t('nextcloud-vue', 'Could not unlink album.')\n\t\t\t\t}\n\t\t\t} catch (err) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.error('[CnPhotosTab] unlink failed', err)\n\t\t\t\tthis.error = t('nextcloud-vue', 'Could not unlink album.')\n\t\t\t}\n\t\t},\n\n\t\tasync fetchAlbums() {\n\t\t\tif (!this.register || !this.schema || !this.objectId) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.loading = true\n\t\t\tthis.error = ''\n\t\t\tthis.degraded = ''\n\t\t\ttry {\n\t\t\t\tconst response = await fetch(prefixUrl(this.photosEndpoint()), { headers: buildHeaders() })\n\t\t\t\tif (response.ok) {\n\t\t\t\t\tconst data = await response.json()\n\t\t\t\t\tthis.albums = data.results || data.items || (Array.isArray(data) ? data : []) || []\n\t\t\t\t} else if (response.status === 503 || response.status === 501) {\n\t\t\t\t\tthis.albums = []\n\t\t\t\t\tthis.degraded = this.unavailableLabel\n\t\t\t\t} else {\n\t\t\t\t\tthis.albums = []\n\t\t\t\t\tthis.error = t('nextcloud-vue', 'Could not load albums.')\n\t\t\t\t}\n\t\t\t} catch (err) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.error('[CnPhotosTab] failed to fetch albums', err)\n\t\t\t\tthis.albums = []\n\t\t\t\tthis.error = t('nextcloud-vue', 'Could not load albums.')\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-photos-tab {\n\tpadding: 12px;\n\toverflow-x: hidden;\n}\n\n.cn-photos-tab__actions {\n\tdisplay: flex;\n\tgap: 8px;\n\tmargin-bottom: 10px;\n\tflex-wrap: wrap;\n}\n\n.cn-photos-tab__banner {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 8px;\n\tpadding: 8px 10px;\n\tmargin-bottom: 10px;\n\tborder-radius: var(--border-radius);\n\tbackground: var(--color-warning, #e9a40f);\n\tcolor: var(--color-main-background);\n\tfont-size: 0.9em;\n}\n\n.cn-photos-tab__error {\n\tcolor: var(--color-error);\n\tfont-size: 0.9em;\n\tmargin: 4px 0 8px;\n}\n\n.cn-photos-tab__empty {\n\tdisplay: flex;\n\tflex-direction: column;\n\talign-items: center;\n\tgap: 8px;\n\tpadding: 16px 8px;\n\tcolor: var(--color-text-maxcontrast);\n\ttext-align: center;\n}\n\n.cn-photos-tab__empty-icon {\n\tcolor: var(--color-text-maxcontrast);\n}\n\n/*\n * Dense edge-to-edge mosaic that mirrors the NC Photos timeline: square\n * thumbnails packed tight with a thin main-background gutter (the same\n * 2px border NC Photos draws between tiles), name + date revealed in a\n * bottom scrim on hover/focus so the imagery dominates at rest.\n */\n.cn-photos-tab__grid {\n\tdisplay: grid;\n\tgrid-template-columns: repeat(auto-fill, minmax(104px, 1fr));\n\tgap: 2px;\n\tborder-radius: var(--border-radius-large, var(--border-radius));\n\toverflow: hidden;\n}\n\n.cn-photos-tab__tile {\n\tposition: relative;\n}\n\n.cn-photos-tab__unlink {\n\tposition: absolute;\n\ttop: 4px;\n\tright: 4px;\n\tz-index: 3;\n\topacity: 0;\n\tbackground: var(--color-main-background);\n\tborder-radius: 50%;\n\ttransition: opacity 0.15s ease;\n}\n\n.cn-photos-tab__tile:hover .cn-photos-tab__unlink,\n.cn-photos-tab__tile:focus-within .cn-photos-tab__unlink {\n\topacity: 1;\n}\n\n.cn-photos-tab__tile-link {\n\tdisplay: block;\n\ttext-decoration: none;\n\tcolor: inherit;\n}\n\n.cn-photos-tab__cover {\n\tposition: relative;\n\taspect-ratio: 1 / 1;\n\twidth: 100%;\n\t/* Match the NC Photos selection outline on focus. */\n\tborder: 2px solid var(--color-main-background);\n\tbox-sizing: border-box;\n\tbackground: var(--color-primary-element-light, var(--color-background-darker));\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: center;\n\toverflow: hidden;\n}\n\n.cn-photos-tab__tile-link:focus-visible .cn-photos-tab__cover {\n\toutline: var(--color-primary-element) solid 2px;\n\toutline-offset: -2px;\n}\n\n.cn-photos-tab__cover-img {\n\twidth: 100%;\n\theight: 100%;\n\tobject-fit: cover;\n\tdisplay: block;\n\ttransition: transform 0.2s ease;\n}\n\n.cn-photos-tab__tile:hover .cn-photos-tab__cover-img,\n.cn-photos-tab__tile:focus-within .cn-photos-tab__cover-img {\n\ttransform: scale(1.04);\n}\n\n.cn-photos-tab__cover-placeholder {\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: center;\n\twidth: 100%;\n\theight: 100%;\n}\n\n.cn-photos-tab__cover-fallback {\n\tcolor: var(--color-primary-element);\n}\n\n/* Photo-count badge (mirrors the album stack count overlay). */\n.cn-photos-tab__badge {\n\tposition: absolute;\n\ttop: 4px;\n\tleft: 4px;\n\tz-index: 2;\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 3px;\n\tpadding: 1px 6px 1px 4px;\n\tborder-radius: var(--border-radius-pill, 12px);\n\tbackground: rgba(0, 0, 0, 0.55);\n\tcolor: #fff;\n\tfont-size: 11px;\n\tfont-weight: 500;\n\tline-height: 1.6;\n}\n\n.cn-photos-tab__badge-count {\n\twhite-space: nowrap;\n}\n\n/* Bottom scrim: hidden at rest, revealed on hover/focus. */\n.cn-photos-tab__meta {\n\tposition: absolute;\n\tleft: 0;\n\tright: 0;\n\tbottom: 0;\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 1px;\n\tpadding: 16px 8px 6px;\n\tbackground: linear-gradient(to top, rgba(0, 0, 0, 0.7), rgba(0, 0, 0, 0));\n\topacity: 0;\n\ttransition: opacity 0.15s ease;\n\tpointer-events: none;\n}\n\n.cn-photos-tab__tile:hover .cn-photos-tab__meta,\n.cn-photos-tab__tile:focus-within .cn-photos-tab__meta {\n\topacity: 1;\n}\n\n.cn-photos-tab__name {\n\tfont-size: 12px;\n\tfont-weight: 600;\n\tcolor: #fff;\n\twhite-space: nowrap;\n\toverflow: hidden;\n\ttext-overflow: ellipsis;\n}\n\n.cn-photos-tab__when {\n\tfont-size: 10px;\n\tcolor: rgba(255, 255, 255, 0.85);\n\twhite-space: nowrap;\n\toverflow: hidden;\n\ttext-overflow: ellipsis;\n}\n</style>\n"],"names":["CnPhotoAlbumPicker","CnPhotoAlbumCreate","t","n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"CnPhotosTab.vue2.js","sources":["../../../../../src/integrations/builtin/photos/CnPhotosTab.vue"],"sourcesContent":["<!--\n CnPhotosTab — bespoke sidebar tab for the `photos` integration leaf.\n\n Replaces the generic `CnIntegrationTab` for the `photos` leaf: renders\n the linked NC Photos albums as a card grid (cover thumbnail, album\n name, photo count, last-edited date). Clicking a card deep-links into\n the NC Photos app at `/index.php/apps/photos/albums/{albumId}`.\n\n Tier-2 surface (this commit):\n - \"Link existing album\" → opens CnPhotoAlbumPicker (modal)\n - \"Create new album\" → opens CnPhotoAlbumCreate (modal)\n - Per-tile unlink → DELETE …/photos/{albumId}\n\n Talks to the OpenRegister Tier-2 photo-link endpoints\n GET /api/objects/{r}/{s}/{id}/photos — list\n POST /api/objects/{r}/{s}/{id}/photos — link existing\n POST /api/objects/{r}/{s}/{id}/photos/new — create + link\n DELETE /api/objects/{r}/{s}/{id}/photos/{albumId} — unlink\n GET /api/integrations/photos/available — picker source\n served by `OCA\\OpenRegister\\Controller\\PhotoLinksController`.\n\n The endpoint returns rows shaped:\n { id, title, url, albumId, albumName, coverPhotoUrl, photoCount,\n lastEdited, data: { … } }\n\n Empty + loading + error + 503 unavailable states follow ADR-017 and\n AD-23 graceful degradation. All UI strings are passed through\n `t('nextcloud-vue', ...)` per ADR-007. Styling uses Nextcloud CSS\n variables only so the nldesign overrides apply transparently\n (ADR-010).\n\n Bespoke-vs-generic rationale: the generic tab renders a flat link list\n which loses the cover thumbnail + photo count + recency — the three\n signals users rely on when triaging which album is relevant for a\n case. The bespoke tab surfaces them per card.\n-->\n<template>\n\t<div class=\"cn-sidebar-tab cn-photos-tab\">\n\t\t<!-- 503 unavailable banner (AD-23) -->\n\t\t<div v-if=\"degraded\" class=\"cn-photos-tab__banner\" role=\"alert\">\n\t\t\t<AlertCircleOutline :size=\"18\" />\n\t\t\t<span>{{ degraded }}</span>\n\t\t</div>\n\n\t\t<div class=\"cn-photos-tab__actions\">\n\t\t\t<NcButton variant=\"secondary\" @click=\"openPicker\">\n\t\t\t\t<template #icon>\n\t\t\t\t\t<LinkVariant :size=\"18\" />\n\t\t\t\t</template>\n\t\t\t\t{{ t('nextcloud-vue', 'Link existing album') }}\n\t\t\t</NcButton>\n\t\t\t<NcButton variant=\"primary\" @click=\"openCreate\">\n\t\t\t\t<template #icon>\n\t\t\t\t\t<Plus :size=\"18\" />\n\t\t\t\t</template>\n\t\t\t\t{{ t('nextcloud-vue', 'Create new album') }}\n\t\t\t</NcButton>\n\t\t</div>\n\n\t\t<!-- Loading -->\n\t\t<NcLoadingIcon v-if=\"loading\" />\n\n\t\t<!-- Error -->\n\t\t<div v-else-if=\"error\" class=\"cn-photos-tab__error\" role=\"alert\">\n\t\t\t{{ error }}\n\t\t</div>\n\n\t\t<!-- Empty -->\n\t\t<div v-else-if=\"albums.length === 0\" class=\"cn-sidebar-tab__empty cn-photos-tab__empty\">\n\t\t\t<ImageIcon :size=\"32\" class=\"cn-photos-tab__empty-icon\" />\n\t\t\t<p>{{ emptyLabel }}</p>\n\t\t\t<NcButton\n\t\t\t\tvariant=\"primary\"\n\t\t\t\t:href=\"photosAppUrl\"\n\t\t\t\ttarget=\"_blank\">\n\t\t\t\t<template #icon>\n\t\t\t\t\t<ImageIcon :size=\"20\" />\n\t\t\t\t</template>\n\t\t\t\t{{ openPhotosLabel }}\n\t\t\t</NcButton>\n\t\t</div>\n\n\t\t<!-- Photo mosaic grid (mirrors the NC Photos timeline) -->\n\t\t<div v-else class=\"cn-photos-tab__grid\">\n\t\t\t<div\n\t\t\t\tv-for=\"album in albums\"\n\t\t\t\t:key=\"albumKey(album)\"\n\t\t\t\tclass=\"cn-photos-tab__tile\">\n\t\t\t\t<a\n\t\t\t\t\t:href=\"albumUrl(album)\"\n\t\t\t\t\tclass=\"cn-photos-tab__tile-link\"\n\t\t\t\t\ttarget=\"_blank\"\n\t\t\t\t\trel=\"noopener noreferrer\"\n\t\t\t\t\t:title=\"albumName(album)\">\n\t\t\t\t\t<div class=\"cn-photos-tab__cover\">\n\t\t\t\t\t\t<img\n\t\t\t\t\t\t\tv-if=\"coverUrl(album)\"\n\t\t\t\t\t\t\t:src=\"coverUrl(album)\"\n\t\t\t\t\t\t\t:alt=\"albumName(album)\"\n\t\t\t\t\t\t\tclass=\"cn-photos-tab__cover-img\"\n\t\t\t\t\t\t\tloading=\"lazy\"\n\t\t\t\t\t\t\t@error=\"onCoverError(album)\">\n\t\t\t\t\t\t<span v-else class=\"cn-photos-tab__cover-placeholder\">\n\t\t\t\t\t\t\t<ImageMultiple :size=\"36\" class=\"cn-photos-tab__cover-fallback\" />\n\t\t\t\t\t\t</span>\n\t\t\t\t\t\t<!-- Photo-count badge, top-left (mirrors album stack overlay) -->\n\t\t\t\t\t\t<span v-if=\"photoCount(album) !== null\" class=\"cn-photos-tab__badge\">\n\t\t\t\t\t\t\t<ImageMultiple :size=\"12\" />\n\t\t\t\t\t\t\t<span class=\"cn-photos-tab__badge-count\">{{ photoCountLabel(album) }}</span>\n\t\t\t\t\t\t</span>\n\t\t\t\t\t\t<!-- Hover scrim with name + date -->\n\t\t\t\t\t\t<div class=\"cn-photos-tab__meta\">\n\t\t\t\t\t\t\t<span class=\"cn-photos-tab__name\">{{ albumName(album) }}</span>\n\t\t\t\t\t\t\t<span v-if=\"lastEdited(album)\" class=\"cn-photos-tab__when\">\n\t\t\t\t\t\t\t\t{{ formatWhen(lastEdited(album)) }}\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</a>\n\t\t\t\t<NcButton\n\t\t\t\t\tvariant=\"tertiary-no-background\"\n\t\t\t\t\t:aria-label=\"t('nextcloud-vue', 'Unlink album')\"\n\t\t\t\t\tclass=\"cn-photos-tab__unlink\"\n\t\t\t\t\t@click=\"unlinkAlbum(album)\">\n\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t<LinkOff :size=\"16\" />\n\t\t\t\t\t</template>\n\t\t\t\t</NcButton>\n\t\t\t</div>\n\t\t</div>\n\n\t\t<CnPhotoAlbumPicker\n\t\t\tv-if=\"pickerOpen\"\n\t\t\t:apiBase=\"apiBase\"\n\t\t\t@close=\"pickerOpen = false\"\n\t\t\t@link=\"onLinkPick\" />\n\n\t\t<CnPhotoAlbumCreate\n\t\t\tv-if=\"createOpen\"\n\t\t\t@close=\"createOpen = false\"\n\t\t\t@create=\"onCreatePick\" />\n\t</div>\n</template>\n\n<script>\nimport { translatePlural as n, translate as t } from '@nextcloud/l10n'\nimport { NcButton, NcLoadingIcon } from '@nextcloud/vue'\nimport AlertCircleOutline from 'vue-material-design-icons/AlertCircleOutline.vue'\nimport ImageIcon from 'vue-material-design-icons/Image.vue'\nimport ImageMultiple from 'vue-material-design-icons/ImageMultiple.vue'\nimport LinkOff from 'vue-material-design-icons/LinkOff.vue'\nimport LinkVariant from 'vue-material-design-icons/LinkVariant.vue'\nimport Plus from 'vue-material-design-icons/Plus.vue'\nimport CnPhotoAlbumCreate from '../../../components/CnPhotoAlbumCreate/CnPhotoAlbumCreate.vue'\nimport CnPhotoAlbumPicker from '../../../components/CnPhotoAlbumPicker/CnPhotoAlbumPicker.vue'\nimport { buildHeaders, prefixUrl, safeHref } from '../../../utils/index.js'\n\n/**\n * CnPhotosTab — bespoke album grid for the `photos` integration.\n *\n * Renders one tile per linked NC Photos album with cover thumbnail,\n * album name, photo count, and last-edited date. Clicking a tile opens\n * the album in the NC Photos app in a new tab. Tier-2: includes\n * link/create modals and per-tile unlink.\n */\nexport default {\n\tname: 'CnPhotosTab',\n\n\tcomponents: {\n\t\tNcButton,\n\t\tNcLoadingIcon,\n\t\tAlertCircleOutline,\n\t\tImageIcon,\n\t\tImageMultiple,\n\t\tLinkOff,\n\t\tLinkVariant,\n\t\tPlus,\n\t\tCnPhotoAlbumPicker,\n\t\tCnPhotoAlbumCreate,\n\t},\n\n\tprops: {\n\t\t/* eslint-disable vue/no-unused-properties -- the integration dispatch binds integrationId on every integration component (see CnIntegrationWidgetGrid), so declaring it keeps it out of $attrs */\n\t\t/** Stable integration id (forwarded from the registry — always `'photos'`). */\n\t\tintegrationId: { type: String, default: 'photos' },\n\t\t/* eslint-enable vue/no-unused-properties */\n\t\t/** Parent object id. */\n\t\tobjectId: { type: String, required: true },\n\t\t/** OpenRegister register id (slug or uuid). */\n\t\tregister: { type: String, default: '' },\n\t\t/** OpenRegister schema id (slug or uuid). */\n\t\tschema: { type: String, default: '' },\n\t\t/** Base API URL. */\n\t\tapiBase: { type: String, default: '/apps/openregister/api' },\n\t\t/** Pre-translated empty-state label. */\n\t\temptyLabel: { type: String, default: () => t('nextcloud-vue', 'No albums linked yet') },\n\t\t/** Pre-translated \"Open Photos\" CTA label. */\n\t\topenPhotosLabel: { type: String, default: () => t('nextcloud-vue', 'Open Photos') },\n\t\t/** Pre-translated 503 unavailable banner copy. */\n\t\tunavailableLabel: { type: String, default: () => t('nextcloud-vue', 'NC Photos is currently unavailable.') },\n\t\t/** URL of the NC Photos app entry. */\n\t\tphotosAppUrl: { type: String, default: '/index.php/apps/photos' },\n\t},\n\n\tdata() {\n\t\treturn {\n\t\t\talbums: [],\n\t\t\tloading: false,\n\t\t\terror: '',\n\t\t\tdegraded: '',\n\t\t\tbrokenCovers: {},\n\t\t\tpickerOpen: false,\n\t\t\tcreateOpen: false,\n\t\t}\n\t},\n\n\twatch: {\n\t\tobjectId: { immediate: true, handler(id) {\n\t\t\tif (id) {\n\t\t\t\tthis.fetchAlbums()\n\t\t\t}\n\t\t} },\n\n\t\tregister() {\n\t\t\tthis.fetchAlbums()\n\t\t},\n\n\t\tschema() {\n\t\t\tthis.fetchAlbums()\n\t\t},\n\t},\n\n\tmethods: {\n\t\tt,\n\n\t\t/**\n\t\t * Base for the Tier-2 photos endpoints (list/link/new/destroy).\n\t\t *\n\t\t * @return {string} The endpoint URL.\n\t\t */\n\t\tphotosEndpoint() {\n\t\t\treturn `${this.apiBase}/objects/${this.register}/${this.schema}/${this.objectId}/photos`\n\t\t},\n\n\t\talbumKey(album) {\n\t\t\treturn (album && (album.albumId ?? album.id ?? album.data?.album_id)) || ''\n\t\t},\n\n\t\t/**\n\t\t * Resolve the displayable album name from the row, stripping any\n\t\t * legacy `[or:{uuid}]` marker (pre-Tier-2 albums embedded the\n\t\t * marker inside `name`).\n\t\t *\n\t\t * @param {object} album Provider row.\n\t\t *\n\t\t * @return {string} The display name.\n\t\t */\n\t\talbumName(album) {\n\t\t\tconst raw = (album && (album.albumName || album.title || album.name || album.data?.name)) || ''\n\t\t\treturn String(raw).replace(/\\s*\\[or:[^\\]]+\\]\\s*/g, ' ').trim() || t('nextcloud-vue', 'Untitled album')\n\t\t},\n\n\t\talbumUrl(album) {\n\t\t\tif (album && album.url && safeHref(album.url) !== '#') {\n\t\t\t\treturn album.url\n\t\t\t}\n\t\t\tconst id = this.albumKey(album)\n\t\t\treturn `/index.php/apps/photos/albums/${encodeURIComponent(id)}`\n\t\t},\n\n\t\tcoverUrl(album) {\n\t\t\tif (!album || this.brokenCovers[this.albumKey(album)]) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn album.coverPhotoUrl || album.data?.coverPhotoUrl || ''\n\t\t},\n\n\t\tonCoverError(album) {\n\t\t\tthis.brokenCovers[this.albumKey(album)] = true\n\t\t},\n\n\t\tphotoCount(album) {\n\t\t\tconst raw = album?.photoCount ?? album?.data?.photoCount ?? album?.data?.count\n\t\t\tif (raw === null || raw === undefined || raw === '') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst num = Number(raw)\n\t\t\treturn Number.isFinite(num) ? num : null\n\t\t},\n\n\t\tphotoCountLabel(album) {\n\t\t\tconst count = this.photoCount(album)\n\t\t\tif (count === null) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn n('nextcloud-vue', '{count} photo', '{count} photos', count, { count })\n\t\t},\n\n\t\tlastEdited(album) {\n\t\t\treturn album?.lastEdited\n\t\t\t\t|| album?.data?.lastEdited\n\t\t\t\t|| album?.data?.created\n\t\t\t\t|| ''\n\t\t},\n\n\t\tformatWhen(value) {\n\t\t\tif (!value) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\ttry {\n\t\t\t\t// NC Photos stores `created` as a unix timestamp in\n\t\t\t\t// seconds; accept ISO strings transparently too.\n\t\t\t\tconst num = Number(value)\n\t\t\t\tconst d = Number.isFinite(num) && num > 0 && String(value).length <= 12\n\t\t\t\t\t? new Date(num * 1000)\n\t\t\t\t\t: new Date(value)\n\t\t\t\tif (Number.isNaN(d.getTime())) {\n\t\t\t\t\treturn String(value)\n\t\t\t\t}\n\t\t\t\treturn d.toLocaleDateString(undefined, { dateStyle: 'medium' })\n\t\t\t} catch {\n\t\t\t\treturn String(value)\n\t\t\t}\n\t\t},\n\n\t\topenPicker() {\n\t\t\tthis.pickerOpen = true\n\t\t},\n\n\t\topenCreate() {\n\t\t\tthis.createOpen = true\n\t\t},\n\n\t\tasync onLinkPick(payload) {\n\t\t\tthis.pickerOpen = false\n\t\t\ttry {\n\t\t\t\tconst response = await fetch(prefixUrl(this.photosEndpoint()), {\n\t\t\t\t\tmethod: 'POST',\n\t\t\t\t\theaders: { ...buildHeaders(), 'Content-Type': 'application/json' },\n\t\t\t\t\tbody: JSON.stringify(payload),\n\t\t\t\t})\n\t\t\t\tif (response.ok) {\n\t\t\t\t\tawait this.fetchAlbums()\n\t\t\t\t} else if (response.status === 409) {\n\t\t\t\t\tthis.error = t('nextcloud-vue', 'This album is already linked.')\n\t\t\t\t} else {\n\t\t\t\t\tthis.error = t('nextcloud-vue', 'Could not link album.')\n\t\t\t\t}\n\t\t\t} catch (err) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.error('[CnPhotosTab] link failed', err)\n\t\t\t\tthis.error = t('nextcloud-vue', 'Could not link album.')\n\t\t\t}\n\t\t},\n\n\t\tasync onCreatePick(payload) {\n\t\t\tthis.createOpen = false\n\t\t\ttry {\n\t\t\t\tconst response = await fetch(prefixUrl(`${this.photosEndpoint()}/new`), {\n\t\t\t\t\tmethod: 'POST',\n\t\t\t\t\theaders: { ...buildHeaders(), 'Content-Type': 'application/json' },\n\t\t\t\t\tbody: JSON.stringify(payload),\n\t\t\t\t})\n\t\t\t\tif (response.ok) {\n\t\t\t\t\tawait this.fetchAlbums()\n\t\t\t\t} else {\n\t\t\t\t\tthis.error = t('nextcloud-vue', 'Could not create album.')\n\t\t\t\t}\n\t\t\t} catch (err) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.error('[CnPhotosTab] create failed', err)\n\t\t\t\tthis.error = t('nextcloud-vue', 'Could not create album.')\n\t\t\t}\n\t\t},\n\n\t\tasync unlinkAlbum(album) {\n\t\t\tconst albumId = this.albumKey(album)\n\t\t\tif (!albumId) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tconst response = await fetch(prefixUrl(`${this.photosEndpoint()}/${albumId}`), {\n\t\t\t\t\tmethod: 'DELETE',\n\t\t\t\t\theaders: buildHeaders(),\n\t\t\t\t})\n\t\t\t\tif (response.ok) {\n\t\t\t\t\tawait this.fetchAlbums()\n\t\t\t\t} else {\n\t\t\t\t\tthis.error = t('nextcloud-vue', 'Could not unlink album.')\n\t\t\t\t}\n\t\t\t} catch (err) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.error('[CnPhotosTab] unlink failed', err)\n\t\t\t\tthis.error = t('nextcloud-vue', 'Could not unlink album.')\n\t\t\t}\n\t\t},\n\n\t\tasync fetchAlbums() {\n\t\t\tif (!this.register || !this.schema || !this.objectId) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.loading = true\n\t\t\tthis.error = ''\n\t\t\tthis.degraded = ''\n\t\t\ttry {\n\t\t\t\tconst response = await fetch(prefixUrl(this.photosEndpoint()), { headers: buildHeaders() })\n\t\t\t\tif (response.ok) {\n\t\t\t\t\tconst data = await response.json()\n\t\t\t\t\tthis.albums = data.results || data.items || (Array.isArray(data) ? data : []) || []\n\t\t\t\t} else if (response.status === 503 || response.status === 501) {\n\t\t\t\t\tthis.albums = []\n\t\t\t\t\tthis.degraded = this.unavailableLabel\n\t\t\t\t} else {\n\t\t\t\t\tthis.albums = []\n\t\t\t\t\tthis.error = t('nextcloud-vue', 'Could not load albums.')\n\t\t\t\t}\n\t\t\t} catch (err) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.error('[CnPhotosTab] failed to fetch albums', err)\n\t\t\t\tthis.albums = []\n\t\t\t\tthis.error = t('nextcloud-vue', 'Could not load albums.')\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-photos-tab {\n\tpadding: 12px;\n\toverflow-x: hidden;\n}\n\n.cn-photos-tab__actions {\n\tdisplay: flex;\n\tgap: 8px;\n\tmargin-bottom: 10px;\n\tflex-wrap: wrap;\n}\n\n.cn-photos-tab__banner {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 8px;\n\tpadding: 8px 10px;\n\tmargin-bottom: 10px;\n\tborder-radius: var(--border-radius);\n\tbackground: var(--color-warning, #e9a40f);\n\tcolor: var(--color-main-background);\n\tfont-size: 0.9em;\n}\n\n.cn-photos-tab__error {\n\tcolor: var(--color-error);\n\tfont-size: 0.9em;\n\tmargin: 4px 0 8px;\n}\n\n.cn-photos-tab__empty {\n\tdisplay: flex;\n\tflex-direction: column;\n\talign-items: center;\n\tgap: 8px;\n\tpadding: 16px 8px;\n\tcolor: var(--color-text-maxcontrast);\n\ttext-align: center;\n}\n\n.cn-photos-tab__empty-icon {\n\tcolor: var(--color-text-maxcontrast);\n}\n\n/*\n * Dense edge-to-edge mosaic that mirrors the NC Photos timeline: square\n * thumbnails packed tight with a thin main-background gutter (the same\n * 2px border NC Photos draws between tiles), name + date revealed in a\n * bottom scrim on hover/focus so the imagery dominates at rest.\n */\n.cn-photos-tab__grid {\n\tdisplay: grid;\n\tgrid-template-columns: repeat(auto-fill, minmax(104px, 1fr));\n\tgap: 2px;\n\tborder-radius: var(--border-radius-large, var(--border-radius));\n\toverflow: hidden;\n}\n\n.cn-photos-tab__tile {\n\tposition: relative;\n}\n\n.cn-photos-tab__unlink {\n\tposition: absolute;\n\ttop: 4px;\n\tright: 4px;\n\tz-index: 3;\n\topacity: 0;\n\tbackground: var(--color-main-background);\n\tborder-radius: 50%;\n\ttransition: opacity 0.15s ease;\n}\n\n.cn-photos-tab__tile:hover .cn-photos-tab__unlink,\n.cn-photos-tab__tile:focus-within .cn-photos-tab__unlink {\n\topacity: 1;\n}\n\n.cn-photos-tab__tile-link {\n\tdisplay: block;\n\ttext-decoration: none;\n\tcolor: inherit;\n}\n\n.cn-photos-tab__cover {\n\tposition: relative;\n\taspect-ratio: 1 / 1;\n\twidth: 100%;\n\t/* Match the NC Photos selection outline on focus. */\n\tborder: 2px solid var(--color-main-background);\n\tbox-sizing: border-box;\n\tbackground: var(--color-primary-element-light, var(--color-background-darker));\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: center;\n\toverflow: hidden;\n}\n\n.cn-photos-tab__tile-link:focus-visible .cn-photos-tab__cover {\n\toutline: var(--color-primary-element) solid 2px;\n\toutline-offset: -2px;\n}\n\n.cn-photos-tab__cover-img {\n\twidth: 100%;\n\theight: 100%;\n\tobject-fit: cover;\n\tdisplay: block;\n\ttransition: transform 0.2s ease;\n}\n\n.cn-photos-tab__tile:hover .cn-photos-tab__cover-img,\n.cn-photos-tab__tile:focus-within .cn-photos-tab__cover-img {\n\ttransform: scale(1.04);\n}\n\n.cn-photos-tab__cover-placeholder {\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: center;\n\twidth: 100%;\n\theight: 100%;\n}\n\n.cn-photos-tab__cover-fallback {\n\tcolor: var(--color-primary-element);\n}\n\n/* Photo-count badge (mirrors the album stack count overlay). */\n.cn-photos-tab__badge {\n\tposition: absolute;\n\ttop: 4px;\n\tleft: 4px;\n\tz-index: 2;\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 3px;\n\tpadding: 1px 6px 1px 4px;\n\tborder-radius: var(--border-radius-pill, 12px);\n\tbackground: rgba(0, 0, 0, 0.55);\n\tcolor: #fff;\n\tfont-size: 11px;\n\tfont-weight: 500;\n\tline-height: 1.6;\n}\n\n.cn-photos-tab__badge-count {\n\twhite-space: nowrap;\n}\n\n/* Bottom scrim: hidden at rest, revealed on hover/focus. */\n.cn-photos-tab__meta {\n\tposition: absolute;\n\tleft: 0;\n\tright: 0;\n\tbottom: 0;\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 1px;\n\tpadding: 16px 8px 6px;\n\tbackground: linear-gradient(to top, rgba(0, 0, 0, 0.7), rgba(0, 0, 0, 0));\n\topacity: 0;\n\ttransition: opacity 0.15s ease;\n\tpointer-events: none;\n}\n\n.cn-photos-tab__tile:hover .cn-photos-tab__meta,\n.cn-photos-tab__tile:focus-within .cn-photos-tab__meta {\n\topacity: 1;\n}\n\n.cn-photos-tab__name {\n\tfont-size: 12px;\n\tfont-weight: 600;\n\tcolor: #fff;\n\twhite-space: nowrap;\n\toverflow: hidden;\n\ttext-overflow: ellipsis;\n}\n\n.cn-photos-tab__when {\n\tfont-size: 10px;\n\tcolor: rgba(255, 255, 255, 0.85);\n\twhite-space: nowrap;\n\toverflow: hidden;\n\ttext-overflow: ellipsis;\n}\n</style>\n"],"names":["CnPhotoAlbumPicker","CnPhotoAlbumCreate","t","n"],"mappings":"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"}
|
|
@@ -98,8 +98,7 @@ function render(_ctx, _cache, $props, $setup, $data, $options) {
|
|
|
98
98
|
createVNode(_component_NcButton, {
|
|
99
99
|
variant: "primary",
|
|
100
100
|
href: $props.photosAppUrl,
|
|
101
|
-
target: "_blank"
|
|
102
|
-
rel: "noopener noreferrer"
|
|
101
|
+
target: "_blank"
|
|
103
102
|
}, {
|
|
104
103
|
icon: withCtx(() => [
|
|
105
104
|
createVNode(_component_ImageIcon, { size: 20 })
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CnPhotosTab.vue3.js","sources":["../../../../../src/integrations/builtin/photos/CnPhotosTab.vue?vue&type=template&id=7921c8e4&lang.js"],"sourcesContent":["<!--\n CnPhotosTab — bespoke sidebar tab for the `photos` integration leaf.\n\n Replaces the generic `CnIntegrationTab` for the `photos` leaf: renders\n the linked NC Photos albums as a card grid (cover thumbnail, album\n name, photo count, last-edited date). Clicking a card deep-links into\n the NC Photos app at `/index.php/apps/photos/albums/{albumId}`.\n\n Tier-2 surface (this commit):\n - \"Link existing album\" → opens CnPhotoAlbumPicker (modal)\n - \"Create new album\" → opens CnPhotoAlbumCreate (modal)\n - Per-tile unlink → DELETE …/photos/{albumId}\n\n Talks to the OpenRegister Tier-2 photo-link endpoints\n GET /api/objects/{r}/{s}/{id}/photos — list\n POST /api/objects/{r}/{s}/{id}/photos — link existing\n POST /api/objects/{r}/{s}/{id}/photos/new — create + link\n DELETE /api/objects/{r}/{s}/{id}/photos/{albumId} — unlink\n GET /api/integrations/photos/available — picker source\n served by `OCA\\OpenRegister\\Controller\\PhotoLinksController`.\n\n The endpoint returns rows shaped:\n { id, title, url, albumId, albumName, coverPhotoUrl, photoCount,\n lastEdited, data: { … } }\n\n Empty + loading + error + 503 unavailable states follow ADR-017 and\n AD-23 graceful degradation. All UI strings are passed through\n `t('nextcloud-vue', ...)` per ADR-007. Styling uses Nextcloud CSS\n variables only so the nldesign overrides apply transparently\n (ADR-010).\n\n Bespoke-vs-generic rationale: the generic tab renders a flat link list\n which loses the cover thumbnail + photo count + recency — the three\n signals users rely on when triaging which album is relevant for a\n case. The bespoke tab surfaces them per card.\n-->\n<template>\n\t<div class=\"cn-sidebar-tab cn-photos-tab\">\n\t\t<!-- 503 unavailable banner (AD-23) -->\n\t\t<div v-if=\"degraded\" class=\"cn-photos-tab__banner\" role=\"alert\">\n\t\t\t<AlertCircleOutline :size=\"18\" />\n\t\t\t<span>{{ degraded }}</span>\n\t\t</div>\n\n\t\t<div class=\"cn-photos-tab__actions\">\n\t\t\t<NcButton variant=\"secondary\" @click=\"openPicker\">\n\t\t\t\t<template #icon>\n\t\t\t\t\t<LinkVariant :size=\"18\" />\n\t\t\t\t</template>\n\t\t\t\t{{ t('nextcloud-vue', 'Link existing album') }}\n\t\t\t</NcButton>\n\t\t\t<NcButton variant=\"primary\" @click=\"openCreate\">\n\t\t\t\t<template #icon>\n\t\t\t\t\t<Plus :size=\"18\" />\n\t\t\t\t</template>\n\t\t\t\t{{ t('nextcloud-vue', 'Create new album') }}\n\t\t\t</NcButton>\n\t\t</div>\n\n\t\t<!-- Loading -->\n\t\t<NcLoadingIcon v-if=\"loading\" />\n\n\t\t<!-- Error -->\n\t\t<div v-else-if=\"error\" class=\"cn-photos-tab__error\" role=\"alert\">\n\t\t\t{{ error }}\n\t\t</div>\n\n\t\t<!-- Empty -->\n\t\t<div v-else-if=\"albums.length === 0\" class=\"cn-sidebar-tab__empty cn-photos-tab__empty\">\n\t\t\t<ImageIcon :size=\"32\" class=\"cn-photos-tab__empty-icon\" />\n\t\t\t<p>{{ emptyLabel }}</p>\n\t\t\t<NcButton\n\t\t\t\tvariant=\"primary\"\n\t\t\t\t:href=\"photosAppUrl\"\n\t\t\t\ttarget=\"_blank\"\n\t\t\t\trel=\"noopener noreferrer\">\n\t\t\t\t<template #icon>\n\t\t\t\t\t<ImageIcon :size=\"20\" />\n\t\t\t\t</template>\n\t\t\t\t{{ openPhotosLabel }}\n\t\t\t</NcButton>\n\t\t</div>\n\n\t\t<!-- Photo mosaic grid (mirrors the NC Photos timeline) -->\n\t\t<div v-else class=\"cn-photos-tab__grid\">\n\t\t\t<div\n\t\t\t\tv-for=\"album in albums\"\n\t\t\t\t:key=\"albumKey(album)\"\n\t\t\t\tclass=\"cn-photos-tab__tile\">\n\t\t\t\t<a\n\t\t\t\t\t:href=\"albumUrl(album)\"\n\t\t\t\t\tclass=\"cn-photos-tab__tile-link\"\n\t\t\t\t\ttarget=\"_blank\"\n\t\t\t\t\trel=\"noopener noreferrer\"\n\t\t\t\t\t:title=\"albumName(album)\">\n\t\t\t\t\t<div class=\"cn-photos-tab__cover\">\n\t\t\t\t\t\t<img\n\t\t\t\t\t\t\tv-if=\"coverUrl(album)\"\n\t\t\t\t\t\t\t:src=\"coverUrl(album)\"\n\t\t\t\t\t\t\t:alt=\"albumName(album)\"\n\t\t\t\t\t\t\tclass=\"cn-photos-tab__cover-img\"\n\t\t\t\t\t\t\tloading=\"lazy\"\n\t\t\t\t\t\t\t@error=\"onCoverError(album)\">\n\t\t\t\t\t\t<span v-else class=\"cn-photos-tab__cover-placeholder\">\n\t\t\t\t\t\t\t<ImageMultiple :size=\"36\" class=\"cn-photos-tab__cover-fallback\" />\n\t\t\t\t\t\t</span>\n\t\t\t\t\t\t<!-- Photo-count badge, top-left (mirrors album stack overlay) -->\n\t\t\t\t\t\t<span v-if=\"photoCount(album) !== null\" class=\"cn-photos-tab__badge\">\n\t\t\t\t\t\t\t<ImageMultiple :size=\"12\" />\n\t\t\t\t\t\t\t<span class=\"cn-photos-tab__badge-count\">{{ photoCountLabel(album) }}</span>\n\t\t\t\t\t\t</span>\n\t\t\t\t\t\t<!-- Hover scrim with name + date -->\n\t\t\t\t\t\t<div class=\"cn-photos-tab__meta\">\n\t\t\t\t\t\t\t<span class=\"cn-photos-tab__name\">{{ albumName(album) }}</span>\n\t\t\t\t\t\t\t<span v-if=\"lastEdited(album)\" class=\"cn-photos-tab__when\">\n\t\t\t\t\t\t\t\t{{ formatWhen(lastEdited(album)) }}\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</a>\n\t\t\t\t<NcButton\n\t\t\t\t\tvariant=\"tertiary-no-background\"\n\t\t\t\t\t:aria-label=\"t('nextcloud-vue', 'Unlink album')\"\n\t\t\t\t\tclass=\"cn-photos-tab__unlink\"\n\t\t\t\t\t@click=\"unlinkAlbum(album)\">\n\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t<LinkOff :size=\"16\" />\n\t\t\t\t\t</template>\n\t\t\t\t</NcButton>\n\t\t\t</div>\n\t\t</div>\n\n\t\t<CnPhotoAlbumPicker\n\t\t\tv-if=\"pickerOpen\"\n\t\t\t:apiBase=\"apiBase\"\n\t\t\t@close=\"pickerOpen = false\"\n\t\t\t@link=\"onLinkPick\" />\n\n\t\t<CnPhotoAlbumCreate\n\t\t\tv-if=\"createOpen\"\n\t\t\t@close=\"createOpen = false\"\n\t\t\t@create=\"onCreatePick\" />\n\t</div>\n</template>\n\n<script>\nimport { translatePlural as n, translate as t } from '@nextcloud/l10n'\nimport { NcButton, NcLoadingIcon } from '@nextcloud/vue'\nimport AlertCircleOutline from 'vue-material-design-icons/AlertCircleOutline.vue'\nimport ImageIcon from 'vue-material-design-icons/Image.vue'\nimport ImageMultiple from 'vue-material-design-icons/ImageMultiple.vue'\nimport LinkOff from 'vue-material-design-icons/LinkOff.vue'\nimport LinkVariant from 'vue-material-design-icons/LinkVariant.vue'\nimport Plus from 'vue-material-design-icons/Plus.vue'\nimport CnPhotoAlbumCreate from '../../../components/CnPhotoAlbumCreate/CnPhotoAlbumCreate.vue'\nimport CnPhotoAlbumPicker from '../../../components/CnPhotoAlbumPicker/CnPhotoAlbumPicker.vue'\nimport { buildHeaders, prefixUrl } from '../../../utils/index.js'\n\n/**\n * CnPhotosTab — bespoke album grid for the `photos` integration.\n *\n * Renders one tile per linked NC Photos album with cover thumbnail,\n * album name, photo count, and last-edited date. Clicking a tile opens\n * the album in the NC Photos app in a new tab. Tier-2: includes\n * link/create modals and per-tile unlink.\n */\nexport default {\n\tname: 'CnPhotosTab',\n\n\tcomponents: {\n\t\tNcButton,\n\t\tNcLoadingIcon,\n\t\tAlertCircleOutline,\n\t\tImageIcon,\n\t\tImageMultiple,\n\t\tLinkOff,\n\t\tLinkVariant,\n\t\tPlus,\n\t\tCnPhotoAlbumPicker,\n\t\tCnPhotoAlbumCreate,\n\t},\n\n\tprops: {\n\t\t/* eslint-disable vue/no-unused-properties -- the integration dispatch binds integrationId on every integration component (see CnIntegrationWidgetGrid), so declaring it keeps it out of $attrs */\n\t\t/** Stable integration id (forwarded from the registry — always `'photos'`). */\n\t\tintegrationId: { type: String, default: 'photos' },\n\t\t/* eslint-enable vue/no-unused-properties */\n\t\t/** Parent object id. */\n\t\tobjectId: { type: String, required: true },\n\t\t/** OpenRegister register id (slug or uuid). */\n\t\tregister: { type: String, default: '' },\n\t\t/** OpenRegister schema id (slug or uuid). */\n\t\tschema: { type: String, default: '' },\n\t\t/** Base API URL. */\n\t\tapiBase: { type: String, default: '/apps/openregister/api' },\n\t\t/** Pre-translated empty-state label. */\n\t\temptyLabel: { type: String, default: () => t('nextcloud-vue', 'No albums linked yet') },\n\t\t/** Pre-translated \"Open Photos\" CTA label. */\n\t\topenPhotosLabel: { type: String, default: () => t('nextcloud-vue', 'Open Photos') },\n\t\t/** Pre-translated 503 unavailable banner copy. */\n\t\tunavailableLabel: { type: String, default: () => t('nextcloud-vue', 'NC Photos is currently unavailable.') },\n\t\t/** URL of the NC Photos app entry. */\n\t\tphotosAppUrl: { type: String, default: '/index.php/apps/photos' },\n\t},\n\n\tdata() {\n\t\treturn {\n\t\t\talbums: [],\n\t\t\tloading: false,\n\t\t\terror: '',\n\t\t\tdegraded: '',\n\t\t\tbrokenCovers: {},\n\t\t\tpickerOpen: false,\n\t\t\tcreateOpen: false,\n\t\t}\n\t},\n\n\twatch: {\n\t\tobjectId: { immediate: true, handler(id) {\n\t\t\tif (id) {\n\t\t\t\tthis.fetchAlbums()\n\t\t\t}\n\t\t} },\n\n\t\tregister() {\n\t\t\tthis.fetchAlbums()\n\t\t},\n\n\t\tschema() {\n\t\t\tthis.fetchAlbums()\n\t\t},\n\t},\n\n\tmethods: {\n\t\tt,\n\n\t\t/**\n\t\t * Base for the Tier-2 photos endpoints (list/link/new/destroy).\n\t\t *\n\t\t * @return {string} The endpoint URL.\n\t\t */\n\t\tphotosEndpoint() {\n\t\t\treturn `${this.apiBase}/objects/${this.register}/${this.schema}/${this.objectId}/photos`\n\t\t},\n\n\t\talbumKey(album) {\n\t\t\treturn (album && (album.albumId ?? album.id ?? album.data?.album_id)) || ''\n\t\t},\n\n\t\t/**\n\t\t * Resolve the displayable album name from the row, stripping any\n\t\t * legacy `[or:{uuid}]` marker (pre-Tier-2 albums embedded the\n\t\t * marker inside `name`).\n\t\t *\n\t\t * @param {object} album Provider row.\n\t\t *\n\t\t * @return {string} The display name.\n\t\t */\n\t\talbumName(album) {\n\t\t\tconst raw = (album && (album.albumName || album.title || album.name || album.data?.name)) || ''\n\t\t\treturn String(raw).replace(/\\s*\\[or:[^\\]]+\\]\\s*/g, ' ').trim() || t('nextcloud-vue', 'Untitled album')\n\t\t},\n\n\t\talbumUrl(album) {\n\t\t\tif (album && album.url) {\n\t\t\t\treturn album.url\n\t\t\t}\n\t\t\tconst id = this.albumKey(album)\n\t\t\treturn `/index.php/apps/photos/albums/${encodeURIComponent(id)}`\n\t\t},\n\n\t\tcoverUrl(album) {\n\t\t\tif (!album || this.brokenCovers[this.albumKey(album)]) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn album.coverPhotoUrl || album.data?.coverPhotoUrl || ''\n\t\t},\n\n\t\tonCoverError(album) {\n\t\t\tthis.brokenCovers[this.albumKey(album)] = true\n\t\t},\n\n\t\tphotoCount(album) {\n\t\t\tconst raw = album?.photoCount ?? album?.data?.photoCount ?? album?.data?.count\n\t\t\tif (raw === null || raw === undefined || raw === '') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst num = Number(raw)\n\t\t\treturn Number.isFinite(num) ? num : null\n\t\t},\n\n\t\tphotoCountLabel(album) {\n\t\t\tconst count = this.photoCount(album)\n\t\t\tif (count === null) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn n('nextcloud-vue', '{count} photo', '{count} photos', count, { count })\n\t\t},\n\n\t\tlastEdited(album) {\n\t\t\treturn album?.lastEdited\n\t\t\t\t|| album?.data?.lastEdited\n\t\t\t\t|| album?.data?.created\n\t\t\t\t|| ''\n\t\t},\n\n\t\tformatWhen(value) {\n\t\t\tif (!value) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\ttry {\n\t\t\t\t// NC Photos stores `created` as a unix timestamp in\n\t\t\t\t// seconds; accept ISO strings transparently too.\n\t\t\t\tconst num = Number(value)\n\t\t\t\tconst d = Number.isFinite(num) && num > 0 && String(value).length <= 12\n\t\t\t\t\t? new Date(num * 1000)\n\t\t\t\t\t: new Date(value)\n\t\t\t\tif (Number.isNaN(d.getTime())) {\n\t\t\t\t\treturn String(value)\n\t\t\t\t}\n\t\t\t\treturn d.toLocaleDateString(undefined, { dateStyle: 'medium' })\n\t\t\t} catch {\n\t\t\t\treturn String(value)\n\t\t\t}\n\t\t},\n\n\t\topenPicker() {\n\t\t\tthis.pickerOpen = true\n\t\t},\n\n\t\topenCreate() {\n\t\t\tthis.createOpen = true\n\t\t},\n\n\t\tasync onLinkPick(payload) {\n\t\t\tthis.pickerOpen = false\n\t\t\ttry {\n\t\t\t\tconst response = await fetch(prefixUrl(this.photosEndpoint()), {\n\t\t\t\t\tmethod: 'POST',\n\t\t\t\t\theaders: { ...buildHeaders(), 'Content-Type': 'application/json' },\n\t\t\t\t\tbody: JSON.stringify(payload),\n\t\t\t\t})\n\t\t\t\tif (response.ok) {\n\t\t\t\t\tawait this.fetchAlbums()\n\t\t\t\t} else if (response.status === 409) {\n\t\t\t\t\tthis.error = t('nextcloud-vue', 'This album is already linked.')\n\t\t\t\t} else {\n\t\t\t\t\tthis.error = t('nextcloud-vue', 'Could not link album.')\n\t\t\t\t}\n\t\t\t} catch (err) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.error('[CnPhotosTab] link failed', err)\n\t\t\t\tthis.error = t('nextcloud-vue', 'Could not link album.')\n\t\t\t}\n\t\t},\n\n\t\tasync onCreatePick(payload) {\n\t\t\tthis.createOpen = false\n\t\t\ttry {\n\t\t\t\tconst response = await fetch(prefixUrl(`${this.photosEndpoint()}/new`), {\n\t\t\t\t\tmethod: 'POST',\n\t\t\t\t\theaders: { ...buildHeaders(), 'Content-Type': 'application/json' },\n\t\t\t\t\tbody: JSON.stringify(payload),\n\t\t\t\t})\n\t\t\t\tif (response.ok) {\n\t\t\t\t\tawait this.fetchAlbums()\n\t\t\t\t} else {\n\t\t\t\t\tthis.error = t('nextcloud-vue', 'Could not create album.')\n\t\t\t\t}\n\t\t\t} catch (err) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.error('[CnPhotosTab] create failed', err)\n\t\t\t\tthis.error = t('nextcloud-vue', 'Could not create album.')\n\t\t\t}\n\t\t},\n\n\t\tasync unlinkAlbum(album) {\n\t\t\tconst albumId = this.albumKey(album)\n\t\t\tif (!albumId) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tconst response = await fetch(prefixUrl(`${this.photosEndpoint()}/${albumId}`), {\n\t\t\t\t\tmethod: 'DELETE',\n\t\t\t\t\theaders: buildHeaders(),\n\t\t\t\t})\n\t\t\t\tif (response.ok) {\n\t\t\t\t\tawait this.fetchAlbums()\n\t\t\t\t} else {\n\t\t\t\t\tthis.error = t('nextcloud-vue', 'Could not unlink album.')\n\t\t\t\t}\n\t\t\t} catch (err) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.error('[CnPhotosTab] unlink failed', err)\n\t\t\t\tthis.error = t('nextcloud-vue', 'Could not unlink album.')\n\t\t\t}\n\t\t},\n\n\t\tasync fetchAlbums() {\n\t\t\tif (!this.register || !this.schema || !this.objectId) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.loading = true\n\t\t\tthis.error = ''\n\t\t\tthis.degraded = ''\n\t\t\ttry {\n\t\t\t\tconst response = await fetch(prefixUrl(this.photosEndpoint()), { headers: buildHeaders() })\n\t\t\t\tif (response.ok) {\n\t\t\t\t\tconst data = await response.json()\n\t\t\t\t\tthis.albums = data.results || data.items || (Array.isArray(data) ? data : []) || []\n\t\t\t\t} else if (response.status === 503 || response.status === 501) {\n\t\t\t\t\tthis.albums = []\n\t\t\t\t\tthis.degraded = this.unavailableLabel\n\t\t\t\t} else {\n\t\t\t\t\tthis.albums = []\n\t\t\t\t\tthis.error = t('nextcloud-vue', 'Could not load albums.')\n\t\t\t\t}\n\t\t\t} catch (err) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.error('[CnPhotosTab] failed to fetch albums', err)\n\t\t\t\tthis.albums = []\n\t\t\t\tthis.error = t('nextcloud-vue', 'Could not load albums.')\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-photos-tab {\n\tpadding: 12px;\n\toverflow-x: hidden;\n}\n\n.cn-photos-tab__actions {\n\tdisplay: flex;\n\tgap: 8px;\n\tmargin-bottom: 10px;\n\tflex-wrap: wrap;\n}\n\n.cn-photos-tab__banner {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 8px;\n\tpadding: 8px 10px;\n\tmargin-bottom: 10px;\n\tborder-radius: var(--border-radius);\n\tbackground: var(--color-warning, #e9a40f);\n\tcolor: var(--color-main-background);\n\tfont-size: 0.9em;\n}\n\n.cn-photos-tab__error {\n\tcolor: var(--color-error);\n\tfont-size: 0.9em;\n\tmargin: 4px 0 8px;\n}\n\n.cn-photos-tab__empty {\n\tdisplay: flex;\n\tflex-direction: column;\n\talign-items: center;\n\tgap: 8px;\n\tpadding: 16px 8px;\n\tcolor: var(--color-text-maxcontrast);\n\ttext-align: center;\n}\n\n.cn-photos-tab__empty-icon {\n\tcolor: var(--color-text-maxcontrast);\n}\n\n/*\n * Dense edge-to-edge mosaic that mirrors the NC Photos timeline: square\n * thumbnails packed tight with a thin main-background gutter (the same\n * 2px border NC Photos draws between tiles), name + date revealed in a\n * bottom scrim on hover/focus so the imagery dominates at rest.\n */\n.cn-photos-tab__grid {\n\tdisplay: grid;\n\tgrid-template-columns: repeat(auto-fill, minmax(104px, 1fr));\n\tgap: 2px;\n\tborder-radius: var(--border-radius-large, var(--border-radius));\n\toverflow: hidden;\n}\n\n.cn-photos-tab__tile {\n\tposition: relative;\n}\n\n.cn-photos-tab__unlink {\n\tposition: absolute;\n\ttop: 4px;\n\tright: 4px;\n\tz-index: 3;\n\topacity: 0;\n\tbackground: var(--color-main-background);\n\tborder-radius: 50%;\n\ttransition: opacity 0.15s ease;\n}\n\n.cn-photos-tab__tile:hover .cn-photos-tab__unlink,\n.cn-photos-tab__tile:focus-within .cn-photos-tab__unlink {\n\topacity: 1;\n}\n\n.cn-photos-tab__tile-link {\n\tdisplay: block;\n\ttext-decoration: none;\n\tcolor: inherit;\n}\n\n.cn-photos-tab__cover {\n\tposition: relative;\n\taspect-ratio: 1 / 1;\n\twidth: 100%;\n\t/* Match the NC Photos selection outline on focus. */\n\tborder: 2px solid var(--color-main-background);\n\tbox-sizing: border-box;\n\tbackground: var(--color-primary-element-light, var(--color-background-darker));\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: center;\n\toverflow: hidden;\n}\n\n.cn-photos-tab__tile-link:focus-visible .cn-photos-tab__cover {\n\toutline: var(--color-primary-element) solid 2px;\n\toutline-offset: -2px;\n}\n\n.cn-photos-tab__cover-img {\n\twidth: 100%;\n\theight: 100%;\n\tobject-fit: cover;\n\tdisplay: block;\n\ttransition: transform 0.2s ease;\n}\n\n.cn-photos-tab__tile:hover .cn-photos-tab__cover-img,\n.cn-photos-tab__tile:focus-within .cn-photos-tab__cover-img {\n\ttransform: scale(1.04);\n}\n\n.cn-photos-tab__cover-placeholder {\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: center;\n\twidth: 100%;\n\theight: 100%;\n}\n\n.cn-photos-tab__cover-fallback {\n\tcolor: var(--color-primary-element);\n}\n\n/* Photo-count badge (mirrors the album stack count overlay). */\n.cn-photos-tab__badge {\n\tposition: absolute;\n\ttop: 4px;\n\tleft: 4px;\n\tz-index: 2;\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 3px;\n\tpadding: 1px 6px 1px 4px;\n\tborder-radius: var(--border-radius-pill, 12px);\n\tbackground: rgba(0, 0, 0, 0.55);\n\tcolor: #fff;\n\tfont-size: 11px;\n\tfont-weight: 500;\n\tline-height: 1.6;\n}\n\n.cn-photos-tab__badge-count {\n\twhite-space: nowrap;\n}\n\n/* Bottom scrim: hidden at rest, revealed on hover/focus. */\n.cn-photos-tab__meta {\n\tposition: absolute;\n\tleft: 0;\n\tright: 0;\n\tbottom: 0;\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 1px;\n\tpadding: 16px 8px 6px;\n\tbackground: linear-gradient(to top, rgba(0, 0, 0, 0.7), rgba(0, 0, 0, 0));\n\topacity: 0;\n\ttransition: opacity 0.15s ease;\n\tpointer-events: none;\n}\n\n.cn-photos-tab__tile:hover .cn-photos-tab__meta,\n.cn-photos-tab__tile:focus-within .cn-photos-tab__meta {\n\topacity: 1;\n}\n\n.cn-photos-tab__name {\n\tfont-size: 12px;\n\tfont-weight: 600;\n\tcolor: #fff;\n\twhite-space: nowrap;\n\toverflow: hidden;\n\ttext-overflow: ellipsis;\n}\n\n.cn-photos-tab__when {\n\tfont-size: 10px;\n\tcolor: rgba(255, 255, 255, 0.85);\n\twhite-space: nowrap;\n\toverflow: hidden;\n\ttext-overflow: ellipsis;\n}\n</style>\n"],"names":["_openBlock","_createElementBlock","_createVNode","_createElementVNode","_createTextVNode","_createBlock","_toDisplayString"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"CnPhotosTab.vue3.js","sources":["../../../../../src/integrations/builtin/photos/CnPhotosTab.vue?vue&type=template&id=7921c8e4&lang.js"],"sourcesContent":["<!--\n CnPhotosTab — bespoke sidebar tab for the `photos` integration leaf.\n\n Replaces the generic `CnIntegrationTab` for the `photos` leaf: renders\n the linked NC Photos albums as a card grid (cover thumbnail, album\n name, photo count, last-edited date). Clicking a card deep-links into\n the NC Photos app at `/index.php/apps/photos/albums/{albumId}`.\n\n Tier-2 surface (this commit):\n - \"Link existing album\" → opens CnPhotoAlbumPicker (modal)\n - \"Create new album\" → opens CnPhotoAlbumCreate (modal)\n - Per-tile unlink → DELETE …/photos/{albumId}\n\n Talks to the OpenRegister Tier-2 photo-link endpoints\n GET /api/objects/{r}/{s}/{id}/photos — list\n POST /api/objects/{r}/{s}/{id}/photos — link existing\n POST /api/objects/{r}/{s}/{id}/photos/new — create + link\n DELETE /api/objects/{r}/{s}/{id}/photos/{albumId} — unlink\n GET /api/integrations/photos/available — picker source\n served by `OCA\\OpenRegister\\Controller\\PhotoLinksController`.\n\n The endpoint returns rows shaped:\n { id, title, url, albumId, albumName, coverPhotoUrl, photoCount,\n lastEdited, data: { … } }\n\n Empty + loading + error + 503 unavailable states follow ADR-017 and\n AD-23 graceful degradation. All UI strings are passed through\n `t('nextcloud-vue', ...)` per ADR-007. Styling uses Nextcloud CSS\n variables only so the nldesign overrides apply transparently\n (ADR-010).\n\n Bespoke-vs-generic rationale: the generic tab renders a flat link list\n which loses the cover thumbnail + photo count + recency — the three\n signals users rely on when triaging which album is relevant for a\n case. The bespoke tab surfaces them per card.\n-->\n<template>\n\t<div class=\"cn-sidebar-tab cn-photos-tab\">\n\t\t<!-- 503 unavailable banner (AD-23) -->\n\t\t<div v-if=\"degraded\" class=\"cn-photos-tab__banner\" role=\"alert\">\n\t\t\t<AlertCircleOutline :size=\"18\" />\n\t\t\t<span>{{ degraded }}</span>\n\t\t</div>\n\n\t\t<div class=\"cn-photos-tab__actions\">\n\t\t\t<NcButton variant=\"secondary\" @click=\"openPicker\">\n\t\t\t\t<template #icon>\n\t\t\t\t\t<LinkVariant :size=\"18\" />\n\t\t\t\t</template>\n\t\t\t\t{{ t('nextcloud-vue', 'Link existing album') }}\n\t\t\t</NcButton>\n\t\t\t<NcButton variant=\"primary\" @click=\"openCreate\">\n\t\t\t\t<template #icon>\n\t\t\t\t\t<Plus :size=\"18\" />\n\t\t\t\t</template>\n\t\t\t\t{{ t('nextcloud-vue', 'Create new album') }}\n\t\t\t</NcButton>\n\t\t</div>\n\n\t\t<!-- Loading -->\n\t\t<NcLoadingIcon v-if=\"loading\" />\n\n\t\t<!-- Error -->\n\t\t<div v-else-if=\"error\" class=\"cn-photos-tab__error\" role=\"alert\">\n\t\t\t{{ error }}\n\t\t</div>\n\n\t\t<!-- Empty -->\n\t\t<div v-else-if=\"albums.length === 0\" class=\"cn-sidebar-tab__empty cn-photos-tab__empty\">\n\t\t\t<ImageIcon :size=\"32\" class=\"cn-photos-tab__empty-icon\" />\n\t\t\t<p>{{ emptyLabel }}</p>\n\t\t\t<NcButton\n\t\t\t\tvariant=\"primary\"\n\t\t\t\t:href=\"photosAppUrl\"\n\t\t\t\ttarget=\"_blank\">\n\t\t\t\t<template #icon>\n\t\t\t\t\t<ImageIcon :size=\"20\" />\n\t\t\t\t</template>\n\t\t\t\t{{ openPhotosLabel }}\n\t\t\t</NcButton>\n\t\t</div>\n\n\t\t<!-- Photo mosaic grid (mirrors the NC Photos timeline) -->\n\t\t<div v-else class=\"cn-photos-tab__grid\">\n\t\t\t<div\n\t\t\t\tv-for=\"album in albums\"\n\t\t\t\t:key=\"albumKey(album)\"\n\t\t\t\tclass=\"cn-photos-tab__tile\">\n\t\t\t\t<a\n\t\t\t\t\t:href=\"albumUrl(album)\"\n\t\t\t\t\tclass=\"cn-photos-tab__tile-link\"\n\t\t\t\t\ttarget=\"_blank\"\n\t\t\t\t\trel=\"noopener noreferrer\"\n\t\t\t\t\t:title=\"albumName(album)\">\n\t\t\t\t\t<div class=\"cn-photos-tab__cover\">\n\t\t\t\t\t\t<img\n\t\t\t\t\t\t\tv-if=\"coverUrl(album)\"\n\t\t\t\t\t\t\t:src=\"coverUrl(album)\"\n\t\t\t\t\t\t\t:alt=\"albumName(album)\"\n\t\t\t\t\t\t\tclass=\"cn-photos-tab__cover-img\"\n\t\t\t\t\t\t\tloading=\"lazy\"\n\t\t\t\t\t\t\t@error=\"onCoverError(album)\">\n\t\t\t\t\t\t<span v-else class=\"cn-photos-tab__cover-placeholder\">\n\t\t\t\t\t\t\t<ImageMultiple :size=\"36\" class=\"cn-photos-tab__cover-fallback\" />\n\t\t\t\t\t\t</span>\n\t\t\t\t\t\t<!-- Photo-count badge, top-left (mirrors album stack overlay) -->\n\t\t\t\t\t\t<span v-if=\"photoCount(album) !== null\" class=\"cn-photos-tab__badge\">\n\t\t\t\t\t\t\t<ImageMultiple :size=\"12\" />\n\t\t\t\t\t\t\t<span class=\"cn-photos-tab__badge-count\">{{ photoCountLabel(album) }}</span>\n\t\t\t\t\t\t</span>\n\t\t\t\t\t\t<!-- Hover scrim with name + date -->\n\t\t\t\t\t\t<div class=\"cn-photos-tab__meta\">\n\t\t\t\t\t\t\t<span class=\"cn-photos-tab__name\">{{ albumName(album) }}</span>\n\t\t\t\t\t\t\t<span v-if=\"lastEdited(album)\" class=\"cn-photos-tab__when\">\n\t\t\t\t\t\t\t\t{{ formatWhen(lastEdited(album)) }}\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</a>\n\t\t\t\t<NcButton\n\t\t\t\t\tvariant=\"tertiary-no-background\"\n\t\t\t\t\t:aria-label=\"t('nextcloud-vue', 'Unlink album')\"\n\t\t\t\t\tclass=\"cn-photos-tab__unlink\"\n\t\t\t\t\t@click=\"unlinkAlbum(album)\">\n\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t<LinkOff :size=\"16\" />\n\t\t\t\t\t</template>\n\t\t\t\t</NcButton>\n\t\t\t</div>\n\t\t</div>\n\n\t\t<CnPhotoAlbumPicker\n\t\t\tv-if=\"pickerOpen\"\n\t\t\t:apiBase=\"apiBase\"\n\t\t\t@close=\"pickerOpen = false\"\n\t\t\t@link=\"onLinkPick\" />\n\n\t\t<CnPhotoAlbumCreate\n\t\t\tv-if=\"createOpen\"\n\t\t\t@close=\"createOpen = false\"\n\t\t\t@create=\"onCreatePick\" />\n\t</div>\n</template>\n\n<script>\nimport { translatePlural as n, translate as t } from '@nextcloud/l10n'\nimport { NcButton, NcLoadingIcon } from '@nextcloud/vue'\nimport AlertCircleOutline from 'vue-material-design-icons/AlertCircleOutline.vue'\nimport ImageIcon from 'vue-material-design-icons/Image.vue'\nimport ImageMultiple from 'vue-material-design-icons/ImageMultiple.vue'\nimport LinkOff from 'vue-material-design-icons/LinkOff.vue'\nimport LinkVariant from 'vue-material-design-icons/LinkVariant.vue'\nimport Plus from 'vue-material-design-icons/Plus.vue'\nimport CnPhotoAlbumCreate from '../../../components/CnPhotoAlbumCreate/CnPhotoAlbumCreate.vue'\nimport CnPhotoAlbumPicker from '../../../components/CnPhotoAlbumPicker/CnPhotoAlbumPicker.vue'\nimport { buildHeaders, prefixUrl, safeHref } from '../../../utils/index.js'\n\n/**\n * CnPhotosTab — bespoke album grid for the `photos` integration.\n *\n * Renders one tile per linked NC Photos album with cover thumbnail,\n * album name, photo count, and last-edited date. Clicking a tile opens\n * the album in the NC Photos app in a new tab. Tier-2: includes\n * link/create modals and per-tile unlink.\n */\nexport default {\n\tname: 'CnPhotosTab',\n\n\tcomponents: {\n\t\tNcButton,\n\t\tNcLoadingIcon,\n\t\tAlertCircleOutline,\n\t\tImageIcon,\n\t\tImageMultiple,\n\t\tLinkOff,\n\t\tLinkVariant,\n\t\tPlus,\n\t\tCnPhotoAlbumPicker,\n\t\tCnPhotoAlbumCreate,\n\t},\n\n\tprops: {\n\t\t/* eslint-disable vue/no-unused-properties -- the integration dispatch binds integrationId on every integration component (see CnIntegrationWidgetGrid), so declaring it keeps it out of $attrs */\n\t\t/** Stable integration id (forwarded from the registry — always `'photos'`). */\n\t\tintegrationId: { type: String, default: 'photos' },\n\t\t/* eslint-enable vue/no-unused-properties */\n\t\t/** Parent object id. */\n\t\tobjectId: { type: String, required: true },\n\t\t/** OpenRegister register id (slug or uuid). */\n\t\tregister: { type: String, default: '' },\n\t\t/** OpenRegister schema id (slug or uuid). */\n\t\tschema: { type: String, default: '' },\n\t\t/** Base API URL. */\n\t\tapiBase: { type: String, default: '/apps/openregister/api' },\n\t\t/** Pre-translated empty-state label. */\n\t\temptyLabel: { type: String, default: () => t('nextcloud-vue', 'No albums linked yet') },\n\t\t/** Pre-translated \"Open Photos\" CTA label. */\n\t\topenPhotosLabel: { type: String, default: () => t('nextcloud-vue', 'Open Photos') },\n\t\t/** Pre-translated 503 unavailable banner copy. */\n\t\tunavailableLabel: { type: String, default: () => t('nextcloud-vue', 'NC Photos is currently unavailable.') },\n\t\t/** URL of the NC Photos app entry. */\n\t\tphotosAppUrl: { type: String, default: '/index.php/apps/photos' },\n\t},\n\n\tdata() {\n\t\treturn {\n\t\t\talbums: [],\n\t\t\tloading: false,\n\t\t\terror: '',\n\t\t\tdegraded: '',\n\t\t\tbrokenCovers: {},\n\t\t\tpickerOpen: false,\n\t\t\tcreateOpen: false,\n\t\t}\n\t},\n\n\twatch: {\n\t\tobjectId: { immediate: true, handler(id) {\n\t\t\tif (id) {\n\t\t\t\tthis.fetchAlbums()\n\t\t\t}\n\t\t} },\n\n\t\tregister() {\n\t\t\tthis.fetchAlbums()\n\t\t},\n\n\t\tschema() {\n\t\t\tthis.fetchAlbums()\n\t\t},\n\t},\n\n\tmethods: {\n\t\tt,\n\n\t\t/**\n\t\t * Base for the Tier-2 photos endpoints (list/link/new/destroy).\n\t\t *\n\t\t * @return {string} The endpoint URL.\n\t\t */\n\t\tphotosEndpoint() {\n\t\t\treturn `${this.apiBase}/objects/${this.register}/${this.schema}/${this.objectId}/photos`\n\t\t},\n\n\t\talbumKey(album) {\n\t\t\treturn (album && (album.albumId ?? album.id ?? album.data?.album_id)) || ''\n\t\t},\n\n\t\t/**\n\t\t * Resolve the displayable album name from the row, stripping any\n\t\t * legacy `[or:{uuid}]` marker (pre-Tier-2 albums embedded the\n\t\t * marker inside `name`).\n\t\t *\n\t\t * @param {object} album Provider row.\n\t\t *\n\t\t * @return {string} The display name.\n\t\t */\n\t\talbumName(album) {\n\t\t\tconst raw = (album && (album.albumName || album.title || album.name || album.data?.name)) || ''\n\t\t\treturn String(raw).replace(/\\s*\\[or:[^\\]]+\\]\\s*/g, ' ').trim() || t('nextcloud-vue', 'Untitled album')\n\t\t},\n\n\t\talbumUrl(album) {\n\t\t\tif (album && album.url && safeHref(album.url) !== '#') {\n\t\t\t\treturn album.url\n\t\t\t}\n\t\t\tconst id = this.albumKey(album)\n\t\t\treturn `/index.php/apps/photos/albums/${encodeURIComponent(id)}`\n\t\t},\n\n\t\tcoverUrl(album) {\n\t\t\tif (!album || this.brokenCovers[this.albumKey(album)]) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn album.coverPhotoUrl || album.data?.coverPhotoUrl || ''\n\t\t},\n\n\t\tonCoverError(album) {\n\t\t\tthis.brokenCovers[this.albumKey(album)] = true\n\t\t},\n\n\t\tphotoCount(album) {\n\t\t\tconst raw = album?.photoCount ?? album?.data?.photoCount ?? album?.data?.count\n\t\t\tif (raw === null || raw === undefined || raw === '') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst num = Number(raw)\n\t\t\treturn Number.isFinite(num) ? num : null\n\t\t},\n\n\t\tphotoCountLabel(album) {\n\t\t\tconst count = this.photoCount(album)\n\t\t\tif (count === null) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn n('nextcloud-vue', '{count} photo', '{count} photos', count, { count })\n\t\t},\n\n\t\tlastEdited(album) {\n\t\t\treturn album?.lastEdited\n\t\t\t\t|| album?.data?.lastEdited\n\t\t\t\t|| album?.data?.created\n\t\t\t\t|| ''\n\t\t},\n\n\t\tformatWhen(value) {\n\t\t\tif (!value) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\ttry {\n\t\t\t\t// NC Photos stores `created` as a unix timestamp in\n\t\t\t\t// seconds; accept ISO strings transparently too.\n\t\t\t\tconst num = Number(value)\n\t\t\t\tconst d = Number.isFinite(num) && num > 0 && String(value).length <= 12\n\t\t\t\t\t? new Date(num * 1000)\n\t\t\t\t\t: new Date(value)\n\t\t\t\tif (Number.isNaN(d.getTime())) {\n\t\t\t\t\treturn String(value)\n\t\t\t\t}\n\t\t\t\treturn d.toLocaleDateString(undefined, { dateStyle: 'medium' })\n\t\t\t} catch {\n\t\t\t\treturn String(value)\n\t\t\t}\n\t\t},\n\n\t\topenPicker() {\n\t\t\tthis.pickerOpen = true\n\t\t},\n\n\t\topenCreate() {\n\t\t\tthis.createOpen = true\n\t\t},\n\n\t\tasync onLinkPick(payload) {\n\t\t\tthis.pickerOpen = false\n\t\t\ttry {\n\t\t\t\tconst response = await fetch(prefixUrl(this.photosEndpoint()), {\n\t\t\t\t\tmethod: 'POST',\n\t\t\t\t\theaders: { ...buildHeaders(), 'Content-Type': 'application/json' },\n\t\t\t\t\tbody: JSON.stringify(payload),\n\t\t\t\t})\n\t\t\t\tif (response.ok) {\n\t\t\t\t\tawait this.fetchAlbums()\n\t\t\t\t} else if (response.status === 409) {\n\t\t\t\t\tthis.error = t('nextcloud-vue', 'This album is already linked.')\n\t\t\t\t} else {\n\t\t\t\t\tthis.error = t('nextcloud-vue', 'Could not link album.')\n\t\t\t\t}\n\t\t\t} catch (err) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.error('[CnPhotosTab] link failed', err)\n\t\t\t\tthis.error = t('nextcloud-vue', 'Could not link album.')\n\t\t\t}\n\t\t},\n\n\t\tasync onCreatePick(payload) {\n\t\t\tthis.createOpen = false\n\t\t\ttry {\n\t\t\t\tconst response = await fetch(prefixUrl(`${this.photosEndpoint()}/new`), {\n\t\t\t\t\tmethod: 'POST',\n\t\t\t\t\theaders: { ...buildHeaders(), 'Content-Type': 'application/json' },\n\t\t\t\t\tbody: JSON.stringify(payload),\n\t\t\t\t})\n\t\t\t\tif (response.ok) {\n\t\t\t\t\tawait this.fetchAlbums()\n\t\t\t\t} else {\n\t\t\t\t\tthis.error = t('nextcloud-vue', 'Could not create album.')\n\t\t\t\t}\n\t\t\t} catch (err) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.error('[CnPhotosTab] create failed', err)\n\t\t\t\tthis.error = t('nextcloud-vue', 'Could not create album.')\n\t\t\t}\n\t\t},\n\n\t\tasync unlinkAlbum(album) {\n\t\t\tconst albumId = this.albumKey(album)\n\t\t\tif (!albumId) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tconst response = await fetch(prefixUrl(`${this.photosEndpoint()}/${albumId}`), {\n\t\t\t\t\tmethod: 'DELETE',\n\t\t\t\t\theaders: buildHeaders(),\n\t\t\t\t})\n\t\t\t\tif (response.ok) {\n\t\t\t\t\tawait this.fetchAlbums()\n\t\t\t\t} else {\n\t\t\t\t\tthis.error = t('nextcloud-vue', 'Could not unlink album.')\n\t\t\t\t}\n\t\t\t} catch (err) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.error('[CnPhotosTab] unlink failed', err)\n\t\t\t\tthis.error = t('nextcloud-vue', 'Could not unlink album.')\n\t\t\t}\n\t\t},\n\n\t\tasync fetchAlbums() {\n\t\t\tif (!this.register || !this.schema || !this.objectId) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.loading = true\n\t\t\tthis.error = ''\n\t\t\tthis.degraded = ''\n\t\t\ttry {\n\t\t\t\tconst response = await fetch(prefixUrl(this.photosEndpoint()), { headers: buildHeaders() })\n\t\t\t\tif (response.ok) {\n\t\t\t\t\tconst data = await response.json()\n\t\t\t\t\tthis.albums = data.results || data.items || (Array.isArray(data) ? data : []) || []\n\t\t\t\t} else if (response.status === 503 || response.status === 501) {\n\t\t\t\t\tthis.albums = []\n\t\t\t\t\tthis.degraded = this.unavailableLabel\n\t\t\t\t} else {\n\t\t\t\t\tthis.albums = []\n\t\t\t\t\tthis.error = t('nextcloud-vue', 'Could not load albums.')\n\t\t\t\t}\n\t\t\t} catch (err) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.error('[CnPhotosTab] failed to fetch albums', err)\n\t\t\t\tthis.albums = []\n\t\t\t\tthis.error = t('nextcloud-vue', 'Could not load albums.')\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-photos-tab {\n\tpadding: 12px;\n\toverflow-x: hidden;\n}\n\n.cn-photos-tab__actions {\n\tdisplay: flex;\n\tgap: 8px;\n\tmargin-bottom: 10px;\n\tflex-wrap: wrap;\n}\n\n.cn-photos-tab__banner {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 8px;\n\tpadding: 8px 10px;\n\tmargin-bottom: 10px;\n\tborder-radius: var(--border-radius);\n\tbackground: var(--color-warning, #e9a40f);\n\tcolor: var(--color-main-background);\n\tfont-size: 0.9em;\n}\n\n.cn-photos-tab__error {\n\tcolor: var(--color-error);\n\tfont-size: 0.9em;\n\tmargin: 4px 0 8px;\n}\n\n.cn-photos-tab__empty {\n\tdisplay: flex;\n\tflex-direction: column;\n\talign-items: center;\n\tgap: 8px;\n\tpadding: 16px 8px;\n\tcolor: var(--color-text-maxcontrast);\n\ttext-align: center;\n}\n\n.cn-photos-tab__empty-icon {\n\tcolor: var(--color-text-maxcontrast);\n}\n\n/*\n * Dense edge-to-edge mosaic that mirrors the NC Photos timeline: square\n * thumbnails packed tight with a thin main-background gutter (the same\n * 2px border NC Photos draws between tiles), name + date revealed in a\n * bottom scrim on hover/focus so the imagery dominates at rest.\n */\n.cn-photos-tab__grid {\n\tdisplay: grid;\n\tgrid-template-columns: repeat(auto-fill, minmax(104px, 1fr));\n\tgap: 2px;\n\tborder-radius: var(--border-radius-large, var(--border-radius));\n\toverflow: hidden;\n}\n\n.cn-photos-tab__tile {\n\tposition: relative;\n}\n\n.cn-photos-tab__unlink {\n\tposition: absolute;\n\ttop: 4px;\n\tright: 4px;\n\tz-index: 3;\n\topacity: 0;\n\tbackground: var(--color-main-background);\n\tborder-radius: 50%;\n\ttransition: opacity 0.15s ease;\n}\n\n.cn-photos-tab__tile:hover .cn-photos-tab__unlink,\n.cn-photos-tab__tile:focus-within .cn-photos-tab__unlink {\n\topacity: 1;\n}\n\n.cn-photos-tab__tile-link {\n\tdisplay: block;\n\ttext-decoration: none;\n\tcolor: inherit;\n}\n\n.cn-photos-tab__cover {\n\tposition: relative;\n\taspect-ratio: 1 / 1;\n\twidth: 100%;\n\t/* Match the NC Photos selection outline on focus. */\n\tborder: 2px solid var(--color-main-background);\n\tbox-sizing: border-box;\n\tbackground: var(--color-primary-element-light, var(--color-background-darker));\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: center;\n\toverflow: hidden;\n}\n\n.cn-photos-tab__tile-link:focus-visible .cn-photos-tab__cover {\n\toutline: var(--color-primary-element) solid 2px;\n\toutline-offset: -2px;\n}\n\n.cn-photos-tab__cover-img {\n\twidth: 100%;\n\theight: 100%;\n\tobject-fit: cover;\n\tdisplay: block;\n\ttransition: transform 0.2s ease;\n}\n\n.cn-photos-tab__tile:hover .cn-photos-tab__cover-img,\n.cn-photos-tab__tile:focus-within .cn-photos-tab__cover-img {\n\ttransform: scale(1.04);\n}\n\n.cn-photos-tab__cover-placeholder {\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: center;\n\twidth: 100%;\n\theight: 100%;\n}\n\n.cn-photos-tab__cover-fallback {\n\tcolor: var(--color-primary-element);\n}\n\n/* Photo-count badge (mirrors the album stack count overlay). */\n.cn-photos-tab__badge {\n\tposition: absolute;\n\ttop: 4px;\n\tleft: 4px;\n\tz-index: 2;\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 3px;\n\tpadding: 1px 6px 1px 4px;\n\tborder-radius: var(--border-radius-pill, 12px);\n\tbackground: rgba(0, 0, 0, 0.55);\n\tcolor: #fff;\n\tfont-size: 11px;\n\tfont-weight: 500;\n\tline-height: 1.6;\n}\n\n.cn-photos-tab__badge-count {\n\twhite-space: nowrap;\n}\n\n/* Bottom scrim: hidden at rest, revealed on hover/focus. */\n.cn-photos-tab__meta {\n\tposition: absolute;\n\tleft: 0;\n\tright: 0;\n\tbottom: 0;\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 1px;\n\tpadding: 16px 8px 6px;\n\tbackground: linear-gradient(to top, rgba(0, 0, 0, 0.7), rgba(0, 0, 0, 0));\n\topacity: 0;\n\ttransition: opacity 0.15s ease;\n\tpointer-events: none;\n}\n\n.cn-photos-tab__tile:hover .cn-photos-tab__meta,\n.cn-photos-tab__tile:focus-within .cn-photos-tab__meta {\n\topacity: 1;\n}\n\n.cn-photos-tab__name {\n\tfont-size: 12px;\n\tfont-weight: 600;\n\tcolor: #fff;\n\twhite-space: nowrap;\n\toverflow: hidden;\n\ttext-overflow: ellipsis;\n}\n\n.cn-photos-tab__when {\n\tfont-size: 10px;\n\tcolor: rgba(255, 255, 255, 0.85);\n\twhite-space: nowrap;\n\toverflow: hidden;\n\ttext-overflow: ellipsis;\n}\n</style>\n"],"names":["_openBlock","_createElementBlock","_createVNode","_createElementVNode","_createTextVNode","_createBlock","_toDisplayString"],"mappings":"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"}
|
|
@@ -6,6 +6,7 @@ import Poll from 'vue-material-design-icons/Poll.vue';
|
|
|
6
6
|
import __sfc_main from '../../../components/CnDetailCard/CnDetailCard.vue.js';
|
|
7
7
|
import __sfc_main$1 from '../../../components/CnStatusBadge/CnStatusBadge.vue.js';
|
|
8
8
|
import { stripMarker } from '../../utils/marker.js';
|
|
9
|
+
import { safeHref } from '../../../utils/safeHref.js';
|
|
9
10
|
import { prefixUrl, buildHeaders } from '../../../utils/headers.js';
|
|
10
11
|
|
|
11
12
|
const VALID_SURFACES = ['user-dashboard', 'app-dashboard', 'detail-page', 'single-entity'];
|
|
@@ -142,7 +143,7 @@ var script = {
|
|
|
142
143
|
},
|
|
143
144
|
|
|
144
145
|
pollUrl(poll) {
|
|
145
|
-
if (poll.url) {
|
|
146
|
+
if (poll.url && safeHref(poll.url) !== '#') {
|
|
146
147
|
return poll.url
|
|
147
148
|
}
|
|
148
149
|
const id = poll.id ?? '';
|