@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
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { SPLIT_GROUPS, FormData, NodeConfig, FlowTypes } from '../types';
|
|
2
|
+
import { Node, Category, Exit } from '../../store/flow-definition';
|
|
3
|
+
import { generateUUID } from '../../utils';
|
|
4
|
+
import {
|
|
5
|
+
resultNameField,
|
|
6
|
+
categoriesToLocalizationFormData,
|
|
7
|
+
localizationFormDataToCategories
|
|
8
|
+
} from './shared';
|
|
9
|
+
|
|
10
|
+
const DIAL_CATEGORIES = ['Answered', 'No Answer', 'Busy', 'Failed'];
|
|
11
|
+
const DIAL_CASES = [
|
|
12
|
+
{ type: 'has_only_text', arguments: ['answered'], categoryName: 'Answered' },
|
|
13
|
+
{
|
|
14
|
+
type: 'has_only_text',
|
|
15
|
+
arguments: ['no_answer'],
|
|
16
|
+
categoryName: 'No Answer'
|
|
17
|
+
},
|
|
18
|
+
{ type: 'has_only_text', arguments: ['busy'], categoryName: 'Busy' }
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
export const wait_for_dial: NodeConfig = {
|
|
22
|
+
type: 'wait_for_dial',
|
|
23
|
+
name: 'Redirect Call',
|
|
24
|
+
group: SPLIT_GROUPS.wait,
|
|
25
|
+
flowTypes: [FlowTypes.VOICE],
|
|
26
|
+
router: {
|
|
27
|
+
type: 'switch',
|
|
28
|
+
defaultCategory: 'Failed',
|
|
29
|
+
rules: DIAL_CASES.map((c) => ({
|
|
30
|
+
type: c.type as any,
|
|
31
|
+
arguments: c.arguments,
|
|
32
|
+
categoryName: c.categoryName
|
|
33
|
+
}))
|
|
34
|
+
},
|
|
35
|
+
form: {
|
|
36
|
+
phone: {
|
|
37
|
+
type: 'text',
|
|
38
|
+
label: 'Phone Number',
|
|
39
|
+
required: true,
|
|
40
|
+
evaluated: true,
|
|
41
|
+
placeholder: 'Phone number or expression'
|
|
42
|
+
},
|
|
43
|
+
dial_limit_seconds: {
|
|
44
|
+
type: 'text',
|
|
45
|
+
label: 'Dial Limit (seconds)',
|
|
46
|
+
required: false,
|
|
47
|
+
placeholder: '60'
|
|
48
|
+
},
|
|
49
|
+
call_limit_seconds: {
|
|
50
|
+
type: 'text',
|
|
51
|
+
label: 'Call Limit (seconds)',
|
|
52
|
+
required: false,
|
|
53
|
+
placeholder: '7200'
|
|
54
|
+
},
|
|
55
|
+
result_name: resultNameField
|
|
56
|
+
},
|
|
57
|
+
layout: [
|
|
58
|
+
'phone',
|
|
59
|
+
{
|
|
60
|
+
type: 'row',
|
|
61
|
+
items: ['dial_limit_seconds', 'call_limit_seconds']
|
|
62
|
+
},
|
|
63
|
+
'result_name'
|
|
64
|
+
],
|
|
65
|
+
toFormData: (node: Node) => {
|
|
66
|
+
const wait = node.router?.wait;
|
|
67
|
+
return {
|
|
68
|
+
uuid: node.uuid,
|
|
69
|
+
phone: wait?.phone || '',
|
|
70
|
+
dial_limit_seconds: wait?.dial_limit_seconds
|
|
71
|
+
? String(wait.dial_limit_seconds)
|
|
72
|
+
: '',
|
|
73
|
+
call_limit_seconds: wait?.call_limit_seconds
|
|
74
|
+
? String(wait.call_limit_seconds)
|
|
75
|
+
: '',
|
|
76
|
+
result_name: node.router?.result_name || ''
|
|
77
|
+
};
|
|
78
|
+
},
|
|
79
|
+
fromFormData: (formData: FormData, originalNode: Node): Node => {
|
|
80
|
+
const existingCategories = originalNode.router?.categories || [];
|
|
81
|
+
const existingExits = originalNode.exits || [];
|
|
82
|
+
const existingCases = originalNode.router?.cases || [];
|
|
83
|
+
|
|
84
|
+
const categories: Category[] = [];
|
|
85
|
+
const exits: Exit[] = [];
|
|
86
|
+
const cases: any[] = [];
|
|
87
|
+
|
|
88
|
+
// Build categories and cases for each dial outcome
|
|
89
|
+
for (const catName of DIAL_CATEGORIES) {
|
|
90
|
+
const existing = existingCategories.find(
|
|
91
|
+
(c: Category) => c.name === catName
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
if (existing) {
|
|
95
|
+
categories.push(existing);
|
|
96
|
+
const existingExit = existingExits.find(
|
|
97
|
+
(e: Exit) => e.uuid === existing.exit_uuid
|
|
98
|
+
);
|
|
99
|
+
exits.push(
|
|
100
|
+
existingExit || {
|
|
101
|
+
uuid: existing.exit_uuid,
|
|
102
|
+
destination_uuid: null
|
|
103
|
+
}
|
|
104
|
+
);
|
|
105
|
+
} else {
|
|
106
|
+
const exitUuid = generateUUID();
|
|
107
|
+
categories.push({
|
|
108
|
+
uuid: generateUUID(),
|
|
109
|
+
name: catName,
|
|
110
|
+
exit_uuid: exitUuid
|
|
111
|
+
});
|
|
112
|
+
exits.push({ uuid: exitUuid, destination_uuid: null });
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Build cases for non-default categories
|
|
117
|
+
for (const dialCase of DIAL_CASES) {
|
|
118
|
+
const category = categories.find((c) => c.name === dialCase.categoryName);
|
|
119
|
+
if (!category) continue;
|
|
120
|
+
|
|
121
|
+
const existingCase = existingCases.find(
|
|
122
|
+
(c: any) =>
|
|
123
|
+
c.type === dialCase.type && c.arguments?.[0] === dialCase.arguments[0]
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
cases.push({
|
|
127
|
+
uuid: existingCase?.uuid || generateUUID(),
|
|
128
|
+
type: dialCase.type,
|
|
129
|
+
arguments: dialCase.arguments,
|
|
130
|
+
category_uuid: category.uuid
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const failedCategory = categories.find((c) => c.name === 'Failed');
|
|
135
|
+
|
|
136
|
+
// Build wait config
|
|
137
|
+
const phone = (formData.phone || '').trim();
|
|
138
|
+
const dialLimit = parseInt(formData.dial_limit_seconds, 10);
|
|
139
|
+
const callLimit = parseInt(formData.call_limit_seconds, 10);
|
|
140
|
+
|
|
141
|
+
const waitConfig: any = {
|
|
142
|
+
type: 'dial',
|
|
143
|
+
phone
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
if (!isNaN(dialLimit) && dialLimit > 0) {
|
|
147
|
+
waitConfig.dial_limit_seconds = dialLimit;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (!isNaN(callLimit) && callLimit > 0) {
|
|
151
|
+
waitConfig.call_limit_seconds = callLimit;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const router: any = {
|
|
155
|
+
type: 'switch',
|
|
156
|
+
operand: '@(default(resume.dial.status, ""))',
|
|
157
|
+
default_category_uuid: failedCategory?.uuid,
|
|
158
|
+
cases,
|
|
159
|
+
categories,
|
|
160
|
+
wait: waitConfig
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
if (formData.result_name && formData.result_name.trim() !== '') {
|
|
164
|
+
router.result_name = formData.result_name.trim();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
...originalNode,
|
|
169
|
+
router,
|
|
170
|
+
exits
|
|
171
|
+
};
|
|
172
|
+
},
|
|
173
|
+
localizable: 'categories',
|
|
174
|
+
toLocalizationFormData: categoriesToLocalizationFormData,
|
|
175
|
+
fromLocalizationFormData: localizationFormDataToCategories
|
|
176
|
+
};
|
|
@@ -1,8 +1,92 @@
|
|
|
1
|
-
import { SPLIT_GROUPS, NodeConfig, FlowTypes } from '../types';
|
|
1
|
+
import { SPLIT_GROUPS, FormData, NodeConfig, FlowTypes } from '../types';
|
|
2
|
+
import { Node } from '../../store/flow-definition';
|
|
3
|
+
import { createRulesRouter } from '../../utils';
|
|
4
|
+
import {
|
|
5
|
+
getDigitOperators,
|
|
6
|
+
operatorsToSelectOptions,
|
|
7
|
+
getOperatorConfig
|
|
8
|
+
} from '../operators';
|
|
9
|
+
import {
|
|
10
|
+
resultNameField,
|
|
11
|
+
categoriesToLocalizationFormData,
|
|
12
|
+
localizationFormDataToCategories
|
|
13
|
+
} from './shared';
|
|
14
|
+
import {
|
|
15
|
+
createRulesArrayConfig,
|
|
16
|
+
extractUserRules,
|
|
17
|
+
casesToFormRules
|
|
18
|
+
} from './shared-rules';
|
|
2
19
|
|
|
3
20
|
export const wait_for_digits: NodeConfig = {
|
|
4
21
|
type: 'wait_for_digits',
|
|
5
22
|
name: 'Wait for Digits',
|
|
6
23
|
group: SPLIT_GROUPS.wait,
|
|
7
|
-
flowTypes: [FlowTypes.VOICE]
|
|
24
|
+
flowTypes: [FlowTypes.VOICE],
|
|
25
|
+
dialogSize: 'large',
|
|
26
|
+
form: {
|
|
27
|
+
rules: createRulesArrayConfig(
|
|
28
|
+
operatorsToSelectOptions(getDigitOperators())
|
|
29
|
+
),
|
|
30
|
+
result_name: resultNameField
|
|
31
|
+
},
|
|
32
|
+
layout: [
|
|
33
|
+
{
|
|
34
|
+
type: 'text',
|
|
35
|
+
text: 'Rules match against all digits pressed by the caller followed by the # sign.'
|
|
36
|
+
},
|
|
37
|
+
'rules',
|
|
38
|
+
'result_name'
|
|
39
|
+
],
|
|
40
|
+
validate: (_formData: FormData) => {
|
|
41
|
+
return {
|
|
42
|
+
valid: true,
|
|
43
|
+
errors: {}
|
|
44
|
+
};
|
|
45
|
+
},
|
|
46
|
+
toFormData: (node: Node) => {
|
|
47
|
+
const rules = casesToFormRules(node);
|
|
48
|
+
return {
|
|
49
|
+
uuid: node.uuid,
|
|
50
|
+
rules,
|
|
51
|
+
result_name: node.router?.result_name || ''
|
|
52
|
+
};
|
|
53
|
+
},
|
|
54
|
+
fromFormData: (formData: FormData, originalNode: Node): Node => {
|
|
55
|
+
const userRules = extractUserRules(formData);
|
|
56
|
+
const existingCategories = originalNode.router?.categories || [];
|
|
57
|
+
const existingExits = originalNode.exits || [];
|
|
58
|
+
const existingCases = originalNode.router?.cases || [];
|
|
59
|
+
|
|
60
|
+
const { router, exits } = createRulesRouter(
|
|
61
|
+
'@input.text',
|
|
62
|
+
userRules,
|
|
63
|
+
getOperatorConfig,
|
|
64
|
+
existingCategories,
|
|
65
|
+
existingExits,
|
|
66
|
+
existingCases
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
const finalRouter: any = {
|
|
70
|
+
...router,
|
|
71
|
+
wait: {
|
|
72
|
+
type: 'msg',
|
|
73
|
+
hint: {
|
|
74
|
+
type: 'digits'
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
if (formData.result_name && formData.result_name.trim() !== '') {
|
|
80
|
+
finalRouter.result_name = formData.result_name.trim();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
...originalNode,
|
|
85
|
+
router: finalRouter,
|
|
86
|
+
exits
|
|
87
|
+
};
|
|
88
|
+
},
|
|
89
|
+
localizable: 'categories',
|
|
90
|
+
toLocalizationFormData: categoriesToLocalizationFormData,
|
|
91
|
+
fromLocalizationFormData: localizationFormDataToCategories
|
|
8
92
|
};
|
|
@@ -1,8 +1,214 @@
|
|
|
1
|
-
import { SPLIT_GROUPS, NodeConfig, FlowTypes } from '../types';
|
|
1
|
+
import { SPLIT_GROUPS, FormData, NodeConfig, FlowTypes } from '../types';
|
|
2
|
+
import { Node, Category, Exit } from '../../store/flow-definition';
|
|
3
|
+
import { generateUUID } from '../../utils';
|
|
4
|
+
import {
|
|
5
|
+
resultNameField,
|
|
6
|
+
categoriesToLocalizationFormData,
|
|
7
|
+
localizationFormDataToCategories
|
|
8
|
+
} from './shared';
|
|
9
|
+
|
|
10
|
+
// Menu digits in display order: 1-9 then 0
|
|
11
|
+
const MENU_DIGITS = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '0'];
|
|
12
|
+
|
|
13
|
+
function digitFieldKey(digit: string): string {
|
|
14
|
+
return `digit_${digit}`;
|
|
15
|
+
}
|
|
2
16
|
|
|
3
17
|
export const wait_for_menu: NodeConfig = {
|
|
4
18
|
type: 'wait_for_menu',
|
|
5
|
-
name: 'Wait for Menu
|
|
19
|
+
name: 'Wait for Menu',
|
|
6
20
|
group: SPLIT_GROUPS.wait,
|
|
7
|
-
flowTypes: [FlowTypes.VOICE]
|
|
21
|
+
flowTypes: [FlowTypes.VOICE],
|
|
22
|
+
form: {
|
|
23
|
+
...Object.fromEntries(
|
|
24
|
+
MENU_DIGITS.map((digit) => [
|
|
25
|
+
digitFieldKey(digit),
|
|
26
|
+
{
|
|
27
|
+
type: 'text' as const,
|
|
28
|
+
required: false,
|
|
29
|
+
placeholder: '',
|
|
30
|
+
flavor: 'xsmall' as const
|
|
31
|
+
}
|
|
32
|
+
])
|
|
33
|
+
),
|
|
34
|
+
result_name: resultNameField
|
|
35
|
+
},
|
|
36
|
+
layout: [
|
|
37
|
+
{
|
|
38
|
+
type: 'row' as const,
|
|
39
|
+
items: ['digit_1', 'digit_2', 'digit_3'],
|
|
40
|
+
gap: '2rem',
|
|
41
|
+
marginBottom: '0.5rem',
|
|
42
|
+
inlineLabels: { digit_1: '1', digit_2: '2', digit_3: '3' }
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
type: 'row' as const,
|
|
46
|
+
items: ['digit_4', 'digit_5', 'digit_6'],
|
|
47
|
+
gap: '2rem',
|
|
48
|
+
marginBottom: '0.5rem',
|
|
49
|
+
inlineLabels: { digit_4: '4', digit_5: '5', digit_6: '6' }
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
type: 'row' as const,
|
|
53
|
+
items: ['digit_7', 'digit_8', 'digit_9'],
|
|
54
|
+
gap: '2rem',
|
|
55
|
+
marginBottom: '0.5rem',
|
|
56
|
+
inlineLabels: { digit_7: '7', digit_8: '8', digit_9: '9' }
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
type: 'row' as const,
|
|
60
|
+
items: [
|
|
61
|
+
{ type: 'spacer' as const },
|
|
62
|
+
'digit_0',
|
|
63
|
+
{ type: 'spacer' as const }
|
|
64
|
+
],
|
|
65
|
+
gap: '2rem',
|
|
66
|
+
inlineLabels: { digit_0: '0' }
|
|
67
|
+
},
|
|
68
|
+
'result_name'
|
|
69
|
+
],
|
|
70
|
+
toFormData: (node: Node) => {
|
|
71
|
+
const formData: FormData = {
|
|
72
|
+
uuid: node.uuid,
|
|
73
|
+
result_name: node.router?.result_name || ''
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// Initialize all digit fields as empty
|
|
77
|
+
for (const digit of MENU_DIGITS) {
|
|
78
|
+
formData[digitFieldKey(digit)] = '';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Fill in category names from cases
|
|
82
|
+
if (node.router?.cases && node.router?.categories) {
|
|
83
|
+
for (const case_ of node.router.cases) {
|
|
84
|
+
if (case_.type === 'has_number_eq' && case_.arguments?.[0]) {
|
|
85
|
+
const digit = case_.arguments[0];
|
|
86
|
+
const category = node.router.categories.find(
|
|
87
|
+
(cat: Category) => cat.uuid === case_.category_uuid
|
|
88
|
+
);
|
|
89
|
+
if (category && MENU_DIGITS.includes(digit)) {
|
|
90
|
+
formData[digitFieldKey(digit)] = category.name;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return formData;
|
|
97
|
+
},
|
|
98
|
+
fromFormData: (formData: FormData, originalNode: Node): Node => {
|
|
99
|
+
const existingCategories = originalNode.router?.categories || [];
|
|
100
|
+
const existingExits = originalNode.exits || [];
|
|
101
|
+
const existingCases = originalNode.router?.cases || [];
|
|
102
|
+
|
|
103
|
+
const categories: Category[] = [];
|
|
104
|
+
const exits: Exit[] = [];
|
|
105
|
+
const cases: any[] = [];
|
|
106
|
+
|
|
107
|
+
// Build categories and cases for each filled digit
|
|
108
|
+
for (const digit of MENU_DIGITS) {
|
|
109
|
+
const categoryName = (formData[digitFieldKey(digit)] || '').trim();
|
|
110
|
+
if (!categoryName) continue;
|
|
111
|
+
|
|
112
|
+
// Check if a category with this name already exists in our new list
|
|
113
|
+
let category = categories.find((c) => c.name === categoryName);
|
|
114
|
+
|
|
115
|
+
if (!category) {
|
|
116
|
+
// Try to find existing category with same name to preserve UUIDs
|
|
117
|
+
const existingCat = existingCategories.find(
|
|
118
|
+
(c: Category) => c.name === categoryName
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
if (existingCat) {
|
|
122
|
+
category = existingCat;
|
|
123
|
+
const existingExit = existingExits.find(
|
|
124
|
+
(e: Exit) => e.uuid === existingCat.exit_uuid
|
|
125
|
+
);
|
|
126
|
+
categories.push(category);
|
|
127
|
+
exits.push(
|
|
128
|
+
existingExit || {
|
|
129
|
+
uuid: existingCat.exit_uuid,
|
|
130
|
+
destination_uuid: null
|
|
131
|
+
}
|
|
132
|
+
);
|
|
133
|
+
} else {
|
|
134
|
+
const exitUuid = generateUUID();
|
|
135
|
+
category = {
|
|
136
|
+
uuid: generateUUID(),
|
|
137
|
+
name: categoryName,
|
|
138
|
+
exit_uuid: exitUuid
|
|
139
|
+
};
|
|
140
|
+
categories.push(category);
|
|
141
|
+
exits.push({ uuid: exitUuid, destination_uuid: null });
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Find existing case for this digit to preserve UUID
|
|
146
|
+
const existingCase = existingCases.find(
|
|
147
|
+
(c: any) => c.type === 'has_number_eq' && c.arguments?.[0] === digit
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
cases.push({
|
|
151
|
+
uuid: existingCase?.uuid || generateUUID(),
|
|
152
|
+
type: 'has_number_eq',
|
|
153
|
+
arguments: [digit],
|
|
154
|
+
category_uuid: category.uuid
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Add "Other" default category
|
|
159
|
+
const existingOther = existingCategories.find(
|
|
160
|
+
(c: Category) => c.name === 'Other'
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
let otherCategory: Category;
|
|
164
|
+
if (existingOther) {
|
|
165
|
+
otherCategory = existingOther;
|
|
166
|
+
const existingExit = existingExits.find(
|
|
167
|
+
(e: Exit) => e.uuid === existingOther.exit_uuid
|
|
168
|
+
);
|
|
169
|
+
exits.push(
|
|
170
|
+
existingExit || {
|
|
171
|
+
uuid: existingOther.exit_uuid,
|
|
172
|
+
destination_uuid: null
|
|
173
|
+
}
|
|
174
|
+
);
|
|
175
|
+
} else {
|
|
176
|
+
const exitUuid = generateUUID();
|
|
177
|
+
otherCategory = {
|
|
178
|
+
uuid: generateUUID(),
|
|
179
|
+
name: 'Other',
|
|
180
|
+
exit_uuid: exitUuid
|
|
181
|
+
};
|
|
182
|
+
exits.push({ uuid: exitUuid, destination_uuid: null });
|
|
183
|
+
}
|
|
184
|
+
categories.push(otherCategory);
|
|
185
|
+
|
|
186
|
+
const router: any = {
|
|
187
|
+
type: 'switch',
|
|
188
|
+
operand: '@input.text',
|
|
189
|
+
default_category_uuid: otherCategory.uuid,
|
|
190
|
+
cases,
|
|
191
|
+
categories,
|
|
192
|
+
wait: {
|
|
193
|
+
type: 'msg',
|
|
194
|
+
hint: {
|
|
195
|
+
type: 'digits',
|
|
196
|
+
count: 1
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
if (formData.result_name && formData.result_name.trim() !== '') {
|
|
202
|
+
router.result_name = formData.result_name.trim();
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
...originalNode,
|
|
207
|
+
router,
|
|
208
|
+
exits
|
|
209
|
+
};
|
|
210
|
+
},
|
|
211
|
+
localizable: 'categories',
|
|
212
|
+
toLocalizationFormData: categoriesToLocalizationFormData,
|
|
213
|
+
fromLocalizationFormData: localizationFormDataToCategories
|
|
8
214
|
};
|
package/src/flow/operators.ts
CHANGED
|
@@ -44,11 +44,6 @@ export const OPERATORS: OperatorConfig[] = [
|
|
|
44
44
|
operands: 0,
|
|
45
45
|
categoryName: 'Has Text'
|
|
46
46
|
},
|
|
47
|
-
{
|
|
48
|
-
type: 'has_pattern',
|
|
49
|
-
name: 'matches regex',
|
|
50
|
-
operands: 1
|
|
51
|
-
},
|
|
52
47
|
|
|
53
48
|
// Number operators
|
|
54
49
|
{
|
|
@@ -180,6 +175,11 @@ export const OPERATORS: OperatorConfig[] = [
|
|
|
180
175
|
operands: 0,
|
|
181
176
|
categoryName: 'Not Empty',
|
|
182
177
|
visibility: 'hidden'
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
type: 'has_pattern',
|
|
181
|
+
name: 'matches regex',
|
|
182
|
+
operands: 1
|
|
183
183
|
}
|
|
184
184
|
];
|
|
185
185
|
|
|
@@ -190,6 +190,24 @@ export const getWaitForResponseOperators = (): OperatorConfig[] => {
|
|
|
190
190
|
);
|
|
191
191
|
};
|
|
192
192
|
|
|
193
|
+
// Number operator types used for digit-based routing
|
|
194
|
+
const DIGIT_OPERATOR_TYPES = new Set([
|
|
195
|
+
'has_beginning',
|
|
196
|
+
'has_number',
|
|
197
|
+
'has_number_between',
|
|
198
|
+
'has_number_lt',
|
|
199
|
+
'has_number_lte',
|
|
200
|
+
'has_number_eq',
|
|
201
|
+
'has_number_gte',
|
|
202
|
+
'has_number_gt',
|
|
203
|
+
'has_pattern'
|
|
204
|
+
]);
|
|
205
|
+
|
|
206
|
+
// Get operators suitable for wait_for_digits rules (number operators + regex)
|
|
207
|
+
export const getDigitOperators = (): OperatorConfig[] => {
|
|
208
|
+
return OPERATORS.filter((op) => DIGIT_OPERATOR_TYPES.has(op.type));
|
|
209
|
+
};
|
|
210
|
+
|
|
193
211
|
// Get operator configuration by type
|
|
194
212
|
export const getOperatorConfig = (type: string): OperatorConfig | undefined => {
|
|
195
213
|
return OPERATORS.find((op) => op.type === type);
|
package/src/flow/types.ts
CHANGED
|
@@ -114,6 +114,8 @@ export interface NodeConfig extends FormConfig {
|
|
|
114
114
|
rules?: {
|
|
115
115
|
type:
|
|
116
116
|
| 'has_number_between'
|
|
117
|
+
| 'has_number_eq'
|
|
118
|
+
| 'has_only_text'
|
|
117
119
|
| 'has_string'
|
|
118
120
|
| 'has_value'
|
|
119
121
|
| 'has_not_value'
|
|
@@ -253,6 +255,12 @@ export interface MessageEditorFieldConfig extends BaseFieldConfig {
|
|
|
253
255
|
disableCompletion?: boolean;
|
|
254
256
|
}
|
|
255
257
|
|
|
258
|
+
export interface MediaFieldConfig extends BaseFieldConfig {
|
|
259
|
+
type: 'media';
|
|
260
|
+
accept?: string; // MIME filter, e.g. 'audio/*'
|
|
261
|
+
endpoint?: string; // upload endpoint, defaults to DEFAULT_MEDIA_ENDPOINT
|
|
262
|
+
}
|
|
263
|
+
|
|
256
264
|
export type FieldConfig =
|
|
257
265
|
| TextFieldConfig
|
|
258
266
|
| TextareaFieldConfig
|
|
@@ -260,7 +268,8 @@ export type FieldConfig =
|
|
|
260
268
|
| KeyValueFieldConfig
|
|
261
269
|
| ArrayFieldConfig
|
|
262
270
|
| CheckboxFieldConfig
|
|
263
|
-
| MessageEditorFieldConfig
|
|
271
|
+
| MessageEditorFieldConfig
|
|
272
|
+
| MediaFieldConfig;
|
|
264
273
|
|
|
265
274
|
// Layout configurations for better form organization
|
|
266
275
|
// Recursive layout system - any layout item can contain other layout items
|
|
@@ -276,6 +285,8 @@ export interface RowLayoutConfig {
|
|
|
276
285
|
gap?: string; // CSS gap value, defaults to '1rem'
|
|
277
286
|
label?: string; // optional label for the entire row
|
|
278
287
|
helpText?: string; // optional help text for the entire row
|
|
288
|
+
inlineLabels?: Record<string, string>; // map of field name to inline label text
|
|
289
|
+
marginBottom?: string; // CSS margin-bottom for spacing below the row
|
|
279
290
|
}
|
|
280
291
|
|
|
281
292
|
export interface GroupLayoutConfig {
|
|
@@ -288,10 +299,21 @@ export interface GroupLayoutConfig {
|
|
|
288
299
|
getGroupValueCount?: (formData: FormData) => number; // optional function to get count for bubble display
|
|
289
300
|
}
|
|
290
301
|
|
|
302
|
+
export interface SpacerLayoutConfig {
|
|
303
|
+
type: 'spacer';
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export interface TextLayoutConfig {
|
|
307
|
+
type: 'text';
|
|
308
|
+
text: string;
|
|
309
|
+
}
|
|
310
|
+
|
|
291
311
|
export type LayoutItem =
|
|
292
312
|
| FieldItemConfig
|
|
293
313
|
| RowLayoutConfig
|
|
294
314
|
| GroupLayoutConfig
|
|
315
|
+
| SpacerLayoutConfig
|
|
316
|
+
| TextLayoutConfig
|
|
295
317
|
| string; // string is shorthand for field
|
|
296
318
|
|
|
297
319
|
/**
|