Merge branch 'develop' into chore/improve-conversation-snooze

This commit is contained in:
Sivin Varghese
2026-02-20 20:31:24 +05:30
committed by GitHub
41 changed files with 228 additions and 98 deletions
+1 -1
View File
@@ -1 +1 @@
4.11.0
4.11.1
@@ -82,7 +82,13 @@ const attributeIcon = computed(() => {
sm
@click="emit('edit', attribute)"
/>
<Button icon="i-woot-bin" slate sm @click="emit('delete', attribute)" />
<Button
icon="i-woot-bin"
slate
sm
class="hover:enabled:text-n-ruby-11 hover:enabled:bg-n-ruby-2"
@click="emit('delete', attribute)"
/>
</div>
</div>
</div>
@@ -100,6 +100,9 @@ export default {
v-if="currentChat.id"
:chat="currentChat"
:show-back-button="isOnExpandedLayout && !isInboxView"
:class="{
'border-b border-b-n-weak !pt-2': !dashboardApps.length,
}"
/>
<woot-tabs
v-if="dashboardApps.length && currentChat.id"
@@ -182,6 +182,7 @@ onMounted(() => {
icon="i-woot-bin"
slate
sm
class="hover:enabled:text-n-ruby-11 hover:enabled:bg-n-ruby-2"
:is-loading="loading[bot.id]"
@click="openDeletePopup(bot)"
/>
@@ -268,6 +268,7 @@ const confirmDeletion = () => {
icon="i-woot-bin"
slate
sm
class="hover:enabled:text-n-ruby-11 hover:enabled:bg-n-ruby-2"
:is-loading="loading[agent.id]"
@click="openDeletePopup(agent, index)"
/>
@@ -70,14 +70,6 @@ const automationActive = computed({
:is-loading="loading"
@click="$emit('edit', automation)"
/>
<Button
v-tooltip.top="$t('AUTOMATION.FORM.DELETE')"
:is-loading="loading"
icon="i-woot-bin"
slate
sm
@click="$emit('delete', automation)"
/>
<Button
v-tooltip.top="$t('AUTOMATION.CLONE.TOOLTIP')"
icon="i-woot-clone"
@@ -86,6 +78,15 @@ const automationActive = computed({
:is-loading="loading"
@click="$emit('clone', automation)"
/>
<Button
v-tooltip.top="$t('AUTOMATION.FORM.DELETE')"
:is-loading="loading"
icon="i-woot-bin"
slate
sm
class="hover:enabled:text-n-ruby-11 hover:enabled:bg-n-ruby-2"
@click="$emit('delete', automation)"
/>
</div>
</BaseTableCell>
</template>
@@ -232,6 +232,7 @@ const tableHeaders = computed(() => {
icon="i-woot-bin"
slate
sm
class="hover:enabled:text-n-ruby-11 hover:enabled:bg-n-ruby-2"
:is-loading="loading[cannedItem.id]"
@click="openDeletePopup(cannedItem)"
/>
@@ -66,6 +66,7 @@ const getFormattedPermissions = role => {
icon="i-woot-bin"
slate
sm
class="hover:enabled:text-n-ruby-11 hover:enabled:bg-n-ruby-2"
:is-loading="loading[customRole.id]"
@click="emit('delete', customRole)"
/>
@@ -116,17 +116,15 @@ onMounted(() => {
</script>
<template>
<div class="w-full p-8 overflow-auto">
<div
class="grid max-w-3xl grid-cols-1 xs:grid-cols-2 mx-0 gap-6 sm:grid-cols-3"
>
<ChannelItem
v-for="channel in channelList"
:key="channel.key"
:channel="channel"
:enabled-features="enabledFeatures"
@channel-item-click="initChannelAuth"
/>
</div>
<div
class="grid max-w-3xl grid-cols-1 xs:grid-cols-2 mx-0 gap-6 sm:grid-cols-3 p-8"
>
<ChannelItem
v-for="channel in channelList"
:key="channel.key"
:channel="channel"
:enabled-features="enabledFeatures"
@channel-item-click="initChannelAuth"
/>
</div>
</template>
@@ -115,7 +115,7 @@ export default {
<woot-input
v-model="address"
:class="{ error: v$.address.$error }"
class="max-w-[75%] w-full"
class="w-full"
:label="$t('INBOX_MGMT.IMAP.ADDRESS.LABEL')"
:placeholder="$t('INBOX_MGMT.IMAP.ADDRESS.PLACE_HOLDER')"
@blur="v$.address.$touch"
@@ -124,7 +124,7 @@ export default {
v-model="port"
type="number"
:class="{ error: v$.port.$error }"
class="max-w-[75%] w-full"
class="w-full"
:label="$t('INBOX_MGMT.IMAP.PORT.LABEL')"
:placeholder="$t('INBOX_MGMT.IMAP.PORT.PLACE_HOLDER')"
@blur="v$.port.$touch"
@@ -132,7 +132,7 @@ export default {
<woot-input
v-model="login"
:class="{ error: v$.login.$error }"
class="max-w-[75%] w-full"
class="w-full"
:label="$t('INBOX_MGMT.IMAP.LOGIN.LABEL')"
:placeholder="$t('INBOX_MGMT.IMAP.LOGIN.PLACE_HOLDER')"
@blur="v$.login.$touch"
@@ -140,7 +140,7 @@ export default {
<woot-input
v-model="password"
:class="{ error: v$.password.$error }"
class="max-w-[75%] w-full"
class="w-full"
:label="$t('INBOX_MGMT.IMAP.PASSWORD.LABEL')"
:placeholder="$t('INBOX_MGMT.IMAP.PASSWORD.PLACE_HOLDER')"
type="password"
@@ -62,14 +62,14 @@ const items = computed(() => {
<div class="mx-auto flex flex-col gap-6 mb-8 max-w-7xl w-full !px-6">
<PageHeader class="block lg:hidden !mb-0" :header-title="pageTitle" />
<div
class="grid grid-cols-1 lg:grid-cols-8 lg:divide-x lg:divide-n-weak rounded-xl border border-n-weak min-h-[52rem]"
class="grid grid-cols-1 lg:grid-cols-8 lg:divide-x lg:divide-n-weak rounded-xl border border-n-weak h-full min-h-[50dvh]"
>
<woot-wizard
class="hidden lg:block col-span-2 h-fit py-8 px-6"
:global-config="globalConfig"
:items="items"
/>
<div class="col-span-6 overflow-hidden">
<div class="col-span-6 flex flex-col overflow-y-auto">
<router-view />
</div>
</div>
@@ -170,6 +170,7 @@ const openDelete = inbox => {
icon="i-woot-bin"
slate
sm
class="hover:enabled:text-n-ruby-11 hover:enabled:bg-n-ruby-2"
@click="openDelete(inbox)"
/>
</div>
@@ -214,6 +214,18 @@ export default {
const { medium, channel_type: type } = this.inbox;
return getInboxIconByType(type, medium, 'line');
},
bannerMaxWidth() {
const narrowTabs = [
'collaborators',
'configuration',
'bot-configuration',
];
if (narrowTabs.includes(this.selectedTabKey)) return 'max-w-4xl';
if (this.selectedTabKey === 'inbox-settings') {
return this.isAWebWidgetInbox ? 'max-w-7xl' : 'max-w-4xl';
}
return 'max-w-7xl';
},
inboxName() {
if (this.isATwilioSMSChannel || this.isATwilioWhatsAppChannel) {
return `${this.inbox.name} (${
@@ -571,44 +583,57 @@ export default {
v-if="microsoftUnauthorized"
:inbox="inbox"
class="mb-4"
:class="bannerMaxWidth"
/>
<FacebookReauthorize
v-if="facebookUnauthorized"
:inbox="inbox"
class="mb-4"
:class="bannerMaxWidth"
/>
<GoogleReauthorize
v-if="googleUnauthorized"
:inbox="inbox"
class="mb-4"
:class="bannerMaxWidth"
/>
<InstagramReauthorize
v-if="instagramUnauthorized"
:inbox="inbox"
class="mb-4"
:class="bannerMaxWidth"
/>
<TiktokReauthorize
v-if="tiktokUnauthorized"
:inbox="inbox"
class="mb-4"
:class="bannerMaxWidth"
/>
<WhatsappReauthorize
v-if="whatsappUnauthorized"
:whatsapp-registration-incomplete="whatsappRegistrationIncomplete"
:inbox="inbox"
class="mb-4"
:class="bannerMaxWidth"
/>
<DuplicateInboxBanner
v-if="hasDuplicateInstagramInbox"
:content="$t('INBOX_MGMT.ADD.INSTAGRAM.DUPLICATE_INBOX_BANNER')"
class="mx-6 mb-4"
:class="bannerMaxWidth"
/>
<div
v-if="selectedTabKey === 'inbox-settings'"
class="flex flex-col md:flex-row items-center lg:items-start justify-between gap-5 lg:gap-10 mx-6"
>
<div class="max-w-2xl flex-1 flex flex-col min-w-0">
<div
class="flex-1 flex flex-col min-w-0"
:class="{
'max-w-2xl': isAWebWidgetInbox,
'max-w-4xl': !isAWebWidgetInbox,
}"
>
<div class="flex flex-col gap-1 items-start mb-4">
<label class="text-heading-3 text-n-slate-12">
{{ $t('INBOX_MGMT.ADD.WEBSITE_CHANNEL.CHANNEL_AVATAR.LABEL') }}
@@ -755,6 +780,7 @@ export default {
<SenderNameExamplePreview
:sender-name-type="senderNameType"
:business-name="businessName"
:is-website-channel="isAWebWidgetInbox"
@update="toggleSenderNameType"
/>
</template>
@@ -1107,10 +1133,10 @@ export default {
</div>
</div>
<div v-if="selectedTabKey === 'collaborators'" class="mx-6 max-w-3xl">
<div v-if="selectedTabKey === 'collaborators'" class="mx-6 max-w-4xl">
<CollaboratorsPage :inbox="inbox" />
</div>
<div v-if="selectedTabKey === 'configuration'">
<div v-if="selectedTabKey === 'configuration'" class="mx-6 max-w-4xl">
<ConfigurationPage :inbox="inbox" />
</div>
<div v-if="selectedTabKey === 'csat'">
@@ -175,7 +175,7 @@ export default {
<woot-input
v-model="address"
:class="{ error: v$.address.$error }"
class="max-w-[75%] w-full"
class="w-full"
:label="$t('INBOX_MGMT.SMTP.ADDRESS.LABEL')"
:placeholder="$t('INBOX_MGMT.SMTP.ADDRESS.PLACE_HOLDER')"
@blur="v$.address.$touch"
@@ -184,7 +184,7 @@ export default {
v-model="port"
type="number"
:class="{ error: v$.port.$error }"
class="max-w-[75%] w-full"
class="w-full"
:label="$t('INBOX_MGMT.SMTP.PORT.LABEL')"
:placeholder="$t('INBOX_MGMT.SMTP.PORT.PLACE_HOLDER')"
@blur="v$.port.$touch"
@@ -192,7 +192,7 @@ export default {
<woot-input
v-model="login"
:class="{ error: v$.login.$error }"
class="max-w-[75%] w-full"
class="w-full"
:label="$t('INBOX_MGMT.SMTP.LOGIN.LABEL')"
:placeholder="$t('INBOX_MGMT.SMTP.LOGIN.PLACE_HOLDER')"
@blur="v$.login.$touch"
@@ -200,7 +200,7 @@ export default {
<woot-input
v-model="password"
:class="{ error: v$.password.$error }"
class="max-w-[75%] w-full"
class="w-full"
:label="$t('INBOX_MGMT.SMTP.PASSWORD.LABEL')"
:placeholder="$t('INBOX_MGMT.SMTP.PASSWORD.PLACE_HOLDER')"
type="password"
@@ -209,7 +209,7 @@ export default {
<woot-input
v-model="domain"
:class="{ error: v$.domain.$error }"
class="max-w-[75%] w-full"
class="w-full"
:label="$t('INBOX_MGMT.SMTP.DOMAIN.LABEL')"
:placeholder="$t('INBOX_MGMT.SMTP.DOMAIN.PLACE_HOLDER')"
@blur="v$.domain.$touch"
@@ -220,14 +220,14 @@ export default {
:action="handleEncryptionChange"
/>
<SingleSelectDropdown
class="max-w-[75%] w-full"
class="w-full"
:label="$t('INBOX_MGMT.SMTP.OPEN_SSL_VERIFY_MODE')"
:selected="openSSLVerifyMode"
:options="openSSLVerifyModes"
:action="handleSSLModeChange"
/>
<SingleSelectDropdown
class="max-w-[75%] w-full"
class="w-full"
:label="$t('INBOX_MGMT.SMTP.AUTH_MECHANISM')"
:selected="authMechanism"
:options="authMechanisms"
@@ -84,7 +84,7 @@ export default {
</script>
<template>
<div class="mx-6 max-w-3xl">
<div class="mx-6 max-w-4xl">
<LoadingState v-if="uiFlags.isFetching || uiFlags.isFetchingAgentBot" />
<form v-else @submit.prevent="updateActiveAgentBot">
<SettingsFieldSection
@@ -13,6 +13,10 @@ const props = defineProps({
type: String,
default: '',
},
isWebsiteChannel: {
type: Boolean,
default: false,
},
});
const emit = defineEmits(['update']);
@@ -56,7 +60,10 @@ const toggleSenderNameType = key => {
<template>
<div
class="flex flex-col sm:flex-row md:flex-col xl:flex-row items-start gap-4 mt-3 min-w-0"
class="flex flex-col items-start gap-4 mt-3 min-w-0"
:class="
isWebsiteChannel ? 'sm:flex-row md:flex-col xl:flex-row' : 'sm:flex-row'
"
>
<RadioCard
v-for="keyOption in senderNameKeyOptions"
@@ -149,7 +149,7 @@ export default {
</script>
<template>
<div v-if="isATwilioChannel" class="mx-6">
<div v-if="isATwilioChannel">
<SettingsFieldSection
:label="$t('INBOX_MGMT.ADD.TWILIO.API_CALLBACK.TITLE')"
:help-text="$t('INBOX_MGMT.ADD.TWILIO.API_CALLBACK.SUBTITLE')"
@@ -168,7 +168,7 @@ export default {
</NextButton>
</SettingsFieldSection>
</div>
<div v-else-if="isAVoiceChannel" class="mx-6">
<div v-else-if="isAVoiceChannel">
<SettingsFieldSection
:label="$t('INBOX_MGMT.ADD.VOICE.CONFIGURATION.TWILIO_VOICE_URL_TITLE')"
:help-text="
@@ -187,7 +187,7 @@ export default {
</SettingsFieldSection>
</div>
<div v-else-if="isALineChannel" class="mx-6">
<div v-else-if="isALineChannel">
<SettingsFieldSection
:label="$t('INBOX_MGMT.ADD.LINE_CHANNEL.API_CALLBACK.TITLE')"
:help-text="$t('INBOX_MGMT.ADD.LINE_CHANNEL.API_CALLBACK.SUBTITLE')"
@@ -196,7 +196,7 @@ export default {
</SettingsFieldSection>
</div>
<div v-else-if="isAWebWidgetInbox">
<div class="mx-6">
<div>
<SettingsFieldSection
:label="$t('INBOX_MGMT.SETTINGS_POPUP.ALLOWED_DOMAINS.TITLE')"
:help-text="$t('INBOX_MGMT.SETTINGS_POPUP.ALLOWED_DOMAINS.SUBTITLE')"
@@ -266,7 +266,7 @@ export default {
</SettingsFieldSection>
</div>
</div>
<div v-else-if="isAPIInbox" class="mx-6">
<div v-else-if="isAPIInbox">
<SettingsFieldSection
:label="$t('INBOX_MGMT.SETTINGS_POPUP.INBOX_IDENTIFIER')"
:help-text="$t('INBOX_MGMT.SETTINGS_POPUP.INBOX_IDENTIFIER_SUB_TEXT')"
@@ -297,7 +297,7 @@ export default {
</div>
</SettingsFieldSection>
</div>
<div v-else-if="isAnEmailChannel" class="mx-6">
<div v-else-if="isAnEmailChannel">
<div>
<SettingsFieldSection
:label="$t('INBOX_MGMT.SETTINGS_POPUP.FORWARD_EMAIL_TITLE')"
@@ -325,7 +325,7 @@ export default {
<SmtpSettings v-if="inbox.imap_enabled" :inbox="inbox" />
</div>
<div v-else-if="isAWhatsAppChannel && !isATwilioChannel">
<div v-if="inbox.provider_config" class="mx-6">
<div v-if="inbox.provider_config">
<!-- Embedded Signup Section -->
<template v-if="isEmbeddedSignupWhatsApp">
<SettingsFieldSection
@@ -51,6 +51,7 @@ defineEmits(['edit', 'delete']);
icon="i-woot-bin"
slate
sm
class="hover:enabled:text-n-ruby-11 hover:enabled:bg-n-ruby-2"
@click="$emit('delete', app)"
/>
</div>
@@ -133,6 +133,7 @@ const inboxName = hook => (hook.inbox ? hook.inbox.name : '');
icon="i-woot-bin"
slate
sm
class="hover:enabled:text-n-ruby-11 hover:enabled:bg-n-ruby-2"
@click="$emit('delete', hook)"
/>
</div>
@@ -73,6 +73,7 @@ const subscribedEvents = computed(() => {
icon="i-woot-bin"
slate
sm
class="hover:enabled:text-n-ruby-11 hover:enabled:bg-n-ruby-2"
@click="emit('delete', webhook, index)"
/>
</div>
@@ -174,6 +174,7 @@ onBeforeMount(() => {
icon="i-woot-bin"
slate
sm
class="hover:enabled:text-n-ruby-11 hover:enabled:bg-n-ruby-2"
:is-loading="loading[label.id]"
@click="openDeletePopup(label)"
/>
@@ -96,6 +96,7 @@ const visibilityLabel = computed(() => {
icon="i-woot-bin"
slate
sm
class="hover:enabled:text-n-ruby-11 hover:enabled:bg-n-ruby-2"
@click="$emit('delete')"
/>
</div>
@@ -288,6 +288,7 @@ export default {
icon="i-woot-bin"
slate
sm
class="hover:enabled:text-n-ruby-11 hover:enabled:bg-n-ruby-2"
:is-loading="loading[sla.id]"
@click="openDeletePopup(sla)"
/>
@@ -88,19 +88,14 @@ export default {
</script>
<template>
<div class="h-full w-full p-6 col-span-6">
<form
class="flex flex-wrap mx-0 overflow-x-auto"
@submit.prevent="addAgents"
>
<div class="w-full">
<PageHeader
:header-title="headerTitle"
:header-content="$t('TEAMS_SETTINGS.ADD.DESC')"
/>
</div>
<div class="h-full w-full p-8 col-span-6 overflow-auto">
<form class="flex flex-col gap-4 mx-0" @submit.prevent="addAgents">
<PageHeader
:header-title="headerTitle"
:header-content="$t('TEAMS_SETTINGS.ADD.DESC')"
/>
<div class="w-full">
<div class="w-full h-full">
<div v-if="v$.selectedAgents.$error">
<p class="error-message pb-2">
{{ $t('TEAMS_SETTINGS.ADD.AGENT_VALIDATION_ERROR') }}
@@ -37,7 +37,7 @@ export default {
</script>
<template>
<div class="h-full w-full p-6 col-span-6">
<div class="h-full w-full p-6 col-span-6 overflow-y-auto">
<PageHeader
:header-title="$t('TEAMS_SETTINGS.CREATE_FLOW.CREATE.TITLE')"
:header-content="$t('TEAMS_SETTINGS.CREATE_FLOW.CREATE.DESC')"
@@ -25,7 +25,7 @@ export default {
<template>
<div class="flex flex-col gap-6 mb-8 max-w-7xl mx-auto w-full !px-6">
<div
class="grid grid-cols-1 lg:grid-cols-8 lg:divide-x lg:divide-n-weak rounded-xl border border-n-weak min-h-[43rem] w-full"
class="grid grid-cols-1 lg:grid-cols-8 lg:divide-x lg:divide-n-weak rounded-xl border border-n-weak h-full min-h-[50dvh] w-full"
>
<woot-wizard
class="hidden lg:block col-span-2 h-fit py-8 px-6"
@@ -103,19 +103,14 @@ export default {
</script>
<template>
<div class="h-full w-full p-8 col-span-6">
<form
class="flex flex-wrap mx-0 overflow-x-auto"
@submit.prevent="addAgents"
>
<div class="w-full">
<PageHeader
:header-title="headerTitle"
:header-content="$t('TEAMS_SETTINGS.EDIT_FLOW.AGENTS.DESC')"
/>
</div>
<div class="h-full w-full p-8 col-span-6 overflow-auto">
<form class="flex flex-col gap-4 mx-0" @submit.prevent="addAgents">
<PageHeader
:header-title="headerTitle"
:header-content="$t('TEAMS_SETTINGS.EDIT_FLOW.AGENTS.DESC')"
/>
<div class="w-full">
<div class="w-full h-full">
<div v-if="v$.selectedAgents.$error">
<p class="error-message pb-2">
{{ $t('TEAMS_SETTINGS.ADD.AGENT_VALIDATION_ERROR') }}
@@ -57,7 +57,7 @@ export default {
</script>
<template>
<div class="h-full w-full p-8 col-span-6">
<div class="h-full w-full p-8 col-span-6 overflow-y-auto">
<PageHeader
:header-title="$t('TEAMS_SETTINGS.EDIT_FLOW.CREATE.TITLE')"
:header-content="$t('TEAMS_SETTINGS.EDIT_FLOW.CREATE.DESC')"
@@ -29,7 +29,7 @@ export default {
<template>
<div class="flex flex-col gap-6 mb-8 max-w-7xl mx-auto w-full !px-6">
<div
class="grid grid-cols-1 lg:grid-cols-8 lg:divide-x lg:divide-n-weak rounded-xl border border-n-weak min-h-[43rem] w-full"
class="grid grid-cols-1 lg:grid-cols-8 lg:divide-x lg:divide-n-weak rounded-xl border border-n-weak h-full min-h-[50dvh] w-full"
>
<woot-wizard
class="hidden lg:block col-span-2 h-fit py-8 px-6"
@@ -161,6 +161,7 @@ const confirmPlaceHolderText = computed(() =>
icon="i-woot-bin"
slate
sm
class="hover:enabled:text-n-ruby-11 hover:enabled:bg-n-ruby-2"
:is-loading="loading[team.id]"
@click="openDelete(team)"
/>
@@ -24,7 +24,7 @@ describe('GoogleOAuthButton.vue', () => {
const googleAuthUrl = new URL(wrapper.vm.getGoogleAuthUrl());
const params = googleAuthUrl.searchParams;
expect(googleAuthUrl.origin).toBe('https://accounts.google.com');
expect(googleAuthUrl.pathname).toBe('/o/oauth2/auth/oauthchooseaccount');
expect(googleAuthUrl.pathname).toBe('/o/oauth2/auth');
expect(params.get('client_id')).toBe('clientId');
expect(params.get('redirect_uri')).toBe(
'http://localhost:3000/test-callback'
@@ -6,8 +6,7 @@ export default {
// Creating the URL manually because the devise-token-auth with
// omniauth has a standing issue on redirecting the post request
// https://github.com/lynndylanhurley/devise_token_auth/issues/1466
const baseUrl =
'https://accounts.google.com/o/oauth2/auth/oauthchooseaccount';
const baseUrl = 'https://accounts.google.com/o/oauth2/auth';
const clientId = window.chatwootConfig.googleOAuthClientId;
const redirectUri = window.chatwootConfig.googleOAuthCallbackUrl;
const responseType = 'code';
+5
View File
@@ -30,6 +30,11 @@
description: 'Search FAQ responses using semantic similarity'
icon: 'search'
- id: resolve_conversation
title: 'Resolve Conversation'
description: 'Resolve a conversation when the issue has been addressed'
icon: 'checkmark'
- id: handoff
title: 'Handoff to Human'
description: 'Hand off the conversation to a human agent'
+1 -1
View File
@@ -1,5 +1,5 @@
shared: &shared
version: '4.11.0'
version: '4.11.1'
development:
<<: *shared
+1
View File
@@ -229,6 +229,7 @@ en:
activity:
captain:
resolved: 'Conversation was marked resolved by %{user_name} due to inactivity'
resolved_by_tool: 'Conversation was marked resolved by %{user_name}: %{reason}'
open: 'Conversation was marked open by %{user_name}'
agent_bot:
error_moved_to_open: 'Conversation was marked open by system due to an error with the agent bot.'
@@ -1,22 +1,23 @@
module Enterprise::ActivityMessageHandler
def automation_status_change_activity_content
if Current.executed_by.instance_of?(Captain::Assistant)
locale = Current.executed_by.account.locale
if resolved?
I18n.t(
'conversations.activity.captain.resolved',
user_name: Current.executed_by.name,
locale: locale
)
elsif open?
I18n.t(
'conversations.activity.captain.open',
user_name: Current.executed_by.name,
locale: locale
)
end
else
super
return super unless Current.executed_by.instance_of?(Captain::Assistant)
locale = Current.executed_by.account.locale
key = captain_activity_key
return unless key
I18n.t(key, user_name: Current.executed_by.name, reason: Current.captain_resolve_reason, locale: locale)
end
private
def captain_activity_key
if resolved? && Current.captain_resolve_reason.present?
'conversations.activity.captain.resolved_by_tool'
elsif resolved?
'conversations.activity.captain.resolved'
elsif open?
'conversations.activity.captain.open'
end
end
end
@@ -0,0 +1,27 @@
class Captain::Tools::ResolveConversationTool < Captain::Tools::BasePublicTool
description 'Resolve a conversation when the issue has been addressed or the conversation should be closed'
param :reason, type: 'string', desc: 'Brief reason for resolving the conversation', required: true
def perform(tool_context, reason:)
conversation = find_conversation(tool_context.state)
return 'Conversation not found' unless conversation
return "Conversation ##{conversation.display_id} is already resolved" if conversation.resolved?
log_tool_usage('resolve_conversation', { conversation_id: conversation.id, reason: reason })
Current.captain_resolve_reason = reason
begin
conversation.resolved!
ensure
Current.captain_resolve_reason = nil
end
"Conversation ##{conversation.display_id} resolved#{" (Reason: #{reason})" if reason}"
end
private
def permissions
%w[conversation_manage conversation_unassigned_manage conversation_participating_manage]
end
end
+2
View File
@@ -4,6 +4,7 @@ module Current
thread_mattr_accessor :account_user
thread_mattr_accessor :executed_by
thread_mattr_accessor :contact
thread_mattr_accessor :captain_resolve_reason
def self.reset
Current.user = nil
@@ -11,5 +12,6 @@ module Current
Current.account_user = nil
Current.executed_by = nil
Current.contact = nil
Current.captain_resolve_reason = nil
end
end
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@chatwoot/chatwoot",
"version": "4.11.0",
"version": "4.11.1",
"license": "MIT",
"scripts": {
"eslint": "eslint app/**/*.{js,vue}",
@@ -0,0 +1,52 @@
require 'rails_helper'
RSpec.describe Captain::Tools::ResolveConversationTool do
let(:account) { create(:account) }
let(:assistant) { create(:captain_assistant, account: account) }
let(:inbox) { create(:inbox, account: account) }
let(:conversation) { create(:conversation, account: account, inbox: inbox, status: :open) }
let(:tool) { described_class.new(assistant) }
let(:tool_context) { Struct.new(:state).new({ conversation: { id: conversation.id } }) }
before do
Current.executed_by = assistant
end
after do
Current.reset
end
describe 'resolving a conversation' do
it 'marks resolved and enqueues an activity message with the reason' do
tool.perform(tool_context, reason: 'Possible spam')
expect(conversation.reload).to be_resolved
expect(Conversations::ActivityMessageJob).to have_been_enqueued.with(
conversation,
hash_including(
content: I18n.t('conversations.activity.captain.resolved_by_tool', user_name: assistant.name, reason: 'Possible spam')
)
)
end
it 'clears captain_resolve_reason after execution' do
tool.perform(tool_context, reason: 'Possible spam')
expect(Current.captain_resolve_reason).to be_nil
end
end
describe 'resolving an already resolved conversation' do
let(:conversation) { create(:conversation, account: account, inbox: inbox, status: :resolved) }
it 'does not re-resolve and returns an already resolved message' do
queue_adapter = ActiveJob::Base.queue_adapter
queue_adapter.enqueued_jobs.clear
result = tool.perform(tool_context, reason: 'Possible spam')
expect(result).to include('already resolved')
expect(Conversations::ActivityMessageJob).not_to have_been_enqueued
end
end
end
+1 -1
View File
@@ -211,7 +211,7 @@ export const icons = {
height: 24,
},
gmail: {
body: `<path d="M20 7V17C20 17.5675 19.5675 18 19 18H18V8.9245L12 13.2345L6 8.9245V18H5C4.4325 18 4 17.5675 4 17V7C4 6.716 4.107 6.466 4.2865 6.289C4.466 6.1065 4.716 6 5 6H5.3335L12 10.8335L18.6665 6H19C19.284 6 19.534 6.107 19.7135 6.289C19.893 6.466 20 6.716 20 7Z" fill="currentColor"/>`,
body: `<path d="M20 6.25V16.75C20 17.3175 19.5675 17.75 19 17.75H18V8.1745L12 11.8095L6 8.1745V17.75H5C4.4325 17.75 4 17.3175 4 16.75V6.25C4 5.966 4.107 5.716 4.2865 5.539C4.466 5.3565 4.716 5.25 5 5.25H5.3335L12 9.3835L18.6665 5.25H19C19.284 5.25 19.534 5.357 19.7135 5.539C19.893 5.716 20 5.966 20 6.25Z" fill="currentColor"/>`,
width: 24,
height: 24,
},