@conduction/nextcloud-vue 2.135.0 → 2.137.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 (123) 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/CnAdminSettingsShell/CnAdminSettingsShell.vue.js +1 -1
  7. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue2.js +9 -0
  8. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue2.js.map +1 -1
  9. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue3.js +4 -3
  10. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue3.js.map +1 -1
  11. package/dist/esm/components/CnAppNav/CnAppNav.vue2.js +4 -1
  12. package/dist/esm/components/CnAppNav/CnAppNav.vue2.js.map +1 -1
  13. package/dist/esm/components/CnAppNav/CnAppNav.vue3.js +61 -28
  14. package/dist/esm/components/CnAppNav/CnAppNav.vue3.js.map +1 -1
  15. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue.js +1 -1
  16. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue2.js +6 -0
  17. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue2.js.map +1 -1
  18. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue3.js +9 -1
  19. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue3.js.map +1 -1
  20. package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue.js +1 -1
  21. package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue2.js.map +1 -1
  22. package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue3.js +4 -11
  23. package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue3.js.map +1 -1
  24. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +32 -0
  25. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
  26. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +34 -26
  27. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
  28. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +24 -0
  29. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  30. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +2 -2
  31. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  32. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue.js +1 -1
  33. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
  34. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js +2 -1
  35. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
  36. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +39 -2
  37. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  38. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +6 -5
  39. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  40. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue.js +1 -1
  41. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js +11 -0
  42. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js.map +1 -1
  43. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue3.js +10 -2
  44. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue3.js.map +1 -1
  45. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
  46. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +18 -0
  47. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
  48. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js +38 -23
  49. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
  50. package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue.js +1 -1
  51. package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue2.js.map +1 -1
  52. package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue3.js +1 -0
  53. package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue3.js.map +1 -1
  54. package/dist/esm/components/CnRetentionWidget/CnRetentionWidget.vue.js +1 -1
  55. package/dist/esm/components/CnRetentionWidget/CnRetentionWidget.vue2.js.map +1 -1
  56. package/dist/esm/components/CnRetentionWidget/CnRetentionWidget.vue3.js.map +1 -1
  57. package/dist/esm/components/CnSavedViewMenuSettings/CnSavedViewMenuSettings.vue.js +1 -1
  58. package/dist/esm/components/CnSavedViewMenuSettings/CnSavedViewMenuSettings.vue2.js +14 -4
  59. package/dist/esm/components/CnSavedViewMenuSettings/CnSavedViewMenuSettings.vue2.js.map +1 -1
  60. package/dist/esm/components/CnSavedViewMenuSettings/CnSavedViewMenuSettings.vue3.js.map +1 -1
  61. package/dist/esm/components/CnTimelineWidget/CnTimelineManage.vue.js +1 -1
  62. package/dist/esm/components/CnTimelineWidget/CnTimelineManage.vue2.js +10 -0
  63. package/dist/esm/components/CnTimelineWidget/CnTimelineManage.vue2.js.map +1 -1
  64. package/dist/esm/components/CnTimelineWidget/CnTimelineManage.vue3.js +29 -13
  65. package/dist/esm/components/CnTimelineWidget/CnTimelineManage.vue3.js.map +1 -1
  66. package/dist/esm/components/CnTimelineWidget/dashboardRegistration.js +3 -1
  67. package/dist/esm/components/CnTimelineWidget/dashboardRegistration.js.map +1 -1
  68. package/dist/esm/components/CnWorkingCalendarWidget/CnWorkingCalendarWidget.vue.js +1 -1
  69. package/dist/esm/components/CnWorkingCalendarWidget/CnWorkingCalendarWidget.vue2.js.map +1 -1
  70. package/dist/esm/components/CnWorkingCalendarWidget/CnWorkingCalendarWidget.vue3.js.map +1 -1
  71. package/dist/esm/composables/indexSources.js +69 -3
  72. package/dist/esm/composables/indexSources.js.map +1 -1
  73. package/dist/esm/l10n/en.json.js +20 -20
  74. package/dist/esm/l10n/nl.json.js +21 -21
  75. package/dist/esm/library-style.js +1 -1
  76. package/dist/esm/nextcloud-vue.css +580 -324
  77. package/dist/esm/utils/contentHeaderLink.js +7 -1
  78. package/dist/esm/utils/contentHeaderLink.js.map +1 -1
  79. package/dist/esm/utils/savedViewMenu.js +4 -1
  80. package/dist/esm/utils/savedViewMenu.js.map +1 -1
  81. package/dist/esm/utils/savedViewSummary.js +11 -4
  82. package/dist/esm/utils/savedViewSummary.js.map +1 -1
  83. package/dist/esm/utils/validateManifestV2.compiled.js +2 -2
  84. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  85. package/dist/esm/utils/widgetDispatch.js +3 -1
  86. package/dist/esm/utils/widgetDispatch.js.map +1 -1
  87. package/dist/nextcloud-vue.cjs.js +1159 -812
  88. package/dist/nextcloud-vue.cjs.js.map +2 -2
  89. package/dist/nextcloud-vue.css +580 -324
  90. package/l10n/en.json +20 -20
  91. package/l10n/nl.json +21 -21
  92. package/package.json +1 -1
  93. package/src/components/CnActionDialog/CnActionDialog.vue +45 -4
  94. package/src/components/CnAdminSettingsShell/CnAdminSettingsShell.vue +13 -1
  95. package/src/components/CnAppNav/CnAppNav.vue +28 -3
  96. package/src/components/CnAuditTrailCard/CnAuditTrailCard.vue +15 -0
  97. package/src/components/CnAuditTrailWidget/CnAuditTrailWidget.vue +2 -10
  98. package/src/components/CnDataTable/CnDataTable.md +1 -0
  99. package/src/components/CnDataTable/CnDataTable.vue +38 -1
  100. package/src/components/CnDetailPage/CnDetailPage.md +1 -0
  101. package/src/components/CnDetailPage/CnDetailPage.vue +26 -2
  102. package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +1 -0
  103. package/src/components/CnIndexPage/CnIndexPage.vue +44 -6
  104. package/src/components/CnObjectDataWidget/CnObjectDataWidget.md +1 -0
  105. package/src/components/CnObjectDataWidget/CnObjectDataWidget.vue +21 -0
  106. package/src/components/CnRecordListWidget/CnRecordListWidget.vue +38 -2
  107. package/src/components/CnRegisterMapping/CnRegisterMapping.vue +7 -0
  108. package/src/components/CnRetentionWidget/CnRetentionWidget.vue +13 -0
  109. package/src/components/CnSavedViewMenuSettings/CnSavedViewMenuSettings.vue +14 -4
  110. package/src/components/CnTimelineWidget/CnTimelineManage.vue +23 -0
  111. package/src/components/CnTimelineWidget/dashboardRegistration.js +3 -1
  112. package/src/components/CnWorkingCalendarWidget/CnWorkingCalendarWidget.vue +7 -0
  113. package/src/composables/indexSources.js +69 -3
  114. package/src/css/look-board-detail.css +57 -1
  115. package/src/css/look-board-index.css +76 -1
  116. package/src/css/look-board.css +51 -4
  117. package/src/css/table.css +5 -0
  118. package/src/schemas/app-manifest-v2.schema.json +4 -2
  119. package/src/utils/contentHeaderLink.js +7 -1
  120. package/src/utils/savedViewMenu.js +4 -1
  121. package/src/utils/savedViewSummary.js +11 -4
  122. package/src/utils/validateManifestV2.compiled.js +2 -2
  123. package/src/utils/widgetDispatch.js +3 -1
