@conduction/nextcloud-vue 2.140.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 (74) hide show
  1. package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue.js +1 -1
  2. package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue2.js +10 -2
  3. package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue2.js.map +1 -1
  4. package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue3.js.map +1 -1
  5. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue.js +1 -1
  6. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js +11 -3
  7. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js.map +1 -1
  8. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js.map +1 -1
  9. package/dist/esm/components/CnChartWidget/CnChartWidget.vue.js +1 -1
  10. package/dist/esm/components/CnChartWidget/CnChartWidget.vue2.js +12 -0
  11. package/dist/esm/components/CnChartWidget/CnChartWidget.vue2.js.map +1 -1
  12. package/dist/esm/components/CnChartWidget/CnChartWidget.vue3.js +57 -40
  13. package/dist/esm/components/CnChartWidget/CnChartWidget.vue3.js.map +1 -1
  14. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue.js +1 -1
  15. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +1 -0
  16. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  17. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  18. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +5 -3
  19. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
  20. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +1 -0
  21. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
  22. package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue2.js +65 -1
  23. package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue2.js.map +1 -1
  24. package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue3.js +120 -69
  25. package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue3.js.map +1 -1
  26. package/dist/esm/components/CnMapWidget/CnMapWidget.vue.js +1 -1
  27. package/dist/esm/components/CnMapWidget/CnMapWidget.vue2.js +47 -9
  28. package/dist/esm/components/CnMapWidget/CnMapWidget.vue2.js.map +1 -1
  29. package/dist/esm/components/CnMapWidget/CnMapWidget.vue3.js.map +1 -1
  30. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue.js +1 -1
  31. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +3 -3
  32. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
  33. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js.map +1 -1
  34. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
  35. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +2 -0
  36. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
  37. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js +1 -1
  38. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
  39. package/dist/esm/components/CnRecordListWidget/CnRecordNoteRows.vue.js +1 -1
  40. package/dist/esm/components/CnRecordListWidget/CnRecordNoteRows.vue2.js +10 -0
  41. package/dist/esm/components/CnRecordListWidget/CnRecordNoteRows.vue2.js.map +1 -1
  42. package/dist/esm/components/CnRecordListWidget/CnRecordNoteRows.vue3.js +34 -26
  43. package/dist/esm/components/CnRecordListWidget/CnRecordNoteRows.vue3.js.map +1 -1
  44. package/dist/esm/l10n/en.json.js +2 -1
  45. package/dist/esm/l10n/en.json.js.map +1 -1
  46. package/dist/esm/l10n/nl.json.js +2 -1
  47. package/dist/esm/l10n/nl.json.js.map +1 -1
  48. package/dist/esm/library-style.js +1 -1
  49. package/dist/esm/nextcloud-vue.css +652 -391
  50. package/dist/esm/utils/groupSublines.js +120 -0
  51. package/dist/esm/utils/groupSublines.js.map +1 -0
  52. package/dist/nextcloud-vue.cjs.js +974 -638
  53. package/dist/nextcloud-vue.cjs.js.map +2 -2
  54. package/dist/nextcloud-vue.css +652 -391
  55. package/l10n/en.json +2 -1
  56. package/l10n/nl.json +2 -1
  57. package/package.json +1 -1
  58. package/src/components/CnAssignDialog/CnAssignDialog.vue +10 -2
  59. package/src/components/CnCellRenderer/CnCellRenderer.vue +11 -3
  60. package/src/components/CnChartWidget/CnChartWidget.md +1 -0
  61. package/src/components/CnChartWidget/CnChartWidget.vue +49 -4
  62. package/src/components/CnDashboardPage/CnDashboardPage.vue +1 -0
  63. package/src/components/CnDataTable/CnDataTable.vue +6 -3
  64. package/src/components/CnIndexSidebar/CnIndexSidebar.vue +101 -3
  65. package/src/components/CnMapWidget/CnMapWidget.vue +47 -9
  66. package/src/components/CnObjectListWidget/CnObjectListWidget.vue +3 -3
  67. package/src/components/CnRecordListWidget/CnRecordListWidget.vue +20 -1
  68. package/src/components/CnRecordListWidget/CnRecordNoteRows.vue +33 -9
  69. package/src/css/dialog.css +13 -1
  70. package/src/css/look-board-detail.css +12 -9
  71. package/src/css/look-board-index.css +90 -7
  72. package/src/css/look-board.css +92 -0
  73. package/src/css/table.css +12 -0
  74. package/src/utils/groupSublines.js +125 -0
@@ -52,13 +52,11 @@
52
52
  grid-row: 2;
53
53
  }
54
54
 
