@nyaruka/temba-components 0.156.8 → 0.156.10
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 +17 -0
- package/dist/temba-components.js +678 -631
- package/dist/temba-components.js.map +1 -1
- package/package.json +1 -1
- package/src/display/Chat.ts +8 -8
- package/src/display/FloatingTab.ts +2 -2
- package/src/display/Options.ts +8 -2
- package/src/flow/CanvasMenu.ts +20 -25
- package/src/flow/CanvasNode.ts +16 -12
- package/src/flow/DragManager.ts +93 -33
- package/src/flow/Editor.ts +64 -59
- package/src/flow/EditorToolbar.ts +19 -20
- package/src/flow/FlowSearch.ts +9 -7
- package/src/flow/MessageTable.ts +181 -74
- package/src/flow/NodeEditor.ts +55 -72
- package/src/flow/RevisionsWindow.ts +2 -4
- package/src/flow/ZoomManager.ts +1 -2
- package/src/flow/actions/play_audio.ts +1 -28
- package/src/flow/actions/say_msg.ts +1 -40
- package/src/flow/actions/send_broadcast.ts +1 -2
- package/src/flow/actions/send_email.ts +5 -56
- package/src/flow/actions/send_msg.ts +10 -2
- package/src/flow/actions/start_session.ts +1 -2
- package/src/flow/categoryLocalization.ts +1 -5
- package/src/flow/categoryUtils.ts +139 -0
- package/src/flow/nodes/shared-rules.ts +6 -16
- package/src/flow/nodes/shared.ts +113 -6
- package/src/flow/nodes/split_by_airtime.ts +41 -63
- package/src/flow/nodes/split_by_contact_field.ts +8 -17
- package/src/flow/nodes/split_by_expression.ts +8 -17
- package/src/flow/nodes/split_by_groups.ts +34 -112
- package/src/flow/nodes/split_by_llm.ts +1 -7
- package/src/flow/nodes/split_by_llm_categorize.ts +27 -43
- package/src/flow/nodes/split_by_random.ts +39 -99
- package/src/flow/nodes/split_by_resthook.ts +5 -19
- package/src/flow/nodes/split_by_run_result.ts +8 -17
- package/src/flow/nodes/split_by_scheme.ts +39 -124
- package/src/flow/nodes/split_by_subflow.ts +1 -7
- package/src/flow/nodes/split_by_ticket.ts +1 -7
- package/src/flow/nodes/split_by_webhook.ts +2 -8
- package/src/flow/nodes/wait_for_audio.ts +1 -7
- package/src/flow/nodes/wait_for_dial.ts +2 -8
- package/src/flow/nodes/wait_for_digits.ts +5 -7
- package/src/flow/nodes/wait_for_menu.ts +5 -7
- package/src/flow/nodes/wait_for_response.ts +10 -18
- package/src/flow/types.ts +27 -0
- package/src/flow/utils.ts +111 -3
- package/src/form/Compose.ts +11 -4
- package/src/form/MessageEditor.ts +5 -3
- package/src/form/RichEditor.ts +3 -1
- package/src/form/TemplateEditor.ts +5 -1
- package/src/form/select/Select.ts +11 -9
- package/src/layout/AccordionSection.ts +9 -3
- package/src/layout/Modax.ts +1 -3
- package/src/live/ContactChat.ts +54 -46
- package/src/simulator/Simulator.ts +9 -3
- package/src/store/AppState.ts +1 -1
- package/src/store/Store.ts +6 -1
- package/src/utils.ts +21 -16
|
@@ -10,9 +10,7 @@ import {
|
|
|
10
10
|
resultNameField,
|
|
11
11
|
localizeRulesField,
|
|
12
12
|
localizeCategoriesField,
|
|
13
|
-
nodeOptionsAccordion
|
|
14
|
-
categoriesToLocalizationFormData,
|
|
15
|
-
localizationFormDataToCategories
|
|
13
|
+
nodeOptionsAccordion
|
|
16
14
|
} from './shared';
|
|
17
15
|
import {
|
|
18
16
|
createRulesArrayConfig,
|
|
@@ -20,6 +18,7 @@ import {
|
|
|
20
18
|
casesToFormRules
|
|
21
19
|
} from './shared-rules';
|
|
22
20
|
import { getStore } from '../../store/Store';
|
|
21
|
+
import { validateWith } from '../utils';
|
|
23
22
|
|
|
24
23
|
// delimit index options (first through 20th)
|
|
25
24
|
const DELIMIT_INDEX_OPTIONS = [
|
|
@@ -125,19 +124,11 @@ export const split_by_run_result: NodeConfig = {
|
|
|
125
124
|
'rules',
|
|
126
125
|
nodeOptionsAccordion
|
|
127
126
|
],
|
|
128
|
-
validate: (formData
|
|
129
|
-
const errors: { [key: string]: string } = {};
|
|
130
|
-
|
|
131
|
-
// Validate result is provided
|
|
127
|
+
validate: validateWith((formData, errors) => {
|
|
132
128
|
if (!formData.result || formData.result.length === 0) {
|
|
133
129
|
errors.result = 'A flow result is required';
|
|
134
130
|
}
|
|
135
|
-
|
|
136
|
-
return {
|
|
137
|
-
valid: Object.keys(errors).length === 0,
|
|
138
|
-
errors
|
|
139
|
-
};
|
|
140
|
-
},
|
|
131
|
+
}),
|
|
141
132
|
toFormData: (node: Node, nodeUI?: any) => {
|
|
142
133
|
// Get the result from the UI config operand (source of truth)
|
|
143
134
|
// Normalize: toUIConfig saves { id, name, type } but the Select component
|
|
@@ -274,12 +265,12 @@ export const split_by_run_result: NodeConfig = {
|
|
|
274
265
|
: 'split_by_run_result';
|
|
275
266
|
|
|
276
267
|
config.localizeRules = !!formData.localizeRules;
|
|
277
|
-
config.localizeCategories = formData.result_name
|
|
268
|
+
config.localizeCategories = formData.result_name
|
|
269
|
+
? !!formData.localizeCategories
|
|
270
|
+
: false;
|
|
278
271
|
return config;
|
|
279
272
|
},
|
|
280
273
|
|
|
281
274
|
// Localization support for categories
|
|
282
|
-
localizable: 'categories'
|
|
283
|
-
toLocalizationFormData: categoriesToLocalizationFormData,
|
|
284
|
-
fromLocalizationFormData: localizationFormDataToCategories
|
|
275
|
+
localizable: 'categories'
|
|
285
276
|
};
|
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
import { SPLIT_GROUPS, FormData, NodeConfig, FlowTypes } from '../types';
|
|
2
|
-
import { Node,
|
|
3
|
-
import {
|
|
4
|
-
import { SCHEMES } from '../utils';
|
|
2
|
+
import { Node, Case } from '../../store/flow-definition.d';
|
|
3
|
+
import { SCHEMES, validateWith } from '../utils';
|
|
5
4
|
import {
|
|
6
5
|
resultNameField,
|
|
7
6
|
nodeOptionsAccordionSimple,
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
buildCategoriesExitsCases,
|
|
8
|
+
appendOtherCategory
|
|
10
9
|
} from './shared';
|
|
11
10
|
|
|
12
|
-
// Helper function to get scheme options for the select dropdown
|
|
13
11
|
const getSchemeOptions = () => {
|
|
14
12
|
return SCHEMES.map((scheme) => ({
|
|
15
13
|
value: scheme.scheme,
|
|
@@ -17,95 +15,8 @@ const getSchemeOptions = () => {
|
|
|
17
15
|
}));
|
|
18
16
|
};
|
|
19
17
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
selectedSchemes: string[],
|
|
23
|
-
existingCategories: Category[] = [],
|
|
24
|
-
existingExits: Exit[] = [],
|
|
25
|
-
existingCases: Case[] = [],
|
|
26
|
-
resultName: string = ''
|
|
27
|
-
) => {
|
|
28
|
-
const categories: Category[] = [];
|
|
29
|
-
const exits: Exit[] = [];
|
|
30
|
-
const cases: Case[] = [];
|
|
31
|
-
|
|
32
|
-
// Create categories, exits, and cases for each selected scheme
|
|
33
|
-
selectedSchemes.forEach((scheme) => {
|
|
34
|
-
const schemeObj = SCHEMES.find((s) => s.scheme === scheme);
|
|
35
|
-
const schemeName = schemeObj?.name || scheme;
|
|
36
|
-
|
|
37
|
-
// Try to find existing category by scheme name
|
|
38
|
-
const existingCategory = existingCategories.find(
|
|
39
|
-
(cat) => cat.name === schemeName
|
|
40
|
-
);
|
|
41
|
-
const existingExit = existingCategory
|
|
42
|
-
? existingExits.find((exit) => exit.uuid === existingCategory.exit_uuid)
|
|
43
|
-
: null;
|
|
44
|
-
const existingCase = existingCases.find((c) => c.arguments?.[0] === scheme);
|
|
45
|
-
|
|
46
|
-
const exitUuid = existingExit?.uuid || generateUUID();
|
|
47
|
-
const categoryUuid = existingCategory?.uuid || generateUUID();
|
|
48
|
-
const caseUuid = existingCase?.uuid || generateUUID();
|
|
49
|
-
|
|
50
|
-
categories.push({
|
|
51
|
-
uuid: categoryUuid,
|
|
52
|
-
name: schemeName,
|
|
53
|
-
exit_uuid: exitUuid
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
exits.push({
|
|
57
|
-
uuid: exitUuid,
|
|
58
|
-
destination_uuid: existingExit?.destination_uuid || null
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
cases.push({
|
|
62
|
-
uuid: caseUuid,
|
|
63
|
-
type: 'has_only_phrase',
|
|
64
|
-
arguments: [scheme],
|
|
65
|
-
category_uuid: categoryUuid
|
|
66
|
-
});
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
// Add default "Other" category for schemes not in the selected list
|
|
70
|
-
const existingOtherCategory = existingCategories.find((cat) => {
|
|
71
|
-
const matchesSelected = selectedSchemes.some((scheme) => {
|
|
72
|
-
const schemeObj = SCHEMES.find((s) => s.scheme === scheme);
|
|
73
|
-
return schemeObj?.name === cat.name;
|
|
74
|
-
});
|
|
75
|
-
return cat.name === 'Other' && !matchesSelected;
|
|
76
|
-
});
|
|
77
|
-
const existingOtherExit = existingOtherCategory
|
|
78
|
-
? existingExits.find(
|
|
79
|
-
(exit) => exit.uuid === existingOtherCategory.exit_uuid
|
|
80
|
-
)
|
|
81
|
-
: null;
|
|
82
|
-
|
|
83
|
-
const otherExitUuid = existingOtherExit?.uuid || generateUUID();
|
|
84
|
-
const otherCategoryUuid = existingOtherCategory?.uuid || generateUUID();
|
|
85
|
-
|
|
86
|
-
categories.push({
|
|
87
|
-
uuid: otherCategoryUuid,
|
|
88
|
-
name: 'Other',
|
|
89
|
-
exit_uuid: otherExitUuid
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
exits.push({
|
|
93
|
-
uuid: otherExitUuid,
|
|
94
|
-
destination_uuid: existingOtherExit?.destination_uuid || null
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
return {
|
|
98
|
-
router: {
|
|
99
|
-
type: 'switch' as const,
|
|
100
|
-
cases: cases,
|
|
101
|
-
categories: categories,
|
|
102
|
-
default_category_uuid: otherCategoryUuid,
|
|
103
|
-
operand: '@(urn_parts(contact.urn).scheme)',
|
|
104
|
-
result_name: resultName
|
|
105
|
-
},
|
|
106
|
-
exits: exits
|
|
107
|
-
};
|
|
108
|
-
};
|
|
18
|
+
const getSchemeName = (scheme: string) =>
|
|
19
|
+
SCHEMES.find((s) => s.scheme === scheme)?.name || scheme;
|
|
109
20
|
|
|
110
21
|
export const split_by_scheme: NodeConfig = {
|
|
111
22
|
type: 'split_by_scheme',
|
|
@@ -127,9 +38,7 @@ export const split_by_scheme: NodeConfig = {
|
|
|
127
38
|
result_name: resultNameField
|
|
128
39
|
},
|
|
129
40
|
layout: ['schemes', nodeOptionsAccordionSimple],
|
|
130
|
-
validate: (formData
|
|
131
|
-
const errors: { [key: string]: string } = {};
|
|
132
|
-
|
|
41
|
+
validate: validateWith((formData, errors) => {
|
|
133
42
|
if (
|
|
134
43
|
!formData.schemes ||
|
|
135
44
|
!Array.isArray(formData.schemes) ||
|
|
@@ -137,14 +46,8 @@ export const split_by_scheme: NodeConfig = {
|
|
|
137
46
|
) {
|
|
138
47
|
errors.schemes = 'At least one channel type is required';
|
|
139
48
|
}
|
|
140
|
-
|
|
141
|
-
return {
|
|
142
|
-
valid: Object.keys(errors).length === 0,
|
|
143
|
-
errors
|
|
144
|
-
};
|
|
145
|
-
},
|
|
49
|
+
}),
|
|
146
50
|
toFormData: (node: Node) => {
|
|
147
|
-
// Extract schemes from the existing node structure
|
|
148
51
|
const schemes: string[] = [];
|
|
149
52
|
|
|
150
53
|
if (node.router?.cases) {
|
|
@@ -157,43 +60,57 @@ export const split_by_scheme: NodeConfig = {
|
|
|
157
60
|
|
|
158
61
|
return {
|
|
159
62
|
uuid: node.uuid,
|
|
160
|
-
schemes: schemes.map((scheme) => {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
name: schemeObj?.name || scheme
|
|
165
|
-
};
|
|
166
|
-
}),
|
|
63
|
+
schemes: schemes.map((scheme) => ({
|
|
64
|
+
value: scheme,
|
|
65
|
+
name: getSchemeName(scheme)
|
|
66
|
+
})),
|
|
167
67
|
result_name: node.router?.result_name || ''
|
|
168
68
|
};
|
|
169
69
|
},
|
|
170
70
|
fromFormData: (formData: FormData, originalNode: Node): Node => {
|
|
171
|
-
// Get selected schemes (handle both array of objects and array of strings)
|
|
172
71
|
const selectedSchemes = (formData.schemes || [])
|
|
173
72
|
.filter((scheme: any) => scheme)
|
|
174
73
|
.map((scheme: any) =>
|
|
175
74
|
typeof scheme === 'string' ? scheme : scheme.value
|
|
176
75
|
);
|
|
177
76
|
|
|
178
|
-
// Create router and exits using existing data when possible
|
|
179
77
|
const existingCategories = originalNode.router?.categories || [];
|
|
180
78
|
const existingExits = originalNode.exits || [];
|
|
181
79
|
const existingCases = originalNode.router?.cases || [];
|
|
182
80
|
|
|
183
|
-
const {
|
|
184
|
-
selectedSchemes
|
|
81
|
+
const { categories, exits, cases } = buildCategoriesExitsCases(
|
|
82
|
+
selectedSchemes.map((scheme) => ({
|
|
83
|
+
name: getSchemeName(scheme),
|
|
84
|
+
case: {
|
|
85
|
+
type: 'has_only_phrase',
|
|
86
|
+
arguments: [scheme]
|
|
87
|
+
}
|
|
88
|
+
})),
|
|
89
|
+
existingCategories,
|
|
90
|
+
existingExits,
|
|
91
|
+
existingCases
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
const defaultCategoryUuid = appendOtherCategory(
|
|
95
|
+
categories,
|
|
96
|
+
exits,
|
|
185
97
|
existingCategories,
|
|
186
98
|
existingExits,
|
|
187
|
-
|
|
188
|
-
formData.result_name || ''
|
|
99
|
+
selectedSchemes.map(getSchemeName)
|
|
189
100
|
);
|
|
190
101
|
|
|
191
|
-
// Return the complete node
|
|
192
102
|
return {
|
|
193
103
|
uuid: originalNode.uuid,
|
|
194
104
|
actions: originalNode.actions || [],
|
|
195
|
-
router:
|
|
196
|
-
|
|
105
|
+
router: {
|
|
106
|
+
type: 'switch',
|
|
107
|
+
cases,
|
|
108
|
+
categories,
|
|
109
|
+
default_category_uuid: defaultCategoryUuid,
|
|
110
|
+
operand: '@(urn_parts(contact.urn).scheme)',
|
|
111
|
+
result_name: formData.result_name || ''
|
|
112
|
+
},
|
|
113
|
+
exits
|
|
197
114
|
};
|
|
198
115
|
},
|
|
199
116
|
router: {
|
|
@@ -202,7 +119,5 @@ export const split_by_scheme: NodeConfig = {
|
|
|
202
119
|
},
|
|
203
120
|
|
|
204
121
|
// Localization support for categories
|
|
205
|
-
localizable: 'categories'
|
|
206
|
-
toLocalizationFormData: categoriesToLocalizationFormData,
|
|
207
|
-
fromLocalizationFormData: localizationFormDataToCategories
|
|
122
|
+
localizable: 'categories'
|
|
208
123
|
};
|
|
@@ -4,10 +4,6 @@ import { generateUUID } from '../../utils';
|
|
|
4
4
|
import { html } from 'lit';
|
|
5
5
|
import { renderFlowLinks } from '../utils';
|
|
6
6
|
import { shouldExcludeFlow } from '../flow-utils';
|
|
7
|
-
import {
|
|
8
|
-
categoriesToLocalizationFormData,
|
|
9
|
-
localizationFormDataToCategories
|
|
10
|
-
} from './shared';
|
|
11
7
|
|
|
12
8
|
export const split_by_subflow: NodeConfig = {
|
|
13
9
|
type: 'split_by_subflow',
|
|
@@ -293,7 +289,5 @@ export const split_by_subflow: NodeConfig = {
|
|
|
293
289
|
|
|
294
290
|
// Localization support for categories
|
|
295
291
|
localizable: 'categories',
|
|
296
|
-
nonTranslatableCategories: 'all'
|
|
297
|
-
toLocalizationFormData: categoriesToLocalizationFormData,
|
|
298
|
-
fromLocalizationFormData: localizationFormDataToCategories
|
|
292
|
+
nonTranslatableCategories: 'all'
|
|
299
293
|
};
|
|
@@ -2,10 +2,6 @@ import { ACTION_GROUPS, FormData, NodeConfig, FlowTypes } from '../types';
|
|
|
2
2
|
import { Node, OpenTicket } from '../../store/flow-definition';
|
|
3
3
|
import { generateUUID, createSuccessFailureRouter } from '../../utils';
|
|
4
4
|
import { html } from 'lit';
|
|
5
|
-
import {
|
|
6
|
-
categoriesToLocalizationFormData,
|
|
7
|
-
localizationFormDataToCategories
|
|
8
|
-
} from './shared';
|
|
9
5
|
|
|
10
6
|
export const split_by_ticket: NodeConfig = {
|
|
11
7
|
type: 'split_by_ticket',
|
|
@@ -147,7 +143,5 @@ export const split_by_ticket: NodeConfig = {
|
|
|
147
143
|
|
|
148
144
|
// Localization support for categories
|
|
149
145
|
localizable: 'categories',
|
|
150
|
-
nonTranslatableCategories: 'all'
|
|
151
|
-
toLocalizationFormData: categoriesToLocalizationFormData,
|
|
152
|
-
fromLocalizationFormData: localizationFormDataToCategories
|
|
146
|
+
nonTranslatableCategories: 'all'
|
|
153
147
|
};
|
|
@@ -2,11 +2,7 @@ import { ACTION_GROUPS, FormData, NodeConfig, FlowTypes } from '../types';
|
|
|
2
2
|
import { CallWebhook, Node } from '../../store/flow-definition';
|
|
3
3
|
import { generateUUID, createSuccessFailureRouter } from '../../utils';
|
|
4
4
|
import { html } from 'lit';
|
|
5
|
-
import {
|
|
6
|
-
resultNameField,
|
|
7
|
-
categoriesToLocalizationFormData,
|
|
8
|
-
localizationFormDataToCategories
|
|
9
|
-
} from './shared';
|
|
5
|
+
import { resultNameField } from './shared';
|
|
10
6
|
import { renderClamped, renderHighlightedText } from '../utils';
|
|
11
7
|
|
|
12
8
|
// Default headers as {key, value} arrays (format used by key-value editor)
|
|
@@ -267,7 +263,5 @@ export const split_by_webhook: NodeConfig = {
|
|
|
267
263
|
|
|
268
264
|
// Localization support for categories
|
|
269
265
|
localizable: 'categories',
|
|
270
|
-
nonTranslatableCategories: 'all'
|
|
271
|
-
toLocalizationFormData: categoriesToLocalizationFormData,
|
|
272
|
-
fromLocalizationFormData: localizationFormDataToCategories
|
|
266
|
+
nonTranslatableCategories: 'all'
|
|
273
267
|
};
|
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
import { SPLIT_GROUPS, FormData, NodeConfig, FlowTypes } from '../types';
|
|
2
2
|
import { Node, Category, Exit } from '../../store/flow-definition';
|
|
3
3
|
import { generateUUID } from '../../utils';
|
|
4
|
-
import {
|
|
5
|
-
categoriesToLocalizationFormData,
|
|
6
|
-
localizationFormDataToCategories
|
|
7
|
-
} from './shared';
|
|
8
4
|
|
|
9
5
|
export const wait_for_audio: NodeConfig = {
|
|
10
6
|
type: 'wait_for_audio',
|
|
@@ -84,7 +80,5 @@ export const wait_for_audio: NodeConfig = {
|
|
|
84
80
|
};
|
|
85
81
|
},
|
|
86
82
|
localizable: 'categories',
|
|
87
|
-
nonTranslatableCategories: 'all'
|
|
88
|
-
toLocalizationFormData: categoriesToLocalizationFormData,
|
|
89
|
-
fromLocalizationFormData: localizationFormDataToCategories
|
|
83
|
+
nonTranslatableCategories: 'all'
|
|
90
84
|
};
|
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
import { SPLIT_GROUPS, FormData, NodeConfig, FlowTypes } from '../types';
|
|
2
2
|
import { Node, Category, Exit } from '../../store/flow-definition';
|
|
3
3
|
import { generateUUID } from '../../utils';
|
|
4
|
-
import {
|
|
5
|
-
resultNameField,
|
|
6
|
-
categoriesToLocalizationFormData,
|
|
7
|
-
localizationFormDataToCategories
|
|
8
|
-
} from './shared';
|
|
4
|
+
import { resultNameField } from './shared';
|
|
9
5
|
|
|
10
6
|
const DIAL_CATEGORIES = ['Answered', 'No Answer', 'Busy', 'Failed'];
|
|
11
7
|
const DIAL_CASES = [
|
|
@@ -171,7 +167,5 @@ export const wait_for_dial: NodeConfig = {
|
|
|
171
167
|
};
|
|
172
168
|
},
|
|
173
169
|
localizable: 'categories',
|
|
174
|
-
nonTranslatableCategories: 'all'
|
|
175
|
-
toLocalizationFormData: categoriesToLocalizationFormData,
|
|
176
|
-
fromLocalizationFormData: localizationFormDataToCategories
|
|
170
|
+
nonTranslatableCategories: 'all'
|
|
177
171
|
};
|
|
@@ -10,9 +10,7 @@ import {
|
|
|
10
10
|
resultNameField,
|
|
11
11
|
localizeRulesField,
|
|
12
12
|
localizeCategoriesField,
|
|
13
|
-
nodeOptionsAccordion
|
|
14
|
-
categoriesToLocalizationFormData,
|
|
15
|
-
localizationFormDataToCategories
|
|
13
|
+
nodeOptionsAccordion
|
|
16
14
|
} from './shared';
|
|
17
15
|
import {
|
|
18
16
|
createRulesArrayConfig,
|
|
@@ -62,7 +60,9 @@ export const wait_for_digits: NodeConfig = {
|
|
|
62
60
|
toUIConfig: (formData: FormData) => {
|
|
63
61
|
const config: Record<string, any> = {};
|
|
64
62
|
config.localizeRules = !!formData.localizeRules;
|
|
65
|
-
config.localizeCategories = formData.result_name
|
|
63
|
+
config.localizeCategories = formData.result_name
|
|
64
|
+
? !!formData.localizeCategories
|
|
65
|
+
: false;
|
|
66
66
|
return config;
|
|
67
67
|
},
|
|
68
68
|
fromFormData: (formData: FormData, originalNode: Node): Node => {
|
|
@@ -100,7 +100,5 @@ export const wait_for_digits: NodeConfig = {
|
|
|
100
100
|
exits
|
|
101
101
|
};
|
|
102
102
|
},
|
|
103
|
-
localizable: 'categories'
|
|
104
|
-
toLocalizationFormData: categoriesToLocalizationFormData,
|
|
105
|
-
fromLocalizationFormData: localizationFormDataToCategories
|
|
103
|
+
localizable: 'categories'
|
|
106
104
|
};
|
|
@@ -5,9 +5,7 @@ import {
|
|
|
5
5
|
resultNameField,
|
|
6
6
|
localizeRulesField,
|
|
7
7
|
localizeCategoriesField,
|
|
8
|
-
nodeOptionsAccordion
|
|
9
|
-
categoriesToLocalizationFormData,
|
|
10
|
-
localizationFormDataToCategories
|
|
8
|
+
nodeOptionsAccordion
|
|
11
9
|
} from './shared';
|
|
12
10
|
|
|
13
11
|
// Menu digits in display order: 1-9 then 0
|
|
@@ -105,7 +103,9 @@ export const wait_for_menu: NodeConfig = {
|
|
|
105
103
|
toUIConfig: (formData: FormData) => {
|
|
106
104
|
const config: Record<string, any> = {};
|
|
107
105
|
config.localizeRules = !!formData.localizeRules;
|
|
108
|
-
config.localizeCategories = formData.result_name
|
|
106
|
+
config.localizeCategories = formData.result_name
|
|
107
|
+
? !!formData.localizeCategories
|
|
108
|
+
: false;
|
|
109
109
|
return config;
|
|
110
110
|
},
|
|
111
111
|
fromFormData: (formData: FormData, originalNode: Node): Node => {
|
|
@@ -221,7 +221,5 @@ export const wait_for_menu: NodeConfig = {
|
|
|
221
221
|
exits
|
|
222
222
|
};
|
|
223
223
|
},
|
|
224
|
-
localizable: 'categories'
|
|
225
|
-
toLocalizationFormData: categoriesToLocalizationFormData,
|
|
226
|
-
fromLocalizationFormData: localizationFormDataToCategories
|
|
224
|
+
localizable: 'categories'
|
|
227
225
|
};
|
|
@@ -10,15 +10,14 @@ import {
|
|
|
10
10
|
resultNameField,
|
|
11
11
|
localizeRulesField,
|
|
12
12
|
localizeCategoriesField,
|
|
13
|
-
nodeOptionsAccordion
|
|
14
|
-
categoriesToLocalizationFormData,
|
|
15
|
-
localizationFormDataToCategories
|
|
13
|
+
nodeOptionsAccordion
|
|
16
14
|
} from './shared';
|
|
17
15
|
import {
|
|
18
16
|
createRulesArrayConfig,
|
|
19
17
|
extractUserRules,
|
|
20
18
|
casesToFormRules
|
|
21
19
|
} from './shared-rules';
|
|
20
|
+
import { validateWith } from '../utils';
|
|
22
21
|
|
|
23
22
|
const TIMEOUT_OPTIONS = [
|
|
24
23
|
{ value: '60', name: '1 minute' },
|
|
@@ -127,17 +126,10 @@ export const wait_for_response: NodeConfig = {
|
|
|
127
126
|
gap: '0.5rem'
|
|
128
127
|
}
|
|
129
128
|
],
|
|
130
|
-
validate: (
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
// Rules with the same category name will be merged to use the same exit
|
|
135
|
-
|
|
136
|
-
return {
|
|
137
|
-
valid: Object.keys(errors).length === 0,
|
|
138
|
-
errors
|
|
139
|
-
};
|
|
140
|
-
},
|
|
129
|
+
validate: validateWith(() => {
|
|
130
|
+
// No validation needed - allow multiple rules to use same category name.
|
|
131
|
+
// Rules with the same category name will be merged to use the same exit.
|
|
132
|
+
}),
|
|
141
133
|
toFormData: (node: Node, nodeUI?: any) => {
|
|
142
134
|
// Extract rules from router cases using shared function
|
|
143
135
|
const rules = casesToFormRules(node);
|
|
@@ -165,7 +157,9 @@ export const wait_for_response: NodeConfig = {
|
|
|
165
157
|
toUIConfig: (formData: FormData) => {
|
|
166
158
|
const config: Record<string, any> = {};
|
|
167
159
|
config.localizeRules = !!formData.localizeRules;
|
|
168
|
-
config.localizeCategories = formData.result_name
|
|
160
|
+
config.localizeCategories = formData.result_name
|
|
161
|
+
? !!formData.localizeCategories
|
|
162
|
+
: false;
|
|
169
163
|
return config;
|
|
170
164
|
},
|
|
171
165
|
fromFormData: (formData: FormData, originalNode: Node): Node => {
|
|
@@ -413,7 +407,5 @@ export const wait_for_response: NodeConfig = {
|
|
|
413
407
|
|
|
414
408
|
// Localization support for categories
|
|
415
409
|
localizable: 'categories',
|
|
416
|
-
nonTranslatableCategories: ['Timeout']
|
|
417
|
-
toLocalizationFormData: categoriesToLocalizationFormData,
|
|
418
|
-
fromLocalizationFormData: localizationFormDataToCategories
|
|
410
|
+
nonTranslatableCategories: ['Timeout']
|
|
419
411
|
};
|
package/src/flow/types.ts
CHANGED
|
@@ -17,6 +17,33 @@ export const FlowTypes = {
|
|
|
17
17
|
|
|
18
18
|
export type FlowType = (typeof FlowTypes)[keyof typeof FlowTypes];
|
|
19
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Shortcut entry shown in the canvas context menu. The `type` is the
|
|
22
|
+
* action or node type to open the editor with when selected.
|
|
23
|
+
*/
|
|
24
|
+
export interface ContextMenuShortcut {
|
|
25
|
+
type: string;
|
|
26
|
+
name: string;
|
|
27
|
+
icon: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Per-flow-type context menu shortcuts.
|
|
32
|
+
*/
|
|
33
|
+
export const CONTEXT_MENU_SHORTCUTS: Record<FlowType, ContextMenuShortcut[]> = {
|
|
34
|
+
[FlowTypes.MESSAGE]: [
|
|
35
|
+
{ type: 'send_msg', name: 'Send Message', icon: 'send' },
|
|
36
|
+
{ type: 'wait_for_response', name: 'Wait for Response', icon: 'message' }
|
|
37
|
+
],
|
|
38
|
+
[FlowTypes.VOICE]: [
|
|
39
|
+
{ type: 'say_msg', name: 'Say Message', icon: 'send' },
|
|
40
|
+
{ type: 'wait_for_menu', name: 'Wait for Menu', icon: 'dots-grid' }
|
|
41
|
+
],
|
|
42
|
+
[FlowTypes.BACKGROUND]: [
|
|
43
|
+
{ type: 'set_contact_field', name: 'Update Contact', icon: 'contact' }
|
|
44
|
+
]
|
|
45
|
+
};
|
|
46
|
+
|
|
20
47
|
/**
|
|
21
48
|
* Features - defines the features available in the account
|
|
22
49
|
*/
|
package/src/flow/utils.ts
CHANGED
|
@@ -1,17 +1,125 @@
|
|
|
1
1
|
import { html, TemplateResult } from 'lit-html';
|
|
2
2
|
import { Action, NamedObject, FlowPosition } from '../store/flow-definition';
|
|
3
|
-
import { FlowIssue } from '../store/AppState';
|
|
3
|
+
import { FlowIssue, zustand } from '../store/AppState';
|
|
4
4
|
import { CustomEventType } from '../interfaces';
|
|
5
5
|
import { tokenize, TokenType } from '../excellent/tokenizer';
|
|
6
6
|
import { TOKEN_COLORS } from '../excellent/token-styles';
|
|
7
7
|
import { messageParser, sessionParser } from '../excellent/helpers';
|
|
8
|
+
import { FormData, ValidationResult, NodeConfig, ActionConfig } from './types';
|
|
9
|
+
import {
|
|
10
|
+
categoriesToLocalizationFormData,
|
|
11
|
+
localizationFormDataToCategories
|
|
12
|
+
} from './nodes/shared';
|
|
8
13
|
|
|
9
|
-
|
|
14
|
+
/**
|
|
15
|
+
* Wraps a validation check into the standard `validate` signature. The check
|
|
16
|
+
* populates an `errors` object; the wrapper computes `valid` and returns the
|
|
17
|
+
* ValidationResult. Eliminates the `const errors = {}; ...; return {valid, errors}`
|
|
18
|
+
* boilerplate that appears in every node and action config.
|
|
19
|
+
*/
|
|
20
|
+
export function validateWith(
|
|
21
|
+
check: (formData: FormData, errors: { [key: string]: string }) => void
|
|
22
|
+
): (formData: FormData) => ValidationResult {
|
|
23
|
+
return (formData: FormData) => {
|
|
24
|
+
const errors: { [key: string]: string } = {};
|
|
25
|
+
check(formData, errors);
|
|
26
|
+
return { valid: Object.keys(errors).length === 0, errors };
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function makeTextToLocalizationFormData(fields: readonly string[]) {
|
|
31
|
+
return (
|
|
32
|
+
action: { uuid: string },
|
|
33
|
+
localization: Record<string, any>
|
|
34
|
+
): FormData => {
|
|
35
|
+
const formData: FormData = { uuid: action.uuid };
|
|
36
|
+
fields.forEach((field) => {
|
|
37
|
+
const value = localization[field];
|
|
38
|
+
formData[field] = Array.isArray(value) ? value[0] || '' : '';
|
|
39
|
+
});
|
|
40
|
+
return formData;
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function makeTextFromLocalizationFormData(fields: readonly string[]) {
|
|
45
|
+
return (
|
|
46
|
+
formData: FormData,
|
|
47
|
+
action: Record<string, any>
|
|
48
|
+
): Record<string, any> => {
|
|
49
|
+
const localization: Record<string, any> = {};
|
|
50
|
+
fields.forEach((field) => {
|
|
51
|
+
const value = formData[field];
|
|
52
|
+
if (value && value.trim() !== '' && value !== action[field]) {
|
|
53
|
+
localization[field] = [value];
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
return localization;
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
type ToLocalizationFn = (
|
|
61
|
+
nodeOrAction: any,
|
|
62
|
+
localization: Record<string, any>
|
|
63
|
+
) => FormData;
|
|
64
|
+
|
|
65
|
+
type FromLocalizationFn = (
|
|
66
|
+
formData: FormData,
|
|
67
|
+
nodeOrAction: any
|
|
68
|
+
) => Record<string, any>;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Returns the `toLocalizationFormData` impl for a node or action config. Uses
|
|
72
|
+
* the explicit impl if the config provides one; otherwise infers from the
|
|
73
|
+
* `localizable` shape: `'categories'` → category helper, `string[]` → text
|
|
74
|
+
* factory. Returns undefined if the config isn't localizable.
|
|
75
|
+
*/
|
|
76
|
+
export function resolveToLocalizationFormData(
|
|
77
|
+
config: NodeConfig | ActionConfig
|
|
78
|
+
): ToLocalizationFn | undefined {
|
|
79
|
+
if (config.toLocalizationFormData) {
|
|
80
|
+
return config.toLocalizationFormData as ToLocalizationFn;
|
|
81
|
+
}
|
|
82
|
+
if (config.localizable === 'categories') {
|
|
83
|
+
return categoriesToLocalizationFormData;
|
|
84
|
+
}
|
|
85
|
+
if (Array.isArray(config.localizable)) {
|
|
86
|
+
return makeTextToLocalizationFormData(config.localizable);
|
|
87
|
+
}
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Mirror of resolveToLocalizationFormData for `fromLocalizationFormData`.
|
|
93
|
+
*/
|
|
94
|
+
export function resolveFromLocalizationFormData(
|
|
95
|
+
config: NodeConfig | ActionConfig
|
|
96
|
+
): FromLocalizationFn | undefined {
|
|
97
|
+
if (config.fromLocalizationFormData) {
|
|
98
|
+
return config.fromLocalizationFormData as FromLocalizationFn;
|
|
99
|
+
}
|
|
100
|
+
if (config.localizable === 'categories') {
|
|
101
|
+
return localizationFormDataToCategories;
|
|
102
|
+
}
|
|
103
|
+
if (Array.isArray(config.localizable)) {
|
|
104
|
+
return makeTextFromLocalizationFormData(config.localizable);
|
|
105
|
+
}
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const intlLanguageNames = new Intl.DisplayNames(['en'], { type: 'language' });
|
|
10
110
|
|
|
11
111
|
export function getLanguageDisplayName(code: string): string {
|
|
12
112
|
if (code === 'und') return 'Unknown';
|
|
113
|
+
|
|
114
|
+
// Prefer names from the RapidPro languages endpoint, which supplies
|
|
115
|
+
// ISO 639-3 codes (e.g. prd, pst) that Intl.DisplayNames doesn't cover.
|
|
116
|
+
const storeName = zustand.getState().languageNames?.[code];
|
|
117
|
+
if (storeName) {
|
|
118
|
+
return storeName;
|
|
119
|
+
}
|
|
120
|
+
|
|
13
121
|
try {
|
|
14
|
-
return
|
|
122
|
+
return intlLanguageNames.of(code) || code;
|
|
15
123
|
} catch {
|
|
16
124
|
return code;
|
|
17
125
|
}
|