mirror of
https://github.com/lingble/chatwoot.git
synced 2025-10-29 18:22:53 +00:00
feat: Update the slack integration-flow to allow users to select the channel (#7637)
This commit is contained in:
@@ -1,27 +1,27 @@
|
||||
class Api::V1::Accounts::Integrations::SlackController < Api::V1::Accounts::BaseController
|
||||
before_action :check_admin_authorization?
|
||||
before_action :fetch_hook, only: [:update, :destroy]
|
||||
before_action :fetch_hook, only: [:update, :destroy, :list_all_channels]
|
||||
|
||||
def list_all_channels
|
||||
@channels = channel_builder.fetch_channels
|
||||
end
|
||||
|
||||
def create
|
||||
ActiveRecord::Base.transaction do
|
||||
builder = Integrations::Slack::HookBuilder.new(
|
||||
account: Current.account,
|
||||
code: params[:code],
|
||||
inbox_id: params[:inbox_id]
|
||||
)
|
||||
@hook = builder.perform
|
||||
create_chatwoot_slack_channel
|
||||
end
|
||||
hook_builder = Integrations::Slack::HookBuilder.new(
|
||||
account: Current.account,
|
||||
code: params[:code],
|
||||
inbox_id: params[:inbox_id]
|
||||
)
|
||||
@hook = hook_builder.perform
|
||||
end
|
||||
|
||||
def update
|
||||
create_chatwoot_slack_channel
|
||||
render json: @hook
|
||||
@hook = channel_builder.update(permitted_params[:reference_id])
|
||||
render json: { error: I18n.t('errors.slack.invalid_channel_id') }, status: :unprocessable_entity if @hook.blank?
|
||||
end
|
||||
|
||||
def destroy
|
||||
@hook.destroy!
|
||||
|
||||
head :ok
|
||||
end
|
||||
|
||||
@@ -31,11 +31,11 @@ class Api::V1::Accounts::Integrations::SlackController < Api::V1::Accounts::Base
|
||||
@hook = Integrations::Hook.where(account: Current.account).find_by(app_id: 'slack')
|
||||
end
|
||||
|
||||
def create_chatwoot_slack_channel
|
||||
channel = params[:channel] || 'customer-conversations'
|
||||
builder = Integrations::Slack::ChannelBuilder.new(
|
||||
hook: @hook, channel: channel
|
||||
)
|
||||
builder.perform
|
||||
def channel_builder
|
||||
Integrations::Slack::ChannelBuilder.new(hook: @hook)
|
||||
end
|
||||
|
||||
def permitted_params
|
||||
params.permit(:reference_id)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -8,11 +8,19 @@ class IntegrationsAPI extends ApiClient {
|
||||
}
|
||||
|
||||
connectSlack(code) {
|
||||
return axios.post(`${this.baseUrl()}/integrations/slack`, {
|
||||
code: code,
|
||||
return axios.post(`${this.baseUrl()}/integrations/slack`, { code });
|
||||
}
|
||||
|
||||
updateSlack({ referenceId }) {
|
||||
return axios.patch(`${this.baseUrl()}/integrations/slack`, {
|
||||
reference_id: referenceId,
|
||||
});
|
||||
}
|
||||
|
||||
listAllSlackChannels() {
|
||||
return axios.get(`${this.baseUrl()}/integrations/slack/list_all_channels`);
|
||||
}
|
||||
|
||||
delete(integrationId) {
|
||||
return axios.delete(`${this.baseUrl()}/integrations/${integrationId}`);
|
||||
}
|
||||
|
||||
@@ -11,7 +11,9 @@ describe('#integrationAPI', () => {
|
||||
expect(integrationAPI).toHaveProperty('update');
|
||||
expect(integrationAPI).toHaveProperty('delete');
|
||||
expect(integrationAPI).toHaveProperty('connectSlack');
|
||||
expect(integrationAPI).toHaveProperty('createHook');
|
||||
expect(integrationAPI).toHaveProperty('updateSlack');
|
||||
expect(integrationAPI).toHaveProperty('updateSlack');
|
||||
expect(integrationAPI).toHaveProperty('listAllSlackChannels');
|
||||
expect(integrationAPI).toHaveProperty('deleteHook');
|
||||
});
|
||||
describeWithAPIMock('API calls', context => {
|
||||
@@ -26,6 +28,24 @@ describe('#integrationAPI', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('#updateSlack', () => {
|
||||
const updateObj = { referenceId: 'SDFSDGSVE' };
|
||||
integrationAPI.updateSlack(updateObj);
|
||||
expect(context.axiosMock.patch).toHaveBeenCalledWith(
|
||||
'/api/v1/integrations/slack',
|
||||
{
|
||||
reference_id: updateObj.referenceId,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('#listAllSlackChannels', () => {
|
||||
integrationAPI.listAllSlackChannels();
|
||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||
'/api/v1/integrations/slack/list_all_channels'
|
||||
);
|
||||
});
|
||||
|
||||
it('#delete', () => {
|
||||
integrationAPI.delete(2);
|
||||
expect(context.axiosMock.delete).toHaveBeenCalledWith(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div
|
||||
class="conversations-list-wrap flex-shrink-0 flex-basis-custom overflow-hidden flex flex-col border-r rtl:border-r-0 rtl:border-l border-slate-50 dark:border-slate-800/50"
|
||||
class="conversations-list-wrap flex-basis-clamp flex-shrink-0 flex-basis-custom overflow-hidden flex flex-col border-r rtl:border-r-0 rtl:border-l border-slate-50 dark:border-slate-800/50"
|
||||
:class="{
|
||||
hide: !showConversationList,
|
||||
'list--full-width': isOnExpandedLayout,
|
||||
@@ -969,8 +969,6 @@ export default {
|
||||
|
||||
<style scoped lang="scss">
|
||||
.conversations-list-wrap {
|
||||
@apply flex-basis-clamp;
|
||||
|
||||
&.hide {
|
||||
@apply hidden;
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
>
|
||||
<fluent-icon v-if="!!iconClass" :icon="iconClass" class="icon" />
|
||||
<span>{{ buttonText }}</span>
|
||||
<spinner v-if="loading" />
|
||||
<spinner v-if="loading" class="ml-2" :color-scheme="spinnerClass" />
|
||||
</button>
|
||||
</template>
|
||||
|
||||
@@ -40,6 +40,10 @@ export default {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
spinnerClass: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: 'submit',
|
||||
|
||||
@@ -29,6 +29,7 @@ const settings = accountId => ({
|
||||
'settings_inboxes_page_channel',
|
||||
'settings_integrations_dashboard_apps',
|
||||
'settings_integrations_integration',
|
||||
'settings_integrations_slack',
|
||||
'settings_integrations_webhook',
|
||||
'settings_integrations',
|
||||
'settings_teams_add_agents',
|
||||
|
||||
@@ -70,10 +70,26 @@
|
||||
}
|
||||
},
|
||||
"SLACK": {
|
||||
"DELETE": "Delete",
|
||||
"DELETE_CONFIRMATION": {
|
||||
"TITLE": "Delete the integration",
|
||||
"MESSAGE": "Are you sure you want to delete the integration? Doing so will result in the loss of access to conversations on your Slack workspace."
|
||||
},
|
||||
"HELP_TEXT": {
|
||||
"TITLE": "Using Slack Integration",
|
||||
"BODY": "<br/><p>Chatwoot will now sync all the incoming conversations into the <b><i>customer-conversations</i></b> channel inside your slack workplace.</p><p>Replying to a conversation thread in <b><i>customer-conversations</i></b> slack channel will create a response back to the customer through chatwoot.</p><p>Start the replies with <b><i>note:</i></b> to create private notes instead of replies.</p><p>If the replier on slack has an agent profile in chatwoot under the same email, the replies will be associated accordingly.</p><p>When the replier doesn't have an associated agent profile, the replies will be made from the bot profile.</p>"
|
||||
}
|
||||
"TITLE": "How to use the Slack Integration?",
|
||||
"BODY": "With this integration, all of your incoming conversations will be synced to the ***%{selectedChannelName}*** channel in your Slack workspace. You can manage all your customer conversations right within the channel and never miss a message.\n\nHere are the main features of the integration:\n\n**Respond to conversations from within Slack:** To respond to a conversation in the ***%{selectedChannelName}*** Slack channel, simply type out your message and send it as a thread. This will create a response back to the customer through Chatwoot. It's that simple!\n\n **Create private notes:** If you want to create private notes instead of replies, start your message with ***`note:`***. This ensures that your message is kept private and won't be visible to the customer.\n\n**Associate an agent profile:** If the person who replied on Slack has an agent profile in Chatwoot under the same email, the replies will be associated with that agent profile automatically. This means you can easily track who said what and when. On the other hand, when the replier doesn't have an associated agent profile, the replies will appear from the bot profile to the customer.",
|
||||
"SELECTED": "selected"
|
||||
},
|
||||
"SELECT_CHANNEL": {
|
||||
"OPTION_LABEL": "Select a channel",
|
||||
"UPDATE": "Update",
|
||||
"BUTTON_TEXT": "Connect channel",
|
||||
"DESCRIPTION": "Your Slack workspace is now linked with Chatwoot. However, the integration is currently inactive. To activate the integration and connect a channel to Chatwoot, please click the button below.\n\n**Note:** If you are attempting to connect a private channel, add the Chatwoot app to the Slack channel before proceeding with this step.",
|
||||
"ATTENTION_REQUIRED": "Attention required"
|
||||
},
|
||||
"UPDATE_ERROR": "There was an error updating the integration, please try again",
|
||||
"UPDATE_SUCCESS": "The channel is connected successfully",
|
||||
"FAILED_TO_FETCH_CHANNELS": "There was an error fetching the channels from Slack, please try again"
|
||||
},
|
||||
"DYTE": {
|
||||
"CLICK_HERE_TO_JOIN": "Click here to join",
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
<template>
|
||||
<div class="flex">
|
||||
<div class="flex h-[6.25rem] w-[6.25rem]">
|
||||
<div class="flex flex-col md:flex-row items-start md:items-center">
|
||||
<div class="flex items-center justify-center m-0 mx-4 flex-1">
|
||||
<img
|
||||
:src="'/dashboard/images/integrations/' + integrationLogo"
|
||||
class="max-w-full p-6"
|
||||
class="p-2 h-16 w-16 mr-4"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col justify-center m-0 mx-4 flex-1">
|
||||
<h3 class="text-xl text-slate-800 dark:text-slate-100">
|
||||
{{ integrationName }}
|
||||
</h3>
|
||||
<p>
|
||||
{{
|
||||
useInstallationName(
|
||||
integrationDescription,
|
||||
globalConfig.installationName
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
<div>
|
||||
<h3 class="text-xl text-slate-800 dark:text-slate-100">
|
||||
{{ integrationName }}
|
||||
</h3>
|
||||
<p>
|
||||
{{
|
||||
useInstallationName(
|
||||
integrationDescription,
|
||||
globalConfig.installationName
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-center items-center mb-0 w-[15%]">
|
||||
<router-link
|
||||
@@ -29,13 +29,13 @@
|
||||
>
|
||||
<div v-if="integrationEnabled">
|
||||
<div v-if="integrationAction === 'disconnect'">
|
||||
<div @click="openDeletePopup()">
|
||||
<div @click="openDeletePopup">
|
||||
<woot-submit-button
|
||||
:button-text="
|
||||
$t('INTEGRATION_SETTINGS.WEBHOOK.DELETE.BUTTON_TEXT')
|
||||
actionButtonText ||
|
||||
$t('INTEGRATION_SETTINGS.WEBHOOK.DELETE.BUTTON_TEXT')
|
||||
"
|
||||
icon-class="dismiss-circle"
|
||||
button-class="nice alert"
|
||||
button-class="smooth alert"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -56,8 +56,14 @@
|
||||
: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')"
|
||||
:title="
|
||||
deleteConfirmationText.title ||
|
||||
$t('INTEGRATION_SETTINGS.WEBHOOK.DELETE.CONFIRM.TITLE')
|
||||
"
|
||||
:message="
|
||||
deleteConfirmationText.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')"
|
||||
/>
|
||||
@@ -81,6 +87,8 @@ export default {
|
||||
integrationDescription: { type: String, default: '' },
|
||||
integrationEnabled: { type: Boolean, default: false },
|
||||
integrationAction: { type: String, default: '' },
|
||||
actionButtonText: { type: String, default: '' },
|
||||
deleteConfirmationText: { type: Object, default: () => ({}) },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
||||
@@ -65,7 +65,7 @@ export default {
|
||||
}),
|
||||
},
|
||||
mounted() {
|
||||
this.intializeSlackIntegration();
|
||||
this.fetchIntegrations();
|
||||
},
|
||||
methods: {
|
||||
integrationAction() {
|
||||
@@ -74,13 +74,8 @@ export default {
|
||||
}
|
||||
return this.integration.action;
|
||||
},
|
||||
async intializeSlackIntegration() {
|
||||
async fetchIntegrations() {
|
||||
await this.$store.dispatch('integrations/get', this.integrationId);
|
||||
if (this.code) {
|
||||
await this.$store.dispatch('integrations/connectSlack', this.code);
|
||||
// we are clearing code from the path as subsequent request would throw error
|
||||
this.$router.replace(this.$route.path);
|
||||
}
|
||||
this.integrationLoaded = true;
|
||||
},
|
||||
},
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="integrationLoaded && !uiFlags.isCreatingSlack"
|
||||
class="flex flex-col flex-1 overflow-auto"
|
||||
>
|
||||
<div
|
||||
class="bg-white dark:bg-slate-800 border-b border-solid border-slate-75 dark:border-slate-700/50 rounded-sm p-4"
|
||||
>
|
||||
<integration
|
||||
:integration-id="integration.id"
|
||||
:integration-logo="integration.logo"
|
||||
:integration-name="integration.name"
|
||||
:integration-description="integration.description"
|
||||
:integration-enabled="integration.enabled"
|
||||
:integration-action="integrationAction"
|
||||
:action-button-text="$t('INTEGRATION_SETTINGS.SLACK.DELETE')"
|
||||
:delete-confirmation-text="{
|
||||
title: $t('INTEGRATION_SETTINGS.SLACK.DELETE_CONFIRMATION.TITLE'),
|
||||
message: $t('INTEGRATION_SETTINGS.SLACK.DELETE_CONFIRMATION.MESSAGE'),
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="areHooksAvailable" class="p-6 flex-1">
|
||||
<select-channel-warning v-if="!isIntegrationHookEnabled" />
|
||||
<slack-integration-help-text
|
||||
:selected-channel-name="selectedChannelName"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="flex flex-1 items-center justify-center">
|
||||
<spinner size="" color-scheme="primary" />
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import globalConfigMixin from 'shared/mixins/globalConfigMixin';
|
||||
import Integration from './Integration.vue';
|
||||
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||
import SelectChannelWarning from './Slack/SelectChannelWarning.vue';
|
||||
import SlackIntegrationHelpText from './Slack/SlackIntegrationHelpText.vue';
|
||||
import Spinner from 'shared/components/Spinner';
|
||||
export default {
|
||||
components: {
|
||||
Spinner,
|
||||
Integration,
|
||||
SelectChannelWarning,
|
||||
SlackIntegrationHelpText,
|
||||
},
|
||||
mixins: [globalConfigMixin, messageFormatterMixin],
|
||||
props: {
|
||||
code: { type: String, default: '' },
|
||||
},
|
||||
data() {
|
||||
return { integrationLoaded: false };
|
||||
},
|
||||
computed: {
|
||||
integration() {
|
||||
return this.$store.getters['integrations/getIntegration']('slack');
|
||||
},
|
||||
areHooksAvailable() {
|
||||
const { hooks = [] } = this.integration || {};
|
||||
return !!hooks.length;
|
||||
},
|
||||
isIntegrationHookEnabled() {
|
||||
const { hooks = [] } = this.integration || {};
|
||||
const [hook = {}] = hooks;
|
||||
return hook.status || false;
|
||||
},
|
||||
selectedChannelName() {
|
||||
const { hooks = [] } = this.integration || {};
|
||||
const [hook = {}] = hooks;
|
||||
if (hook.status) {
|
||||
const { settings: { channel_name: channelName = '' } = {} } = hook;
|
||||
return channelName || 'customer-conversations';
|
||||
}
|
||||
return this.$t('INTEGRATION_SETTINGS.SLACK.HELP_TEXT.SELECTED');
|
||||
},
|
||||
...mapGetters({
|
||||
currentUser: 'getCurrentUser',
|
||||
globalConfig: 'globalConfig/get',
|
||||
accountId: 'getCurrentAccountId',
|
||||
uiFlags: 'integrations/getUIFlags',
|
||||
}),
|
||||
|
||||
integrationAction() {
|
||||
if (this.integration.enabled) {
|
||||
return 'disconnect';
|
||||
}
|
||||
return this.integration.action;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.intializeSlackIntegration();
|
||||
},
|
||||
methods: {
|
||||
async intializeSlackIntegration() {
|
||||
await this.$store.dispatch('integrations/get', 'slack');
|
||||
if (this.code) {
|
||||
await this.$store.dispatch('integrations/connectSlack', this.code);
|
||||
// Clear the query param `code` from the URL as the
|
||||
// subsequent reloads would result in an error
|
||||
this.$router.replace(this.$route.path);
|
||||
}
|
||||
this.integrationLoaded = true;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,117 @@
|
||||
<template>
|
||||
<div
|
||||
class="rounded-md bg-yellow-50 border border-yellow-200 dark:border-slate-700 dark:bg-slate-800 px-6 py-4 mb-4"
|
||||
>
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0 mt-0.5">
|
||||
<fluent-icon
|
||||
icon="alert"
|
||||
class="text-yellow-500 dark:text-yellow-400"
|
||||
size="24"
|
||||
/>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<p
|
||||
class="text-base font-semibold text-yellow-900 dark:text-yellow-500 mb-1"
|
||||
>
|
||||
{{
|
||||
$t('INTEGRATION_SETTINGS.SLACK.SELECT_CHANNEL.ATTENTION_REQUIRED')
|
||||
}}
|
||||
</p>
|
||||
<div class="text-sm text-yellow-800 dark:text-yellow-600 mt-2">
|
||||
<p
|
||||
v-dompurify-html="
|
||||
formatMessage(
|
||||
useInstallationName(
|
||||
$t('INTEGRATION_SETTINGS.SLACK.SELECT_CHANNEL.DESCRIPTION'),
|
||||
globalConfig.installationName
|
||||
),
|
||||
false
|
||||
)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-8 mt-2">
|
||||
<woot-submit-button
|
||||
v-if="!availableChannels.length"
|
||||
button-class="smooth small warning"
|
||||
:loading="uiFlags.isFetchingSlackChannels"
|
||||
:button-text="
|
||||
$t('INTEGRATION_SETTINGS.SLACK.SELECT_CHANNEL.BUTTON_TEXT')
|
||||
"
|
||||
spinner-class="warning"
|
||||
@click="fetchChannels"
|
||||
/>
|
||||
<div v-else class="inline-flex">
|
||||
<select
|
||||
v-model="selectedChannelId"
|
||||
class="h-8 border-yellow-300 border mr-4 text-xs leading-4 py-1"
|
||||
>
|
||||
<option value="">
|
||||
{{ $t('INTEGRATION_SETTINGS.SLACK.SELECT_CHANNEL.OPTION_LABEL') }}
|
||||
</option>
|
||||
<option
|
||||
v-for="channel in availableChannels"
|
||||
:key="channel.id"
|
||||
:value="channel.id"
|
||||
>
|
||||
#{{ channel.name }}
|
||||
</option>
|
||||
</select>
|
||||
<woot-submit-button
|
||||
button-class="smooth small success"
|
||||
:button-text="$t('INTEGRATION_SETTINGS.SLACK.SELECT_CHANNEL.UPDATE')"
|
||||
spinner-class="success"
|
||||
:loading="uiFlags.isUpdatingSlack"
|
||||
@click="updateIntegration"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import globalConfigMixin from 'shared/mixins/globalConfigMixin';
|
||||
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
|
||||
export default {
|
||||
mixins: [alertMixin, globalConfigMixin, messageFormatterMixin],
|
||||
data() {
|
||||
return { selectedChannelId: '', availableChannels: [] };
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
globalConfig: 'globalConfig/get',
|
||||
uiFlags: 'integrations/getUIFlags',
|
||||
}),
|
||||
},
|
||||
methods: {
|
||||
async fetchChannels() {
|
||||
try {
|
||||
this.availableChannels = await this.$store.dispatch(
|
||||
'integrations/listAllSlackChannels'
|
||||
);
|
||||
this.availableChannels.sort((c1, c2) => c1.name - c2.name);
|
||||
} catch {
|
||||
this.$t('INTEGRATION_SETTINGS.SLACK.FAILED_TO_FETCH_CHANNELS');
|
||||
this.availableChannels = [];
|
||||
}
|
||||
},
|
||||
async updateIntegration() {
|
||||
try {
|
||||
await this.$store.dispatch('integrations/updateSlack', {
|
||||
referenceId: this.selectedChannelId,
|
||||
});
|
||||
this.showAlert(this.$t('INTEGRATION_SETTINGS.SLACK.UPDATE_SUCCESS'));
|
||||
} catch (error) {
|
||||
this.showAlert(
|
||||
error.message || 'INTEGRATION_SETTINGS.SLACK.UPDATE_ERROR'
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,35 @@
|
||||
<template>
|
||||
<div
|
||||
class="flex-1 w-full p-6 bg-white rounded-md border border-slate-50 dark:border-slate-700 dark:bg-slate-800 dark:text-slate-200 "
|
||||
>
|
||||
<div class="prose-lg max-w-5xl">
|
||||
<h5 class="dark:text-slate-100">
|
||||
{{ $t('INTEGRATION_SETTINGS.SLACK.HELP_TEXT.TITLE') }}
|
||||
</h5>
|
||||
<p>
|
||||
<span
|
||||
v-dompurify-html="
|
||||
formatMessage(
|
||||
$t('INTEGRATION_SETTINGS.SLACK.HELP_TEXT.BODY', {
|
||||
selectedChannelName: selectedChannelName,
|
||||
}),
|
||||
false
|
||||
)
|
||||
"
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||
export default {
|
||||
mixins: [messageFormatterMixin],
|
||||
props: {
|
||||
selectedChannelName: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -3,6 +3,7 @@ import SettingsContent from '../Wrapper';
|
||||
import Webhook from './Webhooks/Index';
|
||||
import DashboardApps from './DashboardApps/Index';
|
||||
import ShowIntegration from './ShowIntegration';
|
||||
import Slack from './Slack';
|
||||
import { frontendURL } from '../../../../helper/URLHelper';
|
||||
|
||||
export default {
|
||||
@@ -42,6 +43,13 @@ export default {
|
||||
name: 'settings_integrations_dashboard_apps',
|
||||
roles: ['administrator'],
|
||||
},
|
||||
{
|
||||
path: 'slack',
|
||||
name: 'settings_integrations_slack',
|
||||
component: Slack,
|
||||
roles: ['administrator'],
|
||||
props: route => ({ code: route.query.code }),
|
||||
},
|
||||
{
|
||||
path: ':integration_id',
|
||||
name: 'settings_integrations_integration',
|
||||
|
||||
@@ -3,6 +3,7 @@ import Vue from 'vue';
|
||||
import * as MutationHelpers from 'shared/helpers/vuex/mutationHelpers';
|
||||
import * as types from '../mutation-types';
|
||||
import IntegrationsAPI from '../../api/integrations';
|
||||
import { throwErrorMessage } from 'dashboard/store/utils/api';
|
||||
|
||||
const state = {
|
||||
records: [],
|
||||
@@ -13,6 +14,9 @@ const state = {
|
||||
isUpdating: false,
|
||||
isCreatingHook: false,
|
||||
isDeletingHook: false,
|
||||
isCreatingSlack: false,
|
||||
isUpdatingSlack: false,
|
||||
isFetchingSlackChannels: false,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -52,15 +56,45 @@ export const actions = {
|
||||
},
|
||||
|
||||
connectSlack: async ({ commit }, code) => {
|
||||
commit(types.default.SET_INTEGRATIONS_UI_FLAG, { isUpdating: true });
|
||||
commit(types.default.SET_INTEGRATIONS_UI_FLAG, { isCreatingSlack: true });
|
||||
try {
|
||||
const response = await IntegrationsAPI.connectSlack(code);
|
||||
commit(types.default.ADD_INTEGRATION, response.data);
|
||||
commit(types.default.SET_INTEGRATIONS_UI_FLAG, { isUpdating: false });
|
||||
} catch (error) {
|
||||
commit(types.default.SET_INTEGRATIONS_UI_FLAG, { isUpdating: false });
|
||||
throwErrorMessage(error);
|
||||
} finally {
|
||||
commit(types.default.SET_INTEGRATIONS_UI_FLAG, {
|
||||
isCreatingSlack: false,
|
||||
});
|
||||
}
|
||||
},
|
||||
updateSlack: async ({ commit }, slackObj) => {
|
||||
commit(types.default.SET_INTEGRATIONS_UI_FLAG, { isUpdatingSlack: true });
|
||||
try {
|
||||
const response = await IntegrationsAPI.updateSlack(slackObj);
|
||||
commit(types.default.ADD_INTEGRATION, response.data);
|
||||
} catch (error) {
|
||||
throwErrorMessage(error);
|
||||
} finally {
|
||||
commit(types.default.SET_INTEGRATIONS_UI_FLAG, {
|
||||
isUpdatingSlack: false,
|
||||
});
|
||||
}
|
||||
},
|
||||
listAllSlackChannels: async ({ commit }) => {
|
||||
commit(types.default.SET_INTEGRATIONS_UI_FLAG, {
|
||||
isFetchingSlackChannels: true,
|
||||
});
|
||||
try {
|
||||
const response = await IntegrationsAPI.listAllSlackChannels();
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
commit(types.default.SET_INTEGRATIONS_UI_FLAG, {
|
||||
isFetchingSlackChannels: false,
|
||||
});
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
deleteIntegration: async ({ commit }, integrationId) => {
|
||||
commit(types.default.SET_INTEGRATIONS_UI_FLAG, { isDeleting: true });
|
||||
@@ -75,6 +109,17 @@ export const actions = {
|
||||
commit(types.default.SET_INTEGRATIONS_UI_FLAG, { isDeleting: false });
|
||||
}
|
||||
},
|
||||
showHook: async ({ commit }, hookId) => {
|
||||
commit(types.default.SET_INTEGRATIONS_UI_FLAG, { isFetchingItem: true });
|
||||
try {
|
||||
const response = await IntegrationsAPI.showHook(hookId);
|
||||
commit(types.default.ADD_INTEGRATION_HOOKS, response.data);
|
||||
commit(types.default.SET_INTEGRATIONS_UI_FLAG, { isFetchingItem: false });
|
||||
} catch (error) {
|
||||
commit(types.default.SET_INTEGRATIONS_UI_FLAG, { isFetchingItem: false });
|
||||
throw new Error(error);
|
||||
}
|
||||
},
|
||||
createHook: async ({ commit }, hookData) => {
|
||||
commit(types.default.SET_INTEGRATIONS_UI_FLAG, { isCreatingHook: true });
|
||||
try {
|
||||
|
||||
@@ -31,21 +31,42 @@ describe('#actions', () => {
|
||||
|
||||
describe('#connectSlack:', () => {
|
||||
it('sends correct actions if API is success', async () => {
|
||||
let data = { id: 'slack', enabled: true };
|
||||
let data = { hooks: [{ id: 'slack', enabled: false }] };
|
||||
axios.post.mockResolvedValue({ data: data });
|
||||
await actions.connectSlack({ commit });
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.SET_INTEGRATIONS_UI_FLAG, { isUpdating: true }],
|
||||
[types.SET_INTEGRATIONS_UI_FLAG, { isCreatingSlack: true }],
|
||||
[types.ADD_INTEGRATION, data],
|
||||
[types.SET_INTEGRATIONS_UI_FLAG, { isUpdating: false }],
|
||||
[types.SET_INTEGRATIONS_UI_FLAG, { isCreatingSlack: false }],
|
||||
]);
|
||||
});
|
||||
it('sends correct actions if API is error', async () => {
|
||||
axios.post.mockRejectedValue(errorMessage);
|
||||
await actions.connectSlack({ commit });
|
||||
await expect(actions.connectSlack({ commit })).rejects.toThrow(Error);
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.SET_INTEGRATIONS_UI_FLAG, { isUpdating: true }],
|
||||
[types.SET_INTEGRATIONS_UI_FLAG, { isUpdating: false }],
|
||||
[types.SET_INTEGRATIONS_UI_FLAG, { isCreatingSlack: true }],
|
||||
[types.SET_INTEGRATIONS_UI_FLAG, { isCreatingSlack: false }],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#updateSlack', () => {
|
||||
it('sends correct actions if API is success', async () => {
|
||||
let data = { hooks: [{ id: 'slack', enabled: false }] };
|
||||
axios.patch.mockResolvedValue({ data: data });
|
||||
await actions.updateSlack({ commit }, { referenceId: '12345' });
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.SET_INTEGRATIONS_UI_FLAG, { isUpdatingSlack: true }],
|
||||
[types.ADD_INTEGRATION, data],
|
||||
[types.SET_INTEGRATIONS_UI_FLAG, { isUpdatingSlack: false }],
|
||||
]);
|
||||
});
|
||||
it('sends correct actions if API is error', async () => {
|
||||
axios.patch.mockRejectedValue(errorMessage);
|
||||
await expect(actions.updateSlack({ commit })).rejects.toThrow(Error);
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.SET_INTEGRATIONS_UI_FLAG, { isUpdatingSlack: true }],
|
||||
[types.SET_INTEGRATIONS_UI_FLAG, { isUpdatingSlack: false }],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,6 +19,14 @@ export default {
|
||||
return 'before:!border-t-woot-500';
|
||||
}
|
||||
|
||||
if (this.colorScheme === 'warning') {
|
||||
return 'before:!border-t-yellow-500';
|
||||
}
|
||||
|
||||
if (this.colorScheme === 'success') {
|
||||
return 'before:!border-t-success-500';
|
||||
}
|
||||
|
||||
return this.colorScheme;
|
||||
},
|
||||
},
|
||||
|
||||
@@ -2,6 +2,8 @@ class HookJob < ApplicationJob
|
||||
queue_as :medium
|
||||
|
||||
def perform(hook, event_name, event_data = {})
|
||||
return if hook.disabled?
|
||||
|
||||
case hook.app_id
|
||||
when 'slack'
|
||||
process_slack_integration(hook, event_name, event_data)
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
json.id @hook.app_id
|
||||
json.enabled true
|
||||
json.partial! 'api/v1/models/app', formats: [:json], resource: @hook.app
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
json.array! @channels do |channel|
|
||||
json.id channel['id']
|
||||
json.name channel['name']
|
||||
end
|
||||
@@ -0,0 +1 @@
|
||||
json.partial! 'api/v1/models/app', formats: [:json], resource: @hook.app
|
||||
@@ -13,7 +13,7 @@ slack:
|
||||
id: slack
|
||||
logo: slack.png
|
||||
i18n_key: slack
|
||||
action: https://slack.com/oauth/v2/authorize?scope=commands,chat:write,channels:read,channels:manage,channels:join,groups:write,im:write,mpim:write,users:read,users:read.email,chat:write.customize,channels:history,groups:history,mpim:history,im:history
|
||||
action: https://slack.com/oauth/v2/authorize?scope=commands,chat:write,channels:read,channels:manage,channels:join,groups:read,groups:write,im:write,mpim:write,users:read,users:read.email,chat:write.customize,channels:history,groups:history,mpim:history,im:history
|
||||
hook_type: account
|
||||
allow_multiple_hooks: false
|
||||
webhooks:
|
||||
|
||||
@@ -63,6 +63,8 @@ en:
|
||||
unique: should be unique in the category and portal
|
||||
dyte:
|
||||
invalid_message_type: "Invalid message type. Action not permitted"
|
||||
slack:
|
||||
invalid_channel_id: "Invalid slack channel. Please try again"
|
||||
inboxes:
|
||||
imap:
|
||||
socket_error: Please check the network connection, IMAP address and try again.
|
||||
|
||||
@@ -190,12 +190,16 @@ Rails.application.routes.draw do
|
||||
resources :webhooks, only: [:index, :create, :update, :destroy]
|
||||
namespace :integrations do
|
||||
resources :apps, only: [:index, :show]
|
||||
resources :hooks, only: [:create, :update, :destroy] do
|
||||
resources :hooks, only: [:show, :create, :update, :destroy] do
|
||||
member do
|
||||
post :process_event
|
||||
end
|
||||
end
|
||||
resource :slack, only: [:create, :update, :destroy], controller: 'slack'
|
||||
resource :slack, only: [:create, :update, :destroy], controller: 'slack' do
|
||||
member do
|
||||
get :list_all_channels
|
||||
end
|
||||
end
|
||||
resource :dyte, controller: 'dyte', only: [] do
|
||||
collection do
|
||||
post :create_a_meeting
|
||||
|
||||
@@ -5,9 +5,12 @@ class Integrations::Slack::ChannelBuilder
|
||||
@params = params
|
||||
end
|
||||
|
||||
def perform
|
||||
find_or_create_channel
|
||||
update_reference_id
|
||||
def fetch_channels
|
||||
channels
|
||||
end
|
||||
|
||||
def update(reference_id)
|
||||
update_reference_id(reference_id)
|
||||
end
|
||||
|
||||
private
|
||||
@@ -20,19 +23,26 @@ class Integrations::Slack::ChannelBuilder
|
||||
@slack_client ||= Slack::Web::Client.new(token: hook.access_token)
|
||||
end
|
||||
|
||||
def find_or_create_channel
|
||||
current_list = slack_client.conversations_list
|
||||
channels = current_list.channels
|
||||
while current_list.response_metadata.next_cursor.present?
|
||||
current_list = slack_client.conversations_list(cursor: current_list.response_metadata.next_cursor)
|
||||
channels.concat(current_list.channels)
|
||||
def channels
|
||||
conversations_list = slack_client.conversations_list(types: 'public_channel, private_channel')
|
||||
channel_list = conversations_list.channels
|
||||
while conversations_list.response_metadata.next_cursor.present?
|
||||
conversations_list = slack_client.conversations_list(cursor: conversations_list.response_metadata.next_cursor)
|
||||
channel_list.concat(conversations_list.channel_list)
|
||||
end
|
||||
existing_channel = channels.find { |channel| channel['name'] == params[:channel] }
|
||||
@channel = existing_channel || slack_client.conversations_create(name: params[:channel])['channel']
|
||||
channel_list
|
||||
end
|
||||
|
||||
def update_reference_id
|
||||
slack_client.conversations_join(channel: channel[:id])
|
||||
@hook.update(reference_id: channel[:id])
|
||||
def find_channel(reference_id)
|
||||
channels.find { |channel| channel['id'] == reference_id }
|
||||
end
|
||||
|
||||
def update_reference_id(reference_id)
|
||||
channel = find_channel(reference_id)
|
||||
return if channel.blank?
|
||||
|
||||
slack_client.conversations_join(channel: channel[:id]) if channel[:is_private] == false
|
||||
@hook.update!(reference_id: channel[:id], settings: { channel_name: channel[:name] }, status: 'enabled')
|
||||
@hook
|
||||
end
|
||||
end
|
||||
|
||||
@@ -10,7 +10,7 @@ class Integrations::Slack::HookBuilder
|
||||
|
||||
hook = account.hooks.new(
|
||||
access_token: token,
|
||||
status: 'enabled',
|
||||
status: 'disabled',
|
||||
inbox_id: params[:inbox_id],
|
||||
app_id: 'slack'
|
||||
)
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"eslint": "eslint app/**/*.{js,vue}",
|
||||
"eslint:fix": "eslint app/**/*.{js,vue} --fix",
|
||||
"pretest": "rimraf .jest-cache",
|
||||
"test": "jest -w 1 --no-cache",
|
||||
"test": "jest -w 1 --no-cache --no-coverage",
|
||||
"test:watch": "jest -w 1 --watch --no-cache",
|
||||
"test:coverage": "jest -w 1 --no-cache --collectCoverage",
|
||||
"webpacker-start": "webpack-dev-server -d --config webpack.dev.config.js --content-base public/ --progress --colors",
|
||||
|
||||
@@ -15,6 +15,20 @@ RSpec.describe HookJob do
|
||||
.on_queue('medium')
|
||||
end
|
||||
|
||||
context 'when the hook is disabled' do
|
||||
it 'does not execute the job' do
|
||||
hook = create(:integrations_hook, status: 'disabled', account: account)
|
||||
allow(SendOnSlackJob).to receive(:perform_later)
|
||||
allow(Integrations::Dialogflow::ProcessorService).to receive(:new)
|
||||
allow(Integrations::GoogleTranslate::DetectLanguageService).to receive(:new)
|
||||
|
||||
expect(SendOnSlackJob).not_to receive(:perform_later)
|
||||
expect(Integrations::GoogleTranslate::DetectLanguageService).not_to receive(:new)
|
||||
expect(Integrations::Dialogflow::ProcessorService).not_to receive(:new)
|
||||
described_class.perform_now(hook, event_name, event_data)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when handleable events like message.created' do
|
||||
let(:process_service) { double }
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Api::V1::Accounts::Integrations::Slacks' do
|
||||
let(:account) { create(:account) }
|
||||
let(:agent) { create(:user, account: account, role: :administrator) }
|
||||
let(:admin) { create(:user, account: account, role: :administrator) }
|
||||
let!(:hook) { create(:integrations_hook, account: account) }
|
||||
|
||||
describe 'POST /api/v1/accounts/{account.id}/integrations/slack' do
|
||||
@@ -15,65 +15,99 @@ RSpec.describe 'Api::V1::Accounts::Integrations::Slacks' do
|
||||
|
||||
context 'when it is an authenticated user' do
|
||||
it 'creates hook' do
|
||||
hook_builder = Integrations::Slack::HookBuilder.new(account: account, code: SecureRandom.hex)
|
||||
hook_builder = double
|
||||
expect(hook_builder).to receive(:perform).and_return(hook)
|
||||
expect(Integrations::Slack::HookBuilder).to receive(:new).and_return(hook_builder)
|
||||
|
||||
channel_builder = Integrations::Slack::ChannelBuilder.new(hook: hook, channel: 'channel')
|
||||
expect(channel_builder).to receive(:perform)
|
||||
expect(Integrations::Slack::ChannelBuilder).to receive(:new).and_return(channel_builder)
|
||||
|
||||
post "/api/v1/accounts/#{account.id}/integrations/slack",
|
||||
params: { code: SecureRandom.hex },
|
||||
headers: agent.create_new_auth_token
|
||||
headers: admin.create_new_auth_token
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
json_response = response.parsed_body
|
||||
expect(json_response['id']).to eql('slack')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'PUT /api/v1/accounts/{account.id}/integrations/slack/' do
|
||||
context 'when it is an unauthenticated user' do
|
||||
it 'returns unauthorized' do
|
||||
put "/api/v1/accounts/#{account.id}/integrations/slack/", params: {}
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an authenticated user' do
|
||||
it 'updates hook' do
|
||||
channel_builder = Integrations::Slack::ChannelBuilder.new(hook: hook, channel: 'channel')
|
||||
expect(channel_builder).to receive(:perform)
|
||||
|
||||
expect(Integrations::Slack::ChannelBuilder).to receive(:new).and_return(channel_builder)
|
||||
|
||||
put "/api/v1/accounts/#{account.id}/integrations/slack",
|
||||
params: { channel: SecureRandom.hex },
|
||||
headers: agent.create_new_auth_token
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
json_response = response.parsed_body
|
||||
expect(json_response['app_id']).to eql('slack')
|
||||
end
|
||||
describe 'PUT /api/v1/accounts/{account.id}/integrations/slack/' do
|
||||
context 'when it is an unauthenticated user' do
|
||||
it 'returns unauthorized' do
|
||||
put "/api/v1/accounts/#{account.id}/integrations/slack/", params: {}
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'DELETE /api/v1/accounts/{account.id}/integrations/slack' do
|
||||
context 'when it is an unauthenticated user' do
|
||||
it 'returns unauthorized' do
|
||||
delete "/api/v1/accounts/#{account.id}/integrations/slack", params: {}
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
context 'when it is an authenticated user' do
|
||||
it 'updates hook if the channel id is correct' do
|
||||
channel_builder = double
|
||||
expect(channel_builder).to receive(:update).and_return(hook)
|
||||
expect(Integrations::Slack::ChannelBuilder).to receive(:new).and_return(channel_builder)
|
||||
|
||||
put "/api/v1/accounts/#{account.id}/integrations/slack",
|
||||
params: { channel: SecureRandom.hex },
|
||||
headers: admin.create_new_auth_token
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
json_response = response.parsed_body
|
||||
expect(json_response['hooks'][0]['id']).to eql(hook.id)
|
||||
end
|
||||
|
||||
context 'when it is an authenticated user' do
|
||||
it 'deletes hook' do
|
||||
delete "/api/v1/accounts/#{account.id}/integrations/slack",
|
||||
headers: agent.create_new_auth_token
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(Integrations::Hook.find_by(id: hook.id)).to be_nil
|
||||
end
|
||||
it 'does not update the hook if the channel id is not correct' do
|
||||
channel_builder = double
|
||||
expect(channel_builder).to receive(:update)
|
||||
expect(Integrations::Slack::ChannelBuilder).to receive(:new).and_return(channel_builder)
|
||||
|
||||
put "/api/v1/accounts/#{account.id}/integrations/slack",
|
||||
params: { channel: SecureRandom.hex },
|
||||
headers: admin.create_new_auth_token
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
json_response = response.parsed_body
|
||||
expect(json_response['error']).to eql('Invalid slack channel. Please try again')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET /api/v1/accounts/{account.id}/integrations/slack/list_all_channels' do
|
||||
context 'when it is an unauthenticated user' do
|
||||
it 'returns unauthorized' do
|
||||
get "/api/v1/accounts/#{account.id}/integrations/slack/list_all_channels", params: {}
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an authenticated user' do
|
||||
it 'updates hook if the channel id is correct' do
|
||||
channel_builder = double
|
||||
expect(channel_builder).to receive(:fetch_channels).and_return([{ 'id' => '1', 'name' => 'channel-1' }])
|
||||
expect(Integrations::Slack::ChannelBuilder).to receive(:new).and_return(channel_builder)
|
||||
|
||||
get "/api/v1/accounts/#{account.id}/integrations/slack/list_all_channels",
|
||||
params: { channel: SecureRandom.hex },
|
||||
headers: admin.create_new_auth_token
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
json_response = response.parsed_body
|
||||
expect(json_response).to eql([{ 'id' => '1', 'name' => 'channel-1' }])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'DELETE /api/v1/accounts/{account.id}/integrations/slack' do
|
||||
context 'when it is an unauthenticated user' do
|
||||
it 'returns unauthorized' do
|
||||
delete "/api/v1/accounts/#{account.id}/integrations/slack", params: {}
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an authenticated user' do
|
||||
it 'deletes hook' do
|
||||
delete "/api/v1/accounts/#{account.id}/integrations/slack",
|
||||
headers: admin.create_new_auth_token
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(Integrations::Hook.find_by(id: hook.id)).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user