@nyaruka/temba-components 0.129.6 → 0.129.8
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/.devcontainer/Dockerfile +11 -4
- package/.devcontainer/devcontainer.json +3 -2
- package/.github/workflows/build.yml +4 -14
- package/CHANGELOG.md +25 -1
- package/demo/components/flow/example.html +9 -2
- package/demo/components/flow/index.html +206 -0
- package/demo/components/message-editor/example.html +125 -0
- package/demo/components/textinput/completion.html +1 -0
- package/demo/data/flows/food-order.json +132 -0
- package/demo/data/flows/sample-flow.json +40 -24
- package/demo/index.html +1 -1
- package/dist/temba-components.js +518 -220
- package/dist/temba-components.js.map +1 -1
- package/out-tsc/src/display/Thumbnail.js +2 -1
- package/out-tsc/src/display/Thumbnail.js.map +1 -1
- package/out-tsc/src/events.js.map +1 -1
- package/out-tsc/src/flow/CanvasNode.js +10 -2
- package/out-tsc/src/flow/CanvasNode.js.map +1 -1
- package/out-tsc/src/flow/NodeEditor.js +245 -22
- package/out-tsc/src/flow/NodeEditor.js.map +1 -1
- package/out-tsc/src/flow/StickyNote.js +1 -1
- package/out-tsc/src/flow/StickyNote.js.map +1 -1
- package/out-tsc/src/flow/actions/call_webhook.js +26 -17
- package/out-tsc/src/flow/actions/call_webhook.js.map +1 -1
- package/out-tsc/src/flow/actions/send_email.js +1 -2
- package/out-tsc/src/flow/actions/send_email.js.map +1 -1
- package/out-tsc/src/flow/actions/send_msg.js +155 -7
- package/out-tsc/src/flow/actions/send_msg.js.map +1 -1
- package/out-tsc/src/flow/types.js.map +1 -1
- package/out-tsc/src/form/ArrayEditor.js +111 -38
- package/out-tsc/src/form/ArrayEditor.js.map +1 -1
- package/out-tsc/src/form/BaseListEditor.js +19 -4
- package/out-tsc/src/form/BaseListEditor.js.map +1 -1
- package/out-tsc/src/form/FormField.js +1 -1
- package/out-tsc/src/form/FormField.js.map +1 -1
- package/out-tsc/src/form/KeyValueEditor.js +1 -1
- package/out-tsc/src/form/KeyValueEditor.js.map +1 -1
- package/out-tsc/src/form/MediaPicker.js +13 -1
- package/out-tsc/src/form/MediaPicker.js.map +1 -1
- package/out-tsc/src/form/MessageEditor.js +422 -0
- package/out-tsc/src/form/MessageEditor.js.map +1 -0
- package/out-tsc/src/form/TextInput.js +12 -5
- package/out-tsc/src/form/TextInput.js.map +1 -1
- package/out-tsc/src/form/select/Select.js +4 -4
- package/out-tsc/src/form/select/Select.js.map +1 -1
- package/out-tsc/src/live/ContactChat.js +29 -4
- package/out-tsc/src/live/ContactChat.js.map +1 -1
- package/out-tsc/temba-modules.js +2 -0
- package/out-tsc/temba-modules.js.map +1 -1
- package/out-tsc/test/temba-field-config.test.js +4 -2
- package/out-tsc/test/temba-field-config.test.js.map +1 -1
- package/out-tsc/test/temba-message-editor.test.js +194 -0
- package/out-tsc/test/temba-message-editor.test.js.map +1 -0
- package/out-tsc/test/temba-node-editor.test.js +71 -0
- package/out-tsc/test/temba-node-editor.test.js.map +1 -1
- package/out-tsc/test/temba-select.test.js +1 -1
- package/out-tsc/test/temba-select.test.js.map +1 -1
- package/out-tsc/test/temba-textinput.test.js +16 -0
- package/out-tsc/test/temba-textinput.test.js.map +1 -1
- package/out-tsc/test/temba-webchat.test.js +4 -0
- package/out-tsc/test/temba-webchat.test.js.map +1 -1
- package/out-tsc/test/utils.test.js +2 -8
- package/out-tsc/test/utils.test.js.map +1 -1
- package/package.json +7 -4
- package/screenshots/truth/actions/add_contact_groups/editor/descriptive-group-names.png +0 -0
- package/screenshots/truth/actions/add_contact_groups/editor/long-group-names.png +0 -0
- package/screenshots/truth/actions/add_contact_groups/editor/many-groups.png +0 -0
- package/screenshots/truth/actions/add_contact_groups/editor/multiple-groups.png +0 -0
- package/screenshots/truth/actions/add_contact_groups/editor/single-group.png +0 -0
- package/screenshots/truth/actions/add_contact_groups/render/descriptive-group-names.png +0 -0
- package/screenshots/truth/actions/add_contact_groups/render/long-group-names.png +0 -0
- package/screenshots/truth/actions/add_contact_groups/render/many-groups.png +0 -0
- package/screenshots/truth/actions/add_contact_groups/render/multiple-groups.png +0 -0
- package/screenshots/truth/actions/add_contact_groups/render/single-group.png +0 -0
- package/screenshots/truth/actions/remove_contact_groups/editor/cleanup-groups.png +0 -0
- package/screenshots/truth/actions/remove_contact_groups/editor/long-descriptive-group-names.png +0 -0
- package/screenshots/truth/actions/remove_contact_groups/editor/many-groups.png +0 -0
- package/screenshots/truth/actions/remove_contact_groups/editor/multiple-groups.png +0 -0
- package/screenshots/truth/actions/remove_contact_groups/editor/single-group.png +0 -0
- package/screenshots/truth/actions/remove_contact_groups/render/cleanup-groups.png +0 -0
- package/screenshots/truth/actions/remove_contact_groups/render/long-descriptive-group-names.png +0 -0
- package/screenshots/truth/actions/remove_contact_groups/render/many-groups.png +0 -0
- package/screenshots/truth/actions/remove_contact_groups/render/multiple-groups.png +0 -0
- package/screenshots/truth/actions/remove_contact_groups/render/remove-from-all-groups.png +0 -0
- package/screenshots/truth/actions/remove_contact_groups/render/single-group.png +0 -0
- package/screenshots/truth/actions/send_email/editor/complex-business-email.png +0 -0
- package/screenshots/truth/actions/send_email/editor/empty-body.png +0 -0
- package/screenshots/truth/actions/send_email/editor/empty-subject.png +0 -0
- package/screenshots/truth/actions/send_email/editor/long-subject.png +0 -0
- package/screenshots/truth/actions/send_email/editor/multiline-body.png +0 -0
- package/screenshots/truth/actions/send_email/editor/multiple-recipients.png +0 -0
- package/screenshots/truth/actions/send_email/editor/simple-email.png +0 -0
- package/screenshots/truth/actions/send_email/editor/with-expressions.png +0 -0
- package/screenshots/truth/actions/send_email/render/complex-business-email.png +0 -0
- package/screenshots/truth/actions/send_email/render/empty-body.png +0 -0
- package/screenshots/truth/actions/send_email/render/empty-subject.png +0 -0
- package/screenshots/truth/actions/send_email/render/long-subject.png +0 -0
- package/screenshots/truth/actions/send_email/render/multiline-body.png +0 -0
- package/screenshots/truth/actions/send_email/render/multiple-recipients.png +0 -0
- package/screenshots/truth/actions/send_email/render/simple-email.png +0 -0
- package/screenshots/truth/actions/send_email/render/with-expressions.png +0 -0
- package/screenshots/truth/actions/send_msg/editor/long-quick-replies.png +0 -0
- package/screenshots/truth/actions/send_msg/editor/multiline-text-with-replies.png +0 -0
- package/screenshots/truth/actions/send_msg/editor/simple-text.png +0 -0
- package/screenshots/truth/actions/send_msg/editor/text-with-linebreaks.png +0 -0
- package/screenshots/truth/actions/send_msg/editor/text-with-many-quick-replies.png +0 -0
- package/screenshots/truth/actions/send_msg/editor/text-with-quick-replies.png +0 -0
- package/screenshots/truth/actions/send_msg/editor/text-without-quick-replies.png +0 -0
- package/screenshots/truth/actions/send_msg/render/long-quick-replies.png +0 -0
- package/screenshots/truth/actions/send_msg/render/multiline-text-with-replies.png +0 -0
- package/screenshots/truth/actions/send_msg/render/simple-text.png +0 -0
- package/screenshots/truth/actions/send_msg/render/text-with-linebreaks.png +0 -0
- package/screenshots/truth/actions/send_msg/render/text-with-many-quick-replies.png +0 -0
- package/screenshots/truth/actions/send_msg/render/text-with-quick-replies.png +0 -0
- package/screenshots/truth/actions/send_msg/render/text-without-quick-replies.png +0 -0
- package/screenshots/truth/editor/send_msg.png +0 -0
- package/screenshots/truth/editor/set_contact_language.png +0 -0
- package/screenshots/truth/editor/set_contact_name.png +0 -0
- package/screenshots/truth/editor/set_run_result.png +0 -0
- package/screenshots/truth/formfield/markdown-errors.png +0 -0
- package/screenshots/truth/formfield/no-errors.png +0 -0
- package/screenshots/truth/formfield/plain-text-errors.png +0 -0
- package/screenshots/truth/message-editor/autogrow-initial-content.png +0 -0
- package/screenshots/truth/message-editor/default.png +0 -0
- package/screenshots/truth/message-editor/drag-highlight.png +0 -0
- package/screenshots/truth/message-editor/filtered-attachments.png +0 -0
- package/screenshots/truth/message-editor/with-completion.png +0 -0
- package/screenshots/truth/message-editor/with-properties.png +0 -0
- package/screenshots/truth/sticky-note/blue-color.png +0 -0
- package/screenshots/truth/sticky-note/blue.png +0 -0
- package/screenshots/truth/sticky-note/color-picker-expanded.png +0 -0
- package/screenshots/truth/sticky-note/default.png +0 -0
- package/screenshots/truth/sticky-note/gray-color.png +0 -0
- package/screenshots/truth/sticky-note/gray.png +0 -0
- package/screenshots/truth/sticky-note/green-color.png +0 -0
- package/screenshots/truth/sticky-note/green.png +0 -0
- package/screenshots/truth/sticky-note/pink-color.png +0 -0
- package/screenshots/truth/sticky-note/pink.png +0 -0
- package/screenshots/truth/sticky-note/yellow-color.png +0 -0
- package/screenshots/truth/sticky-note/yellow.png +0 -0
- package/screenshots/truth/textinput/autogrow-initial.png +0 -0
- package/screenshots/truth/textinput/input-form.png +0 -0
- package/src/display/Thumbnail.ts +2 -1
- package/src/events.ts +6 -2
- package/src/flow/CanvasNode.ts +10 -2
- package/src/flow/NodeEditor.ts +269 -23
- package/src/flow/StickyNote.ts +1 -1
- package/src/flow/actions/call_webhook.ts +28 -18
- package/src/flow/actions/send_email.ts +1 -2
- package/src/flow/actions/send_msg.ts +178 -7
- package/src/flow/types.ts +21 -2
- package/src/form/ArrayEditor.ts +120 -42
- package/src/form/BaseListEditor.ts +22 -6
- package/src/form/FormField.ts +1 -1
- package/src/form/KeyValueEditor.ts +1 -1
- package/src/form/MediaPicker.ts +13 -1
- package/src/form/MessageEditor.ts +449 -0
- package/src/form/TextInput.ts +15 -7
- package/src/form/select/Select.ts +4 -4
- package/src/live/ContactChat.ts +32 -6
- package/src/store/flow-definition.d.ts +25 -4
- package/static/css/temba-components.css +2 -0
- package/static/mr/docs/en-us/editor.json +2588 -0
- package/stress-test.js +138 -0
- package/temba-modules.ts +2 -0
- package/test/temba-field-config.test.ts +4 -2
- package/test/temba-message-editor.test.ts +300 -0
- package/test/temba-node-editor.test.ts +94 -0
- package/test/temba-select.test.ts +1 -1
- package/test/temba-textinput.test.ts +26 -0
- package/test/temba-webchat.test.ts +5 -0
- package/test/utils.test.ts +2 -13
- package/test-assets/contacts/history.json +20 -2
- package/test-assets/style.css +2 -0
- package/web-dev-mock.mjs +433 -0
- package/web-dev-server.config.mjs +71 -6
- package/web-test-runner.config.mjs +9 -4
package/test-assets/style.css
CHANGED
package/web-dev-mock.mjs
ADDED
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
import { Client as MinioClient } from 'minio';
|
|
2
|
+
import busboy from 'busboy';
|
|
3
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Generates FlowInfo dynamically from a FlowDefinition
|
|
7
|
+
* This is a plain JavaScript version that can be imported by the web-dev-server
|
|
8
|
+
*/
|
|
9
|
+
export function generateFlowInfo(definition) {
|
|
10
|
+
const dependencies = [];
|
|
11
|
+
const results = [];
|
|
12
|
+
const locals = [];
|
|
13
|
+
|
|
14
|
+
// Track unique dependencies by key/uuid to avoid duplicates
|
|
15
|
+
const dependencyMap = new Map();
|
|
16
|
+
// Track results by name to collect node_uuids
|
|
17
|
+
const resultMap = new Map();
|
|
18
|
+
|
|
19
|
+
// Process all nodes
|
|
20
|
+
definition.nodes.forEach((node) => {
|
|
21
|
+
// Process actions
|
|
22
|
+
node.actions.forEach((action) => {
|
|
23
|
+
extractDependenciesFromAction(action, dependencyMap, resultMap, node.uuid);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// Process router
|
|
27
|
+
if (node.router) {
|
|
28
|
+
extractDependenciesFromRouter(
|
|
29
|
+
node.router,
|
|
30
|
+
dependencyMap,
|
|
31
|
+
resultMap,
|
|
32
|
+
node.uuid
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// Extract unique dependencies from map
|
|
38
|
+
dependencies.push(...Array.from(dependencyMap.values()));
|
|
39
|
+
|
|
40
|
+
// Extract results from map
|
|
41
|
+
results.push(...Array.from(resultMap.values()));
|
|
42
|
+
|
|
43
|
+
// Count languages from localization
|
|
44
|
+
const languageCount = Object.keys(definition.localization || {}).length;
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
results,
|
|
48
|
+
dependencies,
|
|
49
|
+
counts: {
|
|
50
|
+
nodes: definition.nodes.length,
|
|
51
|
+
languages: languageCount
|
|
52
|
+
},
|
|
53
|
+
locals
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function extractDependenciesFromAction(action, dependencyMap, resultMap, nodeUuid) {
|
|
58
|
+
switch (action.type) {
|
|
59
|
+
case 'set_contact_field':
|
|
60
|
+
if (action.field?.name) {
|
|
61
|
+
const key = `field:${action.field.name}`;
|
|
62
|
+
dependencyMap.set(key, {
|
|
63
|
+
uuid: action.field.uuid || '',
|
|
64
|
+
name: action.field.name,
|
|
65
|
+
type: 'field'
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
break;
|
|
69
|
+
|
|
70
|
+
case 'call_webhook':
|
|
71
|
+
if (action.url) {
|
|
72
|
+
const key = `webhook:${action.url}`;
|
|
73
|
+
dependencyMap.set(key, {
|
|
74
|
+
uuid: action.uuid,
|
|
75
|
+
name: action.url,
|
|
76
|
+
type: 'webhook'
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
break;
|
|
80
|
+
|
|
81
|
+
case 'add_contact_groups':
|
|
82
|
+
action.groups?.forEach((group) => {
|
|
83
|
+
if (group.name) {
|
|
84
|
+
const key = `group:${group.uuid || group.name}`;
|
|
85
|
+
dependencyMap.set(key, {
|
|
86
|
+
uuid: group.uuid || '',
|
|
87
|
+
name: group.name,
|
|
88
|
+
type: 'group'
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
break;
|
|
93
|
+
|
|
94
|
+
case 'remove_contact_groups':
|
|
95
|
+
action.groups?.forEach((group) => {
|
|
96
|
+
if (group.name) {
|
|
97
|
+
const key = `group:${group.uuid || group.name}`;
|
|
98
|
+
dependencyMap.set(key, {
|
|
99
|
+
uuid: group.uuid || '',
|
|
100
|
+
name: group.name,
|
|
101
|
+
type: 'group'
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
break;
|
|
106
|
+
|
|
107
|
+
case 'set_contact_channel':
|
|
108
|
+
if (action.channel?.name) {
|
|
109
|
+
const key = `channel:${action.channel.uuid}`;
|
|
110
|
+
dependencyMap.set(key, {
|
|
111
|
+
uuid: action.channel.uuid,
|
|
112
|
+
name: action.channel.name,
|
|
113
|
+
type: 'channel'
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
break;
|
|
117
|
+
|
|
118
|
+
case 'send_broadcast':
|
|
119
|
+
action.groups?.forEach((group) => {
|
|
120
|
+
if (group.name) {
|
|
121
|
+
const key = `group:${group.uuid || group.name}`;
|
|
122
|
+
dependencyMap.set(key, {
|
|
123
|
+
uuid: group.uuid || '',
|
|
124
|
+
name: group.name,
|
|
125
|
+
type: 'group'
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
action.contacts?.forEach((contact) => {
|
|
130
|
+
if (contact.name) {
|
|
131
|
+
const key = `contact:${contact.uuid}`;
|
|
132
|
+
dependencyMap.set(key, {
|
|
133
|
+
uuid: contact.uuid,
|
|
134
|
+
name: contact.name,
|
|
135
|
+
type: 'contact'
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
break;
|
|
140
|
+
|
|
141
|
+
case 'enter_flow':
|
|
142
|
+
if (action.flow?.name) {
|
|
143
|
+
const key = `flow:${action.flow.uuid}`;
|
|
144
|
+
dependencyMap.set(key, {
|
|
145
|
+
uuid: action.flow.uuid,
|
|
146
|
+
name: action.flow.name,
|
|
147
|
+
type: 'flow'
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
break;
|
|
151
|
+
|
|
152
|
+
case 'start_session':
|
|
153
|
+
if (action.flow?.name) {
|
|
154
|
+
const key = `flow:${action.flow.uuid}`;
|
|
155
|
+
dependencyMap.set(key, {
|
|
156
|
+
uuid: action.flow.uuid,
|
|
157
|
+
name: action.flow.name,
|
|
158
|
+
type: 'flow'
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
action.groups?.forEach((group) => {
|
|
162
|
+
if (group.name) {
|
|
163
|
+
const key = `group:${group.uuid || group.name}`;
|
|
164
|
+
dependencyMap.set(key, {
|
|
165
|
+
uuid: group.uuid || '',
|
|
166
|
+
name: group.name,
|
|
167
|
+
type: 'group'
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
break;
|
|
172
|
+
|
|
173
|
+
case 'call_classifier':
|
|
174
|
+
if (action.classifier?.name) {
|
|
175
|
+
const key = `classifier:${action.classifier.uuid}`;
|
|
176
|
+
dependencyMap.set(key, {
|
|
177
|
+
uuid: action.classifier.uuid,
|
|
178
|
+
name: action.classifier.name,
|
|
179
|
+
type: 'classifier'
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
break;
|
|
183
|
+
|
|
184
|
+
case 'call_resthook':
|
|
185
|
+
if (action.resthook) {
|
|
186
|
+
const key = `resthook:${action.resthook}`;
|
|
187
|
+
dependencyMap.set(key, {
|
|
188
|
+
uuid: action.uuid,
|
|
189
|
+
name: action.resthook,
|
|
190
|
+
type: 'resthook'
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
break;
|
|
194
|
+
|
|
195
|
+
case 'call_llm':
|
|
196
|
+
if (action.llm?.name) {
|
|
197
|
+
const key = `llm:${action.llm.uuid}`;
|
|
198
|
+
dependencyMap.set(key, {
|
|
199
|
+
uuid: action.llm.uuid,
|
|
200
|
+
name: action.llm.name,
|
|
201
|
+
type: 'llm'
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
break;
|
|
205
|
+
|
|
206
|
+
case 'open_ticket':
|
|
207
|
+
if (action.assignee?.name) {
|
|
208
|
+
const key = `user:${action.assignee.uuid}`;
|
|
209
|
+
dependencyMap.set(key, {
|
|
210
|
+
uuid: action.assignee.uuid,
|
|
211
|
+
name: action.assignee.name,
|
|
212
|
+
type: 'user'
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
if (action.topic?.name) {
|
|
216
|
+
const key = `topic:${action.topic.uuid}`;
|
|
217
|
+
dependencyMap.set(key, {
|
|
218
|
+
uuid: action.topic.uuid,
|
|
219
|
+
name: action.topic.name,
|
|
220
|
+
type: 'topic'
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
break;
|
|
224
|
+
|
|
225
|
+
case 'request_optin':
|
|
226
|
+
if (action.optin?.name) {
|
|
227
|
+
const key = `optin:${action.optin.uuid}`;
|
|
228
|
+
dependencyMap.set(key, {
|
|
229
|
+
uuid: action.optin.uuid,
|
|
230
|
+
name: action.optin.name,
|
|
231
|
+
type: 'optin'
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
break;
|
|
235
|
+
|
|
236
|
+
case 'add_input_labels':
|
|
237
|
+
action.labels?.forEach((label) => {
|
|
238
|
+
if (label.name) {
|
|
239
|
+
const key = `label:${label.uuid}`;
|
|
240
|
+
dependencyMap.set(key, {
|
|
241
|
+
uuid: label.uuid,
|
|
242
|
+
name: label.name,
|
|
243
|
+
type: 'label'
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
break;
|
|
248
|
+
|
|
249
|
+
case 'send_msg':
|
|
250
|
+
if (action.template?.name) {
|
|
251
|
+
const key = `template:${action.template.uuid}`;
|
|
252
|
+
dependencyMap.set(key, {
|
|
253
|
+
uuid: action.template.uuid,
|
|
254
|
+
name: action.template.name,
|
|
255
|
+
type: 'template'
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
break;
|
|
259
|
+
|
|
260
|
+
case 'set_run_result':
|
|
261
|
+
if (action.name) {
|
|
262
|
+
const existingResult = resultMap.get(action.name);
|
|
263
|
+
if (existingResult) {
|
|
264
|
+
// Add this node to existing result if not already present
|
|
265
|
+
if (!existingResult.node_uuids.includes(nodeUuid)) {
|
|
266
|
+
existingResult.node_uuids.push(nodeUuid);
|
|
267
|
+
}
|
|
268
|
+
// Add category if specified and not already present
|
|
269
|
+
if (action.category && !existingResult.categories.includes(action.category)) {
|
|
270
|
+
existingResult.categories.push(action.category);
|
|
271
|
+
}
|
|
272
|
+
} else {
|
|
273
|
+
// Create new result
|
|
274
|
+
resultMap.set(action.name, {
|
|
275
|
+
key: action.name.toLowerCase(),
|
|
276
|
+
name: action.name,
|
|
277
|
+
categories: action.category ? [action.category] : [],
|
|
278
|
+
node_uuids: [nodeUuid]
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
break;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function extractDependenciesFromRouter(router, dependencyMap, resultMap, nodeUuid) {
|
|
287
|
+
// Extract result information
|
|
288
|
+
if (router.result_name && router.categories) {
|
|
289
|
+
const existingResult = resultMap.get(router.result_name);
|
|
290
|
+
if (existingResult) {
|
|
291
|
+
// Add this node to existing result
|
|
292
|
+
existingResult.node_uuids.push(nodeUuid);
|
|
293
|
+
} else {
|
|
294
|
+
// Create new result
|
|
295
|
+
const result = {
|
|
296
|
+
key: router.result_name,
|
|
297
|
+
name: router.result_name,
|
|
298
|
+
categories: router.categories.map((cat) => cat.name),
|
|
299
|
+
node_uuids: [nodeUuid]
|
|
300
|
+
};
|
|
301
|
+
resultMap.set(router.result_name, result);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Process cases for potential dependencies
|
|
306
|
+
router.cases?.forEach((case_) => {
|
|
307
|
+
if (case_.type === 'has_group' && case_.arguments?.length >= 2) {
|
|
308
|
+
// Group dependency from split_by_groups
|
|
309
|
+
const groupUuid = case_.arguments[0];
|
|
310
|
+
const groupName = case_.arguments[1];
|
|
311
|
+
if (groupName) {
|
|
312
|
+
const key = `group:${groupUuid}`;
|
|
313
|
+
dependencyMap.set(key, {
|
|
314
|
+
uuid: groupUuid,
|
|
315
|
+
name: groupName,
|
|
316
|
+
type: 'group'
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Initialize Minio client for file uploads
|
|
324
|
+
export const minioClient = new MinioClient({
|
|
325
|
+
endPoint: 'minio',
|
|
326
|
+
port: 9000,
|
|
327
|
+
useSSL: false,
|
|
328
|
+
accessKey: 'root',
|
|
329
|
+
secretKey: 'tembatemba'
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
// Helper function to generate the correct public URL for uploaded files
|
|
333
|
+
export function getPublicUrl(bucketName, fileName, request) {
|
|
334
|
+
// Check if request is coming from localhost/127.0.0.1 (host machine)
|
|
335
|
+
// or from within docker network
|
|
336
|
+
const host = request.headers.host;
|
|
337
|
+
const userAgent = request.headers['user-agent'] || '';
|
|
338
|
+
|
|
339
|
+
// If accessing from host machine (localhost:3010), use localhost for minio too
|
|
340
|
+
if (host && host.startsWith('localhost:')) {
|
|
341
|
+
return `http://localhost:9000/${bucketName}/${fileName}`;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// If accessing from docker network, use internal hostname
|
|
345
|
+
return `http://minio:9000/${bucketName}/${fileName}`;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Handle minio file uploads for media
|
|
349
|
+
export function handleMinioUpload(context) {
|
|
350
|
+
return new Promise((resolve) => {
|
|
351
|
+
try {
|
|
352
|
+
const bb = busboy({ headers: context.request.headers });
|
|
353
|
+
let fileInfo = null;
|
|
354
|
+
let fileBuffer = null;
|
|
355
|
+
|
|
356
|
+
bb.on('file', (name, file, info) => {
|
|
357
|
+
fileInfo = info;
|
|
358
|
+
const chunks = [];
|
|
359
|
+
|
|
360
|
+
file.on('data', (chunk) => {
|
|
361
|
+
chunks.push(chunk);
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
file.on('end', () => {
|
|
365
|
+
fileBuffer = Buffer.concat(chunks);
|
|
366
|
+
});
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
bb.on('finish', async () => {
|
|
370
|
+
if (!fileBuffer || !fileInfo) {
|
|
371
|
+
context.status = 400;
|
|
372
|
+
context.body = JSON.stringify({ error: 'No file uploaded' });
|
|
373
|
+
resolve();
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
try {
|
|
378
|
+
const fileUuid = uuidv4();
|
|
379
|
+
const fileName = `${fileUuid}-${fileInfo.filename}`;
|
|
380
|
+
const bucketName = 'temba-attachments';
|
|
381
|
+
|
|
382
|
+
// Upload to minio
|
|
383
|
+
await minioClient.putObject(bucketName, fileName, fileBuffer, {
|
|
384
|
+
'Content-Type': fileInfo.mimeType
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
// Return success response with appropriate URL based on request source
|
|
388
|
+
const publicUrl = getPublicUrl(bucketName, fileName, context.request);
|
|
389
|
+
|
|
390
|
+
// Debug logging
|
|
391
|
+
console.log('🔧 Upload Debug:', {
|
|
392
|
+
fileUuid,
|
|
393
|
+
fileName,
|
|
394
|
+
bucketName,
|
|
395
|
+
publicUrl,
|
|
396
|
+
contentType: fileInfo.mimeType,
|
|
397
|
+
host: context.request.headers.host
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
context.contentType = 'application/json';
|
|
401
|
+
context.body = JSON.stringify({
|
|
402
|
+
uuid: fileUuid,
|
|
403
|
+
content_type: fileInfo.mimeType,
|
|
404
|
+
url: publicUrl,
|
|
405
|
+
filename: fileInfo.filename,
|
|
406
|
+
size: fileBuffer.length
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
} catch (uploadError) {
|
|
410
|
+
console.error('Minio upload error:', uploadError);
|
|
411
|
+
context.status = 500;
|
|
412
|
+
context.body = JSON.stringify({
|
|
413
|
+
error: 'Upload failed',
|
|
414
|
+
details: uploadError.message
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
resolve();
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
context.req.pipe(bb);
|
|
422
|
+
|
|
423
|
+
} catch (error) {
|
|
424
|
+
console.error('File upload processing error:', error);
|
|
425
|
+
context.status = 500;
|
|
426
|
+
context.body = JSON.stringify({
|
|
427
|
+
error: 'Upload processing failed',
|
|
428
|
+
details: error.message
|
|
429
|
+
});
|
|
430
|
+
resolve();
|
|
431
|
+
}
|
|
432
|
+
});
|
|
433
|
+
}
|
|
@@ -3,14 +3,26 @@ import { fromRollup } from '@web/dev-server-rollup';
|
|
|
3
3
|
import fs from 'fs';
|
|
4
4
|
import path from 'path';
|
|
5
5
|
|
|
6
|
+
// Import the shared flow info generator and Minio functionality
|
|
7
|
+
import { generateFlowInfo, handleMinioUpload } from './web-dev-mock.mjs';
|
|
8
|
+
|
|
6
9
|
const replacePlugin = fromRollup(replace);
|
|
7
10
|
|
|
11
|
+
// Simple wrapper function to use the shared flow info generator
|
|
12
|
+
function generateFlowMetadata(flowDefinition) {
|
|
13
|
+
return generateFlowInfo(flowDefinition);
|
|
14
|
+
}
|
|
15
|
+
|
|
8
16
|
export default {
|
|
9
17
|
nodeResolve: true,
|
|
10
18
|
plugins: [
|
|
11
19
|
replacePlugin({
|
|
12
20
|
preventAssignment: true,
|
|
13
21
|
'process.env.NODE_ENV': JSON.stringify('development'),
|
|
22
|
+
'process.env.MINIO_ENDPOINT': JSON.stringify('http://minio:9000'),
|
|
23
|
+
'process.env.MINIO_PUBLIC_ENDPOINT': JSON.stringify('http://localhost:9000'),
|
|
24
|
+
'process.env.MINIO_ACCESS_KEY': JSON.stringify('root'),
|
|
25
|
+
'process.env.MINIO_SECRET_KEY': JSON.stringify('tembatemba'),
|
|
14
26
|
}),
|
|
15
27
|
{
|
|
16
28
|
name: 'api-mock-server',
|
|
@@ -22,7 +34,7 @@ export default {
|
|
|
22
34
|
'/api/v2/groups.json': './static/api/groups.json',
|
|
23
35
|
'/api/v2/fields.json': './static/api/fields.json',
|
|
24
36
|
'/api/v2/globals.json': './static/api/globals.json',
|
|
25
|
-
'/api/v2/completion.json': './static/
|
|
37
|
+
'/api/v2/completion.json': './static/mr/docs/en-us/editor.json',
|
|
26
38
|
'/api/v2/functions.json': './static/api/functions.json',
|
|
27
39
|
'/api/internal/templates.json': './static/api/templates.json',
|
|
28
40
|
'/api/v2/media.json': './static/api/media.json',
|
|
@@ -44,6 +56,31 @@ export default {
|
|
|
44
56
|
return;
|
|
45
57
|
}
|
|
46
58
|
}
|
|
59
|
+
|
|
60
|
+
// Handle minio file uploads for media
|
|
61
|
+
if (context.request.method === 'POST' && context.path === '/api/v2/media.json') {
|
|
62
|
+
return handleMinioUpload(context);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
name: 'flows-directory-listing',
|
|
68
|
+
serve(context) {
|
|
69
|
+
// Handle directory listing for flows using a special API endpoint
|
|
70
|
+
|
|
71
|
+
if (context.request.method === 'GET' && context.path === '/api/flows-list') {
|
|
72
|
+
|
|
73
|
+
const flowsDir = path.resolve('./demo/data/flows');
|
|
74
|
+
|
|
75
|
+
if (fs.existsSync(flowsDir)) {
|
|
76
|
+
const files = fs.readdirSync(flowsDir).filter(file => file.endsWith('.json'));
|
|
77
|
+
|
|
78
|
+
// Return JSON array of filenames
|
|
79
|
+
context.contentType = 'application/json';
|
|
80
|
+
context.body = JSON.stringify(files);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
47
84
|
}
|
|
48
85
|
},
|
|
49
86
|
{
|
|
@@ -60,15 +97,31 @@ export default {
|
|
|
60
97
|
context.req.on('end', () => {
|
|
61
98
|
context.contentType = 'application/json';
|
|
62
99
|
if (body) {
|
|
100
|
+
const flowDefinition = JSON.parse(body);
|
|
63
101
|
fs.writeFileSync(
|
|
64
102
|
path.resolve(`./demo/data/flows/${uuid}.json`),
|
|
65
|
-
JSON.stringify({ definition:
|
|
103
|
+
JSON.stringify({ definition: flowDefinition }, null, 2)
|
|
66
104
|
);
|
|
67
|
-
|
|
105
|
+
|
|
106
|
+
// Generate metadata similar to production
|
|
107
|
+
const metadata = generateFlowMetadata(flowDefinition);
|
|
108
|
+
|
|
68
109
|
context.body = {
|
|
69
110
|
status: 'success',
|
|
70
|
-
|
|
71
|
-
|
|
111
|
+
saved_on: new Date().toISOString(),
|
|
112
|
+
revision: {
|
|
113
|
+
id: Math.floor(Math.random() * 1000) + 1,
|
|
114
|
+
user: {
|
|
115
|
+
email: 'test@textit.com',
|
|
116
|
+
name: 'Test User'
|
|
117
|
+
},
|
|
118
|
+
created_on: new Date().toISOString(),
|
|
119
|
+
version: flowDefinition.spec_version || '14.3.0',
|
|
120
|
+
revision: flowDefinition.revision || 1
|
|
121
|
+
},
|
|
122
|
+
info: metadata,
|
|
123
|
+
issues: [],
|
|
124
|
+
metadata: metadata
|
|
72
125
|
};
|
|
73
126
|
context.status = 200;
|
|
74
127
|
} else {
|
|
@@ -88,10 +141,22 @@ export default {
|
|
|
88
141
|
const parts = context.path.split('/');
|
|
89
142
|
const uuid = parts[3];
|
|
90
143
|
context.contentType = 'application/json';
|
|
91
|
-
|
|
144
|
+
|
|
145
|
+
// Read the flow definition from file
|
|
146
|
+
const flowFileContent = fs.readFileSync(
|
|
92
147
|
path.resolve(`./demo/data/flows/${uuid}.json`),
|
|
93
148
|
'utf-8',
|
|
94
149
|
);
|
|
150
|
+
|
|
151
|
+
const flowData = JSON.parse(flowFileContent);
|
|
152
|
+
|
|
153
|
+
if (flowData.definition) {
|
|
154
|
+
const info = generateFlowMetadata(flowData.definition);
|
|
155
|
+
context.body = JSON.stringify({
|
|
156
|
+
definition: flowData.definition,
|
|
157
|
+
info: info
|
|
158
|
+
});
|
|
159
|
+
}
|
|
95
160
|
}
|
|
96
161
|
}
|
|
97
162
|
}
|
|
@@ -13,6 +13,7 @@ import rimraf from 'rimraf';
|
|
|
13
13
|
|
|
14
14
|
import replace from '@rollup/plugin-replace';
|
|
15
15
|
import { fromRollup } from '@web/dev-server-rollup';
|
|
16
|
+
|
|
16
17
|
const replacePlugin = fromRollup(replace);
|
|
17
18
|
|
|
18
19
|
const SCREENSHOTS = 'screenshots';
|
|
@@ -151,10 +152,14 @@ const wireScreenshots = async (page, context, wait, replaceScreenshots) => {
|
|
|
151
152
|
const truthFile = await getPath(TRUTH, filename);
|
|
152
153
|
|
|
153
154
|
// Only wait for network idle if explicitly requested
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
155
|
+
try{
|
|
156
|
+
if (wait) {
|
|
157
|
+
await page.waitForNetworkIdle({idleTime: 500, timeout: 2000});
|
|
158
|
+
} else {
|
|
159
|
+
await page.waitForNetworkIdle({ idleTime: 100, timeout: 1000 });
|
|
160
|
+
}
|
|
161
|
+
} catch (error) {
|
|
162
|
+
console.error('Error waiting for network idle, proceeding: ' + filename);
|
|
158
163
|
}
|
|
159
164
|
|
|
160
165
|
if (!(await fileExists(truthFile))) {
|