feat: Add Integration hooks UI (#2301)
This commit is contained in:
parent
c6487877bf
commit
14b51e108a
35 changed files with 1108 additions and 31 deletions
|
@ -16,6 +16,14 @@ class IntegrationsAPI extends ApiClient {
|
|||
delete(integrationId) {
|
||||
return axios.delete(`${this.baseUrl()}/integrations/${integrationId}`);
|
||||
}
|
||||
|
||||
createHook(hookData) {
|
||||
return axios.post(`${this.baseUrl()}/integrations/hooks`, hookData);
|
||||
}
|
||||
|
||||
deleteHook(hookId) {
|
||||
return axios.delete(`${this.baseUrl()}/integrations/hooks/${hookId}`);
|
||||
}
|
||||
}
|
||||
|
||||
export default new IntegrationsAPI();
|
||||
|
|
|
@ -5,6 +5,7 @@ function apiSpecHelper() {
|
|||
post: jest.fn(() => Promise.resolve()),
|
||||
get: jest.fn(() => Promise.resolve()),
|
||||
patch: jest.fn(() => Promise.resolve()),
|
||||
delete: jest.fn(() => Promise.resolve()),
|
||||
};
|
||||
window.axios = this.axiosMock;
|
||||
});
|
||||
|
|
55
app/javascript/dashboard/api/specs/integrations.spec.js
Normal file
55
app/javascript/dashboard/api/specs/integrations.spec.js
Normal file
|
@ -0,0 +1,55 @@
|
|||
import integrationAPI from '../integrations';
|
||||
import ApiClient from '../ApiClient';
|
||||
import describeWithAPIMock from './apiSpecHelper';
|
||||
|
||||
describe('#integrationAPI', () => {
|
||||
it('creates correct instance', () => {
|
||||
expect(integrationAPI).toBeInstanceOf(ApiClient);
|
||||
expect(integrationAPI).toHaveProperty('get');
|
||||
expect(integrationAPI).toHaveProperty('show');
|
||||
expect(integrationAPI).toHaveProperty('create');
|
||||
expect(integrationAPI).toHaveProperty('update');
|
||||
expect(integrationAPI).toHaveProperty('delete');
|
||||
expect(integrationAPI).toHaveProperty('connectSlack');
|
||||
expect(integrationAPI).toHaveProperty('createHook');
|
||||
expect(integrationAPI).toHaveProperty('deleteHook');
|
||||
});
|
||||
describeWithAPIMock('API calls', context => {
|
||||
it('#connectSlack', () => {
|
||||
const code = 'SDNFJNSDFNDSJN';
|
||||
integrationAPI.connectSlack(code);
|
||||
expect(context.axiosMock.post).toHaveBeenCalledWith(
|
||||
'/api/v1/integrations/slack',
|
||||
{
|
||||
code,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('#delete', () => {
|
||||
integrationAPI.delete(2);
|
||||
expect(context.axiosMock.delete).toHaveBeenCalledWith(
|
||||
'/api/v1/integrations/2'
|
||||
);
|
||||
});
|
||||
|
||||
it('#createHook', () => {
|
||||
const hookData = {
|
||||
app_id: 'fullcontact',
|
||||
settings: { api_key: 'SDFSDGSVE' },
|
||||
};
|
||||
integrationAPI.createHook(hookData);
|
||||
expect(context.axiosMock.post).toHaveBeenCalledWith(
|
||||
'/api/v1/integrations/hooks',
|
||||
hookData
|
||||
);
|
||||
});
|
||||
|
||||
it('#deleteHook', () => {
|
||||
integrationAPI.deleteHook(2);
|
||||
expect(context.axiosMock.delete).toHaveBeenCalledWith(
|
||||
'/api/v1/integrations/hooks/2'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
18
app/javascript/dashboard/assets/scss/_formulate.scss
Normal file
18
app/javascript/dashboard/assets/scss/_formulate.scss
Normal file
|
@ -0,0 +1,18 @@
|
|||
@import '~dashboard/assets/scss/variables';
|
||||
|
||||
.formulate-input {
|
||||
.formulate-input-errors {
|
||||
list-style-type: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.formulate-input-error {
|
||||
color: var(--r-400);
|
||||
display: block;
|
||||
font-size: var(--font-size-small);
|
||||
font-weight: $font-weight-normal;
|
||||
margin-bottom: $space-one;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
|
@ -11,13 +11,13 @@
|
|||
@import 'mixins';
|
||||
@import 'foundation-settings';
|
||||
@import 'helper-classes';
|
||||
@import 'formulate';
|
||||
|
||||
@import 'foundation-sites/scss/foundation';
|
||||
@import '~bourbon/core/bourbon';
|
||||
|
||||
@include foundation-everything($flex: true);
|
||||
|
||||
|
||||
@import 'typography';
|
||||
@import 'layout';
|
||||
@import 'animations';
|
||||
|
|
|
@ -13,6 +13,18 @@ export default () => {
|
|||
}
|
||||
};
|
||||
|
||||
export const isEmptyObject = obj =>
|
||||
Object.keys(obj).length === 0 && obj.constructor === Object;
|
||||
|
||||
export const isJSONValid = value => {
|
||||
try {
|
||||
JSON.parse(value);
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
export const getTypingUsersText = (users = []) => {
|
||||
const count = users.length;
|
||||
if (count === 1) {
|
||||
|
|
|
@ -74,6 +74,9 @@ export const getSidebarItems = accountId => ({
|
|||
'settings_integrations',
|
||||
'settings_integrations_webhook',
|
||||
'settings_integrations_integration',
|
||||
'settings_applications',
|
||||
'settings_applications_webhook',
|
||||
'settings_applications_integration',
|
||||
'general_settings',
|
||||
'general_settings_index',
|
||||
'settings_teams_list',
|
||||
|
@ -136,6 +139,13 @@ export const getSidebarItems = accountId => ({
|
|||
toState: frontendURL(`accounts/${accountId}/settings/integrations`),
|
||||
toStateName: 'settings_integrations',
|
||||
},
|
||||
settings_applications: {
|
||||
icon: 'ion-asterisk',
|
||||
label: 'APPLICATIONS',
|
||||
hasSubMenu: false,
|
||||
toState: frontendURL(`accounts/${accountId}/settings/applications`),
|
||||
toStateName: 'settings_applications',
|
||||
},
|
||||
general_settings_index: {
|
||||
icon: 'ion-gear-a',
|
||||
label: 'ACCOUNT_SETTINGS',
|
||||
|
|
|
@ -15,6 +15,7 @@ import { default as _setNewPassword } from './setNewPassword.json';
|
|||
import { default as _settings } from './settings.json';
|
||||
import { default as _signup } from './signup.json';
|
||||
import { default as _teamsSettings } from './teamsSettings.json';
|
||||
import { default as _integrationApps } from './integrationApps.json';
|
||||
|
||||
export default {
|
||||
..._agentMgmt,
|
||||
|
@ -34,4 +35,5 @@ export default {
|
|||
..._settings,
|
||||
..._signup,
|
||||
..._teamsSettings,
|
||||
..._integrationApps,
|
||||
};
|
||||
|
|
62
app/javascript/dashboard/i18n/locale/en/integrationApps.json
Normal file
62
app/javascript/dashboard/i18n/locale/en/integrationApps.json
Normal file
|
@ -0,0 +1,62 @@
|
|||
{
|
||||
"INTEGRATION_APPS": {
|
||||
"FETCHING": "Fetching Integrations",
|
||||
"NO_HOOK_CONFIGURED": "There are no %{integrationId} integrations configured in this account.",
|
||||
"HEADER": "Applications",
|
||||
"STATUS": {
|
||||
"ENABLED": "Enabled",
|
||||
"DISABLED": "Disabled"
|
||||
},
|
||||
"CONFIGURE": "Configure",
|
||||
"ADD_BUTTON": "Add a new hook",
|
||||
"DELETE": {
|
||||
"TITLE": {
|
||||
"INBOX": "Confirm deletion",
|
||||
"ACCOUNT": "Disconnect"
|
||||
},
|
||||
"MESSAGE": {
|
||||
"INBOX": "Are you sure to delete?",
|
||||
"ACCOUNT": "Are you sure to disconnect?"
|
||||
},
|
||||
"CONFIRM_BUTTON_TEXT": {
|
||||
"INBOX": "Yes, Delete",
|
||||
"ACCOUNT": "Yes, Disconnect"
|
||||
},
|
||||
"CANCEL_BUTTON_TEXT": "Cancel",
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Hook deleted successfully",
|
||||
"ERROR_MESSAGE": "Could not connect to Woot Server, Please try again later"
|
||||
}
|
||||
},
|
||||
"LIST": {
|
||||
"FETCHING": "Fetching integration hooks",
|
||||
"INBOX": "Inbox",
|
||||
"DELETE": {
|
||||
"BUTTON_TEXT": "Delete"
|
||||
}
|
||||
},
|
||||
"ADD": {
|
||||
"FORM": {
|
||||
"INBOX": {
|
||||
"LABEL": "Select Inbox",
|
||||
"PLACEHOLDER": "Select Inbox"
|
||||
},
|
||||
"SUBMIT": "Create",
|
||||
"CANCEL": "Cancel"
|
||||
},
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Integration hook added successfully",
|
||||
"ERROR_MESSAGE": "Could not connect to Woot Server, Please try again later"
|
||||
}
|
||||
},
|
||||
"CONNECT": {
|
||||
"BUTTON_TEXT": "Connect"
|
||||
},
|
||||
"DISCONNECT": {
|
||||
"BUTTON_TEXT": "Disconnect"
|
||||
},
|
||||
"SIDEBAR_DESCRIPTION": {
|
||||
"DIALOGFLOW": "Dialogflow is a natural language understanding platform that makes it easy to design and integrate a conversational user interface into your mobile app, web application, device, bot, interactive voice response system, and so on. <br /> <br /> Dialogflow integration with %{installationName} allows you to configure a Dialogflow bot with your inboxes which lets the bot handle the queries initially and hand them over to an agent when needed. Dialogflow can be used to qualifying the leads, reduce the workload of agents by providing frequently asked questions etc. <br /> <br /> To add Dialogflow, you need to create a Service Account in your Google project console and share the credentials. Please refer to the Dialogflow docs for more information."
|
||||
}
|
||||
}
|
||||
}
|
|
@ -133,6 +133,7 @@
|
|||
"CANNED_RESPONSES": "Canned Responses",
|
||||
"INTEGRATIONS": "Integrations",
|
||||
"ACCOUNT_SETTINGS": "Account Settings",
|
||||
"APPLICATIONS": "Applications",
|
||||
"LABELS": "Labels",
|
||||
"TEAMS": "Teams"
|
||||
},
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
<template>
|
||||
<div class="column content-box">
|
||||
<div class="row">
|
||||
<div class="empty-wrapper">
|
||||
<woot-loading-state
|
||||
v-if="uiFlags.isFetching"
|
||||
:message="$t('INTEGRATION_APPS.FETCHING')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="small-12 columns integrations-wrap">
|
||||
<div class="row integrations">
|
||||
<div
|
||||
v-for="item in integrationsList"
|
||||
:key="item.id"
|
||||
class="small-12 columns integration"
|
||||
>
|
||||
<integration-item
|
||||
:integration-id="item.id"
|
||||
:integration-logo="item.logo"
|
||||
:integration-name="item.name"
|
||||
:integration-description="item.description"
|
||||
:integration-enabled="item.hooks.length"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import IntegrationItem from './IntegrationItem';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
IntegrationItem,
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
uiFlags: 'labels/getUIFlags',
|
||||
integrationsList: 'integrations/getAppIntegrations',
|
||||
}),
|
||||
},
|
||||
mounted() {
|
||||
this.$store.dispatch('integrations/get');
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '~dashboard/assets/scss/variables';
|
||||
.empty-wrapper {
|
||||
margin: var(--space-zero) auto;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,155 @@
|
|||
<template>
|
||||
<div class="row content-box full-height">
|
||||
<woot-button
|
||||
v-if="showAddButton"
|
||||
color-scheme="success"
|
||||
class-names="button--fixed-right-top"
|
||||
icon="ion-android-add-circle"
|
||||
@click="openAddHookModal"
|
||||
>
|
||||
{{ $t('INTEGRATION_APPS.ADD_BUTTON') }}
|
||||
</woot-button>
|
||||
<div v-if="showIntegrationHooks" class="integration-hooks">
|
||||
<div v-if="isIntegrationMultiple">
|
||||
<multiple-integration-hooks
|
||||
:integration="integration"
|
||||
@delete="openDeletePopup"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="isIntegrationSingle">
|
||||
<single-integration-hooks
|
||||
:integration="integration"
|
||||
@add="openAddHookModal"
|
||||
@delete="openDeletePopup"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<woot-modal :show.sync="showAddHookModal" :on-close="hideAddHookModal">
|
||||
<new-hook :integration="integration" @close="hideAddHookModal" />
|
||||
</woot-modal>
|
||||
|
||||
<woot-delete-modal
|
||||
:show.sync="showDeleteConfirmationPopup"
|
||||
:on-close="closeDeletePopup"
|
||||
:on-confirm="confirmDeletion"
|
||||
:title="deleteTitle"
|
||||
:message="deleteMessage"
|
||||
:confirm-text="confirmText"
|
||||
:reject-text="cancelText"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { isEmptyObject } from '../../../../helper/commons';
|
||||
import { mapGetters } from 'vuex';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import hookMixin from './hookMixin';
|
||||
import NewHook from './NewHook';
|
||||
import SingleIntegrationHooks from './SingleIntegrationHooks';
|
||||
import MultipleIntegrationHooks from './MultipleIntegrationHooks';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
NewHook,
|
||||
SingleIntegrationHooks,
|
||||
MultipleIntegrationHooks,
|
||||
},
|
||||
mixins: [alertMixin, hookMixin],
|
||||
props: {
|
||||
integrationId: {
|
||||
type: [String, Number],
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: {},
|
||||
showAddHookModal: false,
|
||||
showDeleteConfirmationPopup: false,
|
||||
selectedHook: {},
|
||||
alertMessage: '',
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({ uiFlags: 'integrations/getUIFlags' }),
|
||||
integration() {
|
||||
return this.$store.getters['integrations/getIntegration'](
|
||||
this.integrationId
|
||||
);
|
||||
},
|
||||
showIntegrationHooks() {
|
||||
return !this.uiFlags.isFetching && !isEmptyObject(this.integration);
|
||||
},
|
||||
integrationType() {
|
||||
return this.integration.allow_multiple_hooks ? 'multiple' : 'single';
|
||||
},
|
||||
isIntegrationMultiple() {
|
||||
return this.integrationType === 'multiple';
|
||||
},
|
||||
isIntegrationSingle() {
|
||||
return this.integrationType === 'single';
|
||||
},
|
||||
showAddButton() {
|
||||
return this.showIntegrationHooks && this.isIntegrationMultiple;
|
||||
},
|
||||
deleteTitle() {
|
||||
return this.isHookTypeInbox
|
||||
? this.$t('INTEGRATION_APPS.DELETE.TITLE.INBOX')
|
||||
: this.$t('INTEGRATION_APPS.DELETE.TITLE.ACCOUNT');
|
||||
},
|
||||
deleteMessage() {
|
||||
return this.isHookTypeInbox
|
||||
? this.$t('INTEGRATION_APPS.DELETE.MESSAGE.INBOX')
|
||||
: this.$t('INTEGRATION_APPS.DELETE.MESSAGE.ACCOUNT');
|
||||
},
|
||||
confirmText() {
|
||||
return this.isHookTypeInbox
|
||||
? this.$t('INTEGRATION_APPS.DELETE.CONFIRM_BUTTON_TEXT.INBOX')
|
||||
: this.$t('INTEGRATION_APPS.DELETE.CONFIRM_BUTTON_TEXT.ACCOUNT');
|
||||
},
|
||||
cancelText() {
|
||||
return this.$t('INTEGRATION_APPS.DELETE.CANCEL_BUTTON_TEXT');
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
openAddHookModal() {
|
||||
this.showAddHookModal = true;
|
||||
},
|
||||
hideAddHookModal() {
|
||||
this.showAddHookModal = false;
|
||||
},
|
||||
openDeletePopup(response) {
|
||||
this.showDeleteConfirmationPopup = true;
|
||||
this.selectedHook = response;
|
||||
},
|
||||
closeDeletePopup() {
|
||||
this.showDeleteConfirmationPopup = false;
|
||||
},
|
||||
async confirmDeletion() {
|
||||
try {
|
||||
await this.$store.dispatch('integrations/deleteHook', {
|
||||
hookId: this.selectedHook.id,
|
||||
appId: this.selectedHook.app_id,
|
||||
});
|
||||
this.alertMessage = this.$t(
|
||||
'INTEGRATION_APPS.DELETE.API.SUCCESS_MESSAGE'
|
||||
);
|
||||
this.closeDeletePopup();
|
||||
} catch (error) {
|
||||
const errorMessage = error?.response?.data?.message;
|
||||
this.alertMessage =
|
||||
errorMessage || this.$t('INTEGRATION_APPS.DELETE.API.ERROR_MESSAGE');
|
||||
} finally {
|
||||
this.showAlert(this.alertMessage);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.integration-hooks {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,78 @@
|
|||
<template>
|
||||
<div class="row">
|
||||
<div class="integration--image">
|
||||
<img :src="'/dashboard/images/integrations/' + integrationLogo" />
|
||||
</div>
|
||||
<div class="column">
|
||||
<h3 class="integration--title">
|
||||
{{ integrationName }}
|
||||
</h3>
|
||||
<p class="integration--description">
|
||||
{{ integrationDescription }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="small-2 column button-wrap">
|
||||
<woot-label :title="labelText" :color-scheme="labelColor" />
|
||||
</div>
|
||||
<div class="small-2 column button-wrap">
|
||||
<router-link
|
||||
:to="
|
||||
frontendURL(
|
||||
`accounts/${accountId}/settings/applications/` + integrationId
|
||||
)
|
||||
"
|
||||
>
|
||||
<woot-button icon="ion-gear-b">
|
||||
{{ $t('INTEGRATION_APPS.CONFIGURE') }}
|
||||
</woot-button>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { frontendURL } from '../../../../helper/URLHelper';
|
||||
import WootLabel from 'dashboard/components/ui/Label';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
WootLabel,
|
||||
},
|
||||
props: {
|
||||
integrationId: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
integrationLogo: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
integrationName: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
integrationDescription: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
integrationEnabled: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({ accountId: 'getCurrentAccountId' }),
|
||||
labelText() {
|
||||
return this.integrationEnabled
|
||||
? this.$t('INTEGRATION_APPS.STATUS.ENABLED')
|
||||
: this.$t('INTEGRATION_APPS.STATUS.DISABLED');
|
||||
},
|
||||
labelColor() {
|
||||
return this.integrationEnabled ? 'success' : 'secondary';
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
frontendURL,
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -0,0 +1,106 @@
|
|||
<template>
|
||||
<div class="row ">
|
||||
<div class="small-8 columns">
|
||||
<table v-if="hasConnectedHooks" class="woot-table">
|
||||
<thead>
|
||||
<th v-for="hookHeader in hookHeaders" :key="hookHeader">
|
||||
{{ hookHeader }}
|
||||
</th>
|
||||
<th v-if="isHookTypeInbox">
|
||||
{{ $t('INTEGRATION_APPS.LIST.INBOX') }}
|
||||
</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="hook in hooks" :key="hook.id">
|
||||
<td
|
||||
v-for="property in hook.properties"
|
||||
:key="property"
|
||||
class="hook-item"
|
||||
>
|
||||
{{ property }}
|
||||
</td>
|
||||
<td v-if="isHookTypeInbox" class="hook-item">
|
||||
{{ inboxName(hook) }}
|
||||
</td>
|
||||
<td class="button-wrapper">
|
||||
<woot-button
|
||||
variant="link"
|
||||
color-scheme="secondary"
|
||||
icon="ion-close-circled"
|
||||
class-names="grey-btn"
|
||||
@click="$emit('delete', hook)"
|
||||
>
|
||||
{{ $t('INTEGRATION_APPS.LIST.DELETE.BUTTON_TEXT') }}
|
||||
</woot-button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p v-else class="no-items-error-message">
|
||||
{{
|
||||
$t('INTEGRATION_APPS.NO_HOOK_CONFIGURED', {
|
||||
integrationId: integration.id,
|
||||
})
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
<div class="small-4 columns">
|
||||
<p>
|
||||
<b>{{ integration.name }}</b>
|
||||
</p>
|
||||
<p
|
||||
v-html="
|
||||
$t(
|
||||
`INTEGRATION_APPS.SIDEBAR_DESCRIPTION.${integration.name.toUpperCase()}`,
|
||||
{ installationName: globalConfig.installationName }
|
||||
)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import hookMixin from './hookMixin';
|
||||
export default {
|
||||
mixins: [hookMixin],
|
||||
props: {
|
||||
integration: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
globalConfig: 'globalConfig/get',
|
||||
}),
|
||||
hookHeaders() {
|
||||
return this.integration.visible_properties;
|
||||
},
|
||||
hooks() {
|
||||
if (!this.hasConnectedHooks) {
|
||||
return [];
|
||||
}
|
||||
const { hooks } = this.integration;
|
||||
return hooks.map(hook => ({
|
||||
...hook,
|
||||
id: hook.id,
|
||||
properties: this.hookHeaders.map(property =>
|
||||
hook.settings[property] ? hook.settings[property] : '--'
|
||||
),
|
||||
}));
|
||||
},
|
||||
},
|
||||
mounted() {},
|
||||
methods: {
|
||||
inboxName(hook) {
|
||||
return hook.inbox ? hook.inbox.name : '';
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.hook-item {
|
||||
word-break: break-word;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,136 @@
|
|||
<template>
|
||||
<div class="column content-box">
|
||||
<woot-modal-header
|
||||
:header-title="integration.name"
|
||||
:header-content="integration.description"
|
||||
/>
|
||||
<formulate-form
|
||||
#default="{ hasErrors }"
|
||||
v-model="values"
|
||||
@submit="submitForm"
|
||||
>
|
||||
<formulate-input
|
||||
v-for="item in formItems"
|
||||
:key="item.name"
|
||||
v-bind="item"
|
||||
/>
|
||||
<formulate-input
|
||||
v-if="isHookTypeInbox"
|
||||
:options="inboxes"
|
||||
type="select"
|
||||
name="inbox"
|
||||
:placeholder="$t('INTEGRATION_APPS.ADD.FORM.INBOX.LABEL')"
|
||||
:label="$t('INTEGRATION_APPS.ADD.FORM.INBOX.PLACEHOLDER')"
|
||||
validation="required"
|
||||
validation-name="Inbox"
|
||||
/>
|
||||
<div class="modal-footer">
|
||||
<woot-button :disabled="hasErrors" :loading="uiFlags.isCreatingHook">
|
||||
{{ $t('INTEGRATION_APPS.ADD.FORM.SUBMIT') }}
|
||||
</woot-button>
|
||||
<woot-button class="button clear" @click.prevent="onClose">
|
||||
{{ $t('INTEGRATION_APPS.ADD.FORM.CANCEL') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</formulate-form>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import hookMixin from './hookMixin';
|
||||
|
||||
export default {
|
||||
mixins: [alertMixin, hookMixin],
|
||||
props: {
|
||||
integration: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
endPoint: '',
|
||||
alertMessage: '',
|
||||
values: {},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
uiFlags: 'integrations/getUIFlags',
|
||||
websiteInboxes: 'inboxes/getWebsiteInboxes',
|
||||
}),
|
||||
inboxes() {
|
||||
return this.websiteInboxes
|
||||
.filter(inbox => {
|
||||
if (!this.isIntegrationDialogflow) {
|
||||
return true;
|
||||
}
|
||||
return !this.connectedDialogflowInboxIds.includes(inbox.id);
|
||||
})
|
||||
.map(inbox => ({ label: inbox.name, value: inbox.id }));
|
||||
},
|
||||
|
||||
connectedDialogflowInboxIds() {
|
||||
if (!this.isIntegrationDialogflow) {
|
||||
return [];
|
||||
}
|
||||
return this.integration.hooks.map(hook => hook.inbox?.id);
|
||||
},
|
||||
formItems() {
|
||||
return this.integration.settings_form_schema;
|
||||
},
|
||||
isIntegrationDialogflow() {
|
||||
return this.integration.id === 'dialogflow';
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onClose() {
|
||||
this.$emit('close');
|
||||
},
|
||||
buildHookPayload() {
|
||||
const hookPayload = {
|
||||
app_id: this.integration.id,
|
||||
settings: {},
|
||||
};
|
||||
|
||||
hookPayload.settings = Object.keys(this.values).reduce((acc, key) => {
|
||||
if (key !== 'inbox') {
|
||||
acc[key] = this.values[key];
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
this.formItems.forEach(item => {
|
||||
if (item.validation.includes('JSON')) {
|
||||
hookPayload.settings[item.name] = JSON.parse(
|
||||
hookPayload.settings[item.name]
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
if (this.isHookTypeInbox && this.values.inbox) {
|
||||
hookPayload.inbox_id = this.values.inbox;
|
||||
}
|
||||
|
||||
return hookPayload;
|
||||
},
|
||||
async submitForm() {
|
||||
try {
|
||||
await this.$store.dispatch(
|
||||
'integrations/createHook',
|
||||
this.buildHookPayload()
|
||||
);
|
||||
this.alertMessage = this.$t('INTEGRATION_APPS.ADD.API.SUCCESS_MESSAGE');
|
||||
this.onClose();
|
||||
} catch (error) {
|
||||
const errorMessage = error?.response?.data?.message;
|
||||
this.alertMessage =
|
||||
errorMessage || this.$t('INTEGRATION_APPS.ADD.API.ERROR_MESSAGE');
|
||||
} finally {
|
||||
this.showAlert(this.alertMessage);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -0,0 +1,47 @@
|
|||
<template>
|
||||
<div class="column content-box">
|
||||
<div class="small-12 columns integrations-wrap">
|
||||
<div class="small-12 columns integration">
|
||||
<div class="row">
|
||||
<div class="integration--image">
|
||||
<img :src="'/dashboard/images/integrations/' + integration.logo" />
|
||||
</div>
|
||||
<div class="column">
|
||||
<h3 class="integration--title">
|
||||
{{ integration.name }}
|
||||
</h3>
|
||||
<p class="integration--description">
|
||||
{{ integration.description }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="small-2 column button-wrap">
|
||||
<div v-if="hasConnectedHooks">
|
||||
<div @click="$emit('delete', integration.hooks[0])">
|
||||
<woot-button class="nice alert">
|
||||
{{ $t('INTEGRATION_APPS.DISCONNECT.BUTTON_TEXT') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<woot-button class="button nice" @click="$emit('add')">
|
||||
{{ $t('INTEGRATION_APPS.CONNECT.BUTTON_TEXT') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import hookMixin from './hookMixin';
|
||||
export default {
|
||||
mixins: [hookMixin],
|
||||
props: {
|
||||
integration: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -0,0 +1,10 @@
|
|||
export default {
|
||||
computed: {
|
||||
isHookTypeInbox() {
|
||||
return this.integration.hook_type === 'inbox';
|
||||
},
|
||||
hasConnectedHooks() {
|
||||
return !!this.integration.hooks.length;
|
||||
},
|
||||
},
|
||||
};
|
|
@ -0,0 +1,43 @@
|
|||
import Index from './Index';
|
||||
import SettingsContent from '../Wrapper';
|
||||
import IntegrationHooks from './IntegrationHooks';
|
||||
import { frontendURL } from '../../../../helper/URLHelper';
|
||||
|
||||
export default {
|
||||
routes: [
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/settings/applications'),
|
||||
component: SettingsContent,
|
||||
props: params => {
|
||||
const showBackButton = params.name !== 'settings_applications';
|
||||
const backUrl =
|
||||
params.name === 'settings_applications_integration'
|
||||
? { name: 'settings_applications' }
|
||||
: '';
|
||||
return {
|
||||
headerTitle: 'INTEGRATION_APPS.HEADER',
|
||||
icon: 'ion-asterisk',
|
||||
showBackButton,
|
||||
backUrl,
|
||||
};
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'settings_applications',
|
||||
component: Index,
|
||||
roles: ['administrator'],
|
||||
},
|
||||
{
|
||||
path: ':integration_id',
|
||||
name: 'settings_applications_integration',
|
||||
component: IntegrationHooks,
|
||||
roles: ['administrator'],
|
||||
props: route => ({
|
||||
integrationId: route.params.integration_id,
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
|
@ -0,0 +1,26 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import hookMixin from '../hookMixin';
|
||||
|
||||
describe('hookMixin', () => {
|
||||
const Component = {
|
||||
render() {},
|
||||
mixins: [hookMixin],
|
||||
data() {
|
||||
return {
|
||||
integration: {
|
||||
hook_type: 'inbox',
|
||||
hooks: [{ id: 1, properties: {} }],
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
const wrapper = shallowMount(Component);
|
||||
|
||||
it('#isHookTypeInbox returns correct value', () => {
|
||||
expect(wrapper.vm.isHookTypeInbox).toBe(true);
|
||||
});
|
||||
|
||||
it('#hasConnectedHooks returns correct value', () => {
|
||||
expect(wrapper.vm.hasConnectedHooks).toBe(true);
|
||||
});
|
||||
});
|
|
@ -4,6 +4,7 @@ import agent from './agents/agent.routes';
|
|||
import canned from './canned/canned.routes';
|
||||
import inbox from './inbox/inbox.routes';
|
||||
import integrations from './integrations/integrations.routes';
|
||||
import integrationapps from './integrationapps/integrations.routes';
|
||||
import labels from './labels/labels.routes';
|
||||
import profile from './profile/profile.routes';
|
||||
import reports from './reports/reports.routes';
|
||||
|
@ -32,5 +33,6 @@ export default {
|
|||
...profile.routes,
|
||||
...reports.routes,
|
||||
...teams.routes,
|
||||
...integrationapps.routes,
|
||||
],
|
||||
};
|
||||
|
|
|
@ -65,6 +65,11 @@ export const getters = {
|
|||
getUIFlags($state) {
|
||||
return $state.uiFlags;
|
||||
},
|
||||
getWebsiteInboxes($state) {
|
||||
return $state.records.filter(
|
||||
item => item.channel_type === 'Channel::WebWidget'
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const actions = {
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
/* eslint no-param-reassign: 0 */
|
||||
import Vue from 'vue';
|
||||
import * as MutationHelpers from 'shared/helpers/vuex/mutationHelpers';
|
||||
import * as types from '../mutation-types';
|
||||
import IntegrationsAPI from '../../api/integrations';
|
||||
|
@ -6,15 +7,23 @@ import IntegrationsAPI from '../../api/integrations';
|
|||
const state = {
|
||||
records: [],
|
||||
uiFlags: {
|
||||
isCreating: false,
|
||||
isFetching: false,
|
||||
isFetchingItem: false,
|
||||
isUpdating: false,
|
||||
isCreatingHook: false,
|
||||
isDeletingHook: false,
|
||||
},
|
||||
};
|
||||
|
||||
export const getters = {
|
||||
getIntegrations($state) {
|
||||
return $state.records;
|
||||
return $state.records.filter(
|
||||
item => item.id !== 'fullcontact' && item.id !== 'dialogflow'
|
||||
);
|
||||
},
|
||||
getAppIntegrations($state) {
|
||||
return $state.records.filter(item => item.id === 'dialogflow');
|
||||
},
|
||||
getIntegration: $state => integrationId => {
|
||||
const [integration] = $state.records.filter(
|
||||
|
@ -63,6 +72,28 @@ export const actions = {
|
|||
commit(types.default.SET_INTEGRATIONS_UI_FLAG, { isDeleting: false });
|
||||
}
|
||||
},
|
||||
createHook: async ({ commit }, hookData) => {
|
||||
commit(types.default.SET_INTEGRATIONS_UI_FLAG, { isCreatingHook: true });
|
||||
try {
|
||||
const response = await IntegrationsAPI.createHook(hookData);
|
||||
commit(types.default.ADD_INTEGRATION_HOOKS, response.data);
|
||||
commit(types.default.SET_INTEGRATIONS_UI_FLAG, { isCreatingHook: false });
|
||||
} catch (error) {
|
||||
commit(types.default.SET_INTEGRATIONS_UI_FLAG, { isCreatingHook: false });
|
||||
throw new Error(error);
|
||||
}
|
||||
},
|
||||
deleteHook: async ({ commit }, { appId, hookId }) => {
|
||||
commit(types.default.SET_INTEGRATIONS_UI_FLAG, { isDeletingHook: true });
|
||||
try {
|
||||
await IntegrationsAPI.deleteHook(hookId);
|
||||
commit(types.default.DELETE_INTEGRATION_HOOKS, { appId, hookId });
|
||||
commit(types.default.SET_INTEGRATIONS_UI_FLAG, { isDeletingHook: false });
|
||||
} catch (error) {
|
||||
commit(types.default.SET_INTEGRATIONS_UI_FLAG, { isDeletingHook: false });
|
||||
throw new Error(error);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export const mutations = {
|
||||
|
@ -72,6 +103,25 @@ export const mutations = {
|
|||
[types.default.SET_INTEGRATIONS]: MutationHelpers.set,
|
||||
[types.default.ADD_INTEGRATION]: MutationHelpers.updateAttributes,
|
||||
[types.default.DELETE_INTEGRATION]: MutationHelpers.updateAttributes,
|
||||
[types.default.ADD_INTEGRATION_HOOKS]: ($state, data) => {
|
||||
$state.records.forEach((element, index) => {
|
||||
if (element.id === data.app_id) {
|
||||
const record = $state.records[index];
|
||||
Vue.set(record, 'hooks', [...record.hooks, data]);
|
||||
}
|
||||
});
|
||||
},
|
||||
[types.default.DELETE_INTEGRATION_HOOKS]: ($state, { appId, hookId }) => {
|
||||
$state.records.forEach((element, index) => {
|
||||
if (element.id === appId) {
|
||||
const record = $state.records[index];
|
||||
const hooksWithoutDeletedHook = record.hooks.filter(
|
||||
hook => hook.id !== hookId
|
||||
);
|
||||
Vue.set(record, 'hooks', hooksWithoutDeletedHook);
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export default {
|
||||
|
|
|
@ -9,6 +9,11 @@ describe('#getters', () => {
|
|||
expect(getters.getInboxes(state)).toEqual(inboxList);
|
||||
});
|
||||
|
||||
it('getWebsiteInboxes', () => {
|
||||
const state = { records: inboxList };
|
||||
expect(getters.getWebsiteInboxes(state).length).toEqual(3);
|
||||
});
|
||||
|
||||
it('getInbox', () => {
|
||||
const state = {
|
||||
records: inboxList,
|
||||
|
|
|
@ -1,29 +1,30 @@
|
|||
import axios from 'axios';
|
||||
import { actions } from '../../integrations';
|
||||
import * as types from '../../../mutation-types';
|
||||
import types from '../../../mutation-types';
|
||||
import integrationsList from './fixtures';
|
||||
|
||||
const commit = jest.fn();
|
||||
global.axios = axios;
|
||||
jest.mock('axios');
|
||||
|
||||
const errorMessage = { message: 'Incorrect header' };
|
||||
describe('#actions', () => {
|
||||
describe('#get', () => {
|
||||
it('sends correct actions if API is success', async () => {
|
||||
axios.get.mockResolvedValue({ data: integrationsList });
|
||||
await actions.get({ commit });
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.default.SET_INTEGRATIONS_UI_FLAG, { isFetching: true }],
|
||||
[types.default.SET_INTEGRATIONS, integrationsList.payload],
|
||||
[types.default.SET_INTEGRATIONS_UI_FLAG, { isFetching: false }],
|
||||
[types.SET_INTEGRATIONS_UI_FLAG, { isFetching: true }],
|
||||
[types.SET_INTEGRATIONS, integrationsList.payload],
|
||||
[types.SET_INTEGRATIONS_UI_FLAG, { isFetching: false }],
|
||||
]);
|
||||
});
|
||||
it('sends correct actions if API is error', async () => {
|
||||
axios.get.mockRejectedValue({ message: 'Incorrect header' });
|
||||
axios.get.mockRejectedValue(errorMessage);
|
||||
await actions.get({ commit });
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.default.SET_INTEGRATIONS_UI_FLAG, { isFetching: true }],
|
||||
[types.default.SET_INTEGRATIONS_UI_FLAG, { isFetching: false }],
|
||||
[types.SET_INTEGRATIONS_UI_FLAG, { isFetching: true }],
|
||||
[types.SET_INTEGRATIONS_UI_FLAG, { isFetching: false }],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
@ -34,17 +35,17 @@ describe('#actions', () => {
|
|||
axios.post.mockResolvedValue({ data: data });
|
||||
await actions.connectSlack({ commit });
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.default.SET_INTEGRATIONS_UI_FLAG, { isUpdating: true }],
|
||||
[types.default.ADD_INTEGRATION, data],
|
||||
[types.default.SET_INTEGRATIONS_UI_FLAG, { isUpdating: false }],
|
||||
[types.SET_INTEGRATIONS_UI_FLAG, { isUpdating: true }],
|
||||
[types.ADD_INTEGRATION, data],
|
||||
[types.SET_INTEGRATIONS_UI_FLAG, { isUpdating: false }],
|
||||
]);
|
||||
});
|
||||
it('sends correct actions if API is error', async () => {
|
||||
axios.post.mockRejectedValue({ message: 'Incorrect header' });
|
||||
axios.post.mockRejectedValue(errorMessage);
|
||||
await actions.connectSlack({ commit });
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.default.SET_INTEGRATIONS_UI_FLAG, { isUpdating: true }],
|
||||
[types.default.SET_INTEGRATIONS_UI_FLAG, { isUpdating: false }],
|
||||
[types.SET_INTEGRATIONS_UI_FLAG, { isUpdating: true }],
|
||||
[types.SET_INTEGRATIONS_UI_FLAG, { isUpdating: false }],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
@ -55,17 +56,59 @@ describe('#actions', () => {
|
|||
axios.delete.mockResolvedValue({ data: data });
|
||||
await actions.deleteIntegration({ commit }, data.id);
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.default.SET_INTEGRATIONS_UI_FLAG, { isDeleting: true }],
|
||||
[types.default.DELETE_INTEGRATION, data],
|
||||
[types.default.SET_INTEGRATIONS_UI_FLAG, { isDeleting: false }],
|
||||
[types.SET_INTEGRATIONS_UI_FLAG, { isDeleting: true }],
|
||||
[types.DELETE_INTEGRATION, data],
|
||||
[types.SET_INTEGRATIONS_UI_FLAG, { isDeleting: false }],
|
||||
]);
|
||||
});
|
||||
it('sends correct actions if API is error', async () => {
|
||||
axios.delete.mockRejectedValue({ message: 'Incorrect header' });
|
||||
axios.delete.mockRejectedValue(errorMessage);
|
||||
await actions.deleteIntegration({ commit });
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.default.SET_INTEGRATIONS_UI_FLAG, { isDeleting: true }],
|
||||
[types.default.SET_INTEGRATIONS_UI_FLAG, { isDeleting: false }],
|
||||
[types.SET_INTEGRATIONS_UI_FLAG, { isDeleting: true }],
|
||||
[types.SET_INTEGRATIONS_UI_FLAG, { isDeleting: false }],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#createHooks', () => {
|
||||
it('sends correct actions if API is success', async () => {
|
||||
let data = { id: 'slack', enabled: false };
|
||||
axios.post.mockResolvedValue({ data: data });
|
||||
await actions.createHook({ commit }, data);
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.SET_INTEGRATIONS_UI_FLAG, { isCreatingHook: true }],
|
||||
[types.ADD_INTEGRATION_HOOKS, data],
|
||||
[types.SET_INTEGRATIONS_UI_FLAG, { isCreatingHook: false }],
|
||||
]);
|
||||
});
|
||||
it('sends correct actions if API is error', async () => {
|
||||
axios.post.mockRejectedValue(errorMessage);
|
||||
await expect(actions.createHook({ commit })).rejects.toThrow(Error);
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.SET_INTEGRATIONS_UI_FLAG, { isCreatingHook: true }],
|
||||
[types.SET_INTEGRATIONS_UI_FLAG, { isCreatingHook: false }],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#deleteHook', () => {
|
||||
it('sends correct actions if API is success', async () => {
|
||||
let data = { appId: 'dialogflow', hookId: 2 };
|
||||
axios.delete.mockResolvedValue({ data });
|
||||
await actions.deleteHook({ commit }, data);
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.SET_INTEGRATIONS_UI_FLAG, { isDeletingHook: true }],
|
||||
[types.DELETE_INTEGRATION_HOOKS, { appId: 'dialogflow', hookId: 2 }],
|
||||
[types.SET_INTEGRATIONS_UI_FLAG, { isDeletingHook: false }],
|
||||
]);
|
||||
});
|
||||
it('sends correct actions if API is error', async () => {
|
||||
axios.delete.mockRejectedValue(errorMessage);
|
||||
await expect(actions.deleteHook({ commit }, {})).rejects.toThrow(Error);
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.SET_INTEGRATIONS_UI_FLAG, { isDeletingHook: true }],
|
||||
[types.SET_INTEGRATIONS_UI_FLAG, { isDeletingHook: false }],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -34,6 +34,33 @@ describe('#getters', () => {
|
|||
]);
|
||||
});
|
||||
|
||||
it('getAppIntegrations', () => {
|
||||
const state = {
|
||||
records: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'test1',
|
||||
logo: 'test',
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
id: 'dialogflow',
|
||||
name: 'test2',
|
||||
logo: 'test',
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
expect(getters.getAppIntegrations(state)).toEqual([
|
||||
{
|
||||
id: 'dialogflow',
|
||||
name: 'test2',
|
||||
logo: 'test',
|
||||
enabled: true,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('getUIFlags', () => {
|
||||
const state = {
|
||||
uiFlags: {
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import * as types from '../../../mutation-types';
|
||||
import types from '../../../mutation-types';
|
||||
import { mutations } from '../../integrations';
|
||||
|
||||
describe('#mutations', () => {
|
||||
describe('#GET_INTEGRATIONS', () => {
|
||||
it('set integrations records', () => {
|
||||
const state = { records: [] };
|
||||
mutations[types.default.SET_INTEGRATIONS](state, [
|
||||
mutations[types.SET_INTEGRATIONS](state, [
|
||||
{
|
||||
id: 1,
|
||||
name: 'test1',
|
||||
|
@ -23,4 +23,59 @@ describe('#mutations', () => {
|
|||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#ADD_INTEGRATION_HOOKS', () => {
|
||||
it('set integrations hook records', () => {
|
||||
const state = { records: [{ id: 'dialogflow', hooks: [] }] };
|
||||
const hookRecord = {
|
||||
id: 1,
|
||||
app_id: 'dialogflow',
|
||||
status: false,
|
||||
inbox: { id: 1, name: 'Chatwoot' },
|
||||
account_id: 1,
|
||||
hook_type: 'inbox',
|
||||
settings: { project_id: 'test', credentials: {} },
|
||||
};
|
||||
mutations[types.ADD_INTEGRATION_HOOKS](state, hookRecord);
|
||||
expect(state.records).toEqual([
|
||||
{
|
||||
id: 'dialogflow',
|
||||
hooks: [hookRecord],
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#DELETE_INTEGRATION_HOOKS', () => {
|
||||
it('delete integrations hook record', () => {
|
||||
const state = {
|
||||
records: [
|
||||
{
|
||||
id: 'dialogflow',
|
||||
hooks: [
|
||||
{
|
||||
id: 1,
|
||||
app_id: 'dialogflow',
|
||||
status: false,
|
||||
inbox: { id: 1, name: 'Chatwoot' },
|
||||
account_id: 1,
|
||||
hook_type: 'inbox',
|
||||
settings: { project_id: 'test', credentials: {} },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
mutations[types.DELETE_INTEGRATION_HOOKS](state, {
|
||||
appId: 'dialogflow',
|
||||
hookId: 1,
|
||||
});
|
||||
expect(state.records).toEqual([
|
||||
{
|
||||
id: 'dialogflow',
|
||||
hooks: [],
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -83,6 +83,8 @@ export default {
|
|||
SET_INTEGRATIONS: 'SET_INTEGRATIONS',
|
||||
ADD_INTEGRATION: 'ADD_INTEGRATION',
|
||||
DELETE_INTEGRATION: 'DELETE_INTEGRATION',
|
||||
ADD_INTEGRATION_HOOKS: 'ADD_INTEGRATION_HOOKS',
|
||||
DELETE_INTEGRATION_HOOKS: 'DELETE_INTEGRATION_HOOKS',
|
||||
|
||||
// WebHook
|
||||
SET_WEBHOOK_UI_FLAG: 'SET_WEBHOOK_UI_FLAG',
|
||||
|
|
|
@ -10,6 +10,7 @@ import axios from 'axios';
|
|||
// Global Components
|
||||
import hljs from 'highlight.js';
|
||||
import Multiselect from 'vue-multiselect';
|
||||
import VueFormulate from '@braid/vue-formulate';
|
||||
import WootSwitch from 'components/ui/Switch';
|
||||
import WootWizard from 'components/ui/Wizard';
|
||||
import { sync } from 'vuex-router-sync';
|
||||
|
@ -19,7 +20,7 @@ import WootUiKit from '../dashboard/components';
|
|||
import App from '../dashboard/App';
|
||||
import i18n from '../dashboard/i18n';
|
||||
import createAxios from '../dashboard/helper/APIHelper';
|
||||
import commonHelpers from '../dashboard/helper/commons';
|
||||
import commonHelpers, { isJSONValid } from '../dashboard/helper/commons';
|
||||
import { getAlertAudio } from '../shared/helpers/AudioNotificationHelper';
|
||||
import { initFaviconSwitcher } from '../shared/helpers/faviconHelper';
|
||||
import router from '../dashboard/routes';
|
||||
|
@ -48,6 +49,11 @@ Vue.use(VueRouter);
|
|||
Vue.use(VueI18n);
|
||||
Vue.use(WootUiKit);
|
||||
Vue.use(Vuelidate);
|
||||
Vue.use(VueFormulate, {
|
||||
rules: {
|
||||
JSON: ({ value }) => isJSONValid(value),
|
||||
},
|
||||
});
|
||||
Vue.use(VTooltip, {
|
||||
defaultHtml: false,
|
||||
});
|
||||
|
|
|
@ -38,8 +38,8 @@ class Integrations::App
|
|||
case params[:id]
|
||||
when 'slack'
|
||||
ENV['SLACK_CLIENT_SECRET'].present?
|
||||
when 'dialogflow'
|
||||
false
|
||||
when 'dialogflow', 'fullcontact'
|
||||
true
|
||||
else
|
||||
true
|
||||
end
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
json.id resource.id
|
||||
json.app_id resource.app_id
|
||||
json.status resource.enabled?
|
||||
json.inbox_id resource.inbox_id
|
||||
json.inbox resource.inbox&.slice(:id, :name)
|
||||
json.account_id resource.account_id
|
||||
json.hook_type resource.hook_type
|
||||
json.settings resource.settings
|
||||
|
|
|
@ -43,11 +43,38 @@ dialogflow:
|
|||
{
|
||||
"label": "Dialogflow Project ID",
|
||||
"type": "text",
|
||||
"name": "project_id"
|
||||
"name": "project_id",
|
||||
"validation": "required",
|
||||
"validationName": 'Project Id',
|
||||
},
|
||||
{
|
||||
"label": "Dialogflow Project Key File",
|
||||
"type": "textarea",
|
||||
"name": "credentials",
|
||||
"validation": "required|JSON",
|
||||
"validationName": 'Credentials',
|
||||
"validation-messages": {
|
||||
"JSON": "Invalid JSON",
|
||||
"required": "Credentials is required"
|
||||
}
|
||||
}
|
||||
]
|
||||
visible_properties: ['project_id']
|
||||
fullcontact:
|
||||
id: fullcontact
|
||||
logo: fullcontact.png
|
||||
i18n_key: fullcontact
|
||||
action: /fullcontact
|
||||
hook_type: account
|
||||
allow_multiple_hooks: false
|
||||
settings_json_schema:
|
||||
{
|
||||
'type': 'object',
|
||||
'properties': { 'api_key': { 'type': 'string' } },
|
||||
'required': ['api_key'],
|
||||
'additionalProperties': false,
|
||||
}
|
||||
settings_form_schema:
|
||||
[{ 'label': 'API Key', 'type': 'text', 'name': 'api_key',"validation": "required", }]
|
||||
visible_properties: ['api_key']
|
||||
|
||||
|
|
|
@ -95,4 +95,7 @@ en:
|
|||
description: "Webhook events provide you the realtime information about what's happening in your 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."
|
||||
dialogflow:
|
||||
name: "Dialogflow"
|
||||
description: "Connect your Dialogflow bot to your inbox. Let the bots handle the queries before handing it off to the customer service agent."
|
||||
description: "Build chatbots using Dialogflow and connect them to your inbox quickly. Let the bots handle the queries before handing them off to a customer service agent."
|
||||
fullcontact:
|
||||
name: "Fullcontact"
|
||||
description: "FullContact integration helps to enrich visitor profiles. Identify the users as soon as they share their email address and offer them tailored customer service. Connect your FullContact to your account by sharing the FullContact API Key."
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
"build-storybook": "build-storybook"
|
||||
},
|
||||
"dependencies": {
|
||||
"@braid/vue-formulate": "^2.5.2",
|
||||
"@chatwoot/prosemirror-schema": "https://github.com/chatwoot/prosemirror-schema.git#7e8acadd10d7b932c0dc0bd0a18f804434f83517",
|
||||
"@chatwoot/utils": "^0.0.3",
|
||||
"@rails/actioncable": "6.1.3",
|
||||
|
|
BIN
public/dashboard/images/integrations/fullcontact.png
Normal file
BIN
public/dashboard/images/integrations/fullcontact.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 7 KiB |
27
yarn.lock
27
yarn.lock
|
@ -1081,6 +1081,21 @@
|
|||
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
|
||||
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
|
||||
|
||||
"@braid/vue-formulate-i18n@^1.16.0":
|
||||
version "1.16.0"
|
||||
resolved "https://registry.yarnpkg.com/@braid/vue-formulate-i18n/-/vue-formulate-i18n-1.16.0.tgz#71c9121c909c7bbdfd4dfcf6a57d554f5bb1ec75"
|
||||
integrity sha512-CuX1kN7bWg4ukynnTS3LGsmPKrUvS2Wh75zKatIe+nHQQy0gSvfmggQsCz4QZey7Ois+pGYmOIgrE1SvX+zQTw==
|
||||
|
||||
"@braid/vue-formulate@^2.5.2":
|
||||
version "2.5.2"
|
||||
resolved "https://registry.yarnpkg.com/@braid/vue-formulate/-/vue-formulate-2.5.2.tgz#32076d6a513760763a3a89dd69e8911c1bad1fcb"
|
||||
integrity sha512-0LWKp3Rjq2a+WwA4y49VaXqQOVClJGy6mmdivxJJyRvNB0aUcTOKOINLumeU/XHf7/c0HNhtbZih2vs6hMDyvQ==
|
||||
dependencies:
|
||||
"@braid/vue-formulate-i18n" "^1.16.0"
|
||||
is-plain-object "^3.0.1"
|
||||
is-url "^1.2.4"
|
||||
nanoid "^2.1.11"
|
||||
|
||||
"@chatwoot/prosemirror-schema@https://github.com/chatwoot/prosemirror-schema.git#7e8acadd10d7b932c0dc0bd0a18f804434f83517":
|
||||
version "1.0.0"
|
||||
resolved "https://github.com/chatwoot/prosemirror-schema.git#7e8acadd10d7b932c0dc0bd0a18f804434f83517"
|
||||
|
@ -8366,7 +8381,7 @@ is-plain-obj@^2.0.0:
|
|||
resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287"
|
||||
integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==
|
||||
|
||||
is-plain-object@3.0.1:
|
||||
is-plain-object@3.0.1, is-plain-object@^3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-3.0.1.tgz#662d92d24c0aa4302407b0d45d21f2251c85f85b"
|
||||
integrity sha512-Xnpx182SBMrr/aBik8y+GuR4U1L9FqMSojwDQwPMmxyC6bvEqly9UBCxhauBF5vNh2gwWJNX6oDV7O+OM4z34g==
|
||||
|
@ -8448,6 +8463,11 @@ is-unicode-supported@^0.1.0:
|
|||
resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7"
|
||||
integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==
|
||||
|
||||
is-url@^1.2.4:
|
||||
version "1.2.4"
|
||||
resolved "https://registry.yarnpkg.com/is-url/-/is-url-1.2.4.tgz#04a4df46d28c4cff3d73d01ff06abeb318a1aa52"
|
||||
integrity sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==
|
||||
|
||||
is-whitespace-character@^1.0.0:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/is-whitespace-character/-/is-whitespace-character-1.0.4.tgz#0858edd94a95594c7c9dd0b5c174ec6e45ee4aa7"
|
||||
|
@ -10116,6 +10136,11 @@ nan@^2.12.1:
|
|||
resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.2.tgz#f5376400695168f4cc694ac9393d0c9585eeea19"
|
||||
integrity sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==
|
||||
|
||||
nanoid@^2.1.11:
|
||||
version "2.1.11"
|
||||
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-2.1.11.tgz#ec24b8a758d591561531b4176a01e3ab4f0f0280"
|
||||
integrity sha512-s/snB+WGm6uwi0WjsZdaVcuf3KJXlfGl2LcxgwkEwJF0D/BWzVWAZW/XY4bFaiR7s0Jk3FPvlnepg1H1b1UwlA==
|
||||
|
||||
nanoid@^3.1.22:
|
||||
version "3.1.22"
|
||||
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.22.tgz#b35f8fb7d151990a8aebd5aa5015c03cf726f844"
|
||||
|
|
Loading…
Reference in a new issue