@conduction/nextcloud-vue 2.83.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 (246) 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/CnAuditTrailCard/CnAuditTrailCard.vue.js +1 -1
  7. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue2.js +1 -1
  8. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue2.js.map +1 -1
  9. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue3.js.map +1 -1
  10. package/dist/esm/components/CnBoardView/CnBoardView.vue.js +1 -1
  11. package/dist/esm/components/CnBoardView/CnBoardView.vue2.js +232 -4
  12. package/dist/esm/components/CnBoardView/CnBoardView.vue2.js.map +1 -1
  13. package/dist/esm/components/CnBoardView/CnBoardView.vue3.js +95 -51
  14. package/dist/esm/components/CnBoardView/CnBoardView.vue3.js.map +1 -1
  15. package/dist/esm/components/CnBodySections/CnBodySections.vue.js +1 -1
  16. package/dist/esm/components/CnBodySections/CnBodySections.vue2.js +22 -1
  17. package/dist/esm/components/CnBodySections/CnBodySections.vue2.js.map +1 -1
  18. package/dist/esm/components/CnBodySections/CnBodySections.vue3.js.map +1 -1
  19. package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue.js +1 -1
  20. package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue2.js +47 -4
  21. package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue2.js.map +1 -1
  22. package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue3.js.map +1 -1
  23. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue.js +1 -1
  24. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue2.js +239 -3
  25. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue2.js.map +1 -1
  26. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue3.js +5 -5
  27. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue3.js.map +1 -1
  28. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue.js +1 -1
  29. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +50 -0
  30. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  31. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +22 -12
  32. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  33. package/dist/esm/components/CnDeltaWidget/CnDeltaWidget.vue.js +1 -1
  34. package/dist/esm/components/CnDeltaWidget/CnDeltaWidget.vue2.js +1 -1
  35. package/dist/esm/components/CnDeltaWidget/CnDeltaWidget.vue2.js.map +1 -1
  36. package/dist/esm/components/CnDeltaWidget/CnDeltaWidget.vue3.js.map +1 -1
  37. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +226 -8
  38. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  39. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  40. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue.js +1 -1
  41. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js +3 -1
  42. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
  43. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
  44. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue.js +1 -1
  45. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js +18 -5
  46. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js.map +1 -1
  47. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js.map +1 -1
  48. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue.js +1 -1
  49. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js +38 -1
  50. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js.map +1 -1
  51. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js +45 -5
  52. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js.map +1 -1
  53. package/dist/esm/components/CnGaugeWidget/CnGaugeWidget.vue.js +1 -1
  54. package/dist/esm/components/CnGaugeWidget/CnGaugeWidget.vue2.js +1 -1
  55. package/dist/esm/components/CnGaugeWidget/CnGaugeWidget.vue2.js.map +1 -1
  56. package/dist/esm/components/CnGaugeWidget/CnGaugeWidget.vue3.js.map +1 -1
  57. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +548 -77
  58. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  59. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +191 -82
  60. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  61. package/dist/esm/components/CnIndexPage/useSelfFetchList.js +11 -2
  62. package/dist/esm/components/CnIndexPage/useSelfFetchList.js.map +1 -1
  63. package/dist/esm/components/CnInteractionFormWidget/CnInteractionFormWidget.vue2.js +29 -6
  64. package/dist/esm/components/CnInteractionFormWidget/CnInteractionFormWidget.vue2.js.map +1 -1
  65. package/dist/esm/components/CnInteractionFormWidget/CnInteractionFormWidget.vue3.js.map +1 -1
  66. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue.js +1 -1
  67. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js +126 -2
  68. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js.map +1 -1
  69. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue3.js +24 -2
  70. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue3.js.map +1 -1
  71. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
  72. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +106 -3
  73. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
  74. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
  75. package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue.js +1 -1
  76. package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue2.js +67 -5
  77. package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue2.js.map +1 -1
  78. package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue3.js +181 -124
  79. package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue3.js.map +1 -1
  80. package/dist/esm/components/CnReportsPage/CnReportsPage.vue.js +1 -1
  81. package/dist/esm/components/CnReportsPage/CnReportsPage.vue2.js +135 -2
  82. package/dist/esm/components/CnReportsPage/CnReportsPage.vue2.js.map +1 -1
  83. package/dist/esm/components/CnReportsPage/CnReportsPage.vue3.js +95 -43
  84. package/dist/esm/components/CnReportsPage/CnReportsPage.vue3.js.map +1 -1
  85. package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue.js +1 -1
  86. package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue2.js +5 -1
  87. package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue2.js.map +1 -1
  88. package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue3.js.map +1 -1
  89. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue.js +1 -1
  90. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue2.js +72 -5
  91. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue2.js.map +1 -1
  92. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue3.js +33 -8
  93. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue3.js.map +1 -1
  94. package/dist/esm/components/CnStatWidget/CnStatWidget.vue.js +1 -1
  95. package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js +135 -11
  96. package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js.map +1 -1
  97. package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js +6 -2
  98. package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js.map +1 -1
  99. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue.js +1 -1
  100. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue2.js +17 -2
  101. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue2.js.map +1 -1
  102. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue3.js +1 -1
  103. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue3.js.map +1 -1
  104. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue.js +1 -1
  105. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +39 -6
  106. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
  107. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
  108. package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue.js +1 -1
  109. package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue2.js +140 -7
  110. package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue2.js.map +1 -1
  111. package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue3.js.map +1 -1
  112. package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue.js +1 -1
  113. package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue2.js +37 -9
  114. package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue2.js.map +1 -1
  115. package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue3.js +6 -5
  116. package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue3.js.map +1 -1
  117. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue.js +1 -1
  118. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue2.js +16 -0
  119. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue2.js.map +1 -1
  120. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue3.js.map +1 -1
  121. package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue.js +1 -1
  122. package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue2.js +2 -1
  123. package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue2.js.map +1 -1
  124. package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue3.js.map +1 -1
  125. package/dist/esm/components/CnWidgetCardGrid/CnWidgetCardGrid.vue2.js +53 -1
  126. package/dist/esm/components/CnWidgetCardGrid/CnWidgetCardGrid.vue2.js.map +1 -1
  127. package/dist/esm/components/CnWidgetCardGrid/CnWidgetCardGrid.vue3.js +3 -2
  128. package/dist/esm/components/CnWidgetCardGrid/CnWidgetCardGrid.vue3.js.map +1 -1
  129. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue.js +1 -1
  130. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js +135 -0
  131. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js.map +1 -1
  132. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue3.js.map +1 -1
  133. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue.js +1 -1
  134. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue2.js +19 -0
  135. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue2.js.map +1 -1
  136. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue3.js +37 -19
  137. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue3.js.map +1 -1
  138. package/dist/esm/composables/useListView.js +21 -0
  139. package/dist/esm/composables/useListView.js.map +1 -1
  140. package/dist/esm/dialogs/CnTransitionInputDialog.vue.js +1 -1
  141. package/dist/esm/dialogs/CnTransitionInputDialog.vue2.js +68 -6
  142. package/dist/esm/dialogs/CnTransitionInputDialog.vue2.js.map +1 -1
  143. package/dist/esm/dialogs/CnTransitionInputDialog.vue3.js +133 -105
  144. package/dist/esm/dialogs/CnTransitionInputDialog.vue3.js.map +1 -1
  145. package/dist/esm/index.js +1 -0
  146. package/dist/esm/index.js.map +1 -1
  147. package/dist/esm/l10n/en.json.js +38 -1
  148. package/dist/esm/l10n/en.json.js.map +1 -1
  149. package/dist/esm/l10n/nl.json.js +78 -1
  150. package/dist/esm/l10n/nl.json.js.map +1 -1
  151. package/dist/esm/library-style.js +1 -1
  152. package/dist/esm/nextcloud-vue.css +1030 -533
  153. package/dist/esm/store/useObjectStore.js +16 -2
  154. package/dist/esm/store/useObjectStore.js.map +1 -1
  155. package/dist/esm/utils/boardColumns.js +8 -1
  156. package/dist/esm/utils/boardColumns.js.map +1 -1
  157. package/dist/esm/utils/boardReferenceColumns.js +278 -0
  158. package/dist/esm/utils/boardReferenceColumns.js.map +1 -0
  159. package/dist/esm/utils/buildManifest.js +41 -4
  160. package/dist/esm/utils/buildManifest.js.map +1 -1
  161. package/dist/esm/utils/detailWidgetVisibility.js +149 -0
  162. package/dist/esm/utils/detailWidgetVisibility.js.map +1 -0
  163. package/dist/esm/utils/formatMetric.js +39 -3
  164. package/dist/esm/utils/formatMetric.js.map +1 -1
  165. package/dist/esm/utils/gridStack.js +40 -1
  166. package/dist/esm/utils/gridStack.js.map +1 -1
  167. package/dist/esm/utils/humaniseKey.js +41 -0
  168. package/dist/esm/utils/humaniseKey.js.map +1 -0
  169. package/dist/esm/utils/notificationRuleLabel.js +6 -23
  170. package/dist/esm/utils/notificationRuleLabel.js.map +1 -1
  171. package/dist/esm/utils/resolveManifestTokens.js +1 -0
  172. package/dist/esm/utils/resolveManifestTokens.js.map +1 -1
  173. package/dist/esm/utils/sentinelTokens.js +9 -0
  174. package/dist/esm/utils/sentinelTokens.js.map +1 -1
  175. package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
  176. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  177. package/dist/esm/utils/widgetDispatch.js +8 -2
  178. package/dist/esm/utils/widgetDispatch.js.map +1 -1
  179. package/dist/nextcloud-vue.cjs.js +17159 -13422
  180. package/dist/nextcloud-vue.cjs.js.map +2 -2
  181. package/dist/nextcloud-vue.css +1030 -533
  182. package/l10n/en.json +38 -1
  183. package/l10n/nl.json +78 -1
  184. package/package.json +1 -1
  185. package/src/components/CnAdminSettingsShell/CnAdminSettingsShell.vue +160 -8
  186. package/src/components/CnAuditTrailCard/CnAuditTrailCard.vue +1 -1
  187. package/src/components/CnBoardView/CnBoardView.vue +262 -6
  188. package/src/components/CnBodySections/CnBodySections.vue +22 -1
  189. package/src/components/CnCountdownWidget/CnCountdownWidget.vue +47 -4
  190. package/src/components/CnDashboardGrid/CnDashboardGrid.vue +244 -8
  191. package/src/components/CnDashboardPage/CnDashboardPage.vue +62 -2
  192. package/src/components/CnDeltaWidget/CnDeltaWidget.vue +1 -1
  193. package/src/components/CnDetailPage/CnDetailPage.md +3 -1
  194. package/src/components/CnDetailPage/CnDetailPage.vue +226 -8
  195. package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +3 -1
  196. package/src/components/CnFactsWidget/CnFactsWidget.vue +18 -5
  197. package/src/components/CnFieldListWidget/CnFieldListWidget.vue +63 -1
  198. package/src/components/CnGaugeWidget/CnGaugeWidget.vue +1 -1
  199. package/src/components/CnIndexPage/CnIndexPage.vue +586 -25
  200. package/src/components/CnIndexPage/useSelfFetchList.js +10 -1
  201. package/src/components/CnInteractionFormWidget/CnInteractionFormWidget.vue +29 -6
  202. package/src/components/CnObjectDataWidget/CnObjectDataWidget.md +1 -1
  203. package/src/components/CnObjectDataWidget/CnObjectDataWidget.vue +154 -5
  204. package/src/components/CnRecordListWidget/CnRecordListWidget.vue +106 -3
  205. package/src/components/CnRegisterMapping/CnRegisterMapping.md +5 -0
  206. package/src/components/CnRegisterMapping/CnRegisterMapping.vue +231 -96
  207. package/src/components/CnReportsPage/CnReportsPage.vue +179 -1
  208. package/src/components/CnSettingsPage/CnSettingsPage.vue +5 -1
  209. package/src/components/CnStagesWidget/CnStagesWidget.vue +103 -6
  210. package/src/components/CnStatWidget/CnStatWidget.vue +137 -12
  211. package/src/components/CnStatsBlockWidget/CnStatsBlockWidget.vue +34 -3
  212. package/src/components/CnTabsWidget/CnTabsWidget.vue +39 -6
  213. package/src/components/CnToggleTableWidget/CnToggleTableWidget.vue +140 -7
  214. package/src/components/CnVersionInfoCard/CnVersionInfoCard.md +2 -1
  215. package/src/components/CnVersionInfoCard/CnVersionInfoCard.vue +42 -13
  216. package/src/components/CnWalkthrough/CnWalkthrough.vue +16 -0
  217. package/src/components/CnWeekStripWidget/CnWeekStripWidget.vue +2 -1
  218. package/src/components/CnWidgetCardGrid/CnWidgetCardGrid.vue +55 -2
  219. package/src/components/CnWidgetObjectTable/CnWidgetObjectTable.vue +135 -0
  220. package/src/components/CnWidgetWrapper/CnWidgetWrapper.md +10 -0
  221. package/src/components/CnWidgetWrapper/CnWidgetWrapper.vue +44 -1
  222. package/src/composables/useListView.js +21 -0
  223. package/src/css/board.css +65 -4
  224. package/src/css/dialog.css +4 -2
  225. package/src/css/form-field.css +14 -0
  226. package/src/css/form-widget.css +11 -0
  227. package/src/css/index-page.css +49 -0
  228. package/src/css/kpi-card.css +13 -0
  229. package/src/css/look-board-reports.css +58 -0
  230. package/src/css/look-board.css +108 -7
  231. package/src/dialogs/CnTransitionInputDialog.vue +87 -7
  232. package/src/index.js +2 -0
  233. package/src/schemas/app-manifest-v2.schema.json +137 -6
  234. package/src/store/useObjectStore.js +16 -2
  235. package/src/utils/boardColumns.js +8 -1
  236. package/src/utils/boardReferenceColumns.js +274 -0
  237. package/src/utils/buildManifest.js +40 -3
  238. package/src/utils/detailWidgetVisibility.js +145 -0
  239. package/src/utils/formatMetric.js +38 -2
  240. package/src/utils/gridStack.js +39 -0
  241. package/src/utils/humaniseKey.js +38 -0
  242. package/src/utils/notificationRuleLabel.js +5 -23
  243. package/src/utils/resolveManifestTokens.js +1 -0
  244. package/src/utils/sentinelTokens.js +9 -0
  245. package/src/utils/validateManifestV2.compiled.js +3 -3
  246. package/src/utils/widgetDispatch.js +8 -2
