feat: Add conversation delete feature (#11677)

<img width="1240" alt="Screenshot 2025-06-05 at 12 39 04 AM"
src="https://github.com/user-attachments/assets/0071cd23-38c3-4638-946e-f1fbd11ec845"
/>


## Changes

Give the admins an option to delete conversation via the context menu

- enable conversation deletion in routes and controller
- expose delete API on conversations
- add delete option in conversation context menu and integrate with card
and list
- implement store action and mutation for delete
- update i18n with new strings

fixes: https://github.com/chatwoot/chatwoot/issues/947

---------

Co-authored-by: iamsivin <iamsivin@gmail.com>
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: Pranav <pranavrajs@gmail.com>
This commit is contained in:
Sojan Jose
2025-06-05 15:53:17 -05:00
committed by GitHub
parent 4c0d096e4d
commit 273c277d47
22 changed files with 312 additions and 22 deletions

View File

@@ -926,4 +926,63 @@ RSpec.describe 'Conversations API', type: :request do
end
end
end
describe 'DELETE /api/v1/accounts/{account.id}/conversations/:id' do
let(:conversation) { create(:conversation, account: account) }
let(:agent) { create(:user, account: account, role: :agent) }
let(:administrator) { create(:user, account: account, role: :administrator) }
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
delete "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated agent' do
before do
create(:inbox_member, user: agent, inbox: conversation.inbox)
end
it 'returns unauthorized' do
delete "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}",
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unauthorized)
response_body = response.parsed_body
expect(response_body['error']).to eq('You are not authorized to do this action')
end
end
context 'when it is an authenticated administrator' do
before do
create(:inbox_member, user: administrator, inbox: conversation.inbox)
end
it 'successfully deletes the conversation' do
expect do
delete "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}",
headers: administrator.create_new_auth_token,
as: :json
end.to have_enqueued_job(DeleteObjectJob).with(conversation, administrator, anything)
expect(response).to have_http_status(:ok)
end
it 'can delete conversations from inboxes without direct access' do
other_inbox = create(:inbox, account: account)
other_conversation = create(:conversation, account: account, inbox: other_inbox)
expect do
delete "/api/v1/accounts/#{account.id}/conversations/#{other_conversation.display_id}",
headers: administrator.create_new_auth_token,
as: :json
end.to have_enqueued_job(DeleteObjectJob).with(other_conversation, administrator, anything)
expect(response).to have_http_status(:ok)
end
end
end
end

View File

@@ -0,0 +1,34 @@
require 'rails_helper'
RSpec.describe 'Conversation Audit', type: :model do
let(:account) { create(:account) }
let(:conversation) { create(:conversation, account: account) }
before do
# Enable auditing for conversations
conversation.class.send(:include, Enterprise::Audit::Conversation) if defined?(Enterprise::Audit::Conversation)
end
describe 'audit logging on destroy' do
it 'creates an audit log when conversation is destroyed' do
skip 'Enterprise audit module not available' unless defined?(Enterprise::Audit::Conversation)
expect do
conversation.destroy!
end.to change(Audited::Audit, :count).by(1)
audit = Audited::Audit.last
expect(audit.auditable_type).to eq('Conversation')
expect(audit.action).to eq('destroy')
expect(audit.auditable_id).to eq(conversation.id)
end
it 'does not create audit log for other actions by default' do
skip 'Enterprise audit module not available' unless defined?(Enterprise::Audit::Conversation)
expect do
conversation.update!(priority: 'high')
end.not_to(change(Audited::Audit, :count))
end
end
end

View File

@@ -0,0 +1,34 @@
require 'rails_helper'
RSpec.describe ConversationPolicy, type: :policy do
subject { described_class }
let(:account) { create(:account) }
let(:conversation) { create(:conversation, account: account) }
let(:administrator) { create(:user, account: account, role: :administrator) }
let(:agent) { create(:user, account: account, role: :agent) }
let(:administrator_context) { { user: administrator, account: account, account_user: administrator.account_users.first } }
let(:agent_context) { { user: agent, account: account, account_user: agent.account_users.first } }
permissions :destroy? do
context 'when user is an administrator' do
it 'allows destroy' do
expect(subject).to permit(administrator_context, conversation)
end
end
context 'when user is an agent' do
it 'denies destroy' do
expect(subject).not_to permit(agent_context, conversation)
end
end
end
permissions :index? do
context 'when user is authenticated' do
it 'allows index' do
expect(subject).to permit(agent_context, conversation)
end
end
end
end