From 904673020690e017307829229e03d4ec5369bc5b Mon Sep 17 00:00:00 2001 From: Muhsin Keloth Date: Tue, 18 Jun 2024 10:38:06 +0530 Subject: [PATCH 01/85] feat: FCM HTTP v1 API changes (#9629) Fixes https://linear.app/chatwoot/issue/CW-3210/legacy-firebase-changes --- .../super_admin/app_configs_controller.rb | 2 +- app/services/notification/fcm_service.rb | 40 ++++++++++ .../notification/push_notification_service.rb | 75 +++++++++++++++---- config/installation_config.yml | 13 ++++ lib/chatwoot_hub.rb | 4 +- .../services/notification/fcm_service_spec.rb | 70 +++++++++++++++++ .../push_notification_service_spec.rb | 16 ++-- 7 files changed, 196 insertions(+), 24 deletions(-) create mode 100644 app/services/notification/fcm_service.rb create mode 100644 spec/services/notification/fcm_service_spec.rb diff --git a/app/controllers/super_admin/app_configs_controller.rb b/app/controllers/super_admin/app_configs_controller.rb index 9d9494fc4..b8f3bd9a9 100644 --- a/app/controllers/super_admin/app_configs_controller.rb +++ b/app/controllers/super_admin/app_configs_controller.rb @@ -40,7 +40,7 @@ class SuperAdmin::AppConfigsController < SuperAdmin::ApplicationController when 'email' ['MAILER_INBOUND_EMAIL_DOMAIN'] else - %w[ENABLE_ACCOUNT_SIGNUP] + %w[ENABLE_ACCOUNT_SIGNUP FIREBASE_PROJECT_ID FIREBASE_CREDENTIALS] end end end diff --git a/app/services/notification/fcm_service.rb b/app/services/notification/fcm_service.rb new file mode 100644 index 000000000..fc0ed6b05 --- /dev/null +++ b/app/services/notification/fcm_service.rb @@ -0,0 +1,40 @@ +class Notification::FcmService + SCOPES = ['https://www.googleapis.com/auth/firebase.messaging'].freeze + + def initialize(project_id, credentials) + @project_id = project_id + @credentials = credentials + @token_info = nil + end + + def fcm_client + FCM.new(current_token, credentials_path, @project_id) + end + + private + + def current_token + @token_info = generate_token if @token_info.nil? || token_expired? + @token_info[:token] + end + + def token_expired? + Time.zone.now >= @token_info[:expires_at] + end + + def generate_token + authorizer = Google::Auth::ServiceAccountCredentials.make_creds( + json_key_io: credentials_path, + scope: SCOPES + ) + token = authorizer.fetch_access_token! + { + token: token['access_token'], + expires_at: Time.zone.now + token['expires_in'].to_i + } + end + + def credentials_path + StringIO.new(@credentials) + end +end diff --git a/app/services/notification/push_notification_service.rb b/app/services/notification/push_notification_service.rb index b7fc231fa..3c7ed52e7 100644 --- a/app/services/notification/push_notification_service.rb +++ b/app/services/notification/push_notification_service.rb @@ -70,36 +70,81 @@ class Notification::PushNotificationService end def send_fcm_push(subscription) - return unless ENV['FCM_SERVER_KEY'] + return unless firebase_credentials_present? return unless subscription.fcm? - fcm = FCM.new(ENV.fetch('FCM_SERVER_KEY', nil)) - response = fcm.send([subscription.subscription_attributes['push_token']], fcm_options) + fcm_service = Notification::FcmService.new( + GlobalConfigService.load('FIREBASE_PROJECT_ID', nil), GlobalConfigService.load('FIREBASE_CREDENTIALS', nil) + ) + fcm = fcm_service.fcm_client + response = fcm.send_v1(fcm_options(subscription)) remove_subscription_if_error(subscription, response) end def send_push_via_chatwoot_hub(subscription) - return if ENV['FCM_SERVER_KEY'] - return unless ActiveModel::Type::Boolean.new.cast(ENV.fetch('ENABLE_PUSH_RELAY_SERVER', true)) + return if firebase_credentials_present? + return unless chatwoot_hub_enabled? return unless subscription.fcm? - ChatwootHub.send_browser_push([subscription.subscription_attributes['push_token']], fcm_options) + ChatwootHub.send_push(fcm_options(subscription)) + end + + def firebase_credentials_present? + GlobalConfigService.load('FIREBASE_PROJECT_ID', nil) && GlobalConfigService.load('FIREBASE_CREDENTIALS', nil) + end + + def chatwoot_hub_enabled? + ActiveModel::Type::Boolean.new.cast(ENV.fetch('ENABLE_PUSH_RELAY_SERVER', true)) end def remove_subscription_if_error(subscription, response) subscription.destroy! if JSON.parse(response[:body])['results']&.first&.keys&.include?('error') end - def fcm_options + def fcm_options(subscription) { - notification: { - title: notification.push_message_title, - body: notification.push_message_body, - sound: 'default' - }, - android: { priority: 'high' }, - data: { notification: notification.fcm_push_data.to_json }, - collapse_key: "chatwoot_#{notification.primary_actor_type.downcase}_#{notification.primary_actor_id}" + 'token': subscription.subscription_attributes['push_token'], + 'data': fcm_data, + 'notification': fcm_notification, + 'android': fcm_android_options, + 'apns': fcm_apns_options, + 'fcm_options': { + analytics_label: 'Label' + } + } + end + + def fcm_data + { + payload: { + data: { + notification: notification.fcm_push_data + } + }.to_json + } + end + + def fcm_notification + { + title: notification.push_message_title, + body: notification.push_message_body + } + end + + def fcm_android_options + { + priority: 'high' + } + end + + def fcm_apns_options + { + payload: { + aps: { + sound: 'default', + category: Time.zone.now.to_i.to_s + } + } } end end diff --git a/config/installation_config.yml b/config/installation_config.yml index 419a63352..5784c3b6e 100644 --- a/config/installation_config.yml +++ b/config/installation_config.yml @@ -204,3 +204,16 @@ locked: false description: 'Disable rendering profile update page for users' ## ------ End of Configs added for enterprise clients ------ ## + +## ------ Configs added for FCM v1 notifications ------ ## +- name: FIREBASE_PROJECT_ID + display_title: 'Firebase Project ID' + value: + locked: false + description: 'Firebase project ID' +- name: FIREBASE_CREDENTIALS + display_title: 'Firebase Credentials' + value: + locked: false + description: 'Contents on your firebase credentials json file' +## ------ End of Configs added for FCM v1 notifications ------ ## diff --git a/lib/chatwoot_hub.rb b/lib/chatwoot_hub.rb index 6d28b10fa..22addcacb 100644 --- a/lib/chatwoot_hub.rb +++ b/lib/chatwoot_hub.rb @@ -77,8 +77,8 @@ class ChatwootHub ChatwootExceptionTracker.new(e).capture_exception end - def self.send_browser_push(fcm_token_list, fcm_options) - info = { fcm_token_list: fcm_token_list, fcm_options: fcm_options } + def self.send_push(fcm_options) + info = { fcm_options: fcm_options } RestClient.post(PUSH_NOTIFICATION_URL, info.merge(instance_config).to_json, { content_type: :json, accept: :json }) rescue *ExceptionList::REST_CLIENT_EXCEPTIONS => e Rails.logger.error "Exception: #{e.message}" diff --git a/spec/services/notification/fcm_service_spec.rb b/spec/services/notification/fcm_service_spec.rb new file mode 100644 index 000000000..4da9c2bb3 --- /dev/null +++ b/spec/services/notification/fcm_service_spec.rb @@ -0,0 +1,70 @@ +require 'rails_helper' + +describe Notification::FcmService do + let(:project_id) { 'test_project_id' } + let(:credentials) { '{ "type": "service_account", "project_id": "test_project_id" }' } + let(:fcm_service) { described_class.new(project_id, credentials) } + let(:fcm_double) { instance_double(FCM) } + let(:token_info) { { token: 'test_token', expires_at: 1.hour.from_now } } + let(:creds_double) do + instance_double(Google::Auth::ServiceAccountCredentials, fetch_access_token!: { 'access_token' => 'test_token', 'expires_in' => 3600 }) + end + + before do + allow(FCM).to receive(:new).and_return(fcm_double) + allow(fcm_service).to receive(:generate_token).and_return(token_info) + allow(Google::Auth::ServiceAccountCredentials).to receive(:make_creds).and_return(creds_double) + end + + describe '#fcm_client' do + it 'returns an FCM client' do + expect(fcm_service.fcm_client).to eq(fcm_double) + expect(FCM).to have_received(:new).with('test_token', anything, project_id) + end + + it 'generates a new token if expired' do + allow(fcm_service).to receive(:generate_token).and_return(token_info) + allow(fcm_service).to receive(:token_expired?).and_return(true) + + expect(fcm_service.fcm_client).to eq(fcm_double) + expect(FCM).to have_received(:new).with('test_token', anything, project_id) + expect(fcm_service).to have_received(:generate_token) + end + end + + describe 'private methods' do + describe '#current_token' do + it 'returns the current token if not expired' do + fcm_service.instance_variable_set(:@token_info, token_info) + expect(fcm_service.send(:current_token)).to eq('test_token') + end + + it 'generates a new token if expired' do + expired_token_info = { token: 'expired_token', expires_at: 1.hour.ago } + fcm_service.instance_variable_set(:@token_info, expired_token_info) + allow(fcm_service).to receive(:generate_token).and_return(token_info) + + expect(fcm_service.send(:current_token)).to eq('test_token') + expect(fcm_service).to have_received(:generate_token) + end + end + + describe '#generate_token' do + it 'generates a new token' do + allow(Google::Auth::ServiceAccountCredentials).to receive(:make_creds).and_return(creds_double) + + token = fcm_service.send(:generate_token) + expect(token[:token]).to eq('test_token') + expect(token[:expires_at]).to be_within(1.second).of(Time.zone.now + 3600) + end + end + + describe '#credentials_path' do + it 'creates a StringIO with credentials' do + string_io = fcm_service.send(:credentials_path) + expect(string_io).to be_a(StringIO) + expect(string_io.read).to eq(credentials) + end + end + end +end diff --git a/spec/services/notification/push_notification_service_spec.rb b/spec/services/notification/push_notification_service_spec.rb index 17fc5f6f3..bb8ed5c66 100644 --- a/spec/services/notification/push_notification_service_spec.rb +++ b/spec/services/notification/push_notification_service_spec.rb @@ -4,12 +4,15 @@ describe Notification::PushNotificationService do let!(:account) { create(:account) } let!(:user) { create(:user, account: account) } let!(:notification) { create(:notification, user: user, account: user.accounts.first) } - let(:fcm_double) { double } + let(:fcm_double) { instance_double(FCM) } + let(:fcm_service_double) { instance_double(Notification::FcmService, fcm_client: fcm_double) } before do allow(WebPush).to receive(:payload_send).and_return(true) - allow(FCM).to receive(:new).and_return(fcm_double) - allow(fcm_double).to receive(:send).and_return({ body: { 'results': [] }.to_json }) + allow(Notification::FcmService).to receive(:new).and_return(fcm_service_double) + allow(fcm_double).to receive(:send_v1).and_return({ body: { 'results': [] }.to_json }) + allow(GlobalConfigService).to receive(:load).with('FIREBASE_PROJECT_ID', nil).and_return('test_project_id') + allow(GlobalConfigService).to receive(:load).with('FIREBASE_CREDENTIALS', nil).and_return('test_credentials') end describe '#perform' do @@ -19,16 +22,17 @@ describe Notification::PushNotificationService do described_class.new(notification: notification).perform expect(WebPush).to have_received(:payload_send) - expect(FCM).not_to have_received(:new) + expect(Notification::FcmService).not_to have_received(:new) end end it 'sends a fcm notification for firebase subscription' do - with_modified_env FCM_SERVER_KEY: 'test', ENABLE_PUSH_RELAY_SERVER: 'false' do + with_modified_env ENABLE_PUSH_RELAY_SERVER: 'false' do create(:notification_subscription, user: notification.user, subscription_type: 'fcm') described_class.new(notification: notification).perform - expect(FCM).to have_received(:new) + expect(Notification::FcmService).to have_received(:new) + expect(fcm_double).to have_received(:send_v1) expect(WebPush).not_to have_received(:payload_send) end end From c5979c4575d273c87675be9535d3d4ab264cc5ce Mon Sep 17 00:00:00 2001 From: Ali Behnamfard Date: Tue, 18 Jun 2024 09:06:37 +0330 Subject: [PATCH 02/85] feat: Add smtp timeout options (#9613) - Added two Actionmailer's option (open_timeout & read_timeout) to environment variables. --- .env.example | 2 ++ config/initializers/mailer.rb | 2 ++ 2 files changed, 4 insertions(+) diff --git a/.env.example b/.env.example index 1b57141f0..befcde463 100644 --- a/.env.example +++ b/.env.example @@ -85,6 +85,8 @@ SMTP_OPENSSL_VERIFY_MODE=peer # Comment out the following environment variables if required by your SMTP server # SMTP_TLS= # SMTP_SSL= +# SMTP_OPEN_TIMEOUT +# SMTP_READ_TIMEOUT # Mail Incoming # This is the domain set for the reply emails when conversation continuity is enabled diff --git a/config/initializers/mailer.rb b/config/initializers/mailer.rb index 8e5053872..a0e5d7b73 100644 --- a/config/initializers/mailer.rb +++ b/config/initializers/mailer.rb @@ -24,6 +24,8 @@ Rails.application.configure do smtp_settings[:openssl_verify_mode] = ENV['SMTP_OPENSSL_VERIFY_MODE'] if ENV['SMTP_OPENSSL_VERIFY_MODE'].present? smtp_settings[:ssl] = ActiveModel::Type::Boolean.new.cast(ENV.fetch('SMTP_SSL', true)) if ENV['SMTP_SSL'] smtp_settings[:tls] = ActiveModel::Type::Boolean.new.cast(ENV.fetch('SMTP_TLS', true)) if ENV['SMTP_TLS'] + smtp_settings[:open_timeout] = ENV['SMTP_OPEN_TIMEOUT'].to_i if ENV['SMTP_OPEN_TIMEOUT'].present? + smtp_settings[:read_timeout] = ENV['SMTP_READ_TIMEOUT'].to_i if ENV['SMTP_READ_TIMEOUT'].present? config.action_mailer.delivery_method = :smtp unless Rails.env.test? config.action_mailer.smtp_settings = smtp_settings From 6b0a5bea43632a20199b8009122410ca012be83d Mon Sep 17 00:00:00 2001 From: Sojan Date: Mon, 17 Jun 2024 23:58:50 -0700 Subject: [PATCH 03/85] Bump version to 3.10.0 --- config/app.yml | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/config/app.yml b/config/app.yml index 592467ee1..251613d8e 100644 --- a/config/app.yml +++ b/config/app.yml @@ -1,5 +1,5 @@ shared: &shared - version: '3.9.0' + version: '3.10.0' development: <<: *shared diff --git a/package.json b/package.json index 8f1140507..8700c998e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@chatwoot/chatwoot", - "version": "3.9.0", + "version": "3.10.0", "license": "MIT", "scripts": { "eslint": "eslint app/**/*.{js,vue}", From 3dee50d2b1319194a1eb3cc860609011d1e5c857 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 18 Jun 2024 12:39:27 -0700 Subject: [PATCH 04/85] chore(deps): bump ws from 6.2.2 to 6.2.3 (#9650) Bumps [ws](https://github.com/websockets/ws) from 6.2.2 to 6.2.3.
Release notes

Sourced from ws's releases.

6.2.3

Bug fixes

  • Backported e55e5106 to the 6.x release line (eeb76d31).
Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=ws&package-manager=npm_and_yarn&previous-version=6.2.2&new-version=6.2.3)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/chatwoot/chatwoot/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- yarn.lock | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/yarn.lock b/yarn.lock index 437e6c544..a03c27a0f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -21315,21 +21315,16 @@ write-file-atomic@^4.0.2: signal-exit "^3.0.7" ws@^6.2.1: - version "6.2.2" - resolved "https://registry.yarnpkg.com/ws/-/ws-6.2.2.tgz#dd5cdbd57a9979916097652d78f1cc5faea0c32e" - integrity sha512-zmhltoSR8u1cnDsD43TX59mzoMZsLKqUweyYBAIvTngR3shc0W6aOZylZmq/7hqyVxPdi+5Ud2QInblgyE72fw== + version "6.2.3" + resolved "https://registry.yarnpkg.com/ws/-/ws-6.2.3.tgz#ccc96e4add5fd6fedbc491903075c85c5a11d9ee" + integrity sha512-jmTjYU0j60B+vHey6TfR3Z7RD61z/hmxBS3VMSGIrroOWXQEneK1zNuotOUrGyBHQj0yrpsLHPWtigEFd13ndA== dependencies: async-limiter "~1.0.0" -ws@^8.11.0: - version "8.14.2" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.14.2.tgz#6c249a806eb2db7a20d26d51e7709eab7b2e6c7f" - integrity sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g== - -ws@^8.2.3: - version "8.13.0" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.13.0.tgz#9a9fb92f93cf41512a0735c8f4dd09b8a1211cd0" - integrity sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA== +ws@^8.11.0, ws@^8.2.3: + version "8.17.1" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.17.1.tgz#9293da530bb548febc95371d90f9c878727d919b" + integrity sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ== x-default-browser@^0.4.0: version "0.4.0" From ef606204a2145fc930d57579d7f58262c31a52fc Mon Sep 17 00:00:00 2001 From: Pranav Date: Tue, 18 Jun 2024 21:10:41 -0700 Subject: [PATCH 05/85] fix: Temporarily fix text/plain email rendering (#9653) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is hacky fix for plain text email rendering. The issue happens only for the text/plain only emails. If there was an HTML component, then the rendering works fine. **How was this tested?** Mac Email client allows you to send text/plain emails. I've sent one to myself and imported it on Chatwoot. I've also verified that the email contains only text/plain part. Sample rendered email below. Screenshot 2024-06-18 at 8 15 10 PM Fixes https://github.com/chatwoot/chatwoot/issues/9649 Fixes https://github.com/chatwoot/chatwoot/issues/9480 --- .../components/widgets/conversation/Message.vue | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/app/javascript/dashboard/components/widgets/conversation/Message.vue b/app/javascript/dashboard/components/widgets/conversation/Message.vue index fc80b871c..63ab5643f 100644 --- a/app/javascript/dashboard/components/widgets/conversation/Message.vue +++ b/app/javascript/dashboard/components/widgets/conversation/Message.vue @@ -257,7 +257,16 @@ export default { html_content: { full: fullHTMLContent } = {}, text_content: { full: fullTextContent } = {}, } = this.contentAttributes.email || {}; - return fullHTMLContent || fullTextContent || ''; + + if (fullHTMLContent) { + return fullHTMLContent; + } + + if (fullTextContent) { + return fullTextContent.replace(/\n/g, '
'); + } + + return ''; }, displayQuotedButton() { if (this.emailMessageContent.includes(' Date: Thu, 20 Jun 2024 15:36:08 -0700 Subject: [PATCH 06/85] fix: [Snyk] Security upgrade rspec-rails from 6.1.2 to 6.1.3 (#9658) - security upgrade for gems --- Gemfile | 2 +- Gemfile.lock | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Gemfile b/Gemfile index 302094e8f..43f255074 100644 --- a/Gemfile +++ b/Gemfile @@ -228,7 +228,7 @@ group :development, :test do gem 'mock_redis' gem 'pry-rails' gem 'rspec_junit_formatter' - gem 'rspec-rails', '>= 6.0.3' + gem 'rspec-rails', '>= 6.1.3' gem 'rubocop', require: false gem 'rubocop-performance', require: false gem 'rubocop-rails', require: false diff --git a/Gemfile.lock b/Gemfile.lock index 323847645..bbb1aa063 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -223,7 +223,7 @@ GEM http (>= 3.0) ruby2_keywords email_reply_trimmer (0.1.13) - erubi (1.12.0) + erubi (1.13.0) et-orbi (1.2.7) tzinfo execjs (2.8.1) @@ -460,7 +460,7 @@ GEM mini_magick (4.12.0) mini_mime (1.1.5) mini_portile2 (2.8.7) - minitest (5.23.1) + minitest (5.24.0) mock_redis (0.36.0) ruby2_keywords msgpack (1.7.0) @@ -489,14 +489,14 @@ GEM newrelic_rpm (9.6.0) base64 nio4r (2.7.3) - nokogiri (1.16.5) + nokogiri (1.16.6) mini_portile2 (~> 2.8.2) racc (~> 1.4) - nokogiri (1.16.5-arm64-darwin) + nokogiri (1.16.6-arm64-darwin) racc (~> 1.4) - nokogiri (1.16.5-x86_64-darwin) + nokogiri (1.16.6-x86_64-darwin) racc (~> 1.4) - nokogiri (1.16.5-x86_64-linux) + nokogiri (1.16.6-x86_64-linux) racc (~> 1.4) oauth (1.1.0) oauth-tty (~> 1.0, >= 1.0.1) @@ -631,13 +631,13 @@ GEM strscan (>= 3.0.9) rspec-core (3.13.0) rspec-support (~> 3.13.0) - rspec-expectations (3.13.0) + rspec-expectations (3.13.1) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) rspec-mocks (3.13.1) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) - rspec-rails (6.1.2) + rspec-rails (6.1.3) actionpack (>= 6.1) activesupport (>= 6.1) railties (>= 6.1) @@ -819,7 +819,7 @@ GEM working_hours (1.4.1) activesupport (>= 3.2) tzinfo - zeitwerk (2.6.15) + zeitwerk (2.6.16) PLATFORMS arm64-darwin-20 @@ -922,7 +922,7 @@ DEPENDENCIES responders (>= 3.1.1) rest-client reverse_markdown - rspec-rails (>= 6.0.3) + rspec-rails (>= 6.1.3) rspec_junit_formatter rubocop rubocop-performance From ee2844877cba441c12d80fd2c402f8771d4ec5c0 Mon Sep 17 00:00:00 2001 From: Pranav Date: Fri, 21 Jun 2024 14:58:36 -0700 Subject: [PATCH 07/85] fix: Add catch for additional webpush errors (#9662) Webpush gem throws errors such as `WebPush::ExpiredSubscription`, `WebPush::InvalidSubscription`, `WebPush::Unauthorized`. We handled only ExpiredSubscription. If the SDK threw any other errors, it would pause sending the notification to all other devices for that user. This change would update the logic to remove the expired subscription and handle the rest of the errors gracefully. Fixes https://linear.app/chatwoot/issue/CW-3399/webpushinvalidsubscription-host-fcmgoogleapiscom-nethttpnotfound-404 --- .../notification/push_notification_service.rb | 29 ++++++--- spec/factories/notification_subscriptions.rb | 8 +++ .../push_notification_service_spec.rb | 65 +++++++++++++------ 3 files changed, 74 insertions(+), 28 deletions(-) diff --git a/app/services/notification/push_notification_service.rb b/app/services/notification/push_notification_service.rb index 3c7ed52e7..263800d4a 100644 --- a/app/services/notification/push_notification_service.rb +++ b/app/services/notification/push_notification_service.rb @@ -42,14 +42,12 @@ class Notification::PushNotificationService app_account_conversation_url(account_id: conversation.account_id, id: conversation.display_id) end - def send_browser_push?(subscription) + def can_send_browser_push?(subscription) VapidService.public_key && subscription.browser_push? end - def send_browser_push(subscription) - return unless send_browser_push?(subscription) - - WebPush.payload_send( + def browser_push_payload(subscription) + { message: JSON.generate(push_message), endpoint: subscription.subscription_attributes['endpoint'], p256dh: subscription.subscription_attributes['p256dh'], @@ -62,11 +60,22 @@ class Notification::PushNotificationService ssl_timeout: 5, open_timeout: 5, read_timeout: 5 - ) - rescue WebPush::ExpiredSubscription + } + end + + def send_browser_push(subscription) + return unless can_send_browser_push?(subscription) + + WebPush.payload_send(browser_push_payload(subscription)) + Rails.logger.info("Browser push sent to #{user.email} with title #{push_message[:title]}") + rescue WebPush::ExpiredSubscription, WebPush::InvalidSubscription, WebPush::Unauthorized => e + Rails.logger.info "WebPush subscription expired: #{e.message}" subscription.destroy! rescue Errno::ECONNRESET, Net::OpenTimeout, Net::ReadTimeout => e Rails.logger.error "WebPush operation error: #{e.message}" + rescue StandardError => e + ChatwootExceptionTracker.new(e, account: notification.account).capture_exception + true end def send_fcm_push(subscription) @@ -98,7 +107,11 @@ class Notification::PushNotificationService end def remove_subscription_if_error(subscription, response) - subscription.destroy! if JSON.parse(response[:body])['results']&.first&.keys&.include?('error') + if JSON.parse(response[:body])['results']&.first&.keys&.include?('error') + subscription.destroy! + else + Rails.logger.info("FCM push sent to #{user.email} with title #{push_message[:title]}") + end end def fcm_options(subscription) diff --git a/spec/factories/notification_subscriptions.rb b/spec/factories/notification_subscriptions.rb index 581c198a6..c886a12c0 100644 --- a/spec/factories/notification_subscriptions.rb +++ b/spec/factories/notification_subscriptions.rb @@ -6,5 +6,13 @@ FactoryBot.define do identifier { 'test' } subscription_type { 'browser_push' } subscription_attributes { { endpoint: 'test', auth: 'test' } } + + trait :browser_push do + subscription_type { 'browser_push' } + end + + trait :fcm do + subscription_type { 'fcm' } + end end end diff --git a/spec/services/notification/push_notification_service_spec.rb b/spec/services/notification/push_notification_service_spec.rb index bb8ed5c66..bba912715 100644 --- a/spec/services/notification/push_notification_service_spec.rb +++ b/spec/services/notification/push_notification_service_spec.rb @@ -7,33 +7,58 @@ describe Notification::PushNotificationService do let(:fcm_double) { instance_double(FCM) } let(:fcm_service_double) { instance_double(Notification::FcmService, fcm_client: fcm_double) } - before do - allow(WebPush).to receive(:payload_send).and_return(true) - allow(Notification::FcmService).to receive(:new).and_return(fcm_service_double) - allow(fcm_double).to receive(:send_v1).and_return({ body: { 'results': [] }.to_json }) - allow(GlobalConfigService).to receive(:load).with('FIREBASE_PROJECT_ID', nil).and_return('test_project_id') - allow(GlobalConfigService).to receive(:load).with('FIREBASE_CREDENTIALS', nil).and_return('test_credentials') - end - describe '#perform' do - it 'sends webpush notifications for webpush subscription' do - with_modified_env VAPID_PUBLIC_KEY: 'test' do - create(:notification_subscription, user: notification.user) + context 'when the push server returns success' do + before do + allow(WebPush).to receive(:payload_send).and_return(true) + allow(Rails.logger).to receive(:info) + allow(Notification::FcmService).to receive(:new).and_return(fcm_service_double) + allow(fcm_double).to receive(:send_v1).and_return({ body: { 'results': [] }.to_json }) + allow(GlobalConfigService).to receive(:load).with('FIREBASE_PROJECT_ID', nil).and_return('test_project_id') + allow(GlobalConfigService).to receive(:load).with('FIREBASE_CREDENTIALS', nil).and_return('test_credentials') + end - described_class.new(notification: notification).perform - expect(WebPush).to have_received(:payload_send) - expect(Notification::FcmService).not_to have_received(:new) + it 'sends webpush notifications for webpush subscription' do + with_modified_env VAPID_PUBLIC_KEY: 'test' do + create(:notification_subscription, user: notification.user) + + described_class.new(notification: notification).perform + expect(WebPush).to have_received(:payload_send) + expect(Notification::FcmService).not_to have_received(:new) + expect(Rails.logger).to have_received(:info).with("Browser push sent to #{user.email} with title #{notification.push_message_title}") + end + end + + it 'sends a fcm notification for firebase subscription' do + with_modified_env ENABLE_PUSH_RELAY_SERVER: 'false' do + create(:notification_subscription, user: notification.user, subscription_type: 'fcm') + + described_class.new(notification: notification).perform + expect(Notification::FcmService).to have_received(:new) + expect(fcm_double).to have_received(:send_v1) + expect(WebPush).not_to have_received(:payload_send) + expect(Rails.logger).to have_received(:info).with("FCM push sent to #{user.email} with title #{notification.push_message_title}") + end end end + end - it 'sends a fcm notification for firebase subscription' do - with_modified_env ENABLE_PUSH_RELAY_SERVER: 'false' do - create(:notification_subscription, user: notification.user, subscription_type: 'fcm') + context 'when the push server returns error' do + it 'sends webpush notifications for webpush subscription' do + with_modified_env VAPID_PUBLIC_KEY: 'test' do + mock_response = instance_double(Net::HTTPResponse, body: 'Subscription is invalid') + mock_host = 'fcm.googleapis.com' + + allow(WebPush).to receive(:payload_send).and_raise(WebPush::InvalidSubscription.new(mock_response, mock_host)) + allow(Rails.logger).to receive(:info) + + create(:notification_subscription, :browser_push, user: notification.user) + + expect(Rails.logger).to receive(:info) do |message| + expect(message).to include('WebPush subscription expired:') + end described_class.new(notification: notification).perform - expect(Notification::FcmService).to have_received(:new) - expect(fcm_double).to have_received(:send_v1) - expect(WebPush).not_to have_received(:payload_send) end end end From e72a34361256531761a813bcbb4aee068d9107ca Mon Sep 17 00:00:00 2001 From: Sojan Date: Fri, 21 Jun 2024 15:06:28 -0700 Subject: [PATCH 08/85] : Bump version to 3.10.1 --- config/app.yml | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/config/app.yml b/config/app.yml index 251613d8e..8a133ee53 100644 --- a/config/app.yml +++ b/config/app.yml @@ -1,5 +1,5 @@ shared: &shared - version: '3.10.0' + version: '3.10.1' development: <<: *shared diff --git a/package.json b/package.json index 8700c998e..6b017503e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@chatwoot/chatwoot", - "version": "3.10.0", + "version": "3.10.1", "license": "MIT", "scripts": { "eslint": "eslint app/**/*.{js,vue}", From f7f687ce5374525652f4186095aad73df7dad356 Mon Sep 17 00:00:00 2001 From: Pranav Date: Fri, 21 Jun 2024 17:28:48 -0700 Subject: [PATCH 09/85] fix: Update notification payload (#9666) - Fix notification payload to avoid argument error. --- app/services/notification/push_notification_service.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/services/notification/push_notification_service.rb b/app/services/notification/push_notification_service.rb index 263800d4a..9878107c1 100644 --- a/app/services/notification/push_notification_service.rb +++ b/app/services/notification/push_notification_service.rb @@ -66,7 +66,7 @@ class Notification::PushNotificationService def send_browser_push(subscription) return unless can_send_browser_push?(subscription) - WebPush.payload_send(browser_push_payload(subscription)) + WebPush.payload_send(**browser_push_payload(subscription)) Rails.logger.info("Browser push sent to #{user.email} with title #{push_message[:title]}") rescue WebPush::ExpiredSubscription, WebPush::InvalidSubscription, WebPush::Unauthorized => e Rails.logger.info "WebPush subscription expired: #{e.message}" From 96f4f50d2deff8a09f312e860bf891ad4d454c36 Mon Sep 17 00:00:00 2001 From: Clairton Rodrigo Heinzen Date: Wed, 26 Jun 2024 16:40:36 -0300 Subject: [PATCH 10/85] feat: Add the ability to un-assign teams using automation (#9668) Co-authored-by: Pranav --- .../dashboard/helper/automationHelper.js | 6 +-- app/models/concerns/json_schema_validator.rb | 1 - app/services/action_service.rb | 6 ++- spec/factories/conversations.rb | 12 +++++ spec/services/action_service_spec.rb | 44 +++++++++++++++++-- 5 files changed, 60 insertions(+), 9 deletions(-) diff --git a/app/javascript/dashboard/helper/automationHelper.js b/app/javascript/dashboard/helper/automationHelper.js index 665e8faf6..33e920edc 100644 --- a/app/javascript/dashboard/helper/automationHelper.js +++ b/app/javascript/dashboard/helper/automationHelper.js @@ -121,7 +121,7 @@ export const generateConditionOptions = (options, key = 'id') => { }; // Add the "None" option to the agent list -export const agentList = agents => [ +export const addNoneToList = agents => [ { id: 'nil', name: 'None', @@ -137,8 +137,8 @@ export const getActionOptions = ({ type, }) => { const actionsMap = { - assign_agent: agentList(agents), - assign_team: teams, + assign_agent: addNoneToList(agents), + assign_team: addNoneToList(teams), send_email_to_team: teams, add_label: generateConditionOptions(labels, 'title'), remove_label: generateConditionOptions(labels, 'title'), diff --git a/app/models/concerns/json_schema_validator.rb b/app/models/concerns/json_schema_validator.rb index 4ae94df12..808564be9 100644 --- a/app/models/concerns/json_schema_validator.rb +++ b/app/models/concerns/json_schema_validator.rb @@ -48,7 +48,6 @@ class JsonSchemaValidator < ActiveModel::Validator # Add validation errors to the record with a formatted statement validation_errors.each do |error| - # byebug format_and_append_error(error, record) end end diff --git a/app/services/action_service.rb b/app/services/action_service.rb index 79f1234a0..4f33a302b 100644 --- a/app/services/action_service.rb +++ b/app/services/action_service.rb @@ -50,7 +50,11 @@ class ActionService end def assign_team(team_ids = []) - return unassign_team if team_ids[0]&.zero? + # FIXME: The explicit checks for zero or nil (string) is bad. Move + # this to a separate unassign action. + should_unassign = team_ids.blank? || %w[nil 0].include?(team_ids[0].to_s) + return @conversation.update!(team_id: nil) if should_unassign + # check if team belongs to account only if team_id is present # if team_id is nil, then it means that the team is being unassigned return unless !team_ids[0].nil? && team_belongs_to_account?(team_ids) diff --git a/spec/factories/conversations.rb b/spec/factories/conversations.rb index 6f2bc34ef..c552e6c30 100644 --- a/spec/factories/conversations.rb +++ b/spec/factories/conversations.rb @@ -16,5 +16,17 @@ FactoryBot.define do conversation.contact ||= create(:contact, :with_email, account: conversation.account) conversation.contact_inbox ||= create(:contact_inbox, contact: conversation.contact, inbox: conversation.inbox) end + + trait :with_team do + after(:build) do |conversation| + conversation.team ||= create(:team, account: conversation.account) + end + end + + trait :with_assignee do + after(:build) do |conversation| + conversation.assignee ||= create(:user, account: conversation.account, role: :agent) + end + end end end diff --git a/spec/services/action_service_spec.rb b/spec/services/action_service_spec.rb index bdc9c88ff..c28761844 100644 --- a/spec/services/action_service_spec.rb +++ b/spec/services/action_service_spec.rb @@ -31,18 +31,54 @@ describe ActionService do describe '#assign_agent' do let(:agent) { create(:user, account: account, role: :agent) } - let(:conversation) { create(:conversation, account: account) } let(:inbox_member) { create(:inbox_member, inbox: conversation.inbox, user: agent) } + let(:conversation) { create(:conversation, :with_assignee, account: account) } let(:action_service) { described_class.new(conversation) } it 'unassigns the conversation if agent id is nil' do action_service.assign_agent(['nil']) expect(conversation.reload.assignee).to be_nil end + end - it 'unassigns the team if team_id is nil' do - action_service.assign_team([nil]) - expect(conversation.reload.team).to be_nil + describe '#assign_team' do + let(:agent) { create(:user, account: account, role: :agent) } + let(:inbox_member) { create(:inbox_member, inbox: conversation.inbox, user: agent) } + let(:team) { create(:team, name: 'ConversationTeam', account: account) } + let(:conversation) { create(:conversation, :with_team, account: account) } + let(:action_service) { described_class.new(conversation) } + + context 'when team_id is not present' do + it 'unassign the if team_id is "nil"' do + expect do + action_service.assign_team(['nil']) + end.not_to raise_error + expect(conversation.reload.team).to be_nil + end + + it 'unassign the if team_id is 0' do + expect do + action_service.assign_team([0]) + end.not_to raise_error + expect(conversation.reload.team).to be_nil + end + end + + context 'when team_id is present' do + it 'assign the team if the team is part of the account' do + original_team = conversation.team + expect do + action_service.assign_team([team.id]) + end.to change { conversation.reload.team }.from(original_team) + end + + it 'does not assign the team if the team is part of the account' do + original_team = conversation.team + invalid_team_id = 999_999_999 + expect do + action_service.assign_team([invalid_team_id]) + end.not_to change { conversation.reload.team }.from(original_team) + end end end end From 73c4180e6498c44995deb5b4db26b0651d569f2c Mon Sep 17 00:00:00 2001 From: Sojan Jose Date: Wed, 26 Jun 2024 12:46:21 -0700 Subject: [PATCH 11/85] chore(snyk): Upgrade sentry-rails & sentry-sidekiq to 5.18.0 (#9686) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit

This PR was automatically created by Snyk using the credentials of a real user.


![snyk-top-banner](https://github.com/andygongea/OWASP-Benchmark/assets/818805/c518c423-16fe-447e-b67f-ad5a49b5d123) ### Snyk has created this PR to fix 1 vulnerabilities in the rubygems dependencies of this project. #### Snyk changed the following file(s): - `Gemfile` - `Gemfile.lock` #### Vulnerabilities that will be fixed with an upgrade: | | Issue | Score | :-------------------------:|:-------------------------|:------------------------- ![medium severity](https://res.cloudinary.com/snyk/image/upload/w_20,h_20/v1561977819/icon/m.png 'medium severity') | Web Cache Poisoning
[SNYK-RUBY-RACK-1061917](https://snyk.io/vuln/SNYK-RUBY-RACK-1061917) |   **616**   --- > [!IMPORTANT] > > - Check the changes in this PR to ensure they won't cause issues with your project. > - Max score is 1000. Note that the real score may have changed since the PR was raised. > - This PR was automatically created by Snyk using the credentials of a real user. --- **Note:** _You are seeing this because you or someone else with access to this repository has authorized Snyk to open fix PRs._ For more information: 🧐 [View latest project report](https://app.snyk.io/org/chatwoot/project/b7197bbd-6200-4f23-931d-c39928584360?utm_source=github&utm_medium=referral&page=fix-pr) 📜 [Customise PR templates](https://docs.snyk.io/scan-using-snyk/pull-requests/snyk-fix-pull-or-merge-requests/customize-pr-templates) 🛠 [Adjust project settings](https://app.snyk.io/org/chatwoot/project/b7197bbd-6200-4f23-931d-c39928584360?utm_source=github&utm_medium=referral&page=fix-pr/settings) 📚 [Read about Snyk's upgrade logic](https://support.snyk.io/hc/en-us/articles/360003891078-Snyk-patches-to-fix-vulnerabilities) --- **Learn how to fix vulnerabilities with free interactive lessons:** 🦉 [Learn about vulnerability in an interactive lesson of Snyk Learn.](https://learn.snyk.io/?loc=fix-pr) [//]: # 'snyk:metadata:{"customTemplate":{"variablesUsed":[],"fieldsUsed":[]},"dependencies":[{"name":"sentry-rails","from":"5.17.3","to":"5.18.0"},{"name":"sentry-sidekiq","from":"5.17.3","to":"5.18.0"}],"env":"prod","issuesToFix":[{"exploit_maturity":"Proof of Concept","id":"SNYK-RUBY-RACK-1061917","priority_score":616,"priority_score_factors":[{"type":"exploit","label":"Proof of Concept","score":107},{"type":"fixability","label":true,"score":214},{"type":"cvssScore","label":"5.9","score":295},{"type":"scoreVersion","label":"v1","score":1}],"severity":"medium","title":"Web Cache Poisoning"},{"exploit_maturity":"Proof of Concept","id":"SNYK-RUBY-RACK-1061917","priority_score":616,"priority_score_factors":[{"type":"exploit","label":"Proof of Concept","score":107},{"type":"fixability","label":true,"score":214},{"type":"cvssScore","label":"5.9","score":295},{"type":"scoreVersion","label":"v1","score":1}],"severity":"medium","title":"Web Cache Poisoning"},{"exploit_maturity":"Proof of Concept","id":"SNYK-RUBY-RACK-1061917","priority_score":616,"priority_score_factors":[{"type":"exploit","label":"Proof of Concept","score":107},{"type":"fixability","label":true,"score":214},{"type":"cvssScore","label":"5.9","score":295},{"type":"scoreVersion","label":"v1","score":1}],"severity":"medium","title":"Web Cache Poisoning"}],"prId":"a3fcec38-ff99-4d64-ae69-545ad067aff5","prPublicId":"a3fcec38-ff99-4d64-ae69-545ad067aff5","packageManager":"rubygems","priorityScoreList":[616],"projectPublicId":"b7197bbd-6200-4f23-931d-c39928584360","projectUrl":"https://app.snyk.io/org/chatwoot/project/b7197bbd-6200-4f23-931d-c39928584360?utm_source=github&utm_medium=referral&page=fix-pr","prType":"fix","templateFieldSources":{"branchName":"default","commitMessage":"default","description":"default","title":"default"},"templateVariants":["priorityScore"],"type":"auto","upgrade":["SNYK-RUBY-RACK-1061917"],"vulns":["SNYK-RUBY-RACK-1061917"],"patch":[],"isBreakingChange":false,"remediationStrategy":"vuln"}' Co-authored-by: snyk-bot Co-authored-by: Pranav --- Gemfile | 4 ++-- Gemfile.lock | 18 +++++++++--------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Gemfile b/Gemfile index 43f255074..5e1e5917f 100644 --- a/Gemfile +++ b/Gemfile @@ -111,9 +111,9 @@ gem 'elastic-apm', require: false gem 'newrelic_rpm', require: false gem 'newrelic-sidekiq-metrics', '>= 1.6.2', require: false gem 'scout_apm', require: false -gem 'sentry-rails', '>= 5.14.0', require: false +gem 'sentry-rails', '>= 5.18.0', require: false gem 'sentry-ruby', require: false -gem 'sentry-sidekiq', '>= 5.15.0', require: false +gem 'sentry-sidekiq', '>= 5.18.0', require: false ##-- background job processing --## gem 'sidekiq', '>= 7.2.4' diff --git a/Gemfile.lock b/Gemfile.lock index bbb1aa063..04a8fb534 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -150,7 +150,7 @@ GEM statsd-ruby (~> 1.1) base64 (0.2.0) bcrypt (3.1.20) - bigdecimal (3.1.7) + bigdecimal (3.1.8) bindex (0.8.1) bootsnap (1.16.0) msgpack (~> 1.2) @@ -603,7 +603,7 @@ GEM ffi (~> 1.0) redis (5.0.6) redis-client (>= 0.9.0) - redis-client (0.22.1) + redis-client (0.22.2) connection_pool redis-namespace (1.10.0) redis (>= 4) @@ -703,14 +703,14 @@ GEM activesupport (>= 4) selectize-rails (0.12.6) semantic_range (3.0.0) - sentry-rails (5.17.3) + sentry-rails (5.18.0) railties (>= 5.0) - sentry-ruby (~> 5.17.3) - sentry-ruby (5.17.3) + sentry-ruby (~> 5.18.0) + sentry-ruby (5.18.0) bigdecimal concurrent-ruby (~> 1.0, >= 1.0.2) - sentry-sidekiq (5.17.3) - sentry-ruby (~> 5.17.3) + sentry-sidekiq (5.18.0) + sentry-ruby (~> 5.18.0) sidekiq (>= 3.0) sexp_processor (4.17.0) shoulda-matchers (5.3.0) @@ -931,9 +931,9 @@ DEPENDENCIES scout_apm scss_lint seed_dump - sentry-rails (>= 5.14.0) + sentry-rails (>= 5.18.0) sentry-ruby - sentry-sidekiq (>= 5.15.0) + sentry-sidekiq (>= 5.18.0) shoulda-matchers sidekiq (>= 7.2.4) sidekiq-cron (>= 1.12.0) From b2de6843f68fdb71917b7ea3e8767cb95b1bb0bc Mon Sep 17 00:00:00 2001 From: What are you searching? Date: Thu, 27 Jun 2024 02:54:57 +0700 Subject: [PATCH 12/85] chore()Update Translate vi.yml (#9656) --- config/locales/vi.yml | 80 +++++++++++++++++++++---------------------- 1 file changed, 40 insertions(+), 40 deletions(-) diff --git a/config/locales/vi.yml b/config/locales/vi.yml index 189184d4d..8fb3e4507 100644 --- a/config/locales/vi.yml +++ b/config/locales/vi.yml @@ -31,7 +31,7 @@ vi: disposable_email: Chúng tôi không cho phép các email dùng một lần invalid_email: Bạn đã nhập một email không hợp lệ email_already_exists: "Bạn đã đăng ký một tài khoản với %{email}" - invalid_params: 'Invalid, please check the signup paramters and try again' + invalid_params: 'Không hợp lệ, vui lòng kiểm tra thông số đăng ký và thử lại' failed: Đăng ký thât bại data_import: data_type: @@ -51,7 +51,7 @@ vi: dyte: invalid_message_type: "Loại tin nhắn không hợp lệ. Hành động không được phép" slack: - invalid_channel_id: "Invalid slack channel. Please try again" + invalid_channel_id: "Kênh chùng không hợp lệ. Vui lòng thử lại" inboxes: imap: socket_error: Vui lòng kiểm tra kết nối mạng, địa chỉ IMAP và thử lại. @@ -63,43 +63,43 @@ vi: name: không nên bắt đầu hoặc kết thúc bằng các ký hiệu và không nên có kí tự < > / \ @. custom_filters: number_of_records: Đã đạt giới hạn. Số lượng tuỳ chọn lọc tối đa cho mỗi mỗi người dùng mỗi tài khoản là 50. - invalid_attribute: Invalid attribute key - [%{key}]. The key should be one of [%{allowed_keys}] or a custom attribute defined in the account. - invalid_operator: Invalid operator. The allowed operators for %{attribute_name} are [%{allowed_keys}]. - invalid_value: Invalid value. The values provided for %{attribute_name} are invalid + invalid_attribute: Khóa thuộc tính không hợp lệ - [%{key}]. Chìa khóa phải là một trong [%{allowed_keys}] hoặc thuộc tính tùy chỉnh được xác định trong tài khoản. + invalid_operator: Toán tử không hợp lệ. Các toán tử được phép cho %{attribute_name} là [%{allowed_keys}]. + invalid_value: Giá trị không hợp lệ. Các giá trị được cung cấp cho %{attribute_name} không hợp lệ reports: period: Thời gian báo cáo từ %{since} đến %{until} utc_warning: Báo cáo đã được tạo với múi giờ UTC agent_csv: agent_name: Tên tổng đài viên - conversations_count: Assigned conversations - avg_first_response_time: Avg first response time + conversations_count: Cuộc trò chuyện được chỉ định + avg_first_response_time: Thời gian phản hồi đầu tiên trung bình avg_resolution_time: Avg resolution time resolution_count: Số lượng giải quyết - avg_customer_waiting_time: Avg customer waiting time + avg_customer_waiting_time: Thời gian chờ đợi trung bình của khách hàng inbox_csv: inbox_name: Tên kênh inbox_type: Kiểu kênh conversations_count: Số hội thoại - avg_first_response_time: Avg first response time - avg_resolution_time: Avg resolution time + avg_first_response_time: Thời gian phản hồi đầu tiên trung bình + avg_resolution_time: Thời gian giải quyết trung bình label_csv: label_title: Nhãn conversations_count: Số hội thoại - avg_first_response_time: Avg first response time - avg_resolution_time: Avg resolution time + avg_first_response_time: Thời gian phản hồi đầu tiên trung bình + avg_resolution_time: Thời gian giải quyết trung bình team_csv: team_name: Tên nhóm conversations_count: Số hội thoại - avg_first_response_time: Avg first response time - avg_resolution_time: Avg resolution time + avg_first_response_time: Thời gian phản hồi đầu tiên trung bình + avg_resolution_time: Thời gian giải quyết trung bình resolution_count: Số lượng giải quyết - avg_customer_waiting_time: Avg customer waiting time + avg_customer_waiting_time: Thời gian chờ đợi trung bình của khách hàng conversation_traffic_csv: timezone: Múi giờ sla_csv: - conversation_id: Conversation ID + conversation_id: ID hội thoại sla_policy_breached: SLA Policy - assignee: Assignee + assignee: Đại lý được chỉ định team: Nhóm inbox: Hộp thư đến labels: Nhãn @@ -118,22 +118,22 @@ vi: recorded_at: Ngày nghi notifications: notification_title: - conversation_creation: "A conversation (#%{display_id}) has been created in %{inbox_name}" - conversation_assignment: "A conversation (#%{display_id}) has been assigned to you" - assigned_conversation_new_message: "A new message is created in conversation (#%{display_id})" - conversation_mention: "You have been mentioned in conversation (#%{display_id})" - sla_missed_first_response: "SLA target first response missed for conversation (#%{display_id})" - sla_missed_next_response: "SLA target next response missed for conversation (#%{display_id})" - sla_missed_resolution: "SLA target resolution missed for conversation (#%{display_id})" - attachment: "Attachment" - no_content: "No content" + conversation_creation: "Một cuộc trò chuyện (#%{display_id}) đã được tạo trong %{inbox_name}" + conversation_assignment: "Một cuộc trò chuyện (#%{display_id}) đã được chỉ định cho bạn" + assigned_conversation_new_message: "Một tin nhắn mới được tạo trong cuộc trò chuyện (#%{display_id})" + conversation_mention: "Bạn đã được nhắc đến trong cuộc trò chuyện (#%{display_id})" + sla_missed_first_response: "Mục tiêu SLA phản hồi đầu tiên bị bỏ lỡ cho cuộc trò chuyện (#%{display_id})" + sla_missed_next_response: "Mục tiêu SLA phản hồi tiếp theo bị bỏ lỡ cho cuộc trò chuyện (#%{display_id})" + sla_missed_resolution: "Độ phân giải mục tiêu SLA bị bỏ lỡ cho cuộc trò chuyện (#%{display_id})" + attachment: "Tập tin đính kèm" + no_content: "Không có nội dung" conversations: messages: instagram_story_content: "%{story_sender} đã đề cập đến bạn trong hội thoại: " instagram_deleted_story_content: Hội thoại này không còn nữa. deleted: Tin nhắn đã bị xoá delivery_status: - error_code: "Error code: %{error_code}" + error_code: "Mã lỗi: %{error_code}" activity: status: resolved: "Cuộc trò chuyện được đánh dấu là đã giải quyết bởi %{user_name}" @@ -221,7 +221,7 @@ vi: common: home: Trang Chủ last_updated_on: 'Cập nhật lần cuối: %{last_updated_on}' - view_all_articles: View all + view_all_articles: Xem tất cả article: bài viết articles: bài viết author: tác giả @@ -233,17 +233,17 @@ vi: footer: made_with: Tạo bởi header: - go_to_homepage: Website + go_to_homepage: Trang web appearance: - system: System - light: Light - dark: Dark - featured_articles: Featured Articles + system: Hệ thống + light: Sáng + dark: Tối + featured_articles: Bài viết nổi bật uncategorized: Chưa được phân loại 404: - title: Page not found - description: We couldn't find the page you were looking for. - back_to_home: Go to home page + title: Không tìm thấy trang + description: Chúng tôi không thể tìm thấy trang bạn đang tìm kiếm. + back_to_home: Tới trang chủ slack_unfurl: fields: name: Tên @@ -255,10 +255,10 @@ vi: button: Mở cuộc trò chuyện time_units: days: - other: "%{count} days" + other: "%{count} ngày" hours: - other: "%{count} hours" + other: "%{count} giờ" minutes: - other: "%{count} minutes" + other: "%{count} phút" seconds: - other: "%{count} seconds" + other: "%{count} giây" From 8f3234cf4bbab7c170e4cd8d0a8097e1709b2c9b Mon Sep 17 00:00:00 2001 From: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Date: Thu, 27 Jun 2024 01:51:41 +0530 Subject: [PATCH 13/85] feat: Add video message viewing in to the user bubble in widget (#9642) --- .../widget/components/UserMessage.vue | 16 +++++++++++ .../widget/components/VideoBubble.vue | 27 +++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 app/javascript/widget/components/VideoBubble.vue diff --git a/app/javascript/widget/components/UserMessage.vue b/app/javascript/widget/components/UserMessage.vue index 0631b2f90..5a97422db 100755 --- a/app/javascript/widget/components/UserMessage.vue +++ b/app/javascript/widget/components/UserMessage.vue @@ -39,6 +39,14 @@ :readable-time="readableTime" @error="onImageLoadError" /> + + + +defineProps({ + url: { type: String, default: '' }, + readableTime: { type: String, default: '' }, +}); + +const emits = defineEmits(['error']); + +const onVideoError = () => { + emits('error'); +}; + + From 7ed7c1b618b3212220f949cd56871d5b8c5a02e8 Mon Sep 17 00:00:00 2001 From: Sojan Date: Wed, 26 Jun 2024 17:04:56 -0700 Subject: [PATCH 14/85] Bump version to 3.10.2 --- config/app.yml | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/config/app.yml b/config/app.yml index 8a133ee53..b8c238254 100644 --- a/config/app.yml +++ b/config/app.yml @@ -1,5 +1,5 @@ shared: &shared - version: '3.10.1' + version: '3.10.2' development: <<: *shared diff --git a/package.json b/package.json index 6b017503e..79ac41e02 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@chatwoot/chatwoot", - "version": "3.10.1", + "version": "3.10.2", "license": "MIT", "scripts": { "eslint": "eslint app/**/*.{js,vue}", From 2c94c890772de324248ee1306c80ff6f47c4598c Mon Sep 17 00:00:00 2001 From: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Date: Sat, 29 Jun 2024 01:21:27 +0530 Subject: [PATCH 15/85] feat: Add video message viewer in agent widget bubble (#9691) Fixes https://linear.app/chatwoot/issue/CW-3384/video-message-display-issue --- .../widget/components/AgentMessage.vue | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/app/javascript/widget/components/AgentMessage.vue b/app/javascript/widget/components/AgentMessage.vue index eb64e0c36..c38726031 100755 --- a/app/javascript/widget/components/AgentMessage.vue +++ b/app/javascript/widget/components/AgentMessage.vue @@ -30,7 +30,7 @@ />
+ + + @@ -84,6 +92,7 @@ import AgentMessageBubble from 'widget/components/AgentMessageBubble.vue'; import MessageReplyButton from 'widget/components/MessageReplyButton.vue'; import timeMixin from 'dashboard/mixins/time'; import ImageBubble from 'widget/components/ImageBubble.vue'; +import VideoBubble from 'widget/components/VideoBubble.vue'; import FileBubble from 'widget/components/FileBubble.vue'; import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue'; import { MESSAGE_TYPE } from 'widget/helpers/constants'; @@ -100,6 +109,7 @@ export default { components: { AgentMessageBubble, ImageBubble, + VideoBubble, Thumbnail, UserMessage, FileBubble, @@ -120,6 +130,7 @@ export default { data() { return { hasImageError: false, + hasVideoError: false, }; }, computed: { @@ -215,15 +226,20 @@ export default { watch: { message() { this.hasImageError = false; + this.hasVideoError = false; }, }, mounted() { this.hasImageError = false; + this.hasVideoError = false; }, methods: { onImageLoadError() { this.hasImageError = true; }, + onVideoLoadError() { + this.hasVideoError = true; + }, toggleReply() { emitter.emit(BUS_EVENTS.TOGGLE_REPLY_TO_MESSAGE, this.message); }, From 46621b098316260d6db97c87c126dd17bd118ad9 Mon Sep 17 00:00:00 2001 From: Sojan Jose Date: Fri, 28 Jun 2024 12:52:48 -0700 Subject: [PATCH 16/85] chore: Add permissions to auth data (#9695) This API change sets the foundation for an upcoming frontend update, transitioning from a role-based model to a permission-based model. This new approach will determine eligibility for various actions and UI elements based on specific permissions rather than roles, enhancing flexibility and security in user access management. --- app/models/account_user.rb | 4 ++++ app/views/api/v1/models/_user.json.jbuilder | 2 ++ spec/controllers/devise/session_controller_spec.rb | 11 +++++++++++ spec/models/account_user_spec.rb | 11 +++++++++++ 4 files changed, 28 insertions(+) diff --git a/app/models/account_user.rb b/app/models/account_user.rb index 78b874334..176d52102 100644 --- a/app/models/account_user.rb +++ b/app/models/account_user.rb @@ -49,6 +49,10 @@ class AccountUser < ApplicationRecord ::Agents::DestroyJob.perform_later(account, user) end + def permissions + administrator? ? ['administrator'] : ['agent'] + end + def push_event_data { id: id, diff --git a/app/views/api/v1/models/_user.json.jbuilder b/app/views/api/v1/models/_user.json.jbuilder index 74df836c0..0e8c95adb 100644 --- a/app/views/api/v1/models/_user.json.jbuilder +++ b/app/views/api/v1/models/_user.json.jbuilder @@ -14,6 +14,7 @@ json.provider resource.provider json.pubsub_token resource.pubsub_token json.custom_attributes resource.custom_attributes if resource.custom_attributes.present? json.role resource.active_account_user&.role +json.permissions resource.active_account_user&.permissions json.ui_settings resource.ui_settings json.uid resource.uid json.type resource.type @@ -24,6 +25,7 @@ json.accounts do json.status account_user.account.status json.active_at account_user.active_at json.role account_user.role + json.permissions account_user.permissions # the actual availability user has configured json.availability account_user.availability # availability derived from presence diff --git a/spec/controllers/devise/session_controller_spec.rb b/spec/controllers/devise/session_controller_spec.rb index 42f8b8bd8..ad6d76bf9 100644 --- a/spec/controllers/devise/session_controller_spec.rb +++ b/spec/controllers/devise/session_controller_spec.rb @@ -41,6 +41,17 @@ RSpec.describe 'Session', type: :request do expect(response).to have_http_status(:success) expect(response.body).to include(user_with_new_pwd.email) end + + it 'returns the permission of the user' do + params = { email: user.email, password: 'Password1!' } + + post new_user_session_url, + params: params, + as: :json + + expect(response).to have_http_status(:success) + expect(response.parsed_body['data']['permissions']).to eq(['agent']) + end end context 'when it is invalid sso auth token' do diff --git a/spec/models/account_user_spec.rb b/spec/models/account_user_spec.rb index 2b8ef790a..e5a560fe9 100644 --- a/spec/models/account_user_spec.rb +++ b/spec/models/account_user_spec.rb @@ -17,6 +17,17 @@ RSpec.describe AccountUser do end end + describe 'permissions' do + it 'returns the right permissions' do + expect(account_user.permissions).to eq(['agent']) + end + + it 'returns the right permissions for administrator' do + account_user.administrator! + expect(account_user.permissions).to eq(['administrator']) + end + end + describe 'destroy call agent::destroy service' do it 'gets created with the right default settings' do create(:conversation, account: account_user.account, assignee: account_user.user, inbox: inbox) From 5520bf68f37c5bbc189f5c4bbbdc325e1a4fad22 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Mon, 1 Jul 2024 11:11:57 +0530 Subject: [PATCH 17/85] feat: disable scripts on password reset page (#9693) --- app/controllers/dashboard_controller.rb | 12 +++++++++++- app/javascript/v3/views/index.js | 12 ++++++++---- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb index e656c2550..332f1528f 100644 --- a/app/controllers/dashboard_controller.rb +++ b/app/controllers/dashboard_controller.rb @@ -37,7 +37,7 @@ class DashboardController < ActionController::Base end def set_dashboard_scripts - @dashboard_scripts = GlobalConfig.get_value('DASHBOARD_SCRIPTS') + @dashboard_scripts = sensitive_path? ? nil : GlobalConfig.get_value('DASHBOARD_SCRIPTS') end def ensure_installation_onboarding @@ -75,4 +75,14 @@ class DashboardController < ActionController::Base 'application' end end + + def sensitive_path? + # dont load dashboard scripts on sensitive paths like password reset + sensitive_paths = [edit_user_password_path].freeze + + # remove app prefix + current_path = request.path.gsub(%r{^/app}, '') + + sensitive_paths.include?(current_path) + end end diff --git a/app/javascript/v3/views/index.js b/app/javascript/v3/views/index.js index 57da18116..dd30f0372 100644 --- a/app/javascript/v3/views/index.js +++ b/app/javascript/v3/views/index.js @@ -6,12 +6,16 @@ import { validateRouteAccess } from '../helpers/RouteHelper'; export const router = new VueRouter({ mode: 'history', routes }); +const sensitiveRouteNames = ['auth_password_edit']; + export const initalizeRouter = () => { router.beforeEach((to, _, next) => { - AnalyticsHelper.page(to.name || '', { - path: to.path, - name: to.name, - }); + if (!sensitiveRouteNames.includes(to.name)) { + AnalyticsHelper.page(to.name || '', { + path: to.path, + name: to.name, + }); + } return validateRouteAccess(to, next, window.chatwootConfig); }); From cc4851b19d1e3aedced341b01ff3bee5e43e0983 Mon Sep 17 00:00:00 2001 From: Sojan Jose Date: Wed, 3 Jul 2024 15:13:16 -0700 Subject: [PATCH 18/85] chore: Move frontend authorization to permission based system (#9709) We previously relied on user roles to determine whether to render specific routes in our frontend components. A permissions-based model is replacing this approach. Follow up: #9695 Co-authored-by: Pranav --- .../dashboard/components/layout/Sidebar.vue | 11 ++- .../layout/config/sidebarItems/primaryMenu.js | 9 +- .../layout/config/sidebarItems/settings.js | 46 +++++++++- .../layout/sidebarComponents/Secondary.vue | 26 +++--- .../sidebarComponents/SecondaryNavItem.vue | 47 +++++----- .../dashboard/components/policy.vue | 23 +++++ .../dashboard/helper/permissionsHelper.js | 34 +++++++ .../dashboard/helper/routeHelpers.js | 23 ++--- .../helper/specs/permissionsHelper.spec.js | 84 ++++++++++++++++++ .../helper/specs/routeHelpers.spec.js | 66 +++++++------- .../dashboard/modules/search/search.routes.js | 4 +- .../routes/dashboard/contacts/routes.js | 16 +++- .../conversation/conversation.routes.js | 64 ++++++++++---- .../routes/dashboard/dashboard.routes.js | 4 +- .../dashboard/helpcenter/helpcenter.routes.js | 88 ++++++++++++++----- .../routes/dashboard/inbox/routes.js | 8 +- .../routes/dashboard/notifications/routes.js | 4 +- .../settings/account/account.routes.js | 8 +- .../settings/agentBots/agentBot.routes.js | 16 +++- .../dashboard/settings/agents/agent.routes.js | 5 +- .../settings/attributes/attributes.routes.js | 5 +- .../settings/auditlogs/audit.routes.js | 5 +- .../settings/automation/automation.routes.js | 5 +- .../settings/billing/billing.routes.js | 8 +- .../settings/campaigns/campaigns.routes.js | 8 +- .../settings/canned/canned.routes.js | 5 +- .../dashboard/settings/inbox/inbox.routes.js | 25 ++++-- .../integrationapps/integrations.routes.js | 8 +- .../integrations/integrations.routes.js | 20 +++-- .../settings/labels/labels.routes.js | 8 +- .../settings/macros/macros.routes.js | 12 ++- .../settings/profile/profile.routes.js | 8 +- .../settings/reports/reports.routes.js | 36 ++++++-- .../dashboard/settings/settings.routes.js | 4 +- .../dashboard/settings/sla/sla.routes.js | 8 +- .../dashboard/settings/teams/teams.routes.js | 29 ++++-- app/javascript/dashboard/routes/index.js | 31 +------ 37 files changed, 582 insertions(+), 229 deletions(-) create mode 100644 app/javascript/dashboard/components/policy.vue create mode 100644 app/javascript/dashboard/helper/permissionsHelper.js create mode 100644 app/javascript/dashboard/helper/specs/permissionsHelper.spec.js diff --git a/app/javascript/dashboard/components/layout/Sidebar.vue b/app/javascript/dashboard/components/layout/Sidebar.vue index 0f3a1a857..4ccaa97df 100644 --- a/app/javascript/dashboard/components/layout/Sidebar.vue +++ b/app/javascript/dashboard/components/layout/Sidebar.vue @@ -20,7 +20,7 @@ :teams="teams" :custom-views="customViews" :menu-config="activeSecondaryMenu" - :current-role="currentRole" + :current-user="currentUser" :is-on-chatwoot-cloud="isOnChatwootCloud" @add-label="showAddLabelPopup" @toggle-accounts="toggleAccountModal" @@ -37,7 +37,8 @@ import alertMixin from 'shared/mixins/alertMixin'; import PrimarySidebar from './sidebarComponents/Primary.vue'; import SecondarySidebar from './sidebarComponents/Secondary.vue'; import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins'; -import router from '../../routes'; +import router, { routesWithPermissions } from '../../routes'; +import { hasPermissions } from '../../helper/permissionsHelper'; export default { components: { @@ -98,9 +99,13 @@ export default { return getSidebarItems(this.accountId); }, primaryMenuItems() { + const userPermissions = this.currentUser.permissions; const menuItems = this.sideMenuConfig.primaryMenu; return menuItems.filter(menuItem => { - const isAvailableForTheUser = menuItem.roles.includes(this.currentRole); + const isAvailableForTheUser = hasPermissions( + routesWithPermissions[menuItem.toStateName], + userPermissions + ); if (!isAvailableForTheUser) { return false; diff --git a/app/javascript/dashboard/components/layout/config/sidebarItems/primaryMenu.js b/app/javascript/dashboard/components/layout/config/sidebarItems/primaryMenu.js index 7513e3d1c..92b8765c6 100644 --- a/app/javascript/dashboard/components/layout/config/sidebarItems/primaryMenu.js +++ b/app/javascript/dashboard/components/layout/config/sidebarItems/primaryMenu.js @@ -9,7 +9,6 @@ const primaryMenuItems = accountId => [ featureFlag: FEATURE_FLAGS.INBOX_VIEW, toState: frontendURL(`accounts/${accountId}/inbox-view`), toStateName: 'inbox_view', - roles: ['administrator', 'agent'], }, { icon: 'chat', @@ -17,7 +16,6 @@ const primaryMenuItems = accountId => [ label: 'CONVERSATIONS', toState: frontendURL(`accounts/${accountId}/dashboard`), toStateName: 'home', - roles: ['administrator', 'agent'], }, { icon: 'book-contacts', @@ -26,7 +24,6 @@ const primaryMenuItems = accountId => [ featureFlag: FEATURE_FLAGS.CRM, toState: frontendURL(`accounts/${accountId}/contacts`), toStateName: 'contacts_dashboard', - roles: ['administrator', 'agent'], }, { icon: 'arrow-trending-lines', @@ -34,8 +31,7 @@ const primaryMenuItems = accountId => [ label: 'REPORTS', featureFlag: FEATURE_FLAGS.REPORTS, toState: frontendURL(`accounts/${accountId}/reports`), - toStateName: 'settings_account_reports', - roles: ['administrator'], + toStateName: 'account_overview_reports', }, { icon: 'megaphone', @@ -44,7 +40,6 @@ const primaryMenuItems = accountId => [ featureFlag: FEATURE_FLAGS.CAMPAIGNS, toState: frontendURL(`accounts/${accountId}/campaigns`), toStateName: 'ongoing_campaigns', - roles: ['administrator'], }, { icon: 'library', @@ -54,7 +49,6 @@ const primaryMenuItems = accountId => [ alwaysVisibleOnChatwootInstances: true, toState: frontendURL(`accounts/${accountId}/portals`), toStateName: 'default_portal_articles', - roles: ['administrator'], }, { icon: 'settings', @@ -62,7 +56,6 @@ const primaryMenuItems = accountId => [ label: 'SETTINGS', toState: frontendURL(`accounts/${accountId}/settings`), toStateName: 'settings_home', - roles: ['administrator', 'agent'], }, ]; diff --git a/app/javascript/dashboard/components/layout/config/sidebarItems/settings.js b/app/javascript/dashboard/components/layout/config/sidebarItems/settings.js index f450bd7a7..f7d63f6a1 100644 --- a/app/javascript/dashboard/components/layout/config/sidebarItems/settings.js +++ b/app/javascript/dashboard/components/layout/config/sidebarItems/settings.js @@ -24,7 +24,6 @@ const settings = accountId => ({ 'settings_inbox_list', 'settings_inbox_new', 'settings_inbox_show', - 'settings_inbox', 'settings_inboxes_add_agents', 'settings_inboxes_page_channel', 'settings_integrations_dashboard_apps', @@ -46,6 +45,9 @@ const settings = accountId => ({ icon: 'briefcase', label: 'ACCOUNT_SETTINGS', hasSubMenu: false, + meta: { + permissions: ['administrator'], + }, toState: frontendURL(`accounts/${accountId}/settings/general`), toStateName: 'general_settings_index', }, @@ -53,6 +55,9 @@ const settings = accountId => ({ icon: 'people', label: 'AGENTS', hasSubMenu: false, + meta: { + permissions: ['administrator'], + }, toState: frontendURL(`accounts/${accountId}/settings/agents/list`), toStateName: 'agent_list', featureFlag: FEATURE_FLAGS.AGENT_MANAGEMENT, @@ -61,6 +66,9 @@ const settings = accountId => ({ icon: 'people-team', label: 'TEAMS', hasSubMenu: false, + meta: { + permissions: ['administrator'], + }, toState: frontendURL(`accounts/${accountId}/settings/teams/list`), toStateName: 'settings_teams_list', featureFlag: FEATURE_FLAGS.TEAM_MANAGEMENT, @@ -69,6 +77,9 @@ const settings = accountId => ({ icon: 'mail-inbox-all', label: 'INBOXES', hasSubMenu: false, + meta: { + permissions: ['administrator'], + }, toState: frontendURL(`accounts/${accountId}/settings/inboxes/list`), toStateName: 'settings_inbox_list', featureFlag: FEATURE_FLAGS.INBOX_MANAGEMENT, @@ -77,6 +88,9 @@ const settings = accountId => ({ icon: 'tag', label: 'LABELS', hasSubMenu: false, + meta: { + permissions: ['administrator'], + }, toState: frontendURL(`accounts/${accountId}/settings/labels/list`), toStateName: 'labels_list', featureFlag: FEATURE_FLAGS.LABELS, @@ -85,6 +99,9 @@ const settings = accountId => ({ icon: 'code', label: 'CUSTOM_ATTRIBUTES', hasSubMenu: false, + meta: { + permissions: ['administrator'], + }, toState: frontendURL( `accounts/${accountId}/settings/custom-attributes/list` ), @@ -95,6 +112,9 @@ const settings = accountId => ({ icon: 'automation', label: 'AUTOMATION', hasSubMenu: false, + meta: { + permissions: ['administrator'], + }, toState: frontendURL(`accounts/${accountId}/settings/automation/list`), toStateName: 'automation_list', featureFlag: FEATURE_FLAGS.AUTOMATIONS, @@ -103,6 +123,9 @@ const settings = accountId => ({ icon: 'bot', label: 'AGENT_BOTS', hasSubMenu: false, + meta: { + permissions: ['administrator'], + }, globalConfigFlag: 'csmlEditorHost', toState: frontendURL(`accounts/${accountId}/settings/agent-bots`), toStateName: 'agent_bots', @@ -112,6 +135,9 @@ const settings = accountId => ({ icon: 'flash-settings', label: 'MACROS', hasSubMenu: false, + meta: { + permissions: ['administrator', 'agent'], + }, toState: frontendURL(`accounts/${accountId}/settings/macros`), toStateName: 'macros_wrapper', featureFlag: FEATURE_FLAGS.MACROS, @@ -120,6 +146,9 @@ const settings = accountId => ({ icon: 'chat-multiple', label: 'CANNED_RESPONSES', hasSubMenu: false, + meta: { + permissions: ['administrator', 'agent'], + }, toState: frontendURL( `accounts/${accountId}/settings/canned-response/list` ), @@ -130,6 +159,9 @@ const settings = accountId => ({ icon: 'flash-on', label: 'INTEGRATIONS', hasSubMenu: false, + meta: { + permissions: ['administrator'], + }, toState: frontendURL(`accounts/${accountId}/settings/integrations`), toStateName: 'settings_integrations', featureFlag: FEATURE_FLAGS.INTEGRATIONS, @@ -138,6 +170,9 @@ const settings = accountId => ({ icon: 'star-emphasis', label: 'APPLICATIONS', hasSubMenu: false, + meta: { + permissions: ['administrator'], + }, toState: frontendURL(`accounts/${accountId}/settings/applications`), toStateName: 'settings_applications', featureFlag: FEATURE_FLAGS.INTEGRATIONS, @@ -146,6 +181,9 @@ const settings = accountId => ({ icon: 'key', label: 'AUDIT_LOGS', hasSubMenu: false, + meta: { + permissions: ['administrator'], + }, toState: frontendURL(`accounts/${accountId}/settings/audit-log/list`), toStateName: 'auditlogs_list', isEnterpriseOnly: true, @@ -156,6 +194,9 @@ const settings = accountId => ({ icon: 'document-list-clock', label: 'SLA', hasSubMenu: false, + meta: { + permissions: ['administrator'], + }, toState: frontendURL(`accounts/${accountId}/settings/sla/list`), toStateName: 'sla_list', isEnterpriseOnly: true, @@ -166,6 +207,9 @@ const settings = accountId => ({ icon: 'credit-card-person', label: 'BILLING', hasSubMenu: false, + meta: { + permissions: ['administrator'], + }, toState: frontendURL(`accounts/${accountId}/settings/billing`), toStateName: 'billing_settings_index', showOnlyOnCloud: true, diff --git a/app/javascript/dashboard/components/layout/sidebarComponents/Secondary.vue b/app/javascript/dashboard/components/layout/sidebarComponents/Secondary.vue index f291d221e..b4cd3ca90 100644 --- a/app/javascript/dashboard/components/layout/sidebarComponents/Secondary.vue +++ b/app/javascript/dashboard/components/layout/sidebarComponents/Secondary.vue @@ -29,6 +29,8 @@ import SecondaryNavItem from './SecondaryNavItem.vue'; import AccountContext from './AccountContext.vue'; import { mapGetters } from 'vuex'; import { FEATURE_FLAGS } from '../../../featureFlags'; +import { hasPermissions } from '../../../helper/permissionsHelper'; +import { routesWithPermissions } from '../../../routes'; export default { components: { @@ -60,9 +62,9 @@ export default { type: Object, default: () => {}, }, - currentRole: { - type: String, - default: '', + currentUser: { + type: Object, + default: () => {}, }, isOnChatwootCloud: { type: Boolean, @@ -80,16 +82,16 @@ export default { return this.customViews.filter(view => view.filter_type === 'contact'); }, accessibleMenuItems() { - if (!this.currentRole) { - return []; - } - const menuItemsFilteredByRole = this.menuConfig.menuItems.filter( - menuItem => - window.roleWiseRoutes[this.currentRole].indexOf( - menuItem.toStateName - ) > -1 + const menuItemsFilteredByPermissions = this.menuConfig.menuItems.filter( + menuItem => { + const { permissions: userPermissions = [] } = this.currentUser; + return hasPermissions( + routesWithPermissions[menuItem.toStateName], + userPermissions + ); + } ); - return menuItemsFilteredByRole.filter(item => { + return menuItemsFilteredByPermissions.filter(item => { if (item.showOnlyOnCloud) { return this.isOnChatwootCloud; } diff --git a/app/javascript/dashboard/components/layout/sidebarComponents/SecondaryNavItem.vue b/app/javascript/dashboard/components/layout/sidebarComponents/SecondaryNavItem.vue index a7009523a..bc3b03646 100644 --- a/app/javascript/dashboard/components/layout/sidebarComponents/SecondaryNavItem.vue +++ b/app/javascript/dashboard/components/layout/sidebarComponents/SecondaryNavItem.vue @@ -65,27 +65,29 @@ :show-child-count="showChildCount(child.count)" :child-item-count="child.count" /> - -
  • - - - {{ $t(`SIDEBAR.${menuItem.newLinkTag}`) }} - - -
  • -
    + + +
  • + + + {{ $t(`SIDEBAR.${menuItem.newLinkTag}`) }} + + +
  • +
    +
    @@ -105,9 +107,10 @@ import { isOnMentionsView, isOnUnattendedView, } from '../../../store/modules/conversations/helpers/actionHelpers'; +import Policy from '../../policy.vue'; export default { - components: { SecondaryChildNavItem }, + components: { SecondaryChildNavItem, Policy }, mixins: [adminMixin, configMixin], props: { menuItem: { diff --git a/app/javascript/dashboard/components/policy.vue b/app/javascript/dashboard/components/policy.vue new file mode 100644 index 000000000..f888de5a2 --- /dev/null +++ b/app/javascript/dashboard/components/policy.vue @@ -0,0 +1,23 @@ + + + diff --git a/app/javascript/dashboard/helper/permissionsHelper.js b/app/javascript/dashboard/helper/permissionsHelper.js new file mode 100644 index 000000000..135238371 --- /dev/null +++ b/app/javascript/dashboard/helper/permissionsHelper.js @@ -0,0 +1,34 @@ +export const hasPermissions = ( + requiredPermissions = [], + availablePermissions = [] +) => { + return requiredPermissions.some(permission => + availablePermissions.includes(permission) + ); +}; + +const isPermissionsPresentInRoute = route => + route.meta && route.meta.permissions; + +export const buildPermissionsFromRouter = (routes = []) => + routes.reduce((acc, route) => { + if (route.name) { + if (!isPermissionsPresentInRoute(route)) { + // eslint-disable-next-line + console.error(route); + throw new Error( + "The route doesn't have the required permissions defined" + ); + } + acc[route.name] = route.meta.permissions; + } + + if (route.children) { + acc = { + ...acc, + ...buildPermissionsFromRouter(route.children), + }; + } + + return acc; + }, {}); diff --git a/app/javascript/dashboard/helper/routeHelpers.js b/app/javascript/dashboard/helper/routeHelpers.js index 68cf0a627..6c4036dd1 100644 --- a/app/javascript/dashboard/helper/routeHelpers.js +++ b/app/javascript/dashboard/helper/routeHelpers.js @@ -1,19 +1,16 @@ +import { hasPermissions } from './permissionsHelper'; + // eslint-disable-next-line default-param-last export const getCurrentAccount = ({ accounts } = {}, accountId) => { return accounts.find(account => account.id === accountId); }; -// eslint-disable-next-line default-param-last -export const getUserRole = ({ accounts } = {}, accountId) => { - const currentAccount = getCurrentAccount({ accounts }, accountId) || {}; - return currentAccount.role || null; +export const routeIsAccessibleFor = (route, userPermissions = []) => { + const { meta: { permissions: routePermissions = [] } = {} } = route; + return hasPermissions(routePermissions, userPermissions); }; -export const routeIsAccessibleFor = (route, role, roleWiseRoutes) => { - return roleWiseRoutes[role].includes(route); -}; - -const validateActiveAccountRoutes = (to, user, roleWiseRoutes) => { +const validateActiveAccountRoutes = (to, user) => { // If the current account is active, then check for the route permissions const accountDashboardURL = `accounts/${to.params.accountId}/dashboard`; @@ -22,15 +19,13 @@ const validateActiveAccountRoutes = (to, user, roleWiseRoutes) => { return accountDashboardURL; } - const userRole = getUserRole(user, Number(to.params.accountId)); - const isAccessible = routeIsAccessibleFor(to.name, userRole, roleWiseRoutes); + const isAccessible = routeIsAccessibleFor(to, user.permissions); // If the route is not accessible for the user, return to dashboard screen return isAccessible ? null : accountDashboardURL; }; -export const validateLoggedInRoutes = (to, user, roleWiseRoutes) => { +export const validateLoggedInRoutes = (to, user) => { const currentAccount = getCurrentAccount(user, Number(to.params.accountId)); - // If current account is missing, either user does not have // access to the account or the account is deleted, return to login screen if (!currentAccount) { @@ -40,7 +35,7 @@ export const validateLoggedInRoutes = (to, user, roleWiseRoutes) => { const isCurrentAccountActive = currentAccount.status === 'active'; if (isCurrentAccountActive) { - return validateActiveAccountRoutes(to, user, roleWiseRoutes); + return validateActiveAccountRoutes(to, user); } // If the current account is not active, then redirect the user to the suspended screen diff --git a/app/javascript/dashboard/helper/specs/permissionsHelper.spec.js b/app/javascript/dashboard/helper/specs/permissionsHelper.spec.js new file mode 100644 index 000000000..34f434f7d --- /dev/null +++ b/app/javascript/dashboard/helper/specs/permissionsHelper.spec.js @@ -0,0 +1,84 @@ +import { + buildPermissionsFromRouter, + hasPermissions, +} from '../permissionsHelper'; + +describe('hasPermissions', () => { + it('returns true if permission is present', () => { + expect( + hasPermissions(['contact_manage'], ['team_manage', 'contact_manage']) + ).toBe(true); + }); + + it('returns true if permission is not present', () => { + expect( + hasPermissions(['contact_manage'], ['team_manage', 'user_manage']) + ).toBe(false); + expect(hasPermissions()).toBe(false); + expect(hasPermissions([])).toBe(false); + }); +}); + +describe('buildPermissionsFromRouter', () => { + it('returns a valid object when routes have permissions defined', () => { + expect( + buildPermissionsFromRouter([ + { + path: 'agent', + name: 'agent_list', + meta: { permissions: ['agent_admin'] }, + }, + { + path: 'inbox', + children: [ + { + path: '', + name: 'inbox_list', + meta: { permissions: ['inbox_admin'] }, + }, + ], + }, + { + path: 'conversations', + children: [ + { + path: '', + children: [ + { + path: 'attachments', + name: 'attachments_list', + meta: { permissions: ['conversation_admin'] }, + }, + ], + }, + ], + }, + ]) + ).toEqual({ + agent_list: ['agent_admin'], + inbox_list: ['inbox_admin'], + attachments_list: ['conversation_admin'], + }); + }); + + it('throws an error if a named routed does not have permissions defined', () => { + expect(() => { + buildPermissionsFromRouter([ + { + path: 'agent', + name: 'agent_list', + }, + ]); + }).toThrow("The route doesn't have the required permissions defined"); + + expect(() => { + buildPermissionsFromRouter([ + { + path: 'agent', + name: 'agent_list', + meta: {}, + }, + ]); + }).toThrow("The route doesn't have the required permissions defined"); + }); +}); diff --git a/app/javascript/dashboard/helper/specs/routeHelpers.spec.js b/app/javascript/dashboard/helper/specs/routeHelpers.spec.js index 1e100a678..5aa9c4ee2 100644 --- a/app/javascript/dashboard/helper/specs/routeHelpers.spec.js +++ b/app/javascript/dashboard/helper/specs/routeHelpers.spec.js @@ -1,7 +1,6 @@ import { getConversationDashboardRoute, getCurrentAccount, - getUserRole, isAConversationRoute, routeIsAccessibleFor, validateLoggedInRoutes, @@ -15,24 +14,11 @@ describe('#getCurrentAccount', () => { }); }); -describe('#getUserRole', () => { - it('should return the current role', () => { - expect( - getUserRole({ accounts: [{ id: 1, role: 'administrator' }] }, 1) - ).toEqual('administrator'); - expect(getUserRole({ accounts: [] }, 1)).toEqual(null); - }); -}); - describe('#routeIsAccessibleFor', () => { it('should return the correct access', () => { - const roleWiseRoutes = { agent: ['conversations'], admin: ['billing'] }; - expect(routeIsAccessibleFor('billing', 'agent', roleWiseRoutes)).toEqual( - false - ); - expect(routeIsAccessibleFor('billing', 'admin', roleWiseRoutes)).toEqual( - true - ); + let route = { meta: { permissions: ['administrator'] } }; + expect(routeIsAccessibleFor(route, ['agent'])).toEqual(false); + expect(routeIsAccessibleFor(route, ['administrator'])).toEqual(true); }); }); @@ -40,11 +26,7 @@ describe('#validateLoggedInRoutes', () => { describe('when account access is missing', () => { it('should return the login route', () => { expect( - validateLoggedInRoutes( - { params: { accountId: 1 } }, - { accounts: [] }, - {} - ) + validateLoggedInRoutes({ params: { accountId: 1 } }, { accounts: [] }) ).toEqual(`app/login`); }); }); @@ -53,9 +35,12 @@ describe('#validateLoggedInRoutes', () => { it('return suspended route', () => { expect( validateLoggedInRoutes( - { name: 'conversations', params: { accountId: 1 } }, - { accounts: [{ id: 1, role: 'agent', status: 'suspended' }] }, - { agent: ['conversations'] } + { + name: 'conversations', + params: { accountId: 1 }, + meta: { permissions: ['agent'] }, + }, + { accounts: [{ id: 1, role: 'agent', status: 'suspended' }] } ) ).toEqual(`accounts/1/suspended`); }); @@ -65,9 +50,22 @@ describe('#validateLoggedInRoutes', () => { it('returns null (no action required)', () => { expect( validateLoggedInRoutes( - { name: 'conversations', params: { accountId: 1 } }, - { accounts: [{ id: 1, role: 'agent', status: 'active' }] }, - { agent: ['conversations'] } + { + name: 'conversations', + params: { accountId: 1 }, + meta: { permissions: ['agent'] }, + }, + { + permissions: ['agent'], + accounts: [ + { + id: 1, + role: 'agent', + permissions: ['agent'], + status: 'active', + }, + ], + } ) ).toEqual(null); }); @@ -76,9 +74,12 @@ describe('#validateLoggedInRoutes', () => { it('returns dashboard url', () => { expect( validateLoggedInRoutes( - { name: 'conversations', params: { accountId: 1 } }, - { accounts: [{ id: 1, role: 'agent', status: 'active' }] }, - { admin: ['conversations'], agent: [] } + { + name: 'billing', + params: { accountId: 1 }, + meta: { permissions: ['administrator'] }, + }, + { accounts: [{ id: 1, role: 'agent', status: 'active' }] } ) ).toEqual(`accounts/1/dashboard`); }); @@ -88,8 +89,7 @@ describe('#validateLoggedInRoutes', () => { expect( validateLoggedInRoutes( { name: 'account_suspended', params: { accountId: 1 } }, - { accounts: [{ id: 1, role: 'agent', status: 'active' }] }, - { agent: ['account_suspended'] } + { accounts: [{ id: 1, role: 'agent', status: 'active' }] } ) ).toEqual(`accounts/1/dashboard`); }); diff --git a/app/javascript/dashboard/modules/search/search.routes.js b/app/javascript/dashboard/modules/search/search.routes.js index 320f64a44..d2d7b19c4 100644 --- a/app/javascript/dashboard/modules/search/search.routes.js +++ b/app/javascript/dashboard/modules/search/search.routes.js @@ -7,7 +7,9 @@ export const routes = [ { path: frontendURL('accounts/:accountId/search'), name: 'search', - roles: ['administrator', 'agent'], + meta: { + permissions: ['administrator', 'agent'], + }, component: SearchView, }, ]; diff --git a/app/javascript/dashboard/routes/dashboard/contacts/routes.js b/app/javascript/dashboard/routes/dashboard/contacts/routes.js index 10a560740..a07ca6bf3 100644 --- a/app/javascript/dashboard/routes/dashboard/contacts/routes.js +++ b/app/javascript/dashboard/routes/dashboard/contacts/routes.js @@ -7,13 +7,17 @@ export const routes = [ { path: frontendURL('accounts/:accountId/contacts'), name: 'contacts_dashboard', - roles: ['administrator', 'agent'], + meta: { + permissions: ['administrator', 'agent'], + }, component: ContactsView, }, { path: frontendURL('accounts/:accountId/contacts/custom_view/:id'), name: 'contacts_segments_dashboard', - roles: ['administrator', 'agent'], + meta: { + permissions: ['administrator', 'agent'], + }, component: ContactsView, props: route => { return { segmentsId: route.params.id }; @@ -22,7 +26,9 @@ export const routes = [ { path: frontendURL('accounts/:accountId/labels/:label/contacts'), name: 'contacts_labels_dashboard', - roles: ['administrator', 'agent'], + meta: { + permissions: ['administrator', 'agent'], + }, component: ContactsView, props: route => { return { label: route.params.label }; @@ -31,7 +37,9 @@ export const routes = [ { path: frontendURL('accounts/:accountId/contacts/:contactId'), name: 'contact_profile_dashboard', - roles: ['administrator', 'agent'], + meta: { + permissions: ['administrator', 'agent'], + }, component: ContactManageView, props: route => { return { contactId: route.params.contactId }; diff --git a/app/javascript/dashboard/routes/dashboard/conversation/conversation.routes.js b/app/javascript/dashboard/routes/dashboard/conversation/conversation.routes.js index 14487682e..7a81e2f4e 100644 --- a/app/javascript/dashboard/routes/dashboard/conversation/conversation.routes.js +++ b/app/javascript/dashboard/routes/dashboard/conversation/conversation.routes.js @@ -7,7 +7,9 @@ export default { { path: frontendURL('accounts/:accountId/dashboard'), name: 'home', - roles: ['administrator', 'agent'], + meta: { + permissions: ['administrator', 'agent'], + }, component: ConversationView, props: () => { return { inboxId: 0 }; @@ -16,7 +18,9 @@ export default { { path: frontendURL('accounts/:accountId/conversations/:conversation_id'), name: 'inbox_conversation', - roles: ['administrator', 'agent'], + meta: { + permissions: ['administrator', 'agent'], + }, component: ConversationView, props: route => { return { inboxId: 0, conversationId: route.params.conversation_id }; @@ -25,7 +29,9 @@ export default { { path: frontendURL('accounts/:accountId/inbox/:inbox_id'), name: 'inbox_dashboard', - roles: ['administrator', 'agent'], + meta: { + permissions: ['administrator', 'agent'], + }, component: ConversationView, props: route => { return { inboxId: route.params.inbox_id }; @@ -36,7 +42,9 @@ export default { 'accounts/:accountId/inbox/:inbox_id/conversations/:conversation_id' ), name: 'conversation_through_inbox', - roles: ['administrator', 'agent'], + meta: { + permissions: ['administrator', 'agent'], + }, component: ConversationView, props: route => { return { @@ -48,7 +56,9 @@ export default { { path: frontendURL('accounts/:accountId/label/:label'), name: 'label_conversations', - roles: ['administrator', 'agent'], + meta: { + permissions: ['administrator', 'agent'], + }, component: ConversationView, props: route => ({ label: route.params.label }), }, @@ -57,7 +67,9 @@ export default { 'accounts/:accountId/label/:label/conversations/:conversation_id' ), name: 'conversations_through_label', - roles: ['administrator', 'agent'], + meta: { + permissions: ['administrator', 'agent'], + }, component: ConversationView, props: route => ({ conversationId: route.params.conversation_id, @@ -67,7 +79,9 @@ export default { { path: frontendURL('accounts/:accountId/team/:teamId'), name: 'team_conversations', - roles: ['administrator', 'agent'], + meta: { + permissions: ['administrator', 'agent'], + }, component: ConversationView, props: route => ({ teamId: route.params.teamId }), }, @@ -76,7 +90,9 @@ export default { 'accounts/:accountId/team/:teamId/conversations/:conversationId' ), name: 'conversations_through_team', - roles: ['administrator', 'agent'], + meta: { + permissions: ['administrator', 'agent'], + }, component: ConversationView, props: route => ({ conversationId: route.params.conversationId, @@ -86,7 +102,9 @@ export default { { path: frontendURL('accounts/:accountId/custom_view/:id'), name: 'folder_conversations', - roles: ['administrator', 'agent'], + meta: { + permissions: ['administrator', 'agent'], + }, component: ConversationView, props: route => ({ foldersId: route.params.id }), }, @@ -95,7 +113,9 @@ export default { 'accounts/:accountId/custom_view/:id/conversations/:conversation_id' ), name: 'conversations_through_folders', - roles: ['administrator', 'agent'], + meta: { + permissions: ['administrator', 'agent'], + }, component: ConversationView, props: route => ({ conversationId: route.params.conversation_id, @@ -105,7 +125,9 @@ export default { { path: frontendURL('accounts/:accountId/mentions/conversations'), name: 'conversation_mentions', - roles: ['administrator', 'agent'], + meta: { + permissions: ['administrator', 'agent'], + }, component: ConversationView, props: () => ({ conversationType: 'mention' }), }, @@ -114,7 +136,9 @@ export default { 'accounts/:accountId/mentions/conversations/:conversationId' ), name: 'conversation_through_mentions', - roles: ['administrator', 'agent'], + meta: { + permissions: ['administrator', 'agent'], + }, component: ConversationView, props: route => ({ conversationId: route.params.conversationId, @@ -124,7 +148,9 @@ export default { { path: frontendURL('accounts/:accountId/unattended/conversations'), name: 'conversation_unattended', - roles: ['administrator', 'agent'], + meta: { + permissions: ['administrator', 'agent'], + }, component: ConversationView, props: () => ({ conversationType: 'unattended' }), }, @@ -133,7 +159,9 @@ export default { 'accounts/:accountId/unattended/conversations/:conversationId' ), name: 'conversation_through_unattended', - roles: ['administrator', 'agent'], + meta: { + permissions: ['administrator', 'agent'], + }, component: ConversationView, props: route => ({ conversationId: route.params.conversationId, @@ -143,7 +171,9 @@ export default { { path: frontendURL('accounts/:accountId/participating/conversations'), name: 'conversation_participating', - roles: ['administrator', 'agent'], + meta: { + permissions: ['administrator', 'agent'], + }, component: ConversationView, props: () => ({ conversationType: 'participating' }), }, @@ -152,7 +182,9 @@ export default { 'accounts/:accountId/participating/conversations/:conversationId' ), name: 'conversation_through_participating', - roles: ['administrator', 'agent'], + meta: { + permissions: ['administrator', 'agent'], + }, component: ConversationView, props: route => ({ conversationId: route.params.conversationId, diff --git a/app/javascript/dashboard/routes/dashboard/dashboard.routes.js b/app/javascript/dashboard/routes/dashboard/dashboard.routes.js index 9e18ca42c..b02f29342 100644 --- a/app/javascript/dashboard/routes/dashboard/dashboard.routes.js +++ b/app/javascript/dashboard/routes/dashboard/dashboard.routes.js @@ -28,7 +28,9 @@ export default { { path: frontendURL('accounts/:accountId/suspended'), name: 'account_suspended', - roles: ['administrator', 'agent'], + meta: { + permissions: ['administrator', 'agent'], + }, component: Suspended, }, ], diff --git a/app/javascript/dashboard/routes/dashboard/helpcenter/helpcenter.routes.js b/app/javascript/dashboard/routes/dashboard/helpcenter/helpcenter.routes.js index a165b0277..0326534f3 100644 --- a/app/javascript/dashboard/routes/dashboard/helpcenter/helpcenter.routes.js +++ b/app/javascript/dashboard/routes/dashboard/helpcenter/helpcenter.routes.js @@ -30,13 +30,17 @@ const portalRoutes = [ { path: getPortalRoute(''), name: 'default_portal_articles', - roles: ['administrator', 'agent'], + meta: { + permissions: ['administrator'], + }, component: DefaultPortalArticles, }, { path: getPortalRoute('all'), name: 'list_all_portals', - roles: ['administrator', 'agent'], + meta: { + permissions: ['administrator', 'agent'], + }, component: ListAllPortals, }, { @@ -47,55 +51,73 @@ const portalRoutes = [ path: '', name: 'new_portal_information', component: PortalDetails, - roles: ['administrator'], + meta: { + permissions: ['administrator'], + }, }, { path: ':portalSlug/customization', name: 'portal_customization', component: PortalCustomization, - roles: ['administrator'], + meta: { + permissions: ['administrator'], + }, }, { path: ':portalSlug/finish', name: 'portal_finish', component: PortalSettingsFinish, - roles: ['administrator'], + meta: { + permissions: ['administrator'], + }, }, ], }, { path: getPortalRoute(':portalSlug'), name: 'portalSlug', - roles: ['administrator', 'agent'], + meta: { + permissions: ['administrator', 'agent'], + }, component: ShowPortal, }, { path: getPortalRoute(':portalSlug/edit'), - roles: ['administrator', 'agent'], + meta: { + permissions: ['administrator', 'agent'], + }, component: EditPortal, children: [ { path: '', name: 'edit_portal_information', component: EditPortalBasic, - roles: ['administrator'], + meta: { + permissions: ['administrator'], + }, }, { path: 'customizations', name: 'edit_portal_customization', component: EditPortalCustomization, - roles: ['administrator'], + meta: { + permissions: ['administrator'], + }, }, { path: 'locales', name: 'edit_portal_locales', component: EditPortalLocales, - roles: ['administrator'], + meta: { + permissions: ['administrator'], + }, }, { path: 'categories', name: 'list_all_locale_categories', - roles: ['administrator', 'agent'], + meta: { + permissions: ['administrator', 'agent'], + }, component: ListAllCategories, }, ], @@ -106,39 +128,51 @@ const articleRoutes = [ { path: getPortalRoute(':portalSlug/:locale/articles'), name: 'list_all_locale_articles', - roles: ['administrator', 'agent'], + meta: { + permissions: ['administrator', 'agent'], + }, component: ListAllArticles, }, { path: getPortalRoute(':portalSlug/:locale/articles/new'), name: 'new_article', - roles: ['administrator', 'agent'], + meta: { + permissions: ['administrator', 'agent'], + }, component: NewArticle, }, { path: getPortalRoute(':portalSlug/:locale/articles/mine'), name: 'list_mine_articles', - roles: ['administrator', 'agent'], + meta: { + permissions: ['administrator', 'agent'], + }, component: ListAllArticles, }, { path: getPortalRoute(':portalSlug/:locale/articles/archived'), name: 'list_archived_articles', - roles: ['administrator', 'agent'], + meta: { + permissions: ['administrator', 'agent'], + }, component: ListAllArticles, }, { path: getPortalRoute(':portalSlug/:locale/articles/draft'), name: 'list_draft_articles', - roles: ['administrator', 'agent'], + meta: { + permissions: ['administrator', 'agent'], + }, component: ListAllArticles, }, { path: getPortalRoute(':portalSlug/:locale/articles/:articleSlug'), name: 'edit_article', - roles: ['administrator', 'agent'], + meta: { + permissions: ['administrator', 'agent'], + }, component: EditArticle, }, ]; @@ -147,19 +181,25 @@ const categoryRoutes = [ { path: getPortalRoute(':portalSlug/:locale/categories'), name: 'all_locale_categories', - roles: ['administrator', 'agent'], + meta: { + permissions: ['administrator', 'agent'], + }, component: ListAllCategories, }, { path: getPortalRoute(':portalSlug/:locale/categories/new'), name: 'new_category_in_locale', - roles: ['administrator', 'agent'], + meta: { + permissions: ['administrator', 'agent'], + }, component: NewCategory, }, { path: getPortalRoute(':portalSlug/:locale/categories/:categorySlug'), name: 'show_category', - roles: ['administrator', 'agent'], + meta: { + permissions: ['administrator', 'agent'], + }, component: ListAllArticles, }, { @@ -167,13 +207,17 @@ const categoryRoutes = [ ':portalSlug/:locale/categories/:categorySlug/articles' ), name: 'show_category_articles', - roles: ['administrator', 'agent'], + meta: { + permissions: ['administrator', 'agent'], + }, component: ListCategoryArticles, }, { path: getPortalRoute(':portalSlug/:locale/categories/:categorySlug'), name: 'edit_category', - roles: ['administrator', 'agent'], + meta: { + permissions: ['administrator', 'agent'], + }, component: EditCategory, }, ]; diff --git a/app/javascript/dashboard/routes/dashboard/inbox/routes.js b/app/javascript/dashboard/routes/dashboard/inbox/routes.js index 9c26fa64e..729fee76d 100644 --- a/app/javascript/dashboard/routes/dashboard/inbox/routes.js +++ b/app/javascript/dashboard/routes/dashboard/inbox/routes.js @@ -12,13 +12,17 @@ export const routes = [ path: '', name: 'inbox_view', component: InboxEmptyStateView, - roles: ['administrator', 'agent'], + meta: { + permissions: ['administrator', 'agent'], + }, }, { path: ':notification_id', name: 'inbox_view_conversation', component: InboxDetailView, - roles: ['administrator', 'agent'], + meta: { + permissions: ['administrator', 'agent'], + }, }, ], }, diff --git a/app/javascript/dashboard/routes/dashboard/notifications/routes.js b/app/javascript/dashboard/routes/dashboard/notifications/routes.js index 38812fc92..e3d125333 100644 --- a/app/javascript/dashboard/routes/dashboard/notifications/routes.js +++ b/app/javascript/dashboard/routes/dashboard/notifications/routes.js @@ -18,7 +18,9 @@ export const routes = [ path: '', name: 'notifications_index', component: NotificationsView, - roles: ['administrator', 'agent'], + meta: { + permissions: ['administrator', 'agent'], + }, }, ], }, diff --git a/app/javascript/dashboard/routes/dashboard/settings/account/account.routes.js b/app/javascript/dashboard/routes/dashboard/settings/account/account.routes.js index 0acfd5bd5..2742621e9 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/account/account.routes.js +++ b/app/javascript/dashboard/routes/dashboard/settings/account/account.routes.js @@ -6,7 +6,9 @@ export default { routes: [ { path: frontendURL('accounts/:accountId/settings/general'), - roles: ['administrator'], + meta: { + permissions: ['administrator'], + }, component: SettingsContent, props: { headerTitle: 'GENERAL_SETTINGS.TITLE', @@ -18,7 +20,9 @@ export default { path: '', name: 'general_settings_index', component: Index, - roles: ['administrator'], + meta: { + permissions: ['administrator'], + }, }, ], }, diff --git a/app/javascript/dashboard/routes/dashboard/settings/agentBots/agentBot.routes.js b/app/javascript/dashboard/routes/dashboard/settings/agentBots/agentBot.routes.js index 382594c73..55210397f 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/agentBots/agentBot.routes.js +++ b/app/javascript/dashboard/routes/dashboard/settings/agentBots/agentBot.routes.js @@ -8,7 +8,9 @@ export default { routes: [ { path: frontendURL('accounts/:accountId/settings/agent-bots'), - roles: ['administrator'], + meta: { + permissions: ['administrator'], + }, component: SettingsContent, props: { headerTitle: 'AGENT_BOTS.HEADER', @@ -20,19 +22,25 @@ export default { path: '', name: 'agent_bots', component: Bot, - roles: ['administrator'], + meta: { + permissions: ['administrator'], + }, }, { path: 'csml/new', name: 'agent_bots_csml_new', component: CsmlNewBot, - roles: ['administrator'], + meta: { + permissions: ['administrator'], + }, }, { path: 'csml/:botId', name: 'agent_bots_csml_edit', component: CsmlEditBot, - roles: ['administrator'], + meta: { + permissions: ['administrator'], + }, }, ], }, diff --git a/app/javascript/dashboard/routes/dashboard/settings/agents/agent.routes.js b/app/javascript/dashboard/routes/dashboard/settings/agents/agent.routes.js index cd23432ff..854450c5d 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/agents/agent.routes.js +++ b/app/javascript/dashboard/routes/dashboard/settings/agents/agent.routes.js @@ -15,14 +15,15 @@ export default { children: [ { path: '', - name: 'agents_wrapper', redirect: 'list', }, { path: 'list', name: 'agent_list', component: AgentHome, - roles: ['administrator'], + meta: { + permissions: ['administrator'], + }, }, ], }, diff --git a/app/javascript/dashboard/routes/dashboard/settings/attributes/attributes.routes.js b/app/javascript/dashboard/routes/dashboard/settings/attributes/attributes.routes.js index 72d79a9de..93c4f8eda 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/attributes/attributes.routes.js +++ b/app/javascript/dashboard/routes/dashboard/settings/attributes/attributes.routes.js @@ -15,14 +15,15 @@ export default { children: [ { path: '', - name: 'attributes_wrapper', redirect: 'list', }, { path: 'list', name: 'attributes_list', component: AttributesHome, - roles: ['administrator'], + meta: { + permissions: ['administrator'], + }, }, ], }, diff --git a/app/javascript/dashboard/routes/dashboard/settings/auditlogs/audit.routes.js b/app/javascript/dashboard/routes/dashboard/settings/auditlogs/audit.routes.js index 00c1fda74..acf061288 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/auditlogs/audit.routes.js +++ b/app/javascript/dashboard/routes/dashboard/settings/auditlogs/audit.routes.js @@ -16,13 +16,14 @@ export default { children: [ { path: '', - name: 'auditlogs_wrapper', redirect: 'list', }, { path: 'list', name: 'auditlogs_list', - roles: ['administrator'], + meta: { + permissions: ['administrator'], + }, component: AuditLogsHome, }, ], diff --git a/app/javascript/dashboard/routes/dashboard/settings/automation/automation.routes.js b/app/javascript/dashboard/routes/dashboard/settings/automation/automation.routes.js index 3a5d6c887..14b03a45a 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/automation/automation.routes.js +++ b/app/javascript/dashboard/routes/dashboard/settings/automation/automation.routes.js @@ -15,14 +15,15 @@ export default { children: [ { path: '', - name: 'automation_wrapper', redirect: 'list', }, { path: 'list', name: 'automation_list', component: Automation, - roles: ['administrator'], + meta: { + permissions: ['administrator'], + }, }, ], }, diff --git a/app/javascript/dashboard/routes/dashboard/settings/billing/billing.routes.js b/app/javascript/dashboard/routes/dashboard/settings/billing/billing.routes.js index 7a37f320a..f1042e81a 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/billing/billing.routes.js +++ b/app/javascript/dashboard/routes/dashboard/settings/billing/billing.routes.js @@ -6,7 +6,9 @@ export default { routes: [ { path: frontendURL('accounts/:accountId/settings/billing'), - roles: ['administrator'], + meta: { + permissions: ['administrator'], + }, component: SettingsContent, props: { headerTitle: 'BILLING_SETTINGS.TITLE', @@ -18,7 +20,9 @@ export default { path: '', name: 'billing_settings_index', component: Index, - roles: ['administrator'], + meta: { + permissions: ['administrator'], + }, }, ], }, diff --git a/app/javascript/dashboard/routes/dashboard/settings/campaigns/campaigns.routes.js b/app/javascript/dashboard/routes/dashboard/settings/campaigns/campaigns.routes.js index 1ac007ed6..5c8581981 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/campaigns/campaigns.routes.js +++ b/app/javascript/dashboard/routes/dashboard/settings/campaigns/campaigns.routes.js @@ -19,7 +19,9 @@ export default { { path: 'ongoing', name: 'ongoing_campaigns', - roles: ['administrator'], + meta: { + permissions: ['administrator'], + }, component: Index, }, ], @@ -35,7 +37,9 @@ export default { { path: 'one_off', name: 'one_off', - roles: ['administrator'], + meta: { + permissions: ['administrator'], + }, component: Index, }, ], diff --git a/app/javascript/dashboard/routes/dashboard/settings/canned/canned.routes.js b/app/javascript/dashboard/routes/dashboard/settings/canned/canned.routes.js index 73b370e2a..236801c8c 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/canned/canned.routes.js +++ b/app/javascript/dashboard/routes/dashboard/settings/canned/canned.routes.js @@ -16,13 +16,14 @@ export default { children: [ { path: '', - name: 'canned_wrapper', redirect: 'list', }, { path: 'list', name: 'canned_list', - roles: ['administrator', 'agent'], + meta: { + permissions: ['administrator', 'agent'], + }, component: CannedHome, }, ], diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/inbox.routes.js b/app/javascript/dashboard/routes/dashboard/settings/inbox/inbox.routes.js index 4e4c14c00..3866b96bd 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/inbox/inbox.routes.js +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/inbox.routes.js @@ -28,14 +28,15 @@ export default { children: [ { path: '', - name: 'settings_inbox', redirect: 'list', }, { path: 'list', name: 'settings_inbox_list', component: InboxHome, - roles: ['administrator'], + meta: { + permissions: ['administrator'], + }, }, { path: 'new', @@ -45,19 +46,25 @@ export default { path: '', name: 'settings_inbox_new', component: ChannelList, - roles: ['administrator'], + meta: { + permissions: ['administrator'], + }, }, { path: ':inbox_id/finish', name: 'settings_inbox_finish', component: FinishSetup, - roles: ['administrator'], + meta: { + permissions: ['administrator'], + }, }, { path: ':sub_page', name: 'settings_inboxes_page_channel', component: channelFactory.create(), - roles: ['administrator'], + meta: { + permissions: ['administrator'], + }, props: route => { return { channel_name: route.params.sub_page }; }, @@ -65,7 +72,9 @@ export default { { path: ':inbox_id/agents', name: 'settings_inboxes_add_agents', - roles: ['administrator'], + meta: { + permissions: ['administrator'], + }, component: AddAgents, }, ], @@ -74,7 +83,9 @@ export default { path: ':inboxId', name: 'settings_inbox_show', component: Settings, - roles: ['administrator'], + meta: { + permissions: ['administrator'], + }, }, ], }, diff --git a/app/javascript/dashboard/routes/dashboard/settings/integrationapps/integrations.routes.js b/app/javascript/dashboard/routes/dashboard/settings/integrationapps/integrations.routes.js index a0f8477b2..d3074f62d 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/integrationapps/integrations.routes.js +++ b/app/javascript/dashboard/routes/dashboard/settings/integrationapps/integrations.routes.js @@ -26,13 +26,17 @@ export default { path: '', name: 'settings_applications', component: Index, - roles: ['administrator'], + meta: { + permissions: ['administrator'], + }, }, { path: ':integration_id', name: 'settings_applications_integration', component: IntegrationHooks, - roles: ['administrator'], + meta: { + permissions: ['administrator'], + }, props: route => ({ integrationId: route.params.integration_id, }), diff --git a/app/javascript/dashboard/routes/dashboard/settings/integrations/integrations.routes.js b/app/javascript/dashboard/routes/dashboard/settings/integrations/integrations.routes.js index 542cf591e..bdebc0329 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/integrations/integrations.routes.js +++ b/app/javascript/dashboard/routes/dashboard/settings/integrations/integrations.routes.js @@ -30,32 +30,42 @@ export default { path: '', name: 'settings_integrations', component: Index, - roles: ['administrator'], + meta: { + permissions: ['administrator'], + }, }, { path: 'webhook', component: Webhook, name: 'settings_integrations_webhook', - roles: ['administrator'], + meta: { + permissions: ['administrator'], + }, }, { path: 'dashboard-apps', component: DashboardApps, name: 'settings_integrations_dashboard_apps', - roles: ['administrator'], + meta: { + permissions: ['administrator'], + }, }, { path: 'slack', name: 'settings_integrations_slack', component: Slack, - roles: ['administrator'], + meta: { + permissions: ['administrator'], + }, props: route => ({ code: route.query.code }), }, { path: ':integration_id', name: 'settings_integrations_integration', component: ShowIntegration, - roles: ['administrator'], + meta: { + permissions: ['administrator'], + }, props: route => { return { integrationId: route.params.integration_id, diff --git a/app/javascript/dashboard/routes/dashboard/settings/labels/labels.routes.js b/app/javascript/dashboard/routes/dashboard/settings/labels/labels.routes.js index 4fc514da6..088c565d0 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/labels/labels.routes.js +++ b/app/javascript/dashboard/routes/dashboard/settings/labels/labels.routes.js @@ -17,13 +17,17 @@ export default { { path: '', name: 'labels_wrapper', - roles: ['administrator'], + meta: { + permissions: ['administrator'], + }, redirect: 'list', }, { path: 'list', name: 'labels_list', - roles: ['administrator'], + meta: { + permissions: ['administrator'], + }, component: Index, }, ], diff --git a/app/javascript/dashboard/routes/dashboard/settings/macros/macros.routes.js b/app/javascript/dashboard/routes/dashboard/settings/macros/macros.routes.js index 58a26819f..06d0014b9 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/macros/macros.routes.js +++ b/app/javascript/dashboard/routes/dashboard/settings/macros/macros.routes.js @@ -23,19 +23,25 @@ export default { path: '', name: 'macros_wrapper', component: Macros, - roles: ['administrator', 'agent'], + meta: { + permissions: ['administrator', 'agent'], + }, }, { path: 'new', name: 'macros_new', component: MacroEditor, - roles: ['administrator', 'agent'], + meta: { + permissions: ['administrator', 'agent'], + }, }, { path: ':macroId/edit', name: 'macros_edit', component: MacroEditor, - roles: ['administrator', 'agent'], + meta: { + permissions: ['administrator', 'agent'], + }, }, ], }, 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 357c20e85..31f386e37 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/profile/profile.routes.js +++ b/app/javascript/dashboard/routes/dashboard/settings/profile/profile.routes.js @@ -8,14 +8,18 @@ export default { { path: frontendURL('accounts/:accountId/profile'), name: 'profile_settings', - roles: ['administrator', 'agent'], + meta: { + permissions: ['administrator', 'agent'], + }, component: SettingsContent, children: [ { path: 'settings', name: 'profile_settings_index', component: Index, - roles: ['administrator', 'agent'], + meta: { + permissions: ['administrator', 'agent'], + }, }, ], }, diff --git a/app/javascript/dashboard/routes/dashboard/settings/reports/reports.routes.js b/app/javascript/dashboard/routes/dashboard/settings/reports/reports.routes.js index 3792567da..eaca0e76d 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/reports/reports.routes.js +++ b/app/javascript/dashboard/routes/dashboard/settings/reports/reports.routes.js @@ -29,7 +29,9 @@ export default { { path: 'overview', name: 'account_overview_reports', - roles: ['administrator'], + meta: { + permissions: ['administrator'], + }, component: LiveReports, }, ], @@ -46,7 +48,9 @@ export default { { path: 'conversation', name: 'conversation_reports', - roles: ['administrator'], + meta: { + permissions: ['administrator'], + }, component: Index, }, ], @@ -63,7 +67,9 @@ export default { { path: 'csat', name: 'csat_reports', - roles: ['administrator'], + meta: { + permissions: ['administrator'], + }, component: CsatResponses, }, ], @@ -80,7 +86,9 @@ export default { { path: 'bot', name: 'bot_reports', - roles: ['administrator'], + meta: { + permissions: ['administrator'], + }, component: BotReports, }, ], @@ -97,7 +105,9 @@ export default { { path: 'agent', name: 'agent_reports', - roles: ['administrator'], + meta: { + permissions: ['administrator'], + }, component: AgentReports, }, ], @@ -114,7 +124,9 @@ export default { { path: 'label', name: 'label_reports', - roles: ['administrator'], + meta: { + permissions: ['administrator'], + }, component: LabelReports, }, ], @@ -131,7 +143,9 @@ export default { { path: 'inboxes', name: 'inbox_reports', - roles: ['administrator'], + meta: { + permissions: ['administrator'], + }, component: InboxReports, }, ], @@ -147,7 +161,9 @@ export default { { path: 'teams', name: 'team_reports', - roles: ['administrator'], + meta: { + permissions: ['administrator'], + }, component: TeamReports, }, ], @@ -164,7 +180,9 @@ export default { { path: 'sla', name: 'sla_reports', - roles: ['administrator'], + meta: { + permissions: ['administrator'], + }, component: SLAReports, }, ], diff --git a/app/javascript/dashboard/routes/dashboard/settings/settings.routes.js b/app/javascript/dashboard/routes/dashboard/settings/settings.routes.js index dd5291fe8..2829ed1d9 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/settings.routes.js +++ b/app/javascript/dashboard/routes/dashboard/settings/settings.routes.js @@ -24,7 +24,9 @@ export default { { path: frontendURL('accounts/:accountId/settings'), name: 'settings_home', - roles: ['administrator', 'agent'], + meta: { + permissions: ['administrator', 'agent'], + }, redirect: () => { if (store.getters.getCurrentRole === 'administrator') { return frontendURL('accounts/:accountId/settings/general'); diff --git a/app/javascript/dashboard/routes/dashboard/settings/sla/sla.routes.js b/app/javascript/dashboard/routes/dashboard/settings/sla/sla.routes.js index a9c68470a..762d85d58 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/sla/sla.routes.js +++ b/app/javascript/dashboard/routes/dashboard/settings/sla/sla.routes.js @@ -13,13 +13,17 @@ export default { { path: '', name: 'sla_wrapper', - roles: ['administrator'], + meta: { + permissions: ['administrator'], + }, redirect: 'list', }, { path: 'list', name: 'sla_list', - roles: ['administrator'], + meta: { + permissions: ['administrator'], + }, component: Index, }, ], diff --git a/app/javascript/dashboard/routes/dashboard/settings/teams/teams.routes.js b/app/javascript/dashboard/routes/dashboard/settings/teams/teams.routes.js index 4228e72e7..c968351f4 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/teams/teams.routes.js +++ b/app/javascript/dashboard/routes/dashboard/settings/teams/teams.routes.js @@ -29,14 +29,15 @@ export default { children: [ { path: '', - name: 'settings_teams', redirect: 'list', }, { path: 'list', name: 'settings_teams_list', component: TeamsHome, - roles: ['administrator'], + meta: { + permissions: ['administrator'], + }, }, { path: 'new', @@ -46,18 +47,24 @@ export default { path: '', name: 'settings_teams_new', component: CreateTeam, - roles: ['administrator'], + meta: { + permissions: ['administrator'], + }, }, { path: ':teamId/finish', name: 'settings_teams_finish', component: FinishSetup, - roles: ['administrator'], + meta: { + permissions: ['administrator'], + }, }, { path: ':teamId/agents', name: 'settings_teams_add_agents', - roles: ['administrator'], + meta: { + permissions: ['administrator'], + }, component: AddAgents, }, ], @@ -70,18 +77,24 @@ export default { path: '', name: 'settings_teams_edit', component: EditTeam, - roles: ['administrator'], + meta: { + permissions: ['administrator'], + }, }, { path: 'agents', name: 'settings_teams_edit_members', component: EditAgents, - roles: ['administrator'], + meta: { + permissions: ['administrator'], + }, }, { path: 'finish', name: 'settings_teams_edit_finish', - roles: ['administrator'], + meta: { + permissions: ['administrator'], + }, component: FinishSetup, }, ], diff --git a/app/javascript/dashboard/routes/index.js b/app/javascript/dashboard/routes/index.js index e2245e20f..70d870700 100644 --- a/app/javascript/dashboard/routes/index.js +++ b/app/javascript/dashboard/routes/index.js @@ -5,33 +5,12 @@ import dashboard from './dashboard/dashboard.routes'; import store from '../store'; import { validateLoggedInRoutes } from '../helper/routeHelpers'; import AnalyticsHelper from '../helper/AnalyticsHelper'; +import { buildPermissionsFromRouter } from '../helper/permissionsHelper'; const routes = [...dashboard.routes]; -window.roleWiseRoutes = { - agent: [], - administrator: [], -}; - -// generateRoleWiseRoute - updates window object with agent/admin route -const generateRoleWiseRoute = route => { - route.forEach(element => { - if (element.children) { - generateRoleWiseRoute(element.children); - } - if (element.roles) { - element.roles.forEach(roleEl => { - window.roleWiseRoutes[roleEl].push(element.name); - }); - } - }); -}; -// Create a object of routes -// accessible by each role. -// returns an object with roles as keys and routeArr as values -generateRoleWiseRoute(routes); - export const router = new VueRouter({ mode: 'history', routes }); +export const routesWithPermissions = buildPermissionsFromRouter(routes); export const validateAuthenticateRoutePermission = (to, next, { getters }) => { const { isLoggedIn, getCurrentUser: user } = getters; @@ -45,11 +24,7 @@ export const validateAuthenticateRoutePermission = (to, next, { getters }) => { return next(frontendURL(`accounts/${user.account_id}/dashboard`)); } - const nextRoute = validateLoggedInRoutes( - to, - getters.getCurrentUser, - window.roleWiseRoutes - ); + const nextRoute = validateLoggedInRoutes(to, getters.getCurrentUser); return nextRoute ? next(frontendURL(nextRoute)) : next(); }; From aaf47b4c1fc20e787cd088b4bd2efc7fb092efcb Mon Sep 17 00:00:00 2001 From: Sojan Jose Date: Wed, 3 Jul 2024 15:48:57 -0700 Subject: [PATCH 19/85] chore: [Snyk] Security upgrade sidekiq from 7.2.4 to 7.3.0 (#9710) Upgrade gems to mitigate vulnerabilities. Co-authored-by: snyk-bot --- Gemfile | 2 +- Gemfile.lock | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Gemfile b/Gemfile index 5e1e5917f..ad66c46c6 100644 --- a/Gemfile +++ b/Gemfile @@ -116,7 +116,7 @@ gem 'sentry-ruby', require: false gem 'sentry-sidekiq', '>= 5.18.0', require: false ##-- background job processing --## -gem 'sidekiq', '>= 7.2.4' +gem 'sidekiq', '>= 7.3.0' # We want cron jobs gem 'sidekiq-cron', '>= 1.12.0' diff --git a/Gemfile.lock b/Gemfile.lock index 04a8fb534..54226c13a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -434,6 +434,7 @@ GEM llhttp-ffi (0.4.0) ffi-compiler (~> 1.0) rake (~> 13.0) + logger (1.6.0) lograge (0.14.0) actionpack (>= 4) activesupport (>= 4) @@ -715,11 +716,12 @@ GEM sexp_processor (4.17.0) shoulda-matchers (5.3.0) activesupport (>= 5.2.0) - sidekiq (7.2.4) + sidekiq (7.3.0) concurrent-ruby (< 2) connection_pool (>= 2.3.0) + logger rack (>= 2.2.4) - redis-client (>= 0.19.0) + redis-client (>= 0.22.2) sidekiq-cron (1.12.0) fugit (~> 1.8) globalid (>= 1.0.1) @@ -935,7 +937,7 @@ DEPENDENCIES sentry-ruby sentry-sidekiq (>= 5.18.0) shoulda-matchers - sidekiq (>= 7.2.4) + sidekiq (>= 7.3.0) sidekiq-cron (>= 1.12.0) simplecov (= 0.17.1) slack-ruby-client (~> 2.2.0) From 6ae606c981ffdbf5f711de28fb7e587fe5d0da20 Mon Sep 17 00:00:00 2001 From: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Date: Thu, 4 Jul 2024 13:13:03 +0530 Subject: [PATCH 20/85] fix: Custom snooze is not working in mobile view (#9717) # Pull Request Template ## Description Currently, when a user navigates to a chat and attempts to access the custom snooze modal, it is not visible, making it unable to set custom snooze options. With this fix, the custom snooze modal will correctly display even when a chat is open in mobile view. **Cause of this issue** The `` component is added to the `` component. To accommodate small screen views, we are using the expanded view. However, if we open a chat and select the custom snooze option from the chat header in the message view, the `` component is hidden in the `` component. **Solution** So, I moved the `` to the wrapper component `` so we can use in all cases like, 1. Right-click to custom snooze 2. CMD bar custom snooze 3. Small screen custom snooze --- .../dashboard/components/ChatList.vue | 58 ----------------- .../conversation/ConversationView.vue | 62 ++++++++++++++++++- 2 files changed, 60 insertions(+), 60 deletions(-) diff --git a/app/javascript/dashboard/components/ChatList.vue b/app/javascript/dashboard/components/ChatList.vue index eb410f7d5..ee5354027 100644 --- a/app/javascript/dashboard/components/ChatList.vue +++ b/app/javascript/dashboard/components/ChatList.vue @@ -111,15 +111,6 @@ @updateFolder="onUpdateSavedFilter" /> - - -
    @@ -152,10 +143,6 @@ 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 { @@ -170,7 +157,6 @@ export default { ConversationBulkActions, IntersectionObserver, VirtualList, - CustomSnoozeModal, }, mixins: [ timeMixin, @@ -247,7 +233,6 @@ 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. @@ -283,7 +268,6 @@ export default { campaigns: 'campaigns/getAllCampaigns', labels: 'labels/getLabels', selectedConversations: 'bulkActions/getSelectedConversationIds', - contextMenuChatId: 'getContextMenuChatId', }), hasAppliedFilters() { return this.appliedFilters.length !== 0; @@ -517,11 +501,6 @@ export default { this.$emitter.on('fetch_conversation_stats', () => { this.$store.dispatch('conversationStats/get', this.conversationFilters); }); - - this.$emitter.on(CMD_SNOOZE_CONVERSATION, this.onCmdSnoozeConversation); - }, - beforeDestroy() { - this.$emitter.off(CMD_SNOOZE_CONVERSATION, this.onCmdSnoozeConversation); }, methods: { updateVirtualListProps(key, value) { @@ -999,43 +978,6 @@ export default { 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/routes/dashboard/conversation/ConversationView.vue b/app/javascript/dashboard/routes/dashboard/conversation/ConversationView.vue index 2235b38f0..cd3cdfb80 100644 --- a/app/javascript/dashboard/routes/dashboard/conversation/ConversationView.vue +++ b/app/javascript/dashboard/routes/dashboard/conversation/ConversationView.vue @@ -22,25 +22,40 @@ :is-on-expanded-layout="isOnExpandedLayout" @contact-panel-toggle="onToggleContactPanel" /> + + + From 31bcdaa3ddfdb43fae1118f4ef5d6123af42a73c Mon Sep 17 00:00:00 2001 From: Sojan Jose Date: Thu, 4 Jul 2024 23:50:07 -0700 Subject: [PATCH 21/85] chore: Upgrade ruby to 3.3.3 (#9664) - Upgrade the ruby version to 3.3.3 --------- Co-authored-by: Vishnu Narayanan --- .circleci/config.yml | 2 +- .devcontainer/docker-compose.yml | 4 ++-- .ruby-version | 2 +- Gemfile | 2 +- Gemfile.lock | 31 +++++++++++++++------------- deployment/chatwoot-web.1.service | 6 +++--- deployment/chatwoot-worker.1.service | 6 +++--- deployment/setup_20.04.sh | 4 ++-- docker/Dockerfile | 10 ++++----- 9 files changed, 35 insertions(+), 32 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index c67063ae6..cdddc4a06 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -7,7 +7,7 @@ defaults: &defaults working_directory: ~/build docker: # specify the version you desire here - - image: cimg/ruby:3.2.2-browsers + - image: cimg/ruby:3.3.3-browsers # Specify service dependencies here if necessary # CircleCI maintains a library of pre-built images diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml index 17021d1e7..a804bb15c 100644 --- a/.devcontainer/docker-compose.yml +++ b/.devcontainer/docker-compose.yml @@ -12,7 +12,7 @@ services: args: VARIANT: "ubuntu-22.04" NODE_VERSION: "20.9.0" - RUBY_VERSION: "3.2.2" + RUBY_VERSION: "3.3.3" # On Linux, you may need to update USER_UID and USER_GID below if not your local UID is not 1000. USER_UID: "1000" USER_GID: "1000" @@ -25,7 +25,7 @@ services: args: VARIANT: "ubuntu-22.04" NODE_VERSION: "20.9.0" - RUBY_VERSION: "3.2.2" + RUBY_VERSION: "3.3.3" # On Linux, you may need to update USER_UID and USER_GID below if not your local UID is not 1000. USER_UID: "1000" USER_GID: "1000" diff --git a/.ruby-version b/.ruby-version index be94e6f53..619b53766 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -3.2.2 +3.3.3 diff --git a/Gemfile b/Gemfile index ad66c46c6..d7bfb6258 100644 --- a/Gemfile +++ b/Gemfile @@ -1,6 +1,6 @@ source 'https://rubygems.org' -ruby '3.2.2' +ruby '3.3.3' ##-- base gems for rails --## gem 'rack-cors', '2.0.0', require: 'rack/cors' diff --git a/Gemfile.lock b/Gemfile.lock index 54226c13a..23da215bc 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -183,13 +183,16 @@ GEM activerecord (>= 5.a) database_cleaner-core (~> 2.0.0) database_cleaner-core (2.0.1) - date (3.3.4) - ddtrace (1.11.1) - debase-ruby_core_source (>= 0.10.16, <= 3.2.0) - libdatadog (~> 2.0.0.1.0) - libddwaf (~> 1.8.2.0.0) + datadog-ci (0.8.3) msgpack - debase-ruby_core_source (3.2.0) + date (3.3.4) + ddtrace (1.23.2) + datadog-ci (~> 0.8.1) + debase-ruby_core_source (= 3.3.1) + libdatadog (~> 7.0.0.1.0) + libddwaf (~> 1.14.0.0.0) + msgpack + debase-ruby_core_source (3.3.1) debug (1.8.0) irb (>= 1.5.0) reline (>= 0.3.1) @@ -416,15 +419,15 @@ GEM addressable (~> 2.8) letter_opener (1.8.1) launchy (>= 2.2, < 3) - libdatadog (2.0.0.1.0) - libdatadog (2.0.0.1.0-x86_64-linux) - libddwaf (1.8.2.0.0) + libdatadog (7.0.0.1.0) + libdatadog (7.0.0.1.0-x86_64-linux) + libddwaf (1.14.0.0.0) ffi (~> 1.0) - libddwaf (1.8.2.0.0-arm64-darwin) + libddwaf (1.14.0.0.0-arm64-darwin) ffi (~> 1.0) - libddwaf (1.8.2.0.0-x86_64-darwin) + libddwaf (1.14.0.0.0-x86_64-darwin) ffi (~> 1.0) - libddwaf (1.8.2.0.0-x86_64-linux) + libddwaf (1.14.0.0.0-x86_64-linux) ffi (~> 1.0) line-bot-api (1.28.0) liquid (5.4.0) @@ -962,7 +965,7 @@ DEPENDENCIES working_hours RUBY VERSION - ruby 3.2.2p185 + ruby 3.3.3p89 BUNDLED WITH - 2.4.6 + 2.5.14 diff --git a/deployment/chatwoot-web.1.service b/deployment/chatwoot-web.1.service index 37889b1aa..049ec85db 100644 --- a/deployment/chatwoot-web.1.service +++ b/deployment/chatwoot-web.1.service @@ -16,10 +16,10 @@ KillMode=mixed StandardInput=null SyslogIdentifier=%p -Environment="PATH=/home/chatwoot/.rvm/gems/ruby-3.2.2/bin:/home/chatwoot/.rvm/gems/ruby-3.2.2@global/bin:/home/chatwoot/.rvm/rubies/ruby-3.2.2/bin:/home/chatwoot/.rvm/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin:/home/chatwoot/.rvm/bin:/home/chatwoot/.rvm/bin" +Environment="PATH=/home/chatwoot/.rvm/gems/ruby-3.3.3/bin:/home/chatwoot/.rvm/gems/ruby-3.3.3@global/bin:/home/chatwoot/.rvm/rubies/ruby-3.3.3/bin:/home/chatwoot/.rvm/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin:/home/chatwoot/.rvm/bin:/home/chatwoot/.rvm/bin" Environment="PORT=3000" Environment="RAILS_ENV=production" Environment="NODE_ENV=production" Environment="RAILS_LOG_TO_STDOUT=true" -Environment="GEM_HOME=/home/chatwoot/.rvm/gems/ruby-3.2.2" -Environment="GEM_PATH=/home/chatwoot/.rvm/gems/ruby-3.2.2:/home/chatwoot/.rvm/gems/ruby-3.2.2@global" +Environment="GEM_HOME=/home/chatwoot/.rvm/gems/ruby-3.3.3" +Environment="GEM_PATH=/home/chatwoot/.rvm/gems/ruby-3.3.3:/home/chatwoot/.rvm/gems/ruby-3.3.3@global" diff --git a/deployment/chatwoot-worker.1.service b/deployment/chatwoot-worker.1.service index 8000743c4..cbb97e88c 100644 --- a/deployment/chatwoot-worker.1.service +++ b/deployment/chatwoot-worker.1.service @@ -16,10 +16,10 @@ KillMode=mixed StandardInput=null SyslogIdentifier=%p -Environment="PATH=/home/chatwoot/.rvm/gems/ruby-3.2.2/bin:/home/chatwoot/.rvm/gems/ruby-3.2.2@global/bin:/home/chatwoot/.rvm/rubies/ruby-3.2.2/bin:/home/chatwoot/.rvm/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin:/home/chatwoot/.rvm/bin:/home/chatwoot/.rvm/bin" +Environment="PATH=/home/chatwoot/.rvm/gems/ruby-3.3.3/bin:/home/chatwoot/.rvm/gems/ruby-3.3.3@global/bin:/home/chatwoot/.rvm/rubies/ruby-3.3.3/bin:/home/chatwoot/.rvm/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin:/home/chatwoot/.rvm/bin:/home/chatwoot/.rvm/bin" Environment="PORT=3000" Environment="RAILS_ENV=production" Environment="NODE_ENV=production" Environment="RAILS_LOG_TO_STDOUT=true" -Environment="GEM_HOME=/home/chatwoot/.rvm/gems/ruby-3.2.2" -Environment="GEM_PATH=/home/chatwoot/.rvm/gems/ruby-3.2.2:/home/chatwoot/.rvm/gems/ruby-3.2.2@global" +Environment="GEM_HOME=/home/chatwoot/.rvm/gems/ruby-3.3.3" +Environment="GEM_PATH=/home/chatwoot/.rvm/gems/ruby-3.3.3:/home/chatwoot/.rvm/gems/ruby-3.3.3@global" diff --git a/deployment/setup_20.04.sh b/deployment/setup_20.04.sh index 8918e9d54..2f8d83f8b 100644 --- a/deployment/setup_20.04.sh +++ b/deployment/setup_20.04.sh @@ -336,8 +336,8 @@ function setup_chatwoot() { sudo -i -u chatwoot << EOF rvm --version rvm autolibs disable - rvm install "ruby-3.2.2" - rvm use 3.2.2 --default + rvm install "ruby-3.3.3" + rvm use 3.3.3 --default git clone https://github.com/chatwoot/chatwoot.git cd chatwoot diff --git a/docker/Dockerfile b/docker/Dockerfile index 6503530b7..9dcc73ad9 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,5 +1,5 @@ # pre-build stage -FROM ruby:3.2.2-alpine3.18 AS pre-builder +FROM ruby:3.3.3-alpine3.19 AS pre-builder # ARG default to production settings # For development docker-compose file overrides ARGS @@ -25,7 +25,7 @@ RUN apk update && apk add --no-cache \ tzdata \ postgresql-dev \ postgresql-client \ - nodejs-current \ + nodejs=20.12.1-r0 \ yarn \ git \ && mkdir -p /var/app \ @@ -67,13 +67,13 @@ RUN if [ "$RAILS_ENV" = "production" ]; then \ RUN git rev-parse HEAD > /app/.git_sha # Remove unnecessary files -RUN rm -rf /gems/ruby/3.2.0/cache/*.gem \ - && find /gems/ruby/3.2.0/gems/ \( -name "*.c" -o -name "*.o" \) -delete \ +RUN rm -rf /gems/ruby/3.3.0/cache/*.gem \ + && find /gems/ruby/3.3.0/gems/ \( -name "*.c" -o -name "*.o" \) -delete \ && rm -rf .git \ && rm .gitignore # final build stage -FROM ruby:3.2.2-alpine3.18 +FROM ruby:3.3.3-alpine3.19 ARG BUNDLE_WITHOUT="development:test" From fe246698b6dcd0d0164accb9f5f57760b381591a Mon Sep 17 00:00:00 2001 From: Sojan Jose Date: Mon, 8 Jul 2024 13:00:54 -0500 Subject: [PATCH 22/85] chore: [Snyk] Fix for 1 vulnerabilities (#9720) - updates for security vulnerabilities Co-authored-by: snyk-bot --- Gemfile | 4 ++-- Gemfile.lock | 16 ++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Gemfile b/Gemfile index d7bfb6258..f690e6876 100644 --- a/Gemfile +++ b/Gemfile @@ -111,9 +111,9 @@ gem 'elastic-apm', require: false gem 'newrelic_rpm', require: false gem 'newrelic-sidekiq-metrics', '>= 1.6.2', require: false gem 'scout_apm', require: false -gem 'sentry-rails', '>= 5.18.0', require: false +gem 'sentry-rails', '>= 5.18.1', require: false gem 'sentry-ruby', require: false -gem 'sentry-sidekiq', '>= 5.18.0', require: false +gem 'sentry-sidekiq', '>= 5.18.1', require: false ##-- background job processing --## gem 'sidekiq', '>= 7.3.0' diff --git a/Gemfile.lock b/Gemfile.lock index 23da215bc..98a1e9345 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -464,7 +464,7 @@ GEM mini_magick (4.12.0) mini_mime (1.1.5) mini_portile2 (2.8.7) - minitest (5.24.0) + minitest (5.24.1) mock_redis (0.36.0) ruby2_keywords msgpack (1.7.0) @@ -707,14 +707,14 @@ GEM activesupport (>= 4) selectize-rails (0.12.6) semantic_range (3.0.0) - sentry-rails (5.18.0) + sentry-rails (5.18.1) railties (>= 5.0) - sentry-ruby (~> 5.18.0) - sentry-ruby (5.18.0) + sentry-ruby (~> 5.18.1) + sentry-ruby (5.18.1) bigdecimal concurrent-ruby (~> 1.0, >= 1.0.2) - sentry-sidekiq (5.18.0) - sentry-ruby (~> 5.18.0) + sentry-sidekiq (5.18.1) + sentry-ruby (~> 5.18.1) sidekiq (>= 3.0) sexp_processor (4.17.0) shoulda-matchers (5.3.0) @@ -936,9 +936,9 @@ DEPENDENCIES scout_apm scss_lint seed_dump - sentry-rails (>= 5.18.0) + sentry-rails (>= 5.18.1) sentry-ruby - sentry-sidekiq (>= 5.18.0) + sentry-sidekiq (>= 5.18.1) shoulda-matchers sidekiq (>= 7.3.0) sidekiq-cron (>= 1.12.0) From a2cb932d541a71b0384fb9c6df95c5ce5143929a Mon Sep 17 00:00:00 2001 From: Pranav Date: Tue, 9 Jul 2024 09:03:05 -0700 Subject: [PATCH 23/85] chore: Upgrade csv-safe to the latest version (#9739) The following error starting is shown on the console after the ruby upgrade. csv.rb was loaded from the standard library, but will no longer be part of the default gems since Ruby 3.4.0. Add csv to your Gemfile or gemspec. Also contact author of csv-safe-3.2.1 to add csv into its gemspec. Csv-safe has already added a patch via https://github.com/zvory/csv-safe/pull/20. This PR updates the version to the latest version of csv-safe (3.3.1) --- Gemfile.lock | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 98a1e9345..91d3b3d8b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -174,7 +174,9 @@ GEM crack (0.4.5) rexml crass (1.0.6) - csv-safe (3.2.1) + csv (3.3.0) + csv-safe (3.3.1) + csv (~> 3.0) cypress-on-rails (1.16.0) rack database_cleaner (2.0.2) From 9498d1f0036f6e08fe47eb7448a65e8a0a307719 Mon Sep 17 00:00:00 2001 From: wudi Date: Wed, 10 Jul 2024 13:16:45 +0800 Subject: [PATCH 24/85] fix: Localize 'Social Profiles' text in ContactForm (#9745) --- .../routes/dashboard/conversation/contact/ContactForm.vue | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/javascript/dashboard/routes/dashboard/conversation/contact/ContactForm.vue b/app/javascript/dashboard/routes/dashboard/conversation/contact/ContactForm.vue index b958d4211..cf4028976 100644 --- a/app/javascript/dashboard/routes/dashboard/conversation/contact/ContactForm.vue +++ b/app/javascript/dashboard/routes/dashboard/conversation/contact/ContactForm.vue @@ -117,7 +117,7 @@ />
    - +
    From 9de8c27368ba4da91f4416403108934f257cea9c Mon Sep 17 00:00:00 2001 From: Pranav Date: Wed, 10 Jul 2024 08:32:16 -0700 Subject: [PATCH 25/85] feat: Use vitest instead of jest, run all the specs anywhere in app/ folder in the CI (#9722) Due to the pattern `**/specs/*.spec.js` defined in CircleCI, none of the frontend spec in the folders such as `specs//getters.spec.js` were not executed in Circle CI. This PR fixes the issue, along with the following changes: - Use vitest instead of jest - Remove jest dependancies - Update tests to work with vitest --------- Co-authored-by: Muhsin Keloth --- .circleci/config.yml | 5 +- .eslintrc.js | 2 +- app/javascript/dashboard/App.Vue.spec.js | 11 - .../api/enterprise/specs/account.spec.js | 8 +- .../dashboard/api/specs/account.spec.js | 8 +- .../api/specs/accountActions.spec.js | 8 +- .../dashboard/api/specs/agents.spec.js | 2 +- .../dashboard/api/specs/article.spec.js | 40 +- .../api/specs/assignableAgents.spec.js | 8 +- .../api/specs/channel/fbChannel.spec.js | 8 +- .../dashboard/api/specs/contacts.spec.js | 8 +- .../dashboard/api/specs/conversations.spec.js | 8 +- .../dashboard/api/specs/csatReports.spec.js | 8 +- .../api/specs/inbox/conversation.spec.js | 8 +- .../dashboard/api/specs/inbox/message.spec.js | 8 +- .../dashboard/api/specs/inboxes.spec.js | 8 +- .../dashboard/api/specs/integrations.spec.js | 8 +- .../api/specs/integrations/dyte.spec.js | 16 +- .../api/specs/integrations/linear.spec.js | 56 +- .../dashboard/api/specs/notifications.spec.js | 8 +- .../dashboard/api/specs/reports.spec.js | 8 +- .../dashboard/api/specs/slaReports.spec.js | 8 +- .../dashboard/api/specs/teams.spec.js | 8 +- .../specs/AccountSelector.spec.js | 8 +- .../specs/AgentDetails.spec.js | 6 +- .../specs/NotificationBell.spec.js | 6 +- .../layout/specs/AvailabilityStatus.spec.js | 21 +- .../components/specs/SidemenuIcon.spec.js | 2 +- .../__snapshots__/SidemenuIcon.spec.js.snap | 4 +- .../helpers/specs/DatePickerHelper.spec.js | 14 +- .../widgets/conversation/ReplyBox.vue | 2 +- .../conversation/specs/MoreActions.spec.js | 24 +- .../mentionSelectionKeyboardMixin.spec.js | 8 +- .../composables/spec/emitter.spec.js | 12 +- .../AnalyticsHelper/specs/helper.spec.js | 16 +- .../AnalyticsHelper/specs/plugin.spec.js | 31 +- .../specs/CacheHelper/DataManger.spec.js | 2 +- .../helper/specs/ReconnectService.spec.js | 62 +- .../dashboard/helper/specs/commons.spec.js | 41 +- .../helper/specs/directives/resize.spec.js | 14 +- .../helper/specs/editorHelper.spec.js | 14 +- .../helper/specs/themeHelper.spec.js | 2 +- .../helper/specs/uploadHelper.spec.js | 3 +- .../dashboard/mixins/specs/aiMixin.spec.js | 18 +- .../mixins/specs/attributeMixin.spec.js | 2 +- .../mixins/specs/fileUploadMixin.spec.js | 24 +- .../dashboard/mixins/specs/time.spec.js | 8 +- .../dashboard/mixins/specs/uiSettings.spec.js | 2 +- .../dashboard/routes/dashboard/Dashboard.vue | 2 +- .../components/specs/AddReminder.spec.js | 6 +- .../conversation/conversation.routes.js | 2 +- .../components/HelpCenterLayout.vue | 5 +- .../dashboard/helpcenter/helpcenter.routes.js | 38 +- .../pages/categories/NameEmojiInput.vue | 2 +- .../settings/inbox/channel-factory.js | 18 +- .../specs/Filters/FiltersAgents.spec.js | 6 +- .../specs/Filters/FiltersDateGroupBy.spec.js | 2 +- .../specs/Filters/FiltersDateRange.spec.js | 2 +- .../specs/Filters/FiltersInboxes.spec.js | 4 +- .../specs/Filters/FiltersLabels.spec.js | 4 +- .../specs/Filters/FiltersRatings.spec.js | 2 +- .../specs/Filters/FiltersTeams.spec.js | 2 +- .../__snapshots__/CSATMetrics.spec.js.snap | 8 +- app/javascript/dashboard/routes/index.spec.js | 31 +- .../helpCenterArticles/specs/action.spec.js | 8 +- .../specs/actions.spec.js | 4 +- .../helpCenterPortals/specs/actions.spec.js | 6 +- .../modules/specs/account/actions.spec.js | 4 +- .../modules/specs/account/getters.spec.js | 10 +- .../modules/specs/agentBots/agentBots.spec.js | 4 +- .../modules/specs/agents/actions.spec.js | 6 +- .../modules/specs/attributes/actions.spec.js | 4 +- .../store/modules/specs/auth/actions.spec.js | 25 +- .../store/modules/specs/auth/getters.spec.js | 3 - .../modules/specs/automations/actions.spec.js | 4 +- .../modules/specs/bulkActions/actions.spec.js | 4 +- .../modules/specs/campaigns/actions.spec.js | 4 +- .../contactConversations/actions.spec.js | 4 +- .../specs/contactLabels/actions.spec.js | 4 +- .../specs/contactNotes/actions.spec.js | 4 +- .../modules/specs/contacts/actions.spec.js | 4 +- .../specs/conversationLabels/actions.spec.js | 4 +- .../specs/conversationPage/actions.spec.js | 2 +- .../specs/conversationSearch/actions.spec.js | 4 +- .../specs/conversationStats/actions.spec.js | 4 +- .../conversationTypingStatus/actions.spec.js | 2 +- .../conversationWatchers/actions.spec.js | 4 +- .../specs/conversations/actions.spec.js | 12 +- .../specs/conversations/mutations.spec.js | 14 +- .../store/modules/specs/csat/actions.spec.js | 4 +- .../modules/specs/customViews/actions.spec.js | 4 +- .../specs/dashboardApps/actions.spec.js | 4 +- .../specs/draftMessages/actions.spec.js | 4 +- .../inboxAssignableMembers/actions.spec.js | 4 +- .../modules/specs/inboxes/actions.spec.js | 6 +- .../specs/integrations/actions.spec.js | 4 +- .../modules/specs/labels/actions.spec.js | 6 +- .../modules/specs/macros/actions.spec.js | 4 +- .../specs/notifications/actions.spec.js | 4 +- .../modules/specs/reports/actions.spec.js | 13 +- .../store/modules/specs/sla/actions.spec.js | 6 +- .../modules/specs/slaReports/actions.spec.js | 4 +- .../modules/specs/teamMembers/actions.spec.js | 4 +- .../store/modules/specs/teams/actions.spec.js | 6 +- .../userNotificationSettings/actions.spec.js | 4 +- .../modules/specs/webhooks/actions.spec.js | 4 +- app/javascript/portal/specs/portal.spec.js | 47 +- .../portal/specs/portalTheme.spec.js | 18 +- .../sdk/specs/cookieHelpers.spec.js | 72 +- .../components/specs/DateSeparator.spec.js | 2 +- .../shared/components/specs/Spinner.spec.js | 2 +- .../__snapshots__/DateSeparator.spec.js.snap | 9 +- .../specs/__snapshots__/Spinner.spec.js.snap | 5 +- .../shared/helpers/specs/CustomErrors.spec.js | 2 +- .../helpers/specs/CustomEventHelper.spec.js | 2 +- .../shared/helpers/specs/mitt.spec.js | 2 +- .../shared/mixins/specs/rtlMixin.spec.js | 29 +- .../v3/helpers/specs/RouteHelper.spec.js | 24 +- .../widget/api/specs/endPoints.spec.js | 44 +- .../widget/components/ChatInputWrap.vue | 2 +- .../helpers/specs/urlParamsHelper.spec.js | 2 +- .../widget/helpers/specs/utils.spec.js | 2 +- .../mixins/specs/availabilityMixin.spec.js | 26 +- .../mixins/specs/nextAvailabilityTime.spec.js | 73 +- .../store/modules/specs/agent/actions.spec.js | 10 +- .../modules/specs/appConfig/actions.spec.js | 2 +- .../modules/specs/article/actions.spec.js | 10 +- .../modules/specs/campaign/actions.spec.js | 12 +- .../modules/specs/campaign/getters.spec.js | 5 +- .../modules/specs/campaign/mutations.spec.js | 5 +- .../modules/specs/contact/actions.spec.js | 18 +- .../specs/conversation/actions.spec.js | 16 +- .../conversationAttributes/actions.spec.js | 4 +- .../modules/specs/message/actions.spec.js | 4 +- app/test-matchers.js | 423 --- jest.config.js | 49 - jest.setup.js | 3 - package.json | 21 +- vite.config.ts | 42 + yarn.lock | 2528 ++++++----------- 140 files changed, 1678 insertions(+), 2810 deletions(-) delete mode 100644 app/javascript/dashboard/App.Vue.spec.js delete mode 100644 app/test-matchers.js delete mode 100644 jest.config.js delete mode 100644 jest.setup.js create mode 100644 vite.config.ts diff --git a/.circleci/config.yml b/.circleci/config.yml index cdddc4a06..15c40dc77 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -130,10 +130,7 @@ jobs: command: | mkdir -p ~/tmp/test-results/frontend_specs ~/tmp/cc-test-reporter before-build - TESTFILES=$(circleci tests glob **/specs/*.spec.js | circleci tests split --split-by=timings) - yarn test:coverage --profile 10 \ - --out ~/tmp/test-results/yarn.xml \ - -- ${TESTFILES} + yarn test:coverage - run: name: Code Climate Test Coverage command: | diff --git a/.eslintrc.js b/.eslintrc.js index 03e7e995b..18162ee76 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -65,10 +65,10 @@ module.exports = { }, env: { browser: true, - jest: true, node: true, }, globals: { bus: true, + vi: true, }, }; diff --git a/app/javascript/dashboard/App.Vue.spec.js b/app/javascript/dashboard/App.Vue.spec.js deleted file mode 100644 index 43c6a74ed..000000000 --- a/app/javascript/dashboard/App.Vue.spec.js +++ /dev/null @@ -1,11 +0,0 @@ -import App from './App'; -import '../../test-matchers'; - -describe(`App component`, () => { - it(`should be a component`, () => { - // Arrange - // Act - expect(App).toBeVueComponent('App'); - // Assert - }); -}); diff --git a/app/javascript/dashboard/api/enterprise/specs/account.spec.js b/app/javascript/dashboard/api/enterprise/specs/account.spec.js index 6c9dca986..4fb1bd0ee 100644 --- a/app/javascript/dashboard/api/enterprise/specs/account.spec.js +++ b/app/javascript/dashboard/api/enterprise/specs/account.spec.js @@ -15,10 +15,10 @@ describe('#enterpriseAccountAPI', () => { 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()), + post: vi.fn(() => Promise.resolve()), + get: vi.fn(() => Promise.resolve()), + patch: vi.fn(() => Promise.resolve()), + delete: vi.fn(() => Promise.resolve()), }; beforeEach(() => { diff --git a/app/javascript/dashboard/api/specs/account.spec.js b/app/javascript/dashboard/api/specs/account.spec.js index 7e213b2a8..4da8b3a46 100644 --- a/app/javascript/dashboard/api/specs/account.spec.js +++ b/app/javascript/dashboard/api/specs/account.spec.js @@ -15,10 +15,10 @@ describe('#accountAPI', () => { 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()), + post: vi.fn(() => Promise.resolve()), + get: vi.fn(() => Promise.resolve()), + patch: vi.fn(() => Promise.resolve()), + delete: vi.fn(() => Promise.resolve()), }; beforeEach(() => { diff --git a/app/javascript/dashboard/api/specs/accountActions.spec.js b/app/javascript/dashboard/api/specs/accountActions.spec.js index 330c117ff..dc73e4948 100644 --- a/app/javascript/dashboard/api/specs/accountActions.spec.js +++ b/app/javascript/dashboard/api/specs/accountActions.spec.js @@ -10,10 +10,10 @@ describe('#ContactsAPI', () => { 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()), + post: vi.fn(() => Promise.resolve()), + get: vi.fn(() => Promise.resolve()), + patch: vi.fn(() => Promise.resolve()), + delete: vi.fn(() => Promise.resolve()), }; beforeEach(() => { diff --git a/app/javascript/dashboard/api/specs/agents.spec.js b/app/javascript/dashboard/api/specs/agents.spec.js index 20dd36688..0df0fd8d9 100644 --- a/app/javascript/dashboard/api/specs/agents.spec.js +++ b/app/javascript/dashboard/api/specs/agents.spec.js @@ -14,7 +14,7 @@ describe('#AgentAPI', () => { describe('API calls', () => { const originalAxios = window.axios; const axiosMock = { - post: jest.fn(() => Promise.resolve()), + post: vi.fn(() => Promise.resolve()), }; beforeEach(() => { diff --git a/app/javascript/dashboard/api/specs/article.spec.js b/app/javascript/dashboard/api/specs/article.spec.js index 02c0f82d8..71128682c 100644 --- a/app/javascript/dashboard/api/specs/article.spec.js +++ b/app/javascript/dashboard/api/specs/article.spec.js @@ -14,10 +14,10 @@ describe('#PortalAPI', () => { 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()), + post: vi.fn(() => Promise.resolve()), + get: vi.fn(() => Promise.resolve()), + patch: vi.fn(() => Promise.resolve()), + delete: vi.fn(() => Promise.resolve()), }; beforeEach(() => { @@ -44,10 +44,10 @@ describe('#PortalAPI', () => { 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()), + post: vi.fn(() => Promise.resolve()), + get: vi.fn(() => Promise.resolve()), + patch: vi.fn(() => Promise.resolve()), + delete: vi.fn(() => Promise.resolve()), }; beforeEach(() => { @@ -71,10 +71,10 @@ describe('#PortalAPI', () => { 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()), + post: vi.fn(() => Promise.resolve()), + get: vi.fn(() => Promise.resolve()), + patch: vi.fn(() => Promise.resolve()), + delete: vi.fn(() => Promise.resolve()), }; beforeEach(() => { @@ -98,10 +98,10 @@ describe('#PortalAPI', () => { 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()), + post: vi.fn(() => Promise.resolve()), + get: vi.fn(() => Promise.resolve()), + patch: vi.fn(() => Promise.resolve()), + delete: vi.fn(() => Promise.resolve()), }; beforeEach(() => { @@ -129,10 +129,10 @@ describe('#PortalAPI', () => { 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()), + post: vi.fn(() => Promise.resolve()), + get: vi.fn(() => Promise.resolve()), + patch: vi.fn(() => Promise.resolve()), + delete: vi.fn(() => Promise.resolve()), }; beforeEach(() => { diff --git a/app/javascript/dashboard/api/specs/assignableAgents.spec.js b/app/javascript/dashboard/api/specs/assignableAgents.spec.js index 5280162b3..d553d55cb 100644 --- a/app/javascript/dashboard/api/specs/assignableAgents.spec.js +++ b/app/javascript/dashboard/api/specs/assignableAgents.spec.js @@ -4,10 +4,10 @@ describe('#AssignableAgentsAPI', () => { 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()), + post: vi.fn(() => Promise.resolve()), + get: vi.fn(() => Promise.resolve()), + patch: vi.fn(() => Promise.resolve()), + delete: vi.fn(() => Promise.resolve()), }; beforeEach(() => { diff --git a/app/javascript/dashboard/api/specs/channel/fbChannel.spec.js b/app/javascript/dashboard/api/specs/channel/fbChannel.spec.js index 2cdcec56e..c79051977 100644 --- a/app/javascript/dashboard/api/specs/channel/fbChannel.spec.js +++ b/app/javascript/dashboard/api/specs/channel/fbChannel.spec.js @@ -13,10 +13,10 @@ describe('#FBChannel', () => { 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()), + post: vi.fn(() => Promise.resolve()), + get: vi.fn(() => Promise.resolve()), + patch: vi.fn(() => Promise.resolve()), + delete: vi.fn(() => Promise.resolve()), }; beforeEach(() => { diff --git a/app/javascript/dashboard/api/specs/contacts.spec.js b/app/javascript/dashboard/api/specs/contacts.spec.js index b4eaf7333..0059518b0 100644 --- a/app/javascript/dashboard/api/specs/contacts.spec.js +++ b/app/javascript/dashboard/api/specs/contacts.spec.js @@ -17,10 +17,10 @@ describe('#ContactsAPI', () => { 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()), + post: vi.fn(() => Promise.resolve()), + get: vi.fn(() => Promise.resolve()), + patch: vi.fn(() => Promise.resolve()), + delete: vi.fn(() => Promise.resolve()), }; beforeEach(() => { diff --git a/app/javascript/dashboard/api/specs/conversations.spec.js b/app/javascript/dashboard/api/specs/conversations.spec.js index 6b3db7404..7ae4eb774 100644 --- a/app/javascript/dashboard/api/specs/conversations.spec.js +++ b/app/javascript/dashboard/api/specs/conversations.spec.js @@ -16,10 +16,10 @@ describe('#ConversationApi', () => { 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()), + post: vi.fn(() => Promise.resolve()), + get: vi.fn(() => Promise.resolve()), + patch: vi.fn(() => Promise.resolve()), + delete: vi.fn(() => Promise.resolve()), }; beforeEach(() => { diff --git a/app/javascript/dashboard/api/specs/csatReports.spec.js b/app/javascript/dashboard/api/specs/csatReports.spec.js index 7c1707e1e..788f0eba2 100644 --- a/app/javascript/dashboard/api/specs/csatReports.spec.js +++ b/app/javascript/dashboard/api/specs/csatReports.spec.js @@ -11,10 +11,10 @@ describe('#Reports API', () => { 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()), + post: vi.fn(() => Promise.resolve()), + get: vi.fn(() => Promise.resolve()), + patch: vi.fn(() => Promise.resolve()), + delete: vi.fn(() => Promise.resolve()), }; beforeEach(() => { diff --git a/app/javascript/dashboard/api/specs/inbox/conversation.spec.js b/app/javascript/dashboard/api/specs/inbox/conversation.spec.js index 3287d7477..dd1615802 100644 --- a/app/javascript/dashboard/api/specs/inbox/conversation.spec.js +++ b/app/javascript/dashboard/api/specs/inbox/conversation.spec.js @@ -24,10 +24,10 @@ describe('#ConversationAPI', () => { 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()), + post: vi.fn(() => Promise.resolve()), + get: vi.fn(() => Promise.resolve()), + patch: vi.fn(() => Promise.resolve()), + delete: vi.fn(() => Promise.resolve()), }; beforeEach(() => { diff --git a/app/javascript/dashboard/api/specs/inbox/message.spec.js b/app/javascript/dashboard/api/specs/inbox/message.spec.js index 0d45b2157..941f5c99c 100644 --- a/app/javascript/dashboard/api/specs/inbox/message.spec.js +++ b/app/javascript/dashboard/api/specs/inbox/message.spec.js @@ -15,10 +15,10 @@ describe('#ConversationAPI', () => { 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()), + post: vi.fn(() => Promise.resolve()), + get: vi.fn(() => Promise.resolve()), + patch: vi.fn(() => Promise.resolve()), + delete: vi.fn(() => Promise.resolve()), }; beforeEach(() => { diff --git a/app/javascript/dashboard/api/specs/inboxes.spec.js b/app/javascript/dashboard/api/specs/inboxes.spec.js index 8834ceb07..628ce0f34 100644 --- a/app/javascript/dashboard/api/specs/inboxes.spec.js +++ b/app/javascript/dashboard/api/specs/inboxes.spec.js @@ -17,10 +17,10 @@ describe('#InboxesAPI', () => { 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()), + post: vi.fn(() => Promise.resolve()), + get: vi.fn(() => Promise.resolve()), + patch: vi.fn(() => Promise.resolve()), + delete: vi.fn(() => Promise.resolve()), }; beforeEach(() => { diff --git a/app/javascript/dashboard/api/specs/integrations.spec.js b/app/javascript/dashboard/api/specs/integrations.spec.js index 5ccbda436..cc20fd8f6 100644 --- a/app/javascript/dashboard/api/specs/integrations.spec.js +++ b/app/javascript/dashboard/api/specs/integrations.spec.js @@ -18,10 +18,10 @@ describe('#integrationAPI', () => { 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()), + post: vi.fn(() => Promise.resolve()), + get: vi.fn(() => Promise.resolve()), + patch: vi.fn(() => Promise.resolve()), + delete: vi.fn(() => Promise.resolve()), }; beforeEach(() => { diff --git a/app/javascript/dashboard/api/specs/integrations/dyte.spec.js b/app/javascript/dashboard/api/specs/integrations/dyte.spec.js index 4bbe0484a..1c544f976 100644 --- a/app/javascript/dashboard/api/specs/integrations/dyte.spec.js +++ b/app/javascript/dashboard/api/specs/integrations/dyte.spec.js @@ -11,10 +11,10 @@ describe('#accountAPI', () => { describe('createAMeeting', () => { 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()), + post: vi.fn(() => Promise.resolve()), + get: vi.fn(() => Promise.resolve()), + patch: vi.fn(() => Promise.resolve()), + delete: vi.fn(() => Promise.resolve()), }; beforeEach(() => { @@ -39,10 +39,10 @@ describe('#accountAPI', () => { describe('addParticipantToMeeting', () => { 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()), + post: vi.fn(() => Promise.resolve()), + get: vi.fn(() => Promise.resolve()), + patch: vi.fn(() => Promise.resolve()), + delete: vi.fn(() => Promise.resolve()), }; beforeEach(() => { diff --git a/app/javascript/dashboard/api/specs/integrations/linear.spec.js b/app/javascript/dashboard/api/specs/integrations/linear.spec.js index cc16feb16..e4bf679a6 100644 --- a/app/javascript/dashboard/api/specs/integrations/linear.spec.js +++ b/app/javascript/dashboard/api/specs/integrations/linear.spec.js @@ -16,10 +16,10 @@ describe('#linearAPI', () => { describe('getTeams', () => { 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()), + post: vi.fn(() => Promise.resolve()), + get: vi.fn(() => Promise.resolve()), + patch: vi.fn(() => Promise.resolve()), + delete: vi.fn(() => Promise.resolve()), }; beforeEach(() => { @@ -41,10 +41,10 @@ describe('#linearAPI', () => { describe('getTeamEntities', () => { 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()), + post: vi.fn(() => Promise.resolve()), + get: vi.fn(() => Promise.resolve()), + patch: vi.fn(() => Promise.resolve()), + delete: vi.fn(() => Promise.resolve()), }; beforeEach(() => { @@ -66,10 +66,10 @@ describe('#linearAPI', () => { describe('createIssue', () => { 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()), + post: vi.fn(() => Promise.resolve()), + get: vi.fn(() => Promise.resolve()), + patch: vi.fn(() => Promise.resolve()), + delete: vi.fn(() => Promise.resolve()), }; beforeEach(() => { @@ -96,10 +96,10 @@ describe('#linearAPI', () => { describe('link_issue', () => { 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()), + post: vi.fn(() => Promise.resolve()), + get: vi.fn(() => Promise.resolve()), + patch: vi.fn(() => Promise.resolve()), + delete: vi.fn(() => Promise.resolve()), }; beforeEach(() => { @@ -125,10 +125,10 @@ describe('#linearAPI', () => { describe('getLinkedIssue', () => { 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()), + post: vi.fn(() => Promise.resolve()), + get: vi.fn(() => Promise.resolve()), + patch: vi.fn(() => Promise.resolve()), + delete: vi.fn(() => Promise.resolve()), }; beforeEach(() => { @@ -150,10 +150,10 @@ describe('#linearAPI', () => { describe('unlinkIssue', () => { 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()), + post: vi.fn(() => Promise.resolve()), + get: vi.fn(() => Promise.resolve()), + patch: vi.fn(() => Promise.resolve()), + delete: vi.fn(() => Promise.resolve()), }; beforeEach(() => { @@ -178,10 +178,10 @@ describe('#linearAPI', () => { describe('searchIssues', () => { 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()), + post: vi.fn(() => Promise.resolve()), + get: vi.fn(() => Promise.resolve()), + patch: vi.fn(() => Promise.resolve()), + delete: vi.fn(() => Promise.resolve()), }; beforeEach(() => { diff --git a/app/javascript/dashboard/api/specs/notifications.spec.js b/app/javascript/dashboard/api/specs/notifications.spec.js index fe748fe19..770a6840d 100644 --- a/app/javascript/dashboard/api/specs/notifications.spec.js +++ b/app/javascript/dashboard/api/specs/notifications.spec.js @@ -13,10 +13,10 @@ describe('#NotificationAPI', () => { 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()), + post: vi.fn(() => Promise.resolve()), + get: vi.fn(() => Promise.resolve()), + patch: vi.fn(() => Promise.resolve()), + delete: vi.fn(() => Promise.resolve()), }; beforeEach(() => { diff --git a/app/javascript/dashboard/api/specs/reports.spec.js b/app/javascript/dashboard/api/specs/reports.spec.js index 05d4a152c..e458633d0 100644 --- a/app/javascript/dashboard/api/specs/reports.spec.js +++ b/app/javascript/dashboard/api/specs/reports.spec.js @@ -20,10 +20,10 @@ describe('#Reports API', () => { 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()), + post: vi.fn(() => Promise.resolve()), + get: vi.fn(() => Promise.resolve()), + patch: vi.fn(() => Promise.resolve()), + delete: vi.fn(() => Promise.resolve()), }; beforeEach(() => { diff --git a/app/javascript/dashboard/api/specs/slaReports.spec.js b/app/javascript/dashboard/api/specs/slaReports.spec.js index f540b6acc..827b44cad 100644 --- a/app/javascript/dashboard/api/specs/slaReports.spec.js +++ b/app/javascript/dashboard/api/specs/slaReports.spec.js @@ -12,10 +12,10 @@ describe('#SLAReports API', () => { 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()), + post: vi.fn(() => Promise.resolve()), + get: vi.fn(() => Promise.resolve()), + patch: vi.fn(() => Promise.resolve()), + delete: vi.fn(() => Promise.resolve()), }; beforeEach(() => { diff --git a/app/javascript/dashboard/api/specs/teams.spec.js b/app/javascript/dashboard/api/specs/teams.spec.js index 3a59f2c51..c7bfc4d1c 100644 --- a/app/javascript/dashboard/api/specs/teams.spec.js +++ b/app/javascript/dashboard/api/specs/teams.spec.js @@ -16,10 +16,10 @@ describe('#TeamsAPI', () => { 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()), + post: vi.fn(() => Promise.resolve()), + get: vi.fn(() => Promise.resolve()), + patch: vi.fn(() => Promise.resolve()), + delete: vi.fn(() => Promise.resolve()), }; beforeEach(() => { diff --git a/app/javascript/dashboard/components/layout/sidebarComponents/specs/AccountSelector.spec.js b/app/javascript/dashboard/components/layout/sidebarComponents/specs/AccountSelector.spec.js index b10ba9a57..fb76e0ff4 100644 --- a/app/javascript/dashboard/components/layout/sidebarComponents/specs/AccountSelector.spec.js +++ b/app/javascript/dashboard/components/layout/sidebarComponents/specs/AccountSelector.spec.js @@ -1,12 +1,12 @@ -import AccountSelector from '../AccountSelector'; +import AccountSelector from '../AccountSelector.vue'; import { createLocalVue, mount } from '@vue/test-utils'; import Vuex from 'vuex'; import VueI18n from 'vue-i18n'; import i18n from 'dashboard/i18n'; -import WootModal from 'dashboard/components/Modal'; -import WootModalHeader from 'dashboard/components/ModalHeader'; -import FluentIcon from 'shared/components/FluentIcon/DashboardIcon'; +import WootModal from 'dashboard/components/Modal.vue'; +import WootModalHeader from 'dashboard/components/ModalHeader.vue'; +import FluentIcon from 'shared/components/FluentIcon/DashboardIcon.vue'; const localVue = createLocalVue(); localVue.component('woot-modal', WootModal); diff --git a/app/javascript/dashboard/components/layout/sidebarComponents/specs/AgentDetails.spec.js b/app/javascript/dashboard/components/layout/sidebarComponents/specs/AgentDetails.spec.js index 073881a58..ca6c00a43 100644 --- a/app/javascript/dashboard/components/layout/sidebarComponents/specs/AgentDetails.spec.js +++ b/app/javascript/dashboard/components/layout/sidebarComponents/specs/AgentDetails.spec.js @@ -1,12 +1,12 @@ -import AgentDetails from '../AgentDetails'; +import AgentDetails from '../AgentDetails.vue'; import { createLocalVue, shallowMount } from '@vue/test-utils'; import Vuex from 'vuex'; import VueI18n from 'vue-i18n'; import VTooltip from 'v-tooltip'; import i18n from 'dashboard/i18n'; -import Thumbnail from 'dashboard/components/widgets/Thumbnail'; -import WootButton from 'dashboard/components/ui/WootButton'; +import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue'; +import WootButton from 'dashboard/components/ui/WootButton.vue'; const localVue = createLocalVue(); localVue.use(Vuex); localVue.use(VueI18n); diff --git a/app/javascript/dashboard/components/layout/sidebarComponents/specs/NotificationBell.spec.js b/app/javascript/dashboard/components/layout/sidebarComponents/specs/NotificationBell.spec.js index 64d1fe791..85596bb3e 100644 --- a/app/javascript/dashboard/components/layout/sidebarComponents/specs/NotificationBell.spec.js +++ b/app/javascript/dashboard/components/layout/sidebarComponents/specs/NotificationBell.spec.js @@ -1,13 +1,15 @@ -import NotificationBell from '../NotificationBell'; +import NotificationBell from '../NotificationBell.vue'; import { createLocalVue, shallowMount } from '@vue/test-utils'; import Vuex from 'vuex'; import VueI18n from 'vue-i18n'; +import FluentIcon from 'shared/components/FluentIcon/DashboardIcon.vue'; import i18n from 'dashboard/i18n'; const localVue = createLocalVue(); localVue.use(Vuex); localVue.use(VueI18n); +localVue.component('fluent-icon', FluentIcon); const i18nConfig = new VueI18n({ locale: 'en', @@ -27,7 +29,7 @@ describe('notificationBell', () => { beforeEach(() => { actions = { - showNotification: jest.fn(), + showNotification: vi.fn(), }; modules = { auth: { diff --git a/app/javascript/dashboard/components/layout/specs/AvailabilityStatus.spec.js b/app/javascript/dashboard/components/layout/specs/AvailabilityStatus.spec.js index 50d959b33..e6864d052 100644 --- a/app/javascript/dashboard/components/layout/specs/AvailabilityStatus.spec.js +++ b/app/javascript/dashboard/components/layout/specs/AvailabilityStatus.spec.js @@ -2,15 +2,21 @@ import AvailabilityStatus from '../AvailabilityStatus.vue'; import { createLocalVue, mount } from '@vue/test-utils'; import Vuex from 'vuex'; import VueI18n from 'vue-i18n'; +import VTooltip from 'v-tooltip'; + +import WootButton from 'dashboard/components/ui/WootButton.vue'; +import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem.vue'; +import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu.vue'; +import WootDropdownHeader from 'shared/components/ui/dropdown/DropdownHeader.vue'; +import WootDropdownDivider from 'shared/components/ui/dropdown/DropdownDivider.vue'; +import FluentIcon from 'shared/components/FluentIcon/DashboardIcon.vue'; -import WootButton from 'dashboard/components/ui/WootButton'; -import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem'; -import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu'; -import WootDropdownHeader from 'shared/components/ui/dropdown/DropdownHeader'; -import WootDropdownDivider from 'shared/components/ui/dropdown/DropdownDivider'; import i18n from 'dashboard/i18n'; const localVue = createLocalVue(); +localVue.use(VTooltip, { + defaultHtml: false, +}); localVue.use(Vuex); localVue.use(VueI18n); localVue.component('woot-button', WootButton); @@ -18,12 +24,14 @@ localVue.component('woot-dropdown-header', WootDropdownHeader); localVue.component('woot-dropdown-menu', WootDropdownMenu); localVue.component('woot-dropdown-divider', WootDropdownDivider); localVue.component('woot-dropdown-item', WootDropdownItem); +localVue.component('fluent-icon', FluentIcon); const i18nConfig = new VueI18n({ locale: 'en', messages: i18n }); describe('AvailabilityStatus', () => { const currentAvailability = 'online'; const currentAccountId = '1'; + const currentUserAutoOffline = false; let store = null; let actions = null; let modules = null; @@ -31,7 +39,7 @@ describe('AvailabilityStatus', () => { beforeEach(() => { actions = { - updateAvailability: jest.fn(() => { + updateAvailability: vi.fn(() => { return Promise.resolve(); }), }; @@ -41,6 +49,7 @@ describe('AvailabilityStatus', () => { getters: { getCurrentUserAvailability: () => currentAvailability, getCurrentAccountId: () => currentAccountId, + getCurrentUserAutoOffline: () => currentUserAutoOffline, }, }, }; diff --git a/app/javascript/dashboard/components/specs/SidemenuIcon.spec.js b/app/javascript/dashboard/components/specs/SidemenuIcon.spec.js index fea394572..5029eed48 100644 --- a/app/javascript/dashboard/components/specs/SidemenuIcon.spec.js +++ b/app/javascript/dashboard/components/specs/SidemenuIcon.spec.js @@ -1,5 +1,5 @@ import { shallowMount } from '@vue/test-utils'; -import SidemenuIcon from '../SidemenuIcon'; +import SidemenuIcon from '../SidemenuIcon.vue'; describe('SidemenuIcon', () => { test('matches snapshot', () => { diff --git a/app/javascript/dashboard/components/specs/__snapshots__/SidemenuIcon.spec.js.snap b/app/javascript/dashboard/components/specs/__snapshots__/SidemenuIcon.spec.js.snap index 577dbb3aa..0229af06e 100644 --- a/app/javascript/dashboard/components/specs/__snapshots__/SidemenuIcon.spec.js.snap +++ b/app/javascript/dashboard/components/specs/__snapshots__/SidemenuIcon.spec.js.snap @@ -1,6 +1,6 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`SidemenuIcon matches snapshot 1`] = ` +exports[`SidemenuIcon > matches snapshot 1`] = `