@@ -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"
@@ -925,7 +926,7 @@
925
926
  <template v-if="hasRowActions" #row-actions="{ row }">
926
927
  <slot name="row-actions" :row="row">
927
928
  <CnRowActions
928
- :actions="rowActionsFor(row)"
929
+ :actions="rowMenuActionsFor(row)"
929
930
  :row="row"
930
931
  :rowLabel="rowTitleFor(row)"
931
932
  @action="onRowAction" />
@@ -1172,7 +1173,7 @@
1172
1173
  <template v-if="hasRowActions || $slots['row-actions']" #row-actions="{ object }">
1173
1174
  <slot name="row-actions" :row="object">
1174
1175
  <CnRowActions
1175
- :actions="rowActionsFor(object)"
1176
+ :actions="rowMenuActionsFor(object)"
1176
1177
  :row="object"
1177
1178
  :rowLabel="rowTitleFor(object)"
1178
1179
  @action="onRowAction" />
@@ -1224,7 +1225,7 @@
1224
1225
  <template v-if="hasRowActions" #card-actions="{ object }">
1225
1226
  <slot name="row-actions" :row="object">
1226
1227
  <CnRowActions
1227
- :actions="rowActionsFor(object)"
1228
+ :actions="rowMenuActionsFor(object)"
1228
1229
  :row="object"
