@nyaruka/temba-components 0.130.1 → 0.130.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (246) hide show
  1. package/CHANGELOG.md +28 -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 +764 -628
  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 +114 -34
  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/test/NodeHelper.js +25 -27
  59. package/out-tsc/test/NodeHelper.js.map +1 -1
  60. package/out-tsc/test/nodes/split_by_llm.test.js +12 -4
  61. package/out-tsc/test/nodes/split_by_llm.test.js.map +1 -1
  62. package/out-tsc/test/nodes/split_by_llm_categorize.test.js +101 -91
  63. package/out-tsc/test/nodes/split_by_llm_categorize.test.js.map +1 -1
  64. package/out-tsc/test/nodes/split_by_random.test.js +120 -112
  65. package/out-tsc/test/nodes/split_by_random.test.js.map +1 -1
  66. package/out-tsc/test/nodes/wait_for_digits.test.js +131 -111
  67. package/out-tsc/test/nodes/wait_for_digits.test.js.map +1 -1
  68. package/out-tsc/test/nodes/wait_for_response.test.js +549 -85
  69. package/out-tsc/test/nodes/wait_for_response.test.js.map +1 -1
  70. package/out-tsc/test/temba-checkbox.test.js +32 -32
  71. package/out-tsc/test/temba-checkbox.test.js.map +1 -1
  72. package/out-tsc/test/temba-contact-chat.test.js +2 -1
  73. package/out-tsc/test/temba-contact-chat.test.js.map +1 -1
  74. package/out-tsc/test/temba-dropdown.test.js +0 -4
  75. package/out-tsc/test/temba-dropdown.test.js.map +1 -1
  76. package/out-tsc/test/temba-flow-editor-node.test.js +9 -4
  77. package/out-tsc/test/temba-flow-editor-node.test.js.map +1 -1
  78. package/out-tsc/test/temba-integration-markdown.test.js +13 -15
  79. package/out-tsc/test/temba-integration-markdown.test.js.map +1 -1
  80. package/out-tsc/test/temba-node-editor.test.js +5 -38
  81. package/out-tsc/test/temba-node-editor.test.js.map +1 -1
  82. package/out-tsc/test/temba-run-list.test.js +2 -2
  83. package/out-tsc/test/temba-run-list.test.js.map +1 -1
  84. package/out-tsc/test/utils.test.js +2 -1
  85. package/out-tsc/test/utils.test.js.map +1 -1
  86. package/package.json +6 -2
  87. package/screenshots/truth/actions/add_contact_groups/editor/descriptive-group-names.png +0 -0
  88. package/screenshots/truth/actions/add_contact_groups/editor/long-group-names.png +0 -0
  89. package/screenshots/truth/actions/add_contact_groups/editor/many-groups.png +0 -0
  90. package/screenshots/truth/actions/add_contact_groups/editor/multiple-groups.png +0 -0
  91. package/screenshots/truth/actions/add_contact_groups/editor/single-group.png +0 -0
  92. package/screenshots/truth/actions/add_contact_groups/render/descriptive-group-names.png +0 -0
  93. package/screenshots/truth/actions/add_contact_groups/render/long-group-names.png +0 -0
  94. package/screenshots/truth/actions/add_contact_groups/render/many-groups.png +0 -0
  95. package/screenshots/truth/actions/add_contact_groups/render/multiple-groups.png +0 -0
  96. package/screenshots/truth/actions/add_contact_groups/render/single-group.png +0 -0
  97. package/screenshots/truth/actions/remove_contact_groups/editor/cleanup-groups.png +0 -0
  98. package/screenshots/truth/actions/remove_contact_groups/editor/long-descriptive-group-names.png +0 -0
  99. package/screenshots/truth/actions/remove_contact_groups/editor/many-groups.png +0 -0
  100. package/screenshots/truth/actions/remove_contact_groups/editor/multiple-groups.png +0 -0
  101. package/screenshots/truth/actions/remove_contact_groups/editor/remove-from-all-groups.png +0 -0
  102. package/screenshots/truth/actions/remove_contact_groups/editor/single-group.png +0 -0
  103. package/screenshots/truth/actions/remove_contact_groups/render/cleanup-groups.png +0 -0
  104. package/screenshots/truth/actions/remove_contact_groups/render/long-descriptive-group-names.png +0 -0
  105. package/screenshots/truth/actions/remove_contact_groups/render/many-groups.png +0 -0
  106. package/screenshots/truth/actions/remove_contact_groups/render/multiple-groups.png +0 -0
  107. package/screenshots/truth/actions/remove_contact_groups/render/remove-from-all-groups.png +0 -0
  108. package/screenshots/truth/actions/remove_contact_groups/render/single-group.png +0 -0
  109. package/screenshots/truth/actions/send_email/editor/complex-business-email.png +0 -0
  110. package/screenshots/truth/actions/send_email/editor/empty-body.png +0 -0
  111. package/screenshots/truth/actions/send_email/editor/empty-subject.png +0 -0
  112. package/screenshots/truth/actions/send_email/editor/long-subject.png +0 -0
  113. package/screenshots/truth/actions/send_email/editor/multiline-body.png +0 -0
  114. package/screenshots/truth/actions/send_email/editor/multiple-recipients.png +0 -0
  115. package/screenshots/truth/actions/send_email/editor/simple-email.png +0 -0
  116. package/screenshots/truth/actions/send_email/editor/with-expressions.png +0 -0
  117. package/screenshots/truth/actions/send_email/render/complex-business-email.png +0 -0
  118. package/screenshots/truth/actions/send_email/render/empty-body.png +0 -0
  119. package/screenshots/truth/actions/send_email/render/empty-subject.png +0 -0
  120. package/screenshots/truth/actions/send_email/render/long-subject.png +0 -0
  121. package/screenshots/truth/actions/send_email/render/multiline-body.png +0 -0
  122. package/screenshots/truth/actions/send_email/render/multiple-recipients.png +0 -0
  123. package/screenshots/truth/actions/send_email/render/simple-email.png +0 -0
  124. package/screenshots/truth/actions/send_email/render/with-expressions.png +0 -0
  125. package/screenshots/truth/actions/send_msg/editor/long-quick-replies.png +0 -0
  126. package/screenshots/truth/actions/send_msg/editor/multiline-text-with-replies.png +0 -0
  127. package/screenshots/truth/actions/send_msg/editor/simple-text.png +0 -0
  128. package/screenshots/truth/actions/send_msg/editor/text-with-linebreaks.png +0 -0
  129. package/screenshots/truth/actions/send_msg/editor/text-with-many-quick-replies.png +0 -0
  130. package/screenshots/truth/actions/send_msg/editor/text-with-quick-replies.png +0 -0
  131. package/screenshots/truth/actions/send_msg/editor/text-without-quick-replies.png +0 -0
  132. package/screenshots/truth/actions/send_msg/render/long-quick-replies.png +0 -0
  133. package/screenshots/truth/actions/send_msg/render/multiline-text-with-replies.png +0 -0
  134. package/screenshots/truth/actions/send_msg/render/simple-text.png +0 -0
  135. package/screenshots/truth/actions/send_msg/render/text-with-linebreaks.png +0 -0
  136. package/screenshots/truth/actions/send_msg/render/text-with-many-quick-replies.png +0 -0
  137. package/screenshots/truth/actions/send_msg/render/text-with-quick-replies.png +0 -0
  138. package/screenshots/truth/actions/send_msg/render/text-without-quick-replies.png +0 -0
  139. package/screenshots/truth/checkbox/checkbox-label-background-hover.png +0 -0
  140. package/screenshots/truth/checkbox/checkbox-whitespace-label-no-background-hover.png +0 -0
  141. package/screenshots/truth/checkbox/checkbox-with-help-text.png +0 -0
  142. package/screenshots/truth/checkbox/checked.png +0 -0
  143. package/screenshots/truth/checkbox/default.png +0 -0
  144. package/screenshots/truth/editor/wait.png +0 -0
  145. package/screenshots/truth/integration/textinput-markdown-errors.png +0 -0
  146. package/screenshots/truth/lightbox/img-zoomed.png +0 -0
  147. package/screenshots/truth/nodes/split_by_llm/editor/information-extraction.png +0 -0
  148. package/screenshots/truth/nodes/split_by_llm/editor/sentiment-analysis.png +0 -0
  149. package/screenshots/truth/nodes/split_by_llm/editor/summarization.png +0 -0
  150. package/screenshots/truth/nodes/split_by_llm/editor/translation-task.png +0 -0
  151. package/screenshots/truth/nodes/split_by_llm/render/information-extraction.png +0 -0
  152. package/screenshots/truth/nodes/split_by_llm/render/sentiment-analysis.png +0 -0
  153. package/screenshots/truth/nodes/split_by_llm/render/summarization.png +0 -0
  154. package/screenshots/truth/nodes/split_by_llm/render/translation-task.png +0 -0
  155. package/screenshots/truth/nodes/split_by_llm_categorize/editor/basic-categorization.png +0 -0
  156. package/screenshots/truth/nodes/split_by_llm_categorize/editor/custom-input-and-result-name.png +0 -0
  157. package/screenshots/truth/nodes/split_by_llm_categorize/editor/feedback-categorization.png +0 -0
  158. package/screenshots/truth/nodes/split_by_llm_categorize/editor/many-categories.png +0 -0
  159. package/screenshots/truth/nodes/split_by_llm_categorize/editor/minimal-categories.png +0 -0
  160. package/screenshots/truth/nodes/split_by_llm_categorize/render/basic-categorization.png +0 -0
  161. package/screenshots/truth/nodes/split_by_llm_categorize/render/custom-input-and-result-name.png +0 -0
  162. package/screenshots/truth/nodes/split_by_llm_categorize/render/feedback-categorization.png +0 -0
  163. package/screenshots/truth/nodes/split_by_llm_categorize/render/many-categories.png +0 -0
  164. package/screenshots/truth/nodes/split_by_llm_categorize/render/minimal-categories.png +0 -0
  165. package/screenshots/truth/nodes/split_by_random/editor/ab-test-multiple-variants.png +0 -0
  166. package/screenshots/truth/nodes/split_by_random/editor/sampling-split.png +0 -0
  167. package/screenshots/truth/nodes/split_by_random/editor/three-way-split.png +0 -0
  168. package/screenshots/truth/nodes/split_by_random/editor/two-bucket-split.png +0 -0
  169. package/screenshots/truth/nodes/split_by_random/render/ab-test-multiple-variants.png +0 -0
  170. package/screenshots/truth/nodes/split_by_random/render/sampling-split.png +0 -0
  171. package/screenshots/truth/nodes/split_by_random/render/three-way-split.png +0 -0
  172. package/screenshots/truth/nodes/split_by_random/render/two-bucket-split.png +0 -0
  173. package/screenshots/truth/nodes/wait_for_digits/editor/basic-digits-wait.png +0 -0
  174. package/screenshots/truth/nodes/wait_for_digits/editor/phone-number-collection.png +0 -0
  175. package/screenshots/truth/nodes/wait_for_digits/editor/single-digit-with-timeout.png +0 -0
  176. package/screenshots/truth/nodes/wait_for_digits/editor/verification-code.png +0 -0
  177. package/screenshots/truth/nodes/wait_for_digits/render/basic-digits-wait.png +0 -0
  178. package/screenshots/truth/nodes/wait_for_digits/render/phone-number-collection.png +0 -0
  179. package/screenshots/truth/nodes/wait_for_digits/render/single-digit-with-timeout.png +0 -0
  180. package/screenshots/truth/nodes/wait_for_digits/render/verification-code.png +0 -0
  181. package/screenshots/truth/nodes/wait_for_response/editor/basic-wait.png +0 -0
  182. package/screenshots/truth/nodes/wait_for_response/editor/custom-result-name.png +0 -0
  183. package/screenshots/truth/nodes/wait_for_response/editor/no-timeout.png +0 -0
  184. package/screenshots/truth/nodes/wait_for_response/editor/short-timeout.png +0 -0
  185. package/screenshots/truth/nodes/wait_for_response/render/basic-wait.png +0 -0
  186. package/screenshots/truth/nodes/wait_for_response/render/custom-result-name.png +0 -0
  187. package/screenshots/truth/nodes/wait_for_response/render/no-timeout.png +0 -0
  188. package/screenshots/truth/nodes/wait_for_response/render/short-timeout.png +0 -0
  189. package/screenshots/truth/run-list/basic.png +0 -0
  190. package/screenshots/truth/templates/default.png +0 -0
  191. package/screenshots/truth/wait-for-response/rules-editor.png +0 -0
  192. package/screenshots/truth/wait-for-response/timeout-editor-unchecked.png +0 -0
  193. package/screenshots/truth/wait-for-response/timeout-editor.png +0 -0
  194. package/scripts/dev-data-sync.mjs +182 -0
  195. package/src/display/Chat.ts +6 -4
  196. package/src/events.ts +6 -5
  197. package/src/flow/CanvasNode.ts +89 -79
  198. package/src/flow/Editor.ts +1 -0
  199. package/src/flow/NodeEditor.ts +55 -3
  200. package/src/flow/actions/add_contact_urn.ts +1 -1
  201. package/src/flow/actions/set_contact_channel.ts +1 -1
  202. package/src/flow/actions/set_contact_field.ts +2 -1
  203. package/src/flow/actions/set_contact_language.ts +3 -1
  204. package/src/flow/actions/set_contact_name.ts +1 -1
  205. package/src/flow/actions/set_contact_status.ts +18 -18
  206. package/src/flow/actions/set_run_result.ts +1 -1
  207. package/src/flow/nodes/split_by_llm.ts +14 -13
  208. package/src/flow/nodes/wait_for_response.ts +717 -5
  209. package/src/flow/operators.ts +215 -0
  210. package/src/flow/types.ts +10 -2
  211. package/src/form/ArrayEditor.ts +117 -37
  212. package/src/form/Checkbox.ts +12 -0
  213. package/src/form/FieldRenderer.ts +24 -3
  214. package/src/form/TextInput.ts +19 -1
  215. package/src/form/select/Select.ts +7 -0
  216. package/src/interfaces.ts +1 -1
  217. package/src/layout/Dialog.ts +4 -4
  218. package/src/list/RunList.ts +2 -2
  219. package/src/live/ContactChat.ts +144 -58
  220. package/src/live/ContactDetails.ts +7 -0
  221. package/src/live/ContactNameFetch.ts +1 -1
  222. package/static/api/labels.json +6 -1
  223. package/test/NodeHelper.ts +38 -40
  224. package/test/nodes/split_by_llm.test.ts +43 -32
  225. package/test/nodes/split_by_llm_categorize.test.ts +130 -120
  226. package/test/nodes/split_by_random.test.ts +136 -128
  227. package/test/nodes/wait_for_digits.test.ts +147 -127
  228. package/test/nodes/wait_for_response.test.ts +657 -104
  229. package/test/temba-checkbox.test.ts +36 -32
  230. package/test/temba-contact-chat.test.ts +2 -1
  231. package/test/temba-dropdown.test.ts +0 -12
  232. package/test/temba-flow-editor-node.test.ts +11 -4
  233. package/test/temba-integration-markdown.test.ts +16 -17
  234. package/test/temba-node-editor.test.ts +5 -43
  235. package/test/temba-run-list.test.ts +2 -2
  236. package/test/utils.test.ts +2 -1
  237. package/test-assets/list/runs.json +8 -8
  238. package/web-dev-mock.mjs +86 -30
  239. package/web-dev-server.config.mjs +272 -31
  240. package/screenshots/truth/dropdown/bottom-edge-collision.png +0 -0
  241. package/screenshots/truth/dropdown/right-edge-collision.png +0 -0
  242. package/screenshots/truth/editor/send_msg.png +0 -0
  243. package/screenshots/truth/editor/set_contact_language.png +0 -0
  244. package/screenshots/truth/editor/set_contact_name.png +0 -0
  245. package/screenshots/truth/editor/set_run_result.png +0 -0
  246. package/screenshots/truth/integration/checkbox-markdown-errors.png +0 -0
