@conduction/nextcloud-vue 2.80.0 → 2.81.0
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
|
@@ -220,3 +220,103 @@ export async function resolveObjectFolderInfo({ apiBase, register, schema, objec
|
|
|
220
220
|
return null
|
|
221
221
|
}
|
|
222
222
|
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Read a dotted path off a value without throwing on a missing segment.
|
|
226
|
+
*
|
|
227
|
+
* @param {(Array|object|string|number|boolean|null|undefined)} value The value.
|
|
228
|
+
* @param {string} path A dotted path; empty answers the value itself.
|
|
229
|
+
* @return {(Array|object|string|number|boolean|null|undefined)} What sits at the path, or undefined.
|
|
230
|
+
*/
|
|
231
|
+
function readPath(value, path) {
|
|
232
|
+
if (typeof path !== 'string' || path === '') {
|
|
233
|
+
return value
|
|
234
|
+
}
|
|
235
|
+
return path.split('.').reduce((acc, key) => (acc === null || acc === undefined ? undefined : acc[key]), value)
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Turn a host endpoint's answer into the browser's `rowData`: the host's own
|
|
240
|
+
* record for each file, keyed by file id.
|
|
241
|
+
*
|
|
242
|
+
* The answer may be a list of records (directly, at `path`, or under `items`
|
|
243
|
+
* or `results`), each naming its file in `key` (`fileId` unless the host says
|
|
244
|
+
* otherwise); a record without one is skipped. An object that is not a list
|
|
245
|
+
* and holds neither is taken as already keyed by file id. Anything else is
|
|
246
|
+
* an empty map: the extra columns stay empty, the files still list.
|
|
247
|
+
*
|
|
248
|
+
* @param {(Array|object|string|number|boolean|null|undefined)} data The endpoint answer.
|
|
249
|
+
* @param {object} [options] Where to look.
|
|
250
|
+
* @param {string} [options.path] A dotted path to the list in the answer.
|
|
251
|
+
* @param {string} [options.key] The record field that holds the file id.
|
|
252
|
+
* @return {object} The records by file id.
|
|
253
|
+
*/
|
|
254
|
+
export function indexRowData(data, { path = '', key = 'fileId' } = {}) {
|
|
255
|
+
let picked = readPath(data, path)
|
|
256
|
+
if (picked && typeof picked === 'object' && !Array.isArray(picked)) {
|
|
257
|
+
if (Array.isArray(picked.items)) {
|
|
258
|
+
picked = picked.items
|
|
259
|
+
} else if (Array.isArray(picked.results)) {
|
|
260
|
+
picked = picked.results
|
|
261
|
+
} else {
|
|
262
|
+
return { ...picked }
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
if (!Array.isArray(picked)) {
|
|
266
|
+
return {}
|
|
267
|
+
}
|
|
268
|
+
const byId = {}
|
|
269
|
+
picked.forEach((record) => {
|
|
270
|
+
if (!record || typeof record !== 'object') {
|
|
271
|
+
return
|
|
272
|
+
}
|
|
273
|
+
const id = record[key]
|
|
274
|
+
if (id === null || id === undefined || id === '') {
|
|
275
|
+
return
|
|
276
|
+
}
|
|
277
|
+
byId[String(id)] = record
|
|
278
|
+
})
|
|
279
|
+
return byId
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Whether a host row action is offered on a file, by its `visibleIf`.
|
|
284
|
+
*
|
|
285
|
+
* `visibleIf.extension` lists file extensions (no case, a leading dot is
|
|
286
|
+
* allowed); `visibleIf.mime` lists mime types, where one ending in `/` or
|
|
287
|
+
* `/*` names the whole family. Every condition named must hold. An action
|
|
288
|
+
* without `visibleIf` is offered on every file, as before.
|
|
289
|
+
*
|
|
290
|
+
* @param {object} action The declared action.
|
|
291
|
+
* @param {object} node The file (`basename`, `mime`).
|
|
292
|
+
* @return {boolean} True when the action belongs on this file.
|
|
293
|
+
*/
|
|
294
|
+
export function hostActionApplies(action, node) {
|
|
295
|
+
const condition = action && action.visibleIf
|
|
296
|
+
if (!condition || typeof condition !== 'object') {
|
|
297
|
+
return true
|
|
298
|
+
}
|
|
299
|
+
if (Array.isArray(condition.extension)) {
|
|
300
|
+
const name = String((node && node.basename) || '')
|
|
301
|
+
const dot = name.lastIndexOf('.')
|
|
302
|
+
const extension = dot > 0 ? name.slice(dot + 1).toLowerCase() : ''
|
|
303
|
+
const wanted = condition.extension.map((entry) => String(entry).replace(/^\./, '').toLowerCase())
|
|
304
|
+
if (extension === '' || !wanted.includes(extension)) {
|
|
305
|
+
return false
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
if (Array.isArray(condition.mime)) {
|
|
309
|
+
const mime = String((node && node.mime) || '').toLowerCase()
|
|
310
|
+
const matches = condition.mime.some((entry) => {
|
|
311
|
+
const wanted = String(entry).toLowerCase()
|
|
312
|
+
if (wanted.endsWith('/*') || wanted.endsWith('/')) {
|
|
313
|
+
return mime.startsWith(wanted.replace(/\*$/, ''))
|
|
314
|
+
}
|
|
315
|
+
return mime === wanted
|
|
316
|
+
})
|
|
317
|
+
if (!matches) {
|
|
318
|
+
return false
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
return true
|
|
322
|
+
}
|
|
@@ -219,3 +219,143 @@ export function sortNodesByColumn(nodes, column, ascending, rowData = null, isFo
|
|
|
219
219
|
return String(left).localeCompare(String(right)) * direction
|
|
220
220
|
})
|
|
221
221
|
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Whether a cell value counts as filled in.
|
|
225
|
+
*
|
|
226
|
+
* @param {(Array|object|string|number|boolean|null|undefined)} value The value.
|
|
227
|
+
* @return {boolean} False for nothing, an empty string or an empty list.
|
|
228
|
+
*/
|
|
229
|
+
function isFilled(value) {
|
|
230
|
+
if (value === null || value === undefined || value === '') {
|
|
231
|
+
return false
|
|
232
|
+
}
|
|
233
|
+
return !(Array.isArray(value) && value.length === 0)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* The single value a node is grouped under, as a string key: a list groups
|
|
238
|
+
* under its first entry, an object under its label, title, name or id.
|
|
239
|
+
*
|
|
240
|
+
* @param {(Array|object|string|number|boolean|null|undefined)} value The cell value.
|
|
241
|
+
* @return {(string|null)} The group key, or null when nothing is filled in.
|
|
242
|
+
*/
|
|
243
|
+
function groupKeyOf(value) {
|
|
244
|
+
const first = Array.isArray(value) ? value[0] : value
|
|
245
|
+
if (!isFilled(first)) {
|
|
246
|
+
return null
|
|
247
|
+
}
|
|
248
|
+
if (typeof first === 'object') {
|
|
249
|
+
const label = first.label ?? first.title ?? first.name ?? first.id
|
|
250
|
+
return isFilled(label) ? String(label) : null
|
|
251
|
+
}
|
|
252
|
+
return String(first)
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* The values a node carries in a column, as strings, for counting and
|
|
257
|
+
* filtering: every entry of a list, the one value otherwise.
|
|
258
|
+
*
|
|
259
|
+
* @param {object} column A resolved column.
|
|
260
|
+
* @param {object} node The node.
|
|
261
|
+
* @param {object} [rowData] The host's per-file data.
|
|
262
|
+
* @return {string[]} The values, deduplicated.
|
|
263
|
+
*/
|
|
264
|
+
function facetValuesOf(column, node, rowData) {
|
|
265
|
+
const value = fileColumnValue(column, node, rowData)
|
|
266
|
+
const entries = Array.isArray(value) ? value : [value]
|
|
267
|
+
const out = []
|
|
268
|
+
entries.forEach((entry) => {
|
|
269
|
+
const key = groupKeyOf(entry)
|
|
270
|
+
if (key !== null && !out.includes(key)) {
|
|
271
|
+
out.push(key)
|
|
272
|
+
}
|
|
273
|
+
})
|
|
274
|
+
return out
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Group nodes on a column's value, in the order each value is first met.
|
|
279
|
+
* Nodes with no value come last, in one group whose value is null.
|
|
280
|
+
*
|
|
281
|
+
* @param {Array<object>} nodes The nodes, already in display order.
|
|
282
|
+
* @param {object} column The resolved column.
|
|
283
|
+
* @param {object} [rowData] The host's per-file data.
|
|
284
|
+
* @return {Array<{value: (string|null), nodes: Array<object>}>} The groups.
|
|
285
|
+
*/
|
|
286
|
+
export function groupNodes(nodes, column, rowData = null) {
|
|
287
|
+
const groups = []
|
|
288
|
+
const byKey = new Map()
|
|
289
|
+
const empty = { value: null, nodes: [] }
|
|
290
|
+
;(Array.isArray(nodes) ? nodes : []).forEach((node) => {
|
|
291
|
+
const key = column ? groupKeyOf(fileColumnValue(column, node, rowData)) : null
|
|
292
|
+
if (key === null) {
|
|
293
|
+
empty.nodes.push(node)
|
|
294
|
+
return
|
|
295
|
+
}
|
|
296
|
+
if (!byKey.has(key)) {
|
|
297
|
+
const group = { value: key, nodes: [] }
|
|
298
|
+
byKey.set(key, group)
|
|
299
|
+
groups.push(group)
|
|
300
|
+
}
|
|
301
|
+
byKey.get(key).nodes.push(node)
|
|
302
|
+
})
|
|
303
|
+
if (empty.nodes.length > 0) {
|
|
304
|
+
groups.push(empty)
|
|
305
|
+
}
|
|
306
|
+
return groups
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* The values in use in a column across the nodes, with how many nodes carry
|
|
311
|
+
* each; a list counts once per entry. Most used first, then in first-seen
|
|
312
|
+
* order.
|
|
313
|
+
*
|
|
314
|
+
* @param {Array<object>} nodes The nodes.
|
|
315
|
+
* @param {object} column The resolved column.
|
|
316
|
+
* @param {object} [rowData] The host's per-file data.
|
|
317
|
+
* @return {Array<{value: string, count: number}>} The values and counts.
|
|
318
|
+
*/
|
|
319
|
+
export function facetCounts(nodes, column, rowData = null) {
|
|
320
|
+
const counts = []
|
|
321
|
+
const byValue = new Map()
|
|
322
|
+
;(Array.isArray(nodes) ? nodes : []).forEach((node) => {
|
|
323
|
+
facetValuesOf(column, node, rowData).forEach((value) => {
|
|
324
|
+
if (!byValue.has(value)) {
|
|
325
|
+
const entry = { value, count: 0 }
|
|
326
|
+
byValue.set(value, entry)
|
|
327
|
+
counts.push(entry)
|
|
328
|
+
}
|
|
329
|
+
byValue.get(value).count += 1
|
|
330
|
+
})
|
|
331
|
+
})
|
|
332
|
+
return counts
|
|
333
|
+
.map((entry, index) => ({ entry, index }))
|
|
334
|
+
.sort((a, b) => (b.entry.count - a.entry.count) || (a.index - b.index))
|
|
335
|
+
.map(({ entry }) => entry)
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Whether a node passes the chosen facet values: within one facet any chosen
|
|
340
|
+
* value will do, and every facet with a choice must pass. A facet with
|
|
341
|
+
* nothing chosen does not filter.
|
|
342
|
+
*
|
|
343
|
+
* @param {object} node The node.
|
|
344
|
+
* @param {object} selections Chosen values by column key.
|
|
345
|
+
* @param {object} columnsByKey Resolved columns by key.
|
|
346
|
+
* @param {object} [rowData] The host's per-file data.
|
|
347
|
+
* @return {boolean} True when the node stays in the list.
|
|
348
|
+
*/
|
|
349
|
+
export function nodeMatchesFacets(node, selections, columnsByKey, rowData = null) {
|
|
350
|
+
return Object.entries(selections || {}).every(([key, chosen]) => {
|
|
351
|
+
if (!Array.isArray(chosen) || chosen.length === 0) {
|
|
352
|
+
return true
|
|
353
|
+
}
|
|
354
|
+
const column = columnsByKey && columnsByKey[key]
|
|
355
|
+
if (!column) {
|
|
356
|
+
return true
|
|
357
|
+
}
|
|
358
|
+
const values = facetValuesOf(column, node, rowData)
|
|
359
|
+
return chosen.some((value) => values.includes(value))
|
|
360
|
+
})
|
|
361
|
+
}
|
|
@@ -116,6 +116,19 @@
|
|
|
116
116
|
v-if="!submitted || mode !== 'public'"
|
|
117
117
|
class="cn-form-page__form"
|
|
118
118
|
@submit.prevent="submit">
|
|
119
|
+
<!-- Spam protection: a field no person sees or reaches. A filled one
|
|
120
|
+
means a bot; the submit then sends nothing (see `honeypot`). -->
|
|
121
|
+
<div v-if="honeypot" class="cn-form-page__honeypot" aria-hidden="true">
|
|
122
|
+
<label :for="`cn-form-page-hp-${honeypot}`">{{ t('nextcloud-vue', 'Leave this field empty') }}</label>
|
|
123
|
+
<input
|
|
124
|
+
:id="`cn-form-page-hp-${honeypot}`"
|
|
125
|
+
v-model="honeypotValue"
|
|
126
|
+
:name="honeypot"
|
|
127
|
+
type="text"
|
|
128
|
+
tabindex="-1"
|
|
129
|
+
autocomplete="off"
|
|
130
|
+
data-testid="cn-form-page-honeypot">
|
|
131
|
+
</div>
|
|
119
132
|
<!-- Paste to fill: only where it may be used (see smartPasteAvailable). -->
|
|
120
133
|
<div v-if="smartPasteAvailable" class="cn-form-page__smart-paste">
|
|
121
134
|
<NcButton type="button" data-testid="cn-form-page-smart-paste" @click="smartPasteOpen = true">
|
|
@@ -610,6 +623,17 @@ export default {
|
|
|
610
623
|
default: '',
|
|
611
624
|
},
|
|
612
625
|
|
|
626
|
+
/**
|
|
627
|
+
* Name of a honeypot field for a form anonymous visitors fill in.
|
|
628
|
+
* The field is rendered out of sight and out of the tab order; when
|
|
629
|
+
* it is filled, submit sends nothing and shows the normal success,
|
|
630
|
+
* so a bot learns nothing. Empty (default): no honeypot.
|
|
631
|
+
*/
|
|
632
|
+
honeypot: {
|
|
633
|
+
type: String,
|
|
634
|
+
default: '',
|
|
635
|
+
},
|
|
636
|
+
|
|
613
637
|
/** HTTP method for endpoint mode. POST | PUT | PATCH. */
|
|
614
638
|
submitMethod: {
|
|
615
639
|
type: String,
|
|
@@ -748,6 +772,8 @@ export default {
|
|
|
748
772
|
summaryKeys: [],
|
|
749
773
|
/** Per-field validation error messages, keyed by field.key. */
|
|
750
774
|
fieldErrors: {},
|
|
775
|
+
/** What a bot typed into the honeypot field; stays empty for a person. */
|
|
776
|
+
honeypotValue: '',
|
|
751
777
|
/**
|
|
752
778
|
* Cache of resolved `endpoint`/`source` visibleWhen outcomes,
|
|
753
779
|
* keyed by field.key. Resolved ONCE in `mounted()` — see the
|
|
@@ -1783,6 +1809,11 @@ export default {
|
|
|
1783
1809
|
}
|
|
1784
1810
|
|
|
1785
1811
|
this.lastError = null
|
|
1812
|
+
if (this.honeypot && this.honeypotValue !== '') {
|
|
1813
|
+
// A bot filled the field no person sees: send nothing, look done.
|
|
1814
|
+
this.submitted = true
|
|
1815
|
+
return
|
|
1816
|
+
}
|
|
1786
1817
|
this.submitting = true
|
|
1787
1818
|
try {
|
|
1788
1819
|
if (this.submitEndpoint) {
|
|
@@ -1808,6 +1839,9 @@ export default {
|
|
|
1808
1839
|
// describes the transport ("Request failed with status code 400")
|
|
1809
1840
|
// and leaves the user to guess which field it meant.
|
|
1810
1841
|
this.lastError = serverErrorMessage(err)
|
|
1842
|
+
// A refusal that names its fields (`findings[]`, as OpenRegister's
|
|
1843
|
+
// form destination check answers 422) is marked on each field.
|
|
1844
|
+
this.showServerFindings(err)
|
|
1811
1845
|
/**
|
|
1812
1846
|
* Submit failure event. Payload is the thrown error / rejected reason.
|
|
1813
1847
|
*
|
|
@@ -1820,6 +1854,45 @@ export default {
|
|
|
1820
1854
|
}
|
|
1821
1855
|
},
|
|
1822
1856
|
|
|
1857
|
+
/**
|
|
1858
|
+
* Put each finding of a refused submit on its field.
|
|
1859
|
+
*
|
|
1860
|
+
* Reads `response.data.findings[]`, each `{ field?, property, message }`,
|
|
1861
|
+
* and marks the field whose key is the finding's `field`, or else its
|
|
1862
|
+
* `property`. Findings on no field of this form stay in the general
|
|
1863
|
+
* error above the form. Nothing happens for an answer without findings.
|
|
1864
|
+
*
|
|
1865
|
+
* @param {object} err The rejected request.
|
|
1866
|
+
* @return {void}
|
|
1867
|
+
*/
|
|
1868
|
+
showServerFindings(err) {
|
|
1869
|
+
const findings = err?.response?.data?.findings
|
|
1870
|
+
if (!Array.isArray(findings) || findings.length === 0) {
|
|
1871
|
+
return
|
|
1872
|
+
}
|
|
1873
|
+
const keys = new Set(this.fields.map((f) => f && f.key))
|
|
1874
|
+
const marked = []
|
|
1875
|
+
for (const finding of findings) {
|
|
1876
|
+
const key = finding && (finding.field || finding.property)
|
|
1877
|
+
if (!key || !keys.has(key) || this.fieldErrors[key]) {
|
|
1878
|
+
continue
|
|
1879
|
+
}
|
|
1880
|
+
this.fieldErrors[key] = String(finding.message || t('nextcloud-vue', 'This answer was refused.'))
|
|
1881
|
+
marked.push(key)
|
|
1882
|
+
}
|
|
1883
|
+
if (marked.length === 0) {
|
|
1884
|
+
return
|
|
1885
|
+
}
|
|
1886
|
+
this.summaryKeys = this.fields.map((f) => f.key).filter((key) => marked.includes(key))
|
|
1887
|
+
if (this.hasSteps) {
|
|
1888
|
+
const stepIndex = this.steps.findIndex((step) => this.stepFields(step).some((f) => f.key === this.summaryKeys[0]))
|
|
1889
|
+
if (stepIndex >= 0) {
|
|
1890
|
+
this.currentStepIndex = stepIndex
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
this.focusFirstError(this.summaryKeys[0])
|
|
1894
|
+
},
|
|
1895
|
+
|
|
1823
1896
|
async submitViaEndpoint() {
|
|
1824
1897
|
const method = (this.submitMethod || 'POST').toLowerCase()
|
|
1825
1898
|
const url = resolveParams(this.submitEndpoint, this.$route?.params || {})
|
|
@@ -1843,6 +1916,14 @@ export default {
|
|
|
1843
1916
|
</script>
|
|
1844
1917
|
|
|
1845
1918
|
<style>
|
|
1919
|
+
.cn-form-page__honeypot {
|
|
1920
|
+
position: absolute;
|
|
1921
|
+
left: -10000px;
|
|
1922
|
+
width: 1px;
|
|
1923
|
+
height: 1px;
|
|
1924
|
+
overflow: hidden;
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1846
1927
|
.cn-form-page {
|
|
1847
1928
|
display: flex;
|
|
1848
1929
|
flex-direction: column;
|
|
@@ -263,6 +263,31 @@
|
|
|
263
263
|
@update:activeIndex="onQuickFilterChange"
|
|
264
264
|
@update:selectedIndices="onQuickFilterMultiChange" />
|
|
265
265
|
</template>
|
|
266
|
+
<!-- Board look, `filterMenu` on a page with no filter sidebar: the
|
|
267
|
+
board's Filter button, a menu of each enum column's values as
|
|
268
|
+
toggleable filters. -->
|
|
269
|
+
<template v-if="boardFilterMenu" #filter-menu>
|
|
270
|
+
<NcActions
|
|
271
|
+
class="cn-actions-bar__filter-menu"
|
|
272
|
+
variant="secondary"
|
|
273
|
+
:forceMenu="true"
|
|
274
|
+
:menuName="t('nextcloud-vue', 'Filter')"
|
|
275
|
+
data-testid="cn-index-filter-menu">
|
|
276
|
+
<template #icon>
|
|
277
|
+
<Tune :size="16" />
|
|
278
|
+
</template>
|
|
279
|
+
<template v-for="field in filterableFields" :key="field.key">
|
|
280
|
+
<NcActionCaption :name="field.label" />
|
|
281
|
+
<NcActionCheckbox
|
|
282
|
+
v-for="val in field.values"
|
|
283
|
+
:key="`${field.key}-${val}`"
|
|
284
|
+
:modelValue="isFilterActive(field.key, val)"
|
|
285
|
+
@update:modelValue="toggleFilter(field.key, val)">
|
|
286
|
+
{{ val }}
|
|
287
|
+
</NcActionCheckbox>
|
|
288
|
+
</template>
|
|
289
|
+
</NcActions>
|
|
290
|
+
</template>
|
|
266
291
|
</CnActionsBar>
|
|
267
292
|
|
|
268
293
|
<!-- Quick edit: a few fields of one row, over the list, with the list
|
|
@@ -625,10 +650,10 @@
|
|
|
625
650
|
the column menu lists every governed column as a visibility
|
|
626
651
|
checkbox — compact, in-table alternatives to the sidebar. -->
|
|
627
652
|
<template
|
|
628
|
-
v-if="(
|
|
653
|
+
v-if="(showHeaderFilterMenu) || (columnMenu && governedColumns.length)"
|
|
629
654
|
#actions-header>
|
|
630
655
|
<NcActions
|
|
631
|
-
v-if="
|
|
656
|
+
v-if="showHeaderFilterMenu"
|
|
632
657
|
:forceMenu="true"
|
|
633
658
|
:aria-label="t('nextcloud-vue', 'Filter')">
|
|
634
659
|
<template #icon>
|
|
@@ -968,6 +993,7 @@ import DotsHorizontal from 'vue-material-design-icons/DotsHorizontal.vue'
|
|
|
968
993
|
import Export from 'vue-material-design-icons/Export.vue'
|
|
969
994
|
import Eye from 'vue-material-design-icons/Eye.vue'
|
|
970
995
|
import FilterOutline from 'vue-material-design-icons/FilterOutline.vue'
|
|
996
|
+
import Tune from 'vue-material-design-icons/Tune.vue'
|
|
971
997
|
import ViewColumnOutline from 'vue-material-design-icons/ViewColumnOutline.vue'
|
|
972
998
|
import CnConfirmDialog from '../../dialogs/CnConfirmDialog.vue'
|
|
973
999
|
import CnQuickEditDialog from '../../dialogs/CnQuickEditDialog.vue'
|
|
@@ -1275,6 +1301,7 @@ export default {
|
|
|
1275
1301
|
DatabaseSearch,
|
|
1276
1302
|
Export,
|
|
1277
1303
|
FilterOutline,
|
|
1304
|
+
Tune,
|
|
1278
1305
|
ViewColumnOutline,
|
|
1279
1306
|
NcCheckboxRadioSwitch,
|
|
1280
1307
|
CnPageHeader,
|
|
@@ -1456,7 +1483,6 @@ export default {
|
|
|
1456
1483
|
default: true,
|
|
1457
1484
|
},
|
|
1458
1485
|
|
|
1459
|
-
// eslint-disable-next-line vue/no-unused-properties
|
|
1460
1486
|
/**
|
|
1461
1487
|
* Draw this page in the `board` look (or `nextcloud`) whatever the app
|
|
1462
1488
|
* does. Empty follows the `cnLook` CnAppRoot provides. Manifest key
|
|
@@ -1607,7 +1633,10 @@ export default {
|
|
|
1607
1633
|
* unchanged. Under the board look a tab with `activeLabel` ("Pipeline:
|
|
1608
1634
|
* Sales") adds a removable chip with that text to the Active line and
|
|
1609
1635
|
* counts on the Filter badge while it is selected; removing the chip
|
|
1610
|
-
* goes back to the default tab.
|
|
1636
|
+
* goes back to the default tab. A tab can say `empty: "field"` (or a
|
|
1637
|
+
* list of fields) for the rows where the field has no value, and
|
|
1638
|
+
* `notEmpty` for the rows where it has one: it reads as the filter
|
|
1639
|
+
* `{ field: { isnull: true } }`.
|
|
1611
1640
|
*/
|
|
1612
1641
|
quickFilters: {
|
|
1613
1642
|
type: Array,
|
|
@@ -2892,6 +2921,8 @@ export default {
|
|
|
2892
2921
|
* Show a filter menu (funnel button) in the table header, above the
|
|
2893
2922
|
* row-actions column. Its menu lists every enum/badge column's values as
|
|
2894
2923
|
* toggleable facet filters — a compact alternative to the facet sidebar.
|
|
2924
|
+
* Under the board look, on a page with no filter sidebar, it is the
|
|
2925
|
+
* toolbar's labelled Filter button instead (row 1, before the view switch).
|
|
2895
2926
|
* Fed from the manifest as `pages[].config.filterMenu`.
|
|
2896
2927
|
*/
|
|
2897
2928
|
filterMenu: {
|
|
@@ -5606,6 +5637,27 @@ export default {
|
|
|
5606
5637
|
return !!this.resolvedSidebar.enabled && this.resolvedSidebar.show !== false
|
|
5607
5638
|
},
|
|
5608
5639
|
|
|
5640
|
+
/**
|
|
5641
|
+
* Whether the board toolbar draws the Filter button as a menu: the
|
|
5642
|
+
* board look, `filterMenu` on, something to filter by, and no filter
|
|
5643
|
+
* sidebar (whose toggle is that button otherwise).
|
|
5644
|
+
*
|
|
5645
|
+
* @return {boolean}
|
|
5646
|
+
*/
|
|
5647
|
+
boardFilterMenu() {
|
|
5648
|
+
return this.isBoardLook && this.filterMenu && !this.hasSidebar && this.filterableFields.length > 0
|
|
5649
|
+
},
|
|
5650
|
+
|
|
5651
|
+
/**
|
|
5652
|
+
* Whether the funnel menu sits in the table header: `filterMenu` with
|
|
5653
|
+
* values to offer, unless the toolbar's Filter button is that menu.
|
|
5654
|
+
*
|
|
5655
|
+
* @return {boolean}
|
|
5656
|
+
*/
|
|
5657
|
+
showHeaderFilterMenu() {
|
|
5658
|
+
return this.filterMenu && this.filterableFields.length > 0 && !this.boardFilterMenu
|
|
5659
|
+
},
|
|
5660
|
+
|
|
5609
5661
|
/** Search props forwarded to the embedded CnIndexSidebar (defaults applied per CnIndexSidebar). */
|
|
5610
5662
|
sidebarSearchProps() {
|
|
5611
5663
|
return (this.sidebar && this.sidebar.search) || {}
|
|
@@ -270,7 +270,7 @@ import Pin from 'vue-material-design-icons/Pin.vue'
|
|
|
270
270
|
import PinOutline from 'vue-material-design-icons/PinOutline.vue'
|
|
271
271
|
import ViewColumnOutline from 'vue-material-design-icons/ViewColumnOutline.vue'
|
|
272
272
|
import { METADATA_COLUMNS } from '../../constants/metadata.js'
|
|
273
|
-
import {
|
|
273
|
+
import { facetOptions } from '../../utils/facets.js'
|
|
274
274
|
import { labelLang } from '../../utils/manifestTranslate.js'
|
|
275
275
|
import { columnsFromSchema, filtersFromSchema } from '../../utils/schema.js'
|
|
276
276
|
import { CnDateRangePicker, DEFAULT_DATE_RANGE_PRESETS } from '../CnDateRangePicker/index.js'
|
|
@@ -867,12 +867,12 @@ export default {
|
|
|
867
867
|
* @param {object} filter Filter object
|
|
868
868
|
*/
|
|
869
869
|
getFilterOptions(filter) {
|
|
870
|
-
const
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
870
|
+
const live = facetOptions(
|
|
871
|
+
this.facetData[filter.key],
|
|
872
|
+
(count) => t('nextcloud-vue', 'No value ({count})', { count }),
|
|
873
|
+
)
|
|
874
|
+
if (live !== null) {
|
|
875
|
+
return live
|
|
876
876
|
}
|
|
877
877
|
// A reference picker's rows come from another list, and neither a
|
|
878
878
|
// schema enum nor a facet bucket can carry them.
|
|
@@ -21,7 +21,7 @@ import { resolveNextStep } from '../../utils/detailActionModel.js'
|
|
|
21
21
|
* Renders CnNextStepCard for the bound record's stage from
|
|
22
22
|
* `content: { field?, stages: { <stage>: { title?, checklist, after? } } }`.
|
|
23
23
|
* The checklist only: the page's own primary button stays where the page
|
|
24
|
-
* puts it. Resolved by its registry key,
|
|
24
|
+
* puts it. Resolved by its registry key, and exported for an app that renders it itself.
|
|
25
25
|
*/
|
|
26
26
|
export default {
|
|
27
27
|
name: 'CnNextStepWidget',
|
|
@@ -151,6 +151,8 @@
|
|
|
151
151
|
:sortKey="localSort.field || null"
|
|
152
152
|
:sortOrder="localSort.dir || 'asc'"
|
|
153
153
|
borderless
|
|
154
|
+
:fitWidth="fitWidthActive"
|
|
155
|
+
:fixedLayout="fixedLayoutActive"
|
|
154
156
|
:hideHeader="content.hideHeader === true"
|
|
155
157
|
@rowClick="onRowClick"
|
|
156
158
|
@rowAuxClick="onRowAuxClick"
|
|
@@ -173,6 +175,8 @@
|
|
|
173
175
|
:sortKey="localSort.field || null"
|
|
174
176
|
:sortOrder="localSort.dir || 'asc'"
|
|
175
177
|
borderless
|
|
178
|
+
:fitWidth="fitWidthActive"
|
|
179
|
+
:fixedLayout="fixedLayoutActive"
|
|
176
180
|
:hideHeader="content.hideHeader === true"
|
|
177
181
|
@rowClick="onRowClick"
|
|
178
182
|
@rowAuxClick="onRowAuxClick"
|
|
@@ -448,6 +452,15 @@ export default {
|
|
|
448
452
|
page: 1,
|
|
449
453
|
/** Rows that fit the host cell; null = unconstrained (dashboards). */
|
|
450
454
|
fitRows: null,
|
|
455
|
+
/**
|
|
456
|
+
* How far the table has been squeezed to the widget's width, so the
|
|
457
|
+
* card never scrolls sideways: 0 = its own auto layout, 1 = one column
|
|
458
|
+
* takes what is left and is cut with an ellipsis while the rest keep
|
|
459
|
+
* their content on one line, 2 = a fixed layout, every column an equal
|
|
460
|
+
* share with its text wrapped. Each step is taken only while the table
|
|
461
|
+
* still scrolls sideways. `content.fitWidth` forces stage 1 on or off.
|
|
462
|
+
*/
|
|
463
|
+
fitStage: 0,
|
|
451
464
|
/** Whether the create dialog is open. */
|
|
452
465
|
showCreate: false,
|
|
453
466
|
/**
|
|
@@ -480,6 +493,27 @@ export default {
|
|
|
480
493
|
},
|
|
481
494
|
|
|
482
495
|
computed: {
|
|
496
|
+
/**
|
|
497
|
+
* Whether the table fits the widget's width (stage 1): `content.fitWidth`
|
|
498
|
+
* when it says, else once the columns were measured not to fit.
|
|
499
|
+
*
|
|
500
|
+
* @return {boolean}
|
|
501
|
+
*/
|
|
502
|
+
fitWidthActive() {
|
|
503
|
+
const forced = this.content?.fitWidth
|
|
504
|
+
return typeof forced === 'boolean' ? forced : this.fitStage === 1
|
|
505
|
+
},
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* Whether the table takes a fixed layout, the last resort when even the
|
|
509
|
+
* fitted table is wider than the widget (not with `content.fitWidth` set).
|
|
510
|
+
*
|
|
511
|
+
* @return {boolean}
|
|
512
|
+
*/
|
|
513
|
+
fixedLayoutActive() {
|
|
514
|
+
return typeof this.content?.fitWidth !== 'boolean' && this.fitStage >= 2
|
|
515
|
+
},
|
|
516
|
+
|
|
483
517
|
/**
|
|
484
518
|
* The unwrapped detail-page object context for token resolution, or null
|
|
485
519
|
* on surfaces (dashboards) that don't provide one.
|
|
@@ -619,7 +653,7 @@ export default {
|
|
|
619
653
|
// `secondary` (a second, muted line under the cell) rides
|
|
620
654
|
// along too: the table draws it, and dropping it here took the
|
|
621
655
|
// list rows' second line away (zuiddrecht-pixel-gaps-3).
|
|
622
|
-
for (const k of ['format', 'widget', 'widgetProps', 'formatter', 'align', 'width', 'type', 'enum', 'enumLabels', 'sortable', 'secondary']) {
|
|
656
|
+
for (const k of ['format', 'widget', 'widgetProps', 'formatter', 'align', 'width', 'type', 'enum', 'enumLabels', 'sortable', 'secondary', 'colorMap', 'grow']) {
|
|
623
657
|
if (c[k] !== undefined) {
|
|
624
658
|
out[k] = c[k]
|
|
625
659
|
}
|
|
@@ -1165,6 +1199,12 @@ export default {
|
|
|
1165
1199
|
mounted() {
|
|
1166
1200
|
this.fetchRows()
|
|
1167
1201
|
this.loadHeadingsIfNeeded()
|
|
1202
|
+
// A table wider than the widget fits its columns to the width instead of
|
|
1203
|
+
// scrolling sideways (no inner scrollbars).
|
|
1204
|
+
if (typeof ResizeObserver !== 'undefined') {
|
|
1205
|
+
this._widthObserver = new ResizeObserver(() => this.measureWidthFit())
|
|
1206
|
+
this._widthObserver.observe(this.$el)
|
|
1207
|
+
}
|
|
1168
1208
|
// Re-read on a page-level refresh. This list fetches its own rows from
|
|
1169
1209
|
// OpenRegister and subscribed to nothing, so after a write elsewhere on
|
|
1170
1210
|
// the page it went on rendering the result set it fetched on mount —
|
|
@@ -1203,6 +1243,9 @@ export default {
|
|
|
1203
1243
|
if (this._fitObserver) {
|
|
1204
1244
|
this._fitObserver.disconnect()
|
|
1205
1245
|
}
|
|
1246
|
+
if (this._widthObserver) {
|
|
1247
|
+
this._widthObserver.disconnect()
|
|
1248
|
+
}
|
|
1206
1249
|
},
|
|
1207
1250
|
|
|
1208
1251
|
methods: {
|
|
@@ -1277,7 +1320,10 @@ export default {
|
|
|
1277
1320
|
const res = await axios.get(url, { params })
|
|
1278
1321
|
this.rows = (res && res.data && res.data.results) || []
|
|
1279
1322
|
this.total = (res && res.data && typeof res.data.total === 'number') ? res.data.total : this.rows.length
|
|
1280
|
-
this.$nextTick(() =>
|
|
1323
|
+
this.$nextTick(() => {
|
|
1324
|
+
this.measureFit()
|
|
1325
|
+
this.measureWidthFit()
|
|
1326
|
+
})
|
|
1281
1327
|
} catch (e) {
|
|
1282
1328
|
// Keep the raw message OUT of the template — log it for
|
|
1283
1329
|
// debugging, surface only the quiet `loadErrorLabel` line.
|
|
@@ -1291,6 +1337,27 @@ export default {
|
|
|
1291
1337
|
}
|
|
1292
1338
|
},
|
|
1293
1339
|
|
|
1340
|
+
/**
|
|
1341
|
+
* Squeeze the table to the widget's width while it scrolls sideways: its
|
|
1342
|
+
* `.cn-data-table__scroll` wrapper is wider than it is visible. First the
|
|
1343
|
+
* fit (stage 1), then, if that is still too wide, a fixed layout
|
|
1344
|
+
* (stage 2). It never goes back (a squeezed table does not overflow, so
|
|
1345
|
+
* there is nothing to measure to relax it). No-op when `content.fitWidth`
|
|
1346
|
+
* is set.
|
|
1347
|
+
*
|
|
1348
|
+
* @return {void}
|
|
1349
|
+
*/
|
|
1350
|
+
measureWidthFit() {
|
|
1351
|
+
if (this.fitStage >= 2 || typeof this.content?.fitWidth === 'boolean') {
|
|
1352
|
+
return
|
|
1353
|
+
}
|
|
1354
|
+
const scroller = this.$el && this.$el.querySelector ? this.$el.querySelector('.cn-data-table__scroll') : null
|
|
1355
|
+
if (scroller && scroller.scrollWidth > scroller.clientWidth + 1) {
|
|
1356
|
+
this.fitStage += 1
|
|
1357
|
+
this.$nextTick(() => this.measureWidthFit())
|
|
1358
|
+
}
|
|
1359
|
+
},
|
|
1360
|
+
|
|
1294
1361
|
/**
|
|
1295
1362
|
* Fit the visible row count to the host grid cell (ADR-062 — the cell
|
|
1296
1363
|
* is the budget). Measures the cell's remaining height below the table
|