@conduction/nextcloud-vue 2.96.0 → 2.98.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 (163) hide show
  1. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js +17 -0
  2. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
  3. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js.map +1 -1
  4. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue.js +1 -1
  5. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js +33 -1
  6. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js.map +1 -1
  7. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js +383 -359
  8. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js.map +1 -1
  9. package/dist/esm/components/CnChartWidget/CnChartWidget.vue.js +1 -1
  10. package/dist/esm/components/CnChartWidget/CnChartWidget.vue2.js +106 -2
  11. package/dist/esm/components/CnChartWidget/CnChartWidget.vue2.js.map +1 -1
  12. package/dist/esm/components/CnChartWidget/CnChartWidget.vue3.js.map +1 -1
  13. package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue.js +9 -0
  14. package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue.js.map +1 -0
  15. package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue2.js +964 -0
  16. package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue2.js.map +1 -0
  17. package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue3.js +282 -0
  18. package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue3.js.map +1 -0
  19. package/dist/esm/components/CnChecklistRunWidget/index.js +28 -0
  20. package/dist/esm/components/CnChecklistRunWidget/index.js.map +1 -0
  21. package/dist/esm/components/CnConversationThread/CnConversationThread.vue.js +1 -1
  22. package/dist/esm/components/CnConversationThread/CnConversationThread.vue2.js +17 -3
  23. package/dist/esm/components/CnConversationThread/CnConversationThread.vue2.js.map +1 -1
  24. package/dist/esm/components/CnConversationThread/CnConversationThread.vue3.js +61 -43
  25. package/dist/esm/components/CnConversationThread/CnConversationThread.vue3.js.map +1 -1
  26. package/dist/esm/components/CnConversationThread/CnConversationWidget.vue.js +1 -1
  27. package/dist/esm/components/CnConversationThread/CnConversationWidget.vue2.js +159 -18
  28. package/dist/esm/components/CnConversationThread/CnConversationWidget.vue2.js.map +1 -1
  29. package/dist/esm/components/CnConversationThread/CnConversationWidget.vue3.js +9 -7
  30. package/dist/esm/components/CnConversationThread/CnConversationWidget.vue3.js.map +1 -1
  31. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue.js +1 -1
  32. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +1 -0
  33. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  34. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  35. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +26 -8
  36. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
  37. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
  38. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +188 -5
  39. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  40. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  41. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue.js +1 -1
  42. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js +48 -3
  43. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js.map +1 -1
  44. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js +46 -12
  45. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js.map +1 -1
  46. package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue.js +1 -1
  47. package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue2.js +3 -3
  48. package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue2.js.map +1 -1
  49. package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue3.js.map +1 -1
  50. package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue.js +1 -1
  51. package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue2.js +27 -0
  52. package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue2.js.map +1 -1
  53. package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue3.js +16 -2
  54. package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue3.js.map +1 -1
  55. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue.js +1 -1
  56. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js +144 -0
  57. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js.map +1 -1
  58. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js +488 -399
  59. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js.map +1 -1
  60. package/dist/esm/components/CnFilesBrowser/filesBrowserColumns.js +8 -2
  61. package/dist/esm/components/CnFilesBrowser/filesBrowserColumns.js.map +1 -1
  62. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +344 -21
  63. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  64. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +29 -7
  65. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  66. package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue.js +1 -1
  67. package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue2.js +2 -2
  68. package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue2.js.map +1 -1
  69. package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue3.js +2 -2
  70. package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue3.js.map +1 -1
  71. package/dist/esm/components/CnIndexPage/useNamedSource.js +1 -1
  72. package/dist/esm/components/CnIndexPage/useNamedSource.js.map +1 -1
  73. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue.js +1 -1
  74. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js +13 -0
  75. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js.map +1 -1
  76. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js +5 -1
  77. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js.map +1 -1
  78. package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue.js +9 -0
  79. package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue.js.map +1 -0
  80. package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue2.js +220 -0
  81. package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue2.js.map +1 -0
  82. package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue3.js +121 -0
  83. package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue3.js.map +1 -0
  84. package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue.js +1 -1
  85. package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue2.js +83 -9
  86. package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue2.js.map +1 -1
  87. package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue3.js +80 -11
  88. package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue3.js.map +1 -1
  89. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js +1 -0
  90. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js.map +1 -1
  91. package/dist/esm/composables/indexSources.js +420 -5
  92. package/dist/esm/composables/indexSources.js.map +1 -1
  93. package/dist/esm/composables/useOrganisationStore.js +115 -0
  94. package/dist/esm/composables/useOrganisationStore.js.map +1 -0
  95. package/dist/esm/index.js +2 -0
  96. package/dist/esm/index.js.map +1 -1
  97. package/dist/esm/l10n/en.json.js +21 -1
  98. package/dist/esm/l10n/en.json.js.map +1 -1
  99. package/dist/esm/l10n/nl.json.js +36 -1
  100. package/dist/esm/l10n/nl.json.js.map +1 -1
  101. package/dist/esm/library-style.js +1 -1
  102. package/dist/esm/nextcloud-vue.css +725 -383
  103. package/dist/esm/utils/builtInFormatters.js +34 -1
  104. package/dist/esm/utils/builtInFormatters.js.map +1 -1
  105. package/dist/esm/utils/libraryWidgetKeys.js +1 -0
  106. package/dist/esm/utils/libraryWidgetKeys.js.map +1 -1
  107. package/dist/esm/utils/matrixCells.js +158 -0
  108. package/dist/esm/utils/matrixCells.js.map +1 -0
  109. package/dist/esm/utils/nextcloudDirectory.js +121 -0
  110. package/dist/esm/utils/nextcloudDirectory.js.map +1 -0
  111. package/dist/esm/utils/queryPresets.js +13 -2
  112. package/dist/esm/utils/queryPresets.js.map +1 -1
  113. package/dist/esm/utils/scopeListLayout.js +37 -1
  114. package/dist/esm/utils/scopeListLayout.js.map +1 -1
  115. package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
  116. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  117. package/dist/nextcloud-vue.cjs.js +14443 -10519
  118. package/dist/nextcloud-vue.cjs.js.map +2 -2
  119. package/dist/nextcloud-vue.css +725 -383
  120. package/l10n/en.json +21 -1
  121. package/l10n/nl.json +36 -1
  122. package/package.json +2 -2
  123. package/scripts/merge-l10n.js +196 -0
  124. package/src/components/CnAppRoot/CnAppRoot.md +1 -0
  125. package/src/components/CnAppRoot/CnAppRoot.vue +17 -0
  126. package/src/components/CnCellRenderer/CnCellRenderer.vue +57 -1
  127. package/src/components/CnChartWidget/CnChartWidget.md +2 -1
  128. package/src/components/CnChartWidget/CnChartWidget.vue +106 -2
  129. package/src/components/CnChecklistRunWidget/CnChecklistRunWidget.vue +1269 -0
  130. package/src/components/CnChecklistRunWidget/index.js +27 -0
  131. package/src/components/CnConversationThread/CnConversationThread.md +12 -0
  132. package/src/components/CnConversationThread/CnConversationThread.vue +30 -4
  133. package/src/components/CnConversationThread/CnConversationWidget.vue +167 -24
  134. package/src/components/CnDashboardPage/CnDashboardPage.vue +1 -0
  135. package/src/components/CnDataTable/CnDataTable.vue +26 -8
  136. package/src/components/CnDetailPage/CnDetailPage.md +48 -0
  137. package/src/components/CnDetailPage/CnDetailPage.vue +188 -5
  138. package/src/components/CnFactsWidget/CnFactsWidget.vue +114 -3
  139. package/src/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue +3 -3
  140. package/src/components/CnFeaturesBoard/CnFeaturesBoard.vue +38 -2
  141. package/src/components/CnFilesBrowser/CnFilesBrowser.vue +508 -236
  142. package/src/components/CnFilesBrowser/filesBrowserColumns.js +8 -2
  143. package/src/components/CnIndexPage/CnIndexPage.md +28 -0
  144. package/src/components/CnIndexPage/CnIndexPage.vue +367 -27
  145. package/src/components/CnIndexPage/CnIndexPageWidgets.vue +13 -4
  146. package/src/components/CnIndexPage/useNamedSource.js +1 -1
  147. package/src/components/CnObjectSidebar/CnFilesTab.vue +17 -0
  148. package/src/components/CnOrderedListField/CnOrderedListField.vue +386 -0
  149. package/src/components/CnOrderedListField/index.js +7 -0
  150. package/src/components/CnUserSettingsDialog/CnUserSettingsDialog.vue +139 -10
  151. package/src/components/CnWidgetGrid/registerDashboardWidgets.js +1 -0
  152. package/src/components/index.js +2 -0
  153. package/src/composables/indexSources.js +419 -4
  154. package/src/composables/useOrganisationStore.js +111 -0
  155. package/src/index.js +2 -0
  156. package/src/schemas/app-manifest-v2.schema.json +42 -5
  157. package/src/utils/builtInFormatters.js +33 -0
  158. package/src/utils/libraryWidgetKeys.js +1 -0
  159. package/src/utils/matrixCells.js +154 -0
  160. package/src/utils/nextcloudDirectory.js +117 -0
  161. package/src/utils/queryPresets.js +13 -2
  162. package/src/utils/scopeListLayout.js +37 -1
  163. package/src/utils/validateManifestV2.compiled.js +3 -3
