@nyaruka/temba-components 0.129.7 → 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 (127) 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 +8 -3
  5. package/demo/components/flow/example.html +1 -1
  6. package/demo/components/message-editor/example.html +125 -0
  7. package/demo/components/textinput/completion.html +1 -0
  8. package/demo/data/flows/food-order.json +12 -21
  9. package/demo/data/flows/sample-flow.json +42 -26
  10. package/dist/temba-components.js +506 -218
  11. package/dist/temba-components.js.map +1 -1
  12. package/out-tsc/src/display/Thumbnail.js +2 -1
  13. package/out-tsc/src/display/Thumbnail.js.map +1 -1
  14. package/out-tsc/src/events.js.map +1 -1
  15. package/out-tsc/src/flow/NodeEditor.js +245 -22
  16. package/out-tsc/src/flow/NodeEditor.js.map +1 -1
  17. package/out-tsc/src/flow/actions/call_webhook.js +26 -17
  18. package/out-tsc/src/flow/actions/call_webhook.js.map +1 -1
  19. package/out-tsc/src/flow/actions/send_msg.js +147 -6
  20. package/out-tsc/src/flow/actions/send_msg.js.map +1 -1
  21. package/out-tsc/src/flow/types.js.map +1 -1
  22. package/out-tsc/src/form/ArrayEditor.js +111 -38
  23. package/out-tsc/src/form/ArrayEditor.js.map +1 -1
  24. package/out-tsc/src/form/BaseListEditor.js +19 -4
  25. package/out-tsc/src/form/BaseListEditor.js.map +1 -1
  26. package/out-tsc/src/form/FormField.js +1 -1
  27. package/out-tsc/src/form/FormField.js.map +1 -1
  28. package/out-tsc/src/form/KeyValueEditor.js +1 -1
  29. package/out-tsc/src/form/KeyValueEditor.js.map +1 -1
  30. package/out-tsc/src/form/MediaPicker.js +13 -1
  31. package/out-tsc/src/form/MediaPicker.js.map +1 -1
  32. package/out-tsc/src/form/MessageEditor.js +422 -0
  33. package/out-tsc/src/form/MessageEditor.js.map +1 -0
  34. package/out-tsc/src/form/TextInput.js +12 -5
  35. package/out-tsc/src/form/TextInput.js.map +1 -1
  36. package/out-tsc/src/form/select/Select.js +4 -4
  37. package/out-tsc/src/form/select/Select.js.map +1 -1
  38. package/out-tsc/src/live/ContactChat.js +27 -2
  39. package/out-tsc/src/live/ContactChat.js.map +1 -1
  40. package/out-tsc/temba-modules.js +2 -0
  41. package/out-tsc/temba-modules.js.map +1 -1
  42. package/out-tsc/test/temba-field-config.test.js +4 -2
  43. package/out-tsc/test/temba-field-config.test.js.map +1 -1
  44. package/out-tsc/test/temba-message-editor.test.js +194 -0
  45. package/out-tsc/test/temba-message-editor.test.js.map +1 -0
  46. package/out-tsc/test/temba-node-editor.test.js +71 -0
  47. package/out-tsc/test/temba-node-editor.test.js.map +1 -1
  48. package/out-tsc/test/temba-select.test.js +1 -1
  49. package/out-tsc/test/temba-select.test.js.map +1 -1
  50. package/out-tsc/test/temba-textinput.test.js +16 -0
  51. package/out-tsc/test/temba-textinput.test.js.map +1 -1
  52. package/out-tsc/test/temba-webchat.test.js +4 -0
  53. package/out-tsc/test/temba-webchat.test.js.map +1 -1
  54. package/out-tsc/test/utils.test.js +2 -8
  55. package/out-tsc/test/utils.test.js.map +1 -1
  56. package/package.json +7 -4
  57. package/screenshots/truth/actions/add_contact_groups/editor/descriptive-group-names.png +0 -0
  58. package/screenshots/truth/actions/add_contact_groups/editor/long-group-names.png +0 -0
  59. package/screenshots/truth/actions/add_contact_groups/editor/many-groups.png +0 -0
  60. package/screenshots/truth/actions/add_contact_groups/editor/multiple-groups.png +0 -0
  61. package/screenshots/truth/actions/add_contact_groups/editor/single-group.png +0 -0
  62. package/screenshots/truth/actions/remove_contact_groups/editor/cleanup-groups.png +0 -0
  63. package/screenshots/truth/actions/remove_contact_groups/editor/long-descriptive-group-names.png +0 -0
  64. package/screenshots/truth/actions/remove_contact_groups/editor/many-groups.png +0 -0
  65. package/screenshots/truth/actions/remove_contact_groups/editor/multiple-groups.png +0 -0
  66. package/screenshots/truth/actions/remove_contact_groups/editor/single-group.png +0 -0
  67. package/screenshots/truth/actions/send_email/editor/complex-business-email.png +0 -0
  68. package/screenshots/truth/actions/send_email/editor/empty-body.png +0 -0
  69. package/screenshots/truth/actions/send_email/editor/empty-subject.png +0 -0
  70. package/screenshots/truth/actions/send_email/editor/long-subject.png +0 -0
  71. package/screenshots/truth/actions/send_email/editor/multiline-body.png +0 -0
  72. package/screenshots/truth/actions/send_email/editor/multiple-recipients.png +0 -0
  73. package/screenshots/truth/actions/send_email/editor/simple-email.png +0 -0
  74. package/screenshots/truth/actions/send_email/editor/with-expressions.png +0 -0
  75. package/screenshots/truth/actions/send_msg/editor/long-quick-replies.png +0 -0
  76. package/screenshots/truth/actions/send_msg/editor/multiline-text-with-replies.png +0 -0
  77. package/screenshots/truth/actions/send_msg/editor/simple-text.png +0 -0
  78. package/screenshots/truth/actions/send_msg/editor/text-with-linebreaks.png +0 -0
  79. package/screenshots/truth/actions/send_msg/editor/text-with-many-quick-replies.png +0 -0
  80. package/screenshots/truth/actions/send_msg/editor/text-with-quick-replies.png +0 -0
  81. package/screenshots/truth/actions/send_msg/editor/text-without-quick-replies.png +0 -0
  82. package/screenshots/truth/editor/send_msg.png +0 -0
  83. package/screenshots/truth/editor/set_contact_language.png +0 -0
  84. package/screenshots/truth/editor/set_contact_name.png +0 -0
  85. package/screenshots/truth/editor/set_run_result.png +0 -0
  86. package/screenshots/truth/formfield/markdown-errors.png +0 -0
  87. package/screenshots/truth/formfield/no-errors.png +0 -0
  88. package/screenshots/truth/formfield/plain-text-errors.png +0 -0
  89. package/screenshots/truth/message-editor/autogrow-initial-content.png +0 -0
  90. package/screenshots/truth/message-editor/default.png +0 -0
  91. package/screenshots/truth/message-editor/drag-highlight.png +0 -0
  92. package/screenshots/truth/message-editor/filtered-attachments.png +0 -0
  93. package/screenshots/truth/message-editor/with-completion.png +0 -0
  94. package/screenshots/truth/message-editor/with-properties.png +0 -0
  95. package/screenshots/truth/textinput/autogrow-initial.png +0 -0
  96. package/screenshots/truth/textinput/input-form.png +0 -0
  97. package/src/display/Thumbnail.ts +2 -1
  98. package/src/events.ts +5 -0
  99. package/src/flow/NodeEditor.ts +269 -23
  100. package/src/flow/actions/call_webhook.ts +28 -18
  101. package/src/flow/actions/send_msg.ts +170 -6
  102. package/src/flow/types.ts +21 -2
  103. package/src/form/ArrayEditor.ts +120 -42
  104. package/src/form/BaseListEditor.ts +22 -6
  105. package/src/form/FormField.ts +1 -1
  106. package/src/form/KeyValueEditor.ts +1 -1
  107. package/src/form/MediaPicker.ts +13 -1
  108. package/src/form/MessageEditor.ts +449 -0
  109. package/src/form/TextInput.ts +15 -7
  110. package/src/form/select/Select.ts +4 -4
  111. package/src/live/ContactChat.ts +30 -4
  112. package/static/css/temba-components.css +2 -0
  113. package/static/mr/docs/en-us/editor.json +2588 -0
  114. package/stress-test.js +138 -0
  115. package/temba-modules.ts +2 -0
  116. package/test/temba-field-config.test.ts +4 -2
  117. package/test/temba-message-editor.test.ts +300 -0
  118. package/test/temba-node-editor.test.ts +94 -0
  119. package/test/temba-select.test.ts +1 -1
  120. package/test/temba-textinput.test.ts +26 -0
  121. package/test/temba-webchat.test.ts +5 -0
  122. package/test/utils.test.ts +2 -13
  123. package/test-assets/contacts/history.json +19 -0
  124. package/test-assets/style.css +2 -0
  125. package/web-dev-mock.mjs +433 -0
  126. package/web-dev-server.config.mjs +51 -5
  127. package/web-test-runner.config.mjs +9 -4
