@nyaruka/temba-components 0.136.0 → 0.137.0
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 +19 -0
- package/demo/components/webchat/example.html +2 -2
- package/dist/temba-components.js +537 -578
- package/dist/temba-components.js.map +1 -1
- package/out-tsc/src/display/Chat.js +123 -44
- package/out-tsc/src/display/Chat.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/events/eventRenderers.js +442 -0
- package/out-tsc/src/events/eventRenderers.js.map +1 -0
- package/out-tsc/src/flow/CanvasNode.js +18 -1
- package/out-tsc/src/flow/CanvasNode.js.map +1 -1
- package/out-tsc/src/flow/Editor.js +10 -7
- package/out-tsc/src/flow/Editor.js.map +1 -1
- package/out-tsc/src/flow/NodeEditor.js +0 -1
- package/out-tsc/src/flow/NodeEditor.js.map +1 -1
- package/out-tsc/src/layout/FloatingWindow.js.map +1 -1
- package/out-tsc/src/list/ShortcutList.js +1 -1
- package/out-tsc/src/list/ShortcutList.js.map +1 -1
- package/out-tsc/src/live/ContactChat.js +12 -321
- package/out-tsc/src/live/ContactChat.js.map +1 -1
- package/out-tsc/src/simulator/Simulator.js +432 -541
- package/out-tsc/src/simulator/Simulator.js.map +1 -1
- package/out-tsc/src/store/AppState.js +33 -0
- package/out-tsc/src/store/AppState.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 +261 -0
- package/out-tsc/test/temba-flow-editor.test.js.map +1 -1
- package/out-tsc/test/temba-simulator.test.js +51 -32
- package/out-tsc/test/temba-simulator.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/contacts/chat-failure.png +0 -0
- package/screenshots/truth/contacts/chat-for-archived-contact.png +0 -0
- package/screenshots/truth/contacts/chat-for-blocked-contact.png +0 -0
- package/screenshots/truth/contacts/chat-for-stopped-contact.png +0 -0
- package/screenshots/truth/contacts/chat-sends-attachments-only.png +0 -0
- package/screenshots/truth/contacts/chat-sends-text-and-attachments.png +0 -0
- package/screenshots/truth/contacts/chat-sends-text-only.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/display/Chat.ts +123 -44
- package/src/display/FloatingTab.ts +2 -7
- package/src/events/eventRenderers.ts +527 -0
- package/src/flow/CanvasNode.ts +18 -1
- package/src/flow/Editor.ts +11 -7
- package/src/flow/NodeEditor.ts +0 -1
- package/src/layout/FloatingWindow.ts +1 -1
- package/src/list/ShortcutList.ts +1 -1
- package/src/live/ContactChat.ts +17 -376
- package/src/simulator/Simulator.ts +492 -564
- package/src/store/AppState.ts +56 -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 +297 -0
- package/test/temba-simulator.test.ts +64 -34
package/src/store/AppState.ts
CHANGED
|
@@ -18,6 +18,32 @@ import { produce } from 'immer';
|
|
|
18
18
|
export const FLOW_SPEC_VERSION = '14.3';
|
|
19
19
|
const CANVAS_PADDING = 800;
|
|
20
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Sorts nodes by their position - first by y (top), then by x (left)
|
|
23
|
+
*/
|
|
24
|
+
function sortNodesByPosition(
|
|
25
|
+
nodes: Node[],
|
|
26
|
+
nodePositions: Record<string, NodeUI>
|
|
27
|
+
): void {
|
|
28
|
+
nodes.sort((a, b) => {
|
|
29
|
+
const posA = nodePositions[a.uuid]?.position;
|
|
30
|
+
const posB = nodePositions[b.uuid]?.position;
|
|
31
|
+
|
|
32
|
+
// if either position is missing, maintain current order
|
|
33
|
+
if (!posA || !posB) {
|
|
34
|
+
return 0;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// sort by y (top) first
|
|
38
|
+
if (posA.top !== posB.top) {
|
|
39
|
+
return posA.top - posB.top;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// if y is same, sort by x (left)
|
|
43
|
+
return posA.left - posB.left;
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
21
47
|
export interface InfoResult {
|
|
22
48
|
key: string;
|
|
23
49
|
name: string;
|
|
@@ -261,6 +287,14 @@ export const zustand = createStore<AppState>()(
|
|
|
261
287
|
// Reset to the flow's default language when loading a new flow
|
|
262
288
|
state.languageCode = flowLang;
|
|
263
289
|
state.isTranslating = false;
|
|
290
|
+
|
|
291
|
+
// Sort nodes by position when loading flow
|
|
292
|
+
if (state.flowDefinition?.nodes && state.flowDefinition?._ui?.nodes) {
|
|
293
|
+
sortNodesByPosition(
|
|
294
|
+
state.flowDefinition.nodes,
|
|
295
|
+
state.flowDefinition._ui.nodes
|
|
296
|
+
);
|
|
297
|
+
}
|
|
264
298
|
});
|
|
265
299
|
},
|
|
266
300
|
|
|
@@ -313,6 +347,13 @@ export const zustand = createStore<AppState>()(
|
|
|
313
347
|
positions[uuid];
|
|
314
348
|
}
|
|
315
349
|
}
|
|
350
|
+
|
|
351
|
+
// Sort nodes by position since positions may have changed
|
|
352
|
+
sortNodesByPosition(
|
|
353
|
+
state.flowDefinition.nodes,
|
|
354
|
+
state.flowDefinition._ui.nodes
|
|
355
|
+
);
|
|
356
|
+
|
|
316
357
|
state.dirtyDate = new Date();
|
|
317
358
|
});
|
|
318
359
|
},
|
|
@@ -371,6 +412,9 @@ export const zustand = createStore<AppState>()(
|
|
|
371
412
|
}
|
|
372
413
|
});
|
|
373
414
|
});
|
|
415
|
+
|
|
416
|
+
// Sort nodes by position
|
|
417
|
+
sortNodesByPosition(draft.nodes, draft._ui.nodes);
|
|
374
418
|
});
|
|
375
419
|
|
|
376
420
|
state.dirtyDate = new Date();
|
|
@@ -505,6 +549,12 @@ export const zustand = createStore<AppState>()(
|
|
|
505
549
|
config: {}
|
|
506
550
|
};
|
|
507
551
|
|
|
552
|
+
// Sort nodes by position
|
|
553
|
+
sortNodesByPosition(
|
|
554
|
+
state.flowDefinition.nodes,
|
|
555
|
+
state.flowDefinition._ui.nodes
|
|
556
|
+
);
|
|
557
|
+
|
|
508
558
|
state.dirtyDate = new Date();
|
|
509
559
|
});
|
|
510
560
|
|
|
@@ -523,6 +573,12 @@ export const zustand = createStore<AppState>()(
|
|
|
523
573
|
|
|
524
574
|
state.flowDefinition._ui.nodes[node.uuid] = nodeUI;
|
|
525
575
|
|
|
576
|
+
// Sort nodes by position
|
|
577
|
+
sortNodesByPosition(
|
|
578
|
+
state.flowDefinition.nodes,
|
|
579
|
+
state.flowDefinition._ui.nodes
|
|
580
|
+
);
|
|
581
|
+
|
|
526
582
|
state.dirtyDate = new Date();
|
|
527
583
|
});
|
|
528
584
|
},
|
|
@@ -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
|
});
|