@conduction/nextcloud-vue 2.83.0 → 2.85.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/CnAdminSettingsShell/CnAdminSettingsShell.vue.js +1 -1
- package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue2.js +131 -3
- package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue2.js.map +1 -1
- package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue3.js +54 -18
- package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue3.js.map +1 -1
- package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue.js +1 -1
- package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue2.js +1 -1
- package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue2.js.map +1 -1
- package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue3.js.map +1 -1
- package/dist/esm/components/CnBoardView/CnBoardView.vue.js +1 -1
- package/dist/esm/components/CnBoardView/CnBoardView.vue2.js +232 -4
- package/dist/esm/components/CnBoardView/CnBoardView.vue2.js.map +1 -1
- package/dist/esm/components/CnBoardView/CnBoardView.vue3.js +95 -51
- package/dist/esm/components/CnBoardView/CnBoardView.vue3.js.map +1 -1
- package/dist/esm/components/CnBodySections/CnBodySections.vue.js +1 -1
- package/dist/esm/components/CnBodySections/CnBodySections.vue2.js +22 -1
- package/dist/esm/components/CnBodySections/CnBodySections.vue2.js.map +1 -1
- package/dist/esm/components/CnBodySections/CnBodySections.vue3.js.map +1 -1
- package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue.js +1 -1
- package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue2.js +47 -4
- package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue.js +1 -1
- package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue2.js +239 -3
- package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue2.js.map +1 -1
- package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue3.js +5 -5
- package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue3.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue.js +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +50 -0
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +22 -12
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
- package/dist/esm/components/CnDeltaWidget/CnDeltaWidget.vue.js +1 -1
- package/dist/esm/components/CnDeltaWidget/CnDeltaWidget.vue2.js +1 -1
- package/dist/esm/components/CnDeltaWidget/CnDeltaWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnDeltaWidget/CnDeltaWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +226 -8
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue.js +1 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js +3 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue.js +1 -1
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js +18 -5
- 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.vue.js +1 -1
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js +38 -1
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js +45 -5
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnGaugeWidget/CnGaugeWidget.vue.js +1 -1
- package/dist/esm/components/CnGaugeWidget/CnGaugeWidget.vue2.js +1 -1
- package/dist/esm/components/CnGaugeWidget/CnGaugeWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnGaugeWidget/CnGaugeWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +548 -77
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +191 -82
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/useSelfFetchList.js +11 -2
- package/dist/esm/components/CnIndexPage/useSelfFetchList.js.map +1 -1
- package/dist/esm/components/CnInteractionFormWidget/CnInteractionFormWidget.vue2.js +29 -6
- package/dist/esm/components/CnInteractionFormWidget/CnInteractionFormWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnInteractionFormWidget/CnInteractionFormWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue.js +1 -1
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js +126 -2
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue3.js +24 -2
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +106 -3
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue.js +1 -1
- package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue2.js +67 -5
- package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue2.js.map +1 -1
- package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue3.js +181 -124
- package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue3.js.map +1 -1
- package/dist/esm/components/CnReportsPage/CnReportsPage.vue.js +1 -1
- package/dist/esm/components/CnReportsPage/CnReportsPage.vue2.js +135 -2
- package/dist/esm/components/CnReportsPage/CnReportsPage.vue2.js.map +1 -1
- package/dist/esm/components/CnReportsPage/CnReportsPage.vue3.js +95 -43
- package/dist/esm/components/CnReportsPage/CnReportsPage.vue3.js.map +1 -1
- package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue.js +1 -1
- package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue2.js +5 -1
- package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue2.js.map +1 -1
- package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue3.js.map +1 -1
- package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue.js +1 -1
- package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue2.js +72 -5
- package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue3.js +33 -8
- package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue.js +1 -1
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js +135 -11
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js +6 -2
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue.js +1 -1
- package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue2.js +17 -2
- package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue3.js +1 -1
- package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue.js +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +39 -6
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue.js +1 -1
- package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue2.js +140 -7
- package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue.js +1 -1
- package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue2.js +37 -9
- package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue2.js.map +1 -1
- package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue3.js +6 -5
- package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue3.js.map +1 -1
- package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue.js +1 -1
- package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue2.js +16 -0
- package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue2.js.map +1 -1
- package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue3.js.map +1 -1
- package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue.js +1 -1
- package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue2.js +2 -1
- package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnWidgetCardGrid/CnWidgetCardGrid.vue2.js +53 -1
- package/dist/esm/components/CnWidgetCardGrid/CnWidgetCardGrid.vue2.js.map +1 -1
- package/dist/esm/components/CnWidgetCardGrid/CnWidgetCardGrid.vue3.js +3 -2
- package/dist/esm/components/CnWidgetCardGrid/CnWidgetCardGrid.vue3.js.map +1 -1
- package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue.js +1 -1
- package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js +135 -0
- 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/CnWidgetWrapper/CnWidgetWrapper.vue.js +1 -1
- package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue2.js +19 -0
- package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue2.js.map +1 -1
- package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue3.js +37 -19
- package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue3.js.map +1 -1
- package/dist/esm/composables/useListView.js +21 -0
- package/dist/esm/composables/useListView.js.map +1 -1
- package/dist/esm/dialogs/CnTransitionInputDialog.vue.js +1 -1
- package/dist/esm/dialogs/CnTransitionInputDialog.vue2.js +68 -6
- package/dist/esm/dialogs/CnTransitionInputDialog.vue2.js.map +1 -1
- package/dist/esm/dialogs/CnTransitionInputDialog.vue3.js +133 -105
- package/dist/esm/dialogs/CnTransitionInputDialog.vue3.js.map +1 -1
- package/dist/esm/index.js +1 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/l10n/en.json.js +38 -1
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +78 -1
- package/dist/esm/l10n/nl.json.js.map +1 -1
- package/dist/esm/library-style.js +1 -1
- package/dist/esm/nextcloud-vue.css +1030 -533
- package/dist/esm/store/useObjectStore.js +16 -2
- package/dist/esm/store/useObjectStore.js.map +1 -1
- package/dist/esm/utils/boardColumns.js +8 -1
- package/dist/esm/utils/boardColumns.js.map +1 -1
- package/dist/esm/utils/boardReferenceColumns.js +278 -0
- package/dist/esm/utils/boardReferenceColumns.js.map +1 -0
- package/dist/esm/utils/buildManifest.js +41 -4
- package/dist/esm/utils/buildManifest.js.map +1 -1
- package/dist/esm/utils/detailWidgetVisibility.js +149 -0
- package/dist/esm/utils/detailWidgetVisibility.js.map +1 -0
- package/dist/esm/utils/formatMetric.js +39 -3
- package/dist/esm/utils/formatMetric.js.map +1 -1
- package/dist/esm/utils/gridStack.js +40 -1
- package/dist/esm/utils/gridStack.js.map +1 -1
- package/dist/esm/utils/humaniseKey.js +41 -0
- package/dist/esm/utils/humaniseKey.js.map +1 -0
- package/dist/esm/utils/notificationRuleLabel.js +6 -23
- package/dist/esm/utils/notificationRuleLabel.js.map +1 -1
- package/dist/esm/utils/resolveManifestTokens.js +1 -0
- package/dist/esm/utils/resolveManifestTokens.js.map +1 -1
- package/dist/esm/utils/sentinelTokens.js +9 -0
- package/dist/esm/utils/sentinelTokens.js.map +1 -1
- package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
- package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
- package/dist/esm/utils/widgetDispatch.js +8 -2
- package/dist/esm/utils/widgetDispatch.js.map +1 -1
- package/dist/nextcloud-vue.cjs.js +17159 -13422
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +1030 -533
- package/l10n/en.json +38 -1
- package/l10n/nl.json +78 -1
- package/package.json +1 -1
- package/src/components/CnAdminSettingsShell/CnAdminSettingsShell.vue +160 -8
- package/src/components/CnAuditTrailCard/CnAuditTrailCard.vue +1 -1
- package/src/components/CnBoardView/CnBoardView.vue +262 -6
- package/src/components/CnBodySections/CnBodySections.vue +22 -1
- package/src/components/CnCountdownWidget/CnCountdownWidget.vue +47 -4
- package/src/components/CnDashboardGrid/CnDashboardGrid.vue +244 -8
- package/src/components/CnDashboardPage/CnDashboardPage.vue +62 -2
- package/src/components/CnDeltaWidget/CnDeltaWidget.vue +1 -1
- package/src/components/CnDetailPage/CnDetailPage.md +3 -1
- package/src/components/CnDetailPage/CnDetailPage.vue +226 -8
- package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +3 -1
- package/src/components/CnFactsWidget/CnFactsWidget.vue +18 -5
- package/src/components/CnFieldListWidget/CnFieldListWidget.vue +63 -1
- package/src/components/CnGaugeWidget/CnGaugeWidget.vue +1 -1
- package/src/components/CnIndexPage/CnIndexPage.vue +586 -25
- package/src/components/CnIndexPage/useSelfFetchList.js +10 -1
- package/src/components/CnInteractionFormWidget/CnInteractionFormWidget.vue +29 -6
- package/src/components/CnObjectDataWidget/CnObjectDataWidget.md +1 -1
- package/src/components/CnObjectDataWidget/CnObjectDataWidget.vue +154 -5
- package/src/components/CnRecordListWidget/CnRecordListWidget.vue +106 -3
- package/src/components/CnRegisterMapping/CnRegisterMapping.md +5 -0
- package/src/components/CnRegisterMapping/CnRegisterMapping.vue +231 -96
- package/src/components/CnReportsPage/CnReportsPage.vue +179 -1
- package/src/components/CnSettingsPage/CnSettingsPage.vue +5 -1
- package/src/components/CnStagesWidget/CnStagesWidget.vue +103 -6
- package/src/components/CnStatWidget/CnStatWidget.vue +137 -12
- package/src/components/CnStatsBlockWidget/CnStatsBlockWidget.vue +34 -3
- package/src/components/CnTabsWidget/CnTabsWidget.vue +39 -6
- package/src/components/CnToggleTableWidget/CnToggleTableWidget.vue +140 -7
- package/src/components/CnVersionInfoCard/CnVersionInfoCard.md +2 -1
- package/src/components/CnVersionInfoCard/CnVersionInfoCard.vue +42 -13
- package/src/components/CnWalkthrough/CnWalkthrough.vue +16 -0
- package/src/components/CnWeekStripWidget/CnWeekStripWidget.vue +2 -1
- package/src/components/CnWidgetCardGrid/CnWidgetCardGrid.vue +55 -2
- package/src/components/CnWidgetObjectTable/CnWidgetObjectTable.vue +135 -0
- package/src/components/CnWidgetWrapper/CnWidgetWrapper.md +10 -0
- package/src/components/CnWidgetWrapper/CnWidgetWrapper.vue +44 -1
- package/src/composables/useListView.js +21 -0
- package/src/css/board.css +65 -4
- package/src/css/dialog.css +4 -2
- package/src/css/form-field.css +14 -0
- package/src/css/form-widget.css +11 -0
- package/src/css/index-page.css +49 -0
- package/src/css/kpi-card.css +13 -0
- package/src/css/look-board-reports.css +58 -0
- package/src/css/look-board.css +108 -7
- package/src/dialogs/CnTransitionInputDialog.vue +87 -7
- package/src/index.js +2 -0
- package/src/schemas/app-manifest-v2.schema.json +137 -6
- package/src/store/useObjectStore.js +16 -2
- package/src/utils/boardColumns.js +8 -1
- package/src/utils/boardReferenceColumns.js +274 -0
- package/src/utils/buildManifest.js +40 -3
- package/src/utils/detailWidgetVisibility.js +145 -0
- package/src/utils/formatMetric.js +38 -2
- package/src/utils/gridStack.js +39 -0
- package/src/utils/humaniseKey.js +38 -0
- package/src/utils/notificationRuleLabel.js +5 -23
- package/src/utils/resolveManifestTokens.js +1 -0
- package/src/utils/sentinelTokens.js +9 -0
- package/src/utils/validateManifestV2.compiled.js +3 -3
- package/src/utils/widgetDispatch.js +8 -2
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { cnFetchJson } from './cnFetch.js';
|
|
2
|
+
import { isWorkspaceCondition, isLocallyDecidableVisibleWhen, evaluateVisibleWhenLocal } from './visibleWhen.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* SPDX-FileCopyrightText: 2026 Conduction B.V.
|
|
6
|
+
* SPDX-License-Identifier: EUPL-1.2
|
|
7
|
+
*
|
|
8
|
+
* detailWidgetVisibility: whether a widget on a detail page shows for the
|
|
9
|
+
* loaded record, through the shared `visibleWhen` grammar.
|
|
10
|
+
*
|
|
11
|
+
* A detail page can name records it reads beside the loaded one
|
|
12
|
+
* (`visibilityContext`, e.g. a case's case type). A condition reads such a
|
|
13
|
+
* record as `@context.<name>.<path>`, so a widget can be shown for some case types
|
|
14
|
+
* only while the configuration stays in the app manifest.
|
|
15
|
+
*
|
|
16
|
+
* @module utils/detailWidgetVisibility
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Read a reference value as an id: a plain string or number, or an object
|
|
22
|
+
* carrying `id`, `uuid` or `@self.id` (an extended reference).
|
|
23
|
+
*
|
|
24
|
+
* @param {unknown} value The value of the reference field.
|
|
25
|
+
* @return {string} The id, or '' when there is none.
|
|
26
|
+
*/
|
|
27
|
+
function referenceId(value) {
|
|
28
|
+
if (typeof value === 'string' || typeof value === 'number') {
|
|
29
|
+
return String(value)
|
|
30
|
+
}
|
|
31
|
+
if (value && typeof value === 'object') {
|
|
32
|
+
const id = value.id ?? value.uuid ?? (value['@self'] && value['@self'].id);
|
|
33
|
+
return id === undefined || id === null ? '' : String(id)
|
|
34
|
+
}
|
|
35
|
+
return ''
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The records a `visibilityContext` names for one loaded record.
|
|
40
|
+
*
|
|
41
|
+
* @param {object} context `{ <name>: { field, register, schema } }`.
|
|
42
|
+
* @param {object|null} record The loaded record.
|
|
43
|
+
* @return {Array<{key: string, register: string, schema: string, id: string}>} One entry per usable name; `id` is '' when the record holds no reference.
|
|
44
|
+
* @spec openspec/changes/detail-widgets-visible-when/specs/detail-widgets-visible-when/spec.md#requirement-a-detail-page-can-load-records-a-condition-names
|
|
45
|
+
*/
|
|
46
|
+
function visibilityContextRefs(context, record) {
|
|
47
|
+
if (!context || typeof context !== 'object' || !record || typeof record !== 'object') {
|
|
48
|
+
return []
|
|
49
|
+
}
|
|
50
|
+
return Object.entries(context)
|
|
51
|
+
.filter(([name, ref]) => name !== '' && ref && typeof ref === 'object'
|
|
52
|
+
&& typeof ref.field === 'string' && ref.field !== ''
|
|
53
|
+
&& typeof ref.register === 'string' && ref.register !== ''
|
|
54
|
+
&& typeof ref.schema === 'string' && ref.schema !== '')
|
|
55
|
+
.map(([name, ref]) => ({
|
|
56
|
+
key: name,
|
|
57
|
+
register: ref.register,
|
|
58
|
+
schema: ref.schema,
|
|
59
|
+
id: referenceId(ref.field.split('.').reduce((obj, part) => (obj === null || obj === undefined ? obj : obj[part]), record)),
|
|
60
|
+
}))
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Read the records `refs` names. A reference without an id, and a read that
|
|
65
|
+
* fails, resolve `null`: the record is absent, and a condition on it fails
|
|
66
|
+
* safe rather than throwing out of the page.
|
|
67
|
+
*
|
|
68
|
+
* @param {Array<{key: string, register: string, schema: string, id: string}>} refs From {@link visibilityContextRefs}.
|
|
69
|
+
* @param {Map<string, object|null>} [cache] Rows already read, by `register/schema/id`.
|
|
70
|
+
* @return {Promise<object>} `{ <name>: row | null }`.
|
|
71
|
+
* @spec openspec/changes/detail-widgets-visible-when/specs/detail-widgets-visible-when/spec.md#requirement-a-detail-page-can-load-records-a-condition-names
|
|
72
|
+
*/
|
|
73
|
+
async function fetchVisibilityRows(refs, cache) {
|
|
74
|
+
const rows = {};
|
|
75
|
+
await Promise.all((refs || []).map(async (ref) => {
|
|
76
|
+
if (!ref.id) {
|
|
77
|
+
rows[ref.key] = null;
|
|
78
|
+
return
|
|
79
|
+
}
|
|
80
|
+
const cacheKey = `${ref.register}/${ref.schema}/${ref.id}`;
|
|
81
|
+
if (cache && cache.has(cacheKey)) {
|
|
82
|
+
rows[ref.key] = cache.get(cacheKey);
|
|
83
|
+
return
|
|
84
|
+
}
|
|
85
|
+
let row;
|
|
86
|
+
try {
|
|
87
|
+
const body = await cnFetchJson(`/apps/openregister/api/objects/${encodeURIComponent(ref.register)}/${encodeURIComponent(ref.schema)}/${encodeURIComponent(ref.id)}`);
|
|
88
|
+
row = body && typeof body === 'object' ? body : null;
|
|
89
|
+
} catch {
|
|
90
|
+
row = null;
|
|
91
|
+
}
|
|
92
|
+
if (cache) {
|
|
93
|
+
cache.set(cacheKey, row);
|
|
94
|
+
}
|
|
95
|
+
rows[ref.key] = row;
|
|
96
|
+
}));
|
|
97
|
+
return rows
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* The data a widget condition reads: the record, with the named records
|
|
102
|
+
* beside it under `@context.<name>`. Null while the record has not loaded.
|
|
103
|
+
*
|
|
104
|
+
* @param {object|null} record The loaded record.
|
|
105
|
+
* @param {object} [rows] `{ <name>: row | null }`.
|
|
106
|
+
* @return {object|null} The condition data.
|
|
107
|
+
* @spec openspec/changes/detail-widgets-visible-when/specs/detail-widgets-visible-when/spec.md#requirement-a-detail-grid-widget-shows-only-while-its-visiblewhen-holds-for-the-record
|
|
108
|
+
*/
|
|
109
|
+
function widgetConditionData(record, rows) {
|
|
110
|
+
if (!record || typeof record !== 'object') {
|
|
111
|
+
return null
|
|
112
|
+
}
|
|
113
|
+
return { ...record, '@context': { ...(rows || {}) } }
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Whether a widget's own `visibleWhen` lets it show on a detail page.
|
|
118
|
+
*
|
|
119
|
+
* No condition: shown, as before. A workspace condition: shown, a detail page
|
|
120
|
+
* has no workspace. While the data is not there (`data` null, or `pending`):
|
|
121
|
+
* hidden, so the widget never shows and then vanishes. A local condition is
|
|
122
|
+
* decided here; an endpoint or source condition takes its answer from
|
|
123
|
+
* `answers` (by widget id) and is hidden until it has one.
|
|
124
|
+
*
|
|
125
|
+
* @param {object|null} widget The widget definition.
|
|
126
|
+
* @param {object|null} data From {@link widgetConditionData}.
|
|
127
|
+
* @param {{pending?: boolean, answers?: object}} [state] Whether named records are still on their way, and the remote answers.
|
|
128
|
+
* @return {boolean} Whether the widget shows.
|
|
129
|
+
* @spec openspec/changes/detail-widgets-visible-when/specs/detail-widgets-visible-when/spec.md#requirement-a-detail-grid-widget-shows-only-while-its-visiblewhen-holds-for-the-record
|
|
130
|
+
*/
|
|
131
|
+
function detailWidgetVisible(widget, data, state = {}) {
|
|
132
|
+
const cond = widget && typeof widget === 'object' ? widget.visibleWhen : null;
|
|
133
|
+
if (cond === null || cond === undefined) {
|
|
134
|
+
return true
|
|
135
|
+
}
|
|
136
|
+
if (typeof cond === 'object' && !Array.isArray(cond) && isWorkspaceCondition(cond)) {
|
|
137
|
+
return true
|
|
138
|
+
}
|
|
139
|
+
if (!data || state.pending === true) {
|
|
140
|
+
return false
|
|
141
|
+
}
|
|
142
|
+
if (isLocallyDecidableVisibleWhen(cond)) {
|
|
143
|
+
return evaluateVisibleWhenLocal(cond, data)
|
|
144
|
+
}
|
|
145
|
+
return Boolean(state.answers && state.answers[widget.id] === true)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export { detailWidgetVisible, fetchVisibilityRows, visibilityContextRefs, widgetConditionData };
|
|
149
|
+
//# sourceMappingURL=detailWidgetVisibility.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"detailWidgetVisibility.js","sources":["../../../src/utils/detailWidgetVisibility.js"],"sourcesContent":["/**\n * SPDX-FileCopyrightText: 2026 Conduction B.V.\n * SPDX-License-Identifier: EUPL-1.2\n *\n * detailWidgetVisibility: whether a widget on a detail page shows for the\n * loaded record, through the shared `visibleWhen` grammar.\n *\n * A detail page can name records it reads beside the loaded one\n * (`visibilityContext`, e.g. a case's case type). A condition reads such a\n * record as `@context.<name>.<path>`, so a widget can be shown for some case types\n * only while the configuration stays in the app manifest.\n *\n * @module utils/detailWidgetVisibility\n */\n\nimport { cnFetchJson } from './cnFetch.js'\nimport { evaluateVisibleWhenLocal, isLocallyDecidableVisibleWhen, isWorkspaceCondition } from './visibleWhen.js'\n\n/**\n * Read a reference value as an id: a plain string or number, or an object\n * carrying `id`, `uuid` or `@self.id` (an extended reference).\n *\n * @param {unknown} value The value of the reference field.\n * @return {string} The id, or '' when there is none.\n */\nfunction referenceId(value) {\n\tif (typeof value === 'string' || typeof value === 'number') {\n\t\treturn String(value)\n\t}\n\tif (value && typeof value === 'object') {\n\t\tconst id = value.id ?? value.uuid ?? (value['@self'] && value['@self'].id)\n\t\treturn id === undefined || id === null ? '' : String(id)\n\t}\n\treturn ''\n}\n\n/**\n * The records a `visibilityContext` names for one loaded record.\n *\n * @param {object} context `{ <name>: { field, register, schema } }`.\n * @param {object|null} record The loaded record.\n * @return {Array<{key: string, register: string, schema: string, id: string}>} One entry per usable name; `id` is '' when the record holds no reference.\n * @spec openspec/changes/detail-widgets-visible-when/specs/detail-widgets-visible-when/spec.md#requirement-a-detail-page-can-load-records-a-condition-names\n */\nexport function visibilityContextRefs(context, record) {\n\tif (!context || typeof context !== 'object' || !record || typeof record !== 'object') {\n\t\treturn []\n\t}\n\treturn Object.entries(context)\n\t\t.filter(([name, ref]) => name !== '' && ref && typeof ref === 'object'\n\t\t\t&& typeof ref.field === 'string' && ref.field !== ''\n\t\t\t&& typeof ref.register === 'string' && ref.register !== ''\n\t\t\t&& typeof ref.schema === 'string' && ref.schema !== '')\n\t\t.map(([name, ref]) => ({\n\t\t\tkey: name,\n\t\t\tregister: ref.register,\n\t\t\tschema: ref.schema,\n\t\t\tid: referenceId(ref.field.split('.').reduce((obj, part) => (obj === null || obj === undefined ? obj : obj[part]), record)),\n\t\t}))\n}\n\n/**\n * Read the records `refs` names. A reference without an id, and a read that\n * fails, resolve `null`: the record is absent, and a condition on it fails\n * safe rather than throwing out of the page.\n *\n * @param {Array<{key: string, register: string, schema: string, id: string}>} refs From {@link visibilityContextRefs}.\n * @param {Map<string, object|null>} [cache] Rows already read, by `register/schema/id`.\n * @return {Promise<object>} `{ <name>: row | null }`.\n * @spec openspec/changes/detail-widgets-visible-when/specs/detail-widgets-visible-when/spec.md#requirement-a-detail-page-can-load-records-a-condition-names\n */\nexport async function fetchVisibilityRows(refs, cache) {\n\tconst rows = {}\n\tawait Promise.all((refs || []).map(async (ref) => {\n\t\tif (!ref.id) {\n\t\t\trows[ref.key] = null\n\t\t\treturn\n\t\t}\n\t\tconst cacheKey = `${ref.register}/${ref.schema}/${ref.id}`\n\t\tif (cache && cache.has(cacheKey)) {\n\t\t\trows[ref.key] = cache.get(cacheKey)\n\t\t\treturn\n\t\t}\n\t\tlet row\n\t\ttry {\n\t\t\tconst body = await cnFetchJson(`/apps/openregister/api/objects/${encodeURIComponent(ref.register)}/${encodeURIComponent(ref.schema)}/${encodeURIComponent(ref.id)}`)\n\t\t\trow = body && typeof body === 'object' ? body : null\n\t\t} catch {\n\t\t\trow = null\n\t\t}\n\t\tif (cache) {\n\t\t\tcache.set(cacheKey, row)\n\t\t}\n\t\trows[ref.key] = row\n\t}))\n\treturn rows\n}\n\n/**\n * The data a widget condition reads: the record, with the named records\n * beside it under `@context.<name>`. Null while the record has not loaded.\n *\n * @param {object|null} record The loaded record.\n * @param {object} [rows] `{ <name>: row | null }`.\n * @return {object|null} The condition data.\n * @spec openspec/changes/detail-widgets-visible-when/specs/detail-widgets-visible-when/spec.md#requirement-a-detail-grid-widget-shows-only-while-its-visiblewhen-holds-for-the-record\n */\nexport function widgetConditionData(record, rows) {\n\tif (!record || typeof record !== 'object') {\n\t\treturn null\n\t}\n\treturn { ...record, '@context': { ...(rows || {}) } }\n}\n\n/**\n * Whether a widget's own `visibleWhen` lets it show on a detail page.\n *\n * No condition: shown, as before. A workspace condition: shown, a detail page\n * has no workspace. While the data is not there (`data` null, or `pending`):\n * hidden, so the widget never shows and then vanishes. A local condition is\n * decided here; an endpoint or source condition takes its answer from\n * `answers` (by widget id) and is hidden until it has one.\n *\n * @param {object|null} widget The widget definition.\n * @param {object|null} data From {@link widgetConditionData}.\n * @param {{pending?: boolean, answers?: object}} [state] Whether named records are still on their way, and the remote answers.\n * @return {boolean} Whether the widget shows.\n * @spec openspec/changes/detail-widgets-visible-when/specs/detail-widgets-visible-when/spec.md#requirement-a-detail-grid-widget-shows-only-while-its-visiblewhen-holds-for-the-record\n */\nexport function detailWidgetVisible(widget, data, state = {}) {\n\tconst cond = widget && typeof widget === 'object' ? widget.visibleWhen : null\n\tif (cond === null || cond === undefined) {\n\t\treturn true\n\t}\n\tif (typeof cond === 'object' && !Array.isArray(cond) && isWorkspaceCondition(cond)) {\n\t\treturn true\n\t}\n\tif (!data || state.pending === true) {\n\t\treturn false\n\t}\n\tif (isLocallyDecidableVisibleWhen(cond)) {\n\t\treturn evaluateVisibleWhenLocal(cond, data)\n\t}\n\treturn Boolean(state.answers && state.answers[widget.id] === true)\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -220,6 +220,35 @@ function metricLocale() {
|
|
|
220
220
|
}
|
|
221
221
|
}
|
|
222
222
|
|
|
223
|
+
/**
|
|
224
|
+
* A format spec with its literal `prefix` / `suffix` run through the host
|
|
225
|
+
* translate function. A `@`-token, an empty string, or a missing translate
|
|
226
|
+
* leaves the affix as written.
|
|
227
|
+
*
|
|
228
|
+
* @param {object} format The `content.format` spec.
|
|
229
|
+
* @param {((text: string) => string)|null} translate The host translate function.
|
|
230
|
+
* @return {object} A copy of the spec (the same object when nothing changes).
|
|
231
|
+
* @spec openspec/changes/screens-metric-affix-translate/specs/dashboard-page/spec.md#requirement-a-metric-prefix-and-suffix-are-translated
|
|
232
|
+
*/
|
|
233
|
+
function translateMetricAffixes(format, translate) {
|
|
234
|
+
const fmt = format || {};
|
|
235
|
+
if (typeof translate !== 'function') {
|
|
236
|
+
return fmt
|
|
237
|
+
}
|
|
238
|
+
let out = fmt;
|
|
239
|
+
for (const key of ['prefix', 'suffix']) {
|
|
240
|
+
const text = fmt[key];
|
|
241
|
+
if (typeof text === 'string' && text.trim() !== '' && text.charAt(0) !== '@') {
|
|
242
|
+
const translated = translate(text);
|
|
243
|
+
if (typeof translated === 'string' && translated !== text) {
|
|
244
|
+
out = out === fmt ? { ...fmt } : out;
|
|
245
|
+
out[key] = translated;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return out
|
|
250
|
+
}
|
|
251
|
+
|
|
223
252
|
/**
|
|
224
253
|
* Format a numeric value per a manifest `content.format` spec.
|
|
225
254
|
*
|
|
@@ -244,11 +273,18 @@ function metricLocale() {
|
|
|
244
273
|
* @param {unknown} value The raw value to format.
|
|
245
274
|
* @param {object} format The `content.format` spec (`{ style, currency, currencyField, decimals, prefix, suffix }`).
|
|
246
275
|
* @param {object} configCtx The page-level app-config map for `@config.<key>` resolution.
|
|
276
|
+
* A literal `prefix` / `suffix` (one that is not a `@config.` or `@object.`
|
|
277
|
+
* token) is run through `translate` when one is given, as every other
|
|
278
|
+
* manifest-authored string is, so an app's catalogue entry for " days"
|
|
279
|
+
* (" dagen") applies. A token is resolved, never translated.
|
|
280
|
+
*
|
|
247
281
|
* @param {object|null} [object] The detail page's object, for `currencyField` and `@object.<field>`.
|
|
282
|
+
* @param {((text: string) => string)|null} [translate] The host app's translate function (`cnTranslate`).
|
|
248
283
|
* @return {string} The formatted display string.
|
|
249
284
|
* @spec openspec/changes/stat-currency-from-object/specs/dashboard-page/spec.md#requirement-a-money-value-is-shown-in-the-currency-the-entry-names
|
|
285
|
+
* @spec openspec/changes/screens-metric-affix-translate/specs/dashboard-page/spec.md#requirement-a-metric-prefix-and-suffix-are-translated
|
|
250
286
|
*/
|
|
251
|
-
function formatMetricValue(value, format, configCtx, object = null) {
|
|
287
|
+
function formatMetricValue(value, format, configCtx, object = null, translate = null) {
|
|
252
288
|
if (value === null || value === undefined) {
|
|
253
289
|
return '—'
|
|
254
290
|
}
|
|
@@ -257,7 +293,7 @@ function formatMetricValue(value, format, configCtx, object = null) {
|
|
|
257
293
|
return String(value)
|
|
258
294
|
}
|
|
259
295
|
|
|
260
|
-
const fmt = resolveConfigFormat(format, configCtx, object);
|
|
296
|
+
const fmt = resolveConfigFormat(translateMetricAffixes(format, translate), configCtx, object);
|
|
261
297
|
const locale = metricLocale();
|
|
262
298
|
// `duration-hours` and `decimal` default to ONE fraction digit; the
|
|
263
299
|
// other styles keep the pre-existing default of 0.
|
|
@@ -293,5 +329,5 @@ function formatMetricValue(value, format, configCtx, object = null) {
|
|
|
293
329
|
return `${fmt.prefix || ''}${body}${fmt.suffix || ''}`
|
|
294
330
|
}
|
|
295
331
|
|
|
296
|
-
export { formatMetricValue, formatMoney, metricLocale, normalizeMetricFormat, resolveConfigFormat, resolveFormatCurrency, safeCurrencyCode, unwrapAppConfig };
|
|
332
|
+
export { formatMetricValue, formatMoney, metricLocale, normalizeMetricFormat, resolveConfigFormat, resolveFormatCurrency, safeCurrencyCode, translateMetricAffixes, unwrapAppConfig };
|
|
297
333
|
//# sourceMappingURL=formatMetric.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"formatMetric.js","sources":["../../../src/utils/formatMetric.js"],"sourcesContent":["/**\n * SPDX-FileCopyrightText: 2026 Conduction B.V.\n * SPDX-License-Identifier: EUPL-1.2\n */\n\nimport { getCanonicalLocale } from '@nextcloud/l10n'\nimport { boardDateLocale } from './boardDate.js'\nimport { resolveFilterValue } from './resolveFilterTokens.js'\n\n/**\n * The format styles a stat or stats-block entry may name.\n *\n * @type {Array<string>}\n */\nexport const METRIC_FORMAT_STYLES = ['number', 'currency', 'percent', 'duration-hours', 'decimal']\n\n/**\n * Unwrap a possibly-ref-wrapped `cnAppConfig` inject into a plain object.\n *\n * Dashboard / detail pages provide `cnAppConfig` either as a plain object or as\n * a Vue ref (`{ value: {...} }`, depending on how the page seeds it). Widgets\n * need the plain map to resolve `@config.<key>` tokens against it.\n *\n * @param {object} cnAppConfig The injected app-config value (object or ref).\n * @return {object} The plain config map (always an object; `{}` when absent).\n */\nexport function unwrapAppConfig(cnAppConfig) {\n\tconst c = cnAppConfig\n\tconst unwrapped = (c && typeof c === 'object' && 'value' in c) ? c.value : c\n\treturn (unwrapped && typeof unwrapped === 'object') ? unwrapped : {}\n}\n\n/**\n * Resolve `@config.<key>` tokens in a `content.format` spec's string fields.\n *\n * Each of `currency` / `prefix` / `suffix` may be a `@config.<key>` token (e.g.\n * `currency: '@config.currency'`), or an `@object.<field>` token read off the\n * detail page's object when `object` is given. Tokens are resolved against\n * `configCtx`; a token that stays unresolved (the config key is unset, or the\n * config map has not been injected yet during a route transition) is DROPPED\n * so the downstream literal default applies instead of a raw `@config.…`\n * string leaking into `Intl.NumberFormat`.\n *\n * @param {object} format The raw `content.format` spec.\n * @param {object} configCtx The page-level app-config map (from `unwrapAppConfig`).\n * @param {object|null} [object] The detail page's object, for `@object.<field>` tokens.\n * @return {object} A copy of `format` with its string tokens resolved/dropped.\n */\nexport function resolveConfigFormat(format, configCtx, object = null) {\n\tconst fmt = format || {}\n\tconst out = { ...fmt }\n\tconst ctx = {\n\t\tconfig: (configCtx && typeof configCtx === 'object') ? configCtx : {},\n\t\tobject: (object && typeof object === 'object') ? object : null,\n\t}\n\tfor (const key of ['currency', 'prefix', 'suffix']) {\n\t\tconst raw = fmt[key]\n\t\tif (typeof raw !== 'string' || raw.charAt(0) !== '@') {\n\t\t\tcontinue\n\t\t}\n\t\tconst resolved = resolveFilterValue(raw, ctx)\n\t\tout[key] = (typeof resolved === 'string' && resolved.charAt(0) === '@') ? undefined : resolved\n\t}\n\treturn out\n}\n\n/**\n * Coerce a currency value to a safe ISO-4217-shaped code.\n *\n * `Intl.NumberFormat({ style: 'currency', currency })` throws a `RangeError`\n * when `currency` is not a three-letter code — which is exactly what happens\n * when an unresolved `@config.currency` token (or any bad value) reaches it.\n * This guard is the single choke point that keeps a malformed currency from\n * ever throwing: anything that is not three ASCII letters falls back to `EUR`.\n *\n * @param {unknown} currency The (possibly unresolved / invalid) currency value.\n * @return {string} A safe upper-case three-letter code (falls back to `EUR`).\n */\nexport function safeCurrencyCode(currency) {\n\treturn (typeof currency === 'string' && /^[A-Za-z]{3}$/.test(currency))\n\t\t? currency.toUpperCase()\n\t\t: 'EUR'\n}\n\n/**\n * Whether a value is a three-letter currency code.\n *\n * @param {unknown} value The candidate.\n * @return {boolean}\n */\nfunction isCurrencyCode(value) {\n\treturn typeof value === 'string' && /^[A-Za-z]{3}$/.test(value)\n}\n\n/**\n * Read a dot-path off an object.\n *\n * @param {object} object The object.\n * @param {string} path The path, e.g. `currency` or `price.currency`.\n * @return {unknown} The value, or undefined.\n */\nfunction readPath(object, path) {\n\treturn String(path).split('.').reduce((acc, key) => (acc && typeof acc === 'object' ? acc[key] : undefined), object)\n}\n\n/**\n * The currency a money value is shown in. In order:\n *\n * 1. `currencyField`: the field of the page's object that holds the currency\n * (a detail page; e.g. a contract's own `currency`).\n * 2. `currency`: a literal code, or a token (`@config.currency`,\n * `@object.currency`).\n * 3. The app's reporting currency, `currency` in the page's app config.\n * 4. `EUR`.\n *\n * A step whose value is not a three-letter code is skipped, so an object\n * without a currency falls back to the reporting currency instead of\n * throwing in `Intl.NumberFormat`.\n *\n * @param {object} format The format spec (`{ currency?, currencyField? }`).\n * @param {object} configCtx The page-level app-config map.\n * @param {object|null} [object] The detail page's object.\n * @return {string} An upper-case three-letter code.\n * @spec openspec/changes/stat-currency-from-object/specs/dashboard-page/spec.md#requirement-a-money-value-is-shown-in-the-currency-the-entry-names\n */\nexport function resolveFormatCurrency(format, configCtx, object = null) {\n\tconst fmt = format || {}\n\tif (typeof fmt.currencyField === 'string' && fmt.currencyField !== '' && object && typeof object === 'object') {\n\t\tconst own = readPath(object, fmt.currencyField)\n\t\tif (isCurrencyCode(own)) {\n\t\t\treturn own.toUpperCase()\n\t\t}\n\t}\n\tconst declared = resolveConfigFormat({ currency: fmt.currency }, configCtx, object).currency\n\tif (isCurrencyCode(declared)) {\n\t\treturn declared.toUpperCase()\n\t}\n\tconst reporting = configCtx && typeof configCtx === 'object' ? configCtx.currency : undefined\n\treturn safeCurrencyCode(reporting)\n}\n\n/**\n * A money amount in a currency, written for the reader's language (\"€ 1.234,50\").\n * The currency is the literal `currency` or the code in the row's\n * `currencyField` (a dot path); when neither is a three-letter code it is the\n * app's reporting currency (`currency` in the app config), then EUR. Empty and non-numeric values answer ''.\n *\n * @param {unknown} value The amount.\n * @param {{currency?: string, currencyField?: string, decimals?: number}} [format] The format spec.\n * @param {object|null} [row] The row `currencyField` reads from.\n * @param {object|null} [configCtx] The app config map; its `currency` (the reporting currency) is used when neither resolves.\n * @return {string} The text.\n * @spec openspec/changes/screens-currency-format/specs/currency-format/spec.md#requirement-a-money-value-reads-in-its-currency\n */\nexport function formatMoney(value, format = {}, row = null, configCtx = null) {\n\tif (value === null || value === undefined || value === '' || typeof value === 'object') {\n\t\treturn ''\n\t}\n\tconst num = Number(value)\n\tif (!Number.isFinite(num)) {\n\t\treturn ''\n\t}\n\tconst fmt = format || {}\n\tconst decimals = Number.isFinite(fmt.decimals) ? fmt.decimals : 2\n\tconst options = {\n\t\tstyle: 'currency',\n\t\tcurrency: resolveFormatCurrency(fmt, configCtx, row),\n\t\tminimumFractionDigits: decimals,\n\t\tmaximumFractionDigits: decimals,\n\t}\n\ttry {\n\t\treturn new Intl.NumberFormat(boardDateLocale(), options).format(num)\n\t} catch {\n\t\treturn new Intl.NumberFormat(undefined, options).format(num)\n\t}\n}\n\n/**\n * The format spec of a stat or stats-block entry in one shape.\n *\n * `format` may be a style name (`\"currency\"`) or an object (`{ style,\n * currency, currencyField, decimals, prefix, suffix }`). A sibling `currency`\n * or `currencyField` on the entry itself fills the same keys, and either one\n * makes the style `currency` when none is named. Answers null when the entry\n * declares no format at all, so the caller keeps its own rendering.\n *\n * @param {object} entry The declaration (`{ format?, currency?, currencyField? }`).\n * @return {object|null} The format spec, or null.\n * @spec openspec/changes/stat-currency-from-object/specs/dashboard-page/spec.md#requirement-a-stats-block-entry-formats-its-value\n */\nexport function normalizeMetricFormat(entry) {\n\tconst source = entry && typeof entry === 'object' ? entry : {}\n\tlet fmt = null\n\tif (typeof source.format === 'string' && source.format !== '') {\n\t\tfmt = { style: source.format }\n\t} else if (source.format && typeof source.format === 'object' && !Array.isArray(source.format)) {\n\t\tfmt = { ...source.format }\n\t}\n\tfor (const key of ['currency', 'currencyField']) {\n\t\tif (typeof source[key] === 'string' && source[key] !== '') {\n\t\t\tfmt = fmt || {}\n\t\t\tif (fmt[key] === undefined) {\n\t\t\t\tfmt[key] = source[key]\n\t\t\t}\n\t\t}\n\t}\n\tif (fmt && !fmt.style && (fmt.currency || fmt.currencyField)) {\n\t\tfmt.style = 'currency'\n\t}\n\treturn fmt\n}\n\n/**\n * The user's locale for number formatting: the Nextcloud locale setting,\n * else the browser's.\n *\n * @return {string|undefined} A BCP 47 tag, or undefined for the runtime default.\n */\nexport function metricLocale() {\n\ttry {\n\t\tconst tag = getCanonicalLocale()\n\t\t// A tag Intl does not accept would throw a RangeError on every value.\n\t\treturn tag && Intl.NumberFormat.supportedLocalesOf([tag]).length > 0 ? tag : undefined\n\t} catch {\n\t\treturn undefined\n\t}\n}\n\n/**\n * Format a numeric value per a manifest `content.format` spec.\n *\n * Handles the `number` / `currency` / `percent` / `duration-hours` /\n * `decimal` styles, resolves `@config.<key>` tokens in `currency` / `prefix`\n * / `suffix` against `configCtx`, and guards the currency code so an\n * unresolved or invalid currency can never throw a `RangeError`. Returns\n * `'—'` for null/undefined and the raw string for non-numeric input,\n * matching the KPI widgets' prior behaviour.\n *\n * Style notes:\n * - `duration-hours` renders an hour count with an `h` suffix and ONE\n * fraction digit by default (`42.5h` — the pipelinq resolution-time KPI\n * contract); `decimals` overrides the fraction digits.\n * - `decimal` is a plain number with ONE fraction digit by default (the\n * fleet KPIs' `toFixed(1)` convention), where `number` defaults to 0.\n *\n * The currency follows `resolveFormatCurrency`: the object's own currency\n * (`currencyField`), then `currency`, then the app's reporting currency, then\n * EUR. Numbers are formatted in the user's locale (`metricLocale`).\n *\n * @param {unknown} value The raw value to format.\n * @param {object} format The `content.format` spec (`{ style, currency, currencyField, decimals, prefix, suffix }`).\n * @param {object} configCtx The page-level app-config map for `@config.<key>` resolution.\n * @param {object|null} [object] The detail page's object, for `currencyField` and `@object.<field>`.\n * @return {string} The formatted display string.\n * @spec openspec/changes/stat-currency-from-object/specs/dashboard-page/spec.md#requirement-a-money-value-is-shown-in-the-currency-the-entry-names\n */\nexport function formatMetricValue(value, format, configCtx, object = null) {\n\tif (value === null || value === undefined) {\n\t\treturn '—'\n\t}\n\tconst num = Number(value)\n\tif (!Number.isFinite(num)) {\n\t\treturn String(value)\n\t}\n\n\tconst fmt = resolveConfigFormat(format, configCtx, object)\n\tconst locale = metricLocale()\n\t// `duration-hours` and `decimal` default to ONE fraction digit; the\n\t// other styles keep the pre-existing default of 0.\n\tconst oneDigitDefault = fmt.style === 'duration-hours' || fmt.style === 'decimal'\n\tconst decimals = Number.isFinite(fmt.decimals) ? fmt.decimals : (oneDigitDefault ? 1 : 0)\n\n\tlet body\n\tif (fmt.style === 'currency') {\n\t\tbody = new Intl.NumberFormat(locale, {\n\t\t\tstyle: 'currency',\n\t\t\tcurrency: resolveFormatCurrency(format, configCtx, object),\n\t\t\tminimumFractionDigits: decimals,\n\t\t\tmaximumFractionDigits: decimals,\n\t\t}).format(num)\n\t} else if (fmt.style === 'percent') {\n\t\t// Values are stored as the literal percent (83.3), not a 0–1 ratio.\n\t\tbody = new Intl.NumberFormat(locale, {\n\t\t\tminimumFractionDigits: decimals,\n\t\t\tmaximumFractionDigits: decimals,\n\t\t}).format(num) + '%'\n\t} else if (fmt.style === 'duration-hours') {\n\t\t// An hour count (e.g. mean resolution time): `42.5h`.\n\t\tbody = new Intl.NumberFormat(locale, {\n\t\t\tminimumFractionDigits: decimals,\n\t\t\tmaximumFractionDigits: decimals,\n\t\t}).format(num) + 'h'\n\t} else {\n\t\tbody = new Intl.NumberFormat(locale, {\n\t\t\tminimumFractionDigits: decimals,\n\t\t\tmaximumFractionDigits: decimals,\n\t\t}).format(num)\n\t}\n\treturn `${fmt.prefix || ''}${body}${fmt.suffix || ''}`\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"formatMetric.js","sources":["../../../src/utils/formatMetric.js"],"sourcesContent":["/**\n * SPDX-FileCopyrightText: 2026 Conduction B.V.\n * SPDX-License-Identifier: EUPL-1.2\n */\n\nimport { getCanonicalLocale } from '@nextcloud/l10n'\nimport { boardDateLocale } from './boardDate.js'\nimport { resolveFilterValue } from './resolveFilterTokens.js'\n\n/**\n * The format styles a stat or stats-block entry may name.\n *\n * @type {Array<string>}\n */\nexport const METRIC_FORMAT_STYLES = ['number', 'currency', 'percent', 'duration-hours', 'decimal']\n\n/**\n * Unwrap a possibly-ref-wrapped `cnAppConfig` inject into a plain object.\n *\n * Dashboard / detail pages provide `cnAppConfig` either as a plain object or as\n * a Vue ref (`{ value: {...} }`, depending on how the page seeds it). Widgets\n * need the plain map to resolve `@config.<key>` tokens against it.\n *\n * @param {object} cnAppConfig The injected app-config value (object or ref).\n * @return {object} The plain config map (always an object; `{}` when absent).\n */\nexport function unwrapAppConfig(cnAppConfig) {\n\tconst c = cnAppConfig\n\tconst unwrapped = (c && typeof c === 'object' && 'value' in c) ? c.value : c\n\treturn (unwrapped && typeof unwrapped === 'object') ? unwrapped : {}\n}\n\n/**\n * Resolve `@config.<key>` tokens in a `content.format` spec's string fields.\n *\n * Each of `currency` / `prefix` / `suffix` may be a `@config.<key>` token (e.g.\n * `currency: '@config.currency'`), or an `@object.<field>` token read off the\n * detail page's object when `object` is given. Tokens are resolved against\n * `configCtx`; a token that stays unresolved (the config key is unset, or the\n * config map has not been injected yet during a route transition) is DROPPED\n * so the downstream literal default applies instead of a raw `@config.…`\n * string leaking into `Intl.NumberFormat`.\n *\n * @param {object} format The raw `content.format` spec.\n * @param {object} configCtx The page-level app-config map (from `unwrapAppConfig`).\n * @param {object|null} [object] The detail page's object, for `@object.<field>` tokens.\n * @return {object} A copy of `format` with its string tokens resolved/dropped.\n */\nexport function resolveConfigFormat(format, configCtx, object = null) {\n\tconst fmt = format || {}\n\tconst out = { ...fmt }\n\tconst ctx = {\n\t\tconfig: (configCtx && typeof configCtx === 'object') ? configCtx : {},\n\t\tobject: (object && typeof object === 'object') ? object : null,\n\t}\n\tfor (const key of ['currency', 'prefix', 'suffix']) {\n\t\tconst raw = fmt[key]\n\t\tif (typeof raw !== 'string' || raw.charAt(0) !== '@') {\n\t\t\tcontinue\n\t\t}\n\t\tconst resolved = resolveFilterValue(raw, ctx)\n\t\tout[key] = (typeof resolved === 'string' && resolved.charAt(0) === '@') ? undefined : resolved\n\t}\n\treturn out\n}\n\n/**\n * Coerce a currency value to a safe ISO-4217-shaped code.\n *\n * `Intl.NumberFormat({ style: 'currency', currency })` throws a `RangeError`\n * when `currency` is not a three-letter code — which is exactly what happens\n * when an unresolved `@config.currency` token (or any bad value) reaches it.\n * This guard is the single choke point that keeps a malformed currency from\n * ever throwing: anything that is not three ASCII letters falls back to `EUR`.\n *\n * @param {unknown} currency The (possibly unresolved / invalid) currency value.\n * @return {string} A safe upper-case three-letter code (falls back to `EUR`).\n */\nexport function safeCurrencyCode(currency) {\n\treturn (typeof currency === 'string' && /^[A-Za-z]{3}$/.test(currency))\n\t\t? currency.toUpperCase()\n\t\t: 'EUR'\n}\n\n/**\n * Whether a value is a three-letter currency code.\n *\n * @param {unknown} value The candidate.\n * @return {boolean}\n */\nfunction isCurrencyCode(value) {\n\treturn typeof value === 'string' && /^[A-Za-z]{3}$/.test(value)\n}\n\n/**\n * Read a dot-path off an object.\n *\n * @param {object} object The object.\n * @param {string} path The path, e.g. `currency` or `price.currency`.\n * @return {unknown} The value, or undefined.\n */\nfunction readPath(object, path) {\n\treturn String(path).split('.').reduce((acc, key) => (acc && typeof acc === 'object' ? acc[key] : undefined), object)\n}\n\n/**\n * The currency a money value is shown in. In order:\n *\n * 1. `currencyField`: the field of the page's object that holds the currency\n * (a detail page; e.g. a contract's own `currency`).\n * 2. `currency`: a literal code, or a token (`@config.currency`,\n * `@object.currency`).\n * 3. The app's reporting currency, `currency` in the page's app config.\n * 4. `EUR`.\n *\n * A step whose value is not a three-letter code is skipped, so an object\n * without a currency falls back to the reporting currency instead of\n * throwing in `Intl.NumberFormat`.\n *\n * @param {object} format The format spec (`{ currency?, currencyField? }`).\n * @param {object} configCtx The page-level app-config map.\n * @param {object|null} [object] The detail page's object.\n * @return {string} An upper-case three-letter code.\n * @spec openspec/changes/stat-currency-from-object/specs/dashboard-page/spec.md#requirement-a-money-value-is-shown-in-the-currency-the-entry-names\n */\nexport function resolveFormatCurrency(format, configCtx, object = null) {\n\tconst fmt = format || {}\n\tif (typeof fmt.currencyField === 'string' && fmt.currencyField !== '' && object && typeof object === 'object') {\n\t\tconst own = readPath(object, fmt.currencyField)\n\t\tif (isCurrencyCode(own)) {\n\t\t\treturn own.toUpperCase()\n\t\t}\n\t}\n\tconst declared = resolveConfigFormat({ currency: fmt.currency }, configCtx, object).currency\n\tif (isCurrencyCode(declared)) {\n\t\treturn declared.toUpperCase()\n\t}\n\tconst reporting = configCtx && typeof configCtx === 'object' ? configCtx.currency : undefined\n\treturn safeCurrencyCode(reporting)\n}\n\n/**\n * A money amount in a currency, written for the reader's language (\"€ 1.234,50\").\n * The currency is the literal `currency` or the code in the row's\n * `currencyField` (a dot path); when neither is a three-letter code it is the\n * app's reporting currency (`currency` in the app config), then EUR. Empty and non-numeric values answer ''.\n *\n * @param {unknown} value The amount.\n * @param {{currency?: string, currencyField?: string, decimals?: number}} [format] The format spec.\n * @param {object|null} [row] The row `currencyField` reads from.\n * @param {object|null} [configCtx] The app config map; its `currency` (the reporting currency) is used when neither resolves.\n * @return {string} The text.\n * @spec openspec/changes/screens-currency-format/specs/currency-format/spec.md#requirement-a-money-value-reads-in-its-currency\n */\nexport function formatMoney(value, format = {}, row = null, configCtx = null) {\n\tif (value === null || value === undefined || value === '' || typeof value === 'object') {\n\t\treturn ''\n\t}\n\tconst num = Number(value)\n\tif (!Number.isFinite(num)) {\n\t\treturn ''\n\t}\n\tconst fmt = format || {}\n\tconst decimals = Number.isFinite(fmt.decimals) ? fmt.decimals : 2\n\tconst options = {\n\t\tstyle: 'currency',\n\t\tcurrency: resolveFormatCurrency(fmt, configCtx, row),\n\t\tminimumFractionDigits: decimals,\n\t\tmaximumFractionDigits: decimals,\n\t}\n\ttry {\n\t\treturn new Intl.NumberFormat(boardDateLocale(), options).format(num)\n\t} catch {\n\t\treturn new Intl.NumberFormat(undefined, options).format(num)\n\t}\n}\n\n/**\n * The format spec of a stat or stats-block entry in one shape.\n *\n * `format` may be a style name (`\"currency\"`) or an object (`{ style,\n * currency, currencyField, decimals, prefix, suffix }`). A sibling `currency`\n * or `currencyField` on the entry itself fills the same keys, and either one\n * makes the style `currency` when none is named. Answers null when the entry\n * declares no format at all, so the caller keeps its own rendering.\n *\n * @param {object} entry The declaration (`{ format?, currency?, currencyField? }`).\n * @return {object|null} The format spec, or null.\n * @spec openspec/changes/stat-currency-from-object/specs/dashboard-page/spec.md#requirement-a-stats-block-entry-formats-its-value\n */\nexport function normalizeMetricFormat(entry) {\n\tconst source = entry && typeof entry === 'object' ? entry : {}\n\tlet fmt = null\n\tif (typeof source.format === 'string' && source.format !== '') {\n\t\tfmt = { style: source.format }\n\t} else if (source.format && typeof source.format === 'object' && !Array.isArray(source.format)) {\n\t\tfmt = { ...source.format }\n\t}\n\tfor (const key of ['currency', 'currencyField']) {\n\t\tif (typeof source[key] === 'string' && source[key] !== '') {\n\t\t\tfmt = fmt || {}\n\t\t\tif (fmt[key] === undefined) {\n\t\t\t\tfmt[key] = source[key]\n\t\t\t}\n\t\t}\n\t}\n\tif (fmt && !fmt.style && (fmt.currency || fmt.currencyField)) {\n\t\tfmt.style = 'currency'\n\t}\n\treturn fmt\n}\n\n/**\n * The user's locale for number formatting: the Nextcloud locale setting,\n * else the browser's.\n *\n * @return {string|undefined} A BCP 47 tag, or undefined for the runtime default.\n */\nexport function metricLocale() {\n\ttry {\n\t\tconst tag = getCanonicalLocale()\n\t\t// A tag Intl does not accept would throw a RangeError on every value.\n\t\treturn tag && Intl.NumberFormat.supportedLocalesOf([tag]).length > 0 ? tag : undefined\n\t} catch {\n\t\treturn undefined\n\t}\n}\n\n/**\n * A format spec with its literal `prefix` / `suffix` run through the host\n * translate function. A `@`-token, an empty string, or a missing translate\n * leaves the affix as written.\n *\n * @param {object} format The `content.format` spec.\n * @param {((text: string) => string)|null} translate The host translate function.\n * @return {object} A copy of the spec (the same object when nothing changes).\n * @spec openspec/changes/screens-metric-affix-translate/specs/dashboard-page/spec.md#requirement-a-metric-prefix-and-suffix-are-translated\n */\nexport function translateMetricAffixes(format, translate) {\n\tconst fmt = format || {}\n\tif (typeof translate !== 'function') {\n\t\treturn fmt\n\t}\n\tlet out = fmt\n\tfor (const key of ['prefix', 'suffix']) {\n\t\tconst text = fmt[key]\n\t\tif (typeof text === 'string' && text.trim() !== '' && text.charAt(0) !== '@') {\n\t\t\tconst translated = translate(text)\n\t\t\tif (typeof translated === 'string' && translated !== text) {\n\t\t\t\tout = out === fmt ? { ...fmt } : out\n\t\t\t\tout[key] = translated\n\t\t\t}\n\t\t}\n\t}\n\treturn out\n}\n\n/**\n * Format a numeric value per a manifest `content.format` spec.\n *\n * Handles the `number` / `currency` / `percent` / `duration-hours` /\n * `decimal` styles, resolves `@config.<key>` tokens in `currency` / `prefix`\n * / `suffix` against `configCtx`, and guards the currency code so an\n * unresolved or invalid currency can never throw a `RangeError`. Returns\n * `'—'` for null/undefined and the raw string for non-numeric input,\n * matching the KPI widgets' prior behaviour.\n *\n * Style notes:\n * - `duration-hours` renders an hour count with an `h` suffix and ONE\n * fraction digit by default (`42.5h` — the pipelinq resolution-time KPI\n * contract); `decimals` overrides the fraction digits.\n * - `decimal` is a plain number with ONE fraction digit by default (the\n * fleet KPIs' `toFixed(1)` convention), where `number` defaults to 0.\n *\n * The currency follows `resolveFormatCurrency`: the object's own currency\n * (`currencyField`), then `currency`, then the app's reporting currency, then\n * EUR. Numbers are formatted in the user's locale (`metricLocale`).\n *\n * @param {unknown} value The raw value to format.\n * @param {object} format The `content.format` spec (`{ style, currency, currencyField, decimals, prefix, suffix }`).\n * @param {object} configCtx The page-level app-config map for `@config.<key>` resolution.\n * A literal `prefix` / `suffix` (one that is not a `@config.` or `@object.`\n * token) is run through `translate` when one is given, as every other\n * manifest-authored string is, so an app's catalogue entry for \" days\"\n * (\" dagen\") applies. A token is resolved, never translated.\n *\n * @param {object|null} [object] The detail page's object, for `currencyField` and `@object.<field>`.\n * @param {((text: string) => string)|null} [translate] The host app's translate function (`cnTranslate`).\n * @return {string} The formatted display string.\n * @spec openspec/changes/stat-currency-from-object/specs/dashboard-page/spec.md#requirement-a-money-value-is-shown-in-the-currency-the-entry-names\n * @spec openspec/changes/screens-metric-affix-translate/specs/dashboard-page/spec.md#requirement-a-metric-prefix-and-suffix-are-translated\n */\nexport function formatMetricValue(value, format, configCtx, object = null, translate = null) {\n\tif (value === null || value === undefined) {\n\t\treturn '—'\n\t}\n\tconst num = Number(value)\n\tif (!Number.isFinite(num)) {\n\t\treturn String(value)\n\t}\n\n\tconst fmt = resolveConfigFormat(translateMetricAffixes(format, translate), configCtx, object)\n\tconst locale = metricLocale()\n\t// `duration-hours` and `decimal` default to ONE fraction digit; the\n\t// other styles keep the pre-existing default of 0.\n\tconst oneDigitDefault = fmt.style === 'duration-hours' || fmt.style === 'decimal'\n\tconst decimals = Number.isFinite(fmt.decimals) ? fmt.decimals : (oneDigitDefault ? 1 : 0)\n\n\tlet body\n\tif (fmt.style === 'currency') {\n\t\tbody = new Intl.NumberFormat(locale, {\n\t\t\tstyle: 'currency',\n\t\t\tcurrency: resolveFormatCurrency(format, configCtx, object),\n\t\t\tminimumFractionDigits: decimals,\n\t\t\tmaximumFractionDigits: decimals,\n\t\t}).format(num)\n\t} else if (fmt.style === 'percent') {\n\t\t// Values are stored as the literal percent (83.3), not a 0–1 ratio.\n\t\tbody = new Intl.NumberFormat(locale, {\n\t\t\tminimumFractionDigits: decimals,\n\t\t\tmaximumFractionDigits: decimals,\n\t\t}).format(num) + '%'\n\t} else if (fmt.style === 'duration-hours') {\n\t\t// An hour count (e.g. mean resolution time): `42.5h`.\n\t\tbody = new Intl.NumberFormat(locale, {\n\t\t\tminimumFractionDigits: decimals,\n\t\t\tmaximumFractionDigits: decimals,\n\t\t}).format(num) + 'h'\n\t} else {\n\t\tbody = new Intl.NumberFormat(locale, {\n\t\t\tminimumFractionDigits: decimals,\n\t\t\tmaximumFractionDigits: decimals,\n\t\t}).format(num)\n\t}\n\treturn `${fmt.prefix || ''}${body}${fmt.suffix || ''}`\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -55,5 +55,44 @@ function readGridGeometry(items) {
|
|
|
55
55
|
return map
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
|
|
58
|
+
/**
|
|
59
|
+
* Where the items under a growing cell go so they keep their order: every item
|
|
60
|
+
* the growth would overlap moves down by the growth, and so does every item
|
|
61
|
+
* that move would overlap in turn. Items beside the cell stay put.
|
|
62
|
+
*
|
|
63
|
+
* GridStack resolves the same collision itself, but not in order: when
|
|
64
|
+
* pipelinq's size-to-content banner grew from two rows to three, the
|
|
65
|
+
* full-width card under the stat tiles landed below the lists.
|
|
66
|
+
*
|
|
67
|
+
* @spec openspec/changes/dashboard-growth-keeps-order/specs/dashboard-grid/spec.md#requirement-a-growing-cell-keeps-the-order-of-the-cards-under-it
|
|
68
|
+
* @param {Array<{x: number, y: number, w: number, h: number}>} nodes The grid's items (the growing one among them).
|
|
69
|
+
* @param {{x: number, y: number, w: number, h: number}} node The growing item.
|
|
70
|
+
* @param {number} h Its new height, in grid rows.
|
|
71
|
+
* @return {Array<{node: object, y: number}>} The items to move and their new row, bottom-most first.
|
|
72
|
+
*/
|
|
73
|
+
function planGrowth(nodes, node, h) {
|
|
74
|
+
const delta = h - node.h;
|
|
75
|
+
if (!(delta > 0)) {
|
|
76
|
+
return []
|
|
77
|
+
}
|
|
78
|
+
const moved = new Map();
|
|
79
|
+
const overlaps = (a, b) => a.x < b.x + b.w && b.x < a.x + a.w && a.y < b.y + b.h && b.y < a.y + a.h;
|
|
80
|
+
const queue = [{ x: node.x, y: node.y, w: node.w, h }];
|
|
81
|
+
while (queue.length > 0) {
|
|
82
|
+
const rect = queue.shift();
|
|
83
|
+
for (const other of nodes) {
|
|
84
|
+
if (other === node || moved.has(other) || !overlaps(rect, other)) {
|
|
85
|
+
continue
|
|
86
|
+
}
|
|
87
|
+
const next = { x: other.x, y: other.y + delta, w: other.w, h: other.h };
|
|
88
|
+
moved.set(other, next.y);
|
|
89
|
+
queue.push(next);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return [...moved.entries()]
|
|
93
|
+
.map(([item, y]) => ({ node: item, y }))
|
|
94
|
+
.sort((a, b) => b.y - a.y)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export { initGridStack, planGrowth, readGridGeometry };
|
|
59
98
|
//# sourceMappingURL=gridStack.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gridStack.js","sources":["../../../src/utils/gridStack.js"],"sourcesContent":["/**\n * Thin GridStack bootstrap shared by the drag/resize grids (CnDashboardGrid and\n * CnWidgetGrid's editable body slot). Keeps the GridStack option shape in one\n * place so the two engines stay consistent.\n *\n * @module utils/gridStack\n */\n\nimport { GridStack } from 'gridstack'\n// `gridstack` is a peerDependency, not bundled — see the matching comment in\n// CnDashboardGrid.vue. The consumer must install `gridstack` themselves and\n// import its stylesheet (`gridstack/dist/gridstack.min.css`) from that same\n// copy; nc-vue ships no GridStack CSS of its own so the JS driving the grid\n// and the CSS sizing it can never come from two different versions.\n\n/**\n * Initialise a GridStack instance on a `.grid-stack` element.\n *\n * @param {HTMLElement} el The `.grid-stack` container element.\n * @param {object} opts Options.\n * @param {number} opts.columns Column count.\n * @param {boolean} opts.editable Whether drag + resize are enabled.\n * @param {number} [opts.cellHeight] Cell height in px (default 80).\n * @param {number} [opts.margin] Grid margin in px (default 12).\n * @return {import('gridstack').GridStack} The GridStack instance.\n */\nexport function initGridStack(el, opts) {\n\tconst { columns, editable, cellHeight = 80, margin = 12 } = opts\n\treturn GridStack.init({\n\t\tcolumn: columns,\n\t\tcellHeight,\n\t\tmargin,\n\t\tfloat: true,\n\t\tanimate: true,\n\t\tdisableDrag: !editable,\n\t\tdisableResize: !editable,\n\t\tremovable: false,\n\t}, el)\n}\n\n/**\n * Read the geometry GridStack reports for a changed set of items into a plain\n * map keyed by node id.\n *\n * @param {Array<{id: (string|number), x: number, y: number, w: number, h: number}>} items\n * The items GridStack passes to its `change` handler.\n * @return {Map<string, {gridX: number, gridY: number, gridWidth: number, gridHeight: number}>}\n */\nexport function readGridGeometry(items) {\n\tconst map = new Map()\n\tfor (const it of items || []) {\n\t\tmap.set(String(it.id), { gridX: it.x, gridY: it.y, gridWidth: it.w, gridHeight: it.h })\n\t}\n\treturn map\n}\n"],"names":[],"mappings":";;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAAS,aAAa,CAAC,EAAE,EAAE,IAAI,EAAE;AACxC,CAAC,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,UAAU,GAAG,EAAE,EAAE,MAAM,GAAG,EAAE,EAAE,GAAG,KAAI;AACjE,CAAC,OAAO,SAAS,CAAC,IAAI,CAAC;AACvB,EAAE,MAAM,EAAE,OAAO;AACjB,EAAE,UAAU;AACZ,EAAE,MAAM;AACR,EAAE,KAAK,EAAE,IAAI;AACb,EAAE,OAAO,EAAE,IAAI;AACf,EAAE,WAAW,EAAE,CAAC,QAAQ;AACxB,EAAE,aAAa,EAAE,CAAC,QAAQ;AAC1B,EAAE,SAAS,EAAE,KAAK;AAClB,EAAE,EAAE,EAAE,CAAC;AACP,CAAC;AACD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAAS,gBAAgB,CAAC,KAAK,EAAE;AACxC,CAAC,MAAM,GAAG,GAAG,IAAI,GAAG,GAAE;AACtB,CAAC,KAAK,MAAM,EAAE,IAAI,KAAK,IAAI,EAAE,EAAE;AAC/B,EAAE,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,KAAK,EAAE,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,EAAE,CAAC,CAAC,EAAE,SAAS,EAAE,EAAE,CAAC,CAAC,EAAE,UAAU,EAAE,EAAE,CAAC,CAAC,EAAE,EAAC;AACzF,EAAE;AACF,CAAC,OAAO,GAAG;AACX;;;;"}
|
|
1
|
+
{"version":3,"file":"gridStack.js","sources":["../../../src/utils/gridStack.js"],"sourcesContent":["/**\n * Thin GridStack bootstrap shared by the drag/resize grids (CnDashboardGrid and\n * CnWidgetGrid's editable body slot). Keeps the GridStack option shape in one\n * place so the two engines stay consistent.\n *\n * @module utils/gridStack\n */\n\nimport { GridStack } from 'gridstack'\n// `gridstack` is a peerDependency, not bundled — see the matching comment in\n// CnDashboardGrid.vue. The consumer must install `gridstack` themselves and\n// import its stylesheet (`gridstack/dist/gridstack.min.css`) from that same\n// copy; nc-vue ships no GridStack CSS of its own so the JS driving the grid\n// and the CSS sizing it can never come from two different versions.\n\n/**\n * Initialise a GridStack instance on a `.grid-stack` element.\n *\n * @param {HTMLElement} el The `.grid-stack` container element.\n * @param {object} opts Options.\n * @param {number} opts.columns Column count.\n * @param {boolean} opts.editable Whether drag + resize are enabled.\n * @param {number} [opts.cellHeight] Cell height in px (default 80).\n * @param {number} [opts.margin] Grid margin in px (default 12).\n * @return {import('gridstack').GridStack} The GridStack instance.\n */\nexport function initGridStack(el, opts) {\n\tconst { columns, editable, cellHeight = 80, margin = 12 } = opts\n\treturn GridStack.init({\n\t\tcolumn: columns,\n\t\tcellHeight,\n\t\tmargin,\n\t\tfloat: true,\n\t\tanimate: true,\n\t\tdisableDrag: !editable,\n\t\tdisableResize: !editable,\n\t\tremovable: false,\n\t}, el)\n}\n\n/**\n * Read the geometry GridStack reports for a changed set of items into a plain\n * map keyed by node id.\n *\n * @param {Array<{id: (string|number), x: number, y: number, w: number, h: number}>} items\n * The items GridStack passes to its `change` handler.\n * @return {Map<string, {gridX: number, gridY: number, gridWidth: number, gridHeight: number}>}\n */\nexport function readGridGeometry(items) {\n\tconst map = new Map()\n\tfor (const it of items || []) {\n\t\tmap.set(String(it.id), { gridX: it.x, gridY: it.y, gridWidth: it.w, gridHeight: it.h })\n\t}\n\treturn map\n}\n\n/**\n * Where the items under a growing cell go so they keep their order: every item\n * the growth would overlap moves down by the growth, and so does every item\n * that move would overlap in turn. Items beside the cell stay put.\n *\n * GridStack resolves the same collision itself, but not in order: when\n * pipelinq's size-to-content banner grew from two rows to three, the\n * full-width card under the stat tiles landed below the lists.\n *\n * @spec openspec/changes/dashboard-growth-keeps-order/specs/dashboard-grid/spec.md#requirement-a-growing-cell-keeps-the-order-of-the-cards-under-it\n * @param {Array<{x: number, y: number, w: number, h: number}>} nodes The grid's items (the growing one among them).\n * @param {{x: number, y: number, w: number, h: number}} node The growing item.\n * @param {number} h Its new height, in grid rows.\n * @return {Array<{node: object, y: number}>} The items to move and their new row, bottom-most first.\n */\nexport function planGrowth(nodes, node, h) {\n\tconst delta = h - node.h\n\tif (!(delta > 0)) {\n\t\treturn []\n\t}\n\tconst moved = new Map()\n\tconst overlaps = (a, b) => a.x < b.x + b.w && b.x < a.x + a.w && a.y < b.y + b.h && b.y < a.y + a.h\n\tconst queue = [{ x: node.x, y: node.y, w: node.w, h }]\n\twhile (queue.length > 0) {\n\t\tconst rect = queue.shift()\n\t\tfor (const other of nodes) {\n\t\t\tif (other === node || moved.has(other) || !overlaps(rect, other)) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tconst next = { x: other.x, y: other.y + delta, w: other.w, h: other.h }\n\t\t\tmoved.set(other, next.y)\n\t\t\tqueue.push(next)\n\t\t}\n\t}\n\treturn [...moved.entries()]\n\t\t.map(([item, y]) => ({ node: item, y }))\n\t\t.sort((a, b) => b.y - a.y)\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SPDX-FileCopyrightText: 2026 Conduction B.V. <info@conduction.nl>
|
|
3
|
+
* SPDX-License-Identifier: EUPL-1.2
|
|
4
|
+
*
|
|
5
|
+
* Make an identifier-shaped key readable for a person. Pure: no Vue.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Split camelCase, snake_case and kebab-case into words, keep acronyms, and
|
|
10
|
+
* capitalise only the first word.
|
|
11
|
+
*
|
|
12
|
+
* `caseAssigned` → "Case assigned", `object_created` → "Object created",
|
|
13
|
+
* `newBRPRecord` → "New BRP record".
|
|
14
|
+
*
|
|
15
|
+
* @param {string} key The key.
|
|
16
|
+
* @return {string} A readable sentence-case label, or the key when it has no words.
|
|
17
|
+
* @spec openspec/changes/data-widget-nested-objects/specs/data-widget-nested-objects/spec.md#requirement-a-nested-object-reads-as-a-list-per-key
|
|
18
|
+
*/
|
|
19
|
+
function humaniseKey(key) {
|
|
20
|
+
const words = String(key || '')
|
|
21
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
|
22
|
+
.replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2')
|
|
23
|
+
.split(/[\s_\-.]+/)
|
|
24
|
+
.filter(Boolean);
|
|
25
|
+
if (words.length === 0) {
|
|
26
|
+
return String(key || '')
|
|
27
|
+
}
|
|
28
|
+
return words
|
|
29
|
+
.map((word, index) => {
|
|
30
|
+
const isAcronym = word.length > 1 && word === word.toUpperCase() && /[A-Z]/.test(word);
|
|
31
|
+
if (isAcronym) {
|
|
32
|
+
return word
|
|
33
|
+
}
|
|
34
|
+
const lower = word.toLowerCase();
|
|
35
|
+
return index === 0 ? lower.charAt(0).toUpperCase() + lower.slice(1) : lower
|
|
36
|
+
})
|
|
37
|
+
.join(' ')
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export { humaniseKey };
|
|
41
|
+
//# sourceMappingURL=humaniseKey.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"humaniseKey.js","sources":["../../../src/utils/humaniseKey.js"],"sourcesContent":["/**\n * SPDX-FileCopyrightText: 2026 Conduction B.V. <info@conduction.nl>\n * SPDX-License-Identifier: EUPL-1.2\n *\n * Make an identifier-shaped key readable for a person. Pure: no Vue.\n */\n\n/**\n * Split camelCase, snake_case and kebab-case into words, keep acronyms, and\n * capitalise only the first word.\n *\n * `caseAssigned` → \"Case assigned\", `object_created` → \"Object created\",\n * `newBRPRecord` → \"New BRP record\".\n *\n * @param {string} key The key.\n * @return {string} A readable sentence-case label, or the key when it has no words.\n * @spec openspec/changes/data-widget-nested-objects/specs/data-widget-nested-objects/spec.md#requirement-a-nested-object-reads-as-a-list-per-key\n */\nexport function humaniseKey(key) {\n\tconst words = String(key || '')\n\t\t.replace(/([a-z0-9])([A-Z])/g, '$1 $2')\n\t\t.replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2')\n\t\t.split(/[\\s_\\-.]+/)\n\t\t.filter(Boolean)\n\tif (words.length === 0) {\n\t\treturn String(key || '')\n\t}\n\treturn words\n\t\t.map((word, index) => {\n\t\t\tconst isAcronym = word.length > 1 && word === word.toUpperCase() && /[A-Z]/.test(word)\n\t\t\tif (isAcronym) {\n\t\t\t\treturn word\n\t\t\t}\n\t\t\tconst lower = word.toLowerCase()\n\t\t\treturn index === 0 ? lower.charAt(0).toUpperCase() + lower.slice(1) : lower\n\t\t})\n\t\t.join(' ')\n}\n"],"names":[],"mappings":"AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAAS,WAAW,CAAC,GAAG,EAAE;AACjC,CAAC,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,IAAI,EAAE,CAAC;AAChC,GAAG,OAAO,CAAC,oBAAoB,EAAE,OAAO,CAAC;AACzC,GAAG,OAAO,CAAC,uBAAuB,EAAE,OAAO,CAAC;AAC5C,GAAG,KAAK,CAAC,WAAW,CAAC;AACrB,GAAG,MAAM,CAAC,OAAO,EAAC;AAClB,CAAC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE;AACzB,EAAE,OAAO,MAAM,CAAC,GAAG,IAAI,EAAE,CAAC;AAC1B,EAAE;AACF,CAAC,OAAO,KAAK;AACb,GAAG,GAAG,CAAC,CAAC,IAAI,EAAE,KAAK,KAAK;AACxB,GAAG,MAAM,SAAS,GAAG,IAAI,CAAC,MAAM,GAAG,CAAC,IAAI,IAAI,KAAK,IAAI,CAAC,WAAW,EAAE,IAAI,OAAO,CAAC,IAAI,CAAC,IAAI,EAAC;AACzF,GAAG,IAAI,SAAS,EAAE;AAClB,IAAI,OAAO,IAAI;AACf,IAAI;AACJ,GAAG,MAAM,KAAK,GAAG,IAAI,CAAC,WAAW,GAAE;AACnC,GAAG,OAAO,KAAK,KAAK,CAAC,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,KAAK;AAC9E,GAAG,CAAC;AACJ,GAAG,IAAI,CAAC,GAAG,CAAC;AACZ;;;;"}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { humaniseKey } from './humaniseKey.js';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* SPDX-FileCopyrightText: 2026 Conduction B.V. <info@conduction.nl>
|
|
3
5
|
* SPDX-License-Identifier: EUPL-1.2
|
|
@@ -28,6 +30,7 @@
|
|
|
28
30
|
* @spec openspec/changes/notification-rule-labels-and-runtime-version/specs/notification-preferences/spec.md
|
|
29
31
|
*/
|
|
30
32
|
|
|
33
|
+
|
|
31
34
|
/**
|
|
32
35
|
* Pick the string for the current language out of a string or per-locale map.
|
|
33
36
|
*
|
|
@@ -54,34 +57,14 @@ function resolveLocalised(value, language = 'en') {
|
|
|
54
57
|
}
|
|
55
58
|
|
|
56
59
|
/**
|
|
57
|
-
* Make a rule key readable
|
|
58
|
-
* words, keep acronyms, and capitalise only the first word.
|
|
59
|
-
*
|
|
60
|
-
* `caseAssigned` → "Case assigned", `object_created` → "Object created",
|
|
61
|
-
* `newBRPRecord` → "New BRP record".
|
|
60
|
+
* Make a rule key readable (`caseAssigned` → "Case assigned"); see `humaniseKey`.
|
|
62
61
|
*
|
|
63
62
|
* @param {string} key The rule key.
|
|
64
63
|
* @return {string} A readable sentence-case label, or the key when it has no words.
|
|
64
|
+
* @spec openspec/changes/notification-rule-labels-and-runtime-version/specs/notification-preferences/spec.md#requirement-a-notification-rule-reads-as-a-label
|
|
65
65
|
*/
|
|
66
66
|
function humaniseRuleKey(key) {
|
|
67
|
-
|
|
68
|
-
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
|
69
|
-
.replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2')
|
|
70
|
-
.split(/[\s_\-.]+/)
|
|
71
|
-
.filter(Boolean);
|
|
72
|
-
if (words.length === 0) {
|
|
73
|
-
return String(key || '')
|
|
74
|
-
}
|
|
75
|
-
return words
|
|
76
|
-
.map((word, index) => {
|
|
77
|
-
const isAcronym = word.length > 1 && word === word.toUpperCase() && /[A-Z]/.test(word);
|
|
78
|
-
if (isAcronym) {
|
|
79
|
-
return word
|
|
80
|
-
}
|
|
81
|
-
const lower = word.toLowerCase();
|
|
82
|
-
return index === 0 ? lower.charAt(0).toUpperCase() + lower.slice(1) : lower
|
|
83
|
-
})
|
|
84
|
-
.join(' ')
|
|
67
|
+
return humaniseKey(key)
|
|
85
68
|
}
|
|
86
69
|
|
|
87
70
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"notificationRuleLabel.js","sources":["../../../src/utils/notificationRuleLabel.js"],"sourcesContent":["/**\n * SPDX-FileCopyrightText: 2026 Conduction B.V. <info@conduction.nl>\n * SPDX-License-Identifier: EUPL-1.2\n *\n * The label a person reads for one notification rule in their settings.\n *\n * A rule is declared in a schema's `x-openregister-notifications` map under a\n * KEY (`caseAssigned`, `clientUpdated`, `onCreate`). The key is an identifier\n * for a developer. The settings pane used to print it as is, so a person saw\n * \"substitutionRegisteredForSubstitute\" next to a switch and had to guess.\n *\n * The label is taken from the first source that has one, most specific first:\n *\n * 1. the rule's own words, when OpenRegister passes them on the preference\n * entry (`label`, then `title`), as a plain string or a per-locale map\n * like the dialect's `subject` (`{ \"nl\": \"…\", \"en\": \"…\" }`);\n * 2. the app's own label for the rule, from the `labels` map the app hands\n * to CnAppRoot (`notificationLabels`), keyed `<schema>.<key>` or `<key>`;\n * 3. the library's wording for the generic object-event keys;\n * 4. the key made readable: `caseAssigned` becomes \"Case assigned\".\n *\n * The rule's `subject` is used only when it carries no `{{placeholder}}`: it\n * is the notification TITLE template, and \"Case \"{{title}}\" assigned to you\"\n * reads worse as a setting than the readable key does.\n *\n * Pure: no store, no fetch, no Vue.\n *\n * @spec openspec/changes/notification-rule-labels-and-runtime-version/specs/notification-preferences/spec.md\n */\n\n/**\n * Pick the string for the current language out of a string or per-locale map.\n *\n * @param {string|object|null|undefined} value A string, or `{ nl, en, … }`.\n * @param {string} [language] The current language, e.g. `nl` or `de_DE`.\n * @return {string} The resolved text, or '' when there is none.\n */\nexport function resolveLocalised(value, language = 'en') {\n\tif (typeof value === 'string') {\n\t\treturn value.trim()\n\t}\n\tif (!value || typeof value !== 'object' || Array.isArray(value)) {\n\t\treturn ''\n\t}\n\tconst lang = String(language || 'en')\n\tconst base = lang.split(/[-_]/)[0]\n\tfor (const candidate of [lang, base, 'en']) {\n\t\tif (typeof value[candidate] === 'string' && value[candidate].trim() !== '') {\n\t\t\treturn value[candidate].trim()\n\t\t}\n\t}\n\tconst first = Object.values(value).find((text) => typeof text === 'string' && text.trim() !== '')\n\treturn first ? first.trim() : ''\n}\n\n/**\n * Make a rule key readable
|
|
1
|
+
{"version":3,"file":"notificationRuleLabel.js","sources":["../../../src/utils/notificationRuleLabel.js"],"sourcesContent":["/**\n * SPDX-FileCopyrightText: 2026 Conduction B.V. <info@conduction.nl>\n * SPDX-License-Identifier: EUPL-1.2\n *\n * The label a person reads for one notification rule in their settings.\n *\n * A rule is declared in a schema's `x-openregister-notifications` map under a\n * KEY (`caseAssigned`, `clientUpdated`, `onCreate`). The key is an identifier\n * for a developer. The settings pane used to print it as is, so a person saw\n * \"substitutionRegisteredForSubstitute\" next to a switch and had to guess.\n *\n * The label is taken from the first source that has one, most specific first:\n *\n * 1. the rule's own words, when OpenRegister passes them on the preference\n * entry (`label`, then `title`), as a plain string or a per-locale map\n * like the dialect's `subject` (`{ \"nl\": \"…\", \"en\": \"…\" }`);\n * 2. the app's own label for the rule, from the `labels` map the app hands\n * to CnAppRoot (`notificationLabels`), keyed `<schema>.<key>` or `<key>`;\n * 3. the library's wording for the generic object-event keys;\n * 4. the key made readable: `caseAssigned` becomes \"Case assigned\".\n *\n * The rule's `subject` is used only when it carries no `{{placeholder}}`: it\n * is the notification TITLE template, and \"Case \"{{title}}\" assigned to you\"\n * reads worse as a setting than the readable key does.\n *\n * Pure: no store, no fetch, no Vue.\n *\n * @spec openspec/changes/notification-rule-labels-and-runtime-version/specs/notification-preferences/spec.md\n */\n\nimport { humaniseKey } from './humaniseKey.js'\n\n/**\n * Pick the string for the current language out of a string or per-locale map.\n *\n * @param {string|object|null|undefined} value A string, or `{ nl, en, … }`.\n * @param {string} [language] The current language, e.g. `nl` or `de_DE`.\n * @return {string} The resolved text, or '' when there is none.\n */\nexport function resolveLocalised(value, language = 'en') {\n\tif (typeof value === 'string') {\n\t\treturn value.trim()\n\t}\n\tif (!value || typeof value !== 'object' || Array.isArray(value)) {\n\t\treturn ''\n\t}\n\tconst lang = String(language || 'en')\n\tconst base = lang.split(/[-_]/)[0]\n\tfor (const candidate of [lang, base, 'en']) {\n\t\tif (typeof value[candidate] === 'string' && value[candidate].trim() !== '') {\n\t\t\treturn value[candidate].trim()\n\t\t}\n\t}\n\tconst first = Object.values(value).find((text) => typeof text === 'string' && text.trim() !== '')\n\treturn first ? first.trim() : ''\n}\n\n/**\n * Make a rule key readable (`caseAssigned` → \"Case assigned\"); see `humaniseKey`.\n *\n * @param {string} key The rule key.\n * @return {string} A readable sentence-case label, or the key when it has no words.\n * @spec openspec/changes/notification-rule-labels-and-runtime-version/specs/notification-preferences/spec.md#requirement-a-notification-rule-reads-as-a-label\n */\nexport function humaniseRuleKey(key) {\n\treturn humaniseKey(key)\n}\n\n/**\n * The label for one preference entry.\n *\n * @param {object} entry A preference entry: `{ schema, notification, label?, title?, subject? }`.\n * @param {object} [options] Where else a label may come from.\n * @param {object} [options.labels] App-supplied labels keyed `<schema>.<key>` or `<key>`;\n * values are strings or per-locale maps.\n * @param {object} [options.known] Library wording for generic keys, keyed by rule key.\n * @param {string} [options.language] The current language.\n * @return {string} The label to show.\n */\nexport function notificationRuleLabel(entry, { labels = {}, known = {}, language = 'en' } = {}) {\n\tconst key = String(entry?.notification ?? '')\n\tconst schema = String(entry?.schema ?? '')\n\n\tfor (const field of ['label', 'title']) {\n\t\tconst own = resolveLocalised(entry?.[field], language)\n\t\tif (own) {\n\t\t\treturn own\n\t\t}\n\t}\n\n\tconst map = labels && typeof labels === 'object' ? labels : {}\n\tfor (const lookup of [schema ? `${schema}.${key}` : '', key]) {\n\t\tif (lookup && Object.hasOwn(map, lookup)) {\n\t\t\tconst fromApp = resolveLocalised(map[lookup], language)\n\t\t\tif (fromApp) {\n\t\t\t\treturn fromApp\n\t\t\t}\n\t\t}\n\t}\n\n\tif (known && typeof known[key] === 'string' && known[key]) {\n\t\treturn known[key]\n\t}\n\n\tconst subject = resolveLocalised(entry?.subject, language)\n\tif (subject && !subject.includes('{{')) {\n\t\treturn subject\n\t}\n\n\treturn humaniseRuleKey(key)\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -58,6 +58,7 @@ const SENTINEL_RESOLVERS = Object.freeze({
|
|
|
58
58
|
answer: 'resolveAssignValue (utils/formAssign.js)',
|
|
59
59
|
row: 'CnFieldListWidget (rowActions request)',
|
|
60
60
|
subject: 'CnBannerWidget (action route)',
|
|
61
|
+
context: 'detailWidgetVisible (utils/detailWidgetVisibility.js)',
|
|
61
62
|
});
|
|
62
63
|
|
|
63
64
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resolveManifestTokens.js","sources":["../../../src/utils/resolveManifestTokens.js"],"sourcesContent":["/**\n * SPDX-FileCopyrightText: 2026 Conduction B.V.\n * SPDX-License-Identifier: EUPL-1.2\n *\n * Single documented resolver-dispatch entrypoint for the closed sentinel-token\n * vocabulary. Before this module the four resolvers were invoked ad-hoc by\n * callers who had to know which resolver owned which token. This entrypoint\n * routes a token to its owning resolver BY CONTEXT (see\n * {@link module:utils/sentinelTokens}) so the vocabulary cannot drift between\n * the schema, the runtime, and the gate.\n *\n * It does NOT re-implement resolution: it composes the existing resolver\n * functions verbatim —\n *\n * - route context → {@link resolveRouteSentinels} (reference-preserving walk +\n * per-page unresolved warning);\n * - filter / object / workspace / config(@config.<key>) contexts →\n * {@link resolveFilterValue} per leaf (identical behaviour to\n * resolveFilterTokens, which is that function applied over a filter map);\n * - config `@resolve:<key>` context → handled at manifest-LOAD time by\n * `resolveManifestSentinels` (async IAppConfig), so a render-time\n * subtree pass leaves it untouched — matching today's split;\n * - declarative `@self`/`@ref`/`@aggregate` → resolved server-side by\n * OpenRegister, so they pass through the client walk untouched;\n * - visibleWhen `@total` (manifest-form-logic) → resolved by\n * {@link module:utils/visibleWhen}'s own `evaluateVisibleWhen` /\n * `evaluateVisibleWhenLocal`, not by this generic subtree walk, so it\n * likewise passes through untouched.\n *\n * On top it adds the migration-window behaviour the vocabulary requires: a\n * one-time `console.warn` naming the canonical replacement whenever a\n * DEPRECATED token is encountered.\n *\n * @module utils/resolveManifestTokens\n */\n\nimport { isOptionalUnresolved, resolveFilterValue } from './resolveFilterTokens.js'\nimport resolveRouteSentinels from './resolveRouteSentinels.js'\nimport { contextOf, looksLikeSentinel, matchDeprecation, SENTINEL_CONTEXTS } from './sentinelTokens.js'\n\n/**\n * Dispatch table: canonical context → the resolver that owns it. Documentation\n * + programmatic lookup; the actual routing in {@link resolveManifestSubtree}\n * uses the same source-of-truth classifier ({@link contextOf}).\n *\n * @type {Readonly<Record<string, string>>}\n */\nexport const SENTINEL_RESOLVERS = Object.freeze({\n\tfilter: 'resolveFilterTokens',\n\tconfig: 'resolveManifestSentinels / resolveFilterTokens',\n\tobject: 'resolveFilterTokens',\n\tworkspace: 'resolveFilterTokens',\n\troute: 'resolveRouteSentinels',\n\tdeclarative: 'fetchAggregate (OpenRegister server-side)',\n\tvisibleWhen: 'evaluateVisibleWhen (utils/visibleWhen.js)',\n\tanswer: 'resolveAssignValue (utils/formAssign.js)',\n\trow: 'CnFieldListWidget (rowActions request)',\n\tsubject: 'CnBannerWidget (action route)',\n})\n\n/**\n * Process-wide set of deprecated tokens already warned about, so the same\n * deprecated token referenced from many pages warns once. Cleared by\n * {@link clearDeprecationWarnings} (test-only).\n *\n * @type {Set<string>}\n */\nconst _warnedDeprecations = new Set()\n\n/**\n * Reset the one-time deprecation-warning dedup set. Test-only.\n *\n * @return {void}\n */\nexport function clearDeprecationWarnings() {\n\t_warnedDeprecations.clear()\n}\n\n/**\n * Emit a one-time deprecation warning for `token` if it is on the deprecation\n * map. Idempotent per token for the process lifetime.\n *\n * @param {string} token The `@`-prefixed token.\n * @param {(...args: unknown[]) => void} warn console.warn override (tests).\n * @return {boolean} True when the token is deprecated (regardless of whether a\n * warning was emitted this call).\n */\nexport function warnIfDeprecated(token, warn) {\n\tconst dep = matchDeprecation(token)\n\tif (!dep) {\n\t\treturn false\n\t}\n\tif (!_warnedDeprecations.has(token)) {\n\t\t_warnedDeprecations.add(token)\n\t\tconst target = dep.replacement\n\t\t\t? `use '${dep.replacement}' instead`\n\t\t\t: 'it will be removed with no direct replacement'\n\t\twarn(`[resolveManifestTokens] Deprecated sentinel token '${token}' — ${target} `\n\t\t\t+ `(removal ${dep.removal}). ${dep.note}`)\n\t}\n\treturn true\n}\n\n/**\n * Resolve every sentinel token in a manifest subtree through the single\n * dispatch, routing each by context to its existing resolver.\n *\n * Behaviour is the composition of the existing resolvers, so this is a\n * drop-in for a caller that previously ran `resolveRouteSentinels` then a\n * `resolveFilterTokens`-style pass by hand.\n *\n * @param {unknown} value The subtree (typically a `pages[].config` block). Not mutated.\n * @param {object} [opts] Resolution inputs.\n * @param {object} [opts.params] vue-router params for `@route.<param>`.\n * @param {{objectId?: (string|number), object?: object, workspace?: object, config?: object}} [opts.ctx]\n * Context forwarded to {@link resolveFilterValue} for filter / object /\n * workspace / `@config.<key>` tokens.\n * @param {string} [opts.pageId] Page id for route-resolver warning dedup.\n * @param {(...args: unknown[]) => void} [opts.warn] console.warn override (tests).\n * @return {{value: unknown, unresolved: string[]}} The resolved subtree plus the\n * list of tokens that stayed unresolved (excluding OPTIONAL `?` tokens, which\n * are meant to be dropped, not waited on).\n */\nexport function resolveManifestSubtree(value, opts = {}) {\n\tconst params = opts.params && typeof opts.params === 'object' ? opts.params : {}\n\tconst ctx = opts.ctx && typeof opts.ctx === 'object' ? opts.ctx : {}\n\tconst pageId = opts.pageId || '<unknown>'\n\tconst warn = opts.warn ?? ((...args) => {\n\t\t// eslint-disable-next-line no-console\n\t\tconsole.warn(...args)\n\t})\n\n\t// Pass 1: route context (reuses the reference-preserving route walker,\n\t// including its own per-page unresolved warning).\n\tconst afterRoute = resolveRouteSentinels(value, params, pageId)\n\n\t// Pass 2: filter / object / workspace / @config.<key> contexts, per leaf,\n\t// via the existing resolveFilterValue. Deprecated tokens warn once.\n\tconst unresolved = []\n\tconst walk = (node) => {\n\t\tif (typeof node === 'string') {\n\t\t\tif (!looksLikeSentinel(node)) {\n\t\t\t\treturn node\n\t\t\t}\n\t\t\twarnIfDeprecated(node, warn)\n\t\t\tconst resolved = resolveFilterValue(node, ctx)\n\t\t\tif (resolved === node && !isOptionalUnresolved(node)) {\n\t\t\t\t// Still a raw token: unresolved unless it is a load-time / server-side\n\t\t\t\t// context this render-pass deliberately leaves alone.\n\t\t\t\tconst c = contextOf(node)\n\t\t\t\tif (c !== 'config' && c !== 'declarative' && c !== 'visibleWhen') {\n\t\t\t\t\tunresolved.push(node)\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn resolved\n\t\t}\n\t\tif (Array.isArray(node)) {\n\t\t\treturn node.map(walk)\n\t\t}\n\t\tif (node && typeof node === 'object') {\n\t\t\tconst out = {}\n\t\t\tfor (const [k, v] of Object.entries(node)) {\n\t\t\t\tout[k] = walk(v)\n\t\t\t}\n\t\t\treturn out\n\t\t}\n\t\treturn node\n\t}\n\n\treturn { value: walk(afterRoute), unresolved }\n}\n\nexport { SENTINEL_CONTEXTS }\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;AAIA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACY,MAAC,kBAAkB,GAAG,MAAM,CAAC,MAAM,CAAC;AAChD,CAAC,MAAM,EAAE,qBAAqB;AAC9B,CAAC,MAAM,EAAE,gDAAgD;AACzD,CAAC,MAAM,EAAE,qBAAqB;AAC9B,CAAC,SAAS,EAAE,qBAAqB;AACjC,CAAC,KAAK,EAAE,uBAAuB;AAC/B,CAAC,WAAW,EAAE,2CAA2C;AACzD,CAAC,WAAW,EAAE,4CAA4C;AAC1D,CAAC,MAAM,EAAE,0CAA0C;AACnD,CAAC,GAAG,EAAE,wCAAwC;AAC9C,CAAC,OAAO,EAAE,+BAA+B;AACzC,CAAC,EAAC;AACF;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAM,mBAAmB,GAAG,IAAI,GAAG,GAAE;AACrC;AACA;AACA;AACA;AACA;AACA;AACO,SAAS,wBAAwB,GAAG;AAC3C,CAAC,mBAAmB,CAAC,KAAK,GAAE;AAC5B,CAAC;AACD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAAS,gBAAgB,CAAC,KAAK,EAAE,IAAI,EAAE;AAC9C,CAAC,MAAM,GAAG,GAAG,gBAAgB,CAAC,KAAK,EAAC;AACpC,CAAC,IAAI,CAAC,GAAG,EAAE;AACX,EAAE,OAAO,KAAK;AACd,EAAE;AACF,CAAC,IAAI,CAAC,mBAAmB,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE;AACtC,EAAE,mBAAmB,CAAC,GAAG,CAAC,KAAK,EAAC;AAChC,EAAE,MAAM,MAAM,GAAG,GAAG,CAAC,WAAW;AAChC,KAAK,CAAC,KAAK,EAAE,GAAG,CAAC,WAAW,CAAC,SAAS,CAAC;AACvC,KAAK,gDAA+C;AACpD,EAAE,IAAI,CAAC,CAAC,mDAAmD,EAAE,KAAK,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC;AAClF,KAAK,CAAC,SAAS,EAAE,GAAG,CAAC,OAAO,CAAC,GAAG,EAAE,GAAG,CAAC,IAAI,CAAC,CAAC,EAAC;AAC7C,EAAE;AACF,CAAC,OAAO,IAAI;AACZ,CAAC;AACD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAAS,sBAAsB,CAAC,KAAK,EAAE,IAAI,GAAG,EAAE,EAAE;AACzD,CAAC,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,IAAI,OAAO,IAAI,CAAC,MAAM,KAAK,QAAQ,GAAG,IAAI,CAAC,MAAM,GAAG,GAAE;AACjF,CAAC,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,IAAI,OAAO,IAAI,CAAC,GAAG,KAAK,QAAQ,GAAG,IAAI,CAAC,GAAG,GAAG,GAAE;AACrE,CAAC,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,IAAI,YAAW;AAC1C,CAAC,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,KAAK,CAAC,GAAG,IAAI,KAAK;AACzC;AACA,EAAE,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,EAAC;AACvB,EAAE,EAAC;AACH;AACA;AACA;AACA,CAAC,MAAM,UAAU,GAAG,qBAAqB,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAC;AAChE;AACA;AACA;AACA,CAAC,MAAM,UAAU,GAAG,GAAE;AACtB,CAAC,MAAM,IAAI,GAAG,CAAC,IAAI,KAAK;AACxB,EAAE,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE;AAChC,GAAG,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,EAAE;AACjC,IAAI,OAAO,IAAI;AACf,IAAI;AACJ,GAAG,gBAAgB,CAAC,IAAI,EAAE,IAAI,EAAC;AAC/B,GAAG,MAAM,QAAQ,GAAG,kBAAkB,CAAC,IAAI,EAAE,GAAG,EAAC;AACjD,GAAG,IAAI,QAAQ,KAAK,IAAI,IAAI,CAAC,oBAAoB,CAAC,IAAI,CAAC,EAAE;AACzD;AACA;AACA,IAAI,MAAM,CAAC,GAAG,SAAS,CAAC,IAAI,EAAC;AAC7B,IAAI,IAAI,CAAC,KAAK,QAAQ,IAAI,CAAC,KAAK,aAAa,IAAI,CAAC,KAAK,aAAa,EAAE;AACtE,KAAK,UAAU,CAAC,IAAI,CAAC,IAAI,EAAC;AAC1B,KAAK;AACL,IAAI;AACJ,GAAG,OAAO,QAAQ;AAClB,GAAG;AACH,EAAE,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE;AAC3B,GAAG,OAAO,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC;AACxB,GAAG;AACH,EAAE,IAAI,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE;AACxC,GAAG,MAAM,GAAG,GAAG,GAAE;AACjB,GAAG,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE;AAC9C,IAAI,GAAG,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,EAAC;AACpB,IAAI;AACJ,GAAG,OAAO,GAAG;AACb,GAAG;AACH,EAAE,OAAO,IAAI;AACb,GAAE;AACF;AACA,CAAC,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC,UAAU,CAAC,EAAE,UAAU,EAAE;AAC/C;;;;"}
|
|
1
|
+
{"version":3,"file":"resolveManifestTokens.js","sources":["../../../src/utils/resolveManifestTokens.js"],"sourcesContent":["/**\n * SPDX-FileCopyrightText: 2026 Conduction B.V.\n * SPDX-License-Identifier: EUPL-1.2\n *\n * Single documented resolver-dispatch entrypoint for the closed sentinel-token\n * vocabulary. Before this module the four resolvers were invoked ad-hoc by\n * callers who had to know which resolver owned which token. This entrypoint\n * routes a token to its owning resolver BY CONTEXT (see\n * {@link module:utils/sentinelTokens}) so the vocabulary cannot drift between\n * the schema, the runtime, and the gate.\n *\n * It does NOT re-implement resolution: it composes the existing resolver\n * functions verbatim —\n *\n * - route context → {@link resolveRouteSentinels} (reference-preserving walk +\n * per-page unresolved warning);\n * - filter / object / workspace / config(@config.<key>) contexts →\n * {@link resolveFilterValue} per leaf (identical behaviour to\n * resolveFilterTokens, which is that function applied over a filter map);\n * - config `@resolve:<key>` context → handled at manifest-LOAD time by\n * `resolveManifestSentinels` (async IAppConfig), so a render-time\n * subtree pass leaves it untouched — matching today's split;\n * - declarative `@self`/`@ref`/`@aggregate` → resolved server-side by\n * OpenRegister, so they pass through the client walk untouched;\n * - visibleWhen `@total` (manifest-form-logic) → resolved by\n * {@link module:utils/visibleWhen}'s own `evaluateVisibleWhen` /\n * `evaluateVisibleWhenLocal`, not by this generic subtree walk, so it\n * likewise passes through untouched.\n *\n * On top it adds the migration-window behaviour the vocabulary requires: a\n * one-time `console.warn` naming the canonical replacement whenever a\n * DEPRECATED token is encountered.\n *\n * @module utils/resolveManifestTokens\n */\n\nimport { isOptionalUnresolved, resolveFilterValue } from './resolveFilterTokens.js'\nimport resolveRouteSentinels from './resolveRouteSentinels.js'\nimport { contextOf, looksLikeSentinel, matchDeprecation, SENTINEL_CONTEXTS } from './sentinelTokens.js'\n\n/**\n * Dispatch table: canonical context → the resolver that owns it. Documentation\n * + programmatic lookup; the actual routing in {@link resolveManifestSubtree}\n * uses the same source-of-truth classifier ({@link contextOf}).\n *\n * @type {Readonly<Record<string, string>>}\n */\nexport const SENTINEL_RESOLVERS = Object.freeze({\n\tfilter: 'resolveFilterTokens',\n\tconfig: 'resolveManifestSentinels / resolveFilterTokens',\n\tobject: 'resolveFilterTokens',\n\tworkspace: 'resolveFilterTokens',\n\troute: 'resolveRouteSentinels',\n\tdeclarative: 'fetchAggregate (OpenRegister server-side)',\n\tvisibleWhen: 'evaluateVisibleWhen (utils/visibleWhen.js)',\n\tanswer: 'resolveAssignValue (utils/formAssign.js)',\n\trow: 'CnFieldListWidget (rowActions request)',\n\tsubject: 'CnBannerWidget (action route)',\n\tcontext: 'detailWidgetVisible (utils/detailWidgetVisibility.js)',\n})\n\n/**\n * Process-wide set of deprecated tokens already warned about, so the same\n * deprecated token referenced from many pages warns once. Cleared by\n * {@link clearDeprecationWarnings} (test-only).\n *\n * @type {Set<string>}\n */\nconst _warnedDeprecations = new Set()\n\n/**\n * Reset the one-time deprecation-warning dedup set. Test-only.\n *\n * @return {void}\n */\nexport function clearDeprecationWarnings() {\n\t_warnedDeprecations.clear()\n}\n\n/**\n * Emit a one-time deprecation warning for `token` if it is on the deprecation\n * map. Idempotent per token for the process lifetime.\n *\n * @param {string} token The `@`-prefixed token.\n * @param {(...args: unknown[]) => void} warn console.warn override (tests).\n * @return {boolean} True when the token is deprecated (regardless of whether a\n * warning was emitted this call).\n */\nexport function warnIfDeprecated(token, warn) {\n\tconst dep = matchDeprecation(token)\n\tif (!dep) {\n\t\treturn false\n\t}\n\tif (!_warnedDeprecations.has(token)) {\n\t\t_warnedDeprecations.add(token)\n\t\tconst target = dep.replacement\n\t\t\t? `use '${dep.replacement}' instead`\n\t\t\t: 'it will be removed with no direct replacement'\n\t\twarn(`[resolveManifestTokens] Deprecated sentinel token '${token}' — ${target} `\n\t\t\t+ `(removal ${dep.removal}). ${dep.note}`)\n\t}\n\treturn true\n}\n\n/**\n * Resolve every sentinel token in a manifest subtree through the single\n * dispatch, routing each by context to its existing resolver.\n *\n * Behaviour is the composition of the existing resolvers, so this is a\n * drop-in for a caller that previously ran `resolveRouteSentinels` then a\n * `resolveFilterTokens`-style pass by hand.\n *\n * @param {unknown} value The subtree (typically a `pages[].config` block). Not mutated.\n * @param {object} [opts] Resolution inputs.\n * @param {object} [opts.params] vue-router params for `@route.<param>`.\n * @param {{objectId?: (string|number), object?: object, workspace?: object, config?: object}} [opts.ctx]\n * Context forwarded to {@link resolveFilterValue} for filter / object /\n * workspace / `@config.<key>` tokens.\n * @param {string} [opts.pageId] Page id for route-resolver warning dedup.\n * @param {(...args: unknown[]) => void} [opts.warn] console.warn override (tests).\n * @return {{value: unknown, unresolved: string[]}} The resolved subtree plus the\n * list of tokens that stayed unresolved (excluding OPTIONAL `?` tokens, which\n * are meant to be dropped, not waited on).\n */\nexport function resolveManifestSubtree(value, opts = {}) {\n\tconst params = opts.params && typeof opts.params === 'object' ? opts.params : {}\n\tconst ctx = opts.ctx && typeof opts.ctx === 'object' ? opts.ctx : {}\n\tconst pageId = opts.pageId || '<unknown>'\n\tconst warn = opts.warn ?? ((...args) => {\n\t\t// eslint-disable-next-line no-console\n\t\tconsole.warn(...args)\n\t})\n\n\t// Pass 1: route context (reuses the reference-preserving route walker,\n\t// including its own per-page unresolved warning).\n\tconst afterRoute = resolveRouteSentinels(value, params, pageId)\n\n\t// Pass 2: filter / object / workspace / @config.<key> contexts, per leaf,\n\t// via the existing resolveFilterValue. Deprecated tokens warn once.\n\tconst unresolved = []\n\tconst walk = (node) => {\n\t\tif (typeof node === 'string') {\n\t\t\tif (!looksLikeSentinel(node)) {\n\t\t\t\treturn node\n\t\t\t}\n\t\t\twarnIfDeprecated(node, warn)\n\t\t\tconst resolved = resolveFilterValue(node, ctx)\n\t\t\tif (resolved === node && !isOptionalUnresolved(node)) {\n\t\t\t\t// Still a raw token: unresolved unless it is a load-time / server-side\n\t\t\t\t// context this render-pass deliberately leaves alone.\n\t\t\t\tconst c = contextOf(node)\n\t\t\t\tif (c !== 'config' && c !== 'declarative' && c !== 'visibleWhen') {\n\t\t\t\t\tunresolved.push(node)\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn resolved\n\t\t}\n\t\tif (Array.isArray(node)) {\n\t\t\treturn node.map(walk)\n\t\t}\n\t\tif (node && typeof node === 'object') {\n\t\t\tconst out = {}\n\t\t\tfor (const [k, v] of Object.entries(node)) {\n\t\t\t\tout[k] = walk(v)\n\t\t\t}\n\t\t\treturn out\n\t\t}\n\t\treturn node\n\t}\n\n\treturn { value: walk(afterRoute), unresolved }\n}\n\nexport { SENTINEL_CONTEXTS }\n"],"names":[],"mappings":"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"}
|