From abc335cc8ed2da2cfbf6a1f266f0155e57ad6599 Mon Sep 17 00:00:00 2001 From: Pranav Raj S Date: Wed, 8 Jul 2020 13:13:48 +0530 Subject: [PATCH 01/50] Feature: Add support for labels in form (#1019) --- app/javascript/shared/components/ChatForm.vue | 44 ++++++++++++++++++- app/javascript/widget/assets/scss/_forms.scss | 2 + .../widget/components/AgentMessageBubble.vue | 1 + 3 files changed, 46 insertions(+), 1 deletion(-) diff --git a/app/javascript/shared/components/ChatForm.vue b/app/javascript/shared/components/ChatForm.vue index 7922595a2..56391fdb8 100644 --- a/app/javascript/shared/components/ChatForm.vue +++ b/app/javascript/shared/components/ChatForm.vue @@ -18,6 +18,18 @@ :placeholder="item.placeholder" :disabled="!!submittedValues.length" /> + @@ -36,6 +48,10 @@ import { mapGetters } from 'vuex'; export default { props: { + buttonLabel: { + type: String, + default: '', + }, items: { type: Array, default: () => [], @@ -116,6 +132,7 @@ export default { border-radius: $space-smaller; border: 1px solid $color-border; display: block; + font-family: inherit; font-size: $font-size-default; line-height: 1.5; padding: $space-one; @@ -126,6 +143,31 @@ export default { } } + textarea { + resize: none; + } + + select { + width: 110%; + padding: $space-smaller; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + border: 1px solid $color-border; + border-radius: $space-smaller; + background-color: $color-white; + font-family: inherit; + font-size: $space-normal; + font-weight: normal; + line-height: 1.5; + background-image: url("data:image/svg+xml;utf8,"); + background-origin: content-box; + background-position: right -1.6rem center; + background-repeat: no-repeat; + background-size: 9px 6px; + padding-right: 2.4rem; + } + .button { font-size: $font-size-default; } diff --git a/app/javascript/widget/assets/scss/_forms.scss b/app/javascript/widget/assets/scss/_forms.scss index 752fa223c..6ea4666aa 100755 --- a/app/javascript/widget/assets/scss/_forms.scss +++ b/app/javascript/widget/assets/scss/_forms.scss @@ -69,6 +69,8 @@ $input-height: $space-two * 2; // Form element: Textarea textarea.form-input { + font-family: $font-family; + @include placeholder { color: $color-light-gray; } diff --git a/app/javascript/widget/components/AgentMessageBubble.vue b/app/javascript/widget/components/AgentMessageBubble.vue index 2676839a6..dfbcbed74 100755 --- a/app/javascript/widget/components/AgentMessageBubble.vue +++ b/app/javascript/widget/components/AgentMessageBubble.vue @@ -23,6 +23,7 @@ From 929575f11a7d55f82773b53151423e806b6e0707 Mon Sep 17 00:00:00 2001 From: Pranav Raj S Date: Thu, 9 Jul 2020 16:28:09 +0530 Subject: [PATCH 02/50] Chore: Update copy on Signup page (#1023) --- .../dashboard/components/widgets/ChannelItem.vue | 2 +- app/javascript/dashboard/routes/auth/Signup.vue | 2 +- semantic.yml | 12 ++++++++++++ 3 files changed, 14 insertions(+), 2 deletions(-) create mode 100644 semantic.yml diff --git a/app/javascript/dashboard/components/widgets/ChannelItem.vue b/app/javascript/dashboard/components/widgets/ChannelItem.vue index bd4636fd8..52f4aff52 100644 --- a/app/javascript/dashboard/components/widgets/ChannelItem.vue +++ b/app/javascript/dashboard/components/widgets/ChannelItem.vue @@ -54,7 +54,7 @@ export default { return this.enabledFeatures.channel_facebook; } if (channel === 'twitter') { - return this.enabledFeatures.channel_facebook; + return this.enabledFeatures.channel_twitter; } return ['website', 'twilio'].includes(channel); }, diff --git a/app/javascript/dashboard/routes/auth/Signup.vue b/app/javascript/dashboard/routes/auth/Signup.vue index 8e7047e5a..7ca18e6df 100644 --- a/app/javascript/dashboard/routes/auth/Signup.vue +++ b/app/javascript/dashboard/routes/auth/Signup.vue @@ -13,7 +13,7 @@
+
+ + +
Twilio SMS + + Email + + + Api + diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/channel-factory.js b/app/javascript/dashboard/routes/dashboard/settings/inbox/channel-factory.js index 9d15b3664..47c4e4e6e 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/inbox/channel-factory.js +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/channel-factory.js @@ -2,12 +2,16 @@ import Facebook from './channels/Facebook'; import Website from './channels/Website'; import Twitter from './channels/Twitter'; import Twilio from './channels/Twilio'; +import Api from './channels/Api'; +import Email from './channels/Email'; const channelViewList = { facebook: Facebook, website: Website, twitter: Twitter, twilio: Twilio, + api: Api, + email: Email, }; export default { diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/Api.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/Api.vue new file mode 100644 index 000000000..8c19892d6 --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/Api.vue @@ -0,0 +1,110 @@ + + + diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/Email.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/Email.vue new file mode 100644 index 000000000..b17273d44 --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/Email.vue @@ -0,0 +1,113 @@ + + + diff --git a/app/javascript/dashboard/store/modules/inboxes.js b/app/javascript/dashboard/store/modules/inboxes.js index 38c51195a..15e9eb66e 100644 --- a/app/javascript/dashboard/store/modules/inboxes.js +++ b/app/javascript/dashboard/store/modules/inboxes.js @@ -55,6 +55,18 @@ export const actions = { commit(types.default.SET_INBOXES_UI_FLAG, { isFetching: false }); } }, + createChannel: async ({ commit }, params) => { + try { + commit(types.default.SET_INBOXES_UI_FLAG, { isCreating: true }); + const response = await WebChannel.create(params); + commit(types.default.ADD_INBOXES, response.data); + commit(types.default.SET_INBOXES_UI_FLAG, { isCreating: false }); + return response.data; + } catch (error) { + commit(types.default.SET_INBOXES_UI_FLAG, { isCreating: false }); + throw new Error(error); + } + }, createWebsiteChannel: async ({ commit }, params) => { try { commit(types.default.SET_INBOXES_UI_FLAG, { isCreating: true }); diff --git a/app/listeners/webhook_listener.rb b/app/listeners/webhook_listener.rb index 14ad91709..a463fea00 100644 --- a/app/listeners/webhook_listener.rb +++ b/app/listeners/webhook_listener.rb @@ -50,9 +50,7 @@ class WebhookListener < BaseListener WebhookJob.perform_later(webhook.url, payload) end - # Inbox webhooks - inbox.webhooks.inbox.each do |webhook| - WebhookJob.perform_later(webhook.url, payload) - end + # Deliver for API Inbox + WebhookJob.perform_later(inbox.channel.webhook_url, payload) if inbox.channel_type == 'Channel::Api' end end diff --git a/app/models/account.rb b/app/models/account.rb index 12fb04921..f5d6b29f9 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -43,6 +43,8 @@ class Account < ApplicationRecord has_many :twilio_sms, dependent: :destroy, class_name: '::Channel::TwilioSms' has_many :twitter_profiles, dependent: :destroy, class_name: '::Channel::TwitterProfile' has_many :web_widgets, dependent: :destroy, class_name: '::Channel::WebWidget' + has_many :email_channels, dependent: :destroy, class_name: '::Channel::Email' + has_many :api_channels, dependent: :destroy, class_name: '::Channel::Api' has_many :canned_responses, dependent: :destroy has_many :webhooks, dependent: :destroy has_many :labels, dependent: :destroy diff --git a/app/models/channel/api.rb b/app/models/channel/api.rb new file mode 100644 index 000000000..5f080f232 --- /dev/null +++ b/app/models/channel/api.rb @@ -0,0 +1,19 @@ +# == Schema Information +# +# Table name: channel_api +# +# id :bigint not null, primary key +# webhook_url :string not null +# created_at :datetime not null +# updated_at :datetime not null +# account_id :integer not null +# + +class Channel::Api < ApplicationRecord + self.table_name = 'channel_api' + + validates :account_id, presence: true + belongs_to :account + + has_one :inbox, as: :channel, dependent: :destroy +end diff --git a/app/models/channel/email.rb b/app/models/channel/email.rb new file mode 100644 index 000000000..303d59619 --- /dev/null +++ b/app/models/channel/email.rb @@ -0,0 +1,35 @@ +# == Schema Information +# +# Table name: channel_email +# +# id :bigint not null, primary key +# email :string not null +# forward_to_address :string not null +# created_at :datetime not null +# updated_at :datetime not null +# account_id :integer not null +# +# Indexes +# +# index_channel_email_on_email (email) UNIQUE +# index_channel_email_on_forward_to_address (forward_to_address) UNIQUE +# + +class Channel::Email < ApplicationRecord + self.table_name = 'channel_email' + + validates :account_id, presence: true + belongs_to :account + validates :email, uniqueness: true + validates :forward_to_address, uniqueness: true + + has_one :inbox, as: :channel, dependent: :destroy + before_validation :ensure_forward_to_address, on: :create + + private + + def ensure_forward_to_address + # TODO : implement better logic here + self.forward_to_address ||= "#{SecureRandom.hex}@xyc.com" + end +end diff --git a/app/models/channel/facebook_page.rb b/app/models/channel/facebook_page.rb index 7c087bda9..bcad26387 100644 --- a/app/models/channel/facebook_page.rb +++ b/app/models/channel/facebook_page.rb @@ -17,9 +17,6 @@ # class Channel::FacebookPage < ApplicationRecord - # FIXME: this should be removed post 1.4 release. we moved avatars to inbox - include Avatarable - self.table_name = 'channel_facebook_pages' validates :account_id, presence: true diff --git a/app/views/api/v1/accounts/contacts/create.json.jbuilder b/app/views/api/v1/accounts/contacts/create.json.jbuilder new file mode 100644 index 000000000..3fd338c4a --- /dev/null +++ b/app/views/api/v1/accounts/contacts/create.json.jbuilder @@ -0,0 +1,9 @@ +json.payload do + json.contact do + json.partial! 'api/v1/models/contact.json.jbuilder', resource: @contact + end + json.contact_inbox do + json.inbox @contact_inbox&.inbox + json.source_id @contact_inbox&.source_id + end +end diff --git a/app/views/api/v1/accounts/inboxes/create.json.jbuilder b/app/views/api/v1/accounts/inboxes/create.json.jbuilder index c046402bc..981c1dec0 100644 --- a/app/views/api/v1/accounts/inboxes/create.json.jbuilder +++ b/app/views/api/v1/accounts/inboxes/create.json.jbuilder @@ -1,14 +1 @@ -json.id @inbox.id -json.channel_id @inbox.channel_id -json.name @inbox.name -json.channel_type @inbox.channel_type -json.greeting_enabled @inbox.greeting_enabled -json.greeting_message @inbox.greeting_message -json.avatar_url @inbox.try(:avatar_url) -json.website_token @inbox.channel.try(:website_token) -json.widget_color @inbox.channel.try(:widget_color) -json.website_url @inbox.channel.try(:website_url) -json.welcome_title @inbox.channel.try(:welcome_title) -json.welcome_tagline @inbox.channel.try(:welcome_tagline) -json.web_widget_script @inbox.channel.try(:web_widget_script) -json.enable_auto_assignment @inbox.enable_auto_assignment +json.partial! 'api/v1/models/inbox.json.jbuilder', resource: @inbox diff --git a/app/views/api/v1/accounts/inboxes/index.json.jbuilder b/app/views/api/v1/accounts/inboxes/index.json.jbuilder index da52d6632..c01d66d06 100644 --- a/app/views/api/v1/accounts/inboxes/index.json.jbuilder +++ b/app/views/api/v1/accounts/inboxes/index.json.jbuilder @@ -1,19 +1,5 @@ json.payload do json.array! @inboxes do |inbox| - json.id inbox.id - json.channel_id inbox.channel_id - json.name inbox.name - json.channel_type inbox.channel_type - json.greeting_enabled inbox.greeting_enabled - json.greeting_message inbox.greeting_message - json.avatar_url inbox.try(:avatar_url) - json.page_id inbox.channel.try(:page_id) - json.widget_color inbox.channel.try(:widget_color) - json.website_url inbox.channel.try(:website_url) - json.welcome_title inbox.channel.try(:welcome_title) - json.welcome_tagline inbox.channel.try(:welcome_tagline) - json.enable_auto_assignment inbox.enable_auto_assignment - json.web_widget_script inbox.channel.try(:web_widget_script) - json.phone_number inbox.channel.try(:phone_number) + json.partial! 'api/v1/models/inbox.json.jbuilder', resource: inbox end end diff --git a/app/views/api/v1/accounts/inboxes/update.json.jbuilder b/app/views/api/v1/accounts/inboxes/update.json.jbuilder index c046402bc..981c1dec0 100644 --- a/app/views/api/v1/accounts/inboxes/update.json.jbuilder +++ b/app/views/api/v1/accounts/inboxes/update.json.jbuilder @@ -1,14 +1 @@ -json.id @inbox.id -json.channel_id @inbox.channel_id -json.name @inbox.name -json.channel_type @inbox.channel_type -json.greeting_enabled @inbox.greeting_enabled -json.greeting_message @inbox.greeting_message -json.avatar_url @inbox.try(:avatar_url) -json.website_token @inbox.channel.try(:website_token) -json.widget_color @inbox.channel.try(:widget_color) -json.website_url @inbox.channel.try(:website_url) -json.welcome_title @inbox.channel.try(:welcome_title) -json.welcome_tagline @inbox.channel.try(:welcome_tagline) -json.web_widget_script @inbox.channel.try(:web_widget_script) -json.enable_auto_assignment @inbox.enable_auto_assignment +json.partial! 'api/v1/models/inbox.json.jbuilder', resource: @inbox diff --git a/app/views/api/v1/models/_inbox.json.jbuilder b/app/views/api/v1/models/_inbox.json.jbuilder new file mode 100644 index 000000000..260b5ad52 --- /dev/null +++ b/app/views/api/v1/models/_inbox.json.jbuilder @@ -0,0 +1,16 @@ +json.id resource.id +json.channel_id resource.channel_id +json.name resource.name +json.channel_type resource.channel_type +json.greeting_enabled resource.greeting_enabled +json.greeting_message resource.greeting_message +json.avatar_url resource.try(:avatar_url) +json.page_id resource.channel.try(:page_id) +json.widget_color resource.channel.try(:widget_color) +json.website_url resource.channel.try(:website_url) +json.welcome_title resource.channel.try(:welcome_title) +json.welcome_tagline resource.channel.try(:welcome_tagline) +json.enable_auto_assignment resource.enable_auto_assignment +json.web_widget_script resource.channel.try(:web_widget_script) +json.forward_to_address resource.channel.try(:forward_to_address) +json.phone_number resource.channel.try(:phone_number) diff --git a/db/migrate/20200627115105_create_api_channel.rb b/db/migrate/20200627115105_create_api_channel.rb new file mode 100644 index 000000000..3c9e92553 --- /dev/null +++ b/db/migrate/20200627115105_create_api_channel.rb @@ -0,0 +1,9 @@ +class CreateApiChannel < ActiveRecord::Migration[6.0] + def change + create_table :channel_api do |t| + t.integer :account_id, null: false + t.string :webhook_url, null: false + t.timestamps + end + end +end diff --git a/db/migrate/20200715124113_create_email_channel.rb b/db/migrate/20200715124113_create_email_channel.rb new file mode 100644 index 000000000..ca066aacb --- /dev/null +++ b/db/migrate/20200715124113_create_email_channel.rb @@ -0,0 +1,10 @@ +class CreateEmailChannel < ActiveRecord::Migration[6.0] + def change + create_table :channel_email do |t| + t.integer :account_id, null: false + t.string :email, null: false, index: { unique: true } + t.string :forward_to_address, null: false, index: { unique: true } + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index c28dc946c..0addc1f8e 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -120,6 +120,23 @@ ActiveRecord::Schema.define(version: 2020_07_19_171437) do t.datetime "updated_at", null: false end + create_table "channel_api", force: :cascade do |t| + t.integer "account_id", null: false + t.string "webhook_url", null: false + t.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + end + + create_table "channel_email", force: :cascade do |t| + t.integer "account_id", null: false + t.string "email", null: false + t.string "forward_to_address", null: false + t.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + t.index ["email"], name: "index_channel_email_on_email", unique: true + t.index ["forward_to_address"], name: "index_channel_email_on_forward_to_address", unique: true + end + create_table "channel_facebook_pages", id: :serial, force: :cascade do |t| t.string "page_id", null: false t.string "user_access_token", null: false diff --git a/lib/integrations/facebook/message_creator.rb b/lib/integrations/facebook/message_creator.rb index abf6c93e2..2669b840a 100644 --- a/lib/integrations/facebook/message_creator.rb +++ b/lib/integrations/facebook/message_creator.rb @@ -9,10 +9,10 @@ class Integrations::Facebook::MessageCreator def perform # begin - if outgoing_message_via_echo? - create_outgoing_message + if agent_message_via_echo? + create_agent_message else - create_incoming_message + create_contact_message end # rescue => e # Raven.capture_exception(e) @@ -21,22 +21,22 @@ class Integrations::Facebook::MessageCreator private - def outgoing_message_via_echo? + def agent_message_via_echo? response.echo? && !response.sent_from_chatwoot_app? - # this means that it is an outgoing message from page, but not sent from chatwoot. - # User can send from fb page directly on mobile messenger, so this case should be handled as outgoing message + # this means that it is an agent message from page, but not sent from chatwoot. + # User can send from fb page directly on mobile / web messenger, so this case should be handled as agent message end - def create_outgoing_message + def create_agent_message Channel::FacebookPage.where(page_id: response.sender_id).each do |page| - mb = Messages::Outgoing::EchoBuilder.new(response, page.inbox, true) + mb = Messages::Facebook::MessageBuilder.new(response, page.inbox, true) mb.perform end end - def create_incoming_message + def create_contact_message Channel::FacebookPage.where(page_id: response.recipient_id).each do |page| - mb = Messages::IncomingMessageBuilder.new(response, page.inbox) + mb = Messages::Facebook::MessageBuilder.new(response, page.inbox) mb.perform end end diff --git a/lib/webhooks/trigger.rb b/lib/webhooks/trigger.rb index 57020e299..efa555845 100644 --- a/lib/webhooks/trigger.rb +++ b/lib/webhooks/trigger.rb @@ -1,6 +1,6 @@ class Webhooks::Trigger def self.execute(url, payload) - RestClient.post(url, payload) + RestClient.post(url, payload.to_json, { content_type: :json, accept: :json }) rescue StandardError => e Raven.capture_exception(e) end diff --git a/spec/builders/messages/incoming_message_builder_spec.rb b/spec/builders/messages/facebook/message_builder_spec.rb similarity index 95% rename from spec/builders/messages/incoming_message_builder_spec.rb rename to spec/builders/messages/facebook/message_builder_spec.rb index 0e91d3ae6..52edd0413 100644 --- a/spec/builders/messages/incoming_message_builder_spec.rb +++ b/spec/builders/messages/facebook/message_builder_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -describe ::Messages::IncomingMessageBuilder do +describe ::Messages::Facebook::MessageBuilder do subject(:message_builder) { described_class.new(incoming_fb_text_message, facebook_channel.inbox).perform } let!(:facebook_channel) { create(:channel_facebook_page) } diff --git a/spec/builders/messages/message_builder_spec.rb b/spec/builders/messages/message_builder_spec.rb new file mode 100644 index 000000000..6227f2a48 --- /dev/null +++ b/spec/builders/messages/message_builder_spec.rb @@ -0,0 +1,54 @@ +require 'rails_helper' + +describe ::Messages::MessageBuilder do + subject(:message_builder) { described_class.new(user, conversation, params).perform } + + let(:account) { create(:account) } + let(:user) { create(:user, account: account) } + let(:inbox) { create(:inbox, account: account) } + let(:inbox_member) { create(:inbox_member, inbox: inbox, account: account) } + let(:conversation) { create(:conversation, inbox: inbox, account: account) } + let(:params) do + ActionController::Parameters.new({ + content: 'test' + }) + end + + describe '#perform' do + it 'creates a message' do + message = message_builder + expect(message.content).to eq params[:content] + end + end + + describe '#perform when message_type is incoming' do + context 'when channel is not api' do + let(:params) do + ActionController::Parameters.new({ + content: 'test', + message_type: 'incoming' + }) + end + + it 'creates throws error when channel is not api' do + expect { message_builder }.to raise_error 'Incoming messages are only allowed in Api inboxes' + end + end + + context 'when channel is api' do + let(:channel_api) { create(:channel_api, account: account) } + let(:conversation) { create(:conversation, inbox: channel_api.inbox, account: account) } + let(:params) do + ActionController::Parameters.new({ + content: 'test', + message_type: 'incoming' + }) + end + + it 'creates message when channel is api' do + message = message_builder + expect(message.message_type).to eq params[:message_type] + end + end + end +end diff --git a/spec/controllers/api/v1/accounts/contacts_controller_spec.rb b/spec/controllers/api/v1/accounts/contacts_controller_spec.rb index 1a1b82442..16cd13593 100644 --- a/spec/controllers/api/v1/accounts/contacts_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/contacts_controller_spec.rb @@ -65,6 +65,7 @@ RSpec.describe 'Contacts API', type: :request do context 'when it is an authenticated user' do let(:admin) { create(:user, account: account, role: :administrator) } + let(:inbox) { create(:inbox, account: account) } it 'creates the contact' do expect do @@ -74,6 +75,15 @@ RSpec.describe 'Contacts API', type: :request do expect(response).to have_http_status(:success) end + + it 'creates the contact identifier 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 }) + end.to change(ContactInbox, :count).by(1) + + expect(response).to have_http_status(:success) + end end end diff --git a/spec/factories/channel/channel_api.rb b/spec/factories/channel/channel_api.rb new file mode 100644 index 000000000..7a01b5355 --- /dev/null +++ b/spec/factories/channel/channel_api.rb @@ -0,0 +1,9 @@ +FactoryBot.define do + factory :channel_api, class: 'Channel::Api' do + webhook_url { 'http://example.com' } + account + after(:create) do |channel_api| + create(:inbox, channel: channel_api, account: channel_api.account) + end + end +end diff --git a/spec/lib/webhooks/trigger_spec.rb b/spec/lib/webhooks/trigger_spec.rb index 17564510e..7608634ac 100644 --- a/spec/lib/webhooks/trigger_spec.rb +++ b/spec/lib/webhooks/trigger_spec.rb @@ -5,10 +5,10 @@ describe Webhooks::Trigger do describe '#execute' do it 'triggers webhook' do - params = { hello: 'hello' } - url = 'htpps://test.com' + params = { hello: :hello } + url = 'https://test.com' - expect(RestClient).to receive(:post).with(url, params).once + expect(RestClient).to receive(:post).with(url, params.to_json, { accept: :json, content_type: :json }).once trigger.execute(url, params) end end From fcb7625616a8bb657a9eb4e6349c3f64df365f26 Mon Sep 17 00:00:00 2001 From: Sony Mathew Date: Tue, 21 Jul 2020 12:20:46 +0530 Subject: [PATCH 24/50] Bug: Fixed quoted text parsing in incoming email (#1076) * Bugfix: Fixed quoted text parsing in incoming email We had changed the support email from an account based only value to account based with fallback on global config and environment variable. Incoming email quoted text parsing was still based on account based support email. Changed this to utilize the newly introduced fallback values from global config and environment for parsing quoted text. * Bugfix: Added one more sender agnostic regex for quoted text parsing --- app/presenters/mail_presenter.rb | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/app/presenters/mail_presenter.rb b/app/presenters/mail_presenter.rb index cd3d75fc4..f97dd1b4c 100644 --- a/app/presenters/mail_presenter.rb +++ b/app/presenters/mail_presenter.rb @@ -85,18 +85,30 @@ class MailPresenter < SimpleDelegator end def quoted_text_regexes - sender_agnostic_regexes = [ + return sender_agnostic_regexes if @account.nil? || account_support_email.blank? + + [ + Regexp.new("From:\s*" + Regexp.escape(account_support_email), Regexp::IGNORECASE), + Regexp.new('<' + Regexp.escape(account_support_email) + '>', Regexp::IGNORECASE), + Regexp.new(Regexp.escape(account_support_email) + "\s+wrote:", Regexp::IGNORECASE), + Regexp.new('On(.*)' + Regexp.escape(account_support_email) + '(.*)wrote:', Regexp::IGNORECASE) + ] + sender_agnostic_regexes + end + + def sender_agnostic_regexes + @sender_agnostic_regexes ||= [ Regexp.new("^.*On.*(\n)?wrote:$", Regexp::IGNORECASE), + Regexp.new('^.*On(.*)(.*)wrote:$', Regexp::IGNORECASE), Regexp.new("-+original\s+message-+\s*$", Regexp::IGNORECASE), Regexp.new("from:\s*$", Regexp::IGNORECASE) ] - return sender_agnostic_regexes if @account.nil? || @account.support_email.blank? + end - [ - Regexp.new("From:\s*" + Regexp.escape(@account.support_email), Regexp::IGNORECASE), - Regexp.new('<' + Regexp.escape(@account.support_email) + '>', Regexp::IGNORECASE), - Regexp.new(Regexp.escape(@account.support_email) + "\s+wrote:", Regexp::IGNORECASE), - Regexp.new('On(.*)' + Regexp.escape(@account.support_email) + '(.*)wrote:', Regexp::IGNORECASE) - ] + sender_agnostic_regexes + def account_support_email + @account_support_email ||= begin + @account.support_email || + GlobalConfig.get('MAILER_SUPPORT_EMAIL')['MAILER_SUPPORT_EMAIL'] || + ENV.fetch('MAILER_SENDER_EMAIL', nil) + end end end From d6f309ce22cc6c63a2a14fa5f93ec2bd35baedc5 Mon Sep 17 00:00:00 2001 From: Sojan Jose Date: Tue, 21 Jul 2020 20:11:22 +0530 Subject: [PATCH 25/50] Chore: Initialize Cypress tests (#1078) Addresses: #412 Co-authored-by: Pranav Raj S --- .gitignore | 4 + Gemfile | 7 + Gemfile.lock | 5 + Procfile.test | 3 + .../components/buttons/FormSubmitButton.vue | 1 + .../dashboard/routes/login/Login.vue | 2 + config/initializers/cypress_on_rails.rb | 9 + docs/README.md | 5 +- docs/development/project-setup/quick-setup.md | 16 +- package.json | 1 + spec/cypress.json | 4 + .../app_commands/activerecord_fixtures.rb | 22 + spec/cypress/app_commands/clean.rb | 10 + spec/cypress/app_commands/eval.rb | 1 + spec/cypress/app_commands/factory_bot.rb | 12 + spec/cypress/app_commands/load_seed.rb | 1 + spec/cypress/app_commands/log_fail.rb | 23 ++ .../cypress/app_commands/scenarios/default.rb | 1 + spec/cypress/cypress_helper.rb | 33 ++ .../admin_dashboard_authentication.js | 20 + spec/cypress/plugins/index.js | 21 + spec/cypress/support/commands.js | 25 ++ spec/cypress/support/index.js | 21 + spec/cypress/support/on-rails.js | 54 +++ yarn.lock | 391 ++++++++++++++++-- 25 files changed, 654 insertions(+), 38 deletions(-) create mode 100644 Procfile.test create mode 100644 config/initializers/cypress_on_rails.rb create mode 100644 spec/cypress.json create mode 100644 spec/cypress/app_commands/activerecord_fixtures.rb create mode 100644 spec/cypress/app_commands/clean.rb create mode 100644 spec/cypress/app_commands/eval.rb create mode 100644 spec/cypress/app_commands/factory_bot.rb create mode 100644 spec/cypress/app_commands/load_seed.rb create mode 100644 spec/cypress/app_commands/log_fail.rb create mode 100644 spec/cypress/app_commands/scenarios/default.rb create mode 100644 spec/cypress/cypress_helper.rb create mode 100644 spec/cypress/integration/happy_paths/admin_dashboard_authentication.js create mode 100644 spec/cypress/plugins/index.js create mode 100644 spec/cypress/support/commands.js create mode 100644 spec/cypress/support/index.js create mode 100644 spec/cypress/support/on-rails.js diff --git a/.gitignore b/.gitignore index d058d20d1..3bf2fdbb1 100644 --- a/.gitignore +++ b/.gitignore @@ -55,3 +55,7 @@ node_modules package-lock.json *.dump + + +# cypress +test/cypress/videos/* \ No newline at end of file diff --git a/Gemfile b/Gemfile index a769187de..ec8cd8455 100644 --- a/Gemfile +++ b/Gemfile @@ -96,6 +96,13 @@ group :development do gem 'json_refs', git: 'https://github.com/tzmfreedom/json_refs', ref: 'e32deb0' end +group :test do + # Cypress in rails. + gem 'cypress-on-rails', '~> 1.0' + # fast cleaning of database + gem 'database_cleaner' +end + group :development, :test do # locking until https://github.com/codeclimate/test-reporter/issues/418 is resolved gem 'action-cable-testing' diff --git a/Gemfile.lock b/Gemfile.lock index 40e05f084..27235335d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -146,6 +146,9 @@ GEM crack (0.4.3) safe_yaml (~> 1.0.0) crass (1.0.6) + cypress-on-rails (1.7.0) + rack + database_cleaner (1.8.5) datetime_picker_rails (0.0.7) momentjs-rails (>= 2.8.1) declarative (0.0.10) @@ -560,6 +563,8 @@ DEPENDENCIES bullet bundle-audit byebug + cypress-on-rails (~> 1.0) + database_cleaner devise devise_token_auth dotenv-rails diff --git a/Procfile.test b/Procfile.test new file mode 100644 index 000000000..760852e80 --- /dev/null +++ b/Procfile.test @@ -0,0 +1,3 @@ +backend: RAILS_ENV=test bin/rails s -p 5050 +frontend: bin/webpack-dev-server +worker: RAILS_ENV=test bundle exec sidekiq -C config/sidekiq.yml diff --git a/app/javascript/dashboard/components/buttons/FormSubmitButton.vue b/app/javascript/dashboard/components/buttons/FormSubmitButton.vue index d8934f675..9223ebcef 100644 --- a/app/javascript/dashboard/components/buttons/FormSubmitButton.vue +++ b/app/javascript/dashboard/components/buttons/FormSubmitButton.vue @@ -1,6 +1,7 @@ diff --git a/app/javascript/dashboard/components/widgets/conversation/bubble/Text.vue b/app/javascript/dashboard/components/widgets/conversation/bubble/Text.vue index 1d266516c..1a8c44a6f 100644 --- a/app/javascript/dashboard/components/widgets/conversation/bubble/Text.vue +++ b/app/javascript/dashboard/components/widgets/conversation/bubble/Text.vue @@ -1,12 +1,24 @@ diff --git a/app/javascript/dashboard/i18n/locale/en/chatlist.json b/app/javascript/dashboard/i18n/locale/en/chatlist.json index 52f8a93c6..083aa86da 100644 --- a/app/javascript/dashboard/i18n/locale/en/chatlist.json +++ b/app/javascript/dashboard/i18n/locale/en/chatlist.json @@ -76,6 +76,7 @@ "ICON": "ion-link", "CONTENT": "has shared a url" } - } + }, + "RECEIVED_VIA_EMAIL": "Received via email" } } diff --git a/app/javascript/shared/constants/contentType.js b/app/javascript/shared/constants/contentType.js new file mode 100644 index 000000000..e5b37217f --- /dev/null +++ b/app/javascript/shared/constants/contentType.js @@ -0,0 +1,3 @@ +export const CONTENT_TYPES = { + INCOMING_EMAIL: 'incoming_email', +}; diff --git a/app/javascript/shared/mixins/contentTypeMixin.js b/app/javascript/shared/mixins/contentTypeMixin.js new file mode 100644 index 000000000..149d7b7b7 --- /dev/null +++ b/app/javascript/shared/mixins/contentTypeMixin.js @@ -0,0 +1,9 @@ +import { CONTENT_TYPES } from '../constants/contentType'; + +export default { + computed: { + isEmailContentType() { + return this.contentType === CONTENT_TYPES.INCOMING_EMAIL; + }, + }, +}; diff --git a/app/javascript/shared/mixins/specs/contentTypeMixin.spec.js b/app/javascript/shared/mixins/specs/contentTypeMixin.spec.js new file mode 100644 index 000000000..a523c0513 --- /dev/null +++ b/app/javascript/shared/mixins/specs/contentTypeMixin.spec.js @@ -0,0 +1,32 @@ +import { shallowMount } from '@vue/test-utils'; +import contentTypeMixin from '../contentTypeMixin'; + +describe('contentTypeMixin', () => { + it('returns true if contentType is incoming_email', () => { + const Component = { + render() {}, + mixins: [contentTypeMixin], + computed: { + contentType() { + return 'incoming_email'; + }, + }, + }; + const wrapper = shallowMount(Component); + expect(wrapper.vm.isEmailContentType).toBe(true); + }); + + it('returns false if contentType is not incoming_email', () => { + const Component = { + render() {}, + mixins: [contentTypeMixin], + computed: { + contentType() { + return 'input_select'; + }, + }, + }; + const wrapper = shallowMount(Component); + expect(wrapper.vm.isEmailContentType).toBe(false); + }); +}); diff --git a/app/views/api/v1/accounts/conversations/messages/index.json.jbuilder b/app/views/api/v1/accounts/conversations/messages/index.json.jbuilder index a89ea7f0c..873518df1 100644 --- a/app/views/api/v1/accounts/conversations/messages/index.json.jbuilder +++ b/app/views/api/v1/accounts/conversations/messages/index.json.jbuilder @@ -13,6 +13,7 @@ json.payload do json.inbox_id message.inbox_id json.conversation_id message.conversation.display_id json.message_type message.message_type_before_type_cast + json.content_type message.content_type json.created_at message.created_at.to_i json.private message.private json.source_id message.source_id diff --git a/package.json b/package.json index f7f59d831..ad69ca804 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,6 @@ "url-loader": "^2.0.0", "v-tooltip": "~2.0.2", "vue": "^2.6.0", - "vue-aplayer": "~0.1.1", "vue-audio": "~0.0.7", "vue-axios": "~1.2.2", "vue-chartjs": "^3.4.2", diff --git a/yarn.lock b/yarn.lock index 3c67aab7a..9d485c69a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1585,15 +1585,6 @@ anymatch@^3.0.3: normalize-path "^3.0.0" picomatch "^2.0.4" -aplayer@^1.5.8: - version "1.10.1" - resolved "https://registry.yarnpkg.com/aplayer/-/aplayer-1.10.1.tgz#318289206107452cc39e8f552fa6cc6cb459a90c" - integrity sha512-HAfyxgCUTLAqtYlxzzK9Fyqg6y+kZ9CqT1WfeWE8FSzwspT6oBqWOZHANPHF3RGTtC33IsyEgrfthPDzU5r9kQ== - dependencies: - balloon-css "^0.5.0" - promise-polyfill "7.1.0" - smoothscroll "0.4.0" - aproba@^1.0.3, aproba@^1.1.1: version "1.2.0" resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" @@ -2021,11 +2012,6 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= -balloon-css@^0.5.0: - version "0.5.2" - resolved "https://registry.yarnpkg.com/balloon-css/-/balloon-css-0.5.2.tgz#9e2163565a136c9d4aa20e8400772ce3b738d3ff" - integrity sha512-zheJpzwyNrG4t39vusA67v3BYg1HTVXOF8cErPEHzWK88PEOFwgo6Ea9VHOgOWNMgeuOtFVtB73NE2NWl9uDyQ== - base64-js@^1.0.2: version "1.3.1" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1" @@ -8539,11 +8525,6 @@ promise-inflight@^1.0.1: resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3" integrity sha1-mEcocL8igTL8vdhoEputEsPAKeM= -promise-polyfill@7.1.0: - version "7.1.0" - resolved "https://registry.yarnpkg.com/promise-polyfill/-/promise-polyfill-7.1.0.tgz#4d749485b44577c14137591c6f36e5d7e2dd3378" - integrity sha512-P6NJ2wU/8fac44ENORsuqT8TiolKGB2u0fEClPtXezn7w5cmLIjM/7mhPlTebke2EPr6tmqZbXvnX0TxwykGrg== - prompts@^2.0.1: version "2.3.1" resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.3.1.tgz#b63a9ce2809f106fa9ae1277c275b167af46ea05" @@ -9442,11 +9423,6 @@ slice-ansi@^2.1.0: astral-regex "^1.0.0" is-fullwidth-code-point "^2.0.0" -smoothscroll@0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/smoothscroll/-/smoothscroll-0.4.0.tgz#40e507b46461408ba1b787d0081e1e883c4124a5" - integrity sha512-sggQ3U2Un38b3+q/j1P4Y4fCboCtoUIaBYoge+Lb6Xg1H8RTIif/hugVr+ErMtIDpvBbhQfTjtiTeYAfbw1ZGQ== - snapdragon-node@^2.0.1: version "2.1.1" resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" @@ -10524,13 +10500,6 @@ vm-browserify@^1.0.1: resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0" integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ== -vue-aplayer@~0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/vue-aplayer/-/vue-aplayer-0.1.1.tgz#c5b486c664ac2818618ccf29a6dd1e4e2856dcdc" - integrity sha1-xbSGxmSsKBhhjM8ppt0eTihW3Nw= - dependencies: - aplayer "^1.5.8" - vue-audio@~0.0.7: version "0.0.12" resolved "https://registry.yarnpkg.com/vue-audio/-/vue-audio-0.0.12.tgz#0e4d7af19bc2cc8924a90a57e01edd7de0945cc4" From a5262a6ff3668fe8c7e5f89f70845938826b105a Mon Sep 17 00:00:00 2001 From: Pranav Raj S Date: Sat, 1 Aug 2020 23:36:59 +0530 Subject: [PATCH 42/50] chore: Fix scroll in message view (#1113) * chore: Fix scrolling in messages view * Remove console.log * Remove inline-style --- .../widgets/conversation/MessagesView.vue | 64 +++++++++++-------- 1 file changed, 37 insertions(+), 27 deletions(-) diff --git a/app/javascript/dashboard/components/widgets/conversation/MessagesView.vue b/app/javascript/dashboard/components/widgets/conversation/MessagesView.vue index e0805c08c..1951588dd 100644 --- a/app/javascript/dashboard/components/widgets/conversation/MessagesView.vue +++ b/app/javascript/dashboard/components/widgets/conversation/MessagesView.vue @@ -19,7 +19,7 @@
    -
  • +
  • @@ -52,7 +52,7 @@ @@ -155,35 +155,44 @@ export default { created() { bus.$on('scrollToMessage', () => { - this.focusLastMessage(); + setTimeout(() => this.scrollToBottom(), 0); this.makeMessagesRead(); }); }, - methods: { - focusLastMessage() { - setTimeout(() => { - this.attachListner(); - }, 0); - }, + mounted() { + this.addScrollListener(); + }, + unmounted() { + this.removeScrollListener(); + }, + + methods: { + addScrollListener() { + this.conversationPanel = this.$el.querySelector('.conversation-panel'); + this.setScrollParams(); + this.conversationPanel.addEventListener('scroll', this.handleScroll); + this.scrollToBottom(); + this.isLoadingPrevious = false; + }, + removeScrollListener() { + this.conversationPanel.removeEventListener('scroll', this.handleScroll); + }, + scrollToBottom() { + this.conversationPanel.scrollTop = this.conversationPanel.scrollHeight; + }, onToggleContactPanel() { this.$emit('contactPanelToggle'); }, - - attachListner() { - this.conversationPanel = this.$el.querySelector('.conversation-panel'); - this.heightBeforeLoad = - this.getUnreadCount === 0 - ? this.conversationPanel.scrollHeight - : this.$el.querySelector('.conversation-panel .unread--toast') - .offsetTop - 56; - this.conversationPanel.scrollTop = this.heightBeforeLoad; - this.conversationPanel.addEventListener('scroll', this.handleScroll); - this.isLoadingPrevious = false; + setScrollParams() { + this.heightBeforeLoad = this.conversationPanel.scrollHeight; + this.scrollTopBeforeLoad = this.conversationPanel.scrollTop; }, handleScroll(e) { + this.setScrollParams(); + const dataFetchCheck = this.getMessages.dataFetched === true && this.shouldLoadMoreChats; if ( @@ -198,15 +207,12 @@ export default { before: this.getMessages.messages[0].id, }) .then(() => { + const heightDifference = + this.conversationPanel.scrollHeight - this.heightBeforeLoad; this.conversationPanel.scrollTop = - this.conversationPanel.scrollHeight - - (this.heightBeforeLoad - this.conversationPanel.scrollTop); + this.scrollTopBeforeLoad + heightDifference; this.isLoadingPrevious = false; - this.heightBeforeLoad = - this.getUnreadCount === 0 - ? this.conversationPanel.scrollHeight - : this.$el.querySelector('.conversation-panel .unread--toast') - .offsetTop - 56; + this.setScrollParams(); }); } }, @@ -237,4 +243,8 @@ export default { font-size: var(--font-size-mini); } } + +.spinner--container { + min-height: var(--space-jumbo); +} From 941272cccd1bd68a74d65cba1000ff5a2bd5c75d Mon Sep 17 00:00:00 2001 From: Pranav Raj S Date: Mon, 3 Aug 2020 13:22:51 +0530 Subject: [PATCH 43/50] Bugfix: Move integration logos out of public/assets (#1115) --- .../dashboard/settings/integrations/Index.vue | 2 +- .../dashboard/settings/integrations/Integration.vue | 2 +- .../settings/integrations/ShowIntegration.vue | 2 +- config/integration/apps.yml | 3 +-- .../images}/integrations/cable.svg | 0 .../images}/integrations/slack.png | Bin 6 files changed, 4 insertions(+), 5 deletions(-) rename public/{assets/dashboard => dashboard/images}/integrations/cable.svg (100%) rename public/{assets/dashboard => dashboard/images}/integrations/slack.png (100%) diff --git a/app/javascript/dashboard/routes/dashboard/settings/integrations/Index.vue b/app/javascript/dashboard/routes/dashboard/settings/integrations/Index.vue index 6d8ca1895..5bfeceac2 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/integrations/Index.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/integrations/Index.vue @@ -1,7 +1,7 @@