@conduction/nextcloud-vue 2.84.0 → 2.85.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 (166) hide show
  1. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue.js +1 -1
  2. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue2.js +131 -3
  3. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue2.js.map +1 -1
  4. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue3.js +54 -18
  5. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue3.js.map +1 -1
  6. package/dist/esm/components/CnBoardView/CnBoardView.vue.js +1 -1
  7. package/dist/esm/components/CnBoardView/CnBoardView.vue2.js +232 -4
  8. package/dist/esm/components/CnBoardView/CnBoardView.vue2.js.map +1 -1
  9. package/dist/esm/components/CnBoardView/CnBoardView.vue3.js +95 -51
  10. package/dist/esm/components/CnBoardView/CnBoardView.vue3.js.map +1 -1
  11. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue.js +1 -1
  12. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue2.js +114 -0
  13. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue2.js.map +1 -1
  14. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue3.js +2 -2
  15. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue3.js.map +1 -1
  16. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue.js +1 -1
  17. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +45 -0
  18. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  19. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +22 -12
  20. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  21. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +23 -4
  22. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  23. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  24. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue.js +1 -1
  25. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js +3 -1
  26. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
  27. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
  28. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue.js +1 -1
  29. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js +18 -5
  30. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js.map +1 -1
  31. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js.map +1 -1
  32. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +548 -77
  33. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  34. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +191 -82
  35. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  36. package/dist/esm/components/CnIndexPage/useSelfFetchList.js +11 -2
  37. package/dist/esm/components/CnIndexPage/useSelfFetchList.js.map +1 -1
  38. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue.js +1 -1
  39. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js +95 -0
  40. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js.map +1 -1
  41. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue3.js +24 -2
  42. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue3.js.map +1 -1
  43. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
  44. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +70 -2
  45. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
  46. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
  47. package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue.js +1 -1
  48. package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue2.js +67 -5
  49. package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue2.js.map +1 -1
  50. package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue3.js +181 -124
  51. package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue3.js.map +1 -1
  52. package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue.js +1 -1
  53. package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue2.js +5 -1
  54. package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue2.js.map +1 -1
  55. package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue3.js.map +1 -1
  56. package/dist/esm/components/CnStatWidget/CnStatWidget.vue.js +1 -1
  57. package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js +134 -10
  58. package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js.map +1 -1
  59. package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js +6 -2
  60. package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js.map +1 -1
  61. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue.js +1 -1
  62. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +6 -3
  63. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
  64. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
  65. package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue.js +1 -1
  66. package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue2.js +140 -7
  67. package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue2.js.map +1 -1
  68. package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue3.js.map +1 -1
  69. package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue.js +1 -1
  70. package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue2.js +37 -9
  71. package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue2.js.map +1 -1
  72. package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue3.js +6 -5
  73. package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue3.js.map +1 -1
  74. package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue.js +1 -1
  75. package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue2.js +2 -1
  76. package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue2.js.map +1 -1
  77. package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue3.js.map +1 -1
  78. package/dist/esm/components/CnWidgetCardGrid/CnWidgetCardGrid.vue2.js +53 -1
  79. package/dist/esm/components/CnWidgetCardGrid/CnWidgetCardGrid.vue2.js.map +1 -1
  80. package/dist/esm/components/CnWidgetCardGrid/CnWidgetCardGrid.vue3.js +3 -2
  81. package/dist/esm/components/CnWidgetCardGrid/CnWidgetCardGrid.vue3.js.map +1 -1
  82. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue.js +1 -1
  83. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js +135 -0
  84. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js.map +1 -1
  85. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue3.js.map +1 -1
  86. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue.js +1 -1
  87. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue2.js +19 -0
  88. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue2.js.map +1 -1
  89. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue3.js +37 -19
  90. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue3.js.map +1 -1
  91. package/dist/esm/composables/useListView.js +21 -0
  92. package/dist/esm/composables/useListView.js.map +1 -1
  93. package/dist/esm/index.js +1 -0
  94. package/dist/esm/index.js.map +1 -1
  95. package/dist/esm/l10n/en.json.js +23 -1
  96. package/dist/esm/l10n/en.json.js.map +1 -1
  97. package/dist/esm/l10n/nl.json.js +41 -1
  98. package/dist/esm/l10n/nl.json.js.map +1 -1
  99. package/dist/esm/library-style.js +1 -1
  100. package/dist/esm/nextcloud-vue.css +742 -397
  101. package/dist/esm/store/useObjectStore.js +16 -2
  102. package/dist/esm/store/useObjectStore.js.map +1 -1
  103. package/dist/esm/utils/boardColumns.js +8 -1
  104. package/dist/esm/utils/boardColumns.js.map +1 -1
  105. package/dist/esm/utils/boardReferenceColumns.js +278 -0
  106. package/dist/esm/utils/boardReferenceColumns.js.map +1 -0
  107. package/dist/esm/utils/buildManifest.js +4 -1
  108. package/dist/esm/utils/buildManifest.js.map +1 -1
  109. package/dist/esm/utils/gridStack.js +40 -1
  110. package/dist/esm/utils/gridStack.js.map +1 -1
  111. package/dist/esm/utils/humaniseKey.js +41 -0
  112. package/dist/esm/utils/humaniseKey.js.map +1 -0
  113. package/dist/esm/utils/notificationRuleLabel.js +6 -23
  114. package/dist/esm/utils/notificationRuleLabel.js.map +1 -1
  115. package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
  116. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  117. package/dist/esm/utils/widgetDispatch.js +8 -2
  118. package/dist/esm/utils/widgetDispatch.js.map +1 -1
  119. package/dist/nextcloud-vue.cjs.js +15075 -12590
  120. package/dist/nextcloud-vue.cjs.js.map +2 -2
  121. package/dist/nextcloud-vue.css +742 -397
  122. package/l10n/en.json +23 -1
  123. package/l10n/nl.json +41 -1
  124. package/package.json +1 -1
  125. package/src/components/CnAdminSettingsShell/CnAdminSettingsShell.vue +160 -8
  126. package/src/components/CnBoardView/CnBoardView.vue +262 -6
  127. package/src/components/CnDashboardGrid/CnDashboardGrid.vue +116 -2
  128. package/src/components/CnDashboardPage/CnDashboardPage.vue +57 -2
  129. package/src/components/CnDetailPage/CnDetailPage.md +1 -1
  130. package/src/components/CnDetailPage/CnDetailPage.vue +23 -4
  131. package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +3 -1
  132. package/src/components/CnFactsWidget/CnFactsWidget.vue +18 -5
  133. package/src/components/CnIndexPage/CnIndexPage.vue +586 -25
  134. package/src/components/CnIndexPage/useSelfFetchList.js +10 -1
  135. package/src/components/CnObjectDataWidget/CnObjectDataWidget.vue +123 -3
  136. package/src/components/CnRecordListWidget/CnRecordListWidget.vue +70 -2
  137. package/src/components/CnRegisterMapping/CnRegisterMapping.md +5 -0
  138. package/src/components/CnRegisterMapping/CnRegisterMapping.vue +231 -96
  139. package/src/components/CnSettingsPage/CnSettingsPage.vue +5 -1
  140. package/src/components/CnStatWidget/CnStatWidget.vue +136 -11
  141. package/src/components/CnTabsWidget/CnTabsWidget.vue +6 -3
  142. package/src/components/CnToggleTableWidget/CnToggleTableWidget.vue +140 -7
  143. package/src/components/CnVersionInfoCard/CnVersionInfoCard.md +2 -1
  144. package/src/components/CnVersionInfoCard/CnVersionInfoCard.vue +42 -13
  145. package/src/components/CnWeekStripWidget/CnWeekStripWidget.vue +2 -1
  146. package/src/components/CnWidgetCardGrid/CnWidgetCardGrid.vue +55 -2
  147. package/src/components/CnWidgetObjectTable/CnWidgetObjectTable.vue +135 -0
  148. package/src/components/CnWidgetWrapper/CnWidgetWrapper.md +10 -0
  149. package/src/components/CnWidgetWrapper/CnWidgetWrapper.vue +44 -1
  150. package/src/composables/useListView.js +21 -0
  151. package/src/css/board.css +65 -4
  152. package/src/css/dialog.css +4 -2
  153. package/src/css/index-page.css +49 -0
  154. package/src/css/kpi-card.css +13 -0
  155. package/src/css/look-board.css +96 -7
  156. package/src/index.js +2 -0
  157. package/src/schemas/app-manifest-v2.schema.json +115 -5
  158. package/src/store/useObjectStore.js +16 -2
  159. package/src/utils/boardColumns.js +8 -1
  160. package/src/utils/boardReferenceColumns.js +274 -0
  161. package/src/utils/buildManifest.js +4 -1
  162. package/src/utils/gridStack.js +39 -0
  163. package/src/utils/humaniseKey.js +38 -0
  164. package/src/utils/notificationRuleLabel.js +5 -23
  165. package/src/utils/validateManifestV2.compiled.js +3 -3
  166. package/src/utils/widgetDispatch.js +8 -2
