@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
|
@@ -3,8 +3,16 @@
|
|
|
3
3
|
* SPDX-License-Identifier: EUPL-1.2
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { getCanonicalLocale } from '@nextcloud/l10n'
|
|
6
7
|
import { resolveFilterValue } from './resolveFilterTokens.js'
|
|
7
8
|
|
|
9
|
+
/**
|
|
10
|
+
* The format styles a stat or stats-block entry may name.
|
|
11
|
+
*
|
|
12
|
+
* @type {Array<string>}
|
|
13
|
+
*/
|
|
14
|
+
export const METRIC_FORMAT_STYLES = ['number', 'currency', 'percent', 'duration-hours', 'decimal']
|
|
15
|
+
|
|
8
16
|
/**
|
|
9
17
|
* Unwrap a possibly-ref-wrapped `cnAppConfig` inject into a plain object.
|
|
10
18
|
*
|
|
@@ -25,20 +33,25 @@ export function unwrapAppConfig(cnAppConfig) {
|
|
|
25
33
|
* Resolve `@config.<key>` tokens in a `content.format` spec's string fields.
|
|
26
34
|
*
|
|
27
35
|
* Each of `currency` / `prefix` / `suffix` may be a `@config.<key>` token (e.g.
|
|
28
|
-
* `currency: '@config.currency'`)
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
36
|
+
* `currency: '@config.currency'`), or an `@object.<field>` token read off the
|
|
37
|
+
* detail page's object when `object` is given. Tokens are resolved against
|
|
38
|
+
* `configCtx`; a token that stays unresolved (the config key is unset, or the
|
|
39
|
+
* config map has not been injected yet during a route transition) is DROPPED
|
|
40
|
+
* so the downstream literal default applies instead of a raw `@config.…`
|
|
41
|
+
* string leaking into `Intl.NumberFormat`.
|
|
33
42
|
*
|
|
34
43
|
* @param {object} format The raw `content.format` spec.
|
|
35
44
|
* @param {object} configCtx The page-level app-config map (from `unwrapAppConfig`).
|
|
45
|
+
* @param {object|null} [object] The detail page's object, for `@object.<field>` tokens.
|
|
36
46
|
* @return {object} A copy of `format` with its string tokens resolved/dropped.
|
|
37
47
|
*/
|
|
38
|
-
export function resolveConfigFormat(format, configCtx) {
|
|
48
|
+
export function resolveConfigFormat(format, configCtx, object = null) {
|
|
39
49
|
const fmt = format || {}
|
|
40
50
|
const out = { ...fmt }
|
|
41
|
-
const ctx = {
|
|
51
|
+
const ctx = {
|
|
52
|
+
config: (configCtx && typeof configCtx === 'object') ? configCtx : {},
|
|
53
|
+
object: (object && typeof object === 'object') ? object : null,
|
|
54
|
+
}
|
|
42
55
|
for (const key of ['currency', 'prefix', 'suffix']) {
|
|
43
56
|
const raw = fmt[key]
|
|
44
57
|
if (typeof raw !== 'string' || raw.charAt(0) !== '@') {
|
|
@@ -68,6 +81,114 @@ export function safeCurrencyCode(currency) {
|
|
|
68
81
|
: 'EUR'
|
|
69
82
|
}
|
|
70
83
|
|
|
84
|
+
/**
|
|
85
|
+
* Whether a value is a three-letter currency code.
|
|
86
|
+
*
|
|
87
|
+
* @param {unknown} value The candidate.
|
|
88
|
+
* @return {boolean}
|
|
89
|
+
*/
|
|
90
|
+
function isCurrencyCode(value) {
|
|
91
|
+
return typeof value === 'string' && /^[A-Za-z]{3}$/.test(value)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Read a dot-path off an object.
|
|
96
|
+
*
|
|
97
|
+
* @param {object} object The object.
|
|
98
|
+
* @param {string} path The path, e.g. `currency` or `price.currency`.
|
|
99
|
+
* @return {unknown} The value, or undefined.
|
|
100
|
+
*/
|
|
101
|
+
function readPath(object, path) {
|
|
102
|
+
return String(path).split('.').reduce((acc, key) => (acc && typeof acc === 'object' ? acc[key] : undefined), object)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* The currency a money value is shown in. In order:
|
|
107
|
+
*
|
|
108
|
+
* 1. `currencyField`: the field of the page's object that holds the currency
|
|
109
|
+
* (a detail page; e.g. a contract's own `currency`).
|
|
110
|
+
* 2. `currency`: a literal code, or a token (`@config.currency`,
|
|
111
|
+
* `@object.currency`).
|
|
112
|
+
* 3. The app's reporting currency, `currency` in the page's app config.
|
|
113
|
+
* 4. `EUR`.
|
|
114
|
+
*
|
|
115
|
+
* A step whose value is not a three-letter code is skipped, so an object
|
|
116
|
+
* without a currency falls back to the reporting currency instead of
|
|
117
|
+
* throwing in `Intl.NumberFormat`.
|
|
118
|
+
*
|
|
119
|
+
* @param {object} format The format spec (`{ currency?, currencyField? }`).
|
|
120
|
+
* @param {object} configCtx The page-level app-config map.
|
|
121
|
+
* @param {object|null} [object] The detail page's object.
|
|
122
|
+
* @return {string} An upper-case three-letter code.
|
|
123
|
+
* @spec openspec/changes/stat-currency-from-object/specs/dashboard-page/spec.md#requirement-a-money-value-is-shown-in-the-currency-the-entry-names
|
|
124
|
+
*/
|
|
125
|
+
export function resolveFormatCurrency(format, configCtx, object = null) {
|
|
126
|
+
const fmt = format || {}
|
|
127
|
+
if (typeof fmt.currencyField === 'string' && fmt.currencyField !== '' && object && typeof object === 'object') {
|
|
128
|
+
const own = readPath(object, fmt.currencyField)
|
|
129
|
+
if (isCurrencyCode(own)) {
|
|
130
|
+
return own.toUpperCase()
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
const declared = resolveConfigFormat({ currency: fmt.currency }, configCtx, object).currency
|
|
134
|
+
if (isCurrencyCode(declared)) {
|
|
135
|
+
return declared.toUpperCase()
|
|
136
|
+
}
|
|
137
|
+
const reporting = configCtx && typeof configCtx === 'object' ? configCtx.currency : undefined
|
|
138
|
+
return safeCurrencyCode(reporting)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* The format spec of a stat or stats-block entry in one shape.
|
|
143
|
+
*
|
|
144
|
+
* `format` may be a style name (`"currency"`) or an object (`{ style,
|
|
145
|
+
* currency, currencyField, decimals, prefix, suffix }`). A sibling `currency`
|
|
146
|
+
* or `currencyField` on the entry itself fills the same keys, and either one
|
|
147
|
+
* makes the style `currency` when none is named. Answers null when the entry
|
|
148
|
+
* declares no format at all, so the caller keeps its own rendering.
|
|
149
|
+
*
|
|
150
|
+
* @param {object} entry The declaration (`{ format?, currency?, currencyField? }`).
|
|
151
|
+
* @return {object|null} The format spec, or null.
|
|
152
|
+
* @spec openspec/changes/stat-currency-from-object/specs/dashboard-page/spec.md#requirement-a-stats-block-entry-formats-its-value
|
|
153
|
+
*/
|
|
154
|
+
export function normalizeMetricFormat(entry) {
|
|
155
|
+
const source = entry && typeof entry === 'object' ? entry : {}
|
|
156
|
+
let fmt = null
|
|
157
|
+
if (typeof source.format === 'string' && source.format !== '') {
|
|
158
|
+
fmt = { style: source.format }
|
|
159
|
+
} else if (source.format && typeof source.format === 'object' && !Array.isArray(source.format)) {
|
|
160
|
+
fmt = { ...source.format }
|
|
161
|
+
}
|
|
162
|
+
for (const key of ['currency', 'currencyField']) {
|
|
163
|
+
if (typeof source[key] === 'string' && source[key] !== '') {
|
|
164
|
+
fmt = fmt || {}
|
|
165
|
+
if (fmt[key] === undefined) {
|
|
166
|
+
fmt[key] = source[key]
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (fmt && !fmt.style && (fmt.currency || fmt.currencyField)) {
|
|
171
|
+
fmt.style = 'currency'
|
|
172
|
+
}
|
|
173
|
+
return fmt
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* The user's locale for number formatting: the Nextcloud locale setting,
|
|
178
|
+
* else the browser's.
|
|
179
|
+
*
|
|
180
|
+
* @return {string|undefined} A BCP 47 tag, or undefined for the runtime default.
|
|
181
|
+
*/
|
|
182
|
+
export function metricLocale() {
|
|
183
|
+
try {
|
|
184
|
+
const tag = getCanonicalLocale()
|
|
185
|
+
// A tag Intl does not accept would throw a RangeError on every value.
|
|
186
|
+
return tag && Intl.NumberFormat.supportedLocalesOf([tag]).length > 0 ? tag : undefined
|
|
187
|
+
} catch {
|
|
188
|
+
return undefined
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
71
192
|
/**
|
|
72
193
|
* Format a numeric value per a manifest `content.format` spec.
|
|
73
194
|
*
|
|
@@ -85,12 +206,18 @@ export function safeCurrencyCode(currency) {
|
|
|
85
206
|
* - `decimal` is a plain number with ONE fraction digit by default (the
|
|
86
207
|
* fleet KPIs' `toFixed(1)` convention), where `number` defaults to 0.
|
|
87
208
|
*
|
|
209
|
+
* The currency follows `resolveFormatCurrency`: the object's own currency
|
|
210
|
+
* (`currencyField`), then `currency`, then the app's reporting currency, then
|
|
211
|
+
* EUR. Numbers are formatted in the user's locale (`metricLocale`).
|
|
212
|
+
*
|
|
88
213
|
* @param {unknown} value The raw value to format.
|
|
89
|
-
* @param {object} format The `content.format` spec (`{ style, currency, decimals, prefix, suffix }`).
|
|
214
|
+
* @param {object} format The `content.format` spec (`{ style, currency, currencyField, decimals, prefix, suffix }`).
|
|
90
215
|
* @param {object} configCtx The page-level app-config map for `@config.<key>` resolution.
|
|
216
|
+
* @param {object|null} [object] The detail page's object, for `currencyField` and `@object.<field>`.
|
|
91
217
|
* @return {string} The formatted display string.
|
|
218
|
+
* @spec openspec/changes/stat-currency-from-object/specs/dashboard-page/spec.md#requirement-a-money-value-is-shown-in-the-currency-the-entry-names
|
|
92
219
|
*/
|
|
93
|
-
export function formatMetricValue(value, format, configCtx) {
|
|
220
|
+
export function formatMetricValue(value, format, configCtx, object = null) {
|
|
94
221
|
if (value === null || value === undefined) {
|
|
95
222
|
return '—'
|
|
96
223
|
}
|
|
@@ -99,7 +226,8 @@ export function formatMetricValue(value, format, configCtx) {
|
|
|
99
226
|
return String(value)
|
|
100
227
|
}
|
|
101
228
|
|
|
102
|
-
const fmt = resolveConfigFormat(format, configCtx)
|
|
229
|
+
const fmt = resolveConfigFormat(format, configCtx, object)
|
|
230
|
+
const locale = metricLocale()
|
|
103
231
|
// `duration-hours` and `decimal` default to ONE fraction digit; the
|
|
104
232
|
// other styles keep the pre-existing default of 0.
|
|
105
233
|
const oneDigitDefault = fmt.style === 'duration-hours' || fmt.style === 'decimal'
|
|
@@ -107,26 +235,26 @@ export function formatMetricValue(value, format, configCtx) {
|
|
|
107
235
|
|
|
108
236
|
let body
|
|
109
237
|
if (fmt.style === 'currency') {
|
|
110
|
-
body = new Intl.NumberFormat(
|
|
238
|
+
body = new Intl.NumberFormat(locale, {
|
|
111
239
|
style: 'currency',
|
|
112
|
-
currency:
|
|
240
|
+
currency: resolveFormatCurrency(format, configCtx, object),
|
|
113
241
|
minimumFractionDigits: decimals,
|
|
114
242
|
maximumFractionDigits: decimals,
|
|
115
243
|
}).format(num)
|
|
116
244
|
} else if (fmt.style === 'percent') {
|
|
117
245
|
// Values are stored as the literal percent (83.3), not a 0–1 ratio.
|
|
118
|
-
body = new Intl.NumberFormat(
|
|
246
|
+
body = new Intl.NumberFormat(locale, {
|
|
119
247
|
minimumFractionDigits: decimals,
|
|
120
248
|
maximumFractionDigits: decimals,
|
|
121
249
|
}).format(num) + '%'
|
|
122
250
|
} else if (fmt.style === 'duration-hours') {
|
|
123
251
|
// An hour count (e.g. mean resolution time): `42.5h`.
|
|
124
|
-
body = new Intl.NumberFormat(
|
|
252
|
+
body = new Intl.NumberFormat(locale, {
|
|
125
253
|
minimumFractionDigits: decimals,
|
|
126
254
|
maximumFractionDigits: decimals,
|
|
127
255
|
}).format(num) + 'h'
|
|
128
256
|
} else {
|
|
129
|
-
body = new Intl.NumberFormat(
|
|
257
|
+
body = new Intl.NumberFormat(locale, {
|
|
130
258
|
minimumFractionDigits: decimals,
|
|
131
259
|
maximumFractionDigits: decimals,
|
|
132
260
|
}).format(num)
|