@conduction/nextcloud-vue 2.79.0 → 2.81.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 (390) hide show
  1. package/dist/esm/components/CnActionButtons/CnActionButtons.vue2.js.map +1 -1
  2. package/dist/esm/components/CnActionButtons/CnActionButtons.vue3.js +3 -1
  3. package/dist/esm/components/CnActionButtons/CnActionButtons.vue3.js.map +1 -1
  4. package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js +32 -3
  5. package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js.map +1 -1
  6. package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js +13 -11
  7. package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js.map +1 -1
  8. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js +80 -4
  9. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
  10. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js +95 -91
  11. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js.map +1 -1
  12. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js +179 -13
  13. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js.map +1 -1
  14. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue3.js +52 -16
  15. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue3.js.map +1 -1
  16. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js +96 -4
  17. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js.map +1 -1
  18. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js +158 -110
  19. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js.map +1 -1
  20. package/dist/esm/components/CnChipsFieldWidget/CnChipsFieldWidget.vue.js +10 -0
  21. package/dist/esm/components/CnChipsFieldWidget/CnChipsFieldWidget.vue.js.map +1 -0
  22. package/dist/esm/components/CnChipsFieldWidget/CnChipsFieldWidget.vue2.js +252 -0
  23. package/dist/esm/components/CnChipsFieldWidget/CnChipsFieldWidget.vue2.js.map +1 -0
  24. package/dist/esm/components/CnChipsFieldWidget/CnChipsFieldWidget.vue3.js +140 -0
  25. package/dist/esm/components/CnChipsFieldWidget/CnChipsFieldWidget.vue3.js.map +1 -0
  26. package/dist/esm/components/CnChipsFieldWidget/index.js +26 -0
  27. package/dist/esm/components/CnChipsFieldWidget/index.js.map +1 -0
  28. package/dist/esm/components/CnChoicePickerWidget/CnChoicePickerWidget.vue.js +10 -0
  29. package/dist/esm/components/CnChoicePickerWidget/CnChoicePickerWidget.vue.js.map +1 -0
  30. package/dist/esm/components/CnChoicePickerWidget/CnChoicePickerWidget.vue2.js +354 -0
  31. package/dist/esm/components/CnChoicePickerWidget/CnChoicePickerWidget.vue2.js.map +1 -0
  32. package/dist/esm/components/CnChoicePickerWidget/CnChoicePickerWidget.vue3.js +172 -0
  33. package/dist/esm/components/CnChoicePickerWidget/CnChoicePickerWidget.vue3.js.map +1 -0
  34. package/dist/esm/components/CnChoicePickerWidget/index.js +27 -0
  35. package/dist/esm/components/CnChoicePickerWidget/index.js.map +1 -0
  36. package/dist/esm/components/CnConversationThread/CnConversationWidget.vue2.js +1 -1
  37. package/dist/esm/components/CnConversationThread/CnConversationWidget.vue2.js.map +1 -1
  38. package/dist/esm/components/CnConversationThread/CnConversationWidget.vue3.js.map +1 -1
  39. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue2.js +2 -2
  40. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue2.js.map +1 -1
  41. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue3.js.map +1 -1
  42. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +65 -5
  43. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  44. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +2 -1
  45. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  46. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +19 -11
  47. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
  48. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +4 -2
  49. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
  50. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +134 -4
  51. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  52. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +21 -4
  53. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  54. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js +24 -0
  55. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
  56. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js +43 -4
  57. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
  58. package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewWidget.vue2.js +1 -1
  59. package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewWidget.vue2.js.map +1 -1
  60. package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewWidget.vue3.js.map +1 -1
  61. package/dist/esm/components/CnFacetSidebar/CnFacetSidebar.vue2.js +7 -7
  62. package/dist/esm/components/CnFacetSidebar/CnFacetSidebar.vue2.js.map +1 -1
  63. package/dist/esm/components/CnFacetSidebar/CnFacetSidebar.vue3.js.map +1 -1
  64. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue.js +11 -0
  65. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue.js.map +1 -0
  66. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js +867 -0
  67. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js.map +1 -0
  68. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js +238 -0
  69. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js.map +1 -0
  70. package/dist/esm/components/CnFactsWidget/index.js +30 -0
  71. package/dist/esm/components/CnFactsWidget/index.js.map +1 -0
  72. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue.js +10 -0
  73. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue.js.map +1 -0
  74. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js +626 -0
  75. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js.map +1 -0
  76. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js +396 -0
  77. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js.map +1 -0
  78. package/dist/esm/components/CnFieldListWidget/index.js +29 -0
  79. package/dist/esm/components/CnFieldListWidget/index.js.map +1 -0
  80. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js +302 -4
  81. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js.map +1 -1
  82. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js +417 -244
  83. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js.map +1 -1
  84. package/dist/esm/components/CnFilesBrowser/filesBrowser.js +101 -1
  85. package/dist/esm/components/CnFilesBrowser/filesBrowser.js.map +1 -1
  86. package/dist/esm/components/CnFilesBrowser/filesBrowserColumns.js +141 -1
  87. package/dist/esm/components/CnFilesBrowser/filesBrowserColumns.js.map +1 -1
  88. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js +65 -17
  89. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
  90. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js +4 -4
  91. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
  92. package/dist/esm/components/CnFormPage/CnFormPage.vue2.js +78 -3
  93. package/dist/esm/components/CnFormPage/CnFormPage.vue2.js.map +1 -1
  94. package/dist/esm/components/CnFormPage/CnFormPage.vue3.js +74 -49
  95. package/dist/esm/components/CnFormPage/CnFormPage.vue3.js.map +1 -1
  96. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +333 -73
  97. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  98. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +79 -17
  99. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  100. package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue.js +10 -0
  101. package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue.js.map +1 -0
  102. package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue2.js +99 -0
  103. package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue2.js.map +1 -0
  104. package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue3.js +48 -0
  105. package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue3.js.map +1 -0
  106. package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue2.js +7 -7
  107. package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue2.js.map +1 -1
  108. package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue3.js.map +1 -1
  109. package/dist/esm/components/CnNextStepCard/CnNextStepWidget.vue2.js +1 -1
  110. package/dist/esm/components/CnNextStepCard/CnNextStepWidget.vue2.js.map +1 -1
  111. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +65 -2
  112. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
  113. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js +6 -2
  114. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js.map +1 -1
  115. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js +63 -1
  116. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js.map +1 -1
  117. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js +8 -1
  118. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js.map +1 -1
  119. package/dist/esm/components/CnObjectSidebar/CnNotesTab.vue2.js +14 -0
  120. package/dist/esm/components/CnObjectSidebar/CnNotesTab.vue2.js.map +1 -1
  121. package/dist/esm/components/CnObjectSidebar/CnNotesTab.vue3.js +5 -1
  122. package/dist/esm/components/CnObjectSidebar/CnNotesTab.vue3.js.map +1 -1
  123. package/dist/esm/components/CnPageRenderer/CnPageRenderer.vue2.js +29 -1
  124. package/dist/esm/components/CnPageRenderer/CnPageRenderer.vue2.js.map +1 -1
  125. package/dist/esm/components/CnPageRenderer/CnPageRenderer.vue3.js.map +1 -1
  126. package/dist/esm/components/CnRadioCardsWidget/CnRadioCardsWidget.vue.js +10 -0
  127. package/dist/esm/components/CnRadioCardsWidget/CnRadioCardsWidget.vue.js.map +1 -0
  128. package/dist/esm/components/CnRadioCardsWidget/CnRadioCardsWidget.vue2.js +174 -0
  129. package/dist/esm/components/CnRadioCardsWidget/CnRadioCardsWidget.vue2.js.map +1 -0
  130. package/dist/esm/components/CnRadioCardsWidget/CnRadioCardsWidget.vue3.js +75 -0
  131. package/dist/esm/components/CnRadioCardsWidget/CnRadioCardsWidget.vue3.js.map +1 -0
  132. package/dist/esm/components/CnRadioCardsWidget/index.js +28 -0
  133. package/dist/esm/components/CnRadioCardsWidget/index.js.map +1 -0
  134. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +10 -0
  135. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js.map +1 -0
  136. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +411 -0
  137. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -0
  138. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js +178 -0
  139. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -0
  140. package/dist/esm/components/CnRecordListWidget/index.js +31 -0
  141. package/dist/esm/components/CnRecordListWidget/index.js.map +1 -0
  142. package/dist/esm/components/CnSavedViewsWidget/CnSavedViewsWidget.vue.js +10 -0
  143. package/dist/esm/components/CnSavedViewsWidget/CnSavedViewsWidget.vue.js.map +1 -0
  144. package/dist/esm/components/CnSavedViewsWidget/CnSavedViewsWidget.vue2.js +222 -0
  145. package/dist/esm/components/CnSavedViewsWidget/CnSavedViewsWidget.vue2.js.map +1 -0
  146. package/dist/esm/components/CnSavedViewsWidget/CnSavedViewsWidget.vue3.js +64 -0
  147. package/dist/esm/components/CnSavedViewsWidget/CnSavedViewsWidget.vue3.js.map +1 -0
  148. package/dist/esm/components/CnSavedViewsWidget/index.js +30 -0
  149. package/dist/esm/components/CnSavedViewsWidget/index.js.map +1 -0
  150. package/dist/esm/components/CnSectionsWidget/CnSectionsWidget.vue.js +10 -0
  151. package/dist/esm/components/CnSectionsWidget/CnSectionsWidget.vue.js.map +1 -0
  152. package/dist/esm/components/CnSectionsWidget/CnSectionsWidget.vue2.js +351 -0
  153. package/dist/esm/components/CnSectionsWidget/CnSectionsWidget.vue2.js.map +1 -0
  154. package/dist/esm/components/CnSectionsWidget/CnSectionsWidget.vue3.js +71 -0
  155. package/dist/esm/components/CnSectionsWidget/CnSectionsWidget.vue3.js.map +1 -0
  156. package/dist/esm/components/CnSectionsWidget/index.js +35 -0
  157. package/dist/esm/components/CnSectionsWidget/index.js.map +1 -0
  158. package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue2.js.map +1 -1
  159. package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue3.js.map +1 -1
  160. package/dist/esm/components/CnSettingsFormWidget/CnSettingsFormWidget.vue.js +10 -0
  161. package/dist/esm/components/CnSettingsFormWidget/CnSettingsFormWidget.vue.js.map +1 -0
  162. package/dist/esm/components/CnSettingsFormWidget/CnSettingsFormWidget.vue2.js +347 -0
  163. package/dist/esm/components/CnSettingsFormWidget/CnSettingsFormWidget.vue2.js.map +1 -0
  164. package/dist/esm/components/CnSettingsFormWidget/CnSettingsFormWidget.vue3.js +184 -0
  165. package/dist/esm/components/CnSettingsFormWidget/CnSettingsFormWidget.vue3.js.map +1 -0
  166. package/dist/esm/components/CnSettingsFormWidget/index.js +27 -0
  167. package/dist/esm/components/CnSettingsFormWidget/index.js.map +1 -0
  168. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue2.js +81 -2
  169. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue2.js.map +1 -1
  170. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue3.js.map +1 -1
  171. package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js +87 -6
  172. package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js.map +1 -1
  173. package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js +1 -0
  174. package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js.map +1 -1
  175. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue2.js +84 -1
  176. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue2.js.map +1 -1
  177. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue3.js +85 -41
  178. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue3.js.map +1 -1
  179. package/dist/esm/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue.js +10 -0
  180. package/dist/esm/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue.js.map +1 -0
  181. package/dist/esm/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue2.js +159 -0
  182. package/dist/esm/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue2.js.map +1 -0
  183. package/dist/esm/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue3.js +60 -0
  184. package/dist/esm/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue3.js.map +1 -0
  185. package/dist/esm/components/CnSwitchRowsWidget/index.js +26 -0
  186. package/dist/esm/components/CnSwitchRowsWidget/index.js.map +1 -0
  187. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +168 -7
  188. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
  189. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js +86 -23
  190. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
  191. package/dist/esm/components/CnTasksWidget/CnTasksWidget.vue2.js +80 -1
  192. package/dist/esm/components/CnTasksWidget/CnTasksWidget.vue2.js.map +1 -1
  193. package/dist/esm/components/CnTasksWidget/CnTasksWidget.vue3.js +75 -32
  194. package/dist/esm/components/CnTasksWidget/CnTasksWidget.vue3.js.map +1 -1
  195. package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue.js +10 -0
  196. package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue.js.map +1 -0
  197. package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue2.js +269 -0
  198. package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue2.js.map +1 -0
  199. package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue3.js +106 -0
  200. package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue3.js.map +1 -0
  201. package/dist/esm/components/CnToggleTableWidget/index.js +27 -0
  202. package/dist/esm/components/CnToggleTableWidget/index.js.map +1 -0
  203. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js +11 -0
  204. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js.map +1 -1
  205. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js +7 -2
  206. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js.map +1 -1
  207. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue3.js.map +1 -1
  208. package/dist/esm/components/CnWidgetObjectTable/dashboardRegistration.js +5 -0
  209. package/dist/esm/components/CnWidgetObjectTable/dashboardRegistration.js.map +1 -1
  210. package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue2.js +301 -6
  211. package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue2.js.map +1 -1
  212. package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue3.js +153 -55
  213. package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue3.js.map +1 -1
  214. package/dist/esm/composables/useFormDraft.js +140 -9
  215. package/dist/esm/composables/useFormDraft.js.map +1 -1
  216. package/dist/esm/composables/useListView.js +4 -4
  217. package/dist/esm/composables/useListView.js.map +1 -1
  218. package/dist/esm/index.js +21 -1
  219. package/dist/esm/index.js.map +1 -1
  220. package/dist/esm/integrations/builtin/field-inspection/CnFieldInspectionCard.vue2.js +28 -20
  221. package/dist/esm/integrations/builtin/field-inspection/CnFieldInspectionCard.vue2.js.map +1 -1
  222. package/dist/esm/integrations/builtin/field-inspection/CnFieldInspectionCard.vue3.js +9 -0
  223. package/dist/esm/integrations/builtin/field-inspection/CnFieldInspectionCard.vue3.js.map +1 -1
  224. package/dist/esm/integrations/builtin/field-inspection.js +21 -0
  225. package/dist/esm/integrations/builtin/field-inspection.js.map +1 -1
  226. package/dist/esm/integrations/offline/checklistSubmission.js +141 -0
  227. package/dist/esm/integrations/offline/checklistSubmission.js.map +1 -0
  228. package/dist/esm/integrations/offline/fieldCollectionHelpers.js +70 -5
  229. package/dist/esm/integrations/offline/fieldCollectionHelpers.js.map +1 -1
  230. package/dist/esm/integrations/offline/index.js +2 -1
  231. package/dist/esm/integrations/offline/index.js.map +1 -1
  232. package/dist/esm/integrations/offline/offlineDb.js +6 -1
  233. package/dist/esm/integrations/offline/offlineDb.js.map +1 -1
  234. package/dist/esm/integrations/offline/syncReplayService.js +8 -2
  235. package/dist/esm/integrations/offline/syncReplayService.js.map +1 -1
  236. package/dist/esm/l10n/en.json.js +20 -1
  237. package/dist/esm/l10n/en.json.js.map +1 -1
  238. package/dist/esm/l10n/nl.json.js +70 -2
  239. package/dist/esm/l10n/nl.json.js.map +1 -1
  240. package/dist/esm/nextcloud-vue.css +2192 -222
  241. package/dist/esm/utils/contentHeaderLink.js +51 -0
  242. package/dist/esm/utils/contentHeaderLink.js.map +1 -0
  243. package/dist/esm/utils/detailActionModel.js +60 -3
  244. package/dist/esm/utils/detailActionModel.js.map +1 -1
  245. package/dist/esm/utils/facets.js +75 -3
  246. package/dist/esm/utils/facets.js.map +1 -1
  247. package/dist/esm/utils/fetchFilterCounts.js +40 -5
  248. package/dist/esm/utils/fetchFilterCounts.js.map +1 -1
  249. package/dist/esm/utils/fillTemplate.js +120 -0
  250. package/dist/esm/utils/fillTemplate.js.map +1 -0
  251. package/dist/esm/utils/initials.js +32 -0
  252. package/dist/esm/utils/initials.js.map +1 -0
  253. package/dist/esm/utils/libraryWidgetKeys.js +11 -0
  254. package/dist/esm/utils/libraryWidgetKeys.js.map +1 -1
  255. package/dist/esm/utils/objectWrite.js +59 -0
  256. package/dist/esm/utils/objectWrite.js.map +1 -0
  257. package/dist/esm/utils/pageControls.js +81 -0
  258. package/dist/esm/utils/pageControls.js.map +1 -0
  259. package/dist/esm/utils/personalLenses.js +40 -3
  260. package/dist/esm/utils/personalLenses.js.map +1 -1
  261. package/dist/esm/utils/queryPresets.js +96 -0
  262. package/dist/esm/utils/queryPresets.js.map +1 -0
  263. package/dist/esm/utils/resolveFilterTokens.js +12 -1
  264. package/dist/esm/utils/resolveFilterTokens.js.map +1 -1
  265. package/dist/esm/utils/resolveManifestTokens.js +2 -0
  266. package/dist/esm/utils/resolveManifestTokens.js.map +1 -1
  267. package/dist/esm/utils/schema.js +21 -0
  268. package/dist/esm/utils/schema.js.map +1 -1
  269. package/dist/esm/utils/sentinelTokens.js +22 -4
  270. package/dist/esm/utils/sentinelTokens.js.map +1 -1
  271. package/dist/esm/utils/userGroupsPeek.js +91 -0
  272. package/dist/esm/utils/userGroupsPeek.js.map +1 -0
  273. package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
  274. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  275. package/dist/esm/utils/visibleWhen.js +57 -5
  276. package/dist/esm/utils/visibleWhen.js.map +1 -1
  277. package/dist/esm/utils/widgetDispatch.js +7 -1
  278. package/dist/esm/utils/widgetDispatch.js.map +1 -1
  279. package/dist/esm/utils/widgetVisibility.js +4 -57
  280. package/dist/esm/utils/widgetVisibility.js.map +1 -1
  281. package/dist/nextcloud-vue.cjs.js +19708 -9301
  282. package/dist/nextcloud-vue.cjs.js.map +2 -2
  283. package/dist/nextcloud-vue.css +2192 -222
  284. package/l10n/en.json +20 -1
  285. package/l10n/nl.json +70 -2
  286. package/package.json +2 -1
  287. package/scripts/check-docs-mdx.mjs +108 -0
  288. package/src/components/CnActionButtons/CnActionButtons.vue +2 -0
  289. package/src/components/CnActionsBar/CnActionsBar.md +4 -0
  290. package/src/components/CnActionsBar/CnActionsBar.vue +48 -11
  291. package/src/components/CnAppRoot/CnAppRoot.md +4 -2
  292. package/src/components/CnAppRoot/CnAppRoot.vue +151 -59
  293. package/src/components/CnBannerWidget/CnBannerWidget.vue +244 -23
  294. package/src/components/CnCellRenderer/CnCellRenderer.md +2 -0
  295. package/src/components/CnCellRenderer/CnCellRenderer.vue +235 -5
  296. package/src/components/CnChipsFieldWidget/CnChipsFieldWidget.vue +433 -0
  297. package/src/components/CnChipsFieldWidget/index.js +25 -0
  298. package/src/components/CnChoicePickerWidget/CnChoicePickerWidget.vue +556 -0
  299. package/src/components/CnChoicePickerWidget/index.js +26 -0
  300. package/src/components/CnConversationThread/CnConversationWidget.vue +1 -1
  301. package/src/components/CnDashboardGrid/CnDashboardGrid.vue +2 -2
  302. package/src/components/CnDashboardPage/CnDashboardPage.vue +78 -5
  303. package/src/components/CnDataTable/CnDataTable.vue +22 -12
  304. package/src/components/CnDetailPage/CnDetailPage.md +1 -1
  305. package/src/components/CnDetailPage/CnDetailPage.vue +152 -6
  306. package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +66 -0
  307. package/src/components/CnDocumentReviewList/CnDocumentReviewWidget.vue +1 -1
  308. package/src/components/CnFacetSidebar/CnFacetSidebar.vue +7 -7
  309. package/src/components/CnFactsWidget/CnFactsWidget.vue +1190 -0
  310. package/src/components/CnFactsWidget/index.js +29 -0
  311. package/src/components/CnFieldListWidget/CnFieldListWidget.vue +1008 -0
  312. package/src/components/CnFieldListWidget/index.js +28 -0
  313. package/src/components/CnFilesBrowser/CnFilesBrowser.vue +588 -117
  314. package/src/components/CnFilesBrowser/filesBrowser.js +100 -0
  315. package/src/components/CnFilesBrowser/filesBrowserColumns.js +140 -0
  316. package/src/components/CnFormDialog/CnFormDialog.md +9 -3
  317. package/src/components/CnFormDialog/CnFormDialog.vue +69 -21
  318. package/src/components/CnFormPage/CnFormPage.vue +99 -3
  319. package/src/components/CnIndexPage/CnIndexPage.vue +309 -9
  320. package/src/components/CnIndexPage/CnIndexPageWidgets.vue +169 -0
  321. package/src/components/CnIndexSidebar/CnIndexSidebar.vue +7 -7
  322. package/src/components/CnNextStepCard/CnNextStepWidget.vue +1 -1
  323. package/src/components/CnObjectListWidget/CnObjectListWidget.vue +69 -2
  324. package/src/components/CnObjectSidebar/CnFilesTab.vue +70 -1
  325. package/src/components/CnObjectSidebar/CnNotesTab.vue +16 -0
  326. package/src/components/CnPageRenderer/CnPageRenderer.vue +29 -1
  327. package/src/components/CnRadioCardsWidget/CnRadioCardsWidget.vue +313 -0
  328. package/src/components/CnRadioCardsWidget/index.js +27 -0
  329. package/src/components/CnRecordListWidget/CnRecordListWidget.vue +652 -0
  330. package/src/components/CnRecordListWidget/index.js +30 -0
  331. package/src/components/CnSavedViewsWidget/CnSavedViewsWidget.vue +296 -0
  332. package/src/components/CnSavedViewsWidget/index.js +29 -0
  333. package/src/components/CnSectionsWidget/CnSectionsWidget.vue +489 -0
  334. package/src/components/CnSectionsWidget/index.js +34 -0
  335. package/src/components/CnSegmentedControl/CnSegmentedControl.vue +13 -0
  336. package/src/components/CnSettingsFormWidget/CnSettingsFormWidget.vue +555 -0
  337. package/src/components/CnSettingsFormWidget/index.js +26 -0
  338. package/src/components/CnStagesWidget/CnStagesWidget.vue +81 -2
  339. package/src/components/CnStatWidget/CnStatWidget.vue +88 -7
  340. package/src/components/CnStatsBlockWidget/CnStatsBlockWidget.vue +180 -3
  341. package/src/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue +226 -0
  342. package/src/components/CnSwitchRowsWidget/index.js +25 -0
  343. package/src/components/CnTabsWidget/CnTabsWidget.vue +263 -8
  344. package/src/components/CnTasksWidget/CnTasksWidget.vue +175 -15
  345. package/src/components/CnToggleTableWidget/CnToggleTableWidget.vue +386 -0
  346. package/src/components/CnToggleTableWidget/index.js +26 -0
  347. package/src/components/CnWidgetGrid/registerDashboardWidgets.js +11 -0
  348. package/src/components/CnWidgetObjectTable/CnWidgetObjectTable.vue +7 -2
  349. package/src/components/CnWidgetObjectTable/dashboardRegistration.js +5 -0
  350. package/src/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue +493 -11
  351. package/src/components/index.js +19 -0
  352. package/src/composables/__tests__/useFormDraft.spec.js +69 -0
  353. package/src/composables/useFormDraft.js +139 -8
  354. package/src/composables/useListView.js +4 -4
  355. package/src/css/form-field.css +14 -0
  356. package/src/css/index-page.css +71 -0
  357. package/src/css/kpi-card.css +32 -0
  358. package/src/css/look-board-detail.css +113 -2
  359. package/src/css/look-board-index.css +90 -8
  360. package/src/css/look-board.css +109 -2
  361. package/src/css/table.css +19 -0
  362. package/src/index.js +19 -0
  363. package/src/integrations/builtin/field-inspection/CnFieldInspectionCard.vue +35 -20
  364. package/src/integrations/builtin/field-inspection.js +21 -0
  365. package/src/integrations/offline/checklistSubmission.js +137 -0
  366. package/src/integrations/offline/fieldCollectionHelpers.js +69 -4
  367. package/src/integrations/offline/index.js +3 -0
  368. package/src/integrations/offline/offlineDb.js +6 -1
  369. package/src/integrations/offline/syncReplayService.js +8 -2
  370. package/src/schemas/app-manifest-v2.schema.json +196 -13
  371. package/src/utils/contentHeaderLink.js +48 -0
  372. package/src/utils/detailActionModel.js +59 -2
  373. package/src/utils/facets.js +74 -2
  374. package/src/utils/fetchFilterCounts.js +39 -4
  375. package/src/utils/fillTemplate.js +116 -0
  376. package/src/utils/initials.js +29 -0
  377. package/src/utils/libraryWidgetKeys.js +11 -0
  378. package/src/utils/objectWrite.js +55 -0
  379. package/src/utils/pageControls.js +78 -0
  380. package/src/utils/personalLenses.js +39 -2
  381. package/src/utils/queryPresets.js +93 -0
  382. package/src/utils/resolveFilterTokens.js +14 -1
  383. package/src/utils/resolveManifestTokens.js +2 -0
  384. package/src/utils/schema.js +21 -0
  385. package/src/utils/sentinelTokens.js +22 -4
  386. package/src/utils/userGroupsPeek.js +87 -0
  387. package/src/utils/validateManifestV2.compiled.js +3 -3
  388. package/src/utils/visibleWhen.js +56 -4
  389. package/src/utils/widgetDispatch.js +7 -1
  390. package/src/utils/widgetVisibility.js +7 -56
@@ -0,0 +1,1190 @@
1
+ <!--
2
+ - SPDX-FileCopyrightText: 2026 Conduction B.V.
3
+ - SPDX-License-Identifier: EUPL-1.2
4
+ -->
5
+
6
+ <template>
7
+ <div class="cn-facts-widget" data-testid="cn-facts-widget">
8
+ <p v-if="error" class="cn-facts-widget__muted" data-testid="cn-facts-error">
9
+ {{ tr('Could not load this information') }}
10
+ </p>
11
+ <div v-else-if="loading && !record" class="cn-facts-widget__loading">
12
+ <NcLoadingIcon :size="20" />
13
+ </div>
14
+ <p v-else-if="blocks.length === 0" class="cn-facts-widget__muted" data-testid="cn-facts-empty">
15
+ {{ emptyLabel }}
16
+ </p>
17
+ <template v-else>
18
+ <template v-for="block in blocks" :key="block.key">
19
+ <dl
20
+ v-if="block.kind === 'rows'"
21
+ class="cn-facts-widget__rows">
22
+ <div
23
+ v-for="row in block.rows"
24
+ :key="row.key"
25
+ class="cn-facts-widget__row"
26
+ :data-testid="`cn-fact-${row.key}`">
27
+ <dt class="cn-facts-widget__label" :class="{ 'cn-facts-widget__label--icon': row.icon }">
28
+ <CnIcon
29
+ v-if="row.icon"
30
+ :name="row.icon"
31
+ :size="18"
32
+ aria-hidden="true" />
33
+ <span :class="{ 'hidden-visually': row.icon }">{{ row.label }}</span>
34
+ </dt>
35
+ <dd class="cn-facts-widget__value">
36
+ <a
37
+ v-if="row.href"
38
+ :href="row.href"
39
+ class="cn-facts-widget__value-link"
40
+ :target="row.external ? '_blank' : undefined"
41
+ :rel="row.external ? 'noopener noreferrer' : undefined">{{ row.value }}</a>
42
+ <span v-else>{{ row.masked && !revealed[row.key] ? row.masked : row.value }}</span>
43
+ <button
44
+ v-if="row.reveal && !revealed[row.key]"
45
+ type="button"
46
+ class="cn-facts-widget__inline-button"
47
+ :aria-label="revealLabel(row)"
48
+ :data-testid="`cn-fact-reveal-${row.key}`"
49
+ @click="reveal(row)">
50
+ <EyeOutline :size="16" aria-hidden="true" />
51
+ {{ showLabel }}
52
+ </button>
53
+ <button
54
+ v-if="row.copy"
55
+ type="button"
56
+ class="cn-facts-widget__icon-button"
57
+ :aria-label="copyLabel(row)"
58
+ :title="copyLabel(row)"
59
+ :data-testid="`cn-fact-copy-${row.key}`"
60
+ @click="copy(row)">
61
+ <Check v-if="copied === row.key" :size="16" aria-hidden="true" />
62
+ <ContentCopy v-else :size="16" aria-hidden="true" />
63
+ </button>
64
+ </dd>
65
+ </div>
66
+ </dl>
67
+ <component
68
+ :is="block.to ? 'router-link' : 'strong'"
69
+ v-else-if="block.kind === 'title'"
70
+ :to="block.to || undefined"
71
+ class="cn-facts-widget__title"
72
+ :data-testid="`cn-fact-${block.key}`">
73
+ {{ block.value }}
74
+ </component>
75
+ <span
76
+ v-else-if="block.kind === 'line'"
77
+ class="cn-facts-widget__line"
78
+ :data-testid="`cn-fact-${block.key}`">
79
+ {{ block.value }}
80
+ </span>
81
+ <span
82
+ v-else-if="block.kind === 'text'"
83
+ class="cn-facts-widget__text"
84
+ :data-testid="`cn-fact-${block.key}`">
85
+ {{ block.value }}
86
+ </span>
87
+ <div
88
+ v-else-if="block.kind === 'lead'"
89
+ class="cn-facts-widget__lead"
90
+ :data-testid="`cn-fact-${block.key}`">
91
+ <span
92
+ class="cn-facts-widget__lead-tile"
93
+ :class="`cn-facts-widget__lead-tile--${block.tone}`"
94
+ aria-hidden="true">
95
+ <CnIcon v-if="block.icon" :name="block.icon" :size="20" />
96
+ <template v-else>{{ block.initials }}</template>
97
+ </span>
98
+ <span class="cn-facts-widget__lead-text">
99
+ <strong class="cn-facts-widget__lead-title">{{ block.value }}</strong>
100
+ <span v-if="block.subtitle" class="cn-facts-widget__lead-subtitle">{{ block.subtitle }}</span>
101
+ </span>
102
+ </div>
103
+ <div v-else-if="block.kind === 'pill'">
104
+ <CnStatusBadge
105
+ :label="block.value"
106
+ :variant="block.variant"
107
+ size="small"
108
+ :data-testid="`cn-fact-${block.key}`" />
109
+ </div>
110
+ </template>
111
+ </template>
112
+ <component
113
+ :is="link.to ? 'router-link' : 'a'"
114
+ v-if="link && record"
115
+ :to="link.to || undefined"
116
+ :href="link.href || undefined"
117
+ class="cn-facts-widget__link"
118
+ data-testid="cn-facts-link">
119
+ {{ link.label }}
120
+ </component>
121
+ <button
122
+ v-if="action && record"
123
+ type="button"
124
+ class="cn-facts-widget__action"
125
+ data-testid="cn-facts-action"
126
+ @click="runAction(content.action)">
127
+ <CnIcon v-if="action.icon" :name="action.icon" :size="16" />
128
+ {{ action.label }}
129
+ </button>
130
+ </div>
131
+ </template>
132
+
133
+ <script>
134
+ import { emit } from '@nextcloud/event-bus'
135
+ import { translate as t } from '@nextcloud/l10n'
136
+ import { NcLoadingIcon } from '@nextcloud/vue'
137
+ import Check from 'vue-material-design-icons/Check.vue'
138
+ import ContentCopy from 'vue-material-design-icons/ContentCopy.vue'
139
+ import EyeOutline from 'vue-material-design-icons/EyeOutline.vue'
140
+ import CnIcon from '../CnIcon/CnIcon.vue'
141
+ import CnStatusBadge from '../CnStatusBadge/CnStatusBadge.vue'
142
+ import { resolveEndpointRequest } from '../../composables/useEndpointSource.js'
143
+ import { useRefLabels } from '../../composables/useRefLabels.js'
144
+ import { dispatchAction } from '../../utils/actionsDispatcher.js'
145
+ import { isBadgeVariant } from '../../utils/badgeVariants.js'
146
+ import { formatBoardDate } from '../../utils/boardDate.js'
147
+ import { parseDateValue } from '../../utils/dateVariant.js'
148
+ import { readPath } from '../../utils/readPath.js'
149
+ import { hasUnresolvedDeepTokens, resolveDeepTokens } from '../../utils/resolveFilterTokens.js'
150
+
151
+ /** Kinds of entry a `fields[]` item can be. A labelled item without `as` is a row. */
152
+ const KINDS = Object.freeze(['row', 'title', 'line', 'pill', 'lead', 'text'])
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
+ /**
173
+ * Key of a resolved reference in the widget's `labels` data.
174
+ *
175
+ * @param {{register: string, schema: string, labelField?: string}} resolve The entry's `resolve` config.
176
+ * @param {string} id The referenced object's id.
177
+ * @return {string} The key.
178
+ */
179
+ function refKey(resolve, id) {
180
+ return `${resolve.register}/${resolve.schema}/${resolve.labelField || ''}/${id}`
181
+ }
182
+
183
+ /** Event-bus channel a reveal of a masked value is announced on, so an app can log it. */
184
+ export const FACTS_REVEAL_CHANNEL = 'cn:facts:reveal'
185
+
186
+ /**
187
+ * CnFactsWidget — the `facts` widget: a card of facts about ONE object.
188
+ *
189
+ * Renders the configured fields of the current detail-page object, or of one
190
+ * other object named by `content.source`, as the side cards on the screens
191
+ * draw them: label/value rows (a two-column grid, label muted), a bold title
192
+ * line that may link, muted meta lines that join several fields with " · ",
193
+ * a status pill, and one link at the foot.
194
+ *
195
+ * Everything is configuration; nothing in it knows an app:
196
+ *
197
+ * ```json
198
+ * {
199
+ * "type": "facts",
200
+ * "title": "Zaak",
201
+ * "content": {
202
+ * "source": { "register": "dossiq", "schema": "case", "id": "@object.case" },
203
+ * "fields": [
204
+ * { "as": "title", "path": "title", "route": { "name": "CaseDetail", "params": { "id": "@source.id" } } },
205
+ * { "as": "line", "parts": ["identifier", "caseTypeTitle"] },
206
+ * { "as": "pill", "path": "status", "map": { "open": "In behandeling" } },
207
+ * { "path": "deadline", "label": "Termijn", "format": "date" }
208
+ * ],
209
+ * "link": { "label": "Naar de zaak", "route": { "name": "CaseDetail", "params": { "id": "@source.id" } } },
210
+ * "emptyText": "Geen zaak"
211
+ * }
212
+ * }
213
+ * ```
214
+ *
215
+ * Tokens: `source.id` takes the shared filter tokens; `source.url` takes them
216
+ * inside the url, the endpoint grammar (`/apps/x/api/y/@objectId`)
217
+ * (`@objectId`, `@object.<field>`, `@workspace.<key>`, `@me`, ...). Routes
218
+ * and the link also take `@source.<field>` (a field of the object shown).
219
+ * A source whose id token does not resolve renders the empty text, never a
220
+ * request for the literal token.
221
+ *
222
+ * Labels, map values, the link label and the empty text go through the host
223
+ * app's translate (`cnTranslate`).
224
+ */
225
+ export default {
226
+ name: 'CnFactsWidget',
227
+
228
+ components: { Check, CnIcon, CnStatusBadge, ContentCopy, EyeOutline, NcLoadingIcon },
229
+
230
+ inject: {
231
+ cnTranslate: { default: () => (key) => key },
232
+ cnObjectContext: { default: null },
233
+ cnWorkspaceContext: { default: null },
234
+ /** Pre-bound action dispatcher of CnPageRenderer, for `action` / `headerAction`. */
235
+ cnDispatchAction: { default: null },
236
+ },
237
+
238
+ props: {
239
+ /**
240
+ * The widget configuration.
241
+ *
242
+ * - `fields` — the entries, in order. Each is `{ path, label?, as?, format?, map?, itemPath?, parts?, separator?, route?, variant?, variantMap?, icon?, copy?, reveal?, text?, title?, subtitle?, initialsPath?, tone? }`.
243
+ * `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`.
244
+ * 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.
245
+ * A part may be `{ text }`, a constant; a `line` or `text` entry without `path` or `parts` shows its `text`.
246
+ * `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.
247
+ * - `action` — an action drawn as a button under the facts; `headerAction` — one drawn in the card header (reported through the `header` event).
248
+ * `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`).
249
+ * `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.
250
+ * - `source` — `{ register, schema, id }` or `{ url }`; omitted = the current object.
251
+ * - `link` — `{ label, route? | href? }` at the foot.
252
+ * - `hideEmpty` — drop an entry whose value is empty (default true).
253
+ * - `emptyText` — shown when no entry has a value.
254
+ *
255
+ * @type {{fields?: Array<object>, source?: object, link?: object, hideEmpty?: boolean, emptyText?: string}}
256
+ */
257
+ content: {
258
+ type: Object,
259
+ default: () => ({}),
260
+ },
261
+
262
+ /**
263
+ * The current detail-page object, passed by the detail page host.
264
+ * Read when `content.source` is not set.
265
+ */
266
+ objectData: {
267
+ type: Object,
268
+ default: null,
269
+ },
270
+
271
+ /** The current detail-page object id, for the `@objectId` token. */
272
+ objectId: {
273
+ type: [String, Number],
274
+ default: null,
275
+ },
276
+ },
277
+
278
+ emits: ['header', 'reveal'],
279
+
280
+ data() {
281
+ return {
282
+ fetched: null,
283
+ loading: false,
284
+ error: false,
285
+ /** Keys of the masked rows the reader chose to show. */
286
+ revealed: {},
287
+ /** Key of the row whose value was just copied. */
288
+ copied: '',
289
+ /** Labels of the referenced objects read so far, by `refKey`. */
290
+ labels: {},
291
+ }
292
+ },
293
+
294
+ computed: {
295
+ /**
296
+ * The token context: the detail object (prop, else the injected context)
297
+ * and the page workspace.
298
+ *
299
+ * @return {object}
300
+ */
301
+ tokenCtx() {
302
+ const unwrap = (c) => (c && typeof c === 'object' && 'value' in c ? c.value : c)
303
+ const injected = unwrap(this.cnObjectContext) || {}
304
+ return {
305
+ ...injected,
306
+ objectId: this.objectId ?? injected.objectId ?? this.objectData?.id ?? null,
307
+ object: this.objectData || injected.object || null,
308
+ workspace: unwrap(this.cnWorkspaceContext) || {},
309
+ }
310
+ },
311
+
312
+ /**
313
+ * The source declaration with its tokens resolved, or null for the current object.
314
+ *
315
+ * @return {object|null}
316
+ */
317
+ resolvedSource() {
318
+ const source = this.content?.source
319
+ if (!source || typeof source !== 'object') {
320
+ return null
321
+ }
322
+ if (typeof source.url === 'string' && source.url !== '') {
323
+ // The endpoint grammar: tokens INSIDE the url, and a request
324
+ // that is blocked while one leaves an empty path segment.
325
+ const request = resolveEndpointRequest({ url: source.url }, this.tokenCtx)
326
+ return { url: request.blocked ? '' : request.url }
327
+ }
328
+ return resolveDeepTokens(source, this.tokenCtx)
329
+ },
330
+
331
+ /**
332
+ * A stable key of the resolved source, so the widget refetches only when it changes.
333
+ *
334
+ * @return {string}
335
+ */
336
+ sourceKey() {
337
+ return this.resolvedSource ? JSON.stringify(this.resolvedSource) : ''
338
+ },
339
+
340
+ /**
341
+ * The object the facts are read from.
342
+ *
343
+ * @return {object|null}
344
+ */
345
+ record() {
346
+ return this.resolvedSource ? this.fetched : this.objectData
347
+ },
348
+
349
+ /**
350
+ * The entries to draw, consecutive rows grouped into one list.
351
+ *
352
+ * @return {Array<object>}
353
+ */
354
+ blocks() {
355
+ const record = this.record
356
+ if (!record) {
357
+ return []
358
+ }
359
+ const fields = Array.isArray(this.content?.fields) ? this.content.fields : []
360
+ const hideEmpty = this.content?.hideEmpty !== false
361
+ const out = []
362
+ fields.forEach((field, index) => {
363
+ if (!field || typeof field !== 'object') {
364
+ return
365
+ }
366
+ const kind = this.kindOf(field)
367
+ const key = String(field.key || field.path || `${kind}-${index}`).replace(/[^\w-]/g, '-')
368
+ const value = this.entryValue(record, field, kind)
369
+ if (value === '' && (hideEmpty || kind === 'lead')) {
370
+ return
371
+ }
372
+ if (kind === 'row') {
373
+ const last = out[out.length - 1]
374
+ const row = { key, label: this.tr(field.label || ''), value, ...this.rowExtras(record, field, value) }
375
+ if (last && last.kind === 'rows') {
376
+ last.rows.push(row)
377
+ } else {
378
+ out.push({ kind: 'rows', key: `rows-${index}`, rows: [row] })
379
+ }
380
+ return
381
+ }
382
+ const block = { kind, key, value }
383
+ if (kind === 'lead') {
384
+ block.subtitle = this.partsValue(record, field.subtitle)
385
+ block.icon = typeof field.icon === 'string' ? field.icon : ''
386
+ block.initials = block.icon ? '' : this.initialsOf(readPath(record, field.initialsPath))
387
+ block.tone = ['success', 'info', 'warning', 'error'].includes(field.tone) ? field.tone : 'neutral'
388
+ }
389
+ if (kind === 'title' && field.route) {
390
+ block.to = this.resolveRoute(field.route, record)
391
+ }
392
+ if (kind === 'pill') {
393
+ const raw = readPath(record, field.path)
394
+ const variant = (field.variantMap && field.variantMap[raw]) || field.variant
395
+ block.variant = isBadgeVariant(variant) ? variant : 'default'
396
+ }
397
+ out.push(block)
398
+ })
399
+ return out
400
+ },
401
+
402
+ /**
403
+ * The foot link, resolved, or null.
404
+ *
405
+ * @return {{label: string, to?: object, href?: string}|null}
406
+ */
407
+ link() {
408
+ const link = this.content?.link
409
+ if (!link || typeof link !== 'object' || !link.label) {
410
+ return null
411
+ }
412
+ if (link.route) {
413
+ const to = this.resolveRoute(link.route, this.record)
414
+ return to ? { label: this.tr(link.label), to } : null
415
+ }
416
+ if (typeof link.href === 'string' && link.href !== '') {
417
+ const href = this.resolveTemplate(link.href, this.record)
418
+ return href ? { label: this.tr(link.label), href } : null
419
+ }
420
+ return null
421
+ },
422
+
423
+ /**
424
+ * The action under the facts, as drawn: `{ label, icon }`, or null.
425
+ *
426
+ * @return {{label: string, icon: string}|null}
427
+ * @spec openspec/changes/screens-relation-pages/specs/relation-pages/spec.md#requirement-the-facts-card-draws-contact-details-identity-and-a-lead
428
+ */
429
+ action() {
430
+ return this.actionView(this.content?.action)
431
+ },
432
+
433
+ /**
434
+ * The header action, as drawn: `{ label, icon }`, or null.
435
+ *
436
+ * @return {{label: string, icon: string}|null}
437
+ * @spec openspec/changes/screens-relation-pages/specs/relation-pages/spec.md#requirement-the-facts-card-draws-contact-details-identity-and-a-lead
438
+ */
439
+ headerAction() {
440
+ return this.actionView(this.content?.headerAction)
441
+ },
442
+
443
+ /** @return {string} The reveal button's text. */
444
+ showLabel() {
445
+ return t('nextcloud-vue', 'Show')
446
+ },
447
+
448
+ /**
449
+ * The empty line.
450
+ *
451
+ * @return {string}
452
+ */
453
+ emptyLabel() {
454
+ return this.tr(this.content?.emptyText || 'Nothing to show yet')
455
+ },
456
+ },
457
+
458
+ watch: {
459
+ sourceKey: {
460
+ immediate: true,
461
+ handler() {
462
+ this.load()
463
+ },
464
+ },
465
+
466
+ record: {
467
+ immediate: true,
468
+ handler() {
469
+ this.loadLabels()
470
+ },
471
+ },
472
+
473
+ headerAction: {
474
+ immediate: true,
475
+ handler() {
476
+ this.emitHeader()
477
+ },
478
+ },
479
+ },
480
+
481
+ methods: {
482
+ /**
483
+ * Translate through the host app.
484
+ *
485
+ * @param {string} text The text.
486
+ * @return {string}
487
+ */
488
+ tr(text) {
489
+ if (typeof text !== 'string' || text === '') {
490
+ return ''
491
+ }
492
+ return typeof this.cnTranslate === 'function' ? this.cnTranslate(text) : text
493
+ },
494
+
495
+ /**
496
+ * The kind of a field entry.
497
+ *
498
+ * @param {object} field The entry.
499
+ * @return {string} `row`, `title`, `line` or `pill`.
500
+ */
501
+ kindOf(field) {
502
+ if (KINDS.includes(field.as)) {
503
+ return field.as
504
+ }
505
+ return field.label || field.icon ? 'row' : 'line'
506
+ },
507
+
508
+ /**
509
+ * The display text of an entry of any kind: a lead's title, the parts of
510
+ * a line or text, a constant `text`, else the value at `path`.
511
+ *
512
+ * @param {object} record The object.
513
+ * @param {object} field The entry.
514
+ * @param {string} kind The entry's kind.
515
+ * @return {string}
516
+ * @spec openspec/changes/screens-relation-pages/specs/relation-pages/spec.md#requirement-the-facts-card-draws-contact-details-identity-and-a-lead
517
+ */
518
+ entryValue(record, field, kind) {
519
+ if (kind === 'lead') {
520
+ return this.partsValue(record, field.title)
521
+ }
522
+ if ((kind === 'line' || kind === 'text') && Array.isArray(field.parts)) {
523
+ return this.lineValue(record, field)
524
+ }
525
+ if ((kind === 'line' || kind === 'text') && !field.path && typeof field.text === 'string') {
526
+ return this.tr(field.text)
527
+ }
528
+ return this.valueOf(record, field)
529
+ },
530
+
531
+ /**
532
+ * A lead's title or subtitle: a path, a part spec, or an array of parts
533
+ * joined as a line joins them.
534
+ *
535
+ * @param {object} record The object.
536
+ * @param {string|object|Array} spec The declaration.
537
+ * @return {string}
538
+ */
539
+ partsValue(record, spec) {
540
+ if (typeof spec === 'string' && spec !== '') {
541
+ return this.valueOf(record, { path: spec })
542
+ }
543
+ if (Array.isArray(spec)) {
544
+ return this.lineValue(record, { parts: spec })
545
+ }
546
+ if (spec && typeof spec === 'object') {
547
+ return this.lineValue(record, { parts: [spec] })
548
+ }
549
+ return ''
550
+ },
551
+
552
+ /**
553
+ * The initials of a name: "Sanne de Vries" reads "SV".
554
+ *
555
+ * @param {unknown} name The name.
556
+ * @return {string}
557
+ */
558
+ initialsOf(name) {
559
+ if (typeof name !== 'string' || name.trim() === '') {
560
+ return ''
561
+ }
562
+ const words = name.trim().split(/\s+/).filter((word) => /^\p{Lu}/u.test(word))
563
+ const picked = words.length ? words : name.trim().split(/\s+/)
564
+ const first = picked[0].charAt(0)
565
+ const last = picked.length > 1 ? picked[picked.length - 1].charAt(0) : ''
566
+ return (first + last).toUpperCase()
567
+ },
568
+
569
+ /**
570
+ * What a row adds to its value: an icon, a link for the email, phone
571
+ * and url formats, the masked text, and the copy and reveal buttons.
572
+ *
573
+ * @param {object} record The object.
574
+ * @param {object} field The entry.
575
+ * @param {string} value The display text.
576
+ * @return {object}
577
+ * @spec openspec/changes/screens-relation-pages/specs/relation-pages/spec.md#requirement-the-facts-card-draws-contact-details-identity-and-a-lead
578
+ */
579
+ rowExtras(record, field, value) {
580
+ const extras = {
581
+ icon: typeof field.icon === 'string' ? field.icon : '',
582
+ path: field.path || '',
583
+ raw: readPath(record, field.path),
584
+ copy: field.copy === true && value !== '',
585
+ reveal: false,
586
+ masked: '',
587
+ href: '',
588
+ external: false,
589
+ }
590
+ if (value === '') {
591
+ return extras
592
+ }
593
+ if (field.format === 'email') {
594
+ extras.href = `mailto:${value}`
595
+ } else if (field.format === 'phone') {
596
+ extras.href = `tel:${value.replace(/[^\d+]/g, '')}`
597
+ } else if (field.format === 'url') {
598
+ extras.href = /^https?:\/\//i.test(value) ? value : `https://${value}`
599
+ extras.external = true
600
+ } else if (field.format === 'masked') {
601
+ extras.masked = value.length > 4 ? '*'.repeat(Math.min(value.length - 4, 5)) + value.slice(-4) : value
602
+ extras.reveal = field.reveal === true && value.length > 4
603
+ }
604
+ return extras
605
+ },
606
+
607
+ /**
608
+ * Show a masked value, and say so: the `reveal` event and the event-bus
609
+ * channel {@link FACTS_REVEAL_CHANNEL}, so an app can log who saw it.
610
+ *
611
+ * @param {object} row The row.
612
+ * @spec openspec/changes/screens-relation-pages/specs/relation-pages/spec.md#requirement-the-facts-card-draws-contact-details-identity-and-a-lead
613
+ */
614
+ reveal(row) {
615
+ this.revealed = { ...this.revealed, [row.key]: true }
616
+ const detail = { path: row.path }
617
+ /**
618
+ * @event reveal Emitted when the reader shows a masked value.
619
+ * @type {{path: string}} The path of the shown field.
620
+ */
621
+ this.$emit('reveal', detail)
622
+ emit(FACTS_REVEAL_CHANNEL, { ...detail, objectId: this.tokenCtx.objectId, register: this.tokenCtx.register || null, schema: this.tokenCtx.schema || null })
623
+ },
624
+
625
+ /**
626
+ * Copy a row's value to the clipboard.
627
+ *
628
+ * @param {object} row The row.
629
+ * @return {Promise<void>}
630
+ */
631
+ async copy(row) {
632
+ const text = row.raw === null || row.raw === undefined || typeof row.raw === 'object' ? row.value : String(row.raw)
633
+ try {
634
+ await navigator.clipboard.writeText(text)
635
+ this.copied = row.key
636
+ setTimeout(() => {
637
+ if (this.copied === row.key) {
638
+ this.copied = ''
639
+ }
640
+ }, 2000)
641
+ } catch (e) {
642
+ // eslint-disable-next-line no-console
643
+ console.warn('[CnFactsWidget] could not copy:', e?.message || e)
644
+ }
645
+ },
646
+
647
+ /**
648
+ * @param {object} row The row.
649
+ * @return {string} The copy button's accessible name.
650
+ */
651
+ copyLabel(row) {
652
+ return this.copied === row.key
653
+ ? t('nextcloud-vue', 'Copied')
654
+ : t('nextcloud-vue', 'Copy {label}', { label: row.label || row.value })
655
+ },
656
+
657
+ /**
658
+ * @param {object} row The row.
659
+ * @return {string} The reveal button's accessible name.
660
+ */
661
+ revealLabel(row) {
662
+ return t('nextcloud-vue', 'Show {label}', { label: row.label || '' }).trim()
663
+ },
664
+
665
+ /**
666
+ * The drawn part of an action declaration.
667
+ *
668
+ * @param {object} action The declaration.
669
+ * @return {{label: string, icon: string}|null}
670
+ */
671
+ actionView(action) {
672
+ if (!action || typeof action !== 'object' || typeof action.label !== 'string' || action.label === '') {
673
+ return null
674
+ }
675
+ return { label: this.tr(action.label), icon: typeof action.icon === 'string' ? action.icon : '' }
676
+ },
677
+
678
+ /**
679
+ * Dispatch an action with the record's tokens resolved (`@objectId`,
680
+ * `@object.<field>`), through the page's dispatcher when there is one.
681
+ *
682
+ * @param {object} action The declaration.
683
+ * @return {unknown}
684
+ * @spec openspec/changes/screens-relation-pages/specs/relation-pages/spec.md#requirement-the-facts-card-draws-contact-details-identity-and-a-lead
685
+ */
686
+ runAction(action) {
687
+ if (!action || typeof action !== 'object') {
688
+ return undefined
689
+ }
690
+ const resolved = resolveDeepTokens(action, this.tokenCtx)
691
+ if (typeof this.cnDispatchAction === 'function') {
692
+ return this.cnDispatchAction(resolved)
693
+ }
694
+ return dispatchAction(resolved, { router: this.$router || null })
695
+ },
696
+
697
+ /** Tell the card around the facts what its header shows (the header action). */
698
+ emitHeader() {
699
+ const view = this.headerAction
700
+ /**
701
+ * @event header Emitted with the card header's parts: the header action, if any.
702
+ * @type {{link: null, note: string, action: (object|null)}} `action` is `{ label, icon, run }`.
703
+ */
704
+ this.$emit('header', {
705
+ link: null,
706
+ note: '',
707
+ action: view ? { ...view, run: () => this.runAction(this.content.headerAction) } : null,
708
+ })
709
+ },
710
+
711
+ /**
712
+ * The text of a reference: the labels of the objects its id(s) name, joined
713
+ * by ", ". An id whose label is not known yet (or cannot be read) adds nothing.
714
+ *
715
+ * @param {unknown} raw The id, or a list of ids (or of `{ id }` objects).
716
+ * @param {object} spec `{ resolve: { register, schema, labelField? }, itemPath? }`.
717
+ * @return {string}
718
+ */
719
+ refText(raw, spec) {
720
+ return this.refIds(raw, spec)
721
+ .map((id) => this.labels[refKey(spec.resolve, id)])
722
+ .filter((label) => typeof label === 'string' && label !== '')
723
+ .join(', ')
724
+ },
725
+
726
+ /**
727
+ * The ids a reference value holds.
728
+ *
729
+ * @param {unknown} raw The value.
730
+ * @param {object} spec `{ itemPath? }`.
731
+ * @return {string[]}
732
+ */
733
+ refIds(raw, spec) {
734
+ return (Array.isArray(raw) ? raw : [raw])
735
+ .map((item) => (item && typeof item === 'object' ? (spec.itemPath ? readPath(item, spec.itemPath) : (item.id ?? item.uuid)) : item))
736
+ .filter((id) => id !== null && id !== undefined && id !== '' && typeof id !== 'object')
737
+ .map(String)
738
+ },
739
+
740
+ /**
741
+ * Every entry or part that carries `resolve`, with the record's value at its path.
742
+ *
743
+ * @param {object} record The object.
744
+ * @return {Array<{spec: object, raw: unknown}>}
745
+ */
746
+ refSpecs(record) {
747
+ const fields = Array.isArray(this.content?.fields) ? this.content.fields : []
748
+ const specs = []
749
+ const visit = (spec) => {
750
+ if (!spec || typeof spec !== 'object') {
751
+ return
752
+ }
753
+ if (spec.resolve && typeof spec.resolve === 'object' && spec.path) {
754
+ specs.push({ spec, raw: readPath(record, spec.path) })
755
+ }
756
+ const nested = [...(Array.isArray(spec.parts) ? spec.parts : []), ...(Array.isArray(spec.title) ? spec.title : [spec.title]), ...(Array.isArray(spec.subtitle) ? spec.subtitle : [spec.subtitle])]
757
+ nested.forEach(visit)
758
+ }
759
+ fields.forEach(visit)
760
+ return specs
761
+ },
762
+
763
+ /**
764
+ * Read the labels of the referenced objects, one batched request per
765
+ * register and schema, and keep them in `labels`.
766
+ *
767
+ * @param {object|null} record The object shown (defaults to the current record).
768
+ * @return {Promise<void>}
769
+ */
770
+ async loadLabels(record = this.record) {
771
+ if (!record) {
772
+ return
773
+ }
774
+ const groups = new Map()
775
+ for (const { spec, raw } of this.refSpecs(record)) {
776
+ const { register, schema, labelField } = spec.resolve
777
+ if (!register || !schema) {
778
+ continue
779
+ }
780
+ const groupKey = `${register}/${schema}/${labelField || ''}`
781
+ const group = groups.get(groupKey) || { resolve: spec.resolve, ids: new Set() }
782
+ this.refIds(raw, spec).forEach((id) => group.ids.add(id))
783
+ groups.set(groupKey, group)
784
+ }
785
+ if (groups.size === 0) {
786
+ return
787
+ }
788
+ const resolver = sharedRefLabels()
789
+ const next = { ...this.labels }
790
+ await Promise.all([...groups.values()].map(async ({ resolve, ids }) => {
791
+ const found = await resolver.resolve(resolve.register, resolve.schema, [...ids], resolve.labelField)
792
+ Object.entries(found).forEach(([id, label]) => {
793
+ next[refKey(resolve, id)] = label
794
+ })
795
+ }))
796
+ this.labels = next
797
+ },
798
+
799
+ /**
800
+ * Read the source object.
801
+ *
802
+ * @return {Promise<void>}
803
+ */
804
+ async load() {
805
+ const source = this.resolvedSource
806
+ this.error = false
807
+ if (!source) {
808
+ this.fetched = null
809
+ return
810
+ }
811
+ const url = this.sourceUrl(source)
812
+ if (!url) {
813
+ this.fetched = null
814
+ return
815
+ }
816
+ this.loading = true
817
+ try {
818
+ const [{ default: axios }, { generateUrl }] = await Promise.all([
819
+ import('@nextcloud/axios'),
820
+ import('@nextcloud/router'),
821
+ ])
822
+ const full = url.startsWith('/apps/') || url.startsWith('/ocs/') ? generateUrl(url) : url
823
+ const response = await axios.get(full)
824
+ const data = response && response.data
825
+ this.fetched = data && typeof data === 'object' && !Array.isArray(data) ? data : null
826
+ } catch (e) {
827
+ // eslint-disable-next-line no-console
828
+ console.warn('[CnFactsWidget] the source could not be read:', e?.message || e)
829
+ this.fetched = null
830
+ this.error = true
831
+ } finally {
832
+ this.loading = false
833
+ }
834
+ },
835
+
836
+ /**
837
+ * The URL of a resolved source, or '' when it cannot be read yet.
838
+ *
839
+ * @param {object} source The resolved source.
840
+ * @return {string}
841
+ */
842
+ sourceUrl(source) {
843
+ if (typeof source.url === 'string') {
844
+ return source.url
845
+ }
846
+ if (hasUnresolvedDeepTokens(source)) {
847
+ return ''
848
+ }
849
+ const { register, schema, id } = source
850
+ if (!register || !schema || id === undefined || id === null || id === '' || typeof id === 'object') {
851
+ return ''
852
+ }
853
+ return `/apps/openregister/api/objects/${encodeURIComponent(register)}/${encodeURIComponent(schema)}/${encodeURIComponent(String(id))}`
854
+ },
855
+
856
+ /**
857
+ * The display text of one field.
858
+ *
859
+ * @param {object} record The object.
860
+ * @param {object} field The entry.
861
+ * @return {string}
862
+ */
863
+ valueOf(record, field) {
864
+ const raw = readPath(record, field.path)
865
+ return this.format(raw, field)
866
+ },
867
+
868
+ /**
869
+ * The text of a `line` entry that joins several parts.
870
+ *
871
+ * @param {object} record The object.
872
+ * @param {object} field The entry.
873
+ * @return {string}
874
+ */
875
+ lineValue(record, field) {
876
+ const separator = typeof field.separator === 'string' ? field.separator : ' · '
877
+ return field.parts
878
+ .map((part) => {
879
+ const spec = typeof part === 'string' ? { path: part } : (part || {})
880
+ // A constant part (`{ text }`): "Handelsregister" after the legal form.
881
+ if (!spec.path && typeof spec.text === 'string') {
882
+ return this.tr(spec.text)
883
+ }
884
+ const value = this.format(readPath(record, spec.path), spec)
885
+ if (value === '') {
886
+ return ''
887
+ }
888
+ return spec.label ? `${this.tr(spec.label)} ${value}` : value
889
+ })
890
+ .filter((value) => value !== '')
891
+ .join(separator)
892
+ },
893
+
894
+ /**
895
+ * Format a raw value.
896
+ *
897
+ * @param {unknown} raw The value.
898
+ * @param {object} spec `{ format?, map?, itemPath? }`.
899
+ * @return {string} The text, '' when empty.
900
+ */
901
+ format(raw, spec) {
902
+ if (raw === null || raw === undefined || raw === '') {
903
+ return ''
904
+ }
905
+ if (spec.resolve && typeof spec.resolve === 'object') {
906
+ return this.refText(raw, spec)
907
+ }
908
+ const format = spec.format || 'text'
909
+ if (format === 'list' || Array.isArray(raw)) {
910
+ const items = (Array.isArray(raw) ? raw : [raw])
911
+ .map((item) => (spec.itemPath && item && typeof item === 'object' ? readPath(item, spec.itemPath) : item))
912
+ .map((item) => this.format(item, { ...spec, format: 'text' }))
913
+ .filter((item) => item !== '')
914
+ return items.join(', ')
915
+ }
916
+ if (format === 'date' || format === 'date-long') {
917
+ const date = parseDateValue(raw)
918
+ if (date === null) {
919
+ return ''
920
+ }
921
+ // A year far from now forces the long form, so date-long reuses the board formatter.
922
+ return format === 'date-long'
923
+ ? formatBoardDate(date, new Date(date.getFullYear() + 1, 0, 1))
924
+ : formatBoardDate(date)
925
+ }
926
+ if (typeof raw === 'object') {
927
+ const label = raw.title || raw.name || raw.label || raw.displayName
928
+ return typeof label === 'string' ? label : ''
929
+ }
930
+ if (typeof raw === 'boolean') {
931
+ const mapped = spec.map && spec.map[String(raw)]
932
+ return mapped ? this.tr(mapped) : this.tr(raw ? 'Yes' : 'No')
933
+ }
934
+ if (spec.map && Object.hasOwn(spec.map, String(raw))) {
935
+ return this.tr(spec.map[String(raw)])
936
+ }
937
+ return String(raw)
938
+ },
939
+
940
+ /**
941
+ * Replace `@source.<field>` / `@source.id` and the shared tokens in a string.
942
+ *
943
+ * @param {string} value The template.
944
+ * @param {object|null} record The object shown.
945
+ * @return {string} The value, '' when a token stays unresolved.
946
+ */
947
+ resolveTemplate(value, record) {
948
+ if (typeof value !== 'string') {
949
+ return value
950
+ }
951
+ let unresolved = false
952
+ const replaced = value.replace(/@source\.([\w.@]+)/g, (match, path) => {
953
+ const read = record ? (path === 'id' ? (record.id ?? readPath(record, '@self.id')) : readPath(record, path)) : undefined
954
+ if (read === undefined || read === null || read === '') {
955
+ unresolved = true
956
+ return match
957
+ }
958
+ return String(read)
959
+ })
960
+ if (unresolved) {
961
+ return ''
962
+ }
963
+ if (replaced.charAt(0) === '@') {
964
+ const resolved = resolveDeepTokens(replaced, this.tokenCtx)
965
+ return typeof resolved === 'string' && resolved.charAt(0) === '@' ? '' : String(resolved)
966
+ }
967
+ return replaced
968
+ },
969
+
970
+ /**
971
+ * Resolve a route declaration (`{ name, params?, query? }`).
972
+ *
973
+ * @param {object} route The declaration.
974
+ * @param {object|null} record The object shown.
975
+ * @return {object|null} The route, or null when a param stays unresolved.
976
+ */
977
+ resolveRoute(route, record) {
978
+ if (!route || typeof route !== 'object' || !route.name) {
979
+ return null
980
+ }
981
+ const out = { name: route.name }
982
+ for (const key of ['params', 'query']) {
983
+ if (route[key] && typeof route[key] === 'object') {
984
+ out[key] = {}
985
+ for (const [name, value] of Object.entries(route[key])) {
986
+ const resolved = this.resolveTemplate(value, record)
987
+ if (resolved === '') {
988
+ return null
989
+ }
990
+ out[key][name] = resolved
991
+ }
992
+ }
993
+ }
994
+ return out
995
+ },
996
+ },
997
+ }
998
+ </script>
999
+
1000
+ <style scoped>
1001
+ .cn-facts-widget {
1002
+ display: flex;
1003
+ flex-direction: column;
1004
+ gap: 10px;
1005
+ min-width: 0;
1006
+ }
1007
+
1008
+ .cn-facts-widget__rows {
1009
+ display: grid;
1010
+ grid-template-columns: max-content 1fr;
1011
+ gap: 8px 16px;
1012
+ margin: 0;
1013
+ font-size: 14px;
1014
+ }
1015
+
1016
+ /* Each row is a grid of its own children: the wrapper takes no box. */
1017
+ .cn-facts-widget__row {
1018
+ display: contents;
1019
+ }
1020
+
1021
+ /* Nextcloud core styles `dt` and `dd` (text-align end, 12px padding), which
1022
+ pushed the label to the right of its column and added 12px around both. The
1023
+ class is doubled so these resets outrank that rule, and a dossiq-style
1024
+ `:deep()` override in an app is not needed. */
1025
+ .cn-facts-widget__label.cn-facts-widget__label {
1026
+ margin: 0;
1027
+ padding: 0;
1028
+ text-align: start;
1029
+ color: var(--color-text-maxcontrast);
1030
+ }
1031
+
1032
+ .cn-facts-widget__value.cn-facts-widget__value {
1033
+ margin: 0;
1034
+ padding: 0;
1035
+ text-align: start;
1036
+ font-weight: 500;
1037
+ min-width: 0;
1038
+ overflow-wrap: anywhere;
1039
+ }
1040
+
1041
+ .cn-facts-widget__title {
1042
+ font-size: 15px;
1043
+ font-weight: 600;
1044
+ color: var(--color-main-text);
1045
+ }
1046
+
1047
+ .cn-facts-widget__line {
1048
+ font-size: 13px;
1049
+ color: var(--color-text-maxcontrast);
1050
+ }
1051
+
1052
+ .cn-facts-widget__muted {
1053
+ margin: 0;
1054
+ color: var(--color-text-maxcontrast);
1055
+ }
1056
+
1057
+ .cn-facts-widget__label .hidden-visually {
1058
+ clip-path: inset(50%);
1059
+ height: 1px;
1060
+ overflow: hidden;
1061
+ position: absolute;
1062
+ white-space: nowrap;
1063
+ width: 1px;
1064
+ }
1065
+
1066
+ .cn-facts-widget__label--icon {
1067
+ align-items: center;
1068
+ display: flex;
1069
+ }
1070
+
1071
+ .cn-facts-widget__value-link {
1072
+ color: var(--color-primary-element);
1073
+ text-decoration: underline;
1074
+ }
1075
+
1076
+ .cn-facts-widget__inline-button,
1077
+ .cn-facts-widget__icon-button,
1078
+ .cn-facts-widget__action {
1079
+ align-items: center;
1080
+ cursor: pointer;
1081
+ display: inline-flex;
1082
+ font: inherit;
1083
+ }
1084
+
1085
+ .cn-facts-widget__inline-button {
1086
+ background: var(--color-main-background);
1087
+ border: 1px solid var(--color-border-dark);
1088
+ border-radius: 16px;
1089
+ font-size: 13px;
1090
+ font-weight: 600;
1091
+ gap: 4px;
1092
+ margin-inline-start: 8px;
1093
+ min-height: 28px;
1094
+ padding: 0 10px;
1095
+ }
1096
+
1097
+ .cn-facts-widget__icon-button {
1098
+ background: transparent;
1099
+ border: 0;
1100
+ border-radius: 6px;
1101
+ color: var(--color-text-maxcontrast);
1102
+ margin-inline-start: 4px;
1103
+ padding: 2px;
1104
+ vertical-align: middle;
1105
+ }
1106
+
1107
+ .cn-facts-widget__icon-button:hover,
1108
+ .cn-facts-widget__icon-button:focus-visible {
1109
+ background: var(--color-background-hover);
1110
+ color: var(--color-main-text);
1111
+ }
1112
+
1113
+ .cn-facts-widget__text {
1114
+ color: var(--color-main-text);
1115
+ font-size: 15px;
1116
+ }
1117
+
1118
+ /* A lead: an icon tile or initials beside a bold title and a muted subtitle. */
1119
+ .cn-facts-widget__lead {
1120
+ align-items: center;
1121
+ display: flex;
1122
+ gap: 12px;
1123
+ }
1124
+
1125
+ .cn-facts-widget__lead-tile {
1126
+ align-items: center;
1127
+ background: var(--color-background-dark);
1128
+ border-radius: 8px;
1129
+ color: var(--color-main-text);
1130
+ display: flex;
1131
+ flex: none;
1132
+ font-size: 14px;
1133
+ font-weight: 700;
1134
+ height: 40px;
1135
+ justify-content: center;
1136
+ width: 40px;
1137
+ }
1138
+
1139
+ .cn-facts-widget__lead-tile--success {
1140
+ background: color-mix(in srgb, var(--color-success) 14%, var(--color-main-background));
1141
+ border-radius: 50%;
1142
+ color: var(--color-success-text, var(--color-success));
1143
+ }
1144
+
1145
+ .cn-facts-widget__lead-tile--info {
1146
+ background: var(--color-primary-element-light);
1147
+ border-radius: 50%;
1148
+ color: var(--color-primary-element-light-text, var(--color-primary-element));
1149
+ }
1150
+
1151
+ .cn-facts-widget__lead-text {
1152
+ display: flex;
1153
+ flex-direction: column;
1154
+ min-width: 0;
1155
+ }
1156
+
1157
+ .cn-facts-widget__lead-title {
1158
+ font-size: 15px;
1159
+ }
1160
+
1161
+ .cn-facts-widget__lead-subtitle {
1162
+ color: var(--color-text-maxcontrast);
1163
+ font-size: 13px;
1164
+ }
1165
+
1166
+ .cn-facts-widget__action {
1167
+ align-self: flex-start;
1168
+ background: var(--color-primary-element-light);
1169
+ border: 0;
1170
+ border-radius: 16px;
1171
+ color: var(--color-primary-element-light-text, var(--color-primary-element));
1172
+ font-size: 13px;
1173
+ font-weight: 700;
1174
+ gap: 6px;
1175
+ min-height: 32px;
1176
+ padding: 0 14px;
1177
+ }
1178
+
1179
+ .cn-facts-widget__action:hover,
1180
+ .cn-facts-widget__action:focus-visible {
1181
+ background: var(--color-primary-element-light-hover, var(--color-primary-element-light));
1182
+ }
1183
+
1184
+ .cn-facts-widget__link {
1185
+ font-size: 14px;
1186
+ font-weight: 600;
1187
+ color: var(--color-primary-element);
1188
+ text-decoration: underline;
1189
+ }
1190
+ </style>