@nyaruka/temba-components 0.129.8 → 0.129.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (282) hide show
  1. package/CHANGELOG.md +37 -3
  2. package/demo/data/flows/sample-flow.json +186 -96
  3. package/demo/test-colorpicker.html +30 -0
  4. package/dist/temba-components.js +1126 -1111
  5. package/dist/temba-components.js.map +1 -1
  6. package/out-tsc/src/events.js.map +1 -1
  7. package/out-tsc/src/excellent/helpers.js +2 -2
  8. package/out-tsc/src/excellent/helpers.js.map +1 -1
  9. package/out-tsc/src/flow/CanvasNode.js +25 -7
  10. package/out-tsc/src/flow/CanvasNode.js.map +1 -1
  11. package/out-tsc/src/flow/Editor.js +11 -1
  12. package/out-tsc/src/flow/Editor.js.map +1 -1
  13. package/out-tsc/src/flow/NodeEditor.js +133 -290
  14. package/out-tsc/src/flow/NodeEditor.js.map +1 -1
  15. package/out-tsc/src/flow/actions/add_input_labels.js +40 -0
  16. package/out-tsc/src/flow/actions/add_input_labels.js.map +1 -1
  17. package/out-tsc/src/flow/actions/call_llm.js +56 -3
  18. package/out-tsc/src/flow/actions/call_llm.js.map +1 -1
  19. package/out-tsc/src/flow/actions/call_webhook.js +1 -1
  20. package/out-tsc/src/flow/actions/call_webhook.js.map +1 -1
  21. package/out-tsc/src/flow/actions/open_ticket.js +65 -3
  22. package/out-tsc/src/flow/actions/open_ticket.js.map +1 -1
  23. package/out-tsc/src/flow/actions/set_run_result.js +75 -0
  24. package/out-tsc/src/flow/actions/set_run_result.js.map +1 -1
  25. package/out-tsc/src/flow/config.js +4 -0
  26. package/out-tsc/src/flow/config.js.map +1 -1
  27. package/out-tsc/src/flow/nodes/split_by_llm_categorize.js +227 -0
  28. package/out-tsc/src/flow/nodes/split_by_llm_categorize.js.map +1 -0
  29. package/out-tsc/src/flow/nodes/split_by_ticket.js +18 -0
  30. package/out-tsc/src/flow/nodes/split_by_ticket.js.map +1 -0
  31. package/out-tsc/src/flow/nodes/wait_for_response.js +27 -1
  32. package/out-tsc/src/flow/nodes/wait_for_response.js.map +1 -1
  33. package/out-tsc/src/flow/types.js +0 -65
  34. package/out-tsc/src/flow/types.js.map +1 -1
  35. package/out-tsc/src/form/ArrayEditor.js +63 -117
  36. package/out-tsc/src/form/ArrayEditor.js.map +1 -1
  37. package/out-tsc/src/form/BaseListEditor.js +4 -3
  38. package/out-tsc/src/form/BaseListEditor.js.map +1 -1
  39. package/out-tsc/src/form/Checkbox.js +77 -24
  40. package/out-tsc/src/form/Checkbox.js.map +1 -1
  41. package/out-tsc/src/form/ColorPicker.js +28 -40
  42. package/out-tsc/src/form/ColorPicker.js.map +1 -1
  43. package/out-tsc/src/form/Completion.js +44 -53
  44. package/out-tsc/src/form/Completion.js.map +1 -1
  45. package/out-tsc/src/form/Compose.js +7 -8
  46. package/out-tsc/src/form/Compose.js.map +1 -1
  47. package/out-tsc/src/form/ContactSearch.js +3 -4
  48. package/out-tsc/src/form/ContactSearch.js.map +1 -1
  49. package/out-tsc/src/form/DatePicker.js +29 -36
  50. package/out-tsc/src/form/DatePicker.js.map +1 -1
  51. package/out-tsc/src/form/{FormField.js → FieldElement.js} +81 -53
  52. package/out-tsc/src/form/FieldElement.js.map +1 -0
  53. package/out-tsc/src/form/FieldRenderer.js +306 -0
  54. package/out-tsc/src/form/FieldRenderer.js.map +1 -0
  55. package/out-tsc/src/form/ImagePicker.js +122 -126
  56. package/out-tsc/src/form/ImagePicker.js.map +1 -1
  57. package/out-tsc/src/form/KeyValueEditor.js +41 -37
  58. package/out-tsc/src/form/KeyValueEditor.js.map +1 -1
  59. package/out-tsc/src/form/MessageEditor.js +55 -63
  60. package/out-tsc/src/form/MessageEditor.js.map +1 -1
  61. package/out-tsc/src/form/TembaSlider.js +3 -3
  62. package/out-tsc/src/form/TembaSlider.js.map +1 -1
  63. package/out-tsc/src/form/TemplateEditor.js +3 -3
  64. package/out-tsc/src/form/TemplateEditor.js.map +1 -1
  65. package/out-tsc/src/form/TextInput.js +23 -27
  66. package/out-tsc/src/form/TextInput.js.map +1 -1
  67. package/out-tsc/src/form/select/Select.js +57 -35
  68. package/out-tsc/src/form/select/Select.js.map +1 -1
  69. package/out-tsc/src/form/select/UserSelect.js +8 -9
  70. package/out-tsc/src/form/select/UserSelect.js.map +1 -1
  71. package/out-tsc/src/form/select/WorkspaceSelect.js +7 -8
  72. package/out-tsc/src/form/select/WorkspaceSelect.js.map +1 -1
  73. package/out-tsc/src/live/ContactChat.js +62 -44
  74. package/out-tsc/src/live/ContactChat.js.map +1 -1
  75. package/out-tsc/src/live/ContactFieldEditor.js.map +1 -1
  76. package/out-tsc/src/markdown.js +13 -11
  77. package/out-tsc/src/markdown.js.map +1 -1
  78. package/out-tsc/temba-modules.js +3 -2
  79. package/out-tsc/temba-modules.js.map +1 -1
  80. package/out-tsc/test/ActionHelper.js +2 -0
  81. package/out-tsc/test/ActionHelper.js.map +1 -1
  82. package/out-tsc/test/NodeHelper.js +148 -0
  83. package/out-tsc/test/NodeHelper.js.map +1 -0
  84. package/out-tsc/test/actions/call_llm.test.js +103 -0
  85. package/out-tsc/test/actions/call_llm.test.js.map +1 -0
  86. package/out-tsc/test/nodes/split_by_llm_categorize.test.js +532 -0
  87. package/out-tsc/test/nodes/split_by_llm_categorize.test.js.map +1 -0
  88. package/out-tsc/test/nodes/split_by_random.test.js +150 -0
  89. package/out-tsc/test/nodes/split_by_random.test.js.map +1 -0
  90. package/out-tsc/test/nodes/wait_for_digits.test.js +150 -0
  91. package/out-tsc/test/nodes/wait_for_digits.test.js.map +1 -0
  92. package/out-tsc/test/nodes/wait_for_response.test.js +171 -0
  93. package/out-tsc/test/nodes/wait_for_response.test.js.map +1 -0
  94. package/out-tsc/test/temba-add-input-labels.test.js +70 -0
  95. package/out-tsc/test/temba-add-input-labels.test.js.map +1 -0
  96. package/out-tsc/test/temba-checkbox.test.js +16 -0
  97. package/out-tsc/test/temba-checkbox.test.js.map +1 -1
  98. package/out-tsc/test/temba-field-renderer.test.js +296 -0
  99. package/out-tsc/test/temba-field-renderer.test.js.map +1 -0
  100. package/out-tsc/test/temba-integration-markdown.test.js +2 -4
  101. package/out-tsc/test/temba-integration-markdown.test.js.map +1 -1
  102. package/out-tsc/test/temba-markdown.test.js +1 -1
  103. package/out-tsc/test/temba-markdown.test.js.map +1 -1
  104. package/out-tsc/test/temba-node-editor.test.js +400 -0
  105. package/out-tsc/test/temba-node-editor.test.js.map +1 -1
  106. package/out-tsc/test/temba-select.test.js +6 -3
  107. package/out-tsc/test/temba-select.test.js.map +1 -1
  108. package/out-tsc/test/temba-slider.test.js +0 -1
  109. package/out-tsc/test/temba-slider.test.js.map +1 -1
  110. package/out-tsc/test/temba-webchat.test.js +1 -1
  111. package/out-tsc/test/temba-webchat.test.js.map +1 -1
  112. package/package.json +1 -1
  113. package/screenshots/truth/actions/add_contact_groups/editor/descriptive-group-names.png +0 -0
  114. package/screenshots/truth/actions/add_contact_groups/editor/long-group-names.png +0 -0
  115. package/screenshots/truth/actions/add_contact_groups/editor/many-groups.png +0 -0
  116. package/screenshots/truth/actions/call_llm/editor/information-extraction.png +0 -0
  117. package/screenshots/truth/actions/call_llm/editor/sentiment-analysis.png +0 -0
  118. package/screenshots/truth/actions/call_llm/editor/summarization.png +0 -0
  119. package/screenshots/truth/actions/call_llm/editor/translation-task.png +0 -0
  120. package/screenshots/truth/actions/call_llm/render/information-extraction.png +0 -0
  121. package/screenshots/truth/actions/call_llm/render/sentiment-analysis.png +0 -0
  122. package/screenshots/truth/actions/call_llm/render/summarization.png +0 -0
  123. package/screenshots/truth/actions/call_llm/render/translation-task.png +0 -0
  124. package/screenshots/truth/actions/remove_contact_groups/editor/cleanup-groups.png +0 -0
  125. package/screenshots/truth/actions/remove_contact_groups/editor/long-descriptive-group-names.png +0 -0
  126. package/screenshots/truth/actions/remove_contact_groups/editor/many-groups.png +0 -0
  127. package/screenshots/truth/actions/remove_contact_groups/editor/multiple-groups.png +0 -0
  128. package/screenshots/truth/actions/remove_contact_groups/editor/remove-from-all-groups.png +0 -0
  129. package/screenshots/truth/actions/remove_contact_groups/editor/single-group.png +0 -0
  130. package/screenshots/truth/actions/send_email/editor/complex-business-email.png +0 -0
  131. package/screenshots/truth/actions/send_email/editor/multiple-recipients.png +0 -0
  132. package/screenshots/truth/actions/send_msg/editor/long-quick-replies.png +0 -0
  133. package/screenshots/truth/actions/send_msg/editor/multiline-text-with-replies.png +0 -0
  134. package/screenshots/truth/actions/send_msg/editor/simple-text.png +0 -0
  135. package/screenshots/truth/actions/send_msg/editor/text-with-linebreaks.png +0 -0
  136. package/screenshots/truth/actions/send_msg/editor/text-with-many-quick-replies.png +0 -0
  137. package/screenshots/truth/actions/send_msg/editor/text-with-quick-replies.png +0 -0
  138. package/screenshots/truth/actions/send_msg/editor/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-no-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/colorpicker/default.png +0 -0
  145. package/screenshots/truth/colorpicker/focused.png +0 -0
  146. package/screenshots/truth/colorpicker/initialized.png +0 -0
  147. package/screenshots/truth/colorpicker/selected.png +0 -0
  148. package/screenshots/truth/editor/router.png +0 -0
  149. package/screenshots/truth/editor/send_msg.png +0 -0
  150. package/screenshots/truth/editor/set_contact_language.png +0 -0
  151. package/screenshots/truth/editor/set_contact_name.png +0 -0
  152. package/screenshots/truth/editor/set_run_result.png +0 -0
  153. package/screenshots/truth/editor/wait.png +0 -0
  154. package/screenshots/truth/field-renderer/checkbox-checked.png +0 -0
  155. package/screenshots/truth/field-renderer/checkbox-unchecked.png +0 -0
  156. package/screenshots/truth/field-renderer/checkbox-with-errors.png +0 -0
  157. package/screenshots/truth/field-renderer/context-comparison.png +0 -0
  158. package/screenshots/truth/field-renderer/key-value-with-label.png +0 -0
  159. package/screenshots/truth/field-renderer/message-editor-with-label.png +0 -0
  160. package/screenshots/truth/field-renderer/select-multi.png +0 -0
  161. package/screenshots/truth/field-renderer/select-no-label.png +0 -0
  162. package/screenshots/truth/field-renderer/select-with-label.png +0 -0
  163. package/screenshots/truth/field-renderer/text-evaluated.png +0 -0
  164. package/screenshots/truth/field-renderer/text-no-label.png +0 -0
  165. package/screenshots/truth/field-renderer/text-with-errors.png +0 -0
  166. package/screenshots/truth/field-renderer/text-with-label.png +0 -0
  167. package/screenshots/truth/field-renderer/textarea-evaluated.png +0 -0
  168. package/screenshots/truth/field-renderer/textarea-with-label.png +0 -0
  169. package/screenshots/truth/integration/checkbox-markdown-errors.png +0 -0
  170. package/screenshots/truth/nodes/split_by_llm_categorize/editor/basic-categorization.png +0 -0
  171. package/screenshots/truth/nodes/split_by_llm_categorize/editor/custom-input-and-result-name.png +0 -0
  172. package/screenshots/truth/nodes/split_by_llm_categorize/editor/feedback-categorization.png +0 -0
  173. package/screenshots/truth/nodes/split_by_llm_categorize/editor/many-categories.png +0 -0
  174. package/screenshots/truth/nodes/split_by_llm_categorize/editor/minimal-categories.png +0 -0
  175. package/screenshots/truth/nodes/split_by_llm_categorize/render/basic-categorization.png +0 -0
  176. package/screenshots/truth/nodes/split_by_llm_categorize/render/custom-input-and-result-name.png +0 -0
  177. package/screenshots/truth/nodes/split_by_llm_categorize/render/feedback-categorization.png +0 -0
  178. package/screenshots/truth/nodes/split_by_llm_categorize/render/many-categories.png +0 -0
  179. package/screenshots/truth/nodes/split_by_llm_categorize/render/minimal-categories.png +0 -0
  180. package/screenshots/truth/nodes/split_by_random/editor/ab-test-multiple-variants.png +0 -0
  181. package/screenshots/truth/nodes/split_by_random/editor/sampling-split.png +0 -0
  182. package/screenshots/truth/nodes/split_by_random/editor/three-way-split.png +0 -0
  183. package/screenshots/truth/nodes/split_by_random/editor/two-bucket-split.png +0 -0
  184. package/screenshots/truth/nodes/split_by_random/render/ab-test-multiple-variants.png +0 -0
  185. package/screenshots/truth/nodes/split_by_random/render/sampling-split.png +0 -0
  186. package/screenshots/truth/nodes/split_by_random/render/three-way-split.png +0 -0
  187. package/screenshots/truth/nodes/split_by_random/render/two-bucket-split.png +0 -0
  188. package/screenshots/truth/nodes/wait_for_digits/editor/basic-digits-wait.png +0 -0
  189. package/screenshots/truth/nodes/wait_for_digits/editor/phone-number-collection.png +0 -0
  190. package/screenshots/truth/nodes/wait_for_digits/editor/single-digit-with-timeout.png +0 -0
  191. package/screenshots/truth/nodes/wait_for_digits/editor/verification-code.png +0 -0
  192. package/screenshots/truth/nodes/wait_for_digits/render/basic-digits-wait.png +0 -0
  193. package/screenshots/truth/nodes/wait_for_digits/render/phone-number-collection.png +0 -0
  194. package/screenshots/truth/nodes/wait_for_digits/render/single-digit-with-timeout.png +0 -0
  195. package/screenshots/truth/nodes/wait_for_digits/render/verification-code.png +0 -0
  196. package/screenshots/truth/nodes/wait_for_response/editor/basic-wait.png +0 -0
  197. package/screenshots/truth/nodes/wait_for_response/editor/custom-result-name.png +0 -0
  198. package/screenshots/truth/nodes/wait_for_response/editor/no-timeout.png +0 -0
  199. package/screenshots/truth/nodes/wait_for_response/editor/short-timeout.png +0 -0
  200. package/screenshots/truth/nodes/wait_for_response/render/basic-wait.png +0 -0
  201. package/screenshots/truth/nodes/wait_for_response/render/custom-result-name.png +0 -0
  202. package/screenshots/truth/nodes/wait_for_response/render/no-timeout.png +0 -0
  203. package/screenshots/truth/nodes/wait_for_response/render/short-timeout.png +0 -0
  204. package/screenshots/truth/omnibox/selected.png +0 -0
  205. package/screenshots/truth/run-list/basic.png +0 -0
  206. package/screenshots/truth/select/functions.png +0 -0
  207. package/screenshots/truth/select/multi-with-endpoint.png +0 -0
  208. package/screenshots/truth/select/search-enabled.png +0 -0
  209. package/src/events.ts +12 -6
  210. package/src/excellent/helpers.ts +2 -2
  211. package/src/flow/CanvasNode.ts +22 -1
  212. package/src/flow/Editor.ts +12 -1
  213. package/src/flow/NodeEditor.ts +186 -374
  214. package/src/flow/actions/add_input_labels.ts +45 -0
  215. package/src/flow/actions/call_llm.ts +57 -3
  216. package/src/flow/actions/call_webhook.ts +1 -1
  217. package/src/flow/actions/open_ticket.ts +74 -3
  218. package/src/flow/actions/set_run_result.ts +83 -0
  219. package/src/flow/config.ts +4 -0
  220. package/src/flow/nodes/split_by_llm_categorize.ts +277 -0
  221. package/src/flow/nodes/split_by_ticket.ts +19 -0
  222. package/src/flow/nodes/wait_for_response.ts +28 -1
  223. package/src/flow/types.ts +26 -127
  224. package/src/form/ArrayEditor.ts +79 -139
  225. package/src/form/BaseListEditor.ts +4 -4
  226. package/src/form/Checkbox.ts +81 -24
  227. package/src/form/ColorPicker.ts +31 -43
  228. package/src/form/Completion.ts +49 -56
  229. package/src/form/Compose.ts +8 -8
  230. package/src/form/ContactSearch.ts +3 -4
  231. package/src/form/DatePicker.ts +32 -38
  232. package/src/form/{FormField.ts → FieldElement.ts} +108 -55
  233. package/src/form/FieldRenderer.ts +466 -0
  234. package/src/form/ImagePicker.ts +107 -110
  235. package/src/form/KeyValueEditor.ts +43 -39
  236. package/src/form/MessageEditor.ts +61 -67
  237. package/src/form/TembaSlider.ts +3 -3
  238. package/src/form/TemplateEditor.ts +3 -3
  239. package/src/form/TextInput.ts +26 -29
  240. package/src/form/select/Select.ts +63 -37
  241. package/src/form/select/UserSelect.ts +10 -11
  242. package/src/form/select/WorkspaceSelect.ts +9 -10
  243. package/src/live/ContactChat.ts +62 -47
  244. package/src/live/ContactFieldEditor.ts +2 -2
  245. package/src/markdown.ts +19 -11
  246. package/src/store/flow-definition.d.ts +5 -2
  247. package/static/api/labels.json +31 -0
  248. package/static/api/topics.json +24 -9
  249. package/static/api/users.json +35 -16
  250. package/static/css/temba-components.css +3 -3
  251. package/stress-test.js +18 -13
  252. package/temba-modules.ts +3 -2
  253. package/test/ActionHelper.ts +2 -0
  254. package/test/NodeHelper.ts +184 -0
  255. package/test/actions/call_llm.test.ts +137 -0
  256. package/test/nodes/README.md +78 -0
  257. package/test/nodes/split_by_llm_categorize.test.ts +698 -0
  258. package/test/nodes/split_by_random.test.ts +177 -0
  259. package/test/nodes/wait_for_digits.test.ts +176 -0
  260. package/test/nodes/wait_for_response.test.ts +206 -0
  261. package/test/temba-add-input-labels.test.ts +87 -0
  262. package/test/temba-checkbox.test.ts +26 -0
  263. package/test/temba-field-renderer.test.ts +482 -0
  264. package/test/temba-integration-markdown.test.ts +2 -4
  265. package/test/temba-markdown.test.ts +1 -1
  266. package/test/temba-node-editor.test.ts +496 -0
  267. package/test/temba-select.test.ts +6 -6
  268. package/test/temba-slider.test.ts +0 -1
  269. package/test/temba-webchat.test.ts +1 -1
  270. package/test-assets/contacts/history.json +7 -20
  271. package/test-assets/select/llms.json +18 -0
  272. package/web-dev-mock.mjs +96 -6
  273. package/web-dev-server.config.mjs +29 -7
  274. package/out-tsc/src/form/FormElement.js +0 -67
  275. package/out-tsc/src/form/FormElement.js.map +0 -1
  276. package/out-tsc/src/form/FormField.js.map +0 -1
  277. package/out-tsc/test/temba-formfield.test.js +0 -94
  278. package/out-tsc/test/temba-formfield.test.js.map +0 -1
  279. package/src/form/FormElement.ts +0 -69
  280. package/test/temba-flow-editor.test.ts.backup +0 -563
  281. package/test/temba-formfield.test.ts +0 -121
  282. package/test/temba-utils-index.test.ts.backup +0 -1737
