@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
@@ -4,7 +4,7 @@
4
4
  -->
5
5
 
6
6
  <template>
7
- <div class="cn-object-parties" data-testid="cn-object-parties">
7
+ <div class="cn-object-parties" :class="{ 'cn-object-parties--board': isBoard }" data-testid="cn-object-parties">
8
8
  <div v-if="canAdd" class="cn-object-parties__manage">
9
9
  <NcButton data-testid="cn-object-parties-add" @click="adding = true">
10
10
  <template #icon>
@@ -112,51 +112,89 @@
112
112
  class="cn-object-parties__party"
113
113
  :data-party="party.partyUuid || party.contactUid"
114
114
  data-testid="cn-object-parties-party">
115
- <span class="cn-object-parties__name">{{ nameOf(party) }}</span>
116
- <span
117
- v-if="primary && party.partyUuid === primary"
118
- class="cn-status-badge cn-status-badge--primary"
119
- data-testid="cn-object-parties-primary">{{ label('primary') }}</span>
120
- <span v-if="kindOf(party)" class="cn-object-parties__muted">{{ kindOf(party) }}</span>
121
- <span
122
- v-if="notAccepted(party)"
123
- class="cn-status-badge cn-status-badge--warning"
124
- data-testid="cn-object-parties-not-accepted">{{ label('notAccepted') }}</span>
125
- <span
126
- v-for="(line, index) in extraLinesOf(party)"
127
- :key="`line-${index}`"
128
- class="cn-object-parties__muted"
129
- data-testid="cn-object-parties-extra-line">{{ line }}</span>
130
- <span v-if="party.email" class="cn-object-parties__muted">{{ party.email }}</span>
131
- <span
132
- v-if="representedBy(party)"
133
- class="cn-object-parties__muted"
134
- data-testid="cn-object-parties-represented">{{ label('representedBy') }}: {{ representedBy(party) }}</span>
135
- <span
136
- v-if="correspondenceOf(party)"
137
- class="cn-object-parties__muted"
138
- :data-party-documents="party.partyUuid || party.contactUid"
139
- data-testid="cn-object-parties-documents">{{ correspondenceOf(party) }}</span>
140
- <span
141
- v-for="indicator in indicatorsFor(party)"
142
- :key="`${party.partyUuid}-${indicator.key}`"
143
- class="cn-status-badge cn-status-badge--default"
144
- :class="`cn-object-parties__indicator--${indicator.severity}`"
145
- :data-effect="indicator.effect"
146
- :title="indicator.verdict"
147
- data-testid="cn-object-parties-indicator">{{ indicator.label }} · {{ indicator.verdict }}</span>
148
- <NcButton
149
- v-if="canRemove && party.partyUuid"
150
- class="cn-object-parties__remove"
151
- variant="tertiary"
152
- :aria-label="removeLabel(party, group)"
153
- :title="removeLabel(party, group)"
154
- data-testid="cn-object-parties-remove"
155
- @click="removing = { party, group }">
156
- <template #icon>
157
- <TrashCanOutline :size="20" />
158
- </template>
159
- </NcButton>
115
+ <!-- The board look draws a party as one ruled row (DqZaakBetrokkenen
116
+ "Rollen"): an initials tile, the name over one meta line that
117
+ names the role, the pills at the end. -->
118
+ <template v-if="isBoard">
119
+ <span class="cn-object-parties__tile" aria-hidden="true">{{ initialsOf(nameOf(party)) }}</span>
120
+ <span class="cn-object-parties__text">
121
+ <span class="cn-object-parties__name">{{ nameOf(party) }}</span>
122
+ <span class="cn-object-parties__meta" data-testid="cn-object-parties-meta">{{ boardMetaOf(party, group) }}</span>
123
+ </span>
124
+ <span class="cn-object-parties__end">
125
+ <span
126
+ v-if="notAccepted(party)"
127
+ class="cn-status-badge cn-status-badge--warning"
128
+ data-testid="cn-object-parties-not-accepted">{{ label('notAccepted') }}</span>
129
+ <span
130
+ v-for="indicator in indicatorsFor(party)"
131
+ :key="`${party.partyUuid}-${indicator.key}`"
132
+ class="cn-status-badge cn-status-badge--default"
133
+ :class="`cn-object-parties__indicator--${indicator.severity}`"
134
+ :data-effect="indicator.effect"
135
+ :title="indicator.verdict"
136
+ data-testid="cn-object-parties-indicator">{{ indicator.label }}</span>
137
+ <NcButton
138
+ v-if="canRemove && party.partyUuid"
139
+ class="cn-object-parties__remove"
140
+ variant="tertiary"
141
+ :aria-label="removeLabel(party, group)"
142
+ :title="removeLabel(party, group)"
143
+ data-testid="cn-object-parties-remove"
144
+ @click="removing = { party, group }">
145
+ <template #icon>
146
+ <TrashCanOutline :size="20" />
147
+ </template>
148
+ </NcButton>
149
+ </span>
150
+ </template>
151
+ <template v-else>
152
+ <span class="cn-object-parties__name">{{ nameOf(party) }}</span>
153
+ <span
154
+ v-if="primary && party.partyUuid === primary"
155
+ class="cn-status-badge cn-status-badge--primary"
156
+ data-testid="cn-object-parties-primary">{{ label('primary') }}</span>
157
+ <span v-if="kindOf(party)" class="cn-object-parties__muted">{{ kindOf(party) }}</span>
158
+ <span
159
+ v-if="notAccepted(party)"
160
+ class="cn-status-badge cn-status-badge--warning"
161
+ data-testid="cn-object-parties-not-accepted">{{ label('notAccepted') }}</span>
162
+ <span
163
+ v-for="(line, index) in extraLinesOf(party)"
164
+ :key="`line-${index}`"
165
+ class="cn-object-parties__muted"
166
+ data-testid="cn-object-parties-extra-line">{{ line }}</span>
167
+ <span v-if="party.email" class="cn-object-parties__muted">{{ party.email }}</span>
168
+ <span
169
+ v-if="representedBy(party)"
170
+ class="cn-object-parties__muted"
171
+ data-testid="cn-object-parties-represented">{{ label('representedBy') }}: {{ representedBy(party) }}</span>
172
+ <span
173
+ v-if="correspondenceOf(party)"
174
+ class="cn-object-parties__muted"
175
+ :data-party-documents="party.partyUuid || party.contactUid"
176
+ data-testid="cn-object-parties-documents">{{ correspondenceOf(party) }}</span>
177
+ <span
178
+ v-for="indicator in indicatorsFor(party)"
179
+ :key="`${party.partyUuid}-${indicator.key}`"
180
+ class="cn-status-badge cn-status-badge--default"
181
+ :class="`cn-object-parties__indicator--${indicator.severity}`"
182
+ :data-effect="indicator.effect"
183
+ :title="indicator.verdict"
184
+ data-testid="cn-object-parties-indicator">{{ indicator.label }} · {{ indicator.verdict }}</span>
185
+ <NcButton
186
+ v-if="canRemove && party.partyUuid"
187
+ class="cn-object-parties__remove"
188
+ variant="tertiary"
189
+ :aria-label="removeLabel(party, group)"
190
+ :title="removeLabel(party, group)"
191
+ data-testid="cn-object-parties-remove"
192
+ @click="removing = { party, group }">
193
+ <template #icon>
194
+ <TrashCanOutline :size="20" />
195
+ </template>
196
+ </NcButton>
197
+ </template>
160
198
  </li>