1229
1230
  :rowLabel="rowTitleFor(object)"
1230
1231
  :triggerLabel="isBoardLook ? t('nextcloud-vue', 'More actions for {name}', { name: rowTitleFor(object) }) : ''"
@@ -1236,7 +1237,7 @@
1236
1237
  <!-- Right-click context menu (positioned at cursor via CSS). Same per-row list as the row actions menu, so the two never drift. -->
1237
1238
  <CnContextMenu
1238
1239
  v-model:open="contextMenuOpen"
1239
- :actions="rowActionsFor(contextMenuShownRow)"
1240
+ :actions="rowMenuActionsFor(contextMenuShownRow)"
1240
1241
  :targetItem="contextMenuShownRow"
1241
1242
  @action="onRowAction"
1242
1243
  @close="closeContextMenu" />
@@ -8016,7 +8017,26 @@ export default {
8016
8017
  },
8017
8018
 
8018
8019
  hasRowActions() {
8019
- return this.$slots['row-actions'] || this.mergedActions.length > 0
8020
+ const refs = this.columnActionRefs
8021
+ return this.$slots['row-actions'] || this.mergedActions.some((action) => action && !refs.has(String(action.id || action.label)) && !refs.has(String(action.label)))
8022
+ },
8023
+
8024
+ /**
8025
+ * The row actions an `actions` column already shows as buttons, by the
8026
+ * ids (or labels) its `widgetProps.actions` names. The row menu leaves
8027
+ * them out, so a row does not offer the same action twice (DqPostvak).
8028
+ *
8029
+ * @return {Set<string>}
8030
+ * @spec openspec/changes/column-actions-leave-the-row-menu/specs/column-actions-leave-the-row-menu/spec.md
8031
+ */
8032
+ columnActionRefs() {
8033
+ const refs = new Set()
8034
+ for (const col of Array.isArray(this.columns) ? this.columns : []) {
8035
+ if (col && typeof col === 'object' && col.widget === 'actions' && Array.isArray(col.widgetProps?.actions)) {
8036
+ col.widgetProps.actions.forEach((ref) => refs.add(String(ref)))
8037
+ }
8038
+ }
8039
+ return refs
8020
8040
  },
8021
8041
 
