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:
parent
9059f5906a
commit
1ca1b4d36b
6 changed files with 295 additions and 1 deletions
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
|
||||||
|
|
Loading…
Reference in a new issue