mirror of
				https://github.com/lingble/chatwoot.git
				synced 2025-10-31 11:08:04 +00:00 
			
		
		
		
	feat: bulk actions to update conversation objects (#3934)
Added the endpoints for bulk updating conversation objects Fixes: #3845 #3940 #3943
This commit is contained in:
		
							
								
								
									
										26
									
								
								app/controllers/api/v1/accounts/bulk_actions_controller.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								app/controllers/api/v1/accounts/bulk_actions_controller.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,26 @@ | |||||||
|  | class Api::V1::Accounts::BulkActionsController < Api::V1::Accounts::BaseController | ||||||
|  |   before_action :type_matches? | ||||||
|  |  | ||||||
|  |   def create | ||||||
|  |     if type_matches? | ||||||
|  |       ::BulkActionsJob.perform_later( | ||||||
|  |         account: @current_account, | ||||||
|  |         user: current_user, | ||||||
|  |         params: permitted_params | ||||||
|  |       ) | ||||||
|  |       head :ok | ||||||
|  |     else | ||||||
|  |       render json: { success: false }, status: :unprocessable_entity | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   private | ||||||
|  |  | ||||||
|  |   def type_matches? | ||||||
|  |     ['Conversation'].include?(params[:type]) | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def permitted_params | ||||||
|  |     params.permit(:type, ids: [], fields: [:status, :assignee_id, :team_id], labels: [add: [], remove: []]) | ||||||
|  |   end | ||||||
|  | end | ||||||
							
								
								
									
										59
									
								
								app/jobs/bulk_actions_job.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								app/jobs/bulk_actions_job.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,59 @@ | |||||||
|  | class BulkActionsJob < ApplicationJob | ||||||
|  |   queue_as :medium | ||||||
|  |   attr_accessor :records | ||||||
|  |  | ||||||
|  |   MODEL_TYPE = ['Conversation'].freeze | ||||||
|  |  | ||||||
|  |   def perform(account:, params:, user:) | ||||||
|  |     @account = account | ||||||
|  |     Current.user = user | ||||||
|  |     @params = params | ||||||
|  |     @records = records_to_updated(params[:ids]) | ||||||
|  |     bulk_update | ||||||
|  |   ensure | ||||||
|  |     Current.reset | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def bulk_update | ||||||
|  |     bulk_remove_labels | ||||||
|  |     bulk_conversation_update | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def bulk_conversation_update | ||||||
|  |     params = available_params(@params) | ||||||
|  |     records.each do |conversation| | ||||||
|  |       bulk_add_labels(conversation) | ||||||
|  |       conversation.update(params) if params | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def bulk_remove_labels | ||||||
|  |     records.each do |conversation| | ||||||
|  |       remove_labels(conversation) | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def available_params(params) | ||||||
|  |     return unless params[:fields] | ||||||
|  |  | ||||||
|  |     params[:fields].delete_if { |_k, v| v.nil? } | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def bulk_add_labels(conversation) | ||||||
|  |     conversation.add_labels(@params[:labels][:add]) if @params[:labels] && @params[:labels][:add] | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def remove_labels(conversation) | ||||||
|  |     return unless @params[:labels] && @params[:labels][:remove] | ||||||
|  |  | ||||||
|  |     labels = conversation.label_list - @params[:labels][:remove] | ||||||
|  |     conversation.update(label_list: labels) | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def records_to_updated(ids) | ||||||
|  |     current_model = @params[:type].camelcase | ||||||
|  |     return unless MODEL_TYPE.include?(current_model) | ||||||
|  |  | ||||||
|  |     current_model.constantize&.where(account_id: @account.id, display_id: ids) | ||||||
|  |   end | ||||||
|  | end | ||||||
| @@ -40,6 +40,7 @@ Rails.application.routes.draw do | |||||||
|             resource :contact_merge, only: [:create] |             resource :contact_merge, only: [:create] | ||||||
|           end |           end | ||||||
|  |  | ||||||
|  |           resource :bulk_actions, only: [:create] | ||||||
|           resources :agents, only: [:index, :create, :update, :destroy] |           resources :agents, only: [:index, :create, :update, :destroy] | ||||||
|           resources :agent_bots, only: [:index, :create, :show, :update, :destroy] |           resources :agent_bots, only: [:index, :create, :show, :update, :destroy] | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										145
									
								
								spec/controllers/api/v1/accounts/bulk_actions_controller_spec.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										145
									
								
								spec/controllers/api/v1/accounts/bulk_actions_controller_spec.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,145 @@ | |||||||
|  | require 'rails_helper' | ||||||
|  |  | ||||||
|  | RSpec.describe 'Api::V1::Accounts::BulkActionsController', type: :request do | ||||||
|  |   include ActiveJob::TestHelper | ||||||
|  |   let(:account) { create(:account) } | ||||||
|  |   let(:agent_1) { create(:user, account: account, role: :agent) } | ||||||
|  |   let(:agent_2) { create(:user, account: account, role: :agent) } | ||||||
|  |  | ||||||
|  |   before do | ||||||
|  |     create(:conversation, account_id: account.id, status: :open) | ||||||
|  |     create(:conversation, account_id: account.id, status: :open) | ||||||
|  |     create(:conversation, account_id: account.id, status: :open) | ||||||
|  |     create(:conversation, account_id: account.id, status: :open) | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   describe 'POST /api/v1/accounts/{account.id}/bulk_action' do | ||||||
|  |     context 'when it is an unauthenticated user' do | ||||||
|  |       let(:agent) { create(:user) } | ||||||
|  |  | ||||||
|  |       it 'returns unauthorized' do | ||||||
|  |         post "/api/v1/accounts/#{account.id}/bulk_actions", | ||||||
|  |              headers: agent.create_new_auth_token, | ||||||
|  |              params: { type: 'Conversation', fields: { status: 'open' }, ids: [1, 2, 3] } | ||||||
|  |  | ||||||
|  |         expect(response).to have_http_status(:unauthorized) | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |  | ||||||
|  |     context 'when it is an authenticated user' do | ||||||
|  |       let(:agent) { create(:user, account: account, role: :agent) } | ||||||
|  |  | ||||||
|  |       it 'Ignores bulk_actions for wrong type' do | ||||||
|  |         post "/api/v1/accounts/#{account.id}/bulk_actions", | ||||||
|  |              headers: agent.create_new_auth_token, | ||||||
|  |              params: { type: 'Test', fields: { status: 'snoozed' }, ids: %w[1 2 3] } | ||||||
|  |  | ||||||
|  |         expect(response).to have_http_status(:unprocessable_entity) | ||||||
|  |       end | ||||||
|  |  | ||||||
|  |       it 'Bulk update conversation status' do | ||||||
|  |         expect(Conversation.first.status).to eq('open') | ||||||
|  |         expect(Conversation.last.status).to eq('open') | ||||||
|  |         expect(Conversation.first.assignee_id).to eq(nil) | ||||||
|  |  | ||||||
|  |         perform_enqueued_jobs do | ||||||
|  |           post "/api/v1/accounts/#{account.id}/bulk_actions", | ||||||
|  |                headers: agent.create_new_auth_token, | ||||||
|  |                params: { type: 'Conversation', fields: { status: 'snoozed' }, ids: Conversation.first(3).pluck(:display_id) } | ||||||
|  |  | ||||||
|  |           expect(response).to have_http_status(:success) | ||||||
|  |         end | ||||||
|  |  | ||||||
|  |         expect(Conversation.first.status).to eq('snoozed') | ||||||
|  |         expect(Conversation.last.status).to eq('open') | ||||||
|  |         expect(Conversation.first.assignee_id).to eq(nil) | ||||||
|  |       end | ||||||
|  |  | ||||||
|  |       it 'Bulk update conversation assignee id' do | ||||||
|  |         params = { type: 'Conversation', fields: { assignee_id: agent_1.id }, ids: Conversation.first(3).pluck(:display_id) } | ||||||
|  |  | ||||||
|  |         expect(Conversation.first.status).to eq('open') | ||||||
|  |         expect(Conversation.first.assignee_id).to eq(nil) | ||||||
|  |         expect(Conversation.second.assignee_id).to eq(nil) | ||||||
|  |  | ||||||
|  |         perform_enqueued_jobs do | ||||||
|  |           post "/api/v1/accounts/#{account.id}/bulk_actions", | ||||||
|  |                headers: agent.create_new_auth_token, | ||||||
|  |                params: params | ||||||
|  |  | ||||||
|  |           expect(response).to have_http_status(:success) | ||||||
|  |         end | ||||||
|  |  | ||||||
|  |         expect(Conversation.first.assignee_id).to eq(agent_1.id) | ||||||
|  |         expect(Conversation.second.assignee_id).to eq(agent_1.id) | ||||||
|  |         expect(Conversation.first.status).to eq('open') | ||||||
|  |       end | ||||||
|  |  | ||||||
|  |       it 'Bulk update conversation status and assignee id' do | ||||||
|  |         params = { type: 'Conversation', fields: { assignee_id: agent_1.id, status: 'snoozed' }, ids: Conversation.first(3).pluck(:display_id) } | ||||||
|  |  | ||||||
|  |         expect(Conversation.first.status).to eq('open') | ||||||
|  |         expect(Conversation.second.assignee_id).to eq(nil) | ||||||
|  |  | ||||||
|  |         perform_enqueued_jobs do | ||||||
|  |           post "/api/v1/accounts/#{account.id}/bulk_actions", | ||||||
|  |                headers: agent.create_new_auth_token, | ||||||
|  |                params: params | ||||||
|  |  | ||||||
|  |           expect(response).to have_http_status(:success) | ||||||
|  |         end | ||||||
|  |  | ||||||
|  |         expect(Conversation.first.assignee_id).to eq(agent_1.id) | ||||||
|  |         expect(Conversation.second.assignee_id).to eq(agent_1.id) | ||||||
|  |         expect(Conversation.first.status).to eq('snoozed') | ||||||
|  |         expect(Conversation.second.status).to eq('snoozed') | ||||||
|  |       end | ||||||
|  |  | ||||||
|  |       it 'Bulk update conversation labels' do | ||||||
|  |         params = { type: 'Conversation', ids: Conversation.first(3).pluck(:display_id), labels: { add: %w[support priority_customer] } } | ||||||
|  |  | ||||||
|  |         expect(Conversation.first.labels).to eq([]) | ||||||
|  |         expect(Conversation.second.labels).to eq([]) | ||||||
|  |  | ||||||
|  |         perform_enqueued_jobs do | ||||||
|  |           post "/api/v1/accounts/#{account.id}/bulk_actions", | ||||||
|  |                headers: agent.create_new_auth_token, | ||||||
|  |                params: params | ||||||
|  |  | ||||||
|  |           expect(response).to have_http_status(:success) | ||||||
|  |         end | ||||||
|  |  | ||||||
|  |         expect(Conversation.first.label_list).to eq(%w[support priority_customer]) | ||||||
|  |         expect(Conversation.second.label_list).to eq(%w[support priority_customer]) | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   describe 'POST /api/v1/accounts/{account.id}/bulk_actions' do | ||||||
|  |     context 'when it is an authenticated user' do | ||||||
|  |       let(:agent) { create(:user, account: account, role: :agent) } | ||||||
|  |  | ||||||
|  |       it 'Bulk delete conversation labels' do | ||||||
|  |         Conversation.first.add_labels(%w[support priority_customer]) | ||||||
|  |         Conversation.second.add_labels(%w[support priority_customer]) | ||||||
|  |         Conversation.third.add_labels(%w[support priority_customer]) | ||||||
|  |  | ||||||
|  |         params = { type: 'Conversation', ids: Conversation.first(3).pluck(:display_id), labels: { remove: %w[support] } } | ||||||
|  |  | ||||||
|  |         expect(Conversation.first.label_list).to eq(%w[support priority_customer]) | ||||||
|  |         expect(Conversation.second.label_list).to eq(%w[support priority_customer]) | ||||||
|  |  | ||||||
|  |         perform_enqueued_jobs do | ||||||
|  |           post "/api/v1/accounts/#{account.id}/bulk_actions", | ||||||
|  |                headers: agent.create_new_auth_token, | ||||||
|  |                params: params | ||||||
|  |  | ||||||
|  |           expect(response).to have_http_status(:success) | ||||||
|  |         end | ||||||
|  |  | ||||||
|  |         expect(Conversation.first.label_list).to eq(['priority_customer']) | ||||||
|  |         expect(Conversation.second.label_list).to eq(['priority_customer']) | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | end | ||||||
							
								
								
									
										63
									
								
								spec/jobs/bulk_actions_job_spec.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								spec/jobs/bulk_actions_job_spec.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,63 @@ | |||||||
|  | require 'rails_helper' | ||||||
|  |  | ||||||
|  | RSpec.describe BulkActionsJob, type: :job do | ||||||
|  |   params = { | ||||||
|  |     type: 'Conversation', | ||||||
|  |     fields: { status: 'snoozed' }, | ||||||
|  |     ids: Conversation.first(3).pluck(:display_id) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   subject(:job) { described_class.perform_later(account: account, params: params, user: agent) } | ||||||
|  |  | ||||||
|  |   let(:account) { create(:account) } | ||||||
|  |   let!(:agent) { create(:user, account: account, role: :agent) } | ||||||
|  |   let!(:conversation_1) { create(:conversation, account_id: account.id, status: :open) } | ||||||
|  |   let!(:conversation_2) { create(:conversation, account_id: account.id, status: :open) } | ||||||
|  |   let!(:conversation_3) { create(:conversation, account_id: account.id, status: :open) } | ||||||
|  |  | ||||||
|  |   it 'enqueues the job' do | ||||||
|  |     expect { job }.to have_enqueued_job(described_class) | ||||||
|  |       .with(account: account, params: params, user: agent) | ||||||
|  |       .on_queue('medium') | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   context 'when job is triggered' do | ||||||
|  |     let(:bulk_action_job) { double } | ||||||
|  |  | ||||||
|  |     before do | ||||||
|  |       allow(bulk_action_job).to receive(:perform) | ||||||
|  |     end | ||||||
|  |  | ||||||
|  |     it 'bulk updates the status' do | ||||||
|  |       params = { | ||||||
|  |         type: 'Conversation', | ||||||
|  |         fields: { status: 'snoozed', assignee_id: agent.id }, | ||||||
|  |         ids: Conversation.first(3).pluck(:display_id) | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       expect(Conversation.first.status).to eq('open') | ||||||
|  |  | ||||||
|  |       described_class.perform_now(account: account, params: params, user: agent) | ||||||
|  |  | ||||||
|  |       expect(conversation_1.reload.status).to eq('snoozed') | ||||||
|  |       expect(conversation_2.reload.status).to eq('snoozed') | ||||||
|  |       expect(conversation_3.reload.status).to eq('snoozed') | ||||||
|  |     end | ||||||
|  |  | ||||||
|  |     it 'bulk updates the assignee_id' do | ||||||
|  |       params = { | ||||||
|  |         type: 'Conversation', | ||||||
|  |         fields: { status: 'snoozed', assignee_id: agent.id }, | ||||||
|  |         ids: Conversation.first(3).pluck(:display_id) | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       expect(Conversation.first.assignee_id).to eq(nil) | ||||||
|  |  | ||||||
|  |       described_class.perform_now(account: account, params: params, user: agent) | ||||||
|  |  | ||||||
|  |       expect(Conversation.first.assignee_id).to eq(agent.id) | ||||||
|  |       expect(Conversation.second.assignee_id).to eq(agent.id) | ||||||
|  |       expect(Conversation.third.assignee_id).to eq(agent.id) | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | end | ||||||
| @@ -162,7 +162,7 @@ describe ::Contacts::FilterService do | |||||||
|         expected_count = Contact.where("created_at < ? AND custom_attributes->>'customer_type' = ?", Date.tomorrow, 'platinum').count |         expected_count = Contact.where("created_at < ? AND custom_attributes->>'customer_type' = ?", Date.tomorrow, 'platinum').count | ||||||
|  |  | ||||||
|         expect(result[:contacts].length).to be expected_count |         expect(result[:contacts].length).to be expected_count | ||||||
|         expect(result[:contacts].first.id).to eq(el_contact.id) |         expect(result[:contacts].pluck(:id)).to include(el_contact.id) | ||||||
|       end |       end | ||||||
|  |  | ||||||
|       context 'with x_days_before filter' do |       context 'with x_days_before filter' do | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Tejaswini Chile
					Tejaswini Chile