feat: Add Integration hooks UI (#2301)
This commit is contained in:
parent
c6487877bf
commit
14b51e108a
35 changed files with 1108 additions and 31 deletions
|
@ -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,
|
||||
],
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue