@conduction/nextcloud-vue 2.56.0-beta.5 → 2.56.0-beta.7

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 (239) hide show
  1. package/dist/esm/components/CnActionsMenu/CnActionsMenu.vue2.js +14 -1
  2. package/dist/esm/components/CnActionsMenu/CnActionsMenu.vue2.js.map +1 -1
  3. package/dist/esm/components/CnActionsMenu/CnActionsMenu.vue3.js.map +1 -1
  4. package/dist/esm/components/CnAppNav/CnAppNav.vue2.js +39 -0
  5. package/dist/esm/components/CnAppNav/CnAppNav.vue2.js.map +1 -1
  6. package/dist/esm/components/CnAppNav/CnAppNav.vue3.js +46 -4
  7. package/dist/esm/components/CnAppNav/CnAppNav.vue3.js.map +1 -1
  8. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js +224 -9
  9. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js.map +1 -1
  10. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue3.js +79 -25
  11. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue3.js.map +1 -1
  12. package/dist/esm/components/CnBannerWidgetForm/CnBannerWidgetForm.vue2.js +42 -3
  13. package/dist/esm/components/CnBannerWidgetForm/CnBannerWidgetForm.vue2.js.map +1 -1
  14. package/dist/esm/components/CnBannerWidgetForm/CnBannerWidgetForm.vue3.js +39 -6
  15. package/dist/esm/components/CnBannerWidgetForm/CnBannerWidgetForm.vue3.js.map +1 -1
  16. package/dist/esm/components/CnBoardView/CnBoardView.vue2.js +46 -0
  17. package/dist/esm/components/CnBoardView/CnBoardView.vue2.js.map +1 -1
  18. package/dist/esm/components/CnBoardView/CnBoardView.vue3.js +11 -4
  19. package/dist/esm/components/CnBoardView/CnBoardView.vue3.js.map +1 -1
  20. package/dist/esm/components/CnBrandStripe/CnBrandStripe.vue.js +10 -0
  21. package/dist/esm/components/CnBrandStripe/CnBrandStripe.vue.js.map +1 -0
  22. package/dist/esm/components/CnBrandStripe/CnBrandStripe.vue2.js +39 -0
  23. package/dist/esm/components/CnBrandStripe/CnBrandStripe.vue2.js.map +1 -0
  24. package/dist/esm/components/CnBrandStripe/CnBrandStripe.vue3.js +16 -0
  25. package/dist/esm/components/CnBrandStripe/CnBrandStripe.vue3.js.map +1 -0
  26. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js +117 -1
  27. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js.map +1 -1
  28. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js +147 -89
  29. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js.map +1 -1
  30. package/dist/esm/components/CnConversationThread/CnConversationThread.vue.js +10 -0
  31. package/dist/esm/components/CnConversationThread/CnConversationThread.vue.js.map +1 -0
  32. package/dist/esm/components/CnConversationThread/CnConversationThread.vue2.js +259 -0
  33. package/dist/esm/components/CnConversationThread/CnConversationThread.vue2.js.map +1 -0
  34. package/dist/esm/components/CnConversationThread/CnConversationThread.vue3.js +131 -0
  35. package/dist/esm/components/CnConversationThread/CnConversationThread.vue3.js.map +1 -0
  36. package/dist/esm/components/CnConversationThread/CnConversationWidget.vue.js +10 -0
  37. package/dist/esm/components/CnConversationThread/CnConversationWidget.vue.js.map +1 -0
  38. package/dist/esm/components/CnConversationThread/CnConversationWidget.vue2.js +116 -0
  39. package/dist/esm/components/CnConversationThread/CnConversationWidget.vue2.js.map +1 -0
  40. package/dist/esm/components/CnConversationThread/CnConversationWidget.vue3.js +24 -0
  41. package/dist/esm/components/CnConversationThread/CnConversationWidget.vue3.js.map +1 -0
  42. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +503 -28
  43. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  44. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +300 -142
  45. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  46. package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewList.vue.js +10 -0
  47. package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewList.vue.js.map +1 -0
  48. package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewList.vue2.js +237 -0
  49. package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewList.vue2.js.map +1 -0
  50. package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewList.vue3.js +120 -0
  51. package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewList.vue3.js.map +1 -0
  52. package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewWidget.vue.js +10 -0
  53. package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewWidget.vue.js.map +1 -0
  54. package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewWidget.vue2.js +77 -0
  55. package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewWidget.vue2.js.map +1 -0
  56. package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewWidget.vue3.js +27 -0
  57. package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewWidget.vue3.js.map +1 -0
  58. package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue2.js +169 -12
  59. package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue2.js.map +1 -1
  60. package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue3.js +15 -7
  61. package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue3.js.map +1 -1
  62. package/dist/esm/components/CnHeaderWidgetForm/CnHeaderWidgetForm.vue2.js +37 -3
  63. package/dist/esm/components/CnHeaderWidgetForm/CnHeaderWidgetForm.vue2.js.map +1 -1
  64. package/dist/esm/components/CnHeaderWidgetForm/CnHeaderWidgetForm.vue3.js +44 -16
  65. package/dist/esm/components/CnHeaderWidgetForm/CnHeaderWidgetForm.vue3.js.map +1 -1
  66. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +132 -3
  67. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  68. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +6 -3
  69. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  70. package/dist/esm/components/CnNextStepCard/CnNextStepCard.vue.js +10 -0
  71. package/dist/esm/components/CnNextStepCard/CnNextStepCard.vue.js.map +1 -0
  72. package/dist/esm/components/CnNextStepCard/CnNextStepCard.vue2.js +150 -0
  73. package/dist/esm/components/CnNextStepCard/CnNextStepCard.vue2.js.map +1 -0
  74. package/dist/esm/components/CnNextStepCard/CnNextStepCard.vue3.js +106 -0
  75. package/dist/esm/components/CnNextStepCard/CnNextStepCard.vue3.js.map +1 -0
  76. package/dist/esm/components/CnNextStepCard/CnNextStepWidget.vue.js +9 -0
  77. package/dist/esm/components/CnNextStepCard/CnNextStepWidget.vue.js.map +1 -0
  78. package/dist/esm/components/CnNextStepCard/CnNextStepWidget.vue2.js +47 -0
  79. package/dist/esm/components/CnNextStepCard/CnNextStepWidget.vue2.js.map +1 -0
  80. package/dist/esm/components/CnNextStepCard/CnNextStepWidget.vue3.js +17 -0
  81. package/dist/esm/components/CnNextStepCard/CnNextStepWidget.vue3.js.map +1 -0
  82. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js +49 -0
  83. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js.map +1 -1
  84. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue3.js.map +1 -1
  85. package/dist/esm/components/CnObjectDataWidgetForm/CnObjectDataWidgetForm.vue2.js +6 -0
  86. package/dist/esm/components/CnObjectDataWidgetForm/CnObjectDataWidgetForm.vue2.js.map +1 -1
  87. package/dist/esm/components/CnObjectDataWidgetForm/CnObjectDataWidgetForm.vue3.js.map +1 -1
  88. package/dist/esm/components/CnObjectKanban/CnObjectKanban.vue2.js +46 -0
  89. package/dist/esm/components/CnObjectKanban/CnObjectKanban.vue2.js.map +1 -1
  90. package/dist/esm/components/CnObjectKanban/CnObjectKanban.vue3.js +15 -3
  91. package/dist/esm/components/CnObjectKanban/CnObjectKanban.vue3.js.map +1 -1
  92. package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue2.js +37 -1
  93. package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue2.js.map +1 -1
  94. package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue3.js +29 -13
  95. package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue3.js.map +1 -1
  96. package/dist/esm/components/CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue2.js +100 -3
  97. package/dist/esm/components/CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue2.js.map +1 -1
  98. package/dist/esm/components/CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue3.js.map +1 -1
  99. package/dist/esm/components/CnSavedViewsControl/CnSavedViewsControl.vue2.js +50 -3
  100. package/dist/esm/components/CnSavedViewsControl/CnSavedViewsControl.vue2.js.map +1 -1
  101. package/dist/esm/components/CnSavedViewsControl/CnSavedViewsControl.vue3.js.map +1 -1
  102. package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue.js +10 -0
  103. package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue.js.map +1 -0
  104. package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue2.js +186 -0
  105. package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue2.js.map +1 -0
  106. package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue3.js +46 -0
  107. package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue3.js.map +1 -0
  108. package/dist/esm/components/CnStackedBarWidget/CnStackedBarWidget.vue.js +11 -0
  109. package/dist/esm/components/CnStackedBarWidget/CnStackedBarWidget.vue.js.map +1 -0
  110. package/dist/esm/components/CnStackedBarWidget/CnStackedBarWidget.vue2.js +298 -0
  111. package/dist/esm/components/CnStackedBarWidget/CnStackedBarWidget.vue2.js.map +1 -0
  112. package/dist/esm/components/CnStackedBarWidget/CnStackedBarWidget.vue3.js +77 -0
  113. package/dist/esm/components/CnStackedBarWidget/CnStackedBarWidget.vue3.js.map +1 -0
  114. package/dist/esm/components/CnStackedBarWidgetForm/CnStackedBarWidgetForm.vue.js +10 -0
  115. package/dist/esm/components/CnStackedBarWidgetForm/CnStackedBarWidgetForm.vue.js.map +1 -0
  116. package/dist/esm/components/CnStackedBarWidgetForm/CnStackedBarWidgetForm.vue2.js +169 -0
  117. package/dist/esm/components/CnStackedBarWidgetForm/CnStackedBarWidgetForm.vue2.js.map +1 -0
  118. package/dist/esm/components/CnStackedBarWidgetForm/CnStackedBarWidgetForm.vue3.js +48 -0
  119. package/dist/esm/components/CnStackedBarWidgetForm/CnStackedBarWidgetForm.vue3.js.map +1 -0
  120. package/dist/esm/components/CnTabs/CnTab.vue2.js +35 -0
  121. package/dist/esm/components/CnTabs/CnTab.vue2.js.map +1 -1
  122. package/dist/esm/components/CnTabs/CnTab.vue3.js.map +1 -1
  123. package/dist/esm/components/CnTabs/CnTabs.vue2.js +120 -7
  124. package/dist/esm/components/CnTabs/CnTabs.vue2.js.map +1 -1
  125. package/dist/esm/components/CnTabs/CnTabs.vue3.js +57 -5
  126. package/dist/esm/components/CnTabs/CnTabs.vue3.js.map +1 -1
  127. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +59 -4
  128. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
  129. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js +6 -2
  130. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
  131. package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue.js +10 -0
  132. package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue.js.map +1 -0
  133. package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue2.js +484 -0
  134. package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue2.js.map +1 -0
  135. package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue3.js +115 -0
  136. package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue3.js.map +1 -0
  137. package/dist/esm/components/CnWeekStripWidgetForm/CnWeekStripWidgetForm.vue.js +10 -0
  138. package/dist/esm/components/CnWeekStripWidgetForm/CnWeekStripWidgetForm.vue.js.map +1 -0
  139. package/dist/esm/components/CnWeekStripWidgetForm/CnWeekStripWidgetForm.vue2.js +189 -0
  140. package/dist/esm/components/CnWeekStripWidgetForm/CnWeekStripWidgetForm.vue2.js.map +1 -0
  141. package/dist/esm/components/CnWeekStripWidgetForm/CnWeekStripWidgetForm.vue3.js +74 -0
  142. package/dist/esm/components/CnWeekStripWidgetForm/CnWeekStripWidgetForm.vue3.js.map +1 -0
  143. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js +82 -8
  144. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js.map +1 -1
  145. package/dist/esm/index.js +10 -0
  146. package/dist/esm/index.js.map +1 -1
  147. package/dist/esm/l10n/en.json.js +54 -1
  148. package/dist/esm/l10n/en.json.js.map +1 -1
  149. package/dist/esm/l10n/nl.json.js +54 -1
  150. package/dist/esm/l10n/nl.json.js.map +1 -1
  151. package/dist/esm/nextcloud-vue.css +931 -4
  152. package/dist/esm/utils/dateVariant.js +124 -0
  153. package/dist/esm/utils/dateVariant.js.map +1 -0
  154. package/dist/esm/utils/detailActionModel.js +260 -0
  155. package/dist/esm/utils/detailActionModel.js.map +1 -0
  156. package/dist/esm/utils/dueRule.js +84 -0
  157. package/dist/esm/utils/dueRule.js.map +1 -0
  158. package/dist/esm/utils/fetchAggregate.js +37 -1
  159. package/dist/esm/utils/fetchAggregate.js.map +1 -1
  160. package/dist/esm/utils/fetchFilterCounts.js +186 -0
  161. package/dist/esm/utils/fetchFilterCounts.js.map +1 -0
  162. package/dist/esm/utils/libraryWidgetKeys.js +5 -0
  163. package/dist/esm/utils/libraryWidgetKeys.js.map +1 -1
  164. package/dist/esm/utils/readPath.js +30 -0
  165. package/dist/esm/utils/readPath.js.map +1 -0
  166. package/dist/esm/utils/validateManifestV2.compiled.js +2 -2
  167. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  168. package/dist/nextcloud-vue.cjs.js +15070 -8994
  169. package/dist/nextcloud-vue.cjs.js.map +2 -2
  170. package/dist/nextcloud-vue.css +931 -4
  171. package/l10n/en.json +54 -1
  172. package/l10n/nl.json +54 -1
  173. package/package.json +1 -1
  174. package/scripts/.jsdoc-baselines.json +3 -0
  175. package/src/components/CnActionsMenu/CnActionsMenu.vue +14 -1
  176. package/src/components/CnAppNav/CnAppNav.md +26 -0
  177. package/src/components/CnAppNav/CnAppNav.vue +104 -1
  178. package/src/components/CnBannerWidget/CnBannerWidget.vue +365 -10
  179. package/src/components/CnBannerWidgetForm/CnBannerWidgetForm.vue +65 -3
  180. package/src/components/CnBoardView/CnBoardView.vue +77 -0
  181. package/src/components/CnBrandStripe/CnBrandStripe.md +22 -0
  182. package/src/components/CnBrandStripe/CnBrandStripe.vue +100 -0
  183. package/src/components/CnBrandStripe/index.js +4 -0
  184. package/src/components/CnCellRenderer/CnCellRenderer.vue +207 -1
  185. package/src/components/CnConversationThread/CnConversationThread.md +51 -0
  186. package/src/components/CnConversationThread/CnConversationThread.vue +457 -0
  187. package/src/components/CnConversationThread/CnConversationWidget.vue +144 -0
  188. package/src/components/CnConversationThread/index.js +4 -0
  189. package/src/components/CnDetailPage/CnDetailPage.md +16 -0
  190. package/src/components/CnDetailPage/CnDetailPage.vue +683 -48
  191. package/src/components/CnDocumentReviewList/CnDocumentReviewList.md +55 -0
  192. package/src/components/CnDocumentReviewList/CnDocumentReviewList.vue +384 -0
  193. package/src/components/CnDocumentReviewList/CnDocumentReviewWidget.vue +108 -0
  194. package/src/components/CnDocumentReviewList/index.js +4 -0
  195. package/src/components/CnHeaderWidget/CnHeaderWidget.vue +189 -14
  196. package/src/components/CnHeaderWidgetForm/CnHeaderWidgetForm.vue +53 -3
  197. package/src/components/CnIndexPage/CnIndexPage.vue +135 -3
  198. package/src/components/CnNextStepCard/CnNextStepCard.md +40 -0
  199. package/src/components/CnNextStepCard/CnNextStepCard.vue +319 -0
  200. package/src/components/CnNextStepCard/CnNextStepWidget.vue +59 -0
  201. package/src/components/CnNextStepCard/index.js +4 -0
  202. package/src/components/CnObjectDataWidget/CnObjectDataWidget.vue +49 -0
  203. package/src/components/CnObjectDataWidgetForm/CnObjectDataWidgetForm.vue +6 -0
  204. package/src/components/CnObjectKanban/CnObjectKanban.vue +86 -2
  205. package/src/components/CnQuickFilterBar/CnQuickFilterBar.md +3 -0
  206. package/src/components/CnQuickFilterBar/CnQuickFilterBar.vue +60 -1
  207. package/src/components/CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue +100 -3
  208. package/src/components/CnSavedViewsControl/CnSavedViewsControl.vue +50 -3
  209. package/src/components/CnSegmentedControl/CnSegmentedControl.md +46 -0
  210. package/src/components/CnSegmentedControl/CnSegmentedControl.vue +291 -0
  211. package/src/components/CnSegmentedControl/index.js +4 -0
  212. package/src/components/CnStackedBarWidget/CnStackedBarWidget.md +15 -0
  213. package/src/components/CnStackedBarWidget/CnStackedBarWidget.vue +414 -0
  214. package/src/components/CnStackedBarWidget/index.js +14 -0
  215. package/src/components/CnStackedBarWidgetForm/CnStackedBarWidgetForm.vue +224 -0
  216. package/src/components/CnStackedBarWidgetForm/index.js +4 -0
  217. package/src/components/CnTabs/CnTab.vue +35 -0
  218. package/src/components/CnTabs/CnTabs.vue +244 -10
  219. package/src/components/CnTabsWidget/CnTabsWidget.vue +63 -4
  220. package/src/components/CnWeekStripWidget/CnWeekStripWidget.md +31 -0
  221. package/src/components/CnWeekStripWidget/CnWeekStripWidget.vue +668 -0
  222. package/src/components/CnWeekStripWidget/index.js +14 -0
  223. package/src/components/CnWeekStripWidgetForm/CnWeekStripWidgetForm.vue +265 -0
  224. package/src/components/CnWeekStripWidgetForm/index.js +4 -0
  225. package/src/components/CnWidgetGrid/registerDashboardWidgets.js +74 -0
  226. package/src/components/index.js +11 -0
  227. package/src/css/detail-page.css +47 -0
  228. package/src/index.js +9 -0
  229. package/src/schemas/app-manifest-v2.schema.json +389 -1
  230. package/src/schemas/app-manifest.schema.json +2 -1
  231. package/src/utils/dateVariant.js +120 -0
  232. package/src/utils/detailActionModel.js +256 -0
  233. package/src/utils/dueRule.js +94 -0
  234. package/src/utils/fetchAggregate.js +36 -0
  235. package/src/utils/fetchFilterCounts.js +182 -0
  236. package/src/utils/index.js +1 -0
  237. package/src/utils/libraryWidgetKeys.js +5 -0
  238. package/src/utils/readPath.js +27 -0
  239. package/src/utils/validateManifestV2.compiled.js +2 -2
