@conduction/nextcloud-vue 2.139.0 → 2.141.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 (144) hide show
  1. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js +9 -2
  2. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
  3. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js.map +1 -1
  4. package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue.js +1 -1
  5. package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue2.js +10 -2
  6. package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue2.js.map +1 -1
  7. package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue3.js.map +1 -1
  8. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue.js +1 -1
  9. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js +28 -4
  10. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js.map +1 -1
  11. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js +2 -2
  12. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js.map +1 -1
  13. package/dist/esm/components/CnChartWidget/CnChartWidget.vue.js +1 -1
  14. package/dist/esm/components/CnChartWidget/CnChartWidget.vue2.js +64 -1
  15. package/dist/esm/components/CnChartWidget/CnChartWidget.vue2.js.map +1 -1
  16. package/dist/esm/components/CnChartWidget/CnChartWidget.vue3.js +76 -37
  17. package/dist/esm/components/CnChartWidget/CnChartWidget.vue3.js.map +1 -1
  18. package/dist/esm/components/CnConversationThread/CnConversationWidget.vue.js +1 -1
  19. package/dist/esm/components/CnConversationThread/CnConversationWidget.vue2.js +106 -0
  20. package/dist/esm/components/CnConversationThread/CnConversationWidget.vue2.js.map +1 -1
  21. package/dist/esm/components/CnConversationThread/CnConversationWidget.vue3.js +30 -7
  22. package/dist/esm/components/CnConversationThread/CnConversationWidget.vue3.js.map +1 -1
  23. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue.js +1 -1
  24. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +1 -0
  25. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  26. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  27. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +5 -3
  28. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
  29. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +1 -0
  30. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
  31. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue.js +1 -1
  32. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js +59 -3
  33. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js.map +1 -1
  34. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js +44 -34
  35. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js.map +1 -1
  36. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue.js +1 -1
  37. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js +28 -1
  38. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js.map +1 -1
  39. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js.map +1 -1
  40. package/dist/esm/components/CnFilesBrowser/filesBrowser.js +7 -1
  41. package/dist/esm/components/CnFilesBrowser/filesBrowser.js.map +1 -1
  42. package/dist/esm/components/CnFilesBrowser/filesBrowserColumns.js +5 -0
  43. package/dist/esm/components/CnFilesBrowser/filesBrowserColumns.js.map +1 -1
  44. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +30 -2
  45. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  46. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  47. package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue2.js +65 -1
  48. package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue2.js.map +1 -1
  49. package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue3.js +120 -69
  50. package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue3.js.map +1 -1
  51. package/dist/esm/components/CnMapWidget/CnMapWidget.vue.js +1 -1
  52. package/dist/esm/components/CnMapWidget/CnMapWidget.vue2.js +47 -9
  53. package/dist/esm/components/CnMapWidget/CnMapWidget.vue2.js.map +1 -1
  54. package/dist/esm/components/CnMapWidget/CnMapWidget.vue3.js.map +1 -1
  55. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue.js +1 -1
  56. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js +28 -3
  57. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js.map +1 -1
  58. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue3.js.map +1 -1
  59. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue.js +1 -1
  60. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +3 -3
  61. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
  62. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js.map +1 -1
  63. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
  64. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +2 -0
  65. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
  66. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js +1 -1
  67. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
  68. package/dist/esm/components/CnRecordListWidget/CnRecordNoteRows.vue.js +1 -1
  69. package/dist/esm/components/CnRecordListWidget/CnRecordNoteRows.vue2.js +20 -0
  70. package/dist/esm/components/CnRecordListWidget/CnRecordNoteRows.vue2.js.map +1 -1
  71. package/dist/esm/components/CnRecordListWidget/CnRecordNoteRows.vue3.js +34 -26
  72. package/dist/esm/components/CnRecordListWidget/CnRecordNoteRows.vue3.js.map +1 -1
  73. package/dist/esm/components/CnStackedBarWidget/CnStackedBarWidget.vue.js +1 -1
  74. package/dist/esm/components/CnStackedBarWidget/CnStackedBarWidget.vue2.js +12 -1
  75. package/dist/esm/components/CnStackedBarWidget/CnStackedBarWidget.vue2.js.map +1 -1
  76. package/dist/esm/components/CnStackedBarWidget/CnStackedBarWidget.vue3.js +1 -1
  77. package/dist/esm/components/CnStackedBarWidget/CnStackedBarWidget.vue3.js.map +1 -1
  78. package/dist/esm/components/CnTransitionInputFields/CnTransitionInputFields.vue.js +1 -1
  79. package/dist/esm/components/CnTransitionInputFields/CnTransitionInputFields.vue2.js +23 -1
  80. package/dist/esm/components/CnTransitionInputFields/CnTransitionInputFields.vue2.js.map +1 -1
  81. package/dist/esm/components/CnTransitionInputFields/CnTransitionInputFields.vue3.js +43 -13
  82. package/dist/esm/components/CnTransitionInputFields/CnTransitionInputFields.vue3.js.map +1 -1
  83. package/dist/esm/composables/indexSources.js +5 -2
  84. package/dist/esm/composables/indexSources.js.map +1 -1
  85. package/dist/esm/composables/useTaskInboxStore.js +4 -3
  86. package/dist/esm/composables/useTaskInboxStore.js.map +1 -1
  87. package/dist/esm/l10n/en.json.js +3 -1
  88. package/dist/esm/l10n/en.json.js.map +1 -1
  89. package/dist/esm/l10n/nl.json.js +6 -1
  90. package/dist/esm/l10n/nl.json.js.map +1 -1
  91. package/dist/esm/library-style.js +1 -1
  92. package/dist/esm/nextcloud-vue.css +961 -605
  93. package/dist/esm/utils/builtInFormatters.js +23 -1
  94. package/dist/esm/utils/builtInFormatters.js.map +1 -1
  95. package/dist/esm/utils/fillTemplate.js +7 -1
  96. package/dist/esm/utils/fillTemplate.js.map +1 -1
  97. package/dist/esm/utils/groupSublines.js +120 -0
  98. package/dist/esm/utils/groupSublines.js.map +1 -0
  99. package/dist/esm/utils/myTeam.js +24 -4
  100. package/dist/esm/utils/myTeam.js.map +1 -1
  101. package/dist/esm/utils/resolveFilterTokens.js +5 -4
  102. package/dist/esm/utils/resolveFilterTokens.js.map +1 -1
  103. package/dist/esm/utils/transitionInputs.js +43 -2
  104. package/dist/esm/utils/transitionInputs.js.map +1 -1
  105. package/dist/nextcloud-vue.cjs.js +1731 -846
  106. package/dist/nextcloud-vue.cjs.js.map +2 -2
  107. package/dist/nextcloud-vue.css +961 -605
  108. package/l10n/en.json +3 -1
  109. package/l10n/nl.json +6 -1
  110. package/package.json +1 -1
  111. package/src/components/CnAppRoot/CnAppRoot.vue +9 -2
  112. package/src/components/CnAssignDialog/CnAssignDialog.vue +10 -2
  113. package/src/components/CnCellRenderer/CnCellRenderer.vue +30 -6
  114. package/src/components/CnChartWidget/CnChartWidget.md +2 -0
  115. package/src/components/CnChartWidget/CnChartWidget.vue +121 -5
  116. package/src/components/CnConversationThread/CnConversationWidget.vue +126 -1
  117. package/src/components/CnDashboardPage/CnDashboardPage.vue +1 -0
  118. package/src/components/CnDataTable/CnDataTable.vue +6 -3
  119. package/src/components/CnFactsWidget/CnFactsWidget.vue +72 -5
  120. package/src/components/CnFilesBrowser/CnFilesBrowser.vue +28 -1
  121. package/src/components/CnFilesBrowser/filesBrowser.js +7 -1
  122. package/src/components/CnFilesBrowser/filesBrowserColumns.js +5 -0
  123. package/src/components/CnIndexPage/CnIndexPage.vue +30 -2
  124. package/src/components/CnIndexSidebar/CnIndexSidebar.vue +101 -3
  125. package/src/components/CnMapWidget/CnMapWidget.vue +47 -9
  126. package/src/components/CnObjectDataWidget/CnObjectDataWidget.vue +28 -3
  127. package/src/components/CnObjectListWidget/CnObjectListWidget.vue +3 -3
  128. package/src/components/CnRecordListWidget/CnRecordListWidget.vue +20 -1
  129. package/src/components/CnRecordListWidget/CnRecordNoteRows.vue +43 -9
  130. package/src/components/CnStackedBarWidget/CnStackedBarWidget.vue +68 -2
  131. package/src/components/CnTransitionInputFields/CnTransitionInputFields.vue +43 -1
  132. package/src/composables/indexSources.js +5 -2
  133. package/src/composables/useTaskInboxStore.js +4 -3
  134. package/src/css/dialog.css +13 -1
  135. package/src/css/look-board-detail.css +12 -9
  136. package/src/css/look-board-index.css +90 -7
  137. package/src/css/look-board.css +102 -2
  138. package/src/css/table.css +25 -0
  139. package/src/utils/builtInFormatters.js +22 -0
  140. package/src/utils/fillTemplate.js +7 -1
  141. package/src/utils/groupSublines.js +125 -0
  142. package/src/utils/myTeam.js +24 -4
  143. package/src/utils/resolveFilterTokens.js +5 -4
  144. package/src/utils/transitionInputs.js +42 -1
