@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
@@ -1765,17 +1765,6 @@ export default {
1765
1765
  return (col && typeof col.format === 'object') ? col.format : null
1766
1766
  },
1767
1767
 
1768
- /**
1769
- * Value to render in a cell. For a column that declares `aggregate`
1770
- * (a count of related objects), returns the cached count once
1771
- * `loadAggregates()` has resolved it (or `'…'` while pending, `'—'`
1772
- * if it failed / there's nothing to count). Otherwise the row's
1773
- * property value via `getCellValue`.
1774
- *
1775
- * @param {object} row The row data.
1776
- * @param {object} col The column definition.
1777
- * @return {unknown} The value handed to the slot / CnCellRenderer.
1778
- */
1779
1768
  /**
1780
1769
  * The secondary line of a cell, from `col.secondary`: a function is
1781
1770
  * called with the row; a string with `{field}` placeholders is a
@@ -1806,6 +1795,21 @@ export default {
1806
1795
  return asText(this.getCellValue(row, spec))
1807
1796
  },
1808
1797
 
1798
+ /**
1799
+ * Value to render in a cell. For a column that declares `aggregate`
1800
+ * (a count of related objects), returns the cached count once
1801
+ * `loadAggregates()` has resolved it (or `'…'` while pending, `'—'`
1802
+ * if it failed / there's nothing to count). A count of zero reads as
1803
+ * `aggregate.zeroLabel` when the column sets one ("None", which the
1804
+ * app's `cnTranslate` turns into "Geen" as the boards draw it); without
1805
+ * it the cell shows `0` as before. Otherwise the row's property value
1806
+ * via `getCellValue`.
1807
+ *
1808
+ * @spec openspec/changes/aggregate-zero-label/specs/aggregate-zero-label/spec.md#requirement-a-count-column-can-name-zero
1809
+ * @param {object} row The row data.
1810
+ * @param {object} col The column definition.
1811
+ * @return {unknown} The value handed to the slot / CnCellRenderer.
1812
+ */
1809
1813
  cellValue(row, col) {
1810
1814
  if (col && col.aggregate) {
1811
1815
  const cached = this.aggregateValues[String(row[this.rowKey])]
@@ -1813,6 +1817,10 @@ export default {
1813
1817
  if (v === undefined) {
1814
1818
  return this.aggregateLoading ? '…' : '—'
1815
1819
  }
1820
+ const zeroLabel = col.aggregate.zeroLabel
1821
+ if ((v === 0 || v === '0') && typeof zeroLabel === 'string' && zeroLabel !== '') {
1822
+ return typeof this.cnTranslate === 'function' ? this.cnTranslate(zeroLabel) : zeroLabel
1823
+ }
1816
1824
  return v
1817
1825
  }
1818
1826
  return this.getCellValue(row, col.key)
@@ -745,7 +745,7 @@
745
745
  :layout="section.layout"
746
746
  :editable="section.editable"
747
747
  :columns="12"
748
- :columnOpts="columnOpts"
748
+ :columnOpts="bodyColumnOpts"
749
749
  class="cn-detail-page__grid"
750
750
  @layoutChange="section.view ? onViewLayoutChange($event) : onBodyLayoutChange($event)">
751
751
  <template #widget="{ item }">
@@ -1295,6 +1295,35 @@ const DEFAULT_DETAIL_COLUMN_OPTS = Object.freeze({
1295
1295
  breakpointForWindow: false,
1296
1296
  })
1297
1297
 
1298
+ /**
1299
+ * Where the default table stacks when a side column sits beside the body grid.
1300
+ *
1301
+ * The side column takes about 320px, so the grid it leaves is far narrower than
1302
+ * the page: 790px at a 1100px window. Stacking that at 1000px made a row of
1303
+ * small tiles (a KPI row of 5 or 6 stat tiles) impossible beside a side column.
1304
+ * 1000px exists to keep a `gridWidth: 3` side PANEL inside the grid readable; a
1305
+ * page with a real side column has no such panel, and 12 columns at 600px is
1306
+ * still 50px a column, enough for a tile. Below 600px it stacks as before.
1307
+ */
1308
+ const SIDE_COLUMN_STACK_WIDTH = 600
1309
+
1310
+ /**
1311
+ * Whether `opts` is the untouched default table, so a page that passed its own
1312
+ * `columnOpts` keeps exactly what it asked for.
1313
+ *
1314
+ * @param {object|null} opts The `columnOpts` value.
1315
+ * @return {boolean}
1316
+ */
1317
+ function isDefaultColumnOpts(opts) {
1318
+ const breakpoints = opts && opts.breakpoints
1319
+ return !!opts
1320
+ && opts.columnMax === DEFAULT_DETAIL_COLUMN_OPTS.columnMax
1321
+ && Array.isArray(breakpoints)
1322
+ && breakpoints.length === 1
1323
+ && breakpoints[0].w === DEFAULT_DETAIL_COLUMN_OPTS.breakpoints[0].w
1324
+ && breakpoints[0].c === 1
1325
+ }
1326
+
1298
1327
  /**
1299
1328
  * Event-bus channel a page-level refresh is announced on. The page's own
1300
1329
  * Actions menu emits it, the manifest `refresh` / `api-call` actions bump it,
@@ -3335,6 +3364,20 @@ export default {
3335
3364
  .map(({ widget }) => widget)
3336
3365
  },
3337
3366
 
3367
+ /**
3368
+ * The `columnOpts` the body grid gets: the default table stacks at 1000px
3369
+ * of grid width, or at 600px when a side column narrows the grid. An
3370
+ * explicit `columnOpts` prop is passed through untouched.
3371
+ *
3372
+ * @return {object|null} The column options.
3373
+ */
3374
+ bodyColumnOpts() {
3375
+ if (!this.showsSideColumn || !isDefaultColumnOpts(this.columnOpts)) {
3376
+ return this.columnOpts
3377
+ }
3378
+ return { ...this.columnOpts, breakpoints: [{ w: SIDE_COLUMN_STACK_WIDTH, c: 1 }] }
3379
+ },
3380
+
3338
3381
  /**
3339
3382
  * Whether the side column renders.
3340
3383
  *
@@ -31,7 +31,7 @@ import { readPath } from '../../utils/readPath.js'
31
31
  * Renders CnDocumentReviewList from a list on the bound record:
32
32
  * `content: { field, title?, statusField?, statuses?, rowAction?: { label, route } }`.
33
33
  * A row action with a `route` opens that route with the row's key as `id`.
34
- * Resolved by its registry key, not exported.
34
+ * Resolved by its registry key, and exported for an app that renders it itself.
35
35
  */
36
36
  export default {
37
37
  name: 'CnDocumentReviewWidget',
@@ -62,7 +62,7 @@
62
62
  <script>
63
63
  import { translate as t } from '@nextcloud/l10n'
64
64
  import { NcButton, NcCheckboxRadioSwitch, NcLoadingIcon, NcSelect, NcTextField } from '@nextcloud/vue'
65
- import { facetOptionLabel } from '../../utils/facets.js'
65
+ import { facetOptions } from '../../utils/facets.js'
66
66
  import { filtersFromSchema } from '../../utils/schema.js'
67
67
 
68
68
  /**
@@ -175,12 +175,12 @@ export default {
175
175
 
176
176
  getFilterOptions(filter) {
177
177
  // Use facet data if available (live values with counts)
178
- const facet = this.facetData[filter.key]
179
- if (facet?.values?.length > 0) {
180
- return facet.values.map((v) => ({
181
- id: v.value,
182
- label: facetOptionLabel(v),
183
- }))
178
+ const live = facetOptions(
179
+ this.facetData[filter.key],
180
+ (count) => t('nextcloud-vue', 'No value ({count})', { count }),
181
+ )
182
+ if (live !== null) {
183
+ return live
184
184
  }
185
185
  // Fall back to static enum options from schema
186
186
  return filter.options || []
@@ -140,6 +140,7 @@ import EyeOutline from 'vue-material-design-icons/EyeOutline.vue'
140
140
  import CnIcon from '../CnIcon/CnIcon.vue'
141
141
  import CnStatusBadge from '../CnStatusBadge/CnStatusBadge.vue'
142
142
  import { resolveEndpointRequest } from '../../composables/useEndpointSource.js'
143
+ import { useRefLabels } from '../../composables/useRefLabels.js'
143
144
  import { dispatchAction } from '../../utils/actionsDispatcher.js'
144
145
  import { isBadgeVariant } from '../../utils/badgeVariants.js'
145
146
  import { formatBoardDate } from '../../utils/boardDate.js'
@@ -150,6 +151,29 @@ import { hasUnresolvedDeepTokens, resolveDeepTokens } from '../../utils/resolveF
150
151
  /** Kinds of entry a `fields[]` item can be. A labelled item without `as` is a row. */
151
152
  const KINDS = Object.freeze(['row', 'title', 'line', 'pill', 'lead', 'text'])
152
153
 
154
+ /**
155
+ * The label resolver every facts widget on the page shares, made on first use
156
+ * (the object store needs an active pinia). One batched request per register and
157
+ * schema for the ids it has not seen; a label it has read stays until the page
158
+ * reloads, and an object the store already holds is not fetched at all.
159
+ *
160
+ * @type {{resolve: Function}|null}
161
+ */
162
+ let refLabels = null
163
+
164
+ /** The page-wide reference label resolver. */
165
+ function sharedRefLabels() {
166
+ if (!refLabels) {
167
+ refLabels = useRefLabels()
168
+ }
169
+ return refLabels
170
+ }
171
+
172
+ /** Key of a resolved reference in the widget's `labels` data. */
173
+ function refKey(resolve, id) {
174
+ return `${resolve.register}/${resolve.schema}/${resolve.labelField || ''}/${id}`
175
+ }
176
+
153
177
  /** Event-bus channel a reveal of a masked value is announced on, so an app can log it. */
154
178
  export const FACTS_REVEAL_CHANNEL = 'cn:facts:reveal'
155
179
 
@@ -213,6 +237,7 @@ export default {
213
237
  * `as` is `row` (default when `label` or `icon` is set), `title`, `line`, `text` (a line in the normal ink), `lead` (an icon tile or initials beside `title` and `subtitle`) or `pill`.
214
238
  * A row's `format` may also be `email`, `phone` or `url` (a link) or `masked` (the last four characters; `reveal: true` adds Show). `copy: true` adds a copy button; `icon` replaces the label.
215
239
  * A part may be `{ text }`, a constant; a `line` or `text` entry without `path` or `parts` shows its `text`.
240
+ * `resolve: { register, schema, labelField? }` on an entry or a part turns a value that is a reference (an id, or a list of ids) into the label of the referenced object (`labelField`, else `title`, then `name`): the case type or status of a task shows its name, not its uuid. The ids are fetched once per register and schema through the object store and shared by every facts widget on the page; the entry stays empty until its label is known, and when no label can be read.
216
241
  * - `action` — an action drawn as a button under the facts; `headerAction` — one drawn in the card header (reported through the `header` event).
217
242
  * `format` is `text` (default), `date` (board short date), `date-long` (always with the year) or `list` (an array joined by ", ", each item read at `itemPath`).
218
243
  * `map` turns a raw value into a label (translated). `parts` (on a `line`) lists paths, or `{ path, label?, format? }`, joined by `separator` (default " · "); empty parts are dropped.
@@ -255,6 +280,8 @@ export default {
255
280
  revealed: {},
256
281
  /** Key of the row whose value was just copied. */
257
282
  copied: '',
283
+ /** Labels of the referenced objects read so far, by `refKey`. */
284
+ labels: {},
258
285
  }
259
286
  },
260
287
 
@@ -430,6 +457,13 @@ export default {
430
457
  },
431
458
  },
432
459
 
460
+ record: {
461
+ immediate: true,
462
+ handler() {
463
+ this.loadLabels()
464
+ },
465
+ },
466
+
433
467
  headerAction: {
434
468
  immediate: true,
435
469
  handler() {
@@ -668,6 +702,94 @@ export default {
668
702
  })
669
703
  },
670
704
 
705
+ /**
706
+ * The text of a reference: the labels of the objects its id(s) name, joined
707
+ * by ", ". An id whose label is not known yet (or cannot be read) adds nothing.
708
+ *
709
+ * @param {unknown} raw The id, or a list of ids (or of `{ id }` objects).
710
+ * @param {object} spec `{ resolve: { register, schema, labelField? }, itemPath? }`.
711
+ * @return {string}
712
+ */
713
+ refText(raw, spec) {
714
+ return this.refIds(raw, spec)
715
+ .map((id) => this.labels[refKey(spec.resolve, id)])
716
+ .filter((label) => typeof label === 'string' && label !== '')
717
+ .join(', ')
718
+ },
719
+
720
+ /**
721
+ * The ids a reference value holds.
722
+ *
723
+ * @param {unknown} raw The value.
724
+ * @param {object} spec `{ itemPath? }`.
725
+ * @return {string[]}
726
+ */
727
+ refIds(raw, spec) {
728
+ return (Array.isArray(raw) ? raw : [raw])
729
+ .map((item) => (item && typeof item === 'object' ? (spec.itemPath ? readPath(item, spec.itemPath) : (item.id ?? item.uuid)) : item))
730
+ .filter((id) => id !== null && id !== undefined && id !== '' && typeof id !== 'object')
731
+ .map(String)
732
+ },
733
+
734
+ /**
735
+ * Every entry or part that carries `resolve`, with the record's value at its path.
736
+ *
737
+ * @param {object} record The object.
738
+ * @return {Array<{spec: object, raw: unknown}>}
739
+ */
740
+ refSpecs(record) {
741
+ const fields = Array.isArray(this.content?.fields) ? this.content.fields : []
742
+ const specs = []
743
+ const visit = (spec) => {
744
+ if (!spec || typeof spec !== 'object') {
745
+ return
746
+ }
747
+ if (spec.resolve && typeof spec.resolve === 'object' && spec.path) {
748
+ specs.push({ spec, raw: readPath(record, spec.path) })
749
+ }
750
+ const nested = [...(Array.isArray(spec.parts) ? spec.parts : []), ...(Array.isArray(spec.title) ? spec.title : [spec.title]), ...(Array.isArray(spec.subtitle) ? spec.subtitle : [spec.subtitle])]
751
+ nested.forEach(visit)
752
+ }
753
+ fields.forEach(visit)
754
+ return specs
755
+ },
756
+
757
+ /**
758
+ * Read the labels of the referenced objects, one batched request per
759
+ * register and schema, and keep them in `labels`.
760
+ *
761
+ * @param {object|null} record The object shown (defaults to the current record).
762
+ * @return {Promise<void>}
763
+ */
764
+ async loadLabels(record = this.record) {
765
+ if (!record) {
766
+ return
767
+ }
768
+ const groups = new Map()
769
+ for (const { spec, raw } of this.refSpecs(record)) {
770
+ const { register, schema, labelField } = spec.resolve
771
+ if (!register || !schema) {
772
+ continue
773
+ }
774
+ const groupKey = `${register}/${schema}/${labelField || ''}`
775
+ const group = groups.get(groupKey) || { resolve: spec.resolve, ids: new Set() }
776
+ this.refIds(raw, spec).forEach((id) => group.ids.add(id))
777
+ groups.set(groupKey, group)
778
+ }
779
+ if (groups.size === 0) {
780
+ return
781
+ }
782
+ const resolver = sharedRefLabels()
783
+ const next = { ...this.labels }
784
+ await Promise.all([...groups.values()].map(async ({ resolve, ids }) => {
785
+ const found = await resolver.resolve(resolve.register, resolve.schema, [...ids], resolve.labelField)
786
+ Object.entries(found).forEach(([id, label]) => {
787
+ next[refKey(resolve, id)] = label
788
+ })
789
+ }))
790
+ this.labels = next
791
+ },
792
+
671
793
  /**
672
794
  * Read the source object.
673
795
  *
@@ -774,6 +896,9 @@ export default {
774
896
  if (raw === null || raw === undefined || raw === '') {
775
897
  return ''
776
898
  }
899
+ if (spec.resolve && typeof spec.resolve === 'object') {
900
+ return this.refText(raw, spec)
901
+ }
777
902
  const format = spec.format || 'text'
778
903
  if (format === 'list' || Array.isArray(raw)) {
779
904
  const items = (Array.isArray(raw) ? raw : [raw])
@@ -137,7 +137,7 @@ export default {
137
137
  *
138
138
  * - `field` (required) — the object field holding the array.
139
139
  * - `key` — the item field that identifies an entry (default: the item itself for strings).
140
- * - `row` — `{ title, titleFallback?, avatar?, meta?: [paths|{ path, map?, format? }], pill?: { path, map?, variantMap? }, aside?: { path, format?, prefix? }, detail?: { template, when? } }`.
140
+ * - `row` — `{ title, titleFallback?, avatar?, meta?: [paths|{ path, map?, format? }], pill?: { path, map?, variantMap? }, aside?: { path, format?, prefix?, fallback? }, detail?: { template, when? } }`.
141
141
  * - `add` — `{ label?, fields: [{ key, label, type?: 'text'|'select', options?, required?, pattern?, patternMessage? }], unique?, defaults? }`.
142
142
  * - `remove` — `{ label? }`; omitted = no remove.
143
143
  * - `rowActions` — `[{ id, label, when?, request: { method?, url, payload? } }]`; tokens `@object.<field>`, `@objectId`, `@row.<field>`.
@@ -375,7 +375,8 @@ export default {
375
375
  asideOf(entry, spec) {
376
376
  const value = this.text(entry, spec)
377
377
  if (value === '') {
378
- return ''
378
+ // No value: the optional `fallback` label, translated, instead of an empty aside.
379
+ return this.tr(spec.fallback)
379
380
  }
380
381
  return spec.prefix ? `${this.tr(spec.prefix)} ${value}` : value
381
382
  },