@@ -0,0 +1,259 @@
1
+ import axios__default from '@nextcloud/axios';
2
+ import { translate } from '@nextcloud/l10n';
3
+ import { NcButton } from '@nextcloud/vue';
4
+ import { prefixUrl } from '../../utils/headers.js';
5
+
6
+ let conversationThreadUid = 0;
7
+
8
+ /**
9
+ * CnConversationThread: the messages between you and the other party, and a box to answer.
10
+ *
11
+ * Each message has an author, a time and a side: `us` for the organisation,
12
+ * `them` for the resident or customer. The reply box emits `send`, and also
13
+ * posts to `endpoint` when one is set. It needs no Talk and no other app:
14
+ * where the messages are stored is the host's business.
15
+ *
16
+ * ```vue
17
+ * <CnConversationThread
18
+ * title="Contact with the resident"
19
+ * reply-label="Answer to the resident"
20
+ * :messages="[
21
+ * { id: 1, author: 'Sanne de Vries', time: '2026-10-04T10:52:00', side: 'them',
22
+ * text: 'Thank you. I would like to see all documents.' },
23
+ * { id: 2, time: '2026-10-04T10:24:00', side: 'us',
24
+ * text: 'Construction starts in March.' },
25
+ * ]"
26
+ * @send="saveReply" />
27
+ * ```
28
+ *
29
+ * The `conversation` detail widget type renders this thread from a field on
30
+ * the record.
31
+ */
32
+ var script = {
33
+ name: 'CnConversationThread',
34
+
35
+ components: {
36
+ NcButton,
37
+ },
38
+
39
+ props: {
40
+ /**
41
+ * The messages, oldest first: `{ id?, author?, time?, text, side? }`.
42
+ * `side` is `us` or `them` (default `them`). `time` is an ISO
43
+ * date, or any text to show as is.
44
+ */
45
+ messages: {
46
+ type: Array,
47
+ default: () => [],
48
+ },
49
+
50
+ /** Heading of the thread. */
51
+ title: {
52
+ type: String,
53
+ default: '',
54
+ },
55
+
56
+ /** Element the heading renders as. */
57
+ titleTag: {
58
+ type: String,
59
+ default: 'h3',
60
+ },
61
+
62
+ /** Whether the reply box renders. Turn it off for a closed or read-only record. */
63
+ allowReply: {
64
+ type: Boolean,
65
+ default: true,
66
+ },
67
+
68
+ /** Label of the reply box. */
69
+ replyLabel: {
70
+ type: String,
71
+ default: () => translate('nextcloud-vue', 'Reply'),
72
+ },
73
+
74
+ /** Placeholder inside the reply box. */
75
+ placeholder: {
76
+ type: String,
77
+ default: '',
78
+ },
79
+
80
+ /** Label of the send button. */
81
+ sendLabel: {
82
+ type: String,
83
+ default: () => translate('nextcloud-vue', 'Send'),
84
+ },
85
+
86
+ /** Author shown on an `us` message that names none. */
87
+ usLabel: {
88
+ type: String,
89
+ default: () => translate('nextcloud-vue', 'You'),
90
+ },
91
+
92
+ /**
93
+ * URL the reply is POSTed to as `{ message }`. Empty means the host
94
+ * handles `send` itself and nothing is requested.
95
+ */
96
+ endpoint: {
97
+ type: String,
98
+ default: '',
99
+ },
100
+
101
+ /** Extra fields sent along with `message` when `endpoint` is set. */
102
+ payload: {
103
+ type: Object,
104
+ default: () => ({}),
105
+ },
106
+
107
+ /** Shown when there are no messages yet. */
108
+ emptyText: {
109
+ type: String,
110
+ default: () => translate('nextcloud-vue', 'No messages yet.'),
111
+ },
112
+ },
113
+
114
+ emits: [
115
+ /** A reply was submitted. Payload: the text. */
116
+ 'send',
117
+ /** The endpoint accepted the reply. Payload: the response body. */
118
+ 'sent',
119
+ /** The endpoint refused the reply. Payload: the error. */
120
+ 'error',
121
+ ],
122
+
123
+ data() {
124
+ conversationThreadUid += 1;
125
+ return {
126
+ draft: '',
127
+ sending: false,
128
+ errorMessage: '',
129
+ titleId: `cn-conversation-title-${conversationThreadUid}`,
130
+ inputId: `cn-conversation-input-${conversationThreadUid}`,
131
+ errorId: `cn-conversation-error-${conversationThreadUid}`,
132
+ }
133
+ },
134
+
135
+ computed: {
136
+ /**
137
+ * The messages, with author, side, time and initials resolved.
138
+ *
139
+ * @return {Array<{key: string, author: string, initials: string, side: string, text: string, timeLabel: string, datetime: string}>} The rows.
140
+ * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-conversation-thread
141
+ */
142
+ rows() {
143
+ return (Array.isArray(this.messages) ? this.messages : [])
144
+ .filter((message) => message && typeof message === 'object')
145
+ .map((message, index) => {
146
+ const side = message.side === 'us' ? 'us' : 'them';
147
+ const author = typeof message.author === 'string' && message.author !== ''
148
+ ? message.author
149
+ : (side === 'us' ? this.usLabel : '');
150
+ const time = this.formatTime(message.time);
151
+ return {
152
+ key: message.id === undefined || message.id === null ? `message-${index}` : String(message.id),
153
+ author,
154
+ initials: this.initialsOf(author),
155
+ side,
156
+ text: message.text === undefined || message.text === null ? '' : String(message.text),
157
+ timeLabel: time.label,
158
+ datetime: time.datetime,
159
+ }
160
+ })
161
+ },
162
+
163
+ /**
164
+ * Whether there is something to send and nothing in flight.
165
+ *
166
+ * @return {boolean} True when Send is enabled.
167
+ * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-conversation-thread
168
+ */
169
+ canSend() {
170
+ return !this.sending && this.draft.trim() !== ''
171
+ },
172
+
173
+ /**
174
+ * Accessible name for a thread without a title.
175
+ *
176
+ * @return {string} The label.
177
+ * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-conversation-thread
178
+ */
179
+ fallbackLabel() {
180
+ return translate('nextcloud-vue', 'Conversation')
181
+ },
182
+ },
183
+
184
+ methods: {
185
+ /**
186
+ * Up to two initials of a name, for the avatar.
187
+ *
188
+ * @param {string} name The author.
189
+ * @return {string} The initials, upper case.
190
+ * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-conversation-thread
191
+ */
192
+ initialsOf(name) {
193
+ const parts = String(name || '').trim().split(/\s+/).filter(Boolean);
194
+ if (parts.length === 0) {
195
+ return ''
196
+ }
197
+ const first = parts[0].charAt(0);
198
+ const last = parts.length > 1 ? parts[parts.length - 1].charAt(0) : '';
199
+ return (first + last).toUpperCase()
200
+ },
201
+
202
+ /**
203
+ * Turn a message time into what the reader sees and what `<time>` carries.
204
+ *
205
+ * @param {unknown} value An ISO date, a Date, or text to show as is.
206
+ * @return {{label: string, datetime: string}} The visible label and the machine value.
207
+ * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-conversation-thread
208
+ */
209
+ formatTime(value) {
210
+ if (value === null || value === undefined || value === '') {
211
+ return { label: '', datetime: '' }
212
+ }
213
+ const date = value instanceof Date ? value : new Date(value);
214
+ const looksLikeDate = value instanceof Date || typeof value === 'number' || /^\d{4}-\d{2}-\d{2}/.test(String(value));
215
+ if (!looksLikeDate || Number.isNaN(date.getTime())) {
216
+ return { label: String(value), datetime: '' }
217
+ }
218
+ const label = date.toLocaleString(undefined, { day: 'numeric', month: 'short', hour: '2-digit', minute: '2-digit' });
219
+ return { label, datetime: date.toISOString() }
220
+ },
221
+
222
+ /**
223
+ * Submit the reply: tell the host, and post it when an endpoint is set.
224
+ *
225
+ * Without an endpoint the box clears straight away, because the host
226
+ * owns the send. With one it clears only when the request succeeds, so
227
+ * a refused reply is still there to try again.
228
+ *
229
+ * @return {Promise<void>}
230
+ * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-conversation-thread
231
+ */
232
+ async onSend() {
233
+ if (!this.canSend) {
234
+ return
235
+ }
236
+ const text = this.draft.trim();
237
+ this.errorMessage = '';
238
+ this.$emit('send', text);
239
+ if (!this.endpoint) {
240
+ this.draft = '';
241
+ return
242
+ }
243
+ this.sending = true;
244
+ try {
245
+ const response = await axios__default.post(prefixUrl(this.endpoint), { ...this.payload, message: text });
246
+ this.draft = '';
247
+ this.$emit('sent', response?.data);
248
+ } catch (error) {
249
+ this.errorMessage = translate('nextcloud-vue', 'The message could not be sent. Try again.');
250
+ this.$emit('error', error);
251
+ } finally {
252
+ this.sending = false;
253
+ }
254
+ },
255
+ },
256
+ };
257
+
258
+ export { script as default };
259
+ //# sourceMappingURL=CnConversationThread.vue2.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CnConversationThread.vue2.js","sources":["../../../../src/components/CnConversationThread/CnConversationThread.vue"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n\n<template>\n\t<section\n\t\tclass=\"cn-conversation-thread\"\n\t\tdata-testid=\"cn-conversation-thread\"\n\t\t:aria-labelledby=\"title ? titleId : null\"\n\t\t:aria-label=\"title ? null : fallbackLabel\">\n\t\t<component\n\t\t\t:is=\"titleTag\"\n\t\t\tv-if=\"title\"\n\t\t\t:id=\"titleId\"\n\t\t\tclass=\"cn-conversation-thread__title\">\n\t\t\t{{ title }}\n\t\t</component>\n\n\t\t<p v-if=\"rows.length === 0\" class=\"cn-conversation-thread__empty\" data-testid=\"cn-conversation-empty\">\n\t\t\t{{ emptyText }}\n\t\t</p>\n\n\t\t<ol v-else class=\"cn-conversation-thread__messages\">\n\t\t\t<li\n\t\t\t\tv-for=\"row in rows\"\n\t\t\t\t:key=\"row.key\"\n\t\t\t\tclass=\"cn-conversation-thread__message\"\n\t\t\t\t:class=\"`cn-conversation-thread__message--${row.side}`\"\n\t\t\t\tdata-testid=\"cn-conversation-message\"\n\t\t\t\t:data-side=\"row.side\">\n\t\t\t\t<span class=\"cn-conversation-thread__avatar\" aria-hidden=\"true\">{{ row.initials }}</span>\n\t\t\t\t<div class=\"cn-conversation-thread__bubble\">\n\t\t\t\t\t<span class=\"cn-conversation-thread__meta\">\n\t\t\t\t\t\t<span class=\"cn-conversation-thread__author\">{{ row.author }}</span>\n\t\t\t\t\t\t<time v-if=\"row.timeLabel\" :datetime=\"row.datetime || null\">{{ row.timeLabel }}</time>\n\t\t\t\t\t</span>\n\t\t\t\t\t<span class=\"cn-conversation-thread__text\">{{ row.text }}</span>\n\t\t\t\t</div>\n\t\t\t</li>\n\t\t</ol>\n\n\t\t<form\n\t\t\tv-if=\"allowReply\"\n\t\t\tclass=\"cn-conversation-thread__reply\"\n\t\t\tdata-testid=\"cn-conversation-reply\"\n\t\t\t@submit.prevent=\"onSend\">\n\t\t\t<label :for=\"inputId\" class=\"cn-conversation-thread__reply-label\">{{ replyLabel }}</label>\n\t\t\t<textarea\n\t\t\t\t:id=\"inputId\"\n\t\t\t\tv-model=\"draft\"\n\t\t\t\tclass=\"cn-conversation-thread__input\"\n\t\t\t\trows=\"2\"\n\t\t\t\t:placeholder=\"placeholder || null\"\n\t\t\t\t:disabled=\"sending\"\n\t\t\t\t:aria-describedby=\"errorMessage ? errorId : null\"\n\t\t\t\tdata-testid=\"cn-conversation-input\"\n\t\t\t\t@keydown.ctrl.enter.prevent=\"onSend\"\n\t\t\t\t@keydown.meta.enter.prevent=\"onSend\" />\n\t\t\t<p\n\t\t\t\tv-if=\"errorMessage\"\n\t\t\t\t:id=\"errorId\"\n\t\t\t\tclass=\"cn-conversation-thread__error\"\n\t\t\t\trole=\"alert\"\n\t\t\t\tdata-testid=\"cn-conversation-error\">\n\t\t\t\t{{ errorMessage }}\n\t\t\t</p>\n\t\t\t<div class=\"cn-conversation-thread__reply-actions\">\n\t\t\t\t<!-- @slot reply-actions Extra buttons before Send, for example \"Save draft\". -->\n\t\t\t\t<!-- @binding {string} draft The text in the reply box. -->\n\t\t\t\t<slot name=\"reply-actions\" :draft=\"draft\" />\n\t\t\t\t<NcButton\n\t\t\t\t\tvariant=\"primary\"\n\t\t\t\t\ttype=\"submit\"\n\t\t\t\t\t:disabled=\"!canSend\"\n\t\t\t\t\tdata-testid=\"cn-conversation-send\">\n\t\t\t\t\t{{ sendLabel }}\n\t\t\t\t</NcButton>\n\t\t\t</div>\n\t\t</form>\n\t</section>\n</template>\n\n<script>\nimport axios from '@nextcloud/axios'\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcButton } from '@nextcloud/vue'\nimport { prefixUrl } from '../../utils/headers.js'\n\nlet conversationThreadUid = 0\n\n/**\n * CnConversationThread: the messages between you and the other party, and a box to answer.\n *\n * Each message has an author, a time and a side: `us` for the organisation,\n * `them` for the resident or customer. The reply box emits `send`, and also\n * posts to `endpoint` when one is set. It needs no Talk and no other app:\n * where the messages are stored is the host's business.\n *\n * ```vue\n * <CnConversationThread\n * title=\"Contact with the resident\"\n * reply-label=\"Answer to the resident\"\n * :messages=\"[\n * { id: 1, author: 'Sanne de Vries', time: '2026-10-04T10:52:00', side: 'them',\n * text: 'Thank you. I would like to see all documents.' },\n * { id: 2, time: '2026-10-04T10:24:00', side: 'us',\n * text: 'Construction starts in March.' },\n * ]\"\n * @send=\"saveReply\" />\n * ```\n *\n * The `conversation` detail widget type renders this thread from a field on\n * the record.\n */\nexport default {\n\tname: 'CnConversationThread',\n\n\tcomponents: {\n\t\tNcButton,\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The messages, oldest first: `{ id?, author?, time?, text, side? }`.\n\t\t * `side` is `us` or `them` (default `them`). `time` is an ISO\n\t\t * date, or any text to show as is.\n\t\t */\n\t\tmessages: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/** Heading of the thread. */\n\t\ttitle: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** Element the heading renders as. */\n\t\ttitleTag: {\n\t\t\ttype: String,\n\t\t\tdefault: 'h3',\n\t\t},\n\n\t\t/** Whether the reply box renders. Turn it off for a closed or read-only record. */\n\t\tallowReply: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/** Label of the reply box. */\n\t\treplyLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'Reply'),\n\t\t},\n\n\t\t/** Placeholder inside the reply box. */\n\t\tplaceholder: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** Label of the send button. */\n\t\tsendLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'Send'),\n\t\t},\n\n\t\t/** Author shown on an `us` message that names none. */\n\t\tusLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'You'),\n\t\t},\n\n\t\t/**\n\t\t * URL the reply is POSTed to as `{ message }`. Empty means the host\n\t\t * handles `send` itself and nothing is requested.\n\t\t */\n\t\tendpoint: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** Extra fields sent along with `message` when `endpoint` is set. */\n\t\tpayload: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/** Shown when there are no messages yet. */\n\t\temptyText: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'No messages yet.'),\n\t\t},\n\t},\n\n\temits: [\n\t\t/** A reply was submitted. Payload: the text. */\n\t\t'send',\n\t\t/** The endpoint accepted the reply. Payload: the response body. */\n\t\t'sent',\n\t\t/** The endpoint refused the reply. Payload: the error. */\n\t\t'error',\n\t],\n\n\tdata() {\n\t\tconversationThreadUid += 1\n\t\treturn {\n\t\t\tdraft: '',\n\t\t\tsending: false,\n\t\t\terrorMessage: '',\n\t\t\ttitleId: `cn-conversation-title-${conversationThreadUid}`,\n\t\t\tinputId: `cn-conversation-input-${conversationThreadUid}`,\n\t\t\terrorId: `cn-conversation-error-${conversationThreadUid}`,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * The messages, with author, side, time and initials resolved.\n\t\t *\n\t\t * @return {Array<{key: string, author: string, initials: string, side: string, text: string, timeLabel: string, datetime: string}>} The rows.\n\t\t * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-conversation-thread\n\t\t */\n\t\trows() {\n\t\t\treturn (Array.isArray(this.messages) ? this.messages : [])\n\t\t\t\t.filter((message) => message && typeof message === 'object')\n\t\t\t\t.map((message, index) => {\n\t\t\t\t\tconst side = message.side === 'us' ? 'us' : 'them'\n\t\t\t\t\tconst author = typeof message.author === 'string' && message.author !== ''\n\t\t\t\t\t\t? message.author\n\t\t\t\t\t\t: (side === 'us' ? this.usLabel : '')\n\t\t\t\t\tconst time = this.formatTime(message.time)\n\t\t\t\t\treturn {\n\t\t\t\t\t\tkey: message.id === undefined || message.id === null ? `message-${index}` : String(message.id),\n\t\t\t\t\t\tauthor,\n\t\t\t\t\t\tinitials: this.initialsOf(author),\n\t\t\t\t\t\tside,\n\t\t\t\t\t\ttext: message.text === undefined || message.text === null ? '' : String(message.text),\n\t\t\t\t\t\ttimeLabel: time.label,\n\t\t\t\t\t\tdatetime: time.datetime,\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * Whether there is something to send and nothing in flight.\n\t\t *\n\t\t * @return {boolean} True when Send is enabled.\n\t\t * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-conversation-thread\n\t\t */\n\t\tcanSend() {\n\t\t\treturn !this.sending && this.draft.trim() !== ''\n\t\t},\n\n\t\t/**\n\t\t * Accessible name for a thread without a title.\n\t\t *\n\t\t * @return {string} The label.\n\t\t * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-conversation-thread\n\t\t */\n\t\tfallbackLabel() {\n\t\t\treturn t('nextcloud-vue', 'Conversation')\n\t\t},\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Up to two initials of a name, for the avatar.\n\t\t *\n\t\t * @param {string} name The author.\n\t\t * @return {string} The initials, upper case.\n\t\t * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-conversation-thread\n\t\t */\n\t\tinitialsOf(name) {\n\t\t\tconst parts = String(name || '').trim().split(/\\s+/).filter(Boolean)\n\t\t\tif (parts.length === 0) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tconst first = parts[0].charAt(0)\n\t\t\tconst last = parts.length > 1 ? parts[parts.length - 1].charAt(0) : ''\n\t\t\treturn (first + last).toUpperCase()\n\t\t},\n\n\t\t/**\n\t\t * Turn a message time into what the reader sees and what `<time>` carries.\n\t\t *\n\t\t * @param {unknown} value An ISO date, a Date, or text to show as is.\n\t\t * @return {{label: string, datetime: string}} The visible label and the machine value.\n\t\t * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-conversation-thread\n\t\t */\n\t\tformatTime(value) {\n\t\t\tif (value === null || value === undefined || value === '') {\n\t\t\t\treturn { label: '', datetime: '' }\n\t\t\t}\n\t\t\tconst date = value instanceof Date ? value : new Date(value)\n\t\t\tconst looksLikeDate = value instanceof Date || typeof value === 'number' || /^\\d{4}-\\d{2}-\\d{2}/.test(String(value))\n\t\t\tif (!looksLikeDate || Number.isNaN(date.getTime())) {\n\t\t\t\treturn { label: String(value), datetime: '' }\n\t\t\t}\n\t\t\tconst label = date.toLocaleString(undefined, { day: 'numeric', month: 'short', hour: '2-digit', minute: '2-digit' })\n\t\t\treturn { label, datetime: date.toISOString() }\n\t\t},\n\n\t\t/**\n\t\t * Submit the reply: tell the host, and post it when an endpoint is set.\n\t\t *\n\t\t * Without an endpoint the box clears straight away, because the host\n\t\t * owns the send. With one it clears only when the request succeeds, so\n\t\t * a refused reply is still there to try again.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-conversation-thread\n\t\t */\n\t\tasync onSend() {\n\t\t\tif (!this.canSend) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst text = this.draft.trim()\n\t\t\tthis.errorMessage = ''\n\t\t\tthis.$emit('send', text)\n\t\t\tif (!this.endpoint) {\n\t\t\t\tthis.draft = ''\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.sending = true\n\t\t\ttry {\n\t\t\t\tconst response = await axios.post(prefixUrl(this.endpoint), { ...this.payload, message: text })\n\t\t\t\tthis.draft = ''\n\t\t\t\tthis.$emit('sent', response?.data)\n\t\t\t} catch (error) {\n\t\t\t\tthis.errorMessage = t('nextcloud-vue', 'The message could not be sent. Try again.')\n\t\t\t\tthis.$emit('error', error)\n\t\t\t} finally {\n\t\t\t\tthis.sending = false\n\t\t\t}\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-conversation-thread {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: calc(3.5 * var(--default-grid-baseline));\n}\n\n.cn-conversation-thread__title {\n\tfont-size: 1.1em;\n\tfont-weight: 700;\n\tmargin: 0;\n}\n\n.cn-conversation-thread__empty {\n\tcolor: var(--color-text-maxcontrast);\n\tmargin: 0;\n}\n\n.cn-conversation-thread__messages {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: calc(3.5 * var(--default-grid-baseline));\n\tlist-style: none;\n\tmargin: 0;\n\tpadding: 0;\n}\n\n.cn-conversation-thread__message {\n\tdisplay: flex;\n\tgap: calc(3 * var(--default-grid-baseline));\n}\n\n.cn-conversation-thread__message--us {\n\tflex-direction: row-reverse;\n}\n\n.cn-conversation-thread__avatar {\n\talign-items: center;\n\tbackground: var(--color-background-dark);\n\tborder-radius: 50%;\n\tcolor: var(--color-main-text);\n\tdisplay: flex;\n\tflex: none;\n\tfont-size: 0.8em;\n\tfont-weight: 700;\n\theight: 34px;\n\tjustify-content: center;\n\twidth: 34px;\n}\n\n.cn-conversation-thread__bubble {\n\tbackground: var(--color-background-hover);\n\tborder-radius: var(--border-radius-large);\n\tborder-start-start-radius: var(--border-radius-small, 4px);\n\tcolor: var(--color-main-text);\n\tdisplay: flex;\n\tflex: 1;\n\tflex-direction: column;\n\tgap: var(--default-grid-baseline);\n\tmin-width: 0;\n\tpadding: calc(3 * var(--default-grid-baseline)) calc(3.5 * var(--default-grid-baseline));\n}\n\n.cn-conversation-thread__message--us .cn-conversation-thread__bubble {\n\tbackground: var(--color-primary-element-light);\n\tborder-start-end-radius: var(--border-radius-small, 4px);\n\tborder-start-start-radius: var(--border-radius-large);\n\tcolor: var(--color-primary-element-light-text);\n}\n\n.cn-conversation-thread__meta {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\tfont-size: 0.9em;\n\tgap: calc(2 * var(--default-grid-baseline));\n}\n\n.cn-conversation-thread__author {\n\tfont-weight: 600;\n}\n\n.cn-conversation-thread__text {\n\toverflow-wrap: anywhere;\n\twhite-space: pre-wrap;\n}\n\n.cn-conversation-thread__reply {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: calc(2 * var(--default-grid-baseline));\n}\n\n.cn-conversation-thread__reply-label {\n\tfont-weight: 600;\n}\n\n.cn-conversation-thread__input {\n\tbox-sizing: border-box;\n\tmargin: 0;\n\tresize: vertical;\n\twidth: 100%;\n}\n\n.cn-conversation-thread__error {\n\tcolor: var(--color-error-text, var(--color-error));\n\tmargin: 0;\n}\n\n.cn-conversation-thread__reply-actions {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\tgap: calc(2.5 * var(--default-grid-baseline));\n\tjustify-content: flex-end;\n}\n</style>\n"],"names":["t","axios"],"mappings":"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"}
@@ -0,0 +1,131 @@
1
+ import { resolveComponent, openBlock, createElementBlock, createBlock, resolveDynamicComponent, withCtx, createTextVNode, toDisplayString, createCommentVNode, Fragment, renderList, normalizeClass, createElementVNode, withModifiers, withDirectives, withKeys, vModelText, renderSlot, createVNode } from 'vue';
2
+
3
+ const _hoisted_1 = ["aria-labelledby", "aria-label"];
4
+ const _hoisted_2 = {
5
+ key: 1,
6
+ class: "cn-conversation-thread__empty",
7
+ "data-testid": "cn-conversation-empty"
8
+ };
9
+ const _hoisted_3 = {
10
+ key: 2,
11
+ class: "cn-conversation-thread__messages"
12
+ };
13
+ const _hoisted_4 = ["data-side"];
14
+ const _hoisted_5 = {
15
+ class: "cn-conversation-thread__avatar",
16
+ "aria-hidden": "true"
17
+ };
18
+ const _hoisted_6 = { class: "cn-conversation-thread__bubble" };
19
+ const _hoisted_7 = { class: "cn-conversation-thread__meta" };
20
+ const _hoisted_8 = { class: "cn-conversation-thread__author" };
21
+ const _hoisted_9 = ["datetime"];
22
+ const _hoisted_10 = { class: "cn-conversation-thread__text" };
23
+ const _hoisted_11 = ["for"];
24
+ const _hoisted_12 = ["id", "placeholder", "disabled", "aria-describedby"];
25
+ const _hoisted_13 = ["id"];
26
+ const _hoisted_14 = { class: "cn-conversation-thread__reply-actions" };
27
+
28
+ function render(_ctx, _cache, $props, $setup, $data, $options) {
29
+ const _component_NcButton = resolveComponent("NcButton");
30
+
31
+ return (openBlock(), createElementBlock("section", {
32
+ class: "cn-conversation-thread",
33
+ "data-testid": "cn-conversation-thread",
34
+ "aria-labelledby": $props.title ? $data.titleId : null,
35
+ "aria-label": $props.title ? null : $options.fallbackLabel
36
+ }, [
37
+ ($props.title)
38
+ ? (openBlock(), createBlock(resolveDynamicComponent($props.titleTag), {
39
+ key: 0,
40
+ id: $data.titleId,
41
+ class: "cn-conversation-thread__title"
42
+ }, {
43
+ default: withCtx(() => [
44
+ createTextVNode(toDisplayString($props.title), 1 /* TEXT */)
45
+ ]),
46
+ _: 1 /* STABLE */
47
+ }, 8 /* PROPS */, ["id"]))
48
+ : createCommentVNode("v-if", true),
49
+ ($options.rows.length === 0)
50
+ ? (openBlock(), createElementBlock("p", _hoisted_2, toDisplayString($props.emptyText), 1 /* TEXT */))
51
+ : (openBlock(), createElementBlock("ol", _hoisted_3, [
52
+ (openBlock(true), createElementBlock(Fragment, null, renderList($options.rows, (row) => {
53
+ return (openBlock(), createElementBlock("li", {
54
+ key: row.key,
55
+ class: normalizeClass(["cn-conversation-thread__message", `cn-conversation-thread__message--${row.side}`]),
56
+ "data-testid": "cn-conversation-message",
57
+ "data-side": row.side
58
+ }, [
59
+ createElementVNode("span", _hoisted_5, toDisplayString(row.initials), 1 /* TEXT */),
60
+ createElementVNode("div", _hoisted_6, [
61
+ createElementVNode("span", _hoisted_7, [
62
+ createElementVNode("span", _hoisted_8, toDisplayString(row.author), 1 /* TEXT */),
63
+ (row.timeLabel)
64
+ ? (openBlock(), createElementBlock("time", {
65
+ key: 0,
66
+ datetime: row.datetime || null
67
+ }, toDisplayString(row.timeLabel), 9 /* TEXT, PROPS */, _hoisted_9))
68
+ : createCommentVNode("v-if", true)
69
+ ]),
70
+ createElementVNode("span", _hoisted_10, toDisplayString(row.text), 1 /* TEXT */)
71
+ ])
72
+ ], 10 /* CLASS, PROPS */, _hoisted_4))
73
+ }), 128 /* KEYED_FRAGMENT */))
74
+ ])),
75
+ ($props.allowReply)
76
+ ? (openBlock(), createElementBlock("form", {
77
+ key: 3,
78
+ class: "cn-conversation-thread__reply",
79
+ "data-testid": "cn-conversation-reply",
80
+ onSubmit: _cache[3] || (_cache[3] = withModifiers((...args) => ($options.onSend && $options.onSend(...args)), ["prevent"]))
81
+ }, [
82
+ createElementVNode("label", {
83
+ for: $data.inputId,
84
+ class: "cn-conversation-thread__reply-label"
85
+ }, toDisplayString($props.replyLabel), 9 /* TEXT, PROPS */, _hoisted_11),
86
+ withDirectives(createElementVNode("textarea", {
87
+ id: $data.inputId,
88
+ "onUpdate:modelValue": _cache[0] || (_cache[0] = $event => (($data.draft) = $event)),
89
+ class: "cn-conversation-thread__input",
90
+ rows: "2",
91
+ placeholder: $props.placeholder || null,
92
+ disabled: $data.sending,
93
+ "aria-describedby": $data.errorMessage ? $data.errorId : null,
94
+ "data-testid": "cn-conversation-input",
95
+ onKeydown: [
96
+ _cache[1] || (_cache[1] = withKeys(withModifiers((...args) => ($options.onSend && $options.onSend(...args)), ["ctrl","prevent"]), ["enter"])),
97
+ _cache[2] || (_cache[2] = withKeys(withModifiers((...args) => ($options.onSend && $options.onSend(...args)), ["meta","prevent"]), ["enter"]))
98
+ ]
99
+ }, null, 40 /* PROPS, NEED_HYDRATION */, _hoisted_12), [
100
+ [vModelText, $data.draft]
101
+ ]),
102
+ ($data.errorMessage)
103
+ ? (openBlock(), createElementBlock("p", {
104
+ key: 0,
105
+ id: $data.errorId,
106
+ class: "cn-conversation-thread__error",
107
+ role: "alert",
108
+ "data-testid": "cn-conversation-error"
109
+ }, toDisplayString($data.errorMessage), 9 /* TEXT, PROPS */, _hoisted_13))
110
+ : createCommentVNode("v-if", true),
111
+ createElementVNode("div", _hoisted_14, [
112
+ renderSlot(_ctx.$slots, "reply-actions", { draft: $data.draft }),
113
+ createVNode(_component_NcButton, {
114
+ variant: "primary",
115
+ type: "submit",
116
+ disabled: !$options.canSend,
117
+ "data-testid": "cn-conversation-send"
118
+ }, {
119
+ default: withCtx(() => [
120
+ createTextVNode(toDisplayString($props.sendLabel), 1 /* TEXT */)
121
+ ]),
122
+ _: 1 /* STABLE */
123
+ }, 8 /* PROPS */, ["disabled"])
124
+ ])
125
+ ], 32 /* NEED_HYDRATION */))
126
+ : createCommentVNode("v-if", true)
127
+ ], 8 /* PROPS */, _hoisted_1))
128
+ }
129
+
130
+ export { render };
131
+ //# sourceMappingURL=CnConversationThread.vue3.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CnConversationThread.vue3.js","sources":["../../../../src/components/CnConversationThread/CnConversationThread.vue?vue&type=template&id=22b6724f&lang.js"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n\n<template>\n\t<section\n\t\tclass=\"cn-conversation-thread\"\n\t\tdata-testid=\"cn-conversation-thread\"\n\t\t:aria-labelledby=\"title ? titleId : null\"\n\t\t:aria-label=\"title ? null : fallbackLabel\">\n\t\t<component\n\t\t\t:is=\"titleTag\"\n\t\t\tv-if=\"title\"\n\t\t\t:id=\"titleId\"\n\t\t\tclass=\"cn-conversation-thread__title\">\n\t\t\t{{ title }}\n\t\t</component>\n\n\t\t<p v-if=\"rows.length === 0\" class=\"cn-conversation-thread__empty\" data-testid=\"cn-conversation-empty\">\n\t\t\t{{ emptyText }}\n\t\t</p>\n\n\t\t<ol v-else class=\"cn-conversation-thread__messages\">\n\t\t\t<li\n\t\t\t\tv-for=\"row in rows\"\n\t\t\t\t:key=\"row.key\"\n\t\t\t\tclass=\"cn-conversation-thread__message\"\n\t\t\t\t:class=\"`cn-conversation-thread__message--${row.side}`\"\n\t\t\t\tdata-testid=\"cn-conversation-message\"\n\t\t\t\t:data-side=\"row.side\">\n\t\t\t\t<span class=\"cn-conversation-thread__avatar\" aria-hidden=\"true\">{{ row.initials }}</span>\n\t\t\t\t<div class=\"cn-conversation-thread__bubble\">\n\t\t\t\t\t<span class=\"cn-conversation-thread__meta\">\n\t\t\t\t\t\t<span class=\"cn-conversation-thread__author\">{{ row.author }}</span>\n\t\t\t\t\t\t<time v-if=\"row.timeLabel\" :datetime=\"row.datetime || null\">{{ row.timeLabel }}</time>\n\t\t\t\t\t</span>\n\t\t\t\t\t<span class=\"cn-conversation-thread__text\">{{ row.text }}</span>\n\t\t\t\t</div>\n\t\t\t</li>\n\t\t</ol>\n\n\t\t<form\n\t\t\tv-if=\"allowReply\"\n\t\t\tclass=\"cn-conversation-thread__reply\"\n\t\t\tdata-testid=\"cn-conversation-reply\"\n\t\t\t@submit.prevent=\"onSend\">\n\t\t\t<label :for=\"inputId\" class=\"cn-conversation-thread__reply-label\">{{ replyLabel }}</label>\n\t\t\t<textarea\n\t\t\t\t:id=\"inputId\"\n\t\t\t\tv-model=\"draft\"\n\t\t\t\tclass=\"cn-conversation-thread__input\"\n\t\t\t\trows=\"2\"\n\t\t\t\t:placeholder=\"placeholder || null\"\n\t\t\t\t:disabled=\"sending\"\n\t\t\t\t:aria-describedby=\"errorMessage ? errorId : null\"\n\t\t\t\tdata-testid=\"cn-conversation-input\"\n\t\t\t\t@keydown.ctrl.enter.prevent=\"onSend\"\n\t\t\t\t@keydown.meta.enter.prevent=\"onSend\" />\n\t\t\t<p\n\t\t\t\tv-if=\"errorMessage\"\n\t\t\t\t:id=\"errorId\"\n\t\t\t\tclass=\"cn-conversation-thread__error\"\n\t\t\t\trole=\"alert\"\n\t\t\t\tdata-testid=\"cn-conversation-error\">\n\t\t\t\t{{ errorMessage }}\n\t\t\t</p>\n\t\t\t<div class=\"cn-conversation-thread__reply-actions\">\n\t\t\t\t<!-- @slot reply-actions Extra buttons before Send, for example \"Save draft\". -->\n\t\t\t\t<!-- @binding {string} draft The text in the reply box. -->\n\t\t\t\t<slot name=\"reply-actions\" :draft=\"draft\" />\n\t\t\t\t<NcButton\n\t\t\t\t\tvariant=\"primary\"\n\t\t\t\t\ttype=\"submit\"\n\t\t\t\t\t:disabled=\"!canSend\"\n\t\t\t\t\tdata-testid=\"cn-conversation-send\">\n\t\t\t\t\t{{ sendLabel }}\n\t\t\t\t</NcButton>\n\t\t\t</div>\n\t\t</form>\n\t</section>\n</template>\n\n<script>\nimport axios from '@nextcloud/axios'\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcButton } from '@nextcloud/vue'\nimport { prefixUrl } from '../../utils/headers.js'\n\nlet conversationThreadUid = 0\n\n/**\n * CnConversationThread: the messages between you and the other party, and a box to answer.\n *\n * Each message has an author, a time and a side: `us` for the organisation,\n * `them` for the resident or customer. The reply box emits `send`, and also\n * posts to `endpoint` when one is set. It needs no Talk and no other app:\n * where the messages are stored is the host's business.\n *\n * ```vue\n * <CnConversationThread\n * title=\"Contact with the resident\"\n * reply-label=\"Answer to the resident\"\n * :messages=\"[\n * { id: 1, author: 'Sanne de Vries', time: '2026-10-04T10:52:00', side: 'them',\n * text: 'Thank you. I would like to see all documents.' },\n * { id: 2, time: '2026-10-04T10:24:00', side: 'us',\n * text: 'Construction starts in March.' },\n * ]\"\n * @send=\"saveReply\" />\n * ```\n *\n * The `conversation` detail widget type renders this thread from a field on\n * the record.\n */\nexport default {\n\tname: 'CnConversationThread',\n\n\tcomponents: {\n\t\tNcButton,\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The messages, oldest first: `{ id?, author?, time?, text, side? }`.\n\t\t * `side` is `us` or `them` (default `them`). `time` is an ISO\n\t\t * date, or any text to show as is.\n\t\t */\n\t\tmessages: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/** Heading of the thread. */\n\t\ttitle: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** Element the heading renders as. */\n\t\ttitleTag: {\n\t\t\ttype: String,\n\t\t\tdefault: 'h3',\n\t\t},\n\n\t\t/** Whether the reply box renders. Turn it off for a closed or read-only record. */\n\t\tallowReply: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/** Label of the reply box. */\n\t\treplyLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'Reply'),\n\t\t},\n\n\t\t/** Placeholder inside the reply box. */\n\t\tplaceholder: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** Label of the send button. */\n\t\tsendLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'Send'),\n\t\t},\n\n\t\t/** Author shown on an `us` message that names none. */\n\t\tusLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'You'),\n\t\t},\n\n\t\t/**\n\t\t * URL the reply is POSTed to as `{ message }`. Empty means the host\n\t\t * handles `send` itself and nothing is requested.\n\t\t */\n\t\tendpoint: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** Extra fields sent along with `message` when `endpoint` is set. */\n\t\tpayload: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/** Shown when there are no messages yet. */\n\t\temptyText: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'No messages yet.'),\n\t\t},\n\t},\n\n\temits: [\n\t\t/** A reply was submitted. Payload: the text. */\n\t\t'send',\n\t\t/** The endpoint accepted the reply. Payload: the response body. */\n\t\t'sent',\n\t\t/** The endpoint refused the reply. Payload: the error. */\n\t\t'error',\n\t],\n\n\tdata() {\n\t\tconversationThreadUid += 1\n\t\treturn {\n\t\t\tdraft: '',\n\t\t\tsending: false,\n\t\t\terrorMessage: '',\n\t\t\ttitleId: `cn-conversation-title-${conversationThreadUid}`,\n\t\t\tinputId: `cn-conversation-input-${conversationThreadUid}`,\n\t\t\terrorId: `cn-conversation-error-${conversationThreadUid}`,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * The messages, with author, side, time and initials resolved.\n\t\t *\n\t\t * @return {Array<{key: string, author: string, initials: string, side: string, text: string, timeLabel: string, datetime: string}>} The rows.\n\t\t * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-conversation-thread\n\t\t */\n\t\trows() {\n\t\t\treturn (Array.isArray(this.messages) ? this.messages : [])\n\t\t\t\t.filter((message) => message && typeof message === 'object')\n\t\t\t\t.map((message, index) => {\n\t\t\t\t\tconst side = message.side === 'us' ? 'us' : 'them'\n\t\t\t\t\tconst author = typeof message.author === 'string' && message.author !== ''\n\t\t\t\t\t\t? message.author\n\t\t\t\t\t\t: (side === 'us' ? this.usLabel : '')\n\t\t\t\t\tconst time = this.formatTime(message.time)\n\t\t\t\t\treturn {\n\t\t\t\t\t\tkey: message.id === undefined || message.id === null ? `message-${index}` : String(message.id),\n\t\t\t\t\t\tauthor,\n\t\t\t\t\t\tinitials: this.initialsOf(author),\n\t\t\t\t\t\tside,\n\t\t\t\t\t\ttext: message.text === undefined || message.text === null ? '' : String(message.text),\n\t\t\t\t\t\ttimeLabel: time.label,\n\t\t\t\t\t\tdatetime: time.datetime,\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * Whether there is something to send and nothing in flight.\n\t\t *\n\t\t * @return {boolean} True when Send is enabled.\n\t\t * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-conversation-thread\n\t\t */\n\t\tcanSend() {\n\t\t\treturn !this.sending && this.draft.trim() !== ''\n\t\t},\n\n\t\t/**\n\t\t * Accessible name for a thread without a title.\n\t\t *\n\t\t * @return {string} The label.\n\t\t * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-conversation-thread\n\t\t */\n\t\tfallbackLabel() {\n\t\t\treturn t('nextcloud-vue', 'Conversation')\n\t\t},\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Up to two initials of a name, for the avatar.\n\t\t *\n\t\t * @param {string} name The author.\n\t\t * @return {string} The initials, upper case.\n\t\t * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-conversation-thread\n\t\t */\n\t\tinitialsOf(name) {\n\t\t\tconst parts = String(name || '').trim().split(/\\s+/).filter(Boolean)\n\t\t\tif (parts.length === 0) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tconst first = parts[0].charAt(0)\n\t\t\tconst last = parts.length > 1 ? parts[parts.length - 1].charAt(0) : ''\n\t\t\treturn (first + last).toUpperCase()\n\t\t},\n\n\t\t/**\n\t\t * Turn a message time into what the reader sees and what `<time>` carries.\n\t\t *\n\t\t * @param {unknown} value An ISO date, a Date, or text to show as is.\n\t\t * @return {{label: string, datetime: string}} The visible label and the machine value.\n\t\t * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-conversation-thread\n\t\t */\n\t\tformatTime(value) {\n\t\t\tif (value === null || value === undefined || value === '') {\n\t\t\t\treturn { label: '', datetime: '' }\n\t\t\t}\n\t\t\tconst date = value instanceof Date ? value : new Date(value)\n\t\t\tconst looksLikeDate = value instanceof Date || typeof value === 'number' || /^\\d{4}-\\d{2}-\\d{2}/.test(String(value))\n\t\t\tif (!looksLikeDate || Number.isNaN(date.getTime())) {\n\t\t\t\treturn { label: String(value), datetime: '' }\n\t\t\t}\n\t\t\tconst label = date.toLocaleString(undefined, { day: 'numeric', month: 'short', hour: '2-digit', minute: '2-digit' })\n\t\t\treturn { label, datetime: date.toISOString() }\n\t\t},\n\n\t\t/**\n\t\t * Submit the reply: tell the host, and post it when an endpoint is set.\n\t\t *\n\t\t * Without an endpoint the box clears straight away, because the host\n\t\t * owns the send. With one it clears only when the request succeeds, so\n\t\t * a refused reply is still there to try again.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-conversation-thread\n\t\t */\n\t\tasync onSend() {\n\t\t\tif (!this.canSend) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst text = this.draft.trim()\n\t\t\tthis.errorMessage = ''\n\t\t\tthis.$emit('send', text)\n\t\t\tif (!this.endpoint) {\n\t\t\t\tthis.draft = ''\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.sending = true\n\t\t\ttry {\n\t\t\t\tconst response = await axios.post(prefixUrl(this.endpoint), { ...this.payload, message: text })\n\t\t\t\tthis.draft = ''\n\t\t\t\tthis.$emit('sent', response?.data)\n\t\t\t} catch (error) {\n\t\t\t\tthis.errorMessage = t('nextcloud-vue', 'The message could not be sent. Try again.')\n\t\t\t\tthis.$emit('error', error)\n\t\t\t} finally {\n\t\t\t\tthis.sending = false\n\t\t\t}\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-conversation-thread {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: calc(3.5 * var(--default-grid-baseline));\n}\n\n.cn-conversation-thread__title {\n\tfont-size: 1.1em;\n\tfont-weight: 700;\n\tmargin: 0;\n}\n\n.cn-conversation-thread__empty {\n\tcolor: var(--color-text-maxcontrast);\n\tmargin: 0;\n}\n\n.cn-conversation-thread__messages {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: calc(3.5 * var(--default-grid-baseline));\n\tlist-style: none;\n\tmargin: 0;\n\tpadding: 0;\n}\n\n.cn-conversation-thread__message {\n\tdisplay: flex;\n\tgap: calc(3 * var(--default-grid-baseline));\n}\n\n.cn-conversation-thread__message--us {\n\tflex-direction: row-reverse;\n}\n\n.cn-conversation-thread__avatar {\n\talign-items: center;\n\tbackground: var(--color-background-dark);\n\tborder-radius: 50%;\n\tcolor: var(--color-main-text);\n\tdisplay: flex;\n\tflex: none;\n\tfont-size: 0.8em;\n\tfont-weight: 700;\n\theight: 34px;\n\tjustify-content: center;\n\twidth: 34px;\n}\n\n.cn-conversation-thread__bubble {\n\tbackground: var(--color-background-hover);\n\tborder-radius: var(--border-radius-large);\n\tborder-start-start-radius: var(--border-radius-small, 4px);\n\tcolor: var(--color-main-text);\n\tdisplay: flex;\n\tflex: 1;\n\tflex-direction: column;\n\tgap: var(--default-grid-baseline);\n\tmin-width: 0;\n\tpadding: calc(3 * var(--default-grid-baseline)) calc(3.5 * var(--default-grid-baseline));\n}\n\n.cn-conversation-thread__message--us .cn-conversation-thread__bubble {\n\tbackground: var(--color-primary-element-light);\n\tborder-start-end-radius: var(--border-radius-small, 4px);\n\tborder-start-start-radius: var(--border-radius-large);\n\tcolor: var(--color-primary-element-light-text);\n}\n\n.cn-conversation-thread__meta {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\tfont-size: 0.9em;\n\tgap: calc(2 * var(--default-grid-baseline));\n}\n\n.cn-conversation-thread__author {\n\tfont-weight: 600;\n}\n\n.cn-conversation-thread__text {\n\toverflow-wrap: anywhere;\n\twhite-space: pre-wrap;\n}\n\n.cn-conversation-thread__reply {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: calc(2 * var(--default-grid-baseline));\n}\n\n.cn-conversation-thread__reply-label {\n\tfont-weight: 600;\n}\n\n.cn-conversation-thread__input {\n\tbox-sizing: border-box;\n\tmargin: 0;\n\tresize: vertical;\n\twidth: 100%;\n}\n\n.cn-conversation-thread__error {\n\tcolor: var(--color-error-text, var(--color-error));\n\tmargin: 0;\n}\n\n.cn-conversation-thread__reply-actions {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\tgap: calc(2.5 * var(--default-grid-baseline));\n\tjustify-content: flex-end;\n}\n</style>\n"],"names":["_createElementBlock","_openBlock","_createBlock","_toDisplayString","_Fragment","_renderList","_normalizeClass","_createElementVNode","_renderSlot"],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ import script from './CnConversationWidget.vue2.js';
2
+ import { render } from './CnConversationWidget.vue3.js';
3
+
4
+ script.render = render;
5
+ script.__scopeId = "data-v-57081189";
6
+ script.__file = "src/components/CnConversationThread/CnConversationWidget.vue";
7
+ const __sfc_main = script;
8
+
9
+ export { __sfc_main as default };
10
+ //# sourceMappingURL=CnConversationWidget.vue.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CnConversationWidget.vue.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;"}
@@ -0,0 +1,116 @@
1
+ import __sfc_main from './CnConversationThread.vue.js';
2
+ import { readPath } from '../../utils/readPath.js';
3
+
4
+ /**
5
+ * CnConversationWidget: the `conversation` detail widget type.
6
+ *
7
+ * Renders CnConversationThread from a list on the bound record:
8
+ * `content: { field, endpoint?, authorField?, timeField?, textField?, sideField?, usValue? }`.
9
+ * `@objectId` in `endpoint` is replaced by the record's id. A reply the
10
+ * endpoint accepts is shown at once, until the record is read again.
11
+ * Resolved by its registry key, not exported.
12
+ */
13
+ var script = {
14
+ name: 'CnConversationWidget',
15
+
16
+ components: {
17
+ CnConversationThread: __sfc_main,
18
+ },
19
+
20
+ props: {
21
+ /** The widget's content. `field` is the dot-path to the messages on the record. */
22
+ content: {
23
+ type: Object,
24
+ default: () => ({}),
25
+ },
26
+
27
+ /** The bound record. */
28
+ objectData: {
29
+ type: Object,
30
+ default: null,
31
+ },
32
+
33
+ /** The bound record's id. */
34
+ objectId: {
35
+ type: [String, Number],
36
+ default: '',
37
+ },
38
+ },
39
+
40
+ data() {
41
+ return {
42
+ /** Replies sent from this widget that the record does not hold yet. */
43
+ sentHere: [],
44
+ }
45
+ },
46
+
47
+ computed: {
48
+ /**
49
+ * The record's messages in the thread's shape, plus the replies sent here.
50
+ *
51
+ * @return {Array<object>} The messages.
52
+ * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-conversation-thread
53
+ */
54
+ messages() {
55
+ const c = this.content;
56
+ const value = readPath(this.objectData || {}, c.field || 'messages');
57
+ const usValue = String(c.usValue === undefined ? 'us' : c.usValue);
58
+ const stored = (Array.isArray(value) ? value : [])
59
+ .filter((message) => message && typeof message === 'object')
60
+ .map((message) => ({
61
+ id: message.id,
62
+ author: readPath(message, c.authorField || 'author'),
63
+ time: readPath(message, c.timeField || 'time'),
64
+ text: readPath(message, c.textField || 'text'),
65
+ side: String(readPath(message, c.sideField || 'side')) === usValue ? 'us' : 'them',
66
+ }));
67
+ return [...stored, ...this.sentHere]
68
+ },
69
+
70
+ /**
71
+ * The endpoint with the record's id filled in.
72
+ *
73
+ * @return {string} The URL, or '' when the content names none.
74
+ * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-conversation-thread
75
+ */
76
+ endpoint() {
77
+ const url = this.content.endpoint;
78
+ if (typeof url !== 'string' || url === '') {
79
+ return ''
80
+ }
81
+ return url.replace(/@objectId/g, encodeURIComponent(String(this.objectId ?? '')))
82
+ },
83
+ },
84
+
85
+ watch: {
86
+ // The record was read again, so it now holds what was sent from here.
87
+ objectData() {
88
+ this.sentHere = [];
89
+ },
90
+ },
91
+
92
+ methods: {
93
+ /**
94
+ * Show an accepted reply at once, without waiting for the record.
95
+ *
96
+ * @param {object} data The endpoint's response.
97
+ * @return {void}
98
+ * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-conversation-thread
99
+ */
100
+ onSent(data) {
101
+ const text = data?.message ?? data?.text;
102
+ if (typeof text !== 'string' || text === '') {
103
+ return
104
+ }
105
+ this.sentHere.push({
106
+ id: `sent-${this.sentHere.length}`,
107
+ time: new Date().toISOString(),
108
+ text,
109
+ side: 'us',
110
+ });
111
+ },
112
+ },
113
+ };
114
+
115
+ export { script as default };
116
+ //# sourceMappingURL=CnConversationWidget.vue2.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CnConversationWidget.vue2.js","sources":["../../../../src/components/CnConversationThread/CnConversationWidget.vue"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n\n<template>\n\t<div class=\"cn-case-widget-card\">\n\t\t<CnConversationThread\n\t\t\t:messages=\"messages\"\n\t\t\t:title=\"content.title || ''\"\n\t\t\t:allowReply=\"content.allowReply !== false\"\n\t\t\t:replyLabel=\"content.replyLabel || undefined\"\n\t\t\t:placeholder=\"content.placeholder || ''\"\n\t\t\t:sendLabel=\"content.sendLabel || undefined\"\n\t\t\t:endpoint=\"endpoint\"\n\t\t\t:emptyText=\"content.emptyText || undefined\"\n\t\t\t@sent=\"onSent\" />\n\t</div>\n</template>\n\n<script>\nimport CnConversationThread from './CnConversationThread.vue'\nimport { readPath } from '../../utils/readPath.js'\n\n/**\n * CnConversationWidget: the `conversation` detail widget type.\n *\n * Renders CnConversationThread from a list on the bound record:\n * `content: { field, endpoint?, authorField?, timeField?, textField?, sideField?, usValue? }`.\n * `@objectId` in `endpoint` is replaced by the record's id. A reply the\n * endpoint accepts is shown at once, until the record is read again.\n * Resolved by its registry key, not exported.\n */\nexport default {\n\tname: 'CnConversationWidget',\n\n\tcomponents: {\n\t\tCnConversationThread,\n\t},\n\n\tprops: {\n\t\t/** The widget's content. `field` is the dot-path to the messages on the record. */\n\t\tcontent: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/** The bound record. */\n\t\tobjectData: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** The bound record's id. */\n\t\tobjectId: {\n\t\t\ttype: [String, Number],\n\t\t\tdefault: '',\n\t\t},\n\t},\n\n\tdata() {\n\t\treturn {\n\t\t\t/** Replies sent from this widget that the record does not hold yet. */\n\t\t\tsentHere: [],\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * The record's messages in the thread's shape, plus the replies sent here.\n\t\t *\n\t\t * @return {Array<object>} The messages.\n\t\t * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-conversation-thread\n\t\t */\n\t\tmessages() {\n\t\t\tconst c = this.content\n\t\t\tconst value = readPath(this.objectData || {}, c.field || 'messages')\n\t\t\tconst usValue = String(c.usValue === undefined ? 'us' : c.usValue)\n\t\t\tconst stored = (Array.isArray(value) ? value : [])\n\t\t\t\t.filter((message) => message && typeof message === 'object')\n\t\t\t\t.map((message) => ({\n\t\t\t\t\tid: message.id,\n\t\t\t\t\tauthor: readPath(message, c.authorField || 'author'),\n\t\t\t\t\ttime: readPath(message, c.timeField || 'time'),\n\t\t\t\t\ttext: readPath(message, c.textField || 'text'),\n\t\t\t\t\tside: String(readPath(message, c.sideField || 'side')) === usValue ? 'us' : 'them',\n\t\t\t\t}))\n\t\t\treturn [...stored, ...this.sentHere]\n\t\t},\n\n\t\t/**\n\t\t * The endpoint with the record's id filled in.\n\t\t *\n\t\t * @return {string} The URL, or '' when the content names none.\n\t\t * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-conversation-thread\n\t\t */\n\t\tendpoint() {\n\t\t\tconst url = this.content.endpoint\n\t\t\tif (typeof url !== 'string' || url === '') {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn url.replace(/@objectId/g, encodeURIComponent(String(this.objectId ?? '')))\n\t\t},\n\t},\n\n\twatch: {\n\t\t// The record was read again, so it now holds what was sent from here.\n\t\tobjectData() {\n\t\t\tthis.sentHere = []\n\t\t},\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Show an accepted reply at once, without waiting for the record.\n\t\t *\n\t\t * @param {object} data The endpoint's response.\n\t\t * @return {void}\n\t\t * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-conversation-thread\n\t\t */\n\t\tonSent(data) {\n\t\t\tconst text = data?.message ?? data?.text\n\t\t\tif (typeof text !== 'string' || text === '') {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.sentHere.push({\n\t\t\t\tid: `sent-${this.sentHere.length}`,\n\t\t\t\ttime: new Date().toISOString(),\n\t\t\t\ttext,\n\t\t\t\tside: 'us',\n\t\t\t})\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-case-widget-card {\n\tbackground: var(--color-main-background);\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius-large);\n\tpadding: calc(5 * var(--default-grid-baseline));\n}\n</style>\n"],"names":["CnConversationThread"],"mappings":"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"}
@@ -0,0 +1,24 @@
1
+ import { resolveComponent, openBlock, createElementBlock, createVNode } from 'vue';
2
+
3
+ const _hoisted_1 = { class: "cn-case-widget-card" };
4
+
5
+ function render(_ctx, _cache, $props, $setup, $data, $options) {
6
+ const _component_CnConversationThread = resolveComponent("CnConversationThread");
7
+
8
+ return (openBlock(), createElementBlock("div", _hoisted_1, [
9
+ createVNode(_component_CnConversationThread, {
10
+ messages: $options.messages,
11
+ title: $props.content.title || '',
12
+ allowReply: $props.content.allowReply !== false,
13
+ replyLabel: $props.content.replyLabel || undefined,
14
+ placeholder: $props.content.placeholder || '',
15
+ sendLabel: $props.content.sendLabel || undefined,
16
+ endpoint: $options.endpoint,
17
+ emptyText: $props.content.emptyText || undefined,
18
+ onSent: $options.onSent
19
+ }, null, 8 /* PROPS */, ["messages", "title", "allowReply", "replyLabel", "placeholder", "sendLabel", "endpoint", "emptyText", "onSent"])
20
+ ]))
21
+ }
22
+
23
+ export { render };
24
+ //# sourceMappingURL=CnConversationWidget.vue3.js.map