@conduction/nextcloud-vue 2.68.0 → 2.69.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/CnDashboardPage/CnDashboardPage.vue2.js +21 -35
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +17 -0
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +40 -3
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +58 -16
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js +2 -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.vue2.js +70 -1
- package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue3.js +23 -3
- package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue3.js.map +1 -1
- package/dist/esm/l10n/en.json.js +3 -0
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +3 -0
- package/dist/esm/l10n/nl.json.js.map +1 -1
- package/dist/esm/mixins/pageViews.js +278 -11
- package/dist/esm/mixins/pageViews.js.map +1 -1
- package/dist/esm/store/plugins/dashboardLayouts.js +35 -1
- package/dist/esm/store/plugins/dashboardLayouts.js.map +1 -1
- package/dist/esm/utils/formatMetric.js +137 -16
- package/dist/esm/utils/formatMetric.js.map +1 -1
- package/dist/esm/utils/validateManifestV2.compiled.js +2 -2
- package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
- package/dist/nextcloud-vue.cjs.js +678 -83
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/l10n/en.json +3 -0
- package/l10n/nl.json +3 -0
- package/package.json +1 -1
- package/src/components/CnDashboardPage/CnDashboardPage.md +2 -0
- package/src/components/CnDashboardPage/CnDashboardPage.vue +34 -35
- package/src/components/CnDetailPage/CnDetailPage.md +6 -0
- package/src/components/CnDetailPage/CnDetailPage.vue +67 -5
- package/src/components/CnStatWidget/CnStatWidget.vue +2 -2
- package/src/components/CnStatsBlockWidget/CnStatsBlockWidget.vue +82 -3
- package/src/mixins/pageViews.js +277 -10
- package/src/schemas/app-manifest-v2.schema.json +28 -2
- package/src/store/plugins/dashboardLayouts.js +34 -0
- package/src/utils/formatMetric.js +143 -15
- package/src/utils/validateManifestV2.compiled.js +2 -2
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getCanonicalLocale } from '@nextcloud/l10n';
|
|
1
2
|
import { resolveFilterValue } from './resolveFilterTokens.js';
|
|
2
3
|
|
|
3
4
|
/**
|
|
@@ -26,20 +27,25 @@ function unwrapAppConfig(cnAppConfig) {
|
|
|
26
27
|
* Resolve `@config.<key>` tokens in a `content.format` spec's string fields.
|
|
27
28
|
*
|
|
28
29
|
* Each of `currency` / `prefix` / `suffix` may be a `@config.<key>` token (e.g.
|
|
29
|
-
* `currency: '@config.currency'`)
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
30
|
+
* `currency: '@config.currency'`), or an `@object.<field>` token read off the
|
|
31
|
+
* detail page's object when `object` is given. Tokens are resolved against
|
|
32
|
+
* `configCtx`; a token that stays unresolved (the config key is unset, or the
|
|
33
|
+
* config map has not been injected yet during a route transition) is DROPPED
|
|
34
|
+
* so the downstream literal default applies instead of a raw `@config.…`
|
|
35
|
+
* string leaking into `Intl.NumberFormat`.
|
|
34
36
|
*
|
|
35
37
|
* @param {object} format The raw `content.format` spec.
|
|
36
38
|
* @param {object} configCtx The page-level app-config map (from `unwrapAppConfig`).
|
|
39
|
+
* @param {object|null} [object] The detail page's object, for `@object.<field>` tokens.
|
|
37
40
|
* @return {object} A copy of `format` with its string tokens resolved/dropped.
|
|
38
41
|
*/
|
|
39
|
-
function resolveConfigFormat(format, configCtx) {
|
|
42
|
+
function resolveConfigFormat(format, configCtx, object = null) {
|
|
40
43
|
const fmt = format || {};
|
|
41
44
|
const out = { ...fmt };
|
|
42
|
-
const ctx = {
|
|
45
|
+
const ctx = {
|
|
46
|
+
config: (configCtx && typeof configCtx === 'object') ? configCtx : {},
|
|
47
|
+
object: (object && typeof object === 'object') ? object : null,
|
|
48
|
+
};
|
|
43
49
|
for (const key of ['currency', 'prefix', 'suffix']) {
|
|
44
50
|
const raw = fmt[key];
|
|
45
51
|
if (typeof raw !== 'string' || raw.charAt(0) !== '@') {
|
|
@@ -69,6 +75,114 @@ function safeCurrencyCode(currency) {
|
|
|
69
75
|
: 'EUR'
|
|
70
76
|
}
|
|
71
77
|
|
|
78
|
+
/**
|
|
79
|
+
* Whether a value is a three-letter currency code.
|
|
80
|
+
*
|
|
81
|
+
* @param {unknown} value The candidate.
|
|
82
|
+
* @return {boolean}
|
|
83
|
+
*/
|
|
84
|
+
function isCurrencyCode(value) {
|
|
85
|
+
return typeof value === 'string' && /^[A-Za-z]{3}$/.test(value)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Read a dot-path off an object.
|
|
90
|
+
*
|
|
91
|
+
* @param {object} object The object.
|
|
92
|
+
* @param {string} path The path, e.g. `currency` or `price.currency`.
|
|
93
|
+
* @return {unknown} The value, or undefined.
|
|
94
|
+
*/
|
|
95
|
+
function readPath(object, path) {
|
|
96
|
+
return String(path).split('.').reduce((acc, key) => (acc && typeof acc === 'object' ? acc[key] : undefined), object)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* The currency a money value is shown in. In order:
|
|
101
|
+
*
|
|
102
|
+
* 1. `currencyField`: the field of the page's object that holds the currency
|
|
103
|
+
* (a detail page; e.g. a contract's own `currency`).
|
|
104
|
+
* 2. `currency`: a literal code, or a token (`@config.currency`,
|
|
105
|
+
* `@object.currency`).
|
|
106
|
+
* 3. The app's reporting currency, `currency` in the page's app config.
|
|
107
|
+
* 4. `EUR`.
|
|
108
|
+
*
|
|
109
|
+
* A step whose value is not a three-letter code is skipped, so an object
|
|
110
|
+
* without a currency falls back to the reporting currency instead of
|
|
111
|
+
* throwing in `Intl.NumberFormat`.
|
|
112
|
+
*
|
|
113
|
+
* @param {object} format The format spec (`{ currency?, currencyField? }`).
|
|
114
|
+
* @param {object} configCtx The page-level app-config map.
|
|
115
|
+
* @param {object|null} [object] The detail page's object.
|
|
116
|
+
* @return {string} An upper-case three-letter code.
|
|
117
|
+
* @spec openspec/changes/stat-currency-from-object/specs/dashboard-page/spec.md#requirement-a-money-value-is-shown-in-the-currency-the-entry-names
|
|
118
|
+
*/
|
|
119
|
+
function resolveFormatCurrency(format, configCtx, object = null) {
|
|
120
|
+
const fmt = format || {};
|
|
121
|
+
if (typeof fmt.currencyField === 'string' && fmt.currencyField !== '' && object && typeof object === 'object') {
|
|
122
|
+
const own = readPath(object, fmt.currencyField);
|
|
123
|
+
if (isCurrencyCode(own)) {
|
|
124
|
+
return own.toUpperCase()
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
const declared = resolveConfigFormat({ currency: fmt.currency }, configCtx, object).currency;
|
|
128
|
+
if (isCurrencyCode(declared)) {
|
|
129
|
+
return declared.toUpperCase()
|
|
130
|
+
}
|
|
131
|
+
const reporting = configCtx && typeof configCtx === 'object' ? configCtx.currency : undefined;
|
|
132
|
+
return safeCurrencyCode(reporting)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* The format spec of a stat or stats-block entry in one shape.
|
|
137
|
+
*
|
|
138
|
+
* `format` may be a style name (`"currency"`) or an object (`{ style,
|
|
139
|
+
* currency, currencyField, decimals, prefix, suffix }`). A sibling `currency`
|
|
140
|
+
* or `currencyField` on the entry itself fills the same keys, and either one
|
|
141
|
+
* makes the style `currency` when none is named. Answers null when the entry
|
|
142
|
+
* declares no format at all, so the caller keeps its own rendering.
|
|
143
|
+
*
|
|
144
|
+
* @param {object} entry The declaration (`{ format?, currency?, currencyField? }`).
|
|
145
|
+
* @return {object|null} The format spec, or null.
|
|
146
|
+
* @spec openspec/changes/stat-currency-from-object/specs/dashboard-page/spec.md#requirement-a-stats-block-entry-formats-its-value
|
|
147
|
+
*/
|
|
148
|
+
function normalizeMetricFormat(entry) {
|
|
149
|
+
const source = entry && typeof entry === 'object' ? entry : {};
|
|
150
|
+
let fmt = null;
|
|
151
|
+
if (typeof source.format === 'string' && source.format !== '') {
|
|
152
|
+
fmt = { style: source.format };
|
|
153
|
+
} else if (source.format && typeof source.format === 'object' && !Array.isArray(source.format)) {
|
|
154
|
+
fmt = { ...source.format };
|
|
155
|
+
}
|
|
156
|
+
for (const key of ['currency', 'currencyField']) {
|
|
157
|
+
if (typeof source[key] === 'string' && source[key] !== '') {
|
|
158
|
+
fmt = fmt || {};
|
|
159
|
+
if (fmt[key] === undefined) {
|
|
160
|
+
fmt[key] = source[key];
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
if (fmt && !fmt.style && (fmt.currency || fmt.currencyField)) {
|
|
165
|
+
fmt.style = 'currency';
|
|
166
|
+
}
|
|
167
|
+
return fmt
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* The user's locale for number formatting: the Nextcloud locale setting,
|
|
172
|
+
* else the browser's.
|
|
173
|
+
*
|
|
174
|
+
* @return {string|undefined} A BCP 47 tag, or undefined for the runtime default.
|
|
175
|
+
*/
|
|
176
|
+
function metricLocale() {
|
|
177
|
+
try {
|
|
178
|
+
const tag = getCanonicalLocale();
|
|
179
|
+
// A tag Intl does not accept would throw a RangeError on every value.
|
|
180
|
+
return tag && Intl.NumberFormat.supportedLocalesOf([tag]).length > 0 ? tag : undefined
|
|
181
|
+
} catch {
|
|
182
|
+
return undefined
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
72
186
|
/**
|
|
73
187
|
* Format a numeric value per a manifest `content.format` spec.
|
|
74
188
|
*
|
|
@@ -86,12 +200,18 @@ function safeCurrencyCode(currency) {
|
|
|
86
200
|
* - `decimal` is a plain number with ONE fraction digit by default (the
|
|
87
201
|
* fleet KPIs' `toFixed(1)` convention), where `number` defaults to 0.
|
|
88
202
|
*
|
|
203
|
+
* The currency follows `resolveFormatCurrency`: the object's own currency
|
|
204
|
+
* (`currencyField`), then `currency`, then the app's reporting currency, then
|
|
205
|
+
* EUR. Numbers are formatted in the user's locale (`metricLocale`).
|
|
206
|
+
*
|
|
89
207
|
* @param {unknown} value The raw value to format.
|
|
90
|
-
* @param {object} format The `content.format` spec (`{ style, currency, decimals, prefix, suffix }`).
|
|
208
|
+
* @param {object} format The `content.format` spec (`{ style, currency, currencyField, decimals, prefix, suffix }`).
|
|
91
209
|
* @param {object} configCtx The page-level app-config map for `@config.<key>` resolution.
|
|
210
|
+
* @param {object|null} [object] The detail page's object, for `currencyField` and `@object.<field>`.
|
|
92
211
|
* @return {string} The formatted display string.
|
|
212
|
+
* @spec openspec/changes/stat-currency-from-object/specs/dashboard-page/spec.md#requirement-a-money-value-is-shown-in-the-currency-the-entry-names
|
|
93
213
|
*/
|
|
94
|
-
function formatMetricValue(value, format, configCtx) {
|
|
214
|
+
function formatMetricValue(value, format, configCtx, object = null) {
|
|
95
215
|
if (value === null || value === undefined) {
|
|
96
216
|
return '—'
|
|
97
217
|
}
|
|
@@ -100,7 +220,8 @@ function formatMetricValue(value, format, configCtx) {
|
|
|
100
220
|
return String(value)
|
|
101
221
|
}
|
|
102
222
|
|
|
103
|
-
const fmt = resolveConfigFormat(format, configCtx);
|
|
223
|
+
const fmt = resolveConfigFormat(format, configCtx, object);
|
|
224
|
+
const locale = metricLocale();
|
|
104
225
|
// `duration-hours` and `decimal` default to ONE fraction digit; the
|
|
105
226
|
// other styles keep the pre-existing default of 0.
|
|
106
227
|
const oneDigitDefault = fmt.style === 'duration-hours' || fmt.style === 'decimal';
|
|
@@ -108,26 +229,26 @@ function formatMetricValue(value, format, configCtx) {
|
|
|
108
229
|
|
|
109
230
|
let body;
|
|
110
231
|
if (fmt.style === 'currency') {
|
|
111
|
-
body = new Intl.NumberFormat(
|
|
232
|
+
body = new Intl.NumberFormat(locale, {
|
|
112
233
|
style: 'currency',
|
|
113
|
-
currency:
|
|
234
|
+
currency: resolveFormatCurrency(format, configCtx, object),
|
|
114
235
|
minimumFractionDigits: decimals,
|
|
115
236
|
maximumFractionDigits: decimals,
|
|
116
237
|
}).format(num);
|
|
117
238
|
} else if (fmt.style === 'percent') {
|
|
118
239
|
// Values are stored as the literal percent (83.3), not a 0–1 ratio.
|
|
119
|
-
body = new Intl.NumberFormat(
|
|
240
|
+
body = new Intl.NumberFormat(locale, {
|
|
120
241
|
minimumFractionDigits: decimals,
|
|
121
242
|
maximumFractionDigits: decimals,
|
|
122
243
|
}).format(num) + '%';
|
|
123
244
|
} else if (fmt.style === 'duration-hours') {
|
|
124
245
|
// An hour count (e.g. mean resolution time): `42.5h`.
|
|
125
|
-
body = new Intl.NumberFormat(
|
|
246
|
+
body = new Intl.NumberFormat(locale, {
|
|
126
247
|
minimumFractionDigits: decimals,
|
|
127
248
|
maximumFractionDigits: decimals,
|
|
128
249
|
}).format(num) + 'h';
|
|
129
250
|
} else {
|
|
130
|
-
body = new Intl.NumberFormat(
|
|
251
|
+
body = new Intl.NumberFormat(locale, {
|
|
131
252
|
minimumFractionDigits: decimals,
|
|
132
253
|
maximumFractionDigits: decimals,
|
|
133
254
|
}).format(num);
|
|
@@ -135,5 +256,5 @@ function formatMetricValue(value, format, configCtx) {
|
|
|
135
256
|
return `${fmt.prefix || ''}${body}${fmt.suffix || ''}`
|
|
136
257
|
}
|
|
137
258
|
|
|
138
|
-
export { formatMetricValue, resolveConfigFormat, safeCurrencyCode, unwrapAppConfig };
|
|
259
|
+
export { formatMetricValue, metricLocale, normalizeMetricFormat, resolveConfigFormat, resolveFormatCurrency, safeCurrencyCode, unwrapAppConfig };
|
|
139
260
|
//# sourceMappingURL=formatMetric.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"formatMetric.js","sources":["../../../src/utils/formatMetric.js"],"sourcesContent":["/**\n * SPDX-FileCopyrightText: 2026 Conduction B.V.\n * SPDX-License-Identifier: EUPL-1.2\n */\n\nimport { resolveFilterValue } from './resolveFilterTokens.js'\n\n/**\n * Unwrap a possibly-ref-wrapped `cnAppConfig` inject into a plain object.\n *\n * Dashboard / detail pages provide `cnAppConfig` either as a plain object or as\n * a Vue ref (`{ value: {...} }`, depending on how the page seeds it). Widgets\n * need the plain map to resolve `@config.<key>` tokens against it.\n *\n * @param {object} cnAppConfig The injected app-config value (object or ref).\n * @return {object} The plain config map (always an object; `{}` when absent).\n */\nexport function unwrapAppConfig(cnAppConfig) {\n\tconst c = cnAppConfig\n\tconst unwrapped = (c && typeof c === 'object' && 'value' in c) ? c.value : c\n\treturn (unwrapped && typeof unwrapped === 'object') ? unwrapped : {}\n}\n\n/**\n * Resolve `@config.<key>` tokens in a `content.format` spec's string fields.\n *\n * Each of `currency` / `prefix` / `suffix` may be a `@config.<key>` token (e.g.\n * `currency: '@config.currency'`). Tokens are resolved against `configCtx`; a\n * token that stays unresolved (the config key is unset, or the config map has\n * not been injected yet during a route transition) is DROPPED so the downstream\n * literal default applies instead of a raw `@config.…` string leaking into\n * `Intl.NumberFormat`.\n *\n * @param {object} format The raw `content.format` spec.\n * @param {object} configCtx The page-level app-config map (from `unwrapAppConfig`).\n * @return {object} A copy of `format` with its string tokens resolved/dropped.\n */\nexport function resolveConfigFormat(format, configCtx) {\n\tconst fmt = format || {}\n\tconst out = { ...fmt }\n\tconst ctx = { config: (configCtx && typeof configCtx === 'object') ? configCtx : {} }\n\tfor (const key of ['currency', 'prefix', 'suffix']) {\n\t\tconst raw = fmt[key]\n\t\tif (typeof raw !== 'string' || raw.charAt(0) !== '@') {\n\t\t\tcontinue\n\t\t}\n\t\tconst resolved = resolveFilterValue(raw, ctx)\n\t\tout[key] = (typeof resolved === 'string' && resolved.charAt(0) === '@') ? undefined : resolved\n\t}\n\treturn out\n}\n\n/**\n * Coerce a currency value to a safe ISO-4217-shaped code.\n *\n * `Intl.NumberFormat({ style: 'currency', currency })` throws a `RangeError`\n * when `currency` is not a three-letter code — which is exactly what happens\n * when an unresolved `@config.currency` token (or any bad value) reaches it.\n * This guard is the single choke point that keeps a malformed currency from\n * ever throwing: anything that is not three ASCII letters falls back to `EUR`.\n *\n * @param {unknown} currency The (possibly unresolved / invalid) currency value.\n * @return {string} A safe upper-case three-letter code (falls back to `EUR`).\n */\nexport function safeCurrencyCode(currency) {\n\treturn (typeof currency === 'string' && /^[A-Za-z]{3}$/.test(currency))\n\t\t? currency.toUpperCase()\n\t\t: 'EUR'\n}\n\n/**\n * Format a numeric value per a manifest `content.format` spec.\n *\n * Handles the `number` / `currency` / `percent` / `duration-hours` /\n * `decimal` styles, resolves `@config.<key>` tokens in `currency` / `prefix`\n * / `suffix` against `configCtx`, and guards the currency code so an\n * unresolved or invalid currency can never throw a `RangeError`. Returns\n * `'—'` for null/undefined and the raw string for non-numeric input,\n * matching the KPI widgets' prior behaviour.\n *\n * Style notes:\n * - `duration-hours` renders an hour count with an `h` suffix and ONE\n * fraction digit by default (`42.5h` — the pipelinq resolution-time KPI\n * contract); `decimals` overrides the fraction digits.\n * - `decimal` is a plain number with ONE fraction digit by default (the\n * fleet KPIs' `toFixed(1)` convention), where `number` defaults to 0.\n *\n * @param {unknown} value The raw value to format.\n * @param {object} format The `content.format` spec (`{ style, currency, decimals, prefix, suffix }`).\n * @param {object} configCtx The page-level app-config map for `@config.<key>` resolution.\n * @return {string} The formatted display string.\n */\nexport function formatMetricValue(value, format, configCtx) {\n\tif (value === null || value === undefined) {\n\t\treturn '—'\n\t}\n\tconst num = Number(value)\n\tif (!Number.isFinite(num)) {\n\t\treturn String(value)\n\t}\n\n\tconst fmt = resolveConfigFormat(format, configCtx)\n\t// `duration-hours` and `decimal` default to ONE fraction digit; the\n\t// other styles keep the pre-existing default of 0.\n\tconst oneDigitDefault = fmt.style === 'duration-hours' || fmt.style === 'decimal'\n\tconst decimals = Number.isFinite(fmt.decimals) ? fmt.decimals : (oneDigitDefault ? 1 : 0)\n\n\tlet body\n\tif (fmt.style === 'currency') {\n\t\tbody = new Intl.NumberFormat(undefined, {\n\t\t\tstyle: 'currency',\n\t\t\tcurrency: safeCurrencyCode(fmt.currency),\n\t\t\tminimumFractionDigits: decimals,\n\t\t\tmaximumFractionDigits: decimals,\n\t\t}).format(num)\n\t} else if (fmt.style === 'percent') {\n\t\t// Values are stored as the literal percent (83.3), not a 0–1 ratio.\n\t\tbody = new Intl.NumberFormat(undefined, {\n\t\t\tminimumFractionDigits: decimals,\n\t\t\tmaximumFractionDigits: decimals,\n\t\t}).format(num) + '%'\n\t} else if (fmt.style === 'duration-hours') {\n\t\t// An hour count (e.g. mean resolution time): `42.5h`.\n\t\tbody = new Intl.NumberFormat(undefined, {\n\t\t\tminimumFractionDigits: decimals,\n\t\t\tmaximumFractionDigits: decimals,\n\t\t}).format(num) + 'h'\n\t} else {\n\t\tbody = new Intl.NumberFormat(undefined, {\n\t\t\tminimumFractionDigits: decimals,\n\t\t\tmaximumFractionDigits: decimals,\n\t\t}).format(num)\n\t}\n\treturn `${fmt.prefix || ''}${body}${fmt.suffix || ''}`\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"formatMetric.js","sources":["../../../src/utils/formatMetric.js"],"sourcesContent":["/**\n * SPDX-FileCopyrightText: 2026 Conduction B.V.\n * SPDX-License-Identifier: EUPL-1.2\n */\n\nimport { getCanonicalLocale } from '@nextcloud/l10n'\nimport { resolveFilterValue } from './resolveFilterTokens.js'\n\n/**\n * The format styles a stat or stats-block entry may name.\n *\n * @type {Array<string>}\n */\nexport const METRIC_FORMAT_STYLES = ['number', 'currency', 'percent', 'duration-hours', 'decimal']\n\n/**\n * Unwrap a possibly-ref-wrapped `cnAppConfig` inject into a plain object.\n *\n * Dashboard / detail pages provide `cnAppConfig` either as a plain object or as\n * a Vue ref (`{ value: {...} }`, depending on how the page seeds it). Widgets\n * need the plain map to resolve `@config.<key>` tokens against it.\n *\n * @param {object} cnAppConfig The injected app-config value (object or ref).\n * @return {object} The plain config map (always an object; `{}` when absent).\n */\nexport function unwrapAppConfig(cnAppConfig) {\n\tconst c = cnAppConfig\n\tconst unwrapped = (c && typeof c === 'object' && 'value' in c) ? c.value : c\n\treturn (unwrapped && typeof unwrapped === 'object') ? unwrapped : {}\n}\n\n/**\n * Resolve `@config.<key>` tokens in a `content.format` spec's string fields.\n *\n * Each of `currency` / `prefix` / `suffix` may be a `@config.<key>` token (e.g.\n * `currency: '@config.currency'`), or an `@object.<field>` token read off the\n * detail page's object when `object` is given. Tokens are resolved against\n * `configCtx`; a token that stays unresolved (the config key is unset, or the\n * config map has not been injected yet during a route transition) is DROPPED\n * so the downstream literal default applies instead of a raw `@config.…`\n * string leaking into `Intl.NumberFormat`.\n *\n * @param {object} format The raw `content.format` spec.\n * @param {object} configCtx The page-level app-config map (from `unwrapAppConfig`).\n * @param {object|null} [object] The detail page's object, for `@object.<field>` tokens.\n * @return {object} A copy of `format` with its string tokens resolved/dropped.\n */\nexport function resolveConfigFormat(format, configCtx, object = null) {\n\tconst fmt = format || {}\n\tconst out = { ...fmt }\n\tconst ctx = {\n\t\tconfig: (configCtx && typeof configCtx === 'object') ? configCtx : {},\n\t\tobject: (object && typeof object === 'object') ? object : null,\n\t}\n\tfor (const key of ['currency', 'prefix', 'suffix']) {\n\t\tconst raw = fmt[key]\n\t\tif (typeof raw !== 'string' || raw.charAt(0) !== '@') {\n\t\t\tcontinue\n\t\t}\n\t\tconst resolved = resolveFilterValue(raw, ctx)\n\t\tout[key] = (typeof resolved === 'string' && resolved.charAt(0) === '@') ? undefined : resolved\n\t}\n\treturn out\n}\n\n/**\n * Coerce a currency value to a safe ISO-4217-shaped code.\n *\n * `Intl.NumberFormat({ style: 'currency', currency })` throws a `RangeError`\n * when `currency` is not a three-letter code — which is exactly what happens\n * when an unresolved `@config.currency` token (or any bad value) reaches it.\n * This guard is the single choke point that keeps a malformed currency from\n * ever throwing: anything that is not three ASCII letters falls back to `EUR`.\n *\n * @param {unknown} currency The (possibly unresolved / invalid) currency value.\n * @return {string} A safe upper-case three-letter code (falls back to `EUR`).\n */\nexport function safeCurrencyCode(currency) {\n\treturn (typeof currency === 'string' && /^[A-Za-z]{3}$/.test(currency))\n\t\t? currency.toUpperCase()\n\t\t: 'EUR'\n}\n\n/**\n * Whether a value is a three-letter currency code.\n *\n * @param {unknown} value The candidate.\n * @return {boolean}\n */\nfunction isCurrencyCode(value) {\n\treturn typeof value === 'string' && /^[A-Za-z]{3}$/.test(value)\n}\n\n/**\n * Read a dot-path off an object.\n *\n * @param {object} object The object.\n * @param {string} path The path, e.g. `currency` or `price.currency`.\n * @return {unknown} The value, or undefined.\n */\nfunction readPath(object, path) {\n\treturn String(path).split('.').reduce((acc, key) => (acc && typeof acc === 'object' ? acc[key] : undefined), object)\n}\n\n/**\n * The currency a money value is shown in. In order:\n *\n * 1. `currencyField`: the field of the page's object that holds the currency\n * (a detail page; e.g. a contract's own `currency`).\n * 2. `currency`: a literal code, or a token (`@config.currency`,\n * `@object.currency`).\n * 3. The app's reporting currency, `currency` in the page's app config.\n * 4. `EUR`.\n *\n * A step whose value is not a three-letter code is skipped, so an object\n * without a currency falls back to the reporting currency instead of\n * throwing in `Intl.NumberFormat`.\n *\n * @param {object} format The format spec (`{ currency?, currencyField? }`).\n * @param {object} configCtx The page-level app-config map.\n * @param {object|null} [object] The detail page's object.\n * @return {string} An upper-case three-letter code.\n * @spec openspec/changes/stat-currency-from-object/specs/dashboard-page/spec.md#requirement-a-money-value-is-shown-in-the-currency-the-entry-names\n */\nexport function resolveFormatCurrency(format, configCtx, object = null) {\n\tconst fmt = format || {}\n\tif (typeof fmt.currencyField === 'string' && fmt.currencyField !== '' && object && typeof object === 'object') {\n\t\tconst own = readPath(object, fmt.currencyField)\n\t\tif (isCurrencyCode(own)) {\n\t\t\treturn own.toUpperCase()\n\t\t}\n\t}\n\tconst declared = resolveConfigFormat({ currency: fmt.currency }, configCtx, object).currency\n\tif (isCurrencyCode(declared)) {\n\t\treturn declared.toUpperCase()\n\t}\n\tconst reporting = configCtx && typeof configCtx === 'object' ? configCtx.currency : undefined\n\treturn safeCurrencyCode(reporting)\n}\n\n/**\n * The format spec of a stat or stats-block entry in one shape.\n *\n * `format` may be a style name (`\"currency\"`) or an object (`{ style,\n * currency, currencyField, decimals, prefix, suffix }`). A sibling `currency`\n * or `currencyField` on the entry itself fills the same keys, and either one\n * makes the style `currency` when none is named. Answers null when the entry\n * declares no format at all, so the caller keeps its own rendering.\n *\n * @param {object} entry The declaration (`{ format?, currency?, currencyField? }`).\n * @return {object|null} The format spec, or null.\n * @spec openspec/changes/stat-currency-from-object/specs/dashboard-page/spec.md#requirement-a-stats-block-entry-formats-its-value\n */\nexport function normalizeMetricFormat(entry) {\n\tconst source = entry && typeof entry === 'object' ? entry : {}\n\tlet fmt = null\n\tif (typeof source.format === 'string' && source.format !== '') {\n\t\tfmt = { style: source.format }\n\t} else if (source.format && typeof source.format === 'object' && !Array.isArray(source.format)) {\n\t\tfmt = { ...source.format }\n\t}\n\tfor (const key of ['currency', 'currencyField']) {\n\t\tif (typeof source[key] === 'string' && source[key] !== '') {\n\t\t\tfmt = fmt || {}\n\t\t\tif (fmt[key] === undefined) {\n\t\t\t\tfmt[key] = source[key]\n\t\t\t}\n\t\t}\n\t}\n\tif (fmt && !fmt.style && (fmt.currency || fmt.currencyField)) {\n\t\tfmt.style = 'currency'\n\t}\n\treturn fmt\n}\n\n/**\n * The user's locale for number formatting: the Nextcloud locale setting,\n * else the browser's.\n *\n * @return {string|undefined} A BCP 47 tag, or undefined for the runtime default.\n */\nexport function metricLocale() {\n\ttry {\n\t\tconst tag = getCanonicalLocale()\n\t\t// A tag Intl does not accept would throw a RangeError on every value.\n\t\treturn tag && Intl.NumberFormat.supportedLocalesOf([tag]).length > 0 ? tag : undefined\n\t} catch {\n\t\treturn undefined\n\t}\n}\n\n/**\n * Format a numeric value per a manifest `content.format` spec.\n *\n * Handles the `number` / `currency` / `percent` / `duration-hours` /\n * `decimal` styles, resolves `@config.<key>` tokens in `currency` / `prefix`\n * / `suffix` against `configCtx`, and guards the currency code so an\n * unresolved or invalid currency can never throw a `RangeError`. Returns\n * `'—'` for null/undefined and the raw string for non-numeric input,\n * matching the KPI widgets' prior behaviour.\n *\n * Style notes:\n * - `duration-hours` renders an hour count with an `h` suffix and ONE\n * fraction digit by default (`42.5h` — the pipelinq resolution-time KPI\n * contract); `decimals` overrides the fraction digits.\n * - `decimal` is a plain number with ONE fraction digit by default (the\n * fleet KPIs' `toFixed(1)` convention), where `number` defaults to 0.\n *\n * The currency follows `resolveFormatCurrency`: the object's own currency\n * (`currencyField`), then `currency`, then the app's reporting currency, then\n * EUR. Numbers are formatted in the user's locale (`metricLocale`).\n *\n * @param {unknown} value The raw value to format.\n * @param {object} format The `content.format` spec (`{ style, currency, currencyField, decimals, prefix, suffix }`).\n * @param {object} configCtx The page-level app-config map for `@config.<key>` resolution.\n * @param {object|null} [object] The detail page's object, for `currencyField` and `@object.<field>`.\n * @return {string} The formatted display string.\n * @spec openspec/changes/stat-currency-from-object/specs/dashboard-page/spec.md#requirement-a-money-value-is-shown-in-the-currency-the-entry-names\n */\nexport function formatMetricValue(value, format, configCtx, object = null) {\n\tif (value === null || value === undefined) {\n\t\treturn '—'\n\t}\n\tconst num = Number(value)\n\tif (!Number.isFinite(num)) {\n\t\treturn String(value)\n\t}\n\n\tconst fmt = resolveConfigFormat(format, configCtx, object)\n\tconst locale = metricLocale()\n\t// `duration-hours` and `decimal` default to ONE fraction digit; the\n\t// other styles keep the pre-existing default of 0.\n\tconst oneDigitDefault = fmt.style === 'duration-hours' || fmt.style === 'decimal'\n\tconst decimals = Number.isFinite(fmt.decimals) ? fmt.decimals : (oneDigitDefault ? 1 : 0)\n\n\tlet body\n\tif (fmt.style === 'currency') {\n\t\tbody = new Intl.NumberFormat(locale, {\n\t\t\tstyle: 'currency',\n\t\t\tcurrency: resolveFormatCurrency(format, configCtx, object),\n\t\t\tminimumFractionDigits: decimals,\n\t\t\tmaximumFractionDigits: decimals,\n\t\t}).format(num)\n\t} else if (fmt.style === 'percent') {\n\t\t// Values are stored as the literal percent (83.3), not a 0–1 ratio.\n\t\tbody = new Intl.NumberFormat(locale, {\n\t\t\tminimumFractionDigits: decimals,\n\t\t\tmaximumFractionDigits: decimals,\n\t\t}).format(num) + '%'\n\t} else if (fmt.style === 'duration-hours') {\n\t\t// An hour count (e.g. mean resolution time): `42.5h`.\n\t\tbody = new Intl.NumberFormat(locale, {\n\t\t\tminimumFractionDigits: decimals,\n\t\t\tmaximumFractionDigits: decimals,\n\t\t}).format(num) + 'h'\n\t} else {\n\t\tbody = new Intl.NumberFormat(locale, {\n\t\t\tminimumFractionDigits: decimals,\n\t\t\tmaximumFractionDigits: decimals,\n\t\t}).format(num)\n\t}\n\treturn `${fmt.prefix || ''}${body}${fmt.suffix || ''}`\n}\n"],"names":[],"mappings":"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"}
|