@conduction/nextcloud-vue 2.56.0-beta.30 → 2.56.0-beta.31

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 (173) hide show
  1. package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js.map +1 -1
  2. package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js +3 -3
  3. package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js.map +1 -1
  4. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js +80 -4
  5. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
  6. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js +95 -91
  7. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js.map +1 -1
  8. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js +85 -6
  9. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js.map +1 -1
  10. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue3.js +1 -1
  11. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue3.js.map +1 -1
  12. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js +4 -1
  13. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js.map +1 -1
  14. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js.map +1 -1
  15. package/dist/esm/components/CnConversationThread/CnConversationWidget.vue2.js +1 -1
  16. package/dist/esm/components/CnConversationThread/CnConversationWidget.vue2.js.map +1 -1
  17. package/dist/esm/components/CnConversationThread/CnConversationWidget.vue3.js.map +1 -1
  18. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue2.js +2 -2
  19. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue2.js.map +1 -1
  20. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue3.js.map +1 -1
  21. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +40 -3
  22. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  23. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  24. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +19 -11
  25. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
  26. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
  27. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +43 -0
  28. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  29. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +1 -1
  30. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  31. package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewWidget.vue2.js +1 -1
  32. package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewWidget.vue2.js.map +1 -1
  33. package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewWidget.vue3.js.map +1 -1
  34. package/dist/esm/components/CnFacetSidebar/CnFacetSidebar.vue2.js +7 -7
  35. package/dist/esm/components/CnFacetSidebar/CnFacetSidebar.vue2.js.map +1 -1
  36. package/dist/esm/components/CnFacetSidebar/CnFacetSidebar.vue3.js.map +1 -1
  37. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js +125 -0
  38. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js.map +1 -1
  39. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js.map +1 -1
  40. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js +3 -2
  41. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js.map +1 -1
  42. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js.map +1 -1
  43. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js +302 -4
  44. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js.map +1 -1
  45. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js +417 -244
  46. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js.map +1 -1
  47. package/dist/esm/components/CnFilesBrowser/filesBrowser.js +101 -1
  48. package/dist/esm/components/CnFilesBrowser/filesBrowser.js.map +1 -1
  49. package/dist/esm/components/CnFilesBrowser/filesBrowserColumns.js +141 -1
  50. package/dist/esm/components/CnFilesBrowser/filesBrowserColumns.js.map +1 -1
  51. package/dist/esm/components/CnFormPage/CnFormPage.vue2.js +60 -0
  52. package/dist/esm/components/CnFormPage/CnFormPage.vue2.js.map +1 -1
  53. package/dist/esm/components/CnFormPage/CnFormPage.vue3.js +74 -49
  54. package/dist/esm/components/CnFormPage/CnFormPage.vue3.js.map +1 -1
  55. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +29 -1
  56. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  57. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +48 -4
  58. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  59. package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue2.js +7 -7
  60. package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue2.js.map +1 -1
  61. package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue3.js.map +1 -1
  62. package/dist/esm/components/CnNextStepCard/CnNextStepWidget.vue2.js +1 -1
  63. package/dist/esm/components/CnNextStepCard/CnNextStepWidget.vue2.js.map +1 -1
  64. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +1 -1
  65. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
  66. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js.map +1 -1
  67. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js +63 -1
  68. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js.map +1 -1
  69. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js +8 -1
  70. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js.map +1 -1
  71. package/dist/esm/components/CnObjectSidebar/CnNotesTab.vue2.js +14 -0
  72. package/dist/esm/components/CnObjectSidebar/CnNotesTab.vue2.js.map +1 -1
  73. package/dist/esm/components/CnObjectSidebar/CnNotesTab.vue3.js +5 -1
  74. package/dist/esm/components/CnObjectSidebar/CnNotesTab.vue3.js.map +1 -1
  75. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +13 -6
  76. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
  77. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
  78. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue2.js +84 -1
  79. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue2.js.map +1 -1
  80. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue3.js +85 -41
  81. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue3.js.map +1 -1
  82. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js +7 -2
  83. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js.map +1 -1
  84. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue3.js.map +1 -1
  85. package/dist/esm/components/CnWidgetObjectTable/dashboardRegistration.js +5 -0
  86. package/dist/esm/components/CnWidgetObjectTable/dashboardRegistration.js.map +1 -1
  87. package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue2.js +197 -6
  88. package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue2.js.map +1 -1
  89. package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue3.js +94 -50
  90. package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue3.js.map +1 -1
  91. package/dist/esm/composables/useListView.js +4 -4
  92. package/dist/esm/composables/useListView.js.map +1 -1
  93. package/dist/esm/index.js +10 -1
  94. package/dist/esm/index.js.map +1 -1
  95. package/dist/esm/l10n/en.json.js +8 -1
  96. package/dist/esm/l10n/en.json.js.map +1 -1
  97. package/dist/esm/l10n/nl.json.js +54 -1
  98. package/dist/esm/l10n/nl.json.js.map +1 -1
  99. package/dist/esm/nextcloud-vue.css +198 -8
  100. package/dist/esm/utils/facets.js +75 -3
  101. package/dist/esm/utils/facets.js.map +1 -1
  102. package/dist/esm/utils/fillTemplate.js +7 -1
  103. package/dist/esm/utils/fillTemplate.js.map +1 -1
  104. package/dist/esm/utils/pageControls.js +81 -0
  105. package/dist/esm/utils/pageControls.js.map +1 -0
  106. package/dist/esm/utils/personalLenses.js +40 -3
  107. package/dist/esm/utils/personalLenses.js.map +1 -1
  108. package/dist/esm/utils/resolveFilterTokens.js +1 -1
  109. package/dist/esm/utils/resolveFilterTokens.js.map +1 -1
  110. package/dist/esm/utils/resolveManifestTokens.js +2 -0
  111. package/dist/esm/utils/resolveManifestTokens.js.map +1 -1
  112. package/dist/esm/utils/sentinelTokens.js +19 -1
  113. package/dist/esm/utils/sentinelTokens.js.map +1 -1
  114. package/dist/esm/utils/userGroupsPeek.js +91 -0
  115. package/dist/esm/utils/userGroupsPeek.js.map +1 -0
  116. package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
  117. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  118. package/dist/esm/utils/visibleWhen.js +57 -5
  119. package/dist/esm/utils/visibleWhen.js.map +1 -1
  120. package/dist/esm/utils/widgetVisibility.js +4 -57
  121. package/dist/esm/utils/widgetVisibility.js.map +1 -1
  122. package/dist/nextcloud-vue.cjs.js +4309 -2233
  123. package/dist/nextcloud-vue.cjs.js.map +2 -2
  124. package/dist/nextcloud-vue.css +198 -8
  125. package/l10n/en.json +8 -1
  126. package/l10n/nl.json +54 -1
  127. package/package.json +2 -1
  128. package/scripts/check-docs-mdx.mjs +108 -0
  129. package/src/components/CnActionsBar/CnActionsBar.vue +6 -1
  130. package/src/components/CnAppRoot/CnAppRoot.md +4 -2
  131. package/src/components/CnAppRoot/CnAppRoot.vue +151 -59
  132. package/src/components/CnBannerWidget/CnBannerWidget.vue +112 -7
  133. package/src/components/CnCellRenderer/CnCellRenderer.vue +4 -1
  134. package/src/components/CnConversationThread/CnConversationWidget.vue +1 -1
  135. package/src/components/CnDashboardGrid/CnDashboardGrid.vue +2 -2
  136. package/src/components/CnDashboardPage/CnDashboardPage.vue +40 -3
  137. package/src/components/CnDataTable/CnDataTable.vue +19 -11
  138. package/src/components/CnDetailPage/CnDetailPage.vue +44 -1
  139. package/src/components/CnDocumentReviewList/CnDocumentReviewWidget.vue +1 -1
  140. package/src/components/CnFacetSidebar/CnFacetSidebar.vue +7 -7
  141. package/src/components/CnFactsWidget/CnFactsWidget.vue +125 -0
  142. package/src/components/CnFieldListWidget/CnFieldListWidget.vue +3 -2
  143. package/src/components/CnFilesBrowser/CnFilesBrowser.vue +588 -117
  144. package/src/components/CnFilesBrowser/filesBrowser.js +100 -0
  145. package/src/components/CnFilesBrowser/filesBrowserColumns.js +140 -0
  146. package/src/components/CnFormPage/CnFormPage.vue +81 -0
  147. package/src/components/CnIndexPage/CnIndexPage.vue +56 -3
  148. package/src/components/CnIndexSidebar/CnIndexSidebar.vue +7 -7
  149. package/src/components/CnNextStepCard/CnNextStepWidget.vue +1 -1
  150. package/src/components/CnObjectListWidget/CnObjectListWidget.vue +1 -1
  151. package/src/components/CnObjectSidebar/CnFilesTab.vue +70 -1
  152. package/src/components/CnObjectSidebar/CnNotesTab.vue +16 -0
  153. package/src/components/CnRecordListWidget/CnRecordListWidget.vue +13 -6
  154. package/src/components/CnStatsBlockWidget/CnStatsBlockWidget.vue +180 -3
  155. package/src/components/CnWidgetObjectTable/CnWidgetObjectTable.vue +7 -2
  156. package/src/components/CnWidgetObjectTable/dashboardRegistration.js +5 -0
  157. package/src/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue +281 -10
  158. package/src/components/index.js +8 -0
  159. package/src/composables/useListView.js +4 -4
  160. package/src/css/look-board-index.css +16 -8
  161. package/src/index.js +8 -0
  162. package/src/schemas/app-manifest-v2.schema.json +93 -9
  163. package/src/utils/facets.js +74 -2
  164. package/src/utils/fillTemplate.js +7 -1
  165. package/src/utils/pageControls.js +78 -0
  166. package/src/utils/personalLenses.js +39 -2
  167. package/src/utils/resolveFilterTokens.js +3 -1
  168. package/src/utils/resolveManifestTokens.js +2 -0
  169. package/src/utils/sentinelTokens.js +19 -1
  170. package/src/utils/userGroupsPeek.js +87 -0
  171. package/src/utils/validateManifestV2.compiled.js +3 -3
  172. package/src/utils/visibleWhen.js +56 -4
  173. package/src/utils/widgetVisibility.js +7 -56