55
- /* A list page's breadcrumb and note read 15px; the case page's row stays 14px. */
56
- .cn-look-board .cn-index-page .cn-page-header .cn-header-trail.cn-detail-page__header-row2 {
57
- font-size: 15px;
58
- }
59
-
60
- .cn-look-board .cn-index-page .cn-header-trail.cn-detail-page__header-row2 .cn-detail-page__breadcrumbs .vue-crumb .button-vue.button-vue {
61
- font-size: 15px;
55
+ /* The note after the breadcrumb ("38 contracten bij 21 leveranciers") is 14px
56
+ muted on every Dq list board (37 of 37).
57
+ @spec openspec/changes/board-index-header-meta-14/specs/index-page/spec.md */
58
+ .cn-look-board .cn-index-page .cn-page-header .cn-header-trail .cn-detail-page__header-meta {
59
+ font-size: 14px;
62
60
  }
63
61
 
64
62
  .cn-look-board .cn-index-page .cn-page-header .cn-header-trail ~ .cn-page-header__description {
@@ -982,6 +980,17 @@
982
980
  text-decoration: none;
983
981
  }
984
982
 
983
+ /* `rowTitle: "strong"` (DqZaaktypeStatussen "Statussen", "Resultaten"): the
984
+ name column of a card's list, the cell's 14px at weight 600 in the main
985
+ ink, no underline; the row stays a link.
986
+ @spec openspec/changes/table-strong-title/specs/table-strong-title/spec.md#requirement-a-tables-title-column-can-read-as-a-strong-name */
987
+ .cn-look-board .cn-table-container--board.cn-table-container--title-strong .cn-data-table td.cn-table-col--title {
988
+ color: var(--color-main-text);
989
+ font-size: 14px;
990
+ font-weight: 600;
991
+ text-decoration: none;
992
+ }
993
+
985
994
  /* An avatar in the title column (PtAccounts): a 32px chip circle, 10px before
986
995
  the name, the e-mail under the name beside it. The name keeps the title's
987
996
  weight and size, and its underline: an inline-flex box is atomic, so the
@@ -2101,3 +2110,77 @@
2101
2110
  .cn-look-board .cn-index-page--with-side-panel:not(.cn-look-nextcloud *) > .cn-index-page__quick-filter-row {
2102
2111
  margin-bottom: calc(var(--cn-board-section-gap) + 12px);
2103
2112
  }
2113
+
2114
+ /* ---- The side panel's filters as checkbox lists (DqZakenSorteren) ------- */
2115
+ /* "Filters" with "Alles wissen" beside it; per field its 14px semibold label
2116
+ over rows of a checkbox, the value and its count in 13px muted at the end,
2117
+ 32px each.
2118
+ @spec openspec/changes/board-sidebar-filters-as-checkbox-lists/specs/index-sidebar/spec.md#requirement-the-board-look-draws-a-short-filter-as-a-checkbox-list-with-counts */
2119
+ .cn-look-board .cn-index-sidebar__filters-head {
2120
+ display: flex;
2121
+ align-items: center;
2122
+ justify-content: space-between;
2123
+ gap: 8px;
2124
+ margin-bottom: 14px;
2125
+ }
2126
+
2127
+ .cn-look-board .cn-index-sidebar__filters-head h3 {
2128
+ margin: 0;
2129
+ font-size: 15px;
2130
+ font-weight: 700;
2131
+ }
2132
+
2133
+ .cn-look-board .cn-index-sidebar__filters-clear {
2134
+ min-height: 0;
2135
+ margin: 0;
2136
+ padding: 0;
2137
+ border: 0;
2138
+ background: transparent;
2139
+ color: var(--color-primary-element);
2140
+ font-size: 14px;
2141
+ font-weight: 600;
2142
+ text-decoration: underline;
2143
+ cursor: pointer;
2144
+ }
2145
+
2146
+ .cn-look-board .cn-index-sidebar__filter-group:has(> .cn-index-sidebar__checklist) .cn-index-sidebar__filter-label {
2147
+ font-size: 14px;
2148
+ font-weight: 600;
2149
+ }
2150
+
2151
+ .cn-look-board .cn-index-sidebar__checklist {
2152
+ display: flex;
2153
+ flex-direction: column;
2154
+ gap: 4px;
2155
+ margin: 0;
2156
+ padding: 0;
2157
+ border: 0;
2158
+ }
2159
+
2160
+ .cn-look-board .cn-index-sidebar__checklist-row {
2161
+ display: flex;
2162
+ align-items: center;
2163
+ gap: 10px;
2164
+ min-height: 32px;
2165
+ font-size: 14px;
2166
+ cursor: pointer;
2167
+ }
2168
+
2169
+ .cn-look-board .cn-index-sidebar__checklist-row > input[type='checkbox'] {
2170
+ width: 18px;
2171
+ min-width: 18px;
2172
+ height: 18px;
2173
+ min-height: 18px;
2174
+ margin: 0;
2175
+ accent-color: var(--color-primary-element);
2176
+ }
2177
+
2178
+ .cn-look-board .cn-index-sidebar__checklist-label {
2179
+ flex: 1;
2180
+ min-width: 0;
2181
+ }
2182
+
2183
+ .cn-look-board .cn-index-sidebar__checklist-count {
2184
+ color: var(--color-text-maxcontrast);
2185
+ font-size: 13px;
2186
+ }
@@ -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,6 +3077,23 @@ 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
3099
  edged in the dark border grey, and an 18px radio. The chosen option keeps
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
@@ -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
+ }