diff --git a/.env.example b/.env.example index 55b998f19..26b1487ae 100644 --- a/.env.example +++ b/.env.example @@ -23,6 +23,9 @@ FORCE_SSL=false ENABLE_ACCOUNT_SIGNUP=false # Redis config +# specify the configs via single URL or individual variables +# ref: https://www.iana.org/assignments/uri-schemes/prov/redis +# You can also use the following format for the URL: redis://:password@host:port/db_number REDIS_URL=redis://redis:6379 # If you are using docker-compose, set this variable's value to be any string, # which will be the password for the redis service running inside the docker-compose diff --git a/Gemfile b/Gemfile index f39655a5c..1b2ed90b9 100644 --- a/Gemfile +++ b/Gemfile @@ -71,13 +71,13 @@ gem 'barnes' ##--- gems for authentication & authorization ---## gem 'devise', '>= 4.9.4' gem 'devise-secure_password', git: 'https://github.com/chatwoot/devise-secure_password', branch: 'chatwoot' -gem 'devise_token_auth' +gem 'devise_token_auth', '>= 1.2.3' # authorization gem 'jwt' gem 'pundit' # super admin gem 'administrate', '>= 0.20.1' -gem 'administrate-field-active_storage', '>= 1.0.1' +gem 'administrate-field-active_storage', '>= 1.0.3' gem 'administrate-field-belongs_to_search', '>= 0.9.0' ##--- gems for pubsub service ---## @@ -166,7 +166,7 @@ gem 'audited', '~> 5.4', '>= 5.4.1' # need for google auth gem 'omniauth', '>= 2.1.2' gem 'omniauth-google-oauth2', '>= 1.1.2' -gem 'omniauth-rails_csrf_protection', '~> 1.0' +gem 'omniauth-rails_csrf_protection', '~> 1.0', '>= 1.0.2' ## Gems for reponse bot # adds cosine similarity to postgres using vector extension diff --git a/Gemfile.lock b/Gemfile.lock index b5ad38993..6b806fc3d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -33,70 +33,70 @@ GIT GEM remote: https://rubygems.org/ specs: - actioncable (7.0.8.1) - actionpack (= 7.0.8.1) - activesupport (= 7.0.8.1) + actioncable (7.0.8.3) + actionpack (= 7.0.8.3) + activesupport (= 7.0.8.3) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailbox (7.0.8.1) - actionpack (= 7.0.8.1) - activejob (= 7.0.8.1) - activerecord (= 7.0.8.1) - activestorage (= 7.0.8.1) - activesupport (= 7.0.8.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) mail (>= 2.7.1) net-imap net-pop net-smtp - actionmailer (7.0.8.1) - actionpack (= 7.0.8.1) - actionview (= 7.0.8.1) - activejob (= 7.0.8.1) - activesupport (= 7.0.8.1) + 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) mail (~> 2.5, >= 2.5.4) net-imap net-pop net-smtp rails-dom-testing (~> 2.0) - actionpack (7.0.8.1) - actionview (= 7.0.8.1) - activesupport (= 7.0.8.1) + actionpack (7.0.8.3) + actionview (= 7.0.8.3) + activesupport (= 7.0.8.3) 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.1) - actionpack (= 7.0.8.1) - activerecord (= 7.0.8.1) - activestorage (= 7.0.8.1) - activesupport (= 7.0.8.1) + 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) globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (7.0.8.1) - activesupport (= 7.0.8.1) + actionview (7.0.8.3) + activesupport (= 7.0.8.3) 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.1) - activesupport (= 7.0.8.1) + activejob (7.0.8.3) + activesupport (= 7.0.8.3) globalid (>= 0.3.6) - activemodel (7.0.8.1) - activesupport (= 7.0.8.1) - activerecord (7.0.8.1) - activemodel (= 7.0.8.1) - activesupport (= 7.0.8.1) + 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) activerecord-import (1.4.1) activerecord (>= 4.2) - activestorage (7.0.8.1) - actionpack (= 7.0.8.1) - activejob (= 7.0.8.1) - activerecord (= 7.0.8.1) - activesupport (= 7.0.8.1) + 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) marcel (~> 1.0) mini_mime (>= 1.1.0) - activesupport (7.0.8.1) + activesupport (7.0.8.3) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) @@ -113,7 +113,7 @@ GEM kaminari (~> 1.2.2) sassc-rails (~> 2.1) selectize-rails (~> 0.6) - administrate-field-active_storage (1.0.1) + administrate-field-active_storage (1.0.3) administrate (>= 0.2.2) rails (>= 7.0) administrate-field-belongs_to_search (0.9.0) @@ -148,7 +148,7 @@ GEM barnes (0.0.9) multi_json (~> 1) statsd-ruby (~> 1.1) - base64 (0.1.1) + base64 (0.2.0) bcrypt (3.1.20) bigdecimal (3.1.7) bindex (0.8.1) @@ -200,10 +200,10 @@ GEM railties (>= 4.1.0) responders warden (~> 1.2.3) - devise_token_auth (1.2.1) + devise_token_auth (1.2.3) bcrypt (~> 3.0) devise (> 3.5.2, < 5) - rails (>= 4.2.0, < 7.1) + rails (>= 4.2.0, < 7.2) diff-lcs (1.5.0) digest-crc (0.6.4) rake (>= 12.0.0, < 14.0.0) @@ -369,7 +369,7 @@ GEM mini_mime (>= 1.0.0) multi_xml (>= 0.5.2) httpclient (2.8.3) - i18n (1.14.4) + i18n (1.14.5) concurrent-ruby (~> 1.0) image_processing (1.12.2) mini_magick (>= 4.9.5, < 5) @@ -448,20 +448,20 @@ GEM net-imap net-pop net-smtp - marcel (1.0.2) + marcel (1.0.4) maxminddb (0.1.22) memoist (0.16.2) meta_request (0.8.2) rack-contrib (>= 1.1, < 3) railties (>= 3.0.0, < 8) - method_source (1.0.0) + method_source (1.1.0) mime-types (3.4.1) mime-types-data (~> 3.2015) mime-types-data (3.2023.0218.1) mini_magick (4.12.0) mini_mime (1.1.5) mini_portile2 (2.8.6) - minitest (5.22.3) + minitest (5.23.0) mock_redis (0.36.0) ruby2_keywords msgpack (1.7.0) @@ -474,7 +474,7 @@ GEM uri net-http-persistent (4.0.2) connection_pool (~> 2.2) - net-imap (0.4.9) + net-imap (0.4.11) date net-protocol net-pop (0.1.2) @@ -489,15 +489,15 @@ GEM sidekiq newrelic_rpm (9.6.0) base64 - nio4r (2.7.0) - nokogiri (1.16.4) + nio4r (2.7.3) + nokogiri (1.16.5) mini_portile2 (~> 2.8.2) racc (~> 1.4) - nokogiri (1.16.4-arm64-darwin) + nokogiri (1.16.5-arm64-darwin) racc (~> 1.4) - nokogiri (1.16.4-x86_64-darwin) + nokogiri (1.16.5-x86_64-darwin) racc (~> 1.4) - nokogiri (1.16.4-x86_64-linux) + nokogiri (1.16.5-x86_64-linux) racc (~> 1.4) oauth (1.1.0) oauth-tty (~> 1.0, >= 1.0.1) @@ -524,7 +524,7 @@ GEM omniauth-oauth2 (1.8.0) oauth2 (>= 1.4, < 3) omniauth (~> 2.0) - omniauth-rails_csrf_protection (1.0.1) + omniauth-rails_csrf_protection (1.0.2) actionpack (>= 4.2) omniauth (~> 2.0) openssl (3.1.0) @@ -569,20 +569,20 @@ GEM rack-test (2.1.0) rack (>= 1.3) rack-timeout (0.6.3) - rails (7.0.8.1) - actioncable (= 7.0.8.1) - actionmailbox (= 7.0.8.1) - actionmailer (= 7.0.8.1) - actionpack (= 7.0.8.1) - actiontext (= 7.0.8.1) - actionview (= 7.0.8.1) - activejob (= 7.0.8.1) - activemodel (= 7.0.8.1) - activerecord (= 7.0.8.1) - activestorage (= 7.0.8.1) - activesupport (= 7.0.8.1) + 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) bundler (>= 1.15.0) - railties (= 7.0.8.1) + railties (= 7.0.8.3) rails-dom-testing (2.2.0) activesupport (>= 5.0.0) minitest @@ -590,9 +590,9 @@ GEM rails-html-sanitizer (1.6.0) loofah (~> 2.21) nokogiri (~> 1.14) - railties (7.0.8.1) - actionpack (= 7.0.8.1) - activesupport (= 7.0.8.1) + railties (7.0.8.3) + actionpack (= 7.0.8.3) + activesupport (= 7.0.8.3) method_source rake (>= 12.2) thor (~> 1.0) @@ -628,7 +628,8 @@ GEM retriable (3.1.2) reverse_markdown (2.1.1) nokogiri - rexml (3.2.5) + rexml (3.2.8) + strscan (>= 3.0.9) rspec-core (3.12.2) rspec-support (~> 3.12.0) rspec-expectations (3.12.3) @@ -758,6 +759,7 @@ GEM stackprof (0.2.25) statsd-ruby (1.5.0) stripe (8.5.0) + strscan (3.1.0) telephone_number (1.4.20) test-prof (1.2.1) thor (1.3.1) @@ -819,7 +821,7 @@ GEM working_hours (1.4.1) activesupport (>= 3.2) tzinfo - zeitwerk (2.6.13) + zeitwerk (2.6.14) PLATFORMS arm64-darwin-20 @@ -837,7 +839,7 @@ DEPENDENCIES activerecord-import acts-as-taggable-on administrate (>= 0.20.1) - administrate-field-active_storage (>= 1.0.1) + administrate-field-active_storage (>= 1.0.3) administrate-field-belongs_to_search (>= 0.9.0) annotate attr_extras @@ -860,7 +862,7 @@ DEPENDENCIES debug (~> 1.8) devise (>= 4.9.4) devise-secure_password! - devise_token_auth + devise_token_auth (>= 1.2.3) dotenv-rails down elastic-apm @@ -904,7 +906,7 @@ DEPENDENCIES omniauth (>= 2.1.2) omniauth-google-oauth2 (>= 1.1.2) omniauth-oauth2 - omniauth-rails_csrf_protection (~> 1.0) + omniauth-rails_csrf_protection (~> 1.0, >= 1.0.2) pg pg_search pgvector diff --git a/app.json b/app.json index 4ee025f06..d50040814 100644 --- a/app.json +++ b/app.json @@ -55,7 +55,7 @@ "plan": "heroku-redis:mini" }, { - "plan": "heroku-postgresql:mini" + "plan": "heroku-postgresql:essential-0" } ], "stack": "heroku-20", diff --git a/app/builders/contact_inbox_with_contact_builder.rb b/app/builders/contact_inbox_with_contact_builder.rb index 6e913eda8..2f30093db 100644 --- a/app/builders/contact_inbox_with_contact_builder.rb +++ b/app/builders/contact_inbox_with_contact_builder.rb @@ -19,9 +19,9 @@ class ContactInboxWithContactBuilder ActiveRecord::Base.transaction(requires_new: true) do build_contact_with_contact_inbox - update_contact_avatar(@contact) unless @contact.avatar.attached? - @contact_inbox end + update_contact_avatar(@contact) unless @contact.avatar.attached? + @contact_inbox end private 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/bulk_actions_controller.rb b/app/controllers/api/v1/accounts/bulk_actions_controller.rb index 832f5a49b..34db47861 100644 --- a/app/controllers/api/v1/accounts/bulk_actions_controller.rb +++ b/app/controllers/api/v1/accounts/bulk_actions_controller.rb @@ -21,6 +21,6 @@ class Api::V1::Accounts::BulkActionsController < Api::V1::Accounts::BaseControll end def permitted_params - params.permit(:type, ids: [], fields: [:status, :assignee_id, :team_id], labels: [add: [], remove: []]) + params.permit(:type, :snoozed_until, ids: [], fields: [:status, :assignee_id, :team_id], labels: [add: [], remove: []]) end end diff --git a/app/controllers/api/v1/accounts/contacts_controller.rb b/app/controllers/api/v1/accounts/contacts_controller.rb index 729db34b5..250c64a86 100644 --- a/app/controllers/api/v1/accounts/contacts_controller.rb +++ b/app/controllers/api/v1/accounts/contacts_controller.rb @@ -46,7 +46,8 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController def export column_names = params['column_names'] - Account::ContactsExportJob.perform_later(Current.account.id, column_names, Current.user.email) + filter_params = { :payload => params.permit!['payload'], :label => params.permit!['label'] } + Account::ContactsExportJob.perform_later(Current.account.id, Current.user.id, column_names, filter_params) head :ok, message: I18n.t('errors.contacts.export.success') end @@ -61,7 +62,7 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController def show; end def filter - result = ::Contacts::FilterService.new(params.permit!, current_user).perform + result = ::Contacts::FilterService.new(Current.account, Current.user, params.permit!).perform contacts = result[:contacts] @contacts_count = result[:count] @contacts = fetch_contacts(contacts) diff --git a/app/controllers/api/v1/accounts/conversations/draft_messages_controller.rb b/app/controllers/api/v1/accounts/conversations/draft_messages_controller.rb new file mode 100644 index 000000000..86b7ce5bb --- /dev/null +++ b/app/controllers/api/v1/accounts/conversations/draft_messages_controller.rb @@ -0,0 +1,28 @@ +class Api::V1::Accounts::Conversations::DraftMessagesController < Api::V1::Accounts::Conversations::BaseController + def show + render json: { has_draft: false } and return unless Redis::Alfred.exists?(draft_redis_key) + + draft_message = Redis::Alfred.get(draft_redis_key) + render json: { has_draft: true, message: draft_message } + end + + def update + Redis::Alfred.set(draft_redis_key, draft_message_params) + head :ok + end + + def destroy + Redis::Alfred.delete(draft_redis_key) + head :ok + end + + private + + def draft_redis_key + format(Redis::Alfred::CONVERSATION_DRAFT_MESSAGE, id: @conversation.id) + end + + def draft_message_params + params.dig(:draft_message, :message) || '' + end +end diff --git a/app/controllers/api/v1/accounts/integrations/linear_controller.rb b/app/controllers/api/v1/accounts/integrations/linear_controller.rb new file mode 100644 index 000000000..9d5d76d75 --- /dev/null +++ b/app/controllers/api/v1/accounts/integrations/linear_controller.rb @@ -0,0 +1,93 @@ +class Api::V1::Accounts::Integrations::LinearController < Api::V1::Accounts::BaseController + before_action :fetch_conversation, only: [:link_issue, :linked_issues] + + def teams + teams = linear_processor_service.teams + if teams[:error] + render json: { error: teams[:error] }, status: :unprocessable_entity + else + render json: teams[:data], status: :ok + end + end + + def team_entities + team_id = permitted_params[:team_id] + team_entities = linear_processor_service.team_entities(team_id) + if team_entities[:error] + render json: { error: team_entities[:error] }, status: :unprocessable_entity + else + render json: team_entities[:data], status: :ok + end + end + + def create_issue + issue = linear_processor_service.create_issue(permitted_params) + if issue[:error] + render json: { error: issue[:error] }, status: :unprocessable_entity + else + render json: issue[:data], status: :ok + end + end + + def link_issue + issue_id = permitted_params[:issue_id] + title = permitted_params[:title] + issue = linear_processor_service.link_issue(conversation_link, issue_id, title) + if issue[:error] + render json: { error: issue[:error] }, status: :unprocessable_entity + else + render json: issue[:data], status: :ok + end + end + + def unlink_issue + link_id = permitted_params[:link_id] + issue = linear_processor_service.unlink_issue(link_id) + + if issue[:error] + render json: { error: issue[:error] }, status: :unprocessable_entity + else + render json: issue[:data], status: :ok + end + end + + def linked_issues + issues = linear_processor_service.linked_issues(conversation_link) + + if issues[:error] + render json: { error: issues[:error] }, status: :unprocessable_entity + else + render json: issues[:data], status: :ok + end + end + + def search_issue + render json: { error: 'Specify search string with parameter q' }, status: :unprocessable_entity if params[:q].blank? && return + + term = params[:q] + issues = linear_processor_service.search_issue(term) + if issues[:error] + render json: { error: issues[:error] }, status: :unprocessable_entity + else + render json: issues[:data], status: :ok + end + end + + private + + def conversation_link + "#{ENV.fetch('FRONTEND_URL', nil)}/app/accounts/#{Current.account.id}/conversations/#{@conversation.display_id}" + end + + def fetch_conversation + @conversation = Current.account.conversations.find_by!(display_id: permitted_params[:conversation_id]) + end + + def linear_processor_service + Integrations::Linear::ProcessorService.new(account: Current.account) + end + + def permitted_params + params.permit(:team_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/concerns/microsoft_concern.rb b/app/controllers/concerns/microsoft_concern.rb index 3aa3e4e81..0f37bd03f 100644 --- a/app/controllers/concerns/microsoft_concern.rb +++ b/app/controllers/concerns/microsoft_concern.rb @@ -2,7 +2,10 @@ module MicrosoftConcern extend ActiveSupport::Concern def microsoft_client - ::OAuth2::Client.new(ENV.fetch('AZURE_APP_ID', nil), ENV.fetch('AZURE_APP_SECRET', nil), + app_id = GlobalConfigService.load('AZURE_APP_ID', nil) + app_secret = GlobalConfigService.load('AZURE_APP_SECRET', nil) + + ::OAuth2::Client.new(app_id, app_secret, { site: 'https://login.microsoftonline.com', authorize_url: 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize', diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb index 047fd10c3..521d027cc 100644 --- a/app/controllers/dashboard_controller.rb +++ b/app/controllers/dashboard_controller.rb @@ -58,7 +58,7 @@ class DashboardController < ActionController::Base FB_APP_ID: GlobalConfigService.load('FB_APP_ID', ''), FACEBOOK_API_VERSION: GlobalConfigService.load('FACEBOOK_API_VERSION', 'v17.0'), IS_ENTERPRISE: ChatwootApp.enterprise?, - AZURE_APP_ID: ENV.fetch('AZURE_APP_ID', ''), + AZURE_APP_ID: GlobalConfigService.load('AZURE_APP_ID', ''), GIT_SHA: GIT_HASH } end diff --git a/app/controllers/microsoft_controller.rb b/app/controllers/microsoft_controller.rb index 07e58d4db..e6a12dafa 100644 --- a/app/controllers/microsoft_controller.rb +++ b/app/controllers/microsoft_controller.rb @@ -12,6 +12,6 @@ class MicrosoftController < ApplicationController end def microsoft_indentity - @identity_json = ENV.fetch('AZURE_APP_ID', nil) + @identity_json = GlobalConfigService.load('AZURE_APP_ID', nil) end end diff --git a/app/controllers/super_admin/app_configs_controller.rb b/app/controllers/super_admin/app_configs_controller.rb index 6223f7174..9d9494fc4 100644 --- a/app/controllers/super_admin/app_configs_controller.rb +++ b/app/controllers/super_admin/app_configs_controller.rb @@ -35,6 +35,8 @@ class SuperAdmin::AppConfigsController < SuperAdmin::ApplicationController @allowed_configs = case @config when 'facebook' %w[FB_APP_ID FB_VERIFY_TOKEN FB_APP_SECRET IG_VERIFY_TOKEN FACEBOOK_API_VERSION ENABLE_MESSENGER_CHANNEL_HUMAN_AGENT] + when 'microsoft' + %w[AZURE_APP_ID AZURE_APP_SECRET] when 'email' ['MAILER_INBOUND_EMAIL_DOMAIN'] else diff --git a/app/dispatchers/async_dispatcher.rb b/app/dispatchers/async_dispatcher.rb index f2f238cd4..b582bc5dd 100644 --- a/app/dispatchers/async_dispatcher.rb +++ b/app/dispatchers/async_dispatcher.rb @@ -10,14 +10,15 @@ class AsyncDispatcher < BaseDispatcher def listeners [ + AutomationRuleListener.instance, CampaignListener.instance, CsatSurveyListener.instance, HookListener.instance, InstallationWebhookListener.instance, NotificationListener.instance, + ParticipationListener.instance, ReportingEventListener.instance, - WebhookListener.instance, - AutomationRuleListener.instance + WebhookListener.instance ] end end diff --git a/app/helpers/api/v2/accounts/reports_helper.rb b/app/helpers/api/v2/accounts/reports_helper.rb index 74c7af43e..4d5a6f115 100644 --- a/app/helpers/api/v2/accounts/reports_helper.rb +++ b/app/helpers/api/v2/accounts/reports_helper.rb @@ -1,7 +1,7 @@ module Api::V2::Accounts::ReportsHelper def generate_agents_report Current.account.users.map do |agent| - agent_report = generate_report({ type: :agent, id: agent.id }) + agent_report = report_builder({ type: :agent, id: agent.id }).summary [agent.name] + generate_readable_report_metrics(agent_report) end end @@ -15,7 +15,7 @@ module Api::V2::Accounts::ReportsHelper def generate_teams_report Current.account.teams.map do |team| - team_report = generate_report({ type: :team, id: team.id }) + team_report = report_builder({ type: :team, id: team.id }).summary [team.name] + generate_readable_report_metrics(team_report) end end @@ -27,7 +27,7 @@ module Api::V2::Accounts::ReportsHelper end end - def generate_report(report_params) + def report_builder(report_params) V2::ReportBuilder.new( Current.account, report_params.merge( @@ -37,7 +37,11 @@ module Api::V2::Accounts::ReportsHelper business_hours: ActiveModel::Type::Boolean.new.cast(params[:business_hours]) } ) - ).short_summary + ) + end + + def generate_report(report_params) + report_builder(report_params).short_summary end private @@ -46,7 +50,9 @@ module Api::V2::Accounts::ReportsHelper [ report_metric[:conversations_count], Reports::TimeFormatPresenter.new(report_metric[:avg_first_response_time]).format, - Reports::TimeFormatPresenter.new(report_metric[:avg_resolution_time]).format + Reports::TimeFormatPresenter.new(report_metric[:avg_resolution_time]).format, + Reports::TimeFormatPresenter.new(report_metric[:reply_time]).format, + report_metric[:resolutions_count] ] end end 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/api/contacts.js b/app/javascript/dashboard/api/contacts.js index 61312dd24..85c4bba1d 100644 --- a/app/javascript/dashboard/api/contacts.js +++ b/app/javascript/dashboard/api/contacts.js @@ -77,8 +77,8 @@ class ContactAPI extends ApiClient { return axios.delete(`${this.url}/${contactId}/avatar`); } - exportContacts() { - return axios.get(`${this.url}/export`); + exportContacts(queryPayload) { + return axios.post(`${this.url}/export`, queryPayload); } } diff --git a/app/javascript/dashboard/assets/scss/widgets/_base.scss b/app/javascript/dashboard/assets/scss/widgets/_base.scss index 1ebcef53c..d5d2e227f 100644 --- a/app/javascript/dashboard/assets/scss/widgets/_base.scss +++ b/app/javascript/dashboard/assets/scss/widgets/_base.scss @@ -12,7 +12,6 @@ h6 { p { text-rendering: optimizeLegibility; - word-spacing: 0.12em; @apply mb-2 leading-[1.65] text-sm; diff --git a/app/javascript/dashboard/components/ChannelSelector.vue b/app/javascript/dashboard/components/ChannelSelector.vue index 33877de94..dd9513fb6 100644 --- a/app/javascript/dashboard/components/ChannelSelector.vue +++ b/app/javascript/dashboard/components/ChannelSelector.vue @@ -1,9 +1,9 @@ @@ -180,8 +127,8 @@ import { mapGetters } from 'vuex'; import VirtualList from 'vue-virtual-scroll-list'; +import ChatListHeader from './ChatListHeader.vue'; import ConversationAdvancedFilter from './widgets/conversation/ConversationAdvancedFilter.vue'; -import ConversationBasicFilter from './widgets/conversation/ConversationBasicFilter.vue'; import ChatTypeTabs from './widgets/ChatTypeTabs.vue'; import ConversationItem from './ConversationItem.vue'; import timeMixin from '../mixins/time'; @@ -205,10 +152,15 @@ import { isOnUnattendedView, } from '../store/modules/conversations/helpers/actionHelpers'; import { CONVERSATION_EVENTS } from '../helper/AnalyticsHelper/events'; +import { CMD_SNOOZE_CONVERSATION } from 'dashboard/routes/dashboard/commands/commandBarBusEvents'; +import { findSnoozeTime } from 'dashboard/helper/snoozeHelpers'; +import { getUnixTime } from 'date-fns'; +import CustomSnoozeModal from 'dashboard/components/CustomSnoozeModal.vue'; import IntersectionObserver from './IntersectionObserver.vue'; export default { components: { + ChatListHeader, AddCustomViews, ChatTypeTabs, // eslint-disable-next-line vue/no-unused-components @@ -216,9 +168,9 @@ export default { ConversationAdvancedFilter, DeleteCustomViews, ConversationBulkActions, - ConversationBasicFilter, IntersectionObserver, VirtualList, + CustomSnoozeModal, }, mixins: [ timeMixin, @@ -288,7 +240,6 @@ export default { foldersQuery: {}, showAddFoldersModal: false, showDeleteFoldersModal: false, - selectedConversations: [], selectedInboxes: [], isContextMenuOpen: false, appliedFilter: [], @@ -296,6 +247,7 @@ export default { root: this.$refs.conversationList, rootMargin: '100px 0px 100px 0px', }, + showCustomSnoozeModal: false, itemComponent: ConversationItem, // virtualListExtraProps is to pass the props to the conversationItem component. @@ -329,12 +281,14 @@ export default { inboxesList: 'inboxes/getInboxes', campaigns: 'campaigns/getAllCampaigns', labels: 'labels/getLabels', + selectedConversations: 'bulkActions/getSelectedConversationIds', + contextMenuChatId: 'getContextMenuChatId', }), hasAppliedFilters() { return this.appliedFilters.length !== 0; }, hasActiveFolders() { - return this.activeFolder && this.foldersId !== 0; + return Boolean(this.activeFolder && this.foldersId !== 0); }, hasAppliedFiltersOrActiveFolders() { return this.hasAppliedFilters || this.hasActiveFolders; @@ -558,6 +512,11 @@ export default { bus.$on('fetch_conversation_stats', () => { this.$store.dispatch('conversationStats/get', this.conversationFilters); }); + + bus.$on(CMD_SNOOZE_CONVERSATION, this.onCmdSnoozeConversation); + }, + beforeDestroy() { + bus.$off(CMD_SNOOZE_CONVERSATION, this.onCmdSnoozeConversation); }, methods: { updateVirtualListProps(key, value) { @@ -799,7 +758,7 @@ export default { }); }, resetBulkActions() { - this.selectedConversations = []; + this.$store.dispatch('bulkActions/clearSelectedConversationIds'); this.selectedInboxes = []; }, onBasicFilterChange(value, type) { @@ -830,12 +789,16 @@ export default { return this.selectedConversations.includes(id); }, selectConversation(conversationId, inboxId) { - this.selectedConversations.push(conversationId); + this.$store.dispatch( + 'bulkActions/setSelectedConversationIds', + conversationId + ); this.selectedInboxes.push(inboxId); }, deSelectConversation(conversationId, inboxId) { - this.selectedConversations = this.selectedConversations.filter( - item => item !== conversationId + this.$store.dispatch( + 'bulkActions/removeSelectedConversationIds', + conversationId ); this.selectedInboxes = this.selectedInboxes.filter( item => item !== inboxId @@ -843,7 +806,10 @@ export default { }, selectAllConversations(check) { if (check) { - this.selectedConversations = this.conversationList.map(item => item.id); + this.$store.dispatch( + 'bulkActions/setSelectedConversationIds', + this.conversationList.map(item => item.id) + ); this.selectedInboxes = this.conversationList.map(item => item.inbox_id); } else { this.resetBulkActions(); @@ -859,7 +825,7 @@ export default { assignee_id: agent.id, }, }); - this.selectedConversations = []; + this.$store.dispatch('bulkActions/clearSelectedConversationIds'); if (conversationId) { this.showAlert( this.$t( @@ -957,7 +923,7 @@ export default { add: labels, }, }); - this.selectedConversations = []; + this.$store.dispatch('bulkActions/clearSelectedConversationIds'); if (conversationId) { this.showAlert( this.$t( @@ -984,13 +950,13 @@ export default { team_id: team.id, }, }); - this.selectedConversations = []; + this.$store.dispatch('bulkActions/clearSelectedConversationIds'); this.showAlert(this.$t('BULK_ACTION.TEAMS.ASSIGN_SUCCESFUL')); } catch (err) { this.showAlert(this.$t('BULK_ACTION.TEAMS.ASSIGN_FAILED')); } }, - async onUpdateConversations(status) { + async onUpdateConversations(status, snoozedUntil) { try { await this.$store.dispatch('bulkActions/process', { type: 'Conversation', @@ -998,8 +964,9 @@ export default { fields: { status, }, + snoozed_until: snoozedUntil, }); - this.selectedConversations = []; + this.$store.dispatch('bulkActions/clearSelectedConversationIds'); this.showAlert(this.$t('BULK_ACTION.UPDATE.UPDATE_SUCCESFUL')); } catch (err) { this.showAlert(this.$t('BULK_ACTION.UPDATE.UPDATE_FAILED')); @@ -1020,12 +987,49 @@ export default { allSelectedConversationsStatus(status) { if (!this.selectedConversations.length) return false; return this.selectedConversations.every(item => { - return this.$store.getters.getConversationById(item).status === status; + return this.$store.getters.getConversationById(item)?.status === status; }); }, onContextMenuToggle(state) { this.isContextMenuOpen = state; }, + onCmdSnoozeConversation(snoozeType) { + if (snoozeType === wootConstants.SNOOZE_OPTIONS.UNTIL_CUSTOM_TIME) { + this.showCustomSnoozeModal = true; + } else { + this.toggleStatus( + wootConstants.STATUS_TYPE.SNOOZED, + findSnoozeTime(snoozeType) || null + ); + } + }, + chooseSnoozeTime(customSnoozeTime) { + this.showCustomSnoozeModal = false; + if (customSnoozeTime) { + this.toggleStatus( + wootConstants.STATUS_TYPE.SNOOZED, + getUnixTime(customSnoozeTime) + ); + } + }, + toggleStatus(status, snoozedUntil) { + this.$store + .dispatch('toggleStatus', { + conversationId: this.currentChat?.id || this.contextMenuChatId, + status, + snoozedUntil, + }) + .then(() => { + this.$store.dispatch('setContextMenuChatId', null); + this.showAlert(this.$t('CONVERSATION.CHANGE_STATUS')); + }); + }, + hideCustomSnoozeModal() { + // if we select custom snooze and then the custom snooze modal is open + // Then if the custom snooze modal is closed and set the context menu chat id to null + this.$store.dispatch('setContextMenuChatId', null); + this.showCustomSnoozeModal = false; + }, }, }; diff --git a/app/javascript/dashboard/components/ChatListHeader.vue b/app/javascript/dashboard/components/ChatListHeader.vue new file mode 100644 index 000000000..d2df2bea3 --- /dev/null +++ b/app/javascript/dashboard/components/ChatListHeader.vue @@ -0,0 +1,115 @@ + + + diff --git a/app/javascript/dashboard/components/buttons/ResolveAction.vue b/app/javascript/dashboard/components/buttons/ResolveAction.vue index 61375dc21..c56f27e14 100644 --- a/app/javascript/dashboard/components/buttons/ResolveAction.vue +++ b/app/javascript/dashboard/components/buttons/ResolveAction.vue @@ -73,25 +73,13 @@ - - - diff --git a/app/javascript/dashboard/components/widgets/conversation/components/SLACardLabel.vue b/app/javascript/dashboard/components/widgets/conversation/components/SLACardLabel.vue index 57ddd451e..042e25838 100644 --- a/app/javascript/dashboard/components/widgets/conversation/components/SLACardLabel.vue +++ b/app/javascript/dashboard/components/widgets/conversation/components/SLACardLabel.vue @@ -49,7 +49,7 @@ + diff --git a/app/javascript/dashboard/components/widgets/forms/PhoneInput.vue b/app/javascript/dashboard/components/widgets/forms/PhoneInput.vue index 9b77faafe..6b09acbac 100644 --- a/app/javascript/dashboard/components/widgets/forms/PhoneInput.vue +++ b/app/javascript/dashboard/components/widgets/forms/PhoneInput.vue @@ -1,9 +1,16 @@ diff --git a/app/javascript/dashboard/routes/dashboard/commands/conversationHotKeys.js b/app/javascript/dashboard/routes/dashboard/commands/conversationHotKeys.js index 5d0b1960d..aaa73c5d4 100644 --- a/app/javascript/dashboard/routes/dashboard/commands/conversationHotKeys.js +++ b/app/javascript/dashboard/routes/dashboard/commands/conversationHotKeys.js @@ -55,11 +55,15 @@ export default { replyMode() { this.setCommandbarData(); }, + contextMenuChatId() { + this.setCommandbarData(); + }, }, computed: { ...mapGetters({ currentChat: 'getSelectedChat', replyMode: 'draftMessages/getReplyEditorMode', + contextMenuChatId: 'getContextMenuChatId', }), draftMessage() { return this.$store.getters['draftMessages/get'](this.draftKey); @@ -93,6 +97,7 @@ export default { } return this.prepareActions(actions); }, + priorityOptions() { return [ { @@ -327,25 +332,42 @@ export default { ]; }, - conversationHotKeys() { - if ( + isConversationOrInboxRoute() { + return ( isAConversationRoute(this.$route.name) || isAInboxViewRoute(this.$route.name) - ) { - const defaultConversationHotKeys = [ - ...this.statusActions, - ...this.conversationAdditionalActions, - ...this.assignAgentActions, - ...this.assignTeamActions, - ...this.labelActions, - ...this.assignPriorityActions, - ]; - if (this.isAIIntegrationEnabled) { - return [...defaultConversationHotKeys, ...this.AIAssistActions]; - } - return defaultConversationHotKeys; - } + ); + }, + shouldShowSnoozeOption() { + return ( + isAConversationRoute(this.$route.name, true, false) && + this.contextMenuChatId + ); + }, + + getDefaultConversationHotKeys() { + const defaultConversationHotKeys = [ + ...this.statusActions, + ...this.conversationAdditionalActions, + ...this.assignAgentActions, + ...this.assignTeamActions, + ...this.labelActions, + ...this.assignPriorityActions, + ]; + if (this.isAIIntegrationEnabled) { + return [...defaultConversationHotKeys, ...this.AIAssistActions]; + } + return defaultConversationHotKeys; + }, + + conversationHotKeys() { + if (this.shouldShowSnoozeOption) { + return this.prepareActions(SNOOZE_CONVERSATION_ACTIONS); + } + if (this.isConversationOrInboxRoute) { + return this.getDefaultConversationHotKeys; + } return []; }, }, diff --git a/app/javascript/dashboard/routes/dashboard/contacts/components/ContactsView.vue b/app/javascript/dashboard/routes/dashboard/contacts/components/ContactsView.vue index 41e0dff56..1a824f587 100644 --- a/app/javascript/dashboard/routes/dashboard/contacts/components/ContactsView.vue +++ b/app/javascript/dashboard/routes/dashboard/contacts/components/ContactsView.vue @@ -1,5 +1,5 @@ @@ -139,7 +150,6 @@ export default { components: { TableHeaderCell, FormSwitch, - CheckBox, }, mixins: [alertMixin, configMixin, uiSettingsMixin], diff --git a/app/javascript/dashboard/routes/dashboard/settings/profile/NotificationSettings.vue b/app/javascript/dashboard/routes/dashboard/settings/profile/NotificationSettings.vue deleted file mode 100644 index cdbc40ec4..000000000 --- a/app/javascript/dashboard/routes/dashboard/settings/profile/NotificationSettings.vue +++ /dev/null @@ -1,649 +0,0 @@ - - - - - diff --git a/app/javascript/dashboard/routes/dashboard/settings/personal/UserBasicDetails.vue b/app/javascript/dashboard/routes/dashboard/settings/profile/UserBasicDetails.vue similarity index 100% rename from app/javascript/dashboard/routes/dashboard/settings/personal/UserBasicDetails.vue rename to app/javascript/dashboard/routes/dashboard/settings/profile/UserBasicDetails.vue diff --git a/app/javascript/dashboard/routes/dashboard/settings/personal/UserProfilePicture.vue b/app/javascript/dashboard/routes/dashboard/settings/profile/UserProfilePicture.vue similarity index 100% rename from app/javascript/dashboard/routes/dashboard/settings/personal/UserProfilePicture.vue rename to app/javascript/dashboard/routes/dashboard/settings/profile/UserProfilePicture.vue diff --git a/app/javascript/dashboard/routes/dashboard/settings/profile/Wrapper.vue b/app/javascript/dashboard/routes/dashboard/settings/profile/Wrapper.vue new file mode 100644 index 000000000..f0931afdc --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/profile/Wrapper.vue @@ -0,0 +1,19 @@ + + + diff --git a/app/javascript/dashboard/routes/dashboard/settings/personal/constants.js b/app/javascript/dashboard/routes/dashboard/settings/profile/constants.js similarity index 100% rename from app/javascript/dashboard/routes/dashboard/settings/personal/constants.js rename to app/javascript/dashboard/routes/dashboard/settings/profile/constants.js diff --git a/app/javascript/dashboard/routes/dashboard/settings/profile/profile.routes.js b/app/javascript/dashboard/routes/dashboard/settings/profile/profile.routes.js index 634d15491..357c20e85 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/profile/profile.routes.js +++ b/app/javascript/dashboard/routes/dashboard/settings/profile/profile.routes.js @@ -1,6 +1,6 @@ import { frontendURL } from '../../../../helper/URLHelper'; -const SettingsContent = () => import('../Wrapper.vue'); +const SettingsContent = () => import('./Wrapper.vue'); const Index = () => import('./Index.vue'); export default { @@ -10,12 +10,6 @@ export default { name: 'profile_settings', roles: ['administrator', 'agent'], component: SettingsContent, - props: { - headerTitle: 'PROFILE_SETTINGS.TITLE', - icon: 'edit', - showNewButton: false, - showSidemenuIcon: false, - }, children: [ { path: 'settings', diff --git a/app/javascript/dashboard/routes/dashboard/settings/reports/components/Filters/v3/ActiveFilterChip.vue b/app/javascript/dashboard/routes/dashboard/settings/reports/components/Filters/v3/ActiveFilterChip.vue index 67046acc4..623c15026 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/reports/components/Filters/v3/ActiveFilterChip.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/reports/components/Filters/v3/ActiveFilterChip.vue @@ -1,6 +1,6 @@ diff --git a/app/javascript/v3/components/Form/CheckBox.vue b/app/javascript/v3/components/Form/CheckBox.vue index a047be63a..c70a122e6 100644 --- a/app/javascript/v3/components/Form/CheckBox.vue +++ b/app/javascript/v3/components/Form/CheckBox.vue @@ -4,12 +4,12 @@ v-model="checked" type="checkbox" :value="value" - class="flex-shrink-0 mt-0.5 border-ash-200 border checked:border-none checked:bg-primary-600 dark:checked:bg-primary-600 shadow appearance-none rounded-[4px] w-4 h-4 focus:ring-1 after:content-[''] after:text-white checked:after:content-['✓'] after:flex after:items-center after:justify-center after:text-center after:text-xs after:font-bold after:relative" + class="flex-shrink-0 mt-0.5 border-ash-200 border bg-ash-50 checked:border-none checked:bg-primary-600 dark:checked:bg-primary-600 shadow-sm appearance-none rounded-[4px] w-4 h-4 focus:ring-1 after:content-[''] after:text-white checked:after:content-['✓'] after:flex after:items-center after:justify-center after:text-center after:text-xs after:font-bold after:relative" /> <% if @global_config['ANALYTICS_TOKEN'].present? %>