@@ -395,6 +395,7 @@
395
395
  <CnStatsBlockWidget
396
396
  v-bind="getStatsBlockProps(item)"
397
397
  :title="getWidgetTitle(item)"
398
+ :eyebrow="getWidgetEyebrow(item)"
398
399
  :data-source="getWidgetDataSource(item)" />
399
400
  </CnWidgetWrapper>
400
401
  <CnWidgetWrapper
@@ -420,7 +421,7 @@
420
421
  :is="registryRenderer(item)"
421
422
  :widgetId="item.widgetId"
422
423
  :content="registryWidgetBindings(item)"
423
- v-bind="registryWidgetBindings(item)"
424
+ v-bind="{ ...registryWidgetBindings(item), ...containerBindings(item) }"
424
425
  @header="onWidgetHeader(item, $event)" />
425
426
  </CnWidgetWrapper>
426
427
  <CnWidgetWrapper
@@ -433,6 +434,7 @@
433
434
  <CnWidgetWrapper
434
435
  v-else
435
436
  :title="getWidgetTitle(item)"
437
+ :eyebrow="getWidgetEyebrow(item)"
436
438
  :showTitle="false"
437
439
  :showRefresh="false">
438
440
  <div class="cn-dashboard-page__unknown">
@@ -494,6 +496,7 @@
494
496
  <CnWidgetWrapper
