diff --git a/app/javascript/dashboard/components-next/Inbox/InboxCard.vue b/app/javascript/dashboard/components-next/Inbox/InboxCard.vue
index 79a451404..90cc1ff53 100644
--- a/app/javascript/dashboard/components-next/Inbox/InboxCard.vue
+++ b/app/javascript/dashboard/components-next/Inbox/InboxCard.vue
@@ -63,11 +63,12 @@ const lastActivityAt = computed(() => {
});
const menuItems = computed(() => [
- { key: 'delete', label: t('INBOX.MENU_ITEM.DELETE') },
{
key: isUnread.value ? 'mark_as_read' : 'mark_as_unread',
+ icon: isUnread.value ? 'mail' : 'mail-unread',
label: t(`INBOX.MENU_ITEM.MARK_AS_${isUnread.value ? 'READ' : 'UNREAD'}`),
},
+ { key: 'delete', icon: 'delete', label: t('INBOX.MENU_ITEM.DELETE') },
]);
const messageClasses = computed(() => ({
diff --git a/app/javascript/dashboard/components/app/UpgradeBanner.vue b/app/javascript/dashboard/components/app/UpgradeBanner.vue
deleted file mode 100644
index d41ce3438..000000000
--- a/app/javascript/dashboard/components/app/UpgradeBanner.vue
+++ /dev/null
@@ -1,94 +0,0 @@
-
-
-
-
-
-
diff --git a/app/javascript/dashboard/components/copilot/CopilotContainer.vue b/app/javascript/dashboard/components/copilot/CopilotContainer.vue
index 41ffa6b61..da0e69492 100644
--- a/app/javascript/dashboard/components/copilot/CopilotContainer.vue
+++ b/app/javascript/dashboard/components/copilot/CopilotContainer.vue
@@ -4,6 +4,7 @@ import { useStore } from 'dashboard/composables/store';
import Copilot from 'dashboard/components-next/copilot/Copilot.vue';
import { useMapGetter } from 'dashboard/composables/store';
import { useUISettings } from 'dashboard/composables/useUISettings';
+import { useConfig } from 'dashboard/composables/useConfig';
import { useWindowSize } from '@vueuse/core';
import { vOnClickOutside } from '@vueuse/components';
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
@@ -18,6 +19,7 @@ defineProps({
const store = useStore();
const { uiSettings, updateUISettings } = useUISettings();
+const { isEnterprise } = useConfig();
const { width: windowWidth } = useWindowSize();
const currentUser = useMapGetter('getCurrentUser');
@@ -82,6 +84,9 @@ const setAssistant = async assistant => {
};
const shouldShowCopilotPanel = computed(() => {
+ if (!isEnterprise) {
+ return false;
+ }
const isCaptainEnabled = isFeatureEnabledonAccount.value(
currentAccountId.value,
FEATURE_FLAGS.CAPTAIN
@@ -113,7 +118,9 @@ const sendMessage = async message => {
};
onMounted(() => {
- store.dispatch('captainAssistants/get');
+ if (isEnterprise) {
+ store.dispatch('captainAssistants/get');
+ }
});
diff --git a/app/javascript/dashboard/components/widgets/conversation/ConversationCard.vue b/app/javascript/dashboard/components/widgets/conversation/ConversationCard.vue
index 30e99ca9d..e87bea268 100644
--- a/app/javascript/dashboard/components/widgets/conversation/ConversationCard.vue
+++ b/app/javascript/dashboard/components/widgets/conversation/ConversationCard.vue
@@ -68,6 +68,10 @@ export default {
type: Boolean,
default: false,
},
+ allowedContextMenuOptions: {
+ type: Array,
+ default: () => [],
+ },
},
emits: [
'contextMenuToggle',
@@ -151,11 +155,9 @@ export default {
hasSlaPolicyId() {
return this.chat?.sla_policy_id;
},
- },
- methods: {
- onCardClick(e) {
+ conversationPath() {
const { activeInbox, chat } = this;
- const path = frontendURL(
+ return frontendURL(
conversationUrl({
accountId: this.accountId,
activeInbox,
@@ -166,18 +168,26 @@ export default {
conversationType: this.conversationType,
})
);
+ },
+ },
+ methods: {
+ onCardClick(e) {
+ const path = this.conversationPath;
+ if (!path) return;
+ // Handle Ctrl/Cmd + Click for new tab
if (e.metaKey || e.ctrlKey) {
+ e.preventDefault();
window.open(
- window.chatwootConfig.hostURL + path,
+ `${window.chatwootConfig.hostURL}${path}`,
'_blank',
- 'noopener noreferrer nofollow'
+ 'noopener,noreferrer'
);
return;
}
- if (this.isActiveChat) {
- return;
- }
+
+ // Skip if already active
+ if (this.isActiveChat) return;
router.push({ path });
},
@@ -359,6 +369,8 @@ export default {
:priority="chat.priority"
:chat-id="chat.id"
:has-unread-messages="hasUnread"
+ :conversation-url="conversationPath"
+ :allowed-options="allowedContextMenuOptions"
@update-conversation="onUpdateConversation"
@assign-agent="onAssignAgent"
@assign-label="onAssignLabel"
@@ -367,6 +379,7 @@ export default {
@mark-as-read="markAsRead"
@assign-priority="assignPriority"
@delete-conversation="deleteConversation"
+ @close="closeContextMenu"
/>
diff --git a/app/javascript/dashboard/components/widgets/conversation/contextMenu/Index.vue b/app/javascript/dashboard/components/widgets/conversation/contextMenu/Index.vue
index 6c788bbd2..a6f79500a 100644
--- a/app/javascript/dashboard/components/widgets/conversation/contextMenu/Index.vue
+++ b/app/javascript/dashboard/components/widgets/conversation/contextMenu/Index.vue
@@ -1,5 +1,8 @@
-
-
-
-
-
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
+
+
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/routes/dashboard/inbox/InboxList.vue b/app/javascript/dashboard/routes/dashboard/inbox/InboxList.vue
index ce6bd8be0..6f6f6cad3 100644
--- a/app/javascript/dashboard/routes/dashboard/inbox/InboxList.vue
+++ b/app/javascript/dashboard/routes/dashboard/inbox/InboxList.vue
@@ -246,7 +246,7 @@ onMounted(() => {
:key="notificationItem.id"
:inbox-item="notificationItem"
:state-inbox="stateInbox(notificationItem.primaryActor?.inboxId)"
- class="inbox-card rounded-lg hover:rounded-lg hover:bg-n-alpha-1 dark:hover:bg-n-alpha-3"
+ class="inbox-card rounded-none hover:rounded-lg hover:bg-n-alpha-1 dark:hover:bg-n-alpha-3"
:class="
currentConversationId === notificationItem.primaryActor?.id
? 'bg-n-alpha-1 dark:bg-n-alpha-3 rounded-lg active'
diff --git a/app/javascript/dashboard/routes/dashboard/inbox/components/InboxContextMenu.vue b/app/javascript/dashboard/routes/dashboard/inbox/components/InboxContextMenu.vue
index 8156bebbe..4671df3bb 100644
--- a/app/javascript/dashboard/routes/dashboard/inbox/components/InboxContextMenu.vue
+++ b/app/javascript/dashboard/routes/dashboard/inbox/components/InboxContextMenu.vue
@@ -1,32 +1,27 @@
-
@@ -37,12 +32,14 @@ export default {
@close="handleClose"
>
diff --git a/app/javascript/dashboard/routes/dashboard/upgrade/UpgradePage.vue b/app/javascript/dashboard/routes/dashboard/upgrade/UpgradePage.vue
index bd028779d..a650aac37 100644
--- a/app/javascript/dashboard/routes/dashboard/upgrade/UpgradePage.vue
+++ b/app/javascript/dashboard/routes/dashboard/upgrade/UpgradePage.vue
@@ -4,6 +4,7 @@ import { useStore } from 'dashboard/composables/store';
import { useMapGetter } from 'dashboard/composables/store.js';
import { useRouter } from 'vue-router';
import { useAccount } from 'dashboard/composables/useAccount';
+import { useConfig } from 'dashboard/composables/useConfig';
import { differenceInDays } from 'date-fns';
import { useAdmin } from 'dashboard/composables/useAdmin';
import { useI18n } from 'vue-i18n';
@@ -22,6 +23,7 @@ const router = useRouter();
const store = useStore();
const { t } = useI18n();
const { accountId, currentAccount } = useAccount();
+const { isEnterprise } = useConfig();
const { isAdmin } = useAdmin();
const isOnChatwootCloud = useMapGetter('globalConfig/isOnChatwootCloud');
@@ -100,7 +102,11 @@ const routeToBilling = () => {
});
};
-onMounted(() => fetchLimits());
+onMounted(() => {
+ if (isEnterprise) {
+ fetchLimits();
+ }
+});
defineExpose({ shouldShowUpgradePage });
diff --git a/app/listeners/hook_listener.rb b/app/listeners/hook_listener.rb
index 3360e23da..936d104a0 100644
--- a/app/listeners/hook_listener.rb
+++ b/app/listeners/hook_listener.rb
@@ -37,10 +37,11 @@ class HookListener < BaseListener
private
def execute_hooks(event, message)
- message.account.hooks.each do |hook|
+ message.account.hooks.find_each do |hook|
# In case of dialogflow, we would have a hook for each inbox.
# Which means we will execute the same hook multiple times if the below filter isn't there
next if hook.inbox.present? && hook.inbox != message.inbox
+ next unless supported_hook_event?(hook, event.name)
HookJob.perform_later(hook, event.name, message: message)
end
@@ -48,7 +49,24 @@ class HookListener < BaseListener
def execute_account_hooks(event, account, event_data = {})
account.hooks.account_hooks.find_each do |hook|
+ next unless supported_hook_event?(hook, event.name)
+
HookJob.perform_later(hook, event.name, event_data)
end
end
+
+ def supported_hook_event?(hook, event_name)
+ return false if hook.disabled?
+
+ supported_events_map = {
+ 'slack' => ['message.created'],
+ 'dialogflow' => ['message.created', 'message.updated'],
+ 'google_translate' => ['message.created'],
+ 'leadsquared' => ['contact.updated', 'conversation.created', 'conversation.resolved']
+ }
+
+ return false unless supported_events_map.key?(hook.app_id)
+
+ supported_events_map[hook.app_id].include?(event_name)
+ end
end
diff --git a/app/services/whatsapp/incoming_message_base_service.rb b/app/services/whatsapp/incoming_message_base_service.rb
index f057fadbd..94ad5c7d1 100644
--- a/app/services/whatsapp/incoming_message_base_service.rb
+++ b/app/services/whatsapp/incoming_message_base_service.rb
@@ -156,11 +156,18 @@ class Whatsapp::IncomingMessageBaseService
phones = contact[:phones]
phones = [{ phone: 'Phone number is not available' }] if phones.blank?
+ name_info = contact['name'] || {}
+ contact_meta = {
+ firstName: name_info['first_name'],
+ lastName: name_info['last_name']
+ }.compact
+
phones.each do |phone|
@message.attachments.new(
account_id: @message.account_id,
file_type: file_content_type(message_type),
- fallback_title: phone[:phone].to_s
+ fallback_title: phone[:phone].to_s,
+ meta: contact_meta
)
end
end
diff --git a/spec/listeners/hook_listener_spec.rb b/spec/listeners/hook_listener_spec.rb
index 4caa4dafd..3ae031237 100644
--- a/spec/listeners/hook_listener_spec.rb
+++ b/spec/listeners/hook_listener_spec.rb
@@ -10,6 +10,8 @@ describe HookListener do
account: account, inbox: inbox, conversation: conversation)
end
let!(:event) { Events::Base.new(event_name, Time.zone.now, message: message) }
+ let(:contact_event) { Events::Base.new('contact.updated', Time.zone.now, contact: conversation.contact) }
+ let(:conversation_event) { Events::Base.new('conversation.created', Time.zone.now, conversation: conversation) }
describe '#message_created' do
let(:event_name) { 'message.created' }
@@ -42,10 +44,88 @@ describe HookListener do
context 'when hook is configured' do
it 'triggers hook job' do
- hook = create(:integrations_hook, account: account)
+ hook = create(:integrations_hook, :dialogflow, account: account, inbox: inbox)
expect(HookJob).to receive(:perform_later).with(hook, 'message.updated', message: message).once
listener.message_updated(event)
end
end
end
+
+ describe 'hook job enqueuing behavior' do
+ let(:event_name) { 'message.created' }
+
+ context 'when app_id is not in the allowed list' do
+ it 'does not enqueue the job' do
+ create(:integrations_hook, account: account, app_id: 'unsupported_app')
+ expect(HookJob).not_to receive(:perform_later)
+
+ listener.message_created(event)
+ end
+ end
+
+ context 'when hook is enabled and app_id is supported' do
+ it 'enqueues the job for slack' do
+ hook = create(:integrations_hook, account: account)
+ expect(HookJob).to receive(:perform_later).with(hook, event_name, message: message)
+
+ listener.message_created(event)
+ end
+
+ it 'enqueues the job for dialogflow' do
+ hook = create(:integrations_hook, :dialogflow, account: account, inbox: inbox)
+ expect(HookJob).to receive(:perform_later).with(hook, event_name, message: message)
+
+ listener.message_created(event)
+ end
+
+ it 'enqueues the job for google_translate' do
+ hook = create(:integrations_hook, :google_translate, account: account)
+ expect(HookJob).to receive(:perform_later).with(hook, event_name, message: message)
+
+ listener.message_created(event)
+ end
+ end
+
+ context 'with disabled hook' do
+ it 'does not enqueue job for disabled hooks' do
+ create(:integrations_hook, account: account, status: 'disabled', app_id: 'slack')
+ expect(HookJob).not_to receive(:perform_later)
+
+ listener.message_created(event)
+ end
+ end
+
+ context 'with unsupported app_id and event combination' do
+ it 'does not enqueue job for unsupported app_id' do
+ create(:integrations_hook, account: account, app_id: 'unsupported_app')
+ expect(HookJob).not_to receive(:perform_later)
+
+ listener.message_created(event)
+ end
+ end
+
+ context 'with leadsquared hook' do
+ let(:hook) { create(:integrations_hook, :leadsquared, account: account) }
+
+ before do
+ account.enable_features(:crm_integration)
+ end
+
+ it 'enqueues the job for conversation.created' do
+ expect(HookJob)
+ .to receive(:perform_later)
+ .with(hook, 'conversation.created', { conversation: conversation })
+
+ listener.conversation_created(conversation_event)
+ end
+
+ it 'enqueues the job for contact.updated' do
+ expect(HookJob)
+ .to receive(:perform_later)
+ .with(hook, 'contact.updated', { contact: conversation.contact })
+
+ listener.contact_updated(contact_event)
+ end
+ end
+ end
end
diff --git a/spec/services/whatsapp/incoming_message_service_spec.rb b/spec/services/whatsapp/incoming_message_service_spec.rb
index 0bcbf2a3e..4035a47df 100644
--- a/spec/services/whatsapp/incoming_message_service_spec.rb
+++ b/spec/services/whatsapp/incoming_message_service_spec.rb
@@ -267,19 +267,16 @@ describe Whatsapp::IncomingMessageService do
] }] }.with_indifferent_access
described_class.new(inbox: whatsapp_channel.inbox, params: params).perform
expect(Contact.all.first.name).to eq('Kedar')
-
expect(whatsapp_channel.inbox.conversations.count).not_to eq(0)
- # Two messages are tested deliberately to ensure multiple contact attachments work.
m1 = whatsapp_channel.inbox.messages.first
- contact_attachments = m1.attachments.first
expect(m1.content).to eq('Apple Inc.')
- expect(contact_attachments.fallback_title).to eq('+911800')
+ expect(m1.attachments.first.fallback_title).to eq('+911800')
+ expect(m1.attachments.first.meta).to eq({})
m2 = whatsapp_channel.inbox.messages.last
- contact_attachments = m2.attachments.first
expect(m2.content).to eq('Chatwoot')
- expect(contact_attachments.fallback_title).to eq('+1 (415) 341-8386')
+ expect(m2.attachments.first.meta).to eq({ 'firstName' => 'Chatwoot' })
end
end