@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.
Files changed (177) hide show
  1. package/.devcontainer/Dockerfile +11 -4
  2. package/.devcontainer/devcontainer.json +3 -2
  3. package/.github/workflows/build.yml +4 -14
  4. package/CHANGELOG.md +25 -1
  5. package/demo/components/flow/example.html +9 -2
  6. package/demo/components/flow/index.html +206 -0
  7. package/demo/components/message-editor/example.html +125 -0
  8. package/demo/components/textinput/completion.html +1 -0
  9. package/demo/data/flows/food-order.json +132 -0
  10. package/demo/data/flows/sample-flow.json +40 -24
  11. package/demo/index.html +1 -1
  12. package/dist/temba-components.js +518 -220
  13. package/dist/temba-components.js.map +1 -1
  14. package/out-tsc/src/display/Thumbnail.js +2 -1
  15. package/out-tsc/src/display/Thumbnail.js.map +1 -1
  16. package/out-tsc/src/events.js.map +1 -1
  17. package/out-tsc/src/flow/CanvasNode.js +10 -2
  18. package/out-tsc/src/flow/CanvasNode.js.map +1 -1
  19. package/out-tsc/src/flow/NodeEditor.js +245 -22
  20. package/out-tsc/src/flow/NodeEditor.js.map +1 -1
  21. package/out-tsc/src/flow/StickyNote.js +1 -1
  22. package/out-tsc/src/flow/StickyNote.js.map +1 -1
  23. package/out-tsc/src/flow/actions/call_webhook.js +26 -17
  24. package/out-tsc/src/flow/actions/call_webhook.js.map +1 -1
  25. package/out-tsc/src/flow/actions/send_email.js +1 -2
  26. package/out-tsc/src/flow/actions/send_email.js.map +1 -1
  27. package/out-tsc/src/flow/actions/send_msg.js +155 -7
  28. package/out-tsc/src/flow/actions/send_msg.js.map +1 -1
  29. package/out-tsc/src/flow/types.js.map +1 -1
  30. package/out-tsc/src/form/ArrayEditor.js +111 -38
  31. package/out-tsc/src/form/ArrayEditor.js.map +1 -1
  32. package/out-tsc/src/form/BaseListEditor.js +19 -4
  33. package/out-tsc/src/form/BaseListEditor.js.map +1 -1
  34. package/out-tsc/src/form/FormField.js +1 -1
  35. package/out-tsc/src/form/FormField.js.map +1 -1
  36. package/out-tsc/src/form/KeyValueEditor.js +1 -1
  37. package/out-tsc/src/form/KeyValueEditor.js.map +1 -1
  38. package/out-tsc/src/form/MediaPicker.js +13 -1
  39. package/out-tsc/src/form/MediaPicker.js.map +1 -1
  40. package/out-tsc/src/form/MessageEditor.js +422 -0
  41. package/out-tsc/src/form/MessageEditor.js.map +1 -0
  42. package/out-tsc/src/form/TextInput.js +12 -5
  43. package/out-tsc/src/form/TextInput.js.map +1 -1
  44. package/out-tsc/src/form/select/Select.js +4 -4
  45. package/out-tsc/src/form/select/Select.js.map +1 -1
  46. package/out-tsc/src/live/ContactChat.js +29 -4
  47. package/out-tsc/src/live/ContactChat.js.map +1 -1
  48. package/out-tsc/temba-modules.js +2 -0
  49. package/out-tsc/temba-modules.js.map +1 -1
  50. package/out-tsc/test/temba-field-config.test.js +4 -2
  51. package/out-tsc/test/temba-field-config.test.js.map +1 -1
  52. package/out-tsc/test/temba-message-editor.test.js +194 -0
  53. package/out-tsc/test/temba-message-editor.test.js.map +1 -0
  54. package/out-tsc/test/temba-node-editor.test.js +71 -0
  55. package/out-tsc/test/temba-node-editor.test.js.map +1 -1
  56. package/out-tsc/test/temba-select.test.js +1 -1
  57. package/out-tsc/test/temba-select.test.js.map +1 -1
  58. package/out-tsc/test/temba-textinput.test.js +16 -0
  59. package/out-tsc/test/temba-textinput.test.js.map +1 -1
  60. package/out-tsc/test/temba-webchat.test.js +4 -0
  61. package/out-tsc/test/temba-webchat.test.js.map +1 -1
  62. package/out-tsc/test/utils.test.js +2 -8
  63. package/out-tsc/test/utils.test.js.map +1 -1
  64. package/package.json +7 -4
  65. package/screenshots/truth/actions/add_contact_groups/editor/descriptive-group-names.png +0 -0
  66. package/screenshots/truth/actions/add_contact_groups/editor/long-group-names.png +0 -0
  67. package/screenshots/truth/actions/add_contact_groups/editor/many-groups.png +0 -0
  68. package/screenshots/truth/actions/add_contact_groups/editor/multiple-groups.png +0 -0
  69. package/screenshots/truth/actions/add_contact_groups/editor/single-group.png +0 -0
  70. package/screenshots/truth/actions/add_contact_groups/render/descriptive-group-names.png +0 -0
  71. package/screenshots/truth/actions/add_contact_groups/render/long-group-names.png +0 -0
  72. package/screenshots/truth/actions/add_contact_groups/render/many-groups.png +0 -0
  73. package/screenshots/truth/actions/add_contact_groups/render/multiple-groups.png +0 -0
  74. package/screenshots/truth/actions/add_contact_groups/render/single-group.png +0 -0
  75. package/screenshots/truth/actions/remove_contact_groups/editor/cleanup-groups.png +0 -0
  76. package/screenshots/truth/actions/remove_contact_groups/editor/long-descriptive-group-names.png +0 -0
  77. package/screenshots/truth/actions/remove_contact_groups/editor/many-groups.png +0 -0
  78. package/screenshots/truth/actions/remove_contact_groups/editor/multiple-groups.png +0 -0
  79. package/screenshots/truth/actions/remove_contact_groups/editor/single-group.png +0 -0
  80. package/screenshots/truth/actions/remove_contact_groups/render/cleanup-groups.png +0 -0
  81. package/screenshots/truth/actions/remove_contact_groups/render/long-descriptive-group-names.png +0 -0
  82. package/screenshots/truth/actions/remove_contact_groups/render/many-groups.png +0 -0
  83. package/screenshots/truth/actions/remove_contact_groups/render/multiple-groups.png +0 -0
  84. package/screenshots/truth/actions/remove_contact_groups/render/remove-from-all-groups.png +0 -0
  85. package/screenshots/truth/actions/remove_contact_groups/render/single-group.png +0 -0
  86. package/screenshots/truth/actions/send_email/editor/complex-business-email.png +0 -0
  87. package/screenshots/truth/actions/send_email/editor/empty-body.png +0 -0
  88. package/screenshots/truth/actions/send_email/editor/empty-subject.png +0 -0
  89. package/screenshots/truth/actions/send_email/editor/long-subject.png +0 -0
  90. package/screenshots/truth/actions/send_email/editor/multiline-body.png +0 -0
  91. package/screenshots/truth/actions/send_email/editor/multiple-recipients.png +0 -0
  92. package/screenshots/truth/actions/send_email/editor/simple-email.png +0 -0
  93. package/screenshots/truth/actions/send_email/editor/with-expressions.png +0 -0
  94. package/screenshots/truth/actions/send_email/render/complex-business-email.png +0 -0
  95. package/screenshots/truth/actions/send_email/render/empty-body.png +0 -0
  96. package/screenshots/truth/actions/send_email/render/empty-subject.png +0 -0
  97. package/screenshots/truth/actions/send_email/render/long-subject.png +0 -0
  98. package/screenshots/truth/actions/send_email/render/multiline-body.png +0 -0
  99. package/screenshots/truth/actions/send_email/render/multiple-recipients.png +0 -0
  100. package/screenshots/truth/actions/send_email/render/simple-email.png +0 -0
  101. package/screenshots/truth/actions/send_email/render/with-expressions.png +0 -0
  102. package/screenshots/truth/actions/send_msg/editor/long-quick-replies.png +0 -0
  103. package/screenshots/truth/actions/send_msg/editor/multiline-text-with-replies.png +0 -0
  104. package/screenshots/truth/actions/send_msg/editor/simple-text.png +0 -0
  105. package/screenshots/truth/actions/send_msg/editor/text-with-linebreaks.png +0 -0
  106. package/screenshots/truth/actions/send_msg/editor/text-with-many-quick-replies.png +0 -0
  107. package/screenshots/truth/actions/send_msg/editor/text-with-quick-replies.png +0 -0
  108. package/screenshots/truth/actions/send_msg/editor/text-without-quick-replies.png +0 -0
  109. package/screenshots/truth/actions/send_msg/render/long-quick-replies.png +0 -0
  110. package/screenshots/truth/actions/send_msg/render/multiline-text-with-replies.png +0 -0
  111. package/screenshots/truth/actions/send_msg/render/simple-text.png +0 -0
  112. package/screenshots/truth/actions/send_msg/render/text-with-linebreaks.png +0 -0
  113. package/screenshots/truth/actions/send_msg/render/text-with-many-quick-replies.png +0 -0
  114. package/screenshots/truth/actions/send_msg/render/text-with-quick-replies.png +0 -0
  115. package/screenshots/truth/actions/send_msg/render/text-without-quick-replies.png +0 -0
  116. package/screenshots/truth/editor/send_msg.png +0 -0
  117. package/screenshots/truth/editor/set_contact_language.png +0 -0
  118. package/screenshots/truth/editor/set_contact_name.png +0 -0
  119. package/screenshots/truth/editor/set_run_result.png +0 -0
  120. package/screenshots/truth/formfield/markdown-errors.png +0 -0
  121. package/screenshots/truth/formfield/no-errors.png +0 -0
  122. package/screenshots/truth/formfield/plain-text-errors.png +0 -0
  123. package/screenshots/truth/message-editor/autogrow-initial-content.png +0 -0
  124. package/screenshots/truth/message-editor/default.png +0 -0
  125. package/screenshots/truth/message-editor/drag-highlight.png +0 -0
  126. package/screenshots/truth/message-editor/filtered-attachments.png +0 -0
  127. package/screenshots/truth/message-editor/with-completion.png +0 -0
  128. package/screenshots/truth/message-editor/with-properties.png +0 -0
  129. package/screenshots/truth/sticky-note/blue-color.png +0 -0
  130. package/screenshots/truth/sticky-note/blue.png +0 -0
  131. package/screenshots/truth/sticky-note/color-picker-expanded.png +0 -0
  132. package/screenshots/truth/sticky-note/default.png +0 -0
  133. package/screenshots/truth/sticky-note/gray-color.png +0 -0
  134. package/screenshots/truth/sticky-note/gray.png +0 -0
  135. package/screenshots/truth/sticky-note/green-color.png +0 -0
  136. package/screenshots/truth/sticky-note/green.png +0 -0
  137. package/screenshots/truth/sticky-note/pink-color.png +0 -0
  138. package/screenshots/truth/sticky-note/pink.png +0 -0
  139. package/screenshots/truth/sticky-note/yellow-color.png +0 -0
  140. package/screenshots/truth/sticky-note/yellow.png +0 -0
  141. package/screenshots/truth/textinput/autogrow-initial.png +0 -0
  142. package/screenshots/truth/textinput/input-form.png +0 -0
  143. package/src/display/Thumbnail.ts +2 -1
  144. package/src/events.ts +6 -2
  145. package/src/flow/CanvasNode.ts +10 -2
  146. package/src/flow/NodeEditor.ts +269 -23
  147. package/src/flow/StickyNote.ts +1 -1
  148. package/src/flow/actions/call_webhook.ts +28 -18
  149. package/src/flow/actions/send_email.ts +1 -2
  150. package/src/flow/actions/send_msg.ts +178 -7
  151. package/src/flow/types.ts +21 -2
  152. package/src/form/ArrayEditor.ts +120 -42
  153. package/src/form/BaseListEditor.ts +22 -6
  154. package/src/form/FormField.ts +1 -1
  155. package/src/form/KeyValueEditor.ts +1 -1
  156. package/src/form/MediaPicker.ts +13 -1
  157. package/src/form/MessageEditor.ts +449 -0
  158. package/src/form/TextInput.ts +15 -7
  159. package/src/form/select/Select.ts +4 -4
  160. package/src/live/ContactChat.ts +32 -6
  161. package/src/store/flow-definition.d.ts +25 -4
  162. package/static/css/temba-components.css +2 -0
  163. package/static/mr/docs/en-us/editor.json +2588 -0
  164. package/stress-test.js +138 -0
  165. package/temba-modules.ts +2 -0
  166. package/test/temba-field-config.test.ts +4 -2
  167. package/test/temba-message-editor.test.ts +300 -0
  168. package/test/temba-node-editor.test.ts +94 -0
  169. package/test/temba-select.test.ts +1 -1
  170. package/test/temba-textinput.test.ts +26 -0
  171. package/test/temba-webchat.test.ts +5 -0
  172. package/test/utils.test.ts +2 -13
  173. package/test-assets/contacts/history.json +20 -2
  174. package/test-assets/style.css +2 -0
  175. package/web-dev-mock.mjs +433 -0
  176. package/web-dev-server.config.mjs +71 -6
  177. package/web-test-runner.config.mjs +9 -4
@@ -60,6 +60,8 @@ html {
60
60
  --error-rgb: 255, 99, 71;
61
61
  --success-rgb: 102, 186, 104;
62
62
 
63
+ --color-label: #333;
64
+
63
65
  --selection-light-rgb: 240, 240, 240;
64
66
  --selection-dark-rgb: 180, 180, 180;
65
67
 
@@ -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/api/completion.json',
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: JSON.parse(body) }, null, 2)
103
+ JSON.stringify({ definition: flowDefinition }, null, 2)
66
104
  );
67
- // console.log(`Flow ${uuid} saved successfully.`);
105
+
106
+ // Generate metadata similar to production
107
+ const metadata = generateFlowMetadata(flowDefinition);
108
+
68
109
  context.body = {
69
110
  status: 'success',
70
- message: `Flow ${uuid} saved successfully.`,
71
- definition: JSON.parse(body),
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
- context.body = fs.readFileSync(
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
- if (wait) {
155
- await page.waitForNetworkIdle();
156
- } else {
157
- await page.waitForNetworkIdle({ idleTime: 100, timeout: 1000 });
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))) {