@conduction/nextcloud-vue 2.143.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 (165) hide show
  1. package/dist/esm/components/CnActionDialog/CnActionDialog.vue.js +1 -1
  2. package/dist/esm/components/CnActionDialog/CnActionDialog.vue2.js +7 -2
  3. package/dist/esm/components/CnActionDialog/CnActionDialog.vue2.js.map +1 -1
  4. package/dist/esm/components/CnActionDialog/CnActionDialog.vue3.js +1 -0
  5. package/dist/esm/components/CnActionDialog/CnActionDialog.vue3.js.map +1 -1
  6. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue.js +1 -1
  7. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue2.js +55 -2
  8. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue2.js.map +1 -1
  9. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue3.js.map +1 -1
  10. package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue.js +1 -1
  11. package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue2.js +16 -0
  12. package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue2.js.map +1 -1
  13. package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue3.js +3 -1
  14. package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue3.js.map +1 -1
  15. package/dist/esm/components/CnConversationThread/CnConversationThread.vue.js +1 -1
  16. package/dist/esm/components/CnConversationThread/CnConversationThread.vue2.js.map +1 -1
  17. package/dist/esm/components/CnConversationThread/CnConversationThread.vue3.js +1 -1
  18. package/dist/esm/components/CnConversationThread/CnConversationThread.vue3.js.map +1 -1
  19. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue.js +1 -1
  20. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue2.js +1 -1
  21. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue2.js.map +1 -1
  22. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue3.js +3 -3
  23. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue3.js.map +1 -1
  24. package/dist/esm/components/CnEventsCard/CnEventsCard.vue.js +1 -1
  25. package/dist/esm/components/CnEventsCard/CnEventsCard.vue2.js +9 -0
  26. package/dist/esm/components/CnEventsCard/CnEventsCard.vue2.js.map +1 -1
  27. package/dist/esm/components/CnEventsCard/CnEventsCard.vue3.js +24 -13
  28. package/dist/esm/components/CnEventsCard/CnEventsCard.vue3.js.map +1 -1
  29. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue.js +1 -1
  30. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js +2 -2
  31. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js.map +1 -1
  32. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js.map +1 -1
  33. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue.js +1 -1
  34. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js +2 -1
  35. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js.map +1 -1
  36. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js.map +1 -1
  37. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +9 -4
  38. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  39. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  40. package/dist/esm/components/CnMailTab/CnMailTab.vue.js +1 -1
  41. package/dist/esm/components/CnMailTab/CnMailTab.vue2.js.map +1 -1
  42. package/dist/esm/components/CnMailTab/CnMailTab.vue3.js +2 -2
  43. package/dist/esm/components/CnMailTab/CnMailTab.vue3.js.map +1 -1
  44. package/dist/esm/components/CnNotesCard/CnNotesCard.vue.js +1 -1
  45. package/dist/esm/components/CnNotesCard/CnNotesCard.vue2.js +3 -0
  46. package/dist/esm/components/CnNotesCard/CnNotesCard.vue2.js.map +1 -1
  47. package/dist/esm/components/CnNotesCard/CnNotesCard.vue3.js +19 -14
  48. package/dist/esm/components/CnNotesCard/CnNotesCard.vue3.js.map +1 -1
  49. package/dist/esm/components/CnNotificationMatrix/CnNotificationMatrix.vue.js +1 -1
  50. package/dist/esm/components/CnNotificationMatrix/CnNotificationMatrix.vue2.js.map +1 -1
  51. package/dist/esm/components/CnNotificationMatrix/CnNotificationMatrix.vue3.js.map +1 -1
  52. package/dist/esm/components/CnObjectPartiesWidget/CnObjectPartiesWidget.vue.js +1 -1
  53. package/dist/esm/components/CnObjectPartiesWidget/CnObjectPartiesWidget.vue2.js +30 -0
  54. package/dist/esm/components/CnObjectPartiesWidget/CnObjectPartiesWidget.vue2.js.map +1 -1
  55. package/dist/esm/components/CnObjectPartiesWidget/CnObjectPartiesWidget.vue3.js +158 -102
  56. package/dist/esm/components/CnObjectPartiesWidget/CnObjectPartiesWidget.vue3.js.map +1 -1
  57. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
  58. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +73 -12
  59. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
  60. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js +165 -126
  61. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
  62. package/dist/esm/components/CnRetentionWidget/CnRetentionWidget.vue.js +1 -1
  63. package/dist/esm/components/CnRetentionWidget/CnRetentionWidget.vue2.js.map +1 -1
  64. package/dist/esm/components/CnRetentionWidget/CnRetentionWidget.vue3.js +12 -0
  65. package/dist/esm/components/CnRetentionWidget/CnRetentionWidget.vue3.js.map +1 -1
  66. package/dist/esm/components/CnSettingsField/CnSettingsField.vue.js +1 -1
  67. package/dist/esm/components/CnSettingsField/CnSettingsField.vue2.js +14 -0
  68. package/dist/esm/components/CnSettingsField/CnSettingsField.vue2.js.map +1 -1
  69. package/dist/esm/components/CnSettingsField/CnSettingsField.vue3.js +2 -1
  70. package/dist/esm/components/CnSettingsField/CnSettingsField.vue3.js.map +1 -1
  71. package/dist/esm/components/CnTasksWidget/CnTasksWidget.vue.js +1 -1
  72. package/dist/esm/components/CnTasksWidget/CnTasksWidget.vue2.js +110 -2
  73. package/dist/esm/components/CnTasksWidget/CnTasksWidget.vue2.js.map +1 -1
  74. package/dist/esm/components/CnTasksWidget/CnTasksWidget.vue3.js +146 -80
  75. package/dist/esm/components/CnTasksWidget/CnTasksWidget.vue3.js.map +1 -1
  76. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue.js +1 -1
  77. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js +13 -8
  78. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js.map +1 -1
  79. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue3.js.map +1 -1
  80. package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue.js +1 -1
  81. package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue2.js +10 -2
  82. package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue2.js.map +1 -1
  83. package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue3.js.map +1 -1
  84. package/dist/esm/composables/indexSources.js +35 -9
  85. package/dist/esm/composables/indexSources.js.map +1 -1
  86. package/dist/esm/integrations/builtin/calendar/CnCalendarCard.vue.js +1 -1
  87. package/dist/esm/integrations/builtin/calendar/CnCalendarCard.vue2.js +45 -0
  88. package/dist/esm/integrations/builtin/calendar/CnCalendarCard.vue2.js.map +1 -1
  89. package/dist/esm/integrations/builtin/calendar/CnCalendarCard.vue3.js +67 -34
  90. package/dist/esm/integrations/builtin/calendar/CnCalendarCard.vue3.js.map +1 -1
  91. package/dist/esm/integrations/builtin/calendar/CnCalendarTab.vue.js +1 -1
  92. package/dist/esm/integrations/builtin/calendar/CnCalendarTab.vue2.js +3 -22
  93. package/dist/esm/integrations/builtin/calendar/CnCalendarTab.vue2.js.map +1 -1
  94. package/dist/esm/integrations/builtin/calendar/CnCalendarTab.vue3.js.map +1 -1
  95. package/dist/esm/integrations/builtin/calendar/eventStatus.js +67 -0
  96. package/dist/esm/integrations/builtin/calendar/eventStatus.js.map +1 -0
  97. package/dist/esm/integrations/builtin/deck/CnDeckCard.vue.js +1 -1
  98. package/dist/esm/integrations/builtin/deck/CnDeckCard.vue2.js +39 -0
  99. package/dist/esm/integrations/builtin/deck/CnDeckCard.vue2.js.map +1 -1
  100. package/dist/esm/integrations/builtin/deck/CnDeckCard.vue3.js +81 -32
  101. package/dist/esm/integrations/builtin/deck/CnDeckCard.vue3.js.map +1 -1
  102. package/dist/esm/integrations/builtin/deck/CnDeckTab.vue.js +1 -1
  103. package/dist/esm/integrations/builtin/deck/CnDeckTab.vue2.js +5 -6
  104. package/dist/esm/integrations/builtin/deck/CnDeckTab.vue2.js.map +1 -1
  105. package/dist/esm/integrations/builtin/deck/CnDeckTab.vue3.js.map +1 -1
  106. package/dist/esm/integrations/builtin/deck/deckCardParts.js +59 -0
  107. package/dist/esm/integrations/builtin/deck/deckCardParts.js.map +1 -0
  108. package/dist/esm/l10n/en.json.js +6 -0
  109. package/dist/esm/l10n/en.json.js.map +1 -1
  110. package/dist/esm/l10n/nl.json.js +8 -0
  111. package/dist/esm/l10n/nl.json.js.map +1 -1
  112. package/dist/esm/library-style.js +1 -1
  113. package/dist/esm/nextcloud-vue.css +1505 -637
  114. package/dist/esm/utils/builtInFormatters.js +18 -1
  115. package/dist/esm/utils/builtInFormatters.js.map +1 -1
  116. package/dist/esm/utils/fetchFilterCounts.js +5 -2
  117. package/dist/esm/utils/fetchFilterCounts.js.map +1 -1
  118. package/dist/esm/utils/localHeaderRows.js +95 -0
  119. package/dist/esm/utils/localHeaderRows.js.map +1 -0
  120. package/dist/esm/utils/nextcloudDirectory.js +19 -6
  121. package/dist/esm/utils/nextcloudDirectory.js.map +1 -1
  122. package/dist/nextcloud-vue.cjs.js +1848 -924
  123. package/dist/nextcloud-vue.cjs.js.map +2 -2
  124. package/dist/nextcloud-vue.css +1505 -637
  125. package/l10n/en.json +6 -0
  126. package/l10n/nl.json +8 -0
  127. package/package.json +1 -1
  128. package/src/components/CnActionDialog/CnActionDialog.vue +36 -4
  129. package/src/components/CnAuditTrailCard/CnAuditTrailCard.vue +55 -2
  130. package/src/components/CnAuditTrailWidget/CnAuditTrailWidget.vue +18 -0
  131. package/src/components/CnConversationThread/CnConversationThread.vue +5 -2
  132. package/src/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue +2 -1
  133. package/src/components/CnEventsCard/CnEventsCard.vue +27 -5
  134. package/src/components/CnFieldListWidget/CnFieldListWidget.vue +2 -2
  135. package/src/components/CnFilesBrowser/CnFilesBrowser.vue +15 -1
  136. package/src/components/CnIndexPage/CnIndexPage.vue +9 -4
  137. package/src/components/CnMailTab/CnMailTab.vue +6 -4
  138. package/src/components/CnNotesCard/CnNotesCard.vue +8 -0
  139. package/src/components/CnNotificationMatrix/CnNotificationMatrix.vue +4 -1
  140. package/src/components/CnObjectPartiesWidget/CnObjectPartiesWidget.vue +114 -46
  141. package/src/components/CnRecordListWidget/CnRecordListWidget.vue +131 -20
  142. package/src/components/CnRetentionWidget/CnRetentionWidget.vue +26 -1
  143. package/src/components/CnSettingsField/CnSettingsField.vue +15 -0
  144. package/src/components/CnTasksWidget/CnTasksWidget.vue +170 -13
  145. package/src/components/CnWidgetObjectTable/CnWidgetObjectTable.vue +13 -8
  146. package/src/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue +10 -2
  147. package/src/composables/indexSources.js +35 -9
  148. package/src/css/dialog.css +25 -10
  149. package/src/css/kpi-card.css +3 -0
  150. package/src/css/look-board-detail.css +513 -0
  151. package/src/css/look-board-index.css +52 -4
  152. package/src/css/look-board.css +163 -8
  153. package/src/css/table.css +8 -1
  154. package/src/integrations/builtin/calendar/CnCalendarCard.vue +76 -11
  155. package/src/integrations/builtin/calendar/CnCalendarTab.vue +3 -22
  156. package/src/integrations/builtin/calendar/__tests__/CnCalendarCardBoardRows.spec.js +52 -0
  157. package/src/integrations/builtin/calendar/eventStatus.js +63 -0
  158. package/src/integrations/builtin/deck/CnDeckCard.vue +66 -0
  159. package/src/integrations/builtin/deck/CnDeckTab.vue +5 -6
  160. package/src/integrations/builtin/deck/__tests__/CnDeckCardBoardRows.spec.js +49 -0
  161. package/src/integrations/builtin/deck/deckCardParts.js +56 -0
  162. package/src/utils/builtInFormatters.js +17 -0
  163. package/src/utils/fetchFilterCounts.js +5 -2
  164. package/src/utils/localHeaderRows.js +91 -0
  165. package/src/utils/nextcloudDirectory.js +19 -6
