@conduction/nextcloud-vue 2.82.0 → 2.83.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/CnDashboardPage/CnDashboardPage.vue.js +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +9 -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/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/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/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/CnReportsPage/CnReportsPage.vue.js +1 -1
- package/dist/esm/components/CnReportsPage/CnReportsPage.vue2.js +43 -0
- package/dist/esm/components/CnReportsPage/CnReportsPage.vue2.js.map +1 -1
- package/dist/esm/components/CnReportsPage/CnReportsPage.vue3.js +130 -53
- 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/CnStatWidget/CnStatWidget.vue.js +1 -1
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js +3 -1
- 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 +55 -3
- 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/l10n/nl.json.js +3 -3
- package/dist/esm/library-style.js +1 -1
- package/dist/esm/nextcloud-vue.css +549 -338
- package/dist/esm/utils/fetchAggregate.js +25 -2
- package/dist/esm/utils/fetchAggregate.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 +36086 -35536
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +549 -338
- package/l10n/nl.json +3 -3
- package/package.json +1 -1
- package/src/components/CnAdminSettingsShell/CnAdminSettingsShell.vue +31 -2
- package/src/components/CnDashboardPage/CnDashboardPage.vue +9 -0
- package/src/components/CnIndexPage/CnIndexPage.vue +125 -15
- package/src/components/CnRadioCardsWidget/CnRadioCardsWidget.vue +54 -3
- package/src/components/CnReportsPage/CnReportsPage.vue +84 -4
- package/src/components/CnSettingsFormWidget/CnSettingsFormWidget.vue +110 -18
- package/src/components/CnStatWidget/CnStatWidget.vue +3 -1
- package/src/components/CnStatsBlockWidget/CnStatsBlockWidget.vue +92 -5
- package/src/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue +18 -4
- package/src/css/index-page.css +11 -0
- package/src/css/index.css +1 -0
- package/src/css/look-board-reports.css +117 -0
- package/src/css/look-board.css +32 -1
- package/src/schemas/app-manifest-v2.schema.json +2 -2
- package/src/utils/fetchAggregate.js +24 -1
- 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;
|
|
@@ -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'
|
|
@@ -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 }]">
|
|
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,32 @@ 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
|
+
|
|
244
291
|
/**
|
|
245
292
|
* How the single-source value is shown: a style name (`'currency'`,
|
|
246
293
|
* `'number'`, `'percent'`, `'decimal'`, `'duration-hours'`) or
|
|
@@ -400,6 +447,28 @@ export default {
|
|
|
400
447
|
return this.layout === 'rows'
|
|
401
448
|
},
|
|
402
449
|
|
|
450
|
+
/**
|
|
451
|
+
* Whether the widget draws its `stats` as tiles in a grid
|
|
452
|
+
* (`layout: "grid"` with at least one stat).
|
|
453
|
+
*
|
|
454
|
+
* @return {boolean}
|
|
455
|
+
* @spec openspec/changes/screens-stats-block-grid/specs/stats-block-grid/spec.md#requirement-a-stats-block-can-hold-stat-tiles-in-a-grid
|
|
456
|
+
*/
|
|
457
|
+
isGrid() {
|
|
458
|
+
return this.layout === 'grid' && Array.isArray(this.stats) && this.stats.length > 0
|
|
459
|
+
},
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* The grid's column count, clamped to 1-4.
|
|
463
|
+
*
|
|
464
|
+
* @return {number}
|
|
465
|
+
* @spec openspec/changes/screens-stats-block-grid/specs/stats-block-grid/spec.md#requirement-a-stats-block-can-hold-stat-tiles-in-a-grid
|
|
466
|
+
*/
|
|
467
|
+
gridColumns() {
|
|
468
|
+
const n = Math.floor(Number(this.columns))
|
|
469
|
+
return Number.isFinite(n) ? Math.min(4, Math.max(1, n)) : 2
|
|
470
|
+
},
|
|
471
|
+
|
|
403
472
|
/**
|
|
404
473
|
* The rows of the rows layout: one per entry, or a single row from the
|
|
405
474
|
* `dataSource` (labelled by `countLabel`). A row is `{ key, label,
|
|
@@ -617,7 +686,8 @@ export default {
|
|
|
617
686
|
// the cross-prop rule lives here (and, for manifests, in
|
|
618
687
|
// validateManifestV2's post-schema check).
|
|
619
688
|
const hasDataSource = !!this.dataSource
|
|
620
|
-
|
|
689
|
+
// A grid of stat tiles carries its sources in `stats`.
|
|
690
|
+
if (!this.isGrid && hasDataSource === this.hasEntries) {
|
|
621
691
|
// eslint-disable-next-line no-console
|
|
622
692
|
console.error('[CnStatsBlockWidget] Exactly one of `dataSource` / `entries` must be provided.')
|
|
623
693
|
}
|
|
@@ -764,7 +834,8 @@ export default {
|
|
|
764
834
|
params[`filter[${k}][${op}]`] = ov
|
|
765
835
|
}
|
|
766
836
|
} else if (v !== '' && v !== null && v !== undefined) {
|
|
767
|
-
|
|
837
|
+
// `field[gte]` goes out as `filter[field][gte]`, not `filter[field[gte]]`.
|
|
838
|
+
params[aggFilterParam(k)] = v
|
|
768
839
|
}
|
|
769
840
|
}
|
|
770
841
|
const res = await axios.get(url, { params })
|
|
@@ -890,6 +961,22 @@ export default {
|
|
|
890
961
|
color: var(--color-text-maxcontrast);
|
|
891
962
|
}
|
|
892
963
|
|
|
964
|
+
.cn-stats-block-widget--grid {
|
|
965
|
+
padding: 4px 24px 20px;
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
.cn-stats-block-widget__grid {
|
|
969
|
+
display: grid;
|
|
970
|
+
grid-template-columns: repeat(var(--cn-stats-block-columns, 2), minmax(0, 1fr));
|
|
971
|
+
gap: 16px 20px;
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
/* A tile in the grid is a part of the card, not a card: no box, no inset. */
|
|
975
|
+
.cn-stats-block-widget__grid-stat {
|
|
976
|
+
padding: 0;
|
|
977
|
+
min-width: 0;
|
|
978
|
+
}
|
|
979
|
+
|
|
893
980
|
.cn-stats-block-widget__rows {
|
|
894
981
|
display: flex;
|
|
895
982
|
flex-direction: column;
|
|
@@ -32,6 +32,7 @@ import { NcCheckboxRadioSwitch, NcNoteCard } from '@nextcloud/vue'
|
|
|
32
32
|
import { objectUrlOf, writeFields } from '../../utils/objectWrite.js'
|
|
33
33
|
import { readPath } from '../../utils/readPath.js'
|
|
34
34
|
import { serverErrorMessage } from '../../utils/serverErrorMessage.js'
|
|
35
|
+
import { switchIsOn, switchStoredValue } from '../../utils/switchValues.js'
|
|
35
36
|
|
|
36
37
|
/**
|
|
37
38
|
* CnSwitchRowsWidget — the `switch-rows` widget: a card of switches, each with
|
|
@@ -54,7 +55,8 @@ export default {
|
|
|
54
55
|
/**
|
|
55
56
|
* The widget configuration.
|
|
56
57
|
*
|
|
57
|
-
* - `rows` (required) — `[{ key, label, hint?, invert? }]`; `key` is a boolean field, a top-level name or a dot-path. `invert` shows the switch on when the field is false (a stored `disabled` read as "enabled").
|
|
58
|
+
* - `rows` (required) — `[{ key, label, hint?, invert?, values? }]`; `key` is a boolean field, a top-level name or a dot-path. `invert` shows the switch on when the field is false (a stored `disabled` read as "enabled"). `values: { on, off }` reads and writes those values instead of true/false (an app config flag stored as `"1"`/`"0"`).
|
|
59
|
+
* - `values` — `{ on, off }` for every row that declares none.
|
|
58
60
|
* - `save` — `{ url, method?, payload? }`, write through an app endpoint (tokens `{value}`, `{field}`, `{changes}`, `{object.<path>}`).
|
|
59
61
|
* - `readOnly`.
|
|
60
62
|
*
|
|
@@ -137,11 +139,22 @@ export default {
|
|
|
137
139
|
return typeof this.cnTranslate === 'function' ? this.cnTranslate(text) : text
|
|
138
140
|
},
|
|
139
141
|
|
|
142
|
+
/**
|
|
143
|
+
* A row's value map: its own `values`, else the widget's.
|
|
144
|
+
*
|
|
145
|
+
* @spec openspec/changes/screens-switch-value-map/specs/switch-rows/spec.md#requirement-a-switch-can-read-and-write-a-value-map
|
|
146
|
+
* @param {object} row The row.
|
|
147
|
+
* @return {object|undefined} The declared `values`.
|
|
148
|
+
*/
|
|
149
|
+
valuesOf(row) {
|
|
150
|
+
return row.values !== undefined ? row.values : this.content.values
|
|
151
|
+
},
|
|
152
|
+
|
|
140
153
|
/** Read every switch off the object. */
|
|
141
154
|
adopt() {
|
|
142
155
|
const values = {}
|
|
143
156
|
for (const row of this.rows) {
|
|
144
|
-
const stored = readPath(this.objectData || {}, row.key)
|
|
157
|
+
const stored = switchIsOn(readPath(this.objectData || {}, row.key), this.valuesOf(row))
|
|
145
158
|
values[row.key] = row.invert ? !stored : stored
|
|
146
159
|
}
|
|
147
160
|
this.saved = values
|
|
@@ -165,10 +178,11 @@ export default {
|
|
|
165
178
|
this.busy = row.key
|
|
166
179
|
this.values = { ...this.values, [row.key]: on }
|
|
167
180
|
const stored = row.invert ? !on : on
|
|
181
|
+
const values = this.valuesOf(row)
|
|
168
182
|
try {
|
|
169
|
-
const data = await writeFields(this.objectUrl, { [row.key]: stored }, { save: this.content.save, object: this.objectData })
|
|
183
|
+
const data = await writeFields(this.objectUrl, { [row.key]: switchStoredValue(stored, values) }, { save: this.content.save, object: this.objectData })
|
|
170
184
|
const answered = readPath(data, row.key)
|
|
171
|
-
const written = answered !== undefined ? answered
|
|
185
|
+
const written = answered !== undefined ? switchIsOn(answered, values) : stored
|
|
172
186
|
const shown = row.invert ? !written : written
|
|
173
187
|
this.saved = { ...this.saved, [row.key]: shown }
|
|
174
188
|
this.values = { ...this.values, [row.key]: shown }
|
package/src/css/index-page.css
CHANGED
|
@@ -10,6 +10,17 @@
|
|
|
10
10
|
padding: calc(5 * var(--default-grid-baseline));
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
/* A group heading in the header's Actions menu (`{ heading }` in items), as
|
|
14
|
+
the board's "Ga naar" and "Selectie": 12px bold capitals, muted. The menu
|
|
15
|
+
is a popover outside the page, so the class is not under the look class. */
|
|
16
|
+
.cn-index-header-menu__heading {
|
|
17
|
+
font-size: 12px;
|
|
18
|
+
font-weight: 700;
|
|
19
|
+
letter-spacing: 0.04em;
|
|
20
|
+
text-transform: uppercase;
|
|
21
|
+
color: var(--color-text-maxcontrast);
|
|
22
|
+
}
|
|
23
|
+
|
|
13
24
|
/* The board's header buttons (`headerButtons`) at the end of the title row,
|
|
14
25
|
centred on the title and its count line. They wrap under the title on a
|
|
15
26
|
narrow page. */
|
package/src/css/index.css
CHANGED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The reports page under the board look (screens-reports-page-board-look,
|
|
3
|
+
* board DcRapportages): a 28px title over a muted lead, one section per
|
|
4
|
+
* category headed at 18px, and two-up cards with an icon tile. Scoped under
|
|
5
|
+
* `.cn-look-board` with the page guard, so an app without the look renders as
|
|
6
|
+
* before.
|
|
7
|
+
*/
|
|
8
|
+
.cn-look-board .cn-reports-page:not(.cn-look-nextcloud *) {
|
|
9
|
+
box-sizing: border-box;
|
|
10
|
+
display: flex;
|
|
11
|
+
flex-direction: column;
|
|
12
|
+
gap: var(--cn-board-section-gap);
|
|
13
|
+
padding: var(--cn-board-content-padding);
|
|
14
|
+
max-width: var(--cn-board-content-max-width);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.cn-look-board .cn-reports-page:not(.cn-look-nextcloud *) .cn-reports-page__header {
|
|
18
|
+
display: flex;
|
|
19
|
+
flex-direction: column;
|
|
20
|
+
gap: 14px;
|
|
21
|
+
padding: 4px 0 8px;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/* The 56px inset clears the navigation toggle only while the navigation is
|
|
25
|
+
closed, as on the other board pages. */
|
|
26
|
+
.cn-look-board:has(.app-navigation--closed) .cn-reports-page:not(.cn-look-nextcloud *) .cn-reports-page__header {
|
|
27
|
+
padding-inline-start: 56px;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.cn-look-board .cn-reports-page:not(.cn-look-nextcloud *) .cn-reports-page__title {
|
|
31
|
+
margin: 0;
|
|
32
|
+
font-size: 28px;
|
|
33
|
+
font-weight: 700;
|
|
34
|
+
line-height: 1.2;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.cn-look-board .cn-reports-page:not(.cn-look-nextcloud *) .cn-reports-page__description {
|
|
38
|
+
margin: 0;
|
|
39
|
+
font-size: 14px;
|
|
40
|
+
color: var(--color-text-maxcontrast);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.cn-look-board .cn-reports-page:not(.cn-look-nextcloud *) .cn-reports-page__section {
|
|
44
|
+
display: flex;
|
|
45
|
+
flex-direction: column;
|
|
46
|
+
gap: 12px;
|
|
47
|
+
/* A host's own `section` rule (a width cap, a margin) does not reach it. */
|
|
48
|
+
max-width: none;
|
|
49
|
+
margin: 0;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.cn-look-board .cn-reports-page:not(.cn-look-nextcloud *) .cn-reports-page__section-title {
|
|
53
|
+
margin: 0;
|
|
54
|
+
font-size: 18px;
|
|
55
|
+
font-weight: 700;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.cn-look-board .cn-reports-page:not(.cn-look-nextcloud *) .cn-reports-page__grid {
|
|
59
|
+
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
|
60
|
+
gap: 14px;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.cn-look-board .cn-reports-page:not(.cn-look-nextcloud *) .cn-reports-page__card {
|
|
64
|
+
box-sizing: border-box;
|
|
65
|
+
flex-direction: row;
|
|
66
|
+
align-items: flex-start;
|
|
67
|
+
gap: 14px;
|
|
68
|
+
padding: 18px 20px;
|
|
69
|
+
border: 1px solid var(--color-border);
|
|
70
|
+
border-radius: var(--cn-board-card-radius);
|
|
71
|
+
background: var(--color-main-background);
|
|
72
|
+
color: var(--color-main-text);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.cn-look-board .cn-reports-page:not(.cn-look-nextcloud *) .cn-reports-page__card:hover,
|
|
76
|
+
.cn-look-board .cn-reports-page:not(.cn-look-nextcloud *) .cn-reports-page__card:focus-visible {
|
|
77
|
+
background: var(--color-background-hover);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.cn-look-board .cn-reports-page:not(.cn-look-nextcloud *) .cn-reports-page__card-icon {
|
|
81
|
+
flex: none;
|
|
82
|
+
display: flex;
|
|
83
|
+
align-items: center;
|
|
84
|
+
justify-content: center;
|
|
85
|
+
width: 44px;
|
|
86
|
+
height: 44px;
|
|
87
|
+
border-radius: 10px;
|
|
88
|
+
background: var(--color-primary-element-light);
|
|
89
|
+
color: var(--color-primary-element-light-text);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.cn-look-board .cn-reports-page:not(.cn-look-nextcloud *) .cn-reports-page__card-text {
|
|
93
|
+
display: flex;
|
|
94
|
+
flex-direction: column;
|
|
95
|
+
gap: 4px;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.cn-look-board .cn-reports-page:not(.cn-look-nextcloud *) .cn-reports-page__card-title {
|
|
99
|
+
font-size: 17px;
|
|
100
|
+
font-weight: 700;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.cn-look-board .cn-reports-page:not(.cn-look-nextcloud *) .cn-reports-page__card-description {
|
|
104
|
+
font-size: 14px;
|
|
105
|
+
line-height: 1.5;
|
|
106
|
+
color: var(--color-text-maxcontrast);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/* The widgets under the cards sit in the page's own padding: the dashboard
|
|
110
|
+
inside draws no second frame. */
|
|
111
|
+
.cn-look-board .cn-reports-page:not(.cn-look-nextcloud *) .cn-reports-page__widgets {
|
|
112
|
+
margin-top: 0;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.cn-look-board .cn-reports-page:not(.cn-look-nextcloud *) .cn-dashboard-page {
|
|
116
|
+
padding: 0;
|
|
117
|
+
}
|
package/src/css/look-board.css
CHANGED
|
@@ -436,9 +436,20 @@
|
|
|
436
436
|
align-items: start;
|
|
437
437
|
}
|
|
438
438
|
|
|
439
|
-
/*
|
|
439
|
+
/* An app component that wraps its sections in one element (decidiq's
|
|
440
|
+
Settings.vue) takes no box of its own: its sections are grid items and
|
|
441
|
+
cards, as if the shell held them directly. Only an element whose own
|
|
442
|
+
children are sections; anything else keeps spanning the grid. */
|
|
443
|
+
.cn-look-board .cn-admin-settings-shell:not(.cn-look-nextcloud *) > :not(.settings-section):has(> .settings-section),
|
|
444
|
+
.cn-look-board.cn-admin-settings-shell:not(.cn-look-nextcloud *) > :not(.settings-section):has(> .settings-section) {
|
|
445
|
+
display: contents;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/* The card: a section of the shell (also one inside a wrapping element), or a CnSettingsCard of the page. */
|
|
440
449
|
.cn-look-board .cn-admin-settings-shell:not(.cn-look-nextcloud *) > .settings-section,
|
|
441
450
|
.cn-look-board.cn-admin-settings-shell:not(.cn-look-nextcloud *) > .settings-section,
|
|
451
|
+
.cn-look-board .cn-admin-settings-shell:not(.cn-look-nextcloud *) > :not(.settings-section):has(> .settings-section) > .settings-section,
|
|
452
|
+
.cn-look-board.cn-admin-settings-shell:not(.cn-look-nextcloud *) > :not(.settings-section):has(> .settings-section) > .settings-section,
|
|
442
453
|
.cn-look-board .cn-settings-page__sections:not(.cn-look-nextcloud *) > .cn-settings-card {
|
|
443
454
|
box-sizing: border-box;
|
|
444
455
|
display: flex;
|
|
@@ -465,7 +476,12 @@
|
|
|
465
476
|
display: none;
|
|
466
477
|
}
|
|
467
478
|
|
|
479
|
+
/* NcSettingsSection draws its name `display: inline-flex; justify-content:
|
|
480
|
+
center`; in the card's flex column the name spans the card, so the title
|
|
481
|
+
sat centred. The board's card titles start at the left edge. */
|
|
468
482
|
.cn-look-board .settings-section:not(.cn-look-nextcloud *) .settings-section__name {
|
|
483
|
+
justify-content: flex-start;
|
|
484
|
+
text-align: start;
|
|
469
485
|
margin: 0;
|
|
470
486
|
font-size: 19px;
|
|
471
487
|
font-weight: 700;
|
|
@@ -667,6 +683,21 @@
|
|
|
667
683
|
margin-inline-start: auto;
|
|
668
684
|
}
|
|
669
685
|
|
|
686
|
+
/* The title and meta take the room the pill and the date leave, and a long
|
|
687
|
+
title wraps inside it: the pill and the date stay on the title line, as the
|
|
688
|
+
board draws them. They drop under the text only when the card is too narrow
|
|
689
|
+
to hold 240px of text beside them (PqMijnWerk). */
|
|
690
|
+
.cn-look-board .cn-dashboard-page:not(.cn-look-nextcloud *) .cn-record-list__main {
|
|
691
|
+
flex: 1 1 240px;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
/* ---- Saved views on a dashboard card (PqMijnWerk) ------------------------ */
|
|
695
|
+
/* The card body is flush, so the list and its empty line carry the card's
|
|
696
|
+
inset themselves, the same 24px at the sides as a record list. */
|
|
697
|
+
.cn-look-board .cn-dashboard-page:not(.cn-look-nextcloud *) .cn-saved-views-widget {
|
|
698
|
+
padding: 4px 24px 20px;
|
|
699
|
+
}
|
|
700
|
+
|
|
670
701
|
/* The board draws no chevron after the pill and date. */
|
|
671
702
|
.cn-look-board .cn-dashboard-page:not(.cn-look-nextcloud *) .cn-record-list__chevron {
|
|
672
703
|
display: none;
|