@nyaruka/temba-components 0.130.1 → 0.130.2

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 (246) hide show
  1. package/CHANGELOG.md +28 -4
  2. package/DEV_DATA.md +89 -0
  3. package/demo/data/flows/food-order.json +4 -4
  4. package/demo/data/flows/sample-flow.json +132 -147
  5. package/dist/temba-components.js +764 -628
  6. package/dist/temba-components.js.map +1 -1
  7. package/out-tsc/src/display/Chat.js +5 -3
  8. package/out-tsc/src/display/Chat.js.map +1 -1
  9. package/out-tsc/src/events.js.map +1 -1
  10. package/out-tsc/src/flow/CanvasNode.js +83 -78
  11. package/out-tsc/src/flow/CanvasNode.js.map +1 -1
  12. package/out-tsc/src/flow/Editor.js +1 -0
  13. package/out-tsc/src/flow/Editor.js.map +1 -1
  14. package/out-tsc/src/flow/NodeEditor.js +47 -3
  15. package/out-tsc/src/flow/NodeEditor.js.map +1 -1
  16. package/out-tsc/src/flow/actions/add_contact_urn.js +1 -1
  17. package/out-tsc/src/flow/actions/add_contact_urn.js.map +1 -1
  18. package/out-tsc/src/flow/actions/set_contact_channel.js +1 -1
  19. package/out-tsc/src/flow/actions/set_contact_channel.js.map +1 -1
  20. package/out-tsc/src/flow/actions/set_contact_field.js +2 -1
  21. package/out-tsc/src/flow/actions/set_contact_field.js.map +1 -1
  22. package/out-tsc/src/flow/actions/set_contact_language.js +3 -1
  23. package/out-tsc/src/flow/actions/set_contact_language.js.map +1 -1
  24. package/out-tsc/src/flow/actions/set_contact_name.js +1 -1
  25. package/out-tsc/src/flow/actions/set_contact_name.js.map +1 -1
  26. package/out-tsc/src/flow/actions/set_contact_status.js +17 -14
  27. package/out-tsc/src/flow/actions/set_contact_status.js.map +1 -1
  28. package/out-tsc/src/flow/actions/set_run_result.js +1 -1
  29. package/out-tsc/src/flow/actions/set_run_result.js.map +1 -1
  30. package/out-tsc/src/flow/nodes/split_by_llm.js +12 -12
  31. package/out-tsc/src/flow/nodes/split_by_llm.js.map +1 -1
  32. package/out-tsc/src/flow/nodes/wait_for_response.js +609 -6
  33. package/out-tsc/src/flow/nodes/wait_for_response.js.map +1 -1
  34. package/out-tsc/src/flow/operators.js +194 -0
  35. package/out-tsc/src/flow/operators.js.map +1 -0
  36. package/out-tsc/src/flow/types.js.map +1 -1
  37. package/out-tsc/src/form/ArrayEditor.js +84 -19
  38. package/out-tsc/src/form/ArrayEditor.js.map +1 -1
  39. package/out-tsc/src/form/Checkbox.js +12 -0
  40. package/out-tsc/src/form/Checkbox.js.map +1 -1
  41. package/out-tsc/src/form/FieldRenderer.js +13 -3
  42. package/out-tsc/src/form/FieldRenderer.js.map +1 -1
  43. package/out-tsc/src/form/TextInput.js +20 -1
  44. package/out-tsc/src/form/TextInput.js.map +1 -1
  45. package/out-tsc/src/form/select/Select.js +7 -0
  46. package/out-tsc/src/form/select/Select.js.map +1 -1
  47. package/out-tsc/src/interfaces.js.map +1 -1
  48. package/out-tsc/src/layout/Dialog.js +3 -4
  49. package/out-tsc/src/layout/Dialog.js.map +1 -1
  50. package/out-tsc/src/list/RunList.js +2 -2
  51. package/out-tsc/src/list/RunList.js.map +1 -1
  52. package/out-tsc/src/live/ContactChat.js +114 -34
  53. package/out-tsc/src/live/ContactChat.js.map +1 -1
  54. package/out-tsc/src/live/ContactDetails.js +7 -0
  55. package/out-tsc/src/live/ContactDetails.js.map +1 -1
  56. package/out-tsc/src/live/ContactNameFetch.js +1 -1
  57. package/out-tsc/src/live/ContactNameFetch.js.map +1 -1
  58. package/out-tsc/test/NodeHelper.js +25 -27
  59. package/out-tsc/test/NodeHelper.js.map +1 -1
  60. package/out-tsc/test/nodes/split_by_llm.test.js +12 -4
  61. package/out-tsc/test/nodes/split_by_llm.test.js.map +1 -1
  62. package/out-tsc/test/nodes/split_by_llm_categorize.test.js +101 -91
  63. package/out-tsc/test/nodes/split_by_llm_categorize.test.js.map +1 -1
  64. package/out-tsc/test/nodes/split_by_random.test.js +120 -112
  65. package/out-tsc/test/nodes/split_by_random.test.js.map +1 -1
  66. package/out-tsc/test/nodes/wait_for_digits.test.js +131 -111
  67. package/out-tsc/test/nodes/wait_for_digits.test.js.map +1 -1
  68. package/out-tsc/test/nodes/wait_for_response.test.js +549 -85
  69. package/out-tsc/test/nodes/wait_for_response.test.js.map +1 -1
  70. package/out-tsc/test/temba-checkbox.test.js +32 -32
  71. package/out-tsc/test/temba-checkbox.test.js.map +1 -1
  72. package/out-tsc/test/temba-contact-chat.test.js +2 -1
  73. package/out-tsc/test/temba-contact-chat.test.js.map +1 -1
  74. package/out-tsc/test/temba-dropdown.test.js +0 -4
  75. package/out-tsc/test/temba-dropdown.test.js.map +1 -1
  76. package/out-tsc/test/temba-flow-editor-node.test.js +9 -4
  77. package/out-tsc/test/temba-flow-editor-node.test.js.map +1 -1
  78. package/out-tsc/test/temba-integration-markdown.test.js +13 -15
  79. package/out-tsc/test/temba-integration-markdown.test.js.map +1 -1
  80. package/out-tsc/test/temba-node-editor.test.js +5 -38
  81. package/out-tsc/test/temba-node-editor.test.js.map +1 -1
  82. package/out-tsc/test/temba-run-list.test.js +2 -2
  83. package/out-tsc/test/temba-run-list.test.js.map +1 -1
  84. package/out-tsc/test/utils.test.js +2 -1
  85. package/out-tsc/test/utils.test.js.map +1 -1
  86. package/package.json +6 -2
  87. package/screenshots/truth/actions/add_contact_groups/editor/descriptive-group-names.png +0 -0
  88. package/screenshots/truth/actions/add_contact_groups/editor/long-group-names.png +0 -0
  89. package/screenshots/truth/actions/add_contact_groups/editor/many-groups.png +0 -0
  90. package/screenshots/truth/actions/add_contact_groups/editor/multiple-groups.png +0 -0
  91. package/screenshots/truth/actions/add_contact_groups/editor/single-group.png +0 -0
  92. package/screenshots/truth/actions/add_contact_groups/render/descriptive-group-names.png +0 -0
  93. package/screenshots/truth/actions/add_contact_groups/render/long-group-names.png +0 -0
  94. package/screenshots/truth/actions/add_contact_groups/render/many-groups.png +0 -0
  95. package/screenshots/truth/actions/add_contact_groups/render/multiple-groups.png +0 -0
  96. package/screenshots/truth/actions/add_contact_groups/render/single-group.png +0 -0
  97. package/screenshots/truth/actions/remove_contact_groups/editor/cleanup-groups.png +0 -0
  98. package/screenshots/truth/actions/remove_contact_groups/editor/long-descriptive-group-names.png +0 -0
  99. package/screenshots/truth/actions/remove_contact_groups/editor/many-groups.png +0 -0
  100. package/screenshots/truth/actions/remove_contact_groups/editor/multiple-groups.png +0 -0
  101. package/screenshots/truth/actions/remove_contact_groups/editor/remove-from-all-groups.png +0 -0
  102. package/screenshots/truth/actions/remove_contact_groups/editor/single-group.png +0 -0
  103. package/screenshots/truth/actions/remove_contact_groups/render/cleanup-groups.png +0 -0
  104. package/screenshots/truth/actions/remove_contact_groups/render/long-descriptive-group-names.png +0 -0
  105. package/screenshots/truth/actions/remove_contact_groups/render/many-groups.png +0 -0
  106. package/screenshots/truth/actions/remove_contact_groups/render/multiple-groups.png +0 -0
  107. package/screenshots/truth/actions/remove_contact_groups/render/remove-from-all-groups.png +0 -0
  108. package/screenshots/truth/actions/remove_contact_groups/render/single-group.png +0 -0
  109. package/screenshots/truth/actions/send_email/editor/complex-business-email.png +0 -0
  110. package/screenshots/truth/actions/send_email/editor/empty-body.png +0 -0
  111. package/screenshots/truth/actions/send_email/editor/empty-subject.png +0 -0
  112. package/screenshots/truth/actions/send_email/editor/long-subject.png +0 -0
  113. package/screenshots/truth/actions/send_email/editor/multiline-body.png +0 -0
  114. package/screenshots/truth/actions/send_email/editor/multiple-recipients.png +0 -0
  115. package/screenshots/truth/actions/send_email/editor/simple-email.png +0 -0
  116. package/screenshots/truth/actions/send_email/editor/with-expressions.png +0 -0
  117. package/screenshots/truth/actions/send_email/render/complex-business-email.png +0 -0
  118. package/screenshots/truth/actions/send_email/render/empty-body.png +0 -0
  119. package/screenshots/truth/actions/send_email/render/empty-subject.png +0 -0
  120. package/screenshots/truth/actions/send_email/render/long-subject.png +0 -0
  121. package/screenshots/truth/actions/send_email/render/multiline-body.png +0 -0
  122. package/screenshots/truth/actions/send_email/render/multiple-recipients.png +0 -0
  123. package/screenshots/truth/actions/send_email/render/simple-email.png +0 -0
  124. package/screenshots/truth/actions/send_email/render/with-expressions.png +0 -0
  125. package/screenshots/truth/actions/send_msg/editor/long-quick-replies.png +0 -0
  126. package/screenshots/truth/actions/send_msg/editor/multiline-text-with-replies.png +0 -0
  127. package/screenshots/truth/actions/send_msg/editor/simple-text.png +0 -0
  128. package/screenshots/truth/actions/send_msg/editor/text-with-linebreaks.png +0 -0
  129. package/screenshots/truth/actions/send_msg/editor/text-with-many-quick-replies.png +0 -0
  130. package/screenshots/truth/actions/send_msg/editor/text-with-quick-replies.png +0 -0
  131. package/screenshots/truth/actions/send_msg/editor/text-without-quick-replies.png +0 -0
  132. package/screenshots/truth/actions/send_msg/render/long-quick-replies.png +0 -0
  133. package/screenshots/truth/actions/send_msg/render/multiline-text-with-replies.png +0 -0
  134. package/screenshots/truth/actions/send_msg/render/simple-text.png +0 -0
  135. package/screenshots/truth/actions/send_msg/render/text-with-linebreaks.png +0 -0
  136. package/screenshots/truth/actions/send_msg/render/text-with-many-quick-replies.png +0 -0
  137. package/screenshots/truth/actions/send_msg/render/text-with-quick-replies.png +0 -0
  138. package/screenshots/truth/actions/send_msg/render/text-without-quick-replies.png +0 -0
  139. package/screenshots/truth/checkbox/checkbox-label-background-hover.png +0 -0
  140. package/screenshots/truth/checkbox/checkbox-whitespace-label-no-background-hover.png +0 -0
  141. package/screenshots/truth/checkbox/checkbox-with-help-text.png +0 -0
  142. package/screenshots/truth/checkbox/checked.png +0 -0
  143. package/screenshots/truth/checkbox/default.png +0 -0
  144. package/screenshots/truth/editor/wait.png +0 -0
  145. package/screenshots/truth/integration/textinput-markdown-errors.png +0 -0
  146. package/screenshots/truth/lightbox/img-zoomed.png +0 -0
  147. package/screenshots/truth/nodes/split_by_llm/editor/information-extraction.png +0 -0
  148. package/screenshots/truth/nodes/split_by_llm/editor/sentiment-analysis.png +0 -0
  149. package/screenshots/truth/nodes/split_by_llm/editor/summarization.png +0 -0
  150. package/screenshots/truth/nodes/split_by_llm/editor/translation-task.png +0 -0
  151. package/screenshots/truth/nodes/split_by_llm/render/information-extraction.png +0 -0
  152. package/screenshots/truth/nodes/split_by_llm/render/sentiment-analysis.png +0 -0
  153. package/screenshots/truth/nodes/split_by_llm/render/summarization.png +0 -0
  154. package/screenshots/truth/nodes/split_by_llm/render/translation-task.png +0 -0
  155. package/screenshots/truth/nodes/split_by_llm_categorize/editor/basic-categorization.png +0 -0
  156. package/screenshots/truth/nodes/split_by_llm_categorize/editor/custom-input-and-result-name.png +0 -0
  157. package/screenshots/truth/nodes/split_by_llm_categorize/editor/feedback-categorization.png +0 -0
  158. package/screenshots/truth/nodes/split_by_llm_categorize/editor/many-categories.png +0 -0
  159. package/screenshots/truth/nodes/split_by_llm_categorize/editor/minimal-categories.png +0 -0
  160. package/screenshots/truth/nodes/split_by_llm_categorize/render/basic-categorization.png +0 -0
  161. package/screenshots/truth/nodes/split_by_llm_categorize/render/custom-input-and-result-name.png +0 -0
  162. package/screenshots/truth/nodes/split_by_llm_categorize/render/feedback-categorization.png +0 -0
  163. package/screenshots/truth/nodes/split_by_llm_categorize/render/many-categories.png +0 -0
  164. package/screenshots/truth/nodes/split_by_llm_categorize/render/minimal-categories.png +0 -0
  165. package/screenshots/truth/nodes/split_by_random/editor/ab-test-multiple-variants.png +0 -0
  166. package/screenshots/truth/nodes/split_by_random/editor/sampling-split.png +0 -0
  167. package/screenshots/truth/nodes/split_by_random/editor/three-way-split.png +0 -0
  168. package/screenshots/truth/nodes/split_by_random/editor/two-bucket-split.png +0 -0
  169. package/screenshots/truth/nodes/split_by_random/render/ab-test-multiple-variants.png +0 -0
  170. package/screenshots/truth/nodes/split_by_random/render/sampling-split.png +0 -0
  171. package/screenshots/truth/nodes/split_by_random/render/three-way-split.png +0 -0
  172. package/screenshots/truth/nodes/split_by_random/render/two-bucket-split.png +0 -0
  173. package/screenshots/truth/nodes/wait_for_digits/editor/basic-digits-wait.png +0 -0
  174. package/screenshots/truth/nodes/wait_for_digits/editor/phone-number-collection.png +0 -0
  175. package/screenshots/truth/nodes/wait_for_digits/editor/single-digit-with-timeout.png +0 -0
  176. package/screenshots/truth/nodes/wait_for_digits/editor/verification-code.png +0 -0
  177. package/screenshots/truth/nodes/wait_for_digits/render/basic-digits-wait.png +0 -0
  178. package/screenshots/truth/nodes/wait_for_digits/render/phone-number-collection.png +0 -0
  179. package/screenshots/truth/nodes/wait_for_digits/render/single-digit-with-timeout.png +0 -0
  180. package/screenshots/truth/nodes/wait_for_digits/render/verification-code.png +0 -0
  181. package/screenshots/truth/nodes/wait_for_response/editor/basic-wait.png +0 -0
  182. package/screenshots/truth/nodes/wait_for_response/editor/custom-result-name.png +0 -0
  183. package/screenshots/truth/nodes/wait_for_response/editor/no-timeout.png +0 -0
  184. package/screenshots/truth/nodes/wait_for_response/editor/short-timeout.png +0 -0
  185. package/screenshots/truth/nodes/wait_for_response/render/basic-wait.png +0 -0
  186. package/screenshots/truth/nodes/wait_for_response/render/custom-result-name.png +0 -0
  187. package/screenshots/truth/nodes/wait_for_response/render/no-timeout.png +0 -0
  188. package/screenshots/truth/nodes/wait_for_response/render/short-timeout.png +0 -0
  189. package/screenshots/truth/run-list/basic.png +0 -0
  190. package/screenshots/truth/templates/default.png +0 -0
  191. package/screenshots/truth/wait-for-response/rules-editor.png +0 -0
  192. package/screenshots/truth/wait-for-response/timeout-editor-unchecked.png +0 -0
  193. package/screenshots/truth/wait-for-response/timeout-editor.png +0 -0
  194. package/scripts/dev-data-sync.mjs +182 -0
  195. package/src/display/Chat.ts +6 -4
  196. package/src/events.ts +6 -5
  197. package/src/flow/CanvasNode.ts +89 -79
  198. package/src/flow/Editor.ts +1 -0
  199. package/src/flow/NodeEditor.ts +55 -3
  200. package/src/flow/actions/add_contact_urn.ts +1 -1
  201. package/src/flow/actions/set_contact_channel.ts +1 -1
  202. package/src/flow/actions/set_contact_field.ts +2 -1
  203. package/src/flow/actions/set_contact_language.ts +3 -1
  204. package/src/flow/actions/set_contact_name.ts +1 -1
  205. package/src/flow/actions/set_contact_status.ts +18 -18
  206. package/src/flow/actions/set_run_result.ts +1 -1
  207. package/src/flow/nodes/split_by_llm.ts +14 -13
  208. package/src/flow/nodes/wait_for_response.ts +717 -5
  209. package/src/flow/operators.ts +215 -0
  210. package/src/flow/types.ts +10 -2
  211. package/src/form/ArrayEditor.ts +117 -37
  212. package/src/form/Checkbox.ts +12 -0
  213. package/src/form/FieldRenderer.ts +24 -3
  214. package/src/form/TextInput.ts +19 -1
  215. package/src/form/select/Select.ts +7 -0
  216. package/src/interfaces.ts +1 -1
  217. package/src/layout/Dialog.ts +4 -4
  218. package/src/list/RunList.ts +2 -2
  219. package/src/live/ContactChat.ts +144 -58
  220. package/src/live/ContactDetails.ts +7 -0
  221. package/src/live/ContactNameFetch.ts +1 -1
  222. package/static/api/labels.json +6 -1
  223. package/test/NodeHelper.ts +38 -40
  224. package/test/nodes/split_by_llm.test.ts +43 -32
  225. package/test/nodes/split_by_llm_categorize.test.ts +130 -120
  226. package/test/nodes/split_by_random.test.ts +136 -128
  227. package/test/nodes/wait_for_digits.test.ts +147 -127
  228. package/test/nodes/wait_for_response.test.ts +657 -104
  229. package/test/temba-checkbox.test.ts +36 -32
  230. package/test/temba-contact-chat.test.ts +2 -1
  231. package/test/temba-dropdown.test.ts +0 -12
  232. package/test/temba-flow-editor-node.test.ts +11 -4
  233. package/test/temba-integration-markdown.test.ts +16 -17
  234. package/test/temba-node-editor.test.ts +5 -43
  235. package/test/temba-run-list.test.ts +2 -2
  236. package/test/utils.test.ts +2 -1
  237. package/test-assets/list/runs.json +8 -8
  238. package/web-dev-mock.mjs +86 -30
  239. package/web-dev-server.config.mjs +272 -31
  240. package/screenshots/truth/dropdown/bottom-edge-collision.png +0 -0
  241. package/screenshots/truth/dropdown/right-edge-collision.png +0 -0
  242. package/screenshots/truth/editor/send_msg.png +0 -0
  243. package/screenshots/truth/editor/set_contact_language.png +0 -0
  244. package/screenshots/truth/editor/set_contact_name.png +0 -0
  245. package/screenshots/truth/editor/set_run_result.png +0 -0
  246. package/screenshots/truth/integration/checkbox-markdown-errors.png +0 -0
