mirror of
https://github.com/lingble/chatwoot.git
synced 2025-10-30 18:47:51 +00:00
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:
@@ -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?
|
||||
|
||||
@@ -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}`,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
33
app/models/concerns/priority_activity_message_handler.rb
Normal file
33
app/models/concerns/priority_activity_message_handler.rb
Normal 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
|
||||
@@ -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)
|
||||
|
||||
26
app/services/conversations/typing_status_manager.rb
Normal file
26
app/services/conversations/typing_status_manager.rb
Normal 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
|
||||
@@ -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}"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) }
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user