diff --git a/.env.example b/.env.example index 26b1487ae..1b57141f0 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,9 @@ +# Learn about the various environment variables at +# https://www.chatwoot.com/docs/self-hosted/configuration/environment-variables/#rails-production-variables + # Used to verify the integrity of signed cookies. so ensure a secure value is set +# SECRET_KEY_BASE should be alphanumeric. Avoid special characters or symbols. +# Use `rake secret` to generate this variable SECRET_KEY_BASE=replace_with_lengthy_secure_hex # Replace with the URL you are planning to use for your app @@ -184,12 +189,6 @@ ANDROID_SHA256_CERT_FINGERPRINT=AC:73:8E:DE:EB:56:EA:CC:10:87:02:A7:65:37:7B:38: # SENTRY_DSN= -# MICROSOFT CLARITY -# MS_CLARITY_TOKEN=xxxxxxxxx - -# GOOGLE_TAG_MANAGER -# GOOGLE_TAG = GTM-XXXXXXX - ## Scout ## https://scoutapm.com/docs/ruby/configuration # SCOUT_KEY=YOURKEY diff --git a/Gemfile b/Gemfile index 1b2ed90b9..302094e8f 100644 --- a/Gemfile +++ b/Gemfile @@ -4,7 +4,7 @@ ruby '3.2.2' ##-- base gems for rails --## gem 'rack-cors', '2.0.0', require: 'rack/cors' -gem 'rails', '~> 7.0.8.1' +gem 'rails', '~> 7.0.8.4' # Reduces boot times through caching; required in config/boot.rb gem 'bootsnap', require: false @@ -61,7 +61,7 @@ gem 'redis-namespace' gem 'activerecord-import' ##--- gems for server & infra configuration ---## -gem 'dotenv-rails' +gem 'dotenv-rails', '>= 3.0.0' gem 'foreman' gem 'puma' gem 'webpacker' @@ -122,7 +122,7 @@ gem 'sidekiq-cron', '>= 1.12.0' ##-- Push notification service --## gem 'fcm' -gem 'web-push' +gem 'web-push', '>= 3.0.1' ##-- geocoding / parse location from ip --## # http://www.rubygeocoder.com/ @@ -228,7 +228,7 @@ group :development, :test do gem 'mock_redis' gem 'pry-rails' gem 'rspec_junit_formatter' - gem 'rspec-rails' + gem 'rspec-rails', '>= 6.0.3' gem 'rubocop', require: false gem 'rubocop-performance', require: false gem 'rubocop-rails', require: false diff --git a/Gemfile.lock b/Gemfile.lock index 6b806fc3d..b2b319966 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -33,70 +33,70 @@ GIT GEM remote: https://rubygems.org/ specs: - actioncable (7.0.8.3) - actionpack (= 7.0.8.3) - activesupport (= 7.0.8.3) + actioncable (7.0.8.4) + actionpack (= 7.0.8.4) + activesupport (= 7.0.8.4) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailbox (7.0.8.3) - actionpack (= 7.0.8.3) - activejob (= 7.0.8.3) - activerecord (= 7.0.8.3) - activestorage (= 7.0.8.3) - activesupport (= 7.0.8.3) + actionmailbox (7.0.8.4) + actionpack (= 7.0.8.4) + activejob (= 7.0.8.4) + activerecord (= 7.0.8.4) + activestorage (= 7.0.8.4) + activesupport (= 7.0.8.4) mail (>= 2.7.1) net-imap net-pop net-smtp - actionmailer (7.0.8.3) - actionpack (= 7.0.8.3) - actionview (= 7.0.8.3) - activejob (= 7.0.8.3) - activesupport (= 7.0.8.3) + actionmailer (7.0.8.4) + actionpack (= 7.0.8.4) + actionview (= 7.0.8.4) + activejob (= 7.0.8.4) + activesupport (= 7.0.8.4) mail (~> 2.5, >= 2.5.4) net-imap net-pop net-smtp rails-dom-testing (~> 2.0) - actionpack (7.0.8.3) - actionview (= 7.0.8.3) - activesupport (= 7.0.8.3) + actionpack (7.0.8.4) + actionview (= 7.0.8.4) + activesupport (= 7.0.8.4) rack (~> 2.0, >= 2.2.4) rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.2.0) - actiontext (7.0.8.3) - actionpack (= 7.0.8.3) - activerecord (= 7.0.8.3) - activestorage (= 7.0.8.3) - activesupport (= 7.0.8.3) + actiontext (7.0.8.4) + actionpack (= 7.0.8.4) + activerecord (= 7.0.8.4) + activestorage (= 7.0.8.4) + activesupport (= 7.0.8.4) globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (7.0.8.3) - activesupport (= 7.0.8.3) + actionview (7.0.8.4) + activesupport (= 7.0.8.4) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.1, >= 1.2.0) active_record_query_trace (1.8) - activejob (7.0.8.3) - activesupport (= 7.0.8.3) + activejob (7.0.8.4) + activesupport (= 7.0.8.4) globalid (>= 0.3.6) - activemodel (7.0.8.3) - activesupport (= 7.0.8.3) - activerecord (7.0.8.3) - activemodel (= 7.0.8.3) - activesupport (= 7.0.8.3) + activemodel (7.0.8.4) + activesupport (= 7.0.8.4) + activerecord (7.0.8.4) + activemodel (= 7.0.8.4) + activesupport (= 7.0.8.4) activerecord-import (1.4.1) activerecord (>= 4.2) - activestorage (7.0.8.3) - actionpack (= 7.0.8.3) - activejob (= 7.0.8.3) - activerecord (= 7.0.8.3) - activesupport (= 7.0.8.3) + activestorage (7.0.8.4) + actionpack (= 7.0.8.4) + activejob (= 7.0.8.4) + activerecord (= 7.0.8.4) + activesupport (= 7.0.8.4) marcel (~> 1.0) mini_mime (>= 1.1.0) - activesupport (7.0.8.3) + activesupport (7.0.8.4) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) @@ -169,7 +169,7 @@ GEM climate_control (1.2.0) coderay (1.1.3) commonmarker (0.23.10) - concurrent-ruby (1.2.3) + concurrent-ruby (1.3.1) connection_pool (2.4.1) crack (0.4.5) rexml @@ -204,16 +204,16 @@ GEM bcrypt (~> 3.0) devise (> 3.5.2, < 5) rails (>= 4.2.0, < 7.2) - diff-lcs (1.5.0) + diff-lcs (1.5.1) digest-crc (0.6.4) rake (>= 12.0.0, < 14.0.0) docile (1.4.0) domain_name (0.5.20190701) unf (>= 0.0.5, < 1.0.0) - dotenv (2.8.1) - dotenv-rails (2.8.1) - dotenv (= 2.8.1) - railties (>= 3.2) + dotenv (3.1.2) + dotenv-rails (3.1.2) + dotenv (= 3.1.2) + railties (>= 6.1) down (5.4.0) addressable (~> 2.8) ecma-re-validator (0.4.0) @@ -355,7 +355,6 @@ GEM hana (1.3.7) hashdiff (1.0.1) hashie (5.0.0) - hkdf (1.0.0) http (5.1.1) addressable (~> 2.8) http-cookie (~> 1.0) @@ -460,8 +459,8 @@ GEM mime-types-data (3.2023.0218.1) mini_magick (4.12.0) mini_mime (1.1.5) - mini_portile2 (2.8.6) - minitest (5.23.0) + mini_portile2 (2.8.7) + minitest (5.23.1) mock_redis (0.36.0) ruby2_keywords msgpack (1.7.0) @@ -474,7 +473,7 @@ GEM uri net-http-persistent (4.0.2) connection_pool (~> 2.2) - net-imap (0.4.11) + net-imap (0.4.12) date net-protocol net-pop (0.1.2) @@ -527,7 +526,7 @@ GEM omniauth-rails_csrf_protection (1.0.2) actionpack (>= 4.2) omniauth (~> 2.0) - openssl (3.1.0) + openssl (3.2.0) orm_adapter (0.5.0) os (1.1.4) parallel (1.23.0) @@ -551,11 +550,11 @@ GEM pundit (2.3.0) activesupport (>= 3.0.0) raabro (1.4.0) - racc (1.7.3) + racc (1.8.0) rack (2.2.9) rack-attack (6.7.0) rack (>= 1.0, < 4) - rack-contrib (2.4.0) + rack-contrib (2.5.0) rack (< 4) rack-cors (2.0.0) rack (>= 2.0.0) @@ -569,20 +568,20 @@ GEM rack-test (2.1.0) rack (>= 1.3) rack-timeout (0.6.3) - rails (7.0.8.3) - actioncable (= 7.0.8.3) - actionmailbox (= 7.0.8.3) - actionmailer (= 7.0.8.3) - actionpack (= 7.0.8.3) - actiontext (= 7.0.8.3) - actionview (= 7.0.8.3) - activejob (= 7.0.8.3) - activemodel (= 7.0.8.3) - activerecord (= 7.0.8.3) - activestorage (= 7.0.8.3) - activesupport (= 7.0.8.3) + rails (7.0.8.4) + actioncable (= 7.0.8.4) + actionmailbox (= 7.0.8.4) + actionmailer (= 7.0.8.4) + actionpack (= 7.0.8.4) + actiontext (= 7.0.8.4) + actionview (= 7.0.8.4) + activejob (= 7.0.8.4) + activemodel (= 7.0.8.4) + activerecord (= 7.0.8.4) + activestorage (= 7.0.8.4) + activesupport (= 7.0.8.4) bundler (>= 1.15.0) - railties (= 7.0.8.3) + railties (= 7.0.8.4) rails-dom-testing (2.2.0) activesupport (>= 5.0.0) minitest @@ -590,9 +589,9 @@ GEM rails-html-sanitizer (1.6.0) loofah (~> 2.21) nokogiri (~> 1.14) - railties (7.0.8.3) - actionpack (= 7.0.8.3) - activesupport (= 7.0.8.3) + railties (7.0.8.4) + actionpack (= 7.0.8.4) + activesupport (= 7.0.8.4) method_source rake (>= 12.2) thor (~> 1.0) @@ -630,23 +629,23 @@ GEM nokogiri rexml (3.2.8) strscan (>= 3.0.9) - rspec-core (3.12.2) - rspec-support (~> 3.12.0) - rspec-expectations (3.12.3) + rspec-core (3.13.0) + rspec-support (~> 3.13.0) + rspec-expectations (3.13.0) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.12.0) - rspec-mocks (3.12.5) + rspec-support (~> 3.13.0) + rspec-mocks (3.13.1) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.12.0) - rspec-rails (6.0.2) + rspec-support (~> 3.13.0) + rspec-rails (6.1.2) actionpack (>= 6.1) activesupport (>= 6.1) railties (>= 6.1) - rspec-core (~> 3.12) - rspec-expectations (~> 3.12) - rspec-mocks (~> 3.12) - rspec-support (~> 3.12) - rspec-support (3.12.0) + rspec-core (~> 3.13) + rspec-expectations (~> 3.13) + rspec-mocks (~> 3.13) + rspec-support (~> 3.13) + rspec-support (3.13.1) rspec_junit_formatter (0.6.0) rspec-core (>= 2, < 4, != 2.12.0) rubocop (1.50.2) @@ -800,8 +799,7 @@ GEM activemodel (>= 6.0.0) bindex (>= 0.4.0) railties (>= 6.0.0) - web-push (3.0.0) - hkdf (~> 1.0) + web-push (3.0.1) jwt (~> 2.0) openssl (~> 3.0) webmock (3.18.1) @@ -821,7 +819,7 @@ GEM working_hours (1.4.1) activesupport (>= 3.2) tzinfo - zeitwerk (2.6.14) + zeitwerk (2.6.15) PLATFORMS arm64-darwin-20 @@ -863,7 +861,7 @@ DEPENDENCIES devise (>= 4.9.4) devise-secure_password! devise_token_auth (>= 1.2.3) - dotenv-rails + dotenv-rails (>= 3.0.0) down elastic-apm email_reply_trimmer @@ -918,13 +916,13 @@ DEPENDENCIES rack-cors (= 2.0.0) rack-mini-profiler (>= 3.2.0) rack-timeout - rails (~> 7.0.8.1) + rails (~> 7.0.8.4) redis redis-namespace responders (>= 3.1.1) rest-client reverse_markdown - rspec-rails + rspec-rails (>= 6.0.3) rspec_junit_formatter rubocop rubocop-performance @@ -955,7 +953,7 @@ DEPENDENCIES uglifier valid_email2 web-console (>= 4.2.1) - web-push + web-push (>= 3.0.1) webmock webpacker wisper (= 2.0.0) diff --git a/VERSION_CW b/VERSION_CW index bea438e9a..a5c4c7633 100644 --- a/VERSION_CW +++ b/VERSION_CW @@ -1 +1 @@ -3.3.1 +3.9.0 diff --git a/VERSION_CWCTL b/VERSION_CWCTL index 24ba9a38d..834f26295 100644 --- a/VERSION_CWCTL +++ b/VERSION_CWCTL @@ -1 +1 @@ -2.7.0 +2.8.0 diff --git a/app/builders/v2/reports/conversations/base_report_builder.rb b/app/builders/v2/reports/conversations/base_report_builder.rb new file mode 100644 index 000000000..a7961b0d6 --- /dev/null +++ b/app/builders/v2/reports/conversations/base_report_builder.rb @@ -0,0 +1,30 @@ +class V2::Reports::Conversations::BaseReportBuilder + pattr_initialize :account, :params + + private + + AVG_METRICS = %w[avg_first_response_time avg_resolution_time reply_time].freeze + COUNT_METRICS = %w[ + conversations_count + incoming_messages_count + outgoing_messages_count + resolutions_count + bot_resolutions_count + bot_handoffs_count + ].freeze + + def builder_class(metric) + case metric + when *AVG_METRICS + V2::Reports::Timeseries::AverageReportBuilder + when *COUNT_METRICS + V2::Reports::Timeseries::CountReportBuilder + end + end + + def log_invalid_metric + Rails.logger.error "ReportBuilder: Invalid metric - #{params[:metric]}" + + {} + end +end diff --git a/app/builders/v2/reports/conversations/metric_builder.rb b/app/builders/v2/reports/conversations/metric_builder.rb new file mode 100644 index 000000000..6635fb186 --- /dev/null +++ b/app/builders/v2/reports/conversations/metric_builder.rb @@ -0,0 +1,30 @@ +class V2::Reports::Conversations::MetricBuilder < V2::Reports::Conversations::BaseReportBuilder + def summary + { + conversations_count: count('conversations_count'), + incoming_messages_count: count('incoming_messages_count'), + outgoing_messages_count: count('outgoing_messages_count'), + avg_first_response_time: count('avg_first_response_time'), + avg_resolution_time: count('avg_resolution_time'), + resolutions_count: count('resolutions_count'), + reply_time: count('reply_time') + } + end + + def bot_summary + { + bot_resolutions_count: count('bot_resolutions_count'), + bot_handoffs_count: count('bot_handoffs_count') + } + end + + private + + def count(metric) + builder_class(metric).new(account, builder_params(metric)).aggregate_value + end + + def builder_params(metric) + params.merge({ metric: metric }) + end +end diff --git a/app/builders/v2/reports/conversations/report_builder.rb b/app/builders/v2/reports/conversations/report_builder.rb new file mode 100644 index 000000000..8f992d4c6 --- /dev/null +++ b/app/builders/v2/reports/conversations/report_builder.rb @@ -0,0 +1,21 @@ +class V2::Reports::Conversations::ReportBuilder < V2::Reports::Conversations::BaseReportBuilder + def timeseries + perform_action(:timeseries) + end + + def aggregate_value + perform_action(:aggregate_value) + end + + private + + def perform_action(method_name) + return builder.new(account, params).public_send(method_name) if builder.present? + + log_invalid_metric + end + + def builder + builder_class(params[:metric]) + end +end diff --git a/app/builders/v2/reports/timeseries/average_report_builder.rb b/app/builders/v2/reports/timeseries/average_report_builder.rb new file mode 100644 index 000000000..3e30557e1 --- /dev/null +++ b/app/builders/v2/reports/timeseries/average_report_builder.rb @@ -0,0 +1,48 @@ +class V2::Reports::Timeseries::AverageReportBuilder < V2::Reports::Timeseries::BaseTimeseriesBuilder + def timeseries + grouped_average_time = reporting_events.average(average_value_key) + grouped_event_count = reporting_events.count + grouped_average_time.each_with_object([]) do |element, arr| + event_date, average_time = element + arr << { + value: average_time, + timestamp: event_date.in_time_zone(timezone).to_i, + count: grouped_event_count[event_date] + } + end + end + + def aggregate_value + object_scope.average(average_value_key) + end + + private + + def event_name + metric_to_event_name = { + avg_first_response_time: :first_response, + avg_resolution_time: :conversation_resolved, + reply_time: :reply_time + } + metric_to_event_name[params[:metric].to_sym] + end + + def object_scope + scope.reporting_events.where(name: event_name, created_at: range) + end + + def reporting_events + @grouped_values = object_scope.group_by_period( + group_by, + :created_at, + default_value: 0, + range: range, + permit: %w[day week month year hour], + time_zone: timezone + ) + end + + def average_value_key + @average_value_key ||= params[:business_hours].present? ? :value_in_business_hours : :value + end +end diff --git a/app/builders/v2/reports/timeseries/base_timeseries_builder.rb b/app/builders/v2/reports/timeseries/base_timeseries_builder.rb new file mode 100644 index 000000000..50699417d --- /dev/null +++ b/app/builders/v2/reports/timeseries/base_timeseries_builder.rb @@ -0,0 +1,46 @@ +class V2::Reports::Timeseries::BaseTimeseriesBuilder + include TimezoneHelper + include DateRangeHelper + DEFAULT_GROUP_BY = 'day'.freeze + + pattr_initialize :account, :params + + def scope + case params[:type].to_sym + when :account + account + when :inbox + inbox + when :agent + user + when :label + label + when :team + team + end + end + + def inbox + @inbox ||= account.inboxes.find(params[:id]) + end + + def user + @user ||= account.users.find(params[:id]) + end + + def label + @label ||= account.labels.find(params[:id]) + end + + def team + @team ||= account.teams.find(params[:id]) + end + + def group_by + @group_by ||= %w[day week month year hour].include?(params[:group_by]) ? params[:group_by] : DEFAULT_GROUP_BY + end + + def timezone + @timezone ||= timezone_name_from_offset(params[:timezone_offset]) + end +end diff --git a/app/builders/v2/reports/timeseries/count_report_builder.rb b/app/builders/v2/reports/timeseries/count_report_builder.rb new file mode 100644 index 000000000..03a87a6fa --- /dev/null +++ b/app/builders/v2/reports/timeseries/count_report_builder.rb @@ -0,0 +1,71 @@ +class V2::Reports::Timeseries::CountReportBuilder < V2::Reports::Timeseries::BaseTimeseriesBuilder + def timeseries + grouped_count.each_with_object([]) do |element, arr| + event_date, event_count = element + + # The `event_date` is in Date format (without time), such as "Wed, 15 May 2024". + # We need a timestamp for the start of the day. However, we can't use `event_date.to_time.to_i` + # because it converts the date to 12:00 AM server timezone. + # The desired output should be 12:00 AM in the specified timezone. + arr << { value: event_count, timestamp: event_date.in_time_zone(timezone).to_i } + end + end + + def aggregate_value + object_scope.count + end + + private + + def metric + @metric ||= params[:metric] + end + + def object_scope + send("scope_for_#{metric}") + end + + def scope_for_conversations_count + scope.conversations.where(account_id: account.id, created_at: range) + end + + def scope_for_incoming_messages_count + scope.messages.where(account_id: account.id, created_at: range).incoming.unscope(:order) + end + + def scope_for_outgoing_messages_count + scope.messages.where(account_id: account.id, created_at: range).outgoing.unscope(:order) + end + + def scope_for_resolutions_count + scope.reporting_events.joins(:conversation).select(:conversation_id).where( + name: :conversation_resolved, + conversations: { status: :resolved }, created_at: range + ).distinct + end + + def scope_for_bot_resolutions_count + scope.reporting_events.joins(:conversation).select(:conversation_id).where( + name: :conversation_bot_resolved, + conversations: { status: :resolved }, created_at: range + ).distinct + end + + def scope_for_bot_handoffs_count + scope.reporting_events.joins(:conversation).select(:conversation_id).where( + name: :conversation_bot_handoff, + created_at: range + ).distinct + end + + def grouped_count + @grouped_values = object_scope.group_by_period( + group_by, + :created_at, + default_value: 0, + range: range, + permit: %w[day week month year hour], + time_zone: timezone + ).count + end +end diff --git a/app/controllers/api/v1/accounts/integrations/linear_controller.rb b/app/controllers/api/v1/accounts/integrations/linear_controller.rb index 9d5d76d75..814373c7e 100644 --- a/app/controllers/api/v1/accounts/integrations/linear_controller.rb +++ b/app/controllers/api/v1/accounts/integrations/linear_controller.rb @@ -88,6 +88,6 @@ class Api::V1::Accounts::Integrations::LinearController < Api::V1::Accounts::Bas end def permitted_params - params.permit(:team_id, :conversation_id, :issue_id, :link_id, :title, :description, :assignee_id, :priority, label_ids: []) + params.permit(:team_id, :project_id, :conversation_id, :issue_id, :link_id, :title, :description, :assignee_id, :priority, label_ids: []) end end diff --git a/app/controllers/api/v2/accounts/reports_controller.rb b/app/controllers/api/v2/accounts/reports_controller.rb index c67b74a43..ed5be5518 100644 --- a/app/controllers/api/v2/accounts/reports_controller.rb +++ b/app/controllers/api/v2/accounts/reports_controller.rb @@ -5,19 +5,17 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController before_action :check_authorization def index - builder = V2::ReportBuilder.new(Current.account, report_params) - data = builder.build + builder = V2::Reports::Conversations::ReportBuilder.new(Current.account, report_params) + data = builder.timeseries render json: data end def summary - render json: summary_metrics + render json: build_summary(:summary) end def bot_summary - summary = V2::ReportBuilder.new(Current.account, current_summary_params).bot_summary - summary[:previous] = V2::ReportBuilder.new(Current.account, previous_summary_params).bot_summary - render json: summary + render json: build_summary(:bot_summary) end def agents @@ -126,10 +124,11 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController } end - def summary_metrics - summary = V2::ReportBuilder.new(Current.account, current_summary_params).summary - summary[:previous] = V2::ReportBuilder.new(Current.account, previous_summary_params).summary - summary + def build_summary(method) + builder = V2::Reports::Conversations::MetricBuilder + current_summary = builder.new(Current.account, current_summary_params).send(method) + previous_summary = builder.new(Current.account, previous_summary_params).send(method) + current_summary.merge(previous: previous_summary) end def conversation_metrics diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb index 521d027cc..e656c2550 100644 --- a/app/controllers/dashboard_controller.rb +++ b/app/controllers/dashboard_controller.rb @@ -3,6 +3,7 @@ class DashboardController < ActionController::Base before_action :set_application_pack before_action :set_global_config + before_action :set_dashboard_scripts around_action :switch_locale before_action :ensure_installation_onboarding, only: [:index] before_action :render_hc_if_custom_domain, only: [:index] @@ -35,6 +36,10 @@ class DashboardController < ActionController::Base ).merge(app_config) end + def set_dashboard_scripts + @dashboard_scripts = GlobalConfig.get_value('DASHBOARD_SCRIPTS') + end + def ensure_installation_onboarding redirect_to '/installation/onboarding' if ::Redis::Alfred.get(::Redis::Alfred::CHATWOOT_INSTALLATION_ONBOARDING) end diff --git a/app/controllers/microsoft/callbacks_controller.rb b/app/controllers/microsoft/callbacks_controller.rb index 215103bd4..aa1c22d91 100644 --- a/app/controllers/microsoft/callbacks_controller.rb +++ b/app/controllers/microsoft/callbacks_controller.rb @@ -7,9 +7,14 @@ class Microsoft::CallbacksController < ApplicationController redirect_uri: "#{base_url}/microsoft/callback" ) - inbox = find_or_create_inbox + inbox, already_exists = find_or_create_inbox ::Redis::Alfred.delete(users_data['email'].downcase) - redirect_to app_microsoft_inbox_agents_url(account_id: account.id, inbox_id: inbox.id) + + if already_exists + redirect_to app_microsoft_inbox_settings_url(account_id: account.id, inbox_id: inbox.id) + else + redirect_to app_microsoft_inbox_agents_url(account_id: account.id, inbox_id: inbox.id) + end rescue StandardError => e ChatwootExceptionTracker.new(e).capture_exception redirect_to '/' @@ -40,9 +45,17 @@ class Microsoft::CallbacksController < ApplicationController def find_or_create_inbox channel_email = Channel::Email.find_by(email: users_data['email'], account: account) + # we need this value to know where to redirect on sucessful processing of the callback + channel_exists = channel_email.present? + channel_email ||= create_microsoft_channel_with_inbox update_microsoft_channel(channel_email) - channel_email.inbox + + # reauthorize channel, this code path only triggers when microsoft auth is successful + # reauthorized will also update cache keys for the associated inbox + channel_email.reauthorized! + + [channel_email.inbox, channel_exists] end # Fallback name, for when name field is missing from users_data diff --git a/app/helpers/timezone_helper.rb b/app/helpers/timezone_helper.rb new file mode 100644 index 000000000..b016cc9d9 --- /dev/null +++ b/app/helpers/timezone_helper.rb @@ -0,0 +1,19 @@ +module TimezoneHelper + # ActiveSupport TimeZone is not aware of the current time, so ActiveSupport::Timezone[offset] + # would return the timezone without considering day light savings. To get the correct timezone, + # this method uses zone.now.utc_offset for comparison as referenced in the issues below + # + # https://github.com/rails/rails/pull/22243 + # https://github.com/rails/rails/issues/21501 + # https://github.com/rails/rails/issues/7297 + def timezone_name_from_offset(offset) + return 'UTC' if offset.blank? + + offset_in_seconds = offset.to_f * 3600 + matching_zone = ActiveSupport::TimeZone.all.find do |zone| + zone.now.utc_offset == offset_in_seconds + end + + return matching_zone.name if matching_zone + end +end diff --git a/app/javascript/dashboard/App.vue b/app/javascript/dashboard/App.vue index 481c671ce..c343fbe4f 100644 --- a/app/javascript/dashboard/App.vue +++ b/app/javascript/dashboard/App.vue @@ -27,6 +27,7 @@ + - - diff --git a/app/javascript/dashboard/components/SidemenuIcon.vue b/app/javascript/dashboard/components/SidemenuIcon.vue index e2811f65b..6106b9ad0 100644 --- a/app/javascript/dashboard/components/SidemenuIcon.vue +++ b/app/javascript/dashboard/components/SidemenuIcon.vue @@ -21,7 +21,7 @@ export default { }, methods: { onMenuItemClick() { - bus.$emit(BUS_EVENTS.TOGGLE_SIDEMENU); + this.$emitter.emit(BUS_EVENTS.TOGGLE_SIDEMENU); }, }, }; diff --git a/app/javascript/dashboard/components/SnackbarContainer.vue b/app/javascript/dashboard/components/SnackbarContainer.vue index 553ad2a2e..4785e4d4b 100644 --- a/app/javascript/dashboard/components/SnackbarContainer.vue +++ b/app/javascript/dashboard/components/SnackbarContainer.vue @@ -15,11 +15,13 @@