diff --git a/.env.example b/.env.example index 177cb7fc8..55b998f19 100644 --- a/.env.example +++ b/.env.example @@ -180,8 +180,6 @@ ANDROID_SHA256_CERT_FINGERPRINT=AC:73:8E:DE:EB:56:EA:CC:10:87:02:A7:65:37:7B:38: ## Sentry # SENTRY_DSN= -## LogRocket -# LOG_ROCKET_PROJECT_ID=xxxxx/some-project # MICROSOFT CLARITY # MS_CLARITY_TOKEN=xxxxxxxxx @@ -251,9 +249,6 @@ AZURE_APP_SECRET= ## OpenAI key # OPENAI_API_KEY= -# Sentiment analysis model file path -SENTIMENT_FILE_PATH= - # Housekeeping/Performance related configurations # Set to true if you want to remove stale contact inboxes # contact_inboxes with no conversation older than 90 days will be removed diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 1015fe997..499e5c120 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,7 +1,2 @@ -## All javascript files should be reviewed by pranav before merging -*.js @pranavrajs -*.vue @pranavrajs - - ## All enterprise related files should be reviewed by sojan before merging /enterprise/* @sojan-official diff --git a/Gemfile b/Gemfile index 59bfc5d7f..f39655a5c 100644 --- a/Gemfile +++ b/Gemfile @@ -69,7 +69,7 @@ gem 'webpacker' gem 'barnes' ##--- gems for authentication & authorization ---## -gem 'devise', '>= 4.9.3' +gem 'devise', '>= 4.9.4' gem 'devise-secure_password', git: 'https://github.com/chatwoot/devise-secure_password', branch: 'chatwoot' gem 'devise_token_auth' # authorization @@ -113,10 +113,10 @@ gem 'newrelic-sidekiq-metrics', '>= 1.6.2', require: false gem 'scout_apm', require: false gem 'sentry-rails', '>= 5.14.0', require: false gem 'sentry-ruby', require: false -gem 'sentry-sidekiq', '>= 5.14.0', require: false +gem 'sentry-sidekiq', '>= 5.15.0', require: false ##-- background job processing --## -gem 'sidekiq', '>= 7.2.1' +gem 'sidekiq', '>= 7.2.4' # We want cron jobs gem 'sidekiq-cron', '>= 1.12.0' @@ -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 @@ -175,9 +175,6 @@ gem 'pgvector' # Convert Website HTML to Markdown gem 'reverse_markdown' -# Sentiment analysis -gem 'informers' - ### Gems required only in specific deployment environments ### ############################################################## @@ -203,7 +200,7 @@ group :development do gem 'rack-mini-profiler', '>= 3.2.0', require: false gem 'stackprof' # Should install the associated chrome extension to view query logs - gem 'meta_request' + gem 'meta_request', '>= 0.8.0' end group :test do diff --git a/Gemfile.lock b/Gemfile.lock index 41cb0f20b..b5ad38993 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -149,9 +149,9 @@ GEM multi_json (~> 1) statsd-ruby (~> 1.1) base64 (0.1.1) - bcrypt (3.1.19) + bcrypt (3.1.20) + bigdecimal (3.1.7) bindex (0.8.1) - blingfire (0.1.8) bootsnap (1.16.0) msgpack (~> 1.2) brakeman (5.4.1) @@ -194,7 +194,7 @@ GEM irb (>= 1.5.0) reline (>= 0.3.1) declarative (0.0.20) - devise (4.9.3) + devise (4.9.4) bcrypt (~> 3.0) orm_adapter (~> 0.1) railties (>= 4.1.0) @@ -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) @@ -316,15 +316,15 @@ GEM google-cloud-translate-v3 (0.6.0) gapic-common (>= 0.17.1, < 2.a) google-cloud-errors (~> 1.0) - google-protobuf (3.25.2) - google-protobuf (3.25.2-arm64-darwin) - google-protobuf (3.25.2-x86_64-darwin) - google-protobuf (3.25.2-x86_64-linux) + google-protobuf (3.25.3) + google-protobuf (3.25.3-arm64-darwin) + google-protobuf (3.25.3-x86_64-darwin) + google-protobuf (3.25.3-x86_64-linux) googleapis-common-protos (1.4.0) google-protobuf (~> 3.14) googleapis-common-protos-types (~> 1.2) grpc (~> 1.27) - googleapis-common-protos-types (1.11.0) + googleapis-common-protos-types (1.14.0) google-protobuf (~> 3.18) googleauth (1.5.2) faraday (>= 0.17.3, < 3.a) @@ -335,14 +335,17 @@ GEM signet (>= 0.16, < 2.a) groupdate (6.2.1) activesupport (>= 5.2) - grpc (1.54.3) - google-protobuf (~> 3.21) + grpc (1.62.0) + google-protobuf (~> 3.25) googleapis-common-protos-types (~> 1.0) - grpc (1.54.3-x86_64-darwin) - google-protobuf (~> 3.21) + grpc (1.62.0-arm64-darwin) + google-protobuf (~> 3.25) googleapis-common-protos-types (~> 1.0) - grpc (1.54.3-x86_64-linux) - google-protobuf (~> 3.21) + grpc (1.62.0-x86_64-darwin) + google-protobuf (~> 3.25) + googleapis-common-protos-types (~> 1.0) + grpc (1.62.0-x86_64-linux) + google-protobuf (~> 3.25) googleapis-common-protos-types (~> 1.0) haikunator (1.1.1) hairtrigger (1.0.0) @@ -366,15 +369,11 @@ GEM mini_mime (>= 1.0.0) multi_xml (>= 0.5.2) httpclient (2.8.3) - i18n (1.14.1) + i18n (1.14.4) concurrent-ruby (~> 1.0) image_processing (1.12.2) mini_magick (>= 4.9.5, < 5) ruby-vips (>= 2.0.17, < 3) - informers (0.2.0) - blingfire (>= 0.1.7) - numo-narray - onnxruntime (>= 0.5.1) io-console (0.6.0) irb (1.7.2) reline (>= 0.3.6) @@ -394,7 +393,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) @@ -451,17 +451,17 @@ GEM marcel (1.0.2) maxminddb (0.1.22) memoist (0.16.2) - meta_request (0.7.4) + meta_request (0.8.2) rack-contrib (>= 1.1, < 3) - railties (>= 3.0.0, < 7.1) + railties (>= 3.0.0, < 8) method_source (1.0.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.5) - minitest (5.21.2) + mini_portile2 (2.8.6) + minitest (5.22.3) mock_redis (0.36.0) ruby2_keywords msgpack (1.7.0) @@ -470,6 +470,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) @@ -488,16 +490,15 @@ GEM newrelic_rpm (9.6.0) base64 nio4r (2.7.0) - nokogiri (1.16.2) + nokogiri (1.16.4) mini_portile2 (~> 2.8.2) racc (~> 1.4) - nokogiri (1.16.2-arm64-darwin) + nokogiri (1.16.4-arm64-darwin) racc (~> 1.4) - nokogiri (1.16.2-x86_64-darwin) + nokogiri (1.16.4-x86_64-darwin) racc (~> 1.4) - nokogiri (1.16.2-x86_64-linux) + nokogiri (1.16.4-x86_64-linux) racc (~> 1.4) - numo-narray (0.9.2.1) oauth (1.1.0) oauth-tty (~> 1.0, >= 1.0.1) snaky_hash (~> 2.0) @@ -515,25 +516,17 @@ 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) omniauth-rails_csrf_protection (1.0.1) actionpack (>= 4.2) omniauth (~> 2.0) - onnxruntime (0.7.6) - ffi - onnxruntime (0.7.6-arm64-darwin) - ffi - onnxruntime (0.7.6-x86_64-darwin) - ffi - onnxruntime (0.7.6-x86_64-linux) - ffi openssl (3.1.0) orm_adapter (0.5.0) os (1.1.4) @@ -559,7 +552,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 +561,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 @@ -604,13 +598,13 @@ GEM thor (~> 1.0) zeitwerk (~> 2.5) rainbow (3.1.1) - rake (13.1.0) + rake (13.2.1) rb-fsevent (0.11.2) rb-inotify (0.10.1) ffi (~> 1.0) redis (5.0.6) redis-client (>= 0.9.0) - redis-client (0.19.1) + redis-client (0.22.1) connection_pool redis-namespace (1.10.0) redis (>= 4) @@ -709,18 +703,19 @@ GEM activesupport (>= 4) selectize-rails (0.12.6) semantic_range (3.0.0) - sentry-rails (5.14.0) + sentry-rails (5.17.3) railties (>= 5.0) - sentry-ruby (~> 5.14.0) - sentry-ruby (5.14.0) + sentry-ruby (~> 5.17.3) + sentry-ruby (5.17.3) + bigdecimal concurrent-ruby (~> 1.0, >= 1.0.2) - sentry-sidekiq (5.14.0) - sentry-ruby (~> 5.14.0) + sentry-sidekiq (5.17.3) + sentry-ruby (~> 5.17.3) sidekiq (>= 3.0) sexp_processor (4.17.0) shoulda-matchers (5.3.0) activesupport (>= 5.2.0) - sidekiq (7.2.1) + sidekiq (7.2.4) concurrent-ruby (< 2) connection_pool (>= 2.3.0) rack (>= 2.2.4) @@ -765,7 +760,7 @@ GEM stripe (8.5.0) telephone_number (1.4.20) test-prof (1.2.1) - thor (1.3.0) + thor (1.3.1) tilt (2.3.0) time_diff (0.3.0) activesupport @@ -790,11 +785,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) @@ -823,7 +819,7 @@ GEM working_hours (1.4.1) activesupport (>= 3.2) tzinfo - zeitwerk (2.6.12) + zeitwerk (2.6.13) PLATFORMS arm64-darwin-20 @@ -862,7 +858,7 @@ DEPENDENCIES database_cleaner ddtrace debug (~> 1.8) - devise (>= 4.9.3) + devise (>= 4.9.4) devise-secure_password! devise_token_auth dotenv-rails @@ -887,7 +883,6 @@ DEPENDENCIES hashie html2text! image_processing - informers jbuilder json_refs json_schemer @@ -900,14 +895,14 @@ DEPENDENCIES listen lograge (~> 0.14.0) maxminddb - meta_request + meta_request (>= 0.8.0) mock_redis neighbor net-smtp (~> 0.3.4) 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 @@ -938,9 +933,9 @@ DEPENDENCIES seed_dump sentry-rails (>= 5.14.0) sentry-ruby - sentry-sidekiq (>= 5.14.0) + sentry-sidekiq (>= 5.15.0) shoulda-matchers - sidekiq (>= 7.2.1) + sidekiq (>= 7.2.4) sidekiq-cron (>= 1.12.0) simplecov (= 0.17.1) slack-ruby-client (~> 2.2.0) diff --git a/Makefile b/Makefile index 16eb80718..7f2680a2d 100644 --- a/Makefile +++ b/Makefile @@ -17,6 +17,9 @@ db_migrate: db_seed: RAILS_ENV=$(RAILS_ENV) bundle exec rails db:seed +db_reset: + RAILS_ENV=$(RAILS_ENV) bundle exec rails db:reset + db: RAILS_ENV=$(RAILS_ENV) bundle exec rails db:chatwoot_prepare @@ -49,4 +52,4 @@ debug_worker: docker: docker build -t $(APP_NAME) -f ./docker/Dockerfile . -.PHONY: setup db_create db_migrate db_seed db console server burn docker run force_run debug debug_worker +.PHONY: setup db_create db_migrate db_seed db_reset db console server burn docker run force_run debug debug_worker diff --git a/app/builders/agent_builder.rb b/app/builders/agent_builder.rb index 07b0e7345..54f478920 100644 --- a/app/builders/agent_builder.rb +++ b/app/builders/agent_builder.rb @@ -16,7 +16,6 @@ class AgentBuilder def perform ActiveRecord::Base.transaction do @user = find_or_create_user - send_confirmation_if_required create_account_user end @user @@ -34,11 +33,6 @@ class AgentBuilder User.create!(email: email, name: name, password: temp_password, password_confirmation: temp_password) end - # Sends confirmation instructions if the user is persisted and not confirmed. - def send_confirmation_if_required - @user.send_confirmation_instructions if user_needs_confirmation? - end - # Checks if the user needs confirmation. # @return [Boolean] true if the user is persisted and not confirmed, false otherwise. def user_needs_confirmation? 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/api/v1/accounts/contacts_controller.rb b/app/controllers/api/v1/accounts/contacts_controller.rb index 71e9100e7..729db34b5 100644 --- a/app/controllers/api/v1/accounts/contacts_controller.rb +++ b/app/controllers/api/v1/accounts/contacts_controller.rb @@ -65,6 +65,10 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController contacts = result[:contacts] @contacts_count = result[:count] @contacts = fetch_contacts(contacts) + rescue CustomExceptions::CustomFilter::InvalidAttribute, + CustomExceptions::CustomFilter::InvalidOperator, + CustomExceptions::CustomFilter::InvalidValue => e + render_could_not_create_error(e.message) end def contactable_inboxes diff --git a/app/controllers/api/v1/accounts/conversations_controller.rb b/app/controllers/api/v1/accounts/conversations_controller.rb index 281ff95de..2aedf1928 100644 --- a/app/controllers/api/v1/accounts/conversations_controller.rb +++ b/app/controllers/api/v1/accounts/conversations_controller.rb @@ -36,10 +36,18 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro end end + def update + @conversation.update!(permitted_update_params) + end + def filter result = ::Conversations::FilterService.new(params.permit!, current_user).perform @conversations = result[:conversations] @conversations_count = result[:count] + rescue CustomExceptions::CustomFilter::InvalidAttribute, + CustomExceptions::CustomFilter::InvalidOperator, + CustomExceptions::CustomFilter::InvalidValue => e + render_could_not_create_error(e.message) end def mute @@ -110,6 +118,11 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro private + def permitted_update_params + # TODO: Move the other conversation attributes to this method and remove specific endpoints for each attribute + params.permit(:priority) + end + def update_last_seen_on_conversation(last_seen_at, update_assignee) # rubocop:disable Rails/SkipsModelValidations @conversation.update_column(:agent_last_seen_at, last_seen_at) @@ -176,3 +189,5 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro @conversation.assignee_id? && Current.user == @conversation.assignee end end + +Api::V1::Accounts::ConversationsController.prepend_mod_with('Api::V1::Accounts::ConversationsController') diff --git a/app/controllers/api/v1/widget/conversations_controller.rb b/app/controllers/api/v1/widget/conversations_controller.rb index 1d1ba8a7e..7e6d84bd2 100644 --- a/app/controllers/api/v1/widget/conversations_controller.rb +++ b/app/controllers/api/v1/widget/conversations_controller.rb @@ -33,10 +33,10 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController end def transcript - if permitted_params[:email].present? && conversation.present? + if conversation.present? && conversation.contact.present? && conversation.contact.email.present? ConversationReplyMailer.with(account: conversation.account).conversation_transcript( conversation, - permitted_params[:email] + conversation.contact.email )&.deliver_later end head :ok 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/controllers/concerns/access_token_auth_helper.rb b/app/controllers/concerns/access_token_auth_helper.rb index cd760f7ce..2ee9f9854 100644 --- a/app/controllers/concerns/access_token_auth_helper.rb +++ b/app/controllers/concerns/access_token_auth_helper.rb @@ -1,6 +1,6 @@ module AccessTokenAuthHelper BOT_ACCESSIBLE_ENDPOINTS = { - 'api/v1/accounts/conversations' => %w[toggle_status toggle_priority create], + 'api/v1/accounts/conversations' => %w[toggle_status toggle_priority create update], 'api/v1/accounts/conversations/messages' => ['create'], 'api/v1/accounts/conversations/assignments' => ['create'] }.freeze diff --git a/app/controllers/concerns/domain_helper.rb b/app/controllers/concerns/domain_helper.rb new file mode 100644 index 000000000..1b7d8f187 --- /dev/null +++ b/app/controllers/concerns/domain_helper.rb @@ -0,0 +1,5 @@ +module DomainHelper + def self.chatwoot_domain?(domain = request.host) + [URI.parse(ENV.fetch('FRONTEND_URL', '')).host, URI.parse(ENV.fetch('HELPCENTER_URL', '')).host].include?(domain) + end +end diff --git a/app/controllers/concerns/switch_locale.rb b/app/controllers/concerns/switch_locale.rb index 744a70da9..3013ff3cc 100644 --- a/app/controllers/concerns/switch_locale.rb +++ b/app/controllers/concerns/switch_locale.rb @@ -6,6 +6,7 @@ module SwitchLocale def switch_locale(&) # priority is for locale set in query string (mostly for widget/from js sdk) locale ||= locale_from_params + locale ||= locale_from_custom_domain # if locale is not set in account, let's use DEFAULT_LOCALE env variable locale ||= locale_from_env_variable set_locale(locale, &) @@ -16,6 +17,20 @@ module SwitchLocale set_locale(locale, &) end + # If the request is coming from a custom domain, it should be for a helpcenter portal + # We will use the portal locale in such cases + def locale_from_custom_domain(&) + return if params[:locale] + + domain = request.host + return if DomainHelper.chatwoot_domain?(domain) + + @portal = Portal.find_by(custom_domain: domain) + return unless @portal + + @portal.default_locale + end + def set_locale(locale, &) # if locale is empty, use default_locale locale ||= I18n.default_locale diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb index 0aea9df83..047fd10c3 100644 --- a/app/controllers/dashboard_controller.rb +++ b/app/controllers/dashboard_controller.rb @@ -18,6 +18,7 @@ class DashboardController < ActionController::Base 'LOGO', 'LOGO_DARK', 'LOGO_THUMBNAIL', 'INSTALLATION_NAME', 'WIDGET_BRAND_URL', 'TERMS_URL', + 'BRAND_URL', 'BRAND_NAME', 'PRIVACY_URL', 'DISPLAY_MANIFEST', 'CREATE_NEW_ACCOUNT_FROM_DASHBOARD', diff --git a/app/controllers/devise_overrides/sessions_controller.rb b/app/controllers/devise_overrides/sessions_controller.rb index e623e52f7..fc7b12767 100644 --- a/app/controllers/devise_overrides/sessions_controller.rb +++ b/app/controllers/devise_overrides/sessions_controller.rb @@ -4,6 +4,10 @@ class DeviseOverrides::SessionsController < DeviseTokenAuth::SessionsController wrap_parameters format: [] before_action :process_sso_auth_token, only: [:create] + def new + redirect_to login_page_url(error: 'access-denied') + end + def create # Authenticate user via the temporary sso auth token if params[:sso_auth_token].present? && @resource.present? @@ -21,6 +25,12 @@ class DeviseOverrides::SessionsController < DeviseTokenAuth::SessionsController private + def login_page_url(error: nil) + frontend_url = ENV.fetch('FRONTEND_URL', nil) + + "#{frontend_url}/app/login?error=#{error}" + end + def authenticate_resource_with_sso_token @token = @resource.create_token @resource.save! diff --git a/app/controllers/public/api/v1/portals/articles_controller.rb b/app/controllers/public/api/v1/portals/articles_controller.rb index 4e967cfbb..46ecf19a7 100644 --- a/app/controllers/public/api/v1/portals/articles_controller.rb +++ b/app/controllers/public/api/v1/portals/articles_controller.rb @@ -7,7 +7,7 @@ class Public::Api::V1::Portals::ArticlesController < Public::Api::V1::Portals::B def index @articles = @portal.articles - @articles = @articles.search(list_params) if list_params.present? + search_articles order_by_sort_param @articles.page(list_params[:page]) if list_params[:page].present? end @@ -16,6 +16,10 @@ class Public::Api::V1::Portals::ArticlesController < Public::Api::V1::Portals::B private + def search_articles + @articles = @articles.search(list_params) if list_params.present? + end + def order_by_sort_param @articles = if list_params[:sort].present? && list_params[:sort] == 'views' @articles.order_by_views @@ -51,3 +55,5 @@ class Public::Api::V1::Portals::ArticlesController < Public::Api::V1::Portals::B ChatwootMarkdownRenderer.new(content).render_article end end + +Public::Api::V1::Portals::ArticlesController.prepend_mod_with('Public::Api::V1::Portals::ArticlesController') diff --git a/app/controllers/public/api/v1/portals/base_controller.rb b/app/controllers/public/api/v1/portals/base_controller.rb index 4d3cc56b8..f6c10f7c4 100644 --- a/app/controllers/public/api/v1/portals/base_controller.rb +++ b/app/controllers/public/api/v1/portals/base_controller.rb @@ -47,7 +47,7 @@ class Public::Api::V1::Portals::BaseController < PublicController @locale = if article.category.present? article.category.locale else - 'en' + article.portal.default_locale end I18n.with_locale(@locale, &) diff --git a/app/controllers/public/api/v1/portals_controller.rb b/app/controllers/public/api/v1/portals_controller.rb index fe51ed161..e8fb867cb 100644 --- a/app/controllers/public/api/v1/portals_controller.rb +++ b/app/controllers/public/api/v1/portals_controller.rb @@ -6,6 +6,12 @@ class Public::Api::V1::PortalsController < Public::Api::V1::Portals::BaseControl def show; end + def sitemap + @help_center_url = @portal.custom_domain || ChatwootApp.help_center_root + # if help_center_url does not contain a protocol, prepend it with https + @help_center_url = "https://#{@help_center_url}" unless @help_center_url.include?('://') + end + private def portal diff --git a/app/controllers/public_controller.rb b/app/controllers/public_controller.rb index 0c3f52ff6..3b83a2210 100644 --- a/app/controllers/public_controller.rb +++ b/app/controllers/public_controller.rb @@ -8,8 +8,7 @@ class PublicController < ActionController::Base def ensure_custom_domain_request domain = request.host - - return if [URI.parse(ENV.fetch('FRONTEND_URL', '')).host, URI.parse(ENV.fetch('HELPCENTER_URL', '')).host].include?(domain) + return if DomainHelper.chatwoot_domain?(domain) @portal = ::Portal.find_by(custom_domain: domain) return if @portal.present? 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/helpers/application_helper.rb b/app/helpers/application_helper.rb index b52b2300e..76d0bacc8 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -2,4 +2,11 @@ module ApplicationHelper def available_locales_with_name LANGUAGES_CONFIG.map { |_key, val| val.slice(:name, :iso_639_1_code) } end + + def feature_help_urls + features = YAML.safe_load(Rails.root.join('config/features.yml').read).freeze + features.each_with_object({}) do |feature, hash| + hash[feature['name']] = feature['help_url'] if feature['help_url'] + end + end end diff --git a/app/helpers/filter_helper.rb b/app/helpers/filter_helper.rb new file mode 100644 index 000000000..bce2de5ea --- /dev/null +++ b/app/helpers/filter_helper.rb @@ -0,0 +1,84 @@ +module FilterHelper + def build_condition_query(model_filters, query_hash, current_index) + current_filter = model_filters[query_hash['attribute_key']] + + # Throw InvalidOperator Error if the attribute is a standard attribute + # and the operator is not allowed in the config + if current_filter.present? && current_filter['filter_operators'].exclude?(query_hash[:filter_operator]) + raise CustomExceptions::CustomFilter::InvalidOperator.new( + attribute_name: query_hash['attribute_key'], + allowed_keys: current_filter['filter_operators'] + ) + end + + # Every other filter expects a value to be present + if %w[is_present is_not_present].exclude?(query_hash[:filter_operator]) && query_hash['values'].blank? + raise CustomExceptions::CustomFilter::InvalidValue.new(attribute_name: query_hash['attribute_key']) + end + + condition_query = build_condition_query_string(current_filter, query_hash, current_index) + # The query becomes empty only when it doesn't match to any supported + # standard attribute or custom attribute defined in the account. + if condition_query.empty? + raise CustomExceptions::CustomFilter::InvalidAttribute.new(key: query_hash['attribute_key'], + allowed_keys: model_filters.keys) + end + + condition_query + end + + def build_condition_query_string(current_filter, query_hash, current_index) + filter_operator_value = filter_operation(query_hash, current_index) + + return handle_nil_filter(query_hash, current_index) if current_filter.nil? + + case current_filter['attribute_type'] + when 'additional_attributes' + handle_additional_attributes(query_hash, filter_operator_value, current_filter['data_type']) + else + handle_standard_attributes(current_filter, query_hash, current_index, filter_operator_value) + end + end + + def handle_nil_filter(query_hash, current_index) + attribute_type = "#{filter_config[:entity].downcase}_attribute" + custom_attribute_query(query_hash, attribute_type, current_index) + end + + def handle_additional_attributes(query_hash, filter_operator_value, data_type) + if data_type == 'text_case_insensitive' + "LOWER(#{filter_config[:table_name]}.additional_attributes ->> '#{query_hash[:attribute_key]}') " \ + "#{filter_operator_value} #{query_hash[:query_operator]}" + else + "#{filter_config[:table_name]}.additional_attributes ->> '#{query_hash[:attribute_key]}' " \ + "#{filter_operator_value} #{query_hash[:query_operator]} " + end + end + + def handle_standard_attributes(current_filter, query_hash, current_index, filter_operator_value) + case current_filter['data_type'] + when 'date' + date_filter(current_filter, query_hash, filter_operator_value) + when 'labels' + tag_filter_query(query_hash, current_index) + when 'text_case_insensitive' + text_case_insensitive_filter(query_hash, filter_operator_value) + else + default_filter(query_hash, filter_operator_value) + end + end + + def date_filter(current_filter, query_hash, filter_operator_value) + "(#{filter_config[:table_name]}.#{query_hash[:attribute_key]})::#{current_filter['data_type']} " \ + "#{filter_operator_value}#{current_filter['data_type']} #{query_hash[:query_operator]}" + end + + def text_case_insensitive_filter(query_hash, filter_operator_value) + "LOWER(#{filter_config[:table_name]}.#{query_hash[:attribute_key]}) " \ + "#{filter_operator_value} #{query_hash[:query_operator]}" + end + + def default_filter(query_hash, filter_operator_value) + "#{filter_config[:table_name]}.#{query_hash[:attribute_key]} #{filter_operator_value} #{query_hash[:query_operator]}" + end +end diff --git a/app/javascript/dashboard/api/reports.js b/app/javascript/dashboard/api/reports.js index 987b69701..52fa7f444 100644 --- a/app/javascript/dashboard/api/reports.js +++ b/app/javascript/dashboard/api/reports.js @@ -84,6 +84,24 @@ class ReportsAPI extends ApiClient { params: { since, until, business_hours: businessHours }, }); } + + getBotMetrics({ from, to } = {}) { + return axios.get(`${this.url}/bot_metrics`, { + params: { since: from, until: to }, + }); + } + + getBotSummary({ from, to, groupBy, businessHours } = {}) { + return axios.get(`${this.url}/bot_summary`, { + params: { + since: from, + until: to, + type: 'account', + group_by: groupBy, + business_hours: businessHours, + }, + }); + } } export default new ReportsAPI(); diff --git a/app/javascript/dashboard/api/slaReports.js b/app/javascript/dashboard/api/slaReports.js new file mode 100644 index 000000000..fedc988b2 --- /dev/null +++ b/app/javascript/dashboard/api/slaReports.js @@ -0,0 +1,78 @@ +/* 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, + label_list, + page, + } = {}) { + return axios.get(this.url, { + params: { + since: from, + until: to, + assigned_agent_id, + inbox_id, + team_id, + sla_policy_id, + label_list, + page, + }, + }); + } + + download({ + from, + to, + assigned_agent_id, + inbox_id, + team_id, + sla_policy_id, + label_list, + } = {}) { + return axios.get(`${this.url}/download`, { + params: { + since: from, + until: to, + assigned_agent_id, + inbox_id, + team_id, + label_list, + sla_policy_id, + }, + }); + } + + getMetrics({ + from, + to, + assigned_agent_id, + inbox_id, + team_id, + label_list, + sla_policy_id, + } = {}) { + return axios.get(`${this.url}/metrics`, { + params: { + since: from, + until: to, + assigned_agent_id, + inbox_id, + label_list, + team_id, + sla_policy_id, + }, + }); + } +} + +export default new SLAReportsAPI(); diff --git a/app/javascript/dashboard/api/specs/reports.spec.js b/app/javascript/dashboard/api/specs/reports.spec.js index 7822dad8f..05d4a152c 100644 --- a/app/javascript/dashboard/api/specs/reports.spec.js +++ b/app/javascript/dashboard/api/specs/reports.spec.js @@ -111,6 +111,40 @@ describe('#Reports API', () => { }); }); + it('#getBotMetrics', () => { + reportsAPI.getBotMetrics({ from: 1621103400, to: 1621621800 }); + expect(axiosMock.get).toHaveBeenCalledWith( + '/api/v2/reports/bot_metrics', + { + params: { + since: 1621103400, + until: 1621621800, + }, + } + ); + }); + + it('#getBotSummary', () => { + reportsAPI.getBotSummary({ + from: 1621103400, + to: 1621621800, + groupBy: 'date', + businessHours: true, + }); + expect(axiosMock.get).toHaveBeenCalledWith( + '/api/v2/reports/bot_summary', + { + params: { + since: 1621103400, + until: 1621621800, + type: 'account', + group_by: 'date', + business_hours: true, + }, + } + ); + }); + it('#getConversationMetric', () => { reportsAPI.getConversationMetric('account'); expect(axiosMock.get).toHaveBeenCalledWith( 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..f540b6acc --- /dev/null +++ b/app/javascript/dashboard/api/specs/slaReports.spec.js @@ -0,0 +1,104 @@ +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, + label_list: ['label1'], + }); + 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, + label_list: ['label1'], + }, + }); + }); + it('#getMetrics', () => { + SLAReportsAPI.getMetrics({ + from: 1622485800, + to: 1623695400, + assigned_agent_id: 1, + inbox_id: 1, + team_id: 1, + sla_policy_id: 1, + label_list: ['label1'], + }); + 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, + label_list: ['label1'], + }, + } + ); + }); + it('#download', () => { + SLAReportsAPI.download({ + from: 1622485800, + to: 1623695400, + assigned_agent_id: 1, + inbox_id: 1, + team_id: 1, + sla_policy_id: 1, + label_list: ['label1'], + }); + 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, + label_list: ['label1'], + }, + } + ); + }); + }); +}); diff --git a/app/javascript/dashboard/assets/scss/_date-picker.scss b/app/javascript/dashboard/assets/scss/_date-picker.scss index 2132d5fd5..60c24b421 100644 --- a/app/javascript/dashboard/assets/scss/_date-picker.scss +++ b/app/javascript/dashboard/assets/scss/_date-picker.scss @@ -1,6 +1,13 @@ @import '~vue2-datepicker/scss/index'; .date-picker { + // To be removed one SLA reports date picker is created + &.small { + .mx-input { + @apply h-8 text-sm; + } + } + &.no-margin { .mx-input { @apply mb-0; diff --git a/app/javascript/dashboard/assets/scss/_layout.scss b/app/javascript/dashboard/assets/scss/_layout.scss index ea40c1f3a..b9c8a9bf1 100644 --- a/app/javascript/dashboard/assets/scss/_layout.scss +++ b/app/javascript/dashboard/assets/scss/_layout.scss @@ -1,17 +1,17 @@ // scss-lint:disable SpaceAfterPropertyColon -// @import 'shared/assets/fonts/inter'; - +@import 'shared/assets/fonts/inter'; +// Inter, html, body { font-family: 'PlusJakarta', - Inter, -apple-system, system-ui, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', + Tahoma, Arial, sans-serif !important; -moz-osx-font-smoothing: grayscale; diff --git a/app/javascript/dashboard/assets/scss/_woot.scss b/app/javascript/dashboard/assets/scss/_woot.scss index 5728dd51b..4b33d5b10 100644 --- a/app/javascript/dashboard/assets/scss/_woot.scss +++ b/app/javascript/dashboard/assets/scss/_woot.scss @@ -40,3 +40,347 @@ .hide { @apply hidden; } + +@layer base { + // scss-lint:disable PropertySortOrder + :root { + --color-amber-25: 254 253 251; + --color-amber-50: 255 249 237; + --color-amber-75: 255 243 208; + --color-amber-100: 255 236 183; + --color-amber-200: 255 224 161; + --color-amber-300: 245 208 140; + --color-amber-400: 228 187 120; + --color-amber-500: 214 163 92; + --color-amber-600: 214 163 92; + --color-amber-700: 255 186 26; + --color-amber-800: 145 89 48; + --color-amber-900: 79 52 34; + + --color-ash-100: 235 235 239; + --color-ash-200: 228 228 233; + --color-ash-25: 252 252 253; + --color-ash-300: 221 221 227; + --color-ash-400: 211 212 219; + --color-ash-50: 249 249 251; + --color-ash-500: 185 187 198; + --color-ash-600: 139 141 152; + --color-ash-700: 126 128 138; + --color-ash-75: 242 242 245; + --color-ash-800: 96 100 108; + --color-ash-900: 28 32 36; + + --color-primary-25: 251 253 255; + --color-primary-50: 245 249 255; + --color-primary-75: 233 243 255; + --color-primary-100: 218 236 255; + --color-primary-200: 201 226 255; + --color-primary-300: 181 213 255; + --color-primary-400: 155 195 252; + --color-primary-500: 117 171 247; + --color-primary-600: 39 129 246; + --color-primary-700: 16 115 233; + --color-primary-800: 8 109 224; + --color-primary-900: 11 50 101; + + --color-ruby-100: 255 220 225; + --color-ruby-200: 255 206 214; + --color-ruby-25: 255 252 253; + --color-ruby-300: 248 191 200; + --color-ruby-400: 239 172 184; + --color-ruby-50: 255 247 248; + --color-ruby-500: 229 146 163; + --color-ruby-600: 229 70 102; + --color-ruby-700: 220 59 93; + --color-ruby-75: 254 234 237; + --color-ruby-800: 202 36 77; + --color-ruby-900: 100 23 43; + + --color-teal-100: 224 248 243; + --color-teal-200: 204 243 234; + --color-teal-25: 250 254 253; + --color-teal-300: 184 234 224; + --color-teal-400: 161 222 210; + --color-teal-50: 243 251 249; + --color-teal-500: 83 185 171; + --color-teal-600: 18 165 148; + --color-teal-700: 13 155 138; + --color-teal-75: 236 249 255; + --color-teal-800: 0 133 115; + --color-teal-900: 13 61 56; + + --color-green-25: 251 254 252; + --color-green-50: 244 251 246; + --color-green-75: 230 246 235; + --color-green-100: 214 241 223; + --color-green-200: 196 232 209; + --color-green-300: 173 221 192; + --color-green-400: 142 206 170; + --color-green-500: 91 185 139; + --color-green-600: 48 164 108; + --color-green-700: 43 154 102; + --color-green-800: 33 131 88; + --color-green-900: 25 59 45; + + --color-mint-25: 249 254 253; + --color-mint-50: 242 251 249; + --color-mint-75: 221 249 242; + --color-mint-100: 200 244 233; + --color-mint-200: 179 236 222; + --color-mint-300: 156 224 208; + --color-mint-400: 126 207 189; + --color-mint-500: 76 187 165; + --color-mint-600: 134 234 212; + --color-mint-700: 125 224 203; + --color-mint-800: 2 120 100; + --color-mint-900: 22 67 60; + + --color-sky-25: 249 254 255; + --color-sky-50: 241 250 253; + --color-sky-75: 225 246 253; + --color-sky-100: 209 240 250; + --color-sky-200: 190 231 245; + --color-sky-300: 169 218 237; + --color-sky-400: 141 202 227; + --color-sky-500: 96 179 215; + --color-sky-600: 124 226 254; + --color-sky-700: 116 218 248; + --color-sky-800: 0 116 158; + --color-sky-900: 29 62 86; + + --color-indigo-25: 253 253 254; + --color-indigo-50: 247 249 255; + --color-indigo-75: 237 242 254; + --color-indigo-100: 225 233 255; + --color-indigo-200: 210 222 255; + --color-indigo-300: 193 208 255; + --color-indigo-400: 171 189 249; + --color-indigo-500: 141 164 239; + --color-indigo-600: 62 99 221; + --color-indigo-700: 51 88 212; + --color-indigo-800: 58 91 199; + --color-indigo-900: 31 45 92; + + --color-iris-25: 253 253 255; + --color-iris-50: 248 248 255; + --color-iris-75: 240 241 254; + --color-iris-100: 230 231 255; + --color-iris-200: 218 220 255; + --color-iris-300: 203 205 255; + --color-iris-400: 184 186 248; + --color-iris-500: 155 158 240; + --color-iris-600: 91 91 214; + --color-iris-700: 81 81 205; + --color-iris-800: 87 83 198; + --color-iris-900: 39 41 98; + + --color-violet-25: 253 252 254; + --color-violet-50: 250 248 255; + --color-violet-75: 244 240 254; + --color-violet-100: 235 228 255; + --color-violet-200: 225 217 255; + --color-violet-300: 212 202 254; + --color-violet-400: 194 181 245; + --color-violet-500: 170 153 236; + --color-violet-600: 110 86 207; + --color-violet-700: 101 77 196; + --color-violet-800: 101 80 185; + --color-violet-900: 47 38 95; + + --color-pink-25: 255 252 254; + --color-pink-50: 254 247 251; + --color-pink-75: 254 233 245; + --color-pink-100: 251 220 239; + --color-pink-200: 246 206 231; + --color-pink-300: 239 191 221; + --color-pink-400: 231 172 208; + --color-pink-500: 221 147 194; + --color-pink-600: 214 64 159; + --color-pink-700: 207 56 151; + --color-pink-800: 194 41 138; + --color-pink-900: 101 18 73; + + --color-orange-25: 254 252 251; + --color-orange-50: 255 247 237; + --color-orange-75: 255 239 214; + --color-orange-100: 255 223 181; + --color-orange-200: 255 209 154; + --color-orange-300: 255 193 130; + --color-orange-400: 245 174 115; + --color-orange-500: 236 148 85; + --color-orange-600: 247 107 21; + --color-orange-700: 239 95 0; + --color-orange-800: 204 78 0; + --color-orange-900: 88 45 29; + } + // scss-lint:disable QualifyingElement + body.dark { + --color-amber-25: 31 19 0; + --color-amber-50: 37 24 4; + --color-amber-75: 48 32 11; + --color-amber-100: 57 39 15; + --color-amber-200: 67 46 18; + --color-amber-300: 83 57 22; + --color-amber-400: 111 77 29; + --color-amber-500: 169 118 42; + --color-amber-600: 169 118 42; + --color-amber-700: 255 203 71; + --color-amber-800: 255 204 77; + --color-amber-900: 255 231 179; + + --color-ash-100: 46 48 53; + --color-ash-200: 53 55 60; + --color-ash-25: 24 24 26; + --color-ash-300: 60 63 68; + --color-ash-400: 70 75 80; + --color-ash-50: 27 27 31; + --color-ash-500: 90 97 101; + --color-ash-600: 105 110 119; + --color-ash-700: 120 127 133; + --color-ash-75: 39 40 45; + --color-ash-800: 173 177 184; + --color-ash-900: 237 238 240; + + --color-primary-25: 10 17 28; + --color-primary-50: 15 24 38; + --color-primary-75: 15 39 72; + --color-primary-100: 10 49 99; + --color-primary-200: 18 61 117; + --color-primary-300: 29 74 134; + --color-primary-400: 40 89 156; + --color-primary-500: 48 106 186; + --color-primary-600: 39 129 246; + --color-primary-700: 21 116 231; + --color-primary-800: 126 182 255; + --color-primary-900: 205 227 255; + + --color-ruby-100: 78 19 37; + --color-ruby-200: 94 26 46; + --color-ruby-25: 25 17 19; + --color-ruby-300: 111 37 57; + --color-ruby-400: 136 52 71; + --color-ruby-50: 30 21 23; + --color-ruby-500: 179 68 90; + --color-ruby-600: 229 70 102; + --color-ruby-700: 236 90 114; + --color-ruby-75: 58 20 30; + --color-ruby-800: 255 148 157; + --color-ruby-900: 254 210 225; + + --color-teal-100: 2 59 55; + --color-teal-200: 8 72 67; + --color-teal-25: 13 21 20; + --color-teal-300: 28 105 97; + --color-teal-400: 28 105 97; + --color-teal-50: 17 28 27; + --color-teal-500: 32 126 115; + --color-teal-600: 41 163 131; + --color-teal-700: 14 179 158; + --color-teal-75: 13 45 42; + --color-teal-800: 11 216 182; + --color-teal-900: 173 240 221; + + --color-green-25: 14 21 18; + --color-green-50: 18 27 23; + --color-green-75: 19 45 33; + --color-green-100: 17 59 41; + --color-green-200: 23 73 51; + --color-green-300: 32 87 62; + --color-green-400: 40 104 74; + --color-green-500: 47 124 87; + --color-green-600: 48 164 108; + --color-green-700: 51 176 116; + --color-green-800: 61 214 140; + --color-green-900: 177 241 203; + + --color-mint-25: 14 21 21; + --color-mint-50: 15 27 27; + --color-mint-75: 9 44 43; + --color-mint-100: 0 58 56; + --color-mint-200: 0 71 68; + --color-mint-300: 16 86 80; + --color-mint-400: 30 104 95; + --color-mint-500: 39 127 112; + --color-mint-600: 134 234 212; + --color-mint-700: 168 245 229; + --color-mint-800: 88 213 186; + --color-mint-900: 196 245 225; + + --color-sky-25: 14 21 21; + --color-sky-50: 15 27 27; + --color-sky-75: 9 44 43; + --color-sky-100: 0 58 56; + --color-sky-200: 0 71 68; + --color-sky-300: 16 86 80; + --color-sky-400: 30 104 95; + --color-sky-500: 39 127 112; + --color-sky-600: 134 234 212; + --color-sky-700: 168 245 229; + --color-sky-800: 88 213 186; + --color-sky-900: 196 245 225; + + --color-indigo-25: 17 19 31; + --color-indigo-50: 20 23 38; + --color-indigo-75: 24 36 73; + --color-indigo-100: 29 46 98; + --color-indigo-200: 37 57 116; + --color-indigo-300: 48 67 132; + --color-indigo-400: 58 79 151; + --color-indigo-500: 67 93 177; + --color-indigo-600: 62 99 221; + --color-indigo-700: 84 114 228; + --color-indigo-800: 158 177 255; + --color-indigo-900: 214 225 255; + + --color-iris-25: 19 19 30; + --color-iris-50: 23 22 37; + --color-iris-75: 32 34 72; + --color-iris-100: 38 42 101; + --color-iris-200: 48 51 116; + --color-iris-300: 61 62 130; + --color-iris-400: 74 74 149; + --color-iris-500: 89 88 177; + --color-iris-600: 91 91 214; + --color-iris-700: 110 106 222; + --color-iris-800: 177 169 255; + --color-iris-900: 224 223 254; + + --color-violet-25: 20 18 31; + --color-violet-50: 27 21 37; + --color-violet-75: 41 31 67; + --color-violet-100: 51 37 91; + --color-violet-200: 60 46 105; + --color-violet-300: 71 56 118; + --color-violet-400: 86 70 139; + --color-violet-500: 105 88 173; + --color-violet-600: 110 86 207; + --color-violet-700: 125 102 217; + --color-violet-800: 186 167 255; + --color-violet-900: 226 221 254; + + --color-pink-25: 25 17 23; + --color-pink-50: 33 18 29; + --color-pink-75: 55 23 47; + --color-pink-100: 75 20 61; + --color-pink-200: 89 28 71; + --color-pink-300: 105 41 85; + --color-pink-400: 131 56 105; + --color-pink-500: 168 72 133; + --color-pink-600: 214 64 159; + --color-pink-700: 222 81 168; + --color-pink-800: 255 141 204; + --color-pink-900: 253 209 234; + --color-orange-25: 23 18 14; + --color-orange-50: 30 22 15; + --color-orange-75: 51 30 11; + --color-orange-100: 70 33 0; + --color-orange-200: 86 40 0; + --color-orange-300: 102 53 12; + --color-orange-400: 126 69 29; + --color-orange-500: 163 88 41; + --color-orange-600: 247 107 21; + --color-orange-700: 255 128 31; + --color-orange-800: 255 160 87; + --color-orange-900: 255 224 194; + } +} diff --git a/app/javascript/dashboard/assets/scss/plugins/_multiselect.scss b/app/javascript/dashboard/assets/scss/plugins/_multiselect.scss index 9170715e0..99b006045 100644 --- a/app/javascript/dashboard/assets/scss/plugins/_multiselect.scss +++ b/app/javascript/dashboard/assets/scss/plugins/_multiselect.scss @@ -153,6 +153,29 @@ } .multiselect-wrap--small { + // To be removed one SLA reports date picker is created + &.tiny { + .multiselect.no-margin { + @apply min-h-[32px]; + } + + .multiselect__select { + @apply min-h-[32px] h-8; + + &::before { + @apply top-[60%]; + } + } + + .multiselect__tags { + @apply min-h-[32px] max-h-[32px]; + + .multiselect__single { + @apply pt-1 pb-1; + } + } + } + .multiselect__tags, .multiselect__input, .multiselect { diff --git a/app/javascript/dashboard/assets/scss/widgets/_base.scss b/app/javascript/dashboard/assets/scss/widgets/_base.scss index e64231dfc..1ebcef53c 100644 --- a/app/javascript/dashboard/assets/scss/widgets/_base.scss +++ b/app/javascript/dashboard/assets/scss/widgets/_base.scss @@ -63,12 +63,12 @@ input:focus { } // Inputs -input[type='text'], -input[type='number'], -input[type='password'], -input[type='date'], -input[type='email'], -input[type='url'] { +input[type='text']:not(.reset-base), +input[type='number']:not(.reset-base), +input[type='password']:not(.reset-base), +input[type='date']:not(.reset-base), +input[type='email']:not(.reset-base), +input[type='url']:not(.reset-base) { @apply block box-border w-full transition-colors focus:border-woot-500 dark:focus:border-woot-600 duration-[0.25s] ease-[ease-in-out] h-10 appearance-none mx-0 mt-0 mb-4 p-2 rounded-md text-base font-normal bg-white dark:bg-slate-900 focus:bg-white focus:dark:bg-slate-900 text-slate-900 dark:text-slate-100 border border-solid border-slate-200 dark:border-slate-600; &[disabled] { diff --git a/app/javascript/dashboard/assets/scss/widgets/_buttons.scss b/app/javascript/dashboard/assets/scss/widgets/_buttons.scss index 54fc27e25..587b810ca 100644 --- a/app/javascript/dashboard/assets/scss/widgets/_buttons.scss +++ b/app/javascript/dashboard/assets/scss/widgets/_buttons.scss @@ -118,7 +118,7 @@ button { @apply border border-woot-500 bg-transparent dark:bg-transparent dark:border-woot-500 text-woot-500 dark:text-woot-500 hover:bg-woot-50 dark:hover:bg-woot-900; &.secondary { - @apply text-slate-700 border-slate-200 dark:border-slate-600 dark:text-slate-100 hover:bg-slate-50 dark:hover:bg-slate-700; + @apply text-slate-700 border-slate-100 dark:border-slate-800 dark:text-slate-100 hover:bg-slate-50 dark:hover:bg-slate-700; } &.success { diff --git a/app/javascript/dashboard/components/ChatList.vue b/app/javascript/dashboard/components/ChatList.vue index a75f0c12b..e7fae6e01 100644 --- a/app/javascript/dashboard/components/ChatList.vue +++ b/app/javascript/dashboard/components/ChatList.vue @@ -1,14 +1,14 @@ diff --git a/app/javascript/dashboard/components/widgets/conversation/components/SLAEventItem.vue b/app/javascript/dashboard/components/widgets/conversation/components/SLAEventItem.vue new file mode 100644 index 000000000..3affa52c8 --- /dev/null +++ b/app/javascript/dashboard/components/widgets/conversation/components/SLAEventItem.vue @@ -0,0 +1,35 @@ + + diff --git a/app/javascript/dashboard/components/widgets/conversation/components/SLAPopoverCard.vue b/app/javascript/dashboard/components/widgets/conversation/components/SLAPopoverCard.vue new file mode 100644 index 000000000..479e2794f --- /dev/null +++ b/app/javascript/dashboard/components/widgets/conversation/components/SLAPopoverCard.vue @@ -0,0 +1,85 @@ + + diff --git a/app/javascript/dashboard/components/widgets/conversation/conversationBulkActions/AgentSelector.vue b/app/javascript/dashboard/components/widgets/conversation/conversationBulkActions/AgentSelector.vue index 1a4b4febe..4dd06f673 100644 --- a/app/javascript/dashboard/components/widgets/conversation/conversationBulkActions/AgentSelector.vue +++ b/app/javascript/dashboard/components/widgets/conversation/conversationBulkActions/AgentSelector.vue @@ -101,14 +101,12 @@ import { mapGetters } from 'vuex'; import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue'; import Spinner from 'shared/components/Spinner.vue'; -import { mixin as clickaway } from 'vue-clickaway'; export default { components: { Thumbnail, Spinner, }, - mixins: [clickaway], props: { selectedInboxes: { type: Array, diff --git a/app/javascript/dashboard/components/widgets/conversation/conversationBulkActions/LabelActions.vue b/app/javascript/dashboard/components/widgets/conversation/conversationBulkActions/LabelActions.vue index 5118ec69c..80224532a 100644 --- a/app/javascript/dashboard/components/widgets/conversation/conversationBulkActions/LabelActions.vue +++ b/app/javascript/dashboard/components/widgets/conversation/conversationBulkActions/LabelActions.vue @@ -74,11 +74,9 @@ + + diff --git a/app/javascript/dashboard/routes/dashboard/conversation/ConversationParticipant.vue b/app/javascript/dashboard/routes/dashboard/conversation/ConversationParticipant.vue index 2daf16c3f..820295b57 100644 --- a/app/javascript/dashboard/routes/dashboard/conversation/ConversationParticipant.vue +++ b/app/javascript/dashboard/routes/dashboard/conversation/ConversationParticipant.vue @@ -78,7 +78,6 @@ - - diff --git a/app/javascript/dashboard/routes/dashboard/conversation/customAttributes/CustomAttributeDropDownItem.vue b/app/javascript/dashboard/routes/dashboard/conversation/customAttributes/CustomAttributeDropDownItem.vue deleted file mode 100644 index 8bae9c1b0..000000000 --- a/app/javascript/dashboard/routes/dashboard/conversation/customAttributes/CustomAttributeDropDownItem.vue +++ /dev/null @@ -1,79 +0,0 @@ - - - - - diff --git a/app/javascript/dashboard/routes/dashboard/conversation/customAttributes/CustomAttributeSelector.vue b/app/javascript/dashboard/routes/dashboard/conversation/customAttributes/CustomAttributeSelector.vue deleted file mode 100644 index 60ef9d860..000000000 --- a/app/javascript/dashboard/routes/dashboard/conversation/customAttributes/CustomAttributeSelector.vue +++ /dev/null @@ -1,138 +0,0 @@ - - - - - diff --git a/app/javascript/dashboard/routes/dashboard/conversation/customAttributes/CustomAttributes.vue b/app/javascript/dashboard/routes/dashboard/conversation/customAttributes/CustomAttributes.vue index ca721d4d8..20bef48f6 100644 --- a/app/javascript/dashboard/routes/dashboard/conversation/customAttributes/CustomAttributes.vue +++ b/app/javascript/dashboard/routes/dashboard/conversation/customAttributes/CustomAttributes.vue @@ -1,23 +1,42 @@ @@ -25,13 +44,14 @@ import CustomAttribute from 'dashboard/components/CustomAttribute.vue'; import alertMixin from 'shared/mixins/alertMixin'; import attributeMixin from 'dashboard/mixins/attributeMixin'; +import uiSettingsMixin from 'dashboard/mixins/uiSettings'; import { copyTextToClipboard } from 'shared/helpers/clipboard'; export default { components: { CustomAttribute, }, - mixins: [alertMixin, attributeMixin], + mixins: [alertMixin, attributeMixin, uiSettingsMixin], props: { attributeType: { type: String, @@ -42,8 +62,71 @@ export default { default: '', }, contactId: { type: Number, default: null }, + attributeFrom: { + type: String, + required: true, + }, + emptyStateMessage: { + type: String, + default: '', + }, + }, + data() { + return { + showAllAttributes: false, + }; + }, + computed: { + toggleButtonText() { + return !this.showAllAttributes + ? this.$t('CUSTOM_ATTRIBUTES.SHOW_MORE') + : this.$t('CUSTOM_ATTRIBUTES.SHOW_LESS'); + }, + filteredAttributes() { + return this.attributes.map(attribute => { + // Check if the attribute key exists in customAttributes + const hasValue = Object.hasOwnProperty.call( + this.customAttributes, + attribute.attribute_key + ); + + const isCheckbox = attribute.attribute_display_type === 'checkbox'; + const defaultValue = isCheckbox ? false : ''; + + return { + ...attribute, + // Set value from customAttributes if it exists, otherwise use default value + value: hasValue + ? this.customAttributes[attribute.attribute_key] + : defaultValue, + }; + }); + }, + displayedAttributes() { + // Show only the first 5 attributes or all depending on showAllAttributes + if (this.showAllAttributes || this.filteredAttributes.length <= 5) { + return this.filteredAttributes; + } + return this.filteredAttributes.slice(0, 5); + }, + showMoreUISettingsKey() { + return `show_all_attributes_${this.attributeFrom}`; + }, + }, + mounted() { + this.initializeSettings(); }, methods: { + initializeSettings() { + this.showAllAttributes = + this.uiSettings[this.showMoreUISettingsKey] || false; + }, + onClickToggle() { + this.showAllAttributes = !this.showAllAttributes; + this.updateUISettings({ + [this.showMoreUISettingsKey]: this.showAllAttributes, + }); + }, async onUpdate(key, value) { const updatedAttributes = { ...this.customAttributes, [key]: value }; try { @@ -96,16 +179,17 @@ export default { }, }; + + +function onFileChange({ file }) { + if (checkFileSizeLimit(file, MAXIMUM_FILE_UPLOAD_SIZE)) { + uploadLogoToStorage(file); + } else { + const errorKey = + 'PROFILE_SETTINGS.FORM.MESSAGE_SIGNATURE_SECTION.IMAGE_UPLOAD_SIZE_ERROR'; + useAlert(t(errorKey, { size: MAXIMUM_FILE_UPLOAD_SIZE })); + } +} + + + diff --git a/app/javascript/dashboard/routes/dashboard/helpcenter/components/PortalSettingsCustomizationForm.vue b/app/javascript/dashboard/routes/dashboard/helpcenter/components/PortalSettingsCustomizationForm.vue index c00ab9beb..7572f5b80 100644 --- a/app/javascript/dashboard/routes/dashboard/helpcenter/components/PortalSettingsCustomizationForm.vue +++ b/app/javascript/dashboard/routes/dashboard/helpcenter/components/PortalSettingsCustomizationForm.vue @@ -1,68 +1,140 @@ + + - diff --git a/app/javascript/dashboard/routes/dashboard/helpcenter/pages/categories/NameEmojiInput.vue b/app/javascript/dashboard/routes/dashboard/helpcenter/pages/categories/NameEmojiInput.vue index 5d6862758..b888a5440 100644 --- a/app/javascript/dashboard/routes/dashboard/helpcenter/pages/categories/NameEmojiInput.vue +++ b/app/javascript/dashboard/routes/dashboard/helpcenter/pages/categories/NameEmojiInput.vue @@ -36,13 +36,10 @@ + - - diff --git a/app/javascript/dashboard/routes/dashboard/helpcenter/pages/portals/EditPortalCustomization.vue b/app/javascript/dashboard/routes/dashboard/helpcenter/pages/portals/EditPortalCustomization.vue index d1c9341f1..902dc3ca6 100644 --- a/app/javascript/dashboard/routes/dashboard/helpcenter/pages/portals/EditPortalCustomization.vue +++ b/app/javascript/dashboard/routes/dashboard/helpcenter/pages/portals/EditPortalCustomization.vue @@ -1,3 +1,48 @@ + + - - diff --git a/app/javascript/dashboard/routes/dashboard/helpcenter/pages/portals/EditPortalLocales.vue b/app/javascript/dashboard/routes/dashboard/helpcenter/pages/portals/EditPortalLocales.vue index 3c1c4c872..e6cee06a4 100644 --- a/app/javascript/dashboard/routes/dashboard/helpcenter/pages/portals/EditPortalLocales.vue +++ b/app/javascript/dashboard/routes/dashboard/helpcenter/pages/portals/EditPortalLocales.vue @@ -1,6 +1,117 @@ + + - - - diff --git a/app/javascript/dashboard/routes/dashboard/helpcenter/pages/portals/NewPortal.vue b/app/javascript/dashboard/routes/dashboard/helpcenter/pages/portals/NewPortal.vue index e14723476..0222044bc 100644 --- a/app/javascript/dashboard/routes/dashboard/helpcenter/pages/portals/NewPortal.vue +++ b/app/javascript/dashboard/routes/dashboard/helpcenter/pages/portals/NewPortal.vue @@ -1,5 +1,5 @@ + - - diff --git a/app/javascript/dashboard/routes/dashboard/helpcenter/pages/portals/PortalSettingsFinish.vue b/app/javascript/dashboard/routes/dashboard/helpcenter/pages/portals/PortalSettingsFinish.vue index 690f058a4..3a3e77142 100644 --- a/app/javascript/dashboard/routes/dashboard/helpcenter/pages/portals/PortalSettingsFinish.vue +++ b/app/javascript/dashboard/routes/dashboard/helpcenter/pages/portals/PortalSettingsFinish.vue @@ -1,6 +1,6 @@ - diff --git a/app/javascript/dashboard/routes/dashboard/inbox/components/InboxListHeader.vue b/app/javascript/dashboard/routes/dashboard/inbox/components/InboxListHeader.vue index 35411f546..556a22de0 100644 --- a/app/javascript/dashboard/routes/dashboard/inbox/components/InboxListHeader.vue +++ b/app/javascript/dashboard/routes/dashboard/inbox/components/InboxListHeader.vue @@ -57,7 +57,6 @@ + diff --git a/app/javascript/dashboard/routes/dashboard/settings/SettingsWrapper.vue b/app/javascript/dashboard/routes/dashboard/settings/SettingsWrapper.vue new file mode 100644 index 000000000..3f5bf16f9 --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/SettingsWrapper.vue @@ -0,0 +1,21 @@ + + + diff --git a/app/javascript/dashboard/routes/dashboard/settings/auditlogs/Index.vue b/app/javascript/dashboard/routes/dashboard/settings/auditlogs/Index.vue index 9d2cd8ee2..709d70b25 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/auditlogs/Index.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/auditlogs/Index.vue @@ -57,7 +57,7 @@ :current-page="Number(meta.currentPage)" :total-count="meta.totalEntries" :page-size="meta.perPage" - class="dark:bg-slate-900" + class="!bg-slate-25 dark:!bg-slate-900 border-t border-slate-75 dark:border-slate-700/50" @page-change="onPageChange" /> diff --git a/app/javascript/dashboard/routes/dashboard/settings/campaigns/CampaignsTable.vue b/app/javascript/dashboard/routes/dashboard/settings/campaigns/CampaignsTable.vue index cf900d1a8..3c73cbf77 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/campaigns/CampaignsTable.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/campaigns/CampaignsTable.vue @@ -21,7 +21,6 @@ + + diff --git a/app/javascript/dashboard/routes/dashboard/settings/components/BaseSettingsListItem.vue b/app/javascript/dashboard/routes/dashboard/settings/components/BaseSettingsListItem.vue new file mode 100644 index 000000000..337c370cd --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/components/BaseSettingsListItem.vue @@ -0,0 +1,53 @@ + + diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/Settings.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/Settings.vue index 87b6e6724..8823e593f 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/inbox/Settings.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/Settings.vue @@ -589,7 +589,9 @@ export default { return this.inbox.name; }, canLocktoSingleConversation() { - return this.isASmsInbox || this.isAWhatsAppChannel; + return ( + this.isASmsInbox || this.isAWhatsAppChannel || this.isAFacebookInbox + ); }, inboxNameLabel() { if (this.isAWebWidgetInbox) { diff --git a/app/javascript/dashboard/routes/dashboard/settings/personal/AudioAlertCondition.vue b/app/javascript/dashboard/routes/dashboard/settings/personal/AudioAlertCondition.vue new file mode 100644 index 000000000..ead4b77f2 --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/personal/AudioAlertCondition.vue @@ -0,0 +1,42 @@ + + diff --git a/app/javascript/dashboard/routes/dashboard/settings/personal/AudioAlertEvent.vue b/app/javascript/dashboard/routes/dashboard/settings/personal/AudioAlertEvent.vue new file mode 100644 index 000000000..ef4b95a36 --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/personal/AudioAlertEvent.vue @@ -0,0 +1,66 @@ + + + diff --git a/app/javascript/dashboard/routes/dashboard/settings/personal/AudioAlertTone.vue b/app/javascript/dashboard/routes/dashboard/settings/personal/AudioAlertTone.vue new file mode 100644 index 000000000..3246e12af --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/personal/AudioAlertTone.vue @@ -0,0 +1,56 @@ + + diff --git a/app/javascript/dashboard/routes/dashboard/settings/personal/AudioNotifications.vue b/app/javascript/dashboard/routes/dashboard/settings/personal/AudioNotifications.vue new file mode 100644 index 000000000..84f369f5a --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/personal/AudioNotifications.vue @@ -0,0 +1,128 @@ + + + diff --git a/app/javascript/dashboard/routes/dashboard/settings/personal/ChangePassword.vue b/app/javascript/dashboard/routes/dashboard/settings/personal/ChangePassword.vue new file mode 100644 index 000000000..0c2fa9e78 --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/personal/ChangePassword.vue @@ -0,0 +1,137 @@ + + + diff --git a/app/javascript/dashboard/routes/dashboard/settings/personal/HotKeyCard.vue b/app/javascript/dashboard/routes/dashboard/settings/personal/HotKeyCard.vue new file mode 100644 index 000000000..35cbbeb7b --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/personal/HotKeyCard.vue @@ -0,0 +1,64 @@ + + + diff --git a/app/javascript/dashboard/routes/dashboard/settings/personal/Index.vue b/app/javascript/dashboard/routes/dashboard/settings/personal/Index.vue new file mode 100644 index 000000000..ce3eb5846 --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/personal/Index.vue @@ -0,0 +1,245 @@ + + diff --git a/app/javascript/dashboard/routes/dashboard/settings/personal/MessageSignature.vue b/app/javascript/dashboard/routes/dashboard/settings/personal/MessageSignature.vue new file mode 100644 index 000000000..7639465be --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/personal/MessageSignature.vue @@ -0,0 +1,50 @@ + + diff --git a/app/javascript/dashboard/routes/dashboard/settings/personal/NotificationCheckBox.vue b/app/javascript/dashboard/routes/dashboard/settings/personal/NotificationCheckBox.vue new file mode 100644 index 000000000..feb180014 --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/personal/NotificationCheckBox.vue @@ -0,0 +1,53 @@ + + + diff --git a/app/javascript/dashboard/routes/dashboard/settings/personal/NotificationPreferences.vue b/app/javascript/dashboard/routes/dashboard/settings/personal/NotificationPreferences.vue new file mode 100644 index 000000000..a2c4555a7 --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/personal/NotificationPreferences.vue @@ -0,0 +1,284 @@ + + + diff --git a/app/javascript/dashboard/routes/dashboard/settings/personal/UserBasicDetails.vue b/app/javascript/dashboard/routes/dashboard/settings/personal/UserBasicDetails.vue new file mode 100644 index 000000000..1a0fb2eae --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/personal/UserBasicDetails.vue @@ -0,0 +1,135 @@ + + diff --git a/app/javascript/dashboard/routes/dashboard/settings/personal/UserProfilePicture.vue b/app/javascript/dashboard/routes/dashboard/settings/personal/UserProfilePicture.vue new file mode 100644 index 000000000..d4ab08bc1 --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/personal/UserProfilePicture.vue @@ -0,0 +1,40 @@ + + diff --git a/app/javascript/dashboard/routes/dashboard/settings/personal/constants.js b/app/javascript/dashboard/routes/dashboard/settings/personal/constants.js new file mode 100644 index 000000000..e25d5b52a --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/personal/constants.js @@ -0,0 +1,52 @@ +export const NOTIFICATION_TYPES = [ + { + label: 'PROFILE_SETTINGS.FORM.NOTIFICATIONS.TYPES.CONVERSATION_CREATED', + value: 'conversation_creation', + }, + { + label: 'PROFILE_SETTINGS.FORM.NOTIFICATIONS.TYPES.CONVERSATION_ASSIGNED', + value: 'conversation_assignment', + }, + { + label: 'PROFILE_SETTINGS.FORM.NOTIFICATIONS.TYPES.CONVERSATION_MENTION', + value: 'conversation_mention', + }, + { + label: + 'PROFILE_SETTINGS.FORM.NOTIFICATIONS.TYPES.ASSIGNED_CONVERSATION_NEW_MESSAGE', + value: 'assigned_conversation_new_message', + }, + { + label: + 'PROFILE_SETTINGS.FORM.NOTIFICATIONS.TYPES.PARTICIPATING_CONVERSATION_NEW_MESSAGE', + value: 'participating_conversation_new_message', + }, + { + label: + 'PROFILE_SETTINGS.FORM.NOTIFICATIONS.TYPES.SLA_MISSED_FIRST_RESPONSE', + value: 'sla_missed_first_response', + }, + { + label: 'PROFILE_SETTINGS.FORM.NOTIFICATIONS.TYPES.SLA_MISSED_NEXT_RESPONSE', + value: 'sla_missed_next_response', + }, + { + label: 'PROFILE_SETTINGS.FORM.NOTIFICATIONS.TYPES.SLA_MISSED_RESOLUTION', + value: 'sla_missed_resolution', + }, +]; + +export const ALERT_EVENTS = [ + { + value: 'none', + label: 'none', + }, + { + value: 'mine', + label: 'mine', + }, + { + value: 'all', + label: 'all', + }, +]; diff --git a/app/javascript/dashboard/routes/dashboard/settings/personal/personal.routes.js b/app/javascript/dashboard/routes/dashboard/settings/personal/personal.routes.js new file mode 100644 index 000000000..66aaf0a11 --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/personal/personal.routes.js @@ -0,0 +1,20 @@ +import { frontendURL } from 'dashboard/helper/URLHelper'; + +const Index = () => import('./Index.vue'); + +export default { + routes: [ + { + path: frontendURL('accounts/:accountId/personal'), + name: 'personal_settings', + roles: ['administrator', 'agent'], + component: Index, + props: { + headerTitle: 'PROFILE_SETTINGS.TITLE', + icon: 'edit', + showNewButton: false, + showSidemenuIcon: false, + }, + }, + ], +}; diff --git a/app/javascript/dashboard/routes/dashboard/settings/profile/ChangePassword.vue b/app/javascript/dashboard/routes/dashboard/settings/profile/ChangePassword.vue index 8eb7201ae..99cee0325 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/profile/ChangePassword.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/profile/ChangePassword.vue @@ -74,6 +74,7 @@ import { required, minLength } from 'vuelidate/lib/validators'; import { mapGetters } from 'vuex'; import alertMixin from 'shared/mixins/alertMixin'; +import { parseAPIErrorResponse } from 'dashboard/store/utils/api'; export default { mixins: [alertMixin], @@ -125,10 +126,9 @@ export default { }); this.errorMessage = this.$t('PROFILE_SETTINGS.PASSWORD_UPDATE_SUCCESS'); } catch (error) { - this.errorMessage = this.$t('RESET_PASSWORD.API.ERROR_MESSAGE'); - if (error?.response?.data?.message) { - this.errorMessage = error.response.data.message; - } + this.errorMessage = + parseAPIErrorResponse(error) || + this.$t('RESET_PASSWORD.API.ERROR_MESSAGE'); } finally { this.isPasswordChanging = false; this.showAlert(this.errorMessage); diff --git a/app/javascript/dashboard/routes/dashboard/settings/profile/NotificationSettings.vue b/app/javascript/dashboard/routes/dashboard/settings/profile/NotificationSettings.vue index 7214d0deb..cdbc40ec4 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/profile/NotificationSettings.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/profile/NotificationSettings.vue @@ -236,6 +236,54 @@ }} +
+ + +
+
+ + +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
@@ -367,6 +466,7 @@ import { requestPushPermissions, verifyServiceWorkerExistence, } from '../../../../helper/pushHelper'; +import { FEATURE_FLAGS } from 'dashboard/featureFlags'; export default { mixins: [alertMixin, configMixin, uiSettingsMixin], @@ -393,13 +493,18 @@ export default { }, computed: { ...mapGetters({ + accountId: 'getCurrentAccountId', emailFlags: 'userNotificationSettings/getSelectedEmailFlags', pushFlags: 'userNotificationSettings/getSelectedPushFlags', uiSettings: 'getUISettings', + isFeatureEnabledonAccount: 'accounts/isFeatureEnabledonAccount', }), hasPushAPISupport() { return !!('Notification' in window); }, + isSLAEnabled() { + return this.isFeatureEnabledonAccount(this.accountId, FEATURE_FLAGS.SLA); + }, }, watch: { emailFlags(value) { diff --git a/app/javascript/dashboard/routes/dashboard/settings/reports/BotReports.vue b/app/javascript/dashboard/routes/dashboard/settings/reports/BotReports.vue new file mode 100644 index 000000000..a75051aaf --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/reports/BotReports.vue @@ -0,0 +1,106 @@ + + diff --git a/app/javascript/dashboard/routes/dashboard/settings/reports/ReportContainer.vue b/app/javascript/dashboard/routes/dashboard/settings/reports/ReportContainer.vue index c601b4277..273f814b7 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/reports/ReportContainer.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/reports/ReportContainer.vue @@ -7,7 +7,7 @@ :key="metric.KEY" class="p-4 rounded-md mb-3" > - +
+
+ + + {{ $t('SLA_REPORTS.DOWNLOAD_SLA_REPORTS') }} + +
+ + +
+
+ + diff --git a/app/javascript/dashboard/routes/dashboard/settings/reports/components/BotMetrics.vue b/app/javascript/dashboard/routes/dashboard/settings/reports/components/BotMetrics.vue new file mode 100644 index 000000000..46227f945 --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/reports/components/BotMetrics.vue @@ -0,0 +1,68 @@ + + + diff --git a/app/javascript/dashboard/routes/dashboard/settings/reports/components/ChartElements/ChartStats.vue b/app/javascript/dashboard/routes/dashboard/settings/reports/components/ChartElements/ChartStats.vue index 94fe1359d..4faf4de2b 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/reports/components/ChartElements/ChartStats.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/reports/components/ChartElements/ChartStats.vue @@ -1,6 +1,8 @@