495
497
  v-if="missingRequiredApp(item)"
496
498
  :title="getWidgetTitle(item)"
499
+ :eyebrow="getWidgetEyebrow(item)"
497
500
  :showTitle="widgetShowTitle(item)">
498
501
  <!-- Compact on purpose: a two-row tile is about 150px, less
499
502
  a title row. NcEmptyContent's 64px icon, 20px name,
@@ -532,6 +535,7 @@
532
535
  <CnWidgetWrapper
533
536
  :widgetId="item.widgetId"
534
537
  :title="getWidgetTitle(item)"
538
+ :eyebrow="getWidgetEyebrow(item)"
535
539
  :iconUrl="getWidgetIconUrl(item)"
536
540
  :iconClass="getWidgetIconClass(item)"
537
541
  :showTitle="widgetShowTitle(item)"
@@ -613,6 +617,7 @@
613
617
  :widgetId="item.widgetId"
614
618
  :class="{ 'cn-dashboard-page__chart-fit': isChartFitted(item) }"
615
619
  :title="getWidgetTitle(item)"
620
+ :eyebrow="getWidgetEyebrow(item)"
616
621
  :iconUrl="getWidgetIconUrl(item)"
617
622
  :iconClass="getWidgetIconClass(item)"
618
623
  :showTitle="widgetShowTitle(item)"
@@ -691,6 +696,7 @@
691
696
  <CnWidgetWrapper
692
697
  :widgetId="item.widgetId"
693
698
  :title="getWidgetTitle(item)"
699
+ :eyebrow="getWidgetEyebrow(item)"
694
700
  :iconUrl="getWidgetIconUrl(item)"
695
701
  :iconClass="getWidgetIconClass(item)"
696
702
  :showTitle="widgetShowTitle(item)"
@@ -718,6 +724,7 @@
718
724
  <CnWidgetWrapper
719
725
  :widgetId="item.widgetId"
720
726
  :title="getWidgetTitle(item)"
727
+ :eyebrow="getWidgetEyebrow(item)"
721
728
  :iconUrl="getWidgetIconUrl(item)"
722
729
  :iconClass="getWidgetIconClass(item)"
723
730
  :showTitle="widgetShowTitle(item)"
@@ -757,6 +764,7 @@
757
764
  <CnWidgetWrapper
758
765
  :widgetId="item.widgetId"
759
766
  :title="getWidgetTitle(item)"
767
+ :eyebrow="getWidgetEyebrow(item)"
760
768
  :iconUrl="getWidgetIconUrl(item)"
761
769
  :iconClass="getWidgetIconClass(item)"
762
770
  :showTitle="widgetShowTitle(item)"
@@ -782,6 +790,7 @@
782
790
  <CnWidgetWrapper