@@ -1253,6 +1253,7 @@ import {
1253
1253
  stageEntry,
1254
1254
  stageOf,
1255
1255
  } from '../../utils/detailActionModel.js'
1256
+ import { detailWidgetVisible, fetchVisibilityRows, visibilityContextRefs, widgetConditionData } from '../../utils/detailWidgetVisibility.js'
1256
1257
  import { reportBindingProblems } from '../../utils/diagnostics.js'
1257
1258
  import { fetchListTotal } from '../../utils/fetchFilterCounts.js'
1258
1259
  import { cnGridCellStyle, hasGridRow } from '../../utils/grid.js'
@@ -1260,6 +1261,7 @@ import { isActivityWidget, resolveHeaderMeta } from '../../utils/headerMeta.js'
1260
1261
  import { patchStoredSelf } from '../../utils/patchStoredSelf.js'
1261
1262
  import { setReadState } from '../../utils/recordInteractions.js'
1262
1263
  import { slotRenders } from '../../utils/slotContent.js'
1264
+ import { evaluateVisibleWhen, isLocallyDecidableVisibleWhen } from '../../utils/visibleWhen.js'
1263
1265
  import {
1264
1266
  isCardWidgetDef,
1265
1267
  isContentOnlyWidgetDef,
@@ -1512,8 +1514,8 @@ export default {
1512
1514
 
1513
1515
  /**
1514
1516
  * Offer the board look's tab list name to the tabs widget below this page
1515
- * (`config.tabsLabel`, default "<type> parts"). A getter object, so the
1516
- * widget reads it live.
1517
+ * (`config.tabsLabel`, default "<type> parts"), and the records
1518
+ * `visibilityContext` names. Getter objects, so the widget reads them live.
1517
1519
  *
1518
1520
  * @return {object} The provided values.
1519
1521
  * @spec openspec/changes/screens-detail-page-parity/specs/detail-page-board-look/spec.md#requirement-folder-tabs-take-the-board-strip
@@ -1521,6 +1523,7 @@ export default {
1521
1523
  provide() {
1522
1524
  const isBoard = computed(() => this.isBoard)
1523
1525
  const tabsLabel = computed(() => this.resolvedTabsLabel)
1526
+ const visibilityRows = computed(() => (this.visibilityRowsPending ? null : this.visibilityRows))
1524
1527
  return {
1525
1528
  // The page's own look reaches the tabs, the cards and the activity
1526
1529
  // below it, also when it came from the `look` prop and not the app.
@@ -1530,6 +1533,15 @@ export default {
1530
1533
  return tabsLabel.value
1531
1534
  },
1532
1535
  },
1536
+
1537
+ // The records `visibilityContext` names, keyed `@context.<name>`, so a tab's
1538
+ // `visibleWhen` reads the case type as a grid widget's does. Null
1539
+ // while they are on their way.
1540
+ cnDetailVisibilityRows: {
1541
+ get value() {
1542
+ return visibilityRows.value
1543
+ },
1544
+ },
1533
1545
  }
1534
1546
  },
1535
1547
 
@@ -2572,6 +2584,25 @@ export default {
2572
2584
  default: '',
2573
2585
  },
2574
2586
 
2587
+ /**
2588
+ * Records the page reads beside the loaded one, so a `visibleWhen`
2589
+ * can ask about them (manifest `config.visibilityContext`). Each entry
2590
+ * is `{ <name>: { field, register, schema } }`: `field` is the
2591
+ * property of the loaded record holding the other record's id, and a
2592
+ * condition reads that record as `@context.<name>.<path>`. A case page that
2593
+ * declares `{ caseType: { field: "caseType", register: "dossiq",
2594
+ * schema: "caseType" } }` can show a widget for some case types only
2595
+ * with `{ field: "@context.caseType.category", op: "in", value: [...] }`.
2596
+ * A record that cannot be read is absent, so its condition fails safe.
2597
+ * Empty (the default) reads nothing extra.
2598
+ *
2599
+ * @spec openspec/changes/detail-widgets-visible-when/specs/detail-widgets-visible-when/spec.md#requirement-a-detail-page-can-load-records-a-condition-names
2600
+ */
2601
+ visibilityContext: {
2602
+ type: Object,
2603
+ default: () => ({}),
2604
+ },
2605
+
2575
2606
  /**
2576
2607
  * Whether the current user administers this instance. Shows the
2577
2608
  * `adminOnly` header actions. `null` (the default) reads it from
@@ -2633,6 +2664,19 @@ export default {
2633
2664
  type: String,
2634
2665
  default: '',
2635
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
+ },
2636
2680
  },
2637
2681
 
2638
2682
  emits: [
@@ -2799,6 +2843,19 @@ export default {
2799
2843
 
2800
2844
  data() {
2801
2845
  return {
2846
+ /**
2847
+ * The records `visibilityContext` names, keyed `@context.<name>`: the row,
2848
+ * `null` when it could not be read, absent while it is on its way.
2849
+ */
2850
+ visibilityRows: {},
2851
+ /** The `visibilityRefsKey` the rows in `visibilityRows` were read for. */
2852
+ visibilityRowsFor: '',
2853
+ /** Sequence of the last `visibilityContext` read, so a late answer for another record is dropped. */
2854
+ visibilityRowsSeq: 0,
2855
+ /** The answers of endpoint and source `visibleWhen` conditions on grid widgets, by widget id. */
2856
+ widgetConditionAnswers: {},
2857
+ /** Sequence of the last remote condition round. */
2858
+ widgetConditionSeq: 0,
2802
2859
  /** The counts of the header's count pills (`countFrom`), by pill key (`type`, `status`). */
2803
2860
  pillCounts: {},
2804
2861
  /** Sequence of the last pill count round, so a late answer does not overwrite a newer one. */
@@ -3058,11 +3115,17 @@ export default {
3058
3115
  * because a keyboard user who cannot see the page needs to know what
3059
3116
  * they are being offered before they take it.
3060
3117
  *
3118
+ * One library string with a placeholder, not "Skip to" from the host's
3119
+ * catalogue glued to the label: no app carried that fragment, so every
3120
+ * Dutch reader got "Skip to In behandeling nemen" (Zuiddrecht live
3121
+ * round 3), and a fragment cannot be put in Dutch word order anyway.
3122
+ *
3061
3123
  * @return {string} The link text.
3062
3124
  * @spec openspec/changes/case-page-and-list-as-a-place/specs/index-page/spec.md
3125
+ * @spec openspec/changes/detail-page-strings-dutch/specs/detail-page-strings-dutch/spec.md#requirement-the-detail-page-speaks-the-readers-language
3063
3126
  */
3064
3127
  skipLinkLabel() {
3065
- return `${this.effectiveTranslate('Skip to')} ${this.effectivePrimaryLabel}`
3128
+ return t('nextcloud-vue', 'Skip to {action}', { action: this.effectivePrimaryLabel })
3066
3129
  },
3067
3130
 
3068
3131
  // ── The action model (detail-action-model-and-case-surfaces) ────
@@ -3904,6 +3967,68 @@ export default {
3904
3967
  return store.objects?.[this.objectType]?.[this.objectId] ?? null
3905
3968
  },
3906
3969
 
3970
+ /**
3971
+ * The records `visibilityContext` names for the loaded record.
3972
+ *
3973
+ * @return {Array<{key: string, register: string, schema: string, id: string}>}
3974
+ * @spec openspec/changes/detail-widgets-visible-when/specs/detail-widgets-visible-when/spec.md#requirement-a-detail-page-can-load-records-a-condition-names
3975
+ */
3976
+ visibilityRefs() {
3977
+ return visibilityContextRefs(this.visibilityContext, this.resolvedObject)
3978
+ },
3979
+
3980
+ /**
3981
+ * Whether a record `visibilityContext` names is still on its way.
3982
+ *
3983
+ * @return {boolean}
3984
+ * @spec openspec/changes/detail-widgets-visible-when/specs/detail-widgets-visible-when/spec.md#requirement-a-detail-page-can-load-records-a-condition-names
3985
+ */
3986
+ visibilityRowsPending() {
3987
+ return this.visibilityRefs.length > 0 && this.visibilityRowsFor !== this.visibilityRefsKey
3988
+ },
3989
+
3990
+ /**
3991
+ * The named references as one key, so another record (or another case
3992
+ * type on the same record) reads them again.
3993
+ *
3994
+ * @return {string}
3995
+ * @spec openspec/changes/detail-widgets-visible-when/specs/detail-widgets-visible-when/spec.md#requirement-a-detail-page-can-load-records-a-condition-names
3996
+ */
3997
+ visibilityRefsKey() {
3998
+ return JSON.stringify(this.visibilityRefs)
3999
+ },
4000
+
4001
+ /**
4002
+ * What a grid widget's `visibleWhen` reads: the record, with the
4003
+ * `visibilityContext` records under `@context.<name>`. Null until the record loads.
4004
+ *
4005
+ * @return {object|null}
4006
+ * @spec openspec/changes/detail-widgets-visible-when/specs/detail-widgets-visible-when/spec.md#requirement-a-detail-grid-widget-shows-only-while-its-visiblewhen-holds-for-the-record
4007
+ */
4008
+ widgetVisibilityData() {
4009
+ return widgetConditionData(this.resolvedObject, this.visibilityRows)
4010
+ },
4011
+
4012
+ /**
4013
+ * The grid widgets whose `visibleWhen` needs a request (endpoint or
4014
+ * source), with what they ask, as one key: a change re-asks them.
4015
+ *
4016
+ * @return {string}
4017
+ * @spec openspec/changes/detail-widgets-visible-when/specs/detail-widgets-visible-when/spec.md#requirement-a-detail-grid-widget-shows-only-while-its-visiblewhen-holds-for-the-record
4018
+ */
4019
+ remoteWidgetConditionsKey() {
4020
+ if (!this.widgetVisibilityData || this.visibilityRowsPending) {
4021
+ return ''
4022
+ }
4023
+ const defs = [...(this.bodyGridWidgets || []), ...((this.activeView && this.activeView.widgets) || [])]
4024
+ const remote = defs.filter((def) => def && def.visibleWhen && typeof def.visibleWhen === 'object'
4025
+ && !isLocallyDecidableVisibleWhen(def.visibleWhen))
4026
+ if (remote.length === 0) {
4027
+ return ''
4028
+ }
4029
+ return JSON.stringify([String(this.objectId || ''), remote.map((def) => [def.id, def.visibleWhen])])
4030
+ },
4031
+
3907
4032
  /**
3908
4033
  * The `@self` block of the resolved object (interaction markers live here).
3909
4034
  *
@@ -3990,10 +4115,10 @@ export default {
3990
4115
  },
3991
4116
 
3992
4117
  /**
3993
- * The resolved record's human display name, drawn from the loaded
3994
- * object via the same fallback chain the relation resolver uses:
3995
- * `@self.name` → `name` / `title` / `displayName` → `firstName
3996
- * 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
3997
4122
  * name-like field exists (a raw id is never used as a name — ADR-062).
3998
4123
  *
3999
4124
  * @return {string}
@@ -4003,6 +4128,12 @@ export default {
4003
4128
  if (!obj || typeof obj !== 'object') {
4004
4129
  return ''
4005
4130
  }
4131
+ if (this.titleField) {
4132
+ const named = stageOf(obj, this.titleField)
4133
+ if (named !== '') {
4134
+ return named
4135
+ }
4136
+ }
4006
4137
  const self = obj['@self'] || {}
4007
4138
  const id = this.objectId !== null && this.objectId !== undefined ? String(this.objectId) : ''
4008
4139
  const candidates = [self.name, self.title, obj.name, obj.title, obj.displayName]
@@ -4436,6 +4567,22 @@ export default {
4436
4567
  },
4437
4568
 
4438
4569
  watch: {
4570
+ // The records a condition names are read again for another record.
4571
+ visibilityRefsKey: {
4572
+ immediate: true,
4573
+ handler() {
4574
+ this.refreshVisibilityRows()
4575
+ },
4576
+ },
4577
+
4578
+ // Endpoint and source conditions on grid widgets are asked once per record.
4579
+ remoteWidgetConditionsKey: {
4580
+ immediate: true,
4581
+ handler() {
4582
+ this.refreshWidgetConditions()
4583
+ },
4584
+ },
4585
+
4439
4586
  // The header's count pills count again for another record or other lists.
4440
4587
  pillCountKey: {
4441
4588
  immediate: true,
@@ -5674,6 +5821,62 @@ export default {
5674
5821
  || undefined
5675
5822
  },
5676
5823
 
5824
+ /**
5825
+ * Read the records `visibilityContext` names for the loaded record. A
5826
+ * late answer for a record the page has left is dropped.
5827
+ *
5828
+ * @return {Promise<void>}
5829
+ * @spec openspec/changes/detail-widgets-visible-when/specs/detail-widgets-visible-when/spec.md#requirement-a-detail-page-can-load-records-a-condition-names
5830
+ */
5831
+ async refreshVisibilityRows() {
5832
+ const key = this.visibilityRefsKey
5833
+ const refs = this.visibilityRefs
5834
+ const seq = ++this.visibilityRowsSeq
5835
+ if (refs.length === 0) {
5836
+ // Write only on a change, so a page without `visibilityContext`
5837
+ // never touches the state its layout could read.
5838
+ if (Object.keys(this.visibilityRows).length > 0) {
5839
+ this.visibilityRows = {}
5840
+ }
5841
+ if (this.visibilityRowsFor !== key) {
5842
+ this.visibilityRowsFor = key
5843
+ }
5844
+ return
5845
+ }
5846
+ if (!this._visibilityRowCache) {
5847
+ this._visibilityRowCache = new Map()
5848
+ }
5849
+ const rows = await fetchVisibilityRows(refs, this._visibilityRowCache)
5850
+ if (seq !== this.visibilityRowsSeq) {
5851
+ return
5852
+ }
5853
+ this.visibilityRows = rows
5854
+ this.visibilityRowsFor = key
5855
+ },
5856
+
5857
+ /**
5858
+ * Ask the endpoint and source `visibleWhen` conditions of the grid
5859
+ * widgets, once per record. Each is fail-safe: an error hides the widget.
5860
+ *
5861
+ * @return {Promise<void>}
5862
+ * @spec openspec/changes/detail-widgets-visible-when/specs/detail-widgets-visible-when/spec.md#requirement-a-detail-grid-widget-shows-only-while-its-visiblewhen-holds-for-the-record
5863
+ */
5864
+ async refreshWidgetConditions() {
5865
+ const seq = ++this.widgetConditionSeq
5866
+ if (this.remoteWidgetConditionsKey === '') {
5867
+ return
5868
+ }
5869
+ const [, entries] = JSON.parse(this.remoteWidgetConditionsKey)
5870
+ const ctx = { objectId: this.objectId, object: this.widgetVisibilityData || undefined }
5871
+ const answers = {}
5872
+ await Promise.all(entries.map(async ([id, cond]) => {
5873
+ answers[id] = await evaluateVisibleWhen(cond, ctx)
5874
+ }))
5875
+ if (seq === this.widgetConditionSeq) {
5876
+ this.widgetConditionAnswers = answers
5877
+ }
5878
+ },
5879
+
5677
5880
  /**
5678
5881
  * Drop the items that draw nothing, then close the rows they freed —
5679
5882
  * `float: true` leaves the item below pinned under a gap nobody owns.
@@ -5696,7 +5899,10 @@ export default {
5696
5899
  * Whether a layout item has anything to draw, so a grid cell is worth
5697
5900
  * spending on it.
5698
5901
  *
5699
- * TWO widget types are judged, and no others.
5902
+ * A widget whose definition carries `visibleWhen` draws only while the
5903
+ * condition holds for the record (see `detailWidgetVisible`).
5904
+ *
5905
+ * Beyond that, TWO widget types are judged, and no others.
5700
5906
  *
5701
5907
  * `integration`, in the case where nothing can ever appear: a leaf from
5702
5908
  * another app that is not registered, with no `requiredApp` to fall back
@@ -5743,6 +5949,18 @@ export default {
5743
5949
  if (!widget) {
5744
5950
  return true
5745
5951
  }
5952
+ // The widget's own `visibleWhen`, read against the record and the
5953
+ // records `visibilityContext` names (a case's case type). Without
5954
+ // one the widget shows, as it always did, and the layout takes no
5955
+ // dependency on the record: a page without conditions re-lays out
5956
+ // exactly as often as before.
5957
+ if (widget.visibleWhen !== undefined && widget.visibleWhen !== null
5958
+ && !detailWidgetVisible(widget, this.widgetVisibilityData, {
5959
+ pending: this.visibilityRowsPending,
5960
+ answers: this.widgetConditionAnswers,
5961
+ })) {
5962
+ return false
5963
+ }
5746
5964
  // The board look shows the activity as the History tab. When a tabs
5747
5965
  // widget already holds it, it is not also a body section.
5748
5966
  if (this.isBoard && isActivityWidget(widget) && this.tabbedWidgetIds.has(widget.id)) {
@@ -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 : ''
@@ -111,6 +111,14 @@
111
111
  role="note"
112
112
  :data-testid="`cn-field-list-band-${row.key}`">
113
113
  <span class="cn-field-list-widget__band-text">{{ row.detail }}</span>
114
+ <NcButton
115
+ v-for="copy in row.copies"
116
+ :key="`copy-${copy.path}`"
117
+ variant="tertiary"
118
+ :data-testid="`cn-field-list-copy-${copy.path}-${row.key}`"
119
+ @click="copyValue(copy)">
120
+ {{ copy.label }}
121
+ </NcButton>
114
122
  <NcButton
115
123
  v-for="action in row.bandActions"
116
124
  :key="action.id"
@@ -162,6 +170,16 @@
162
170
  <p v-if="row.detail" class="cn-field-list-widget__detail">
163
171
  {{ row.detail }}
164
172
  </p>
173
+ <div v-if="row.detail && row.copies.length > 0" class="cn-field-list-widget__copies">
174
+ <NcButton
175
+ v-for="copy in row.copies"
176
+ :key="`copy-${copy.path}`"
177
+ variant="tertiary"
178
+ :data-testid="`cn-field-list-copy-${copy.path}-${row.key}`"
179
+ @click="copyValue(copy)">
180
+ {{ copy.label }}
181
+ </NcButton>
182
+ </div>
165
183
  </li>
166
184
  </ul>
167
185
 
@@ -211,6 +229,7 @@
211
229
  </NcButton>
212
230
  </form>
213
231
 
232
+ <span class="cn-field-list-widget__sr" aria-live="polite" data-testid="cn-field-list-copied">{{ copied }}</span>
214
233
  <NcNoteCard v-if="notice" :type="noticeType" data-testid="cn-field-list-notice">
215
234
  {{ notice }}
216
235
  </NcNoteCard>
@@ -260,7 +279,7 @@ export default {
260
279
  *
261
280
  * - `field` (required) — the object field holding the array.
262
281
  * - `key` — the item field that identifies an entry (default: the item itself for strings).
263
- * - `row` — `{ title, titleFallback?, avatar?, meta?: [paths|{ path, map?, format? }], pill?: { path, map?, variantMap? }, aside?: { path, format?, prefix?, fallback? }, detail?: { template, when? } }`.
282
+ * - `row` — `{ title, titleFallback?, avatar?, meta?: [paths|{ path, map?, format? }], pill?: { path, map?, variantMap? }, aside?: { path, format?, prefix?, fallback? }, detail?: { template, when?, copy?: [{ path, label }] } }`. `detail.copy` puts a copy button beside the detail line for each named value of the row (an empty value gets none), such as a DNS record's name and value.
264
283
  * - `add` — `{ label?, fields: [{ key, label, type?: 'text'|'select', options?, required?, pattern?, patternMessage? }], unique?, defaults? }`.
265
284
  * - `remove` — `{ label? }`; omitted = no remove.
266
285
  * - `rowActions` — `[{ id, label, when?, placement?: 'band', request: { method?, url, payload? } }]`; tokens `@object.<field>`, `@objectId`, `@row.<field>`.
@@ -316,6 +335,8 @@ export default {
316
335
  busy: '',
317
336
  notice: '',
318
337
  noticeType: 'error',
338
+ // What the last copy button put on the clipboard, said to a screen reader.
339
+ copied: '',
319
340
  }
320
341
  },
321
342
 
@@ -358,6 +379,7 @@ export default {
358
379
  pill: this.pillOf(entry, spec.pill),
359
380
  aside: spec.aside ? this.asideOf(entry, spec.aside) : '',
360
381
  detail: spec.detail && this.matches(entry, spec.detail.when) ? this.fill(spec.detail.template, entry) : '',
382
+ copies: spec.detail && this.matches(entry, spec.detail.when) ? this.copiesOf(entry, spec.detail.copy) : [],
361
383
  actions: this.actionsFor(entry),
362
384
  cells: this.isTable ? this.cellsOf(entry) : [],
363
385
  }
@@ -818,6 +840,40 @@ export default {
818
840
  * @return {Promise<void>}
819
841
  * @spec openspec/changes/abstract-field-list/specs/field-list/spec.md#requirement-a-field-list-row-can-run-a-configured-request
820
842
  */
843
+ /**
844
+ * The copy buttons of a row's detail line: one per named value that is not empty.
845
+ *
846
+ * @param {object} entry The entry.
847
+ * @param {Array<{path: string, label?: string}>|undefined} declared The `detail.copy` list.
848
+ * @return {Array<{path: string, label: string, value: string}>}
849
+ */
850
+ copiesOf(entry, declared) {
851
+ return (Array.isArray(declared) ? declared : [])
852
+ .filter((copy) => copy && typeof copy.path === 'string' && copy.path !== '')
853
+ .map((copy) => ({
854
+ path: copy.path,
855
+ label: this.tr(copy.label || 'Copy'),
856
+ value: String(readPath(entry, copy.path) ?? ''),
857
+ }))
858
+ .filter((copy) => copy.value !== '')
859
+ },
860
+
861
+ /**
862
+ * Put one value on the clipboard and say so; a browser that refuses says that instead.
863
+ *
864
+ * @param {{label: string, value: string}} copy The copy button.
865
+ * @return {Promise<void>}
866
+ */
867
+ async copyValue(copy) {
868
+ this.copied = ''
869
+ try {
870
+ await navigator.clipboard.writeText(copy.value)
871
+ this.copied = `${this.tr('Copied')}: ${copy.label}`
872
+ } catch {
873
+ this.notify(this.tr('Copying did not work. Select the text and copy it yourself.'))
874
+ }
875
+ },
876
+
821
877
  async runAction(action, row) {
822
878
  this.busy = row.key
823
879
  this.notice = ''
@@ -912,6 +968,12 @@ export default {
912
968
 
913
969
  .cn-field-list-widget__meta,
914
970
  .cn-field-list-widget__aside,
971
+ .cn-field-list-widget__copies {
972
+ display: flex;
973
+ flex-wrap: wrap;
974
+ gap: 4px;
975
+ }
976
+
915
977
  .cn-field-list-widget__detail {
916
978
  font-size: 13px;
917
979
  }
@@ -280,7 +280,7 @@ export default {
280
280
  * @return {string} The formatted string.
281
281
  */
282
282
  formatNumber(value) {
283
- return formatMetricValue(value, this.resolvedFormat, this.configCtx)
283
+ return formatMetricValue(value, this.resolvedFormat, this.configCtx, null, this.effectiveTranslate)
284
284
  },
285
285
 
286
286
  /**