feat: Add Integration hooks UI (#2301)

This commit is contained in:
Muhsin Keloth 2021-06-06 16:59:05 +05:30 committed by GitHub
parent c6487877bf
commit 14b51e108a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
35 changed files with 1108 additions and 31 deletions

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -0,0 +1,10 @@
export default {
computed: {
isHookTypeInbox() {
return this.integration.hook_type === 'inbox';
},
hasConnectedHooks() {
return !!this.integration.hooks.length;
},
},
};

View file

@ -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,
}),
},
],
},
],
};

View file

@ -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);
});
});

View file

@ -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,
],
};