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:
parent
fa51fd1d73
commit
899176a793
25 changed files with 552 additions and 359 deletions
|
@ -23,7 +23,7 @@ class Api::V1::Accounts::WebhooksController < Api::V1::Accounts::BaseController
|
|||
private
|
||||
|
||||
def webhook_params
|
||||
params.require(:webhook).permit(:inbox_id, :url)
|
||||
params.require(:webhook).permit(:inbox_id, :url, subscriptions: [])
|
||||
end
|
||||
|
||||
def fetch_webhook
|
||||
|
|
|
@ -2,6 +2,10 @@
|
|||
margin-right: var(--space-small);
|
||||
}
|
||||
|
||||
.margin-bottom-small {
|
||||
margin-bottom: var(--space-small);
|
||||
}
|
||||
|
||||
.margin-right-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": {
|
||||
"HEADER": "Integrations",
|
||||
"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",
|
||||
"CONFIGURE": "Configure",
|
||||
"HEADER": "Webhook settings",
|
||||
|
@ -17,35 +40,16 @@
|
|||
"EDIT": {
|
||||
"BUTTON_TEXT": "Edit",
|
||||
"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": {
|
||||
"SUCCESS_MESSAGE": "Webhook URL updated successfully",
|
||||
"SUCCESS_MESSAGE": "Webhook configuration updated successfully",
|
||||
"ERROR_MESSAGE": "Could not connect to Woot Server, Please try again later"
|
||||
}
|
||||
},
|
||||
"ADD": {
|
||||
"CANCEL": "Cancel",
|
||||
"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": {
|
||||
"SUCCESS_MESSAGE": "Webhook added successfully",
|
||||
"SUCCESS_MESSAGE": "Webhook configuration added successfully",
|
||||
"ERROR_MESSAGE": "Could not connect to Woot Server, Please try again later"
|
||||
}
|
||||
},
|
||||
|
@ -57,16 +61,16 @@
|
|||
},
|
||||
"CONFIRM": {
|
||||
"TITLE": "Confirm Deletion",
|
||||
"MESSAGE": "Are you sure to delete ",
|
||||
"MESSAGE": "Are you sure to delete the webhook? (%{webhookURL})",
|
||||
"YES": "Yes, Delete ",
|
||||
"NO": "No, Keep it"
|
||||
}
|
||||
}
|
||||
},
|
||||
"SLACK": {
|
||||
"HELP_TEXT" : {
|
||||
"HELP_TEXT" : {
|
||||
"TITLE": "Using Slack Integration",
|
||||
"BODY": "<br/><p>Chatwoot will now sync all the incoming conversations into the <b><i>customer-conversations</i></b> channel inside your slack workplace.</p><p>Replying to a conversation thread in <b><i>customer-conversations</i></b> slack channel will create a response back to the customer through chatwoot.</p><p>Start the replies with <b><i>note:</i></b> to create private notes instead of replies.</p><p>If the replier on slack has an agent profile in chatwoot under the same email, the replies will be associated accordingly.</p><p>When the replier doesn't have an associated agent profile, the replies will be made from the bot profile.</p>"
|
||||
"BODY": "<br/><p>Chatwoot will now sync all the incoming conversations into the <b><i>customer-conversations</i></b> channel inside your slack workplace.</p><p>Replying to a conversation thread in <b><i>customer-conversations</i></b> slack channel will create a response back to the customer through chatwoot.</p><p>Start the replies with <b><i>note:</i></b> to create private notes instead of replies.</p><p>If the replier on slack has an agent profile in chatwoot under the same email, the replies will be associated accordingly.</p><p>When the replier doesn't have an associated agent profile, the replies will be made from the bot profile.</p>"
|
||||
}
|
||||
},
|
||||
"DELETE": {
|
||||
|
|
|
@ -127,6 +127,10 @@
|
|||
"BUTTON_TEXT": "Copy",
|
||||
"COPY_SUCCESSFUL": "Code copied to clipboard successfully"
|
||||
},
|
||||
"SHOW_MORE_BLOCK": {
|
||||
"SHOW_MORE": "Show More",
|
||||
"SHOW_LESS": "Show Less"
|
||||
},
|
||||
"FILE_BUBBLE": {
|
||||
"DOWNLOAD": "Download",
|
||||
"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"
|
||||
class-names="button--fixed-right-top"
|
||||
icon="add-circle"
|
||||
@click="openAddPopup()"
|
||||
@click="openAddPopup"
|
||||
>
|
||||
{{ $t('INTEGRATION_SETTINGS.WEBHOOK.HEADER_BTN_TXT') }}
|
||||
</woot-button>
|
||||
|
@ -37,35 +37,14 @@
|
|||
</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(webHookItem, index) in records" :key="webHookItem.id">
|
||||
<td class="webhook-link">
|
||||
{{ webHookItem.url }}
|
||||
</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="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>
|
||||
<webhook-row
|
||||
v-for="(webHookItem, index) in records"
|
||||
:key="webHookItem.id"
|
||||
:index="index"
|
||||
:webhook="webHookItem"
|
||||
@edit="openEditPopup"
|
||||
@delete="openDeletePopup"
|
||||
/>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
@ -83,24 +62,27 @@
|
|||
</div>
|
||||
|
||||
<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 :show.sync="showEditPopup" :on-close="hideEditPopup">
|
||||
<edit-webhook
|
||||
v-if="showEditPopup"
|
||||
:id="selectedWebHook.id"
|
||||
:url="selectedWebHook.url"
|
||||
:value="selectedWebHook"
|
||||
:on-close="hideEditPopup"
|
||||
/>
|
||||
</woot-modal>
|
||||
|
||||
<woot-delete-modal
|
||||
:show.sync="showDeleteConfirmationPopup"
|
||||
:on-close="closeDeletePopup"
|
||||
:on-confirm="confirmDeletion"
|
||||
: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')"
|
||||
:reject-text="$t('INTEGRATION_SETTINGS.WEBHOOK.DELETE.CONFIRM.NO')"
|
||||
/>
|
||||
|
@ -112,11 +94,13 @@ import NewWebhook from './NewWebHook';
|
|||
import EditWebhook from './EditWebHook';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import globalConfigMixin from 'shared/mixins/globalConfigMixin';
|
||||
import WebhookRow from './WebhookRow';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
NewWebhook,
|
||||
EditWebhook,
|
||||
WebhookRow,
|
||||
},
|
||||
mixins: [alertMixin, globalConfigMixin],
|
||||
data() {
|
||||
|
@ -179,11 +163,3 @@ export default {
|
|||
},
|
||||
};
|
||||
</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 SettingsContent from '../Wrapper';
|
||||
import Webhook from './Webhook';
|
||||
import Webhook from './Webhooks/Index';
|
||||
import ShowIntegration from './ShowIntegration';
|
||||
import { frontendURL } from '../../../../helper/URLHelper';
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@ const state = {
|
|||
|
||||
export const getters = {
|
||||
getWebhooks(_state) {
|
||||
return _state.records;
|
||||
return _state.records.sort((w1, w2) => w1.id - w2.id);
|
||||
},
|
||||
getUIFlags(_state) {
|
||||
return _state.uiFlags;
|
||||
|
|
|
@ -1,22 +1,4 @@
|
|||
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)
|
||||
conversation = extract_conversation_and_account(event)[0]
|
||||
changed_attributes = extract_changed_attributes(event)
|
||||
|
@ -71,15 +53,23 @@ class WebhookListener < BaseListener
|
|||
|
||||
private
|
||||
|
||||
def deliver_webhook_payloads(payload, inbox)
|
||||
# Account webhooks
|
||||
def deliver_account_webhooks(payload, inbox)
|
||||
inbox.account.webhooks.account.each do |webhook|
|
||||
next unless webhook.subscriptions.include?(payload[:event])
|
||||
|
||||
WebhookJob.perform_later(webhook.url, payload)
|
||||
end
|
||||
end
|
||||
|
||||
def deliver_api_inbox_webhooks(payload, inbox)
|
||||
return unless inbox.channel_type == 'Channel::Api'
|
||||
return if inbox.channel.webhook_url.blank?
|
||||
|
||||
WebhookJob.perform_later(inbox.channel.webhook_url, payload)
|
||||
end
|
||||
|
||||
def deliver_webhook_payloads(payload, inbox)
|
||||
deliver_account_webhooks(payload, inbox)
|
||||
deliver_api_inbox_webhooks(payload, inbox)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -2,13 +2,14 @@
|
|||
#
|
||||
# Table name: webhooks
|
||||
#
|
||||
# id :bigint not null, primary key
|
||||
# url :string
|
||||
# webhook_type :integer default("account")
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# account_id :integer
|
||||
# inbox_id :integer
|
||||
# id :bigint not null, primary key
|
||||
# subscriptions :jsonb
|
||||
# url :string
|
||||
# webhook_type :integer default("account")
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# account_id :integer
|
||||
# inbox_id :integer
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
|
@ -21,6 +22,18 @@ class Webhook < ApplicationRecord
|
|||
|
||||
validates :account_id, presence: true
|
||||
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 }
|
||||
|
||||
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
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
json.id webhook.id
|
||||
json.url webhook.url
|
||||
json.account_id webhook.account_id
|
||||
json.subscriptions webhook.subscriptions
|
||||
if webhook.inbox
|
||||
json.inbox do
|
||||
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.
|
||||
|
||||
errors:
|
||||
webhook:
|
||||
invalid: Invalid events
|
||||
signup:
|
||||
disposable_email: We do not allow disposable emails
|
||||
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.
|
||||
|
||||
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
|
||||
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 "updated_at", precision: 6, null: false
|
||||
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
|
||||
end
|
||||
|
||||
|
|
|
@ -57,6 +57,36 @@ RSpec.describe 'Webhooks API', type: :request do
|
|||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(JSON.parse(response.body)['message']).to eql 'Url is invalid'
|
||||
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
|
||||
|
||||
|
|
|
@ -3,5 +3,15 @@ FactoryBot.define do
|
|||
account_id { 1 }
|
||||
inbox_id { 1 }
|
||||
url { 'https://api.chatwoot.com' }
|
||||
subscriptions do
|
||||
%w[
|
||||
conversation_status_changed
|
||||
conversation_updated
|
||||
conversation_created
|
||||
message_created
|
||||
message_updated
|
||||
webwidget_triggered
|
||||
]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -23,14 +23,22 @@ describe WebhookListener do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when webhook is configured' do
|
||||
it 'triggers webhook' do
|
||||
context 'when webhook is configured and event is subscribed' do
|
||||
it 'triggers the webhook event' do
|
||||
webhook = create(:webhook, inbox: inbox, account: account)
|
||||
expect(WebhookJob).to receive(:perform_later).with(webhook.url, message.webhook_data.merge(event: 'message_created')).once
|
||||
listener.message_created(message_created_event)
|
||||
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
|
||||
it 'triggers webhook if webhook_url is present' do
|
||||
channel_api = create(:channel_api, account: account)
|
||||
|
@ -106,36 +114,6 @@ describe WebhookListener do
|
|||
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
|
||||
let(:custom_attributes) { { test: nil } }
|
||||
let!(:conversation_updated_event) do
|
||||
|
|
Loading…
Reference in a new issue