From 293a29ec9861a7fd29bdd1a5e368e5d8bc45c50c Mon Sep 17 00:00:00 2001 From: Vishnu Narayanan Date: Fri, 20 Jun 2025 13:05:14 +0530 Subject: [PATCH 01/11] chore: refactor account deletion email (#11772) - Refactor `Marked for deletion` to be `Deletion due at` in the compliance email --- .../account_compliance_mailer/account_deleted.liquid | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/mailers/administrator_notifications/account_compliance_mailer/account_deleted.liquid b/app/views/mailers/administrator_notifications/account_compliance_mailer/account_deleted.liquid index 636a5daa2..2e49a7830 100644 --- a/app/views/mailers/administrator_notifications/account_compliance_mailer/account_deleted.liquid +++ b/app/views/mailers/administrator_notifications/account_compliance_mailer/account_deleted.liquid @@ -6,8 +6,8 @@ Chatwoot Installation: {{ meta.instance_url }}
Account ID: {{ meta.account_id }}
Account Name: {{ meta.account_name }}
+ Deletion due at: {{ meta.marked_for_deletion_at }}
Deleted At: {{ meta.deleted_at }}
- Marked for Deletion at: {{ meta.marked_for_deletion_at }}
Deletion Reason: {{ meta.deletion_reason }}

From a2857cac38de2d4636cf4c16a68b5d6bbe18288c Mon Sep 17 00:00:00 2001 From: Tanmay Deep Sharma <32020192+tds-1@users.noreply.github.com> Date: Fri, 20 Jun 2025 23:28:00 +0530 Subject: [PATCH 02/11] feat: Expose custom attributes in conversation to Captain (#11769) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Pull Request Template ## Linear Link https://linear.app/chatwoot/issue/CW-4480/expose-custom-attributes-in-conversation-to-captain-so-that-it-can ## Description Expose custom attributes in conversation to Captain so that it can provide more information ## Type of change - [ ] New feature (non-breaking change which adds functionality) ## How Has This Been Tested? ![Screenshot 2025-06-19 at 9 50 45 AM](https://github.com/user-attachments/assets/5216e116-bd89-4d0c-b6a6-416b082638f7) ![Screenshot 2025-06-19 at 9 50 40 AM](https://github.com/user-attachments/assets/a81cb4ad-973b-405c-b188-295d1acce814) ## Checklist: - [ ] My code follows the style guidelines of this project - [ ] 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 - [ ] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [ ] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published in downstream modules --- .../conversation_llm_formatter.rb | 14 +++++++++++ .../conversation_llm_formatter_spec.rb | 25 +++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/app/services/llm_formatter/conversation_llm_formatter.rb b/app/services/llm_formatter/conversation_llm_formatter.rb index 1444d75c1..a43471e5b 100644 --- a/app/services/llm_formatter/conversation_llm_formatter.rb +++ b/app/services/llm_formatter/conversation_llm_formatter.rb @@ -11,6 +11,13 @@ class LlmFormatter::ConversationLlmFormatter < LlmFormatter::DefaultLlmFormatter end sections << "Contact Details: #{@record.contact.to_llm_text}" if config[:include_contact_details] + + attributes = build_attributes + if attributes.present? + sections << 'Conversation Attributes:' + sections << attributes + end + sections.join("\n") end @@ -30,4 +37,11 @@ class LlmFormatter::ConversationLlmFormatter < LlmFormatter::DefaultLlmFormatter sender = message.message_type == 'incoming' ? 'User' : 'Support agent' "#{sender}: #{message.content}\n" end + + def build_attributes + attributes = @record.account.custom_attribute_definitions.with_attribute_model('conversation_attribute').map do |attribute| + "#{attribute.attribute_display_name}: #{@record.custom_attributes[attribute.attribute_key]}" + end + attributes.join("\n") + end end diff --git a/spec/services/llm_formatter/conversation_llm_formatter_spec.rb b/spec/services/llm_formatter/conversation_llm_formatter_spec.rb index 93fec14f7..49fcc1a18 100644 --- a/spec/services/llm_formatter/conversation_llm_formatter_spec.rb +++ b/spec/services/llm_formatter/conversation_llm_formatter_spec.rb @@ -61,5 +61,30 @@ RSpec.describe LlmFormatter::ConversationLlmFormatter do expect(formatter.format(include_contact_details: true)).to eq(expected_output) end end + + context 'when conversation has custom attributes' do + it 'includes formatted custom attributes in the output' do + create( + :custom_attribute_definition, + account: account, + attribute_display_name: 'Order ID', + attribute_key: 'order_id', + attribute_model: :conversation_attribute + ) + + conversation.update(custom_attributes: { 'order_id' => '12345' }) + + expected_output = [ + "Conversation ID: ##{conversation.display_id}", + "Channel: #{conversation.inbox.channel.name}", + 'Message History:', + 'No messages in this conversation', + 'Conversation Attributes:', + 'Order ID: 12345' + ].join("\n") + + expect(formatter.format).to eq(expected_output) + end + end end end From ea4477ccdeb807bfc55588b2a72e696638a75b70 Mon Sep 17 00:00:00 2001 From: Pranav Date: Fri, 20 Jun 2025 13:20:55 -0700 Subject: [PATCH 03/11] fix: Update ActiveStorage::FileNotFoundError error and fix the captain condition in audio transcription (#11779) Update the error to `ActiveStorage::FileNotFoundError`. Fix the condition to enable audio transcription and added a spec for it. --- .../jobs/captain/conversation/response_builder_job.rb | 2 +- .../services/messages/audio_transcription_service.rb | 2 +- .../messages/audio_transcription_service_spec.rb | 10 ++++++++++ 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/enterprise/app/jobs/captain/conversation/response_builder_job.rb b/enterprise/app/jobs/captain/conversation/response_builder_job.rb index eb62a9a38..f341a6e98 100644 --- a/enterprise/app/jobs/captain/conversation/response_builder_job.rb +++ b/enterprise/app/jobs/captain/conversation/response_builder_job.rb @@ -13,7 +13,7 @@ class Captain::Conversation::ResponseBuilderJob < ApplicationJob generate_and_process_response end rescue StandardError => e - raise e if e.is_a?(ActiveJob::FileNotFoundError) + raise e if e.is_a?(ActiveStorage::FileNotFoundError) handle_error(e) ensure diff --git a/enterprise/app/services/messages/audio_transcription_service.rb b/enterprise/app/services/messages/audio_transcription_service.rb index c00328c66..b7d05766d 100644 --- a/enterprise/app/services/messages/audio_transcription_service.rb +++ b/enterprise/app/services/messages/audio_transcription_service.rb @@ -20,7 +20,7 @@ class Messages::AudioTranscriptionService < Llm::BaseOpenAiService private def can_transcribe? - return false if account.feature_enabled?('captain_integration') + return false unless account.feature_enabled?('captain_integration') return false if account.audio_transcriptions.blank? account.usage_limits[:captain][:responses][:current_available].positive? diff --git a/spec/enterprise/services/messages/audio_transcription_service_spec.rb b/spec/enterprise/services/messages/audio_transcription_service_spec.rb index 78879e1a1..41a4cae83 100644 --- a/spec/enterprise/services/messages/audio_transcription_service_spec.rb +++ b/spec/enterprise/services/messages/audio_transcription_service_spec.rb @@ -18,6 +18,16 @@ RSpec.describe Messages::AudioTranscriptionService, type: :service do describe '#perform' do let(:service) { described_class.new(attachment) } + context 'when captain_integration feature is not enabled' do + before do + account.disable_features!('captain_integration') + end + + it 'returns transcription limit exceeded' do + expect(service.perform).to eq({ error: 'Transcription limit exceeded' }) + end + end + context 'when transcription is successful' do before do # Mock can_transcribe? to return true and transcribe_audio method From be6bc88f804a2aa675aa1df733d8db1f625cfa0a Mon Sep 17 00:00:00 2001 From: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Date: Tue, 24 Jun 2025 12:38:05 +0530 Subject: [PATCH 04/11] fix: Translation issue in reports table headers on reload (#11793) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Pull Request Template ## Description This PR fixes the translation inconsistency in the reports pages, where table-column headers reverted to English after a page reload. **Cause** The components defined the columns array statically, so header labels were translated only once during component creation. On reload, the table showed the default system language (English) until the user’s locale finished loading. **Solution** Replaced the static columns array with a computed property and passed it to `Tanstack useVueTable` via a getter. This makes the headers reactive, ensuring they automatically update whenever the locale changes and remain translated after every reload. Fixes https://linear.app/chatwoot/issue/CW-4539/translation-issue-in-reports-page-table-header-on-reload ## 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 --- app/javascript/dashboard/components/table/Table.vue | 2 +- .../settings/reports/components/SummaryReports.vue | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/app/javascript/dashboard/components/table/Table.vue b/app/javascript/dashboard/components/table/Table.vue index 81edc4840..3e274f683 100644 --- a/app/javascript/dashboard/components/table/Table.vue +++ b/app/javascript/dashboard/components/table/Table.vue @@ -21,7 +21,7 @@ const props = defineProps({ const isRelaxed = computed(() => props.type === 'relaxed'); const headerClass = computed(() => isRelaxed.value - ? 'first:rounded-bl-lg first:rounded-tl-lg last:rounded-br-lg last:rounded-tr-lg' + ? 'ltr:first:rounded-bl-lg ltr:first:rounded-tl-lg ltr:last:rounded-br-lg ltr:last:rounded-tr-lg rtl:first:rounded-br-lg rtl:first:rounded-tr-lg rtl:last:rounded-bl-lg rtl:last:rounded-tl-lg' : '' ); diff --git a/app/javascript/dashboard/routes/dashboard/settings/reports/components/SummaryReports.vue b/app/javascript/dashboard/routes/dashboard/settings/reports/components/SummaryReports.vue index 7e0b9be8f..9b966c8fe 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/reports/components/SummaryReports.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/reports/components/SummaryReports.vue @@ -59,7 +59,7 @@ const defaulSpanRender = cellProps => cellProps.getValue() ); -const columns = [ +const columns = computed(() => [ columnHelper.accessor('name', { header: t(`SUMMARY_REPORTS.${props.type.toUpperCase()}`), width: 300, @@ -90,7 +90,7 @@ const columns = [ width: 200, cell: defaulSpanRender, }), -]; +]); const renderAvgTime = value => (value ? formatTime(value) : '--'); @@ -142,7 +142,9 @@ const table = useVueTable({ get data() { return tableData.value; }, - columns, + get columns() { + return columns.value; + }, enableSorting: false, getCoreRowModel: getCoreRowModel(), }); From 9edfb1e902b0e463d130f726f92c399d66687318 Mon Sep 17 00:00:00 2001 From: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Date: Tue, 24 Jun 2025 13:31:39 +0530 Subject: [PATCH 05/11] fix: Disable push notifications (#11786) # Pull Request Template ## Description Fixes [CW-4512](https://linear.app/chatwoot/issue/CW-4512/cant-turn-off-push-notification-toggle) https://github.com/chatwoot/chatwoot/issues/11760 ## 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 --- .../profile/NotificationPreferences.vue | 32 ++++++++++++++++--- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/app/javascript/dashboard/routes/dashboard/settings/profile/NotificationPreferences.vue b/app/javascript/dashboard/routes/dashboard/settings/profile/NotificationPreferences.vue index 215b46923..6ab2b34d4 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/profile/NotificationPreferences.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/profile/NotificationPreferences.vue @@ -75,10 +75,34 @@ export default { onRegistrationSuccess() { this.hasEnabledPushPermissions = true; }, - onRequestPermissions() { - requestPushPermissions({ - onSuccess: this.onRegistrationSuccess, - }); + onRequestPermissions(value) { + if (value) { + // Enable / re-enable push notifications + requestPushPermissions({ + onSuccess: this.onRegistrationSuccess, + }); + } else { + // Disable push notifications + this.disablePushPermissions(); + } + }, + disablePushPermissions() { + verifyServiceWorkerExistence(registration => + registration.pushManager + .getSubscription() + .then(subscription => { + if (subscription) { + return subscription.unsubscribe(); + } + return null; + }) + .finally(() => { + this.hasEnabledPushPermissions = false; + }) + .catch(() => { + // error + }) + ); }, getPushSubscription() { verifyServiceWorkerExistence(registration => From 92c51a10deff176519fc41b26e3ad0fe4669bcd2 Mon Sep 17 00:00:00 2001 From: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Date: Tue, 24 Jun 2025 16:33:42 +0530 Subject: [PATCH 06/11] chore: Update captain FAQ bulk action UI (#11780) --- .../captain/assistant/ResponseCard.vue | 2 +- .../i18n/locale/en/integrations.json | 2 + .../dashboard/captain/responses/Index.vue | 42 ++++++++++++++----- 3 files changed, 35 insertions(+), 11 deletions(-) diff --git a/app/javascript/dashboard/components-next/captain/assistant/ResponseCard.vue b/app/javascript/dashboard/components-next/captain/assistant/ResponseCard.vue index f00354105..7879411c8 100644 --- a/app/javascript/dashboard/components-next/captain/assistant/ResponseCard.vue +++ b/app/javascript/dashboard/components-next/captain/assistant/ResponseCard.vue @@ -123,7 +123,7 @@ const handleDocumentableClick = () => { @mouseenter="emit('hover', true)" @mouseleave="emit('hover', false)" > -
+
diff --git a/app/javascript/dashboard/i18n/locale/en/integrations.json b/app/javascript/dashboard/i18n/locale/en/integrations.json index 071c95604..41f63d0a2 100644 --- a/app/javascript/dashboard/i18n/locale/en/integrations.json +++ b/app/javascript/dashboard/i18n/locale/en/integrations.json @@ -537,6 +537,8 @@ "CONVERSATION": "Conversation #{id}" }, "SELECTED": "{count} selected", + "SELECT_ALL": "Select all ({count})", + "UNSELECT_ALL": "Unselect all ({count})", "BULK_APPROVE_BUTTON": "Approve", "BULK_DELETE_BUTTON": "Delete", "BULK_APPROVE": { diff --git a/app/javascript/dashboard/routes/dashboard/captain/responses/Index.vue b/app/javascript/dashboard/routes/dashboard/captain/responses/Index.vue index 258e1c0c9..e771e72ed 100644 --- a/app/javascript/dashboard/routes/dashboard/captain/responses/Index.vue +++ b/app/javascript/dashboard/routes/dashboard/captain/responses/Index.vue @@ -157,6 +157,13 @@ const bulkCheckbox = computed({ }, }); +const buildSelectedCountLabel = computed(() => { + const count = responses.value?.length || 0; + return bulkSelectionState.value.allSelected + ? t('CAPTAIN.RESPONSES.UNSELECT_ALL', { count }) + : t('CAPTAIN.RESPONSES.SELECT_ALL', { count }); +}); + const handleCardHover = (isHovered, id) => { hoveredCard.value = isHovered ? id : null; }; @@ -270,7 +277,11 @@ onMounted(() => {