@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.
- package/CHANGELOG.md +25 -0
- package/demo/components/webchat/example.html +4 -2
- package/dist/static/svg/index.svg +1 -1
- package/dist/temba-components.js +1351 -322
- package/dist/temba-components.js.map +1 -1
- package/out-tsc/src/Icons.js +2 -1
- package/out-tsc/src/Icons.js.map +1 -1
- package/out-tsc/src/display/FloatingTab.js +2 -6
- package/out-tsc/src/display/FloatingTab.js.map +1 -1
- package/out-tsc/src/flow/CanvasNode.js +29 -1
- package/out-tsc/src/flow/CanvasNode.js.map +1 -1
- package/out-tsc/src/flow/Editor.js +229 -5
- package/out-tsc/src/flow/Editor.js.map +1 -1
- package/out-tsc/src/flow/Plumber.js +320 -1
- package/out-tsc/src/flow/Plumber.js.map +1 -1
- package/out-tsc/src/interfaces.js +1 -0
- package/out-tsc/src/interfaces.js.map +1 -1
- package/out-tsc/src/layout/FloatingWindow.js +30 -8
- package/out-tsc/src/layout/FloatingWindow.js.map +1 -1
- package/out-tsc/src/simulator/Simulator.js +1861 -0
- package/out-tsc/src/simulator/Simulator.js.map +1 -0
- package/out-tsc/src/store/AppState.js +66 -0
- package/out-tsc/src/store/AppState.js.map +1 -1
- package/out-tsc/src/utils.js +48 -0
- package/out-tsc/src/utils.js.map +1 -1
- package/out-tsc/temba-modules.js +2 -0
- package/out-tsc/temba-modules.js.map +1 -1
- package/out-tsc/test/temba-appstate-node-sorting.test.js +430 -0
- package/out-tsc/test/temba-appstate-node-sorting.test.js.map +1 -0
- package/out-tsc/test/temba-floating-tab.test.js +0 -9
- package/out-tsc/test/temba-floating-tab.test.js.map +1 -1
- package/out-tsc/test/temba-flow-editor.test.js +262 -1
- package/out-tsc/test/temba-flow-editor.test.js.map +1 -1
- package/out-tsc/test/temba-flow-plumber-connections.test.js +3 -1
- package/out-tsc/test/temba-flow-plumber-connections.test.js.map +1 -1
- package/out-tsc/test/temba-flow-plumber.test.js +3 -1
- package/out-tsc/test/temba-flow-plumber.test.js.map +1 -1
- package/out-tsc/test/temba-simulator.test.js +642 -0
- package/out-tsc/test/temba-simulator.test.js.map +1 -0
- package/out-tsc/test/utils.test.js +1 -1
- package/out-tsc/test/utils.test.js.map +1 -1
- package/package.json +1 -1
- package/screenshots/truth/actions/add_contact_groups/render/descriptive-group-names.png +0 -0
- package/screenshots/truth/actions/add_contact_groups/render/long-group-names.png +0 -0
- package/screenshots/truth/actions/add_contact_groups/render/many-groups.png +0 -0
- package/screenshots/truth/actions/add_contact_groups/render/multiple-groups.png +0 -0
- package/screenshots/truth/actions/add_contact_groups/render/single-group.png +0 -0
- package/screenshots/truth/actions/add_contact_urn/render/expression-facebook.png +0 -0
- package/screenshots/truth/actions/add_contact_urn/render/expression-phone.png +0 -0
- package/screenshots/truth/actions/add_contact_urn/render/facebook-id.png +0 -0
- package/screenshots/truth/actions/add_contact_urn/render/instagram-handle.png +0 -0
- package/screenshots/truth/actions/add_contact_urn/render/line-id.png +0 -0
- package/screenshots/truth/actions/add_contact_urn/render/phone-number.png +0 -0
- package/screenshots/truth/actions/add_contact_urn/render/telegram-id.png +0 -0
- package/screenshots/truth/actions/add_contact_urn/render/viber-id.png +0 -0
- package/screenshots/truth/actions/add_contact_urn/render/wechat-id.png +0 -0
- package/screenshots/truth/actions/add_contact_urn/render/whatsapp.png +0 -0
- package/screenshots/truth/actions/remove_contact_groups/render/cleanup-groups.png +0 -0
- package/screenshots/truth/actions/remove_contact_groups/render/long-descriptive-group-names.png +0 -0
- package/screenshots/truth/actions/remove_contact_groups/render/many-groups.png +0 -0
- package/screenshots/truth/actions/remove_contact_groups/render/multiple-groups.png +0 -0
- package/screenshots/truth/actions/remove_contact_groups/render/remove-from-all-groups.png +0 -0
- package/screenshots/truth/actions/remove_contact_groups/render/single-group.png +0 -0
- package/screenshots/truth/actions/send_broadcast/render/contacts-only.png +0 -0
- package/screenshots/truth/actions/send_broadcast/render/groups-and-contacts.png +0 -0
- package/screenshots/truth/actions/send_broadcast/render/groups-only.png +0 -0
- package/screenshots/truth/actions/send_broadcast/render/many-groups.png +0 -0
- package/screenshots/truth/actions/send_broadcast/render/multiline-text.png +0 -0
- package/screenshots/truth/actions/send_email/render/complex-business-email.png +0 -0
- package/screenshots/truth/actions/send_email/render/empty-body.png +0 -0
- package/screenshots/truth/actions/send_email/render/empty-subject.png +0 -0
- package/screenshots/truth/actions/send_email/render/long-subject.png +0 -0
- package/screenshots/truth/actions/send_email/render/multiline-body.png +0 -0
- package/screenshots/truth/actions/send_email/render/multiple-recipients.png +0 -0
- package/screenshots/truth/actions/send_email/render/simple-email.png +0 -0
- package/screenshots/truth/actions/send_email/render/with-expressions.png +0 -0
- package/screenshots/truth/actions/send_msg/render/long-quick-replies.png +0 -0
- package/screenshots/truth/actions/send_msg/render/multiline-text-with-replies.png +0 -0
- package/screenshots/truth/actions/send_msg/render/simple-text.png +0 -0
- package/screenshots/truth/actions/send_msg/render/text-with-linebreaks.png +0 -0
- package/screenshots/truth/actions/send_msg/render/text-with-many-quick-replies.png +0 -0
- package/screenshots/truth/actions/send_msg/render/text-with-quick-replies.png +0 -0
- package/screenshots/truth/actions/send_msg/render/text-without-quick-replies.png +0 -0
- package/screenshots/truth/actions/start_session/render/contact-query.png +0 -0
- package/screenshots/truth/actions/start_session/render/contacts-only.png +0 -0
- package/screenshots/truth/actions/start_session/render/create-contact.png +0 -0
- package/screenshots/truth/actions/start_session/render/groups-and-contacts.png +0 -0
- package/screenshots/truth/actions/start_session/render/groups-only.png +0 -0
- package/screenshots/truth/actions/start_session/render/many-recipients.png +0 -0
- package/screenshots/truth/floating-tab/gray.png +0 -0
- package/screenshots/truth/floating-tab/green.png +0 -0
- package/screenshots/truth/floating-tab/purple.png +0 -0
- package/screenshots/truth/nodes/split_by_llm/render/information-extraction.png +0 -0
- package/screenshots/truth/nodes/split_by_llm/render/sentiment-analysis.png +0 -0
- package/screenshots/truth/nodes/split_by_llm/render/summarization.png +0 -0
- package/screenshots/truth/nodes/split_by_llm/render/translation-task.png +0 -0
- package/screenshots/truth/nodes/split_by_llm_categorize/editor/feedback-categorization.png +0 -0
- package/screenshots/truth/nodes/split_by_llm_categorize/render/basic-categorization.png +0 -0
- package/screenshots/truth/nodes/split_by_llm_categorize/render/custom-input-and-result-name.png +0 -0
- package/screenshots/truth/nodes/split_by_llm_categorize/render/feedback-categorization.png +0 -0
- package/screenshots/truth/nodes/split_by_llm_categorize/render/many-categories.png +0 -0
- package/screenshots/truth/nodes/split_by_llm_categorize/render/minimal-categories.png +0 -0
- package/screenshots/truth/nodes/split_by_random/render/ab-test-multiple-variants.png +0 -0
- package/screenshots/truth/nodes/split_by_random/render/sampling-split.png +0 -0
- package/screenshots/truth/nodes/split_by_random/render/three-way-split.png +0 -0
- package/screenshots/truth/nodes/split_by_random/render/two-bucket-split.png +0 -0
- package/screenshots/truth/nodes/wait_for_digits/render/basic-digits-wait.png +0 -0
- package/screenshots/truth/nodes/wait_for_digits/render/phone-number-collection.png +0 -0
- package/screenshots/truth/nodes/wait_for_digits/render/single-digit-with-timeout.png +0 -0
- package/screenshots/truth/nodes/wait_for_digits/render/verification-code.png +0 -0
- package/screenshots/truth/nodes/wait_for_response/render/basic-wait.png +0 -0
- package/screenshots/truth/nodes/wait_for_response/render/custom-result-name.png +0 -0
- package/screenshots/truth/nodes/wait_for_response/render/no-timeout.png +0 -0
- package/screenshots/truth/nodes/wait_for_response/render/short-timeout.png +0 -0
- package/screenshots/truth/simulator/after-message-sent.png +0 -0
- package/screenshots/truth/simulator/after-reset.png +0 -0
- package/screenshots/truth/simulator/attachment-menu.png +0 -0
- package/screenshots/truth/simulator/context-expanded.png +0 -0
- package/screenshots/truth/simulator/context-explorer-open.png +0 -0
- package/screenshots/truth/simulator/event-info.png +0 -0
- package/screenshots/truth/simulator/image-attachment.png +0 -0
- package/screenshots/truth/simulator/open-initial.png +0 -0
- package/screenshots/truth/simulator/quick-replies.png +0 -0
- package/src/Icons.ts +2 -1
- package/src/display/FloatingTab.ts +2 -7
- package/src/flow/CanvasNode.ts +30 -1
- package/src/flow/Editor.ts +246 -4
- package/src/flow/Plumber.ts +371 -2
- package/src/interfaces.ts +2 -1
- package/src/layout/FloatingWindow.ts +37 -12
- package/src/simulator/Simulator.ts +2061 -0
- package/src/store/AppState.ts +109 -0
- package/src/utils.ts +53 -0
- package/static/svg/index.svg +1 -1
- package/static/svg/work/traced/route.svg +1 -0
- package/static/svg/work/used/route.svg +3 -0
- package/temba-modules.ts +2 -0
- package/test/temba-appstate-node-sorting.test.ts +506 -0
- package/test/temba-floating-tab.test.ts +0 -11
- package/test/temba-flow-editor.test.ts +298 -1
- package/test/temba-flow-plumber-connections.test.ts +4 -1
- package/test/temba-flow-plumber.test.ts +4 -1
- package/test/temba-simulator.test.ts +866 -0
- package/test/utils.test.ts +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M4.115 1.133 C 2.696 1.471,1.633 2.478,1.181 3.914 C 0.936 4.690,0.923 4.940,1.088 5.726 C 1.460 7.499,2.897 8.803,4.614 8.927 C 6.021 9.029,6.796 8.745,7.768 7.773 C 8.690 6.851,8.958 6.199,8.959 4.871 C 8.961 3.418,7.848 1.809,6.480 1.286 C 5.895 1.062,4.728 0.987,4.115 1.133 M6.096 3.406 C 7.120 4.095,7.251 5.524,6.372 6.404 C 5.480 7.295,4.019 7.123,3.364 6.050 C 2.244 4.213,4.316 2.208,6.096 3.406 M10.920 4.236 C 10.375 4.633,10.456 5.444,11.091 5.942 C 11.107 5.955,12.293 6.009,13.726 6.062 C 16.491 6.166,16.632 6.211,16.638 6.988 C 16.641 7.448,15.690 8.236,11.840 10.960 C 5.191 15.664,4.940 15.895,4.836 17.416 C 4.781 18.216,4.831 18.405,5.272 19.065 C 6.043 20.217,6.559 20.369,9.970 20.447 C 12.518 20.505,12.853 20.482,13.130 20.231 C 13.504 19.892,13.534 19.157,13.189 18.811 C 12.991 18.614,12.553 18.558,11.149 18.552 C 8.766 18.541,7.214 18.359,7.031 18.070 C 6.729 17.594,6.883 17.143,7.537 16.582 C 7.898 16.273,10.113 14.651,12.459 12.978 C 14.805 11.304,17.032 9.672,17.408 9.350 C 18.968 8.015,19.151 6.319,17.877 4.998 C 17.180 4.276,16.607 4.144,13.760 4.053 C 11.631 3.985,11.229 4.011,10.920 4.236 M17.840 15.168 C 16.626 15.596,15.476 16.734,15.192 17.789 C 14.564 20.122,15.936 22.423,18.241 22.904 C 21.670 23.620,24.304 19.567,22.260 16.721 C 21.521 15.692,20.481 15.135,19.219 15.092 C 18.637 15.072,18.016 15.106,17.840 15.168 M19.762 17.190 C 20.837 17.640,21.287 19.101,20.649 20.069 C 19.758 21.421,17.803 21.227,17.175 19.725 C 16.644 18.454,17.522 17.139,19.005 16.982 C 19.118 16.970,19.458 17.064,19.762 17.190 " stroke="none" fill-rule="evenodd" fill="black"></path></svg>
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<path d="M11.5 5H11.9344C14.9816 5 16.5053 5 17.0836 5.54729C17.5836 6.02037 17.8051 6.71728 17.6702 7.39221C17.514 8.17302 16.2701 9.05285 13.7823 10.8125L9.71772 13.6875C7.2299 15.4471 5.98599 16.327 5.82984 17.1078C5.69486 17.7827 5.91642 18.4796 6.41636 18.9527C6.99474 19.5 8.51836 19.5 11.5656 19.5H12.5M8 5C8 6.65685 6.65685 8 5 8C3.34315 8 2 6.65685 2 5C2 3.34315 3.34315 2 5 2C6.65685 2 8 3.34315 8 5ZM22 19C22 20.6569 20.6569 22 19 22C17.3431 22 16 20.6569 16 19C16 17.3431 17.3431 16 19 16C20.6569 16 22 17.3431 22 19Z" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
3
|
+
</svg>
|
package/temba-modules.ts
CHANGED
|
@@ -76,6 +76,7 @@ import { MessageEditor } from './src/form/MessageEditor';
|
|
|
76
76
|
import './src/form/BaseListEditor'; // Import base class
|
|
77
77
|
import { FloatingTab } from './src/display/FloatingTab';
|
|
78
78
|
import { FloatingWindow } from './src/layout/FloatingWindow';
|
|
79
|
+
import { Simulator } from './src/simulator/Simulator';
|
|
79
80
|
|
|
80
81
|
export function addCustomElement(name: string, comp: any) {
|
|
81
82
|
if (!window.customElements.get(name)) {
|
|
@@ -162,3 +163,4 @@ addCustomElement('temba-array-editor', TembaArrayEditor);
|
|
|
162
163
|
addCustomElement('temba-message-editor', MessageEditor);
|
|
163
164
|
addCustomElement('temba-floating-tab', FloatingTab);
|
|
164
165
|
addCustomElement('temba-floating-window', FloatingWindow);
|
|
166
|
+
addCustomElement('temba-simulator', Simulator);
|
|
@@ -0,0 +1,506 @@
|
|
|
1
|
+
import { expect } from '@open-wc/testing';
|
|
2
|
+
import { zustand } from '../src/store/AppState';
|
|
3
|
+
import { Node, NodeUI } from '../src/store/flow-definition';
|
|
4
|
+
|
|
5
|
+
describe('AppState Node Sorting', () => {
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
// reset the store state before each test
|
|
8
|
+
const state = zustand.getState();
|
|
9
|
+
zustand.setState({
|
|
10
|
+
...state,
|
|
11
|
+
flowDefinition: {
|
|
12
|
+
language: 'en',
|
|
13
|
+
localization: {},
|
|
14
|
+
name: 'Test Flow',
|
|
15
|
+
nodes: [],
|
|
16
|
+
uuid: 'test-uuid',
|
|
17
|
+
type: 'messaging' as const,
|
|
18
|
+
revision: 1,
|
|
19
|
+
spec_version: '14.3',
|
|
20
|
+
_ui: {
|
|
21
|
+
nodes: {},
|
|
22
|
+
languages: []
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe('addNode', () => {
|
|
29
|
+
it('should sort nodes by position when adding nodes', () => {
|
|
30
|
+
const state = zustand.getState();
|
|
31
|
+
|
|
32
|
+
// add nodes in non-sorted order
|
|
33
|
+
const node1: Node = {
|
|
34
|
+
uuid: 'node-1',
|
|
35
|
+
actions: [],
|
|
36
|
+
exits: [{ uuid: 'exit-1', destination_uuid: null }]
|
|
37
|
+
};
|
|
38
|
+
const nodeUI1: NodeUI = {
|
|
39
|
+
position: { left: 100, top: 300 }, // middle
|
|
40
|
+
type: 'send_msg'
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const node2: Node = {
|
|
44
|
+
uuid: 'node-2',
|
|
45
|
+
actions: [],
|
|
46
|
+
exits: [{ uuid: 'exit-2', destination_uuid: null }]
|
|
47
|
+
};
|
|
48
|
+
const nodeUI2: NodeUI = {
|
|
49
|
+
position: { left: 100, top: 100 }, // top
|
|
50
|
+
type: 'send_msg'
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const node3: Node = {
|
|
54
|
+
uuid: 'node-3',
|
|
55
|
+
actions: [],
|
|
56
|
+
exits: [{ uuid: 'exit-3', destination_uuid: null }]
|
|
57
|
+
};
|
|
58
|
+
const nodeUI3: NodeUI = {
|
|
59
|
+
position: { left: 100, top: 500 }, // bottom
|
|
60
|
+
type: 'send_msg'
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// add in order: middle, top, bottom
|
|
64
|
+
state.addNode(node1, nodeUI1);
|
|
65
|
+
state.addNode(node2, nodeUI2);
|
|
66
|
+
state.addNode(node3, nodeUI3);
|
|
67
|
+
|
|
68
|
+
const nodes = zustand.getState().flowDefinition.nodes;
|
|
69
|
+
|
|
70
|
+
// nodes should be sorted by y position (top to bottom)
|
|
71
|
+
expect(nodes[0].uuid).to.equal('node-2'); // top: 100
|
|
72
|
+
expect(nodes[1].uuid).to.equal('node-1'); // top: 300
|
|
73
|
+
expect(nodes[2].uuid).to.equal('node-3'); // top: 500
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should sort by x when y positions are the same', () => {
|
|
77
|
+
const state = zustand.getState();
|
|
78
|
+
|
|
79
|
+
// add nodes with same y but different x
|
|
80
|
+
const node1: Node = {
|
|
81
|
+
uuid: 'node-1',
|
|
82
|
+
actions: [],
|
|
83
|
+
exits: [{ uuid: 'exit-1', destination_uuid: null }]
|
|
84
|
+
};
|
|
85
|
+
const nodeUI1: NodeUI = {
|
|
86
|
+
position: { left: 300, top: 100 },
|
|
87
|
+
type: 'send_msg'
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const node2: Node = {
|
|
91
|
+
uuid: 'node-2',
|
|
92
|
+
actions: [],
|
|
93
|
+
exits: [{ uuid: 'exit-2', destination_uuid: null }]
|
|
94
|
+
};
|
|
95
|
+
const nodeUI2: NodeUI = {
|
|
96
|
+
position: { left: 100, top: 100 },
|
|
97
|
+
type: 'send_msg'
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const node3: Node = {
|
|
101
|
+
uuid: 'node-3',
|
|
102
|
+
actions: [],
|
|
103
|
+
exits: [{ uuid: 'exit-3', destination_uuid: null }]
|
|
104
|
+
};
|
|
105
|
+
const nodeUI3: NodeUI = {
|
|
106
|
+
position: { left: 500, top: 100 },
|
|
107
|
+
type: 'send_msg'
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
// add in order: middle, left, right
|
|
111
|
+
state.addNode(node1, nodeUI1);
|
|
112
|
+
state.addNode(node2, nodeUI2);
|
|
113
|
+
state.addNode(node3, nodeUI3);
|
|
114
|
+
|
|
115
|
+
const nodes = zustand.getState().flowDefinition.nodes;
|
|
116
|
+
|
|
117
|
+
// nodes should be sorted by x position (left to right) since y is same
|
|
118
|
+
expect(nodes[0].uuid).to.equal('node-2'); // left: 100
|
|
119
|
+
expect(nodes[1].uuid).to.equal('node-1'); // left: 300
|
|
120
|
+
expect(nodes[2].uuid).to.equal('node-3'); // left: 500
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('should handle complex sorting with mixed positions', () => {
|
|
124
|
+
const state = zustand.getState();
|
|
125
|
+
|
|
126
|
+
// create a grid of nodes
|
|
127
|
+
// row 1: (100, 100), (200, 100)
|
|
128
|
+
// row 2: (100, 200), (200, 200)
|
|
129
|
+
|
|
130
|
+
const nodes = [
|
|
131
|
+
{
|
|
132
|
+
node: {
|
|
133
|
+
uuid: 'node-1',
|
|
134
|
+
actions: [],
|
|
135
|
+
exits: [{ uuid: 'exit-1', destination_uuid: null }]
|
|
136
|
+
},
|
|
137
|
+
ui: { position: { left: 200, top: 200 }, type: 'send_msg' as const }
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
node: {
|
|
141
|
+
uuid: 'node-2',
|
|
142
|
+
actions: [],
|
|
143
|
+
exits: [{ uuid: 'exit-2', destination_uuid: null }]
|
|
144
|
+
},
|
|
145
|
+
ui: { position: { left: 100, top: 100 }, type: 'send_msg' as const }
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
node: {
|
|
149
|
+
uuid: 'node-3',
|
|
150
|
+
actions: [],
|
|
151
|
+
exits: [{ uuid: 'exit-3', destination_uuid: null }]
|
|
152
|
+
},
|
|
153
|
+
ui: { position: { left: 200, top: 100 }, type: 'send_msg' as const }
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
node: {
|
|
157
|
+
uuid: 'node-4',
|
|
158
|
+
actions: [],
|
|
159
|
+
exits: [{ uuid: 'exit-4', destination_uuid: null }]
|
|
160
|
+
},
|
|
161
|
+
ui: { position: { left: 100, top: 200 }, type: 'send_msg' as const }
|
|
162
|
+
}
|
|
163
|
+
];
|
|
164
|
+
|
|
165
|
+
// add in random order
|
|
166
|
+
nodes.forEach((n) => state.addNode(n.node, n.ui));
|
|
167
|
+
|
|
168
|
+
const sortedNodes = zustand.getState().flowDefinition.nodes;
|
|
169
|
+
|
|
170
|
+
// expected order: (100,100), (200,100), (100,200), (200,200)
|
|
171
|
+
expect(sortedNodes[0].uuid).to.equal('node-2'); // (100, 100)
|
|
172
|
+
expect(sortedNodes[1].uuid).to.equal('node-3'); // (200, 100)
|
|
173
|
+
expect(sortedNodes[2].uuid).to.equal('node-4'); // (100, 200)
|
|
174
|
+
expect(sortedNodes[3].uuid).to.equal('node-1'); // (200, 200)
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
describe('createNode', () => {
|
|
179
|
+
it('should sort nodes after creating a new node', () => {
|
|
180
|
+
const state = zustand.getState();
|
|
181
|
+
|
|
182
|
+
// create nodes in non-sorted order
|
|
183
|
+
const uuid1 = state.createNode('send_msg', { left: 300, top: 100 });
|
|
184
|
+
const uuid2 = state.createNode('send_msg', { left: 100, top: 100 });
|
|
185
|
+
const uuid3 = state.createNode('send_msg', { left: 200, top: 100 });
|
|
186
|
+
|
|
187
|
+
const nodes = zustand.getState().flowDefinition.nodes;
|
|
188
|
+
|
|
189
|
+
// nodes should be sorted by x position
|
|
190
|
+
expect(nodes[0].uuid).to.equal(uuid2); // left: 100
|
|
191
|
+
expect(nodes[1].uuid).to.equal(uuid3); // left: 200
|
|
192
|
+
expect(nodes[2].uuid).to.equal(uuid1); // left: 300
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
describe('removeNodes', () => {
|
|
197
|
+
it('should maintain sorting after removing nodes', () => {
|
|
198
|
+
const state = zustand.getState();
|
|
199
|
+
|
|
200
|
+
// create nodes
|
|
201
|
+
const node1: Node = {
|
|
202
|
+
uuid: 'node-1',
|
|
203
|
+
actions: [],
|
|
204
|
+
exits: [{ uuid: 'exit-1', destination_uuid: null }]
|
|
205
|
+
};
|
|
206
|
+
const nodeUI1: NodeUI = {
|
|
207
|
+
position: { left: 100, top: 100 },
|
|
208
|
+
type: 'send_msg'
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
const node2: Node = {
|
|
212
|
+
uuid: 'node-2',
|
|
213
|
+
actions: [],
|
|
214
|
+
exits: [{ uuid: 'exit-2', destination_uuid: null }]
|
|
215
|
+
};
|
|
216
|
+
const nodeUI2: NodeUI = {
|
|
217
|
+
position: { left: 200, top: 100 },
|
|
218
|
+
type: 'send_msg'
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
const node3: Node = {
|
|
222
|
+
uuid: 'node-3',
|
|
223
|
+
actions: [],
|
|
224
|
+
exits: [{ uuid: 'exit-3', destination_uuid: null }]
|
|
225
|
+
};
|
|
226
|
+
const nodeUI3: NodeUI = {
|
|
227
|
+
position: { left: 300, top: 100 },
|
|
228
|
+
type: 'send_msg'
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
state.addNode(node1, nodeUI1);
|
|
232
|
+
state.addNode(node2, nodeUI2);
|
|
233
|
+
state.addNode(node3, nodeUI3);
|
|
234
|
+
|
|
235
|
+
// remove middle node
|
|
236
|
+
state.removeNodes(['node-2']);
|
|
237
|
+
|
|
238
|
+
const nodes = zustand.getState().flowDefinition.nodes;
|
|
239
|
+
|
|
240
|
+
// remaining nodes should still be sorted
|
|
241
|
+
expect(nodes.length).to.equal(2);
|
|
242
|
+
expect(nodes[0].uuid).to.equal('node-1'); // left: 100
|
|
243
|
+
expect(nodes[1].uuid).to.equal('node-3'); // left: 300
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('should sort nodes after connection rerouting during removal', () => {
|
|
247
|
+
const state = zustand.getState();
|
|
248
|
+
|
|
249
|
+
// create a chain of nodes
|
|
250
|
+
const node1: Node = {
|
|
251
|
+
uuid: 'node-1',
|
|
252
|
+
actions: [],
|
|
253
|
+
exits: [{ uuid: 'exit-1', destination_uuid: 'node-2' }]
|
|
254
|
+
};
|
|
255
|
+
const nodeUI1: NodeUI = {
|
|
256
|
+
position: { left: 100, top: 300 },
|
|
257
|
+
type: 'send_msg'
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
const node2: Node = {
|
|
261
|
+
uuid: 'node-2',
|
|
262
|
+
actions: [],
|
|
263
|
+
exits: [{ uuid: 'exit-2', destination_uuid: 'node-3' }]
|
|
264
|
+
};
|
|
265
|
+
const nodeUI2: NodeUI = {
|
|
266
|
+
position: { left: 200, top: 200 },
|
|
267
|
+
type: 'send_msg'
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
const node3: Node = {
|
|
271
|
+
uuid: 'node-3',
|
|
272
|
+
actions: [],
|
|
273
|
+
exits: [{ uuid: 'exit-3', destination_uuid: null }]
|
|
274
|
+
};
|
|
275
|
+
const nodeUI3: NodeUI = {
|
|
276
|
+
position: { left: 300, top: 100 },
|
|
277
|
+
type: 'send_msg'
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
state.addNode(node1, nodeUI1);
|
|
281
|
+
state.addNode(node2, nodeUI2);
|
|
282
|
+
state.addNode(node3, nodeUI3);
|
|
283
|
+
|
|
284
|
+
// verify initial sorting
|
|
285
|
+
let nodes = zustand.getState().flowDefinition.nodes;
|
|
286
|
+
expect(nodes[0].uuid).to.equal('node-3'); // top: 100
|
|
287
|
+
expect(nodes[1].uuid).to.equal('node-2'); // top: 200
|
|
288
|
+
expect(nodes[2].uuid).to.equal('node-1'); // top: 300
|
|
289
|
+
|
|
290
|
+
// remove middle node - should reroute connection
|
|
291
|
+
state.removeNodes(['node-2']);
|
|
292
|
+
|
|
293
|
+
nodes = zustand.getState().flowDefinition.nodes;
|
|
294
|
+
|
|
295
|
+
// nodes should still be sorted
|
|
296
|
+
expect(nodes.length).to.equal(2);
|
|
297
|
+
expect(nodes[0].uuid).to.equal('node-3'); // top: 100
|
|
298
|
+
expect(nodes[1].uuid).to.equal('node-1'); // top: 300
|
|
299
|
+
|
|
300
|
+
// verify rerouting happened
|
|
301
|
+
expect(nodes[1].exits[0].destination_uuid).to.equal('node-3');
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
describe('updateCanvasPositions', () => {
|
|
306
|
+
it('should re-sort nodes when positions change', () => {
|
|
307
|
+
const state = zustand.getState();
|
|
308
|
+
|
|
309
|
+
// create nodes in sorted order
|
|
310
|
+
const node1: Node = {
|
|
311
|
+
uuid: 'node-1',
|
|
312
|
+
actions: [],
|
|
313
|
+
exits: [{ uuid: 'exit-1', destination_uuid: null }]
|
|
314
|
+
};
|
|
315
|
+
const nodeUI1: NodeUI = {
|
|
316
|
+
position: { left: 100, top: 100 },
|
|
317
|
+
type: 'send_msg'
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
const node2: Node = {
|
|
321
|
+
uuid: 'node-2',
|
|
322
|
+
actions: [],
|
|
323
|
+
exits: [{ uuid: 'exit-2', destination_uuid: null }]
|
|
324
|
+
};
|
|
325
|
+
const nodeUI2: NodeUI = {
|
|
326
|
+
position: { left: 100, top: 200 },
|
|
327
|
+
type: 'send_msg'
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
const node3: Node = {
|
|
331
|
+
uuid: 'node-3',
|
|
332
|
+
actions: [],
|
|
333
|
+
exits: [{ uuid: 'exit-3', destination_uuid: null }]
|
|
334
|
+
};
|
|
335
|
+
const nodeUI3: NodeUI = {
|
|
336
|
+
position: { left: 100, top: 300 },
|
|
337
|
+
type: 'send_msg'
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
state.addNode(node1, nodeUI1);
|
|
341
|
+
state.addNode(node2, nodeUI2);
|
|
342
|
+
state.addNode(node3, nodeUI3);
|
|
343
|
+
|
|
344
|
+
let nodes = zustand.getState().flowDefinition.nodes;
|
|
345
|
+
expect(nodes[0].uuid).to.equal('node-1'); // top: 100
|
|
346
|
+
expect(nodes[1].uuid).to.equal('node-2'); // top: 200
|
|
347
|
+
expect(nodes[2].uuid).to.equal('node-3'); // top: 300
|
|
348
|
+
|
|
349
|
+
// move node-1 to the bottom
|
|
350
|
+
state.updateCanvasPositions({
|
|
351
|
+
'node-1': { left: 100, top: 400 }
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
nodes = zustand.getState().flowDefinition.nodes;
|
|
355
|
+
|
|
356
|
+
// nodes should be re-sorted
|
|
357
|
+
expect(nodes[0].uuid).to.equal('node-2'); // top: 200
|
|
358
|
+
expect(nodes[1].uuid).to.equal('node-3'); // top: 300
|
|
359
|
+
expect(nodes[2].uuid).to.equal('node-1'); // top: 400
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
it('should handle multiple position updates at once', () => {
|
|
363
|
+
const state = zustand.getState();
|
|
364
|
+
|
|
365
|
+
// create nodes
|
|
366
|
+
const node1: Node = {
|
|
367
|
+
uuid: 'node-1',
|
|
368
|
+
actions: [],
|
|
369
|
+
exits: [{ uuid: 'exit-1', destination_uuid: null }]
|
|
370
|
+
};
|
|
371
|
+
const nodeUI1: NodeUI = {
|
|
372
|
+
position: { left: 100, top: 100 },
|
|
373
|
+
type: 'send_msg'
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
const node2: Node = {
|
|
377
|
+
uuid: 'node-2',
|
|
378
|
+
actions: [],
|
|
379
|
+
exits: [{ uuid: 'exit-2', destination_uuid: null }]
|
|
380
|
+
};
|
|
381
|
+
const nodeUI2: NodeUI = {
|
|
382
|
+
position: { left: 100, top: 200 },
|
|
383
|
+
type: 'send_msg'
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
const node3: Node = {
|
|
387
|
+
uuid: 'node-3',
|
|
388
|
+
actions: [],
|
|
389
|
+
exits: [{ uuid: 'exit-3', destination_uuid: null }]
|
|
390
|
+
};
|
|
391
|
+
const nodeUI3: NodeUI = {
|
|
392
|
+
position: { left: 100, top: 300 },
|
|
393
|
+
type: 'send_msg'
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
state.addNode(node1, nodeUI1);
|
|
397
|
+
state.addNode(node2, nodeUI2);
|
|
398
|
+
state.addNode(node3, nodeUI3);
|
|
399
|
+
|
|
400
|
+
// swap positions of node-1 and node-3
|
|
401
|
+
state.updateCanvasPositions({
|
|
402
|
+
'node-1': { left: 100, top: 300 },
|
|
403
|
+
'node-3': { left: 100, top: 100 }
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
const nodes = zustand.getState().flowDefinition.nodes;
|
|
407
|
+
|
|
408
|
+
// nodes should be re-sorted
|
|
409
|
+
expect(nodes[0].uuid).to.equal('node-3'); // top: 100
|
|
410
|
+
expect(nodes[1].uuid).to.equal('node-2'); // top: 200
|
|
411
|
+
expect(nodes[2].uuid).to.equal('node-1'); // top: 300
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
it('should not affect sticky notes when updating positions', () => {
|
|
415
|
+
const state = zustand.getState();
|
|
416
|
+
|
|
417
|
+
// create a node
|
|
418
|
+
const node: Node = {
|
|
419
|
+
uuid: 'node-1',
|
|
420
|
+
actions: [],
|
|
421
|
+
exits: [{ uuid: 'exit-1', destination_uuid: null }]
|
|
422
|
+
};
|
|
423
|
+
const nodeUI: NodeUI = {
|
|
424
|
+
position: { left: 100, top: 100 },
|
|
425
|
+
type: 'send_msg'
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
state.addNode(node, nodeUI);
|
|
429
|
+
|
|
430
|
+
// create a sticky note
|
|
431
|
+
const stickyUuid = state.createStickyNote({ left: 200, top: 200 });
|
|
432
|
+
|
|
433
|
+
// update positions for both
|
|
434
|
+
state.updateCanvasPositions({
|
|
435
|
+
'node-1': { left: 100, top: 300 },
|
|
436
|
+
[stickyUuid]: { left: 200, top: 100 }
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
const flowDef = zustand.getState().flowDefinition;
|
|
440
|
+
|
|
441
|
+
// verify node position was updated
|
|
442
|
+
expect(flowDef._ui.nodes['node-1'].position.top).to.equal(300);
|
|
443
|
+
|
|
444
|
+
// verify sticky position was updated
|
|
445
|
+
expect(flowDef._ui.stickies[stickyUuid].position.top).to.equal(100);
|
|
446
|
+
});
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
describe('edge cases', () => {
|
|
450
|
+
it('should handle nodes with missing position data', () => {
|
|
451
|
+
const state = zustand.getState();
|
|
452
|
+
|
|
453
|
+
// manually create a flow definition with a node that has no UI data
|
|
454
|
+
zustand.setState({
|
|
455
|
+
...zustand.getState(),
|
|
456
|
+
flowDefinition: {
|
|
457
|
+
language: 'en',
|
|
458
|
+
localization: {},
|
|
459
|
+
name: 'Test Flow',
|
|
460
|
+
nodes: [
|
|
461
|
+
{
|
|
462
|
+
uuid: 'node-1',
|
|
463
|
+
actions: [],
|
|
464
|
+
exits: [{ uuid: 'exit-1', destination_uuid: null }]
|
|
465
|
+
}
|
|
466
|
+
],
|
|
467
|
+
uuid: 'test-uuid',
|
|
468
|
+
type: 'messaging' as const,
|
|
469
|
+
revision: 1,
|
|
470
|
+
spec_version: '14.3',
|
|
471
|
+
_ui: {
|
|
472
|
+
nodes: {}, // no UI data for node-1
|
|
473
|
+
languages: []
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
// add a node with position data
|
|
479
|
+
const node2: Node = {
|
|
480
|
+
uuid: 'node-2',
|
|
481
|
+
actions: [],
|
|
482
|
+
exits: [{ uuid: 'exit-2', destination_uuid: null }]
|
|
483
|
+
};
|
|
484
|
+
const nodeUI2: NodeUI = {
|
|
485
|
+
position: { left: 100, top: 100 },
|
|
486
|
+
type: 'send_msg'
|
|
487
|
+
};
|
|
488
|
+
|
|
489
|
+
// should not throw error
|
|
490
|
+
expect(() => state.addNode(node2, nodeUI2)).to.not.throw();
|
|
491
|
+
|
|
492
|
+
const nodes = zustand.getState().flowDefinition.nodes;
|
|
493
|
+
expect(nodes.length).to.equal(2);
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
it('should handle empty nodes array', () => {
|
|
497
|
+
const state = zustand.getState();
|
|
498
|
+
|
|
499
|
+
// verify initial state is empty
|
|
500
|
+
expect(zustand.getState().flowDefinition.nodes.length).to.equal(0);
|
|
501
|
+
|
|
502
|
+
// try to remove nodes from empty flow - should not throw
|
|
503
|
+
expect(() => state.removeNodes(['non-existent'])).to.not.throw();
|
|
504
|
+
});
|
|
505
|
+
});
|
|
506
|
+
});
|
|
@@ -96,15 +96,4 @@ describe('temba-floating-tab', () => {
|
|
|
96
96
|
await assertScreenshot('floating-tab/gray', getClip(tab2));
|
|
97
97
|
await assertScreenshot('floating-tab/purple', getClip(tab3));
|
|
98
98
|
});
|
|
99
|
-
|
|
100
|
-
it('supports custom positioning', async () => {
|
|
101
|
-
const tab = (await getComponent('temba-floating-tab', {
|
|
102
|
-
icon: 'phone',
|
|
103
|
-
label: 'Phone Simulator',
|
|
104
|
-
color: '#10b981',
|
|
105
|
-
top: 250
|
|
106
|
-
})) as FloatingTab;
|
|
107
|
-
|
|
108
|
-
expect(tab.top).to.equal(250);
|
|
109
|
-
});
|
|
110
99
|
});
|