@@ -1,14 +1,27 @@
1
1
  import __sfc_main from './CnConversationThread.vue.js';
2
+ import { dispatchAction } from '../../utils/actionsDispatcher.js';
3
+ import { fetchReferenceObjects } from '../../utils/boardReferenceColumns.js';
2
4
  import { readPath } from '../../utils/readPath.js';
5
+ import { resolveDeepTokens, hasUnresolvedDeepTokens } from '../../utils/resolveFilterTokens.js';
3
6
 
4
7
  /**
5
8
  * CnConversationWidget: the `conversation` detail widget type.
6
9
  *
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, and exported for an app that renders it itself.
10
+ * Renders CnConversationThread from the messages of a record, oldest first.
11
+ * The messages are a list on the bound record (`field`), or the objects of a
12
+ * register list (`source: { register, schema, filter?, limit? }`, the filter
13
+ * token-resolved like a record-list's: `{ "case": "@objectId" }`).
14
+ *
15
+ * Mapping keys name where each message keeps its parts: `textField` (alias
16
+ * `bodyField`), `timeField` (alias `timestampField`), `authorField` (alias
17
+ * `senderField`), `sideField` (alias `directionField`) with `usValue` (alias
18
+ * `outValue`) for the messages the organisation sent, and `channelField` with
19
+ * an optional `channelMap` from stored value to label ("email" to "E-mail").
20
+ *
21
+ * The reply box posts to `endpoint` (`@objectId` filled in), or runs
22
+ * `replyAction`, an action descriptor the app decides the meaning of: it is
23
+ * dispatched with `args: [{ text, objectId }]` and `props.text`. After a send
24
+ * the source is read again.
12
25
  */