@@ -2173,6 +2173,26 @@ body:has(.cn-look-board):not(:has(.cn-look-nextcloud)) .v-popper__popper.action-
2173
2173
  line-height: normal;
2174
2174
  }
2175
2175
 
2176
+ /* A list in a side card draws a hairline over every row, the first too, so
2177
+ a line separates the title from the list (DqZaaktype*, DqTenant,
2178
+ DqSociaalDomein, DqTaak "Versies" and the like). Its row titles keep the
2179
+ main ink without an underline: the plain side rule marks them as links,
2180
+ and the board underlines on hover only. The footer note is 13px muted; the
2181
+ theme sets paragraphs through its tokens with `!important`, so the note
2182
+ hands it its size and ink through those tokens. */
2183
+ .cn-look-board .cn-detail-page__side .cn-record-list--rows:not(.cn-look-nextcloud *) .cn-record-list__item {
2184
+ border-top: 1px solid var(--cn-board-hairline);
2185
+ }
2186
+
2187
+ .cn-look-board .cn-detail-page__side .cn-record-list--rows:not(.cn-look-nextcloud *) .cn-record-list__row:not(:hover):not(:focus-visible) .cn-record-list__title {
2188
+ text-decoration: none;
2189
+ }
2190
+
2191
+ .cn-look-board .cn-record-list__footer-note:not(.cn-look-nextcloud *) {
2192
+ --nldesign-component-paragraph-font-size: 13px;
2193
+ --nldesign-component-paragraph-color: var(--color-text-maxcontrast);
2194
+ }
2195
+
2176
2196
  /* An audit row is a board list row (DqHandhaving, DqTenant "Historie"): 10px
2177
2197
  above and below, a hairline over every row including the first, the head
2178
2198
  and the line under it at their normal line height, 2px apart: 56px, the
@@ -2412,6 +2432,29 @@ body:has(.cn-look-board):not(:has(.cn-look-nextcloud)) .v-popper__popper.action-
2412
2432
  gap: 2px;
2413
2433
  }
2414
2434
 
2435
+ /* A stacked list (`density: "stacked"`) keeps its rows unruled and unpadded
2436
+ in the board look too, 10px apart (DqDagAfsluiten "Morgen eerst").
2437
+ @spec openspec/changes/record-list-stacked-density/specs/record-list-stacked-density/spec.md#requirement-a-record-list-can-stack-its-rows */
2438
+ .cn-look-board .cn-dashboard-page:not(.cn-look-nextcloud *) .cn-record-list--stacked .cn-record-list__row {
2439
+ min-height: 0;
2440
+ padding-block: 0;
2441
+ }
2442
+
2443
+ .cn-look-board .cn-dashboard-page:not(.cn-look-nextcloud *) .cn-record-list--stacked.cn-record-list--rows .cn-record-list__item + .cn-record-list__item {
2444
+ border-top: 0;
2445
+ }
2446
+
2447
+ /* A dashboard card's task list and decision list rule above every row, the
2448
+ first one under the title included, as the record lists with ruleAbove do
2449
+ (DqMijnWerk "Mijn taken", "Mijn archiefbeoordelingen"); no rule under the
2450
+ last row.
2451
+ @spec openspec/changes/board-dashboard-lists-rule-above/specs/board-dashboard-lists-rule-above/spec.md#requirement-a-dashboard-task-or-decision-list-rules-above-every-row */
2452
+ .cn-look-board .cn-dashboard-page:not(.cn-look-nextcloud *) .cn-tasks-widget__row,
2453
+ .cn-look-board .cn-dashboard-page:not(.cn-look-nextcloud *) .cn-decision-list__row {
2454
+ border-block-end: 0;
2455
+ border-top: 1px solid var(--color-border);
2456
+ }
2457
+
2415
2458
  /* Hover: the whole row takes the hover ground, 8px beyond the text so the
2416
2459
  text does not move. */
