@nyaruka/temba-components 0.156.9 → 0.156.10

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 (58) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/dist/temba-components.js +568 -521
  3. package/dist/temba-components.js.map +1 -1
  4. package/package.json +1 -1
  5. package/src/display/Chat.ts +8 -8
  6. package/src/display/FloatingTab.ts +2 -2
  7. package/src/display/Options.ts +8 -2
  8. package/src/flow/CanvasMenu.ts +20 -25
  9. package/src/flow/CanvasNode.ts +16 -12
  10. package/src/flow/DragManager.ts +93 -33
  11. package/src/flow/Editor.ts +59 -54
  12. package/src/flow/EditorToolbar.ts +19 -20
  13. package/src/flow/FlowSearch.ts +9 -7
  14. package/src/flow/MessageTable.ts +181 -74
  15. package/src/flow/NodeEditor.ts +55 -72
  16. package/src/flow/RevisionsWindow.ts +2 -4
  17. package/src/flow/ZoomManager.ts +1 -2
  18. package/src/flow/actions/play_audio.ts +1 -28
  19. package/src/flow/actions/say_msg.ts +1 -40
  20. package/src/flow/actions/send_broadcast.ts +1 -2
  21. package/src/flow/actions/send_email.ts +5 -56
  22. package/src/flow/actions/send_msg.ts +10 -2
  23. package/src/flow/actions/start_session.ts +1 -2
  24. package/src/flow/categoryLocalization.ts +1 -5
  25. package/src/flow/categoryUtils.ts +139 -0
  26. package/src/flow/nodes/shared-rules.ts +6 -16
  27. package/src/flow/nodes/shared.ts +113 -6
  28. package/src/flow/nodes/split_by_airtime.ts +41 -63
  29. package/src/flow/nodes/split_by_contact_field.ts +8 -17
  30. package/src/flow/nodes/split_by_expression.ts +8 -17
  31. package/src/flow/nodes/split_by_groups.ts +34 -112
  32. package/src/flow/nodes/split_by_llm.ts +1 -7
  33. package/src/flow/nodes/split_by_llm_categorize.ts +27 -43
  34. package/src/flow/nodes/split_by_random.ts +39 -99
  35. package/src/flow/nodes/split_by_resthook.ts +5 -19
  36. package/src/flow/nodes/split_by_run_result.ts +8 -17
  37. package/src/flow/nodes/split_by_scheme.ts +39 -124
  38. package/src/flow/nodes/split_by_subflow.ts +1 -7
  39. package/src/flow/nodes/split_by_ticket.ts +1 -7
  40. package/src/flow/nodes/split_by_webhook.ts +2 -8
  41. package/src/flow/nodes/wait_for_audio.ts +1 -7
  42. package/src/flow/nodes/wait_for_dial.ts +2 -8
  43. package/src/flow/nodes/wait_for_digits.ts +5 -7
  44. package/src/flow/nodes/wait_for_menu.ts +5 -7
  45. package/src/flow/nodes/wait_for_response.ts +10 -18
  46. package/src/flow/types.ts +27 -0
  47. package/src/flow/utils.ts +111 -3
  48. package/src/form/Compose.ts +11 -4
  49. package/src/form/MessageEditor.ts +5 -3
  50. package/src/form/RichEditor.ts +3 -1
  51. package/src/form/TemplateEditor.ts +5 -1
  52. package/src/form/select/Select.ts +11 -9
  53. package/src/layout/AccordionSection.ts +9 -3
  54. package/src/layout/Modax.ts +1 -3
  55. package/src/live/ContactChat.ts +54 -46
  56. package/src/simulator/Simulator.ts +9 -3
  57. package/src/store/AppState.ts +1 -1
  58. package/src/utils.ts +21 -16
@@ -87,7 +87,9 @@ export class MessageTable extends RapidElement {
87
87
  padding: 2px 16px 2px 26px;
88
88
  }
89
89
 
