@conduction/nextcloud-vue 2.135.0 → 2.136.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 (63) hide show
  1. package/dist/esm/components/CnActionDialog/CnActionDialog.vue.js +1 -1
  2. package/dist/esm/components/CnActionDialog/CnActionDialog.vue2.js +2 -1
  3. package/dist/esm/components/CnActionDialog/CnActionDialog.vue2.js.map +1 -1
  4. package/dist/esm/components/CnActionDialog/CnActionDialog.vue3.js +65 -50
  5. package/dist/esm/components/CnActionDialog/CnActionDialog.vue3.js.map +1 -1
  6. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue.js +1 -1
  7. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue2.js +6 -0
  8. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue2.js.map +1 -1
  9. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue3.js +9 -1
  10. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue3.js.map +1 -1
  11. package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue.js +1 -1
  12. package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue2.js.map +1 -1
  13. package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue3.js +4 -11
  14. package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue3.js.map +1 -1
  15. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +12 -0
  16. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
  17. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +1 -1
  18. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
  19. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +11 -0
  20. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  21. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +1 -1
  22. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  23. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +3 -1
  24. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  25. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +2 -1
  26. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  27. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
  28. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +18 -0
  29. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
  30. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js +38 -23
  31. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
  32. package/dist/esm/components/CnWorkingCalendarWidget/CnWorkingCalendarWidget.vue.js +1 -1
  33. package/dist/esm/components/CnWorkingCalendarWidget/CnWorkingCalendarWidget.vue2.js.map +1 -1
  34. package/dist/esm/components/CnWorkingCalendarWidget/CnWorkingCalendarWidget.vue3.js.map +1 -1
  35. package/dist/esm/composables/indexSources.js +69 -3
  36. package/dist/esm/composables/indexSources.js.map +1 -1
  37. package/dist/esm/l10n/en.json.js +20 -20
  38. package/dist/esm/l10n/nl.json.js +21 -21
  39. package/dist/esm/library-style.js +1 -1
  40. package/dist/esm/nextcloud-vue.css +322 -164
  41. package/dist/esm/utils/widgetDispatch.js +3 -1
  42. package/dist/esm/utils/widgetDispatch.js.map +1 -1
  43. package/dist/nextcloud-vue.cjs.js +739 -589
  44. package/dist/nextcloud-vue.cjs.js.map +2 -2
  45. package/dist/nextcloud-vue.css +322 -164
  46. package/l10n/en.json +20 -20
  47. package/l10n/nl.json +21 -21
  48. package/package.json +1 -1
  49. package/src/components/CnActionDialog/CnActionDialog.vue +45 -4
  50. package/src/components/CnAuditTrailCard/CnAuditTrailCard.vue +15 -0
  51. package/src/components/CnAuditTrailWidget/CnAuditTrailWidget.vue +2 -10
  52. package/src/components/CnDataTable/CnDataTable.md +1 -0
  53. package/src/components/CnDataTable/CnDataTable.vue +13 -1
  54. package/src/components/CnDetailPage/CnDetailPage.md +1 -0
  55. package/src/components/CnDetailPage/CnDetailPage.vue +12 -1
  56. package/src/components/CnIndexPage/CnIndexPage.vue +4 -1
  57. package/src/components/CnRecordListWidget/CnRecordListWidget.vue +38 -2
  58. package/src/components/CnWorkingCalendarWidget/CnWorkingCalendarWidget.vue +7 -0
  59. package/src/composables/indexSources.js +69 -3
  60. package/src/css/look-board-detail.css +47 -1
  61. package/src/css/look-board-index.css +26 -1
  62. package/src/css/look-board.css +46 -3
  63. package/src/utils/widgetDispatch.js +3 -1
package/l10n/en.json CHANGED
@@ -1484,18 +1484,6 @@
1484
1484
  "Pages": "Pages",
1485
1485
  "Open {module}": "Open {module}",
1486
1486
  "{list} and {last}": "{list} and {last}",
1487
- "%n page: {pages}": [
1488
- "%n page: {pages}",
1489
- "%n pages: {pages}"
1490
- ],
1491
- "%n page, including {pages}": [
1492
- "%n page, including {pages}",
1493
- "%n pages, including {pages}"
1494
- ],
1495
- "{on} of %n is in the menu": [
1496
- "{on} of %n is in the menu",
1497
- "{on} of %n are in the menu"
1498
- ],
1499
1487
  "Put it in the menu:": "Put it in the menu:",