@@ -59,7 +59,8 @@ export class Thumbnail extends RapidElement {
59
59
  background-repeat: no-repeat;
60
60
  border-radius: var(--curvature);
61
61
  max-height: calc(var(--thumb-size, 4em) * 2);
62
- width: var(--thumb-size, 4em);
62
+ max-width: calc(var(--thumb-size, 4em) * 2);
63
+ height: var(--thumb-size, 4em);
63
64
  display: flex;
64
65
  align-items: center;
65
66
  justify-content: center;
@@ -1 +1 @@
1
- {"version":3,"file":"Thumbnail.js","sourceRoot":"","sources":["../../../src/display/Thumbnail.ts"],"names":[],"mappings":"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","sourcesContent":["import { PropertyValueMap, css, html } from 'lit';\nimport { RapidElement } from '../RapidElement';\nimport { property } from 'lit/decorators.js';\nimport { getClasses } from '../utils';\nimport { Lightbox } from './Lightbox';\nimport { WebChatIcon } from '../webchat';\n\nenum ThumbnailContentType {\n IMAGE = 'image',\n AUDIO = 'audio',\n VIDEO = 'video',\n DOCUMENT = 'document',\n OTHER = 'other'\n}\n\nconst ThumbnailIcons = {\n [ThumbnailContentType.IMAGE]: WebChatIcon.attachment_image,\n [ThumbnailContentType.AUDIO]: WebChatIcon.attachment_audio,\n [ThumbnailContentType.VIDEO]: WebChatIcon.attachment_video,\n [ThumbnailContentType.DOCUMENT]: WebChatIcon.attachment_document,\n [ThumbnailContentType.OTHER]: WebChatIcon.attachment\n};\n\nexport class Thumbnail extends RapidElement {\n static get styles() {\n return css`\n :host {\n display: inline;\n }\n\n .wrapper {\n padding: var(--thumb-padding, 0.4em);\n background: var(--thumb-background, #fff);\n box-shadow: var(--widget-box-shadow);\n cursor: pointer;\n border-radius: calc(var(--curvature) * 1.5);\n border: 0px solid #f3f3f3;\n }\n\n .wrapper.zoom {\n border: none;\n padding: 0 !important;\n border-radius: 0 !important;\n overflow: hidden !important;\n }\n\n .zoom .thumb {\n border-radius: 0px !important;\n width: calc(var(--thumb-size, 4em) + 0.8em);\n max-height: calc(90vh - 10em);\n }\n\n .thumb {\n background-size: cover;\n background-position: center;\n background-repeat: no-repeat;\n border-radius: var(--curvature);\n max-height: calc(var(--thumb-size, 4em) * 2);\n width: var(--thumb-size, 4em);\n display: flex;\n align-items: center;\n justify-content: center;\n font-weight: 400;\n color: var(--thumb-icon, #bbb);\n }\n\n .thumb.document,\n .thumb.audio,\n .thumb.video {\n border: 1px solid #eee;\n }\n\n .wrapper:hover .thumb.icon {\n }\n\n .viewer {\n display: block;\n }\n\n .zoom .viewer {\n display: block;\n }\n\n .download {\n display: none;\n position: absolute;\n right: 0em;\n bottom: 0em;\n border-radius: var(--curvature);\n transform: scale(0.2) translate(3em, 3em);\n padding: 0.4em;\n }\n\n .zoom .download {\n display: block;\n background: rgba(0, 0, 0, 0.5);\n }\n\n .zoom .download:hover {\n background: rgba(0, 0, 0, 0.6);\n cursor: pointer;\n }\n `;\n }\n\n @property({ type: String })\n url: string;\n\n @property({ type: String })\n attachment: string;\n\n @property({ type: Number })\n ratio: number = 0;\n\n @property({ type: Boolean })\n preview: boolean = true;\n\n @property({ type: Boolean, attribute: false })\n zoom: boolean;\n\n @property({ type: String, attribute: true })\n contentType: string;\n\n protected updated(\n changes: PropertyValueMap<any> | Map<PropertyKey, unknown>\n ): void {\n super.updated(changes);\n\n if (\n changes.has('contentType') &&\n this.contentType === ThumbnailContentType.IMAGE\n ) {\n const toObserve = this.shadowRoot.querySelector('.observe');\n if (toObserve) {\n new ResizeObserver((e, observer) => {\n if (toObserve.clientHeight > 0 && toObserve.clientWidth > 0) {\n this.ratio = toObserve.clientHeight / toObserve.clientWidth;\n this.preview =\n this.ratio === 0 || (this.ratio > 0.25 && this.ratio <= 1.5);\n observer.disconnect();\n }\n }).observe(toObserve);\n }\n }\n\n // convert our attachment to a url and label\n if (changes.has('attachment')) {\n if (this.attachment) {\n const splitIndex = this.attachment.indexOf(':');\n if (splitIndex === -1) {\n this.url = this.attachment;\n } else {\n const contentType = this.attachment.substring(0, splitIndex);\n this.url = this.attachment.substring(splitIndex + 1);\n\n if (contentType.startsWith('image')) {\n this.contentType = ThumbnailContentType.IMAGE;\n } else if (contentType.startsWith('audio')) {\n this.contentType = ThumbnailContentType.AUDIO;\n } else if (contentType.startsWith('video')) {\n this.contentType = ThumbnailContentType.VIDEO;\n } else if (contentType.startsWith('application')) {\n this.contentType = ThumbnailContentType.DOCUMENT;\n } else {\n this.contentType = ThumbnailContentType.OTHER;\n }\n }\n }\n }\n }\n\n public handleThumbnailClicked() {\n if (this.contentType === ThumbnailContentType.IMAGE && this.preview) {\n window.setTimeout(() => {\n const lightbox = document.querySelector('temba-lightbox') as Lightbox;\n lightbox.showElement(this);\n }, 100);\n } else {\n window.open(this.url, '_blank');\n }\n }\n\n public handleDownload(e: Event) {\n e.stopPropagation();\n e.preventDefault();\n\n // open this.url in another tab\n window.open(this.url, '_blank');\n }\n\n public render() {\n return html`\n <div\n @click=${this.handleThumbnailClicked.bind(this)}\n class=\"${getClasses({ wrapper: true, zoom: this.zoom })}\"\n url=${this.url}\n >\n ${this.contentType === ThumbnailContentType.IMAGE && this.preview\n ? html`<div class=\"\"><div class=\"download\" @click=${this.handleDownload.bind(\n this\n )}><temba-icon size=\"1\" style=\"color:#fff;\" name=\"download\"></temba-icon></div><img\n class=\"observe thumb ${this.contentType}\"\n src=\"${this.url}\"\n ></img></div>`\n : html`<div\n style=\"padding:1em; background:rgba(0,0,0,.05);border-radius:var(--curvature);\"\n >\n <temba-icon\n size=\"1.5\"\n name=\"${ThumbnailIcons[this.contentType]}\"\n ></temba-icon>\n </div>`}\n </div>\n `;\n }\n}\n"]}
1
+ {"version":3,"file":"Thumbnail.js","sourceRoot":"","sources":["../../../src/display/Thumbnail.ts"],"names":[],"mappings":"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","sourcesContent":["import { PropertyValueMap, css, html } from 'lit';\nimport { RapidElement } from '../RapidElement';\nimport { property } from 'lit/decorators.js';\nimport { getClasses } from '../utils';\nimport { Lightbox } from './Lightbox';\nimport { WebChatIcon } from '../webchat';\n\nenum ThumbnailContentType {\n IMAGE = 'image',\n AUDIO = 'audio',\n VIDEO = 'video',\n DOCUMENT = 'document',\n OTHER = 'other'\n}\n\nconst ThumbnailIcons = {\n [ThumbnailContentType.IMAGE]: WebChatIcon.attachment_image,\n [ThumbnailContentType.AUDIO]: WebChatIcon.attachment_audio,\n [ThumbnailContentType.VIDEO]: WebChatIcon.attachment_video,\n [ThumbnailContentType.DOCUMENT]: WebChatIcon.attachment_document,\n [ThumbnailContentType.OTHER]: WebChatIcon.attachment\n};\n\nexport class Thumbnail extends RapidElement {\n static get styles() {\n return css`\n :host {\n display: inline;\n }\n\n .wrapper {\n padding: var(--thumb-padding, 0.4em);\n background: var(--thumb-background, #fff);\n box-shadow: var(--widget-box-shadow);\n cursor: pointer;\n border-radius: calc(var(--curvature) * 1.5);\n border: 0px solid #f3f3f3;\n }\n\n .wrapper.zoom {\n border: none;\n padding: 0 !important;\n border-radius: 0 !important;\n overflow: hidden !important;\n }\n\n .zoom .thumb {\n border-radius: 0px !important;\n width: calc(var(--thumb-size, 4em) + 0.8em);\n max-height: calc(90vh - 10em);\n }\n\n .thumb {\n background-size: cover;\n background-position: center;\n background-repeat: no-repeat;\n border-radius: var(--curvature);\n max-height: calc(var(--thumb-size, 4em) * 2);\n max-width: calc(var(--thumb-size, 4em) * 2);\n height: var(--thumb-size, 4em);\n display: flex;\n align-items: center;\n justify-content: center;\n font-weight: 400;\n color: var(--thumb-icon, #bbb);\n }\n\n .thumb.document,\n .thumb.audio,\n .thumb.video {\n border: 1px solid #eee;\n }\n\n .wrapper:hover .thumb.icon {\n }\n\n .viewer {\n display: block;\n }\n\n .zoom .viewer {\n display: block;\n }\n\n .download {\n display: none;\n position: absolute;\n right: 0em;\n bottom: 0em;\n border-radius: var(--curvature);\n transform: scale(0.2) translate(3em, 3em);\n padding: 0.4em;\n }\n\n .zoom .download {\n display: block;\n background: rgba(0, 0, 0, 0.5);\n }\n\n .zoom .download:hover {\n background: rgba(0, 0, 0, 0.6);\n cursor: pointer;\n }\n `;\n }\n\n @property({ type: String })\n url: string;\n\n @property({ type: String })\n attachment: string;\n\n @property({ type: Number })\n ratio: number = 0;\n\n @property({ type: Boolean })\n preview: boolean = true;\n\n @property({ type: Boolean, attribute: false })\n zoom: boolean;\n\n @property({ type: String, attribute: true })\n contentType: string;\n\n protected updated(\n changes: PropertyValueMap<any> | Map<PropertyKey, unknown>\n ): void {\n super.updated(changes);\n\n if (\n changes.has('contentType') &&\n this.contentType === ThumbnailContentType.IMAGE\n ) {\n const toObserve = this.shadowRoot.querySelector('.observe');\n if (toObserve) {\n new ResizeObserver((e, observer) => {\n if (toObserve.clientHeight > 0 && toObserve.clientWidth > 0) {\n this.ratio = toObserve.clientHeight / toObserve.clientWidth;\n this.preview =\n this.ratio === 0 || (this.ratio > 0.25 && this.ratio <= 1.5);\n observer.disconnect();\n }\n }).observe(toObserve);\n }\n }\n\n // convert our attachment to a url and label\n if (changes.has('attachment')) {\n if (this.attachment) {\n const splitIndex = this.attachment.indexOf(':');\n if (splitIndex === -1) {\n this.url = this.attachment;\n } else {\n const contentType = this.attachment.substring(0, splitIndex);\n this.url = this.attachment.substring(splitIndex + 1);\n\n if (contentType.startsWith('image')) {\n this.contentType = ThumbnailContentType.IMAGE;\n } else if (contentType.startsWith('audio')) {\n this.contentType = ThumbnailContentType.AUDIO;\n } else if (contentType.startsWith('video')) {\n this.contentType = ThumbnailContentType.VIDEO;\n } else if (contentType.startsWith('application')) {\n this.contentType = ThumbnailContentType.DOCUMENT;\n } else {\n this.contentType = ThumbnailContentType.OTHER;\n }\n }\n }\n }\n }\n\n public handleThumbnailClicked() {\n if (this.contentType === ThumbnailContentType.IMAGE && this.preview) {\n window.setTimeout(() => {\n const lightbox = document.querySelector('temba-lightbox') as Lightbox;\n lightbox.showElement(this);\n }, 100);\n } else {\n window.open(this.url, '_blank');\n }\n }\n\n public handleDownload(e: Event) {\n e.stopPropagation();\n e.preventDefault();\n\n // open this.url in another tab\n window.open(this.url, '_blank');\n }\n\n public render() {\n return html`\n <div\n @click=${this.handleThumbnailClicked.bind(this)}\n class=\"${getClasses({ wrapper: true, zoom: this.zoom })}\"\n url=${this.url}\n >\n ${this.contentType === ThumbnailContentType.IMAGE && this.preview\n ? html`<div class=\"\"><div class=\"download\" @click=${this.handleDownload.bind(\n this\n )}><temba-icon size=\"1\" style=\"color:#fff;\" name=\"download\"></temba-icon></div><img\n class=\"observe thumb ${this.contentType}\"\n src=\"${this.url}\"\n ></img></div>`\n : html`<div\n style=\"padding:1em; background:rgba(0,0,0,.05);border-radius:var(--curvature);\"\n >\n <temba-icon\n size=\"1.5\"\n name=\"${ThumbnailIcons[this.contentType]}\"\n ></temba-icon>\n </div>`}\n </div>\n `;\n }\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"events.js","sourceRoot":"","sources":["../../src/events.ts"],"names":[],"mappings":"","sourcesContent":["import { Msg, ObjectReference, User } from './interfaces';\n\nexport interface EventGroup {\n type: string;\n events: ContactEvent[];\n open: boolean;\n}\n\nexport interface ContactEvent {\n uuid?: string;\n type: string;\n created_on: string;\n created_by?: User;\n}\n\nexport interface ChannelEvent extends ContactEvent {\n channel_event_type: string;\n duration: number;\n\n event: {\n type: string;\n channel: { uuid: string; name: string };\n duration?: number;\n optin?: {\n uuid: string;\n name: string;\n };\n };\n}\n\nexport interface ContactLanguageChangedEvent extends ContactEvent {\n language: string;\n step_uuid: string;\n session_uuid: string;\n}\n\nexport interface OptinRequestedEvent extends ContactEvent {\n optin: {\n uuid: string;\n name: string;\n };\n}\n\nexport interface MsgEvent extends ContactEvent {\n msg: Msg;\n status: string;\n failed_reason?: string;\n failed_reason_display?: string;\n logs_url: string;\n recipient_count?: number;\n created_by?: User;\n optin?: ObjectReference;\n}\n\nexport interface FlowEvent extends ContactEvent {\n flow: ObjectReference;\n status: string;\n}\n\nexport interface URNsChangedEvent extends ContactEvent {\n urns: string[];\n}\n\nexport interface TicketEvent extends ContactEvent {\n note?: string;\n assignee?: User;\n ticket: {\n uuid: string;\n topic?: ObjectReference;\n closed_on?: string;\n opened_on?: string;\n };\n topic?: ObjectReference;\n created_by?: User;\n}\n\nexport interface NameChangedEvent extends ContactEvent {\n name: string;\n}\n\nexport interface UpdateFieldEvent extends ContactEvent {\n field: { key: string; name: string };\n value: { text: string };\n}\n\nexport interface ContactGroupsEvent extends ContactEvent {\n groups_added: ObjectReference[];\n groups_removed: ObjectReference[];\n}\n\nexport interface AirtimeTransferredEvent extends ContactEvent {\n sender: string;\n recipient: string;\n currency: string;\n amount: string;\n}\n\nexport type CallStartedEvent = ContactEvent;\n\nexport interface ContactHistoryPage {\n has_older: boolean;\n recent_only: boolean;\n next_before: number;\n next_after: number;\n start_date: Date;\n events: ContactEvent[];\n}\n"]}
1
+ {"version":3,"file":"events.js","sourceRoot":"","sources":["../../src/events.ts"],"names":[],"mappings":"","sourcesContent":["import { Msg, ObjectReference, User } from './interfaces';\n\nexport interface EventGroup {\n type: string;\n events: ContactEvent[];\n open: boolean;\n}\n\nexport interface ContactEvent {\n uuid?: string;\n type: string;\n created_on: string;\n created_by?: User;\n}\n\nexport interface ChannelEvent extends ContactEvent {\n channel_event_type: string;\n duration: number;\n\n event: {\n type: string;\n channel: { uuid: string; name: string };\n duration?: number;\n optin?: {\n uuid: string;\n name: string;\n };\n };\n}\n\nexport interface ContactLanguageChangedEvent extends ContactEvent {\n language: string;\n step_uuid: string;\n session_uuid: string;\n}\n\nexport interface OptinRequestedEvent extends ContactEvent {\n optin: {\n uuid: string;\n name: string;\n };\n}\n\nexport interface MsgEvent extends ContactEvent {\n msg: Msg;\n status: string;\n failed_reason?: string;\n failed_reason_display?: string;\n logs_url: string;\n recipient_count?: number;\n created_by?: User;\n optin?: ObjectReference;\n}\n\nexport interface FlowEvent extends ContactEvent {\n flow: ObjectReference;\n status: string;\n}\n\nexport interface RunEvent extends ContactEvent {\n flow: ObjectReference;\n status: string;\n}\n\nexport interface URNsChangedEvent extends ContactEvent {\n urns: string[];\n}\n\nexport interface TicketEvent extends ContactEvent {\n note?: string;\n assignee?: User;\n ticket: {\n uuid: string;\n topic?: ObjectReference;\n closed_on?: string;\n opened_on?: string;\n };\n topic?: ObjectReference;\n created_by?: User;\n}\n\nexport interface NameChangedEvent extends ContactEvent {\n name: string;\n}\n\nexport interface UpdateFieldEvent extends ContactEvent {\n field: { key: string; name: string };\n value: { text: string };\n}\n\nexport interface ContactGroupsEvent extends ContactEvent {\n groups_added: ObjectReference[];\n groups_removed: ObjectReference[];\n}\n\nexport interface AirtimeTransferredEvent extends ContactEvent {\n sender: string;\n recipient: string;\n currency: string;\n amount: string;\n}\n\nexport type CallStartedEvent = ContactEvent;\n\nexport interface ContactHistoryPage {\n has_older: boolean;\n recent_only: boolean;\n next_before: number;\n next_after: number;\n start_date: Date;\n events: ContactEvent[];\n}\n"]}
@@ -13,6 +13,7 @@ export class NodeEditor extends RapidElement {
13
13
  this.originalFormData = {};
14
14
  this.errors = {};
15
15
  this.groupCollapseState = {};
16
+ this.groupHoverState = {};
16
17
  }
17
18
  static get styles() {
18
19
  return css `
@@ -23,6 +24,10 @@ export class NodeEditor extends RapidElement {
23
24
  gap: 15px;
24
25
  min-width: 400px;
25
26
  padding-bottom: 40px;
27
+
28
+ --color-bubble-bg: rgba(255, 255, 255, 0.8);
29
+ --color-bubble-border: #999;
30
+ --color-bubble-text: #777;
26
31
  }
27
32
 
28
33
  .form-field {
@@ -31,10 +36,6 @@ export class NodeEditor extends RapidElement {
31
36
  }
32
37
 
33
38
  .form-field label {
34
- font-weight: 500;
35
- margin-bottom: 6px;
36
- color: #333;
37
- font-size: 14px;
38
39
  }
39
40
 
40
41
  .field-errors {
@@ -95,10 +96,16 @@ export class NodeEditor extends RapidElement {
95
96
  border-color: var(--color-error, tomato);
96
97
  }
97
98
 
99
+ .form-group.has-bubble {
100
+ border-width: 1px;
101
+ border-color: var(--color-bubble-border, #aaa);
102
+ }
103
+
98
104
  .form-group-header {
99
105
  background: #f8f9fa;
100
- padding: 12px 15px;
106
+ padding: 8px 10px;
101
107
  border-bottom: 1px solid #e0e0e0;
108
+
102
109
  display: flex;
103
110
  align-items: center;
104
111
  justify-content: space-between;
@@ -106,6 +113,17 @@ export class NodeEditor extends RapidElement {
106
113
  user-select: none;
107
114
  }
108
115
 
116
+ .form-group.has-bubble .form-group-header {
117
+ }
118
+
119
+ .collapsed .form-group-header {
120
+ border: none;
121
+ }
122
+
123
+ .form-group-header:hover {
124
+ background: rgba(0, 0, 0, 0.05);
125
+ }
126
+
109
127
  .form-group-header.collapsible:hover {
110
128
  background: #f1f3f4;
111
129
  }
@@ -116,7 +134,7 @@ export class NodeEditor extends RapidElement {
116
134
 
117
135
  .form-group-title {
118
136
  font-weight: 500;
119
- color: #333;
137
+ color: var(--color-label, #777);
120
138
  font-size: 14px;
121
139
  display: flex;
122
140
  }
@@ -139,13 +157,13 @@ export class NodeEditor extends RapidElement {
139
157
  }
140
158
 
141
159
  .form-group-content {
142
- padding: 15px;
160
+ padding: 6px;
143
161
  display: flex;
144
162
  flex-direction: column;
145
163
  gap: 15px;
146
164
  overflow: hidden;
147
- transition: all 0.3s ease;
148
- max-height: 1000px; /* Large enough to accommodate most content */
165
+ transition: all 0.2s ease-in-out;
166
+
149
167
  opacity: 1;
150
168
  }
151
169
 
@@ -158,9 +176,14 @@ export class NodeEditor extends RapidElement {
158
176
 
159
177
  .group-toggle-icon {
160
178
  color: #666;
161
- transition: transform 0.3s ease;
179
+ transition: transform 0.3s ease, opacity 0.3s ease;
162
180
  cursor: pointer;
163
181
  transform: rotate(0deg);
182
+ opacity: 1;
183
+ }
184
+
185
+ .group-toggle-icon.faded {
186
+ opacity: 0;
164
187
  }
165
188
 
166
189
  .group-toggle-icon.expanded {
@@ -179,6 +202,58 @@ export class NodeEditor extends RapidElement {
179
202
  color: var(--color-error, tomato);
180
203
  margin-right: 8px;
181
204
  }
205
+
206
+ .group-count-bubble {
207
+ border-radius: 50%;
208
+ display: flex;
209
+ align-items: center;
210
+ justify-content: center;
211
+ font-size: 11px;
212
+ font-weight: 600;
213
+ padding: 4px;
214
+ min-width: 12px;
215
+ min-height: 12px;
216
+ position: absolute;
217
+ top: 50%;
218
+ left: 50%;
219
+ transform: translate(-50%, -50%);
220
+ line-height: 0px;
221
+ opacity: 1;
222
+ transition: opacity 0.3s ease;
223
+ background: var(--color-bubble-bg, #fff);
224
+ border: 1px solid var(--color-bubble-border, #777);
225
+ color: var(--color-bubble-text, #000);
226
+ }
227
+
228
+ .group-count-bubble.hidden {
229
+ opacity: 0;
230
+ pointer-events: none;
231
+ }
232
+
233
+ .group-checkmark-icon {
234
+ position: absolute;
235
+ top: 50%;
236
+ left: 50%;
237
+ transform: translate(-50%, -50%);
238
+ opacity: 1;
239
+ transition: opacity 0.3s ease;
240
+ border-radius: 50%;
241
+ color: var(--color-bubble-text, #000);
242
+ background: var(--color-bubble-bg, #fff);
243
+ border: 1px solid var(--color-bubble-border, #777);
244
+ padding: 0.2em;
245
+ }
246
+
247
+ .group-checkmark-icon.hidden {
248
+ opacity: 0;
249
+ pointer-events: none;
250
+ }
251
+
252
+ .group-toggle-container {
253
+ position: relative;
254
+ display: flex;
255
+ align-items: center;
256
+ }
182
257
  `;
183
258
  }
184
259
  connectedCallback() {
@@ -206,6 +281,7 @@ export class NodeEditor extends RapidElement {
206
281
  this.formData = {};
207
282
  this.errors = {};
208
283
  this.groupCollapseState = {};
284
+ this.groupHoverState = {};
209
285
  }
210
286
  initializeFormData() {
211
287
  if (this.action) {
@@ -417,7 +493,7 @@ export class NodeEditor extends RapidElement {
417
493
  // Check required fields
418
494
  if (fieldConfig.required &&
419
495
  (!value || (Array.isArray(value) && value.length === 0))) {
420
- errors[fieldName] = `${fieldConfig.label || fieldName} is required`;
496
+ errors[fieldName] = `${fieldConfig.label || fieldName} is required.`;
421
497
  }
422
498
  // Check minLength for text fields
423
499
  if (typeof value === 'string' &&
@@ -437,6 +513,9 @@ export class NodeEditor extends RapidElement {
437
513
  if (actionConfig === null || actionConfig === void 0 ? void 0 : actionConfig.validate) {
438
514
  // Convert form data back to action for validation
439
515
  let actionForValidation;
516
+ if (actionConfig.sanitize) {
517
+ actionConfig.sanitize(this.formData);
518
+ }
440
519
  if (actionConfig.fromFormData) {
441
520
  actionForValidation = actionConfig.fromFormData(this.formData);
442
521
  }
@@ -693,9 +772,43 @@ export class NodeEditor extends RapidElement {
693
772
  }
694
773
  // Check for computed values in dependent fields
695
774
  this.updateComputedFields(propertyName);
775
+ // Re-evaluate group collapse states that depend on form data
776
+ this.updateGroupCollapseStates();
696
777
  // Trigger re-render to handle conditional field visibility
697
778
  this.requestUpdate();
698
779
  }
780
+ updateGroupCollapseStates() {
781
+ if (!this.action)
782
+ return;
783
+ const config = ACTION_CONFIG[this.action.type];
784
+ if (!(config === null || config === void 0 ? void 0 : config.layout))
785
+ return;
786
+ this.updateGroupCollapseStatesRecursive(config.layout);
787
+ }
788
+ updateGroupCollapseStatesRecursive(items) {
789
+ items.forEach((item) => {
790
+ if (typeof item === 'object' && item.type === 'group') {
791
+ const { label, collapsed, collapsible } = item;
792
+ // Only update if the group is collapsible and has a function-based collapsed property
793
+ if (collapsible && typeof collapsed === 'function') {
794
+ const newCollapsedState = collapsed(this.formData);
795
+ // Only update if the state has changed to avoid unnecessary re-renders
796
+ if (this.groupCollapseState[label] !== newCollapsedState) {
797
+ this.groupCollapseState = {
798
+ ...this.groupCollapseState,
799
+ [label]: newCollapsedState
800
+ };
801
+ }
802
+ }
803
+ // Recursively check nested items
804
+ this.updateGroupCollapseStatesRecursive(item.items);
805
+ }
806
+ else if (typeof item === 'object' && item.type === 'row') {
807
+ // Recursively check items in rows
808
+ this.updateGroupCollapseStatesRecursive(item.items);
809
+ }
810
+ });
811
+ }
699
812
  updateComputedFields(changedFieldName) {
700
813
  if (!this.action)
701
814
  return;
@@ -813,6 +926,7 @@ export class NodeEditor extends RapidElement {
813
926
  nameKey="${selectConfig.nameKey || 'name'}"
814
927
  endpoint="${selectConfig.endpoint || ''}"
815
928
  .helpText="${config.helpText || ''}"
929
+ flavor="${selectConfig.flavor || 'small'}"
816
930
  @change="${(e) => this.handleFormFieldChange(fieldName, e)}"
817
931
  >
818
932
  ${(_a = selectConfig.options) === null || _a === void 0 ? void 0 : _a.map((option) => {
@@ -860,8 +974,10 @@ export class NodeEditor extends RapidElement {
860
974
  .sortable="${config.sortable}"
861
975
  .itemLabel="${config.itemLabel || 'Item'}"
862
976
  .minItems="${config.minItems || 0}"
977
+ .maxItems="${config.maxItems || 0}"
863
978
  .onItemChange="${config.onItemChange}"
864
- @change="${(e) => this.handleNewFieldChange(fieldName, e.detail.value)}"
979
+ .isEmptyItemFn="${config.isEmptyItem}"
980
+ @change="${(e) => this.handleNewFieldChange(fieldName, e.target.value)}"
865
981
  ></temba-array-editor>
866
982
  ${errors.length
867
983
  ? html `<div class="field-errors">${errors.join(', ')}</div>`
@@ -885,6 +1001,28 @@ export class NodeEditor extends RapidElement {
885
1001
  ? html `<div class="field-errors">${errors.join(', ')}</div>`
886
1002
  : ''}
887
1003
  </div>`;
1004
+ }
1005
+ case 'message-editor': {
1006
+ const messageConfig = config;
1007
+ return html `<temba-message-editor
1008
+ name="${fieldName}"
1009
+ label="${config.label}"
1010
+ ?required="${config.required}"
1011
+ .errors="${errors}"
1012
+ .value="${value || ''}"
1013
+ .attachments="${this.formData.attachments || []}"
1014
+ placeholder="${messageConfig.placeholder || ''}"
1015
+ .helpText="${config.helpText || ''}"
1016
+ ?autogrow="${messageConfig.autogrow}"
1017
+ ?gsm="${messageConfig.gsm}"
1018
+ ?disableCompletion="${messageConfig.disableCompletion}"
1019
+ counter="${messageConfig.counter || ''}"
1020
+ accept="${messageConfig.accept || ''}"
1021
+ endpoint="${messageConfig.endpoint || ''}"
1022
+ max-attachments="${messageConfig.maxAttachments || 3}"
1023
+ minHeight="${messageConfig.minHeight || 60}"
1024
+ @change="${(e) => this.handleMessageEditorChange(fieldName, e)}"
1025
+ ></temba-message-editor>`;
888
1026
  }
889
1027
  default:
890
1028
  return html `<div>Unsupported field type: ${config.type}</div>`;
@@ -896,6 +1034,18 @@ export class NodeEditor extends RapidElement {
896
1034
  [groupLabel]: !this.groupCollapseState[groupLabel]
897
1035
  };
898
1036
  }
1037
+ handleGroupMouseEnter(groupLabel) {
1038
+ this.groupHoverState = {
1039
+ ...this.groupHoverState,
1040
+ [groupLabel]: true
1041
+ };
1042
+ }
1043
+ handleGroupMouseLeave(groupLabel) {
1044
+ this.groupHoverState = {
1045
+ ...this.groupHoverState,
1046
+ [groupLabel]: false
1047
+ };
1048
+ }
899
1049
  expandGroupsWithErrors(errors) {
900
1050
  if (!this.action)
901
1051
  return;
@@ -969,25 +1119,54 @@ export class NodeEditor extends RapidElement {
969
1119
  `;
970
1120
  }
971
1121
  renderGroup(groupConfig, config, renderedFields) {
972
- var _a;
973
- const { label, items, collapsible = false, collapsed = false, helpText } = groupConfig;
1122
+ var _a, _b;
1123
+ const { label, items, collapsible = false, collapsed = false, helpText, getGroupValueCount } = groupConfig;
974
1124
  // Initialize collapse state if not set
975
1125
  if (collapsible && !(label in this.groupCollapseState)) {
1126
+ // Evaluate collapsed property - can be boolean or function
1127
+ const initialCollapsed = typeof collapsed === 'function' ? collapsed(this.formData) : collapsed;
976
1128
  this.groupCollapseState = {
977
1129
  ...this.groupCollapseState,
978
- [label]: collapsed
1130
+ [label]: initialCollapsed
979
1131
  };
980
1132
  }
981
1133
  const isCollapsed = collapsible
982
- ? (_a = this.groupCollapseState[label]) !== null && _a !== void 0 ? _a : collapsed
1134
+ ? (_a = this.groupCollapseState[label]) !== null && _a !== void 0 ? _a : (typeof collapsed === 'function' ? collapsed(this.formData) : collapsed)
983
1135
  : false;
984
1136
  // Check if any field in this group has errors
985
1137
  const fieldsInGroup = this.collectFieldsFromItems(items);
986
1138
  const groupHasErrors = fieldsInGroup.some((fieldName) => this.errors[fieldName]);
1139
+ // Calculate count for bubble display
1140
+ let valueCount = 0;
1141
+ let showBubble = false;
1142
+ let showCheckmark = false;
1143
+ let hasValue = false;
1144
+ const isHovered = (_b = this.groupHoverState[label]) !== null && _b !== void 0 ? _b : false;
1145
+ if (getGroupValueCount && collapsible) {
1146
+ try {
1147
+ const result = getGroupValueCount(this.formData);
1148
+ if (typeof result === 'boolean') {
1149
+ // Boolean result - show checkmark when true
1150
+ showCheckmark = result && isCollapsed && !isHovered;
1151
+ hasValue = result;
1152
+ }
1153
+ else if (typeof result === 'number') {
1154
+ // Numeric result - show count bubble
1155
+ valueCount = result;
1156
+ showBubble = valueCount > 0 && isCollapsed && !isHovered;
1157
+ hasValue = valueCount > 0;
1158
+ }
1159
+ }
1160
+ catch (error) {
1161
+ console.error(`Error calculating group value count for ${label}:`, error);
1162
+ }
1163
+ }
987
1164
  return html `
988
1165
  <div
989
1166
  class="form-group ${collapsible ? 'collapsible' : ''} ${groupHasErrors
990
1167
  ? 'has-errors'
1168
+ : ''} ${isCollapsed ? 'collapsed' : 'expanded'} ${hasValue
1169
+ ? 'has-bubble'
991
1170
  : ''}"
992
1171
  >
993
1172
  <div
@@ -995,6 +1174,12 @@ export class NodeEditor extends RapidElement {
995
1174
  @click=${collapsible
996
1175
  ? () => this.handleGroupToggle(label)
997
1176
  : undefined}
1177
+ @mouseenter=${collapsible
1178
+ ? () => this.handleGroupMouseEnter(label)
1179
+ : undefined}
1180
+ @mouseleave=${collapsible
1181
+ ? () => this.handleGroupMouseLeave(label)
1182
+ : undefined}
998
1183
  >
999
1184
  <div class="form-group-info">
1000
1185
  <div class="form-group-title">${label}</div>
@@ -1010,13 +1195,28 @@ export class NodeEditor extends RapidElement {
1010
1195
  ></temba-icon>`
1011
1196
  : ''}
1012
1197
  ${collapsible && !groupHasErrors
1013
- ? html `<temba-icon
1014
- name="arrow_right"
1015
- size="1.5"
1016
- class="group-toggle-icon ${isCollapsed
1198
+ ? html `<div class="group-toggle-container">
1199
+ <temba-icon
1200
+ name="arrow_right"
1201
+ size="1.5"
1202
+ class="group-toggle-icon ${isCollapsed
1017
1203
  ? 'collapsed'
1018
- : 'expanded'}"
1019
- ></temba-icon>`
1204
+ : 'expanded'} ${showBubble || showCheckmark ? 'faded' : ''}"
1205
+ ></temba-icon>
1206
+ ${showCheckmark
1207
+ ? html `<temba-icon
1208
+ name="check"
1209
+ size="1"
1210
+ class="group-checkmark-icon"
1211
+ ></temba-icon>`
1212
+ : showBubble
1213
+ ? html `<div
1214
+ class="group-count-bubble ${!showBubble ? 'hidden' : ''}"
1215
+ >
1216
+ ${valueCount}
1217
+ </div>`
1218
+ : ''}
1219
+ </div>`
1020
1220
  : ''}
1021
1221
  </div>
1022
1222
  <div
@@ -1064,6 +1264,26 @@ export class NodeEditor extends RapidElement {
1064
1264
  delete newErrors[fieldName];
1065
1265
  this.errors = newErrors;
1066
1266
  }
1267
+ // Re-evaluate group collapse states that depend on form data
1268
+ this.updateGroupCollapseStates();
1269
+ // Trigger re-render
1270
+ this.requestUpdate();
1271
+ }
1272
+ handleMessageEditorChange(fieldName, event) {
1273
+ const target = event.target;
1274
+ // Update both text and attachments from the message editor
1275
+ this.formData = {
1276
+ ...this.formData,
1277
+ [fieldName]: target.value,
1278
+ attachments: target.attachments || []
1279
+ };
1280
+ // Clear any existing errors for both fields
1281
+ if (this.errors[fieldName]) {
1282
+ const newErrors = { ...this.errors };
1283
+ delete newErrors[fieldName];
1284
+ delete newErrors.attachments;
1285
+ this.errors = newErrors;
1286
+ }
1067
1287
  // Trigger re-render
1068
1288
  this.requestUpdate();
1069
1289
  }
@@ -1208,4 +1428,7 @@ __decorate([
1208
1428
  __decorate([
1209
1429
  state()
1210
1430
  ], NodeEditor.prototype, "groupCollapseState", void 0);
1431
+ __decorate([
1432
+ state()
1433
+ ], NodeEditor.prototype, "groupHoverState", void 0);
1211
1434
  //# sourceMappingURL=NodeEditor.js.map