783
791
  :widgetId="item.widgetId"
784
792
  :title="getWidgetTitle(item)"
793
+ :eyebrow="getWidgetEyebrow(item)"
785
794
  :showTitle="widgetShowTitle(item)"
786
795
  :showActions="widgetShowActions(item)"
787
796
  :showRefresh="getWidgetShowRefresh(item)"
@@ -873,7 +882,7 @@
873
882
  :is="registryRenderer(item)"
874
883
  :widgetId="item.widgetId"
875
884
  :content="registryWidgetBindings(item)"
876
- v-bind="registryWidgetBindings(item)"
885
+ v-bind="{ ...registryWidgetBindings(item), ...containerBindings(item) }"
877
886
  @header="onWidgetHeader(item, $event)" />
878
887
  </CnWidgetWrapper>
879
888
  </template>
@@ -882,6 +891,7 @@
882
891
  <CnWidgetWrapper
883
892
  v-else
884
893
  :title="getWidgetTitle(item)"
894
+ :eyebrow="getWidgetEyebrow(item)"
885
895
  :showTitle="widgetShowTitle(item)"
886
896
  :showRefresh="false">
887
897
  <div class="cn-dashboard-page__unknown">
@@ -2944,6 +2954,7 @@ export default {
2944
2954
  return {
2945
2955
  widgetId: item.widgetId,
2946
2956
  title: this.getWidgetTitle(item),
2957
+ eyebrow: this.getWidgetEyebrow(item),
2947
2958
  showTitle: this.widgetShowTitle(item),
2948
2959
  showActions: this.widgetShowActions(item),
2949
2960
  showRefresh: this.getWidgetShowRefresh(item),
@@ -3630,6 +3641,35 @@ export default {
3630
3641
  return outcome ? { ...content, conditionOutcome: outcome } : content
3631
3642
  },
3632
3643
 
3644
+ /**
3645
+ * What a container widget (registry `container`, such as `tabs`) needs
3646
+ * beside its content: the page's widget definitions, so its tabs find
3647
+ * the widgets they name, and the consumer registry for custom child
3648
+ * types. Empty for every other widget, so a leaf gets no extra
3649
+ * attributes on its root.
3650
+ *
3651
+ * @param {object} item Layout item.
3652
+ * @return {object} The extra bindings.
3653
+ * @spec openspec/changes/dashboard-tabs-children/specs/dashboard-page/spec.md#requirement-a-tabs-widget-on-a-dashboard-renders-the-widgets-its-tabs-name
3654
+ */
3655
+ containerBindings(item) {
3656
+ const def = this.getWidgetDef(item.widgetId)
3657
+ if (!def || !def.type) {
3658
+ return {}
3659
+ }
3660
+ const entry = getWidgetTypeEntry(def.type) || getWidgetTypeEntry(canonicalWidgetType(def.type))
3661
+ if (!entry || entry.container !== true) {
3662
+ return {}
3663
+ }
3664
+ const own = Array.isArray(this.widgets) ? this.widgets.filter(Boolean) : []
3665
+ const fromView = this.activeView && Array.isArray(this.activeView.widgets) ? this.activeView.widgets.filter(Boolean) : []
3666
+ return {
3667
+ availableWidgets: [...own, ...fromView],
3668
+ cnRegistry: this.cnRegistry || {},
3669
+ surface: this.surface,
3670
+ }
3671
+ },
3672
+
3633
3673
  /**
3634
3674
  * A widget's content with the board's quiet collection footer: in the
3635
3675
  * board look a dashboard that drops the widget Actions menu
@@ -4002,6 +4042,21 @@ export default {
4002
4042
  this.showWidgetConfig = false
4003
4043
  },
4004
4044
 
4045
+ /**
4046
+ * The line above a widget's title (`eyebrow` on the widget
4047
+ * definition: "Saldo per week" above "Prognose 13 weken"), through the
4048
+ * host translate, or '' for none.
4049
+ *
4050
+ * @param {object} item Layout item.
4051
+ * @return {string}
4052
+ * @spec openspec/changes/widget-title-eyebrow/specs/dashboard-page/spec.md#requirement-a-widget-card-can-carry-a-line-above-its-title
4053
+ */
4054
+ getWidgetEyebrow(item) {
4055
+ const def = this.getWidgetDef(item.widgetId)
4056
+ const eyebrow = def && typeof def.eyebrow === 'string' ? def.eyebrow : ''
4057
+ return eyebrow ? this.effectiveTranslate(eyebrow) : ''
4058
+ },
4059
+
4005
4060
  /**
4006
4061
  * The widget's title as AUTHORED — the untranslated manifest source
4007
4062
  * string — resolved by widget id rather than by item, so a descendant
@@ -460,4 +460,4 @@ When the schema-driven fetch answers 404 the page shows a not-found state whose
460
460
 
461
461
  ### The board look
462
462
 
463
- `look` (String, default `''`): `nextcloud` or `board`; empty takes the app's look. `headerMeta` (String): the meta line on row 2 of the board header, a field template such as `via {channel}` (manifest `config.headerMeta`). `tabsLabel` (String): the accessible name of the tab list (manifest `config.tabsLabel`). `identifierField` (String): the field named in the last line of the History side card (manifest `config.identifierField`). See `docs/components/cn-detail-page.md`.
463
+ `look` (String, default `''`): `nextcloud` or `board`; empty takes the app's look. `headerMeta` (String): the meta line on row 2 of the board header, a field template such as `via {channel}` (manifest `config.headerMeta`). `tabsLabel` (String): the accessible name of the tab list (manifest `config.tabsLabel`). `identifierField` (String): the field named in the last line of the History side card (manifest `config.identifierField`). `titleField` (String): the record field the header names the record by, such as an invoice number (manifest `config.titleField`); an empty value falls back to the display name. See `docs/components/cn-detail-page.md`.
@@ -2664,6 +2664,19 @@ export default {
2664
2664
  type: String,
2665
2665
  default: '',
2666
2666
  },
2667
+
2668
+ /**
2669
+ * The record field the header names the record by (manifest
2670
+ * `config.titleField`, a dotted path such as `number`), as an invoice
2671
+ * page names the invoice number. An empty value falls back to the
2672
+ * display name. Empty (the default) changes nothing.
2673
+ *
2674
+ * @spec openspec/changes/detail-title-field/specs/detail-page/spec.md#requirement-a-detail-page-can-name-the-record-by-a-field
2675
+ */
2676
+ titleField: {
2677
+ type: String,
2678
+ default: '',
2679
+ },
2667
2680
  },
2668
2681
 
2669
2682
  emits: [
@@ -4102,10 +4115,10 @@ export default {
4102
4115
  },
4103
4116
 
4104
4117
  /**
4105
- * The resolved record's human display name, drawn from the loaded
4106
- * object via the same fallback chain the relation resolver uses:
4107
- * `@self.name` → `name` / `title` / `displayName` → `firstName
4108
- * lastName`. Empty string while the object is still loading or when no
4118
+ * The resolved record's human display name: the `titleField` value
4119
+ * when the page names one and it is filled, else the same fallback
4120
+ * chain the relation resolver uses: `@self.name` → `name` / `title` /
4121
+ * `displayName` → `firstName lastName`. Empty string while the object is still loading or when no
4109
4122
  * name-like field exists (a raw id is never used as a name — ADR-062).
4110
4123
  *
4111
4124
  * @return {string}
@@ -4115,6 +4128,12 @@ export default {
4115
4128
  if (!obj || typeof obj !== 'object') {
4116
4129
  return ''
4117
4130
  }
4131
+ if (this.titleField) {
4132
+ const named = stageOf(obj, this.titleField)
4133
+ if (named !== '') {
4134
+ return named
4135
+ }
4136
+ }
4118
4137
  const self = obj['@self'] || {}
4119
4138
  const id = this.objectId !== null && this.objectId !== undefined ? String(this.objectId) : ''
4120
4139
  const candidates = [self.name, self.title, obj.name, obj.title, obj.displayName]
@@ -725,7 +725,9 @@ export default {
725
725
  if (this.widget && this.widget.type === 'files' && !(this.cnRegistry && this.cnRegistry.files) && this.register && this.schema) {
726
726
  return CnObjectFilesWidget
727
727
  }
728
- return resolveRegistryRenderer(this.widget, this.cnRegistry)
728
+ // Inside a dashboard's tabs the dashboard's resolution holds.
729
+ const onDashboard = this.surface === 'app-dashboard' || this.surface === 'user-dashboard'
730
+ return resolveRegistryRenderer(this.widget, this.cnRegistry, { typeAsWritten: onDashboard })
729
731
  },
730
732
 
731
733
  /**
@@ -145,6 +145,7 @@ import { dispatchAction } from '../../utils/actionsDispatcher.js'
145
145
  import { isBadgeVariant } from '../../utils/badgeVariants.js'
146
146
  import { formatBoardDate } from '../../utils/boardDate.js'
147
147
  import { parseDateValue } from '../../utils/dateVariant.js'
148
+ import { formatMoney, unwrapAppConfig } from '../../utils/formatMetric.js'
148
149
  import { readPath } from '../../utils/readPath.js'
149
150
  import { hasUnresolvedDeepTokens, resolveDeepTokens } from '../../utils/resolveFilterTokens.js'
150
151
 
@@ -246,6 +247,8 @@ export default {
246
247
  cnTranslate: { default: () => (key) => key },
247
248
  cnObjectContext: { default: null },
248
249
  cnWorkspaceContext: { default: null },
250
+ /** The page's app config; its `currency` is the reporting currency a money value falls back to. */
251
+ cnAppConfig: { default: null },
249
252
  /** Pre-bound action dispatcher of CnPageRenderer, for `action` / `headerAction`. */
250
253
  cnDispatchAction: { default: null },
251
254
  },
@@ -260,7 +263,7 @@ export default {
260
263
  * A part may be `{ text }`, a constant; a `line` or `text` entry without `path` or `parts` shows its `text`.
261
264
  * `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. A resolved value works as a pill too: `map` is applied to the resolved label, and `variantMap` / `colorMap` look the tone up by the value of `resolve.toneField` on the referenced object when that is set (a status named by its `name`, coloured by its `state`), then by the resolved label, then by the stored value.
262
265
  * - `action` — an action drawn as a button under the facts; `headerAction` — one drawn in the card header (reported through the `header` event).
263
- * `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`).
266
+ * `format` is `text` (default), `date` (board short date), `date-long` (always with the year), `list` (an array joined by ", ", each item read at `itemPath`) or `currency` (a money amount in the reader's language, "€ 1.234,50": the code in `currency`, else the record's `currencyField`, else the app's reporting currency, else EUR; `decimals` defaults to 2).
264
267
  * `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.
265
268
  * - `source` — `{ register, schema, id }` or `{ url }`; omitted = the current object.
266
269
  * - `link` — `{ label, route? | href? }` at the foot.
@@ -900,7 +903,7 @@ export default {
900
903
  */
901
904
  valueOf(record, field) {
902
905
  const raw = readPath(record, field.path)
903
- return this.format(raw, field)
906
+ return this.format(raw, field, record)
904
907
  },
905
908
 
906
909
  /**
@@ -919,7 +922,7 @@ export default {
919
922
  if (!spec.path && typeof spec.text === 'string') {
920
923
  return this.tr(spec.text)
921
924
  }
922
- const value = this.format(readPath(record, spec.path), spec)
925
+ const value = this.format(readPath(record, spec.path), spec, record)
923
926
  if (value === '') {
924
927
  return ''
925
928
  }
@@ -933,10 +936,12 @@ export default {
933
936
  * Format a raw value.
934
937
  *
935
938
  * @param {unknown} raw The value.
936
- * @param {object} spec `{ format?, map?, itemPath? }`.
939
+ * @param {object} spec `{ format?, map?, itemPath?, currency?, currencyField?, decimals? }`.
940
+ * @param {object|null} [record] The object shown, which `currencyField` reads.
937
941
  * @return {string} The text, '' when empty.
942
+ * @spec openspec/changes/facts-currency-format/specs/facts-widget/spec.md#requirement-a-facts-entry-can-show-a-money-amount
938
943
  */
939
- format(raw, spec) {
944
+ format(raw, spec, record = null) {
940
945
  if (raw === null || raw === undefined || raw === '') {
941
946
  return ''
942
947
  }
@@ -961,6 +966,14 @@ export default {
961
966
  ? formatBoardDate(date, new Date(date.getFullYear() + 1, 0, 1))
962
967
  : formatBoardDate(date)
963
968
  }
969
+ if (format === 'currency') {
970
+ const money = formatMoney(raw, { currency: spec.currency, currencyField: spec.currencyField, decimals: spec.decimals }, record, unwrapAppConfig(this.cnAppConfig))
971
+ if (money !== '') {
972
+ return money
973
+ }
974
+ // Not a number: shown as written, never as "[object Object]".
975
+ return typeof raw === 'object' ? '' : String(raw)
976
+ }
964
977
  if (typeof raw === 'object') {
965
978
  const label = raw.title || raw.name || raw.label || raw.displayName
966
979
  return typeof label === 'string' ? label : ''