@nyaruka/temba-components 0.131.3 → 0.133.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 +22 -0
- package/demo/components/flow/example.html +1 -0
- package/demo/static/css/tailwind.css +30019 -0
- package/dist/temba-components.js +449 -417
- package/dist/temba-components.js.map +1 -1
- package/out-tsc/src/display/Chat.js +26 -6
- package/out-tsc/src/display/Chat.js.map +1 -1
- package/out-tsc/src/display/FloatingTab.js +4 -4
- package/out-tsc/src/display/FloatingTab.js.map +1 -1
- package/out-tsc/src/events.js.map +1 -1
- package/out-tsc/src/flow/CanvasNode.js +124 -58
- package/out-tsc/src/flow/CanvasNode.js.map +1 -1
- package/out-tsc/src/flow/Editor.js +89 -40
- package/out-tsc/src/flow/Editor.js.map +1 -1
- package/out-tsc/src/flow/NodeTypeSelector.js +8 -2
- package/out-tsc/src/flow/NodeTypeSelector.js.map +1 -1
- package/out-tsc/src/flow/config.js +17 -4
- package/out-tsc/src/flow/config.js.map +1 -1
- package/out-tsc/src/flow/nodes/split_by_llm_categorize.js +2 -2
- package/out-tsc/src/flow/nodes/split_by_llm_categorize.js.map +1 -1
- package/out-tsc/src/flow/nodes/split_by_run_result.js +6 -0
- package/out-tsc/src/flow/nodes/split_by_run_result.js.map +1 -1
- package/out-tsc/src/flow/types.js.map +1 -1
- package/out-tsc/src/layout/FloatingWindow.js +1 -2
- package/out-tsc/src/layout/FloatingWindow.js.map +1 -1
- package/out-tsc/src/list/ContentMenu.js +1 -0
- package/out-tsc/src/list/ContentMenu.js.map +1 -1
- package/out-tsc/src/list/SortableList.js +3 -2
- package/out-tsc/src/list/SortableList.js.map +1 -1
- package/out-tsc/src/live/ContactChat.js +105 -69
- package/out-tsc/src/live/ContactChat.js.map +1 -1
- package/out-tsc/src/store/AppState.js +39 -1
- package/out-tsc/src/store/AppState.js.map +1 -1
- package/out-tsc/src/utils.js +3 -3
- package/out-tsc/src/utils.js.map +1 -1
- package/out-tsc/test/ActionHelper.js +6 -5
- package/out-tsc/test/ActionHelper.js.map +1 -1
- package/out-tsc/test/actions/send_broadcast.test.js +1 -1
- package/out-tsc/test/actions/send_broadcast.test.js.map +1 -1
- package/out-tsc/test/nodes/split_by_run_result.test.js +83 -0
- package/out-tsc/test/nodes/split_by_run_result.test.js.map +1 -1
- package/out-tsc/test/temba-backwards-compatibility.test.js +30 -0
- package/out-tsc/test/temba-backwards-compatibility.test.js.map +1 -0
- package/out-tsc/test/temba-contact-chat.test.js +28 -13
- package/out-tsc/test/temba-contact-chat.test.js.map +1 -1
- package/out-tsc/test/temba-floating-window.test.js +0 -2
- package/out-tsc/test/temba-floating-window.test.js.map +1 -1
- package/out-tsc/test/temba-flow-editor-node.test.js +109 -0
- package/out-tsc/test/temba-flow-editor-node.test.js.map +1 -1
- package/out-tsc/test/temba-localization.test.js +24 -5
- package/out-tsc/test/temba-localization.test.js.map +1 -1
- package/out-tsc/test/temba-node-type-selector.test.js +70 -3
- package/out-tsc/test/temba-node-type-selector.test.js.map +1 -1
- package/out-tsc/test/temba-utils-uuid.test.js +45 -1
- package/out-tsc/test/temba-utils-uuid.test.js.map +1 -1
- package/out-tsc/test/utils.test.js +3 -3
- 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_broadcast/render/with-attachments.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/default.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/hover.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/src/display/Chat.ts +29 -7
- package/src/display/FloatingTab.ts +4 -4
- package/src/events.ts +1 -4
- package/src/flow/CanvasNode.ts +130 -57
- package/src/flow/Editor.ts +107 -40
- package/src/flow/NodeTypeSelector.ts +7 -1
- package/src/flow/config.ts +22 -4
- package/src/flow/nodes/split_by_llm_categorize.ts +2 -8
- package/src/flow/nodes/split_by_run_result.ts +7 -0
- package/src/flow/types.ts +2 -0
- package/src/layout/FloatingWindow.ts +1 -3
- package/src/list/ContentMenu.ts +1 -0
- package/src/list/SortableList.ts +3 -2
- package/src/live/ContactChat.ts +112 -78
- package/src/store/AppState.ts +53 -1
- package/src/utils.ts +3 -3
- package/test/ActionHelper.ts +13 -5
- package/test/actions/send_broadcast.test.ts +2 -1
- package/test/nodes/split_by_run_result.test.ts +99 -0
- package/test/temba-backwards-compatibility.test.ts +37 -0
- package/test/temba-contact-chat.test.ts +28 -13
- package/test/temba-floating-window.test.ts +0 -2
- package/test/temba-flow-editor-node.test.ts +129 -0
- package/test/temba-localization.test.ts +29 -5
- package/test/temba-node-type-selector.test.ts +89 -3
- package/test/temba-utils-uuid.test.ts +61 -1
- package/test/utils.test.ts +8 -3
- package/test-assets/contacts/history.json +22 -9
- package/web-test-runner.config.mjs +3 -3
|
@@ -3,6 +3,7 @@ import { split_by_run_result } from '../../src/flow/nodes/split_by_run_result';
|
|
|
3
3
|
import { Node } from '../../src/store/flow-definition';
|
|
4
4
|
import { NodeTest } from '../NodeHelper';
|
|
5
5
|
import { zustand } from '../../src/store/AppState';
|
|
6
|
+
import { NODE_CONFIG } from '../../src/flow/config';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Test suite for the split_by_run_result node configuration.
|
|
@@ -928,6 +929,34 @@ describe('split_by_run_result node config', () => {
|
|
|
928
929
|
expect(config.index).to.be.undefined;
|
|
929
930
|
expect(config.delimiter).to.be.undefined;
|
|
930
931
|
});
|
|
932
|
+
|
|
933
|
+
it('should set type to split_by_run_result_delimited when delimiter is enabled', () => {
|
|
934
|
+
const formData = {
|
|
935
|
+
result: [{ value: 'favorite_color', name: 'Favorite Color' }],
|
|
936
|
+
delimit_by: [{ value: '+', name: 'plusses' }],
|
|
937
|
+
delimit_index: [{ value: '3', name: 'fourth' }],
|
|
938
|
+
rules: [],
|
|
939
|
+
result_name: ''
|
|
940
|
+
};
|
|
941
|
+
|
|
942
|
+
const config = split_by_run_result.toUIConfig!(formData);
|
|
943
|
+
|
|
944
|
+
expect(config.type).to.equal('split_by_run_result_delimited');
|
|
945
|
+
});
|
|
946
|
+
|
|
947
|
+
it('should set type to split_by_run_result when delimiter is not enabled', () => {
|
|
948
|
+
const formData = {
|
|
949
|
+
result: [{ value: 'favorite_color', name: 'Favorite Color' }],
|
|
950
|
+
delimit_by: [{ value: '', name: "Don't delimit" }],
|
|
951
|
+
delimit_index: [{ value: '0', name: 'first' }],
|
|
952
|
+
rules: [],
|
|
953
|
+
result_name: ''
|
|
954
|
+
};
|
|
955
|
+
|
|
956
|
+
const config = split_by_run_result.toUIConfig!(formData);
|
|
957
|
+
|
|
958
|
+
expect(config.type).to.equal('split_by_run_result');
|
|
959
|
+
});
|
|
931
960
|
});
|
|
932
961
|
|
|
933
962
|
describe('round-trip tests', () => {
|
|
@@ -1106,4 +1135,74 @@ describe('split_by_run_result node config', () => {
|
|
|
1106
1135
|
});
|
|
1107
1136
|
});
|
|
1108
1137
|
});
|
|
1138
|
+
|
|
1139
|
+
describe('backwards compatibility', () => {
|
|
1140
|
+
it('should support split_by_run_result_delimited type from old flows', () => {
|
|
1141
|
+
// Verify that split_by_run_result_delimited points to the same config as split_by_run_result
|
|
1142
|
+
expect(NODE_CONFIG['split_by_run_result_delimited']).to.equal(
|
|
1143
|
+
NODE_CONFIG['split_by_run_result']
|
|
1144
|
+
);
|
|
1145
|
+
expect(NODE_CONFIG['split_by_run_result_delimited']).to.equal(
|
|
1146
|
+
split_by_run_result
|
|
1147
|
+
);
|
|
1148
|
+
|
|
1149
|
+
// Verify we can look up the config with the old type name
|
|
1150
|
+
const config = NODE_CONFIG['split_by_run_result_delimited'];
|
|
1151
|
+
expect(config).to.not.be.undefined;
|
|
1152
|
+
expect(config.name).to.equal('Split by Result');
|
|
1153
|
+
});
|
|
1154
|
+
|
|
1155
|
+
it('should correctly process old flow with split_by_run_result_delimited type', () => {
|
|
1156
|
+
// Simulate a node from an old flow with the delimited type
|
|
1157
|
+
const oldNode: Node = {
|
|
1158
|
+
uuid: 'old-node-uuid',
|
|
1159
|
+
actions: [],
|
|
1160
|
+
router: {
|
|
1161
|
+
type: 'switch',
|
|
1162
|
+
operand: '@(field(results.bloop, 0, "+"))',
|
|
1163
|
+
cases: [
|
|
1164
|
+
{
|
|
1165
|
+
uuid: 'case-1',
|
|
1166
|
+
type: 'has_any_word',
|
|
1167
|
+
arguments: ['red'],
|
|
1168
|
+
category_uuid: 'cat-1'
|
|
1169
|
+
}
|
|
1170
|
+
],
|
|
1171
|
+
categories: [
|
|
1172
|
+
{ uuid: 'cat-1', name: 'Red', exit_uuid: 'exit-1' },
|
|
1173
|
+
{ uuid: 'cat-other', name: 'Other', exit_uuid: 'exit-other' }
|
|
1174
|
+
],
|
|
1175
|
+
default_category_uuid: 'cat-other'
|
|
1176
|
+
},
|
|
1177
|
+
exits: [
|
|
1178
|
+
{ uuid: 'exit-1', destination_uuid: null },
|
|
1179
|
+
{ uuid: 'exit-other', destination_uuid: null }
|
|
1180
|
+
]
|
|
1181
|
+
};
|
|
1182
|
+
|
|
1183
|
+
const oldNodeUI = {
|
|
1184
|
+
type: 'split_by_run_result_delimited',
|
|
1185
|
+
config: {
|
|
1186
|
+
operand: {
|
|
1187
|
+
id: 'bloop',
|
|
1188
|
+
name: 'Bloop',
|
|
1189
|
+
type: 'result'
|
|
1190
|
+
},
|
|
1191
|
+
index: 0,
|
|
1192
|
+
delimiter: '+'
|
|
1193
|
+
}
|
|
1194
|
+
};
|
|
1195
|
+
|
|
1196
|
+
// Get config using the old type name - this should work due to backwards compatibility
|
|
1197
|
+
const config = NODE_CONFIG[oldNodeUI.type];
|
|
1198
|
+
expect(config).to.not.be.undefined;
|
|
1199
|
+
|
|
1200
|
+
// Verify we can convert to form data
|
|
1201
|
+
const formData = config.toFormData!(oldNode, oldNodeUI);
|
|
1202
|
+
expect(formData.result[0].id).to.equal('bloop');
|
|
1203
|
+
expect(formData.result[0].name).to.equal('Bloop');
|
|
1204
|
+
expect(formData.delimit_by[0].value).to.equal('+');
|
|
1205
|
+
expect(formData.delimit_index[0].value).to.equal('0');
|
|
1206
|
+
});
|
|
1207
|
+
});
|
|
1109
1208
|
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { expect } from '@open-wc/testing';
|
|
2
|
+
import { NODE_CONFIG } from '../src/flow/config';
|
|
3
|
+
|
|
4
|
+
describe('Backwards Compatibility', () => {
|
|
5
|
+
describe('split_by_run_result_delimited alias', () => {
|
|
6
|
+
it('should map split_by_run_result_delimited to split_by_run_result config', () => {
|
|
7
|
+
// verify the alias exists in NODE_CONFIG
|
|
8
|
+
expect(NODE_CONFIG['split_by_run_result_delimited']).to.exist;
|
|
9
|
+
|
|
10
|
+
// verify it points to the same config as split_by_run_result
|
|
11
|
+
expect(NODE_CONFIG['split_by_run_result_delimited']).to.equal(
|
|
12
|
+
NODE_CONFIG['split_by_run_result']
|
|
13
|
+
);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('should have the correct type on the split_by_run_result config', () => {
|
|
17
|
+
const config = NODE_CONFIG['split_by_run_result'];
|
|
18
|
+
expect(config.type).to.equal('split_by_run_result');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('should declare the alias in the config', () => {
|
|
22
|
+
const config = NODE_CONFIG['split_by_run_result'];
|
|
23
|
+
expect(config.aliases).to.exist;
|
|
24
|
+
expect(config.aliases).to.include('split_by_run_result_delimited');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should allow old flows with split_by_run_result_delimited type to load', () => {
|
|
28
|
+
// simulate loading an old flow with the delimited type
|
|
29
|
+
const oldType = 'split_by_run_result_delimited';
|
|
30
|
+
const config = NODE_CONFIG[oldType];
|
|
31
|
+
|
|
32
|
+
expect(config).to.exist;
|
|
33
|
+
expect(config.name).to.equal('Split by Result');
|
|
34
|
+
expect(config.group).to.equal('split');
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
});
|
|
@@ -55,7 +55,7 @@ describe('temba-contact-chat', () => {
|
|
|
55
55
|
mockedNow = mockNow('2021-03-31T00:31:00.000-00:00');
|
|
56
56
|
clearMockPosts();
|
|
57
57
|
mockGET(
|
|
58
|
-
/\/contact\/
|
|
58
|
+
/\/contact\/chat\/contact-.*/,
|
|
59
59
|
'/test-assets/contacts/history.json'
|
|
60
60
|
);
|
|
61
61
|
|
|
@@ -125,11 +125,16 @@ describe('temba-contact-chat', () => {
|
|
|
125
125
|
await updateComponent(compose, text);
|
|
126
126
|
|
|
127
127
|
const response_body = {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
128
|
+
event: {
|
|
129
|
+
uuid: 'msg-uuid',
|
|
130
|
+
contact: { uuid: 'contact-dave-active', name: 'Dave Matthews' },
|
|
131
|
+
msg: {
|
|
132
|
+
text: text,
|
|
133
|
+
attachments: []
|
|
134
|
+
}
|
|
135
|
+
}
|
|
131
136
|
};
|
|
132
|
-
mockPOST(/
|
|
137
|
+
mockPOST(/contact\/chat\/contact-dave-active\//, response_body);
|
|
133
138
|
|
|
134
139
|
const listener = oneEvent(compose, CustomEventType.Submitted, false);
|
|
135
140
|
await typeInto('temba-contact-chat:temba-compose', text, true, true);
|
|
@@ -149,14 +154,19 @@ describe('temba-contact-chat', () => {
|
|
|
149
154
|
await updateComponent(compose, null, attachments);
|
|
150
155
|
const response_attachments = getResponseSuccessFiles(attachments);
|
|
151
156
|
const response_body = {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
157
|
+
event: {
|
|
158
|
+
uuid: 'msg-uuid',
|
|
159
|
+
contact: { uuid: 'contact-dave-active', name: 'Dave Matthews' },
|
|
160
|
+
msg: {
|
|
161
|
+
text: '',
|
|
162
|
+
attachments: response_attachments
|
|
163
|
+
}
|
|
164
|
+
}
|
|
155
165
|
};
|
|
156
166
|
const response_headers = {};
|
|
157
167
|
const response_status = '200';
|
|
158
168
|
mockPOST(
|
|
159
|
-
/
|
|
169
|
+
/contact\/chat\/contact-dave-active\//,
|
|
160
170
|
response_body,
|
|
161
171
|
response_headers,
|
|
162
172
|
response_status
|
|
@@ -184,11 +194,16 @@ describe('temba-contact-chat', () => {
|
|
|
184
194
|
await updateComponent(compose, text, attachments);
|
|
185
195
|
const response_attachments = getResponseSuccessFiles(attachments);
|
|
186
196
|
const response_body = {
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
197
|
+
event: {
|
|
198
|
+
uuid: 'msg-uuid',
|
|
199
|
+
contact: { uuid: 'contact-dave-active', name: 'Dave Matthews' },
|
|
200
|
+
msg: {
|
|
201
|
+
text,
|
|
202
|
+
attachments: response_attachments
|
|
203
|
+
}
|
|
204
|
+
}
|
|
190
205
|
};
|
|
191
|
-
mockPOST(/
|
|
206
|
+
mockPOST(/contact\/chat\/contact-dave-active\//, response_body);
|
|
192
207
|
|
|
193
208
|
// press enter
|
|
194
209
|
const listener = oneEvent(compose, CustomEventType.Submitted, false);
|
|
@@ -51,7 +51,6 @@ describe('temba-floating-window', () => {
|
|
|
51
51
|
)) as FloatingWindow;
|
|
52
52
|
|
|
53
53
|
expect(window.hidden).to.equal(true);
|
|
54
|
-
expect(window.classList.contains('hidden')).to.equal(true);
|
|
55
54
|
});
|
|
56
55
|
|
|
57
56
|
it('can be shown and hidden', async () => {
|
|
@@ -74,7 +73,6 @@ describe('temba-floating-window', () => {
|
|
|
74
73
|
window.hide();
|
|
75
74
|
await window.updateComplete;
|
|
76
75
|
expect(window.hidden).to.equal(true);
|
|
77
|
-
expect(window.classList.contains('hidden')).to.equal(true);
|
|
78
76
|
});
|
|
79
77
|
|
|
80
78
|
it('fires close event when close button clicked', async () => {
|
|
@@ -1200,6 +1200,135 @@ describe('EditorNode', () => {
|
|
|
1200
1200
|
// 3. New JSPlumb connections are created with connectIds
|
|
1201
1201
|
// This sequence ensures JSPlumb visuals stay in sync with the flow definition
|
|
1202
1202
|
});
|
|
1203
|
+
|
|
1204
|
+
it('reroutes connections when removing node with multiple exits pointing to same destination', async () => {
|
|
1205
|
+
// Test case: node with multiple exits, but all point to the same destination
|
|
1206
|
+
const mockNode: Node = {
|
|
1207
|
+
uuid: 'test-node',
|
|
1208
|
+
actions: [
|
|
1209
|
+
{
|
|
1210
|
+
type: 'send_msg',
|
|
1211
|
+
uuid: 'action-1',
|
|
1212
|
+
text: 'Hello',
|
|
1213
|
+
quick_replies: []
|
|
1214
|
+
} as any
|
|
1215
|
+
],
|
|
1216
|
+
exits: [
|
|
1217
|
+
{ uuid: 'exit-1', destination_uuid: 'node-after' },
|
|
1218
|
+
{ uuid: 'exit-2', destination_uuid: 'node-after' },
|
|
1219
|
+
{ uuid: 'exit-3', destination_uuid: 'node-after' }
|
|
1220
|
+
]
|
|
1221
|
+
};
|
|
1222
|
+
|
|
1223
|
+
editorNode['node'] = mockNode;
|
|
1224
|
+
|
|
1225
|
+
const mockFlowDefinition = {
|
|
1226
|
+
nodes: [
|
|
1227
|
+
{
|
|
1228
|
+
uuid: 'node-before',
|
|
1229
|
+
exits: [{ uuid: 'exit-before', destination_uuid: 'test-node' }]
|
|
1230
|
+
},
|
|
1231
|
+
mockNode,
|
|
1232
|
+
{
|
|
1233
|
+
uuid: 'node-after',
|
|
1234
|
+
exits: []
|
|
1235
|
+
}
|
|
1236
|
+
]
|
|
1237
|
+
};
|
|
1238
|
+
|
|
1239
|
+
// Verify all exits point to the same destination
|
|
1240
|
+
const destinations = mockNode.exits
|
|
1241
|
+
.map((exit) => exit.destination_uuid)
|
|
1242
|
+
.filter((dest) => dest);
|
|
1243
|
+
|
|
1244
|
+
expect(destinations).to.have.length(3);
|
|
1245
|
+
expect(destinations.every((dest) => dest === 'node-after')).to.be.true;
|
|
1246
|
+
|
|
1247
|
+
// Find incoming connections
|
|
1248
|
+
const incomingConnections: {
|
|
1249
|
+
exitUuid: string;
|
|
1250
|
+
sourceNodeUuid: string;
|
|
1251
|
+
}[] = [];
|
|
1252
|
+
|
|
1253
|
+
for (const node of mockFlowDefinition.nodes) {
|
|
1254
|
+
if (node.uuid !== mockNode.uuid) {
|
|
1255
|
+
for (const exit of node.exits) {
|
|
1256
|
+
if (exit.destination_uuid === mockNode.uuid) {
|
|
1257
|
+
incomingConnections.push({
|
|
1258
|
+
exitUuid: exit.uuid,
|
|
1259
|
+
sourceNodeUuid: node.uuid
|
|
1260
|
+
});
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
// Verify we found incoming connections
|
|
1267
|
+
expect(incomingConnections).to.have.length(1);
|
|
1268
|
+
expect(incomingConnections[0].exitUuid).to.equal('exit-before');
|
|
1269
|
+
|
|
1270
|
+
// This validates that when a node has multiple exits but they all point
|
|
1271
|
+
// to the same destination, the rerouting logic should still apply
|
|
1272
|
+
});
|
|
1273
|
+
|
|
1274
|
+
it('does not reroute connections when node has exits with different destinations', async () => {
|
|
1275
|
+
// Test case: node with multiple exits pointing to different destinations
|
|
1276
|
+
const mockNode: Node = {
|
|
1277
|
+
uuid: 'test-node',
|
|
1278
|
+
actions: [
|
|
1279
|
+
{
|
|
1280
|
+
type: 'send_msg',
|
|
1281
|
+
uuid: 'action-1',
|
|
1282
|
+
text: 'Hello',
|
|
1283
|
+
quick_replies: []
|
|
1284
|
+
} as any
|
|
1285
|
+
],
|
|
1286
|
+
exits: [
|
|
1287
|
+
{ uuid: 'exit-1', destination_uuid: 'node-after-1' },
|
|
1288
|
+
{ uuid: 'exit-2', destination_uuid: 'node-after-2' }
|
|
1289
|
+
]
|
|
1290
|
+
};
|
|
1291
|
+
|
|
1292
|
+
const destinations = mockNode.exits
|
|
1293
|
+
.map((exit) => exit.destination_uuid)
|
|
1294
|
+
.filter((dest) => dest);
|
|
1295
|
+
|
|
1296
|
+
// Verify exits point to different destinations
|
|
1297
|
+
expect(destinations).to.have.length(2);
|
|
1298
|
+
expect(destinations.every((dest) => dest === destinations[0])).to.be
|
|
1299
|
+
.false;
|
|
1300
|
+
|
|
1301
|
+
// This validates that rerouting does NOT apply when exits point to different places
|
|
1302
|
+
});
|
|
1303
|
+
|
|
1304
|
+
it('does not reroute connections when node has exits with null destinations', async () => {
|
|
1305
|
+
// Test case: node with some exits having null destinations
|
|
1306
|
+
const mockNode: Node = {
|
|
1307
|
+
uuid: 'test-node',
|
|
1308
|
+
actions: [
|
|
1309
|
+
{
|
|
1310
|
+
type: 'send_msg',
|
|
1311
|
+
uuid: 'action-1',
|
|
1312
|
+
text: 'Hello',
|
|
1313
|
+
quick_replies: []
|
|
1314
|
+
} as any
|
|
1315
|
+
],
|
|
1316
|
+
exits: [
|
|
1317
|
+
{ uuid: 'exit-1', destination_uuid: 'node-after' },
|
|
1318
|
+
{ uuid: 'exit-2', destination_uuid: null }
|
|
1319
|
+
]
|
|
1320
|
+
};
|
|
1321
|
+
|
|
1322
|
+
const destinations = mockNode.exits
|
|
1323
|
+
.map((exit) => exit.destination_uuid)
|
|
1324
|
+
.filter((dest) => dest);
|
|
1325
|
+
|
|
1326
|
+
// Verify not all exits have destinations
|
|
1327
|
+
expect(destinations).to.have.length(1);
|
|
1328
|
+
expect(destinations.length).to.not.equal(mockNode.exits.length);
|
|
1329
|
+
|
|
1330
|
+
// This validates that rerouting does NOT apply when some exits have no destination
|
|
1331
|
+
});
|
|
1203
1332
|
});
|
|
1204
1333
|
|
|
1205
1334
|
describe('add action button', () => {
|
|
@@ -13,7 +13,22 @@ describe('Localization Editing', () => {
|
|
|
13
13
|
const languageNames: Record<string, string> = {
|
|
14
14
|
eng: 'English',
|
|
15
15
|
fra: 'French',
|
|
16
|
-
|
|
16
|
+
spa: 'Spanish'
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const setupWorkspace = () => {
|
|
20
|
+
zustand.setState({
|
|
21
|
+
workspace: {
|
|
22
|
+
uuid: 'test-workspace',
|
|
23
|
+
name: 'Test Workspace',
|
|
24
|
+
languages: ['eng', 'fra', 'spa'],
|
|
25
|
+
primary_language: 'eng',
|
|
26
|
+
timezone: 'UTC',
|
|
27
|
+
date_style: 'day_first',
|
|
28
|
+
country: 'US',
|
|
29
|
+
anon: false
|
|
30
|
+
}
|
|
31
|
+
});
|
|
17
32
|
};
|
|
18
33
|
|
|
19
34
|
const buildCategoryFlowDefinition = (
|
|
@@ -89,6 +104,9 @@ describe('Localization Editing', () => {
|
|
|
89
104
|
});
|
|
90
105
|
|
|
91
106
|
beforeEach(async () => {
|
|
107
|
+
// Set workspace with languages
|
|
108
|
+
setupWorkspace();
|
|
109
|
+
|
|
92
110
|
// Create a flow definition with localization data
|
|
93
111
|
const flowDefinition: FlowDefinition = {
|
|
94
112
|
uuid: 'test-flow',
|
|
@@ -98,7 +116,7 @@ describe('Localization Editing', () => {
|
|
|
98
116
|
revision: 1,
|
|
99
117
|
spec_version: '14.3',
|
|
100
118
|
localization: {
|
|
101
|
-
|
|
119
|
+
spa: {
|
|
102
120
|
'action-1': {
|
|
103
121
|
text: ['Hola mundo'],
|
|
104
122
|
quick_replies: ['Sí', 'No']
|
|
@@ -225,12 +243,12 @@ describe('Localization Editing', () => {
|
|
|
225
243
|
const select = windowEl.querySelector('temba-select') as any;
|
|
226
244
|
expect(select).to.exist;
|
|
227
245
|
|
|
228
|
-
select.values = [{ name: 'Spanish', value: '
|
|
246
|
+
select.values = [{ name: 'Spanish', value: 'spa' }];
|
|
229
247
|
select.dispatchEvent(new CustomEvent('change', { bubbles: true }));
|
|
230
248
|
await editor.updateComplete;
|
|
231
249
|
|
|
232
250
|
const state = zustand.getState();
|
|
233
|
-
expect(state.languageCode).to.equal('
|
|
251
|
+
expect(state.languageCode).to.equal('spa');
|
|
234
252
|
const summary = windowEl.querySelector('.localization-progress-summary');
|
|
235
253
|
expect(summary?.textContent.trim()).to.equal('All items are translated.');
|
|
236
254
|
});
|
|
@@ -238,6 +256,8 @@ describe('Localization Editing', () => {
|
|
|
238
256
|
it('should include category translations when include categories is enabled', async () => {
|
|
239
257
|
editor?.remove();
|
|
240
258
|
|
|
259
|
+
setupWorkspace();
|
|
260
|
+
|
|
241
261
|
const categoryFlowDefinition: FlowDefinition =
|
|
242
262
|
buildCategoryFlowDefinition();
|
|
243
263
|
|
|
@@ -286,6 +306,8 @@ describe('Localization Editing', () => {
|
|
|
286
306
|
editor?.remove();
|
|
287
307
|
editor = null;
|
|
288
308
|
|
|
309
|
+
setupWorkspace();
|
|
310
|
+
|
|
289
311
|
const flowDefinition = buildCategoryFlowDefinition({
|
|
290
312
|
fra: {
|
|
291
313
|
'cat-1': { name: ['Premier choix'] },
|
|
@@ -334,6 +356,8 @@ describe('Localization Editing', () => {
|
|
|
334
356
|
editor?.remove();
|
|
335
357
|
editor = null;
|
|
336
358
|
|
|
359
|
+
setupWorkspace();
|
|
360
|
+
|
|
337
361
|
const flowDefinition = buildCategoryFlowDefinition({
|
|
338
362
|
fra: {
|
|
339
363
|
'cat-1': { name: ['Premier choix'] }
|
|
@@ -518,7 +542,7 @@ describe('Localization Editing', () => {
|
|
|
518
542
|
|
|
519
543
|
it('should include language name in dialog header when translating', async () => {
|
|
520
544
|
// Switch to Spanish
|
|
521
|
-
zustand.getState().setLanguageCode('
|
|
545
|
+
zustand.getState().setLanguageCode('spa');
|
|
522
546
|
|
|
523
547
|
const action: SendMsg = {
|
|
524
548
|
type: 'send_msg',
|
|
@@ -238,8 +238,9 @@ describe('temba-node-type-selector', () => {
|
|
|
238
238
|
item.textContent?.trim()
|
|
239
239
|
);
|
|
240
240
|
|
|
241
|
-
//
|
|
242
|
-
|
|
241
|
+
// split_by_llm_categorize (Split by AI) is filtered out for old editor compatibility
|
|
242
|
+
// so it should NOT appear even when AI feature is enabled
|
|
243
|
+
expect(titles).to.not.include('Split by AI');
|
|
243
244
|
});
|
|
244
245
|
|
|
245
246
|
it('filters by features - without AI feature, AI splits are hidden', async () => {
|
|
@@ -256,7 +257,8 @@ describe('temba-node-type-selector', () => {
|
|
|
256
257
|
item.textContent?.trim()
|
|
257
258
|
);
|
|
258
259
|
|
|
259
|
-
// without ai feature, should not have Split by AI
|
|
260
|
+
// without ai feature, should not have Call AI or Split by AI
|
|
261
|
+
expect(titles).to.not.include('Call AI');
|
|
260
262
|
expect(titles).to.not.include('Split by AI');
|
|
261
263
|
});
|
|
262
264
|
|
|
@@ -352,4 +354,88 @@ describe('temba-node-type-selector', () => {
|
|
|
352
354
|
);
|
|
353
355
|
expect(isAvailableVoice).to.be.true;
|
|
354
356
|
});
|
|
357
|
+
|
|
358
|
+
describe('alias filtering', () => {
|
|
359
|
+
it('should not show split_by_run_result twice when aliases exist', async () => {
|
|
360
|
+
const selector = await createSelector();
|
|
361
|
+
|
|
362
|
+
selector.show('split', { x: 100, y: 100 });
|
|
363
|
+
await selector.updateComplete;
|
|
364
|
+
|
|
365
|
+
// Get all the node items rendered in the selector
|
|
366
|
+
const nodeItems = selector.shadowRoot!.querySelectorAll('.node-item');
|
|
367
|
+
|
|
368
|
+
// Count how many times "Split by Result" appears
|
|
369
|
+
let splitByResultCount = 0;
|
|
370
|
+
nodeItems.forEach((item) => {
|
|
371
|
+
const title = item.querySelector('.node-item-title');
|
|
372
|
+
if (title?.textContent?.includes('Split by Result')) {
|
|
373
|
+
splitByResultCount++;
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
// Should only appear once, not twice
|
|
378
|
+
expect(splitByResultCount).to.equal(1);
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
it('should not show split_by_run_result_delimited type in the selector', async () => {
|
|
382
|
+
const selector = await createSelector();
|
|
383
|
+
|
|
384
|
+
selector.show('split', { x: 100, y: 100 });
|
|
385
|
+
await selector.updateComplete;
|
|
386
|
+
|
|
387
|
+
// Get all the node items and check their data-type attributes
|
|
388
|
+
const nodeItems = selector.shadowRoot!.querySelectorAll('.node-item');
|
|
389
|
+
|
|
390
|
+
let foundDelimitedType = false;
|
|
391
|
+
nodeItems.forEach((item) => {
|
|
392
|
+
const typeAttr = item.getAttribute('data-type');
|
|
393
|
+
if (typeAttr === 'split_by_run_result_delimited') {
|
|
394
|
+
foundDelimitedType = true;
|
|
395
|
+
}
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
expect(foundDelimitedType).to.be.false;
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
it('should not show split_by_llm_categorize in split mode', async () => {
|
|
402
|
+
const selector = await createSelector();
|
|
403
|
+
|
|
404
|
+
selector.show('split', { x: 100, y: 100 });
|
|
405
|
+
await selector.updateComplete;
|
|
406
|
+
|
|
407
|
+
// Get all the node items and check their data-type attributes
|
|
408
|
+
const nodeItems = selector.shadowRoot!.querySelectorAll('.node-item');
|
|
409
|
+
|
|
410
|
+
let foundLLMCategorize = false;
|
|
411
|
+
nodeItems.forEach((item) => {
|
|
412
|
+
const typeAttr = item.getAttribute('data-type');
|
|
413
|
+
if (typeAttr === 'split_by_llm_categorize') {
|
|
414
|
+
foundLLMCategorize = true;
|
|
415
|
+
}
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
expect(foundLLMCategorize).to.be.false;
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
it('should not show split_by_llm_categorize in action mode', async () => {
|
|
422
|
+
const selector = await createSelector();
|
|
423
|
+
|
|
424
|
+
selector.show('action', { x: 100, y: 100 });
|
|
425
|
+
await selector.updateComplete;
|
|
426
|
+
|
|
427
|
+
// Get all the node items and check their data-type attributes
|
|
428
|
+
const nodeItems = selector.shadowRoot!.querySelectorAll('.node-item');
|
|
429
|
+
|
|
430
|
+
let foundLLMCategorize = false;
|
|
431
|
+
nodeItems.forEach((item) => {
|
|
432
|
+
const typeAttr = item.getAttribute('data-type');
|
|
433
|
+
if (typeAttr === 'split_by_llm_categorize') {
|
|
434
|
+
foundLLMCategorize = true;
|
|
435
|
+
}
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
expect(foundLLMCategorize).to.be.false;
|
|
439
|
+
});
|
|
440
|
+
});
|
|
355
441
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { assert } from '@open-wc/testing';
|
|
2
|
-
import { generateUUID } from '../src/utils';
|
|
2
|
+
import { generateUUID, generateUUIDv7 } from '../src/utils';
|
|
3
3
|
|
|
4
4
|
describe('UUID Generation', () => {
|
|
5
5
|
it('generates a valid UUID v4 format', () => {
|
|
@@ -45,4 +45,64 @@ describe('UUID Generation', () => {
|
|
|
45
45
|
// All should be unique
|
|
46
46
|
assert.equal(uuids.size, count, 'All generated UUIDs should be unique');
|
|
47
47
|
});
|
|
48
|
+
|
|
49
|
+
it('generates a valid UUID v7 format', () => {
|
|
50
|
+
const uuid = generateUUIDv7();
|
|
51
|
+
|
|
52
|
+
// check that it's a string
|
|
53
|
+
assert.isString(uuid);
|
|
54
|
+
|
|
55
|
+
// check UUID v7 format: xxxxxxxx-xxxx-7xxx-yxxx-xxxxxxxxxxxx
|
|
56
|
+
const uuidPattern =
|
|
57
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
58
|
+
assert.match(uuid, uuidPattern, 'Should match UUID v7 format');
|
|
59
|
+
|
|
60
|
+
// check length
|
|
61
|
+
assert.equal(uuid.length, 36);
|
|
62
|
+
|
|
63
|
+
// check that it contains hyphens in the right places
|
|
64
|
+
assert.equal(uuid[8], '-');
|
|
65
|
+
assert.equal(uuid[13], '-');
|
|
66
|
+
assert.equal(uuid[18], '-');
|
|
67
|
+
assert.equal(uuid[23], '-');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('generates unique UUIDs v7', () => {
|
|
71
|
+
const uuid1 = generateUUIDv7();
|
|
72
|
+
const uuid2 = generateUUIDv7();
|
|
73
|
+
const uuid3 = generateUUIDv7();
|
|
74
|
+
|
|
75
|
+
// all should be different
|
|
76
|
+
assert.notEqual(uuid1, uuid2);
|
|
77
|
+
assert.notEqual(uuid2, uuid3);
|
|
78
|
+
assert.notEqual(uuid1, uuid3);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('generates time-ordered UUIDs v7', () => {
|
|
82
|
+
const uuid1 = generateUUIDv7();
|
|
83
|
+
// small delay to ensure different timestamp
|
|
84
|
+
const delayPromise = new Promise((resolve) => setTimeout(resolve, 5));
|
|
85
|
+
return delayPromise.then(() => {
|
|
86
|
+
const uuid2 = generateUUIDv7();
|
|
87
|
+
|
|
88
|
+
// uuid v7 should be sortable by timestamp
|
|
89
|
+
// the first uuid should come before the second when compared as strings
|
|
90
|
+
assert.isTrue(
|
|
91
|
+
uuid1 < uuid2,
|
|
92
|
+
'Earlier UUID should be lexicographically smaller'
|
|
93
|
+
);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('generates many unique UUIDs v7', () => {
|
|
98
|
+
const uuids = new Set();
|
|
99
|
+
const count = 1000;
|
|
100
|
+
|
|
101
|
+
for (let i = 0; i < count; i++) {
|
|
102
|
+
uuids.add(generateUUIDv7());
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// all should be unique
|
|
106
|
+
assert.equal(uuids.size, count, 'All generated UUIDs should be unique');
|
|
107
|
+
});
|
|
48
108
|
});
|
package/test/utils.test.ts
CHANGED
|
@@ -13,7 +13,7 @@ import { expect, fixture, html, assert } from '@open-wc/testing';
|
|
|
13
13
|
const style = document.createElement('style');
|
|
14
14
|
style.textContent = `
|
|
15
15
|
* {
|
|
16
|
-
--transition-
|
|
16
|
+
--transition-speed: 0ms !important;
|
|
17
17
|
}
|
|
18
18
|
`;
|
|
19
19
|
document.head.appendChild(style);
|
|
@@ -240,7 +240,11 @@ export const waitForCondition = async (
|
|
|
240
240
|
}
|
|
241
241
|
};
|
|
242
242
|
|
|
243
|
-
export const assertScreenshot = async (
|
|
243
|
+
export const assertScreenshot = async (
|
|
244
|
+
filename: string,
|
|
245
|
+
clip: Clip,
|
|
246
|
+
waitForNetwork: boolean = false
|
|
247
|
+
) => {
|
|
244
248
|
// detect if we're running in copilot's environment and use adaptive threshold
|
|
245
249
|
const isCopilotEnvironment = (window as any).isCopilotEnvironment;
|
|
246
250
|
const threshold = isCopilotEnvironment ? 1.0 : 0.1;
|
|
@@ -251,7 +255,8 @@ export const assertScreenshot = async (filename: string, clip: Clip) => {
|
|
|
251
255
|
`${filename}.png`,
|
|
252
256
|
clip,
|
|
253
257
|
exclude,
|
|
254
|
-
threshold
|
|
258
|
+
threshold,
|
|
259
|
+
waitForNetwork
|
|
255
260
|
);
|
|
256
261
|
} catch (error) {
|
|
257
262
|
if (error.message) {
|