@@ -47,17 +47,42 @@
47
47
  <table v-else class="cn-tasks-widget__table" data-testid="cn-tasks-widget-table">
48
48
  <thead>
49
49
  <tr>
50
- <th scope="col">
51
- {{ tr('Task') }}
52
- </th>
53
- <th scope="col">
54
- {{ tr('Handler') }}
55
- </th>
56
- <th scope="col">
57
- {{ tr('Task deadline') }}
58
- </th>
59
- <th scope="col">
60
- {{ tr('Status') }}
50
+ <!-- `content.sortable` / `content.filterable`: a head sorts by its
51
+ column and filters on its values (DqZaakTaken). -->
52
+ <th v-for="column in headColumns"
53
+ :key="column.key"
54
+ scope="col"
55
+ :aria-sort="headSort.key === column.key ? (headSort.dir === 'asc' ? 'ascending' : 'descending') : null"
56
+ :data-testid="`cn-tasks-widget-head-${column.key}`">
57
+ <span v-if="content.sortable === true || content.filterable === true" class="cn-tasks-widget__head">
58
+ <button v-if="content.sortable === true"
59
+ type="button"
60
+ class="cn-tasks-widget__head-sort"
61
+ :data-testid="`cn-tasks-widget-sort-${column.key}`"
62
+ @click="sortBy(column.key)">
63
+ {{ column.label }}
64
+ <ChevronUp v-if="headSort.key === column.key && headSort.dir === 'asc'" :size="14" />
65
+ <ChevronDown v-else :size="14" />
66
+ </button>
67
+ <template v-else>{{ column.label }}</template>
68
+ <NcActions v-if="content.filterable === true"
69
+ class="cn-tasks-widget__head-filter"
70
+ :aria-label="tr('Filter by {column}').replace('{column}', column.label)"
71
+ :data-testid="`cn-tasks-widget-filter-${column.key}`">
72
+ <template #icon>
73
+ <FilterOutline :size="14" />
74
+ </template>
75
+ <NcActionCheckbox v-for="option in headOptions(column.key)"
76
+ :key="option.value"
77
+ :modelValue="(headFilters[column.key] || []).includes(option.value)"
78
+ @update:modelValue="toggleHeadFilter(column.key, option.value)">
79
+ {{ option.label }}
80
+ </NcActionCheckbox>
81
+ </NcActions>
82
+ </span>
83
+ <template v-else>
84
+ {{ column.label }}
85
+ </template>
61
86
  </th>
