Feature: As a admin, I should be able to add webhooks to account (#572)

Co-authored-by: Pranav Raj S <pranavrajs@gmail.com>
This commit is contained in:
Nithin David Thomas 2020-02-29 17:43:49 +05:30 committed by GitHub
parent e8cf59c661
commit c119c6577b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
36 changed files with 845 additions and 49 deletions

View file

@ -23,7 +23,7 @@ class Api::V1::Account::WebhooksController < Api::BaseController
private
def webhook_params
params.require(:webhook).permit(:account_id, :inbox_id, :url)
params.require(:webhook).permit(:inbox_id, :url)
end
def fetch_webhook

View file

@ -31,9 +31,10 @@ class Api::V1::Widget::MessagesController < Api::V1::Widget::BaseController
def message_params
{
account_id: conversation.account_id,
contact_id: @contact.id,
content: permitted_params[:message][:content],
inbox_id: conversation.inbox_id,
message_type: :incoming,
content: permitted_params[:message][:content]
message_type: :incoming
}
end

View file

@ -5,7 +5,7 @@ class AsyncDispatcher < BaseDispatcher
end
def listeners
listeners = [ReportingListener.instance]
listeners = [ReportingListener.instance, WebhookListener.instance]
listeners << SubscriptionListener.instance if ENV['BILLING_ENABLED']
listeners
end

View file

@ -0,0 +1,9 @@
import ApiClient from './ApiClient';
class WebHooks extends ApiClient {
constructor() {
super('account/webhooks');
}
}
export default new WebHooks();

View file

@ -0,0 +1,64 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 21.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
<path style="fill:#4D4D4D;" d="M188.287,512c-41.473,0-75.213-33.74-75.213-75.213V246.75c0-4.142,3.358-7.5,7.5-7.5
s7.5,3.358,7.5,7.5v190.037c0,33.202,27.011,60.213,60.213,60.213c16.082,0,31.204-6.266,42.582-17.644
c11.37-11.37,17.631-26.488,17.631-42.569V75.213C248.5,33.74,282.24,0,323.713,0c20.088,0,38.978,7.826,53.189,22.037
c14.203,14.202,22.024,33.087,22.024,53.176V256c0,4.142-3.358,7.5-7.5,7.5s-7.5-3.358-7.5-7.5V75.213
c0-16.082-6.261-31.2-17.63-42.569C354.918,21.266,339.794,15,323.713,15C290.511,15,263.5,42.011,263.5,75.213v361.574
c0,20.088-7.822,38.973-22.024,53.176C227.265,504.174,208.376,512,188.287,512z"/>
<g>
<rect x="113.07" y="246.75" style="fill:#3B3B3B;" width="15" height="26.875"/>
<rect x="383.93" y="235.31" style="fill:#3B3B3B;" width="15" height="26.875"/>
</g>
<rect x="361.9" y="385" style="fill:#CCCCCC;" width="57.983" height="39.944"/>
<rect x="361.9" y="385" style="fill:#ADADAD;" width="57.983" height="22.19"/>
<path style="fill:#A6E2E3;" d="M432.802,298.678v86.977c0,3.616-2.932,6.548-6.548,6.548h-70.721c-3.617,0-6.548-2.932-6.548-6.548
v-87.746c0-23.439,19.239-42.39,42.803-41.899C414.709,256.486,432.802,275.751,432.802,298.678z"/>
<rect x="92.11" y="87.06" style="fill:#CCCCCC;" width="57.983" height="36.28"/>
<rect x="92.11" y="105.43" style="fill:#ADADAD;" width="57.983" height="17.907"/>
<path style="fill:#FFA638;" d="M163.015,126.345v86.977c0,22.927-18.093,42.191-41.015,42.668
c-23.564,0.49-42.803-18.461-42.803-41.899v-87.746c0-3.616,2.932-6.548,6.548-6.548l0,0h70.721l0,0
C160.083,119.797,163.015,122.729,163.015,126.345z"/>
<path style="fill:#7CCBCC;" d="M391.787,256.009c-5.066-0.105-9.93,0.693-14.447,2.236c0.396-0.081,0.781-0.166,1.142-0.257
c2.982-0.755,5.201-0.896,7.513-0.85c18.954,0.395,34.375,16.494,34.375,35.888v86.981c0,3.614-2.93,6.544-6.544,6.544H355.53
c-3.614,0-6.544-2.93-6.544-6.544l0,0v5.648c0,3.616,2.932,6.548,6.548,6.548h70.721c3.617,0,6.548-2.932,6.548-6.548v-86.977
C432.802,275.751,414.709,256.486,391.787,256.009z"/>
<path style="fill:#EB7100;" d="M79.527,217.153l-0.23-0.322c0.081,1.261,0.209,2.509,0.399,3.737
C79.586,219.444,79.527,218.305,79.527,217.153z"/>
<path style="fill:#ED8300;" d="M156.467,119.797h-11.188c2.613,0.858,4.502,3.314,4.502,6.215v90.372
c0,19.395-15.42,35.494-34.375,35.888c-0.251,0.005-0.503,0.008-0.753,0.008c-9.651,0-18.405-3.914-24.76-10.236
c7.856,8.766,19.342,14.212,32.106,13.947c22.922-0.477,41.015-19.741,41.015-42.668v-86.977
C163.015,122.728,160.083,119.797,156.467,119.797z"/>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3 KiB

View file

@ -25,6 +25,7 @@
@import 'views/settings/inbox';
@import 'views/settings/channel';
@import 'views/settings/integrations';
@import 'views/signup';
@import 'plugins/multiselect';

View file

@ -0,0 +1,37 @@
.integrations-wrap {
.integration {
background: $color-white;
border: 2px solid $color-border;
border-radius: $space-slab;
padding: $space-normal;
.integration--image {
display: flex;
margin-right: $space-normal;
width: 8rem;
img {
max-width: 8rem;
padding: $space-small;
}
}
.integration--title {
font-size: $font-size-large;
}
.integration--description {
padding-right: $space-medium;
}
.button-wrap {
@include flex;
@include flex-align(center, middle);
margin-bottom: 0;
}
}
}
.help-wrap {
padding-left: $space-large;
}

View file

@ -12,9 +12,19 @@
<script>
export default {
props: {
closeOnBackdropClick: {
type: Boolean,
default: true,
},
show: Boolean,
onClose: Function,
className: String,
onClose: {
type: Function,
required: true,
},
className: {
type: String,
default: '',
},
},
mounted() {
document.addEventListener('keydown', e => {
@ -25,7 +35,9 @@ export default {
},
methods: {
close() {
this.onClose();
if (this.closeOnBackdropClick) {
this.onClose();
}
},
},
};

View file

@ -52,6 +52,8 @@ export default {
'settings_inboxes_add_agents',
'settings_inbox_finish',
'billing',
'settings_integrations',
'settings_integrations_webhook',
],
menuItems: {
back: {
@ -89,12 +91,12 @@ export default {
toState: frontendURL('settings/billing'),
toStateName: 'billing',
},
account: {
icon: 'ion-beer',
label: 'Account Settings',
settings_integrations: {
icon: 'ion-flash',
label: 'Integrations',
hasSubMenu: false,
toState: frontendURL('settings/account'),
toStateName: 'account',
toState: frontendURL('settings/integrations'),
toStateName: 'settings_integrations',
},
},
},

View file

@ -59,7 +59,7 @@
"TITLE": "Confirm Deletion",
"MESSAGE": "Are you sure to delete ",
"YES": "Yes, Delete ",
"NO": "No, Keep "
"NO": "No, Keep it "
}
},
"EDIT": {

View file

@ -67,7 +67,7 @@
"TITLE": "Confirm Deletion",
"MESSAGE": "Are you sure to delete ",
"YES": "Yes, Delete ",
"NO": "No, Keep "
"NO": "No, Keep it "
}
}
}

View file

@ -88,7 +88,7 @@
"TITLE": "Confirm Deletion",
"MESSAGE": "Are you sure to delete ",
"YES": "Yes, Delete ",
"NO": "No, Keep "
"NO": "No, Keep it "
},
"API": {
"SUCCESS_MESSAGE": "Inbox deleted successfully",

View file

@ -12,6 +12,7 @@ import { default as _resetPassword } from './resetPassword.json';
import { default as _setNewPassword } from './setNewPassword.json';
import { default as _settings } from './settings.json';
import { default as _signup } from './signup.json';
import { default as _integrations } from './integrations.json';
export default {
..._agentMgmt,
@ -27,4 +28,5 @@ export default {
..._setNewPassword,
..._settings,
..._signup,
..._integrations,
};

View file

@ -0,0 +1,54 @@
{
"INTEGRATION_SETTINGS": {
"HEADER": "Integrations",
"WEBHOOK": {
"TITLE": "Webhook",
"CONFIGURE": "Configure",
"HEADER": "Webhook settings",
"HEADER_BTN_TXT": "Add new webhook",
"INTEGRATION_TXT": "Webhook events provide you the realtime information about what's happening in your Chatwoot account. You can make use of the webhooks to communicate the events to your favourite apps like Slack or Github. Click on Configure to set up your webhooks.",
"LOADING": "Fetching attached webhooks",
"SEARCH_404": "There are no items matching this query",
"SIDEBAR_TXT": "<p><b>Webhooks</b> </p> <p>Webhooks are HTTP callbacks which can be defined for every account. They are triggered by events like message creation in Chatwoot. You can create more than one webhook for this account. <br /><br /> For creating a <b>webhook</b>, click on the <b>Add new webhook</b> button. You can also remove any existing webhook by clicking on the Delete button.</p>",
"LIST": {
"404": "There are no webhooks configured for this account.",
"TITLE": "Manage webhooks",
"DESC": "Webhooks are predefined reply templates which can be used to quickly send out replies to tickets.",
"TABLE_HEADER": [
"Webhook endpoint",
"Actions"
]
},
"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",
"ERROR_MESSAGE": "Could not connect to Woot Server, Please try again later"
}
},
"DELETE": {
"BUTTON_TEXT": "Delete",
"API": {
"SUCCESS_MESSAGE": "Webhook deleted successfully",
"ERROR_MESSAGE": "Could not connect to Woot Server, Please try again later"
},
"CONFIRM": {
"TITLE": "Confirm Deletion",
"MESSAGE": "Are you sure to delete ",
"YES": "Yes, Delete ",
"NO": "No, Keep it "
}
}
}
}
}

