@conduction/nextcloud-vue 2.56.0-beta.30 → 2.56.0-beta.32
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.map +1 -1
- package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js +3 -3
- package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js.map +1 -1
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js +80 -4
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js +95 -91
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js.map +1 -1
- package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js +140 -10
- package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue3.js +52 -16
- package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js +17 -3
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js.map +1 -1
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js +63 -52
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js.map +1 -1
- package/dist/esm/components/CnChipsFieldWidget/CnChipsFieldWidget.vue.js +10 -0
- package/dist/esm/components/CnChipsFieldWidget/CnChipsFieldWidget.vue.js.map +1 -0
- package/dist/esm/components/CnChipsFieldWidget/CnChipsFieldWidget.vue2.js +252 -0
- package/dist/esm/components/CnChipsFieldWidget/CnChipsFieldWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnChipsFieldWidget/CnChipsFieldWidget.vue3.js +140 -0
- package/dist/esm/components/CnChipsFieldWidget/CnChipsFieldWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnChipsFieldWidget/index.js +26 -0
- package/dist/esm/components/CnChipsFieldWidget/index.js.map +1 -0
- package/dist/esm/components/CnChoicePickerWidget/CnChoicePickerWidget.vue.js +10 -0
- package/dist/esm/components/CnChoicePickerWidget/CnChoicePickerWidget.vue.js.map +1 -0
- package/dist/esm/components/CnChoicePickerWidget/CnChoicePickerWidget.vue2.js +354 -0
- package/dist/esm/components/CnChoicePickerWidget/CnChoicePickerWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnChoicePickerWidget/CnChoicePickerWidget.vue3.js +172 -0
- package/dist/esm/components/CnChoicePickerWidget/CnChoicePickerWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnChoicePickerWidget/index.js +27 -0
- package/dist/esm/components/CnChoicePickerWidget/index.js.map +1 -0
- package/dist/esm/components/CnConversationThread/CnConversationWidget.vue2.js +1 -1
- package/dist/esm/components/CnConversationThread/CnConversationWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnConversationThread/CnConversationWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue2.js +2 -2
- package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue2.js.map +1 -1
- package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue3.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +65 -5
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +2 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +19 -11
- package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +3 -2
- package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +72 -0
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +21 -4
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
- package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewWidget.vue2.js +1 -1
- package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnFacetSidebar/CnFacetSidebar.vue2.js +7 -7
- package/dist/esm/components/CnFacetSidebar/CnFacetSidebar.vue2.js.map +1 -1
- package/dist/esm/components/CnFacetSidebar/CnFacetSidebar.vue3.js.map +1 -1
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js +131 -0
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js +80 -5
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js +282 -69
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js +302 -4
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js.map +1 -1
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js +417 -244
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js.map +1 -1
- package/dist/esm/components/CnFilesBrowser/filesBrowser.js +101 -1
- package/dist/esm/components/CnFilesBrowser/filesBrowser.js.map +1 -1
- package/dist/esm/components/CnFilesBrowser/filesBrowserColumns.js +141 -1
- package/dist/esm/components/CnFilesBrowser/filesBrowserColumns.js.map +1 -1
- package/dist/esm/components/CnFormPage/CnFormPage.vue2.js +60 -0
- package/dist/esm/components/CnFormPage/CnFormPage.vue2.js.map +1 -1
- package/dist/esm/components/CnFormPage/CnFormPage.vue3.js +74 -49
- package/dist/esm/components/CnFormPage/CnFormPage.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +29 -2
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +48 -4
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue2.js +7 -7
- package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue3.js.map +1 -1
- package/dist/esm/components/CnNextStepCard/CnNextStepWidget.vue2.js +1 -1
- package/dist/esm/components/CnNextStepCard/CnNextStepWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +65 -2
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js +6 -2
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js +63 -1
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js +8 -1
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js.map +1 -1
- package/dist/esm/components/CnObjectSidebar/CnNotesTab.vue2.js +14 -0
- package/dist/esm/components/CnObjectSidebar/CnNotesTab.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectSidebar/CnNotesTab.vue3.js +5 -1
- package/dist/esm/components/CnObjectSidebar/CnNotesTab.vue3.js.map +1 -1
- package/dist/esm/components/CnRadioCardsWidget/CnRadioCardsWidget.vue.js +10 -0
- package/dist/esm/components/CnRadioCardsWidget/CnRadioCardsWidget.vue.js.map +1 -0
- package/dist/esm/components/CnRadioCardsWidget/CnRadioCardsWidget.vue2.js +174 -0
- package/dist/esm/components/CnRadioCardsWidget/CnRadioCardsWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnRadioCardsWidget/CnRadioCardsWidget.vue3.js +75 -0
- package/dist/esm/components/CnRadioCardsWidget/CnRadioCardsWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnRadioCardsWidget/index.js +28 -0
- package/dist/esm/components/CnRadioCardsWidget/index.js.map +1 -0
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +30 -6
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js +34 -25
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnSavedViewsWidget/CnSavedViewsWidget.vue.js +10 -0
- package/dist/esm/components/CnSavedViewsWidget/CnSavedViewsWidget.vue.js.map +1 -0
- package/dist/esm/components/CnSavedViewsWidget/CnSavedViewsWidget.vue2.js +222 -0
- package/dist/esm/components/CnSavedViewsWidget/CnSavedViewsWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnSavedViewsWidget/CnSavedViewsWidget.vue3.js +64 -0
- package/dist/esm/components/CnSavedViewsWidget/CnSavedViewsWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnSavedViewsWidget/index.js +30 -0
- package/dist/esm/components/CnSavedViewsWidget/index.js.map +1 -0
- package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue2.js.map +1 -1
- package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue3.js.map +1 -1
- package/dist/esm/components/CnSettingsFormWidget/CnSettingsFormWidget.vue.js +10 -0
- package/dist/esm/components/CnSettingsFormWidget/CnSettingsFormWidget.vue.js.map +1 -0
- package/dist/esm/components/CnSettingsFormWidget/CnSettingsFormWidget.vue2.js +347 -0
- package/dist/esm/components/CnSettingsFormWidget/CnSettingsFormWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnSettingsFormWidget/CnSettingsFormWidget.vue3.js +184 -0
- package/dist/esm/components/CnSettingsFormWidget/CnSettingsFormWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnSettingsFormWidget/index.js +27 -0
- package/dist/esm/components/CnSettingsFormWidget/index.js.map +1 -0
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js +87 -6
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue2.js +84 -1
- package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue3.js +85 -41
- package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue.js +10 -0
- package/dist/esm/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue.js.map +1 -0
- package/dist/esm/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue2.js +159 -0
- package/dist/esm/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue3.js +60 -0
- package/dist/esm/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnSwitchRowsWidget/index.js +26 -0
- package/dist/esm/components/CnSwitchRowsWidget/index.js.map +1 -0
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +47 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js +16 -14
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnTasksWidget/CnTasksWidget.vue2.js +80 -1
- package/dist/esm/components/CnTasksWidget/CnTasksWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnTasksWidget/CnTasksWidget.vue3.js +75 -32
- package/dist/esm/components/CnTasksWidget/CnTasksWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue.js +10 -0
- package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue.js.map +1 -0
- package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue2.js +269 -0
- package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue3.js +106 -0
- package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnToggleTableWidget/index.js +27 -0
- package/dist/esm/components/CnToggleTableWidget/index.js.map +1 -0
- package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js +7 -0
- package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js.map +1 -1
- package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js +7 -2
- package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js.map +1 -1
- package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue3.js.map +1 -1
- package/dist/esm/components/CnWidgetObjectTable/dashboardRegistration.js +5 -0
- package/dist/esm/components/CnWidgetObjectTable/dashboardRegistration.js.map +1 -1
- package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue2.js +197 -6
- package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue3.js +96 -50
- package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue3.js.map +1 -1
- package/dist/esm/composables/useListView.js +4 -4
- package/dist/esm/composables/useListView.js.map +1 -1
- package/dist/esm/index.js +17 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/l10n/en.json.js +9 -1
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +55 -1
- package/dist/esm/l10n/nl.json.js.map +1 -1
- package/dist/esm/nextcloud-vue.css +1041 -12
- package/dist/esm/utils/contentHeaderLink.js +51 -0
- package/dist/esm/utils/contentHeaderLink.js.map +1 -0
- package/dist/esm/utils/facets.js +75 -3
- package/dist/esm/utils/facets.js.map +1 -1
- package/dist/esm/utils/fetchFilterCounts.js +40 -5
- package/dist/esm/utils/fetchFilterCounts.js.map +1 -1
- package/dist/esm/utils/fillTemplate.js +7 -1
- package/dist/esm/utils/fillTemplate.js.map +1 -1
- package/dist/esm/utils/initials.js +32 -0
- package/dist/esm/utils/initials.js.map +1 -0
- package/dist/esm/utils/libraryWidgetKeys.js +7 -0
- package/dist/esm/utils/libraryWidgetKeys.js.map +1 -1
- package/dist/esm/utils/objectWrite.js +59 -0
- package/dist/esm/utils/objectWrite.js.map +1 -0
- package/dist/esm/utils/pageControls.js +81 -0
- package/dist/esm/utils/pageControls.js.map +1 -0
- package/dist/esm/utils/personalLenses.js +40 -3
- package/dist/esm/utils/personalLenses.js.map +1 -1
- package/dist/esm/utils/resolveFilterTokens.js +1 -1
- package/dist/esm/utils/resolveFilterTokens.js.map +1 -1
- package/dist/esm/utils/resolveManifestTokens.js +2 -0
- package/dist/esm/utils/resolveManifestTokens.js.map +1 -1
- package/dist/esm/utils/sentinelTokens.js +19 -1
- package/dist/esm/utils/sentinelTokens.js.map +1 -1
- package/dist/esm/utils/userGroupsPeek.js +91 -0
- package/dist/esm/utils/userGroupsPeek.js.map +1 -0
- package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
- package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
- package/dist/esm/utils/visibleWhen.js +57 -5
- package/dist/esm/utils/visibleWhen.js.map +1 -1
- package/dist/esm/utils/widgetDispatch.js +1 -1
- package/dist/esm/utils/widgetDispatch.js.map +1 -1
- package/dist/esm/utils/widgetVisibility.js +4 -57
- package/dist/esm/utils/widgetVisibility.js.map +1 -1
- package/dist/nextcloud-vue.cjs.js +13589 -7833
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +1041 -12
- package/l10n/en.json +9 -1
- package/l10n/nl.json +55 -1
- package/package.json +2 -1
- package/scripts/check-docs-mdx.mjs +108 -0
- package/src/components/CnActionsBar/CnActionsBar.vue +6 -1
- package/src/components/CnAppRoot/CnAppRoot.md +4 -2
- package/src/components/CnAppRoot/CnAppRoot.vue +151 -59
- package/src/components/CnBannerWidget/CnBannerWidget.vue +205 -20
- package/src/components/CnCellRenderer/CnCellRenderer.md +2 -0
- package/src/components/CnCellRenderer/CnCellRenderer.vue +47 -4
- package/src/components/CnChipsFieldWidget/CnChipsFieldWidget.vue +433 -0
- package/src/components/CnChipsFieldWidget/index.js +25 -0
- package/src/components/CnChoicePickerWidget/CnChoicePickerWidget.vue +556 -0
- package/src/components/CnChoicePickerWidget/index.js +26 -0
- package/src/components/CnConversationThread/CnConversationWidget.vue +1 -1
- package/src/components/CnDashboardGrid/CnDashboardGrid.vue +2 -2
- package/src/components/CnDashboardPage/CnDashboardPage.vue +78 -5
- package/src/components/CnDataTable/CnDataTable.vue +21 -12
- package/src/components/CnDetailPage/CnDetailPage.vue +89 -3
- package/src/components/CnDocumentReviewList/CnDocumentReviewWidget.vue +1 -1
- package/src/components/CnFacetSidebar/CnFacetSidebar.vue +7 -7
- package/src/components/CnFactsWidget/CnFactsWidget.vue +142 -2
- package/src/components/CnFieldListWidget/CnFieldListWidget.vue +268 -6
- package/src/components/CnFilesBrowser/CnFilesBrowser.vue +588 -117
- package/src/components/CnFilesBrowser/filesBrowser.js +100 -0
- package/src/components/CnFilesBrowser/filesBrowserColumns.js +140 -0
- package/src/components/CnFormPage/CnFormPage.vue +81 -0
- package/src/components/CnIndexPage/CnIndexPage.vue +56 -4
- package/src/components/CnIndexSidebar/CnIndexSidebar.vue +7 -7
- package/src/components/CnNextStepCard/CnNextStepWidget.vue +1 -1
- package/src/components/CnObjectListWidget/CnObjectListWidget.vue +69 -2
- package/src/components/CnObjectSidebar/CnFilesTab.vue +70 -1
- package/src/components/CnObjectSidebar/CnNotesTab.vue +16 -0
- package/src/components/CnRadioCardsWidget/CnRadioCardsWidget.vue +313 -0
- package/src/components/CnRadioCardsWidget/index.js +27 -0
- package/src/components/CnRecordListWidget/CnRecordListWidget.vue +55 -6
- package/src/components/CnSavedViewsWidget/CnSavedViewsWidget.vue +296 -0
- package/src/components/CnSavedViewsWidget/index.js +29 -0
- package/src/components/CnSegmentedControl/CnSegmentedControl.vue +13 -0
- package/src/components/CnSettingsFormWidget/CnSettingsFormWidget.vue +555 -0
- package/src/components/CnSettingsFormWidget/index.js +26 -0
- package/src/components/CnStatWidget/CnStatWidget.vue +87 -6
- package/src/components/CnStatsBlockWidget/CnStatsBlockWidget.vue +180 -3
- package/src/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue +226 -0
- package/src/components/CnSwitchRowsWidget/index.js +25 -0
- package/src/components/CnTabsWidget/CnTabsWidget.vue +82 -3
- package/src/components/CnTasksWidget/CnTasksWidget.vue +175 -15
- package/src/components/CnToggleTableWidget/CnToggleTableWidget.vue +386 -0
- package/src/components/CnToggleTableWidget/index.js +26 -0
- package/src/components/CnWidgetGrid/registerDashboardWidgets.js +7 -0
- package/src/components/CnWidgetObjectTable/CnWidgetObjectTable.vue +7 -2
- package/src/components/CnWidgetObjectTable/dashboardRegistration.js +5 -0
- package/src/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue +309 -11
- package/src/components/index.js +15 -0
- package/src/composables/useListView.js +4 -4
- package/src/css/look-board-detail.css +36 -2
- package/src/css/look-board-index.css +39 -8
- package/src/css/look-board.css +72 -0
- package/src/index.js +15 -0
- package/src/schemas/app-manifest-v2.schema.json +93 -9
- package/src/utils/contentHeaderLink.js +48 -0
- package/src/utils/facets.js +74 -2
- package/src/utils/fetchFilterCounts.js +39 -4
- package/src/utils/fillTemplate.js +7 -1
- package/src/utils/initials.js +29 -0
- package/src/utils/libraryWidgetKeys.js +7 -0
- package/src/utils/objectWrite.js +55 -0
- package/src/utils/pageControls.js +78 -0
- package/src/utils/personalLenses.js +39 -2
- package/src/utils/resolveFilterTokens.js +3 -1
- package/src/utils/resolveManifestTokens.js +2 -0
- package/src/utils/sentinelTokens.js +19 -1
- package/src/utils/userGroupsPeek.js +87 -0
- package/src/utils/validateManifestV2.compiled.js +3 -3
- package/src/utils/visibleWhen.js +56 -4
- package/src/utils/widgetDispatch.js +1 -1
- package/src/utils/widgetVisibility.js +7 -56
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CnRecordListWidget.vue3.js","sources":["../../../../src/components/CnRecordListWidget/CnRecordListWidget.vue?vue&type=template&id=7073b362&lang.js"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n\n<template>\n\t<div\n\t\tclass=\"cn-record-list\"\n\t\t:class=\"`cn-record-list--${presentation}`\"\n\t\tdata-testid=\"cn-record-list\">\n\t\t<p v-if=\"error\" class=\"cn-record-list__error\" data-testid=\"cn-record-list-error\">\n\t\t\t{{ errorLabel }}\n\t\t</p>\n\t\t<div v-else-if=\"loading && rows.length === 0\" class=\"cn-record-list__loading\">\n\t\t\t<NcLoadingIcon :size=\"20\" :name=\"loadingLabel\" />\n\t\t</div>\n\t\t<p v-else-if=\"rows.length === 0\" class=\"cn-record-list__empty\" data-testid=\"cn-record-list-empty\">\n\t\t\t{{ emptyLabel }}\n\t\t</p>\n\t\t<ul v-else class=\"cn-record-list__list\">\n\t\t\t<li\n\t\t\t\tv-for=\"item in items\"\n\t\t\t\t:key=\"item.key\"\n\t\t\t\tclass=\"cn-record-list__item\"\n\t\t\t\tdata-testid=\"cn-record-list-item\">\n\t\t\t\t<!-- rows: title and meta, then the pill, the date and a chevron. -->\n\t\t\t\t<component\n\t\t\t\t\t:is=\"item.tag\"\n\t\t\t\t\tv-if=\"presentation === 'rows'\"\n\t\t\t\t\tclass=\"cn-record-list__row\"\n\t\t\t\t\t:class=\"{ 'cn-record-list__row--link': item.tag !== 'div' }\"\n\t\t\t\t\tv-bind=\"item.attrs\">\n\t\t\t\t\t<span class=\"cn-record-list__main\">\n\t\t\t\t\t\t<span v-if=\"item.title\" class=\"cn-record-list__title\">{{ item.title }}</span>\n\t\t\t\t\t\t<span v-if=\"item.meta\" class=\"cn-record-list__meta\">{{ item.meta }}</span>\n\t\t\t\t\t</span>\n\t\t\t\t\t<span v-if=\"item.pill || item.trailing\" class=\"cn-record-list__aside\">\n\t\t\t\t\t\t<CnStatusBadge\n\t\t\t\t\t\t\tv-if=\"item.pill\"\n\t\t\t\t\t\t\t:label=\"item.pill.label\"\n\t\t\t\t\t\t\t:variant=\"item.pill.variant\"\n\t\t\t\t\t\t\tsize=\"small\" />\n\t\t\t\t\t\t<span v-if=\"item.trailing\" class=\"cn-record-list__trailing\">{{ item.trailing }}</span>\n\t\t\t\t\t</span>\n\t\t\t\t\t<ChevronRight\n\t\t\t\t\t\tv-if=\"item.tag !== 'div'\"\n\t\t\t\t\t\tclass=\"cn-record-list__chevron\"\n\t\t\t\t\t\t:size=\"18\"\n\t\t\t\t\t\taria-hidden=\"true\" />\n\t\t\t\t</component>\n\n\t\t\t\t<!-- feed: a round icon, a meta line and the text. -->\n\t\t\t\t<div v-else-if=\"presentation === 'feed'\" class=\"cn-record-list__feed-row\">\n\t\t\t\t\t<span class=\"cn-record-list__feed-icon\" aria-hidden=\"true\">\n\t\t\t\t\t\t<CnIcon :name=\"item.icon\" :size=\"18\" />\n\t\t\t\t\t</span>\n\t\t\t\t\t<component :is=\"item.tag\" class=\"cn-record-list__main\" v-bind=\"item.attrs\">\n\t\t\t\t\t\t<span v-if=\"item.meta\" class=\"cn-record-list__meta\">{{ item.meta }}</span>\n\t\t\t\t\t\t<span v-if=\"item.text\" class=\"cn-record-list__text\">{{ item.text }}</span>\n\t\t\t\t\t</component>\n\t\t\t\t</div>\n\n\t\t\t\t<!-- inline: a label and the title, the title a link. -->\n\t\t\t\t<div v-else class=\"cn-record-list__inline-row\">\n\t\t\t\t\t<span v-if=\"item.label\" class=\"cn-record-list__label\">{{ item.label }}</span>\n\t\t\t\t\t<component :is=\"item.tag\" class=\"cn-record-list__inline-title\" v-bind=\"item.attrs\">\n\t\t\t\t\t\t{{ item.title }}\n\t\t\t\t\t</component>\n\t\t\t\t</div>\n\t\t\t</li>\n\t\t</ul>\n\t</div>\n</template>\n\n<script>\nimport { subscribe, unsubscribe } from '@nextcloud/event-bus'\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcLoadingIcon } from '@nextcloud/vue'\nimport ChevronRight from 'vue-material-design-icons/ChevronRight.vue'\nimport CnIcon from '../CnIcon/CnIcon.vue'\nimport CnStatusBadge from '../CnStatusBadge/CnStatusBadge.vue'\nimport { isBadgeVariant } from '../../utils/badgeVariants.js'\nimport { fillTemplate, firstFilled } from '../../utils/fillTemplate.js'\nimport { readPath } from '../../utils/readPath.js'\nimport { dropOptionalUnresolved, hasUnresolvedTokens, resolveFilterTokens } from '../../utils/resolveFilterTokens.js'\n\nconst PAGE_REFRESH_CHANNEL = 'cn:page:refresh'\nconst PRESENTATIONS = ['rows', 'feed', 'inline']\n\n/**\n * Unwrap a value that may be a ref.\n *\n * @param {unknown} value The value.\n * @return {unknown} The plain value.\n */\nfunction unwrap(value) {\n\treturn value && typeof value === 'object' && 'value' in value ? value.value : value\n}\n\n/**\n * CnRecordListWidget — the objects of another schema that point at this\n * record, drawn as the boards draw them (screens-relation-pages).\n *\n * A person page lists its open cases (\"Verlichting fietspad Lindelaan,\n * 2026-0082 · Woo-verzoek, In behandeling, Termijn 1 nov\"), its latest contact\n * moments (an icon by channel, a meta line, the summary) and its relations\n * (\"Secretaris van Buurtvereniging Oost\"). Each is a list of another schema's\n * objects filtered on a reference to this record. The source keys are those of\n * `object-list` (`register`, `schema`, `filter` with the `@objectId` /\n * `@object.<field>` / `@workspace.<key>` tokens, `sort`, `limit`, `extend`,\n * `rowRoute`); what differs is the drawing: `presentation` is `rows`, `feed`\n * or `inline`, and each line is a template (`\"{identifier} · {caseTypeName}\"`,\n * `\"Termijn {deadline|date}\"`).\n *\n * A content-only catalog type: on a card surface the detail host draws the\n * titled card. The widget emits `header` with `{ link, note }` (the\n * `headerLink` and `headerNote` with `{total}` filled in) for that card's\n * header.\n */\nexport default {\n\tname: 'CnRecordListWidget',\n\n\tcomponents: { ChevronRight, CnIcon, CnStatusBadge, NcLoadingIcon },\n\n\tinject: {\n\t\t/** Detail-page object context (`{ objectId, object, register, schema }`). */\n\t\tcnObjectContext: { default: null },\n\t\t/** Page workspace bag, for `@workspace.<key>` tokens. */\n\t\tcnWorkspaceContext: { default: null },\n\t\t/** The host app's translate function. */\n\t\tcnTranslate: { default: () => (key) => key },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The list config: `{ register, schema, filter?, sort?, limit?, extend?,\n\t\t * presentation?, title?, meta?, text?, label?, pill?, trailing?, icon?,\n\t\t * rowRoute?, rowIdField?, headerLink?, headerNote?, emptyText?, errorText? }`.\n\t\t *\n\t\t * @type {object}\n\t\t */\n\t\tcontent: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/** The current record's id, for the `@objectId` token. */\n\t\tobjectId: {\n\t\t\ttype: [String, Number],\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** The current record, for `@object.<field>` tokens. */\n\t\tobjectData: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\temits: ['header'],\n\n\tdata() {\n\t\treturn {\n\t\t\trows: [],\n\t\t\ttotal: 0,\n\t\t\tloading: false,\n\t\t\terror: false,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/** @return {string} `rows`, `feed` or `inline`. */\n\t\tpresentation() {\n\t\t\tconst value = this.content?.presentation\n\t\t\treturn PRESENTATIONS.includes(value) ? value : 'rows'\n\t\t},\n\n\t\t/** @return {object} The token context: record, id and workspace. */\n\t\ttokenCtx() {\n\t\t\tconst injected = unwrap(this.cnObjectContext) || {}\n\t\t\treturn {\n\t\t\t\t...injected,\n\t\t\t\tobjectId: this.objectId ?? injected.objectId ?? this.objectData?.id ?? null,\n\t\t\t\tobject: this.objectData || injected.object || null,\n\t\t\t\tworkspace: unwrap(this.cnWorkspaceContext) || {},\n\t\t\t}\n\t\t},\n\n\t\t/** @return {object} The filter with its tokens resolved. */\n\t\tresolvedFilter() {\n\t\t\tconst filter = this.content?.filter\n\t\t\treturn dropOptionalUnresolved(resolveFilterTokens(filter && typeof filter === 'object' ? filter : {}, this.tokenCtx))\n\t\t},\n\n\t\t/** @return {boolean} A required token (the record id, say) has no value yet. */\n\t\twaiting() {\n\t\t\treturn hasUnresolvedTokens(this.resolvedFilter)\n\t\t},\n\n\t\t/** @return {number} The fetch cap. */\n\t\tlimit() {\n\t\t\tconst value = Number(this.content?.limit)\n\t\t\treturn Number.isFinite(value) && value > 0 ? Math.floor(value) : 5\n\t\t},\n\n\t\t/** @return {string} What a fetch depends on; a change fetches again. */\n\t\trequestKey() {\n\t\t\tconst c = this.content || {}\n\t\t\treturn JSON.stringify([c.register, c.schema, this.resolvedFilter, c.sort, this.limit, c.extend, this.waiting])\n\t\t},\n\n\t\t/** @return {Array<object>} The rows ready to draw. */\n\t\titems() {\n\t\t\tconst c = this.content || {}\n\t\t\treturn this.rows.slice(0, this.limit).map((row, index) => {\n\t\t\t\tconst link = this.rowLink(row)\n\t\t\t\treturn {\n\t\t\t\t\tkey: (row && (row.id || row['@self']?.id)) || `row-${index}`,\n\t\t\t\t\ttag: link ? 'router-link' : 'div',\n\t\t\t\t\tattrs: link ? { to: link } : {},\n\t\t\t\t\ttitle: this.fill(c.title, row),\n\t\t\t\t\tmeta: this.fill(c.meta, row),\n\t\t\t\t\ttext: this.fill(c.text, row),\n\t\t\t\t\tlabel: this.fill(c.label, row),\n\t\t\t\t\ttrailing: c.trailing ? firstFilled(c.trailing, row, this.translate) : '',\n\t\t\t\t\tpill: this.pillOf(row),\n\t\t\t\t\ticon: this.iconOf(row),\n\t\t\t\t}\n\t\t\t})\n\t\t},\n\n\t\t/** @return {object|null} The header link with `{total}` filled and tokens resolved. */\n\t\theaderLink() {\n\t\t\tconst link = this.content?.headerLink\n\t\t\tif (!link || typeof link !== 'object' || typeof link.label !== 'string' || link.label === '') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst resolved = { label: this.withTotal(link.label) }\n\t\t\tif (link.route) {\n\t\t\t\tresolved.route = link.route\n\t\t\t\tif (link.params && typeof link.params === 'object') {\n\t\t\t\t\tresolved.params = resolveFilterTokens(link.params, this.tokenCtx)\n\t\t\t\t}\n\t\t\t\tif (link.query && typeof link.query === 'object') {\n\t\t\t\t\tresolved.query = resolveFilterTokens(link.query, this.tokenCtx)\n\t\t\t\t}\n\t\t\t} else if (typeof link.href === 'string') {\n\t\t\t\tresolved.href = link.href\n\t\t\t}\n\t\t\treturn resolved\n\t\t},\n\n\t\t/** @return {string} The header note with `{total}` filled. */\n\t\theaderNote() {\n\t\t\tconst note = this.content?.headerNote\n\t\t\treturn typeof note === 'string' && note !== '' ? this.withTotal(note) : ''\n\t\t},\n\n\t\t/** @return {string} The empty line. */\n\t\temptyLabel() {\n\t\t\treturn this.content?.emptyText ? this.translate(this.content.emptyText) : t('nextcloud-vue', 'Nothing here yet')\n\t\t},\n\n\t\t/** @return {string} The error line. */\n\t\terrorLabel() {\n\t\t\treturn this.content?.errorText ? this.translate(this.content.errorText) : t('nextcloud-vue', 'This list could not be loaded.')\n\t\t},\n\n\t\t/** @return {string} The spinner's accessible name. */\n\t\tloadingLabel() {\n\t\t\treturn t('nextcloud-vue', 'Loading …')\n\t\t},\n\t},\n\n\twatch: {\n\t\trequestKey() {\n\t\t\tthis.fetchRows()\n\t\t},\n\t},\n\n\tmounted() {\n\t\tthis.onRefresh = () => this.fetchRows()\n\t\tsubscribe(PAGE_REFRESH_CHANNEL, this.onRefresh)\n\t\tthis.fetchRows()\n\t},\n\n\tbeforeUnmount() {\n\t\tunsubscribe(PAGE_REFRESH_CHANNEL, this.onRefresh)\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Translate a manifest string through the host app.\n\t\t *\n\t\t * @param {string} text The text.\n\t\t * @return {string} The translation.\n\t\t */\n\t\ttranslate(text) {\n\t\t\treturn typeof this.cnTranslate === 'function' ? this.cnTranslate(text) : text\n\t\t},\n\n\t\t/**\n\t\t * Fill a template from a row, after translating it.\n\t\t *\n\t\t * @param {string} template The template.\n\t\t * @param {object} row The row.\n\t\t * @return {string} The text, '' when a token is empty.\n\t\t */\n\t\tfill(template, row) {\n\t\t\treturn typeof template === 'string' && template !== '' ? fillTemplate(this.translate(template), row) : ''\n\t\t},\n\n\t\t/**\n\t\t * Replace `{total}` in a translated label.\n\t\t *\n\t\t * @param {string} label The label.\n\t\t * @return {string} The text.\n\t\t */\n\t\twithTotal(label) {\n\t\t\treturn this.translate(label).replace(/\\{total\\}/g, String(this.total))\n\t\t},\n\n\t\t/**\n\t\t * The route a row opens: `rowRoute` with the id at `rowIdField`.\n\t\t *\n\t\t * @param {object} row The row.\n\t\t * @return {object|null} The router location, or null.\n\t\t */\n\t\trowLink(row) {\n\t\t\tconst route = this.content?.rowRoute\n\t\t\tif (!route || !this.$router || !row) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst field = this.content?.rowIdField\n\t\t\tconst id = field ? readPath(row, field) : (row.id || row['@self']?.id)\n\t\t\tif (id === null || id === undefined || id === '' || typeof id === 'object') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\treturn { name: route, params: { id: String(id) } }\n\t\t},\n\n\t\t/**\n\t\t * The row's pill: `{ field, labels?, colorMap?, variant? }`.\n\t\t *\n\t\t * @param {object} row The row.\n\t\t * @return {{label: string, variant: string}|null} The pill, or null.\n\t\t */\n\t\tpillOf(row) {\n\t\t\tconst pill = this.content?.pill\n\t\t\tif (!pill || typeof pill !== 'object' || !pill.field) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst raw = readPath(row, pill.field)\n\t\t\tif (raw === null || raw === undefined || raw === '' || typeof raw === 'object') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst key = String(raw)\n\t\t\tconst labels = pill.labels && typeof pill.labels === 'object' ? pill.labels : {}\n\t\t\tconst map = pill.colorMap && typeof pill.colorMap === 'object' ? pill.colorMap : {}\n\t\t\tconst found = Object.keys(map).find((k) => k.toLowerCase() === key.toLowerCase())\n\t\t\tconst variant = found ? map[found] : pill.variant\n\t\t\treturn {\n\t\t\t\tlabel: labels[key] ? this.translate(labels[key]) : key,\n\t\t\t\tvariant: isBadgeVariant(variant) ? variant : 'default',\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * The feed icon: an icon name, or `{ field, map, default }`.\n\t\t *\n\t\t * @param {object} row The row.\n\t\t * @return {string} The MDI icon name.\n\t\t */\n\t\ticonOf(row) {\n\t\t\tconst icon = this.content?.icon\n\t\t\tif (typeof icon === 'string' && icon !== '') {\n\t\t\t\treturn icon\n\t\t\t}\n\t\t\tif (icon && typeof icon === 'object') {\n\t\t\t\tconst raw = icon.field ? readPath(row, icon.field) : undefined\n\t\t\t\tconst map = icon.map && typeof icon.map === 'object' ? icon.map : {}\n\t\t\t\tif (raw !== null && raw !== undefined && map[String(raw)]) {\n\t\t\t\t\treturn map[String(raw)]\n\t\t\t\t}\n\t\t\t\tif (typeof icon.default === 'string' && icon.default !== '') {\n\t\t\t\t\treturn icon.default\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn 'CircleSmall'\n\t\t},\n\n\t\t/** Tell the card around this list what its header shows. */\n\t\temitHeader() {\n\t\t\t/**\n\t\t\t * @event header Emitted after every read with the card header's parts.\n\t\t\t * @type {{link: (object|null), note: string}} The header link (resolved, with `{total}` filled) and the note.\n\t\t\t */\n\t\t\tthis.$emit('header', { link: this.headerLink, note: this.headerNote })\n\t\t},\n\n\t\t/**\n\t\t * Fetch the list. Sends nothing while a required token is unresolved\n\t\t * or the source is incomplete.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync fetchRows() {\n\t\t\tconst c = this.content || {}\n\t\t\tif (!c.register || !c.schema || this.waiting) {\n\t\t\t\tthis.rows = []\n\t\t\t\tthis.total = 0\n\t\t\t\tthis.error = false\n\t\t\t\tthis.emitHeader()\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.loading = true\n\t\t\tthis.error = false\n\t\t\ttry {\n\t\t\t\tconst [{ default: axios }, { generateUrl }] = await Promise.all([\n\t\t\t\t\timport('@nextcloud/axios'),\n\t\t\t\t\timport('@nextcloud/router'),\n\t\t\t\t])\n\t\t\t\tconst url = generateUrl('/apps/openregister/api/objects/{register}/{schema}', { register: c.register, schema: c.schema })\n\t\t\t\tconst params = { _limit: this.limit }\n\t\t\t\tif (c.sort && typeof c.sort === 'object' && c.sort.field) {\n\t\t\t\t\tparams[`_order[${c.sort.field}]`] = c.sort.dir === 'desc' ? 'desc' : 'asc'\n\t\t\t\t}\n\t\t\t\tif (Array.isArray(c.extend)) {\n\t\t\t\t\tconst extend = c.extend.filter((e) => typeof e === 'string' && e !== '')\n\t\t\t\t\tif (extend.length > 0) {\n\t\t\t\t\t\tparams._extend = extend\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tfor (const [key, value] of Object.entries(this.resolvedFilter)) {\n\t\t\t\t\tif (value && typeof value === 'object' && !Array.isArray(value)) {\n\t\t\t\t\t\tfor (const [op, operand] of Object.entries(value)) {\n\t\t\t\t\t\t\tparams[`${key}[${op}]`] = operand\n\t\t\t\t\t\t}\n\t\t\t\t\t} else if (value !== '' && value !== null && value !== undefined) {\n\t\t\t\t\t\tparams[key] = value\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tconst response = await axios.get(url, { params })\n\t\t\t\tconst data = response && response.data ? response.data : {}\n\t\t\t\tthis.rows = Array.isArray(data.results) ? data.results : []\n\t\t\t\tthis.total = typeof data.total === 'number' ? data.total : this.rows.length\n\t\t\t} catch (e) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.warn('[CnRecordListWidget] failed to load objects:', e)\n\t\t\t\tthis.rows = []\n\t\t\t\tthis.total = 0\n\t\t\t\tthis.error = true\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t\tthis.emitHeader()\n\t\t\t}\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-record-list__list {\n\tlist-style: none;\n\tmargin: 0;\n\tpadding: 0;\n}\n\n.cn-record-list__empty,\n.cn-record-list__error {\n\tcolor: var(--color-text-maxcontrast);\n\tmargin: 0;\n}\n\n.cn-record-list__loading {\n\tdisplay: flex;\n\tjustify-content: center;\n\tpadding: 8px 0;\n}\n\n/* rows */\n.cn-record-list--rows .cn-record-list__item + .cn-record-list__item {\n\tborder-top: 1px solid var(--color-border);\n}\n\n.cn-record-list__row {\n\talign-items: center;\n\tcolor: var(--color-main-text);\n\tdisplay: flex;\n\tgap: 16px;\n\tpadding: 12px 0;\n\ttext-decoration: none;\n}\n\n.cn-record-list__row--link:hover .cn-record-list__title,\n.cn-record-list__row--link:focus-visible .cn-record-list__title {\n\ttext-decoration: underline;\n}\n\n.cn-record-list__main {\n\tdisplay: flex;\n\tflex: 1 1 auto;\n\tflex-direction: column;\n\tgap: 2px;\n\tmin-width: 0;\n}\n\n.cn-record-list__title {\n\tfont-size: 15px;\n\tfont-weight: 700;\n}\n\n.cn-record-list__meta {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 13px;\n}\n\n.cn-record-list__aside {\n\talign-items: center;\n\tdisplay: flex;\n\tflex: none;\n\tgap: 16px;\n}\n\n.cn-record-list__trailing {\n\tfont-size: 14px;\n\twhite-space: nowrap;\n}\n\n.cn-record-list__chevron {\n\tcolor: var(--color-text-maxcontrast);\n\tflex: none;\n}\n\n/* feed */\n.cn-record-list--feed .cn-record-list__item + .cn-record-list__item {\n\tborder-top: 1px solid var(--color-border);\n}\n\n.cn-record-list__feed-row {\n\talign-items: flex-start;\n\tdisplay: flex;\n\tgap: 14px;\n\tpadding: 12px 0;\n}\n\n.cn-record-list__feed-icon {\n\talign-items: center;\n\tbackground: var(--color-primary-element-light);\n\tborder-radius: 50%;\n\tcolor: var(--color-primary-element-light-text, var(--color-primary-element));\n\tdisplay: flex;\n\tflex: none;\n\theight: 36px;\n\tjustify-content: center;\n\twidth: 36px;\n}\n\n.cn-record-list__text {\n\tfont-size: 15px;\n}\n\n.cn-record-list--feed .cn-record-list__main {\n\tcolor: var(--color-main-text);\n\ttext-decoration: none;\n}\n\n/* inline */\n.cn-record-list__inline-row {\n\tpadding: 4px 0;\n}\n\n.cn-record-list__label {\n\tmargin-inline-end: 4px;\n}\n\n.cn-record-list__inline-title {\n\tcolor: var(--color-main-text);\n}\n\na.cn-record-list__inline-title {\n\tcolor: var(--color-primary-element);\n\ttext-decoration: underline;\n}\n</style>\n\n<style>\n/* In a narrow side column a row stacks: title, meta, then pill and date. */\n.cn-detail-page__side .cn-record-list__row {\n\talign-items: flex-start;\n\tflex-direction: column;\n\tgap: 6px;\n}\n\n.cn-detail-page__side .cn-record-list__row .cn-record-list__title {\n\tcolor: var(--color-primary-element);\n\ttext-decoration: underline;\n}\n\n.cn-detail-page__side .cn-record-list__chevron {\n\tdisplay: none;\n}\n</style>\n"],"names":["_normalizeClass","_createElementBlock","_toDisplayString","_openBlock","_createVNode","_createBlock","_resolveDynamicComponent","_mergeProps","_createElementVNode","_createTextVNode"],"mappings":";;;;AAeO,EAAA,KAAA,EAAA,uBAAA;AAAA,EAAA,aAAA,EAAA,sBAAA;;;;AAGF,EAAA,KAAA,EAAA,yBAAA;;;;AAGmB,EAAA,KAAA,EAAA,uBAAA;AAAA,EAAA,aAAA,EAAA,sBAAA;;;;AAGX,EAAA,KAAK,EAAC,sBAAkB;;AAa1B,MAAA,UAAA,GAAA,EAAA,KAAK,EAAC,sBAAE,GAAA;;;AACG,EAAA,KAAA,EAAA,uBAAA;;;;AACO,EAAA,KAAC,EAAA,sBAAA;;;;AAET,EAAA,KAAA,EAAA,uBAAA;;;;AAMN,EAAA,KAAA,EAAA,0BAAA;;;;AAU8B,EAAA,KAAK,EAAC,0BAA0B;;;AACnE,EAAA,KAAM,EAAC,2BAA2B;AAAC,EAAA,aAAW,EAAC,MAAM;;;;AAIlC,EAAA,KAAK,EAAC,sBAAqB;;;;AAC3B,EAAA,KAAK,EAAC,sBAAe;;;;AAKzC,EAAA,KAAA,EAAA,4BAAA;;;;AACF,EAAA,KAAA,EAAA,uBAAA;;;;;;;;;;IAzDL,KAAG,EAAAA,cAAA,CAAA,CAAA,gBAAA,EAAA,CAAA,gBAAA,EACwB,QAAY,CAAA,YAAA,CAAA,CAAA,CAAA,CAAA;AACtC,IAAA,aAAU,EAAC,gBAAgB;;KACvB,KAAA,CAAA,KAAA;sBAALC,kBAEI,CAAA,GAAA,EAFJ,UAEI,EAAAC,eAAA,CADA,QAAU,CAAA,UAAA,CAAA,EAAA,CAAA,YAAA;SAEX,KAAA,CAAA,OAAA,IAAA,KAAA,CAAA,IAAA,CAAA,MAAA,KAAA,CAAA;AAAH,WAAAC,SAAA,EAAA,EAAAF,kBAAA,CAEG,OAFH,UAEG,EAAA;YADFG,WAAsC,CAAA,wBAAA,EAAA;AAAtB,cAAA,IAAI,EAAE,EAAE;AAAG,cAAA,IAAI,EAAE,QAAK,CAAA,YAAA;;;AAEzB,UAAA,CAAA,KAAA,CAAA,IAAI,CAAC,MAAG,KAAA,CAAA;0BAApBH,kBAEE,CAAA,GAAA,EAFF,UAEE,EAAAC,eAAA,CADA,QAAU,CAAA,UAAA,CAAA,EAAA,CAAA,YAAA;AAEZ,aAAAC,SAAA,EAAA,EAAAF,kBAAA,OAAA;8EAES,QAAA,CAAA,KAAA,EAAA,CAAF,IAAE,KAAA;;kBACR,GAAG,EAAE,IAAI,CAAC,GAAG;uBACT,EAAC,sBAAsB;+BACjB,EAAC,qBAAe;;mBAIrB,QAAa,CAAA,YAAA,KAAA,MAAA;AAFlB,uBAAAE,SAAA,EAAA,EAAAE,WAAA,CAuBWC,uBAtBP,CAAA,IAAK,CAAC,GAAG,GADbC,UAuBW,CAAA;;AApBV,wBAAA,KAAI,EAAA,CAAA,qBAAsB,EACrB,EAAA,2BAAA,EAAA,IAAA,CAAA,GAAA,KAAA,KAAA,EAAA,CAAA;AACE,uBAAA,EAAA,EAAA,OAAA,EAAA,IAAA,EAAA,EAAA,IAAI,CAAC,KAAK,CAAA,EAAA;+CAIX;6CAAA,kBAAA,EAAA;AAFM,4BAAA,CAAA,IAAI,CAAA,KAAA;AAAf,iCAAAJ,SAAA,EAAA,EAAAF,kBAAA,CAAe,MAAf,EAAA,UAAe,EAAAC,eAAA,CAAA,IAAA,CAAA,KAAA,CAAA,EAAA,CAAA,YAAA;;AACJ,4BAAA,CAAA,IAAI,CAAC,IAAI;iEAAG,kBAAA,EAAAA,eAAA,CAAA,IAAA,CAAA,IAAA,CAAA,EAAA,CAAA,YAAA;;;AAEb,0BAAA,CAAA,IAAI,CAAA,IAAA,IAAA,IAAA,CAAA,QAAA;+DAOT,kBAAA,EAAA;AALD,gCAAA,CAAA,IAAA,CAAA,IAAA;kDADLG,WAIgB,CAAA,wBAAA,EAAA;;AAFd,sCAAA,KAAK,EAAE,IAAE,CAAA,IAAA,CAAA,KAAA;AACT,sCAAA,OAAO,EAAE,IAAI,CAAC,IAAI,CAAC,OAAG;AACvB,sCAAA,IAAI,EAAC,OAAO;;;AACJ,gCAAA,CAAA,IAAA,CAAA,QAAA;AAAT,qCAAAF,SAAA,EAAA,EAAAF,kBAAA,CAAS,MAAT,EAAA,UAAS,EAAAC,eAAA,CAAA,IAAA,CAAA,QAAA,CAAA,EAAA,CAAA,YAAA;;;;;4CAEVG,WAIsB,CAAA,uBAAA,EAAA;;AAFrB,gCAAA,KAAK,EAAC,yBAAyB;AAC9B,gCAAA,IAAI,EAAE,EAAE;AACT,gCAAA,aAAW,EAAC,MAAM;;;;;;uBAIJ,QAAY,CAAA,YAAA,KAAA,MAAA;AAA1B,yBAAAF,SAAA,EAAA,EAAAF,kBAAA,CAQI,OARJ,WAQI,EAAA;AAPJ,0BAAAO,kBAAA,CAEG,QAFH,WAEG,EAAA;4BADHJ,WAAU,CAAA,iBAAA,EAAA;8BAAD,IAAC,EAAA,IAAA,CAAA,IAAA;AAAA,8BAAA,IAAA,EAAA,EAAA;;;oDAKCE,8CAAA;6CAFX,MAAsD;AAAzC,8BAAA,CAAA,IAAG,CAAC,IAAI;AAArB,mCAAAH,SAAA,EAAA,EAAAF,kBAAA,CAAsD,MAAtD,EAAA,WAAsD,EAAAC,eAAA,CAAA,IAAA,CAAA,IAAA,CAAA,EAAA,CAAA,YAAA;;AAC1C,8BAAA,CAAA,IAAI,CAAC,IAAI;AAArB,mCAAAC,SAAA,EAAA,EAAAF,kBAAA,CAA4C,MAA5C,EAAA,WAA4C,EAAAC,eAAA,CAAA,IAAA,CAAA,IAAA,CAAA,EAAA,CAAA,YAAA;;;;;;AAK9C,yBAAAC,SAAA,EAAA,EAAAF,kBAAA,QAAA;AACG,0BAAA,CAAA,IAAA,CAAA,KAAA;AAAF,+BAAAE,SAAA,EAAA,EAAAF,kBAAA,CAAE,MAAF,EAAA,WAAE,EAAAC,eAAA,CAAA,IAAA,CAAA,KAAA,CAAA,EAAA,CAAA,YAAA;;AACF,2BAAAC,SAAA,EAAA,EAAAE,WAAA,wBAAC,CAAA,IAAA,CAAA,GAAA,CAAD,EAAAE,UAAA,GAAC,KAAA,EAAA,8BAAA,EAAA,EAAA,EAAA,OAAA,EAAA,IAAA,EAAA,EAAA,IAAA,CAAA,KAAA,CAAA,EAAA;6CACA,MAAA;AAAA,8BAAAE,eAAA,CAAAP,eAAA,CAAA,IAAA,CAAA,KAAA,CAAA,EAAA,CAAA,YAAA;;;;;;;;;;;;;"}
|
|
1
|
+
{"version":3,"file":"CnRecordListWidget.vue3.js","sources":["../../../../src/components/CnRecordListWidget/CnRecordListWidget.vue?vue&type=template&id=7073b362&lang.js"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n\n<template>\n\t<div\n\t\tclass=\"cn-record-list\"\n\t\t:class=\"`cn-record-list--${presentation}`\"\n\t\tdata-testid=\"cn-record-list\">\n\t\t<p v-if=\"error\" class=\"cn-record-list__error\" data-testid=\"cn-record-list-error\">\n\t\t\t{{ errorLabel }}\n\t\t</p>\n\t\t<div v-else-if=\"loading && rows.length === 0\" class=\"cn-record-list__loading\">\n\t\t\t<NcLoadingIcon :size=\"20\" :name=\"loadingLabel\" />\n\t\t</div>\n\t\t<p v-else-if=\"rows.length === 0\" class=\"cn-record-list__empty\" data-testid=\"cn-record-list-empty\">\n\t\t\t{{ emptyLabel }}\n\t\t</p>\n\t\t<ul v-else class=\"cn-record-list__list\">\n\t\t\t<li\n\t\t\t\tv-for=\"item in items\"\n\t\t\t\t:key=\"item.key\"\n\t\t\t\tclass=\"cn-record-list__item\"\n\t\t\t\tdata-testid=\"cn-record-list-item\">\n\t\t\t\t<!-- rows: title and meta, then the pill, the date and a chevron. -->\n\t\t\t\t<component\n\t\t\t\t\t:is=\"item.tag\"\n\t\t\t\t\tv-if=\"presentation === 'rows'\"\n\t\t\t\t\tclass=\"cn-record-list__row\"\n\t\t\t\t\t:class=\"{ 'cn-record-list__row--link': item.tag !== 'div' }\"\n\t\t\t\t\tv-bind=\"item.attrs\">\n\t\t\t\t\t<span v-if=\"item.initials\"\n\t\t\t\t\t\tclass=\"cn-record-list__avatar\"\n\t\t\t\t\t\taria-hidden=\"true\"\n\t\t\t\t\t\tdata-testid=\"cn-record-list-avatar\">{{ item.initials }}</span>\n\t\t\t\t\t<span class=\"cn-record-list__main\">\n\t\t\t\t\t\t<span v-if=\"item.title\" class=\"cn-record-list__title\">{{ item.title }}</span>\n\t\t\t\t\t\t<span v-if=\"item.meta\" class=\"cn-record-list__meta\">{{ item.meta }}</span>\n\t\t\t\t\t</span>\n\t\t\t\t\t<span v-if=\"item.pill || item.trailing\" class=\"cn-record-list__aside\">\n\t\t\t\t\t\t<CnStatusBadge\n\t\t\t\t\t\t\tv-if=\"item.pill\"\n\t\t\t\t\t\t\t:label=\"item.pill.label\"\n\t\t\t\t\t\t\t:variant=\"item.pill.variant\"\n\t\t\t\t\t\t\tsize=\"small\" />\n\t\t\t\t\t\t<span v-if=\"item.trailing\" class=\"cn-record-list__trailing\">{{ item.trailing }}</span>\n\t\t\t\t\t</span>\n\t\t\t\t\t<ChevronRight\n\t\t\t\t\t\tv-if=\"item.tag !== 'div'\"\n\t\t\t\t\t\tclass=\"cn-record-list__chevron\"\n\t\t\t\t\t\t:size=\"18\"\n\t\t\t\t\t\taria-hidden=\"true\" />\n\t\t\t\t</component>\n\n\t\t\t\t<!-- feed: a round icon, a meta line and the text. -->\n\t\t\t\t<div v-else-if=\"presentation === 'feed'\" class=\"cn-record-list__feed-row\">\n\t\t\t\t\t<span class=\"cn-record-list__feed-icon\" aria-hidden=\"true\">\n\t\t\t\t\t\t<CnIcon :name=\"item.icon\" :size=\"18\" />\n\t\t\t\t\t</span>\n\t\t\t\t\t<component :is=\"item.tag\" class=\"cn-record-list__main\" v-bind=\"item.attrs\">\n\t\t\t\t\t\t<span v-if=\"item.meta\" class=\"cn-record-list__meta\">{{ item.meta }}</span>\n\t\t\t\t\t\t<span v-if=\"item.text\" class=\"cn-record-list__text\">{{ item.text }}</span>\n\t\t\t\t\t</component>\n\t\t\t\t</div>\n\n\t\t\t\t<!-- inline: a label and the title, the title a link. -->\n\t\t\t\t<div v-else class=\"cn-record-list__inline-row\">\n\t\t\t\t\t<span v-if=\"item.label\" class=\"cn-record-list__label\">{{ item.label }}</span>\n\t\t\t\t\t<component :is=\"item.tag\" class=\"cn-record-list__inline-title\" v-bind=\"item.attrs\">\n\t\t\t\t\t\t{{ item.title }}\n\t\t\t\t\t</component>\n\t\t\t\t</div>\n\t\t\t</li>\n\t\t</ul>\n\t</div>\n</template>\n\n<script>\nimport { subscribe, unsubscribe } from '@nextcloud/event-bus'\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcLoadingIcon } from '@nextcloud/vue'\nimport ChevronRight from 'vue-material-design-icons/ChevronRight.vue'\nimport CnIcon from '../CnIcon/CnIcon.vue'\nimport CnStatusBadge from '../CnStatusBadge/CnStatusBadge.vue'\nimport { isBadgeVariant } from '../../utils/badgeVariants.js'\nimport { fillTemplate, firstFilled } from '../../utils/fillTemplate.js'\nimport { initialsOf } from '../../utils/initials.js'\nimport { pageFilter, pageSort } from '../../utils/pageControls.js'\nimport { readPath } from '../../utils/readPath.js'\nimport { dropOptionalUnresolved, hasUnresolvedTokens, resolveFilterTokens } from '../../utils/resolveFilterTokens.js'\n\nconst PAGE_REFRESH_CHANNEL = 'cn:page:refresh'\nconst PRESENTATIONS = ['rows', 'feed', 'inline']\n\n/**\n * Unwrap a value that may be a ref.\n *\n * @param {unknown} value The value.\n * @return {unknown} The plain value.\n */\nfunction unwrap(value) {\n\treturn value && typeof value === 'object' && 'value' in value ? value.value : value\n}\n\n/**\n * CnRecordListWidget — the objects of another schema that point at this\n * record, drawn as the boards draw them (screens-relation-pages).\n *\n * A person page lists its open cases (\"Verlichting fietspad Lindelaan,\n * 2026-0082 · Woo-verzoek, In behandeling, Termijn 1 nov\"), its latest contact\n * moments (an icon by channel, a meta line, the summary) and its relations\n * (\"Secretaris van Buurtvereniging Oost\"). Each is a list of another schema's\n * objects filtered on a reference to this record. The source keys are those of\n * `object-list` (`register`, `schema`, `filter` with the `@objectId` /\n * `@object.<field>` / `@workspace.<key>` tokens, `sort`, `limit`, `extend`,\n * `rowRoute`, `pageControls`); `avatar` puts a circle with the initials of a name\n * template before the title; what differs is the drawing: `presentation` is `rows`, `feed`\n * or `inline`, and each line is a template (`\"{identifier} · {caseTypeName}\"`,\n * `\"Termijn {deadline|date}\"`).\n *\n * A content-only catalog type: on a card surface the detail host draws the\n * titled card. The widget emits `header` with `{ link, note }` (the\n * `headerLink` and `headerNote` with `{total}` filled in) for that card's\n * header.\n */\nexport default {\n\tname: 'CnRecordListWidget',\n\n\tcomponents: { ChevronRight, CnIcon, CnStatusBadge, NcLoadingIcon },\n\n\tinject: {\n\t\t/** Detail-page object context (`{ objectId, object, register, schema }`). */\n\t\tcnObjectContext: { default: null },\n\t\t/** Page workspace bag, for `@workspace.<key>` tokens. */\n\t\tcnWorkspaceContext: { default: null },\n\t\t/** The host app's translate function. */\n\t\tcnTranslate: { default: () => (key) => key },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The list config: `{ register, schema, filter?, sort?, limit?, extend?,\n\t\t * presentation?, title?, meta?, text?, label?, pill?, trailing?, icon?,\n\t\t * rowRoute?, rowIdField?, headerLink?, headerNote?, emptyText?, errorText? }`.\n\t\t *\n\t\t * @type {object}\n\t\t */\n\t\tcontent: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/** The current record's id, for the `@objectId` token. */\n\t\tobjectId: {\n\t\t\ttype: [String, Number],\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** The current record, for `@object.<field>` tokens. */\n\t\tobjectData: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\temits: ['header'],\n\n\tdata() {\n\t\treturn {\n\t\t\trows: [],\n\t\t\ttotal: 0,\n\t\t\tloading: false,\n\t\t\terror: false,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/** @return {string} `rows`, `feed` or `inline`. */\n\t\tpresentation() {\n\t\t\tconst value = this.content?.presentation\n\t\t\treturn PRESENTATIONS.includes(value) ? value : 'rows'\n\t\t},\n\n\t\t/** @return {object} The token context: record, id and workspace. */\n\t\ttokenCtx() {\n\t\t\tconst injected = unwrap(this.cnObjectContext) || {}\n\t\t\treturn {\n\t\t\t\t...injected,\n\t\t\t\tobjectId: this.objectId ?? injected.objectId ?? this.objectData?.id ?? null,\n\t\t\t\tobject: this.objectData || injected.object || null,\n\t\t\t\tworkspace: unwrap(this.cnWorkspaceContext) || {},\n\t\t\t}\n\t\t},\n\n\t\t/** @return {object} The filter with its tokens resolved. */\n\t\tresolvedFilter() {\n\t\t\tconst filter = pageFilter(this.content?.pageControls, this.tokenCtx.workspace, this.content?.filter)\n\t\t\treturn dropOptionalUnresolved(resolveFilterTokens(filter, this.tokenCtx))\n\t\t},\n\n\t\t/** @return {boolean} A required token (the record id, say) has no value yet. */\n\t\twaiting() {\n\t\t\treturn hasUnresolvedTokens(this.resolvedFilter)\n\t\t},\n\n\t\t/** @return {object|null} The sort: the page's sort control when the widget listens to one, else its own. */\n\t\teffectiveSort() {\n\t\t\treturn pageSort(this.content?.pageControls, this.tokenCtx.workspace, this.content?.sort)\n\t\t},\n\n\t\t/** @return {number} The fetch cap. */\n\t\tlimit() {\n\t\t\tconst value = Number(this.content?.limit)\n\t\t\treturn Number.isFinite(value) && value > 0 ? Math.floor(value) : 5\n\t\t},\n\n\t\t/** @return {string} What a fetch depends on; a change fetches again. */\n\t\trequestKey() {\n\t\t\tconst c = this.content || {}\n\t\t\treturn JSON.stringify([c.register, c.schema, this.resolvedFilter, this.effectiveSort, this.limit, c.extend, this.waiting])\n\t\t},\n\n\t\t/** @return {Array<object>} The rows ready to draw. */\n\t\titems() {\n\t\t\tconst c = this.content || {}\n\t\t\treturn this.rows.slice(0, this.limit).map((row, index) => {\n\t\t\t\tconst link = this.rowLink(row)\n\t\t\t\treturn {\n\t\t\t\t\tkey: (row && (row.id || row['@self']?.id)) || `row-${index}`,\n\t\t\t\t\ttag: link ? 'router-link' : 'div',\n\t\t\t\t\tattrs: link ? { to: link } : {},\n\t\t\t\t\tinitials: this.initialsFor(row),\n\t\t\t\t\ttitle: this.fill(c.title, row),\n\t\t\t\t\tmeta: this.fill(c.meta, row),\n\t\t\t\t\ttext: this.fill(c.text, row),\n\t\t\t\t\tlabel: this.fill(c.label, row),\n\t\t\t\t\ttrailing: c.trailing ? firstFilled(c.trailing, row, this.translate) : '',\n\t\t\t\t\tpill: this.pillOf(row),\n\t\t\t\t\ticon: this.iconOf(row),\n\t\t\t\t}\n\t\t\t})\n\t\t},\n\n\t\t/** @return {object|null} The header link with `{total}` filled and tokens resolved. */\n\t\theaderLink() {\n\t\t\tconst link = this.content?.headerLink\n\t\t\tif (!link || typeof link !== 'object' || typeof link.label !== 'string' || link.label === '') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst resolved = { label: this.withTotal(link.label) }\n\t\t\tif (link.route) {\n\t\t\t\tresolved.route = link.route\n\t\t\t\tif (link.params && typeof link.params === 'object') {\n\t\t\t\t\tresolved.params = resolveFilterTokens(link.params, this.tokenCtx)\n\t\t\t\t}\n\t\t\t\tif (link.query && typeof link.query === 'object') {\n\t\t\t\t\tresolved.query = resolveFilterTokens(link.query, this.tokenCtx)\n\t\t\t\t}\n\t\t\t} else if (typeof link.href === 'string') {\n\t\t\t\tresolved.href = link.href\n\t\t\t}\n\t\t\treturn resolved\n\t\t},\n\n\t\t/** @return {string} The header note with `{total}` filled. */\n\t\theaderNote() {\n\t\t\tconst note = this.content?.headerNote\n\t\t\treturn typeof note === 'string' && note !== '' ? this.withTotal(note) : ''\n\t\t},\n\n\t\t/** @return {string} The empty line. */\n\t\temptyLabel() {\n\t\t\treturn this.content?.emptyText ? this.translate(this.content.emptyText) : t('nextcloud-vue', 'Nothing here yet')\n\t\t},\n\n\t\t/** @return {string} The error line. */\n\t\terrorLabel() {\n\t\t\treturn this.content?.errorText ? this.translate(this.content.errorText) : t('nextcloud-vue', 'This list could not be loaded.')\n\t\t},\n\n\t\t/** @return {string} The spinner's accessible name. */\n\t\tloadingLabel() {\n\t\t\treturn t('nextcloud-vue', 'Loading …')\n\t\t},\n\t},\n\n\twatch: {\n\t\trequestKey() {\n\t\t\tthis.fetchRows()\n\t\t},\n\t},\n\n\tmounted() {\n\t\tthis.onRefresh = () => this.fetchRows()\n\t\tsubscribe(PAGE_REFRESH_CHANNEL, this.onRefresh)\n\t\tthis.fetchRows()\n\t},\n\n\tbeforeUnmount() {\n\t\tunsubscribe(PAGE_REFRESH_CHANNEL, this.onRefresh)\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Translate a manifest string through the host app.\n\t\t *\n\t\t * @param {string} text The text.\n\t\t * @return {string} The translation.\n\t\t */\n\t\ttranslate(text) {\n\t\t\treturn typeof this.cnTranslate === 'function' ? this.cnTranslate(text) : text\n\t\t},\n\n\t\t/**\n\t\t * The initials of the row's avatar (`avatar`: a name template such as\n\t\t * `\"{client.name}\"`, or `{ text }`), or '' without one.\n\t\t *\n\t\t * @param {object} row The row.\n\t\t * @return {string} One or two letters.\n\t\t * @spec openspec/changes/screens-record-list-avatar/specs/record-list-avatar/spec.md#requirement-a-record-list-row-can-carry-an-avatar\n\t\t */\n\t\tinitialsFor(row) {\n\t\t\tconst avatar = this.content?.avatar\n\t\t\tconst template = typeof avatar === 'string' ? avatar : (avatar && typeof avatar === 'object' ? avatar.text : '')\n\t\t\treturn initialsOf(this.fill(template, row))\n\t\t},\n\n\t\t/**\n\t\t * Fill a template from a row, after translating it.\n\t\t *\n\t\t * @param {string} template The template.\n\t\t * @param {object} row The row.\n\t\t * @return {string} The text, '' when a token is empty.\n\t\t */\n\t\tfill(template, row) {\n\t\t\treturn typeof template === 'string' && template !== '' ? fillTemplate(this.translate(template), row) : ''\n\t\t},\n\n\t\t/**\n\t\t * Replace `{total}` in a translated label.\n\t\t *\n\t\t * @param {string} label The label.\n\t\t * @return {string} The text.\n\t\t */\n\t\twithTotal(label) {\n\t\t\treturn this.translate(label).replace(/\\{total\\}/g, String(this.total))\n\t\t},\n\n\t\t/**\n\t\t * The route a row opens: `rowRoute` with the id at `rowIdField`.\n\t\t *\n\t\t * @param {object} row The row.\n\t\t * @return {object|null} The router location, or null.\n\t\t */\n\t\trowLink(row) {\n\t\t\tconst route = this.content?.rowRoute\n\t\t\tif (!route || !this.$router || !row) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst field = this.content?.rowIdField\n\t\t\tconst id = field ? readPath(row, field) : (row.id || row['@self']?.id)\n\t\t\tif (id === null || id === undefined || id === '' || typeof id === 'object') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\treturn { name: route, params: { id: String(id) } }\n\t\t},\n\n\t\t/**\n\t\t * The row's pill: `{ field, labels?, colorMap?, variant? }`.\n\t\t *\n\t\t * @param {object} row The row.\n\t\t * @return {{label: string, variant: string}|null} The pill, or null.\n\t\t */\n\t\tpillOf(row) {\n\t\t\tconst pill = this.content?.pill\n\t\t\tif (!pill || typeof pill !== 'object' || !pill.field) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst raw = readPath(row, pill.field)\n\t\t\tif (raw === null || raw === undefined || raw === '' || typeof raw === 'object') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst key = String(raw)\n\t\t\tconst labels = pill.labels && typeof pill.labels === 'object' ? pill.labels : {}\n\t\t\tconst map = pill.colorMap && typeof pill.colorMap === 'object' ? pill.colorMap : {}\n\t\t\tconst found = Object.keys(map).find((k) => k.toLowerCase() === key.toLowerCase())\n\t\t\tconst variant = found ? map[found] : pill.variant\n\t\t\treturn {\n\t\t\t\tlabel: labels[key] ? this.translate(labels[key]) : key,\n\t\t\t\tvariant: isBadgeVariant(variant) ? variant : 'default',\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * The feed icon: an icon name, or `{ field, map, default }`.\n\t\t *\n\t\t * @param {object} row The row.\n\t\t * @return {string} The MDI icon name.\n\t\t */\n\t\ticonOf(row) {\n\t\t\tconst icon = this.content?.icon\n\t\t\tif (typeof icon === 'string' && icon !== '') {\n\t\t\t\treturn icon\n\t\t\t}\n\t\t\tif (icon && typeof icon === 'object') {\n\t\t\t\tconst raw = icon.field ? readPath(row, icon.field) : undefined\n\t\t\t\tconst map = icon.map && typeof icon.map === 'object' ? icon.map : {}\n\t\t\t\tif (raw !== null && raw !== undefined && map[String(raw)]) {\n\t\t\t\t\treturn map[String(raw)]\n\t\t\t\t}\n\t\t\t\tif (typeof icon.default === 'string' && icon.default !== '') {\n\t\t\t\t\treturn icon.default\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn 'CircleSmall'\n\t\t},\n\n\t\t/** Tell the card around this list what its header shows. */\n\t\temitHeader() {\n\t\t\t/**\n\t\t\t * @event header Emitted after every read with the card header's parts.\n\t\t\t * @type {{link: (object|null), note: string}} The header link (resolved, with `{total}` filled) and the note.\n\t\t\t */\n\t\t\tthis.$emit('header', { link: this.headerLink, note: this.headerNote })\n\t\t},\n\n\t\t/**\n\t\t * Fetch the list. Sends nothing while a required token is unresolved\n\t\t * or the source is incomplete.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync fetchRows() {\n\t\t\tconst c = this.content || {}\n\t\t\tif (!c.register || !c.schema || this.waiting) {\n\t\t\t\tthis.rows = []\n\t\t\t\tthis.total = 0\n\t\t\t\tthis.error = false\n\t\t\t\tthis.emitHeader()\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.loading = true\n\t\t\tthis.error = false\n\t\t\ttry {\n\t\t\t\tconst [{ default: axios }, { generateUrl }] = await Promise.all([\n\t\t\t\t\timport('@nextcloud/axios'),\n\t\t\t\t\timport('@nextcloud/router'),\n\t\t\t\t])\n\t\t\t\tconst url = generateUrl('/apps/openregister/api/objects/{register}/{schema}', { register: c.register, schema: c.schema })\n\t\t\t\tconst params = { _limit: this.limit }\n\t\t\t\tconst sort = this.effectiveSort\n\t\t\t\tif (sort && typeof sort === 'object' && sort.field) {\n\t\t\t\t\tparams[`_order[${sort.field}]`] = sort.dir === 'desc' ? 'desc' : 'asc'\n\t\t\t\t}\n\t\t\t\tif (Array.isArray(c.extend)) {\n\t\t\t\t\tconst extend = c.extend.filter((e) => typeof e === 'string' && e !== '')\n\t\t\t\t\tif (extend.length > 0) {\n\t\t\t\t\t\tparams._extend = extend\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tfor (const [key, value] of Object.entries(this.resolvedFilter)) {\n\t\t\t\t\tif (value && typeof value === 'object' && !Array.isArray(value)) {\n\t\t\t\t\t\tfor (const [op, operand] of Object.entries(value)) {\n\t\t\t\t\t\t\tparams[`${key}[${op}]`] = operand\n\t\t\t\t\t\t}\n\t\t\t\t\t} else if (value !== '' && value !== null && value !== undefined) {\n\t\t\t\t\t\tparams[key] = value\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tconst response = await axios.get(url, { params })\n\t\t\t\tconst data = response && response.data ? response.data : {}\n\t\t\t\tthis.rows = Array.isArray(data.results) ? data.results : []\n\t\t\t\tthis.total = typeof data.total === 'number' ? data.total : this.rows.length\n\t\t\t} catch (e) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.warn('[CnRecordListWidget] failed to load objects:', e)\n\t\t\t\tthis.rows = []\n\t\t\t\tthis.total = 0\n\t\t\t\tthis.error = true\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t\tthis.emitHeader()\n\t\t\t}\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-record-list__list {\n\tlist-style: none;\n\tmargin: 0;\n\tpadding: 0;\n}\n\n.cn-record-list__empty,\n.cn-record-list__error {\n\tcolor: var(--color-text-maxcontrast);\n\tmargin: 0;\n}\n\n.cn-record-list__loading {\n\tdisplay: flex;\n\tjustify-content: center;\n\tpadding: 8px 0;\n}\n\n/* rows */\n.cn-record-list--rows .cn-record-list__item + .cn-record-list__item {\n\tborder-top: 1px solid var(--color-border);\n}\n\n.cn-record-list__row {\n\talign-items: center;\n\tcolor: var(--color-main-text);\n\tdisplay: flex;\n\tgap: 16px;\n\tpadding: 12px 0;\n\ttext-decoration: none;\n}\n\n.cn-record-list__row--link:hover .cn-record-list__title,\n.cn-record-list__row--link:focus-visible .cn-record-list__title {\n\ttext-decoration: underline;\n}\n\n.cn-record-list__avatar {\n\talign-items: center;\n\tbackground: var(--color-background-hover);\n\tborder-radius: 50%;\n\tcolor: var(--color-main-text);\n\tdisplay: flex;\n\tflex: none;\n\tfont-size: 12px;\n\tfont-weight: 700;\n\theight: 36px;\n\tjustify-content: center;\n\twidth: 36px;\n}\n\n.cn-record-list__main {\n\tdisplay: flex;\n\tflex: 1 1 auto;\n\tflex-direction: column;\n\tgap: 2px;\n\tmin-width: 0;\n}\n\n.cn-record-list__title {\n\tfont-size: 15px;\n\tfont-weight: 700;\n}\n\n.cn-record-list__meta {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 13px;\n}\n\n.cn-record-list__aside {\n\talign-items: center;\n\tdisplay: flex;\n\tflex: none;\n\tgap: 16px;\n}\n\n.cn-record-list__trailing {\n\tfont-size: 14px;\n\twhite-space: nowrap;\n}\n\n.cn-record-list__chevron {\n\tcolor: var(--color-text-maxcontrast);\n\tflex: none;\n}\n\n/* feed */\n.cn-record-list--feed .cn-record-list__item + .cn-record-list__item {\n\tborder-top: 1px solid var(--color-border);\n}\n\n.cn-record-list__feed-row {\n\talign-items: flex-start;\n\tdisplay: flex;\n\tgap: 14px;\n\tpadding: 12px 0;\n}\n\n.cn-record-list__feed-icon {\n\talign-items: center;\n\tbackground: var(--color-primary-element-light);\n\tborder-radius: 50%;\n\tcolor: var(--color-primary-element-light-text, var(--color-primary-element));\n\tdisplay: flex;\n\tflex: none;\n\theight: 36px;\n\tjustify-content: center;\n\twidth: 36px;\n}\n\n.cn-record-list__text {\n\tfont-size: 15px;\n}\n\n.cn-record-list--feed .cn-record-list__main {\n\tcolor: var(--color-main-text);\n\ttext-decoration: none;\n}\n\n/* inline */\n.cn-record-list__inline-row {\n\tpadding: 4px 0;\n}\n\n.cn-record-list__label {\n\tmargin-inline-end: 4px;\n}\n\n.cn-record-list__inline-title {\n\tcolor: var(--color-main-text);\n}\n\na.cn-record-list__inline-title {\n\tcolor: var(--color-primary-element);\n\ttext-decoration: underline;\n}\n</style>\n\n<style>\n/* In a narrow side column a row stacks: title, meta, then pill and date. */\n.cn-detail-page__side .cn-record-list__row {\n\talign-items: flex-start;\n\tflex-direction: column;\n\tgap: 6px;\n}\n\n.cn-detail-page__side .cn-record-list__row .cn-record-list__title {\n\tcolor: var(--color-primary-element);\n\ttext-decoration: underline;\n}\n\n.cn-detail-page__side .cn-record-list__chevron {\n\tdisplay: none;\n}\n\n/* A row is a touch target of at least 44px on a phone. */\n@media (max-width: 640px) {\n\t.cn-record-list__row {\n\t\tmin-height: 44px;\n\t}\n}\n</style>\n"],"names":["_normalizeClass","_createElementBlock","_toDisplayString","_openBlock","_createVNode","_createBlock","_resolveDynamicComponent","_mergeProps","_createElementVNode","_createTextVNode"],"mappings":"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"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import script from './CnSavedViewsWidget.vue2.js';
|
|
2
|
+
import { render } from './CnSavedViewsWidget.vue3.js';
|
|
3
|
+
|
|
4
|
+
script.render = render;
|
|
5
|
+
script.__scopeId = "data-v-8ca66162";
|
|
6
|
+
script.__file = "src/components/CnSavedViewsWidget/CnSavedViewsWidget.vue";
|
|
7
|
+
const __sfc_main = script;
|
|
8
|
+
|
|
9
|
+
export { __sfc_main as default };
|
|
10
|
+
//# sourceMappingURL=CnSavedViewsWidget.vue.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CnSavedViewsWidget.vue.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;"}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { getCurrentUser } from '@nextcloud/auth';
|
|
2
|
+
import { subscribe, unsubscribe } from '@nextcloud/event-bus';
|
|
3
|
+
import { translate } from '@nextcloud/l10n';
|
|
4
|
+
import { NcLoadingIcon } from '@nextcloud/vue';
|
|
5
|
+
import { useSavedViewsApi } from '../../composables/useSavedViewsApi.js';
|
|
6
|
+
import { fetchListTotal } from '../../utils/fetchFilterCounts.js';
|
|
7
|
+
import { isOwnView, viewMatchesScope, buildRouteQueryFromViewState, extractViewState } from '../../utils/savedViewHelpers.js';
|
|
8
|
+
|
|
9
|
+
const PAGE_REFRESH_CHANNEL = 'cn:page:refresh';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* CnSavedViewsWidget — the reader's own saved views as a list of links
|
|
13
|
+
* (`saved-views`), the "Opgeslagen weergaven" card of a board dashboard.
|
|
14
|
+
*
|
|
15
|
+
* `saved-view` draws the rows of ONE view that the manifest names by id. This
|
|
16
|
+
* one lists the views themselves and has no id to configure: it asks the Views
|
|
17
|
+
* API for the signed-in user's views (their own, or with `own: false` also the
|
|
18
|
+
* ones shared with them), keeps those that belong to the configured source,
|
|
19
|
+
* and links each to the index page `route` with the view's filters, search and
|
|
20
|
+
* sort in the query, which the index page applies on load. A view saved or
|
|
21
|
+
* deleted later shows up on the next load or page refresh.
|
|
22
|
+
*
|
|
23
|
+
* ```js
|
|
24
|
+
* { type: 'saved-views', content: { register: 'pipelinq', schema: 'ticket', route: 'Tickets', showCounts: true } }
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
var script = {
|
|
28
|
+
name: 'CnSavedViewsWidget',
|
|
29
|
+
|
|
30
|
+
components: { NcLoadingIcon },
|
|
31
|
+
|
|
32
|
+
inject: {
|
|
33
|
+
/** The host app's translate function. */
|
|
34
|
+
cnTranslate: { default: () => (key) => key },
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
props: {
|
|
38
|
+
/**
|
|
39
|
+
* The configuration: `{ register, schema, scope?, route, own?, showCounts?,
|
|
40
|
+
* limit?, emptyText?, errorText? }`. `register` and `schema` (or `scope`,
|
|
41
|
+
* the index page's `savedViewsScope`) pick the views of one list page;
|
|
42
|
+
* `route` is the name of that index page's route; `own` (default true)
|
|
43
|
+
* keeps the signed-in user's views only; `showCounts` adds how many records
|
|
44
|
+
* each view matches; `limit` caps the list (default 10).
|
|
45
|
+
*
|
|
46
|
+
* @type {{register?: string, schema?: string, scope?: string, route?: (string|object), own?: boolean, showCounts?: boolean, limit?: number, emptyText?: string, errorText?: string}}
|
|
47
|
+
*/
|
|
48
|
+
content: {
|
|
49
|
+
type: Object,
|
|
50
|
+
default: () => ({}),
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Injected views API, for tests and hosts that stub the transport.
|
|
55
|
+
*
|
|
56
|
+
* @type {{fetchViews: Function}|null}
|
|
57
|
+
*/
|
|
58
|
+
api: {
|
|
59
|
+
type: Object,
|
|
60
|
+
default: null,
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
data() {
|
|
65
|
+
return {
|
|
66
|
+
views: [],
|
|
67
|
+
counts: {},
|
|
68
|
+
loading: true,
|
|
69
|
+
error: false,
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
computed: {
|
|
74
|
+
/**
|
|
75
|
+
* The views to show: the reader's own (unless `own: false`) that belong to
|
|
76
|
+
* the configured source, newest first, capped at `limit`.
|
|
77
|
+
*
|
|
78
|
+
* @return {Array<object>} The View API objects.
|
|
79
|
+
*/
|
|
80
|
+
shown() {
|
|
81
|
+
const c = this.content || {};
|
|
82
|
+
const uid = this.userId();
|
|
83
|
+
const page = { scope: c.scope, register: c.register, schema: c.schema };
|
|
84
|
+
const limit = Number(c.limit);
|
|
85
|
+
return this.views
|
|
86
|
+
.filter((view) => (c.own === false ? true : isOwnView(view, uid)))
|
|
87
|
+
.filter((view) => viewMatchesScope(view, page))
|
|
88
|
+
.slice(0, limit > 0 ? Math.floor(limit) : 10)
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* The rows: a label (the view's name and its count) and the location that
|
|
93
|
+
* applies the view on the index page.
|
|
94
|
+
*
|
|
95
|
+
* @return {Array<{key: string, label: string, to: (object|null)}>} The rows.
|
|
96
|
+
*/
|
|
97
|
+
items() {
|
|
98
|
+
const route = this.content?.route;
|
|
99
|
+
return this.shown.map((view) => {
|
|
100
|
+
const count = this.counts[this.keyOf(view)];
|
|
101
|
+
const query = buildRouteQueryFromViewState(extractViewState(view));
|
|
102
|
+
const location = typeof route === 'string' && route !== '' ? { name: route, query } : (route && typeof route === 'object' && route.name ? { ...route, query } : null);
|
|
103
|
+
return {
|
|
104
|
+
key: this.keyOf(view),
|
|
105
|
+
label: Number.isFinite(count) ? `${view.name} (${count})` : String(view.name ?? ''),
|
|
106
|
+
to: location && this.$router ? location : null,
|
|
107
|
+
}
|
|
108
|
+
})
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
|
|
112
|
+
watch: {
|
|
113
|
+
'content.register': 'load',
|
|
114
|
+
'content.schema': 'load',
|
|
115
|
+
'content.scope': 'load',
|
|
116
|
+
'content.own': 'load',
|
|
117
|
+
},
|
|
118
|
+
|
|
119
|
+
created() {
|
|
120
|
+
this.load();
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
mounted() {
|
|
124
|
+
this.onRefresh = () => this.load();
|
|
125
|
+
subscribe(PAGE_REFRESH_CHANNEL, this.onRefresh);
|
|
126
|
+
},
|
|
127
|
+
|
|
128
|
+
beforeUnmount() {
|
|
129
|
+
unsubscribe(PAGE_REFRESH_CHANNEL, this.onRefresh);
|
|
130
|
+
},
|
|
131
|
+
|
|
132
|
+
methods: {
|
|
133
|
+
t: translate,
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Translate a manifest string through the host app.
|
|
137
|
+
*
|
|
138
|
+
* @param {string} text The text.
|
|
139
|
+
* @return {string} The translation, or '' for no text.
|
|
140
|
+
*/
|
|
141
|
+
tr(text) {
|
|
142
|
+
return typeof text === 'string' && text !== '' && typeof this.cnTranslate === 'function' ? this.cnTranslate(text) : ''
|
|
143
|
+
},
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* The signed-in user's id, or '' when it cannot be read.
|
|
147
|
+
*
|
|
148
|
+
* @return {string} The id.
|
|
149
|
+
*/
|
|
150
|
+
userId() {
|
|
151
|
+
try {
|
|
152
|
+
return getCurrentUser()?.uid || ''
|
|
153
|
+
} catch {
|
|
154
|
+
return ''
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* A view's key: its id, else its uuid.
|
|
160
|
+
*
|
|
161
|
+
* @param {object} view The View API object.
|
|
162
|
+
* @return {string} The key.
|
|
163
|
+
*/
|
|
164
|
+
keyOf(view) {
|
|
165
|
+
return String(view?.id ?? view?.uuid ?? view?.name ?? '')
|
|
166
|
+
},
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Read the views, and when asked how many records each matches. A failed
|
|
170
|
+
* read says so; a failed count only leaves that view without one.
|
|
171
|
+
*
|
|
172
|
+
* @return {Promise<void>}
|
|
173
|
+
* @spec openspec/changes/screens-own-saved-views/specs/own-saved-views/spec.md#requirement-a-dashboard-lists-the-readers-own-saved-views
|
|
174
|
+
*/
|
|
175
|
+
async load() {
|
|
176
|
+
this.loading = true;
|
|
177
|
+
this.error = false;
|
|
178
|
+
try {
|
|
179
|
+
const api = this.api || useSavedViewsApi();
|
|
180
|
+
this.views = await api.fetchViews();
|
|
181
|
+
} catch (e) {
|
|
182
|
+
// eslint-disable-next-line no-console
|
|
183
|
+
console.warn('[CnSavedViewsWidget] failed to load saved views:', e);
|
|
184
|
+
this.views = [];
|
|
185
|
+
this.error = true;
|
|
186
|
+
this.loading = false;
|
|
187
|
+
return
|
|
188
|
+
}
|
|
189
|
+
this.loading = false;
|
|
190
|
+
await this.loadCounts();
|
|
191
|
+
},
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* One list request per shown view (`_limit=1`, the `total`), with the
|
|
195
|
+
* view's filters.
|
|
196
|
+
*
|
|
197
|
+
* @return {Promise<void>}
|
|
198
|
+
*/
|
|
199
|
+
async loadCounts() {
|
|
200
|
+
const c = this.content || {};
|
|
201
|
+
if (c.showCounts !== true || !c.register || !c.schema) {
|
|
202
|
+
this.counts = {};
|
|
203
|
+
return
|
|
204
|
+
}
|
|
205
|
+
const next = {};
|
|
206
|
+
await Promise.all(this.shown.map(async (view) => {
|
|
207
|
+
try {
|
|
208
|
+
const total = await fetchListTotal(c.register, c.schema, extractViewState(view).filters, {});
|
|
209
|
+
if (total !== null) {
|
|
210
|
+
next[this.keyOf(view)] = total;
|
|
211
|
+
}
|
|
212
|
+
} catch {
|
|
213
|
+
// No count for this view.
|
|
214
|
+
}
|
|
215
|
+
}));
|
|
216
|
+
this.counts = next;
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
export { script as default };
|
|
222
|
+
//# sourceMappingURL=CnSavedViewsWidget.vue2.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CnSavedViewsWidget.vue2.js","sources":["../../../../src/components/CnSavedViewsWidget/CnSavedViewsWidget.vue"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n\n<template>\n\t<div class=\"cn-saved-views-widget\" data-testid=\"cn-saved-views-widget\">\n\t\t<p v-if=\"error\" class=\"cn-saved-views-widget__note\" data-testid=\"cn-saved-views-error\">\n\t\t\t{{ tr(content.errorText) || t('nextcloud-vue', 'Your saved views could not be loaded.') }}\n\t\t</p>\n\t\t<div v-else-if=\"loading\" class=\"cn-saved-views-widget__loading\">\n\t\t\t<NcLoadingIcon :size=\"20\" :name=\"t('nextcloud-vue', 'Loading …')\" />\n\t\t</div>\n\t\t<p v-else-if=\"items.length === 0\" class=\"cn-saved-views-widget__note\" data-testid=\"cn-saved-views-empty\">\n\t\t\t{{ tr(content.emptyText) || t('nextcloud-vue', 'You have no saved views yet.') }}\n\t\t</p>\n\t\t<ul v-else class=\"cn-saved-views-widget__list\">\n\t\t\t<li v-for=\"item in items\" :key=\"item.key\" class=\"cn-saved-views-widget__item\">\n\t\t\t\t<component\n\t\t\t\t\t:is=\"item.to ? 'router-link' : 'span'\"\n\t\t\t\t\tclass=\"cn-saved-views-widget__link\"\n\t\t\t\t\t:to=\"item.to || undefined\"\n\t\t\t\t\tdata-testid=\"cn-saved-views-link\">\n\t\t\t\t\t{{ item.label }}\n\t\t\t\t</component>\n\t\t\t</li>\n\t\t</ul>\n\t</div>\n</template>\n\n<script>\nimport { getCurrentUser } from '@nextcloud/auth'\nimport { subscribe, unsubscribe } from '@nextcloud/event-bus'\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcLoadingIcon } from '@nextcloud/vue'\nimport { useSavedViewsApi } from '../../composables/useSavedViewsApi.js'\nimport { fetchListTotal } from '../../utils/fetchFilterCounts.js'\nimport { buildRouteQueryFromViewState, extractViewState, isOwnView, viewMatchesScope } from '../../utils/savedViewHelpers.js'\n\nconst PAGE_REFRESH_CHANNEL = 'cn:page:refresh'\n\n/**\n * CnSavedViewsWidget — the reader's own saved views as a list of links\n * (`saved-views`), the \"Opgeslagen weergaven\" card of a board dashboard.\n *\n * `saved-view` draws the rows of ONE view that the manifest names by id. This\n * one lists the views themselves and has no id to configure: it asks the Views\n * API for the signed-in user's views (their own, or with `own: false` also the\n * ones shared with them), keeps those that belong to the configured source,\n * and links each to the index page `route` with the view's filters, search and\n * sort in the query, which the index page applies on load. A view saved or\n * deleted later shows up on the next load or page refresh.\n *\n * ```js\n * { type: 'saved-views', content: { register: 'pipelinq', schema: 'ticket', route: 'Tickets', showCounts: true } }\n * ```\n */\nexport default {\n\tname: 'CnSavedViewsWidget',\n\n\tcomponents: { NcLoadingIcon },\n\n\tinject: {\n\t\t/** The host app's translate function. */\n\t\tcnTranslate: { default: () => (key) => key },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The configuration: `{ register, schema, scope?, route, own?, showCounts?,\n\t\t * limit?, emptyText?, errorText? }`. `register` and `schema` (or `scope`,\n\t\t * the index page's `savedViewsScope`) pick the views of one list page;\n\t\t * `route` is the name of that index page's route; `own` (default true)\n\t\t * keeps the signed-in user's views only; `showCounts` adds how many records\n\t\t * each view matches; `limit` caps the list (default 10).\n\t\t *\n\t\t * @type {{register?: string, schema?: string, scope?: string, route?: (string|object), own?: boolean, showCounts?: boolean, limit?: number, emptyText?: string, errorText?: string}}\n\t\t */\n\t\tcontent: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * Injected views API, for tests and hosts that stub the transport.\n\t\t *\n\t\t * @type {{fetchViews: Function}|null}\n\t\t */\n\t\tapi: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\tdata() {\n\t\treturn {\n\t\t\tviews: [],\n\t\t\tcounts: {},\n\t\t\tloading: true,\n\t\t\terror: false,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * The views to show: the reader's own (unless `own: false`) that belong to\n\t\t * the configured source, newest first, capped at `limit`.\n\t\t *\n\t\t * @return {Array<object>} The View API objects.\n\t\t */\n\t\tshown() {\n\t\t\tconst c = this.content || {}\n\t\t\tconst uid = this.userId()\n\t\t\tconst page = { scope: c.scope, register: c.register, schema: c.schema }\n\t\t\tconst limit = Number(c.limit)\n\t\t\treturn this.views\n\t\t\t\t.filter((view) => (c.own === false ? true : isOwnView(view, uid)))\n\t\t\t\t.filter((view) => viewMatchesScope(view, page))\n\t\t\t\t.slice(0, limit > 0 ? Math.floor(limit) : 10)\n\t\t},\n\n\t\t/**\n\t\t * The rows: a label (the view's name and its count) and the location that\n\t\t * applies the view on the index page.\n\t\t *\n\t\t * @return {Array<{key: string, label: string, to: (object|null)}>} The rows.\n\t\t */\n\t\titems() {\n\t\t\tconst route = this.content?.route\n\t\t\treturn this.shown.map((view) => {\n\t\t\t\tconst count = this.counts[this.keyOf(view)]\n\t\t\t\tconst query = buildRouteQueryFromViewState(extractViewState(view))\n\t\t\t\tconst location = typeof route === 'string' && route !== '' ? { name: route, query } : (route && typeof route === 'object' && route.name ? { ...route, query } : null)\n\t\t\t\treturn {\n\t\t\t\t\tkey: this.keyOf(view),\n\t\t\t\t\tlabel: Number.isFinite(count) ? `${view.name} (${count})` : String(view.name ?? ''),\n\t\t\t\t\tto: location && this.$router ? location : null,\n\t\t\t\t}\n\t\t\t})\n\t\t},\n\t},\n\n\twatch: {\n\t\t'content.register': 'load',\n\t\t'content.schema': 'load',\n\t\t'content.scope': 'load',\n\t\t'content.own': 'load',\n\t},\n\n\tcreated() {\n\t\tthis.load()\n\t},\n\n\tmounted() {\n\t\tthis.onRefresh = () => this.load()\n\t\tsubscribe(PAGE_REFRESH_CHANNEL, this.onRefresh)\n\t},\n\n\tbeforeUnmount() {\n\t\tunsubscribe(PAGE_REFRESH_CHANNEL, this.onRefresh)\n\t},\n\n\tmethods: {\n\t\tt,\n\n\t\t/**\n\t\t * Translate a manifest string through the host app.\n\t\t *\n\t\t * @param {string} text The text.\n\t\t * @return {string} The translation, or '' for no text.\n\t\t */\n\t\ttr(text) {\n\t\t\treturn typeof text === 'string' && text !== '' && typeof this.cnTranslate === 'function' ? this.cnTranslate(text) : ''\n\t\t},\n\n\t\t/**\n\t\t * The signed-in user's id, or '' when it cannot be read.\n\t\t *\n\t\t * @return {string} The id.\n\t\t */\n\t\tuserId() {\n\t\t\ttry {\n\t\t\t\treturn getCurrentUser()?.uid || ''\n\t\t\t} catch {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * A view's key: its id, else its uuid.\n\t\t *\n\t\t * @param {object} view The View API object.\n\t\t * @return {string} The key.\n\t\t */\n\t\tkeyOf(view) {\n\t\t\treturn String(view?.id ?? view?.uuid ?? view?.name ?? '')\n\t\t},\n\n\t\t/**\n\t\t * Read the views, and when asked how many records each matches. A failed\n\t\t * read says so; a failed count only leaves that view without one.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t * @spec openspec/changes/screens-own-saved-views/specs/own-saved-views/spec.md#requirement-a-dashboard-lists-the-readers-own-saved-views\n\t\t */\n\t\tasync load() {\n\t\t\tthis.loading = true\n\t\t\tthis.error = false\n\t\t\ttry {\n\t\t\t\tconst api = this.api || useSavedViewsApi()\n\t\t\t\tthis.views = await api.fetchViews()\n\t\t\t} catch (e) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.warn('[CnSavedViewsWidget] failed to load saved views:', e)\n\t\t\t\tthis.views = []\n\t\t\t\tthis.error = true\n\t\t\t\tthis.loading = false\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.loading = false\n\t\t\tawait this.loadCounts()\n\t\t},\n\n\t\t/**\n\t\t * One list request per shown view (`_limit=1`, the `total`), with the\n\t\t * view's filters.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync loadCounts() {\n\t\t\tconst c = this.content || {}\n\t\t\tif (c.showCounts !== true || !c.register || !c.schema) {\n\t\t\t\tthis.counts = {}\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst next = {}\n\t\t\tawait Promise.all(this.shown.map(async (view) => {\n\t\t\t\ttry {\n\t\t\t\t\tconst total = await fetchListTotal(c.register, c.schema, extractViewState(view).filters, {})\n\t\t\t\t\tif (total !== null) {\n\t\t\t\t\t\tnext[this.keyOf(view)] = total\n\t\t\t\t\t}\n\t\t\t\t} catch {\n\t\t\t\t\t// No count for this view.\n\t\t\t\t}\n\t\t\t}))\n\t\t\tthis.counts = next\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-saved-views-widget__list {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 10px;\n\tlist-style: none;\n\tmargin: 0;\n\tpadding: 0;\n}\n\n.cn-saved-views-widget__link {\n\tcolor: var(--color-primary-element);\n\tdisplay: inline-block;\n\tfont-size: 14px;\n\tfont-weight: 600;\n\tmin-height: 24px;\n\ttext-decoration: none;\n}\n\n.cn-saved-views-widget__link:hover,\n.cn-saved-views-widget__link:focus-visible {\n\ttext-decoration: underline;\n}\n\n.cn-saved-views-widget__note {\n\tcolor: var(--color-text-maxcontrast);\n\tmargin: 0;\n}\n\n.cn-saved-views-widget__loading {\n\tdisplay: flex;\n\tjustify-content: center;\n\tpadding: 8px 0;\n}\n\n/* A phone: each link is a 44px touch target. */\n@media (max-width: 640px) {\n\t.cn-saved-views-widget__link {\n\t\talign-items: center;\n\t\tdisplay: inline-flex;\n\t\tmin-height: 44px;\n\t}\n}\n</style>\n"],"names":["t"],"mappings":"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"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { resolveComponent, openBlock, createElementBlock, toDisplayString, createVNode, Fragment, renderList, createBlock, resolveDynamicComponent, withCtx, createTextVNode } from 'vue';
|
|
2
|
+
|
|
3
|
+
const _hoisted_1 = {
|
|
4
|
+
class: "cn-saved-views-widget",
|
|
5
|
+
"data-testid": "cn-saved-views-widget"
|
|
6
|
+
};
|
|
7
|
+
const _hoisted_2 = {
|
|
8
|
+
key: 0,
|
|
9
|
+
class: "cn-saved-views-widget__note",
|
|
10
|
+
"data-testid": "cn-saved-views-error"
|
|
11
|
+
};
|
|
12
|
+
const _hoisted_3 = {
|
|
13
|
+
key: 1,
|
|
14
|
+
class: "cn-saved-views-widget__loading"
|
|
15
|
+
};
|
|
16
|
+
const _hoisted_4 = {
|
|
17
|
+
key: 2,
|
|
18
|
+
class: "cn-saved-views-widget__note",
|
|
19
|
+
"data-testid": "cn-saved-views-empty"
|
|
20
|
+
};
|
|
21
|
+
const _hoisted_5 = {
|
|
22
|
+
key: 3,
|
|
23
|
+
class: "cn-saved-views-widget__list"
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
function render(_ctx, _cache, $props, $setup, $data, $options) {
|
|
27
|
+
const _component_NcLoadingIcon = resolveComponent("NcLoadingIcon");
|
|
28
|
+
|
|
29
|
+
return (openBlock(), createElementBlock("div", _hoisted_1, [
|
|
30
|
+
($data.error)
|
|
31
|
+
? (openBlock(), createElementBlock("p", _hoisted_2, toDisplayString($options.tr($props.content.errorText) || $options.t('nextcloud-vue', 'Your saved views could not be loaded.')), 1 /* TEXT */))
|
|
32
|
+
: ($data.loading)
|
|
33
|
+
? (openBlock(), createElementBlock("div", _hoisted_3, [
|
|
34
|
+
createVNode(_component_NcLoadingIcon, {
|
|
35
|
+
size: 20,
|
|
36
|
+
name: $options.t('nextcloud-vue', 'Loading …')
|
|
37
|
+
}, null, 8 /* PROPS */, ["name"])
|
|
38
|
+
]))
|
|
39
|
+
: ($options.items.length === 0)
|
|
40
|
+
? (openBlock(), createElementBlock("p", _hoisted_4, toDisplayString($options.tr($props.content.emptyText) || $options.t('nextcloud-vue', 'You have no saved views yet.')), 1 /* TEXT */))
|
|
41
|
+
: (openBlock(), createElementBlock("ul", _hoisted_5, [
|
|
42
|
+
(openBlock(true), createElementBlock(Fragment, null, renderList($options.items, (item) => {
|
|
43
|
+
return (openBlock(), createElementBlock("li", {
|
|
44
|
+
key: item.key,
|
|
45
|
+
class: "cn-saved-views-widget__item"
|
|
46
|
+
}, [
|
|
47
|
+
(openBlock(), createBlock(resolveDynamicComponent(item.to ? 'router-link' : 'span'), {
|
|
48
|
+
class: "cn-saved-views-widget__link",
|
|
49
|
+
to: item.to || undefined,
|
|
50
|
+
"data-testid": "cn-saved-views-link"
|
|
51
|
+
}, {
|
|
52
|
+
default: withCtx(() => [
|
|
53
|
+
createTextVNode(toDisplayString(item.label), 1 /* TEXT */)
|
|
54
|
+
]),
|
|
55
|
+
_: 2 /* DYNAMIC */
|
|
56
|
+
}, 1032 /* PROPS, DYNAMIC_SLOTS */, ["to"]))
|
|
57
|
+
]))
|
|
58
|
+
}), 128 /* KEYED_FRAGMENT */))
|
|
59
|
+
]))
|
|
60
|
+
]))
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export { render };
|
|
64
|
+
//# sourceMappingURL=CnSavedViewsWidget.vue3.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CnSavedViewsWidget.vue3.js","sources":["../../../../src/components/CnSavedViewsWidget/CnSavedViewsWidget.vue?vue&type=template&id=8ca66162&lang.js"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n\n<template>\n\t<div class=\"cn-saved-views-widget\" data-testid=\"cn-saved-views-widget\">\n\t\t<p v-if=\"error\" class=\"cn-saved-views-widget__note\" data-testid=\"cn-saved-views-error\">\n\t\t\t{{ tr(content.errorText) || t('nextcloud-vue', 'Your saved views could not be loaded.') }}\n\t\t</p>\n\t\t<div v-else-if=\"loading\" class=\"cn-saved-views-widget__loading\">\n\t\t\t<NcLoadingIcon :size=\"20\" :name=\"t('nextcloud-vue', 'Loading …')\" />\n\t\t</div>\n\t\t<p v-else-if=\"items.length === 0\" class=\"cn-saved-views-widget__note\" data-testid=\"cn-saved-views-empty\">\n\t\t\t{{ tr(content.emptyText) || t('nextcloud-vue', 'You have no saved views yet.') }}\n\t\t</p>\n\t\t<ul v-else class=\"cn-saved-views-widget__list\">\n\t\t\t<li v-for=\"item in items\" :key=\"item.key\" class=\"cn-saved-views-widget__item\">\n\t\t\t\t<component\n\t\t\t\t\t:is=\"item.to ? 'router-link' : 'span'\"\n\t\t\t\t\tclass=\"cn-saved-views-widget__link\"\n\t\t\t\t\t:to=\"item.to || undefined\"\n\t\t\t\t\tdata-testid=\"cn-saved-views-link\">\n\t\t\t\t\t{{ item.label }}\n\t\t\t\t</component>\n\t\t\t</li>\n\t\t</ul>\n\t</div>\n</template>\n\n<script>\nimport { getCurrentUser } from '@nextcloud/auth'\nimport { subscribe, unsubscribe } from '@nextcloud/event-bus'\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcLoadingIcon } from '@nextcloud/vue'\nimport { useSavedViewsApi } from '../../composables/useSavedViewsApi.js'\nimport { fetchListTotal } from '../../utils/fetchFilterCounts.js'\nimport { buildRouteQueryFromViewState, extractViewState, isOwnView, viewMatchesScope } from '../../utils/savedViewHelpers.js'\n\nconst PAGE_REFRESH_CHANNEL = 'cn:page:refresh'\n\n/**\n * CnSavedViewsWidget — the reader's own saved views as a list of links\n * (`saved-views`), the \"Opgeslagen weergaven\" card of a board dashboard.\n *\n * `saved-view` draws the rows of ONE view that the manifest names by id. This\n * one lists the views themselves and has no id to configure: it asks the Views\n * API for the signed-in user's views (their own, or with `own: false` also the\n * ones shared with them), keeps those that belong to the configured source,\n * and links each to the index page `route` with the view's filters, search and\n * sort in the query, which the index page applies on load. A view saved or\n * deleted later shows up on the next load or page refresh.\n *\n * ```js\n * { type: 'saved-views', content: { register: 'pipelinq', schema: 'ticket', route: 'Tickets', showCounts: true } }\n * ```\n */\nexport default {\n\tname: 'CnSavedViewsWidget',\n\n\tcomponents: { NcLoadingIcon },\n\n\tinject: {\n\t\t/** The host app's translate function. */\n\t\tcnTranslate: { default: () => (key) => key },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The configuration: `{ register, schema, scope?, route, own?, showCounts?,\n\t\t * limit?, emptyText?, errorText? }`. `register` and `schema` (or `scope`,\n\t\t * the index page's `savedViewsScope`) pick the views of one list page;\n\t\t * `route` is the name of that index page's route; `own` (default true)\n\t\t * keeps the signed-in user's views only; `showCounts` adds how many records\n\t\t * each view matches; `limit` caps the list (default 10).\n\t\t *\n\t\t * @type {{register?: string, schema?: string, scope?: string, route?: (string|object), own?: boolean, showCounts?: boolean, limit?: number, emptyText?: string, errorText?: string}}\n\t\t */\n\t\tcontent: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * Injected views API, for tests and hosts that stub the transport.\n\t\t *\n\t\t * @type {{fetchViews: Function}|null}\n\t\t */\n\t\tapi: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\tdata() {\n\t\treturn {\n\t\t\tviews: [],\n\t\t\tcounts: {},\n\t\t\tloading: true,\n\t\t\terror: false,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * The views to show: the reader's own (unless `own: false`) that belong to\n\t\t * the configured source, newest first, capped at `limit`.\n\t\t *\n\t\t * @return {Array<object>} The View API objects.\n\t\t */\n\t\tshown() {\n\t\t\tconst c = this.content || {}\n\t\t\tconst uid = this.userId()\n\t\t\tconst page = { scope: c.scope, register: c.register, schema: c.schema }\n\t\t\tconst limit = Number(c.limit)\n\t\t\treturn this.views\n\t\t\t\t.filter((view) => (c.own === false ? true : isOwnView(view, uid)))\n\t\t\t\t.filter((view) => viewMatchesScope(view, page))\n\t\t\t\t.slice(0, limit > 0 ? Math.floor(limit) : 10)\n\t\t},\n\n\t\t/**\n\t\t * The rows: a label (the view's name and its count) and the location that\n\t\t * applies the view on the index page.\n\t\t *\n\t\t * @return {Array<{key: string, label: string, to: (object|null)}>} The rows.\n\t\t */\n\t\titems() {\n\t\t\tconst route = this.content?.route\n\t\t\treturn this.shown.map((view) => {\n\t\t\t\tconst count = this.counts[this.keyOf(view)]\n\t\t\t\tconst query = buildRouteQueryFromViewState(extractViewState(view))\n\t\t\t\tconst location = typeof route === 'string' && route !== '' ? { name: route, query } : (route && typeof route === 'object' && route.name ? { ...route, query } : null)\n\t\t\t\treturn {\n\t\t\t\t\tkey: this.keyOf(view),\n\t\t\t\t\tlabel: Number.isFinite(count) ? `${view.name} (${count})` : String(view.name ?? ''),\n\t\t\t\t\tto: location && this.$router ? location : null,\n\t\t\t\t}\n\t\t\t})\n\t\t},\n\t},\n\n\twatch: {\n\t\t'content.register': 'load',\n\t\t'content.schema': 'load',\n\t\t'content.scope': 'load',\n\t\t'content.own': 'load',\n\t},\n\n\tcreated() {\n\t\tthis.load()\n\t},\n\n\tmounted() {\n\t\tthis.onRefresh = () => this.load()\n\t\tsubscribe(PAGE_REFRESH_CHANNEL, this.onRefresh)\n\t},\n\n\tbeforeUnmount() {\n\t\tunsubscribe(PAGE_REFRESH_CHANNEL, this.onRefresh)\n\t},\n\n\tmethods: {\n\t\tt,\n\n\t\t/**\n\t\t * Translate a manifest string through the host app.\n\t\t *\n\t\t * @param {string} text The text.\n\t\t * @return {string} The translation, or '' for no text.\n\t\t */\n\t\ttr(text) {\n\t\t\treturn typeof text === 'string' && text !== '' && typeof this.cnTranslate === 'function' ? this.cnTranslate(text) : ''\n\t\t},\n\n\t\t/**\n\t\t * The signed-in user's id, or '' when it cannot be read.\n\t\t *\n\t\t * @return {string} The id.\n\t\t */\n\t\tuserId() {\n\t\t\ttry {\n\t\t\t\treturn getCurrentUser()?.uid || ''\n\t\t\t} catch {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * A view's key: its id, else its uuid.\n\t\t *\n\t\t * @param {object} view The View API object.\n\t\t * @return {string} The key.\n\t\t */\n\t\tkeyOf(view) {\n\t\t\treturn String(view?.id ?? view?.uuid ?? view?.name ?? '')\n\t\t},\n\n\t\t/**\n\t\t * Read the views, and when asked how many records each matches. A failed\n\t\t * read says so; a failed count only leaves that view without one.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t * @spec openspec/changes/screens-own-saved-views/specs/own-saved-views/spec.md#requirement-a-dashboard-lists-the-readers-own-saved-views\n\t\t */\n\t\tasync load() {\n\t\t\tthis.loading = true\n\t\t\tthis.error = false\n\t\t\ttry {\n\t\t\t\tconst api = this.api || useSavedViewsApi()\n\t\t\t\tthis.views = await api.fetchViews()\n\t\t\t} catch (e) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.warn('[CnSavedViewsWidget] failed to load saved views:', e)\n\t\t\t\tthis.views = []\n\t\t\t\tthis.error = true\n\t\t\t\tthis.loading = false\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.loading = false\n\t\t\tawait this.loadCounts()\n\t\t},\n\n\t\t/**\n\t\t * One list request per shown view (`_limit=1`, the `total`), with the\n\t\t * view's filters.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync loadCounts() {\n\t\t\tconst c = this.content || {}\n\t\t\tif (c.showCounts !== true || !c.register || !c.schema) {\n\t\t\t\tthis.counts = {}\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst next = {}\n\t\t\tawait Promise.all(this.shown.map(async (view) => {\n\t\t\t\ttry {\n\t\t\t\t\tconst total = await fetchListTotal(c.register, c.schema, extractViewState(view).filters, {})\n\t\t\t\t\tif (total !== null) {\n\t\t\t\t\t\tnext[this.keyOf(view)] = total\n\t\t\t\t\t}\n\t\t\t\t} catch {\n\t\t\t\t\t// No count for this view.\n\t\t\t\t}\n\t\t\t}))\n\t\t\tthis.counts = next\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-saved-views-widget__list {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 10px;\n\tlist-style: none;\n\tmargin: 0;\n\tpadding: 0;\n}\n\n.cn-saved-views-widget__link {\n\tcolor: var(--color-primary-element);\n\tdisplay: inline-block;\n\tfont-size: 14px;\n\tfont-weight: 600;\n\tmin-height: 24px;\n\ttext-decoration: none;\n}\n\n.cn-saved-views-widget__link:hover,\n.cn-saved-views-widget__link:focus-visible {\n\ttext-decoration: underline;\n}\n\n.cn-saved-views-widget__note {\n\tcolor: var(--color-text-maxcontrast);\n\tmargin: 0;\n}\n\n.cn-saved-views-widget__loading {\n\tdisplay: flex;\n\tjustify-content: center;\n\tpadding: 8px 0;\n}\n\n/* A phone: each link is a 44px touch target. */\n@media (max-width: 640px) {\n\t.cn-saved-views-widget__link {\n\t\talign-items: center;\n\t\tdisplay: inline-flex;\n\t\tmin-height: 44px;\n\t}\n}\n</style>\n"],"names":["_openBlock","_createElementBlock","_toDisplayString","_createVNode"],"mappings":";;;AAWM,EAAA,KAAK,EAAC,uBAAuB;AAAC,EAAA,aAAW,EAAC,uBAAsB;;;;AAC/D,EAAA,KAAA,EAAA,6BAAA;AAAA,EAAA,aAAA,EAAA,sBAAA;;;;AAGF,EAAA,KAAA,EAAA,gCAAA;;;;AAGQ,EAAA,KAAA,EAAA,6BAAA;AAAA,EAAA,aAAA,EAAA,sBAAA;;;;AAGA,EAAA,KAAK,EAAC,6BAAW;;;;;;AAV3B,EAAA,QAAAA,SAAA,EAAA,EAAAC,kBAAA,QAAA;KACI,KAAA,CAAA,KAAA;sBAALA,kBAEI,CAAA,GAAA,EAFJ,UAEI,EADAC,eAAA,CAAA,QAAA,CAAA,EAAE,CAAC,MAAO,CAAA,OAAA,CAAC,SAAS,CAAA,IAAK;SAE1B,KAAA,CAAA,OAAA;AAAH,WAAAF,SAAA,EAAA,EAAAC,kBAAA,CAEM,OAFN,UAEM,EAAA;YADLE,WAA6C,CAAA,wBAAA,EAAA;AAA7B,cAAA,IAAI,EAAE,EAAE;AAAG,cAAA,IAAI,EAAE,QAAC,CAAA,CAAA,CAAA,eAAA,EAAA,WAAA,CAAA;;;AAExB,UAAA,CAAA,QAAA,CAAA,KAAA,CAAA,MAAA,KAAA,CAAA;0BAATF,kBAEE,CAAA,GAAA,EAFF,UAEE,EADAC,eAAA,CAAA,QAAA,CAAA,EAAE,CAAC,MAAO,CAAA,OAAA,CAAE,SAAQ,CAAA,IAAK,QAAC,CAAA,CAAA,CAAA,eAAA,EAAA,8BAAA,CAAA,CAAA,EAAA,CAAA,YAAA;;gCAG3BD,8CAAiB,QAAK,CAAA,KAAA,EAAA,CAAb,IAAI,KAAA;qCAAbA;kBAAyB,GAAG,EAAE,IAAG,CAAA,GAAA;AAAA,kBAAA,KAAA,EAAA,6BAAA;;oEAE5B,IAAI,CAAC,EAAA,GAAA,aAAA,GAAA,MAAA,CAAA,EAAA;AACV,oBAAA,KAAE,EAAA,6BAAA;AACD,oBAAA,EAAA,EAAA,IAAA,CAAA,EAAA,IAAA,SAAA;AACD,oBAAA,aAAC,EAAA,qBAAA;;;;;;;;;;;;;;;"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import __sfc_main from './CnSavedViewsWidget.vue.js';
|
|
2
|
+
import { registerDashboardWidget } from '../CnWidgetGrid/dashboardWidgetRegistry.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* SPDX-FileCopyrightText: 2026 Conduction B.V.
|
|
6
|
+
* SPDX-License-Identifier: EUPL-1.2
|
|
7
|
+
*
|
|
8
|
+
* CnSavedViewsWidget — self-registers the `saved-views` widget type (a list of
|
|
9
|
+
* the reader's own saved views, each a link into the index page that applies
|
|
10
|
+
* it) into the shared dashboardWidgetRegistry at module load.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
registerDashboardWidget('saved-views', {
|
|
15
|
+
renderer: __sfc_main,
|
|
16
|
+
form: null,
|
|
17
|
+
defaultContent: {
|
|
18
|
+
register: '',
|
|
19
|
+
schema: '',
|
|
20
|
+
route: '',
|
|
21
|
+
own: true,
|
|
22
|
+
limit: 10,
|
|
23
|
+
},
|
|
24
|
+
displayName: 'Saved views',
|
|
25
|
+
icon: 'ViewList',
|
|
26
|
+
surfaces: ['app-dashboard'],
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
export { __sfc_main as CnSavedViewsWidget, __sfc_main as default };
|
|
30
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../../../../src/components/CnSavedViewsWidget/index.js"],"sourcesContent":["/**\n * SPDX-FileCopyrightText: 2026 Conduction B.V.\n * SPDX-License-Identifier: EUPL-1.2\n *\n * CnSavedViewsWidget — self-registers the `saved-views` widget type (a list of\n * the reader's own saved views, each a link into the index page that applies\n * it) into the shared dashboardWidgetRegistry at module load.\n */\n\nimport CnSavedViewsWidget from './CnSavedViewsWidget.vue'\nimport { registerDashboardWidget } from '../CnWidgetGrid/dashboardWidgetRegistry.js'\n\nregisterDashboardWidget('saved-views', {\n\trenderer: CnSavedViewsWidget,\n\tform: null,\n\tdefaultContent: {\n\t\tregister: '',\n\t\tschema: '',\n\t\troute: '',\n\t\town: true,\n\t\tlimit: 10,\n\t},\n\tdisplayName: 'Saved views',\n\ticon: 'ViewList',\n\tsurfaces: ['app-dashboard'],\n})\n\nexport { CnSavedViewsWidget }\nexport default CnSavedViewsWidget\n"],"names":["CnSavedViewsWidget"],"mappings":";;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAGA;AACA,uBAAuB,CAAC,aAAa,EAAE;AACvC,CAAC,QAAQ,EAAEA,UAAkB;AAC7B,CAAC,IAAI,EAAE,IAAI;AACX,CAAC,cAAc,EAAE;AACjB,EAAE,QAAQ,EAAE,EAAE;AACd,EAAE,MAAM,EAAE,EAAE;AACZ,EAAE,KAAK,EAAE,EAAE;AACX,EAAE,GAAG,EAAE,IAAI;AACX,EAAE,KAAK,EAAE,EAAE;AACX,EAAE;AACF,CAAC,WAAW,EAAE,aAAa;AAC3B,CAAC,IAAI,EAAE,UAAU;AACjB,CAAC,QAAQ,EAAE,CAAC,eAAe,CAAC;AAC5B,CAAC;;;;"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CnSegmentedControl.vue2.js","sources":["../../../../src/components/CnSegmentedControl/CnSegmentedControl.vue"],"sourcesContent":["<!--\n SPDX-FileCopyrightText: 2026 Conduction B.V.\n SPDX-License-Identifier: EUPL-1.2\n-->\n<template>\n\t<div\n\t\tclass=\"cn-segmented-control\"\n\t\t:class=\"{ 'cn-segmented-control--stretch': stretch, 'cn-segmented-control--compact': size === 'compact' }\"\n\t\t:role=\"isToggle ? 'group' : 'radiogroup'\"\n\t\t:aria-label=\"ariaLabel || null\"\n\t\t:aria-labelledby=\"ariaLabelledby || null\"\n\t\t@keydown=\"onKeydown\">\n\t\t<button\n\t\t\tv-for=\"(option, index) in normalizedOptions\"\n\t\t\t:key=\"option.key\"\n\t\t\ttype=\"button\"\n\t\t\t:role=\"isToggle ? null : 'radio'\"\n\t\t\tclass=\"cn-segmented-control__option\"\n\t\t\t:class=\"{ 'cn-segmented-control__option--checked': isChecked(option), 'cn-segmented-control__option--icon-only': option.iconOnly }\"\n\t\t\t:aria-checked=\"isToggle ? null : (isChecked(option) ? 'true' : 'false')\"\n\t\t\t:aria-pressed=\"isToggle ? (isChecked(option) ? 'true' : 'false') : null\"\n\t\t\t:tabindex=\"isToggle || index === focusableIndex ? 0 : -1\"\n\t\t\t:disabled=\"option.disabled || null\"\n\t\t\t:aria-controls=\"controls || null\"\n\t\t\t:data-value=\"option.key\"\n\t\t\t@click=\"select(option)\">\n\t\t\t<!-- @slot option Replaces the content of one option. -->\n\t\t\t<!-- @binding {object} option The option: `{ value, label, disabled, count }`. -->\n\t\t\t<!-- @binding {boolean} checked Whether this option is the chosen one. -->\n\t\t\t<slot name=\"option\" :option=\"option\" :checked=\"isChecked(option)\">\n\t\t\t\t<span class=\"cn-segmented-control__label\">{{ option.label }}</span>\n\t\t\t\t<!-- The leading space is not drawn in this flex row; it keeps a\n\t\t\t\t screen reader from reading label and count as one word. -->\n\t\t\t\t<span v-if=\"option.count !== null\" class=\"cn-segmented-control__count\">{{ ' ' + option.count }}</span>\n\t\t\t</slot>\n\t\t</button>\n\t</div>\n</template>\n\n<script>\n/**\n * CnSegmentedControl is a switch between a few views of the same thing, such\n * as \"My work / My team\". One option is always the chosen one.\n *\n * It is a radio group: screen readers announce the group and which option is\n * checked, the arrow keys move the choice, and Tab enters and leaves the group\n * in one step. Give it a name with `aria-label` (or `aria-labelledby`).\n *\n * ```vue\n * <CnSegmentedControl\n * v-model=\"view\"\n * aria-label=\"View\"\n * :options=\"[\n * { value: 'mine', label: 'My work' },\n * { value: 'team', label: 'My team' },\n * ]\" />\n * ```\n *\n * To switch between panels of content, use `CnTabs` with `variant=\"segmented\"`\n * instead: that keeps the tab and panel semantics and looks the same.\n */\nexport default {\n\tname: 'CnSegmentedControl',\n\n\tprops: {\n\t\t/**\n\t\t * The options. Each is `{ value, label, disabled?, count? }`, or a\n\t\t * plain string that is both the value and the label. `count` shows a\n\t\t * number after the label.\n\t\t *\n\t\t * @type {Array<string|{value: (string|number|boolean), label: string, disabled?: boolean, count?: number}>}\n\t\t */\n\t\toptions: {\n\t\t\ttype: Array,\n\t\t\trequired: true,\n\t\t},\n\n\t\t/**\n\t\t * The value of the chosen option (v-model).\n\t\t *\n\t\t * @type {string|number|boolean|null}\n\t\t */\n\t\tmodelValue: {\n\t\t\ttype: [Boolean, String, Number],\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Accessible name of the group, read out when focus enters it. */\n\t\tariaLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** Id of an element that names the group, as an alternative to `ariaLabel`. */\n\t\tariaLabelledby: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Id of the element the options switch, such as the region holding a\n\t\t * page view's widgets. Set as `aria-controls` on every option.\n\t\t *\n\t\t * @spec openspec/changes/view-switch-containers/specs/view-switch-containers/spec.md#requirement-the-view-switch-is-accessible\n\t\t */\n\t\tcontrols: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Size of the control: `normal` (default) or `compact` (the board\n\t\t * group: 3px track padding, 2px gap, 34px segments).\n\t\t *\n\t\t * @spec openspec/changes/screens-dashboard-parity/specs/dashboard-page/spec.md#requirement-a-compact-segmented-control\n\t\t */\n\t\tsize: {\n\t\t\ttype: String,\n\t\t\tdefault: 'normal',\n\t\t\tvalidator: (v) => ['normal', 'compact'].includes(v),\n\t\t},\n\n\t\t/**\n\t\t * `radio` (default) is a radio group: one option is always chosen and\n\t\t * the arrow keys move the choice. `toggle` is a group of buttons, each\n\t\t * with `aria-pressed`, for a set of presets (the board's period group);\n\t\t * Tab visits every button.\n\t\t *\n\t\t * @spec openspec/changes/screens-dashboard-parity/specs/dashboard-page/spec.md#requirement-the-period-as-a-segmented-group\n\t\t */\n\t\tmode: {\n\t\t\ttype: String,\n\t\t\tdefault: 'radio',\n\t\t\tvalidator: (v) => ['radio', 'toggle'].includes(v),\n\t\t},\n\n\t\t/** Stretch the control to the full width, with options of equal width. */\n\t\tstretch: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\t},\n\n\temits: [\n\t\t/**\n\t\t * Emitted with the value of the newly chosen option.\n\t\t *\n\t\t * @event update:modelValue\n\t\t * @type {string|number|boolean}\n\t\t */\n\t\t'update:modelValue',\n\t],\n\n\tcomputed: {\n\t\t/**\n\t\t * Whether the options are pressable buttons rather than radios.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\tisToggle() {\n\t\t\treturn this.mode === 'toggle'\n\t\t},\n\n\t\t/**\n\t\t * The options in one shape.\n\t\t *\n\t\t * @return {Array<{key: string, value: (string|number|boolean), label: string, disabled: boolean, count: (number|null)}>}\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-segmented-control\n\t\t */\n\t\tnormalizedOptions() {\n\t\t\treturn (Array.isArray(this.options) ? this.options : [])\n\t\t\t\t.filter((option) => option !== null && option !== undefined)\n\t\t\t\t.map((option) => {\n\t\t\t\t\tif (typeof option !== 'object') {\n\t\t\t\t\t\treturn { key: String(option), value: option, label: String(option), disabled: false, count: null, iconOnly: false }\n\t\t\t\t\t}\n\t\t\t\t\treturn {\n\t\t\t\t\t\tkey: String(option.value),\n\t\t\t\t\t\tvalue: option.value,\n\t\t\t\t\t\tlabel: String(option.label ?? option.value),\n\t\t\t\t\t\tdisabled: option.disabled === true,\n\t\t\t\t\t\tcount: Number.isFinite(option.count) ? option.count : null,\n\t\t\t\t\t\ticonOnly: option.iconOnly === true,\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * The option Tab lands on: the chosen one, else the first enabled one.\n\t\t *\n\t\t * @return {number} Its index, or -1 when no option can take focus.\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-segmented-control\n\t\t */\n\t\tfocusableIndex() {\n\t\t\tconst checked = this.normalizedOptions.findIndex((option) => this.isChecked(option) && !option.disabled)\n\t\t\tif (checked !== -1) {\n\t\t\t\treturn checked\n\t\t\t}\n\t\t\treturn this.normalizedOptions.findIndex((option) => !option.disabled)\n\t\t},\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Whether an option is the chosen one.\n\t\t *\n\t\t * @param {{value: (string|number|boolean)}} option The option.\n\t\t * @return {boolean}\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-segmented-control\n\t\t */\n\t\tisChecked(option) {\n\t\t\treturn this.modelValue !== null && String(option.value) === String(this.modelValue)\n\t\t},\n\n\t\t/**\n\t\t * Choose an option.\n\t\t *\n\t\t * @param {{value: (string|number|boolean), disabled: boolean}} option The option.\n\t\t * @return {void}\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-segmented-control\n\t\t */\n\t\tselect(option) {\n\t\t\tif (option.disabled || this.isChecked(option)) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.$emit('update:modelValue', option.value)\n\t\t},\n\n\t\t/**\n\t\t * Arrow keys, Home and End move the choice, as in a native radio\n\t\t * group. Disabled options are passed over and the ends wrap around.\n\t\t *\n\t\t * @param {KeyboardEvent} event The key press.\n\t\t * @return {void}\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-segmented-control\n\t\t */\n\t\tonKeydown(event) {\n\t\t\tif (this.isToggle) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst enabled = this.normalizedOptions\n\t\t\t\t.map((option, index) => ({ option, index }))\n\t\t\t\t.filter((entry) => !entry.option.disabled)\n\t\t\tif (enabled.length === 0) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst buttons = Array.from(this.$el.querySelectorAll('[role=\"radio\"]'))\n\t\t\tconst focused = buttons.indexOf(document.activeElement)\n\t\t\tconst from = focused !== -1 ? focused : this.focusableIndex\n\t\t\tlet position = enabled.findIndex((entry) => entry.index === from)\n\t\t\tif (position === -1) {\n\t\t\t\tposition = 0\n\t\t\t}\n\t\t\tif (event.key === 'ArrowRight' || event.key === 'ArrowDown') {\n\t\t\t\tposition = (position + 1) % enabled.length\n\t\t\t} else if (event.key === 'ArrowLeft' || event.key === 'ArrowUp') {\n\t\t\t\tposition = (position - 1 + enabled.length) % enabled.length\n\t\t\t} else if (event.key === 'Home') {\n\t\t\t\tposition = 0\n\t\t\t} else if (event.key === 'End') {\n\t\t\t\tposition = enabled.length - 1\n\t\t\t} else {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tevent.preventDefault()\n\t\t\tconst target = enabled[position]\n\t\t\tthis.select(target.option)\n\t\t\tif (buttons[target.index] && typeof buttons[target.index].focus === 'function') {\n\t\t\t\tbuttons[target.index].focus()\n\t\t\t}\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-segmented-control {\n\tdisplay: inline-flex;\n\tgap: 4px;\n\tmax-width: 100%;\n\tpadding: 4px;\n\tborder-radius: var(--border-radius-container, var(--border-radius-large, 10px));\n\tbackground-color: var(--color-background-dark);\n\toverflow-x: auto;\n}\n\n.cn-segmented-control--stretch {\n\tdisplay: flex;\n}\n\n.cn-segmented-control--stretch .cn-segmented-control__option {\n\tflex: 1 1 0;\n}\n\n/* The parent class outscores Nextcloud's server rule that gives every plain\n `button` a margin and a border. */\n.cn-segmented-control .cn-segmented-control__option {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tjustify-content: center;\n\tgap: 8px;\n\tbox-sizing: border-box;\n\tmin-height: 36px;\n\tmargin: 0;\n\tpadding: 0 16px;\n\tborder: 1px solid transparent;\n\tborder-radius: var(--border-radius-element, var(--border-radius-large, 8px));\n\tbackground-color: transparent;\n\t/* Full-contrast label on every option: the choice is carried by the\n\t raised surface, not by dimming the others. */\n\tcolor: var(--color-main-text);\n\tfont: inherit;\n\tfont-weight: 600;\n\twhite-space: nowrap;\n\tcursor: pointer;\n}\n\n.cn-segmented-control .cn-segmented-control__option:hover {\n\tbackground-color: var(--color-background-hover);\n}\n\n.cn-segmented-control .cn-segmented-control__option:focus-visible {\n\toutline: 2px solid var(--color-primary-element);\n\toutline-offset: 2px;\n}\n\n/* The border survives forced-colours mode, which drops background and shadow. */\n.cn-segmented-control .cn-segmented-control__option--checked,\n.cn-segmented-control .cn-segmented-control__option--checked:hover {\n\tborder-color: var(--color-border-dark);\n\tbackground-color: var(--color-main-background);\n\tbox-shadow: 0 1px 2px var(--color-box-shadow);\n}\n\n.cn-segmented-control .cn-segmented-control__option[disabled] {\n\tcursor: default;\n\topacity: 0.5;\n}\n\n.cn-segmented-control__count {\n\tfont-weight: 400;\n}\n\n/* Compact (screens-dashboard-parity): the board's period group. */\n.cn-segmented-control.cn-segmented-control--compact {\n\tgap: 2px;\n\tpadding: 3px;\n\tborder-radius: 8px;\n}\n\n.cn-segmented-control.cn-segmented-control--compact .cn-segmented-control__option {\n\tmin-height: 34px;\n\theight: 34px;\n\tpadding: 0 12px;\n\tborder-radius: 6px;\n\tfont-size: 14px;\n\tfont-weight: 600;\n}\n\n.cn-segmented-control.cn-segmented-control--compact .cn-segmented-control__option--icon-only {\n\tmin-width: 40px;\n\tpadding: 0;\n}\n\n.cn-segmented-control.cn-segmented-control--compact .cn-segmented-control__option--checked,\n.cn-segmented-control.cn-segmented-control--compact .cn-segmented-control__option--checked:hover {\n\tborder-color: transparent;\n\tbox-shadow: 0 1px 2px var(--color-box-shadow);\n}\n</style>\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"CnSegmentedControl.vue2.js","sources":["../../../../src/components/CnSegmentedControl/CnSegmentedControl.vue"],"sourcesContent":["<!--\n SPDX-FileCopyrightText: 2026 Conduction B.V.\n SPDX-License-Identifier: EUPL-1.2\n-->\n<template>\n\t<div\n\t\tclass=\"cn-segmented-control\"\n\t\t:class=\"{ 'cn-segmented-control--stretch': stretch, 'cn-segmented-control--compact': size === 'compact' }\"\n\t\t:role=\"isToggle ? 'group' : 'radiogroup'\"\n\t\t:aria-label=\"ariaLabel || null\"\n\t\t:aria-labelledby=\"ariaLabelledby || null\"\n\t\t@keydown=\"onKeydown\">\n\t\t<button\n\t\t\tv-for=\"(option, index) in normalizedOptions\"\n\t\t\t:key=\"option.key\"\n\t\t\ttype=\"button\"\n\t\t\t:role=\"isToggle ? null : 'radio'\"\n\t\t\tclass=\"cn-segmented-control__option\"\n\t\t\t:class=\"{ 'cn-segmented-control__option--checked': isChecked(option), 'cn-segmented-control__option--icon-only': option.iconOnly }\"\n\t\t\t:aria-checked=\"isToggle ? null : (isChecked(option) ? 'true' : 'false')\"\n\t\t\t:aria-pressed=\"isToggle ? (isChecked(option) ? 'true' : 'false') : null\"\n\t\t\t:tabindex=\"isToggle || index === focusableIndex ? 0 : -1\"\n\t\t\t:disabled=\"option.disabled || null\"\n\t\t\t:aria-controls=\"controls || null\"\n\t\t\t:data-value=\"option.key\"\n\t\t\t@click=\"select(option)\">\n\t\t\t<!-- @slot option Replaces the content of one option. -->\n\t\t\t<!-- @binding {object} option The option: `{ value, label, disabled, count }`. -->\n\t\t\t<!-- @binding {boolean} checked Whether this option is the chosen one. -->\n\t\t\t<slot name=\"option\" :option=\"option\" :checked=\"isChecked(option)\">\n\t\t\t\t<span class=\"cn-segmented-control__label\">{{ option.label }}</span>\n\t\t\t\t<!-- The leading space is not drawn in this flex row; it keeps a\n\t\t\t\t screen reader from reading label and count as one word. -->\n\t\t\t\t<span v-if=\"option.count !== null\" class=\"cn-segmented-control__count\">{{ ' ' + option.count }}</span>\n\t\t\t</slot>\n\t\t</button>\n\t</div>\n</template>\n\n<script>\n/**\n * CnSegmentedControl is a switch between a few views of the same thing, such\n * as \"My work / My team\". One option is always the chosen one.\n *\n * It is a radio group: screen readers announce the group and which option is\n * checked, the arrow keys move the choice, and Tab enters and leaves the group\n * in one step. Give it a name with `aria-label` (or `aria-labelledby`).\n *\n * ```vue\n * <CnSegmentedControl\n * v-model=\"view\"\n * aria-label=\"View\"\n * :options=\"[\n * { value: 'mine', label: 'My work' },\n * { value: 'team', label: 'My team' },\n * ]\" />\n * ```\n *\n * To switch between panels of content, use `CnTabs` with `variant=\"segmented\"`\n * instead: that keeps the tab and panel semantics and looks the same.\n */\nexport default {\n\tname: 'CnSegmentedControl',\n\n\tprops: {\n\t\t/**\n\t\t * The options. Each is `{ value, label, disabled?, count? }`, or a\n\t\t * plain string that is both the value and the label. `count` shows a\n\t\t * number after the label.\n\t\t *\n\t\t * @type {Array<string|{value: (string|number|boolean), label: string, disabled?: boolean, count?: number}>}\n\t\t */\n\t\toptions: {\n\t\t\ttype: Array,\n\t\t\trequired: true,\n\t\t},\n\n\t\t/**\n\t\t * The value of the chosen option (v-model).\n\t\t *\n\t\t * @type {string|number|boolean|null}\n\t\t */\n\t\tmodelValue: {\n\t\t\ttype: [Boolean, String, Number],\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Accessible name of the group, read out when focus enters it. */\n\t\tariaLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** Id of an element that names the group, as an alternative to `ariaLabel`. */\n\t\tariaLabelledby: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Id of the element the options switch, such as the region holding a\n\t\t * page view's widgets. Set as `aria-controls` on every option.\n\t\t *\n\t\t * @spec openspec/changes/view-switch-containers/specs/view-switch-containers/spec.md#requirement-the-view-switch-is-accessible\n\t\t */\n\t\tcontrols: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Size of the control: `normal` (default) or `compact` (the board\n\t\t * group: 3px track padding, 2px gap, 34px segments).\n\t\t *\n\t\t * @spec openspec/changes/screens-dashboard-parity/specs/dashboard-page/spec.md#requirement-a-compact-segmented-control\n\t\t */\n\t\tsize: {\n\t\t\ttype: String,\n\t\t\tdefault: 'normal',\n\t\t\tvalidator: (v) => ['normal', 'compact'].includes(v),\n\t\t},\n\n\t\t/**\n\t\t * `radio` (default) is a radio group: one option is always chosen and\n\t\t * the arrow keys move the choice. `toggle` is a group of buttons, each\n\t\t * with `aria-pressed`, for a set of presets (the board's period group);\n\t\t * Tab visits every button.\n\t\t *\n\t\t * @spec openspec/changes/screens-dashboard-parity/specs/dashboard-page/spec.md#requirement-the-period-as-a-segmented-group\n\t\t */\n\t\tmode: {\n\t\t\ttype: String,\n\t\t\tdefault: 'radio',\n\t\t\tvalidator: (v) => ['radio', 'toggle'].includes(v),\n\t\t},\n\n\t\t/** Stretch the control to the full width, with options of equal width. */\n\t\tstretch: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\t},\n\n\temits: [\n\t\t/**\n\t\t * Emitted with the value of the newly chosen option.\n\t\t *\n\t\t * @event update:modelValue\n\t\t * @type {string|number|boolean}\n\t\t */\n\t\t'update:modelValue',\n\t],\n\n\tcomputed: {\n\t\t/**\n\t\t * Whether the options are pressable buttons rather than radios.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\tisToggle() {\n\t\t\treturn this.mode === 'toggle'\n\t\t},\n\n\t\t/**\n\t\t * The options in one shape.\n\t\t *\n\t\t * @return {Array<{key: string, value: (string|number|boolean), label: string, disabled: boolean, count: (number|null)}>}\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-segmented-control\n\t\t */\n\t\tnormalizedOptions() {\n\t\t\treturn (Array.isArray(this.options) ? this.options : [])\n\t\t\t\t.filter((option) => option !== null && option !== undefined)\n\t\t\t\t.map((option) => {\n\t\t\t\t\tif (typeof option !== 'object') {\n\t\t\t\t\t\treturn { key: String(option), value: option, label: String(option), disabled: false, count: null, iconOnly: false }\n\t\t\t\t\t}\n\t\t\t\t\treturn {\n\t\t\t\t\t\tkey: String(option.value),\n\t\t\t\t\t\tvalue: option.value,\n\t\t\t\t\t\tlabel: String(option.label ?? option.value),\n\t\t\t\t\t\tdisabled: option.disabled === true,\n\t\t\t\t\t\tcount: Number.isFinite(option.count) ? option.count : null,\n\t\t\t\t\t\ticonOnly: option.iconOnly === true,\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * The option Tab lands on: the chosen one, else the first enabled one.\n\t\t *\n\t\t * @return {number} Its index, or -1 when no option can take focus.\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-segmented-control\n\t\t */\n\t\tfocusableIndex() {\n\t\t\tconst checked = this.normalizedOptions.findIndex((option) => this.isChecked(option) && !option.disabled)\n\t\t\tif (checked !== -1) {\n\t\t\t\treturn checked\n\t\t\t}\n\t\t\treturn this.normalizedOptions.findIndex((option) => !option.disabled)\n\t\t},\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Whether an option is the chosen one.\n\t\t *\n\t\t * @param {{value: (string|number|boolean)}} option The option.\n\t\t * @return {boolean}\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-segmented-control\n\t\t */\n\t\tisChecked(option) {\n\t\t\treturn this.modelValue !== null && String(option.value) === String(this.modelValue)\n\t\t},\n\n\t\t/**\n\t\t * Choose an option.\n\t\t *\n\t\t * @param {{value: (string|number|boolean), disabled: boolean}} option The option.\n\t\t * @return {void}\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-segmented-control\n\t\t */\n\t\tselect(option) {\n\t\t\tif (option.disabled || this.isChecked(option)) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.$emit('update:modelValue', option.value)\n\t\t},\n\n\t\t/**\n\t\t * Arrow keys, Home and End move the choice, as in a native radio\n\t\t * group. Disabled options are passed over and the ends wrap around.\n\t\t *\n\t\t * @param {KeyboardEvent} event The key press.\n\t\t * @return {void}\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-segmented-control\n\t\t */\n\t\tonKeydown(event) {\n\t\t\tif (this.isToggle) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst enabled = this.normalizedOptions\n\t\t\t\t.map((option, index) => ({ option, index }))\n\t\t\t\t.filter((entry) => !entry.option.disabled)\n\t\t\tif (enabled.length === 0) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst buttons = Array.from(this.$el.querySelectorAll('[role=\"radio\"]'))\n\t\t\tconst focused = buttons.indexOf(document.activeElement)\n\t\t\tconst from = focused !== -1 ? focused : this.focusableIndex\n\t\t\tlet position = enabled.findIndex((entry) => entry.index === from)\n\t\t\tif (position === -1) {\n\t\t\t\tposition = 0\n\t\t\t}\n\t\t\tif (event.key === 'ArrowRight' || event.key === 'ArrowDown') {\n\t\t\t\tposition = (position + 1) % enabled.length\n\t\t\t} else if (event.key === 'ArrowLeft' || event.key === 'ArrowUp') {\n\t\t\t\tposition = (position - 1 + enabled.length) % enabled.length\n\t\t\t} else if (event.key === 'Home') {\n\t\t\t\tposition = 0\n\t\t\t} else if (event.key === 'End') {\n\t\t\t\tposition = enabled.length - 1\n\t\t\t} else {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tevent.preventDefault()\n\t\t\tconst target = enabled[position]\n\t\t\tthis.select(target.option)\n\t\t\tif (buttons[target.index] && typeof buttons[target.index].focus === 'function') {\n\t\t\t\tbuttons[target.index].focus()\n\t\t\t}\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-segmented-control {\n\tdisplay: inline-flex;\n\tgap: 4px;\n\tmax-width: 100%;\n\tpadding: 4px;\n\tborder-radius: var(--border-radius-container, var(--border-radius-large, 10px));\n\tbackground-color: var(--color-background-dark);\n\toverflow-x: auto;\n}\n\n.cn-segmented-control--stretch {\n\tdisplay: flex;\n}\n\n.cn-segmented-control--stretch .cn-segmented-control__option {\n\tflex: 1 1 0;\n}\n\n/* The parent class outscores Nextcloud's server rule that gives every plain\n `button` a margin and a border. */\n.cn-segmented-control .cn-segmented-control__option {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tjustify-content: center;\n\tgap: 8px;\n\tbox-sizing: border-box;\n\tmin-height: 36px;\n\tmargin: 0;\n\tpadding: 0 16px;\n\tborder: 1px solid transparent;\n\tborder-radius: var(--border-radius-element, var(--border-radius-large, 8px));\n\tbackground-color: transparent;\n\t/* Full-contrast label on every option: the choice is carried by the\n\t raised surface, not by dimming the others. */\n\tcolor: var(--color-main-text);\n\tfont: inherit;\n\tfont-weight: 600;\n\twhite-space: nowrap;\n\tcursor: pointer;\n}\n\n.cn-segmented-control .cn-segmented-control__option:hover {\n\tbackground-color: var(--color-background-hover);\n}\n\n.cn-segmented-control .cn-segmented-control__option:focus-visible {\n\toutline: 2px solid var(--color-primary-element);\n\toutline-offset: 2px;\n}\n\n/* The border survives forced-colours mode, which drops background and shadow. */\n.cn-segmented-control .cn-segmented-control__option--checked,\n.cn-segmented-control .cn-segmented-control__option--checked:hover {\n\tborder-color: var(--color-border-dark);\n\tbackground-color: var(--color-main-background);\n\tbox-shadow: 0 1px 2px var(--color-box-shadow);\n}\n\n.cn-segmented-control .cn-segmented-control__option[disabled] {\n\tcursor: default;\n\topacity: 0.5;\n}\n\n.cn-segmented-control__count {\n\tfont-weight: 400;\n}\n\n/* Compact (screens-dashboard-parity): the board's period group. */\n.cn-segmented-control.cn-segmented-control--compact {\n\tgap: 2px;\n\tpadding: 3px;\n\tborder-radius: 8px;\n}\n\n.cn-segmented-control.cn-segmented-control--compact .cn-segmented-control__option {\n\tmin-height: 34px;\n\theight: 34px;\n\tpadding: 0 12px;\n\tborder-radius: 6px;\n\tfont-size: 14px;\n\tfont-weight: 600;\n}\n\n.cn-segmented-control.cn-segmented-control--compact .cn-segmented-control__option--icon-only {\n\tmin-width: 40px;\n\tpadding: 0;\n}\n\n.cn-segmented-control.cn-segmented-control--compact .cn-segmented-control__option--checked,\n.cn-segmented-control.cn-segmented-control--compact .cn-segmented-control__option--checked:hover {\n\tborder-color: transparent;\n\tbox-shadow: 0 1px 2px var(--color-box-shadow);\n}\n\n/* On a phone (screens-dashboard-phone) the compact group's segments are 44px\n high touch targets, and the group wraps onto a second row instead of\n running off the screen. */\n@media (max-width: 640px) {\n\t.cn-segmented-control.cn-segmented-control--compact {\n\t\tflex-wrap: wrap;\n\t}\n\n\t.cn-segmented-control.cn-segmented-control--compact .cn-segmented-control__option {\n\t\tmin-height: 44px;\n\t}\n}\n</style>\n"],"names":[],"mappings":"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"}
|