From 22880df42944b1da858e09258edd279075bdce82 Mon Sep 17 00:00:00 2001 From: Sojan Jose Date: Mon, 17 Aug 2020 11:25:13 +0530 Subject: [PATCH] Feature: Send chat transcript via email (#1152) Co-authored-by: Pranav Raj Sreepuram --- .../v1/accounts/conversations_controller.rb | 5 + .../api/v1/widget/conversations_controller.rb | 12 +- .../dashboard/api/inbox/conversation.js | 4 + .../api/specs/inbox/conversation.spec.js | 1 + .../dashboard/assets/scss/_mixins.scss | 9 ++ .../assets/scss/widgets/_conv-header.scss | 3 +- .../{ResolveButton.vue => ResolveAction.vue} | 23 ++- app/javascript/dashboard/components/index.js | 2 + .../dashboard/components/widgets/Button.vue | 49 ++++++ .../widgets/conversation/ConversationBox.vue | 4 +- .../conversation/ConversationHeader.vue | 34 ++-- .../conversation/EmailTranscriptModal.vue | 153 ++++++++++++++++++ .../widgets/conversation/MessagesView.vue | 4 +- .../widgets/conversation/MoreActions.vue | 129 +++++++++++++++ .../dashboard/i18n/locale/en/contact.json | 4 +- .../i18n/locale/en/conversation.json | 17 ++ .../dashboard/i18n/locale/fr/contact.json | 2 + .../i18n/locale/fr/conversation.json | 17 ++ .../dashboard/i18n/locale/nl/contact.json | 4 +- .../i18n/locale/nl/conversation.json | 17 ++ .../dashboard/conversation/ContactPanel.vue | 18 +-- .../conversation/ConversationView.vue | 3 +- .../store/modules/conversations/actions.js | 8 + .../specs/conversations/actions.spec.js | 12 ++ app/mailers/conversation_reply_mailer.rb | 15 +- .../conversation_transcript.html.erb | 20 +++ .../reply_with_summary.html.erb | 2 +- config/locales/en.yml | 1 + config/routes.rb | 2 + .../accounts/conversations_controller_spec.rb | 28 ++++ .../widget/conversations_controller_spec.rb | 16 ++ 31 files changed, 559 insertions(+), 59 deletions(-) rename app/javascript/dashboard/components/buttons/{ResolveButton.vue => ResolveAction.vue} (68%) create mode 100644 app/javascript/dashboard/components/widgets/Button.vue create mode 100644 app/javascript/dashboard/components/widgets/conversation/EmailTranscriptModal.vue create mode 100644 app/javascript/dashboard/components/widgets/conversation/MoreActions.vue create mode 100644 app/views/mailers/conversation_reply_mailer/conversation_transcript.html.erb diff --git a/app/controllers/api/v1/accounts/conversations_controller.rb b/app/controllers/api/v1/accounts/conversations_controller.rb index 74f206e00..51b6aa079 100644 --- a/app/controllers/api/v1/accounts/conversations_controller.rb +++ b/app/controllers/api/v1/accounts/conversations_controller.rb @@ -25,6 +25,11 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro head :ok end + def transcript + ConversationReplyMailer.conversation_transcript(@conversation, params[:email])&.deliver_later if params[:email].present? + head :ok + end + def toggle_status if params[:status] @conversation.status = params[:status] diff --git a/app/controllers/api/v1/widget/conversations_controller.rb b/app/controllers/api/v1/widget/conversations_controller.rb index c8d3b64a7..66b54d4b9 100644 --- a/app/controllers/api/v1/widget/conversations_controller.rb +++ b/app/controllers/api/v1/widget/conversations_controller.rb @@ -13,6 +13,16 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController head :ok end + def transcript + if permitted_params[:email].present? && conversation.present? + ConversationReplyMailer.conversation_transcript( + conversation, + permitted_params[:email] + )&.deliver_later + end + head :ok + end + def toggle_typing head :ok && return if conversation.nil? @@ -32,6 +42,6 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController end def permitted_params - params.permit(:id, :typing_status, :website_token) + params.permit(:id, :typing_status, :website_token, :email) end end diff --git a/app/javascript/dashboard/api/inbox/conversation.js b/app/javascript/dashboard/api/inbox/conversation.js index 5a9173a7a..fa23fda66 100644 --- a/app/javascript/dashboard/api/inbox/conversation.js +++ b/app/javascript/dashboard/api/inbox/conversation.js @@ -53,6 +53,10 @@ class ConversationApi extends ApiClient { }, }); } + + sendEmailTranscript({ conversationId, email }) { + return axios.post(`${this.url}/${conversationId}/transcript`, { email }); + } } export default new ConversationApi(); diff --git a/app/javascript/dashboard/api/specs/inbox/conversation.spec.js b/app/javascript/dashboard/api/specs/inbox/conversation.spec.js index 4eceba728..ddea6b874 100644 --- a/app/javascript/dashboard/api/specs/inbox/conversation.spec.js +++ b/app/javascript/dashboard/api/specs/inbox/conversation.spec.js @@ -15,5 +15,6 @@ describe('#ConversationAPI', () => { expect(conversationAPI).toHaveProperty('toggleTyping'); expect(conversationAPI).toHaveProperty('mute'); expect(conversationAPI).toHaveProperty('meta'); + expect(conversationAPI).toHaveProperty('sendEmailTranscript'); }); }); diff --git a/app/javascript/dashboard/assets/scss/_mixins.scss b/app/javascript/dashboard/assets/scss/_mixins.scss index 9bf19e1c2..36a87a4f9 100644 --- a/app/javascript/dashboard/assets/scss/_mixins.scss +++ b/app/javascript/dashboard/assets/scss/_mixins.scss @@ -235,3 +235,12 @@ $spinner-before-border-color: rgba(255, 255, 255, 0.7); text-overflow: ellipsis; white-space: nowrap; } + + +.justify-space-between { + justify-content: space-between; +} + +.w-100 { + width: 100%; +} diff --git a/app/javascript/dashboard/assets/scss/widgets/_conv-header.scss b/app/javascript/dashboard/assets/scss/widgets/_conv-header.scss index e1aef9307..878e5a788 100644 --- a/app/javascript/dashboard/assets/scss/widgets/_conv-header.scss +++ b/app/javascript/dashboard/assets/scss/widgets/_conv-header.scss @@ -15,10 +15,9 @@ .multiselect-box { @include flex; @include flex-align($x: justify, $y: middle); - @include margin(0 $space-small); @include border-light; border-radius: $space-smaller; - margin-right: $space-normal; + margin-right: var(--space-small); &::before { color: $medium-gray; diff --git a/app/javascript/dashboard/components/buttons/ResolveButton.vue b/app/javascript/dashboard/components/buttons/ResolveAction.vue similarity index 68% rename from app/javascript/dashboard/components/buttons/ResolveButton.vue rename to app/javascript/dashboard/components/buttons/ResolveAction.vue index 9b1181426..8d99d3ce3 100644 --- a/app/javascript/dashboard/components/buttons/ResolveButton.vue +++ b/app/javascript/dashboard/components/buttons/ResolveAction.vue @@ -12,8 +12,6 @@ + + diff --git a/app/javascript/dashboard/components/widgets/conversation/ConversationBox.vue b/app/javascript/dashboard/components/widgets/conversation/ConversationBox.vue index 6d46af8a1..2e0a89cb2 100644 --- a/app/javascript/dashboard/components/widgets/conversation/ConversationBox.vue +++ b/app/javascript/dashboard/components/widgets/conversation/ConversationBox.vue @@ -4,7 +4,7 @@ v-if="currentChat.id" :inbox-id="inboxId" :is-contact-panel-open="isContactPanelOpen" - @contactPanelToggle="onToggleContactPanel" + @contact-panel-toggle="onToggleContactPanel" /> @@ -43,7 +43,7 @@ export default { }, methods: { onToggleContactPanel() { - this.$emit('contactPanelToggle'); + this.$emit('contact-panel-toggle'); }, }, }; diff --git a/app/javascript/dashboard/components/widgets/conversation/ConversationHeader.vue b/app/javascript/dashboard/components/widgets/conversation/ConversationHeader.vue index c5ce3d0be..c11612376 100644 --- a/app/javascript/dashboard/components/widgets/conversation/ConversationHeader.vue +++ b/app/javascript/dashboard/components/widgets/conversation/ConversationHeader.vue @@ -1,6 +1,6 @@ diff --git a/app/javascript/dashboard/components/widgets/conversation/MessagesView.vue b/app/javascript/dashboard/components/widgets/conversation/MessagesView.vue index 2689fe550..5376b1bc4 100644 --- a/app/javascript/dashboard/components/widgets/conversation/MessagesView.vue +++ b/app/javascript/dashboard/components/widgets/conversation/MessagesView.vue @@ -3,7 +3,7 @@ -
- -
@@ -295,11 +286,12 @@ export default { .contact--mute { color: $alert-color; display: block; - text-align: center; + text-align: left; } .contact--actions { display: flex; + flex-direction: column; justify-content: center; } diff --git a/app/javascript/dashboard/routes/dashboard/conversation/ConversationView.vue b/app/javascript/dashboard/routes/dashboard/conversation/ConversationView.vue index e073965f6..5a9e76646 100644 --- a/app/javascript/dashboard/routes/dashboard/conversation/ConversationView.vue +++ b/app/javascript/dashboard/routes/dashboard/conversation/ConversationView.vue @@ -4,7 +4,7 @@ /* eslint no-console: 0 */ -/* global bus */ import { mapGetters } from 'vuex'; import ChatList from '../../../components/ChatList'; diff --git a/app/javascript/dashboard/store/modules/conversations/actions.js b/app/javascript/dashboard/store/modules/conversations/actions.js index e4d33f805..c1ebf50a5 100644 --- a/app/javascript/dashboard/store/modules/conversations/actions.js +++ b/app/javascript/dashboard/store/modules/conversations/actions.js @@ -223,6 +223,14 @@ const actions = { // } }, + + sendEmailTranscript: async (_, { conversationId, email }) => { + try { + await ConversationApi.sendEmailTranscript({ conversationId, email }); + } catch (error) { + throw new Error(error); + } + }, }; export default actions; diff --git a/app/javascript/dashboard/store/modules/specs/conversations/actions.spec.js b/app/javascript/dashboard/store/modules/specs/conversations/actions.spec.js index 2c470207d..666f5fa4b 100644 --- a/app/javascript/dashboard/store/modules/specs/conversations/actions.spec.js +++ b/app/javascript/dashboard/store/modules/specs/conversations/actions.spec.js @@ -177,4 +177,16 @@ describe('#actions', () => { expect(commit.mock.calls).toEqual([]); }); }); + + describe('#sendEmailTranscript', () => { + it('sends correct mutations if api is successful', async () => { + axios.post.mockResolvedValue({}); + await actions.sendEmailTranscript( + { commit }, + { conversationId: 1, email: 'testemail@example.com' } + ); + expect(commit).toHaveBeenCalledTimes(0); + expect(commit.mock.calls).toEqual([]); + }); + }); }); diff --git a/app/mailers/conversation_reply_mailer.rb b/app/mailers/conversation_reply_mailer.rb index 5129cb1ed..a56c2bb63 100644 --- a/app/mailers/conversation_reply_mailer.rb +++ b/app/mailers/conversation_reply_mailer.rb @@ -9,7 +9,6 @@ class ConversationReplyMailer < ApplicationMailer recap_messages = @conversation.messages.chat.where('created_at < ?', message_queued_time).last(10) new_messages = @conversation.messages.chat.where('created_at >= ?', message_queued_time) - @messages = recap_messages + new_messages @messages = @messages.select(&:reportable?) @@ -41,6 +40,20 @@ class ConversationReplyMailer < ApplicationMailer }) end + def conversation_transcript(conversation, to_email) + return unless smtp_config_set_or_development? + + init_conversation_attributes(conversation) + + @messages = @conversation.messages.chat.select(&:reportable?) + + mail({ + to: to_email, + from: from_email, + subject: "[##{@conversation.display_id}] #{I18n.t('conversations.reply.transcript_subject')}" + }) + end + private def init_conversation_attributes(conversation) diff --git a/app/views/mailers/conversation_reply_mailer/conversation_transcript.html.erb b/app/views/mailers/conversation_reply_mailer/conversation_transcript.html.erb new file mode 100644 index 000000000..825d5ce3f --- /dev/null +++ b/app/views/mailers/conversation_reply_mailer/conversation_transcript.html.erb @@ -0,0 +1,20 @@ +<% @messages.each do |message| %> + + + <%= message.sender&.try(:available_name) || message.sender&.name || '' %> + + + + + <% if message.content %> + <%= message.content %> + <% end %> + <% if message.attachments %> + <% message.attachments.each do |attachment| %> + Attachment [Click here to view] + <% end %> + <% end %> +
+ + +<% end %> diff --git a/app/views/mailers/conversation_reply_mailer/reply_with_summary.html.erb b/app/views/mailers/conversation_reply_mailer/reply_with_summary.html.erb index b99ad9d1d..31648f2b8 100644 --- a/app/views/mailers/conversation_reply_mailer/reply_with_summary.html.erb +++ b/app/views/mailers/conversation_reply_mailer/reply_with_summary.html.erb @@ -15,7 +15,7 @@ <% end %> <% if message.attachments %> <% message.attachments.each do |attachment| %> - attachment [click here to view] + Attachment [Click here to view] <% end %> <% end %> diff --git a/config/locales/en.yml b/config/locales/en.yml index f1852aa0c..e6f65e32d 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -56,3 +56,4 @@ en: email_input_box_message_body: "Get notified by email" reply: email_subject: "New messages on this conversation" + transcript_subject: "Conversation Transcript" diff --git a/config/routes.rb b/config/routes.rb index df64f88d4..e41b86f54 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -54,6 +54,7 @@ Rails.application.routes.draw do end member do post :mute + post :transcript post :toggle_status post :toggle_typing_status post :update_last_seen @@ -117,6 +118,7 @@ Rails.application.routes.draw do collection do post :update_last_seen post :toggle_typing + post :transcript end end resource :contact, only: [:update] diff --git a/spec/controllers/api/v1/accounts/conversations_controller_spec.rb b/spec/controllers/api/v1/accounts/conversations_controller_spec.rb index 17ad27118..41fab769f 100644 --- a/spec/controllers/api/v1/accounts/conversations_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/conversations_controller_spec.rb @@ -212,4 +212,32 @@ RSpec.describe 'Conversations API', type: :request do end end end + + describe 'POST /api/v1/accounts/{account.id}/conversations/:id/transcript' 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}/transcript" + + 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(:params) { { email: 'test@test.com' } } + + it 'mutes conversation' do + allow(ConversationReplyMailer).to receive(:conversation_transcript) + post "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/transcript", + headers: agent.create_new_auth_token, + params: params, + as: :json + + expect(response).to have_http_status(:success) + expect(ConversationReplyMailer).to have_received(:conversation_transcript).with(conversation, 'test@test.com') + end + end + end end diff --git a/spec/controllers/api/v1/widget/conversations_controller_spec.rb b/spec/controllers/api/v1/widget/conversations_controller_spec.rb index 8056b552f..89ce60734 100644 --- a/spec/controllers/api/v1/widget/conversations_controller_spec.rb +++ b/spec/controllers/api/v1/widget/conversations_controller_spec.rb @@ -60,4 +60,20 @@ RSpec.describe '/api/v1/widget/conversations/toggle_typing', type: :request do end end end + + describe 'POST /api/v1/widget/conversations/transcript' do + context 'with a conversation' do + it 'sends transcript email' do + allow(ConversationReplyMailer).to receive(:conversation_transcript) + + post '/api/v1/widget/conversations/transcript', + headers: { 'X-Auth-Token' => token }, + params: { website_token: web_widget.website_token, email: 'test@test.com' }, + as: :json + + expect(response).to have_http_status(:success) + expect(ConversationReplyMailer).to have_received(:conversation_transcript).with(conversation, 'test@test.com') + end + end + end end