View file

@ -0,0 +1,5 @@
{
"WEBHOOKS_SETTINGS": {
"HEADER": "Webhook Settings"
}
}

View file

@ -2,12 +2,12 @@
<div class="settings-header">
<h1 class="page-title">
<woot-sidemenu-icon></woot-sidemenu-icon>
<back-button v-if="!showButton"></back-button>
<back-button v-if="showBackButton"></back-button>
<i :class="iconClass"></i>
<span>{{ headerTitle }}</span>
</h1>
<router-link
v-if="showNewButton && showButton && isAdmin"
v-if="showNewButton && isAdmin"
:to="buttonRoute"
class="button icon success nice button--fixed-right-top"
>
@ -41,14 +41,8 @@ export default {
default: '',
type: String,
},
showButton: Boolean,
showNewButton: Boolean,
hideButtonRoutes: {
type: Array,
default() {
return ['agent_list', 'settings_inbox_list'];
},
},
showBackButton: { type: Boolean, default: false },
showNewButton: { type: Boolean, default: false },
},
computed: {
...mapGetters({

View file

@ -5,8 +5,8 @@
:icon="icon"
:header-title="$t(headerTitle)"
:button-text="$t(headerButtonText)"
:show-button="showButton()"
:show-new-button="showNewButton()"
:show-back-button="showBackButton"
:show-new-button="showNewButton"
/>
<keep-alive>
<router-view></router-view>
@ -26,7 +26,14 @@ export default {
headerTitle: String,
headerButtonText: String,
icon: String,
newButtonRoutes: Array,
newButtonRoutes: {
type: Array,
default: () => [],
},
showBackButton: {
type: Boolean,
default: false,
},
},
data() {
return {};
@ -35,16 +42,8 @@ export default {
currentPage() {
return this.$store.state.route.name;
},
},
methods: {
showButton() {
/* eslint-disable no-unneeded-ternary */
return this.newButtonRoutes
? this.newButtonRoutes.indexOf(this.currentPage) > -1
: true;
},
showNewButton() {
return this.newButtonRoutes ? true : false;
return this.newButtonRoutes.length !== 0 && !this.showBackButton;
},
},
};

View file

@ -14,11 +14,15 @@ export default {
{
path: frontendURL('settings/inboxes'),
component: SettingsContent,
props: {
headerTitle: 'INBOX_MGMT.HEADER',
headerButtonText: 'SETTINGS.INBOXES.NEW_INBOX',
icon: 'ion-archive',
newButtonRoutes: ['settings_inbox_list'],
props: params => {
const showBackButton = params.name !== 'settings_inbox_list';
return {
headerTitle: 'INBOX_MGMT.HEADER',
headerButtonText: 'SETTINGS.INBOXES.NEW_INBOX',
icon: 'ion-archive',
newButtonRoutes: ['settings_inbox_list'],
showBackButton,
};
},
children: [
{

View file

@ -0,0 +1,32 @@
<template>
<modal :show.sync="show" :on-close="onClose">
<woot-modal-header :header-title="title" :header-content="message" />
<div class="modal-footer delete-item">
<button class="button" @click="onClose">
{{ rejectText }}
</button>
<button class="alert button" @click="onConfirm">
{{ confirmText }}
</button>
</div>
</modal>
</template>
<script>
import Modal from '../../../../components/Modal';
export default {
components: {
Modal,
},
props: {
show: Boolean,
onClose: Function,
onConfirm: Function,
title: String,
message: String,
confirmText: String,
rejectText: String,
},
};
</script>

View file

@ -0,0 +1,44 @@
<template>
<div class="column content-box">
<div class="row">
<div class="small-8 columns integrations-wrap">
<div class="row integrations">
<div class="small-12 columns integration">
<div class="row">
<div class="integration--image">
<img src="~dashboard/assets/images/integrations/cable.svg" />
</div>
<div class="column">
<h3 class="integration--title">
{{ $t('INTEGRATION_SETTINGS.WEBHOOK.TITLE') }}
</h3>
<p class="integration--description">
{{ $t('INTEGRATION_SETTINGS.WEBHOOK.INTEGRATION_TXT') }}
</p>
</div>
<div class="small-2 column button-wrap">
<router-link :to="frontendURL('settings/integrations/webhook')">
<button class="button success nice">
{{ $t('INTEGRATION_SETTINGS.WEBHOOK.CONFIGURE') }}
</button>
</router-link>
</div>
</div>
</div>
</div>
</div>
<!-- <div class="small-4 columns help-wrap">
<span v-html="$t('INTEGRATION_SETTINGS.SIDEBAR_TXT')"></span>
</div> -->
</div>
</div>
</template>
<script>
import { frontendURL } from '../../../../helper/URLHelper';
export default {
methods: {
frontendURL,
},
};
</script>

View file

@ -0,0 +1,114 @@
<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="$t('INTEGRATION_SETTINGS.WEBHOOK.ADD.DESC')"
/>
<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-submit-button
:disabled="$v.endPoint.$invalid || addWebHook.showLoading"
:button-text="$t('INTEGRATION_SETTINGS.WEBHOOK.ADD.FORM.SUBMIT')"
:loading="addWebHook.showLoading"
/>
<a @click="onClose">
{{ $t('INTEGRATION_SETTINGS.WEBHOOK.ADD.CANCEL') }}
</a>
</div>
</div>
</form>
</div>
</modal>
</template>
<script>
/* global bus */
import { required, url, minLength } from 'vuelidate/lib/validators';
import WootSubmitButton from '../../../../components/buttons/FormSubmitButton';
import Modal from '../../../../components/Modal';
export default {
components: {
WootSubmitButton,
Modal,
},
props: {
onClose: {
type: Function,
required: true,
},
},
data() {
return {
endPoint: '',
addWebHook: {
showAlert: false,
showLoading: false,
message: '',
},
show: true,
};
},
validations: {
endPoint: {
required,
minLength: minLength(7),
url,
},
},
methods: {
showAlert() {
bus.$emit('newToastMessage', this.addWebHook.message);
},
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.showAlert();
this.resetForm();
this.onClose();
} catch (error) {
this.addWebHook.showLoading = false;
this.addWebHook.message =
error.response.data.message ||
this.$t('INTEGRATION_SETTINGS.WEBHOOK.ADD.API.ERROR_MESSAGE');
this.showAlert();
}
},
},
};
</script>

View file

@ -0,0 +1,141 @@
<template>
<div class="row content-box full-height">
<button
class="button nice icon success button--fixed-right-top"
@click="openAddPopup()"
>
<i class="icon ion-android-add-circle"></i>
{{ $t('INTEGRATION_SETTINGS.WEBHOOK.HEADER_BTN_TXT') }}
</button>
<div class="row">
<div class="small-8 columns">
<p
v-if="!uiFlags.fetchingList && !records.length"
class="no-items-error-message"
>
{{ $t('INTEGRATION_SETTINGS.WEBHOOK.LIST.404') }}
</p>
<woot-loading-state
v-if="uiFlags.fetchingList"
:message="$t('INTEGRATION_SETTINGS.WEBHOOK.LOADING')"
/>
<table
v-if="!uiFlags.fetchingList && records.length"
class="woot-table"
>
<thead>
<th
v-for="thHeader in $t(
'INTEGRATION_SETTINGS.WEBHOOK.LIST.TABLE_HEADER'
)"
:key="thHeader"
>
{{ thHeader }}
</th>
</thead>
<tbody>
<tr v-for="(webHookItem, index) in records" :key="webHookItem.id">
<td>{{ webHookItem.url }}</td>
<td class="button-wrapper">
<div @click="openDeletePopup(webHookItem, index)">
<woot-submit-button
:button-text="
$t('INTEGRATION_SETTINGS.WEBHOOK.DELETE.BUTTON_TEXT')
"
:loading="loading[webHookItem.id]"
icon-class="ion-close-circled"
button-class="link hollow grey-btn"
/>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<div class="small-4 columns">
<span v-html="$t('INTEGRATION_SETTINGS.WEBHOOK.SIDEBAR_TXT')"></span>
</div>
</div>
<woot-modal :show.sync="showAddPopup" :on-close="hideAddPopup">
<new-webhook :on-close="hideAddPopup" />
</woot-modal>
<delete-webhook
: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')"
:confirm-text="$t('INTEGRATION_SETTINGS.WEBHOOK.DELETE.CONFIRM.YES')"
:reject-text="$t('INTEGRATION_SETTINGS.WEBHOOK.DELETE.CONFIRM.NO')"
/>
</div>
</template>
<script>
/* global bus */
import { mapGetters } from 'vuex';
import NewWebhook from './New';
import DeleteWebhook from './Delete';
export default {
components: {
NewWebhook,
DeleteWebhook,
},
data() {
return {
loading: {},
showAddPopup: false,
showDeleteConfirmationPopup: false,
selectedWebHook: {},
};
},
computed: {
...mapGetters({
records: 'webhooks/getWebhooks',
uiFlags: 'webhooks/getUIFlags',
}),
},
mounted() {
this.$store.dispatch('webhooks/get');
},
methods: {
showAlert(message) {
bus.$emit('newToastMessage', message);
},
openAddPopup() {
this.showAddPopup = true;
},
hideAddPopup() {
this.showAddPopup = false;
},
openDeletePopup(response) {
this.showDeleteConfirmationPopup = true;
this.selectedWebHook = response;
},
closeDeletePopup() {
this.showDeleteConfirmationPopup = false;
},
confirmDeletion() {
this.loading[this.selectedWebHook.id] = true;
this.closeDeletePopup();
this.deleteWebhook(this.selectedWebHook.id);
},
async deleteWebhook(id) {
try {
await this.$store.dispatch('webhooks/delete', id);
this.showAlert(
this.$t('INTEGRATION_SETTINGS.WEBHOOK.DELETE.API.SUCCESS_MESSAGE')
);
} catch (error) {
this.showAlert(
this.$t('INTEGRATION_SETTINGS.WEBHOOK.DELETE.API.ERROR_MESSAGE')
);
}
},
},
};
</script>

View file

@ -0,0 +1,35 @@
import Index from './Index';
import SettingsContent from '../Wrapper';
import Webhook from './Webhook';
import { frontendURL } from '../../../../helper/URLHelper';
export default {
routes: [
{
path: frontendURL('settings/integrations'),
component: SettingsContent,
props: params => {
const showBackButton = params.name !== 'settings_integrations';
return {
headerTitle: 'INTEGRATION_SETTINGS.HEADER',
icon: 'ion-flash',
showBackButton,
};
},
children: [
{
path: '',
name: 'settings_integrations',
component: Index,
roles: ['administrator'],
},
{
path: 'webhook',
component: Webhook,
name: 'settings_integrations_webhook',
roles: ['administrator'],
},
],
},
],
};

View file

@ -6,6 +6,7 @@ import canned from './canned/canned.routes';
import inbox from './inbox/inbox.routes';
import profile from './profile/profile.routes';
import reports from './reports/reports.routes';
import integrations from './integrations/integrations.routes';
export default {
routes: [
@ -26,5 +27,6 @@ export default {
...inbox.routes,
...profile.routes,
...reports.routes,
...integrations.routes,
],
};

View file

@ -15,6 +15,7 @@ import conversations from './modules/conversations';
import inboxes from './modules/inboxes';
import inboxMembers from './modules/inboxMembers';
import reports from './modules/reports';
import webhooks from './modules/webhooks';
Vue.use(Vuex);
export default new Vuex.Store({
@ -33,5 +34,6 @@ export default new Vuex.Store({
inboxes,
inboxMembers,
reports,
webhooks,
},
});

View file

@ -0,0 +1,76 @@
import axios from 'axios';
import { actions } from '../../webhooks';
import * as types from '../../../mutation-types';
import webhooks from './fixtures';
const commit = jest.fn();
global.axios = axios;
jest.mock('axios');
describe('#actions', () => {
describe('#get', () => {
it('sends correct actions if API is success', async () => {
axios.get.mockResolvedValue({ data: { payload: { webhooks } } });
await actions.get({ commit });
expect(commit.mock.calls).toEqual([
[types.default.SET_WEBHOOK_UI_FLAG, { fetchingList: true }],
[types.default.SET_WEBHOOK, webhooks],
[types.default.SET_WEBHOOK_UI_FLAG, { fetchingList: false }],
]);
});
it('sends correct actions if API is error', async () => {
axios.get.mockRejectedValue({ message: 'Incorrect header' });
await actions.get({ commit });
expect(commit.mock.calls).toEqual([
[types.default.SET_WEBHOOK_UI_FLAG, { fetchingList: true }],
[types.default.SET_WEBHOOK_UI_FLAG, { fetchingList: false }],
]);
});
});
describe('#create', () => {
it('sends correct actions if API is success', async () => {
axios.post.mockResolvedValue({
data: { payload: { webhook: webhooks[0] } },
});
await actions.create({ commit }, webhooks[0]);
expect(commit.mock.calls).toEqual([
[types.default.SET_WEBHOOK_UI_FLAG, { creatingItem: true }],
[types.default.ADD_WEBHOOK, webhooks[0]],
[types.default.SET_WEBHOOK_UI_FLAG, { creatingItem: false }],
]);
});
it('sends correct actions if API is error', async () => {
axios.post.mockRejectedValue({ message: 'Incorrect header' });
await expect(actions.create({ commit }, webhooks[0].id)).rejects.toEqual({
message: 'Incorrect header',
});
expect(commit.mock.calls).toEqual([
[types.default.SET_WEBHOOK_UI_FLAG, { creatingItem: true }],
[types.default.SET_WEBHOOK_UI_FLAG, { creatingItem: false }],
]);
});
});
describe('#delete', () => {
it('sends correct actions if API is success', async () => {
axios.delete.mockResolvedValue({ data: webhooks[0] });
await actions.delete({ commit }, webhooks[0].id);
expect(commit.mock.calls).toEqual([
[types.default.SET_WEBHOOK_UI_FLAG, { deletingItem: true }],
[types.default.DELETE_WEBHOOK, webhooks[0].id],
[types.default.SET_WEBHOOK_UI_FLAG, { deletingItem: false }],
]);
});
it('sends correct actions if API is error', async () => {
axios.delete.mockRejectedValue({ message: 'Incorrect header' });
await expect(actions.delete({ commit }, webhooks[0].id)).rejects.toEqual({
message: 'Incorrect header',
});
expect(commit.mock.calls).toEqual([
[types.default.SET_WEBHOOK_UI_FLAG, { deletingItem: true }],
[types.default.SET_WEBHOOK_UI_FLAG, { deletingItem: false }],
]);
});
});
});

View file

@ -0,0 +1,17 @@
export default [
{
id: 4,
url: 'https://1.chatwoot.com',
account_id: 1,
},
{
id: 5,
url: 'https://2.chatwoot.com',
account_id: 1,
},
{
id: 6,
url: 'https://3.chatwoot.com',
account_id: 1,
},
];

View file

@ -0,0 +1,30 @@
import { getters } from '../../webhooks';
import webhooks from './fixtures';
describe('#getters', () => {
it('getInboxes', () => {
const state = {
records: webhooks,
};
expect(getters.getWebhooks(state)).toEqual(webhooks);
});
it('getUIFlags', () => {
const state = {
uiFlags: {
fetchingList: false,
fetchingItem: false,
creatingItem: false,
updatingItem: false,
deletingItem: false,
},
};
expect(getters.getUIFlags(state)).toEqual({
fetchingList: false,
fetchingItem: false,
creatingItem: false,
updatingItem: false,
deletingItem: false,
});
});
});

View file

@ -0,0 +1,33 @@
import * as types from '../../../mutation-types';
import { mutations } from '../../webhooks';
import webhooks from './fixtures';
describe('#mutations', () => {
describe('#SET_WEBHOOK', () => {
it('set webhook records', () => {
const state = { records: [] };
mutations[types.default.SET_WEBHOOK](state, webhooks);
expect(state.records).toEqual(webhooks);
});
});
describe('#ADD_WEBHOOK', () => {
it('push newly created webhook data to the store', () => {
const state = {
records: [],
};
mutations[types.default.ADD_WEBHOOK](state, webhooks[0]);
expect(state.records).toEqual([webhooks[0]]);
});
});
describe('#DELETE_WEBHOOK', () => {
it('delete webhook record', () => {
const state = {
records: [webhooks[0]],
};
mutations[types.default.DELETE_WEBHOOK](state, webhooks[0].id);
expect(state.records).toEqual([]);
});
});
});

View file

@ -0,0 +1,79 @@
import * as MutationHelpers from 'shared/helpers/vuex/mutationHelpers';
import * as types from '../mutation-types';
import webHookAPI from '../../api/webhooks';
const state = {
records: [],
uiFlags: {
fetchingList: false,
creatingItem: false,
deletingItem: false,
},
};
export const getters = {
getWebhooks(_state) {
return _state.records;
},
getUIFlags(_state) {
return _state.uiFlags;
},
};
export const actions = {
async get({ commit }) {
commit(types.default.SET_WEBHOOK_UI_FLAG, { fetchingList: true });
try {
const response = await webHookAPI.get();
commit(types.default.SET_WEBHOOK, response.data.payload.webhooks);
commit(types.default.SET_WEBHOOK_UI_FLAG, { fetchingList: false });
} catch (error) {
commit(types.default.SET_WEBHOOK_UI_FLAG, { fetchingList: false });
}
},
async create({ commit }, params) {
commit(types.default.SET_WEBHOOK_UI_FLAG, { creatingItem: true });
try {
const response = await webHookAPI.create(params);
commit(types.default.ADD_WEBHOOK, response.data.payload.webhook);
commit(types.default.SET_WEBHOOK_UI_FLAG, { creatingItem: false });
} catch (error) {
commit(types.default.SET_WEBHOOK_UI_FLAG, { creatingItem: false });
throw error;
}
},
async delete({ commit }, id) {
commit(types.default.SET_WEBHOOK_UI_FLAG, { deletingItem: true });
try {
await webHookAPI.delete(id);
commit(types.default.DELETE_WEBHOOK, id);
commit(types.default.SET_WEBHOOK_UI_FLAG, { deletingItem: false });
} catch (error) {
commit(types.default.SET_WEBHOOK_UI_FLAG, { deletingItem: false });
throw error;
}
},
};
export const mutations = {
[types.default.SET_WEBHOOK_UI_FLAG](_state, data) {
_state.uiFlags = {
..._state.uiFlags,
...data,
};
},
[types.default.SET_WEBHOOK]: MutationHelpers.set,
[types.default.ADD_WEBHOOK]: MutationHelpers.create,
[types.default.DELETE_WEBHOOK]: MutationHelpers.destroy,
};
export default {
namespaced: true,
state,
getters,
actions,
mutations,
};

View file

@ -56,6 +56,12 @@ export default {
EDIT_CANNED: 'EDIT_CANNED',
DELETE_CANNED: 'DELETE_CANNED',
// WebHook
SET_WEBHOOK_UI_FLAG: 'SET_WEBHOOK_UI_FLAG',
SET_WEBHOOK: 'SET_WEBHOOK',
ADD_WEBHOOK: 'ADD_WEBHOOK',
DELETE_WEBHOOK: 'DELETE_WEBHOOK',
// Contacts
SET_CONTACT_UI_FLAG: 'SET_CONTACT_UI_FLAG',
SET_CONTACT_ITEM: 'SET_CONTACT_ITEM',

View file

@ -16,6 +16,7 @@ class Webhook < ApplicationRecord
belongs_to :inbox, optional: true
validates :account_id, presence: true
validates :url, uniqueness: { scope: [:account_id] }, format: { with: URI::DEFAULT_PARSER.make_regexp }
enum webhook_type: { account: 0, inbox: 1 }
end

View file

@ -1,5 +1,5 @@
json.payload do
json.webhooks do
json.array! @webhooks, partial: 'webhooks/webhook', as: :webhook
json.array! @webhooks, partial: 'webhook', as: :webhook
end
end

View file

@ -50,7 +50,7 @@
"vue-router": "~2.2.0",
"vue-select": "~2.0.0",
"vue-template-compiler": "^2.6.10",
"vuelidate": "~0.2.0",
"vuelidate": "~0.7.5",
"vuex": "~2.1.1",
"vuex-router-sync": "~4.1.2"
},

View file

@ -2,6 +2,6 @@ FactoryBot.define do
factory :webhook do
account_id { 1 }
inbox_id { 1 }
url { 'MyString' }
url { 'https://api.chatwoot.com' }
end
end

View file

@ -10466,10 +10466,10 @@ vue@^2.5.8, vue@^2.6.0:
resolved "https://registry.yarnpkg.com/vue/-/vue-2.6.11.tgz#76594d877d4b12234406e84e35275c6d514125c5"
integrity sha512-VfPwgcGABbGAue9+sfrD4PuwFar7gPb1yl1UK1MwXoQPAw0BKSqWfoYCT/ThFrdEVWoI51dBuyCoiNU9bZDZxQ==
vuelidate@~0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/vuelidate/-/vuelidate-0.2.0.tgz#7c50b220ef3700b1a28900f32825c442df809337"
integrity sha1-fFCyIO83ALGiiQDzKCXEQt+Akzc=
vuelidate@~0.7.5:
version "0.7.5"
resolved "https://registry.yarnpkg.com/vuelidate/-/vuelidate-0.7.5.tgz#ff48c75ae9d24ea24c24e9ea08065eda0a0cba0a"
integrity sha512-GAAG8QAFVp7BFeQlNaThpTbimq3+HypBPNwdkCkHZZeVaD5zmXXfhp357dcUJXHXTZjSln0PvP6wiwLZXkFTwg==
vuex-router-sync@~4.1.2:
version "4.1.3"