@nyaruka/temba-components 0.137.0 → 0.138.4

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 (88) hide show
  1. package/.devcontainer/Dockerfile +0 -9
  2. package/.devcontainer/devcontainer.json +8 -3
  3. package/.github/workflows/build.yml +6 -1
  4. package/.github/workflows/cla.yml +1 -1
  5. package/.github/workflows/publish.yml +6 -1
  6. package/CHANGELOG.md +42 -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 +445 -278
  16. package/dist/temba-components.js.map +1 -1
  17. package/out-tsc/src/display/FloatingTab.js +16 -8
  18. package/out-tsc/src/display/FloatingTab.js.map +1 -1
  19. package/out-tsc/src/flow/CanvasMenu.js +33 -15
  20. package/out-tsc/src/flow/CanvasMenu.js.map +1 -1
  21. package/out-tsc/src/flow/CanvasNode.js +49 -24
  22. package/out-tsc/src/flow/CanvasNode.js.map +1 -1
  23. package/out-tsc/src/flow/Editor.js +583 -70
  24. package/out-tsc/src/flow/Editor.js.map +1 -1
  25. package/out-tsc/src/flow/NodeTypeSelector.js +13 -11
  26. package/out-tsc/src/flow/NodeTypeSelector.js.map +1 -1
  27. package/out-tsc/src/flow/Plumber.js +110 -64
  28. package/out-tsc/src/flow/Plumber.js.map +1 -1
  29. package/out-tsc/src/flow/actions/set_contact_field.js +5 -1
  30. package/out-tsc/src/flow/actions/set_contact_field.js.map +1 -1
  31. package/out-tsc/src/list/RunList.js +2 -1
  32. package/out-tsc/src/list/RunList.js.map +1 -1
  33. package/out-tsc/src/list/TicketList.js +2 -1
  34. package/out-tsc/src/list/TicketList.js.map +1 -1
  35. package/out-tsc/src/locales/es.js +5 -5
  36. package/out-tsc/src/locales/es.js.map +1 -1
  37. package/out-tsc/src/locales/fr.js +5 -5
  38. package/out-tsc/src/locales/fr.js.map +1 -1
  39. package/out-tsc/src/locales/locale-codes.js +11 -2
  40. package/out-tsc/src/locales/locale-codes.js.map +1 -1
  41. package/out-tsc/src/locales/pt.js +5 -5
  42. package/out-tsc/src/locales/pt.js.map +1 -1
  43. package/out-tsc/src/simulator/Simulator.js +11 -4
  44. package/out-tsc/src/simulator/Simulator.js.map +1 -1
  45. package/out-tsc/src/store/AppState.js +17 -2
  46. package/out-tsc/src/store/AppState.js.map +1 -1
  47. package/out-tsc/test/temba-contact-fields.test.js +3 -3
  48. package/out-tsc/test/temba-contact-fields.test.js.map +1 -1
  49. package/out-tsc/test/temba-flow-editor-node.test.js +3 -1
  50. package/out-tsc/test/temba-flow-editor-node.test.js.map +1 -1
  51. package/out-tsc/test/temba-flow-editor-revisions.test.js +106 -0
  52. package/out-tsc/test/temba-flow-editor-revisions.test.js.map +1 -0
  53. package/out-tsc/test/temba-flow-editor.test.js +14 -10
  54. package/out-tsc/test/temba-flow-editor.test.js.map +1 -1
  55. package/out-tsc/test/temba-flow-plumber-connections.test.js +7 -1
  56. package/out-tsc/test/temba-flow-plumber-connections.test.js.map +1 -1
  57. package/out-tsc/test/temba-flow-plumber.test.js +6 -0
  58. package/out-tsc/test/temba-flow-plumber.test.js.map +1 -1
  59. package/out-tsc/test/temba-select.test.js +1 -0
  60. package/out-tsc/test/temba-select.test.js.map +1 -1
  61. package/package.json +1 -1
  62. package/screenshots/truth/floating-tab/gray.png +0 -0
  63. package/screenshots/truth/floating-tab/green.png +0 -0
  64. package/screenshots/truth/floating-tab/purple.png +0 -0
  65. package/screenshots/truth/node-type-selector/action-mode.png +0 -0
  66. package/screenshots/truth/node-type-selector/split-mode.png +0 -0
  67. package/src/display/FloatingTab.ts +18 -8
  68. package/src/flow/CanvasMenu.ts +38 -16
  69. package/src/flow/CanvasNode.ts +62 -29
  70. package/src/flow/Editor.ts +699 -74
  71. package/src/flow/NodeTypeSelector.ts +13 -11
  72. package/src/flow/Plumber.ts +123 -69
  73. package/src/flow/actions/set_contact_field.ts +5 -1
  74. package/src/list/RunList.ts +2 -1
  75. package/src/list/TicketList.ts +2 -1
  76. package/src/locales/es.ts +18 -13
  77. package/src/locales/fr.ts +18 -13
  78. package/src/locales/locale-codes.ts +11 -2
  79. package/src/locales/pt.ts +18 -13
  80. package/src/simulator/Simulator.ts +11 -5
  81. package/src/store/AppState.ts +18 -2
  82. package/test/temba-contact-fields.test.ts +8 -3
  83. package/test/temba-flow-editor-node.test.ts +3 -1
  84. package/test/temba-flow-editor-revisions.test.ts +134 -0
  85. package/test/temba-flow-editor.test.ts +16 -10
  86. package/test/temba-flow-plumber-connections.test.ts +7 -1
  87. package/test/temba-flow-plumber.test.ts +6 -0
  88. package/test/temba-select.test.ts +1 -0
