From d492a65c24c6c11805c20562af371c02a8aaacb2 Mon Sep 17 00:00:00 2001 From: Nithin David Thomas <1277421+nithindavid@users.noreply.github.com> Date: Thu, 12 Aug 2021 23:21:31 +0530 Subject: [PATCH 01/12] fix: Checks JS window object for null before focusing (#2798) --- .../dashboard/components/widgets/conversation/bubble/File.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/javascript/dashboard/components/widgets/conversation/bubble/File.vue b/app/javascript/dashboard/components/widgets/conversation/bubble/File.vue index 14e01e276..e1cf8c7a5 100644 --- a/app/javascript/dashboard/components/widgets/conversation/bubble/File.vue +++ b/app/javascript/dashboard/components/widgets/conversation/bubble/File.vue @@ -36,7 +36,7 @@ export default { methods: { openLink() { const win = window.open(this.url, '_blank', 'noopener'); - win.focus(); + if (win) win.focus(); }, }, }; From acb39cbc8f6530da8f8b4f634540840e06f94166 Mon Sep 17 00:00:00 2001 From: Sojan Jose Date: Fri, 13 Aug 2021 13:02:46 +0530 Subject: [PATCH 02/12] chore: Ability to create contact with identifiers (#2802) --- .../api/v1/accounts/contacts_controller.rb | 4 +-- lib/exception_list.rb | 3 +- .../v1/accounts/contacts_controller_spec.rb | 15 +++++++++- .../definitions/request/contact/create.yml | 6 ++++ .../definitions/request/contact/update.yml | 6 ++++ swagger/paths/contact/search.yml | 1 + swagger/swagger.json | 29 ++++++++++++++----- 7 files changed, 53 insertions(+), 11 deletions(-) diff --git a/app/controllers/api/v1/accounts/contacts_controller.rb b/app/controllers/api/v1/accounts/contacts_controller.rb index c7b2348ab..b9b9ffafe 100644 --- a/app/controllers/api/v1/accounts/contacts_controller.rb +++ b/app/controllers/api/v1/accounts/contacts_controller.rb @@ -22,7 +22,7 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController render json: { error: 'Specify search string with parameter q' }, status: :unprocessable_entity if params[:q].blank? && return contacts = resolved_contacts.where( - 'name ILIKE :search OR email ILIKE :search OR phone_number ILIKE :search', + 'name ILIKE :search OR email ILIKE :search OR phone_number ILIKE :search OR contacts.identifier LIKE :search', search: "%#{params[:q]}%" ) @contacts_count = contacts.count @@ -108,7 +108,7 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController end def contact_params - params.require(:contact).permit(:name, :email, :phone_number, additional_attributes: {}, custom_attributes: {}) + params.require(:contact).permit(:name, :identifier, :email, :phone_number, additional_attributes: {}, custom_attributes: {}) end def contact_custom_attributes diff --git a/lib/exception_list.rb b/lib/exception_list.rb index 402523486..b0703626d 100644 --- a/lib/exception_list.rb +++ b/lib/exception_list.rb @@ -1,7 +1,8 @@ module ExceptionList REST_CLIENT_EXCEPTIONS = [RestClient::NotFound, RestClient::GatewayTimeout, RestClient::BadRequest, RestClient::MethodNotAllowed, RestClient::Forbidden, RestClient::InternalServerError, - RestClient::Exceptions::OpenTimeout, RestClient::Exceptions::ReadTimeout, SocketError].freeze + RestClient::Exceptions::OpenTimeout, RestClient::Exceptions::ReadTimeout, + RestClient::MovedPermanently, SocketError].freeze SMTP_EXCEPTIONS = [ Net::SMTPSyntaxError ].freeze diff --git a/spec/controllers/api/v1/accounts/contacts_controller_spec.rb b/spec/controllers/api/v1/accounts/contacts_controller_spec.rb index 1dd152898..dedd34e3c 100644 --- a/spec/controllers/api/v1/accounts/contacts_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/contacts_controller_spec.rb @@ -188,6 +188,19 @@ RSpec.describe 'Contacts API', type: :request do expect(response.body).to include(contact2.email) expect(response.body).not_to include(contact1.email) end + + it 'matches the contact respecting the identifier character casing' do + contact_normal = create(:contact, name: 'testcontact', account: account, identifier: 'testidentifer') + contact_special = create(:contact, name: 'testcontact', account: account, identifier: 'TestIdentifier') + get "/api/v1/accounts/#{account.id}/contacts/search", + params: { q: 'TestIdentifier' }, + headers: admin.create_new_auth_token, + as: :json + + expect(response).to have_http_status(:success) + expect(response.body).to include(contact_special.identifier) + expect(response.body).not_to include(contact_normal.identifier) + end end end @@ -284,7 +297,7 @@ RSpec.describe 'Contacts API', type: :request do expect(json_response['payload']['contact']['custom_attributes']).to eq({ 'test' => 'test', 'test1' => 'test1' }) end - it 'creates the contact identifier when inbox id is passed' do + it 'creates the contact inbox when inbox id is passed' do expect do post "/api/v1/accounts/#{account.id}/contacts", headers: admin.create_new_auth_token, params: valid_params.merge({ inbox_id: inbox.id }) diff --git a/swagger/definitions/request/contact/create.yml b/swagger/definitions/request/contact/create.yml index 5dc481a76..26e04463a 100644 --- a/swagger/definitions/request/contact/create.yml +++ b/swagger/definitions/request/contact/create.yml @@ -5,7 +5,13 @@ properties: required: true name: type: string + description: name of the contact email: type: string + description: email of the contact phone_number: type: string + description: phone number of the contact + identifier: + type: string + description: A unique identifier for the contact in external system diff --git a/swagger/definitions/request/contact/update.yml b/swagger/definitions/request/contact/update.yml index cafa35169..7e3d8fc36 100644 --- a/swagger/definitions/request/contact/update.yml +++ b/swagger/definitions/request/contact/update.yml @@ -2,7 +2,13 @@ type: object properties: name: type: string + description: name of the contact email: type: string + description: email of the contact phone_number: type: string + description: phone number of the contact + identifier: + type: string + description: A unique identifier for the contact in external system diff --git a/swagger/paths/contact/search.yml b/swagger/paths/contact/search.yml index 30ade7b5a..84e782d2d 100644 --- a/swagger/paths/contact/search.yml +++ b/swagger/paths/contact/search.yml @@ -8,6 +8,7 @@ get: - name: q in: query type: string + description: Search using contact `name`, `identifier`, `email` or `phone number` - $ref: '#/parameters/contact_sort_param' - $ref: '#/parameters/page' responses: diff --git a/swagger/swagger.json b/swagger/swagger.json index 935044734..43129026e 100644 --- a/swagger/swagger.json +++ b/swagger/swagger.json @@ -1281,7 +1281,8 @@ { "name": "q", "in": "query", - "type": "string" + "type": "string", + "description": "Search using contact `name`, `identifier`, `email` or `phone number`" }, { "$ref": "#/parameters/contact_sort_param" @@ -3376,13 +3377,20 @@ "required": true }, "name": { - "type": "string" + "type": "string", + "description": "name of the contact" }, "email": { - "type": "string" + "type": "string", + "description": "email of the contact" }, "phone_number": { - "type": "string" + "type": "string", + "description": "phone number of the contact" + }, + "identifier": { + "type": "string", + "description": "A unique identifier for the contact in external system" } } }, @@ -3390,13 +3398,20 @@ "type": "object", "properties": { "name": { - "type": "string" + "type": "string", + "description": "name of the contact" }, "email": { - "type": "string" + "type": "string", + "description": "email of the contact" }, "phone_number": { - "type": "string" + "type": "string", + "description": "phone number of the contact" + }, + "identifier": { + "type": "string", + "description": "A unique identifier for the contact in external system" } } }, From 0f148f77002d0ee7e09a22c4051dc0053beb6713 Mon Sep 17 00:00:00 2001 From: Nithin David Thomas <1277421+nithindavid@users.noreply.github.com> Date: Fri, 13 Aug 2021 13:37:51 +0530 Subject: [PATCH 03/12] fix: Fixes parsing issue with custom attributes (#2796) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: Fixes parsing issue with custom attributes * Update app/javascript/dashboard/routes/dashboard/conversation/ContactCustomAttributes.vue Co-authored-by: शैलेश कळमकर (Shailesh Kalamkar) Co-authored-by: Muhsin Keloth Co-authored-by: शैलेश कळमकर (Shailesh Kalamkar) Co-authored-by: Sojan Jose --- .../conversation/ContactCustomAttributes.vue | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/app/javascript/dashboard/routes/dashboard/conversation/ContactCustomAttributes.vue b/app/javascript/dashboard/routes/dashboard/conversation/ContactCustomAttributes.vue index 7639e08e3..0e2cfa8cb 100644 --- a/app/javascript/dashboard/routes/dashboard/conversation/ContactCustomAttributes.vue +++ b/app/javascript/dashboard/routes/dashboard/conversation/ContactCustomAttributes.vue @@ -45,9 +45,20 @@ export default { }, methods: { valueWithLink(attribute) { - const messageFormatter = new MessageFormatter(attribute); + const parsedAttribute = this.parseAttributeToString(attribute); + const messageFormatter = new MessageFormatter(parsedAttribute); return messageFormatter.formattedMessage; }, + parseAttributeToString(attribute) { + switch (typeof attribute) { + case 'string': + return attribute; + case 'object': + return JSON.stringify(attribute); + default: + return `${attribute}`; + } + }, }, }; From dfcc33cbddd75e0ec2b17853e0139877e45979c8 Mon Sep 17 00:00:00 2001 From: Nithin David Thomas <1277421+nithindavid@users.noreply.github.com> Date: Fri, 13 Aug 2021 13:40:12 +0530 Subject: [PATCH 04/12] fix: Fixes error when dropdown button is focused (#2797) Co-authored-by: Muhsin Keloth --- .../components/ui/dropdown/DropdownMenu.vue | 59 ++++++++++--------- 1 file changed, 32 insertions(+), 27 deletions(-) diff --git a/app/javascript/shared/components/ui/dropdown/DropdownMenu.vue b/app/javascript/shared/components/ui/dropdown/DropdownMenu.vue index 89684184f..4857a5014 100644 --- a/app/javascript/shared/components/ui/dropdown/DropdownMenu.vue +++ b/app/javascript/shared/components/ui/dropdown/DropdownMenu.vue @@ -25,47 +25,52 @@ export default { default: 'top', }, }, - mounted() { this.focusItem(); }, methods: { + dropdownMenuButtons() { + return this.$refs.dropdownMenu.querySelectorAll( + 'ul.dropdown li.dropdown-menu__item .button' + ); + }, + activeElementIndex() { + const menuButtons = this.dropdownMenuButtons(); + const focusedButton = this.$refs.dropdownMenu.querySelector( + 'ul.dropdown li.dropdown-menu__item .button:focus' + ); + const activeIndex = [...menuButtons].indexOf(focusedButton); + return activeIndex; + }, focusItem() { - this.$refs.dropdownMenu - .querySelector('ul.dropdown li.dropdown-menu__item .button') - .focus(); + const firstButton = this.$refs.dropdownMenu.querySelector( + 'ul.dropdown li.dropdown-menu__item .button' + ); + + if (firstButton) firstButton.focus(); }, handleKeyEvents(e) { - if (hasPressedArrowUpKey(e)) { - const items = this.$refs.dropdownMenu.querySelectorAll( - 'ul.dropdown li.dropdown-menu__item .button' - ); - const focusItems = this.$refs.dropdownMenu.querySelector( - 'ul.dropdown li.dropdown-menu__item .button:focus' - ); - const activeElementIndex = [...items].indexOf(focusItems); - const lastElementIndex = items.length - 1; + const menuButtons = this.dropdownMenuButtons(); + const lastElementIndex = menuButtons.length - 1; - if (activeElementIndex >= 1) { - items[activeElementIndex - 1].focus(); + if (menuButtons.length === 0) return; + + if (hasPressedArrowUpKey(e)) { + const activeIndex = this.activeElementIndex(); + + if (activeIndex >= 1) { + menuButtons[activeIndex - 1].focus(); } else { - items[lastElementIndex].focus(); + menuButtons[lastElementIndex].focus(); } } if (hasPressedArrowDownKey(e)) { - const items = this.$refs.dropdownMenu.querySelectorAll( - 'li.dropdown-menu__item .button' - ); - const focusItems = this.$refs.dropdownMenu.querySelector( - 'li.dropdown-menu__item .button:focus' - ); - const activeElementIndex = [...items].indexOf(focusItems); - const lastElementIndex = items.length - 1; + const activeIndex = this.activeElementIndex(); - if (activeElementIndex === lastElementIndex) { - items[0].focus(); + if (activeIndex === lastElementIndex) { + menuButtons[0].focus(); } else { - items[activeElementIndex + 1].focus(); + menuButtons[activeIndex + 1].focus(); } } }, From 9e052fd5b2f49bf6fb6cb50872b361160c538d57 Mon Sep 17 00:00:00 2001 From: "Aswin Dev P.S" Date: Fri, 13 Aug 2021 17:26:09 +0530 Subject: [PATCH 05/12] chore: Set phone_number through Website SDK (#2803) Fixes: #2599 --- app/actions/contact_identify_action.rb | 27 ++++++++++++++++--- .../api/v1/widget/contacts_controller.rb | 2 +- .../widget/store/modules/contacts.js | 1 + app/views/widget_tests/index.html.erb | 3 ++- spec/actions/contact_identify_action_spec.rb | 11 ++++++++ 5 files changed, 39 insertions(+), 5 deletions(-) diff --git a/app/actions/contact_identify_action.rb b/app/actions/contact_identify_action.rb index 91060f215..655c6bc1c 100644 --- a/app/actions/contact_identify_action.rb +++ b/app/actions/contact_identify_action.rb @@ -3,8 +3,9 @@ class ContactIdentifyAction def perform ActiveRecord::Base.transaction do - @contact = merge_contact(existing_identified_contact, @contact) if merge_contacts?(existing_identified_contact, @contact) - @contact = merge_contact(existing_email_contact, @contact) if merge_contacts?(existing_email_contact, @contact) + merge_if_existing_identified_contact + merge_if_existing_email_contact + merge_if_existing_phone_number_contact update_contact end @contact @@ -16,6 +17,18 @@ class ContactIdentifyAction @account ||= @contact.account end + def merge_if_existing_identified_contact + @contact = merge_contact(existing_identified_contact, @contact) if merge_contacts?(existing_identified_contact, @contact) + end + + def merge_if_existing_email_contact + @contact = merge_contact(existing_email_contact, @contact) if merge_contacts?(existing_email_contact, @contact) + end + + def merge_if_existing_phone_number_contact + @contact = merge_contact(existing_phone_number_contact, @contact) if merge_contacts?(existing_phone_number_contact, @contact) + end + def existing_identified_contact return if params[:identifier].blank? @@ -28,6 +41,12 @@ class ContactIdentifyAction @existing_email_contact ||= Contact.where(account_id: account.id).find_by(email: params[:email]) end + def existing_phone_number_contact + return if params[:phone_number].blank? + + @existing_phone_number_contact ||= Contact.where(account_id: account.id).find_by(phone_number: params[:phone_number]) + end + def merge_contacts?(existing_contact, _contact) existing_contact && existing_contact.id != @contact.id end @@ -36,7 +55,9 @@ class ContactIdentifyAction custom_attributes = params[:custom_attributes] ? @contact.custom_attributes.merge(params[:custom_attributes]) : @contact.custom_attributes # blank identifier or email will throw unique index error # TODO: replace reject { |_k, v| v.blank? } with compact_blank when rails is upgraded - @contact.update!(params.slice(:name, :email, :identifier).reject { |_k, v| v.blank? }.merge({ custom_attributes: custom_attributes })) + @contact.update!(params.slice(:name, :email, :identifier, :phone_number).reject do |_k, v| + v.blank? + end.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/widget/contacts_controller.rb b/app/controllers/api/v1/widget/contacts_controller.rb index 135d2ced3..8bc5cfd0b 100644 --- a/app/controllers/api/v1/widget/contacts_controller.rb +++ b/app/controllers/api/v1/widget/contacts_controller.rb @@ -29,6 +29,6 @@ class Api::V1::Widget::ContactsController < Api::V1::Widget::BaseController end def permitted_params - params.permit(:website_token, :identifier, :identifier_hash, :email, :name, :avatar_url, custom_attributes: {}) + params.permit(:website_token, :identifier, :identifier_hash, :email, :name, :avatar_url, :phone_number, custom_attributes: {}) end end diff --git a/app/javascript/widget/store/modules/contacts.js b/app/javascript/widget/store/modules/contacts.js index 582b78414..f1ef555e5 100644 --- a/app/javascript/widget/store/modules/contacts.js +++ b/app/javascript/widget/store/modules/contacts.js @@ -29,6 +29,7 @@ export const actions = { name: userObject.name, avatar_url: userObject.avatar_url, identifier_hash: userObject.identifier_hash, + phone_number: userObject.phone_number, }; const { data: { pubsub_token: pubsubToken }, diff --git a/app/views/widget_tests/index.html.erb b/app/views/widget_tests/index.html.erb index 7d76512d6..78dbaa37c 100644 --- a/app/views/widget_tests/index.html.erb +++ b/app/views/widget_tests/index.html.erb @@ -39,7 +39,8 @@ window.addEventListener('chatwoot:ready', function() { window.$chatwoot.setUser('<%= user_id %>', { identifier_hash: '<%= user_hash %>', email: 'jane@example.com', - name: 'Jane Doe' + name: 'Jane Doe', + phone_number: '' }); } }) diff --git a/spec/actions/contact_identify_action_spec.rb b/spec/actions/contact_identify_action_spec.rb index be139a22e..c18284c5d 100644 --- a/spec/actions/contact_identify_action_spec.rb +++ b/spec/actions/contact_identify_action_spec.rb @@ -45,6 +45,17 @@ describe ::ContactIdentifyAction do end end + context 'when contact with same phone_number exists' do + it 'merges the current contact to phone_number contact' do + existing_phone_number_contact = create(:contact, account: account, phone_number: '+919999888877') + params = { phone_number: '+919999888877' } + result = described_class.new(contact: contact, params: params).perform + expect(result.id).to eq existing_phone_number_contact.id + expect(result.name).to eq existing_phone_number_contact.name + expect { contact.reload }.to raise_error(ActiveRecord::RecordNotFound) + end + end + context 'when contacts with blank identifiers exist and identify action is called with blank identifier' do it 'updates the attributes of contact passed in to identify action' do create(:contact, account: account, identifier: '') From 37bf4b7dd809177c9309f8b1bcaa1052566ca24d Mon Sep 17 00:00:00 2001 From: Pascal Jufer Date: Fri, 13 Aug 2021 15:40:47 +0200 Subject: [PATCH 06/12] fix: Use label for "Subscribe to updates" checkbox (#2788) --- .../assets/scss/super_admin/index.scss | 29 ++++++++++--------- .../installation/onboarding/index.html.erb | 6 ++-- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/app/javascript/dashboard/assets/scss/super_admin/index.scss b/app/javascript/dashboard/assets/scss/super_admin/index.scss index 205a10f70..f95f1303f 100644 --- a/app/javascript/dashboard/assets/scss/super_admin/index.scss +++ b/app/javascript/dashboard/assets/scss/super_admin/index.scss @@ -8,6 +8,22 @@ font-weight: var(--font-weight-light); margin-top: var(--space-large); } + + .update-subscription--checkbox { + display: flex; + + input { + line-height: 1.5; + margin-right: var(--space-one); + margin-top: var(--space-smaller); + } + + label { + font-size: var(--font-size-small); + line-height: 1.5; + margin-bottom: var(--space-normal); + } + } } .alert-box { @@ -20,17 +36,4 @@ text-align: center; } -.update-subscription--checkbox { - display: flex; - input { - line-height: 1.5; - margin-right: var(--space-one); - } - - div { - font-size: var(--font-size-small); - line-height: 1.5; - margin-bottom: var(--space-normal); - } -} diff --git a/app/views/installation/onboarding/index.html.erb b/app/views/installation/onboarding/index.html.erb index 8287714cd..b58a4ee41 100644 --- a/app/views/installation/onboarding/index.html.erb +++ b/app/views/installation/onboarding/index.html.erb @@ -43,9 +43,9 @@
<%= check_box_tag "subscribe_to_updates", 'true', true %> -
- Subscribe to release notes, newsletters & product feedback surveys. -
+