From 04bce69dedcd7cb4e3d44a863c4cda9b82f9bdeb Mon Sep 17 00:00:00 2001 From: Sylvain Dumont Date: Tue, 11 Aug 2020 18:34:06 +0200 Subject: [PATCH 01/16] Bugfix: add missing option to s3_compatible to support minio (#1134) minio doesn't work without force_path_style more info here: https://kevinjalbert.com/rails-activestorage-configuration-for-minio/ --- config/storage.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/config/storage.yml b/config/storage.yml index ba053d00e..437f55d42 100644 --- a/config/storage.yml +++ b/config/storage.yml @@ -36,6 +36,7 @@ s3_compatible: region: <%= ENV.fetch('STORAGE_REGION', '') %> bucket: <%= ENV.fetch('STORAGE_BUCKET_NAME', '') %> endpoint: <%= ENV.fetch('STORAGE_ENDPOINT', '') %> + force_path_style: <%= ENV.fetch('STORAGE_FORCE_PATH_STYLE', false) %> # mirror: # service: Mirror From 0117997fb1a9188d79b16a142fb5f360290e8650 Mon Sep 17 00:00:00 2001 From: Pranav Raj S Date: Thu, 13 Aug 2020 15:06:32 +0530 Subject: [PATCH 02/16] chore: Add docs on customizing email template content (#1141) --- docs/customizations/email-notifications.md | 66 ++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 docs/customizations/email-notifications.md diff --git a/docs/customizations/email-notifications.md b/docs/customizations/email-notifications.md new file mode 100644 index 000000000..3c96fb396 --- /dev/null +++ b/docs/customizations/email-notifications.md @@ -0,0 +1,66 @@ +--- +path: "/docs/customizations/email-notifications" +title: "Customize email notifications in Chatwoot" +--- + +Chatwoot allows customization of email notifications in self hosted installations. + +To customize the email notifications, follow the instructions below. Inorder to update the content, you have to add a new template in the Database, here is how you can do it. + +### 1. Login into the rails console. + +For Heroku installation, login to your account, go to the app. Click on "More", select "Run Console" from the dropdown menu. Enter the following command and hit run + +```rb +heroku run rails console +``` + +For Linux VM installations, go to directory where Chatwoot code is available. If you have used the installation script, the default path is `/home/chatwoot/chatwoot`. Run the following command. + +```rb +RAILS_ENV=production bundle exec rails console +``` + +### 2. Create a new template for the emails. Execute the following commands. + +```rb +email_template = EmailTemplate.new +email_template.name = 'conversation_assignment' # Accepts conversation_assignment, conversation_creation +email_template.body = '// Enter your content' +email_template.save! +``` + +#### Variables + +Template would receive 3 variable + +1. `user` - Use `{{ user.name }}` to get the username. +2. `conversation` - Use `{{ conversation.display_id }}` to get the conversation ID +3. `action_url` - This is the URL of the conversation. + +### Default content + +Default content of the above template is as shown below + +#### 1. Conversation Assignment + +```html +

Hi {{user.available_name}},

+

Time to save the world. A new conversation has been assigned to you

+

Click here to get cracking.

+``` + +#### 2. Conversation Creation + +```html +

Hi {{user.available_name}}

+ +

Time to save the world. A new conversation has been created in {{ inbox.name }}

+

+Click here to get cracking. +

+``` + +We use [Liquid templating engine](https://shopify.github.io/liquid/) internally, so all valid operators can be used here. + + From be91b322a43ae75c5ad011728611c72874a93a00 Mon Sep 17 00:00:00 2001 From: Sojan Jose Date: Fri, 14 Aug 2020 00:47:24 +0530 Subject: [PATCH 03/16] chore: Enable additional attributes in liquid (#1144) enable additional liquid variables in mailers Co-authored-by: Pranav Raj Sreepuram --- app/drops/user_drop.rb | 3 +++ app/mailers/application_mailer.rb | 5 ++++- app/mailers/conversation_reply_mailer.rb | 17 +++++++++-------- app/views/layouts/mailer/base.liquid | 4 ++-- 4 files changed, 18 insertions(+), 11 deletions(-) diff --git a/app/drops/user_drop.rb b/app/drops/user_drop.rb index e2876a58a..f10eee131 100644 --- a/app/drops/user_drop.rb +++ b/app/drops/user_drop.rb @@ -1,2 +1,5 @@ class UserDrop < BaseDrop + def available_name + @obj.try(:available_name) + end end diff --git a/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb index c5ce00349..72954f441 100644 --- a/app/mailers/application_mailer.rb +++ b/app/mailers/application_mailer.rb @@ -35,7 +35,10 @@ class ApplicationMailer < ActionMailer::Base # Merge additional objects into this in your mailer # liquid template handler converts these objects into drop objects { - account: Current.account + account: Current.account, + user: @agent, + conversation: @conversation, + inbox: @conversation&.inbox } end diff --git a/app/mailers/conversation_reply_mailer.rb b/app/mailers/conversation_reply_mailer.rb index c60fb4363..5129cb1ed 100644 --- a/app/mailers/conversation_reply_mailer.rb +++ b/app/mailers/conversation_reply_mailer.rb @@ -5,10 +5,7 @@ class ConversationReplyMailer < ApplicationMailer def reply_with_summary(conversation, message_queued_time) return unless smtp_config_set_or_development? - @conversation = conversation - @account = @conversation.account - @contact = @conversation.contact - @agent = @conversation.assignee + init_conversation_attributes(conversation) recap_messages = @conversation.messages.chat.where('created_at < ?', message_queued_time).last(10) new_messages = @conversation.messages.chat.where('created_at >= ?', message_queued_time) @@ -29,10 +26,7 @@ class ConversationReplyMailer < ApplicationMailer def reply_without_summary(conversation, message_queued_time) return unless smtp_config_set_or_development? - @conversation = conversation - @account = @conversation.account - @contact = @conversation.contact - @agent = @conversation.assignee + init_conversation_attributes(conversation) @messages = @conversation.messages.chat.outgoing.where('created_at >= ?', message_queued_time) return false if @messages.count.zero? @@ -49,6 +43,13 @@ class ConversationReplyMailer < ApplicationMailer private + def init_conversation_attributes(conversation) + @conversation = conversation + @account = @conversation.account + @contact = @conversation.contact + @agent = @conversation.assignee + end + def assignee_name @assignee_name ||= @agent&.available_name || 'Notifications' end diff --git a/app/views/layouts/mailer/base.liquid b/app/views/layouts/mailer/base.liquid index 6eea14f4e..e836cdbb6 100644 --- a/app/views/layouts/mailer/base.liquid +++ b/app/views/layouts/mailer/base.liquid @@ -89,7 +89,7 @@ @@ -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 From dfe4b70d91d92e2b57450b860e786a1c0fbb3df3 Mon Sep 17 00:00:00 2001 From: Francois Falala-Sechet Date: Mon, 17 Aug 2020 09:37:12 +0200 Subject: [PATCH 08/16] chore: update swagger definitions (#1151) update swagger definitions --- .../definitions/request/contact/create.yml | 9 ++++-- .../request/conversation/create_message.yml | 7 ++--- swagger/paths/contact/list_create.yml | 1 + swagger/swagger.json | 28 +++++++++++++------ 4 files changed, 30 insertions(+), 15 deletions(-) diff --git a/swagger/definitions/request/contact/create.yml b/swagger/definitions/request/contact/create.yml index 6e83a12fa..5dc481a76 100644 --- a/swagger/definitions/request/contact/create.yml +++ b/swagger/definitions/request/contact/create.yml @@ -1,6 +1,11 @@ type: object properties: - account_id: - type: number inbox_id: type: number + required: true + name: + type: string + email: + type: string + phone_number: + type: string diff --git a/swagger/definitions/request/conversation/create_message.yml b/swagger/definitions/request/conversation/create_message.yml index caa5dcd36..faecf278f 100644 --- a/swagger/definitions/request/conversation/create_message.yml +++ b/swagger/definitions/request/conversation/create_message.yml @@ -1,13 +1,12 @@ type: object properties: - conversation_id: - type: number - description: ID of the conversation - required: true content: type: string description: The content of the message required: true + message_type: + type: string + enum: ['outgoing', 'incoming'] private: type: boolean description: Flag to identify if it is a private note diff --git a/swagger/paths/contact/list_create.yml b/swagger/paths/contact/list_create.yml index eed02018f..28689b78d 100644 --- a/swagger/paths/contact/list_create.yml +++ b/swagger/paths/contact/list_create.yml @@ -23,6 +23,7 @@ post: - Contact operationId: contactCreate description: Create New Contact + summary: Create Contact parameters: - name: data in: body diff --git a/swagger/swagger.json b/swagger/swagger.json index 9fc9a31df..8af12d335 100644 --- a/swagger/swagger.json +++ b/swagger/swagger.json @@ -767,6 +767,7 @@ ], "operationId": "contactCreate", "description": "Create New Contact", + "summary": "Create Contact", "parameters": [ { "name": "data", @@ -1446,11 +1447,18 @@ "contact_create": { "type": "object", "properties": { - "account_id": { - "type": "number" - }, "inbox_id": { - "type": "number" + "type": "number", + "required": true + }, + "name": { + "type": "string" + }, + "email": { + "type": "string" + }, + "phone_number": { + "type": "string" } } }, @@ -1471,16 +1479,18 @@ "conversation_message_create": { "type": "object", "properties": { - "conversation_id": { - "type": "number", - "description": "ID of the conversation", - "required": true - }, "content": { "type": "string", "description": "The content of the message", "required": true }, + "message_type": { + "type": "string", + "enum": [ + "outgoing", + "incoming" + ] + }, "private": { "type": "boolean", "description": "Flag to identify if it is a private note" From 662bb882f76a2cc3fed699ec28bf4aec1f3ca89c Mon Sep 17 00:00:00 2001 From: Pranav Raj S Date: Tue, 18 Aug 2020 12:14:37 +0530 Subject: [PATCH 09/16] chore: Update API docs (#1153) --- swagger/definitions/index.yml | 5 + swagger/definitions/resource/account.yml | 12 ++ swagger/definitions/resource/contact.yml | 4 + .../definitions/resource/contact_inboxes.yml | 8 + swagger/definitions/resource/inbox.yml | 7 +- swagger/definitions/resource/user.yml | 8 + swagger/paths/contact/search.yml | 22 +++ swagger/paths/contact_inboxes/create.yml | 21 +++ swagger/paths/index.yml | 14 ++ swagger/paths/profile/index.yml | 13 ++ swagger/swagger.json | 151 +++++++++++++++++- 11 files changed, 259 insertions(+), 6 deletions(-) create mode 100644 swagger/definitions/resource/account.yml create mode 100644 swagger/definitions/resource/contact_inboxes.yml create mode 100644 swagger/paths/contact/search.yml create mode 100644 swagger/paths/contact_inboxes/create.yml create mode 100644 swagger/paths/profile/index.yml diff --git a/swagger/definitions/index.yml b/swagger/definitions/index.yml index b822c9415..9e09d3332 100644 --- a/swagger/definitions/index.yml +++ b/swagger/definitions/index.yml @@ -19,6 +19,11 @@ inbox: $ref: ./resource/inbox.yml agent_bot: $ref: ./resource/agent_bot.yml +contact_inboxes: + $ref: ./resource/contact_inboxes.yml +account: + $ref: ./resource/account.yml + # RESPONSE ## contact diff --git a/swagger/definitions/resource/account.yml b/swagger/definitions/resource/account.yml new file mode 100644 index 000000000..789c8451c --- /dev/null +++ b/swagger/definitions/resource/account.yml @@ -0,0 +1,12 @@ +type: object +properties: + id: + type: number + description: Account ID + name: + type: string + description: Name of the account + role: + type: string + enum: ['administrator', 'agent'] + description: The user role in the account diff --git a/swagger/definitions/resource/contact.yml b/swagger/definitions/resource/contact.yml index be407b20b..e2462c08b 100644 --- a/swagger/definitions/resource/contact.yml +++ b/swagger/definitions/resource/contact.yml @@ -15,3 +15,7 @@ properties: additional_attributes: type: object description: The object containing additional attributes related to the contact + contact_inboxes: + type: array + items: + $ref: '#/definitions/contact_inboxes' diff --git a/swagger/definitions/resource/contact_inboxes.yml b/swagger/definitions/resource/contact_inboxes.yml new file mode 100644 index 000000000..a223490ff --- /dev/null +++ b/swagger/definitions/resource/contact_inboxes.yml @@ -0,0 +1,8 @@ +type: object +properties: + source_id: + type: string + description: Contact Inbox Source Id + inbox: + type: object + $ref: '#/definitions/inbox' diff --git a/swagger/definitions/resource/inbox.yml b/swagger/definitions/resource/inbox.yml index 92a57d1a0..bba46ea34 100644 --- a/swagger/definitions/resource/inbox.yml +++ b/swagger/definitions/resource/inbox.yml @@ -33,6 +33,9 @@ properties: welcome_tagline: type: string description: Welcome tagline to be displayed on the widget - agent_away_message: + greeting_enabled: + type: boolean + description: The flag which shows whether greeting is enabled + greeting_message: type: string - description: A message which will be sent if there is not agent available. This is not available if agentbot is connected + description: A greeting message when the user starts the conversation diff --git a/swagger/definitions/resource/user.yml b/swagger/definitions/resource/user.yml index ecdcbe30a..cbffd7d68 100644 --- a/swagger/definitions/resource/user.yml +++ b/swagger/definitions/resource/user.yml @@ -6,6 +6,10 @@ properties: type: string name: type: string + available_name: + type: string + display_name: + type: string email: type: string account_id: @@ -17,3 +21,7 @@ properties: type: boolean display_name: type: string + accounts: + type: array + items: + $ref: '#/definitions/account' diff --git a/swagger/paths/contact/search.yml b/swagger/paths/contact/search.yml new file mode 100644 index 000000000..ad17fd8e1 --- /dev/null +++ b/swagger/paths/contact/search.yml @@ -0,0 +1,22 @@ +get: + tags: + - Contact + operationId: contactSearch + description: Search the contacts using a search key, currently supports email search + summary: Search Contacts + parameters: + - name: q + in: query + type: string + responses: + 200: + description: Success + schema: + type: object + properties: + payload: + $ref: '#/definitions/contact_list' + 401: + description: Authentication error + schema: + $ref: '#/definitions/bad_request_error' diff --git a/swagger/paths/contact_inboxes/create.yml b/swagger/paths/contact_inboxes/create.yml new file mode 100644 index 000000000..ec28c1a87 --- /dev/null +++ b/swagger/paths/contact_inboxes/create.yml @@ -0,0 +1,21 @@ +post: + tags: + - Contact + operationId: contactInboxCreation + description: Create a contact inbox record for an inbox + summary: Create contact inbox + parameters: + - name: inbox_id + in: params + type: number + responses: + 200: + description: Success + schema: + $ref: '#/definitions/contact_inboxes' + 401: + description: Authentication error + schema: + $ref: '#/definitions/bad_request_error' + 422: + description: Incorrect payload diff --git a/swagger/paths/index.yml b/swagger/paths/index.yml index d1959a21e..d31f88e4a 100644 --- a/swagger/paths/index.yml +++ b/swagger/paths/index.yml @@ -1,5 +1,6 @@ + # Inboxes /accounts/{account_id}/inboxes: $ref: ./inboxes/index.yml @@ -34,7 +35,20 @@ # Contacts /accounts/{account_id}/contacts: $ref: ./contact/list_create.yml + /accounts/{account_id}/contacts/{id}: $ref: ./contact/crud.yml + /accounts/{account_id}/contacts/{id}/conversations: $ref: ./contact/conversations.yml + +/accounts/{account_id}/contacts/search: + $ref: ./contact/search.yml + +/accounts/{account_id}/contacts/{id}/contact_inboxes: + $ref: ./contact_inboxes/create.yml + + +# Profile +/profile: + $ref: ./profile/index.yml diff --git a/swagger/paths/profile/index.yml b/swagger/paths/profile/index.yml new file mode 100644 index 000000000..e20004cac --- /dev/null +++ b/swagger/paths/profile/index.yml @@ -0,0 +1,13 @@ +get: + tags: + - Profile + operationId: fetchProfile + summary: Fetch user profile + description: Get the user profile details + responses: + 200: + description: Success + schema: + $ref: '#/definitions/user' + 401: + description: Unauthorized diff --git a/swagger/swagger.json b/swagger/swagger.json index 8af12d335..cc8b31482 100644 --- a/swagger/swagger.json +++ b/swagger/swagger.json @@ -895,6 +895,97 @@ } } } + }, + "/accounts/{account_id}/contacts/search": { + "get": { + "tags": [ + "Contact" + ], + "operationId": "contactSearch", + "description": "Search the contacts using a search key, currently supports email search", + "summary": "Search Contacts", + "parameters": [ + { + "name": "q", + "in": "query", + "type": "string" + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "type": "object", + "properties": { + "payload": { + "$ref": "#/definitions/contact_list" + } + } + } + }, + "401": { + "description": "Authentication error", + "schema": { + "$ref": "#/definitions/bad_request_error" + } + } + } + } + }, + "/accounts/{account_id}/contacts/{id}/contact_inboxes": { + "post": { + "tags": [ + "Contact" + ], + "operationId": "contactInboxCreation", + "description": "Create a contact inbox record for an inbox", + "summary": "Create contact inbox", + "parameters": [ + { + "name": "inbox_id", + "in": "params", + "type": "number" + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/contact_inboxes" + } + }, + "401": { + "description": "Authentication error", + "schema": { + "$ref": "#/definitions/bad_request_error" + } + }, + "422": { + "description": "Incorrect payload" + } + } + } + }, + "/profile": { + "get": { + "tags": [ + "Profile" + ], + "operationId": "fetchProfile", + "summary": "Fetch user profile", + "description": "Get the user profile details", + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/user" + } + }, + "401": { + "description": "Unauthorized" + } + } + } } }, "definitions": { @@ -957,6 +1048,12 @@ "additional_attributes": { "type": "object", "description": "The object containing additional attributes related to the contact" + }, + "contact_inboxes": { + "type": "array", + "items": { + "$ref": "#/definitions/contact_inboxes" + } } } }, @@ -1072,6 +1169,12 @@ "name": { "type": "string" }, + "available_name": { + "type": "string" + }, + "display_name": { + "type": "string" + }, "email": { "type": "string" }, @@ -1088,8 +1191,11 @@ "confirmed": { "type": "boolean" }, - "display_name": { - "type": "string" + "accounts": { + "type": "array", + "items": { + "$ref": "#/definitions/account" + } } } }, @@ -1140,9 +1246,13 @@ "type": "string", "description": "Welcome tagline to be displayed on the widget" }, - "agent_away_message": { + "greeting_enabled": { + "type": "boolean", + "description": "The flag which shows whether greeting is enabled" + }, + "greeting_message": { "type": "string", - "description": "A message which will be sent if there is not agent available. This is not available if agentbot is connected" + "description": "A greeting message when the user starts the conversation" } } }, @@ -1167,6 +1277,39 @@ } } }, + "contact_inboxes": { + "type": "object", + "properties": { + "source_id": { + "type": "string", + "description": "Contact Inbox Source Id" + }, + "inbox": { + "$ref": "#/definitions/inbox" + } + } + }, + "account": { + "type": "object", + "properties": { + "id": { + "type": "number", + "description": "Account ID" + }, + "name": { + "type": "string", + "description": "Name of the account" + }, + "role": { + "type": "string", + "enum": [ + "administrator", + "agent" + ], + "description": "The user role in the account" + } + } + }, "extended_contact": { "allOf": [ { From e8912655a5bcd15a931303bdb9209e4647947a4e Mon Sep 17 00:00:00 2001 From: Pranav Raj S Date: Wed, 19 Aug 2020 22:24:02 +0530 Subject: [PATCH 10/16] feat: Allow user to change the bubble title using SDK (#1155) - Allow user to change the bubble title using SDK - Add docs for enabling expanded bubble --- app/javascript/packs/sdk.js | 1 + app/javascript/sdk/IFrameHelper.js | 2 +- docs/channels/website-sdk.md | 9 +++++++++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/app/javascript/packs/sdk.js b/app/javascript/packs/sdk.js index 8c7c97dee..9fb4d1976 100755 --- a/app/javascript/packs/sdk.js +++ b/app/javascript/packs/sdk.js @@ -13,6 +13,7 @@ const runSDK = ({ baseUrl, websiteToken }) => { websiteToken, locale: chatwootSettings.locale, type: getBubbleView(chatwootSettings.type), + launcherTitle: window.chatwootSettings.launcherTitle || '', toggle() { IFrameHelper.events.toggleBubble(); diff --git a/app/javascript/sdk/IFrameHelper.js b/app/javascript/sdk/IFrameHelper.js index 6858f3a40..b868ef51f 100644 --- a/app/javascript/sdk/IFrameHelper.js +++ b/app/javascript/sdk/IFrameHelper.js @@ -113,7 +113,7 @@ export const IFrameHelper = { }, setBubbleLabel(message) { - setBubbleText(message.label); + setBubbleText(window.$chatwoot.launcherTitle || message.label); }, toggleBubble: () => { diff --git a/docs/channels/website-sdk.md b/docs/channels/website-sdk.md index b6d0497d2..84cd2a12a 100644 --- a/docs/channels/website-sdk.md +++ b/docs/channels/website-sdk.md @@ -37,6 +37,15 @@ Chatwoot support 2 designs for for the widget ![Expanded-bubble](./images/sdk/expanded-bubble.gif) +If you are using expanded bubble, you can customize the text used in the bubble by setting `launcherText` parameter on chatwootSettings as described below. + +```js +window.chatwootSettings = { + type: 'expanded_bubble', + launcherText: 'Chat with us' +} +``` + ### To trigger widget without displaying bubble ```js From 507b40a51dcfb5608ef25a6255630592f37473eb Mon Sep 17 00:00:00 2001 From: Sojan Jose Date: Wed, 19 Aug 2020 23:25:55 +0530 Subject: [PATCH 11/16] chore: Control rendering contact inbox (#1157) --- .../api/v1/accounts/contacts/create.json.jbuilder | 2 +- app/views/api/v1/accounts/contacts/index.json.jbuilder | 2 +- .../api/v1/accounts/contacts/search.json.jbuilder | 2 +- app/views/api/v1/accounts/contacts/show.json.jbuilder | 2 +- .../api/v1/accounts/contacts/update.json.jbuilder | 2 +- app/views/api/v1/models/_contact.json.jbuilder | 10 +++++++--- 6 files changed, 12 insertions(+), 8 deletions(-) diff --git a/app/views/api/v1/accounts/contacts/create.json.jbuilder b/app/views/api/v1/accounts/contacts/create.json.jbuilder index 3fd338c4a..a9ac26750 100644 --- a/app/views/api/v1/accounts/contacts/create.json.jbuilder +++ b/app/views/api/v1/accounts/contacts/create.json.jbuilder @@ -1,6 +1,6 @@ json.payload do json.contact do - json.partial! 'api/v1/models/contact.json.jbuilder', resource: @contact + json.partial! 'api/v1/models/contact.json.jbuilder', resource: @contact, with_contact_inboxes: true end json.contact_inbox do json.inbox @contact_inbox&.inbox diff --git a/app/views/api/v1/accounts/contacts/index.json.jbuilder b/app/views/api/v1/accounts/contacts/index.json.jbuilder index 892e5fbdd..b033133ed 100644 --- a/app/views/api/v1/accounts/contacts/index.json.jbuilder +++ b/app/views/api/v1/accounts/contacts/index.json.jbuilder @@ -1,5 +1,5 @@ json.payload do json.array! @contacts do |contact| - json.partial! 'api/v1/models/contact.json.jbuilder', resource: contact + json.partial! 'api/v1/models/contact.json.jbuilder', resource: contact, with_contact_inboxes: true end end diff --git a/app/views/api/v1/accounts/contacts/search.json.jbuilder b/app/views/api/v1/accounts/contacts/search.json.jbuilder index 892e5fbdd..b033133ed 100644 --- a/app/views/api/v1/accounts/contacts/search.json.jbuilder +++ b/app/views/api/v1/accounts/contacts/search.json.jbuilder @@ -1,5 +1,5 @@ json.payload do json.array! @contacts do |contact| - json.partial! 'api/v1/models/contact.json.jbuilder', resource: contact + json.partial! 'api/v1/models/contact.json.jbuilder', resource: contact, with_contact_inboxes: true end end diff --git a/app/views/api/v1/accounts/contacts/show.json.jbuilder b/app/views/api/v1/accounts/contacts/show.json.jbuilder index 276a6323e..64f6c9c31 100644 --- a/app/views/api/v1/accounts/contacts/show.json.jbuilder +++ b/app/views/api/v1/accounts/contacts/show.json.jbuilder @@ -1,3 +1,3 @@ json.payload do - json.partial! 'api/v1/models/contact.json.jbuilder', resource: @contact + json.partial! 'api/v1/models/contact.json.jbuilder', resource: @contact, with_contact_inboxes: true end diff --git a/app/views/api/v1/accounts/contacts/update.json.jbuilder b/app/views/api/v1/accounts/contacts/update.json.jbuilder index 276a6323e..64f6c9c31 100644 --- a/app/views/api/v1/accounts/contacts/update.json.jbuilder +++ b/app/views/api/v1/accounts/contacts/update.json.jbuilder @@ -1,3 +1,3 @@ json.payload do - json.partial! 'api/v1/models/contact.json.jbuilder', resource: @contact + json.partial! 'api/v1/models/contact.json.jbuilder', resource: @contact, with_contact_inboxes: true end diff --git a/app/views/api/v1/models/_contact.json.jbuilder b/app/views/api/v1/models/_contact.json.jbuilder index 80f449a13..b1e3b1dcb 100644 --- a/app/views/api/v1/models/_contact.json.jbuilder +++ b/app/views/api/v1/models/_contact.json.jbuilder @@ -5,8 +5,12 @@ json.id resource.id json.name resource.name json.phone_number resource.phone_number json.thumbnail resource.avatar_url -json.contact_inboxes do - json.array! resource.contact_inboxes do |contact_inbox| - json.partial! 'api/v1/models/contact_inbox.json.jbuilder', resource: contact_inbox + +# we only want to output contact inbox when its /contacts endpoints +if defined?(with_contact_inboxes) && with_contact_inboxes.present? + json.contact_inboxes do + json.array! resource.contact_inboxes do |contact_inbox| + json.partial! 'api/v1/models/contact_inbox.json.jbuilder', resource: contact_inbox + end end end From cdd385b269a4a6031f5251f33cd445dbad389970 Mon Sep 17 00:00:00 2001 From: Sojan Jose Date: Fri, 21 Aug 2020 19:30:27 +0530 Subject: [PATCH 12/16] feat: Custom Attributes for contacts (#1158) Co-authored-by: Pranav Raj Sreepuram --- app/actions/contact_identify_action.rb | 3 +- .../api/v1/accounts/contacts_controller.rb | 21 ++++--- .../api/v1/widget/contacts_controller.rb | 2 +- .../dashboard/i18n/locale/en/contact.json | 3 + .../conversation/ContactCustomAttributes.vue | 59 +++++++++++++++++++ .../conversation/ContactDetailsItem.vue | 2 +- .../dashboard/conversation/ContactPanel.vue | 10 ++++ app/javascript/packs/sdk.js | 18 ++++++ app/javascript/widget/App.vue | 11 +++- app/javascript/widget/api/contacts.js | 5 ++ .../widget/store/modules/contacts.js | 7 +++ app/models/contact.rb | 5 +- .../api/v1/models/_contact.json.jbuilder | 1 + ...90629_add_custom_attributes_to_contacts.rb | 5 ++ db/schema.rb | 3 +- docs/channels/website-sdk.md | 25 ++++++++ spec/actions/contact_identify_action_spec.rb | 7 ++- .../v1/accounts/contacts_controller_spec.rb | 16 +++-- 18 files changed, 182 insertions(+), 21 deletions(-) create mode 100644 app/javascript/dashboard/routes/dashboard/conversation/ContactCustomAttributes.vue create mode 100644 db/migrate/20200819190629_add_custom_attributes_to_contacts.rb diff --git a/app/actions/contact_identify_action.rb b/app/actions/contact_identify_action.rb index 75af71a1f..ff8efd3b5 100644 --- a/app/actions/contact_identify_action.rb +++ b/app/actions/contact_identify_action.rb @@ -33,7 +33,8 @@ class ContactIdentifyAction end def update_contact - @contact.update!(params.slice(:name, :email, :identifier)) + custom_attributes = params[:custom_attributes] ? @contact.custom_attributes.merge(params[:custom_attributes]) : @contact.custom_attributes + @contact.update!(params.slice(:name, :email, :identifier).merge({ custom_attributes: custom_attributes })) ContactAvatarJob.perform_later(@contact, params[:avatar_url]) if params[:avatar_url].present? end diff --git a/app/controllers/api/v1/accounts/contacts_controller.rb b/app/controllers/api/v1/accounts/contacts_controller.rb index eef50ef11..580ba799d 100644 --- a/app/controllers/api/v1/accounts/contacts_controller.rb +++ b/app/controllers/api/v1/accounts/contacts_controller.rb @@ -12,14 +12,14 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController def create ActiveRecord::Base.transaction do - @contact = Current.account.contacts.new(contact_create_params) + @contact = Current.account.contacts.new(contact_params) @contact.save! @contact_inbox = build_contact_inbox end end def update - @contact.update!(contact_params) + @contact.update!(contact_update_params) end def search @@ -43,14 +43,21 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController end def contact_params - params.require(:contact).permit(:name, :email, :phone_number) + params.require(:contact).permit(:name, :email, :phone_number, custom_attributes: {}) + end + + def contact_custom_attributes + return @contact.custom_attributes.merge(contact_params[:custom_attributes]) if contact_params[:custom_attributes] + + @contact.custom_attributes + end + + def contact_update_params + # we want the merged custom attributes not the original one + contact_params.except(:custom_attributes).merge({ custom_attributes: contact_custom_attributes }) end def fetch_contact @contact = Current.account.contacts.find(params[:id]) end - - def contact_create_params - params.require(:contact).permit(:name, :email, :phone_number) - end end diff --git a/app/controllers/api/v1/widget/contacts_controller.rb b/app/controllers/api/v1/widget/contacts_controller.rb index 2a2b184fb..b58d26373 100644 --- a/app/controllers/api/v1/widget/contacts_controller.rb +++ b/app/controllers/api/v1/widget/contacts_controller.rb @@ -10,6 +10,6 @@ class Api::V1::Widget::ContactsController < Api::V1::Widget::BaseController private def permitted_params - params.permit(:website_token, :identifier, :email, :name, :avatar_url) + params.permit(:website_token, :identifier, :email, :name, :avatar_url, custom_attributes: {}) end end diff --git a/app/javascript/dashboard/i18n/locale/en/contact.json b/app/javascript/dashboard/i18n/locale/en/contact.json index 87c9256c2..d13ade2be 100644 --- a/app/javascript/dashboard/i18n/locale/en/contact.json +++ b/app/javascript/dashboard/i18n/locale/en/contact.json @@ -9,6 +9,9 @@ "NO_RECORDS_FOUND": "There are no previous conversations associated to this contact.", "TITLE": "Previous Conversations" }, + "CUSTOM_ATTRIBUTES": { + "TITLE": "Custom Attributes" + }, "LABELS": { "TITLE": "Conversation Labels", "MODAL": { diff --git a/app/javascript/dashboard/routes/dashboard/conversation/ContactCustomAttributes.vue b/app/javascript/dashboard/routes/dashboard/conversation/ContactCustomAttributes.vue new file mode 100644 index 000000000..98c1775e8 --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/conversation/ContactCustomAttributes.vue @@ -0,0 +1,59 @@ + + + + + diff --git a/app/javascript/dashboard/routes/dashboard/conversation/ContactDetailsItem.vue b/app/javascript/dashboard/routes/dashboard/conversation/ContactDetailsItem.vue index fda504c35..75bf0c018 100644 --- a/app/javascript/dashboard/routes/dashboard/conversation/ContactDetailsItem.vue +++ b/app/javascript/dashboard/routes/dashboard/conversation/ContactDetailsItem.vue @@ -36,7 +36,7 @@ export default { @import '~dashboard/assets/scss/mixins'; .conv-details--item { - padding-bottom: $space-normal; + padding-bottom: var(--space-slab); &:last-child { padding-bottom: 0; diff --git a/app/javascript/dashboard/routes/dashboard/conversation/ContactPanel.vue b/app/javascript/dashboard/routes/dashboard/conversation/ContactPanel.vue index e59d80a49..52e6dc2b2 100644 --- a/app/javascript/dashboard/routes/dashboard/conversation/ContactPanel.vue +++ b/app/javascript/dashboard/routes/dashboard/conversation/ContactPanel.vue @@ -84,6 +84,10 @@ icon="ion-clock" />
+ { } }, + setCustomAttributes(customAttributes = {}) { + if (!customAttributes || !Object.keys(customAttributes).length) { + throw new Error('Custom attributes should have atleast one key'); + } else { + IFrameHelper.sendMessage('set-custom-attributes', { customAttributes }); + } + }, + + deleteCustomAttribute(customAttribute = '') { + if (!customAttribute) { + throw new Error('Custom attribute is required'); + } else { + IFrameHelper.sendMessage('delete-custom-attribute', { + customAttribute, + }); + } + }, + setLabel(label = '') { IFrameHelper.sendMessage('set-label', { label }); }, diff --git a/app/javascript/widget/App.vue b/app/javascript/widget/App.vue index 45b45fb48..fb865d03e 100755 --- a/app/javascript/widget/App.vue +++ b/app/javascript/widget/App.vue @@ -15,8 +15,6 @@ diff --git a/app/javascript/dashboard/components/widgets/conversation/CannedResponse.vue b/app/javascript/dashboard/components/widgets/conversation/CannedResponse.vue index 33fbadf67..e80adf24d 100644 --- a/app/javascript/dashboard/components/widgets/conversation/CannedResponse.vue +++ b/app/javascript/dashboard/components/widgets/conversation/CannedResponse.vue @@ -87,11 +87,3 @@ export default { }, }; - - diff --git a/app/javascript/dashboard/components/widgets/conversation/ConversationHeader.vue b/app/javascript/dashboard/components/widgets/conversation/ConversationHeader.vue index c11612376..65567ae26 100644 --- a/app/javascript/dashboard/components/widgets/conversation/ConversationHeader.vue +++ b/app/javascript/dashboard/components/widgets/conversation/ConversationHeader.vue @@ -45,7 +45,6 @@ {{ $t('AGENT_MGMT.SEARCH.NO_RESULTS') }} - diff --git a/app/javascript/dashboard/components/widgets/conversation/MoreActions.vue b/app/javascript/dashboard/components/widgets/conversation/MoreActions.vue index b91226d6f..a5f9860ce 100644 --- a/app/javascript/dashboard/components/widgets/conversation/MoreActions.vue +++ b/app/javascript/dashboard/components/widgets/conversation/MoreActions.vue @@ -5,7 +5,7 @@ :status="currentChat.status" />
-
- - - -
- -
-
- {{ contact.name }} -
- - - -
- {{ `@${contact.additional_attributes.screen_name}` }} -
-
- {{ contact.location }} -
-
-
-
- {{ contact.bio }} -
-
- {{ contact.additional_attributes.description }} -
-
+ + + +
import { mapGetters } from 'vuex'; -import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue'; import ContactConversations from './ContactConversations.vue'; import ContactDetailsItem from './ContactDetailsItem.vue'; +import ContactInfo from './contact/ContactInfo'; import ConversationLabels from './labels/LabelBox.vue'; import ContactCustomAttributes from './ContactCustomAttributes'; @@ -110,8 +56,8 @@ export default { ContactCustomAttributes, ContactConversations, ContactDetailsItem, + ContactInfo, ConversationLabels, - Thumbnail, }, props: { conversationId: { @@ -210,7 +156,11 @@ export default { overflow-y: auto; overflow: auto; position: relative; - padding: $space-normal; + padding: $space-one; + + i { + margin-right: $space-smaller; + } } .close-button { @@ -221,57 +171,9 @@ export default { color: $color-heading; } -.contact--profile { - align-items: center; - padding: $space-medium 0 $space-one; - - .user-thumbnail-box { - margin-right: $space-normal; - } -} - -.contact--details { - margin-top: $space-small; - - p { - margin-bottom: 0; - } -} - -.contact--info { - align-items: center; - display: flex; - flex-direction: column; - text-align: center; -} - -.contact--name { - @include text-ellipsis; - text-transform: capitalize; - - font-weight: $font-weight-bold; - font-size: $font-size-default; -} - -.contact--email { - @include text-ellipsis; - - color: $color-gray; - display: block; - line-height: $space-medium; - - &:hover { - color: $color-woot; - } -} - -.contact--bio { - margin-top: $space-normal; -} - .conversation--details { border-top: 1px solid $color-border-light; - padding: $space-large $space-normal; + padding: $space-normal; } .conversation--labels { diff --git a/app/javascript/dashboard/routes/dashboard/conversation/contact/ContactInfo.vue b/app/javascript/dashboard/routes/dashboard/conversation/contact/ContactInfo.vue new file mode 100644 index 000000000..90ff44543 --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/conversation/contact/ContactInfo.vue @@ -0,0 +1,157 @@ + + + + diff --git a/app/javascript/dashboard/routes/dashboard/conversation/contact/ContactInfoRow.vue b/app/javascript/dashboard/routes/dashboard/conversation/contact/ContactInfoRow.vue new file mode 100644 index 000000000..7c219a5a9 --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/conversation/contact/ContactInfoRow.vue @@ -0,0 +1,59 @@ + + + diff --git a/app/javascript/dashboard/routes/dashboard/conversation/contact/EditContact.vue b/app/javascript/dashboard/routes/dashboard/conversation/contact/EditContact.vue new file mode 100644 index 000000000..f7e1d0ff6 --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/conversation/contact/EditContact.vue @@ -0,0 +1,225 @@ +