@@ -73,7 +73,7 @@ export enum Events {
73
73
  OPTIN_STOPPED = 'optin_stopped',
74
74
  RUN_ENDED = 'run_ended',
75
75
  RUN_STARTED = 'run_started',
76
- TICKET_ASSIGNED = 'ticket_assigned',
76
+ TICKET_ASSIGNEE_CHANGED = 'ticket_assignee_changed',
77
77
  TICKET_CLOSED = 'ticket_closed',
78
78
  TICKET_NOTE_ADDED = 'ticket_note_added',
79
79
  TICKET_OPENED = 'ticket_opened',
@@ -81,32 +81,43 @@ export enum Events {
81
81
  TICKET_TOPIC_CHANGED = 'ticket_topic_changed',
82
82
 
83
83
  // deprecated
84
- CHANNEL_EVENT = 'channel_event'
84
+ CHANNEL_EVENT = 'channel_event',
85
+ TICKET_ASSIGNED = 'ticket_assigned'
85
86
  }
86
87
 
87
- const renderInfoList = (singular: string, plural: string, items: any[]) => {
88
+ const renderInfoList = (
89
+ singular: string,
90
+ plural: string,
91
+ items: any[]
92
+ ): TemplateResult => {
88
93
  if (items.length === 1) {
89
- return `${singular} **${items[0].name}**`;
94
+ return html`<div>${singular} <strong>${items[0].name}</strong></div>`;
90
95
  } else {
91
- const list = items.map((item) => `**${item.name}**`);
96
+ const list = items.map((item) => item.name);
92
97
  if (list.length === 2) {
93
- return `${plural} ${list.join(' and ')}`;
98
+ return html`<div>
99
+ ${plural} <strong>${list[0]}</strong> and <strong>${list[1]}</strong>
100
+ </div>`;
94
101
  } else {
95
102
  const last = list.pop();
96
- return `${plural} ${list.join(', ')}, and ${last}`;
103
+ const middle = list.map(
104
+ (name, index) =>
105
+ html`<strong>${name}</strong>${index < list.length - 1 ? ', ' : ''}`
106
+ );
107
+ return html`<div>${plural} ${middle}, and <strong>${last}</strong></div>`;
97
108
  }
98
109
  }
99
110
  };
100
111
 
101
- const renderChannelEvent = (event: ChannelEvent): string => {
112
+ const renderChannelEvent = (event: ChannelEvent): TemplateResult => {
102
113
  if (event.channel_event_type === 'welcome_message') {
103
- return 'Welcome message sent';
114
+ return html`<div>Welcome message sent</div>`;
104
115
  } else if (event.event.type === 'stop_contact') {
105
- return 'Stopped';
116
+ return html`<div>Stopped</div>`;
106
117
  }
107
118
  };
108
119
 
109
- const renderRunEvent = (event: RunEvent): string => {
120
+ const renderRunEvent = (event: RunEvent): TemplateResult => {
110
121
  let verb = 'Started';
111
122
  if (event.type === Events.RUN_ENDED) {
112
123
  if (event.status === 'completed') {
@@ -118,57 +129,117 @@ const renderRunEvent = (event: RunEvent): string => {
118
129
  }
119
130
  }
120
131
 
121
- return `${verb} [**${event.flow.name}**](/flow/editor/${event.flow.uuid}/)`;
132
+ return html`<div>
133
+ ${verb}
134
+ <a href="/flow/editor/${event.flow.uuid}/"
135
+ ><strong>${event.flow.name}</strong></a
136
+ >
137
+ </div>`;
122
138
  };
123
139
 
124
- const renderChatStartedEvent = (event: ChatStartedEvent): string => {
140
+ const renderChatStartedEvent = (event: ChatStartedEvent): TemplateResult => {
125
141
  if (event.params) {
126
- return `Chat referral`;
142
+ return html`<div>Chat referral</div>`;
127
143
  } else {
128
- return `Chat started`;
144
+ return html`<div>Chat started</div>`;
129
145
  }
130
146
  };
131
147
 
132
- const renderUpdateEvent = (event: UpdateFieldEvent): string => {
148
+ const renderUpdateEvent = (event: UpdateFieldEvent): TemplateResult => {
133
149
  return event.value
134
- ? `Updated **${event.field.name}** to **${event.value.text}**`
135
- : `Cleared **${event.field.name}**`;
150
+ ? html`<div>
151
+ Updated <strong>${event.field.name}</strong> to
152
+ <strong>${event.value.text}</strong>
153
+ </div>`
154
+ : html`<div>Cleared <strong>${event.field.name}</strong></div>`;
136
155
  };
137
156
 
138
- const renderNameChanged = (event: NameChangedEvent): string => {
139
- return `Updated **Contact Name** to **${event.name}**`;
157
+ const renderNameChanged = (event: NameChangedEvent): TemplateResult => {
158
+ return html`<div>
159
+ Updated <strong>name</strong> to <strong>${event.name}</strong>
160
+ </div>`;
140
161
  };
141
162
 
142
- const renderContactURNsChanged = (event: URNsChangedEvent): string => {
143
- return `Updated **URNs** to ${oxfordFn(
144
- event.urns,
145
- (urn: string) => `**${urn.split(':')[1].split('?')[0]}**`
146
- )}`;
163
+ const renderContactURNsChanged = (event: URNsChangedEvent): TemplateResult => {
164
+ return html`<div>
165
+ Updated <strong>URNs</strong> to
166
+ ${oxfordFn(
167
+ event.urns,
168
+ (urn: string) => html`<strong>${urn.split(':')[1].split('?')[0]}</strong>`
169
+ )}
170
+ </div>`;
147
171
  };
148
172
 
149
173
  export const renderTicketAction = (
150
174
  event: TicketEvent,
151
175
  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}/)**`;
176
+ ): TemplateResult => {
177
+ const ticketUUID = event.ticket?.uuid || event.ticket_uuid;
178
+ const userDisplay = event.created_by
179
+ ? getUserDisplay(event.created_by)
180
+ : event._user?.name;
181
+
182
+ if (userDisplay) {
183
+ return html`<div>
184
+ <strong>${userDisplay}</strong> ${action} a
185
+ <strong><a href="/ticket/all/closed/${ticketUUID}/">ticket</a></strong>
186
+ </div>`;
187
+ }
188
+ return html`<div>
189
+ A
190
+ <strong><a href="/ticket/all/closed/${ticketUUID}/">ticket</a></strong> was
191
+ <strong>${action}</strong>
192
+ </div>`;
193
+ };
194
+
195
+ export const renderTicketAssigneeChanged = (
196
+ event: TicketEvent
197
+ ): TemplateResult => {
198
+ if (event._user) {
199
+ if (event.assignee) {
200
+ return html`<div>
201
+ <strong>${event._user.name}</strong> assigned this ticket to
202
+ <strong>${event.assignee.name}</strong>
203
+ </div>`;
204
+ } else {
205
+ return html`<div>
206
+ <strong>${event._user.name}</strong> unassigned this ticket
207
+ </div>`;
208
+ }
209
+ } else {
210
+ if (event.assignee) {
211
+ return html`<div>
212
+ This ticket was assigned to <strong>${event.assignee.name}</strong>
213
+ </div>`;
214
+ } else {
215
+ return html`<div>This ticket was unassigned</div>`;
216
+ }
157
217
  }
158
- return `A **[ticket](/ticket/all/closed/${event.ticket.uuid}/)** was **${action}**`;
159
218
  };
160
219
 
161
- export const renderTicketAssigned = (event: TicketEvent): string => {
220
+ export const renderTicketAssigned = (event: TicketEvent): TemplateResult => {
162
221
  return event.assignee
163
222
  ? 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`;
223
+ ? html`<div>
224
+ <strong>${getDisplayName(event.created_by)}</strong> took this ticket
225
+ </div>`
226
+ : html`<div>
227
+ ${getDisplayName(event.created_by)} assigned this ticket to
228
+ <strong>${getDisplayName(event.assignee)}</strong>
229
+ </div>`
230
+ : html`<div>
231
+ <strong>${getDisplayName(event.created_by)}</strong> unassigned this
232
+ ticket
233
+ </div>`;
234
+ };
235
+
236
+ export const renderTicketOpened = (event: TicketEvent): TemplateResult => {
237
+ return html`<div>${event.ticket.topic.name} ticket was opened</div>`;
169
238
  };
170
239
 
171
- export const renderContactGroupsEvent = (event: ContactGroupsEvent): string => {
240
+ export const renderContactGroupsEvent = (
241
+ event: ContactGroupsEvent
242
+ ): TemplateResult => {
172
243
  const groupsEvent = event as ContactGroupsEvent;
173
244
  if (groupsEvent.groups_added) {
174
245
  return renderInfoList(
@@ -185,48 +256,50 @@ export const renderContactGroupsEvent = (event: ContactGroupsEvent): string => {
185
256
  }
186
257
  };
187
258
 
188
- export const renderTicketOpened = (event: TicketEvent): string => {
189
- return `${event.ticket.topic.name} ticket was opened`;
190
- };
191
-
192
259
  export const renderAirtimeTransferredEvent = (
193
260
  event: AirtimeTransferredEvent
194
- ): string => {
261
+ ): TemplateResult => {
195
262
  if (parseFloat(event.amount) === 0) {
196
- return `Airtime transfer failed`;
263
+ return html`<div>Airtime transfer failed</div>`;
197
264
  }
198
- return `Transferred **${event.amount}** ${event.currency} of airtime`;
265
+ return html`<div>
266
+ Transferred <strong>${event.amount}</strong> ${event.currency} of airtime
267
+ </div>`;
199
268
  };
200
269
 
201
270
  export const renderContactLanguageChangedEvent = (
202
271
  event: ContactLanguageChangedEvent
203
- ): string => {
204
- return `Language updated to **${event.language}**`;
272
+ ): TemplateResult => {
273
+ return html`<div>
274
+ Language updated to <strong>${event.language}</strong>
275
+ </div>`;
205
276
  };
206
277
 
207
278
  export const renderContactStatusChangedEvent = (
208
279
  event: ContactStatusChangedEvent
209
- ): string => {
210
- return `Status updated to **${event.status}**`;
280
+ ): TemplateResult => {
281
+ return html`<div>Status updated to <strong>${event.status}</strong></div>`;
211
282
  };
212
283
 
213
- export const renderCallEvent = (event: CallEvent): string => {
284
+ export const renderCallEvent = (event: CallEvent): TemplateResult => {
214
285
  if (event.type === Events.CALL_CREATED) {
215
- return `Call started`;
286
+ return html`<div>Call started</div>`;
216
287
  } else if (event.type === Events.CALL_MISSED) {
217
- return `Call missed`;
288
+ return html`<div>Call missed</div>`;
218
289
  } else if (event.type === Events.CALL_RECEIVED) {
219
- return `Call answered`;
290
+ return html`<div>Call answered</div>`;
220
291
  }
221
292
  };
222
293
 
223
- export const renderOptInEvent = (event: OptInEvent): string => {
294
+ export const renderOptInEvent = (event: OptInEvent): TemplateResult => {
224
295
  if (event.type === Events.OPTIN_REQUESTED) {
225
- return `Requested opt-in for **${event.optin.name}**`;
296
+ return html`<div>
297
+ Requested opt-in for <strong>${event.optin.name}</strong>
298
+ </div>`;
226
299
  } else if (event.type === Events.OPTIN_STARTED) {
227
- return `Opted in to **${event.optin.name}**`;
300
+ return html`<div>Opted in to <strong>${event.optin.name}</strong></div>`;
228
301
  } else if (event.type === Events.OPTIN_STOPPED) {
229
- return `Opted out of **${event.optin.name}**`;
302
+ return html`<div>Opted out of <strong>${event.optin.name}</strong></div>`;
230
303
  }
231
304
  };
232
305
 
@@ -703,6 +776,12 @@ export class ContactChat extends ContactStoreElement {
703
776
  text: renderTicketAssigned(event as TicketEvent)
704
777
  };
705
778
  break;
779
+ case Events.TICKET_ASSIGNEE_CHANGED:
780
+ message = {
781
+ type: MessageType.Inline,
782
+ text: renderTicketAssigneeChanged(event as TicketEvent)
783
+ };
784
+ break;
706
785
  case Events.TICKET_CLOSED:
707
786
  message = {
708
787
  type: MessageType.Inline,
@@ -724,7 +803,10 @@ export class ContactChat extends ContactStoreElement {
724
803
  case Events.TICKET_TOPIC_CHANGED:
725
804
  message = {
726
805
  type: MessageType.Inline,
727
- text: `Topic changed to **${(event as TicketEvent).topic.name}**`
806
+ text: html`<div>
807
+ Topic changed to
808
+ <strong>${(event as TicketEvent).topic.name}</strong>
809
+ </div>`
728
810
  };
729
811
  break;
730
812
  case Events.CHANNEL_EVENT: // deprecated
@@ -741,6 +823,10 @@ export class ContactChat extends ContactStoreElement {
741
823
  console.error('Unknown event type', event);
742
824
  }
743
825
 
826
+ if (!message.id) {
827
+ message.id = event.uuid || event.type + '@' + event.created_on;
828
+ }
829
+
744
830
  return message;
745
831
  }
746
832
 
@@ -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>`;
@@ -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
  });