diff --git a/Gemfile.lock b/Gemfile.lock index 8f7385de2..ccd3c39cd 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -441,7 +441,7 @@ GEM mini_magick (4.12.0) mini_mime (1.1.2) mini_portile2 (2.8.2) - minitest (5.18.0) + minitest (5.18.1) mock_redis (0.36.0) ruby2_keywords msgpack (1.7.0) @@ -450,7 +450,7 @@ GEM multipart-post (2.3.0) net-http-persistent (4.0.2) connection_pool (~> 2.2) - net-imap (0.3.4) + net-imap (0.3.6) date net-protocol net-pop (0.1.2) @@ -525,7 +525,7 @@ GEM pundit (2.3.0) activesupport (>= 3.0.0) raabro (1.4.0) - racc (1.7.0) + racc (1.7.1) rack (2.2.7) rack-attack (6.6.1) rack (>= 1.0, < 3) @@ -731,7 +731,7 @@ GEM time_diff (0.3.0) activesupport i18n - timeout (0.3.2) + timeout (0.4.0) trailblazer-option (0.1.2) twilio-ruby (5.77.0) faraday (>= 0.9, < 3.0) diff --git a/app/controllers/api/v1/accounts/inboxes_controller.rb b/app/controllers/api/v1/accounts/inboxes_controller.rb index 2f380e0cc..011faaf28 100644 --- a/app/controllers/api/v1/accounts/inboxes_controller.rb +++ b/app/controllers/api/v1/accounts/inboxes_controller.rb @@ -124,7 +124,7 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController def inbox_attributes [:name, :avatar, :greeting_enabled, :greeting_message, :enable_email_collect, :csat_survey_enabled, :enable_auto_assignment, :working_hours_enabled, :out_of_office_message, :timezone, :allow_messages_after_resolved, - :lock_to_single_conversation, :portal_id] + :lock_to_single_conversation, :portal_id, :sender_name_type, :business_name] end def permitted_params(channel_attributes = []) diff --git a/app/javascript/dashboard/components/SettingsSection.vue b/app/javascript/dashboard/components/SettingsSection.vue index b827ad9d4..3222434ae 100644 --- a/app/javascript/dashboard/components/SettingsSection.vue +++ b/app/javascript/dashboard/components/SettingsSection.vue @@ -1,11 +1,11 @@ diff --git a/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json b/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json index 5280c7e1a..540db8c76 100644 --- a/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json +++ b/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json @@ -391,6 +391,25 @@ "ENABLED": "Enabled", "DISABLED": "Disabled" }, + "SENDER_NAME_SECTION": { + "TITLE": "Sender name", + "SUB_TEXT": "Select the name shown to the your customer when they receive emails from your agents.", + "FOR_EG": "For eg:", + "FRIENDLY": { + "TITLE": "Friendly", + "FROM": "from", + "SUBTITLE": "Add the name of the agent who sent the reply in the sender name to make it friendly." + }, + "PROFESSIONAL": { + "TITLE": "Professional", + "SUBTITLE": "Use only the configured business name as the sender name in the email header." + }, + "BUSINESS_NAME": { + "BUTTON_TEXT": "+ Configure your business name", + "PLACEHOLDER": "Enter your business name", + "SAVE_BUTTON_TEXT": "Save" + } + }, "ALLOW_MESSAGES_AFTER_RESOLVED": { "ENABLED": "Enabled", "DISABLED": "Disabled" @@ -454,7 +473,9 @@ "ENABLE_EMAIL_COLLECT_BOX_SUB_TEXT": "Enable or disable email collect box on new conversation", "AUTO_ASSIGNMENT": "Enable auto assignment", "ENABLE_CSAT": "Enable CSAT", + "SENDER_NAME_SECTION": "Enable Agent Name in Email", "ENABLE_CSAT_SUB_TEXT": "Enable/Disable CSAT(Customer satisfaction) survey after resolving a conversation", + "SENDER_NAME_SECTION_TEXT": "Enable/Disable showing Agent's name in email, if disabled it will show business name", "ENABLE_CONTINUITY_VIA_EMAIL": "Enable conversation continuity via email", "ENABLE_CONTINUITY_VIA_EMAIL_SUB_TEXT": "Conversations will continue over email if the contact email address is available.", "LOCK_TO_SINGLE_CONVERSATION": "Lock to single conversation", diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/Settings.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/Settings.vue index 5e1e55cff..be6792f32 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/inbox/Settings.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/Settings.vue @@ -23,6 +23,7 @@ -
+
@@ -313,7 +314,7 @@ {{ $t('INBOX_MGMT.FEATURES.DISPLAY_FILE_PICKER') }}
-
+
-
+
-
+
- + + +
+ +
+ + {{ + $t( + 'INBOX_MGMT.EDIT.SENDER_NAME_SECTION.BUSINESS_NAME.BUTTON_TEXT' + ) + }} + +
+ + + {{ + $t( + 'INBOX_MGMT.EDIT.SENDER_NAME_SECTION.BUSINESS_NAME.SAVE_BUTTON_TEXT' + ) + }} + +
+
+
+
+ { + this.$refs.businessNameInput.focus(); + }); + } + }, }, validations: { webhookUrl: { @@ -717,12 +785,6 @@ export default { } } - .settings--content { - div:last-child { - border-bottom: 0; - } - } - .tabs { padding: 0; margin-bottom: -1px; @@ -742,4 +804,22 @@ export default { padding-bottom: var(--space-smaller); } } + +.business-section { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: var(--space-small); + margin-top: var(--space-small); + + .business-name-input { + display: flex; + gap: var(--space-small); + width: 80%; + + input { + margin-bottom: 0; + } + } +} diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/components/SenderNameExamplePreview.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/components/SenderNameExamplePreview.vue new file mode 100644 index 000000000..33d3e4749 --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/components/SenderNameExamplePreview.vue @@ -0,0 +1,161 @@ + + + + + diff --git a/app/javascript/shared/mixins/specs/automationFixtures.js b/app/javascript/shared/mixins/specs/automationFixtures.js index 99bb9fd9d..81a53ad25 100644 --- a/app/javascript/shared/mixins/specs/automationFixtures.js +++ b/app/javascript/shared/mixins/specs/automationFixtures.js @@ -390,6 +390,7 @@ export const inboxes = [ working_hours_enabled: false, enable_email_collect: true, csat_survey_enabled: true, + sender_name_type: 0, enable_auto_assignment: true, out_of_office_message: 'We are unavailable at the moment. Leave a message we will respond once we are back.', diff --git a/app/mailers/conversation_reply_mailer.rb b/app/mailers/conversation_reply_mailer.rb index d1b185dd1..99749ea95 100644 --- a/app/mailers/conversation_reply_mailer.rb +++ b/app/mailers/conversation_reply_mailer.rb @@ -79,14 +79,31 @@ class ConversationReplyMailer < ApplicationMailer @conversation.messages.chat.where.not(message_type: :incoming)&.last end - def sender_name - @sender_name ||= current_message&.sender&.available_name || @agent&.available_name || 'Notifications' + def sender_name(sender_email) + if @inbox.friendly? + I18n.t('conversations.reply.email.header.friendly_name', sender_name: custom_sender_name, business_name: business_name, + from_email: sender_email) + else + I18n.t('conversations.reply.email.header.professional_name', business_name: business_name, from_email: sender_email) + end end def current_message @message || @conversation.messages.outgoing.last end + def custom_sender_name + current_message&.sender&.available_name || @agent&.available_name || 'Notifications' + end + + def business_name + @inbox.business_name || @inbox.name + end + + def from_email + should_use_conversation_email_address? ? parse_email(@account.support_email) : parse_email(inbox_from_email_address) + end + def mail_subject subject = @conversation.additional_attributes['mail_subject'] return "[##{@conversation.display_id}] #{I18n.t('conversations.reply.email_subject')}" if subject.nil? @@ -101,26 +118,18 @@ class ConversationReplyMailer < ApplicationMailer def reply_email if should_use_conversation_email_address? - I18n.t('conversations.reply.email.header.reply_with_name', assignee_name: sender_name, inbox_name: @inbox.name, - reply_email: "#{@conversation.uuid}@#{@account.inbound_email_domain}") + sender_name("reply+#{@conversation.uuid}@#{@account.inbound_email_domain}") else @inbox.email_address || @agent&.email end end def from_email_with_name - if should_use_conversation_email_address? - I18n.t('conversations.reply.email.header.from_with_name', assignee_name: sender_name, inbox_name: @inbox.name, - from_email: parse_email(@account.support_email)) - else - I18n.t('conversations.reply.email.header.from_with_name', assignee_name: sender_name, inbox_name: @inbox.name, - from_email: parse_email(inbox_from_email_address)) - end + sender_name(from_email) end def channel_email_with_name - I18n.t('conversations.reply.channel_email.header.reply_with_name', assignee_name: sender_name, inbox_name: @inbox.name, - from_email: @channel.email) + sender_name(@channel.email) end def parse_email(email_string) diff --git a/app/models/inbox.rb b/app/models/inbox.rb index 592ac0923..fcafc2ff0 100644 --- a/app/models/inbox.rb +++ b/app/models/inbox.rb @@ -7,6 +7,7 @@ # id :integer not null, primary key # allow_messages_after_resolved :boolean default(TRUE) # auto_assignment_config :jsonb +# business_name :string # channel_type :string # csat_survey_enabled :boolean default(FALSE) # email_address :string @@ -17,6 +18,7 @@ # lock_to_single_conversation :boolean default(FALSE), not null # name :string not null # out_of_office_message :string +# sender_name_type :integer default("friendly"), not null # timezone :string default("UTC") # working_hours_enabled :boolean default(FALSE) # created_at :datetime not null @@ -69,6 +71,8 @@ class Inbox < ApplicationRecord has_many :webhooks, dependent: :destroy_async has_many :hooks, dependent: :destroy_async, class_name: 'Integrations::Hook' + enum sender_name_type: { friendly: 0, professional: 1 } + after_destroy :delete_round_robin_agents scope :order_by_name, -> { order('lower(name) ASC') } diff --git a/app/views/api/v1/models/_inbox.json.jbuilder b/app/views/api/v1/models/_inbox.json.jbuilder index a82e63bc9..09db8fdc4 100644 --- a/app/views/api/v1/models/_inbox.json.jbuilder +++ b/app/views/api/v1/models/_inbox.json.jbuilder @@ -16,6 +16,8 @@ json.timezone resource.timezone json.callback_webhook_url resource.callback_webhook_url json.allow_messages_after_resolved resource.allow_messages_after_resolved json.lock_to_single_conversation resource.lock_to_single_conversation +json.sender_name_type resource.sender_name_type +json.business_name resource.business_name if resource.portal.present? json.help_center do diff --git a/config/locales/en.yml b/config/locales/en.yml index b4ea686d9..c83718201 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -160,6 +160,8 @@ en: header: from_with_name: "%{assignee_name} from %{inbox_name} <%{from_email}>" reply_with_name: "%{assignee_name} from %{inbox_name} " + friendly_name: "%{sender_name} from %{business_name} <%{from_email}>" + professional_name: "%{business_name} <%{from_email}>" channel_email: header: reply_with_name: "%{assignee_name} from %{inbox_name} <%{from_email}>" diff --git a/db/migrate/20230525085402_add_custom_sender_name_toggle.rb b/db/migrate/20230525085402_add_custom_sender_name_toggle.rb new file mode 100644 index 000000000..1491ee2b8 --- /dev/null +++ b/db/migrate/20230525085402_add_custom_sender_name_toggle.rb @@ -0,0 +1,5 @@ +class AddCustomSenderNameToggle < ActiveRecord::Migration[7.0] + def change + add_column :inboxes, :sender_name_type, :integer, default: 0, null: false + end +end diff --git a/db/migrate/20230614044633_add_sender_name_to_in.rb b/db/migrate/20230614044633_add_sender_name_to_in.rb new file mode 100644 index 000000000..a0dc3331f --- /dev/null +++ b/db/migrate/20230614044633_add_sender_name_to_in.rb @@ -0,0 +1,5 @@ +class AddSenderNameToIn < ActiveRecord::Migration[7.0] + def change + add_column :inboxes, :business_name, :string, null: true + end +end diff --git a/db/schema.rb b/db/schema.rb index 595604d2b..b8c552ab4 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -584,6 +584,8 @@ ActiveRecord::Schema[7.0].define(version: 2023_06_20_212340) do t.jsonb "auto_assignment_config", default: {} t.boolean "lock_to_single_conversation", default: false, null: false t.bigint "portal_id" + t.string "business_name" + t.integer "sender_name_type", default: 0, null: false t.index ["account_id"], name: "index_inboxes_on_account_id" t.index ["channel_id", "channel_type"], name: "index_inboxes_on_channel_id_and_channel_type" t.index ["portal_id"], name: "index_inboxes_on_portal_id" diff --git a/spec/mailers/conversation_reply_mailer_spec.rb b/spec/mailers/conversation_reply_mailer_spec.rb index 77f993cc2..e4bfac413 100644 --- a/spec/mailers/conversation_reply_mailer_spec.rb +++ b/spec/mailers/conversation_reply_mailer_spec.rb @@ -44,6 +44,7 @@ RSpec.describe ConversationReplyMailer do bcc_emails: 'agent_bcc1@example.com' }) end + let(:private_message) { create(:message, account: account, content: 'This is a private message', conversation: conversation) } let(:mail) { described_class.reply_with_summary(message.conversation, message.id).deliver_now } let(:cc_mail) { described_class.reply_with_summary(cc_message.conversation, message.id).deliver_now } @@ -186,13 +187,84 @@ RSpec.describe ConversationReplyMailer do expect(mail['from'].value).to eq "#{conversation.assignee.available_name} from #{smtp_email_channel.inbox.name} <#{smtp_email_channel.email}>" end - it 'renders inbox name as sender and assignee not present' do + it 'renders inbox name as sender and assignee or business_name not present' do message.update(sender_id: nil) conversation.update(assignee_id: nil) mail = described_class.email_reply(message) expect(mail['from'].value).to eq "Notifications from #{smtp_email_channel.inbox.name} <#{smtp_email_channel.email}>" end + + context 'when friendly name enabled' do + before do + conversation.inbox.update(sender_name_type: 0) + conversation.inbox.update(business_name: 'Business Name') + end + + it 'renders sender name as sender and assignee and business_name not present' do + message.update(sender_id: nil) + conversation.update(assignee_id: nil) + conversation.inbox.update(business_name: nil) + + mail = described_class.email_reply(message) + + expect(mail['from'].value).to eq "Notifications from #{conversation.inbox.name} <#{smtp_email_channel.email}>" + end + + it 'renders sender name as sender and assignee nil and business_name present' do + message.update(sender_id: nil) + conversation.update(assignee_id: nil) + + mail = described_class.email_reply(message) + + expect(mail['from'].value).to eq( + "Notifications from #{conversation.inbox.business_name} <#{smtp_email_channel.email}>" + ) + end + + it 'renders sender name as sender nil and assignee and business_name present' do + message.update(sender_id: nil) + conversation.update(assignee_id: agent.id) + + mail = described_class.email_reply(message) + expect(mail['from'].value).to eq "#{agent.available_name} from #{conversation.inbox.business_name} <#{smtp_email_channel.email}>" + end + + it 'renders sender name as sender and assignee and business_name present' do + agent_2 = create(:user, email: 'agent2@example.com', account: account) + message.update(sender_id: agent_2.id) + conversation.update(assignee_id: agent.id) + + mail = described_class.email_reply(message) + expect(mail['from'].value).to eq "#{agent_2.available_name} from #{conversation.inbox.business_name} <#{smtp_email_channel.email}>" + end + end + + context 'when friendly name disabled' do + before do + conversation.inbox.update(sender_name_type: 1) + conversation.inbox.update(business_name: 'Business Name') + end + + it 'renders sender name as business_name not present' do + message.update(sender_id: nil) + conversation.update(assignee_id: nil) + conversation.inbox.update(business_name: nil) + + mail = described_class.email_reply(message) + + expect(mail['from'].value).to eq "#{conversation.inbox.name} <#{smtp_email_channel.email}>" + end + + it 'renders sender name as business_name present' do + message.update(sender_id: nil) + conversation.update(assignee_id: nil) + + mail = described_class.email_reply(message) + + expect(mail['from'].value).to eq "#{conversation.inbox.business_name} <#{smtp_email_channel.email}>" + end + end end context 'when smtp enabled for microsoft email channel' do