8022
8042
  /**
@@ -10863,7 +10883,8 @@ export default {
10863
10883
  /**
10864
10884
  * A view's state as the edit dialog shows it under a criteria choice:
10865
10885
  * its filters, search term and sort in sentences, read against the
10866
- * page schema's titles.
10886
+ * page schema's titles and the page's `filterLabels` (a `hidden`
10887
+ * filter is left out).
10867
10888
  *
10868
10889
  * @param {object} state A view state (`extractViewState` or `currentViewState`).
10869
10890
  * @return {string} The sentences.
@@ -10873,6 +10894,7 @@ export default {
10873
10894
  const schema = this.effectiveSchema
10874
10895
  return viewCriteriaSummary(state, {
10875
10896
  properties: (schema && schema.properties) || {},
10897
+ filterLabels: this.filterLabels || {},
10876
10898
  translate: (text) => this.cnTranslate(text),
10877
10899
  })
10878
10900
  },
@@ -11861,6 +11883,22 @@ export default {
11861
11883
  )
11862
11884
  },
11863
11885
 
11886
+ /**
11887
+ * The row menu's actions: the row's actions less those an `actions`
11888
+ * column already shows as buttons.
11889
+ *
11890
+ * @param {object} row The row.
11891
+ * @return {Array<object>}
11892
+ * @spec openspec/changes/column-actions-leave-the-row-menu/specs/column-actions-leave-the-row-menu/spec.md
11893
+ */
11894
+ rowMenuActionsFor(row) {
11895
+ const refs = this.columnActionRefs
11896
+ if (refs.size === 0) {
11897
+ return this.rowActionsFor(row)
11898
+ }
11899
+ return this.rowActionsFor(row).filter((action) => !refs.has(String(action.id || action.label)) && !refs.has(String(action.label)))
11900
+ },
11901
+
11864
11902
  /**
11865
11903
  * The row's own name, for the row menu button's accessible name
11866
11904
  * ("Actions for <title>"): the `nameFormatter`, else the
@@ -141,3 +141,4 @@ export default {
141
141
  | `borderless` | Boolean | `false` | Drop the card border and background, for a host that draws its own (a tab panel). |
142
142
  | `flush` | Boolean | `false` | Drop the content padding, for a host that supplies its own inset. |
143
143
  | `chromeless` | Boolean | `false` | Draw no card at all: what a tab panel wants, in one prop instead of three. Implies `show-title="false"`, `borderless` and `flush`, and drops the header divider too. The Save button keeps its header. Forwarded to `CnWidgetWrapper`. |
144
+ | `footnote` | String | `''` | A muted line under the fields, through the host translate (DqZaakContact "Ontvangstbevestiging"). |
@@ -396,6 +396,10 @@
396
396
  </NcButton>
397
397
  </div>
398
398
 
399
+ <p v-if="footnote" class="cn-object-data-widget__footnote" data-testid="cn-object-data-widget-footnote">
400
+ {{ cnTranslate(footnote) }}
401
+ </p>
402
+
399
403
  <!-- Read-only @self metadata, surfaced on demand from the
400
404
  Metadata action item rather than as a permanent page widget. -->
401
405
  <CnObjectMetadataModal
@@ -837,6 +841,17 @@ export default {
837
841
  default: () => t('nextcloud-vue', 'No data available'),
838
842
  },
839
843
 
844
+ /**
845
+ * A muted line under the fields, through the host translate
846
+ * (DqZaakContact "Ontvangstbevestiging").
847
+ *
848
+ * @spec openspec/changes/data-widget-footnote/specs/data-widget-footnote/spec.md#requirement-a-data-card-can-end-with-a-footnote
849
+ */
850
+ footnote: {
851
+ type: String,
852
+ default: '',
853
+ },
854
+
840
855
  /** Label for the Metadata item in the overflow Actions menu. */