package/src/flow/types.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { TemplateResult } from 'lit-html';
2
- import { Action } from '../store/flow-definition';
2
+ import { Action, Node } from '../store/flow-definition';
3
3
 
4
4
  export interface ValidationResult {
5
5
  valid: boolean;
@@ -65,39 +65,16 @@ export interface SliderAttributes {
65
65
  range?: boolean;
66
66
  }
67
67
 
68
- // Widget configuration using discriminated union for type safety
69
- export type WidgetConfig =
70
- | { type: 'temba-textinput'; attributes?: TextInputAttributes }
71
- | { type: 'temba-completion'; attributes?: CompletionAttributes }
72
- | { type: 'temba-select'; attributes?: SelectAttributes }
73
- | { type: 'temba-checkbox'; attributes?: CheckboxAttributes }
74
- | { type: 'temba-slider'; attributes?: SliderAttributes }
75
- | { type: string; attributes?: { [key: string]: any } }; // Generic fallback
68
+ export interface FormData extends Record<string, any> {}
76
69
 
77
- // Property configuration with the clean structure you want
78
- export interface PropertyConfig {
79
- // Form field metadata
80
- label?: string;
81
- helpText?: string;
82
- required?: boolean;
83
- maxLength?: number;
84
- minLength?: number;
85
- pattern?: string;
86
-
87
- // Widget configuration
88
- widget: WidgetConfig;
89
-
90
- // Conditional behavior based on other field values
91
- conditions?: {
92
- // When to show this field
93
- visible?: (formData: any) => boolean;
94
-
95
- // When this field is disabled
96
- disabled?: (formData: any) => boolean;
97
- };
70
+ export interface FormConfig {
71
+ form?: Record<string, FieldConfig>;
72
+ layout?: LayoutItem[];
73
+ sanitize?: (formData: FormData) => void;
74
+ validate?: (formData: FormData) => ValidationResult;
98
75
  }
99
76
 
100
- export interface NodeConfig {
77
+ export interface NodeConfig extends FormConfig {
101
78
  type: string;
102
79
  name?: string;
103
80
  color?: string;
@@ -108,22 +85,28 @@ export interface NodeConfig {
108
85
  operand?: string;
109
86
  configurable?: boolean; // can the rules be configured in the UI
110
87
  rules?: {
111
- type: 'has_number_between' | 'has_string' | 'has_value' | 'has_not_value';
88
+ type:
89
+ | 'has_number_between'
90
+ | 'has_string'
91
+ | 'has_value'
92
+ | 'has_not_value'
93
+ | 'has_text';
112
94
  arguments: string[];
113
95
  categoryName: string;
114
96
  }[];
115
97
  };
116
- properties?: { [key: string]: PropertyConfig };
117
- toFormData?: (node: any) => any;
118
- fromFormData?: (formData: any, originalNode: any) => any;
98
+
99
+ toFormData?: (node: Node) => FormData;
100
+ fromFormData?: (formData: FormData, originalNode: Node) => Node;
101
+ render?: (node: Node) => TemplateResult;
119
102
  }
120
103
 
121
104
  // New field configuration system for generic form generation
122
105
  export interface BaseFieldConfig {
123
106
  label?: string;
124
107
  required?: boolean;
125
- evaluated?: boolean; // if this field supports expression evaluation
126
- dependsOn?: string[]; // fields this field depends on
108
+ evaluated?: boolean;
109
+ dependsOn?: string[];
127
110
  computeValue?: (
128
111
  values: Record<string, any>,
129
112
  currentValue: any,
@@ -137,7 +120,7 @@ export interface BaseFieldConfig {
137
120
  helpText?: string;
138
121
 
139
122
  // Layout properties
140
- maxWidth?: string; // CSS max-width value (e.g., '200px', '50%', '10rem')
123
+ maxWidth?: string;
141
124
 
142
125
  // Conditional rendering
143
126
  conditions?: {
@@ -160,7 +143,7 @@ export interface TextareaFieldConfig extends BaseFieldConfig {
160
143
 
161
144
  export interface SelectFieldConfig extends BaseFieldConfig {
162
145
  type: 'select';
163
- options: string[] | { value: string; label: string }[];
146
+ options?: string[] | { value: string; label: string }[];
164
147
  multi?: boolean;
165
148
  clearable?: boolean;
166
149
  searchable?: boolean;
@@ -171,7 +154,10 @@ export interface SelectFieldConfig extends BaseFieldConfig {
171
154
  nameKey?: string;
172
155
  endpoint?: string;
173
156
  emails?: boolean;
157
+ getName?: (item: any) => string;
174
158
  flavor?: 'small' | 'large';
159
+ createArbitraryOption?: (input: string, options: any[]) => any;
160
+ allowCreate?: boolean;
175
161
  }
176
162
 
177
163
  export interface KeyValueFieldConfig extends BaseFieldConfig {
@@ -256,7 +242,7 @@ export type LayoutItem =
256
242
  | GroupLayoutConfig
257
243
  | string; // string is shorthand for field
258
244
 
259
- export interface ActionConfig {
245
+ export interface ActionConfig extends FormConfig {
260
246
  name: string;
261
247
  color: string;
262
248
  evaluated?: string[];
@@ -265,13 +251,8 @@ export interface ActionConfig {
265
251
  form?: Record<string, FieldConfig>;
266
252
  layout?: LayoutItem[]; // optional layout configuration - array of layout items
267
253
 
268
- // Action editor configuration (legacy)
269
- // Form-level transformations
270
- sanitize?: (formData: any) => any;
271
254
  toFormData?: (action: Action) => any;
272
255
  fromFormData?: (formData: any) => Action;
273
-
274
- validate?: (action: Action) => ValidationResult;
275
256
  }
276
257
 
277
258
  export const COLORS = {
@@ -287,85 +268,3 @@ export const COLORS = {
287
268
  add: '#309c42',
288
269
  remove: '#e74c3c'
289
270
  };
290
-
291
- // Default property type mappings
292
- export function getDefaultComponent(value: any): WidgetConfig['type'] {
293
- if (typeof value === 'boolean') {
294
- return 'temba-checkbox';
295
- }
296
- if (typeof value === 'number') {
297
- return 'temba-textinput';
298
- }
299
- if (Array.isArray(value)) {
300
- return 'temba-select'; // For arrays, use multi-select
301
- }
302
- // Default to text input for strings and unknown types
303
- return 'temba-textinput';
304
- }
305
-
306
- // Get component properties for default mappings with proper typing
307
- export function getDefaultComponentProps(value: any): PropertyConfig {
308
- if (typeof value === 'boolean') {
309
- return {
310
- widget: { type: 'temba-checkbox' }
311
- };
312
- }
313
- if (typeof value === 'number') {
314
- return {
315
- widget: {
316
- type: 'temba-textinput',
317
- attributes: { type: 'number' }
318
- }
319
- };
320
- }
321
- if (Array.isArray(value)) {
322
- if (value.length > 0 && typeof value[0] === 'string') {
323
- return {
324
- widget: {
325
- type: 'temba-select',
326
- attributes: { multi: true, tags: true }
327
- }
328
- };
329
- }
330
- return {
331
- widget: {
332
- type: 'temba-select',
333
- attributes: { multi: true }
334
- }
335
- };
336
- }
337
- return {
338
- widget: { type: 'temba-textinput' }
339
- };
340
- }
341
-
342
- // Type guard functions for working with WidgetConfig
343
- export function isTextInputWidget(
344
- config: WidgetConfig
345
- ): config is { type: 'temba-textinput'; attributes?: TextInputAttributes } {
346
- return config.type === 'temba-textinput';
347
- }
348
-
349
- export function isCompletionWidget(
350
- config: WidgetConfig
351
- ): config is { type: 'temba-completion'; attributes?: CompletionAttributes } {
352
- return config.type === 'temba-completion';
353
- }
354
-
355
- export function isSelectWidget(
356
- config: WidgetConfig
357
- ): config is { type: 'temba-select'; attributes?: SelectAttributes } {
358
- return config.type === 'temba-select';
359
- }
360
-
361
- export function isCheckboxWidget(
362
- config: WidgetConfig
363
- ): config is { type: 'temba-checkbox'; attributes?: CheckboxAttributes } {
364
- return config.type === 'temba-checkbox';
365
- }
366
-
367
- export function isSliderWidget(
368
- config: WidgetConfig
369
- ): config is { type: 'slider'; attributes?: SliderAttributes } {
370
- return config.type === 'temba-slider';
371
- }
@@ -2,6 +2,7 @@ import { html, css, TemplateResult } from 'lit';
2
2
  import { customElement, property } from 'lit/decorators.js';
3
3
  import { FieldConfig, SelectFieldConfig } from '../flow/types';
4
4
  import { BaseListEditor, ListItem } from './BaseListEditor';
5
+ import { FieldRenderer } from './FieldRenderer';
5
6
 
6
7
  @customElement('temba-array-editor')
7
8
  export class TembaArrayEditor extends BaseListEditor<ListItem> {
@@ -125,94 +126,49 @@ export class TembaArrayEditor extends BaseListEditor<ListItem> {
125
126
  return currentValue;
126
127
  }
127
128
 
128
- private renderField(
129
+ private renderArrayField(
129
130
  itemIndex: number,
130
131
  fieldName: string,
131
132
  config: FieldConfig
132
133
  ): TemplateResult {
133
134
  const computedValue = this.computeFieldValue(itemIndex, fieldName, config);
134
135
 
135
- switch (config.type) {
136
- case 'text':
137
- return html`<temba-textinput
138
- .value=${computedValue || ''}
139
- .placeholder=${config.placeholder}
140
- @change=${(e: any) =>
141
- this.handleFieldChange(itemIndex, fieldName, e.target.value)}
142
- ></temba-textinput>`;
143
-
144
- case 'textarea':
145
- return html`<temba-textinput
146
- .value=${computedValue || ''}
147
- .placeholder=${config.placeholder}
148
- textarea
149
- .rows=${config.rows || 3}
150
- @change=${(e: any) =>
151
- this.handleFieldChange(itemIndex, fieldName, e.target.value)}
152
- ></temba-textinput>`;
153
-
154
- case 'select': {
155
- const selectConfig = config as SelectFieldConfig;
156
- const fieldValue = this.computeFieldValue(itemIndex, fieldName, config);
157
-
158
- return html`<temba-select
159
- class="form-control"
160
- ?clearable="${selectConfig.clearable || false}"
161
- ?searchable="${selectConfig.searchable || false}"
162
- ?tags="${selectConfig.tags || false}"
163
- ?multi="${selectConfig.multi || false}"
164
- ?emails="${selectConfig.emails || false}"
165
- placeholder="${selectConfig.placeholder || ''}"
166
- maxItems="${selectConfig.maxItems || 0}"
167
- valueKey="${selectConfig.valueKey || 'value'}"
168
- nameKey="${selectConfig.nameKey || 'name'}"
169
- endpoint="${selectConfig.endpoint || ''}"
170
- value="${fieldValue || ''}"
171
- flavor="small"
172
- @change="${(e: Event) => {
173
- const target = e.target as any;
174
- let value: any;
175
-
176
- // For temba-select, extract the correct value
177
- if (target.tagName === 'TEMBA-SELECT') {
178
- if (target.multi || target.emails || target.tags) {
179
- value = target.values || [];
180
- } else {
181
- // Single select: extract value from first selected option
182
- const values = target.values || [];
183
- value =
184
- values.length > 0 && values[0]
185
- ? values[0].value !== undefined
186
- ? values[0].value
187
- : values[0]
188
- : '';
189
- }
136
+ // Use FieldRenderer for consistent field rendering
137
+ return FieldRenderer.renderField(fieldName, config, computedValue, {
138
+ showLabel: false, // ArrayEditor doesn't show labels for individual fields
139
+ flavor: 'small', // ArrayEditor uses small flavor
140
+ extraClasses: 'form-control',
141
+ onChange: (e: Event) => {
142
+ let value: any;
143
+ const target = e.target as any;
144
+
145
+ // Handle different field types and their change events
146
+ if (config.type === 'select') {
147
+ // For temba-select, extract the correct value
148
+ if (target.tagName === 'TEMBA-SELECT') {
149
+ if (target.multi || target.emails || target.tags) {
150
+ value = target.values || [];
190
151
  } else {
191
- value = target.value;
152
+ // Single select: extract value from first selected option
153
+ const values = target.values || [];
154
+ value =
155
+ values.length > 0 && values[0]
156
+ ? values[0].value !== undefined
157
+ ? values[0].value
158
+ : values[0]
159
+ : '';
192
160
  }
193
-
194
- this.handleFieldChange(itemIndex, fieldName, value);
195
- }}"
196
- >
197
- ${selectConfig.options?.map((option: any) => {
198
- if (typeof option === 'string') {
199
- return html`<temba-option
200
- name="${option}"
201
- value="${option}"
202
- ></temba-option>`;
203
- } else {
204
- return html`<temba-option
205
- name="${option.label || option.name}"
206
- value="${option.value}"
207
- ></temba-option>`;
208
- }
209
- })}
210
- </temba-select>`;
161
+ } else {
162
+ value = target.value;
163
+ }
164
+ } else {
165
+ // For other field types, use the target value directly
166
+ value = target.value;
167
+ }
168
+
169
+ this.handleFieldChange(itemIndex, fieldName, value);
211
170
  }
212
-
213
- default:
214
- return html`<span>Unsupported field type: ${config.type}</span>`;
215
- }
171
+ });
216
172
  }
217
173
 
218
174
  renderItem(item: ListItem, index: number): TemplateResult {
@@ -224,7 +180,7 @@ export class TembaArrayEditor extends BaseListEditor<ListItem> {
224
180
  ${Object.entries(this.itemConfig).map(
225
181
  ([fieldName, config]) => html`
226
182
  <div class="field">
227
- ${this.renderField(index, fieldName, config)}
183
+ ${this.renderArrayField(index, fieldName, config)}
228
184
  </div>
229
185
  `
230
186
  )}
@@ -247,72 +203,56 @@ export class TembaArrayEditor extends BaseListEditor<ListItem> {
247
203
  return 'array-editor';
248
204
  }
249
205
 
250
- protected renderAddButton(): TemplateResult {
251
- return html`
252
- <button class="add-btn" @click=${() => this.addItem()}>
253
- Add ${this.itemLabel}
254
- </button>
255
- `;
256
- }
206
+ static get styles() {
207
+ return css`
208
+ ${super.styles}
257
209
 
258
- static styles = css`
259
- .array-editor {
260
- }
261
-
262
- .array-item {
263
- }
264
-
265
- .item-header {
266
- display: flex;
267
- justify-content: space-between;
268
- align-items: center;
269
- }
210
+ .array-editor {
211
+ }
270
212
 
271
- .item-title {
272
- font-weight: 600;
273
- color: #333;
274
- }
213
+ .array-item {
214
+ }
275
215
 
276
- .item-fields {
277
- display: flex;
278
- gap: 12px;
279
- align-items: center;
280
- }
216
+ .item-header {
217
+ display: flex;
218
+ justify-content: space-between;
219
+ align-items: center;
220
+ }
281
221
 
282
- .field {
283
- flex: 1;
284
- }
222
+ .item-title {
223
+ font-weight: 600;
224
+ color: #333;
225
+ }
285
226
 
286
- .field:first-child {
287
- flex: 0 0 140px; /* Fixed width for type dropdown */
288
- }
227
+ .item-fields {
228
+ display: flex;
229
+ gap: 12px;
230
+ align-items: center;
231
+ }
289
232
 
290
- .field label {
291
- display: block;
292
- margin-bottom: 4px;
293
- font-weight: 500;
294
- color: #555;
295
- font-size: 14px;
296
- }
233
+ .field {
234
+ flex: 1;
235
+ }
297
236
 
298
- .add-btn,
299
- .remove-btn {
300
- padding: 8px;
301
- border: 1px solid #ccc;
302
- border-radius: 4px;
303
- background: white;
304
- cursor: pointer;
305
- font-size: 14px;
306
- }
237
+ .add-btn,
238
+ .remove-btn {
239
+ padding: 8px;
240
+ border: 1px solid #ccc;
241
+ border-radius: 4px;
242
+ background: white;
243
+ cursor: pointer;
244
+ font-size: 14px;
245
+ }
307
246
 
308
- .add-btn:hover,
309
- .remove-btn:hover {
310
- background: #f8f8f8;
311
- }
247
+ .add-btn:hover,
248
+ .remove-btn:hover {
249
+ background: #f8f8f8;
250
+ }
312
251
 
313
- .remove-btn {
314
- background: #fefefe;
315
- color: #999;
316
- }
317
- `;
252
+ .remove-btn {
253
+ background: #fefefe;
254
+ color: #999;
255
+ }
256
+ `;
257
+ }
318
258
  }
@@ -1,5 +1,6 @@
1
- import { LitElement, TemplateResult, html } from 'lit';
1
+ import { TemplateResult, html } from 'lit';
2
2
  import { property } from 'lit/decorators.js';
3
+ import { FieldElement } from './FieldElement';
3
4
 
4
5
  export interface ListItem {
5
6
  [key: string]: any;
@@ -22,7 +23,7 @@ export interface ListEditorConfig {
22
23
 
23
24
  export abstract class BaseListEditor<
24
25
  T extends ListItem = ListItem
25
- > extends LitElement {
26
+ > extends FieldElement {
26
27
  @property({ attribute: false })
27
28
  protected _items: T[] = [];
28
29
 
@@ -60,9 +61,8 @@ export abstract class BaseListEditor<
60
61
  return !this.maxItems || this._items.length < this.maxItems;
61
62
  }
62
63
 
63
- render(): TemplateResult {
64
+ renderWidget(): TemplateResult {
64
65
  const items = this.displayItems;
65
-
66
66
  return html`
67
67
  <div class=${this.getContainerClass()}>
68
68
  <div
@@ -1,11 +1,14 @@
1
1
  import { TemplateResult, html, css } from 'lit';
2
- import { FormElement } from './FormElement';
2
+ import { FieldElement } from './FieldElement';
3
3
  import { property } from 'lit/decorators.js';
4
4
  import { Icon } from '../Icons';
5
+ import { renderMarkdownInline } from '../markdown';
5
6
 
6
- export class Checkbox extends FormElement {
7
+ export class Checkbox extends FieldElement {
7
8
  static get styles() {
8
9
  return css`
10
+ ${super.styles}
11
+
9
12
  :host {
10
13
  color: var(--color-text);
11
14
  display: inline-block;
@@ -24,25 +27,45 @@ export class Checkbox extends FormElement {
24
27
  background: var(--checkbox-hover-bg, #f9f9f9);
25
28
  }
26
29
 
27
- temba-field {
28
- --help-text-margin-left: 24px;
29
- cursor: pointer;
30
- }
31
-
32
30
  .checkbox-container {
33
31
  cursor: pointer;
34
32
  display: flex;
33
+ align-items: flex-start;
35
34
  user-select: none;
36
35
  -webkit-user-select: none;
37
36
  }
38
37
 
38
+ .checkbox-container temba-icon {
39
+ align-self: flex-start;
40
+ vertical-align: top;
41
+ line-height: 1;
42
+ }
43
+
44
+ .label-and-help {
45
+ flex-grow: 1;
46
+ margin-left: 8px;
47
+ }
48
+
39
49
  .checkbox-label {
40
50
  font-family: var(--font-family);
41
51
  padding: 0px;
42
- margin-left: 8px;
52
+ margin: 0px;
43
53
  font-size: 14px;
44
54
  line-height: 19px;
45
- flex-grow: 1;
55
+ }
56
+
57
+ .checkbox-help-text {
58
+ font-family: var(--font-family);
59
+ font-size: var(--help-text-size, 0.85em);
60
+ line-height: normal;
61
+ color: var(--color-text-help);
62
+ margin-top: 4px;
63
+ opacity: 1;
64
+ }
65
+
66
+ /* Checkbox help text should align with the checkbox icon, not indented */
67
+ .help-text {
68
+ margin-left: 0;
46
69
  }
47
70
 
48
71
  .far {
@@ -123,7 +146,7 @@ export class Checkbox extends FormElement {
123
146
  super.click();
124
147
  }
125
148
 
126
- public render(): TemplateResult {
149
+ protected renderWidget(): TemplateResult {
127
150
  const icon = html`<temba-icon
128
151
  name="${this.checked
129
152
  ? Icon.checkbox_checked
@@ -132,27 +155,61 @@ export class Checkbox extends FormElement {
132
155
  : Icon.checkbox}"
133
156
  size="${this.size}"
134
157
  animatechange="${this.animateChange}"
135
- />`;
158
+ ></temba-icon>`;
136
159
 
137
160
  return html`
138
- <div class="wrapper ${this.label ? 'label' : ''}">
139
- <temba-field
140
- name=${this.name}
141
- .helpText=${this.helpText}
142
- .errors=${this.errors}
143
- .widgetOnly=${this.widgetOnly}
144
- .helpAlways=${true}
145
- ?disabled=${this.disabled}
146
- @click=${this.handleClick}
147
- >
148
- <div class="checkbox-container ${this.disabled ? 'disabled' : ''}">
149
- ${icon}
161
+ <div
162
+ class="wrapper ${this.label ? 'label' : ''}"
163
+ @click=${this.handleClick}
164
+ >
165
+ <div class="checkbox-container ${this.disabled ? 'disabled' : ''}">
166
+ ${icon}
167
+ <div class="label-and-help">
150
168
  ${this.label && String(this.label).trim()
151
169
  ? html`<div class="checkbox-label">${this.label}</div>`
152
170
  : null}
171
+ ${this.helpText && this.helpText !== 'None'
172
+ ? html` <div class="checkbox-help-text">${this.helpText}</div> `
173
+ : null}
153
174
  </div>
154
- </temba-field>
175
+ </div>
176
+ </div>
177
+ `;
178
+ }
179
+
180
+ protected renderField(): TemplateResult {
181
+ // Use standard FieldElement behavior but skip the field label since checkbox renders its own inline
182
+ const hasErrors = !this.hideErrors && this.errors && this.errors.length > 0;
183
+ const errors = hasErrors
184
+ ? this.errors.map((error: string) => {
185
+ return html`
186
+ <div class="alert-error">${renderMarkdownInline(error)}</div>
187
+ `;
188
+ })
189
+ : [];
190
+
191
+ if (this.widgetOnly) {
192
+ return html`
193
+ <div class="${this.disabled ? 'disabled' : ''}">
194
+ ${this.renderWidget()}
195
+ </div>
196
+ ${errors}
197
+ `;
198
+ }
199
+
200
+ // This matches FieldElement.renderField() but without the field label
201
+ return html`
202
+ <div
203
+ class="field ${this.disabled ? 'disabled' : ''} ${hasErrors
204
+ ? 'has-error'
205
+ : ''}"
206
+ >
207
+ <div class="widget">${this.renderWidget()} ${errors}</div>
155
208
  </div>
156
209
  `;
157
210
  }
211
+
212
+ public render(): TemplateResult {
213
+ return this.renderField();
214
+ }
158
215
  }