@@ -81,6 +81,7 @@ import CnIcon from '../CnIcon/CnIcon.vue'
81
81
  import CnStatusBadge from '../CnStatusBadge/CnStatusBadge.vue'
82
82
  import { isBadgeVariant } from '../../utils/badgeVariants.js'
83
83
  import { fillTemplate, firstFilled } from '../../utils/fillTemplate.js'
84
+ import { pageFilter, pageSort } from '../../utils/pageControls.js'
84
85
  import { readPath } from '../../utils/readPath.js'
85
86
  import { dropOptionalUnresolved, hasUnresolvedTokens, resolveFilterTokens } from '../../utils/resolveFilterTokens.js'
86
87
 
@@ -108,7 +109,7 @@ function unwrap(value) {
108
109
  * objects filtered on a reference to this record. The source keys are those of
109
110
  * `object-list` (`register`, `schema`, `filter` with the `@objectId` /
110
111
  * `@object.<field>` / `@workspace.<key>` tokens, `sort`, `limit`, `extend`,
111
- * `rowRoute`); what differs is the drawing: `presentation` is `rows`, `feed`
112
+ * `rowRoute`, `pageControls`); what differs is the drawing: `presentation` is `rows`, `feed`
112
113
  * or `inline`, and each line is a template (`"{identifier} · {caseTypeName}"`,
113
114
  * `"Termijn {deadline|date}"`).
