@conduction/nextcloud-vue 2.143.0 → 2.144.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 (101) 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.map +1 -1
  5. package/dist/esm/components/CnConversationThread/CnConversationThread.vue.js +1 -1
  6. package/dist/esm/components/CnConversationThread/CnConversationThread.vue2.js.map +1 -1
  7. package/dist/esm/components/CnConversationThread/CnConversationThread.vue3.js +1 -1
  8. package/dist/esm/components/CnConversationThread/CnConversationThread.vue3.js.map +1 -1
  9. package/dist/esm/components/CnEventsCard/CnEventsCard.vue.js +1 -1
  10. package/dist/esm/components/CnEventsCard/CnEventsCard.vue2.js +9 -0
  11. package/dist/esm/components/CnEventsCard/CnEventsCard.vue2.js.map +1 -1
  12. package/dist/esm/components/CnEventsCard/CnEventsCard.vue3.js +24 -13
  13. package/dist/esm/components/CnEventsCard/CnEventsCard.vue3.js.map +1 -1
  14. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue.js +1 -1
  15. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js +2 -1
  16. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js.map +1 -1
  17. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js.map +1 -1
  18. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +3 -1
  19. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  20. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  21. package/dist/esm/components/CnNotesCard/CnNotesCard.vue.js +1 -1
  22. package/dist/esm/components/CnNotesCard/CnNotesCard.vue2.js +3 -0
  23. package/dist/esm/components/CnNotesCard/CnNotesCard.vue2.js.map +1 -1
  24. package/dist/esm/components/CnNotesCard/CnNotesCard.vue3.js +19 -14
  25. package/dist/esm/components/CnNotesCard/CnNotesCard.vue3.js.map +1 -1
  26. package/dist/esm/components/CnObjectPartiesWidget/CnObjectPartiesWidget.vue.js +1 -1
  27. package/dist/esm/components/CnObjectPartiesWidget/CnObjectPartiesWidget.vue2.js +30 -0
  28. package/dist/esm/components/CnObjectPartiesWidget/CnObjectPartiesWidget.vue2.js.map +1 -1
  29. package/dist/esm/components/CnObjectPartiesWidget/CnObjectPartiesWidget.vue3.js +158 -102
  30. package/dist/esm/components/CnObjectPartiesWidget/CnObjectPartiesWidget.vue3.js.map +1 -1
  31. package/dist/esm/components/CnRetentionWidget/CnRetentionWidget.vue.js +1 -1
  32. package/dist/esm/components/CnRetentionWidget/CnRetentionWidget.vue2.js.map +1 -1
  33. package/dist/esm/components/CnRetentionWidget/CnRetentionWidget.vue3.js +12 -0
  34. package/dist/esm/components/CnRetentionWidget/CnRetentionWidget.vue3.js.map +1 -1
  35. package/dist/esm/components/CnTasksWidget/CnTasksWidget.vue.js +1 -1
  36. package/dist/esm/components/CnTasksWidget/CnTasksWidget.vue2.js +110 -2
  37. package/dist/esm/components/CnTasksWidget/CnTasksWidget.vue2.js.map +1 -1
  38. package/dist/esm/components/CnTasksWidget/CnTasksWidget.vue3.js +146 -80
  39. package/dist/esm/components/CnTasksWidget/CnTasksWidget.vue3.js.map +1 -1
  40. package/dist/esm/integrations/builtin/calendar/CnCalendarCard.vue.js +1 -1
  41. package/dist/esm/integrations/builtin/calendar/CnCalendarCard.vue2.js +45 -0
  42. package/dist/esm/integrations/builtin/calendar/CnCalendarCard.vue2.js.map +1 -1
  43. package/dist/esm/integrations/builtin/calendar/CnCalendarCard.vue3.js +67 -34
  44. package/dist/esm/integrations/builtin/calendar/CnCalendarCard.vue3.js.map +1 -1
  45. package/dist/esm/integrations/builtin/calendar/CnCalendarTab.vue.js +1 -1
  46. package/dist/esm/integrations/builtin/calendar/CnCalendarTab.vue2.js +3 -22
  47. package/dist/esm/integrations/builtin/calendar/CnCalendarTab.vue2.js.map +1 -1
  48. package/dist/esm/integrations/builtin/calendar/CnCalendarTab.vue3.js.map +1 -1
  49. package/dist/esm/integrations/builtin/calendar/eventStatus.js +67 -0
  50. package/dist/esm/integrations/builtin/calendar/eventStatus.js.map +1 -0
  51. package/dist/esm/integrations/builtin/deck/CnDeckCard.vue.js +1 -1
  52. package/dist/esm/integrations/builtin/deck/CnDeckCard.vue2.js +39 -0
  53. package/dist/esm/integrations/builtin/deck/CnDeckCard.vue2.js.map +1 -1
  54. package/dist/esm/integrations/builtin/deck/CnDeckCard.vue3.js +81 -32
  55. package/dist/esm/integrations/builtin/deck/CnDeckCard.vue3.js.map +1 -1
  56. package/dist/esm/integrations/builtin/deck/CnDeckTab.vue.js +1 -1
  57. package/dist/esm/integrations/builtin/deck/CnDeckTab.vue2.js +5 -6
  58. package/dist/esm/integrations/builtin/deck/CnDeckTab.vue2.js.map +1 -1
  59. package/dist/esm/integrations/builtin/deck/CnDeckTab.vue3.js.map +1 -1
  60. package/dist/esm/integrations/builtin/deck/deckCardParts.js +59 -0
  61. package/dist/esm/integrations/builtin/deck/deckCardParts.js.map +1 -0
  62. package/dist/esm/l10n/en.json.js +6 -0
  63. package/dist/esm/l10n/en.json.js.map +1 -1
  64. package/dist/esm/l10n/nl.json.js +6 -0
  65. package/dist/esm/l10n/nl.json.js.map +1 -1
  66. package/dist/esm/library-style.js +1 -1
  67. package/dist/esm/nextcloud-vue.css +815 -363
  68. package/dist/esm/utils/fetchFilterCounts.js +5 -2
  69. package/dist/esm/utils/fetchFilterCounts.js.map +1 -1
  70. package/dist/esm/utils/nextcloudDirectory.js +19 -6
  71. package/dist/esm/utils/nextcloudDirectory.js.map +1 -1
  72. package/dist/nextcloud-vue.cjs.js +1347 -758
  73. package/dist/nextcloud-vue.cjs.js.map +2 -2
  74. package/dist/nextcloud-vue.css +815 -363
  75. package/l10n/en.json +6 -0
  76. package/l10n/nl.json +6 -0
  77. package/package.json +1 -1
  78. package/src/components/CnActionDialog/CnActionDialog.vue +28 -3
  79. package/src/components/CnConversationThread/CnConversationThread.vue +5 -2
  80. package/src/components/CnEventsCard/CnEventsCard.vue +27 -5
  81. package/src/components/CnFilesBrowser/CnFilesBrowser.vue +15 -1
  82. package/src/components/CnIndexPage/CnIndexPage.vue +3 -1
  83. package/src/components/CnNotesCard/CnNotesCard.vue +8 -0
  84. package/src/components/CnObjectPartiesWidget/CnObjectPartiesWidget.vue +114 -46
  85. package/src/components/CnRetentionWidget/CnRetentionWidget.vue +26 -1
  86. package/src/components/CnTasksWidget/CnTasksWidget.vue +170 -13
  87. package/src/css/dialog.css +7 -1
  88. package/src/css/kpi-card.css +3 -0
  89. package/src/css/look-board-detail.css +317 -0
  90. package/src/css/look-board-index.css +14 -0
  91. package/src/css/look-board.css +25 -0
  92. package/src/integrations/builtin/calendar/CnCalendarCard.vue +76 -11
  93. package/src/integrations/builtin/calendar/CnCalendarTab.vue +3 -22
  94. package/src/integrations/builtin/calendar/__tests__/CnCalendarCardBoardRows.spec.js +52 -0
  95. package/src/integrations/builtin/calendar/eventStatus.js +63 -0
  96. package/src/integrations/builtin/deck/CnDeckCard.vue +66 -0
  97. package/src/integrations/builtin/deck/CnDeckTab.vue +5 -6
  98. package/src/integrations/builtin/deck/__tests__/CnDeckCardBoardRows.spec.js +49 -0
  99. package/src/integrations/builtin/deck/deckCardParts.js +56 -0
  100. package/src/utils/fetchFilterCounts.js +5 -2
  101. package/src/utils/nextcloudDirectory.js +19 -6
