From 4d362da9f0e54172ada5d955f16af0be0cb4a32e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Pedro=20Baza=20Garcia=20Rodrigues?= <142340792+joao-baza@users.noreply.github.com> Date: Fri, 13 Feb 2026 04:15:40 -0400 Subject: [PATCH 01/10] fix: Prevent user enumeration on password reset endpoint (#13528) ## Description The current password reset endpoint returns different HTTP status codes and messages depending on whether the email exists in the system (200 for existing emails, 404 for non-existing ones). This allows attackers to enumerate valid email addresses via the password reset form. ## Changes ### `app/controllers/devise_overrides/passwords_controller.rb` - Removed the `if/else` branch that returned different responses based on email existence - Now always returns a generic `200 OK` response with the same message regardless of whether the email exists - Uses safe navigation operator (`&.`) to send reset instructions only if the user exists ### `config/locales/en.yml` - Consolidated `reset_password_success` and `reset_password_failure` into a single generic `reset_password` key - New message does not reveal whether the email exists in the system ## Security Impact - **Before**: An attacker could determine if an email was registered by observing the HTTP status code (200 vs 404) and response message - **After**: All requests receive the same 200 response with a generic message, preventing user enumeration This follows [OWASP guidelines for authentication error messages](https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html#authentication-responses). Fixes #13527 --- app/controllers/devise_overrides/passwords_controller.rb | 8 ++------ config/locales/en.yml | 3 +-- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/app/controllers/devise_overrides/passwords_controller.rb b/app/controllers/devise_overrides/passwords_controller.rb index 00976c3cd..c69541f6f 100644 --- a/app/controllers/devise_overrides/passwords_controller.rb +++ b/app/controllers/devise_overrides/passwords_controller.rb @@ -6,12 +6,8 @@ class DeviseOverrides::PasswordsController < Devise::PasswordsController def create @user = User.from_email(params[:email]) - if @user - @user.send_reset_password_instructions - build_response(I18n.t('messages.reset_password_success'), 200) - else - build_response(I18n.t('messages.reset_password_failure'), 404) - end + @user&.send_reset_password_instructions + build_response(I18n.t('messages.reset_password'), 200) end def update diff --git a/config/locales/en.yml b/config/locales/en.yml index f8d5b119e..07d9b0e2f 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -41,8 +41,7 @@ en: invalid_email: 'Please enter a valid email address' authentication_failed: 'Authentication failed. Please check your credentials and try again.' messages: - reset_password_success: Woot! Request for password reset is successful. Check your mail for instructions. - reset_password_failure: Uh ho! We could not find any user with the specified email. + reset_password: Request for password reset is successful. A email with instructions will be sent to your email if it exists. reset_password_saml_user: This account uses SAML authentication. Password reset is not available. Please contact your administrator. login_saml_user: This account uses SAML authentication. Please sign in through your organization's SAML provider. saml_not_available: SAML authentication is not available in this installation. From 6b7180d051a1338d66cd8f5f9939b6cb1076c0eb Mon Sep 17 00:00:00 2001 From: Sojan Jose Date: Fri, 13 Feb 2026 14:06:12 -0800 Subject: [PATCH 02/10] fix(twilio): prevent dead jobs on missing channel lookup (#13522) ## Why We observed `Webhooks::TwilioEventsJob` failures ending up in Sidekiq dead jobs when Twilio callback payloads could not be mapped to a `Channel::TwilioSms` record. In this scenario, channel lookup raised `ActiveRecord::RecordNotFound`, which caused retries and eventual dead jobs instead of a graceful drop. Related Sentry issue/search: - https://chatwoot-p3.sentry.io/issues/?project=6382945&query=Webhooks%3A%3ATwilioEventsJob%20ActiveRecord%3A%3ARecordNotFound ## What changed This PR keeps the existing lookup flow but makes it non-raising: - `app/services/twilio/incoming_message_service.rb` - `find_by!` -> `find_by` for account SID + phone lookup - Added warning log when channel lookup misses - `app/services/twilio/delivery_status_service.rb` - `find_by!` -> `find_by` for account SID + phone lookup - Added warning log when channel lookup misses ## Reproduction Configure a Twilio webhook callback that reaches Chatwoot but does not match an existing Twilio channel lookup path. Before this change, the job raises `RecordNotFound` and can end up in dead jobs after retries. After this change, the job logs the miss and exits safely. ## Testing - `bundle exec rspec spec/services/twilio/incoming_message_service_spec.rb spec/services/twilio/delivery_status_service_spec.rb` - `bundle exec rubocop app/services/twilio/incoming_message_service.rb app/services/twilio/delivery_status_service.rb` --- app/services/twilio/delivery_status_service.rb | 14 +++++++++++++- app/services/twilio/incoming_message_service.rb | 15 +++++++++++++-- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/app/services/twilio/delivery_status_service.rb b/app/services/twilio/delivery_status_service.rb index bf8422fcd..bed390aa5 100644 --- a/app/services/twilio/delivery_status_service.rb +++ b/app/services/twilio/delivery_status_service.rb @@ -47,8 +47,10 @@ class Twilio::DeliveryStatusService @twilio_channel ||= if params[:MessagingServiceSid].present? ::Channel::TwilioSms.find_by(messaging_service_sid: params[:MessagingServiceSid]) elsif params[:AccountSid].present? && params[:From].present? - ::Channel::TwilioSms.find_by!(account_sid: params[:AccountSid], phone_number: params[:From]) + ::Channel::TwilioSms.find_by(account_sid: params[:AccountSid], phone_number: params[:From]) end + log_channel_not_found if @twilio_channel.blank? + @twilio_channel end def message @@ -56,4 +58,14 @@ class Twilio::DeliveryStatusService @message ||= twilio_channel.inbox.messages.find_by(source_id: params[:MessageSid]) end + + def log_channel_not_found + Rails.logger.warn( + '[TWILIO] Delivery status channel lookup failed ' \ + "account_sid=#{params[:AccountSid]} " \ + "from=#{params[:From]} " \ + "messaging_service_sid=#{params[:MessagingServiceSid]} " \ + "message_sid=#{params[:MessageSid]}" + ) + end end diff --git a/app/services/twilio/incoming_message_service.rb b/app/services/twilio/incoming_message_service.rb index 5d695ebb2..d67b6d515 100644 --- a/app/services/twilio/incoming_message_service.rb +++ b/app/services/twilio/incoming_message_service.rb @@ -26,12 +26,23 @@ class Twilio::IncomingMessageService def twilio_channel @twilio_channel ||= ::Channel::TwilioSms.find_by(messaging_service_sid: params[:MessagingServiceSid]) if params[:MessagingServiceSid].present? if params[:AccountSid].present? && params[:To].present? - @twilio_channel ||= ::Channel::TwilioSms.find_by!(account_sid: params[:AccountSid], - phone_number: params[:To]) + @twilio_channel ||= ::Channel::TwilioSms.find_by(account_sid: params[:AccountSid], + phone_number: params[:To]) end + log_channel_not_found if @twilio_channel.blank? @twilio_channel end + def log_channel_not_found + Rails.logger.warn( + '[TWILIO] Incoming message channel lookup failed ' \ + "account_sid=#{params[:AccountSid]} " \ + "to=#{params[:To]} " \ + "messaging_service_sid=#{params[:MessagingServiceSid]} " \ + "sms_sid=#{params[:SmsSid]}" + ) + end + def inbox @inbox ||= twilio_channel.inbox end From fd5ac2a8a3a048b2ec062c0f75d2ecd68faaecb2 Mon Sep 17 00:00:00 2001 From: Sojan Jose Date: Fri, 13 Feb 2026 16:47:25 -0800 Subject: [PATCH 03/10] fix: apply installation branding replacement in tooltip copy (#13538) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Fix hardcoded `Chatwoot` branding in two UI tooltips using the existing `useBranding` flow so self-hosted/white-label deployments no longer show the wrong brand text. ## Changes - LabelSuggestion tooltip now uses: - `replaceInstallationName($t('LABEL_MGMT.SUGGESTIONS.POWERED_BY'))` - Message avatar tooltip (native app/external echo) now uses: - `replaceInstallationName(t('CONVERSATION.NATIVE_APP_ADVISORY'))` ## Why This follows the existing branding pattern already used in the product and keeps behavior consistent across deployments. ## Notes - No change to message logic or API behavior. - `AGENTS.md` updated with a branding guidance note. ## Fixes - Fixes https://github.com/chatwoot/chatwoot/issues/13306 - Fixes https://github.com/chatwoot/chatwoot/issues/13466 ## Testing Screenshot 2026-02-13 at 3 55 39 PM Screenshot 2026-02-13 at 3 55 48 PM --- AGENTS.md | 9 +++++++++ .../dashboard/components-next/message/Message.vue | 4 +++- .../conversation/conversation/LabelSuggestion.vue | 8 ++++++-- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 3b1bcb024..301633d7f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,6 +4,11 @@ - **Setup**: `bundle install && pnpm install` - **Run Dev**: `pnpm dev` or `overmind start -f ./Procfile.dev` +- **Seed Local Test Data**: `bundle exec rails db:seed` (quickly populates minimal data for standard feature verification) +- **Seed Search Test Data**: `bundle exec rails search:setup_test_data` (bulk fixture generation for search/performance/manual load scenarios) +- **Seed Account Sample Data (richer test data)**: `Seeders::AccountSeeder` is available as an internal utility and is exposed through Super Admin `Accounts#seed`, but can be used directly in dev workflows too: + - UI path: Super Admin → Accounts → Seed (enqueues `Internal::SeedAccountJob`). + - CLI path: `bundle exec rails runner "Internal::SeedAccountJob.perform_now(Account.find())"` (or call `Seeders::AccountSeeder.new(account: Account.find()).perform!` directly). - **Lint JS/Vue**: `pnpm eslint` / `pnpm eslint:fix` - **Lint Ruby**: `bundle exec rubocop -a` - **Test JS**: `pnpm test` or `pnpm test:watch` @@ -93,3 +98,7 @@ Practical checklist for any change impacting core logic or public APIs - When renaming/moving shared code, mirror the change in `enterprise/` to prevent drift. - Tests: Add Enterprise-specific specs under `spec/enterprise`, mirroring OSS spec layout where applicable. - When modifying existing OSS features for Enterprise-only behavior, add an Enterprise module (via `prepend_mod_with`/`include_mod_with`) instead of editing OSS files directly—especially for policies, controllers, and services. For Enterprise-exclusive features, place code directly under `enterprise/`. + +## Branding / White-labeling note + +- For user-facing strings that currently contain "Chatwoot" but should adapt to branded/self-hosted installs, prefer applying `replaceInstallationName` from `shared/composables/useBranding` in the UI layer (for example tooltip and suggestion labels) instead of adding hardcoded brand-specific copy. diff --git a/app/javascript/dashboard/components-next/message/Message.vue b/app/javascript/dashboard/components-next/message/Message.vue index 0f6ab85a8..78888d1e0 100644 --- a/app/javascript/dashboard/components-next/message/Message.vue +++ b/app/javascript/dashboard/components-next/message/Message.vue @@ -43,6 +43,7 @@ import VoiceCallBubble from './bubbles/VoiceCall.vue'; import MessageError from './MessageError.vue'; import ContextMenu from 'dashboard/modules/conversations/components/MessageContextMenu.vue'; +import { useBranding } from 'shared/composables/useBranding'; /** * @typedef {Object} Attachment @@ -143,6 +144,7 @@ const { t } = useI18n(); const route = useRoute(); const inboxGetter = useMapGetter('inboxes/getInbox'); const inbox = computed(() => inboxGetter.value(props.inboxId) || {}); +const { replaceInstallationName } = useBranding(); /** * Computes the message variant based on props @@ -472,7 +474,7 @@ const avatarInfo = computed(() => { const avatarTooltip = computed(() => { if (props.contentAttributes?.externalEcho) { - return t('CONVERSATION.NATIVE_APP_ADVISORY'); + return replaceInstallationName(t('CONVERSATION.NATIVE_APP_ADVISORY')); } if (avatarInfo.value.name === '') return ''; return `${t('CONVERSATION.SENT_BY')} ${avatarInfo.value.name}`; diff --git a/app/javascript/dashboard/components/widgets/conversation/conversation/LabelSuggestion.vue b/app/javascript/dashboard/components/widgets/conversation/conversation/LabelSuggestion.vue index 9075eb4ed..0c8a4fb57 100644 --- a/app/javascript/dashboard/components/widgets/conversation/conversation/LabelSuggestion.vue +++ b/app/javascript/dashboard/components/widgets/conversation/conversation/LabelSuggestion.vue @@ -2,6 +2,7 @@ // components import NextButton from 'dashboard/components-next/button/Button.vue'; import Avatar from 'dashboard/components-next/avatar/Avatar.vue'; +import { useBranding } from 'shared/composables/useBranding'; // composables import { useCaptain } from 'dashboard/composables/useCaptain'; @@ -34,8 +35,9 @@ export default { }, setup() { const { captainTasksEnabled } = useCaptain(); + const { replaceInstallationName } = useBranding(); - return { captainTasksEnabled }; + return { captainTasksEnabled, replaceInstallationName }; }, data() { return { @@ -228,7 +230,9 @@ export default {
Date: Mon, 16 Feb 2026 14:39:20 +0530 Subject: [PATCH 04/10] fix: Enforce team boundaries to prevent cross-team assignments (#13353) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description Fixes a critical bug where conversations assigned to a team could be auto-assigned to agents outside that team when all team members were at capacity. ## Type of change - [ ] Bug fix (non-breaking change which fixes an issue) ## 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 --- > [!NOTE] > **Medium Risk** > Changes core assignment selection for both legacy and v2 flows; misconfiguration of `allow_auto_assign` or team membership could cause conversations to remain unassigned. > > **Overview** > Prevents auto-assignment from crossing team boundaries by filtering eligible agents to the conversation’s `team` members (and requiring `team.allow_auto_assign`) in both the legacy `AutoAssignmentHandler` path and the v2 `AutoAssignment::AssignmentService` (including the Enterprise override). > > Adds test coverage to ensure team-scoped conversations only assign to team members, and are skipped when team auto-assign is disabled or no team members are available; also updates the conversations controller spec setup to include team membership. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 67ed2bda0cd8ffd56c7e0253b86369dead2e6155. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). --- .../concerns/auto_assignment_handler.rb | 10 +++- .../auto_assignment/assignment_service.rb | 19 ++++++-- .../auto_assignment/assignment_service.rb | 7 ++- .../accounts/conversations_controller_spec.rb | 1 + .../assignment_service_spec.rb | 47 +++++++++++++++++++ 5 files changed, 78 insertions(+), 6 deletions(-) diff --git a/app/models/concerns/auto_assignment_handler.rb b/app/models/concerns/auto_assignment_handler.rb index a1198200a..dca154842 100644 --- a/app/models/concerns/auto_assignment_handler.rb +++ b/app/models/concerns/auto_assignment_handler.rb @@ -19,10 +19,18 @@ module AutoAssignmentHandler AutoAssignment::AssignmentJob.perform_later(inbox_id: inbox.id) else # Use legacy assignment system - AutoAssignment::AgentAssignmentService.new(conversation: self, allowed_agent_ids: inbox.member_ids_with_assignment_capacity).perform + # If conversation has a team, only consider team members for assignment + allowed_agent_ids = team_id.present? ? team_member_ids_with_capacity : inbox.member_ids_with_assignment_capacity + AutoAssignment::AgentAssignmentService.new(conversation: self, allowed_agent_ids: allowed_agent_ids).perform end end + def team_member_ids_with_capacity + return [] if team.blank? || team.allow_auto_assign.blank? + + inbox.member_ids_with_assignment_capacity & team.members.ids + end + def should_run_auto_assignment? return false unless inbox.enable_auto_assignment? diff --git a/app/services/auto_assignment/assignment_service.rb b/app/services/auto_assignment/assignment_service.rb index 5d75c515f..89eff9d1c 100644 --- a/app/services/auto_assignment/assignment_service.rb +++ b/app/services/auto_assignment/assignment_service.rb @@ -19,7 +19,7 @@ class AutoAssignment::AssignmentService def perform_for_conversation(conversation) return false unless assignable?(conversation) - agent = find_available_agent + agent = find_available_agent(conversation) return false unless agent assign_conversation(conversation, agent) @@ -44,13 +44,26 @@ class AutoAssignment::AssignmentService scope.limit(limit) end - def find_available_agent - agents = filter_agents_by_rate_limit(inbox.available_agents) + def find_available_agent(conversation = nil) + agents = filter_agents_by_team(inbox.available_agents, conversation) + return nil if agents.nil? + + agents = filter_agents_by_rate_limit(agents) return nil if agents.empty? round_robin_selector.select_agent(agents) end + def filter_agents_by_team(agents, conversation) + return agents if conversation&.team_id.blank? + + team = conversation.team + return nil if team.blank? || team.allow_auto_assign.blank? + + team_member_ids = team.members.ids + agents.where(user_id: team_member_ids) + end + def filter_agents_by_rate_limit(agents) agents.select do |agent_member| rate_limiter = build_rate_limiter(agent_member.user) diff --git a/enterprise/app/services/enterprise/auto_assignment/assignment_service.rb b/enterprise/app/services/enterprise/auto_assignment/assignment_service.rb index 46422f9bc..66cdc31e5 100644 --- a/enterprise/app/services/enterprise/auto_assignment/assignment_service.rb +++ b/enterprise/app/services/enterprise/auto_assignment/assignment_service.rb @@ -14,8 +14,11 @@ module Enterprise::AutoAssignment::AssignmentService end # Extend agent finding to add capacity checks - def find_available_agent - agents = filter_agents_by_rate_limit(inbox.available_agents) + def find_available_agent(conversation = nil) + agents = filter_agents_by_team(inbox.available_agents, conversation) + return nil if agents.nil? + + agents = filter_agents_by_rate_limit(agents) agents = filter_agents_by_capacity(agents) if capacity_filtering_enabled? return nil if agents.empty? diff --git a/spec/controllers/api/v1/accounts/conversations_controller_spec.rb b/spec/controllers/api/v1/accounts/conversations_controller_spec.rb index 3c380c155..bc7b4097f 100644 --- a/spec/controllers/api/v1/accounts/conversations_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/conversations_controller_spec.rb @@ -330,6 +330,7 @@ RSpec.describe 'Conversations API', type: :request do context 'when it is an authenticated user who has access to the inbox' do before do create(:inbox_member, user: agent, inbox: inbox) + create(:team_member, user: agent, team: team) end it 'creates a new conversation' do diff --git a/spec/services/auto_assignment/assignment_service_spec.rb b/spec/services/auto_assignment/assignment_service_spec.rb index 2139e5e78..36a8c7816 100644 --- a/spec/services/auto_assignment/assignment_service_spec.rb +++ b/spec/services/auto_assignment/assignment_service_spec.rb @@ -307,5 +307,52 @@ RSpec.describe AutoAssignment::AssignmentService do end end end + + context 'with team assignments' do + let(:team) { create(:team, account: account, allow_auto_assign: true) } + let(:team_member) { create(:user, account: account, role: :agent, availability: :online) } + let(:rate_limiter) { instance_double(AutoAssignment::RateLimiter) } + + before do + create(:team_member, team: team, user: team_member) + create(:inbox_member, inbox: inbox, user: team_member) + + allow(OnlineStatusTracker).to receive(:get_available_users).and_return({ team_member.id.to_s => 'online' }) + + allow(AutoAssignment::RateLimiter).to receive(:new).and_return(rate_limiter) + allow(rate_limiter).to receive(:within_limit?).and_return(true) + allow(rate_limiter).to receive(:track_assignment) + + round_robin_selector = instance_double(AutoAssignment::RoundRobinSelector) + allow(AutoAssignment::RoundRobinSelector).to receive(:new).and_return(round_robin_selector) + allow(round_robin_selector).to receive(:select_agent).and_return(team_member) + end + + it 'assigns conversation with team to team member' do + conversation_with_team = create(:conversation, inbox: inbox, team: team, assignee: nil) + + service.perform_bulk_assignment(limit: 1) + + expect(conversation_with_team.reload.assignee).to eq(team_member) + end + + it 'skips assignment when team has allow_auto_assign false' do + team.update!(allow_auto_assign: false) + conversation_with_team = create(:conversation, inbox: inbox, team: team, assignee: nil) + + service.perform_bulk_assignment(limit: 1) + + expect(conversation_with_team.reload.assignee).to be_nil + end + + it 'skips assignment when no team members are available' do + allow(OnlineStatusTracker).to receive(:get_available_users).and_return({}) + conversation_with_team = create(:conversation, inbox: inbox, team: team, assignee: nil) + + service.perform_bulk_assignment(limit: 1) + + expect(conversation_with_team.reload.assignee).to be_nil + end + end end end From 9cd7c4ef89a7922b345923777586d87a1bdc5423 Mon Sep 17 00:00:00 2001 From: Tanmay Deep Sharma <32020192+tds-1@users.noreply.github.com> Date: Mon, 16 Feb 2026 14:47:33 +0530 Subject: [PATCH 05/10] fix: Enhance notification emails with message details and handle failed messages (#13273) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description Handle messages with null content properly in UI and email notifications ## Type of change - [ ] Bug fix (non-breaking change which fixes an issue) ## Relevant Screenshots: Screenshot 2026-01-21 at 4 43 00 PM ## 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 --- > [!NOTE] > **Medium Risk** > Touches notification email templates and message rendering conditions; mistakes could lead to missing content/attachments in emails or incorrect UI visibility, but changes are localized and non-auth/security related. > > **Overview** > Agent notification emails for *assigned* and *participating* new messages now include the actual message details (sender name, rendered text when present, and attachment links) and gracefully fall back when content is unavailable. > > To support this, the mailer now passes `@message` into Liquid via `MessageDrop` (adding `attachments` URLs), and the dashboard message UI now renders failed/external-error messages even when `content` is `null` while tightening retry eligibility to require content or attachments (and still within 1 day). > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 475c8cedda54eb5e806990f977faf8098d0b27d8. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). --------- Co-authored-by: Muhsin Keloth --- .../dashboard/components-next/message/Message.vue | 6 +++++- .../dashboard/components-next/message/MessageError.vue | 9 +++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/app/javascript/dashboard/components-next/message/Message.vue b/app/javascript/dashboard/components-next/message/Message.vue index 78888d1e0..66234984c 100644 --- a/app/javascript/dashboard/components-next/message/Message.vue +++ b/app/javascript/dashboard/components-next/message/Message.vue @@ -391,13 +391,17 @@ const shouldRenderMessage = computed(() => { const isUnsupported = props.contentAttributes?.isUnsupported; const isAnIntegrationMessage = props.contentType === CONTENT_TYPES.INTEGRATIONS; + const isFailedMessage = props.status === MESSAGE_STATUS.FAILED; + const hasExternalError = !!props.contentAttributes?.externalError; return ( hasAttachments || props.content || isEmailContentType || isUnsupported || - isAnIntegrationMessage + isAnIntegrationMessage || + isFailedMessage || + hasExternalError ); }); diff --git a/app/javascript/dashboard/components-next/message/MessageError.vue b/app/javascript/dashboard/components-next/message/MessageError.vue index cd17c1e3f..fe508c805 100644 --- a/app/javascript/dashboard/components-next/message/MessageError.vue +++ b/app/javascript/dashboard/components-next/message/MessageError.vue @@ -12,11 +12,16 @@ defineProps({ const emit = defineEmits(['retry']); -const { orientation, status, createdAt } = useMessageContext(); +const { orientation, status, createdAt, content, attachments } = + useMessageContext(); const { t } = useI18n(); -const canRetry = computed(() => !hasOneDayPassed(createdAt.value)); +const canRetry = computed(() => { + const hasContent = content.value !== null; + const hasAttachments = attachments.value && attachments.value.length > 0; + return !hasOneDayPassed(createdAt.value) && (hasContent || hasAttachments); +}); diff --git a/app/javascript/dashboard/composables/chatlist/useBulkActions.js b/app/javascript/dashboard/composables/chatlist/useBulkActions.js index a32c4c512..45421b978 100644 --- a/app/javascript/dashboard/composables/chatlist/useBulkActions.js +++ b/app/javascript/dashboard/composables/chatlist/useBulkActions.js @@ -102,6 +102,28 @@ export function useBulkActions() { } } + // Only used in context menu + async function onRemoveLabels(labelsToRemove, conversationId = null) { + try { + await store.dispatch('bulkActions/process', { + type: 'Conversation', + ids: conversationId || selectedConversations.value, + labels: { + remove: labelsToRemove, + }, + }); + + useAlert( + t('CONVERSATION.CARD_CONTEXT_MENU.API.LABEL_REMOVAL.SUCCESFUL', { + labelName: labelsToRemove[0], + conversationId, + }) + ); + } catch (err) { + useAlert(t('CONVERSATION.CARD_CONTEXT_MENU.API.LABEL_REMOVAL.FAILED')); + } + } + async function onAssignTeamsForBulk(team) { try { await store.dispatch('bulkActions/process', { @@ -189,6 +211,7 @@ export function useBulkActions() { isConversationSelected, onAssignAgent, onAssignLabels, + onRemoveLabels, onAssignTeamsForBulk, onUpdateConversations, }; diff --git a/app/javascript/dashboard/i18n/locale/en/conversation.json b/app/javascript/dashboard/i18n/locale/en/conversation.json index 99e0bb072..c1c87bc0c 100644 --- a/app/javascript/dashboard/i18n/locale/en/conversation.json +++ b/app/javascript/dashboard/i18n/locale/en/conversation.json @@ -174,6 +174,10 @@ "SUCCESFUL": "Assigned label #{labelName} to conversation id {conversationId}", "FAILED": "Couldn't assign label. Please try again." }, + "LABEL_REMOVAL": { + "SUCCESFUL": "Removed label #{labelName} from conversation id {conversationId}", + "FAILED": "Couldn't remove label. Please try again." + }, "TEAM_ASSIGNMENT": { "SUCCESFUL": "Assigned team \"{team}\" to conversation id {conversationId}", "FAILED": "Couldn't assign team. Please try again."