@nyaruka/temba-components 0.130.1 → 0.130.3

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 (250) hide show
  1. package/CHANGELOG.md +34 -4
  2. package/DEV_DATA.md +89 -0
  3. package/demo/data/flows/food-order.json +4 -4
  4. package/demo/data/flows/sample-flow.json +132 -147
  5. package/dist/temba-components.js +787 -659
  6. package/dist/temba-components.js.map +1 -1
  7. package/out-tsc/src/display/Chat.js +5 -3
  8. package/out-tsc/src/display/Chat.js.map +1 -1
  9. package/out-tsc/src/events.js.map +1 -1
  10. package/out-tsc/src/flow/CanvasNode.js +83 -78
  11. package/out-tsc/src/flow/CanvasNode.js.map +1 -1
  12. package/out-tsc/src/flow/Editor.js +1 -0
  13. package/out-tsc/src/flow/Editor.js.map +1 -1
  14. package/out-tsc/src/flow/NodeEditor.js +47 -3
  15. package/out-tsc/src/flow/NodeEditor.js.map +1 -1
  16. package/out-tsc/src/flow/actions/add_contact_urn.js +1 -1
  17. package/out-tsc/src/flow/actions/add_contact_urn.js.map +1 -1
  18. package/out-tsc/src/flow/actions/set_contact_channel.js +1 -1
  19. package/out-tsc/src/flow/actions/set_contact_channel.js.map +1 -1
  20. package/out-tsc/src/flow/actions/set_contact_field.js +2 -1
  21. package/out-tsc/src/flow/actions/set_contact_field.js.map +1 -1
  22. package/out-tsc/src/flow/actions/set_contact_language.js +3 -1
  23. package/out-tsc/src/flow/actions/set_contact_language.js.map +1 -1
  24. package/out-tsc/src/flow/actions/set_contact_name.js +1 -1
  25. package/out-tsc/src/flow/actions/set_contact_name.js.map +1 -1
  26. package/out-tsc/src/flow/actions/set_contact_status.js +17 -14
  27. package/out-tsc/src/flow/actions/set_contact_status.js.map +1 -1
  28. package/out-tsc/src/flow/actions/set_run_result.js +1 -1
  29. package/out-tsc/src/flow/actions/set_run_result.js.map +1 -1
  30. package/out-tsc/src/flow/nodes/split_by_llm.js +12 -12
  31. package/out-tsc/src/flow/nodes/split_by_llm.js.map +1 -1
  32. package/out-tsc/src/flow/nodes/wait_for_response.js +609 -6
  33. package/out-tsc/src/flow/nodes/wait_for_response.js.map +1 -1
  34. package/out-tsc/src/flow/operators.js +194 -0
  35. package/out-tsc/src/flow/operators.js.map +1 -0
  36. package/out-tsc/src/flow/types.js.map +1 -1
  37. package/out-tsc/src/form/ArrayEditor.js +84 -19
  38. package/out-tsc/src/form/ArrayEditor.js.map +1 -1
  39. package/out-tsc/src/form/Checkbox.js +12 -0
  40. package/out-tsc/src/form/Checkbox.js.map +1 -1
  41. package/out-tsc/src/form/FieldRenderer.js +13 -3
  42. package/out-tsc/src/form/FieldRenderer.js.map +1 -1
  43. package/out-tsc/src/form/TextInput.js +20 -1
  44. package/out-tsc/src/form/TextInput.js.map +1 -1
  45. package/out-tsc/src/form/select/Select.js +7 -0
  46. package/out-tsc/src/form/select/Select.js.map +1 -1
  47. package/out-tsc/src/interfaces.js.map +1 -1
  48. package/out-tsc/src/layout/Dialog.js +3 -4
  49. package/out-tsc/src/layout/Dialog.js.map +1 -1
  50. package/out-tsc/src/list/RunList.js +2 -2
  51. package/out-tsc/src/list/RunList.js.map +1 -1
  52. package/out-tsc/src/live/ContactChat.js +101 -44
  53. package/out-tsc/src/live/ContactChat.js.map +1 -1
  54. package/out-tsc/src/live/ContactDetails.js +7 -0
  55. package/out-tsc/src/live/ContactDetails.js.map +1 -1
  56. package/out-tsc/src/live/ContactNameFetch.js +1 -1
  57. package/out-tsc/src/live/ContactNameFetch.js.map +1 -1
  58. package/out-tsc/src/webchat/index.js +0 -11
  59. package/out-tsc/src/webchat/index.js.map +1 -1
  60. package/out-tsc/test/NodeHelper.js +25 -27
  61. package/out-tsc/test/NodeHelper.js.map +1 -1
  62. package/out-tsc/test/nodes/split_by_llm.test.js +12 -4
  63. package/out-tsc/test/nodes/split_by_llm.test.js.map +1 -1
  64. package/out-tsc/test/nodes/split_by_llm_categorize.test.js +101 -91
  65. package/out-tsc/test/nodes/split_by_llm_categorize.test.js.map +1 -1
  66. package/out-tsc/test/nodes/split_by_random.test.js +120 -112
  67. package/out-tsc/test/nodes/split_by_random.test.js.map +1 -1
  68. package/out-tsc/test/nodes/wait_for_digits.test.js +131 -111
  69. package/out-tsc/test/nodes/wait_for_digits.test.js.map +1 -1
  70. package/out-tsc/test/nodes/wait_for_response.test.js +549 -85
  71. package/out-tsc/test/nodes/wait_for_response.test.js.map +1 -1
  72. package/out-tsc/test/temba-checkbox.test.js +32 -32
  73. package/out-tsc/test/temba-checkbox.test.js.map +1 -1
  74. package/out-tsc/test/temba-contact-chat.test.js +2 -1
  75. package/out-tsc/test/temba-contact-chat.test.js.map +1 -1
  76. package/out-tsc/test/temba-dropdown.test.js +0 -4
  77. package/out-tsc/test/temba-dropdown.test.js.map +1 -1
  78. package/out-tsc/test/temba-flow-editor-node.test.js +9 -4
  79. package/out-tsc/test/temba-flow-editor-node.test.js.map +1 -1
  80. package/out-tsc/test/temba-integration-markdown.test.js +13 -15
  81. package/out-tsc/test/temba-integration-markdown.test.js.map +1 -1
  82. package/out-tsc/test/temba-node-editor.test.js +5 -38
  83. package/out-tsc/test/temba-node-editor.test.js.map +1 -1
  84. package/out-tsc/test/temba-run-list.test.js +2 -2
  85. package/out-tsc/test/temba-run-list.test.js.map +1 -1
  86. package/out-tsc/test/utils.test.js +2 -1
  87. package/out-tsc/test/utils.test.js.map +1 -1
  88. package/package.json +6 -2
  89. package/screenshots/truth/actions/add_contact_groups/editor/descriptive-group-names.png +0 -0
  90. package/screenshots/truth/actions/add_contact_groups/editor/long-group-names.png +0 -0
  91. package/screenshots/truth/actions/add_contact_groups/editor/many-groups.png +0 -0
  92. package/screenshots/truth/actions/add_contact_groups/editor/multiple-groups.png +0 -0
  93. package/screenshots/truth/actions/add_contact_groups/editor/single-group.png +0 -0
  94. package/screenshots/truth/actions/add_contact_groups/render/descriptive-group-names.png +0 -0
  95. package/screenshots/truth/actions/add_contact_groups/render/long-group-names.png +0 -0
  96. package/screenshots/truth/actions/add_contact_groups/render/many-groups.png +0 -0
  97. package/screenshots/truth/actions/add_contact_groups/render/multiple-groups.png +0 -0
  98. package/screenshots/truth/actions/add_contact_groups/render/single-group.png +0 -0
  99. package/screenshots/truth/actions/remove_contact_groups/editor/cleanup-groups.png +0 -0
  100. package/screenshots/truth/actions/remove_contact_groups/editor/long-descriptive-group-names.png +0 -0
  101. package/screenshots/truth/actions/remove_contact_groups/editor/many-groups.png +0 -0
  102. package/screenshots/truth/actions/remove_contact_groups/editor/multiple-groups.png +0 -0
  103. package/screenshots/truth/actions/remove_contact_groups/editor/remove-from-all-groups.png +0 -0
  104. package/screenshots/truth/actions/remove_contact_groups/editor/single-group.png +0 -0
  105. package/screenshots/truth/actions/remove_contact_groups/render/cleanup-groups.png +0 -0
  106. package/screenshots/truth/actions/remove_contact_groups/render/long-descriptive-group-names.png +0 -0
  107. package/screenshots/truth/actions/remove_contact_groups/render/many-groups.png +0 -0
  108. package/screenshots/truth/actions/remove_contact_groups/render/multiple-groups.png +0 -0
  109. package/screenshots/truth/actions/remove_contact_groups/render/remove-from-all-groups.png +0 -0
  110. package/screenshots/truth/actions/remove_contact_groups/render/single-group.png +0 -0
  111. package/screenshots/truth/actions/send_email/editor/complex-business-email.png +0 -0
  112. package/screenshots/truth/actions/send_email/editor/empty-body.png +0 -0
  113. package/screenshots/truth/actions/send_email/editor/empty-subject.png +0 -0
  114. package/screenshots/truth/actions/send_email/editor/long-subject.png +0 -0
  115. package/screenshots/truth/actions/send_email/editor/multiline-body.png +0 -0
  116. package/screenshots/truth/actions/send_email/editor/multiple-recipients.png +0 -0
  117. package/screenshots/truth/actions/send_email/editor/simple-email.png +0 -0
  118. package/screenshots/truth/actions/send_email/editor/with-expressions.png +0 -0
  119. package/screenshots/truth/actions/send_email/render/complex-business-email.png +0 -0
  120. package/screenshots/truth/actions/send_email/render/empty-body.png +0 -0
  121. package/screenshots/truth/actions/send_email/render/empty-subject.png +0 -0
  122. package/screenshots/truth/actions/send_email/render/long-subject.png +0 -0
  123. package/screenshots/truth/actions/send_email/render/multiline-body.png +0 -0
  124. package/screenshots/truth/actions/send_email/render/multiple-recipients.png +0 -0
  125. package/screenshots/truth/actions/send_email/render/simple-email.png +0 -0
  126. package/screenshots/truth/actions/send_email/render/with-expressions.png +0 -0
  127. package/screenshots/truth/actions/send_msg/editor/long-quick-replies.png +0 -0
  128. package/screenshots/truth/actions/send_msg/editor/multiline-text-with-replies.png +0 -0
  129. package/screenshots/truth/actions/send_msg/editor/simple-text.png +0 -0
  130. package/screenshots/truth/actions/send_msg/editor/text-with-linebreaks.png +0 -0
  131. package/screenshots/truth/actions/send_msg/editor/text-with-many-quick-replies.png +0 -0
  132. package/screenshots/truth/actions/send_msg/editor/text-with-quick-replies.png +0 -0
  133. package/screenshots/truth/actions/send_msg/editor/text-without-quick-replies.png +0 -0
  134. package/screenshots/truth/actions/send_msg/render/long-quick-replies.png +0 -0
  135. package/screenshots/truth/actions/send_msg/render/multiline-text-with-replies.png +0 -0
  136. package/screenshots/truth/actions/send_msg/render/simple-text.png +0 -0
  137. package/screenshots/truth/actions/send_msg/render/text-with-linebreaks.png +0 -0
  138. package/screenshots/truth/actions/send_msg/render/text-with-many-quick-replies.png +0 -0
  139. package/screenshots/truth/actions/send_msg/render/text-with-quick-replies.png +0 -0
  140. package/screenshots/truth/actions/send_msg/render/text-without-quick-replies.png +0 -0
  141. package/screenshots/truth/checkbox/checkbox-label-background-hover.png +0 -0
  142. package/screenshots/truth/checkbox/checkbox-whitespace-label-no-background-hover.png +0 -0
  143. package/screenshots/truth/checkbox/checkbox-with-help-text.png +0 -0
  144. package/screenshots/truth/checkbox/checked.png +0 -0
  145. package/screenshots/truth/checkbox/default.png +0 -0
  146. package/screenshots/truth/editor/wait.png +0 -0
  147. package/screenshots/truth/integration/textinput-markdown-errors.png +0 -0
  148. package/screenshots/truth/lightbox/img-zoomed.png +0 -0
  149. package/screenshots/truth/nodes/split_by_llm/editor/information-extraction.png +0 -0
  150. package/screenshots/truth/nodes/split_by_llm/editor/sentiment-analysis.png +0 -0
  151. package/screenshots/truth/nodes/split_by_llm/editor/summarization.png +0 -0
  152. package/screenshots/truth/nodes/split_by_llm/editor/translation-task.png +0 -0
  153. package/screenshots/truth/nodes/split_by_llm/render/information-extraction.png +0 -0
  154. package/screenshots/truth/nodes/split_by_llm/render/sentiment-analysis.png +0 -0
  155. package/screenshots/truth/nodes/split_by_llm/render/summarization.png +0 -0
  156. package/screenshots/truth/nodes/split_by_llm/render/translation-task.png +0 -0
  157. package/screenshots/truth/nodes/split_by_llm_categorize/editor/basic-categorization.png +0 -0
  158. package/screenshots/truth/nodes/split_by_llm_categorize/editor/custom-input-and-result-name.png +0 -0
  159. package/screenshots/truth/nodes/split_by_llm_categorize/editor/feedback-categorization.png +0 -0
  160. package/screenshots/truth/nodes/split_by_llm_categorize/editor/many-categories.png +0 -0
  161. package/screenshots/truth/nodes/split_by_llm_categorize/editor/minimal-categories.png +0 -0
  162. package/screenshots/truth/nodes/split_by_llm_categorize/render/basic-categorization.png +0 -0
  163. package/screenshots/truth/nodes/split_by_llm_categorize/render/custom-input-and-result-name.png +0 -0
  164. package/screenshots/truth/nodes/split_by_llm_categorize/render/feedback-categorization.png +0 -0
  165. package/screenshots/truth/nodes/split_by_llm_categorize/render/many-categories.png +0 -0
  166. package/screenshots/truth/nodes/split_by_llm_categorize/render/minimal-categories.png +0 -0
  167. package/screenshots/truth/nodes/split_by_random/editor/ab-test-multiple-variants.png +0 -0
  168. package/screenshots/truth/nodes/split_by_random/editor/sampling-split.png +0 -0
  169. package/screenshots/truth/nodes/split_by_random/editor/three-way-split.png +0 -0
  170. package/screenshots/truth/nodes/split_by_random/editor/two-bucket-split.png +0 -0
  171. package/screenshots/truth/nodes/split_by_random/render/ab-test-multiple-variants.png +0 -0
  172. package/screenshots/truth/nodes/split_by_random/render/sampling-split.png +0 -0
  173. package/screenshots/truth/nodes/split_by_random/render/three-way-split.png +0 -0
  174. package/screenshots/truth/nodes/split_by_random/render/two-bucket-split.png +0 -0
  175. package/screenshots/truth/nodes/wait_for_digits/editor/basic-digits-wait.png +0 -0
  176. package/screenshots/truth/nodes/wait_for_digits/editor/phone-number-collection.png +0 -0
  177. package/screenshots/truth/nodes/wait_for_digits/editor/single-digit-with-timeout.png +0 -0
  178. package/screenshots/truth/nodes/wait_for_digits/editor/verification-code.png +0 -0
  179. package/screenshots/truth/nodes/wait_for_digits/render/basic-digits-wait.png +0 -0
  180. package/screenshots/truth/nodes/wait_for_digits/render/phone-number-collection.png +0 -0
  181. package/screenshots/truth/nodes/wait_for_digits/render/single-digit-with-timeout.png +0 -0
  182. package/screenshots/truth/nodes/wait_for_digits/render/verification-code.png +0 -0
  183. package/screenshots/truth/nodes/wait_for_response/editor/basic-wait.png +0 -0
  184. package/screenshots/truth/nodes/wait_for_response/editor/custom-result-name.png +0 -0
  185. package/screenshots/truth/nodes/wait_for_response/editor/no-timeout.png +0 -0
  186. package/screenshots/truth/nodes/wait_for_response/editor/short-timeout.png +0 -0
  187. package/screenshots/truth/nodes/wait_for_response/render/basic-wait.png +0 -0
  188. package/screenshots/truth/nodes/wait_for_response/render/custom-result-name.png +0 -0
  189. package/screenshots/truth/nodes/wait_for_response/render/no-timeout.png +0 -0
  190. package/screenshots/truth/nodes/wait_for_response/render/short-timeout.png +0 -0
  191. package/screenshots/truth/run-list/basic.png +0 -0
  192. package/screenshots/truth/templates/default.png +0 -0
  193. package/screenshots/truth/wait-for-response/rules-editor.png +0 -0
  194. package/screenshots/truth/wait-for-response/timeout-editor-unchecked.png +0 -0
  195. package/screenshots/truth/wait-for-response/timeout-editor.png +0 -0
  196. package/scripts/dev-data-sync.mjs +182 -0
  197. package/src/display/Chat.ts +6 -4
  198. package/src/events.ts +6 -6
  199. package/src/flow/CanvasNode.ts +89 -79
  200. package/src/flow/Editor.ts +1 -0
  201. package/src/flow/NodeEditor.ts +55 -3
  202. package/src/flow/actions/add_contact_urn.ts +1 -1
  203. package/src/flow/actions/set_contact_channel.ts +1 -1
  204. package/src/flow/actions/set_contact_field.ts +2 -1
  205. package/src/flow/actions/set_contact_language.ts +3 -1
  206. package/src/flow/actions/set_contact_name.ts +1 -1
  207. package/src/flow/actions/set_contact_status.ts +18 -18
  208. package/src/flow/actions/set_run_result.ts +1 -1
  209. package/src/flow/nodes/split_by_llm.ts +14 -13
  210. package/src/flow/nodes/wait_for_response.ts +717 -5
  211. package/src/flow/operators.ts +215 -0
  212. package/src/flow/types.ts +10 -2
  213. package/src/form/ArrayEditor.ts +117 -37
  214. package/src/form/Checkbox.ts +12 -0
  215. package/src/form/FieldRenderer.ts +24 -3
  216. package/src/form/TextInput.ts +19 -1
  217. package/src/form/select/Select.ts +7 -0
  218. package/src/interfaces.ts +1 -1
  219. package/src/layout/Dialog.ts +4 -4
  220. package/src/list/RunList.ts +2 -2
  221. package/src/live/ContactChat.ts +128 -67
  222. package/src/live/ContactDetails.ts +7 -0
  223. package/src/live/ContactNameFetch.ts +1 -1
  224. package/src/webchat/index.ts +0 -16
  225. package/static/api/labels.json +6 -1
  226. package/test/NodeHelper.ts +38 -40
  227. package/test/nodes/split_by_llm.test.ts +43 -32
  228. package/test/nodes/split_by_llm_categorize.test.ts +130 -120
  229. package/test/nodes/split_by_random.test.ts +136 -128
  230. package/test/nodes/wait_for_digits.test.ts +147 -127
  231. package/test/nodes/wait_for_response.test.ts +657 -104
  232. package/test/temba-checkbox.test.ts +36 -32
  233. package/test/temba-contact-chat.test.ts +2 -1
  234. package/test/temba-dropdown.test.ts +0 -12
  235. package/test/temba-flow-editor-node.test.ts +11 -4
  236. package/test/temba-integration-markdown.test.ts +16 -17
  237. package/test/temba-node-editor.test.ts +5 -43
  238. package/test/temba-run-list.test.ts +2 -2
  239. package/test/utils.test.ts +2 -1
  240. package/test-assets/contacts/history.json +4 -7
  241. package/test-assets/list/runs.json +8 -8
  242. package/web-dev-mock.mjs +86 -30
  243. package/web-dev-server.config.mjs +272 -31
  244. package/screenshots/truth/dropdown/bottom-edge-collision.png +0 -0
  245. package/screenshots/truth/dropdown/right-edge-collision.png +0 -0
  246. package/screenshots/truth/editor/send_msg.png +0 -0
  247. package/screenshots/truth/editor/set_contact_language.png +0 -0
  248. package/screenshots/truth/editor/set_contact_name.png +0 -0
  249. package/screenshots/truth/editor/set_run_result.png +0 -0
  250. package/screenshots/truth/integration/checkbox-markdown-errors.png +0 -0