2417
2460
  .cn-look-board .cn-dashboard-page:not(.cn-look-nextcloud *) .cn-record-list__row--link {
@@ -2680,6 +2723,17 @@ body:has(.cn-look-board):not(:has(.cn-look-nextcloud)) .v-popper__popper.action-
2680
2723
  padding: 0;
2681
2724
  }
2682
2725
 
2726
+ /* A chart in a section of a sections widget (a framed section card, 14px
2727
+ 16px in) takes none of the dashboard card's inset either: its bars and plot
2728
+ start 17px in, not 39 (DqDoorlooptijd "Naleving": Naleving per zaaktype,
2729
+ Verdeling doorlooptijd, Maandelijkse SLA-trend, Doorstroom).
2730
+ @spec openspec/changes/board-sections-chart-single-inset/specs/board-sections-chart-single-inset/spec.md#requirement-a-chart-inside-a-dashboard-sections-card-is-inset-once */
2731
+ .cn-look-board .cn-dashboard-page:not(.cn-look-nextcloud *) .cn-widget-wrapper:not(.cn-widget-wrapper--borderless) .cn-sections-widget__section .cn-chart-widget__html,
2732
+ .cn-look-board .cn-dashboard-page:not(.cn-look-nextcloud *) .cn-widget-wrapper:not(.cn-widget-wrapper--borderless) .cn-sections-widget__section .cn-chart-widget__canvas,
2733
+ .cn-look-board .cn-dashboard-page:not(.cn-look-nextcloud *) .cn-widget-wrapper:not(.cn-widget-wrapper--borderless) .cn-sections-widget__section .cn-chart-widget > .cn-chart-widget__footnote {
2734
+ padding: 0;
2735
+ }
2736
+
2683
2737
  /* ---- A plain sections widget on a dashboard card ------------------------ */