841
856
  metadataLabel: {
842
857
  type: String,
@@ -2535,6 +2550,12 @@ export default {
2535
2550
  outline: none;
2536
2551
  }
2537
2552
 
2553
+ .cn-object-data-widget__footnote {
2554
+ margin: calc(var(--default-grid-baseline, 4px) * 3) 0 0;
2555
+ color: var(--color-text-maxcontrast);
2556
+ font-size: 13px;
2557
+ }
2558
+
2538
2559
  .cn-object-data-widget__empty {
2539
2560
  color: var(--color-text-maxcontrast);
2540
2561
  font-style: italic;
@@ -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;
@@ -72,6 +72,7 @@
72
72
  :options="schemaSelectOptions(groupIdx)"
73
73
  :placeholder="labels.selectSchema"
74
74
  :inputLabel="type.label"
75
+ :clearable="false"
75
76
  label="label"
76
77
  trackBy="value"
77
78
  @update:modelValue="handleSchemaChange(groupIdx, type, $event)" />
@@ -1371,6 +1372,12 @@ export default {
1371
1372
  max-width: 50%;
1372
1373
  }
1373
1374
 
1375
+ /* DqBeheerinstellingen: the register select fills its half of the card. */
1376
+ .cn-register-mapping__selects-register :deep(.v-select) {
1377
+ width: 100%;
1378
+ max-width: none;
1379
+ }
1380
+
1374
1381
  .cn-register-mapping__select-grid {
1375
1382
  display: grid;
1376
1383
  grid-template-columns: repeat(var(--cn-register-mapping-columns, 3), minmax(0, 1fr));
@@ -603,13 +603,26 @@ export default {
603
603
  margin: 0;
604
604
  }
605
605
 
606
+ /* The server draws every dt as a 130px right-aligned inline block and pads
607
+ dt and dd 12px; a fact is a label over its value. */
608
+ .cn-retention__fact dt,
609
+ .cn-retention__fact dd {
610
+ display: block;
611
+ width: auto;
612
+ white-space: normal;
613
+ text-align: start;
614
+ }
615
+
606
616
  .cn-retention__fact dt {
617
+ margin: 0;
618
+ padding: 0;
607
619
  color: var(--color-text-maxcontrast);
608
620
  font-size: 13px;
609
621
  }
610
622
 
611
623
  .cn-retention__fact dd {
612
624
  margin: 0;
625
+ padding: 0;
613
626
  font-weight: 600;
614
627
  }
615
628
 
@@ -93,13 +93,23 @@ function loadSchema(register, schema) {
93
93
  /**
94
94
  * The names of referenced records, each asked once.
95
95
  *
96
- * @param {{register: string, schema: string, labelField: string, ids: Array<string>}} ref The reference filter.
96
+ * @param {{register: string, schema: string, labelField: string, matchField?: string, ids: Array<string>}} ref The reference filter; with `matchField` the ids are that field's values.
97
97
  * @return {Promise<{[id: string]: string}>}
98
98
  */
99
99
  async function loadLabels(ref) {
100
- const prefix = `${ref.register}|${ref.schema}|${ref.labelField}|`
100
+ const prefix = `${ref.register}|${ref.schema}|${ref.labelField}|${ref.matchField || ''}|`
101
101
  const missing = ref.ids.filter((id) => !labelCache.has(prefix + id))
102
- if (missing.length > 0) {
102
+ // A value that identifies a record by another field: one read per value.
103
+ if (missing.length > 0 && ref.matchField) {
104
+ for (const value of missing) {
105
+ labelCache.set(prefix + value, getJson(`/apps/openregister/api/objects/${encodeURIComponent(ref.register)}/${encodeURIComponent(ref.schema)}`, { [ref.matchField]: value, _limit: 1 })
106
+ .then((data) => {
107
+ const row = Array.isArray(data && data.results) ? data.results[0] : null
108
+ return row ? pickRefLabel(row, ref.labelField) : null
109
+ })
110
+ .catch(() => null))
111
+ }
112
+ } else if (missing.length > 0) {
103
113
  const request = getJson(`/apps/openregister/api/objects/${encodeURIComponent(ref.register)}/${encodeURIComponent(ref.schema)}`, { _ids: missing.join(','), _limit: missing.length })
104
114
  .then((data) => {
105
115
  const byId = {}
@@ -372,7 +382,7 @@ export default {
372
382
  const fields = fieldsFromSchema(schema, { includeReadOnly: true })
373
383
  const refs = this.candidates
374
384
  .filter((c) => c.page === page.id)
375
- .flatMap((c) => viewFilterReferences(c.state, fields))
385
+ .flatMap((c) => viewFilterReferences(c.state, fields, page.filterLabels))
376
386
  for (const ref of refs) {
377
387
  const labels = await loadLabels({ ...ref, register: ref.register || page.register })
378
388
  if (Object.keys(labels).length === 0) {
@@ -169,6 +169,19 @@
169
169
  </div>
170
170
  </template>
171
171
 
172
+ <div v-if="composerOnly && canManage && composerEnabled"
173
+ class="cn-timeline-manage__header"
174
+ data-testid="cn-timeline-composer-heading">
175
+ <div class="cn-timeline-manage__heading">
176
+ <h3 class="cn-timeline-manage__title">
177
+ {{ composerTitleText }}
178
+ </h3>
179
+ <p class="cn-timeline-manage__subtitle">
180
+ {{ composerSubtitleText }}
181
+ </p>
182
+ </div>
183
+ </div>
184
+
172
185
  <NcNoteCard v-if="composerOnly && error" type="error" data-testid="cn-timeline-error">
173
186
  {{ error }}
174
187
  </NcNoteCard>
@@ -332,6 +345,16 @@ export default {
332
345
  return this.content.subtitle ? this.translate(this.content.subtitle) : t('nextcloud-vue', 'What happened, who did it and when, newest first')
333
346
  },
334
347
 
348
+ /** @return {string} The title of the note form's own card (`composer: "only"`): `content.title`, else Add note. */
349
+ composerTitleText() {
350
+ return this.content.title ? this.translate(this.content.title) : t('nextcloud-vue', 'Add note')
351
+ },
352
+
353
+ /** @return {string} The line under that title: `content.subtitle`, else where the note goes. */
354
+ composerSubtitleText() {
355
+ return this.content.subtitle ? this.translate(this.content.subtitle) : t('nextcloud-vue', 'Goes on the timeline above, internal unless you make it visible')
356
+ },
357
+
335
358
  publicLabel() {
336
359
  return this.content.publicLabel ? this.translate(this.content.publicLabel) : t('nextcloud-vue', 'Visible to the requester')
337
360
  },
@@ -6,7 +6,8 @@
6
6
  * dashboardWidgetRegistry. Like `audit-trail` it is a DETAIL-PAGE surface:
7
7
  * the object comes from the page's object context. It has no config form
8
8
  * yet, so it is placed from the manifest and never offered in the
9
- * Add-widget picker.
9
+ * Add-widget picker. It draws its own title row and asks for the card, so a
10
+ * tab stack or the side column stands it in an untitled card.
10
11
  *
11
12
  * @spec openspec/changes/timeline-widget/specs/timeline-widget/spec.md#requirement-a-timeline-widget-shows-an-objects-dated-events-in-order
12
13
  */
@@ -29,4 +30,5 @@ registerDashboardWidget('timeline', {
29
30
  icon: 'TimelineClockOutline',
30
31
  surfaces: ['detail-page'],
31
32
  ownsTitle: true,
33
+ chrome: 'card',
32
34
  })
@@ -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
  */
@@ -91,7 +91,7 @@
91
91
  }
92
92
 
93
93
  .cn-look-board .cn-detail-page .cn-detail-page__header-actions > .button-vue--secondary:hover,
94
- .cn-look-board .cn-detail-page .cn-detail-page__header-actions .action-item--secondary .action-item__menutoggle:hover {
94
+ .cn-look-board .cn-detail-page .cn-detail-page__header-actions .action-item--secondary:not(.action-item--open) .action-item__menutoggle:hover {
95
95
  background: var(--color-background-hover) !important;
96
96
  }
97
97
 
@@ -1213,6 +1213,16 @@
1213
1213
  font-weight: 500;
1214
1214
  }
1215
1215
 
1216
+ /* "Bewaren voor een procedure" and "Overdracht naar het e-Depot" are 15px/600
1217
+ section titles over their rows, not the inner heading's 1.4em. */
1218
+ .cn-look-board .cn-retention .cn-retention__section > .cn-inner-heading,
1219
+ .cn-look-board .cn-retention .cn-retention__outcome > .cn-inner-heading {
1220
+ margin: 4px 0 0;
1221
+ font-size: 15px;
1222
+ font-weight: 600;
1223
+ line-height: 1.2;
1224
+ }
1225
+
1216
1226
  /* ---- The task table's row button (CnTasksWidget table, DqZaakTaken) ------ */
1217
1227
  /* "Afronden" is a 34px secondary with 13px text 14px in. NcButton pads a
1218
1228
  text-only label 12px plus 4px inside; the doubled class outranks its
@@ -1317,6 +1327,25 @@
1317
1327
  font-size: 15px;
1318
1328
  }
1319
1329
 
1330
+ /* A card with actions holds them at its end, beside the title and meta (the
1331
+ board's "···" on each task). */
1332
+ .cn-look-board .cn-grouped-cards__item.cn-grouped-cards__item:has(> .cn-grouped-cards__item-actions):not(.cn-look-nextcloud *) {
1333
+ display: grid;
1334
+ grid-template-columns: minmax(0, 1fr) auto;
1335
+ column-gap: 12px;
1336
+ align-items: center;
1337
+ }
1338
+
1339
+ .cn-look-board .cn-grouped-cards__item:has(> .cn-grouped-cards__item-actions) > :not(.cn-grouped-cards__item-actions):not(.cn-look-nextcloud *) {
1340
+ grid-column: 1;
1341
+ }
1342
+
1343
+ .cn-look-board .cn-grouped-cards__item:has(> .cn-grouped-cards__item-actions) > .cn-grouped-cards__item-actions:not(.cn-look-nextcloud *) {
1344
+ grid-column: 2;
1345
+ grid-row: 1 / span 3;
1346
+ margin: 0;
1347
+ }
1348
+
1320
1349
  /* ---- A file's side card (CnFilesBrowser) --------------------------------- */
1321
1350
  /* The board's side card: radius 12, 20px 22px of padding, the title a 15px
1322
1351
  muted heading at 600, the facts 8px 16px apart with the values at 500
@@ -1501,3 +1530,30 @@
1501
1530
  border-radius: var(--cn-board-control-radius);
1502
1531
  overflow: hidden;
1503
1532
  }
1533
+
1534
+ /* ---- A record list drawn as a data table (CnRecordListWidget) ----------- */
1535
+ /* `tableLook: "data"` (DqZaaktype "Berichten aan de aanvrager"): the board's
1536
+ card table rather than the compact grid, a near-white header band at
1537
+ 14px/600 in the muted ink, 10px 12px, and 14px cells, 11px 12px, a
1538
+ hairline above each row. Two classes more than the compact grid's rule.
1539
+ @spec openspec/changes/record-list-data-table-and-notice-footnote/specs/record-list/spec.md#requirement-a-record-list-table-can-be-drawn-as-a-data-table */
1540
+ .cn-look-board .cn-record-list.cn-record-list--table.cn-record-list--data-table .cn-record-list__table th:not(.cn-look-nextcloud *) {
1541
+ --nldesign-component-table-header-background-color: var(--cn-board-table-head-bg, color-mix(in srgb, var(--color-background-hover) 40%, var(--color-main-background)));
1542
+ --nldesign-component-table-header-color: var(--color-text-maxcontrast);
1543
+ --nldesign-component-table-header-font-weight: 600;
1544
+ padding: 10px 12px;
1545
+ border-bottom: 0;
1546
+ background: var(--cn-board-table-head-bg, color-mix(in srgb, var(--color-background-hover) 40%, var(--color-main-background)));
1547
+ color: var(--color-text-maxcontrast);
1548
+ font-size: 14px;
1549
+ font-weight: 600;
1550
+ line-height: 1.2;
1551
+ }
1552
+
1553
+ .cn-look-board .cn-record-list.cn-record-list--table.cn-record-list--data-table .cn-record-list__table td:not(.cn-look-nextcloud *) {
1554
+ padding: 11px 12px;
1555
+ border-top: 1px solid var(--cn-board-hairline);
1556
+ border-bottom: 0;
1557
+ font-size: 14px;
1558
+ line-height: 1.2;
1559
+ }