@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
@@ -1 +1 @@
1
- {"version":3,"file":"call_webhook.js","sourceRoot":"","sources":["../../../../src/flow/actions/call_webhook.ts"],"names":[],"mappings":"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","sourcesContent":["import { html } from 'lit-html';\nimport { ActionConfig, COLORS } from '../types';\nimport { Node, CallWebhook } from '../../store/flow-definition';\n\nexport const call_webhook: ActionConfig = {\n name: 'Call Webhook',\n color: COLORS.call,\n render: (_node: Node, action: CallWebhook) => {\n return html`<div\n style=\"word-wrap: break-word; overflow-wrap: break-word; hyphens: auto;\"\n >\n ${action.url}\n </div>`;\n },\n evaluated: ['url', 'headers', 'body'], // keep for backward compatibility\n form: {\n method: {\n type: 'select',\n required: true,\n options: ['GET', 'POST', 'PUT', 'DELETE', 'HEAD', 'PATCH'],\n maxWidth: '120px',\n searchable: true\n },\n url: {\n type: 'text',\n required: true,\n evaluated: true,\n placeholder: 'https://example.com/webhook'\n },\n headers: {\n type: 'key-value',\n sortable: true,\n keyPlaceholder: 'Header name',\n valuePlaceholder: 'Header value',\n minRows: 0\n },\n body: {\n type: 'textarea',\n evaluated: true,\n placeholder: 'Request body content (JSON, XML, etc.)',\n minHeight: 200,\n dependsOn: ['method'],\n computeValue: (\n values: Record<string, any>,\n currentValue: any,\n originalValues?: Record<string, any>\n ) => {\n // Check if method is POST (handle both string and select object formats)\n const method =\n Array.isArray(values.method) && values.method.length > 0\n ? values.method[0].value || values.method[0].name\n : values.method;\n\n const defaultTemplate = `@(json(object(\n \"contact\", object(\n \"uuid\", contact.uuid, \n \"name\", contact.name, \n \"urn\", contact.urn\n ),\n \"flow\", object(\n \"uuid\", run.flow.uuid, \n \"name\", run.flow.name\n ),\n \"results\", foreach_value(results, extract_object, \"value\", \"category\")\n)))`;\n\n if (method === 'POST') {\n // For POST, provide the template if body is empty or was never set by user\n if (!currentValue || currentValue.trim() === '') {\n return defaultTemplate;\n }\n } else {\n // For non-POST methods, clear the body if it was auto-generated or empty\n // Check if the original body was empty (user never specified a body)\n const originalBody = originalValues?.body || '';\n const isOriginallyEmpty = !originalBody || originalBody.trim() === '';\n\n // Clear if: originally empty, contains default template, or is currently empty\n if (\n isOriginallyEmpty ||\n !currentValue ||\n currentValue.trim() === '' ||\n currentValue.trim() === defaultTemplate.trim()\n ) {\n return '';\n }\n }\n\n return currentValue; // Keep existing value if user has customized it\n }\n }\n },\n layout: [\n // Row with method and URL side by side\n { type: 'row', items: ['method', 'url'] },\n // Advanced group with nested layouts\n {\n type: 'group',\n label: 'Headers',\n items: ['headers'],\n collapsible: true,\n collapsed: true,\n helpText: 'Configure authentication or custom headers'\n },\n {\n type: 'group',\n label: 'Body',\n items: ['body'],\n collapsible: true,\n collapsed: true,\n helpText: 'Configure the request payload'\n }\n ],\n toFormData: (action: CallWebhook) => {\n return {\n uuid: action.uuid,\n url: action.url || '',\n method: [{ value: action.method, name: action.method }],\n headers: action.headers || [],\n body: action.body || ''\n };\n },\n fromFormData: (data: Record<string, any>) => {\n return {\n uuid: data.uuid,\n type: 'call_webhook',\n url: data.url,\n method: data.method[0].value,\n headers: data.headers || [],\n body: data.body || ''\n } as CallWebhook;\n }\n};\n"]}
1
+ {"version":3,"file":"call_webhook.js","sourceRoot":"","sources":["../../../../src/flow/actions/call_webhook.ts"],"names":[],"mappings":"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","sourcesContent":["import { html } from 'lit-html';\nimport { ActionConfig, COLORS } from '../types';\nimport { Node, CallWebhook } from '../../store/flow-definition';\n\nconst defaultPost = `@(json(object(\n \"contact\", object(\n \"uuid\", contact.uuid, \n \"name\", contact.name, \n \"urn\", contact.urn\n ),\n \"flow\", object(\n \"uuid\", run.flow.uuid, \n \"name\", run.flow.name\n ),\n \"results\", foreach_value(results, extract_object, \"value\", \"category\")\n)))`;\n\nexport const call_webhook: ActionConfig = {\n name: 'Call Webhook',\n color: COLORS.call,\n render: (_node: Node, action: CallWebhook) => {\n return html`<div\n style=\"word-wrap: break-word; overflow-wrap: break-word; hyphens: auto;\"\n >\n ${action.url}\n </div>`;\n },\n evaluated: ['url', 'headers', 'body'], // keep for backward compatibility\n form: {\n method: {\n type: 'select',\n required: true,\n options: ['GET', 'POST', 'PUT', 'DELETE', 'HEAD', 'PATCH'],\n maxWidth: '120px',\n searchable: false\n },\n url: {\n type: 'text',\n required: true,\n evaluated: true,\n placeholder: 'https://example.com/webhook'\n },\n headers: {\n type: 'key-value',\n sortable: true,\n keyPlaceholder: 'Header name',\n valuePlaceholder: 'Header value',\n minRows: 0\n },\n body: {\n type: 'textarea',\n evaluated: true,\n placeholder: 'Request body content (JSON, XML, etc.)',\n minHeight: 200,\n dependsOn: ['method'],\n computeValue: (\n values: Record<string, any>,\n currentValue: any,\n originalValues?: Record<string, any>\n ) => {\n // Check if method is POST (handle both string and select object formats)\n const method =\n Array.isArray(values.method) && values.method.length > 0\n ? values.method[0].value || values.method[0].name\n : values.method;\n\n if (method === 'POST') {\n // For POST, provide the template if body is empty or was never set by user\n if (!currentValue || currentValue.trim() === '') {\n return defaultPost;\n }\n } else {\n // For non-POST methods, clear the body if it was auto-generated or empty\n // Check if the original body was empty (user never specified a body)\n const originalBody = originalValues?.body || '';\n const isOriginallyEmpty = !originalBody || originalBody.trim() === '';\n\n // Clear if: originally empty, contains default template, or is currently empty\n if (\n isOriginallyEmpty ||\n !currentValue ||\n currentValue.trim() === '' ||\n currentValue.trim() === defaultPost.trim()\n ) {\n return '';\n }\n }\n\n return currentValue; // Keep existing value if user has customized it\n }\n }\n },\n layout: [\n // Row with method and URL side by side\n { type: 'row', items: ['method', 'url'] },\n // Advanced group with nested layouts\n {\n type: 'group',\n label: 'Headers',\n items: ['headers'],\n collapsible: true,\n collapsed: true,\n helpText: 'Configure authentication or custom headers',\n getGroupValueCount: (formData: any) => {\n return formData.headers?.length + 10 || 0;\n }\n },\n {\n type: 'group',\n label: 'Body',\n items: ['body'],\n collapsible: true,\n collapsed: true,\n helpText: 'Configure the request payload',\n getGroupValueCount: (formData: any) => {\n return !!(\n formData.body &&\n formData.body.trim() !== '' &&\n formData.body !== defaultPost\n );\n }\n }\n ],\n toFormData: (action: CallWebhook) => {\n return {\n uuid: action.uuid,\n url: action.url || '',\n method: [{ value: action.method, name: action.method }],\n headers: action.headers || [],\n body: action.body || ''\n };\n },\n fromFormData: (data: Record<string, any>) => {\n return {\n uuid: data.uuid,\n type: 'call_webhook',\n url: data.url,\n method: data.method[0].value,\n headers: data.headers || [],\n body: data.body || ''\n } as CallWebhook;\n }\n};\n"]}
@@ -38,8 +38,7 @@ export const send_email = {
38
38
  type: 'textarea',
39
39
  required: true,
40
40
  evaluated: true,
41
- rows: 4,
42
- minHeight: 75
41
+ minHeight: 175
43
42
  }
44
43
  },