1500
1488
  "Admin settings": "Admin settings",
1501
1489
  "Support this app": "Support this app",
@@ -1513,10 +1501,6 @@
1513
1501
  "{status}, {expected}": "{status}, {expected}",
1514
1502
  "Build on this app": "Build on this app",
1515
1503
  "AI skills": "AI skills",
1516
- "%n feature": [
1517
- "%n feature",
1518
- "%n features"
1519
- ],
1520
1504
  "Your account": "Your account",
1521
1505
  "Only for you. Colleagues keep their own settings.": "Only for you. Colleagues keep their own settings.",
1522
1506
  "Sections of the settings": "Sections of the settings",
@@ -2254,10 +2238,6 @@
2254
2238
  "from {from}": "from {from}",
2255
2239
  "up to {to}": "up to {to}",
2256
2240
  "this week": "this week",
2257
- "within %n day": [
2258
- "within %n day",
2259
- "within %n days"
2260
- ],
2261
2241
  "Search term: {term}": "Search term: {term}",
2262
2242
  "New contact": "New contact",
2263
2243
  "Template": "Template",
@@ -2578,6 +2558,26 @@
2578
2558
  "%n minute": [
2579
2559
  "%n minute",
2580
2560
  "%n minutes"
2561
+ ],
2562
+ "%n page: {pages}": [
2563
+ "%n page: {pages}",
2564
+ "%n pages: {pages}"
2565
+ ],
2566
+ "%n page, including {pages}": [
2567
+ "%n page, including {pages}",
2568
+ "%n pages, including {pages}"
2569
+ ],
2570
+ "{on} of %n is in the menu": [
2571
+ "{on} of %n is in the menu",
2572
+ "{on} of %n are in the menu"
2573
+ ],
2574
+ "%n feature": [
2575
+ "%n feature",
2576
+ "%n features"
2577
+ ],
2578
+ "within %n day": [
2579
+ "within %n day",
2580
+ "within %n days"
2581
2581
  ]
2582
2582
  }
2583
2583
  }
package/l10n/nl.json CHANGED
@@ -56,7 +56,7 @@
56
56
  "Save sharing": "Delen opslaan",
57
57
  "Failed to save sharing": "Delen opslaan is mislukt",
58
58
  "My views": "Mijn weergaven",
59
- "Shared with me": "Met mij gedeeld",
59
+ "Shared with me": "Gedeeld met mij",
60
60
  "Share \"{name}\"": "\"{name}\" delen",
61
61
  "Save the current view to \"{name}\"": "Huidige weergave opslaan in \"{name}\"",
62
62
  "Save \"{name}\" as my view": "\"{name}\" opslaan als mijn weergave",
@@ -1671,18 +1671,6 @@
1671
1671
  "Pages": "Pagina's",
1672
1672
  "Open {module}": "{module} openen",
1673
1673
  "{list} and {last}": "{list} en {last}",
1674
- "%n page: {pages}": [
1675
- "%n pagina: {pages}",
1676
- "%n pagina's: {pages}"
1677
- ],
1678
- "%n page, including {pages}": [
1679
- "%n pagina, waaronder {pages}",
1680
- "%n pagina's, waaronder {pages}"
1681
- ],
1682
- "{on} of %n is in the menu": [
1683
- "{on} van %n staat in het menu",
1684
- "{on} van %n staan in het menu"
1685
- ],
1686
1674
  "Put it in the menu:": "In het menu zetten:",
1687
1675
  "Support this app": "Steun deze app",
1688
1676
  "No features with this status": "Geen functies met deze status",
@@ -1699,10 +1687,6 @@
1699
1687
  "{status}, {expected}": "{status}, {expected}",
1700
1688
  "Build on this app": "Bouw verder op deze app",
1701
1689
  "AI skills": "AI-vaardigheden",
1702
- "%n feature": [
1703
- "%n functie",
1704
- "%n functies"
1705
- ],
1706
1690
  "Your account": "Uw account",
1707
1691
  "Only for you. Colleagues keep their own settings.": "Alleen voor jou. Collega's houden hun eigen instellingen.",
1708
1692
  "Sections of the settings": "Onderdelen van de instellingen",
@@ -2504,10 +2488,6 @@
2504
2488
  "from {from}": "vanaf {from}",
2505
2489
  "up to {to}": "tot en met {to}",
2506
2490
  "this week": "deze week",
