@nyaruka/temba-components 0.131.2 → 0.131.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 (223) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/demo/components/floating-tabs/example.html +400 -0
  3. package/demo/components/flow/index.html +1 -1
  4. package/demo/data/flows/sample-flow.json +41 -2
  5. package/demo/data/flows/voicemail.json +613 -0
  6. package/demo/index.html +6 -0
  7. package/dist/locales/es.js +5 -5
  8. package/dist/locales/es.js.map +1 -1
  9. package/dist/locales/fr.js +5 -5
  10. package/dist/locales/fr.js.map +1 -1
  11. package/dist/locales/locale-codes.js +11 -2
  12. package/dist/locales/locale-codes.js.map +1 -1
  13. package/dist/locales/pt.js +5 -5
  14. package/dist/locales/pt.js.map +1 -1
  15. package/dist/temba-components.js +1109 -535
  16. package/dist/temba-components.js.map +1 -1
  17. package/out-tsc/src/display/FloatingTab.js +167 -0
  18. package/out-tsc/src/display/FloatingTab.js.map +1 -0
  19. package/out-tsc/src/display/ProgressBar.js +22 -2
  20. package/out-tsc/src/display/ProgressBar.js.map +1 -1
  21. package/out-tsc/src/events.js.map +1 -1
  22. package/out-tsc/src/flow/CanvasNode.js +165 -31
  23. package/out-tsc/src/flow/CanvasNode.js.map +1 -1
  24. package/out-tsc/src/flow/Editor.js +857 -3
  25. package/out-tsc/src/flow/Editor.js.map +1 -1
  26. package/out-tsc/src/flow/NodeEditor.js +239 -19
  27. package/out-tsc/src/flow/NodeEditor.js.map +1 -1
  28. package/out-tsc/src/flow/NodeTypeSelector.js +44 -3
  29. package/out-tsc/src/flow/NodeTypeSelector.js.map +1 -1
  30. package/out-tsc/src/flow/StickyNote.js +12 -3
  31. package/out-tsc/src/flow/StickyNote.js.map +1 -1
  32. package/out-tsc/src/flow/actions/add_contact_groups.js +2 -1
  33. package/out-tsc/src/flow/actions/add_contact_groups.js.map +1 -1
  34. package/out-tsc/src/flow/actions/add_contact_urn.js +2 -1
  35. package/out-tsc/src/flow/actions/add_contact_urn.js.map +1 -1
  36. package/out-tsc/src/flow/actions/add_input_labels.js +2 -1
  37. package/out-tsc/src/flow/actions/add_input_labels.js.map +1 -1
  38. package/out-tsc/src/flow/actions/play_audio.js +2 -1
  39. package/out-tsc/src/flow/actions/play_audio.js.map +1 -1
  40. package/out-tsc/src/flow/actions/remove_contact_groups.js +2 -1
  41. package/out-tsc/src/flow/actions/remove_contact_groups.js.map +1 -1
  42. package/out-tsc/src/flow/actions/request_optin.js +1 -0
  43. package/out-tsc/src/flow/actions/request_optin.js.map +1 -1
  44. package/out-tsc/src/flow/actions/say_msg.js +2 -1
  45. package/out-tsc/src/flow/actions/say_msg.js.map +1 -1
  46. package/out-tsc/src/flow/actions/send_broadcast.js +2 -1
  47. package/out-tsc/src/flow/actions/send_broadcast.js.map +1 -1
  48. package/out-tsc/src/flow/actions/send_email.js +2 -1
  49. package/out-tsc/src/flow/actions/send_email.js.map +1 -1
  50. package/out-tsc/src/flow/actions/send_msg.js +93 -3
  51. package/out-tsc/src/flow/actions/send_msg.js.map +1 -1
  52. package/out-tsc/src/flow/actions/set_contact_channel.js +2 -1
  53. package/out-tsc/src/flow/actions/set_contact_channel.js.map +1 -1
  54. package/out-tsc/src/flow/actions/set_contact_field.js +2 -1
  55. package/out-tsc/src/flow/actions/set_contact_field.js.map +1 -1
  56. package/out-tsc/src/flow/actions/set_contact_language.js +2 -1
  57. package/out-tsc/src/flow/actions/set_contact_language.js.map +1 -1
  58. package/out-tsc/src/flow/actions/set_contact_name.js +2 -1
  59. package/out-tsc/src/flow/actions/set_contact_name.js.map +1 -1
  60. package/out-tsc/src/flow/actions/set_contact_status.js +2 -1
  61. package/out-tsc/src/flow/actions/set_contact_status.js.map +1 -1
  62. package/out-tsc/src/flow/actions/set_run_result.js +2 -1
  63. package/out-tsc/src/flow/actions/set_run_result.js.map +1 -1
  64. package/out-tsc/src/flow/actions/start_session.js +2 -1
  65. package/out-tsc/src/flow/actions/start_session.js.map +1 -1
  66. package/out-tsc/src/flow/config.js +2 -10
  67. package/out-tsc/src/flow/config.js.map +1 -1
  68. package/out-tsc/src/flow/nodes/shared.js +54 -0
  69. package/out-tsc/src/flow/nodes/shared.js.map +1 -1
  70. package/out-tsc/src/flow/nodes/split_by_airtime.js +9 -3
  71. package/out-tsc/src/flow/nodes/split_by_airtime.js.map +1 -1
  72. package/out-tsc/src/flow/nodes/split_by_contact_field.js +8 -3
  73. package/out-tsc/src/flow/nodes/split_by_contact_field.js.map +1 -1
  74. package/out-tsc/src/flow/nodes/split_by_expression.js +8 -3
  75. package/out-tsc/src/flow/nodes/split_by_expression.js.map +1 -1
  76. package/out-tsc/src/flow/nodes/split_by_groups.js +8 -3
  77. package/out-tsc/src/flow/nodes/split_by_groups.js.map +1 -1
  78. package/out-tsc/src/flow/nodes/split_by_intent.js +3 -2
  79. package/out-tsc/src/flow/nodes/split_by_intent.js.map +1 -1
  80. package/out-tsc/src/flow/nodes/split_by_llm.js +9 -2
  81. package/out-tsc/src/flow/nodes/split_by_llm.js.map +1 -1
  82. package/out-tsc/src/flow/nodes/split_by_llm_categorize.js +9 -2
  83. package/out-tsc/src/flow/nodes/split_by_llm_categorize.js.map +1 -1
  84. package/out-tsc/src/flow/nodes/split_by_random.js +8 -2
  85. package/out-tsc/src/flow/nodes/split_by_random.js.map +1 -1
  86. package/out-tsc/src/flow/nodes/split_by_resthook.js +8 -3
  87. package/out-tsc/src/flow/nodes/split_by_resthook.js.map +1 -1
  88. package/out-tsc/src/flow/nodes/split_by_run_result.js +8 -3
  89. package/out-tsc/src/flow/nodes/split_by_run_result.js.map +1 -1
  90. package/out-tsc/src/flow/nodes/split_by_scheme.js +8 -3
  91. package/out-tsc/src/flow/nodes/split_by_scheme.js.map +1 -1
  92. package/out-tsc/src/flow/nodes/split_by_subflow.js +8 -2
  93. package/out-tsc/src/flow/nodes/split_by_subflow.js.map +1 -1
  94. package/out-tsc/src/flow/nodes/split_by_ticket.js +8 -2
  95. package/out-tsc/src/flow/nodes/split_by_ticket.js.map +1 -1
  96. package/out-tsc/src/flow/nodes/split_by_webhook.js +8 -2
  97. package/out-tsc/src/flow/nodes/split_by_webhook.js.map +1 -1
  98. package/out-tsc/src/flow/nodes/wait_for_digits.js +3 -2
  99. package/out-tsc/src/flow/nodes/wait_for_digits.js.map +1 -1
  100. package/out-tsc/src/flow/nodes/wait_for_menu.js +3 -2
  101. package/out-tsc/src/flow/nodes/wait_for_menu.js.map +1 -1
  102. package/out-tsc/src/flow/nodes/wait_for_response.js +8 -3
  103. package/out-tsc/src/flow/nodes/wait_for_response.js.map +1 -1
  104. package/out-tsc/src/flow/types.js +15 -0
  105. package/out-tsc/src/flow/types.js.map +1 -1
  106. package/out-tsc/src/layout/FloatingWindow.js +346 -0
  107. package/out-tsc/src/layout/FloatingWindow.js.map +1 -0
  108. package/out-tsc/src/live/ContactChat.js +3 -19
  109. package/out-tsc/src/live/ContactChat.js.map +1 -1
  110. package/out-tsc/src/locales/es.js +5 -5
  111. package/out-tsc/src/locales/es.js.map +1 -1
  112. package/out-tsc/src/locales/fr.js +5 -5
  113. package/out-tsc/src/locales/fr.js.map +1 -1
  114. package/out-tsc/src/locales/locale-codes.js +11 -2
  115. package/out-tsc/src/locales/locale-codes.js.map +1 -1
  116. package/out-tsc/src/locales/pt.js +5 -5
  117. package/out-tsc/src/locales/pt.js.map +1 -1
  118. package/out-tsc/src/store/AppState.js +67 -0
  119. package/out-tsc/src/store/AppState.js.map +1 -1
  120. package/out-tsc/temba-modules.js +4 -0
  121. package/out-tsc/temba-modules.js.map +1 -1
  122. package/out-tsc/test/temba-floating-tab.test.js +91 -0
  123. package/out-tsc/test/temba-floating-tab.test.js.map +1 -0
  124. package/out-tsc/test/temba-floating-window.test.js +301 -0
  125. package/out-tsc/test/temba-floating-window.test.js.map +1 -0
  126. package/out-tsc/test/temba-flow-editor-node.test.js +117 -0
  127. package/out-tsc/test/temba-flow-editor-node.test.js.map +1 -1
  128. package/out-tsc/test/temba-localization.test.js +471 -0
  129. package/out-tsc/test/temba-localization.test.js.map +1 -0
  130. package/out-tsc/test/temba-node-type-selector.test.js +150 -0
  131. package/out-tsc/test/temba-node-type-selector.test.js.map +1 -1
  132. package/out-tsc/test/utils.test.js +18 -0
  133. package/out-tsc/test/utils.test.js.map +1 -1
  134. package/package.json +1 -1
  135. package/screenshots/truth/floating-tab/default.png +0 -0
  136. package/screenshots/truth/floating-tab/gray.png +0 -0
  137. package/screenshots/truth/floating-tab/green.png +0 -0
  138. package/screenshots/truth/floating-tab/hidden.png +0 -0
  139. package/screenshots/truth/floating-tab/hover.png +0 -0
  140. package/screenshots/truth/floating-tab/purple.png +0 -0
  141. package/screenshots/truth/floating-window/chromeless.png +0 -0
  142. package/screenshots/truth/floating-window/custom-size.png +0 -0
  143. package/screenshots/truth/floating-window/default.png +0 -0
  144. package/screenshots/truth/floating-window/with-header.png +0 -0
  145. package/screenshots/truth/node-type-selector/action-mode.png +0 -0
  146. package/screenshots/truth/node-type-selector/split-mode.png +0 -0
  147. package/screenshots/truth/nodes/split_by_llm_categorize/editor/feedback-categorization.png +0 -0
  148. package/src/display/FloatingTab.ts +174 -0
  149. package/src/display/ProgressBar.ts +22 -2
  150. package/src/events.ts +2 -4
  151. package/src/flow/CanvasNode.ts +190 -32
  152. package/src/flow/Editor.ts +1040 -3
  153. package/src/flow/NodeEditor.ts +317 -19
  154. package/src/flow/NodeTypeSelector.ts +47 -3
  155. package/src/flow/StickyNote.ts +12 -3
  156. package/src/flow/actions/add_contact_groups.ts +2 -1
  157. package/src/flow/actions/add_contact_urn.ts +3 -1
  158. package/src/flow/actions/add_input_labels.ts +2 -1
  159. package/src/flow/actions/play_audio.ts +2 -1
  160. package/src/flow/actions/remove_contact_groups.ts +3 -1
  161. package/src/flow/actions/request_optin.ts +1 -0
  162. package/src/flow/actions/say_msg.ts +2 -1
  163. package/src/flow/actions/send_broadcast.ts +2 -1
  164. package/src/flow/actions/send_email.ts +3 -1
  165. package/src/flow/actions/send_msg.ts +134 -3
  166. package/src/flow/actions/set_contact_channel.ts +2 -1
  167. package/src/flow/actions/set_contact_field.ts +2 -1
  168. package/src/flow/actions/set_contact_language.ts +3 -1
  169. package/src/flow/actions/set_contact_name.ts +2 -1
  170. package/src/flow/actions/set_contact_status.ts +2 -1
  171. package/src/flow/actions/set_run_result.ts +2 -1
  172. package/src/flow/actions/start_session.ts +3 -1
  173. package/src/flow/config.ts +2 -12
  174. package/src/flow/nodes/shared.ts +70 -1
  175. package/src/flow/nodes/split_by_airtime.ts +20 -3
  176. package/src/flow/nodes/split_by_contact_field.ts +13 -3
  177. package/src/flow/nodes/split_by_expression.ts +13 -3
  178. package/src/flow/nodes/split_by_groups.ts +13 -3
  179. package/src/flow/nodes/split_by_intent.ts +3 -2
  180. package/src/flow/nodes/split_by_llm.ts +19 -2
  181. package/src/flow/nodes/split_by_llm_categorize.ts +19 -2
  182. package/src/flow/nodes/split_by_random.ts +12 -2
  183. package/src/flow/nodes/split_by_resthook.ts +13 -3
  184. package/src/flow/nodes/split_by_run_result.ts +13 -3
  185. package/src/flow/nodes/split_by_scheme.ts +13 -3
  186. package/src/flow/nodes/split_by_subflow.ts +12 -2
  187. package/src/flow/nodes/split_by_ticket.ts +12 -2
  188. package/src/flow/nodes/split_by_webhook.ts +12 -2
  189. package/src/flow/nodes/wait_for_digits.ts +3 -2
  190. package/src/flow/nodes/wait_for_menu.ts +3 -2
  191. package/src/flow/nodes/wait_for_response.ts +13 -3
  192. package/src/flow/types.ts +47 -0
  193. package/src/layout/FloatingWindow.ts +386 -0
  194. package/src/live/ContactChat.ts +4 -19
  195. package/src/locales/es.ts +18 -13
  196. package/src/locales/fr.ts +18 -13
  197. package/src/locales/locale-codes.ts +11 -2
  198. package/src/locales/pt.ts +18 -13
  199. package/src/store/AppState.ts +104 -0
  200. package/static/api/llms.json +18 -0
  201. package/temba-modules.ts +4 -0
  202. package/test/temba-floating-tab.test.ts +110 -0
  203. package/test/temba-floating-window.test.ts +477 -0
  204. package/test/temba-flow-editor-node.test.ts +144 -0
  205. package/test/temba-localization.test.ts +611 -0
  206. package/test/temba-node-type-selector.test.ts +203 -0
  207. package/test/utils.test.ts +20 -0
  208. package/test-assets/contacts/history.json +5 -6
  209. package/test-assets/select/llms.json +2 -2
  210. package/web-dev-server.config.mjs +47 -1
  211. package/web-test-runner.config.mjs +0 -1
  212. package/out-tsc/src/flow/nodes/wait_for_audio.js +0 -7
  213. package/out-tsc/src/flow/nodes/wait_for_audio.js.map +0 -1
  214. package/out-tsc/src/flow/nodes/wait_for_image.js +0 -7
  215. package/out-tsc/src/flow/nodes/wait_for_image.js.map +0 -1
  216. package/out-tsc/src/flow/nodes/wait_for_location.js +0 -7
  217. package/out-tsc/src/flow/nodes/wait_for_location.js.map +0 -1
  218. package/out-tsc/src/flow/nodes/wait_for_video.js +0 -7
  219. package/out-tsc/src/flow/nodes/wait_for_video.js.map +0 -1
  220. package/src/flow/nodes/wait_for_audio.ts +0 -7
  221. package/src/flow/nodes/wait_for_image.ts +0 -7
  222. package/src/flow/nodes/wait_for_location.ts +0 -7
  223. package/src/flow/nodes/wait_for_video.ts +0 -7
