feat: add activity message for priority change (#6933)

* feat: add priority const

* feat: add toggle priority method

* feat: update controller route and specs

* refactor: status change method

* refactor: abstract label change and mute activity

* feat: add priority change_activity

* fix: interpolation for previous_changes

* refactor: reduce cognitive complexity of priority_change_activity

* refactor: move priority activity message handler to a separate module

* refactor: move typing logic to a service

* refactor: tests to reduce complexity

* fix: typo

* fix: constants

* fix: priority conditions

* fix: add a response

* fix: argument destructuring in I18n.t

---------

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
This commit is contained in:
Shivam Mishra
2023-04-20 16:41:53 +05:30
committed by GitHub
parent 6b2736aa63
commit a34729c153
11 changed files with 217 additions and 54 deletions

View File

@@ -64,13 +64,14 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
assign_conversation if @conversation.status == 'open' && Current.user.is_a?(User) && Current.user&.agent? assign_conversation if @conversation.status == 'open' && Current.user.is_a?(User) && Current.user&.agent?
end end
def toggle_typing_status def toggle_priority
case params[:typing_status] @conversation.toggle_priority(params[:priority])
when 'on' head :ok
trigger_typing_event(CONVERSATION_TYPING_ON, params[:is_private])
when 'off'
trigger_typing_event(CONVERSATION_TYPING_OFF, params[:is_private])
end end
def toggle_typing_status
typing_status_manager = ::Conversations::TypingStatusManager.new(@conversation, current_user, params)
typing_status_manager.toggle_typing_status
head :ok head :ok
end end
@@ -111,11 +112,6 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
@conversation.update_assignee(@agent) @conversation.update_assignee(@agent)
end end
def trigger_typing_event(event, is_private)
user = current_user.presence || @resource
Rails.configuration.dispatcher.dispatch(event, Time.zone.now, conversation: @conversation, user: user, is_private: is_private)
end
def conversation def conversation
@conversation ||= Current.account.conversations.find_by!(display_id: params[:id]) @conversation ||= Current.account.conversations.find_by!(display_id: params[:id])
authorize @conversation.inbox, :show? authorize @conversation.inbox, :show?

View File

@@ -52,6 +52,12 @@ class ConversationApi extends ApiClient {
}); });
} }
togglePriority({ conversationId, priority }) {
return axios.post(`${this.url}/${conversationId}/toggle_priority`, {
priority,
});
}
assignAgent({ conversationId, agentId }) { assignAgent({ conversationId, agentId }) {
return axios.post( return axios.post(
`${this.url}/${conversationId}/assignments?assignee_id=${agentId}`, `${this.url}/${conversationId}/assignments?assignee_id=${agentId}`,

View File

@@ -19,6 +19,14 @@ export const CONVERSATION_STATUS = {
PENDING: 'pending', PENDING: 'pending',
SNOOZED: 'snoozed', SNOOZED: 'snoozed',
}; };
export const CONVERSATION_PRIORITY = {
URGENT: 'urgent',
HIGH: 'high',
LOW: 'low',
MEDIUM: 'medium',
};
// Size in mega bytes // Size in mega bytes
export const MAXIMUM_FILE_UPLOAD_SIZE = 40; export const MAXIMUM_FILE_UPLOAD_SIZE = 40;
export const MAXIMUM_FILE_UPLOAD_SIZE_TWILIO_SMS_CHANNEL = 5; export const MAXIMUM_FILE_UPLOAD_SIZE_TWILIO_SMS_CHANNEL = 5;

View File

@@ -1,80 +1,77 @@
module ActivityMessageHandler module ActivityMessageHandler
extend ActiveSupport::Concern extend ActiveSupport::Concern
include PriorityActivityMessageHandler
private private
def create_activity def create_activity
user_name = Current.user.name if Current.user.present? user_name = Current.user.name if Current.user.present?
status_change_activity(user_name) if saved_change_to_status? status_change_activity(user_name) if saved_change_to_status?
priority_change_activity(user_name) if saved_change_to_priority?
create_label_change(activity_message_ownner(user_name)) if saved_change_to_label_list? create_label_change(activity_message_ownner(user_name)) if saved_change_to_label_list?
end end
def status_change_activity(user_name) def status_change_activity(user_name)
return send_automation_activity if Current.executed_by.present? content = if Current.executed_by.present?
automation_status_change_activity_content
create_status_change_message(user_name) else
user_status_change_activity_content(user_name)
end end
def activity_message_params(content) ::Conversations::ActivityMessageJob.perform_later(self, activity_message_params(content)) if content
{ account_id: account_id, inbox_id: inbox_id, message_type: :activity, content: content }
end end
def create_status_change_message(user_name) def user_status_change_activity_content(user_name)
content = if user_name if user_name
I18n.t("conversations.activity.status.#{status}", user_name: user_name) I18n.t("conversations.activity.status.#{status}", user_name: user_name)
elsif Current.contact.present? && resolved? elsif Current.contact.present? && resolved?
I18n.t('conversations.activity.status.contact_resolved', contact_name: Current.contact.name.capitalize) I18n.t('conversations.activity.status.contact_resolved', contact_name: Current.contact.name.capitalize)
elsif resolved? elsif resolved?
I18n.t('conversations.activity.status.auto_resolved', duration: auto_resolve_duration) I18n.t('conversations.activity.status.auto_resolved', duration: auto_resolve_duration)
end end
::Conversations::ActivityMessageJob.perform_later(self, activity_message_params(content)) if content
end end
def send_automation_activity def automation_status_change_activity_content
content = if Current.executed_by.instance_of?(AutomationRule) if Current.executed_by.instance_of?(AutomationRule)
I18n.t("conversations.activity.status.#{status}", user_name: 'Automation System') I18n.t("conversations.activity.status.#{status}", user_name: 'Automation System')
elsif Current.executed_by.instance_of?(Contact) elsif Current.executed_by.instance_of?(Contact)
Current.executed_by = nil Current.executed_by = nil
I18n.t('conversations.activity.status.system_auto_open') I18n.t('conversations.activity.status.system_auto_open')
end end
end
::Conversations::ActivityMessageJob.perform_later(self, activity_message_params(content)) if content def activity_message_params(content)
{ account_id: account_id, inbox_id: inbox_id, message_type: :activity, content: content }
end end
def create_label_added(user_name, labels = []) def create_label_added(user_name, labels = [])
return unless labels.size.positive? create_label_change_activity('added', user_name, labels)
params = { user_name: user_name, labels: labels.join(', ') }
content = I18n.t('conversations.activity.labels.added', **params)
::Conversations::ActivityMessageJob.perform_later(self, activity_message_params(content)) if content
end end
def create_label_removed(user_name, labels = []) def create_label_removed(user_name, labels = [])
create_label_change_activity('removed', user_name, labels)
end
def create_label_change_activity(change_type, user_name, labels = [])
return unless labels.size.positive? return unless labels.size.positive?
params = { user_name: user_name, labels: labels.join(', ') } content = I18n.t("conversations.activity.labels.#{change_type}", user_name: user_name, labels: labels.join(', '))
content = I18n.t('conversations.activity.labels.removed', **params)
::Conversations::ActivityMessageJob.perform_later(self, activity_message_params(content)) if content ::Conversations::ActivityMessageJob.perform_later(self, activity_message_params(content)) if content
end end
def create_muted_message def create_muted_message
return unless Current.user create_mute_change_activity('muted')
params = { user_name: Current.user.name }
content = I18n.t('conversations.activity.muted', **params)
::Conversations::ActivityMessageJob.perform_later(self, activity_message_params(content)) if content
end end
def create_unmuted_message def create_unmuted_message
create_mute_change_activity('unmuted')
end
def create_mute_change_activity(change_type)
return unless Current.user return unless Current.user
params = { user_name: Current.user.name } content = I18n.t("conversations.activity.#{change_type}", user_name: Current.user.name)
content = I18n.t('conversations.activity.unmuted', **params)
::Conversations::ActivityMessageJob.perform_later(self, activity_message_params(content)) if content ::Conversations::ActivityMessageJob.perform_later(self, activity_message_params(content)) if content
end end

View File

@@ -0,0 +1,33 @@
module PriorityActivityMessageHandler
extend ActiveSupport::Concern
private
def priority_change_activity(user_name)
old_priority, new_priority = previous_changes.values_at('priority')[0]
return unless priority_change?(old_priority, new_priority)
user = Current.executed_by.instance_of?(AutomationRule) ? 'Automation System' : user_name
content = build_priority_change_content(user, old_priority, new_priority)
::Conversations::ActivityMessageJob.perform_later(self, activity_message_params(content)) if content
end
def priority_change?(old_priority, new_priority)
old_priority.present? || new_priority.present?
end
def build_priority_change_content(user_name, old_priority = nil, new_priority = nil)
change_type = get_priority_change_type(old_priority, new_priority)
I18n.t("conversations.activity.priority.#{change_type}", user_name: user_name, new_priority: new_priority, old_priority: old_priority)
end
def get_priority_change_type(old_priority, new_priority)
case [old_priority.present?, new_priority.present?]
when [true, true] then 'updated'
when [false, true] then 'added'
when [true, false] then 'removed'
end
end
end

View File

@@ -149,6 +149,11 @@ class Conversation < ApplicationRecord
save save
end end
def toggle_priority(priority = nil)
self.priority = priority.presence
save
end
def bot_handoff! def bot_handoff!
open! open!
dispatcher_dispatch(CONVERSATION_BOT_HANDOFF) dispatcher_dispatch(CONVERSATION_BOT_HANDOFF)

View File

@@ -0,0 +1,26 @@
class Conversations::TypingStatusManager
include Events::Types
attr_reader :conversation, :user, :params
def initialize(conversation, user, params)
@conversation = conversation
@user = user
@params = params
end
def trigger_typing_event(event, is_private)
user = @user.presence || @resource
Rails.configuration.dispatcher.dispatch(event, Time.zone.now, conversation: @conversation, user: user, is_private: is_private)
end
def toggle_typing_status
case params[:typing_status]
when 'on'
trigger_typing_event(CONVERSATION_TYPING_ON, params[:is_private])
when 'off'
trigger_typing_event(CONVERSATION_TYPING_OFF, params[:is_private])
end
# Return the head :ok response from the controller
end
end

View File

@@ -129,6 +129,10 @@ en:
snoozed: "Conversation was snoozed by %{user_name}" snoozed: "Conversation was snoozed by %{user_name}"
auto_resolved: "Conversation was marked resolved by system due to %{duration} days of inactivity" auto_resolved: "Conversation was marked resolved by system due to %{duration} days of inactivity"
system_auto_open: System reopened the conversation due to a new incoming message. system_auto_open: System reopened the conversation due to a new incoming message.
priority:
added: '%{user_name} set the priority to %{new_priority}'
updated: '%{user_name} changed the priority from %{old_priority} to %{new_priority}'
removed: '%{user_name} removed the priority'
assignee: assignee:
self_assigned: "%{user_name} self-assigned this conversation" self_assigned: "%{user_name} self-assigned this conversation"
assigned: "Assigned to %{assignee_name} by %{user_name}" assigned: "Assigned to %{assignee_name} by %{user_name}"

View File

@@ -91,6 +91,7 @@ Rails.application.routes.draw do
post :unmute post :unmute
post :transcript post :transcript
post :toggle_status post :toggle_status
post :toggle_priority
post :toggle_typing_status post :toggle_typing_status
post :update_last_seen post :update_last_seen
post :unread post :unread

View File

@@ -434,6 +434,52 @@ RSpec.describe 'Conversations API', type: :request do
end end
end end
describe 'POST /api/v1/accounts/{account.id}/conversations/:id/toggle_priority' do
let(:conversation) { create(:conversation, account: account) }
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
post "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/toggle_priority"
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) }
let(:administrator) { create(:user, account: account, role: :administrator) }
before do
create(:inbox_member, user: agent, inbox: conversation.inbox)
end
it 'toggles the conversation priority to nil if no value is passed' do
expect(conversation.priority).to be_nil
post "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/toggle_priority",
headers: agent.create_new_auth_token,
params: { priority: 'low' },
as: :json
expect(response).to have_http_status(:success)
expect(conversation.reload.priority).to eq('low')
end
it 'toggles the conversation priority' do
conversation.priority = 'low'
conversation.save!
expect(conversation.reload.priority).to eq('low')
post "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/toggle_priority",
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(conversation.reload.priority).to be_nil
end
end
end
describe 'POST /api/v1/accounts/{account.id}/conversations/:id/toggle_typing_status' do describe 'POST /api/v1/accounts/{account.id}/conversations/:id/toggle_typing_status' do
let(:conversation) { create(:conversation, account: account) } let(:conversation) { create(:conversation, account: account) }

View File

@@ -292,6 +292,47 @@ RSpec.describe Conversation, type: :model do
end end
end end
describe '#toggle_priority' do
it 'defaults priority to nil when created' do
conversation = create(:conversation, status: 'open')
expect(conversation.priority).to be_nil
end
it 'toggles the priority to nil if nothing is passed' do
conversation = create(:conversation, status: 'open', priority: 'high')
expect(conversation.toggle_priority).to be(true)
expect(conversation.reload.priority).to be_nil
end
it 'sets the priority to low' do
conversation = create(:conversation, status: 'open')
expect(conversation.toggle_priority('low')).to be(true)
expect(conversation.reload.priority).to eq('low')
end
it 'sets the priority to medium' do
conversation = create(:conversation, status: 'open')
expect(conversation.toggle_priority('medium')).to be(true)
expect(conversation.reload.priority).to eq('medium')
end
it 'sets the priority to high' do
conversation = create(:conversation, status: 'open')
expect(conversation.toggle_priority('high')).to be(true)
expect(conversation.reload.priority).to eq('high')
end
it 'sets the priority to urgent' do
conversation = create(:conversation, status: 'open')
expect(conversation.toggle_priority('urgent')).to be(true)
expect(conversation.reload.priority).to eq('urgent')
end
end
describe '#ensure_snooze_until_reset' do describe '#ensure_snooze_until_reset' do
it 'resets the snoozed_until when status is toggled' do it 'resets the snoozed_until when status is toggled' do
conversation = create(:conversation, status: 'snoozed', snoozed_until: 2.days.from_now) conversation = create(:conversation, status: 'snoozed', snoozed_until: 2.days.from_now)