Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
76ec732640 |
@@ -29,6 +29,13 @@ class IntegrationsAPI extends ApiClient {
|
||||
return axios.post(`${this.baseUrl()}/integrations/hooks`, hookData);
|
||||
}
|
||||
|
||||
updateHook(hookId, hookData) {
|
||||
return axios.patch(
|
||||
`${this.baseUrl()}/integrations/hooks/${hookId}`,
|
||||
hookData
|
||||
);
|
||||
}
|
||||
|
||||
deleteHook(hookId) {
|
||||
return axios.delete(`${this.baseUrl()}/integrations/hooks/${hookId}`);
|
||||
}
|
||||
|
||||
@@ -132,6 +132,22 @@
|
||||
"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."
|
||||
},
|
||||
"INTEGRATION_MODE": {
|
||||
"TITLE": "Integration mode",
|
||||
"DESCRIPTION": "Choose how your team uses the connected Slack channel.",
|
||||
"TWO_WAY": {
|
||||
"LABEL": "Two-way communication",
|
||||
"DESCRIPTION": "Replies in Slack threads are sent to the customer. Start a message with 'note:' to keep it private."
|
||||
},
|
||||
"ALERTS_ONLY": {
|
||||
"LABEL": "Alerts only",
|
||||
"DESCRIPTION": "Conversations are synced to Slack for internal discussion, but messages sent in Slack threads never reach the customer. Replies to customers are sent from the dashboard."
|
||||
},
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Integration mode updated successfully",
|
||||
"ERROR_MESSAGE": "There was an error updating the integration mode, please try again"
|
||||
}
|
||||
},
|
||||
"HELP_TEXT": {
|
||||
"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.",
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useRouter, useRoute } from 'vue-router';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import Integration from './Integration.vue';
|
||||
import SelectChannelWarning from './Slack/SelectChannelWarning.vue';
|
||||
import SlackIntegrationMode from './Slack/SlackIntegrationMode.vue';
|
||||
import SlackIntegrationHelpText from './Slack/SlackIntegrationHelpText.vue';
|
||||
import SettingsLayout from '../SettingsLayout.vue';
|
||||
import BaseSettingsHeader from '../components/BaseSettingsHeader.vue';
|
||||
@@ -103,11 +104,12 @@ onMounted(() => {
|
||||
),
|
||||
}"
|
||||
/>
|
||||
<div v-if="areHooksAvailable" class="flex-1">
|
||||
<div v-if="areHooksAvailable" class="flex-1 space-y-5">
|
||||
<SelectChannelWarning
|
||||
v-if="!isIntegrationHookEnabled"
|
||||
:has-connected-a-channel="hasConnectedAChannel"
|
||||
/>
|
||||
<SlackIntegrationMode v-if="isIntegrationHookEnabled" :hook="hook" />
|
||||
<SlackIntegrationHelpText
|
||||
:selected-channel-name="selectedChannelName"
|
||||
/>
|
||||
|
||||
+1
-1
@@ -61,7 +61,7 @@ const updateIntegration = async () => {
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="px-6 py-4 mb-4 outline outline-n-container outline-1 bg-n-card rounded-xl"
|
||||
class="px-6 py-4 outline outline-n-container outline-1 bg-n-card rounded-xl"
|
||||
>
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { useStore } from 'vuex';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
|
||||
const props = defineProps({
|
||||
hook: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const store = useStore();
|
||||
const { t } = useI18n();
|
||||
|
||||
const modeOptions = computed(() => [
|
||||
{
|
||||
value: 'two_way',
|
||||
label: t('INTEGRATION_SETTINGS.SLACK.INTEGRATION_MODE.TWO_WAY.LABEL'),
|
||||
description: t(
|
||||
'INTEGRATION_SETTINGS.SLACK.INTEGRATION_MODE.TWO_WAY.DESCRIPTION'
|
||||
),
|
||||
},
|
||||
{
|
||||
value: 'alerts_only',
|
||||
label: t('INTEGRATION_SETTINGS.SLACK.INTEGRATION_MODE.ALERTS_ONLY.LABEL'),
|
||||
description: t(
|
||||
'INTEGRATION_SETTINGS.SLACK.INTEGRATION_MODE.ALERTS_ONLY.DESCRIPTION'
|
||||
),
|
||||
},
|
||||
]);
|
||||
|
||||
const selectedMode = computed(
|
||||
() => props.hook.settings?.integration_mode || 'two_way'
|
||||
);
|
||||
|
||||
const updateMode = async mode => {
|
||||
if (mode === selectedMode.value) return;
|
||||
try {
|
||||
await store.dispatch('integrations/updateHook', {
|
||||
hookId: props.hook.id,
|
||||
settings: { ...props.hook.settings, integration_mode: mode },
|
||||
});
|
||||
useAlert(
|
||||
t('INTEGRATION_SETTINGS.SLACK.INTEGRATION_MODE.API.SUCCESS_MESSAGE')
|
||||
);
|
||||
} catch (error) {
|
||||
useAlert(
|
||||
t('INTEGRATION_SETTINGS.SLACK.INTEGRATION_MODE.API.ERROR_MESSAGE')
|
||||
);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="px-6 py-5 outline outline-n-container outline-1 bg-n-card rounded-xl"
|
||||
>
|
||||
<h5 class="mb-1 text-n-slate-12 text-heading-2">
|
||||
{{ t('INTEGRATION_SETTINGS.SLACK.INTEGRATION_MODE.TITLE') }}
|
||||
</h5>
|
||||
<p class="text-n-slate-11 text-body-main">
|
||||
{{ t('INTEGRATION_SETTINGS.SLACK.INTEGRATION_MODE.DESCRIPTION') }}
|
||||
</p>
|
||||
<div class="flex flex-col gap-3 mt-4">
|
||||
<label
|
||||
v-for="option in modeOptions"
|
||||
:key="option.value"
|
||||
class="flex items-start gap-3 cursor-pointer"
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="slack-integration-mode"
|
||||
:value="option.value"
|
||||
:checked="selectedMode === option.value"
|
||||
class="mt-1"
|
||||
@change="updateMode(option.value)"
|
||||
/>
|
||||
<div>
|
||||
<span class="text-n-slate-12 text-body-main font-medium">
|
||||
{{ option.label }}
|
||||
</span>
|
||||
<p class="mb-0 text-n-slate-11 text-body-main">
|
||||
{{ option.description }}
|
||||
</p>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -12,6 +12,7 @@ const state = {
|
||||
isFetchingItem: false,
|
||||
isUpdating: false,
|
||||
isCreatingHook: false,
|
||||
isUpdatingHook: false,
|
||||
isDeletingHook: false,
|
||||
isCreatingSlack: false,
|
||||
isUpdatingSlack: false,
|
||||
@@ -124,6 +125,17 @@ export const actions = {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
updateHook: async ({ commit }, { hookId, ...hookData }) => {
|
||||
commit(types.default.SET_INTEGRATIONS_UI_FLAG, { isUpdatingHook: true });
|
||||
try {
|
||||
const response = await IntegrationsAPI.updateHook(hookId, hookData);
|
||||
commit(types.default.UPDATE_INTEGRATION_HOOKS, response.data);
|
||||
commit(types.default.SET_INTEGRATIONS_UI_FLAG, { isUpdatingHook: false });
|
||||
} catch (error) {
|
||||
commit(types.default.SET_INTEGRATIONS_UI_FLAG, { isUpdatingHook: false });
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
deleteHook: async ({ commit }, { appId, hookId }) => {
|
||||
commit(types.default.SET_INTEGRATIONS_UI_FLAG, { isDeletingHook: true });
|
||||
try {
|
||||
@@ -155,6 +167,17 @@ export const mutations = {
|
||||
return record;
|
||||
});
|
||||
},
|
||||
[types.default.UPDATE_INTEGRATION_HOOKS]: ($state, data) => {
|
||||
$state.records = $state.records.map(record => {
|
||||
if (record.id === data.app_id) {
|
||||
return {
|
||||
...record,
|
||||
hooks: record.hooks.map(hook => (hook.id === data.id ? data : hook)),
|
||||
};
|
||||
}
|
||||
return record;
|
||||
});
|
||||
},
|
||||
[types.default.DELETE_INTEGRATION_HOOKS]: ($state, { appId, hookId }) => {
|
||||
$state.records = $state.records.map(record => {
|
||||
if (record.id === appId) {
|
||||
|
||||
@@ -126,6 +126,7 @@ export default {
|
||||
ADD_INTEGRATION: 'ADD_INTEGRATION',
|
||||
DELETE_INTEGRATION: 'DELETE_INTEGRATION',
|
||||
ADD_INTEGRATION_HOOKS: 'ADD_INTEGRATION_HOOKS',
|
||||
UPDATE_INTEGRATION_HOOKS: 'UPDATE_INTEGRATION_HOOKS',
|
||||
DELETE_INTEGRATION_HOOKS: 'DELETE_INTEGRATION_HOOKS',
|
||||
|
||||
// WebHook
|
||||
|
||||
@@ -79,7 +79,7 @@ slack:
|
||||
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,files:read,files:write
|
||||
hook_type: account
|
||||
allow_multiple_hooks: false
|
||||
visible_properties: ['channel_name']
|
||||
visible_properties: ['channel_name', 'integration_mode']
|
||||
dialogflow:
|
||||
id: dialogflow
|
||||
logo: dialogflow.png
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
class BackfillIntegrationModeOnSlackHooks < ActiveRecord::Migration[7.1]
|
||||
def up
|
||||
Integrations::Hook.where(app_id: 'slack').find_each do |hook|
|
||||
next if hook.settings&.key?('integration_mode')
|
||||
|
||||
# update_column skips validations like ensure_feature_enabled, which would
|
||||
# fail for accounts that disabled the slack_integration feature after connecting.
|
||||
hook.update_column(:settings, (hook.settings || {}).merge('integration_mode' => 'two_way')) # rubocop:disable Rails/SkipsModelValidations
|
||||
end
|
||||
end
|
||||
|
||||
def down
|
||||
# no-op: a missing integration_mode already behaves as two_way
|
||||
end
|
||||
end
|
||||
+1
-1
@@ -10,7 +10,7 @@
|
||||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[7.1].define(version: 2026_07_13_184351) do
|
||||
ActiveRecord::Schema[7.1].define(version: 2026_07_22_000000) do
|
||||
# These extensions should be enabled to support this database
|
||||
enable_extension "pg_stat_statements"
|
||||
enable_extension "pg_trgm"
|
||||
|
||||
@@ -64,7 +64,7 @@ class Integrations::Slack::ChannelBuilder
|
||||
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.update!(reference_id: channel[:id], settings: hook.settings.merge('channel_name' => channel[:name]), status: 'enabled')
|
||||
@hook
|
||||
end
|
||||
end
|
||||
|
||||
@@ -68,7 +68,13 @@ class Integrations::Slack::IncomingMessageBuilder
|
||||
end
|
||||
|
||||
def process_message_payload?
|
||||
thread_timestamp_available? && supported_message? && integration_hook
|
||||
thread_timestamp_available? && supported_message? && integration_hook && !alerts_only_mode?
|
||||
end
|
||||
|
||||
# In alerts_only mode, the Slack channel is a notification/internal discussion
|
||||
# channel — thread replies are never synced back to the conversation.
|
||||
def alerts_only_mode?
|
||||
integration_hook.settings['integration_mode'] == 'alerts_only'
|
||||
end
|
||||
|
||||
def link_shared?
|
||||
|
||||
Reference in New Issue
Block a user