45
44
  validate: (action) => {
@@ -1 +1 @@
1
- {"version":3,"file":"send_email.js","sourceRoot":"","sources":["../../../../src/flow/actions/send_email.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,UAAU,CAAC;AAChC,OAAO,EAAgB,MAAM,EAAoB,MAAM,UAAU,CAAC;AAElE,OAAO,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAC5C,OAAO,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AAEnC,MAAM,CAAC,MAAM,UAAU,GAAiB;IACtC,IAAI,EAAE,YAAY;IAClB,KAAK,EAAE,MAAM,CAAC,IAAI;IAClB,MAAM,EAAE,CAAC,KAAW,EAAE,MAAiB,EAAE,EAAE;QACzC,OAAO,IAAI,CAAA;aACF,gBAAgB,CAAC,MAAM,CAAC,SAAS,EAAE,IAAI,CAAC,KAAK,CAAC;;;;;YAK/C,MAAM,CAAC,OAAO;;;WAGf,CAAC;IACV,CAAC;IAED,IAAI,EAAE;QACJ,SAAS,EAAE;YACT,IAAI,EAAE,QAAQ;YACd,KAAK,EAAE,YAAY;YACnB,OAAO,EAAE,EAAE;YACX,KAAK,EAAE,IAAI;YACX,UAAU,EAAE,IAAI;YAChB,WAAW,EAAE,wBAAwB;YACrC,MAAM,EAAE,IAAI;SACb;QACD,OAAO,EAAE;YACP,IAAI,EAAE,MAAM;YACZ,KAAK,EAAE,SAAS;YAChB,QAAQ,EAAE,IAAI;YACd,WAAW,EAAE,qBAAqB;YAClC,SAAS,EAAE,GAAG;SACf;QACD,IAAI,EAAE;YACJ,IAAI,EAAE,UAAU;YAChB,QAAQ,EAAE,IAAI;YACd,SAAS,EAAE,IAAI;YACf,IAAI,EAAE,CAAC;YACP,SAAS,EAAE,EAAE;SACd;KACF;IACD,QAAQ,EAAE,CAAC,MAAiB,EAAoB,EAAE;QAChD,MAAM,MAAM,GAA8B,EAAE,CAAC;QAE7C,IAAI,CAAC,MAAM,CAAC,SAAS,IAAI,MAAM,CAAC,SAAS,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACvD,MAAM,CAAC,SAAS,GAAG,kDAAkD,CAAC;QACxE,CAAC;QAED,OAAO;YACL,KAAK,EAAE,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,MAAM,KAAK,CAAC;YACvC,MAAM;SACP,CAAC;IACJ,CAAC;CACF,CAAC","sourcesContent":["import { html } from 'lit-html';\nimport { ActionConfig, COLORS, ValidationResult } from '../types';\nimport { Node, SendEmail } from '../../store/flow-definition';\nimport { renderStringList } from '../utils';\nimport { Icon } from '../../Icons';\n\nexport const send_email: ActionConfig = {\n name: 'Send Email',\n color: COLORS.send,\n render: (_node: Node, action: SendEmail) => {\n return html`<div>\n <div>${renderStringList(action.addresses, Icon.email)}</div>\n <div style=\"margin-top: 0.5em\">\n <div\n style=\"word-wrap: break-word; overflow-wrap: break-word; hyphens: auto;\"\n >\n ${action.subject}\n </div>\n </div>\n </div>`;\n },\n\n form: {\n addresses: {\n type: 'select',\n label: 'Recipients',\n options: [],\n multi: true,\n searchable: true,\n placeholder: 'Search for contacts...',\n emails: true\n },\n subject: {\n type: 'text',\n label: 'Subject',\n required: true,\n placeholder: 'Enter email subject',\n maxLength: 255\n },\n body: {\n type: 'textarea',\n required: true,\n evaluated: true,\n rows: 4,\n minHeight: 75\n }\n },\n validate: (action: SendEmail): ValidationResult => {\n const errors: { [key: string]: string } = {};\n\n if (!action.addresses || action.addresses.length === 0) {\n errors.addresses = 'At least one recipient email address is required';\n }\n\n return {\n valid: Object.keys(errors).length === 0,\n errors\n };\n }\n};\n"]}
1
+ {"version":3,"file":"send_email.js","sourceRoot":"","sources":["../../../../src/flow/actions/send_email.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,UAAU,CAAC;AAChC,OAAO,EAAgB,MAAM,EAAoB,MAAM,UAAU,CAAC;AAElE,OAAO,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAC5C,OAAO,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AAEnC,MAAM,CAAC,MAAM,UAAU,GAAiB;IACtC,IAAI,EAAE,YAAY;IAClB,KAAK,EAAE,MAAM,CAAC,IAAI;IAClB,MAAM,EAAE,CAAC,KAAW,EAAE,MAAiB,EAAE,EAAE;QACzC,OAAO,IAAI,CAAA;aACF,gBAAgB,CAAC,MAAM,CAAC,SAAS,EAAE,IAAI,CAAC,KAAK,CAAC;;;;;YAK/C,MAAM,CAAC,OAAO;;;WAGf,CAAC;IACV,CAAC;IAED,IAAI,EAAE;QACJ,SAAS,EAAE;YACT,IAAI,EAAE,QAAQ;YACd,KAAK,EAAE,YAAY;YACnB,OAAO,EAAE,EAAE;YACX,KAAK,EAAE,IAAI;YACX,UAAU,EAAE,IAAI;YAChB,WAAW,EAAE,wBAAwB;YACrC,MAAM,EAAE,IAAI;SACb;QACD,OAAO,EAAE;YACP,IAAI,EAAE,MAAM;YACZ,KAAK,EAAE,SAAS;YAChB,QAAQ,EAAE,IAAI;YACd,WAAW,EAAE,qBAAqB;YAClC,SAAS,EAAE,GAAG;SACf;QACD,IAAI,EAAE;YACJ,IAAI,EAAE,UAAU;YAChB,QAAQ,EAAE,IAAI;YACd,SAAS,EAAE,IAAI;YACf,SAAS,EAAE,GAAG;SACf;KACF;IACD,QAAQ,EAAE,CAAC,MAAiB,EAAoB,EAAE;QAChD,MAAM,MAAM,GAA8B,EAAE,CAAC;QAE7C,IAAI,CAAC,MAAM,CAAC,SAAS,IAAI,MAAM,CAAC,SAAS,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACvD,MAAM,CAAC,SAAS,GAAG,kDAAkD,CAAC;QACxE,CAAC;QAED,OAAO;YACL,KAAK,EAAE,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,MAAM,KAAK,CAAC;YACvC,MAAM;SACP,CAAC;IACJ,CAAC;CACF,CAAC","sourcesContent":["import { html } from 'lit-html';\nimport { ActionConfig, COLORS, ValidationResult } from '../types';\nimport { Node, SendEmail } from '../../store/flow-definition';\nimport { renderStringList } from '../utils';\nimport { Icon } from '../../Icons';\n\nexport const send_email: ActionConfig = {\n name: 'Send Email',\n color: COLORS.send,\n render: (_node: Node, action: SendEmail) => {\n return html`<div>\n <div>${renderStringList(action.addresses, Icon.email)}</div>\n <div style=\"margin-top: 0.5em\">\n <div\n style=\"word-wrap: break-word; overflow-wrap: break-word; hyphens: auto;\"\n >\n ${action.subject}\n </div>\n </div>\n </div>`;\n },\n\n form: {\n addresses: {\n type: 'select',\n label: 'Recipients',\n options: [],\n multi: true,\n searchable: true,\n placeholder: 'Search for contacts...',\n emails: true\n },\n subject: {\n type: 'text',\n label: 'Subject',\n required: true,\n placeholder: 'Enter email subject',\n maxLength: 255\n },\n body: {\n type: 'textarea',\n required: true,\n evaluated: true,\n minHeight: 175\n }\n },\n validate: (action: SendEmail): ValidationResult => {\n const errors: { [key: string]: string } = {};\n\n if (!action.addresses || action.addresses.length === 0) {\n errors.addresses = 'At least one recipient email address is required';\n }\n\n return {\n valid: Object.keys(errors).length === 0,\n errors\n };\n }\n};\n"]}
@@ -14,24 +14,35 @@ export const send_msg = {
14
14
  ${action.quick_replies.map((reply) => {
15
15
  return html `<div class="quick-reply">${reply}</div>`;
16
16
  })}
17
+ ${action.template
18
+ ? html `<div
19
+ style="border: 1px solid var(--color-widget-border);padding: 0.5em;margin-top: 1em;border-radius: var(--curvature); display:flex;background: rgba(0,0,0,.03);"
20
+ >
21
+ <temba-icon name="channel_wac"></temba-icon>
22
+ <div style="margin-left:0.5em">${action.template.name}</div>
23
+ </div>`
24
+ : null}
17
25
  </div>`
18
26
  : null}
19
27
  `;
20
28
  },
21
29
  form: {
22
30
  text: {
23
- type: 'textarea',
24
- label: 'Message Text',
25
- helpText: 'Enter the message to send. You can use expressions like @contact.name',
31
+ type: 'message-editor',
32
+ label: 'Message',
33
+ helpText: 'Enter the message to send with optional attachments. You can use expressions like @contact.name',
26
34
  required: true,
27
35
  evaluated: true,
28
- rows: 5,
29
- minHeight: 75
36
+ placeholder: 'Type your message here...',
37
+ maxAttachments: 10,
38
+ accept: '',
39
+ endpoint: '/api/v2/media.json',
40
+ counter: 'temba-charcount',
41
+ gsm: true,
42
+ autogrow: true
30
43
  },
31
44
  quick_replies: {
32
45
  type: 'select',
33
- label: 'Quick Replies',
34
- helpText: 'Add quick reply options for this message',
35
46
  options: [],
36
47
  multi: true,
37
48
  tags: true,
@@ -39,6 +50,123 @@ export const send_msg = {
39
50
  placeholder: 'Add quick replies...',
40
51
  maxItems: 10,
41
52
  evaluated: true
53
+ },
54
+ runtime_attachments: {
55
+ type: 'array',
56
+ helpText: 'Add dynamic attachments using expressions',
57
+ itemLabel: 'Attachment',
58
+ maxItems: 10,
59
+ isEmptyItem: (item) => {
60
+ return !item.expression || item.expression.trim() === '';
61
+ },
62
+ itemConfig: {
63
+ type: {
64
+ type: 'select',
65
+ options: [
66
+ { value: 'image', label: 'Image' },
67
+ { value: 'audio', label: 'Audio' },
68
+ { value: 'video', label: 'Video' },
69
+ { value: 'document', label: 'Document' }
70
+ ],
71
+ required: true,
72
+ searchable: false
73
+ },
74
+ expression: {
75
+ type: 'text',
76
+ placeholder: 'Expression (e.g. @contact.photo)',
77
+ required: true,
78
+ evaluated: true
79
+ }
80
+ }
81
+ }
82
+ },
83
+ layout: [
84
+ 'text',
85
+ {
86
+ type: 'group',
87
+ label: 'Quick Replies',
88
+ items: ['quick_replies'],
89
+ collapsible: true,
90
+ collapsed: (formData) => {
91
+ // Collapse only if there are no quick replies
92
+ return !formData.quick_replies || formData.quick_replies.length === 0;
93
+ },
94
+ getGroupValueCount: (formData) => {
95
+ var _a;
96
+ return ((_a = formData.quick_replies) === null || _a === void 0 ? void 0 : _a.length) || 0;
97
+ }
98
+ },
99
+ {
100
+ type: 'group',
101
+ label: 'Runtime Attachments',
102
+ items: ['runtime_attachments'],
103
+ collapsible: true,
104
+ collapsed: true,
105
+ helpText: 'Add dynamic attachments that are evaluated at runtime',
106
+ getGroupValueCount: (formData) => {
107
+ var _a;
108
+ return (((_a = formData.runtime_attachments) === null || _a === void 0 ? void 0 : _a.filter((item) => item && item.expression && item.expression.trim() !== '').length) || 0);
109
+ }
110
+ }
111
+ ],
112
+ toFormData: (action) => {
113
+ // Extract runtime attachments from the text field attachments
114
+ const runtimeAttachments = [];
115
+ const staticAttachments = [];
116
+ if (action.attachments && Array.isArray(action.attachments)) {
117
+ action.attachments.forEach((attachment) => {
118
+ if (typeof attachment === 'string' && attachment.includes(':')) {
119
+ const colonIndex = attachment.indexOf(':');
120
+ const contentType = attachment.substring(0, colonIndex);
121
+ const value = attachment.substring(colonIndex + 1);
122
+ if (!contentType.includes('/')) {
123
+ // This is a runtime attachment
124
+ runtimeAttachments.push({
125
+ type: contentType,
126
+ expression: value
127
+ });
128
+ }
129
+ else {
130
+ // This is a static attachment
131
+ staticAttachments.push(attachment);
132
+ }
133
+ }
134
+ });
135
+ }
136
+ return {
137
+ uuid: action.uuid,
138
+ text: action.text || '',
139
+ attachments: staticAttachments,
140
+ runtime_attachments: runtimeAttachments,
141
+ quick_replies: (action.quick_replies || []).map((reply) => ({
142
+ name: reply,
143
+ value: reply
144
+ }))
145
+ };
146
+ },
147
+ fromFormData: (data) => {
148
+ const result = {
149
+ uuid: data.uuid,
150
+ type: 'send_msg',
151
+ text: data.text || '',
152
+ attachments: [],
153
+ quick_replies: (data.quick_replies || []).map((reply) => typeof reply === 'string' ? reply : reply.value || reply.name || reply)
154
+ };
155
+ // Combine static attachments from text field with runtime attachments
156
+ const staticAttachments = data.attachments || [];
157
+ const runtimeAttachments = (data.runtime_attachments || [])
158
+ .filter((item) => item && item.type && item.expression) // Filter out invalid items
159
+ .map((item) => `${item.type}:${item.expression}`);
160
+ result.attachments = [...staticAttachments, ...runtimeAttachments];
161
+ // Remove quick_replies if empty to match original format
162
+ if (result.quick_replies.length === 0) {
163
+ delete result.quick_replies;
164
+ }
165
+ return result;
166
+ },
167
+ sanitize: (formData) => {
168
+ if (formData.text && typeof formData.text === 'string') {
169
+ formData.text = formData.text.trim();
42
170
  }
43
171
  },
44
172
  validate: (action) => {
@@ -46,6 +174,26 @@ export const send_msg = {
46
174
  if (!action.text || action.text.trim() === '') {
47
175
  errors.text = 'Message text is required';
48
176
  }
177
+ const attachments = action.attachments || [];
178
+ if (attachments.length > 10) {
179
+ const staticAttachments = attachments.filter((attachment) => typeof attachment === 'string' &&
180
+ attachment.substring(0, attachment.indexOf(':')).includes('/'));
181
+ const runtimeAttachments = attachments.filter((attachment) => typeof attachment === 'string' &&
182
+ !attachment.substring(0, attachment.indexOf(':')).includes('/'));
183
+ if (runtimeAttachments.length > 0) {
184
+ errors.runtime_attachments =
185
+ 'Each message can only have up to 10 attachments';
186
+ }
187
+ if (staticAttachments.length > 0) {
188
+ const message = 'Each message can only have up to 10 total attachments';
189
+ if (errors.text) {
190
+ errors.text += ` ${message}`;
191
+ }
192
+ else {
193
+ errors.text = message;
194
+ }
195
+ }
196
+ }
49
197
  return {
50
198
  valid: Object.keys(errors).length === 0,
51
199
  errors
@@ -1 +1 @@
1
- {"version":3,"file":"send_msg.js","sourceRoot":"","sources":["../../../../src/flow/actions/send_msg.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,UAAU,CAAC;AAChC,OAAO,EAAE,UAAU,EAAE,MAAM,oCAAoC,CAAC;AAChE,OAAO,EAAgB,MAAM,EAAoB,MAAM,UAAU,CAAC;AAGlE,MAAM,CAAC,MAAM,QAAQ,GAAiB;IACpC,IAAI,EAAE,cAAc;IACpB,KAAK,EAAE,MAAM,CAAC,IAAI;IAClB,MAAM,EAAE,CAAC,KAAW,EAAE,MAAe,EAAE,EAAE;;QACvC,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;QAChD,OAAO,IAAI,CAAA;QACP,UAAU,CAAC,IAAI,CAAC;QAChB,CAAA,MAAA,MAAM,CAAC,aAAa,0CAAE,MAAM,IAAG,CAAC;YAChC,CAAC,CAAC,IAAI,CAAA;cACA,MAAM,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE;gBACnC,OAAO,IAAI,CAAA,4BAA4B,KAAK,QAAQ,CAAC;YACvD,CAAC,CAAC;iBACG;YACT,CAAC,CAAC,IAAI;KACT,CAAC;IACJ,CAAC;IACD,IAAI,EAAE;QACJ,IAAI,EAAE;YACJ,IAAI,EAAE,UAAU;YAChB,KAAK,EAAE,cAAc;YACrB,QAAQ,EACN,uEAAuE;YACzE,QAAQ,EAAE,IAAI;YACd,SAAS,EAAE,IAAI;YACf,IAAI,EAAE,CAAC;YACP,SAAS,EAAE,EAAE;SACd;QACD,aAAa,EAAE;YACb,IAAI,EAAE,QAAQ;YACd,KAAK,EAAE,eAAe;YACtB,QAAQ,EAAE,0CAA0C;YACpD,OAAO,EAAE,EAAE;YACX,KAAK,EAAE,IAAI;YACX,IAAI,EAAE,IAAI;YACV,UAAU,EAAE,IAAI;YAChB,WAAW,EAAE,sBAAsB;YACnC,QAAQ,EAAE,EAAE;YACZ,SAAS,EAAE,IAAI;SAChB;KACF;IACD,QAAQ,EAAE,CAAC,MAAe,EAAoB,EAAE;QAC9C,MAAM,MAAM,GAA8B,EAAE,CAAC;QAE7C,IAAI,CAAC,MAAM,CAAC,IAAI,IAAI,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;YAC9C,MAAM,CAAC,IAAI,GAAG,0BAA0B,CAAC;QAC3C,CAAC;QAED,OAAO;YACL,KAAK,EAAE,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,MAAM,KAAK,CAAC;YACvC,MAAM;SACP,CAAC;IACJ,CAAC;CACF,CAAC","sourcesContent":["import { html } from 'lit-html';\nimport { unsafeHTML } from 'lit-html/directives/unsafe-html.js';\nimport { ActionConfig, COLORS, ValidationResult } from '../types';\nimport { Node, SendMsg } from '../../store/flow-definition';\n\nexport const send_msg: ActionConfig = {\n name: 'Send Message',\n color: COLORS.send,\n render: (_node: Node, action: SendMsg) => {\n const text = action.text.replace(/\\n/g, '<br>');\n return html`\n ${unsafeHTML(text)}\n ${action.quick_replies?.length > 0\n ? html`<div class=\"quick-replies\">\n ${action.quick_replies.map((reply) => {\n return html`<div class=\"quick-reply\">${reply}</div>`;\n })}\n </div>`\n : null}\n `;\n },\n form: {\n text: {\n type: 'textarea',\n label: 'Message Text',\n helpText:\n 'Enter the message to send. You can use expressions like @contact.name',\n required: true,\n evaluated: true,\n rows: 5,\n minHeight: 75\n },\n quick_replies: {\n type: 'select',\n label: 'Quick Replies',\n helpText: 'Add quick reply options for this message',\n options: [],\n multi: true,\n tags: true,\n searchable: true,\n placeholder: 'Add quick replies...',\n maxItems: 10,\n evaluated: true\n }\n },\n validate: (action: SendMsg): ValidationResult => {\n const errors: { [key: string]: string } = {};\n\n if (!action.text || action.text.trim() === '') {\n errors.text = 'Message text is required';\n }\n\n return {\n valid: Object.keys(errors).length === 0,\n errors\n };\n }\n};\n"]}
1
+ {"version":3,"file":"send_msg.js","sourceRoot":"","sources":["../../../../src/flow/actions/send_msg.ts"],"names":[],"mappings":"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","sourcesContent":["import { html } from 'lit-html';\nimport { unsafeHTML } from 'lit-html/directives/unsafe-html.js';\nimport { ActionConfig, COLORS, ValidationResult } from '../types';\nimport { Node, SendMsg } from '../../store/flow-definition';\n\nexport const send_msg: ActionConfig = {\n name: 'Send Message',\n color: COLORS.send,\n render: (_node: Node, action: SendMsg) => {\n const text = action.text.replace(/\\n/g, '<br>');\n return html`\n ${unsafeHTML(text)}\n ${action.quick_replies?.length > 0\n ? html`<div class=\"quick-replies\">\n ${action.quick_replies.map((reply) => {\n return html`<div class=\"quick-reply\">${reply}</div>`;\n })}\n ${action.template\n ? html`<div\n style=\"border: 1px solid var(--color-widget-border);padding: 0.5em;margin-top: 1em;border-radius: var(--curvature); display:flex;background: rgba(0,0,0,.03);\"\n >\n <temba-icon name=\"channel_wac\"></temba-icon>\n <div style=\"margin-left:0.5em\">${action.template.name}</div>\n </div>`\n : null}\n </div>`\n : null}\n `;\n },\n form: {\n text: {\n type: 'message-editor',\n label: 'Message',\n helpText:\n 'Enter the message to send with optional attachments. You can use expressions like @contact.name',\n required: true,\n evaluated: true,\n placeholder: 'Type your message here...',\n maxAttachments: 10,\n accept: '',\n endpoint: '/api/v2/media.json',\n counter: 'temba-charcount',\n gsm: true,\n autogrow: true\n },\n quick_replies: {\n type: 'select',\n options: [],\n multi: true,\n tags: true,\n searchable: true,\n placeholder: 'Add quick replies...',\n maxItems: 10,\n evaluated: true\n },\n runtime_attachments: {\n type: 'array',\n helpText: 'Add dynamic attachments using expressions',\n itemLabel: 'Attachment',\n maxItems: 10,\n isEmptyItem: (item: any) => {\n return !item.expression || item.expression.trim() === '';\n },\n itemConfig: {\n type: {\n type: 'select',\n options: [\n { value: 'image', label: 'Image' },\n { value: 'audio', label: 'Audio' },\n { value: 'video', label: 'Video' },\n { value: 'document', label: 'Document' }\n ],\n required: true,\n searchable: false\n },\n expression: {\n type: 'text',\n placeholder: 'Expression (e.g. @contact.photo)',\n required: true,\n evaluated: true\n }\n }\n }\n },\n layout: [\n 'text',\n {\n type: 'group',\n label: 'Quick Replies',\n items: ['quick_replies'],\n collapsible: true,\n collapsed: (formData: any) => {\n // Collapse only if there are no quick replies\n return !formData.quick_replies || formData.quick_replies.length === 0;\n },\n getGroupValueCount: (formData: any) => {\n return formData.quick_replies?.length || 0;\n }\n },\n {\n type: 'group',\n label: 'Runtime Attachments',\n items: ['runtime_attachments'],\n collapsible: true,\n collapsed: true,\n helpText: 'Add dynamic attachments that are evaluated at runtime',\n getGroupValueCount: (formData: any) => {\n return (\n formData.runtime_attachments?.filter(\n (item: any) =>\n item && item.expression && item.expression.trim() !== ''\n ).length || 0\n );\n }\n }\n ],\n toFormData: (action: SendMsg) => {\n // Extract runtime attachments from the text field attachments\n const runtimeAttachments: { type: string; expression: string }[] = [];\n const staticAttachments: string[] = [];\n\n if (action.attachments && Array.isArray(action.attachments)) {\n action.attachments.forEach((attachment) => {\n if (typeof attachment === 'string' && attachment.includes(':')) {\n const colonIndex = attachment.indexOf(':');\n const contentType = attachment.substring(0, colonIndex);\n const value = attachment.substring(colonIndex + 1);\n\n if (!contentType.includes('/')) {\n // This is a runtime attachment\n runtimeAttachments.push({\n type: contentType,\n expression: value\n });\n } else {\n // This is a static attachment\n staticAttachments.push(attachment);\n }\n }\n });\n }\n\n return {\n uuid: action.uuid,\n text: action.text || '',\n attachments: staticAttachments,\n runtime_attachments: runtimeAttachments,\n quick_replies: (action.quick_replies || []).map((reply) => ({\n name: reply,\n value: reply\n }))\n };\n },\n fromFormData: (data: Record<string, any>) => {\n const result = {\n uuid: data.uuid,\n type: 'send_msg',\n text: data.text || '',\n attachments: [],\n quick_replies: (data.quick_replies || []).map((reply: any) =>\n typeof reply === 'string' ? reply : reply.value || reply.name || reply\n )\n };\n\n // Combine static attachments from text field with runtime attachments\n const staticAttachments = data.attachments || [];\n const runtimeAttachments = (data.runtime_attachments || [])\n .filter((item: any) => item && item.type && item.expression) // Filter out invalid items\n .map(\n (item: { type: string; expression: string }) =>\n `${item.type}:${item.expression}`\n );\n\n result.attachments = [...staticAttachments, ...runtimeAttachments];\n\n // Remove quick_replies if empty to match original format\n if (result.quick_replies.length === 0) {\n delete (result as any).quick_replies;\n }\n\n return result as SendMsg;\n },\n sanitize: (formData: any): void => {\n if (formData.text && typeof formData.text === 'string') {\n formData.text = formData.text.trim();\n }\n },\n validate: (action: SendMsg): ValidationResult => {\n const errors: { [key: string]: string } = {};\n\n if (!action.text || action.text.trim() === '') {\n errors.text = 'Message text is required';\n }\n\n const attachments = action.attachments || [];\n if (attachments.length > 10) {\n const staticAttachments = attachments.filter(\n (attachment) =>\n typeof attachment === 'string' &&\n attachment.substring(0, attachment.indexOf(':')).includes('/')\n );\n\n const runtimeAttachments = attachments.filter(\n (attachment) =>\n typeof attachment === 'string' &&\n !attachment.substring(0, attachment.indexOf(':')).includes('/')\n );\n\n if (runtimeAttachments.length > 0) {\n errors.runtime_attachments =\n 'Each message can only have up to 10 attachments';\n }\n\n if (staticAttachments.length > 0) {\n const message = 'Each message can only have up to 10 total attachments';\n if (errors.text) {\n errors.text += ` ${message}`;\n } else {\n errors.text = message;\n }\n }\n }\n\n return {\n valid: Object.keys(errors).length === 0,\n errors\n };\n }\n};\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/flow/types.ts"],"names":[],"mappings":"AAiQA,MAAM,CAAC,MAAM,MAAM,GAAG;IACpB,IAAI,EAAE,SAAS;IACf,MAAM,EAAE,SAAS;IACjB,SAAS,EAAE,SAAS;IACpB,IAAI,EAAE,SAAS;IACf,MAAM,EAAE,SAAS;IACjB,IAAI,EAAE,SAAS;IACf,KAAK,EAAE,SAAS;IAChB,OAAO,EAAE,SAAS;IAClB,IAAI,EAAE,SAAS;IACf,GAAG,EAAE,SAAS;IACd,MAAM,EAAE,SAAS;CAClB,CAAC;AAEF,iCAAiC;AACjC,MAAM,UAAU,mBAAmB,CAAC,KAAU;IAC5C,IAAI,OAAO,KAAK,KAAK,SAAS,EAAE,CAAC;QAC/B,OAAO,gBAAgB,CAAC;IAC1B,CAAC;IACD,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC9B,OAAO,iBAAiB,CAAC;IAC3B,CAAC;IACD,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO,cAAc,CAAC,CAAC,+BAA+B;IACxD,CAAC;IACD,sDAAsD;IACtD,OAAO,iBAAiB,CAAC;AAC3B,CAAC;AAED,mEAAmE;AACnE,MAAM,UAAU,wBAAwB,CAAC,KAAU;IACjD,IAAI,OAAO,KAAK,KAAK,SAAS,EAAE,CAAC;QAC/B,OAAO;YACL,MAAM,EAAE,EAAE,IAAI,EAAE,gBAAgB,EAAE;SACnC,CAAC;IACJ,CAAC;IACD,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC9B,OAAO;YACL,MAAM,EAAE;gBACN,IAAI,EAAE,iBAAiB;gBACvB,UAAU,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;aAC/B;SACF,CAAC;IACJ,CAAC;IACD,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACzB,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,IAAI,OAAO,KAAK,CAAC,CAAC,CAAC,KAAK,QAAQ,EAAE,CAAC;YACrD,OAAO;gBACL,MAAM,EAAE;oBACN,IAAI,EAAE,cAAc;oBACpB,UAAU,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE;iBACxC;aACF,CAAC;QACJ,CAAC;QACD,OAAO;YACL,MAAM,EAAE;gBACN,IAAI,EAAE,cAAc;gBACpB,UAAU,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE;aAC5B;SACF,CAAC;IACJ,CAAC;IACD,OAAO;QACL,MAAM,EAAE,EAAE,IAAI,EAAE,iBAAiB,EAAE;KACpC,CAAC;AACJ,CAAC;AAED,qDAAqD;AACrD,MAAM,UAAU,iBAAiB,CAC/B,MAAoB;IAEpB,OAAO,MAAM,CAAC,IAAI,KAAK,iBAAiB,CAAC;AAC3C,CAAC;AAED,MAAM,UAAU,kBAAkB,CAChC,MAAoB;IAEpB,OAAO,MAAM,CAAC,IAAI,KAAK,kBAAkB,CAAC;AAC5C,CAAC;AAED,MAAM,UAAU,cAAc,CAC5B,MAAoB;IAEpB,OAAO,MAAM,CAAC,IAAI,KAAK,cAAc,CAAC;AACxC,CAAC;AAED,MAAM,UAAU,gBAAgB,CAC9B,MAAoB;IAEpB,OAAO,MAAM,CAAC,IAAI,KAAK,gBAAgB,CAAC;AAC1C,CAAC;AAED,MAAM,UAAU,cAAc,CAC5B,MAAoB;IAEpB,OAAO,MAAM,CAAC,IAAI,KAAK,cAAc,CAAC;AACxC,CAAC","sourcesContent":["import { TemplateResult } from 'lit-html';\nimport { Action } from '../store/flow-definition';\n\nexport interface ValidationResult {\n valid: boolean;\n errors: { [key: string]: string };\n}\n\n// Component attribute interfaces - these define what's allowed for each component type\nexport interface TextInputAttributes {\n type?: 'text' | 'email' | 'number' | 'url' | 'tel';\n placeholder?: string;\n clearable?: boolean;\n maxlength?: number;\n gsm?: boolean;\n autogrow?: boolean;\n textarea?: boolean;\n submitOnEnter?: boolean;\n}\n\nexport interface CompletionAttributes {\n placeholder?: string;\n clearable?: boolean;\n maxlength?: number;\n gsm?: boolean;\n autogrow?: boolean;\n textarea?: boolean;\n expressions?: string;\n counter?: string;\n minHeight?: number;\n}\n\nexport interface SelectAttributes {\n placeholder?: string;\n multi?: boolean;\n searchable?: boolean;\n tags?: boolean;\n emails?: boolean;\n clearable?: boolean;\n endpoint?: string;\n valueKey?: string;\n nameKey?: string;\n queryParam?: string;\n maxItems?: number;\n maxItemsText?: string;\n expressions?: string;\n options?: Array<{ name: string; value: any }>;\n sorted?: boolean;\n allowCreate?: boolean;\n jsonValue?: boolean;\n spaceSelect?: boolean;\n infoText?: string;\n}\n\nexport interface CheckboxAttributes {\n label?: string;\n size?: number;\n disabled?: boolean;\n animateChange?: string;\n}\n\nexport interface SliderAttributes {\n min?: number;\n max?: number;\n range?: boolean;\n}\n\n// Widget configuration using discriminated union for type safety\nexport type WidgetConfig =\n | { type: 'temba-textinput'; attributes?: TextInputAttributes }\n | { type: 'temba-completion'; attributes?: CompletionAttributes }\n | { type: 'temba-select'; attributes?: SelectAttributes }\n | { type: 'temba-checkbox'; attributes?: CheckboxAttributes }\n | { type: 'temba-slider'; attributes?: SliderAttributes }\n | { type: string; attributes?: { [key: string]: any } }; // Generic fallback\n\n// Property configuration with the clean structure you want\nexport interface PropertyConfig {\n // Form field metadata\n label?: string;\n helpText?: string;\n required?: boolean;\n maxLength?: number;\n minLength?: number;\n pattern?: string;\n\n // Widget configuration\n widget: WidgetConfig;\n\n // Conditional behavior based on other field values\n conditions?: {\n // When to show this field\n visible?: (formData: any) => boolean;\n\n // When this field is disabled\n disabled?: (formData: any) => boolean;\n };\n}\n\nexport interface NodeConfig {\n type: string;\n name?: string;\n color?: string;\n action?: ActionConfig;\n router?: {\n type: 'switch' | 'random';\n defaultCategory?: string;\n operand?: string;\n configurable?: boolean; // can the rules be configured in the UI\n rules?: {\n type: 'has_number_between' | 'has_string' | 'has_value' | 'has_not_value';\n arguments: string[];\n categoryName: string;\n }[];\n };\n properties?: { [key: string]: PropertyConfig };\n toFormData?: (node: any) => any;\n fromFormData?: (formData: any, originalNode: any) => any;\n}\n\n// New field configuration system for generic form generation\nexport interface BaseFieldConfig {\n label?: string;\n required?: boolean;\n evaluated?: boolean; // if this field supports expression evaluation\n dependsOn?: string[]; // fields this field depends on\n computeValue?: (\n values: Record<string, any>,\n currentValue: any,\n originalValues?: Record<string, any>\n ) => any;\n\n // Validation properties\n minLength?: number;\n maxLength?: number;\n pattern?: string;\n helpText?: string;\n\n // Layout properties\n maxWidth?: string; // CSS max-width value (e.g., '200px', '50%', '10rem')\n\n // Conditional rendering\n conditions?: {\n visible?: (formData: Record<string, any>) => boolean;\n disabled?: (formData: Record<string, any>) => boolean;\n };\n}\n\nexport interface TextFieldConfig extends BaseFieldConfig {\n type: 'text';\n placeholder?: string;\n}\n\nexport interface TextareaFieldConfig extends BaseFieldConfig {\n type: 'textarea';\n placeholder?: string;\n rows?: number;\n minHeight?: number;\n}\n\nexport interface SelectFieldConfig extends BaseFieldConfig {\n type: 'select';\n options: string[] | { value: string; label: string }[];\n multi?: boolean;\n searchable?: boolean;\n tags?: boolean;\n placeholder?: string;\n maxItems?: number;\n valueKey?: string;\n nameKey?: string;\n endpoint?: string;\n emails?: boolean;\n}\n\nexport interface KeyValueFieldConfig extends BaseFieldConfig {\n type: 'key-value';\n sortable?: boolean;\n keyPlaceholder?: string;\n valuePlaceholder?: string;\n minRows?: number;\n}\n\nexport interface ArrayFieldConfig extends BaseFieldConfig {\n type: 'array';\n itemConfig: Record<string, FieldConfig>;\n sortable?: boolean;\n minItems?: number;\n maxItems?: number;\n itemLabel?: string;\n onItemChange?: (\n itemIndex: number,\n field: string,\n value: any,\n allItems: any[]\n ) => any[];\n}\n\nexport interface CheckboxFieldConfig extends BaseFieldConfig {\n type: 'checkbox';\n size?: number;\n animateChange?: string;\n}\n\nexport type FieldConfig =\n | TextFieldConfig\n | TextareaFieldConfig\n | SelectFieldConfig\n | KeyValueFieldConfig\n | ArrayFieldConfig\n | CheckboxFieldConfig;\n\n// Layout configurations for better form organization\n// Recursive layout system - any layout item can contain other layout items\n\nexport interface FieldItemConfig {\n type: 'field';\n field: string; // field name to render\n}\n\nexport interface RowLayoutConfig {\n type: 'row';\n items: LayoutItem[]; // can contain fields, groups, or other rows\n gap?: string; // CSS gap value, defaults to '1rem'\n}\n\nexport interface GroupLayoutConfig {\n type: 'group';\n label: string;\n items: LayoutItem[]; // can contain fields, rows, or other groups\n collapsible?: boolean;\n collapsed?: boolean; // initial state if collapsible\n helpText?: string;\n}\n\nexport type LayoutItem =\n | FieldItemConfig\n | RowLayoutConfig\n | GroupLayoutConfig\n | string; // string is shorthand for field\n\nexport interface ActionConfig {\n name: string;\n color: string;\n evaluated?: string[];\n render?: (node: any, action: any) => TemplateResult;\n\n form?: Record<string, FieldConfig>;\n layout?: LayoutItem[]; // optional layout configuration - array of layout items\n\n // Action editor configuration (legacy)\n // Form-level transformations\n toFormData?: (action: Action) => any;\n fromFormData?: (formData: any) => Action;\n\n validate?: (action: Action) => ValidationResult;\n}\n\nexport const COLORS = {\n send: '#3498db',\n update: '#01c1af',\n broadcast: '#8e5ea7',\n call: '#e68628',\n create: '#df419f',\n save: '#1a777c',\n split: '#aaaaaa',\n execute: '#666666',\n wait: '#4d7dad',\n add: '#309c42',\n remove: '#e74c3c'\n};\n\n// Default property type mappings\nexport function getDefaultComponent(value: any): WidgetConfig['type'] {\n if (typeof value === 'boolean') {\n return 'temba-checkbox';\n }\n if (typeof value === 'number') {\n return 'temba-textinput';\n }\n if (Array.isArray(value)) {\n return 'temba-select'; // For arrays, use multi-select\n }\n // Default to text input for strings and unknown types\n return 'temba-textinput';\n}\n\n// Get component properties for default mappings with proper typing\nexport function getDefaultComponentProps(value: any): PropertyConfig {\n if (typeof value === 'boolean') {\n return {\n widget: { type: 'temba-checkbox' }\n };\n }\n if (typeof value === 'number') {\n return {\n widget: {\n type: 'temba-textinput',\n attributes: { type: 'number' }\n }\n };\n }\n if (Array.isArray(value)) {\n if (value.length > 0 && typeof value[0] === 'string') {\n return {\n widget: {\n type: 'temba-select',\n attributes: { multi: true, tags: true }\n }\n };\n }\n return {\n widget: {\n type: 'temba-select',\n attributes: { multi: true }\n }\n };\n }\n return {\n widget: { type: 'temba-textinput' }\n };\n}\n\n// Type guard functions for working with WidgetConfig\nexport function isTextInputWidget(\n config: WidgetConfig\n): config is { type: 'temba-textinput'; attributes?: TextInputAttributes } {\n return config.type === 'temba-textinput';\n}\n\nexport function isCompletionWidget(\n config: WidgetConfig\n): config is { type: 'temba-completion'; attributes?: CompletionAttributes } {\n return config.type === 'temba-completion';\n}\n\nexport function isSelectWidget(\n config: WidgetConfig\n): config is { type: 'temba-select'; attributes?: SelectAttributes } {\n return config.type === 'temba-select';\n}\n\nexport function isCheckboxWidget(\n config: WidgetConfig\n): config is { type: 'temba-checkbox'; attributes?: CheckboxAttributes } {\n return config.type === 'temba-checkbox';\n}\n\nexport function isSliderWidget(\n config: WidgetConfig\n): config is { type: 'slider'; attributes?: SliderAttributes } {\n return config.type === 'temba-slider';\n}\n"]}
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/flow/types.ts"],"names":[],"mappings":"AAoRA,MAAM,CAAC,MAAM,MAAM,GAAG;IACpB,IAAI,EAAE,SAAS;IACf,MAAM,EAAE,SAAS;IACjB,SAAS,EAAE,SAAS;IACpB,IAAI,EAAE,SAAS;IACf,MAAM,EAAE,SAAS;IACjB,IAAI,EAAE,SAAS;IACf,KAAK,EAAE,SAAS;IAChB,OAAO,EAAE,SAAS;IAClB,IAAI,EAAE,SAAS;IACf,GAAG,EAAE,SAAS;IACd,MAAM,EAAE,SAAS;CAClB,CAAC;AAEF,iCAAiC;AACjC,MAAM,UAAU,mBAAmB,CAAC,KAAU;IAC5C,IAAI,OAAO,KAAK,KAAK,SAAS,EAAE,CAAC;QAC/B,OAAO,gBAAgB,CAAC;IAC1B,CAAC;IACD,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC9B,OAAO,iBAAiB,CAAC;IAC3B,CAAC;IACD,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO,cAAc,CAAC,CAAC,+BAA+B;IACxD,CAAC;IACD,sDAAsD;IACtD,OAAO,iBAAiB,CAAC;AAC3B,CAAC;AAED,mEAAmE;AACnE,MAAM,UAAU,wBAAwB,CAAC,KAAU;IACjD,IAAI,OAAO,KAAK,KAAK,SAAS,EAAE,CAAC;QAC/B,OAAO;YACL,MAAM,EAAE,EAAE,IAAI,EAAE,gBAAgB,EAAE;SACnC,CAAC;IACJ,CAAC;IACD,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC9B,OAAO;YACL,MAAM,EAAE;gBACN,IAAI,EAAE,iBAAiB;gBACvB,UAAU,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;aAC/B;SACF,CAAC;IACJ,CAAC;IACD,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACzB,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,IAAI,OAAO,KAAK,CAAC,CAAC,CAAC,KAAK,QAAQ,EAAE,CAAC;YACrD,OAAO;gBACL,MAAM,EAAE;oBACN,IAAI,EAAE,cAAc;oBACpB,UAAU,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE;iBACxC;aACF,CAAC;QACJ,CAAC;QACD,OAAO;YACL,MAAM,EAAE;gBACN,IAAI,EAAE,cAAc;gBACpB,UAAU,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE;aAC5B;SACF,CAAC;IACJ,CAAC;IACD,OAAO;QACL,MAAM,EAAE,EAAE,IAAI,EAAE,iBAAiB,EAAE;KACpC,CAAC;AACJ,CAAC;AAED,qDAAqD;AACrD,MAAM,UAAU,iBAAiB,CAC/B,MAAoB;IAEpB,OAAO,MAAM,CAAC,IAAI,KAAK,iBAAiB,CAAC;AAC3C,CAAC;AAED,MAAM,UAAU,kBAAkB,CAChC,MAAoB;IAEpB,OAAO,MAAM,CAAC,IAAI,KAAK,kBAAkB,CAAC;AAC5C,CAAC;AAED,MAAM,UAAU,cAAc,CAC5B,MAAoB;IAEpB,OAAO,MAAM,CAAC,IAAI,KAAK,cAAc,CAAC;AACxC,CAAC;AAED,MAAM,UAAU,gBAAgB,CAC9B,MAAoB;IAEpB,OAAO,MAAM,CAAC,IAAI,KAAK,gBAAgB,CAAC;AAC1C,CAAC;AAED,MAAM,UAAU,cAAc,CAC5B,MAAoB;IAEpB,OAAO,MAAM,CAAC,IAAI,KAAK,cAAc,CAAC;AACxC,CAAC","sourcesContent":["import { TemplateResult } from 'lit-html';\nimport { Action } from '../store/flow-definition';\n\nexport interface ValidationResult {\n valid: boolean;\n errors: { [key: string]: string };\n}\n\n// Component attribute interfaces - these define what's allowed for each component type\nexport interface TextInputAttributes {\n type?: 'text' | 'email' | 'number' | 'url' | 'tel';\n placeholder?: string;\n clearable?: boolean;\n maxlength?: number;\n gsm?: boolean;\n autogrow?: boolean;\n textarea?: boolean;\n submitOnEnter?: boolean;\n}\n\nexport interface CompletionAttributes {\n placeholder?: string;\n clearable?: boolean;\n maxlength?: number;\n gsm?: boolean;\n autogrow?: boolean;\n textarea?: boolean;\n expressions?: string;\n counter?: string;\n minHeight?: number;\n}\n\nexport interface SelectAttributes {\n placeholder?: string;\n multi?: boolean;\n searchable?: boolean;\n tags?: boolean;\n emails?: boolean;\n clearable?: boolean;\n endpoint?: string;\n valueKey?: string;\n nameKey?: string;\n queryParam?: string;\n maxItems?: number;\n maxItemsText?: string;\n expressions?: string;\n options?: Array<{ name: string; value: any }>;\n sorted?: boolean;\n allowCreate?: boolean;\n jsonValue?: boolean;\n spaceSelect?: boolean;\n infoText?: string;\n}\n\nexport interface CheckboxAttributes {\n label?: string;\n size?: number;\n disabled?: boolean;\n animateChange?: string;\n}\n\nexport interface SliderAttributes {\n min?: number;\n max?: number;\n range?: boolean;\n}\n\n// Widget configuration using discriminated union for type safety\nexport type WidgetConfig =\n | { type: 'temba-textinput'; attributes?: TextInputAttributes }\n | { type: 'temba-completion'; attributes?: CompletionAttributes }\n | { type: 'temba-select'; attributes?: SelectAttributes }\n | { type: 'temba-checkbox'; attributes?: CheckboxAttributes }\n | { type: 'temba-slider'; attributes?: SliderAttributes }\n | { type: string; attributes?: { [key: string]: any } }; // Generic fallback\n\n// Property configuration with the clean structure you want\nexport interface PropertyConfig {\n // Form field metadata\n label?: string;\n helpText?: string;\n required?: boolean;\n maxLength?: number;\n minLength?: number;\n pattern?: string;\n\n // Widget configuration\n widget: WidgetConfig;\n\n // Conditional behavior based on other field values\n conditions?: {\n // When to show this field\n visible?: (formData: any) => boolean;\n\n // When this field is disabled\n disabled?: (formData: any) => boolean;\n };\n}\n\nexport interface NodeConfig {\n type: string;\n name?: string;\n color?: string;\n action?: ActionConfig;\n router?: {\n type: 'switch' | 'random';\n defaultCategory?: string;\n operand?: string;\n configurable?: boolean; // can the rules be configured in the UI\n rules?: {\n type: 'has_number_between' | 'has_string' | 'has_value' | 'has_not_value';\n arguments: string[];\n categoryName: string;\n }[];\n };\n properties?: { [key: string]: PropertyConfig };\n toFormData?: (node: any) => any;\n fromFormData?: (formData: any, originalNode: any) => any;\n}\n\n// New field configuration system for generic form generation\nexport interface BaseFieldConfig {\n label?: string;\n required?: boolean;\n evaluated?: boolean; // if this field supports expression evaluation\n dependsOn?: string[]; // fields this field depends on\n computeValue?: (\n values: Record<string, any>,\n currentValue: any,\n originalValues?: Record<string, any>\n ) => any;\n\n // Validation properties\n minLength?: number;\n maxLength?: number;\n pattern?: string;\n helpText?: string;\n\n // Layout properties\n maxWidth?: string; // CSS max-width value (e.g., '200px', '50%', '10rem')\n\n // Conditional rendering\n conditions?: {\n visible?: (formData: Record<string, any>) => boolean;\n disabled?: (formData: Record<string, any>) => boolean;\n };\n}\n\nexport interface TextFieldConfig extends BaseFieldConfig {\n type: 'text';\n placeholder?: string;\n}\n\nexport interface TextareaFieldConfig extends BaseFieldConfig {\n type: 'textarea';\n placeholder?: string;\n rows?: number;\n minHeight?: number;\n}\n\nexport interface SelectFieldConfig extends BaseFieldConfig {\n type: 'select';\n options: string[] | { value: string; label: string }[];\n multi?: boolean;\n clearable?: boolean;\n searchable?: boolean;\n tags?: boolean;\n placeholder?: string;\n maxItems?: number;\n valueKey?: string;\n nameKey?: string;\n endpoint?: string;\n emails?: boolean;\n flavor?: 'small' | 'large';\n}\n\nexport interface KeyValueFieldConfig extends BaseFieldConfig {\n type: 'key-value';\n sortable?: boolean;\n keyPlaceholder?: string;\n valuePlaceholder?: string;\n minRows?: number;\n}\n\nexport interface ArrayFieldConfig extends BaseFieldConfig {\n type: 'array';\n itemConfig: Record<string, FieldConfig>;\n sortable?: boolean;\n minItems?: number;\n maxItems?: number;\n itemLabel?: string;\n onItemChange?: (\n itemIndex: number,\n field: string,\n value: any,\n allItems: any[]\n ) => any[];\n isEmptyItem?: (item: any) => boolean;\n}\n\nexport interface CheckboxFieldConfig extends BaseFieldConfig {\n type: 'checkbox';\n size?: number;\n animateChange?: string;\n}\n\nexport interface MessageEditorFieldConfig extends BaseFieldConfig {\n type: 'message-editor';\n placeholder?: string;\n minHeight?: number;\n maxAttachments?: number;\n accept?: string;\n endpoint?: string;\n counter?: string;\n gsm?: boolean;\n autogrow?: boolean;\n disableCompletion?: boolean;\n}\n\nexport type FieldConfig =\n | TextFieldConfig\n | TextareaFieldConfig\n | SelectFieldConfig\n | KeyValueFieldConfig\n | ArrayFieldConfig\n | CheckboxFieldConfig\n | MessageEditorFieldConfig;\n\n// Layout configurations for better form organization\n// Recursive layout system - any layout item can contain other layout items\n\nexport interface FieldItemConfig {\n type: 'field';\n field: string; // field name to render\n}\n\nexport interface RowLayoutConfig {\n type: 'row';\n items: LayoutItem[]; // can contain fields, groups, or other rows\n gap?: string; // CSS gap value, defaults to '1rem'\n}\n\nexport interface GroupLayoutConfig {\n type: 'group';\n label: string;\n items: LayoutItem[]; // can contain fields, rows, or other groups\n collapsible?: boolean;\n collapsed?: boolean | ((formData: any) => boolean); // initial state if collapsible - can be a function\n helpText?: string;\n getGroupValueCount?: (formData: any) => number; // optional function to get count for bubble display\n}\n\nexport type LayoutItem =\n | FieldItemConfig\n | RowLayoutConfig\n | GroupLayoutConfig\n | string; // string is shorthand for field\n\nexport interface ActionConfig {\n name: string;\n color: string;\n evaluated?: string[];\n render?: (node: any, action: any) => TemplateResult;\n\n form?: Record<string, FieldConfig>;\n layout?: LayoutItem[]; // optional layout configuration - array of layout items\n\n // Action editor configuration (legacy)\n // Form-level transformations\n sanitize?: (formData: any) => any;\n toFormData?: (action: Action) => any;\n fromFormData?: (formData: any) => Action;\n\n validate?: (action: Action) => ValidationResult;\n}\n\nexport const COLORS = {\n send: '#3498db',\n update: '#01c1af',\n broadcast: '#8e5ea7',\n call: '#e68628',\n create: '#df419f',\n save: '#1a777c',\n split: '#aaaaaa',\n execute: '#666666',\n wait: '#4d7dad',\n add: '#309c42',\n remove: '#e74c3c'\n};\n\n// Default property type mappings\nexport function getDefaultComponent(value: any): WidgetConfig['type'] {\n if (typeof value === 'boolean') {\n return 'temba-checkbox';\n }\n if (typeof value === 'number') {\n return 'temba-textinput';\n }\n if (Array.isArray(value)) {\n return 'temba-select'; // For arrays, use multi-select\n }\n // Default to text input for strings and unknown types\n return 'temba-textinput';\n}\n\n// Get component properties for default mappings with proper typing\nexport function getDefaultComponentProps(value: any): PropertyConfig {\n if (typeof value === 'boolean') {\n return {\n widget: { type: 'temba-checkbox' }\n };\n }\n if (typeof value === 'number') {\n return {\n widget: {\n type: 'temba-textinput',\n attributes: { type: 'number' }\n }\n };\n }\n if (Array.isArray(value)) {\n if (value.length > 0 && typeof value[0] === 'string') {\n return {\n widget: {\n type: 'temba-select',\n attributes: { multi: true, tags: true }\n }\n };\n }\n return {\n widget: {\n type: 'temba-select',\n attributes: { multi: true }\n }\n };\n }\n return {\n widget: { type: 'temba-textinput' }\n };\n}\n\n// Type guard functions for working with WidgetConfig\nexport function isTextInputWidget(\n config: WidgetConfig\n): config is { type: 'temba-textinput'; attributes?: TextInputAttributes } {\n return config.type === 'temba-textinput';\n}\n\nexport function isCompletionWidget(\n config: WidgetConfig\n): config is { type: 'temba-completion'; attributes?: CompletionAttributes } {\n return config.type === 'temba-completion';\n}\n\nexport function isSelectWidget(\n config: WidgetConfig\n): config is { type: 'temba-select'; attributes?: SelectAttributes } {\n return config.type === 'temba-select';\n}\n\nexport function isCheckboxWidget(\n config: WidgetConfig\n): config is { type: 'temba-checkbox'; attributes?: CheckboxAttributes } {\n return config.type === 'temba-checkbox';\n}\n\nexport function isSliderWidget(\n config: WidgetConfig\n): config is { type: 'slider'; attributes?: SliderAttributes } {\n return config.type === 'temba-slider';\n}\n"]}
@@ -7,6 +7,7 @@ let TembaArrayEditor = class TembaArrayEditor extends BaseListEditor {
7
7
  super();
8
8
  this.itemConfig = {};
9
9
  this.itemLabel = 'Item';
10
+ this.maintainEmptyItem = true; // Enable by default for better UX
10
11
  this._items = [];
11
12
  }
12
13
  // External API
@@ -19,7 +20,25 @@ let TembaArrayEditor = class TembaArrayEditor extends BaseListEditor {
19
20
  }
20
21
  // Implement abstract methods
21
22
  isEmptyItem(item) {
22
- return Object.values(item).every((value) => value === undefined || value === null || value === '');
23
+ // Use configurable function if provided
24
+ if (this.isEmptyItemFn) {
25
+ return this.isEmptyItemFn(item);
26
+ }
27
+ // Default behavior: check if all values are empty
28
+ const values = Object.values(item);
29
+ if (values.length === 0) {
30
+ return true;
31
+ }
32
+ return values.every((value) => value === undefined || value === null || value === '');
33
+ }
34
+ // Override cleanItems to be more permissive for form data
35
+ cleanItems(items) {
36
+ // For runtime attachments, keep items that have at least one non-empty field
37
+ return items.filter((item) => {
38
+ const values = Object.values(item);
39
+ return (values.length > 0 &&
40
+ values.some((value) => value !== undefined && value !== null && value !== ''));
41
+ });
23
42
  }
24
43
  createEmptyItem() {
25
44
  return {};
@@ -44,9 +63,17 @@ let TembaArrayEditor = class TembaArrayEditor extends BaseListEditor {
44
63
  if (config.computeValue) {
45
64
  return config.computeValue(item, currentValue);
46
65
  }
66
+ // For select fields, ensure we return the right type
67
+ if (config.type === 'select') {
68
+ const selectConfig = config;
69
+ if (currentValue === undefined || currentValue === null) {
70
+ return selectConfig.multi ? [] : '';
71
+ }
72
+ }
47
73
  return currentValue;
48
74
  }
49
75
  renderField(itemIndex, fieldName, config) {
76
+ var _a;
50
77
  const computedValue = this.computeFieldValue(itemIndex, fieldName, config);
51
78
  switch (config.type) {
52
79
  case 'text':
@@ -63,12 +90,64 @@ let TembaArrayEditor = class TembaArrayEditor extends BaseListEditor {
63
90
  .rows=${config.rows || 3}
64
91
  @change=${(e) => this.handleFieldChange(itemIndex, fieldName, e.target.value)}
65
92
  ></temba-textinput>`;
66
- case 'select':
93
+ case 'select': {
94
+ const selectConfig = config;
95
+ const fieldValue = this.computeFieldValue(itemIndex, fieldName, config);
67
96
  return html `<temba-select
68
- .value=${computedValue || ''}
69
- .options=${config.options}
70
- @change=${(e) => this.handleFieldChange(itemIndex, fieldName, e.target.value)}
71
- ></temba-select>`;
97
+ class="form-control"
98
+ ?clearable="${selectConfig.clearable || false}"
99
+ ?searchable="${selectConfig.searchable || false}"
100
+ ?tags="${selectConfig.tags || false}"
101
+ ?multi="${selectConfig.multi || false}"
102
+ ?emails="${selectConfig.emails || false}"
103
+ placeholder="${selectConfig.placeholder || ''}"
104
+ maxItems="${selectConfig.maxItems || 0}"
105
+ valueKey="${selectConfig.valueKey || 'value'}"
106
+ nameKey="${selectConfig.nameKey || 'name'}"
107
+ endpoint="${selectConfig.endpoint || ''}"
108
+ value="${fieldValue || ''}"
109
+ flavor="small"
110
+ @change="${(e) => {
111
+ const target = e.target;
112
+ let value;
113
+ // For temba-select, extract the correct value
114
+ if (target.tagName === 'TEMBA-SELECT') {
115
+ if (target.multi || target.emails || target.tags) {
116
+ value = target.values || [];
117
+ }
118
+ else {
119
+ // Single select: extract value from first selected option
120
+ const values = target.values || [];
121
+ value =
122
+ values.length > 0 && values[0]
123
+ ? values[0].value !== undefined
124
+ ? values[0].value
125
+ : values[0]
126
+ : '';
127
+ }
128
+ }
129
+ else {
130
+ value = target.value;
131
+ }
132
+ this.handleFieldChange(itemIndex, fieldName, value);
133
+ }}"
134
+ >
135
+ ${(_a = selectConfig.options) === null || _a === void 0 ? void 0 : _a.map((option) => {
136
+ if (typeof option === 'string') {
137
+ return html `<temba-option
138
+ name="${option}"
139
+ value="${option}"
140
+ ></temba-option>`;
141
+ }
142
+ else {
143
+ return html `<temba-option
144
+ name="${option.label || option.name}"
145
+ value="${option.value}"
146
+ ></temba-option>`;
147
+ }
148
+ })}
149
+ </temba-select>`;
150
+ }
72
151
  default:
73
152
  return html `<span>Unsupported field type: ${config.type}</span>`;
74
153
  }
@@ -77,27 +156,23 @@ let TembaArrayEditor = class TembaArrayEditor extends BaseListEditor {
77
156
  const canRemove = this.canRemoveItem(index);
78
157
  return html `
79
158
  <div class="array-item">
80
- <div class="item-header">
81
- <span class="item-title">${this.itemLabel} ${index + 1}</span>
159
+ <div class="item-fields">
160
+ ${Object.entries(this.itemConfig).map(([fieldName, config]) => html `
161
+ <div class="field">
162
+ ${this.renderField(index, fieldName, config)}
163
+ </div>
164
+ `)}
82
165
  ${canRemove
83
166
  ? html `
84
167
  <button
85
168
  @click=${() => this.removeItem(index)}
86
169
  class="remove-btn"
87
170
  >
88
- Remove
171
+ <temba-icon name="x"></temba-icon>
89
172
  </button>
90
173
  `
91
174
  : ''}
92
175
  </div>
93
- <div class="item-fields">
94
- ${Object.entries(this.itemConfig).map(([fieldName, config]) => html `
95
- <div class="field">
96
- <label>${config.label}${config.required ? ' *' : ''}</label>
97
- ${this.renderField(index, fieldName, config)}
98
- </div>
99
- `)}
100
- </div>
101
176
  </div>
102
177
  `;
103
178
  }
@@ -114,27 +189,15 @@ let TembaArrayEditor = class TembaArrayEditor extends BaseListEditor {
114
189
  };
115
190
  TembaArrayEditor.styles = css `
116
191
  .array-editor {
117
- border: 1px solid #e0e0e0;
118
- border-radius: 6px;
119
- padding: 16px;
120
- background: #fafafa;
121
192
  }
122
193
 
123
194
  .array-item {
124
- border: 1px solid #d0d0d0;
125
- border-radius: 4px;
126
- padding: 16px;
127
- margin-bottom: 12px;
128
- background: white;
129
195
  }
130
196
 
131
197
  .item-header {
132
198
  display: flex;
133
199
  justify-content: space-between;
134
200
  align-items: center;
135
- margin-bottom: 12px;
136
- padding-bottom: 8px;
137
- border-bottom: 1px solid #eee;
138
201
  }
139
202
 
140
203
  .item-title {
@@ -143,8 +206,17 @@ TembaArrayEditor.styles = css `
143
206
  }
144
207
 
145
208
  .item-fields {
146
- display: grid;
209
+ display: flex;
147
210
  gap: 12px;
211
+ align-items: center;
212
+ }
213
+
214
+ .field {
215
+ flex: 1;
216
+ }
217
+
218
+ .field:first-child {
219
+ flex: 0 0 140px; /* Fixed width for type dropdown */
148
220
  }
149
221
 
150
222
  .field label {
@@ -157,7 +229,7 @@ TembaArrayEditor.styles = css `
157
229
 
158
230
  .add-btn,
159
231
  .remove-btn {
160
- padding: 8px 16px;
232
+ padding: 8px;
161
233
  border: 1px solid #ccc;
162
234
  border-radius: 4px;
163
235
  background: white;
@@ -171,13 +243,8 @@ TembaArrayEditor.styles = css `
171
243
  }
172
244
 
173
245
  .remove-btn {
174
- background: #fff5f5;
175
- border-color: #fecaca;
176
- color: #dc2626;
177
- }
178
-
179
- .remove-btn:hover {
180
- background: #fef2f2;
246
+ background: #fefefe;
247
+ color: #999;
181
248
  }
182
249
  `;
183
250
  __decorate([
@@ -189,6 +256,12 @@ __decorate([
189
256
  __decorate([
190
257
  property({ type: Function })
191
258
  ], TembaArrayEditor.prototype, "onItemChange", void 0);
259
+ __decorate([
260
+ property({ type: Function })
261
+ ], TembaArrayEditor.prototype, "isEmptyItemFn", void 0);
262
+ __decorate([
263
+ property({ type: Boolean })
264
+ ], TembaArrayEditor.prototype, "maintainEmptyItem", void 0);
192
265
  __decorate([
193
266
  property({ type: Array })
194
267
  ], TembaArrayEditor.prototype, "value", null);