90
- .message-table tr.localization-paired-row:not(.localization-paired-last) td {
90
+ .message-table
91
+ tr.localization-paired-row:not(.localization-paired-last)
92
+ td {
91
93
  border-bottom: none;
92
94
  }
93
95
 
@@ -278,11 +280,13 @@ export class MessageTable extends RapidElement {
278
280
  transition: background 0.15s;
279
281
  }
280
282
 
281
- .translation-cell.category-translation-cell:hover .category-translation-item {
283
+ .translation-cell.category-translation-cell:hover
284
+ .category-translation-item {
282
285
  background: #f5f8ff;
283
286
  }
284
287
 
285
- .translation-cell.category-translation-cell:hover .category-translation-item.missing {
288
+ .translation-cell.category-translation-cell:hover
289
+ .category-translation-item.missing {
286
290
  color: #9bb8df;
287
291
  }
288
292
 
@@ -399,14 +403,24 @@ export class MessageTable extends RapidElement {
399
403
  const nodeType = nodeUI?.type;
400
404
 
401
405
  // Collect rules that have localized values or are flagged for localization
402
- const langLocalization = this.definition?.localization?.[this.languageCode] || {};
403
- let rules: Array<{ uuid: string; type: string; arguments: string[] }> = [];
406
+ const langLocalization =
407
+ this.definition?.localization?.[this.languageCode] || {};
408
+ let rules: Array<{ uuid: string; type: string; arguments: string[] }> =
409
+ [];
404
410
  if (node.router?.cases?.length) {
405
411
  const hasLocalizeRules = nodeUI?.config?.localizeRules;
406
412
  rules = node.router.cases
407
413
  .filter((c) => c.arguments?.length > 0 && c.arguments.some((a) => a))
408
- .filter((c) => hasLocalizeRules || langLocalization[c.uuid]?.arguments?.some((a: string) => a))
409
- .map((c) => ({ uuid: c.uuid, type: c.type, arguments: [...c.arguments] }));
414
+ .filter(
415
+ (c) =>
416
+ hasLocalizeRules ||
417
+ langLocalization[c.uuid]?.arguments?.some((a: string) => a)
418
+ )
419
+ .map((c) => ({
420
+ uuid: c.uuid,
421
+ type: c.type,
422
+ arguments: [...c.arguments]
423
+ }));
410
424
  }
411
425
 
412
426
  // Collect categories that have localized values or are flagged for localization
@@ -424,8 +438,8 @@ export class MessageTable extends RapidElement {
424
438
  if (hasLocalizeCategories) {
425
439
  categories = translatableCategories;
426
440
  } else {
427
- categories = translatableCategories.filter(
428
- (cat) => langLocalization[cat.uuid]?.name?.some((n: string) => n)
441
+ categories = translatableCategories.filter((cat) =>
442
+ langLocalization[cat.uuid]?.name?.some((n: string) => n)
429
443
  );
430
444
  }
431
445
  }
@@ -492,9 +506,7 @@ export class MessageTable extends RapidElement {
492
506
  }
493
507
 
494
508
  return localization.quick_replies
495
- .map((reply: unknown) =>
496
- typeof reply === 'string' ? reply.trim() : ''
497
- )
509
+ .map((reply: unknown) => (typeof reply === 'string' ? reply.trim() : ''))
498
510
  .filter((reply: string) => reply.length > 0);
499
511
  }
500
512
 
@@ -568,7 +580,10 @@ export class MessageTable extends RapidElement {
568
580
  // Count string-type localizable fields (exclude arrays like attachments, quick_replies)
569
581
  const textFields = config.localizable.filter((key) => {
570
582
  const fieldConfig = config.form?.[key];
571
- return fieldConfig && (fieldConfig.type === 'text' || fieldConfig.type === 'textarea');
583
+ return (
584
+ fieldConfig &&
585
+ (fieldConfig.type === 'text' || fieldConfig.type === 'textarea')
586
+ );
572
587
  });
573
588
  return textFields.length > 1;
574
589
  }
@@ -587,11 +602,15 @@ export class MessageTable extends RapidElement {
587
602
  return config.localizable
588
603
  .filter((key) => {
589
604
  const fieldConfig = config.form?.[key];
590
- return fieldConfig && (fieldConfig.type === 'text' || fieldConfig.type === 'textarea');
605
+ return (
606
+ fieldConfig &&
607
+ (fieldConfig.type === 'text' || fieldConfig.type === 'textarea')
608
+ );
591
609
  })
592
610
  .map((key) => {
593
611
  const fieldConfig = config.form![key];
594
- const label = typeof fieldConfig.label === 'string' ? fieldConfig.label : key;
612
+ const label =
613
+ typeof fieldConfig.label === 'string' ? fieldConfig.label : key;
595
614
  return {
596
615
  key,
597
616
  label,
@@ -609,7 +628,8 @@ export class MessageTable extends RapidElement {
609
628
  if (colonIdx < 0) return html``;
610
629
  const contentType = att.substring(0, colonIdx);
611
630
  const baseType = contentType.split('/')[0];
612
- const iconName = MessageTable.ATTACHMENT_ICONS[baseType] || Icon.attachment;
631
+ const iconName =
632
+ MessageTable.ATTACHMENT_ICONS[baseType] || Icon.attachment;
613
633
  return html`<div class="attachment-icon">
614
634
  <temba-icon name="${iconName}"></temba-icon>
615
635
  </div>`;
@@ -628,10 +648,17 @@ export class MessageTable extends RapidElement {
628
648
  if (config?.form) {
629
649
  for (const key of localizable) {
630
650
  const fc = config.form[key];
631
- if (fc && (fc.type === 'text' || fc.type === 'textarea' || fc.type === 'message-editor')) {
651
+ if (
652
+ fc &&
653
+ (fc.type === 'text' ||
654
+ fc.type === 'textarea' ||
655
+ fc.type === 'message-editor')
656
+ ) {
632
657
  const text = action[key] || '';
633
658
  if (text) {
634
- parts.push(renderHighlightedText(this.stripLeadingLineBreaks(text), true));
659
+ parts.push(
660
+ renderHighlightedText(this.stripLeadingLineBreaks(text), true)
661
+ );
635
662
  }
636
663
  }
637
664
  }
@@ -640,9 +667,15 @@ export class MessageTable extends RapidElement {
640
667
  // Quick replies (send_msg)
641
668
  const quickReplies: string[] = action.quick_replies || [];
642
669
  if (quickReplies.length > 0) {
643
- parts.push(html`<div class="quick-replies ${parts.length === 0 ? 'standalone' : ''}">${quickReplies.map(
644
- (reply) => html`<div class="quick-reply">${reply}</div>`
645
- )}</div>`);
670
+ parts.push(
671
+ html`<div
672
+ class="quick-replies ${parts.length === 0 ? 'standalone' : ''}"
673
+ >
674
+ ${quickReplies.map(
675
+ (reply) => html`<div class="quick-reply">${reply}</div>`
676
+ )}
677
+ </div>`
678
+ );
646
679
  }
647
680
 
648
681
  // Attachments
@@ -669,10 +702,20 @@ export class MessageTable extends RapidElement {
669
702
  if (config?.form) {
670
703
  for (const key of localizable) {
671
704
  const fc = config.form[key];
672
- if (fc && (fc.type === 'text' || fc.type === 'textarea' || fc.type === 'message-editor')) {
705
+ if (
706
+ fc &&
707
+ (fc.type === 'text' ||
708
+ fc.type === 'textarea' ||
709
+ fc.type === 'message-editor')
710
+ ) {
673
711
  const translatedText = this.getTranslatedField(action.uuid, key);
674
712
  if (typeof translatedText === 'string') {
675
- parts.push(renderHighlightedText(this.stripLeadingLineBreaks(translatedText), true));
713
+ parts.push(
714
+ renderHighlightedText(
715
+ this.stripLeadingLineBreaks(translatedText),
716
+ true
717
+ )
718
+ );
676
719
  }
677
720
  }
678
721
  }
@@ -681,13 +724,22 @@ export class MessageTable extends RapidElement {
681
724
  // Translated quick replies
682
725
  const translatedQuickReplies = this.getTranslatedQuickReplies(action.uuid);
683
726
  if (translatedQuickReplies.length > 0) {
684
- parts.push(html`<div class="quick-replies ${parts.length === 0 ? 'standalone' : ''}">${translatedQuickReplies.map(
685
- (reply) => html`<div class="quick-reply">${reply}</div>`
686
- )}</div>`);
727
+ parts.push(
728
+ html`<div
729
+ class="quick-replies ${parts.length === 0 ? 'standalone' : ''}"
730
+ >
731
+ ${translatedQuickReplies.map(
732
+ (reply) => html`<div class="quick-reply">${reply}</div>`
733
+ )}
734
+ </div>`
735
+ );
687
736
  }
688
737
 
689
738
  // Translated attachments
690
- const translatedAttachments = this.getTranslatedArrayField(action.uuid, 'attachments');
739
+ const translatedAttachments = this.getTranslatedArrayField(
740
+ action.uuid,
741
+ 'attachments'
742
+ );
691
743
  if (translatedAttachments.length > 0) {
692
744
  parts.push(this.renderAttachments(translatedAttachments));
693
745
  }
@@ -739,9 +791,7 @@ export class MessageTable extends RapidElement {
739
791
  });
740
792
  }
741
793
 
742
- private getGroupTranslations(
743
- entry: LocalizationGroupEntry
744
- ): Array<{
794
+ private getGroupTranslations(entry: LocalizationGroupEntry): Array<{
745
795
  original: string;
746
796
  translated: string | null;
747
797
  isRule: boolean;
@@ -795,7 +845,10 @@ export class MessageTable extends RapidElement {
795
845
  }
796
846
 
797
847
  private renderPairedRows(
798
- items: Array<{ original: TemplateResult; translated: TemplateResult | null }>,
848
+ items: Array<{
849
+ original: TemplateResult;
850
+ translated: TemplateResult | null;
851
+ }>,
799
852
  entry: TableEntry,
800
853
  handleBaseClick: () => void,
801
854
  handleTranslationClick: () => void,
@@ -805,11 +858,17 @@ export class MessageTable extends RapidElement {
805
858
  ${items.map(
806
859
  (item, idx) => html`
807
860
  <tr
808
- class="category-row localization-paired-row ${idx === 0 ? 'localization-paired-first' : ''} ${idx === items.length - 1 ? 'localization-paired-last' : ''}"
861
+ class="category-row localization-paired-row ${idx === 0
862
+ ? 'localization-paired-first'
863
+ : ''} ${idx === items.length - 1
864
+ ? 'localization-paired-last'
865
+ : ''}"
809
866
  style=${`--node-rail-color: ${this.getEntryRailColor(entry)};`}
810
867
  data-node-uuid=${entry.node.uuid}
811
868
  data-entry-kind=${entry.kind}
812
- data-action-uuid=${entry.kind === 'message' ? entry.action.uuid : ''}
869
+ data-action-uuid=${entry.kind === 'message'
870
+ ? entry.action.uuid
871
+ : ''}
813
872
  >
814
873
  <td>
815
874
  <div
@@ -830,9 +889,16 @@ export class MessageTable extends RapidElement {
830
889
  title="Click to edit translation"
831
890
  >
832
891
  <div
833
- class="category-item category-translation-item ${item.translated !== null ? '' : 'missing'}"
892
+ class="category-item category-translation-item ${item.translated !==
893
+ null
894
+ ? ''
895
+ : 'missing'}"
834
896
  >
835
- <span>${item.translated !== null ? item.translated : 'No translation'}</span>
897
+ <span
898
+ >${item.translated !== null
899
+ ? item.translated
900
+ : 'No translation'}</span
901
+ >
836
902
  </div>
837
903
  </div>
838
904
  </td>`
@@ -880,32 +946,55 @@ export class MessageTable extends RapidElement {
880
946
  const groupTranslations = this.getGroupTranslations(entry);
881
947
  const items = groupTranslations.map((item) => ({
882
948
  original: html`${item.isRule && item.operatorName
883
- ? html`<span class="rule-operator">${item.operatorName}</span> `
949
+ ? html`<span class="rule-operator"
950
+ >${item.operatorName}</span
951
+ > `
884
952
  : ''}${renderHighlightedText(item.original, true)}`,
885
- translated: item.translated !== null
886
- ? html`${item.isRule && item.operatorName
887
- ? html`<span class="rule-operator">${item.operatorName}</span> `
888
- : ''}${renderHighlightedText(item.translated, true)}`
889
- : null
953
+ translated:
954
+ item.translated !== null
955
+ ? html`${item.isRule && item.operatorName
956
+ ? html`<span class="rule-operator"
957
+ >${item.operatorName}</span
958
+ > `
959
+ : ''}${renderHighlightedText(item.translated, true)}`
960
+ : null
890
961
  }));
891
- return this.renderPairedRows(items, entry, handleBaseClick, handleTranslationClickFn, showTranslation);
962
+ return this.renderPairedRows(
963
+ items,
964
+ entry,
965
+ handleBaseClick,
966
+ handleTranslationClickFn,
967
+ showTranslation
968
+ );
892
969
  }
893
970
 
894
971
  // Multi-field actions (e.g. send_email with subject + body) - paired rows
895
- if (
896
- entry.kind === 'message' &&
897
- this.usesPairedRows(entry.action)
898
- ) {
972
+ if (entry.kind === 'message' && this.usesPairedRows(entry.action)) {
899
973
  const fields = this.getPairedFields(entry.action);
900
974
  const items = fields.map((f) => ({
901
975
  original: html`${f.original
902
- ? renderHighlightedText(this.stripLeadingLineBreaks(f.original), true)
903
- : html`<span style="color: #bbb; font-style: italic;">Empty</span>`}`,
904
- translated: f.translated !== null
905
- ? html`${renderHighlightedText(this.stripLeadingLineBreaks(f.translated), true)}`
906
- : null
976
+ ? renderHighlightedText(
977
+ this.stripLeadingLineBreaks(f.original),
978
+ true
979
+ )
980
+ : html`<span style="color: #bbb; font-style: italic;"
981
+ >Empty</span
982
+ >`}`,
983
+ translated:
984
+ f.translated !== null
985
+ ? html`${renderHighlightedText(
986
+ this.stripLeadingLineBreaks(f.translated),
987
+ true
988
+ )}`
989
+ : null
907
990
  }));
908
- return this.renderPairedRows(items, entry, handleBaseClick, handleTranslationClickFn, showTranslation);
991
+ return this.renderPairedRows(
992
+ items,
993
+ entry,
994
+ handleBaseClick,
995
+ handleTranslationClickFn,
996
+ showTranslation
997
+ );
909
998
  }
910
999
 
911
1000
  // Single-row entries (send_msg, send_broadcast, etc.)
@@ -913,9 +1002,9 @@ export class MessageTable extends RapidElement {
913
1002
  entry.kind === 'message' &&
914
1003
  showTranslation &&
915
1004
  this.hasAnyTranslation(entry);
916
- const translationCellClass = `translation-cell ${hasTranslation
917
- ? 'has-translation'
918
- : 'missing-translation'}`;
1005
+ const translationCellClass = `translation-cell ${
1006
+ hasTranslation ? 'has-translation' : 'missing-translation'
1007
+ }`;
919
1008
 
920
1009
  return html`
921
1010
  <tr
@@ -936,23 +1025,41 @@ export class MessageTable extends RapidElement {
936
1025
  ? this.renderOriginalContent(entry)
937
1026
  : isGroupEntry
938
1027
  ? html`
939
- <div class="category-stack category-stack-original">
940
- ${entry.rules.map(
941
- (c) => html`
942
- <div class="category-item category-original-item">
943
- <span><span class="rule-operator">${getOperatorConfig(c.type)?.name || c.type}</span> ${renderHighlightedText(c.arguments.join(', '), true)}</span>
944
- </div>
945
- `
946
- )}
947
- ${entry.categories.map(
948
- (category) => html`
949
- <div class="category-item category-original-item">
950
- <span>${renderHighlightedText(category.name, true)}</span>
951
- </div>
952
- `
953
- )}
954
- </div>
955
- `
1028
+ <div class="category-stack category-stack-original">
1029
+ ${entry.rules.map(
1030
+ (c) => html`
1031
+ <div
1032
+ class="category-item category-original-item"
1033
+ >
1034
+ <span
1035
+ ><span class="rule-operator"
1036
+ >${getOperatorConfig(c.type)?.name ||
1037
+ c.type}</span
1038
+ >
1039
+ ${renderHighlightedText(
1040
+ c.arguments.join(', '),
1041
+ true
1042
+ )}</span
1043
+ >
1044
+ </div>
1045
+ `
1046
+ )}
1047
+ ${entry.categories.map(
1048
+ (category) => html`
1049
+ <div
1050
+ class="category-item category-original-item"
1051
+ >
1052
+ <span
1053
+ >${renderHighlightedText(
1054
+ category.name,
1055
+ true
1056
+ )}</span
1057
+ >
1058
+ </div>
1059
+ `
1060
+ )}
1061
+ </div>
1062
+ `
956
1063
  : ''}
957
1064
  </div>
958
1065
  </td>
@@ -964,8 +1071,8 @@ export class MessageTable extends RapidElement {
964
1071
  title="Click to edit translation"
965
1072
  >
966
1073
  ${entry.kind === 'message'
967
- ? this.renderTranslatedContent(entry)
968
- : 'No translation'}
1074
+ ? this.renderTranslatedContent(entry)
1075
+ : 'No translation'}
969
1076
  </div>
970
1077
  </td>`
971
1078
  : ''}
@@ -21,8 +21,13 @@ import {
21
21
  } from './types';
22
22
  import { CustomEventType } from '../interfaces';
23
23
  import { generateUUID } from '../utils';
24
- import { formatIssueMessage } from './utils';
24
+ import {
25
+ formatIssueMessage,
26
+ resolveToLocalizationFormData,
27
+ resolveFromLocalizationFormData
28
+ } from './utils';
25
29
  import { getTranslatableCategoriesForNode } from './categoryLocalization';
30
+ import { collectReservedCategoryErrors } from './categoryUtils';
26
31
  import { FieldRenderer } from '../form/FieldRenderer';
27
32
  import { renderMarkdownInline } from '../markdown';
28
33
  import {
@@ -587,21 +592,17 @@ export class NodeEditor extends RapidElement {
587
592
  const actionConfig = ACTION_CONFIG[this.action.type];
588
593
 
589
594
  // Check if we're in localization mode
590
- if (
591
- this.isTranslating &&
592
- actionConfig?.localizable &&
593
- actionConfig.toLocalizationFormData
594
- ) {
595
+ const actionToLocalization = actionConfig
596
+ ? resolveToLocalizationFormData(actionConfig)
597
+ : undefined;
598
+ if (this.isTranslating && actionToLocalization) {
595
599
  // Get localized values for this action
596
600
  const localization =
597
601
  this.flowDefinition?.localization?.[this.languageCode]?.[
598
602
  this.action.uuid
599
603
  ] || {};
600
604
 
601
- this.formData = actionConfig.toLocalizationFormData(
602
- this.action,
603
- localization
604
- );
605
+ this.formData = actionToLocalization(this.action, localization);
605
606
  } else if (actionConfig?.toFormData) {
606
607
  this.formData = actionConfig.toFormData(this.action);
607
608
  } else {
@@ -618,19 +619,19 @@ export class NodeEditor extends RapidElement {
618
619
  const nodeConfig = this.getNodeConfig();
619
620
 
620
621
  // Check if we're in localization mode for a node with localizable categories
622
+ const nodeToLocalization = nodeConfig
623
+ ? resolveToLocalizationFormData(nodeConfig)
624
+ : undefined;
621
625
  if (
622
626
  this.isTranslating &&
623
627
  nodeConfig?.localizable === 'categories' &&
624
- nodeConfig.toLocalizationFormData
628
+ nodeToLocalization
625
629
  ) {
626
630
  // Get localized values for this node's categories
627
631
  const localization =
628
632
  this.flowDefinition?.localization?.[this.languageCode] || {};
629
633
 
630
- this.formData = nodeConfig.toLocalizationFormData(
631
- this.node,
632
- localization
633
- );
634
+ this.formData = nodeToLocalization(this.node, localization);
634
635
 
635
636
  const translatableCategoryUuids = new Set(
636
637
  getTranslatableCategoriesForNode(
@@ -648,8 +649,8 @@ export class NodeEditor extends RapidElement {
648
649
  this.formData = {
649
650
  ...this.formData,
650
651
  categories: Object.fromEntries(
651
- Object.entries(this.formData.categories).filter(([categoryUuid]) =>
652
- translatableCategoryUuids.has(categoryUuid)
652
+ Object.entries(this.formData.categories).filter(
653
+ ([categoryUuid]) => translatableCategoryUuids.has(categoryUuid)
653
654
  )
654
655
  )
655
656
  };
@@ -690,9 +691,7 @@ export class NodeEditor extends RapidElement {
690
691
  this.flowInfo?.results &&
691
692
  !this.flowDefinition?._ui?.nodes?.[this.node.uuid]
692
693
  ) {
693
- const existingNames = new Set(
694
- this.flowInfo.results.map((r) => r.name)
695
- );
694
+ const existingNames = new Set(this.flowInfo.results.map((r) => r.name));
696
695
  let candidate = 'Result';
697
696
  let i = 2;
698
697
  while (existingNames.has(candidate)) {
@@ -906,12 +905,12 @@ export class NodeEditor extends RapidElement {
906
905
  if (this.action) {
907
906
  const actionConfig = ACTION_CONFIG[this.action.type];
908
907
 
909
- if (
910
- actionConfig?.localizable &&
911
- actionConfig.fromLocalizationFormData
912
- ) {
908
+ const actionFromLocalization = actionConfig
909
+ ? resolveFromLocalizationFormData(actionConfig)
910
+ : undefined;
911
+ if (actionFromLocalization) {
913
912
  // Save to localization structure
914
- const localizationData = actionConfig.fromLocalizationFormData(
913
+ const localizationData = actionFromLocalization(
915
914
  processedFormData,
916
915
  this.action
917
916
  );
@@ -933,11 +932,11 @@ export class NodeEditor extends RapidElement {
933
932
  if (this.node) {
934
933
  const nodeConfig = this.getNodeConfig();
935
934
 
936
- if (
937
- nodeConfig?.localizable === 'categories' &&
938
- nodeConfig.fromLocalizationFormData
939
- ) {
940
- const localizationData = nodeConfig.fromLocalizationFormData(
935
+ const nodeFromLocalization = nodeConfig
936
+ ? resolveFromLocalizationFormData(nodeConfig)
937
+ : undefined;
938
+ if (nodeConfig?.localizable === 'categories' && nodeFromLocalization) {
939
+ const localizationData = nodeFromLocalization(
941
940
  processedFormData,
942
941
  this.node
943
942
  );
@@ -1238,40 +1237,7 @@ export class NodeEditor extends RapidElement {
1238
1237
  }
1239
1238
 
1240
1239
  private validateCategoryNames(errors: { [key: string]: string }): void {
1241
- // Universal validation for category names across all node types
1242
- // Prevents use of reserved category names that have special meaning in the system
1243
- // Define reserved category names (case-insensitive)
1244
- const reservedNames = [
1245
- 'other',
1246
- 'failure',
1247
- 'success',
1248
- 'all responses',
1249
- 'no response'
1250
- ];
1251
-
1252
- // Check all form fields for category arrays
1253
- Object.entries(this.formData).forEach(([fieldName, value]) => {
1254
- if (Array.isArray(value) && fieldName === 'categories') {
1255
- const categories = value.filter(
1256
- (item: any) => item?.name && item.name.trim() !== ''
1257
- );
1258
-
1259
- // Check for reserved names
1260
- const reservedUsed = categories
1261
- .filter((item: any) => {
1262
- const lowerName = item.name.trim().toLowerCase();
1263
- return reservedNames.includes(lowerName);
1264
- })
1265
- .map((item: any) => item.name.trim()); // Preserve original case
1266
-
1267
- if (reservedUsed.length > 0) {
1268
- errors[fieldName] =
1269
- `Reserved category names cannot be used: ${reservedUsed.join(
1270
- ', '
1271
- )}`;
1272
- }
1273
- }
1274
- });
1240
+ Object.assign(errors, collectReservedCategoryErrors(this.formData));
1275
1241
  }
1276
1242
 
1277
1243
  private formDataToNode(formData: FormData = this.formData): Node {
@@ -1673,14 +1639,17 @@ export class NodeEditor extends RapidElement {
1673
1639
  ? html`
1674
1640
  <div class="category-localization-row">
1675
1641
  <div class="original-name">
1676
- <span class="rule-operator">${ruleData.operatorName}</span>
1642
+ <span class="rule-operator"
1643
+ >${ruleData.operatorName}</span
1644
+ >
1677
1645
  <span>${arg}</span>
1678
1646
  </div>
1679
1647
  <div class="localized-name">
1680
1648
  <temba-textinput
1681
1649
  name="rule-${caseUuid}-${i}"
1682
1650
  placeholder=""
1683
- value="${ruleData.localizedArguments?.[i] || ''}"
1651
+ value="${ruleData.localizedArguments?.[i] ||
1652
+ ''}"
1684
1653
  @change=${(e: Event) =>
1685
1654
  this.handleRuleLocalizationChange(
1686
1655
  caseUuid,
@@ -1750,7 +1719,10 @@ export class NodeEditor extends RapidElement {
1750
1719
  this.formData.rules = {};
1751
1720
  }
1752
1721
  if (!this.formData.rules[caseUuid]) {
1753
- this.formData.rules[caseUuid] = { originalArguments: [], localizedArguments: [] };
1722
+ this.formData.rules[caseUuid] = {
1723
+ originalArguments: [],
1724
+ localizedArguments: []
1725
+ };
1754
1726
  }
1755
1727
  if (!this.formData.rules[caseUuid].localizedArguments) {
1756
1728
  this.formData.rules[caseUuid].localizedArguments = [];
@@ -2200,9 +2172,15 @@ export class NodeEditor extends RapidElement {
2200
2172
  : undefined}
2201
2173
  >
2202
2174
  <div class="form-group-info">
2203
- <div class="form-group-title">${icon
2204
- ? html`<temba-icon name=${icon} size="1" style="margin-right:6px;color:#999;"></temba-icon>`
2205
- : ''}${label}</div>
2175
+ <div class="form-group-title">
2176
+ ${icon
2177
+ ? html`<temba-icon
2178
+ name=${icon}
2179
+ size="1"
2180
+ style="margin-right:6px;color:#999;"
2181
+ ></temba-icon>`
2182
+ : ''}${label}
2183
+ </div>
2206
2184
  ${helpText
2207
2185
  ? html`<div class="form-group-help">
2208
2186
  ${renderMarkdownInline(helpText)}
@@ -2248,7 +2226,9 @@ export class NodeEditor extends RapidElement {
2248
2226
  style="${[
2249
2227
  contentPadding ? `--group-content-padding: ${contentPadding}` : '',
2250
2228
  gap ? `gap: ${gap}` : ''
2251
- ].filter(Boolean).join(';')}"
2229
+ ]
2230
+ .filter(Boolean)
2231
+ .join(';')}"
2252
2232
  >
2253
2233
  ${items.map((item) =>
2254
2234
  this.renderLayoutItem(item, config, renderedFields)
@@ -2288,7 +2268,10 @@ export class NodeEditor extends RapidElement {
2288
2268
  }
2289
2269
 
2290
2270
  return html`
2291
- <temba-accordion ?multi=${multi} @toggle=${this.handleAccordionSectionToggle}>
2271
+ <temba-accordion
2272
+ ?multi=${multi}
2273
+ @toggle=${this.handleAccordionSectionToggle}
2274
+ >
2292
2275
  ${visibleSections.map((section) => {
2293
2276
  const { label, collapsed = true, getValueCount } = section;
2294
2277
  const stateKey = `accordion:${label}`;
@@ -213,8 +213,7 @@ export class RevisionsWindow extends RapidElement {
213
213
 
214
214
  private cancelRevisionView() {
215
215
  const store = getStore().getState();
216
- const preservedLanguageCode =
217
- this.browseLanguageCode || store.languageCode;
216
+ const preservedLanguageCode = this.browseLanguageCode || store.languageCode;
218
217
 
219
218
  if (this.preRevertState) {
220
219
  const currentInfo = store.flowInfo;
@@ -246,8 +245,7 @@ export class RevisionsWindow extends RapidElement {
246
245
  if (!this.viewingRevision || !this.preRevertState) return;
247
246
 
248
247
  const store = getStore().getState();
249
- const preservedLanguageCode =
250
- this.browseLanguageCode || store.languageCode;
248
+ const preservedLanguageCode = this.browseLanguageCode || store.languageCode;
251
249
 
252
250
  const definitionToSave = {
253
251
  ...store.flowDefinition,