diff --git a/Gemfile b/Gemfile index 59bfc5d7f..7f44af03d 100644 --- a/Gemfile +++ b/Gemfile @@ -165,7 +165,7 @@ gem 'audited', '~> 5.4', '>= 5.4.1' # need for google auth gem 'omniauth', '>= 2.1.2' -gem 'omniauth-google-oauth2' +gem 'omniauth-google-oauth2', '>= 1.1.2' gem 'omniauth-rails_csrf_protection', '~> 1.0' ## Gems for reponse bot diff --git a/Gemfile.lock b/Gemfile.lock index 41cb0f20b..0f1595a72 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -237,9 +237,8 @@ GEM railties (>= 5.0.0) faker (3.2.0) i18n (>= 1.8.11, < 2) - faraday (2.7.4) - faraday-net_http (>= 2.0, < 3.1) - ruby2_keywords (>= 0.0.4) + faraday (2.9.0) + faraday-net_http (>= 2.0, < 3.2) faraday-follow_redirects (0.3.0) faraday (>= 1, < 3) faraday-mashify (0.1.1) @@ -247,7 +246,8 @@ GEM hashie faraday-multipart (1.0.4) multipart-post (~> 2) - faraday-net_http (3.0.2) + faraday-net_http (3.1.0) + net-http faraday-net_http_persistent (2.1.0) faraday (~> 2.5) net-http-persistent (~> 4.0) @@ -394,7 +394,8 @@ GEM hana (~> 1.3) regexp_parser (~> 2.0) uri_template (~> 0.7) - jwt (2.7.0) + jwt (2.8.1) + base64 kaminari (1.2.2) activesupport (>= 4.1.0) kaminari-actionview (= 1.2.2) @@ -470,6 +471,8 @@ GEM multipart-post (2.3.0) neighbor (0.2.3) activerecord (>= 5.2) + net-http (0.4.1) + uri net-http-persistent (4.0.2) connection_pool (~> 2.2) net-imap (0.4.9) @@ -515,11 +518,11 @@ GEM hashie (>= 3.4.6) rack (>= 2.2.3) rack-protection - omniauth-google-oauth2 (1.1.1) + omniauth-google-oauth2 (1.1.2) jwt (>= 2.0) - oauth2 (~> 2.0.6) + oauth2 (~> 2.0) omniauth (~> 2.0) - omniauth-oauth2 (~> 1.8.0) + omniauth-oauth2 (~> 1.8) omniauth-oauth2 (1.8.0) oauth2 (>= 1.4, < 3) omniauth (~> 2.0) @@ -559,7 +562,7 @@ GEM activesupport (>= 3.0.0) raabro (1.4.0) racc (1.7.3) - rack (2.2.8.1) + rack (2.2.9) rack-attack (6.7.0) rack (>= 1.0, < 4) rack-contrib (2.4.0) @@ -568,7 +571,8 @@ GEM rack (>= 2.0.0) rack-mini-profiler (3.2.0) rack (>= 1.2.0) - rack-protection (3.1.0) + rack-protection (3.2.0) + base64 (>= 0.1.0) rack (~> 2.2, >= 2.2.4) rack-proxy (0.7.6) rack @@ -790,11 +794,12 @@ GEM unf_ext (0.0.8.2) unicode-display_width (2.4.2) uniform_notifier (1.16.0) + uri (0.13.0) uri_template (0.7.0) valid_email2 (4.0.6) activemodel (>= 3.2) mail (~> 2.5) - version_gem (1.1.3) + version_gem (1.1.4) warden (1.2.9) rack (>= 2.0.9) web-console (4.2.1) @@ -907,7 +912,7 @@ DEPENDENCIES newrelic-sidekiq-metrics (>= 1.6.2) newrelic_rpm omniauth (>= 2.1.2) - omniauth-google-oauth2 + omniauth-google-oauth2 (>= 1.1.2) omniauth-oauth2 omniauth-rails_csrf_protection (~> 1.0) pg diff --git a/app/builders/messages/facebook/message_builder.rb b/app/builders/messages/facebook/message_builder.rb index fec298bce..2c55922f6 100644 --- a/app/builders/messages/facebook/message_builder.rb +++ b/app/builders/messages/facebook/message_builder.rb @@ -53,7 +53,23 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder end def conversation - @conversation ||= Conversation.find_by(conversation_params) || build_conversation + @conversation ||= set_conversation_based_on_inbox_config + end + + def set_conversation_based_on_inbox_config + if @inbox.lock_to_single_conversation + Conversation.where(conversation_params).order(created_at: :desc).first || build_conversation + else + find_or_build_for_multiple_conversations + end + end + + def find_or_build_for_multiple_conversations + # If lock to single conversation is disabled, we will create a new conversation if previous conversation is resolved + last_conversation = Conversation.where(conversation_params).where.not(status: :resolved).order(created_at: :desc).first + return build_conversation if last_conversation.nil? + + last_conversation end def build_conversation diff --git a/app/builders/messages/instagram/message_builder.rb b/app/builders/messages/instagram/message_builder.rb index e9debb767..7f1b9cab2 100644 --- a/app/builders/messages/instagram/message_builder.rb +++ b/app/builders/messages/instagram/message_builder.rb @@ -69,9 +69,28 @@ class Messages::Instagram::MessageBuilder < Messages::Messenger::MessageBuilder end def conversation - @conversation ||= Conversation.where(conversation_params).find_by( - "additional_attributes ->> 'type' = 'instagram_direct_message'" - ) || build_conversation + @conversation ||= set_conversation_based_on_inbox_config + end + + def instagram_direct_message_conversation + Conversation.where(conversation_params) + .where("additional_attributes ->> 'type' = 'instagram_direct_message'") + end + + def set_conversation_based_on_inbox_config + if @inbox.lock_to_single_conversation + instagram_direct_message_conversation.order(created_at: :desc).first || build_conversation + else + find_or_build_for_multiple_conversations + end + end + + def find_or_build_for_multiple_conversations + last_conversation = instagram_direct_message_conversation.where.not(status: :resolved).order(created_at: :desc).first + + return build_conversation if last_conversation.nil? + + last_conversation end def message_content diff --git a/app/controllers/api/v1/accounts/agents_controller.rb b/app/controllers/api/v1/accounts/agents_controller.rb index eff9975f7..4eff10127 100644 --- a/app/controllers/api/v1/accounts/agents_controller.rb +++ b/app/controllers/api/v1/accounts/agents_controller.rb @@ -19,7 +19,7 @@ class Api::V1::Accounts::AgentsController < Api::V1::Accounts::BaseController account: Current.account ) - builder.perform + @agent = builder.perform end def update diff --git a/app/controllers/api/v1/accounts/channels/twilio_channels_controller.rb b/app/controllers/api/v1/accounts/channels/twilio_channels_controller.rb index d2d51baef..ebf8e49dd 100644 --- a/app/controllers/api/v1/accounts/channels/twilio_channels_controller.rb +++ b/app/controllers/api/v1/accounts/channels/twilio_channels_controller.rb @@ -2,13 +2,9 @@ class Api::V1::Accounts::Channels::TwilioChannelsController < Api::V1::Accounts: before_action :authorize_request def create - ActiveRecord::Base.transaction do - authenticate_twilio - build_inbox - setup_webhooks if @twilio_channel.sms? - rescue StandardError => e - render_could_not_create_error(e.message) - end + process_create + rescue StandardError => e + render_could_not_create_error(e.message) end private @@ -17,6 +13,14 @@ class Api::V1::Accounts::Channels::TwilioChannelsController < Api::V1::Accounts: authorize ::Inbox end + def process_create + ActiveRecord::Base.transaction do + authenticate_twilio + build_inbox + setup_webhooks if @twilio_channel.sms? + end + end + def authenticate_twilio client = if permitted_params[:api_key_sid].present? Twilio::REST::Client.new(permitted_params[:api_key_sid], permitted_params[:auth_token], permitted_params[:account_sid]) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index d2960a699..2f389049d 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -25,3 +25,4 @@ class ApplicationController < ActionController::Base } end end +ApplicationController.include_mod_with('Concerns::ApplicationControllerConcern') diff --git a/app/finders/conversation_finder.rb b/app/finders/conversation_finder.rb index f3c85be7a..44592c201 100644 --- a/app/finders/conversation_finder.rb +++ b/app/finders/conversation_finder.rb @@ -163,10 +163,14 @@ class ConversationFinder params[:page] || 1 end - def conversations - @conversations = @conversations.includes( + def conversations_base_query + @conversations.includes( :taggings, :inbox, { assignee: { avatar_attachment: [:blob] } }, { contact: { avatar_attachment: [:blob] } }, :team, :contact_inbox ) + end + + def conversations + @conversations = conversations_base_query sort_by, sort_order = SORT_OPTIONS[params[:sort_by]] || SORT_OPTIONS['last_activity_at_desc'] @conversations = @conversations.send(sort_by, sort_order) @@ -178,3 +182,4 @@ class ConversationFinder end end end +ConversationFinder.prepend_mod_with('ConversationFinder') diff --git a/app/javascript/dashboard/api/slaReports.js b/app/javascript/dashboard/api/slaReports.js new file mode 100644 index 000000000..c187c58a3 --- /dev/null +++ b/app/javascript/dashboard/api/slaReports.js @@ -0,0 +1,72 @@ +/* global axios */ +import ApiClient from './ApiClient'; + +class SLAReportsAPI extends ApiClient { + constructor() { + super('applied_slas', { accountScoped: true }); + } + + get({ + from, + to, + assigned_agent_id, + inbox_id, + team_id, + sla_policy_id, + page, + } = {}) { + return axios.get(this.url, { + params: { + since: from, + until: to, + assigned_agent_id, + inbox_id, + team_id, + sla_policy_id, + page, + }, + }); + } + + download({ + from, + to, + assigned_agent_id, + inbox_id, + team_id, + sla_policy_id, + } = {}) { + return axios.get(`${this.url}/download`, { + params: { + since: from, + until: to, + assigned_agent_id, + inbox_id, + team_id, + sla_policy_id, + }, + }); + } + + getMetrics({ + from, + to, + assigned_agent_id, + inbox_id, + team_id, + sla_policy_id, + } = {}) { + return axios.get(`${this.url}/metrics`, { + params: { + since: from, + until: to, + assigned_agent_id, + inbox_id, + team_id, + sla_policy_id, + }, + }); + } +} + +export default new SLAReportsAPI(); diff --git a/app/javascript/dashboard/api/specs/slaReports.spec.js b/app/javascript/dashboard/api/specs/slaReports.spec.js new file mode 100644 index 000000000..51ac8bfe4 --- /dev/null +++ b/app/javascript/dashboard/api/specs/slaReports.spec.js @@ -0,0 +1,98 @@ +import SLAReportsAPI from '../slaReports'; +import ApiClient from '../ApiClient'; + +describe('#SLAReports API', () => { + it('creates correct instance', () => { + expect(SLAReportsAPI).toBeInstanceOf(ApiClient); + expect(SLAReportsAPI.apiVersion).toBe('/api/v1'); + expect(SLAReportsAPI).toHaveProperty('get'); + expect(SLAReportsAPI).toHaveProperty('getMetrics'); + }); + + describe('API calls', () => { + const originalAxios = window.axios; + const axiosMock = { + post: jest.fn(() => Promise.resolve()), + get: jest.fn(() => Promise.resolve()), + patch: jest.fn(() => Promise.resolve()), + delete: jest.fn(() => Promise.resolve()), + }; + + beforeEach(() => { + window.axios = axiosMock; + }); + + afterEach(() => { + window.axios = originalAxios; + }); + + it('#get', () => { + SLAReportsAPI.get({ + page: 1, + from: 1622485800, + to: 1623695400, + assigned_agent_id: 1, + inbox_id: 1, + team_id: 1, + sla_policy_id: 1, + }); + expect(axiosMock.get).toHaveBeenCalledWith('/api/v1/applied_slas', { + params: { + page: 1, + since: 1622485800, + until: 1623695400, + assigned_agent_id: 1, + inbox_id: 1, + team_id: 1, + sla_policy_id: 1, + }, + }); + }); + it('#getMetrics', () => { + SLAReportsAPI.getMetrics({ + from: 1622485800, + to: 1623695400, + assigned_agent_id: 1, + inbox_id: 1, + team_id: 1, + sla_policy_id: 1, + }); + expect(axiosMock.get).toHaveBeenCalledWith( + '/api/v1/applied_slas/metrics', + { + params: { + since: 1622485800, + until: 1623695400, + assigned_agent_id: 1, + inbox_id: 1, + team_id: 1, + sla_policy_id: 1, + }, + } + ); + }); + it('#download', () => { + SLAReportsAPI.download({ + from: 1622485800, + to: 1623695400, + assigned_agent_id: 1, + inbox_id: 1, + team_id: 1, + sla_policy_id: 1, + }); + expect(axiosMock.get).toHaveBeenCalledWith( + '/api/v1/applied_slas/download', + { + params: { + since: 1622485800, + until: 1623695400, + assigned_agent_id: 1, + inbox_id: 1, + team_id: 1, + sla_policy_id: 1, + }, + } + ); + }); + }); +}); diff --git a/app/javascript/dashboard/components/ChatList.vue b/app/javascript/dashboard/components/ChatList.vue index a75f0c12b..26c057f98 100644 --- a/app/javascript/dashboard/components/ChatList.vue +++ b/app/javascript/dashboard/components/ChatList.vue @@ -1,14 +1,14 @@