62
87
  <th scope="col">
63
88
  <span class="hidden-visually">{{ tr('Task actions') }}</span>
@@ -375,10 +400,13 @@ import axios from '@nextcloud/axios'
375
400
  import { showError, showSuccess } from '@nextcloud/dialogs'
376
401
  import { translate as t } from '@nextcloud/l10n'
377
402
  import { generateUrl } from '@nextcloud/router'
378
- import { NcActionButton, NcActions, NcAvatar, NcButton, NcLoadingIcon, NcTextField } from '@nextcloud/vue'
403
+ import { NcActionButton, NcActionCheckbox, NcActions, NcAvatar, NcButton, NcLoadingIcon, NcTextField } from '@nextcloud/vue'
379
404
  import { inject, ref } from 'vue'
380
405
  import CheckCircle from 'vue-material-design-icons/CheckCircle.vue'
406
+ import ChevronDown from 'vue-material-design-icons/ChevronDown.vue'
407
+ import ChevronUp from 'vue-material-design-icons/ChevronUp.vue'
381
408
  import CircleOutline from 'vue-material-design-icons/CircleOutline.vue'
409
+ import FilterOutline from 'vue-material-design-icons/FilterOutline.vue'
382
410
  import CnStatusBadge from '../CnStatusBadge/CnStatusBadge.vue'
