@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
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SPDX-FileCopyrightText: 2026 Conduction B.V.
|
|
3
|
+
* SPDX-License-Identifier: EUPL-1.2
|
|
4
|
+
*
|
|
5
|
+
* contentHeaderLink — the text link a widget's content asks its card header
|
|
6
|
+
* to carry: `content.headerLink`, or the shorter `viewAllRoute` (with an
|
|
7
|
+
* optional `viewAllLabel`) that an inbox-style widget uses for its "View all".
|
|
8
|
+
*
|
|
9
|
+
* @module utils/contentHeaderLink
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The header link a widget's `content` declares, in the shape
|
|
14
|
+
* CnWidgetWrapper's `headerLink` takes, or null when it declares none.
|
|
15
|
+
* The label is left as written: the wrapper translates it.
|
|
16
|
+
*
|
|
17
|
+
* `viewAllRoute` is a route name, or a location `{ name, params?, query? }`.
|
|
18
|
+
*
|
|
19
|
+
* @param {object|null|undefined} content The widget's content.
|
|
20
|
+
* @return {{label: string, route?: string, params?: object, query?: object, href?: string}|null}
|
|
21
|
+
*/
|
|
22
|
+
function contentHeaderLink(content) {
|
|
23
|
+
if (!content || typeof content !== 'object') {
|
|
24
|
+
return null
|
|
25
|
+
}
|
|
26
|
+
const declared = content.headerLink;
|
|
27
|
+
if (declared && typeof declared === 'object' && typeof declared.label === 'string' && declared.label !== '') {
|
|
28
|
+
return declared
|
|
29
|
+
}
|
|
30
|
+
const view = content.viewAllRoute;
|
|
31
|
+
const name = typeof view === 'string' ? view : (view && typeof view === 'object' ? view.name : '');
|
|
32
|
+
if (typeof name !== 'string' || name === '') {
|
|
33
|
+
return null
|
|
34
|
+
}
|
|
35
|
+
const link = {
|
|
36
|
+
label: typeof content.viewAllLabel === 'string' && content.viewAllLabel !== '' ? content.viewAllLabel : 'View all',
|
|
37
|
+
route: name,
|
|
38
|
+
};
|
|
39
|
+
if (view && typeof view === 'object') {
|
|
40
|
+
if (view.params && typeof view.params === 'object') {
|
|
41
|
+
link.params = view.params;
|
|
42
|
+
}
|
|
43
|
+
if (view.query && typeof view.query === 'object') {
|
|
44
|
+
link.query = view.query;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return link
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export { contentHeaderLink };
|
|
51
|
+
//# sourceMappingURL=contentHeaderLink.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contentHeaderLink.js","sources":["../../../src/utils/contentHeaderLink.js"],"sourcesContent":["/**\n * SPDX-FileCopyrightText: 2026 Conduction B.V.\n * SPDX-License-Identifier: EUPL-1.2\n *\n * contentHeaderLink — the text link a widget's content asks its card header\n * to carry: `content.headerLink`, or the shorter `viewAllRoute` (with an\n * optional `viewAllLabel`) that an inbox-style widget uses for its \"View all\".\n *\n * @module utils/contentHeaderLink\n */\n\n/**\n * The header link a widget's `content` declares, in the shape\n * CnWidgetWrapper's `headerLink` takes, or null when it declares none.\n * The label is left as written: the wrapper translates it.\n *\n * `viewAllRoute` is a route name, or a location `{ name, params?, query? }`.\n *\n * @param {object|null|undefined} content The widget's content.\n * @return {{label: string, route?: string, params?: object, query?: object, href?: string}|null}\n */\nexport function contentHeaderLink(content) {\n\tif (!content || typeof content !== 'object') {\n\t\treturn null\n\t}\n\tconst declared = content.headerLink\n\tif (declared && typeof declared === 'object' && typeof declared.label === 'string' && declared.label !== '') {\n\t\treturn declared\n\t}\n\tconst view = content.viewAllRoute\n\tconst name = typeof view === 'string' ? view : (view && typeof view === 'object' ? view.name : '')\n\tif (typeof name !== 'string' || name === '') {\n\t\treturn null\n\t}\n\tconst link = {\n\t\tlabel: typeof content.viewAllLabel === 'string' && content.viewAllLabel !== '' ? content.viewAllLabel : 'View all',\n\t\troute: name,\n\t}\n\tif (view && typeof view === 'object') {\n\t\tif (view.params && typeof view.params === 'object') {\n\t\t\tlink.params = view.params\n\t\t}\n\t\tif (view.query && typeof view.query === 'object') {\n\t\t\tlink.query = view.query\n\t\t}\n\t}\n\treturn link\n}\n"],"names":[],"mappings":"AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAAS,iBAAiB,CAAC,OAAO,EAAE;AAC3C,CAAC,IAAI,CAAC,OAAO,IAAI,OAAO,OAAO,KAAK,QAAQ,EAAE;AAC9C,EAAE,OAAO,IAAI;AACb,EAAE;AACF,CAAC,MAAM,QAAQ,GAAG,OAAO,CAAC,WAAU;AACpC,CAAC,IAAI,QAAQ,IAAI,OAAO,QAAQ,KAAK,QAAQ,IAAI,OAAO,QAAQ,CAAC,KAAK,KAAK,QAAQ,IAAI,QAAQ,CAAC,KAAK,KAAK,EAAE,EAAE;AAC9G,EAAE,OAAO,QAAQ;AACjB,EAAE;AACF,CAAC,MAAM,IAAI,GAAG,OAAO,CAAC,aAAY;AAClC,CAAC,MAAM,IAAI,GAAG,OAAO,IAAI,KAAK,QAAQ,GAAG,IAAI,IAAI,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ,GAAG,IAAI,CAAC,IAAI,GAAG,EAAE,EAAC;AACnG,CAAC,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,KAAK,EAAE,EAAE;AAC9C,EAAE,OAAO,IAAI;AACb,EAAE;AACF,CAAC,MAAM,IAAI,GAAG;AACd,EAAE,KAAK,EAAE,OAAO,OAAO,CAAC,YAAY,KAAK,QAAQ,IAAI,OAAO,CAAC,YAAY,KAAK,EAAE,GAAG,OAAO,CAAC,YAAY,GAAG,UAAU;AACpH,EAAE,KAAK,EAAE,IAAI;AACb,GAAE;AACF,CAAC,IAAI,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE;AACvC,EAAE,IAAI,IAAI,CAAC,MAAM,IAAI,OAAO,IAAI,CAAC,MAAM,KAAK,QAAQ,EAAE;AACtD,GAAG,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,OAAM;AAC5B,GAAG;AACH,EAAE,IAAI,IAAI,CAAC,KAAK,IAAI,OAAO,IAAI,CAAC,KAAK,KAAK,QAAQ,EAAE;AACpD,GAAG,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,MAAK;AAC1B,GAAG;AACH,EAAE;AACF,CAAC,OAAO,IAAI;AACZ;;;;"}
|
package/dist/esm/utils/facets.js
CHANGED
|
@@ -37,14 +37,29 @@ function normalizeFacetBucket(bucket) {
|
|
|
37
37
|
return option
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
/**
|
|
41
|
+
* The option id that stands for "this property holds no value".
|
|
42
|
+
*
|
|
43
|
+
* Chosen so no real bucket key can collide with it. Selecting it is sent to
|
|
44
|
+
* OpenRegister as `<property>_isnull=true` by {@link facetFilterParams}.
|
|
45
|
+
*
|
|
46
|
+
* @type {string}
|
|
47
|
+
*/
|
|
48
|
+
const MISSING_VALUE = '__cn_missing__';
|
|
49
|
+
|
|
40
50
|
/**
|
|
41
51
|
* Normalise the `facets` block of a collection response into sidebar format.
|
|
42
52
|
*
|
|
43
53
|
* Fields whose facet carries no bucket list are skipped entirely, so a caller
|
|
44
54
|
* can tell an unfaceted field from one that faceted to nothing.
|
|
45
55
|
*
|
|
56
|
+
* What OpenRegister puts beside the buckets is carried through rather than
|
|
57
|
+
* dropped: `missing` (how many objects in scope hold no value, sent as
|
|
58
|
+
* `{ results }` or `{ count }`), `type` and `title`. A missing count is only
|
|
59
|
+
* set when the backend sent one, for the same reason a bucket count is.
|
|
60
|
+
*
|
|
46
61
|
* @param {object} facets The `facets` block of an API response
|
|
47
|
-
* @return {object} `{ fieldName: { values: [{ value, count?, label? }] } }`
|
|
62
|
+
* @return {object} `{ fieldName: { values: [{ value, count?, label? }], missing?, type?, title? } }`
|
|
48
63
|
*/
|
|
49
64
|
function normalizeFacets(facets) {
|
|
50
65
|
const transformed = {};
|
|
@@ -54,12 +69,69 @@ function normalizeFacets(facets) {
|
|
|
54
69
|
if (!buckets) {
|
|
55
70
|
continue
|
|
56
71
|
}
|
|
57
|
-
|
|
72
|
+
const normalized = { values: buckets.map(normalizeFacetBucket) };
|
|
73
|
+
|
|
74
|
+
const missing = facet.missing?.results ?? facet.missing?.count;
|
|
75
|
+
if (typeof missing === 'number') {
|
|
76
|
+
normalized.missing = missing;
|
|
77
|
+
}
|
|
78
|
+
for (const name of ['type', 'title']) {
|
|
79
|
+
if (facet[name] !== undefined && facet[name] !== null) {
|
|
80
|
+
normalized[name] = facet[name];
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
transformed[key] = normalized;
|
|
58
85
|
}
|
|
59
86
|
|
|
60
87
|
return transformed
|
|
61
88
|
}
|
|
62
89
|
|
|
90
|
+
/**
|
|
91
|
+
* The sidebar options for one normalised facet, with the missing-value option
|
|
92
|
+
* appended when some objects hold no value.
|
|
93
|
+
*
|
|
94
|
+
* @param {object|undefined} facet A normalised facet from {@link normalizeFacets}
|
|
95
|
+
* @param {function(number): string} missingLabel Renders the missing option's label from its count
|
|
96
|
+
* @return {Array<{id: unknown, label: string}>|null} The options, or null when the
|
|
97
|
+
* facet offers nothing, so the caller can fall back to its static options
|
|
98
|
+
*/
|
|
99
|
+
function facetOptions(facet, missingLabel) {
|
|
100
|
+
const options = (facet?.values || []).map((v) => ({
|
|
101
|
+
id: v.value,
|
|
102
|
+
label: facetOptionLabel(v),
|
|
103
|
+
}));
|
|
104
|
+
|
|
105
|
+
if (typeof facet?.missing === 'number' && facet.missing > 0) {
|
|
106
|
+
options.push({ id: MISSING_VALUE, label: missingLabel(facet.missing) });
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return options.length > 0 ? options : null
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* The query parameters one facet selection sends.
|
|
114
|
+
*
|
|
115
|
+
* An ordinary selection is the property itself, one value unwrapped and
|
|
116
|
+
* several as a list. The missing-value option is exclusive: no object both
|
|
117
|
+
* holds one of the chosen values and holds none, so combining them would
|
|
118
|
+
* always answer an empty list. It is sent as `<property>_isnull=true`.
|
|
119
|
+
*
|
|
120
|
+
* @param {string} key The filtered property
|
|
121
|
+
* @param {Array} values The selected option ids
|
|
122
|
+
* @return {object} The parameters to merge into the request
|
|
123
|
+
*/
|
|
124
|
+
function facetFilterParams(key, values) {
|
|
125
|
+
if (!Array.isArray(values) || values.length === 0) {
|
|
126
|
+
return {}
|
|
127
|
+
}
|
|
128
|
+
if (values.includes(MISSING_VALUE)) {
|
|
129
|
+
return { [`${key}_isnull`]: 'true' }
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return { [key]: values.length === 1 ? values[0] : values }
|
|
133
|
+
}
|
|
134
|
+
|
|
63
135
|
/**
|
|
64
136
|
* Build the visible label for one faceted filter option.
|
|
65
137
|
*
|
|
@@ -82,5 +154,5 @@ function facetOptionLabel(bucket) {
|
|
|
82
154
|
return `${text} (${bucket.count})`
|
|
83
155
|
}
|
|
84
156
|
|
|
85
|
-
export { facetOptionLabel, normalizeFacetBucket, normalizeFacets };
|
|
157
|
+
export { MISSING_VALUE, facetFilterParams, facetOptionLabel, facetOptions, normalizeFacetBucket, normalizeFacets };
|
|
86
158
|
//# sourceMappingURL=facets.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"facets.js","sources":["../../../src/utils/facets.js"],"sourcesContent":["/**\n * Facet helpers shared by the object store, the search plugin and the two\n * filter sidebars.\n *\n * The shape on the wire is not ours. OpenRegister emits a terms bucket as\n * `{ key, results, label }` from its object search, and `{ value, count,\n * label }` from its facetable-field discovery. Both are real, so both are\n * read here rather than one being declared correct.\n *\n * The rule these helpers exist to hold is narrower than the field names: a\n * number that was never sent must never reach the screen as a number. An\n * absent count normalises to an absent count and renders as nothing, because\n * `(0)` is a claim about the data that no response supports.\n */\n\n/**\n * Normalise one facet bucket into the sidebar's option shape.\n *\n * `count` and `label` are omitted when the bucket carries neither name for\n * them, so a caller can distinguish \"no count\" from \"a count of zero\".\n *\n * @param {object} bucket A raw facet bucket from the API\n * @return {{value: unknown, count?: unknown, label?: unknown}} The normalised option\n */\nexport function normalizeFacetBucket(bucket) {\n\tconst option = { value: bucket.key ?? bucket.value }\n\n\tconst count = bucket.results ?? bucket.count\n\tif (count !== undefined && count !== null) {\n\t\toption.count = count\n\t}\n\n\tif (bucket.label !== undefined && bucket.label !== null) {\n\t\toption.label = bucket.label\n\t}\n\n\treturn option\n}\n\n/**\n * Normalise the `facets` block of a collection response into sidebar format.\n *\n * Fields whose facet carries no bucket list are skipped entirely, so a caller\n * can tell an unfaceted field from one that faceted to nothing.\n *\n * @param {object} facets The `facets` block of an API response\n * @return {object} `{ fieldName: { values: [{ value, count?, label? }] } }`\n */\nexport function normalizeFacets(facets) {\n\tconst transformed = {}\n\n\tfor (const [key, facet] of Object.entries(facets || {})) {\n\t\tconst buckets = facet?.buckets || facet?.data?.buckets\n\t\tif (!buckets) {\n\t\t\tcontinue\n\t\t}\n\t\
|
|
1
|
+
{"version":3,"file":"facets.js","sources":["../../../src/utils/facets.js"],"sourcesContent":["/**\n * Facet helpers shared by the object store, the search plugin and the two\n * filter sidebars.\n *\n * The shape on the wire is not ours. OpenRegister emits a terms bucket as\n * `{ key, results, label }` from its object search, and `{ value, count,\n * label }` from its facetable-field discovery. Both are real, so both are\n * read here rather than one being declared correct.\n *\n * The rule these helpers exist to hold is narrower than the field names: a\n * number that was never sent must never reach the screen as a number. An\n * absent count normalises to an absent count and renders as nothing, because\n * `(0)` is a claim about the data that no response supports.\n */\n\n/**\n * Normalise one facet bucket into the sidebar's option shape.\n *\n * `count` and `label` are omitted when the bucket carries neither name for\n * them, so a caller can distinguish \"no count\" from \"a count of zero\".\n *\n * @param {object} bucket A raw facet bucket from the API\n * @return {{value: unknown, count?: unknown, label?: unknown}} The normalised option\n */\nexport function normalizeFacetBucket(bucket) {\n\tconst option = { value: bucket.key ?? bucket.value }\n\n\tconst count = bucket.results ?? bucket.count\n\tif (count !== undefined && count !== null) {\n\t\toption.count = count\n\t}\n\n\tif (bucket.label !== undefined && bucket.label !== null) {\n\t\toption.label = bucket.label\n\t}\n\n\treturn option\n}\n\n/**\n * The option id that stands for \"this property holds no value\".\n *\n * Chosen so no real bucket key can collide with it. Selecting it is sent to\n * OpenRegister as `<property>_isnull=true` by {@link facetFilterParams}.\n *\n * @type {string}\n */\nexport const MISSING_VALUE = '__cn_missing__'\n\n/**\n * Normalise the `facets` block of a collection response into sidebar format.\n *\n * Fields whose facet carries no bucket list are skipped entirely, so a caller\n * can tell an unfaceted field from one that faceted to nothing.\n *\n * What OpenRegister puts beside the buckets is carried through rather than\n * dropped: `missing` (how many objects in scope hold no value, sent as\n * `{ results }` or `{ count }`), `type` and `title`. A missing count is only\n * set when the backend sent one, for the same reason a bucket count is.\n *\n * @param {object} facets The `facets` block of an API response\n * @return {object} `{ fieldName: { values: [{ value, count?, label? }], missing?, type?, title? } }`\n */\nexport function normalizeFacets(facets) {\n\tconst transformed = {}\n\n\tfor (const [key, facet] of Object.entries(facets || {})) {\n\t\tconst buckets = facet?.buckets || facet?.data?.buckets\n\t\tif (!buckets) {\n\t\t\tcontinue\n\t\t}\n\t\tconst normalized = { values: buckets.map(normalizeFacetBucket) }\n\n\t\tconst missing = facet.missing?.results ?? facet.missing?.count\n\t\tif (typeof missing === 'number') {\n\t\t\tnormalized.missing = missing\n\t\t}\n\t\tfor (const name of ['type', 'title']) {\n\t\t\tif (facet[name] !== undefined && facet[name] !== null) {\n\t\t\t\tnormalized[name] = facet[name]\n\t\t\t}\n\t\t}\n\n\t\ttransformed[key] = normalized\n\t}\n\n\treturn transformed\n}\n\n/**\n * The sidebar options for one normalised facet, with the missing-value option\n * appended when some objects hold no value.\n *\n * @param {object|undefined} facet A normalised facet from {@link normalizeFacets}\n * @param {function(number): string} missingLabel Renders the missing option's label from its count\n * @return {Array<{id: unknown, label: string}>|null} The options, or null when the\n * facet offers nothing, so the caller can fall back to its static options\n */\nexport function facetOptions(facet, missingLabel) {\n\tconst options = (facet?.values || []).map((v) => ({\n\t\tid: v.value,\n\t\tlabel: facetOptionLabel(v),\n\t}))\n\n\tif (typeof facet?.missing === 'number' && facet.missing > 0) {\n\t\toptions.push({ id: MISSING_VALUE, label: missingLabel(facet.missing) })\n\t}\n\n\treturn options.length > 0 ? options : null\n}\n\n/**\n * The query parameters one facet selection sends.\n *\n * An ordinary selection is the property itself, one value unwrapped and\n * several as a list. The missing-value option is exclusive: no object both\n * holds one of the chosen values and holds none, so combining them would\n * always answer an empty list. It is sent as `<property>_isnull=true`.\n *\n * @param {string} key The filtered property\n * @param {Array} values The selected option ids\n * @return {object} The parameters to merge into the request\n */\nexport function facetFilterParams(key, values) {\n\tif (!Array.isArray(values) || values.length === 0) {\n\t\treturn {}\n\t}\n\tif (values.includes(MISSING_VALUE)) {\n\t\treturn { [`${key}_isnull`]: 'true' }\n\t}\n\n\treturn { [key]: values.length === 1 ? values[0] : values }\n}\n\n/**\n * Build the visible label for one faceted filter option.\n *\n * A bucket that carries a label renders the label: a facet over a `$ref` keys\n * its buckets by identifier, so without this the filter shows the user a\n * column of uuids. A bucket with no count renders no count at all.\n *\n * @param {object} bucket A normalised facet value: `{ value, count?, label? }`\n * @return {string} The label to render for this option\n */\nexport function facetOptionLabel(bucket) {\n\tconst text = (bucket.label !== undefined && bucket.label !== null)\n\t\t? String(bucket.label)\n\t\t: String(bucket.value)\n\n\tif (bucket.count === undefined || bucket.count === null) {\n\t\treturn text\n\t}\n\n\treturn `${text} (${bucket.count})`\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -13,8 +13,9 @@ import { resolveFilterTokens } from './resolveFilterTokens.js';
|
|
|
13
13
|
* `status: closed`, ...). Those are answered by one `/grouped` request on that
|
|
14
14
|
* field. A filter that narrows on something else (two fields, an operator, a
|
|
15
15
|
* list of values) cannot be read off a grouped result, so each of those costs
|
|
16
|
-
* one `/value` count. A filter that holds a LIST of values
|
|
17
|
-
* the list endpoint, because the
|
|
16
|
+
* one `/value` count. A filter that holds a LIST of values, or tests a field
|
|
17
|
+
* for no value (`isnull`), is counted through the list endpoint, because the
|
|
18
|
+
* aggregation endpoints cannot express it.
|
|
18
19
|
* Nothing is requested for an entry that did not ask.
|
|
19
20
|
*
|
|
20
21
|
* @module utils/fetchFilterCounts
|
|
@@ -39,6 +40,37 @@ function hasListValue(filter) {
|
|
|
39
40
|
return Object.values(filter).some((value) => Array.isArray(value))
|
|
40
41
|
}
|
|
41
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Whether a filter tests a field for "no value" (`{ assignee: null }` or
|
|
45
|
+
* `{ supersededBy: { isnull: true } }`).
|
|
46
|
+
*
|
|
47
|
+
* OpenRegister's aggregation endpoints have no null operator: `isnull` is
|
|
48
|
+
* refused there and an empty value is dropped or matches nothing, so such a
|
|
49
|
+
* filter counted 0 while the list showed rows. The list endpoint does read
|
|
50
|
+
* `field[isnull]=true`, so a filter like this is counted through it.
|
|
51
|
+
*
|
|
52
|
+
* @param {object} filter A filter map.
|
|
53
|
+
* @return {boolean} True when any value is null or carries an `isnull` operator.
|
|
54
|
+
*/
|
|
55
|
+
function hasNullTest(filter) {
|
|
56
|
+
if (!filter || typeof filter !== 'object') {
|
|
57
|
+
return false
|
|
58
|
+
}
|
|
59
|
+
return Object.values(filter).some((value) => value === null
|
|
60
|
+
|| (value && typeof value === 'object' && !Array.isArray(value) && Object.keys(value).some((op) => op.toLowerCase() === 'isnull')))
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Whether the aggregation endpoints cannot express a filter, so it is counted
|
|
65
|
+
* through the list endpoint: a list of values, or a test for no value.
|
|
66
|
+
*
|
|
67
|
+
* @param {object} filter A filter map.
|
|
68
|
+
* @return {boolean} True when the filter needs the list endpoint.
|
|
69
|
+
*/
|
|
70
|
+
function needsListEndpoint(filter) {
|
|
71
|
+
return hasListValue(filter) || hasNullTest(filter)
|
|
72
|
+
}
|
|
73
|
+
|
|
42
74
|
/**
|
|
43
75
|
* Count the records a filter matches by asking the LIST endpoint for one
|
|
44
76
|
* record and reading its `total`. The filter is sent the way the list request
|
|
@@ -60,6 +92,9 @@ async function fetchListTotal(register, schema, filter, ctx) {
|
|
|
60
92
|
for (const [op, operand] of Object.entries(value)) {
|
|
61
93
|
params[`${key}[${op}]`] = operand;
|
|
62
94
|
}
|
|
95
|
+
} else if (value === null) {
|
|
96
|
+
// "No value": the list endpoint reads it as the isnull operator.
|
|
97
|
+
params[`${key}[isnull]`] = 'true';
|
|
63
98
|
} else {
|
|
64
99
|
params[key] = value;
|
|
65
100
|
}
|
|
@@ -126,12 +161,12 @@ async function fetchFilterCounts({ register, schema, entries, baseFilter, ctx }
|
|
|
126
161
|
// and `/value` returns exactly its number.
|
|
127
162
|
// A list of values anywhere in a request rules the aggregation endpoints
|
|
128
163
|
// out for it. A base filter with one rules them out for every entry.
|
|
129
|
-
const baseHasList =
|
|
164
|
+
const baseHasList = needsListEndpoint(base);
|
|
130
165
|
const byField = {};
|
|
131
166
|
const single = [];
|
|
132
167
|
const viaList = [];
|
|
133
168
|
for (const entry of entries) {
|
|
134
|
-
if (baseHasList ||
|
|
169
|
+
if (baseHasList || needsListEndpoint(entry.filter)) {
|
|
135
170
|
viaList.push(entry);
|
|
136
171
|
continue
|
|
137
172
|
}
|
|
@@ -182,5 +217,5 @@ async function fetchFilterCounts({ register, schema, entries, baseFilter, ctx }
|
|
|
182
217
|
return counts
|
|
183
218
|
}
|
|
184
219
|
|
|
185
|
-
export { fetchFilterCounts, fetchListTotal, hasListValue, singleEqualityOf };
|
|
220
|
+
export { fetchFilterCounts, fetchListTotal, hasListValue, hasNullTest, needsListEndpoint, singleEqualityOf };
|
|
186
221
|
//# sourceMappingURL=fetchFilterCounts.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fetchFilterCounts.js","sources":["../../../src/utils/fetchFilterCounts.js"],"sourcesContent":["/**\n * SPDX-FileCopyrightText: 2026 Conduction B.V.\n * SPDX-License-Identifier: EUPL-1.2\n *\n * fetchFilterCounts: how many records each of a set of filters matches, in as\n * few requests as the API allows.\n *\n * Quick filters and saved views usually differ in ONE field (`status: open`,\n * `status: closed`, ...). Those are answered by one `/grouped` request on that\n * field. A filter that narrows on something else (two fields, an operator, a\n * list of values) cannot be read off a grouped result, so each of those costs\n * one `/value` count. A filter that holds a LIST of values is counted through\n * the list endpoint, because the aggregation endpoints cannot express it.\n * Nothing is requested for an entry that did not ask.\n *\n * @module utils/fetchFilterCounts\n */\n\nimport { fetchAggregateValue, fetchGroupedCounts } from './fetchAggregate.js'\nimport { buildHeaders, buildQueryString, prefixUrl } from './headers.js'\nimport { resolveFilterTokens } from './resolveFilterTokens.js'\n\n/**\n * Whether a filter holds a list of values (`{ status: ['open', 'hold'] }`).\n *\n * The aggregation endpoints have no spelling for \"one of these values\", so a\n * filter like this is never sent to them. It is counted through the list\n * endpoint instead, which is where the page's own request sends it.\n *\n * @param {object} filter A filter map.\n * @return {boolean} True when any value is an array.\n * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-counts-on-filters-and-views\n */\nexport function hasListValue(filter) {\n\tif (!filter || typeof filter !== 'object') {\n\t\treturn false\n\t}\n\treturn Object.values(filter).some((value) => Array.isArray(value))\n}\n\n/**\n * Count the records a filter matches by asking the LIST endpoint for one\n * record and reading its `total`. The filter is sent the way the list request\n * sends it: a list of values as repeated `field[]=`, an operator as\n * `field[op]=`.\n *\n * @param {string} register The register slug.\n * @param {string} schema The schema slug.\n * @param {object} filter The filter map.\n * @param {object} [ctx] Token-resolution context for the filter.\n * @return {Promise<number|null>} The total, or null when the response carries none.\n * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-counts-on-filters-and-views\n */\nexport async function fetchListTotal(register, schema, filter, ctx) {\n\tconst resolved = resolveFilterTokens(filter || {}, ctx || {})\n\tconst params = {}\n\tfor (const [key, value] of Object.entries(resolved || {})) {\n\t\tif (value && typeof value === 'object' && !Array.isArray(value)) {\n\t\t\tfor (const [op, operand] of Object.entries(value)) {\n\t\t\t\tparams[`${key}[${op}]`] = operand\n\t\t\t}\n\t\t} else {\n\t\t\tparams[key] = value\n\t\t}\n\t}\n\tconst qs = buildQueryString({ ...params, _limit: 1 })\n\tconst url = prefixUrl(`/apps/openregister/api/objects/${encodeURIComponent(register)}/${encodeURIComponent(schema)}${qs}`)\n\tconst response = await fetch(url, { headers: buildHeaders() })\n\tif (!response.ok) {\n\t\tthrow new Error(`list returned ${response.status}`)\n\t}\n\tconst data = await response.json()\n\treturn typeof data.total === 'number' ? data.total : null\n}\n\n/**\n * The single field a filter narrows on with a plain equality, or null when the\n * filter is anything else.\n *\n * @param {object} filter A filter map.\n * @return {{field: string, value: string}|null} The field and the value it must equal.\n * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-counts-on-filters-and-views\n */\nexport function singleEqualityOf(filter) {\n\tif (!filter || typeof filter !== 'object' || Array.isArray(filter)) {\n\t\treturn null\n\t}\n\tconst keys = Object.keys(filter)\n\tif (keys.length !== 1) {\n\t\treturn null\n\t}\n\tconst value = filter[keys[0]]\n\tif (value === null || value === undefined || value === '' || typeof value === 'object') {\n\t\treturn null\n\t}\n\t// A token (`@me`, `@today`) resolves to a value the grouped keys are\n\t// compared against only after resolution, which the count request does.\n\tif (typeof value === 'string' && value.charAt(0) === '@') {\n\t\treturn null\n\t}\n\treturn { field: keys[0], value: String(value) }\n}\n\n/**\n * Count the records each entry's filter matches.\n *\n * @param {object} options The request.\n * @param {string} options.register The register slug.\n * @param {string} options.schema The schema slug.\n * @param {Array<{key: (string|number), filter: object}>} options.entries The filters to count, each under its own key.\n * @param {object} [options.baseFilter] A filter every count is narrowed by (the page's fixed filter).\n * @param {object} [options.ctx] Token-resolution context for the filters.\n * @return {Promise<{[key: string]: number}>} The count per entry key. An entry whose count failed is left out.\n * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-counts-on-filters-and-views\n */\nexport async function fetchFilterCounts({ register, schema, entries, baseFilter, ctx } = {}) {\n\tconst counts = {}\n\tif (!register || !schema || !Array.isArray(entries) || entries.length === 0) {\n\t\treturn counts\n\t}\n\tconst base = (baseFilter && typeof baseFilter === 'object') ? baseFilter : {}\n\n\t// Entries that share one equality field go into one grouped request, but\n\t// only when at least two do: a lone entry costs one request either way,\n\t// and `/value` returns exactly its number.\n\t// A list of values anywhere in a request rules the aggregation endpoints\n\t// out for it. A base filter with one rules them out for every entry.\n\tconst baseHasList = hasListValue(base)\n\tconst byField = {}\n\tconst single = []\n\tconst viaList = []\n\tfor (const entry of entries) {\n\t\tif (baseHasList || hasListValue(entry.filter)) {\n\t\t\tviaList.push(entry)\n\t\t\tcontinue\n\t\t}\n\t\tconst eq = singleEqualityOf(entry.filter)\n\t\tif (eq !== null && !(eq.field in base)) {\n\t\t\t(byField[eq.field] = byField[eq.field] || []).push({ entry, value: eq.value })\n\t\t} else {\n\t\t\tsingle.push(entry)\n\t\t}\n\t}\n\tconst jobs = []\n\tfor (const [field, members] of Object.entries(byField)) {\n\t\tif (members.length < 2) {\n\t\t\tsingle.push(members[0].entry)\n\t\t\tcontinue\n\t\t}\n\t\tjobs.push(fetchGroupedCounts({ register, schema, groupBy: field, filter: base }, ctx)\n\t\t\t.then((groups) => {\n\t\t\t\tconst map = {}\n\t\t\t\tfor (const group of groups) {\n\t\t\t\t\tmap[group.key] = group.count\n\t\t\t\t}\n\t\t\t\tfor (const member of members) {\n\t\t\t\t\tcounts[member.entry.key] = map[member.value] || 0\n\t\t\t\t}\n\t\t\t})\n\t\t\t.catch(() => {}))\n\t}\n\tfor (const entry of single) {\n\t\tjobs.push(fetchAggregateValue({ register, schema, metric: 'count', filter: { ...base, ...(entry.filter || {}) } }, ctx)\n\t\t\t.then((value) => {\n\t\t\t\tif (value !== null && Number.isFinite(Number(value))) {\n\t\t\t\t\tcounts[entry.key] = Number(value)\n\t\t\t\t}\n\t\t\t})\n\t\t\t.catch(() => {}))\n\t}\n\tfor (const entry of viaList) {\n\t\tjobs.push(fetchListTotal(register, schema, { ...base, ...(entry.filter || {}) }, ctx)\n\t\t\t.then((total) => {\n\t\t\t\tif (total !== null) {\n\t\t\t\t\tcounts[entry.key] = total\n\t\t\t\t}\n\t\t\t})\n\t\t\t.catch(() => {}))\n\t}\n\tawait Promise.all(jobs)\n\treturn counts\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"fetchFilterCounts.js","sources":["../../../src/utils/fetchFilterCounts.js"],"sourcesContent":["/**\n * SPDX-FileCopyrightText: 2026 Conduction B.V.\n * SPDX-License-Identifier: EUPL-1.2\n *\n * fetchFilterCounts: how many records each of a set of filters matches, in as\n * few requests as the API allows.\n *\n * Quick filters and saved views usually differ in ONE field (`status: open`,\n * `status: closed`, ...). Those are answered by one `/grouped` request on that\n * field. A filter that narrows on something else (two fields, an operator, a\n * list of values) cannot be read off a grouped result, so each of those costs\n * one `/value` count. A filter that holds a LIST of values, or tests a field\n * for no value (`isnull`), is counted through the list endpoint, because the\n * aggregation endpoints cannot express it.\n * Nothing is requested for an entry that did not ask.\n *\n * @module utils/fetchFilterCounts\n */\n\nimport { fetchAggregateValue, fetchGroupedCounts } from './fetchAggregate.js'\nimport { buildHeaders, buildQueryString, prefixUrl } from './headers.js'\nimport { resolveFilterTokens } from './resolveFilterTokens.js'\n\n/**\n * Whether a filter holds a list of values (`{ status: ['open', 'hold'] }`).\n *\n * The aggregation endpoints have no spelling for \"one of these values\", so a\n * filter like this is never sent to them. It is counted through the list\n * endpoint instead, which is where the page's own request sends it.\n *\n * @param {object} filter A filter map.\n * @return {boolean} True when any value is an array.\n * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-counts-on-filters-and-views\n */\nexport function hasListValue(filter) {\n\tif (!filter || typeof filter !== 'object') {\n\t\treturn false\n\t}\n\treturn Object.values(filter).some((value) => Array.isArray(value))\n}\n\n/**\n * Whether a filter tests a field for \"no value\" (`{ assignee: null }` or\n * `{ supersededBy: { isnull: true } }`).\n *\n * OpenRegister's aggregation endpoints have no null operator: `isnull` is\n * refused there and an empty value is dropped or matches nothing, so such a\n * filter counted 0 while the list showed rows. The list endpoint does read\n * `field[isnull]=true`, so a filter like this is counted through it.\n *\n * @param {object} filter A filter map.\n * @return {boolean} True when any value is null or carries an `isnull` operator.\n */\nexport function hasNullTest(filter) {\n\tif (!filter || typeof filter !== 'object') {\n\t\treturn false\n\t}\n\treturn Object.values(filter).some((value) => value === null\n\t\t|| (value && typeof value === 'object' && !Array.isArray(value) && Object.keys(value).some((op) => op.toLowerCase() === 'isnull')))\n}\n\n/**\n * Whether the aggregation endpoints cannot express a filter, so it is counted\n * through the list endpoint: a list of values, or a test for no value.\n *\n * @param {object} filter A filter map.\n * @return {boolean} True when the filter needs the list endpoint.\n */\nexport function needsListEndpoint(filter) {\n\treturn hasListValue(filter) || hasNullTest(filter)\n}\n\n/**\n * Count the records a filter matches by asking the LIST endpoint for one\n * record and reading its `total`. The filter is sent the way the list request\n * sends it: a list of values as repeated `field[]=`, an operator as\n * `field[op]=`.\n *\n * @param {string} register The register slug.\n * @param {string} schema The schema slug.\n * @param {object} filter The filter map.\n * @param {object} [ctx] Token-resolution context for the filter.\n * @return {Promise<number|null>} The total, or null when the response carries none.\n * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-counts-on-filters-and-views\n */\nexport async function fetchListTotal(register, schema, filter, ctx) {\n\tconst resolved = resolveFilterTokens(filter || {}, ctx || {})\n\tconst params = {}\n\tfor (const [key, value] of Object.entries(resolved || {})) {\n\t\tif (value && typeof value === 'object' && !Array.isArray(value)) {\n\t\t\tfor (const [op, operand] of Object.entries(value)) {\n\t\t\t\tparams[`${key}[${op}]`] = operand\n\t\t\t}\n\t\t} else if (value === null) {\n\t\t\t// \"No value\": the list endpoint reads it as the isnull operator.\n\t\t\tparams[`${key}[isnull]`] = 'true'\n\t\t} else {\n\t\t\tparams[key] = value\n\t\t}\n\t}\n\tconst qs = buildQueryString({ ...params, _limit: 1 })\n\tconst url = prefixUrl(`/apps/openregister/api/objects/${encodeURIComponent(register)}/${encodeURIComponent(schema)}${qs}`)\n\tconst response = await fetch(url, { headers: buildHeaders() })\n\tif (!response.ok) {\n\t\tthrow new Error(`list returned ${response.status}`)\n\t}\n\tconst data = await response.json()\n\treturn typeof data.total === 'number' ? data.total : null\n}\n\n/**\n * The single field a filter narrows on with a plain equality, or null when the\n * filter is anything else.\n *\n * @param {object} filter A filter map.\n * @return {{field: string, value: string}|null} The field and the value it must equal.\n * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-counts-on-filters-and-views\n */\nexport function singleEqualityOf(filter) {\n\tif (!filter || typeof filter !== 'object' || Array.isArray(filter)) {\n\t\treturn null\n\t}\n\tconst keys = Object.keys(filter)\n\tif (keys.length !== 1) {\n\t\treturn null\n\t}\n\tconst value = filter[keys[0]]\n\tif (value === null || value === undefined || value === '' || typeof value === 'object') {\n\t\treturn null\n\t}\n\t// A token (`@me`, `@today`) resolves to a value the grouped keys are\n\t// compared against only after resolution, which the count request does.\n\tif (typeof value === 'string' && value.charAt(0) === '@') {\n\t\treturn null\n\t}\n\treturn { field: keys[0], value: String(value) }\n}\n\n/**\n * Count the records each entry's filter matches.\n *\n * @param {object} options The request.\n * @param {string} options.register The register slug.\n * @param {string} options.schema The schema slug.\n * @param {Array<{key: (string|number), filter: object}>} options.entries The filters to count, each under its own key.\n * @param {object} [options.baseFilter] A filter every count is narrowed by (the page's fixed filter).\n * @param {object} [options.ctx] Token-resolution context for the filters.\n * @return {Promise<{[key: string]: number}>} The count per entry key. An entry whose count failed is left out.\n * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-counts-on-filters-and-views\n */\nexport async function fetchFilterCounts({ register, schema, entries, baseFilter, ctx } = {}) {\n\tconst counts = {}\n\tif (!register || !schema || !Array.isArray(entries) || entries.length === 0) {\n\t\treturn counts\n\t}\n\tconst base = (baseFilter && typeof baseFilter === 'object') ? baseFilter : {}\n\n\t// Entries that share one equality field go into one grouped request, but\n\t// only when at least two do: a lone entry costs one request either way,\n\t// and `/value` returns exactly its number.\n\t// A list of values anywhere in a request rules the aggregation endpoints\n\t// out for it. A base filter with one rules them out for every entry.\n\tconst baseHasList = needsListEndpoint(base)\n\tconst byField = {}\n\tconst single = []\n\tconst viaList = []\n\tfor (const entry of entries) {\n\t\tif (baseHasList || needsListEndpoint(entry.filter)) {\n\t\t\tviaList.push(entry)\n\t\t\tcontinue\n\t\t}\n\t\tconst eq = singleEqualityOf(entry.filter)\n\t\tif (eq !== null && !(eq.field in base)) {\n\t\t\t(byField[eq.field] = byField[eq.field] || []).push({ entry, value: eq.value })\n\t\t} else {\n\t\t\tsingle.push(entry)\n\t\t}\n\t}\n\tconst jobs = []\n\tfor (const [field, members] of Object.entries(byField)) {\n\t\tif (members.length < 2) {\n\t\t\tsingle.push(members[0].entry)\n\t\t\tcontinue\n\t\t}\n\t\tjobs.push(fetchGroupedCounts({ register, schema, groupBy: field, filter: base }, ctx)\n\t\t\t.then((groups) => {\n\t\t\t\tconst map = {}\n\t\t\t\tfor (const group of groups) {\n\t\t\t\t\tmap[group.key] = group.count\n\t\t\t\t}\n\t\t\t\tfor (const member of members) {\n\t\t\t\t\tcounts[member.entry.key] = map[member.value] || 0\n\t\t\t\t}\n\t\t\t})\n\t\t\t.catch(() => {}))\n\t}\n\tfor (const entry of single) {\n\t\tjobs.push(fetchAggregateValue({ register, schema, metric: 'count', filter: { ...base, ...(entry.filter || {}) } }, ctx)\n\t\t\t.then((value) => {\n\t\t\t\tif (value !== null && Number.isFinite(Number(value))) {\n\t\t\t\t\tcounts[entry.key] = Number(value)\n\t\t\t\t}\n\t\t\t})\n\t\t\t.catch(() => {}))\n\t}\n\tfor (const entry of viaList) {\n\t\tjobs.push(fetchListTotal(register, schema, { ...base, ...(entry.filter || {}) }, ctx)\n\t\t\t.then((total) => {\n\t\t\t\tif (total !== null) {\n\t\t\t\t\tcounts[entry.key] = total\n\t\t\t\t}\n\t\t\t})\n\t\t\t.catch(() => {}))\n\t}\n\tawait Promise.all(jobs)\n\treturn counts\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -50,6 +50,11 @@ function formatValue(value, format, now) {
|
|
|
50
50
|
if (format === 'datetime') {
|
|
51
51
|
return formatBoardDateTime(value, now)
|
|
52
52
|
}
|
|
53
|
+
if (format === 'days') {
|
|
54
|
+
// Whole days from the date to now (0 for today; negative for a future date).
|
|
55
|
+
const date = parseDateValue(value);
|
|
56
|
+
return date === null ? '' : String(Math.floor((now.getTime() - date.getTime()) / 86400000))
|
|
57
|
+
}
|
|
53
58
|
return String(value)
|
|
54
59
|
}
|
|
55
60
|
|
|
@@ -58,7 +63,8 @@ function formatValue(value, format, now) {
|
|
|
58
63
|
* `"Termijn {deadline|date}"` from an object.
|
|
59
64
|
*
|
|
60
65
|
* Every `{path}` token is a dot path into the object; `{path|date}` and
|
|
61
|
-
* `{path|datetime}` format it as the boards write dates
|
|
66
|
+
* `{path|datetime}` format it as the boards write dates, and `{path|days}` is the
|
|
67
|
+
* whole days since that date ("has waited {createdAt|days} days"). A token without a
|
|
62
68
|
* value empties the WHOLE template, because "Termijn " is worse than nothing.
|
|
63
69
|
* A template without tokens is returned as written (a constant label).
|
|
64
70
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fillTemplate.js","sources":["../../../src/utils/fillTemplate.js"],"sourcesContent":["// SPDX-FileCopyrightText: 2026 Conduction B.V.\n// SPDX-License-Identifier: EUPL-1.2\n\nimport { boardDateLocale, formatBoardDate } from './boardDate.js'\nimport { parseDateValue } from './dateVariant.js'\nimport { readPath } from './readPath.js'\n\nconst TOKEN = /\\{([^{}\\s|]+)(?:\\|([a-z]+))?\\}/g\n\n/**\n * A moment in the board's short form with the time: \"4 okt, 10:52\", or\n * \"14 feb 2024, 09:00\" outside the current year.\n *\n * @param {unknown} value A Date, a timestamp or an ISO string.\n * @param {Date} [now] The reference moment.\n * @return {string} The text, or '' when the value is not a date.\n * @spec openspec/changes/screens-relation-pages/specs/relation-pages/spec.md#requirement-a-record-list-shows-the-objects-that-point-at-this-record\n */\nexport function formatBoardDateTime(value, now = new Date()) {\n\tconst day = formatBoardDate(value, now)\n\tconst date = parseDateValue(value)\n\tif (day === '' || date === null) {\n\t\treturn ''\n\t}\n\tlet time\n\ttry {\n\t\ttime = new Intl.DateTimeFormat(boardDateLocale(), { hour: '2-digit', minute: '2-digit' }).format(date)\n\t} catch {\n\t\ttime = new Intl.DateTimeFormat(undefined, { hour: '2-digit', minute: '2-digit' }).format(date)\n\t}\n\treturn `${day}, ${time}`\n}\n\n/**\n * Format one token value.\n *\n * @param {unknown} value The raw value.\n * @param {string} format `date`, `datetime` or '' (as is).\n * @param {Date} now The reference moment.\n * @return {string} The text, '' when nothing can be shown.\n */\nfunction formatValue(value, format, now) {\n\tif (value === null || value === undefined || value === '' || typeof value === 'object') {\n\t\treturn ''\n\t}\n\tif (format === 'date') {\n\t\treturn formatBoardDate(value, now)\n\t}\n\tif (format === 'datetime') {\n\t\treturn formatBoardDateTime(value, now)\n\t}\n\treturn String(value)\n}\n\n/**\n * Fill a row template such as `\"{identifier} · {caseTypeName}\"` or\n * `\"Termijn {deadline|date}\"` from an object.\n *\n * Every `{path}` token is a dot path into the object; `{path|date}` and\n * `{path|datetime}` format it as the boards write dates. A token without a\n * value empties the WHOLE template, because \"Termijn \" is worse than nothing.\n * A template without tokens is returned as written (a constant label).\n *\n * @param {string} template The template, already translated.\n * @param {object|null} object The object to read.\n * @param {Date} [now] The reference moment for dates.\n * @return {string} The filled text, or ''.\n * @spec openspec/changes/screens-relation-pages/specs/relation-pages/spec.md#requirement-a-record-list-shows-the-objects-that-point-at-this-record\n */\nexport function fillTemplate(template, object, now = new Date()) {\n\tif (typeof template !== 'string' || template.trim() === '') {\n\t\treturn ''\n\t}\n\tlet missing = false\n\tconst text = template.replace(TOKEN, (_match, path, format) => {\n\t\tconst value = object && typeof object === 'object' ? readPath(object, path) : undefined\n\t\tconst shown = formatValue(value, format || '', now)\n\t\tif (shown === '') {\n\t\t\tmissing = true\n\t\t}\n\t\treturn shown\n\t})\n\treturn missing ? '' : text.trim()\n}\n\n/**\n * The first of several templates that fills: `[\"Klaar {closedAt|date}\",\n * \"Termijn {deadline|date}\"]` reads \"Klaar ...\" once the case is closed and\n * \"Termijn ...\" while it is open. A single string is one candidate.\n *\n * @param {string|string[]} templates The candidates, in order.\n * @param {object|null} object The object to read.\n * @param {(key: string) => string} [translate] Applied to each template before filling.\n * @param {Date} [now] The reference moment for dates.\n * @return {string} The first filled text, or ''.\n * @spec openspec/changes/screens-relation-pages/specs/relation-pages/spec.md#requirement-a-record-list-shows-the-objects-that-point-at-this-record\n */\nexport function firstFilled(templates, object, translate = (key) => key, now = new Date()) {\n\tconst list = Array.isArray(templates) ? templates : [templates]\n\tfor (const template of list) {\n\t\tif (typeof template !== 'string') {\n\t\t\tcontinue\n\t\t}\n\t\tconst text = fillTemplate(translate(template), object, now)\n\t\tif (text !== '') {\n\t\t\treturn text\n\t\t}\n\t}\n\treturn ''\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"fillTemplate.js","sources":["../../../src/utils/fillTemplate.js"],"sourcesContent":["// SPDX-FileCopyrightText: 2026 Conduction B.V.\n// SPDX-License-Identifier: EUPL-1.2\n\nimport { boardDateLocale, formatBoardDate } from './boardDate.js'\nimport { parseDateValue } from './dateVariant.js'\nimport { readPath } from './readPath.js'\n\nconst TOKEN = /\\{([^{}\\s|]+)(?:\\|([a-z]+))?\\}/g\n\n/**\n * A moment in the board's short form with the time: \"4 okt, 10:52\", or\n * \"14 feb 2024, 09:00\" outside the current year.\n *\n * @param {unknown} value A Date, a timestamp or an ISO string.\n * @param {Date} [now] The reference moment.\n * @return {string} The text, or '' when the value is not a date.\n * @spec openspec/changes/screens-relation-pages/specs/relation-pages/spec.md#requirement-a-record-list-shows-the-objects-that-point-at-this-record\n */\nexport function formatBoardDateTime(value, now = new Date()) {\n\tconst day = formatBoardDate(value, now)\n\tconst date = parseDateValue(value)\n\tif (day === '' || date === null) {\n\t\treturn ''\n\t}\n\tlet time\n\ttry {\n\t\ttime = new Intl.DateTimeFormat(boardDateLocale(), { hour: '2-digit', minute: '2-digit' }).format(date)\n\t} catch {\n\t\ttime = new Intl.DateTimeFormat(undefined, { hour: '2-digit', minute: '2-digit' }).format(date)\n\t}\n\treturn `${day}, ${time}`\n}\n\n/**\n * Format one token value.\n *\n * @param {unknown} value The raw value.\n * @param {string} format `date`, `datetime` or '' (as is).\n * @param {Date} now The reference moment.\n * @return {string} The text, '' when nothing can be shown.\n */\nfunction formatValue(value, format, now) {\n\tif (value === null || value === undefined || value === '' || typeof value === 'object') {\n\t\treturn ''\n\t}\n\tif (format === 'date') {\n\t\treturn formatBoardDate(value, now)\n\t}\n\tif (format === 'datetime') {\n\t\treturn formatBoardDateTime(value, now)\n\t}\n\tif (format === 'days') {\n\t\t// Whole days from the date to now (0 for today; negative for a future date).\n\t\tconst date = parseDateValue(value)\n\t\treturn date === null ? '' : String(Math.floor((now.getTime() - date.getTime()) / 86400000))\n\t}\n\treturn String(value)\n}\n\n/**\n * Fill a row template such as `\"{identifier} · {caseTypeName}\"` or\n * `\"Termijn {deadline|date}\"` from an object.\n *\n * Every `{path}` token is a dot path into the object; `{path|date}` and\n * `{path|datetime}` format it as the boards write dates, and `{path|days}` is the\n * whole days since that date (\"has waited {createdAt|days} days\"). A token without a\n * value empties the WHOLE template, because \"Termijn \" is worse than nothing.\n * A template without tokens is returned as written (a constant label).\n *\n * @param {string} template The template, already translated.\n * @param {object|null} object The object to read.\n * @param {Date} [now] The reference moment for dates.\n * @return {string} The filled text, or ''.\n * @spec openspec/changes/screens-relation-pages/specs/relation-pages/spec.md#requirement-a-record-list-shows-the-objects-that-point-at-this-record\n */\nexport function fillTemplate(template, object, now = new Date()) {\n\tif (typeof template !== 'string' || template.trim() === '') {\n\t\treturn ''\n\t}\n\tlet missing = false\n\tconst text = template.replace(TOKEN, (_match, path, format) => {\n\t\tconst value = object && typeof object === 'object' ? readPath(object, path) : undefined\n\t\tconst shown = formatValue(value, format || '', now)\n\t\tif (shown === '') {\n\t\t\tmissing = true\n\t\t}\n\t\treturn shown\n\t})\n\treturn missing ? '' : text.trim()\n}\n\n/**\n * The first of several templates that fills: `[\"Klaar {closedAt|date}\",\n * \"Termijn {deadline|date}\"]` reads \"Klaar ...\" once the case is closed and\n * \"Termijn ...\" while it is open. A single string is one candidate.\n *\n * @param {string|string[]} templates The candidates, in order.\n * @param {object|null} object The object to read.\n * @param {(key: string) => string} [translate] Applied to each template before filling.\n * @param {Date} [now] The reference moment for dates.\n * @return {string} The first filled text, or ''.\n * @spec openspec/changes/screens-relation-pages/specs/relation-pages/spec.md#requirement-a-record-list-shows-the-objects-that-point-at-this-record\n */\nexport function firstFilled(templates, object, translate = (key) => key, now = new Date()) {\n\tconst list = Array.isArray(templates) ? templates : [templates]\n\tfor (const template of list) {\n\t\tif (typeof template !== 'string') {\n\t\t\tcontinue\n\t\t}\n\t\tconst text = fillTemplate(translate(template), object, now)\n\t\tif (text !== '') {\n\t\t\treturn text\n\t\t}\n\t}\n\treturn ''\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SPDX-FileCopyrightText: 2026 Conduction B.V.
|
|
3
|
+
* SPDX-License-Identifier: EUPL-1.2
|
|
4
|
+
*
|
|
5
|
+
* initials: the letters an avatar shows for a free name.
|
|
6
|
+
*
|
|
7
|
+
* @module utils/initials
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The initials of a name: the first letters of its first and its last word,
|
|
12
|
+
* upper-cased ("Sanne de Vries" reads "SV", "Jan" reads "J").
|
|
13
|
+
*
|
|
14
|
+
* @param {unknown} name The name.
|
|
15
|
+
* @return {string} One or two letters, or '' when there is no name.
|
|
16
|
+
* @spec openspec/changes/screens-record-list-avatar/specs/record-list-avatar/spec.md#requirement-a-record-list-row-can-carry-an-avatar
|
|
17
|
+
*/
|
|
18
|
+
function initialsOf(name) {
|
|
19
|
+
if (typeof name !== 'string') {
|
|
20
|
+
return ''
|
|
21
|
+
}
|
|
22
|
+
const words = name.trim().split(/\s+/).filter((word) => word !== '');
|
|
23
|
+
if (words.length === 0) {
|
|
24
|
+
return ''
|
|
25
|
+
}
|
|
26
|
+
const first = Array.from(words[0])[0] || '';
|
|
27
|
+
const last = words.length > 1 ? (Array.from(words[words.length - 1])[0] || '') : '';
|
|
28
|
+
return (first + last).toLocaleUpperCase()
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export { initialsOf };
|
|
32
|
+
//# sourceMappingURL=initials.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"initials.js","sources":["../../../src/utils/initials.js"],"sourcesContent":["/**\n * SPDX-FileCopyrightText: 2026 Conduction B.V.\n * SPDX-License-Identifier: EUPL-1.2\n *\n * initials: the letters an avatar shows for a free name.\n *\n * @module utils/initials\n */\n\n/**\n * The initials of a name: the first letters of its first and its last word,\n * upper-cased (\"Sanne de Vries\" reads \"SV\", \"Jan\" reads \"J\").\n *\n * @param {unknown} name The name.\n * @return {string} One or two letters, or '' when there is no name.\n * @spec openspec/changes/screens-record-list-avatar/specs/record-list-avatar/spec.md#requirement-a-record-list-row-can-carry-an-avatar\n */\nexport function initialsOf(name) {\n\tif (typeof name !== 'string') {\n\t\treturn ''\n\t}\n\tconst words = name.trim().split(/\\s+/).filter((word) => word !== '')\n\tif (words.length === 0) {\n\t\treturn ''\n\t}\n\tconst first = Array.from(words[0])[0] || ''\n\tconst last = words.length > 1 ? (Array.from(words[words.length - 1])[0] || '') : ''\n\treturn (first + last).toLocaleUpperCase()\n}\n"],"names":[],"mappings":"AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAAS,UAAU,CAAC,IAAI,EAAE;AACjC,CAAC,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE;AAC/B,EAAE,OAAO,EAAE;AACX,EAAE;AACF,CAAC,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,KAAK,IAAI,KAAK,EAAE,EAAC;AACrE,CAAC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE;AACzB,EAAE,OAAO,EAAE;AACX,EAAE;AACF,CAAC,MAAM,KAAK,GAAG,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,GAAE;AAC5C,CAAC,MAAM,IAAI,GAAG,KAAK,CAAC,MAAM,GAAG,CAAC,IAAI,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,IAAI,GAAE;AACpF,CAAC,OAAO,CAAC,KAAK,GAAG,IAAI,EAAE,iBAAiB,EAAE;AAC1C;;;;"}
|
|
@@ -94,6 +94,8 @@ const DASHBOARD_CATALOG_WIDGET_KEYS = Object.freeze([
|
|
|
94
94
|
'banner',
|
|
95
95
|
'calendar',
|
|
96
96
|
'chart',
|
|
97
|
+
'chips-field',
|
|
98
|
+
'choice-picker',
|
|
97
99
|
'container',
|
|
98
100
|
'conversation',
|
|
99
101
|
'countdown',
|
|
@@ -126,21 +128,26 @@ const DASHBOARD_CATALOG_WIDGET_KEYS = Object.freeze([
|
|
|
126
128
|
'object-table',
|
|
127
129
|
'people',
|
|
128
130
|
'quicklinks',
|
|
131
|
+
'radio-cards',
|
|
129
132
|
'record-list',
|
|
130
133
|
'related',
|
|
131
134
|
'saved-view',
|
|
135
|
+
'saved-views',
|
|
132
136
|
'sections',
|
|
137
|
+
'settings-form',
|
|
133
138
|
'spend-analytics',
|
|
134
139
|
'stacked-bar',
|
|
135
140
|
'stages',
|
|
136
141
|
'stat',
|
|
137
142
|
'stats-block',
|
|
143
|
+
'switch-rows',
|
|
138
144
|
'table',
|
|
139
145
|
'tabs',
|
|
140
146
|
'tasks',
|
|
141
147
|
'text',
|
|
142
148
|
'tile',
|
|
143
149
|
'timeline',
|
|
150
|
+
'toggle-table',
|
|
144
151
|
'video',
|
|
145
152
|
'week-strip',
|
|
146
153
|
'workspace-filter',
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"libraryWidgetKeys.js","sources":["../../../src/utils/libraryWidgetKeys.js"],"sourcesContent":["/**\n * SPDX-FileCopyrightText: 2026 Conduction B.V.\n * SPDX-License-Identifier: EUPL-1.2\n *\n * libraryWidgetKeys — the one vocabulary of widget keys the LIBRARY itself\n * renders, and the only list `validateManifestV2` is allowed to consult.\n *\n * WHY THIS MODULE EXISTS\n * ----------------------\n * The validator used to carry its own hand-written array of \"library built-in\n * widget keys\", with a comment instructing whoever added a built-in to append\n * it \"in the same PR\". That instruction was followed for five keys and missed\n * for six, so the validator knew 11 keys while the library actually rendered\n * 45. Nothing failed when the two disagreed, because the only consumer is an\n * exemption: a key the validator does not recognise is treated as a CUSTOM\n * registry component, which makes a legitimate single-widget library dashboard\n * fail the ADR-036 Decision 1 anti-pattern rule with advice (\"declare it as\n * type:'custom'\") that is wrong for a library widget.\n *\n * A comment cannot enforce anything, so the array moved here and\n * `tests/utils/libraryWidgetKeys.spec.js` now binds it to the two registries\n * that actually resolve a key at runtime:\n *\n * 1. `BUILT_IN_WIDGETS` (components/CnWidgetGrid/builtInWidgets.js) — the v2\n * manifest widget set `CnWidgetGrid` resolves a `widgetKey` against.\n * 2. The dashboard widget catalog (`dashboardWidgetRegistry`), populated by\n * `registerDashboardWidgets.js`. `CnWidgetGrid` falls back to it (third in\n * the chain, after the consumer registry and `BUILT_IN_WIDGETS`), so a\n * catalog key renders on a v2 page exactly like a `BUILT_IN_WIDGETS` one.\n *\n * The test asserts SET EQUALITY in both directions against the live registries,\n * so adding a widget to either registry without touching this file fails CI,\n * and deleting one without removing it here fails too. The lists below are the\n * validator's view; the registries are the truth the test holds them to.\n *\n * THE CATALOG IS NOT LEGACY. It is a live half of the same vocabulary, and the\n * old array already conceded the point by exempting `chart` and `stats-block`,\n * which are catalog-only keys that `BUILT_IN_WIDGETS` has never held. What it\n * got wrong was doing that for two keys and not the other thirty-seven.\n * `widgetDispatch.js` unified the two resolution paths for the same reason: a\n * widget authored against one vocabulary was invisible to the other and said\n * nothing about why (101 of 236 detail widgets on hrmq).\n *\n * @module utils/libraryWidgetKeys\n */\n\nimport { WIDGET_TYPE_ALIASES } from './widgetTypeAliases.js'\n\n/**\n * Keys of `BUILT_IN_WIDGETS` — the v2 manifest widget set.\n *\n * Kept in the registry's own declaration order so a diff against\n * `builtInWidgets.js` reads straight down.\n *\n * @type {string[]} Frozen.\n */\nexport const BUILT_IN_WIDGET_KEYS = Object.freeze([\n\t'object-table',\n\t'form-renderer',\n\t'map-viewer',\n\t'object-geo',\n\t'card-grid',\n\t'nav-card-grid',\n\t'data',\n\t'metadata',\n\t'related',\n\t'integration',\n\t'banner',\n\t'audit-trail',\n\t'timeline',\n\t'header',\n\t'text',\n\t'divider',\n])\n\n/**\n * Widget types the LIBRARY registers into the dashboard catalog at import time\n * (`registerDashboardWidgets.js` and the widget `index.js` modules it pulls in).\n *\n * Deliberately a static list rather than a read of `dashboardWidgetRegistry`:\n * that object is mutable and consumer-extensible, so deriving the exemption\n * from it at call time would let an app register its own full-page component in\n * the catalog and walk straight through the ADR-036 Decision 1 rule the\n * exemption exists to enforce. Only keys the library itself ships are exempt.\n *\n * Sorted, because unlike `BUILT_IN_WIDGETS` these registrations are spread over\n * ~30 files and have no meaningful declaration order.\n *\n * @type {string[]} Frozen.\n */\nexport const DASHBOARD_CATALOG_WIDGET_KEYS = Object.freeze([\n\t'audit-trail',\n\t'banner',\n\t'calendar',\n\t'chart',\n\t'container',\n\t'conversation',\n\t'countdown',\n\t'data',\n\t'delta',\n\t'divider',\n\t'document-review',\n\t'facts',\n\t'field-list',\n\t'files',\n\t'flow-runs',\n\t'gauge',\n\t'header',\n\t'image',\n\t'interaction-form',\n\t'kb-search',\n\t'label',\n\t'link',\n\t'links',\n\t'map',\n\t'markdown',\n\t'menu',\n\t'nc-widget',\n\t'next-step',\n\t'notepad',\n\t// `news` was here until the widget moved to LaunchPad, which owns the feed\n\t// fetching. An app that renders one now gets it from LaunchPad's registry.\n\t'object-geo',\n\t'object-list',\n\t'object-table',\n\t'people',\n\t'quicklinks',\n\t'record-list',\n\t'related',\n\t'saved-view',\n\t'sections',\n\t'spend-analytics',\n\t'stacked-bar',\n\t'stages',\n\t'stat',\n\t'stats-block',\n\t'table',\n\t'tabs',\n\t'tasks',\n\t'text',\n\t'tile',\n\t'timeline',\n\t'video',\n\t'week-strip',\n\t'workspace-filter',\n])\n\n/**\n * Expand a key set over `WIDGET_TYPE_ALIASES` so both spellings of an aliased\n * widget are present whenever either one is.\n *\n * `CnWidgetGrid` resolves a key and its canonical form (`object-table` ⇄\n * `table`, `map-viewer` ⇄ `map`), so both spellings render and both must be\n * exempt. Today each registry happens to hold one side of each pair, which is\n * exactly the accident that would silently stop being true when the next alias\n * lands.\n *\n * @param {Set<string>} keys The key set to expand, mutated in place.\n * @return {Set<string>} The same set.\n */\nfunction expandAliases(keys) {\n\tfor (const [alias, canonical] of Object.entries(WIDGET_TYPE_ALIASES)) {\n\t\tif (keys.has(alias) || keys.has(canonical)) {\n\t\t\tkeys.add(alias)\n\t\t\tkeys.add(canonical)\n\t\t}\n\t}\n\treturn keys\n}\n\n/**\n * Every widget key the library renders itself: both registries, plus both\n * spellings of every alias.\n *\n * A `widgetKey` in this list is served by a library `Cn*` SFC and does NOT\n * count as a custom registry component for the single-12×12-widget dashboard\n * rule (ADR-036 Decision 1).\n *\n * @type {string[]} Frozen.\n */\nexport const LIBRARY_WIDGET_KEYS = Object.freeze([\n\t...expandAliases(new Set([\n\t\t...BUILT_IN_WIDGET_KEYS,\n\t\t...DASHBOARD_CATALOG_WIDGET_KEYS,\n\t])),\n])\n"],"names":[],"mappings":";;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACY,MAAC,oBAAoB,GAAG,MAAM,CAAC,MAAM,CAAC;AAClD,CAAC,cAAc;AACf,CAAC,eAAe;AAChB,CAAC,YAAY;AACb,CAAC,YAAY;AACb,CAAC,WAAW;AACZ,CAAC,eAAe;AAChB,CAAC,MAAM;AACP,CAAC,UAAU;AACX,CAAC,SAAS;AACV,CAAC,aAAa;AACd,CAAC,QAAQ;AACT,CAAC,aAAa;AACd,CAAC,UAAU;AACX,CAAC,QAAQ;AACT,CAAC,MAAM;AACP,CAAC,SAAS;AACV,CAAC,EAAC;AACF;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACY,MAAC,6BAA6B,GAAG,MAAM,CAAC,MAAM,CAAC;AAC3D,CAAC,aAAa;AACd,CAAC,QAAQ;AACT,CAAC,UAAU;AACX,CAAC,OAAO;AACR,CAAC,WAAW;AACZ,CAAC,cAAc;AACf,CAAC,WAAW;AACZ,CAAC,MAAM;AACP,CAAC,OAAO;AACR,CAAC,SAAS;AACV,CAAC,iBAAiB;AAClB,CAAC,OAAO;AACR,CAAC,YAAY;AACb,CAAC,OAAO;AACR,CAAC,WAAW;AACZ,CAAC,OAAO;AACR,CAAC,QAAQ;AACT,CAAC,OAAO;AACR,CAAC,kBAAkB;AACnB,CAAC,WAAW;AACZ,CAAC,OAAO;AACR,CAAC,MAAM;AACP,CAAC,OAAO;AACR,CAAC,KAAK;AACN,CAAC,UAAU;AACX,CAAC,MAAM;AACP,CAAC,WAAW;AACZ,CAAC,WAAW;AACZ,CAAC,SAAS;AACV;AACA;AACA,CAAC,YAAY;AACb,CAAC,aAAa;AACd,CAAC,cAAc;AACf,CAAC,QAAQ;AACT,CAAC,YAAY;AACb,CAAC,aAAa;AACd,CAAC,SAAS;AACV,CAAC,YAAY;AACb,CAAC,UAAU;AACX,CAAC,iBAAiB;AAClB,CAAC,aAAa;AACd,CAAC,QAAQ;AACT,CAAC,MAAM;AACP,CAAC,aAAa;AACd,CAAC,OAAO;AACR,CAAC,MAAM;AACP,CAAC,OAAO;AACR,CAAC,MAAM;AACP,CAAC,MAAM;AACP,CAAC,UAAU;AACX,CAAC,OAAO;AACR,CAAC,YAAY;AACb,CAAC,kBAAkB;AACnB,CAAC,EAAC;AACF;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,SAAS,aAAa,CAAC,IAAI,EAAE;AAC7B,CAAC,KAAK,MAAM,CAAC,KAAK,EAAE,SAAS,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,mBAAmB,CAAC,EAAE;AACvE,EAAE,IAAI,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE;AAC9C,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,EAAC;AAClB,GAAG,IAAI,CAAC,GAAG,CAAC,SAAS,EAAC;AACtB,GAAG;AACH,EAAE;AACF,CAAC,OAAO,IAAI;AACZ,CAAC;AACD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACY,MAAC,mBAAmB,GAAG,MAAM,CAAC,MAAM,CAAC;AACjD,CAAC,GAAG,aAAa,CAAC,IAAI,GAAG,CAAC;AAC1B,EAAE,GAAG,oBAAoB;AACzB,EAAE,GAAG,6BAA6B;AAClC,EAAE,CAAC,CAAC;AACJ,CAAC;;;;"}
|
|
1
|
+
{"version":3,"file":"libraryWidgetKeys.js","sources":["../../../src/utils/libraryWidgetKeys.js"],"sourcesContent":["/**\n * SPDX-FileCopyrightText: 2026 Conduction B.V.\n * SPDX-License-Identifier: EUPL-1.2\n *\n * libraryWidgetKeys — the one vocabulary of widget keys the LIBRARY itself\n * renders, and the only list `validateManifestV2` is allowed to consult.\n *\n * WHY THIS MODULE EXISTS\n * ----------------------\n * The validator used to carry its own hand-written array of \"library built-in\n * widget keys\", with a comment instructing whoever added a built-in to append\n * it \"in the same PR\". That instruction was followed for five keys and missed\n * for six, so the validator knew 11 keys while the library actually rendered\n * 45. Nothing failed when the two disagreed, because the only consumer is an\n * exemption: a key the validator does not recognise is treated as a CUSTOM\n * registry component, which makes a legitimate single-widget library dashboard\n * fail the ADR-036 Decision 1 anti-pattern rule with advice (\"declare it as\n * type:'custom'\") that is wrong for a library widget.\n *\n * A comment cannot enforce anything, so the array moved here and\n * `tests/utils/libraryWidgetKeys.spec.js` now binds it to the two registries\n * that actually resolve a key at runtime:\n *\n * 1. `BUILT_IN_WIDGETS` (components/CnWidgetGrid/builtInWidgets.js) — the v2\n * manifest widget set `CnWidgetGrid` resolves a `widgetKey` against.\n * 2. The dashboard widget catalog (`dashboardWidgetRegistry`), populated by\n * `registerDashboardWidgets.js`. `CnWidgetGrid` falls back to it (third in\n * the chain, after the consumer registry and `BUILT_IN_WIDGETS`), so a\n * catalog key renders on a v2 page exactly like a `BUILT_IN_WIDGETS` one.\n *\n * The test asserts SET EQUALITY in both directions against the live registries,\n * so adding a widget to either registry without touching this file fails CI,\n * and deleting one without removing it here fails too. The lists below are the\n * validator's view; the registries are the truth the test holds them to.\n *\n * THE CATALOG IS NOT LEGACY. It is a live half of the same vocabulary, and the\n * old array already conceded the point by exempting `chart` and `stats-block`,\n * which are catalog-only keys that `BUILT_IN_WIDGETS` has never held. What it\n * got wrong was doing that for two keys and not the other thirty-seven.\n * `widgetDispatch.js` unified the two resolution paths for the same reason: a\n * widget authored against one vocabulary was invisible to the other and said\n * nothing about why (101 of 236 detail widgets on hrmq).\n *\n * @module utils/libraryWidgetKeys\n */\n\nimport { WIDGET_TYPE_ALIASES } from './widgetTypeAliases.js'\n\n/**\n * Keys of `BUILT_IN_WIDGETS` — the v2 manifest widget set.\n *\n * Kept in the registry's own declaration order so a diff against\n * `builtInWidgets.js` reads straight down.\n *\n * @type {string[]} Frozen.\n */\nexport const BUILT_IN_WIDGET_KEYS = Object.freeze([\n\t'object-table',\n\t'form-renderer',\n\t'map-viewer',\n\t'object-geo',\n\t'card-grid',\n\t'nav-card-grid',\n\t'data',\n\t'metadata',\n\t'related',\n\t'integration',\n\t'banner',\n\t'audit-trail',\n\t'timeline',\n\t'header',\n\t'text',\n\t'divider',\n])\n\n/**\n * Widget types the LIBRARY registers into the dashboard catalog at import time\n * (`registerDashboardWidgets.js` and the widget `index.js` modules it pulls in).\n *\n * Deliberately a static list rather than a read of `dashboardWidgetRegistry`:\n * that object is mutable and consumer-extensible, so deriving the exemption\n * from it at call time would let an app register its own full-page component in\n * the catalog and walk straight through the ADR-036 Decision 1 rule the\n * exemption exists to enforce. Only keys the library itself ships are exempt.\n *\n * Sorted, because unlike `BUILT_IN_WIDGETS` these registrations are spread over\n * ~30 files and have no meaningful declaration order.\n *\n * @type {string[]} Frozen.\n */\nexport const DASHBOARD_CATALOG_WIDGET_KEYS = Object.freeze([\n\t'audit-trail',\n\t'banner',\n\t'calendar',\n\t'chart',\n\t'chips-field',\n\t'choice-picker',\n\t'container',\n\t'conversation',\n\t'countdown',\n\t'data',\n\t'delta',\n\t'divider',\n\t'document-review',\n\t'facts',\n\t'field-list',\n\t'files',\n\t'flow-runs',\n\t'gauge',\n\t'header',\n\t'image',\n\t'interaction-form',\n\t'kb-search',\n\t'label',\n\t'link',\n\t'links',\n\t'map',\n\t'markdown',\n\t'menu',\n\t'nc-widget',\n\t'next-step',\n\t'notepad',\n\t// `news` was here until the widget moved to LaunchPad, which owns the feed\n\t// fetching. An app that renders one now gets it from LaunchPad's registry.\n\t'object-geo',\n\t'object-list',\n\t'object-table',\n\t'people',\n\t'quicklinks',\n\t'radio-cards',\n\t'record-list',\n\t'related',\n\t'saved-view',\n\t'saved-views',\n\t'sections',\n\t'settings-form',\n\t'spend-analytics',\n\t'stacked-bar',\n\t'stages',\n\t'stat',\n\t'stats-block',\n\t'switch-rows',\n\t'table',\n\t'tabs',\n\t'tasks',\n\t'text',\n\t'tile',\n\t'timeline',\n\t'toggle-table',\n\t'video',\n\t'week-strip',\n\t'workspace-filter',\n])\n\n/**\n * Expand a key set over `WIDGET_TYPE_ALIASES` so both spellings of an aliased\n * widget are present whenever either one is.\n *\n * `CnWidgetGrid` resolves a key and its canonical form (`object-table` ⇄\n * `table`, `map-viewer` ⇄ `map`), so both spellings render and both must be\n * exempt. Today each registry happens to hold one side of each pair, which is\n * exactly the accident that would silently stop being true when the next alias\n * lands.\n *\n * @param {Set<string>} keys The key set to expand, mutated in place.\n * @return {Set<string>} The same set.\n */\nfunction expandAliases(keys) {\n\tfor (const [alias, canonical] of Object.entries(WIDGET_TYPE_ALIASES)) {\n\t\tif (keys.has(alias) || keys.has(canonical)) {\n\t\t\tkeys.add(alias)\n\t\t\tkeys.add(canonical)\n\t\t}\n\t}\n\treturn keys\n}\n\n/**\n * Every widget key the library renders itself: both registries, plus both\n * spellings of every alias.\n *\n * A `widgetKey` in this list is served by a library `Cn*` SFC and does NOT\n * count as a custom registry component for the single-12×12-widget dashboard\n * rule (ADR-036 Decision 1).\n *\n * @type {string[]} Frozen.\n */\nexport const LIBRARY_WIDGET_KEYS = Object.freeze([\n\t...expandAliases(new Set([\n\t\t...BUILT_IN_WIDGET_KEYS,\n\t\t...DASHBOARD_CATALOG_WIDGET_KEYS,\n\t])),\n])\n"],"names":[],"mappings":";;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACY,MAAC,oBAAoB,GAAG,MAAM,CAAC,MAAM,CAAC;AAClD,CAAC,cAAc;AACf,CAAC,eAAe;AAChB,CAAC,YAAY;AACb,CAAC,YAAY;AACb,CAAC,WAAW;AACZ,CAAC,eAAe;AAChB,CAAC,MAAM;AACP,CAAC,UAAU;AACX,CAAC,SAAS;AACV,CAAC,aAAa;AACd,CAAC,QAAQ;AACT,CAAC,aAAa;AACd,CAAC,UAAU;AACX,CAAC,QAAQ;AACT,CAAC,MAAM;AACP,CAAC,SAAS;AACV,CAAC,EAAC;AACF;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACY,MAAC,6BAA6B,GAAG,MAAM,CAAC,MAAM,CAAC;AAC3D,CAAC,aAAa;AACd,CAAC,QAAQ;AACT,CAAC,UAAU;AACX,CAAC,OAAO;AACR,CAAC,aAAa;AACd,CAAC,eAAe;AAChB,CAAC,WAAW;AACZ,CAAC,cAAc;AACf,CAAC,WAAW;AACZ,CAAC,MAAM;AACP,CAAC,OAAO;AACR,CAAC,SAAS;AACV,CAAC,iBAAiB;AAClB,CAAC,OAAO;AACR,CAAC,YAAY;AACb,CAAC,OAAO;AACR,CAAC,WAAW;AACZ,CAAC,OAAO;AACR,CAAC,QAAQ;AACT,CAAC,OAAO;AACR,CAAC,kBAAkB;AACnB,CAAC,WAAW;AACZ,CAAC,OAAO;AACR,CAAC,MAAM;AACP,CAAC,OAAO;AACR,CAAC,KAAK;AACN,CAAC,UAAU;AACX,CAAC,MAAM;AACP,CAAC,WAAW;AACZ,CAAC,WAAW;AACZ,CAAC,SAAS;AACV;AACA;AACA,CAAC,YAAY;AACb,CAAC,aAAa;AACd,CAAC,cAAc;AACf,CAAC,QAAQ;AACT,CAAC,YAAY;AACb,CAAC,aAAa;AACd,CAAC,aAAa;AACd,CAAC,SAAS;AACV,CAAC,YAAY;AACb,CAAC,aAAa;AACd,CAAC,UAAU;AACX,CAAC,eAAe;AAChB,CAAC,iBAAiB;AAClB,CAAC,aAAa;AACd,CAAC,QAAQ;AACT,CAAC,MAAM;AACP,CAAC,aAAa;AACd,CAAC,aAAa;AACd,CAAC,OAAO;AACR,CAAC,MAAM;AACP,CAAC,OAAO;AACR,CAAC,MAAM;AACP,CAAC,MAAM;AACP,CAAC,UAAU;AACX,CAAC,cAAc;AACf,CAAC,OAAO;AACR,CAAC,YAAY;AACb,CAAC,kBAAkB;AACnB,CAAC,EAAC;AACF;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,SAAS,aAAa,CAAC,IAAI,EAAE;AAC7B,CAAC,KAAK,MAAM,CAAC,KAAK,EAAE,SAAS,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,mBAAmB,CAAC,EAAE;AACvE,EAAE,IAAI,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE;AAC9C,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,EAAC;AAClB,GAAG,IAAI,CAAC,GAAG,CAAC,SAAS,EAAC;AACtB,GAAG;AACH,EAAE;AACF,CAAC,OAAO,IAAI;AACZ,CAAC;AACD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACY,MAAC,mBAAmB,GAAG,MAAM,CAAC,MAAM,CAAC;AACjD,CAAC,GAAG,aAAa,CAAC,IAAI,GAAG,CAAC;AAC1B,EAAE,GAAG,oBAAoB;AACzB,EAAE,GAAG,6BAA6B;AAClC,EAAE,CAAC,CAAC;AACJ,CAAC;;;;"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { prefixUrl } from './headers.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SPDX-FileCopyrightText: 2026 Conduction B.V.
|
|
5
|
+
* SPDX-License-Identifier: EUPL-1.2
|
|
6
|
+
*
|
|
7
|
+
* objectWrite: the one-object PATCH the settings widgets share.
|
|
8
|
+
*
|
|
9
|
+
* @module utils/objectWrite
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The OpenRegister URL of one object, or '' when register, schema or id is missing.
|
|
15
|
+
*
|
|
16
|
+
* @param {string|object} register Register slug.
|
|
17
|
+
* @param {string|object} schema Schema slug.
|
|
18
|
+
* @param {string|number} id Object id.
|
|
19
|
+
* @return {string} The object URL.
|
|
20
|
+
*/
|
|
21
|
+
function objectUrlOf(register, schema, id) {
|
|
22
|
+
if (typeof register !== 'string' || typeof schema !== 'string' || !register || !schema || id === undefined || id === null || id === '') {
|
|
23
|
+
return ''
|
|
24
|
+
}
|
|
25
|
+
return `/apps/openregister/api/objects/${encodeURIComponent(register)}/${encodeURIComponent(schema)}/${encodeURIComponent(String(id))}`
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* PATCH only the given fields of one object.
|
|
30
|
+
*
|
|
31
|
+
* @param {string} url The object URL (see objectUrlOf).
|
|
32
|
+
* @param {object} patch The fields to write.
|
|
33
|
+
* @return {Promise<object>} The object the server answered with.
|
|
34
|
+
*/
|
|
35
|
+
async function patchObject(url, patch) {
|
|
36
|
+
const { default: axios } = await import('@nextcloud/axios');
|
|
37
|
+
const { data } = await axios.patch(prefixUrl(url), patch);
|
|
38
|
+
return data && typeof data === 'object' ? data : {}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The fields of `next` that differ from `base`, compared by value.
|
|
43
|
+
*
|
|
44
|
+
* @param {object} base What the server last held.
|
|
45
|
+
* @param {object} next What the form holds.
|
|
46
|
+
* @return {object} The changed fields only.
|
|
47
|
+
*/
|
|
48
|
+
function changedFields(base, next) {
|
|
49
|
+
const patch = {};
|
|
50
|
+
for (const [key, value] of Object.entries(next)) {
|
|
51
|
+
if (JSON.stringify(value === undefined ? null : value) !== JSON.stringify(base[key] === undefined ? null : base[key])) {
|
|
52
|
+
patch[key] = value;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return patch
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export { changedFields, objectUrlOf, patchObject };
|
|
59
|
+
//# sourceMappingURL=objectWrite.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"objectWrite.js","sources":["../../../src/utils/objectWrite.js"],"sourcesContent":["/**\n * SPDX-FileCopyrightText: 2026 Conduction B.V.\n * SPDX-License-Identifier: EUPL-1.2\n *\n * objectWrite: the one-object PATCH the settings widgets share.\n *\n * @module utils/objectWrite\n */\n\nimport { prefixUrl } from './headers.js'\n\n/**\n * The OpenRegister URL of one object, or '' when register, schema or id is missing.\n *\n * @param {string|object} register Register slug.\n * @param {string|object} schema Schema slug.\n * @param {string|number} id Object id.\n * @return {string} The object URL.\n */\nexport function objectUrlOf(register, schema, id) {\n\tif (typeof register !== 'string' || typeof schema !== 'string' || !register || !schema || id === undefined || id === null || id === '') {\n\t\treturn ''\n\t}\n\treturn `/apps/openregister/api/objects/${encodeURIComponent(register)}/${encodeURIComponent(schema)}/${encodeURIComponent(String(id))}`\n}\n\n/**\n * PATCH only the given fields of one object.\n *\n * @param {string} url The object URL (see objectUrlOf).\n * @param {object} patch The fields to write.\n * @return {Promise<object>} The object the server answered with.\n */\nexport async function patchObject(url, patch) {\n\tconst { default: axios } = await import('@nextcloud/axios')\n\tconst { data } = await axios.patch(prefixUrl(url), patch)\n\treturn data && typeof data === 'object' ? data : {}\n}\n\n/**\n * The fields of `next` that differ from `base`, compared by value.\n *\n * @param {object} base What the server last held.\n * @param {object} next What the form holds.\n * @return {object} The changed fields only.\n */\nexport function changedFields(base, next) {\n\tconst patch = {}\n\tfor (const [key, value] of Object.entries(next)) {\n\t\tif (JSON.stringify(value === undefined ? null : value) !== JSON.stringify(base[key] === undefined ? null : base[key])) {\n\t\t\tpatch[key] = value\n\t\t}\n\t}\n\treturn patch\n}\n"],"names":[],"mappings":";;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAAS,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE;AAClD,CAAC,IAAI,OAAO,QAAQ,KAAK,QAAQ,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,CAAC,QAAQ,IAAI,CAAC,MAAM,IAAI,EAAE,KAAK,SAAS,IAAI,EAAE,KAAK,IAAI,IAAI,EAAE,KAAK,EAAE,EAAE;AACzI,EAAE,OAAO,EAAE;AACX,EAAE;AACF,CAAC,OAAO,CAAC,+BAA+B,EAAE,kBAAkB,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,kBAAkB,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,kBAAkB,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;AACxI,CAAC;AACD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,eAAe,WAAW,CAAC,GAAG,EAAE,KAAK,EAAE;AAC9C,CAAC,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,GAAG,MAAM,OAAO,kBAAkB,EAAC;AAC5D,CAAC,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,KAAK,CAAC,KAAK,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE,KAAK,EAAC;AAC1D,CAAC,OAAO,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ,GAAG,IAAI,GAAG,EAAE;AACpD,CAAC;AACD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAAS,aAAa,CAAC,IAAI,EAAE,IAAI,EAAE;AAC1C,CAAC,MAAM,KAAK,GAAG,GAAE;AACjB,CAAC,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE;AAClD,EAAE,IAAI,IAAI,CAAC,SAAS,CAAC,KAAK,KAAK,SAAS,GAAG,IAAI,GAAG,KAAK,CAAC,KAAK,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,SAAS,GAAG,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE;AACzH,GAAG,KAAK,CAAC,GAAG,CAAC,GAAG,MAAK;AACrB,GAAG;AACH,EAAE;AACF,CAAC,OAAO,KAAK;AACb;;;;"}
|