@conduction/nextcloud-vue 2.79.0 → 2.80.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 (197) hide show
  1. package/dist/esm/components/CnActionButtons/CnActionButtons.vue2.js.map +1 -1
  2. package/dist/esm/components/CnActionButtons/CnActionButtons.vue3.js +3 -1
  3. package/dist/esm/components/CnActionButtons/CnActionButtons.vue3.js.map +1 -1
  4. package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js +32 -3
  5. package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js.map +1 -1
  6. package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js +10 -8
  7. package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js.map +1 -1
  8. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js +39 -3
  9. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js.map +1 -1
  10. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue3.js.map +1 -1
  11. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js +79 -1
  12. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js.map +1 -1
  13. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js +133 -96
  14. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js.map +1 -1
  15. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
  16. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +1 -0
  17. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
  18. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +62 -4
  19. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  20. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  21. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js +24 -0
  22. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
  23. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js +43 -4
  24. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
  25. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue.js +11 -0
  26. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue.js.map +1 -0
  27. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js +736 -0
  28. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js.map +1 -0
  29. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js +238 -0
  30. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js.map +1 -0
  31. package/dist/esm/components/CnFactsWidget/index.js +30 -0
  32. package/dist/esm/components/CnFactsWidget/index.js.map +1 -0
  33. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue.js +10 -0
  34. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue.js.map +1 -0
  35. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js +551 -0
  36. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js.map +1 -0
  37. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js +183 -0
  38. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js.map +1 -0
  39. package/dist/esm/components/CnFieldListWidget/index.js +29 -0
  40. package/dist/esm/components/CnFieldListWidget/index.js.map +1 -0
  41. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js +65 -17
  42. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
  43. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js +4 -4
  44. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
  45. package/dist/esm/components/CnFormPage/CnFormPage.vue2.js +18 -3
  46. package/dist/esm/components/CnFormPage/CnFormPage.vue2.js.map +1 -1
  47. package/dist/esm/components/CnFormPage/CnFormPage.vue3.js.map +1 -1
  48. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +305 -72
  49. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  50. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +31 -13
  51. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  52. package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue.js +10 -0
  53. package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue.js.map +1 -0
  54. package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue2.js +99 -0
  55. package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue2.js.map +1 -0
  56. package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue3.js +48 -0
  57. package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue3.js.map +1 -0
  58. package/dist/esm/components/CnPageRenderer/CnPageRenderer.vue2.js +29 -1
  59. package/dist/esm/components/CnPageRenderer/CnPageRenderer.vue2.js.map +1 -1
  60. package/dist/esm/components/CnPageRenderer/CnPageRenderer.vue3.js.map +1 -1
  61. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +10 -0
  62. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js.map +1 -0
  63. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +387 -0
  64. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -0
  65. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js +169 -0
  66. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -0
  67. package/dist/esm/components/CnRecordListWidget/index.js +31 -0
  68. package/dist/esm/components/CnRecordListWidget/index.js.map +1 -0
  69. package/dist/esm/components/CnSectionsWidget/CnSectionsWidget.vue.js +10 -0
  70. package/dist/esm/components/CnSectionsWidget/CnSectionsWidget.vue.js.map +1 -0
  71. package/dist/esm/components/CnSectionsWidget/CnSectionsWidget.vue2.js +351 -0
  72. package/dist/esm/components/CnSectionsWidget/CnSectionsWidget.vue2.js.map +1 -0
  73. package/dist/esm/components/CnSectionsWidget/CnSectionsWidget.vue3.js +71 -0
  74. package/dist/esm/components/CnSectionsWidget/CnSectionsWidget.vue3.js.map +1 -0
  75. package/dist/esm/components/CnSectionsWidget/index.js +35 -0
  76. package/dist/esm/components/CnSectionsWidget/index.js.map +1 -0
  77. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue2.js +81 -2
  78. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue2.js.map +1 -1
  79. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue3.js.map +1 -1
  80. package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js.map +1 -1
  81. package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js +1 -0
  82. package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js.map +1 -1
  83. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +122 -7
  84. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
  85. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js +84 -23
  86. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
  87. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js +4 -0
  88. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js.map +1 -1
  89. package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue2.js +106 -2
  90. package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue2.js.map +1 -1
  91. package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue3.js +105 -53
  92. package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue3.js.map +1 -1
  93. package/dist/esm/composables/useFormDraft.js +140 -9
  94. package/dist/esm/composables/useFormDraft.js.map +1 -1
  95. package/dist/esm/index.js +4 -0
  96. package/dist/esm/index.js.map +1 -1
  97. package/dist/esm/integrations/builtin/field-inspection/CnFieldInspectionCard.vue2.js +28 -20
  98. package/dist/esm/integrations/builtin/field-inspection/CnFieldInspectionCard.vue2.js.map +1 -1
  99. package/dist/esm/integrations/builtin/field-inspection/CnFieldInspectionCard.vue3.js +9 -0
  100. package/dist/esm/integrations/builtin/field-inspection/CnFieldInspectionCard.vue3.js.map +1 -1
  101. package/dist/esm/integrations/builtin/field-inspection.js +21 -0
  102. package/dist/esm/integrations/builtin/field-inspection.js.map +1 -1
  103. package/dist/esm/integrations/offline/checklistSubmission.js +141 -0
  104. package/dist/esm/integrations/offline/checklistSubmission.js.map +1 -0
  105. package/dist/esm/integrations/offline/fieldCollectionHelpers.js +70 -5
  106. package/dist/esm/integrations/offline/fieldCollectionHelpers.js.map +1 -1
  107. package/dist/esm/integrations/offline/index.js +2 -1
  108. package/dist/esm/integrations/offline/index.js.map +1 -1
  109. package/dist/esm/integrations/offline/offlineDb.js +6 -1
  110. package/dist/esm/integrations/offline/offlineDb.js.map +1 -1
  111. package/dist/esm/integrations/offline/syncReplayService.js +8 -2
  112. package/dist/esm/integrations/offline/syncReplayService.js.map +1 -1
  113. package/dist/esm/l10n/en.json.js +12 -1
  114. package/dist/esm/l10n/en.json.js.map +1 -1
  115. package/dist/esm/l10n/nl.json.js +16 -2
  116. package/dist/esm/l10n/nl.json.js.map +1 -1
  117. package/dist/esm/nextcloud-vue.css +1000 -59
  118. package/dist/esm/utils/detailActionModel.js +60 -3
  119. package/dist/esm/utils/detailActionModel.js.map +1 -1
  120. package/dist/esm/utils/fillTemplate.js +114 -0
  121. package/dist/esm/utils/fillTemplate.js.map +1 -0
  122. package/dist/esm/utils/libraryWidgetKeys.js +4 -0
  123. package/dist/esm/utils/libraryWidgetKeys.js.map +1 -1
  124. package/dist/esm/utils/queryPresets.js +96 -0
  125. package/dist/esm/utils/queryPresets.js.map +1 -0
  126. package/dist/esm/utils/resolveFilterTokens.js +11 -0
  127. package/dist/esm/utils/resolveFilterTokens.js.map +1 -1
  128. package/dist/esm/utils/schema.js +21 -0
  129. package/dist/esm/utils/schema.js.map +1 -1
  130. package/dist/esm/utils/sentinelTokens.js +3 -3
  131. package/dist/esm/utils/sentinelTokens.js.map +1 -1
  132. package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
  133. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  134. package/dist/esm/utils/widgetDispatch.js +7 -1
  135. package/dist/esm/utils/widgetDispatch.js.map +1 -1
  136. package/dist/nextcloud-vue.cjs.js +12368 -7717
  137. package/dist/nextcloud-vue.cjs.js.map +2 -2
  138. package/dist/nextcloud-vue.css +1000 -59
  139. package/l10n/en.json +12 -1
  140. package/l10n/nl.json +16 -2
  141. package/package.json +1 -1
  142. package/src/components/CnActionButtons/CnActionButtons.vue +2 -0
  143. package/src/components/CnActionsBar/CnActionsBar.md +4 -0
  144. package/src/components/CnActionsBar/CnActionsBar.vue +42 -10
  145. package/src/components/CnBannerWidget/CnBannerWidget.vue +39 -3
  146. package/src/components/CnCellRenderer/CnCellRenderer.vue +188 -1
  147. package/src/components/CnDataTable/CnDataTable.vue +1 -0
  148. package/src/components/CnDetailPage/CnDetailPage.md +1 -1
  149. package/src/components/CnDetailPage/CnDetailPage.vue +63 -3
  150. package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +66 -0
  151. package/src/components/CnFactsWidget/CnFactsWidget.vue +1050 -0
  152. package/src/components/CnFactsWidget/index.js +29 -0
  153. package/src/components/CnFieldListWidget/CnFieldListWidget.vue +746 -0
  154. package/src/components/CnFieldListWidget/index.js +28 -0
  155. package/src/components/CnFormDialog/CnFormDialog.md +9 -3
  156. package/src/components/CnFormDialog/CnFormDialog.vue +69 -21
  157. package/src/components/CnFormPage/CnFormPage.vue +18 -3
  158. package/src/components/CnIndexPage/CnIndexPage.vue +254 -6
  159. package/src/components/CnIndexPage/CnIndexPageWidgets.vue +169 -0
  160. package/src/components/CnPageRenderer/CnPageRenderer.vue +29 -1
  161. package/src/components/CnRecordListWidget/CnRecordListWidget.vue +603 -0
  162. package/src/components/CnRecordListWidget/index.js +30 -0
  163. package/src/components/CnSectionsWidget/CnSectionsWidget.vue +489 -0
  164. package/src/components/CnSectionsWidget/index.js +34 -0
  165. package/src/components/CnStagesWidget/CnStagesWidget.vue +81 -2
  166. package/src/components/CnStatWidget/CnStatWidget.vue +1 -1
  167. package/src/components/CnTabsWidget/CnTabsWidget.vue +184 -8
  168. package/src/components/CnWidgetGrid/registerDashboardWidgets.js +4 -0
  169. package/src/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue +188 -4
  170. package/src/components/index.js +4 -0
  171. package/src/composables/__tests__/useFormDraft.spec.js +69 -0
  172. package/src/composables/useFormDraft.js +139 -8
  173. package/src/css/form-field.css +14 -0
  174. package/src/css/index-page.css +71 -0
  175. package/src/css/kpi-card.css +32 -0
  176. package/src/css/look-board-detail.css +77 -0
  177. package/src/css/look-board-index.css +51 -0
  178. package/src/css/look-board.css +37 -2
  179. package/src/css/table.css +19 -0
  180. package/src/index.js +4 -0
  181. package/src/integrations/builtin/field-inspection/CnFieldInspectionCard.vue +35 -20
  182. package/src/integrations/builtin/field-inspection.js +21 -0
  183. package/src/integrations/offline/checklistSubmission.js +137 -0
  184. package/src/integrations/offline/fieldCollectionHelpers.js +69 -4
  185. package/src/integrations/offline/index.js +3 -0
  186. package/src/integrations/offline/offlineDb.js +6 -1
  187. package/src/integrations/offline/syncReplayService.js +8 -2
  188. package/src/schemas/app-manifest-v2.schema.json +104 -5
  189. package/src/utils/detailActionModel.js +59 -2
  190. package/src/utils/fillTemplate.js +110 -0
  191. package/src/utils/libraryWidgetKeys.js +4 -0
  192. package/src/utils/queryPresets.js +93 -0
  193. package/src/utils/resolveFilterTokens.js +11 -0
  194. package/src/utils/schema.js +21 -0
  195. package/src/utils/sentinelTokens.js +3 -3
  196. package/src/utils/validateManifestV2.compiled.js +3 -3
  197. package/src/utils/widgetDispatch.js +7 -1