114
115
  *
@@ -188,8 +189,8 @@ export default {
188
189
 
189
190
  /** @return {object} The filter with its tokens resolved. */
190
191
  resolvedFilter() {
191
- const filter = this.content?.filter
192
- return dropOptionalUnresolved(resolveFilterTokens(filter && typeof filter === 'object' ? filter : {}, this.tokenCtx))
192
+ const filter = pageFilter(this.content?.pageControls, this.tokenCtx.workspace, this.content?.filter)
193
+ return dropOptionalUnresolved(resolveFilterTokens(filter, this.tokenCtx))
193
194
  },
194
195
 
195
196
  /** @return {boolean} A required token (the record id, say) has no value yet. */
@@ -197,6 +198,11 @@ export default {
197
198
  return hasUnresolvedTokens(this.resolvedFilter)
198
199
  },
199
200
 
201
+ /** @return {object|null} The sort: the page's sort control when the widget listens to one, else its own. */
202
+ effectiveSort() {
203
+ return pageSort(this.content?.pageControls, this.tokenCtx.workspace, this.content?.sort)
204
+ },
205
+
200
206
  /** @return {number} The fetch cap. */
201
207
  limit() {
202
208
  const value = Number(this.content?.limit)
@@ -206,7 +212,7 @@ export default {
206
212
  /** @return {string} What a fetch depends on; a change fetches again. */
207
213
  requestKey() {
208
214
  const c = this.content || {}
209
- return JSON.stringify([c.register, c.schema, this.resolvedFilter, c.sort, this.limit, c.extend, this.waiting])
215
+ return JSON.stringify([c.register, c.schema, this.resolvedFilter, this.effectiveSort, this.limit, c.extend, this.waiting])
210
216
  },
211
217
 
212
218
  /** @return {Array<object>} The rows ready to draw. */
@@ -422,8 +428,9 @@ export default {
422
428
  ])
423
429
  const url = generateUrl('/apps/openregister/api/objects/{register}/{schema}', { register: c.register, schema: c.schema })
424
430
  const params = { _limit: this.limit }
425
- if (c.sort && typeof c.sort === 'object' && c.sort.field) {
426
- params[`_order[${c.sort.field}]`] = c.sort.dir === 'desc' ? 'desc' : 'asc'
431
+ const sort = this.effectiveSort
432
+ if (sort && typeof sort === 'object' && sort.field) {
433
+ params[`_order[${sort.field}]`] = sort.dir === 'desc' ? 'desc' : 'asc'
427
434
  }
428
435
  if (Array.isArray(c.extend)) {
429
436
  const extend = c.extend.filter((e) => typeof e === 'string' && e !== '')
@@ -17,10 +17,42 @@
17
17
  <template>
18
18
  <div
19
19
  class="cn-stats-block-widget"
20
- :class="[iconClass, { 'cn-stats-block-widget--multi': hasEntries }]">
20
+ :class="[iconClass, { 'cn-stats-block-widget--multi': hasEntries && !isRows, 'cn-stats-block-widget--rows': isRows }]">
21
+ <!-- Rows layout: a title and one label-left, number-right row per entry
22
+ (the board's "Mijn Zuiddrecht nu" card). -->
23
+ <template v-if="isRows">
24
+ <h2 v-if="title" class="cn-stats-block-widget__rows-title" data-testid="cn-stats-block-rows-title">
25
+ {{ tl(title) }}
26
+ </h2>
27
+ <dl class="cn-stats-block-widget__rows" data-testid="cn-stats-block-rows">
28
+ <div
29
+ v-for="view in rowViews"
30
+ :key="view.key"
31
+ class="cn-stats-block-widget__row"
32
+ data-testid="cn-stats-block-row">
33
+ <dt class="cn-stats-block-widget__row-label">
34
+ <a
35
+ v-if="view.href"
36
+ :href="view.href"
37
+ data-testid="cn-stats-block-row-link"
38
+ @click="onRowClick($event, view.route)">{{ view.label }}</a>
39
+ <template v-else>
40
+ {{ view.label }}
41
+ </template>
42
+ </dt>
43
+ <dd
44
+ class="cn-stats-block-widget__row-value"
45
+ :class="'cn-stats-block-widget__row-value--' + view.variant"
46
+ data-testid="cn-stats-block-row-value">
47
+ {{ view.display }}
48
+ </dd>
49
+ </div>
50
+ </dl>
51
+ </template>
52
+
21
53
  <!-- Multi-entry mode: one KPI per entry, all inside this widget card.
22
54
  An entry with hideWhenZero whose resolved count is 0 is omitted. -->
23
- <template v-if="hasEntries">
55
+ <template v-else-if="hasEntries">
24
56
  <CnStatsBlock
25
57
  v-for="view in entryViews"
26
58
  :key="view.key"
@@ -71,6 +103,7 @@ import { subscribe, unsubscribe } from '@nextcloud/event-bus'
71
103
  import CnStatsBlock from '../CnStatsBlock/CnStatsBlock.vue'
72
104
  import { useDataSource } from '../../composables/useDataSource.js'
73
105
  import { formatMetricValue, normalizeMetricFormat, unwrapAppConfig } from '../../utils/formatMetric.js'
106
+ import { followLinkClick, resolveHref } from '../../utils/linkNavigation.js'
74
107
  import { dropOptionalUnresolved, hasUnresolvedTokens, resolveFilterTokens } from '../../utils/resolveFilterTokens.js'
75
108
 
76
109
  // The canonical KPI scale (`--cn-kpi-*`) lives in one stylesheet. Imported
@@ -167,6 +200,8 @@ export default {
167
200
  * currency (`currency`) and `@config.<key>` tokens in `format`.
168
201
  */
169
202
  cnAppConfig: { default: () => ({}) },
203
+ /** The host's label lookup (CnAppRoot provides it); identity without one. */
204
+ cnTranslate: { default: () => (key) => key },
170
205
  },
171
206
 
172
207
  props: {
@@ -264,7 +299,10 @@ export default {
264
299
 
265
300
  /**
266
301
  * `stacked` gives every block the board look (see CnStatsBlock
267
- * `layout`). Empty (the default) keeps the KPI card.
302
+ * `layout`). `rows` draws the title and then one row per entry (or the
303
+ * single source): the label on the left, the number on the right, the
304
+ * label a link when the entry has a `route`. Empty (the default) keeps
305
+ * the KPI card.
268
306
  *
269
307
  * @spec openspec/changes/zuiddrecht-pixel-gaps-3/specs/zuiddrecht-pixel-gaps-3/spec.md#requirement-a-stats-block-can-take-the-stacked-board-look
270
308
  */
@@ -353,6 +391,52 @@ export default {
353
391
  return Array.isArray(this.entries) && this.entries.length > 0
354
392
  },
355
393
 
394
+ /**
395
+ * Whether the widget draws label/number rows (`layout: "rows"`).
396
+ *
397
+ * @return {boolean}
398
+ */
399
+ isRows() {
400
+ return this.layout === 'rows'
401
+ },
402
+
403
+ /**
404
+ * The rows of the rows layout: one per entry, or a single row from the
405
+ * `dataSource` (labelled by `countLabel`). A row is `{ key, label,
406
+ * display, variant, route, href }`; `display` is the formatted value,
407
+ * a dash while it loads or when the count could not be read.
408
+ *
409
+ * @return {Array<object>}
410
+ */
411
+ rowViews() {
412
+ if (!this.hasEntries) {
413
+ const formatted = this.formattedSingle
414
+ return [{
415
+ key: 'single',
416
+ label: this.tl(this.countLabel),
417
+ display: this.loading ? '\u2014' : (formatted !== null ? formatted : this.formatCount(this.resolvedCount)),
418
+ variant: this.variant || 'default',
419
+ route: this.route,
420
+ href: this.route ? resolveHref(this.route, this.$router) : '',
421
+ }]
422
+ }
423
+ return this.entryViews.map((view) => {
424
+ const resolved = view.resolved === true
425
+ let display = '\u2014'
426
+ if (resolved) {
427
+ display = view.formatted !== null ? view.formatted : this.formatCount(view.count)
428
+ }
429
+ return {
430
+ key: view.key,
431
+ label: this.tl(view.entry.title || ''),
432
+ display,
433
+ variant: view.entry.variant || 'default',
434
+ route: view.route,
435
+ href: view.route ? resolveHref(view.route, this.$router) : '',
436
+ }
437
+ })
438
+ },
439
+
356
440
  /**
357
441
  * The unwrapped detail-page object context for token resolution, or
358
442
  * null on surfaces (dashboards) that don't provide one.
@@ -566,6 +650,37 @@ export default {
566
650
  },
567
651
 
568
652
  methods: {
653
+ /**
654
+ * A manifest string through the host's label lookup.
655
+ *
656
+ * @param {string} text The text as written in the manifest.
657
+ * @return {string} The translated text.
658
+ */
659
+ tl(text) {
660
+ return typeof this.cnTranslate === 'function' ? this.cnTranslate(text) : text
661
+ },
662
+
663
+ /**
664
+ * A count in the user's locale.
665
+ *
666
+ * @param {number} count The count.
667
+ * @return {string} The localized count.
668
+ */
669
+ formatCount(count) {
670
+ return new Intl.NumberFormat().format(count)
671
+ },
672
+
673
+ /**
674
+ * Follow a row label's link in the router, leaving a modified click to the browser.
675
+ *
676
+ * @param {MouseEvent} event The click.
677
+ * @param {object} route The route location.
678
+ * @return {void}
679
+ */
680
+ onRowClick(event, route) {
681
+ followLinkClick(event, route, this.$router)
682
+ },
683
+
569
684
  /**
570
685
  * Re-query the tile's counts — the REST `/value` aggregation, the
571
686
  * per-entry counts of multi-entry mode, and the `dataSource` GraphQL
@@ -758,4 +873,66 @@ export default {
758
873
  flex-direction: column;
759
874
  gap: var(--cn-kpi-stack-gap, 12px);
760
875
  }
876
+
877
+ /* Rows layout: the board's "label left, number right" card body. */
878
+ .cn-stats-block-widget--rows {
879
+ display: flex;
880
+ flex-direction: column;
881
+ gap: 12px;
882
+ box-sizing: border-box;
883
+ padding: 20px 22px;
884
+ }
885
+
886
+ .cn-stats-block-widget__rows-title {
887
+ margin: 0;
888
+ font-size: 15px;
889
+ font-weight: 600;
890
+ color: var(--color-text-maxcontrast);
891
+ }
892
+
893
+ .cn-stats-block-widget__rows {
894
+ display: flex;
895
+ flex-direction: column;
896
+ gap: 8px;
897
+ margin: 0;
898
+ font-size: 14px;
899
+ }
900
+
901
+ .cn-stats-block-widget__row {
902
+ display: flex;
903
+ align-items: baseline;
904
+ justify-content: space-between;
905
+ gap: 16px;
906
+ }
907
+
908
+ .cn-stats-block-widget__row-label {
909
+ min-width: 0;
910
+ margin: 0;
911
+ overflow: hidden;
912
+ text-overflow: ellipsis;
913
+ }
914
+
915
+ .cn-stats-block-widget__row-value {
916
+ flex: none;
917
+ margin: 0;
918
+ font-size: 17px;
919
+ font-weight: 700;
920
+ text-align: right;
921
+ }
922
+
923
+ .cn-stats-block-widget__row-value--success {
924
+ color: var(--color-text-success, var(--color-success-text));
925
+ }
926
+
927
+ .cn-stats-block-widget__row-value--warning {
928
+ color: var(--color-text-warning, var(--color-warning-text));
929
+ }
930
+
931
+ .cn-stats-block-widget__row-value--error {
932
+ color: var(--color-text-error, var(--color-error-text));
933
+ }
934
+
935
+ .cn-stats-block-widget__row-value--primary {
936
+ color: var(--color-primary-element);
937
+ }
761
938
  </style>
@@ -86,6 +86,7 @@ import { useEndpointSource } from '../../composables/useEndpointSource.js'
86
86
  import { useObjectStore } from '../../store/useObjectStore.js'
87
87
  import { dispatchAction, resolveObjectOpType } from '../../utils/actionsDispatcher.js'
88
88
  import { resolveObjectTokenContext } from '../../utils/detailObjectContext.js'
89
+ import { pageFilter, pageSort } from '../../utils/pageControls.js'
89
90
  import { dropOptionalUnresolved, hasUnresolvedTokens, resolveFilterTokens } from '../../utils/resolveFilterTokens.js'
90
91
  import { compareVisibleWhen, readVisibleWhenPath } from '../../utils/visibleWhen.js'
91
92
  import { CnIcon } from '../CnIcon/index.js'
@@ -517,7 +518,8 @@ export default {
517
518
  * @return {object}
518
519
  */
519
520
  resolvedFilter() {
520
- return dropOptionalUnresolved(resolveFilterTokens((this.source && this.source.filter) || {}, this.tokenCtx))
521
+ const filter = pageFilter(this.source && this.source.pageControls, this.workspaceCtx, this.source && this.source.filter)
522
+ return dropOptionalUnresolved(resolveFilterTokens(filter, this.tokenCtx))
521
523
  },
522
524
 
523
525
  /**
@@ -590,7 +592,10 @@ export default {
590
592
  resolvedFetchParams() {
591
593
  const src = this.source || {}
592
594
  const params = {}
593
- for (const [field, dir] of Object.entries(src.order || {})) {
595
+ // A page sort control (`source.pageControls.sort`) replaces the order.
596
+ const chosen = pageSort(src.pageControls, this.workspaceCtx, null)
597
+ const order = chosen ? { [chosen.field]: chosen.dir } : (src.order || {})
598
+ for (const [field, dir] of Object.entries(order)) {
594
599
  params[`_order[${field}]`] = dir === 'desc' ? 'desc' : 'asc'
595
600
  }
596
601
  if (typeof src.limit === 'number' && src.limit > 0) {
@@ -73,6 +73,11 @@ export function objectTableContentToProps(content) {
73
73
  if (hasLimit) {
74
74
  source.limit = limit
75
75
  }
76
+ // The page controls the table listens to belong to its source.
77
+ if (rest.pageControls && typeof rest.pageControls === 'object') {
78
+ source.pageControls = rest.pageControls
79
+ delete rest.pageControls
80
+ }
76
81
  return { ...rest, source }
77
82
  }
78
83