@conduction/nextcloud-vue 2.82.0 → 2.84.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js +2 -2
- package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js.map +1 -1
- package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue.js +1 -1
- package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue2.js +24 -0
- package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue2.js.map +1 -1
- package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue3.js +9 -4
- 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/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 +125 -3
- package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue2.js.map +1 -1
- package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue3.js +3 -3
- 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 +14 -0
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
- package/dist/esm/components/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 +203 -4
- 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/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 +96 -12
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +95 -50
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.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/CnMassActionBar/CnMassActionBar.vue2.js +2 -2
- package/dist/esm/components/CnMassActionBar/CnMassActionBar.vue2.js.map +1 -1
- package/dist/esm/components/CnMassImportDialog/CnMassImportDialog.vue2.js +2 -2
- package/dist/esm/components/CnMassImportDialog/CnMassImportDialog.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue.js +1 -1
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js +31 -2
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnRadioCardsWidget/CnRadioCardsWidget.vue.js +1 -1
- package/dist/esm/components/CnRadioCardsWidget/CnRadioCardsWidget.vue2.js +16 -2
- package/dist/esm/components/CnRadioCardsWidget/CnRadioCardsWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnRadioCardsWidget/CnRadioCardsWidget.vue3.js +3 -3
- package/dist/esm/components/CnRadioCardsWidget/CnRadioCardsWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +37 -2
- 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/CnReportsPage/CnReportsPage.vue.js +1 -1
- package/dist/esm/components/CnReportsPage/CnReportsPage.vue2.js +176 -0
- package/dist/esm/components/CnReportsPage/CnReportsPage.vue2.js.map +1 -1
- package/dist/esm/components/CnReportsPage/CnReportsPage.vue3.js +181 -52
- package/dist/esm/components/CnReportsPage/CnReportsPage.vue3.js.map +1 -1
- package/dist/esm/components/CnSettingsFormWidget/CnSettingsFormWidget.vue.js +1 -1
- package/dist/esm/components/CnSettingsFormWidget/CnSettingsFormWidget.vue2.js +70 -5
- package/dist/esm/components/CnSettingsFormWidget/CnSettingsFormWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnSettingsFormWidget/CnSettingsFormWidget.vue3.js +61 -35
- package/dist/esm/components/CnSettingsFormWidget/CnSettingsFormWidget.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 +4 -2
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue.js +1 -1
- package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue2.js +72 -5
- package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue3.js +88 -71
- package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue.js +1 -1
- package/dist/esm/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue2.js +18 -4
- package/dist/esm/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue.js +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +33 -3
- 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/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/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/l10n/en.json.js +16 -1
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +41 -4
- package/dist/esm/l10n/nl.json.js.map +1 -1
- package/dist/esm/library-style.js +1 -1
- package/dist/esm/nextcloud-vue.css +900 -537
- package/dist/esm/utils/buildManifest.js +37 -3
- 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/fetchAggregate.js +25 -2
- package/dist/esm/utils/fetchAggregate.js.map +1 -1
- package/dist/esm/utils/formatMetric.js +39 -3
- package/dist/esm/utils/formatMetric.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/switchValues.js +62 -0
- package/dist/esm/utils/switchValues.js.map +1 -0
- package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
- package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
- package/dist/nextcloud-vue.cjs.js +36711 -34909
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +900 -537
- package/l10n/en.json +16 -1
- package/l10n/nl.json +41 -4
- package/package.json +1 -1
- package/src/components/CnAdminSettingsShell/CnAdminSettingsShell.vue +31 -2
- package/src/components/CnAuditTrailCard/CnAuditTrailCard.vue +1 -1
- package/src/components/CnBodySections/CnBodySections.vue +22 -1
- package/src/components/CnCountdownWidget/CnCountdownWidget.vue +47 -4
- package/src/components/CnDashboardGrid/CnDashboardGrid.vue +128 -6
- package/src/components/CnDashboardPage/CnDashboardPage.vue +14 -0
- package/src/components/CnDeltaWidget/CnDeltaWidget.vue +1 -1
- package/src/components/CnDetailPage/CnDetailPage.md +2 -0
- package/src/components/CnDetailPage/CnDetailPage.vue +203 -4
- package/src/components/CnFieldListWidget/CnFieldListWidget.vue +63 -1
- package/src/components/CnGaugeWidget/CnGaugeWidget.vue +1 -1
- package/src/components/CnIndexPage/CnIndexPage.vue +125 -15
- package/src/components/CnInteractionFormWidget/CnInteractionFormWidget.vue +29 -6
- package/src/components/CnObjectDataWidget/CnObjectDataWidget.md +1 -1
- package/src/components/CnObjectDataWidget/CnObjectDataWidget.vue +31 -2
- package/src/components/CnRadioCardsWidget/CnRadioCardsWidget.vue +54 -3
- package/src/components/CnRecordListWidget/CnRecordListWidget.vue +37 -2
- package/src/components/CnReportsPage/CnReportsPage.vue +263 -5
- package/src/components/CnSettingsFormWidget/CnSettingsFormWidget.vue +110 -18
- package/src/components/CnStagesWidget/CnStagesWidget.vue +103 -6
- package/src/components/CnStatWidget/CnStatWidget.vue +4 -2
- package/src/components/CnStatsBlockWidget/CnStatsBlockWidget.vue +125 -7
- package/src/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue +18 -4
- package/src/components/CnTabsWidget/CnTabsWidget.vue +33 -3
- package/src/components/CnWalkthrough/CnWalkthrough.vue +16 -0
- package/src/css/form-field.css +14 -0
- package/src/css/form-widget.css +11 -0
- package/src/css/index-page.css +11 -0
- package/src/css/index.css +1 -0
- package/src/css/look-board-reports.css +175 -0
- package/src/css/look-board.css +44 -1
- package/src/dialogs/CnTransitionInputDialog.vue +87 -7
- package/src/schemas/app-manifest-v2.schema.json +24 -3
- package/src/utils/buildManifest.js +36 -2
- package/src/utils/detailWidgetVisibility.js +145 -0
- package/src/utils/fetchAggregate.js +24 -1
- package/src/utils/formatMetric.js +38 -2
- package/src/utils/resolveManifestTokens.js +1 -0
- package/src/utils/sentinelTokens.js +9 -0
- package/src/utils/switchValues.js +59 -0
- package/src/utils/validateManifestV2.compiled.js +3 -3
|
@@ -2,7 +2,7 @@ import script from './CnDeltaWidget.vue2.js';
|
|
|
2
2
|
import { render } from './CnDeltaWidget.vue3.js';
|
|
3
3
|
|
|
4
4
|
script.render = render;
|
|
5
|
-
script.__scopeId = "data-v-
|
|
5
|
+
script.__scopeId = "data-v-ee481872";
|
|
6
6
|
const __sfc_main = script;
|
|
7
7
|
|
|
8
8
|
export { __sfc_main as default };
|
|
@@ -457,7 +457,7 @@ var script = {
|
|
|
457
457
|
* @return {string} The formatted string.
|
|
458
458
|
*/
|
|
459
459
|
formatNumber(value) {
|
|
460
|
-
return formatMetricValue(value, this.resolvedFormat, unwrapAppConfig(this.cnAppConfig))
|
|
460
|
+
return formatMetricValue(value, this.resolvedFormat, unwrapAppConfig(this.cnAppConfig), null, this.effectiveTranslate)
|
|
461
461
|
},
|
|
462
462
|
|
|
463
463
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CnDeltaWidget.vue2.js","sources":["../../../../src/components/CnDeltaWidget/CnDeltaWidget.vue"],"sourcesContent":["<!--\n SPDX-FileCopyrightText: 2026 Conduction B.V.\n SPDX-License-Identifier: EUPL-1.2\n-->\n<template>\n\t<component\n\t\t:is=\"linkTag\"\n\t\tclass=\"cn-kpi-card cn-delta-widget\"\n\t\t:class=\"[\n\t\t\t{ 'cn-kpi-card--clickable': isLinked, 'cn-delta-widget--linked': isLinked },\n\t\t\t'cn-kpi-card--' + cardLayout,\n\t\t\tflat ? 'cn-kpi-card--flat' : 'cn-kpi-card--filled',\n\t\t]\"\n\t\tv-bind=\"linkAttrs\">\n\t\t<div\n\t\t\tv-if=\"content.icon\"\n\t\t\tclass=\"cn-kpi-card__icon cn-delta-widget__icon\"\n\t\t\t:style=\"iconCircleStyle\">\n\t\t\t<CnWidgetIcon :name=\"content.icon\" :size=\"24\" />\n\t\t</div>\n\n\t\t<div class=\"cn-kpi-card__body cn-delta-widget__body\">\n\t\t\t<div v-if=\"content.label\" class=\"cn-kpi-card__title cn-delta-widget__label\" :title=\"resolvedLabel\">\n\t\t\t\t{{ resolvedLabel }}\n\t\t\t</div>\n\n\t\t\t<div class=\"cn-kpi-card__value-row cn-delta-widget__value-row\">\n\t\t\t\t<NcLoadingIcon v-if=\"displayLoading\" :size=\"22\" />\n\t\t\t\t<span v-else-if=\"displayError\" class=\"cn-delta-widget__error\" :title=\"displayError\">—</span>\n\t\t\t\t<template v-else>\n\t\t\t\t\t<span class=\"cn-kpi-card__value cn-delta-widget__value\" :style=\"valueStyle\">{{ formattedCurrent }}</span>\n\t\t\t\t\t<span\n\t\t\t\t\t\tv-if=\"deltaPct !== null\"\n\t\t\t\t\t\tclass=\"cn-delta-widget__delta\"\n\t\t\t\t\t\t:style=\"{ color: deltaColor }\">\n\t\t\t\t\t\t<component :is=\"deltaIcon\" :size=\"16\" />\n\t\t\t\t\t\t{{ formattedDelta }}\n\t\t\t\t\t</span>\n\t\t\t\t</template>\n\t\t\t</div>\n\n\t\t\t<div v-if=\"!displayLoading && !displayError && content.caption\" class=\"cn-kpi-card__label cn-delta-widget__caption\">\n\t\t\t\t{{ resolvedCaption }}\n\t\t\t</div>\n\t\t</div>\n\t</component>\n</template>\n\n<script>\nimport { NcLoadingIcon } from '@nextcloud/vue'\nimport { inject, ref } from 'vue'\nimport TrendingDown from 'vue-material-design-icons/TrendingDown.vue'\nimport TrendingNeutral from 'vue-material-design-icons/TrendingNeutral.vue'\nimport TrendingUp from 'vue-material-design-icons/TrendingUp.vue'\nimport CnWidgetIcon from '../CnWidgetGrid/CnWidgetIcon.vue'\nimport { getByPath, useEndpointSource } from '../../composables/useEndpointSource.js'\nimport widgetLink from '../../mixins/widgetLink.js'\nimport { resolveObjectTokenContext } from '../../utils/detailObjectContext.js'\nimport { fetchAggregateValue } from '../../utils/fetchAggregate.js'\nimport { formatMetricValue, unwrapAppConfig } from '../../utils/formatMetric.js'\nimport { dropOptionalUnresolved, resolveFilterTokens, resolveFilterValue } from '../../utils/resolveFilterTokens.js'\nimport { STATUS_TEXT_COLORS } from '../../utils/statusColors.js'\n\n// The canonical KPI card, shared with CnStatWidget and CnStatsBlock.\n// Imported here so the tile is styled even when the consuming app pulls in\n// components individually rather than through css/index.css.\nimport '../../css/kpi-card.css'\n\n/**\n * CnDeltaWidget — an abstract comparison / delta KPI tile.\n *\n * Reads TWO scalars from OpenRegister's aggregation `/value` endpoint — a\n * `current` value and a `previous` value — and renders the current value\n * alongside the percentage change between them, with a directional arrow tinted\n * good/bad according to `goodDirection`. Use it for period-over-period KPIs\n * (e.g. \"this month's revenue vs last month\") by combining relative filter\n * tokens (`@monthStart`, `@quarterStart`, …) in each leg's filter. Nothing is\n * hard-coded — every fleet app gets the same editable delta tile (ADR-041).\n *\n * Resolved by its registry type key `delta`; reference it from a manifest\n * placement `{ id, type: 'delta', content: { label, source, format, ... } }`.\n *\n * Example content blob:\n * ```js\n * content: {\n * label: 'Revenue (MTD)',\n * icon: 'Cash',\n * format: { style: 'currency', currency: 'EUR', decimals: 0 },\n * source: {\n * register: 'pipelinq', schema: 'lead', metric: 'sum', field: 'value',\n * goodDirection: 'up',\n * current: { filter: { status: 'won', closedAt: { gte: '@monthStart' } } },\n * previous: { filter: { status: 'won', closedAt: { gte: '@monthStart-1mo', lt: '@monthStart' } } },\n * },\n * }\n * ```\n *\n * ENDPOINT BINDING (Wave 2, #91) — instead of the two OpenRegister legs, the\n * tile can read BOTH values from one app REST payload through the shared\n * `useEndpointSource` engine (token-resolved params, request dedup + TTL\n * cache, `cn:page:refresh` / `cn:widget:refresh` subscription). Exactly ONE\n * of `source` | `endpointSource` may be configured (validator-enforced).\n * The pipelinq `previousPeriod` overview contract:\n * ```js\n * content: {\n * label: 'Revenue',\n * format: { style: 'currency', currency: 'EUR', decimals: 0 },\n * endpointSource: {\n * url: '/apps/pipelinq/api/analytics/commercial',\n * params: { period: '@workspace.datePreset?' },\n * },\n * valueField: 'revenue', // current value (dot-path)\n * previousField: 'previousPeriod.revenue', // previous value (dot-path)\n * // deltaField: 'revenueDeltaPct', // OR a server-computed percent\n * goodDirection: 'up',\n * }\n * ```\n */\nexport default {\n\tname: 'CnDeltaWidget',\n\n\tcomponents: {\n\t\tNcLoadingIcon,\n\t\tCnWidgetIcon,\n\t\tTrendingUp,\n\t\tTrendingDown,\n\t\tTrendingNeutral,\n\t},\n\n\tmixins: [widgetLink],\n\n\tinject: {\n\t\t/**\n\t\t * Detail-page object context (`{ objectId, object }`) for `@objectId` /\n\t\t * `@object.<field>` filter tokens. Null off a detail page.\n\t\t */\n\t\tcnObjectContext: { default: null },\n\t\t/**\n\t\t * v2 slot-grid detail context holder provided by CnPageRenderer —\n\t\t * backfills the object token context where CnDetailPage is not an\n\t\t * ancestor (#91 Wave 3).\n\t\t */\n\t\tcnDetailObjectContext: { default: null },\n\t\t/**\n\t\t * Page-level workspace context (a reactive `{ <key>: value }` map)\n\t\t * provided by CnDashboardPage. Drives `@workspace.<param>` token\n\t\t * resolution — e.g. the date-range pills publish `dateFrom` / `dateTo`,\n\t\t * so a leg filter can scope to the picked window.\n\t\t */\n\t\tcnWorkspaceContext: { default: () => ({}) },\n\t\t/**\n\t\t * Page-level app config map (reactive `{ <key>: value }`) provided by\n\t\t * CnDashboardPage / CnDetailPage. Resolves `@config.<key>` tokens in the\n\t\t * `format.currency` / `format.prefix` / `format.suffix` strings (e.g. the\n\t\t * reporting currency captured by the setup wizard). Defaults to `{}`.\n\t\t */\n\t\tcnAppConfig: { default: () => ({}) },\n\t\t/**\n\t\t * Host translate function provided by CnAppRoot as\n\t\t * `cnTranslate: this.translate` (bound to the host app's id). The\n\t\t * manifest-authored `content.label` / `content.caption` are run\n\t\t * through it, exactly as CnStatWidget does. Defaults to an identity\n\t\t * function so an untranslated key renders as itself.\n\t\t */\n\t\tcnTranslate: { default: () => (key) => key },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The widget's persisted configuration blob. An optional `route`\n\t\t * (vue-router location), `clickRoute` (Wave-2 alias), or `link`\n\t\t * (external href) turns the whole tile into a click-through target\n\t\t * (see the widgetLink mixin).\n\t\t *\n\t\t * Wave 2 (#91): `endpointSource` (`{ url, method?, params?,\n\t\t * responsePath? }`) reads BOTH legs from one app REST payload —\n\t\t * `valueField` plucks the current value, `previousField` the previous\n\t\t * value (the delta percent is computed client-side), or `deltaField`\n\t\t * supplies a server-computed percent directly. Exactly one of\n\t\t * `source` | `endpointSource`. `goodDirection` moves to the content\n\t\t * top level in endpoint mode (`source.goodDirection` still wins for\n\t\t * the OpenRegister form).\n\t\t *\n\t\t * @type {{label?: string, icon?: string, iconColor?: string, caption?: string, route?: (object|string), clickRoute?: (object|string), link?: string, format?: {style?: string, currency?: string, decimals?: number, prefix?: string, suffix?: string}, source?: {register?: string, schema?: string, metric?: string, field?: string, goodDirection?: ('up'|'down'), current?: {filter?: object}, previous?: {filter?: object}}, endpointSource?: {url: string, method?: string, params?: object, responsePath?: string}, valueField?: string, previousField?: string, deltaField?: string, goodDirection?: ('up'|'down')}}\n\t\t */\n\t\tcontent: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * Translate function. Falls back to the injected `cnTranslate`\n\t\t * (itself an identity function by default). Provide explicitly when\n\t\t * mounting CnDeltaWidget outside a CnAppRoot ancestor.\n\t\t *\n\t\t * @type {((key: string) => string)|null}\n\t\t */\n\t\ttranslate: {\n\t\t\ttype: Function,\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\tsetup(props) {\n\t\t// Endpoint binding (Wave 2): the shared useEndpointSource engine owns\n\t\t// token resolution, request dedup + TTL caching, and the\n\t\t// cn:page:refresh / cn:widget:refresh subscriptions. No-op while\n\t\t// `content.endpointSource` is absent, so the two-leg OpenRegister\n\t\t// path below is untouched. Injects re-read in setup (same resolution\n\t\t// as the Options `inject` block — the CnChartWidget precedent).\n\t\tconst objectCtxRaw = inject('cnObjectContext', null)\n\t\tconst detailCtxRaw = inject('cnDetailObjectContext', null)\n\t\tconst workspaceRaw = inject('cnWorkspaceContext', ref({}))\n\t\tconst appConfigRaw = inject('cnAppConfig', ref({}))\n\t\tconst unwrap = (v) => ((v && typeof v === 'object' && 'value' in v) ? v.value : v)\n\t\tconst { data, loading, error, refetch } = useEndpointSource(\n\t\t\t() => (props.content && props.content.endpointSource) || null,\n\t\t\t{\n\t\t\t\tctx: () => ({\n\t\t\t\t\t...(resolveObjectTokenContext(objectCtxRaw, detailCtxRaw) || {}),\n\t\t\t\t\tworkspace: unwrap(workspaceRaw) || {},\n\t\t\t\t\tconfig: unwrap(appConfigRaw) || {},\n\t\t\t\t}),\n\t\t\t},\n\t\t)\n\t\treturn { epData: data, epLoading: loading, epError: error, epRefetch: refetch }\n\t},\n\n\tdata() {\n\t\treturn {\n\t\t\tcurrent: null,\n\t\t\tprevious: null,\n\t\t\tloading: false,\n\t\t\terror: '',\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * Effective translate function: the explicit `translate` prop when\n\t\t * given, else the injected `cnTranslate` (identity by default).\n\t\t *\n\t\t * @return {(key: string) => string}\n\t\t */\n\t\teffectiveTranslate() {\n\t\t\treturn this.translate ?? this.cnTranslate\n\t\t},\n\n\t\t/**\n\t\t * The tile label, run through the host translate function so a\n\t\t * manifest-authored source string localises to the user's language.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tresolvedLabel() {\n\t\t\tconst label = this.content.label\n\t\t\treturn label ? this.effectiveTranslate(label) : ''\n\t\t},\n\n\t\t/**\n\t\t * The tile caption, run through the host translate function.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tresolvedCaption() {\n\t\t\tconst caption = this.content.caption\n\t\t\treturn caption ? this.effectiveTranslate(caption) : ''\n\t\t},\n\n\t\t/**\n\t\t * The unwrapped page-level app config map for `@config.*` token\n\t\t * resolution. Always an object (defaults to `{}`).\n\t\t *\n\t\t * @return {object}\n\t\t */\n\t\tconfigCtx() {\n\t\t\tconst c = this.cnAppConfig\n\t\t\tconst unwrapped = (c && typeof c === 'object' && 'value' in c) ? c.value : c\n\t\t\treturn (unwrapped && typeof unwrapped === 'object') ? unwrapped : {}\n\t\t},\n\n\t\t/**\n\t\t * The `content.format` spec with its `currency` / `prefix` / `suffix`\n\t\t * `@config.<key>` tokens resolved against the page-level app config. A\n\t\t * required token that stays unset is dropped so the downstream default\n\t\t * applies (EUR for currency, empty for prefix/suffix) rather than being\n\t\t * passed to `Intl.NumberFormat` as a literal `@config.…` (which throws).\n\t\t *\n\t\t * @return {object} The resolved format spec.\n\t\t */\n\t\tresolvedFormat() {\n\t\t\tconst fmt = this.content.format || {}\n\t\t\tconst ctx = { config: this.configCtx }\n\t\t\tconst out = { ...fmt }\n\t\t\tfor (const key of ['currency', 'prefix', 'suffix']) {\n\t\t\t\tconst raw = fmt[key]\n\t\t\t\tif (typeof raw !== 'string' || raw.charAt(0) !== '@') {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tconst resolved = resolveFilterValue(raw, ctx)\n\t\t\t\tout[key] = (typeof resolved === 'string' && resolved.charAt(0) === '@') ? undefined : resolved\n\t\t\t}\n\t\t\treturn out\n\t\t},\n\n\t\t/** Inline style for the icon circle (tinted with iconColor). */\n\t\ticonCircleStyle() {\n\t\t\tconst color = this.content.iconColor || this.content.valueColor || 'var(--color-primary-element)'\n\t\t\treturn { color, backgroundColor: this.tint(color) }\n\t\t},\n\n\t\t/**\n\t\t * Card orientation. Horizontal (icon beside the number) is the\n\t\t * canonical KPI card; `content.layout: 'vertical'` stacks the icon\n\t\t * above a centred number.\n\t\t *\n\t\t * @return {'horizontal'|'vertical'}\n\t\t */\n\t\tcardLayout() {\n\t\t\treturn (this.content || {}).layout === 'vertical' ? 'vertical' : 'horizontal'\n\t\t},\n\n\t\t/**\n\t\t * Whether the card draws no box of its own. On by default — the tile\n\t\t * is rendered inside a CnWidgetWrapper that already draws a card.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\tflat() {\n\t\t\treturn (this.content || {}).flat !== false\n\t\t},\n\n\t\t/** Inline colour for the value, when the tile names one. */\n\t\tvalueStyle() {\n\t\t\treturn this.content.valueColor ? { color: this.content.valueColor } : {}\n\t\t},\n\n\t\t/**\n\t\t * Whether the tile is endpoint-bound (Wave 2): a `content.endpointSource`\n\t\t * with a `url` reads both legs from one shared payload instead of the\n\t\t * two OpenRegister aggregation legs. Exactly one of `source` |\n\t\t * `endpointSource` is allowed (validator-enforced); endpointSource\n\t\t * wins when both slip through.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\tendpointMode() {\n\t\t\tconst es = this.content.endpointSource\n\t\t\treturn !!(es && es.url)\n\t\t},\n\n\t\t/**\n\t\t * The current value for the active source: the payload value at\n\t\t * `content.valueField` in endpoint mode, else the OpenRegister\n\t\t * `current` leg.\n\t\t *\n\t\t * @return {number|string|null}\n\t\t */\n\t\teffectiveCurrent() {\n\t\t\tif (!this.endpointMode) {\n\t\t\t\treturn this.current\n\t\t\t}\n\t\t\tconst v = getByPath(this.epData, this.content.valueField)\n\t\t\treturn v === undefined ? null : v\n\t\t},\n\n\t\t/**\n\t\t * The previous value for the active source: the payload value at\n\t\t * `content.previousField` in endpoint mode, else the OpenRegister\n\t\t * `previous` leg.\n\t\t *\n\t\t * @return {number|string|null}\n\t\t */\n\t\teffectivePrevious() {\n\t\t\tif (!this.endpointMode) {\n\t\t\t\treturn this.previous\n\t\t\t}\n\t\t\tconst v = getByPath(this.epData, this.content.previousField)\n\t\t\treturn v === undefined ? null : v\n\t\t},\n\n\t\t/**\n\t\t * Loading state for the active source (endpoint or OpenRegister).\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\tdisplayLoading() {\n\t\t\treturn this.endpointMode ? this.epLoading : this.loading\n\t\t},\n\n\t\t/**\n\t\t * Error message for the active source ('' = none).\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tdisplayError() {\n\t\t\treturn this.endpointMode ? this.epError : this.error\n\t\t},\n\n\t\t/** The current value, number-formatted per content.format. */\n\t\tformattedCurrent() {\n\t\t\treturn this.formatNumber(this.effectiveCurrent)\n\t\t},\n\n\t\t/**\n\t\t * Percentage change current vs previous, or null when not computable.\n\t\t * In endpoint mode a `content.deltaField` (a server-computed percent\n\t\t * plucked from the payload) wins over the client-side computation.\n\t\t */\n\t\tdeltaPct() {\n\t\t\tif (this.endpointMode && this.content.deltaField) {\n\t\t\t\tconst v = Number(getByPath(this.epData, this.content.deltaField))\n\t\t\t\treturn Number.isFinite(v) ? v : null\n\t\t\t}\n\t\t\tconst prev = Number(this.effectivePrevious)\n\t\t\tconst cur = Number(this.effectiveCurrent)\n\t\t\tif (!Number.isFinite(prev) || prev === 0 || !Number.isFinite(cur)) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\treturn ((cur - prev) / Math.abs(prev)) * 100\n\t\t},\n\n\t\t/** The signed percentage, e.g. \"+12.3%\". */\n\t\tformattedDelta() {\n\t\t\tif (this.deltaPct === null) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tconst sign = this.deltaPct > 0 ? '+' : ''\n\t\t\treturn `${sign}${this.deltaPct.toFixed(1)}%`\n\t\t},\n\n\t\t/** The arrow component for the delta direction. */\n\t\tdeltaIcon() {\n\t\t\tif (this.deltaPct === null || Math.abs(this.deltaPct) < 0.05) {\n\t\t\t\treturn 'TrendingNeutral'\n\t\t\t}\n\t\t\treturn this.deltaPct > 0 ? 'TrendingUp' : 'TrendingDown'\n\t\t},\n\n\t\t/**\n\t\t * Green when the change is in the configured good direction, else red.\n\t\t * `source.goodDirection` (the OpenRegister form) wins; endpoint-bound\n\t\t * tiles declare `content.goodDirection` at the top level.\n\t\t */\n\t\tdeltaColor() {\n\t\t\tif (this.deltaPct === null || Math.abs(this.deltaPct) < 0.05) {\n\t\t\t\treturn 'var(--color-text-maxcontrast)'\n\t\t\t}\n\t\t\tconst good = (this.content.source && this.content.source.goodDirection)\n\t\t\t\t|| this.content.goodDirection || 'up'\n\t\t\tconst rising = this.deltaPct > 0\n\t\t\tconst isGood = good === 'up' ? rising : !rising\n\t\t\treturn isGood ? STATUS_TEXT_COLORS.success : STATUS_TEXT_COLORS.error\n\t\t},\n\n\t\t/** Stable signature so the watcher only refetches on real change. */\n\t\tsourceKey() {\n\t\t\treturn JSON.stringify(this.content.source || {})\n\t\t},\n\n\t\t/** Merged detail-page object context (`{ objectId, object, register, schema }`) or null — both detail-surface injects (#91 Wave 3). */\n\t\tobjectCtx() {\n\t\t\treturn resolveObjectTokenContext(this.cnObjectContext, this.cnDetailObjectContext)\n\t\t},\n\n\t\t/** Unwrapped page-level workspace context map (always an object). */\n\t\tpageCtx() {\n\t\t\tconst c = this.cnWorkspaceContext\n\t\t\tconst v = (c && typeof c === 'object' && 'value' in c) ? c.value : c\n\t\t\treturn v || {}\n\t\t},\n\n\t\t/** Workspace signature so the widget refetches when the date window changes. */\n\t\tworkspaceKey() {\n\t\t\treturn JSON.stringify(this.pageCtx || {})\n\t\t},\n\t},\n\n\twatch: {\n\t\tsourceKey() {\n\t\t\tthis.fetchValues()\n\t\t},\n\n\t\tworkspaceKey() {\n\t\t\tthis.fetchValues()\n\t\t},\n\t},\n\n\tmounted() {\n\t\tthis.fetchValues()\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Derive a faint background tint for the icon circle from a colour.\n\t\t *\n\t\t * @param {string} color The base colour (hex or CSS var).\n\t\t * @return {string} A translucent or token background.\n\t\t */\n\t\ttint(color) {\n\t\t\tif (typeof color === 'string' && /^#([0-9a-f]{6})$/i.test(color)) {\n\t\t\t\treturn color + '1f'\n\t\t\t}\n\t\t\treturn 'var(--color-primary-element-light, rgba(0,130,201,0.1))'\n\t\t},\n\n\t\t/**\n\t\t * Format a number per the content.format spec (number/currency/percent).\n\t\t *\n\t\t * @param {number|string|null} value The raw value.\n\t\t * @return {string} The formatted string.\n\t\t */\n\t\tformatNumber(value) {\n\t\t\treturn formatMetricValue(value, this.resolvedFormat, unwrapAppConfig(this.cnAppConfig))\n\t\t},\n\n\t\t/**\n\t\t * Fetch the current and previous aggregates from OpenRegister.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync fetchValues() {\n\t\t\t// Endpoint-bound tiles are fetched by the shared useEndpointSource\n\t\t\t// engine (see setup) — the two-leg OpenRegister path must not fire.\n\t\t\tif (this.endpointMode) {\n\t\t\t\tthis.current = null\n\t\t\t\tthis.previous = null\n\t\t\t\tthis.error = ''\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst s = this.content.source || {}\n\t\t\tif (!s.register || !s.schema) {\n\t\t\t\tthis.current = null\n\t\t\t\tthis.previous = null\n\t\t\t\tthis.error = ''\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.loading = true\n\t\t\tthis.error = ''\n\t\t\ttry {\n\t\t\t\tconst base = { register: s.register, schema: s.schema, metric: s.metric, field: s.field }\n\t\t\t\t// Resolve `@workspace.*` (date-range pills) / `@objectId` / `@object.*`\n\t\t\t\t// per leg, then drop any optional `@workspace.<key>?` that stayed\n\t\t\t\t// unresolved (an unset window) so the leg falls back to its base\n\t\t\t\t// scope instead of sending a literal token. fetchAggregateValue does\n\t\t\t\t// NOT drop optional tokens itself, so we pre-resolve here.\n\t\t\t\tconst ctx = { ...(this.objectCtx || {}), workspace: this.pageCtx }\n\t\t\t\tconst curFilter = dropOptionalUnresolved(resolveFilterTokens((s.current && s.current.filter) || {}, ctx))\n\t\t\t\tconst prevFilter = dropOptionalUnresolved(resolveFilterTokens((s.previous && s.previous.filter) || {}, ctx))\n\t\t\t\tconst [cur, prev] = await Promise.all([\n\t\t\t\t\tfetchAggregateValue({ ...base, filter: curFilter }, ctx),\n\t\t\t\t\tfetchAggregateValue({ ...base, filter: prevFilter }, ctx),\n\t\t\t\t])\n\t\t\t\tthis.current = cur\n\t\t\t\tthis.previous = prev\n\t\t\t} catch (e) {\n\t\t\t\tthis.error = (e && e.message) || 'error'\n\t\t\t\tthis.current = null\n\t\t\t\tthis.previous = null\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n/*\n * The card itself — its box, body stack, icon circle, title and value\n * typography — is the CANONICAL KPI card in src/css/kpi-card.css, shared with\n * CnStatWidget and CnStatsBlock. This tile is the one the canonical look was\n * modelled on, and it used to draw that look from its OWN copy of the rules\n * here: a 44px circle, a 16px gap, a 1.8em value. That copy is exactly how a\n * \"canonical\" card ends up with a second definition, so it is gone. What is\n * left is only what belongs to THIS widget: the delta chip and the error glyph.\n */\n\n/* The percentage-change chip beside the value. Tinted good/bad at runtime from\n * `goodDirection`, so the colour is an inline style, not a class. */\n.cn-delta-widget__delta {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 2px;\n\tfont-size: var(--cn-kpi-label-size, 13px);\n\tfont-weight: var(--cn-kpi-title-weight, 600);\n}\n\n/* The dash shown when the number could not be fetched. It stands where the\n * value stands, so it takes the value's size and weight — only the colour\n * differs, because a failure is not a number. */\n.cn-delta-widget__error {\n\tfont-size: var(--cn-kpi-value-size, 2rem);\n\tfont-weight: var(--cn-kpi-value-weight, 700);\n\tline-height: var(--cn-kpi-value-line, 1.1);\n\tcolor: var(--cn-kpi-label-color, var(--color-text-maxcontrast));\n}\n</style>\n"],"names":["CnWidgetIcon","TrendingUp"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"CnDeltaWidget.vue2.js","sources":["../../../../src/components/CnDeltaWidget/CnDeltaWidget.vue"],"sourcesContent":["<!--\n SPDX-FileCopyrightText: 2026 Conduction B.V.\n SPDX-License-Identifier: EUPL-1.2\n-->\n<template>\n\t<component\n\t\t:is=\"linkTag\"\n\t\tclass=\"cn-kpi-card cn-delta-widget\"\n\t\t:class=\"[\n\t\t\t{ 'cn-kpi-card--clickable': isLinked, 'cn-delta-widget--linked': isLinked },\n\t\t\t'cn-kpi-card--' + cardLayout,\n\t\t\tflat ? 'cn-kpi-card--flat' : 'cn-kpi-card--filled',\n\t\t]\"\n\t\tv-bind=\"linkAttrs\">\n\t\t<div\n\t\t\tv-if=\"content.icon\"\n\t\t\tclass=\"cn-kpi-card__icon cn-delta-widget__icon\"\n\t\t\t:style=\"iconCircleStyle\">\n\t\t\t<CnWidgetIcon :name=\"content.icon\" :size=\"24\" />\n\t\t</div>\n\n\t\t<div class=\"cn-kpi-card__body cn-delta-widget__body\">\n\t\t\t<div v-if=\"content.label\" class=\"cn-kpi-card__title cn-delta-widget__label\" :title=\"resolvedLabel\">\n\t\t\t\t{{ resolvedLabel }}\n\t\t\t</div>\n\n\t\t\t<div class=\"cn-kpi-card__value-row cn-delta-widget__value-row\">\n\t\t\t\t<NcLoadingIcon v-if=\"displayLoading\" :size=\"22\" />\n\t\t\t\t<span v-else-if=\"displayError\" class=\"cn-delta-widget__error\" :title=\"displayError\">—</span>\n\t\t\t\t<template v-else>\n\t\t\t\t\t<span class=\"cn-kpi-card__value cn-delta-widget__value\" :style=\"valueStyle\">{{ formattedCurrent }}</span>\n\t\t\t\t\t<span\n\t\t\t\t\t\tv-if=\"deltaPct !== null\"\n\t\t\t\t\t\tclass=\"cn-delta-widget__delta\"\n\t\t\t\t\t\t:style=\"{ color: deltaColor }\">\n\t\t\t\t\t\t<component :is=\"deltaIcon\" :size=\"16\" />\n\t\t\t\t\t\t{{ formattedDelta }}\n\t\t\t\t\t</span>\n\t\t\t\t</template>\n\t\t\t</div>\n\n\t\t\t<div v-if=\"!displayLoading && !displayError && content.caption\" class=\"cn-kpi-card__label cn-delta-widget__caption\">\n\t\t\t\t{{ resolvedCaption }}\n\t\t\t</div>\n\t\t</div>\n\t</component>\n</template>\n\n<script>\nimport { NcLoadingIcon } from '@nextcloud/vue'\nimport { inject, ref } from 'vue'\nimport TrendingDown from 'vue-material-design-icons/TrendingDown.vue'\nimport TrendingNeutral from 'vue-material-design-icons/TrendingNeutral.vue'\nimport TrendingUp from 'vue-material-design-icons/TrendingUp.vue'\nimport CnWidgetIcon from '../CnWidgetGrid/CnWidgetIcon.vue'\nimport { getByPath, useEndpointSource } from '../../composables/useEndpointSource.js'\nimport widgetLink from '../../mixins/widgetLink.js'\nimport { resolveObjectTokenContext } from '../../utils/detailObjectContext.js'\nimport { fetchAggregateValue } from '../../utils/fetchAggregate.js'\nimport { formatMetricValue, unwrapAppConfig } from '../../utils/formatMetric.js'\nimport { dropOptionalUnresolved, resolveFilterTokens, resolveFilterValue } from '../../utils/resolveFilterTokens.js'\nimport { STATUS_TEXT_COLORS } from '../../utils/statusColors.js'\n\n// The canonical KPI card, shared with CnStatWidget and CnStatsBlock.\n// Imported here so the tile is styled even when the consuming app pulls in\n// components individually rather than through css/index.css.\nimport '../../css/kpi-card.css'\n\n/**\n * CnDeltaWidget — an abstract comparison / delta KPI tile.\n *\n * Reads TWO scalars from OpenRegister's aggregation `/value` endpoint — a\n * `current` value and a `previous` value — and renders the current value\n * alongside the percentage change between them, with a directional arrow tinted\n * good/bad according to `goodDirection`. Use it for period-over-period KPIs\n * (e.g. \"this month's revenue vs last month\") by combining relative filter\n * tokens (`@monthStart`, `@quarterStart`, …) in each leg's filter. Nothing is\n * hard-coded — every fleet app gets the same editable delta tile (ADR-041).\n *\n * Resolved by its registry type key `delta`; reference it from a manifest\n * placement `{ id, type: 'delta', content: { label, source, format, ... } }`.\n *\n * Example content blob:\n * ```js\n * content: {\n * label: 'Revenue (MTD)',\n * icon: 'Cash',\n * format: { style: 'currency', currency: 'EUR', decimals: 0 },\n * source: {\n * register: 'pipelinq', schema: 'lead', metric: 'sum', field: 'value',\n * goodDirection: 'up',\n * current: { filter: { status: 'won', closedAt: { gte: '@monthStart' } } },\n * previous: { filter: { status: 'won', closedAt: { gte: '@monthStart-1mo', lt: '@monthStart' } } },\n * },\n * }\n * ```\n *\n * ENDPOINT BINDING (Wave 2, #91) — instead of the two OpenRegister legs, the\n * tile can read BOTH values from one app REST payload through the shared\n * `useEndpointSource` engine (token-resolved params, request dedup + TTL\n * cache, `cn:page:refresh` / `cn:widget:refresh` subscription). Exactly ONE\n * of `source` | `endpointSource` may be configured (validator-enforced).\n * The pipelinq `previousPeriod` overview contract:\n * ```js\n * content: {\n * label: 'Revenue',\n * format: { style: 'currency', currency: 'EUR', decimals: 0 },\n * endpointSource: {\n * url: '/apps/pipelinq/api/analytics/commercial',\n * params: { period: '@workspace.datePreset?' },\n * },\n * valueField: 'revenue', // current value (dot-path)\n * previousField: 'previousPeriod.revenue', // previous value (dot-path)\n * // deltaField: 'revenueDeltaPct', // OR a server-computed percent\n * goodDirection: 'up',\n * }\n * ```\n */\nexport default {\n\tname: 'CnDeltaWidget',\n\n\tcomponents: {\n\t\tNcLoadingIcon,\n\t\tCnWidgetIcon,\n\t\tTrendingUp,\n\t\tTrendingDown,\n\t\tTrendingNeutral,\n\t},\n\n\tmixins: [widgetLink],\n\n\tinject: {\n\t\t/**\n\t\t * Detail-page object context (`{ objectId, object }`) for `@objectId` /\n\t\t * `@object.<field>` filter tokens. Null off a detail page.\n\t\t */\n\t\tcnObjectContext: { default: null },\n\t\t/**\n\t\t * v2 slot-grid detail context holder provided by CnPageRenderer —\n\t\t * backfills the object token context where CnDetailPage is not an\n\t\t * ancestor (#91 Wave 3).\n\t\t */\n\t\tcnDetailObjectContext: { default: null },\n\t\t/**\n\t\t * Page-level workspace context (a reactive `{ <key>: value }` map)\n\t\t * provided by CnDashboardPage. Drives `@workspace.<param>` token\n\t\t * resolution — e.g. the date-range pills publish `dateFrom` / `dateTo`,\n\t\t * so a leg filter can scope to the picked window.\n\t\t */\n\t\tcnWorkspaceContext: { default: () => ({}) },\n\t\t/**\n\t\t * Page-level app config map (reactive `{ <key>: value }`) provided by\n\t\t * CnDashboardPage / CnDetailPage. Resolves `@config.<key>` tokens in the\n\t\t * `format.currency` / `format.prefix` / `format.suffix` strings (e.g. the\n\t\t * reporting currency captured by the setup wizard). Defaults to `{}`.\n\t\t */\n\t\tcnAppConfig: { default: () => ({}) },\n\t\t/**\n\t\t * Host translate function provided by CnAppRoot as\n\t\t * `cnTranslate: this.translate` (bound to the host app's id). The\n\t\t * manifest-authored `content.label` / `content.caption` are run\n\t\t * through it, exactly as CnStatWidget does. Defaults to an identity\n\t\t * function so an untranslated key renders as itself.\n\t\t */\n\t\tcnTranslate: { default: () => (key) => key },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The widget's persisted configuration blob. An optional `route`\n\t\t * (vue-router location), `clickRoute` (Wave-2 alias), or `link`\n\t\t * (external href) turns the whole tile into a click-through target\n\t\t * (see the widgetLink mixin).\n\t\t *\n\t\t * Wave 2 (#91): `endpointSource` (`{ url, method?, params?,\n\t\t * responsePath? }`) reads BOTH legs from one app REST payload —\n\t\t * `valueField` plucks the current value, `previousField` the previous\n\t\t * value (the delta percent is computed client-side), or `deltaField`\n\t\t * supplies a server-computed percent directly. Exactly one of\n\t\t * `source` | `endpointSource`. `goodDirection` moves to the content\n\t\t * top level in endpoint mode (`source.goodDirection` still wins for\n\t\t * the OpenRegister form).\n\t\t *\n\t\t * @type {{label?: string, icon?: string, iconColor?: string, caption?: string, route?: (object|string), clickRoute?: (object|string), link?: string, format?: {style?: string, currency?: string, decimals?: number, prefix?: string, suffix?: string}, source?: {register?: string, schema?: string, metric?: string, field?: string, goodDirection?: ('up'|'down'), current?: {filter?: object}, previous?: {filter?: object}}, endpointSource?: {url: string, method?: string, params?: object, responsePath?: string}, valueField?: string, previousField?: string, deltaField?: string, goodDirection?: ('up'|'down')}}\n\t\t */\n\t\tcontent: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * Translate function. Falls back to the injected `cnTranslate`\n\t\t * (itself an identity function by default). Provide explicitly when\n\t\t * mounting CnDeltaWidget outside a CnAppRoot ancestor.\n\t\t *\n\t\t * @type {((key: string) => string)|null}\n\t\t */\n\t\ttranslate: {\n\t\t\ttype: Function,\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\tsetup(props) {\n\t\t// Endpoint binding (Wave 2): the shared useEndpointSource engine owns\n\t\t// token resolution, request dedup + TTL caching, and the\n\t\t// cn:page:refresh / cn:widget:refresh subscriptions. No-op while\n\t\t// `content.endpointSource` is absent, so the two-leg OpenRegister\n\t\t// path below is untouched. Injects re-read in setup (same resolution\n\t\t// as the Options `inject` block — the CnChartWidget precedent).\n\t\tconst objectCtxRaw = inject('cnObjectContext', null)\n\t\tconst detailCtxRaw = inject('cnDetailObjectContext', null)\n\t\tconst workspaceRaw = inject('cnWorkspaceContext', ref({}))\n\t\tconst appConfigRaw = inject('cnAppConfig', ref({}))\n\t\tconst unwrap = (v) => ((v && typeof v === 'object' && 'value' in v) ? v.value : v)\n\t\tconst { data, loading, error, refetch } = useEndpointSource(\n\t\t\t() => (props.content && props.content.endpointSource) || null,\n\t\t\t{\n\t\t\t\tctx: () => ({\n\t\t\t\t\t...(resolveObjectTokenContext(objectCtxRaw, detailCtxRaw) || {}),\n\t\t\t\t\tworkspace: unwrap(workspaceRaw) || {},\n\t\t\t\t\tconfig: unwrap(appConfigRaw) || {},\n\t\t\t\t}),\n\t\t\t},\n\t\t)\n\t\treturn { epData: data, epLoading: loading, epError: error, epRefetch: refetch }\n\t},\n\n\tdata() {\n\t\treturn {\n\t\t\tcurrent: null,\n\t\t\tprevious: null,\n\t\t\tloading: false,\n\t\t\terror: '',\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * Effective translate function: the explicit `translate` prop when\n\t\t * given, else the injected `cnTranslate` (identity by default).\n\t\t *\n\t\t * @return {(key: string) => string}\n\t\t */\n\t\teffectiveTranslate() {\n\t\t\treturn this.translate ?? this.cnTranslate\n\t\t},\n\n\t\t/**\n\t\t * The tile label, run through the host translate function so a\n\t\t * manifest-authored source string localises to the user's language.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tresolvedLabel() {\n\t\t\tconst label = this.content.label\n\t\t\treturn label ? this.effectiveTranslate(label) : ''\n\t\t},\n\n\t\t/**\n\t\t * The tile caption, run through the host translate function.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tresolvedCaption() {\n\t\t\tconst caption = this.content.caption\n\t\t\treturn caption ? this.effectiveTranslate(caption) : ''\n\t\t},\n\n\t\t/**\n\t\t * The unwrapped page-level app config map for `@config.*` token\n\t\t * resolution. Always an object (defaults to `{}`).\n\t\t *\n\t\t * @return {object}\n\t\t */\n\t\tconfigCtx() {\n\t\t\tconst c = this.cnAppConfig\n\t\t\tconst unwrapped = (c && typeof c === 'object' && 'value' in c) ? c.value : c\n\t\t\treturn (unwrapped && typeof unwrapped === 'object') ? unwrapped : {}\n\t\t},\n\n\t\t/**\n\t\t * The `content.format` spec with its `currency` / `prefix` / `suffix`\n\t\t * `@config.<key>` tokens resolved against the page-level app config. A\n\t\t * required token that stays unset is dropped so the downstream default\n\t\t * applies (EUR for currency, empty for prefix/suffix) rather than being\n\t\t * passed to `Intl.NumberFormat` as a literal `@config.…` (which throws).\n\t\t *\n\t\t * @return {object} The resolved format spec.\n\t\t */\n\t\tresolvedFormat() {\n\t\t\tconst fmt = this.content.format || {}\n\t\t\tconst ctx = { config: this.configCtx }\n\t\t\tconst out = { ...fmt }\n\t\t\tfor (const key of ['currency', 'prefix', 'suffix']) {\n\t\t\t\tconst raw = fmt[key]\n\t\t\t\tif (typeof raw !== 'string' || raw.charAt(0) !== '@') {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tconst resolved = resolveFilterValue(raw, ctx)\n\t\t\t\tout[key] = (typeof resolved === 'string' && resolved.charAt(0) === '@') ? undefined : resolved\n\t\t\t}\n\t\t\treturn out\n\t\t},\n\n\t\t/** Inline style for the icon circle (tinted with iconColor). */\n\t\ticonCircleStyle() {\n\t\t\tconst color = this.content.iconColor || this.content.valueColor || 'var(--color-primary-element)'\n\t\t\treturn { color, backgroundColor: this.tint(color) }\n\t\t},\n\n\t\t/**\n\t\t * Card orientation. Horizontal (icon beside the number) is the\n\t\t * canonical KPI card; `content.layout: 'vertical'` stacks the icon\n\t\t * above a centred number.\n\t\t *\n\t\t * @return {'horizontal'|'vertical'}\n\t\t */\n\t\tcardLayout() {\n\t\t\treturn (this.content || {}).layout === 'vertical' ? 'vertical' : 'horizontal'\n\t\t},\n\n\t\t/**\n\t\t * Whether the card draws no box of its own. On by default — the tile\n\t\t * is rendered inside a CnWidgetWrapper that already draws a card.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\tflat() {\n\t\t\treturn (this.content || {}).flat !== false\n\t\t},\n\n\t\t/** Inline colour for the value, when the tile names one. */\n\t\tvalueStyle() {\n\t\t\treturn this.content.valueColor ? { color: this.content.valueColor } : {}\n\t\t},\n\n\t\t/**\n\t\t * Whether the tile is endpoint-bound (Wave 2): a `content.endpointSource`\n\t\t * with a `url` reads both legs from one shared payload instead of the\n\t\t * two OpenRegister aggregation legs. Exactly one of `source` |\n\t\t * `endpointSource` is allowed (validator-enforced); endpointSource\n\t\t * wins when both slip through.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\tendpointMode() {\n\t\t\tconst es = this.content.endpointSource\n\t\t\treturn !!(es && es.url)\n\t\t},\n\n\t\t/**\n\t\t * The current value for the active source: the payload value at\n\t\t * `content.valueField` in endpoint mode, else the OpenRegister\n\t\t * `current` leg.\n\t\t *\n\t\t * @return {number|string|null}\n\t\t */\n\t\teffectiveCurrent() {\n\t\t\tif (!this.endpointMode) {\n\t\t\t\treturn this.current\n\t\t\t}\n\t\t\tconst v = getByPath(this.epData, this.content.valueField)\n\t\t\treturn v === undefined ? null : v\n\t\t},\n\n\t\t/**\n\t\t * The previous value for the active source: the payload value at\n\t\t * `content.previousField` in endpoint mode, else the OpenRegister\n\t\t * `previous` leg.\n\t\t *\n\t\t * @return {number|string|null}\n\t\t */\n\t\teffectivePrevious() {\n\t\t\tif (!this.endpointMode) {\n\t\t\t\treturn this.previous\n\t\t\t}\n\t\t\tconst v = getByPath(this.epData, this.content.previousField)\n\t\t\treturn v === undefined ? null : v\n\t\t},\n\n\t\t/**\n\t\t * Loading state for the active source (endpoint or OpenRegister).\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\tdisplayLoading() {\n\t\t\treturn this.endpointMode ? this.epLoading : this.loading\n\t\t},\n\n\t\t/**\n\t\t * Error message for the active source ('' = none).\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tdisplayError() {\n\t\t\treturn this.endpointMode ? this.epError : this.error\n\t\t},\n\n\t\t/** The current value, number-formatted per content.format. */\n\t\tformattedCurrent() {\n\t\t\treturn this.formatNumber(this.effectiveCurrent)\n\t\t},\n\n\t\t/**\n\t\t * Percentage change current vs previous, or null when not computable.\n\t\t * In endpoint mode a `content.deltaField` (a server-computed percent\n\t\t * plucked from the payload) wins over the client-side computation.\n\t\t */\n\t\tdeltaPct() {\n\t\t\tif (this.endpointMode && this.content.deltaField) {\n\t\t\t\tconst v = Number(getByPath(this.epData, this.content.deltaField))\n\t\t\t\treturn Number.isFinite(v) ? v : null\n\t\t\t}\n\t\t\tconst prev = Number(this.effectivePrevious)\n\t\t\tconst cur = Number(this.effectiveCurrent)\n\t\t\tif (!Number.isFinite(prev) || prev === 0 || !Number.isFinite(cur)) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\treturn ((cur - prev) / Math.abs(prev)) * 100\n\t\t},\n\n\t\t/** The signed percentage, e.g. \"+12.3%\". */\n\t\tformattedDelta() {\n\t\t\tif (this.deltaPct === null) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tconst sign = this.deltaPct > 0 ? '+' : ''\n\t\t\treturn `${sign}${this.deltaPct.toFixed(1)}%`\n\t\t},\n\n\t\t/** The arrow component for the delta direction. */\n\t\tdeltaIcon() {\n\t\t\tif (this.deltaPct === null || Math.abs(this.deltaPct) < 0.05) {\n\t\t\t\treturn 'TrendingNeutral'\n\t\t\t}\n\t\t\treturn this.deltaPct > 0 ? 'TrendingUp' : 'TrendingDown'\n\t\t},\n\n\t\t/**\n\t\t * Green when the change is in the configured good direction, else red.\n\t\t * `source.goodDirection` (the OpenRegister form) wins; endpoint-bound\n\t\t * tiles declare `content.goodDirection` at the top level.\n\t\t */\n\t\tdeltaColor() {\n\t\t\tif (this.deltaPct === null || Math.abs(this.deltaPct) < 0.05) {\n\t\t\t\treturn 'var(--color-text-maxcontrast)'\n\t\t\t}\n\t\t\tconst good = (this.content.source && this.content.source.goodDirection)\n\t\t\t\t|| this.content.goodDirection || 'up'\n\t\t\tconst rising = this.deltaPct > 0\n\t\t\tconst isGood = good === 'up' ? rising : !rising\n\t\t\treturn isGood ? STATUS_TEXT_COLORS.success : STATUS_TEXT_COLORS.error\n\t\t},\n\n\t\t/** Stable signature so the watcher only refetches on real change. */\n\t\tsourceKey() {\n\t\t\treturn JSON.stringify(this.content.source || {})\n\t\t},\n\n\t\t/** Merged detail-page object context (`{ objectId, object, register, schema }`) or null — both detail-surface injects (#91 Wave 3). */\n\t\tobjectCtx() {\n\t\t\treturn resolveObjectTokenContext(this.cnObjectContext, this.cnDetailObjectContext)\n\t\t},\n\n\t\t/** Unwrapped page-level workspace context map (always an object). */\n\t\tpageCtx() {\n\t\t\tconst c = this.cnWorkspaceContext\n\t\t\tconst v = (c && typeof c === 'object' && 'value' in c) ? c.value : c\n\t\t\treturn v || {}\n\t\t},\n\n\t\t/** Workspace signature so the widget refetches when the date window changes. */\n\t\tworkspaceKey() {\n\t\t\treturn JSON.stringify(this.pageCtx || {})\n\t\t},\n\t},\n\n\twatch: {\n\t\tsourceKey() {\n\t\t\tthis.fetchValues()\n\t\t},\n\n\t\tworkspaceKey() {\n\t\t\tthis.fetchValues()\n\t\t},\n\t},\n\n\tmounted() {\n\t\tthis.fetchValues()\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Derive a faint background tint for the icon circle from a colour.\n\t\t *\n\t\t * @param {string} color The base colour (hex or CSS var).\n\t\t * @return {string} A translucent or token background.\n\t\t */\n\t\ttint(color) {\n\t\t\tif (typeof color === 'string' && /^#([0-9a-f]{6})$/i.test(color)) {\n\t\t\t\treturn color + '1f'\n\t\t\t}\n\t\t\treturn 'var(--color-primary-element-light, rgba(0,130,201,0.1))'\n\t\t},\n\n\t\t/**\n\t\t * Format a number per the content.format spec (number/currency/percent).\n\t\t *\n\t\t * @param {number|string|null} value The raw value.\n\t\t * @return {string} The formatted string.\n\t\t */\n\t\tformatNumber(value) {\n\t\t\treturn formatMetricValue(value, this.resolvedFormat, unwrapAppConfig(this.cnAppConfig), null, this.effectiveTranslate)\n\t\t},\n\n\t\t/**\n\t\t * Fetch the current and previous aggregates from OpenRegister.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync fetchValues() {\n\t\t\t// Endpoint-bound tiles are fetched by the shared useEndpointSource\n\t\t\t// engine (see setup) — the two-leg OpenRegister path must not fire.\n\t\t\tif (this.endpointMode) {\n\t\t\t\tthis.current = null\n\t\t\t\tthis.previous = null\n\t\t\t\tthis.error = ''\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst s = this.content.source || {}\n\t\t\tif (!s.register || !s.schema) {\n\t\t\t\tthis.current = null\n\t\t\t\tthis.previous = null\n\t\t\t\tthis.error = ''\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.loading = true\n\t\t\tthis.error = ''\n\t\t\ttry {\n\t\t\t\tconst base = { register: s.register, schema: s.schema, metric: s.metric, field: s.field }\n\t\t\t\t// Resolve `@workspace.*` (date-range pills) / `@objectId` / `@object.*`\n\t\t\t\t// per leg, then drop any optional `@workspace.<key>?` that stayed\n\t\t\t\t// unresolved (an unset window) so the leg falls back to its base\n\t\t\t\t// scope instead of sending a literal token. fetchAggregateValue does\n\t\t\t\t// NOT drop optional tokens itself, so we pre-resolve here.\n\t\t\t\tconst ctx = { ...(this.objectCtx || {}), workspace: this.pageCtx }\n\t\t\t\tconst curFilter = dropOptionalUnresolved(resolveFilterTokens((s.current && s.current.filter) || {}, ctx))\n\t\t\t\tconst prevFilter = dropOptionalUnresolved(resolveFilterTokens((s.previous && s.previous.filter) || {}, ctx))\n\t\t\t\tconst [cur, prev] = await Promise.all([\n\t\t\t\t\tfetchAggregateValue({ ...base, filter: curFilter }, ctx),\n\t\t\t\t\tfetchAggregateValue({ ...base, filter: prevFilter }, ctx),\n\t\t\t\t])\n\t\t\t\tthis.current = cur\n\t\t\t\tthis.previous = prev\n\t\t\t} catch (e) {\n\t\t\t\tthis.error = (e && e.message) || 'error'\n\t\t\t\tthis.current = null\n\t\t\t\tthis.previous = null\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n/*\n * The card itself — its box, body stack, icon circle, title and value\n * typography — is the CANONICAL KPI card in src/css/kpi-card.css, shared with\n * CnStatWidget and CnStatsBlock. This tile is the one the canonical look was\n * modelled on, and it used to draw that look from its OWN copy of the rules\n * here: a 44px circle, a 16px gap, a 1.8em value. That copy is exactly how a\n * \"canonical\" card ends up with a second definition, so it is gone. What is\n * left is only what belongs to THIS widget: the delta chip and the error glyph.\n */\n\n/* The percentage-change chip beside the value. Tinted good/bad at runtime from\n * `goodDirection`, so the colour is an inline style, not a class. */\n.cn-delta-widget__delta {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 2px;\n\tfont-size: var(--cn-kpi-label-size, 13px);\n\tfont-weight: var(--cn-kpi-title-weight, 600);\n}\n\n/* The dash shown when the number could not be fetched. It stands where the\n * value stands, so it takes the value's size and weight — only the colour\n * differs, because a failure is not a number. */\n.cn-delta-widget__error {\n\tfont-size: var(--cn-kpi-value-size, 2rem);\n\tfont-weight: var(--cn-kpi-value-weight, 700);\n\tline-height: var(--cn-kpi-value-line, 1.1);\n\tcolor: var(--cn-kpi-label-color, var(--color-text-maxcontrast));\n}\n</style>\n"],"names":["CnWidgetIcon","TrendingUp"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CnDeltaWidget.vue3.js","sources":["../../../../src/components/CnDeltaWidget/CnDeltaWidget.vue?vue&type=template&id=66680dc6&lang.js"],"sourcesContent":["<!--\n SPDX-FileCopyrightText: 2026 Conduction B.V.\n SPDX-License-Identifier: EUPL-1.2\n-->\n<template>\n\t<component\n\t\t:is=\"linkTag\"\n\t\tclass=\"cn-kpi-card cn-delta-widget\"\n\t\t:class=\"[\n\t\t\t{ 'cn-kpi-card--clickable': isLinked, 'cn-delta-widget--linked': isLinked },\n\t\t\t'cn-kpi-card--' + cardLayout,\n\t\t\tflat ? 'cn-kpi-card--flat' : 'cn-kpi-card--filled',\n\t\t]\"\n\t\tv-bind=\"linkAttrs\">\n\t\t<div\n\t\t\tv-if=\"content.icon\"\n\t\t\tclass=\"cn-kpi-card__icon cn-delta-widget__icon\"\n\t\t\t:style=\"iconCircleStyle\">\n\t\t\t<CnWidgetIcon :name=\"content.icon\" :size=\"24\" />\n\t\t</div>\n\n\t\t<div class=\"cn-kpi-card__body cn-delta-widget__body\">\n\t\t\t<div v-if=\"content.label\" class=\"cn-kpi-card__title cn-delta-widget__label\" :title=\"resolvedLabel\">\n\t\t\t\t{{ resolvedLabel }}\n\t\t\t</div>\n\n\t\t\t<div class=\"cn-kpi-card__value-row cn-delta-widget__value-row\">\n\t\t\t\t<NcLoadingIcon v-if=\"displayLoading\" :size=\"22\" />\n\t\t\t\t<span v-else-if=\"displayError\" class=\"cn-delta-widget__error\" :title=\"displayError\">—</span>\n\t\t\t\t<template v-else>\n\t\t\t\t\t<span class=\"cn-kpi-card__value cn-delta-widget__value\" :style=\"valueStyle\">{{ formattedCurrent }}</span>\n\t\t\t\t\t<span\n\t\t\t\t\t\tv-if=\"deltaPct !== null\"\n\t\t\t\t\t\tclass=\"cn-delta-widget__delta\"\n\t\t\t\t\t\t:style=\"{ color: deltaColor }\">\n\t\t\t\t\t\t<component :is=\"deltaIcon\" :size=\"16\" />\n\t\t\t\t\t\t{{ formattedDelta }}\n\t\t\t\t\t</span>\n\t\t\t\t</template>\n\t\t\t</div>\n\n\t\t\t<div v-if=\"!displayLoading && !displayError && content.caption\" class=\"cn-kpi-card__label cn-delta-widget__caption\">\n\t\t\t\t{{ resolvedCaption }}\n\t\t\t</div>\n\t\t</div>\n\t</component>\n</template>\n\n<script>\nimport { NcLoadingIcon } from '@nextcloud/vue'\nimport { inject, ref } from 'vue'\nimport TrendingDown from 'vue-material-design-icons/TrendingDown.vue'\nimport TrendingNeutral from 'vue-material-design-icons/TrendingNeutral.vue'\nimport TrendingUp from 'vue-material-design-icons/TrendingUp.vue'\nimport CnWidgetIcon from '../CnWidgetGrid/CnWidgetIcon.vue'\nimport { getByPath, useEndpointSource } from '../../composables/useEndpointSource.js'\nimport widgetLink from '../../mixins/widgetLink.js'\nimport { resolveObjectTokenContext } from '../../utils/detailObjectContext.js'\nimport { fetchAggregateValue } from '../../utils/fetchAggregate.js'\nimport { formatMetricValue, unwrapAppConfig } from '../../utils/formatMetric.js'\nimport { dropOptionalUnresolved, resolveFilterTokens, resolveFilterValue } from '../../utils/resolveFilterTokens.js'\nimport { STATUS_TEXT_COLORS } from '../../utils/statusColors.js'\n\n// The canonical KPI card, shared with CnStatWidget and CnStatsBlock.\n// Imported here so the tile is styled even when the consuming app pulls in\n// components individually rather than through css/index.css.\nimport '../../css/kpi-card.css'\n\n/**\n * CnDeltaWidget — an abstract comparison / delta KPI tile.\n *\n * Reads TWO scalars from OpenRegister's aggregation `/value` endpoint — a\n * `current` value and a `previous` value — and renders the current value\n * alongside the percentage change between them, with a directional arrow tinted\n * good/bad according to `goodDirection`. Use it for period-over-period KPIs\n * (e.g. \"this month's revenue vs last month\") by combining relative filter\n * tokens (`@monthStart`, `@quarterStart`, …) in each leg's filter. Nothing is\n * hard-coded — every fleet app gets the same editable delta tile (ADR-041).\n *\n * Resolved by its registry type key `delta`; reference it from a manifest\n * placement `{ id, type: 'delta', content: { label, source, format, ... } }`.\n *\n * Example content blob:\n * ```js\n * content: {\n * label: 'Revenue (MTD)',\n * icon: 'Cash',\n * format: { style: 'currency', currency: 'EUR', decimals: 0 },\n * source: {\n * register: 'pipelinq', schema: 'lead', metric: 'sum', field: 'value',\n * goodDirection: 'up',\n * current: { filter: { status: 'won', closedAt: { gte: '@monthStart' } } },\n * previous: { filter: { status: 'won', closedAt: { gte: '@monthStart-1mo', lt: '@monthStart' } } },\n * },\n * }\n * ```\n *\n * ENDPOINT BINDING (Wave 2, #91) — instead of the two OpenRegister legs, the\n * tile can read BOTH values from one app REST payload through the shared\n * `useEndpointSource` engine (token-resolved params, request dedup + TTL\n * cache, `cn:page:refresh` / `cn:widget:refresh` subscription). Exactly ONE\n * of `source` | `endpointSource` may be configured (validator-enforced).\n * The pipelinq `previousPeriod` overview contract:\n * ```js\n * content: {\n * label: 'Revenue',\n * format: { style: 'currency', currency: 'EUR', decimals: 0 },\n * endpointSource: {\n * url: '/apps/pipelinq/api/analytics/commercial',\n * params: { period: '@workspace.datePreset?' },\n * },\n * valueField: 'revenue', // current value (dot-path)\n * previousField: 'previousPeriod.revenue', // previous value (dot-path)\n * // deltaField: 'revenueDeltaPct', // OR a server-computed percent\n * goodDirection: 'up',\n * }\n * ```\n */\nexport default {\n\tname: 'CnDeltaWidget',\n\n\tcomponents: {\n\t\tNcLoadingIcon,\n\t\tCnWidgetIcon,\n\t\tTrendingUp,\n\t\tTrendingDown,\n\t\tTrendingNeutral,\n\t},\n\n\tmixins: [widgetLink],\n\n\tinject: {\n\t\t/**\n\t\t * Detail-page object context (`{ objectId, object }`) for `@objectId` /\n\t\t * `@object.<field>` filter tokens. Null off a detail page.\n\t\t */\n\t\tcnObjectContext: { default: null },\n\t\t/**\n\t\t * v2 slot-grid detail context holder provided by CnPageRenderer —\n\t\t * backfills the object token context where CnDetailPage is not an\n\t\t * ancestor (#91 Wave 3).\n\t\t */\n\t\tcnDetailObjectContext: { default: null },\n\t\t/**\n\t\t * Page-level workspace context (a reactive `{ <key>: value }` map)\n\t\t * provided by CnDashboardPage. Drives `@workspace.<param>` token\n\t\t * resolution — e.g. the date-range pills publish `dateFrom` / `dateTo`,\n\t\t * so a leg filter can scope to the picked window.\n\t\t */\n\t\tcnWorkspaceContext: { default: () => ({}) },\n\t\t/**\n\t\t * Page-level app config map (reactive `{ <key>: value }`) provided by\n\t\t * CnDashboardPage / CnDetailPage. Resolves `@config.<key>` tokens in the\n\t\t * `format.currency` / `format.prefix` / `format.suffix` strings (e.g. the\n\t\t * reporting currency captured by the setup wizard). Defaults to `{}`.\n\t\t */\n\t\tcnAppConfig: { default: () => ({}) },\n\t\t/**\n\t\t * Host translate function provided by CnAppRoot as\n\t\t * `cnTranslate: this.translate` (bound to the host app's id). The\n\t\t * manifest-authored `content.label` / `content.caption` are run\n\t\t * through it, exactly as CnStatWidget does. Defaults to an identity\n\t\t * function so an untranslated key renders as itself.\n\t\t */\n\t\tcnTranslate: { default: () => (key) => key },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The widget's persisted configuration blob. An optional `route`\n\t\t * (vue-router location), `clickRoute` (Wave-2 alias), or `link`\n\t\t * (external href) turns the whole tile into a click-through target\n\t\t * (see the widgetLink mixin).\n\t\t *\n\t\t * Wave 2 (#91): `endpointSource` (`{ url, method?, params?,\n\t\t * responsePath? }`) reads BOTH legs from one app REST payload —\n\t\t * `valueField` plucks the current value, `previousField` the previous\n\t\t * value (the delta percent is computed client-side), or `deltaField`\n\t\t * supplies a server-computed percent directly. Exactly one of\n\t\t * `source` | `endpointSource`. `goodDirection` moves to the content\n\t\t * top level in endpoint mode (`source.goodDirection` still wins for\n\t\t * the OpenRegister form).\n\t\t *\n\t\t * @type {{label?: string, icon?: string, iconColor?: string, caption?: string, route?: (object|string), clickRoute?: (object|string), link?: string, format?: {style?: string, currency?: string, decimals?: number, prefix?: string, suffix?: string}, source?: {register?: string, schema?: string, metric?: string, field?: string, goodDirection?: ('up'|'down'), current?: {filter?: object}, previous?: {filter?: object}}, endpointSource?: {url: string, method?: string, params?: object, responsePath?: string}, valueField?: string, previousField?: string, deltaField?: string, goodDirection?: ('up'|'down')}}\n\t\t */\n\t\tcontent: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * Translate function. Falls back to the injected `cnTranslate`\n\t\t * (itself an identity function by default). Provide explicitly when\n\t\t * mounting CnDeltaWidget outside a CnAppRoot ancestor.\n\t\t *\n\t\t * @type {((key: string) => string)|null}\n\t\t */\n\t\ttranslate: {\n\t\t\ttype: Function,\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\tsetup(props) {\n\t\t// Endpoint binding (Wave 2): the shared useEndpointSource engine owns\n\t\t// token resolution, request dedup + TTL caching, and the\n\t\t// cn:page:refresh / cn:widget:refresh subscriptions. No-op while\n\t\t// `content.endpointSource` is absent, so the two-leg OpenRegister\n\t\t// path below is untouched. Injects re-read in setup (same resolution\n\t\t// as the Options `inject` block — the CnChartWidget precedent).\n\t\tconst objectCtxRaw = inject('cnObjectContext', null)\n\t\tconst detailCtxRaw = inject('cnDetailObjectContext', null)\n\t\tconst workspaceRaw = inject('cnWorkspaceContext', ref({}))\n\t\tconst appConfigRaw = inject('cnAppConfig', ref({}))\n\t\tconst unwrap = (v) => ((v && typeof v === 'object' && 'value' in v) ? v.value : v)\n\t\tconst { data, loading, error, refetch } = useEndpointSource(\n\t\t\t() => (props.content && props.content.endpointSource) || null,\n\t\t\t{\n\t\t\t\tctx: () => ({\n\t\t\t\t\t...(resolveObjectTokenContext(objectCtxRaw, detailCtxRaw) || {}),\n\t\t\t\t\tworkspace: unwrap(workspaceRaw) || {},\n\t\t\t\t\tconfig: unwrap(appConfigRaw) || {},\n\t\t\t\t}),\n\t\t\t},\n\t\t)\n\t\treturn { epData: data, epLoading: loading, epError: error, epRefetch: refetch }\n\t},\n\n\tdata() {\n\t\treturn {\n\t\t\tcurrent: null,\n\t\t\tprevious: null,\n\t\t\tloading: false,\n\t\t\terror: '',\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * Effective translate function: the explicit `translate` prop when\n\t\t * given, else the injected `cnTranslate` (identity by default).\n\t\t *\n\t\t * @return {(key: string) => string}\n\t\t */\n\t\teffectiveTranslate() {\n\t\t\treturn this.translate ?? this.cnTranslate\n\t\t},\n\n\t\t/**\n\t\t * The tile label, run through the host translate function so a\n\t\t * manifest-authored source string localises to the user's language.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tresolvedLabel() {\n\t\t\tconst label = this.content.label\n\t\t\treturn label ? this.effectiveTranslate(label) : ''\n\t\t},\n\n\t\t/**\n\t\t * The tile caption, run through the host translate function.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tresolvedCaption() {\n\t\t\tconst caption = this.content.caption\n\t\t\treturn caption ? this.effectiveTranslate(caption) : ''\n\t\t},\n\n\t\t/**\n\t\t * The unwrapped page-level app config map for `@config.*` token\n\t\t * resolution. Always an object (defaults to `{}`).\n\t\t *\n\t\t * @return {object}\n\t\t */\n\t\tconfigCtx() {\n\t\t\tconst c = this.cnAppConfig\n\t\t\tconst unwrapped = (c && typeof c === 'object' && 'value' in c) ? c.value : c\n\t\t\treturn (unwrapped && typeof unwrapped === 'object') ? unwrapped : {}\n\t\t},\n\n\t\t/**\n\t\t * The `content.format` spec with its `currency` / `prefix` / `suffix`\n\t\t * `@config.<key>` tokens resolved against the page-level app config. A\n\t\t * required token that stays unset is dropped so the downstream default\n\t\t * applies (EUR for currency, empty for prefix/suffix) rather than being\n\t\t * passed to `Intl.NumberFormat` as a literal `@config.…` (which throws).\n\t\t *\n\t\t * @return {object} The resolved format spec.\n\t\t */\n\t\tresolvedFormat() {\n\t\t\tconst fmt = this.content.format || {}\n\t\t\tconst ctx = { config: this.configCtx }\n\t\t\tconst out = { ...fmt }\n\t\t\tfor (const key of ['currency', 'prefix', 'suffix']) {\n\t\t\t\tconst raw = fmt[key]\n\t\t\t\tif (typeof raw !== 'string' || raw.charAt(0) !== '@') {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tconst resolved = resolveFilterValue(raw, ctx)\n\t\t\t\tout[key] = (typeof resolved === 'string' && resolved.charAt(0) === '@') ? undefined : resolved\n\t\t\t}\n\t\t\treturn out\n\t\t},\n\n\t\t/** Inline style for the icon circle (tinted with iconColor). */\n\t\ticonCircleStyle() {\n\t\t\tconst color = this.content.iconColor || this.content.valueColor || 'var(--color-primary-element)'\n\t\t\treturn { color, backgroundColor: this.tint(color) }\n\t\t},\n\n\t\t/**\n\t\t * Card orientation. Horizontal (icon beside the number) is the\n\t\t * canonical KPI card; `content.layout: 'vertical'` stacks the icon\n\t\t * above a centred number.\n\t\t *\n\t\t * @return {'horizontal'|'vertical'}\n\t\t */\n\t\tcardLayout() {\n\t\t\treturn (this.content || {}).layout === 'vertical' ? 'vertical' : 'horizontal'\n\t\t},\n\n\t\t/**\n\t\t * Whether the card draws no box of its own. On by default — the tile\n\t\t * is rendered inside a CnWidgetWrapper that already draws a card.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\tflat() {\n\t\t\treturn (this.content || {}).flat !== false\n\t\t},\n\n\t\t/** Inline colour for the value, when the tile names one. */\n\t\tvalueStyle() {\n\t\t\treturn this.content.valueColor ? { color: this.content.valueColor } : {}\n\t\t},\n\n\t\t/**\n\t\t * Whether the tile is endpoint-bound (Wave 2): a `content.endpointSource`\n\t\t * with a `url` reads both legs from one shared payload instead of the\n\t\t * two OpenRegister aggregation legs. Exactly one of `source` |\n\t\t * `endpointSource` is allowed (validator-enforced); endpointSource\n\t\t * wins when both slip through.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\tendpointMode() {\n\t\t\tconst es = this.content.endpointSource\n\t\t\treturn !!(es && es.url)\n\t\t},\n\n\t\t/**\n\t\t * The current value for the active source: the payload value at\n\t\t * `content.valueField` in endpoint mode, else the OpenRegister\n\t\t * `current` leg.\n\t\t *\n\t\t * @return {number|string|null}\n\t\t */\n\t\teffectiveCurrent() {\n\t\t\tif (!this.endpointMode) {\n\t\t\t\treturn this.current\n\t\t\t}\n\t\t\tconst v = getByPath(this.epData, this.content.valueField)\n\t\t\treturn v === undefined ? null : v\n\t\t},\n\n\t\t/**\n\t\t * The previous value for the active source: the payload value at\n\t\t * `content.previousField` in endpoint mode, else the OpenRegister\n\t\t * `previous` leg.\n\t\t *\n\t\t * @return {number|string|null}\n\t\t */\n\t\teffectivePrevious() {\n\t\t\tif (!this.endpointMode) {\n\t\t\t\treturn this.previous\n\t\t\t}\n\t\t\tconst v = getByPath(this.epData, this.content.previousField)\n\t\t\treturn v === undefined ? null : v\n\t\t},\n\n\t\t/**\n\t\t * Loading state for the active source (endpoint or OpenRegister).\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\tdisplayLoading() {\n\t\t\treturn this.endpointMode ? this.epLoading : this.loading\n\t\t},\n\n\t\t/**\n\t\t * Error message for the active source ('' = none).\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tdisplayError() {\n\t\t\treturn this.endpointMode ? this.epError : this.error\n\t\t},\n\n\t\t/** The current value, number-formatted per content.format. */\n\t\tformattedCurrent() {\n\t\t\treturn this.formatNumber(this.effectiveCurrent)\n\t\t},\n\n\t\t/**\n\t\t * Percentage change current vs previous, or null when not computable.\n\t\t * In endpoint mode a `content.deltaField` (a server-computed percent\n\t\t * plucked from the payload) wins over the client-side computation.\n\t\t */\n\t\tdeltaPct() {\n\t\t\tif (this.endpointMode && this.content.deltaField) {\n\t\t\t\tconst v = Number(getByPath(this.epData, this.content.deltaField))\n\t\t\t\treturn Number.isFinite(v) ? v : null\n\t\t\t}\n\t\t\tconst prev = Number(this.effectivePrevious)\n\t\t\tconst cur = Number(this.effectiveCurrent)\n\t\t\tif (!Number.isFinite(prev) || prev === 0 || !Number.isFinite(cur)) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\treturn ((cur - prev) / Math.abs(prev)) * 100\n\t\t},\n\n\t\t/** The signed percentage, e.g. \"+12.3%\". */\n\t\tformattedDelta() {\n\t\t\tif (this.deltaPct === null) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tconst sign = this.deltaPct > 0 ? '+' : ''\n\t\t\treturn `${sign}${this.deltaPct.toFixed(1)}%`\n\t\t},\n\n\t\t/** The arrow component for the delta direction. */\n\t\tdeltaIcon() {\n\t\t\tif (this.deltaPct === null || Math.abs(this.deltaPct) < 0.05) {\n\t\t\t\treturn 'TrendingNeutral'\n\t\t\t}\n\t\t\treturn this.deltaPct > 0 ? 'TrendingUp' : 'TrendingDown'\n\t\t},\n\n\t\t/**\n\t\t * Green when the change is in the configured good direction, else red.\n\t\t * `source.goodDirection` (the OpenRegister form) wins; endpoint-bound\n\t\t * tiles declare `content.goodDirection` at the top level.\n\t\t */\n\t\tdeltaColor() {\n\t\t\tif (this.deltaPct === null || Math.abs(this.deltaPct) < 0.05) {\n\t\t\t\treturn 'var(--color-text-maxcontrast)'\n\t\t\t}\n\t\t\tconst good = (this.content.source && this.content.source.goodDirection)\n\t\t\t\t|| this.content.goodDirection || 'up'\n\t\t\tconst rising = this.deltaPct > 0\n\t\t\tconst isGood = good === 'up' ? rising : !rising\n\t\t\treturn isGood ? STATUS_TEXT_COLORS.success : STATUS_TEXT_COLORS.error\n\t\t},\n\n\t\t/** Stable signature so the watcher only refetches on real change. */\n\t\tsourceKey() {\n\t\t\treturn JSON.stringify(this.content.source || {})\n\t\t},\n\n\t\t/** Merged detail-page object context (`{ objectId, object, register, schema }`) or null — both detail-surface injects (#91 Wave 3). */\n\t\tobjectCtx() {\n\t\t\treturn resolveObjectTokenContext(this.cnObjectContext, this.cnDetailObjectContext)\n\t\t},\n\n\t\t/** Unwrapped page-level workspace context map (always an object). */\n\t\tpageCtx() {\n\t\t\tconst c = this.cnWorkspaceContext\n\t\t\tconst v = (c && typeof c === 'object' && 'value' in c) ? c.value : c\n\t\t\treturn v || {}\n\t\t},\n\n\t\t/** Workspace signature so the widget refetches when the date window changes. */\n\t\tworkspaceKey() {\n\t\t\treturn JSON.stringify(this.pageCtx || {})\n\t\t},\n\t},\n\n\twatch: {\n\t\tsourceKey() {\n\t\t\tthis.fetchValues()\n\t\t},\n\n\t\tworkspaceKey() {\n\t\t\tthis.fetchValues()\n\t\t},\n\t},\n\n\tmounted() {\n\t\tthis.fetchValues()\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Derive a faint background tint for the icon circle from a colour.\n\t\t *\n\t\t * @param {string} color The base colour (hex or CSS var).\n\t\t * @return {string} A translucent or token background.\n\t\t */\n\t\ttint(color) {\n\t\t\tif (typeof color === 'string' && /^#([0-9a-f]{6})$/i.test(color)) {\n\t\t\t\treturn color + '1f'\n\t\t\t}\n\t\t\treturn 'var(--color-primary-element-light, rgba(0,130,201,0.1))'\n\t\t},\n\n\t\t/**\n\t\t * Format a number per the content.format spec (number/currency/percent).\n\t\t *\n\t\t * @param {number|string|null} value The raw value.\n\t\t * @return {string} The formatted string.\n\t\t */\n\t\tformatNumber(value) {\n\t\t\treturn formatMetricValue(value, this.resolvedFormat, unwrapAppConfig(this.cnAppConfig))\n\t\t},\n\n\t\t/**\n\t\t * Fetch the current and previous aggregates from OpenRegister.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync fetchValues() {\n\t\t\t// Endpoint-bound tiles are fetched by the shared useEndpointSource\n\t\t\t// engine (see setup) — the two-leg OpenRegister path must not fire.\n\t\t\tif (this.endpointMode) {\n\t\t\t\tthis.current = null\n\t\t\t\tthis.previous = null\n\t\t\t\tthis.error = ''\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst s = this.content.source || {}\n\t\t\tif (!s.register || !s.schema) {\n\t\t\t\tthis.current = null\n\t\t\t\tthis.previous = null\n\t\t\t\tthis.error = ''\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.loading = true\n\t\t\tthis.error = ''\n\t\t\ttry {\n\t\t\t\tconst base = { register: s.register, schema: s.schema, metric: s.metric, field: s.field }\n\t\t\t\t// Resolve `@workspace.*` (date-range pills) / `@objectId` / `@object.*`\n\t\t\t\t// per leg, then drop any optional `@workspace.<key>?` that stayed\n\t\t\t\t// unresolved (an unset window) so the leg falls back to its base\n\t\t\t\t// scope instead of sending a literal token. fetchAggregateValue does\n\t\t\t\t// NOT drop optional tokens itself, so we pre-resolve here.\n\t\t\t\tconst ctx = { ...(this.objectCtx || {}), workspace: this.pageCtx }\n\t\t\t\tconst curFilter = dropOptionalUnresolved(resolveFilterTokens((s.current && s.current.filter) || {}, ctx))\n\t\t\t\tconst prevFilter = dropOptionalUnresolved(resolveFilterTokens((s.previous && s.previous.filter) || {}, ctx))\n\t\t\t\tconst [cur, prev] = await Promise.all([\n\t\t\t\t\tfetchAggregateValue({ ...base, filter: curFilter }, ctx),\n\t\t\t\t\tfetchAggregateValue({ ...base, filter: prevFilter }, ctx),\n\t\t\t\t])\n\t\t\t\tthis.current = cur\n\t\t\t\tthis.previous = prev\n\t\t\t} catch (e) {\n\t\t\t\tthis.error = (e && e.message) || 'error'\n\t\t\t\tthis.current = null\n\t\t\t\tthis.previous = null\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n/*\n * The card itself — its box, body stack, icon circle, title and value\n * typography — is the CANONICAL KPI card in src/css/kpi-card.css, shared with\n * CnStatWidget and CnStatsBlock. This tile is the one the canonical look was\n * modelled on, and it used to draw that look from its OWN copy of the rules\n * here: a 44px circle, a 16px gap, a 1.8em value. That copy is exactly how a\n * \"canonical\" card ends up with a second definition, so it is gone. What is\n * left is only what belongs to THIS widget: the delta chip and the error glyph.\n */\n\n/* The percentage-change chip beside the value. Tinted good/bad at runtime from\n * `goodDirection`, so the colour is an inline style, not a class. */\n.cn-delta-widget__delta {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 2px;\n\tfont-size: var(--cn-kpi-label-size, 13px);\n\tfont-weight: var(--cn-kpi-title-weight, 600);\n}\n\n/* The dash shown when the number could not be fetched. It stands where the\n * value stands, so it takes the value's size and weight — only the colour\n * differs, because a failure is not a number. */\n.cn-delta-widget__error {\n\tfont-size: var(--cn-kpi-value-size, 2rem);\n\tfont-weight: var(--cn-kpi-value-weight, 700);\n\tline-height: var(--cn-kpi-value-line, 1.1);\n\tcolor: var(--cn-kpi-label-color, var(--color-text-maxcontrast));\n}\n</style>\n"],"names":["_createBlock","_mergeProps","_createElementBlock","_createVNode","_Fragment","_openBlock","_resolveDynamicComponent","_createTextVNode"],"mappings":";;;;AA8BQ,MAAA,UAAA,GAAA,EAAA,KAAK,EAAC,mDAAmD,GAAA;;;;AAerD,EAAA,KAAA,EAAA,6CAAA;;;;;;;uBApCTA,oCACI,IAAO,CAAA,OAAA,CAAA,EADXC;SAEK,GAAA,6BAA6B,EAAA;AAClC,GAAA,EAAA,wBAAA,EAAA,IAAA,CAAA,QAAA,6BAAA,IAAA,CAAA,QAAA,EAAA;qBAAA,QAAA,CAAA,UAAA;GAAA,QAAA,CAAA,IAAA,GAAA,mBAAA,GAAA,qBAAA;;KAKO,IAAS,CAAA,SAAA,CAAA,EAAA;qBAChB,MAKK;AAJD,MAAA,CAAA,MAAA,CAAA,OAAA,CAAA,IAAA;wBADJC,kBAKK,CAAA,KAAA,EAAA;;;AAFJ,YAAA,KAAK,iBAAE,QAAe,CAAA,eAAA,CAAA;;YACvBC,WAAgD,CAAA,uBAAA,EAAA;cAAjC,IAAI,EAAE,MAAO,CAAA,OAAA,CAAC,IAAI;AAAG,cAAA,IAAI,EAAE,EAAE;;;;;AAIjC,QAAA,CAAA,MAAA,CAAA,OAAO,CAAC,KAAK;0BAAxBD;;AAA0B,cAAA,KAAK,EAAC,2CAA8B;AAAA,cAAA,KAAA,EAAA,QAAA,CAAA,aAAA;+BAC1D,QAAa,CAAA,aAAA,CAAA,EAAA,CAAA,oBAAA,UAAA,CAAA;;2BAgBZ,iBAAA,EAAA;WAZC,QAAA,CAAA,cAAA;uCAAA,CAAA,wBAAA,EAAA;;AAAA,gBAAA,IAAA,EAAA,EAAA;;eACY,QAAQ,CAAA,YAAA;8BAAvBA,kBAAuB,CAAA,MAAA,EAAA;;AAAA,kBAAA,KAAA,EAAA,wBAAA;AAAA,kBAAA,KAAA,EAAA,QAAA,CAAA,YAAA;mBAAA,GAAA,EAAA,CAAA,cAAA,UAAA,CAAA;gDAUd,CAAAE,QAAA,EAAA,EAAA,GAAA,EAAA,CAAA,EAAA,EAAA;oCARqB,CAAA,MAAA,EAAA;AAAzB,oBAAA,KAAK,EAAC,2CAAmB;AAAA,oBAAA,KAAA,iBAAA,QAAA,CAAA,UAAA,CAAA;qCAAA,QAAA,CAAA,gBAAA,CAAA,EAAA,CAAA,mBAAA;mBAExB,QAAQ,CAAA,QAAA,KAAA,IAAA;oCADdF,kBAMM,CAAA,MAAA,EAAA;;AAJN,wBAAA,KAAK,EAAA,wBAAA;AACJ,wBAAA,KAAK,0BAAE,QAAA,CAAA,UAAA,EAAA,CAAA;;AACR,yBAAAG,SAAA,EAAA,EAAAL,WAAA,CAAEM,uBAAA,CAAA,QAAA,CAAA,SAAA,CAAA,EAAA,EAAA,IAAA,EAAA,EAAA,EAAA,CAAA;AAAA,wBAAAC,eAAA,CAAA;;;;;AAMI,QAAA,CAAA,CAAA,QAAA,CAAA,cAAA,IAAA,CAAA,QAAA,CAAA,YAAA,IAAA,MAAA,CAAA,OAAA,CAAA,OAAA;0BAATL,0BAAA;;;;;;;;;;"}
|
|
1
|
+
{"version":3,"file":"CnDeltaWidget.vue3.js","sources":["../../../../src/components/CnDeltaWidget/CnDeltaWidget.vue?vue&type=template&id=ee481872&lang.js"],"sourcesContent":["<!--\n SPDX-FileCopyrightText: 2026 Conduction B.V.\n SPDX-License-Identifier: EUPL-1.2\n-->\n<template>\n\t<component\n\t\t:is=\"linkTag\"\n\t\tclass=\"cn-kpi-card cn-delta-widget\"\n\t\t:class=\"[\n\t\t\t{ 'cn-kpi-card--clickable': isLinked, 'cn-delta-widget--linked': isLinked },\n\t\t\t'cn-kpi-card--' + cardLayout,\n\t\t\tflat ? 'cn-kpi-card--flat' : 'cn-kpi-card--filled',\n\t\t]\"\n\t\tv-bind=\"linkAttrs\">\n\t\t<div\n\t\t\tv-if=\"content.icon\"\n\t\t\tclass=\"cn-kpi-card__icon cn-delta-widget__icon\"\n\t\t\t:style=\"iconCircleStyle\">\n\t\t\t<CnWidgetIcon :name=\"content.icon\" :size=\"24\" />\n\t\t</div>\n\n\t\t<div class=\"cn-kpi-card__body cn-delta-widget__body\">\n\t\t\t<div v-if=\"content.label\" class=\"cn-kpi-card__title cn-delta-widget__label\" :title=\"resolvedLabel\">\n\t\t\t\t{{ resolvedLabel }}\n\t\t\t</div>\n\n\t\t\t<div class=\"cn-kpi-card__value-row cn-delta-widget__value-row\">\n\t\t\t\t<NcLoadingIcon v-if=\"displayLoading\" :size=\"22\" />\n\t\t\t\t<span v-else-if=\"displayError\" class=\"cn-delta-widget__error\" :title=\"displayError\">—</span>\n\t\t\t\t<template v-else>\n\t\t\t\t\t<span class=\"cn-kpi-card__value cn-delta-widget__value\" :style=\"valueStyle\">{{ formattedCurrent }}</span>\n\t\t\t\t\t<span\n\t\t\t\t\t\tv-if=\"deltaPct !== null\"\n\t\t\t\t\t\tclass=\"cn-delta-widget__delta\"\n\t\t\t\t\t\t:style=\"{ color: deltaColor }\">\n\t\t\t\t\t\t<component :is=\"deltaIcon\" :size=\"16\" />\n\t\t\t\t\t\t{{ formattedDelta }}\n\t\t\t\t\t</span>\n\t\t\t\t</template>\n\t\t\t</div>\n\n\t\t\t<div v-if=\"!displayLoading && !displayError && content.caption\" class=\"cn-kpi-card__label cn-delta-widget__caption\">\n\t\t\t\t{{ resolvedCaption }}\n\t\t\t</div>\n\t\t</div>\n\t</component>\n</template>\n\n<script>\nimport { NcLoadingIcon } from '@nextcloud/vue'\nimport { inject, ref } from 'vue'\nimport TrendingDown from 'vue-material-design-icons/TrendingDown.vue'\nimport TrendingNeutral from 'vue-material-design-icons/TrendingNeutral.vue'\nimport TrendingUp from 'vue-material-design-icons/TrendingUp.vue'\nimport CnWidgetIcon from '../CnWidgetGrid/CnWidgetIcon.vue'\nimport { getByPath, useEndpointSource } from '../../composables/useEndpointSource.js'\nimport widgetLink from '../../mixins/widgetLink.js'\nimport { resolveObjectTokenContext } from '../../utils/detailObjectContext.js'\nimport { fetchAggregateValue } from '../../utils/fetchAggregate.js'\nimport { formatMetricValue, unwrapAppConfig } from '../../utils/formatMetric.js'\nimport { dropOptionalUnresolved, resolveFilterTokens, resolveFilterValue } from '../../utils/resolveFilterTokens.js'\nimport { STATUS_TEXT_COLORS } from '../../utils/statusColors.js'\n\n// The canonical KPI card, shared with CnStatWidget and CnStatsBlock.\n// Imported here so the tile is styled even when the consuming app pulls in\n// components individually rather than through css/index.css.\nimport '../../css/kpi-card.css'\n\n/**\n * CnDeltaWidget — an abstract comparison / delta KPI tile.\n *\n * Reads TWO scalars from OpenRegister's aggregation `/value` endpoint — a\n * `current` value and a `previous` value — and renders the current value\n * alongside the percentage change between them, with a directional arrow tinted\n * good/bad according to `goodDirection`. Use it for period-over-period KPIs\n * (e.g. \"this month's revenue vs last month\") by combining relative filter\n * tokens (`@monthStart`, `@quarterStart`, …) in each leg's filter. Nothing is\n * hard-coded — every fleet app gets the same editable delta tile (ADR-041).\n *\n * Resolved by its registry type key `delta`; reference it from a manifest\n * placement `{ id, type: 'delta', content: { label, source, format, ... } }`.\n *\n * Example content blob:\n * ```js\n * content: {\n * label: 'Revenue (MTD)',\n * icon: 'Cash',\n * format: { style: 'currency', currency: 'EUR', decimals: 0 },\n * source: {\n * register: 'pipelinq', schema: 'lead', metric: 'sum', field: 'value',\n * goodDirection: 'up',\n * current: { filter: { status: 'won', closedAt: { gte: '@monthStart' } } },\n * previous: { filter: { status: 'won', closedAt: { gte: '@monthStart-1mo', lt: '@monthStart' } } },\n * },\n * }\n * ```\n *\n * ENDPOINT BINDING (Wave 2, #91) — instead of the two OpenRegister legs, the\n * tile can read BOTH values from one app REST payload through the shared\n * `useEndpointSource` engine (token-resolved params, request dedup + TTL\n * cache, `cn:page:refresh` / `cn:widget:refresh` subscription). Exactly ONE\n * of `source` | `endpointSource` may be configured (validator-enforced).\n * The pipelinq `previousPeriod` overview contract:\n * ```js\n * content: {\n * label: 'Revenue',\n * format: { style: 'currency', currency: 'EUR', decimals: 0 },\n * endpointSource: {\n * url: '/apps/pipelinq/api/analytics/commercial',\n * params: { period: '@workspace.datePreset?' },\n * },\n * valueField: 'revenue', // current value (dot-path)\n * previousField: 'previousPeriod.revenue', // previous value (dot-path)\n * // deltaField: 'revenueDeltaPct', // OR a server-computed percent\n * goodDirection: 'up',\n * }\n * ```\n */\nexport default {\n\tname: 'CnDeltaWidget',\n\n\tcomponents: {\n\t\tNcLoadingIcon,\n\t\tCnWidgetIcon,\n\t\tTrendingUp,\n\t\tTrendingDown,\n\t\tTrendingNeutral,\n\t},\n\n\tmixins: [widgetLink],\n\n\tinject: {\n\t\t/**\n\t\t * Detail-page object context (`{ objectId, object }`) for `@objectId` /\n\t\t * `@object.<field>` filter tokens. Null off a detail page.\n\t\t */\n\t\tcnObjectContext: { default: null },\n\t\t/**\n\t\t * v2 slot-grid detail context holder provided by CnPageRenderer —\n\t\t * backfills the object token context where CnDetailPage is not an\n\t\t * ancestor (#91 Wave 3).\n\t\t */\n\t\tcnDetailObjectContext: { default: null },\n\t\t/**\n\t\t * Page-level workspace context (a reactive `{ <key>: value }` map)\n\t\t * provided by CnDashboardPage. Drives `@workspace.<param>` token\n\t\t * resolution — e.g. the date-range pills publish `dateFrom` / `dateTo`,\n\t\t * so a leg filter can scope to the picked window.\n\t\t */\n\t\tcnWorkspaceContext: { default: () => ({}) },\n\t\t/**\n\t\t * Page-level app config map (reactive `{ <key>: value }`) provided by\n\t\t * CnDashboardPage / CnDetailPage. Resolves `@config.<key>` tokens in the\n\t\t * `format.currency` / `format.prefix` / `format.suffix` strings (e.g. the\n\t\t * reporting currency captured by the setup wizard). Defaults to `{}`.\n\t\t */\n\t\tcnAppConfig: { default: () => ({}) },\n\t\t/**\n\t\t * Host translate function provided by CnAppRoot as\n\t\t * `cnTranslate: this.translate` (bound to the host app's id). The\n\t\t * manifest-authored `content.label` / `content.caption` are run\n\t\t * through it, exactly as CnStatWidget does. Defaults to an identity\n\t\t * function so an untranslated key renders as itself.\n\t\t */\n\t\tcnTranslate: { default: () => (key) => key },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The widget's persisted configuration blob. An optional `route`\n\t\t * (vue-router location), `clickRoute` (Wave-2 alias), or `link`\n\t\t * (external href) turns the whole tile into a click-through target\n\t\t * (see the widgetLink mixin).\n\t\t *\n\t\t * Wave 2 (#91): `endpointSource` (`{ url, method?, params?,\n\t\t * responsePath? }`) reads BOTH legs from one app REST payload —\n\t\t * `valueField` plucks the current value, `previousField` the previous\n\t\t * value (the delta percent is computed client-side), or `deltaField`\n\t\t * supplies a server-computed percent directly. Exactly one of\n\t\t * `source` | `endpointSource`. `goodDirection` moves to the content\n\t\t * top level in endpoint mode (`source.goodDirection` still wins for\n\t\t * the OpenRegister form).\n\t\t *\n\t\t * @type {{label?: string, icon?: string, iconColor?: string, caption?: string, route?: (object|string), clickRoute?: (object|string), link?: string, format?: {style?: string, currency?: string, decimals?: number, prefix?: string, suffix?: string}, source?: {register?: string, schema?: string, metric?: string, field?: string, goodDirection?: ('up'|'down'), current?: {filter?: object}, previous?: {filter?: object}}, endpointSource?: {url: string, method?: string, params?: object, responsePath?: string}, valueField?: string, previousField?: string, deltaField?: string, goodDirection?: ('up'|'down')}}\n\t\t */\n\t\tcontent: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * Translate function. Falls back to the injected `cnTranslate`\n\t\t * (itself an identity function by default). Provide explicitly when\n\t\t * mounting CnDeltaWidget outside a CnAppRoot ancestor.\n\t\t *\n\t\t * @type {((key: string) => string)|null}\n\t\t */\n\t\ttranslate: {\n\t\t\ttype: Function,\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\tsetup(props) {\n\t\t// Endpoint binding (Wave 2): the shared useEndpointSource engine owns\n\t\t// token resolution, request dedup + TTL caching, and the\n\t\t// cn:page:refresh / cn:widget:refresh subscriptions. No-op while\n\t\t// `content.endpointSource` is absent, so the two-leg OpenRegister\n\t\t// path below is untouched. Injects re-read in setup (same resolution\n\t\t// as the Options `inject` block — the CnChartWidget precedent).\n\t\tconst objectCtxRaw = inject('cnObjectContext', null)\n\t\tconst detailCtxRaw = inject('cnDetailObjectContext', null)\n\t\tconst workspaceRaw = inject('cnWorkspaceContext', ref({}))\n\t\tconst appConfigRaw = inject('cnAppConfig', ref({}))\n\t\tconst unwrap = (v) => ((v && typeof v === 'object' && 'value' in v) ? v.value : v)\n\t\tconst { data, loading, error, refetch } = useEndpointSource(\n\t\t\t() => (props.content && props.content.endpointSource) || null,\n\t\t\t{\n\t\t\t\tctx: () => ({\n\t\t\t\t\t...(resolveObjectTokenContext(objectCtxRaw, detailCtxRaw) || {}),\n\t\t\t\t\tworkspace: unwrap(workspaceRaw) || {},\n\t\t\t\t\tconfig: unwrap(appConfigRaw) || {},\n\t\t\t\t}),\n\t\t\t},\n\t\t)\n\t\treturn { epData: data, epLoading: loading, epError: error, epRefetch: refetch }\n\t},\n\n\tdata() {\n\t\treturn {\n\t\t\tcurrent: null,\n\t\t\tprevious: null,\n\t\t\tloading: false,\n\t\t\terror: '',\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * Effective translate function: the explicit `translate` prop when\n\t\t * given, else the injected `cnTranslate` (identity by default).\n\t\t *\n\t\t * @return {(key: string) => string}\n\t\t */\n\t\teffectiveTranslate() {\n\t\t\treturn this.translate ?? this.cnTranslate\n\t\t},\n\n\t\t/**\n\t\t * The tile label, run through the host translate function so a\n\t\t * manifest-authored source string localises to the user's language.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tresolvedLabel() {\n\t\t\tconst label = this.content.label\n\t\t\treturn label ? this.effectiveTranslate(label) : ''\n\t\t},\n\n\t\t/**\n\t\t * The tile caption, run through the host translate function.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tresolvedCaption() {\n\t\t\tconst caption = this.content.caption\n\t\t\treturn caption ? this.effectiveTranslate(caption) : ''\n\t\t},\n\n\t\t/**\n\t\t * The unwrapped page-level app config map for `@config.*` token\n\t\t * resolution. Always an object (defaults to `{}`).\n\t\t *\n\t\t * @return {object}\n\t\t */\n\t\tconfigCtx() {\n\t\t\tconst c = this.cnAppConfig\n\t\t\tconst unwrapped = (c && typeof c === 'object' && 'value' in c) ? c.value : c\n\t\t\treturn (unwrapped && typeof unwrapped === 'object') ? unwrapped : {}\n\t\t},\n\n\t\t/**\n\t\t * The `content.format` spec with its `currency` / `prefix` / `suffix`\n\t\t * `@config.<key>` tokens resolved against the page-level app config. A\n\t\t * required token that stays unset is dropped so the downstream default\n\t\t * applies (EUR for currency, empty for prefix/suffix) rather than being\n\t\t * passed to `Intl.NumberFormat` as a literal `@config.…` (which throws).\n\t\t *\n\t\t * @return {object} The resolved format spec.\n\t\t */\n\t\tresolvedFormat() {\n\t\t\tconst fmt = this.content.format || {}\n\t\t\tconst ctx = { config: this.configCtx }\n\t\t\tconst out = { ...fmt }\n\t\t\tfor (const key of ['currency', 'prefix', 'suffix']) {\n\t\t\t\tconst raw = fmt[key]\n\t\t\t\tif (typeof raw !== 'string' || raw.charAt(0) !== '@') {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tconst resolved = resolveFilterValue(raw, ctx)\n\t\t\t\tout[key] = (typeof resolved === 'string' && resolved.charAt(0) === '@') ? undefined : resolved\n\t\t\t}\n\t\t\treturn out\n\t\t},\n\n\t\t/** Inline style for the icon circle (tinted with iconColor). */\n\t\ticonCircleStyle() {\n\t\t\tconst color = this.content.iconColor || this.content.valueColor || 'var(--color-primary-element)'\n\t\t\treturn { color, backgroundColor: this.tint(color) }\n\t\t},\n\n\t\t/**\n\t\t * Card orientation. Horizontal (icon beside the number) is the\n\t\t * canonical KPI card; `content.layout: 'vertical'` stacks the icon\n\t\t * above a centred number.\n\t\t *\n\t\t * @return {'horizontal'|'vertical'}\n\t\t */\n\t\tcardLayout() {\n\t\t\treturn (this.content || {}).layout === 'vertical' ? 'vertical' : 'horizontal'\n\t\t},\n\n\t\t/**\n\t\t * Whether the card draws no box of its own. On by default — the tile\n\t\t * is rendered inside a CnWidgetWrapper that already draws a card.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\tflat() {\n\t\t\treturn (this.content || {}).flat !== false\n\t\t},\n\n\t\t/** Inline colour for the value, when the tile names one. */\n\t\tvalueStyle() {\n\t\t\treturn this.content.valueColor ? { color: this.content.valueColor } : {}\n\t\t},\n\n\t\t/**\n\t\t * Whether the tile is endpoint-bound (Wave 2): a `content.endpointSource`\n\t\t * with a `url` reads both legs from one shared payload instead of the\n\t\t * two OpenRegister aggregation legs. Exactly one of `source` |\n\t\t * `endpointSource` is allowed (validator-enforced); endpointSource\n\t\t * wins when both slip through.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\tendpointMode() {\n\t\t\tconst es = this.content.endpointSource\n\t\t\treturn !!(es && es.url)\n\t\t},\n\n\t\t/**\n\t\t * The current value for the active source: the payload value at\n\t\t * `content.valueField` in endpoint mode, else the OpenRegister\n\t\t * `current` leg.\n\t\t *\n\t\t * @return {number|string|null}\n\t\t */\n\t\teffectiveCurrent() {\n\t\t\tif (!this.endpointMode) {\n\t\t\t\treturn this.current\n\t\t\t}\n\t\t\tconst v = getByPath(this.epData, this.content.valueField)\n\t\t\treturn v === undefined ? null : v\n\t\t},\n\n\t\t/**\n\t\t * The previous value for the active source: the payload value at\n\t\t * `content.previousField` in endpoint mode, else the OpenRegister\n\t\t * `previous` leg.\n\t\t *\n\t\t * @return {number|string|null}\n\t\t */\n\t\teffectivePrevious() {\n\t\t\tif (!this.endpointMode) {\n\t\t\t\treturn this.previous\n\t\t\t}\n\t\t\tconst v = getByPath(this.epData, this.content.previousField)\n\t\t\treturn v === undefined ? null : v\n\t\t},\n\n\t\t/**\n\t\t * Loading state for the active source (endpoint or OpenRegister).\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\tdisplayLoading() {\n\t\t\treturn this.endpointMode ? this.epLoading : this.loading\n\t\t},\n\n\t\t/**\n\t\t * Error message for the active source ('' = none).\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tdisplayError() {\n\t\t\treturn this.endpointMode ? this.epError : this.error\n\t\t},\n\n\t\t/** The current value, number-formatted per content.format. */\n\t\tformattedCurrent() {\n\t\t\treturn this.formatNumber(this.effectiveCurrent)\n\t\t},\n\n\t\t/**\n\t\t * Percentage change current vs previous, or null when not computable.\n\t\t * In endpoint mode a `content.deltaField` (a server-computed percent\n\t\t * plucked from the payload) wins over the client-side computation.\n\t\t */\n\t\tdeltaPct() {\n\t\t\tif (this.endpointMode && this.content.deltaField) {\n\t\t\t\tconst v = Number(getByPath(this.epData, this.content.deltaField))\n\t\t\t\treturn Number.isFinite(v) ? v : null\n\t\t\t}\n\t\t\tconst prev = Number(this.effectivePrevious)\n\t\t\tconst cur = Number(this.effectiveCurrent)\n\t\t\tif (!Number.isFinite(prev) || prev === 0 || !Number.isFinite(cur)) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\treturn ((cur - prev) / Math.abs(prev)) * 100\n\t\t},\n\n\t\t/** The signed percentage, e.g. \"+12.3%\". */\n\t\tformattedDelta() {\n\t\t\tif (this.deltaPct === null) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tconst sign = this.deltaPct > 0 ? '+' : ''\n\t\t\treturn `${sign}${this.deltaPct.toFixed(1)}%`\n\t\t},\n\n\t\t/** The arrow component for the delta direction. */\n\t\tdeltaIcon() {\n\t\t\tif (this.deltaPct === null || Math.abs(this.deltaPct) < 0.05) {\n\t\t\t\treturn 'TrendingNeutral'\n\t\t\t}\n\t\t\treturn this.deltaPct > 0 ? 'TrendingUp' : 'TrendingDown'\n\t\t},\n\n\t\t/**\n\t\t * Green when the change is in the configured good direction, else red.\n\t\t * `source.goodDirection` (the OpenRegister form) wins; endpoint-bound\n\t\t * tiles declare `content.goodDirection` at the top level.\n\t\t */\n\t\tdeltaColor() {\n\t\t\tif (this.deltaPct === null || Math.abs(this.deltaPct) < 0.05) {\n\t\t\t\treturn 'var(--color-text-maxcontrast)'\n\t\t\t}\n\t\t\tconst good = (this.content.source && this.content.source.goodDirection)\n\t\t\t\t|| this.content.goodDirection || 'up'\n\t\t\tconst rising = this.deltaPct > 0\n\t\t\tconst isGood = good === 'up' ? rising : !rising\n\t\t\treturn isGood ? STATUS_TEXT_COLORS.success : STATUS_TEXT_COLORS.error\n\t\t},\n\n\t\t/** Stable signature so the watcher only refetches on real change. */\n\t\tsourceKey() {\n\t\t\treturn JSON.stringify(this.content.source || {})\n\t\t},\n\n\t\t/** Merged detail-page object context (`{ objectId, object, register, schema }`) or null — both detail-surface injects (#91 Wave 3). */\n\t\tobjectCtx() {\n\t\t\treturn resolveObjectTokenContext(this.cnObjectContext, this.cnDetailObjectContext)\n\t\t},\n\n\t\t/** Unwrapped page-level workspace context map (always an object). */\n\t\tpageCtx() {\n\t\t\tconst c = this.cnWorkspaceContext\n\t\t\tconst v = (c && typeof c === 'object' && 'value' in c) ? c.value : c\n\t\t\treturn v || {}\n\t\t},\n\n\t\t/** Workspace signature so the widget refetches when the date window changes. */\n\t\tworkspaceKey() {\n\t\t\treturn JSON.stringify(this.pageCtx || {})\n\t\t},\n\t},\n\n\twatch: {\n\t\tsourceKey() {\n\t\t\tthis.fetchValues()\n\t\t},\n\n\t\tworkspaceKey() {\n\t\t\tthis.fetchValues()\n\t\t},\n\t},\n\n\tmounted() {\n\t\tthis.fetchValues()\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Derive a faint background tint for the icon circle from a colour.\n\t\t *\n\t\t * @param {string} color The base colour (hex or CSS var).\n\t\t * @return {string} A translucent or token background.\n\t\t */\n\t\ttint(color) {\n\t\t\tif (typeof color === 'string' && /^#([0-9a-f]{6})$/i.test(color)) {\n\t\t\t\treturn color + '1f'\n\t\t\t}\n\t\t\treturn 'var(--color-primary-element-light, rgba(0,130,201,0.1))'\n\t\t},\n\n\t\t/**\n\t\t * Format a number per the content.format spec (number/currency/percent).\n\t\t *\n\t\t * @param {number|string|null} value The raw value.\n\t\t * @return {string} The formatted string.\n\t\t */\n\t\tformatNumber(value) {\n\t\t\treturn formatMetricValue(value, this.resolvedFormat, unwrapAppConfig(this.cnAppConfig), null, this.effectiveTranslate)\n\t\t},\n\n\t\t/**\n\t\t * Fetch the current and previous aggregates from OpenRegister.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync fetchValues() {\n\t\t\t// Endpoint-bound tiles are fetched by the shared useEndpointSource\n\t\t\t// engine (see setup) — the two-leg OpenRegister path must not fire.\n\t\t\tif (this.endpointMode) {\n\t\t\t\tthis.current = null\n\t\t\t\tthis.previous = null\n\t\t\t\tthis.error = ''\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst s = this.content.source || {}\n\t\t\tif (!s.register || !s.schema) {\n\t\t\t\tthis.current = null\n\t\t\t\tthis.previous = null\n\t\t\t\tthis.error = ''\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.loading = true\n\t\t\tthis.error = ''\n\t\t\ttry {\n\t\t\t\tconst base = { register: s.register, schema: s.schema, metric: s.metric, field: s.field }\n\t\t\t\t// Resolve `@workspace.*` (date-range pills) / `@objectId` / `@object.*`\n\t\t\t\t// per leg, then drop any optional `@workspace.<key>?` that stayed\n\t\t\t\t// unresolved (an unset window) so the leg falls back to its base\n\t\t\t\t// scope instead of sending a literal token. fetchAggregateValue does\n\t\t\t\t// NOT drop optional tokens itself, so we pre-resolve here.\n\t\t\t\tconst ctx = { ...(this.objectCtx || {}), workspace: this.pageCtx }\n\t\t\t\tconst curFilter = dropOptionalUnresolved(resolveFilterTokens((s.current && s.current.filter) || {}, ctx))\n\t\t\t\tconst prevFilter = dropOptionalUnresolved(resolveFilterTokens((s.previous && s.previous.filter) || {}, ctx))\n\t\t\t\tconst [cur, prev] = await Promise.all([\n\t\t\t\t\tfetchAggregateValue({ ...base, filter: curFilter }, ctx),\n\t\t\t\t\tfetchAggregateValue({ ...base, filter: prevFilter }, ctx),\n\t\t\t\t])\n\t\t\t\tthis.current = cur\n\t\t\t\tthis.previous = prev\n\t\t\t} catch (e) {\n\t\t\t\tthis.error = (e && e.message) || 'error'\n\t\t\t\tthis.current = null\n\t\t\t\tthis.previous = null\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n/*\n * The card itself — its box, body stack, icon circle, title and value\n * typography — is the CANONICAL KPI card in src/css/kpi-card.css, shared with\n * CnStatWidget and CnStatsBlock. This tile is the one the canonical look was\n * modelled on, and it used to draw that look from its OWN copy of the rules\n * here: a 44px circle, a 16px gap, a 1.8em value. That copy is exactly how a\n * \"canonical\" card ends up with a second definition, so it is gone. What is\n * left is only what belongs to THIS widget: the delta chip and the error glyph.\n */\n\n/* The percentage-change chip beside the value. Tinted good/bad at runtime from\n * `goodDirection`, so the colour is an inline style, not a class. */\n.cn-delta-widget__delta {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 2px;\n\tfont-size: var(--cn-kpi-label-size, 13px);\n\tfont-weight: var(--cn-kpi-title-weight, 600);\n}\n\n/* The dash shown when the number could not be fetched. It stands where the\n * value stands, so it takes the value's size and weight — only the colour\n * differs, because a failure is not a number. */\n.cn-delta-widget__error {\n\tfont-size: var(--cn-kpi-value-size, 2rem);\n\tfont-weight: var(--cn-kpi-value-weight, 700);\n\tline-height: var(--cn-kpi-value-line, 1.1);\n\tcolor: var(--cn-kpi-label-color, var(--color-text-maxcontrast));\n}\n</style>\n"],"names":["_createBlock","_mergeProps","_createElementBlock","_createVNode","_Fragment","_openBlock","_resolveDynamicComponent","_createTextVNode"],"mappings":";;;;AA8BQ,MAAA,UAAA,GAAA,EAAA,KAAK,EAAC,mDAAmD,GAAA;;;;AAerD,EAAA,KAAA,EAAA,6CAAA;;;;;;;uBApCTA,oCACI,IAAO,CAAA,OAAA,CAAA,EADXC;SAEK,GAAA,6BAA6B,EAAA;AAClC,GAAA,EAAA,wBAAA,EAAA,IAAA,CAAA,QAAA,6BAAA,IAAA,CAAA,QAAA,EAAA;qBAAA,QAAA,CAAA,UAAA;GAAA,QAAA,CAAA,IAAA,GAAA,mBAAA,GAAA,qBAAA;;KAKO,IAAS,CAAA,SAAA,CAAA,EAAA;qBAChB,MAKK;AAJD,MAAA,CAAA,MAAA,CAAA,OAAA,CAAA,IAAA;wBADJC,kBAKK,CAAA,KAAA,EAAA;;;AAFJ,YAAA,KAAK,iBAAE,QAAe,CAAA,eAAA,CAAA;;YACvBC,WAAgD,CAAA,uBAAA,EAAA;cAAjC,IAAI,EAAE,MAAO,CAAA,OAAA,CAAC,IAAI;AAAG,cAAA,IAAI,EAAE,EAAE;;;;;AAIjC,QAAA,CAAA,MAAA,CAAA,OAAO,CAAC,KAAK;0BAAxBD;;AAA0B,cAAA,KAAK,EAAC,2CAA8B;AAAA,cAAA,KAAA,EAAA,QAAA,CAAA,aAAA;+BAC1D,QAAa,CAAA,aAAA,CAAA,EAAA,CAAA,oBAAA,UAAA,CAAA;;2BAgBZ,iBAAA,EAAA;WAZC,QAAA,CAAA,cAAA;uCAAA,CAAA,wBAAA,EAAA;;AAAA,gBAAA,IAAA,EAAA,EAAA;;eACY,QAAQ,CAAA,YAAA;8BAAvBA,kBAAuB,CAAA,MAAA,EAAA;;AAAA,kBAAA,KAAA,EAAA,wBAAA;AAAA,kBAAA,KAAA,EAAA,QAAA,CAAA,YAAA;mBAAA,GAAA,EAAA,CAAA,cAAA,UAAA,CAAA;gDAUd,CAAAE,QAAA,EAAA,EAAA,GAAA,EAAA,CAAA,EAAA,EAAA;oCARqB,CAAA,MAAA,EAAA;AAAzB,oBAAA,KAAK,EAAC,2CAAmB;AAAA,oBAAA,KAAA,iBAAA,QAAA,CAAA,UAAA,CAAA;qCAAA,QAAA,CAAA,gBAAA,CAAA,EAAA,CAAA,mBAAA;mBAExB,QAAQ,CAAA,QAAA,KAAA,IAAA;oCADdF,kBAMM,CAAA,MAAA,EAAA;;AAJN,wBAAA,KAAK,EAAA,wBAAA;AACJ,wBAAA,KAAK,0BAAE,QAAA,CAAA,UAAA,EAAA,CAAA;;AACR,yBAAAG,SAAA,EAAA,EAAAL,WAAA,CAAEM,uBAAA,CAAA,QAAA,CAAA,SAAA,CAAA,EAAA,EAAA,IAAA,EAAA,EAAA,EAAA,CAAA;AAAA,wBAAAC,eAAA,CAAA;;;;;AAMI,QAAA,CAAA,CAAA,QAAA,CAAA,cAAA,IAAA,CAAA,QAAA,CAAA,YAAA,IAAA,MAAA,CAAA,OAAA,CAAA,OAAA;0BAATL,0BAAA;;;;;;;;;;"}
|
|
@@ -46,6 +46,7 @@ import { isAppInstalled } from '../../utils/appInstalled.js';
|
|
|
46
46
|
import { compactLayoutRows } from '../../utils/dashboardPlacement.js';
|
|
47
47
|
import { defaultDetailGrid } from '../../utils/defaultDetailGrid.js';
|
|
48
48
|
import { stageOf, normalisePinnedAction, stageEntry, MAX_QUICK_ACTIONS, groupMenuEntries, resolveNextStep, pillCountSource, resolveCountPill, resolvePill } from '../../utils/detailActionModel.js';
|
|
49
|
+
import { visibilityContextRefs, widgetConditionData, fetchVisibilityRows, detailWidgetVisible } from '../../utils/detailWidgetVisibility.js';
|
|
49
50
|
import { reportBindingProblems } from '../../utils/diagnostics.js';
|
|
50
51
|
import { fetchListTotal } from '../../utils/fetchFilterCounts.js';
|
|
51
52
|
import { cnGridCellStyle, hasGridRow } from '../../utils/grid.js';
|
|
@@ -53,6 +54,7 @@ import { resolveHeaderMeta, isActivityWidget } from '../../utils/headerMeta.js';
|
|
|
53
54
|
import { patchStoredSelf } from '../../utils/patchStoredSelf.js';
|
|
54
55
|
import { setReadState } from '../../utils/recordInteractions.js';
|
|
55
56
|
import { slotRenders } from '../../utils/slotContent.js';
|
|
57
|
+
import { isLocallyDecidableVisibleWhen, evaluateVisibleWhen } from '../../utils/visibleWhen.js';
|
|
56
58
|
import { isDataWidgetDef, isRelatedWidgetDef, isGeoWidgetDef, isContentOnlyWidgetDef, isCardWidgetDef, widgetContentOf, resolveRegistryRenderer, widgetTitleOf } from '../../utils/widgetDispatch.js';
|
|
57
59
|
import __sfc_main$a from '../CnActionButtons/CnActionButtons.vue.js';
|
|
58
60
|
import __sfc_main$b from '../CnBreadcrumbs/CnBreadcrumbs.vue.js';
|
|
@@ -296,8 +298,8 @@ var script = {
|
|
|
296
298
|
|
|
297
299
|
/**
|
|
298
300
|
* Offer the board look's tab list name to the tabs widget below this page
|
|
299
|
-
* (`config.tabsLabel`, default "<type> parts")
|
|
300
|
-
* widget reads
|
|
301
|
+
* (`config.tabsLabel`, default "<type> parts"), and the records
|
|
302
|
+
* `visibilityContext` names. Getter objects, so the widget reads them live.
|
|
301
303
|
*
|
|
302
304
|
* @return {object} The provided values.
|
|
303
305
|
* @spec openspec/changes/screens-detail-page-parity/specs/detail-page-board-look/spec.md#requirement-folder-tabs-take-the-board-strip
|
|
@@ -305,6 +307,7 @@ var script = {
|
|
|
305
307
|
provide() {
|
|
306
308
|
const isBoard = computed(() => this.isBoard);
|
|
307
309
|
const tabsLabel = computed(() => this.resolvedTabsLabel);
|
|
310
|
+
const visibilityRows = computed(() => (this.visibilityRowsPending ? null : this.visibilityRows));
|
|
308
311
|
return {
|
|
309
312
|
// The page's own look reaches the tabs, the cards and the activity
|
|
310
313
|
// below it, also when it came from the `look` prop and not the app.
|
|
@@ -314,6 +317,15 @@ var script = {
|
|
|
314
317
|
return tabsLabel.value
|
|
315
318
|
},
|
|
316
319
|
},
|
|
320
|
+
|
|
321
|
+
// The records `visibilityContext` names, keyed `@context.<name>`, so a tab's
|
|
322
|
+
// `visibleWhen` reads the case type as a grid widget's does. Null
|
|
323
|
+
// while they are on their way.
|
|
324
|
+
cnDetailVisibilityRows: {
|
|
325
|
+
get value() {
|
|
326
|
+
return visibilityRows.value
|
|
327
|
+
},
|
|
328
|
+
},
|
|
317
329
|
}
|
|
318
330
|
},
|
|
319
331
|
|
|
@@ -1356,6 +1368,25 @@ var script = {
|
|
|
1356
1368
|
default: '',
|
|
1357
1369
|
},
|
|
1358
1370
|
|
|
1371
|
+
/**
|
|
1372
|
+
* Records the page reads beside the loaded one, so a `visibleWhen`
|
|
1373
|
+
* can ask about them (manifest `config.visibilityContext`). Each entry
|
|
1374
|
+
* is `{ <name>: { field, register, schema } }`: `field` is the
|
|
1375
|
+
* property of the loaded record holding the other record's id, and a
|
|
1376
|
+
* condition reads that record as `@context.<name>.<path>`. A case page that
|
|
1377
|
+
* declares `{ caseType: { field: "caseType", register: "dossiq",
|
|
1378
|
+
* schema: "caseType" } }` can show a widget for some case types only
|
|
1379
|
+
* with `{ field: "@context.caseType.category", op: "in", value: [...] }`.
|
|
1380
|
+
* A record that cannot be read is absent, so its condition fails safe.
|
|
1381
|
+
* Empty (the default) reads nothing extra.
|
|
1382
|
+
*
|
|
1383
|
+
* @spec openspec/changes/detail-widgets-visible-when/specs/detail-widgets-visible-when/spec.md#requirement-a-detail-page-can-load-records-a-condition-names
|
|
1384
|
+
*/
|
|
1385
|
+
visibilityContext: {
|
|
1386
|
+
type: Object,
|
|
1387
|
+
default: () => ({}),
|
|
1388
|
+
},
|
|
1389
|
+
|
|
1359
1390
|
/**
|
|
1360
1391
|
* Whether the current user administers this instance. Shows the
|
|
1361
1392
|
* `adminOnly` header actions. `null` (the default) reads it from
|
|
@@ -1583,6 +1614,19 @@ var script = {
|
|
|
1583
1614
|
|
|
1584
1615
|
data() {
|
|
1585
1616
|
return {
|
|
1617
|
+
/**
|
|
1618
|
+
* The records `visibilityContext` names, keyed `@context.<name>`: the row,
|
|
1619
|
+
* `null` when it could not be read, absent while it is on its way.
|
|
1620
|
+
*/
|
|
1621
|
+
visibilityRows: {},
|
|
1622
|
+
/** The `visibilityRefsKey` the rows in `visibilityRows` were read for. */
|
|
1623
|
+
visibilityRowsFor: '',
|
|
1624
|
+
/** Sequence of the last `visibilityContext` read, so a late answer for another record is dropped. */
|
|
1625
|
+
visibilityRowsSeq: 0,
|
|
1626
|
+
/** The answers of endpoint and source `visibleWhen` conditions on grid widgets, by widget id. */
|
|
1627
|
+
widgetConditionAnswers: {},
|
|
1628
|
+
/** Sequence of the last remote condition round. */
|
|
1629
|
+
widgetConditionSeq: 0,
|
|
1586
1630
|
/** The counts of the header's count pills (`countFrom`), by pill key (`type`, `status`). */
|
|
1587
1631
|
pillCounts: {},
|
|
1588
1632
|
/** Sequence of the last pill count round, so a late answer does not overwrite a newer one. */
|
|
@@ -1842,11 +1886,17 @@ var script = {
|
|
|
1842
1886
|
* because a keyboard user who cannot see the page needs to know what
|
|
1843
1887
|
* they are being offered before they take it.
|
|
1844
1888
|
*
|
|
1889
|
+
* One library string with a placeholder, not "Skip to" from the host's
|
|
1890
|
+
* catalogue glued to the label: no app carried that fragment, so every
|
|
1891
|
+
* Dutch reader got "Skip to In behandeling nemen" (Zuiddrecht live
|
|
1892
|
+
* round 3), and a fragment cannot be put in Dutch word order anyway.
|
|
1893
|
+
*
|
|
1845
1894
|
* @return {string} The link text.
|
|
1846
1895
|
* @spec openspec/changes/case-page-and-list-as-a-place/specs/index-page/spec.md
|
|
1896
|
+
* @spec openspec/changes/detail-page-strings-dutch/specs/detail-page-strings-dutch/spec.md#requirement-the-detail-page-speaks-the-readers-language
|
|
1847
1897
|
*/
|
|
1848
1898
|
skipLinkLabel() {
|
|
1849
|
-
return
|
|
1899
|
+
return translate('nextcloud-vue', 'Skip to {action}', { action: this.effectivePrimaryLabel })
|
|
1850
1900
|
},
|
|
1851
1901
|
|
|
1852
1902
|
// ── The action model (detail-action-model-and-case-surfaces) ────
|
|
@@ -2688,6 +2738,68 @@ var script = {
|
|
|
2688
2738
|
return store.objects?.[this.objectType]?.[this.objectId] ?? null
|
|
2689
2739
|
},
|
|
2690
2740
|
|
|
2741
|
+
/**
|
|
2742
|
+
* The records `visibilityContext` names for the loaded record.
|
|
2743
|
+
*
|
|
2744
|
+
* @return {Array<{key: string, register: string, schema: string, id: string}>}
|
|
2745
|
+
* @spec openspec/changes/detail-widgets-visible-when/specs/detail-widgets-visible-when/spec.md#requirement-a-detail-page-can-load-records-a-condition-names
|
|
2746
|
+
*/
|
|
2747
|
+
visibilityRefs() {
|
|
2748
|
+
return visibilityContextRefs(this.visibilityContext, this.resolvedObject)
|
|
2749
|
+
},
|
|
2750
|
+
|
|
2751
|
+
/**
|
|
2752
|
+
* Whether a record `visibilityContext` names is still on its way.
|
|
2753
|
+
*
|
|
2754
|
+
* @return {boolean}
|
|
2755
|
+
* @spec openspec/changes/detail-widgets-visible-when/specs/detail-widgets-visible-when/spec.md#requirement-a-detail-page-can-load-records-a-condition-names
|
|
2756
|
+
*/
|
|
2757
|
+
visibilityRowsPending() {
|
|
2758
|
+
return this.visibilityRefs.length > 0 && this.visibilityRowsFor !== this.visibilityRefsKey
|
|
2759
|
+
},
|
|
2760
|
+
|
|
2761
|
+
/**
|
|
2762
|
+
* The named references as one key, so another record (or another case
|
|
2763
|
+
* type on the same record) reads them again.
|
|
2764
|
+
*
|
|
2765
|
+
* @return {string}
|
|
2766
|
+
* @spec openspec/changes/detail-widgets-visible-when/specs/detail-widgets-visible-when/spec.md#requirement-a-detail-page-can-load-records-a-condition-names
|
|
2767
|
+
*/
|
|
2768
|
+
visibilityRefsKey() {
|
|
2769
|
+
return JSON.stringify(this.visibilityRefs)
|
|
2770
|
+
},
|
|
2771
|
+
|
|
2772
|
+
/**
|
|
2773
|
+
* What a grid widget's `visibleWhen` reads: the record, with the
|
|
2774
|
+
* `visibilityContext` records under `@context.<name>`. Null until the record loads.
|
|
2775
|
+
*
|
|
2776
|
+
* @return {object|null}
|
|
2777
|
+
* @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
|
|
2778
|
+
*/
|
|
2779
|
+
widgetVisibilityData() {
|
|
2780
|
+
return widgetConditionData(this.resolvedObject, this.visibilityRows)
|
|
2781
|
+
},
|
|
2782
|
+
|
|
2783
|
+
/**
|
|
2784
|
+
* The grid widgets whose `visibleWhen` needs a request (endpoint or
|
|
2785
|
+
* source), with what they ask, as one key: a change re-asks them.
|
|
2786
|
+
*
|
|
2787
|
+
* @return {string}
|
|
2788
|
+
* @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
|
|
2789
|
+
*/
|
|
2790
|
+
remoteWidgetConditionsKey() {
|
|
2791
|
+
if (!this.widgetVisibilityData || this.visibilityRowsPending) {
|
|
2792
|
+
return ''
|
|
2793
|
+
}
|
|
2794
|
+
const defs = [...(this.bodyGridWidgets || []), ...((this.activeView && this.activeView.widgets) || [])];
|
|
2795
|
+
const remote = defs.filter((def) => def && def.visibleWhen && typeof def.visibleWhen === 'object'
|
|
2796
|
+
&& !isLocallyDecidableVisibleWhen(def.visibleWhen));
|
|
2797
|
+
if (remote.length === 0) {
|
|
2798
|
+
return ''
|
|
2799
|
+
}
|
|
2800
|
+
return JSON.stringify([String(this.objectId || ''), remote.map((def) => [def.id, def.visibleWhen])])
|
|
2801
|
+
},
|
|
2802
|
+
|
|
2691
2803
|
/**
|
|
2692
2804
|
* The `@self` block of the resolved object (interaction markers live here).
|
|
2693
2805
|
*
|
|
@@ -3220,6 +3332,22 @@ var script = {
|
|
|
3220
3332
|
},
|
|
3221
3333
|
|
|
3222
3334
|
watch: {
|
|
3335
|
+
// The records a condition names are read again for another record.
|
|
3336
|
+
visibilityRefsKey: {
|
|
3337
|
+
immediate: true,
|
|
3338
|
+
handler() {
|
|
3339
|
+
this.refreshVisibilityRows();
|
|
3340
|
+
},
|
|
3341
|
+
},
|
|
3342
|
+
|
|
3343
|
+
// Endpoint and source conditions on grid widgets are asked once per record.
|
|
3344
|
+
remoteWidgetConditionsKey: {
|
|
3345
|
+
immediate: true,
|
|
3346
|
+
handler() {
|
|
3347
|
+
this.refreshWidgetConditions();
|
|
3348
|
+
},
|
|
3349
|
+
},
|
|
3350
|
+
|
|
3223
3351
|
// The header's count pills count again for another record or other lists.
|
|
3224
3352
|
pillCountKey: {
|
|
3225
3353
|
immediate: true,
|
|
@@ -4458,6 +4586,62 @@ var script = {
|
|
|
4458
4586
|
|| undefined
|
|
4459
4587
|
},
|
|
4460
4588
|
|
|
4589
|
+
/**
|
|
4590
|
+
* Read the records `visibilityContext` names for the loaded record. A
|
|
4591
|
+
* late answer for a record the page has left is dropped.
|
|
4592
|
+
*
|
|
4593
|
+
* @return {Promise<void>}
|
|
4594
|
+
* @spec openspec/changes/detail-widgets-visible-when/specs/detail-widgets-visible-when/spec.md#requirement-a-detail-page-can-load-records-a-condition-names
|
|
4595
|
+
*/
|
|
4596
|
+
async refreshVisibilityRows() {
|
|
4597
|
+
const key = this.visibilityRefsKey;
|
|
4598
|
+
const refs = this.visibilityRefs;
|
|
4599
|
+
const seq = ++this.visibilityRowsSeq;
|
|
4600
|
+
if (refs.length === 0) {
|
|
4601
|
+
// Write only on a change, so a page without `visibilityContext`
|
|
4602
|
+
// never touches the state its layout could read.
|
|
4603
|
+
if (Object.keys(this.visibilityRows).length > 0) {
|
|
4604
|
+
this.visibilityRows = {};
|
|
4605
|
+
}
|
|
4606
|
+
if (this.visibilityRowsFor !== key) {
|
|
4607
|
+
this.visibilityRowsFor = key;
|
|
4608
|
+
}
|
|
4609
|
+
return
|
|
4610
|
+
}
|
|
4611
|
+
if (!this._visibilityRowCache) {
|
|
4612
|
+
this._visibilityRowCache = new Map();
|
|
4613
|
+
}
|
|
4614
|
+
const rows = await fetchVisibilityRows(refs, this._visibilityRowCache);
|
|
4615
|
+
if (seq !== this.visibilityRowsSeq) {
|
|
4616
|
+
return
|
|
4617
|
+
}
|
|
4618
|
+
this.visibilityRows = rows;
|
|
4619
|
+
this.visibilityRowsFor = key;
|
|
4620
|
+
},
|
|
4621
|
+
|
|
4622
|
+
/**
|
|
4623
|
+
* Ask the endpoint and source `visibleWhen` conditions of the grid
|
|
4624
|
+
* widgets, once per record. Each is fail-safe: an error hides the widget.
|
|
4625
|
+
*
|
|
4626
|
+
* @return {Promise<void>}
|
|
4627
|
+
* @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
|
|
4628
|
+
*/
|
|
4629
|
+
async refreshWidgetConditions() {
|
|
4630
|
+
const seq = ++this.widgetConditionSeq;
|
|
4631
|
+
if (this.remoteWidgetConditionsKey === '') {
|
|
4632
|
+
return
|
|
4633
|
+
}
|
|
4634
|
+
const [, entries] = JSON.parse(this.remoteWidgetConditionsKey);
|
|
4635
|
+
const ctx = { objectId: this.objectId, object: this.widgetVisibilityData || undefined };
|
|
4636
|
+
const answers = {};
|
|
4637
|
+
await Promise.all(entries.map(async ([id, cond]) => {
|
|
4638
|
+
answers[id] = await evaluateVisibleWhen(cond, ctx);
|
|
4639
|
+
}));
|
|
4640
|
+
if (seq === this.widgetConditionSeq) {
|
|
4641
|
+
this.widgetConditionAnswers = answers;
|
|
4642
|
+
}
|
|
4643
|
+
},
|
|
4644
|
+
|
|
4461
4645
|
/**
|
|
4462
4646
|
* Drop the items that draw nothing, then close the rows they freed —
|
|
4463
4647
|
* `float: true` leaves the item below pinned under a gap nobody owns.
|
|
@@ -4480,7 +4664,10 @@ var script = {
|
|
|
4480
4664
|
* Whether a layout item has anything to draw, so a grid cell is worth
|
|
4481
4665
|
* spending on it.
|
|
4482
4666
|
*
|
|
4483
|
-
*
|
|
4667
|
+
* A widget whose definition carries `visibleWhen` draws only while the
|
|
4668
|
+
* condition holds for the record (see `detailWidgetVisible`).
|
|
4669
|
+
*
|
|
4670
|
+
* Beyond that, TWO widget types are judged, and no others.
|
|
4484
4671
|
*
|
|
4485
4672
|
* `integration`, in the case where nothing can ever appear: a leaf from
|
|
4486
4673
|
* another app that is not registered, with no `requiredApp` to fall back
|
|
@@ -4527,6 +4714,18 @@ var script = {
|
|
|
4527
4714
|
if (!widget) {
|
|
4528
4715
|
return true
|
|
4529
4716
|
}
|
|
4717
|
+
// The widget's own `visibleWhen`, read against the record and the
|
|
4718
|
+
// records `visibilityContext` names (a case's case type). Without
|
|
4719
|
+
// one the widget shows, as it always did, and the layout takes no
|
|
4720
|
+
// dependency on the record: a page without conditions re-lays out
|
|
4721
|
+
// exactly as often as before.
|
|
4722
|
+
if (widget.visibleWhen !== undefined && widget.visibleWhen !== null
|
|
4723
|
+
&& !detailWidgetVisible(widget, this.widgetVisibilityData, {
|
|
4724
|
+
pending: this.visibilityRowsPending,
|
|
4725
|
+
answers: this.widgetConditionAnswers,
|
|
4726
|
+
})) {
|
|
4727
|
+
return false
|
|
4728
|
+
}
|
|
4530
4729
|
// The board look shows the activity as the History tab. When a tabs
|
|
4531
4730
|
// widget already holds it, it is not also a body section.
|
|
4532
4731
|
if (this.isBoard && isActivityWidget(widget) && this.tabbedWidgetIds.has(widget.id)) {
|