@@ -1,10 +1,11 @@
1
1
  import { html } from 'lit-html';
2
- import { ActionConfig, ACTION_GROUPS } from '../types';
2
+ import { ActionConfig, ACTION_GROUPS, FlowTypes } from '../types';
3
3
  import { Node, SayMsg } from '../../store/flow-definition';
4
4
 
5
5
  export const say_msg: ActionConfig = {
6
6
  name: 'Say Message',
7
7
  group: ACTION_GROUPS.send,
8
+ flowTypes: [FlowTypes.VOICE],
8
9
  render: (_node: Node, _action: SayMsg) => {
9
10
  // This will need to be implemented based on the actual render logic
10
11
  return html`<div>Say Message</div>`;
@@ -1,5 +1,5 @@
1
1
  import { html } from 'lit-html';
2
- import { ActionConfig, ACTION_GROUPS, FormData } from '../types';
2
+ import { ActionConfig, ACTION_GROUPS, FormData, FlowTypes } from '../types';
3
3
  import { Node, SendBroadcast } from '../../store/flow-definition';
4
4
  import { renderStringList } from '../utils';
5
5
  import { Icon } from '../../Icons';
@@ -7,6 +7,7 @@ import { Icon } from '../../Icons';
7
7
  export const send_broadcast: ActionConfig = {
8
8
  name: 'Send Broadcast',
9
9
  group: ACTION_GROUPS.broadcast,
10
+ flowTypes: [FlowTypes.VOICE, FlowTypes.MESSAGE, FlowTypes.BACKGROUND],
10
11
  render: (_node: Node, action: SendBroadcast) => {
11
12
  const recipients = [
12
13
  ...(action.contacts || []).map((c) => c.name),
@@ -3,7 +3,8 @@ import {
3
3
  ActionConfig,
4
4
  ACTION_GROUPS,
5
5
  FormData,
6
- ValidationResult
6
+ ValidationResult,
7
+ FlowTypes
7
8
  } from '../types';
8
9
  import { Node, SendEmail } from '../../store/flow-definition';
9
10
  import { renderStringList } from '../utils';
@@ -12,6 +13,7 @@ import { Icon } from '../../Icons';
12
13
  export const send_email: ActionConfig = {
13
14
  name: 'Send Email',
14
15
  group: ACTION_GROUPS.broadcast,
16
+ flowTypes: [FlowTypes.VOICE, FlowTypes.MESSAGE, FlowTypes.BACKGROUND],
15
17
  render: (_node: Node, action: SendEmail) => {
16
18
  return html`<div>
17
19
  <div>${renderStringList(action.addresses, Icon.email)}</div>
@@ -4,7 +4,8 @@ import {
4
4
  ActionConfig,
5
5
  ACTION_GROUPS,
6
6
  FormData,
7
- ValidationResult
7
+ ValidationResult,
8
+ FlowTypes
8
9
  } from '../types';
9
10
  import { Node, SendMsg } from '../../store/flow-definition';
10
11
  import { titleCase } from '../../utils';
@@ -12,13 +13,14 @@ import { titleCase } from '../../utils';
12
13
  export const send_msg: ActionConfig = {
13
14
  name: 'Send Message',
14
15
  group: ACTION_GROUPS.send,
16
+ flowTypes: [FlowTypes.VOICE, FlowTypes.MESSAGE, FlowTypes.BACKGROUND],
15
17
  render: (_node: Node, action: SendMsg) => {
16
18
  const text = action.text.replace(/\n/g, '<br>');
17
19
  return html`
18
20
  ${unsafeHTML(text)}
19
- ${action.quick_replies?.length > 0
21
+ ${(action.quick_replies || [])?.length > 0
20
22
  ? html`<div class="quick-replies">
21
- ${action.quick_replies.map((reply) => {
23
+ ${(action.quick_replies || []).map((reply) => {
22
24
  return html`<div class="quick-reply">${reply}</div>`;
23
25
  })}
24
26
  ${action.template
@@ -225,5 +227,134 @@ export const send_msg: ActionConfig = {
225
227
  valid: Object.keys(errors).length === 0,
226
228
  errors
227
229
  };
230
+ },
231
+ localizable: ['text', 'quick_replies', 'attachments'],
232
+ toLocalizationFormData: (
233
+ action: SendMsg,
234
+ localization: Record<string, any>
235
+ ) => {
236
+ // Convert localized values to form data format
237
+ // Localized values are stored as arrays even for single values
238
+ const formData: FormData = {
239
+ uuid: action.uuid
240
+ };
241
+
242
+ // Handle text (single value, but stored as array in localization)
243
+ if (localization.text && Array.isArray(localization.text)) {
244
+ formData.text = localization.text[0] || '';
245
+ } else {
246
+ // Fall back to empty string if no localization
247
+ formData.text = '';
248
+ }
249
+
250
+ // Handle attachments (already an array)
251
+ if (localization.attachments && Array.isArray(localization.attachments)) {
252
+ formData.attachments = localization.attachments;
253
+ }
254
+
255
+ // Handle quick_replies (already an array)
256
+ if (
257
+ localization.quick_replies &&
258
+ Array.isArray(localization.quick_replies)
259
+ ) {
260
+ formData.quick_replies = localization.quick_replies.map((reply) => ({
261
+ name: reply,
262
+ value: reply
263
+ }));
264
+ }
265
+
266
+ // Extract runtime attachments from localized attachments
267
+ const runtimeAttachments: {
268
+ type: { name: string; value: string };
269
+ expression: string;
270
+ }[] = [];
271
+ const staticAttachments: string[] = [];
272
+
273
+ if (formData.attachments && Array.isArray(formData.attachments)) {
274
+ formData.attachments.forEach((attachment) => {
275
+ if (typeof attachment === 'string' && attachment.includes(':')) {
276
+ const colonIndex = attachment.indexOf(':');
277
+ const contentType = attachment.substring(0, colonIndex);
278
+ const value = attachment.substring(colonIndex + 1);
279
+
280
+ if (!contentType.includes('/')) {
281
+ runtimeAttachments.push({
282
+ type: { name: titleCase(contentType), value: contentType },
283
+ expression: value
284
+ });
285
+ } else {
286
+ staticAttachments.push(attachment);
287
+ }
288
+ }
289
+ });
290
+ }
291
+
292
+ formData.attachments = staticAttachments;
293
+ formData.runtime_attachments = runtimeAttachments;
294
+
295
+ return formData;
296
+ },
297
+ fromLocalizationFormData: (formData: FormData, action: SendMsg) => {
298
+ // Convert form data to localization format
299
+ // All values must be stored as arrays
300
+ const localization: Record<string, any> = {};
301
+
302
+ // Handle text (store as single-element array)
303
+ // Only save if not empty and different from base action
304
+ if (formData.text && formData.text.trim() !== '') {
305
+ if (formData.text !== action.text) {
306
+ localization.text = [formData.text];
307
+ }
308
+ }
309
+
310
+ // Handle quick_replies (store as array)
311
+ const quickReplies = (formData.quick_replies || [])
312
+ .map((reply: any) =>
313
+ typeof reply === 'string' ? reply : reply.value || reply.name || reply
314
+ )
315
+ .filter((reply: string) => reply && reply.trim() !== '');
316
+
317
+ // Only save if there are quick replies and different from base action
318
+ if (quickReplies.length > 0) {
319
+ if (
320
+ JSON.stringify(quickReplies) !==
321
+ JSON.stringify(action.quick_replies || [])
322
+ ) {
323
+ localization.quick_replies = quickReplies;
324
+ }
325
+ }
326
+
327
+ // Handle attachments (combine static and runtime attachments)
328
+ const staticAttachments = (formData.attachments || []).filter(
329
+ (att: string) => att && att.trim() !== ''
330
+ );
331
+ const runtimeAttachments = (formData.runtime_attachments || [])
332
+ .filter(
333
+ (item: {
334
+ type: [{ name: string; value: string }];
335
+ expression: string;
336
+ }) =>
337
+ item && item.type && item.expression && item.expression.trim() !== ''
338
+ )
339
+ .map(
340
+ (item: {
341
+ type: [{ name: string; value: string }];
342
+ expression: string;
343
+ }) => `${item.type[0].value}:${item.expression}`
344
+ );
345
+
346
+ const allAttachments = [...staticAttachments, ...runtimeAttachments];
347
+
348
+ // Only save if there are attachments and different from base action
349
+ if (allAttachments.length > 0) {
350
+ if (
351
+ JSON.stringify(allAttachments) !==
352
+ JSON.stringify(action.attachments || [])
353
+ ) {
354
+ localization.attachments = allAttachments;
355
+ }
356
+ }
357
+
358
+ return localization;
228
359
  }
229
360
  };
@@ -1,10 +1,11 @@
1
1
  import { html } from 'lit-html';
2
- import { ActionConfig, ACTION_GROUPS } from '../types';
2
+ import { ActionConfig, ACTION_GROUPS, FlowTypes } from '../types';
3
3
  import { Node, SetContactChannel } from '../../store/flow-definition';
4
4
 
5
5
  export const set_contact_channel: ActionConfig = {
6
6
  name: 'Update Channel',
7
7
  group: ACTION_GROUPS.contacts,
8
+ flowTypes: [FlowTypes.VOICE, FlowTypes.MESSAGE, FlowTypes.BACKGROUND],
8
9
  render: (_node: Node, action: SetContactChannel) => {
9
10
  return html`<div>Set to <strong>${action.channel.name}</strong></div>`;
10
11
  },
@@ -1,10 +1,11 @@
1
1
  import { html } from 'lit-html';
2
- import { ActionConfig, ACTION_GROUPS, FormData } from '../types';
2
+ import { ActionConfig, ACTION_GROUPS, FormData, FlowTypes } from '../types';
3
3
  import { Node, SetContactField } from '../../store/flow-definition';
4
4
 
5
5
  export const set_contact_field: ActionConfig = {
6
6
  name: 'Update Field',
7
7
  group: ACTION_GROUPS.contacts,
8
+ flowTypes: [FlowTypes.VOICE, FlowTypes.MESSAGE, FlowTypes.BACKGROUND],
8
9
  render: (_node: Node, action: SetContactField) => {
9
10
  if (action.value) {
10
11
  return html`<div>
@@ -3,7 +3,8 @@ import {
3
3
  ActionConfig,
4
4
  ACTION_GROUPS,
5
5
  FormData,
6
- ValidationResult
6
+ ValidationResult,
7
+ FlowTypes
7
8
  } from '../types';
8
9
  import { Node, SetContactLanguage } from '../../store/flow-definition';
9
10
  import { getStore } from '../../store/Store';
@@ -11,6 +12,7 @@ import { getStore } from '../../store/Store';
11
12
  export const set_contact_language: ActionConfig = {
12
13
  name: 'Update Language',
13
14
  group: ACTION_GROUPS.contacts,
15
+ flowTypes: [FlowTypes.VOICE, FlowTypes.MESSAGE, FlowTypes.BACKGROUND],
14
16
  render: (_node: Node, action: SetContactLanguage) => {
15
17
  const languageNames = new Intl.DisplayNames(['en'], {
16
18
  type: 'language'
@@ -1,10 +1,11 @@
1
1
  import { html } from 'lit-html';
2
- import { ActionConfig, ACTION_GROUPS, FormData } from '../types';
2
+ import { ActionConfig, ACTION_GROUPS, FormData, FlowTypes } from '../types';
3
3
  import { Node, SetContactName } from '../../store/flow-definition';
4
4
 
5
5
  export const set_contact_name: ActionConfig = {
6
6
  name: 'Update Name',
7
7
  group: ACTION_GROUPS.contacts,
8
+ flowTypes: [FlowTypes.VOICE, FlowTypes.MESSAGE, FlowTypes.BACKGROUND],
8
9
  render: (_node: Node, action: SetContactName) => {
9
10
  return html`<div>Set to <strong>${action.name}</strong></div>`;
10
11
  },
@@ -1,11 +1,12 @@
1
1
  import { html } from 'lit-html';
2
- import { ActionConfig, ACTION_GROUPS, FormData } from '../types';
2
+ import { ActionConfig, ACTION_GROUPS, FormData, FlowTypes } from '../types';
3
3
  import { Node, SetContactStatus } from '../../store/flow-definition';
4
4
  import { titleCase } from '../../utils';
5
5
 
6
6
  export const set_contact_status: ActionConfig = {
7
7
  name: 'Update Status',
8
8
  group: ACTION_GROUPS.contacts,
9
+ flowTypes: [FlowTypes.VOICE, FlowTypes.MESSAGE, FlowTypes.BACKGROUND],
9
10
  render: (_node: Node, action: SetContactStatus) => {
10
11
  return html`<div>Set to <strong>${titleCase(action.status)}</strong></div>`;
11
12
  },
@@ -1,11 +1,12 @@
1
1
  import { html } from 'lit-html';
2
- import { ActionConfig, ACTION_GROUPS, FormData } from '../types';
2
+ import { ActionConfig, ACTION_GROUPS, FormData, FlowTypes } from '../types';
3
3
  import { Node, SetRunResult } from '../../store/flow-definition';
4
4
  import { getStore } from '../../store/Store';
5
5
 
6
6
  export const set_run_result: ActionConfig = {
7
7
  name: 'Save Flow Result',
8
8
  group: ACTION_GROUPS.save,
9
+ flowTypes: [FlowTypes.VOICE, FlowTypes.MESSAGE, FlowTypes.BACKGROUND],
9
10
  render: (_node: Node, action: SetRunResult) => {
10
11
  return html`<div>
11
12
  Save <strong>${action.value}</strong> as <strong>${action.name}</strong>
@@ -3,7 +3,8 @@ import {
3
3
  ActionConfig,
4
4
  ACTION_GROUPS,
5
5
  FormData,
6
- ValidationResult
6
+ ValidationResult,
7
+ FlowTypes
7
8
  } from '../types';
8
9
  import { Node, StartSession } from '../../store/flow-definition';
9
10
  import { renderNamedObjects } from '../utils';
@@ -11,6 +12,7 @@ import { renderNamedObjects } from '../utils';
11
12
  export const start_session: ActionConfig = {
12
13
  name: 'Start Flow',
13
14
  group: ACTION_GROUPS.broadcast,
15
+ flowTypes: [FlowTypes.VOICE, FlowTypes.MESSAGE, FlowTypes.BACKGROUND],
14
16
  render: (_node: Node, action: StartSession) => {
15
17
  const hasGroups = action.groups && action.groups.length > 0;
16
18
  const hasContacts = action.contacts && action.contacts.length > 0;
@@ -36,19 +36,16 @@ import { split_by_webhook } from './nodes/split_by_webhook';
36
36
  import { split_by_resthook } from './nodes/split_by_resthook';
37
37
  import { split_by_llm } from './nodes/split_by_llm';
38
38
  import { split_by_llm_categorize } from './nodes/split_by_llm_categorize';
39
- import { wait_for_audio } from './nodes/wait_for_audio';
40
39
  import { wait_for_digits } from './nodes/wait_for_digits';
41
- import { wait_for_image } from './nodes/wait_for_image';
42
- import { wait_for_location } from './nodes/wait_for_location';
43
40
  import { wait_for_menu } from './nodes/wait_for_menu';
44
41
  import { wait_for_response } from './nodes/wait_for_response';
45
- import { wait_for_video } from './nodes/wait_for_video';
46
42
 
47
43
  export const ACTION_CONFIG: {
48
44
  [key: string]: ActionConfig;
49
45
  } = {
46
+ say_msg,
47
+ play_audio,
50
48
  set_contact_field,
51
-
52
49
  send_broadcast,
53
50
  set_run_result,
54
51
  send_msg,
@@ -60,8 +57,6 @@ export const ACTION_CONFIG: {
60
57
  set_contact_channel,
61
58
  set_contact_language,
62
59
  set_contact_status,
63
- say_msg,
64
- play_audio,
65
60
  add_contact_urn,
66
61
  add_input_labels,
67
62
  request_optin
@@ -71,7 +66,6 @@ export const NODE_CONFIG: {
71
66
  [key: string]: NodeConfig;
72
67
  } = {
73
68
  execute_actions,
74
-
75
69
  split_by_contact_field,
76
70
  split_by_expression,
77
71
  split_by_groups,
@@ -84,12 +78,8 @@ export const NODE_CONFIG: {
84
78
  split_by_ticket,
85
79
  split_by_webhook,
86
80
  split_by_resthook,
87
- wait_for_audio,
88
81
  wait_for_digits,
89
- wait_for_image,
90
- wait_for_location,
91
82
  wait_for_menu,
92
83
  wait_for_response,
93
- wait_for_video,
94
84
  split_by_airtime
95
85
  };
@@ -1,4 +1,5 @@
1
- import { TextFieldConfig } from '../types';
1
+ import { FormData, TextFieldConfig } from '../types';
2
+ import { Node } from '../../store/flow-definition';
2
3
 
3
4
  /**
4
5
  * Shared result_name field configuration for router nodes.
@@ -16,3 +17,71 @@ export const resultNameField: TextFieldConfig = {
16
17
  helpText: 'The name to use to reference this result in the flow',
17
18
  optionalLink: 'Save result as...'
18
19
  };
20
+
21
+ /**
22
+ * Shared category localization functions for router nodes.
23
+ * These provide a consistent way to localize category names across all router types.
24
+ */
25
+
26
+ /**
27
+ * Converts a node's categories to localization form data.
28
+ * @param node - The node containing categories to localize
29
+ * @param localization - The existing localization data for this language
30
+ * @returns Form data with category UUIDs mapped to original and localized names
31
+ */
32
+ export function categoriesToLocalizationFormData(
33
+ node: Node,
34
+ localization: Record<string, any>
35
+ ): FormData {
36
+ const categories = node.router?.categories || [];
37
+ const localizationData: Record<string, any> = {};
38
+
39
+ categories.forEach((category: any) => {
40
+ const categoryUuid = category.uuid;
41
+ const categoryLocalization = localization[categoryUuid];
42
+
43
+ localizationData[categoryUuid] = {
44
+ originalName: category.name,
45
+ localizedName:
46
+ categoryLocalization && categoryLocalization.name
47
+ ? Array.isArray(categoryLocalization.name)
48
+ ? categoryLocalization.name[0] || ''
49
+ : categoryLocalization.name
50
+ : ''
51
+ };
52
+ });
53
+
54
+ return {
55
+ categories: localizationData
56
+ };
57
+ }
58
+
59
+ /**
60
+ * Converts localization form data back to the localization structure.
61
+ * @param formData - The form data containing category localizations
62
+ * @param _node - The original node (reserved for future validation)
63
+ * @returns Record mapping category UUIDs to their localization data
64
+ */
65
+ export function localizationFormDataToCategories(
66
+ formData: FormData,
67
+ _node: Node
68
+ ): Record<string, any> {
69
+ const localizationData: Record<string, any> = {};
70
+
71
+ if (formData.categories) {
72
+ Object.keys(formData.categories).forEach((categoryUuid) => {
73
+ const categoryData = formData.categories[categoryUuid];
74
+ const localizedName = categoryData.localizedName?.trim() || '';
75
+ const originalName = categoryData.originalName?.trim() || '';
76
+
77
+ // Only save if localized name is different from original and not empty
78
+ if (localizedName && localizedName !== originalName) {
79
+ localizationData[categoryUuid] = {
80
+ name: [localizedName]
81
+ };
82
+ }
83
+ });
84
+ }
85
+
86
+ return localizationData;
87
+ }
@@ -1,14 +1,26 @@
1
- import { ACTION_GROUPS, FormData, NodeConfig } from '../types';
1
+ import {
2
+ ACTION_GROUPS,
3
+ FormData,
4
+ NodeConfig,
5
+ FlowTypes,
6
+ Features
7
+ } from '../types';
2
8
  import { TransferAirtime, Node } from '../../store/flow-definition';
3
9
  import { generateUUID, createSuccessFailureRouter } from '../../utils';
4
10
  import { html } from 'lit';
5
11
  import { CURRENCY_OPTIONS, CURRENCIES } from '../currencies';
6
- import { resultNameField } from './shared';
12
+ import {
13
+ resultNameField,
14
+ categoriesToLocalizationFormData,
15
+ localizationFormDataToCategories
16
+ } from './shared';
7
17
 
8
18
  export const split_by_airtime: NodeConfig = {
9
19
  type: 'split_by_airtime',
10
20
  name: 'Send Airtime',
11
21
  group: ACTION_GROUPS.services,
22
+ flowTypes: [FlowTypes.VOICE, FlowTypes.MESSAGE, FlowTypes.BACKGROUND],
23
+ features: [Features.AIRTIME],
12
24
  showAsAction: true,
13
25
  form: {
14
26
  amounts: {
@@ -238,5 +250,10 @@ export const split_by_airtime: NodeConfig = {
238
250
  router: finalRouter,
239
251
  exits: exits
240
252
  };
241
- }
253
+ },
254
+
255
+ // Localization support for categories
256
+ localizable: 'categories',
257
+ toLocalizationFormData: categoriesToLocalizationFormData,
258
+ fromLocalizationFormData: localizationFormDataToCategories
242
259
  };
@@ -1,4 +1,4 @@
1
- import { SPLIT_GROUPS, FormData, NodeConfig } from '../types';
1
+ import { SPLIT_GROUPS, FormData, NodeConfig, FlowTypes } from '../types';
2
2
  import { Node } from '../../store/flow-definition';
3
3
  import { createRulesRouter } from '../../utils';
4
4
  import {
@@ -6,7 +6,11 @@ import {
6
6
  operatorsToSelectOptions,
7
7
  getOperatorConfig
8
8
  } from '../operators';
9
- import { resultNameField } from './shared';
9
+ import {
10
+ resultNameField,
11
+ categoriesToLocalizationFormData,
12
+ localizationFormDataToCategories
13
+ } from './shared';
10
14
  import {
11
15
  createRulesArrayConfig,
12
16
  extractUserRules,
@@ -44,6 +48,7 @@ export const split_by_contact_field: NodeConfig = {
44
48
  type: 'split_by_contact_field',
45
49
  name: 'Split by Contact Field',
46
50
  group: SPLIT_GROUPS.split,
51
+ flowTypes: [FlowTypes.VOICE, FlowTypes.MESSAGE, FlowTypes.BACKGROUND],
47
52
  dialogSize: 'large',
48
53
  form: {
49
54
  field: {
@@ -185,5 +190,10 @@ export const split_by_contact_field: NodeConfig = {
185
190
  },
186
191
  renderTitle: (node: Node, nodeUI?: any) => {
187
192
  return html`<div>Split by ${nodeUI.config.operand.name}</div>`;
188
- }
193
+ },
194
+
195
+ // Localization support for categories
196
+ localizable: 'categories',
197
+ toLocalizationFormData: categoriesToLocalizationFormData,
198
+ fromLocalizationFormData: localizationFormDataToCategories
189
199
  };
@@ -1,4 +1,4 @@
1
- import { SPLIT_GROUPS, FormData, NodeConfig } from '../types';
1
+ import { SPLIT_GROUPS, FormData, NodeConfig, FlowTypes } from '../types';
2
2
  import { Node } from '../../store/flow-definition';
3
3
  import { createRulesRouter } from '../../utils';
4
4
  import {
@@ -6,7 +6,11 @@ import {
6
6
  operatorsToSelectOptions,
7
7
  getOperatorConfig
8
8
  } from '../operators';
9
- import { resultNameField } from './shared';
9
+ import {
10
+ resultNameField,
11
+ categoriesToLocalizationFormData,
12
+ localizationFormDataToCategories
13
+ } from './shared';
10
14
  import {
11
15
  createRulesArrayConfig,
12
16
  extractUserRules,
@@ -17,6 +21,7 @@ export const split_by_expression: NodeConfig = {
17
21
  type: 'split_by_expression',
18
22
  name: 'Split by Expression',
19
23
  group: SPLIT_GROUPS.split,
24
+ flowTypes: [FlowTypes.VOICE, FlowTypes.MESSAGE, FlowTypes.BACKGROUND],
20
25
  dialogSize: 'large',
21
26
  form: {
22
27
  operand: {
@@ -95,5 +100,10 @@ export const split_by_expression: NodeConfig = {
95
100
  router: finalRouter,
96
101
  exits: exits
97
102
  };
98
- }
103
+ },
104
+
105
+ // Localization support for categories
106
+ localizable: 'categories',
107
+ toLocalizationFormData: categoriesToLocalizationFormData,
108
+ fromLocalizationFormData: localizationFormDataToCategories
99
109
  };
@@ -1,7 +1,11 @@
1
- import { SPLIT_GROUPS, FormData, NodeConfig } from '../types';
1
+ import { SPLIT_GROUPS, FormData, NodeConfig, FlowTypes } from '../types';
2
2
  import { Node, Category, Exit, Case } from '../../store/flow-definition.d';
3
3
  import { generateUUID } from '../../utils';
4
- import { resultNameField } from './shared';
4
+ import {
5
+ resultNameField,
6
+ categoriesToLocalizationFormData,
7
+ localizationFormDataToCategories
8
+ } from './shared';
5
9
 
6
10
  // Helper function to create a switch router with group cases
7
11
  const createGroupRouter = (
@@ -94,6 +98,7 @@ export const split_by_groups: NodeConfig = {
94
98
  type: 'split_by_groups',
95
99
  name: 'Split by Group',
96
100
  group: SPLIT_GROUPS.split,
101
+ flowTypes: [FlowTypes.VOICE, FlowTypes.MESSAGE, FlowTypes.BACKGROUND],
97
102
  form: {
98
103
  groups: {
99
104
  type: 'select',
@@ -197,5 +202,10 @@ export const split_by_groups: NodeConfig = {
197
202
  router: {
198
203
  type: 'switch',
199
204
  operand: '@contact.groups'
200
- }
205
+ },
206
+
207
+ // Localization support for categories
208
+ localizable: 'categories',
209
+ toLocalizationFormData: categoriesToLocalizationFormData,
210
+ fromLocalizationFormData: localizationFormDataToCategories
201
211
  };
@@ -1,7 +1,8 @@
1
- import { NodeConfig, ACTION_GROUPS } from '../types';
1
+ import { NodeConfig, ACTION_GROUPS, FlowTypes } from '../types';
2
2
 
3
3
  export const split_by_llm_categorize: NodeConfig = {
4
4
  type: 'split_by_intent',
5
5
  name: 'Call classifier',
6
- group: ACTION_GROUPS.services
6
+ group: ACTION_GROUPS.services,
7
+ flowTypes: [FlowTypes.VOICE, FlowTypes.MESSAGE, FlowTypes.BACKGROUND]
7
8
  };
@@ -1,12 +1,24 @@
1
- import { ACTION_GROUPS, FormData, NodeConfig } from '../types';
1
+ import {
2
+ ACTION_GROUPS,
3
+ FormData,
4
+ NodeConfig,
5
+ FlowTypes,
6
+ Features
7
+ } from '../types';
2
8
  import { CallLLM, Node } from '../../store/flow-definition';
3
9
  import { generateUUID, createSuccessFailureRouter } from '../../utils';
4
10
  import { html } from 'lit';
11
+ import {
12
+ categoriesToLocalizationFormData,
13
+ localizationFormDataToCategories
14
+ } from './shared';
5
15
 
6
16
  export const split_by_llm: NodeConfig = {
7
17
  type: 'split_by_llm',
8
18
  name: 'Call AI',
9
19
  group: ACTION_GROUPS.services,
20
+ flowTypes: [FlowTypes.VOICE, FlowTypes.MESSAGE, FlowTypes.BACKGROUND],
21
+ features: [Features.AI],
10
22
  showAsAction: true,
11
23
  render: (node: Node) => {
12
24
  const callLlmAction = node.actions?.find(
@@ -117,5 +129,10 @@ export const split_by_llm: NodeConfig = {
117
129
  router: router,
118
130
  exits: exits
119
131
  };
120
- }
132
+ },
133
+
134
+ // Localization support for categories
135
+ localizable: 'categories',
136
+ toLocalizationFormData: categoriesToLocalizationFormData,
137
+ fromLocalizationFormData: localizationFormDataToCategories
121
138
  };