@@ -1231,12 +1231,15 @@ import {
1231
1231
  groupMenuEntries,
1232
1232
  MAX_QUICK_ACTIONS,
1233
1233
  normalisePinnedAction,
1234
+ pillCountSource,
1235
+ resolveCountPill,
1234
1236
  resolveNextStep,
1235
1237
  resolvePill,
1236
1238
  stageEntry,
1237
1239
  stageOf,
1238
1240
  } from '../../utils/detailActionModel.js'
1239
1241
  import { reportBindingProblems } from '../../utils/diagnostics.js'
1242
+ import { fetchListTotal } from '../../utils/fetchFilterCounts.js'
1240
1243
  import { cnGridCellStyle, hasGridRow } from '../../utils/grid.js'
1241
1244
  import { isActivityWidget, resolveHeaderMeta } from '../../utils/headerMeta.js'
1242
1245
  import { patchStoredSelf } from '../../utils/patchStoredSelf.js'
@@ -2393,9 +2396,13 @@ export default {
2393
2396
  * A pill above the title saying what kind of record this is
2394
2397
  * (manifest `config.typePill`): `{ field, colorMap?, labels?, variant? }`.
2395
2398
  * Rendered through `CnStatusBadge`; `colorMap` is keyed on the field's
2396
- * raw value, `labels` maps a raw value to the text to show.
2399
+ * raw value, `labels` maps a raw value to the text to show. Instead of
2400
+ * `field`: `text`, a constant ("Inwoner"), or `countFrom`
2401
+ * (`{ register, schema, filter? }`) with `countLabel` (`{ one, other }`
2402
+ * with `{count}`), a count of a related list ("1 lopende zaak") that
2403
+ * shows nothing at 0.
2397
2404
  *
2398
- * @type {{field: string, colorMap?: object, labels?: object, variant?: string}|null}
2405
+ * @type {{field?: string, text?: string, countFrom?: object, countLabel?: object, colorMap?: object, labels?: object, variant?: string}|null}
2399
2406
  */
2400
2407
  typePill: {
2401
2408
  type: Object,
@@ -2747,6 +2754,10 @@ export default {
2747
2754
 
2748
2755
  data() {
2749
2756
  return {
2757
+ /** The counts of the header's count pills (`countFrom`), by pill key (`type`, `status`). */
2758
+ pillCounts: {},
2759
+ /** Sequence of the last pill count round, so a late answer does not overwrite a newer one. */
2760
+ pillCountSeq: 0,
2750
2761
  /** `register/schema/id` the read-state call was last made for, so it is sent once per load. */
2751
2762
  readStateSentFor: '',
2752
2763
  /** Whether the user is arranging the chosen view (`userLayout`). */
@@ -3259,12 +3270,25 @@ export default {
3259
3270
  headerPills() {
3260
3271
  return [['type', this.typePill], ['status', this.statusPill]]
3261
3272
  .map(([key, config]) => {
3262
- const pill = resolvePill(config, this.resolvedObject)
3273
+ const pill = pillCountSource(config)
3274
+ ? resolveCountPill(config, this.pillCounts[key], (text) => this.effectiveTranslate(text))
3275
+ : resolvePill(config, this.resolvedObject)
3263
3276
  return pill ? { ...pill, key, label: this.effectiveTranslate(pill.label) } : null
3264
3277
  })
3265
3278
  .filter(Boolean)
3266
3279
  },
3267
3280
 
3281
+ /**
3282
+ * What the count pills depend on: the record id and the lists they
3283
+ * count. A change counts again.
3284
+ *
3285
+ * @return {string} The key.
3286
+ * @spec openspec/changes/screens-relation-pages/specs/relation-pages/spec.md#requirement-a-header-pill-can-be-a-constant-or-a-count
3287
+ */
3288
+ pillCountKey() {
3289
+ return JSON.stringify([this.objectId, pillCountSource(this.typePill), pillCountSource(this.statusPill)])
3290
+ },
3291
+
3268
3292
  /**
3269
3293
  * The widget the header holds (`headerWidget`): the definition in
3270
3294
  * `widgets` with that id, or null without the key or a match.
@@ -4337,6 +4361,14 @@ export default {
4337
4361
  },
4338
4362
 
4339
4363
  watch: {
4364
+ // The header's count pills count again for another record or other lists.
4365
+ pillCountKey: {
4366
+ immediate: true,
4367
+ handler() {
4368
+ this.refreshPillCounts()
4369
+ },
4370
+ },
4371
+
4340
4372
  // Opening a record marks it read, once, after its data has rendered.
4341
4373
  readStateDue: {
4342
4374
  immediate: true,
@@ -4754,6 +4786,33 @@ export default {
4754
4786
  }
4755
4787
  },
4756
4788
 
4789
+ /**
4790
+ * Count the lists behind the header's count pills (`countFrom`): one
4791
+ * list request each (`_limit=1`, the `total`). A failed count, or a
4792
+ * record without an id yet, shows no pill.
4793
+ *
4794
+ * @return {Promise<void>}
4795
+ * @spec openspec/changes/screens-relation-pages/specs/relation-pages/spec.md#requirement-a-header-pill-can-be-a-constant-or-a-count
4796
+ */
4797
+ async refreshPillCounts() {
4798
+ const pills = [['type', this.typePill], ['status', this.statusPill]]
4799
+ .map(([key, config]) => [key, pillCountSource(config)])
4800
+ .filter(([, source]) => source)
4801
+ const seq = ++this.pillCountSeq
4802
+ if (pills.length === 0 || this.objectId === null || this.objectId === undefined || this.objectId === '') {
4803
+ this.pillCounts = {}
4804
+ return
4805
+ }
4806
+ const ctx = { objectId: this.objectId, object: this.resolvedObject || undefined }
4807
+ const counts = {}
4808
+ await Promise.all(pills.map(async ([key, source]) => {
4809
+ counts[key] = await fetchListTotal(source.register, source.schema, source.filter, ctx).catch(() => null)
4810
+ }))
4811
+ if (seq === this.pillCountSeq) {
4812
+ this.pillCounts = counts
4813
+ }
4814
+ },
4815
+
4757
4816
  /**
4758
4817
  * A `cn:page:refresh` reached the page — re-read the object so every
4759
4818
  * object-bound widget re-renders from what the backend now holds.
@@ -4768,6 +4827,7 @@ export default {
4768
4827
  * @return {void}
4769
4828
  */
4770
4829
  onPageRefreshBus(payload) {
4830
+ this.refreshPillCounts()
4771
4831
  if (!this.hasSchemaDrivenFetch) {
4772
4832
  return
4773
4833
  }
@@ -169,10 +169,28 @@
169
169
  :showActions="actionsShown || catalogAddEnabled"
170
170
  :showRefresh="false"
171
171
  :showRequestFeature="false"
172
+ :headerLink="rendererHeader.link"
172
173
  class="cn-detail-page__catalog-card">
173
174
  <template v-if="widget.icon" #title-icon>
174
175
  <CnIcon :name="widget.icon" :size="20" />
175
176
  </template>
177
+ <!-- A renderer that reports its own header (`header` event:
178
+ `{ link, note }`, e.g. record-list's "3 in totaal"). -->
179
+ <template v-if="rendererHeader.note || rendererHeader.action" #actions>
180
+ <span
181
+ v-if="rendererHeader.note"
182
+ class="cn-detail-page__catalog-note"
183
+ data-testid="cn-detail-widget-header-note">{{ rendererHeader.note }}</span>
184
+ <button
185
+ v-if="rendererHeader.action"
186
+ type="button"
187
+ class="cn-detail-page__catalog-action"
188
+ data-testid="cn-detail-widget-header-action"
189
+ @click="rendererHeader.action.run()">
190
+ <CnIcon v-if="rendererHeader.action.icon" :name="rendererHeader.action.icon" :size="16" />
191
+ {{ rendererHeader.action.label }}
192
+ </button>
193
+ </template>
176
194
  <template v-if="catalogAddEnabled" #action-items>
177
195
  <NcActionButton @click="invokeCatalogAdd">
178
196
  <template #icon>
@@ -185,6 +203,7 @@
185
203
  :is="renderer"
186
204
  ref="renderer"
187
205
  v-bind="rendererProps"
206
+ @header="onRendererHeader"
188
207
  @selectObject="onSelectObject" />
189
208
  </CnWidgetWrapper>
190
209
 
@@ -534,6 +553,17 @@ export default {
534
553
  }
535
554
  },
536
555
 
556
+ data() {
557
+ return {
558
+ /**
559
+ * The card header a content-only renderer reports through its
560
+ * `header` event (screens-relation-pages): a link and a note,
561
+ * with the list's total already filled in.
562
+ */
563
+ rendererHeader: { link: null, note: '', action: null },
564
+ }
565
+ },
566
+
537
567
  computed: {
538
568
  /**
539
569
  * Whether the caller owns the title and card.
@@ -992,6 +1022,19 @@ export default {
992
1022
  },
993
1023
 
994
1024
  methods: {
1025
+ /**
1026
+ * Take the header a content-only renderer reports.
1027
+ *
1028
+ * @param {{link?: object|null, note?: string, action?: {label: string, icon?: string, run: () => unknown}|null}} header The header parts.
1029
+ * @spec openspec/changes/screens-relation-pages/specs/relation-pages/spec.md#requirement-a-record-list-shows-the-objects-that-point-at-this-record
1030
+ */
1031
+ onRendererHeader(header) {
1032
+ const link = header && header.link && typeof header.link === 'object' ? header.link : null
1033
+ const note = header && typeof header.note === 'string' ? header.note : ''
1034
+ const action = header && header.action && typeof header.action.run === 'function' && header.action.label ? header.action : null
1035
+ this.rendererHeader = { link, note, action }
1036
+ },
1037
+
995
1038
  /**
996
1039
  * A manifest label through the host's translate; '' stays ''.
997
1040
  *
@@ -1110,6 +1153,29 @@ export default {
1110
1153
  </script>
1111
1154
 
1112
1155
  <style scoped>
1156
+ .cn-detail-page__catalog-action {
1157
+ align-items: center;
1158
+ background: var(--color-primary-element-light);
1159
+ border: 0;
1160
+ border-radius: 16px;
1161
+ color: var(--color-primary-element-light-text, var(--color-primary-element));
1162
+ cursor: pointer;
1163
+ display: inline-flex;
1164
+ font: inherit;
1165
+ font-size: 13px;
1166
+ font-weight: 700;
1167
+ gap: 6px;
1168
+ min-height: 32px;
1169
+ padding: 0 14px;
1170
+ white-space: nowrap;
1171
+ }
1172
+
1173
+ .cn-detail-page__catalog-note {
1174
+ color: var(--color-text-maxcontrast);
1175
+ font-size: 14px;
1176
+ white-space: nowrap;
1177
+ }
1178
+
1113
1179
  .cn-detail-widget-host,
1114
1180
  .cn-detail-widget-host > * {
1115
1181
  height: 100%;