@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,7 +1,7 @@
1
1
  import { html, TemplateResult, css } from 'lit';
2
2
  import { property, state } from 'lit/decorators.js';
3
3
  import { RapidElement } from '../RapidElement';
4
- import { Node, NodeUI, Action } from '../store/flow-definition';
4
+ import { Node, NodeUI, Action, FlowDefinition } from '../store/flow-definition';
5
5
  import {
6
6
  ValidationResult,
7
7
  NodeConfig,
@@ -22,6 +22,8 @@ import { CustomEventType } from '../interfaces';
22
22
  import { generateUUID } from '../utils';
23
23
  import { FieldRenderer } from '../form/FieldRenderer';
24
24
  import { renderMarkdownInline } from '../markdown';
25
+ import { AppState, fromStore, zustand } from '../store/AppState';
26
+ import { getStore } from '../store/Store';
25
27
 
26
28
  export class NodeEditor extends RapidElement {
27
29
  static get styles() {
@@ -319,6 +321,54 @@ export class NodeEditor extends RapidElement {
319
321
  .optional-field-link a:hover {
320
322
  text-decoration: underline;
321
323
  }
324
+
325
+ .original-value {
326
+ background: #fff8dc;
327
+ padding: 10px;
328
+ border-radius: 4px;
329
+ font-size: 13px;
330
+ color: #666;
331
+ }
332
+
333
+ .original-value-content {
334
+ color: #333;
335
+ white-space: pre-wrap;
336
+ word-break: break-word;
337
+ }
338
+
339
+ .category-localization-table {
340
+ width: 100%;
341
+ display: flex;
342
+ flex-direction: column;
343
+ gap: 5px;
344
+ }
345
+
346
+ .category-localization-row {
347
+ display: grid;
348
+ grid-template-columns: 40% 60%;
349
+ }
350
+
351
+ .category-localization-row:last-child {
352
+ border-bottom: none;
353
+ }
354
+
355
+ .original-name {
356
+ padding: 10px 20px;
357
+ background: #fff8dc;
358
+ display: flex;
359
+ align-items: center;
360
+ border-radius: var(--curvature);
361
+ }
362
+
363
+ .localized-name {
364
+ padding: 10px;
365
+ display: flex;
366
+ align-items: center;
367
+ }
368
+
369
+ .localized-name temba-textinput {
370
+ width: 100%;
371
+ }
322
372
  `;
323
373
  }
324
374
 
@@ -352,6 +402,15 @@ export class NodeEditor extends RapidElement {
352
402
  @state()
353
403
  private revealedOptionalFields: Set<string> = new Set();
354
404
 
405
+ @fromStore(zustand, (state: AppState) => state.languageCode)
406
+ private languageCode!: string;
407
+
408
+ @fromStore(zustand, (state: AppState) => state.isTranslating)
409
+ private isTranslating!: boolean;
410
+
411
+ @fromStore(zustand, (state: AppState) => state.flowDefinition)
412
+ private flowDefinition!: FlowDefinition;
413
+
355
414
  connectedCallback(): void {
356
415
  super.connectedCallback();
357
416
  this.initializeFormData();
@@ -392,7 +451,23 @@ export class NodeEditor extends RapidElement {
392
451
  // Action editing mode - use action config
393
452
  const actionConfig = ACTION_CONFIG[this.action.type];
394
453
 
395
- if (actionConfig?.toFormData) {
454
+ // Check if we're in localization mode
455
+ if (
456
+ this.isTranslating &&
457
+ actionConfig?.localizable &&
458
+ actionConfig.toLocalizationFormData
459
+ ) {
460
+ // Get localized values for this action
461
+ const localization =
462
+ this.flowDefinition?.localization?.[this.languageCode]?.[
463
+ this.action.uuid
464
+ ] || {};
465
+
466
+ this.formData = actionConfig.toLocalizationFormData(
467
+ this.action,
468
+ localization
469
+ );
470
+ } else if (actionConfig?.toFormData) {
396
471
  this.formData = actionConfig.toFormData(this.action);
397
472
  } else {
398
473
  this.formData = { ...this.action };
@@ -406,7 +481,22 @@ export class NodeEditor extends RapidElement {
406
481
  } else if (this.node) {
407
482
  // Node editing mode - use node config
408
483
  const nodeConfig = this.getNodeConfig();
409
- if (nodeConfig?.toFormData) {
484
+
485
+ // Check if we're in localization mode for a node with localizable categories
486
+ if (
487
+ this.isTranslating &&
488
+ nodeConfig?.localizable === 'categories' &&
489
+ nodeConfig.toLocalizationFormData
490
+ ) {
491
+ // Get localized values for this node's categories
492
+ const localization =
493
+ this.flowDefinition?.localization?.[this.languageCode] || {};
494
+
495
+ this.formData = nodeConfig.toLocalizationFormData(
496
+ this.node,
497
+ localization
498
+ );
499
+ } else if (nodeConfig?.toFormData) {
410
500
  this.formData = nodeConfig.toFormData(this.node, this.nodeUI);
411
501
  } else {
412
502
  this.formData = { ...this.node };
@@ -551,19 +641,93 @@ export class NodeEditor extends RapidElement {
551
641
  }
552
642
 
553
643
  private handleSave(): void {
554
- // Validate the form
555
- const validation = this.validateForm();
556
- if (!validation.valid) {
557
- this.errors = validation.errors;
644
+ // Process form data first
645
+ const processedFormData = this.processFormDataForSave();
558
646
 
559
- // Expand any groups that contain validation errors
560
- this.expandGroupsWithErrors(validation.errors);
647
+ // Skip validation if we're in localization mode
648
+ // (localization only deals with translating text, not changing structure)
649
+ if (!this.isTranslating) {
650
+ // Validate the form
651
+ const validation = this.validateForm();
652
+ if (!validation.valid) {
653
+ this.errors = validation.errors;
561
654
 
562
- return;
655
+ // Expand any groups that contain validation errors
656
+ this.expandGroupsWithErrors(validation.errors);
657
+
658
+ return;
659
+ }
563
660
  }
564
661
 
565
- // Process form data to convert key-value arrays to Records before saving
566
- const processedFormData = this.processFormDataForSave();
662
+ // Check if we're in localization mode
663
+ if (this.isTranslating) {
664
+ // Handle action localization
665
+ if (this.action) {
666
+ const actionConfig = ACTION_CONFIG[this.action.type];
667
+
668
+ if (
669
+ actionConfig?.localizable &&
670
+ actionConfig.fromLocalizationFormData
671
+ ) {
672
+ // Save to localization structure
673
+ const localizationData = actionConfig.fromLocalizationFormData(
674
+ processedFormData,
675
+ this.action
676
+ );
677
+
678
+ // Update the flow definition's localization
679
+ this.updateLocalization(
680
+ this.languageCode,
681
+ this.action.uuid,
682
+ localizationData
683
+ );
684
+
685
+ // Close the dialog
686
+ this.fireCustomEvent(CustomEventType.NodeEditCancelled, {});
687
+ return;
688
+ }
689
+ }
690
+
691
+ // Handle node localization (for router categories)
692
+ if (this.node) {
693
+ const nodeConfig = this.getNodeConfig();
694
+
695
+ if (
696
+ nodeConfig?.localizable === 'categories' &&
697
+ nodeConfig.fromLocalizationFormData
698
+ ) {
699
+ // Get localization data for all categories
700
+ const localizationData = nodeConfig.fromLocalizationFormData(
701
+ processedFormData,
702
+ this.node
703
+ );
704
+
705
+ const languageLocalization =
706
+ this.flowDefinition?.localization?.[this.languageCode] || {};
707
+ const categories = this.node?.router?.categories || [];
708
+
709
+ categories.forEach((category) => {
710
+ const categoryUuid = category.uuid;
711
+ const nextLocalization = localizationData[categoryUuid];
712
+
713
+ if (nextLocalization) {
714
+ this.updateLocalization(
715
+ this.languageCode,
716
+ categoryUuid,
717
+ nextLocalization
718
+ );
719
+ } else if (languageLocalization[categoryUuid]) {
720
+ // Remove existing localization when the translation was cleared
721
+ this.updateLocalization(this.languageCode, categoryUuid, {});
722
+ }
723
+ });
724
+
725
+ // Close the dialog
726
+ this.fireCustomEvent(CustomEventType.NodeEditCancelled, {});
727
+ return;
728
+ }
729
+ }
730
+ }
567
731
 
568
732
  // Determine whether to use node or action saving based on context
569
733
  // If we have a node with a router, always use node saving (even if action is set)
@@ -605,6 +769,17 @@ export class NodeEditor extends RapidElement {
605
769
  }
606
770
  }
607
771
 
772
+ private updateLocalization(
773
+ languageCode: string,
774
+ actionUuid: string,
775
+ localizationData: Record<string, any>
776
+ ): void {
777
+ // Use the store method to properly update localization with immer
778
+ zustand
779
+ .getState()
780
+ .updateLocalization(languageCode, actionUuid, localizationData);
781
+ }
782
+
608
783
  private processFormDataForSave(): FormData {
609
784
  const processed = { ...this.formData };
610
785
 
@@ -664,8 +839,9 @@ export class NodeEditor extends RapidElement {
664
839
  }
665
840
  }
666
841
 
667
- // Check required fields
842
+ // Check required fields (skip in localization mode since all fields are optional)
668
843
  if (
844
+ !this.isTranslating &&
669
845
  (fieldConfig as any).required &&
670
846
  (!value || (Array.isArray(value) && value.length === 0))
671
847
  ) {
@@ -1114,6 +1290,12 @@ export class NodeEditor extends RapidElement {
1114
1290
  ? `max-width: ${config.maxWidth};`
1115
1291
  : '';
1116
1292
 
1293
+ // Render original value if in localization mode and action has the field
1294
+ const originalValueDisplay =
1295
+ this.isTranslating && this.action && fieldName in this.action
1296
+ ? this.renderOriginalValue(fieldName, this.action[fieldName])
1297
+ : html``;
1298
+
1117
1299
  const fieldContent = this.renderFieldContent(
1118
1300
  fieldName,
1119
1301
  config,
@@ -1121,12 +1303,14 @@ export class NodeEditor extends RapidElement {
1121
1303
  errors
1122
1304
  );
1123
1305
 
1306
+ const content = html` ${originalValueDisplay} ${fieldContent} `;
1307
+
1124
1308
  // Wrap in container with style if maxWidth is specified
1125
1309
  if (containerStyle) {
1126
- return html`<div style="${containerStyle}">${fieldContent}</div>`;
1310
+ return html`<div style="${containerStyle}">${content}</div>`;
1127
1311
  }
1128
1312
 
1129
- return fieldContent;
1313
+ return content;
1130
1314
  }
1131
1315
 
1132
1316
  private renderOptionalField(
@@ -1166,14 +1350,104 @@ export class NodeEditor extends RapidElement {
1166
1350
  ]);
1167
1351
  }
1168
1352
 
1353
+ private renderCategoryLocalizationTable(): TemplateResult {
1354
+ const categories = this.formData.categories || {};
1355
+ const categoryEntries = Object.entries(categories);
1356
+
1357
+ if (categoryEntries.length === 0) {
1358
+ return html`<div>No categories to localize</div>`;
1359
+ }
1360
+
1361
+ const languageName = getStore().getLanguageName(this.languageCode);
1362
+
1363
+ return html`
1364
+ <div class="category-localization-table">
1365
+ ${categoryEntries.map(
1366
+ ([categoryUuid, categoryData]: [string, any]) => html`
1367
+ <div class="category-localization-row">
1368
+ <div class="original-name">${categoryData.originalName}</div>
1369
+ <div class="localized-name">
1370
+ <temba-textinput
1371
+ name="${categoryUuid}"
1372
+ placeholder="${languageName} Translation"
1373
+ value="${categoryData.localizedName || ''}"
1374
+ @change=${(e: Event) =>
1375
+ this.handleCategoryLocalizationChange(
1376
+ categoryUuid,
1377
+ (e.target as any).value
1378
+ )}
1379
+ ></temba-textinput>
1380
+ </div>
1381
+ </div>
1382
+ `
1383
+ )}
1384
+ </div>
1385
+ `;
1386
+ }
1387
+
1388
+ private handleCategoryLocalizationChange(
1389
+ categoryUuid: string,
1390
+ value: string
1391
+ ): void {
1392
+ // Update formData with new localized value
1393
+ if (!this.formData.categories) {
1394
+ this.formData.categories = {};
1395
+ }
1396
+
1397
+ if (!this.formData.categories[categoryUuid]) {
1398
+ this.formData.categories[categoryUuid] = {};
1399
+ }
1400
+
1401
+ this.formData.categories[categoryUuid].localizedName = value;
1402
+
1403
+ // Trigger a re-render
1404
+ this.requestUpdate();
1405
+ }
1406
+
1407
+ private renderOriginalValue(
1408
+ fieldName: string,
1409
+ originalValue: any
1410
+ ): TemplateResult {
1411
+ // Format the original value for display
1412
+ let displayValue = '';
1413
+
1414
+ if (Array.isArray(originalValue)) {
1415
+ if (originalValue.length === 0) {
1416
+ return html``; // Don't show anything for empty arrays
1417
+ }
1418
+ // For arrays, join with commas
1419
+ displayValue = originalValue.join(', ');
1420
+ } else if (typeof originalValue === 'string') {
1421
+ displayValue = originalValue;
1422
+ } else if (originalValue) {
1423
+ displayValue = String(originalValue);
1424
+ }
1425
+
1426
+ // Don't show if empty
1427
+ if (!displayValue || displayValue.trim() === '') {
1428
+ return html``;
1429
+ }
1430
+
1431
+ return html`
1432
+ <div class="original-value">
1433
+ <div class="original-value-content">${displayValue}</div>
1434
+ </div>
1435
+ `;
1436
+ }
1437
+
1169
1438
  private renderFieldContent(
1170
1439
  fieldName: string,
1171
1440
  config: FieldConfig,
1172
1441
  value: any,
1173
1442
  errors: string[]
1174
1443
  ): TemplateResult {
1444
+ // In localization mode, make all fields optional (not required)
1445
+ const fieldConfig = this.isTranslating
1446
+ ? { ...config, required: false }
1447
+ : config;
1448
+
1175
1449
  // Use FieldRenderer for consistent field rendering
1176
- return FieldRenderer.renderField(fieldName, config, value, {
1450
+ return FieldRenderer.renderField(fieldName, fieldConfig, value, {
1177
1451
  errors,
1178
1452
  onChange: (e: Event) => {
1179
1453
  // Handle different change event types
@@ -1613,6 +1887,15 @@ export class NodeEditor extends RapidElement {
1613
1887
  return html` <div>No configuration available</div> `;
1614
1888
  }
1615
1889
 
1890
+ // Special rendering for category localization
1891
+ if (
1892
+ this.isTranslating &&
1893
+ config.localizable === 'categories' &&
1894
+ this.formData.categories
1895
+ ) {
1896
+ return this.renderCategoryLocalizationTable();
1897
+ }
1898
+
1616
1899
  // Use the new fields configuration system
1617
1900
  if (config.form) {
1618
1901
  // If layout is specified, use it
@@ -1675,6 +1958,11 @@ export class NodeEditor extends RapidElement {
1675
1958
  return html``;
1676
1959
  }
1677
1960
 
1961
+ // Don't show gutter when localizing categories
1962
+ if (this.isTranslating && config.localizable === 'categories') {
1963
+ return html``;
1964
+ }
1965
+
1678
1966
  // Use the same layout rendering system for gutter fields
1679
1967
  const renderedFields = new Set<string>();
1680
1968
 
@@ -1756,18 +2044,28 @@ export class NodeEditor extends RapidElement {
1756
2044
  return html``;
1757
2045
  }
1758
2046
 
1759
- const headerColor = this.getHeaderColor();
2047
+ const headerColor = this.isTranslating ? '#505050' : this.getHeaderColor();
2048
+ const headerTextColor = this.isTranslating ? '#fff' : '#fff';
1760
2049
  const config = this.getConfig();
1761
2050
  const dialogSize = config?.dialogSize || 'medium'; // Default to 'large' if not specified
1762
2051
 
2052
+ const languageName = this.isTranslating
2053
+ ? getStore().getLanguageName(this.languageCode)
2054
+ : '';
2055
+
2056
+ let header = config?.name || 'Edit';
2057
+ if (this.isTranslating) {
2058
+ header = languageName ? `${languageName} - ${header}` : header;
2059
+ }
2060
+
1763
2061
  return html`
1764
2062
  <temba-dialog
1765
- header="${config?.name || 'Edit'}"
2063
+ header="${header}"
1766
2064
  .open="${this.isOpen}"
1767
2065
  @temba-button-clicked=${this.handleDialogButtonClick}
1768
2066
  primaryButtonName="Save"
1769
2067
  cancelButtonName="Cancel"
1770
- style="--header-bg: ${headerColor}"
2068
+ style="--header-bg: ${headerColor}; --header-text: ${headerTextColor};"
1771
2069
  size="${dialogSize}"
1772
2070
  >
1773
2071
  <div class="node-editor-form">
@@ -241,6 +241,12 @@ export class NodeTypeSelector extends RapidElement {
241
241
  @property({ type: String })
242
242
  public mode: 'action' | 'split' | 'action-no-branching' = 'action';
243
243
 
244
+ @property({ type: String })
245
+ public flowType: string = 'message';
246
+
247
+ @property({ type: Array })
248
+ public features: string[] = [];
249
+
244
250
  @state()
245
251
  private clickPosition = { x: 0, y: 0 };
246
252
 
@@ -257,6 +263,35 @@ export class NodeTypeSelector extends RapidElement {
257
263
  this.open = false;
258
264
  }
259
265
 
266
+ /**
267
+ * Check if a config is available for the current flow type and features
268
+ */
269
+ private isConfigAvailable(config: NodeConfig | ActionConfig): boolean {
270
+ // Check flow type filter
271
+ if (config.flowTypes !== undefined) {
272
+ // Empty array means not available for any flow type in selector
273
+ if (config.flowTypes.length === 0) {
274
+ return false;
275
+ }
276
+ // Non-empty array means check if current flow type is included
277
+ if (!config.flowTypes.includes(this.flowType as any)) {
278
+ return false;
279
+ }
280
+ }
281
+ // undefined/null flowTypes means available for all flow types
282
+
283
+ // Check features filter - all required features must be present
284
+ if (config.features && config.features.length > 0) {
285
+ for (const requiredFeature of config.features) {
286
+ if (!this.features.includes(requiredFeature)) {
287
+ return false;
288
+ }
289
+ }
290
+ }
291
+
292
+ return true;
293
+ }
294
+
260
295
  private handleNodeTypeClick(nodeType: string) {
261
296
  this.fireCustomEvent(CustomEventType.Selection, {
262
297
  nodeType,
@@ -284,7 +319,12 @@ export class NodeTypeSelector extends RapidElement {
284
319
  // Collect regular actions (from ACTION_CONFIG, unless hideFromActions is true)
285
320
  Object.entries(ACTION_CONFIG)
286
321
  .filter(([_, config]) => {
287
- return config.name && !config.hideFromActions && config.group;
322
+ return (
323
+ config.name &&
324
+ !config.hideFromActions &&
325
+ config.group &&
326
+ this.isConfigAvailable(config)
327
+ );
288
328
  })
289
329
  .forEach(([type, config]) => {
290
330
  const group = config.group;
@@ -303,7 +343,8 @@ export class NodeTypeSelector extends RapidElement {
303
343
  type !== 'execute_actions' &&
304
344
  config.name &&
305
345
  config.showAsAction &&
306
- config.group
346
+ config.group &&
347
+ this.isConfigAvailable(config)
307
348
  );
308
349
  })
309
350
  .forEach(([type, config]) => {
@@ -400,7 +441,10 @@ export class NodeTypeSelector extends RapidElement {
400
441
  // exclude execute_actions (it's the default action-only node)
401
442
  // exclude nodes that have showAsAction=true (they appear in action mode)
402
443
  return (
403
- type !== 'execute_actions' && config.name && !config.showAsAction
444
+ type !== 'execute_actions' &&
445
+ config.name &&
446
+ !config.showAsAction &&
447
+ this.isConfigAvailable(config)
404
448
  );
405
449
  })
406
450
  .forEach(([type, config]) => {
@@ -3,6 +3,7 @@ import { property } from 'lit/decorators.js';
3
3
  import { RapidElement } from '../RapidElement';
4
4
  import { StickyNote as StickyNoteData } from '../store/flow-definition';
5
5
  import { getStore } from '../store/Store';
6
+ import { AppState, fromStore, zustand } from '../store/AppState';
6
7
 
7
8
  export class StickyNote extends RapidElement {
8
9
  @property({ type: String })
@@ -20,6 +21,9 @@ export class StickyNote extends RapidElement {
20
21
  @property({ type: Boolean })
21
22
  private colorPickerExpanded = false;
22
23
 
24
+ @fromStore(zustand, (state: AppState) => state.isTranslating)
25
+ private isTranslating!: boolean;
26
+
23
27
  static get styles() {
24
28
  return css`
25
29
  :host {
@@ -119,6 +123,8 @@ export class StickyNote extends RapidElement {
119
123
  border-top-left-radius: var(--curvature);
120
124
  border-top-right-radius: var(--curvature);
121
125
  flex-grow: 1;
126
+ padding: 4px 8px !important;
127
+ margin: 2px;
122
128
  padding-left: 8px;
123
129
  }
124
130
  .sticky-title:empty::before {
@@ -139,6 +145,7 @@ export class StickyNote extends RapidElement {
139
145
  min-height: 48px;
140
146
  word-wrap: break-word;
141
147
  white-space: pre-wrap;
148
+ margin: 2px;
142
149
  }
143
150
  .sticky-body:empty::before {
144
151
  content: 'Click to add note';
@@ -377,7 +384,7 @@ export class StickyNote extends RapidElement {
377
384
  <temba-icon name="drag" class="drag-handle"></temba-icon>
378
385
  <div
379
386
  class="sticky-title"
380
- contenteditable="true"
387
+ contenteditable="${!this.isTranslating}"
381
388
  @blur="${this.handleTitleBlur}"
382
389
  @keydown="${this.handleKeyDown}"
383
390
  @mousedown="${this.handleContentMouseDown}"
@@ -387,13 +394,15 @@ export class StickyNote extends RapidElement {
387
394
  <div class="sticky-body-container">
388
395
  <div
389
396
  class="sticky-body"
390
- contenteditable="true"
397
+ contenteditable="${!this.isTranslating}"
391
398
  @blur="${this.handleBodyBlur}"
392
399
  @keydown="${this.handleKeyDown}"
393
400
  @mousedown="${this.handleContentMouseDown}"
394
401
  .textContent="${this.data.body}"
395
402
  ></div>
396
- <div class="edit-icon" title="Edit note"></div>
403
+ ${!this.isTranslating
404
+ ? html`<div class="edit-icon" title="Edit note"></div>`
405
+ : ''}
397
406
 
398
407
  <!-- Color picker -->
399
408
  <div
@@ -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, AddToGroup } from '../../store/flow-definition';
4
4
  import { renderNamedObjects } from '../utils';
5
5
 
6
6
  export const add_contact_groups: ActionConfig = {
7
7
  name: 'Add to Group',
8
8
  group: ACTION_GROUPS.contacts,
9
+ flowTypes: [FlowTypes.VOICE, FlowTypes.MESSAGE, FlowTypes.BACKGROUND],
9
10
  render: (_node: Node, action: AddToGroup) => {
10
11
  return html`<div>${renderNamedObjects(action.groups, 'group')}</div>`;
11
12
  },
@@ -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, AddContactUrn } from '../../store/flow-definition';
9
10
  import { SCHEMES } from '../utils';
@@ -11,6 +12,7 @@ import { SCHEMES } from '../utils';
11
12
  export const add_contact_urn: ActionConfig = {
12
13
  name: 'Add URN',
13
14
  group: ACTION_GROUPS.contacts,
15
+ flowTypes: [FlowTypes.VOICE, FlowTypes.MESSAGE, FlowTypes.BACKGROUND],
14
16
  render: (_node: Node, action: AddContactUrn) => {
15
17
  const schemeObj = SCHEMES.find((s) => s.scheme === action.scheme);
16
18
  const friendlyScheme = schemeObj?.path || action.scheme;
@@ -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, AddInputLabels } from '../../store/flow-definition';
4
4
  import { renderNamedObjects } from '../utils';
5
5
 
6
6
  export const add_input_labels: ActionConfig = {
7
7
  name: 'Add Input Labels',
8
8
  group: ACTION_GROUPS.save,
9
+ flowTypes: [FlowTypes.VOICE, FlowTypes.MESSAGE, FlowTypes.BACKGROUND],
9
10
  render: (_node: Node, action: AddInputLabels) => {
10
11
  return html`<div>${renderNamedObjects(action.labels, 'label')}</div>`;
11
12
  },
@@ -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, PlayAudio } from '../../store/flow-definition';
4
4
 
5
5
  export const play_audio: ActionConfig = {
6
6
  name: 'Play Audio',
7
7
  group: ACTION_GROUPS.send,
8
+ flowTypes: [FlowTypes.VOICE],
8
9
  render: (_node: Node, _action: PlayAudio) => {
9
10
  // This will need to be implemented based on the actual render logic
10
11
  return html`<div>Play Audio</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, RemoveFromGroup } from '../../store/flow-definition';
9
10
  import { renderNamedObjects } from '../utils';
@@ -11,6 +12,7 @@ import { renderNamedObjects } from '../utils';
11
12
  export const remove_contact_groups: ActionConfig = {
12
13
  name: 'Remove from Group',
13
14
  group: ACTION_GROUPS.contacts,
15
+ flowTypes: [FlowTypes.VOICE, FlowTypes.MESSAGE, FlowTypes.BACKGROUND],
14
16
  render: (_node: Node, action: RemoveFromGroup) => {
15
17
  if (action.all_groups) {
16
18
  return html`<div>Remove from all groups</div>`;
@@ -5,6 +5,7 @@ import { Node, RequestOptin } from '../../store/flow-definition';
5
5
  export const request_optin: ActionConfig = {
6
6
  name: 'Request Opt-in',
7
7
  group: ACTION_GROUPS.send,
8
+ flowTypes: [],
8
9
  render: (_node: Node, _action: RequestOptin) => {
9
10
  // This will need to be implemented based on the actual render logic
10
11
  return html`<div>Request Opt-in</div>`;