@@ -0,0 +1,134 @@
1
+ import { html, fixture, expect } from '@open-wc/testing';
2
+ import { Editor } from '../src/flow/Editor';
3
+ import { stub, restore, SinonStub } from 'sinon';
4
+
5
+ customElements.define('temba-flow-editor-revisions', Editor);
6
+
7
+ describe('Editor Revisions', () => {
8
+ let element: Editor;
9
+ let fetchStub: SinonStub;
10
+
11
+ beforeEach(async () => {
12
+ restore();
13
+ fetchStub = stub(window, 'fetch');
14
+ // Initialize without 'flow' attribute to prevent firstUpdated from calling getStore().getState()
15
+ element = await fixture(
16
+ html`<temba-flow-editor-revisions></temba-flow-editor-revisions>`
17
+ );
18
+ element.flow = 'test-flow';
19
+ });
20
+
21
+ afterEach(() => {
22
+ restore();
23
+ });
24
+
25
+ it('should exclude the most recent revision from the list', async () => {
26
+ const mockRevisions = [
27
+ {
28
+ id: 3,
29
+ created_on: '2023-01-03',
30
+ user: { id: 1, first_name: 'A', last_name: 'B', username: 'ab' }
31
+ },
32
+ {
33
+ id: 2,
34
+ created_on: '2023-01-02',
35
+ user: { id: 1, first_name: 'A', last_name: 'B', username: 'ab' }
36
+ },
37
+ {
38
+ id: 1,
39
+ created_on: '2023-01-01',
40
+ user: { id: 1, first_name: 'A', last_name: 'B', username: 'ab' }
41
+ }
42
+ ];
43
+
44
+ // Mock the fetch response for the revisions list
45
+ const mockResponse = new Response(
46
+ JSON.stringify({ results: mockRevisions }),
47
+ {
48
+ status: 200,
49
+ headers: { 'Content-Type': 'application/json' }
50
+ }
51
+ );
52
+
53
+ fetchStub.resolves(mockResponse);
54
+
55
+ // Call fetchRevisions (private)
56
+ // Note: fetchRevisions calls fetchResults -> fetchResultsPage -> fetch
57
+ await (element as any).fetchRevisions();
58
+
59
+ const revisions = (element as any).revisions;
60
+ expect(revisions.length).to.equal(2);
61
+ expect(revisions[0].id).to.equal(2);
62
+ expect(revisions[1].id).to.equal(1);
63
+ });
64
+
65
+ it('should handle empty revisions list', async () => {
66
+ const mockResponse = new Response(JSON.stringify({ results: [] }), {
67
+ status: 200
68
+ });
69
+ fetchStub.resolves(mockResponse);
70
+
71
+ await (element as any).fetchRevisions();
72
+ const revisions = (element as any).revisions;
73
+ expect(revisions.length).to.equal(0);
74
+ });
75
+
76
+ it('should handle single revision in list', async () => {
77
+ const mockRevisions = [
78
+ {
79
+ id: 1,
80
+ created_on: '2023-01-01',
81
+ user: { id: 1, first_name: 'A', last_name: 'B', username: 'ab' }
82
+ }
83
+ ];
84
+ const mockResponse = new Response(
85
+ JSON.stringify({ results: mockRevisions }),
86
+ { status: 200 }
87
+ );
88
+ fetchStub.resolves(mockResponse);
89
+
90
+ await (element as any).fetchRevisions();
91
+ const revisions = (element as any).revisions;
92
+ expect(revisions.length).to.equal(0);
93
+ });
94
+
95
+ it('should have purple color for revisions tab and blue for selected item', async () => {
96
+ // Force revisions window to show
97
+ (element as any).revisionsWindowHidden = false;
98
+ (element as any).localizationWindowHidden = true;
99
+
100
+ // Mock revisions so we can see list items
101
+ const mockRevisions = [
102
+ {
103
+ id: 2,
104
+ created_on: '2023-01-02',
105
+ user: { id: 1, first_name: 'A', last_name: 'B', username: 'ab' }
106
+ },
107
+ {
108
+ id: 1,
109
+ created_on: '2023-01-01',
110
+ user: { id: 1, first_name: 'A', last_name: 'B', username: 'ab' }
111
+ }
112
+ ];
113
+ (element as any).revisions = mockRevisions;
114
+ (element as any).viewingRevision = mockRevisions[0]; // Select the first one
115
+
116
+ await element.requestUpdate();
117
+
118
+ // Check tab color
119
+ const tab = element.querySelector('#revisions-tab');
120
+ expect(tab).to.exist;
121
+ expect(tab.getAttribute('color')).to.equal('rgb(142, 94, 167)');
122
+
123
+ // Check selected item styles
124
+ const selectedItem = element.querySelector(
125
+ '.revision-item.selected'
126
+ ) as HTMLElement;
127
+ expect(selectedItem).to.exist;
128
+
129
+ // We need to check inline styles because they are set in the template
130
+ const style = selectedItem.getAttribute('style');
131
+ expect(style).to.contain('#f0f6ff'); // Blue background
132
+ expect(style).to.contain('#a4cafe'); // Blue border
133
+ });
134
+ });
@@ -653,11 +653,14 @@ describe('Editor', () => {
653
653
 
654
654
  await editor.updateComplete;
655
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;
656
+ // node-2 is at top (top: 100 < top: 200) so it should be flow-start
657
+ const node1 = editor.querySelector('temba-flow-node[uuid="node-1"]');
658
+ const node2 = editor.querySelector('temba-flow-node[uuid="node-2"]');
659
+
660
+ expect(node2).to.exist;
661
+ expect(node1).to.exist;
662
+ expect(node2.classList.contains('flow-start')).to.be.true;
663
+ expect(node1.classList.contains('flow-start')).to.be.false;
661
664
 
662
665
  // move node-1 to the top
663
666
  zustand.getState().updateCanvasPositions({
@@ -720,7 +723,7 @@ describe('Editor', () => {
720
723
 
721
724
  await editor.updateComplete;
722
725
 
723
- let flowNodes = editor.querySelectorAll('temba-flow-node');
726
+ const flowNodes = editor.querySelectorAll('temba-flow-node');
724
727
  expect(flowNodes[0].classList.contains('flow-start')).to.be.true;
725
728
 
726
729
  // add a new node at the top
@@ -741,10 +744,13 @@ describe('Editor', () => {
741
744
  await editor.updateComplete;
742
745
 
743
746
  // 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;
747
+ const node1 = editor.querySelector('temba-flow-node[uuid="node-1"]');
748
+ const node2 = editor.querySelector('temba-flow-node[uuid="node-2"]');
749
+
750
+ expect(node2).to.exist;
751
+ expect(node1).to.exist;
752
+ expect(node2.classList.contains('flow-start')).to.be.true;
753
+ expect(node1.classList.contains('flow-start')).to.be.false;
748
754
  });
749
755
 
750
756
  it('should handle flow-start when first node is removed', async () => {
@@ -29,7 +29,14 @@ describe('Plumber - Connection Management', () => {
29
29
  removeClass: stub(),
30
30
  batch: stub().callsFake((fn: any) => fn()),
31
31
  addEndpoint: stub().returns({}),
32
+ revalidate: stub(),
32
33
  connect: stub(),
34
+ getEndpoints: stub().returns([
35
+ { elementId: 'test-from', addClass: stub() }
36
+ ]),
37
+ select: stub().returns({
38
+ deleteAll: stub()
39
+ }),
33
40
  selectEndpoints: stub().returns({
34
41
  deleteAll: stub()
35
42
  }),
@@ -131,7 +138,6 @@ describe('Plumber - Connection Management', () => {
131
138
  expect(result).to.be.true;
132
139
  expect((plumber as any).jsPlumb.deleteConnection).to.have.been
133
140
  .calledTwice;
134
- expect((plumber as any).jsPlumb.removeAllEndpoints).to.have.been.called;
135
141
  });
136
142
 
137
143
  it('returns false when no connections exist', () => {
@@ -31,6 +31,12 @@ describe('Plumber', () => {
31
31
  batch: stub().callsFake((fn) => fn()),
32
32
  addEndpoint: stub().returns({}),
33
33
  connect: stub(),
34
+ getEndpoints: stub().returns([
35
+ { elementId: 'test-from', addClass: stub() }
36
+ ]),
37
+ select: stub().returns({
38
+ deleteAll: stub()
39
+ }),
34
40
  selectEndpoints: stub().returns({
35
41
  deleteAll: stub()
36
42
  }),
@@ -855,6 +855,7 @@ describe('temba-select', () => {
855
855
  })
856
856
  );
857
857
 
858
+ await openSelect(clock, select);
858
859
  await typeInto('temba-select', 're', false);
859
860
  await openSelect(clock, select);
860
861
  assert.equal(select.visibleOptions.length, 2);