161
199
  </ul>
162
200
  </section>
@@ -200,6 +238,7 @@ import TrashCanOutline from 'vue-material-design-icons/TrashCanOutline.vue'
200
238
  import CnConfirmDialog from '../../dialogs/CnConfirmDialog.vue'
201
239
  import CnPartyAddDialog from '../../dialogs/CnPartyAddDialog.vue'
202
240
  import { interpolateUrlTokens } from '../../composables/useEndpointSource.js'
241
+ import { normalizeLook } from '../../composables/useLook.js'
203
242
  import innerHeading from '../../mixins/innerHeading.js'
204
243
  import { conditionalLine } from '../../utils/conditionalLine.js'
205
244
  import { initialsOf } from '../../utils/initials.js'
@@ -287,6 +326,7 @@ export default {
287
326
 
288
327
  inject: {
289
328
  cnTranslate: { default: () => (key) => key },
329
+ cnLook: { default: 'nextcloud' },
290
330
  },
291
331
 
292
332
  props: {
@@ -347,6 +387,11 @@ export default {
347
387
  },
348
388
 
349
389
  computed: {
390
+ /** @return {boolean} The board look draws one ruled row per party. */
391
+ isBoard() {
392
+ return normalizeLook(this.cnLook) === 'board'
393
+ },
394
+
350
395
  /** @return {Array<object>} The role groups, primary first. */
351
396
  groups() {
352
397
  return rolesInOrder(this.listing, this.translatedMap(this.content?.roleLabels))
@@ -916,6 +961,29 @@ export default {
916
961
  * @param {object} party The listing row.
917
962
  * @return {Array<object>}
918
963
  */
964
+ /**
965
+ * The board row's meta line: the primary mark, the role, the kind,
966
+ * whom the party represents, its extra lines, e-mail and documents,
967
+ * separated by middots.
968
+ *
969
+ * @param {object} party The party.
970
+ * @param {object} group Its role group.
971
+ * @return {string}
972
+ */
973
+ boardMetaOf(party, group) {
974
+ const parts = []
975
+ if (this.primary && party.partyUuid === this.primary) {
976
+ parts.push(this.label('primary'))
977
+ }
978
+ parts.push(group.label, this.kindOf(party))
979
+ const represented = this.representedBy(party)
980
+ if (represented) {
981
+ parts.push(`${this.label('representedBy')}: ${represented}`)
982
+ }
983
+ parts.push(...this.extraLinesOf(party), party.email, this.correspondenceOf(party))
984
+ return parts.filter((part) => typeof part === 'string' && part !== '').join(' · ')
985
+ },
986
+
919
987
  indicatorsFor(party) {
920
988
  const record = this.partyRecords[party.partyUuid]
921
989
  if (!record) {
@@ -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));
@@ -85,6 +85,7 @@
85
85
  </component>
86
86
  <ul class="cn-retention__rows">
87
87
  <li v-for="hold in holds" :key="hold.id" class="cn-retention__row">
88
+ <span class="cn-retention__mark cn-retention__mark--hold" aria-hidden="true">LH</span>
88
89
  <span class="cn-retention__row-text">
89
90
  <span class="cn-retention__row-title">{{ hold.reason || t('nextcloud-vue', 'Legal hold') }}</span>
90
91
  <span class="cn-retention__muted">{{ t('nextcloud-vue', 'Placed on {date} by {by}', { date: hold.placedDate, by: hold.placedBy }) }}</span>
@@ -92,6 +93,7 @@
92
93
  <CnStatusBadge :label="t('nextcloud-vue', 'Active')" variant="warning" />
93
94
  </li>
94
95
  <li v-if="holds.length === 0" class="cn-retention__row">
96
+ <span class="cn-retention__mark cn-retention__mark--hold" aria-hidden="true">LH</span>
95
97
  <span class="cn-retention__row-text">
96
98
  <span class="cn-retention__row-title">{{ t('nextcloud-vue', 'No legal hold') }}</span>
97
99
  <span class="cn-retention__muted">{{ t('nextcloud-vue', 'If an objection or appeal runs, destruction is suspended') }}</span>
@@ -107,6 +109,7 @@
107
109
  </component>
108
110
  <ul class="cn-retention__rows">
109
111
  <li class="cn-retention__row">
112
+ <span class="cn-retention__mark" aria-hidden="true">ED</span>
110
113
  <span class="cn-retention__row-text">
111
114
  <span class="cn-retention__row-title">{{ transfers ? t('nextcloud-vue', 'Goes to the e-Depot') : t('nextcloud-vue', 'Not applicable') }}</span>
112
115
  <span class="cn-retention__muted">{{ transfers ? t('nextcloud-vue', 'After the retention period the record is transferred') : t('nextcloud-vue', 'Only records appraised to keep go to the e-Depot') }}</span>
@@ -643,17 +646,39 @@ export default {
643
646
  align-items: center;
644
647
  justify-content: space-between;
645
648
  gap: 12px;
646
- padding: 10px 0;
649
+ padding: 12px 0;
647
650
  border-top: 1px solid var(--color-border);
648
651
  }
649
652
 
650
653
  .cn-retention__row-text {
651
654
  display: flex;
655
+ flex: 1 1 auto;
652
656
  flex-direction: column;
653
657
  gap: 2px;
654
658
  min-width: 0;
655
659
  }
656
660
 
661
+ /* A round mark before a hold or transfer row: LH (legal hold) on the error
662
+ tint, ED (e-Depot) on the chip ground (DqZaakArchivering). */
663
+ .cn-retention__mark {
664
+ display: inline-flex;
665
+ flex: none;
666
+ align-items: center;
667
+ justify-content: center;
668
+ width: 32px;
669
+ height: 32px;
670
+ border-radius: 16px;
671
+ background: var(--color-background-dark);
672
+ color: var(--color-text-maxcontrast);
673
+ font-size: 12px;
674
+ font-weight: 700;
675
+ }
676
+
677
+ .cn-retention__mark--hold {
678
+ background: var(--color-error-light, #fcedec);
679
+ color: var(--color-error-text, #a30000);
680
+ }
681
+
657
682
  .cn-retention__row-title {
658
683
  font-weight: 600;
659
684
  }
@@ -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 || '')