@@ -2,6 +2,7 @@ import { expect } from '@open-wc/testing';
2
2
  import { wait_for_response } from '../../src/flow/nodes/wait_for_response';
3
3
  import { Node } from '../../src/store/flow-definition';
4
4
  import { NodeTest } from '../NodeHelper';
5
+ import { createOperatorOption } from '../../src/flow/operators';
5
6
 
6
7
  /**
7
8
  * Test suite for the wait_for_response node configuration.
@@ -27,116 +28,121 @@ describe('wait_for_response node config', () => {
27
28
 
28
29
  it('has layout configuration', () => {
29
30
  expect(wait_for_response.layout).to.exist;
30
- expect(wait_for_response.layout).to.deep.equal([
31
- 'timeout',
32
- 'result_name'
33
- ]);
31
+ expect(wait_for_response.layout).to.deep.equal(['rules', 'result_name']);
34
32
  });
35
33
  });
36
34
 
37
35
  describe('node scenarios', () => {
38
- helper.testNode(
39
- {
40
- uuid: 'test-wait-node-1',
41
- actions: [],
42
- router: {
43
- type: 'switch',
44
- wait: {
45
- type: 'msg',
46
- timeout: {
47
- category_uuid: 'timeout-cat-1',
48
- seconds: 300
49
- }
36
+ it('renders basic wait', async () => {
37
+ await helper.testNode(
38
+ {
39
+ uuid: 'test-wait-node-1',
40
+ actions: [],
41
+ router: {
42
+ type: 'switch',
43
+ wait: {
44
+ type: 'msg',
45
+ timeout: {
46
+ category_uuid: 'timeout-cat-1',
47
+ seconds: 300
48
+ }
49
+ },
50
+ result_name: 'response',
51
+ categories: [
52
+ {
53
+ uuid: 'timeout-cat-1',
54
+ name: 'No Response',
55
+ exit_uuid: 'timeout-exit-1'
56
+ }
57
+ ]
50
58
  },
51
- result_name: 'response',
52
- categories: [
53
- {
54
- uuid: 'timeout-cat-1',
55
- name: 'No Response',
56
- exit_uuid: 'timeout-exit-1'
57
- }
58
- ]
59
- },
60
- exits: [{ uuid: 'timeout-exit-1', destination_uuid: null }]
61
- } as Node,
62
- { type: 'wait_for_response' },
63
- 'basic-wait'
64
- );
65
-
66
- helper.testNode(
67
- {
68
- uuid: 'test-wait-node-2',
69
- actions: [],
70
- router: {
71
- type: 'switch',
72
- wait: {
73
- type: 'msg',
74
- timeout: {
75
- category_uuid: 'timeout-cat-2',
76
- seconds: 1800
77
- }
59
+ exits: [{ uuid: 'timeout-exit-1', destination_uuid: null }]
60
+ } as Node,
61
+ { type: 'wait_for_response' },
62
+ 'basic-wait'
63
+ );
64
+ });
65
+
66
+ it('renders custom result name', async () => {
67
+ await helper.testNode(
68
+ {
69
+ uuid: 'test-wait-node-2',
70
+ actions: [],
71
+ router: {
72
+ type: 'switch',
73
+ wait: {
74
+ type: 'msg',
75
+ timeout: {
76
+ category_uuid: 'timeout-cat-2',
77
+ seconds: 1800
78
+ }
79
+ },
80
+ result_name: 'user_input',
81
+ categories: [
82
+ {
83
+ uuid: 'timeout-cat-2',
84
+ name: 'No Response',
85
+ exit_uuid: 'timeout-exit-2'
86
+ }
87
+ ]
78
88
  },
79
- result_name: 'user_input',
80
- categories: [
81
- {
82
- uuid: 'timeout-cat-2',
83
- name: 'No Response',
84
- exit_uuid: 'timeout-exit-2'
85
- }
86
- ]
87
- },
88
- exits: [{ uuid: 'timeout-exit-2', destination_uuid: null }]
89
- } as Node,
90
- { type: 'wait_for_response' },
91
- 'custom-result-name'
92
- );
93
-
94
- helper.testNode(
95
- {
96
- uuid: 'test-wait-node-3',
97
- actions: [],
98
- router: {
99
- type: 'switch',
100
- wait: {
101
- type: 'msg',
102
- timeout: {
103
- category_uuid: 'timeout-cat-3',
104
- seconds: 60
105
- }
89
+ exits: [{ uuid: 'timeout-exit-2', destination_uuid: null }]
90
+ } as Node,
91
+ { type: 'wait_for_response' },
92
+ 'custom-result-name'
93
+ );
94
+ });
95
+
96
+ it('renders short timeout', async () => {
97
+ await helper.testNode(
98
+ {
99
+ uuid: 'test-wait-node-3',
100
+ actions: [],
101
+ router: {
102
+ type: 'switch',
103
+ wait: {
104
+ type: 'msg',
105
+ timeout: {
106
+ category_uuid: 'timeout-cat-3',
107
+ seconds: 60
108
+ }
109
+ },
110
+ result_name: 'quick_response',
111
+ categories: [
112
+ {
113
+ uuid: 'timeout-cat-3',
114
+ name: 'No Response',
115
+ exit_uuid: 'timeout-exit-3'
116
+ }
117
+ ]
106
118
  },
107
- result_name: 'quick_response',
108
- categories: [
109
- {
110
- uuid: 'timeout-cat-3',
111
- name: 'No Response',
112
- exit_uuid: 'timeout-exit-3'
113
- }
114
- ]
115
- },
116
- exits: [{ uuid: 'timeout-exit-3', destination_uuid: null }]
117
- } as Node,
118
- { type: 'wait_for_response' },
119
- 'short-timeout'
120
- );
121
-
122
- helper.testNode(
123
- {
124
- uuid: 'test-wait-node-4',
125
- actions: [],
126
- router: {
127
- type: 'switch',
128
- wait: {
129
- type: 'msg'
130
- // No timeout specified
119
+ exits: [{ uuid: 'timeout-exit-3', destination_uuid: null }]
120
+ } as Node,
121
+ { type: 'wait_for_response' },
122
+ 'short-timeout'
123
+ );
124
+ });
125
+
126
+ it('renders no timeout', async () => {
127
+ await helper.testNode(
128
+ {
129
+ uuid: 'test-wait-node-4',
130
+ actions: [],
131
+ router: {
132
+ type: 'switch',
133
+ wait: {
134
+ type: 'msg'
135
+ // No timeout specified
136
+ },
137
+ result_name: 'response',
138
+ categories: []
131
139
  },
132
- result_name: 'response',
133
- categories: []
134
- },
135
- exits: []
136
- } as Node,
137
- { type: 'wait_for_response' },
138
- 'no-timeout'
139
- );
140
+ exits: []
141
+ } as Node,
142
+ { type: 'wait_for_response' },
143
+ 'no-timeout'
144
+ );
145
+ });
140
146
  });
141
147
 
142
148
  describe('data transformation', () => {
@@ -156,12 +162,80 @@ describe('wait_for_response node config', () => {
156
162
 
157
163
  expect(formData.uuid).to.equal('test-node');
158
164
  expect(formData.result_name).to.equal('user_response');
165
+ expect(formData.rules).to.deep.equal([]);
166
+ });
167
+
168
+ it('converts node with rules to form data correctly', () => {
169
+ const node: Node = {
170
+ uuid: 'test-node',
171
+ actions: [],
172
+ router: {
173
+ type: 'switch',
174
+ result_name: 'user_response',
175
+ operand: '@input.text',
176
+ categories: [
177
+ {
178
+ uuid: 'red-cat',
179
+ name: 'Red',
180
+ exit_uuid: 'red-exit'
181
+ },
182
+ {
183
+ uuid: 'green-cat',
184
+ name: 'Green',
185
+ exit_uuid: 'green-exit'
186
+ },
187
+ {
188
+ uuid: 'other-cat',
189
+ name: 'Other',
190
+ exit_uuid: 'other-exit'
191
+ }
192
+ ],
193
+ cases: [
194
+ {
195
+ uuid: 'red-case',
196
+ type: 'has_any_word',
197
+ arguments: ['red'],
198
+ category_uuid: 'red-cat'
199
+ },
200
+ {
201
+ uuid: 'green-case',
202
+ type: 'has_phrase',
203
+ arguments: ['green'],
204
+ category_uuid: 'green-cat'
205
+ }
206
+ ]
207
+ },
208
+ exits: [
209
+ { uuid: 'red-exit', destination_uuid: null },
210
+ { uuid: 'green-exit', destination_uuid: null },
211
+ { uuid: 'other-exit', destination_uuid: null }
212
+ ]
213
+ };
214
+
215
+ const formData = wait_for_response.toFormData!(node);
216
+
217
+ expect(formData.uuid).to.equal('test-node');
218
+ expect(formData.result_name).to.equal('user_response');
219
+ expect(formData.rules).to.have.length(2);
220
+ expect(formData.rules[0]).to.deep.equal({
221
+ operator: createOperatorOption('has_any_word'),
222
+ value1: 'red',
223
+ value2: '',
224
+ category: 'Red'
225
+ });
226
+ expect(formData.rules[1]).to.deep.equal({
227
+ operator: createOperatorOption('has_phrase'),
228
+ value1: 'green',
229
+ value2: '',
230
+ category: 'Green'
231
+ });
159
232
  });
160
233
 
161
234
  it('converts form data to node correctly', () => {
162
235
  const formData = {
163
236
  uuid: 'test-node',
164
- result_name: 'custom_response'
237
+ result_name: 'custom_response',
238
+ rules: []
165
239
  };
166
240
 
167
241
  const originalNode: Node = {
@@ -181,9 +255,68 @@ describe('wait_for_response node config', () => {
181
255
  expect(result.router?.result_name).to.equal('custom_response');
182
256
  });
183
257
 
258
+ it('converts form data with rules to node correctly', () => {
259
+ const formData = {
260
+ uuid: 'test-node',
261
+ result_name: 'custom_response',
262
+ rules: [
263
+ {
264
+ operator: 'has_any_word',
265
+ value1: 'red',
266
+ value2: '',
267
+ category: 'Red'
268
+ },
269
+ {
270
+ operator: 'has_phrase',
271
+ value1: 'blue',
272
+ value2: '',
273
+ category: 'Blue'
274
+ }
275
+ ]
276
+ };
277
+
278
+ const originalNode: Node = {
279
+ uuid: 'test-node',
280
+ actions: [],
281
+ exits: [],
282
+ router: {
283
+ type: 'switch',
284
+ result_name: 'response',
285
+ categories: []
286
+ }
287
+ };
288
+
289
+ const result = wait_for_response.fromFormData!(formData, originalNode);
290
+
291
+ expect(result.uuid).to.equal('test-node');
292
+ expect(result.router?.result_name).to.equal('custom_response');
293
+ expect(result.router?.operand).to.equal('@input.text');
294
+ expect(result.router?.categories).to.have.length(3); // Red, Blue, Other
295
+ expect(result.router?.cases).to.have.length(2);
296
+ expect(result.exits).to.have.length(3);
297
+
298
+ // Check categories
299
+ const categoryNames = result.router!.categories.map((cat) => cat.name);
300
+ expect(categoryNames).to.include.members(['Red', 'Blue', 'Other']);
301
+
302
+ // Check cases
303
+ const redCase = result.router!.cases.find(
304
+ (c) => c.type === 'has_any_word'
305
+ );
306
+ expect(redCase).to.exist;
307
+ expect(redCase!.arguments).to.deep.equal(['red']);
308
+
309
+ const blueCase = result.router!.cases.find(
310
+ (c) => c.type === 'has_phrase'
311
+ );
312
+ expect(blueCase).to.exist;
313
+ expect(blueCase!.arguments).to.deep.equal(['blue']);
314
+ });
315
+
184
316
  it('handles default result name', () => {
185
317
  const formData = {
186
- uuid: 'test-node'
318
+ uuid: 'test-node',
319
+ rules: []
187
320
  // No result_name specified
188
321
  };
189
322
 
@@ -202,5 +335,425 @@ describe('wait_for_response node config', () => {
202
335
  expect(result.uuid).to.equal('test-node');
203
336
  expect(result.router?.result_name).to.equal('response');
204
337
  });
338
+
339
+ it('handles operators with no operands correctly', () => {
340
+ const formData = {
341
+ uuid: 'test-node',
342
+ result_name: 'custom_response',
343
+ rules: [
344
+ {
345
+ operator: 'has_text',
346
+ value: '', // No value needed for has_text
347
+ category: 'Has Text'
348
+ },
349
+ {
350
+ operator: 'has_number',
351
+ value: '', // No value needed for has_number
352
+ category: 'Has Number'
353
+ }
354
+ ]
355
+ };
356
+
357
+ const originalNode: Node = {
358
+ uuid: 'test-node',
359
+ actions: [],
360
+ exits: [],
361
+ router: {
362
+ type: 'switch',
363
+ result_name: 'response',
364
+ categories: []
365
+ }
366
+ };
367
+
368
+ const result = wait_for_response.fromFormData!(formData, originalNode);
369
+
370
+ expect(result.uuid).to.equal('test-node');
371
+ expect(result.router?.result_name).to.equal('custom_response');
372
+ expect(result.router?.categories).to.have.length(3); // Has Text, Has Number, Other
373
+ expect(result.router?.cases).to.have.length(2);
374
+ expect(result.exits).to.have.length(3);
375
+
376
+ // Check categories
377
+ const categoryNames = result.router!.categories.map((cat) => cat.name);
378
+ expect(categoryNames).to.include.members([
379
+ 'Has Text',
380
+ 'Has Number',
381
+ 'Other'
382
+ ]);
383
+
384
+ // Check cases - should have empty arguments for 0-operand operators
385
+ const hasTextCase = result.router!.cases.find(
386
+ (c) => c.type === 'has_text'
387
+ );
388
+ expect(hasTextCase).to.exist;
389
+ expect(hasTextCase!.arguments).to.deep.equal([]);
390
+
391
+ const hasNumberCase = result.router!.cases.find(
392
+ (c) => c.type === 'has_number'
393
+ );
394
+ expect(hasNumberCase).to.exist;
395
+ expect(hasNumberCase!.arguments).to.deep.equal([]);
396
+ });
397
+
398
+ it('preserves timeout categories when adding rules', () => {
399
+ const formData = {
400
+ uuid: 'test-node',
401
+ result_name: 'response',
402
+ timeout_enabled: true,
403
+ timeout_duration: [{ value: '300', name: '5 minutes' }],
404
+ rules: [
405
+ {
406
+ operator: 'has_text',
407
+ value: 'yes',
408
+ category: 'Positive'
409
+ }
410
+ ]
411
+ };
412
+
413
+ const originalNode: Node = {
414
+ uuid: 'test-node',
415
+ actions: [],
416
+ router: {
417
+ type: 'switch',
418
+ wait: {
419
+ type: 'msg',
420
+ timeout: {
421
+ category_uuid: 'timeout-cat',
422
+ seconds: 300
423
+ }
424
+ },
425
+ result_name: 'response',
426
+ categories: [
427
+ {
428
+ uuid: 'timeout-cat',
429
+ name: 'No Response',
430
+ exit_uuid: 'timeout-exit'
431
+ }
432
+ ]
433
+ },
434
+ exits: [{ uuid: 'timeout-exit', destination_uuid: null }]
435
+ };
436
+
437
+ const result = wait_for_response.fromFormData!(formData, originalNode);
438
+
439
+ expect(result.router?.categories).to.have.length(3); // Positive, No Response, Other
440
+ const categoryNames = result.router!.categories.map((cat) => cat.name);
441
+ expect(categoryNames).to.include.members([
442
+ 'Positive',
443
+ 'No Response',
444
+ 'Other'
445
+ ]);
446
+
447
+ // Verify timeout configuration is preserved
448
+ expect(result.router?.wait).to.exist;
449
+ expect(result.router?.wait?.timeout?.category_uuid).to.equal(
450
+ 'timeout-cat'
451
+ );
452
+ });
453
+ });
454
+
455
+ describe('validation', () => {
456
+ it('validates form data correctly with no errors', () => {
457
+ const formData = {
458
+ rules: [
459
+ { operator: 'has_text', value: 'yes', category: 'Positive' },
460
+ { operator: 'has_text', value: 'no', category: 'Negative' }
461
+ ]
462
+ };
463
+
464
+ const validation = wait_for_response.validate!(formData);
465
+ expect(validation.valid).to.be.true;
466
+ expect(validation.errors).to.be.empty;
467
+ });
468
+
469
+ it('allows same category names for multiple rules', () => {
470
+ const formData = {
471
+ rules: [
472
+ { operator: 'has_text', value: 'yes', category: 'Positive' },
473
+ { operator: 'has_text', value: 'ok', category: 'positive' }, // case insensitive same category
474
+ { operator: 'has_text', value: 'no', category: 'Negative' }
475
+ ]
476
+ };
477
+
478
+ const validation = wait_for_response.validate!(formData);
479
+ expect(validation.valid).to.be.true;
480
+ expect(validation.errors).to.be.empty;
481
+ });
482
+
483
+ it('allows rules with operators that need no values', () => {
484
+ const formData = {
485
+ rules: [
486
+ { operator: 'has_text', value: '', category: 'Has Text' },
487
+ { operator: 'has_number', value: '', category: 'Has Number' },
488
+ { operator: 'has_any_word', value: 'hello', category: 'Greeting' }
489
+ ]
490
+ };
491
+
492
+ const validation = wait_for_response.validate!(formData);
493
+ expect(validation.valid).to.be.true;
494
+ expect(validation.errors).to.be.empty;
495
+ });
496
+
497
+ it('ignores empty rules in validation', () => {
498
+ const formData = {
499
+ rules: [
500
+ { operator: 'has_phrase', value: 'yes', category: 'Positive' },
501
+ { operator: '', value: '', category: '' }, // empty rule
502
+ { operator: 'has_phrase', value: 'no', category: 'Negative' }
503
+ ]
504
+ };
505
+
506
+ const validation = wait_for_response.validate!(formData);
507
+ expect(validation.valid).to.be.true;
508
+ expect(validation.errors).to.be.empty;
509
+ });
510
+
511
+ it('preserves category UUIDs when names are updated', () => {
512
+ // Create original node with specific UUIDs
513
+ const originalNode: Node = {
514
+ uuid: 'test-node',
515
+ actions: [],
516
+ router: {
517
+ type: 'switch',
518
+ result_name: 'response',
519
+ categories: [
520
+ {
521
+ uuid: 'category-1-uuid',
522
+ name: 'Old Name 1',
523
+ exit_uuid: 'exit-1-uuid'
524
+ },
525
+ {
526
+ uuid: 'category-2-uuid',
527
+ name: 'Old Name 2',
528
+ exit_uuid: 'exit-2-uuid'
529
+ },
530
+ {
531
+ uuid: 'other-category-uuid',
532
+ name: 'Other',
533
+ exit_uuid: 'other-exit-uuid'
534
+ }
535
+ ],
536
+ cases: [
537
+ {
538
+ uuid: 'case-1-uuid',
539
+ type: 'has_phrase',
540
+ arguments: ['yes'],
541
+ category_uuid: 'category-1-uuid'
542
+ },
543
+ {
544
+ uuid: 'case-2-uuid',
545
+ type: 'has_phrase',
546
+ arguments: ['no'],
547
+ category_uuid: 'category-2-uuid'
548
+ }
549
+ ]
550
+ },
551
+ exits: [
552
+ { uuid: 'exit-1-uuid', destination_uuid: null },
553
+ { uuid: 'exit-2-uuid', destination_uuid: null },
554
+ { uuid: 'other-exit-uuid', destination_uuid: null }
555
+ ]
556
+ };
557
+
558
+ // Update category names but keep same rules in same order
559
+ const formData = {
560
+ uuid: 'test-node',
561
+ result_name: 'response',
562
+ rules: [
563
+ {
564
+ operator: { value: 'has_phrase', name: 'contains phrase' },
565
+ value1: 'yes',
566
+ category: 'New Name 1' // Changed from "Old Name 1"
567
+ },
568
+ {
569
+ operator: { value: 'has_phrase', name: 'contains phrase' },
570
+ value1: 'no',
571
+ category: 'New Name 2' // Changed from "Old Name 2"
572
+ }
573
+ ]
574
+ };
575
+
576
+ const result = wait_for_response.fromFormData!(formData, originalNode);
577
+
578
+ // Verify that UUIDs are preserved despite name changes
579
+ expect(result.router?.categories).to.have.length(3); // Two rules + Other
580
+
581
+ const category1 = result.router!.categories.find(
582
+ (cat) => cat.name === 'New Name 1'
583
+ );
584
+ const category2 = result.router!.categories.find(
585
+ (cat) => cat.name === 'New Name 2'
586
+ );
587
+ const otherCategory = result.router!.categories.find(
588
+ (cat) => cat.name === 'Other'
589
+ );
590
+
591
+ // Verify UUIDs are preserved
592
+ expect(category1?.uuid).to.equal('category-1-uuid');
593
+ expect(category1?.exit_uuid).to.equal('exit-1-uuid');
594
+
595
+ expect(category2?.uuid).to.equal('category-2-uuid');
596
+ expect(category2?.exit_uuid).to.equal('exit-2-uuid');
597
+
598
+ expect(otherCategory?.uuid).to.equal('other-category-uuid');
599
+ expect(otherCategory?.exit_uuid).to.equal('other-exit-uuid');
600
+
601
+ // Verify case UUIDs are also preserved
602
+ const case1 = result.router!.cases.find(
603
+ (c) => c.category_uuid === 'category-1-uuid'
604
+ );
605
+ const case2 = result.router!.cases.find(
606
+ (c) => c.category_uuid === 'category-2-uuid'
607
+ );
608
+
609
+ expect(case1?.uuid).to.equal('case-1-uuid');
610
+ expect(case2?.uuid).to.equal('case-2-uuid');
611
+
612
+ // Verify exits are preserved
613
+ expect(result.exits).to.have.length(3);
614
+ expect(result.exits.map((e) => e.uuid)).to.include.members([
615
+ 'exit-1-uuid',
616
+ 'exit-2-uuid',
617
+ 'other-exit-uuid'
618
+ ]);
619
+ });
620
+
621
+ it('merges rules with same category name into single category', () => {
622
+ const formData = {
623
+ uuid: 'test-node',
624
+ result_name: 'response',
625
+ rules: [
626
+ {
627
+ operator: { value: 'has_phrase', name: 'contains phrase' },
628
+ value1: 'yes',
629
+ category: 'Positive'
630
+ },
631
+ {
632
+ operator: { value: 'has_phrase', name: 'contains phrase' },
633
+ value1: 'ok',
634
+ category: 'Positive' // Same category name
635
+ },
636
+ {
637
+ operator: { value: 'has_phrase', name: 'contains phrase' },
638
+ value1: 'no',
639
+ category: 'Negative'
640
+ }
641
+ ]
642
+ };
643
+
644
+ const originalNode: Node = {
645
+ uuid: 'test-node',
646
+ actions: [],
647
+ router: {
648
+ type: 'switch',
649
+ result_name: 'response',
650
+ categories: [],
651
+ cases: []
652
+ },
653
+ exits: []
654
+ };
655
+
656
+ const result = wait_for_response.fromFormData!(formData, originalNode);
657
+
658
+ // Should have 3 categories: Positive, Negative, Other
659
+ expect(result.router?.categories).to.have.length(3);
660
+
661
+ const categoryNames = result.router!.categories.map((cat) => cat.name);
662
+ expect(categoryNames).to.include.members([
663
+ 'Positive',
664
+ 'Negative',
665
+ 'Other'
666
+ ]);
667
+
668
+ // Should have 3 cases but only 2 user categories (+ Other)
669
+ expect(result.router?.cases).to.have.length(3);
670
+
671
+ // Both "yes" and "ok" rules should reference the same Positive category
672
+ const positiveCategory = result.router!.categories.find(
673
+ (cat) => cat.name === 'Positive'
674
+ );
675
+ expect(positiveCategory).to.exist;
676
+
677
+ const positiveCases = result.router!.cases.filter(
678
+ (case_) => case_.category_uuid === positiveCategory!.uuid
679
+ );
680
+ expect(positiveCases).to.have.length(2);
681
+
682
+ // Verify the cases have the correct arguments
683
+ const yesCase = positiveCases.find((case_) =>
684
+ case_.arguments.includes('yes')
685
+ );
686
+ const okCase = positiveCases.find((case_) =>
687
+ case_.arguments.includes('ok')
688
+ );
689
+
690
+ expect(yesCase).to.exist;
691
+ expect(okCase).to.exist;
692
+
693
+ // Should have 3 exits: Positive, Negative, Other
694
+ expect(result.exits).to.have.length(3);
695
+ });
696
+
697
+ it('preserves category order when merging same category names', () => {
698
+ const formData = {
699
+ uuid: 'test-node',
700
+ result_name: 'response',
701
+ rules: [
702
+ {
703
+ operator: { value: 'has_phrase', name: 'contains phrase' },
704
+ value1: 'yes',
705
+ category: 'First'
706
+ },
707
+ {
708
+ operator: { value: 'has_phrase', name: 'contains phrase' },
709
+ value1: 'maybe',
710
+ category: 'Second'
711
+ },
712
+ {
713
+ operator: { value: 'has_phrase', name: 'contains phrase' },
714
+ value1: 'ok',
715
+ category: 'First' // Same as first rule
716
+ }
717
+ ]
718
+ };
719
+
720
+ const originalNode: Node = {
721
+ uuid: 'test-node',
722
+ actions: [],
723
+ router: {
724
+ type: 'switch',
725
+ result_name: 'response',
726
+ categories: [],
727
+ cases: []
728
+ },
729
+ exits: []
730
+ };
731
+
732
+ const result = wait_for_response.fromFormData!(formData, originalNode);
733
+
734
+ // Should have 3 categories: First, Second, Other (in that order)
735
+ expect(result.router?.categories).to.have.length(3);
736
+
737
+ const categoryNames = result.router!.categories.map((cat) => cat.name);
738
+ expect(categoryNames).to.deep.equal(['First', 'Second', 'Other']);
739
+
740
+ // First category should have 2 cases (yes and ok)
741
+ const firstCategory = result.router!.categories.find(
742
+ (cat) => cat.name === 'First'
743
+ );
744
+ const firstCases = result.router!.cases.filter(
745
+ (case_) => case_.category_uuid === firstCategory!.uuid
746
+ );
747
+ expect(firstCases).to.have.length(2);
748
+
749
+ // Second category should have 1 case (maybe)
750
+ const secondCategory = result.router!.categories.find(
751
+ (cat) => cat.name === 'Second'
752
+ );
753
+ const secondCases = result.router!.cases.filter(
754
+ (case_) => case_.category_uuid === secondCategory!.uuid
755
+ );
756
+ expect(secondCases).to.have.length(1);
757
+ });
205
758
  });
206
759
  });