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:
parent
e8cf59c661
commit
c119c6577b
36 changed files with 845 additions and 49 deletions
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
9
app/javascript/dashboard/api/webhooks.js
Normal file
9
app/javascript/dashboard/api/webhooks.js
Normal file
|
@ -0,0 +1,9 @@
|
|||
import ApiClient from './ApiClient';
|
||||
|
||||
class WebHooks extends ApiClient {
|
||||
constructor() {
|
||||
super('account/webhooks');
|
||||
}
|
||||
}
|
||||
|
||||
export default new WebHooks();
|
|
@ -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 |
|
@ -25,6 +25,7 @@
|
|||
|
||||
@import 'views/settings/inbox';
|
||||
@import 'views/settings/channel';
|
||||
@import 'views/settings/integrations';
|
||||
@import 'views/signup';
|
||||
|
||||
@import 'plugins/multiselect';
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -59,7 +59,7 @@
|
|||
"TITLE": "Confirm Deletion",
|
||||
"MESSAGE": "Are you sure to delete ",
|
||||
"YES": "Yes, Delete ",
|
||||
"NO": "No, Keep "
|
||||
"NO": "No, Keep it "
|
||||
}
|
||||
},
|
||||
"EDIT": {
|
||||
|
|
|
@ -67,7 +67,7 @@
|
|||
"TITLE": "Confirm Deletion",
|
||||
"MESSAGE": "Are you sure to delete ",
|
||||
"YES": "Yes, Delete ",
|
||||
"NO": "No, Keep "
|
||||
"NO": "No, Keep it "
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
54
app/javascript/dashboard/i18n/locale/en/integrations.json
Normal file
54
app/javascript/dashboard/i18n/locale/en/integrations.json
Normal 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 "
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
5
app/javascript/dashboard/i18n/locale/en/webhooks.json
Normal file
5
app/javascript/dashboard/i18n/locale/en/webhooks.json
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"WEBHOOKS_SETTINGS": {
|
||||
"HEADER": "Webhook Settings"
|
||||
}
|
||||
}
|
|
@ -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({
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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: [
|
||||
{
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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'],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
|
@ -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,
|
||||
],
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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 }],
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
},
|
||||
];
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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([]);
|
||||
});
|
||||
});
|
||||
});
|
79
app/javascript/dashboard/store/modules/webhooks.js
Normal file
79
app/javascript/dashboard/store/modules/webhooks.js
Normal 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,
|
||||
};
|
|
@ -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',
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
},
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in a new issue