@nyaruka/temba-components 0.131.2 → 0.131.3
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 +14 -0
- package/demo/components/floating-tabs/example.html +400 -0
- package/demo/components/flow/index.html +1 -1
- package/demo/data/flows/sample-flow.json +41 -2
- package/demo/data/flows/voicemail.json +613 -0
- package/demo/index.html +6 -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 +11 -2
- 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 +1109 -535
- package/dist/temba-components.js.map +1 -1
- package/out-tsc/src/display/FloatingTab.js +167 -0
- package/out-tsc/src/display/FloatingTab.js.map +1 -0
- package/out-tsc/src/display/ProgressBar.js +22 -2
- package/out-tsc/src/display/ProgressBar.js.map +1 -1
- package/out-tsc/src/events.js.map +1 -1
- package/out-tsc/src/flow/CanvasNode.js +165 -31
- package/out-tsc/src/flow/CanvasNode.js.map +1 -1
- package/out-tsc/src/flow/Editor.js +857 -3
- package/out-tsc/src/flow/Editor.js.map +1 -1
- package/out-tsc/src/flow/NodeEditor.js +239 -19
- package/out-tsc/src/flow/NodeEditor.js.map +1 -1
- package/out-tsc/src/flow/NodeTypeSelector.js +44 -3
- package/out-tsc/src/flow/NodeTypeSelector.js.map +1 -1
- package/out-tsc/src/flow/StickyNote.js +12 -3
- package/out-tsc/src/flow/StickyNote.js.map +1 -1
- package/out-tsc/src/flow/actions/add_contact_groups.js +2 -1
- package/out-tsc/src/flow/actions/add_contact_groups.js.map +1 -1
- package/out-tsc/src/flow/actions/add_contact_urn.js +2 -1
- package/out-tsc/src/flow/actions/add_contact_urn.js.map +1 -1
- package/out-tsc/src/flow/actions/add_input_labels.js +2 -1
- package/out-tsc/src/flow/actions/add_input_labels.js.map +1 -1
- package/out-tsc/src/flow/actions/play_audio.js +2 -1
- package/out-tsc/src/flow/actions/play_audio.js.map +1 -1
- package/out-tsc/src/flow/actions/remove_contact_groups.js +2 -1
- package/out-tsc/src/flow/actions/remove_contact_groups.js.map +1 -1
- package/out-tsc/src/flow/actions/request_optin.js +1 -0
- package/out-tsc/src/flow/actions/request_optin.js.map +1 -1
- package/out-tsc/src/flow/actions/say_msg.js +2 -1
- package/out-tsc/src/flow/actions/say_msg.js.map +1 -1
- package/out-tsc/src/flow/actions/send_broadcast.js +2 -1
- package/out-tsc/src/flow/actions/send_broadcast.js.map +1 -1
- package/out-tsc/src/flow/actions/send_email.js +2 -1
- package/out-tsc/src/flow/actions/send_email.js.map +1 -1
- package/out-tsc/src/flow/actions/send_msg.js +93 -3
- package/out-tsc/src/flow/actions/send_msg.js.map +1 -1
- package/out-tsc/src/flow/actions/set_contact_channel.js +2 -1
- package/out-tsc/src/flow/actions/set_contact_channel.js.map +1 -1
- package/out-tsc/src/flow/actions/set_contact_field.js +2 -1
- package/out-tsc/src/flow/actions/set_contact_field.js.map +1 -1
- package/out-tsc/src/flow/actions/set_contact_language.js +2 -1
- package/out-tsc/src/flow/actions/set_contact_language.js.map +1 -1
- package/out-tsc/src/flow/actions/set_contact_name.js +2 -1
- package/out-tsc/src/flow/actions/set_contact_name.js.map +1 -1
- package/out-tsc/src/flow/actions/set_contact_status.js +2 -1
- package/out-tsc/src/flow/actions/set_contact_status.js.map +1 -1
- package/out-tsc/src/flow/actions/set_run_result.js +2 -1
- package/out-tsc/src/flow/actions/set_run_result.js.map +1 -1
- package/out-tsc/src/flow/actions/start_session.js +2 -1
- package/out-tsc/src/flow/actions/start_session.js.map +1 -1
- package/out-tsc/src/flow/config.js +2 -10
- package/out-tsc/src/flow/config.js.map +1 -1
- package/out-tsc/src/flow/nodes/shared.js +54 -0
- package/out-tsc/src/flow/nodes/shared.js.map +1 -1
- package/out-tsc/src/flow/nodes/split_by_airtime.js +9 -3
- package/out-tsc/src/flow/nodes/split_by_airtime.js.map +1 -1
- package/out-tsc/src/flow/nodes/split_by_contact_field.js +8 -3
- package/out-tsc/src/flow/nodes/split_by_contact_field.js.map +1 -1
- package/out-tsc/src/flow/nodes/split_by_expression.js +8 -3
- package/out-tsc/src/flow/nodes/split_by_expression.js.map +1 -1
- package/out-tsc/src/flow/nodes/split_by_groups.js +8 -3
- package/out-tsc/src/flow/nodes/split_by_groups.js.map +1 -1
- package/out-tsc/src/flow/nodes/split_by_intent.js +3 -2
- package/out-tsc/src/flow/nodes/split_by_intent.js.map +1 -1
- package/out-tsc/src/flow/nodes/split_by_llm.js +9 -2
- package/out-tsc/src/flow/nodes/split_by_llm.js.map +1 -1
- package/out-tsc/src/flow/nodes/split_by_llm_categorize.js +9 -2
- package/out-tsc/src/flow/nodes/split_by_llm_categorize.js.map +1 -1
- package/out-tsc/src/flow/nodes/split_by_random.js +8 -2
- package/out-tsc/src/flow/nodes/split_by_random.js.map +1 -1
- package/out-tsc/src/flow/nodes/split_by_resthook.js +8 -3
- package/out-tsc/src/flow/nodes/split_by_resthook.js.map +1 -1
- package/out-tsc/src/flow/nodes/split_by_run_result.js +8 -3
- package/out-tsc/src/flow/nodes/split_by_run_result.js.map +1 -1
- package/out-tsc/src/flow/nodes/split_by_scheme.js +8 -3
- package/out-tsc/src/flow/nodes/split_by_scheme.js.map +1 -1
- package/out-tsc/src/flow/nodes/split_by_subflow.js +8 -2
- package/out-tsc/src/flow/nodes/split_by_subflow.js.map +1 -1
- package/out-tsc/src/flow/nodes/split_by_ticket.js +8 -2
- package/out-tsc/src/flow/nodes/split_by_ticket.js.map +1 -1
- package/out-tsc/src/flow/nodes/split_by_webhook.js +8 -2
- package/out-tsc/src/flow/nodes/split_by_webhook.js.map +1 -1
- package/out-tsc/src/flow/nodes/wait_for_digits.js +3 -2
- package/out-tsc/src/flow/nodes/wait_for_digits.js.map +1 -1
- package/out-tsc/src/flow/nodes/wait_for_menu.js +3 -2
- package/out-tsc/src/flow/nodes/wait_for_menu.js.map +1 -1
- package/out-tsc/src/flow/nodes/wait_for_response.js +8 -3
- package/out-tsc/src/flow/nodes/wait_for_response.js.map +1 -1
- package/out-tsc/src/flow/types.js +15 -0
- package/out-tsc/src/flow/types.js.map +1 -1
- package/out-tsc/src/layout/FloatingWindow.js +346 -0
- package/out-tsc/src/layout/FloatingWindow.js.map +1 -0
- package/out-tsc/src/live/ContactChat.js +3 -19
- package/out-tsc/src/live/ContactChat.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 +11 -2
- 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/store/AppState.js +67 -0
- package/out-tsc/src/store/AppState.js.map +1 -1
- package/out-tsc/temba-modules.js +4 -0
- package/out-tsc/temba-modules.js.map +1 -1
- package/out-tsc/test/temba-floating-tab.test.js +91 -0
- package/out-tsc/test/temba-floating-tab.test.js.map +1 -0
- package/out-tsc/test/temba-floating-window.test.js +301 -0
- package/out-tsc/test/temba-floating-window.test.js.map +1 -0
- package/out-tsc/test/temba-flow-editor-node.test.js +117 -0
- package/out-tsc/test/temba-flow-editor-node.test.js.map +1 -1
- package/out-tsc/test/temba-localization.test.js +471 -0
- package/out-tsc/test/temba-localization.test.js.map +1 -0
- package/out-tsc/test/temba-node-type-selector.test.js +150 -0
- package/out-tsc/test/temba-node-type-selector.test.js.map +1 -1
- package/out-tsc/test/utils.test.js +18 -0
- package/out-tsc/test/utils.test.js.map +1 -1
- package/package.json +1 -1
- 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/hidden.png +0 -0
- package/screenshots/truth/floating-tab/hover.png +0 -0
- package/screenshots/truth/floating-tab/purple.png +0 -0
- package/screenshots/truth/floating-window/chromeless.png +0 -0
- package/screenshots/truth/floating-window/custom-size.png +0 -0
- package/screenshots/truth/floating-window/default.png +0 -0
- package/screenshots/truth/floating-window/with-header.png +0 -0
- package/screenshots/truth/node-type-selector/action-mode.png +0 -0
- package/screenshots/truth/node-type-selector/split-mode.png +0 -0
- package/screenshots/truth/nodes/split_by_llm_categorize/editor/feedback-categorization.png +0 -0
- package/src/display/FloatingTab.ts +174 -0
- package/src/display/ProgressBar.ts +22 -2
- package/src/events.ts +2 -4
- package/src/flow/CanvasNode.ts +190 -32
- package/src/flow/Editor.ts +1040 -3
- package/src/flow/NodeEditor.ts +317 -19
- package/src/flow/NodeTypeSelector.ts +47 -3
- package/src/flow/StickyNote.ts +12 -3
- package/src/flow/actions/add_contact_groups.ts +2 -1
- package/src/flow/actions/add_contact_urn.ts +3 -1
- package/src/flow/actions/add_input_labels.ts +2 -1
- package/src/flow/actions/play_audio.ts +2 -1
- package/src/flow/actions/remove_contact_groups.ts +3 -1
- package/src/flow/actions/request_optin.ts +1 -0
- package/src/flow/actions/say_msg.ts +2 -1
- package/src/flow/actions/send_broadcast.ts +2 -1
- package/src/flow/actions/send_email.ts +3 -1
- package/src/flow/actions/send_msg.ts +134 -3
- package/src/flow/actions/set_contact_channel.ts +2 -1
- package/src/flow/actions/set_contact_field.ts +2 -1
- package/src/flow/actions/set_contact_language.ts +3 -1
- package/src/flow/actions/set_contact_name.ts +2 -1
- package/src/flow/actions/set_contact_status.ts +2 -1
- package/src/flow/actions/set_run_result.ts +2 -1
- package/src/flow/actions/start_session.ts +3 -1
- package/src/flow/config.ts +2 -12
- package/src/flow/nodes/shared.ts +70 -1
- package/src/flow/nodes/split_by_airtime.ts +20 -3
- package/src/flow/nodes/split_by_contact_field.ts +13 -3
- package/src/flow/nodes/split_by_expression.ts +13 -3
- package/src/flow/nodes/split_by_groups.ts +13 -3
- package/src/flow/nodes/split_by_intent.ts +3 -2
- package/src/flow/nodes/split_by_llm.ts +19 -2
- package/src/flow/nodes/split_by_llm_categorize.ts +19 -2
- package/src/flow/nodes/split_by_random.ts +12 -2
- package/src/flow/nodes/split_by_resthook.ts +13 -3
- package/src/flow/nodes/split_by_run_result.ts +13 -3
- package/src/flow/nodes/split_by_scheme.ts +13 -3
- package/src/flow/nodes/split_by_subflow.ts +12 -2
- package/src/flow/nodes/split_by_ticket.ts +12 -2
- package/src/flow/nodes/split_by_webhook.ts +12 -2
- package/src/flow/nodes/wait_for_digits.ts +3 -2
- package/src/flow/nodes/wait_for_menu.ts +3 -2
- package/src/flow/nodes/wait_for_response.ts +13 -3
- package/src/flow/types.ts +47 -0
- package/src/layout/FloatingWindow.ts +386 -0
- package/src/live/ContactChat.ts +4 -19
- package/src/locales/es.ts +18 -13
- package/src/locales/fr.ts +18 -13
- package/src/locales/locale-codes.ts +11 -2
- package/src/locales/pt.ts +18 -13
- package/src/store/AppState.ts +104 -0
- package/static/api/llms.json +18 -0
- package/temba-modules.ts +4 -0
- package/test/temba-floating-tab.test.ts +110 -0
- package/test/temba-floating-window.test.ts +477 -0
- package/test/temba-flow-editor-node.test.ts +144 -0
- package/test/temba-localization.test.ts +611 -0
- package/test/temba-node-type-selector.test.ts +203 -0
- package/test/utils.test.ts +20 -0
- package/test-assets/contacts/history.json +5 -6
- package/test-assets/select/llms.json +2 -2
- package/web-dev-server.config.mjs +47 -1
- package/web-test-runner.config.mjs +0 -1
- package/out-tsc/src/flow/nodes/wait_for_audio.js +0 -7
- package/out-tsc/src/flow/nodes/wait_for_audio.js.map +0 -1
- package/out-tsc/src/flow/nodes/wait_for_image.js +0 -7
- package/out-tsc/src/flow/nodes/wait_for_image.js.map +0 -1
- package/out-tsc/src/flow/nodes/wait_for_location.js +0 -7
- package/out-tsc/src/flow/nodes/wait_for_location.js.map +0 -1
- package/out-tsc/src/flow/nodes/wait_for_video.js +0 -7
- package/out-tsc/src/flow/nodes/wait_for_video.js.map +0 -1
- package/src/flow/nodes/wait_for_audio.ts +0 -7
- package/src/flow/nodes/wait_for_image.ts +0 -7
- package/src/flow/nodes/wait_for_location.ts +0 -7
- package/src/flow/nodes/wait_for_video.ts +0 -7
|
@@ -0,0 +1,611 @@
|
|
|
1
|
+
import { expect, fixture, html } from '@open-wc/testing';
|
|
2
|
+
import { Editor } from '../src/flow/Editor';
|
|
3
|
+
import { NodeEditor } from '../src/flow/NodeEditor';
|
|
4
|
+
import { SendMsg, FlowDefinition } from '../src/store/flow-definition';
|
|
5
|
+
import { zustand } from '../src/store/AppState';
|
|
6
|
+
import { send_msg } from '../src/flow/actions/send_msg';
|
|
7
|
+
import '../temba-modules';
|
|
8
|
+
|
|
9
|
+
describe('Localization Editing', () => {
|
|
10
|
+
let editor: Editor;
|
|
11
|
+
let storeElement: HTMLElement;
|
|
12
|
+
|
|
13
|
+
const languageNames: Record<string, string> = {
|
|
14
|
+
eng: 'English',
|
|
15
|
+
fra: 'French',
|
|
16
|
+
esp: 'Spanish'
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const buildCategoryFlowDefinition = (
|
|
20
|
+
localization: Record<string, any> = {}
|
|
21
|
+
): FlowDefinition => ({
|
|
22
|
+
uuid: 'category-flow',
|
|
23
|
+
name: 'Category Flow',
|
|
24
|
+
language: 'eng',
|
|
25
|
+
type: 'messaging',
|
|
26
|
+
revision: 1,
|
|
27
|
+
spec_version: '14.3',
|
|
28
|
+
localization,
|
|
29
|
+
nodes: [
|
|
30
|
+
{
|
|
31
|
+
uuid: 'split-node',
|
|
32
|
+
actions: [],
|
|
33
|
+
exits: [
|
|
34
|
+
{ uuid: 'split-exit-1', destination_uuid: null },
|
|
35
|
+
{ uuid: 'split-exit-2', destination_uuid: null }
|
|
36
|
+
],
|
|
37
|
+
router: {
|
|
38
|
+
type: 'random',
|
|
39
|
+
categories: [
|
|
40
|
+
{
|
|
41
|
+
uuid: 'cat-1',
|
|
42
|
+
name: 'First bucket',
|
|
43
|
+
exit_uuid: 'split-exit-1'
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
uuid: 'cat-2',
|
|
47
|
+
name: 'Second bucket',
|
|
48
|
+
exit_uuid: 'split-exit-2'
|
|
49
|
+
}
|
|
50
|
+
]
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
],
|
|
54
|
+
_ui: {
|
|
55
|
+
nodes: {
|
|
56
|
+
'split-node': {
|
|
57
|
+
position: { left: 0, top: 0 },
|
|
58
|
+
type: 'split_by_random'
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
languages: []
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const openLocalizationWindow = async (
|
|
66
|
+
flowEditor: Editor
|
|
67
|
+
): Promise<HTMLElement> => {
|
|
68
|
+
const tab = flowEditor.querySelector('#localization-tab');
|
|
69
|
+
tab.dispatchEvent(
|
|
70
|
+
new CustomEvent('temba-button-clicked', { bubbles: true })
|
|
71
|
+
);
|
|
72
|
+
await flowEditor.updateComplete;
|
|
73
|
+
return flowEditor.querySelector('#localization-window') as HTMLElement;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
before(() => {
|
|
77
|
+
storeElement = document.createElement('temba-store');
|
|
78
|
+
(storeElement as any).getLanguageName = (code: string) =>
|
|
79
|
+
languageNames[code];
|
|
80
|
+
document.body.appendChild(storeElement);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
after(() => {
|
|
84
|
+
storeElement?.remove();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
afterEach(() => {
|
|
88
|
+
editor?.remove();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
beforeEach(async () => {
|
|
92
|
+
// Create a flow definition with localization data
|
|
93
|
+
const flowDefinition: FlowDefinition = {
|
|
94
|
+
uuid: 'test-flow',
|
|
95
|
+
name: 'Test Flow',
|
|
96
|
+
language: 'eng',
|
|
97
|
+
type: 'messaging',
|
|
98
|
+
revision: 1,
|
|
99
|
+
spec_version: '14.3',
|
|
100
|
+
localization: {
|
|
101
|
+
esp: {
|
|
102
|
+
'action-1': {
|
|
103
|
+
text: ['Hola mundo'],
|
|
104
|
+
quick_replies: ['Sí', 'No']
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
nodes: [
|
|
109
|
+
{
|
|
110
|
+
uuid: 'node-1',
|
|
111
|
+
actions: [
|
|
112
|
+
{
|
|
113
|
+
type: 'send_msg',
|
|
114
|
+
uuid: 'action-1',
|
|
115
|
+
text: 'Hello world',
|
|
116
|
+
quick_replies: ['Yes', 'No']
|
|
117
|
+
} as SendMsg
|
|
118
|
+
],
|
|
119
|
+
exits: [{ uuid: 'exit-1' }]
|
|
120
|
+
}
|
|
121
|
+
],
|
|
122
|
+
_ui: {
|
|
123
|
+
nodes: {
|
|
124
|
+
'node-1': {
|
|
125
|
+
position: { left: 100, top: 100 }
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
languages: []
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
// Initialize store with flow definition
|
|
133
|
+
zustand.getState().setFlowContents({
|
|
134
|
+
definition: flowDefinition,
|
|
135
|
+
info: {
|
|
136
|
+
results: [],
|
|
137
|
+
dependencies: [],
|
|
138
|
+
counts: { nodes: 1, languages: 2 },
|
|
139
|
+
locals: []
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
editor = await fixture(html`<temba-flow-editor></temba-flow-editor>`);
|
|
144
|
+
await editor.updateComplete;
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('should render localization floating tab when translations exist', () => {
|
|
148
|
+
const tab = editor.querySelector('#localization-tab');
|
|
149
|
+
expect(tab).to.exist;
|
|
150
|
+
|
|
151
|
+
const windowEl = editor.querySelector('#localization-window') as any;
|
|
152
|
+
expect(windowEl).to.exist;
|
|
153
|
+
expect(windowEl.hidden).to.be.true;
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('should open localization window with translation languages excluding base', async () => {
|
|
157
|
+
const tab = editor.querySelector('#localization-tab');
|
|
158
|
+
tab.dispatchEvent(
|
|
159
|
+
new CustomEvent('temba-button-clicked', { bubbles: true })
|
|
160
|
+
);
|
|
161
|
+
await editor.updateComplete;
|
|
162
|
+
|
|
163
|
+
const windowEl = editor.querySelector('#localization-window') as any;
|
|
164
|
+
expect(windowEl.hidden).to.be.false;
|
|
165
|
+
|
|
166
|
+
const select = windowEl.querySelector('temba-select') as any;
|
|
167
|
+
expect(select).to.exist;
|
|
168
|
+
expect(select.values[0].value).to.equal('fra');
|
|
169
|
+
|
|
170
|
+
const options = windowEl.querySelectorAll('temba-option');
|
|
171
|
+
expect(options.length).to.equal(2);
|
|
172
|
+
const optionLabels = Array.from(options).map((opt: Element) =>
|
|
173
|
+
opt.getAttribute('name')
|
|
174
|
+
);
|
|
175
|
+
expect(optionLabels).to.include('French');
|
|
176
|
+
expect(optionLabels).to.include('Spanish');
|
|
177
|
+
expect(optionLabels).to.not.include('English');
|
|
178
|
+
|
|
179
|
+
const state = zustand.getState();
|
|
180
|
+
expect(state.languageCode).to.equal('fra');
|
|
181
|
+
|
|
182
|
+
const summary = windowEl.querySelector('.localization-progress-summary');
|
|
183
|
+
expect(summary?.textContent.trim()).to.equal('0 of 1 items translated');
|
|
184
|
+
|
|
185
|
+
const progress = windowEl.querySelector('temba-progress') as any;
|
|
186
|
+
expect(progress).to.exist;
|
|
187
|
+
expect(progress.current).to.equal(0);
|
|
188
|
+
expect(progress.total).to.equal(1);
|
|
189
|
+
expect(progress.animated).to.be.false;
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('should toggle translation settings and persist include categories preference', async () => {
|
|
193
|
+
await openLocalizationWindow(editor);
|
|
194
|
+
|
|
195
|
+
const toggle = editor.querySelector(
|
|
196
|
+
'.translation-settings-toggle'
|
|
197
|
+
) as HTMLElement;
|
|
198
|
+
expect(toggle).to.exist;
|
|
199
|
+
|
|
200
|
+
toggle.dispatchEvent(new Event('click', { bubbles: true }));
|
|
201
|
+
await editor.updateComplete;
|
|
202
|
+
|
|
203
|
+
const checkbox = editor.querySelector(
|
|
204
|
+
'#translation-settings-panel temba-checkbox'
|
|
205
|
+
) as any;
|
|
206
|
+
expect(checkbox).to.exist;
|
|
207
|
+
expect(Boolean(checkbox.checked)).to.be.false;
|
|
208
|
+
|
|
209
|
+
checkbox.checked = true;
|
|
210
|
+
checkbox.dispatchEvent(new Event('change', { bubbles: true }));
|
|
211
|
+
await editor.updateComplete;
|
|
212
|
+
|
|
213
|
+
const filters = zustand.getState().flowDefinition._ui.translation_filters;
|
|
214
|
+
expect(filters?.categories).to.be.true;
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('should allow toggling translation languages within the window', async () => {
|
|
218
|
+
const tab = editor.querySelector('#localization-tab');
|
|
219
|
+
tab.dispatchEvent(
|
|
220
|
+
new CustomEvent('temba-button-clicked', { bubbles: true })
|
|
221
|
+
);
|
|
222
|
+
await editor.updateComplete;
|
|
223
|
+
|
|
224
|
+
const windowEl = editor.querySelector('#localization-window');
|
|
225
|
+
const select = windowEl.querySelector('temba-select') as any;
|
|
226
|
+
expect(select).to.exist;
|
|
227
|
+
|
|
228
|
+
select.values = [{ name: 'Spanish', value: 'esp' }];
|
|
229
|
+
select.dispatchEvent(new CustomEvent('change', { bubbles: true }));
|
|
230
|
+
await editor.updateComplete;
|
|
231
|
+
|
|
232
|
+
const state = zustand.getState();
|
|
233
|
+
expect(state.languageCode).to.equal('esp');
|
|
234
|
+
const summary = windowEl.querySelector('.localization-progress-summary');
|
|
235
|
+
expect(summary?.textContent.trim()).to.equal('All items are translated.');
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('should include category translations when include categories is enabled', async () => {
|
|
239
|
+
editor?.remove();
|
|
240
|
+
|
|
241
|
+
const categoryFlowDefinition: FlowDefinition =
|
|
242
|
+
buildCategoryFlowDefinition();
|
|
243
|
+
|
|
244
|
+
zustand.getState().setFlowContents({
|
|
245
|
+
definition: categoryFlowDefinition,
|
|
246
|
+
info: {
|
|
247
|
+
results: [],
|
|
248
|
+
dependencies: [],
|
|
249
|
+
counts: { nodes: 1, languages: 2 },
|
|
250
|
+
locals: []
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
editor = await fixture(html`<temba-flow-editor></temba-flow-editor>`);
|
|
255
|
+
await editor.updateComplete;
|
|
256
|
+
|
|
257
|
+
await openLocalizationWindow(editor);
|
|
258
|
+
|
|
259
|
+
let summary = editor
|
|
260
|
+
.querySelector('.localization-progress-summary')
|
|
261
|
+
.textContent.trim();
|
|
262
|
+
expect(summary).to.equal(
|
|
263
|
+
'Add content or enable more options to start translating.'
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
const toggle = editor.querySelector(
|
|
267
|
+
'.translation-settings-toggle'
|
|
268
|
+
) as HTMLElement;
|
|
269
|
+
toggle.dispatchEvent(new Event('click', { bubbles: true }));
|
|
270
|
+
await editor.updateComplete;
|
|
271
|
+
|
|
272
|
+
const checkbox = editor.querySelector(
|
|
273
|
+
'temba-checkbox[name="include-categories"]'
|
|
274
|
+
) as any;
|
|
275
|
+
checkbox.checked = true;
|
|
276
|
+
checkbox.dispatchEvent(new Event('change', { bubbles: true }));
|
|
277
|
+
await editor.updateComplete;
|
|
278
|
+
|
|
279
|
+
summary = editor
|
|
280
|
+
.querySelector('.localization-progress-summary')
|
|
281
|
+
.textContent.trim();
|
|
282
|
+
expect(summary).to.equal('0 of 2 items translated');
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it('should remove category localization when translation is cleared', async () => {
|
|
286
|
+
editor?.remove();
|
|
287
|
+
editor = null;
|
|
288
|
+
|
|
289
|
+
const flowDefinition = buildCategoryFlowDefinition({
|
|
290
|
+
fra: {
|
|
291
|
+
'cat-1': { name: ['Premier choix'] },
|
|
292
|
+
'cat-2': { name: ['Deuxième choix'] }
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
zustand.getState().setFlowContents({
|
|
297
|
+
definition: flowDefinition,
|
|
298
|
+
info: {
|
|
299
|
+
results: [],
|
|
300
|
+
dependencies: [],
|
|
301
|
+
counts: { nodes: 1, languages: 2 },
|
|
302
|
+
locals: []
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
zustand.getState().setLanguageCode('fra');
|
|
306
|
+
|
|
307
|
+
const state = zustand.getState();
|
|
308
|
+
const node = state.flowDefinition.nodes[0];
|
|
309
|
+
const nodeUI = state.flowDefinition._ui.nodes[node.uuid];
|
|
310
|
+
|
|
311
|
+
const nodeEditor: NodeEditor = await fixture(html`
|
|
312
|
+
<temba-node-editor
|
|
313
|
+
.node=${node}
|
|
314
|
+
.nodeUI=${nodeUI}
|
|
315
|
+
.isOpen=${true}
|
|
316
|
+
></temba-node-editor>
|
|
317
|
+
`);
|
|
318
|
+
await nodeEditor.updateComplete;
|
|
319
|
+
|
|
320
|
+
const formData = (nodeEditor as any).formData;
|
|
321
|
+
formData.categories['cat-1'].localizedName = '';
|
|
322
|
+
|
|
323
|
+
(nodeEditor as any).handleSave();
|
|
324
|
+
|
|
325
|
+
const localization =
|
|
326
|
+
zustand.getState().flowDefinition.localization?.fra || {};
|
|
327
|
+
expect(localization['cat-1']).to.be.undefined;
|
|
328
|
+
expect(localization['cat-2']).to.deep.equal({ name: ['Deuxième choix'] });
|
|
329
|
+
|
|
330
|
+
nodeEditor.remove();
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it('should remove empty localization entries when all category translations are cleared', async () => {
|
|
334
|
+
editor?.remove();
|
|
335
|
+
editor = null;
|
|
336
|
+
|
|
337
|
+
const flowDefinition = buildCategoryFlowDefinition({
|
|
338
|
+
fra: {
|
|
339
|
+
'cat-1': { name: ['Premier choix'] }
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
zustand.getState().setFlowContents({
|
|
344
|
+
definition: flowDefinition,
|
|
345
|
+
info: {
|
|
346
|
+
results: [],
|
|
347
|
+
dependencies: [],
|
|
348
|
+
counts: { nodes: 1, languages: 2 },
|
|
349
|
+
locals: []
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
zustand.getState().setLanguageCode('fra');
|
|
353
|
+
|
|
354
|
+
const state = zustand.getState();
|
|
355
|
+
const node = state.flowDefinition.nodes[0];
|
|
356
|
+
const nodeUI = state.flowDefinition._ui.nodes[node.uuid];
|
|
357
|
+
|
|
358
|
+
const nodeEditor: NodeEditor = await fixture(html`
|
|
359
|
+
<temba-node-editor
|
|
360
|
+
.node=${node}
|
|
361
|
+
.nodeUI=${nodeUI}
|
|
362
|
+
.isOpen=${true}
|
|
363
|
+
></temba-node-editor>
|
|
364
|
+
`);
|
|
365
|
+
await nodeEditor.updateComplete;
|
|
366
|
+
|
|
367
|
+
const formData = (nodeEditor as any).formData;
|
|
368
|
+
formData.categories['cat-1'].localizedName = '';
|
|
369
|
+
|
|
370
|
+
(nodeEditor as any).handleSave();
|
|
371
|
+
|
|
372
|
+
const localization = zustand.getState().flowDefinition.localization;
|
|
373
|
+
expect(localization).to.be.undefined;
|
|
374
|
+
|
|
375
|
+
nodeEditor.remove();
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it('should open auto translate dialog when clicking auto translate', async () => {
|
|
379
|
+
await openLocalizationWindow(editor);
|
|
380
|
+
|
|
381
|
+
const autoTranslateButton = editor.querySelector(
|
|
382
|
+
'.auto-translate-button'
|
|
383
|
+
) as HTMLButtonElement;
|
|
384
|
+
expect(autoTranslateButton.disabled).to.be.false;
|
|
385
|
+
|
|
386
|
+
autoTranslateButton.click();
|
|
387
|
+
await editor.updateComplete;
|
|
388
|
+
|
|
389
|
+
expect((editor as any).autoTranslateDialogOpen).to.be.true;
|
|
390
|
+
const dialog = editor.querySelector(
|
|
391
|
+
'temba-dialog[header="Auto translate"]'
|
|
392
|
+
);
|
|
393
|
+
const modelSelect = dialog?.querySelector(
|
|
394
|
+
'.auto-translate-model-select'
|
|
395
|
+
) as HTMLElement;
|
|
396
|
+
expect(modelSelect).to.exist;
|
|
397
|
+
expect(modelSelect.getAttribute('endpoint')).to.equal(
|
|
398
|
+
'/api/internal/llms.json'
|
|
399
|
+
);
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
it('should return to base language when window closes', async () => {
|
|
403
|
+
const tab = editor.querySelector('#localization-tab');
|
|
404
|
+
tab.dispatchEvent(
|
|
405
|
+
new CustomEvent('temba-button-clicked', { bubbles: true })
|
|
406
|
+
);
|
|
407
|
+
await editor.updateComplete;
|
|
408
|
+
|
|
409
|
+
const windowEl = editor.querySelector('#localization-window') as any;
|
|
410
|
+
windowEl.close();
|
|
411
|
+
await editor.updateComplete;
|
|
412
|
+
|
|
413
|
+
const state = zustand.getState();
|
|
414
|
+
expect(state.languageCode).to.equal('eng');
|
|
415
|
+
expect(windowEl.hidden).to.be.true;
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
it('should load base language values when in English', () => {
|
|
419
|
+
const action: SendMsg = {
|
|
420
|
+
type: 'send_msg',
|
|
421
|
+
uuid: 'action-1',
|
|
422
|
+
text: 'Hello world',
|
|
423
|
+
quick_replies: ['Yes', 'No']
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
const formData = send_msg.toFormData(action);
|
|
427
|
+
|
|
428
|
+
expect(formData.text).to.equal('Hello world');
|
|
429
|
+
expect(formData.quick_replies).to.have.lengthOf(2);
|
|
430
|
+
expect(formData.quick_replies[0].value).to.equal('Yes');
|
|
431
|
+
expect(formData.quick_replies[1].value).to.equal('No');
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
it('should load localized values when in Spanish', () => {
|
|
435
|
+
const action: SendMsg = {
|
|
436
|
+
type: 'send_msg',
|
|
437
|
+
uuid: 'action-1',
|
|
438
|
+
text: 'Hello world',
|
|
439
|
+
quick_replies: ['Yes', 'No']
|
|
440
|
+
};
|
|
441
|
+
|
|
442
|
+
const localization = {
|
|
443
|
+
text: ['Hola mundo'],
|
|
444
|
+
quick_replies: ['Sí', 'No']
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
const formData = send_msg.toLocalizationFormData(action, localization);
|
|
448
|
+
|
|
449
|
+
expect(formData.text).to.equal('Hola mundo');
|
|
450
|
+
expect(formData.quick_replies).to.have.lengthOf(2);
|
|
451
|
+
expect(formData.quick_replies[0].value).to.equal('Sí');
|
|
452
|
+
expect(formData.quick_replies[1].value).to.equal('No');
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
it('should fall back to base language if no localization exists', () => {
|
|
456
|
+
const action: SendMsg = {
|
|
457
|
+
type: 'send_msg',
|
|
458
|
+
uuid: 'action-1',
|
|
459
|
+
text: 'Hello world',
|
|
460
|
+
quick_replies: ['Yes', 'No']
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
const localization = {}; // Empty localization
|
|
464
|
+
|
|
465
|
+
const formData = send_msg.toLocalizationFormData(action, localization);
|
|
466
|
+
|
|
467
|
+
// Should show base language values (but empty since localization is empty)
|
|
468
|
+
expect(formData.text).to.equal('');
|
|
469
|
+
expect(formData.quick_replies).to.be.undefined;
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
it('should convert form data to localization format', () => {
|
|
473
|
+
const action: SendMsg = {
|
|
474
|
+
type: 'send_msg',
|
|
475
|
+
uuid: 'action-1',
|
|
476
|
+
text: 'Hello world',
|
|
477
|
+
quick_replies: ['Yes', 'No']
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
const formData = {
|
|
481
|
+
uuid: 'action-1',
|
|
482
|
+
text: 'Bonjour le monde',
|
|
483
|
+
quick_replies: [
|
|
484
|
+
{ name: 'Oui', value: 'Oui' },
|
|
485
|
+
{ name: 'Non', value: 'Non' }
|
|
486
|
+
]
|
|
487
|
+
};
|
|
488
|
+
|
|
489
|
+
const localization = send_msg.fromLocalizationFormData(formData, action);
|
|
490
|
+
|
|
491
|
+
expect(localization.text).to.deep.equal(['Bonjour le monde']);
|
|
492
|
+
expect(localization.quick_replies).to.deep.equal(['Oui', 'Non']);
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
it('should not include unchanged values in localization', () => {
|
|
496
|
+
const action: SendMsg = {
|
|
497
|
+
type: 'send_msg',
|
|
498
|
+
uuid: 'action-1',
|
|
499
|
+
text: 'Hello world',
|
|
500
|
+
quick_replies: ['Yes', 'No']
|
|
501
|
+
};
|
|
502
|
+
|
|
503
|
+
const formData = {
|
|
504
|
+
uuid: 'action-1',
|
|
505
|
+
text: 'Hello world', // Same as base
|
|
506
|
+
quick_replies: [
|
|
507
|
+
{ name: 'Yes', value: 'Yes' },
|
|
508
|
+
{ name: 'No', value: 'No' }
|
|
509
|
+
] // Same as base
|
|
510
|
+
};
|
|
511
|
+
|
|
512
|
+
const localization = send_msg.fromLocalizationFormData(formData, action);
|
|
513
|
+
|
|
514
|
+
// should not include unchanged values
|
|
515
|
+
expect(localization.text).to.be.undefined;
|
|
516
|
+
expect(localization.quick_replies).to.be.undefined;
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
it('should include language name in dialog header when translating', async () => {
|
|
520
|
+
// Switch to Spanish
|
|
521
|
+
zustand.getState().setLanguageCode('esp');
|
|
522
|
+
|
|
523
|
+
const action: SendMsg = {
|
|
524
|
+
type: 'send_msg',
|
|
525
|
+
uuid: 'action-1',
|
|
526
|
+
text: 'Hello world',
|
|
527
|
+
quick_replies: []
|
|
528
|
+
};
|
|
529
|
+
|
|
530
|
+
const nodeEditor: NodeEditor = await fixture(html`
|
|
531
|
+
<temba-node-editor .action=${action} .isOpen=${true}> </temba-node-editor>
|
|
532
|
+
`);
|
|
533
|
+
|
|
534
|
+
await nodeEditor.updateComplete;
|
|
535
|
+
|
|
536
|
+
// Check dialog header
|
|
537
|
+
const dialog = nodeEditor.shadowRoot.querySelector('temba-dialog');
|
|
538
|
+
expect(dialog).to.exist;
|
|
539
|
+
expect(dialog.getAttribute('header')).to.equal('Spanish - Send Message');
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
it('should handle attachments in localization', () => {
|
|
543
|
+
const action: SendMsg = {
|
|
544
|
+
type: 'send_msg',
|
|
545
|
+
uuid: 'action-1',
|
|
546
|
+
text: 'Hello',
|
|
547
|
+
attachments: ['image/jpeg:http://example.com/image.jpg']
|
|
548
|
+
};
|
|
549
|
+
|
|
550
|
+
const localization = {
|
|
551
|
+
text: ['Hola'],
|
|
552
|
+
attachments: ['image/jpeg:http://example.com/imagen.jpg']
|
|
553
|
+
};
|
|
554
|
+
|
|
555
|
+
const formData = send_msg.toLocalizationFormData(action, localization);
|
|
556
|
+
|
|
557
|
+
expect(formData.text).to.equal('Hola');
|
|
558
|
+
expect(formData.attachments).to.have.lengthOf(1);
|
|
559
|
+
expect(formData.attachments[0]).to.equal(
|
|
560
|
+
'image/jpeg:http://example.com/imagen.jpg'
|
|
561
|
+
);
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
it('should handle runtime attachments in localization', () => {
|
|
565
|
+
const action: SendMsg = {
|
|
566
|
+
type: 'send_msg',
|
|
567
|
+
uuid: 'action-1',
|
|
568
|
+
text: 'Hello',
|
|
569
|
+
attachments: ['image:@fields.profile_pic']
|
|
570
|
+
};
|
|
571
|
+
|
|
572
|
+
const localization = {
|
|
573
|
+
text: ['Hola'],
|
|
574
|
+
attachments: ['image:@fields.foto_perfil']
|
|
575
|
+
};
|
|
576
|
+
|
|
577
|
+
const formData = send_msg.toLocalizationFormData(action, localization);
|
|
578
|
+
|
|
579
|
+
expect(formData.text).to.equal('Hola');
|
|
580
|
+
expect(formData.runtime_attachments).to.have.lengthOf(1);
|
|
581
|
+
expect(formData.runtime_attachments[0].expression).to.equal(
|
|
582
|
+
'@fields.foto_perfil'
|
|
583
|
+
);
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
it('should identify localizable fields', () => {
|
|
587
|
+
expect(send_msg.localizable).to.exist;
|
|
588
|
+
expect(send_msg.localizable).to.include('text');
|
|
589
|
+
expect(send_msg.localizable).to.include('quick_replies');
|
|
590
|
+
expect(send_msg.localizable).to.include('attachments');
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
it('should save empty localization when all values match base', () => {
|
|
594
|
+
const action: SendMsg = {
|
|
595
|
+
type: 'send_msg',
|
|
596
|
+
uuid: 'action-1',
|
|
597
|
+
text: 'Hello world',
|
|
598
|
+
quick_replies: []
|
|
599
|
+
};
|
|
600
|
+
|
|
601
|
+
const formData = {
|
|
602
|
+
uuid: 'action-1',
|
|
603
|
+
text: 'Hello world' // Same as base, so shouldn't be saved
|
|
604
|
+
};
|
|
605
|
+
|
|
606
|
+
const localization = send_msg.fromLocalizationFormData(formData, action);
|
|
607
|
+
|
|
608
|
+
// empty localization when nothing changed
|
|
609
|
+
expect(Object.keys(localization)).to.have.lengthOf(0);
|
|
610
|
+
});
|
|
611
|
+
});
|