2507
- "within %n day": [
2508
- "binnen %n dag",
2509
- "binnen %n dagen"
2510
- ],
2511
2491
  "Search term: {term}": "Zoekterm: {term}",
2512
2492
  "New contact": "Nieuw contact",
2513
2493
  "Template": "Sjabloon",
@@ -2845,6 +2825,26 @@
2845
2825
  "%n minute": [
2846
2826
  "%n minuut",
2847
2827
  "%n minuten"
2828
+ ],
2829
+ "%n page: {pages}": [
2830
+ "%n pagina: {pages}",
2831
+ "%n pagina's: {pages}"
2832
+ ],
2833
+ "%n page, including {pages}": [
2834
+ "%n pagina, waaronder {pages}",
2835
+ "%n pagina's, waaronder {pages}"
2836
+ ],
2837
+ "{on} of %n is in the menu": [
2838
+ "{on} van %n staat in het menu",
2839
+ "{on} van %n staan in het menu"
2840
+ ],
2841
+ "%n feature": [
2842
+ "%n functie",
2843
+ "%n functies"
2844
+ ],
2845
+ "within %n day": [
2846
+ "binnen %n dag",
2847
+ "binnen %n dagen"
2848
2848
  ]
2849
2849
  }
2850
2850
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@conduction/nextcloud-vue",
3
- "version": "2.135.0",
3
+ "version": "2.136.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": {
@@ -14,7 +14,9 @@
14
14
  <div v-if="!done" class="cn-action-dialog">
15
15
  <!-- The record the action is about: its title, a meta line, a badge. -->
16
16
  <div v-if="summary" class="cn-action-dialog__summary" data-testid="cn-action-dialog-summary">
17
- <CnIcon :name="config.summary.icon || 'FolderOutline'" :size="20" class="cn-action-dialog__summary-icon" />
17
+ <span class="cn-action-dialog__summary-icon">
18
+ <CnIcon :name="config.summary.icon || 'FolderOutline'" :size="20" />
19
+ </span>
18
20
  <span class="cn-action-dialog__summary-text">
19
21
  <strong>{{ summary.title }}</strong>
20
22
  <span v-if="summary.meta" class="cn-action-dialog__muted">{{ summary.meta }}</span>
@@ -142,6 +144,9 @@
142
144
  :type="noteType"
143
145
  :heading="text(config.note.title)"
144
146
  data-testid="cn-action-dialog-note">
147
+ <template v-if="config.note.icon" #icon>
148
+ <CnIcon :name="config.note.icon" :size="20" />
149
+ </template>
145
150
  {{ text(config.note.text) }}
146
151
  </NcNoteCard>
147
152
 
@@ -239,7 +244,8 @@ let reasonSeq = 0
239
244
  * - `reason`: `{ label?, hint?, required?, field?, rows? }`: a text area; required by
240
245
  * default; sent as `field` (default `reason`). `rows` is its height in lines
241
246
  * (default 2; board DqBesluit draws 4).
242
- * - `note`: `{ variant?, title?, text }`: a read-only note.
247
+ * - `note`: `{ variant?, title?, text, icon? }`: a read-only note; `icon` an
248
+ * icon name in place of the variant's own ("LockOutline").
243
249
  * - `confirm`: `{ label, icon?, request?: { method?, url, body? }, action? }`.
244
250
  * `request` posts itself (`body` templates filled with the record and
245
251
  * `{reason}`); `action` dispatches a manifest action through the page.
@@ -808,10 +814,32 @@ export default {
808
814
  .cn-action-dialog__summary {
809
815
  display: flex;
810
816
  align-items: center;
811
- gap: 10px;
817
+ gap: 12px;
812
818
  padding: 12px 14px;
813
819
  border: 1px solid var(--color-border);
814
- border-radius: var(--border-radius-large, 8px);
820
+ border-radius: var(--border-radius-large, 10px);
821
+ }
822
+
823
+ /* The record's icon on a tinted tile (DqArchiveren). */
824
+ .cn-action-dialog__summary-icon {
825
+ display: flex;
826
+ flex: none;
827
+ align-items: center;
828
+ justify-content: center;
829
+ width: 36px;
830
+ height: 36px;
831
+ border-radius: 8px;
832
+ background: var(--color-primary-element-light);
833
+ color: var(--color-primary-element-light-text);
834
+ }
835
+
836
+ .cn-action-dialog__summary-text strong {
837
+ font-size: 15px;
838
+ font-weight: 600;
839
+ }
840
+
841
+ .cn-action-dialog__summary-text .cn-action-dialog__muted {
842
+ font-size: 13px;
815
843
  }
816
844
 
817
845
  .cn-action-dialog__summary-text {
@@ -853,13 +881,26 @@ export default {
853
881
  border-bottom: 1px solid var(--color-border);
854
882
  }
855
883
 
884
+ /* The server draws every dt as a 130px right-aligned inline block and pads
885
+ dt and dd 12px; a fact is a label over its value. */
886
+ .cn-action-dialog__facts dt,
887
+ .cn-action-dialog__facts dd {
888
+ display: block;
889
+ width: auto;
890
+ white-space: normal;
891
+ text-align: start;
892
+ }
893
+
856
894
  .cn-action-dialog__facts dt {
895
+ margin: 0;
896
+ padding: 0;
857
897
  color: var(--color-text-maxcontrast);
858
898
  font-size: 13px;
859
899
  }
860
900
 
861
901
  .cn-action-dialog__facts dd {
862
902
  margin: 0;
903
+ padding: 0;
863
904
  font-size: 15px;
864
905
  font-weight: 500;
865
906
  }
@@ -47,6 +47,9 @@
47
47
  </p>
48
48
  </li>
49
49
  </ul>
50
+ <p v-if="footnote" class="cn-audit-card__footnote" data-testid="cn-audit-trail-widget-footnote">
51
+ {{ footnote }}
52
+ </p>
50
53
  <template v-if="shownEntries.length > maxDisplay" #footer>
51
54
  <button class="cn-audit-card__show-all" @click="$emit('show-all')">
52
55
  {{ showAllLabel }} ({{ shownEntries.length }})
@@ -93,6 +96,12 @@ export default {
93
96
  components: { CnDetailCard, NcLoadingIcon },
94
97
 
95
98
  props: {
99
+ /**
100
+ * A muted line under the trail, inside the card ("Kenmerk ZT-OMV-REG").
101
+ *
102
+ * @spec openspec/changes/audit-trail-footnote/specs/audit-trail/spec.md#requirement-a-history-card-can-end-in-a-line-about-the-record
103
+ */
104
+ footnote: { type: String, default: '' },
96
105
  /** OpenRegister register id (slug or uuid). Optional for `scope: "app"`. */
97
106
  register: { type: String, default: '' },
98
107
  /** OpenRegister schema id (slug or uuid). Optional for `scope: "app"`. */
@@ -523,6 +532,12 @@ export default {
523
532
  padding: 12px 0;
524
533
  }
525
534
 
535
+ .cn-audit-card__footnote {
536
+ margin: 12px 0 0;
537
+ color: var(--color-text-maxcontrast);
538
+ font-size: 13px;
539
+ }
540
+
526
541
  .cn-audit-card__show-all {
527
542
  background: none;
528
543
  border: none;
@@ -20,10 +20,8 @@
20
20
  :actionLabels="actionLabels"
21
21
  :valueLabels="valueLabels"
22
22
  :relatedSources="relatedSources"
23
+ :footnote="footnote"
23
24
  v-bind="emptyText ? { noEntriesLabel: emptyText } : {}" />
24
- <p v-if="footnote" class="cn-audit-trail-widget__footnote" data-testid="cn-audit-trail-widget-footnote">
25
- {{ footnote }}
26
- </p>
27
25
  </div>
28
26
  </template>
29
27
 
@@ -283,14 +281,8 @@ export default {
283
281
  </script>
284
282
 
285
283
  <style scoped>
286
- /* The card keeps its own box; the wrapper only adds the line under it. */
284
+ /* The card keeps its own box, footnote included. */
287
285
  .cn-audit-trail-widget {
288
286
  display: contents;
289
287
  }
290
-
291
- .cn-audit-trail-widget__footnote {
292
- margin: 0;
293
- font-size: 13px;
294
- color: var(--color-text-maxcontrast);
295
- }
296
288
  </style>
@@ -209,6 +209,7 @@ export default {
209
209
  | `filterable` | Boolean | `false` | Show a filter button in every header whose column can filter: enum, boolean, text, number, date and reference columns backed by a schema property. A column opts out with `filterable: false`. Applying a filter emits `column-filter`; the host owns the state and passes it back as `activeFilters`. `CnIndexPage` turns it on by default. |
210
210
  | `activeFilters` | Object | `{}` | The active filter map, `{ paramKey: values[] }`: the same map the facet sidebar writes, so a header filter and a sidebar filter on one field show the same state. |
211
211
  | `filterSchema` | Object | `null` | The schema the header filters read instead of `schema`, for a page that filters through other fields than the rows' own (a named source's search fields). Null: the header filters read `schema`. |
212
+ | `filterChips` | Boolean | `true` | Draw the active header filters as removable chips above the table. A host that lists them itself (the index page's board Active line) turns this off, so a filter is not shown twice. |
212
213
  | `filterRegister` | String | `''` | Register slug a reference column's filter searches in when the column names none. |
213
214
  | `lensReasonTexts` | Object \| null | `null` | App wording for an unavailable personal lens in self-fetch mode, keyed `<lens>.<reason>` or `<reason>`. Without it the library's text is used (see below). |
214
215
 
@@ -24,7 +24,7 @@
24
24
 
25
25
  <!-- Active header filters, one removable chip each. -->
26
26
  <ul
27
- v-if="!isLoading && activeColumnFilterChips.length > 0"
27
+ v-if="filterChips && !isLoading && activeColumnFilterChips.length > 0"
28
28
  class="cn-data-table__filter-chips"
29
29
  :aria-label="activeFiltersLabel"
30
30
  data-testid="cn-table-filter-chips">
@@ -1081,6 +1081,18 @@ export default {
1081
1081
  default: null,
1082
1082
  },
1083
1083
 
1084
+ /**
1085
+ * Draw the active header filters as removable chips above the table.
1086
+ * A host that lists them itself (the index page's board Active line)
1087
+ * turns this off, so a filter is not shown twice.
1088
+ *
1089
+ * @spec openspec/changes/index-table-filter-chips-once/specs/cn-data-table/spec.md#requirement-a-host-that-lists-the-active-filters-itself-turns-the-table-chips-off
1090
+ */
1091
+ filterChips: {
1092
+ type: Boolean,
1093
+ default: true,
1094
+ },
1095
+
1084
1096
  /**
1085
1097
  * The active filter map, `{ paramKey: values[] }`: the same map the
1086
1098
  * facet sidebar writes, so a header filter and a sidebar filter on one
@@ -372,6 +372,7 @@ The header carries the shared [CnActionsMenu](./cn-actions-menu.md) overflow (Re
372
372
  | `formSize` (`form-size`) | String | `'normal'` | NcDialog size for this page's create and edit form dialogs (`'small'`, `'normal'`, `'large'`). Set from the manifest as `config.formSize`. Before 2.44.0 the page passed no size, so both forms sat at `normal` however many properties the schema declared, while a manifest `open-form` header action could already ask for `large`. |
373
373
  | `formColumns` (`form-columns`) | Number | `1` | How many columns the create and edit forms flow their fields into (`1` or `2`). Pair `2` with `formSize: 'large'`, or the two columns are merely two narrow ones. Textareas and JSON editors still span both, and the layout collapses back to one column below 700px. Set from the manifest as `config.formColumns`. |
374
374
  | `editLabel` | String | `''` | Label for the header Edit button. Defaults to a translated "Edit". |
375
+ | `editTitle` | String | `''` | Title of the record edit form, run through the host translate. Empty uses the Edit button's label. |
375
376
  | `editSubtitle` | String | `''` | One line under the edit form's title, run through the host translate. Empty shows none. |
376
377
  | `showRequestFeature` | Boolean | `true` | Show the Request-a-feature item. Emits `@request-feature`; the default opens the forge's feature-request issue form (repo and forge injected by CnAppRoot) in a new tab, pre-filled with the page title and `surface: "detail:<id>"`. |
377
378
  | `documentationUrl` | String | `''` | When set, adds a **Documentation** entry opening the link in a new tab. Empty hides it. |
@@ -1324,7 +1324,7 @@
1324
1324
  :schema="currentSchema"
1325
1325
  :item="currentObject"
1326
1326
  :register="register"
1327
- :dialogTitle="editActionLabel"
1327
+ :dialogTitle="editFormTitle"
1328
1328
  :subtitle="editFormSubtitle"
1329
1329
  :excludeFields="excludeFields"
1330
1330
  :includeFields="includeFields"
@@ -2454,6 +2454,12 @@ export default {
2454
2454
  default: '',
2455
2455
  },
2456
2456
 
2457
+ /** Title of the record edit form, run through the host translate. Empty uses the Edit button's label. */
2458
+ editTitle: {
2459
+ type: String,
2460
+ default: '',
2461
+ },
2462
+
2457
2463
  /** One line under the edit form's title, run through the host translate. Empty shows none. */
2458
2464
  editSubtitle: {
2459
2465
  type: String,
@@ -4790,6 +4796,11 @@ export default {
4790
4796
  return this.editLabel ? this.effectiveTranslate(this.editLabel) : t('nextcloud-vue', 'Edit')
4791
4797
  },
4792
4798
 
4799
+ /** @return {string} The edit form's title, translated; the Edit button's label when none is configured. */
4800
+ editFormTitle() {
4801
+ return this.editTitle ? this.effectiveTranslate(this.editTitle) : this.editActionLabel
4802
+ },
4803
+
4793
4804
  /** @return {string} The edit form's subtitle, translated; empty when none is configured. */
4794
4805
  editFormSubtitle() {
4795
4806
  return this.editSubtitle ? this.effectiveTranslate(this.editSubtitle) : ''
@@ -871,6 +871,7 @@
871
871
  <CnDataTable
872
872
  v-else-if="currentViewMode === 'table'"
873
873
  :schema="effectiveSchema"
874
+ :filterChips="!isBoardLook"
874
875
  :columns="renderedColumns"
875
876
  :pinnedCount="pinnedColumnCount"
876
877
  :rowIcon="rowIcon"
@@ -10863,7 +10864,8 @@ export default {
10863
10864
  /**
10864
10865
  * A view's state as the edit dialog shows it under a criteria choice:
10865
10866
  * its filters, search term and sort in sentences, read against the
10866
- * page schema's titles.
10867
+ * page schema's titles and the page's `filterLabels` (a `hidden`
10868
+ * filter is left out).
10867
10869
  *
10868
10870
  * @param {object} state A view state (`extractViewState` or `currentViewState`).
10869
10871
  * @return {string} The sentences.
@@ -10873,6 +10875,7 @@ export default {
10873
10875
  const schema = this.effectiveSchema
10874
10876
  return viewCriteriaSummary(state, {
10875
10877
  properties: (schema && schema.properties) || {},
10878
+ filterLabels: this.filterLabels || {},
10876
10879
  translate: (text) => this.cnTranslate(text),
10877
10880
  })
10878
10881
  },
@@ -6,7 +6,7 @@
6
6
  <template>
7
7
  <div
8
8
  class="cn-record-list"
9
- :class="[`cn-record-list--${presentation}`, { 'cn-record-list--compact': content.density === 'compact', 'cn-record-list--tiles': presentation === 'rows' && content.tiles === true, 'cn-record-list--rule-above': content.ruleAbove === true }]"
9
+ :class="[`cn-record-list--${presentation}`, { 'cn-record-list--compact': content.density === 'compact', 'cn-record-list--tiles': presentation === 'rows' && content.tiles === true, 'cn-record-list--rule-above': content.ruleAbove === true, 'cn-record-list--data-table': presentation === 'table' && content.tableLook === 'data' }]"
10
10
  data-testid="cn-record-list">
11
11
  <p v-if="intro && !absent" class="cn-record-list__intro" data-testid="cn-record-list-intro">
12
12
  {{ intro }}
@@ -448,7 +448,21 @@
448
448
  data-testid="cn-record-list-action-error">
449
449
  {{ actionError }}
450
450
  </p>
451
- <p v-if="footnote && !error && !absent && (rows.length > 0 || footnoteFromAnswer)" class="cn-record-list__footnote" data-testid="cn-record-list-footnote">
451
+ <div
452
+ v-if="footnoteTone && footnote && !error && !absent && (rows.length > 0 || footnoteFromAnswer)"
453
+ class="cn-record-list__notice cn-record-list__notice--footnote"
454
+ :class="`cn-record-list__notice--${footnoteTone}`"
455
+ data-testid="cn-record-list-footnote">
456
+ <CnIcon
457
+ :name="footnoteTone === 'info' ? 'InformationOutline' : 'AlertOutline'"
458
+ :size="16"
459
+ class="cn-record-list__notice-icon"
460
+ aria-hidden="true" />
461
+ <p class="cn-record-list__notice-text">
462
+ {{ footnote }}
463
+ </p>
464
+ </div>
465
+ <p v-else-if="footnote && !error && !absent && (rows.length > 0 || footnoteFromAnswer)" class="cn-record-list__footnote" data-testid="cn-record-list-footnote">
452
466
  {{ footnote }}
453
467
  </p>
454
468
  <!-- `footerLink`: a text link under the rows ("Naar Mijn werk"). -->
@@ -685,6 +699,11 @@ export default {
685
699
  * `pill.cases` (`[{ when, label, variant? }]`) labels a row by the
686
700
  * first case whose `when` holds on it, else by the pill's own `label`
687
701
  * and `variant` (Concept, Vastgezet, Actief).
702
+ * `footnoteVariant` (`info`, `warning` or `error`) draws the footnote
703
+ * as a toned notice box with its icon (DqZaaktype opt-out note).
704
+ * `tableLook: "data"` marks a `table` as a data table, which the board
705
+ * look draws as its card table (header band, 40px rows, hairlines)
706
+ * instead of the compact grid (DqZaaktype "Berichten aan de aanvrager").
688
707
  * `groupBy` (a field path) draws the rows in groups under a heading
689
708
  * per value, headed by the field's `valueLabels` entry; `groupOrder`
690
709
  * (values) orders the groups and `groupEmptyLabel` heads the rows
@@ -1212,6 +1231,19 @@ export default {
1212
1231
  return typeof note === 'string' && note !== '' ? this.translate(note) : ''
1213
1232
  },
1214
1233
 
1234
+ /**
1235
+ * The tone the footnote is drawn in as a notice box
1236
+ * (`content.footnoteVariant`: info, warning or error); '' for a plain
1237
+ * muted line.
1238
+ *
1239
+ * @return {string} The tone, or ''.
1240
+ * @spec openspec/changes/record-list-data-table-and-notice-footnote/specs/record-list/spec.md#requirement-a-record-list-footnote-can-be-a-toned-notice
1241
+ */
1242
+ footnoteTone() {
1243
+ const tone = this.content?.footnoteVariant
1244
+ return NOTICE_VARIANTS.includes(tone) ? tone : ''
1245
+ },
1246
+
1215
1247
  /**
1216
1248
  * Whether the footnote reads the answer: it then shows with no rows too.
1217
1249
  *
@@ -2938,6 +2970,10 @@ export default {
2938
2970
  color: var(--color-error-text, var(--color-main-text));
2939
2971
  }
2940
2972
 
2973
+ .cn-record-list__notice--footnote {
2974
+ margin-top: 12px;
2975
+ }
2976
+
2941
2977
  .cn-record-list__notice-icon {
2942
2978
  flex: 0 0 auto;
2943
2979
  margin-top: 1px;
@@ -701,13 +701,20 @@ export default {
701
701
  width: 1px;
702
702
  }
703
703
 
704
+ /* A theme's paragraph rule sets every p's size and colour with !important
705
+ (thematiq), so the note and the footnote also set its tokens.
706
+ @spec openspec/changes/calendar-footnote-paragraph-tokens/specs/working-calendar-footnote/spec.md */
704
707
  .cn-working-calendar__note {
708
+ --nldesign-component-paragraph-color: var(--color-text-maxcontrast);
705
709
  color: var(--color-text-maxcontrast);
706
710
  margin: 0;
707
711
  }
708
712
 
709
713
  /* @spec openspec/changes/working-calendar-footnote/specs/working-calendar-footnote/spec.md */
710
714
  .cn-working-calendar__footnote {
715
+ --nldesign-component-paragraph-font-size: 13px;
716
+ --nldesign-component-paragraph-line-height: 1.3;
717
+ --nldesign-component-paragraph-color: var(--color-text-maxcontrast);
711
718
  margin: 0;
712
719
  font-size: 13px;
713
720
  line-height: 1.3;
@@ -1401,6 +1401,32 @@ function searchSource(config = {}, context = {}) {
1401
1401
  }
1402
1402
  }
1403
1403
 
1404
+ /**
1405
+ * The members that match a search, in the asked order, and the page of them
1406
+ * the pager asks for.
1407
+ *
1408
+ * @param {Array<object>} members The members, by name.
1409
+ * @param {object} config `search`, `sortKey`, `sortOrder`, `limit`, `offset`.
1410
+ * @return {{rows: Array<object>, total: number}} The page and how many match.
1411
+ */
1412
+ function membersPage(members, config = {}) {
1413
+ const term = String(config.search || '').trim().toLowerCase()
1414
+ let rows = term === ''
1415
+ ? members.slice()
1416
+ : members.filter((member) => ['displayname', 'function', 'roles', 'email'].some((field) => String(member[field] || '').toLowerCase().includes(term)))
1417
+ if (config.sortKey) {
1418
+ const sign = config.sortOrder === 'desc' ? -1 : 1
1419
+ rows = rows.sort((a, b) => sign * String(a[config.sortKey] ?? '').localeCompare(String(b[config.sortKey] ?? '')))
1420
+ }
1421
+ const total = rows.length
1422
+ const limit = Number(config.limit) || 0
1423
+ if (limit > 0) {
1424
+ const offset = Number(config.offset) || 0
1425
+ rows = rows.slice(offset, offset + limit)
1426
+ }
1427
+ return { rows, total }
1428
+ }
1429
+
1404
1430
  /**
1405
1431
  * The users source: the members of a Nextcloud group, with their function,
1406
1432
  * roles, e-mail and whether they are in today (`entitySource: "users"`,
@@ -1417,12 +1443,20 @@ function searchSource(config = {}, context = {}) {
1417
1443
  * Listing a group needs an admin or the group's sub-admin; a refused request
1418
1444
  * leaves the list empty.
1419
1445
  *
1446
+ * The page gets a pager (`pages`), and the search term and the header's order
1447
+ * come as `search` and `sortKey` / `sortOrder`: the group's members are read
1448
+ * once and searched, sorted and paged here, so a new page, term or order does
1449
+ * not read every member's presence again.
1450
+ *
1420
1451
  * @return {object} The source adapter.
1421
1452
  * @spec openspec/changes/index-groups-users-source/specs/index-page/spec.md#requirement-an-index-page-can-list-nextcloud-groups-and-their-members
1422
1453
  */
1423
1454
  function usersSource() {
1424
- const state = reactive({ rows: [], loading: false })
1455
+ const state = reactive({ rows: [], loading: false, total: 0 })
1425
1456
  let seq = 0
1457
+ // The members last read, for the group and request they were read for.
1458
+ let readMembers = []
1459
+ let membersKey = null
1426
1460
  const presenceLabels = {
1427
1461
  present: t('nextcloud-vue', 'Present'),
1428
1462
  away: t('nextcloud-vue', 'Away'),
@@ -1441,6 +1475,15 @@ function usersSource() {
1441
1475
  */
1442
1476
  load: async (config) => {
1443
1477
  const group = String((config && (config.group || config.defaultGroup)) || '')
1478
+ const { search, sortKey, sortOrder, limit, offset, ...request } = config || {}
1479
+ const key = JSON.stringify({ ...request, group })
1480
+ // A new page, term or order of the members already read: no request.
1481
+ if (key === membersKey && (offset !== undefined || search !== undefined || sortKey !== undefined)) {
1482
+ const page = membersPage(readMembers, config)
1483
+ state.rows = page.rows
1484
+ state.total = page.total
1485
+ return
1486
+ }
1444
1487
  const mine = ++seq
1445
1488
  state.loading = true
1446
1489
  try {
@@ -1475,11 +1518,18 @@ function usersSource() {
1475
1518
  }
1476
1519
  }))
1477
1520
  if (mine === seq) {
1478
- state.rows = rows.sort((a, b) => a.displayname.localeCompare(b.displayname))
1521
+ readMembers = rows.sort((a, b) => a.displayname.localeCompare(b.displayname))
1522
+ membersKey = key
1523
+ const page = membersPage(readMembers, config)
1524
+ state.rows = page.rows
1525
+ state.total = page.total
1479
1526
  }
1480
1527
  } catch (error) {
1481
1528
  if (mine === seq) {
1529
+ readMembers = []
1530
+ membersKey = null
1482
1531
  state.rows = []
1532
+ state.total = 0
1483
1533
  }
1484
1534
  throw error
1485
1535
  } finally {
@@ -1490,10 +1540,26 @@ function usersSource() {
1490
1540
  },
1491
1541
 
1492
1542
  /**
1493
- * @return {Array<object>} The members.
1543
+ * @return {Array<object>} The members on this page.
1494
1544
  */
1495
1545
  rows: () => state.rows,
1496
1546
 
1547
+ pages: true,
1548
+
1549
+ /**
1550
+ * @return {number} The members that match, on every page.
1551
+ */
1552
+ total: () => state.total,
1553
+
1554
+ searchParam: 'search',
1555
+
1556
+ /**
1557
+ * @param {string} key The column.
1558
+ * @param {string} order `asc` or `desc`.
1559
+ * @return {object} The load arguments.
1560
+ */
1561
+ sortParam: (key, order) => ({ sortKey: key, sortOrder: order }),
1562
+
1497
1563
  /**
1498
1564
  * @return {boolean} Whether a load is in flight.
1499
1565
  */