@@ -37,7 +37,6 @@ import {
37
37
  URNsChangedEvent
38
38
  } from '../events';
39
39
  import { Chat, ChatEvent, MessageType } from '../display/Chat';
40
- import { getUserDisplay } from '../webchat';
41
40
  import { DEFAULT_AVATAR } from '../webchat/assets';
42
41
  import { UserSelect } from '../form/select/UserSelect';
43
42
  import { Select } from '../form/select/Select';
@@ -73,7 +72,7 @@ export enum Events {
73
72
  OPTIN_STOPPED = 'optin_stopped',
74
73
  RUN_ENDED = 'run_ended',
75
74
  RUN_STARTED = 'run_started',
76
- TICKET_ASSIGNED = 'ticket_assigned',
75
+ TICKET_ASSIGNEE_CHANGED = 'ticket_assignee_changed',
77
76
  TICKET_CLOSED = 'ticket_closed',
78
77
  TICKET_NOTE_ADDED = 'ticket_note_added',
79
78
  TICKET_OPENED = 'ticket_opened',
@@ -81,32 +80,43 @@ export enum Events {
81
80
  TICKET_TOPIC_CHANGED = 'ticket_topic_changed',
82
81
 
83
82
  // deprecated
84
- CHANNEL_EVENT = 'channel_event'
83
+ CHANNEL_EVENT = 'channel_event',
84
+ TICKET_ASSIGNED = 'ticket_assigned'
85
85
  }
86
86
 
87
- const renderInfoList = (singular: string, plural: string, items: any[]) => {
87
+ const renderInfoList = (
88
+ singular: string,
89
+ plural: string,
90
+ items: any[]
91
+ ): TemplateResult => {
88
92
  if (items.length === 1) {
89
- return `${singular} **${items[0].name}**`;
93
+ return html`<div>${singular} <strong>${items[0].name}</strong></div>`;
90
94
  } else {
91
- const list = items.map((item) => `**${item.name}**`);
95
+ const list = items.map((item) => item.name);
92
96
  if (list.length === 2) {
93
- return `${plural} ${list.join(' and ')}`;
97
+ return html`<div>
98
+ ${plural} <strong>${list[0]}</strong> and <strong>${list[1]}</strong>
99
+ </div>`;
94
100
  } else {
95
101
  const last = list.pop();
96
- return `${plural} ${list.join(', ')}, and ${last}`;
102
+ const middle = list.map(
103
+ (name, index) =>
104
+ html`<strong>${name}</strong>${index < list.length - 1 ? ', ' : ''}`
105
+ );
106
+ return html`<div>${plural} ${middle}, and <strong>${last}</strong></div>`;
97
107
  }
98
108
  }
99
109
  };
100
110
 
101
- const renderChannelEvent = (event: ChannelEvent): string => {
111
+ const renderChannelEvent = (event: ChannelEvent): TemplateResult => {
102
112
  if (event.channel_event_type === 'welcome_message') {
103
- return 'Welcome message sent';
113
+ return html`<div>Welcome message sent</div>`;
104
114
  } else if (event.event.type === 'stop_contact') {
105
- return 'Stopped';
115
+ return html`<div>Stopped</div>`;
106
116
  }
107
117
  };
108
118
 
109
- const renderRunEvent = (event: RunEvent): string => {
119
+ const renderRunEvent = (event: RunEvent): TemplateResult => {
110
120
  let verb = 'Started';
111
121
  if (event.type === Events.RUN_ENDED) {
112
122
  if (event.status === 'completed') {
@@ -118,57 +128,98 @@ const renderRunEvent = (event: RunEvent): string => {
118
128
  }
119
129
  }
120
130
 
121
- return `${verb} [**${event.flow.name}**](/flow/editor/${event.flow.uuid}/)`;
131
+ return html`<div>
132
+ ${verb}
133
+ <a href="/flow/editor/${event.flow.uuid}/"
134
+ ><strong>${event.flow.name}</strong></a
135
+ >
136
+ </div>`;
122
137
  };
123
138
 
124
- const renderChatStartedEvent = (event: ChatStartedEvent): string => {
139
+ const renderChatStartedEvent = (event: ChatStartedEvent): TemplateResult => {
125
140
  if (event.params) {
126
- return `Chat referral`;
141
+ return html`<div>Chat referral</div>`;
127
142
  } else {
128
- return `Chat started`;
143
+ return html`<div>Chat started</div>`;
129
144
  }
130
145
  };
131
146
 
132
- const renderUpdateEvent = (event: UpdateFieldEvent): string => {
147
+ const renderUpdateEvent = (event: UpdateFieldEvent): TemplateResult => {
133
148
  return event.value
134
- ? `Updated **${event.field.name}** to **${event.value.text}**`
135
- : `Cleared **${event.field.name}**`;
149
+ ? html`<div>
150
+ Updated <strong>${event.field.name}</strong> to
151
+ <strong>${event.value.text}</strong>
152
+ </div>`
153
+ : html`<div>Cleared <strong>${event.field.name}</strong></div>`;
136
154
  };
137
155
 
138
- const renderNameChanged = (event: NameChangedEvent): string => {
139
- return `Updated **Contact Name** to **${event.name}**`;
156
+ const renderNameChanged = (event: NameChangedEvent): TemplateResult => {
157
+ return html`<div>
158
+ Updated <strong>name</strong> to <strong>${event.name}</strong>
159
+ </div>`;
140
160
  };
141
161
 
142
- const renderContactURNsChanged = (event: URNsChangedEvent): string => {
143
- return `Updated **URNs** to ${oxfordFn(
144
- event.urns,
145
- (urn: string) => `**${urn.split(':')[1].split('?')[0]}**`
146
- )}`;
162
+ const renderContactURNsChanged = (event: URNsChangedEvent): TemplateResult => {
163
+ return html`<div>
164
+ Updated <strong>URNs</strong> to
165
+ ${oxfordFn(
166
+ event.urns,
167
+ (urn: string) => html`<strong>${urn.split(':')[1].split('?')[0]}</strong>`
168
+ )}
169
+ </div>`;
147
170
  };
148
171
 
149
172
  export const renderTicketAction = (
150
173
  event: TicketEvent,
151
174
  action: string
152
- ): string => {
153
- if (event.created_by) {
154
- return `**${getUserDisplay(
155
- event.created_by
156
- )}** ${action} a **[ticket](/ticket/all/closed/${event.ticket.uuid}/)**`;
175
+ ): TemplateResult => {
176
+ const ticketUUID = event.ticket?.uuid || event.ticket_uuid;
177
+
178
+ if (event._user) {
179
+ return html`<div>
180
+ <strong>${event._user.name}</strong> ${action} a
181
+ <strong><a href="/ticket/all/closed/${ticketUUID}/">ticket</a></strong>
182
+ </div>`;
157
183
  }
158
- return `A **[ticket](/ticket/all/closed/${event.ticket.uuid}/)** was **${action}**`;
184
+ return html`<div>
185
+ A
186
+ <strong><a href="/ticket/all/closed/${ticketUUID}/">ticket</a></strong> was
187
+ <strong>${action}</strong>
188
+ </div>`;
159
189
  };
160
190
 
161
- export const renderTicketAssigned = (event: TicketEvent): string => {
162
- return event.assignee
163
- ? event.assignee.id === event.created_by.id
164
- ? `**${getDisplayName(event.created_by)}** took this ticket`
165
- : `${getDisplayName(
166
- event.created_by
167
- )} assigned this ticket to **${getDisplayName(event.assignee)}**`
168
- : `**${getDisplayName(event.created_by)}** unassigned this ticket`;
191
+ export const renderTicketAssigneeChanged = (
192
+ event: TicketEvent
193
+ ): TemplateResult => {
194
+ if (event._user) {
195
+ if (event.assignee) {
196
+ return html`<div>
197
+ <strong>${event._user.name}</strong> assigned this ticket to
198
+ <strong>${event.assignee.name}</strong>
199
+ </div>`;
200
+ } else {
201
+ return html`<div>
202
+ <strong>${event._user.name}</strong> unassigned this ticket
203
+ </div>`;
204
+ }
205
+ } else {
206
+ if (event.assignee) {
207
+ return html`<div>
208
+ This ticket was assigned to <strong>${event.assignee.name}</strong>
209
+ </div>`;
210
+ } else {
211
+ return html`<div>This ticket was unassigned</div>`;
212
+ }
213
+ }
169
214
  };
170
215
 
171
- export const renderContactGroupsEvent = (event: ContactGroupsEvent): string => {
216
+ export const renderTicketOpened = (event: TicketEvent): TemplateResult => {
217
+ return html`<div>${event.ticket.topic.name} ticket was opened</div>`;
218
+ };
219
+
220
+ export const renderContactGroupsEvent = (
221
+ event: ContactGroupsEvent
222
+ ): TemplateResult => {
172
223
  const groupsEvent = event as ContactGroupsEvent;
173
224
  if (groupsEvent.groups_added) {
174
225
  return renderInfoList(
@@ -185,48 +236,50 @@ export const renderContactGroupsEvent = (event: ContactGroupsEvent): string => {
185
236
  }
186
237
  };
187
238
 
188
- export const renderTicketOpened = (event: TicketEvent): string => {
189
- return `${event.ticket.topic.name} ticket was opened`;
190
- };
191
-
192
239
  export const renderAirtimeTransferredEvent = (
193
240
  event: AirtimeTransferredEvent
194
- ): string => {
241
+ ): TemplateResult => {
195
242
  if (parseFloat(event.amount) === 0) {
196
- return `Airtime transfer failed`;
243
+ return html`<div>Airtime transfer failed</div>`;
197
244
  }
198
- return `Transferred **${event.amount}** ${event.currency} of airtime`;
245
+ return html`<div>
246
+ Transferred <strong>${event.amount}</strong> ${event.currency} of airtime
247
+ </div>`;
199
248
  };
200
249
 
201
250
  export const renderContactLanguageChangedEvent = (
202
251
  event: ContactLanguageChangedEvent
203
- ): string => {
204
- return `Language updated to **${event.language}**`;
252
+ ): TemplateResult => {
253
+ return html`<div>
254
+ Language updated to <strong>${event.language}</strong>
255
+ </div>`;
205
256
  };
206
257
 
207
258
  export const renderContactStatusChangedEvent = (
208
259
  event: ContactStatusChangedEvent
209
- ): string => {
210
- return `Status updated to **${event.status}**`;
260
+ ): TemplateResult => {
261
+ return html`<div>Status updated to <strong>${event.status}</strong></div>`;
211
262
  };
212
263
 
213
- export const renderCallEvent = (event: CallEvent): string => {
264
+ export const renderCallEvent = (event: CallEvent): TemplateResult => {
214
265
  if (event.type === Events.CALL_CREATED) {
215
- return `Call started`;
266
+ return html`<div>Call started</div>`;
216
267
  } else if (event.type === Events.CALL_MISSED) {
217
- return `Call missed`;
268
+ return html`<div>Call missed</div>`;
218
269
  } else if (event.type === Events.CALL_RECEIVED) {
219
- return `Call answered`;
270
+ return html`<div>Call answered</div>`;
220
271
  }
221
272
  };
222
273
 
223
- export const renderOptInEvent = (event: OptInEvent): string => {
274
+ export const renderOptInEvent = (event: OptInEvent): TemplateResult => {
224
275
  if (event.type === Events.OPTIN_REQUESTED) {
225
- return `Requested opt-in for **${event.optin.name}**`;
276
+ return html`<div>
277
+ Requested opt-in for <strong>${event.optin.name}</strong>
278
+ </div>`;
226
279
  } else if (event.type === Events.OPTIN_STARTED) {
227
- return `Opted in to **${event.optin.name}**`;
280
+ return html`<div>Opted in to <strong>${event.optin.name}</strong></div>`;
228
281
  } else if (event.type === Events.OPTIN_STOPPED) {
229
- return `Opted out of **${event.optin.name}**`;
282
+ return html`<div>Opted out of <strong>${event.optin.name}</strong></div>`;
230
283
  }
231
284
  };
232
285
 
@@ -697,10 +750,10 @@ export class ContactChat extends ContactStoreElement {
697
750
  text: renderRunEvent(event as RunEvent)
698
751
  };
699
752
  break;
700
- case Events.TICKET_ASSIGNED:
753
+ case Events.TICKET_ASSIGNEE_CHANGED:
701
754
  message = {
702
755
  type: MessageType.Inline,
703
- text: renderTicketAssigned(event as TicketEvent)
756
+ text: renderTicketAssigneeChanged(event as TicketEvent)
704
757
  };
705
758
  break;
706
759
  case Events.TICKET_CLOSED:
@@ -724,7 +777,10 @@ export class ContactChat extends ContactStoreElement {
724
777
  case Events.TICKET_TOPIC_CHANGED:
725
778
  message = {
726
779
  type: MessageType.Inline,
727
- text: `Topic changed to **${(event as TicketEvent).topic.name}**`
780
+ text: html`<div>
781
+ Topic changed to
782
+ <strong>${(event as TicketEvent).topic.name}</strong>
783
+ </div>`
728
784
  };
729
785
  break;
730
786
  case Events.CHANNEL_EVENT: // deprecated
@@ -741,23 +797,28 @@ export class ContactChat extends ContactStoreElement {
741
797
  console.error('Unknown event type', event);
742
798
  }
743
799
 
800
+ if (!message.id) {
801
+ message.id = event.uuid || event.type + '@' + event.created_on;
802
+ }
803
+
744
804
  return message;
745
805
  }
746
806
 
747
807
  private getUserForEvent(event: MsgEvent | TicketEvent) {
748
- let user = null;
749
808
  if (event.type === 'msg_received') {
750
- user = {
809
+ return {
751
810
  name: this.currentContact.name
752
811
  };
812
+ } else if (event._user) {
813
+ return event._user;
753
814
  } else if (event.created_by) {
754
- user = {
815
+ return {
755
816
  email: event.created_by.email,
756
817
  name: `${event.created_by.first_name} ${event.created_by.last_name}`.trim(),
757
818
  avatar: event.created_by.avatar
758
819
  };
759
820
  }
760
- return user;
821
+ return null;
761
822
  }
762
823
 
763
824
  private createMessages(page: ContactHistoryPage): ChatEvent[] {
@@ -94,6 +94,13 @@ export class ContactDetails extends ContactStoreElement {
94
94
  disabled
95
95
  ></temba-contact-field>`;
96
96
  })}
97
+ ${this.data.ref
98
+ ? html`<temba-contact-field
99
+ name="Ref"
100
+ value=${this.data.ref}
101
+ disabled
102
+ ></temba-contact-field>`
103
+ : null}
97
104
 
98
105
  <temba-contact-field
99
106
  name="Status"
@@ -22,7 +22,7 @@ export class ContactNameFetch extends ContactStoreElement {
22
22
  public render(): TemplateResult {
23
23
  if (this.data) {
24
24
  return html` <temba-contact-name
25
- name=${this.data.name || this.data.anon_display}
25
+ name=${this.data.name || this.data.ref}
26
26
  urn=${this.data.urns.length > 0 ? this.data.urns[0] : null}
27
27
  ></temba-contact-name>
28
28
  <slot></slot>`;
@@ -1,5 +1,3 @@
1
- import { User } from '../interfaces';
2
-
3
1
  export const SVG_FINGERPRINT = 'febafb41c2fd60efa2bdaead993c7087';
4
2
 
5
3
  // webchat spritesheet
@@ -12,17 +10,3 @@ export enum WebChatIcon {
12
10
  attachment_location = 'marker-pin-01',
13
11
  attachment_video = 'video-recorder'
14
12
  }
15
-
16
- export const getUserDisplay = (user: User) => {
17
- if (user) {
18
- if (user.first_name && user.last_name) {
19
- return `${user.first_name} ${user.last_name}`;
20
- }
21
-
22
- if (user.first_name) {
23
- return user.first_name;
24
- }
25
-
26
- return user.email;
27
- }
28
- };
@@ -26,6 +26,11 @@
26
26
  "uuid": "a3f2990b-4096-452e-a586-dc06a9434dde",
27
27
  "name": "Feedback",
28
28
  "count": 42
29
+ },
30
+ {
31
+ "uuid": "6dc62526-c334-4869-84fa-8b2d73b4cdfd",
32
+ "name": "This is a label",
33
+ "count": 0
29
34
  }
30
35
  ]
31
- }
36
+ }
@@ -92,46 +92,44 @@ export class NodeTest<T extends Node> {
92
92
  * 3. Simulates save and validates round-trip conversion
93
93
  */
94
94
  async testNode(node: T, nodeUI: any, testName: string) {
95
- it(`${testName}`, async () => {
96
- // Step 1: Render node in flow node
97
- const flowNode = await this.renderNode(node, nodeUI);
98
-
99
- // For execute_actions nodes, check for .body, for router nodes check for .router or .categories
100
- const hasContent =
101
- flowNode.querySelector('.body') ||
102
- flowNode.querySelector('.router') ||
103
- flowNode.querySelector('.categories') ||
104
- flowNode.querySelector('.action') ||
105
- flowNode.textContent?.trim();
106
-
107
- expect(hasContent).to.exist;
108
- await assertScreenshot(
109
- `nodes/${this.nodeName}/render/${testName}`,
110
- getClip(flowNode)
111
- );
112
-
113
- // Step 2: Open node editor
114
- const nodeEditor = await this.openNodeEditor(node, nodeUI);
115
- await this.assertDialogScreenshot(
116
- nodeEditor,
117
- `nodes/${this.nodeName}/editor/${testName}`
118
- );
119
-
120
- // Step 3: Test round-trip conversion (simulates save workflow)
121
- if (this.nodeConfig.toFormData && this.nodeConfig.fromFormData) {
122
- const formData = this.nodeConfig.toFormData(node);
123
- const convertedNode = this.nodeConfig.fromFormData(formData, node) as T;
124
-
125
- // Validate the round trip worked
126
- expect(convertedNode.uuid).to.equal(node.uuid);
127
-
128
- // Validate the converted node has expected structure
129
- expect(convertedNode).to.have.property('actions');
130
- expect(convertedNode).to.have.property('exits');
131
-
132
- expect(convertedNode).to.deep.equal(node);
133
- }
134
- });
95
+ // Step 1: Render node in flow node
96
+ const flowNode = await this.renderNode(node, nodeUI);
97
+
98
+ // For execute_actions nodes, check for .body, for router nodes check for .router or .categories
99
+ const hasContent =
100
+ flowNode.querySelector('.body') ||
101
+ flowNode.querySelector('.router') ||
102
+ flowNode.querySelector('.categories') ||
103
+ flowNode.querySelector('.action') ||
104
+ flowNode.textContent?.trim();
105
+
106
+ expect(hasContent).to.exist;
107
+ await assertScreenshot(
108
+ `nodes/${this.nodeName}/render/${testName}`,
109
+ getClip(flowNode)
110
+ );
111
+
112
+ // Step 2: Open node editor
113
+ const nodeEditor = await this.openNodeEditor(node, nodeUI);
114
+ await this.assertDialogScreenshot(
115
+ nodeEditor,
116
+ `nodes/${this.nodeName}/editor/${testName}`
117
+ );
118
+
119
+ // Step 3: Test round-trip conversion (simulates save workflow)
120
+ if (this.nodeConfig.toFormData && this.nodeConfig.fromFormData) {
121
+ const formData = this.nodeConfig.toFormData(node);
122
+ const convertedNode = this.nodeConfig.fromFormData(formData, node) as T;
123
+
124
+ // Validate the round trip worked
125
+ expect(convertedNode.uuid).to.equal(node.uuid);
126
+
127
+ // Validate the converted node has expected structure
128
+ expect(convertedNode).to.have.property('actions');
129
+ expect(convertedNode).to.have.property('exits');
130
+
131
+ expect(convertedNode).to.deep.equal(node);
132
+ }
135
133
  }
136
134
 
137
135
  /**
@@ -196,37 +196,48 @@ describe('split_by_llm node config', () => {
196
196
  position: { left: 50, top: 50 }
197
197
  };
198
198
 
199
- helper.testNode(
200
- createTestNode({ uuid: 'gpt-4', name: 'GPT 4.1' }, 'Translate to French'),
201
- nodeUI,
202
- 'translation-task'
203
- );
204
-
205
- helper.testNode(
206
- createTestNode(
207
- { uuid: 'gpt-5', name: 'GPT 5' },
208
- 'Analyze the sentiment of the following message and classify it as positive, negative, or neutral. Provide a brief explanation for your classification.'
209
- ),
210
- nodeUI,
211
- 'sentiment-analysis'
212
- );
213
-
214
- helper.testNode(
215
- createTestNode(
216
- { uuid: 'gpt-4', name: 'GPT 4.1' },
217
- 'Summarize the key points from the conversation above in bullet format.'
218
- ),
219
- nodeUI,
220
- 'summarization'
221
- );
222
-
223
- helper.testNode(
224
- createTestNode(
225
- { uuid: 'gpt-5', name: 'GPT 5' },
226
- 'Extract any contact information (phone numbers, email addresses) from the text and format them as a JSON object.'
227
- ),
228
- nodeUI,
229
- 'information-extraction'
230
- );
199
+ it('renders translation task', async () => {
200
+ await helper.testNode(
201
+ createTestNode(
202
+ { uuid: 'gpt-4', name: 'GPT 4.1' },
203
+ 'Translate to French'
204
+ ),
205
+ nodeUI,
206
+ 'translation-task'
207
+ );
208
+ });
209
+
210
+ it('renders sentiment analysis', async () => {
211
+ await helper.testNode(
212
+ createTestNode(
213
+ { uuid: 'gpt-5', name: 'GPT 5' },
214
+ 'Analyze the sentiment of the following message and classify it as positive, negative, or neutral. Provide a brief explanation for your classification.'
215
+ ),
216
+ nodeUI,
217
+ 'sentiment-analysis'
218
+ );
219
+ });
220
+
221
+ it('renders summarization', async () => {
222
+ await helper.testNode(
223
+ createTestNode(
224
+ { uuid: 'gpt-4', name: 'GPT 4.1' },
225
+ 'Summarize the key points from the conversation above in bullet format.'
226
+ ),
227
+ nodeUI,
228
+ 'summarization'
229
+ );
230
+ });
231
+
232
+ it('renders information extraction', async () => {
233
+ await helper.testNode(
234
+ createTestNode(
235
+ { uuid: 'gpt-5', name: 'GPT 5' },
236
+ 'Extract any contact information (phone numbers, email addresses) from the text and format them as a JSON object.'
237
+ ),
238
+ nodeUI,
239
+ 'information-extraction'
240
+ );
241
+ });
231
242
  });
232
243
  });