2684
2738
  /* The card is flush, because a sections widget usually draws its own cards;
2685
2739
  the plain one draws none, so its body takes the card's side inset and 20px
@@ -2805,6 +2859,27 @@ body:has(.cn-look-board):not(:has(.cn-look-nextcloud)) .v-popper__popper.action-
2805
2859
  font-weight: 600;
2806
2860
  }
2807
2861
 
2862
+ /* A toned cell takes the theme's status badge grounds, as the board's tones
2863
+ are (DqHandhaving: #E7F3EA, #FFF4DE, #FCEDEC); the darkest tone has its
2864
+ own token and otherwise deepens the error ground. Nextcloud's own mix of
2865
+ the status colours read a shade off each.
2866
+ @spec openspec/changes/board-matrix-tones/specs/board-matrix-tones/spec.md#requirement-a-board-matrix-cell-takes-the-themes-status-grounds */
2867
+ .cn-look-board .cn-chart-widget__matrix:not(.cn-look-nextcloud *) .cn-chart-widget__matrix-cell--success {
2868
+ background: var(--nldesign-component-status-badge-success-background-color, var(--color-success-light, color-mix(in srgb, var(--color-success) 15%, var(--color-main-background))));
2869
+ }
2870
+
2871
+ .cn-look-board .cn-chart-widget__matrix:not(.cn-look-nextcloud *) .cn-chart-widget__matrix-cell--warning {
2872
+ background: var(--nldesign-component-status-badge-warning-background-color, var(--color-warning-light, color-mix(in srgb, var(--color-warning) 15%, var(--color-main-background))));
2873
+ }
2874
+
2875
+ .cn-look-board .cn-chart-widget__matrix:not(.cn-look-nextcloud *) .cn-chart-widget__matrix-cell--error {
2876
+ background: var(--nldesign-component-status-badge-error-background-color, var(--color-error-light, color-mix(in srgb, var(--color-error) 12%, var(--color-main-background))));
2877
+ }
2878
+
2879
+ .cn-look-board .cn-chart-widget__matrix:not(.cn-look-nextcloud *) .cn-chart-widget__matrix-cell--danger {
2880
+ background: var(--nldesign-component-status-badge-danger-background-color, color-mix(in srgb, var(--color-error) 10%, var(--nldesign-component-status-badge-error-background-color, var(--color-main-background))));
2881
+ }
2882
+
2808
2883
  /* A cell without a tone ("geen") sits on the page ground in the muted ink,
2809
2884
  as the board draws it; toned and coloured cells keep their ground.
2810
2885
  @spec openspec/changes/board-matrix-plain-cells/specs/board-matrix-plain-cells/spec.md#requirement-a-board-matrix-cell-without-a-tone-sits-on-the-ground-in-the-muted-ink */
@@ -3002,10 +3077,27 @@ body:has(.cn-look-board):not(:has(.cn-look-nextcloud)) .v-popper__popper.action-
3002
3077
  line-height: normal;
3003
3078
  }
3004
3079
 
3080
+ /* A bar list with the value at the bar's end (DqProcesanalyse "Doorlooptijd
3081
+ per status") sets its rows in 13px, the label in the secondary text, the
3082
+ value in the main ink at 600, rows 34px apart.
3083
+ @spec openspec/changes/chart-bar-list-value-at-end/specs/chart-bar-list-value-at-end/spec.md#requirement-a-bar-list-can-put-each-value-at-its-bars-end */
3084
+ .cn-look-board .cn-chart-widget__bar-list--value-at-end:not(.cn-look-nextcloud *) {
3085
+ gap: 16px;
3086
+ }
3087
+
3088
+ .cn-look-board .cn-chart-widget__bar-list--value-at-end:not(.cn-look-nextcloud *) .cn-chart-widget__bar-line {
3089
+ font-size: 13px;
3090
+ }
3091
+
3092
+ .cn-look-board .cn-chart-widget__bar-list--value-at-end:not(.cn-look-nextcloud *) .cn-chart-widget__bar-label {
3093
+ --nldesign-color-on-surface: var(--cn-board-text-soft);
3094
+ color: var(--cn-board-text-soft);
3095
+ }
3096
+
3005
3097
  /* A dialog's options as the boards draw them (DqVolgendeStap, DqBesluit,
3006
3098
  DqDocumentGenereren, DqFilterBewerken): rounded at 10px, an idle option
3007
- edged in the dark border grey, and an 18px radio. The chosen and the
3008
- refused options keep their own edges. */
3099
+ edged in the dark border grey, and an 18px radio. The chosen option keeps
3100
+ its own edge. */
3009
3101
  .cn-look-board .cn-choice-cards .cn-choice-cards__row:not(.cn-look-nextcloud *),
3010
3102
  .cn-look-board .cn-choice-cards .cn-choice-cards__cell--with-details:not(.cn-look-nextcloud *) {
3011
3103
  border-radius: 10px;
@@ -3015,6 +3107,14 @@ body:has(.cn-look-board):not(:has(.cn-look-nextcloud)) .v-popper__popper.action-
3015
3107
  border-color: var(--color-border-maxcontrast);
3016
3108
  }
3017
3109
 