383
411
  import CnTaskFormDialog from '../CnTaskFormDialog/CnTaskFormDialog.vue'
384
412
  import CnTaskWorkCard from './CnTaskWorkCard.vue'
@@ -458,12 +486,16 @@ export default {
458
486
 
459
487
  components: {
460
488
  CheckCircle,
489
+ ChevronDown,
490
+ ChevronUp,
461
491
  CircleOutline,
462
492
  CnStatusBadge,
463
493
  CnTaskFormDialog,
464
494
  CnTaskWorkCard,
495
+ FilterOutline,
465
496
  NcActions,
466
497
  NcActionButton,
498
+ NcActionCheckbox,
467
499
  NcAvatar,
468
500
  NcButton,
469
501
  NcLoadingIcon,
@@ -618,6 +650,10 @@ export default {
618
650
  data() {
619
651
  return {
620
652
  pollTimer: null,
653
+ /** The table's head sort, `{ key, dir }`; no key keeps open tasks first. */
654
+ headSort: { key: '', dir: 'asc' },
655
+ /** The table's head filters, `{ column: values[] }`. */
656
+ headFilters: {},
621
657
  /** Uuids of the tasks a checkbox is completing, until the next read. */
622
658
  completing: [],
623
659
  /** Whether the table's create form is open. */
@@ -777,7 +813,26 @@ export default {
777
813
  * @spec openspec/changes/record-task-table/specs/record-task-table/spec.md#requirement-a-records-tasks-show-as-a-table-with-their-summary-and-new-task-in-the-header
778
814
  */
779
815
  tableRows() {
780
- return [...this.rows.filter((task) => task.isTerminal !== true), ...this.rows.filter((task) => task.isTerminal === true)]
816
+ const kept = this.rows.filter((task) => Object.entries(this.headFilters).every(([key, values]) => values.length === 0 || values.includes(this.headValue(task, key))))
817
+ if (this.headSort.key) {
818
+ const dir = this.headSort.dir === 'desc' ? -1 : 1
819
+ return [...kept].sort((a, b) => {
820
+ const x = this.headSortValue(a, this.headSort.key)
821
+ const y = this.headSortValue(b, this.headSort.key)
822
+ return (x < y ? -1 : (x > y ? 1 : 0)) * dir
823
+ })
824
+ }
825
+ return [...kept.filter((task) => task.isTerminal !== true), ...kept.filter((task) => task.isTerminal === true)]
826
+ },
827
+
828
+ /** @return {Array<{key: string, label: string}>} The table's data columns. */
829
+ headColumns() {
830
+ return [
831
+ { key: 'title', label: this.tr('Task') },
832
+ { key: 'handler', label: this.tr('Handler') },
833
+ { key: 'due', label: this.tr('Task deadline') },
834
+ { key: 'state', label: this.tr('Status') },
835
+ ]
781
836
  },
782
837
 
783
838
  /**
@@ -1158,6 +1213,84 @@ export default {
1158
1213
  * @param {object} task The task row.
1159
1214
  * @return {string} The name.
1160
1215
  */
1216
+ /**
1217
+ * A row's value in a column, as a head filter keeps it.
1218
+ *
1219
+ * @param {object} task The task row.
1220
+ * @param {string} key The column.
1221
+ * @return {string}
1222
+ */
1223
+ headValue(task, key) {
1224
+ if (key === 'title') {
1225
+ return this.titleOf(task)
1226
+ }
1227
+ if (key === 'handler') {
1228
+ return String(task.assignee || '')
1229
+ }
1230
+ if (key === 'due') {
1231
+ return this.content.dueFormat === 'date' ? this.dueDate(task) : this.dueLabel(task)
1232
+ }
1233
+ return String(task.state || '')
1234
+ },
1235
+
1236
+ /**
1237
+ * A row's value in a column, as its head sorts it: the deadline by date.
1238
+ *
1239
+ * @param {object} task The task row.
1240
+ * @param {string} key The column.
1241
+ * @return {string|number}
1242
+ */
1243
+ headSortValue(task, key) {
1244
+ if (key === 'due') {
1245
+ const time = Date.parse(task.dueAt || task.dueDate || '')
1246
+ return Number.isNaN(time) ? Number.POSITIVE_INFINITY : time
1247
+ }
1248
+ if (key === 'handler') {
1249
+ return this.handlerName(task).toLowerCase()
1250
+ }
1251
+ if (key === 'state') {
1252
+ return this.stateLabel(task).toLowerCase()
1253
+ }
1254
+ return this.titleOf(task).toLowerCase()
1255
+ },
1256
+
1257
+ /**
1258
+ * The values a column's head filter offers, one per value the rows hold.
1259
+ *
1260
+ * @param {string} key The column.
1261
+ * @return {Array<{value: string, label: string}>}
1262
+ */
1263
+ headOptions(key) {
1264
+ const seen = new Map()
1265
+ for (const task of this.rows) {
1266
+ const value = this.headValue(task, key)
1267
+ if (value !== '' && !seen.has(value)) {
1268
+ seen.set(value, key === 'handler' ? this.handlerName(task) : (key === 'state' ? this.stateLabel(task) : value))
1269
+ }
1270
+ }
1271
+ return [...seen].map(([value, label]) => ({ value, label }))
1272
+ },
1273
+
1274
+ /**
1275
+ * Sort by a column: ascending first, then descending.
1276
+ *
1277
+ * @param {string} key The column.
1278
+ */
1279
+ sortBy(key) {
1280
+ this.headSort = this.headSort.key === key && this.headSort.dir === 'asc' ? { key, dir: 'desc' } : { key, dir: 'asc' }
1281
+ },
1282
+
1283
+ /**
1284
+ * Keep or drop one value of a column's head filter.
1285
+ *
1286
+ * @param {string} key The column.
1287
+ * @param {string} value The value.
1288
+ */
1289
+ toggleHeadFilter(key, value) {
1290
+ const current = this.headFilters[key] || []
1291
+ this.headFilters = { ...this.headFilters, [key]: current.includes(value) ? current.filter((v) => v !== value) : [...current, value] }
1292
+ },
1293
+
1161
1294
  handlerName(task) {
1162
1295
  return userDisplayName(task.assignee) || String(task.assignee || '')
1163
1296
  },
@@ -1594,6 +1727,30 @@ function recordUuid(configured, objectId) {
1594
1727
  font-size: 14px;
1595
1728
  }
1596
1729
 
1730
+ .cn-tasks-widget__head {
1731
+ display: inline-flex;
1732
+ align-items: center;
1733
+ gap: 4px;
1734
+ }
1735
+
1736
+ .cn-tasks-widget__head-sort {
1737
+ display: inline-flex;
1738
+ align-items: center;
1739
+ gap: 4px;
1740
+ min-height: 0;
1741
+ margin: 0;
1742
+ padding: 0;
1743
+ border: 0;
1744
+ background: transparent;
1745
+ color: inherit;
1746
+ font: inherit;
1747
+ cursor: pointer;
1748
+ }
1749
+
1750
+ .cn-tasks-widget__head-filter {
1751
+ --button-size: 22px;
1752
+ }
1753
+
1597
1754
  .cn-tasks-widget__table th {
1598
1755
  padding: 12px 16px;
1599
1756
  background: color-mix(in srgb, var(--color-background-hover) 40%, var(--color-main-background));
@@ -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
  }
@@ -1109,7 +1109,9 @@ function fillRowTemplate(template, row) {
1109
1109
  * template, "{case.title}") or `linkedField` (a path) fills the "Linked"
1110
1110
  * column, drawn when a kind declares one; `sourceConfig.linkedLabel` names it.
1111
1111
  * `subtitleTemplate` ("Case {identifier}", translated) fills the line under a
1112
- * result, else `subtitleField`.
1112
+ * result, else `subtitleField`; `subtitleResolve` (`{ field, schema, labelField,
1113
+ * as }`) names a referenced object for it as `{as}`. `sourceConfig.titleWrap`
1114
+ * wraps a long result title instead of cutting it.
1113
1115
  * `linkedUser: true` reads that value as a user id and shows the user's
1114
1116
  * display name. `sourceConfig.columnWidths` sets a width per column key.
1115
1117
  * The page's title and count line can name the term as `{term}`, and the
@@ -1226,9 +1228,32 @@ function searchSource(config = {}, context = {}) {
1226
1228
  return ''
1227
1229
  }
1228
1230
 
1229
- const subtitleOf = (kind, object) => {
1231
+ // `subtitleResolve`: `{ field, schema, register?, labelField?, as }` names
1232
+ // the object a reference points at, for the subtitle template as `{as}`.
1233
+ const subtitleLabels = async (kind) => {
1234
+ const resolve = kind.subtitleResolve
1235
+ if (!resolve || !resolve.schema || !resolve.field) {
1236
+ return null
1237
+ }
1238
+ const objects = await fetchReferenceObjects({ register: resolve.register || kind.register, schema: resolve.schema }).catch(() => [])
1239
+ const labels = new Map()
1240
+ for (const object of objects) {
1241
+ const id = object.id || object.uuid || (object['@self'] && object['@self'].id)
1242
+ const label = readPath(object, resolve.labelField || 'title') ?? object.name
1243
+ if (id !== undefined && label !== undefined && label !== null) {
1244
+ labels.set(String(id), String(label))
1245
+ }
1246
+ }
1247
+ return labels
1248
+ }
1249
+
1250
+ const subtitleOf = (kind, object, labels = null) => {
1230
1251
  if (kind.subtitleTemplate) {
1231
- return fillTemplate(translate(kind.subtitleTemplate), object)
1252
+ const resolve = kind.subtitleResolve
1253
+ const data = labels && resolve
1254
+ ? { ...object, [resolve.as || resolve.field]: labels.get(String(readPath(object, resolve.field) ?? '')) ?? '' }
1255
+ : object
1256
+ return fillTemplate(translate(kind.subtitleTemplate), data)
1232
1257
  }
1233
1258
  return kind.subtitleField ? String(readPath(object, kind.subtitleField) ?? '') : ''
1234
1259
  }
@@ -1247,6 +1272,7 @@ function searchSource(config = {}, context = {}) {
1247
1272
  const results = Array.isArray(body.results) ? body.results : (Array.isArray(body) ? body : [])
1248
1273
  const labels = kind.statusResolve && kind.statusField ? await statusLabels(kind) : null
1249
1274
  const userNames = kind.linkedUser === true ? await linkedUserNames(kind, results) : null
1275
+ const subtitleNames = await subtitleLabels(kind)
1250
1276
  return {
1251
1277
  total: typeof body.total === 'number' ? body.total : results.length,
1252
1278
  rows: results.map((object) => {
@@ -1261,7 +1287,7 @@ function searchSource(config = {}, context = {}) {
1261
1287
  kindLabel: kind.label,
1262
1288
  kindText: kindTextOf(kind, object),
1263
1289
  title: String(readPath(object, kind.titleField || 'title') ?? object.name ?? id ?? ''),
1264
- subtitle: subtitleOf(kind, object),
1290
+ subtitle: subtitleOf(kind, object, subtitleNames),
1265
1291
  status,
1266
1292
  linked: userNames ? (userNames.get(linkedOf(kind, object)) || linkedOf(kind, object)) : linkedOf(kind, object),
1267
1293
  href: kind.href ? generateUrl(fillRowTemplate(kind.href, { ...object, id })) : '',
@@ -1372,7 +1398,7 @@ function searchSource(config = {}, context = {}) {
1372
1398
  loading: () => state.loading,
1373
1399
 
1374
1400
  columns: withColumnWidths([
1375
- { key: 'title', label: t('nextcloud-vue', 'Result'), secondary: 'subtitle', sortable: true },
1401
+ { key: 'title', label: t('nextcloud-vue', 'Result'), secondary: 'subtitle', sortable: true, ...(config && config.titleWrap === true ? { cellClass: 'cn-cell--wrap' } : {}) },
1376
1402
  // `kindAsText`: what one row is, as plain text ("Zaak", "Lantaarnpaal",
1377
1403
  // DqZoekresultaten), instead of the plural kind as a pill.
1378
1404
  config && config.kindAsText === true
@@ -1619,10 +1645,10 @@ function usersSource() {
1619
1645
  loading: () => state.loading,
1620
1646
 
1621
1647
  columns: [
1622
- { key: 'displayname', label: t('nextcloud-vue', 'Employee'), secondary: 'function' },
1623
- { key: 'roles', label: t('nextcloud-vue', 'Roles') },
1624
- { key: 'email', label: t('nextcloud-vue', 'E-mail') },
1625
- { key: 'today', label: t('nextcloud-vue', 'Today'), widget: 'badge', widgetProps: { colorMap: presenceColors, variant: 'warning' } },
1648
+ { key: 'displayname', label: t('nextcloud-vue', 'Employee'), secondary: 'function', sortable: true, cellClass: 'cn-cell--lead' },
1649
+ { key: 'roles', label: t('nextcloud-vue', 'Roles'), sortable: true },
1650
+ { key: 'email', label: t('nextcloud-vue', 'E-mail'), sortable: true },
1651
+ { key: 'today', label: t('nextcloud-vue', 'Today'), widget: 'badge', widgetProps: { colorMap: presenceColors, variant: 'warning' }, sortable: true },
1626
1652
  ],
1627
1653
 
1628
1654
  searchFields: {
@@ -177,11 +177,14 @@ body:has(.cn-look-board):not(:has(.cn-look-nextcloud)) .modal-mask {
177
177
  color: var(--color-text-maxcontrast);
178
178
  }
179
179
 
180
- /* The saved-view edit and share forms start 22px under the subtitle, as
181
- the Dq dialog boards draw a first field: the body's 16px gap and 6px. */
180
+ /* A body starts 22px under the subtitle, as the Dq dialog boards draw a
181
+ first field: the header's foot and the body gap alone, so the saved-view
182
+ edit and share forms and the confirm sentence pad nothing on top
183
+ (r29 on :8080: 28, 28 and 26px with their own padding). */
182
184
  .cn-dialog-board .dialog__content .cn-edit-view,
183
- .cn-dialog-board .dialog__content .cn-share-view {
184
- padding-top: 6px;
185
+ .cn-dialog-board .dialog__content .cn-share-view,
186
+ .cn-dialog-board .dialog__content .cn-confirm__confirm {
187
+ padding-top: 0;
185
188
  }
186
189
 
187
190
  /* The close button is a 36px circle. NcButton sizes an icon-only button by
@@ -216,9 +219,17 @@ body:has(.cn-look-board):not(:has(.cn-look-nextcloud)) .modal-mask {
216
219
  /* A choice card with a checkbox (CnAssignDialog's toggles) is as tight as
217
220
  the board draws it (DqOverdragen, 76px for a label and a two-line note):
218
221
  10px 14px padding, the checkbox row at the 24px minimum target instead of
219
- NcCheckboxRadioSwitch's 34px, the note at 1.2 line height. */
220
- .cn-dialog-board .cn-assign-dialog__toggle {
222
+ NcCheckboxRadioSwitch's 34px, the note at 1.2 line height; the card's
223
+ corners 8px and the label on an 18px line, as the board draws them. */
224
+ /* `.dialog` outranks the component's scoped rule (class + attribute), which
225
+ loads after this sheet and kept 12px 14px and 12px corners on :8080 (r29). */
226
+ .cn-dialog-board .dialog .cn-assign-dialog__toggle {
221
227
  padding: 10px 14px;
228
+ border-radius: 8px;
229
+ }
230
+
231
+ .cn-dialog-board .cn-assign-dialog__toggle .checkbox-content__text {
232
+ line-height: 18px;
222
233
  }
223
234
 
224
235
  .cn-dialog-board .cn-assign-dialog__toggle .checkbox-radio-switch-checkbox .checkbox-content,
@@ -341,10 +352,14 @@ body:has(.cn-look-board):not(:has(.cn-look-nextcloud)) .modal-mask {
341
352
  border and radius 8 (PlanItemDialog, DqWerkvoorraad). NcInputField draws
342
353
  its border as a 65%-transparent shadow, which on the white dialog read as
343
354
  an empty band, and floats its label inside the border. A field with
344
- `labelOutside` keeps the app's own label and takes the same control. */
355
+ `labelOutside` keeps the app's own label and takes the same control. The
356
+ label's box starts where the field starts and the control 23px under it,
357
+ as on every dialog board (72 fields on 11 boards): the label at the font's
358
+ normal line, 21px of room plus the control's 2px focus inset, the label
359
+ 4px above that room's end. */
345
360
  .cn-dialog-board .dialog__content .input-field:has(.input-field__label),
346
361
  .cn-dialog-board .dialog__content .textarea:has(.textarea__label) {
347
- margin-block-start: 26px;
362
+ margin-block-start: 21px;
348
363
  }
349
364
 
350
365
  /* NcSelect's search is an NcInputField inside the select, and the select's
@@ -353,7 +368,7 @@ body:has(.cn-look-board):not(:has(.cn-look-nextcloud)) .modal-mask {
353
368
  select makes the label's room instead.
354
369
  @spec openspec/changes/board-dialog-select-label-room/specs/board-dialog-select-label-room/spec.md#requirement-a-board-dialog-select-makes-room-for-its-raised-label */
355
370
  .cn-dialog-board .dialog__content .v-select:has(.input-field__label) {
356
- margin-block-start: 26px;
371
+ margin-block-start: 21px;
357
372
  }
358
373
 
359
374
  /* A select in a form dialog sits under its label and over its help line
@@ -414,7 +429,7 @@ body:has(.cn-look-board):not(:has(.cn-look-nextcloud)) .modal-mask {
414
429
  color: var(--color-main-text);
415
430
  font-size: var(--cn-field-label-size, 14px);
416
431
  font-weight: 600;
417
- line-height: 20px;
432
+ line-height: normal;
418
433
  pointer-events: auto;
419
434
  transition: none;
420
435
  }
@@ -547,6 +547,9 @@ a.cn-kpi-card--board * {
547
547
  .cn-kpi-card--board.cn-kpi-card--stacked:has(.cn-kpi-card__title--badge-end) {
548
548
  --cn-kpi-stacked-padding: 20px 22px;
549
549
  --cn-kpi-stacked-gap: 8px;
550
+ /* Its label is set at 600, as every board draws this tile (DqDashboard,
551
+ DqMijnTeam, DqPersoonlijkeInstellingen); the plain stacked tile keeps 400. */
552
+ --cn-kpi-stacked-title-weight: 600;
550
553
  }
551
554
 
552
555
  .cn-kpi-card--board.cn-kpi-card--stacked:has(.cn-kpi-card__title--badge-end) .cn-kpi-card__value {