package/l10n/en.json CHANGED
@@ -1706,6 +1706,12 @@
1706
1706
  "expires {date}": "expires {date}",
1707
1707
  "Loading …": "Loading …",
1708
1708
  "Reorder {item}: drag, or press the up and down arrow keys": "Reorder {item}: drag, or press the up and down arrow keys",
1709
+ "Board {name}": "Board {name}",
1710
+ "list {name}": "list {name}",
1711
+ "Tentative": "Tentative",
1712
+ "Confirmed": "Confirmed",
1713
+ "Cancelled": "Cancelled",
1714
+ "{start} until {end}": "{start} until {end}",
1709
1715
  "No contacts linked": "No contacts linked",
1710
1716
  "No relations yet": "No relations yet",
1711
1717
  "Period": "Period",
package/l10n/nl.json CHANGED
@@ -1919,6 +1919,12 @@
1919
1919
  "Made {date}": "Gemaakt {date}",
1920
1920
  "expires {date}": "verloopt {date}",
1921
1921
  "Reorder {item}: drag, or press the up and down arrow keys": "{item} verplaatsen: slepen, of de pijltoetsen omhoog en omlaag",
1922
+ "Board {name}": "Bord {name}",
1923
+ "list {name}": "lijst {name}",
1924
+ "Tentative": "Voorlopig",
1925
+ "Confirmed": "Bevestigd",
1926
+ "Cancelled": "Geannuleerd",
1927
+ "{start} until {end}": "{start} tot {end}",
1922
1928
  "No contacts linked": "Geen contacten gekoppeld",
