@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
package/src/flow/config.ts
CHANGED
|
@@ -42,7 +42,7 @@ import { wait_for_response } from './nodes/wait_for_response';
|
|
|
42
42
|
|
|
43
43
|
export const ACTION_CONFIG: {
|
|
44
44
|
[key: string]: ActionConfig;
|
|
45
|
-
} = {
|
|
45
|
+
} = registerConfigWithAliases({
|
|
46
46
|
say_msg,
|
|
47
47
|
play_audio,
|
|
48
48
|
set_contact_field,
|
|
@@ -60,11 +60,29 @@ export const ACTION_CONFIG: {
|
|
|
60
60
|
add_contact_urn,
|
|
61
61
|
add_input_labels,
|
|
62
62
|
request_optin
|
|
63
|
-
};
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// Helper to register a config and its aliases
|
|
66
|
+
function registerConfigWithAliases<T extends NodeConfig | ActionConfig>(
|
|
67
|
+
config: Record<string, T>
|
|
68
|
+
): Record<string, T> {
|
|
69
|
+
const result = { ...config };
|
|
70
|
+
|
|
71
|
+
// Register aliases for each config
|
|
72
|
+
Object.values(config).forEach((cfg) => {
|
|
73
|
+
if (cfg.aliases) {
|
|
74
|
+
cfg.aliases.forEach((alias) => {
|
|
75
|
+
result[alias] = cfg;
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
return result;
|
|
81
|
+
}
|
|
64
82
|
|
|
65
83
|
export const NODE_CONFIG: {
|
|
66
84
|
[key: string]: NodeConfig;
|
|
67
|
-
} = {
|
|
85
|
+
} = registerConfigWithAliases({
|
|
68
86
|
execute_actions,
|
|
69
87
|
split_by_contact_field,
|
|
70
88
|
split_by_expression,
|
|
@@ -82,4 +100,4 @@ export const NODE_CONFIG: {
|
|
|
82
100
|
wait_for_menu,
|
|
83
101
|
wait_for_response,
|
|
84
102
|
split_by_airtime
|
|
85
|
-
};
|
|
103
|
+
});
|
|
@@ -1,10 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
FormData,
|
|
3
|
-
NodeConfig,
|
|
4
|
-
ACTION_GROUPS,
|
|
5
|
-
FlowTypes,
|
|
6
|
-
Features
|
|
7
|
-
} from '../types';
|
|
1
|
+
import { FormData, NodeConfig, ACTION_GROUPS, Features } from '../types';
|
|
8
2
|
import { CallLLM, Node } from '../../store/flow-definition';
|
|
9
3
|
import { generateUUID, createMultiCategoryRouter } from '../../utils';
|
|
10
4
|
import { html } from 'lit';
|
|
@@ -17,7 +11,7 @@ export const split_by_llm_categorize: NodeConfig = {
|
|
|
17
11
|
type: 'split_by_llm_categorize',
|
|
18
12
|
name: 'Split by AI',
|
|
19
13
|
group: ACTION_GROUPS.services,
|
|
20
|
-
flowTypes: [
|
|
14
|
+
flowTypes: [],
|
|
21
15
|
features: [Features.AI],
|
|
22
16
|
form: {
|
|
23
17
|
llm: {
|
|
@@ -53,6 +53,7 @@ const DELIMIT_BY_OPTIONS = [
|
|
|
53
53
|
export const split_by_run_result: NodeConfig = {
|
|
54
54
|
type: 'split_by_run_result',
|
|
55
55
|
name: 'Split by Result',
|
|
56
|
+
aliases: ['split_by_run_result_delimited'], // backwards compatibility with old flow editor
|
|
56
57
|
group: SPLIT_GROUPS.split,
|
|
57
58
|
flowTypes: [FlowTypes.VOICE, FlowTypes.MESSAGE, FlowTypes.BACKGROUND],
|
|
58
59
|
dialogSize: 'large',
|
|
@@ -253,6 +254,12 @@ export const split_by_run_result: NodeConfig = {
|
|
|
253
254
|
config.delimiter = delimitBy;
|
|
254
255
|
}
|
|
255
256
|
|
|
257
|
+
// Set the type based on whether delimiter is configured
|
|
258
|
+
// Use split_by_run_result_delimited for backward compatibility with old editor
|
|
259
|
+
config.type = hasDelimiter
|
|
260
|
+
? 'split_by_run_result_delimited'
|
|
261
|
+
: 'split_by_run_result';
|
|
262
|
+
|
|
256
263
|
return config;
|
|
257
264
|
},
|
|
258
265
|
|
package/src/flow/types.ts
CHANGED
|
@@ -99,6 +99,7 @@ export interface FormConfig {
|
|
|
99
99
|
export interface NodeConfig extends FormConfig {
|
|
100
100
|
type: string;
|
|
101
101
|
name?: string;
|
|
102
|
+
aliases?: string[]; // alternate type names for backwards compatibility (won't show in selector)
|
|
102
103
|
group?: ActionGroup | SplitGroup; // Nodes can use either when showAsAction is true
|
|
103
104
|
dialogSize?: 'small' | 'medium' | 'large' | 'xlarge';
|
|
104
105
|
action?: ActionConfig;
|
|
@@ -384,6 +385,7 @@ export const SPLIT_GROUP_METADATA: Record<SplitGroup, GroupMetadata> = {
|
|
|
384
385
|
|
|
385
386
|
export interface ActionConfig extends FormConfig {
|
|
386
387
|
name: string;
|
|
388
|
+
aliases?: string[]; // alternate type names for backwards compatibility (won't show in selector)
|
|
387
389
|
group: ActionGroup;
|
|
388
390
|
dialogSize?: 'small' | 'medium' | 'large' | 'xlarge';
|
|
389
391
|
evaluated?: string[];
|
|
@@ -18,7 +18,7 @@ export class FloatingWindow extends RapidElement {
|
|
|
18
18
|
transition: transform var(--transition-duration, 300ms) ease-in-out,
|
|
19
19
|
opacity var(--transition-duration, 300ms) ease-in-out;
|
|
20
20
|
position: fixed;
|
|
21
|
-
z-index:
|
|
21
|
+
z-index: 5000;
|
|
22
22
|
top: 100px;
|
|
23
23
|
background: white;
|
|
24
24
|
border-radius: 8px;
|
|
@@ -193,8 +193,6 @@ export class FloatingWindow extends RapidElement {
|
|
|
193
193
|
): void {
|
|
194
194
|
super.updated(changes);
|
|
195
195
|
if (changes.has('hidden')) {
|
|
196
|
-
this.classList.toggle('hidden', this.hidden);
|
|
197
|
-
|
|
198
196
|
// when hiding, reset positioning behavior to original
|
|
199
197
|
if (this.hidden && !changes.get('hidden')) {
|
|
200
198
|
if (this.defaultLeft === -1) {
|
package/src/list/ContentMenu.ts
CHANGED
package/src/list/SortableList.ts
CHANGED
|
@@ -577,8 +577,9 @@ export class SortableList extends RapidElement {
|
|
|
577
577
|
const fromIdx = originalDragIdx;
|
|
578
578
|
const toIdx = this.pendingDropIndex;
|
|
579
579
|
|
|
580
|
-
// only fire if the position actually changed
|
|
581
|
-
|
|
580
|
+
// only fire if the position actually changed AND this is not an external drag
|
|
581
|
+
// External drags are handled by external drop handlers
|
|
582
|
+
if (fromIdx !== toIdx && !this.isExternalDrag) {
|
|
582
583
|
this.fireCustomEvent(CustomEventType.OrderChanged, {
|
|
583
584
|
swap: [fromIdx, toIdx]
|
|
584
585
|
});
|
package/src/live/ContactChat.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
} from '../interfaces';
|
|
11
11
|
import {
|
|
12
12
|
fetchResults,
|
|
13
|
+
generateUUIDv7,
|
|
13
14
|
getUrl,
|
|
14
15
|
oxfordFn,
|
|
15
16
|
postJSON,
|
|
@@ -525,10 +526,12 @@ export class ContactChat extends ContactStoreElement {
|
|
|
525
526
|
private chat: Chat;
|
|
526
527
|
|
|
527
528
|
ticket = null;
|
|
528
|
-
|
|
529
|
-
|
|
529
|
+
beforeUUID: string = null; // for scrolling back through history
|
|
530
|
+
afterUUID: string = null; // for polling new messages
|
|
530
531
|
refreshId = null;
|
|
531
532
|
polling = false;
|
|
533
|
+
pollingInterval = 2000; // start at 2 seconds
|
|
534
|
+
lastFetchTime: number = null;
|
|
532
535
|
|
|
533
536
|
constructor() {
|
|
534
537
|
super();
|
|
@@ -582,10 +585,12 @@ export class ContactChat extends ContactStoreElement {
|
|
|
582
585
|
}
|
|
583
586
|
this.blockFetching = false;
|
|
584
587
|
this.ticket = null;
|
|
585
|
-
this.
|
|
586
|
-
this.
|
|
588
|
+
this.beforeUUID = null;
|
|
589
|
+
this.afterUUID = null;
|
|
587
590
|
this.refreshId = null;
|
|
588
591
|
this.polling = false;
|
|
592
|
+
this.pollingInterval = 2000;
|
|
593
|
+
this.lastFetchTime = null;
|
|
589
594
|
this.errorMessage = null;
|
|
590
595
|
|
|
591
596
|
const compose = this.shadowRoot.querySelector('temba-compose') as Compose;
|
|
@@ -629,12 +634,19 @@ export class ContactChat extends ContactStoreElement {
|
|
|
629
634
|
}
|
|
630
635
|
|
|
631
636
|
const genericError = 'Send failed, please try again.';
|
|
632
|
-
postJSON(`/
|
|
637
|
+
postJSON(`/contact/chat/${this.currentContact.uuid}/`, payload)
|
|
633
638
|
.then((response) => {
|
|
634
639
|
if (response.status < 400) {
|
|
640
|
+
const msg = this.createChatForMessageEvent(response.json.event);
|
|
641
|
+
this.chat.addMessages([msg], null, true);
|
|
642
|
+
// reset polling interval to 2 seconds after sending a message
|
|
643
|
+
this.pollingInterval = 2000;
|
|
635
644
|
this.checkForNewMessages();
|
|
636
645
|
composeEle.reset();
|
|
637
|
-
this.fireCustomEvent(CustomEventType.MessageSent, {
|
|
646
|
+
this.fireCustomEvent(CustomEventType.MessageSent, {
|
|
647
|
+
msg: payload,
|
|
648
|
+
response
|
|
649
|
+
});
|
|
638
650
|
} else {
|
|
639
651
|
this.errorMessage = genericError;
|
|
640
652
|
}
|
|
@@ -646,30 +658,28 @@ export class ContactChat extends ContactStoreElement {
|
|
|
646
658
|
|
|
647
659
|
private getEndpoint() {
|
|
648
660
|
if (this.contact) {
|
|
649
|
-
return `/contact/
|
|
661
|
+
return `/contact/chat/${this.contact}/`;
|
|
650
662
|
}
|
|
651
663
|
return null;
|
|
652
664
|
}
|
|
653
665
|
|
|
654
|
-
private scheduleRefresh() {
|
|
655
|
-
// knock five seconds off the newest event time so we are
|
|
656
|
-
// a little more aggressive about refreshing short term
|
|
657
|
-
let window = new Date().getTime() - this.newestEventTime / 1000 - 5000;
|
|
658
|
-
|
|
666
|
+
private scheduleRefresh(hasNewEvents = false) {
|
|
659
667
|
if (this.refreshId) {
|
|
660
668
|
clearTimeout(this.refreshId);
|
|
661
669
|
this.refreshId = null;
|
|
662
670
|
}
|
|
663
671
|
|
|
664
|
-
//
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
672
|
+
// reset to 2 seconds if we received new events
|
|
673
|
+
if (hasNewEvents) {
|
|
674
|
+
this.pollingInterval = 2000;
|
|
675
|
+
} else {
|
|
676
|
+
// increase interval by 1 second up to max of 15 seconds
|
|
677
|
+
this.pollingInterval = Math.min(this.pollingInterval + 1000, 15000);
|
|
678
|
+
}
|
|
669
679
|
|
|
670
680
|
this.refreshId = setTimeout(() => {
|
|
671
681
|
this.checkForNewMessages();
|
|
672
|
-
},
|
|
682
|
+
}, this.pollingInterval);
|
|
673
683
|
}
|
|
674
684
|
|
|
675
685
|
public getEventMessage(event: ContactEvent): ChatEvent {
|
|
@@ -812,13 +822,52 @@ export class ContactChat extends ContactStoreElement {
|
|
|
812
822
|
return null;
|
|
813
823
|
}
|
|
814
824
|
|
|
825
|
+
private createChatForMessageEvent(msgEvent: MsgEvent): any {
|
|
826
|
+
return {
|
|
827
|
+
id: msgEvent.uuid,
|
|
828
|
+
type: msgEvent.type === 'msg_received' ? 'msg_in' : 'msg_out',
|
|
829
|
+
user: this.getUserForEvent(msgEvent),
|
|
830
|
+
date: new Date(msgEvent.created_on),
|
|
831
|
+
attachments: msgEvent.msg.attachments,
|
|
832
|
+
text: msgEvent.msg.text,
|
|
833
|
+
sendError:
|
|
834
|
+
msgEvent._status &&
|
|
835
|
+
(msgEvent._status.status === 'errored' ||
|
|
836
|
+
msgEvent._status.status === 'failed'),
|
|
837
|
+
popup: html`<div
|
|
838
|
+
style="display: flex; flex-direction: row; align-items:center; justify-content: space-between;font-size:0.9em;line-height:1em;min-width:10em"
|
|
839
|
+
>
|
|
840
|
+
<div style="justify-content:left;text-align:left">
|
|
841
|
+
<temba-date
|
|
842
|
+
value=${msgEvent.created_on}
|
|
843
|
+
display="duration"
|
|
844
|
+
></temba-date>
|
|
845
|
+
|
|
846
|
+
${msgEvent.optin
|
|
847
|
+
? html`<div style="font-size:0.9em;color:#aaa">
|
|
848
|
+
${msgEvent.optin.name}
|
|
849
|
+
</div>`
|
|
850
|
+
: null}
|
|
851
|
+
</div>
|
|
852
|
+
${msgEvent._logs_url
|
|
853
|
+
? html`<a style="margin-left:0.5em" href="${msgEvent._logs_url}"
|
|
854
|
+
><temba-icon name="log"></temba-icon
|
|
855
|
+
></a>`
|
|
856
|
+
: null}
|
|
857
|
+
</div> `
|
|
858
|
+
};
|
|
859
|
+
}
|
|
860
|
+
|
|
815
861
|
private createMessages(page: ContactHistoryPage): ChatEvent[] {
|
|
816
862
|
if (page.events) {
|
|
817
863
|
let messages = [];
|
|
818
864
|
page.events.forEach((event) => {
|
|
819
|
-
|
|
820
|
-
if (
|
|
821
|
-
this.
|
|
865
|
+
// track the UUID of the newest event for polling
|
|
866
|
+
if (
|
|
867
|
+
!this.afterUUID ||
|
|
868
|
+
event.uuid.toLowerCase() > this.afterUUID.toLowerCase()
|
|
869
|
+
) {
|
|
870
|
+
this.afterUUID = event.uuid;
|
|
822
871
|
}
|
|
823
872
|
|
|
824
873
|
if (event.type === 'ticket_note_added') {
|
|
@@ -849,40 +898,7 @@ export class ContactChat extends ContactStoreElement {
|
|
|
849
898
|
event.type === 'ivr_created'
|
|
850
899
|
) {
|
|
851
900
|
const msgEvent = event as MsgEvent;
|
|
852
|
-
|
|
853
|
-
messages.push({
|
|
854
|
-
id: event.uuid,
|
|
855
|
-
type: msgEvent.type === 'msg_received' ? 'msg_in' : 'msg_out',
|
|
856
|
-
user: this.getUserForEvent(msgEvent),
|
|
857
|
-
date: new Date(msgEvent.created_on),
|
|
858
|
-
attachments: msgEvent.msg.attachments,
|
|
859
|
-
text: msgEvent.msg.text,
|
|
860
|
-
sendError:
|
|
861
|
-
msgEvent._status &&
|
|
862
|
-
(msgEvent._status.status === 'errored' ||
|
|
863
|
-
msgEvent._status.status === 'failed'),
|
|
864
|
-
popup: html`<div
|
|
865
|
-
style="display: flex; flex-direction: row; align-items:center; justify-content: space-between;font-size:0.9em;line-height:1em;min-width:10em"
|
|
866
|
-
>
|
|
867
|
-
<div style="justify-content:left;text-align:left">
|
|
868
|
-
<temba-date
|
|
869
|
-
value=${msgEvent.created_on}
|
|
870
|
-
display="duration"
|
|
871
|
-
></temba-date>
|
|
872
|
-
|
|
873
|
-
${msgEvent.optin
|
|
874
|
-
? html`<div style="font-size:0.9em;color:#aaa">
|
|
875
|
-
${msgEvent.optin.name}
|
|
876
|
-
</div>`
|
|
877
|
-
: null}
|
|
878
|
-
</div>
|
|
879
|
-
${msgEvent._logs_url
|
|
880
|
-
? html`<a style="margin-left:0.5em" href="${msgEvent._logs_url}"
|
|
881
|
-
><temba-icon name="log"></temba-icon
|
|
882
|
-
></a>`
|
|
883
|
-
: null}
|
|
884
|
-
</div> `
|
|
885
|
-
});
|
|
901
|
+
messages.push(this.createChatForMessageEvent(msgEvent));
|
|
886
902
|
} else {
|
|
887
903
|
const msg = this.getEventMessage(event);
|
|
888
904
|
if (msg) {
|
|
@@ -906,8 +922,9 @@ export class ContactChat extends ContactStoreElement {
|
|
|
906
922
|
|
|
907
923
|
const chat = this.chat;
|
|
908
924
|
const contactChat = this;
|
|
909
|
-
if (this.currentContact && this.
|
|
925
|
+
if (this.currentContact && this.afterUUID) {
|
|
910
926
|
this.polling = true;
|
|
927
|
+
this.lastFetchTime = Date.now();
|
|
911
928
|
const endpoint = this.getEndpoint();
|
|
912
929
|
if (!endpoint) {
|
|
913
930
|
return;
|
|
@@ -916,23 +933,24 @@ export class ContactChat extends ContactStoreElement {
|
|
|
916
933
|
const fetchContact = this.currentContact.uuid;
|
|
917
934
|
|
|
918
935
|
fetchContactHistory(
|
|
919
|
-
false,
|
|
920
936
|
endpoint,
|
|
921
937
|
this.currentTicket?.uuid,
|
|
922
938
|
null,
|
|
923
|
-
this.
|
|
939
|
+
this.afterUUID
|
|
924
940
|
).then((page: ContactHistoryPage) => {
|
|
925
941
|
if (fetchContact === this.currentContact.uuid) {
|
|
926
|
-
this.lastEventTime = page.next_before;
|
|
927
942
|
const messages = this.createMessages(page);
|
|
943
|
+
const hasNewEvents = messages.length > 0;
|
|
928
944
|
if (messages.length === 0) {
|
|
929
945
|
contactChat.blockFetching = true;
|
|
930
946
|
}
|
|
931
947
|
messages.reverse();
|
|
932
948
|
chat.addMessages(messages, null, true);
|
|
949
|
+
this.polling = false;
|
|
950
|
+
this.scheduleRefresh(hasNewEvents);
|
|
951
|
+
} else {
|
|
952
|
+
this.polling = false;
|
|
933
953
|
}
|
|
934
|
-
this.polling = false;
|
|
935
|
-
this.scheduleRefresh();
|
|
936
954
|
});
|
|
937
955
|
}
|
|
938
956
|
}
|
|
@@ -951,19 +969,37 @@ export class ContactChat extends ContactStoreElement {
|
|
|
951
969
|
return;
|
|
952
970
|
}
|
|
953
971
|
|
|
972
|
+
// initialize anchor UUID if not set (first fetch)
|
|
973
|
+
if (!this.beforeUUID && !this.afterUUID) {
|
|
974
|
+
// generate a UUID v7 for current time as the anchor
|
|
975
|
+
const anchorUUID = generateUUIDv7();
|
|
976
|
+
this.beforeUUID = anchorUUID;
|
|
977
|
+
this.afterUUID = anchorUUID;
|
|
978
|
+
}
|
|
979
|
+
|
|
954
980
|
fetchContactHistory(
|
|
955
|
-
false,
|
|
956
981
|
endpoint,
|
|
957
982
|
this.currentTicket?.uuid,
|
|
958
|
-
this.
|
|
983
|
+
this.beforeUUID,
|
|
984
|
+
null
|
|
959
985
|
).then((page: ContactHistoryPage) => {
|
|
960
|
-
this.lastEventTime = page.next_before;
|
|
961
986
|
const messages = this.createMessages(page);
|
|
962
987
|
messages.reverse();
|
|
963
988
|
|
|
964
989
|
if (messages.length === 0) {
|
|
965
990
|
contactChat.blockFetching = true;
|
|
991
|
+
} else if (page.next) {
|
|
992
|
+
// update beforeUUID for next fetch of older messages
|
|
993
|
+
this.beforeUUID = page.next;
|
|
994
|
+
} else {
|
|
995
|
+
// no more history, mark end and show oldest event date
|
|
996
|
+
contactChat.blockFetching = true;
|
|
997
|
+
if (page.events && page.events.length > 0) {
|
|
998
|
+
const oldestEvent = page.events[page.events.length - 1];
|
|
999
|
+
chat.setEndOfHistory(new Date(oldestEvent.created_on));
|
|
1000
|
+
}
|
|
966
1001
|
}
|
|
1002
|
+
|
|
967
1003
|
chat.addMessages(messages);
|
|
968
1004
|
this.scheduleRefresh();
|
|
969
1005
|
});
|
|
@@ -1230,34 +1266,32 @@ export const fetchContact = (endpoint: string): Promise<Contact> => {
|
|
|
1230
1266
|
});
|
|
1231
1267
|
};
|
|
1232
1268
|
export const fetchContactHistory = (
|
|
1233
|
-
reset: boolean,
|
|
1234
1269
|
endpoint: string,
|
|
1235
|
-
ticket: string,
|
|
1236
|
-
before:
|
|
1237
|
-
after:
|
|
1270
|
+
ticket: string = undefined,
|
|
1271
|
+
before: string = undefined,
|
|
1272
|
+
after: string = undefined
|
|
1238
1273
|
): Promise<ContactHistoryPage> => {
|
|
1239
|
-
if (reset) {
|
|
1240
|
-
pendingRequests.forEach((controller) => {
|
|
1241
|
-
controller.abort();
|
|
1242
|
-
});
|
|
1243
|
-
pendingRequests = [];
|
|
1244
|
-
}
|
|
1245
|
-
|
|
1246
1274
|
return new Promise<ContactHistoryPage>((resolve) => {
|
|
1247
1275
|
const controller = new AbortController();
|
|
1248
1276
|
pendingRequests.push(controller);
|
|
1249
1277
|
|
|
1250
1278
|
let url = endpoint;
|
|
1279
|
+
const params = [];
|
|
1280
|
+
|
|
1251
1281
|
if (before) {
|
|
1252
|
-
|
|
1282
|
+
params.push(`before=${before}`);
|
|
1253
1283
|
}
|
|
1254
1284
|
|
|
1255
1285
|
if (after) {
|
|
1256
|
-
|
|
1286
|
+
params.push(`after=${after}`);
|
|
1257
1287
|
}
|
|
1258
1288
|
|
|
1259
1289
|
if (ticket) {
|
|
1260
|
-
|
|
1290
|
+
params.push(`ticket=${ticket}`);
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
if (params.length > 0) {
|
|
1294
|
+
url += (url.includes('?') ? '&' : '?') + params.join('&');
|
|
1261
1295
|
}
|
|
1262
1296
|
|
|
1263
1297
|
const store = document.querySelector('temba-store') as Store;
|
package/src/store/AppState.ts
CHANGED
|
@@ -88,6 +88,7 @@ export interface AppState {
|
|
|
88
88
|
|
|
89
89
|
setFlowContents: (flow: FlowContents) => void;
|
|
90
90
|
setFlowInfo: (info: FlowInfo) => void;
|
|
91
|
+
setRevision: (revision: number) => void;
|
|
91
92
|
setLanguageCode: (languageCode: string) => void;
|
|
92
93
|
setDirtyDate: (date: Date) => void;
|
|
93
94
|
expandCanvas: (width: number, height: number) => void;
|
|
@@ -216,6 +217,12 @@ export const zustand = createStore<AppState>()(
|
|
|
216
217
|
});
|
|
217
218
|
},
|
|
218
219
|
|
|
220
|
+
setRevision: (revision: number) => {
|
|
221
|
+
set((state: AppState) => {
|
|
222
|
+
state.flowDefinition.revision = revision;
|
|
223
|
+
});
|
|
224
|
+
},
|
|
225
|
+
|
|
219
226
|
setLanguageCode: (languageCode: string) => {
|
|
220
227
|
set((state: AppState) => {
|
|
221
228
|
state.languageCode = languageCode;
|
|
@@ -264,10 +271,44 @@ export const zustand = createStore<AppState>()(
|
|
|
264
271
|
}
|
|
265
272
|
|
|
266
273
|
state.flowDefinition = produce(state.flowDefinition, (draft) => {
|
|
274
|
+
// For each node being removed, check if we should reroute connections
|
|
275
|
+
uuids.forEach((removedUuid) => {
|
|
276
|
+
const removedNode = draft.nodes.find(
|
|
277
|
+
(n) => n.uuid === removedUuid
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
if (!removedNode || !removedNode.exits.length) return;
|
|
281
|
+
|
|
282
|
+
// Get all destinations (filter out null/undefined)
|
|
283
|
+
const destinations = removedNode.exits
|
|
284
|
+
.map((exit) => exit.destination_uuid)
|
|
285
|
+
.filter((dest) => dest);
|
|
286
|
+
|
|
287
|
+
// Only proceed if all exits have destinations and they all point to the same place
|
|
288
|
+
if (
|
|
289
|
+
destinations.length === removedNode.exits.length &&
|
|
290
|
+
destinations.every((dest) => dest === destinations[0])
|
|
291
|
+
) {
|
|
292
|
+
const targetDestination = destinations[0];
|
|
293
|
+
|
|
294
|
+
// Find all nodes with exits pointing to the node being removed
|
|
295
|
+
draft.nodes.forEach((node) => {
|
|
296
|
+
node.exits.forEach((exit) => {
|
|
297
|
+
if (exit.destination_uuid === removedUuid) {
|
|
298
|
+
// Reroute to the same destination the removed node was going to
|
|
299
|
+
exit.destination_uuid = targetDestination;
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
// Remove the nodes
|
|
267
307
|
draft.nodes = draft.nodes.filter(
|
|
268
308
|
(node) => !uuids.includes(node.uuid)
|
|
269
309
|
);
|
|
270
310
|
|
|
311
|
+
// Clear any remaining connections to removed nodes that weren't rerouted
|
|
271
312
|
draft.nodes.forEach((node) => {
|
|
272
313
|
node.exits.forEach((exit) => {
|
|
273
314
|
if (uuids.includes(exit.destination_uuid)) {
|
|
@@ -308,10 +349,21 @@ export const zustand = createStore<AppState>()(
|
|
|
308
349
|
updateNodeUIConfig: (uuid: string, config: Record<string, any>) => {
|
|
309
350
|
set((state: AppState) => {
|
|
310
351
|
if (state.flowDefinition._ui.nodes[uuid]) {
|
|
352
|
+
// Handle type separately if provided
|
|
353
|
+
if (config.type !== undefined) {
|
|
354
|
+
state.flowDefinition._ui.nodes[uuid].type = config.type;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Update config (excluding type)
|
|
311
358
|
if (!state.flowDefinition._ui.nodes[uuid].config) {
|
|
312
359
|
state.flowDefinition._ui.nodes[uuid].config = {};
|
|
313
360
|
}
|
|
314
|
-
|
|
361
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
362
|
+
const { type, ...configWithoutType } = config;
|
|
363
|
+
Object.assign(
|
|
364
|
+
state.flowDefinition._ui.nodes[uuid].config,
|
|
365
|
+
configWithoutType
|
|
366
|
+
);
|
|
315
367
|
}
|
|
316
368
|
state.dirtyDate = new Date();
|
|
317
369
|
});
|
package/src/utils.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { Dialog } from './layout/Dialog';
|
|
|
5
5
|
import { Attachment, ContactField, Shortcut, Ticket, User } from './interfaces';
|
|
6
6
|
import ColorHash from 'color-hash';
|
|
7
7
|
import { Toast } from './display/Toast';
|
|
8
|
-
import { v4 as generateUUID } from 'uuid';
|
|
8
|
+
import { v4 as generateUUID, v7 as generateUUIDv7 } from 'uuid';
|
|
9
9
|
|
|
10
10
|
export const DEFAULT_MEDIA_ENDPOINT = '/api/v2/media.json';
|
|
11
11
|
|
|
@@ -918,8 +918,8 @@ export const getMiddle = (a: DOMRect, b: DOMRect) => {
|
|
|
918
918
|
return a.top + a.height / 2 - b.height / 2;
|
|
919
919
|
};
|
|
920
920
|
|
|
921
|
-
// Export the UUID
|
|
922
|
-
export { generateUUID };
|
|
921
|
+
// Export the UUID functions from the uuid package
|
|
922
|
+
export { generateUUID, generateUUIDv7 };
|
|
923
923
|
|
|
924
924
|
// Helper types for router creation
|
|
925
925
|
export interface RouterCategory {
|
package/test/ActionHelper.ts
CHANGED
|
@@ -71,12 +71,13 @@ export class ActionTest<T extends Action> {
|
|
|
71
71
|
*/
|
|
72
72
|
private async assertDialogScreenshot(
|
|
73
73
|
el: HTMLElement,
|
|
74
|
-
screenshotName: string
|
|
74
|
+
screenshotName: string,
|
|
75
|
+
waitForNetwork: boolean = false
|
|
75
76
|
) {
|
|
76
77
|
const dialog = el.shadowRoot
|
|
77
78
|
.querySelector('temba-dialog')
|
|
78
79
|
.shadowRoot.querySelector('.dialog-container') as HTMLElement;
|
|
79
|
-
await assertScreenshot(screenshotName, getClip(dialog));
|
|
80
|
+
await assertScreenshot(screenshotName, getClip(dialog), waitForNetwork);
|
|
80
81
|
}
|
|
81
82
|
|
|
82
83
|
/**
|
|
@@ -84,22 +85,29 @@ export class ActionTest<T extends Action> {
|
|
|
84
85
|
* 1. Renders the action in a flow node (with screenshot)
|
|
85
86
|
* 2. Opens the node editor (with screenshot)
|
|
86
87
|
* 3. Simulates save and validates round-trip conversion
|
|
88
|
+
* @param waitForNetwork - If true, waits longer for network idle (useful for slow loading resources)
|
|
87
89
|
*/
|
|
88
|
-
async testAction(
|
|
90
|
+
async testAction(
|
|
91
|
+
action: T,
|
|
92
|
+
testName: string,
|
|
93
|
+
waitForNetwork: boolean = false
|
|
94
|
+
) {
|
|
89
95
|
it(`${testName}`, async () => {
|
|
90
96
|
// Step 1: Render action in flow node
|
|
91
97
|
const flowNode = await this.renderAction(action);
|
|
92
98
|
expect(flowNode.querySelector('.body')).to.exist;
|
|
93
99
|
await assertScreenshot(
|
|
94
100
|
`actions/${this.actionName}/render/${testName}`,
|
|
95
|
-
getClip(flowNode)
|
|
101
|
+
getClip(flowNode),
|
|
102
|
+
waitForNetwork
|
|
96
103
|
);
|
|
97
104
|
|
|
98
105
|
// Step 2: Open node editor
|
|
99
106
|
const nodeEditor = await this.openNodeEditor(action);
|
|
100
107
|
await this.assertDialogScreenshot(
|
|
101
108
|
nodeEditor,
|
|
102
|
-
`actions/${this.actionName}/editor/${testName}
|
|
109
|
+
`actions/${this.actionName}/editor/${testName}`,
|
|
110
|
+
waitForNetwork
|
|
103
111
|
);
|
|
104
112
|
|
|
105
113
|
// Step 3: Test round-trip conversion (simulates save workflow)
|