diff --git a/app/controllers/api/v1/accounts/conversations_controller.rb b/app/controllers/api/v1/accounts/conversations_controller.rb index 10a676738..6e580a497 100644 --- a/app/controllers/api/v1/accounts/conversations_controller.rb +++ b/app/controllers/api/v1/accounts/conversations_controller.rb @@ -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? end + def toggle_priority + @conversation.toggle_priority(params[:priority]) + head :ok + 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 + typing_status_manager = ::Conversations::TypingStatusManager.new(@conversation, current_user, params) + typing_status_manager.toggle_typing_status head :ok end @@ -111,11 +112,6 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro @conversation.update_assignee(@agent) 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 @conversation ||= Current.account.conversations.find_by!(display_id: params[:id]) authorize @conversation.inbox, :show? diff --git a/app/javascript/dashboard/api/inbox/conversation.js b/app/javascript/dashboard/api/inbox/conversation.js index 8d5f5b82c..93dc884f9 100644 --- a/app/javascript/dashboard/api/inbox/conversation.js +++ b/app/javascript/dashboard/api/inbox/conversation.js @@ -52,6 +52,12 @@ class ConversationApi extends ApiClient { }); } + togglePriority({ conversationId, priority }) { + return axios.post(`${this.url}/${conversationId}/toggle_priority`, { + priority, + }); + } + assignAgent({ conversationId, agentId }) { return axios.post( `${this.url}/${conversationId}/assignments?assignee_id=${agentId}`, diff --git a/app/javascript/shared/constants/messages.js b/app/javascript/shared/constants/messages.js index c7a1873ab..63ca99afc 100644 --- a/app/javascript/shared/constants/messages.js +++ b/app/javascript/shared/constants/messages.js @@ -19,6 +19,14 @@ export const CONVERSATION_STATUS = { PENDING: 'pending', SNOOZED: 'snoozed', }; + +export const CONVERSATION_PRIORITY = { + URGENT: 'urgent', + HIGH: 'high', + LOW: 'low', + MEDIUM: 'medium', +}; + // Size in mega bytes export const MAXIMUM_FILE_UPLOAD_SIZE = 40; export const MAXIMUM_FILE_UPLOAD_SIZE_TWILIO_SMS_CHANNEL = 5; diff --git a/app/models/concerns/activity_message_handler.rb b/app/models/concerns/activity_message_handler.rb index 067bccbc1..3c53b6798 100644 --- a/app/models/concerns/activity_message_handler.rb +++ b/app/models/concerns/activity_message_handler.rb @@ -1,80 +1,77 @@ module ActivityMessageHandler extend ActiveSupport::Concern + include PriorityActivityMessageHandler + private def create_activity user_name = Current.user.name if Current.user.present? 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? end 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 + else + user_status_change_activity_content(user_name) + end - create_status_change_message(user_name) + ::Conversations::ActivityMessageJob.perform_later(self, activity_message_params(content)) if content + end + + def user_status_change_activity_content(user_name) + if user_name + I18n.t("conversations.activity.status.#{status}", user_name: user_name) + elsif Current.contact.present? && resolved? + I18n.t('conversations.activity.status.contact_resolved', contact_name: Current.contact.name.capitalize) + elsif resolved? + I18n.t('conversations.activity.status.auto_resolved', duration: auto_resolve_duration) + end + end + + def automation_status_change_activity_content + if Current.executed_by.instance_of?(AutomationRule) + I18n.t("conversations.activity.status.#{status}", user_name: 'Automation System') + elsif Current.executed_by.instance_of?(Contact) + Current.executed_by = nil + I18n.t('conversations.activity.status.system_auto_open') + end end def activity_message_params(content) { account_id: account_id, inbox_id: inbox_id, message_type: :activity, content: content } end - def create_status_change_message(user_name) - content = if user_name - I18n.t("conversations.activity.status.#{status}", user_name: user_name) - elsif Current.contact.present? && resolved? - I18n.t('conversations.activity.status.contact_resolved', contact_name: Current.contact.name.capitalize) - elsif resolved? - I18n.t('conversations.activity.status.auto_resolved', duration: auto_resolve_duration) - end - - ::Conversations::ActivityMessageJob.perform_later(self, activity_message_params(content)) if content - end - - def send_automation_activity - content = if Current.executed_by.instance_of?(AutomationRule) - I18n.t("conversations.activity.status.#{status}", user_name: 'Automation System') - elsif Current.executed_by.instance_of?(Contact) - Current.executed_by = nil - I18n.t('conversations.activity.status.system_auto_open') - end - - ::Conversations::ActivityMessageJob.perform_later(self, activity_message_params(content)) if content - end - def create_label_added(user_name, labels = []) - return unless labels.size.positive? - - 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 + create_label_change_activity('added', user_name, labels) end 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? - params = { user_name: user_name, labels: labels.join(', ') } - content = I18n.t('conversations.activity.labels.removed', **params) - + content = I18n.t("conversations.activity.labels.#{change_type}", user_name: user_name, labels: labels.join(', ')) ::Conversations::ActivityMessageJob.perform_later(self, activity_message_params(content)) if content end def create_muted_message - return unless Current.user - - params = { user_name: Current.user.name } - content = I18n.t('conversations.activity.muted', **params) - - ::Conversations::ActivityMessageJob.perform_later(self, activity_message_params(content)) if content + create_mute_change_activity('muted') end def create_unmuted_message + create_mute_change_activity('unmuted') + end + + def create_mute_change_activity(change_type) return unless Current.user - params = { user_name: Current.user.name } - content = I18n.t('conversations.activity.unmuted', **params) - + content = I18n.t("conversations.activity.#{change_type}", user_name: Current.user.name) ::Conversations::ActivityMessageJob.perform_later(self, activity_message_params(content)) if content end diff --git a/app/models/concerns/priority_activity_message_handler.rb b/app/models/concerns/priority_activity_message_handler.rb new file mode 100644 index 000000000..748c41bdc --- /dev/null +++ b/app/models/concerns/priority_activity_message_handler.rb @@ -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 diff --git a/app/models/conversation.rb b/app/models/conversation.rb index 9230fac61..299a7f2a5 100644 --- a/app/models/conversation.rb +++ b/app/models/conversation.rb @@ -149,6 +149,11 @@ class Conversation < ApplicationRecord save end + def toggle_priority(priority = nil) + self.priority = priority.presence + save + end + def bot_handoff! open! dispatcher_dispatch(CONVERSATION_BOT_HANDOFF) diff --git a/app/services/conversations/typing_status_manager.rb b/app/services/conversations/typing_status_manager.rb new file mode 100644 index 000000000..e3e9cebc6 --- /dev/null +++ b/app/services/conversations/typing_status_manager.rb @@ -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 diff --git a/config/locales/en.yml b/config/locales/en.yml index 97aa64976..189d04102 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -129,6 +129,10 @@ en: snoozed: "Conversation was snoozed by %{user_name}" 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. + 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: self_assigned: "%{user_name} self-assigned this conversation" assigned: "Assigned to %{assignee_name} by %{user_name}" diff --git a/config/routes.rb b/config/routes.rb index 327f991c9..c28dfd41c 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -91,6 +91,7 @@ Rails.application.routes.draw do post :unmute post :transcript post :toggle_status + post :toggle_priority post :toggle_typing_status post :update_last_seen post :unread diff --git a/spec/controllers/api/v1/accounts/conversations_controller_spec.rb b/spec/controllers/api/v1/accounts/conversations_controller_spec.rb index f38d9c1bf..9689bdd4d 100644 --- a/spec/controllers/api/v1/accounts/conversations_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/conversations_controller_spec.rb @@ -434,6 +434,52 @@ RSpec.describe 'Conversations API', type: :request do 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 let(:conversation) { create(:conversation, account: account) } diff --git a/spec/models/conversation_spec.rb b/spec/models/conversation_spec.rb index 2da0af0ca..163c27452 100644 --- a/spec/models/conversation_spec.rb +++ b/spec/models/conversation_spec.rb @@ -292,6 +292,47 @@ RSpec.describe Conversation, type: :model do 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 it 'resets the snoozed_until when status is toggled' do conversation = create(:conversation, status: 'snoozed', snoozed_until: 2.days.from_now)