3110
+ /* A refused option keeps that edge and sits on the sunken ground
3111
+ (DqVolgendeStap: Hervatten, Archiveren, Heropenen). */
3112
+ .cn-look-board .cn-choice-cards .cn-choice-cards__row--disabled:not(.cn-look-nextcloud *),
3113
+ .cn-look-board .cn-choice-cards .cn-choice-cards__row--disabled:hover:not(.cn-look-nextcloud *) {
3114
+ border-color: var(--color-border-maxcontrast);
3115
+ background: var(--cn-board-sunken-ground);
3116
+ }
3117
+
3018
3118
  /* A chosen option stays white under its primary edge, as every Dq dialog board
3019
3119
  draws it (design-system #371); the Nextcloud look keeps the tint.
3020
3120
  @spec openspec/changes/board-choice-row-chosen-white/specs/board-choice-row-chosen-white/spec.md#requirement-a-chosen-board-option-is-white */
package/src/css/table.css CHANGED
@@ -226,6 +226,18 @@
226
226
  overflow-wrap: anywhere;
227
227
  }
228
228
 
229
+ /* In a table fitted to its tile every column but the growing one asks for
230
+ as little width as its content allows (`width: 1%`). With
231
+ `overflow-wrap: anywhere` that is one character, so a wrapping head broke
232
+ letter by letter ("D o e l", DqDoorlooptijd "Prestaties per zaaktype",
233
+ 240px tall). Here a wrapping column breaks between words and only breaks
234
+ a word that is longer than the column.
235
+ @spec openspec/changes/fit-table-wrap-by-word/specs/fit-table-wrap-by-word/spec.md#requirement-a-wrapping-column-in-a-fitted-table-breaks-between-words */
236
+ .cn-data-table--fit th.cn-cell--wrap,
237
+ .cn-data-table--fit td.cn-cell--wrap {
238
+ overflow-wrap: break-word;
239
+ }
240
+
229
241
  /* Opt-in fixed layout (CnDataTable's `fixedLayout` prop).
230
242
 
231
243
  Under the default `table-layout: auto` a column's declared `width` is only a
@@ -289,6 +301,19 @@
289
301
  white-space: nowrap;
290
302
  }
291
303
 
304
+ /* A growing column that opts into wrapping (`cn-cell--wrap`, DqDoorlooptijd
305
+ "Zaken met risico" titles) wraps its text over lines instead of cutting
306
+ it, and keeps a readable width: with `max-width: 0` the wide one-line
307
+ cells beside it squeezed it to its padding (24px).
308
+ @spec openspec/changes/fit-table-wrap-column-keeps-width/specs/fit-table-wrap-column-keeps-width/spec.md#requirement-a-wrapping-growing-column-keeps-a-readable-width */
309
+ .cn-data-table--fit th.cn-table-col--grow.cn-cell--wrap,
310
+ .cn-data-table--fit td.cn-table-col--grow.cn-cell--wrap {
311
+ min-width: 8em;
312
+ max-width: none;
313
+ overflow-wrap: anywhere;
314
+ white-space: normal;
315
+ }
316
+
292
317
  /* The secondary line of the growing column ("2026-0004" under a case title)
293
318
  is a line of its own with its own ellipsis. The cell does not wrap, so in
294
319
  the board look, where the secondary line is an inline block (the title
@@ -470,6 +470,27 @@ export function formatMasked(value, _row, _property, options) {
470
470
  return '*'.repeat(Math.min(text.length - visible, 5)) + (visible > 0 ? text.slice(-visible) : '')
471
471
  }
472
472
 
473
+ /**
474
+ * A stored value through a display map (`valueMap` registry key): the label
475
+ * `options.map` gives the value, else the value as stored. "Besloten
476
+ * Vennootschap" reads "BV" on DqOrganisaties.
477
+ *
478
+ * @param {unknown} value The stored value.
479
+ * @param {object} [_row] The full row (unused).
480
+ * @param {object} [_property] The schema property (unused).
481
+ * @param {{map?: Object<string, string>}} [options] The column's `formatterOptions`.
482
+ * @return {string} The label, or the value.
483
+ * @spec openspec/changes/value-map-formatter/specs/index-page/spec.md#requirement-a-column-can-show-stored-values-through-a-display-map
484
+ */
485
+ export function formatValueMap(value, _row, _property, options) {
486
+ if (value === null || value === undefined || value === '') {
487
+ return ''
488
+ }
489
+ const map = options && options.map && typeof options.map === 'object' ? options.map : {}
490
+ const label = Object.hasOwn(map, String(value)) ? map[String(value)] : undefined
491
+ return typeof label === 'string' && label !== '' ? label : String(value)
492
+ }
493
+
473
494
  /**
474
495
  * Built-in formatter registry merged under any consumer-registered
475
496
  * `formatters` in `CnAppRoot`'s `cnFormatters` provide.
@@ -516,4 +537,5 @@ export const BUILT_IN_FORMATTERS = {
516
537
  connectionSettingsLabel: formatConnectionSettingsLabel,
517
538
  masked: formatMasked,
518
539
  isoDuration: formatIsoDuration,
540
+ valueMap: formatValueMap,
519
541
  }
@@ -64,7 +64,7 @@ export function formatLongDate(value, now = new Date(), withYear = false) {
64
64
  * Format one token value.
65
65
  *
66
66
  * @param {unknown} value The raw value.
67
- * @param {string} format `date`, `datetime`, `weekday`, `longdate`, `time`, `user`, `currency`, `relative`, `relativetime`, `ago`, `week`, `days`, `lower` or '' (as is).
67
+ * @param {string} format `date`, `datetime`, `weekday`, `longdate`, `time`, `user`, `currency`, `relative`, `relativetime`, `ago`, `week`, `days`, `lower`, `mailbox` or '' (as is).
68
68
  * @param {Date} now The reference moment.
69
69
  * @param {string} [arg] The format's argument (`currency:EUR`, or a row path holding the code).
70
70
  * @param {object|null} [object] The row, for a currency read from a field.
@@ -125,6 +125,12 @@ function formatValue(value, format, now, arg = '', object = null, config = null,
125
125
  if (format === 'ago') {
126
126
  return formatAgo(value, now)
127
127
  }
128
+ if (format === 'mailbox') {
129
+ // An address up to and with its @ ("gemeente@" from gemeente@zuiddrecht.nl).
130
+ const text = String(value)
131
+ const at = text.indexOf('@')
132
+ return at > 0 ? text.slice(0, at + 1) : text
133
+ }
128
134
  if (format === 'lower') {
129
135
  return String(value).toLocaleLowerCase()
130
136
  }
@@ -0,0 +1,125 @@
1
+ /**
2
+ * The line under a team in a picker: how many members it has and what it is
3
+ * for ("6 leden · bezwaar, beroep en Woo-advies", board DqOverdragen).
4
+ *
5
+ * The count comes from Nextcloud's group details; that endpoint answers
6
+ * administrators and group admins only, so for anyone else the count is left
7
+ * out rather than guessed. Nextcloud keeps no description for a group, so the
8
+ * text comes from an app's own records when the app names them (`describe`:
9
+ * `{ register, schema, groupField?, textField? }`, by default the record's
10
+ * `ncGroupId` and `description`).
11
+ *
12
+ * SPDX-FileCopyrightText: 2026 Conduction B.V.
13
+ * SPDX-License-Identifier: EUPL-1.2
14
+ *
15
+ * @module utils/groupSublines
16
+ * @spec openspec/changes/assign-team-option-subline/specs/assign-dialog/spec.md
17
+ */
18
+
19
+ import axios from '@nextcloud/axios'
20
+ import { translatePlural as n } from '@nextcloud/l10n'
21
+ import { generateOcsUrl } from '@nextcloud/router'
22
+ import { fetchReferenceObjects } from './boardReferenceColumns.js'
23
+
24
+ const descriptionCache = new Map()
25
+
26
+ /**
27
+ * Member counts by group id for the groups a search finds. Fails soft to `{}`.
28
+ *
29
+ * @param {string} query The search term.
30
+ * @return {Promise<Record<string, number>>} Counts by group id.
31
+ * @spec openspec/changes/assign-team-option-subline/specs/assign-dialog/spec.md
32
+ */
33
+ async function memberCounts(query) {
34
+ try {
35
+ const response = await axios.get(generateOcsUrl('cloud/groups/details'), {
36
+ headers: { 'OCS-APIRequest': 'true', Accept: 'application/json' },
37
+ params: { search: query || '', limit: 50 },
38
+ })
39
+ const data = response && response.data && response.data.ocs ? response.data.ocs.data : (response && response.data)
40
+ const groups = data && Array.isArray(data.groups) ? data.groups : []
41
+ const out = {}
42
+ for (const group of groups) {
43
+ const count = Number(group && group.usercount)
44
+ if (group && group.id && Number.isFinite(count)) {
45
+ out[String(group.id)] = count
46
+ }
47
+ }
48
+ return out
49
+ } catch {
50
+ return {}
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Descriptions by group id from the app's records. Read once per source.
56
+ *
57
+ * @param {{register?: string, schema?: string, groupField?: string, textField?: string}|null} describe The source.
58
+ * @return {Promise<Record<string, string>>} Descriptions by group id.
59
+ * @spec openspec/changes/assign-team-option-subline/specs/assign-dialog/spec.md
60
+ */
61
+ async function descriptions(describe) {
62
+ if (!describe || !describe.register || !describe.schema) {
63
+ return {}
64
+ }
65
+ const groupField = describe.groupField || 'ncGroupId'
66
+ const textField = describe.textField || 'description'
67
+ const key = [describe.register, describe.schema, groupField, textField].join('/')
68
+ if (!descriptionCache.has(key)) {
69
+ descriptionCache.set(key, fetchReferenceObjects({ register: describe.register, schema: describe.schema, limit: 500 })
70
+ .then((rows) => {
71
+ const out = {}
72
+ for (const row of rows) {
73
+ const gid = row && row[groupField]
74
+ const text = row && typeof row[textField] === 'string' ? row[textField].trim() : ''
75
+ if (gid && text && out[String(gid)] === undefined) {
76
+ out[String(gid)] = text
77
+ }
78
+ }
79
+ return out
80
+ })
81
+ .catch(() => {
82
+ descriptionCache.delete(key)
83
+ return {}
84
+ }))
85
+ }
86
+ return descriptionCache.get(key)
87
+ }
88
+
89
+ /**
90
+ * The line under each group: its member count and its description, joined by
91
+ * " · ", either left out when unknown.
92
+ *
93
+ * @param {string[]} ids The group ids to describe.
94
+ * @param {string} query The search the groups came from.
95
+ * @param {object|null} [describe] Where the app keeps a group's description.
96
+ * @return {Promise<Record<string, string>>} The line by group id ('' when nothing is known).
97
+ * @spec openspec/changes/assign-team-option-subline/specs/assign-dialog/spec.md
98
+ */
99
+ export async function groupSublines(ids, query, describe = null) {
100
+ if (!Array.isArray(ids) || ids.length === 0) {
101
+ return {}
102
+ }
103
+ const [counts, texts] = await Promise.all([memberCounts(query), descriptions(describe)])
104
+ const out = {}
105
+ for (const id of ids) {
106
+ const parts = []
107
+ if (counts[id] !== undefined) {
108
+ parts.push(n('nextcloud-vue', '{count} member', '{count} members', counts[id], { count: counts[id] }))
109
+ }
110
+ if (texts[id]) {
111
+ parts.push(texts[id])
112
+ }
113
+ out[id] = parts.join(' · ')
114
+ }
115
+ return out
116
+ }
117
+
118
+ /**
119
+ * Forget the cached descriptions (tests, or after the source changed).
120
+ *
121
+ * @return {void}
122
+ */
123
+ export function clearGroupSublineCache() {
124
+ descriptionCache.clear()
125
+ }
@@ -30,12 +30,32 @@ let _source = null
30
30
  /** The read in flight, shared by every caller. */
31
31
  let _pending = null
32
32
 
33
+ /**
34
+ * The team the app answered, as kept: one group id, a list of two or more,
35
+ * or `''` for none. Blank entries are dropped and a list of one reads as that
36
+ * one id.
37
+ *
38
+ * @spec openspec/changes/my-team-source-may-answer-several-teams/specs/schema-utilities/spec.md#requirement-myteam-may-resolve-to-several-teams
39
+ * @param {unknown} team What the app's loader answered.
40
+ * @return {string|string[]} The kept team.
41
+ */
42
+ function normalizeTeam(team) {
43
+ if (Array.isArray(team)) {
44
+ const ids = team.filter((id) => typeof id === 'string').map((id) => id.trim()).filter((id) => id !== '')
45
+ if (ids.length === 0) {
46
+ return ''
47
+ }
48
+ return ids.length === 1 ? ids[0] : ids
49
+ }
50
+ return typeof team === 'string' ? team.trim() : ''
51
+ }
52
+
33
53
  /**
34
54
  * Name where the current user's primary team is read: a function answering
35
55
  * the team's Nextcloud group id, or null/'' for a user in no team.
36
56
  *
37
57
  * @spec openspec/changes/saved-view-tokens-my-team/specs/schema-utilities/spec.md#requirement-myteam-resolves-to-the-current-users-primary-team
38
- * @param {(() => Promise<string|null>)|null} loader The app's loader, or null to unregister.
58
+ * @param {(() => Promise<string|string[]|null>)|null} loader The app's loader (a group id, or the ids of several teams), or null to unregister.
39
59
  * @return {void}
40
60
  */
41
61
  export function registerMyTeamSource(loader) {
@@ -49,7 +69,7 @@ export function registerMyTeamSource(loader) {
49
69
  * when no source is registered, or when the read fails.
50
70
  *
51
71
  * @spec openspec/changes/saved-view-tokens-my-team/specs/schema-utilities/spec.md#requirement-myteam-resolves-to-the-current-users-primary-team
52
- * @return {Promise<string>} The team's group id, or ''.
72
+ * @return {Promise<string|string[]>} The team's group id (or ids), or ''.
53
73
  */
54
74
  export function loadMyTeam() {
55
75
  if (_state.team !== null) {
@@ -64,7 +84,7 @@ export function loadMyTeam() {
64
84
  }
65
85
  _pending = Promise.resolve()
66
86
  .then(() => _source())
67
- .then((team) => (typeof team === 'string' ? team.trim() : ''))
87
+ .then((team) => normalizeTeam(team))
68
88
  .catch(() => '')
69
89
  .then((team) => {
70
90
  _state.team = team
@@ -79,7 +99,7 @@ export function loadMyTeam() {
79
99
  * it is being read (and starts the read on first use).
80
100
  *
81
101
  * @spec openspec/changes/saved-view-tokens-my-team/specs/schema-utilities/spec.md#requirement-myteam-resolves-to-the-current-users-primary-team
82
- * @return {string|null} The team, '' or null.
102
+ * @return {string|string[]|null} The team (or teams), '' or null.
83
103
  */
84
104
  export function peekMyTeam() {
85
105
  if (_state.team === null) {
@@ -89,15 +89,16 @@ export const MY_GROUPS_TOKEN = '@myGroups'
89
89
  export const MY_TEAM_TOKEN = '@myTeam'
90
90
 
91
91
  /**
92
- * Resolve `@myTeam`: the team's group id; `@myTeam` while it is being read (the
92
+ * Resolve `@myTeam`: the team's group id, or the ids of several teams when the
93
+ * app answers a list (an IN filter); `@myTeam` while it is being read (the
93
94
  * caller waits); `@myTeam?` for a user in no team (the caller drops the key).
94
95
  *
95
96
  * @spec openspec/changes/saved-view-tokens-my-team/specs/schema-utilities/spec.md#requirement-myteam-resolves-to-the-current-users-primary-team
96
- * @param {{myTeam?: string}} [ctx] Optional context; `ctx.myTeam` wins.
97
- * @return {string} The group id or the token.
97
+ * @param {{myTeam?: (string|string[])}} [ctx] Optional context; `ctx.myTeam` wins.
98
+ * @return {string|string[]} The group id(s) or the token.
98
99
  */
99
100
  function resolveMyTeam(ctx) {
100
- const team = ctx && typeof ctx.myTeam === 'string' ? ctx.myTeam : peekMyTeam()
101
+ const team = ctx && (typeof ctx.myTeam === 'string' || Array.isArray(ctx.myTeam)) ? ctx.myTeam : peekMyTeam()
101
102
  if (team === null) {
102
103
  return MY_TEAM_TOKEN
103
104
  }
@@ -223,6 +223,41 @@ function daysAfter(days, now) {
223
223
  return date
224
224
  }
225
225
 
226
+ /**
227
+ * Today plus that many days, as a date input's value.
228
+ *
229
+ * @param {unknown} days Whole days, 1 or more.
230
+ * @param {Date} [now] What counts as today.
231
+ * @return {string} `yyyy-mm-dd`, '' without a positive whole number.
232
+ * @spec openspec/changes/days-input-as-date/specs/transition-inputs/spec.md
233
+ */
234
+ export function dayAfterIso(days, now = new Date()) {
235
+ const date = daysAfter(days, now)
236
+ if (date === null) {
237
+ return ''
238
+ }
239
+ const pad = (number) => String(number).padStart(2, '0')
240
+ return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`
241
+ }
242
+
243
+ /**
244
+ * The whole days from today to a date input's value, at least 1.
245
+ *
246
+ * @param {unknown} iso A `yyyy-mm-dd` date.
247
+ * @param {Date} [now] What counts as today.
248
+ * @return {string} The days, '' without a date.
249
+ * @spec openspec/changes/days-input-as-date/specs/transition-inputs/spec.md
250
+ */
251
+ export function daysUntilIso(iso, now = new Date()) {
252
+ const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(String(iso ?? '').trim())
253
+ if (!match) {
254
+ return ''
255
+ }
256
+ const target = Date.UTC(Number(match[1]), Number(match[2]) - 1, Number(match[3]))
257
+ const today = Date.UTC(now.getFullYear(), now.getMonth(), now.getDate())
258
+ return String(Math.max(1, Math.round((target - today) / 86400000)))
259
+ }
260
+
226
261
  /**
227
262
  * A date as a reader sees it: day, short month and year.
228
263
  *
@@ -290,6 +325,9 @@ export function fillInputTemplate(template, values, now = new Date()) {
290
325
  * in that other input.
291
326
  * - `preview: { label, template, note, testid }`: a line under the input,
292
327
  * filled by `fillInputTemplate` and shown only once it resolves.
328
+ * - `dateField: { label, note, testid }`: on a days input, an editable date
329
+ * under it, today plus the days; a chosen date sets the days to the whole
330
+ * days from today, at least 1. `dateValue` carries the date.
293
331
  * - `default`: the starting value.
294
332
  * - Presentation: `labelOutside`, `id`, `note`, `wide`, `testid`, `rows`,
295
333
  * `placeholder`.
@@ -302,7 +340,7 @@ export function fillInputTemplate(template, values, now = new Date()) {
302
340
  * @param {string} [options.register] Register the reference pickers fall back to.
303
341
  * @param {object} [options.context] What `optionsSource` reads.
304
342
  * @param {object} [options.values] The values so far.
305
- * @param {Date} [options.now] What counts as today, for previews.
343
+ * @param {Date} [options.now] What counts as today, for previews and `dateField`.
306
344
  * @return {Array<object>} The descriptors.
307
345
  */
308
346
  export function resolveInputFields(declared, { schema = null, register = '', context = {}, values = {}, now } = {}) {
@@ -345,6 +383,9 @@ export function resolveInputFields(declared, { schema = null, register = '', con
345
383
  field.previewText = preview && typeof preview === 'object'
346
384
  ? fillInputTemplate(preview.template, values, now)
347
385
  : ''
386
+ field.dateValue = field.input.dateField && typeof field.input.dateField === 'object'
387
+ ? dayAfterIso(values[field.key], now)
388
+ : ''
348
389
  }
349
390
 
350
391
  return fields