@nyaruka/temba-components 0.135.9 → 0.136.1

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 (144) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/demo/components/webchat/example.html +4 -2
  3. package/dist/static/svg/index.svg +1 -1
  4. package/dist/temba-components.js +1351 -322
  5. package/dist/temba-components.js.map +1 -1
  6. package/out-tsc/src/Icons.js +2 -1
  7. package/out-tsc/src/Icons.js.map +1 -1
  8. package/out-tsc/src/display/FloatingTab.js +2 -6
  9. package/out-tsc/src/display/FloatingTab.js.map +1 -1
  10. package/out-tsc/src/flow/CanvasNode.js +29 -1
  11. package/out-tsc/src/flow/CanvasNode.js.map +1 -1
  12. package/out-tsc/src/flow/Editor.js +229 -5
  13. package/out-tsc/src/flow/Editor.js.map +1 -1
  14. package/out-tsc/src/flow/Plumber.js +320 -1
  15. package/out-tsc/src/flow/Plumber.js.map +1 -1
  16. package/out-tsc/src/interfaces.js +1 -0
  17. package/out-tsc/src/interfaces.js.map +1 -1
  18. package/out-tsc/src/layout/FloatingWindow.js +30 -8
  19. package/out-tsc/src/layout/FloatingWindow.js.map +1 -1
  20. package/out-tsc/src/simulator/Simulator.js +1861 -0
  21. package/out-tsc/src/simulator/Simulator.js.map +1 -0
  22. package/out-tsc/src/store/AppState.js +66 -0
  23. package/out-tsc/src/store/AppState.js.map +1 -1
  24. package/out-tsc/src/utils.js +48 -0
  25. package/out-tsc/src/utils.js.map +1 -1
  26. package/out-tsc/temba-modules.js +2 -0
  27. package/out-tsc/temba-modules.js.map +1 -1
  28. package/out-tsc/test/temba-appstate-node-sorting.test.js +430 -0
  29. package/out-tsc/test/temba-appstate-node-sorting.test.js.map +1 -0
  30. package/out-tsc/test/temba-floating-tab.test.js +0 -9
  31. package/out-tsc/test/temba-floating-tab.test.js.map +1 -1
  32. package/out-tsc/test/temba-flow-editor.test.js +262 -1
  33. package/out-tsc/test/temba-flow-editor.test.js.map +1 -1
  34. package/out-tsc/test/temba-flow-plumber-connections.test.js +3 -1
  35. package/out-tsc/test/temba-flow-plumber-connections.test.js.map +1 -1
  36. package/out-tsc/test/temba-flow-plumber.test.js +3 -1
  37. package/out-tsc/test/temba-flow-plumber.test.js.map +1 -1
  38. package/out-tsc/test/temba-simulator.test.js +642 -0
  39. package/out-tsc/test/temba-simulator.test.js.map +1 -0
  40. package/out-tsc/test/utils.test.js +1 -1
  41. package/out-tsc/test/utils.test.js.map +1 -1
  42. package/package.json +1 -1
  43. package/screenshots/truth/actions/add_contact_groups/render/descriptive-group-names.png +0 -0
  44. package/screenshots/truth/actions/add_contact_groups/render/long-group-names.png +0 -0
  45. package/screenshots/truth/actions/add_contact_groups/render/many-groups.png +0 -0
  46. package/screenshots/truth/actions/add_contact_groups/render/multiple-groups.png +0 -0
  47. package/screenshots/truth/actions/add_contact_groups/render/single-group.png +0 -0
  48. package/screenshots/truth/actions/add_contact_urn/render/expression-facebook.png +0 -0
  49. package/screenshots/truth/actions/add_contact_urn/render/expression-phone.png +0 -0
  50. package/screenshots/truth/actions/add_contact_urn/render/facebook-id.png +0 -0
  51. package/screenshots/truth/actions/add_contact_urn/render/instagram-handle.png +0 -0
  52. package/screenshots/truth/actions/add_contact_urn/render/line-id.png +0 -0
  53. package/screenshots/truth/actions/add_contact_urn/render/phone-number.png +0 -0
  54. package/screenshots/truth/actions/add_contact_urn/render/telegram-id.png +0 -0
  55. package/screenshots/truth/actions/add_contact_urn/render/viber-id.png +0 -0
  56. package/screenshots/truth/actions/add_contact_urn/render/wechat-id.png +0 -0
  57. package/screenshots/truth/actions/add_contact_urn/render/whatsapp.png +0 -0
  58. package/screenshots/truth/actions/remove_contact_groups/render/cleanup-groups.png +0 -0
  59. package/screenshots/truth/actions/remove_contact_groups/render/long-descriptive-group-names.png +0 -0
  60. package/screenshots/truth/actions/remove_contact_groups/render/many-groups.png +0 -0
  61. package/screenshots/truth/actions/remove_contact_groups/render/multiple-groups.png +0 -0
  62. package/screenshots/truth/actions/remove_contact_groups/render/remove-from-all-groups.png +0 -0
  63. package/screenshots/truth/actions/remove_contact_groups/render/single-group.png +0 -0
  64. package/screenshots/truth/actions/send_broadcast/render/contacts-only.png +0 -0
  65. package/screenshots/truth/actions/send_broadcast/render/groups-and-contacts.png +0 -0
  66. package/screenshots/truth/actions/send_broadcast/render/groups-only.png +0 -0
  67. package/screenshots/truth/actions/send_broadcast/render/many-groups.png +0 -0
  68. package/screenshots/truth/actions/send_broadcast/render/multiline-text.png +0 -0
  69. package/screenshots/truth/actions/send_email/render/complex-business-email.png +0 -0
  70. package/screenshots/truth/actions/send_email/render/empty-body.png +0 -0
  71. package/screenshots/truth/actions/send_email/render/empty-subject.png +0 -0
  72. package/screenshots/truth/actions/send_email/render/long-subject.png +0 -0
  73. package/screenshots/truth/actions/send_email/render/multiline-body.png +0 -0
  74. package/screenshots/truth/actions/send_email/render/multiple-recipients.png +0 -0
  75. package/screenshots/truth/actions/send_email/render/simple-email.png +0 -0
  76. package/screenshots/truth/actions/send_email/render/with-expressions.png +0 -0
  77. package/screenshots/truth/actions/send_msg/render/long-quick-replies.png +0 -0
  78. package/screenshots/truth/actions/send_msg/render/multiline-text-with-replies.png +0 -0
  79. package/screenshots/truth/actions/send_msg/render/simple-text.png +0 -0
  80. package/screenshots/truth/actions/send_msg/render/text-with-linebreaks.png +0 -0
  81. package/screenshots/truth/actions/send_msg/render/text-with-many-quick-replies.png +0 -0
  82. package/screenshots/truth/actions/send_msg/render/text-with-quick-replies.png +0 -0
  83. package/screenshots/truth/actions/send_msg/render/text-without-quick-replies.png +0 -0
  84. package/screenshots/truth/actions/start_session/render/contact-query.png +0 -0
  85. package/screenshots/truth/actions/start_session/render/contacts-only.png +0 -0
  86. package/screenshots/truth/actions/start_session/render/create-contact.png +0 -0
  87. package/screenshots/truth/actions/start_session/render/groups-and-contacts.png +0 -0
  88. package/screenshots/truth/actions/start_session/render/groups-only.png +0 -0
  89. package/screenshots/truth/actions/start_session/render/many-recipients.png +0 -0
  90. package/screenshots/truth/floating-tab/gray.png +0 -0
  91. package/screenshots/truth/floating-tab/green.png +0 -0
  92. package/screenshots/truth/floating-tab/purple.png +0 -0
  93. package/screenshots/truth/nodes/split_by_llm/render/information-extraction.png +0 -0
  94. package/screenshots/truth/nodes/split_by_llm/render/sentiment-analysis.png +0 -0
  95. package/screenshots/truth/nodes/split_by_llm/render/summarization.png +0 -0
  96. package/screenshots/truth/nodes/split_by_llm/render/translation-task.png +0 -0
  97. package/screenshots/truth/nodes/split_by_llm_categorize/editor/feedback-categorization.png +0 -0
  98. package/screenshots/truth/nodes/split_by_llm_categorize/render/basic-categorization.png +0 -0
  99. package/screenshots/truth/nodes/split_by_llm_categorize/render/custom-input-and-result-name.png +0 -0
  100. package/screenshots/truth/nodes/split_by_llm_categorize/render/feedback-categorization.png +0 -0
  101. package/screenshots/truth/nodes/split_by_llm_categorize/render/many-categories.png +0 -0
  102. package/screenshots/truth/nodes/split_by_llm_categorize/render/minimal-categories.png +0 -0
  103. package/screenshots/truth/nodes/split_by_random/render/ab-test-multiple-variants.png +0 -0
  104. package/screenshots/truth/nodes/split_by_random/render/sampling-split.png +0 -0
  105. package/screenshots/truth/nodes/split_by_random/render/three-way-split.png +0 -0
  106. package/screenshots/truth/nodes/split_by_random/render/two-bucket-split.png +0 -0
  107. package/screenshots/truth/nodes/wait_for_digits/render/basic-digits-wait.png +0 -0
  108. package/screenshots/truth/nodes/wait_for_digits/render/phone-number-collection.png +0 -0
  109. package/screenshots/truth/nodes/wait_for_digits/render/single-digit-with-timeout.png +0 -0
  110. package/screenshots/truth/nodes/wait_for_digits/render/verification-code.png +0 -0
  111. package/screenshots/truth/nodes/wait_for_response/render/basic-wait.png +0 -0
  112. package/screenshots/truth/nodes/wait_for_response/render/custom-result-name.png +0 -0
  113. package/screenshots/truth/nodes/wait_for_response/render/no-timeout.png +0 -0
  114. package/screenshots/truth/nodes/wait_for_response/render/short-timeout.png +0 -0
  115. package/screenshots/truth/simulator/after-message-sent.png +0 -0
  116. package/screenshots/truth/simulator/after-reset.png +0 -0
  117. package/screenshots/truth/simulator/attachment-menu.png +0 -0
  118. package/screenshots/truth/simulator/context-expanded.png +0 -0
  119. package/screenshots/truth/simulator/context-explorer-open.png +0 -0
  120. package/screenshots/truth/simulator/event-info.png +0 -0
  121. package/screenshots/truth/simulator/image-attachment.png +0 -0
  122. package/screenshots/truth/simulator/open-initial.png +0 -0
  123. package/screenshots/truth/simulator/quick-replies.png +0 -0
  124. package/src/Icons.ts +2 -1
  125. package/src/display/FloatingTab.ts +2 -7
  126. package/src/flow/CanvasNode.ts +30 -1
  127. package/src/flow/Editor.ts +246 -4
  128. package/src/flow/Plumber.ts +371 -2
  129. package/src/interfaces.ts +2 -1
  130. package/src/layout/FloatingWindow.ts +37 -12
  131. package/src/simulator/Simulator.ts +2061 -0
  132. package/src/store/AppState.ts +109 -0
  133. package/src/utils.ts +53 -0
  134. package/static/svg/index.svg +1 -1
  135. package/static/svg/work/traced/route.svg +1 -0
  136. package/static/svg/work/used/route.svg +3 -0
  137. package/temba-modules.ts +2 -0
  138. package/test/temba-appstate-node-sorting.test.ts +506 -0
  139. package/test/temba-floating-tab.test.ts +0 -11
  140. package/test/temba-flow-editor.test.ts +298 -1
  141. package/test/temba-flow-plumber-connections.test.ts +4 -1
  142. package/test/temba-flow-plumber.test.ts +4 -1
  143. package/test/temba-simulator.test.ts +866 -0
  144. package/test/utils.test.ts +1 -1
