From 65312744c70cc1a2c15abad6705fe7b30167d66c Mon Sep 17 00:00:00 2001 From: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Date: Tue, 5 Aug 2025 03:15:36 +0530 Subject: [PATCH 01/65] chore: Update inbox view context menu (#12090) # Pull Request Template ## Description This PR updates the inbox view context menu to use the existing conversation card context menu for consistency. ## Type of change - [x] Bug fix (non-breaking change which fixes an issue) ## How Has This Been Tested? ### Screenshots **Before** image image **After** image image ## Checklist: - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my code - [ ] I have commented on my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published in downstream modules --------- Co-authored-by: Pranav --- .../components-next/Inbox/InboxCard.vue | 3 +- .../routes/dashboard/inbox/InboxList.vue | 2 +- .../inbox/components/InboxContextMenu.vue | 51 +++++++++---------- 3 files changed, 27 insertions(+), 29 deletions(-) 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/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" >
From 60a1e9b15dc912db86e03a2e7a791e0f9d11281b Mon Sep 17 00:00:00 2001 From: Muhsin Keloth Date: Tue, 5 Aug 2025 01:50:45 +0400 Subject: [PATCH 02/65] fix: Populate meta field for whatsApp shared contacts (#12097) Fixes https://github.com/chatwoot/chatwoot/issues/11999 --- app/services/whatsapp/incoming_message_base_service.rb | 9 ++++++++- spec/services/whatsapp/incoming_message_service_spec.rb | 9 +++------ 2 files changed, 11 insertions(+), 7 deletions(-) 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/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 From 53fce7be03f2f22c5109671adc11b4cdac9676f0 Mon Sep 17 00:00:00 2001 From: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Date: Tue, 5 Aug 2025 03:36:58 +0530 Subject: [PATCH 03/65] fix: Conditionally fetch limits and assistants for enterprise/cloud (#12099) # Pull Request Template ## Description ### Issue The Community Edition (CE) dashboard was making API requests to enterprise-only endpoints, causing 404 errors: * `/enterprise/api/v1/accounts/1/limits` * `/api/v1/accounts/1/captain/assistants?page=1` ### Solution 1. Added conditional checks to prevent these calls. 2. Remove unused component `app/javascript/dashboard/components/app/UpgradeBanner.vue` Fixes [CW-4695](https://linear.app/chatwoot/issue/CW-4695/440-ce-dashboard-calls-enterprise-urls), https://github.com/chatwoot/chatwoot/issues/12023 ## Type of change - [x] Bug fix (non-breaking change which fixes an issue) ## Checklist: - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my code - [ ] I have commented on my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published in downstream modules --------- Co-authored-by: Pranav --- .../components/app/UpgradeBanner.vue | 94 ------------------- .../components/copilot/CopilotContainer.vue | 9 +- .../dashboard/composables/useCaptain.js | 6 +- .../routes/dashboard/upgrade/UpgradePage.vue | 8 +- 4 files changed, 20 insertions(+), 97 deletions(-) delete mode 100644 app/javascript/dashboard/components/app/UpgradeBanner.vue 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/composables/useCaptain.js b/app/javascript/dashboard/composables/useCaptain.js index d28560944..3f93cfc58 100644 --- a/app/javascript/dashboard/composables/useCaptain.js +++ b/app/javascript/dashboard/composables/useCaptain.js @@ -1,12 +1,14 @@ import { computed } from 'vue'; import { useStore } from 'dashboard/composables/store.js'; import { useAccount } from 'dashboard/composables/useAccount'; +import { useConfig } from 'dashboard/composables/useConfig'; import { useCamelCase } from 'dashboard/composables/useTransformKeys'; import { FEATURE_FLAGS } from 'dashboard/featureFlags'; export function useCaptain() { const store = useStore(); const { isCloudFeatureEnabled, currentAccount } = useAccount(); + const { isEnterprise } = useConfig(); const captainEnabled = computed(() => { return isCloudFeatureEnabled(FEATURE_FLAGS.CAPTAIN); @@ -33,7 +35,9 @@ export function useCaptain() { }); const fetchLimits = () => { - store.dispatch('accounts/limits'); + if (isEnterprise) { + store.dispatch('accounts/limits'); + } }; return { 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 }); From 270f26e47194f6a74bae59b3514d1c4f7b52293b Mon Sep 17 00:00:00 2001 From: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Date: Tue, 5 Aug 2025 03:52:20 +0530 Subject: [PATCH 04/65] chore: Add new tab and copy link to conversation context menu (#12089) # Pull Request Template ## Description This PR includes the following enhancements to the conversation card context menu: 1. **Added "Open in New Tab" and "Copy Conversation Link" options.** * "Open in New Tab" allows users to quickly open a conversation in a separate browser tab. * "Copy Conversation Link" copies the conversation URL to the clipboard for easy sharing. 2. **Enabled the context menu in Previous Conversations card** with support for these two options. Fixes https://linear.app/chatwoot/issue/CW-4722/cannot-open-previous-conversations-in-a-new-tab ## Type of change - [x] Bug fix (non-breaking change which fixes an issue) ## How Has This Been Tested? ### Loom video https://www.loom.com/share/37b45d23c6804db292568d093b645ac0?sid=c3105971-f938-41bd-9f52-0f00d419d1b3 ## Checklist: - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my code - [x] I have commented on my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published in downstream modules --------- Co-authored-by: Pranav --- .../widgets/conversation/ConversationCard.vue | 31 ++- .../conversation/contextMenu/Index.vue | 239 ++++++++++++------ .../conversation/contextMenu/menuItem.vue | 34 ++- .../i18n/locale/en/conversation.json | 3 + .../conversation/ContactConversations.vue | 2 + 5 files changed, 206 insertions(+), 103 deletions(-) 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 @@