13
26
  var script = {
14
27
  name: 'CnConversationWidget',
@@ -17,8 +30,19 @@ var script = {
17
30
  CnConversationThread: __sfc_main,
18
31
  },
19
32
 
33
+ inject: {
34
+ cnTranslate: { default: () => (key) => key },
35
+ /** Pre-bound action dispatcher of the page, for `replyAction`. */
36
+ cnDispatchAction: { default: null },
37
+ },
38
+
20
39
  props: {
21
- /** The widget's content. `field` is the dot-path to the messages on the record. */
40
+ /**
41
+ * The widget's content: `field` (the dot-path to the messages on the
42
+ * record) or `source`, the mapping keys, `endpoint` or `replyAction`,
43
+ * and the texts (`title`, `replyLabel`, `placeholder`, `sendLabel`,
44
+ * `emptyText`, translated).
45
+ */
22
46
  content: {
23
47
  type: Object,
24
48
  default: () => ({}),
@@ -41,29 +65,63 @@ var script = {
41
65
  return {
42
66
  /** Replies sent from this widget that the record does not hold yet. */
43
67
  sentHere: [],
68
+ /** The messages read from `source`. */
69
+ listed: [],
70
+ /** Whether `source` is being read. */
71
+ loading: false,
44
72
  }
45
73
  },
46
74
 
47
75
  computed: {
76
+ /** @return {object|null} The register list the messages come from, or null. */
77
+ source() {
78
+ const source = this.content?.source;
79
+ return source && typeof source === 'object' && source.register && source.schema ? source : null
80
+ },
81
+
82
+ /** @return {object|null} The reply action, or null. */
83
+ replyAction() {
84
+ const action = this.content?.replyAction;
85
+ return action && typeof action === 'object' ? action : null
86
+ },
87
+
48
88
  /**
49
- * The record's messages in the thread's shape, plus the replies sent here.
89
+ * The messages in the thread's shape, oldest first, plus the replies
90
+ * sent here.
50
91
  *
51
92
  * @return {Array<object>} The messages.
52
93
  * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-conversation-thread
94
+ * @spec openspec/changes/conversation-source/specs/conversation-source/spec.md#requirement-a-conversation-reads-its-messages-from-a-list-and-maps-their-parts
53
95
  */
54
96
  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 : [])
97
+ const c = this.content || {};
98
+ const raw = this.source ? this.listed : readPath(this.objectData || {}, c.field || 'messages');
99
+ const usValue = String(c.outValue ?? c.usValue ?? 'us');
100
+ const channelMap = c.channelMap && typeof c.channelMap === 'object' ? c.channelMap : {};
101
+ const stored = (Array.isArray(raw) ? raw : [])
59
102
  .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
- }));
103
+ .map((message, index) => {
104
+ const channel = c.channelField ? readPath(message, c.channelField) : undefined;
105
+ const channelKey = channel === undefined || channel === null ? '' : String(channel);
106
+ return {
107
+ id: message.id ?? `message-${index}`,
108
+ author: readPath(message, c.senderField || c.authorField || 'author'),
109
+ time: readPath(message, c.timestampField || c.timeField || 'time'),
110
+ text: readPath(message, c.bodyField || c.textField || 'text'),
111
+ side: String(readPath(message, c.directionField || c.sideField || 'side')) === usValue ? 'us' : 'them',
112
+ channel: channelKey === '' ? '' : this.tr(Object.hasOwn(channelMap, channelKey) ? channelMap[channelKey] : channelKey),
113
+ order: index,
114
+ }
115
+ });
116
+ const at = (message) => {
117
+ const ms = Date.parse(message.time);
118
+ return Number.isNaN(ms) ? null : ms
119
+ };
120
+ stored.sort((a, b) => {
121
+ const ta = at(a);
122
+ const tb = at(b);
123
+ return (ta === null || tb === null || ta === tb) ? a.order - b.order : ta - tb
124
+ });
67
125
  return [...stored, ...this.sentHere]
68
126
  },
69
127
 
@@ -80,6 +138,11 @@ var script = {
80
138
  }
81
139
  return url.replace(/@objectId/g, encodeURIComponent(String(this.objectId ?? '')))
82
140
  },
141
+
142
+ /** @return {object} The token context for `source.filter` and `replyAction`. */
143
+ tokenCtx() {
144
+ return { objectId: this.objectId, object: this.objectData || {} }
145
+ },
83
146
  },
