@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
|
@@ -52,18 +52,33 @@
|
|
|
52
52
|
{{ field.clearLabel }}
|
|
53
53
|
</NcButton>
|
|
54
54
|
</div>
|
|
55
|
-
<
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
55
|
+
<div v-else-if="field.type === 'reference'" class="cn-settings-form__reference">
|
|
56
|
+
<select
|
|
57
|
+
:id="inputId(field)"
|
|
58
|
+
v-model="form[field.key]"
|
|
59
|
+
class="cn-settings-form__control"
|
|
60
|
+
:disabled="field.readOnly"
|
|
61
|
+
:aria-busy="referenceState[field.key] === 'loading' ? 'true' : null"
|
|
62
|
+
:data-testid="`cn-settings-form-reference-${field.key}`">
|
|
63
|
+
<option v-for="option in referenceChoices(field)" :key="option.value" :value="option.value">
|
|
64
|
+
{{ option.label }}
|
|
65
|
+
</option>
|
|
66
|
+
</select>
|
|
67
|
+
<!-- `free`: a typed value (a URL) besides picking an object. -->
|
|
68
|
+
<template v-if="field.free && !field.readOnly">
|
|
69
|
+
<label class="cn-settings-form__sublabel" :for="`${inputId(field)}-free`">{{ field.freeLabel }}</label>
|
|
70
|
+
<input
|
|
71
|
+
:id="`${inputId(field)}-free`"
|
|
72
|
+
class="cn-settings-form__control"
|
|
73
|
+
type="text"
|
|
74
|
+
inputmode="url"
|
|
75
|
+
:value="freeValue(field)"
|
|
76
|
+
:placeholder="field.freePlaceholder"
|
|
77
|
+
:aria-invalid="errors[field.key] ? 'true' : null"
|
|
78
|
+
:data-testid="`cn-settings-form-reference-free-${field.key}`"
|
|
79
|
+
@input="form[field.key] = $event.target.value.trim()">
|
|
80
|
+
</template>
|
|
81
|
+
</div>
|
|
67
82
|
<select
|
|
68
83
|
v-else-if="field.type === 'select'"
|
|
69
84
|
:id="inputId(field)"
|
|
@@ -97,7 +112,8 @@
|
|
|
97
112
|
<p v-if="errors[field.key]" class="cn-settings-form__error" role="alert">
|
|
98
113
|
{{ errors[field.key] }}
|
|
99
114
|
</p>
|
|
100
|
-
|
|
115
|
+
<!-- A chips field prints its own hint (it gets it in chipsContent). -->
|
|
116
|
+
<p v-else-if="field.hint && field.type !== 'chips'" class="cn-settings-form__hint">
|
|
101
117
|
{{ field.hint }}
|
|
102
118
|
</p>
|
|
103
119
|
</div>
|
|
@@ -129,7 +145,9 @@ import { fillTemplate } from '../../utils/fillTemplate.js'
|
|
|
129
145
|
import { prefixUrl } from '../../utils/headers.js'
|
|
130
146
|
import { changedFields, objectUrlOf, writeFields } from '../../utils/objectWrite.js'
|
|
131
147
|
import { readPath } from '../../utils/readPath.js'
|
|
148
|
+
import { hasUnresolvedTokens, resolveFilterTokens } from '../../utils/resolveFilterTokens.js'
|
|
132
149
|
import { serverErrorMessage } from '../../utils/serverErrorMessage.js'
|
|
150
|
+
import { switchIsOn, switchStoredValue } from '../../utils/switchValues.js'
|
|
133
151
|
|
|
134
152
|
// The native file-picker modal ships its chrome styles here; without this the
|
|
135
153
|
// spawned picker renders unstyled. Rules are scoped to the picker/dialog classes.
|
|
@@ -163,7 +181,7 @@ export default {
|
|
|
163
181
|
/**
|
|
164
182
|
* The widget configuration.
|
|
165
183
|
*
|
|
166
|
-
* - `fields` (required) — `[{ key, label, type?: 'text'|'textarea'|'select'|'switch'|'number'|'file'|'chips'|'reference', register?, schema?, labelField?, valueTemplate?, filter?, limit?, emptyLabel?, default?, minItems?, required?, minItemsMessage?, lowercase?, addLabel?, confirmLabel?, removeLabel?, picker?: { title?, mimeTypes?, startAt?, valueTemplate? }, chooseLabel?, clearLabel?, hint?, placeholder?, options?: [{ value, label }], required?, pattern?, patternMessage?, min?, max?, rows?, wide?, readOnly? }]`; `key` is a field of the object, a top-level name or a dot-path.
|
|
184
|
+
* - `fields` (required) — `[{ key, label, type?: 'text'|'textarea'|'select'|'switch'|'number'|'file'|'chips'|'reference', register?, schema?, labelField?, valueTemplate?, filter?, limit?, emptyLabel?, free?, freeLabel?, freePlaceholder?, freePattern?, freePatternMessage?, default?, minItems?, required?, minItemsMessage?, values?: { on, off }, lowercase?, addLabel?, confirmLabel?, removeLabel?, picker?: { title?, mimeTypes?, startAt?, valueTemplate? }, chooseLabel?, clearLabel?, hint?, placeholder?, options?: [{ value, label }], required?, pattern?, patternMessage?, min?, max?, rows?, wide?, readOnly? }]`; `key` is a field of the object, a top-level name or a dot-path. A `reference` field's `filter` values may be `@objectId` and `@object.<field>` (the record being edited). A `switch` field's `values: { on, off }` reads and writes those values instead of true/false.
|
|
167
185
|
* - `save` — `{ url, method?, payload? }`, write through an app endpoint (tokens `{value}`, `{field}`, `{changes}`, `{object.<path>}`).
|
|
168
186
|
* - `columns` — 1 or 2 (default 2).
|
|
169
187
|
* - `saveLabel`, `savedText`, `readOnly`.
|
|
@@ -212,6 +230,9 @@ export default {
|
|
|
212
230
|
// `reference` fields: the objects to choose from and the read's state (`loading`, `ready`, `error`), by field key.
|
|
213
231
|
referenceOptions: {},
|
|
214
232
|
referenceState: {},
|
|
233
|
+
// The query each `reference` field was last read with, by field key, so a
|
|
234
|
+
// new object re-reads only the fields whose filter changes with it.
|
|
235
|
+
referenceQuery: {},
|
|
215
236
|
errors: {},
|
|
216
237
|
saving: false,
|
|
217
238
|
notice: '',
|
|
@@ -236,6 +257,9 @@ export default {
|
|
|
236
257
|
hint: this.tr(field.hint || ''),
|
|
237
258
|
placeholder: this.tr(field.placeholder || ''),
|
|
238
259
|
chooseLabel: this.tr(field.chooseLabel || 'Choose a file'),
|
|
260
|
+
free: field.type === 'reference' && field.free === true,
|
|
261
|
+
freeLabel: this.tr(field.freeLabel || 'Or type a value'),
|
|
262
|
+
freePlaceholder: this.tr(field.freePlaceholder || ''),
|
|
239
263
|
clearLabel: this.tr(field.clearLabel || 'Remove'),
|
|
240
264
|
rows: field.rows || 3,
|
|
241
265
|
wide: field.wide === true || field.type === 'textarea',
|
|
@@ -269,6 +293,7 @@ export default {
|
|
|
269
293
|
watch: {
|
|
270
294
|
objectData() {
|
|
271
295
|
this.adopt()
|
|
296
|
+
this.loadReferences()
|
|
272
297
|
},
|
|
273
298
|
},
|
|
274
299
|
|
|
@@ -284,7 +309,14 @@ export default {
|
|
|
284
309
|
* parameters, at most `limit`, 100 by default). A failed read leaves the
|
|
285
310
|
* field with its stored value only, and says so in the hint's place.
|
|
286
311
|
*
|
|
312
|
+
* `filter` values may be `@objectId` and `@object.<field>`: the record
|
|
313
|
+
* the form edits, so a list can be narrowed to it (the logos of this
|
|
314
|
+
* portal). Until the record has that value the field is not read; it is
|
|
315
|
+
* read again when the record changes the resolved query, never twice
|
|
316
|
+
* for the same query.
|
|
317
|
+
*
|
|
287
318
|
* @spec openspec/changes/screens-settings-form-reference/specs/settings-form/spec.md#requirement-a-settings-form-field-can-pick-an-object
|
|
319
|
+
* @spec openspec/changes/screens-settings-form-reference-object-filter/specs/settings-form/spec.md#requirement-a-reference-fields-filter-can-name-the-record-being-edited
|
|
288
320
|
* @return {Promise<void>}
|
|
289
321
|
*/
|
|
290
322
|
async loadReferences() {
|
|
@@ -292,13 +324,35 @@ export default {
|
|
|
292
324
|
if (fields.length === 0) {
|
|
293
325
|
return
|
|
294
326
|
}
|
|
327
|
+
const ctx = { objectId: this.objectId || (this.objectData && this.objectData.id) || undefined, object: this.objectData || undefined }
|
|
328
|
+
const pending = []
|
|
329
|
+
for (const field of fields) {
|
|
330
|
+
const filter = resolveFilterTokens(field.filter && typeof field.filter === 'object' ? field.filter : {}, ctx)
|
|
331
|
+
if (hasUnresolvedTokens(filter)) {
|
|
332
|
+
// The record has no value for the token yet: nothing to read.
|
|
333
|
+
continue
|
|
334
|
+
}
|
|
335
|
+
const params = { ...filter, _limit: Number(field.limit) > 0 ? Number(field.limit) : 100 }
|
|
336
|
+
const query = JSON.stringify(params)
|
|
337
|
+
if (this.referenceQuery[field.key] === query) {
|
|
338
|
+
continue
|
|
339
|
+
}
|
|
340
|
+
this.referenceQuery = { ...this.referenceQuery, [field.key]: query }
|
|
341
|
+
pending.push({ field, params, query })
|
|
342
|
+
}
|
|
343
|
+
if (pending.length === 0) {
|
|
344
|
+
return
|
|
345
|
+
}
|
|
295
346
|
const { default: axios } = await import('@nextcloud/axios')
|
|
296
|
-
await Promise.all(
|
|
347
|
+
await Promise.all(pending.map(async ({ field, params, query }) => {
|
|
297
348
|
this.referenceState = { ...this.referenceState, [field.key]: 'loading' }
|
|
298
349
|
try {
|
|
299
350
|
const url = `/apps/openregister/api/objects/${encodeURIComponent(field.register)}/${encodeURIComponent(field.schema)}`
|
|
300
|
-
const params = { ...(field.filter && typeof field.filter === 'object' ? field.filter : {}), _limit: Number(field.limit) > 0 ? Number(field.limit) : 100 }
|
|
301
351
|
const { data } = await axios.get(prefixUrl(url), { params })
|
|
352
|
+
if (this.referenceQuery[field.key] !== query) {
|
|
353
|
+
// A newer record asked for another list meanwhile.
|
|
354
|
+
return
|
|
355
|
+
}
|
|
302
356
|
const rows = Array.isArray(data) ? data : (Array.isArray(data?.results) ? data.results : [])
|
|
303
357
|
const options = []
|
|
304
358
|
for (const row of rows) {
|
|
@@ -340,6 +394,24 @@ export default {
|
|
|
340
394
|
return [...choices, ...options]
|
|
341
395
|
},
|
|
342
396
|
|
|
397
|
+
/**
|
|
398
|
+
* The typed value of a `reference` field with `free`: the value the form
|
|
399
|
+
* holds when it is not one of the objects to pick, else empty (picking
|
|
400
|
+
* an object clears the typed value).
|
|
401
|
+
*
|
|
402
|
+
* @spec openspec/changes/screens-settings-form-reference-free-value/specs/settings-form/spec.md#requirement-a-reference-field-can-also-take-a-typed-value
|
|
403
|
+
* @param {object} field The field.
|
|
404
|
+
* @return {string}
|
|
405
|
+
*/
|
|
406
|
+
freeValue(field) {
|
|
407
|
+
const current = this.form[field.key]
|
|
408
|
+
if (typeof current !== 'string' || current === '') {
|
|
409
|
+
return ''
|
|
410
|
+
}
|
|
411
|
+
const options = this.referenceOptions[field.key] || []
|
|
412
|
+
return options.some((option) => option.value === current) ? '' : current
|
|
413
|
+
},
|
|
414
|
+
|
|
343
415
|
tr(text) {
|
|
344
416
|
if (typeof text !== 'string' || text === '') {
|
|
345
417
|
return ''
|
|
@@ -410,7 +482,7 @@ export default {
|
|
|
410
482
|
stored(field) {
|
|
411
483
|
const value = readPath(this.objectData || {}, field.key)
|
|
412
484
|
if (field.type === 'switch') {
|
|
413
|
-
return value
|
|
485
|
+
return switchIsOn(value, field.values)
|
|
414
486
|
}
|
|
415
487
|
if (field.type === 'chips') {
|
|
416
488
|
return Array.isArray(value) ? value.map((item) => String(item)) : []
|
|
@@ -534,6 +606,9 @@ export default {
|
|
|
534
606
|
if (Number.isNaN(value) || (field.min !== undefined && value < field.min) || (field.max !== undefined && value > field.max)) {
|
|
535
607
|
errors[field.key] = this.tr('This number is not valid.')
|
|
536
608
|
}
|
|
609
|
+
} else if (field.free && field.freePattern && this.freeValue(field) !== '' && !new RegExp(field.freePattern).test(String(value))) {
|
|
610
|
+
// A typed value (not one of the objects) answers to `freePattern`.
|
|
611
|
+
errors[field.key] = this.tr(field.freePatternMessage || 'This value is not valid.')
|
|
537
612
|
} else if (field.pattern && !new RegExp(field.pattern).test(String(value))) {
|
|
538
613
|
errors[field.key] = this.tr(field.patternMessage || 'This value is not valid.')
|
|
539
614
|
}
|
|
@@ -567,6 +642,12 @@ export default {
|
|
|
567
642
|
}
|
|
568
643
|
}
|
|
569
644
|
const patch = changedFields(compare, next)
|
|
645
|
+
// A switch with a value map (`values: { on, off }`) writes those values.
|
|
646
|
+
for (const field of this.fields) {
|
|
647
|
+
if (field.type === 'switch' && Object.hasOwn(patch, field.key)) {
|
|
648
|
+
patch[field.key] = switchStoredValue(patch[field.key], field.values)
|
|
649
|
+
}
|
|
650
|
+
}
|
|
570
651
|
this.saving = true
|
|
571
652
|
try {
|
|
572
653
|
if (Object.keys(patch).length > 0) {
|
|
@@ -578,7 +659,7 @@ export default {
|
|
|
578
659
|
const field = this.fields.find((candidate) => candidate.key === key)
|
|
579
660
|
const answered = readPath(data, key)
|
|
580
661
|
const written = answered !== undefined ? answered : patch[key]
|
|
581
|
-
baseline[key] = field.type === 'switch' ? written
|
|
662
|
+
baseline[key] = field.type === 'switch' ? switchIsOn(written, field.values) : (written === null ? (field.type === 'chips' ? [] : '') : written)
|
|
582
663
|
form[key] = baseline[key]
|
|
583
664
|
loaded[key] = baseline[key]
|
|
584
665
|
}
|
|
@@ -639,6 +720,17 @@ export default {
|
|
|
639
720
|
color: var(--color-main-text);
|
|
640
721
|
}
|
|
641
722
|
|
|
723
|
+
.cn-settings-form__reference {
|
|
724
|
+
display: flex;
|
|
725
|
+
flex-direction: column;
|
|
726
|
+
gap: 6px;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
.cn-settings-form__sublabel {
|
|
730
|
+
font-size: 12px;
|
|
731
|
+
color: var(--color-text-maxcontrast);
|
|
732
|
+
}
|
|
733
|
+
|
|
642
734
|
.cn-settings-form__control {
|
|
643
735
|
width: 100%;
|
|
644
736
|
box-sizing: border-box;
|
|
@@ -59,8 +59,15 @@
|
|
|
59
59
|
class="cn-stages-widget__bar-label"
|
|
60
60
|
:title="stage.hint || stage.label"
|
|
61
61
|
:aria-disabled="stage.disabled && stage.clickable ? 'true' : null"
|
|
62
|
+
:aria-busy="movingTo === stage.id ? 'true' : null"
|
|
62
63
|
:data-testid="`cn-stages-widget-stage-${stage.id}`"
|
|
63
64
|
@click="onBarClick(stage)">
|
|
65
|
+
<NcLoadingIcon
|
|
66
|
+
v-if="movingTo === stage.id"
|
|
67
|
+
class="cn-stages-widget__moving"
|
|
68
|
+
:size="16"
|
|
69
|
+
:name="movingMessage"
|
|
70
|
+
data-testid="cn-stages-widget-moving" />
|
|
64
71
|
{{ stage.label }}<span v-if="stage.state === 'current'" class="cn-stages-widget__sr-only"> ({{ tr('current step') }})</span>
|
|
65
72
|
</component>
|
|
66
73
|
<span v-if="stage.date" class="cn-stages-widget__bar-date">{{ stage.date }}</span>
|
|
@@ -83,7 +90,19 @@
|
|
|
83
90
|
@stageClick="onStageClick"
|
|
84
91
|
@stageBlocked="onStageBlocked">
|
|
85
92
|
<template #label="{ stage }">
|
|
86
|
-
<span
|
|
93
|
+
<span
|
|
94
|
+
class="cn-timeline-stages__label"
|
|
95
|
+
:aria-busy="movingTo === stage.id ? 'true' : null"
|
|
96
|
+
:data-testid="`cn-stages-widget-stage-${stage.id}`">
|
|
97
|
+
<!-- The move in flight. A transition can take seconds, and
|
|
98
|
+
a strip that shows nothing until it returns reads as
|
|
99
|
+
one that never moved. -->
|
|
100
|
+
<NcLoadingIcon
|
|
101
|
+
v-if="movingTo === stage.id"
|
|
102
|
+
class="cn-stages-widget__moving"
|
|
103
|
+
:size="16"
|
|
104
|
+
:name="movingMessage"
|
|
105
|
+
data-testid="cn-stages-widget-moving" />
|
|
87
106
|
{{ stage.label }}
|
|
88
107
|
</span>
|
|
89
108
|
<span v-if="stage.subtitle" class="cn-timeline-stages__subtitle">
|
|
@@ -156,8 +175,11 @@
|
|
|
156
175
|
v-if="pendingAction"
|
|
157
176
|
:transition="pendingAction"
|
|
158
177
|
:schema="schema"
|
|
178
|
+
:error="inputError"
|
|
179
|
+
:fieldErrors="inputFieldErrors"
|
|
180
|
+
:busy="busy"
|
|
159
181
|
@confirm="onInputConfirm"
|
|
160
|
-
@close="
|
|
182
|
+
@close="closeInputDialog" />
|
|
161
183
|
</div>
|
|
162
184
|
</template>
|
|
163
185
|
|
|
@@ -177,6 +199,7 @@ import {
|
|
|
177
199
|
performTransition,
|
|
178
200
|
readAvailableActions,
|
|
179
201
|
transitionError,
|
|
202
|
+
transitionFieldErrors,
|
|
180
203
|
} from '../../composables/useLifecycleTransitions.js'
|
|
181
204
|
import { normalizeLook } from '../../composables/useLook.js'
|
|
182
205
|
import { useObjectStore } from '../../store/useObjectStore.js'
|
|
@@ -534,6 +557,17 @@ export default {
|
|
|
534
557
|
pendingAction: null,
|
|
535
558
|
/** @type {boolean} Whether a move is running. */
|
|
536
559
|
busy: false,
|
|
560
|
+
/**
|
|
561
|
+
* @type {string} The stage a running move leads to, or ''. Drives
|
|
562
|
+
* the loading indicator on that one stage.
|
|
563
|
+
*/
|
|
564
|
+
movingTo: '',
|
|
565
|
+
/** @type {string} A refusal shown inside the open input dialog. */
|
|
566
|
+
inputError: '',
|
|
567
|
+
/** @type {string[]} The input keys that refusal named. */
|
|
568
|
+
inputFieldErrors: [],
|
|
569
|
+
/** @type {string[]} The input keys the last refused move named. */
|
|
570
|
+
lastFieldErrors: [],
|
|
537
571
|
/** @type {string} Why the last move failed, or ''. */
|
|
538
572
|
moveError: '',
|
|
539
573
|
/**
|
|
@@ -766,6 +800,20 @@ export default {
|
|
|
766
800
|
return this.interactive && !this.busy
|
|
767
801
|
},
|
|
768
802
|
|
|
803
|
+
/**
|
|
804
|
+
* What the stage a move is running to says while it runs: the
|
|
805
|
+
* loading indicator's name and the live region's sentence.
|
|
806
|
+
*
|
|
807
|
+
* @return {string} The sentence, or '' when nothing is moving.
|
|
808
|
+
*/
|
|
809
|
+
movingMessage() {
|
|
810
|
+
if (!this.movingTo) {
|
|
811
|
+
return ''
|
|
812
|
+
}
|
|
813
|
+
const stage = this.stages.find((s) => s.id === this.movingTo)
|
|
814
|
+
return this.tr('Moving to {stage}', { stage: stage ? stage.label : this.movingTo })
|
|
815
|
+
},
|
|
816
|
+
|
|
769
817
|
/**
|
|
770
818
|
* The allowed moves by the stage they lead to, or null while they are
|
|
771
819
|
* unknown.
|
|
@@ -1225,13 +1273,44 @@ export default {
|
|
|
1225
1273
|
* @param {object} data The collected input values.
|
|
1226
1274
|
* @return {void}
|
|
1227
1275
|
*/
|
|
1228
|
-
onInputConfirm(data) {
|
|
1276
|
+
async onInputConfirm(data) {
|
|
1229
1277
|
const pending = this.pendingAction
|
|
1230
|
-
this.pendingAction = null
|
|
1231
1278
|
if (!pending || !pending.__request) {
|
|
1279
|
+
this.closeInputDialog()
|
|
1280
|
+
return
|
|
1281
|
+
}
|
|
1282
|
+
this.inputError = ''
|
|
1283
|
+
this.inputFieldErrors = []
|
|
1284
|
+
// THE DIALOG STAYS OPEN WHILE THE MOVE RUNS, with its confirm
|
|
1285
|
+
// button busy. Closing it first left the person looking at an
|
|
1286
|
+
// unchanged strip for as long as the transition took, which on a
|
|
1287
|
+
// live case was long enough to reload the page thinking it had
|
|
1288
|
+
// not been sent.
|
|
1289
|
+
const moved = await this.performMove(pending.__request, data)
|
|
1290
|
+
if (this.pendingAction !== pending) {
|
|
1291
|
+
// Closed while the move ran; nothing left to update.
|
|
1232
1292
|
return
|
|
1233
1293
|
}
|
|
1234
|
-
|
|
1294
|
+
if (moved) {
|
|
1295
|
+
this.closeInputDialog()
|
|
1296
|
+
return
|
|
1297
|
+
}
|
|
1298
|
+
// Refused. The sentence goes into the dialog the person is looking
|
|
1299
|
+
// at, with what they typed still in it.
|
|
1300
|
+
this.inputError = this.moveError
|
|
1301
|
+
this.inputFieldErrors = this.lastFieldErrors
|
|
1302
|
+
this.moveError = ''
|
|
1303
|
+
},
|
|
1304
|
+
|
|
1305
|
+
/**
|
|
1306
|
+
* Close the input dialog and forget the refusal it was showing.
|
|
1307
|
+
*
|
|
1308
|
+
* @return {void}
|
|
1309
|
+
*/
|
|
1310
|
+
closeInputDialog() {
|
|
1311
|
+
this.pendingAction = null
|
|
1312
|
+
this.inputError = ''
|
|
1313
|
+
this.inputFieldErrors = []
|
|
1235
1314
|
},
|
|
1236
1315
|
|
|
1237
1316
|
/**
|
|
@@ -1239,11 +1318,17 @@ export default {
|
|
|
1239
1318
|
*
|
|
1240
1319
|
* @param {{stage: {id: string, label: string}, action: string}} request The move.
|
|
1241
1320
|
* @param {object} [data] The collected transition inputs, when any.
|
|
1242
|
-
* @return {Promise<
|
|
1321
|
+
* @return {Promise<boolean>} Whether the move was made.
|
|
1243
1322
|
*/
|
|
1244
1323
|
async performMove(request, data) {
|
|
1245
1324
|
this.busy = true
|
|
1325
|
+
this.movingTo = request.stage.id
|
|
1246
1326
|
this.moveError = ''
|
|
1327
|
+
this.lastFieldErrors = []
|
|
1328
|
+
// Said as the move starts, not only when it lands: a screen reader
|
|
1329
|
+
// user otherwise hears nothing for as long as the server takes.
|
|
1330
|
+
this.statusMessage = this.movingMessage
|
|
1331
|
+
let moved = false
|
|
1247
1332
|
try {
|
|
1248
1333
|
if (this.lifecycleMode) {
|
|
1249
1334
|
await performTransition(this.recordId, request.action, data)
|
|
@@ -1270,11 +1355,16 @@ export default {
|
|
|
1270
1355
|
if (this.lifecycleMode) {
|
|
1271
1356
|
await this.loadActions()
|
|
1272
1357
|
}
|
|
1358
|
+
moved = true
|
|
1273
1359
|
} catch (error) {
|
|
1274
1360
|
this.moveError = transitionError(error, this.tr('The move could not be made'))
|
|
1361
|
+
this.lastFieldErrors = transitionFieldErrors(error)
|
|
1362
|
+
this.statusMessage = this.moveError
|
|
1275
1363
|
} finally {
|
|
1276
1364
|
this.busy = false
|
|
1365
|
+
this.movingTo = ''
|
|
1277
1366
|
}
|
|
1367
|
+
return moved
|
|
1278
1368
|
},
|
|
1279
1369
|
|
|
1280
1370
|
/**
|
|
@@ -1501,6 +1591,13 @@ export default {
|
|
|
1501
1591
|
padding: 8px 0;
|
|
1502
1592
|
}
|
|
1503
1593
|
|
|
1594
|
+
/* The stage a running move leads to: a small spinner before its label. */
|
|
1595
|
+
.cn-stages-widget__moving {
|
|
1596
|
+
display: inline-flex;
|
|
1597
|
+
vertical-align: middle;
|
|
1598
|
+
margin-inline-end: 4px;
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1504
1601
|
.cn-stages-widget__notice {
|
|
1505
1602
|
margin: 0;
|
|
1506
1603
|
color: var(--color-text-maxcontrast);
|
|
@@ -163,6 +163,7 @@ import widgetLink from '../../mixins/widgetLink.js'
|
|
|
163
163
|
import { useObjectStore } from '../../store/useObjectStore.js'
|
|
164
164
|
import { resolveObjectOpType } from '../../utils/actionsDispatcher.js'
|
|
165
165
|
import { resolveObjectTokenContext } from '../../utils/detailObjectContext.js'
|
|
166
|
+
import { aggFilterParam } from '../../utils/fetchAggregate.js'
|
|
166
167
|
import { fetchListTotal, needsListEndpoint } from '../../utils/fetchFilterCounts.js'
|
|
167
168
|
import { formatMetricValue, unwrapAppConfig } from '../../utils/formatMetric.js'
|
|
168
169
|
import { dropOptionalUnresolved, resolveFilterTokens } from '../../utils/resolveFilterTokens.js'
|
|
@@ -1459,7 +1460,7 @@ export default {
|
|
|
1459
1460
|
* @return {string}
|
|
1460
1461
|
*/
|
|
1461
1462
|
formattedValue() {
|
|
1462
|
-
return formatMetricValue(this.displayValue, this.content.format, this.configCtx, this.objectCtx?.object || null)
|
|
1463
|
+
return formatMetricValue(this.displayValue, this.content.format, this.configCtx, this.objectCtx?.object || null, this.effectiveTranslate)
|
|
1463
1464
|
},
|
|
1464
1465
|
|
|
1465
1466
|
/**
|
|
@@ -1783,7 +1784,8 @@ export default {
|
|
|
1783
1784
|
target[`filter[${k}][${op}]`] = ov
|
|
1784
1785
|
}
|
|
1785
1786
|
} else if (v !== '' && v !== null && v !== undefined) {
|
|
1786
|
-
|
|
1787
|
+
// `field[gte]` goes out as `filter[field][gte]`, not `filter[field[gte]]`.
|
|
1788
|
+
target[aggFilterParam(k)] = v
|
|
1787
1789
|
}
|
|
1788
1790
|
}
|
|
1789
1791
|
},
|
|
@@ -17,10 +17,29 @@
|
|
|
17
17
|
<template>
|
|
18
18
|
<div
|
|
19
19
|
class="cn-stats-block-widget"
|
|
20
|
-
:class="[iconClass, { 'cn-stats-block-widget--multi': hasEntries && !isRows, 'cn-stats-block-widget--rows': isRows }]">
|
|
20
|
+
:class="[iconClass, { 'cn-stats-block-widget--multi': hasEntries && !isRows && !isGrid, 'cn-stats-block-widget--rows': isRows, 'cn-stats-block-widget--grid': isGrid, 'cn-stats-block-widget--tiles-box': isGrid && tiles === 'box' }]">
|
|
21
|
+
<!-- Grid layout: the `stats` (each a stat tile's content, so every
|
|
22
|
+
source kind a stat takes: ratio, elapsed, computed...) in `columns`
|
|
23
|
+
columns, inside this one card (the board's "Je week tot nu toe"
|
|
24
|
+
card). The card's title is the widget wrapper's, as on every other
|
|
25
|
+
card (`showTitle: true` on the widget): a heading drawn here took
|
|
26
|
+
the page's h2 size under a theme. -->
|
|
27
|
+
<template v-if="isGrid">
|
|
28
|
+
<div
|
|
29
|
+
class="cn-stats-block-widget__grid"
|
|
30
|
+
:style="{ '--cn-stats-block-columns': gridColumns }"
|
|
31
|
+
data-testid="cn-stats-block-grid">
|
|
32
|
+
<CnStatWidget
|
|
33
|
+
v-for="(stat, index) in stats"
|
|
34
|
+
:key="stat.id || index"
|
|
35
|
+
class="cn-stats-block-widget__grid-stat"
|
|
36
|
+
:content="stat"
|
|
37
|
+
data-testid="cn-stats-block-grid-stat" />
|
|
38
|
+
</div>
|
|
39
|
+
</template>
|
|
21
40
|
<!-- Rows layout: a title and one label-left, number-right row per entry
|
|
22
41
|
(the board's "Mijn Zuiddrecht nu" card). -->
|
|
23
|
-
<template v-if="isRows">
|
|
42
|
+
<template v-else-if="isRows">
|
|
24
43
|
<h2 v-if="title" class="cn-stats-block-widget__rows-title" data-testid="cn-stats-block-rows-title">
|
|
25
44
|
{{ tl(title) }}
|
|
26
45
|
</h2>
|
|
@@ -101,7 +120,9 @@
|
|
|
101
120
|
<script>
|
|
102
121
|
import { subscribe, unsubscribe } from '@nextcloud/event-bus'
|
|
103
122
|
import CnStatsBlock from '../CnStatsBlock/CnStatsBlock.vue'
|
|
123
|
+
import CnStatWidget from '../CnStatWidget/CnStatWidget.vue'
|
|
104
124
|
import { useDataSource } from '../../composables/useDataSource.js'
|
|
125
|
+
import { aggFilterParam } from '../../utils/fetchAggregate.js'
|
|
105
126
|
import { formatMetricValue, normalizeMetricFormat, unwrapAppConfig } from '../../utils/formatMetric.js'
|
|
106
127
|
import { followLinkClick, resolveHref } from '../../utils/linkNavigation.js'
|
|
107
128
|
import { dropOptionalUnresolved, hasUnresolvedTokens, resolveFilterTokens } from '../../utils/resolveFilterTokens.js'
|
|
@@ -180,7 +201,7 @@ const PAGE_REFRESH_BUS_CHANNEL = 'cn:page:refresh'
|
|
|
180
201
|
export default {
|
|
181
202
|
name: 'CnStatsBlockWidget',
|
|
182
203
|
|
|
183
|
-
components: { CnStatsBlock },
|
|
204
|
+
components: { CnStatsBlock, CnStatWidget },
|
|
184
205
|
|
|
185
206
|
inject: {
|
|
186
207
|
/**
|
|
@@ -241,6 +262,47 @@ export default {
|
|
|
241
262
|
default: () => [],
|
|
242
263
|
},
|
|
243
264
|
|
|
265
|
+
/**
|
|
266
|
+
* The tiles of the grid layout (`layout: "grid"`): each is a stat
|
|
267
|
+
* tile's `content` (`label`, `caption`, `format`, `source` with any
|
|
268
|
+
* kind a `type: "stat"` widget takes: aggregate, `ratio`, `elapsed`,
|
|
269
|
+
* `computed`, `weighted`, `endpoint`). They share this one card; its
|
|
270
|
+
* title is the widget wrapper's (`showTitle: true` on the widget).
|
|
271
|
+
* Used instead of `dataSource` / `entries`.
|
|
272
|
+
*
|
|
273
|
+
* @type {Array<object>}
|
|
274
|
+
* @spec openspec/changes/screens-stats-block-grid/specs/stats-block-grid/spec.md#requirement-a-stats-block-can-hold-stat-tiles-in-a-grid
|
|
275
|
+
*/
|
|
276
|
+
stats: {
|
|
277
|
+
type: Array,
|
|
278
|
+
default: () => [],
|
|
279
|
+
},
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Columns of the grid layout (1 to 4, default 2).
|
|
283
|
+
*
|
|
284
|
+
* @spec openspec/changes/screens-stats-block-grid/specs/stats-block-grid/spec.md#requirement-a-stats-block-can-hold-stat-tiles-in-a-grid
|
|
285
|
+
*/
|
|
286
|
+
columns: {
|
|
287
|
+
type: Number,
|
|
288
|
+
default: 2,
|
|
289
|
+
},
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* How the grid layout draws its tiles: `'box'` gives each tile a
|
|
293
|
+
* surface of its own (a grey ground, 10px radius, 14px 16px padding),
|
|
294
|
+
* as board DcRapportages draws its efficiency figures. Empty (the
|
|
295
|
+
* default) keeps the tiles as parts of the one card, without a box
|
|
296
|
+
* (board PqMijnWerk "Je week tot nu toe").
|
|
297
|
+
*
|
|
298
|
+
* @type {'box'|null}
|
|
299
|
+
* @spec openspec/changes/screens-stats-block-tile-box/specs/stats-block-grid/spec.md#requirement-a-stats-block-grid-can-box-its-tiles
|
|
300
|
+
*/
|
|
301
|
+
tiles: {
|
|
302
|
+
type: String,
|
|
303
|
+
default: null,
|
|
304
|
+
},
|
|
305
|
+
|
|
244
306
|
/**
|
|
245
307
|
* How the single-source value is shown: a style name (`'currency'`,
|
|
246
308
|
* `'number'`, `'percent'`, `'decimal'`, `'duration-hours'`) or
|
|
@@ -400,6 +462,28 @@ export default {
|
|
|
400
462
|
return this.layout === 'rows'
|
|
401
463
|
},
|
|
402
464
|
|
|
465
|
+
/**
|
|
466
|
+
* Whether the widget draws its `stats` as tiles in a grid
|
|
467
|
+
* (`layout: "grid"` with at least one stat).
|
|
468
|
+
*
|
|
469
|
+
* @return {boolean}
|
|
470
|
+
* @spec openspec/changes/screens-stats-block-grid/specs/stats-block-grid/spec.md#requirement-a-stats-block-can-hold-stat-tiles-in-a-grid
|
|
471
|
+
*/
|
|
472
|
+
isGrid() {
|
|
473
|
+
return this.layout === 'grid' && Array.isArray(this.stats) && this.stats.length > 0
|
|
474
|
+
},
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* The grid's column count, clamped to 1-4.
|
|
478
|
+
*
|
|
479
|
+
* @return {number}
|
|
480
|
+
* @spec openspec/changes/screens-stats-block-grid/specs/stats-block-grid/spec.md#requirement-a-stats-block-can-hold-stat-tiles-in-a-grid
|
|
481
|
+
*/
|
|
482
|
+
gridColumns() {
|
|
483
|
+
const n = Math.floor(Number(this.columns))
|
|
484
|
+
return Number.isFinite(n) ? Math.min(4, Math.max(1, n)) : 2
|
|
485
|
+
},
|
|
486
|
+
|
|
403
487
|
/**
|
|
404
488
|
* The rows of the rows layout: one per entry, or a single row from the
|
|
405
489
|
* `dataSource` (labelled by `countLabel`). A row is `{ key, label,
|
|
@@ -497,7 +581,7 @@ export default {
|
|
|
497
581
|
if (!fmt || this.loading) {
|
|
498
582
|
return null
|
|
499
583
|
}
|
|
500
|
-
return formatMetricValue(this.resolvedCount, fmt, this.configCtx, this.pageObject)
|
|
584
|
+
return formatMetricValue(this.resolvedCount, fmt, this.configCtx, this.pageObject, this.tl)
|
|
501
585
|
},
|
|
502
586
|
|
|
503
587
|
/**
|
|
@@ -529,7 +613,7 @@ export default {
|
|
|
529
613
|
return {
|
|
530
614
|
// The value as the entry's `format` says, money in the
|
|
531
615
|
// currency the entry names; null keeps the plain count.
|
|
532
|
-
formatted: fmt && resolved ? formatMetricValue(raw, fmt, this.configCtx, this.pageObject) : null,
|
|
616
|
+
formatted: fmt && resolved ? formatMetricValue(raw, fmt, this.configCtx, this.pageObject, this.tl) : null,
|
|
533
617
|
entry: entry || {},
|
|
534
618
|
key: `${(entry && entry.register) || ''}/${(entry && entry.schema) || ''}/${i}`,
|
|
535
619
|
count: resolved ? raw : 0,
|
|
@@ -617,7 +701,8 @@ export default {
|
|
|
617
701
|
// the cross-prop rule lives here (and, for manifests, in
|
|
618
702
|
// validateManifestV2's post-schema check).
|
|
619
703
|
const hasDataSource = !!this.dataSource
|
|
620
|
-
|
|
704
|
+
// A grid of stat tiles carries its sources in `stats`.
|
|
705
|
+
if (!this.isGrid && hasDataSource === this.hasEntries) {
|
|
621
706
|
// eslint-disable-next-line no-console
|
|
622
707
|
console.error('[CnStatsBlockWidget] Exactly one of `dataSource` / `entries` must be provided.')
|
|
623
708
|
}
|
|
@@ -764,7 +849,8 @@ export default {
|
|
|
764
849
|
params[`filter[${k}][${op}]`] = ov
|
|
765
850
|
}
|
|
766
851
|
} else if (v !== '' && v !== null && v !== undefined) {
|
|
767
|
-
|
|
852
|
+
// `field[gte]` goes out as `filter[field][gte]`, not `filter[field[gte]]`.
|
|
853
|
+
params[aggFilterParam(k)] = v
|
|
768
854
|
}
|
|
769
855
|
}
|
|
770
856
|
const res = await axios.get(url, { params })
|
|
@@ -890,6 +976,38 @@ export default {
|
|
|
890
976
|
color: var(--color-text-maxcontrast);
|
|
891
977
|
}
|
|
892
978
|
|
|
979
|
+
/* `width: 100%` plus this padding overflowed the card by 48px: the last
|
|
980
|
+
column ran past the card's right edge (visible once the tiles have a box). */
|
|
981
|
+
.cn-stats-block-widget--grid {
|
|
982
|
+
box-sizing: border-box;
|
|
983
|
+
padding: 4px 24px 20px;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
.cn-stats-block-widget__grid {
|
|
987
|
+
display: grid;
|
|
988
|
+
grid-template-columns: repeat(var(--cn-stats-block-columns, 2), minmax(0, 1fr));
|
|
989
|
+
gap: 16px 20px;
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
/* A tile in the grid is a part of the card, not a card: no box, no inset. */
|
|
993
|
+
.cn-stats-block-widget__grid-stat {
|
|
994
|
+
padding: 0;
|
|
995
|
+
min-width: 0;
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
/* `tiles: "box"`: each tile on a grey ground of its own (board DcRapportages,
|
|
999
|
+
"Vergaderingen en efficiëntie"), 12px apart. */
|
|
1000
|
+
.cn-stats-block-widget--tiles-box .cn-stats-block-widget__grid {
|
|
1001
|
+
gap: 12px;
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
.cn-stats-block-widget--tiles-box .cn-stats-block-widget__grid-stat {
|
|
1005
|
+
box-sizing: border-box;
|
|
1006
|
+
padding: 14px 16px;
|
|
1007
|
+
border-radius: 10px;
|
|
1008
|
+
background: var(--color-background-hover);
|
|
1009
|
+
}
|
|
1010
|
+
|
|
893
1011
|
.cn-stats-block-widget__rows {
|
|
894
1012
|
display: flex;
|
|
895
1013
|
flex-direction: column;
|