@nyaruka/temba-components 0.138.6 → 0.140.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/.github/workflows/cla.yml +1 -1
- package/.github/workflows/copilot-setup-steps.yml +6 -1
- package/CHANGELOG.md +26 -0
- package/demo/data/flows/sample-flow.json +24 -0
- package/dist/locales/es.js +5 -5
- package/dist/locales/es.js.map +1 -1
- package/dist/locales/fr.js +5 -5
- package/dist/locales/fr.js.map +1 -1
- package/dist/locales/locale-codes.js +2 -11
- package/dist/locales/locale-codes.js.map +1 -1
- package/dist/locales/pt.js +5 -5
- package/dist/locales/pt.js.map +1 -1
- package/dist/temba-components.js +1112 -882
- package/dist/temba-components.js.map +1 -1
- package/out-tsc/src/display/Chat.js +10 -7
- package/out-tsc/src/display/Chat.js.map +1 -1
- package/out-tsc/src/display/Dropdown.js +3 -1
- package/out-tsc/src/display/Dropdown.js.map +1 -1
- package/out-tsc/src/display/FloatingTab.js +25 -32
- package/out-tsc/src/display/FloatingTab.js.map +1 -1
- package/out-tsc/src/display/Thumbnail.js +163 -5
- package/out-tsc/src/display/Thumbnail.js.map +1 -1
- package/out-tsc/src/flow/CanvasMenu.js +5 -3
- package/out-tsc/src/flow/CanvasMenu.js.map +1 -1
- package/out-tsc/src/flow/CanvasNode.js +70 -29
- package/out-tsc/src/flow/CanvasNode.js.map +1 -1
- package/out-tsc/src/flow/Editor.js +290 -239
- package/out-tsc/src/flow/Editor.js.map +1 -1
- package/out-tsc/src/flow/NodeEditor.js +118 -10
- package/out-tsc/src/flow/NodeEditor.js.map +1 -1
- package/out-tsc/src/flow/Plumber.js +757 -403
- package/out-tsc/src/flow/Plumber.js.map +1 -1
- package/out-tsc/src/flow/StickyNote.js +13 -4
- package/out-tsc/src/flow/StickyNote.js.map +1 -1
- package/out-tsc/src/flow/actions/audio-player.js +112 -0
- package/out-tsc/src/flow/actions/audio-player.js.map +1 -0
- package/out-tsc/src/flow/actions/enter_flow.js +43 -0
- package/out-tsc/src/flow/actions/enter_flow.js.map +1 -0
- package/out-tsc/src/flow/actions/play_audio.js +57 -4
- package/out-tsc/src/flow/actions/play_audio.js.map +1 -1
- package/out-tsc/src/flow/actions/say_msg.js +86 -3
- package/out-tsc/src/flow/actions/say_msg.js.map +1 -1
- package/out-tsc/src/flow/config.js +11 -3
- package/out-tsc/src/flow/config.js.map +1 -1
- package/out-tsc/src/flow/nodes/shared-rules.js +1 -1
- package/out-tsc/src/flow/nodes/shared-rules.js.map +1 -1
- package/out-tsc/src/flow/nodes/terminal.js +7 -0
- package/out-tsc/src/flow/nodes/terminal.js.map +1 -0
- package/out-tsc/src/flow/nodes/wait_for_audio.js +77 -0
- package/out-tsc/src/flow/nodes/wait_for_audio.js.map +1 -0
- package/out-tsc/src/flow/nodes/wait_for_dial.js +151 -0
- package/out-tsc/src/flow/nodes/wait_for_dial.js.map +1 -0
- package/out-tsc/src/flow/nodes/wait_for_digits.js +61 -1
- package/out-tsc/src/flow/nodes/wait_for_digits.js.map +1 -1
- package/out-tsc/src/flow/nodes/wait_for_menu.js +173 -2
- package/out-tsc/src/flow/nodes/wait_for_menu.js.map +1 -1
- package/out-tsc/src/flow/operators.js +21 -5
- package/out-tsc/src/flow/operators.js.map +1 -1
- package/out-tsc/src/flow/types.js.map +1 -1
- package/out-tsc/src/flow/utils.js +213 -65
- package/out-tsc/src/flow/utils.js.map +1 -1
- package/out-tsc/src/form/ArrayEditor.js +4 -2
- package/out-tsc/src/form/ArrayEditor.js.map +1 -1
- package/out-tsc/src/form/FieldRenderer.js +49 -0
- package/out-tsc/src/form/FieldRenderer.js.map +1 -1
- package/out-tsc/src/interfaces.js +2 -0
- package/out-tsc/src/interfaces.js.map +1 -1
- package/out-tsc/src/layout/Dialog.js +52 -7
- package/out-tsc/src/layout/Dialog.js.map +1 -1
- package/out-tsc/src/list/TicketList.js +4 -1
- package/out-tsc/src/list/TicketList.js.map +1 -1
- package/out-tsc/src/live/TembaChart.js.map +1 -1
- package/out-tsc/src/locales/es.js +5 -5
- package/out-tsc/src/locales/es.js.map +1 -1
- package/out-tsc/src/locales/fr.js +5 -5
- package/out-tsc/src/locales/fr.js.map +1 -1
- package/out-tsc/src/locales/locale-codes.js +2 -11
- package/out-tsc/src/locales/locale-codes.js.map +1 -1
- package/out-tsc/src/locales/pt.js +5 -5
- package/out-tsc/src/locales/pt.js.map +1 -1
- package/out-tsc/src/simulator/Simulator.js +10 -3
- package/out-tsc/src/simulator/Simulator.js.map +1 -1
- package/out-tsc/src/store/AppState.js +89 -3
- package/out-tsc/src/store/AppState.js.map +1 -1
- package/out-tsc/test/actions/play_audio.test.js +118 -0
- package/out-tsc/test/actions/play_audio.test.js.map +1 -0
- package/out-tsc/test/actions/say_msg.test.js +158 -0
- package/out-tsc/test/actions/say_msg.test.js.map +1 -0
- package/out-tsc/test/nodes/wait_for_audio.test.js +156 -0
- package/out-tsc/test/nodes/wait_for_audio.test.js.map +1 -0
- package/out-tsc/test/nodes/wait_for_dial.test.js +336 -0
- package/out-tsc/test/nodes/wait_for_dial.test.js.map +1 -0
- package/out-tsc/test/nodes/wait_for_digits.test.js +198 -84
- package/out-tsc/test/nodes/wait_for_digits.test.js.map +1 -1
- package/out-tsc/test/nodes/wait_for_menu.test.js +340 -0
- package/out-tsc/test/nodes/wait_for_menu.test.js.map +1 -0
- package/out-tsc/test/temba-floating-tab.test.js +4 -6
- package/out-tsc/test/temba-floating-tab.test.js.map +1 -1
- package/out-tsc/test/temba-flow-collision.test.js +473 -220
- package/out-tsc/test/temba-flow-collision.test.js.map +1 -1
- package/out-tsc/test/temba-flow-editor.test.js +0 -2
- package/out-tsc/test/temba-flow-editor.test.js.map +1 -1
- package/out-tsc/test/temba-flow-plumber-connections.test.js +83 -84
- package/out-tsc/test/temba-flow-plumber-connections.test.js.map +1 -1
- package/out-tsc/test/temba-flow-plumber.test.js +102 -93
- package/out-tsc/test/temba-flow-plumber.test.js.map +1 -1
- package/out-tsc/test/temba-node-type-selector.test.js +6 -6
- package/out-tsc/test/temba-node-type-selector.test.js.map +1 -1
- package/package.json +1 -1
- package/screenshots/truth/actions/play_audio/editor/expression-url.png +0 -0
- package/screenshots/truth/actions/play_audio/editor/static-url.png +0 -0
- package/screenshots/truth/actions/play_audio/render/expression-url.png +0 -0
- package/screenshots/truth/actions/play_audio/render/static-url.png +0 -0
- package/screenshots/truth/actions/say_msg/editor/multiline-text.png +0 -0
- package/screenshots/truth/actions/say_msg/editor/simple-text.png +0 -0
- package/screenshots/truth/actions/say_msg/editor/text-with-audio-url.png +0 -0
- package/screenshots/truth/actions/say_msg/render/multiline-text.png +0 -0
- package/screenshots/truth/actions/say_msg/render/simple-text.png +0 -0
- package/screenshots/truth/actions/say_msg/render/text-with-audio-url.png +0 -0
- package/screenshots/truth/editor/router.png +0 -0
- package/screenshots/truth/editor/wait.png +0 -0
- package/screenshots/truth/nodes/wait_for_audio/editor/basic-audio-wait.png +0 -0
- package/screenshots/truth/nodes/wait_for_audio/render/basic-audio-wait.png +0 -0
- package/screenshots/truth/nodes/wait_for_dial/editor/basic-dial.png +0 -0
- package/screenshots/truth/nodes/wait_for_dial/editor/dial-with-limits.png +0 -0
- package/screenshots/truth/nodes/wait_for_dial/render/basic-dial.png +0 -0
- package/screenshots/truth/nodes/wait_for_dial/render/dial-with-limits.png +0 -0
- package/screenshots/truth/nodes/wait_for_digits/editor/basic-digits-wait.png +0 -0
- package/screenshots/truth/nodes/wait_for_digits/editor/digits-with-rules.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/digits-with-rules.png +0 -0
- package/screenshots/truth/nodes/wait_for_menu/editor/menu-with-digits.png +0 -0
- package/screenshots/truth/nodes/wait_for_menu/render/menu-with-digits.png +0 -0
- package/screenshots/truth/nodes/wait_for_response/editor/basic-wait.png +0 -0
- package/screenshots/truth/nodes/wait_for_response/editor/custom-result-name.png +0 -0
- package/screenshots/truth/nodes/wait_for_response/editor/no-timeout.png +0 -0
- package/screenshots/truth/nodes/wait_for_response/editor/short-timeout.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 +13 -7
- package/src/display/Dropdown.ts +3 -1
- package/src/display/FloatingTab.ts +24 -33
- package/src/display/Thumbnail.ts +162 -2
- package/src/flow/CanvasMenu.ts +8 -3
- package/src/flow/CanvasNode.ts +75 -30
- package/src/flow/Editor.ts +336 -288
- package/src/flow/NodeEditor.ts +137 -9
- package/src/flow/Plumber.ts +1011 -457
- package/src/flow/StickyNote.ts +14 -4
- package/src/flow/actions/audio-player.ts +127 -0
- package/src/flow/actions/enter_flow.ts +44 -0
- package/src/flow/actions/play_audio.ts +64 -5
- package/src/flow/actions/say_msg.ts +94 -4
- package/src/flow/config.ts +11 -3
- package/src/flow/nodes/shared-rules.ts +1 -1
- package/src/flow/nodes/terminal.ts +9 -0
- package/src/flow/nodes/wait_for_audio.ts +88 -0
- package/src/flow/nodes/wait_for_dial.ts +176 -0
- package/src/flow/nodes/wait_for_digits.ts +86 -2
- package/src/flow/nodes/wait_for_menu.ts +209 -3
- package/src/flow/operators.ts +23 -5
- package/src/flow/types.ts +23 -1
- package/src/flow/utils.ts +238 -81
- package/src/form/ArrayEditor.ts +4 -2
- package/src/form/FieldRenderer.ts +64 -1
- package/src/interfaces.ts +3 -1
- package/src/layout/Dialog.ts +53 -7
- package/src/list/TicketList.ts +4 -1
- package/src/live/TembaChart.ts +1 -1
- package/src/locales/es.ts +13 -18
- package/src/locales/fr.ts +13 -18
- package/src/locales/locale-codes.ts +2 -11
- package/src/locales/pt.ts +13 -18
- package/src/simulator/Simulator.ts +13 -3
- package/src/store/AppState.ts +105 -1
- package/src/store/flow-definition.d.ts +2 -0
- package/test/actions/play_audio.test.ts +155 -0
- package/test/actions/say_msg.test.ts +196 -0
- package/test/nodes/wait_for_audio.test.ts +182 -0
- package/test/nodes/wait_for_dial.test.ts +382 -0
- package/test/nodes/wait_for_digits.test.ts +233 -109
- package/test/nodes/wait_for_menu.test.ts +383 -0
- package/test/temba-floating-tab.test.ts +4 -6
- package/test/temba-flow-collision.test.ts +495 -293
- package/test/temba-flow-editor.test.ts +0 -2
- package/test/temba-flow-plumber-connections.test.ts +97 -97
- package/test/temba-flow-plumber.test.ts +116 -103
- package/test/temba-node-type-selector.test.ts +6 -6
- package/screenshots/truth/nodes/wait_for_digits/editor/phone-number-collection.png +0 -0
- package/screenshots/truth/nodes/wait_for_digits/editor/single-digit-with-timeout.png +0 -0
- package/screenshots/truth/nodes/wait_for_digits/editor/verification-code.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
|
@@ -10,18 +10,9 @@ export const sourceLocale = `en`;
|
|
|
10
10
|
* The other locale codes that this application is localized into. Sorted
|
|
11
11
|
* lexicographically.
|
|
12
12
|
*/
|
|
13
|
-
export const targetLocales = [
|
|
14
|
-
`es`,
|
|
15
|
-
`fr`,
|
|
16
|
-
`pt`,
|
|
17
|
-
] as const;
|
|
13
|
+
export const targetLocales = [`es`, `fr`, `pt`] as const;
|
|
18
14
|
|
|
19
15
|
/**
|
|
20
16
|
* All valid project locale codes. Sorted lexicographically.
|
|
21
17
|
*/
|
|
22
|
-
export const allLocales = [
|
|
23
|
-
`en`,
|
|
24
|
-
`es`,
|
|
25
|
-
`fr`,
|
|
26
|
-
`pt`,
|
|
27
|
-
] as const;
|
|
18
|
+
export const allLocales = [`en`, `es`, `fr`, `pt`] as const;
|
package/src/locales/pt.ts
CHANGED
|
@@ -1,18 +1,13 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
's8f02e3a18ffc083a': `Are not currently in a flow`,
|
|
15
|
-
's638236250662c6b3': `Have sent a message in the last`,
|
|
16
|
-
's4788ee206c4570c7': `Have not started this flow in the last 90 days`,
|
|
17
|
-
};
|
|
18
|
-
|
|
1
|
+
// Do not modify this file by hand!
|
|
2
|
+
// Re-generate this file by running lit-localize
|
|
3
|
+
|
|
4
|
+
/* eslint-disable no-irregular-whitespace */
|
|
5
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
6
|
+
|
|
7
|
+
export const templates = {
|
|
8
|
+
s73b4d70c02f4b4e0: `No options`,
|
|
9
|
+
scf1453991c986b25: `Tab to complete, enter to select`,
|
|
10
|
+
s8f02e3a18ffc083a: `Are not currently in a flow`,
|
|
11
|
+
s638236250662c6b3: `Have sent a message in the last`,
|
|
12
|
+
s4788ee206c4570c7: `Have not started this flow in the last 90 days`
|
|
13
|
+
};
|
|
@@ -148,6 +148,10 @@ const SIMULATOR_SIZES: Record<string, SimulatorSize> = {
|
|
|
148
148
|
export class Simulator extends RapidElement {
|
|
149
149
|
static get styles() {
|
|
150
150
|
return css`
|
|
151
|
+
temba-floating-tab {
|
|
152
|
+
--floating-tab-right: 15px;
|
|
153
|
+
}
|
|
154
|
+
|
|
151
155
|
:host {
|
|
152
156
|
/* size-specific dimensions are set dynamically via inline styles */
|
|
153
157
|
--phone-width: 300px;
|
|
@@ -1117,8 +1121,12 @@ export class Simulator extends RapidElement {
|
|
|
1117
1121
|
continue;
|
|
1118
1122
|
}
|
|
1119
1123
|
|
|
1120
|
-
// skip msg_created events without a proper msg property
|
|
1121
|
-
if (
|
|
1124
|
+
// skip msg_created/ivr_created events without a proper msg property
|
|
1125
|
+
if (
|
|
1126
|
+
(rawEvent.type === 'msg_created' ||
|
|
1127
|
+
rawEvent.type === 'ivr_created') &&
|
|
1128
|
+
!(rawEvent as any).msg
|
|
1129
|
+
) {
|
|
1122
1130
|
continue;
|
|
1123
1131
|
}
|
|
1124
1132
|
|
|
@@ -1140,7 +1148,8 @@ export class Simulator extends RapidElement {
|
|
|
1140
1148
|
this.currentQuickReplies = (event as any).msg.quick_replies;
|
|
1141
1149
|
}
|
|
1142
1150
|
|
|
1143
|
-
const isMessage =
|
|
1151
|
+
const isMessage =
|
|
1152
|
+
event.type === 'msg_created' || event.type === 'ivr_created';
|
|
1144
1153
|
const msg = (event as any).msg;
|
|
1145
1154
|
|
|
1146
1155
|
// Check if the event should be displayed.
|
|
@@ -1934,6 +1943,7 @@ export class Simulator extends RapidElement {
|
|
|
1934
1943
|
icon="simulator"
|
|
1935
1944
|
label="Phone Simulator"
|
|
1936
1945
|
color="#10b981"
|
|
1946
|
+
order="4"
|
|
1937
1947
|
.hidden=${this.isVisible}
|
|
1938
1948
|
@temba-button-clicked=${this.handleShow}
|
|
1939
1949
|
></temba-floating-tab>
|
package/src/store/AppState.ts
CHANGED
|
@@ -18,6 +18,59 @@ import { produce } from 'immer';
|
|
|
18
18
|
export const FLOW_SPEC_VERSION = '14.3';
|
|
19
19
|
const CANVAS_PADDING = 800;
|
|
20
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Temporary: Reclassify nodes based on whether they contain terminal actions.
|
|
23
|
+
* - execute_actions nodes with a terminal action become "terminal"
|
|
24
|
+
* - terminal nodes that no longer have a terminal action become "execute_actions"
|
|
25
|
+
* This can be removed once we stop supporting terminal nodes.
|
|
26
|
+
*/
|
|
27
|
+
function reclassifyTerminalNodes(definition: FlowDefinition): void {
|
|
28
|
+
if (!definition?.nodes || !definition?._ui?.nodes) return;
|
|
29
|
+
|
|
30
|
+
for (const node of definition.nodes) {
|
|
31
|
+
const nodeUI = definition._ui.nodes[node.uuid];
|
|
32
|
+
if (!nodeUI) continue;
|
|
33
|
+
|
|
34
|
+
const hasTerminalAction = node.actions?.some(
|
|
35
|
+
(action) => (action as any).terminal === true
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
if (nodeUI.type === 'execute_actions' && hasTerminalAction) {
|
|
39
|
+
nodeUI.type = 'terminal' as any;
|
|
40
|
+
} else if (nodeUI.type === ('terminal' as any) && !hasTerminalAction) {
|
|
41
|
+
nodeUI.type = 'execute_actions';
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Reclassify wait_for_response nodes that are actually voice-specific wait types.
|
|
48
|
+
* The server stores all voice waits as wait_for_response, but we detect the specific
|
|
49
|
+
* type from the router's wait hint:
|
|
50
|
+
* - hint.type === 'digits' && hint.count === 1 → wait_for_menu
|
|
51
|
+
* - hint.type === 'digits' (no count) → wait_for_digits
|
|
52
|
+
* - hint.type === 'audio' → wait_for_audio
|
|
53
|
+
*/
|
|
54
|
+
function reclassifyVoiceWaitNodes(definition: FlowDefinition): void {
|
|
55
|
+
if (!definition?.nodes || !definition?._ui?.nodes) return;
|
|
56
|
+
|
|
57
|
+
for (const node of definition.nodes) {
|
|
58
|
+
const nodeUI = definition._ui.nodes[node.uuid];
|
|
59
|
+
if (!nodeUI || nodeUI.type !== 'wait_for_response') continue;
|
|
60
|
+
|
|
61
|
+
const hint = node.router?.wait?.hint;
|
|
62
|
+
if (!hint) continue;
|
|
63
|
+
|
|
64
|
+
if (hint.type === 'digits' && hint.count === 1) {
|
|
65
|
+
nodeUI.type = 'wait_for_menu' as any;
|
|
66
|
+
} else if (hint.type === 'digits') {
|
|
67
|
+
nodeUI.type = 'wait_for_digits' as any;
|
|
68
|
+
} else if (hint.type === 'audio') {
|
|
69
|
+
nodeUI.type = 'wait_for_audio' as any;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
21
74
|
/**
|
|
22
75
|
* Sorts nodes by their position - first by y (top), then by x (left)
|
|
23
76
|
*/
|
|
@@ -65,11 +118,44 @@ export interface Language {
|
|
|
65
118
|
name: string;
|
|
66
119
|
}
|
|
67
120
|
|
|
121
|
+
export interface FlowIssue {
|
|
122
|
+
type: string;
|
|
123
|
+
node_uuid: string;
|
|
124
|
+
action_uuid?: string;
|
|
125
|
+
description: string;
|
|
126
|
+
dependency?: {
|
|
127
|
+
key: string;
|
|
128
|
+
name: string;
|
|
129
|
+
type: string;
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
68
133
|
export interface FlowInfo {
|
|
69
134
|
results: InfoResult[];
|
|
70
135
|
dependencies: TypedObjectRef[];
|
|
71
136
|
counts: { nodes: number; languages: number };
|
|
72
137
|
locals: string[];
|
|
138
|
+
issues?: FlowIssue[];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function buildIssueMaps(issues: FlowIssue[] = []): {
|
|
142
|
+
byNode: Map<string, FlowIssue[]>;
|
|
143
|
+
byAction: Map<string, FlowIssue[]>;
|
|
144
|
+
} {
|
|
145
|
+
const byNode = new Map<string, FlowIssue[]>();
|
|
146
|
+
const byAction = new Map<string, FlowIssue[]>();
|
|
147
|
+
for (const issue of issues) {
|
|
148
|
+
if (issue.action_uuid) {
|
|
149
|
+
const actionList = byAction.get(issue.action_uuid) || [];
|
|
150
|
+
actionList.push(issue);
|
|
151
|
+
byAction.set(issue.action_uuid, actionList);
|
|
152
|
+
} else {
|
|
153
|
+
const nodeList = byNode.get(issue.node_uuid) || [];
|
|
154
|
+
nodeList.push(issue);
|
|
155
|
+
byNode.set(issue.node_uuid, nodeList);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return { byNode, byAction };
|
|
73
159
|
}
|
|
74
160
|
|
|
75
161
|
export interface FlowContents {
|
|
@@ -100,6 +186,8 @@ export interface Activity {
|
|
|
100
186
|
export interface AppState {
|
|
101
187
|
flowDefinition: FlowDefinition;
|
|
102
188
|
flowInfo: FlowInfo;
|
|
189
|
+
issuesByNode: Map<string, FlowIssue[]>;
|
|
190
|
+
issuesByAction: Map<string, FlowIssue[]>;
|
|
103
191
|
|
|
104
192
|
languageCode: string;
|
|
105
193
|
languageNames: { [code: string]: Language };
|
|
@@ -174,6 +262,8 @@ export const zustand = createStore<AppState>()(
|
|
|
174
262
|
workspace: null,
|
|
175
263
|
flowDefinition: null,
|
|
176
264
|
flowInfo: null,
|
|
265
|
+
issuesByNode: new Map(),
|
|
266
|
+
issuesByAction: new Map(),
|
|
177
267
|
isTranslating: false,
|
|
178
268
|
viewingRevision: false,
|
|
179
269
|
dirtyDate: null,
|
|
@@ -201,10 +291,15 @@ export const zustand = createStore<AppState>()(
|
|
|
201
291
|
throw new Error('Network response was not ok');
|
|
202
292
|
}
|
|
203
293
|
const data = (await response.json()) as FlowContents;
|
|
294
|
+
reclassifyTerminalNodes(data.definition);
|
|
295
|
+
reclassifyVoiceWaitNodes(data.definition);
|
|
296
|
+
const issueMaps = buildIssueMaps(data.info?.issues);
|
|
204
297
|
set({
|
|
205
298
|
flowInfo: data.info,
|
|
206
299
|
flowDefinition: data.definition,
|
|
207
|
-
viewingRevision
|
|
300
|
+
viewingRevision,
|
|
301
|
+
issuesByNode: issueMaps.byNode,
|
|
302
|
+
issuesByAction: issueMaps.byAction
|
|
208
303
|
});
|
|
209
304
|
},
|
|
210
305
|
|
|
@@ -295,10 +390,16 @@ export const zustand = createStore<AppState>()(
|
|
|
295
390
|
nodes: [...(flow.definition.nodes || [])]
|
|
296
391
|
};
|
|
297
392
|
state.flowInfo = flow.info;
|
|
393
|
+
const issueMaps = buildIssueMaps(flow.info?.issues);
|
|
394
|
+
state.issuesByNode = issueMaps.byNode;
|
|
395
|
+
state.issuesByAction = issueMaps.byAction;
|
|
298
396
|
// Reset to the flow's default language when loading a new flow
|
|
299
397
|
state.languageCode = flowLang;
|
|
300
398
|
state.isTranslating = false;
|
|
301
399
|
|
|
400
|
+
reclassifyTerminalNodes(state.flowDefinition);
|
|
401
|
+
reclassifyVoiceWaitNodes(state.flowDefinition);
|
|
402
|
+
|
|
302
403
|
// Sort nodes by position when loading flow
|
|
303
404
|
if (state.flowDefinition?.nodes && state.flowDefinition?._ui?.nodes) {
|
|
304
405
|
sortNodesByPosition(
|
|
@@ -312,6 +413,9 @@ export const zustand = createStore<AppState>()(
|
|
|
312
413
|
setFlowInfo: (info: FlowInfo) => {
|
|
313
414
|
set((state: AppState) => {
|
|
314
415
|
state.flowInfo = info;
|
|
416
|
+
const issueMaps = buildIssueMaps(info?.issues);
|
|
417
|
+
state.issuesByNode = issueMaps.byNode;
|
|
418
|
+
state.issuesByAction = issueMaps.byAction;
|
|
315
419
|
});
|
|
316
420
|
},
|
|
317
421
|
|
|
@@ -28,6 +28,7 @@ export type ActionType =
|
|
|
28
28
|
| 'send_email'
|
|
29
29
|
| 'send_broadcast'
|
|
30
30
|
| 'enter_flow'
|
|
31
|
+
| 'terminal'
|
|
31
32
|
| 'start_session'
|
|
32
33
|
| 'transfer_airtime'
|
|
33
34
|
| 'split_by_airtime'
|
|
@@ -152,6 +153,7 @@ export interface SendBroadcast extends Action {
|
|
|
152
153
|
|
|
153
154
|
export interface EnterFlow extends Action {
|
|
154
155
|
flow: NamedObject;
|
|
156
|
+
terminal?: boolean;
|
|
155
157
|
}
|
|
156
158
|
|
|
157
159
|
export interface StartSession extends Action {
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { expect } from '@open-wc/testing';
|
|
2
|
+
import { play_audio } from '../../src/flow/actions/play_audio';
|
|
3
|
+
import { PlayAudio } from '../../src/store/flow-definition';
|
|
4
|
+
import { ActionTest } from '../ActionHelper';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Test suite for the play_audio action configuration.
|
|
8
|
+
*/
|
|
9
|
+
describe('play_audio action config', () => {
|
|
10
|
+
const helper = new ActionTest(play_audio, 'play_audio');
|
|
11
|
+
|
|
12
|
+
describe('basic properties', () => {
|
|
13
|
+
helper.testBasicProperties();
|
|
14
|
+
|
|
15
|
+
it('has correct name', () => {
|
|
16
|
+
expect(play_audio.name).to.equal('Play Recording');
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('is voice-only', () => {
|
|
20
|
+
expect(play_audio.flowTypes).to.deep.equal(['voice']);
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe('action scenarios', () => {
|
|
25
|
+
helper.testAction(
|
|
26
|
+
{
|
|
27
|
+
uuid: 'test-play-1',
|
|
28
|
+
type: 'play_audio',
|
|
29
|
+
audio_url: '@results.voicemail'
|
|
30
|
+
} as PlayAudio,
|
|
31
|
+
'expression-url'
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
helper.testAction(
|
|
35
|
+
{
|
|
36
|
+
uuid: 'test-play-2',
|
|
37
|
+
type: 'play_audio',
|
|
38
|
+
audio_url: 'https://example.com/greeting.mp3'
|
|
39
|
+
} as PlayAudio,
|
|
40
|
+
'static-url'
|
|
41
|
+
);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe('data transformation', () => {
|
|
45
|
+
it('converts action to form data', () => {
|
|
46
|
+
const action: PlayAudio = {
|
|
47
|
+
uuid: 'test-action',
|
|
48
|
+
type: 'play_audio',
|
|
49
|
+
audio_url: '@results.voicemail'
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const formData = play_audio.toFormData!(action);
|
|
53
|
+
expect(formData.uuid).to.equal('test-action');
|
|
54
|
+
expect(formData.audio_url).to.equal('@results.voicemail');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('handles missing audio_url', () => {
|
|
58
|
+
const action = {
|
|
59
|
+
uuid: 'test-action',
|
|
60
|
+
type: 'play_audio'
|
|
61
|
+
} as PlayAudio;
|
|
62
|
+
|
|
63
|
+
const formData = play_audio.toFormData!(action);
|
|
64
|
+
expect(formData.audio_url).to.equal('');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('converts form data to action', () => {
|
|
68
|
+
const formData = {
|
|
69
|
+
uuid: 'test-action',
|
|
70
|
+
audio_url: '@results.voicemail'
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const action = play_audio.fromFormData!(formData) as PlayAudio;
|
|
74
|
+
expect(action.uuid).to.equal('test-action');
|
|
75
|
+
expect(action.type).to.equal('play_audio');
|
|
76
|
+
expect(action.audio_url).to.equal('@results.voicemail');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('trims whitespace from audio_url', () => {
|
|
80
|
+
const formData = {
|
|
81
|
+
uuid: 'test-action',
|
|
82
|
+
audio_url: ' @results.voicemail '
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const action = play_audio.fromFormData!(formData) as PlayAudio;
|
|
86
|
+
expect(action.audio_url).to.equal('@results.voicemail');
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe('localization', () => {
|
|
91
|
+
it('converts localization to form data', () => {
|
|
92
|
+
const action: PlayAudio = {
|
|
93
|
+
uuid: 'test-action',
|
|
94
|
+
type: 'play_audio',
|
|
95
|
+
audio_url: '@results.voicemail'
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const localization = {
|
|
99
|
+
audio_url: ['@results.voicemail_es']
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const formData = play_audio.toLocalizationFormData!(action, localization);
|
|
103
|
+
expect(formData.audio_url).to.equal('@results.voicemail_es');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('handles missing localization', () => {
|
|
107
|
+
const action: PlayAudio = {
|
|
108
|
+
uuid: 'test-action',
|
|
109
|
+
type: 'play_audio',
|
|
110
|
+
audio_url: '@results.voicemail'
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const formData = play_audio.toLocalizationFormData!(action, {});
|
|
114
|
+
expect(formData.audio_url).to.equal('');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('converts form data to localization', () => {
|
|
118
|
+
const action: PlayAudio = {
|
|
119
|
+
uuid: 'test-action',
|
|
120
|
+
type: 'play_audio',
|
|
121
|
+
audio_url: '@results.voicemail'
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const formData = {
|
|
125
|
+
uuid: 'test-action',
|
|
126
|
+
audio_url: '@results.voicemail_es'
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const localization = play_audio.fromLocalizationFormData!(
|
|
130
|
+
formData,
|
|
131
|
+
action
|
|
132
|
+
);
|
|
133
|
+
expect(localization.audio_url).to.deep.equal(['@results.voicemail_es']);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('omits unchanged localization', () => {
|
|
137
|
+
const action: PlayAudio = {
|
|
138
|
+
uuid: 'test-action',
|
|
139
|
+
type: 'play_audio',
|
|
140
|
+
audio_url: '@results.voicemail'
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const formData = {
|
|
144
|
+
uuid: 'test-action',
|
|
145
|
+
audio_url: '@results.voicemail' // same as original
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const localization = play_audio.fromLocalizationFormData!(
|
|
149
|
+
formData,
|
|
150
|
+
action
|
|
151
|
+
);
|
|
152
|
+
expect(localization.audio_url).to.be.undefined;
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
});
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { expect } from '@open-wc/testing';
|
|
2
|
+
import { say_msg } from '../../src/flow/actions/say_msg';
|
|
3
|
+
import { SayMsg } from '../../src/store/flow-definition';
|
|
4
|
+
import { ActionTest } from '../ActionHelper';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Test suite for the say_msg action configuration.
|
|
8
|
+
*/
|
|
9
|
+
describe('say_msg action config', () => {
|
|
10
|
+
const helper = new ActionTest(say_msg, 'say_msg');
|
|
11
|
+
|
|
12
|
+
describe('basic properties', () => {
|
|
13
|
+
helper.testBasicProperties();
|
|
14
|
+
|
|
15
|
+
it('has correct name', () => {
|
|
16
|
+
expect(say_msg.name).to.equal('Say Message');
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('is voice-only', () => {
|
|
20
|
+
expect(say_msg.flowTypes).to.deep.equal(['voice']);
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe('action scenarios', () => {
|
|
25
|
+
helper.testAction(
|
|
26
|
+
{
|
|
27
|
+
uuid: 'test-say-1',
|
|
28
|
+
type: 'say_msg',
|
|
29
|
+
text: 'Hello, welcome to our service.'
|
|
30
|
+
} as SayMsg,
|
|
31
|
+
'simple-text'
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
helper.testAction(
|
|
35
|
+
{
|
|
36
|
+
uuid: 'test-say-2',
|
|
37
|
+
type: 'say_msg',
|
|
38
|
+
text: 'Press 1 for sales.\nPress 2 for support.\nPress 3 to leave a message.'
|
|
39
|
+
} as SayMsg,
|
|
40
|
+
'multiline-text'
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
helper.testAction(
|
|
44
|
+
{
|
|
45
|
+
uuid: 'test-say-3',
|
|
46
|
+
type: 'say_msg',
|
|
47
|
+
text: 'Please listen to the following recording.',
|
|
48
|
+
audio_url: 'https://example.com/greeting.mp3'
|
|
49
|
+
} as SayMsg,
|
|
50
|
+
'text-with-audio-url'
|
|
51
|
+
);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe('data transformation', () => {
|
|
55
|
+
it('converts action to form data', () => {
|
|
56
|
+
const action: SayMsg = {
|
|
57
|
+
uuid: 'test-action',
|
|
58
|
+
type: 'say_msg',
|
|
59
|
+
text: 'Hello world',
|
|
60
|
+
audio_url: 'https://example.com/audio.mp3'
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const formData = say_msg.toFormData!(action);
|
|
64
|
+
expect(formData.uuid).to.equal('test-action');
|
|
65
|
+
expect(formData.text).to.equal('Hello world');
|
|
66
|
+
expect(formData.audio_url).to.equal('https://example.com/audio.mp3');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('handles missing audio_url in toFormData', () => {
|
|
70
|
+
const action: SayMsg = {
|
|
71
|
+
uuid: 'test-action',
|
|
72
|
+
type: 'say_msg',
|
|
73
|
+
text: 'Hello'
|
|
74
|
+
} as SayMsg;
|
|
75
|
+
|
|
76
|
+
const formData = say_msg.toFormData!(action);
|
|
77
|
+
expect(formData.audio_url).to.equal('');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('converts form data to action', () => {
|
|
81
|
+
const formData = {
|
|
82
|
+
uuid: 'test-action',
|
|
83
|
+
text: 'Hello world',
|
|
84
|
+
audio_url: 'https://example.com/audio.mp3'
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const action = say_msg.fromFormData!(formData) as SayMsg;
|
|
88
|
+
expect(action.uuid).to.equal('test-action');
|
|
89
|
+
expect(action.type).to.equal('say_msg');
|
|
90
|
+
expect(action.text).to.equal('Hello world');
|
|
91
|
+
expect(action.audio_url).to.equal('https://example.com/audio.mp3');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('omits empty audio_url in fromFormData', () => {
|
|
95
|
+
const formData = {
|
|
96
|
+
uuid: 'test-action',
|
|
97
|
+
text: 'Hello world',
|
|
98
|
+
audio_url: ''
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const action = say_msg.fromFormData!(formData) as SayMsg;
|
|
102
|
+
expect(action.audio_url).to.be.undefined;
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('trims whitespace from audio_url', () => {
|
|
106
|
+
const formData = {
|
|
107
|
+
uuid: 'test-action',
|
|
108
|
+
text: 'Hello',
|
|
109
|
+
audio_url: ' https://example.com/audio.mp3 '
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const action = say_msg.fromFormData!(formData) as SayMsg;
|
|
113
|
+
expect(action.audio_url).to.equal('https://example.com/audio.mp3');
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe('sanitize', () => {
|
|
118
|
+
it('trims text whitespace', () => {
|
|
119
|
+
const formData = { text: ' Hello world ' };
|
|
120
|
+
say_msg.sanitize!(formData);
|
|
121
|
+
expect(formData.text).to.equal('Hello world');
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
describe('localization', () => {
|
|
126
|
+
it('converts localization to form data', () => {
|
|
127
|
+
const action: SayMsg = {
|
|
128
|
+
uuid: 'test-action',
|
|
129
|
+
type: 'say_msg',
|
|
130
|
+
text: 'Hello',
|
|
131
|
+
audio_url: 'https://example.com/en.mp3'
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const localization = {
|
|
135
|
+
text: ['Hola'],
|
|
136
|
+
audio_url: ['https://example.com/es.mp3']
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const formData = say_msg.toLocalizationFormData!(action, localization);
|
|
140
|
+
expect(formData.text).to.equal('Hola');
|
|
141
|
+
expect(formData.audio_url).to.equal('https://example.com/es.mp3');
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('handles missing localization fields', () => {
|
|
145
|
+
const action: SayMsg = {
|
|
146
|
+
uuid: 'test-action',
|
|
147
|
+
type: 'say_msg',
|
|
148
|
+
text: 'Hello'
|
|
149
|
+
} as SayMsg;
|
|
150
|
+
|
|
151
|
+
const formData = say_msg.toLocalizationFormData!(action, {});
|
|
152
|
+
expect(formData.text).to.equal('');
|
|
153
|
+
expect(formData.audio_url).to.equal('');
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('converts form data to localization', () => {
|
|
157
|
+
const action: SayMsg = {
|
|
158
|
+
uuid: 'test-action',
|
|
159
|
+
type: 'say_msg',
|
|
160
|
+
text: 'Hello',
|
|
161
|
+
audio_url: 'https://example.com/en.mp3'
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const formData = {
|
|
165
|
+
uuid: 'test-action',
|
|
166
|
+
text: 'Hola',
|
|
167
|
+
audio_url: 'https://example.com/es.mp3'
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const localization = say_msg.fromLocalizationFormData!(formData, action);
|
|
171
|
+
expect(localization.text).to.deep.equal(['Hola']);
|
|
172
|
+
expect(localization.audio_url).to.deep.equal([
|
|
173
|
+
'https://example.com/es.mp3'
|
|
174
|
+
]);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('omits unchanged localization fields', () => {
|
|
178
|
+
const action: SayMsg = {
|
|
179
|
+
uuid: 'test-action',
|
|
180
|
+
type: 'say_msg',
|
|
181
|
+
text: 'Hello',
|
|
182
|
+
audio_url: 'https://example.com/en.mp3'
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const formData = {
|
|
186
|
+
uuid: 'test-action',
|
|
187
|
+
text: 'Hello', // same as original
|
|
188
|
+
audio_url: 'https://example.com/en.mp3' // same as original
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const localization = say_msg.fromLocalizationFormData!(formData, action);
|
|
192
|
+
expect(localization.text).to.be.undefined;
|
|
193
|
+
expect(localization.audio_url).to.be.undefined;
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
});
|