84
147
 
85
148
  watch: {
@@ -87,9 +150,87 @@ var script = {
87
150
  objectData() {
88
151
  this.sentHere = [];
89
152
  },
153
+
154
+ objectId: {
155
+ immediate: true,
156
+ handler() {
157
+ this.loadSource();
158
+ },
159
+ },
90
160
  },
91
161
 
92
162
  methods: {
163
+ /**
164
+ * Translate through the app's translate.
165
+ *
166
+ * @param {string} key The source string.
167
+ * @return {string}
168
+ */
169
+ tr(key) {
170
+ return key && typeof this.cnTranslate === 'function' ? this.cnTranslate(key) : (key || '')
171
+ },
172
+
173
+ /**
174
+ * Read the messages from `source`. A filter whose token does not
175
+ * resolve sends no request; a failed read leaves the thread empty.
176
+ *
177
+ * @return {Promise<void>}
178
+ * @spec openspec/changes/conversation-source/specs/conversation-source/spec.md#requirement-a-conversation-reads-its-messages-from-a-list-and-maps-their-parts
179
+ */
180
+ async loadSource() {
181
+ if (!this.source) {
182
+ return
183
+ }
184
+ const raw = this.source.filter || {};
185
+ const filter = resolveDeepTokens(raw, this.tokenCtx);
186
+ const emptied = Object.keys(raw).some((key) => typeof raw[key] === 'string' && raw[key].startsWith('@') && (filter[key] === '' || filter[key] === null || filter[key] === undefined));
187
+ if (emptied || hasUnresolvedDeepTokens(filter)) {
188
+ this.listed = [];
189
+ return
190
+ }
191
+ this.loading = true;
192
+ try {
193
+ this.listed = await fetchReferenceObjects({ register: this.source.register, schema: this.source.schema, filter, limit: this.source.limit || 200 });
194
+ this.sentHere = [];
195
+ } catch {
196
+ this.listed = [];
197
+ } finally {
198
+ this.loading = false;
199
+ }
200
+ },
201
+
202
+ /**
203
+ * Run `replyAction` with the reply, when there is one; the endpoint
204
+ * path is the thread's own.
205
+ *
206
+ * @param {string} text The reply.
207
+ * @return {Promise<void>}
208
+ * @spec openspec/changes/conversation-source/specs/conversation-source/spec.md#requirement-a-reply-can-run-an-app-action
209
+ */
210
+ async onSend(text) {
211
+ if (!this.replyAction) {
212
+ return
213
+ }
214
+ const resolved = resolveDeepTokens(this.replyAction, this.tokenCtx);
215
+ const action = { ...resolved, args: [{ text, objectId: this.objectId }], props: { ...(resolved.props || {}), text } };
216
+ const result = typeof this.cnDispatchAction === 'function'
217
+ ? this.cnDispatchAction(action)
218
+ : dispatchAction(action, { router: this.$router || null });
219
+ try {
220
+ const outcome = await result;
221
+ if (outcome === false) {
222
+ return
223
+ }
224
+ } catch {
225
+ return
226
+ }
227
+ if (this.source) {
228
+ await this.loadSource();
229
+ return
230
+ }
231
+ this.onSent({ text });
232
+ },
233
+
93
234
  /**
94
235
  * Show an accepted reply at once, without waiting for the record.
95
236
  *
@@ -1 +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, and exported for an app that renders it itself.\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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"}
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=\"tr(content.title || '')\"\n\t\t\t:allowReply=\"content.allowReply !== false\"\n\t\t\t:replyLabel=\"content.replyLabel ? tr(content.replyLabel) : undefined\"\n\t\t\t:placeholder=\"tr(content.placeholder || '')\"\n\t\t\t:sendLabel=\"content.sendLabel ? tr(content.sendLabel) : undefined\"\n\t\t\t:endpoint=\"replyAction ? '' : endpoint\"\n\t\t\t:loading=\"loading\"\n\t\t\t:emptyText=\"content.emptyText ? tr(content.emptyText) : undefined\"\n\t\t\t@send=\"onSend\"\n\t\t\t@sent=\"onSent\" />\n\t</div>\n</template>\n\n<script>\nimport CnConversationThread from './CnConversationThread.vue'\nimport { dispatchAction } from '../../utils/actionsDispatcher.js'\nimport { fetchReferenceObjects } from '../../utils/boardReferenceColumns.js'\nimport { readPath } from '../../utils/readPath.js'\nimport { hasUnresolvedDeepTokens, resolveDeepTokens } from '../../utils/resolveFilterTokens.js'\n\n/**\n * CnConversationWidget: the `conversation` detail widget type.\n *\n * Renders CnConversationThread from the messages of a record, oldest first.\n * The messages are a list on the bound record (`field`), or the objects of a\n * register list (`source: { register, schema, filter?, limit? }`, the filter\n * token-resolved like a record-list's: `{ \"case\": \"@objectId\" }`).\n *\n * Mapping keys name where each message keeps its parts: `textField` (alias\n * `bodyField`), `timeField` (alias `timestampField`), `authorField` (alias\n * `senderField`), `sideField` (alias `directionField`) with `usValue` (alias\n * `outValue`) for the messages the organisation sent, and `channelField` with\n * an optional `channelMap` from stored value to label (\"email\" to \"E-mail\").\n *\n * The reply box posts to `endpoint` (`@objectId` filled in), or runs\n * `replyAction`, an action descriptor the app decides the meaning of: it is\n * dispatched with `args: [{ text, objectId }]` and `props.text`. After a send\n * the source is read again.\n */\nexport default {\n\tname: 'CnConversationWidget',\n\n\tcomponents: {\n\t\tCnConversationThread,\n\t},\n\n\tinject: {\n\t\tcnTranslate: { default: () => (key) => key },\n\t\t/** Pre-bound action dispatcher of the page, for `replyAction`. */\n\t\tcnDispatchAction: { default: null },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The widget's content: `field` (the dot-path to the messages on the\n\t\t * record) or `source`, the mapping keys, `endpoint` or `replyAction`,\n\t\t * and the texts (`title`, `replyLabel`, `placeholder`, `sendLabel`,\n\t\t * `emptyText`, translated).\n\t\t */\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\t/** The messages read from `source`. */\n\t\t\tlisted: [],\n\t\t\t/** Whether `source` is being read. */\n\t\t\tloading: false,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/** @return {object|null} The register list the messages come from, or null. */\n\t\tsource() {\n\t\t\tconst source = this.content?.source\n\t\t\treturn source && typeof source === 'object' && source.register && source.schema ? source : null\n\t\t},\n\n\t\t/** @return {object|null} The reply action, or null. */\n\t\treplyAction() {\n\t\t\tconst action = this.content?.replyAction\n\t\t\treturn action && typeof action === 'object' ? action : null\n\t\t},\n\n\t\t/**\n\t\t * The messages in the thread's shape, oldest first, plus the replies\n\t\t * 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 * @spec openspec/changes/conversation-source/specs/conversation-source/spec.md#requirement-a-conversation-reads-its-messages-from-a-list-and-maps-their-parts\n\t\t */\n\t\tmessages() {\n\t\t\tconst c = this.content || {}\n\t\t\tconst raw = this.source ? this.listed : readPath(this.objectData || {}, c.field || 'messages')\n\t\t\tconst usValue = String(c.outValue ?? c.usValue ?? 'us')\n\t\t\tconst channelMap = c.channelMap && typeof c.channelMap === 'object' ? c.channelMap : {}\n\t\t\tconst stored = (Array.isArray(raw) ? raw : [])\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 channel = c.channelField ? readPath(message, c.channelField) : undefined\n\t\t\t\t\tconst channelKey = channel === undefined || channel === null ? '' : String(channel)\n\t\t\t\t\treturn {\n\t\t\t\t\t\tid: message.id ?? `message-${index}`,\n\t\t\t\t\t\tauthor: readPath(message, c.senderField || c.authorField || 'author'),\n\t\t\t\t\t\ttime: readPath(message, c.timestampField || c.timeField || 'time'),\n\t\t\t\t\t\ttext: readPath(message, c.bodyField || c.textField || 'text'),\n\t\t\t\t\t\tside: String(readPath(message, c.directionField || c.sideField || 'side')) === usValue ? 'us' : 'them',\n\t\t\t\t\t\tchannel: channelKey === '' ? '' : this.tr(Object.hasOwn(channelMap, channelKey) ? channelMap[channelKey] : channelKey),\n\t\t\t\t\t\torder: index,\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\tconst at = (message) => {\n\t\t\t\tconst ms = Date.parse(message.time)\n\t\t\t\treturn Number.isNaN(ms) ? null : ms\n\t\t\t}\n\t\t\tstored.sort((a, b) => {\n\t\t\t\tconst ta = at(a)\n\t\t\t\tconst tb = at(b)\n\t\t\t\treturn (ta === null || tb === null || ta === tb) ? a.order - b.order : ta - tb\n\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\n\t\t/** @return {object} The token context for `source.filter` and `replyAction`. */\n\t\ttokenCtx() {\n\t\t\treturn { objectId: this.objectId, object: this.objectData || {} }\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\n\t\tobjectId: {\n\t\t\timmediate: true,\n\t\t\thandler() {\n\t\t\t\tthis.loadSource()\n\t\t\t},\n\t\t},\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Translate through the app's translate.\n\t\t *\n\t\t * @param {string} key The source string.\n\t\t * @return {string}\n\t\t */\n\t\ttr(key) {\n\t\t\treturn key && typeof this.cnTranslate === 'function' ? this.cnTranslate(key) : (key || '')\n\t\t},\n\n\t\t/**\n\t\t * Read the messages from `source`. A filter whose token does not\n\t\t * resolve sends no request; a failed read leaves the thread empty.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t * @spec openspec/changes/conversation-source/specs/conversation-source/spec.md#requirement-a-conversation-reads-its-messages-from-a-list-and-maps-their-parts\n\t\t */\n\t\tasync loadSource() {\n\t\t\tif (!this.source) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst raw = this.source.filter || {}\n\t\t\tconst filter = resolveDeepTokens(raw, this.tokenCtx)\n\t\t\tconst emptied = Object.keys(raw).some((key) => typeof raw[key] === 'string' && raw[key].startsWith('@') && (filter[key] === '' || filter[key] === null || filter[key] === undefined))\n\t\t\tif (emptied || hasUnresolvedDeepTokens(filter)) {\n\t\t\t\tthis.listed = []\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.loading = true\n\t\t\ttry {\n\t\t\t\tthis.listed = await fetchReferenceObjects({ register: this.source.register, schema: this.source.schema, filter, limit: this.source.limit || 200 })\n\t\t\t\tthis.sentHere = []\n\t\t\t} catch {\n\t\t\t\tthis.listed = []\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Run `replyAction` with the reply, when there is one; the endpoint\n\t\t * path is the thread's own.\n\t\t *\n\t\t * @param {string} text The reply.\n\t\t * @return {Promise<void>}\n\t\t * @spec openspec/changes/conversation-source/specs/conversation-source/spec.md#requirement-a-reply-can-run-an-app-action\n\t\t */\n\t\tasync onSend(text) {\n\t\t\tif (!this.replyAction) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst resolved = resolveDeepTokens(this.replyAction, this.tokenCtx)\n\t\t\tconst action = { ...resolved, args: [{ text, objectId: this.objectId }], props: { ...(resolved.props || {}), text } }\n\t\t\tconst result = typeof this.cnDispatchAction === 'function'\n\t\t\t\t? this.cnDispatchAction(action)\n\t\t\t\t: dispatchAction(action, { router: this.$router || null })\n\t\t\ttry {\n\t\t\t\tconst outcome = await result\n\t\t\t\tif (outcome === false) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (this.source) {\n\t\t\t\tawait this.loadSource()\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.onSent({ text })\n\t\t},\n\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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"}
@@ -8,15 +8,17 @@ function render(_ctx, _cache, $props, $setup, $data, $options) {
8
8
  return (openBlock(), createElementBlock("div", _hoisted_1, [
9
9
  createVNode(_component_CnConversationThread, {
10
10
  messages: $options.messages,
11
- title: $props.content.title || '',
11
+ title: $options.tr($props.content.title || ''),
12
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,
13
+ replyLabel: $props.content.replyLabel ? $options.tr($props.content.replyLabel) : undefined,
14
+ placeholder: $options.tr($props.content.placeholder || ''),
15
+ sendLabel: $props.content.sendLabel ? $options.tr($props.content.sendLabel) : undefined,
16
+ endpoint: $options.replyAction ? '' : $options.endpoint,
17
+ loading: $data.loading,
18
+ emptyText: $props.content.emptyText ? $options.tr($props.content.emptyText) : undefined,
19
+ onSend: $options.onSend,
18
20
  onSent: $options.onSent
19
- }, null, 8 /* PROPS */, ["messages", "title", "allowReply", "replyLabel", "placeholder", "sendLabel", "endpoint", "emptyText", "onSent"])
21
+ }, null, 8 /* PROPS */, ["messages", "title", "allowReply", "replyLabel", "placeholder", "sendLabel", "endpoint", "loading", "emptyText", "onSend", "onSent"])
20
22
  ]))
21
23
  }
22
24
 
@@ -1 +1 @@
1
- {"version":3,"file":"CnConversationWidget.vue3.js","sources":["../../../../src/components/CnConversationThread/CnConversationWidget.vue?vue&type=template&id=2f1f2943&lang.js"],"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, and exported for an app that renders it itself.\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":["_openBlock","_createElementBlock","_createVNode"],"mappings":";;AAWM,MAAA,UAAA,GAAA,EAAA,KAAK,EAAC,qBAAqB,GAAA;;;;;AAA9B,EAAA,QAAAA,SAAA,EAAA,EAAAC,kBAAA,QAAA;IACAC;AACC,MAAA,QAAQ,EAAE,QAAQ,CAAA,QAAA;MAClB,KAAK,EAAE,MAAO,CAAA,OAAA,CAAC,KAAG,IAAA,EAAA;MAClB,UAAU,EAAE,MAAO,CAAA,OAAA,CAAC,UAAU,KAAA,KAAA;AAC9B,MAAA,UAAU,EAAE,MAAA,CAAA,OAAG,CAAA,UAAA,IAAA,SAAA;MACf,WAAE,EAAA,MAAA,CAAA,OAAA,CAAA,WAAA,IAAA,EAAA;;;;;;;;;;;"}
1
+ {"version":3,"file":"CnConversationWidget.vue3.js","sources":["../../../../src/components/CnConversationThread/CnConversationWidget.vue?vue&type=template&id=257593ce&lang.js"],"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=\"tr(content.title || '')\"\n\t\t\t:allowReply=\"content.allowReply !== false\"\n\t\t\t:replyLabel=\"content.replyLabel ? tr(content.replyLabel) : undefined\"\n\t\t\t:placeholder=\"tr(content.placeholder || '')\"\n\t\t\t:sendLabel=\"content.sendLabel ? tr(content.sendLabel) : undefined\"\n\t\t\t:endpoint=\"replyAction ? '' : endpoint\"\n\t\t\t:loading=\"loading\"\n\t\t\t:emptyText=\"content.emptyText ? tr(content.emptyText) : undefined\"\n\t\t\t@send=\"onSend\"\n\t\t\t@sent=\"onSent\" />\n\t</div>\n</template>\n\n<script>\nimport CnConversationThread from './CnConversationThread.vue'\nimport { dispatchAction } from '../../utils/actionsDispatcher.js'\nimport { fetchReferenceObjects } from '../../utils/boardReferenceColumns.js'\nimport { readPath } from '../../utils/readPath.js'\nimport { hasUnresolvedDeepTokens, resolveDeepTokens } from '../../utils/resolveFilterTokens.js'\n\n/**\n * CnConversationWidget: the `conversation` detail widget type.\n *\n * Renders CnConversationThread from the messages of a record, oldest first.\n * The messages are a list on the bound record (`field`), or the objects of a\n * register list (`source: { register, schema, filter?, limit? }`, the filter\n * token-resolved like a record-list's: `{ \"case\": \"@objectId\" }`).\n *\n * Mapping keys name where each message keeps its parts: `textField` (alias\n * `bodyField`), `timeField` (alias `timestampField`), `authorField` (alias\n * `senderField`), `sideField` (alias `directionField`) with `usValue` (alias\n * `outValue`) for the messages the organisation sent, and `channelField` with\n * an optional `channelMap` from stored value to label (\"email\" to \"E-mail\").\n *\n * The reply box posts to `endpoint` (`@objectId` filled in), or runs\n * `replyAction`, an action descriptor the app decides the meaning of: it is\n * dispatched with `args: [{ text, objectId }]` and `props.text`. After a send\n * the source is read again.\n */\nexport default {\n\tname: 'CnConversationWidget',\n\n\tcomponents: {\n\t\tCnConversationThread,\n\t},\n\n\tinject: {\n\t\tcnTranslate: { default: () => (key) => key },\n\t\t/** Pre-bound action dispatcher of the page, for `replyAction`. */\n\t\tcnDispatchAction: { default: null },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The widget's content: `field` (the dot-path to the messages on the\n\t\t * record) or `source`, the mapping keys, `endpoint` or `replyAction`,\n\t\t * and the texts (`title`, `replyLabel`, `placeholder`, `sendLabel`,\n\t\t * `emptyText`, translated).\n\t\t */\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\t/** The messages read from `source`. */\n\t\t\tlisted: [],\n\t\t\t/** Whether `source` is being read. */\n\t\t\tloading: false,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/** @return {object|null} The register list the messages come from, or null. */\n\t\tsource() {\n\t\t\tconst source = this.content?.source\n\t\t\treturn source && typeof source === 'object' && source.register && source.schema ? source : null\n\t\t},\n\n\t\t/** @return {object|null} The reply action, or null. */\n\t\treplyAction() {\n\t\t\tconst action = this.content?.replyAction\n\t\t\treturn action && typeof action === 'object' ? action : null\n\t\t},\n\n\t\t/**\n\t\t * The messages in the thread's shape, oldest first, plus the replies\n\t\t * 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 * @spec openspec/changes/conversation-source/specs/conversation-source/spec.md#requirement-a-conversation-reads-its-messages-from-a-list-and-maps-their-parts\n\t\t */\n\t\tmessages() {\n\t\t\tconst c = this.content || {}\n\t\t\tconst raw = this.source ? this.listed : readPath(this.objectData || {}, c.field || 'messages')\n\t\t\tconst usValue = String(c.outValue ?? c.usValue ?? 'us')\n\t\t\tconst channelMap = c.channelMap && typeof c.channelMap === 'object' ? c.channelMap : {}\n\t\t\tconst stored = (Array.isArray(raw) ? raw : [])\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 channel = c.channelField ? readPath(message, c.channelField) : undefined\n\t\t\t\t\tconst channelKey = channel === undefined || channel === null ? '' : String(channel)\n\t\t\t\t\treturn {\n\t\t\t\t\t\tid: message.id ?? `message-${index}`,\n\t\t\t\t\t\tauthor: readPath(message, c.senderField || c.authorField || 'author'),\n\t\t\t\t\t\ttime: readPath(message, c.timestampField || c.timeField || 'time'),\n\t\t\t\t\t\ttext: readPath(message, c.bodyField || c.textField || 'text'),\n\t\t\t\t\t\tside: String(readPath(message, c.directionField || c.sideField || 'side')) === usValue ? 'us' : 'them',\n\t\t\t\t\t\tchannel: channelKey === '' ? '' : this.tr(Object.hasOwn(channelMap, channelKey) ? channelMap[channelKey] : channelKey),\n\t\t\t\t\t\torder: index,\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\tconst at = (message) => {\n\t\t\t\tconst ms = Date.parse(message.time)\n\t\t\t\treturn Number.isNaN(ms) ? null : ms\n\t\t\t}\n\t\t\tstored.sort((a, b) => {\n\t\t\t\tconst ta = at(a)\n\t\t\t\tconst tb = at(b)\n\t\t\t\treturn (ta === null || tb === null || ta === tb) ? a.order - b.order : ta - tb\n\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\n\t\t/** @return {object} The token context for `source.filter` and `replyAction`. */\n\t\ttokenCtx() {\n\t\t\treturn { objectId: this.objectId, object: this.objectData || {} }\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\n\t\tobjectId: {\n\t\t\timmediate: true,\n\t\t\thandler() {\n\t\t\t\tthis.loadSource()\n\t\t\t},\n\t\t},\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Translate through the app's translate.\n\t\t *\n\t\t * @param {string} key The source string.\n\t\t * @return {string}\n\t\t */\n\t\ttr(key) {\n\t\t\treturn key && typeof this.cnTranslate === 'function' ? this.cnTranslate(key) : (key || '')\n\t\t},\n\n\t\t/**\n\t\t * Read the messages from `source`. A filter whose token does not\n\t\t * resolve sends no request; a failed read leaves the thread empty.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t * @spec openspec/changes/conversation-source/specs/conversation-source/spec.md#requirement-a-conversation-reads-its-messages-from-a-list-and-maps-their-parts\n\t\t */\n\t\tasync loadSource() {\n\t\t\tif (!this.source) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst raw = this.source.filter || {}\n\t\t\tconst filter = resolveDeepTokens(raw, this.tokenCtx)\n\t\t\tconst emptied = Object.keys(raw).some((key) => typeof raw[key] === 'string' && raw[key].startsWith('@') && (filter[key] === '' || filter[key] === null || filter[key] === undefined))\n\t\t\tif (emptied || hasUnresolvedDeepTokens(filter)) {\n\t\t\t\tthis.listed = []\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.loading = true\n\t\t\ttry {\n\t\t\t\tthis.listed = await fetchReferenceObjects({ register: this.source.register, schema: this.source.schema, filter, limit: this.source.limit || 200 })\n\t\t\t\tthis.sentHere = []\n\t\t\t} catch {\n\t\t\t\tthis.listed = []\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Run `replyAction` with the reply, when there is one; the endpoint\n\t\t * path is the thread's own.\n\t\t *\n\t\t * @param {string} text The reply.\n\t\t * @return {Promise<void>}\n\t\t * @spec openspec/changes/conversation-source/specs/conversation-source/spec.md#requirement-a-reply-can-run-an-app-action\n\t\t */\n\t\tasync onSend(text) {\n\t\t\tif (!this.replyAction) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst resolved = resolveDeepTokens(this.replyAction, this.tokenCtx)\n\t\t\tconst action = { ...resolved, args: [{ text, objectId: this.objectId }], props: { ...(resolved.props || {}), text } }\n\t\t\tconst result = typeof this.cnDispatchAction === 'function'\n\t\t\t\t? this.cnDispatchAction(action)\n\t\t\t\t: dispatchAction(action, { router: this.$router || null })\n\t\t\ttry {\n\t\t\t\tconst outcome = await result\n\t\t\t\tif (outcome === false) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (this.source) {\n\t\t\t\tawait this.loadSource()\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.onSent({ text })\n\t\t},\n\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":["_openBlock","_createElementBlock","_createVNode"],"mappings":";;AAWM,MAAA,UAAA,GAAA,EAAA,KAAK,EAAC,qBAAqB,GAAA;;;;;AAA9B,EAAA,QAAAA,SAAA,EAAA,EAAAC,kBAAA,QAAA;IACAC;AACC,MAAA,QAAQ,EAAE,QAAQ,CAAA,QAAA;AAClB,MAAA,KAAK,EAAE,QAAA,CAAA,EAAE,CAAC,MAAA,CAAA,OAAO,CAAC,KAAI,IAAA,EAAA,CAAA;MACtB,UAAU,EAAE,MAAI,CAAA,OAAA,CAAA,UAAA,KAAA,KAAA;MAChB,UAAU,EAAE,MAAO,CAAA,OAAA,CAAC,UAAS,GAAI,QAAE,CAAA,EAAA,CAAC,MAAO,CAAA,OAAA,CAAC,UAAS,CAAA,GAAK,SAAM;AAChE,MAAA,WAAW,EAAC,QAAA,CAAA,EAAA,CAAA,MAAA,CAAA,OAAA,CAAA,WAAA,IAAA,EAAA,CAAA;MACZ,SAAS,EAAE,MAAI,CAAA,OAAA,CAAA,SAAA,GAAA,QAAA,CAAA,EAAA,CAAA,MAAA,CAAA,OAAA,CAAA,SAAA,CAAA,GAAA,SAAA;MACf,QAAE,EAAA,QAAA,CAAA,WAAA,GAAA,EAAA,GAAA,QAAA,CAAA,QAAA;;;;;;;;;;;"}
@@ -2,7 +2,7 @@ import script from './CnDashboardPage.vue2.js';
2
2
  import { render } from './CnDashboardPage.vue3.js';
3
3
 
4
4
  script.render = render;
5
- script.__scopeId = "data-v-7557b574";
5
+ script.__scopeId = "data-v-40c6b068";
6
6
  const __sfc_main = script;
7
7
 
8
8
  export { __sfc_main as default };
@@ -91,6 +91,7 @@ const CHART_PROP_KEYS = [
91
91
  'barMax',
92
92
  'footnote',
93
93
  'highlight',
94
+ 'cellSource',
94
95
  ];
95
96
 
96
97
  /**