@@ -79,7 +79,7 @@ describe('Editor', () => {
79
79
  // Test that calling firstUpdated doesn't throw (without getStore)
80
80
  expect(() => {
81
81
  // Only test the plumber initialization part
82
- (editor as any).plumber = new Plumber(mockCanvas);
82
+ (editor as any).plumber = new Plumber(mockCanvas, editor);
83
83
  }).to.not.throw();
84
84
  });
85
85
 
@@ -521,4 +521,301 @@ describe('Editor', () => {
521
521
  expect(expectedPosition).to.deep.equal({ left: 260, top: 160 });
522
522
  });
523
523
  });
524
+
525
+ describe('flow-start indicator', () => {
526
+ it('should mark the first node as flow-start', async () => {
527
+ const { zustand } = await import('../src/store/AppState');
528
+
529
+ // create a flow definition with multiple nodes
530
+ const mockFlowDefinition = {
531
+ language: 'en',
532
+ localization: {},
533
+ name: 'Test Flow',
534
+ nodes: [
535
+ {
536
+ uuid: 'node-1',
537
+ actions: [
538
+ { type: 'send_msg', uuid: 'action-1', text: 'Message 1' }
539
+ ],
540
+ exits: [{ uuid: 'exit-1', destination_uuid: null }]
541
+ },
542
+ {
543
+ uuid: 'node-2',
544
+ actions: [
545
+ { type: 'send_msg', uuid: 'action-2', text: 'Message 2' }
546
+ ],
547
+ exits: [{ uuid: 'exit-2', destination_uuid: null }]
548
+ },
549
+ {
550
+ uuid: 'node-3',
551
+ actions: [
552
+ { type: 'send_msg', uuid: 'action-3', text: 'Message 3' }
553
+ ],
554
+ exits: [{ uuid: 'exit-3', destination_uuid: null }]
555
+ }
556
+ ],
557
+ uuid: 'test-uuid',
558
+ type: 'messaging' as const,
559
+ revision: 1,
560
+ spec_version: '14.3',
561
+ _ui: {
562
+ nodes: {
563
+ 'node-1': { position: { left: 100, top: 100 }, type: 'send_msg' },
564
+ 'node-2': { position: { left: 200, top: 200 }, type: 'send_msg' },
565
+ 'node-3': { position: { left: 300, top: 300 }, type: 'send_msg' }
566
+ },
567
+ languages: []
568
+ }
569
+ };
570
+
571
+ zustand.getState().setFlowContents({
572
+ definition: mockFlowDefinition as any,
573
+ info: {
574
+ results: [],
575
+ dependencies: [],
576
+ counts: { nodes: 3, languages: 1 },
577
+ locals: []
578
+ }
579
+ });
580
+
581
+ editor = await fixture(html`
582
+ <temba-flow-editor>
583
+ <div id="canvas"></div>
584
+ </temba-flow-editor>
585
+ `);
586
+
587
+ await editor.updateComplete;
588
+
589
+ // get all flow nodes
590
+ const flowNodes = editor.querySelectorAll('temba-flow-node');
591
+ expect(flowNodes.length).to.equal(3);
592
+
593
+ // first node should have flow-start class
594
+ expect(flowNodes[0].classList.contains('flow-start')).to.be.true;
595
+
596
+ // other nodes should not have flow-start class
597
+ expect(flowNodes[1].classList.contains('flow-start')).to.be.false;
598
+ expect(flowNodes[2].classList.contains('flow-start')).to.be.false;
599
+ });
600
+
601
+ it('should update flow-start when node positions change', async () => {
602
+ const { zustand } = await import('../src/store/AppState');
603
+
604
+ // create a flow with nodes in a specific order
605
+ const mockFlowDefinition = {
606
+ language: 'en',
607
+ localization: {},
608
+ name: 'Test Flow',
609
+ nodes: [
610
+ {
611
+ uuid: 'node-1',
612
+ actions: [
613
+ { type: 'send_msg', uuid: 'action-1', text: 'Message 1' }
614
+ ],
615
+ exits: [{ uuid: 'exit-1', destination_uuid: null }]
616
+ },
617
+ {
618
+ uuid: 'node-2',
619
+ actions: [
620
+ { type: 'send_msg', uuid: 'action-2', text: 'Message 2' }
621
+ ],
622
+ exits: [{ uuid: 'exit-2', destination_uuid: null }]
623
+ }
624
+ ],
625
+ uuid: 'test-uuid',
626
+ type: 'messaging' as const,
627
+ revision: 1,
628
+ spec_version: '14.3',
629
+ _ui: {
630
+ nodes: {
631
+ 'node-1': { position: { left: 100, top: 200 }, type: 'send_msg' },
632
+ 'node-2': { position: { left: 100, top: 100 }, type: 'send_msg' }
633
+ },
634
+ languages: []
635
+ }
636
+ };
637
+
638
+ zustand.getState().setFlowContents({
639
+ definition: mockFlowDefinition as any,
640
+ info: {
641
+ results: [],
642
+ dependencies: [],
643
+ counts: { nodes: 2, languages: 1 },
644
+ locals: []
645
+ }
646
+ });
647
+
648
+ editor = await fixture(html`
649
+ <temba-flow-editor>
650
+ <div id="canvas"></div>
651
+ </temba-flow-editor>
652
+ `);
653
+
654
+ await editor.updateComplete;
655
+
656
+ // node-2 should be first (top: 100 < top: 200)
657
+ const flowNodes = editor.querySelectorAll('temba-flow-node');
658
+ expect(flowNodes[0].getAttribute('uuid')).to.equal('node-2');
659
+ expect(flowNodes[0].classList.contains('flow-start')).to.be.true;
660
+ expect(flowNodes[1].classList.contains('flow-start')).to.be.false;
661
+
662
+ // move node-1 to the top
663
+ zustand.getState().updateCanvasPositions({
664
+ 'node-1': { left: 100, top: 50 }
665
+ });
666
+
667
+ await editor.updateComplete;
668
+
669
+ // now node-1 should be first
670
+ const updatedFlowNodes = editor.querySelectorAll('temba-flow-node');
671
+ expect(updatedFlowNodes[0].getAttribute('uuid')).to.equal('node-1');
672
+ expect(updatedFlowNodes[0].classList.contains('flow-start')).to.be.true;
673
+ expect(updatedFlowNodes[1].classList.contains('flow-start')).to.be.false;
674
+ });
675
+
676
+ it('should maintain flow-start when nodes are added', async () => {
677
+ const { zustand } = await import('../src/store/AppState');
678
+
679
+ // start with one node
680
+ const mockFlowDefinition = {
681
+ language: 'en',
682
+ localization: {},
683
+ name: 'Test Flow',
684
+ nodes: [
685
+ {
686
+ uuid: 'node-1',
687
+ actions: [
688
+ { type: 'send_msg', uuid: 'action-1', text: 'Message 1' }
689
+ ],
690
+ exits: [{ uuid: 'exit-1', destination_uuid: null }]
691
+ }
692
+ ],
693
+ uuid: 'test-uuid',
694
+ type: 'messaging' as const,
695
+ revision: 1,
696
+ spec_version: '14.3',
697
+ _ui: {
698
+ nodes: {
699
+ 'node-1': { position: { left: 100, top: 200 }, type: 'send_msg' }
700
+ },
701
+ languages: []
702
+ }
703
+ };
704
+
705
+ zustand.getState().setFlowContents({
706
+ definition: mockFlowDefinition as any,
707
+ info: {
708
+ results: [],
709
+ dependencies: [],
710
+ counts: { nodes: 1, languages: 1 },
711
+ locals: []
712
+ }
713
+ });
714
+
715
+ editor = await fixture(html`
716
+ <temba-flow-editor>
717
+ <div id="canvas"></div>
718
+ </temba-flow-editor>
719
+ `);
720
+
721
+ await editor.updateComplete;
722
+
723
+ let flowNodes = editor.querySelectorAll('temba-flow-node');
724
+ expect(flowNodes[0].classList.contains('flow-start')).to.be.true;
725
+
726
+ // add a new node at the top
727
+ const newNode = {
728
+ uuid: 'node-2',
729
+ actions: [
730
+ { type: 'send_msg' as const, uuid: 'action-2', text: 'Message 2' }
731
+ ],
732
+ exits: [{ uuid: 'exit-2', destination_uuid: null }]
733
+ };
734
+ const newNodeUI = {
735
+ position: { left: 100, top: 100 },
736
+ type: 'send_msg' as const
737
+ };
738
+
739
+ zustand.getState().addNode(newNode, newNodeUI);
740
+
741
+ await editor.updateComplete;
742
+
743
+ // new node should now be the flow-start
744
+ flowNodes = editor.querySelectorAll('temba-flow-node');
745
+ expect(flowNodes[0].getAttribute('uuid')).to.equal('node-2');
746
+ expect(flowNodes[0].classList.contains('flow-start')).to.be.true;
747
+ expect(flowNodes[1].classList.contains('flow-start')).to.be.false;
748
+ });
749
+
750
+ it('should handle flow-start when first node is removed', async () => {
751
+ const { zustand } = await import('../src/store/AppState');
752
+
753
+ // create flow with multiple nodes
754
+ const mockFlowDefinition = {
755
+ language: 'en',
756
+ localization: {},
757
+ name: 'Test Flow',
758
+ nodes: [
759
+ {
760
+ uuid: 'node-1',
761
+ actions: [
762
+ { type: 'send_msg', uuid: 'action-1', text: 'Message 1' }
763
+ ],
764
+ exits: [{ uuid: 'exit-1', destination_uuid: null }]
765
+ },
766
+ {
767
+ uuid: 'node-2',
768
+ actions: [
769
+ { type: 'send_msg', uuid: 'action-2', text: 'Message 2' }
770
+ ],
771
+ exits: [{ uuid: 'exit-2', destination_uuid: null }]
772
+ }
773
+ ],
774
+ uuid: 'test-uuid',
775
+ type: 'messaging' as const,
776
+ revision: 1,
777
+ spec_version: '14.3',
778
+ _ui: {
779
+ nodes: {
780
+ 'node-1': { position: { left: 100, top: 100 }, type: 'send_msg' },
781
+ 'node-2': { position: { left: 100, top: 200 }, type: 'send_msg' }
782
+ },
783
+ languages: []
784
+ }
785
+ };
786
+
787
+ zustand.getState().setFlowContents({
788
+ definition: mockFlowDefinition as any,
789
+ info: {
790
+ results: [],
791
+ dependencies: [],
792
+ counts: { nodes: 2, languages: 1 },
793
+ locals: []
794
+ }
795
+ });
796
+
797
+ editor = await fixture(html`
798
+ <temba-flow-editor>
799
+ <div id="canvas"></div>
800
+ </temba-flow-editor>
801
+ `);
802
+
803
+ await editor.updateComplete;
804
+
805
+ let flowNodes = editor.querySelectorAll('temba-flow-node');
806
+ expect(flowNodes[0].getAttribute('uuid')).to.equal('node-1');
807
+ expect(flowNodes[0].classList.contains('flow-start')).to.be.true;
808
+
809
+ // remove the first node
810
+ zustand.getState().removeNodes(['node-1']);
811
+
812
+ await editor.updateComplete;
813
+
814
+ // node-2 should now be the flow-start
815
+ flowNodes = editor.querySelectorAll('temba-flow-node');
816
+ expect(flowNodes.length).to.equal(1);
817
+ expect(flowNodes[0].getAttribute('uuid')).to.equal('node-2');
818
+ expect(flowNodes[0].classList.contains('flow-start')).to.be.true;
819
+ });
820
+ });
524
821
  });
@@ -16,8 +16,11 @@ describe('Plumber - Connection Management', () => {
16
16
  const mockElement = document.createElement('div');
17
17
  stub(document, 'getElementById').returns(mockElement);
18
18
 
19
+ // Create a mock editor with fireCustomEvent
20
+ const mockEditor = { fireCustomEvent: stub() };
21
+
19
22
  // Create a new plumber instance
20
- plumber = new Plumber(mockCanvas);
23
+ plumber = new Plumber(mockCanvas, mockEditor);
21
24
 
22
25
  // Replace the internal jsPlumb instance with mocks
23
26
  (plumber as any).jsPlumb = {
@@ -17,8 +17,11 @@ describe('Plumber', () => {
17
17
  const mockElement = document.createElement('div');
18
18
  stub(document, 'getElementById').returns(mockElement);
19
19
 
20
+ // Create a mock editor with fireCustomEvent
21
+ const mockEditor = { fireCustomEvent: stub() };
22
+
20
23
  // Create a new plumber instance
21
- plumber = new Plumber(mockCanvas);
24
+ plumber = new Plumber(mockCanvas, mockEditor);
22
25
 
23
26
  // Replace the internal jsPlumb instance with mocks
24
27
  mockJsPlumb = {