1923
1929
  "No relations yet": "Nog geen relaties",
1924
1930
  "Period": "Periode",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@conduction/nextcloud-vue",
3
- "version": "2.143.0",
3
+ "version": "2.144.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": {
@@ -191,6 +191,7 @@ import CircleOutline from 'vue-material-design-icons/CircleOutline.vue'
191
191
  import HelpCircleOutline from 'vue-material-design-icons/HelpCircleOutline.vue'
192
192
  import CnFormField from '../CnFormField/CnFormField.vue'
193
193
  import { fetchEndpointSource, interpolateUrlTokens } from '../../composables/useEndpointSource.js'
194
+ import { BUILT_IN_FORMATTERS } from '../../utils/builtInFormatters.js'
194
195
  import { fillTemplate } from '../../utils/fillTemplate.js'
195
196
  import { readPath } from '../../utils/readPath.js'
196
197
  import { CnDialog } from '../CnDialog/index.js'
@@ -219,7 +220,8 @@ let reasonSeq = 0
219
220
  * `eyebrow` a template filled from the record ("Close off · {identifier}").
220
221
  * - `summary`: `{ source?, title, meta?, badge?, icon? }`: templates filled from
221
222
  * the record (`source` an endpointSource to read it from, else `object`).
222
- * - `facts`: `[{ title?, items: [{ label, value }] }]`, `value` a template.
223
+ * - `facts`: `[{ title?, items: [{ label, value }] }]`, `value` a template; a
224
+ * `{path|<formatter>}` token reads through the built-in and app formatters.
223
225
  * - `checklist`: `{ title?, source, itemsPath?, labels?, required? }`: items
224
226
  * `{ id, met, label?, detail? }` read from `source`; `labels[id]` gives an
225
227
  * item's `{ label, detail?, unmetDetail? }`; the ids in `required` must be
@@ -262,6 +264,8 @@ export default {
262
264
  cnTranslate: { default: () => (key) => key },
263
265
  /** The page's pre-bound action dispatcher (CnPageRenderer), for `confirm.action`. */
264
266
  cnDispatchAction: { default: null },
267
+ /** The app's named formatters (CnAppRoot), for `{path|<formatter>}` in a template. */
268
+ cnFormatters: { default: () => ({}) },
265
269
  },
266
270
 
267
271
  props: {
@@ -594,7 +598,8 @@ export default {
594
598
  if (typeof template !== 'string' || template === '') {
595
599
  return ''
596
600
  }
597
- return fillTemplate(this.cnTranslate(template), row || this.record || {}).trim()
601
+ const formatters = { ...BUILT_IN_FORMATTERS, ...(this.cnFormatters || {}) }
602
+ return fillTemplate(this.cnTranslate(template), row || this.record || {}, new Date(), { formatters }).trim()
598
603
  },
599
604
 
600
605
  /**
@@ -836,6 +841,7 @@ export default {
836
841
  .cn-action-dialog__summary-text strong {
837
842
  font-size: 15px;
838
843
  font-weight: 600;
844
+ line-height: 1.2;
839
845
  }
840
846
 
841
847
  .cn-action-dialog__summary-text .cn-action-dialog__muted {
@@ -949,7 +955,25 @@ export default {
949
955
  padding: 12px 14px;
950
956
  border: 2px solid var(--color-primary-element);
951
957
  border-radius: var(--border-radius-element, 8px);
952
- background: var(--color-primary-element-light);
958
+ background: var(--color-main-background);
959
+ }
960
+
961
+ /* The box sits at the card's inset, 12px before the label, the label on
962
+ its first line (DqArchiveren "Sanne de Vries laten weten ..."):
963
+ NcCheckboxRadioSwitch pads its row and centres a 34px target. */
964
+ .cn-action-dialog__checkbox :deep(.checkbox-content) {
965
+ padding: 0;
966
+ gap: 12px;
967
+ align-items: flex-start;
968
+ min-height: 0;
969
+ }
970
+
971
+ .cn-action-dialog__checkbox :deep(.checkbox-content__icon) {
972
+ margin: 0;
973
+ }
974
+
975
+ .cn-action-dialog__checkbox :deep(.checkbox-radio-switch__input) {
976
+ min-height: 0;
953
977
  }
954
978
 
955
979
  .cn-action-dialog__checkbox-text {
@@ -961,6 +985,7 @@ export default {
961
985
  .cn-action-dialog__checkbox-label {
962
986
  font-size: 15px;
963
987
  font-weight: 600;
988
+ line-height: 1.2;
964
989
  }
965
990
 
966
991
  .cn-action-dialog__checkbox .cn-action-dialog__muted {
@@ -17,9 +17,11 @@
17
17
  class="cn-conversation-thread__title">
18
18
  {{ title }}
19
19
  </component>
20
- <p v-if="subtitle" class="cn-conversation-thread__subtitle" data-testid="cn-conversation-subtitle">
20
+ <!-- A div, not a <p>: a theme that sets every paragraph's size with
21
+ !important would otherwise override the 14px subtitle. -->
22
+ <div v-if="subtitle" class="cn-conversation-thread__subtitle" data-testid="cn-conversation-subtitle">
21
23
  {{ subtitle }}
22
- </p>
24
+ </div>
23
25
  </div>
24
26
  <div v-if="$slots['header-actions']" class="cn-conversation-thread__header-actions">
25
27
  <!-- @slot header-actions Buttons beside the title, for example "Log contact". -->
@@ -633,6 +635,7 @@ export default {
633
635
  .cn-conversation-thread__subtitle {
634
636
  color: var(--color-text-maxcontrast);
635
637
  font-size: 14px;
638
+ line-height: normal;
636
639
  margin: 0;
637
640
  }
638
641
 
@@ -28,9 +28,12 @@
28
28
  :key="keyOf(event, index)"
29
29
  class="cn-events-card__event"
30
30
  data-testid="cn-events-card-event">
31
- <span class="cn-events-card__summary">{{ summaryOf(event) }}</span>
32
- <span class="cn-events-card__when">{{ whenOf(event) }}</span>
33
- <span v-if="event.location" class="cn-events-card__where">{{ event.location }}</span>
31
+ <span v-if="dayOf(event)" class="cn-events-card__day" aria-hidden="true">{{ dayOf(event) }}</span>
32
+ <span class="cn-events-card__text">
33
+ <span class="cn-events-card__summary">{{ summaryOf(event) }}</span>
34
+ <span class="cn-events-card__when">{{ whenOf(event) }}</span>
35
+ <span v-if="event.location" class="cn-events-card__where">{{ event.location }}</span>
36
+ </span>
34
37
  </li>
35
38
  </ul>
36
39
  <p v-else class="cn-events-card__muted" data-testid="cn-events-card-empty">
@@ -292,6 +295,15 @@ export default {
292
295
  return summary === '' ? t('nextcloud-vue', 'Appointment') : summary
293
296
  },
294
297
 
298
+ /**
299
+ * @param {object} event The event.
300
+ * @return {string} The day of the month it starts on, '' when it has no readable start.
301
+ */
302
+ dayOf(event) {
303
+ const parsed = new Date(String(event?.dtstart ?? '').trim())
304
+ return Number.isNaN(parsed.getTime()) ? '' : String(parsed.getDate())
305
+ },
306
+
295
307
  /**
296
308
  * @param {object} event The event.
297
309
  * @return {string} When it is: its start and end, or whichever it has.
@@ -333,8 +345,7 @@ function formatMoment(value) {
333
345
 
334
346
  .cn-events-card__event {
335
347
  display: flex;
336
- flex-direction: column;
337
- gap: 2px;
348
+ gap: 12px;
338
349
  padding-bottom: 8px;
339
350
  border-bottom: 1px solid var(--color-border);
340
351
  }
@@ -344,6 +355,17 @@ function formatMoment(value) {
344
355
  border-bottom: none;
345
356
  }
346
357
 
358
+ .cn-events-card__day {
359
+ display: none;
360
+ }
361
+
362
+ .cn-events-card__text {
363
+ display: flex;
364
+ flex-direction: column;
365
+ gap: 2px;
366
+ min-width: 0;
367
+ }
368
+
347
369
  .cn-events-card__summary {
348
370
  font-weight: 600;
349
371
  }
@@ -2178,7 +2178,8 @@ export default {
2178
2178
  const object = {
2179
2179
  ...entry,
2180
2180
  name: node.basename,
2181
- modified: node.mtime,
2181
+ // A template drops an object value, so the Date is handed over as ISO text.
2182
+ modified: node.mtime instanceof Date ? node.mtime.toISOString() : node.mtime,
2182
2183
  size: this.formatSize(node.size),
2183
2184
  type: this.typeLabelOf(node),
2184
2185
  ...(owner ? { owner } : {}),
@@ -3648,6 +3649,19 @@ export default {
3648
3649
  width: auto;
3649
3650
  }
3650
3651
 
3652
+ /* The row button and the row menu stand side by side on one line, not
3653
+ stacked (DqZaakDocumenten: Beoordelen, then the menu, 14px apart).
3654
+ @spec openspec/changes/files-row-button-beside-menu/specs/files-row-button-beside-menu/spec.md#requirement-a-files-row-button-stands-beside-the-row-menu */
3655
+ .cn-files-browser__col-actions > .cn-files-browser__row-button,
3656
+ .cn-files-browser__col-actions > .cn-files-browser__row-menu {
3657
+ display: inline-flex;
3658
+ vertical-align: middle;
3659
+ }
3660
+
3661
+ .cn-files-browser__col-actions > .cn-files-browser__row-button + .cn-files-browser__row-menu {
3662
+ margin-inline-start: 14px;
3663
+ }
3664
+
3651
3665
  .cn-files-browser__col-name {
3652
3666
  min-width: 0;
3653
3667
  }
@@ -9993,7 +9993,7 @@ export default {
9993
9993
  ]
9994
9994
  let counts
9995
9995
  try {
9996
- counts = await fetchFilterCounts({ register: this.register, schema, entries, baseFilter: base, ctx })
9996
+ counts = await fetchFilterCounts({ register: this.register, schema, entries, baseFilter: base, ctx, via: 'list' })
9997
9997
  } catch {
9998
9998
  counts = {}
9999
9999
  }
@@ -10050,6 +10050,8 @@ export default {
10050
10050
  })),
10051
10051
  baseFilter: resolveFilterMap(this.filter, params, ctx),
10052
10052
  ctx,
10053
+ // The chips count what the list shows: the list's own request.
10054
+ via: 'list',
10053
10055
  })
10054
10056
  if (requestKey !== this.countRequestKey) {
10055
10057
  return
@@ -96,6 +96,7 @@
96
96
  :key="row.note.id"
97
97
  class="cn-notes-card__note"
98
98
  :class="{ 'cn-notes-card__note--reply': row.isReply }">
99
+ <span class="cn-notes-card__initials" aria-hidden="true">{{ initialsOf(getNoteAuthorName(row.note)) }}</span>
99
100
  <div class="cn-notes-card__note-header">
100
101
  <CnUserActionMenu
101
102
  v-if="!isCurrentUser(row.note)"
@@ -175,6 +176,7 @@ import CnUserActionMenu from '../CnUserActionMenu/CnUserActionMenu.vue'
175
176
  import CnVisibilityChip from '../CnVisibilityChip/CnVisibilityChip.vue'
176
177
  import { useFileComments } from '../../composables/useFileComments.js'
177
178
  import { buildHeaders, prefixUrl } from '../../utils/index.js'
179
+ import { initialsOf } from '../../utils/initials.js'
178
180
  import { leafSourceUrl } from '../../utils/leafSourceUrl.js'
179
181
  import { extractMentionedGroupIds, extractMentionedIds } from '../../utils/mentions.js'
180
182
  import { quoteFor, supportsReplies, threadNotes, threadRootId } from '../../utils/noteThreads.js'
@@ -635,6 +637,8 @@ export default {
635
637
  return note.actorId || note.author || ''
636
638
  },
637
639
 
640
+ initialsOf,
641
+
638
642
  getNoteAuthorName(note) {
639
643
  return note.actorDisplayName || note.author || 'Unknown'
640
644
  },
@@ -870,6 +874,10 @@ export default {
870
874
  border-bottom: none;
871
875
  }
872
876
 
877
+ .cn-notes-card__initials {
878
+ display: none;
879
+ }
880
+
873
881
  .cn-notes-card__note-header {
874
882
  display: flex;
875
883
  justify-content: space-between;
@@ -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) {
@@ -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
  }