@conduction/nextcloud-vue 2.59.0 → 2.61.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/CnActionsMenu/CnActionsMenu.vue2.js +14 -1
- package/dist/esm/components/CnActionsMenu/CnActionsMenu.vue2.js.map +1 -1
- package/dist/esm/components/CnActionsMenu/CnActionsMenu.vue3.js.map +1 -1
- package/dist/esm/components/CnAppNav/CnAppNav.vue2.js +80 -1
- package/dist/esm/components/CnAppNav/CnAppNav.vue2.js.map +1 -1
- package/dist/esm/components/CnAppNav/CnAppNav.vue3.js +46 -4
- package/dist/esm/components/CnAppNav/CnAppNav.vue3.js.map +1 -1
- package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js +232 -10
- package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue3.js +79 -25
- package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnBannerWidgetForm/CnBannerWidgetForm.vue2.js +42 -3
- package/dist/esm/components/CnBannerWidgetForm/CnBannerWidgetForm.vue2.js.map +1 -1
- package/dist/esm/components/CnBannerWidgetForm/CnBannerWidgetForm.vue3.js +39 -6
- package/dist/esm/components/CnBannerWidgetForm/CnBannerWidgetForm.vue3.js.map +1 -1
- package/dist/esm/components/CnBoardView/CnBoardView.vue2.js +46 -0
- package/dist/esm/components/CnBoardView/CnBoardView.vue2.js.map +1 -1
- package/dist/esm/components/CnBoardView/CnBoardView.vue3.js +11 -4
- package/dist/esm/components/CnBoardView/CnBoardView.vue3.js.map +1 -1
- package/dist/esm/components/CnBrandStripe/CnBrandStripe.vue.js +10 -0
- package/dist/esm/components/CnBrandStripe/CnBrandStripe.vue.js.map +1 -0
- package/dist/esm/components/CnBrandStripe/CnBrandStripe.vue2.js +39 -0
- package/dist/esm/components/CnBrandStripe/CnBrandStripe.vue2.js.map +1 -0
- package/dist/esm/components/CnBrandStripe/CnBrandStripe.vue3.js +16 -0
- package/dist/esm/components/CnBrandStripe/CnBrandStripe.vue3.js.map +1 -0
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js +117 -1
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js.map +1 -1
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js +147 -89
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js.map +1 -1
- package/dist/esm/components/CnConversationThread/CnConversationThread.vue.js +10 -0
- package/dist/esm/components/CnConversationThread/CnConversationThread.vue.js.map +1 -0
- package/dist/esm/components/CnConversationThread/CnConversationThread.vue2.js +259 -0
- package/dist/esm/components/CnConversationThread/CnConversationThread.vue2.js.map +1 -0
- package/dist/esm/components/CnConversationThread/CnConversationThread.vue3.js +131 -0
- package/dist/esm/components/CnConversationThread/CnConversationThread.vue3.js.map +1 -0
- package/dist/esm/components/CnConversationThread/CnConversationWidget.vue.js +10 -0
- package/dist/esm/components/CnConversationThread/CnConversationWidget.vue.js.map +1 -0
- package/dist/esm/components/CnConversationThread/CnConversationWidget.vue2.js +116 -0
- package/dist/esm/components/CnConversationThread/CnConversationWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnConversationThread/CnConversationWidget.vue3.js +24 -0
- package/dist/esm/components/CnConversationThread/CnConversationWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +10 -2
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +503 -28
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +303 -144
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
- package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewList.vue.js +10 -0
- package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewList.vue.js.map +1 -0
- package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewList.vue2.js +237 -0
- package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewList.vue2.js.map +1 -0
- package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewList.vue3.js +120 -0
- package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewList.vue3.js.map +1 -0
- package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewWidget.vue.js +10 -0
- package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewWidget.vue.js.map +1 -0
- package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewWidget.vue2.js +77 -0
- package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewWidget.vue3.js +27 -0
- package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue2.js +171 -12
- package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue3.js +15 -7
- package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnHeaderWidgetForm/CnHeaderWidgetForm.vue2.js +37 -3
- package/dist/esm/components/CnHeaderWidgetForm/CnHeaderWidgetForm.vue2.js.map +1 -1
- package/dist/esm/components/CnHeaderWidgetForm/CnHeaderWidgetForm.vue3.js +44 -16
- package/dist/esm/components/CnHeaderWidgetForm/CnHeaderWidgetForm.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +132 -3
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +6 -3
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
- package/dist/esm/components/CnLinkCardsPage/CnLinkCardsPage.vue.js +10 -0
- package/dist/esm/components/CnLinkCardsPage/CnLinkCardsPage.vue.js.map +1 -0
- package/dist/esm/components/CnLinkCardsPage/CnLinkCardsPage.vue2.js +316 -0
- package/dist/esm/components/CnLinkCardsPage/CnLinkCardsPage.vue2.js.map +1 -0
- package/dist/esm/components/CnLinkCardsPage/CnLinkCardsPage.vue3.js +105 -0
- package/dist/esm/components/CnLinkCardsPage/CnLinkCardsPage.vue3.js.map +1 -0
- package/dist/esm/components/CnNextStepCard/CnNextStepCard.vue.js +10 -0
- package/dist/esm/components/CnNextStepCard/CnNextStepCard.vue.js.map +1 -0
- package/dist/esm/components/CnNextStepCard/CnNextStepCard.vue2.js +150 -0
- package/dist/esm/components/CnNextStepCard/CnNextStepCard.vue2.js.map +1 -0
- package/dist/esm/components/CnNextStepCard/CnNextStepCard.vue3.js +106 -0
- package/dist/esm/components/CnNextStepCard/CnNextStepCard.vue3.js.map +1 -0
- package/dist/esm/components/CnNextStepCard/CnNextStepWidget.vue.js +9 -0
- package/dist/esm/components/CnNextStepCard/CnNextStepWidget.vue.js.map +1 -0
- package/dist/esm/components/CnNextStepCard/CnNextStepWidget.vue2.js +47 -0
- package/dist/esm/components/CnNextStepCard/CnNextStepWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnNextStepCard/CnNextStepWidget.vue3.js +17 -0
- package/dist/esm/components/CnNextStepCard/CnNextStepWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js +49 -0
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnObjectDataWidgetForm/CnObjectDataWidgetForm.vue2.js +6 -0
- package/dist/esm/components/CnObjectDataWidgetForm/CnObjectDataWidgetForm.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectDataWidgetForm/CnObjectDataWidgetForm.vue3.js.map +1 -1
- package/dist/esm/components/CnObjectKanban/CnObjectKanban.vue2.js +46 -0
- package/dist/esm/components/CnObjectKanban/CnObjectKanban.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectKanban/CnObjectKanban.vue3.js +15 -3
- package/dist/esm/components/CnObjectKanban/CnObjectKanban.vue3.js.map +1 -1
- package/dist/esm/components/CnPageRenderer/pageTypes.js +1 -0
- package/dist/esm/components/CnPageRenderer/pageTypes.js.map +1 -1
- package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue2.js +37 -1
- package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue2.js.map +1 -1
- package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue3.js +29 -13
- package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue3.js.map +1 -1
- package/dist/esm/components/CnSavedViewsControl/CnSavedViewsControl.vue2.js +50 -3
- package/dist/esm/components/CnSavedViewsControl/CnSavedViewsControl.vue2.js.map +1 -1
- package/dist/esm/components/CnSavedViewsControl/CnSavedViewsControl.vue3.js.map +1 -1
- package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue.js +10 -0
- package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue.js.map +1 -0
- package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue2.js +186 -0
- package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue2.js.map +1 -0
- package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue3.js +46 -0
- package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue3.js.map +1 -0
- package/dist/esm/components/CnStackedBarWidget/CnStackedBarWidget.vue.js +11 -0
- package/dist/esm/components/CnStackedBarWidget/CnStackedBarWidget.vue.js.map +1 -0
- package/dist/esm/components/CnStackedBarWidget/CnStackedBarWidget.vue2.js +298 -0
- package/dist/esm/components/CnStackedBarWidget/CnStackedBarWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnStackedBarWidget/CnStackedBarWidget.vue3.js +77 -0
- package/dist/esm/components/CnStackedBarWidget/CnStackedBarWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnStackedBarWidgetForm/CnStackedBarWidgetForm.vue.js +10 -0
- package/dist/esm/components/CnStackedBarWidgetForm/CnStackedBarWidgetForm.vue.js.map +1 -0
- package/dist/esm/components/CnStackedBarWidgetForm/CnStackedBarWidgetForm.vue2.js +169 -0
- package/dist/esm/components/CnStackedBarWidgetForm/CnStackedBarWidgetForm.vue2.js.map +1 -0
- package/dist/esm/components/CnStackedBarWidgetForm/CnStackedBarWidgetForm.vue3.js +48 -0
- package/dist/esm/components/CnStackedBarWidgetForm/CnStackedBarWidgetForm.vue3.js.map +1 -0
- package/dist/esm/components/CnTabs/CnTab.vue2.js +35 -0
- package/dist/esm/components/CnTabs/CnTab.vue2.js.map +1 -1
- package/dist/esm/components/CnTabs/CnTab.vue3.js.map +1 -1
- package/dist/esm/components/CnTabs/CnTabs.vue2.js +120 -7
- package/dist/esm/components/CnTabs/CnTabs.vue2.js.map +1 -1
- package/dist/esm/components/CnTabs/CnTabs.vue3.js +57 -5
- package/dist/esm/components/CnTabs/CnTabs.vue3.js.map +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +59 -4
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js +6 -2
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue.js +10 -0
- package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue.js.map +1 -0
- package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue2.js +484 -0
- package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue3.js +115 -0
- package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnWeekStripWidgetForm/CnWeekStripWidgetForm.vue.js +10 -0
- package/dist/esm/components/CnWeekStripWidgetForm/CnWeekStripWidgetForm.vue.js.map +1 -0
- package/dist/esm/components/CnWeekStripWidgetForm/CnWeekStripWidgetForm.vue2.js +189 -0
- package/dist/esm/components/CnWeekStripWidgetForm/CnWeekStripWidgetForm.vue2.js.map +1 -0
- package/dist/esm/components/CnWeekStripWidgetForm/CnWeekStripWidgetForm.vue3.js +74 -0
- package/dist/esm/components/CnWeekStripWidgetForm/CnWeekStripWidgetForm.vue3.js.map +1 -0
- package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js +82 -8
- package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js.map +1 -1
- package/dist/esm/index.js +16 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/l10n/en.json.js +55 -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 +1036 -19
- package/dist/esm/utils/dateVariant.js +124 -0
- package/dist/esm/utils/dateVariant.js.map +1 -0
- package/dist/esm/utils/detailActionModel.js +260 -0
- package/dist/esm/utils/detailActionModel.js.map +1 -0
- package/dist/esm/utils/dueRule.js +84 -0
- package/dist/esm/utils/dueRule.js.map +1 -0
- package/dist/esm/utils/fetchAggregate.js +37 -1
- package/dist/esm/utils/fetchAggregate.js.map +1 -1
- package/dist/esm/utils/fetchFilterCounts.js +186 -0
- package/dist/esm/utils/fetchFilterCounts.js.map +1 -0
- package/dist/esm/utils/headers.js +28 -2
- package/dist/esm/utils/headers.js.map +1 -1
- package/dist/esm/utils/libraryWidgetKeys.js +5 -0
- package/dist/esm/utils/libraryWidgetKeys.js.map +1 -1
- package/dist/esm/utils/readPath.js +30 -0
- package/dist/esm/utils/readPath.js.map +1 -0
- package/dist/esm/utils/validateManifestV2.compiled.js +2 -2
- package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
- package/dist/nextcloud-vue.cjs.js +16019 -9528
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +1036 -19
- package/l10n/en.json +55 -1
- package/l10n/nl.json +55 -1
- package/package.json +1 -1
- package/scripts/.jsdoc-baselines.json +4 -0
- package/src/components/CnActionsMenu/CnActionsMenu.vue +14 -1
- package/src/components/CnAppNav/CnAppNav.md +26 -0
- package/src/components/CnAppNav/CnAppNav.vue +145 -2
- package/src/components/CnBannerWidget/CnBannerWidget.vue +373 -11
- package/src/components/CnBannerWidgetForm/CnBannerWidgetForm.vue +65 -3
- package/src/components/CnBoardView/CnBoardView.vue +77 -0
- package/src/components/CnBrandStripe/CnBrandStripe.md +22 -0
- package/src/components/CnBrandStripe/CnBrandStripe.vue +100 -0
- package/src/components/CnBrandStripe/index.js +4 -0
- package/src/components/CnCellRenderer/CnCellRenderer.vue +207 -1
- package/src/components/CnConversationThread/CnConversationThread.md +51 -0
- package/src/components/CnConversationThread/CnConversationThread.vue +457 -0
- package/src/components/CnConversationThread/CnConversationWidget.vue +144 -0
- package/src/components/CnConversationThread/index.js +4 -0
- package/src/components/CnDashboardPage/CnDashboardPage.vue +10 -2
- package/src/components/CnDetailPage/CnDetailPage.md +16 -0
- package/src/components/CnDetailPage/CnDetailPage.vue +687 -49
- package/src/components/CnDocumentReviewList/CnDocumentReviewList.md +55 -0
- package/src/components/CnDocumentReviewList/CnDocumentReviewList.vue +384 -0
- package/src/components/CnDocumentReviewList/CnDocumentReviewWidget.vue +108 -0
- package/src/components/CnDocumentReviewList/index.js +4 -0
- package/src/components/CnHeaderWidget/CnHeaderWidget.vue +191 -14
- package/src/components/CnHeaderWidgetForm/CnHeaderWidgetForm.vue +53 -3
- package/src/components/CnIndexPage/CnIndexPage.vue +135 -3
- package/src/components/CnLinkCardsPage/CnLinkCardsPage.vue +484 -0
- package/src/components/CnLinkCardsPage/index.js +4 -0
- package/src/components/CnNextStepCard/CnNextStepCard.md +40 -0
- package/src/components/CnNextStepCard/CnNextStepCard.vue +319 -0
- package/src/components/CnNextStepCard/CnNextStepWidget.vue +59 -0
- package/src/components/CnNextStepCard/index.js +4 -0
- package/src/components/CnObjectDataWidget/CnObjectDataWidget.vue +49 -0
- package/src/components/CnObjectDataWidgetForm/CnObjectDataWidgetForm.vue +6 -0
- package/src/components/CnObjectKanban/CnObjectKanban.vue +86 -2
- package/src/components/CnPageRenderer/pageTypes.js +1 -0
- package/src/components/CnQuickFilterBar/CnQuickFilterBar.md +3 -0
- package/src/components/CnQuickFilterBar/CnQuickFilterBar.vue +60 -1
- package/src/components/CnSavedViewsControl/CnSavedViewsControl.vue +50 -3
- package/src/components/CnSegmentedControl/CnSegmentedControl.md +46 -0
- package/src/components/CnSegmentedControl/CnSegmentedControl.vue +291 -0
- package/src/components/CnSegmentedControl/index.js +4 -0
- package/src/components/CnStackedBarWidget/CnStackedBarWidget.md +15 -0
- package/src/components/CnStackedBarWidget/CnStackedBarWidget.vue +414 -0
- package/src/components/CnStackedBarWidget/index.js +14 -0
- package/src/components/CnStackedBarWidgetForm/CnStackedBarWidgetForm.vue +224 -0
- package/src/components/CnStackedBarWidgetForm/index.js +4 -0
- package/src/components/CnTabs/CnTab.vue +35 -0
- package/src/components/CnTabs/CnTabs.vue +244 -10
- package/src/components/CnTabsWidget/CnTabsWidget.vue +63 -4
- package/src/components/CnWeekStripWidget/CnWeekStripWidget.md +31 -0
- package/src/components/CnWeekStripWidget/CnWeekStripWidget.vue +668 -0
- package/src/components/CnWeekStripWidget/index.js +14 -0
- package/src/components/CnWeekStripWidgetForm/CnWeekStripWidgetForm.vue +265 -0
- package/src/components/CnWeekStripWidgetForm/index.js +4 -0
- package/src/components/CnWidgetGrid/registerDashboardWidgets.js +74 -0
- package/src/components/index.js +12 -0
- package/src/css/detail-page.css +57 -2
- package/src/index.js +16 -0
- package/src/schemas/app-manifest-v2.schema.json +433 -3
- package/src/schemas/app-manifest.schema.json +2 -1
- package/src/types/manifest.d.ts +1 -1
- package/src/utils/dateVariant.js +120 -0
- package/src/utils/detailActionModel.js +256 -0
- package/src/utils/dueRule.js +94 -0
- package/src/utils/fetchAggregate.js +36 -0
- package/src/utils/fetchFilterCounts.js +182 -0
- package/src/utils/headers.js +28 -2
- package/src/utils/index.js +1 -0
- package/src/utils/libraryWidgetKeys.js +5 -0
- package/src/utils/readPath.js +27 -0
- package/src/utils/validateManifestV2.compiled.js +2 -2
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { fetchGroupedCounts, fetchAggregateValue } from './fetchAggregate.js';
|
|
2
|
+
import { buildQueryString, prefixUrl, buildHeaders } from './headers.js';
|
|
3
|
+
import { resolveFilterTokens } from './resolveFilterTokens.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* SPDX-FileCopyrightText: 2026 Conduction B.V.
|
|
7
|
+
* SPDX-License-Identifier: EUPL-1.2
|
|
8
|
+
*
|
|
9
|
+
* fetchFilterCounts: how many records each of a set of filters matches, in as
|
|
10
|
+
* few requests as the API allows.
|
|
11
|
+
*
|
|
12
|
+
* Quick filters and saved views usually differ in ONE field (`status: open`,
|
|
13
|
+
* `status: closed`, ...). Those are answered by one `/grouped` request on that
|
|
14
|
+
* field. A filter that narrows on something else (two fields, an operator, a
|
|
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 is counted through
|
|
17
|
+
* the list endpoint, because the aggregation endpoints cannot express it.
|
|
18
|
+
* Nothing is requested for an entry that did not ask.
|
|
19
|
+
*
|
|
20
|
+
* @module utils/fetchFilterCounts
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Whether a filter holds a list of values (`{ status: ['open', 'hold'] }`).
|
|
26
|
+
*
|
|
27
|
+
* The aggregation endpoints have no spelling for "one of these values", so a
|
|
28
|
+
* filter like this is never sent to them. It is counted through the list
|
|
29
|
+
* endpoint instead, which is where the page's own request sends it.
|
|
30
|
+
*
|
|
31
|
+
* @param {object} filter A filter map.
|
|
32
|
+
* @return {boolean} True when any value is an array.
|
|
33
|
+
* @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-counts-on-filters-and-views
|
|
34
|
+
*/
|
|
35
|
+
function hasListValue(filter) {
|
|
36
|
+
if (!filter || typeof filter !== 'object') {
|
|
37
|
+
return false
|
|
38
|
+
}
|
|
39
|
+
return Object.values(filter).some((value) => Array.isArray(value))
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Count the records a filter matches by asking the LIST endpoint for one
|
|
44
|
+
* record and reading its `total`. The filter is sent the way the list request
|
|
45
|
+
* sends it: a list of values as repeated `field[]=`, an operator as
|
|
46
|
+
* `field[op]=`.
|
|
47
|
+
*
|
|
48
|
+
* @param {string} register The register slug.
|
|
49
|
+
* @param {string} schema The schema slug.
|
|
50
|
+
* @param {object} filter The filter map.
|
|
51
|
+
* @param {object} [ctx] Token-resolution context for the filter.
|
|
52
|
+
* @return {Promise<number|null>} The total, or null when the response carries none.
|
|
53
|
+
* @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-counts-on-filters-and-views
|
|
54
|
+
*/
|
|
55
|
+
async function fetchListTotal(register, schema, filter, ctx) {
|
|
56
|
+
const resolved = resolveFilterTokens(filter || {}, ctx || {});
|
|
57
|
+
const params = {};
|
|
58
|
+
for (const [key, value] of Object.entries(resolved || {})) {
|
|
59
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
60
|
+
for (const [op, operand] of Object.entries(value)) {
|
|
61
|
+
params[`${key}[${op}]`] = operand;
|
|
62
|
+
}
|
|
63
|
+
} else {
|
|
64
|
+
params[key] = value;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const qs = buildQueryString({ ...params, _limit: 1 });
|
|
68
|
+
const url = prefixUrl(`/apps/openregister/api/objects/${encodeURIComponent(register)}/${encodeURIComponent(schema)}${qs}`);
|
|
69
|
+
const response = await fetch(url, { headers: buildHeaders() });
|
|
70
|
+
if (!response.ok) {
|
|
71
|
+
throw new Error(`list returned ${response.status}`)
|
|
72
|
+
}
|
|
73
|
+
const data = await response.json();
|
|
74
|
+
return typeof data.total === 'number' ? data.total : null
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The single field a filter narrows on with a plain equality, or null when the
|
|
79
|
+
* filter is anything else.
|
|
80
|
+
*
|
|
81
|
+
* @param {object} filter A filter map.
|
|
82
|
+
* @return {{field: string, value: string}|null} The field and the value it must equal.
|
|
83
|
+
* @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-counts-on-filters-and-views
|
|
84
|
+
*/
|
|
85
|
+
function singleEqualityOf(filter) {
|
|
86
|
+
if (!filter || typeof filter !== 'object' || Array.isArray(filter)) {
|
|
87
|
+
return null
|
|
88
|
+
}
|
|
89
|
+
const keys = Object.keys(filter);
|
|
90
|
+
if (keys.length !== 1) {
|
|
91
|
+
return null
|
|
92
|
+
}
|
|
93
|
+
const value = filter[keys[0]];
|
|
94
|
+
if (value === null || value === undefined || value === '' || typeof value === 'object') {
|
|
95
|
+
return null
|
|
96
|
+
}
|
|
97
|
+
// A token (`@me`, `@today`) resolves to a value the grouped keys are
|
|
98
|
+
// compared against only after resolution, which the count request does.
|
|
99
|
+
if (typeof value === 'string' && value.charAt(0) === '@') {
|
|
100
|
+
return null
|
|
101
|
+
}
|
|
102
|
+
return { field: keys[0], value: String(value) }
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Count the records each entry's filter matches.
|
|
107
|
+
*
|
|
108
|
+
* @param {object} options The request.
|
|
109
|
+
* @param {string} options.register The register slug.
|
|
110
|
+
* @param {string} options.schema The schema slug.
|
|
111
|
+
* @param {Array<{key: (string|number), filter: object}>} options.entries The filters to count, each under its own key.
|
|
112
|
+
* @param {object} [options.baseFilter] A filter every count is narrowed by (the page's fixed filter).
|
|
113
|
+
* @param {object} [options.ctx] Token-resolution context for the filters.
|
|
114
|
+
* @return {Promise<{[key: string]: number}>} The count per entry key. An entry whose count failed is left out.
|
|
115
|
+
* @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-counts-on-filters-and-views
|
|
116
|
+
*/
|
|
117
|
+
async function fetchFilterCounts({ register, schema, entries, baseFilter, ctx } = {}) {
|
|
118
|
+
const counts = {};
|
|
119
|
+
if (!register || !schema || !Array.isArray(entries) || entries.length === 0) {
|
|
120
|
+
return counts
|
|
121
|
+
}
|
|
122
|
+
const base = (baseFilter && typeof baseFilter === 'object') ? baseFilter : {};
|
|
123
|
+
|
|
124
|
+
// Entries that share one equality field go into one grouped request, but
|
|
125
|
+
// only when at least two do: a lone entry costs one request either way,
|
|
126
|
+
// and `/value` returns exactly its number.
|
|
127
|
+
// A list of values anywhere in a request rules the aggregation endpoints
|
|
128
|
+
// out for it. A base filter with one rules them out for every entry.
|
|
129
|
+
const baseHasList = hasListValue(base);
|
|
130
|
+
const byField = {};
|
|
131
|
+
const single = [];
|
|
132
|
+
const viaList = [];
|
|
133
|
+
for (const entry of entries) {
|
|
134
|
+
if (baseHasList || hasListValue(entry.filter)) {
|
|
135
|
+
viaList.push(entry);
|
|
136
|
+
continue
|
|
137
|
+
}
|
|
138
|
+
const eq = singleEqualityOf(entry.filter);
|
|
139
|
+
if (eq !== null && !(eq.field in base)) {
|
|
140
|
+
(byField[eq.field] = byField[eq.field] || []).push({ entry, value: eq.value });
|
|
141
|
+
} else {
|
|
142
|
+
single.push(entry);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
const jobs = [];
|
|
146
|
+
for (const [field, members] of Object.entries(byField)) {
|
|
147
|
+
if (members.length < 2) {
|
|
148
|
+
single.push(members[0].entry);
|
|
149
|
+
continue
|
|
150
|
+
}
|
|
151
|
+
jobs.push(fetchGroupedCounts({ register, schema, groupBy: field, filter: base }, ctx)
|
|
152
|
+
.then((groups) => {
|
|
153
|
+
const map = {};
|
|
154
|
+
for (const group of groups) {
|
|
155
|
+
map[group.key] = group.count;
|
|
156
|
+
}
|
|
157
|
+
for (const member of members) {
|
|
158
|
+
counts[member.entry.key] = map[member.value] || 0;
|
|
159
|
+
}
|
|
160
|
+
})
|
|
161
|
+
.catch(() => {}));
|
|
162
|
+
}
|
|
163
|
+
for (const entry of single) {
|
|
164
|
+
jobs.push(fetchAggregateValue({ register, schema, metric: 'count', filter: { ...base, ...(entry.filter || {}) } }, ctx)
|
|
165
|
+
.then((value) => {
|
|
166
|
+
if (value !== null && Number.isFinite(Number(value))) {
|
|
167
|
+
counts[entry.key] = Number(value);
|
|
168
|
+
}
|
|
169
|
+
})
|
|
170
|
+
.catch(() => {}));
|
|
171
|
+
}
|
|
172
|
+
for (const entry of viaList) {
|
|
173
|
+
jobs.push(fetchListTotal(register, schema, { ...base, ...(entry.filter || {}) }, ctx)
|
|
174
|
+
.then((total) => {
|
|
175
|
+
if (total !== null) {
|
|
176
|
+
counts[entry.key] = total;
|
|
177
|
+
}
|
|
178
|
+
})
|
|
179
|
+
.catch(() => {}));
|
|
180
|
+
}
|
|
181
|
+
await Promise.all(jobs);
|
|
182
|
+
return counts
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export { fetchFilterCounts, fetchListTotal, hasListValue, singleEqualityOf };
|
|
186
|
+
//# sourceMappingURL=fetchFilterCounts.js.map
|
|
@@ -0,0 +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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"}
|
|
@@ -130,10 +130,15 @@ function capitalize(str) {
|
|
|
130
130
|
*
|
|
131
131
|
* Array values are serialized with PHP bracket notation (`key[]=a&key[]=b`)
|
|
132
132
|
* so the Nextcloud/OpenRegister backend parses them as an array — a plain
|
|
133
|
-
* repeated `key=a&key=b` collapses to the last value in PHP.
|
|
134
|
-
*
|
|
133
|
+
* repeated `key=a&key=b` collapses to the last value in PHP. A plain object
|
|
134
|
+
* under a filter key is a set of operators and goes out in bracket form
|
|
135
|
+
* (`{ deadline: { lt: 'x' } }` becomes `deadline[lt]=x`); a key already
|
|
136
|
+
* written that way (`'deadline[lt]'`) passes through unchanged. Reserved keys
|
|
137
|
+
* that start with `_` keep their JSON form (`_order`). Skips
|
|
135
138
|
* null/undefined/empty values.
|
|
136
139
|
*
|
|
140
|
+
* @spec openspec/changes/link-cards-page/specs/link-cards-page/spec.md#requirement-a-filter-operator-is-serialised-in-bracket-form
|
|
141
|
+
*
|
|
137
142
|
* @param {object} params Key-value pairs for query parameters
|
|
138
143
|
* @return {string} Query string including leading '?' or empty string
|
|
139
144
|
*/
|
|
@@ -159,6 +164,27 @@ function buildQueryString(params = {}) {
|
|
|
159
164
|
queryParams.append(arrayKey, String(item));
|
|
160
165
|
}
|
|
161
166
|
}
|
|
167
|
+
} else if (typeof value === 'object' && !key.startsWith('_')) {
|
|
168
|
+
// A filter operator: `{ deadline: { lt: '2026-10-06' } }` goes out
|
|
169
|
+
// as `deadline[lt]=2026-10-06`, the form OpenRegister reads. As
|
|
170
|
+
// JSON it reached the database as the literal value and a date
|
|
171
|
+
// column answered 500. Reserved `_` keys (`_order`) keep JSON.
|
|
172
|
+
for (const [op, operand] of Object.entries(value)) {
|
|
173
|
+
if (operand === undefined || operand === null || operand === '') {
|
|
174
|
+
continue
|
|
175
|
+
}
|
|
176
|
+
if (Array.isArray(operand)) {
|
|
177
|
+
for (const item of operand) {
|
|
178
|
+
if (item !== undefined && item !== null && item !== '') {
|
|
179
|
+
queryParams.append(`${key}[${op}][]`, String(item));
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
} else if (typeof operand === 'object') {
|
|
183
|
+
queryParams.set(`${key}[${op}]`, JSON.stringify(operand));
|
|
184
|
+
} else {
|
|
185
|
+
queryParams.set(`${key}[${op}]`, String(operand));
|
|
186
|
+
}
|
|
187
|
+
}
|
|
162
188
|
} else if (typeof value === 'object') {
|
|
163
189
|
queryParams.set(key, JSON.stringify(value));
|
|
164
190
|
} else {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"headers.js","sources":["../../../src/utils/headers.js"],"sourcesContent":["import { generateUrl } from '@nextcloud/router'\n\n/**\n * Prefix an app-absolute URL path the way `generateUrl()` would: the instance\n * webroot, plus `/index.php` unless Nextcloud reports working URL rewriting.\n *\n * On an instance without pretty URLs the bare `/apps/<app>/api/...` form is\n * not routed at all and answers the web server's own 404, which is why every\n * internal API path has to pass through here or through `generateUrl()`.\n *\n * Only the PREFIX comes from `generateUrl()`: the path itself is appended\n * untouched, because `generateUrl()` URL-encodes any `{placeholder}` it cannot\n * fill, and callers pass templates filled later and JSON query values.\n *\n * Idempotent, and inert on anything that is not an app-absolute path, since it\n * is wrapped around hundreds of call sites: an absolute or protocol-relative\n * URL, a scheme (`data:`, `blob:`, `mailto:`), a document-relative path, or a\n * path already carrying the webroot or `/index.php` comes back unchanged.\n * `/ocs/`, `/remote.php` and `/public.php` get the webroot only, as they are\n * never served behind `/index.php`.\n *\n * @param {string} path URL path (e.g. '/apps/openregister/api/objects')\n * @return {string} The path as the instance routes it\n */\nexport function prefixUrl(path) {\n\tif (typeof path !== 'string' || path === '') {\n\t\treturn path\n\t}\n\t// Absolute URL, protocol-relative URL, or any scheme (data:, blob:, mailto:).\n\tif (/^[a-z][a-z0-9+.-]*:/i.test(path) || path.startsWith('//')) {\n\t\treturn path\n\t}\n\t// Only app-absolute paths are ours to prefix.\n\tif (!path.startsWith('/')) {\n\t\treturn path\n\t}\n\t// `generateUrl('/')` is `<webroot>[/index.php]/`; the webroot is that minus `/index.php`.\n\tconst prefix = generateUrl('/', {}).replace(/\\/$/, '')\n\tconst root = prefix.replace(/\\/index\\.php$/, '')\n\tif (path.startsWith('/index.php') || (root && (path === root || path.startsWith(root + '/')))) {\n\t\treturn path\n\t}\n\tif (/^\\/(ocs|remote\\.php|public\\.php)(\\/|$)/.test(path)) {\n\t\treturn root + path\n\t}\n\treturn prefix + path\n}\n\n/**\n * Build standard Nextcloud request headers for API calls.\n *\n * Includes the CSRF request token and OCS API request header\n * required by Nextcloud's API layer.\n *\n * Two call shapes are supported:\n *\n * 1. **Positional (back-compat)** — `buildHeaders(contentType)`.\n * Returns the same object the v1 signature did.\n *\n * 2. **Options object** — `buildHeaders({ contentType?, organisationUuid?, targetLanguage? })`.\n * When `organisationUuid` is a non-empty string the returned object\n * includes `X-OpenRegister-Organisation: <uuid>`. The header is the\n * FE side of the `multi-tenancy-context` capability and is consumed\n * by OR's MultiTenancyTrait.\n * When `targetLanguage` is a non-empty BCP-47 string the returned\n * object includes `X-Translation-Target-Language: <bcp47>`. The\n * header is the FE side of the `i18n-source-of-truth` write\n * contract — OR stamps the value into `_translations[<bcp47>]`\n * instead of overwriting the source row. See the\n * `i18n-language-negotiation-getters` change.\n *\n * @param {string|object} [opts] Content-Type string (back-compat) or options object.\n * @param {string} [opts.contentType] Content-Type header value\n * @param {string} [opts.organisationUuid] Active organisation UUID for the request\n * @param {string} [opts.targetLanguage] BCP-47 target language for translation writes\n * @return {object} Headers object for use with fetch()\n */\nexport function buildHeaders(opts = 'application/json') {\n\tlet contentType = 'application/json'\n\tlet organisationUuid = null\n\tlet targetLanguage = null\n\n\tif (typeof opts === 'string') {\n\t\t// v1 positional signature: buildHeaders(contentType)\n\t\tcontentType = opts\n\t} else if (opts === null) {\n\t\t// v1 positional signature: buildHeaders(null) — explicit \"no Content-Type\"\n\t\tcontentType = null\n\t} else if (opts && typeof opts === 'object') {\n\t\t// Options-object signature\n\t\tif (Object.hasOwn(opts, 'contentType')) {\n\t\t\tcontentType = opts.contentType\n\t\t}\n\t\tif (typeof opts.organisationUuid === 'string' && opts.organisationUuid.length > 0) {\n\t\t\torganisationUuid = opts.organisationUuid\n\t\t}\n\t\tif (typeof opts.targetLanguage === 'string' && opts.targetLanguage.length > 0) {\n\t\t\ttargetLanguage = opts.targetLanguage\n\t\t}\n\t}\n\n\tconst headers = {\n\t\trequesttoken: typeof OC !== 'undefined' ? OC.requestToken : '',\n\t\t'OCS-APIREQUEST': 'true',\n\t}\n\tif (contentType) {\n\t\theaders['Content-Type'] = contentType\n\t}\n\tif (organisationUuid) {\n\t\theaders['X-OpenRegister-Organisation'] = organisationUuid\n\t}\n\tif (targetLanguage) {\n\t\theaders['X-Translation-Target-Language'] = targetLanguage\n\t}\n\treturn headers\n}\n\n/**\n * Capitalize the first letter of a string.\n *\n * @param {string} str Input string\n * @return {string} Capitalized string\n */\nexport function capitalize(str) {\n\treturn str.charAt(0).toUpperCase() + str.slice(1)\n}\n\n/**\n * Build a query string from a params object.\n *\n * Array values are serialized with PHP bracket notation (`key[]=a&key[]=b`)\n * so the Nextcloud/OpenRegister backend parses them as an array — a plain\n * repeated `key=a&key=b` collapses to the last value in PHP. Also handles\n * _order serialization (JSON.stringify for objects) and skips\n * null/undefined/empty values.\n *\n * @param {object} params Key-value pairs for query parameters\n * @return {string} Query string including leading '?' or empty string\n */\nexport function buildQueryString(params = {}) {\n\tconst queryParams = new URLSearchParams()\n\n\tfor (const [key, value] of Object.entries(params)) {\n\t\tif (value === undefined || value === null || value === '') {\n\t\t\tcontinue\n\t\t}\n\t\tif (Array.isArray(value) && value.length === 0) {\n\t\t\tcontinue\n\t\t}\n\t\tif (typeof value === 'object' && !Array.isArray(value) && Object.keys(value).length === 0) {\n\t\t\tcontinue\n\t\t}\n\t\tif (Array.isArray(value)) {\n\t\t\t// PHP needs `key[]` to receive repeated params as an array;\n\t\t\t// without the brackets only the last value survives server-side.\n\t\t\tconst arrayKey = key.endsWith('[]') ? key : `${key}[]`\n\t\t\tfor (const item of value) {\n\t\t\t\tif (item !== undefined && item !== null && item !== '') {\n\t\t\t\t\tqueryParams.append(arrayKey, String(item))\n\t\t\t\t}\n\t\t\t}\n\t\t} else if (typeof value === 'object') {\n\t\t\tqueryParams.set(key, JSON.stringify(value))\n\t\t} else {\n\t\t\tqueryParams.set(key, String(value))\n\t\t}\n\t}\n\n\tconst str = queryParams.toString()\n\treturn str ? '?' + str : ''\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"headers.js","sources":["../../../src/utils/headers.js"],"sourcesContent":["import { generateUrl } from '@nextcloud/router'\n\n/**\n * Prefix an app-absolute URL path the way `generateUrl()` would: the instance\n * webroot, plus `/index.php` unless Nextcloud reports working URL rewriting.\n *\n * On an instance without pretty URLs the bare `/apps/<app>/api/...` form is\n * not routed at all and answers the web server's own 404, which is why every\n * internal API path has to pass through here or through `generateUrl()`.\n *\n * Only the PREFIX comes from `generateUrl()`: the path itself is appended\n * untouched, because `generateUrl()` URL-encodes any `{placeholder}` it cannot\n * fill, and callers pass templates filled later and JSON query values.\n *\n * Idempotent, and inert on anything that is not an app-absolute path, since it\n * is wrapped around hundreds of call sites: an absolute or protocol-relative\n * URL, a scheme (`data:`, `blob:`, `mailto:`), a document-relative path, or a\n * path already carrying the webroot or `/index.php` comes back unchanged.\n * `/ocs/`, `/remote.php` and `/public.php` get the webroot only, as they are\n * never served behind `/index.php`.\n *\n * @param {string} path URL path (e.g. '/apps/openregister/api/objects')\n * @return {string} The path as the instance routes it\n */\nexport function prefixUrl(path) {\n\tif (typeof path !== 'string' || path === '') {\n\t\treturn path\n\t}\n\t// Absolute URL, protocol-relative URL, or any scheme (data:, blob:, mailto:).\n\tif (/^[a-z][a-z0-9+.-]*:/i.test(path) || path.startsWith('//')) {\n\t\treturn path\n\t}\n\t// Only app-absolute paths are ours to prefix.\n\tif (!path.startsWith('/')) {\n\t\treturn path\n\t}\n\t// `generateUrl('/')` is `<webroot>[/index.php]/`; the webroot is that minus `/index.php`.\n\tconst prefix = generateUrl('/', {}).replace(/\\/$/, '')\n\tconst root = prefix.replace(/\\/index\\.php$/, '')\n\tif (path.startsWith('/index.php') || (root && (path === root || path.startsWith(root + '/')))) {\n\t\treturn path\n\t}\n\tif (/^\\/(ocs|remote\\.php|public\\.php)(\\/|$)/.test(path)) {\n\t\treturn root + path\n\t}\n\treturn prefix + path\n}\n\n/**\n * Build standard Nextcloud request headers for API calls.\n *\n * Includes the CSRF request token and OCS API request header\n * required by Nextcloud's API layer.\n *\n * Two call shapes are supported:\n *\n * 1. **Positional (back-compat)** — `buildHeaders(contentType)`.\n * Returns the same object the v1 signature did.\n *\n * 2. **Options object** — `buildHeaders({ contentType?, organisationUuid?, targetLanguage? })`.\n * When `organisationUuid` is a non-empty string the returned object\n * includes `X-OpenRegister-Organisation: <uuid>`. The header is the\n * FE side of the `multi-tenancy-context` capability and is consumed\n * by OR's MultiTenancyTrait.\n * When `targetLanguage` is a non-empty BCP-47 string the returned\n * object includes `X-Translation-Target-Language: <bcp47>`. The\n * header is the FE side of the `i18n-source-of-truth` write\n * contract — OR stamps the value into `_translations[<bcp47>]`\n * instead of overwriting the source row. See the\n * `i18n-language-negotiation-getters` change.\n *\n * @param {string|object} [opts] Content-Type string (back-compat) or options object.\n * @param {string} [opts.contentType] Content-Type header value\n * @param {string} [opts.organisationUuid] Active organisation UUID for the request\n * @param {string} [opts.targetLanguage] BCP-47 target language for translation writes\n * @return {object} Headers object for use with fetch()\n */\nexport function buildHeaders(opts = 'application/json') {\n\tlet contentType = 'application/json'\n\tlet organisationUuid = null\n\tlet targetLanguage = null\n\n\tif (typeof opts === 'string') {\n\t\t// v1 positional signature: buildHeaders(contentType)\n\t\tcontentType = opts\n\t} else if (opts === null) {\n\t\t// v1 positional signature: buildHeaders(null) — explicit \"no Content-Type\"\n\t\tcontentType = null\n\t} else if (opts && typeof opts === 'object') {\n\t\t// Options-object signature\n\t\tif (Object.hasOwn(opts, 'contentType')) {\n\t\t\tcontentType = opts.contentType\n\t\t}\n\t\tif (typeof opts.organisationUuid === 'string' && opts.organisationUuid.length > 0) {\n\t\t\torganisationUuid = opts.organisationUuid\n\t\t}\n\t\tif (typeof opts.targetLanguage === 'string' && opts.targetLanguage.length > 0) {\n\t\t\ttargetLanguage = opts.targetLanguage\n\t\t}\n\t}\n\n\tconst headers = {\n\t\trequesttoken: typeof OC !== 'undefined' ? OC.requestToken : '',\n\t\t'OCS-APIREQUEST': 'true',\n\t}\n\tif (contentType) {\n\t\theaders['Content-Type'] = contentType\n\t}\n\tif (organisationUuid) {\n\t\theaders['X-OpenRegister-Organisation'] = organisationUuid\n\t}\n\tif (targetLanguage) {\n\t\theaders['X-Translation-Target-Language'] = targetLanguage\n\t}\n\treturn headers\n}\n\n/**\n * Capitalize the first letter of a string.\n *\n * @param {string} str Input string\n * @return {string} Capitalized string\n */\nexport function capitalize(str) {\n\treturn str.charAt(0).toUpperCase() + str.slice(1)\n}\n\n/**\n * Build a query string from a params object.\n *\n * Array values are serialized with PHP bracket notation (`key[]=a&key[]=b`)\n * so the Nextcloud/OpenRegister backend parses them as an array — a plain\n * repeated `key=a&key=b` collapses to the last value in PHP. A plain object\n * under a filter key is a set of operators and goes out in bracket form\n * (`{ deadline: { lt: 'x' } }` becomes `deadline[lt]=x`); a key already\n * written that way (`'deadline[lt]'`) passes through unchanged. Reserved keys\n * that start with `_` keep their JSON form (`_order`). Skips\n * null/undefined/empty values.\n *\n * @spec openspec/changes/link-cards-page/specs/link-cards-page/spec.md#requirement-a-filter-operator-is-serialised-in-bracket-form\n *\n * @param {object} params Key-value pairs for query parameters\n * @return {string} Query string including leading '?' or empty string\n */\nexport function buildQueryString(params = {}) {\n\tconst queryParams = new URLSearchParams()\n\n\tfor (const [key, value] of Object.entries(params)) {\n\t\tif (value === undefined || value === null || value === '') {\n\t\t\tcontinue\n\t\t}\n\t\tif (Array.isArray(value) && value.length === 0) {\n\t\t\tcontinue\n\t\t}\n\t\tif (typeof value === 'object' && !Array.isArray(value) && Object.keys(value).length === 0) {\n\t\t\tcontinue\n\t\t}\n\t\tif (Array.isArray(value)) {\n\t\t\t// PHP needs `key[]` to receive repeated params as an array;\n\t\t\t// without the brackets only the last value survives server-side.\n\t\t\tconst arrayKey = key.endsWith('[]') ? key : `${key}[]`\n\t\t\tfor (const item of value) {\n\t\t\t\tif (item !== undefined && item !== null && item !== '') {\n\t\t\t\t\tqueryParams.append(arrayKey, String(item))\n\t\t\t\t}\n\t\t\t}\n\t\t} else if (typeof value === 'object' && !key.startsWith('_')) {\n\t\t\t// A filter operator: `{ deadline: { lt: '2026-10-06' } }` goes out\n\t\t\t// as `deadline[lt]=2026-10-06`, the form OpenRegister reads. As\n\t\t\t// JSON it reached the database as the literal value and a date\n\t\t\t// column answered 500. Reserved `_` keys (`_order`) keep JSON.\n\t\t\tfor (const [op, operand] of Object.entries(value)) {\n\t\t\t\tif (operand === undefined || operand === null || operand === '') {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tif (Array.isArray(operand)) {\n\t\t\t\t\tfor (const item of operand) {\n\t\t\t\t\t\tif (item !== undefined && item !== null && item !== '') {\n\t\t\t\t\t\t\tqueryParams.append(`${key}[${op}][]`, String(item))\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} else if (typeof operand === 'object') {\n\t\t\t\t\tqueryParams.set(`${key}[${op}]`, JSON.stringify(operand))\n\t\t\t\t} else {\n\t\t\t\t\tqueryParams.set(`${key}[${op}]`, String(operand))\n\t\t\t\t}\n\t\t\t}\n\t\t} else if (typeof value === 'object') {\n\t\t\tqueryParams.set(key, JSON.stringify(value))\n\t\t} else {\n\t\t\tqueryParams.set(key, String(value))\n\t\t}\n\t}\n\n\tconst str = queryParams.toString()\n\treturn str ? '?' + str : ''\n}\n"],"names":[],"mappings":";;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAAS,SAAS,CAAC,IAAI,EAAE;AAChC,CAAC,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,KAAK,EAAE,EAAE;AAC9C,EAAE,OAAO,IAAI;AACb,EAAE;AACF;AACA,CAAC,IAAI,sBAAsB,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE;AACjE,EAAE,OAAO,IAAI;AACb,EAAE;AACF;AACA,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE;AAC5B,EAAE,OAAO,IAAI;AACb,EAAE;AACF;AACA,CAAC,MAAM,MAAM,GAAG,WAAW,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,EAAC;AACvD,CAAC,MAAM,IAAI,GAAG,MAAM,CAAC,OAAO,CAAC,eAAe,EAAE,EAAE,EAAC;AACjD,CAAC,IAAI,IAAI,CAAC,UAAU,CAAC,YAAY,CAAC,KAAK,IAAI,KAAK,IAAI,KAAK,IAAI,IAAI,IAAI,CAAC,UAAU,CAAC,IAAI,GAAG,GAAG,CAAC,CAAC,CAAC,EAAE;AAChG,EAAE,OAAO,IAAI;AACb,EAAE;AACF,CAAC,IAAI,wCAAwC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE;AAC1D,EAAE,OAAO,IAAI,GAAG,IAAI;AACpB,EAAE;AACF,CAAC,OAAO,MAAM,GAAG,IAAI;AACrB,CAAC;AACD;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;AACO,SAAS,YAAY,CAAC,IAAI,GAAG,kBAAkB,EAAE;AACxD,CAAC,IAAI,WAAW,GAAG,mBAAkB;AACrC,CAAC,IAAI,gBAAgB,GAAG,KAAI;AAC5B,CAAC,IAAI,cAAc,GAAG,KAAI;AAC1B;AACA,CAAC,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE;AAC/B;AACA,EAAE,WAAW,GAAG,KAAI;AACpB,EAAE,MAAM,IAAI,IAAI,KAAK,IAAI,EAAE;AAC3B;AACA,EAAE,WAAW,GAAG,KAAI;AACpB,EAAE,MAAM,IAAI,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE;AAC9C;AACA,EAAE,IAAI,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,aAAa,CAAC,EAAE;AAC1C,GAAG,WAAW,GAAG,IAAI,CAAC,YAAW;AACjC,GAAG;AACH,EAAE,IAAI,OAAO,IAAI,CAAC,gBAAgB,KAAK,QAAQ,IAAI,IAAI,CAAC,gBAAgB,CAAC,MAAM,GAAG,CAAC,EAAE;AACrF,GAAG,gBAAgB,GAAG,IAAI,CAAC,iBAAgB;AAC3C,GAAG;AACH,EAAE,IAAI,OAAO,IAAI,CAAC,cAAc,KAAK,QAAQ,IAAI,IAAI,CAAC,cAAc,CAAC,MAAM,GAAG,CAAC,EAAE;AACjF,GAAG,cAAc,GAAG,IAAI,CAAC,eAAc;AACvC,GAAG;AACH,EAAE;AACF;AACA,CAAC,MAAM,OAAO,GAAG;AACjB,EAAE,YAAY,EAAE,OAAO,EAAE,KAAK,WAAW,GAAG,EAAE,CAAC,YAAY,GAAG,EAAE;AAChE,EAAE,gBAAgB,EAAE,MAAM;AAC1B,GAAE;AACF,CAAC,IAAI,WAAW,EAAE;AAClB,EAAE,OAAO,CAAC,cAAc,CAAC,GAAG,YAAW;AACvC,EAAE;AACF,CAAC,IAAI,gBAAgB,EAAE;AACvB,EAAE,OAAO,CAAC,6BAA6B,CAAC,GAAG,iBAAgB;AAC3D,EAAE;AACF,CAAC,IAAI,cAAc,EAAE;AACrB,EAAE,OAAO,CAAC,+BAA+B,CAAC,GAAG,eAAc;AAC3D,EAAE;AACF,CAAC,OAAO,OAAO;AACf,CAAC;AACD;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAAS,UAAU,CAAC,GAAG,EAAE;AAChC,CAAC,OAAO,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC;AAClD,CAAC;AACD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAAS,gBAAgB,CAAC,MAAM,GAAG,EAAE,EAAE;AAC9C,CAAC,MAAM,WAAW,GAAG,IAAI,eAAe,GAAE;AAC1C;AACA,CAAC,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE;AACpD,EAAE,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,EAAE,EAAE;AAC7D,GAAG,QAAQ;AACX,GAAG;AACH,EAAE,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE;AAClD,GAAG,QAAQ;AACX,GAAG;AACH,EAAE,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,MAAM,KAAK,CAAC,EAAE;AAC7F,GAAG,QAAQ;AACX,GAAG;AACH,EAAE,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE;AAC5B;AACA;AACA,GAAG,MAAM,QAAQ,GAAG,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,GAAG,GAAG,GAAG,CAAC,EAAE,GAAG,CAAC,EAAE,EAAC;AACzD,GAAG,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE;AAC7B,IAAI,IAAI,IAAI,KAAK,SAAS,IAAI,IAAI,KAAK,IAAI,IAAI,IAAI,KAAK,EAAE,EAAE;AAC5D,KAAK,WAAW,CAAC,MAAM,CAAC,QAAQ,EAAE,MAAM,CAAC,IAAI,CAAC,EAAC;AAC/C,KAAK;AACL,IAAI;AACJ,GAAG,MAAM,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE;AAChE;AACA;AACA;AACA;AACA,GAAG,KAAK,MAAM,CAAC,EAAE,EAAE,OAAO,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE;AACtD,IAAI,IAAI,OAAO,KAAK,SAAS,IAAI,OAAO,KAAK,IAAI,IAAI,OAAO,KAAK,EAAE,EAAE;AACrE,KAAK,QAAQ;AACb,KAAK;AACL,IAAI,IAAI,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE;AAChC,KAAK,KAAK,MAAM,IAAI,IAAI,OAAO,EAAE;AACjC,MAAM,IAAI,IAAI,KAAK,SAAS,IAAI,IAAI,KAAK,IAAI,IAAI,IAAI,KAAK,EAAE,EAAE;AAC9D,OAAO,WAAW,CAAC,MAAM,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,EAAC;AAC1D,OAAO;AACP,MAAM;AACN,KAAK,MAAM,IAAI,OAAO,OAAO,KAAK,QAAQ,EAAE;AAC5C,KAAK,WAAW,CAAC,GAAG,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,EAAC;AAC9D,KAAK,MAAM;AACX,KAAK,WAAW,CAAC,GAAG,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,OAAO,CAAC,EAAC;AACtD,KAAK;AACL,IAAI;AACJ,GAAG,MAAM,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE;AACxC,GAAG,WAAW,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,EAAC;AAC9C,GAAG,MAAM;AACT,GAAG,WAAW,CAAC,GAAG,CAAC,GAAG,EAAE,MAAM,CAAC,KAAK,CAAC,EAAC;AACtC,GAAG;AACH,EAAE;AACF;AACA,CAAC,MAAM,GAAG,GAAG,WAAW,CAAC,QAAQ,GAAE;AACnC,CAAC,OAAO,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,EAAE;AAC5B;;;;"}
|
|
@@ -94,10 +94,12 @@ const DASHBOARD_CATALOG_WIDGET_KEYS = Object.freeze([
|
|
|
94
94
|
'calendar',
|
|
95
95
|
'chart',
|
|
96
96
|
'container',
|
|
97
|
+
'conversation',
|
|
97
98
|
'countdown',
|
|
98
99
|
'data',
|
|
99
100
|
'delta',
|
|
100
101
|
'divider',
|
|
102
|
+
'document-review',
|
|
101
103
|
'files',
|
|
102
104
|
'flow-runs',
|
|
103
105
|
'gauge',
|
|
@@ -111,6 +113,7 @@ const DASHBOARD_CATALOG_WIDGET_KEYS = Object.freeze([
|
|
|
111
113
|
'map',
|
|
112
114
|
'menu',
|
|
113
115
|
'nc-widget',
|
|
116
|
+
'next-step',
|
|
114
117
|
// `news` was here until the widget moved to LaunchPad, which owns the feed
|
|
115
118
|
// fetching. An app that renders one now gets it from LaunchPad's registry.
|
|
116
119
|
'object-geo',
|
|
@@ -120,6 +123,7 @@ const DASHBOARD_CATALOG_WIDGET_KEYS = Object.freeze([
|
|
|
120
123
|
'quicklinks',
|
|
121
124
|
'related',
|
|
122
125
|
'spend-analytics',
|
|
126
|
+
'stacked-bar',
|
|
123
127
|
'stages',
|
|
124
128
|
'stat',
|
|
125
129
|
'stats-block',
|
|
@@ -129,6 +133,7 @@ const DASHBOARD_CATALOG_WIDGET_KEYS = Object.freeze([
|
|
|
129
133
|
'text',
|
|
130
134
|
'tile',
|
|
131
135
|
'video',
|
|
136
|
+
'week-strip',
|
|
132
137
|
'workspace-filter',
|
|
133
138
|
]);
|
|
134
139
|
|
|
@@ -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'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'countdown',\n\t'data',\n\t'delta',\n\t'divider',\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'menu',\n\t'nc-widget',\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'related',\n\t'spend-analytics',\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'video',\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,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,WAAW;AACZ,CAAC,MAAM;AACP,CAAC,OAAO;AACR,CAAC,SAAS;AACV,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,MAAM;AACP,CAAC,WAAW;AACZ;AACA;AACA,CAAC,YAAY;AACb,CAAC,aAAa;AACd,CAAC,cAAc;AACf,CAAC,QAAQ;AACT,CAAC,YAAY;AACb,CAAC,SAAS;AACV,CAAC,iBAAiB;AAClB,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,OAAO;AACR,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'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'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'menu',\n\t'nc-widget',\n\t'next-step',\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'related',\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'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":"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"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SPDX-FileCopyrightText: 2026 Conduction B.V.
|
|
3
|
+
* SPDX-License-Identifier: EUPL-1.2
|
|
4
|
+
*
|
|
5
|
+
* readPath: read a dot-path off an object.
|
|
6
|
+
*
|
|
7
|
+
* Its own module, with no imports, so the rules that read a record's fields
|
|
8
|
+
* (stage, checklist, due date) do not hang on a module a host or a test may
|
|
9
|
+
* replace.
|
|
10
|
+
*
|
|
11
|
+
* @module utils/readPath
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Read a dot-path (`'a.b.c'`) off an object.
|
|
16
|
+
*
|
|
17
|
+
* @param {object|null} data The source object.
|
|
18
|
+
* @param {string} [path] Dot-path into the object. Empty returns the object itself.
|
|
19
|
+
* @return {unknown} The value at the path, or undefined when a segment is missing.
|
|
20
|
+
* @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-primary-action-follows-the-stage
|
|
21
|
+
*/
|
|
22
|
+
function readPath(data, path) {
|
|
23
|
+
if (!path) {
|
|
24
|
+
return data
|
|
25
|
+
}
|
|
26
|
+
return String(path).split('.').reduce((value, key) => (value === null || value === undefined ? value : value[key]), data)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export { readPath };
|
|
30
|
+
//# sourceMappingURL=readPath.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"readPath.js","sources":["../../../src/utils/readPath.js"],"sourcesContent":["/**\n * SPDX-FileCopyrightText: 2026 Conduction B.V.\n * SPDX-License-Identifier: EUPL-1.2\n *\n * readPath: read a dot-path off an object.\n *\n * Its own module, with no imports, so the rules that read a record's fields\n * (stage, checklist, due date) do not hang on a module a host or a test may\n * replace.\n *\n * @module utils/readPath\n */\n\n/**\n * Read a dot-path (`'a.b.c'`) off an object.\n *\n * @param {object|null} data The source object.\n * @param {string} [path] Dot-path into the object. Empty returns the object itself.\n * @return {unknown} The value at the path, or undefined when a segment is missing.\n * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-primary-action-follows-the-stage\n */\nexport function readPath(data, path) {\n\tif (!path) {\n\t\treturn data\n\t}\n\treturn String(path).split('.').reduce((value, key) => (value === null || value === undefined ? value : value[key]), data)\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,QAAQ,CAAC,IAAI,EAAE,IAAI,EAAE;AACrC,CAAC,IAAI,CAAC,IAAI,EAAE;AACZ,EAAE,OAAO,IAAI;AACb,EAAE;AACF,CAAC,OAAO,MAAM,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,GAAG,MAAM,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,SAAS,GAAG,KAAK,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC;AAC1H;;;;"}
|