@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.
Files changed (85) hide show
  1. package/dist/esm/components/CnActionDialog/CnActionDialog.vue.js +1 -1
  2. package/dist/esm/components/CnActionDialog/CnActionDialog.vue2.js.map +1 -1
  3. package/dist/esm/components/CnActionDialog/CnActionDialog.vue3.js +1 -0
  4. package/dist/esm/components/CnActionDialog/CnActionDialog.vue3.js.map +1 -1
  5. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue.js +1 -1
  6. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue2.js +55 -2
  7. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue2.js.map +1 -1
  8. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue3.js.map +1 -1
  9. package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue.js +1 -1
  10. package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue2.js +16 -0
  11. package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue2.js.map +1 -1
  12. package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue3.js +3 -1
  13. package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue3.js.map +1 -1
  14. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue.js +1 -1
  15. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue2.js +1 -1
  16. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue2.js.map +1 -1
  17. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue3.js +3 -3
  18. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue3.js.map +1 -1
  19. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue.js +1 -1
  20. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js +2 -2
  21. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js.map +1 -1
  22. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js.map +1 -1
  23. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +6 -3
  24. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  25. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  26. package/dist/esm/components/CnMailTab/CnMailTab.vue.js +1 -1
  27. package/dist/esm/components/CnMailTab/CnMailTab.vue2.js.map +1 -1
  28. package/dist/esm/components/CnMailTab/CnMailTab.vue3.js +2 -2
  29. package/dist/esm/components/CnMailTab/CnMailTab.vue3.js.map +1 -1
  30. package/dist/esm/components/CnNotificationMatrix/CnNotificationMatrix.vue.js +1 -1
  31. package/dist/esm/components/CnNotificationMatrix/CnNotificationMatrix.vue2.js.map +1 -1
  32. package/dist/esm/components/CnNotificationMatrix/CnNotificationMatrix.vue3.js.map +1 -1
  33. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
  34. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +73 -12
  35. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
  36. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js +165 -126
  37. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
  38. package/dist/esm/components/CnSettingsField/CnSettingsField.vue.js +1 -1
  39. package/dist/esm/components/CnSettingsField/CnSettingsField.vue2.js +14 -0
  40. package/dist/esm/components/CnSettingsField/CnSettingsField.vue2.js.map +1 -1
  41. package/dist/esm/components/CnSettingsField/CnSettingsField.vue3.js +2 -1
  42. package/dist/esm/components/CnSettingsField/CnSettingsField.vue3.js.map +1 -1
  43. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue.js +1 -1
  44. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js +13 -8
  45. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js.map +1 -1
  46. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue3.js.map +1 -1
  47. package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue.js +1 -1
  48. package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue2.js +10 -2
  49. package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue2.js.map +1 -1
  50. package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue3.js.map +1 -1
  51. package/dist/esm/composables/indexSources.js +35 -9
  52. package/dist/esm/composables/indexSources.js.map +1 -1
  53. package/dist/esm/l10n/nl.json.js +2 -0
  54. package/dist/esm/l10n/nl.json.js.map +1 -1
  55. package/dist/esm/library-style.js +1 -1
  56. package/dist/esm/nextcloud-vue.css +742 -326
  57. package/dist/esm/utils/builtInFormatters.js +18 -1
  58. package/dist/esm/utils/builtInFormatters.js.map +1 -1
  59. package/dist/esm/utils/localHeaderRows.js +95 -0
  60. package/dist/esm/utils/localHeaderRows.js.map +1 -0
  61. package/dist/nextcloud-vue.cjs.js +514 -179
  62. package/dist/nextcloud-vue.cjs.js.map +2 -2
  63. package/dist/nextcloud-vue.css +742 -326
  64. package/l10n/nl.json +2 -0
  65. package/package.json +1 -1
  66. package/src/components/CnActionDialog/CnActionDialog.vue +8 -1
  67. package/src/components/CnAuditTrailCard/CnAuditTrailCard.vue +55 -2
  68. package/src/components/CnAuditTrailWidget/CnAuditTrailWidget.vue +18 -0
  69. package/src/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue +2 -1
  70. package/src/components/CnFieldListWidget/CnFieldListWidget.vue +2 -2
  71. package/src/components/CnIndexPage/CnIndexPage.vue +6 -3
  72. package/src/components/CnMailTab/CnMailTab.vue +6 -4
  73. package/src/components/CnNotificationMatrix/CnNotificationMatrix.vue +4 -1
  74. package/src/components/CnRecordListWidget/CnRecordListWidget.vue +131 -20
  75. package/src/components/CnSettingsField/CnSettingsField.vue +15 -0
  76. package/src/components/CnWidgetObjectTable/CnWidgetObjectTable.vue +13 -8
  77. package/src/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue +10 -2
  78. package/src/composables/indexSources.js +35 -9
  79. package/src/css/dialog.css +18 -9
  80. package/src/css/look-board-detail.css +219 -23
  81. package/src/css/look-board-index.css +38 -4
  82. package/src/css/look-board.css +138 -8
  83. package/src/css/table.css +8 -1
  84. package/src/utils/builtInFormatters.js +17 -0
  85. 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.144.0",
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 v-if="description" class="cn-mail-tab__subtitle">
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
- </p>
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
- <p v-if="hint" class="cn-mail-tab__hint">
82
+ <div v-if="hint" class="cn-mail-tab__hint">
81
83
  {{ hint }}
82
- </p>
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
- margin: 0;
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
- <button
57
- v-if="col.sortable"
58
- type="button"
59
- class="cn-record-list__sort"
60
- :data-testid="`cn-record-list-sort-${col.key}`"
61
- @click="toggleTableSort(col.key)">
62
- {{ col.label }}
63
- <span class="cn-record-list__sort-mark" aria-hidden="true">{{ sortMarkOf(col.key) }}</span>
64
- </button>
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
- const rows = this.items.map((item) => ({
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`) only where the
873
- // widget fetches its own rows: the server sorts and filters.
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
- const wrapped = (!action.type || action.type === 'handler')
1163
- ? { ...action, args: [...(action.args || []), row] }
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
  }