mirror of
				https://github.com/lingble/chatwoot.git
				synced 2025-10-31 02:57:57 +00:00 
			
		
		
		
	feat: Add event subscription option to webhooks (#4540)
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
This commit is contained in:
		| @@ -23,7 +23,7 @@ class Api::V1::Accounts::WebhooksController < Api::V1::Accounts::BaseController | |||||||
|   private |   private | ||||||
|  |  | ||||||
|   def webhook_params |   def webhook_params | ||||||
|     params.require(:webhook).permit(:inbox_id, :url) |     params.require(:webhook).permit(:inbox_id, :url, subscriptions: []) | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   def fetch_webhook |   def fetch_webhook | ||||||
|   | |||||||
| @@ -2,6 +2,10 @@ | |||||||
|   margin-right: var(--space-small); |   margin-right: var(--space-small); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | .margin-bottom-small { | ||||||
|  |   margin-bottom: var(--space-small); | ||||||
|  | } | ||||||
|  |  | ||||||
| .margin-right-smaller { | .margin-right-smaller { | ||||||
|   margin-right: var(--space-smaller); |   margin-right: var(--space-smaller); | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										50
									
								
								app/javascript/dashboard/components/widgets/ShowMore.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								app/javascript/dashboard/components/widgets/ShowMore.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,50 @@ | |||||||
|  | <template> | ||||||
|  |   <span> | ||||||
|  |     {{ textToBeDisplayed }} | ||||||
|  |     <button class="show-more--button" @click="toggleShowMore"> | ||||||
|  |       {{ buttonLabel }} | ||||||
|  |     </button> | ||||||
|  |   </span> | ||||||
|  | </template> | ||||||
|  | <script> | ||||||
|  | export default { | ||||||
|  |   props: { | ||||||
|  |     text: { | ||||||
|  |       type: String, | ||||||
|  |       default: '', | ||||||
|  |     }, | ||||||
|  |     limit: { | ||||||
|  |       type: Number, | ||||||
|  |       default: 120, | ||||||
|  |     }, | ||||||
|  |   }, | ||||||
|  |   data() { | ||||||
|  |     return { | ||||||
|  |       showMore: false, | ||||||
|  |     }; | ||||||
|  |   }, | ||||||
|  |   computed: { | ||||||
|  |     textToBeDisplayed() { | ||||||
|  |       if (this.showMore) { | ||||||
|  |         return this.text; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       return this.text.slice(0, this.limit) + '...'; | ||||||
|  |     }, | ||||||
|  |     buttonLabel() { | ||||||
|  |       const i18nKey = !this.showMore ? 'SHOW_MORE' : 'SHOW_LESS'; | ||||||
|  |       return this.$t(`COMPONENTS.SHOW_MORE_BLOCK.${i18nKey}`); | ||||||
|  |     }, | ||||||
|  |   }, | ||||||
|  |   methods: { | ||||||
|  |     toggleShowMore() { | ||||||
|  |       this.showMore = !this.showMore; | ||||||
|  |     }, | ||||||
|  |   }, | ||||||
|  | }; | ||||||
|  | </script> | ||||||
|  | <style scoped> | ||||||
|  | .show-more--button { | ||||||
|  |   color: var(--w-500); | ||||||
|  | } | ||||||
|  | </style> | ||||||
| @@ -2,6 +2,29 @@ | |||||||
|   "INTEGRATION_SETTINGS": { |   "INTEGRATION_SETTINGS": { | ||||||
|     "HEADER": "Integrations", |     "HEADER": "Integrations", | ||||||
|     "WEBHOOK": { |     "WEBHOOK": { | ||||||
|  |       "SUBSCRIBED_EVENTS": "Subscribed Events", | ||||||
|  |       "FORM": { | ||||||
|  |         "CANCEL": "Cancel", | ||||||
|  |         "DESC": "Webhook events provide you the realtime information about what's happening in your Chatwoot account. Please enter a valid URL to configure a callback.", | ||||||
|  |         "SUBSCRIPTIONS": { | ||||||
|  |           "LABEL": "Events", | ||||||
|  |           "EVENTS": { | ||||||
|  |             "CONVERSATION_CREATED": "Conversation Created", | ||||||
|  |             "CONVERSATION_STATUS_CHANGED": "Conversation Status Changed", | ||||||
|  |             "CONVERSATION_UPDATED": "Conversation Updated", | ||||||
|  |             "MESSAGE_CREATED": "Message created", | ||||||
|  |             "MESSAGE_UPDATED": "Message updated", | ||||||
|  |             "WEBWIDGET_TRIGGERED": "Live chat widget opened by the user" | ||||||
|  |           } | ||||||
|  |         }, | ||||||
|  |         "END_POINT": { | ||||||
|  |           "LABEL": "Webhook URL", | ||||||
|  |           "PLACEHOLDER": "Example: https://example/api/webhook", | ||||||
|  |           "ERROR": "Please enter a valid URL" | ||||||
|  |         }, | ||||||
|  |         "EDIT_SUBMIT": "Update webhook", | ||||||
|  |         "ADD_SUBMIT": "Create webhook" | ||||||
|  |       }, | ||||||
|       "TITLE": "Webhook", |       "TITLE": "Webhook", | ||||||
|       "CONFIGURE": "Configure", |       "CONFIGURE": "Configure", | ||||||
|       "HEADER": "Webhook settings", |       "HEADER": "Webhook settings", | ||||||
| @@ -17,35 +40,16 @@ | |||||||
|       "EDIT": { |       "EDIT": { | ||||||
|         "BUTTON_TEXT": "Edit", |         "BUTTON_TEXT": "Edit", | ||||||
|         "TITLE": "Edit webhook", |         "TITLE": "Edit webhook", | ||||||
|         "CANCEL": "Cancel", |  | ||||||
|         "DESC": "Webhook events provide you the realtime information about what's happening in your Chatwoot account. Please enter a valid URL to configure a callback.", |  | ||||||
|         "FORM": { |  | ||||||
|           "END_POINT": { |  | ||||||
|             "LABEL": "Webhook URL", |  | ||||||
|             "PLACEHOLDER": "Example: https://example/api/webhook", |  | ||||||
|             "ERROR": "Please enter a valid URL" |  | ||||||
|           }, |  | ||||||
|           "SUBMIT": "Edit webhook" |  | ||||||
|         }, |  | ||||||
|         "API": { |         "API": { | ||||||
|           "SUCCESS_MESSAGE": "Webhook URL updated successfully", |           "SUCCESS_MESSAGE": "Webhook configuration updated successfully", | ||||||
|           "ERROR_MESSAGE": "Could not connect to Woot Server, Please try again later" |           "ERROR_MESSAGE": "Could not connect to Woot Server, Please try again later" | ||||||
|         } |         } | ||||||
|       }, |       }, | ||||||
|       "ADD": { |       "ADD": { | ||||||
|         "CANCEL": "Cancel", |         "CANCEL": "Cancel", | ||||||
|         "TITLE": "Add new webhook", |         "TITLE": "Add new webhook", | ||||||
|         "DESC": "Webhook events provide you the realtime information about what's happening in your Chatwoot account. Please enter a valid URL to configure a callback.", |  | ||||||
|         "FORM": { |  | ||||||
|           "END_POINT": { |  | ||||||
|             "LABEL": "Webhook URL", |  | ||||||
|             "PLACEHOLDER": "Example: https://example/api/webhook", |  | ||||||
|             "ERROR": "Please enter a valid URL" |  | ||||||
|           }, |  | ||||||
|           "SUBMIT": "Create webhook" |  | ||||||
|         }, |  | ||||||
|         "API": { |         "API": { | ||||||
|           "SUCCESS_MESSAGE": "Webhook added successfully", |           "SUCCESS_MESSAGE": "Webhook configuration added successfully", | ||||||
|           "ERROR_MESSAGE": "Could not connect to Woot Server, Please try again later" |           "ERROR_MESSAGE": "Could not connect to Woot Server, Please try again later" | ||||||
|         } |         } | ||||||
|       }, |       }, | ||||||
| @@ -57,7 +61,7 @@ | |||||||
|         }, |         }, | ||||||
|         "CONFIRM": { |         "CONFIRM": { | ||||||
|           "TITLE": "Confirm Deletion", |           "TITLE": "Confirm Deletion", | ||||||
|           "MESSAGE": "Are you sure to delete ", |           "MESSAGE": "Are you sure to delete the webhook? (%{webhookURL})", | ||||||
|           "YES": "Yes, Delete ", |           "YES": "Yes, Delete ", | ||||||
|           "NO": "No, Keep it" |           "NO": "No, Keep it" | ||||||
|         } |         } | ||||||
|   | |||||||
| @@ -127,6 +127,10 @@ | |||||||
|       "BUTTON_TEXT": "Copy", |       "BUTTON_TEXT": "Copy", | ||||||
|       "COPY_SUCCESSFUL": "Code copied to clipboard successfully" |       "COPY_SUCCESSFUL": "Code copied to clipboard successfully" | ||||||
|     }, |     }, | ||||||
|  |     "SHOW_MORE_BLOCK": { | ||||||
|  |       "SHOW_MORE": "Show More", | ||||||
|  |       "SHOW_LESS": "Show Less" | ||||||
|  |     }, | ||||||
|     "FILE_BUBBLE": { |     "FILE_BUBBLE": { | ||||||
|       "DOWNLOAD": "Download", |       "DOWNLOAD": "Download", | ||||||
|       "UPLOADING": "Uploading..." |       "UPLOADING": "Uploading..." | ||||||
|   | |||||||
| @@ -1,108 +0,0 @@ | |||||||
| <template> |  | ||||||
|   <div class="column content-box"> |  | ||||||
|     <woot-modal-header |  | ||||||
|       :header-title="$t('INTEGRATION_SETTINGS.WEBHOOK.EDIT.TITLE')" |  | ||||||
|     /> |  | ||||||
|     <form class="row" @submit.prevent="editWebhook"> |  | ||||||
|       <div class="medium-12 columns"> |  | ||||||
|         <label :class="{ error: $v.endPoint.$error }"> |  | ||||||
|           {{ $t('INTEGRATION_SETTINGS.WEBHOOK.EDIT.FORM.END_POINT.LABEL') }} |  | ||||||
|           <input |  | ||||||
|             v-model.trim="endPoint" |  | ||||||
|             type="text" |  | ||||||
|             name="endPoint" |  | ||||||
|             :placeholder=" |  | ||||||
|               $t('INTEGRATION_SETTINGS.WEBHOOK.EDIT.FORM.END_POINT.PLACEHOLDER') |  | ||||||
|             " |  | ||||||
|             @input="$v.endPoint.$touch" |  | ||||||
|           /> |  | ||||||
|           <span v-if="$v.endPoint.$error" class="message"> |  | ||||||
|             {{ $t('INTEGRATION_SETTINGS.WEBHOOK.EDIT.FORM.END_POINT.ERROR') }} |  | ||||||
|           </span> |  | ||||||
|         </label> |  | ||||||
|       </div> |  | ||||||
|  |  | ||||||
|       <div class="modal-footer"> |  | ||||||
|         <div class="medium-12 columns"> |  | ||||||
|           <woot-button |  | ||||||
|             :is-disabled=" |  | ||||||
|               $v.endPoint.$invalid || uiFlags.updatingItem || endPoint === url |  | ||||||
|             " |  | ||||||
|             :is-loading="uiFlags.updatingItem" |  | ||||||
|           > |  | ||||||
|             {{ $t('INTEGRATION_SETTINGS.WEBHOOK.EDIT.FORM.SUBMIT') }} |  | ||||||
|           </woot-button> |  | ||||||
|           <woot-button class="button clear" @click.prevent="onClose"> |  | ||||||
|             {{ $t('INTEGRATION_SETTINGS.WEBHOOK.ADD.CANCEL') }} |  | ||||||
|           </woot-button> |  | ||||||
|         </div> |  | ||||||
|       </div> |  | ||||||
|     </form> |  | ||||||
|   </div> |  | ||||||
| </template> |  | ||||||
|  |  | ||||||
| <script> |  | ||||||
| import { required, url, minLength } from 'vuelidate/lib/validators'; |  | ||||||
| import alertMixin from 'shared/mixins/alertMixin'; |  | ||||||
| import { mapGetters } from 'vuex'; |  | ||||||
|  |  | ||||||
| export default { |  | ||||||
|   mixins: [alertMixin], |  | ||||||
|   props: { |  | ||||||
|     id: { |  | ||||||
|       type: Number, |  | ||||||
|       required: true, |  | ||||||
|     }, |  | ||||||
|     url: { |  | ||||||
|       type: String, |  | ||||||
|       required: true, |  | ||||||
|     }, |  | ||||||
|     onClose: { |  | ||||||
|       type: Function, |  | ||||||
|       required: true, |  | ||||||
|     }, |  | ||||||
|   }, |  | ||||||
|   data() { |  | ||||||
|     return { |  | ||||||
|       alertMessage: '', |  | ||||||
|       endPoint: this.url, |  | ||||||
|       webhookId: this.id, |  | ||||||
|     }; |  | ||||||
|   }, |  | ||||||
|   validations: { |  | ||||||
|     endPoint: { |  | ||||||
|       required, |  | ||||||
|       minLength: minLength(7), |  | ||||||
|       url, |  | ||||||
|     }, |  | ||||||
|   }, |  | ||||||
|   computed: { |  | ||||||
|     ...mapGetters({ uiFlags: 'webhooks/getUIFlags' }), |  | ||||||
|   }, |  | ||||||
|   methods: { |  | ||||||
|     resetForm() { |  | ||||||
|       this.endPoint = ''; |  | ||||||
|       this.$v.endPoint.$reset(); |  | ||||||
|     }, |  | ||||||
|     async editWebhook() { |  | ||||||
|       try { |  | ||||||
|         await this.$store.dispatch('webhooks/update', { |  | ||||||
|           webhook: { url: this.endPoint }, |  | ||||||
|           id: this.webhookId, |  | ||||||
|         }); |  | ||||||
|         this.alertMessage = this.$t( |  | ||||||
|           'INTEGRATION_SETTINGS.WEBHOOK.EDIT.API.SUCCESS_MESSAGE' |  | ||||||
|         ); |  | ||||||
|         this.resetForm(); |  | ||||||
|         this.onClose(); |  | ||||||
|       } catch (error) { |  | ||||||
|         this.alertMessage = |  | ||||||
|           error.response.data.message || |  | ||||||
|           this.$t('INTEGRATION_SETTINGS.WEBHOOK.EDIT.API.ERROR_MESSAGE'); |  | ||||||
|       } finally { |  | ||||||
|         this.showAlert(this.alertMessage); |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|   }, |  | ||||||
| }; |  | ||||||
| </script> |  | ||||||
| @@ -1,121 +0,0 @@ | |||||||
| <template> |  | ||||||
|   <modal :show.sync="show" :on-close="onClose" :close-on-backdrop-click="false"> |  | ||||||
|     <div class="column content-box"> |  | ||||||
|       <woot-modal-header |  | ||||||
|         :header-title="$t('INTEGRATION_SETTINGS.WEBHOOK.ADD.TITLE')" |  | ||||||
|         :header-content=" |  | ||||||
|           useInstallationName( |  | ||||||
|             $t('INTEGRATION_SETTINGS.WEBHOOK.ADD.DESC'), |  | ||||||
|             globalConfig.installationName |  | ||||||
|           ) |  | ||||||
|         " |  | ||||||
|       /> |  | ||||||
|       <form class="row" @submit.prevent="addWebhook"> |  | ||||||
|         <div class="medium-12 columns"> |  | ||||||
|           <label :class="{ error: $v.endPoint.$error }"> |  | ||||||
|             {{ $t('INTEGRATION_SETTINGS.WEBHOOK.ADD.FORM.END_POINT.LABEL') }} |  | ||||||
|             <input |  | ||||||
|               v-model.trim="endPoint" |  | ||||||
|               type="text" |  | ||||||
|               name="endPoint" |  | ||||||
|               :placeholder=" |  | ||||||
|                 $t( |  | ||||||
|                   'INTEGRATION_SETTINGS.WEBHOOK.ADD.FORM.END_POINT.PLACEHOLDER' |  | ||||||
|                 ) |  | ||||||
|               " |  | ||||||
|               @input="$v.endPoint.$touch" |  | ||||||
|             /> |  | ||||||
|             <span v-if="$v.endPoint.$error" class="message"> |  | ||||||
|               {{ $t('INTEGRATION_SETTINGS.WEBHOOK.ADD.FORM.END_POINT.ERROR') }} |  | ||||||
|             </span> |  | ||||||
|           </label> |  | ||||||
|         </div> |  | ||||||
|  |  | ||||||
|         <div class="modal-footer"> |  | ||||||
|           <div class="medium-12 columns"> |  | ||||||
|             <woot-button |  | ||||||
|               :disabled="$v.endPoint.$invalid || addWebHook.showLoading" |  | ||||||
|               :is-loading="addWebHook.showLoading" |  | ||||||
|             > |  | ||||||
|               {{ $t('INTEGRATION_SETTINGS.WEBHOOK.ADD.FORM.SUBMIT') }} |  | ||||||
|             </woot-button> |  | ||||||
|             <woot-button class="button clear" @click.prevent="onClose"> |  | ||||||
|               {{ $t('INTEGRATION_SETTINGS.WEBHOOK.ADD.CANCEL') }} |  | ||||||
|             </woot-button> |  | ||||||
|           </div> |  | ||||||
|         </div> |  | ||||||
|       </form> |  | ||||||
|     </div> |  | ||||||
|   </modal> |  | ||||||
| </template> |  | ||||||
|  |  | ||||||
| <script> |  | ||||||
| import { required, url, minLength } from 'vuelidate/lib/validators'; |  | ||||||
| import alertMixin from 'shared/mixins/alertMixin'; |  | ||||||
| import Modal from '../../../../components/Modal'; |  | ||||||
| import globalConfigMixin from 'shared/mixins/globalConfigMixin'; |  | ||||||
| import { mapGetters } from 'vuex'; |  | ||||||
|  |  | ||||||
| export default { |  | ||||||
|   components: { |  | ||||||
|     Modal, |  | ||||||
|   }, |  | ||||||
|   mixins: [alertMixin, globalConfigMixin], |  | ||||||
|   props: { |  | ||||||
|     onClose: { |  | ||||||
|       type: Function, |  | ||||||
|       required: true, |  | ||||||
|     }, |  | ||||||
|   }, |  | ||||||
|   data() { |  | ||||||
|     return { |  | ||||||
|       endPoint: '', |  | ||||||
|       addWebHook: { |  | ||||||
|         showAlert: false, |  | ||||||
|         showLoading: false, |  | ||||||
|       }, |  | ||||||
|       show: true, |  | ||||||
|     }; |  | ||||||
|   }, |  | ||||||
|   computed: { |  | ||||||
|     ...mapGetters({ globalConfig: 'globalConfig/get' }), |  | ||||||
|   }, |  | ||||||
|   validations: { |  | ||||||
|     endPoint: { |  | ||||||
|       required, |  | ||||||
|       minLength: minLength(7), |  | ||||||
|       url, |  | ||||||
|     }, |  | ||||||
|   }, |  | ||||||
|   methods: { |  | ||||||
|     resetForm() { |  | ||||||
|       this.endPoint = ''; |  | ||||||
|       this.$v.endPoint.$reset(); |  | ||||||
|     }, |  | ||||||
|     async addWebhook() { |  | ||||||
|       this.addWebHook.showLoading = true; |  | ||||||
|  |  | ||||||
|       try { |  | ||||||
|         await this.$store.dispatch('webhooks/create', { |  | ||||||
|           webhook: { url: this.endPoint }, |  | ||||||
|         }); |  | ||||||
|         this.addWebHook.showLoading = false; |  | ||||||
|  |  | ||||||
|         this.addWebHook.message = this.$t( |  | ||||||
|           'INTEGRATION_SETTINGS.WEBHOOK.ADD.API.SUCCESS_MESSAGE' |  | ||||||
|         ); |  | ||||||
|         this.resetForm(); |  | ||||||
|         this.onClose(); |  | ||||||
|       } catch (error) { |  | ||||||
|         this.addWebHook.showLoading = false; |  | ||||||
|         this.addWebHook.message = |  | ||||||
|           error.response.data.message || |  | ||||||
|           this.$t('INTEGRATION_SETTINGS.WEBHOOK.EDIT.API.ERROR_MESSAGE'); |  | ||||||
|       } finally { |  | ||||||
|         this.addWebHook.showLoading = false; |  | ||||||
|         this.showAlert(this.addWebHook.message); |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|   }, |  | ||||||
| }; |  | ||||||
| </script> |  | ||||||
| @@ -0,0 +1,61 @@ | |||||||
|  | <template> | ||||||
|  |   <div class="column content-box"> | ||||||
|  |     <woot-modal-header | ||||||
|  |       :header-title="$t('INTEGRATION_SETTINGS.WEBHOOK.EDIT.TITLE')" | ||||||
|  |     /> | ||||||
|  |     <webhook-form | ||||||
|  |       :value="value" | ||||||
|  |       :is-submitting="uiFlags.updatingItem" | ||||||
|  |       :submit-label="$t('INTEGRATION_SETTINGS.WEBHOOK.FORM.EDIT_SUBMIT')" | ||||||
|  |       @submit="onSubmit" | ||||||
|  |       @cancel="onClose" | ||||||
|  |     /> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script> | ||||||
|  | import alertMixin from 'shared/mixins/alertMixin'; | ||||||
|  | import { mapGetters } from 'vuex'; | ||||||
|  | import WebhookForm from './WebhookForm.vue'; | ||||||
|  |  | ||||||
|  | export default { | ||||||
|  |   components: { WebhookForm }, | ||||||
|  |   mixins: [alertMixin], | ||||||
|  |   props: { | ||||||
|  |     value: { | ||||||
|  |       type: Object, | ||||||
|  |       required: true, | ||||||
|  |     }, | ||||||
|  |     id: { | ||||||
|  |       type: [Number, String], | ||||||
|  |       required: true, | ||||||
|  |     }, | ||||||
|  |     onClose: { | ||||||
|  |       type: Function, | ||||||
|  |       required: true, | ||||||
|  |     }, | ||||||
|  |   }, | ||||||
|  |   computed: { | ||||||
|  |     ...mapGetters({ uiFlags: 'webhooks/getUIFlags' }), | ||||||
|  |   }, | ||||||
|  |   methods: { | ||||||
|  |     async onSubmit(webhook) { | ||||||
|  |       try { | ||||||
|  |         await this.$store.dispatch('webhooks/update', { | ||||||
|  |           webhook, | ||||||
|  |           id: this.id, | ||||||
|  |         }); | ||||||
|  |         this.showAlert( | ||||||
|  |           this.$t('INTEGRATION_SETTINGS.WEBHOOK.EDIT.API.SUCCESS_MESSAGE') | ||||||
|  |         ); | ||||||
|  |         this.onClose(); | ||||||
|  |       } catch (error) { | ||||||
|  |         const alertMessage = | ||||||
|  |           error.response.data.message || | ||||||
|  |           this.$t('INTEGRATION_SETTINGS.WEBHOOK.EDIT.API.ERROR_MESSAGE'); | ||||||
|  |         this.showAlert(alertMessage); | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |   }, | ||||||
|  | }; | ||||||
|  | </script> | ||||||
| @@ -4,7 +4,7 @@ | |||||||
|       color-scheme="success" |       color-scheme="success" | ||||||
|       class-names="button--fixed-right-top" |       class-names="button--fixed-right-top" | ||||||
|       icon="add-circle" |       icon="add-circle" | ||||||
|       @click="openAddPopup()" |       @click="openAddPopup" | ||||||
|     > |     > | ||||||
|       {{ $t('INTEGRATION_SETTINGS.WEBHOOK.HEADER_BTN_TXT') }} |       {{ $t('INTEGRATION_SETTINGS.WEBHOOK.HEADER_BTN_TXT') }} | ||||||
|     </woot-button> |     </woot-button> | ||||||
| @@ -37,35 +37,14 @@ | |||||||
|             </th> |             </th> | ||||||
|           </thead> |           </thead> | ||||||
|           <tbody> |           <tbody> | ||||||
|             <tr v-for="(webHookItem, index) in records" :key="webHookItem.id"> |             <webhook-row | ||||||
|               <td class="webhook-link"> |               v-for="(webHookItem, index) in records" | ||||||
|                 {{ webHookItem.url }} |               :key="webHookItem.id" | ||||||
|               </td> |               :index="index" | ||||||
|               <td class="button-wrapper"> |               :webhook="webHookItem" | ||||||
|                 <woot-button |               @edit="openEditPopup" | ||||||
|                   v-tooltip.top=" |               @delete="openDeletePopup" | ||||||
|                     $t('INTEGRATION_SETTINGS.WEBHOOK.EDIT.BUTTON_TEXT') |             /> | ||||||
|                   " |  | ||||||
|                   variant="smooth" |  | ||||||
|                   size="tiny" |  | ||||||
|                   color-scheme="secondary" |  | ||||||
|                   icon="edit" |  | ||||||
|                   @click="openEditPopup(webHookItem)" |  | ||||||
|                 > |  | ||||||
|                 </woot-button> |  | ||||||
|                 <woot-button |  | ||||||
|                   v-tooltip.top=" |  | ||||||
|                     $t('INTEGRATION_SETTINGS.WEBHOOK.DELETE.BUTTON_TEXT') |  | ||||||
|                   " |  | ||||||
|                   variant="smooth" |  | ||||||
|                   color-scheme="alert" |  | ||||||
|                   size="tiny" |  | ||||||
|                   icon="dismiss-circle" |  | ||||||
|                   @click="openDeletePopup(webHookItem, index)" |  | ||||||
|                 > |  | ||||||
|                 </woot-button> |  | ||||||
|               </td> |  | ||||||
|             </tr> |  | ||||||
|           </tbody> |           </tbody> | ||||||
|         </table> |         </table> | ||||||
|       </div> |       </div> | ||||||
| @@ -83,24 +62,27 @@ | |||||||
|     </div> |     </div> | ||||||
| 
 | 
 | ||||||
|     <woot-modal :show.sync="showAddPopup" :on-close="hideAddPopup"> |     <woot-modal :show.sync="showAddPopup" :on-close="hideAddPopup"> | ||||||
|       <new-webhook :on-close="hideAddPopup" /> |       <new-webhook v-if="showAddPopup" :on-close="hideAddPopup" /> | ||||||
|     </woot-modal> |     </woot-modal> | ||||||
| 
 | 
 | ||||||
|     <woot-modal :show.sync="showEditPopup" :on-close="hideEditPopup"> |     <woot-modal :show.sync="showEditPopup" :on-close="hideEditPopup"> | ||||||
|       <edit-webhook |       <edit-webhook | ||||||
|         v-if="showEditPopup" |         v-if="showEditPopup" | ||||||
|         :id="selectedWebHook.id" |         :id="selectedWebHook.id" | ||||||
|         :url="selectedWebHook.url" |         :value="selectedWebHook" | ||||||
|         :on-close="hideEditPopup" |         :on-close="hideEditPopup" | ||||||
|       /> |       /> | ||||||
|     </woot-modal> |     </woot-modal> | ||||||
| 
 |  | ||||||
|     <woot-delete-modal |     <woot-delete-modal | ||||||
|       :show.sync="showDeleteConfirmationPopup" |       :show.sync="showDeleteConfirmationPopup" | ||||||
|       :on-close="closeDeletePopup" |       :on-close="closeDeletePopup" | ||||||
|       :on-confirm="confirmDeletion" |       :on-confirm="confirmDeletion" | ||||||
|       :title="$t('INTEGRATION_SETTINGS.WEBHOOK.DELETE.CONFIRM.TITLE')" |       :title="$t('INTEGRATION_SETTINGS.WEBHOOK.DELETE.CONFIRM.TITLE')" | ||||||
|       :message="$t('INTEGRATION_SETTINGS.WEBHOOK.DELETE.CONFIRM.MESSAGE')" |       :message=" | ||||||
|  |         $t('INTEGRATION_SETTINGS.WEBHOOK.DELETE.CONFIRM.MESSAGE', { | ||||||
|  |           webhookURL: selectedWebHook.url, | ||||||
|  |         }) | ||||||
|  |       " | ||||||
|       :confirm-text="$t('INTEGRATION_SETTINGS.WEBHOOK.DELETE.CONFIRM.YES')" |       :confirm-text="$t('INTEGRATION_SETTINGS.WEBHOOK.DELETE.CONFIRM.YES')" | ||||||
|       :reject-text="$t('INTEGRATION_SETTINGS.WEBHOOK.DELETE.CONFIRM.NO')" |       :reject-text="$t('INTEGRATION_SETTINGS.WEBHOOK.DELETE.CONFIRM.NO')" | ||||||
|     /> |     /> | ||||||
| @@ -112,11 +94,13 @@ import NewWebhook from './NewWebHook'; | |||||||
| import EditWebhook from './EditWebHook'; | import EditWebhook from './EditWebHook'; | ||||||
| import alertMixin from 'shared/mixins/alertMixin'; | import alertMixin from 'shared/mixins/alertMixin'; | ||||||
| import globalConfigMixin from 'shared/mixins/globalConfigMixin'; | import globalConfigMixin from 'shared/mixins/globalConfigMixin'; | ||||||
|  | import WebhookRow from './WebhookRow'; | ||||||
| 
 | 
 | ||||||
| export default { | export default { | ||||||
|   components: { |   components: { | ||||||
|     NewWebhook, |     NewWebhook, | ||||||
|     EditWebhook, |     EditWebhook, | ||||||
|  |     WebhookRow, | ||||||
|   }, |   }, | ||||||
|   mixins: [alertMixin, globalConfigMixin], |   mixins: [alertMixin, globalConfigMixin], | ||||||
|   data() { |   data() { | ||||||
| @@ -179,11 +163,3 @@ export default { | |||||||
|   }, |   }, | ||||||
| }; | }; | ||||||
| </script> | </script> | ||||||
| <style scoped lang="scss"> |  | ||||||
| .webhook-link { |  | ||||||
|   word-break: break-word; |  | ||||||
| } |  | ||||||
| .button-wrapper button:nth-child(2) { |  | ||||||
|   margin-left: var(--space-normal); |  | ||||||
| } |  | ||||||
| </style> |  | ||||||
| @@ -0,0 +1,59 @@ | |||||||
|  | <template> | ||||||
|  |   <div class="column content-box"> | ||||||
|  |     <woot-modal-header | ||||||
|  |       :header-title="$t('INTEGRATION_SETTINGS.WEBHOOK.ADD.TITLE')" | ||||||
|  |       :header-content=" | ||||||
|  |         useInstallationName( | ||||||
|  |           $t('INTEGRATION_SETTINGS.WEBHOOK.FORM.DESC'), | ||||||
|  |           globalConfig.installationName | ||||||
|  |         ) | ||||||
|  |       " | ||||||
|  |     /> | ||||||
|  |     <webhook-form | ||||||
|  |       :is-submitting="uiFlags.creatingItem" | ||||||
|  |       :submit-label="$t('INTEGRATION_SETTINGS.WEBHOOK.FORM.ADD_SUBMIT')" | ||||||
|  |       @submit="onSubmit" | ||||||
|  |       @cancel="onClose" | ||||||
|  |     /> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script> | ||||||
|  | import alertMixin from 'shared/mixins/alertMixin'; | ||||||
|  | import globalConfigMixin from 'shared/mixins/globalConfigMixin'; | ||||||
|  | import { mapGetters } from 'vuex'; | ||||||
|  | import WebhookForm from './WebhookForm.vue'; | ||||||
|  |  | ||||||
|  | export default { | ||||||
|  |   components: { WebhookForm }, | ||||||
|  |   mixins: [alertMixin, globalConfigMixin], | ||||||
|  |   props: { | ||||||
|  |     onClose: { | ||||||
|  |       type: Function, | ||||||
|  |       required: true, | ||||||
|  |     }, | ||||||
|  |   }, | ||||||
|  |   computed: { | ||||||
|  |     ...mapGetters({ | ||||||
|  |       globalConfig: 'globalConfig/get', | ||||||
|  |       uiFlags: 'webhooks/getUIFlags', | ||||||
|  |     }), | ||||||
|  |   }, | ||||||
|  |   methods: { | ||||||
|  |     async onSubmit(webhook) { | ||||||
|  |       try { | ||||||
|  |         await this.$store.dispatch('webhooks/create', { webhook }); | ||||||
|  |         this.showAlert( | ||||||
|  |           this.$t('INTEGRATION_SETTINGS.WEBHOOK.ADD.API.SUCCESS_MESSAGE') | ||||||
|  |         ); | ||||||
|  |         this.onClose(); | ||||||
|  |       } catch (error) { | ||||||
|  |         const message = | ||||||
|  |           error.response.data.message || | ||||||
|  |           this.$t('INTEGRATION_SETTINGS.WEBHOOK.EDIT.API.ERROR_MESSAGE'); | ||||||
|  |         this.showAlert(message); | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |   }, | ||||||
|  | }; | ||||||
|  | </script> | ||||||
| @@ -0,0 +1,108 @@ | |||||||
|  | <template> | ||||||
|  |   <form class="row" @submit.prevent="onSubmit"> | ||||||
|  |     <div class="medium-12 columns"> | ||||||
|  |       <label :class="{ error: $v.url.$error }"> | ||||||
|  |         {{ $t('INTEGRATION_SETTINGS.WEBHOOK.FORM.END_POINT.LABEL') }} | ||||||
|  |         <input | ||||||
|  |           v-model.trim="url" | ||||||
|  |           type="text" | ||||||
|  |           name="url" | ||||||
|  |           :placeholder=" | ||||||
|  |             $t('INTEGRATION_SETTINGS.WEBHOOK.FORM.END_POINT.PLACEHOLDER') | ||||||
|  |           " | ||||||
|  |           @input="$v.url.$touch" | ||||||
|  |         /> | ||||||
|  |         <span v-if="$v.url.$error" class="message"> | ||||||
|  |           {{ $t('INTEGRATION_SETTINGS.WEBHOOK.FORM.END_POINT.ERROR') }} | ||||||
|  |         </span> | ||||||
|  |       </label> | ||||||
|  |       <label :class="{ error: $v.url.$error }" class="margin-bottom-small"> | ||||||
|  |         {{ $t('INTEGRATION_SETTINGS.WEBHOOK.FORM.SUBSCRIPTIONS.LABEL') }} | ||||||
|  |       </label> | ||||||
|  |       <div v-for="event in supportedWebhookEvents" :key="event"> | ||||||
|  |         <input | ||||||
|  |           :id="event" | ||||||
|  |           v-model="subscriptions" | ||||||
|  |           type="checkbox" | ||||||
|  |           :value="event" | ||||||
|  |           name="subscriptions" | ||||||
|  |           class="margin-right-small" | ||||||
|  |         /> | ||||||
|  |         <span class="fs-small"> | ||||||
|  |           {{ `${getEventLabel(event)} (${event})` }} | ||||||
|  |         </span> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |  | ||||||
|  |     <div class="modal-footer"> | ||||||
|  |       <div class="medium-12 columns"> | ||||||
|  |         <woot-button | ||||||
|  |           :disabled="$v.$invalid || isSubmitting" | ||||||
|  |           :is-loading="isSubmitting" | ||||||
|  |         > | ||||||
|  |           {{ submitLabel }} | ||||||
|  |         </woot-button> | ||||||
|  |         <woot-button class="button clear" @click.prevent="$emit('cancel')"> | ||||||
|  |           {{ $t('INTEGRATION_SETTINGS.WEBHOOK.FORM.CANCEL') }} | ||||||
|  |         </woot-button> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |   </form> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script> | ||||||
|  | import { required, url, minLength } from 'vuelidate/lib/validators'; | ||||||
|  | import webhookMixin from './webhookMixin'; | ||||||
|  |  | ||||||
|  | const SUPPORTED_WEBHOOK_EVENTS = [ | ||||||
|  |   'conversation_created', | ||||||
|  |   'conversation_status_changed', | ||||||
|  |   'conversation_updated', | ||||||
|  |   'message_created', | ||||||
|  |   'message_updated', | ||||||
|  |   'webwidget_triggered', | ||||||
|  | ]; | ||||||
|  |  | ||||||
|  | export default { | ||||||
|  |   mixins: [webhookMixin], | ||||||
|  |   props: { | ||||||
|  |     value: { | ||||||
|  |       type: Object, | ||||||
|  |       default: () => ({}), | ||||||
|  |     }, | ||||||
|  |     isSubmitting: { | ||||||
|  |       type: Boolean, | ||||||
|  |       default: false, | ||||||
|  |     }, | ||||||
|  |     submitLabel: { | ||||||
|  |       type: String, | ||||||
|  |       required: true, | ||||||
|  |     }, | ||||||
|  |   }, | ||||||
|  |   validations: { | ||||||
|  |     url: { | ||||||
|  |       required, | ||||||
|  |       minLength: minLength(7), | ||||||
|  |       url, | ||||||
|  |     }, | ||||||
|  |     subscriptions: { | ||||||
|  |       required, | ||||||
|  |     }, | ||||||
|  |   }, | ||||||
|  |   data() { | ||||||
|  |     return { | ||||||
|  |       url: this.value.url || '', | ||||||
|  |       subscriptions: this.value.subscriptions || [], | ||||||
|  |       supportedWebhookEvents: SUPPORTED_WEBHOOK_EVENTS, | ||||||
|  |     }; | ||||||
|  |   }, | ||||||
|  |   methods: { | ||||||
|  |     onSubmit() { | ||||||
|  |       this.$emit('submit', { | ||||||
|  |         url: this.url, | ||||||
|  |         subscriptions: this.subscriptions, | ||||||
|  |       }); | ||||||
|  |     }, | ||||||
|  |   }, | ||||||
|  | }; | ||||||
|  | </script> | ||||||
| @@ -0,0 +1,83 @@ | |||||||
|  | <template> | ||||||
|  |   <tr> | ||||||
|  |     <td> | ||||||
|  |       <div class="webhook--link">{{ webhook.url }}</div> | ||||||
|  |       <span class="webhook--subscribed-events"> | ||||||
|  |         <span class="webhook--subscribed-label"> | ||||||
|  |           {{ $t('INTEGRATION_SETTINGS.WEBHOOK.SUBSCRIBED_EVENTS') }}: | ||||||
|  |         </span> | ||||||
|  |         <show-more :text="subscribedEvents" :limit="60" /> | ||||||
|  |       </span> | ||||||
|  |     </td> | ||||||
|  |     <td class="button-wrapper"> | ||||||
|  |       <woot-button | ||||||
|  |         v-tooltip.top="$t('INTEGRATION_SETTINGS.WEBHOOK.EDIT.BUTTON_TEXT')" | ||||||
|  |         variant="smooth" | ||||||
|  |         size="tiny" | ||||||
|  |         color-scheme="secondary" | ||||||
|  |         icon="edit" | ||||||
|  |         @click="$emit('edit', webhook)" | ||||||
|  |       > | ||||||
|  |       </woot-button> | ||||||
|  |       <woot-button | ||||||
|  |         v-tooltip.top="$t('INTEGRATION_SETTINGS.WEBHOOK.DELETE.BUTTON_TEXT')" | ||||||
|  |         variant="smooth" | ||||||
|  |         color-scheme="alert" | ||||||
|  |         size="tiny" | ||||||
|  |         icon="dismiss-circle" | ||||||
|  |         @click="$emit('delete', webhook, index)" | ||||||
|  |       > | ||||||
|  |       </woot-button> | ||||||
|  |     </td> | ||||||
|  |   </tr> | ||||||
|  | </template> | ||||||
|  | <script> | ||||||
|  | import webhookMixin from './webhookMixin'; | ||||||
|  | import ShowMore from 'dashboard/components/widgets/ShowMore'; | ||||||
|  |  | ||||||
|  | export default { | ||||||
|  |   components: { ShowMore }, | ||||||
|  |   mixins: [webhookMixin], | ||||||
|  |   props: { | ||||||
|  |     webhook: { | ||||||
|  |       type: Object, | ||||||
|  |       required: true, | ||||||
|  |     }, | ||||||
|  |     index: { | ||||||
|  |       type: Number, | ||||||
|  |       required: true, | ||||||
|  |     }, | ||||||
|  |   }, | ||||||
|  |   computed: { | ||||||
|  |     subscribedEvents() { | ||||||
|  |       const { subscriptions } = this.webhook; | ||||||
|  |       return subscriptions.map(event => this.getEventLabel(event)).join(', '); | ||||||
|  |     }, | ||||||
|  |   }, | ||||||
|  | }; | ||||||
|  | </script> | ||||||
|  | <style scoped lang="scss"> | ||||||
|  | .webhook--link { | ||||||
|  |   color: var(--s-700); | ||||||
|  |   font-weight: var(--font-weight-medium); | ||||||
|  |   word-break: break-word; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .webhook--subscribed-events { | ||||||
|  |   color: var(--s-500); | ||||||
|  |   font-size: var(--font-size-mini); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .webhook--subscribed-label { | ||||||
|  |   font-weight: var(--font-weight-medium); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .button-wrapper { | ||||||
|  |   max-width: var(--space-mega); | ||||||
|  |   min-width: auto; | ||||||
|  |  | ||||||
|  |   button:nth-child(2) { | ||||||
|  |     margin-left: var(--space-normal); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | </style> | ||||||
| @@ -0,0 +1,26 @@ | |||||||
|  | import { createWrapper } from '@vue/test-utils'; | ||||||
|  | import webhookMixin from '../webhookMixin'; | ||||||
|  | import Vue from 'vue'; | ||||||
|  |  | ||||||
|  | describe('webhookMixin', () => { | ||||||
|  |   describe('#getEventLabel', () => { | ||||||
|  |     it('returns correct i18n translation:', () => { | ||||||
|  |       const Component = { | ||||||
|  |         render() {}, | ||||||
|  |         title: 'WebhookComponent', | ||||||
|  |         mixins: [webhookMixin], | ||||||
|  |         methods: { | ||||||
|  |           $t(text) { | ||||||
|  |             return text; | ||||||
|  |           }, | ||||||
|  |         }, | ||||||
|  |       }; | ||||||
|  |       const Constructor = Vue.extend(Component); | ||||||
|  |       const vm = new Constructor().$mount(); | ||||||
|  |       const wrapper = createWrapper(vm); | ||||||
|  |       expect(wrapper.vm.getEventLabel('message_created')).toEqual( | ||||||
|  |         `INTEGRATION_SETTINGS.WEBHOOK.FORM.SUBSCRIPTIONS.EVENTS.MESSAGE_CREATED` | ||||||
|  |       ); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | }); | ||||||
| @@ -0,0 +1,10 @@ | |||||||
|  | export default { | ||||||
|  |   methods: { | ||||||
|  |     getEventLabel(event) { | ||||||
|  |       const eventName = event.toUpperCase(); | ||||||
|  |       return this.$t( | ||||||
|  |         `INTEGRATION_SETTINGS.WEBHOOK.FORM.SUBSCRIPTIONS.EVENTS.${eventName}` | ||||||
|  |       ); | ||||||
|  |     }, | ||||||
|  |   }, | ||||||
|  | }; | ||||||
| @@ -1,6 +1,6 @@ | |||||||
| import Index from './Index'; | import Index from './Index'; | ||||||
| import SettingsContent from '../Wrapper'; | import SettingsContent from '../Wrapper'; | ||||||
| import Webhook from './Webhook'; | import Webhook from './Webhooks/Index'; | ||||||
| import ShowIntegration from './ShowIntegration'; | import ShowIntegration from './ShowIntegration'; | ||||||
| import { frontendURL } from '../../../../helper/URLHelper'; | import { frontendURL } from '../../../../helper/URLHelper'; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -14,7 +14,7 @@ const state = { | |||||||
|  |  | ||||||
| export const getters = { | export const getters = { | ||||||
|   getWebhooks(_state) { |   getWebhooks(_state) { | ||||||
|     return _state.records; |     return _state.records.sort((w1, w2) => w1.id - w2.id); | ||||||
|   }, |   }, | ||||||
|   getUIFlags(_state) { |   getUIFlags(_state) { | ||||||
|     return _state.uiFlags; |     return _state.uiFlags; | ||||||
|   | |||||||
| @@ -1,22 +1,4 @@ | |||||||
| class WebhookListener < BaseListener | class WebhookListener < BaseListener | ||||||
|   # FIXME: deprecate the opened and resolved events in future in favor of status changed event. |  | ||||||
|   def conversation_resolved(event) |  | ||||||
|     conversation = extract_conversation_and_account(event)[0] |  | ||||||
|     changed_attributes = extract_changed_attributes(event) |  | ||||||
|     inbox = conversation.inbox |  | ||||||
|     payload = conversation.webhook_data.merge(event: __method__.to_s, changed_attributes: changed_attributes) |  | ||||||
|     deliver_webhook_payloads(payload, inbox) |  | ||||||
|   end |  | ||||||
|  |  | ||||||
|   # FIXME: deprecate the opened and resolved events in future in favor of status changed event. |  | ||||||
|   def conversation_opened(event) |  | ||||||
|     conversation = extract_conversation_and_account(event)[0] |  | ||||||
|     changed_attributes = extract_changed_attributes(event) |  | ||||||
|     inbox = conversation.inbox |  | ||||||
|     payload = conversation.webhook_data.merge(event: __method__.to_s, changed_attributes: changed_attributes) |  | ||||||
|     deliver_webhook_payloads(payload, inbox) |  | ||||||
|   end |  | ||||||
|  |  | ||||||
|   def conversation_status_changed(event) |   def conversation_status_changed(event) | ||||||
|     conversation = extract_conversation_and_account(event)[0] |     conversation = extract_conversation_and_account(event)[0] | ||||||
|     changed_attributes = extract_changed_attributes(event) |     changed_attributes = extract_changed_attributes(event) | ||||||
| @@ -71,15 +53,23 @@ class WebhookListener < BaseListener | |||||||
|  |  | ||||||
|   private |   private | ||||||
|  |  | ||||||
|   def deliver_webhook_payloads(payload, inbox) |   def deliver_account_webhooks(payload, inbox) | ||||||
|     # Account webhooks |  | ||||||
|     inbox.account.webhooks.account.each do |webhook| |     inbox.account.webhooks.account.each do |webhook| | ||||||
|  |       next unless webhook.subscriptions.include?(payload[:event]) | ||||||
|  |  | ||||||
|       WebhookJob.perform_later(webhook.url, payload) |       WebhookJob.perform_later(webhook.url, payload) | ||||||
|     end |     end | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def deliver_api_inbox_webhooks(payload, inbox) | ||||||
|     return unless inbox.channel_type == 'Channel::Api' |     return unless inbox.channel_type == 'Channel::Api' | ||||||
|     return if inbox.channel.webhook_url.blank? |     return if inbox.channel.webhook_url.blank? | ||||||
|  |  | ||||||
|     WebhookJob.perform_later(inbox.channel.webhook_url, payload) |     WebhookJob.perform_later(inbox.channel.webhook_url, payload) | ||||||
|   end |   end | ||||||
|  |  | ||||||
|  |   def deliver_webhook_payloads(payload, inbox) | ||||||
|  |     deliver_account_webhooks(payload, inbox) | ||||||
|  |     deliver_api_inbox_webhooks(payload, inbox) | ||||||
|  |   end | ||||||
| end | end | ||||||
|   | |||||||
| @@ -3,6 +3,7 @@ | |||||||
| # Table name: webhooks | # Table name: webhooks | ||||||
| # | # | ||||||
| #  id            :bigint           not null, primary key | #  id            :bigint           not null, primary key | ||||||
|  | #  subscriptions :jsonb | ||||||
| #  url           :string | #  url           :string | ||||||
| #  webhook_type  :integer          default("account") | #  webhook_type  :integer          default("account") | ||||||
| #  created_at    :datetime         not null | #  created_at    :datetime         not null | ||||||
| @@ -21,6 +22,18 @@ class Webhook < ApplicationRecord | |||||||
|  |  | ||||||
|   validates :account_id, presence: true |   validates :account_id, presence: true | ||||||
|   validates :url, uniqueness: { scope: [:account_id] }, format: URI::DEFAULT_PARSER.make_regexp(%w[http https]) |   validates :url, uniqueness: { scope: [:account_id] }, format: URI::DEFAULT_PARSER.make_regexp(%w[http https]) | ||||||
|  |   validate :validate_webhook_subscriptions | ||||||
|   enum webhook_type: { account: 0, inbox: 1 } |   enum webhook_type: { account: 0, inbox: 1 } | ||||||
|  |  | ||||||
|  |   ALLOWED_WEBHOOK_EVENTS = %w[conversation_status_changed conversation_updated conversation_created message_created message_updated | ||||||
|  |                               webwidget_triggered].freeze | ||||||
|  |  | ||||||
|  |   private | ||||||
|  |  | ||||||
|  |   def validate_webhook_subscriptions | ||||||
|  |     invalid_subscriptions = !subscriptions.instance_of?(Array) || | ||||||
|  |                             subscriptions.blank? || | ||||||
|  |                             (subscriptions.uniq - ALLOWED_WEBHOOK_EVENTS).length.positive? | ||||||
|  |     errors.add(:subscriptions, I18n.t('errors.webhook.invalid')) if invalid_subscriptions | ||||||
|  |   end | ||||||
| end | end | ||||||
|   | |||||||
| @@ -1,6 +1,7 @@ | |||||||
| json.id webhook.id | json.id webhook.id | ||||||
| json.url webhook.url | json.url webhook.url | ||||||
| json.account_id webhook.account_id | json.account_id webhook.account_id | ||||||
|  | json.subscriptions webhook.subscriptions | ||||||
| if webhook.inbox | if webhook.inbox | ||||||
|   json.inbox do |   json.inbox do | ||||||
|     json.id webhook.inbox.id |     json.id webhook.inbox.id | ||||||
|   | |||||||
| @@ -36,6 +36,8 @@ en: | |||||||
|     reset_password_failure: Uh ho! We could not find any user with the specified email. |     reset_password_failure: Uh ho! We could not find any user with the specified email. | ||||||
|  |  | ||||||
|   errors: |   errors: | ||||||
|  |     webhook: | ||||||
|  |       invalid: Invalid events | ||||||
|     signup: |     signup: | ||||||
|       disposable_email: We do not allow disposable emails |       disposable_email: We do not allow disposable emails | ||||||
|       invalid_email: You have entered an invalid email |       invalid_email: You have entered an invalid email | ||||||
|   | |||||||
							
								
								
									
										12
									
								
								db/migrate/20220424081117_add_subscriptions_to_webhooks.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								db/migrate/20220424081117_add_subscriptions_to_webhooks.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | |||||||
|  | class AddSubscriptionsToWebhooks < ActiveRecord::Migration[6.1] | ||||||
|  |   def change | ||||||
|  |     add_column :webhooks, :subscriptions, :jsonb, default: %w[ | ||||||
|  |       conversation_status_changed | ||||||
|  |       conversation_updated | ||||||
|  |       conversation_created | ||||||
|  |       message_created | ||||||
|  |       message_updated | ||||||
|  |       webwidget_triggered | ||||||
|  |     ] | ||||||
|  |   end | ||||||
|  | end | ||||||
| @@ -10,7 +10,7 @@ | |||||||
| # | # | ||||||
| # It's strongly recommended that you check this file into your version control system. | # It's strongly recommended that you check this file into your version control system. | ||||||
|  |  | ||||||
| ActiveRecord::Schema.define(version: 2022_04_18_094715) do | ActiveRecord::Schema.define(version: 2022_04_24_081117) do | ||||||
|  |  | ||||||
|   # These are extensions that must be enabled in order to support this database |   # These are extensions that must be enabled in order to support this database | ||||||
|   enable_extension "pg_stat_statements" |   enable_extension "pg_stat_statements" | ||||||
| @@ -763,6 +763,7 @@ ActiveRecord::Schema.define(version: 2022_04_18_094715) do | |||||||
|     t.datetime "created_at", precision: 6, null: false |     t.datetime "created_at", precision: 6, null: false | ||||||
|     t.datetime "updated_at", precision: 6, null: false |     t.datetime "updated_at", precision: 6, null: false | ||||||
|     t.integer "webhook_type", default: 0 |     t.integer "webhook_type", default: 0 | ||||||
|  |     t.jsonb "subscriptions", default: ["conversation_status_changed", "conversation_updated", "conversation_created", "message_created", "message_updated", "webwidget_triggered"] | ||||||
|     t.index ["account_id", "url"], name: "index_webhooks_on_account_id_and_url", unique: true |     t.index ["account_id", "url"], name: "index_webhooks_on_account_id_and_url", unique: true | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   | |||||||
| @@ -57,6 +57,36 @@ RSpec.describe 'Webhooks API', type: :request do | |||||||
|         expect(response).to have_http_status(:unprocessable_entity) |         expect(response).to have_http_status(:unprocessable_entity) | ||||||
|         expect(JSON.parse(response.body)['message']).to eql 'Url is invalid' |         expect(JSON.parse(response.body)['message']).to eql 'Url is invalid' | ||||||
|       end |       end | ||||||
|  |  | ||||||
|  |       it 'throws error if subscription events are invalid' do | ||||||
|  |         post "/api/v1/accounts/#{account.id}/webhooks", | ||||||
|  |              params: { url: 'https://hello.com', subscriptions: ['conversation_random_event'] }, | ||||||
|  |              headers: administrator.create_new_auth_token, | ||||||
|  |              as: :json | ||||||
|  |         expect(response).to have_http_status(:unprocessable_entity) | ||||||
|  |         expect(JSON.parse(response.body)['message']).to eql 'Subscriptions Invalid events' | ||||||
|  |       end | ||||||
|  |  | ||||||
|  |       it 'throws error if subscription events are empty' do | ||||||
|  |         post "/api/v1/accounts/#{account.id}/webhooks", | ||||||
|  |              params: { url: 'https://hello.com', subscriptions: [] }, | ||||||
|  |              headers: administrator.create_new_auth_token, | ||||||
|  |              as: :json | ||||||
|  |         expect(response).to have_http_status(:unprocessable_entity) | ||||||
|  |         expect(JSON.parse(response.body)['message']).to eql 'Subscriptions Invalid events' | ||||||
|  |       end | ||||||
|  |  | ||||||
|  |       it 'use default if subscription events are nil' do | ||||||
|  |         post "/api/v1/accounts/#{account.id}/webhooks", | ||||||
|  |              params: { url: 'https://hello.com', subscriptions: nil }, | ||||||
|  |              headers: administrator.create_new_auth_token, | ||||||
|  |              as: :json | ||||||
|  |         expect(response).to have_http_status(:ok) | ||||||
|  |         expect( | ||||||
|  |           JSON.parse(response.body)['payload']['webhook']['subscriptions'] | ||||||
|  |         ).to eql %w[conversation_status_changed conversation_updated conversation_created message_created message_updated | ||||||
|  |                     webwidget_triggered] | ||||||
|  |       end | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   | |||||||
| @@ -3,5 +3,15 @@ FactoryBot.define do | |||||||
|     account_id { 1 } |     account_id { 1 } | ||||||
|     inbox_id { 1 } |     inbox_id { 1 } | ||||||
|     url { 'https://api.chatwoot.com' } |     url { 'https://api.chatwoot.com' } | ||||||
|  |     subscriptions do | ||||||
|  |       %w[ | ||||||
|  |         conversation_status_changed | ||||||
|  |         conversation_updated | ||||||
|  |         conversation_created | ||||||
|  |         message_created | ||||||
|  |         message_updated | ||||||
|  |         webwidget_triggered | ||||||
|  |       ] | ||||||
|  |     end | ||||||
|   end |   end | ||||||
| end | end | ||||||
|   | |||||||
| @@ -23,14 +23,22 @@ describe WebhookListener do | |||||||
|       end |       end | ||||||
|     end |     end | ||||||
|  |  | ||||||
|     context 'when webhook is configured' do |     context 'when webhook is configured and event is subscribed' do | ||||||
|       it 'triggers webhook' do |       it 'triggers the webhook event' do | ||||||
|         webhook = create(:webhook, inbox: inbox, account: account) |         webhook = create(:webhook, inbox: inbox, account: account) | ||||||
|         expect(WebhookJob).to receive(:perform_later).with(webhook.url, message.webhook_data.merge(event: 'message_created')).once |         expect(WebhookJob).to receive(:perform_later).with(webhook.url, message.webhook_data.merge(event: 'message_created')).once | ||||||
|         listener.message_created(message_created_event) |         listener.message_created(message_created_event) | ||||||
|       end |       end | ||||||
|     end |     end | ||||||
|  |  | ||||||
|  |     context 'when webhook is configured and event is not subscribed' do | ||||||
|  |       it 'does not trigger the webhook event' do | ||||||
|  |         create(:webhook, subscriptions: ['conversation_created'], inbox: inbox, account: account) | ||||||
|  |         expect(WebhookJob).not_to receive(:perform_later) | ||||||
|  |         listener.message_created(message_created_event) | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |  | ||||||
|     context 'when inbox is an API Channel' do |     context 'when inbox is an API Channel' do | ||||||
|       it 'triggers webhook if webhook_url is present' do |       it 'triggers webhook if webhook_url is present' do | ||||||
|         channel_api = create(:channel_api, account: account) |         channel_api = create(:channel_api, account: account) | ||||||
| @@ -106,36 +114,6 @@ describe WebhookListener do | |||||||
|     end |     end | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   describe '#conversation_resolved' do |  | ||||||
|     let!(:conversation_resolved_event) do |  | ||||||
|       Events::Base.new(event_name, Time.zone.now, conversation: conversation.reload, changed_attributes: { status: [:open, :resolved] }) |  | ||||||
|     end |  | ||||||
|     let(:event_name) { :'conversation.resolved' } |  | ||||||
|  |  | ||||||
|     context 'when webhook is not configured' do |  | ||||||
|       it 'does not trigger webhook' do |  | ||||||
|         expect(WebhookJob).to receive(:perform_later).exactly(0).times |  | ||||||
|         listener.conversation_resolved(conversation_resolved_event) |  | ||||||
|       end |  | ||||||
|     end |  | ||||||
|  |  | ||||||
|     context 'when webhook is configured' do |  | ||||||
|       it 'triggers webhook' do |  | ||||||
|         webhook = create(:webhook, inbox: inbox, account: account) |  | ||||||
|  |  | ||||||
|         conversation.update(status: :resolved) |  | ||||||
|  |  | ||||||
|         expect(WebhookJob).to receive(:perform_later).with(webhook.url, |  | ||||||
|                                                            conversation.webhook_data.merge(event: 'conversation_resolved', |  | ||||||
|                                                                                            changed_attributes: [{ status: { |  | ||||||
|                                                                                              current_value: :resolved, previous_value: :open |  | ||||||
|                                                                                            } }])).once |  | ||||||
|  |  | ||||||
|         listener.conversation_resolved(conversation_resolved_event) |  | ||||||
|       end |  | ||||||
|     end |  | ||||||
|   end |  | ||||||
|  |  | ||||||
|   describe '#conversation_updated' do |   describe '#conversation_updated' do | ||||||
|     let(:custom_attributes) { { test: nil } } |     let(:custom_attributes) { { test: nil } } | ||||||
|     let!(:conversation_updated_event) do |     let!(:conversation_updated_event) do | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Pranav Raj S
					Pranav Raj S