@conduction/nextcloud-vue 2.144.0 → 2.145.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/CnActionDialog/CnActionDialog.vue.js +1 -1
- package/dist/esm/components/CnActionDialog/CnActionDialog.vue2.js.map +1 -1
- package/dist/esm/components/CnActionDialog/CnActionDialog.vue3.js +1 -0
- package/dist/esm/components/CnActionDialog/CnActionDialog.vue3.js.map +1 -1
- package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue.js +1 -1
- package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue2.js +55 -2
- 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/CnAuditTrailWidget/CnAuditTrailWidget.vue.js +1 -1
- package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue2.js +16 -0
- package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue3.js +3 -1
- package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue.js +1 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue2.js +1 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue3.js +3 -3
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue3.js.map +1 -1
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue.js +1 -1
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js +2 -2
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +6 -3
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
- package/dist/esm/components/CnMailTab/CnMailTab.vue.js +1 -1
- package/dist/esm/components/CnMailTab/CnMailTab.vue2.js.map +1 -1
- package/dist/esm/components/CnMailTab/CnMailTab.vue3.js +2 -2
- package/dist/esm/components/CnMailTab/CnMailTab.vue3.js.map +1 -1
- package/dist/esm/components/CnNotificationMatrix/CnNotificationMatrix.vue.js +1 -1
- package/dist/esm/components/CnNotificationMatrix/CnNotificationMatrix.vue2.js.map +1 -1
- package/dist/esm/components/CnNotificationMatrix/CnNotificationMatrix.vue3.js.map +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +73 -12
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js +165 -126
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnSettingsField/CnSettingsField.vue.js +1 -1
- package/dist/esm/components/CnSettingsField/CnSettingsField.vue2.js +14 -0
- package/dist/esm/components/CnSettingsField/CnSettingsField.vue2.js.map +1 -1
- package/dist/esm/components/CnSettingsField/CnSettingsField.vue3.js +2 -1
- package/dist/esm/components/CnSettingsField/CnSettingsField.vue3.js.map +1 -1
- package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue.js +1 -1
- package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js +13 -8
- package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js.map +1 -1
- package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue3.js.map +1 -1
- package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue.js +1 -1
- package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue2.js +10 -2
- package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue3.js.map +1 -1
- package/dist/esm/composables/indexSources.js +35 -9
- package/dist/esm/composables/indexSources.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +2 -0
- package/dist/esm/l10n/nl.json.js.map +1 -1
- package/dist/esm/library-style.js +1 -1
- package/dist/esm/nextcloud-vue.css +742 -326
- package/dist/esm/utils/builtInFormatters.js +18 -1
- package/dist/esm/utils/builtInFormatters.js.map +1 -1
- package/dist/esm/utils/localHeaderRows.js +95 -0
- package/dist/esm/utils/localHeaderRows.js.map +1 -0
- package/dist/nextcloud-vue.cjs.js +514 -179
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +742 -326
- package/l10n/nl.json +2 -0
- package/package.json +1 -1
- package/src/components/CnActionDialog/CnActionDialog.vue +8 -1
- package/src/components/CnAuditTrailCard/CnAuditTrailCard.vue +55 -2
- package/src/components/CnAuditTrailWidget/CnAuditTrailWidget.vue +18 -0
- package/src/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue +2 -1
- package/src/components/CnFieldListWidget/CnFieldListWidget.vue +2 -2
- package/src/components/CnIndexPage/CnIndexPage.vue +6 -3
- package/src/components/CnMailTab/CnMailTab.vue +6 -4
- package/src/components/CnNotificationMatrix/CnNotificationMatrix.vue +4 -1
- package/src/components/CnRecordListWidget/CnRecordListWidget.vue +131 -20
- package/src/components/CnSettingsField/CnSettingsField.vue +15 -0
- package/src/components/CnWidgetObjectTable/CnWidgetObjectTable.vue +13 -8
- package/src/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue +10 -2
- package/src/composables/indexSources.js +35 -9
- package/src/css/dialog.css +18 -9
- package/src/css/look-board-detail.css +219 -23
- package/src/css/look-board-index.css +38 -4
- package/src/css/look-board.css +138 -8
- package/src/css/table.css +8 -1
- package/src/utils/builtInFormatters.js +17 -0
- package/src/utils/localHeaderRows.js +91 -0
package/l10n/nl.json
CHANGED
|
@@ -1946,6 +1946,8 @@
|
|
|
1946
1946
|
"Absent today": "Vandaag afwezig",
|
|
1947
1947
|
"{message} until {time}": "{message} tot {time}",
|
|
1948
1948
|
"Away": "Afwezig",
|
|
1949
|
+
"Present": "Aanwezig",
|
|
1950
|
+
"Absent until {date}": "Afwezig tot {date}",
|
|
1949
1951
|
"Busy": "Bezet",
|
|
1950
1952
|
"No one in the team": "Niemand in het team",
|
|
1951
1953
|
"Also replace the criteria with the current filters": "Vervang ook de criteria door de huidige filters",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@conduction/nextcloud-vue",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.145.0",
|
|
4
4
|
"description": "Shared Vue component library for Conduction Nextcloud apps — complements @nextcloud/vue with higher-level components, OpenRegister integration, and NL Design System support",
|
|
5
5
|
"homepage": "https://github.com/ConductionNL/nextcloud-vue#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -145,7 +145,7 @@
|
|
|
145
145
|
:heading="text(config.note.title)"
|
|
146
146
|
data-testid="cn-action-dialog-note">
|
|
147
147
|
<template v-if="config.note.icon" #icon>
|
|
148
|
-
<CnIcon :name="config.note.icon" :size="20" />
|
|
148
|
+
<CnIcon class="cn-action-dialog__note-icon" :name="config.note.icon" :size="20" />
|
|
149
149
|
</template>
|
|
150
150
|
{{ text(config.note.text) }}
|
|
151
151
|
</NcNoteCard>
|
|
@@ -951,6 +951,13 @@ export default {
|
|
|
951
951
|
|
|
952
952
|
/* A checkbox field is a tinted box with a 2px edge, its label bold and its
|
|
953
953
|
hint under it (DqArchiveren). */
|
|
954
|
+
/* A note's own icon stands on the heading's line, as the variant's icon
|
|
955
|
+
does (NcNoteCard centres a slotted icon on the whole note). */
|
|
956
|
+
.cn-action-dialog__note-icon {
|
|
957
|
+
align-self: flex-start;
|
|
958
|
+
margin-top: 1px;
|
|
959
|
+
}
|
|
960
|
+
|
|
954
961
|
.cn-action-dialog__checkbox {
|
|
955
962
|
padding: 12px 14px;
|
|
956
963
|
border: 2px solid var(--color-primary-element);
|
|
@@ -69,9 +69,21 @@ import CnDetailCard from '../CnDetailCard/CnDetailCard.vue'
|
|
|
69
69
|
import { formatBoardDate, formatTimeOfDay } from '../../utils/boardDate.js'
|
|
70
70
|
import { buildHeaders, prefixUrl } from '../../utils/index.js'
|
|
71
71
|
import { leafSourceUrl } from '../../utils/leafSourceUrl.js'
|
|
72
|
+
import { readPath } from '../../utils/readPath.js'
|
|
72
73
|
import { userDisplayName } from '../../utils/userDisplayName.js'
|
|
73
74
|
import { userLocale } from '../../utils/userLocale.js'
|
|
74
75
|
|
|
76
|
+
/**
|
|
77
|
+
* A changed value as an entry records it: OpenRegister writes `{ old, new }`,
|
|
78
|
+
* other sources the value itself.
|
|
79
|
+
*
|
|
80
|
+
* @param {unknown} value The recorded change.
|
|
81
|
+
* @return {unknown} The value it changed to.
|
|
82
|
+
*/
|
|
83
|
+
function newValueOf(value) {
|
|
84
|
+
return value !== null && typeof value === 'object' && Object.hasOwn(value, 'new') ? value.new : value
|
|
85
|
+
}
|
|
86
|
+
|
|
75
87
|
/**
|
|
76
88
|
* @param {object} entry An audit entry.
|
|
77
89
|
* @return {number} When it happened, in ms; 0 when unreadable.
|
|
@@ -202,6 +214,18 @@ export default {
|
|
|
202
214
|
* @type {{[key: string]: {[value: string]: string}}|null}
|
|
203
215
|
*/
|
|
204
216
|
valueLabels: { type: Object, default: null },
|
|
217
|
+
/**
|
|
218
|
+
* What happened, in words, by the field an entry changed: rules
|
|
219
|
+
* `[{ action?, field, to?, label }]`, the first whose `field` the entry
|
|
220
|
+
* changed (to `to`, when given) wins over `actionLabels`. A label fills
|
|
221
|
+
* the same tokens, plus `{object.<path>}` from `object`
|
|
222
|
+
* ("Version {object.version} published" for `isDraft` set to false).
|
|
223
|
+
*
|
|
224
|
+
* @type {Array<{action?: string, field: string, to?: unknown, label: string}>}
|
|
225
|
+
*/
|
|
226
|
+
changeLabels: { type: Array, default: () => [] },
|
|
227
|
+
/** The audited record, for `{object.<path>}` in a label; null without one. */
|
|
228
|
+
object: { type: Object, default: null },
|
|
205
229
|
/**
|
|
206
230
|
* Records beside the object whose events belong in its history:
|
|
207
231
|
* `[{ as, register, schema, filter, limit? }]`. The card lists the
|
|
@@ -394,12 +418,16 @@ export default {
|
|
|
394
418
|
* @return {string} The line.
|
|
395
419
|
*/
|
|
396
420
|
formatAction(entry) {
|
|
397
|
-
const label = this.labelFor(entry)
|
|
421
|
+
const label = this.changeLabelFor(entry) ?? this.labelFor(entry)
|
|
398
422
|
if (label !== null) {
|
|
399
423
|
return label
|
|
400
424
|
.replace('{actor}', this.actorName(entry))
|
|
401
425
|
.replace('{state}', entry.stateAfter || '')
|
|
402
426
|
.replace(/\{changed\.([\w-]+)\}/g, (match, key) => this.changedValue(entry, key))
|
|
427
|
+
.replace(/\{object\.([\w.-]+)\}/g, (match, path) => {
|
|
428
|
+
const value = this.object ? readPath(this.object, path) : undefined
|
|
429
|
+
return value === null || value === undefined || typeof value === 'object' ? '' : String(value)
|
|
430
|
+
})
|
|
403
431
|
}
|
|
404
432
|
const action = entry.action || entry.event || this.actionLabel
|
|
405
433
|
return entry.stateAfter ? `${action} (${entry.stateAfter})` : action
|
|
@@ -416,7 +444,7 @@ export default {
|
|
|
416
444
|
*/
|
|
417
445
|
changedValue(entry, key) {
|
|
418
446
|
const changed = entry && typeof entry.changed === 'object' && entry.changed !== null ? entry.changed : {}
|
|
419
|
-
const value = changed[key]
|
|
447
|
+
const value = newValueOf(changed[key])
|
|
420
448
|
if (value === null || value === undefined || typeof value === 'object') {
|
|
421
449
|
return ''
|
|
422
450
|
}
|
|
@@ -424,6 +452,31 @@ export default {
|
|
|
424
452
|
return words && typeof words[value] === 'string' ? words[value] : String(value)
|
|
425
453
|
},
|
|
426
454
|
|
|
455
|
+
/**
|
|
456
|
+
* The `changeLabels` rule an entry matches: the first whose field the
|
|
457
|
+
* entry changed, for its action and to its value when the rule names them.
|
|
458
|
+
*
|
|
459
|
+
* @param {object} entry The entry.
|
|
460
|
+
* @return {string|null} The label, or null when no rule matches.
|
|
461
|
+
*/
|
|
462
|
+
changeLabelFor(entry) {
|
|
463
|
+
const changed = entry && typeof entry.changed === 'object' && entry.changed !== null ? entry.changed : {}
|
|
464
|
+
const action = entry.action || entry.event || ''
|
|
465
|
+
for (const rule of Array.isArray(this.changeLabels) ? this.changeLabels : []) {
|
|
466
|
+
if (!rule || typeof rule.label !== 'string' || rule.label === '' || typeof rule.field !== 'string' || !Object.hasOwn(changed, rule.field)) {
|
|
467
|
+
continue
|
|
468
|
+
}
|
|
469
|
+
if (rule.action && rule.action !== action) {
|
|
470
|
+
continue
|
|
471
|
+
}
|
|
472
|
+
if (Object.hasOwn(rule, 'to') && String(newValueOf(changed[rule.field])) !== String(rule.to)) {
|
|
473
|
+
continue
|
|
474
|
+
}
|
|
475
|
+
return rule.label
|
|
476
|
+
}
|
|
477
|
+
return null
|
|
478
|
+
},
|
|
479
|
+
|
|
427
480
|
/**
|
|
428
481
|
* The `actionLabels` entry for an entry: by action and state first,
|
|
429
482
|
* then by action.
|
|
@@ -20,6 +20,8 @@
|
|
|
20
20
|
:excludeActions="resolvedExcludeActions"
|
|
21
21
|
:actionLabels="actionLabels"
|
|
22
22
|
:valueLabels="valueLabels"
|
|
23
|
+
:changeLabels="changeLabels"
|
|
24
|
+
:object="ctx.object || ctx.objectData || null"
|
|
23
25
|
:relatedSources="relatedSources"
|
|
24
26
|
:footnote="footnote"
|
|
25
27
|
v-bind="emptyText ? { noEntriesLabel: emptyText } : {}" />
|
|
@@ -193,6 +195,22 @@ export default {
|
|
|
193
195
|
.map(([key, text]) => [key, translate(text)]))
|
|
194
196
|
},
|
|
195
197
|
|
|
198
|
+
/**
|
|
199
|
+
* `content.changeLabels`, each rule's label through the app's translate.
|
|
200
|
+
*
|
|
201
|
+
* @return {Array<object>}
|
|
202
|
+
*/
|
|
203
|
+
changeLabels() {
|
|
204
|
+
const rules = this.content.changeLabels
|
|
205
|
+
if (!Array.isArray(rules)) {
|
|
206
|
+
return []
|
|
207
|
+
}
|
|
208
|
+
const translate = typeof this.cnTranslate === 'function' ? this.cnTranslate : (text) => text
|
|
209
|
+
return rules
|
|
210
|
+
.filter((rule) => rule && typeof rule.label === 'string' && rule.label !== '')
|
|
211
|
+
.map((rule) => ({ ...rule, label: translate(rule.label) }))
|
|
212
|
+
},
|
|
213
|
+
|
|
196
214
|
/**
|
|
197
215
|
* `content.footnote`, a muted line under the trail with the record's
|
|
198
216
|
* fields filled in ("Reference {identifier}"); '' when a field is empty.
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
v-else
|
|
32
32
|
type="button"
|
|
33
33
|
class="cn-detail-page__catalog-action"
|
|
34
|
+
:class="{ 'cn-detail-page__catalog-action--add': action.add === true }"
|
|
34
35
|
data-testid="cn-detail-widget-header-action"
|
|
35
36
|
@click="action.run()">
|
|
36
37
|
<CnIcon v-if="action.icon" :name="action.icon" :size="16" />
|
|
@@ -56,7 +57,7 @@ export default {
|
|
|
56
57
|
components: { CnIcon, CnStatusBadge, NcButton },
|
|
57
58
|
|
|
58
59
|
props: {
|
|
59
|
-
/** The reported header: `{ pill, note, actions: [{ label, icon?, variant?, run }] }
|
|
60
|
+
/** The reported header: `{ pill, note, actions: [{ label, icon?, variant?, add?, run }] }`; `add: true` marks a list's Add. */
|
|
60
61
|
header: {
|
|
61
62
|
type: Object,
|
|
62
63
|
required: true,
|
|
@@ -607,7 +607,7 @@ export default {
|
|
|
607
607
|
/**
|
|
608
608
|
* The card title row's actions: the add button with `add.placement: "title"`.
|
|
609
609
|
*
|
|
610
|
-
* @return {Array<{label: string, icon: string, run: () => void}>} The actions.
|
|
610
|
+
* @return {Array<{label: string, icon: string, add: boolean, run: () => void}>} The actions.
|
|
611
611
|
*/
|
|
612
612
|
titleActions() {
|
|
613
613
|
if (!(this.canAdd && this.addTrigger && this.addPlacement === 'title')) {
|
|
@@ -616,7 +616,7 @@ export default {
|
|
|
616
616
|
const run = () => {
|
|
617
617
|
this.adding = !this.adding
|
|
618
618
|
}
|
|
619
|
-
return [{ label: this.addLabel, icon: 'Plus', run }]
|
|
619
|
+
return [{ label: this.addLabel, icon: 'Plus', add: true, run }]
|
|
620
620
|
},
|
|
621
621
|
|
|
622
622
|
addLabel() {
|
|
@@ -8940,7 +8940,7 @@ export default {
|
|
|
8940
8940
|
// through the app's label lookup.
|
|
8941
8941
|
const countText = this.presentedCountText
|
|
8942
8942
|
// The note after the breadcrumb already says how many there are.
|
|
8943
|
-
if (!countText && !this.countSubtitle && this.headerMetaCarriesCount) {
|
|
8943
|
+
if (!countText && !this.countSubtitle && (this.headerMetaCarriesCount || this.showCount === false)) {
|
|
8944
8944
|
return this.description
|
|
8945
8945
|
}
|
|
8946
8946
|
if (!countText && !this.countSubtitle) {
|
|
@@ -9877,9 +9877,12 @@ export default {
|
|
|
9877
9877
|
}
|
|
9878
9878
|
const sorted = [...groups].sort((a, b) => b.count - a.count)
|
|
9879
9879
|
const total = sorted.reduce((sum, g) => sum + g.count, 0)
|
|
9880
|
+
// Each tab asks to be counted as the list counts it (within the
|
|
9881
|
+
// active filters, through the list endpoint); the grouped number,
|
|
9882
|
+
// over the page filter only, stands until that count arrives.
|
|
9880
9883
|
this.generatedQuickFilters = [
|
|
9881
|
-
{ label: from.allLabel || t('nextcloud-vue', 'All'), filter: {}, default: true, count: total },
|
|
9882
|
-
...sorted.map((g) => ({ label: (labels && labels[g.key]) || g.key, filter: { [from.field]: g.key }, count: g.count })),
|
|
9884
|
+
{ label: from.allLabel || t('nextcloud-vue', 'All'), filter: {}, default: true, count: total, showCount: true },
|
|
9885
|
+
...sorted.map((g) => ({ label: (labels && labels[g.key]) || g.key, filter: { [from.field]: g.key }, count: g.count, showCount: true })),
|
|
9883
9886
|
]
|
|
9884
9887
|
},
|
|
9885
9888
|
|
|
@@ -19,9 +19,11 @@
|
|
|
19
19
|
<h3 v-if="heading" class="cn-mail-tab__title">
|
|
20
20
|
{{ heading }}
|
|
21
21
|
</h3>
|
|
22
|
-
<p
|
|
22
|
+
<!-- Divs, not <p>: a theme that sizes every paragraph with !important
|
|
23
|
+
would override the card's 14px description. -->
|
|
24
|
+
<div v-if="description" class="cn-mail-tab__subtitle">
|
|
23
25
|
{{ description }}
|
|
24
|
-
</
|
|
26
|
+
</div>
|
|
25
27
|
</div>
|
|
26
28
|
<div v-if="hasHeaderActions" class="cn-mail-tab__header-actions">
|
|
27
29
|
<NcButton
|
|
@@ -77,9 +79,9 @@
|
|
|
77
79
|
{{ t('nextcloud-vue', 'Empty draft') }}
|
|
78
80
|
</NcButton>
|
|
79
81
|
</div>
|
|
80
|
-
<
|
|
82
|
+
<div v-if="hint" class="cn-mail-tab__hint">
|
|
81
83
|
{{ hint }}
|
|
82
|
-
</
|
|
84
|
+
</div>
|
|
83
85
|
|
|
84
86
|
<NcNoteCard
|
|
85
87
|
v-if="error"
|
|
@@ -1135,7 +1135,10 @@ export default {
|
|
|
1135
1135
|
min-inline-size: 20px;
|
|
1136
1136
|
block-size: 20px;
|
|
1137
1137
|
min-block-size: 20px;
|
|
1138
|
-
|
|
1138
|
+
/* A block box, so the line's baseline adds no height: the row is
|
|
1139
|
+
42px as on the board (10px, the 1px-low checkbox, 10px, the rule). */
|
|
1140
|
+
display: block;
|
|
1141
|
+
margin: 1px auto 0;
|
|
1139
1142
|
}
|
|
1140
1143
|
|
|
1141
1144
|
.cn-notification-matrix__group {
|
|
@@ -53,15 +53,37 @@
|
|
|
53
53
|
scope="col"
|
|
54
54
|
:aria-sort="col.sortable ? sortStateOf(col.key) : undefined"
|
|
55
55
|
:class="{ 'cn-record-list__cell--end': col.align === 'end' }">
|
|
56
|
-
<
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
56
|
+
<span v-if="col.sortable || tableFilterable" class="cn-record-list__head">
|
|
57
|
+
<button
|
|
58
|
+
v-if="col.sortable"
|
|
59
|
+
type="button"
|
|
60
|
+
class="cn-record-list__sort"
|
|
61
|
+
:data-testid="`cn-record-list-sort-${col.key}`"
|
|
62
|
+
@click="toggleTableSort(col.key)">
|
|
63
|
+
{{ col.label }}
|
|
64
|
+
<span class="cn-record-list__sort-mark" aria-hidden="true">{{ sortMarkOf(col.key) }}</span>
|
|
65
|
+
</button>
|
|
66
|
+
<template v-else>
|
|
67
|
+
{{ col.label }}
|
|
68
|
+
</template>
|
|
69
|
+
<!-- `content.filterable`: the head offers the values the column holds and keeps the rows with a chosen one. -->
|
|
70
|
+
<NcActions
|
|
71
|
+
v-if="tableFilterable"
|
|
72
|
+
class="cn-record-list__head-filter"
|
|
73
|
+
:aria-label="filterLabelOf(col.label)"
|
|
74
|
+
:data-testid="`cn-record-list-filter-${col.key}`">
|
|
75
|
+
<template #icon>
|
|
76
|
+
<FilterOutline :size="14" />
|
|
77
|
+
</template>
|
|
78
|
+
<NcActionCheckbox
|
|
79
|
+
v-for="option in tableFilterOptions(col.key)"
|
|
80
|
+
:key="option"
|
|
81
|
+
:modelValue="(tableFilters[col.key] || []).includes(option)"
|
|
82
|
+
@update:modelValue="toggleTableFilter(col.key, option)">
|
|
83
|
+
{{ option }}
|
|
84
|
+
</NcActionCheckbox>
|
|
85
|
+
</NcActions>
|
|
86
|
+
</span>
|
|
65
87
|
<template v-else>
|
|
66
88
|
{{ col.label }}
|
|
67
89
|
</template>
|
|
@@ -550,10 +572,11 @@
|
|
|
550
572
|
import { subscribe, unsubscribe } from '@nextcloud/event-bus'
|
|
551
573
|
import { translate as t } from '@nextcloud/l10n'
|
|
552
574
|
import { generateUrl } from '@nextcloud/router'
|
|
553
|
-
import { NcActionButton, NcActionLink, NcActions, NcButton, NcLoadingIcon } from '@nextcloud/vue'
|
|
575
|
+
import { NcActionButton, NcActionCheckbox, NcActionLink, NcActions, NcButton, NcLoadingIcon } from '@nextcloud/vue'
|
|
554
576
|
import { inject, ref } from 'vue'
|
|
555
577
|
import Check from 'vue-material-design-icons/Check.vue'
|
|
556
578
|
import ChevronRight from 'vue-material-design-icons/ChevronRight.vue'
|
|
579
|
+
import FilterOutline from 'vue-material-design-icons/FilterOutline.vue'
|
|
557
580
|
import CnIcon from '../CnIcon/CnIcon.vue'
|
|
558
581
|
import CnStatusBadge from '../CnStatusBadge/CnStatusBadge.vue'
|
|
559
582
|
import CnRecordNoteRows from './CnRecordNoteRows.vue'
|
|
@@ -649,7 +672,7 @@ function unwrap(value) {
|
|
|
649
672
|
export default {
|
|
650
673
|
name: 'CnRecordListWidget',
|
|
651
674
|
|
|
652
|
-
components: { Check, ChevronRight, CnIcon, CnRecordNoteRows, CnStatusBadge, NcActionButton, NcActionLink, NcActions, NcButton, NcLoadingIcon },
|
|
675
|
+
components: { Check, ChevronRight, CnIcon, CnRecordNoteRows, CnStatusBadge, FilterOutline, NcActionButton, NcActionCheckbox, NcActionLink, NcActions, NcButton, NcLoadingIcon },
|
|
653
676
|
|
|
654
677
|
inject: {
|
|
655
678
|
/** Detail-page object context (`{ objectId, object, register, schema }`). */
|
|
@@ -714,6 +737,8 @@ export default {
|
|
|
714
737
|
* without a value.
|
|
715
738
|
* `density: "compact"` draws the rows 10px above and below, at least
|
|
716
739
|
* 56px tall, with the title at 500 (DqProcesanalyse).
|
|
740
|
+
* On `cards` it draws the small framed card: 12px 14px in, the title
|
|
741
|
+
* 15px/700 with the pill at its end (DqInspectie).
|
|
717
742
|
* `density: "stacked"` stands the rows 10px apart with no rule and no
|
|
718
743
|
* row padding (DqDagAfsluiten "Morgen eerst").
|
|
719
744
|
* In `rows`, `text` is a line under the meta line ("12 dagen stil"),
|
|
@@ -809,6 +834,8 @@ export default {
|
|
|
809
834
|
return {
|
|
810
835
|
/** The reader's sort of a `table`: the column key and `ascending` or `descending`. */
|
|
811
836
|
tableSort: { key: '', dir: '' },
|
|
837
|
+
/** The reader's head filters of a `table`: `{ [columnKey]: cellTexts[] }`. */
|
|
838
|
+
tableFilters: {},
|
|
812
839
|
rows: [],
|
|
813
840
|
/** The `aggregates` figures per row id: `{ [rowId]: { [key]: value } }`. */
|
|
814
841
|
aggregateValues: {},
|
|
@@ -1183,7 +1210,16 @@ export default {
|
|
|
1183
1210
|
const isTemplate = (value) => typeof value === 'string' || (Array.isArray(value) && value.length > 0 && value.every((entry) => typeof entry === 'string'))
|
|
1184
1211
|
return columns
|
|
1185
1212
|
.filter((col) => col && typeof col === 'object' && isTemplate(col.value))
|
|
1186
|
-
.map((col, index) => ({ ...col, key: `col-${index}`, label: typeof col.label === 'string' ? this.translate(col.label) : '' }))
|
|
1213
|
+
.map((col, index) => ({ ...col, key: `col-${index}`, label: typeof col.label === 'string' ? this.translate(col.label) : '', sortable: col.sortable === true || this.content?.sortable === true }))
|
|
1214
|
+
},
|
|
1215
|
+
|
|
1216
|
+
/**
|
|
1217
|
+
* Whether the `table` heads filter (`content.filterable`).
|
|
1218
|
+
*
|
|
1219
|
+
* @return {boolean}
|
|
1220
|
+
*/
|
|
1221
|
+
tableFilterable() {
|
|
1222
|
+
return this.content?.filterable === true
|
|
1187
1223
|
},
|
|
1188
1224
|
|
|
1189
1225
|
/**
|
|
@@ -1193,8 +1229,27 @@ export default {
|
|
|
1193
1229
|
* @spec openspec/changes/record-list-table/specs/record-list-table/spec.md#requirement-a-record-list-can-draw-its-rows-as-a-small-table
|
|
1194
1230
|
*/
|
|
1195
1231
|
tableRows() {
|
|
1232
|
+
const rows = this.tableAllRows.filter((row) => Object.entries(this.tableFilters).every(([key, values]) => {
|
|
1233
|
+
const index = this.tableColumns.findIndex((col) => col.key === key)
|
|
1234
|
+
return values.length === 0 || index < 0 || values.includes(row.cells[index].text)
|
|
1235
|
+
}))
|
|
1236
|
+
const { key, dir } = this.tableSort
|
|
1237
|
+
const index = this.tableColumns.findIndex((col) => col.key === key)
|
|
1238
|
+
if (index < 0 || !dir) {
|
|
1239
|
+
return rows
|
|
1240
|
+
}
|
|
1241
|
+
const sign = dir === 'descending' ? -1 : 1
|
|
1242
|
+
return [...rows].sort((a, b) => sign * compareCells(a.cells[index].text, b.cells[index].text))
|
|
1243
|
+
},
|
|
1244
|
+
|
|
1245
|
+
/**
|
|
1246
|
+
* Every `table` row, before the reader's head filters and sort.
|
|
1247
|
+
*
|
|
1248
|
+
* @return {Array<object>} `{ key, cells: [{ key, text, align, alert }] }`.
|
|
1249
|
+
*/
|
|
1250
|
+
tableAllRows() {
|
|
1196
1251
|
const indentPath = this.content?.indentPath
|
|
1197
|
-
|
|
1252
|
+
return this.items.map((item) => ({
|
|
1198
1253
|
key: item.key,
|
|
1199
1254
|
row: item.row,
|
|
1200
1255
|
rowActions: this.rowActions.length > 0 ? this.rowActionsOf(item.row) : [],
|
|
@@ -1215,13 +1270,6 @@ export default {
|
|
|
1215
1270
|
}
|
|
1216
1271
|
}),
|
|
1217
1272
|
}))
|
|
1218
|
-
const { key, dir } = this.tableSort
|
|
1219
|
-
const index = this.tableColumns.findIndex((col) => col.key === key)
|
|
1220
|
-
if (index < 0 || !dir) {
|
|
1221
|
-
return rows
|
|
1222
|
-
}
|
|
1223
|
-
const sign = dir === 'descending' ? -1 : 1
|
|
1224
|
-
return [...rows].sort((a, b) => sign * compareCells(a.cells[index].text, b.cells[index].text))
|
|
1225
1273
|
},
|
|
1226
1274
|
|
|
1227
1275
|
/**
|
|
@@ -2246,6 +2294,41 @@ export default {
|
|
|
2246
2294
|
this.tableSort = { key, dir }
|
|
2247
2295
|
},
|
|
2248
2296
|
|
|
2297
|
+
/**
|
|
2298
|
+
* The values a `table` column's head filter offers: each text the column holds, once.
|
|
2299
|
+
*
|
|
2300
|
+
* @param {string} key The column key.
|
|
2301
|
+
* @return {Array<string>} The texts, in row order.
|
|
2302
|
+
*/
|
|
2303
|
+
tableFilterOptions(key) {
|
|
2304
|
+
const index = this.tableColumns.findIndex((col) => col.key === key)
|
|
2305
|
+
if (index < 0) {
|
|
2306
|
+
return []
|
|
2307
|
+
}
|
|
2308
|
+
return [...new Set(this.tableAllRows.map((row) => row.cells[index].text).filter((text) => text !== ''))]
|
|
2309
|
+
},
|
|
2310
|
+
|
|
2311
|
+
/**
|
|
2312
|
+
* Keep or drop one value of a `table` column's head filter.
|
|
2313
|
+
*
|
|
2314
|
+
* @param {string} key The column key.
|
|
2315
|
+
* @param {string} value The cell text.
|
|
2316
|
+
*/
|
|
2317
|
+
toggleTableFilter(key, value) {
|
|
2318
|
+
const current = this.tableFilters[key] || []
|
|
2319
|
+
this.tableFilters = { ...this.tableFilters, [key]: current.includes(value) ? current.filter((v) => v !== value) : [...current, value] }
|
|
2320
|
+
},
|
|
2321
|
+
|
|
2322
|
+
/**
|
|
2323
|
+
* The accessible name of a column's head filter.
|
|
2324
|
+
*
|
|
2325
|
+
* @param {string} label The column label.
|
|
2326
|
+
* @return {string}
|
|
2327
|
+
*/
|
|
2328
|
+
filterLabelOf(label) {
|
|
2329
|
+
return t('nextcloud-vue', 'Filter by {column}', { column: label })
|
|
2330
|
+
},
|
|
2331
|
+
|
|
2249
2332
|
/**
|
|
2250
2333
|
* The rows of a read with `flattenPath` set: each entry of the list at
|
|
2251
2334
|
* that path on a record becomes a row, with the record at `@parent`.
|
|
@@ -3232,6 +3315,16 @@ export default {
|
|
|
3232
3315
|
font-size: 10px;
|
|
3233
3316
|
}
|
|
3234
3317
|
|
|
3318
|
+
.cn-record-list__head {
|
|
3319
|
+
display: inline-flex;
|
|
3320
|
+
gap: 4px;
|
|
3321
|
+
align-items: center;
|
|
3322
|
+
}
|
|
3323
|
+
|
|
3324
|
+
.cn-record-list__head-filter {
|
|
3325
|
+
--button-size: 22px;
|
|
3326
|
+
}
|
|
3327
|
+
|
|
3235
3328
|
.cn-record-list__table .cn-record-list__cell--end {
|
|
3236
3329
|
padding-inline-start: 12px;
|
|
3237
3330
|
text-align: end;
|
|
@@ -3383,6 +3476,24 @@ export default {
|
|
|
3383
3476
|
text-decoration: none;
|
|
3384
3477
|
}
|
|
3385
3478
|
|
|
3479
|
+
/* `density: "compact"` on cards: the boards' small framed rows (DqInspectie
|
|
3480
|
+
"Molenweg 4" and the outbox): 12px 14px in, the title 15px/700 on an
|
|
3481
|
+
18px line with the pill at its end, the meta 6px under it. */
|
|
3482
|
+
.cn-record-list--compact .cn-record-list__card {
|
|
3483
|
+
gap: 6px;
|
|
3484
|
+
padding: 12px 14px;
|
|
3485
|
+
}
|
|
3486
|
+
|
|
3487
|
+
.cn-record-list--compact .cn-record-list__card-head {
|
|
3488
|
+
align-items: flex-start;
|
|
3489
|
+
}
|
|
3490
|
+
|
|
3491
|
+
.cn-record-list--compact .cn-record-list__card .cn-record-list__title {
|
|
3492
|
+
font-size: 15px;
|
|
3493
|
+
font-weight: 700;
|
|
3494
|
+
line-height: 1.2;
|
|
3495
|
+
}
|
|
3496
|
+
|
|
3386
3497
|
.cn-record-list__card--link:hover,
|
|
3387
3498
|
.cn-record-list__card--link:focus-visible {
|
|
3388
3499
|
border-color: var(--color-border-dark, var(--color-border));
|
|
@@ -275,6 +275,7 @@
|
|
|
275
275
|
type="number"
|
|
276
276
|
:min="field.min"
|
|
277
277
|
:max="field.max"
|
|
278
|
+
:step="numberStep"
|
|
278
279
|
:modelValue="stringValue"
|
|
279
280
|
:readonly="field.readOnly"
|
|
280
281
|
:error="!!error"
|
|
@@ -424,6 +425,20 @@ export default {
|
|
|
424
425
|
return INPUT_TYPES.includes(this.kind) ? this.kind : 'text'
|
|
425
426
|
},
|
|
426
427
|
|
|
428
|
+
/**
|
|
429
|
+
* The number input's step: the field's own, 1 for a whole number, else
|
|
430
|
+
* any, so a decimal value (0.8) is not invalid to the browser.
|
|
431
|
+
*
|
|
432
|
+
* @return {string}
|
|
433
|
+
* @spec openspec/changes/settings-number-decimal-step/specs/settings-fields/spec.md#requirement-a-number-setting-steps-as-it-is-declared
|
|
434
|
+
*/
|
|
435
|
+
numberStep() {
|
|
436
|
+
if (typeof this.field.step === 'number' || (typeof this.field.step === 'string' && this.field.step !== '')) {
|
|
437
|
+
return String(this.field.step)
|
|
438
|
+
}
|
|
439
|
+
return this.field.integer ? '1' : 'any'
|
|
440
|
+
},
|
|
441
|
+
|
|
427
442
|
/** @return {string} The label. */
|
|
428
443
|
label() {
|
|
429
444
|
return String(this.field.label || this.field.key || '')
|
|
@@ -118,9 +118,10 @@ import CnConfirmDialog from '../../dialogs/CnConfirmDialog.vue'
|
|
|
118
118
|
import CnDataTable from '../CnDataTable/CnDataTable.vue'
|
|
119
119
|
import { getByPath, useEndpointSource } from '../../composables/useEndpointSource.js'
|
|
120
120
|
import { useObjectStore } from '../../store/useObjectStore.js'
|
|
121
|
-
import { dispatchAction, resolveObjectOpType } from '../../utils/actionsDispatcher.js'
|
|
121
|
+
import { dispatchAction, interpolateActionTarget, resolveObjectOpType } from '../../utils/actionsDispatcher.js'
|
|
122
122
|
import { resolveObjectTokenContext } from '../../utils/detailObjectContext.js'
|
|
123
123
|
import { headerFilterParams, nextHeaderFilters, withSortableColumns } from '../../utils/headerFilterState.js'
|
|
124
|
+
import { localHeaderRows } from '../../utils/localHeaderRows.js'
|
|
124
125
|
import { pageFilter, pageSort } from '../../utils/pageControls.js'
|
|
125
126
|
import { readPath } from '../../utils/readPath.js'
|
|
126
127
|
import { dropOptionalUnresolved, hasUnresolvedTokens, resolveFilterTokens } from '../../utils/resolveFilterTokens.js'
|
|
@@ -869,9 +870,9 @@ export default {
|
|
|
869
870
|
if (this.endpointActive && this.columns.some((col) => col && typeof col === 'object' && col.labelPath)) {
|
|
870
871
|
inner.columns = this.labelledColumns(inner.columns || this.columns)
|
|
871
872
|
}
|
|
872
|
-
// Header sort and filter (`sortable`, `filterable`)
|
|
873
|
-
//
|
|
874
|
-
if (this.selfFetchActive && (this.sortable !== false || this.filterable)) {
|
|
873
|
+
// Header sort and filter (`sortable`, `filterable`): the server sorts
|
|
874
|
+
// and filters a self-fetch; endpoint rows are sorted and filtered here.
|
|
875
|
+
if ((this.selfFetchActive || this.endpointActive) && (this.sortable !== false || this.filterable)) {
|
|
875
876
|
inner.columns = withSortableColumns(inner.columns || this.columns, this.sortable)
|
|
876
877
|
inner.filterable = this.filterable
|
|
877
878
|
inner.activeFilters = this.headerFilters
|
|
@@ -889,7 +890,7 @@ export default {
|
|
|
889
890
|
}
|
|
890
891
|
}
|
|
891
892
|
if (this.endpointActive) {
|
|
892
|
-
inner.rows = this.endpointRows
|
|
893
|
+
inner.rows = localHeaderRows(this.endpointRows, this.headerSort, this.headerFilters)
|
|
893
894
|
inner.loading = this.epLoading || this.loading
|
|
894
895
|
}
|
|
895
896
|
if (this.rowRoute) {
|
|
@@ -1159,9 +1160,13 @@ export default {
|
|
|
1159
1160
|
return { ok: false, error: '' }
|
|
1160
1161
|
}
|
|
1161
1162
|
if (action.type !== 'object-op') {
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
: action
|
|
1163
|
+
let wrapped = action
|
|
1164
|
+
if (!action.type || action.type === 'handler') {
|
|
1165
|
+
wrapped = { ...action, args: [...(action.args || []), row] }
|
|
1166
|
+
} else if (action.type === 'navigate' && row) {
|
|
1167
|
+
// A row's navigate reads that row: `{objectId}` / `@objectId` is its id, `@object.<field>` its fields.
|
|
1168
|
+
wrapped = { ...action, target: interpolateActionTarget(action.target, { objectId: row.id, object: row }) }
|
|
1169
|
+
}
|
|
1165
1170
|
if (typeof this.cnDispatchAction === 'function') {
|
|
1166
1171
|
this.cnDispatchAction(wrapped)
|
|
1167
1172
|
} else {
|
|
@@ -880,15 +880,23 @@ export default {
|
|
|
880
880
|
*/
|
|
881
881
|
/**
|
|
882
882
|
* The value an input starts with: `content.default`, else the current
|
|
883
|
-
* quarter or year for those types, else empty.
|
|
883
|
+
* quarter or year for those types, else empty. For a quarter or year
|
|
884
|
+
* input, `default: "previous"` is the last complete one (DqTermijnen
|
|
885
|
+
* opens on the last complete quarter).
|
|
884
886
|
*
|
|
885
887
|
* @return {string}
|
|
886
888
|
*/
|
|
887
889
|
inputDefault() {
|
|
890
|
+
const today = new Date()
|
|
891
|
+
if (this.content.default === 'previous' && this.inputType === 'quarter') {
|
|
892
|
+
return quarterKey(new Date(today.getFullYear(), today.getMonth() - 3, 1))
|
|
893
|
+
}
|
|
894
|
+
if (this.content.default === 'previous' && this.inputType === 'year') {
|
|
895
|
+
return String(today.getFullYear() - 1)
|
|
896
|
+
}
|
|
888
897
|
if (this.content.default !== undefined && this.content.default !== null) {
|
|
889
898
|
return String(this.content.default)
|
|
890
899
|
}
|
|
891
|
-
const today = new Date()
|
|
892
900
|
if (this.inputType === 'quarter') {
|
|
893
901
|
return quarterKey(today)
|
|
894
902
|
}
|